Files
dingtalk-workspace-cli/docs/shortcut-p2-design.md

205 lines
9.6 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# DWS Shortcut P2 详细设计 — 高频场景自动沉淀为自定义 Shortcut
> 前置:P1 已交付静态声明式 shortcut 框架(`internal/shortcut/`),见 `docs/shortcut-plan.md`。
> P2 目标:**观察用户高频使用 → 主动建议 → 一键沉淀为可复用的自定义 shortcut**,
> 让 CLI 越用越顺手。这是 dws 相对 larksuite/cli 的差异化能力。
## 0. 体验闭环(一句话)
```
用户反复敲 dws chat send_message --json '{"open_conversation_id":"cid_x","text":"..."}'
│ (每次执行被静默记录到 ~/.dws/usage.jsonl)
▼
第 N 次后,dws 主动提示:
💡 你已 12 次向「项目群」发消息,是否沉淀为 `dws chat +notify-team`?[y/N]
│ y
▼
写入 ~/.dws/shortcuts/chat.notify-team.yaml
▼
之后:dws chat +notify-team --text "发布完成" ← 参数从一堆 JSON 收敛成一个 flag
```
## 1. 总体架构
复用 P1 的 `Shortcut` 模型作为「编译目标」,新增四个部件:
| 部件 | 位置(建议) | 职责 |
|------|-------------|------|
| Usage 埋点 | `internal/shortcut/usage/recorder.go` | 每次 MCP 调用后追加一条 usage 记录 |
| 模式挖掘 | `internal/shortcut/usage/miner.go` | 聚合 usage → 高频候选 + 打分 |
| 主动提示 | `internal/shortcut/usage/nudge.go` | 命中候选时在命令收尾处提示 |
| YAML 加载 | `internal/shortcut/userdef/loader.go` | 扫描 `~/.dws/shortcuts/*.yaml` → 编译成 `Shortcut` → 注册 |
| 管理命令 | `internal/shortcut/usage`(cobra) | `dws shortcut list/suggest/add/rm/stats` |
数据流:
```
CallMCP ──► recorder.Append(usage) [写侧,热路径,必须极轻]
┌─► miner.TopCandidates() [读侧,suggest 时才算]
~/.dws/usage.jsonl ─────────────────┤
└─► nudge (命令收尾抽样触发)
~/.dws/shortcuts/*.yaml ──► loader.Compile() ──► shortcut.Register() [启动时]
```
## 2. Usage 埋点
### 2.1 采集点(choke point)
**首选**:装饰 `executor.Runner` / `edition.ToolCaller`。`internal/app/tool_caller_adapter.go`
的 `CallTool(ctx, productID, toolName, args)` 是**所有 MCP 调用的唯一必经点**,天然拿到
`(product, tool, args)` 三元组。用装饰器包一层即可,零侵入命令层:
```go
type recordingCaller struct{ inner edition.ToolCaller }
func (r recordingCaller) CallTool(ctx, product, tool string, args map[string]any) (*edition.ToolResult, error) {
res, err := r.inner.CallTool(ctx, product, tool, args)
usage.Append(product, tool, args, err == nil) // 异步/带 recover,绝不影响主流程
return res, err
}
```
> 注意:P1 的 shortcut 也走这条 `CallMCP → helpers → deps.Caller`,所以内建 shortcut 的
> 使用同样会被记录,可用于「哪些内建 shortcut 最受欢迎」的洞察。
### 2.2 记录内容(**隐私优先:记形状不记值**)
`~/.dws/usage.jsonl`,每行一条:
```json
{
"ts": "2026-07-08T10:12:33+08:00",
"product": "chat",
"tool": "send_message",
"arg_keys": ["open_conversation_id", "text"],
"const_args": {"open_conversation_id": "cid_x"},
"ok": true
}
```
- `arg_keys`:参数键集合(排序),用于识别「同一种调用形状」。
- `const_args`:**仅收敛出的「疑似固定值」**(见 §3 挖掘时判定),写入时不保证脱敏,
因此需要一层白名单/黑名单:`text/content/body/message` 等自由文本字段**永不入库**,
只保留看起来像 ID/枚举的短值(长度阈值 + 无空格 + 非多行)。
- 绝不记录:token、手机号、邮箱、文件内容、消息正文。用 `internal/logging/redact.go`
已有的脱敏能力复核。
### 2.3 热路径约束
- 追加写用 `O_APPEND`,单行 < 1KB;失败静默(`recover` + debug 日志),**绝不阻断命令**。
- 文件滚动:超过 N 行(如 5000)或 M 天,截断/归档,避免无限增长。
- 开关:默认关闭(opt-in),环境变量 `DWS_USAGE_TRACKING=1` 开启(本地遥测即便只记形状也不应未经用户同意默认开启)。
首次启用时在 `dws` 首跑给一次性告知(尊重知情)。
## 3. 模式挖掘
`dws shortcut suggest` 触发(也被 nudge 复用)。算法:
1. 读 usage.jsonl,按 `(product, tool, arg_keys)` 分桶。
2. 对每桶:
- `count` = 出现次数;低于阈值(默认 5)直接丢弃。
- 对每个 arg_key,统计其值的分布:某值占比 ≥ 80% → 判定为**固定值**(进 `const_args`);
否则判定为**可变参数**(沉淀后成为 flag)。
- `recency` = 最近一次使用距今;越近权重越高。
3. 打分 `score = count * log(distinct_days+1) * recencyDecay`,取 TopN。
4. 生成候选 `Candidate{product, tool, fixed{...}, varFlags[...], score, samples}`。
输出示例(`dws shortcut suggest --format table`):
```
候选 | 命令建议 | 依据 | 固定参数 | 可变flag
#1 | chat +notify-team | 12 次 / 近 3 天 | open_conv=cid_x | text
#2 | doc +new-agenda | 7 次 / 近 5 天 | template=agenda | title
```
## 4. 主动提示(nudge)
- **时机**:命令成功收尾时(root `PersistentPostRunE`),**抽样**触发(如每 N 次调用或每次
命中新达标候选时),避免打扰。仅在 TTY 交互态提示;非交互(Agent/管道/`--yes`)**不提示**。
- **频控**:同一候选提示过一次被拒后,冷却期内不再提示(记 `~/.dws/shortcuts/.declined`)。
- **交互**:
```
💡 检测到高频操作:你已 12 次向同一会话发消息。
沉淀为快捷指令 dws chat +notify-team --text "..." ?
[y] 沉淀 [n] 以后再说 [d] 不再提示此项
```
- y → 走 §5 生成 YAML;命名默认 `+<tool 去下划线的动宾>`,允许用户改名。
## 5. 自定义 Shortcut:YAML 格式与运行时加载
### 5.1 YAML schema(与 P1 `Shortcut` 一一对应)
`~/.dws/shortcuts/chat.notify-team.yaml`:
```yaml
version: 1
service: chat
command: "+notify-team"
product: chat
description: "发消息到 项目群(自动沉淀于 2026-07-08)"
risk: write # 默认 read;send 类判定为 write
source: auto # auto=沉淀 / manual=手写
flags:
- name: text
type: string
required: true
desc: 消息内容
execute:
tool: send_message
bind: # 参数绑定:常量 + ${flag} 模板
open_conversation_id: "cid_x"
text: "${text}"
```
### 5.2 编译与注册
`userdef.Compile(yaml)` → `shortcut.Shortcut`,其 `Execute` 由 `bind` 生成:
遍历 `bind`,`${flag}` 用 `rt.Str(flag)` 填充,常量原样,组装 params 后 `rt.CallMCP(tool, params)`。
完全复用 P1 的 runner,不新增执行路径。
加载时机:`legacy.go` 装配点,在 `builtin.Commands()` 之后追加 `userdef.Commands()`,
一起 merge。复用 `internal/plugin/loader.go` 已验证的「扫 `~/.dws/` 目录 + 挂 cobra」模式。
### 5.3 冲突与优先级
- 自定义 shortcut 命令名若与内建 shortcut / helper 冲突:**内建优先**,自定义重命名或跳过并告警。
- `+` 前缀天然与 helper leaf 区分,冲突面小。
## 6. 管理命令面
```
dws shortcut list # 列出内建 + 自定义 shortcut
dws shortcut suggest [--min N] # 展示高频候选(不写入)
dws shortcut add <candidate|--from-last> # 交互式/从最近一次调用沉淀
dws shortcut rm <service> <+cmd> # 删除自定义 shortcut
dws shortcut stats # usage 统计概览
```
`dws shortcut` 本身作为一个新的顶层 utility 命令注册(对齐 `dws plugin`)。
## 7. 安全与隐私边界(红线)
1. **值不入库**:自由文本/正文/凭证一律不记;`const_args` 仅短 ID/枚举,且过 redact 复核。
2. **可关可清**:`DWS_USAGE_TRACKING=0` 关闭;`dws shortcut stats --purge` 清空 usage。
3. **执行白名单**:自定义 shortcut 的 `execute.tool` 必须解析到合法 MCP server(过
`internal/security` endpoint 白名单),禁止指向任意 endpoint。
4. **不自动执行**:沉淀只生成命令定义,**绝不**自动发起写操作;写类 shortcut 仍受 P1 的
risk 确认约束。
5. **知情**:首次开启埋点一次性告知;提示可永久关闭。
## 8. 实现顺序(P2 分步,便于 loop 推进)
- P2-1 usage 埋点:`recordingCaller` 装饰器 + `usage.Append` + jsonl 写 + 开关 + 脱敏白名单。
- P2-2 `dws shortcut stats` / `list`:先让数据可见,验证埋点质量。
- P2-3 miner + `dws shortcut suggest`:离线挖掘与打分。
- P2-4 userdef YAML 加载 + `Compile` + 注册 + 冲突处理(打通「手写 YAML 也能用」)。
- P2-5 `dws shortcut add`(从候选/最近调用沉淀)。
- P2-6 nudge 主动提示(最后做,最谨慎,默认保守频控)。
## 9. 待决策点(需产品确认)
1. 埋点默认开还是默认关?→ **修订后:默认关(opt-in)+ 开启后首跑一次性告知**(原设计默认开,反思后改为 opt-in:自主 agent 不应单方面默认开本地遥测)
(`DWS_USAGE_TRACKING=0` / 配置项关闭;`dws shortcut stats --purge` 清空)。实现时以此为准。
2. `const_args` 允许记录的字段白名单粒度?(保守起步:只记形如 `*_id/*Id/type/status` 的短值)
3. nudge 触发频率与渠道?(建议:仅 TTY、命中新候选时、每候选一生仅一次)
4. 自定义 shortcut 是否需要跨设备同步?(v1 先本地 `~/.dws/`,同步留待后续)