From d0d7b3588cd49b08948ec4a426c704580a8aab29 Mon Sep 17 00:00:00 2001 From: eaiadmin Date: Thu, 17 Sep 2026 23:38:40 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=AF=B9=E8=B1=A1=E5=91=BD?= =?UTF-8?q?=E5=90=8D=E6=A0=87=E5=87=86=E5=8C=96=E8=90=BD=E5=9C=B0=EF=BC=88?= =?UTF-8?q?RoleKind=E2=86=92ObjectKind=E3=80=81capability/assistant=20?= =?UTF-8?q?=E6=94=B6=E5=8F=A3=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 后端 - model.SkillDefinition.RoleKind 改名 ObjectKind(列 role_kind → object_kind); 新增 migrateSkillObjectKindColumn:先拷数据再显式 drop 旧列。 GORM AutoMigrate 只加不删,字段改名不手写迁移就会新旧两列并存。 - Specialist 结构化列更名走同一套 migrateColumnData: role_card_json → interaction_card_json、source_records_json → inputs_records_json - api/capability_definition.go(413 行)拆为 skill_action_definition.go, 不再让 capability 充当一级对象名;该文件同时承载技能与动作定义处理器 - 新增 model/app_definition.go + api/app_definition.go:应用目录定义正式建模 - SkillDefinition 移除已废弃的 LegacyObjectEntryRoute 字段,对应迁移分支一并删除 - skill_keys.go 注释更新:权威来源为前端 staticSkillCatalog 前端 - api/assistant.js → chat.js、api/capability.js → skill.js,新增 api/appDefinition.js - 新增 store/specialistCatalog.js、skillCatalog.js、appCatalog.js, 取代 config/productizedApps.js(已删) - config/workbench.js 静态目录去失真:businessApps → staticSpecialistCatalog (该变量装的其实是专员目录,与应用 app 语义直接冲突)、 availableSkills → staticSkillCatalog - 修复入口 query 协议断裂:写端 appCatalog 用 app_specialist/app_skill/app_prompt, 读端 SmartAssistantPage 读同名参数并挂响应式监听。 此前写读两侧命名不一致且无人读旧名,导致从应用目录打开应用时 预置专员/技能/提示词被静默丢弃、无任何报错 - 页面与组件局部变量去失真:拿到专员对象不再命名为 app 验证:CGO_ENABLED=0 go build ./... 通过;go test ./... 全包通过;npm run build 通过。 旧变量名 businessApps / availableSkills / productizedApps / api/capability / api/assistant 全仓 grep 无残留。 Co-Authored-By: Claude Code --- .../backend-go/internal/api/app_definition.go | 233 +++++++ .../internal/api/capability_definition.go | 413 ------------- .../backend-go/internal/api/router.go | 5 + .../internal/api/skill_action_definition.go | 409 ++++++++++++ .../backend-go/internal/api/specialist.go | 75 ++- .../internal/model/app_definition.go | 38 ++ .../internal/model/skill_definition.go | 45 +- .../backend-go/internal/model/skill_keys.go | 2 +- .../internal/model/skill_keys_test.go | 8 +- .../backend-go/internal/model/specialist.go | 52 +- .../backend-go/internal/store/db.go | 67 +- .../internal/store/db_migration_test.go | 116 +++- .../backend-go/internal/store/seed.go | 580 ++++++++++++++---- .../internal/store/seed_specialist_rules.go | 23 + .../store/seed_specialist_rules_test.go | 2 +- .../internal/store/specialist_records.go | 88 +-- .../frontend/src/api/appDefinition.js | 9 + .../frontend/src/api/capability.js | 5 - .../src/api/{assistant.js => chat.js} | 3 +- eai_agentplatform/frontend/src/api/skill.js | 9 + .../src/components/chat/AppChatRail.vue | 4 +- .../src/components/chat/CurrentObjectChip.vue | 17 +- .../frontend/src/components/chat/PlusMenu.vue | 14 +- .../src/components/chat/SkillStrip.vue | 7 +- .../src/components/chat/SpecialistChip.vue | 11 +- .../src/components/chat/SpecialistPanel.vue | 10 +- .../frontend/src/config/productizedApps.js | 158 ----- .../frontend/src/config/projectTemplates.js | 24 +- .../frontend/src/config/skillSettings.js | 2 +- .../frontend/src/config/workbench.js | 185 +++--- .../frontend/src/layout/MainLayout.vue | 16 +- .../frontend/src/store/appCatalog.js | 159 +++++ .../frontend/src/store/appCenter.js | 13 +- .../frontend/src/store/skillCatalog.js | 147 +++++ .../frontend/src/store/specialistCatalog.js | 83 +++ .../frontend/src/store/workerRuntime.js | 1 - .../workbench/CapabilityCatalogDetailPage.vue | 103 +--- .../views/workbench/CapabilityCatalogPage.vue | 122 +--- .../views/workbench/DocumentCenterPage.vue | 17 +- .../src/views/workbench/MarketPage.vue | 10 +- .../views/workbench/SmartAssistantPage.vue | 117 ++-- 41 files changed, 2180 insertions(+), 1222 deletions(-) create mode 100644 eai_agentplatform/backend-go/internal/api/app_definition.go delete mode 100644 eai_agentplatform/backend-go/internal/api/capability_definition.go create mode 100644 eai_agentplatform/backend-go/internal/api/skill_action_definition.go create mode 100644 eai_agentplatform/backend-go/internal/model/app_definition.go create mode 100644 eai_agentplatform/frontend/src/api/appDefinition.js delete mode 100644 eai_agentplatform/frontend/src/api/capability.js rename eai_agentplatform/frontend/src/api/{assistant.js => chat.js} (59%) create mode 100644 eai_agentplatform/frontend/src/api/skill.js delete mode 100644 eai_agentplatform/frontend/src/config/productizedApps.js create mode 100644 eai_agentplatform/frontend/src/store/appCatalog.js create mode 100644 eai_agentplatform/frontend/src/store/skillCatalog.js create mode 100644 eai_agentplatform/frontend/src/store/specialistCatalog.js diff --git a/eai_agentplatform/backend-go/internal/api/app_definition.go b/eai_agentplatform/backend-go/internal/api/app_definition.go new file mode 100644 index 0000000..2cb6408 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/api/app_definition.go @@ -0,0 +1,233 @@ +package api + +import ( + "strings" + + "github.com/gin-gonic/gin" + + "eai_agentplatform/backend/internal/model" + "eai_agentplatform/backend/internal/store" + "eai_agentplatform/backend/internal/web" +) + +type appDefinitionReq struct { + Key string `json:"key"` + Label string `json:"label"` + Badge string `json:"badge"` + Kind string `json:"kind"` + MarketTag string `json:"market_tag"` + WorkerType string `json:"worker_type"` + Tier string `json:"tier"` + Source string `json:"source"` + Color string `json:"color"` + IconText string `json:"icon_text"` + CoverTone string `json:"cover_tone"` + Summary string `json:"summary"` + Description string `json:"description"` + OpenRoute string `json:"open_route"` + SpecialistKey string `json:"specialist_key"` + SkillKey string `json:"skill_key"` + DefaultPrompt string `json:"default_prompt"` + PromptsJSON string `json:"prompts_json"` + TagsJSON string `json:"tags_json"` + InstallState string `json:"install_state"` + ExposedToUser bool `json:"exposed_to_user"` + State string `json:"state"` + SortOrder int `json:"sort_order"` +} + +func normalizeAppDefinitionReq(req *appDefinitionReq) { + req.Key = strings.TrimSpace(req.Key) + req.Label = strings.TrimSpace(req.Label) + req.Badge = strings.TrimSpace(req.Badge) + req.Kind = strings.TrimSpace(req.Kind) + req.MarketTag = strings.TrimSpace(req.MarketTag) + req.WorkerType = strings.TrimSpace(req.WorkerType) + req.Tier = strings.TrimSpace(req.Tier) + req.Source = strings.TrimSpace(req.Source) + req.Color = strings.TrimSpace(req.Color) + req.IconText = strings.TrimSpace(req.IconText) + req.CoverTone = strings.TrimSpace(req.CoverTone) + req.Summary = strings.TrimSpace(req.Summary) + req.Description = strings.TrimSpace(req.Description) + req.OpenRoute = strings.TrimSpace(req.OpenRoute) + req.SpecialistKey = strings.TrimSpace(req.SpecialistKey) + req.SkillKey = strings.TrimSpace(req.SkillKey) + req.DefaultPrompt = strings.TrimSpace(req.DefaultPrompt) + req.PromptsJSON = strings.TrimSpace(req.PromptsJSON) + req.TagsJSON = strings.TrimSpace(req.TagsJSON) + req.InstallState = strings.TrimSpace(req.InstallState) + req.State = strings.TrimSpace(req.State) +} + +func validateAppDefinitionReq(req *appDefinitionReq) *web.AppError { + normalizeAppDefinitionReq(req) + if req.Key == "" || req.Label == "" { + return web.NewBadRequest("key、label 为必填") + } + if req.WorkerType == "" { + req.WorkerType = "worker" + } + if req.Tier == "" { + req.Tier = "business" + } + if req.Source == "" { + req.Source = "eai" + } + if req.InstallState == "" { + req.InstallState = "installed" + } + if req.State == "" { + req.State = "active" + } + if req.State != "active" && req.State != "inactive" { + return web.NewBadRequest("state 只能是 active 或 inactive") + } + if !store.ValidateJSONStringArray(req.PromptsJSON) { + return web.NewBadRequest("prompts_json 必须是字符串数组") + } + if !store.ValidateJSONStringArray(req.TagsJSON) { + return web.NewBadRequest("tags_json 必须是字符串数组") + } + return nil +} + +func ListAppDefinitions(c *gin.Context) { + q := store.DB.Model(&model.AppDefinition{}) + if c.Query("state") == "" { + q = q.Where("state = ?", "active") + } else { + q = q.Where("state = ?", c.Query("state")) + } + if c.Query("exposed_to_user") != "" { + q = q.Where("exposed_to_user = ?", c.Query("exposed_to_user") == "true") + } + var items []model.AppDefinition + if err := q.Order("sort_order ASC, id ASC").Find(&items).Error; err != nil { + web.Fail(c, web.NewBadRequest("查询应用定义失败")) + return + } + web.OK(c, items) +} + +func GetAppDefinitionByKey(c *gin.Context) { + key := strings.TrimSpace(c.Param("key")) + if key == "" { + web.Fail(c, web.NewBadRequest("应用 key 不能为空")) + return + } + var item model.AppDefinition + if err := store.DB.Where("key = ?", key).First(&item).Error; err != nil { + web.Fail(c, web.NewNotFoundError("应用定义不存在")) + return + } + web.OK(c, item) +} + +func CreateAppDefinition(c *gin.Context) { + var req appDefinitionReq + if err := c.ShouldBindJSON(&req); err != nil { + web.Fail(c, web.NewBadRequest("请求参数错误")) + return + } + if appErr := validateAppDefinitionReq(&req); appErr != nil { + web.Fail(c, appErr) + return + } + item := model.AppDefinition{ + Key: req.Key, + Label: req.Label, + Badge: req.Badge, + Kind: req.Kind, + MarketTag: req.MarketTag, + WorkerType: req.WorkerType, + Tier: req.Tier, + Source: req.Source, + Color: req.Color, + IconText: req.IconText, + CoverTone: req.CoverTone, + Summary: req.Summary, + Description: req.Description, + OpenRoute: req.OpenRoute, + SpecialistKey: req.SpecialistKey, + SkillKey: req.SkillKey, + DefaultPrompt: req.DefaultPrompt, + PromptsJSON: req.PromptsJSON, + TagsJSON: req.TagsJSON, + InstallState: req.InstallState, + ExposedToUser: req.ExposedToUser, + State: req.State, + SortOrder: req.SortOrder, + } + if err := store.DB.Create(&item).Error; err != nil { + web.Fail(c, web.NewBadRequest("创建应用定义失败")) + return + } + web.OK(c, item) +} + +func UpdateAppDefinition(c *gin.Context) { + id, ok := parseID(c, "id") + if !ok { + return + } + var item model.AppDefinition + if err := store.DB.First(&item, id).Error; err != nil { + web.Fail(c, web.NewNotFoundError("应用定义不存在")) + return + } + var req appDefinitionReq + if err := c.ShouldBindJSON(&req); err != nil { + web.Fail(c, web.NewBadRequest("请求参数错误")) + return + } + if appErr := validateAppDefinitionReq(&req); appErr != nil { + web.Fail(c, appErr) + return + } + item.Key = req.Key + item.Label = req.Label + item.Badge = req.Badge + item.Kind = req.Kind + item.MarketTag = req.MarketTag + item.WorkerType = req.WorkerType + item.Tier = req.Tier + item.Source = req.Source + item.Color = req.Color + item.IconText = req.IconText + item.CoverTone = req.CoverTone + item.Summary = req.Summary + item.Description = req.Description + item.OpenRoute = req.OpenRoute + item.SpecialistKey = req.SpecialistKey + item.SkillKey = req.SkillKey + item.DefaultPrompt = req.DefaultPrompt + item.PromptsJSON = req.PromptsJSON + item.TagsJSON = req.TagsJSON + item.InstallState = req.InstallState + item.ExposedToUser = req.ExposedToUser + item.State = req.State + item.SortOrder = req.SortOrder + if err := store.DB.Save(&item).Error; err != nil { + web.Fail(c, web.NewBadRequest("更新应用定义失败")) + return + } + web.OK(c, item) +} + +func DeleteAppDefinition(c *gin.Context) { + id, ok := parseID(c, "id") + if !ok { + return + } + var item model.AppDefinition + if err := store.DB.First(&item, id).Error; err != nil { + web.Fail(c, web.NewNotFoundError("应用定义不存在")) + return + } + if err := store.DB.Delete(&item).Error; err != nil { + web.Fail(c, web.NewBadRequest("删除应用定义失败")) + return + } + web.OK(c, gin.H{"id": id, "deleted": true}) +} diff --git a/eai_agentplatform/backend-go/internal/api/capability_definition.go b/eai_agentplatform/backend-go/internal/api/capability_definition.go deleted file mode 100644 index e3221aa..0000000 --- a/eai_agentplatform/backend-go/internal/api/capability_definition.go +++ /dev/null @@ -1,413 +0,0 @@ -package api - -import ( - "strings" - - "github.com/gin-gonic/gin" - - "eai_agentplatform/backend/internal/model" - "eai_agentplatform/backend/internal/store" - "eai_agentplatform/backend/internal/web" -) - -type skillDefinitionReq struct { - Key string `json:"key"` - Label string `json:"label"` - Description string `json:"description"` - RoleKind string `json:"role_kind"` - Source string `json:"source"` - ObjectEntryRoute string `json:"object_entry_route"` - LegacyObjectEntryRoute string `json:"legacy_object_entry_route"` - ExposedToUser bool `json:"exposed_to_user"` - StarterPromptsJSON string `json:"starter_prompts_json"` - PromptTemplate string `json:"prompt_template"` - InputSchemaJSON string `json:"input_schema_json"` - OutputSchemaJSON string `json:"output_schema_json"` - ArtifactSchemaJSON string `json:"artifact_schema_json"` - ActionRefsJSON string `json:"action_refs_json"` - PolicyRefsJSON string `json:"policy_refs_json"` - OntologyBindingJSON string `json:"ontology_binding_json"` - State string `json:"state"` - SortOrder int `json:"sort_order"` -} - -type actionDefinitionReq struct { - Key string `json:"key"` - Label string `json:"label"` - Description string `json:"description"` - ActionType string `json:"action_type"` - ConnectorRef string `json:"connector_ref"` - InputSchemaJSON string `json:"input_schema_json"` - OutputSchemaJSON string `json:"output_schema_json"` - RiskLevel string `json:"risk_level"` - ApprovalMode string `json:"approval_mode"` - AuditLevel string `json:"audit_level"` - ExposedToUser bool `json:"exposed_to_user"` - OntologyBindingJSON string `json:"ontology_binding_json"` - State string `json:"state"` - SortOrder int `json:"sort_order"` -} - -func normalizeSkillDefinitionReq(req *skillDefinitionReq) { - req.Key = strings.TrimSpace(req.Key) - req.Label = strings.TrimSpace(req.Label) - req.Description = strings.TrimSpace(req.Description) - req.RoleKind = strings.TrimSpace(req.RoleKind) - req.Source = strings.TrimSpace(req.Source) - req.ObjectEntryRoute = strings.TrimSpace(req.ObjectEntryRoute) - req.LegacyObjectEntryRoute = strings.TrimSpace(req.LegacyObjectEntryRoute) - req.StarterPromptsJSON = strings.TrimSpace(req.StarterPromptsJSON) - req.PromptTemplate = strings.TrimSpace(req.PromptTemplate) - req.InputSchemaJSON = strings.TrimSpace(req.InputSchemaJSON) - req.OutputSchemaJSON = strings.TrimSpace(req.OutputSchemaJSON) - req.ArtifactSchemaJSON = strings.TrimSpace(req.ArtifactSchemaJSON) - req.ActionRefsJSON = strings.TrimSpace(req.ActionRefsJSON) - req.PolicyRefsJSON = strings.TrimSpace(req.PolicyRefsJSON) - req.OntologyBindingJSON = strings.TrimSpace(req.OntologyBindingJSON) - req.State = strings.TrimSpace(req.State) -} - -func validateSkillDefinitionReq(req *skillDefinitionReq) *web.AppError { - normalizeSkillDefinitionReq(req) - if req.Key == "" || req.Label == "" { - return web.NewBadRequest("key、label 为必填") - } - if req.RoleKind == "" { - req.RoleKind = "skill" - } - switch req.RoleKind { - case "assistant", "specialist", "skill": - default: - return web.NewBadRequest("role_kind 只能是 assistant、specialist 或 skill") - } - if req.Source == "" { - req.Source = "eai" - } - if req.State == "" { - req.State = "active" - } - if req.State != "active" && req.State != "inactive" { - return web.NewBadRequest("state 只能是 active 或 inactive") - } - if !store.ValidateJSONStringArray(req.StarterPromptsJSON) { - return web.NewBadRequest("starter_prompts_json 必须是字符串数组") - } - if !store.ValidateJSONObjectJSON(req.InputSchemaJSON) { - return web.NewBadRequest("input_schema_json 必须是 JSON 对象") - } - if !store.ValidateJSONObjectJSON(req.OutputSchemaJSON) { - return web.NewBadRequest("output_schema_json 必须是 JSON 对象") - } - if !store.ValidateJSONObjectJSON(req.ArtifactSchemaJSON) { - return web.NewBadRequest("artifact_schema_json 必须是 JSON 对象") - } - if !store.ValidateJSONStringArray(req.ActionRefsJSON) { - return web.NewBadRequest("action_refs_json 必须是字符串数组") - } - if !store.ValidateJSONStringArray(req.PolicyRefsJSON) { - return web.NewBadRequest("policy_refs_json 必须是字符串数组") - } - if !store.ValidateJSONObjectJSON(req.OntologyBindingJSON) { - return web.NewBadRequest("ontology_binding_json 必须是 JSON 对象") - } - return nil -} - -func normalizeActionDefinitionReq(req *actionDefinitionReq) { - req.Key = strings.TrimSpace(req.Key) - req.Label = strings.TrimSpace(req.Label) - req.Description = strings.TrimSpace(req.Description) - req.ActionType = strings.TrimSpace(req.ActionType) - req.ConnectorRef = strings.TrimSpace(req.ConnectorRef) - req.InputSchemaJSON = strings.TrimSpace(req.InputSchemaJSON) - req.OutputSchemaJSON = strings.TrimSpace(req.OutputSchemaJSON) - req.RiskLevel = strings.TrimSpace(req.RiskLevel) - req.ApprovalMode = strings.TrimSpace(req.ApprovalMode) - req.AuditLevel = strings.TrimSpace(req.AuditLevel) - req.OntologyBindingJSON = strings.TrimSpace(req.OntologyBindingJSON) - req.State = strings.TrimSpace(req.State) -} - -func validateActionDefinitionReq(req *actionDefinitionReq) *web.AppError { - normalizeActionDefinitionReq(req) - if req.Key == "" || req.Label == "" { - return web.NewBadRequest("key、label 为必填") - } - if req.ActionType == "" { - req.ActionType = "execution" - } - if req.RiskLevel == "" { - req.RiskLevel = "low" - } - if req.ApprovalMode == "" { - req.ApprovalMode = "not_required" - } - if req.AuditLevel == "" { - req.AuditLevel = "standard" - } - if req.State == "" { - req.State = "active" - } - if req.State != "active" && req.State != "inactive" { - return web.NewBadRequest("state 只能是 active 或 inactive") - } - if !store.ValidateJSONObjectJSON(req.InputSchemaJSON) { - return web.NewBadRequest("input_schema_json 必须是 JSON 对象") - } - if !store.ValidateJSONObjectJSON(req.OutputSchemaJSON) { - return web.NewBadRequest("output_schema_json 必须是 JSON 对象") - } - if !store.ValidateJSONObjectJSON(req.OntologyBindingJSON) { - return web.NewBadRequest("ontology_binding_json 必须是 JSON 对象") - } - return nil -} - -func ListSkillDefinitions(c *gin.Context) { - q := store.DB.Model(&model.SkillDefinition{}) - if c.Query("state") == "" { - q = q.Where("state = ?", "active") - } else { - q = q.Where("state = ?", c.Query("state")) - } - if c.Query("exposed_to_user") != "" { - q = q.Where("exposed_to_user = ?", c.Query("exposed_to_user") == "true") - } - var items []model.SkillDefinition - if err := q.Order("sort_order ASC, id ASC").Find(&items).Error; err != nil { - web.Fail(c, web.NewBadRequest("查询技能定义失败")) - return - } - web.OK(c, items) -} - -func GetSkillDefinitionByKey(c *gin.Context) { - key := strings.TrimSpace(c.Param("key")) - if key == "" { - web.Fail(c, web.NewBadRequest("技能 key 不能为空")) - return - } - var item model.SkillDefinition - if err := store.DB.Where("key = ?", key).First(&item).Error; err != nil { - web.Fail(c, web.NewNotFoundError("技能定义不存在")) - return - } - web.OK(c, item) -} - -func CreateSkillDefinition(c *gin.Context) { - var req skillDefinitionReq - if err := c.ShouldBindJSON(&req); err != nil { - web.Fail(c, web.NewBadRequest("请求参数错误")) - return - } - if appErr := validateSkillDefinitionReq(&req); appErr != nil { - web.Fail(c, appErr) - return - } - item := model.SkillDefinition{ - Key: req.Key, - Label: req.Label, - Description: req.Description, - RoleKind: req.RoleKind, - Source: req.Source, - ObjectEntryRoute: req.ObjectEntryRoute, - LegacyObjectEntryRoute: req.LegacyObjectEntryRoute, - ExposedToUser: req.ExposedToUser, - StarterPromptsJSON: req.StarterPromptsJSON, - PromptTemplate: req.PromptTemplate, - InputSchemaJSON: req.InputSchemaJSON, - OutputSchemaJSON: req.OutputSchemaJSON, - ArtifactSchemaJSON: req.ArtifactSchemaJSON, - ActionRefsJSON: req.ActionRefsJSON, - PolicyRefsJSON: req.PolicyRefsJSON, - OntologyBindingJSON: req.OntologyBindingJSON, - State: req.State, - SortOrder: req.SortOrder, - } - if err := store.DB.Create(&item).Error; err != nil { - web.Fail(c, web.NewBadRequest("创建技能定义失败")) - return - } - web.OK(c, item) -} - -func UpdateSkillDefinition(c *gin.Context) { - id, ok := parseID(c, "id") - if !ok { - return - } - var item model.SkillDefinition - if err := store.DB.First(&item, id).Error; err != nil { - web.Fail(c, web.NewNotFoundError("技能定义不存在")) - return - } - var req skillDefinitionReq - if err := c.ShouldBindJSON(&req); err != nil { - web.Fail(c, web.NewBadRequest("请求参数错误")) - return - } - if appErr := validateSkillDefinitionReq(&req); appErr != nil { - web.Fail(c, appErr) - return - } - item.Key = req.Key - item.Label = req.Label - item.Description = req.Description - item.RoleKind = req.RoleKind - item.Source = req.Source - item.ObjectEntryRoute = req.ObjectEntryRoute - item.LegacyObjectEntryRoute = req.LegacyObjectEntryRoute - item.ExposedToUser = req.ExposedToUser - item.StarterPromptsJSON = req.StarterPromptsJSON - item.PromptTemplate = req.PromptTemplate - item.InputSchemaJSON = req.InputSchemaJSON - item.OutputSchemaJSON = req.OutputSchemaJSON - item.ArtifactSchemaJSON = req.ArtifactSchemaJSON - item.ActionRefsJSON = req.ActionRefsJSON - item.PolicyRefsJSON = req.PolicyRefsJSON - item.OntologyBindingJSON = req.OntologyBindingJSON - item.State = req.State - item.SortOrder = req.SortOrder - if err := store.DB.Save(&item).Error; err != nil { - web.Fail(c, web.NewBadRequest("更新技能定义失败")) - return - } - web.OK(c, item) -} - -func DeleteSkillDefinition(c *gin.Context) { - id, ok := parseID(c, "id") - if !ok { - return - } - var item model.SkillDefinition - if err := store.DB.First(&item, id).Error; err != nil { - web.Fail(c, web.NewNotFoundError("技能定义不存在")) - return - } - if err := store.DB.Delete(&item).Error; err != nil { - web.Fail(c, web.NewBadRequest("删除技能定义失败")) - return - } - web.OK(c, gin.H{"id": id, "deleted": true}) -} - -func ListActionDefinitions(c *gin.Context) { - q := store.DB.Model(&model.ActionDefinition{}) - if c.Query("state") == "" { - q = q.Where("state = ?", "active") - } else { - q = q.Where("state = ?", c.Query("state")) - } - var items []model.ActionDefinition - if err := q.Order("sort_order ASC, id ASC").Find(&items).Error; err != nil { - web.Fail(c, web.NewBadRequest("查询 Action 定义失败")) - return - } - web.OK(c, items) -} - -func GetActionDefinitionByKey(c *gin.Context) { - key := strings.TrimSpace(c.Param("key")) - if key == "" { - web.Fail(c, web.NewBadRequest("action key 不能为空")) - return - } - var item model.ActionDefinition - if err := store.DB.Where("key = ?", key).First(&item).Error; err != nil { - web.Fail(c, web.NewNotFoundError("Action 定义不存在")) - return - } - web.OK(c, item) -} - -func CreateActionDefinition(c *gin.Context) { - var req actionDefinitionReq - if err := c.ShouldBindJSON(&req); err != nil { - web.Fail(c, web.NewBadRequest("请求参数错误")) - return - } - if appErr := validateActionDefinitionReq(&req); appErr != nil { - web.Fail(c, appErr) - return - } - item := model.ActionDefinition{ - Key: req.Key, - Label: req.Label, - Description: req.Description, - ActionType: req.ActionType, - ConnectorRef: req.ConnectorRef, - InputSchemaJSON: req.InputSchemaJSON, - OutputSchemaJSON: req.OutputSchemaJSON, - RiskLevel: req.RiskLevel, - ApprovalMode: req.ApprovalMode, - AuditLevel: req.AuditLevel, - ExposedToUser: req.ExposedToUser, - OntologyBindingJSON: req.OntologyBindingJSON, - State: req.State, - SortOrder: req.SortOrder, - } - if err := store.DB.Create(&item).Error; err != nil { - web.Fail(c, web.NewBadRequest("创建 Action 定义失败")) - return - } - web.OK(c, item) -} - -func UpdateActionDefinition(c *gin.Context) { - id, ok := parseID(c, "id") - if !ok { - return - } - var item model.ActionDefinition - if err := store.DB.First(&item, id).Error; err != nil { - web.Fail(c, web.NewNotFoundError("Action 定义不存在")) - return - } - var req actionDefinitionReq - if err := c.ShouldBindJSON(&req); err != nil { - web.Fail(c, web.NewBadRequest("请求参数错误")) - return - } - if appErr := validateActionDefinitionReq(&req); appErr != nil { - web.Fail(c, appErr) - return - } - item.Key = req.Key - item.Label = req.Label - item.Description = req.Description - item.ActionType = req.ActionType - item.ConnectorRef = req.ConnectorRef - item.InputSchemaJSON = req.InputSchemaJSON - item.OutputSchemaJSON = req.OutputSchemaJSON - item.RiskLevel = req.RiskLevel - item.ApprovalMode = req.ApprovalMode - item.AuditLevel = req.AuditLevel - item.ExposedToUser = req.ExposedToUser - item.OntologyBindingJSON = req.OntologyBindingJSON - item.State = req.State - item.SortOrder = req.SortOrder - if err := store.DB.Save(&item).Error; err != nil { - web.Fail(c, web.NewBadRequest("更新 Action 定义失败")) - return - } - web.OK(c, item) -} - -func DeleteActionDefinition(c *gin.Context) { - id, ok := parseID(c, "id") - if !ok { - return - } - var item model.ActionDefinition - if err := store.DB.First(&item, id).Error; err != nil { - web.Fail(c, web.NewNotFoundError("Action 定义不存在")) - return - } - if err := store.DB.Delete(&item).Error; err != nil { - web.Fail(c, web.NewBadRequest("删除 Action 定义失败")) - return - } - web.OK(c, gin.H{"id": id, "deleted": true}) -} diff --git a/eai_agentplatform/backend-go/internal/api/router.go b/eai_agentplatform/backend-go/internal/api/router.go index 5d3125c..e9e7e5a 100644 --- a/eai_agentplatform/backend-go/internal/api/router.go +++ b/eai_agentplatform/backend-go/internal/api/router.go @@ -45,6 +45,8 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) { r.GET("/api/specialists/summary", middleware.Auth(cfg), SpecialistSummary) r.GET("/api/skills", middleware.Auth(cfg), ListSkillDefinitions) r.GET("/api/skills/by-key/:key", middleware.Auth(cfg), GetSkillDefinitionByKey) + r.GET("/api/apps", middleware.Auth(cfg), ListAppDefinitions) + r.GET("/api/apps/by-key/:key", middleware.Auth(cfg), GetAppDefinitionByKey) r.GET("/api/actions", middleware.Auth(cfg), ListActionDefinitions) r.GET("/api/actions/by-key/:key", middleware.Auth(cfg), GetActionDefinitionByKey) r.GET("/api/workbench/overview", middleware.Auth(cfg), WorkbenchOverview) @@ -205,6 +207,9 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) { admin.POST("/skills", CreateSkillDefinition) admin.PUT("/skills/:id", UpdateSkillDefinition) admin.DELETE("/skills/:id", DeleteSkillDefinition) + admin.POST("/apps", CreateAppDefinition) + admin.PUT("/apps/:id", UpdateAppDefinition) + admin.DELETE("/apps/:id", DeleteAppDefinition) admin.POST("/actions", CreateActionDefinition) admin.PUT("/actions/:id", UpdateActionDefinition) admin.DELETE("/actions/:id", DeleteActionDefinition) diff --git a/eai_agentplatform/backend-go/internal/api/skill_action_definition.go b/eai_agentplatform/backend-go/internal/api/skill_action_definition.go new file mode 100644 index 0000000..65ccd26 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/api/skill_action_definition.go @@ -0,0 +1,409 @@ +package api + +import ( + "strings" + + "github.com/gin-gonic/gin" + + "eai_agentplatform/backend/internal/model" + "eai_agentplatform/backend/internal/store" + "eai_agentplatform/backend/internal/web" +) + +type skillDefinitionReq struct { + Key string `json:"key"` + Label string `json:"label"` + Description string `json:"description"` + ObjectKind string `json:"object_kind"` + Source string `json:"source"` + ObjectEntryRoute string `json:"object_entry_route"` + ExposedToUser bool `json:"exposed_to_user"` + StarterPromptsJSON string `json:"starter_prompts_json"` + PromptTemplate string `json:"prompt_template"` + InputSchemaJSON string `json:"input_schema_json"` + OutputSchemaJSON string `json:"output_schema_json"` + ArtifactSchemaJSON string `json:"artifact_schema_json"` + ActionRefsJSON string `json:"action_refs_json"` + PolicyRefsJSON string `json:"policy_refs_json"` + OntologyBindingJSON string `json:"ontology_binding_json"` + State string `json:"state"` + SortOrder int `json:"sort_order"` +} + +type actionDefinitionReq struct { + Key string `json:"key"` + Label string `json:"label"` + Description string `json:"description"` + ActionType string `json:"action_type"` + ConnectorRef string `json:"connector_ref"` + InputSchemaJSON string `json:"input_schema_json"` + OutputSchemaJSON string `json:"output_schema_json"` + RiskLevel string `json:"risk_level"` + ApprovalMode string `json:"approval_mode"` + AuditLevel string `json:"audit_level"` + ExposedToUser bool `json:"exposed_to_user"` + OntologyBindingJSON string `json:"ontology_binding_json"` + State string `json:"state"` + SortOrder int `json:"sort_order"` +} + +func normalizeSkillDefinitionReq(req *skillDefinitionReq) { + req.Key = strings.TrimSpace(req.Key) + req.Label = strings.TrimSpace(req.Label) + req.Description = strings.TrimSpace(req.Description) + req.ObjectKind = strings.TrimSpace(req.ObjectKind) + req.Source = strings.TrimSpace(req.Source) + req.ObjectEntryRoute = strings.TrimSpace(req.ObjectEntryRoute) + req.StarterPromptsJSON = strings.TrimSpace(req.StarterPromptsJSON) + req.PromptTemplate = strings.TrimSpace(req.PromptTemplate) + req.InputSchemaJSON = strings.TrimSpace(req.InputSchemaJSON) + req.OutputSchemaJSON = strings.TrimSpace(req.OutputSchemaJSON) + req.ArtifactSchemaJSON = strings.TrimSpace(req.ArtifactSchemaJSON) + req.ActionRefsJSON = strings.TrimSpace(req.ActionRefsJSON) + req.PolicyRefsJSON = strings.TrimSpace(req.PolicyRefsJSON) + req.OntologyBindingJSON = strings.TrimSpace(req.OntologyBindingJSON) + req.State = strings.TrimSpace(req.State) +} + +func validateSkillDefinitionReq(req *skillDefinitionReq) *web.AppError { + normalizeSkillDefinitionReq(req) + if req.Key == "" || req.Label == "" { + return web.NewBadRequest("key、label 为必填") + } + if req.ObjectKind == "" { + req.ObjectKind = "skill" + } + switch req.ObjectKind { + case "assistant", "specialist", "skill": + default: + return web.NewBadRequest("object_kind 只能是 assistant、specialist 或 skill") + } + if req.Source == "" { + req.Source = "eai" + } + if req.State == "" { + req.State = "active" + } + if req.State != "active" && req.State != "inactive" { + return web.NewBadRequest("state 只能是 active 或 inactive") + } + if !store.ValidateJSONStringArray(req.StarterPromptsJSON) { + return web.NewBadRequest("starter_prompts_json 必须是字符串数组") + } + if !store.ValidateJSONObjectJSON(req.InputSchemaJSON) { + return web.NewBadRequest("input_schema_json 必须是 JSON 对象") + } + if !store.ValidateJSONObjectJSON(req.OutputSchemaJSON) { + return web.NewBadRequest("output_schema_json 必须是 JSON 对象") + } + if !store.ValidateJSONObjectJSON(req.ArtifactSchemaJSON) { + return web.NewBadRequest("artifact_schema_json 必须是 JSON 对象") + } + if !store.ValidateJSONStringArray(req.ActionRefsJSON) { + return web.NewBadRequest("action_refs_json 必须是字符串数组") + } + if !store.ValidateJSONStringArray(req.PolicyRefsJSON) { + return web.NewBadRequest("policy_refs_json 必须是字符串数组") + } + if !store.ValidateJSONObjectJSON(req.OntologyBindingJSON) { + return web.NewBadRequest("ontology_binding_json 必须是 JSON 对象") + } + return nil +} + +func normalizeActionDefinitionReq(req *actionDefinitionReq) { + req.Key = strings.TrimSpace(req.Key) + req.Label = strings.TrimSpace(req.Label) + req.Description = strings.TrimSpace(req.Description) + req.ActionType = strings.TrimSpace(req.ActionType) + req.ConnectorRef = strings.TrimSpace(req.ConnectorRef) + req.InputSchemaJSON = strings.TrimSpace(req.InputSchemaJSON) + req.OutputSchemaJSON = strings.TrimSpace(req.OutputSchemaJSON) + req.RiskLevel = strings.TrimSpace(req.RiskLevel) + req.ApprovalMode = strings.TrimSpace(req.ApprovalMode) + req.AuditLevel = strings.TrimSpace(req.AuditLevel) + req.OntologyBindingJSON = strings.TrimSpace(req.OntologyBindingJSON) + req.State = strings.TrimSpace(req.State) +} + +func validateActionDefinitionReq(req *actionDefinitionReq) *web.AppError { + normalizeActionDefinitionReq(req) + if req.Key == "" || req.Label == "" { + return web.NewBadRequest("key、label 为必填") + } + if req.ActionType == "" { + req.ActionType = "execution" + } + if req.RiskLevel == "" { + req.RiskLevel = "low" + } + if req.ApprovalMode == "" { + req.ApprovalMode = "not_required" + } + if req.AuditLevel == "" { + req.AuditLevel = "standard" + } + if req.State == "" { + req.State = "active" + } + if req.State != "active" && req.State != "inactive" { + return web.NewBadRequest("state 只能是 active 或 inactive") + } + if !store.ValidateJSONObjectJSON(req.InputSchemaJSON) { + return web.NewBadRequest("input_schema_json 必须是 JSON 对象") + } + if !store.ValidateJSONObjectJSON(req.OutputSchemaJSON) { + return web.NewBadRequest("output_schema_json 必须是 JSON 对象") + } + if !store.ValidateJSONObjectJSON(req.OntologyBindingJSON) { + return web.NewBadRequest("ontology_binding_json 必须是 JSON 对象") + } + return nil +} + +func ListSkillDefinitions(c *gin.Context) { + q := store.DB.Model(&model.SkillDefinition{}) + if c.Query("state") == "" { + q = q.Where("state = ?", "active") + } else { + q = q.Where("state = ?", c.Query("state")) + } + if c.Query("exposed_to_user") != "" { + q = q.Where("exposed_to_user = ?", c.Query("exposed_to_user") == "true") + } + var items []model.SkillDefinition + if err := q.Order("sort_order ASC, id ASC").Find(&items).Error; err != nil { + web.Fail(c, web.NewBadRequest("查询技能定义失败")) + return + } + web.OK(c, items) +} + +func GetSkillDefinitionByKey(c *gin.Context) { + key := strings.TrimSpace(c.Param("key")) + if key == "" { + web.Fail(c, web.NewBadRequest("技能 key 不能为空")) + return + } + var item model.SkillDefinition + if err := store.DB.Where("key = ?", key).First(&item).Error; err != nil { + web.Fail(c, web.NewNotFoundError("技能定义不存在")) + return + } + web.OK(c, item) +} + +func CreateSkillDefinition(c *gin.Context) { + var req skillDefinitionReq + if err := c.ShouldBindJSON(&req); err != nil { + web.Fail(c, web.NewBadRequest("请求参数错误")) + return + } + if appErr := validateSkillDefinitionReq(&req); appErr != nil { + web.Fail(c, appErr) + return + } + item := model.SkillDefinition{ + Key: req.Key, + Label: req.Label, + Description: req.Description, + ObjectKind: req.ObjectKind, + Source: req.Source, + ObjectEntryRoute: req.ObjectEntryRoute, + ExposedToUser: req.ExposedToUser, + StarterPromptsJSON: req.StarterPromptsJSON, + PromptTemplate: req.PromptTemplate, + InputSchemaJSON: req.InputSchemaJSON, + OutputSchemaJSON: req.OutputSchemaJSON, + ArtifactSchemaJSON: req.ArtifactSchemaJSON, + ActionRefsJSON: req.ActionRefsJSON, + PolicyRefsJSON: req.PolicyRefsJSON, + OntologyBindingJSON: req.OntologyBindingJSON, + State: req.State, + SortOrder: req.SortOrder, + } + if err := store.DB.Create(&item).Error; err != nil { + web.Fail(c, web.NewBadRequest("创建技能定义失败")) + return + } + web.OK(c, item) +} + +func UpdateSkillDefinition(c *gin.Context) { + id, ok := parseID(c, "id") + if !ok { + return + } + var item model.SkillDefinition + if err := store.DB.First(&item, id).Error; err != nil { + web.Fail(c, web.NewNotFoundError("技能定义不存在")) + return + } + var req skillDefinitionReq + if err := c.ShouldBindJSON(&req); err != nil { + web.Fail(c, web.NewBadRequest("请求参数错误")) + return + } + if appErr := validateSkillDefinitionReq(&req); appErr != nil { + web.Fail(c, appErr) + return + } + item.Key = req.Key + item.Label = req.Label + item.Description = req.Description + item.ObjectKind = req.ObjectKind + item.Source = req.Source + item.ObjectEntryRoute = req.ObjectEntryRoute + item.ExposedToUser = req.ExposedToUser + item.StarterPromptsJSON = req.StarterPromptsJSON + item.PromptTemplate = req.PromptTemplate + item.InputSchemaJSON = req.InputSchemaJSON + item.OutputSchemaJSON = req.OutputSchemaJSON + item.ArtifactSchemaJSON = req.ArtifactSchemaJSON + item.ActionRefsJSON = req.ActionRefsJSON + item.PolicyRefsJSON = req.PolicyRefsJSON + item.OntologyBindingJSON = req.OntologyBindingJSON + item.State = req.State + item.SortOrder = req.SortOrder + if err := store.DB.Save(&item).Error; err != nil { + web.Fail(c, web.NewBadRequest("更新技能定义失败")) + return + } + web.OK(c, item) +} + +func DeleteSkillDefinition(c *gin.Context) { + id, ok := parseID(c, "id") + if !ok { + return + } + var item model.SkillDefinition + if err := store.DB.First(&item, id).Error; err != nil { + web.Fail(c, web.NewNotFoundError("技能定义不存在")) + return + } + if err := store.DB.Delete(&item).Error; err != nil { + web.Fail(c, web.NewBadRequest("删除技能定义失败")) + return + } + web.OK(c, gin.H{"id": id, "deleted": true}) +} + +func ListActionDefinitions(c *gin.Context) { + q := store.DB.Model(&model.ActionDefinition{}) + if c.Query("state") == "" { + q = q.Where("state = ?", "active") + } else { + q = q.Where("state = ?", c.Query("state")) + } + var items []model.ActionDefinition + if err := q.Order("sort_order ASC, id ASC").Find(&items).Error; err != nil { + web.Fail(c, web.NewBadRequest("查询 Action 定义失败")) + return + } + web.OK(c, items) +} + +func GetActionDefinitionByKey(c *gin.Context) { + key := strings.TrimSpace(c.Param("key")) + if key == "" { + web.Fail(c, web.NewBadRequest("action key 不能为空")) + return + } + var item model.ActionDefinition + if err := store.DB.Where("key = ?", key).First(&item).Error; err != nil { + web.Fail(c, web.NewNotFoundError("Action 定义不存在")) + return + } + web.OK(c, item) +} + +func CreateActionDefinition(c *gin.Context) { + var req actionDefinitionReq + if err := c.ShouldBindJSON(&req); err != nil { + web.Fail(c, web.NewBadRequest("请求参数错误")) + return + } + if appErr := validateActionDefinitionReq(&req); appErr != nil { + web.Fail(c, appErr) + return + } + item := model.ActionDefinition{ + Key: req.Key, + Label: req.Label, + Description: req.Description, + ActionType: req.ActionType, + ConnectorRef: req.ConnectorRef, + InputSchemaJSON: req.InputSchemaJSON, + OutputSchemaJSON: req.OutputSchemaJSON, + RiskLevel: req.RiskLevel, + ApprovalMode: req.ApprovalMode, + AuditLevel: req.AuditLevel, + ExposedToUser: req.ExposedToUser, + OntologyBindingJSON: req.OntologyBindingJSON, + State: req.State, + SortOrder: req.SortOrder, + } + if err := store.DB.Create(&item).Error; err != nil { + web.Fail(c, web.NewBadRequest("创建 Action 定义失败")) + return + } + web.OK(c, item) +} + +func UpdateActionDefinition(c *gin.Context) { + id, ok := parseID(c, "id") + if !ok { + return + } + var item model.ActionDefinition + if err := store.DB.First(&item, id).Error; err != nil { + web.Fail(c, web.NewNotFoundError("Action 定义不存在")) + return + } + var req actionDefinitionReq + if err := c.ShouldBindJSON(&req); err != nil { + web.Fail(c, web.NewBadRequest("请求参数错误")) + return + } + if appErr := validateActionDefinitionReq(&req); appErr != nil { + web.Fail(c, appErr) + return + } + item.Key = req.Key + item.Label = req.Label + item.Description = req.Description + item.ActionType = req.ActionType + item.ConnectorRef = req.ConnectorRef + item.InputSchemaJSON = req.InputSchemaJSON + item.OutputSchemaJSON = req.OutputSchemaJSON + item.RiskLevel = req.RiskLevel + item.ApprovalMode = req.ApprovalMode + item.AuditLevel = req.AuditLevel + item.ExposedToUser = req.ExposedToUser + item.OntologyBindingJSON = req.OntologyBindingJSON + item.State = req.State + item.SortOrder = req.SortOrder + if err := store.DB.Save(&item).Error; err != nil { + web.Fail(c, web.NewBadRequest("更新 Action 定义失败")) + return + } + web.OK(c, item) +} + +func DeleteActionDefinition(c *gin.Context) { + id, ok := parseID(c, "id") + if !ok { + return + } + var item model.ActionDefinition + if err := store.DB.First(&item, id).Error; err != nil { + web.Fail(c, web.NewNotFoundError("Action 定义不存在")) + return + } + if err := store.DB.Delete(&item).Error; err != nil { + web.Fail(c, web.NewBadRequest("删除 Action 定义失败")) + return + } + web.OK(c, gin.H{"id": id, "deleted": true}) +} diff --git a/eai_agentplatform/backend-go/internal/api/specialist.go b/eai_agentplatform/backend-go/internal/api/specialist.go index a26ab8b..168bd41 100644 --- a/eai_agentplatform/backend-go/internal/api/specialist.go +++ b/eai_agentplatform/backend-go/internal/api/specialist.go @@ -110,41 +110,40 @@ func SpecialistSummary(c *gin.Context) { } type specialistReq struct { - Key string `json:"key"` - Label string `json:"label"` - DisplayCode string `json:"display_code"` - EAILogicCode string `json:"eailogic_code"` - Tier string `json:"tier"` - WorkerType string `json:"worker_type"` - ObjectEntryRoute string `json:"object_entry_route"` - Summary string `json:"summary"` - RoleCardJSON string `json:"role_card_json"` - WorkStatus string `json:"work_status"` - RiskLabel string `json:"risk_label"` - Color string `json:"color"` - Stage string `json:"stage"` - Progress int `json:"progress"` - MarketTag string `json:"market_tag"` - Version string `json:"version"` - ConnectorScope string `json:"connector_scope"` - PermissionScope string `json:"permission_scope"` - ResourceBindings string `json:"resource_bindings"` - InfoSources string `json:"info_sources"` - BaseSkills string `json:"base_skills"` - AIAssistance string `json:"ai_assistance"` - GeneratedSkills string `json:"generated_skills"` + Key string `json:"key"` + Label string `json:"label"` + DisplayCode string `json:"display_code"` + EAILogicCode string `json:"eailogic_code"` + Tier string `json:"tier"` + WorkerType string `json:"worker_type"` + ObjectEntryRoute string `json:"object_entry_route"` + Summary string `json:"summary"` + InteractionCardJSON string `json:"interaction_card_json"` + WorkStatus string `json:"work_status"` + RiskLabel string `json:"risk_label"` + Color string `json:"color"` + Stage string `json:"stage"` + Progress int `json:"progress"` + MarketTag string `json:"market_tag"` + Version string `json:"version"` + ConnectorScope string `json:"connector_scope"` + PermissionScope string `json:"permission_scope"` + ResourceBindings string `json:"resource_bindings"` + InfoSources string `json:"info_sources"` + BaseSkills string `json:"base_skills"` + AIAssistance string `json:"ai_assistance"` + GeneratedSkills string `json:"generated_skills"` // RuleFileMarkdown 岗位说明书正文(Markdown),会话创建时注入 System Prompt RuleFileMarkdown string `json:"rule_file_markdown"` // AllowedSkills 绑定技能 key 的 JSON 数组字符串,顺序即优先级(首个为主技能)。 // 元素须在 model.ValidSkillKeys 内;写入前会被规范化成紧凑 JSON。 - AllowedSkills string `json:"allowed_skills"` - InputsRecordsJSON string `json:"inputs_records_json"` - LegacySourceRecordsJSON string `json:"source_records_json"` - PermissionRecordsJSON string `json:"permission_records_json"` - ActionRecordsJSON string `json:"action_records_json"` - ResultRecordsJSON string `json:"result_records_json"` - State string `json:"state"` - SortOrder int `json:"sort_order"` + AllowedSkills string `json:"allowed_skills"` + InputsRecordsJSON string `json:"inputs_records_json"` + PermissionRecordsJSON string `json:"permission_records_json"` + ActionRecordsJSON string `json:"action_records_json"` + ResultRecordsJSON string `json:"result_records_json"` + State string `json:"state"` + SortOrder int `json:"sort_order"` } func normalizeSpecialistReq(req *specialistReq) { @@ -156,7 +155,7 @@ func normalizeSpecialistReq(req *specialistReq) { req.WorkerType = strings.TrimSpace(req.WorkerType) req.ObjectEntryRoute = strings.TrimSpace(req.ObjectEntryRoute) req.Summary = strings.TrimSpace(req.Summary) - req.RoleCardJSON = strings.TrimSpace(req.RoleCardJSON) + req.InteractionCardJSON = strings.TrimSpace(req.InteractionCardJSON) req.WorkStatus = strings.TrimSpace(req.WorkStatus) req.RiskLabel = strings.TrimSpace(req.RiskLabel) req.Color = strings.TrimSpace(req.Color) @@ -173,10 +172,6 @@ func normalizeSpecialistReq(req *specialistReq) { req.RuleFileMarkdown = strings.TrimSpace(req.RuleFileMarkdown) req.AllowedSkills = strings.TrimSpace(req.AllowedSkills) req.InputsRecordsJSON = strings.TrimSpace(req.InputsRecordsJSON) - req.LegacySourceRecordsJSON = strings.TrimSpace(req.LegacySourceRecordsJSON) - if req.InputsRecordsJSON == "" { - req.InputsRecordsJSON = req.LegacySourceRecordsJSON - } req.PermissionRecordsJSON = strings.TrimSpace(req.PermissionRecordsJSON) req.ActionRecordsJSON = strings.TrimSpace(req.ActionRecordsJSON) req.ResultRecordsJSON = strings.TrimSpace(req.ResultRecordsJSON) @@ -212,8 +207,8 @@ func validateSpecialistReq(req *specialistReq) *web.AppError { if !store.ValidateStructuredRecordsJSON(req.InputsRecordsJSON) { return web.NewBadRequest("inputs_records_json 必须是 JSON 数组") } - if !store.ValidateJSONObjectJSON(req.RoleCardJSON) { - return web.NewBadRequest("role_card_json 必须是 JSON 对象") + if !store.ValidateJSONObjectJSON(req.InteractionCardJSON) { + return web.NewBadRequest("interaction_card_json 必须是 JSON 对象") } if !store.ValidateStructuredRecordsJSON(req.PermissionRecordsJSON) { return web.NewBadRequest("permission_records_json 必须是 JSON 数组") @@ -282,7 +277,7 @@ func CreateSpecialist(c *gin.Context) { WorkerType: req.WorkerType, ObjectEntryRoute: req.ObjectEntryRoute, Summary: req.Summary, - RoleCardJSON: req.RoleCardJSON, + InteractionCardJSON: req.InteractionCardJSON, WorkStatus: req.WorkStatus, RiskLabel: req.RiskLabel, Color: req.Color, @@ -351,7 +346,7 @@ func UpdateSpecialist(c *gin.Context) { item.WorkerType = req.WorkerType item.ObjectEntryRoute = req.ObjectEntryRoute item.Summary = req.Summary - item.RoleCardJSON = req.RoleCardJSON + item.InteractionCardJSON = req.InteractionCardJSON item.WorkStatus = req.WorkStatus item.RiskLabel = req.RiskLabel item.Color = req.Color diff --git a/eai_agentplatform/backend-go/internal/model/app_definition.go b/eai_agentplatform/backend-go/internal/model/app_definition.go new file mode 100644 index 0000000..eec6b11 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/model/app_definition.go @@ -0,0 +1,38 @@ +package model + +import "time" + +// AppDefinition 面向用户的一键应用目录定义。 +// 它是“成品入口”这一层:决定应用怎么展示、默认挂哪个专员/技能、打开后走哪条路。 +type AppDefinition struct { + ID uint `gorm:"primaryKey" json:"id"` + Key string `gorm:"size:64;uniqueIndex;not null" json:"key"` + Label string `gorm:"size:128;not null" json:"label"` + DisplayCode string `gorm:"column:display_code;size:16;default:'';index" json:"display_code"` + EAILogicCode string `gorm:"column:eailogic_code;size:32;default:'';index" json:"eailogic_code"` + Badge string `gorm:"size:32;default:''" json:"badge"` + Kind string `gorm:"size:64;default:''" json:"kind"` + MarketTag string `gorm:"column:market_tag;size:32;default:'';index" json:"market_tag"` + WorkerType string `gorm:"size:16;default:'';index" json:"worker_type"` + Tier string `gorm:"size:16;default:'';index" json:"tier"` + Source string `gorm:"size:32;not null;default:eai;index" json:"source"` // eai / custom / imported + Color string `gorm:"size:32;default:''" json:"color"` + IconText string `gorm:"column:icon_text;size:16;default:''" json:"icon_text"` + CoverTone string `gorm:"column:cover_tone;type:text" json:"cover_tone"` + Summary string `gorm:"type:text" json:"summary"` + Description string `gorm:"type:text" json:"description"` + OpenRoute string `gorm:"column:open_route;size:256;default:''" json:"open_route"` + SpecialistKey string `gorm:"column:specialist_key;size:64;default:'';index" json:"specialist_key"` + SkillKey string `gorm:"column:skill_key;size:64;default:'';index" json:"skill_key"` + DefaultPrompt string `gorm:"column:default_prompt;type:text" json:"default_prompt"` + PromptsJSON string `gorm:"column:prompts_json;type:text" json:"prompts_json"` + TagsJSON string `gorm:"column:tags_json;type:text" json:"tags_json"` + InstallState string `gorm:"column:install_state;size:16;not null;default:installed;index" json:"install_state"` + ExposedToUser bool `gorm:"column:exposed_to_user;not null;default:true;index" json:"exposed_to_user"` + State string `gorm:"size:16;not null;default:active;index" json:"state"` // active / inactive + SortOrder int `gorm:"not null;default:0;index" json:"sort_order"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +func (AppDefinition) TableName() string { return "app_definition" } diff --git a/eai_agentplatform/backend-go/internal/model/skill_definition.go b/eai_agentplatform/backend-go/internal/model/skill_definition.go index 6712fcd..f19fab8 100644 --- a/eai_agentplatform/backend-go/internal/model/skill_definition.go +++ b/eai_agentplatform/backend-go/internal/model/skill_definition.go @@ -4,29 +4,28 @@ import "time" // SkillDefinition 对外暴露的任务级能力定义。 type SkillDefinition struct { - ID uint `gorm:"primaryKey" json:"id"` - Key string `gorm:"size:64;uniqueIndex;not null" json:"key"` - Label string `gorm:"size:128;not null" json:"label"` - DisplayCode string `gorm:"column:display_code;size:16;default:'';index" json:"display_code"` - EAILogicCode string `gorm:"column:eailogic_code;size:32;default:'';index" json:"eailogic_code"` - Description string `gorm:"type:text" json:"description"` - RoleKind string `gorm:"size:32;not null;default:skill;index" json:"role_kind"` // assistant / specialist / skill - Source string `gorm:"size:32;not null;default:eai;index" json:"source"` // eai / custom - ObjectEntryRoute string `gorm:"column:object_entry_route;size:128;default:''" json:"object_entry_route"` - LegacyObjectEntryRoute string `gorm:"column:legacy_object_entry_route;size:128;default:''" json:"legacy_object_entry_route"` - ExposedToUser bool `gorm:"not null;default:true;index" json:"exposed_to_user"` - StarterPromptsJSON string `gorm:"type:text" json:"starter_prompts_json"` - PromptTemplate string `gorm:"type:text" json:"prompt_template"` - InputSchemaJSON string `gorm:"type:text" json:"input_schema_json"` - OutputSchemaJSON string `gorm:"type:text" json:"output_schema_json"` - ArtifactSchemaJSON string `gorm:"type:text" json:"artifact_schema_json"` - ActionRefsJSON string `gorm:"type:text" json:"action_refs_json"` - PolicyRefsJSON string `gorm:"type:text" json:"policy_refs_json"` - OntologyBindingJSON string `gorm:"type:text" json:"ontology_binding_json"` - State string `gorm:"size:16;not null;default:active;index" json:"state"` - SortOrder int `gorm:"not null;default:0;index" json:"sort_order"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uint `gorm:"primaryKey" json:"id"` + Key string `gorm:"size:64;uniqueIndex;not null" json:"key"` + Label string `gorm:"size:128;not null" json:"label"` + DisplayCode string `gorm:"column:display_code;size:16;default:'';index" json:"display_code"` + EAILogicCode string `gorm:"column:eailogic_code;size:32;default:'';index" json:"eailogic_code"` + Description string `gorm:"type:text" json:"description"` + ObjectKind string `gorm:"column:object_kind;size:32;not null;default:skill;index" json:"object_kind"` // assistant / specialist / skill + Source string `gorm:"size:32;not null;default:eai;index" json:"source"` // eai / custom + ObjectEntryRoute string `gorm:"column:object_entry_route;size:128;default:''" json:"object_entry_route"` + ExposedToUser bool `gorm:"not null;default:true;index" json:"exposed_to_user"` + StarterPromptsJSON string `gorm:"type:text" json:"starter_prompts_json"` + PromptTemplate string `gorm:"type:text" json:"prompt_template"` + InputSchemaJSON string `gorm:"type:text" json:"input_schema_json"` + OutputSchemaJSON string `gorm:"type:text" json:"output_schema_json"` + ArtifactSchemaJSON string `gorm:"type:text" json:"artifact_schema_json"` + ActionRefsJSON string `gorm:"type:text" json:"action_refs_json"` + PolicyRefsJSON string `gorm:"type:text" json:"policy_refs_json"` + OntologyBindingJSON string `gorm:"type:text" json:"ontology_binding_json"` + State string `gorm:"size:16;not null;default:active;index" json:"state"` + SortOrder int `gorm:"not null;default:0;index" json:"sort_order"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } func (SkillDefinition) TableName() string { return "skill_definition" } diff --git a/eai_agentplatform/backend-go/internal/model/skill_keys.go b/eai_agentplatform/backend-go/internal/model/skill_keys.go index 954932a..7f75c59 100644 --- a/eai_agentplatform/backend-go/internal/model/skill_keys.go +++ b/eai_agentplatform/backend-go/internal/model/skill_keys.go @@ -9,7 +9,7 @@ import ( // ValidSkillKeys 技能 key 全集,用于校验 Specialist.AllowedSkills。 // -// 权威来源是前端 frontend/src/config/workbench.js 的 availableSkills —— +// 权威来源是前端 frontend/src/config/workbench.js 的 staticSkillCatalog —— // 技能实现仍归前端,后端只持有 key 清单,用于录入时挡住拼写错误。 // 两个清单的一致性由 skill_keys_test.go 守着,改动任一侧都会让测试失败。 // diff --git a/eai_agentplatform/backend-go/internal/model/skill_keys_test.go b/eai_agentplatform/backend-go/internal/model/skill_keys_test.go index 2a3b1cc..c05e7bd 100644 --- a/eai_agentplatform/backend-go/internal/model/skill_keys_test.go +++ b/eai_agentplatform/backend-go/internal/model/skill_keys_test.go @@ -12,11 +12,11 @@ import ( // workbenchJSRelPath 前端技能目录的相对路径(相对本包目录)。 const workbenchJSRelPath = "../../../frontend/src/config/workbench.js" -// createSkillDefinitionRe 抓 availableSkills 数组里每个 createSkillDefinition 的字面量 key。 +// createSkillDefinitionRe 抓 staticSkillCatalog 数组里每个 createSkillDefinition 的字面量 key。 // 依赖 workbench.js 里「createSkillDefinition({ 紧跟 key: '…'」这一写法。 var createSkillDefinitionRe = regexp.MustCompile(`createSkillDefinition\(\{\s*key:\s*'([^']+)'`) -// TestValidSkillKeysMatchFrontend 守住 backend 的 ValidSkillKeys 与前端 availableSkills 一致。 +// TestValidSkillKeysMatchFrontend 守住 backend 的 ValidSkillKeys 与前端 staticSkillCatalog 一致。 // // 这两份清单天然会漂移:技能实现在前端,后端只持有 key 做录入校验。任何一侧增删技能 // 而忘了另一侧,这里就会红——这正是本测试存在的唯一理由。 @@ -54,11 +54,11 @@ func TestValidSkillKeysMatchFrontend(t *testing.T) { sort.Strings(onlyFrontend) if len(onlyBackend) > 0 { - t.Errorf("ValidSkillKeys 里有、前端 availableSkills 里没有的 key(后端多写或前端已删):\n %s", + t.Errorf("ValidSkillKeys 里有、前端 staticSkillCatalog 里没有的 key(后端多写或前端已删):\n %s", strings.Join(onlyBackend, "\n ")) } if len(onlyFrontend) > 0 { - t.Errorf("前端 availableSkills 里有、ValidSkillKeys 里没有的 key(新增技能后忘了补后端清单):\n %s", + t.Errorf("前端 staticSkillCatalog 里有、ValidSkillKeys 里没有的 key(新增技能后忘了补后端清单):\n %s", strings.Join(onlyFrontend, "\n ")) } } diff --git a/eai_agentplatform/backend-go/internal/model/specialist.go b/eai_agentplatform/backend-go/internal/model/specialist.go index 83f054a..bf5c0e8 100644 --- a/eai_agentplatform/backend-go/internal/model/specialist.go +++ b/eai_agentplatform/backend-go/internal/model/specialist.go @@ -4,39 +4,39 @@ import "time" // Specialist 数字员工专员目录 type Specialist struct { - ID uint `gorm:"primaryKey" json:"id"` - Key string `gorm:"size:64;uniqueIndex;not null" json:"key"` - Label string `gorm:"size:128;not null" json:"label"` - DisplayCode string `gorm:"column:display_code;size:16;default:'';index" json:"display_code"` - EAILogicCode string `gorm:"column:eailogic_code;size:32;default:'';index" json:"eailogic_code"` - Tier string `gorm:"size:16;not null;index" json:"tier"` // generic / industry - WorkerType string `gorm:"size:16;not null;default:dw;index" json:"worker_type"` - ObjectEntryRoute string `gorm:"column:object_entry_route;size:128;not null;default:''" json:"object_entry_route"` - Summary string `gorm:"type:text" json:"summary"` - RoleCardJSON string `gorm:"type:text" json:"role_card_json"` - WorkStatus string `gorm:"size:64;default:''" json:"work_status"` - RiskLabel string `gorm:"size:64;default:''" json:"risk_label"` - Color string `gorm:"size:16;default:''" json:"color"` - Stage string `gorm:"size:64;default:''" json:"stage"` - Progress int `gorm:"not null;default:0" json:"progress"` - MarketTag string `gorm:"size:32;not null;default:installed;index" json:"market_tag"` // 已安装 / 可升级 / 试用 - Version string `gorm:"size:32;default:''" json:"version"` - ConnectorScope string `gorm:"type:text" json:"connector_scope"` - PermissionScope string `gorm:"type:text" json:"permission_scope"` - ResourceBindings string `gorm:"type:text" json:"resource_bindings"` - InfoSources string `gorm:"type:text" json:"info_sources"` - BaseSkills string `gorm:"type:text" json:"base_skills"` - AIAssistance string `gorm:"type:text" json:"ai_assistance"` - GeneratedSkills string `gorm:"type:text" json:"generated_skills"` + ID uint `gorm:"primaryKey" json:"id"` + Key string `gorm:"size:64;uniqueIndex;not null" json:"key"` + Label string `gorm:"size:128;not null" json:"label"` + DisplayCode string `gorm:"column:display_code;size:16;default:'';index" json:"display_code"` + EAILogicCode string `gorm:"column:eailogic_code;size:32;default:'';index" json:"eailogic_code"` + Tier string `gorm:"size:16;not null;index" json:"tier"` // generic / industry + WorkerType string `gorm:"size:16;not null;default:dw;index" json:"worker_type"` + ObjectEntryRoute string `gorm:"column:object_entry_route;size:128;not null;default:''" json:"object_entry_route"` + Summary string `gorm:"type:text" json:"summary"` + InteractionCardJSON string `gorm:"column:interaction_card_json;type:text" json:"interaction_card_json"` + WorkStatus string `gorm:"size:64;default:''" json:"work_status"` + RiskLabel string `gorm:"size:64;default:''" json:"risk_label"` + Color string `gorm:"size:16;default:''" json:"color"` + Stage string `gorm:"size:64;default:''" json:"stage"` + Progress int `gorm:"not null;default:0" json:"progress"` + MarketTag string `gorm:"size:32;not null;default:installed;index" json:"market_tag"` // 已安装 / 可升级 / 试用 + Version string `gorm:"size:32;default:''" json:"version"` + ConnectorScope string `gorm:"type:text" json:"connector_scope"` + PermissionScope string `gorm:"type:text" json:"permission_scope"` + ResourceBindings string `gorm:"type:text" json:"resource_bindings"` + InfoSources string `gorm:"type:text" json:"info_sources"` + BaseSkills string `gorm:"type:text" json:"base_skills"` + AIAssistance string `gorm:"type:text" json:"ai_assistance"` + GeneratedSkills string `gorm:"type:text" json:"generated_skills"` // RuleFileMarkdown 岗位说明书正文(Markdown 全文)。 // 会话创建时注入 System Prompt,决定这个专员怎么说话、怎么推进、什么时候调用哪个技能。 // 参考 AionCore builtin-assistants 的 rule_file;命名对齐 SY23。 RuleFileMarkdown string `gorm:"type:text" json:"rule_file_markdown"` // AllowedSkills 绑定技能 key 列表,JSON 字符串数组,顺序即优先级(首个为主技能)。 - // 元素取值须通过 model.ValidSkillKeys 校验,对齐前端 availableSkills[].key。 + // 元素取值须通过 model.ValidSkillKeys 校验,对齐前端 staticSkillCatalog[].key。 // 字段名对齐 SY22 §6.1 对象 manifest 的 allowed_skills。 AllowedSkills string `gorm:"type:text" json:"allowed_skills"` - InputsRecordsJSON string `gorm:"column:source_records_json;type:text" json:"inputs_records_json"` + InputsRecordsJSON string `gorm:"column:inputs_records_json;type:text" json:"inputs_records_json"` PermissionRecordsJSON string `gorm:"type:text" json:"permission_records_json"` ActionRecordsJSON string `gorm:"type:text" json:"action_records_json"` ResultRecordsJSON string `gorm:"type:text" json:"result_records_json"` diff --git a/eai_agentplatform/backend-go/internal/store/db.go b/eai_agentplatform/backend-go/internal/store/db.go index efa5146..311a9a2 100644 --- a/eai_agentplatform/backend-go/internal/store/db.go +++ b/eai_agentplatform/backend-go/internal/store/db.go @@ -62,6 +62,7 @@ func Init(dbPath string) (*gorm.DB, error) { &model.WorkerArtifact{}, &model.WorkerRun{}, &model.UserAppCenter{}, + &model.AppDefinition{}, &model.OfficialAccountArticle{}, &model.OfficialAccountHotspot{}, ); err != nil { @@ -71,6 +72,12 @@ func Init(dbPath string) (*gorm.DB, error) { if err := migrateObjectEntryRouteColumns(db); err != nil { return nil, err } + if err := migrateSpecialistStructuredColumns(db); err != nil { + return nil, err + } + if err := migrateSkillObjectKindColumn(db); err != nil { + return nil, err + } DB = db return db, nil @@ -127,24 +134,6 @@ func migrateObjectEntryRouteColumns(db *gorm.DB) error { } } } - if db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_object_entry_route") { - skillLegacyObjectExpr := coalesceRouteExpr( - db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_entry_route"), - "legacy_entry_route", - db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_route"), - "legacy_route", - ) - if skillLegacyObjectExpr != "" { - if err := db.Exec(fmt.Sprintf(` - UPDATE skill_definition - SET legacy_object_entry_route = %s - WHERE COALESCE(legacy_object_entry_route, '') = '' - AND COALESCE(%s, '') <> '' - `, skillLegacyObjectExpr, skillLegacyObjectExpr)).Error; err != nil { - return err - } - } - } if db.Migrator().HasColumn(&model.SkillDefinition{}, "entry_route") { if err := dropColumn(db, "skill_definition", "entry_route"); err != nil { return err @@ -165,11 +154,53 @@ func migrateObjectEntryRouteColumns(db *gorm.DB) error { return err } } + if db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_object_entry_route") { + if err := dropColumn(db, "skill_definition", "legacy_object_entry_route"); err != nil { + return err + } + } } return nil } +func migrateSpecialistStructuredColumns(db *gorm.DB) error { + if !db.Migrator().HasTable(&model.Specialist{}) { + return nil + } + if err := migrateColumnData(db, "specialist", "interaction_card_json", "role_card_json"); err != nil { + return err + } + if err := migrateColumnData(db, "specialist", "inputs_records_json", "source_records_json"); err != nil { + return err + } + return nil +} + +func migrateSkillObjectKindColumn(db *gorm.DB) error { + if !db.Migrator().HasTable(&model.SkillDefinition{}) { + return nil + } + return migrateColumnData(db, "skill_definition", "object_kind", "role_kind") +} + +func migrateColumnData(db *gorm.DB, tableName, targetColumn, legacyColumn string) error { + if !db.Migrator().HasColumn(tableName, legacyColumn) { + return nil + } + if db.Migrator().HasColumn(tableName, targetColumn) { + if err := db.Exec(fmt.Sprintf(` + UPDATE %s + SET %s = %s + WHERE COALESCE(%s, '') = '' + AND COALESCE(%s, '') <> '' + `, tableName, targetColumn, legacyColumn, targetColumn, legacyColumn)).Error; err != nil { + return err + } + } + return dropColumn(db, tableName, legacyColumn) +} + func coalesceRouteExpr(primaryExists bool, primaryColumn string, fallbackExists bool, fallbackColumn string) string { parts := make([]string, 0, 2) if primaryExists { diff --git a/eai_agentplatform/backend-go/internal/store/db_migration_test.go b/eai_agentplatform/backend-go/internal/store/db_migration_test.go index ca8f9a4..4424464 100644 --- a/eai_agentplatform/backend-go/internal/store/db_migration_test.go +++ b/eai_agentplatform/backend-go/internal/store/db_migration_test.go @@ -44,6 +44,9 @@ func TestMigrateObjectEntryRouteColumnsDropsLegacyColumns(t *testing.T) { if err := db.Exec(`ALTER TABLE skill_definition ADD COLUMN legacy_route TEXT DEFAULT ''`).Error; err != nil { t.Fatalf("add skill_definition.legacy_route: %v", err) } + if err := db.Exec(`ALTER TABLE skill_definition ADD COLUMN legacy_object_entry_route TEXT DEFAULT ''`).Error; err != nil { + t.Fatalf("add skill_definition.legacy_object_entry_route: %v", err) + } specialist := model.Specialist{ Key: "spec-demo", @@ -62,13 +65,12 @@ func TestMigrateObjectEntryRouteColumnsDropsLegacyColumns(t *testing.T) { } skill := model.SkillDefinition{ - Key: "skill-demo", - Label: "技能演示", - RoleKind: "skill", - Source: "eai", - ObjectEntryRoute: "", - LegacyObjectEntryRoute: "", - State: "active", + Key: "skill-demo", + Label: "技能演示", + ObjectKind: "skill", + Source: "eai", + ObjectEntryRoute: "", + State: "active", } if err := db.Create(&skill).Error; err != nil { t.Fatalf("create skill: %v", err) @@ -98,9 +100,6 @@ func TestMigrateObjectEntryRouteColumnsDropsLegacyColumns(t *testing.T) { if migratedSkill.ObjectEntryRoute != "/legacy-skill" { t.Fatalf("skill object_entry_route = %q, want %q", migratedSkill.ObjectEntryRoute, "/legacy-skill") } - if migratedSkill.LegacyObjectEntryRoute != "/legacy-old-skill" { - t.Fatalf("skill legacy_object_entry_route = %q, want %q", migratedSkill.LegacyObjectEntryRoute, "/legacy-old-skill") - } if db.Migrator().HasColumn(&model.Specialist{}, "entry_route") { t.Fatalf("specialist.entry_route should be dropped") @@ -120,4 +119,101 @@ func TestMigrateObjectEntryRouteColumnsDropsLegacyColumns(t *testing.T) { if db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_route") { t.Fatalf("skill_definition.legacy_route should be dropped") } + if db.Migrator().HasColumn("skill_definition", "legacy_object_entry_route") { + t.Fatalf("skill_definition.legacy_object_entry_route should be dropped") + } +} + +func TestMigrateSpecialistStructuredColumnsDropsLegacyColumns(t *testing.T) { + db := openMigrationTestDB(t) + if err := db.AutoMigrate(&model.Specialist{}); err != nil { + t.Fatalf("auto migrate: %v", err) + } + + if err := db.Exec(`ALTER TABLE specialist ADD COLUMN role_card_json TEXT DEFAULT ''`).Error; err != nil { + t.Fatalf("add specialist.role_card_json: %v", err) + } + if err := db.Exec(`ALTER TABLE specialist ADD COLUMN source_records_json TEXT DEFAULT ''`).Error; err != nil { + t.Fatalf("add specialist.source_records_json: %v", err) + } + + item := model.Specialist{ + Key: "spec-structured", + Label: "结构化专员", + Tier: "generic", + WorkerType: "dw", + ObjectEntryRoute: "/home", + State: "active", + MarketTag: "installed", + } + if err := db.Create(&item).Error; err != nil { + t.Fatalf("create specialist: %v", err) + } + if err := db.Exec(`UPDATE specialist SET interaction_card_json = '', role_card_json = ?, inputs_records_json = '', source_records_json = ? WHERE id = ?`, + `{"greeting":"hello"}`, `[{"input_name":"crm"}]`, item.ID, + ).Error; err != nil { + t.Fatalf("seed specialist legacy structured columns: %v", err) + } + + if err := migrateSpecialistStructuredColumns(db); err != nil { + t.Fatalf("migrate specialist structured columns: %v", err) + } + + var migrated model.Specialist + if err := db.First(&migrated, item.ID).Error; err != nil { + t.Fatalf("reload specialist: %v", err) + } + if migrated.InteractionCardJSON != `{"greeting":"hello"}` { + t.Fatalf("interaction_card_json = %q", migrated.InteractionCardJSON) + } + if migrated.InputsRecordsJSON != `[{"input_name":"crm"}]` { + t.Fatalf("inputs_records_json = %q", migrated.InputsRecordsJSON) + } + if db.Migrator().HasColumn(&model.Specialist{}, "role_card_json") { + t.Fatalf("specialist.role_card_json should be dropped") + } + if db.Migrator().HasColumn(&model.Specialist{}, "source_records_json") { + t.Fatalf("specialist.source_records_json should be dropped") + } +} + +func TestMigrateSkillObjectKindColumnDropsLegacyColumn(t *testing.T) { + db := openMigrationTestDB(t) + if err := db.AutoMigrate(&model.SkillDefinition{}); err != nil { + t.Fatalf("auto migrate: %v", err) + } + + if err := db.Exec(`ALTER TABLE skill_definition ADD COLUMN role_kind TEXT DEFAULT ''`).Error; err != nil { + t.Fatalf("add skill_definition.role_kind: %v", err) + } + + item := model.SkillDefinition{ + Key: "skill-kind-demo", + Label: "技能类型演示", + ObjectKind: "", + Source: "eai", + ObjectEntryRoute: "/home", + State: "active", + } + if err := db.Create(&item).Error; err != nil { + t.Fatalf("create skill: %v", err) + } + if err := db.Exec(`UPDATE skill_definition SET object_kind = '', role_kind = ? WHERE id = ?`, "skill", item.ID).Error; err != nil { + t.Fatalf("seed skill legacy object kind: %v", err) + } + + if err := migrateSkillObjectKindColumn(db); err != nil { + t.Fatalf("migrate skill object kind column: %v", err) + } + + var migrated model.SkillDefinition + if err := db.First(&migrated, item.ID).Error; err != nil { + t.Fatalf("reload skill: %v", err) + } + if migrated.ObjectKind != "skill" { + t.Fatalf("object_kind = %q", migrated.ObjectKind) + } + if db.Migrator().HasColumn(&model.SkillDefinition{}, "role_kind") { + t.Fatalf("skill_definition.role_kind should be dropped") + } } diff --git a/eai_agentplatform/backend-go/internal/store/seed.go b/eai_agentplatform/backend-go/internal/store/seed.go index 4a507e2..5ed2f34 100644 --- a/eai_agentplatform/backend-go/internal/store/seed.go +++ b/eai_agentplatform/backend-go/internal/store/seed.go @@ -97,6 +97,9 @@ func SeedDefaults() error { if err := seedActionDefinitions(); err != nil { return err } + if err := seedAppDefinitions(); err != nil { + return err + } return nil } @@ -322,6 +325,19 @@ func applySkillCodes(items []model.SkillDefinition) []model.SkillDefinition { return items } +func applyAppCodes(items []model.AppDefinition) []model.AppDefinition { + for i := range items { + suffix := fmt.Sprintf("%02d", i+1) + if strings.TrimSpace(items[i].DisplayCode) == "" { + items[i].DisplayCode = "应" + suffix + } + if strings.TrimSpace(items[i].EAILogicCode) == "" { + items[i].EAILogicCode = "EAI-A-" + suffix + } + } + return items +} + func seedSpecialists() error { items := applySpecialistCodes([]model.Specialist{ { @@ -582,6 +598,30 @@ func seedSpecialists() error { State: "active", SortOrder: 90, }, + { + Key: "presentation-briefing", + Label: "汇报专员", + Tier: "generic", + WorkerType: "dw", + ObjectEntryRoute: "/apps/presentation-briefing", + Summary: "经营汇报、方案汇报与培训课件统一出稿", + WorkStatus: "2 个待成稿", + RiskLabel: "1 个页数待压缩", + Color: "#4f46e5", + Stage: "演示编排", + Progress: 68, + MarketTag: "已安装", + Version: "v1.0", + ConnectorScope: "连接任务结果 / 知识库 / 模板中心", + PermissionScope: "读取材料、生成演示稿、导出讲稿备注", + ResourceBindings: "演示模板、知识库、任务产物、汇报素材", + InfoSources: "主题目标、受众、汇报材料、页数限制", + BaseSkills: "演示结构规划、页面要点提炼、讲稿备注生成、口径统一", + AIAssistance: "自动压缩材料并给出适合管理层、客户或培训场景的页面结构", + GeneratedSkills: "把高质量汇报结构沉淀为可复用的演示模板", + State: "active", + SortOrder: 100, + }, }) // 岗位说明书与技能绑定按 key 挂载(见 seed_specialist_rules.go) if err := applySpecialistRuleFiles(items); err != nil { @@ -608,8 +648,8 @@ func seedSpecialists() error { if existing.WorkerType == "" { updates["worker_type"] = item.WorkerType } - if existing.RoleCardJSON == "" { - updates["role_card_json"] = item.RoleCardJSON + if existing.InteractionCardJSON == "" { + updates["interaction_card_json"] = item.InteractionCardJSON } if existing.PermissionScope == "" { updates["permission_scope"] = item.PermissionScope @@ -637,7 +677,7 @@ func seedSpecialists() error { updates["allowed_skills"] = item.AllowedSkills } if existing.InputsRecordsJSON == "" { - updates["source_records_json"] = item.InputsRecordsJSON + updates["inputs_records_json"] = item.InputsRecordsJSON } if existing.PermissionRecordsJSON == "" { updates["permission_records_json"] = item.PermissionRecordsJSON @@ -666,7 +706,7 @@ func seedSkillDefinitions() error { Key: "smart-assistant", Label: "通用助手", Description: "默认协作入口,负责问题梳理、任务拆解和对话推进。", - RoleKind: "assistant", + ObjectKind: "assistant", Source: "eai", ObjectEntryRoute: "/home", ExposedToUser: true, @@ -681,118 +721,112 @@ func seedSkillDefinitions() error { SortOrder: 10, }, { - Key: "document-translate", - Label: "文档翻译", - Description: "把文本和文档稳定翻成目标语言,并保留语气和结构。", - RoleKind: "skill", - Source: "eai", - ObjectEntryRoute: "/home", - LegacyObjectEntryRoute: "/tools/document-translate", - ExposedToUser: true, - StarterPromptsJSON: mustJSON([]string{"把这段产品说明翻译成英文,保持专业语气", "把这封邮件翻译成日文,语气礼貌一点", "把这份中文文档整理成英文摘要"}), - PromptTemplate: "明确源语言、目标语言、语气和输出格式后执行翻译。", - ActionRefsJSON: mustJSON([]string{"document.translate"}), - InputSchemaJSON: mustJSON(map[string]any{"content": "text_or_document", "target_language": "string"}), - OutputSchemaJSON: mustJSON(map[string]any{"translation": "text"}), - ArtifactSchemaJSON: mustJSON(map[string]any{"artifacts": []string{"translated_document"}}), - OntologyBindingJSON: mustJSON(map[string]any{"object_types": []string{"document"}, "action_types": []string{"translate"}}), - State: "active", - SortOrder: 20, + Key: "document-translate", + Label: "文档翻译", + Description: "把文本和文档稳定翻成目标语言,并保留语气和结构。", + ObjectKind: "skill", + Source: "eai", + ObjectEntryRoute: "/home", + ExposedToUser: true, + StarterPromptsJSON: mustJSON([]string{"把这段产品说明翻译成英文,保持专业语气", "把这封邮件翻译成日文,语气礼貌一点", "把这份中文文档整理成英文摘要"}), + PromptTemplate: "明确源语言、目标语言、语气和输出格式后执行翻译。", + ActionRefsJSON: mustJSON([]string{"document.translate"}), + InputSchemaJSON: mustJSON(map[string]any{"content": "text_or_document", "target_language": "string"}), + OutputSchemaJSON: mustJSON(map[string]any{"translation": "text"}), + ArtifactSchemaJSON: mustJSON(map[string]any{"artifacts": []string{"translated_document"}}), + OntologyBindingJSON: mustJSON(map[string]any{"object_types": []string{"document"}, "action_types": []string{"translate"}}), + State: "active", + SortOrder: 20, }, { - Key: "copy-proofreading", - Label: "文案校对", - Description: "检查错别字、语病和表达不顺,输出润色建议。", - RoleKind: "skill", - Source: "eai", - ObjectEntryRoute: "/home", - LegacyObjectEntryRoute: "/tools/copy-proofreading", - ExposedToUser: true, - StarterPromptsJSON: mustJSON([]string{"检查这段销售话术里的错别字和语病", "把这段产品介绍润得更正式一点", "帮我校对这封对外邮件,保留原意"}), - PromptTemplate: "围绕原文语气给出校对与改写结果。", - ActionRefsJSON: mustJSON([]string{"copy.proofread"}), - InputSchemaJSON: mustJSON(map[string]any{"content": "text"}), - OutputSchemaJSON: mustJSON(map[string]any{"proofread_result": "text"}), - ArtifactSchemaJSON: mustJSON(map[string]any{"artifacts": []string{"clean_copy"}}), - OntologyBindingJSON: mustJSON(map[string]any{"object_types": []string{"copy"}, "action_types": []string{"review"}}), - State: "active", - SortOrder: 30, + Key: "copy-proofreading", + Label: "文案校对", + Description: "检查错别字、语病和表达不顺,输出润色建议。", + ObjectKind: "skill", + Source: "eai", + ObjectEntryRoute: "/home", + ExposedToUser: true, + StarterPromptsJSON: mustJSON([]string{"检查这段销售话术里的错别字和语病", "把这段产品介绍润得更正式一点", "帮我校对这封对外邮件,保留原意"}), + PromptTemplate: "围绕原文语气给出校对与改写结果。", + ActionRefsJSON: mustJSON([]string{"copy.proofread"}), + InputSchemaJSON: mustJSON(map[string]any{"content": "text"}), + OutputSchemaJSON: mustJSON(map[string]any{"proofread_result": "text"}), + ArtifactSchemaJSON: mustJSON(map[string]any{"artifacts": []string{"clean_copy"}}), + OntologyBindingJSON: mustJSON(map[string]any{"object_types": []string{"copy"}, "action_types": []string{"review"}}), + State: "active", + SortOrder: 30, }, { - Key: "audio-transcribe", - Label: "语音转写", - Description: "把音频内容转成结构化文本,便于继续总结与提取。", - RoleKind: "skill", - Source: "eai", - ObjectEntryRoute: "/home", - LegacyObjectEntryRoute: "/tools/audio-transcribe", - ExposedToUser: true, - StarterPromptsJSON: mustJSON([]string{"把这段会议录音转成文字,并分段整理", "先转写这段采访,再提取重点", "把语音内容整理成可发群里的纪要"}), - PromptTemplate: "先转写音频,再按需要输出整理版文本。", - ActionRefsJSON: mustJSON([]string{"audio.transcribe"}), - InputSchemaJSON: mustJSON(map[string]any{"audio": "file_or_url"}), - OutputSchemaJSON: mustJSON(map[string]any{"transcript": "text"}), - ArtifactSchemaJSON: mustJSON(map[string]any{"artifacts": []string{"transcript"}}), - OntologyBindingJSON: mustJSON(map[string]any{"object_types": []string{"audio"}, "action_types": []string{"extract"}}), - State: "active", - SortOrder: 40, + Key: "audio-transcribe", + Label: "语音转写", + Description: "把音频内容转成结构化文本,便于继续总结与提取。", + ObjectKind: "skill", + Source: "eai", + ObjectEntryRoute: "/home", + ExposedToUser: true, + StarterPromptsJSON: mustJSON([]string{"把这段会议录音转成文字,并分段整理", "先转写这段采访,再提取重点", "把语音内容整理成可发群里的纪要"}), + PromptTemplate: "先转写音频,再按需要输出整理版文本。", + ActionRefsJSON: mustJSON([]string{"audio.transcribe"}), + InputSchemaJSON: mustJSON(map[string]any{"audio": "file_or_url"}), + OutputSchemaJSON: mustJSON(map[string]any{"transcript": "text"}), + ArtifactSchemaJSON: mustJSON(map[string]any{"artifacts": []string{"transcript"}}), + OntologyBindingJSON: mustJSON(map[string]any{"object_types": []string{"audio"}, "action_types": []string{"extract"}}), + State: "active", + SortOrder: 40, }, { - Key: "batch-extract", - Label: "批量提取", - Description: "从一批内容里抽取结构化字段和关键信息。", - RoleKind: "skill", - Source: "eai", - ObjectEntryRoute: "/home", - LegacyObjectEntryRoute: "/tools/batch-extract", - ExposedToUser: true, - StarterPromptsJSON: mustJSON([]string{"从这批简历里提取姓名、岗位和年限", "把这些合同里的付款条款统一抽出来", "帮我批量提取文章标题、作者和发布时间"}), - PromptTemplate: "明确字段范围、输出格式和异常处理规则后执行批量提取。", - ActionRefsJSON: mustJSON([]string{"batch.extract"}), - InputSchemaJSON: mustJSON(map[string]any{"items": []string{"text_or_document"}, "fields": []string{"string"}}), - OutputSchemaJSON: mustJSON(map[string]any{"rows": []string{"object"}}), - ArtifactSchemaJSON: mustJSON(map[string]any{"artifacts": []string{"spreadsheet", "json"}}), - OntologyBindingJSON: mustJSON(map[string]any{"object_types": []string{"document", "resume", "contract"}, "action_types": []string{"extract"}}), - State: "active", - SortOrder: 50, + Key: "batch-extract", + Label: "批量提取", + Description: "从一批内容里抽取结构化字段和关键信息。", + ObjectKind: "skill", + Source: "eai", + ObjectEntryRoute: "/home", + ExposedToUser: true, + StarterPromptsJSON: mustJSON([]string{"从这批简历里提取姓名、岗位和年限", "把这些合同里的付款条款统一抽出来", "帮我批量提取文章标题、作者和发布时间"}), + PromptTemplate: "明确字段范围、输出格式和异常处理规则后执行批量提取。", + ActionRefsJSON: mustJSON([]string{"batch.extract"}), + InputSchemaJSON: mustJSON(map[string]any{"items": []string{"text_or_document"}, "fields": []string{"string"}}), + OutputSchemaJSON: mustJSON(map[string]any{"rows": []string{"object"}}), + ArtifactSchemaJSON: mustJSON(map[string]any{"artifacts": []string{"spreadsheet", "json"}}), + OntologyBindingJSON: mustJSON(map[string]any{"object_types": []string{"document", "resume", "contract"}, "action_types": []string{"extract"}}), + State: "active", + SortOrder: 50, }, { - Key: "contract-review", - Label: "合同审查", - Description: "定位风险条款、待确认项并输出审查建议。", - RoleKind: "skill", - Source: "eai", - ObjectEntryRoute: "/home", - LegacyObjectEntryRoute: "/tools/contract-review", - ExposedToUser: true, - StarterPromptsJSON: mustJSON([]string{"审查这份采购合同的风险点", "重点看赔偿责任和终止条款", "帮我整理一版待人工确认的风险清单"}), - PromptTemplate: "按照甲方风控视角识别风险条款、例外项和红线建议。", - ActionRefsJSON: mustJSON([]string{"contract.review"}), - InputSchemaJSON: mustJSON(map[string]any{"contract": "document_or_text"}), - OutputSchemaJSON: mustJSON(map[string]any{"risk_report": "text"}), - ArtifactSchemaJSON: mustJSON(map[string]any{"artifacts": []string{"risk_report", "redline"}}), - OntologyBindingJSON: mustJSON(map[string]any{"object_types": []string{"contract"}, "action_types": []string{"review", "analyze"}}), - State: "active", - SortOrder: 60, + Key: "contract-review", + Label: "合同审查", + Description: "定位风险条款、待确认项并输出审查建议。", + ObjectKind: "skill", + Source: "eai", + ObjectEntryRoute: "/home", + ExposedToUser: true, + StarterPromptsJSON: mustJSON([]string{"审查这份采购合同的风险点", "重点看赔偿责任和终止条款", "帮我整理一版待人工确认的风险清单"}), + PromptTemplate: "按照甲方风控视角识别风险条款、例外项和红线建议。", + ActionRefsJSON: mustJSON([]string{"contract.review"}), + InputSchemaJSON: mustJSON(map[string]any{"contract": "document_or_text"}), + OutputSchemaJSON: mustJSON(map[string]any{"risk_report": "text"}), + ArtifactSchemaJSON: mustJSON(map[string]any{"artifacts": []string{"risk_report", "redline"}}), + OntologyBindingJSON: mustJSON(map[string]any{"object_types": []string{"contract"}, "action_types": []string{"review", "analyze"}}), + State: "active", + SortOrder: 60, }, { - Key: "report-generation", - Label: "报告生成", - Description: "把过程材料整理成日报、周报、复盘和交付说明。", - RoleKind: "skill", - Source: "eai", - ObjectEntryRoute: "/home", - LegacyObjectEntryRoute: "/report-gen", - ExposedToUser: true, - StarterPromptsJSON: mustJSON([]string{"把今天的工作记录整理成日报", "根据这些节点生成项目周报", "帮我把这次交付过程整理成复盘说明"}), - PromptTemplate: "先梳理结论与结构,再输出对应报告版本。", - ActionRefsJSON: mustJSON([]string{"report.generate"}), - InputSchemaJSON: mustJSON(map[string]any{"materials": []string{"text_or_document"}, "report_type": "string"}), - OutputSchemaJSON: mustJSON(map[string]any{"report": "text"}), - ArtifactSchemaJSON: mustJSON(map[string]any{"artifacts": []string{"report"}}), - OntologyBindingJSON: mustJSON(map[string]any{"object_types": []string{"report", "task"}, "action_types": []string{"draft"}}), - State: "active", - SortOrder: 70, + Key: "report-generation", + Label: "报告生成", + Description: "把过程材料整理成日报、周报、复盘和交付说明。", + ObjectKind: "skill", + Source: "eai", + ObjectEntryRoute: "/home", + ExposedToUser: true, + StarterPromptsJSON: mustJSON([]string{"把今天的工作记录整理成日报", "根据这些节点生成项目周报", "帮我把这次交付过程整理成复盘说明"}), + PromptTemplate: "先梳理结论与结构,再输出对应报告版本。", + ActionRefsJSON: mustJSON([]string{"report.generate"}), + InputSchemaJSON: mustJSON(map[string]any{"materials": []string{"text_or_document"}, "report_type": "string"}), + OutputSchemaJSON: mustJSON(map[string]any{"report": "text"}), + ArtifactSchemaJSON: mustJSON(map[string]any{"artifacts": []string{"report"}}), + OntologyBindingJSON: mustJSON(map[string]any{"object_types": []string{"report", "task"}, "action_types": []string{"draft"}}), + State: "active", + SortOrder: 70, }, }) @@ -817,9 +851,6 @@ func seedSkillDefinitions() error { if existing.ObjectEntryRoute == "" { updates["object_entry_route"] = item.ObjectEntryRoute } - if existing.LegacyObjectEntryRoute == "" { - updates["legacy_object_entry_route"] = item.LegacyObjectEntryRoute - } if existing.StarterPromptsJSON == "" { updates["starter_prompts_json"] = item.StarterPromptsJSON } @@ -990,6 +1021,337 @@ func seedActionDefinitions() error { return nil } +func seedAppDefinitions() error { + items := applyAppCodes([]model.AppDefinition{ + { + Key: "knowledge-hub", + Label: "知识库", + Badge: "内建 APP", + Kind: "组织级 APP", + MarketTag: "内建 APP", + WorkerType: "adw", + Tier: "platform", + Source: "eai", + Color: "#5b7cff", + IconText: "知", + CoverTone: "linear-gradient(135deg, #5b7cff 0%, #8fb4ff 100%)", + Summary: "组织级默认存在的知识底座 APP,为专员、技能与其它 APP 提供知识支持。", + Description: "知识库是平台里的知识底座,用来沉淀资料、规则和证据,并为专员、技能和 APP 提供统一知识支持。", + OpenRoute: "/knowledge-hub", + DefaultPrompt: "我想先整理这批知识资料", + PromptsJSON: mustJSON([]string{"我想先整理这批知识资料", "帮我看看这部分内容适合怎么入库", "知识库更适合承接哪些任务"}), + TagsJSON: mustJSON([]string{"知识沉淀", "证据引用", "组织底座"}), + InstallState: "installed", + ExposedToUser: true, + State: "active", + SortOrder: 10, + }, + { + Key: "internal-exam", + Label: "内部考试APP", + Badge: "长程 APP", + Kind: "内部考试", + MarketTag: "长程 APP", + WorkerType: "adw", + Tier: "business", + Source: "eai", + Color: "#29b36a", + IconText: "考", + CoverTone: "linear-gradient(135deg, #1c9f5a 0%, #6ee7b7 100%)", + Summary: "围绕练习、自测、正式考试、记录、证书和岗位应学组织的内部考试 APP。", + Description: "内部考试 APP 用来承接训练、练习、考试、成绩记录和证书结果,适合需要完整过程管理的学习场景。", + OpenRoute: "/exam/self-test", + DefaultPrompt: "我想组织一轮内部考试", + PromptsJSON: mustJSON([]string{"我想组织一轮内部考试", "帮我看看这个考试流程怎么搭", "这个场景适合放到内部考试 APP 吗"}), + TagsJSON: mustJSON([]string{"训练流程", "考试记录", "结果沉淀"}), + InstallState: "installed", + ExposedToUser: true, + State: "active", + SortOrder: 20, + }, + { + Key: "internal-training", + Label: "内部培训APP", + Badge: "长程 APP", + Kind: "内部培训", + MarketTag: "长程 APP", + WorkerType: "worker", + Tier: "business", + Source: "eai", + Color: "#9b6bff", + IconText: "培", + CoverTone: "linear-gradient(135deg, #8b5cf6 0%, #c4b5fd 100%)", + Summary: "统一承接销售培训、产品知识和公司介绍三个培训模块的内部培训 APP。", + Description: "内部培训 APP 用来把销售培训、产品知识和公司介绍收在同一个对象下面,避免把同一条培训链拆成多个 APP。", + OpenRoute: "/courses", + DefaultPrompt: "我想搭一个内部培训 APP", + PromptsJSON: mustJSON([]string{"我想搭一个内部培训 APP", "帮我梳理销售培训、产品知识和公司介绍的关系", "这个培训主题应该挂到内部培训 APP 吗"}), + TagsJSON: mustJSON([]string{"课程组织", "产品知识", "组织导览"}), + InstallState: "installed", + ExposedToUser: true, + State: "active", + SortOrder: 30, + }, + { + Key: "docs-center", + Label: "文档中心", + Badge: "办公中枢", + Kind: "结果沉淀", + MarketTag: "办公中枢", + WorkerType: "adw", + Tier: "business", + Source: "eai", + Color: "#2563eb", + IconText: "档", + CoverTone: "linear-gradient(135deg, #2563eb 0%, #60a5fa 100%)", + Summary: "集中查看任务上传物与产物,把结果留住、找得到、能继续复用。", + Description: "把任务里产生的上传材料和交付结果集中收在一个地方,方便继续搜索、回看、复制和打开原任务。", + OpenRoute: "/docs-center", + DefaultPrompt: "看我最近生成过哪些文档和产物", + PromptsJSON: mustJSON([]string{"看我最近生成过哪些文档和产物", "按任务回看最近交付结果", "集中查看上传材料和生成内容"}), + TagsJSON: mustJSON([]string{"上传物", "产物", "结果沉淀", "集中检索"}), + InstallState: "installed", + ExposedToUser: true, + State: "active", + SortOrder: 40, + }, + { + Key: "app-meeting-minutes", + Label: "会议纪要", + Badge: "成品应用", + Kind: "办公速用", + MarketTag: "成品应用", + WorkerType: "worker", + Tier: "business", + Source: "eai", + Color: "#0f766e", + IconText: "会", + CoverTone: "linear-gradient(135deg, #0f766e 0%, #14b8a6 100%)", + Summary: "直接进入会议纪要应用,快速整理结论、待办和责任人。", + Description: "适合把会议记录、语音转写或零散要点收成正式纪要和行动清单。", + SkillKey: "meeting-minutes", + DefaultPrompt: "把这段会议记录整理成正式纪要", + PromptsJSON: mustJSON([]string{"把这段会议记录整理成正式纪要", "提炼结论、行动项和责任人", "根据讨论内容生成会后同步稿"}), + TagsJSON: mustJSON([]string{"会议纪要", "行动项", "责任人", "汇总"}), + InstallState: "installed", + ExposedToUser: true, + State: "active", + SortOrder: 50, + }, + { + Key: "app-contract-brief", + Label: "合同摘要", + Badge: "成品应用", + Kind: "法务办公", + MarketTag: "成品应用", + WorkerType: "worker", + Tier: "business", + Source: "eai", + Color: "#b91c1c", + IconText: "合", + CoverTone: "linear-gradient(135deg, #991b1b 0%, #ef4444 100%)", + Summary: "直接进入合同摘要应用,快速拿到条款要点、风险和待确认项。", + Description: "适合采购合同、合作协议和服务条款的业务预读与快速审查。", + SkillKey: "contract-brief", + DefaultPrompt: "把这份采购合同提炼成要点摘要", + PromptsJSON: mustJSON([]string{"把这份采购合同提炼成要点摘要", "标记高风险条款和待确认事项", "生成业务负责人可读的合同摘要"}), + TagsJSON: mustJSON([]string{"合同摘要", "风险提示", "待确认项", "快速预读"}), + InstallState: "installed", + ExposedToUser: true, + State: "active", + SortOrder: 60, + }, + { + Key: "app-proposal-summary", + Label: "方案摘要", + Badge: "成品应用", + Kind: "汇报加速", + MarketTag: "成品应用", + WorkerType: "worker", + Tier: "business", + Source: "eai", + Color: "#7c3aed", + IconText: "方", + CoverTone: "linear-gradient(135deg, #6d28d9 0%, #a78bfa 100%)", + Summary: "直接进入方案摘要应用,把长方案压成管理层可读的短版本。", + Description: "适合售前方案、项目方案和内部提案的结论压缩与亮点提炼。", + SkillKey: "proposal-summary", + DefaultPrompt: "把这份解决方案压成一版管理层摘要", + PromptsJSON: mustJSON([]string{"把这份解决方案压成一版管理层摘要", "提炼亮点和实施建议", "生成汇报短版和亮点清单"}), + TagsJSON: mustJSON([]string{"方案摘要", "亮点提炼", "管理层预读", "汇报短版"}), + InstallState: "installed", + ExposedToUser: true, + State: "active", + SortOrder: 70, + }, + { + Key: "app-progress-report", + Label: "周报 / 日报", + Badge: "成品应用", + Kind: "进展同步", + MarketTag: "成品应用", + WorkerType: "worker", + Tier: "business", + Source: "eai", + Color: "#1d4ed8", + IconText: "报", + CoverTone: "linear-gradient(135deg, #1d4ed8 0%, #60a5fa 100%)", + Summary: "直接进入周报 / 日报应用,快速整理进展、风险和下步计划。", + Description: "适合把聊天记录、事项推进和工作流水收成日报、周报或阶段同步稿。", + SkillKey: "progress-report", + DefaultPrompt: "根据这些工作记录整理一版项目周报", + PromptsJSON: mustJSON([]string{"根据这些工作记录整理一版项目周报", "整理日报并补风险和计划", "生成给负责人的阶段同步"}), + TagsJSON: mustJSON([]string{"周报", "日报", "风险同步", "下步计划"}), + InstallState: "installed", + ExposedToUser: true, + State: "active", + SortOrder: 80, + }, + { + Key: "app-ppt-generation", + Label: "汇报 PPT", + Badge: "成品应用", + Kind: "汇报交付", + MarketTag: "成品应用", + WorkerType: "worker", + Tier: "business", + Source: "eai", + Color: "#4f46e5", + IconText: "P", + CoverTone: "linear-gradient(135deg, #4338ca 0%, #818cf8 100%)", + Summary: "直接进入汇报 PPT 应用,默认挂上汇报专员,快速拿到页面结构、讲稿备注和演示骨架。", + Description: "适合经营汇报、方案汇报、培训课件和项目复盘。打开就进入“汇报专员 + PPT 技能”的样板链路。", + SpecialistKey: "presentation-briefing", + SkillKey: "ppt-generation", + DefaultPrompt: "帮我做一版 8 页管理层汇报 PPT", + PromptsJSON: mustJSON([]string{"帮我做一版 8 页管理层汇报 PPT", "按汇报对象输出页面大纲和讲稿备注", "把这批材料压成一版培训课件骨架"}), + TagsJSON: mustJSON([]string{"汇报 PPT", "演示大纲", "讲稿备注", "专员样板"}), + InstallState: "installed", + ExposedToUser: true, + State: "active", + SortOrder: 90, + }, + { + Key: "app-mind-map", + Label: "思维导图", + Badge: "成品应用", + Kind: "结构整理", + MarketTag: "成品应用", + WorkerType: "worker", + Tier: "business", + Source: "eai", + Color: "#ea580c", + IconText: "图", + CoverTone: "linear-gradient(135deg, #c2410c 0%, #fb923c 100%)", + Summary: "直接进入思维导图应用,快速把主题拆成树状结构。", + Description: "适合方案拆解、培训提纲、需求梳理和知识结构整理。", + SkillKey: "mind-map", + DefaultPrompt: "把这个主题整理成思维导图", + PromptsJSON: mustJSON([]string{"把这个主题整理成思维导图", "按树形结构拆解方案", "生成一版培训提纲导图"}), + TagsJSON: mustJSON([]string{"思维导图", "结构拆解", "提纲", "知识整理"}), + InstallState: "installed", + ExposedToUser: true, + State: "active", + SortOrder: 100, + }, + { + Key: "app-ocr-understanding", + Label: "OCR 图文提取", + Badge: "成品应用", + Kind: "图文理解", + MarketTag: "成品应用", + WorkerType: "worker", + Tier: "business", + Source: "eai", + Color: "#0ea5e9", + IconText: "扫", + CoverTone: "linear-gradient(135deg, #0284c7 0%, #38bdf8 100%)", + Summary: "直接进入 OCR 应用,上传图片后提取文字并沉淀到任务产物。", + Description: "适合截图、照片、扫描件和证据图片的文本抽取与继续整理。", + SkillKey: "ocr-understanding", + DefaultPrompt: "识别这张图片里的文字", + PromptsJSON: mustJSON([]string{"识别这张图片里的文字", "把扫描件抽成可继续编辑的文本", "提取截图中的关键信息"}), + TagsJSON: mustJSON([]string{"OCR", "图文提取", "图片识别", "证据整理"}), + InstallState: "installed", + ExposedToUser: true, + State: "active", + SortOrder: 110, + }, + }) + + for _, item := range items { + var existing model.AppDefinition + if err := DB.Where("key = ?", item.Key).First(&existing).Error; err != nil { + if err := DB.Create(&item).Error; err != nil { + return err + } + continue + } + updates := map[string]any{} + if existing.DisplayCode == "" { + updates["display_code"] = item.DisplayCode + } + if existing.EAILogicCode == "" { + updates["eailogic_code"] = item.EAILogicCode + } + if existing.Badge == "" { + updates["badge"] = item.Badge + } + if existing.Kind == "" { + updates["kind"] = item.Kind + } + if existing.MarketTag == "" { + updates["market_tag"] = item.MarketTag + } + if existing.WorkerType == "" { + updates["worker_type"] = item.WorkerType + } + if existing.Tier == "" { + updates["tier"] = item.Tier + } + if existing.Color == "" { + updates["color"] = item.Color + } + if existing.IconText == "" { + updates["icon_text"] = item.IconText + } + if existing.CoverTone == "" { + updates["cover_tone"] = item.CoverTone + } + if existing.Summary == "" { + updates["summary"] = item.Summary + } + if existing.Description == "" { + updates["description"] = item.Description + } + if existing.OpenRoute == "" { + updates["open_route"] = item.OpenRoute + } + if existing.SpecialistKey == "" { + updates["specialist_key"] = item.SpecialistKey + } + if existing.SkillKey == "" { + updates["skill_key"] = item.SkillKey + } + if existing.DefaultPrompt == "" { + updates["default_prompt"] = item.DefaultPrompt + } + if existing.PromptsJSON == "" { + updates["prompts_json"] = item.PromptsJSON + } + if existing.TagsJSON == "" { + updates["tags_json"] = item.TagsJSON + } + if len(updates) > 0 { + if err := DB.Model(&existing).Updates(updates).Error; err != nil { + return err + } + } + } + log.Println("[OK] 应用定义种子已导入") + return nil +} + func ensureSeedMedia(adminID uint, approvedDir string, item mediaSeed, bindID *uint) error { dst := filepath.Join(approvedDir, item.storedName) if err := ensureMediaLink(item.sourcePath, dst); err != nil { diff --git a/eai_agentplatform/backend-go/internal/store/seed_specialist_rules.go b/eai_agentplatform/backend-go/internal/store/seed_specialist_rules.go index 3453ea4..1228f82 100644 --- a/eai_agentplatform/backend-go/internal/store/seed_specialist_rules.go +++ b/eai_agentplatform/backend-go/internal/store/seed_specialist_rules.go @@ -173,6 +173,28 @@ var specialistRuleFileDrafts = map[string]string{ - 不承诺工期、不承诺价格——这两项由销售与交付确认。 - 不替研发承诺功能,不贬低竞品。`, + "presentation-briefing": `# 汇报专员 + +## 你是谁 +你负责把零散材料收成能讲、能交、能继续细化的汇报稿:经营汇报、方案汇报、培训课件、项目复盘。 +你面对的是马上要上台讲的人,他要的是一版结构清楚、页数合适、重点能讲明白的演示稿。 + +## 你怎么推进 +1. 先确认汇报对象、场景和页数限制:给管理层、给客户、给培训学员,写法完全不同。 +2. 先出结构,不急着写满内容:按「开场结论 → 现状 / 问题 → 方案 / 进展 → 风险 / 下一步」组织。 +3. 每页只保留一层主结论,宁可留白,不要把 Word 直接搬进 PPT。 +4. 最后补讲稿备注:每页写清楚要讲什么、强调什么、哪些数字必须口径一致。 + +## 输出要求 +- 先给页纲,再给每页要点,再补讲稿备注。 +- 所有数字、时间、范围要带口径;没有依据的地方写「待补」,不要自己补齐。 +- 如果页数明显超了,优先帮用户压缩,而不是机械地继续加页。 + +## 边界 +- 不编造数据、案例、客户反馈。 +- 不把未确认方案写成既定事实。 +- 视觉美化可以建议,但当前重点是结构与表达,不承诺设计成品。`, + "logistics-fulfillment": `# 履约跟单专员 ## 你是谁 @@ -249,6 +271,7 @@ var specialistSkillBindings = map[string][]string{ "wechat-official-account": {"longform-writing", "copy-proofreading", "mind-map"}, "contract-review": {"contract-review", "contract-brief", "batch-extract"}, "solution-proposal": {"proposal-summary", "ppt-generation", "project-planning", "mind-map"}, + "presentation-briefing": {"ppt-generation", "proposal-summary", "mind-map", "progress-report"}, "logistics-fulfillment": {"progress-report", "table-cleanup", "email-drafting"}, "hr-email-sorter": {"email-drafting", "batch-extract", "table-cleanup", "ocr-understanding"}, "resume-processor": {"ocr-understanding", "batch-extract", "table-cleanup", "interview-summary"}, diff --git a/eai_agentplatform/backend-go/internal/store/seed_specialist_rules_test.go b/eai_agentplatform/backend-go/internal/store/seed_specialist_rules_test.go index ddebf89..3c08edd 100644 --- a/eai_agentplatform/backend-go/internal/store/seed_specialist_rules_test.go +++ b/eai_agentplatform/backend-go/internal/store/seed_specialist_rules_test.go @@ -10,7 +10,7 @@ import ( // seedSpecialistKeys 从 seedSpecialists 的字面量里抽专员 key。 // 直接反射那份清单不方便(它建在函数体里),所以从源码文本抓—— -// 与 skill_keys_test.go 抓前端 availableSkills 是同一套路子。 +// 与 skill_keys_test.go 抓前端 staticSkillCatalog 是同一套路子。 // // 必须限定在 seedSpecialists 到 seedSkillDefinitions 之间:seed.go 里 // 技能、动作清单也用 Key: 字面量,而其中 contract-review / report-generation diff --git a/eai_agentplatform/backend-go/internal/store/specialist_records.go b/eai_agentplatform/backend-go/internal/store/specialist_records.go index 12e2d8d..fc22c96 100644 --- a/eai_agentplatform/backend-go/internal/store/specialist_records.go +++ b/eai_agentplatform/backend-go/internal/store/specialist_records.go @@ -12,7 +12,7 @@ func EnsureSpecialistStructuredRecords(item *model.Specialist) { if item == nil { return } - EnsureSpecialistRoleCard(item) + EnsureSpecialistInteractionCard(item) if strings.TrimSpace(item.InputsRecordsJSON) == "" { item.InputsRecordsJSON = mustJSON(buildInputsRecords(item)) } @@ -27,23 +27,23 @@ func EnsureSpecialistStructuredRecords(item *model.Specialist) { } } -func EnsureSpecialistRoleCard(item *model.Specialist) { - if item == nil || strings.TrimSpace(item.RoleCardJSON) != "" { - return - } - item.RoleCardJSON = mustJSON(map[string]any{ - "name": item.Label, - "tagline": firstNonEmpty(item.Summary, item.WorkStatus, item.Label), - "greeting": "我是" + item.Label + ",已经准备好接手这条任务并继续推进。", - "relationship_to_user": "你的协作搭档", - "tone": "专业、直接、可执行", - "opening_prompt": "告诉我当前事项、目标和约束,我先帮你起步。", - "starter_prompts": defaultStarterPrompts(item), - "boundaries": []string{ - "高风险动作会明确标记待确认", - "需要人工审批的步骤不会默认自动执行", - }, - }) +func EnsureSpecialistInteractionCard(item *model.Specialist) { + if item == nil || strings.TrimSpace(item.InteractionCardJSON) != "" { + return + } + item.InteractionCardJSON = mustJSON(map[string]any{ + "name": item.Label, + "tagline": firstNonEmpty(item.Summary, item.WorkStatus, item.Label), + "greeting": "我是" + item.Label + ",已经准备好接手这条任务并继续推进。", + "relationship_to_user": "你的协作搭档", + "tone": "专业、直接、可执行", + "opening_prompt": "告诉我当前事项、目标和约束,我先帮你起步。", + "starter_prompts": defaultStarterPrompts(item), + "boundaries": []string{ + "高风险动作会明确标记待确认", + "需要人工审批的步骤不会默认自动执行", + }, + }) } func ValidateStructuredRecordsJSON(value string) bool { @@ -56,21 +56,21 @@ func ValidateStructuredRecordsJSON(value string) bool { } func ValidateJSONObjectJSON(value string) bool { - value = strings.TrimSpace(value) - if value == "" { - return true - } - var obj map[string]any - return json.Unmarshal([]byte(value), &obj) == nil + value = strings.TrimSpace(value) + if value == "" { + return true + } + var obj map[string]any + return json.Unmarshal([]byte(value), &obj) == nil } func ValidateJSONStringArray(value string) bool { - value = strings.TrimSpace(value) - if value == "" { - return true - } - var arr []string - return json.Unmarshal([]byte(value), &arr) == nil + value = strings.TrimSpace(value) + if value == "" { + return true + } + var arr []string + return json.Unmarshal([]byte(value), &arr) == nil } func buildInputsRecords(item *model.Specialist) []map[string]any { @@ -216,20 +216,20 @@ func buildResultRecords(item *model.Specialist) []map[string]any { } func defaultStarterPrompts(item *model.Specialist) []string { - prompts := make([]string, 0, 3) - for _, skill := range splitText(item.BaseSkills) { - prompts = append(prompts, "帮我处理:"+skill) - if len(prompts) == 3 { - return prompts - } - } - if item.Summary != "" { - prompts = append(prompts, "继续推进这条事项") - } - if len(prompts) == 0 { - prompts = []string{"先帮我梳理当前事项", "告诉我下一步怎么做", "先看有哪些待确认点"} - } - return prompts + prompts := make([]string, 0, 3) + for _, skill := range splitText(item.BaseSkills) { + prompts = append(prompts, "帮我处理:"+skill) + if len(prompts) == 3 { + return prompts + } + } + if item.Summary != "" { + prompts = append(prompts, "继续推进这条事项") + } + if len(prompts) == 0 { + prompts = []string{"先帮我梳理当前事项", "告诉我下一步怎么做", "先看有哪些待确认点"} + } + return prompts } func mustJSON(v any) string { diff --git a/eai_agentplatform/frontend/src/api/appDefinition.js b/eai_agentplatform/frontend/src/api/appDefinition.js new file mode 100644 index 0000000..da18037 --- /dev/null +++ b/eai_agentplatform/frontend/src/api/appDefinition.js @@ -0,0 +1,9 @@ +import http from './http' + +export function listAppDefinitions(params) { + return http.get('/apps', { params }) +} + +export function getAppDefinitionByKey(key) { + return http.get(`/apps/by-key/${key}`) +} diff --git a/eai_agentplatform/frontend/src/api/capability.js b/eai_agentplatform/frontend/src/api/capability.js deleted file mode 100644 index 1e1faab..0000000 --- a/eai_agentplatform/frontend/src/api/capability.js +++ /dev/null @@ -1,5 +0,0 @@ -import http from './http' - -export function getSkillDefinitionByKey(key) { - return http.get(`/skills/by-key/${key}`) -} diff --git a/eai_agentplatform/frontend/src/api/assistant.js b/eai_agentplatform/frontend/src/api/chat.js similarity index 59% rename from eai_agentplatform/frontend/src/api/assistant.js rename to eai_agentplatform/frontend/src/api/chat.js index 2a56cc2..c5a6113 100644 --- a/eai_agentplatform/frontend/src/api/assistant.js +++ b/eai_agentplatform/frontend/src/api/chat.js @@ -1,4 +1,5 @@ import http from './http' -export function chatWithAssistant(data) { + +export function sendAssistantChatMessage(data) { return http.post('/assistant/chat', data) } diff --git a/eai_agentplatform/frontend/src/api/skill.js b/eai_agentplatform/frontend/src/api/skill.js new file mode 100644 index 0000000..b73acea --- /dev/null +++ b/eai_agentplatform/frontend/src/api/skill.js @@ -0,0 +1,9 @@ +import http from './http' + +export function listSkills(params) { + return http.get('/skills', { params }) +} + +export function getSkillByKey(key) { + return http.get(`/skills/by-key/${key}`) +} diff --git a/eai_agentplatform/frontend/src/components/chat/AppChatRail.vue b/eai_agentplatform/frontend/src/components/chat/AppChatRail.vue index 04d52a4..d493d00 100644 --- a/eai_agentplatform/frontend/src/components/chat/AppChatRail.vue +++ b/eai_agentplatform/frontend/src/components/chat/AppChatRail.vue @@ -65,7 +65,7 @@