refactor: 后端仓库层收口(A1:课程/产品/素材)+ 收进工作区既有对象化重构
本提交含两部分。第一部分是本轮工作;第二部分是此前一直留在工作区、
从未提交的对象化重构,与第一部分在文件上互相咬合(internal/repository
整个包都是未跟踪状态,且 api 层已有文件引用它),无法拆成两个可编译的提交。
一、仓库层收口 A1 批(本轮工作)
把 api 层手写的 store.DB 查询收进具名仓库方法,只给真正获益的对象做方法,
不机械包裹全量。本批迁移 22 处裸查询(courses.go 9 / media.go 12 / products.go 1),
新增方法:
- MediaFileRepo.ListByBind / ListForAudit / MarkExtracted
- KnowledgeChunkRepo.CountByMediaFile
- ProductRepo.GetVisibleByID
两条业务口径改由仓库单点持有,避免各处手写漂移:
「只有 approved 素材出现在课程详情」与「已停用产品不在课程详情露出」。
修掉两个真实缺陷:
- ProductRepo.GetByID 缺 Where 条件。此前 GET /api/products/{id} 对任意 id 都返回
第一条产品、对不存在的 id 返回 200,且 PUT /api/products/{id} 会覆盖第一条产品
—— 数据损坏级。全仓扫描确认这是唯一一处同型写法。
- ProductRepo.Delete 写 status="deleted",而 DELETE 处理器文档与回包都声称
"inactive",接口在说谎;管理员用 status=all 拉列表会看到前端不认识的状态。
已对齐为 inactive(与 CourseRepo.Delete 一致)。
删除 8 个零调用且列名不存在的死方法(一调即 SQL 报错):
- media_file 上的 file_path / file_type / approval_status 三列并不存在,
GetByPath / ListByType / UpdateStatus 全废
- knowledge_chunk 上的 space_id 列不存在(模型早已改为 knowledge_space_key),
List / Total / ListBySpaceIDs / DeleteBySpace / SearchByVector 全废
取舍边界:能对当前 schema 跑通的死方法保留,跑不通的删或修。
CourseRepo.List 补齐 status=all 档(此前传给它会当作 status='all' 过滤出空列表)。
该方法此前零调用,现与产品列表语义对齐。
验证:go build ./... 与 go test ./... 全绿;另用真实 HTTP 请求验证 34 项
(课程 17 / 产品 3 / 素材 14),跑在数据库副本与独立 KB_DATA_DIR 上,
含 multipart 真上传 → 审批 → pdftotext 提取 → 分片入库的完整链路。
二、此前未提交的对象化重构(非本轮工作)
- 新增 internal/repository 仓库层、connectors、skills、specialists、xapps、jsonutil,
model/task_record|task_run|task_artifact、api/task_runtime|action_definition|chat_message
- 删除 api/app_definition、connectors、my_app_center、notification、office_skill、
export_docx|pptx|xlsx、official_account_* 等,随 XApp/Skill/Specialist/Connector
可插拔打包方向(AR10/AR11)调整
- 资产目录归位:backend-go/knowledge_source → assets/knowledge/source、
training_materials → assets/training/materials;README 内相对路径同步加深两级;
deploy env 补 ASSET_ROOT_DIR 并改 KNOWLEDGE_SOURCE_DIR / TRAINING_MATERIALS_DIR
- 前端新增 skills/ specialists/ connectors/ xapps/ 目录与对应页面
验证:前端 npm run build 通过(7.26s)。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -4,23 +4,23 @@ import "time"
|
||||
|
||||
// ActionDefinition 内部原子执行动作定义。
|
||||
type ActionDefinition 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"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
ActionType string `gorm:"size:32;not null;default:execution;index" json:"action_type"`
|
||||
ConnectorRef string `gorm:"size:64;default:'';index" json:"connector_ref"`
|
||||
InputSchemaJSON string `gorm:"type:text" json:"input_schema_json"`
|
||||
OutputSchemaJSON string `gorm:"type:text" json:"output_schema_json"`
|
||||
RiskLevel string `gorm:"size:16;not null;default:low;index" json:"risk_level"`
|
||||
ApprovalMode string `gorm:"size:32;not null;default:not_required" json:"approval_mode"`
|
||||
AuditLevel string `gorm:"size:32;not null;default:standard" json:"audit_level"`
|
||||
ExposedToUser bool `gorm:"not null;default:false;index" json:"exposed_to_user"`
|
||||
OntologyBindingJSON string `gorm:"type:text" json:"ontology_binding_json"`
|
||||
State string `gorm:"size:16;not null;default:active;index" json:"state"`
|
||||
SortOrder int `gorm:"not null;default:0;index" json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
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"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
ActionType string `gorm:"size:32;not null;default:execution;index" json:"action_type"`
|
||||
ConnectorRef string `gorm:"size:64;default:'';index" json:"connector_ref"`
|
||||
InputSchemaJSON string `gorm:"column:input_schema_json;type:text" json:"input_schema_json"` // JSON → map[string]any
|
||||
OutputSchemaJSON string `gorm:"column:output_schema_json;type:text" json:"output_schema_json"` // JSON → map[string]any
|
||||
RiskLevel string `gorm:"size:16;not null;default:low;index" json:"risk_level"`
|
||||
ApprovalMode string `gorm:"size:32;not null;default:not_required" json:"approval_mode"`
|
||||
AuditLevel string `gorm:"size:32;not null;default:standard" json:"audit_level"`
|
||||
ExposedToUser bool `gorm:"not null;default:false;index" json:"exposed_to_user"`
|
||||
OntologyBindingJSON string `gorm:"type:text" json:"ontology_binding_json"`
|
||||
State string `gorm:"size:16;not null;default:active;index" json:"state"`
|
||||
SortOrder int `gorm:"not null;default:0;index" json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (ActionDefinition) TableName() string { return "action_definition" }
|
||||
|
||||
@@ -4,9 +4,9 @@ import "time"
|
||||
|
||||
// AiCallLog AI 调用日志(审计 + 按用户算力点计费)
|
||||
type AiCallLog struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
Capability string `gorm:"size:32;not null;index" json:"capability"` // ai_chat / text_gen / embed
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
UsageKind string `gorm:"column:usage_kind;size:32;not null;index" json:"usage_kind"` // ai_chat / text_gen / embed
|
||||
// SpecialistKey 本次调用以哪个专员的身份进行;空 = 通用助手。
|
||||
// 用来回答「这条回答是谁说的」,也是排查「专员没生效」的第一手线索。
|
||||
SpecialistKey string `gorm:"size:64;index" json:"specialist_key"`
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
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" }
|
||||
@@ -13,7 +13,7 @@ type ExamPaper struct {
|
||||
PassScore int `gorm:"not null" json:"pass_score"`
|
||||
DurationMinutes int `gorm:"not null" json:"duration_minutes"`
|
||||
Randomize bool `gorm:"not null" json:"randomize"`
|
||||
PositionID *uint `gorm:"index" json:"position_id"` // 关联岗位考试,可空;设置后按岗位知识映射抽题
|
||||
PositionID *uint `gorm:"index" json:"position_id"` // 关联岗位考试,可空;设置后按岗位知识映射抽题
|
||||
Status string `gorm:"size:16;not null;default:active;index" json:"status"` // active / inactive
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
|
||||
@@ -14,7 +14,7 @@ type ExamRecord struct {
|
||||
Passed bool `gorm:"not null;index" json:"passed"`
|
||||
CorrectCount int `gorm:"not null" json:"correct_count"`
|
||||
WrongCount int `gorm:"not null" json:"wrong_count"`
|
||||
DetailJSON string `gorm:"type:text;not null" json:"detail_json"` // 答题明细 JSON
|
||||
DetailJSON string `gorm:"column:detail_json;type:text;not null" json:"detail_json"` // 答题明细 JSON → map[string]any
|
||||
SubmittedAt time.Time `gorm:"not null;index" json:"submitted_at"`
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ type KnowledgeFAQ struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
KnowledgeSpaceKey string `gorm:"size:64;index" json:"knowledge_space_key"`
|
||||
Question string `gorm:"size:512;not null" json:"question"`
|
||||
Answer string `gorm:"type:text;not null" json:"answer"`
|
||||
SimilarQuestions string `gorm:"type:text" json:"similar_questions"`
|
||||
Answer string `gorm:"column:answer;type:text;not null" json:"answer"`
|
||||
SimilarQuestions string `gorm:"column:similar_questions;type:text" json:"similar_questions"` // JSON → []string
|
||||
Keywords string `gorm:"size:512" json:"keywords"`
|
||||
Status string `gorm:"size:16;not null;default:active;index" json:"status"`
|
||||
SortOrder int `gorm:"not null;default:0" json:"sort_order"`
|
||||
|
||||
@@ -6,18 +6,18 @@ import "time"
|
||||
// 按 (user_id, question_id, source) 去重:同一学员同一题同一种考试只保留一条,
|
||||
// 再次答错时更新作答与解析并将 resolved 重置为 false。
|
||||
type MistakeRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
QuestionID uint `gorm:"not null;index" json:"question_id"`
|
||||
Source string `gorm:"size:16;not null;index" json:"source"` // self_test / formal
|
||||
QuestionType string `gorm:"size:16;not null" json:"question_type"` // single/multiple/judge/essay(冗余,供错题本独立展示)
|
||||
QuestionStem string `gorm:"type:text;not null" json:"question_stem"`
|
||||
UserAnswer string `gorm:"type:text" json:"user_answer"` // JSON 序列化的作答
|
||||
CorrectAnswer string `gorm:"type:text" json:"correct_answer"` // JSON 序列化的正确答案 / 评分标准
|
||||
Explanation string `gorm:"type:text;default:''" json:"explanation"`
|
||||
Resolved bool `gorm:"not null;default:false;index" json:"resolved"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
QuestionID uint `gorm:"not null;index" json:"question_id"`
|
||||
Source string `gorm:"size:16;not null;index" json:"source"` // self_test / formal
|
||||
QuestionType string `gorm:"size:16;not null" json:"question_type"` // single/multiple/judge/essay
|
||||
QuestionStem string `gorm:"type:text;not null" json:"question_stem"`
|
||||
UserAnswer string `gorm:"column:user_answer;type:text" json:"user_answer"` // JSON 序列化 → 作答 (string, []string, bool)
|
||||
CorrectAnswer string `gorm:"column:correct_answer;type:text" json:"correct_answer"` // JSON 序列化 → 正确答案 / 评分标准 []string
|
||||
Explanation string `gorm:"type:text;default:''" json:"explanation"`
|
||||
Resolved bool `gorm:"not null;default:false;index" json:"resolved"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (MistakeRecord) TableName() string { return "mistake_record" }
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Notification 站内消息通知:考试发布 / 考试通过发证 / 岗位设置等事件推送给学员。
|
||||
type Notification struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;index" json:"user_id"`
|
||||
Type string `gorm:"size:32;not null;index" json:"type"` // exam_publish / exam_pass / position_set
|
||||
Title string `gorm:"size:128;not null" json:"title"`
|
||||
Content string `gorm:"type:text;not null" json:"content"`
|
||||
Link string `gorm:"size:128;default:''" json:"link"` // 前端跳转路由
|
||||
Read bool `gorm:"not null;default:false;index" json:"read"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
func (Notification) TableName() string { return "notification" }
|
||||
@@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
xappmodel "eai_agentplatform/backend/internal/xapps/model"
|
||||
)
|
||||
|
||||
// XAppDefinition / UserXAppCenter 是 Batch D 已合并进 xapps 域模型的类型,
|
||||
// store / seed 与迁移测试仍通过 model.XAppDefinition / model.UserXAppCenter 引用,
|
||||
// 因此这里保留这两个类型别名以兼容既有调用点。
|
||||
// 其余对象(专员、技能等)已按「一对象一目录」各自落位,不再经 model 层转出,
|
||||
// 调用方应直接 import 各对象域的 model 包。
|
||||
type XAppDefinition = xappmodel.XAppDefinition
|
||||
type UserXAppCenter = xappmodel.UserXAppCenter
|
||||
@@ -1,40 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// OfficialAccountArticle 公众号文章状态表,对齐工作流中的主题/标题/提纲/正文/配图/预览/分页显式传递。
|
||||
type OfficialAccountArticle struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
TaskID uint `gorm:"not null;uniqueIndex" json:"task_id"`
|
||||
SpecialistKey string `gorm:"size:64;not null;index" json:"specialist_key"`
|
||||
BusinessDomain string `gorm:"size:32;not null;default:ai;index" json:"business_domain"`
|
||||
Keyword string `gorm:"size:256;not null" json:"keyword"`
|
||||
Audience string `gorm:"size:128;not null" json:"audience"`
|
||||
Goal string `gorm:"size:128;not null" json:"goal"`
|
||||
Tone string `gorm:"size:128;not null" json:"tone"`
|
||||
Requirements string `gorm:"type:text" json:"requirements"`
|
||||
TopicCandidatesJSON string `gorm:"type:text" json:"topic_candidates_json"`
|
||||
HotspotsJSON string `gorm:"type:text" json:"hotspots_json"`
|
||||
SelectedTopic string `gorm:"size:256" json:"selected_topic"`
|
||||
TopicHeat string `gorm:"size:64" json:"topic_heat"`
|
||||
TitleCandidatesJSON string `gorm:"type:text" json:"title_candidates_json"`
|
||||
SelectedTitle string `gorm:"size:256" json:"selected_title"`
|
||||
OutlineStyle string `gorm:"size:64;not null;default:问题拆解型" json:"outline_style"`
|
||||
OutlineWords int `gorm:"not null;default:600" json:"outline_words"`
|
||||
Outline string `gorm:"type:text" json:"outline"`
|
||||
ContentTargetWords int `gorm:"not null;default:1400" json:"content_target_words"`
|
||||
MaxContentImages int `gorm:"not null;default:0" json:"max_content_images"`
|
||||
Content string `gorm:"type:text" json:"content"`
|
||||
ContentWithPrompts string `gorm:"type:text" json:"content_with_prompts"`
|
||||
ImagePromptsJSON string `gorm:"type:text" json:"image_prompts_json"`
|
||||
ImageRefsJSON string `gorm:"type:text" json:"image_refs_json"`
|
||||
PreviewMarkdown string `gorm:"type:text" json:"preview_markdown"`
|
||||
PreviewHTML string `gorm:"type:text" json:"preview_html"`
|
||||
PageMarkdown string `gorm:"type:text" json:"page_markdown"`
|
||||
PagesJSON string `gorm:"type:text" json:"pages_json"`
|
||||
Status string `gorm:"size:32;not null;default:draft;index" json:"status"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (OfficialAccountArticle) TableName() string { return "official_account_article" }
|
||||
@@ -1,25 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// OfficialAccountHotspot 业务域热点缓存表,承接 RSS/网页抓取结果及领域评分。
|
||||
type OfficialAccountHotspot struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
BusinessDomain string `gorm:"size:32;not null;default:ai;index" json:"business_domain"`
|
||||
SourceKey string `gorm:"size:64;not null;index;uniqueIndex:idx_hotspot_source_url" json:"source_key"`
|
||||
SourceLabel string `gorm:"size:128;not null" json:"source_label"`
|
||||
SourceType string `gorm:"size:16;not null" json:"source_type"`
|
||||
Title string `gorm:"size:512;not null" json:"title"`
|
||||
URL string `gorm:"size:1024;not null;uniqueIndex:idx_hotspot_source_url" json:"url"`
|
||||
Summary string `gorm:"type:text" json:"summary"`
|
||||
PublishedAt *time.Time `json:"published_at"`
|
||||
RawScore float64 `gorm:"not null;default:0" json:"raw_score"`
|
||||
DomainScore float64 `gorm:"not null;default:0" json:"domain_score"`
|
||||
HeatLabel string `gorm:"size:32;default:''" json:"heat_label"`
|
||||
TagsJSON string `gorm:"type:text" json:"tags_json"`
|
||||
FetchedAt time.Time `gorm:"index" json:"fetched_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (OfficialAccountHotspot) TableName() string { return "official_account_hotspot" }
|
||||
@@ -7,9 +7,9 @@ import "time"
|
||||
type PositionKnowledge struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
PositionID uint `gorm:"not null;index" json:"position_id"`
|
||||
Domain string `gorm:"size:16;not null;index" json:"domain"` // company / product / sales
|
||||
CourseID *uint `gorm:"index" json:"course_id"` // 绑定具体课程,可空
|
||||
ProductID *uint `gorm:"index" json:"product_id"` // 绑定具体产品,可空
|
||||
Domain string `gorm:"size:16;not null;index" json:"domain"` // company / product / sales
|
||||
CourseID *uint `gorm:"index" json:"course_id"` // 绑定具体课程,可空
|
||||
ProductID *uint `gorm:"index" json:"product_id"` // 绑定具体产品,可空
|
||||
RequiredLevel string `gorm:"size:16;not null;default:L1" json:"required_level"` // L1/L2/L3/L4
|
||||
Weight float64 `gorm:"not null;default:1" json:"weight"`
|
||||
IsMandatory bool `gorm:"not null;default:true" json:"is_mandatory"`
|
||||
|
||||
@@ -6,7 +6,7 @@ import "time"
|
||||
//
|
||||
// 任务记「做的是哪件事」,项目记「这些事是为谁、按什么规矩做的」——
|
||||
// 指令是给 AI 的长期上下文,预置的专员/技能/连接器是这个项目默认带上的能力。
|
||||
// 归属跟 WorkerTask.Owner 同构(全名优先,回退登录名),列表只列自己的。
|
||||
// 归属跟 TaskRecord.Owner 同构(全名优先,回退登录名),列表只列自己的。
|
||||
type Project struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:40;not null" json:"name"`
|
||||
@@ -14,11 +14,11 @@ type Project struct {
|
||||
TemplateKey string `gorm:"size:64;default:''" json:"template_key"`
|
||||
Owner string `gorm:"size:64;not null;index" json:"owner"`
|
||||
Pinned bool `gorm:"not null;default:false" json:"pinned"`
|
||||
// 建项目时选中的能力,存 JSON 数组字符串。key 各自指向 specialist.key /
|
||||
// skill.key / connector.key —— 只存 key,展示时再去取名字。
|
||||
SpecialistKeys string `gorm:"type:text" json:"specialist_keys"`
|
||||
SkillKeys string `gorm:"column:skill_keys;type:text" json:"skill_keys"`
|
||||
ConnectorKeys string `gorm:"type:text" json:"connector_keys"`
|
||||
// 建项目时选中的能力,存 JSON 数组字符串。key 各自指向 specialist.key /
|
||||
// skill.key / connector.key —— 只存 key,展示时再去取名字。
|
||||
SpecialistKeys string `gorm:"type:text" json:"specialist_keys"`
|
||||
SkillKeys string `gorm:"column:skill_keys;type:text" json:"skill_keys"`
|
||||
ConnectorKeys string `gorm:"type:text" json:"connector_keys"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,12 @@ package model
|
||||
|
||||
import "time"
|
||||
|
||||
// QuestionOption 题目选项
|
||||
type QuestionOption struct {
|
||||
Key string `json:"key"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
// Question 题目表
|
||||
type Question struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
@@ -9,8 +15,8 @@ type Question struct {
|
||||
CourseID *uint `gorm:"index" json:"course_id"`
|
||||
Type string `gorm:"size:16;not null" json:"type"` // single / multiple / judge
|
||||
Stem string `gorm:"type:text;not null" json:"stem"`
|
||||
Options string `gorm:"type:text;not null" json:"options"` // JSON 数组:["A","B","C"]
|
||||
Answer string `gorm:"type:text;not null" json:"answer"` // JSON 数组:single=[索引] multiple=[索引...] judge=[bool]
|
||||
Options string `gorm:"column:options;type:text;not null" json:"options"`
|
||||
Answer string `gorm:"column:answer;type:text;not null" json:"answer"`
|
||||
Explanation string `gorm:"type:text;default:''" json:"explanation"`
|
||||
Status string `gorm:"size:16;not null;default:active;index" json:"status"` // active / inactive
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// SkillDefinition 对外暴露的任务级能力定义。
|
||||
type SkillDefinition 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"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
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
|
||||
ObjectEntryRoute string `gorm:"column:object_entry_route;size:128;default:''" json:"object_entry_route"`
|
||||
ExposedToUser bool `gorm:"not null;default:true;index" json:"exposed_to_user"`
|
||||
StarterPromptsJSON string `gorm:"type:text" json:"starter_prompts_json"`
|
||||
PromptTemplate string `gorm:"type:text" json:"prompt_template"`
|
||||
InputSchemaJSON string `gorm:"type:text" json:"input_schema_json"`
|
||||
OutputSchemaJSON string `gorm:"type:text" json:"output_schema_json"`
|
||||
ArtifactSchemaJSON string `gorm:"type:text" json:"artifact_schema_json"`
|
||||
ActionRefsJSON string `gorm:"type:text" json:"action_refs_json"`
|
||||
PolicyRefsJSON string `gorm:"type:text" json:"policy_refs_json"`
|
||||
OntologyBindingJSON string `gorm:"type:text" json:"ontology_binding_json"`
|
||||
State string `gorm:"size:16;not null;default:active;index" json:"state"`
|
||||
SortOrder int `gorm:"not null;default:0;index" json:"sort_order"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (SkillDefinition) TableName() string { return "skill_definition" }
|
||||
@@ -1,104 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ValidSkillKeys 技能 key 全集,用于校验 Specialist.AllowedSkills。
|
||||
//
|
||||
// 权威来源是前端 frontend/src/config/workbench.js 的 staticSkillCatalog ——
|
||||
// 技能实现仍归前端,后端只持有 key 清单,用于录入时挡住拼写错误。
|
||||
// 两个清单的一致性由 skill_keys_test.go 守着,改动任一侧都会让测试失败。
|
||||
//
|
||||
// 背景:前端 22 个技能 vs 后端 skill_definition 7 行,长期无同步机制;
|
||||
// 本清单是 SY23 §2.4 的「单向校验」兜底,不解决同步问题本身。
|
||||
var ValidSkillKeys = map[string]bool{
|
||||
"smart-assistant": true, // 通用助手(默认挂载)
|
||||
"document-translate": true,
|
||||
"copy-proofreading": true,
|
||||
"audio-transcribe": true,
|
||||
"batch-extract": true,
|
||||
"contract-review": true, // 注意:与专员 key contract-review 同名,日志里请带类型前缀
|
||||
"report-generation": true, // 同上,与专员 key report-generation 同名
|
||||
"ppt-generation": true,
|
||||
"mind-map": true,
|
||||
"longform-writing": true,
|
||||
"text-toolkit": true,
|
||||
"ocr-understanding": true,
|
||||
"meeting-minutes": true,
|
||||
"project-planning": true,
|
||||
"email-drafting": true,
|
||||
"table-cleanup": true,
|
||||
"proposal-summary": true,
|
||||
"progress-report": true,
|
||||
"contract-brief": true,
|
||||
"interview-summary": true,
|
||||
"policy-rewrite": true,
|
||||
"survey-summary": true,
|
||||
}
|
||||
|
||||
// SortedValidSkillKeys 返回排序后的 key 清单,用于报错文案与文档生成
|
||||
func SortedValidSkillKeys() []string {
|
||||
keys := make([]string, 0, len(ValidSkillKeys))
|
||||
for k := range ValidSkillKeys {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
return keys
|
||||
}
|
||||
|
||||
// ParseAllowedSkills 解析 AllowedSkills JSON 数组。
|
||||
// 空串、非法 JSON 一律返回 nil —— 读路径不报错,拿不到绑定就退回通用助手行为。
|
||||
func ParseAllowedSkills(raw string) []string {
|
||||
trimmed := strings.TrimSpace(raw)
|
||||
if trimmed == "" {
|
||||
return nil
|
||||
}
|
||||
var keys []string
|
||||
if err := json.Unmarshal([]byte(trimmed), &keys); err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(keys))
|
||||
for _, k := range keys {
|
||||
if k = strings.TrimSpace(k); k != "" {
|
||||
out = append(out, k)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// MarshalAllowedSkills 把 key 列表序列化为 AllowedSkills 的存储形式。
|
||||
// 空列表存空串(而不是 "[]"),与「未绑定」保持同一种表示。
|
||||
func MarshalAllowedSkills(keys []string) (string, error) {
|
||||
if len(keys) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
raw, err := json.Marshal(keys)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(raw), nil
|
||||
}
|
||||
|
||||
// ValidateAllowedSkills 校验 key 列表,返回第一个非法 key 的错误。
|
||||
// 同时挡掉重复项——重复会让「首个为主技能」的语义变得不确定。
|
||||
func ValidateAllowedSkills(keys []string) error {
|
||||
seen := make(map[string]bool, len(keys))
|
||||
for _, k := range keys {
|
||||
k = strings.TrimSpace(k)
|
||||
if k == "" {
|
||||
continue
|
||||
}
|
||||
if !ValidSkillKeys[k] {
|
||||
return fmt.Errorf("技能 key %q 非法,可选值:%s", k, strings.Join(SortedValidSkillKeys(), " / "))
|
||||
}
|
||||
if seen[k] {
|
||||
return fmt.Errorf("技能 key %q 重复绑定", k)
|
||||
}
|
||||
seen[k] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,152 +0,0 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// workbenchJSRelPath 前端技能目录的相对路径(相对本包目录)。
|
||||
const workbenchJSRelPath = "../../../frontend/src/config/workbench.js"
|
||||
|
||||
// createSkillDefinitionRe 抓 staticSkillCatalog 数组里每个 createSkillDefinition 的字面量 key。
|
||||
// 依赖 workbench.js 里「createSkillDefinition({ 紧跟 key: '…'」这一写法。
|
||||
var createSkillDefinitionRe = regexp.MustCompile(`createSkillDefinition\(\{\s*key:\s*'([^']+)'`)
|
||||
|
||||
// TestValidSkillKeysMatchFrontend 守住 backend 的 ValidSkillKeys 与前端 staticSkillCatalog 一致。
|
||||
//
|
||||
// 这两份清单天然会漂移:技能实现在前端,后端只持有 key 做录入校验。任何一侧增删技能
|
||||
// 而忘了另一侧,这里就会红——这正是本测试存在的唯一理由。
|
||||
//
|
||||
// 交付形态是没有前端源码的(单二进制 + 整盘克隆),找不到文件时跳过而不是失败。
|
||||
func TestValidSkillKeysMatchFrontend(t *testing.T) {
|
||||
raw, err := os.ReadFile(filepath.Clean(workbenchJSRelPath))
|
||||
if err != nil {
|
||||
t.Skipf("跳过:读不到前端 %s(交付形态无前端源码): %v", workbenchJSRelPath, err)
|
||||
}
|
||||
|
||||
matches := createSkillDefinitionRe.FindAllStringSubmatch(string(raw), -1)
|
||||
if len(matches) == 0 {
|
||||
t.Fatalf("在前端 %s 里没抓到任何 createSkillDefinition key —— "+
|
||||
"可能是正则失效(写法改了),请同步更新 createSkillDefinitionRe", workbenchJSRelPath)
|
||||
}
|
||||
|
||||
frontendKeys := make(map[string]bool, len(matches))
|
||||
for _, m := range matches {
|
||||
frontendKeys[m[1]] = true
|
||||
}
|
||||
|
||||
var onlyBackend, onlyFrontend []string
|
||||
for k := range ValidSkillKeys {
|
||||
if !frontendKeys[k] {
|
||||
onlyBackend = append(onlyBackend, k)
|
||||
}
|
||||
}
|
||||
for k := range frontendKeys {
|
||||
if !ValidSkillKeys[k] {
|
||||
onlyFrontend = append(onlyFrontend, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(onlyBackend)
|
||||
sort.Strings(onlyFrontend)
|
||||
|
||||
if len(onlyBackend) > 0 {
|
||||
t.Errorf("ValidSkillKeys 里有、前端 staticSkillCatalog 里没有的 key(后端多写或前端已删):\n %s",
|
||||
strings.Join(onlyBackend, "\n "))
|
||||
}
|
||||
if len(onlyFrontend) > 0 {
|
||||
t.Errorf("前端 staticSkillCatalog 里有、ValidSkillKeys 里没有的 key(新增技能后忘了补后端清单):\n %s",
|
||||
strings.Join(onlyFrontend, "\n "))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseAllowedSkills(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
raw string
|
||||
want []string
|
||||
}{
|
||||
{"空串返回 nil", "", nil},
|
||||
{"纯空白返回 nil", " ", nil},
|
||||
{"非法 JSON 返回 nil(读路径不报错)", `{"not":"array"}`, nil},
|
||||
{"正常数组", `["contract-review","batch-extract"]`, []string{"contract-review", "batch-extract"}},
|
||||
{"跳过多余空白", `[" contract-review "]`, []string{"contract-review"}},
|
||||
{"空数组返回空切片", `[]`, []string{}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := ParseAllowedSkills(tc.raw)
|
||||
if len(got) != len(tc.want) {
|
||||
t.Fatalf("ParseAllowedSkills(%q) = %v,期望 %v", tc.raw, got, tc.want)
|
||||
}
|
||||
for i := range got {
|
||||
if got[i] != tc.want[i] {
|
||||
t.Fatalf("ParseAllowedSkills(%q) = %v,期望 %v", tc.raw, got, tc.want)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalAllowedSkills(t *testing.T) {
|
||||
t.Run("空列表存空串而非 []", func(t *testing.T) {
|
||||
got, err := MarshalAllowedSkills(nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got != "" {
|
||||
t.Errorf("期望空串,实际 %q", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("往返一致", func(t *testing.T) {
|
||||
in := []string{"contract-review", "contract-brief"}
|
||||
raw, err := MarshalAllowedSkills(in)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := ParseAllowedSkills(raw)
|
||||
if len(out) != len(in) {
|
||||
t.Fatalf("往返后长度变了: %v -> %v", in, out)
|
||||
}
|
||||
for i := range in {
|
||||
if in[i] != out[i] {
|
||||
t.Errorf("往返后顺序变了: %v -> %v(顺序即优先级,必须保持)", in, out)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateAllowedSkills(t *testing.T) {
|
||||
t.Run("合法列表通过", func(t *testing.T) {
|
||||
if err := ValidateAllowedSkills([]string{"contract-review", "batch-extract"}); err != nil {
|
||||
t.Errorf("不应报错: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("空列表通过", func(t *testing.T) {
|
||||
if err := ValidateAllowedSkills(nil); err != nil {
|
||||
t.Errorf("未绑定技能应合法: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("非法 key 报错", func(t *testing.T) {
|
||||
err := ValidateAllowedSkills([]string{"contract-review", "typo-skill"})
|
||||
if err == nil {
|
||||
t.Fatal("非法 key 应报错")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "typo-skill") {
|
||||
t.Errorf("错误信息应点出非法 key,实际: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("重复 key 报错", func(t *testing.T) {
|
||||
err := ValidateAllowedSkills([]string{"contract-review", "contract-review"})
|
||||
if err == nil {
|
||||
t.Fatal("重复绑定应报错(首个为主技能的语义会变得不确定)")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Specialist 数字员工专员目录
|
||||
type Specialist 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"`
|
||||
Tier string `gorm:"size:16;not null;index" json:"tier"` // generic / industry
|
||||
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"`
|
||||
Summary string `gorm:"type:text" json:"summary"`
|
||||
InteractionCardJSON string `gorm:"column:interaction_card_json;type:text" json:"interaction_card_json"`
|
||||
WorkStatus string `gorm:"size:64;default:''" json:"work_status"`
|
||||
RiskLabel string `gorm:"size:64;default:''" json:"risk_label"`
|
||||
Color string `gorm:"size:16;default:''" json:"color"`
|
||||
Stage string `gorm:"size:64;default:''" json:"stage"`
|
||||
Progress int `gorm:"not null;default:0" json:"progress"`
|
||||
MarketTag string `gorm:"size:32;not null;default:installed;index" json:"market_tag"` // 已安装 / 可升级 / 试用
|
||||
Version string `gorm:"size:32;default:''" json:"version"`
|
||||
ConnectorScope string `gorm:"type:text" json:"connector_scope"`
|
||||
PermissionScope string `gorm:"type:text" json:"permission_scope"`
|
||||
ResourceBindings string `gorm:"type:text" json:"resource_bindings"`
|
||||
InfoSources string `gorm:"type:text" json:"info_sources"`
|
||||
BaseSkills string `gorm:"type:text" json:"base_skills"`
|
||||
AIAssistance string `gorm:"type:text" json:"ai_assistance"`
|
||||
GeneratedSkills string `gorm:"type:text" json:"generated_skills"`
|
||||
// RuleFileMarkdown 岗位说明书正文(Markdown 全文)。
|
||||
// 会话创建时注入 System Prompt,决定这个专员怎么说话、怎么推进、什么时候调用哪个技能。
|
||||
// 参考 AionCore builtin-assistants 的 rule_file;命名对齐 SY23。
|
||||
RuleFileMarkdown string `gorm:"type:text" json:"rule_file_markdown"`
|
||||
// AllowedSkills 绑定技能 key 列表,JSON 字符串数组,顺序即优先级(首个为主技能)。
|
||||
// 元素取值须通过 model.ValidSkillKeys 校验,对齐前端 staticSkillCatalog[].key。
|
||||
// 字段名对齐 SY22 §6.1 对象 manifest 的 allowed_skills。
|
||||
AllowedSkills string `gorm:"type:text" json:"allowed_skills"`
|
||||
InputsRecordsJSON string `gorm:"column:inputs_records_json;type:text" json:"inputs_records_json"`
|
||||
PermissionRecordsJSON string `gorm:"type:text" json:"permission_records_json"`
|
||||
ActionRecordsJSON string `gorm:"type:text" json:"action_records_json"`
|
||||
ResultRecordsJSON string `gorm:"type:text" json:"result_records_json"`
|
||||
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 (Specialist) TableName() string { return "specialist" }
|
||||
+6
-6
@@ -2,20 +2,20 @@ package model
|
||||
|
||||
import "time"
|
||||
|
||||
// WorkerArtifact 专员交付物
|
||||
type WorkerArtifact struct {
|
||||
// TaskArtifact 专员交付物
|
||||
type TaskArtifact struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
TaskID uint `gorm:"not null;index" json:"task_id"`
|
||||
SpecialistKey string `gorm:"size:64;not null;index" json:"specialist_key"`
|
||||
Title string `gorm:"size:128;not null" json:"title"`
|
||||
ArtifactType string `gorm:"size:32;not null;default:text" json:"artifact_type"`
|
||||
Status string `gorm:"size:32;not null;default:draft;index" json:"status"`
|
||||
ContentText string `gorm:"type:text" json:"content_text"`
|
||||
ContentJSON string `gorm:"type:text" json:"content_json"`
|
||||
SourceRefsJSON string `gorm:"type:text" json:"source_refs_json"`
|
||||
ContentText string `gorm:"column:content_text;type:text" json:"content_text"`
|
||||
ContentJSON string `gorm:"column:content_json;type:text" json:"content_json"` // JSON → map[string]any
|
||||
SourceRefsJSON string `gorm:"column:source_refs_json;type:text" json:"source_refs_json"` // JSON → []SourceRef
|
||||
CreatedByRunID *uint `gorm:"index" json:"created_by_run_id"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (WorkerArtifact) TableName() string { return "worker_artifact" }
|
||||
func (TaskArtifact) TableName() string { return "task_artifact" }
|
||||
+6
-6
@@ -2,18 +2,18 @@ package model
|
||||
|
||||
import "time"
|
||||
|
||||
// WorkerTask 专员事项/任务
|
||||
type WorkerTask struct {
|
||||
// TaskRecord 专员事项/任务
|
||||
type TaskRecord struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
SpecialistKey string `gorm:"size:64;not null;index" json:"specialist_key"`
|
||||
Title string `gorm:"size:128;not null" json:"title"`
|
||||
Summary string `gorm:"type:text" json:"summary"`
|
||||
Summary string `gorm:"column:summary;type:text" json:"summary"`
|
||||
Owner string `gorm:"size:64;default:''" json:"owner"`
|
||||
Priority string `gorm:"size:16;not null;default:P2" json:"priority"`
|
||||
Status string `gorm:"size:32;not null;default:待处理;index" json:"status"`
|
||||
ContextJSON string `gorm:"type:text" json:"context_json"`
|
||||
ContextJSON string `gorm:"column:context_json;type:text" json:"context_json"` // JSON → map[string]any
|
||||
CurrentRunID *uint `gorm:"index" json:"current_run_id"`
|
||||
CurrentResult string `gorm:"type:text" json:"current_result"`
|
||||
CurrentResult string `gorm:"column:current_result;type:text" json:"current_result"` // JSON → map[string]any
|
||||
DueAt *time.Time `json:"due_at"`
|
||||
CreatedBy *uint `gorm:"index" json:"created_by"`
|
||||
LastTriggeredAt *time.Time `json:"last_triggered_at"`
|
||||
@@ -26,4 +26,4 @@ type WorkerTask struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (WorkerTask) TableName() string { return "worker_task" }
|
||||
func (TaskRecord) TableName() string { return "task_record" }
|
||||
+6
-6
@@ -2,8 +2,8 @@ package model
|
||||
|
||||
import "time"
|
||||
|
||||
// WorkerRun 专员动作执行记录
|
||||
type WorkerRun struct {
|
||||
// TaskRun 专员动作执行记录
|
||||
type TaskRun struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
TaskID uint `gorm:"not null;index" json:"task_id"`
|
||||
SpecialistKey string `gorm:"size:64;not null;index" json:"specialist_key"`
|
||||
@@ -11,13 +11,13 @@ type WorkerRun struct {
|
||||
ActionTitle string `gorm:"size:128;not null" json:"action_title"`
|
||||
ActionType string `gorm:"size:32;default:''" json:"action_type"`
|
||||
Status string `gorm:"size:16;not null;default:done;index" json:"status"`
|
||||
InputJSON string `gorm:"type:text" json:"input_json"`
|
||||
OutputJSON string `gorm:"type:text" json:"output_json"`
|
||||
LogsJSON string `gorm:"type:text" json:"logs_json"`
|
||||
InputJSON string `gorm:"column:input_json;type:text" json:"input_json"` // JSON → map[string]any
|
||||
OutputJSON string `gorm:"column:output_json;type:text" json:"output_json"` // JSON → map[string]any
|
||||
LogsJSON string `gorm:"column:logs_json;type:text" json:"logs_json"` // JSON → []map[string]any
|
||||
StartedAt time.Time `json:"started_at"`
|
||||
FinishedAt *time.Time `json:"finished_at"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (WorkerRun) TableName() string { return "worker_run" }
|
||||
func (TaskRun) TableName() string { return "task_run" }
|
||||
@@ -1,16 +0,0 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// UserAppCenter 保存用户自己的应用中心状态,用于同步我的应用、收藏和最近使用。
|
||||
type UserAppCenter struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null;uniqueIndex" json:"user_id"`
|
||||
FavoriteKeys string `gorm:"type:text;not null;default:''" json:"favorite_keys"`
|
||||
RecentKeys string `gorm:"type:text;not null;default:''" json:"recent_keys"`
|
||||
CustomApps string `gorm:"type:text;not null;default:''" json:"custom_apps"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func (UserAppCenter) TableName() string { return "user_app_center" }
|
||||
Reference in New Issue
Block a user