9.6 KiB
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) 三元组。用装饰器包一层即可,零侵入命令层:
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,每行一条:
{
"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 复用)。算法:
- 读 usage.jsonl,按
(product, tool, arg_keys)分桶。 - 对每桶:
count= 出现次数;低于阈值(默认 5)直接丢弃。- 对每个 arg_key,统计其值的分布:某值占比 ≥ 80% → 判定为固定值(进
const_args); 否则判定为可变参数(沉淀后成为 flag)。 recency= 最近一次使用距今;越近权重越高。
- 打分
score = count * log(distinct_days+1) * recencyDecay,取 TopN。 - 生成候选
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:
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. 安全与隐私边界(红线)
- 值不入库:自由文本/正文/凭证一律不记;
const_args仅短 ID/枚举,且过 redact 复核。 - 可关可清:
DWS_USAGE_TRACKING=0关闭;dws shortcut stats --purge清空 usage。 - 执行白名单:自定义 shortcut 的
execute.tool必须解析到合法 MCP server(过internal/securityendpoint 白名单),禁止指向任意 endpoint。 - 不自动执行:沉淀只生成命令定义,绝不自动发起写操作;写类 shortcut 仍受 P1 的 risk 确认约束。
- 知情:首次开启埋点一次性告知;提示可永久关闭。
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. 待决策点(需产品确认)
- 埋点默认开还是默认关?→ 修订后:默认关(opt-in)+ 开启后首跑一次性告知(原设计默认开,反思后改为 opt-in:自主 agent 不应单方面默认开本地遥测)
(
DWS_USAGE_TRACKING=0/ 配置项关闭;dws shortcut stats --purge清空)。实现时以此为准。 const_args允许记录的字段白名单粒度?(保守起步:只记形如*_id/*Id/type/status的短值)- nudge 触发频率与渠道?(建议:仅 TTY、命中新候选时、每候选一生仅一次)
- 自定义 shortcut 是否需要跨设备同步?(v1 先本地
~/.dws/,同步留待后续)