init: 数字员工平台初始代码

包含前端(Vue3 + VueFlow 画布)、后端(Go)、文档体系。
- 工作台画布:节点拖放、连线模式、右键菜单、AI 助手
- 后端:连接器 API、专员种子数据
- 导航:左侧导航、工坊、市场、控制台
This commit is contained in:
eaiadmin
2026-08-18 20:19:58 +08:00
commit 4e8817d768
239 changed files with 48631 additions and 0 deletions
@@ -0,0 +1,62 @@
package store
import (
"os"
"path/filepath"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"eaisalestrain/backend/internal/model"
)
// DB 全局数据库句柄
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 {
return nil, err
}
}
db, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{
Logger: logger.Default.LogMode(logger.Warn),
})
if err != nil {
return nil, err
}
if err := db.AutoMigrate(
&model.User{},
&model.Product{},
&model.Course{},
&model.MediaFile{},
&model.KnowledgeChunk{},
&model.Question{},
&model.ExamPaper{},
&model.ExamRecord{},
&model.SystemConfig{},
&model.KnowledgeSource{},
&model.AiCallLog{},
&model.LearningProgress{},
&model.Position{},
&model.PositionKnowledge{},
&model.PositionExamBlueprint{},
&model.MistakeRecord{},
&model.PointEvent{},
&model.Certificate{},
&model.Department{},
&model.Notification{},
&model.StudyNote{},
&model.Specialist{},
); err != nil {
return nil, err
}
DB = db
return db, nil
}
@@ -0,0 +1,649 @@
package store
import (
"encoding/json"
"log"
"os"
"path/filepath"
"strings"
"eaisalestrain/backend/internal/auth"
"eaisalestrain/backend/internal/config"
"eaisalestrain/backend/internal/model"
)
// SeedDefaults 插入默认管理员 + system_config 默认项(幂等,可重复执行)
func SeedDefaults() error {
// 默认管理员 admin / admin123(交付前 Clonezilla 清理时改密)
var c int64
DB.Model(&model.User{}).Where("username = ?", "admin").Count(&c)
if c == 0 {
hash, err := auth.HashPassword("admin123")
if err != nil {
return err
}
admin := model.User{
Username: "admin",
PasswordHash: hash,
FullName: "系统管理员",
Role: "admin",
Status: "active",
AiPoints: 999999, // 管理员算力点默认不限
}
if err := DB.Create(&admin).Error; err != nil {
return err
}
log.Println("[OK] 管理员账号已创建(admin / admin123)")
}
if err := ensureDemoEmployee(); err != nil {
return err
}
// 管理员算力点兜底:历史管理员(自动迁移补列后为默认 100)抬到 999999 不限
DB.Model(&model.User{}).Where("role = ? AND ai_points <= 100", "admin").Update("ai_points", 999999)
defaults := []struct{ key, value, desc string }{
// 7.2.1 公司信息配置
{"company_intro", "", "公司介绍培训页内容(HTML/文本)"},
{"company_name", "博昇", "公司名称"},
{"admin_email", "", "管理员联系邮箱"},
// 7.2.2 文件与存储
{"file_max_size_doc", "209715200", "文档最大字节(200MB)"},
{"file_max_size_video", "2147483648", "视频最大字节(2GB)"},
{"chunk_threshold", "104857600", "分片上传阈值(100MB)"},
// 7.2.3 其他信息配置
{"jwt_expire_minutes", "480", "Token 过期分钟数"},
{"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 模型名"},
{"embed_model", "bge-m3", "Embedding 模型名"},
{"llm_max_tokens", "2048", "LLM 最大输出 Token 数"},
{"llm_temperature", "0.7", "LLM 温度参数(0.0~1.0)"},
{"ai_points_default", "100", "新用户默认 AI 算力点"},
}
for _, d := range defaults {
var n int64
DB.Model(&model.SystemConfig{}).Where("config_key = ?", d.key).Count(&n)
if n == 0 {
if err := DB.Create(&model.SystemConfig{
ConfigKey: d.key,
ConfigValue: d.value,
Description: d.desc,
}).Error; err != nil {
return err
}
}
}
if err := seedCoursesFromJSON(); err != nil {
return err
}
if err := seedTrainingMedia(); err != nil {
return err
}
if err := seedSpecialists(); err != nil {
return err
}
return nil
}
func ensureDemoEmployee() error {
var c int64
DB.Model(&model.User{}).Where("username = ?", "emp_test01").Count(&c)
if c > 0 {
return nil
}
hash, err := auth.HashPassword("test123456")
if err != nil {
return err
}
employee := model.User{
Username: "emp_test01",
PasswordHash: hash,
FullName: "测试员工",
Role: "employee",
Status: "active",
AiPoints: 100,
}
if err := DB.Create(&employee).Error; err != nil {
return err
}
log.Println("[OK] 演示员工账号已创建(emp_test01 / test123456)")
return nil
}
type courseSeedFile struct {
Data []courseSeedItem `json:"data"`
}
type courseSeedItem struct {
Code string `json:"code"`
Name string `json:"name"`
Category string `json:"category"`
TargetCustomers string `json:"target_customers"`
ForbiddenCustomers string `json:"forbidden_customers"`
Scripts string `json:"scripts"`
SalesProcess string `json:"sales_process"`
ObjectionHandling string `json:"objection_handling"`
DeliveryPitfalls string `json:"delivery_pitfalls"`
ReportRules string `json:"report_rules"`
}
func seedCoursesFromJSON() error {
var count int64
DB.Model(&model.Course{}).Count(&count)
if count > 0 {
return nil
}
path := firstExistingPath(
"training_materials/work/courses.json",
"../training_materials/work/courses.json",
"eaisalestrain_app/training_materials/work/courses.json",
)
if path == "" {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
var payload courseSeedFile
if err := json.Unmarshal(data, &payload); err != nil {
return err
}
for _, item := range payload.Data {
if item.Code == "" || item.Name == "" || item.Category == "" {
continue
}
course := model.Course{
Code: item.Code,
Name: item.Name,
Category: item.Category,
TargetCustomers: item.TargetCustomers,
ForbiddenCustomers: item.ForbiddenCustomers,
Scripts: item.Scripts,
SalesProcess: item.SalesProcess,
ObjectionHandling: item.ObjectionHandling,
DeliveryPitfalls: item.DeliveryPitfalls,
ReportRules: item.ReportRules,
Status: "active",
}
if err := DB.Create(&course).Error; err != nil {
return err
}
}
log.Println("[OK] 销售课程种子已导入")
return nil
}
type mediaSeed struct {
filename string
sourcePath string
storedName string
bindType string
bindCode string
}
func seedTrainingMedia() error {
cfg := config.Load()
approvedDir := filepath.Join(cfg.KBDataDir, "approved")
if err := os.MkdirAll(approvedDir, 0o755); err != nil {
return err
}
var admin model.User
if err := DB.Where("username = ?", "admin").First(&admin).Error; err != nil {
return err
}
courseIDs := map[string]uint{}
var courses []model.Course
DB.Where("status = ?", "active").Find(&courses)
for _, course := range courses {
courseIDs[course.Code] = course.ID
}
seeds := []mediaSeed{
{
filename: "01_公司介绍.mp4",
sourcePath: firstExistingPath("training_materials/mp4/01_公司介绍.mp4", "../training_materials/mp4/01_公司介绍.mp4", "eaisalestrain_app/training_materials/mp4/01_公司介绍.mp4"),
storedName: "seed_company_intro_video.mp4",
bindType: "company",
},
{
filename: "01_公司介绍.pdf",
sourcePath: firstExistingPath("training_materials/pdf/01_公司介绍.pdf", "../training_materials/pdf/01_公司介绍.pdf", "eaisalestrain_app/training_materials/pdf/01_公司介绍.pdf"),
storedName: "seed_company_intro_manual.pdf",
bindType: "company",
},
{
filename: "05_资本咨询销售要点.mp4",
sourcePath: firstExistingPath("training_materials/mp4/05_资本咨询销售要点.mp4", "../training_materials/mp4/05_资本咨询销售要点.mp4", "eaisalestrain_app/training_materials/mp4/05_资本咨询销售要点.mp4"),
storedName: "seed_course_co001_video.mp4",
bindType: "course",
bindCode: "CO-001",
},
{
filename: "03_高企认定八大条件.mp4",
sourcePath: firstExistingPath("training_materials/mp4/03_高企认定八大条件.mp4", "../training_materials/mp4/03_高企认定八大条件.mp4", "eaisalestrain_app/training_materials/mp4/03_高企认定八大条件.mp4"),
storedName: "seed_course_co002_video.mp4",
bindType: "course",
bindCode: "CO-002",
},
{
filename: "06_企业AI落地方法论.mp4",
sourcePath: firstExistingPath("training_materials/mp4/06_企业AI落地方法论.mp4", "../training_materials/mp4/06_企业AI落地方法论.mp4", "eaisalestrain_app/training_materials/mp4/06_企业AI落地方法论.mp4"),
storedName: "seed_course_co003_video.mp4",
bindType: "course",
bindCode: "CO-003",
},
{
filename: "03_资本咨询类产品手册.pdf",
sourcePath: firstExistingPath("training_materials/pdf/03_资本咨询类产品手册.pdf", "../training_materials/pdf/03_资本咨询类产品手册.pdf", "eaisalestrain_app/training_materials/pdf/03_资本咨询类产品手册.pdf"),
storedName: "seed_course_co001_manual.pdf",
bindType: "course",
bindCode: "CO-001",
},
{
filename: "05_AI咨询类产品手册.pdf",
sourcePath: firstExistingPath("training_materials/pdf/05_AI咨询类产品手册.pdf", "../training_materials/pdf/05_AI咨询类产品手册.pdf", "eaisalestrain_app/training_materials/pdf/05_AI咨询类产品手册.pdf"),
storedName: "seed_course_co003_manual.pdf",
bindType: "course",
bindCode: "CO-003",
},
}
for _, item := range seeds {
if item.sourcePath == "" {
continue
}
var bindID *uint
if item.bindType == "course" {
id, ok := courseIDs[item.bindCode]
if !ok {
continue
}
bindID = &id
}
if err := ensureSeedMedia(admin.ID, approvedDir, item, bindID); err != nil {
return err
}
}
return nil
}
func seedSpecialists() error {
items := []model.Specialist{
{
Key: "training-delivery",
Label: "培训交付专员",
Tier: "generic",
WorkerType: "dw",
Route: "/apps/training-delivery",
Summary: "新人销售训练营第 3 周",
WorkStatus: "5 个进行中",
RiskLabel: "0 个异常",
Color: "#67c23a",
Stage: "交付归档",
Progress: 84,
MarketTag: "已安装",
Version: "v1.1",
ConnectorScope: "连接课程系统 / 知识库 / 考试模块",
PermissionScope: "班级资料读取、考试结果读取、交付包生成",
ResourceBindings: "课程系统、考试模块、知识库、交付模板中心",
InfoSources: "班级排期、学员成绩、互动记录、教练点评",
BaseSkills: "班级日报、考试预警、复盘归档、交付清单生成",
AIAssistance: "自动总结班级状态并给出补练建议",
GeneratedSkills: "基于优秀班级复盘沉淀新训练营模板",
State: "active",
SortOrder: 10,
},
{
Key: "knowledge-operations",
Label: "知识运营专员",
Tier: "generic",
WorkerType: "dw",
Route: "/apps/knowledge-operations",
Summary: "对象字典、证据索引与规则同步",
WorkStatus: "4 个待整理",
RiskLabel: "1 个映射待确认",
Color: "#8e6cf2",
Stage: "证据整理",
Progress: 63,
MarketTag: "已安装",
Version: "v1.0",
ConnectorScope: "连接知识资产 / 文档系统",
PermissionScope: "知识资产读取、规则发布建议、证据索引维护",
ResourceBindings: "知识库、文档系统、规则字典、对象映射表",
InfoSources: "对象字典、规则文档、证据片段、变更记录",
BaseSkills: "对象映射、证据抽取、规则同步、发布检查",
AIAssistance: "自动识别字段冲突并给出映射建议",
GeneratedSkills: "把高频清洗与映射动作沉淀为新规则技能",
State: "active",
SortOrder: 20,
},
{
Key: "process-coordination",
Label: "流程推进专员",
Tier: "generic",
WorkerType: "adw",
Route: "/apps/process-coordination",
Summary: "跨部门催办、checkpoint 跟进与阻塞升级",
WorkStatus: "6 个待跟进",
RiskLabel: "2 个阻塞",
Color: "#14b8a6",
Stage: "阻塞清理",
Progress: 57,
MarketTag: "可升级",
Version: "v1.0",
ConnectorScope: "连接审批流 / 企微 / 项目事项",
PermissionScope: "事项读写、催办通知、阻塞升级、审批触发",
ResourceBindings: "审批流、项目事项、企微、日程提醒",
InfoSources: "任务看板、流程 checkpoint、阻塞日志、责任人状态",
BaseSkills: "催办升级、checkpoint 跟进、阻塞清理、责任路由",
AIAssistance: "自动判断升级路径并规划下一步协同动作",
GeneratedSkills: "把成功的跨部门推进套路沉淀为自动推进技能",
State: "active",
SortOrder: 30,
},
{
Key: "report-generation",
Label: "报告生成专员",
Tier: "generic",
WorkerType: "dw",
Route: "/apps/report-generation",
Summary: "日报、周报、复盘和交付说明自动生成",
WorkStatus: "3 个待输出",
RiskLabel: "0 个异常",
Color: "#ec4899",
Stage: "报告汇总",
Progress: 76,
MarketTag: "可升级",
Version: "v1.0",
ConnectorScope: "连接交付物 / 知识库 / 模板中心",
PermissionScope: "交付物读取、模板套用、报告产出",
ResourceBindings: "交付模板、知识库、任务结果、证据引用",
InfoSources: "日报数据、周进度、复盘纪要、交付清单",
BaseSkills: "日报生成、周报汇总、复盘摘要、交付说明编排",
AIAssistance: "自动抽取重点变化并生成适配受众的版本",
GeneratedSkills: "把高质量报告结构沉淀为新模板技能",
State: "active",
SortOrder: 40,
},
{
Key: "contract-review",
Label: "合同审查专员",
Tier: "industry",
WorkerType: "adw",
Route: "/apps/contract-review",
Summary: "华东客户主协议修订",
WorkStatus: "2 个待确认",
RiskLabel: "1 个高风险",
Color: "#f56c6c",
Stage: "人工复核",
Progress: 72,
MarketTag: "已安装",
Version: "v1.4",
ConnectorScope: "连接 DMS / OA / 合同模板库",
PermissionScope: "合同文本读取、红线建议、审批流提交、例外升级",
ResourceBindings: "DMS、OA、模板库、法务规则库",
InfoSources: "合同正文、历史条款、谈判纪要、法务规则",
BaseSkills: "条款抽取、风险比对、红线生成、例外说明",
AIAssistance: "自动分析条款冲突并规划复核重点",
GeneratedSkills: "从人工复核结果中提炼新条款规则与红线模式",
State: "active",
SortOrder: 50,
},
{
Key: "solution-proposal",
Label: "售前方案专员",
Tier: "industry",
WorkerType: "adw",
Route: "/apps/solution-proposal",
Summary: "A 客户智能培训升级方案",
WorkStatus: "3 个待办",
RiskLabel: "1 个依赖阻塞",
Color: "#409eff",
Stage: "方案生成",
Progress: 58,
MarketTag: "可升级",
Version: "v1.2",
ConnectorScope: "连接 CRM / OA / 知识库",
PermissionScope: "商机读取、方案草案生成、评审流发起、范围确认",
ResourceBindings: "CRM、OA、知识库、方案模板中心",
InfoSources: "商机信息、调研纪要、行业方案、交付案例",
BaseSkills: "需求澄清、方案草案、连接器范围规划、风险边界说明",
AIAssistance: "自动组合客户上下文并生成多版本方案草案",
GeneratedSkills: "把高转化方案结构沉淀为可复用售前技能",
State: "active",
SortOrder: 60,
},
{
Key: "logistics-fulfillment",
Label: "履约跟单专员",
Tier: "industry",
WorkerType: "adw",
Route: "/apps/logistics-fulfillment",
Summary: "美西航线本周履约看板",
WorkStatus: "1 个异常",
RiskLabel: "2 个节点延迟",
Color: "#e6a23c",
Stage: "异常处置",
Progress: 49,
MarketTag: "试用",
Version: "v0.9",
ConnectorScope: "连接船司 / 邮件 / 企微",
PermissionScope: "节点读取、异常通知、履约状态写回、升级协同",
ResourceBindings: "船司系统、邮件、企微、履约看板",
InfoSources: "节点状态、异常邮件、订舱资料、客户承诺时间",
BaseSkills: "节点跟踪、异常识别、催办升级、状态回写",
AIAssistance: "自动判断异常影响范围并生成处置建议",
GeneratedSkills: "把高频异常处理流程沉淀为自动履约技能",
State: "active",
SortOrder: 70,
},
{
Key: "hr-email-sorter",
Label: "HR 邮件整理专员",
Tier: "industry",
WorkerType: "dw",
Route: "/apps/hr-email-sorter",
Summary: "识别并整理招聘邮箱中的简历邮件",
WorkStatus: "3 封待处理",
RiskLabel: "0 个异常",
Color: "#409eff",
Stage: "邮件处理",
Progress: 0,
MarketTag: "试用",
Version: "v1.0",
ConnectorScope: "连接 HR 招聘邮箱 / 邮件系统",
PermissionScope: "招聘邮箱读取、候选人表写入",
ResourceBindings: "HR 招聘邮箱、候选人管理表",
InfoSources: "HR 招聘邮箱收件箱",
BaseSkills: "简历邮件识别、候选人信息抽取、去重归类",
AIAssistance: "自动识别简历邮件并抽取候选人字段",
GeneratedSkills: "从历史招聘中沉淀简历筛选规则",
State: "active",
SortOrder: 80,
},
{
Key: "resume-processor",
Label: "简历处理专员",
Tier: "industry",
WorkerType: "dw",
Route: "/apps/resume-processor",
Summary: "筛选候选人并安排面试",
WorkStatus: "5 份待筛选",
RiskLabel: "1 个匹配待确认",
Color: "#67c23a",
Stage: "简历筛选",
Progress: 0,
MarketTag: "试用",
Version: "v1.0",
ConnectorScope: "连接候选人表 / 岗位需求表",
PermissionScope: "候选人表读写、面试安排表写入、岗位需求表读取",
ResourceBindings: "候选人表、岗位需求表、面试安排表",
InfoSources: "候选人表、岗位需求表",
BaseSkills: "简历筛选、评分排序、跟进标记、面试安排",
AIAssistance: "自动匹配岗位要求并给出排序建议",
GeneratedSkills: "从录用决策中沉淀筛选模型",
State: "active",
SortOrder: 90,
},
}
for _, item := range items {
EnsureSpecialistStructuredRecords(&item)
var existing model.Specialist
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.WorkerType == "" {
updates["worker_type"] = item.WorkerType
}
if existing.PermissionScope == "" {
updates["permission_scope"] = item.PermissionScope
}
if existing.ResourceBindings == "" {
updates["resource_bindings"] = item.ResourceBindings
}
if existing.InfoSources == "" {
updates["info_sources"] = item.InfoSources
}
if existing.BaseSkills == "" {
updates["base_skills"] = item.BaseSkills
}
if existing.AIAssistance == "" {
updates["ai_assistance"] = item.AIAssistance
}
if existing.GeneratedSkills == "" {
updates["generated_skills"] = item.GeneratedSkills
}
if existing.InputsRecordsJSON == "" {
updates["source_records_json"] = item.InputsRecordsJSON
}
if existing.PermissionRecordsJSON == "" {
updates["permission_records_json"] = item.PermissionRecordsJSON
}
if existing.ActionRecordsJSON == "" {
updates["action_records_json"] = item.ActionRecordsJSON
}
if existing.ResultRecordsJSON == "" {
updates["result_records_json"] = item.ResultRecordsJSON
}
if len(updates) > 0 {
if err := DB.Model(&existing).Updates(updates).Error; err != nil {
return err
}
}
}
log.Println("[OK] 专员目录种子已导入")
return nil
}
func ensureSeedMedia(adminID uint, approvedDir string, item mediaSeed, bindID *uint) error {
dst := filepath.Join(approvedDir, item.storedName)
if err := ensureMediaLink(item.sourcePath, dst); err != nil {
return err
}
query := DB.Where("stored_name = ? AND bind_type = ?", item.storedName, item.bindType)
if bindID == nil {
query = query.Where("bind_id IS NULL")
} else {
query = query.Where("bind_id = ?", *bindID)
}
var existing model.MediaFile
if err := query.First(&existing).Error; err == nil {
updates := map[string]any{
"filename": item.filename,
"stored_path": item.storedName,
"status": "approved",
"source": "admin",
"submitter_id": adminID,
"extracted": existing.FileExt != "pdf",
}
return DB.Model(&existing).Updates(updates).Error
}
info, err := os.Stat(item.sourcePath)
if err != nil {
return err
}
record := model.MediaFile{
Filename: item.filename,
StoredName: item.storedName,
StoredPath: item.storedName,
FileExt: strings.TrimPrefix(strings.ToLower(filepath.Ext(item.filename)), "."),
FileSize: info.Size(),
Status: "approved",
Source: "admin",
SubmitterID: adminID,
BindType: item.bindType,
BindID: bindID,
Extracted: strings.ToLower(filepath.Ext(item.filename)) != ".pdf",
}
return DB.Create(&record).Error
}
func ensureMediaLink(src, dst string) error {
absSrc, err := filepath.Abs(src)
if err != nil {
return err
}
if fi, err := os.Lstat(dst); err == nil {
if fi.Mode()&os.ModeSymlink != 0 {
target, err := os.Readlink(dst)
if err == nil {
if !filepath.IsAbs(target) {
target = filepath.Join(filepath.Dir(dst), target)
}
if absTarget, absErr := filepath.Abs(target); absErr == nil && absTarget == absSrc {
return nil
}
}
if err := os.Remove(dst); err != nil {
return err
}
} else if _, err := os.Stat(dst); err == nil {
return nil
} else if err := os.Remove(dst); err != nil {
return err
}
} else if !os.IsNotExist(err) {
return err
}
if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil {
return err
}
return os.Symlink(absSrc, dst)
}
func firstExistingPath(paths ...string) string {
for _, p := range paths {
if p == "" {
continue
}
if _, err := os.Stat(p); err == nil {
return p
}
}
return ""
}
@@ -0,0 +1,271 @@
package store
import (
"encoding/json"
"strings"
"eaisalestrain/backend/internal/model"
)
// EnsureSpecialistStructuredRecords 补齐四类结构化记录,供工作区 UI 直接消费。
func EnsureSpecialistStructuredRecords(item *model.Specialist) {
if item == nil {
return
}
if strings.TrimSpace(item.InputsRecordsJSON) == "" {
item.InputsRecordsJSON = mustJSON(buildInputsRecords(item))
}
if strings.TrimSpace(item.PermissionRecordsJSON) == "" {
item.PermissionRecordsJSON = mustJSON(buildPermissionRecords(item))
}
if strings.TrimSpace(item.ActionRecordsJSON) == "" {
item.ActionRecordsJSON = mustJSON(buildActionRecords(item))
}
if strings.TrimSpace(item.ResultRecordsJSON) == "" {
item.ResultRecordsJSON = mustJSON(buildResultRecords(item))
}
}
func ValidateStructuredRecordsJSON(value string) bool {
value = strings.TrimSpace(value)
if value == "" {
return true
}
var arr []map[string]any
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) {
records = append(records, map[string]any{
"input_name": value,
"input_type": "connector",
"connector_name": value,
"object_type": "system_scope",
"object_id": item.Key,
"fetched_at": "",
"last_sync_status": "ready",
"confidence": 1,
"citation": "来自连接范围定义",
})
}
for _, value := range splitText(item.InfoSources) {
records = append(records, map[string]any{
"input_name": value,
"input_type": "input",
"connector_name": "workspace_context",
"object_type": "context_input",
"object_id": item.Key,
"fetched_at": "",
"last_sync_status": "ready",
"confidence": 0.92,
"citation": "来自输入定义",
})
}
return records
}
func buildPermissionRecords(item *model.Specialist) []map[string]any {
approvalReason := "关键动作需要人工确认"
forbidden := []string{"自动发布", "批量删除", "跨系统迁移"}
if item.WorkerType == "adw" {
approvalReason = "中高风险动作进入审批或人工复核"
forbidden = []string{"无审批自动发布", "无审批跨系统写回", "高风险批量改写"}
}
return []map[string]any{
{
"title": "资源范围",
"resource_scope": item.PermissionScope,
"access_mode": "mixed",
"approval_required": false,
"approval_role": []string{},
"approval_reason": "",
"forbidden_actions": []string{},
"delegation_allowed": item.WorkerType == "adw",
},
{
"title": "访问边界",
"resource_scope": firstNonEmpty(item.ResourceBindings, item.ConnectorScope),
"access_mode": "read_write_scoped",
"approval_required": false,
"approval_role": []string{},
"approval_reason": "",
"forbidden_actions": []string{},
"delegation_allowed": item.WorkerType == "adw",
},
{
"title": "审批要求",
"resource_scope": item.PermissionScope,
"access_mode": "approval_guarded",
"approval_required": true,
"approval_role": []string{"管理员", "业务负责人"},
"approval_reason": approvalReason,
"forbidden_actions": []string{},
"delegation_allowed": item.WorkerType == "adw",
},
{
"title": "禁止动作",
"resource_scope": "",
"access_mode": "deny",
"approval_required": false,
"approval_role": []string{},
"approval_reason": "",
"forbidden_actions": forbidden,
"delegation_allowed": false,
},
}
}
func buildActionRecords(item *model.Specialist) []map[string]any {
records := make([]map[string]any, 0)
skills := splitText(item.BaseSkills)
if len(skills) == 0 {
skills = []string{"事项处理"}
}
outputs := splitText(item.GeneratedSkills)
infoSources := splitText(item.InfoSources)
for i, value := range skills {
records = append(records, map[string]any{
"action_name": value,
"action_type": inferActionType(value),
"trigger_mode": triggerMode(item.WorkerType),
"input_sources": infoSources,
"expected_output": outputs,
"risk_level": inferRiskLevel(value),
"status": defaultActionStatus(i),
"approval_state": approvalState(item.WorkerType, value),
"operator": strings.ToUpper(firstNonEmpty(item.WorkerType, "dw")),
"started_at": "",
"finished_at": "",
})
}
return records
}
func buildResultRecords(item *model.Specialist) []map[string]any {
results := make([]map[string]any, 0)
derived := splitText(item.BaseSkills)
for _, value := range splitText(item.GeneratedSkills) {
results = append(results, map[string]any{
"result_type": "skill",
"result_title": value,
"result_status": statusByMarketTag(item.MarketTag),
"derived_from_actions": derived,
"derived_from_sources": splitText(item.InfoSources),
"artifact_url": "",
"published_to": []string{"工作区"},
"confirmed_by": "",
"confirmed_at": "",
"version": firstNonEmpty(item.Version, "v1.0"),
})
}
if len(results) == 0 {
results = append(results, map[string]any{
"result_type": "summary",
"result_title": firstNonEmpty(item.WorkStatus, item.Summary, item.Label+"结果"),
"result_status": statusByMarketTag(item.MarketTag),
"derived_from_actions": derived,
"derived_from_sources": splitText(item.InfoSources),
"artifact_url": "",
"published_to": []string{"工作区"},
"confirmed_by": "",
"confirmed_at": "",
"version": firstNonEmpty(item.Version, "v1.0"),
})
}
return results
}
func mustJSON(v any) string {
data, err := json.Marshal(v)
if err != nil {
return "[]"
}
return string(data)
}
func splitText(value string) []string {
parts := strings.FieldsFunc(value, func(r rune) bool {
switch r {
case '、', ',', ',', '/', '|', ';', ';', '\n':
return true
default:
return false
}
})
items := make([]string, 0, len(parts))
for _, item := range parts {
item = strings.TrimSpace(item)
if item != "" {
items = append(items, item)
}
}
return items
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
value = strings.TrimSpace(value)
if value != "" {
return value
}
}
return ""
}
func inferActionType(value string) string {
switch {
case strings.Contains(value, "生成"), strings.Contains(value, "输出"), strings.Contains(value, "编排"):
return "generation"
case strings.Contains(value, "抽取"), strings.Contains(value, "识别"), strings.Contains(value, "比对"):
return "analysis"
case strings.Contains(value, "催办"), strings.Contains(value, "跟进"), strings.Contains(value, "升级"):
return "coordination"
default:
return "execution"
}
}
func triggerMode(workerType string) string {
if workerType == "adw" {
return "auto_assisted"
}
return "manual_assisted"
}
func inferRiskLevel(value string) string {
switch {
case strings.Contains(value, "升级"), strings.Contains(value, "写回"), strings.Contains(value, "审批"):
return "medium"
case strings.Contains(value, "删除"), strings.Contains(value, "发布"):
return "high"
default:
return "low"
}
}
func defaultActionStatus(index int) string {
if index == 0 {
return "ready"
}
return "pending"
}
func approvalState(workerType, value string) string {
if workerType == "adw" && (strings.Contains(value, "升级") || strings.Contains(value, "审批") || strings.Contains(value, "写回")) {
return "approval_required"
}
return "not_required"
}
func statusByMarketTag(tag string) string {
switch tag {
case "可升级":
return "ready_for_upgrade"
case "试用":
return "trial"
default:
return "active"
}
}