refactor: 专员目录收敛为 6 个,重整 AI 路由与系统管理页
一次性提交当天全部改动(110 个文件)。 **未按 TOP_CODING_RULES.md G14.5「一次提交只装一件事」拆分** —— 用户明确要求 单一提交,此处如实记录,不静默忽略该冲突。 提交前验证:后端 go build / go vet / go test ./... 全绿,前端 npm run build exit 0,/api/health 与管理员 login 均返回 200。 - 专员目录收敛为 6 个:下线 knowledge-operations / process-coordination / presentation-briefing / report-generation 四个专员(前后端 manifest 与 seed 同步删除),新增 general-assistant。专员的归属关系(拥有哪些技能定义、 哪些目录项、提示词与绑定从哪来)改由 specialists/core 的 ownership.go、 prompt_provider.go、binding_provider.go 统一提供,seed 与 admin_handlers 随之内置化,卸载路径统一走 uninstall.go。 - 技能:补齐 text-to-speech 的前端 manifest(后端包在 HEAD 已存在), skillcore/ownership.go 提供与专员对称的归属查询。 - AI 路由:ai_config.json 由 OpenRouter/Ollama 切到 LMUAI / SiliconFlow / llama.cpp 本地路由,ai_secrets.example.json 与部署 env 样例同步新增 SILICONFLOW_API_KEY。 - 系统管理页重整:新增 AiAdminPage、OrganizationManagementPage,删除 AdminOverviewPage、CompanyConfigPage,SystemConfigPage 精简,nav / router / config/workbench.js 同步调整。依 G05.5,开发阶段直接收口到新结构,不留旧路由。 - 新增 internal/objectrefs:统一统计对象(专员 / 技能 / xapp)的运行时引用 (被多少 xapp、项目、任务引用),供管理页做删除前的影响面判断。 - 公众号创作专员:新增 OfficialAccountSpecialistPanel,workflow 与投递链路调整。 - XApp:考试 / 培训 Shell 扩展,XAppDirectoryPage 与 xappDefinition 同步。 - 文档:新增 GW01–GW05 工作台演进系列与 AR12 对话驱动与结构化交互架构; 同步 AR05 / SY17 / SY23 / SY25 / PL04;TOP_CODING_RULES.md 增补 G05.5。 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -66,9 +66,9 @@ func SeedDefaults() error {
|
||||
{"password_min_length", "6", "密码最小长度"},
|
||||
{"session_timeout_minutes", "480", "会话超时分钟数(不活动自动退出)"},
|
||||
// 7.2.4 AI 配置
|
||||
{"llm_base_url", "http://localhost:11434/v1", "LLM 服务地址"},
|
||||
{"llm_api_key", "sk-xxx", "LLM API Key(敏感信息,建议写入 ai_secrets.json)"},
|
||||
{"llm_model", "qwen2.5:7b", "LLM 模型名"},
|
||||
{"llm_base_url", "http://localhost:8080/v1", "LLM 服务地址"},
|
||||
{"llm_api_key", "local", "LLM API Key(本地服务可留占位值,远程服务建议写入 ai_secrets.json)"},
|
||||
{"llm_model", "Qwen3.5-4B-Q4_K_M.gguf", "LLM 模型名"},
|
||||
{"embed_model", "bge-m3", "Embedding 模型名"},
|
||||
{"llm_max_tokens", "2048", "LLM 最大输出 Token 数"},
|
||||
{"llm_temperature", "0.7", "LLM 温度参数(0.0~1.0)"},
|
||||
@@ -91,6 +91,18 @@ func SeedDefaults() error {
|
||||
if err := seedCoursesFromJSON(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedProductsFromJSON(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedCompanyIntroContent(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedQuestionsFromJSON(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedDefaultExamPapers(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := seedTrainingMedia(); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -135,6 +147,40 @@ type courseSeedFile struct {
|
||||
Data []courseSeedItem `json:"data"`
|
||||
}
|
||||
|
||||
type productSeedFile struct {
|
||||
Data []productSeedItem `json:"data"`
|
||||
}
|
||||
|
||||
type productSeedItem struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
Category string `json:"category"`
|
||||
Tags string `json:"tags"`
|
||||
Description string `json:"description"`
|
||||
Pricing string `json:"pricing"`
|
||||
CommissionRecommend string `json:"commission_recommend"`
|
||||
CommissionNegotiate string `json:"commission_negotiate"`
|
||||
PublicCourseBonus string `json:"public_course_bonus"`
|
||||
VersionRisk string `json:"version_risk"`
|
||||
ReportRules string `json:"report_rules"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type questionSeedFile struct {
|
||||
Data []questionSeedItem `json:"data"`
|
||||
}
|
||||
|
||||
type questionSeedItem struct {
|
||||
Domain string `json:"domain"`
|
||||
CourseID *uint `json:"course_id"`
|
||||
Type string `json:"type"`
|
||||
Stem string `json:"stem"`
|
||||
Options []model.QuestionOption `json:"options"`
|
||||
Answer []string `json:"answer"`
|
||||
Explanation string `json:"explanation"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
||||
type courseSeedItem struct {
|
||||
Code string `json:"code"`
|
||||
Name string `json:"name"`
|
||||
@@ -197,6 +243,172 @@ func seedCoursesFromJSON() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedProductsFromJSON() error {
|
||||
var count int64
|
||||
DB.Model(&model.Product{}).Count(&count)
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := config.Load()
|
||||
path := filepath.Join(cfg.TrainingMaterialsDir, "work", "products.json")
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var payload productSeedFile
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, item := range payload.Data {
|
||||
if item.Code == "" || item.Name == "" || item.Category == "" {
|
||||
continue
|
||||
}
|
||||
status := item.Status
|
||||
if strings.TrimSpace(status) == "" {
|
||||
status = "active"
|
||||
}
|
||||
product := model.Product{
|
||||
Code: item.Code,
|
||||
Name: item.Name,
|
||||
Category: item.Category,
|
||||
Tags: item.Tags,
|
||||
Description: item.Description,
|
||||
Pricing: item.Pricing,
|
||||
CommissionRecommend: item.CommissionRecommend,
|
||||
CommissionNegotiate: item.CommissionNegotiate,
|
||||
PublicCourseBonus: item.PublicCourseBonus,
|
||||
VersionRisk: item.VersionRisk,
|
||||
ReportRules: item.ReportRules,
|
||||
Status: status,
|
||||
}
|
||||
if err := DB.Create(&product).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("[OK] 产品知识种子已导入")
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedCompanyIntroContent() error {
|
||||
var cfg model.SystemConfig
|
||||
if err := DB.Where("config_key = ?", "company_intro").First(&cfg).Error; err != nil {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(cfg.ConfigValue) != "" {
|
||||
return nil
|
||||
}
|
||||
cfg.ConfigValue = strings.Join([]string{
|
||||
"博昇,聚焦企业增长与 AI 落地的综合服务商。",
|
||||
"",
|
||||
"我们围绕企业从资本运作到资质认定,再到 AI 转型的完整成长链路,提供四大板块服务:",
|
||||
"1. 资本咨询:股权规划、融资顾问、IPO 辅导,帮助企业走通资本路径。",
|
||||
"2. 资质认定辅导:高新技术企业、专精特新、专精特新小巨人的申报陪跑。",
|
||||
"3. AI 咨询与实施:AI 公开课、落地诊断、软件授权与定制开发。",
|
||||
"4. 企业级 AI 工具与平台:按行业匹配知识库、办公智能体、工业/医疗 AI、内容生成与电商运营方案。",
|
||||
"",
|
||||
"本培训平台面向新员工入职培训与内部持续学习,统一承接公司认知、产品知识、销售方法与考试验收。",
|
||||
}, "\n")
|
||||
return DB.Save(&cfg).Error
|
||||
}
|
||||
|
||||
func seedQuestionsFromJSON() error {
|
||||
var count int64
|
||||
DB.Model(&model.Question{}).Count(&count)
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg := config.Load()
|
||||
path := filepath.Join(cfg.TrainingMaterialsDir, "work", "questions.json")
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var payload questionSeedFile
|
||||
if err := json.Unmarshal(data, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, item := range payload.Data {
|
||||
if item.Domain == "" || item.Type == "" || item.Stem == "" || len(item.Answer) == 0 {
|
||||
continue
|
||||
}
|
||||
status := item.Status
|
||||
if strings.TrimSpace(status) == "" {
|
||||
status = "active"
|
||||
}
|
||||
question := model.Question{
|
||||
Domain: item.Domain,
|
||||
CourseID: item.CourseID,
|
||||
Type: item.Type,
|
||||
Stem: item.Stem,
|
||||
Options: mustJSON(item.Options),
|
||||
Answer: mustJSON(item.Answer),
|
||||
Explanation: item.Explanation,
|
||||
Status: status,
|
||||
}
|
||||
if err := DB.Create(&question).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("[OK] 考试题库种子已导入")
|
||||
return nil
|
||||
}
|
||||
|
||||
func seedDefaultExamPapers() error {
|
||||
var count int64
|
||||
DB.Model(&model.ExamPaper{}).Count(&count)
|
||||
if count > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
papers := []model.ExamPaper{
|
||||
{
|
||||
Name: "内部培训综合自测",
|
||||
Type: "self_test",
|
||||
Domain: "company,product,sales",
|
||||
QuestionCount: 20,
|
||||
TotalScore: 100,
|
||||
PassScore: 60,
|
||||
DurationMinutes: 30,
|
||||
Randomize: true,
|
||||
Status: "active",
|
||||
},
|
||||
{
|
||||
Name: "内部培训综合考试",
|
||||
Type: "formal",
|
||||
Domain: "company,product,sales",
|
||||
QuestionCount: 30,
|
||||
TotalScore: 100,
|
||||
PassScore: 80,
|
||||
DurationMinutes: 45,
|
||||
Randomize: true,
|
||||
Status: "active",
|
||||
},
|
||||
}
|
||||
for _, paper := range papers {
|
||||
if err := DB.Create(&paper).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
log.Println("[OK] 默认考试配置已导入")
|
||||
return nil
|
||||
}
|
||||
|
||||
type mediaSeed struct {
|
||||
filename string
|
||||
sourcePath string
|
||||
@@ -345,8 +557,8 @@ func seedSkillDefinitions() error {
|
||||
"opening_prompt": "直接告诉我目标,或者把材料贴给我,我来帮你起步。",
|
||||
"starter_prompts": []string{"帮我拆解这个任务,并列出下一步", "把这段产品介绍整理成正式文案", "先帮我判断这个问题该找哪个专员或技能"},
|
||||
}),
|
||||
State: "active",
|
||||
SortOrder: 10,
|
||||
State: "active",
|
||||
SortOrder: 10,
|
||||
},
|
||||
{
|
||||
Key: "document-translate",
|
||||
@@ -373,8 +585,8 @@ func seedSkillDefinitions() error {
|
||||
"opening_prompt": "把原文贴给我,并说明目标语言、受众和语气要求;如果有术语表,也一起给我。",
|
||||
"starter_prompts": []string{"把这段产品说明翻译成英文,保持专业语气", "把这封邮件翻译成日文,语气礼貌一点", "把这份中文文档整理成英文摘要"},
|
||||
}),
|
||||
State: "active",
|
||||
SortOrder: 20,
|
||||
State: "active",
|
||||
SortOrder: 20,
|
||||
},
|
||||
{
|
||||
Key: "copy-proofreading",
|
||||
@@ -401,8 +613,8 @@ func seedSkillDefinitions() error {
|
||||
"opening_prompt": "把文案贴给我,并告诉我这段内容要发给谁、希望保留什么语气。",
|
||||
"starter_prompts": []string{"检查这段销售话术里的错别字和语病", "把这段产品介绍润得更正式一点", "帮我校对这封对外邮件,保留原意"},
|
||||
}),
|
||||
State: "active",
|
||||
SortOrder: 30,
|
||||
State: "active",
|
||||
SortOrder: 30,
|
||||
},
|
||||
{
|
||||
Key: "audio-transcribe",
|
||||
@@ -429,8 +641,8 @@ func seedSkillDefinitions() error {
|
||||
"opening_prompt": "把音频内容发给我,或者告诉我语言、场景和你最终想拿到什么结果。",
|
||||
"starter_prompts": []string{"把这段会议录音转成文字,并分段整理", "先转写这段采访,再提取重点", "把语音内容整理成可发群里的纪要"},
|
||||
}),
|
||||
State: "active",
|
||||
SortOrder: 40,
|
||||
State: "active",
|
||||
SortOrder: 40,
|
||||
},
|
||||
{
|
||||
Key: "batch-extract",
|
||||
@@ -457,8 +669,8 @@ func seedSkillDefinitions() error {
|
||||
"opening_prompt": "告诉我想抽哪些字段、输出成什么格式、哪些情况需要标记为异常。",
|
||||
"starter_prompts": []string{"从这批简历里提取姓名、岗位和年限", "把这些合同里的付款条款统一抽出来", "帮我批量提取文章标题、作者和发布时间"},
|
||||
}),
|
||||
State: "active",
|
||||
SortOrder: 50,
|
||||
State: "active",
|
||||
SortOrder: 50,
|
||||
},
|
||||
{
|
||||
Key: "contract-review",
|
||||
@@ -485,8 +697,8 @@ func seedSkillDefinitions() error {
|
||||
"opening_prompt": "把合同发给我,并告诉我这次重点想看哪些条款,或者你站在哪一方立场。",
|
||||
"starter_prompts": []string{"审查这份采购合同的风险点", "重点看赔偿责任和终止条款", "帮我整理一版待人工确认的风险清单"},
|
||||
}),
|
||||
State: "active",
|
||||
SortOrder: 60,
|
||||
State: "active",
|
||||
SortOrder: 60,
|
||||
},
|
||||
{
|
||||
Key: "report-generation",
|
||||
@@ -513,8 +725,8 @@ func seedSkillDefinitions() error {
|
||||
"opening_prompt": "把材料给我,并告诉我报告类型、受众、周期和你最想突出哪些重点。",
|
||||
"starter_prompts": []string{"把今天的工作记录整理成日报", "根据这些节点生成项目周报", "帮我把这次交付过程整理成复盘说明"},
|
||||
}),
|
||||
State: "active",
|
||||
SortOrder: 70,
|
||||
State: "active",
|
||||
SortOrder: 70,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -894,9 +1106,9 @@ func seedXAppDefinitions() error {
|
||||
Color: "#4f46e5",
|
||||
IconText: "P",
|
||||
CoverTone: "linear-gradient(135deg, #4338ca 0%, #818cf8 100%)",
|
||||
Summary: "直接进入汇报 PPT 应用,默认挂上汇报专员,快速拿到页面结构、讲稿备注和演示骨架。",
|
||||
Description: "适合经营汇报、方案汇报、培训课件和项目复盘。打开就进入“汇报专员 + PPT 技能”的样板链路。",
|
||||
SpecialistKey: "presentation-briefing",
|
||||
Summary: "直接进入汇报 PPT 应用,快速拿到页面结构、讲稿备注和演示骨架。",
|
||||
Description: "适合经营汇报、方案汇报、培训课件和项目复盘。",
|
||||
SpecialistKey: "",
|
||||
SkillKey: "ppt-generation",
|
||||
DefaultPrompt: "帮我做一版 8 页管理层汇报 PPT",
|
||||
PromptsJSON: mustJSON([]string{"帮我做一版 8 页管理层汇报 PPT", "按汇报对象输出页面大纲和讲稿备注", "把这批材料压成一版培训课件骨架"}),
|
||||
|
||||
Reference in New Issue
Block a user