chore: 工作台产品化进行中的改动
把工作区里其余在制品一并入库,主要是工作台产品化的推进:
后端:新增 capability_definition / project / my_app_center / office_skill
接口与 action_definition / skill_definition / project / user_app_center
模型,config 加路由健康上报。
前端:新增 frontend/src/skills(Office 技能与 workbuddy 复刻)、
项目管理、应用中心、能力目录页,以及配套 api / store / config;
聊天侧新增 SpecialistChip / SpecialistPanel / SkillStrip / AppChatRail
等组件。
清理:移除旧 views/tools 下的单页工具(已并入工作台)、_frozen 冻结组件、
cmd/inspect_oa_debug 调试入口,以及两份调试笔记。
其它:文档与启动脚本同步。
(这批改动与上一提交的 SY23 工作并行进行,此前已在同一工作区内交织。)
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
@@ -15,7 +16,6 @@ import (
|
||||
var DB *gorm.DB
|
||||
|
||||
// Init 打开 SQLite(pure-Go 驱动,CGO_ENABLED=0 可静态编译)并 AutoMigrate 全部表。
|
||||
// 通过 GORM 方言抽象,未来切换 MySQL 仅需替换驱动与 DSN。
|
||||
func Init(dbPath string) (*gorm.DB, error) {
|
||||
if dir := filepath.Dir(dbPath); dir != "." && dir != "" {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
@@ -55,15 +55,149 @@ func Init(dbPath string) (*gorm.DB, error) {
|
||||
&model.Notification{},
|
||||
&model.StudyNote{},
|
||||
&model.Specialist{},
|
||||
&model.SkillDefinition{},
|
||||
&model.ActionDefinition{},
|
||||
&model.Project{},
|
||||
&model.WorkerTask{},
|
||||
&model.WorkerArtifact{},
|
||||
&model.WorkerRun{},
|
||||
&model.UserAppCenter{},
|
||||
&model.OfficialAccountArticle{},
|
||||
&model.OfficialAccountHotspot{},
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := migrateObjectEntryRouteColumns(db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
DB = db
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func migrateObjectEntryRouteColumns(db *gorm.DB) error {
|
||||
if db.Migrator().HasTable(&model.Specialist{}) {
|
||||
if db.Migrator().HasColumn(&model.Specialist{}, "object_entry_route") {
|
||||
specialistLegacyExpr := coalesceRouteExpr(
|
||||
db.Migrator().HasColumn(&model.Specialist{}, "entry_route"),
|
||||
"entry_route",
|
||||
db.Migrator().HasColumn(&model.Specialist{}, "route"),
|
||||
"route",
|
||||
)
|
||||
if specialistLegacyExpr != "" {
|
||||
if err := db.Exec(fmt.Sprintf(`
|
||||
UPDATE specialist
|
||||
SET object_entry_route = %s
|
||||
WHERE COALESCE(object_entry_route, '') = ''
|
||||
AND COALESCE(%s, '') <> ''
|
||||
`, specialistLegacyExpr, specialistLegacyExpr)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.Specialist{}, "entry_route") {
|
||||
if err := dropColumn(db, "specialist", "entry_route"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.Specialist{}, "route") {
|
||||
if err := dropColumn(db, "specialist", "route"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if db.Migrator().HasTable(&model.SkillDefinition{}) {
|
||||
if db.Migrator().HasColumn(&model.SkillDefinition{}, "object_entry_route") {
|
||||
skillLegacyExpr := coalesceRouteExpr(
|
||||
db.Migrator().HasColumn(&model.SkillDefinition{}, "entry_route"),
|
||||
"entry_route",
|
||||
db.Migrator().HasColumn(&model.SkillDefinition{}, "route"),
|
||||
"route",
|
||||
)
|
||||
if skillLegacyExpr != "" {
|
||||
if err := db.Exec(fmt.Sprintf(`
|
||||
UPDATE skill_definition
|
||||
SET object_entry_route = %s
|
||||
WHERE COALESCE(object_entry_route, '') = ''
|
||||
AND COALESCE(%s, '') <> ''
|
||||
`, skillLegacyExpr, skillLegacyExpr)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_object_entry_route") {
|
||||
skillLegacyObjectExpr := coalesceRouteExpr(
|
||||
db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_entry_route"),
|
||||
"legacy_entry_route",
|
||||
db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_route"),
|
||||
"legacy_route",
|
||||
)
|
||||
if skillLegacyObjectExpr != "" {
|
||||
if err := db.Exec(fmt.Sprintf(`
|
||||
UPDATE skill_definition
|
||||
SET legacy_object_entry_route = %s
|
||||
WHERE COALESCE(legacy_object_entry_route, '') = ''
|
||||
AND COALESCE(%s, '') <> ''
|
||||
`, skillLegacyObjectExpr, skillLegacyObjectExpr)).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.SkillDefinition{}, "entry_route") {
|
||||
if err := dropColumn(db, "skill_definition", "entry_route"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.SkillDefinition{}, "route") {
|
||||
if err := dropColumn(db, "skill_definition", "route"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_entry_route") {
|
||||
if err := dropColumn(db, "skill_definition", "legacy_entry_route"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_route") {
|
||||
if err := dropColumn(db, "skill_definition", "legacy_route"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func coalesceRouteExpr(primaryExists bool, primaryColumn string, fallbackExists bool, fallbackColumn string) string {
|
||||
parts := make([]string, 0, 2)
|
||||
if primaryExists {
|
||||
parts = append(parts, fmt.Sprintf("NULLIF(%s, '')", primaryColumn))
|
||||
}
|
||||
if fallbackExists {
|
||||
parts = append(parts, fallbackColumn)
|
||||
}
|
||||
if len(parts) == 0 {
|
||||
return ""
|
||||
}
|
||||
if len(parts) == 1 {
|
||||
return parts[0]
|
||||
}
|
||||
return fmt.Sprintf("COALESCE(%s)", joinCSV(parts))
|
||||
}
|
||||
|
||||
func joinCSV(parts []string) string {
|
||||
result := ""
|
||||
for i, part := range parts {
|
||||
if i > 0 {
|
||||
result += ", "
|
||||
}
|
||||
result += part
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func dropColumn(db *gorm.DB, tableName, columnName string) error {
|
||||
return db.Exec(fmt.Sprintf(`ALTER TABLE "%s" DROP COLUMN "%s"`, tableName, columnName)).Error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"eai_agentplatform/backend/internal/model"
|
||||
)
|
||||
|
||||
func openMigrationTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "migration.db")
|
||||
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestMigrateObjectEntryRouteColumnsDropsLegacyColumns(t *testing.T) {
|
||||
db := openMigrationTestDB(t)
|
||||
if err := db.AutoMigrate(&model.Specialist{}, &model.SkillDefinition{}); err != nil {
|
||||
t.Fatalf("auto migrate: %v", err)
|
||||
}
|
||||
|
||||
if err := db.Exec(`ALTER TABLE specialist ADD COLUMN entry_route TEXT DEFAULT ''`).Error; err != nil {
|
||||
t.Fatalf("add specialist.entry_route: %v", err)
|
||||
}
|
||||
if err := db.Exec(`ALTER TABLE specialist ADD COLUMN route TEXT DEFAULT ''`).Error; err != nil {
|
||||
t.Fatalf("add specialist.route: %v", err)
|
||||
}
|
||||
if err := db.Exec(`ALTER TABLE skill_definition ADD COLUMN entry_route TEXT DEFAULT ''`).Error; err != nil {
|
||||
t.Fatalf("add skill_definition.entry_route: %v", err)
|
||||
}
|
||||
if err := db.Exec(`ALTER TABLE skill_definition ADD COLUMN route TEXT DEFAULT ''`).Error; err != nil {
|
||||
t.Fatalf("add skill_definition.route: %v", err)
|
||||
}
|
||||
if err := db.Exec(`ALTER TABLE skill_definition ADD COLUMN legacy_entry_route TEXT DEFAULT ''`).Error; err != nil {
|
||||
t.Fatalf("add skill_definition.legacy_entry_route: %v", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
specialist := model.Specialist{
|
||||
Key: "spec-demo",
|
||||
Label: "专员演示",
|
||||
Tier: "generic",
|
||||
WorkerType: "dw",
|
||||
ObjectEntryRoute: "",
|
||||
State: "active",
|
||||
MarketTag: "installed",
|
||||
}
|
||||
if err := db.Create(&specialist).Error; err != nil {
|
||||
t.Fatalf("create specialist: %v", err)
|
||||
}
|
||||
if err := db.Exec(`UPDATE specialist SET entry_route = ?, route = ? WHERE id = ?`, "/legacy-specialist", "/legacy-specialist-route", specialist.ID).Error; err != nil {
|
||||
t.Fatalf("seed specialist legacy routes: %v", err)
|
||||
}
|
||||
|
||||
skill := model.SkillDefinition{
|
||||
Key: "skill-demo",
|
||||
Label: "技能演示",
|
||||
RoleKind: "skill",
|
||||
Source: "eai",
|
||||
ObjectEntryRoute: "",
|
||||
LegacyObjectEntryRoute: "",
|
||||
State: "active",
|
||||
}
|
||||
if err := db.Create(&skill).Error; err != nil {
|
||||
t.Fatalf("create skill: %v", err)
|
||||
}
|
||||
if err := db.Exec(`UPDATE skill_definition SET entry_route = ?, route = ?, legacy_entry_route = ?, legacy_route = ? WHERE id = ?`,
|
||||
"/legacy-skill", "/legacy-skill-route", "/legacy-old-skill", "/legacy-old-skill-route", skill.ID,
|
||||
).Error; err != nil {
|
||||
t.Fatalf("seed skill legacy routes: %v", err)
|
||||
}
|
||||
|
||||
if err := migrateObjectEntryRouteColumns(db); err != nil {
|
||||
t.Fatalf("migrate object entry route columns: %v", err)
|
||||
}
|
||||
|
||||
var migratedSpecialist model.Specialist
|
||||
if err := db.First(&migratedSpecialist, specialist.ID).Error; err != nil {
|
||||
t.Fatalf("reload specialist: %v", err)
|
||||
}
|
||||
if migratedSpecialist.ObjectEntryRoute != "/legacy-specialist" {
|
||||
t.Fatalf("specialist object_entry_route = %q, want %q", migratedSpecialist.ObjectEntryRoute, "/legacy-specialist")
|
||||
}
|
||||
|
||||
var migratedSkill model.SkillDefinition
|
||||
if err := db.First(&migratedSkill, skill.ID).Error; err != nil {
|
||||
t.Fatalf("reload skill: %v", err)
|
||||
}
|
||||
if migratedSkill.ObjectEntryRoute != "/legacy-skill" {
|
||||
t.Fatalf("skill object_entry_route = %q, want %q", migratedSkill.ObjectEntryRoute, "/legacy-skill")
|
||||
}
|
||||
if migratedSkill.LegacyObjectEntryRoute != "/legacy-old-skill" {
|
||||
t.Fatalf("skill legacy_object_entry_route = %q, want %q", migratedSkill.LegacyObjectEntryRoute, "/legacy-old-skill")
|
||||
}
|
||||
|
||||
if db.Migrator().HasColumn(&model.Specialist{}, "entry_route") {
|
||||
t.Fatalf("specialist.entry_route should be dropped")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.Specialist{}, "route") {
|
||||
t.Fatalf("specialist.route should be dropped")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.SkillDefinition{}, "entry_route") {
|
||||
t.Fatalf("skill_definition.entry_route should be dropped")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.SkillDefinition{}, "route") {
|
||||
t.Fatalf("skill_definition.route should be dropped")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_entry_route") {
|
||||
t.Fatalf("skill_definition.legacy_entry_route should be dropped")
|
||||
}
|
||||
if db.Migrator().HasColumn(&model.SkillDefinition{}, "legacy_route") {
|
||||
t.Fatalf("skill_definition.legacy_route should be dropped")
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ func EnsureSpecialistStructuredRecords(item *model.Specialist) {
|
||||
if item == nil {
|
||||
return
|
||||
}
|
||||
EnsureSpecialistRoleCard(item)
|
||||
if strings.TrimSpace(item.InputsRecordsJSON) == "" {
|
||||
item.InputsRecordsJSON = mustJSON(buildInputsRecords(item))
|
||||
}
|
||||
@@ -26,6 +27,25 @@ func EnsureSpecialistStructuredRecords(item *model.Specialist) {
|
||||
}
|
||||
}
|
||||
|
||||
func EnsureSpecialistRoleCard(item *model.Specialist) {
|
||||
if item == nil || strings.TrimSpace(item.RoleCardJSON) != "" {
|
||||
return
|
||||
}
|
||||
item.RoleCardJSON = mustJSON(map[string]any{
|
||||
"name": item.Label,
|
||||
"tagline": firstNonEmpty(item.Summary, item.WorkStatus, item.Label),
|
||||
"greeting": "我是" + item.Label + ",已经准备好接手这条任务并继续推进。",
|
||||
"relationship_to_user": "你的协作搭档",
|
||||
"tone": "专业、直接、可执行",
|
||||
"opening_prompt": "告诉我当前事项、目标和约束,我先帮你起步。",
|
||||
"starter_prompts": defaultStarterPrompts(item),
|
||||
"boundaries": []string{
|
||||
"高风险动作会明确标记待确认",
|
||||
"需要人工审批的步骤不会默认自动执行",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func ValidateStructuredRecordsJSON(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
@@ -35,6 +55,24 @@ func ValidateStructuredRecordsJSON(value string) bool {
|
||||
return json.Unmarshal([]byte(value), &arr) == nil
|
||||
}
|
||||
|
||||
func ValidateJSONObjectJSON(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return true
|
||||
}
|
||||
var obj map[string]any
|
||||
return json.Unmarshal([]byte(value), &obj) == nil
|
||||
}
|
||||
|
||||
func ValidateJSONStringArray(value string) bool {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
return true
|
||||
}
|
||||
var arr []string
|
||||
return json.Unmarshal([]byte(value), &arr) == nil
|
||||
}
|
||||
|
||||
func buildInputsRecords(item *model.Specialist) []map[string]any {
|
||||
records := make([]map[string]any, 0)
|
||||
for _, value := range splitText(item.ConnectorScope) {
|
||||
@@ -177,6 +215,23 @@ func buildResultRecords(item *model.Specialist) []map[string]any {
|
||||
return results
|
||||
}
|
||||
|
||||
func defaultStarterPrompts(item *model.Specialist) []string {
|
||||
prompts := make([]string, 0, 3)
|
||||
for _, skill := range splitText(item.BaseSkills) {
|
||||
prompts = append(prompts, "帮我处理:"+skill)
|
||||
if len(prompts) == 3 {
|
||||
return prompts
|
||||
}
|
||||
}
|
||||
if item.Summary != "" {
|
||||
prompts = append(prompts, "继续推进这条事项")
|
||||
}
|
||||
if len(prompts) == 0 {
|
||||
prompts = []string{"先帮我梳理当前事项", "告诉我下一步怎么做", "先看有哪些待确认点"}
|
||||
}
|
||||
return prompts
|
||||
}
|
||||
|
||||
func mustJSON(v any) string {
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user