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:
eaiadmin
2026-09-19 01:23:51 +08:00
co-authored by Claude Code
parent ddd2d2cbd8
commit 14f303459e
448 changed files with 18792 additions and 17027 deletions
@@ -5,22 +5,32 @@
> 关联文档:`SY18_Specialist_Minimal_Definition_Model.md`、`SY20_Role_Card_And_Lightweight_Ontology_Architecture.md`、`SY21_Unified_Role_Skill_Action_Architecture.md`、`SY22_Role_Skill_App_Unified_Task_Architecture.md`
> 外部参照:AionUi / AionCore 内置助手与技能(已下载至 `codebase/AionCore-assets/`)
> **2026-09-18 状态补注**:
> 本文是方案稿,不是现状说明。方案主体思路已部分落地,但文中早期文件路径已有更名:
> - `POST /api/assistant/chat` → `POST /api/chat/message`
> - `frontend/src/api/assistant.js` → `frontend/src/api/chatMessage.js`
> - `frontend/src/store/workerRuntime.js` → `frontend/src/store/taskRuntime.js`
> - `internal/api/smart_assistant.go` → `internal/api/chat_message.go`
> - `model.WorkerTask` / `internal/api/worker_task.go` → `model.TaskRecord` / `internal/api/my_task.go` 与 `internal/api/task_runtime.go`
>
> 下面未逐段改写的旧引用,应按这组映射理解;若与当前代码现实冲突,以现代码与 `AR09_Object_Naming_Standard.md` 为准。
---
## 0. 一句话诊断
**我们现在的 10 个专员,在运行时是同一个助手换了 10 个名字。**
工作台主对话走 `POST /api/assistant/chat`(前端 `api/assistant.js` → `chatWithAssistant`),而这个接口:
工作台主对话现走 `POST /api/chat/message`(前端 `api/chatMessage.js` → `sendChatMessage`)。本文最初写作时对应的是旧链路,下面这个诊断应按当前文件映射阅读:
| 环节 | 现状 | 位置 |
|------|------|------|
| 前端发送的字段 | 只有 `message` / `mode` / `ai_route_id` | `views/workbench/SmartAssistantPage.vue:471` |
| 后端请求体 | **已有** `task_id` / `context` 字段,但前端一个都没发 | `internal/api/smart_assistant.go:17-25` |
| 后端 System Prompt | **两条写死的字符串**,与专员无关 | `internal/api/smart_assistant.go:136-140` |
| 后端是否查过 specialist 表 | **从未** | `callAssistantAI` 全文 26 行 |
| 「专家模式」任务拆解 | **三条写死的假步骤**(准备/执行/完成阶段) | `internal/api/smart_assistant.go:161-174` |
| 知识检索 | 这条链路完全不检索知识库 | 对比 `worker_task.go:707` |
| 后端请求体 | 当前链路已显式带上 `task_id` / `specialist_key` / `context` | `internal/api/chat_message.go` |
| 后端 System Prompt | 当前已改为“基础角色 + 专员岗位说明书”拼装 | `internal/api/chat_message.go` |
| 后端是否查过 specialist 表 | 当前会按 `task_id` / `specialist_key` 解析专员 | `resolveSpecialist` |
| 「专家模式」任务拆解 | 仍属可继续深化区,不再是旧版写死入口 | `internal/api/chat_message.go` |
| 知识检索 | 是否接入取决于当前对话模式与后续编排实现,不应再按旧 `worker_task.go` 路径理解 | 对照当前任务运行链路 |
也就是说:**不管用户选了「合同审查专员」还是「物流履约专员」,后端收到的请求完全一样,产出的 System Prompt 完全一样。** 专员是在前端画出来的差异,不是后端跑出来的差异。
@@ -133,8 +143,8 @@ AllowedSkills string `gorm:"type:text" json:"allowed_skills"`
| 文件 | 改动 |
|------|------|
| `frontend/src/api/assistant.js` | `chatWithAssistant(data)` 已透传整个 data,无需改 |
| `frontend/src/store/workerRuntime.js` | 已有 `currentSpecialistKey` computed(185-192 行),直接用 |
| `frontend/src/api/chatMessage.js` | `sendChatMessage(data)` 负责透传工作台对话请求 |
| `frontend/src/store/taskRuntime.js` | 当前运行时 store,负责维护专员 / 技能 / 任务上下文 |
| `frontend/src/views/workbench/SmartAssistantPage.vue:471` | 请求体补上 `task_id` 与 `specialist_key` |
```js
@@ -151,13 +161,13 @@ const res = await chatWithAssistant({
**后端(1 处)**
`internal/api/smart_assistant.go` 的 `callAssistantAI` 增加专员解析:
对应到当前代码,应在 `internal/api/chat_message.go` 的专员解析链路上做这件事(本文原稿写作时文件名为 `smart_assistant.go`):
```go
// 解析当前专员:优先按 task_id 反查(服务端权威),其次用请求里的 specialist_key
func resolveSpecialist(userID uint, req SmartAssistantRequest) *model.Specialist {
if req.TaskID > 0 {
var task model.WorkerTask
var task model.TaskRecord
if err := store.DB.Where("id = ? AND user_id = ?", req.TaskID, userID).
First(&task).Error; err == nil && task.SpecialistKey != "" {
var s model.Specialist
@@ -176,7 +186,7 @@ func resolveSpecialist(userID uint, req SmartAssistantRequest) *model.Specialist
}
```
> 现成的反查先例:`internal/api/worker_task.go:364` 就是同一套 `store.DB.Where("key = ?", task.SpecialistKey).First(&specialist)`。
> 当前应按任务运行链路里的 `TaskRecord` / `specialist_key` 反查实现理解;这里的 `worker_task.go` 引用属于旧文件名。
`SmartAssistantRequest` 需补一个字段(`TaskID` 已存在,只差 `SpecialistKey`):
@@ -186,7 +196,7 @@ SpecialistKey string `json:"specialist_key"`
### 步骤 3:让说明书进 prompt(注入)
改造 `callAssistantAI` 的 System Prompt 拼装,从「二选一写死」变成「通用底座 + 专员说明书」:
改造当前工作台主对话的 System Prompt 拼装,从「二选一写死」变成「通用底座 + 专员说明书」:
```go
func buildAssistantSystemPrompt(userID uint, req SmartAssistantRequest, enableThinking bool) string {
@@ -219,7 +229,7 @@ func buildAssistantSystemPrompt(userID uint, req SmartAssistantRequest, enableTh
}
```
**同时接上第二条链路**:`internal/api/worker_task.go:708` 已经在拼 System Prompt 且**手上就有 specialist 对象**,把说明书一起拼进去即可——这条链路是免费的,因为它本来就认识专员。
**同时接上第二条链路**:当前任务运行链路里本来就会拿到 `specialist` 对象,把说明书一起拼进去即可——这条链路是免费的,因为它本来就认识专员。
```go
systemPrompt := buildSystemPrompt(contextJSON, knowledge) +
@@ -227,7 +237,7 @@ systemPrompt := buildSystemPrompt(contextJSON, knowledge) +
"\n\n当前任务:" + buildWorkerAITaskPrompt(task, specialist, req)
```
> `specialistPromptSection(s)` 抽成一个共用小函数,`smart_assistant.go` 和 `worker_task.go` 都调它,避免两处 prompt 拼法各写一遍。
> `specialistPromptSection(s)` 抽成一个共用小函数,当前应由 `chat_message.go` 与任务运行链路共用,避免两处 prompt 拼法各写一遍。
### §2.4 技能 key 校验清单
@@ -304,7 +314,7 @@ var ValidSkillKeys = map[string]bool{
|------|------|
| `views/workbench/SmartAssistantPage.vue:471` | 请求体补 `task_id` / `specialist_key`(步骤 2) |
| `views/workbench/CapabilityCatalogDetailPage.vue:257` | 专员详情页 `extraSections: []` 是空的,**正好是挂「绑定技能」「岗位说明书」两个 section 的位置**(技能详情页 160-183 行有现成写法可抄) |
| `store/workerRuntime.js:425-441` | `attachSpecialistToCurrentTask` 目前**强制**把技能清成 `DEFAULT_SKILL_KEY`(438 行)。这正是「选了专员反而更空」的根因——应改为写入该专员的**主技能**(`allowed_skills[0]`) |
| `store/taskRuntime.js` | 当前任务运行时里,挂专员到任务时不应再把技能强制清成默认技能;应优先写入该专员的**主技能**(`allowed_skills[0]`) |
### 建议改
@@ -343,7 +353,7 @@ fallbacks, err := config.GetFallbackRoutes(primary.RouteID) // primary == nil
- `cmd/server/main.go:47` 用的是 `gin.Default()`,自带 Recovery —— 所以表现为 **HTTP 500**,不会打挂进程,但**这两个功能 100% 不可用**
- 前端 `api/contract.js:3`、`api/batch.js:3` 确实在调这两个接口
**修复**:两处改为传入真实路由(`config.GetRoute(...)`,可参照 `smart_assistant.go:146`),或在 `GenerateWithFallback` 入口对 `primary == nil` 显式返回错误而非 panic。**建议两者都做**——前者修功能,后者防复发。
**修复**:两处改为传入真实路由(`config.GetRoute(...)`,当前应参照现行工作台对话入口实现),或在 `GenerateWithFallback` 入口对 `primary == nil` 显式返回错误而非 panic。**建议两者都做**——前者修功能,后者防复发。
---
@@ -366,7 +376,7 @@ fallbacks, err := config.GetFallbackRoutes(primary.RouteID) // primary == nil
| **P0** | 修 §5 的空指针缺陷 | 0.5 天 |
| **P1** | 步骤 1 数据层(2 字段 + 播种 + `ValidSkillKeys` 校验) | 0.5 天 |
| **P2** | 步骤 2 打通链路(前端 2 处 + 后端 `resolveSpecialist`) | 0.5 天 |
| **P3** | 步骤 3 注入 prompt(`buildAssistantSystemPrompt` + `worker_task.go:708` 同步) | 0.5 天 |
| **P3** | 步骤 3 注入 prompt(`buildAssistantSystemPrompt` + 当前任务运行链路同步) | 0.5 天 |
| **P4** | 前端最小集(详情页两个 section + `attachSpecialistToCurrentTask` 改主技能) | 1 天 |
| **P5** | 第一梯队 3 份说明书改写(需先拿到 OfficeCLI) | 每份 0.5–1 天 |