refactor: 专员目录收敛为 6 个,重整 AI 路由与系统管理页
一次性提交当天全部改动(110 个文件)。 **未按 TOP_CODING_RULES.md G14.5「一次提交只装一件事」拆分** —— 用户明确要求 单一提交,此处如实记录,不静默忽略该冲突。 提交前验证:后端 go build / go vet / go test ./... 全绿,前端 npm run build exit 0,/api/health 与管理员 login 均返回 200。 - 专员目录收敛为 6 个:下线 knowledge-operations / process-coordination / presentation-briefing / report-generation 四个专员(前后端 manifest 与 seed 同步删除),新增 general-assistant。专员的归属关系(拥有哪些技能定义、 哪些目录项、提示词与绑定从哪来)改由 specialists/core 的 ownership.go、 prompt_provider.go、binding_provider.go 统一提供,seed 与 admin_handlers 随之内置化,卸载路径统一走 uninstall.go。 - 技能:补齐 text-to-speech 的前端 manifest(后端包在 HEAD 已存在), skillcore/ownership.go 提供与专员对称的归属查询。 - AI 路由:ai_config.json 由 OpenRouter/Ollama 切到 LMUAI / SiliconFlow / llama.cpp 本地路由,ai_secrets.example.json 与部署 env 样例同步新增 SILICONFLOW_API_KEY。 - 系统管理页重整:新增 AiAdminPage、OrganizationManagementPage,删除 AdminOverviewPage、CompanyConfigPage,SystemConfigPage 精简,nav / router / config/workbench.js 同步调整。依 G05.5,开发阶段直接收口到新结构,不留旧路由。 - 新增 internal/objectrefs:统一统计对象(专员 / 技能 / xapp)的运行时引用 (被多少 xapp、项目、任务引用),供管理页做删除前的影响面判断。 - 公众号创作专员:新增 OfficialAccountSpecialistPanel,workflow 与投递链路调整。 - XApp:考试 / 培训 Shell 扩展,XAppDirectoryPage 与 xappDefinition 同步。 - 文档:新增 GW01–GW05 工作台演进系列与 AR12 对话驱动与结构化交互架构; 同步 AR05 / SY17 / SY23 / SY25 / PL04;TOP_CODING_RULES.md 增补 G05.5。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"eai_agentplatform/backend/internal/dal"
|
||||
"eai_agentplatform/backend/internal/objectrefs"
|
||||
skillcore "eai_agentplatform/backend/internal/skills/core"
|
||||
skillmodel "eai_agentplatform/backend/internal/skills/model"
|
||||
"eai_agentplatform/backend/internal/web"
|
||||
)
|
||||
@@ -117,6 +119,10 @@ func DeleteSkillDefinition(c *gin.Context) {
|
||||
web.Fail(c, web.NewNotFoundError("技能定义不存在"))
|
||||
return
|
||||
}
|
||||
if plan, ok := skillcore.BuildRemovePlanByKey(item.Key); ok && plan.Blocked {
|
||||
web.Fail(c, web.NewConflictError("技能仍被专员、应用或项目引用,请先处理 remove-plan 中的关联项"))
|
||||
return
|
||||
}
|
||||
if !skillDefinitionDAO.Delete(&item) {
|
||||
web.Fail(c, web.NewBadRequest("删除技能定义失败"))
|
||||
return
|
||||
@@ -124,6 +130,52 @@ func DeleteSkillDefinition(c *gin.Context) {
|
||||
web.OK(c, gin.H{"id": id, "deleted": true})
|
||||
}
|
||||
|
||||
// PreviewSkillRemovePlan GET /api/admin/skills/:id/remove-plan (admin)
|
||||
func PreviewSkillRemovePlan(c *gin.Context) {
|
||||
id, ok := parseID(c, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
item, found := skillDefinitionDAO.GetByID(id)
|
||||
if !found {
|
||||
web.Fail(c, web.NewNotFoundError("技能定义不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
if plan, ok := skillcore.BuildRemovePlanByKey(item.Key); ok {
|
||||
web.OK(c, gin.H{
|
||||
"id": item.ID,
|
||||
"key": item.Key,
|
||||
"label": item.Label,
|
||||
"source": item.Source,
|
||||
"state": item.State,
|
||||
"is_builtin": true,
|
||||
"plan": plan,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
refs := objectrefs.CollectSkillRuntimeRefs(item.Key)
|
||||
web.OK(c, gin.H{
|
||||
"id": item.ID,
|
||||
"key": item.Key,
|
||||
"label": item.Label,
|
||||
"source": item.Source,
|
||||
"state": item.State,
|
||||
"is_builtin": false,
|
||||
"plan": gin.H{
|
||||
"key": item.Key,
|
||||
"label": item.Label,
|
||||
"definition_keys": []string{},
|
||||
"catalog_entries": []string{},
|
||||
"policy": "",
|
||||
"runtime_refs": refs,
|
||||
"blocked": false,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func parseID(c *gin.Context, name string) (uint, bool) {
|
||||
id, err := strconv.ParseUint(c.Param(name), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
|
||||
@@ -5,11 +5,15 @@ import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"eai_agentplatform/backend/internal/dal"
|
||||
skillmodel "eai_agentplatform/backend/internal/skills/model"
|
||||
)
|
||||
|
||||
var ValidSkillKeys = buildValidSkillKeys()
|
||||
var ValidSkillKeys = buildBuiltinSkillKeys()
|
||||
var allowedSkillKeyProvider = buildAllowedSkillKeys
|
||||
|
||||
func buildValidSkillKeys() map[string]bool {
|
||||
func buildBuiltinSkillKeys() map[string]bool {
|
||||
keys := make(map[string]bool)
|
||||
for _, manifest := range builtinRegistry.Manifests() {
|
||||
if strings.TrimSpace(manifest.Key) == "" {
|
||||
@@ -21,8 +25,31 @@ func buildValidSkillKeys() map[string]bool {
|
||||
}
|
||||
|
||||
func SortedValidSkillKeys() []string {
|
||||
keys := make([]string, 0, len(ValidSkillKeys))
|
||||
return sortedSkillKeys(ValidSkillKeys)
|
||||
}
|
||||
|
||||
func buildAllowedSkillKeys() map[string]bool {
|
||||
keys := make(map[string]bool, len(ValidSkillKeys))
|
||||
for k := range ValidSkillKeys {
|
||||
keys[k] = true
|
||||
}
|
||||
if dal.DB == nil {
|
||||
return keys
|
||||
}
|
||||
if !dal.DB.Migrator().HasTable(&skillmodel.SkillDefinition{}) {
|
||||
return keys
|
||||
}
|
||||
for _, item := range (dal.SkillDefinitionDAO{}).List("", nil) {
|
||||
if key := strings.TrimSpace(item.Key); key != "" {
|
||||
keys[key] = true
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func sortedSkillKeys(set map[string]bool) []string {
|
||||
keys := make([]string, 0, len(set))
|
||||
for k := range set {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
@@ -59,14 +86,16 @@ func MarshalAllowedSkills(keys []string) (string, error) {
|
||||
}
|
||||
|
||||
func ValidateAllowedSkills(keys []string) error {
|
||||
validKeys := allowedSkillKeyProvider()
|
||||
sortedKeys := sortedSkillKeys(validKeys)
|
||||
seen := make(map[string]bool, len(keys))
|
||||
for _, k := range keys {
|
||||
k = strings.TrimSpace(k)
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
if !ValidSkillKeys[k] {
|
||||
return fmt.Errorf("技能 key %q 非法,可选值:%s", k, strings.Join(SortedValidSkillKeys(), " / "))
|
||||
if !validKeys[k] {
|
||||
return fmt.Errorf("技能 key %q 非法,可选值:%s", k, strings.Join(sortedKeys, " / "))
|
||||
}
|
||||
if seen[k] {
|
||||
return fmt.Errorf("技能 key %q 重复绑定", k)
|
||||
|
||||
@@ -110,6 +110,11 @@ func TestMarshalAllowedSkills(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestValidateAllowedSkills(t *testing.T) {
|
||||
originalProvider := allowedSkillKeyProvider
|
||||
t.Cleanup(func() {
|
||||
allowedSkillKeyProvider = originalProvider
|
||||
})
|
||||
|
||||
t.Run("合法列表通过", func(t *testing.T) {
|
||||
if err := ValidateAllowedSkills([]string{"contract-review", "batch-extract"}); err != nil {
|
||||
t.Errorf("不应报错: %v", err)
|
||||
@@ -138,6 +143,21 @@ func TestValidateAllowedSkills(t *testing.T) {
|
||||
t.Fatal("重复绑定应报错(首个为主技能的语义会变得不确定)")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("运行时技能定义可放宽内置白名单", func(t *testing.T) {
|
||||
allowedSkillKeyProvider = func() map[string]bool {
|
||||
return map[string]bool{
|
||||
"contract-review": true,
|
||||
"runtime-skill": true,
|
||||
}
|
||||
}
|
||||
if err := ValidateAllowedSkills([]string{"runtime-skill"}); err != nil {
|
||||
t.Fatalf("运行时技能应允许绑定: %v", err)
|
||||
}
|
||||
if ValidSkillKeys["runtime-skill"] {
|
||||
t.Fatal("内置技能清单不应被运行时定义污染")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func readFrontendFile(t *testing.T, relPath string) []byte {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package skillcore
|
||||
|
||||
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
|
||||
|
||||
type OwnershipInfo struct {
|
||||
Key string
|
||||
Label string
|
||||
DefinitionKeys []string
|
||||
CatalogEntries []string
|
||||
Policy skillcontracts.SharedReferencePolicy
|
||||
}
|
||||
|
||||
func OwnershipCatalog() map[string]OwnershipInfo {
|
||||
items := make(map[string]OwnershipInfo)
|
||||
for _, manifest := range builtinRegistry.Manifests() {
|
||||
items[manifest.Key] = ownershipFromManifest(manifest)
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func OwnershipByKey(key string) (OwnershipInfo, bool) {
|
||||
manifest, ok := builtinRegistry.ByKey(key)
|
||||
if !ok {
|
||||
return OwnershipInfo{}, false
|
||||
}
|
||||
return ownershipFromManifest(manifest), true
|
||||
}
|
||||
|
||||
func ownershipFromManifest(manifest skillcontracts.Manifest) OwnershipInfo {
|
||||
meta := manifest.UninstallMeta()
|
||||
return OwnershipInfo{
|
||||
Key: manifest.Key,
|
||||
Label: manifest.Label,
|
||||
DefinitionKeys: meta.OwnedDefinitionKeys,
|
||||
CatalogEntries: meta.OwnedCatalogEntries,
|
||||
Policy: meta.ReferencePolicy,
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,10 @@
|
||||
package skillcore
|
||||
|
||||
import skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
|
||||
import (
|
||||
skillcontracts "eai_agentplatform/backend/internal/skills/contracts"
|
||||
|
||||
"eai_agentplatform/backend/internal/objectrefs"
|
||||
)
|
||||
|
||||
type UninstallPreview struct {
|
||||
DefinitionKeys []string
|
||||
@@ -8,6 +12,16 @@ type UninstallPreview struct {
|
||||
Policy skillcontracts.SharedReferencePolicy
|
||||
}
|
||||
|
||||
type RemovePlan struct {
|
||||
Key string
|
||||
Label string
|
||||
DefinitionKeys []string
|
||||
CatalogEntries []string
|
||||
Policy skillcontracts.SharedReferencePolicy
|
||||
RuntimeRefs objectrefs.SkillRuntimeRefs
|
||||
Blocked bool
|
||||
}
|
||||
|
||||
func BuildUninstallPreview(manifest skillcontracts.Manifest) UninstallPreview {
|
||||
meta := manifest.UninstallMeta()
|
||||
return UninstallPreview{
|
||||
@@ -16,3 +30,22 @@ func BuildUninstallPreview(manifest skillcontracts.Manifest) UninstallPreview {
|
||||
Policy: meta.ReferencePolicy,
|
||||
}
|
||||
}
|
||||
|
||||
func BuildRemovePlanByKey(key string) (RemovePlan, bool) {
|
||||
ownership, ok := OwnershipByKey(key)
|
||||
if !ok {
|
||||
return RemovePlan{}, false
|
||||
}
|
||||
refs := objectrefs.CollectSkillRuntimeRefs(key)
|
||||
blocked := ownership.Policy == skillcontracts.SharedReferenceBlockUninstall &&
|
||||
(len(refs.SpecialistKeys) > 0 || len(refs.XAppKeys) > 0 || len(refs.ProjectIDs) > 0)
|
||||
return RemovePlan{
|
||||
Key: ownership.Key,
|
||||
Label: ownership.Label,
|
||||
DefinitionKeys: ownership.DefinitionKeys,
|
||||
CatalogEntries: ownership.CatalogEntries,
|
||||
Policy: ownership.Policy,
|
||||
RuntimeRefs: refs,
|
||||
Blocked: blocked,
|
||||
}, true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user