Files
OSGKeyboard/docs/clipboard-voice-command-plan.md
T
Rocky f197b68573 feat(keyboard): add clipboard voice command mode
Long-press mic runs ASR as an instruction over eligible clipboard text,
with host-confirm recording UI, min-record gate, and light ASR prewarm.
Bump CURRENT_PROJECT_VERSION to 53.
2026-08-07 16:08:42 +08:00

323 lines
16 KiB
Markdown
Raw 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.
# 剪贴板语音指令(Clipboard Voice Command)一页规划
> **文档状态**:产品与架构规划(**决策已冻结**;实现进行中)
> **适用范围**:iOS 主 App + 键盘扩展(复用现有 Flow / ASR / 润色管线)
> **分支**`feature/clipboard-voice-command`
> **创建日期**2026-08-07
> **修订日期**2026-08-07(架构评审 + 状态机 + 过滤/手势 + 协议/Prompt 冻结)
> **关联能力**Flow 会话、PiP 保活、Polish Style Packs、字段指纹插入
---
## 1. 一句话定义
用户复制一段材料到剪贴板后,**长按麦克风**口述处理意图;系统以「剪贴板快照 = 材料、ASR = 指令、当前风格包 = 底色」生成结果,并直接写入当前输入框。短按始终回到普通听写。
覆盖场景:**社交回复**(委婉拒绝、温暖安慰)与 **文档处理**(精简、总结)共用一条管线。
---
## 2. 冻结决策
| 项 | 选择 |
|----|------|
| 入口 | **复用麦克风,长按为新增状态**:短按 = 现有听写 toggle;长按 = 剪贴板指令 |
| 长按录音 | 达标后 **立刻开录****松手结束本轮**并进入生成 |
| 生成中麦克风 | **F1:锁定**,等结果上屏后再长按连续改写(不排队、不取消重录) |
| 提示文案 | 有开场资格时改为:**「点击说话,长按处理剪贴板」** |
| 剪贴板读取 | **机会读**(键盘出现 / 回前台 / 触摸键盘)更新提示;长按开场再读一次并 **冻结快照**;不后台轮询 |
| 开场资格时钟 | 复制后 **30 秒内** 可进入 |
| 材料过滤 | R0R6;过短 **有效长度 < 15**;见 §4 |
| 长按达标 | **0.45s**;松手结束;滑出再松仍送生成(见 §4.1) |
| 指令会话时钟 | 开场成功起 **30 秒**;每轮 **成功上屏** 刷新 30 秒;失败不刷新 |
| 与 Flow/PiP | **独立子状态**:指令 30s ≠ Flow 无活动超时;小会话结束不退出 Flow |
| 材料快照 | 开场瞬间冻结;会话中剪贴板变更不影响本会话 |
| 首轮写入 | 空框写入;非空追加 |
| 连续改写 | **干净 → 替换上轮产物**;**不干净 → 追加**(用户自删) |
| 替换实现 | **不依赖**系统撤销 API(扩展无法调用宿主 Undo);用自记文本 + `deleteBackward` 尽力替换 |
| 会话中短按 | **结束指令会话 → 听写** |
| 换输入框 | **结束指令会话**(拿不准是否换框时,按不干净处理:只追加不删) |
| 风格 | 共享基础润色契约;当前 Style Pack 作底色;**本轮指令优先** |
| 翻译开关 | 指令模式 MVP **忽略** |
| 预览 | 直接上屏,不强制确认 |
| 录音条 | **展示**指令 ASR 原文(仅 UI,不上屏到输入框) |
| 语音历史 | **只记成功产物**;不记指令 ASR;快照不落盘 |
| 密码 / 安全框 | **禁止**指令模式 |
| 扩展 jetsam | **指令会话结束**(内存态丢弃,可接受) |
| 失败 fallback | 指令模式 **禁止** raw ASR 插入输入框(与听写失败落原文相反) |
| 协议 | **扩展 FlowCommandA1**`utteranceMode` + `clipboardSnapshot`**start 即携带**;见 §11 |
| 快照上限 | wire 与送模型均为 **3000** 字(超出截断) |
| Prompt | **独立** `ClipboardCommandPromptComposer`;材料 / 指令 / 上一版;风格 **B1 短偏置**;见 §11 |
---
## 3. 交互主路径
```text
复制文本
→ 机会读发现开场资格(≤30s + 通过材料过滤)
→ 提示:「点击说话,长按处理剪贴板」
→ 长按达标 → 立刻录音,再读并冻结 clipboardSnapshot,开启指令会话
→ 松手 → 停录;录音条曾展示指令 ASR
→ ASR(指令) + Snapshot(材料) + StylePack(底色) → LLM(生成中 mic 锁定)
→ 成功:空框写入 / 非空追加;记录本轮产物;刷新会话 30s;可写入语音历史(仅产物)
→ 失败:不插入;不写历史;会话保持但不续期
→(可选)出字后再长按连续改写:
干净则删上轮产物再写新版;否则追加
→ 短按 / 会话到期 / 换输入框 / 收起键盘 / jetsam → 结束会话,丢弃快照
```
---
## 4. 开场过滤(可测规则,冻结)
命中任一条 → 保持听写提示,**不进入指令模式**。
**计数:** trim 首尾空白后用 Swift `String.count`(扩展字形簇),下称「有效长度」。
| # | 规则 | 可测定义 |
|---|------|----------|
| R0 | 空 / 非文本 | trim 后为空 |
| R1 | 整段像号码 | 去掉空白后匹配 `^[\d\-\+\(\)\s]+$` 且至少含 1 位数字(任意长度) |
| R2 | 纯 emoji/符号 | 去掉空白后 **无** 字母、汉字、数字 |
| R3 | 验证码形态 | 有效长度 4…8,整段仅 `[A-Za-z0-9]`,且 **同时含** 字母与数字 |
| R4 | 过短 | 有效长度 **< 15**(定死) |
| R5 | 单字符刷屏 | 去掉空白后长度 ≥ 15,不同字符种类 ≤ 2,且某一字符占比 ≥ 80% |
| R6 | 运行时拒绝 | 安全输入框、无 Full Access、读剪贴板失败(含系统粘贴权限拒绝) |
**不做(MVP):** 用模型判断「有无意义」;因含 URL/邮箱整段拒绝;因过长拒绝入口。
**送模型截断(不影响入口):** 快照超过 **3000** 字则截断(与 wire 上限相同),prompt 可注明已截断。
**例:**
| 文本 | 结果 |
|------|------|
| `13812345678``+86 138-1234-5678` | 拒(R1 |
| `😀😀😀``!!!` | 拒(R2 |
| `A8f2K1` | 拒(R3 |
| `周末吃饭吗`<15 | 拒(R4 |
| `啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊` | 拒(R5 |
| `周末有空一起吃个饭吗?我想聊下项目进度。` | 过 |
首次读剪贴板触发系统粘贴权限且失败 → 无资格 + 弱提示。
---
## 4.1 长按阈值(冻结)
| 项 | 值 |
|----|-----|
| 长按达标 | **0.45s** |
| 未达 0.45s 松手 | 视为 **短按** → 听写 toggle |
| 达到 0.45s | 可选轻震 + **立刻**开指令录音 |
| 达标后滑出按钮再松手 | MVP:**仍结束本轮并送去生成**(不做滑出取消) |
---
## 5. 写入与「干净」判定
**可替换(干净)须同时满足:** 同一指令会话、能判定仍在同一输入框、有上轮插入记录、光标仍在上轮产物末尾、上轮文本未被用户改动。
**推荐执行顺序:** LLM 成功拿到新文本 → 再删旧 → 再插新;删后插失败则尽力写回旧文本。不确定时 **只追加或不插,绝不误删用户原文**
**说明:** 用户仍可自行使用系统撤销(摇一摇等)作为逃生口;产品自动替换不依赖该能力。
---
## 6. 失败态与边界(优先级)
1. **不丢用户原文**(替换不确定 → 追加或不插)
2. **不把指令 ASR 写入输入框**(ASR/LLM 失败均不插入;禁用听写式 raw fallback
3. **不静默滥用剪贴板**(无资格不进指令;机会读 + 开场再读;不轮询)
4. **软失败可重试但不续期**(没听清 / 生成失败:不插入、会话保持、时钟不刷新)
5. **少打断**(弱提示,不弹模态)
| 场景 | 策略 |
|------|------|
| 开场资格过期 | **仅改回普通听写文案**;不必再弹「已过期」。此后长按不进指令 |
| 无有效语音 / ASR 空 | 不调用 LLM;不插入;**麦克风上方文本提示**;会话保持(不续期) |
| 指令不可解析 | 麦克风上方提示说明要怎么处理;不插入 |
| LLM 超时/拒绝/空结果 | 不插入;麦克风上方提示可重试;不刷新时钟 |
| 换输入框 / 收起键盘 / jetsam | 结束会话;已上屏保留 |
| 会话中剪贴板被覆盖 | 本会话仍用旧快照 |
| 密码框 | 禁止指令模式 |
| 生成中再长按 | 忽略(mic 锁定),等上屏后再改写 |
**隐私:** 快照仅内存;会话结束丢弃;不写历史明文;不把指令 ASR 写入历史。
---
## 7. 与现有架构的关系
| 可复用 | 必须新建(实现期) |
|--------|-------------------|
| Flow 命令/结果桥、PiP、ASR、插入器、字段指纹 | 长按手势层;机会读与 R0–R6 过滤 |
| `PolishingService`(含 `systemPrompt` 覆盖) | `ClipboardCommandPromptComposer`finalize 按 `utteranceMode` 分流 |
| Flow 大会话保活 | 指令会话态(扩展);`FlowCommand` mode/snapshot/previousOutput;禁 raw 门禁 |
**两套会话(写死):**
- **Flow / PiP**:主 App 可响应录音(分钟级无活动等现有策略)
- **指令会话**:一次「处理剪贴板」任务(30s 刷新规则如上)
- 小会话结束 ≠ 退出 Flow;勿把指令 30s 接到 `FlowInactivityDuration`
**非目标(本期):** 后台轮询剪贴板、强制预览、独立回复大按钮、生成中排队/取消重录(F2/F3)、把指令模式混进现有 transcript-polish 路径、依赖宿主 Undo API。
---
## 8. 状态机(冻结)
状态机回答三件事:**现在在哪、什么事件会跳转、哪些操作允许**。分两层,勿混用。
### 8.1 层 A — 手势(手指)
| 状态 | 含义 |
|------|------|
| 空闲 | 未按下 |
| 按下待判定 | 已按下,尚未达到长按阈值(可能变成短按) |
| 指令按住录音 | 长按已达标,正在录指令;**松手 → 停录** |
| (听写录音) | 仍由现有 **短按 toggle** 进入/结束,不经本层长按路径 |
### 8.2 层 B — 剪贴板任务(生意)
| 状态 | 提示 / mic | 含义 |
|------|------------|------|
| **无资格** | 普通听写文案 | 材料不合格、已过期、安全框、无权限等 |
| **有资格** | 「点击说话,长按处理剪贴板」 | 机会读通过:合格材料且复制后 ≤30s |
| **指令录音中** | 录音条可展示指令 ASR | 长按已开场,快照已冻结 |
| **生成中** | mic **锁定**(F1) | 转写 + LLM;不接受新的短按/长按开录 |
| **可连续改写** | 可再长按;短按则退出 | 上一轮已结束(成功上屏或失败提示);会话 30s 未到期 |
| **已结束** | — | 清快照与上轮产物记录;再评估有资格/无资格 |
### 8.3 主转移
```text
无资格
│ 机会读:合格且 ≤30s
有资格 ◄─────────────────────────────────────────┐
│ 长按达标(开场读并冻快照) │
▼ │
指令录音中 ──松手──► 生成中 │
│ │
┌───────────┼───────────┐ │
▼ ▼ ▼ │
成功上屏 失败(仅麦克风上方文本提示) │
│ │ │
└─────┬─────┘ │
▼ │
可连续改写 ──再长按───────────────────────┘
│ (回到「指令录音中」,同一快照)
│ 短按听写 / 会话30s到期 / 换输入框
│ / 收起键盘 / jetsam
已结束 ──► 机会读 → 有资格 或 无资格
```
**资格过期:** 机会读发现超时 → **无资格**,提示改回普通文案即可,**不再**单独弹「剪贴板已过期」。
**生成失败:** 不插入、不写历史、不刷新会话 30s;落在 **可连续改写**(若会话未到期),麦克风上方文本提示;若墙钟已超过会话 30s → **已结束**
**换框拿不准:** 不强制跳「已结束」时,按「不干净」只追加不删(见 §5);一旦能判定换框 → **已结束**
### 8.4 事件 × 任务态(摘要)
| 当前任务态 | 短按 | 长按达标 | 松手 |
|------------|------|----------|------|
| 无资格 | 听写 toggle | 不进指令 | — |
| 有资格 | 听写 toggle | → 指令录音中 | — |
| 指令录音中 | — | — | → 生成中 |
| 生成中 | 忽略 | 忽略 | — |
| 可连续改写 | → 已结束,再听写 | → 指令录音中(同快照) | — |
| 已结束 | 听写 | 视重新评估后的资格 | — |
---
## 9. 成功标准(验收)
1. 复制合格文本后,经机会读,30s 内提示变为「点击说话,长按处理剪贴板」;过期后仅改回普通文案。
2. 长按立刻录音,松手后生成;口述「委婉拒绝 / 精简 / 总结」等,结果直接进入当前输入框。
3. 录音条可见指令 ASR;失败时输入框不被指令原文污染,麦克风上方有文本提示。
4. 出字后再长按改写:未手改则替换上轮产物;手改或不干净则追加。
5. 生成中无法再开下一轮录音(F1)。
6. 会话中短按恢复听写;换输入框结束指令会话。
7. 纯数字 / 纯 emoji / 验证码 / 有效长度<15 / 刷屏等不出现指令提示。
8. 语音历史仅出现成功产物,无指令 ASR、无剪贴板快照明文。
9. 长按 0.45s 开录;未达阈值松手走听写;滑出松手仍生成。
---
## 11. 协议与 Prompt(冻结)
### 11.1 FlowCommand 扩展(A1
在现有 `start/stop/abort` 上增加(`protocolVersion` bump,保持旧字段可解码):
| 字段 | 时机 | 含义 |
|------|------|------|
| `utteranceMode` | start(必填语义) | `dictation`(默认/缺省)\| `clipboardCommand` |
| `clipboardSnapshot` | **start 即带** | 开场冻结的材料;≤3000 字;仅 `clipboardCommand` |
| `fieldContext` | 可仍在 stop 时带 | 插入指纹 / 空框判定等(沿用现状) |
```text
长按达标 → 冻快照(≤3000
→ startRecording { mode: clipboardCommand, clipboardSnapshot }
→ 松手 → stopRecording { fieldContext? }
→ host finalizemode=clipboardCommand → 指令 Composer
失败 → errorrawText 可留作调试,ext 不得 insert raw
```
连续改写 = 同一指令会话内多个 utterance;每轮 start **重复携带同一份**内存快照。
「上一版产物」不进 FlowCommand,由扩展在成功上屏后记住,经 host 侧 prompt 用户区传入(见 11.2)——若上一版仅 ext 知道,则需在 stop/start 增加可选 `previousOutput`,或 finalize 前写入 App Group。
**推荐补字段(冻结):** start 或 stop 可选 `previousOutput: String?`(连续改写时由 ext 带上轮成功产物;首轮 nil)。与快照同属 utterance 自描述,避免旁路。
**FlowResult** 回传 `utteranceMode`(或等效标记)。ext 对 `clipboardCommand`**跳过**一切 raw ASR 上屏路径(含 `deliverRawFallbackIfAvailable`)。
### 11.2 Prompt 分流
| | 听写 | 剪贴板指令 |
|--|------|------------|
| Composer | `PolishPromptComposer`(含 R6 | **新建** `ClipboardCommandPromptComposer` |
| 用户文本角色 | ASR = 待整理正文 | ASR = **指令**;快照 = **材料** |
| 翻译开关 | 可生效 | **忽略**,强制非 translate |
| 失败 | 可 fallback raw ASR | **禁止** raw 上屏 |
| Style Pack | 完整听写人格装配 | **B1**:短语气偏置;指令优先 |
**System 契约(精神):** 按材料执行指令;只输出可直接发送的最终文本;无解释套话;不编造材料中没有的关键事实(除非指令要求语气发挥);Style Pack 为弱底色;口述指令覆盖风格。
**User 结构:**
```text
【材料】
{clipboardSnapshot}
【指令】
{asrInstruction}
【上一版结果】 ← 仅连续改写且有 previousOutput
{previousOutput}
```
### 11.3 职责切分
| 职责 | 归属 |
|------|------|
| 机会读、R0R6、资格 30s、hint、长按 0.45s | 扩展 |
| 指令会话 30s、快照内存、上轮产物、干净替换 | 扩展 |
| ASR、指令 LLM、结果 mode 标记 | 主 App |
| 坚持不插 raw、插入/替换执行 | 扩展 |
| Style Pack 短偏置读取 | 主 App(现有 store |
主 App 对指令 utterance **尽量无会话态**(按包执行);指令会话态在扩展 → 与 jetsam=会话结束一致。
---
## 12. 下一步(可进入实现)
1. 实现 Shared:过滤纯函数 + 单测;`FlowCommand`/`FlowResult` 字段与兼容解码。
2. 实现 `ClipboardCommandPromptComposer` + finalize 分流 + 禁 raw。
3. 扩展:机会读 hint、长按 0.45s、指令会话态、插入/替换。
4. 联调验收对照 §9。