refactor: 对象命名标准化落地(RoleKind→ObjectKind、capability/assistant 收口)
后端 - 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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})
|
||||||
|
}
|
||||||
@@ -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})
|
|
||||||
}
|
|
||||||
@@ -45,6 +45,8 @@ func RegisterRoutes(r *gin.Engine, cfg *config.Config) {
|
|||||||
r.GET("/api/specialists/summary", middleware.Auth(cfg), SpecialistSummary)
|
r.GET("/api/specialists/summary", middleware.Auth(cfg), SpecialistSummary)
|
||||||
r.GET("/api/skills", middleware.Auth(cfg), ListSkillDefinitions)
|
r.GET("/api/skills", middleware.Auth(cfg), ListSkillDefinitions)
|
||||||
r.GET("/api/skills/by-key/:key", middleware.Auth(cfg), GetSkillDefinitionByKey)
|
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", middleware.Auth(cfg), ListActionDefinitions)
|
||||||
r.GET("/api/actions/by-key/:key", middleware.Auth(cfg), GetActionDefinitionByKey)
|
r.GET("/api/actions/by-key/:key", middleware.Auth(cfg), GetActionDefinitionByKey)
|
||||||
r.GET("/api/workbench/overview", middleware.Auth(cfg), WorkbenchOverview)
|
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.POST("/skills", CreateSkillDefinition)
|
||||||
admin.PUT("/skills/:id", UpdateSkillDefinition)
|
admin.PUT("/skills/:id", UpdateSkillDefinition)
|
||||||
admin.DELETE("/skills/:id", DeleteSkillDefinition)
|
admin.DELETE("/skills/:id", DeleteSkillDefinition)
|
||||||
|
admin.POST("/apps", CreateAppDefinition)
|
||||||
|
admin.PUT("/apps/:id", UpdateAppDefinition)
|
||||||
|
admin.DELETE("/apps/:id", DeleteAppDefinition)
|
||||||
admin.POST("/actions", CreateActionDefinition)
|
admin.POST("/actions", CreateActionDefinition)
|
||||||
admin.PUT("/actions/:id", UpdateActionDefinition)
|
admin.PUT("/actions/:id", UpdateActionDefinition)
|
||||||
admin.DELETE("/actions/:id", DeleteActionDefinition)
|
admin.DELETE("/actions/:id", DeleteActionDefinition)
|
||||||
|
|||||||
@@ -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})
|
||||||
|
}
|
||||||
@@ -118,7 +118,7 @@ type specialistReq struct {
|
|||||||
WorkerType string `json:"worker_type"`
|
WorkerType string `json:"worker_type"`
|
||||||
ObjectEntryRoute string `json:"object_entry_route"`
|
ObjectEntryRoute string `json:"object_entry_route"`
|
||||||
Summary string `json:"summary"`
|
Summary string `json:"summary"`
|
||||||
RoleCardJSON string `json:"role_card_json"`
|
InteractionCardJSON string `json:"interaction_card_json"`
|
||||||
WorkStatus string `json:"work_status"`
|
WorkStatus string `json:"work_status"`
|
||||||
RiskLabel string `json:"risk_label"`
|
RiskLabel string `json:"risk_label"`
|
||||||
Color string `json:"color"`
|
Color string `json:"color"`
|
||||||
@@ -139,7 +139,6 @@ type specialistReq struct {
|
|||||||
// 元素须在 model.ValidSkillKeys 内;写入前会被规范化成紧凑 JSON。
|
// 元素须在 model.ValidSkillKeys 内;写入前会被规范化成紧凑 JSON。
|
||||||
AllowedSkills string `json:"allowed_skills"`
|
AllowedSkills string `json:"allowed_skills"`
|
||||||
InputsRecordsJSON string `json:"inputs_records_json"`
|
InputsRecordsJSON string `json:"inputs_records_json"`
|
||||||
LegacySourceRecordsJSON string `json:"source_records_json"`
|
|
||||||
PermissionRecordsJSON string `json:"permission_records_json"`
|
PermissionRecordsJSON string `json:"permission_records_json"`
|
||||||
ActionRecordsJSON string `json:"action_records_json"`
|
ActionRecordsJSON string `json:"action_records_json"`
|
||||||
ResultRecordsJSON string `json:"result_records_json"`
|
ResultRecordsJSON string `json:"result_records_json"`
|
||||||
@@ -156,7 +155,7 @@ func normalizeSpecialistReq(req *specialistReq) {
|
|||||||
req.WorkerType = strings.TrimSpace(req.WorkerType)
|
req.WorkerType = strings.TrimSpace(req.WorkerType)
|
||||||
req.ObjectEntryRoute = strings.TrimSpace(req.ObjectEntryRoute)
|
req.ObjectEntryRoute = strings.TrimSpace(req.ObjectEntryRoute)
|
||||||
req.Summary = strings.TrimSpace(req.Summary)
|
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.WorkStatus = strings.TrimSpace(req.WorkStatus)
|
||||||
req.RiskLabel = strings.TrimSpace(req.RiskLabel)
|
req.RiskLabel = strings.TrimSpace(req.RiskLabel)
|
||||||
req.Color = strings.TrimSpace(req.Color)
|
req.Color = strings.TrimSpace(req.Color)
|
||||||
@@ -173,10 +172,6 @@ func normalizeSpecialistReq(req *specialistReq) {
|
|||||||
req.RuleFileMarkdown = strings.TrimSpace(req.RuleFileMarkdown)
|
req.RuleFileMarkdown = strings.TrimSpace(req.RuleFileMarkdown)
|
||||||
req.AllowedSkills = strings.TrimSpace(req.AllowedSkills)
|
req.AllowedSkills = strings.TrimSpace(req.AllowedSkills)
|
||||||
req.InputsRecordsJSON = strings.TrimSpace(req.InputsRecordsJSON)
|
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.PermissionRecordsJSON = strings.TrimSpace(req.PermissionRecordsJSON)
|
||||||
req.ActionRecordsJSON = strings.TrimSpace(req.ActionRecordsJSON)
|
req.ActionRecordsJSON = strings.TrimSpace(req.ActionRecordsJSON)
|
||||||
req.ResultRecordsJSON = strings.TrimSpace(req.ResultRecordsJSON)
|
req.ResultRecordsJSON = strings.TrimSpace(req.ResultRecordsJSON)
|
||||||
@@ -212,8 +207,8 @@ func validateSpecialistReq(req *specialistReq) *web.AppError {
|
|||||||
if !store.ValidateStructuredRecordsJSON(req.InputsRecordsJSON) {
|
if !store.ValidateStructuredRecordsJSON(req.InputsRecordsJSON) {
|
||||||
return web.NewBadRequest("inputs_records_json 必须是 JSON 数组")
|
return web.NewBadRequest("inputs_records_json 必须是 JSON 数组")
|
||||||
}
|
}
|
||||||
if !store.ValidateJSONObjectJSON(req.RoleCardJSON) {
|
if !store.ValidateJSONObjectJSON(req.InteractionCardJSON) {
|
||||||
return web.NewBadRequest("role_card_json 必须是 JSON 对象")
|
return web.NewBadRequest("interaction_card_json 必须是 JSON 对象")
|
||||||
}
|
}
|
||||||
if !store.ValidateStructuredRecordsJSON(req.PermissionRecordsJSON) {
|
if !store.ValidateStructuredRecordsJSON(req.PermissionRecordsJSON) {
|
||||||
return web.NewBadRequest("permission_records_json 必须是 JSON 数组")
|
return web.NewBadRequest("permission_records_json 必须是 JSON 数组")
|
||||||
@@ -282,7 +277,7 @@ func CreateSpecialist(c *gin.Context) {
|
|||||||
WorkerType: req.WorkerType,
|
WorkerType: req.WorkerType,
|
||||||
ObjectEntryRoute: req.ObjectEntryRoute,
|
ObjectEntryRoute: req.ObjectEntryRoute,
|
||||||
Summary: req.Summary,
|
Summary: req.Summary,
|
||||||
RoleCardJSON: req.RoleCardJSON,
|
InteractionCardJSON: req.InteractionCardJSON,
|
||||||
WorkStatus: req.WorkStatus,
|
WorkStatus: req.WorkStatus,
|
||||||
RiskLabel: req.RiskLabel,
|
RiskLabel: req.RiskLabel,
|
||||||
Color: req.Color,
|
Color: req.Color,
|
||||||
@@ -351,7 +346,7 @@ func UpdateSpecialist(c *gin.Context) {
|
|||||||
item.WorkerType = req.WorkerType
|
item.WorkerType = req.WorkerType
|
||||||
item.ObjectEntryRoute = req.ObjectEntryRoute
|
item.ObjectEntryRoute = req.ObjectEntryRoute
|
||||||
item.Summary = req.Summary
|
item.Summary = req.Summary
|
||||||
item.RoleCardJSON = req.RoleCardJSON
|
item.InteractionCardJSON = req.InteractionCardJSON
|
||||||
item.WorkStatus = req.WorkStatus
|
item.WorkStatus = req.WorkStatus
|
||||||
item.RiskLabel = req.RiskLabel
|
item.RiskLabel = req.RiskLabel
|
||||||
item.Color = req.Color
|
item.Color = req.Color
|
||||||
|
|||||||
@@ -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" }
|
||||||
@@ -10,10 +10,9 @@ type SkillDefinition struct {
|
|||||||
DisplayCode string `gorm:"column:display_code;size:16;default:'';index" json:"display_code"`
|
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"`
|
EAILogicCode string `gorm:"column:eailogic_code;size:32;default:'';index" json:"eailogic_code"`
|
||||||
Description string `gorm:"type:text" json:"description"`
|
Description string `gorm:"type:text" json:"description"`
|
||||||
RoleKind string `gorm:"size:32;not null;default:skill;index" json:"role_kind"` // assistant / specialist / skill
|
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
|
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"`
|
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"`
|
ExposedToUser bool `gorm:"not null;default:true;index" json:"exposed_to_user"`
|
||||||
StarterPromptsJSON string `gorm:"type:text" json:"starter_prompts_json"`
|
StarterPromptsJSON string `gorm:"type:text" json:"starter_prompts_json"`
|
||||||
PromptTemplate string `gorm:"type:text" json:"prompt_template"`
|
PromptTemplate string `gorm:"type:text" json:"prompt_template"`
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
|
|
||||||
// ValidSkillKeys 技能 key 全集,用于校验 Specialist.AllowedSkills。
|
// ValidSkillKeys 技能 key 全集,用于校验 Specialist.AllowedSkills。
|
||||||
//
|
//
|
||||||
// 权威来源是前端 frontend/src/config/workbench.js 的 availableSkills ——
|
// 权威来源是前端 frontend/src/config/workbench.js 的 staticSkillCatalog ——
|
||||||
// 技能实现仍归前端,后端只持有 key 清单,用于录入时挡住拼写错误。
|
// 技能实现仍归前端,后端只持有 key 清单,用于录入时挡住拼写错误。
|
||||||
// 两个清单的一致性由 skill_keys_test.go 守着,改动任一侧都会让测试失败。
|
// 两个清单的一致性由 skill_keys_test.go 守着,改动任一侧都会让测试失败。
|
||||||
//
|
//
|
||||||
|
|||||||
@@ -12,11 +12,11 @@ import (
|
|||||||
// workbenchJSRelPath 前端技能目录的相对路径(相对本包目录)。
|
// workbenchJSRelPath 前端技能目录的相对路径(相对本包目录)。
|
||||||
const workbenchJSRelPath = "../../../frontend/src/config/workbench.js"
|
const workbenchJSRelPath = "../../../frontend/src/config/workbench.js"
|
||||||
|
|
||||||
// createSkillDefinitionRe 抓 availableSkills 数组里每个 createSkillDefinition 的字面量 key。
|
// createSkillDefinitionRe 抓 staticSkillCatalog 数组里每个 createSkillDefinition 的字面量 key。
|
||||||
// 依赖 workbench.js 里「createSkillDefinition({ 紧跟 key: '…'」这一写法。
|
// 依赖 workbench.js 里「createSkillDefinition({ 紧跟 key: '…'」这一写法。
|
||||||
var createSkillDefinitionRe = regexp.MustCompile(`createSkillDefinition\(\{\s*key:\s*'([^']+)'`)
|
var createSkillDefinitionRe = regexp.MustCompile(`createSkillDefinition\(\{\s*key:\s*'([^']+)'`)
|
||||||
|
|
||||||
// TestValidSkillKeysMatchFrontend 守住 backend 的 ValidSkillKeys 与前端 availableSkills 一致。
|
// TestValidSkillKeysMatchFrontend 守住 backend 的 ValidSkillKeys 与前端 staticSkillCatalog 一致。
|
||||||
//
|
//
|
||||||
// 这两份清单天然会漂移:技能实现在前端,后端只持有 key 做录入校验。任何一侧增删技能
|
// 这两份清单天然会漂移:技能实现在前端,后端只持有 key 做录入校验。任何一侧增删技能
|
||||||
// 而忘了另一侧,这里就会红——这正是本测试存在的唯一理由。
|
// 而忘了另一侧,这里就会红——这正是本测试存在的唯一理由。
|
||||||
@@ -54,11 +54,11 @@ func TestValidSkillKeysMatchFrontend(t *testing.T) {
|
|||||||
sort.Strings(onlyFrontend)
|
sort.Strings(onlyFrontend)
|
||||||
|
|
||||||
if len(onlyBackend) > 0 {
|
if len(onlyBackend) > 0 {
|
||||||
t.Errorf("ValidSkillKeys 里有、前端 availableSkills 里没有的 key(后端多写或前端已删):\n %s",
|
t.Errorf("ValidSkillKeys 里有、前端 staticSkillCatalog 里没有的 key(后端多写或前端已删):\n %s",
|
||||||
strings.Join(onlyBackend, "\n "))
|
strings.Join(onlyBackend, "\n "))
|
||||||
}
|
}
|
||||||
if len(onlyFrontend) > 0 {
|
if len(onlyFrontend) > 0 {
|
||||||
t.Errorf("前端 availableSkills 里有、ValidSkillKeys 里没有的 key(新增技能后忘了补后端清单):\n %s",
|
t.Errorf("前端 staticSkillCatalog 里有、ValidSkillKeys 里没有的 key(新增技能后忘了补后端清单):\n %s",
|
||||||
strings.Join(onlyFrontend, "\n "))
|
strings.Join(onlyFrontend, "\n "))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ type Specialist struct {
|
|||||||
WorkerType string `gorm:"size:16;not null;default:dw;index" json:"worker_type"`
|
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"`
|
ObjectEntryRoute string `gorm:"column:object_entry_route;size:128;not null;default:''" json:"object_entry_route"`
|
||||||
Summary string `gorm:"type:text" json:"summary"`
|
Summary string `gorm:"type:text" json:"summary"`
|
||||||
RoleCardJSON string `gorm:"type:text" json:"role_card_json"`
|
InteractionCardJSON string `gorm:"column:interaction_card_json;type:text" json:"interaction_card_json"`
|
||||||
WorkStatus string `gorm:"size:64;default:''" json:"work_status"`
|
WorkStatus string `gorm:"size:64;default:''" json:"work_status"`
|
||||||
RiskLabel string `gorm:"size:64;default:''" json:"risk_label"`
|
RiskLabel string `gorm:"size:64;default:''" json:"risk_label"`
|
||||||
Color string `gorm:"size:16;default:''" json:"color"`
|
Color string `gorm:"size:16;default:''" json:"color"`
|
||||||
@@ -33,10 +33,10 @@ type Specialist struct {
|
|||||||
// 参考 AionCore builtin-assistants 的 rule_file;命名对齐 SY23。
|
// 参考 AionCore builtin-assistants 的 rule_file;命名对齐 SY23。
|
||||||
RuleFileMarkdown string `gorm:"type:text" json:"rule_file_markdown"`
|
RuleFileMarkdown string `gorm:"type:text" json:"rule_file_markdown"`
|
||||||
// AllowedSkills 绑定技能 key 列表,JSON 字符串数组,顺序即优先级(首个为主技能)。
|
// AllowedSkills 绑定技能 key 列表,JSON 字符串数组,顺序即优先级(首个为主技能)。
|
||||||
// 元素取值须通过 model.ValidSkillKeys 校验,对齐前端 availableSkills[].key。
|
// 元素取值须通过 model.ValidSkillKeys 校验,对齐前端 staticSkillCatalog[].key。
|
||||||
// 字段名对齐 SY22 §6.1 对象 manifest 的 allowed_skills。
|
// 字段名对齐 SY22 §6.1 对象 manifest 的 allowed_skills。
|
||||||
AllowedSkills string `gorm:"type:text" json:"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"`
|
PermissionRecordsJSON string `gorm:"type:text" json:"permission_records_json"`
|
||||||
ActionRecordsJSON string `gorm:"type:text" json:"action_records_json"`
|
ActionRecordsJSON string `gorm:"type:text" json:"action_records_json"`
|
||||||
ResultRecordsJSON string `gorm:"type:text" json:"result_records_json"`
|
ResultRecordsJSON string `gorm:"type:text" json:"result_records_json"`
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ func Init(dbPath string) (*gorm.DB, error) {
|
|||||||
&model.WorkerArtifact{},
|
&model.WorkerArtifact{},
|
||||||
&model.WorkerRun{},
|
&model.WorkerRun{},
|
||||||
&model.UserAppCenter{},
|
&model.UserAppCenter{},
|
||||||
|
&model.AppDefinition{},
|
||||||
&model.OfficialAccountArticle{},
|
&model.OfficialAccountArticle{},
|
||||||
&model.OfficialAccountHotspot{},
|
&model.OfficialAccountHotspot{},
|
||||||
); err != nil {
|
); err != nil {
|
||||||
@@ -71,6 +72,12 @@ func Init(dbPath string) (*gorm.DB, error) {
|
|||||||
if err := migrateObjectEntryRouteColumns(db); err != nil {
|
if err := migrateObjectEntryRouteColumns(db); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if err := migrateSpecialistStructuredColumns(db); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := migrateSkillObjectKindColumn(db); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
DB = db
|
DB = db
|
||||||
return db, nil
|
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 db.Migrator().HasColumn(&model.SkillDefinition{}, "entry_route") {
|
||||||
if err := dropColumn(db, "skill_definition", "entry_route"); err != nil {
|
if err := dropColumn(db, "skill_definition", "entry_route"); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -165,11 +154,53 @@ func migrateObjectEntryRouteColumns(db *gorm.DB) error {
|
|||||||
return err
|
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
|
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 {
|
func coalesceRouteExpr(primaryExists bool, primaryColumn string, fallbackExists bool, fallbackColumn string) string {
|
||||||
parts := make([]string, 0, 2)
|
parts := make([]string, 0, 2)
|
||||||
if primaryExists {
|
if primaryExists {
|
||||||
|
|||||||
@@ -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 {
|
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)
|
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{
|
specialist := model.Specialist{
|
||||||
Key: "spec-demo",
|
Key: "spec-demo",
|
||||||
@@ -64,10 +67,9 @@ func TestMigrateObjectEntryRouteColumnsDropsLegacyColumns(t *testing.T) {
|
|||||||
skill := model.SkillDefinition{
|
skill := model.SkillDefinition{
|
||||||
Key: "skill-demo",
|
Key: "skill-demo",
|
||||||
Label: "技能演示",
|
Label: "技能演示",
|
||||||
RoleKind: "skill",
|
ObjectKind: "skill",
|
||||||
Source: "eai",
|
Source: "eai",
|
||||||
ObjectEntryRoute: "",
|
ObjectEntryRoute: "",
|
||||||
LegacyObjectEntryRoute: "",
|
|
||||||
State: "active",
|
State: "active",
|
||||||
}
|
}
|
||||||
if err := db.Create(&skill).Error; err != nil {
|
if err := db.Create(&skill).Error; err != nil {
|
||||||
@@ -98,9 +100,6 @@ func TestMigrateObjectEntryRouteColumnsDropsLegacyColumns(t *testing.T) {
|
|||||||
if migratedSkill.ObjectEntryRoute != "/legacy-skill" {
|
if migratedSkill.ObjectEntryRoute != "/legacy-skill" {
|
||||||
t.Fatalf("skill object_entry_route = %q, want %q", 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") {
|
if db.Migrator().HasColumn(&model.Specialist{}, "entry_route") {
|
||||||
t.Fatalf("specialist.entry_route should be dropped")
|
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") {
|
if db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_route") {
|
||||||
t.Fatalf("skill_definition.legacy_route should be dropped")
|
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")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,6 +97,9 @@ func SeedDefaults() error {
|
|||||||
if err := seedActionDefinitions(); err != nil {
|
if err := seedActionDefinitions(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := seedAppDefinitions(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +325,19 @@ func applySkillCodes(items []model.SkillDefinition) []model.SkillDefinition {
|
|||||||
return items
|
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 {
|
func seedSpecialists() error {
|
||||||
items := applySpecialistCodes([]model.Specialist{
|
items := applySpecialistCodes([]model.Specialist{
|
||||||
{
|
{
|
||||||
@@ -582,6 +598,30 @@ func seedSpecialists() error {
|
|||||||
State: "active",
|
State: "active",
|
||||||
SortOrder: 90,
|
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)
|
// 岗位说明书与技能绑定按 key 挂载(见 seed_specialist_rules.go)
|
||||||
if err := applySpecialistRuleFiles(items); err != nil {
|
if err := applySpecialistRuleFiles(items); err != nil {
|
||||||
@@ -608,8 +648,8 @@ func seedSpecialists() error {
|
|||||||
if existing.WorkerType == "" {
|
if existing.WorkerType == "" {
|
||||||
updates["worker_type"] = item.WorkerType
|
updates["worker_type"] = item.WorkerType
|
||||||
}
|
}
|
||||||
if existing.RoleCardJSON == "" {
|
if existing.InteractionCardJSON == "" {
|
||||||
updates["role_card_json"] = item.RoleCardJSON
|
updates["interaction_card_json"] = item.InteractionCardJSON
|
||||||
}
|
}
|
||||||
if existing.PermissionScope == "" {
|
if existing.PermissionScope == "" {
|
||||||
updates["permission_scope"] = item.PermissionScope
|
updates["permission_scope"] = item.PermissionScope
|
||||||
@@ -637,7 +677,7 @@ func seedSpecialists() error {
|
|||||||
updates["allowed_skills"] = item.AllowedSkills
|
updates["allowed_skills"] = item.AllowedSkills
|
||||||
}
|
}
|
||||||
if existing.InputsRecordsJSON == "" {
|
if existing.InputsRecordsJSON == "" {
|
||||||
updates["source_records_json"] = item.InputsRecordsJSON
|
updates["inputs_records_json"] = item.InputsRecordsJSON
|
||||||
}
|
}
|
||||||
if existing.PermissionRecordsJSON == "" {
|
if existing.PermissionRecordsJSON == "" {
|
||||||
updates["permission_records_json"] = item.PermissionRecordsJSON
|
updates["permission_records_json"] = item.PermissionRecordsJSON
|
||||||
@@ -666,7 +706,7 @@ func seedSkillDefinitions() error {
|
|||||||
Key: "smart-assistant",
|
Key: "smart-assistant",
|
||||||
Label: "通用助手",
|
Label: "通用助手",
|
||||||
Description: "默认协作入口,负责问题梳理、任务拆解和对话推进。",
|
Description: "默认协作入口,负责问题梳理、任务拆解和对话推进。",
|
||||||
RoleKind: "assistant",
|
ObjectKind: "assistant",
|
||||||
Source: "eai",
|
Source: "eai",
|
||||||
ObjectEntryRoute: "/home",
|
ObjectEntryRoute: "/home",
|
||||||
ExposedToUser: true,
|
ExposedToUser: true,
|
||||||
@@ -684,10 +724,9 @@ func seedSkillDefinitions() error {
|
|||||||
Key: "document-translate",
|
Key: "document-translate",
|
||||||
Label: "文档翻译",
|
Label: "文档翻译",
|
||||||
Description: "把文本和文档稳定翻成目标语言,并保留语气和结构。",
|
Description: "把文本和文档稳定翻成目标语言,并保留语气和结构。",
|
||||||
RoleKind: "skill",
|
ObjectKind: "skill",
|
||||||
Source: "eai",
|
Source: "eai",
|
||||||
ObjectEntryRoute: "/home",
|
ObjectEntryRoute: "/home",
|
||||||
LegacyObjectEntryRoute: "/tools/document-translate",
|
|
||||||
ExposedToUser: true,
|
ExposedToUser: true,
|
||||||
StarterPromptsJSON: mustJSON([]string{"把这段产品说明翻译成英文,保持专业语气", "把这封邮件翻译成日文,语气礼貌一点", "把这份中文文档整理成英文摘要"}),
|
StarterPromptsJSON: mustJSON([]string{"把这段产品说明翻译成英文,保持专业语气", "把这封邮件翻译成日文,语气礼貌一点", "把这份中文文档整理成英文摘要"}),
|
||||||
PromptTemplate: "明确源语言、目标语言、语气和输出格式后执行翻译。",
|
PromptTemplate: "明确源语言、目标语言、语气和输出格式后执行翻译。",
|
||||||
@@ -703,10 +742,9 @@ func seedSkillDefinitions() error {
|
|||||||
Key: "copy-proofreading",
|
Key: "copy-proofreading",
|
||||||
Label: "文案校对",
|
Label: "文案校对",
|
||||||
Description: "检查错别字、语病和表达不顺,输出润色建议。",
|
Description: "检查错别字、语病和表达不顺,输出润色建议。",
|
||||||
RoleKind: "skill",
|
ObjectKind: "skill",
|
||||||
Source: "eai",
|
Source: "eai",
|
||||||
ObjectEntryRoute: "/home",
|
ObjectEntryRoute: "/home",
|
||||||
LegacyObjectEntryRoute: "/tools/copy-proofreading",
|
|
||||||
ExposedToUser: true,
|
ExposedToUser: true,
|
||||||
StarterPromptsJSON: mustJSON([]string{"检查这段销售话术里的错别字和语病", "把这段产品介绍润得更正式一点", "帮我校对这封对外邮件,保留原意"}),
|
StarterPromptsJSON: mustJSON([]string{"检查这段销售话术里的错别字和语病", "把这段产品介绍润得更正式一点", "帮我校对这封对外邮件,保留原意"}),
|
||||||
PromptTemplate: "围绕原文语气给出校对与改写结果。",
|
PromptTemplate: "围绕原文语气给出校对与改写结果。",
|
||||||
@@ -722,10 +760,9 @@ func seedSkillDefinitions() error {
|
|||||||
Key: "audio-transcribe",
|
Key: "audio-transcribe",
|
||||||
Label: "语音转写",
|
Label: "语音转写",
|
||||||
Description: "把音频内容转成结构化文本,便于继续总结与提取。",
|
Description: "把音频内容转成结构化文本,便于继续总结与提取。",
|
||||||
RoleKind: "skill",
|
ObjectKind: "skill",
|
||||||
Source: "eai",
|
Source: "eai",
|
||||||
ObjectEntryRoute: "/home",
|
ObjectEntryRoute: "/home",
|
||||||
LegacyObjectEntryRoute: "/tools/audio-transcribe",
|
|
||||||
ExposedToUser: true,
|
ExposedToUser: true,
|
||||||
StarterPromptsJSON: mustJSON([]string{"把这段会议录音转成文字,并分段整理", "先转写这段采访,再提取重点", "把语音内容整理成可发群里的纪要"}),
|
StarterPromptsJSON: mustJSON([]string{"把这段会议录音转成文字,并分段整理", "先转写这段采访,再提取重点", "把语音内容整理成可发群里的纪要"}),
|
||||||
PromptTemplate: "先转写音频,再按需要输出整理版文本。",
|
PromptTemplate: "先转写音频,再按需要输出整理版文本。",
|
||||||
@@ -741,10 +778,9 @@ func seedSkillDefinitions() error {
|
|||||||
Key: "batch-extract",
|
Key: "batch-extract",
|
||||||
Label: "批量提取",
|
Label: "批量提取",
|
||||||
Description: "从一批内容里抽取结构化字段和关键信息。",
|
Description: "从一批内容里抽取结构化字段和关键信息。",
|
||||||
RoleKind: "skill",
|
ObjectKind: "skill",
|
||||||
Source: "eai",
|
Source: "eai",
|
||||||
ObjectEntryRoute: "/home",
|
ObjectEntryRoute: "/home",
|
||||||
LegacyObjectEntryRoute: "/tools/batch-extract",
|
|
||||||
ExposedToUser: true,
|
ExposedToUser: true,
|
||||||
StarterPromptsJSON: mustJSON([]string{"从这批简历里提取姓名、岗位和年限", "把这些合同里的付款条款统一抽出来", "帮我批量提取文章标题、作者和发布时间"}),
|
StarterPromptsJSON: mustJSON([]string{"从这批简历里提取姓名、岗位和年限", "把这些合同里的付款条款统一抽出来", "帮我批量提取文章标题、作者和发布时间"}),
|
||||||
PromptTemplate: "明确字段范围、输出格式和异常处理规则后执行批量提取。",
|
PromptTemplate: "明确字段范围、输出格式和异常处理规则后执行批量提取。",
|
||||||
@@ -760,10 +796,9 @@ func seedSkillDefinitions() error {
|
|||||||
Key: "contract-review",
|
Key: "contract-review",
|
||||||
Label: "合同审查",
|
Label: "合同审查",
|
||||||
Description: "定位风险条款、待确认项并输出审查建议。",
|
Description: "定位风险条款、待确认项并输出审查建议。",
|
||||||
RoleKind: "skill",
|
ObjectKind: "skill",
|
||||||
Source: "eai",
|
Source: "eai",
|
||||||
ObjectEntryRoute: "/home",
|
ObjectEntryRoute: "/home",
|
||||||
LegacyObjectEntryRoute: "/tools/contract-review",
|
|
||||||
ExposedToUser: true,
|
ExposedToUser: true,
|
||||||
StarterPromptsJSON: mustJSON([]string{"审查这份采购合同的风险点", "重点看赔偿责任和终止条款", "帮我整理一版待人工确认的风险清单"}),
|
StarterPromptsJSON: mustJSON([]string{"审查这份采购合同的风险点", "重点看赔偿责任和终止条款", "帮我整理一版待人工确认的风险清单"}),
|
||||||
PromptTemplate: "按照甲方风控视角识别风险条款、例外项和红线建议。",
|
PromptTemplate: "按照甲方风控视角识别风险条款、例外项和红线建议。",
|
||||||
@@ -779,10 +814,9 @@ func seedSkillDefinitions() error {
|
|||||||
Key: "report-generation",
|
Key: "report-generation",
|
||||||
Label: "报告生成",
|
Label: "报告生成",
|
||||||
Description: "把过程材料整理成日报、周报、复盘和交付说明。",
|
Description: "把过程材料整理成日报、周报、复盘和交付说明。",
|
||||||
RoleKind: "skill",
|
ObjectKind: "skill",
|
||||||
Source: "eai",
|
Source: "eai",
|
||||||
ObjectEntryRoute: "/home",
|
ObjectEntryRoute: "/home",
|
||||||
LegacyObjectEntryRoute: "/report-gen",
|
|
||||||
ExposedToUser: true,
|
ExposedToUser: true,
|
||||||
StarterPromptsJSON: mustJSON([]string{"把今天的工作记录整理成日报", "根据这些节点生成项目周报", "帮我把这次交付过程整理成复盘说明"}),
|
StarterPromptsJSON: mustJSON([]string{"把今天的工作记录整理成日报", "根据这些节点生成项目周报", "帮我把这次交付过程整理成复盘说明"}),
|
||||||
PromptTemplate: "先梳理结论与结构,再输出对应报告版本。",
|
PromptTemplate: "先梳理结论与结构,再输出对应报告版本。",
|
||||||
@@ -817,9 +851,6 @@ func seedSkillDefinitions() error {
|
|||||||
if existing.ObjectEntryRoute == "" {
|
if existing.ObjectEntryRoute == "" {
|
||||||
updates["object_entry_route"] = item.ObjectEntryRoute
|
updates["object_entry_route"] = item.ObjectEntryRoute
|
||||||
}
|
}
|
||||||
if existing.LegacyObjectEntryRoute == "" {
|
|
||||||
updates["legacy_object_entry_route"] = item.LegacyObjectEntryRoute
|
|
||||||
}
|
|
||||||
if existing.StarterPromptsJSON == "" {
|
if existing.StarterPromptsJSON == "" {
|
||||||
updates["starter_prompts_json"] = item.StarterPromptsJSON
|
updates["starter_prompts_json"] = item.StarterPromptsJSON
|
||||||
}
|
}
|
||||||
@@ -990,6 +1021,337 @@ func seedActionDefinitions() error {
|
|||||||
return nil
|
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 {
|
func ensureSeedMedia(adminID uint, approvedDir string, item mediaSeed, bindID *uint) error {
|
||||||
dst := filepath.Join(approvedDir, item.storedName)
|
dst := filepath.Join(approvedDir, item.storedName)
|
||||||
if err := ensureMediaLink(item.sourcePath, dst); err != nil {
|
if err := ensureMediaLink(item.sourcePath, dst); err != nil {
|
||||||
|
|||||||
@@ -173,6 +173,28 @@ var specialistRuleFileDrafts = map[string]string{
|
|||||||
- 不承诺工期、不承诺价格——这两项由销售与交付确认。
|
- 不承诺工期、不承诺价格——这两项由销售与交付确认。
|
||||||
- 不替研发承诺功能,不贬低竞品。`,
|
- 不替研发承诺功能,不贬低竞品。`,
|
||||||
|
|
||||||
|
"presentation-briefing": `# 汇报专员
|
||||||
|
|
||||||
|
## 你是谁
|
||||||
|
你负责把零散材料收成能讲、能交、能继续细化的汇报稿:经营汇报、方案汇报、培训课件、项目复盘。
|
||||||
|
你面对的是马上要上台讲的人,他要的是一版结构清楚、页数合适、重点能讲明白的演示稿。
|
||||||
|
|
||||||
|
## 你怎么推进
|
||||||
|
1. 先确认汇报对象、场景和页数限制:给管理层、给客户、给培训学员,写法完全不同。
|
||||||
|
2. 先出结构,不急着写满内容:按「开场结论 → 现状 / 问题 → 方案 / 进展 → 风险 / 下一步」组织。
|
||||||
|
3. 每页只保留一层主结论,宁可留白,不要把 Word 直接搬进 PPT。
|
||||||
|
4. 最后补讲稿备注:每页写清楚要讲什么、强调什么、哪些数字必须口径一致。
|
||||||
|
|
||||||
|
## 输出要求
|
||||||
|
- 先给页纲,再给每页要点,再补讲稿备注。
|
||||||
|
- 所有数字、时间、范围要带口径;没有依据的地方写「待补」,不要自己补齐。
|
||||||
|
- 如果页数明显超了,优先帮用户压缩,而不是机械地继续加页。
|
||||||
|
|
||||||
|
## 边界
|
||||||
|
- 不编造数据、案例、客户反馈。
|
||||||
|
- 不把未确认方案写成既定事实。
|
||||||
|
- 视觉美化可以建议,但当前重点是结构与表达,不承诺设计成品。`,
|
||||||
|
|
||||||
"logistics-fulfillment": `# 履约跟单专员
|
"logistics-fulfillment": `# 履约跟单专员
|
||||||
|
|
||||||
## 你是谁
|
## 你是谁
|
||||||
@@ -249,6 +271,7 @@ var specialistSkillBindings = map[string][]string{
|
|||||||
"wechat-official-account": {"longform-writing", "copy-proofreading", "mind-map"},
|
"wechat-official-account": {"longform-writing", "copy-proofreading", "mind-map"},
|
||||||
"contract-review": {"contract-review", "contract-brief", "batch-extract"},
|
"contract-review": {"contract-review", "contract-brief", "batch-extract"},
|
||||||
"solution-proposal": {"proposal-summary", "ppt-generation", "project-planning", "mind-map"},
|
"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"},
|
"logistics-fulfillment": {"progress-report", "table-cleanup", "email-drafting"},
|
||||||
"hr-email-sorter": {"email-drafting", "batch-extract", "table-cleanup", "ocr-understanding"},
|
"hr-email-sorter": {"email-drafting", "batch-extract", "table-cleanup", "ocr-understanding"},
|
||||||
"resume-processor": {"ocr-understanding", "batch-extract", "table-cleanup", "interview-summary"},
|
"resume-processor": {"ocr-understanding", "batch-extract", "table-cleanup", "interview-summary"},
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
|
|
||||||
// seedSpecialistKeys 从 seedSpecialists 的字面量里抽专员 key。
|
// seedSpecialistKeys 从 seedSpecialists 的字面量里抽专员 key。
|
||||||
// 直接反射那份清单不方便(它建在函数体里),所以从源码文本抓——
|
// 直接反射那份清单不方便(它建在函数体里),所以从源码文本抓——
|
||||||
// 与 skill_keys_test.go 抓前端 availableSkills 是同一套路子。
|
// 与 skill_keys_test.go 抓前端 staticSkillCatalog 是同一套路子。
|
||||||
//
|
//
|
||||||
// 必须限定在 seedSpecialists 到 seedSkillDefinitions 之间:seed.go 里
|
// 必须限定在 seedSpecialists 到 seedSkillDefinitions 之间:seed.go 里
|
||||||
// 技能、动作清单也用 Key: 字面量,而其中 contract-review / report-generation
|
// 技能、动作清单也用 Key: 字面量,而其中 contract-review / report-generation
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ func EnsureSpecialistStructuredRecords(item *model.Specialist) {
|
|||||||
if item == nil {
|
if item == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
EnsureSpecialistRoleCard(item)
|
EnsureSpecialistInteractionCard(item)
|
||||||
if strings.TrimSpace(item.InputsRecordsJSON) == "" {
|
if strings.TrimSpace(item.InputsRecordsJSON) == "" {
|
||||||
item.InputsRecordsJSON = mustJSON(buildInputsRecords(item))
|
item.InputsRecordsJSON = mustJSON(buildInputsRecords(item))
|
||||||
}
|
}
|
||||||
@@ -27,11 +27,11 @@ func EnsureSpecialistStructuredRecords(item *model.Specialist) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnsureSpecialistRoleCard(item *model.Specialist) {
|
func EnsureSpecialistInteractionCard(item *model.Specialist) {
|
||||||
if item == nil || strings.TrimSpace(item.RoleCardJSON) != "" {
|
if item == nil || strings.TrimSpace(item.InteractionCardJSON) != "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
item.RoleCardJSON = mustJSON(map[string]any{
|
item.InteractionCardJSON = mustJSON(map[string]any{
|
||||||
"name": item.Label,
|
"name": item.Label,
|
||||||
"tagline": firstNonEmpty(item.Summary, item.WorkStatus, item.Label),
|
"tagline": firstNonEmpty(item.Summary, item.WorkStatus, item.Label),
|
||||||
"greeting": "我是" + item.Label + ",已经准备好接手这条任务并继续推进。",
|
"greeting": "我是" + item.Label + ",已经准备好接手这条任务并继续推进。",
|
||||||
|
|||||||
@@ -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}`)
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import http from './http'
|
|
||||||
|
|
||||||
export function getSkillDefinitionByKey(key) {
|
|
||||||
return http.get(`/skills/by-key/${key}`)
|
|
||||||
}
|
|
||||||
+2
-1
@@ -1,4 +1,5 @@
|
|||||||
import http from './http'
|
import http from './http'
|
||||||
export function chatWithAssistant(data) {
|
|
||||||
|
export function sendAssistantChatMessage(data) {
|
||||||
return http.post('/assistant/chat', data)
|
return http.post('/assistant/chat', data)
|
||||||
}
|
}
|
||||||
@@ -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}`)
|
||||||
|
}
|
||||||
@@ -65,7 +65,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, nextTick, ref, watch } from 'vue'
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { chatWithAssistant } from '@/api/assistant'
|
import { sendAssistantChatMessage } from '@/api/chat'
|
||||||
import LlmRoutePicker from '@/components/chat/LlmRoutePicker.vue'
|
import LlmRoutePicker from '@/components/chat/LlmRoutePicker.vue'
|
||||||
import { useAiRouteStore } from '@/store/llmRoute'
|
import { useAiRouteStore } from '@/store/llmRoute'
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ async function send() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await aiRouteStore.ensureAiChatRoutes()
|
await aiRouteStore.ensureAiChatRoutes()
|
||||||
const res = await chatWithAssistant({
|
const res = await sendAssistantChatMessage({
|
||||||
message: text,
|
message: text,
|
||||||
mode: 'quick',
|
mode: 'quick',
|
||||||
ai_route_id: aiRouteStore.selectedAiChatRouteId,
|
ai_route_id: aiRouteStore.selectedAiChatRouteId,
|
||||||
|
|||||||
@@ -13,10 +13,12 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { businessApps, availableSkills } from '@/config/workbench'
|
import { staticSkillCatalog } from '@/config/workbench'
|
||||||
|
import { useSkillCatalogStore } from '@/store/skillCatalog'
|
||||||
|
import { useSpecialistCatalogStore } from '@/store/specialistCatalog'
|
||||||
import { GENERAL_ASSISTANT_KEY, useWorkerRuntimeStore } from '@/store/workerRuntime'
|
import { GENERAL_ASSISTANT_KEY, useWorkerRuntimeStore } from '@/store/workerRuntime'
|
||||||
import { openGeneralAssistantInWorkbench, openSpecialistInWorkbench, openSkillInWorkbench } from '@/utils/workbenchNavigation'
|
import { openGeneralAssistantInWorkbench, openSpecialistInWorkbench, openSkillInWorkbench } from '@/utils/workbenchNavigation'
|
||||||
|
|
||||||
@@ -35,6 +37,8 @@ const props = defineProps({
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const workerRuntime = useWorkerRuntimeStore()
|
const workerRuntime = useWorkerRuntimeStore()
|
||||||
|
const skillCatalog = useSkillCatalogStore()
|
||||||
|
const specialistCatalog = useSpecialistCatalogStore()
|
||||||
const {
|
const {
|
||||||
currentFocusedObjectKind,
|
currentFocusedObjectKind,
|
||||||
currentSpecialist,
|
currentSpecialist,
|
||||||
@@ -44,11 +48,11 @@ const {
|
|||||||
const runtimeSkillKey = computed(() => props.skillKey || currentSkillKey.value)
|
const runtimeSkillKey = computed(() => props.skillKey || currentSkillKey.value)
|
||||||
|
|
||||||
const specialistEntry = computed(() =>
|
const specialistEntry = computed(() =>
|
||||||
businessApps.find((app) => app.key === currentSpecialistKey.value) || null
|
specialistCatalog.getByKey(currentSpecialistKey.value)
|
||||||
)
|
)
|
||||||
|
|
||||||
const skillEntry = computed(() =>
|
const skillEntry = computed(() =>
|
||||||
availableSkills.find((skill) => skill.key === runtimeSkillKey.value) || null
|
skillCatalog.getByKey(runtimeSkillKey.value) || staticSkillCatalog.find((skill) => skill.key === runtimeSkillKey.value) || null
|
||||||
)
|
)
|
||||||
const isGeneralAssistant = computed(() => {
|
const isGeneralAssistant = computed(() => {
|
||||||
if (currentSpecialistKey.value === GENERAL_ASSISTANT_KEY) return true
|
if (currentSpecialistKey.value === GENERAL_ASSISTANT_KEY) return true
|
||||||
@@ -120,6 +124,11 @@ function openTarget() {
|
|||||||
console.warn('open skill target failed', error)
|
console.warn('open skill target failed', error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
skillCatalog.hydrate()
|
||||||
|
specialistCatalog.hydrate()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -121,9 +121,11 @@ import { ElMessageBox } from 'element-plus'
|
|||||||
import {
|
import {
|
||||||
ArrowRight, Avatar, Check, Connection, EditPen, MagicStick, Plus, Search, Setting, UploadFilled,
|
ArrowRight, Avatar, Check, Connection, EditPen, MagicStick, Plus, Search, Setting, UploadFilled,
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { availableSkills, businessApps } from '@/config/workbench'
|
import { staticSkillCatalog } from '@/config/workbench'
|
||||||
import { listConnectors } from '@/api/connector'
|
import { listConnectors } from '@/api/connector'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
|
import { useSkillCatalogStore } from '@/store/skillCatalog'
|
||||||
|
import { useSpecialistCatalogStore } from '@/store/specialistCatalog'
|
||||||
import { GENERAL_ASSISTANT_KEY, useWorkerRuntimeStore } from '@/store/workerRuntime'
|
import { GENERAL_ASSISTANT_KEY, useWorkerRuntimeStore } from '@/store/workerRuntime'
|
||||||
import { openGeneralAssistantInWorkbench, openSkillInWorkbench, openSpecialistInWorkbench } from '@/utils/workbenchNavigation'
|
import { openGeneralAssistantInWorkbench, openSkillInWorkbench, openSpecialistInWorkbench } from '@/utils/workbenchNavigation'
|
||||||
|
|
||||||
@@ -156,6 +158,8 @@ const POPPER_OPTIONS = {
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
|
const skillCatalog = useSkillCatalogStore()
|
||||||
|
const specialistCatalog = useSpecialistCatalogStore()
|
||||||
const workerRuntime = useWorkerRuntimeStore()
|
const workerRuntime = useWorkerRuntimeStore()
|
||||||
const isAdmin = computed(() => auth.isAdmin)
|
const isAdmin = computed(() => auth.isAdmin)
|
||||||
const open = ref(false)
|
const open = ref(false)
|
||||||
@@ -176,7 +180,7 @@ function routable(path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const generalAssistantItem = computed(() => {
|
const generalAssistantItem = computed(() => {
|
||||||
const item = availableSkills.find((skill) => skill.key === 'smart-assistant')
|
const item = skillCatalog.getByKey('smart-assistant') || staticSkillCatalog.find((skill) => skill.key === 'smart-assistant')
|
||||||
if (!item) return null
|
if (!item) return null
|
||||||
return {
|
return {
|
||||||
key: GENERAL_ASSISTANT_KEY,
|
key: GENERAL_ASSISTANT_KEY,
|
||||||
@@ -209,7 +213,7 @@ const entries = computed(() => {
|
|||||||
empty: '暂无可用专员',
|
empty: '暂无可用专员',
|
||||||
items: [
|
items: [
|
||||||
...(generalAssistantItem.value ? [generalAssistantItem.value] : []),
|
...(generalAssistantItem.value ? [generalAssistantItem.value] : []),
|
||||||
...businessApps
|
...specialistCatalog.specialists
|
||||||
.filter((app) => routable(app.objectEntryRoute))
|
.filter((app) => routable(app.objectEntryRoute))
|
||||||
// key 要在切换时用:选了专员就等于把当前任务改挂到它名下。
|
// key 要在切换时用:选了专员就等于把当前任务改挂到它名下。
|
||||||
.map((app) => ({
|
.map((app) => ({
|
||||||
@@ -232,7 +236,7 @@ const entries = computed(() => {
|
|||||||
type: 'resource',
|
type: 'resource',
|
||||||
searchPlaceholder: '搜索技能',
|
searchPlaceholder: '搜索技能',
|
||||||
empty: '暂无可用技能',
|
empty: '暂无可用技能',
|
||||||
items: availableSkills
|
items: skillCatalog.skills
|
||||||
.filter((skill) => skill.key !== 'smart-assistant')
|
.filter((skill) => skill.key !== 'smart-assistant')
|
||||||
.filter((skill) => routable(skill.objectEntryRoute))
|
.filter((skill) => routable(skill.objectEntryRoute))
|
||||||
.map((skill) => ({
|
.map((skill) => ({
|
||||||
@@ -289,6 +293,8 @@ watch(activeKey, () => { keyword.value = '' })
|
|||||||
watch(open, (value) => { if (value) activeKey.value = entries.value[0]?.key || '' })
|
watch(open, (value) => { if (value) activeKey.value = entries.value[0]?.key || '' })
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await skillCatalog.hydrate()
|
||||||
|
await specialistCatalog.hydrate()
|
||||||
if (!props.showConnectors) return
|
if (!props.showConnectors) return
|
||||||
try {
|
try {
|
||||||
const res = await listConnectors()
|
const res = await listConnectors()
|
||||||
|
|||||||
@@ -75,8 +75,8 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed, ref, watch } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { availableSkills } from '@/config/workbench'
|
|
||||||
import { settingsForSkill } from '@/config/skillSettings'
|
import { settingsForSkill } from '@/config/skillSettings'
|
||||||
|
import { useSkillCatalogStore } from '@/store/skillCatalog'
|
||||||
import { useSavedFlash, useSkillSettingsStore } from '@/store/skillSettings'
|
import { useSavedFlash, useSkillSettingsStore } from '@/store/skillSettings'
|
||||||
import { useWorkerRuntimeStore } from '@/store/workerRuntime'
|
import { useWorkerRuntimeStore } from '@/store/workerRuntime'
|
||||||
import { buildActionRecords, markFlowState } from '@/utils/specialistFlow'
|
import { buildActionRecords, markFlowState } from '@/utils/specialistFlow'
|
||||||
@@ -88,6 +88,7 @@ const props = defineProps({
|
|||||||
})
|
})
|
||||||
|
|
||||||
const settings = useSkillSettingsStore()
|
const settings = useSkillSettingsStore()
|
||||||
|
const skillCatalog = useSkillCatalogStore()
|
||||||
const workerRuntime = useWorkerRuntimeStore()
|
const workerRuntime = useWorkerRuntimeStore()
|
||||||
const { saving, mark } = useSavedFlash()
|
const { saving, mark } = useSavedFlash()
|
||||||
|
|
||||||
@@ -98,7 +99,7 @@ const skillKey = computed(() => {
|
|||||||
return workerRuntime.currentSkillKey || 'smart-assistant'
|
return workerRuntime.currentSkillKey || 'smart-assistant'
|
||||||
})
|
})
|
||||||
|
|
||||||
const skill = computed(() => availableSkills.find((item) => item.key === skillKey.value) || null)
|
const skill = computed(() => skillCatalog.getByKey(skillKey.value) || null)
|
||||||
const skillLabel = computed(() => skill.value?.label || '')
|
const skillLabel = computed(() => skill.value?.label || '')
|
||||||
const cfg = computed(() => settingsForSkill(skillKey.value) || { ico: '', settings: [] })
|
const cfg = computed(() => settingsForSkill(skillKey.value) || { ico: '', settings: [] })
|
||||||
|
|
||||||
@@ -187,6 +188,8 @@ watch(
|
|||||||
},
|
},
|
||||||
{ deep: true },
|
{ deep: true },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
skillCatalog.hydrate()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -23,19 +23,20 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue'
|
import { computed, onMounted } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { businessApps } from '@/config/workbench'
|
import { useSpecialistCatalogStore } from '@/store/specialistCatalog'
|
||||||
import { GENERAL_ASSISTANT_KEY, useWorkerRuntimeStore } from '@/store/workerRuntime'
|
import { GENERAL_ASSISTANT_KEY, useWorkerRuntimeStore } from '@/store/workerRuntime'
|
||||||
import { openSpecialistInWorkbench } from '@/utils/workbenchNavigation'
|
import { openSpecialistInWorkbench } from '@/utils/workbenchNavigation'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const workerRuntime = useWorkerRuntimeStore()
|
const workerRuntime = useWorkerRuntimeStore()
|
||||||
|
const specialistCatalog = useSpecialistCatalogStore()
|
||||||
|
|
||||||
// 专员目录里的配置。objectEntryRoute 要从这儿取,store 里那份是接口返回的,可能还没到。
|
// 专员目录里的配置。objectEntryRoute 要从这儿取,store 里那份是接口返回的,可能还没到。
|
||||||
const catalogEntry = computed(() =>
|
const catalogEntry = computed(() =>
|
||||||
businessApps.find((app) => app.key === workerRuntime.currentSpecialistKey) || null
|
specialistCatalog.getByKey(workerRuntime.currentSpecialistKey)
|
||||||
)
|
)
|
||||||
|
|
||||||
const visible = computed(() => {
|
const visible = computed(() => {
|
||||||
@@ -65,6 +66,10 @@ function openWorkbench() {
|
|||||||
console.warn('open specialist chip failed', error)
|
console.warn('open specialist chip failed', error)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
specialistCatalog.hydrate()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -155,13 +155,14 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { computed, onMounted, ref, watch } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { availableSkills } from '@/config/workbench'
|
|
||||||
import { DEFAULT_SKILL_KEY, GENERAL_ASSISTANT_KEY, useWorkerRuntimeStore } from '@/store/workerRuntime'
|
import { DEFAULT_SKILL_KEY, GENERAL_ASSISTANT_KEY, useWorkerRuntimeStore } from '@/store/workerRuntime'
|
||||||
|
import { useSkillCatalogStore } from '@/store/skillCatalog'
|
||||||
import { artifactsOfStep, buildActionRecords, buildResultRecords, markFlowState } from '@/utils/specialistFlow'
|
import { artifactsOfStep, buildActionRecords, buildResultRecords, markFlowState } from '@/utils/specialistFlow'
|
||||||
|
|
||||||
const emit = defineEmits(['collapse'])
|
const emit = defineEmits(['collapse'])
|
||||||
|
|
||||||
const workerRuntime = useWorkerRuntimeStore()
|
const workerRuntime = useWorkerRuntimeStore()
|
||||||
|
const skillCatalog = useSkillCatalogStore()
|
||||||
const {
|
const {
|
||||||
currentFocusedObjectKey,
|
currentFocusedObjectKey,
|
||||||
currentFocusedObjectKind,
|
currentFocusedObjectKind,
|
||||||
@@ -176,7 +177,7 @@ const tab = ref('flow')
|
|||||||
|
|
||||||
const specialist = computed(() => currentSpecialist.value)
|
const specialist = computed(() => currentSpecialist.value)
|
||||||
const skill = computed(() =>
|
const skill = computed(() =>
|
||||||
availableSkills.find((item) => item.key === currentFocusedObjectKey.value) || null
|
skillCatalog.getByKey(currentFocusedObjectKey.value)
|
||||||
)
|
)
|
||||||
const panelKind = computed(() => {
|
const panelKind = computed(() => {
|
||||||
if (
|
if (
|
||||||
@@ -473,7 +474,10 @@ async function refresh() {
|
|||||||
if (taskId) await workerRuntime.loadTaskDetail(taskId)
|
if (taskId) await workerRuntime.loadTaskDetail(taskId)
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(refresh)
|
onMounted(async () => {
|
||||||
|
await skillCatalog.hydrate()
|
||||||
|
refresh()
|
||||||
|
})
|
||||||
watch(
|
watch(
|
||||||
() => [currentSpecialistKey.value, currentTaskId.value],
|
() => [currentSpecialistKey.value, currentTaskId.value],
|
||||||
refresh,
|
refresh,
|
||||||
|
|||||||
@@ -1,158 +0,0 @@
|
|||||||
export const BASE_CATALOG_APP_COUNT = 3
|
|
||||||
|
|
||||||
function formatAppCode(index) {
|
|
||||||
return String(index).padStart(2, '0')
|
|
||||||
}
|
|
||||||
|
|
||||||
function withAppCodes(items, start = BASE_CATALOG_APP_COUNT + 1) {
|
|
||||||
return items.map((item, index) => {
|
|
||||||
const sequence = start + index
|
|
||||||
const suffix = formatAppCode(sequence)
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
display_code: item.display_code || `应${suffix}`,
|
|
||||||
eailogic_code: item.eailogic_code || `EAI-A-${suffix}`,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const productizedApps = withAppCodes([
|
|
||||||
{
|
|
||||||
key: 'docs-center',
|
|
||||||
label: '文档中心',
|
|
||||||
badge: '办公中枢',
|
|
||||||
kind: '结果沉淀',
|
|
||||||
color: '#2563eb',
|
|
||||||
summary: '集中查看任务上传物与产物,像微趣那样把结果留住、找得到、能继续复用。',
|
|
||||||
description: '把任务里产生的上传材料和交付结果集中收在一个地方,方便继续搜索、回看、复制和打开原任务。',
|
|
||||||
tags: ['上传物', '产物', '结果沉淀', '集中检索'],
|
|
||||||
iconText: '档',
|
|
||||||
coverTone: 'linear-gradient(135deg, #2563eb 0%, #60a5fa 100%)',
|
|
||||||
prompts: ['看我最近生成过哪些文档和产物', '按任务回看最近交付结果', '集中查看上传材料和生成内容'],
|
|
||||||
installState: 'installed',
|
|
||||||
openRoute: '/docs-center',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'app-meeting-minutes',
|
|
||||||
label: '会议纪要',
|
|
||||||
badge: '成品应用',
|
|
||||||
kind: '办公速用',
|
|
||||||
color: '#0f766e',
|
|
||||||
summary: '直接进入会议纪要应用,快速整理结论、待办和责任人。',
|
|
||||||
description: '适合把会议记录、语音转写或零散要点收成正式纪要和行动清单。',
|
|
||||||
tags: ['会议纪要', '行动项', '责任人', '汇总'],
|
|
||||||
iconText: '会',
|
|
||||||
coverTone: 'linear-gradient(135deg, #0f766e 0%, #14b8a6 100%)',
|
|
||||||
prompts: ['把这段会议记录整理成正式纪要', '提炼结论、行动项和责任人', '根据讨论内容生成会后同步稿'],
|
|
||||||
installState: 'installed',
|
|
||||||
skillKey: 'meeting-minutes',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'app-contract-brief',
|
|
||||||
label: '合同摘要',
|
|
||||||
badge: '成品应用',
|
|
||||||
kind: '法务办公',
|
|
||||||
color: '#b91c1c',
|
|
||||||
summary: '直接进入合同摘要应用,快速拿到条款要点、风险和待确认项。',
|
|
||||||
description: '适合采购合同、合作协议和服务条款的业务预读与快速审查。',
|
|
||||||
tags: ['合同摘要', '风险提示', '待确认项', '快速预读'],
|
|
||||||
iconText: '合',
|
|
||||||
coverTone: 'linear-gradient(135deg, #991b1b 0%, #ef4444 100%)',
|
|
||||||
prompts: ['把这份采购合同提炼成要点摘要', '标记高风险条款和待确认事项', '生成业务负责人可读的合同摘要'],
|
|
||||||
installState: 'installed',
|
|
||||||
skillKey: 'contract-brief',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'app-proposal-summary',
|
|
||||||
label: '方案摘要',
|
|
||||||
badge: '成品应用',
|
|
||||||
kind: '汇报加速',
|
|
||||||
color: '#7c3aed',
|
|
||||||
summary: '直接进入方案摘要应用,把长方案压成管理层可读的短版本。',
|
|
||||||
description: '适合售前方案、项目方案和内部提案的结论压缩与亮点提炼。',
|
|
||||||
tags: ['方案摘要', '亮点提炼', '管理层预读', '汇报短版'],
|
|
||||||
iconText: '方',
|
|
||||||
coverTone: 'linear-gradient(135deg, #6d28d9 0%, #a78bfa 100%)',
|
|
||||||
prompts: ['把这份解决方案压成一版管理层摘要', '提炼亮点和实施建议', '生成汇报短版和亮点清单'],
|
|
||||||
installState: 'installed',
|
|
||||||
skillKey: 'proposal-summary',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'app-progress-report',
|
|
||||||
label: '周报 / 日报',
|
|
||||||
badge: '成品应用',
|
|
||||||
kind: '进展同步',
|
|
||||||
color: '#1d4ed8',
|
|
||||||
summary: '直接进入周报 / 日报应用,快速整理进展、风险和下步计划。',
|
|
||||||
description: '适合把聊天记录、事项推进和工作流水收成日报、周报或阶段同步稿。',
|
|
||||||
tags: ['周报', '日报', '风险同步', '下步计划'],
|
|
||||||
iconText: '报',
|
|
||||||
coverTone: 'linear-gradient(135deg, #1d4ed8 0%, #60a5fa 100%)',
|
|
||||||
prompts: ['根据这些工作记录整理一版项目周报', '整理日报并补风险和计划', '生成给负责人的阶段同步'],
|
|
||||||
installState: 'installed',
|
|
||||||
skillKey: 'progress-report',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'app-ppt-generation',
|
|
||||||
label: 'PPT 生成',
|
|
||||||
badge: '成品应用',
|
|
||||||
kind: '汇报材料',
|
|
||||||
color: '#4f46e5',
|
|
||||||
summary: '直接进入 PPT 应用,快速拿到大纲、讲稿和演示骨架。',
|
|
||||||
description: '适合经营汇报、方案汇报、培训课件和项目复盘的演示材料生成。',
|
|
||||||
tags: ['PPT', '演示大纲', '讲稿备注', '汇报骨架'],
|
|
||||||
iconText: 'P',
|
|
||||||
coverTone: 'linear-gradient(135deg, #4338ca 0%, #818cf8 100%)',
|
|
||||||
prompts: ['帮我生成一版汇报 PPT 结构', '输出页面大纲和讲稿备注', '做一版培训课件骨架'],
|
|
||||||
installState: 'installed',
|
|
||||||
skillKey: 'ppt-generation',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'app-mind-map',
|
|
||||||
label: '思维导图',
|
|
||||||
badge: '成品应用',
|
|
||||||
kind: '结构整理',
|
|
||||||
color: '#ea580c',
|
|
||||||
summary: '直接进入思维导图应用,快速把主题拆成树状结构。',
|
|
||||||
description: '适合方案拆解、培训提纲、需求梳理和知识结构整理。',
|
|
||||||
tags: ['思维导图', '结构拆解', '提纲', '知识整理'],
|
|
||||||
iconText: '图',
|
|
||||||
coverTone: 'linear-gradient(135deg, #c2410c 0%, #fb923c 100%)',
|
|
||||||
prompts: ['把这个主题整理成思维导图', '按树形结构拆解方案', '生成一版培训提纲导图'],
|
|
||||||
installState: 'installed',
|
|
||||||
skillKey: 'mind-map',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'app-ocr-understanding',
|
|
||||||
label: 'OCR 图文提取',
|
|
||||||
badge: '成品应用',
|
|
||||||
kind: '图文理解',
|
|
||||||
color: '#0ea5e9',
|
|
||||||
summary: '直接进入 OCR 应用,上传图片后提取文字并沉淀到任务产物。',
|
|
||||||
description: '适合截图、照片、扫描件和证据图片的文本抽取与继续整理。',
|
|
||||||
tags: ['OCR', '图文提取', '图片识别', '证据整理'],
|
|
||||||
iconText: '扫',
|
|
||||||
coverTone: 'linear-gradient(135deg, #0284c7 0%, #38bdf8 100%)',
|
|
||||||
prompts: ['识别这张图片里的文字', '把扫描件抽成可继续编辑的文本', '提取截图中的关键信息'],
|
|
||||||
installState: 'installed',
|
|
||||||
skillKey: 'ocr-understanding',
|
|
||||||
},
|
|
||||||
])
|
|
||||||
|
|
||||||
export const STATIC_APP_CODE_COUNT = BASE_CATALOG_APP_COUNT + productizedApps.length
|
|
||||||
|
|
||||||
export function appOpenRoute(app) {
|
|
||||||
if (!app) return '/catalog/apps'
|
|
||||||
if (app.openRoute) return app.openRoute
|
|
||||||
if (app.skillKey) {
|
|
||||||
return {
|
|
||||||
path: '/home',
|
|
||||||
query: {
|
|
||||||
app_skill: app.skillKey,
|
|
||||||
app_name: app.label || '',
|
|
||||||
app_prompt: app.prompts?.[0] || '',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return '/catalog/apps'
|
|
||||||
}
|
|
||||||
@@ -13,22 +13,14 @@ import {
|
|||||||
Promotion,
|
Promotion,
|
||||||
Reading,
|
Reading,
|
||||||
} from '@element-plus/icons-vue'
|
} from '@element-plus/icons-vue'
|
||||||
import { availableSkills } from '@/config/workbench'
|
import { staticSkillCatalog } from '@/config/workbench'
|
||||||
|
|
||||||
export const SKILL_KEY_LABELS = Object.fromEntries(
|
export const SKILL_KEY_LABELS = Object.fromEntries(
|
||||||
availableSkills.map((item) => [item.key, item.label])
|
staticSkillCatalog.map((item) => [item.key, item.label])
|
||||||
)
|
|
||||||
|
|
||||||
export const LEGACY_SKILL_ROUTE_TO_KEY = Object.fromEntries(
|
|
||||||
availableSkills
|
|
||||||
.filter((item) => item.legacyObjectEntryRoute)
|
|
||||||
.map((item) => [item.legacyObjectEntryRoute, item.key])
|
|
||||||
)
|
)
|
||||||
|
|
||||||
export function normalizeSkillKey(value) {
|
export function normalizeSkillKey(value) {
|
||||||
const raw = String(value || '').trim()
|
return String(value || '').trim()
|
||||||
if (!raw) return ''
|
|
||||||
return LEGACY_SKILL_ROUTE_TO_KEY[raw] || raw
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeSkillKeys(values = []) {
|
export function normalizeSkillKeys(values = []) {
|
||||||
@@ -96,6 +88,16 @@ export const projectTemplates = [
|
|||||||
specialistKeys: ['report-generation'],
|
specialistKeys: ['report-generation'],
|
||||||
skillKeys: ['report-generation', 'document-translate'],
|
skillKeys: ['report-generation', 'document-translate'],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'presentation-briefing',
|
||||||
|
title: '汇报 PPT',
|
||||||
|
desc: '面向管理层、客户和培训场景的演示稿交付',
|
||||||
|
icon: Promotion,
|
||||||
|
prompt:
|
||||||
|
'本项目按汇报交付口径推进:先确认汇报对象、场景、页数和主线,再生成页面结构、每页要点和讲稿备注。所有数字、时间和范围必须带口径;没有依据的地方明确标“待补”,不要为了好看自行补齐。',
|
||||||
|
specialistKeys: ['presentation-briefing'],
|
||||||
|
skillKeys: ['ppt-generation', 'proposal-summary', 'mind-map'],
|
||||||
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
export function findProjectTemplate(id) {
|
export function findProjectTemplate(id) {
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ const MODE_SUM = { quick: '快速模式 · 直出回答', expert: '深思模式
|
|||||||
|
|
||||||
export const skillSettings = {
|
export const skillSettings = {
|
||||||
// 缺省技能。它只有一个设置:模式。原来散在门厅上的两颗胶囊就是它。
|
// 缺省技能。它只有一个设置:模式。原来散在门厅上的两颗胶囊就是它。
|
||||||
// mode 是真发出去的:SmartAssistantPage 的 send() → chatWithAssistant({ message, mode })。
|
// mode 是真发出去的:SmartAssistantPage 的 send() → sendAssistantChatMessage({ message, mode })。
|
||||||
'smart-assistant': {
|
'smart-assistant': {
|
||||||
ico: '通',
|
ico: '通',
|
||||||
settings: [
|
settings: [
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const WORKBENCH_ROUTE = '/home'
|
export const WORKBENCH_ROUTE = '/home'
|
||||||
|
|
||||||
function createRoleCard({
|
function createInteractionCard({
|
||||||
greeting,
|
greeting,
|
||||||
tagline,
|
tagline,
|
||||||
openingPrompt,
|
openingPrompt,
|
||||||
@@ -50,24 +50,22 @@ function createSkillDefinition({
|
|||||||
key,
|
key,
|
||||||
label,
|
label,
|
||||||
caption,
|
caption,
|
||||||
legacyObjectEntryRoute = '',
|
|
||||||
color = '#409eff',
|
color = '#409eff',
|
||||||
capabilityTags = [],
|
capabilityTags = [],
|
||||||
workflowSchema = [],
|
workflowSchema = [],
|
||||||
artifactSchema = [],
|
artifactSchema = [],
|
||||||
roleCard,
|
interactionCard,
|
||||||
}) {
|
}) {
|
||||||
return {
|
return {
|
||||||
key,
|
key,
|
||||||
label,
|
label,
|
||||||
caption,
|
caption,
|
||||||
objectEntryRoute: WORKBENCH_ROUTE,
|
objectEntryRoute: WORKBENCH_ROUTE,
|
||||||
...(legacyObjectEntryRoute ? { legacyObjectEntryRoute } : {}),
|
|
||||||
color,
|
color,
|
||||||
capabilityTags,
|
capabilityTags,
|
||||||
workflowSchema,
|
workflowSchema,
|
||||||
artifactSchema,
|
artifactSchema,
|
||||||
roleCard: createRoleCard(roleCard),
|
interactionCard: createInteractionCard(interactionCard),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,8 +99,8 @@ export const connectors = withObjectCodes([
|
|||||||
{ key: 'console', label: '控制台', caption: '连接器状态、凭据与健康监控', pageRoute: '/console' },
|
{ key: 'console', label: '控制台', caption: '连接器状态、凭据与健康监控', pageRoute: '/console' },
|
||||||
], { displayPrefix: '连', logicPrefix: 'C' })
|
], { displayPrefix: '连', logicPrefix: 'C' })
|
||||||
|
|
||||||
// AI 技能入口(Chat-First 主入口)—— 全部走 ChatLayout 聊天优先界面
|
// AI 技能入口(Chat-First 主入口)—— 全部走 ChatLayout 聊天优先界面。
|
||||||
export const availableSkills = withObjectCodes([
|
export const staticSkillCatalog = withObjectCodes([
|
||||||
createSkillDefinition({
|
createSkillDefinition({
|
||||||
key: 'smart-assistant',
|
key: 'smart-assistant',
|
||||||
label: '通用助手',
|
label: '通用助手',
|
||||||
@@ -151,7 +149,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '博昇AI数字员工,您说,我做',
|
greeting: '博昇AI数字员工,您说,我做',
|
||||||
tagline: '今天帮你做些什么?描述你的任务,或先挂载技能与材料',
|
tagline: '今天帮你做些什么?描述你的任务,或先挂载技能与材料',
|
||||||
openingPrompt: '直接告诉我目标,或者把材料贴给我,我来帮你起步。',
|
openingPrompt: '直接告诉我目标,或者把材料贴给我,我来帮你起步。',
|
||||||
@@ -167,7 +165,6 @@ export const availableSkills = withObjectCodes([
|
|||||||
key: 'document-translate',
|
key: 'document-translate',
|
||||||
label: '文档翻译',
|
label: '文档翻译',
|
||||||
caption: '译文生成 / 术语统一 / 多格式导出',
|
caption: '译文生成 / 术语统一 / 多格式导出',
|
||||||
legacyObjectEntryRoute: '/tools/document-translate',
|
|
||||||
color: '#67c23a',
|
color: '#67c23a',
|
||||||
capabilityTags: ['术语统一', '语气对齐', '格式保留', '双语摘要'],
|
capabilityTags: ['术语统一', '语气对齐', '格式保留', '双语摘要'],
|
||||||
workflowSchema: [
|
workflowSchema: [
|
||||||
@@ -212,7 +209,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先识别语境和术语,再生成可交付译文,避免只做逐句直译。',
|
greeting: '先识别语境和术语,再生成可交付译文,避免只做逐句直译。',
|
||||||
tagline: '不仅翻译文字,还会先统一术语、语气和交付格式。',
|
tagline: '不仅翻译文字,还会先统一术语、语气和交付格式。',
|
||||||
openingPrompt: '把原文贴给我,并说明目标语言、受众和语气要求;如果有术语表,也一起给我。',
|
openingPrompt: '把原文贴给我,并说明目标语言、受众和语气要求;如果有术语表,也一起给我。',
|
||||||
@@ -228,7 +225,6 @@ export const availableSkills = withObjectCodes([
|
|||||||
key: 'copy-proofreading',
|
key: 'copy-proofreading',
|
||||||
label: '文案校对',
|
label: '文案校对',
|
||||||
caption: '问题定位 / 表达优化 / 风格收口',
|
caption: '问题定位 / 表达优化 / 风格收口',
|
||||||
legacyObjectEntryRoute: '/tools/copy-proofreading',
|
|
||||||
color: '#e6a23c',
|
color: '#e6a23c',
|
||||||
capabilityTags: ['错别字检查', '语病修正', '语气统一', '风格优化'],
|
capabilityTags: ['错别字检查', '语病修正', '语气统一', '风格优化'],
|
||||||
workflowSchema: [
|
workflowSchema: [
|
||||||
@@ -273,7 +269,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先找出问题句和语气风险,再给出能直接使用的修订版本。',
|
greeting: '先找出问题句和语气风险,再给出能直接使用的修订版本。',
|
||||||
tagline: '不是只改错字,而是把整段表达修顺、修稳、修清楚。',
|
tagline: '不是只改错字,而是把整段表达修顺、修稳、修清楚。',
|
||||||
openingPrompt: '把文案贴给我,并告诉我这段内容要发给谁、希望保留什么语气。',
|
openingPrompt: '把文案贴给我,并告诉我这段内容要发给谁、希望保留什么语气。',
|
||||||
@@ -289,7 +285,6 @@ export const availableSkills = withObjectCodes([
|
|||||||
key: 'audio-transcribe',
|
key: 'audio-transcribe',
|
||||||
label: '语音转写',
|
label: '语音转写',
|
||||||
caption: '转写 / 分段 / 重点整理',
|
caption: '转写 / 分段 / 重点整理',
|
||||||
legacyObjectEntryRoute: '/tools/audio-transcribe',
|
|
||||||
color: '#5263c3',
|
color: '#5263c3',
|
||||||
capabilityTags: ['逐字转写', '说话人分段', '重点提炼', '纪要整理'],
|
capabilityTags: ['逐字转写', '说话人分段', '重点提炼', '纪要整理'],
|
||||||
workflowSchema: [
|
workflowSchema: [
|
||||||
@@ -334,7 +329,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把语音内容转成可靠文本,再继续整理重点、纪要和后续动作。',
|
greeting: '先把语音内容转成可靠文本,再继续整理重点、纪要和后续动作。',
|
||||||
tagline: '把音频从“听过”变成“可查、可引用、可交付”的文本资产。',
|
tagline: '把音频从“听过”变成“可查、可引用、可交付”的文本资产。',
|
||||||
openingPrompt: '把音频内容发给我,或者告诉我语言、场景和你最终想拿到什么结果。',
|
openingPrompt: '把音频内容发给我,或者告诉我语言、场景和你最终想拿到什么结果。',
|
||||||
@@ -350,7 +345,6 @@ export const availableSkills = withObjectCodes([
|
|||||||
key: 'batch-extract',
|
key: 'batch-extract',
|
||||||
label: '批量提取',
|
label: '批量提取',
|
||||||
caption: '字段设计 / 批量抽取 / 异常复核',
|
caption: '字段设计 / 批量抽取 / 异常复核',
|
||||||
legacyObjectEntryRoute: '/tools/batch-extract',
|
|
||||||
color: '#39b07a',
|
color: '#39b07a',
|
||||||
capabilityTags: ['字段抽取', '批量处理', '结构化输出', '异常回看'],
|
capabilityTags: ['字段抽取', '批量处理', '结构化输出', '异常回看'],
|
||||||
workflowSchema: [
|
workflowSchema: [
|
||||||
@@ -395,7 +389,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把字段方案定义清楚,再批量抽取,并把异常项单独挑出来。',
|
greeting: '先把字段方案定义清楚,再批量抽取,并把异常项单独挑出来。',
|
||||||
tagline: '不是只把内容抓出来,而是直接整理成后续系统能用的结构化结果。',
|
tagline: '不是只把内容抓出来,而是直接整理成后续系统能用的结构化结果。',
|
||||||
openingPrompt: '告诉我想抽哪些字段、输出成什么格式、哪些情况需要标记为异常。',
|
openingPrompt: '告诉我想抽哪些字段、输出成什么格式、哪些情况需要标记为异常。',
|
||||||
@@ -411,7 +405,6 @@ export const availableSkills = withObjectCodes([
|
|||||||
key: 'contract-review',
|
key: 'contract-review',
|
||||||
label: '合同审查',
|
label: '合同审查',
|
||||||
caption: '条款识别 / 风险判断 / 审查交付',
|
caption: '条款识别 / 风险判断 / 审查交付',
|
||||||
legacyObjectEntryRoute: '/tools/contract-review',
|
|
||||||
color: '#f56c6c',
|
color: '#f56c6c',
|
||||||
capabilityTags: ['风险识别', '待确认项', '红线建议', '审查结果'],
|
capabilityTags: ['风险识别', '待确认项', '红线建议', '审查结果'],
|
||||||
workflowSchema: [
|
workflowSchema: [
|
||||||
@@ -456,7 +449,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先定位高风险条款和待确认项,再整理成可交付的审查结果。',
|
greeting: '先定位高风险条款和待确认项,再整理成可交付的审查结果。',
|
||||||
tagline: '不是只报风险,而是把风险依据、红线建议和后续确认项一起交出来。',
|
tagline: '不是只报风险,而是把风险依据、红线建议和后续确认项一起交出来。',
|
||||||
openingPrompt: '把合同发给我,并告诉我这次重点想看哪些条款,或者你站在哪一方立场。',
|
openingPrompt: '把合同发给我,并告诉我这次重点想看哪些条款,或者你站在哪一方立场。',
|
||||||
@@ -472,7 +465,6 @@ export const availableSkills = withObjectCodes([
|
|||||||
key: 'report-generation',
|
key: 'report-generation',
|
||||||
label: '报告生成',
|
label: '报告生成',
|
||||||
caption: '材料归并 / 结构规划 / 正式输出',
|
caption: '材料归并 / 结构规划 / 正式输出',
|
||||||
legacyObjectEntryRoute: '/report-gen',
|
|
||||||
color: '#9c27b0',
|
color: '#9c27b0',
|
||||||
capabilityTags: ['日报周报', '复盘总结', '章节规划', '管理摘要'],
|
capabilityTags: ['日报周报', '复盘总结', '章节规划', '管理摘要'],
|
||||||
workflowSchema: [
|
workflowSchema: [
|
||||||
@@ -517,7 +509,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把材料归并成结构,再输出可直接提交的正式报告。',
|
greeting: '先把材料归并成结构,再输出可直接提交的正式报告。',
|
||||||
tagline: '不只是把记录拼起来,而是把过程信息收成完整、清楚、可汇报的结果。',
|
tagline: '不只是把记录拼起来,而是把过程信息收成完整、清楚、可汇报的结果。',
|
||||||
openingPrompt: '把材料给我,并告诉我报告类型、受众、周期和你最想突出哪些重点。',
|
openingPrompt: '把材料给我,并告诉我报告类型、受众、周期和你最想突出哪些重点。',
|
||||||
@@ -577,7 +569,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把演示逻辑理顺,再生成一版能继续加工的 PPT 骨架。',
|
greeting: '先把演示逻辑理顺,再生成一版能继续加工的 PPT 骨架。',
|
||||||
tagline: '适合经营汇报、售前方案、培训课件和项目复盘。',
|
tagline: '适合经营汇报、售前方案、培训课件和项目复盘。',
|
||||||
openingPrompt: '告诉我主题、受众、页数和你已有的材料,我先帮你起一版演示结构。',
|
openingPrompt: '告诉我主题、受众、页数和你已有的材料,我先帮你起一版演示结构。',
|
||||||
@@ -637,7 +629,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把主题主线和分支结构压出来,再输出可继续编辑的导图。',
|
greeting: '先把主题主线和分支结构压出来,再输出可继续编辑的导图。',
|
||||||
tagline: '适合任务拆解、知识梳理、会议结构和项目地图。',
|
tagline: '适合任务拆解、知识梳理、会议结构和项目地图。',
|
||||||
openingPrompt: '给我一句话主题,或者贴一段材料,我先帮你拆一版导图骨架。',
|
openingPrompt: '给我一句话主题,或者贴一段材料,我先帮你拆一版导图骨架。',
|
||||||
@@ -697,7 +689,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先搭长文结构,再生成能继续细化的正文草稿。',
|
greeting: '先搭长文结构,再生成能继续细化的正文草稿。',
|
||||||
tagline: '统一承接论文、方案、制度文档和长文章任务。',
|
tagline: '统一承接论文、方案、制度文档和长文章任务。',
|
||||||
openingPrompt: '告诉我文体、主题、受众和篇幅要求,我先帮你起大纲。',
|
openingPrompt: '告诉我文体、主题、受众和篇幅要求,我先帮你起大纲。',
|
||||||
@@ -757,7 +749,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把文本里的噪音和格式问题清掉,再输出可继续使用的结果。',
|
greeting: '先把文本里的噪音和格式问题清掉,再输出可继续使用的结果。',
|
||||||
tagline: '适合高频复制粘贴、去 Markdown 和统一格式的场景。',
|
tagline: '适合高频复制粘贴、去 Markdown 和统一格式的场景。',
|
||||||
openingPrompt: '把文本贴给我,并告诉我是要去 Markdown、做统计还是统一格式。',
|
openingPrompt: '把文本贴给我,并告诉我是要去 Markdown、做统计还是统一格式。',
|
||||||
@@ -817,7 +809,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先识别图像里的文字和结构,再整理成可继续使用的摘要或字段表。',
|
greeting: '先识别图像里的文字和结构,再整理成可继续使用的摘要或字段表。',
|
||||||
tagline: '适合截图、扫描件、票据、表格照片和图文混合材料。',
|
tagline: '适合截图、扫描件、票据、表格照片和图文混合材料。',
|
||||||
openingPrompt: '把图片内容贴给我,或者说明你想识别什么、抽哪些字段。',
|
openingPrompt: '把图片内容贴给我,或者说明你想识别什么、抽哪些字段。',
|
||||||
@@ -877,7 +869,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'txt',
|
fileFormat: 'txt',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把讨论收成结论和行动项,再输出一版能直接同步的会议纪要。',
|
greeting: '先把讨论收成结论和行动项,再输出一版能直接同步的会议纪要。',
|
||||||
tagline: '适合例会、评审会、复盘会和项目同步场景。',
|
tagline: '适合例会、评审会、复盘会和项目同步场景。',
|
||||||
openingPrompt: '把会议记录贴给我,或者说明这次会议的主题和你最想保留哪些结论。',
|
openingPrompt: '把会议记录贴给我,或者说明这次会议的主题和你最想保留哪些结论。',
|
||||||
@@ -937,7 +929,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把目标拆成阶段和里程碑,再补齐风险与依赖,形成可执行的项目规划。',
|
greeting: '先把目标拆成阶段和里程碑,再补齐风险与依赖,形成可执行的项目规划。',
|
||||||
tagline: '适合启动、排期、推进对齐和项目治理场景。',
|
tagline: '适合启动、排期、推进对齐和项目治理场景。',
|
||||||
openingPrompt: '告诉我项目目标、时间要求和主要约束,我先帮你搭一版项目计划。',
|
openingPrompt: '告诉我项目目标、时间要求和主要约束,我先帮你搭一版项目计划。',
|
||||||
@@ -997,7 +989,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先理清背景和诉求,再起一版正式、清楚、能直接发的邮件。',
|
greeting: '先理清背景和诉求,再起一版正式、清楚、能直接发的邮件。',
|
||||||
tagline: '适合汇报、通知、催办、确认和对外沟通场景。',
|
tagline: '适合汇报、通知、催办、确认和对外沟通场景。',
|
||||||
openingPrompt: '告诉我收件对象、沟通目的和你希望对方做什么,我先帮你写邮件。',
|
openingPrompt: '告诉我收件对象、沟通目的和你希望对方做什么,我先帮你写邮件。',
|
||||||
@@ -1057,7 +1049,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把杂乱字段收成稳定列结构,再输出一版清楚、可继续流转的表格。',
|
greeting: '先把杂乱字段收成稳定列结构,再输出一版清楚、可继续流转的表格。',
|
||||||
tagline: '适合名单、台账、客户记录和半结构化数据整理。',
|
tagline: '适合名单、台账、客户记录和半结构化数据整理。',
|
||||||
openingPrompt: '把原始记录贴给我,或者说明你想整理成哪些列,我先帮你起表。',
|
openingPrompt: '把原始记录贴给我,或者说明你想整理成哪些列,我先帮你起表。',
|
||||||
@@ -1117,7 +1109,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'txt',
|
fileFormat: 'txt',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先压出结论和亮点,再给你一版真正适合汇报和预读的方案摘要。',
|
greeting: '先压出结论和亮点,再给你一版真正适合汇报和预读的方案摘要。',
|
||||||
tagline: '适合管理层预读、售前汇报和长方案快速同步。',
|
tagline: '适合管理层预读、售前汇报和长方案快速同步。',
|
||||||
openingPrompt: '把方案材料贴给我,或者告诉我这次最想突出哪些价值点,我先帮你压摘要。',
|
openingPrompt: '把方案材料贴给我,或者告诉我这次最想突出哪些价值点,我先帮你压摘要。',
|
||||||
@@ -1177,7 +1169,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把进展、风险和下一步收清楚,再输出一版能直接同步的日报或周报。',
|
greeting: '先把进展、风险和下一步收清楚,再输出一版能直接同步的日报或周报。',
|
||||||
tagline: '适合日报、周报、项目同步和阶段性汇报。',
|
tagline: '适合日报、周报、项目同步和阶段性汇报。',
|
||||||
openingPrompt: '把工作记录贴给我,或者说明这是日报还是周报,我先帮你整理。',
|
openingPrompt: '把工作记录贴给我,或者说明这是日报还是周报,我先帮你整理。',
|
||||||
@@ -1237,7 +1229,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先压出合同核心条款和风险,再给你一版适合业务快速预读的摘要。',
|
greeting: '先压出合同核心条款和风险,再给你一版适合业务快速预读的摘要。',
|
||||||
tagline: '适合采购、合作、服务类合同的快速预读和继续审查。',
|
tagline: '适合采购、合作、服务类合同的快速预读和继续审查。',
|
||||||
openingPrompt: '把合同内容贴给我,或者告诉我最担心哪些条款,我先帮你提炼要点。',
|
openingPrompt: '把合同内容贴给我,或者告诉我最担心哪些条款,我先帮你提炼要点。',
|
||||||
@@ -1297,7 +1289,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把访谈内容收成主题和洞察,再输出一版真正能用的整理稿。',
|
greeting: '先把访谈内容收成主题和洞察,再输出一版真正能用的整理稿。',
|
||||||
tagline: '适合用户访谈、需求调研和深度沟通记录整理。',
|
tagline: '适合用户访谈、需求调研和深度沟通记录整理。',
|
||||||
openingPrompt: '把访谈记录贴给我,或者告诉我这次想重点提炼哪些信息,我先帮你整理。',
|
openingPrompt: '把访谈记录贴给我,或者告诉我这次想重点提炼哪些信息,我先帮你整理。',
|
||||||
@@ -1357,7 +1349,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把制度结构和口径收统一,再重写一版更清楚、正式的正文。',
|
greeting: '先把制度结构和口径收统一,再重写一版更清楚、正式的正文。',
|
||||||
tagline: '适合制度、流程、规范类文档的正式化改写。',
|
tagline: '适合制度、流程、规范类文档的正式化改写。',
|
||||||
openingPrompt: '把原制度贴给我,或者告诉我这次要保留哪些原则,我先帮你改写。',
|
openingPrompt: '把原制度贴给我,或者告诉我这次要保留哪些原则,我先帮你改写。',
|
||||||
@@ -1417,7 +1409,7 @@ export const availableSkills = withObjectCodes([
|
|||||||
fileFormat: 'md',
|
fileFormat: 'md',
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
roleCard: {
|
interactionCard: {
|
||||||
greeting: '先把表单结果和反馈收清楚,再输出一版真正适合同步和决策的汇总稿。',
|
greeting: '先把表单结果和反馈收清楚,再输出一版真正适合同步和决策的汇总稿。',
|
||||||
tagline: '适合反馈表、报名表和调查问卷的结果整理。',
|
tagline: '适合反馈表、报名表和调查问卷的结果整理。',
|
||||||
openingPrompt: '把表单结果贴给我,或者告诉我最想看哪些发现,我先帮你汇总。',
|
openingPrompt: '把表单结果贴给我,或者告诉我最想看哪些发现,我先帮你汇总。',
|
||||||
@@ -1431,7 +1423,8 @@ export const availableSkills = withObjectCodes([
|
|||||||
}),
|
}),
|
||||||
], { displayPrefix: '能', logicPrefix: 'K' })
|
], { displayPrefix: '能', logicPrefix: 'K' })
|
||||||
|
|
||||||
export const businessApps = withObjectCodes([
|
// 专员静态目录。
|
||||||
|
export const staticSpecialistCatalog = withObjectCodes([
|
||||||
{
|
{
|
||||||
key: 'contract-review',
|
key: 'contract-review',
|
||||||
label: '合同审查专员',
|
label: '合同审查专员',
|
||||||
@@ -1439,14 +1432,13 @@ export const businessApps = withObjectCodes([
|
|||||||
workerType: 'adw',
|
workerType: 'adw',
|
||||||
marketTag: '已安装',
|
marketTag: '已安装',
|
||||||
objectEntryRoute: WORKBENCH_ROUTE,
|
objectEntryRoute: WORKBENCH_ROUTE,
|
||||||
legacyObjectEntryRoute: '/apps/contract-review',
|
|
||||||
summary: '华东客户主协议修订',
|
summary: '华东客户主协议修订',
|
||||||
status: '2 个待确认',
|
status: '2 个待确认',
|
||||||
risk: '1 个高风险',
|
risk: '1 个高风险',
|
||||||
color: '#f56c6c',
|
color: '#f56c6c',
|
||||||
stage: '人工复核',
|
stage: '人工复核',
|
||||||
progress: 72,
|
progress: 72,
|
||||||
roleCard: createRoleCard({
|
interactionCard: createInteractionCard({
|
||||||
greeting: '我是合同审查专员,已经准备好帮你定位条款风险、整理红线建议,并输出可交付的审查结果。',
|
greeting: '我是合同审查专员,已经准备好帮你定位条款风险、整理红线建议,并输出可交付的审查结果。',
|
||||||
tagline: '把合同风险、红线和交付意见整理清楚。',
|
tagline: '把合同风险、红线和交付意见整理清楚。',
|
||||||
openingPrompt: '把合同发给我,或者直接告诉我这次要重点盯哪些条款。',
|
openingPrompt: '把合同发给我,或者直接告诉我这次要重点盯哪些条款。',
|
||||||
@@ -1465,14 +1457,13 @@ export const businessApps = withObjectCodes([
|
|||||||
workerType: 'adw',
|
workerType: 'adw',
|
||||||
marketTag: '可升级',
|
marketTag: '可升级',
|
||||||
objectEntryRoute: WORKBENCH_ROUTE,
|
objectEntryRoute: WORKBENCH_ROUTE,
|
||||||
legacyObjectEntryRoute: '/apps/solution-proposal',
|
|
||||||
summary: 'A 客户智能培训升级方案',
|
summary: 'A 客户智能培训升级方案',
|
||||||
status: '3 个待办',
|
status: '3 个待办',
|
||||||
risk: '1 个依赖阻塞',
|
risk: '1 个依赖阻塞',
|
||||||
color: '#409eff',
|
color: '#409eff',
|
||||||
stage: '方案生成',
|
stage: '方案生成',
|
||||||
progress: 58,
|
progress: 58,
|
||||||
roleCard: createRoleCard({
|
interactionCard: createInteractionCard({
|
||||||
greeting: '我是售前方案专员,可以帮你把客户背景、需求和方案结构快速组织起来。',
|
greeting: '我是售前方案专员,可以帮你把客户背景、需求和方案结构快速组织起来。',
|
||||||
tagline: '把客户需求转成一版可讲、可交付、可评审的方案。',
|
tagline: '把客户需求转成一版可讲、可交付、可评审的方案。',
|
||||||
openingPrompt: '告诉我客户背景、目标和限制条件,我先帮你起方案骨架。',
|
openingPrompt: '告诉我客户背景、目标和限制条件,我先帮你起方案骨架。',
|
||||||
@@ -1491,14 +1482,13 @@ export const businessApps = withObjectCodes([
|
|||||||
workerType: 'dw',
|
workerType: 'dw',
|
||||||
marketTag: '已安装',
|
marketTag: '已安装',
|
||||||
objectEntryRoute: WORKBENCH_ROUTE,
|
objectEntryRoute: WORKBENCH_ROUTE,
|
||||||
legacyObjectEntryRoute: '/apps/training-delivery',
|
|
||||||
summary: '新人销售训练营第 3 周',
|
summary: '新人销售训练营第 3 周',
|
||||||
status: '5 个进行中',
|
status: '5 个进行中',
|
||||||
risk: '0 个异常',
|
risk: '0 个异常',
|
||||||
color: '#67c23a',
|
color: '#67c23a',
|
||||||
stage: '交付归档',
|
stage: '交付归档',
|
||||||
progress: 84,
|
progress: 84,
|
||||||
roleCard: createRoleCard({
|
interactionCard: createInteractionCard({
|
||||||
greeting: '我是培训交付专员,可以帮你推进训练计划、交付节点和归档结果。',
|
greeting: '我是培训交付专员,可以帮你推进训练计划、交付节点和归档结果。',
|
||||||
tagline: '把培训交付从排期、推进到归档串成闭环。',
|
tagline: '把培训交付从排期、推进到归档串成闭环。',
|
||||||
openingPrompt: '把交付背景、学员范围和目标发给我,我来帮你推进。',
|
openingPrompt: '把交付背景、学员范围和目标发给我,我来帮你推进。',
|
||||||
@@ -1517,14 +1507,13 @@ export const businessApps = withObjectCodes([
|
|||||||
workerType: 'dw',
|
workerType: 'dw',
|
||||||
marketTag: '已安装',
|
marketTag: '已安装',
|
||||||
objectEntryRoute: WORKBENCH_ROUTE,
|
objectEntryRoute: WORKBENCH_ROUTE,
|
||||||
legacyObjectEntryRoute: '/apps/knowledge-operations',
|
|
||||||
summary: '对象字典、证据索引与规则同步',
|
summary: '对象字典、证据索引与规则同步',
|
||||||
status: '4 个待整理',
|
status: '4 个待整理',
|
||||||
risk: '1 个映射待确认',
|
risk: '1 个映射待确认',
|
||||||
color: '#8e6cf2',
|
color: '#8e6cf2',
|
||||||
stage: '证据整理',
|
stage: '证据整理',
|
||||||
progress: 63,
|
progress: 63,
|
||||||
roleCard: createRoleCard({
|
interactionCard: createInteractionCard({
|
||||||
greeting: '我是知识运营专员,可以帮你整理对象字典、证据索引和规则映射。',
|
greeting: '我是知识运营专员,可以帮你整理对象字典、证据索引和规则映射。',
|
||||||
tagline: '把散的知识资产收成可查、可用、可治理的结构。',
|
tagline: '把散的知识资产收成可查、可用、可治理的结构。',
|
||||||
openingPrompt: '告诉我这次要整理哪类知识资产,我先帮你搭索引。',
|
openingPrompt: '告诉我这次要整理哪类知识资产,我先帮你搭索引。',
|
||||||
@@ -1543,14 +1532,13 @@ export const businessApps = withObjectCodes([
|
|||||||
workerType: 'adw',
|
workerType: 'adw',
|
||||||
marketTag: '可升级',
|
marketTag: '可升级',
|
||||||
objectEntryRoute: WORKBENCH_ROUTE,
|
objectEntryRoute: WORKBENCH_ROUTE,
|
||||||
legacyObjectEntryRoute: '/apps/process-coordination',
|
|
||||||
summary: '跨部门催办、checkpoint 跟进与阻塞升级',
|
summary: '跨部门催办、checkpoint 跟进与阻塞升级',
|
||||||
status: '6 个待跟进',
|
status: '6 个待跟进',
|
||||||
risk: '2 个阻塞',
|
risk: '2 个阻塞',
|
||||||
color: '#14b8a6',
|
color: '#14b8a6',
|
||||||
stage: '阻塞清理',
|
stage: '阻塞清理',
|
||||||
progress: 57,
|
progress: 57,
|
||||||
roleCard: createRoleCard({
|
interactionCard: createInteractionCard({
|
||||||
greeting: '我是流程推进专员,可以帮你梳理 checkpoint、跟进阻塞并推动事项落地。',
|
greeting: '我是流程推进专员,可以帮你梳理 checkpoint、跟进阻塞并推动事项落地。',
|
||||||
tagline: '把多方协作里的待办、阻塞和升级路径理清楚。',
|
tagline: '把多方协作里的待办、阻塞和升级路径理清楚。',
|
||||||
openingPrompt: '把当前事项、参与方和卡点告诉我,我先帮你排推进节奏。',
|
openingPrompt: '把当前事项、参与方和卡点告诉我,我先帮你排推进节奏。',
|
||||||
@@ -1569,14 +1557,13 @@ export const businessApps = withObjectCodes([
|
|||||||
workerType: 'dw',
|
workerType: 'dw',
|
||||||
marketTag: '可升级',
|
marketTag: '可升级',
|
||||||
objectEntryRoute: WORKBENCH_ROUTE,
|
objectEntryRoute: WORKBENCH_ROUTE,
|
||||||
legacyObjectEntryRoute: '/apps/report-generation',
|
|
||||||
summary: '日报、周报、复盘和交付说明自动生成',
|
summary: '日报、周报、复盘和交付说明自动生成',
|
||||||
status: '3 个待输出',
|
status: '3 个待输出',
|
||||||
risk: '0 个异常',
|
risk: '0 个异常',
|
||||||
color: '#ec4899',
|
color: '#ec4899',
|
||||||
stage: '报告汇总',
|
stage: '报告汇总',
|
||||||
progress: 76,
|
progress: 76,
|
||||||
roleCard: createRoleCard({
|
interactionCard: createInteractionCard({
|
||||||
greeting: '我是报告生成专员,可以把过程材料整理成日报、周报、复盘和交付说明。',
|
greeting: '我是报告生成专员,可以把过程材料整理成日报、周报、复盘和交付说明。',
|
||||||
tagline: '把分散信息压成结构完整、可直接提交的报告。',
|
tagline: '把分散信息压成结构完整、可直接提交的报告。',
|
||||||
openingPrompt: '把素材贴给我,或者告诉我报告对象、周期和格式要求。',
|
openingPrompt: '把素材贴给我,或者告诉我报告对象、周期和格式要求。',
|
||||||
@@ -1595,14 +1582,13 @@ export const businessApps = withObjectCodes([
|
|||||||
workerType: 'dw',
|
workerType: 'dw',
|
||||||
marketTag: '已安装',
|
marketTag: '已安装',
|
||||||
objectEntryRoute: WORKBENCH_ROUTE,
|
objectEntryRoute: WORKBENCH_ROUTE,
|
||||||
legacyObjectEntryRoute: '/apps/wechat-official-account',
|
|
||||||
summary: '热点选题、标题、提纲、正文四步创作工作流',
|
summary: '热点选题、标题、提纲、正文四步创作工作流',
|
||||||
status: '4 个节点可执行',
|
status: '4 个节点可执行',
|
||||||
risk: '0 个异常',
|
risk: '0 个异常',
|
||||||
color: '#2563eb',
|
color: '#2563eb',
|
||||||
stage: '正文创作',
|
stage: '正文创作',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
roleCard: createRoleCard({
|
interactionCard: createInteractionCard({
|
||||||
greeting: '我是公众号助手,可以帮你把选题、提纲、正文和发布准备串起来。',
|
greeting: '我是公众号助手,可以帮你把选题、提纲、正文和发布准备串起来。',
|
||||||
tagline: '把内容创作流程从想法推进到可发布。',
|
tagline: '把内容创作流程从想法推进到可发布。',
|
||||||
openingPrompt: '告诉我主题、受众和语气,我先帮你起草内容结构。',
|
openingPrompt: '告诉我主题、受众和语气,我先帮你起草内容结构。',
|
||||||
@@ -1621,14 +1607,13 @@ export const businessApps = withObjectCodes([
|
|||||||
workerType: 'adw',
|
workerType: 'adw',
|
||||||
marketTag: '试用',
|
marketTag: '试用',
|
||||||
objectEntryRoute: WORKBENCH_ROUTE,
|
objectEntryRoute: WORKBENCH_ROUTE,
|
||||||
legacyObjectEntryRoute: '/apps/logistics-fulfillment',
|
|
||||||
summary: '美西航线本周履约看板',
|
summary: '美西航线本周履约看板',
|
||||||
status: '1 个异常',
|
status: '1 个异常',
|
||||||
risk: '2 个节点延迟',
|
risk: '2 个节点延迟',
|
||||||
color: '#e6a23c',
|
color: '#e6a23c',
|
||||||
stage: '异常处置',
|
stage: '异常处置',
|
||||||
progress: 49,
|
progress: 49,
|
||||||
roleCard: createRoleCard({
|
interactionCard: createInteractionCard({
|
||||||
greeting: '我是履约跟单专员,可以帮你跟进节点、识别异常并整理处置建议。',
|
greeting: '我是履约跟单专员,可以帮你跟进节点、识别异常并整理处置建议。',
|
||||||
tagline: '把履约过程里的异常、催办和交付节奏收在一条线上。',
|
tagline: '把履约过程里的异常、催办和交付节奏收在一条线上。',
|
||||||
openingPrompt: '把当前订单、节点和异常情况发给我,我先帮你排查。',
|
openingPrompt: '把当前订单、节点和异常情况发给我,我先帮你排查。',
|
||||||
@@ -1647,14 +1632,13 @@ export const businessApps = withObjectCodes([
|
|||||||
workerType: 'dw',
|
workerType: 'dw',
|
||||||
marketTag: '试用',
|
marketTag: '试用',
|
||||||
objectEntryRoute: WORKBENCH_ROUTE,
|
objectEntryRoute: WORKBENCH_ROUTE,
|
||||||
legacyObjectEntryRoute: '/apps/hr-email-sorter',
|
|
||||||
summary: '识别并整理招聘邮箱中的简历邮件',
|
summary: '识别并整理招聘邮箱中的简历邮件',
|
||||||
status: '3 封待处理',
|
status: '3 封待处理',
|
||||||
risk: '0 个异常',
|
risk: '0 个异常',
|
||||||
color: '#409eff',
|
color: '#409eff',
|
||||||
stage: '邮件处理',
|
stage: '邮件处理',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
roleCard: createRoleCard({
|
interactionCard: createInteractionCard({
|
||||||
greeting: '我是 HR 邮件整理专员,可以帮你识别、分类并整理招聘邮箱里的邮件。',
|
greeting: '我是 HR 邮件整理专员,可以帮你识别、分类并整理招聘邮箱里的邮件。',
|
||||||
tagline: '把招聘邮箱里的内容收成可继续处理的人才线索。',
|
tagline: '把招聘邮箱里的内容收成可继续处理的人才线索。',
|
||||||
openingPrompt: '把邮件内容或整理目标告诉我,我先帮你分组归档。',
|
openingPrompt: '把邮件内容或整理目标告诉我,我先帮你分组归档。',
|
||||||
@@ -1673,14 +1657,13 @@ export const businessApps = withObjectCodes([
|
|||||||
workerType: 'dw',
|
workerType: 'dw',
|
||||||
marketTag: '试用',
|
marketTag: '试用',
|
||||||
objectEntryRoute: WORKBENCH_ROUTE,
|
objectEntryRoute: WORKBENCH_ROUTE,
|
||||||
legacyObjectEntryRoute: '/apps/resume-processor',
|
|
||||||
summary: '筛选候选人并安排面试',
|
summary: '筛选候选人并安排面试',
|
||||||
status: '5 份待筛选',
|
status: '5 份待筛选',
|
||||||
risk: '1 个匹配待确认',
|
risk: '1 个匹配待确认',
|
||||||
color: '#67c23a',
|
color: '#67c23a',
|
||||||
stage: '简历筛选',
|
stage: '简历筛选',
|
||||||
progress: 0,
|
progress: 0,
|
||||||
roleCard: createRoleCard({
|
interactionCard: createInteractionCard({
|
||||||
greeting: '我是简历处理专员,可以帮你筛选候选人、整理匹配理由并安排后续动作。',
|
greeting: '我是简历处理专员,可以帮你筛选候选人、整理匹配理由并安排后续动作。',
|
||||||
tagline: '把简历筛选从海量阅读变成结构化判断。',
|
tagline: '把简历筛选从海量阅读变成结构化判断。',
|
||||||
openingPrompt: '告诉我岗位要求和筛选标准,我先帮你做一轮初筛。',
|
openingPrompt: '告诉我岗位要求和筛选标准,我先帮你做一轮初筛。',
|
||||||
@@ -1692,6 +1675,31 @@ export const businessApps = withObjectCodes([
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'presentation-briefing',
|
||||||
|
label: '汇报专员',
|
||||||
|
tier: 'generic',
|
||||||
|
workerType: 'dw',
|
||||||
|
marketTag: '已安装',
|
||||||
|
objectEntryRoute: WORKBENCH_ROUTE,
|
||||||
|
summary: '经营汇报、方案汇报与培训课件统一出稿',
|
||||||
|
status: '2 个待成稿',
|
||||||
|
risk: '1 个页数待压缩',
|
||||||
|
color: '#4f46e5',
|
||||||
|
stage: '演示编排',
|
||||||
|
progress: 68,
|
||||||
|
interactionCard: createInteractionCard({
|
||||||
|
greeting: '我是汇报专员,可以先判断汇报对象和页数,再把材料压成能讲、能交、能继续细化的演示骨架。',
|
||||||
|
tagline: '把散材料收成一版结构清楚、讲得出去的汇报 PPT。',
|
||||||
|
openingPrompt: '告诉我汇报对象、场景、目标和页数,我先帮你规划页面结构。',
|
||||||
|
relationshipToUser: '你的汇报交付协作搭档',
|
||||||
|
starterPrompts: [
|
||||||
|
'帮我做一版 8 页管理层汇报 PPT',
|
||||||
|
'把这份方案压成可讲的客户汇报结构',
|
||||||
|
'根据这批材料生成培训课件骨架和讲稿备注',
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
},
|
||||||
], { displayPrefix: '专', logicPrefix: 'S' })
|
], { displayPrefix: '专', logicPrefix: 'S' })
|
||||||
|
|
||||||
const appWorkspaceData = {
|
const appWorkspaceData = {
|
||||||
@@ -2202,41 +2210,37 @@ export function getWorkbenchSurface(path) {
|
|||||||
return 'apps'
|
return 'apps'
|
||||||
}
|
}
|
||||||
|
|
||||||
export function findBusinessAppByPath(path) {
|
export function getSpecialistsByTier(tier) {
|
||||||
return businessApps.find((app) => app.legacyObjectEntryRoute === path) || null
|
return staticSpecialistCatalog.filter((specialist) => specialist.tier === tier)
|
||||||
}
|
|
||||||
|
|
||||||
export function getBusinessAppsByTier(tier) {
|
|
||||||
return businessApps.filter((app) => app.tier === tier)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getInstalledSpecialistCount() {
|
export function getInstalledSpecialistCount() {
|
||||||
return businessApps.length
|
return staticSpecialistCatalog.length
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getUpgradeableSpecialistCount() {
|
export function getUpgradeableSpecialistCount() {
|
||||||
return businessApps.filter((app) => app.marketTag === '可升级').length
|
return staticSpecialistCatalog.filter((specialist) => specialist.marketTag === '可升级').length
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSpecialistStats() {
|
export function getSpecialistStats() {
|
||||||
return {
|
return {
|
||||||
total: getInstalledSpecialistCount(),
|
total: getInstalledSpecialistCount(),
|
||||||
generic: getBusinessAppsByTier('generic').length,
|
generic: getSpecialistsByTier('generic').length,
|
||||||
industry: getBusinessAppsByTier('industry').length,
|
industry: getSpecialistsByTier('industry').length,
|
||||||
dw: businessApps.filter((app) => app.workerType === 'dw').length,
|
dw: staticSpecialistCatalog.filter((specialist) => specialist.workerType === 'dw').length,
|
||||||
adw: businessApps.filter((app) => app.workerType === 'adw').length,
|
adw: staticSpecialistCatalog.filter((specialist) => specialist.workerType === 'adw').length,
|
||||||
upgradeable: getUpgradeableSpecialistCount(),
|
upgradeable: getUpgradeableSpecialistCount(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMarketItemsByTier(tier) {
|
export function getMarketItemsByTier(tier) {
|
||||||
return getBusinessAppsByTier(tier).map((app) => ({
|
return getSpecialistsByTier(tier).map((specialist) => ({
|
||||||
key: app.key,
|
key: specialist.key,
|
||||||
title: app.label,
|
title: specialist.label,
|
||||||
tag: app.marketTag || '已安装',
|
tag: specialist.marketTag || '已安装',
|
||||||
desc: app.summary,
|
desc: specialist.summary,
|
||||||
meta: `当前阶段 ${app.stage} · ${app.status} · ${app.risk}`,
|
meta: `当前阶段 ${specialist.stage} · ${specialist.status} · ${specialist.risk}`,
|
||||||
objectEntryRoute: app.objectEntryRoute,
|
objectEntryRoute: specialist.objectEntryRoute,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2253,39 +2257,14 @@ export function getCaseById(appKey, caseId) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkspaceSnapshot(path, caseId = '') {
|
export function getWorkspaceSnapshot(path, caseId = '') {
|
||||||
const app = findBusinessAppByPath(path)
|
void caseId
|
||||||
if (app) {
|
|
||||||
const baseContext = appWorkspaceData[app.key]?.context || null
|
|
||||||
if (!baseContext) return null
|
|
||||||
const selectedCase = getCaseById(app.key, caseId)
|
|
||||||
const patch = selectedCase?.contextPatch
|
|
||||||
if (!patch) return baseContext
|
|
||||||
return {
|
|
||||||
...baseContext,
|
|
||||||
...patch,
|
|
||||||
bottomPanel: {
|
|
||||||
...baseContext.bottomPanel,
|
|
||||||
...patch.bottomPanel,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (path.startsWith('/knowledge/')) return genericSurfaceContext['/knowledge-management']
|
if (path.startsWith('/knowledge/')) return genericSurfaceContext['/knowledge-management']
|
||||||
if (path.startsWith('/system') || path === '/console') return genericSurfaceContext['/console']
|
if (path.startsWith('/system') || path === '/console') return genericSurfaceContext['/console']
|
||||||
return genericSurfaceContext[path] || genericSurfaceContext['/knowledge-hub']
|
return genericSurfaceContext[path] || genericSurfaceContext['/knowledge-hub']
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getWorkspaceDescriptor(path, caseId = '') {
|
export function getWorkspaceDescriptor(path, caseId = '') {
|
||||||
const app = findBusinessAppByPath(path)
|
void path
|
||||||
if (app) {
|
void caseId
|
||||||
const base = appWorkspaceData[app.key]
|
|
||||||
const selectedCase = getCaseById(app.key, caseId) || base?.cases?.[0] || null
|
|
||||||
if (!base) return null
|
|
||||||
return {
|
|
||||||
...base,
|
|
||||||
currentCase: selectedCase,
|
|
||||||
caseTitle: selectedCase?.title || base.caseTitle,
|
|
||||||
subtitle: selectedCase?.summary ? `${base.subtitle} 当前事项:${selectedCase.summary}` : base.subtitle,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,9 +353,11 @@ import SectionTabs from '@/components/SectionTabs.vue'
|
|||||||
import NavSearch from '@/components/NavSearch.vue'
|
import NavSearch from '@/components/NavSearch.vue'
|
||||||
import { useAuthStore } from '@/store/auth'
|
import { useAuthStore } from '@/store/auth'
|
||||||
import { useProjectStore } from '@/store/projectStore'
|
import { useProjectStore } from '@/store/projectStore'
|
||||||
|
import { useSkillCatalogStore } from '@/store/skillCatalog'
|
||||||
|
import { useSpecialistCatalogStore } from '@/store/specialistCatalog'
|
||||||
import { useWorkbenchStore } from '@/store/workbench'
|
import { useWorkbenchStore } from '@/store/workbench'
|
||||||
import { useWorkerRuntimeStore } from '@/store/workerRuntime'
|
import { useWorkerRuntimeStore } from '@/store/workerRuntime'
|
||||||
import { availableSkills, businessApps, getAppCases, getDefaultCaseId } from '@/config/workbench'
|
import { getAppCases, getDefaultCaseId } from '@/config/workbench'
|
||||||
import { relativeTime } from '@/config/projectTemplates'
|
import { relativeTime } from '@/config/projectTemplates'
|
||||||
import {
|
import {
|
||||||
findGroupByPath,
|
findGroupByPath,
|
||||||
@@ -368,6 +370,8 @@ const route = useRoute()
|
|||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const projectStore = useProjectStore()
|
const projectStore = useProjectStore()
|
||||||
|
const skillCatalog = useSkillCatalogStore()
|
||||||
|
const specialistCatalog = useSpecialistCatalogStore()
|
||||||
const workbench = useWorkbenchStore()
|
const workbench = useWorkbenchStore()
|
||||||
const workerRuntime = useWorkerRuntimeStore()
|
const workerRuntime = useWorkerRuntimeStore()
|
||||||
|
|
||||||
@@ -477,6 +481,8 @@ const selectedCaseId = computed(() => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
skillCatalog.hydrate()
|
||||||
|
specialistCatalog.hydrate()
|
||||||
loadMyTasks()
|
loadMyTasks()
|
||||||
projectStore.loadProjects().catch((error) => {
|
projectStore.loadProjects().catch((error) => {
|
||||||
console.warn('load projects for shell failed', error)
|
console.warn('load projects for shell failed', error)
|
||||||
@@ -539,9 +545,9 @@ watch(
|
|||||||
(path) => {
|
(path) => {
|
||||||
// 顶部胶囊是一级粒度的,所以这里只做两件事:记住你落在哪个专员的工作面
|
// 顶部胶囊是一级粒度的,所以这里只做两件事:记住你落在哪个专员的工作面
|
||||||
// (右侧工作台要用),以及把路由喂给 store —— 折算成哪个胶囊由 store 一处决定。
|
// (右侧工作台要用),以及把路由喂给 store —— 折算成哪个胶囊由 store 一处决定。
|
||||||
const app = businessApps.find((item) => item.legacyObjectEntryRoute === path || item.objectEntryRoute === path)
|
const specialist = specialistCatalog.getByPath(path)
|
||||||
if (app?.key && !workbench.selectedCases[app.key]) {
|
if (specialist?.key && !workbench.selectedCases[specialist.key]) {
|
||||||
workbench.setSelectedCase(app.key, getDefaultCaseId(app.key))
|
workbench.setSelectedCase(specialist.key, getDefaultCaseId(specialist.key))
|
||||||
}
|
}
|
||||||
workbench.ensureTab({ pageRoute: path })
|
workbench.ensureTab({ pageRoute: path })
|
||||||
},
|
},
|
||||||
@@ -602,7 +608,7 @@ async function loadMyTasks() {
|
|||||||
// 和「用哪个技能做」,不是完整消息历史。
|
// 和「用哪个技能做」,不是完整消息历史。
|
||||||
async function openMyTask(task) {
|
async function openMyTask(task) {
|
||||||
workerRuntime.openTask(task.id, task.specialist_key)
|
workerRuntime.openTask(task.id, task.specialist_key)
|
||||||
const currentSkill = availableSkills.find((item) => item.key === workerRuntime.skillKeyOfTask(task.id))
|
const currentSkill = skillCatalog.getByKey(workerRuntime.skillKeyOfTask(task.id))
|
||||||
const target = currentSkill?.objectEntryRoute || '/home'
|
const target = currentSkill?.objectEntryRoute || '/home'
|
||||||
if (route.path !== target) await router.push(target)
|
if (route.path !== target) await router.push(target)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,159 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { listAppDefinitions } from '@/api/appDefinition'
|
||||||
|
import { useAppCenterStore } from '@/store/appCenter'
|
||||||
|
|
||||||
|
function safeParseStringArray(value) {
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return value.map((item) => String(item || '').trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) return []
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
return Array.isArray(parsed)
|
||||||
|
? parsed.map((item) => String(item || '').trim()).filter(Boolean)
|
||||||
|
: []
|
||||||
|
} catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeRemoteApp(item) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
key: String(item?.key || '').trim(),
|
||||||
|
label: String(item?.label || '').trim(),
|
||||||
|
display_code: String(item?.display_code || '').trim(),
|
||||||
|
eailogic_code: String(item?.eailogic_code || '').trim(),
|
||||||
|
badge: String(item?.badge || '').trim(),
|
||||||
|
kind: String(item?.kind || '').trim(),
|
||||||
|
marketTag: String(item?.market_tag || item?.marketTag || '').trim(),
|
||||||
|
workerType: String(item?.worker_type || item?.workerType || '').trim(),
|
||||||
|
tier: String(item?.tier || '').trim(),
|
||||||
|
source: String(item?.source || '').trim(),
|
||||||
|
color: String(item?.color || '').trim() || '#409eff',
|
||||||
|
iconText: String(item?.icon_text || item?.iconText || '').trim() || '应用',
|
||||||
|
coverTone: String(item?.cover_tone || item?.coverTone || '').trim() || String(item?.color || '').trim() || '#409eff',
|
||||||
|
summary: String(item?.summary || '').trim(),
|
||||||
|
description: String(item?.description || item?.summary || '').trim(),
|
||||||
|
openRoute: String(item?.open_route || item?.openRoute || '').trim(),
|
||||||
|
specialistKey: String(item?.specialist_key || item?.specialistKey || '').trim(),
|
||||||
|
skillKey: String(item?.skill_key || item?.skillKey || '').trim(),
|
||||||
|
defaultPrompt: String(item?.default_prompt || item?.defaultPrompt || '').trim(),
|
||||||
|
prompts: safeParseStringArray(item?.prompts_json || item?.prompts),
|
||||||
|
tags: safeParseStringArray(item?.tags_json || item?.tags),
|
||||||
|
installState: String(item?.install_state || item?.installState || 'installed').trim(),
|
||||||
|
exposedToUser: item?.exposed_to_user !== false,
|
||||||
|
isCustomApp: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatAppCode(index) {
|
||||||
|
return String(index).padStart(2, '0')
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCustomApp(item, index = 0, remoteCount = 0) {
|
||||||
|
const suffix = formatAppCode(remoteCount + index + 1)
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
key: String(item?.key || '').trim(),
|
||||||
|
label: String(item?.label || '').trim(),
|
||||||
|
display_code: String(item?.display_code || '').trim() || `应${suffix}`,
|
||||||
|
eailogic_code: String(item?.eailogic_code || '').trim() || `EAI-A-${suffix}`,
|
||||||
|
badge: String(item?.badge || '自定义应用').trim(),
|
||||||
|
marketTag: String(item?.marketTag || item?.badge || '自定义应用').trim(),
|
||||||
|
kind: String(item?.kind || '个人工作流').trim(),
|
||||||
|
workerType: String(item?.workerType || 'worker').trim(),
|
||||||
|
tier: String(item?.tier || 'business').trim(),
|
||||||
|
color: String(item?.color || '').trim() || '#409eff',
|
||||||
|
summary: String(item?.summary || '').trim(),
|
||||||
|
description: String(item?.description || item?.summary || '').trim(),
|
||||||
|
tags: Array.isArray(item?.tags) ? item.tags.map((tag) => String(tag || '').trim()).filter(Boolean) : [],
|
||||||
|
prompts: Array.isArray(item?.prompts) ? item.prompts.map((prompt) => String(prompt || '').trim()).filter(Boolean) : [],
|
||||||
|
installState: String(item?.installState || 'installed').trim(),
|
||||||
|
iconText: String(item?.iconText || '').trim() || '应用',
|
||||||
|
coverTone: String(item?.coverTone || item?.color || '').trim() || '#409eff',
|
||||||
|
openRoute: String(item?.openRoute || '').trim(),
|
||||||
|
skillKey: String(item?.skillKey || '').trim(),
|
||||||
|
specialistKey: String(item?.specialistKey || '').trim(),
|
||||||
|
defaultPrompt: String(item?.defaultPrompt || item?.prompts?.[0] || '').trim(),
|
||||||
|
isCustomApp: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useAppCatalogStore = defineStore('appCatalog', () => {
|
||||||
|
const appCenter = useAppCenterStore()
|
||||||
|
const loaded = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const remoteApps = ref([])
|
||||||
|
|
||||||
|
const apps = computed(() => [
|
||||||
|
...remoteApps.value,
|
||||||
|
...appCenter.customApps.map((item, index) => normalizeCustomApp(item, index, remoteApps.value.length)),
|
||||||
|
])
|
||||||
|
|
||||||
|
const appMap = computed(() =>
|
||||||
|
Object.fromEntries(apps.value.map((item) => [item.key, item]))
|
||||||
|
)
|
||||||
|
|
||||||
|
async function hydrate(force = false) {
|
||||||
|
if (loading.value) return
|
||||||
|
if (loaded.value && !force) {
|
||||||
|
await appCenter.hydrate()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [appsResult, centerResult] = await Promise.allSettled([
|
||||||
|
listAppDefinitions(),
|
||||||
|
appCenter.hydrate(),
|
||||||
|
])
|
||||||
|
if (appsResult.status === 'fulfilled') {
|
||||||
|
remoteApps.value = Array.isArray(appsResult.value?.data)
|
||||||
|
? appsResult.value.data.map((item) => normalizeRemoteApp(item))
|
||||||
|
: []
|
||||||
|
} else {
|
||||||
|
console.warn('load app definitions failed', appsResult.reason)
|
||||||
|
}
|
||||||
|
if (centerResult.status === 'rejected') {
|
||||||
|
console.warn('hydrate app center in app catalog failed', centerResult.reason)
|
||||||
|
}
|
||||||
|
loaded.value = true
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('load app catalog failed', error)
|
||||||
|
await appCenter.hydrate()
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getByKey(key) {
|
||||||
|
return appMap.value[String(key || '').trim()] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveOpenRoute(app) {
|
||||||
|
if (!app) return '/catalog/apps'
|
||||||
|
if (app.openRoute) return app.openRoute
|
||||||
|
if (app.specialistKey || app.skillKey || app.defaultPrompt) {
|
||||||
|
const params = new URLSearchParams()
|
||||||
|
if (app.specialistKey) params.set('app_specialist', app.specialistKey)
|
||||||
|
if (app.skillKey) params.set('app_skill', app.skillKey)
|
||||||
|
if (app.defaultPrompt) params.set('app_prompt', app.defaultPrompt)
|
||||||
|
const query = params.toString()
|
||||||
|
return query ? `/home?${query}` : '/home'
|
||||||
|
}
|
||||||
|
return '/catalog/apps'
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loaded,
|
||||||
|
loading,
|
||||||
|
remoteApps,
|
||||||
|
apps,
|
||||||
|
appMap,
|
||||||
|
hydrate,
|
||||||
|
getByKey,
|
||||||
|
resolveOpenRoute,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { getMyAppCenter, updateMyAppCenter } from '@/api/appCenter'
|
import { getMyAppCenter, updateMyAppCenter } from '@/api/appCenter'
|
||||||
import { STATIC_APP_CODE_COUNT } from '@/config/productizedApps'
|
|
||||||
|
|
||||||
const STORAGE_KEY = 'eai-app-center'
|
const STORAGE_KEY = 'eai-app-center'
|
||||||
const MAX_RECENT_COUNT = 8
|
const MAX_RECENT_COUNT = 8
|
||||||
@@ -19,10 +18,6 @@ function pickColor(index) {
|
|||||||
return CUSTOM_APP_COLORS[index % CUSTOM_APP_COLORS.length]
|
return CUSTOM_APP_COLORS[index % CUSTOM_APP_COLORS.length]
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatAppCode(index) {
|
|
||||||
return String(index).padStart(2, '0')
|
|
||||||
}
|
|
||||||
|
|
||||||
function truncateText(value, limit) {
|
function truncateText(value, limit) {
|
||||||
const text = String(value || '').trim()
|
const text = String(value || '').trim()
|
||||||
if (text.length <= limit) return text
|
if (text.length <= limit) return text
|
||||||
@@ -50,15 +45,14 @@ function normalizeCustomApp(item, index = 0) {
|
|||||||
const prompt = truncateText(item?.prompts?.[0] || item?.defaultPrompt || '', 240)
|
const prompt = truncateText(item?.prompts?.[0] || item?.defaultPrompt || '', 240)
|
||||||
const tags = Array.isArray(item?.tags) ? item.tags : []
|
const tags = Array.isArray(item?.tags) ? item.tags : []
|
||||||
const color = String(item?.color || '').trim() || pickColor(index)
|
const color = String(item?.color || '').trim() || pickColor(index)
|
||||||
const sequence = STATIC_APP_CODE_COUNT + index + 1
|
|
||||||
const suffix = formatAppCode(sequence)
|
|
||||||
return {
|
return {
|
||||||
key: String(item?.key || '').trim() || makeCustomKey(label, index),
|
key: String(item?.key || '').trim() || makeCustomKey(label, index),
|
||||||
label,
|
label,
|
||||||
display_code: String(item?.display_code || '').trim() || `应${suffix}`,
|
|
||||||
eailogic_code: String(item?.eailogic_code || '').trim() || `EAI-A-${suffix}`,
|
|
||||||
badge: '自定义应用',
|
badge: '自定义应用',
|
||||||
kind: String(item?.kind || '个人工作流').trim() || '个人工作流',
|
kind: String(item?.kind || '个人工作流').trim() || '个人工作流',
|
||||||
|
marketTag: '自定义应用',
|
||||||
|
workerType: String(item?.workerType || 'worker').trim() || 'worker',
|
||||||
|
tier: String(item?.tier || 'business').trim() || 'business',
|
||||||
color,
|
color,
|
||||||
summary,
|
summary,
|
||||||
description: truncateText(item?.description || '', 180) || summary,
|
description: truncateText(item?.description || '', 180) || summary,
|
||||||
@@ -69,6 +63,7 @@ function normalizeCustomApp(item, index = 0) {
|
|||||||
createdAt: String(item?.createdAt || new Date().toISOString()),
|
createdAt: String(item?.createdAt || new Date().toISOString()),
|
||||||
iconText: normalizeIconText(item?.iconText, label),
|
iconText: normalizeIconText(item?.iconText, label),
|
||||||
coverTone: String(item?.coverTone || '').trim() || color,
|
coverTone: String(item?.coverTone || '').trim() || color,
|
||||||
|
defaultPrompt: prompt || summary,
|
||||||
isCustomApp: true,
|
isCustomApp: true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { listSkills } from '@/api/skill'
|
||||||
|
import { staticSkillCatalog as staticSkills } from '@/config/workbench'
|
||||||
|
|
||||||
|
function safeParse(value, fallback) {
|
||||||
|
if (value == null || value === '') return fallback
|
||||||
|
if (typeof value === 'object') return value
|
||||||
|
try {
|
||||||
|
return JSON.parse(String(value))
|
||||||
|
} catch {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildFallbackInteractionCard(remote, fallback) {
|
||||||
|
const starterPrompts = Array.isArray(remote.starterPrompts) && remote.starterPrompts.length
|
||||||
|
? remote.starterPrompts
|
||||||
|
: (fallback?.interactionCard?.starterPrompts || [])
|
||||||
|
return {
|
||||||
|
greeting:
|
||||||
|
fallback?.interactionCard?.greeting ||
|
||||||
|
`我是${remote.label},可以直接帮你推进这类任务。`,
|
||||||
|
tagline:
|
||||||
|
fallback?.interactionCard?.tagline ||
|
||||||
|
remote.caption ||
|
||||||
|
remote.label,
|
||||||
|
openingPrompt:
|
||||||
|
fallback?.interactionCard?.openingPrompt ||
|
||||||
|
remote.promptTemplate ||
|
||||||
|
`告诉我你希望${remote.label}先处理什么。`,
|
||||||
|
relationshipToUser:
|
||||||
|
fallback?.interactionCard?.relationshipToUser ||
|
||||||
|
'你的技能助手',
|
||||||
|
starterPrompts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeArtifactSchema(raw, fallback) {
|
||||||
|
if (Array.isArray(fallback?.artifactSchema) && fallback.artifactSchema.length) {
|
||||||
|
return fallback.artifactSchema
|
||||||
|
}
|
||||||
|
const parsed = safeParse(raw, {})
|
||||||
|
const artifacts = Array.isArray(parsed?.artifacts) ? parsed.artifacts : []
|
||||||
|
return artifacts.map((item) => ({
|
||||||
|
title: String(item || '').trim(),
|
||||||
|
detail: '',
|
||||||
|
artifactType: String(item || '').trim(),
|
||||||
|
fileFormat: item === 'spreadsheet' ? 'xlsx' : item === 'report' ? 'md' : 'txt',
|
||||||
|
})).filter((item) => item.title)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeWorkflowSchema(remote, fallback) {
|
||||||
|
if (Array.isArray(fallback?.workflowSchema) && fallback.workflowSchema.length) {
|
||||||
|
return fallback.workflowSchema
|
||||||
|
}
|
||||||
|
const actionRefs = safeParse(remote.action_refs_json, [])
|
||||||
|
if (Array.isArray(actionRefs) && actionRefs.length) {
|
||||||
|
return actionRefs.map((item, index) => ({
|
||||||
|
title: `步骤 ${index + 1}`,
|
||||||
|
detail: String(item || '').trim(),
|
||||||
|
expectedOutput: '',
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCapabilityTags(remote, fallback) {
|
||||||
|
if (Array.isArray(fallback?.capabilityTags) && fallback.capabilityTags.length) {
|
||||||
|
return fallback.capabilityTags
|
||||||
|
}
|
||||||
|
const ontology = safeParse(remote.ontology_binding_json, {})
|
||||||
|
return [
|
||||||
|
...(Array.isArray(ontology?.object_types) ? ontology.object_types : []),
|
||||||
|
...(Array.isArray(ontology?.action_types) ? ontology.action_types : []),
|
||||||
|
].map((item) => String(item || '').trim()).filter(Boolean)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSkill(item) {
|
||||||
|
const fallback = staticSkills.find((skill) => skill.key === item?.key) || null
|
||||||
|
const starterPrompts = safeParse(item?.starter_prompts_json, [])
|
||||||
|
const remote = {
|
||||||
|
...item,
|
||||||
|
key: String(item?.key || '').trim(),
|
||||||
|
label: String(item?.label || '').trim(),
|
||||||
|
display_code: String(item?.display_code || '').trim() || String(fallback?.display_code || '').trim(),
|
||||||
|
eailogic_code: String(item?.eailogic_code || '').trim() || String(fallback?.eailogic_code || '').trim(),
|
||||||
|
caption: String(item?.description || '').trim() || String(fallback?.caption || '').trim(),
|
||||||
|
color: String(fallback?.color || '#409eff').trim(),
|
||||||
|
objectEntryRoute:
|
||||||
|
String(item?.object_entry_route || '').trim() ||
|
||||||
|
String(fallback?.objectEntryRoute || '').trim(),
|
||||||
|
starterPrompts: Array.isArray(starterPrompts)
|
||||||
|
? starterPrompts.map((prompt) => String(prompt || '').trim()).filter(Boolean)
|
||||||
|
: [],
|
||||||
|
promptTemplate: String(item?.prompt_template || '').trim(),
|
||||||
|
exposedToUser: item?.exposed_to_user !== false,
|
||||||
|
interactionCard: fallback?.interactionCard || null,
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...remote,
|
||||||
|
capabilityTags: normalizeCapabilityTags(item, fallback),
|
||||||
|
workflowSchema: normalizeWorkflowSchema(item, fallback),
|
||||||
|
artifactSchema: normalizeArtifactSchema(item?.artifact_schema_json, fallback),
|
||||||
|
interactionCard: buildFallbackInteractionCard(remote, fallback),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSkillCatalogStore = defineStore('skillCatalog', () => {
|
||||||
|
const loaded = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const skills = ref([])
|
||||||
|
|
||||||
|
const skillMap = computed(() =>
|
||||||
|
Object.fromEntries(skills.value.map((item) => [item.key, item]))
|
||||||
|
)
|
||||||
|
|
||||||
|
async function hydrate(force = false) {
|
||||||
|
if (loading.value) return
|
||||||
|
if (loaded.value && !force) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await listSkills()
|
||||||
|
skills.value = Array.isArray(res?.data)
|
||||||
|
? res.data.map((item) => normalizeSkill(item))
|
||||||
|
: []
|
||||||
|
loaded.value = true
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('load skill catalog failed', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getByKey(key) {
|
||||||
|
return skillMap.value[String(key || '').trim()] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loaded,
|
||||||
|
loading,
|
||||||
|
skills,
|
||||||
|
skillMap,
|
||||||
|
hydrate,
|
||||||
|
getByKey,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { computed, ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { listSpecialists } from '@/api/specialist'
|
||||||
|
|
||||||
|
function safeParseObject(value, fallback = {}) {
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) return value
|
||||||
|
const text = String(value || '').trim()
|
||||||
|
if (!text) return fallback
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(text)
|
||||||
|
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback
|
||||||
|
} catch {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeSpecialist(item) {
|
||||||
|
const interactionCard = safeParseObject(item?.interaction_card_json, {})
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
key: String(item?.key || '').trim(),
|
||||||
|
label: String(item?.label || '').trim(),
|
||||||
|
summary: String(item?.summary || '').trim(),
|
||||||
|
display_code: String(item?.display_code || '').trim(),
|
||||||
|
eailogic_code: String(item?.eailogic_code || '').trim(),
|
||||||
|
tier: String(item?.tier || '').trim(),
|
||||||
|
workerType: String(item?.worker_type || item?.workerType || '').trim(),
|
||||||
|
objectEntryRoute: String(item?.object_entry_route || item?.objectEntryRoute || '').trim(),
|
||||||
|
marketTag: String(item?.market_tag || item?.marketTag || '').trim(),
|
||||||
|
workStatus: String(item?.work_status || item?.workStatus || '').trim(),
|
||||||
|
riskLabel: String(item?.risk_label || item?.riskLabel || '').trim(),
|
||||||
|
color: String(item?.color || '').trim(),
|
||||||
|
stage: String(item?.stage || '').trim(),
|
||||||
|
interactionCard,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useSpecialistCatalogStore = defineStore('specialistCatalog', () => {
|
||||||
|
const loaded = ref(false)
|
||||||
|
const loading = ref(false)
|
||||||
|
const specialists = ref([])
|
||||||
|
|
||||||
|
const specialistMap = computed(() =>
|
||||||
|
Object.fromEntries(specialists.value.map((item) => [item.key, item]))
|
||||||
|
)
|
||||||
|
|
||||||
|
async function hydrate(force = false) {
|
||||||
|
if (loading.value) return
|
||||||
|
if (loaded.value && !force) return
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await listSpecialists()
|
||||||
|
specialists.value = Array.isArray(res?.data)
|
||||||
|
? res.data.map((item) => normalizeSpecialist(item))
|
||||||
|
: []
|
||||||
|
loaded.value = true
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('load specialist catalog failed', error)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getByKey(key) {
|
||||||
|
return specialistMap.value[String(key || '').trim()] || null
|
||||||
|
}
|
||||||
|
|
||||||
|
function getByPath(path) {
|
||||||
|
const normalized = String(path || '').trim()
|
||||||
|
if (!normalized) return null
|
||||||
|
return specialists.value.find((item) => item.objectEntryRoute === normalized) || null
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
loaded,
|
||||||
|
loading,
|
||||||
|
specialists,
|
||||||
|
specialistMap,
|
||||||
|
hydrate,
|
||||||
|
getByKey,
|
||||||
|
getByPath,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -460,7 +460,6 @@ export const useWorkerRuntimeStore = defineStore('workerRuntime', () => {
|
|||||||
const key = String(skillKey || '').trim()
|
const key = String(skillKey || '').trim()
|
||||||
if (!key) return
|
if (!key) return
|
||||||
if (!currentTaskId.value) {
|
if (!currentTaskId.value) {
|
||||||
pendingSpecialistKey.value = ''
|
|
||||||
pendingSkillKey.value = key
|
pendingSkillKey.value = key
|
||||||
pendingFocusedObject.value =
|
pendingFocusedObject.value =
|
||||||
key === DEFAULT_SKILL_KEY
|
key === DEFAULT_SKILL_KEY
|
||||||
|
|||||||
@@ -64,8 +64,9 @@
|
|||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref } from 'vue'
|
||||||
import { listConnectors } from '@/api/connector'
|
import { listConnectors } from '@/api/connector'
|
||||||
import { getSpecialistByKey } from '@/api/specialist'
|
import { getSpecialistByKey } from '@/api/specialist'
|
||||||
import { availableSkills, businessApps } from '@/config/workbench'
|
import { useAppCatalogStore } from '@/store/appCatalog'
|
||||||
import { productizedApps } from '@/config/productizedApps'
|
import { useSkillCatalogStore } from '@/store/skillCatalog'
|
||||||
|
import { useSpecialistCatalogStore } from '@/store/specialistCatalog'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
catalogType: {
|
catalogType: {
|
||||||
@@ -84,67 +85,12 @@ const props = defineProps({
|
|||||||
|
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
const remoteConnectors = ref([])
|
const remoteConnectors = ref([])
|
||||||
// 专员的技能绑定与信源只有后端有(前端 businessApps 是静态配置)。
|
// 专员详情里更完整的技能绑定与信源仍以后端 by-key 为准。
|
||||||
// 拉不到就退回只显示前端已知的内容,页面不会因此空掉或报错。
|
// 拉不到就退回目录接口那份基础信息,页面不会因此空掉或报错。
|
||||||
const remoteSpecialist = ref(null)
|
const remoteSpecialist = ref(null)
|
||||||
|
const appCatalog = useAppCatalogStore()
|
||||||
const catalogApps = [
|
const skillCatalog = useSkillCatalogStore()
|
||||||
{
|
const specialistCatalog = useSpecialistCatalogStore()
|
||||||
key: 'knowledge-hub',
|
|
||||||
display_code: '应01',
|
|
||||||
eailogic_code: 'EAI-A-01',
|
|
||||||
label: '知识库',
|
|
||||||
marketTag: '内建 APP',
|
|
||||||
workerType: 'adw',
|
|
||||||
tier: 'platform',
|
|
||||||
color: '#5b7cff',
|
|
||||||
summary: '组织级默认存在的知识底座 APP,为专家、技能与其它 APP 提供知识支持。',
|
|
||||||
description: '知识库是平台里的知识底座,用来沉淀资料、规则和证据,并为专家、技能和 APP 提供统一知识支持。',
|
|
||||||
prompts: ['我想先整理这批知识资料', '帮我看看这部分内容适合怎么入库', '知识库更适合承接哪些任务'],
|
|
||||||
tags: ['知识沉淀', '证据引用', '组织底座'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'internal-exam',
|
|
||||||
display_code: '应02',
|
|
||||||
eailogic_code: 'EAI-A-02',
|
|
||||||
label: '内部考试APP',
|
|
||||||
marketTag: '长程 APP',
|
|
||||||
workerType: 'adw',
|
|
||||||
tier: 'business',
|
|
||||||
color: '#29b36a',
|
|
||||||
summary: '围绕练习、自测、正式考试、记录、证书和岗位应学组织的内部考试 APP。',
|
|
||||||
description: '内部考试 APP 用来承接训练、练习、考试、成绩记录和证书结果,适合需要完整过程管理的学习场景。',
|
|
||||||
prompts: ['我想组织一轮内部考试', '帮我看看这个考试流程怎么搭', '这个场景适合放到内部考试 APP 吗'],
|
|
||||||
tags: ['训练流程', '考试记录', '结果沉淀'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'internal-training',
|
|
||||||
display_code: '应03',
|
|
||||||
eailogic_code: 'EAI-A-03',
|
|
||||||
label: '内部培训APP',
|
|
||||||
marketTag: '长程 APP',
|
|
||||||
workerType: 'worker',
|
|
||||||
tier: 'business',
|
|
||||||
color: '#9b6bff',
|
|
||||||
summary: '统一承接销售培训、产品知识和公司介绍三个培训模块的内部培训 APP。',
|
|
||||||
description: '内部培训 APP 用来把销售培训、产品知识和公司介绍收在同一个对象下面,避免把同一条培训链拆成多个 APP。',
|
|
||||||
prompts: ['我想搭一个内部培训 APP', '帮我梳理销售培训、产品知识和公司介绍的关系', '这个培训主题应该挂到内部培训 APP 吗'],
|
|
||||||
tags: ['课程组织', '产品知识', '组织导览'],
|
|
||||||
},
|
|
||||||
].concat(productizedApps.map((item) => ({
|
|
||||||
key: item.key,
|
|
||||||
display_code: item.display_code,
|
|
||||||
eailogic_code: item.eailogic_code,
|
|
||||||
label: item.label,
|
|
||||||
marketTag: item.badge,
|
|
||||||
workerType: item.skillKey ? 'worker' : 'adw',
|
|
||||||
tier: 'business',
|
|
||||||
color: item.color,
|
|
||||||
summary: item.summary,
|
|
||||||
description: item.description,
|
|
||||||
prompts: item.prompts,
|
|
||||||
tags: item.tags,
|
|
||||||
})))
|
|
||||||
|
|
||||||
const resolvedCatalogType = computed(() => String(props.catalogType || 'specialists'))
|
const resolvedCatalogType = computed(() => String(props.catalogType || 'specialists'))
|
||||||
const resolvedObjectKey = computed(() => decodeURIComponent(String(props.objectKey || '')))
|
const resolvedObjectKey = computed(() => decodeURIComponent(String(props.objectKey || '')))
|
||||||
@@ -162,33 +108,33 @@ function withFallbackCodes(items, { displayPrefix, logicPrefix, start = 1 }) {
|
|||||||
|
|
||||||
const detail = computed(() => {
|
const detail = computed(() => {
|
||||||
if (resolvedCatalogType.value === 'skills') {
|
if (resolvedCatalogType.value === 'skills') {
|
||||||
const skill = availableSkills.find((item) => item.key === resolvedObjectKey.value)
|
const skill = skillCatalog.getByKey(resolvedObjectKey.value)
|
||||||
if (!skill) return notFoundDetail('技能')
|
if (!skill) return notFoundDetail('技能')
|
||||||
return {
|
return {
|
||||||
title: skill.label,
|
title: skill.label,
|
||||||
display_code: skill.display_code,
|
display_code: skill.display_code,
|
||||||
eailogic_code: skill.eailogic_code,
|
eailogic_code: skill.eailogic_code,
|
||||||
subtitle: skill.roleCard?.relationshipToUser || '你的技能助手',
|
subtitle: skill.interactionCard?.relationshipToUser || '你的技能助手',
|
||||||
metaText: '已安装 · 在对话里直接挂载',
|
metaText: '已安装 · 在对话里直接挂载',
|
||||||
badge: '技能',
|
badge: '技能',
|
||||||
avatar: skill.label.slice(0, 1),
|
avatar: skill.label.slice(0, 1),
|
||||||
color: skill.color || '#409eff',
|
color: skill.color || '#409eff',
|
||||||
description:
|
description:
|
||||||
skill.roleCard?.greeting ||
|
skill.interactionCard?.greeting ||
|
||||||
`这个技能主要负责 ${skill.caption || skill.label},适合你已经知道目标、希望直接开工的时候使用。`,
|
`这个技能主要负责 ${skill.caption || skill.label},适合你已经知道目标、希望直接开工的时候使用。`,
|
||||||
tags: normalizeArray(skill.capabilityTags).length
|
tags: normalizeArray(skill.capabilityTags).length
|
||||||
? normalizeArray(skill.capabilityTags)
|
? normalizeArray(skill.capabilityTags)
|
||||||
: buildTags(skill.caption, skill.roleCard?.tagline),
|
: buildTags(skill.caption, skill.interactionCard?.tagline),
|
||||||
sceneTitle: '适合什么时候用',
|
sceneTitle: '适合什么时候用',
|
||||||
sceneText: buildSkillUseCase(skill),
|
sceneText: buildSkillUseCase(skill),
|
||||||
extraSections: buildSkillExtraSections(skill),
|
extraSections: buildSkillExtraSections(skill),
|
||||||
promptTitle: '你可以直接这样发起',
|
promptTitle: '你可以直接这样发起',
|
||||||
prompts: normalizeArray(skill.roleCard?.starterPrompts),
|
prompts: normalizeArray(skill.interactionCard?.starterPrompts),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resolvedCatalogType.value === 'apps') {
|
if (resolvedCatalogType.value === 'apps') {
|
||||||
const app = catalogApps.find((item) => item.key === resolvedObjectKey.value)
|
const app = appCatalog.getByKey(resolvedObjectKey.value)
|
||||||
if (!app) return notFoundDetail('APP')
|
if (!app) return notFoundDetail('APP')
|
||||||
return {
|
return {
|
||||||
title: app.label,
|
title: app.label,
|
||||||
@@ -241,30 +187,33 @@ const detail = computed(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const specialist = businessApps.find((item) => item.key === resolvedObjectKey.value)
|
const specialist = specialistCatalog.getByKey(resolvedObjectKey.value)
|
||||||
if (!specialist) return notFoundDetail('专员')
|
if (!specialist) return notFoundDetail('专员')
|
||||||
return {
|
return {
|
||||||
title: specialist.label,
|
title: specialist.label,
|
||||||
display_code: specialist.display_code,
|
display_code: specialist.display_code,
|
||||||
eailogic_code: specialist.eailogic_code,
|
eailogic_code: specialist.eailogic_code,
|
||||||
subtitle: specialist.roleCard?.relationshipToUser || '你的协作搭档',
|
subtitle: specialist.interactionCard?.relationshipToUser || '你的协作搭档',
|
||||||
metaText: `${specialist.tier === 'industry' ? '行业专员' : '通用专员'} · ${specialist.workerType === 'adw' ? '流程推进型' : '执行处理型'} · ${specialist.marketTag || '可用'}`,
|
metaText: `${specialist.tier === 'industry' ? '行业专员' : '通用专员'} · ${specialist.workerType === 'adw' ? '流程推进型' : '执行处理型'} · ${specialist.marketTag || '可用'}`,
|
||||||
badge: '专员',
|
badge: '专员',
|
||||||
avatar: specialist.label.slice(0, 1),
|
avatar: specialist.label.slice(0, 1),
|
||||||
color: specialist.color || '#409eff',
|
color: specialist.color || '#409eff',
|
||||||
description:
|
description:
|
||||||
specialist.roleCard?.greeting ||
|
specialist.interactionCard?.greeting ||
|
||||||
`这个专员主要负责 ${specialist.roleCard?.tagline || specialist.label},可以陪你把事情往前推进。`,
|
`这个专员主要负责 ${specialist.interactionCard?.tagline || specialist.label},可以陪你把事情往前推进。`,
|
||||||
tags: buildTags(specialist.roleCard?.tagline, specialist.summary),
|
tags: buildTags(specialist.interactionCard?.tagline, specialist.summary),
|
||||||
sceneTitle: '适合什么时候用',
|
sceneTitle: '适合什么时候用',
|
||||||
sceneText: buildSpecialistUseCase(specialist),
|
sceneText: buildSpecialistUseCase(specialist),
|
||||||
extraSections: buildSpecialistExtraSections(remoteSpecialist.value),
|
extraSections: buildSpecialistExtraSections(remoteSpecialist.value),
|
||||||
promptTitle: '专员帮你做',
|
promptTitle: '专员帮你做',
|
||||||
prompts: normalizeArray(specialist.roleCard?.starterPrompts),
|
prompts: normalizeArray(specialist.interactionCard?.starterPrompts),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await appCatalog.hydrate()
|
||||||
|
await skillCatalog.hydrate()
|
||||||
|
await specialistCatalog.hydrate()
|
||||||
if (resolvedCatalogType.value === 'connectors') {
|
if (resolvedCatalogType.value === 'connectors') {
|
||||||
try {
|
try {
|
||||||
const res = await listConnectors()
|
const res = await listConnectors()
|
||||||
@@ -332,7 +281,7 @@ function splitKeywords(value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildSpecialistUseCase(specialist) {
|
function buildSpecialistUseCase(specialist) {
|
||||||
const openingPrompt = String(specialist.roleCard?.openingPrompt || '').trim()
|
const openingPrompt = String(specialist.interactionCard?.openingPrompt || '').trim()
|
||||||
if (openingPrompt) {
|
if (openingPrompt) {
|
||||||
return `当你已经有目标、材料,或者已经知道当前卡点时,就适合找它介入。${openingPrompt}`
|
return `当你已经有目标、材料,或者已经知道当前卡点时,就适合找它介入。${openingPrompt}`
|
||||||
}
|
}
|
||||||
@@ -340,7 +289,7 @@ function buildSpecialistUseCase(specialist) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function buildSkillUseCase(skill) {
|
function buildSkillUseCase(skill) {
|
||||||
const openingPrompt = String(skill.roleCard?.openingPrompt || '').trim()
|
const openingPrompt = String(skill.interactionCard?.openingPrompt || '').trim()
|
||||||
const caption = String(skill.caption || skill.label || '').trim()
|
const caption = String(skill.caption || skill.label || '').trim()
|
||||||
if (openingPrompt) {
|
if (openingPrompt) {
|
||||||
return `当你已经明确想做“${caption}”,并且希望它直接开工、尽快给出产物时,就适合挂载这个技能。${openingPrompt}`
|
return `当你已经明确想做“${caption}”,并且希望它直接开工、尽快给出产物时,就适合挂载这个技能。${openingPrompt}`
|
||||||
@@ -363,7 +312,7 @@ function buildSpecialistExtraSections(remote) {
|
|||||||
sections.push({
|
sections.push({
|
||||||
title: '它会调用的技能',
|
title: '它会调用的技能',
|
||||||
items: skillKeys.map((key) => {
|
items: skillKeys.map((key) => {
|
||||||
const label = availableSkills.find((item) => item.key === key)?.label
|
const label = skillCatalog.getByKey(key)?.label
|
||||||
return label ? `${label}(${key})` : key
|
return label ? `${label}(${key})` : key
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -189,14 +189,18 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { listConnectors } from '@/api/connector'
|
import { listConnectors } from '@/api/connector'
|
||||||
import { availableSkills, businessApps } from '@/config/workbench'
|
import { useAppCatalogStore } from '@/store/appCatalog'
|
||||||
import { appOpenRoute, productizedApps } from '@/config/productizedApps'
|
|
||||||
import { useAppCenterStore } from '@/store/appCenter'
|
import { useAppCenterStore } from '@/store/appCenter'
|
||||||
|
import { useSkillCatalogStore } from '@/store/skillCatalog'
|
||||||
|
import { useSpecialistCatalogStore } from '@/store/specialistCatalog'
|
||||||
import CapabilityCatalogDetailPage from '@/views/workbench/CapabilityCatalogDetailPage.vue'
|
import CapabilityCatalogDetailPage from '@/views/workbench/CapabilityCatalogDetailPage.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const appCatalog = useAppCatalogStore()
|
||||||
const appCenter = useAppCenterStore()
|
const appCenter = useAppCenterStore()
|
||||||
|
const skillCatalog = useSkillCatalogStore()
|
||||||
|
const specialistCatalog = useSpecialistCatalogStore()
|
||||||
|
|
||||||
const remoteConnectors = ref([])
|
const remoteConnectors = ref([])
|
||||||
const searchQuery = ref('')
|
const searchQuery = ref('')
|
||||||
@@ -220,66 +224,6 @@ const coverToneOptions = [
|
|||||||
{ label: '深蓝封面', value: 'linear-gradient(135deg, #1d4ed8 0%, #38bdf8 100%)' },
|
{ label: '深蓝封面', value: 'linear-gradient(135deg, #1d4ed8 0%, #38bdf8 100%)' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const baseCatalogApps = [
|
|
||||||
{
|
|
||||||
key: 'knowledge-hub',
|
|
||||||
display_code: '应01',
|
|
||||||
eailogic_code: 'EAI-A-01',
|
|
||||||
title: '知识库',
|
|
||||||
badge: '内建 APP',
|
|
||||||
kind: '组织级 APP',
|
|
||||||
desc: '组织级默认存在的知识底座 APP,为专家、技能与其它 APP 提供知识支持。',
|
|
||||||
color: '#5b7cff',
|
|
||||||
pageRoute: '/knowledge-hub',
|
|
||||||
installState: 'installed',
|
|
||||||
iconText: '知',
|
|
||||||
coverTone: 'linear-gradient(135deg, #5b7cff 0%, #8fb4ff 100%)',
|
|
||||||
meta: ['默认内建', '知识底座', '一级导航'],
|
|
||||||
config: [
|
|
||||||
{ label: '管理入口', value: '/knowledge-management' },
|
|
||||||
{ label: '运行形态', value: '知识应用 + 管理台' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'internal-exam',
|
|
||||||
display_code: '应02',
|
|
||||||
eailogic_code: 'EAI-A-02',
|
|
||||||
title: '内部考试APP',
|
|
||||||
badge: '长程 APP',
|
|
||||||
kind: '内部考试',
|
|
||||||
desc: '围绕练习、自测、正式考试、记录、证书和岗位应学组织的内部考试 APP。',
|
|
||||||
color: '#29b36a',
|
|
||||||
pageRoute: '/exam/self-test',
|
|
||||||
installState: 'installed',
|
|
||||||
iconText: '考',
|
|
||||||
coverTone: 'linear-gradient(135deg, #1c9f5a 0%, #6ee7b7 100%)',
|
|
||||||
meta: ['题卡式', '强状态', '阶段化'],
|
|
||||||
config: [
|
|
||||||
{ label: '正式考试', value: '/exam/formal' },
|
|
||||||
{ label: '运行形态', value: '训练 + 考试 + 记录' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
key: 'internal-training',
|
|
||||||
display_code: '应03',
|
|
||||||
eailogic_code: 'EAI-A-03',
|
|
||||||
title: '内部培训APP',
|
|
||||||
badge: '长程 APP',
|
|
||||||
kind: '内部培训',
|
|
||||||
desc: '统一承接销售培训、产品知识和公司介绍三个培训模块的内部培训 APP。',
|
|
||||||
color: '#9b6bff',
|
|
||||||
pageRoute: '/courses',
|
|
||||||
installState: 'installed',
|
|
||||||
iconText: '培',
|
|
||||||
coverTone: 'linear-gradient(135deg, #8b5cf6 0%, #c4b5fd 100%)',
|
|
||||||
meta: ['课程式', '知识导览', '统一培训入口'],
|
|
||||||
config: [
|
|
||||||
{ label: '子模块', value: '销售培训 / 产品知识 / 公司介绍' },
|
|
||||||
{ label: '运行形态', value: '课程 + 资料 + 导览' },
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const catalogTypes = ['specialists', 'skills', 'apps', 'connectors']
|
const catalogTypes = ['specialists', 'skills', 'apps', 'connectors']
|
||||||
|
|
||||||
const pageType = computed(() => {
|
const pageType = computed(() => {
|
||||||
@@ -317,14 +261,10 @@ const searchPlaceholder = computed(() => {
|
|||||||
return '搜索专员名称、定位或能力标签'
|
return '搜索专员名称、定位或能力标签'
|
||||||
})
|
})
|
||||||
|
|
||||||
const appSkillOptions = computed(() => availableSkills.filter((item) => item.key !== 'smart-assistant'))
|
const appSkillOptions = computed(() => skillCatalog.skills.filter((item) => item.key !== 'smart-assistant'))
|
||||||
const dialogTitle = computed(() => (editingAppKey.value ? '编辑应用' : '新建应用'))
|
const dialogTitle = computed(() => (editingAppKey.value ? '编辑应用' : '新建应用'))
|
||||||
|
|
||||||
const catalogApps = computed(() => [
|
const catalogApps = computed(() => appCatalog.apps.map((item) => buildAppCard(item)))
|
||||||
...baseCatalogApps,
|
|
||||||
...productizedApps.map((item) => buildProductizedAppCard(item)),
|
|
||||||
...appCenter.customApps.map((item) => buildProductizedAppCard(item, { custom: true })),
|
|
||||||
])
|
|
||||||
|
|
||||||
function withFallbackCodes(items, { displayPrefix, logicPrefix, start = 1 }) {
|
function withFallbackCodes(items, { displayPrefix, logicPrefix, start = 1 }) {
|
||||||
return items.map((item, index) => {
|
return items.map((item, index) => {
|
||||||
@@ -339,14 +279,14 @@ function withFallbackCodes(items, { displayPrefix, logicPrefix, start = 1 }) {
|
|||||||
|
|
||||||
const cards = computed(() => {
|
const cards = computed(() => {
|
||||||
if (pageType.value === 'skills') {
|
if (pageType.value === 'skills') {
|
||||||
return availableSkills.map((skill) => ({
|
return skillCatalog.skills.map((skill) => ({
|
||||||
key: skill.key,
|
key: skill.key,
|
||||||
display_code: skill.display_code,
|
display_code: skill.display_code,
|
||||||
eailogic_code: skill.eailogic_code,
|
eailogic_code: skill.eailogic_code,
|
||||||
title: skill.label,
|
title: skill.label,
|
||||||
badge: '技能',
|
badge: '技能',
|
||||||
kind: '任务入口',
|
kind: '任务入口',
|
||||||
desc: skill.roleCard?.tagline || skill.caption,
|
desc: skill.interactionCard?.tagline || skill.caption,
|
||||||
color: skill.color,
|
color: skill.color,
|
||||||
installState: 'installed',
|
installState: 'installed',
|
||||||
meta: [],
|
meta: [],
|
||||||
@@ -382,24 +322,24 @@ const cards = computed(() => {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
return businessApps.map((app) => ({
|
return specialistCatalog.specialists.map((specialist) => ({
|
||||||
key: app.key,
|
key: specialist.key,
|
||||||
display_code: app.display_code,
|
display_code: specialist.display_code,
|
||||||
eailogic_code: app.eailogic_code,
|
eailogic_code: specialist.eailogic_code,
|
||||||
title: app.label,
|
title: specialist.label,
|
||||||
badge: app.marketTag,
|
badge: specialist.marketTag,
|
||||||
kind: app.workerType === 'adw' ? '编排型专员' : '执行型专员',
|
kind: specialist.workerType === 'adw' ? '编排型专员' : '执行型专员',
|
||||||
desc: app.roleCard?.tagline || app.summary,
|
desc: specialist.interactionCard?.tagline || specialist.summary,
|
||||||
color: app.color,
|
color: specialist.color,
|
||||||
installState: app.marketTag === '已安装' ? 'installed' : 'pending',
|
installState: specialist.marketTag === '已安装' ? 'installed' : 'pending',
|
||||||
meta: [],
|
meta: [],
|
||||||
config: [],
|
config: [],
|
||||||
searchTokens: [
|
searchTokens: [
|
||||||
app.tier === 'industry' ? '行业专员' : '通用专员',
|
specialist.tier === 'industry' ? '行业专员' : '通用专员',
|
||||||
app.workerType === 'adw' ? '编排协作' : '执行协作',
|
specialist.workerType === 'adw' ? '编排协作' : '执行协作',
|
||||||
app.marketTag,
|
specialist.marketTag,
|
||||||
app.tier,
|
specialist.tier,
|
||||||
app.workerType,
|
specialist.workerType,
|
||||||
].filter(Boolean),
|
].filter(Boolean),
|
||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
@@ -479,7 +419,9 @@ const catalogSections = computed(() => {
|
|||||||
const visibleCount = computed(() => filteredCards.value.length)
|
const visibleCount = computed(() => filteredCards.value.length)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await appCenter.hydrate()
|
await appCatalog.hydrate()
|
||||||
|
await skillCatalog.hydrate()
|
||||||
|
await specialistCatalog.hydrate()
|
||||||
resetCreateForm()
|
resetCreateForm()
|
||||||
try {
|
try {
|
||||||
const res = await listConnectors()
|
const res = await listConnectors()
|
||||||
@@ -561,7 +503,7 @@ function sectionLabels(type) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildProductizedAppCard(item, { custom = false } = {}) {
|
function buildAppCard(item) {
|
||||||
return {
|
return {
|
||||||
key: item.key,
|
key: item.key,
|
||||||
display_code: item.display_code,
|
display_code: item.display_code,
|
||||||
@@ -577,16 +519,16 @@ function buildProductizedAppCard(item, { custom = false } = {}) {
|
|||||||
tags: item.tags || [],
|
tags: item.tags || [],
|
||||||
iconText: item.iconText || '应用',
|
iconText: item.iconText || '应用',
|
||||||
coverTone: item.coverTone || item.color,
|
coverTone: item.coverTone || item.color,
|
||||||
pageRoute: appOpenRoute(item),
|
pageRoute: appCatalog.resolveOpenRoute(item),
|
||||||
meta: [],
|
meta: [],
|
||||||
config: [],
|
config: [],
|
||||||
searchTokens: [
|
searchTokens: [
|
||||||
...(item.tags || []),
|
...(item.tags || []),
|
||||||
...(custom ? ['我的应用'] : []),
|
...(item.isCustomApp ? ['我的应用'] : []),
|
||||||
...(appCenter.isFavorite(item.key) ? ['已收藏'] : []),
|
...(appCenter.isFavorite(item.key) ? ['已收藏'] : []),
|
||||||
],
|
],
|
||||||
actionLabel: '立即打开',
|
actionLabel: '立即打开',
|
||||||
isCustomApp: custom,
|
isCustomApp: item.isCustomApp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,10 +103,13 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { getWorkerTaskDetail, listMyTasks } from '@/api/worker'
|
import { getWorkerTaskDetail, listMyTasks } from '@/api/worker'
|
||||||
import { availableSkills, businessApps } from '@/config/workbench'
|
import { useSkillCatalogStore } from '@/store/skillCatalog'
|
||||||
|
import { useSpecialistCatalogStore } from '@/store/specialistCatalog'
|
||||||
import { useWorkerRuntimeStore } from '@/store/workerRuntime'
|
import { useWorkerRuntimeStore } from '@/store/workerRuntime'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
const skillCatalog = useSkillCatalogStore()
|
||||||
|
const specialistCatalog = useSpecialistCatalogStore()
|
||||||
const workerRuntime = useWorkerRuntimeStore()
|
const workerRuntime = useWorkerRuntimeStore()
|
||||||
|
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -126,7 +129,11 @@ const taskCount = computed(() => {
|
|||||||
})
|
})
|
||||||
const latestUpdatedLabel = computed(() => formatTime(latestUpdatedAt.value))
|
const latestUpdatedLabel = computed(() => formatTime(latestUpdatedAt.value))
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
skillCatalog.hydrate(),
|
||||||
|
specialistCatalog.hydrate(),
|
||||||
|
])
|
||||||
loadCenter()
|
loadCenter()
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -215,10 +222,10 @@ function parseContext(value) {
|
|||||||
function lookupSpecialistLabel(key) {
|
function lookupSpecialistLabel(key) {
|
||||||
const normalized = String(key || '').trim()
|
const normalized = String(key || '').trim()
|
||||||
if (!normalized) return '通用助手'
|
if (!normalized) return '通用助手'
|
||||||
const skill = availableSkills.find((item) => item.key === normalized)
|
const skill = skillCatalog.getByKey(normalized)
|
||||||
if (skill) return skill.label
|
if (skill) return skill.label
|
||||||
const app = businessApps.find((item) => item.key === normalized)
|
const specialist = specialistCatalog.getByKey(normalized)
|
||||||
if (app) return app.label
|
if (specialist) return specialist.label
|
||||||
return normalized
|
return normalized
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -646,7 +646,7 @@ function openEdit(row) {
|
|||||||
form.base_skills = row.base_skills || ''
|
form.base_skills = row.base_skills || ''
|
||||||
form.ai_assistance = row.ai_assistance || ''
|
form.ai_assistance = row.ai_assistance || ''
|
||||||
form.generated_skills = row.generated_skills || ''
|
form.generated_skills = row.generated_skills || ''
|
||||||
form.inputs_records_json = prettyJSON(row.inputs_records_json || row.source_records_json)
|
form.inputs_records_json = prettyJSON(row.inputs_records_json)
|
||||||
form.permission_records_json = prettyJSON(row.permission_records_json)
|
form.permission_records_json = prettyJSON(row.permission_records_json)
|
||||||
form.action_records_json = prettyJSON(row.action_records_json)
|
form.action_records_json = prettyJSON(row.action_records_json)
|
||||||
form.result_records_json = prettyJSON(row.result_records_json)
|
form.result_records_json = prettyJSON(row.result_records_json)
|
||||||
@@ -733,7 +733,7 @@ async function toggleStatus(row) {
|
|||||||
base_skills: row.base_skills,
|
base_skills: row.base_skills,
|
||||||
ai_assistance: row.ai_assistance,
|
ai_assistance: row.ai_assistance,
|
||||||
generated_skills: row.generated_skills,
|
generated_skills: row.generated_skills,
|
||||||
inputs_records_json: row.inputs_records_json || row.source_records_json,
|
inputs_records_json: row.inputs_records_json,
|
||||||
permission_records_json: row.permission_records_json,
|
permission_records_json: row.permission_records_json,
|
||||||
action_records_json: row.action_records_json,
|
action_records_json: row.action_records_json,
|
||||||
result_records_json: row.result_records_json,
|
result_records_json: row.result_records_json,
|
||||||
@@ -880,7 +880,7 @@ function buildGeneratedRecords(type) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function loadRecordEditors(row = {}) {
|
function loadRecordEditors(row = {}) {
|
||||||
replaceRecords('inputs', parseRecordArray(row.inputs_records_json || row.source_records_json).map(normalizeInputRecord))
|
replaceRecords('inputs', parseRecordArray(row.inputs_records_json).map(normalizeInputRecord))
|
||||||
replaceRecords('permissions', parseRecordArray(row.permission_records_json).map(normalizePermissionRecord))
|
replaceRecords('permissions', parseRecordArray(row.permission_records_json).map(normalizePermissionRecord))
|
||||||
replaceRecords('actions', parseRecordArray(row.action_records_json).map(normalizeActionRecord))
|
replaceRecords('actions', parseRecordArray(row.action_records_json).map(normalizeActionRecord))
|
||||||
replaceRecords('results', parseRecordArray(row.result_records_json).map(normalizeResultRecord))
|
replaceRecords('results', parseRecordArray(row.result_records_json).map(normalizeResultRecord))
|
||||||
|
|||||||
@@ -197,14 +197,16 @@ import { ref, computed, nextTick, watch, onMounted, markRaw } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { storeToRefs } from 'pinia'
|
import { storeToRefs } from 'pinia'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { chatWithAssistant } from '@/api/assistant'
|
import { sendAssistantChatMessage } from '@/api/chat'
|
||||||
import { uploadMedia } from '@/api/media'
|
import { uploadMedia } from '@/api/media'
|
||||||
import ChatLayout from '@/components/chat/ChatLayout.vue'
|
import ChatLayout from '@/components/chat/ChatLayout.vue'
|
||||||
import ChatInputBar from '@/components/chat/ChatInputBar.vue'
|
import ChatInputBar from '@/components/chat/ChatInputBar.vue'
|
||||||
import CurrentObjectChip from '@/components/chat/CurrentObjectChip.vue'
|
import CurrentObjectChip from '@/components/chat/CurrentObjectChip.vue'
|
||||||
import SpecialistPanel from '@/components/chat/SpecialistPanel.vue'
|
import SpecialistPanel from '@/components/chat/SpecialistPanel.vue'
|
||||||
import SkillStrip from '@/components/chat/SkillStrip.vue'
|
import SkillStrip from '@/components/chat/SkillStrip.vue'
|
||||||
import { assistantModes, availableSkills, businessApps } from '@/config/workbench'
|
import { assistantModes, staticSkillCatalog } from '@/config/workbench'
|
||||||
|
import { useSkillCatalogStore } from '@/store/skillCatalog'
|
||||||
|
import { useSpecialistCatalogStore } from '@/store/specialistCatalog'
|
||||||
import { GENERAL_ASSISTANT_KEY, useWorkerRuntimeStore } from '@/store/workerRuntime'
|
import { GENERAL_ASSISTANT_KEY, useWorkerRuntimeStore } from '@/store/workerRuntime'
|
||||||
import { useSkillSettingsStore } from '@/store/skillSettings'
|
import { useSkillSettingsStore } from '@/store/skillSettings'
|
||||||
import { useAiRouteStore } from '@/store/llmRoute'
|
import { useAiRouteStore } from '@/store/llmRoute'
|
||||||
@@ -215,6 +217,8 @@ import { buildComposerBlocks, blocksToMountedResources, hasSendableContent } fro
|
|||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const workerRuntime = useWorkerRuntimeStore()
|
const workerRuntime = useWorkerRuntimeStore()
|
||||||
|
const skillCatalog = useSkillCatalogStore()
|
||||||
|
const specialistCatalog = useSpecialistCatalogStore()
|
||||||
const workbenchStore = useWorkbenchStore()
|
const workbenchStore = useWorkbenchStore()
|
||||||
const {
|
const {
|
||||||
currentFocusedObjectKind,
|
currentFocusedObjectKind,
|
||||||
@@ -257,7 +261,7 @@ const { selectedAiChatRouteId } = storeToRefs(aiRouteStore)
|
|||||||
const composerUploads = ref([])
|
const composerUploads = ref([])
|
||||||
const MAX_UPLOAD_FILES_PER_BATCH = 100
|
const MAX_UPLOAD_FILES_PER_BATCH = 100
|
||||||
|
|
||||||
const defaultRolePresentation = Object.freeze({
|
const defaultAssistantPresentation = Object.freeze({
|
||||||
kind: 'assistant',
|
kind: 'assistant',
|
||||||
label: '通用助手',
|
label: '通用助手',
|
||||||
greeting: '博昇AI数字员工,您说,我做',
|
greeting: '博昇AI数字员工,您说,我做',
|
||||||
@@ -292,14 +296,14 @@ const mountedSkillDefinition = computed(() => {
|
|||||||
icon: registrySkill.icon || 'skill',
|
icon: registrySkill.icon || 'skill',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const workbenchSkill = availableSkills.find((skill) => skill.key === key)
|
const catalogSkill = skillCatalog.getByKey(key) || staticSkillCatalog.find((skill) => skill.key === key)
|
||||||
if (!workbenchSkill) return null
|
if (!catalogSkill) return null
|
||||||
return {
|
return {
|
||||||
key: workbenchSkill.key,
|
key: catalogSkill.key,
|
||||||
label: workbenchSkill.label,
|
label: catalogSkill.label,
|
||||||
description: workbenchSkill.caption || '',
|
description: catalogSkill.caption || '',
|
||||||
skillId: workbenchSkill.key,
|
skillId: catalogSkill.key,
|
||||||
uri: `skill://${workbenchSkill.key}`,
|
uri: `skill://${catalogSkill.key}`,
|
||||||
icon: 'skill',
|
icon: 'skill',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -307,8 +311,8 @@ const mountedSkillDefinition = computed(() => {
|
|||||||
const currentSpecialistPresentation = computed(() => {
|
const currentSpecialistPresentation = computed(() => {
|
||||||
const key = currentSpecialistKey.value
|
const key = currentSpecialistKey.value
|
||||||
if (!key || key === GENERAL_ASSISTANT_KEY) return null
|
if (!key || key === GENERAL_ASSISTANT_KEY) return null
|
||||||
const app = businessApps.find((item) => item.key === key) || null
|
const specialist = specialistCatalog.getByKey(key)
|
||||||
if (!app) {
|
if (!specialist) {
|
||||||
const label = currentSpecialist.value?.label || '当前专员'
|
const label = currentSpecialist.value?.label || '当前专员'
|
||||||
return {
|
return {
|
||||||
kind: 'specialist',
|
kind: 'specialist',
|
||||||
@@ -322,57 +326,57 @@ const currentSpecialistPresentation = computed(() => {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
kind: 'specialist',
|
kind: 'specialist',
|
||||||
label: app.label,
|
label: specialist.label,
|
||||||
greeting: app.label,
|
greeting: specialist.label,
|
||||||
tagline: '',
|
tagline: '',
|
||||||
openingPrompt: app.roleCard?.openingPrompt || `告诉我你希望${app.label}先做什么。`,
|
openingPrompt: specialist.interactionCard?.openingPrompt || `告诉我你希望${specialist.label}先做什么。`,
|
||||||
starterPrompts: app.roleCard?.starterPrompts || [],
|
starterPrompts: specialist.interactionCard?.starterPrompts || [],
|
||||||
meta: '当前挂载专员',
|
meta: '当前挂载专员',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentSkillPresentation = computed(() => {
|
const currentSkillPresentation = computed(() => {
|
||||||
const key = currentSkillKey.value
|
const key = currentSkillKey.value
|
||||||
const skill = availableSkills.find((item) => item.key === key) || null
|
const skill = skillCatalog.getByKey(key) || staticSkillCatalog.find((item) => item.key === key) || null
|
||||||
if (!skill) return null
|
if (!skill) return null
|
||||||
const isHomeSkill = key === HOME_SKILL
|
const isHomeSkill = key === HOME_SKILL
|
||||||
return {
|
return {
|
||||||
kind: isHomeSkill ? 'assistant' : 'skill',
|
kind: isHomeSkill ? 'assistant' : 'skill',
|
||||||
label: skill.label,
|
label: skill.label,
|
||||||
greeting: isHomeSkill ? (skill.roleCard?.greeting || `你好,我是${skill.label}。`) : `${skill.label}技能`,
|
greeting: isHomeSkill ? (skill.interactionCard?.greeting || `你好,我是${skill.label}。`) : `${skill.label}技能`,
|
||||||
tagline: isHomeSkill ? (skill.roleCard?.tagline || skill.caption || '') : '',
|
tagline: isHomeSkill ? (skill.interactionCard?.tagline || skill.caption || '') : '',
|
||||||
openingPrompt: skill.roleCard?.openingPrompt || `告诉我你想让${skill.label}处理什么内容。`,
|
openingPrompt: skill.interactionCard?.openingPrompt || `告诉我你想让${skill.label}处理什么内容。`,
|
||||||
starterPrompts: skill.roleCard?.starterPrompts || [],
|
starterPrompts: skill.interactionCard?.starterPrompts || [],
|
||||||
meta: isHomeSkill ? '默认角色 · 通用协作' : '当前挂载技能',
|
meta: isHomeSkill ? '默认角色 · 通用协作' : '当前挂载技能',
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentRolePresentation = computed(() => (
|
const currentObjectPresentation = computed(() => (
|
||||||
currentFocusedObjectKind.value === 'specialist'
|
currentFocusedObjectKind.value === 'specialist'
|
||||||
? (currentSpecialistPresentation.value || currentSkillPresentation.value || defaultRolePresentation)
|
? (currentSpecialistPresentation.value || currentSkillPresentation.value || defaultAssistantPresentation)
|
||||||
: (currentSkillPresentation.value || currentSpecialistPresentation.value || defaultRolePresentation)
|
: (currentSkillPresentation.value || currentSpecialistPresentation.value || defaultAssistantPresentation)
|
||||||
))
|
))
|
||||||
|
|
||||||
const welcomeKicker = computed(() => currentRolePresentation.value.meta)
|
const welcomeKicker = computed(() => currentObjectPresentation.value.meta)
|
||||||
const welcomeTitle = computed(() => currentRolePresentation.value.greeting)
|
const welcomeTitle = computed(() => currentObjectPresentation.value.greeting)
|
||||||
const welcomeSubtitle = computed(
|
const welcomeSubtitle = computed(
|
||||||
() => {
|
() => {
|
||||||
if (currentRolePresentation.value.kind === 'skill' || currentRolePresentation.value.kind === 'specialist') {
|
if (currentObjectPresentation.value.kind === 'skill' || currentObjectPresentation.value.kind === 'specialist') {
|
||||||
return currentRolePresentation.value.openingPrompt || ''
|
return currentObjectPresentation.value.openingPrompt || ''
|
||||||
}
|
}
|
||||||
return currentRolePresentation.value.tagline || currentRolePresentation.value.openingPrompt
|
return currentObjectPresentation.value.tagline || currentObjectPresentation.value.openingPrompt
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
const starterPromptTitle = computed(() => {
|
const starterPromptTitle = computed(() => {
|
||||||
if (currentRolePresentation.value.kind === 'specialist') return '你可以直接这样交给当前专员'
|
if (currentObjectPresentation.value.kind === 'specialist') return '你可以直接这样交给当前专员'
|
||||||
if (currentRolePresentation.value.kind === 'skill') return '你可以直接从这些技能入口开始'
|
if (currentObjectPresentation.value.kind === 'skill') return '你可以直接从这些技能入口开始'
|
||||||
return '不知道做什么,先从这些任务入口开始'
|
return '不知道做什么,先从这些任务入口开始'
|
||||||
})
|
})
|
||||||
const starterPromptMeta = computed(() => currentRolePresentation.value.label)
|
const starterPromptMeta = computed(() => currentObjectPresentation.value.label)
|
||||||
const visibleStarterPrompts = computed(() => {
|
const visibleStarterPrompts = computed(() => {
|
||||||
const prompts = currentRolePresentation.value.starterPrompts?.length
|
const prompts = currentObjectPresentation.value.starterPrompts?.length
|
||||||
? currentRolePresentation.value.starterPrompts
|
? currentObjectPresentation.value.starterPrompts
|
||||||
: defaultRolePresentation.starterPrompts
|
: defaultAssistantPresentation.starterPrompts
|
||||||
if (prompts.length <= 3) return prompts
|
if (prompts.length <= 3) return prompts
|
||||||
const start = starterPromptOffset.value % prompts.length
|
const start = starterPromptOffset.value % prompts.length
|
||||||
return Array.from({ length: 3 }, (_, index) => prompts[(start + index) % prompts.length])
|
return Array.from({ length: 3 }, (_, index) => prompts[(start + index) % prompts.length])
|
||||||
@@ -401,7 +405,7 @@ const placeholder = computed(
|
|||||||
)
|
)
|
||||||
|
|
||||||
function buildFallbackTaskPlan(skillKey) {
|
function buildFallbackTaskPlan(skillKey) {
|
||||||
const skill = availableSkills.find((item) => item.key === skillKey)
|
const skill = skillCatalog.getByKey(skillKey) || staticSkillCatalog.find((item) => item.key === skillKey)
|
||||||
if (!skill?.workflowSchema?.length) return null
|
if (!skill?.workflowSchema?.length) return null
|
||||||
return {
|
return {
|
||||||
title: `${skill.label}流程`,
|
title: `${skill.label}流程`,
|
||||||
@@ -471,7 +475,7 @@ async function send() {
|
|||||||
// 把当前任务和专员一起带上:后端据此解析出专员,注入岗位说明书,
|
// 把当前任务和专员一起带上:后端据此解析出专员,注入岗位说明书,
|
||||||
// 让这次回答以该专员的口吻说出来。不带的话后端只能退化成通用助手
|
// 让这次回答以该专员的口吻说出来。不带的话后端只能退化成通用助手
|
||||||
// —— 那正是「选谁说话都一样」的老毛病。
|
// —— 那正是「选谁说话都一样」的老毛病。
|
||||||
const res = await chatWithAssistant({
|
const res = await sendAssistantChatMessage({
|
||||||
message: text,
|
message: text,
|
||||||
mode: mode.value,
|
mode: mode.value,
|
||||||
ai_route_id: selectedAiChatRouteId.value,
|
ai_route_id: selectedAiChatRouteId.value,
|
||||||
@@ -550,7 +554,7 @@ async function handlePickFiles(files) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function rotateStarterPrompts() {
|
function rotateStarterPrompts() {
|
||||||
const prompts = currentRolePresentation.value.starterPrompts || []
|
const prompts = currentObjectPresentation.value.starterPrompts || []
|
||||||
if (prompts.length <= 3) return
|
if (prompts.length <= 3) return
|
||||||
starterPromptOffset.value = (starterPromptOffset.value + 1) % prompts.length
|
starterPromptOffset.value = (starterPromptOffset.value + 1) % prompts.length
|
||||||
}
|
}
|
||||||
@@ -596,23 +600,30 @@ function fillPractice(text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function applyRouteAppPreset() {
|
async function applyRouteAppPreset() {
|
||||||
|
const specialistKey = String(route.query.app_specialist || '').trim()
|
||||||
const skillKey = String(route.query.app_skill || '').trim()
|
const skillKey = String(route.query.app_skill || '').trim()
|
||||||
|
const presetPrompt = String(route.query.app_prompt || '').trim()
|
||||||
const pendingText = String(workbenchStore.pendingComposerText || '').trim()
|
const pendingText = String(workbenchStore.pendingComposerText || '').trim()
|
||||||
if (!skillKey && !pendingText) return
|
if (!specialistKey && !skillKey && !presetPrompt && !pendingText) return
|
||||||
if (skillKey) {
|
if (specialistKey || skillKey) {
|
||||||
workerRuntime.startNewTask()
|
workerRuntime.startNewTask()
|
||||||
|
if (specialistKey) {
|
||||||
|
await workerRuntime.attachSpecialistToCurrentTask(specialistKey)
|
||||||
|
}
|
||||||
|
if (skillKey) {
|
||||||
workerRuntime.attachSkillToCurrentTask(skillKey)
|
workerRuntime.attachSkillToCurrentTask(skillKey)
|
||||||
|
}
|
||||||
messages.value = []
|
messages.value = []
|
||||||
composerUploads.value = []
|
composerUploads.value = []
|
||||||
starterPromptOffset.value = 0
|
starterPromptOffset.value = 0
|
||||||
}
|
}
|
||||||
inputText.value = pendingText || String(route.query.app_prompt || '').trim()
|
inputText.value = pendingText || presetPrompt
|
||||||
if (pendingText) {
|
if (pendingText) {
|
||||||
workbenchStore.consumePendingComposerText()
|
workbenchStore.consumePendingComposerText()
|
||||||
}
|
}
|
||||||
await nextTick()
|
await nextTick()
|
||||||
inputBarRef.value?.focus()
|
inputBarRef.value?.focus()
|
||||||
if (skillKey) {
|
if (specialistKey || skillKey || presetPrompt) {
|
||||||
await router.replace({ path: '/home' })
|
await router.replace({ path: '/home' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -681,10 +692,17 @@ function dedupeUploads(items) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(currentRolePresentation, (presentation) => {
|
watch(currentObjectPresentation, () => {
|
||||||
starterPromptOffset.value = 0
|
starterPromptOffset.value = 0
|
||||||
})
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.app_specialist,
|
||||||
|
() => {
|
||||||
|
applyRouteAppPreset()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.query.app_skill,
|
() => route.query.app_skill,
|
||||||
() => {
|
() => {
|
||||||
@@ -692,6 +710,13 @@ watch(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => route.query.app_prompt,
|
||||||
|
() => {
|
||||||
|
applyRouteAppPreset()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => workbenchStore.pendingComposerText,
|
() => workbenchStore.pendingComposerText,
|
||||||
() => {
|
() => {
|
||||||
@@ -700,6 +725,10 @@ watch(
|
|||||||
)
|
)
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
skillCatalog.hydrate(),
|
||||||
|
specialistCatalog.hydrate(),
|
||||||
|
])
|
||||||
await applyRouteAppPreset()
|
await applyRouteAppPreset()
|
||||||
aiRouteStore.ensureAiChatRoutes()
|
aiRouteStore.ensureAiChatRoutes()
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user