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:
eaiadmin
2026-09-23 09:01:28 +08:00
co-authored by Claude Code
parent 7fc2903827
commit 593323a934
110 changed files with 8295 additions and 1715 deletions
@@ -8,6 +8,7 @@ import (
"github.com/gin-gonic/gin"
"eai_agentplatform/backend/internal/dal"
"eai_agentplatform/backend/internal/middleware"
"eai_agentplatform/backend/internal/model"
specialistruntime "eai_agentplatform/backend/internal/specialists/runtime"
@@ -89,6 +90,10 @@ func CreateProject(c *gin.Context) {
web.Fail(c, web.NewNotFoundError(err.Error()))
return
}
if err := validateSkillKeys(req.SkillKeys); err != nil {
web.Fail(c, web.NewNotFoundError(err.Error()))
return
}
instruction := ""
if req.Instruction != nil {
@@ -163,6 +168,10 @@ func UpdateProject(c *gin.Context) {
project.SpecialistKeys = encodeKeys(req.SpecialistKeys)
}
if req.SkillKeys != nil {
if err := validateSkillKeys(req.SkillKeys); err != nil {
web.Fail(c, web.NewNotFoundError(err.Error()))
return
}
project.SkillKeys = encodeKeys(req.SkillKeys)
}
if req.ConnectorKeys != nil {
@@ -246,3 +255,17 @@ func validateSpecialistKeys(keys []string) error {
}
return nil
}
func validateSkillKeys(keys []string) error {
dao := dal.SkillDefinitionDAO{}
for _, key := range keys {
trimmed := strings.TrimSpace(key)
if trimmed == "" {
continue
}
if _, found := dao.GetByKey(trimmed); !found {
return errors.New("技能不存在:" + trimmed)
}
}
return nil
}
@@ -11,8 +11,8 @@ import (
"eai_agentplatform/backend/internal/middleware"
skillapi "eai_agentplatform/backend/internal/skills/api"
specialistapi "eai_agentplatform/backend/internal/specialists/api"
"eai_agentplatform/backend/internal/specialists/runtime"
wechatofficialaccountapi "eai_agentplatform/backend/internal/specialists/packages/wechat_official_account/api"
"eai_agentplatform/backend/internal/specialists/runtime"
xappapi "eai_agentplatform/backend/internal/xapps/api"
)
@@ -206,12 +206,15 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) {
admin.DELETE("/positions/:id", DeletePosition)
admin.POST("/specialists", specialistapi.CreateSpecialist)
admin.PUT("/specialists/:id", specialistapi.UpdateSpecialist)
admin.GET("/specialists/:id/remove-plan", specialistapi.PreviewSpecialistRemovePlan)
admin.DELETE("/specialists/:id", specialistapi.DeleteSpecialist)
admin.POST("/skills", skillapi.CreateSkillDefinition)
admin.PUT("/skills/:id", skillapi.UpdateSkillDefinition)
admin.GET("/skills/:id/remove-plan", skillapi.PreviewSkillRemovePlan)
admin.DELETE("/skills/:id", skillapi.DeleteSkillDefinition)
admin.POST("/xapps", xappapi.CreateXAppDefinition)
admin.PUT("/xapps/:id", xappapi.UpdateXAppDefinition)
admin.GET("/xapps/:id/remove-plan", xappapi.PreviewXAppRemovePlan)
admin.DELETE("/xapps/:id", xappapi.DeleteXAppDefinition)
admin.POST("/actions", CreateActionDefinition)
admin.PUT("/actions/:id", UpdateActionDefinition)
@@ -4,7 +4,7 @@ import (
"fmt"
"strings"
skillcore "eai_agentplatform/backend/internal/skills/core"
specialistcore "eai_agentplatform/backend/internal/specialists/core"
specialistmodel "eai_agentplatform/backend/internal/specialists/model"
)
@@ -63,7 +63,8 @@ func resolveSpecialist(req ChatMessageRequest) *specialistmodel.Specialist {
// 专员为 nil 或什么都没配时返回空串,调用方不做任何拼接——
// 这样「没选专员」与「选了但没配说明书」都不改变原有行为。
func specialistPromptSection(s *specialistmodel.Specialist) string {
if s == nil {
ctx := specialistcore.PromptContextFor(s)
if ctx.Key == "" {
return ""
}
@@ -71,27 +72,27 @@ func specialistPromptSection(s *specialistmodel.Specialist) string {
// 身份
b.WriteString("【当前专员】\n")
label := strings.TrimSpace(s.Label)
label := ctx.Label
if label == "" {
label = s.Key
label = ctx.Key
}
fmt.Fprintf(&b, "%s(%s)\n", label, s.Key)
if summary := strings.TrimSpace(s.Summary); summary != "" {
fmt.Fprintf(&b, "%s(%s)\n", label, ctx.Key)
if summary := ctx.Summary; summary != "" {
b.WriteString(summary + "\n")
}
if workStatus := strings.TrimSpace(s.WorkStatus); workStatus != "" {
if workStatus := ctx.WorkStatus; workStatus != "" {
b.WriteString("当前状态:" + workStatus + "\n")
}
// 可用技能
if skills := skillcore.ParseAllowedSkills(s.AllowedSkills); len(skills) > 0 {
if len(ctx.AllowedSkills) > 0 {
b.WriteString("\n【可用技能】\n")
b.WriteString(strings.Join(skills, "、"))
b.WriteString(strings.Join(ctx.AllowedSkills, "、"))
b.WriteString("\n首个是主技能。只在这些技能范围内承诺能力,范围外的需求如实说做不了。\n")
}
// 岗位说明书
if rule := strings.TrimSpace(s.RuleFileMarkdown); rule != "" {
if rule := ctx.RuleFileMarkdown; rule != "" {
b.WriteString("\n【岗位说明书】\n")
b.WriteString(rule)
b.WriteString("\n")
@@ -133,7 +133,7 @@ func setupAPITestDB(t *testing.T) {
for _, s := range []specialistmodel.Specialist{
{Key: "contract-review", Label: "合同审查专员", State: "active", Tier: "industry", ObjectEntryRoute: "/apps/contract-review"},
{Key: "report-generation", Label: "报告生成专员", State: "active", Tier: "generic", ObjectEntryRoute: "/apps/report-generation"},
{Key: "wechat-official-account", Label: "公众号创作专员", State: "active", Tier: "generic", ObjectEntryRoute: "/apps/wechat-official-account"},
{Key: "retired-one", Label: "已下线专员", State: "inactive", Tier: "generic", ObjectEntryRoute: "/apps/retired"},
{Key: "general-assistant", Label: "通用助手", State: "system", Tier: "generic", ObjectEntryRoute: "/home"},
} {
@@ -168,7 +168,7 @@ func TestResolveSpecialistTaskWinsOverRequest(t *testing.T) {
t.Fatalf("建任务失败: %v", err)
}
got := resolveSpecialist(ChatMessageRequest{TaskID: task.ID, SpecialistKey: "report-generation"})
got := resolveSpecialist(ChatMessageRequest{TaskID: task.ID, SpecialistKey: "wechat-official-account"})
if got == nil || got.Key != "contract-review" {
t.Fatalf("任务上的专员应优先,实际 %v", got)
}
@@ -178,8 +178,8 @@ func TestResolveSpecialistTaskWinsOverRequest(t *testing.T) {
func TestResolveSpecialistFallsBackToRequest(t *testing.T) {
setupAPITestDB(t)
got := resolveSpecialist(ChatMessageRequest{SpecialistKey: "report-generation"})
if got == nil || got.Key != "report-generation" {
got := resolveSpecialist(ChatMessageRequest{SpecialistKey: "wechat-official-account"})
if got == nil || got.Key != "wechat-official-account" {
t.Fatalf("应回退到请求里的 specialist_key,实际 %v", got)
}
}
@@ -228,8 +228,8 @@ func TestResolveSpecialistTaskWithoutSpecialistFallsBack(t *testing.T) {
t.Fatalf("建任务失败: %v", err)
}
got := resolveSpecialist(ChatMessageRequest{TaskID: task.ID, SpecialistKey: "report-generation"})
if got == nil || got.Key != "report-generation" {
got := resolveSpecialist(ChatMessageRequest{TaskID: task.ID, SpecialistKey: "wechat-official-account"})
if got == nil || got.Key != "wechat-official-account" {
t.Fatalf("任务专员失效时应回退到请求里的 key,实际 %v", got)
}
}