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:
eaiadmin
2026-09-17 21:32:35 +08:00
co-authored by Claude Code
parent 8b136d4a10
commit 16d63de4e1
180 changed files with 22283 additions and 13850 deletions
@@ -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 {