chore: 提交剩余开发改动

包含商务通消息映射、会话时间与未读状态、前端消息展示、数据库修复迁移、测试覆盖备份及 Captain 设计文档。
This commit is contained in:
Rogee
2026-08-06 10:10:25 +08:00
parent 8f2cc12628
commit b78f963472
38 changed files with 11691 additions and 1910 deletions
@@ -0,0 +1,819 @@
# Captain Eino SKILLS 实施计划
> 日期:2026-08-04
> 状态:计划中
> 目标:在不大规模重构现有 Captain/Copilot 调用链的前提下,最小化接入 Eino SKILL 能力;第一版采用“每个 Agent 一个独立 SKILLS 目录”的隔离模型,并通过 GoChat 后端适配层提供 scope、路径安全、审计和灰度控制。
---
## 1. 背景与结论
当前 Captain/Copilot 还不是完整 Eino ADK Agent 架构,而是:
```text
CaptainTaskService / CopilotService / CaptainAssistantResponseService
→ llm.Provider
→ ProviderManager / OpenAIProvider / AnthropicProvider / EinoProvider
→ 模型 API
```
Eino SKILL 是 ADK `ChatModelAgent` middleware 能力,不能直接挂到现有 `llm.Provider` 调用链。因此首期需要新增一个窄口径 `CaptainAgentRunner` 作为试点路径:
```text
CaptainTaskService 试点入口
→ CaptainAgentRunner
→ Eino ChatModelAgent
→ GoChatSkillBackend
→ 每个 Agent 独立 SKILLS 目录
```
首期结论:
1. **仅做 SKILL 相关接入**,不引入其他 Agent 能力。
2. 第一版使用文件目录作为 SKILL 存储:每个 Assistant/Agent 一个独立目录。
3. 不能把 Eino filesystem backend 直接作为业务边界;必须包一层 `GoChatSkillBackend`
4. 第一版只支持 inline skill,不支持 `fork` / `fork_with_context`
5. 第一版只读取 `SKILL.md`,不执行 `scripts/`,不开放任意文件系统工具。
6. 首选试点入口是 Captain Playground 或 Copilot reply suggestion,不默认进入 auto-reply 自动发送链路。
---
## 2. 产品与架构决策
### 2.1 SKILLS 隔离方式
第一版采用最简单的隔离方案:**每个 Agent 一个独立 SKILLS 目录**。
推荐目录结构:
```text
backend/data/captain_skills/
account_1/
assistant_10/
refund-policy/
SKILL.md
shipping-status/
SKILL.md
account_1/
assistant_11/
vip-service/
SKILL.md
```
隔离边界:
- 一个 Assistant 只加载自己的 `BaseDir`
- `BaseDir` 只能由后端根据 `account_id + assistant_id` 计算。
- 前端、HTTP request、模型输出都不能直接指定 `BaseDir`
- 第一版只读 `SKILL.md`
- 第一版只支持 inline skill。
### 2.2 为什么仍需要 GoChatSkillBackend
即使每个 Agent 一个目录,也不能直接把 Eino filesystem backend 暴露为业务边界。
必须增加 GoChat 适配层:
```text
CaptainAgentRunner
→ context 注入 CaptainAgentScope
→ GoChatSkillBackend
→ 后端计算 BaseDir
→ 路径规范化与越界校验
→ List/Get SKILL.md
→ 审计日志
→ Eino skill middleware
```
不允许:
```text
HTTP request / 模型输出
→ 任意 BaseDir
→ Eino filesystem skill backend
```
### 2.3 首期能力边界
首期只做:
- SKILL 目录隔离。
- SKILL metadata discovery。
- SKILL content loading。
- inline skill tool。
- scope 校验。
- 路径安全。
- 大小限制。
- 结构化日志/审计。
- 试点入口灰度启用。
首期不做:
- 不新增前端 SKILL 管理页面。
- 不新增正式 `captain_skills` 数据表。
- 不支持 `fork` / `fork_with_context`
- 不执行 `scripts/`
- 不读取 `references/``assets/` 下的任意文件。
- 不开放 filesystem tools。
- 不默认进入 auto-reply 自动发送链路。
- 不做数据库版本管理。
- 不做多实例目录同步。
---
## 3. 实施范围
### 3.1 本计划首期包含
- 最小 `CaptainAgentRunner`,用于试点 Eino ADK Agent。
- `GoChatSkillBackend`,实现 Eino `skill.Backend`
- 每个 Assistant 一个 SKILLS 目录。
- inline skill middleware。
- Captain Playground 或 Copilot reply suggestion 灰度试点。
- 单元测试覆盖 scope 隔离、路径安全、SKILL 解析、runner 分支。
### 3.2 文件命名与目录约定
计划文档路径:
```text
docs/plans/2026-08-04-captain-eino-skills.md
```
运行时 SKILLS 根目录建议:
```text
backend/data/captain_skills
```
说明:
- `backend/data/captain_skills` 是首期 PoC 默认位置。
- 生产部署可后续迁移到共享挂载或数据库模型。
- 首期不新增环境变量;如必须配置 root path,应走数据库配置中心或后端固定配置,而不是让 request 指定。
---
## 4. SKILL 文件格式
### 4.1 目录结构
每个 skill 是一个目录,必须包含 `SKILL.md`
```text
backend/data/captain_skills/account_1/assistant_10/refund-policy/SKILL.md
```
第一版只扫描 Assistant 目录下的第一层子目录:
```text
assistant_10/*/SKILL.md
```
不会递归扫描更深层级。
### 4.2 SKILL.md Frontmatter
首期支持的 frontmatter
```yaml
---
name: refund-policy
description: 处理退款、退货、补偿和售后政策相关问题
context: inline
---
```
字段规则:
- `name` 必填。
- `description` 必填。
- `context` 可选;为空等价于 `inline`
- `context` 只允许空或 `inline`
- `agent` 不支持;出现时返回 validation error。
- `model` 不支持;出现时返回 validation error。
`name` 规则:
```text
[a-z0-9][a-z0-9-]{0,63}
```
### 4.3 SKILL.md Body
body 是给模型看的完整操作说明。
示例:
```markdown
---
name: refund-policy
description: 处理退款、退货、补偿和售后政策相关问题
context: inline
---
当客户询问退款时:
1. 先确认订单状态。
2. 未发货订单可直接退款。
3. 已发货订单需引导客户先拒收或退回商品。
4. 不确定时要求人工客服确认,不要承诺具体到账时间。
```
内容要求:
- 使用业务可读的中文说明。
- 明确适用场景。
- 明确禁止事项。
- 不写密钥、内部接口 token、客户隐私样例。
- 不包含指令要求模型越权访问系统。
---
## 5. 后端设计
### 5.1 CaptainAgentScope
新增文件:
- `backend/internal/service/captain_agent_scope.go`
定义:
```go
type CaptainAgentScope struct {
AccountID uint
AssistantID uint
InboxID uint
ConversationID uint
UserID uint
Feature string
}
```
提供:
```go
func WithCaptainAgentScope(ctx context.Context, scope CaptainAgentScope) context.Context
func CaptainAgentScopeFromContext(ctx context.Context) (CaptainAgentScope, bool)
```
要求:
- `AccountID``AssistantID` 首期必填。
- 缺 scope 时,SkillBackend 必须 fail fast。
- 不允许 scope 零值静默降级到全局目录。
- 未来如果支持 inbox/team scope,应在这里扩展,不要把权限判断散落在 middleware 内。
### 5.2 GoChatSkillBackend
新增文件:
- `backend/internal/service/captain_skill_backend.go`
职责:
- 实现 Eino `skill.Backend`
- 根据 scope 计算 assistant skill directory。
- 扫描第一层子目录下的 `SKILL.md`
- 解析 YAML frontmatter。
- 返回 metadata 或完整 content。
- 做路径安全校验。
- 做大小限制。
- 记录 skill usage 审计日志。
Eino 接口:
```go
type Backend interface {
List(ctx context.Context) ([]skill.FrontMatter, error)
Get(ctx context.Context, name string) (skill.Skill, error)
}
```
目录解析:
```go
root := filepath.Join("data", "captain_skills")
baseDir := filepath.Join(root, fmt.Sprintf("account_%d", scope.AccountID), fmt.Sprintf("assistant_%d", scope.AssistantID))
```
路径安全要求:
- `filepath.Clean`
- 如路径存在,使用 `filepath.EvalSymlinks` 校验真实路径仍在 root 下。
- 禁止 `..` 逃逸。
- 禁止 absolute path 输入参与拼接。
- `Get(ctx, name)` 中的 `name` 必须先通过 slug 校验。
- symlink 指向 root 外部必须失败。
大小限制建议:
- 单个 `SKILL.md` 最大 32KB。
- `List` 最多返回 50 个 skills。
- 所有 metadata description 合计最大 16KB。
不存在目录处理:
- `List`:返回空列表,不报错。
- `Get`:返回 not found error。
解析失败处理:
- frontmatter 缺 `name``description`:该 skill 无效。
- `List` 遇到无效 skill:记录 warning 并跳过,避免一个坏文件阻塞整个 Assistant。
- `Get` 指定到无效 skill:返回明确 validation error。
### 5.3 Skill 内容构造
新增私有函数:
```go
func buildGoChatSkillContent(ctx context.Context, sk skill.Skill, rawArgs string) (string, error)
```
输出格式建议:
```text
# {name}
适用场景:
{description}
操作说明:
{body}
约束:
- 只能基于当前账号/助手授权知识回答。
- 不确定时要求人工客服确认。
- 不要编造订单、物流、退款、支付或账户状态。
```
目的:
- 保持 skill body 原文。
- 追加 GoChat 固定安全边界。
- 降低模型把 skill 当成越权工具的风险。
### 5.4 Skill Tool 描述
新增私有函数:
```go
func buildGoChatSkillToolDescription(ctx context.Context, skills []skill.FrontMatter) string
```
描述要求:
- 告诉模型仅在用户问题匹配 skill 适用场景时调用。
- 告诉模型不能为了泛泛回答加载所有 skill。
- 列出最多 50 个 skill 的 name + description。
- 如果没有 skill,描述应明确当前没有可用 skill。
示例描述要点:
```text
你可以按需加载一个客服处理技能。只有当用户问题明显匹配某个技能描述时才调用。
不要批量加载技能。不要猜测不存在的技能名。
```
### 5.5 CaptainAgentRunner
新增文件:
- `backend/internal/service/captain_agent_runner.go`
- `backend/internal/service/captain_agent_model_adapter.go`
职责:
- 作为试点路径构造 Eino ADK `ChatModelAgent`
- 把 GoChat `llm.Provider` 适配成 Eino `model.BaseModel[*schema.Message]`
- 注入 SKILL middleware。
- 执行 Agent 并返回最终 answer。
首期接口:
```go
type CaptainAgentRunRequest struct {
Scope CaptainAgentScope
Instruction string
Messages []llm.ChatMessage
}
type CaptainAgentRunResult struct {
Message string
Model string
}
type CaptainAgentRunner struct {
provider llm.Provider
skillRoot string
}
func (r *CaptainAgentRunner) Run(ctx context.Context, req CaptainAgentRunRequest) (*CaptainAgentRunResult, error)
```
首期只做同步 Run,不做 stream。
### 5.6 Eino model adapter
新增类型:
```go
type ProviderChatModel struct {
provider llm.Provider
}
```
实现 Eino `Generate` 的最小能力:
- Eino messages → GoChat `llm.ChatMessage`
- 调用 `provider.ChatCompletion`
- GoChat response → Eino `schema.Message`
首期不支持:
- tool calling 以外的自定义工具能力。
- multimodal。
- stream。
- model options 全量映射。
注意:Eino SKILL middleware 本身会注入 `skill` tool,因此 adapter 必须满足 ChatModelAgent 对模型工具调用的最低要求。如果现有 `llm.Provider` 无法承载 Eino tool calling,应改用 Eino 原生 OpenAI-compatible ChatModel 构造模型,或者扩展 adapter 支持 tools option 映射。
该点是实现前必须验证的技术风险。
### 5.7 SKILL middleware 装配
`CaptainAgentRunner.Run` 中:
```go
backend := NewGoChatSkillBackend(skillRoot)
skillHandler, err := skill.NewMiddleware(ctx, &skill.Config{
Backend: backend,
CustomToolDescription: buildGoChatSkillToolDescription,
BuildContent: buildGoChatSkillContent,
})
```
Agent config
```go
agent, err := adk.NewChatModelAgent(ctx, &adk.ChatModelAgentConfig{
Name: "captain",
Instruction: req.Instruction,
Model: chatModel,
Handlers: []adk.ChatModelAgentMiddleware{
skillHandler,
},
MaxIterations: 4,
})
```
首期 `MaxIterations` 控制在 4,避免模型反复调用 skill。
### 5.8 试点接入点
首选接入:Captain Playground 或 Copilot reply suggestion。
建议修改:
- `backend/internal/service/captain_task_service.go`
-`ReplySuggestion` 内增加 feature flag 分支。
- flag 关闭时保持现有逻辑。
- flag 开启时走 `CaptainAgentRunner`
如果当前方法体较复杂,避免大改:新增私有方法:
```go
func (s *CaptainTaskService) replySuggestionWithAgentRunner(ctx context.Context, accountID uint, req *TaskReplySuggestionRequest) (*TaskReplySuggestionResult, error)
```
---
## 6. 灰度开关与配置
首期不做页面配置,使用后端约定和 feature flag
- `captain_agent_runner_enabled`
- `captain_skill_enabled`
如果当前 feature flag 体系已有 account feature map,则先挂在 account feature flags;否则使用内部配置常量,试点时通过种子或测试数据打开。
开关要求:
- flag 关闭时现有 Captain/Copilot 路径行为不变。
- flag 开启但 skill 目录不存在时,runner 仍可执行,只是没有可用 skill。
- skill backend 出错时必须返回错误,不允许静默降级为无 skill,除非明确是目录不存在。
---
## 7. 审计与可观测性
### 7.1 Skill list 日志
`List(ctx)` 记录:
- `account_id`
- `assistant_id`
- `conversation_id`
- `feature`
- `skill_count`
- `skipped_invalid_count`
- `latency_ms`
### 7.2 Skill get 日志
`Get(ctx, name)` 成功时记录:
- `account_id`
- `assistant_id`
- `conversation_id`
- `feature`
- `skill_name`
- `skill_path`
- `skill_hash`
- `skill_mtime`
- `content_bytes`
- `latency_ms`
失败时记录:
- `account_id`
- `assistant_id`
- `conversation_id`
- `skill_name`
- `error_type`
- `latency_ms`
日志禁止包含:
- API key。
- Authorization header。
- 完整客户消息。
- 大段 skill content。
### 7.3 审计落库
首期可以先结构化日志,不新增表。
第二阶段如需要产品级审计,再新增:
- `captain_skill_usages`
建议字段:
- account_id
- assistant_id
- conversation_id
- skill_name
- skill_hash
- user_id
- feature
- created_at
---
## 8. 测试计划
所有 Go 命令从 `backend/` 执行,并使用离线缓存:
```bash
cd backend
GOMODCACHE=/home/yanghao05/go/pkg/mod GOFLAGS=-mod=mod GOPROXY=off go test ./internal/service -run 'CaptainAgent|CaptainSkill|CaptainTask'
GOMODCACHE=/home/yanghao05/go/pkg/mod GOFLAGS=-mod=mod GOPROXY=off go test ./internal/... ./pkg/... ./cmd/...
GOMODCACHE=/home/yanghao05/go/pkg/mod GOFLAGS=-mod=mod GOPROXY=off go build ./...
GOMODCACHE=/home/yanghao05/go/pkg/mod GOFLAGS=-mod=mod GOPROXY=off go vet ./...
```
### 8.1 SkillBackend 单测
覆盖:
1. 缺 scope 直接失败。
2. scope 指向的目录不存在时 `List` 返回空列表,不 panic。
3. `Get` 不存在 skill 时返回 not found。
4. 只列出当前 account/assistant 目录下的 skill。
5. 不列出其他 assistant 的 skill。
6. skill name 非法时 `Get` 失败。
7. `../` 路径逃逸失败。
8. symlink 指向 root 外部时失败。
9. `SKILL.md` 超过大小限制失败。
10. `context: fork` 首期失败。
11. frontmatter 缺 name / description 时 `Get` 失败。
12. `List` 跳过无效 skill 并记录 warning。
13. `Get` 成功时返回 content 并记录审计日志。
14. skill hash / mtime 计算稳定。
### 8.2 CaptainAgentRunner 单测
覆盖:
1. 构造 runner 并执行无 skill 的简单消息。
2. skill middleware 能从当前 assistant 目录加载 skill metadata。
3. 模型调用 skill 后能获得 skill content。
4. skill backend 错误向上返回,不静默降级。
5. feature flag 关闭时原 CaptainTaskService 路径不变。
6. feature flag 开启时走 runner。
7. `MaxIterations` 限制生效。
### 8.3 Eino tool calling 技术验证
必须增加一个最小测试或 spike,验证以下之一成立:
1. `ProviderChatModel` adapter 能接收 Eino `model.WithTools` option,并把 tool schema 传给当前 `llm.Provider`
2. 或者使用 Eino 原生 OpenAI-compatible ChatModel 构造模型,并能和当前 deepseek-v4-flash API 正常 tool call。
如果两者都不成立,SKILL middleware 无法真正被模型调用,应暂停实现并先补模型工具调用适配。
### 8.4 集成验证
准备测试目录:
```text
backend/data/captain_skills/account_1/assistant_1/refund-policy/SKILL.md
```
示例 `SKILL.md`
```markdown
---
name: refund-policy
description: 处理退款、退货、补偿和售后政策相关问题
context: inline
---
当客户询问退款时:
1. 先确认订单状态。
2. 未发货订单可直接退款。
3. 已发货订单需引导客户先拒收或退回商品。
4. 不确定时要求人工客服确认,不要承诺具体到账时间。
```
验证:
- 开启试点 flag。
- 调用 Captain Playground / reply suggestion。
- 提问退款相关问题。
- 日志中出现 skill list/get trace。
- 返回内容遵循 refund policy。
- 其他 assistant 不会加载该 skill。
---
## 9. 风险与控制
### 9.1 模型过度调用 skill
风险:模型明明不需要也加载大量 skill。
控制:
- `MaxIterations` 首期设为 4。
- 每次 run 最多允许调用 skill 2 次。
- SkillBackend 限制 metadata 和 content 大小。
- Custom tool description 明确要求只在匹配时调用。
### 9.2 目录隔离被路径绕过
风险:通过 `../`、软链接、绝对路径读取其他目录。
控制:
- 后端计算 BaseDir。
- Clean + EvalSymlinks。
- slug 校验。
- 不允许 request 传 path。
- 单元测试覆盖 symlink 和路径穿越。
### 9.3 多实例目录不一致
风险:多副本部署时某个实例有 skill,另一个实例没有。
控制:
- PoC 阶段接受本地目录。
- 生产前迁移到共享存储或数据库 `captain_skills` 表。
- 日志记录 skill hash 和 mtime,便于排查实例差异。
### 9.4 Skill 内容变更不可追踪版本
风险:同一会话不同时间加载到不同版本 skill。
控制:
- 首期审计记录 skill path、mtime、hash。
- 第二阶段引入数据库版本和发布状态。
### 9.5 当前模型适配不支持 tool calling
风险:Eino SKILL middleware 依赖 tool call;如果模型 adapter 不支持 toolsAgent 无法调用 `skill`
控制:
- 实施前先做 Eino tool calling spike。
- 如果当前 `llm.Provider` adapter 不适合,首期 CaptainAgentRunner 可直接构造 Eino 原生 OpenAI-compatible ChatModel。
- spike 不通过前,不进入业务接入。
---
## 10. 分阶段任务清单
### 阶段 AEino SKILL tool calling spike
1. 新增最小测试或临时 spike,构造 Eino ChatModelAgent + skill middleware。
2. 使用测试 SKILL 目录。
3. 验证模型能看到 skill tool。
4. 验证模型能调用 skill tool 并获得 content。
5. 明确采用 `ProviderChatModel` adapter 还是 Eino 原生 ChatModel。
6. spike 结果写入实现记录。
### 阶段 BSKILL backend PoC
1. 新增 `CaptainAgentScope` context helper。
2. 新增 `GoChatSkillBackend`
3. 实现 SKILL.md frontmatter parser。
4. 实现目录解析与路径安全校验。
5. 实现 List/Get。
6. 实现 skill content formatter。
7. 实现 tool description builder。
8. 增加结构化日志。
9. 补 SkillBackend 单测。
### 阶段 CCaptainAgentRunner PoC
1. 新增模型 adapter 或 Eino 原生模型构造器。
2. 新增 `CaptainAgentRunner`
3. 装配 Eino skill middleware。
4. 注入 `CaptainAgentScope`
5. 限制 inline skill 和 MaxIterations。
6. 处理 runner final message 提取。
7. 补 runner 单测。
### 阶段 D:试点入口灰度接入
1. 在 Captain Playground 或 reply suggestion 增加 flag 分支。
2. flag 关闭时保持现有逻辑。
3. flag 开启时走 runner。
4. skill 目录不存在时允许无 skill 执行。
5. skill backend 真实错误向上返回。
6. 补 service 单测。
### 阶段 E:验证与收敛
1. 准备本地测试 SKILLS 目录。
2. 跑单元测试和 build/vet。
3. 用真实 deepseek-v4-flash provider 做一次手工验证。
4. 检查日志没有 API key、Authorization header 和完整客户消息。
5. 输出验证报告。
---
## 11. 验收标准
首期完成必须满足:
1. 现有非 Agent Captain/Copilot 路径在 flag 关闭时行为不变。
2. SkillBackend 只能读取当前 account/assistant 目录。
3. SkillBackend 无法通过 `../` 或 symlink 逃逸 root。
4. SkillBackend 对缺 scope fail fast。
5. SkillBackend 对目录不存在返回空列表。
6. SkillBackend 对无效 `SKILL.md` 有明确行为:List 跳过,Get 失败。
7. CaptainAgentRunner 能在试点入口加载 inline skill。
8. 模型能通过 Eino skill middleware 调用 skill 并获得 content。
9. Skill 使用有结构化日志或审计记录。
10. `go test ./internal/service -run 'CaptainAgent|CaptainSkill|CaptainTask'` 通过。
11. `go build ./...` 通过。
12. 日志和错误响应不包含 API key、Authorization header 或完整敏感客户消息。
---
## 12. 后续演进
首期跑通后再考虑:
- 新增 `captain_skills` / `captain_playbooks` 表。
- 前端 SKILL 管理页面。
- SKILL 版本、发布、回滚。
- inbox/team/channel 级 skill scope。
- `fork` / `fork_with_context`
- Eino `summarization`
- Eino `reduction`
- Eino `dynamictool/toolsearch`
- streaming ADK Agent。
- auto-reply 场景白名单启用。
---
## 13. 推荐立即执行顺序
建议先实现:
1. Eino SKILL tool calling spike。
2. GoChatSkillBackend。
3. CaptainAgentRunner PoC。
4. 试点入口灰度接入。
理由:
- SKILL 依赖 Eino ADK Agent 和 tool calling,必须先验证模型适配是否成立。
- 每个 Agent 一个目录足够支持 PoC,但必须有 backend 适配层确保路径和 scope 安全。
- 先从 Playground / reply suggestion 试点,避免影响自动回复客户链路。