Files
wangjia 40760aa884
ci / server (push) Failing after 14s
ci / design-tokens (push) Failing after 11s
dudu MVP:五端语音输入法初始提交
- server:Go 网关(WS 流式识别中继/计费配额/微信登录支付 mock/反馈/埋点),gummy provider 已真实联调
- desktop:Tauri 2(全局快捷键 push-to-talk/浮层/托盘/设置/登录购买/反馈/首启引导)
- android:Compose 主 App + IME(键盘内录音直传)
- ios:App + 键盘扩展(1A spike 实证键盘内不可录音,走 deep link 听写)
- design/design-pipeline:设计系统 + token 导出 iOS/Android 主题
- doc:前后端设计文档(HTML);web:官网宣传页;todo:任务看板

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 00:38:37 +08:00

696 lines
40 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>语音输入法(duduMVP 设计方案</title>
<style>
:root {
--primary: #4f6ef7;
--primary-dark: #3a55d9;
--bg: #f6f7fb;
--card: #ffffff;
--text: #2b2f3a;
--text-light: #6b7280;
--border: #e5e7eb;
--green: #16a34a;
--orange: #ea8a0a;
--red: #dc2626;
--code-bg: #1e2433;
--code-text: #d6e2f0;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", "Segoe UI", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.75;
}
.container { max-width: 980px; margin: 0 auto; padding: 0 24px 80px; }
header.hero {
background: linear-gradient(135deg, #3a55d9 0%, #6c4ff7 100%);
color: #fff;
padding: 56px 24px 48px;
text-align: center;
}
header.hero h1 { margin: 0 0 8px; font-size: 34px; letter-spacing: 1px; }
header.hero p.sub { margin: 0; font-size: 16px; opacity: .85; }
.meta-chips { margin-top: 20px; }
.chip {
display: inline-block; padding: 4px 14px; margin: 4px;
background: rgba(255,255,255,.16); border-radius: 999px; font-size: 13px;
}
section.card {
background: var(--card); border: 1px solid var(--border);
border-radius: 12px; padding: 28px 32px; margin-top: 28px;
box-shadow: 0 1px 3px rgba(0,0,0,.04);
}
h2 {
font-size: 22px; margin: 0 0 16px; padding-bottom: 10px;
border-bottom: 2px solid var(--primary); display: inline-block;
}
h3 { font-size: 17px; margin: 24px 0 8px; color: var(--primary-dark); }
h4 { font-size: 15px; margin: 18px 0 6px; }
table { width: 100%; border-collapse: collapse; margin: 12px 0; font-size: 14px; }
th, td { border: 1px solid var(--border); padding: 8px 12px; text-align: left; vertical-align: top; }
th { background: #f0f2fa; font-weight: 600; }
tr:nth-child(even) td { background: #fafbfe; }
pre.diagram, pre.code {
background: var(--code-bg); color: var(--code-text);
padding: 18px 20px; border-radius: 10px; overflow-x: auto;
font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 13px; line-height: 1.55;
}
code.inline {
background: #eef0f7; color: #3a55d9; padding: 1px 6px;
border-radius: 4px; font-family: "SF Mono", Menlo, Consolas, monospace; font-size: 13px;
}
.callout {
border-left: 4px solid var(--primary); background: #eef2ff;
padding: 12px 18px; border-radius: 0 8px 8px 0; margin: 14px 0; font-size: 14px;
}
.callout.warn { border-color: var(--orange); background: #fff7ed; }
.callout.risk { border-color: var(--red); background: #fef2f2; }
.tag {
display: inline-block; font-size: 12px; padding: 1px 10px; border-radius: 999px;
margin-right: 6px; font-weight: 600;
}
.tag.green { background: #dcfce7; color: var(--green); }
.tag.orange { background: #ffedd5; color: var(--orange); }
.tag.red { background: #fee2e2; color: var(--red); }
.tag.blue { background: #e0e7ff; color: var(--primary-dark); }
ul, ol { padding-left: 22px; }
li { margin: 4px 0; }
.toc { columns: 2; font-size: 14px; }
.toc a { color: var(--primary-dark); text-decoration: none; }
.toc a:hover { text-decoration: underline; }
/* ===== Mockup 通用 ===== */
.mockup-row { display: flex; flex-wrap: wrap; gap: 24px; margin: 16px 0; align-items: flex-start; }
.mockup-caption { text-align: center; font-size: 13px; color: var(--text-light); margin-top: 8px; }
/* 桌面浮层 mockup */
.overlay-mock {
width: 340px; background: rgba(28,30,40,.96); border-radius: 14px;
padding: 14px 18px; color: #fff; box-shadow: 0 12px 32px rgba(0,0,0,.35);
}
.overlay-mock .wave { display: flex; align-items: flex-end; gap: 3px; height: 26px; margin-bottom: 8px; }
.overlay-mock .wave i {
display: block; width: 4px; background: linear-gradient(180deg,#7c9bff,#4f6ef7);
border-radius: 2px;
}
.overlay-mock .listen { color: #9aa6c8; font-size: 14px; }
.overlay-mock .text { font-size: 15px; line-height: 1.6; }
.overlay-mock .text .partial { color: #9aa6c8; }
.overlay-mock .hint { font-size: 11px; color: #6b7595; margin-top: 8px; text-align: right; }
/* 设置窗口 mockup */
.window-mock {
width: 420px; background: #fff; border-radius: 10px; overflow: hidden;
border: 1px solid var(--border); box-shadow: 0 8px 24px rgba(0,0,0,.12); font-size: 13px;
}
.window-mock .titlebar {
background: #f0f1f5; padding: 8px 12px; display: flex; align-items: center; gap: 6px;
border-bottom: 1px solid var(--border);
}
.window-mock .dot { width: 11px; height: 11px; border-radius: 50%; }
.window-mock .titlebar span.title { margin-left: 10px; color: var(--text-light); font-size: 12px; }
.window-mock .body { padding: 16px 18px; }
.window-mock .row { display: flex; justify-content: space-between; align-items: center; padding: 9px 0; border-bottom: 1px dashed var(--border); }
.window-mock .row:last-child { border-bottom: none; }
.kbd {
background: #eef0f7; border: 1px solid #d4d9ea; border-bottom-width: 2px; border-radius: 5px;
padding: 1px 8px; font-family: "SF Mono", Menlo, monospace; font-size: 12px;
}
.btn-mock {
background: var(--primary); color: #fff; border-radius: 6px; padding: 4px 14px; font-size: 12px;
}
.btn-mock.ghost { background: #eef0f7; color: var(--primary-dark); }
/* 手机 mockup */
.phone-mock {
width: 290px; background: #111; border-radius: 32px; padding: 10px;
box-shadow: 0 12px 32px rgba(0,0,0,.25);
}
.phone-mock .screen { background: #fff; border-radius: 24px; overflow: hidden; }
.phone-mock .statusbar { height: 26px; background: #fff; display:flex; justify-content:center; align-items:center; }
.phone-mock .notch { width: 90px; height: 16px; background: #111; border-radius: 0 0 10px 10px; }
.phone-mock .app-area { padding: 14px; min-height: 120px; font-size: 13px; }
.phone-mock .fake-input {
border: 1.5px solid var(--primary); border-radius: 8px; padding: 8px 10px; color: var(--text-light);
font-size: 13px; background: #fafbff;
}
.phone-mock .fake-input .cursor { color: var(--primary); animation: none; }
.keyboard-mock { background: #d4d7de; padding: 8px 6px 10px; }
.keyboard-mock .toolbar {
display: flex; justify-content: space-between; padding: 0 8px 8px; font-size: 11px; color: #555;
}
.keyboard-mock .keys { display: flex; flex-direction: column; gap: 6px; }
.keyboard-mock .keyrow { display: flex; gap: 5px; justify-content: center; }
.keyboard-mock .key {
background: #fff; border-radius: 5px; min-width: 24px; padding: 7px 0; text-align: center;
font-size: 13px; flex: 1; box-shadow: 0 1px 0 rgba(0,0,0,.25);
}
.keyboard-mock .key.fn { background: #b9bec9; font-size: 11px; flex: 1.4; }
.mic-bar {
margin-top: 6px; background: linear-gradient(135deg,#4f6ef7,#6c4ff7); border-radius: 8px;
color: #fff; text-align: center; padding: 12px 0; font-size: 14px; font-weight: 600;
box-shadow: 0 1px 0 rgba(0,0,0,.25);
}
.mic-bar.active { background: linear-gradient(135deg,#16a34a,#22c55e); }
.kb-partial {
background: #fff; margin: 0 6px 6px; border-radius: 8px; padding: 8px 10px; font-size: 13px;
border: 1px solid #c3c8d4;
}
.kb-partial .partial { color: #9aa0b3; }
/* 托盘菜单 mockup */
.tray-mock {
width: 230px; background: rgba(245,246,250,.98); border: 1px solid var(--border);
border-radius: 10px; box-shadow: 0 8px 24px rgba(0,0,0,.16); font-size: 13px; overflow: hidden;
}
.tray-mock .item { padding: 7px 14px; display: flex; justify-content: space-between; }
.tray-mock .item:hover { background: var(--primary); color: #fff; }
.tray-mock .sep { height: 1px; background: var(--border); margin: 4px 0; }
.tray-mock .muted { color: var(--text-light); font-size: 12px; }
footer { text-align: center; color: var(--text-light); font-size: 13px; margin-top: 48px; }
</style>
</head>
<body>
<header class="hero">
<h1>语音输入法(dudu</h1>
<p class="sub">MVP 前后端设计方案 · 大模型识别 · 流式实时上屏 · 全平台</p>
<div class="meta-chips">
<span class="chip">版本 v0.3</span>
<span class="chip">日期 2026-06-11</span>
<span class="chip">状态:已确认</span>
</div>
</header>
<div class="container">
<!-- ============ 变更记录 ============ -->
<section class="card">
<h2>变更记录</h2>
<table>
<tr><th style="width:10%">版本</th><th style="width:16%">日期</th><th>变更</th></tr>
<tr>
<td>v0.2</td><td>2026-06-11</td>
<td>引入 <code class="inline">design/</code> 官方设计系统后修订:① 计费模型由"订阅制 + 每日免费 10 分钟"改为<strong>时长包(不过期)+ 每日免费试用 3 分钟</strong>;② 第八章 mockup 已被设计系统高保真稿取代,正式以 <code class="inline">design/ui_kits/</code> 为准;③ 数据表 plans/subscriptions 废弃,改时长账本。详细设计见配套文档:<a href="../frontend-design.html">前端实现设计 v1.0</a> · <a href="../backend-architecture.html">后端与数据架构 v1.0</a></td>
</tr>
<tr>
<td>v0.3</td><td>2026-06-11</td>
<td>新增两项能力并更新配套文档至 v1.1:① <strong>客户端打点</strong>(响应时间等指标采集与批量上报,监控首字延迟等体验目标);② <strong>反馈问题</strong>(设置内提交文字 + 图片 + 诊断信息)。详见 <a href="../frontend-design.html">前端实现设计 v1.1</a> 第八/九章 · <a href="../backend-architecture.html">后端与数据架构 v1.1</a> 3.5/3.6</td>
</tr>
<tr><td>v0.1</td><td>2026-06-11</td><td>初版 MVP 前后端设计方案</td></tr>
</table>
</section>
<!-- ============ 目录 ============ -->
<section class="card">
<h2>目录</h2>
<div class="toc">
<ol>
<li><a href="#overview">产品概述与 MVP 需求</a></li>
<li><a href="#decisions">关键技术决策</a></li>
<li><a href="#arch">总体架构</a></li>
<li><a href="#backend">后端设计(Go</a></li>
<li><a href="#storage">存储选型说明</a></li>
<li><a href="#desktop">桌面端设计(Tauri 2 + Rust</a></li>
<li><a href="#mobile">移动端设计(系统输入法扩展)</a></li>
<li><a href="#ui">各终端简要设计(界面与交互)</a></li>
<li><a href="#latency">延迟优化清单</a></li>
<li><a href="#repo">仓库结构</a></li>
<li><a href="#milestones">里程碑</a></li>
<li><a href="#verify">风险与验证</a></li>
</ol>
</div>
</section>
<!-- ============ 1. 概述 ============ -->
<section class="card" id="overview">
<h2>一、产品概述与 MVP 需求</h2>
<p>一款付费语音输入法,核心卖点:<strong>大模型识别准确度 + 流式实时上屏 + 极简交互</strong></p>
<table>
<tr><th style="width:30%">MVP 需求</th><th>说明</th></tr>
<tr><td>全平台覆盖</td><td>Mac / Windows / iPhone / iPad / Android</td></tr>
<tr><td>大模型识别</td><td>使用云厂商大模型 ASR,识别准确率显著优于传统方案</td></tr>
<tr><td>极简交互</td><td>桌面端:焦点在输入框时按住全局快捷键说话;移动端:键盘上按住按键说话</td></tr>
<tr><td>流式识别</td><td>说话过程中识别结果实时显示,不等说完</td></tr>
<tr><td>商业化</td><td>付费软件:微信登录 + 微信支付购买时长包</td></tr>
<tr><td>性能</td><td>识别快、无可感知卡顿(首字延迟 P95 &lt; 500ms</td></tr>
</table>
</section>
<!-- ============ 2. 决策 ============ -->
<section class="card" id="decisions">
<h2>二、关键技术决策</h2>
<table>
<tr><th style="width:18%">决策点</th><th style="width:30%">选择</th><th>理由</th></tr>
<tr>
<td>ASR 引擎</td>
<td>国内云厂商大模型流式 ASR<br>首选:阿里云百炼 <code class="inline">gummy-realtime-v1</code><br>备选:火山引擎豆包流式 ASR</td>
<td>原生 WebSocket 流式、国内延迟低、自带标点/ITN/中英混说、按量计费免运维。通过 Provider 抽象接口接入,可随时切换</td>
</tr>
<tr>
<td>后端语言</td>
<td>Go</td>
<td>本质是 WS 流式中继 + 业务 CRUD,瓶颈在云端 ASR 而非自身代码,Rust 性能优势体现不出来;微信支付有官方 Go SDKwechatpay-go);goroutine 天然适合"每连接一个转发循环";开发速度快</td>
</tr>
<tr>
<td>桌面端</td>
<td>Tauri 2 + Rust</td>
<td>体积小(~10MB)、常驻内存低;Rust 侧做全局快捷键/音频采集/文字注入等系统层;一套代码出 Mac + Windows</td>
</tr>
<tr>
<td>移动端形态</td>
<td>系统第三方输入法(键盘扩展)</td>
<td>iOS Keyboard Extension / Android InputMethodService,按住说话直接上屏到任意 App,体验最佳。iOS 麦克风限制为最高风险项(见风险章节)</td>
</tr>
<tr>
<td>移动端技术</td>
<td>Swift / Kotlin 各自原生</td>
<td>协议简单(WS + JSON + PCM),不值得引入跨端框架的复杂度;输入法扩展本身必须原生</td>
</tr>
</table>
</section>
<!-- ============ 3. 架构 ============ -->
<section class="card" id="arch">
<h2>三、总体架构</h2>
<pre class="diagram">
┌─────────────┐ ┌─────────────┐ ┌──────────────────┐
│ 桌面端 Tauri │ │ iOS/Android │ │ │
│ Mac/Windows │ │ 键盘扩展 │ │ 微信开放平台 │
└──────┬──────┘ └──────┬──────┘ │ (OAuth/支付回调) │
│ WSS 音频流 │ └────────┬─────────┘
▼ ▼ │
┌──────────────────────────────────────────────────────┐
│ Go 后端(单体起步) │
│ ┌─────────┐ ┌───────────┐ ┌─────────┐ ┌──────────┐ │
│ │ auth │ │asr-gateway│ │ billing │ │ quota │ │
│ │ 微信登录 │ │ WS 流式中继│ │ 微信支付 │ │ 用量/配额 │ │
│ └─────────┘ └─────┬─────┘ └─────────┘ └──────────┘ │
└────────────────────┼─────────────────────────────────┘
│ WSS(同区域部署,内网级延迟)
┌─────────────────────┐ ┌──────────────┐
│ 阿里云百炼 Gummy │ │ PostgreSQL │
│ (流式大模型 ASR) │ │ + Redis │
└─────────────────────┘ └──────────────┘
</pre>
<div class="callout">
<strong>为什么音频走后端中继而不是客户端直连云 ASR:</strong>
<ol style="margin:6px 0 0">
<li>ASR 密钥不能下发到客户端;</li>
<li>计费基于识别秒数,必须在服务端计量,否则配额可被绕过;</li>
<li>Provider 可切换、可灰度,客户端协议永远不变。</li>
</ol>
中继增加的延迟约 10–50ms(后端与 ASR 同区域部署),对体验无感。
</div>
</section>
<!-- ============ 4. 后端 ============ -->
<section class="card" id="backend">
<h2>四、后端设计(Go</h2>
<h3>4.1 技术栈与部署</h3>
<ul>
<li>ginHTTP API+ gorilla/websocket(流式网关)</li>
<li>PostgreSQL(用户/订单/时长账本)+ Redis(配额计数、登录轮询状态)</li>
<li><code class="inline">wechatpay-go</code> 官方 SDK</li>
<li>部署:阿里云(与百炼同区域)、Docker、单体应用起步</li>
</ul>
<h3>4.2 模块划分</h3>
<pre class="code">server/
cmd/server/main.go
internal/
auth/ 微信 OAuth(扫码 + App 登录)、JWT 签发与中间件
asr/ Provider 接口 + gummy / volcano 两个实现
gateway/ WS /v1/asr/stream 处理器(核心)
billing/ 微信支付下单、回调验签、订单状态机
quota/ 用量计量(Redis INCR + 异步落库)、试用/余额校验
user/ 用户、设备 CRUD、时长包查询
store/ gorm models + migrations
pkg/protocol/ 客户端协议常量与消息结构(5 端对齐的唯一真相源)</pre>
<h3>4.3 ASR Provider 抽象(可切换的关键)</h3>
<pre class="code">type Provider interface {
// 建立一次流式识别会话
StartSession(ctx context.Context, cfg SessionConfig) (Session, error)
}
type Session interface {
SendAudio(pcm []byte) error // 推 100ms 音频帧
Results() &lt;-chan Result // {Text, IsFinal, BeginTime, EndTime}
Close() error
}</pre>
<p>MVP 实现 <code class="inline">gummy</code> providerDashScope WebSocket API,原生流式、自带标点和 ITN、中英混说)。</p>
<h3>4.4 客户端 ↔ 后端流式协议(WSS <code class="inline">/v1/asr/stream</code></h3>
<table>
<tr><th style="width:22%">方向 / 类型</th><th>内容</th></tr>
<tr><td>上行 · 二进制帧</td><td>16kHz / 16bit / mono PCM,每 100ms 一帧(3200 字节)</td></tr>
<tr><td>上行 · 文本帧</td><td><code class="inline">{"type":"start","sample_rate":16000}</code> / <code class="inline">{"type":"stop"}</code></td></tr>
<tr><td>下行 · partial</td><td><code class="inline">{"type":"partial","text":"今天天气"}</code> — 增量结果,客户端实时刷新浮层</td></tr>
<tr><td>下行 · final</td><td><code class="inline">{"type":"final","text":"今天天气不错。"}</code> — 句子定稿,客户端执行注入</td></tr>
<tr><td>下行 · error</td><td><code class="inline">{"type":"error","code":"QUOTA_EXCEEDED","message":"..."}</code></td></tr>
</table>
<p>鉴权:连接时带 <code class="inline">Authorization: Bearer &lt;JWT&gt;</code>。连接为长连接 + ping/pong 心跳,<strong>客户端启动时即预连接</strong>,按下快捷键时零握手开销。</p>
<h3>4.5 微信登录</h3>
<table>
<tr><th></th><th>方式</th><th>微信平台应用类型</th></tr>
<tr><td>桌面端</td><td>扫码登录:后端生成带 state 的二维码页 → 手机微信扫码授权 → 回调 → 客户端轮询 <code class="inline">GET /v1/auth/qr/:state</code> 拿 JWT</td><td>网站应用</td></tr>
<tr><td>移动端</td><td>主 App 集成微信 OpenSDK 拉起授权 → code 换 token</td><td>移动应用</td></tr>
</table>
<p><strong>unionid</strong> 关联同一用户的多端身份(桌面扫码与手机 App 登录是两个不同 openid)。</p>
<h3>4.6 微信支付与计费</h3>
<ul>
<li>桌面端:<strong>Native 支付</strong>(后端下单返回 code_url → 客户端展示二维码)</li>
<li>移动端:<strong>APP 支付</strong>(主 App 拉起微信收银台)</li>
<li>回调 <code class="inline">POST /v1/pay/notify</code>:验签 → 订单状态机(pending → paid)→ 时长入账</li>
<li>计费模型(v0.2):<strong>时长包不过期</strong>100 分钟 ¥9 / 500 分钟 ¥39 / 2000 分钟 ¥129+ <strong>每天免费试用 3 分钟</strong>;用量按识别音频秒数计、先扣试用再扣余额,用尽时网关下发 <code class="inline">QUOTA_EXCEEDED</code>。详见<a href="../backend-architecture.html">后端与数据架构 v1.0</a> 第五章</li>
</ul>
<h3>4.7 核心数据表</h3>
<p><code class="inline">users</code><code class="inline">wechat_identities</code><code class="inline">duration_packs</code><code class="inline">orders</code><code class="inline">balance_ledger</code>(时长账本)、<code class="inline">trial_usage</code>(每日试用)、<code class="inline">asr_sessions</code><code class="inline">devices</code></p>
</section>
<!-- ============ 5. 存储选型 ============ -->
<section class="card" id="storage">
<h2>五、存储选型说明</h2>
<p>一句话总结:<strong>PostgreSQL 管"钱和账"(强一致、持久),Redis 管"快和临时"(计数器、带过期时间的状态)。</strong></p>
<h3>PostgreSQL — 持久化业务数据(不能丢的数据)</h3>
<table>
<tr><th style="width:32%"></th><th>用途</th></tr>
<tr><td>users / wechat_identities</td><td>用户账号,微信 openid/unionid 与用户的关联</td></tr>
<tr><td>duration_packs / orders</td><td>时长包定义(服务端可配)、微信支付订单——支付回调验签后改状态,钱相关数据必须强一致、可审计</td></tr>
<tr><td>balance_ledger / trial_usage</td><td>时长账本(只追加,余额 = 合计)与每日免费试用用量(180 秒/天,自然日重置)</td></tr>
<tr><td>asr_sessions</td><td>每次识别会话的用量明细(时长、试用/余额扣减拆分),用于对账与数据分析</td></tr>
</table>
<p>特点:写入频率低、绝对不能丢,且需要事务("回调到账 → 订单置为已支付 → 时长入账"必须原子完成)。</p>
<h3>Redis — 高频、短生命周期的热数据</h3>
<ol>
<li><strong>实时配额计数</strong>:识别过程中每隔几秒累计用量并校验是否超额。每帧都查 PostgreSQL 既扛不住也拖慢链路,故用 Redis <code class="inline">INCR</code> 实时计数,后台异步批量落库到 balance_ledger / asr_sessions</li>
<li><strong>扫码登录轮询状态</strong>:桌面端展示二维码后轮询"扫了没",state 只活 2 分钟,天然适合带 TTL 的 key;</li>
<li><strong>JWT 黑名单 / 会话踢出</strong>:退出登录、账号踢出立即生效。</li>
</ol>
<div class="callout">MVP 若想再简化可暂不引入 Redis(配额放进程内存 + 定时落库),但多实例部署时会算不准,且阿里云托管 Redis 运维成本接近零,故直接采用。</div>
</section>
<!-- ============ 6. 桌面端 ============ -->
<section class="card" id="desktop">
<h2>六、桌面端设计(Tauri 2 + Rust</h2>
<h3>6.1 核心交互流程(push-to-talk</h3>
<pre class="diagram">按下快捷键 → 浮层弹出(光标附近)→ 采集音频流式上传
→ partial 结果实时显示在浮层 → 松开快捷键
→ final 注入焦点输入框 → 浮层消失</pre>
<h3>6.2 Rust 侧模块</h3>
<table>
<tr><th style="width:20%">模块</th><th>方案</th></tr>
<tr><td>全局快捷键</td><td><code class="inline">tauri-plugin-global-shortcut</code>;默认按住 <span class="kbd">Cmd/Ctrl + Shift + Space</span> 说话(press 开始 / release 结束)。macOS 监听 Fn 键需 CGEventTap,二期再做</td></tr>
<tr><td>音频采集</td><td><code class="inline">cpal</code> 采集 + 重采样到 16kHz mono PCM100ms 分帧</td></tr>
<tr><td>WebSocket</td><td><code class="inline">tokio-tungstenite</code>,启动即预连接、断线指数退避重连</td></tr>
<tr><td>文字注入 macOS</td><td>写剪贴板 → CGEvent 模拟 <span class="kbd">Cmd+V</span> → 恢复剪贴板(兼容性最好);需引导用户授予<strong>辅助功能权限</strong>,首次启动做权限引导页</td></tr>
<tr><td>文字注入 Windows</td><td><code class="inline">SendInput</code> + <code class="inline">KEYEVENTF_UNICODE</code> 直接注入 Unicode,无需剪贴板</td></tr>
<tr><td>浮层窗口</td><td>Tauri 无边框 / 置顶 / 透明 / <strong>不抢焦点</strong>(关键:<code class="inline">focusable: false</code>,否则焦点离开目标输入框)小窗,显示实时文本 + 音量波形</td></tr>
</table>
<div class="callout">
<strong>上屏策略(MVP):</strong>partial 只显示在浮层不注入,final 句子级一次性注入。避免"增量注入 + 退格修正"与目标应用输入状态冲突的复杂性,二期可选讯飞式逐字上屏。
</div>
<h3>6.3 前端 UIWeb 技术)</h3>
<ul>
<li>设置窗口:快捷键自定义、麦克风选择、登录状态、购买时长(Native 支付二维码)</li>
<li>浮层窗口:识别文本 + 波形动画</li>
<li>常驻系统托盘,开机自启</li>
</ul>
</section>
<!-- ============ 7. 移动端 ============ -->
<section class="card" id="mobile">
<h2>七、移动端设计(系统输入法扩展)</h2>
<h3>7.1 iOS / iPadOS(同一套代码)</h3>
<ul>
<li><strong>主 App</strong>(SwiftUI):微信登录、购买时长(微信 APP 支付)、输入法启用引导;</li>
<li><strong>Keyboard Extension</strong>UIInputViewController):键盘上一个大麦克风按钮,按住说话 → 浮动文本区实时显示 partial → 松开后 final 经 <code class="inline">textDocumentProxy.insertText</code> 上屏;</li>
<li>需开启<strong>完全访问权限</strong>(联网必需);token 通过 <strong>App Group</strong> 与主 App 共享。</li>
</ul>
<div class="callout risk">
<strong>⚠️ 最高风险项:</strong>iOS 键盘扩展内麦克风录音受系统限制(历史上 App Extension 不可用 AVAudioSession 录音;讯飞/搜狗有键盘内语音但实现路径需验证)。<strong>M0 第一周必须做技术 spike</strong>;若不可行,降级方案为键盘按钮经 deep link 跳主 App 录音 → App Group 回传 → 键盘上屏(体验打折但可发布)。
</div>
<h3>7.2 Android</h3>
<ul>
<li><strong>主 App</strong>Kotlin + Compose):登录、支付、启用引导;</li>
<li><strong>IME Service</strong><code class="inline">InputMethodService</code>):自绘键盘 + 按住说话,<code class="inline">RECORD_AUDIO</code> 权限即可在 IME 内录音(无 iOS 式限制),<code class="inline">currentInputConnection.commitText</code> 上屏,成熟可控。</li>
</ul>
<p>实施顺序:<strong>先 Android 后 iOS</strong>(风险低、验证快)。两端不引入跨端框架,Swift / Kotlin 各自原生实现协议(对齐 <code class="inline">server/pkg/protocol</code>)。</p>
</section>
<!-- ============ 8. 各终端简要设计 ============ -->
<section class="card" id="ui">
<h2>八、各终端简要设计(界面与交互)</h2>
<div class="callout warn"><strong>v0.2 注:</strong>本章 mockup 为 v0.1 规划草稿,已被 <code class="inline">design/</code> 官方设计系统的高保真稿取代(计费已改时长包、视觉禁用渐变/emoji)。正式界面以 <code class="inline">design/ui_kits/</code> 演示与<a href="../frontend-design.html">前端实现设计 v1.0</a> 为准,本章仅作历史记录保留。</div>
<!-- ---- 8.1 桌面端 ---- -->
<h3>8.1 桌面端(Mac / Windows</h3>
<p>界面清单:<span class="tag blue">① 识别浮层</span><span class="tag blue">② 系统托盘菜单</span><span class="tag blue">③ 设置窗口</span><span class="tag blue">④ 登录/购买窗口</span></p>
<h4>① 识别浮层(核心界面,两态)</h4>
<p>按下快捷键瞬间弹出在光标附近,<strong>不抢焦点</strong>;松开快捷键注入文字后淡出。</p>
<div class="mockup-row">
<div>
<div class="overlay-mock">
<div class="wave">
<i style="height:8px"></i><i style="height:14px"></i><i style="height:22px"></i><i style="height:12px"></i><i style="height:18px"></i><i style="height:26px"></i><i style="height:10px"></i><i style="height:16px"></i><i style="height:6px"></i><i style="height:20px"></i><i style="height:12px"></i><i style="height:8px"></i>
</div>
<div class="listen">🎙️ 聆听中…</div>
<div class="hint">松开 ⌘⇧Space 完成输入</div>
</div>
<div class="mockup-caption">态 A:按下快捷键瞬间(不等首个结果,先给反馈)</div>
</div>
<div>
<div class="overlay-mock">
<div class="wave">
<i style="height:18px"></i><i style="height:24px"></i><i style="height:10px"></i><i style="height:22px"></i><i style="height:14px"></i><i style="height:26px"></i><i style="height:8px"></i><i style="height:20px"></i><i style="height:16px"></i><i style="height:24px"></i><i style="height:10px"></i><i style="height:14px"></i>
</div>
<div class="text">帮我把这份周报整理一下,<span class="partial">重点突出本周的</span></div>
<div class="hint">松开 ⌘⇧Space 完成输入</div>
</div>
<div class="mockup-caption">态 B:流式识别中(深色为 final,浅色为 partial 实时刷新)</div>
</div>
</div>
<h4>② 系统托盘菜单 + ③ 设置窗口</h4>
<div class="mockup-row">
<div>
<div class="tray-mock">
<div class="item"><span>🎙️ dudu 语音输入</span><span class="muted">已就绪</span></div>
<div class="sep"></div>
<div class="item"><span>启用快捷键</span><span class="muted"></span></div>
<div class="item"><span>设置…</span></div>
<div class="item"><span>本月已用</span><span class="muted">128 / 600 分钟</span></div>
<div class="sep"></div>
<div class="item"><span>账号:wang***(已订阅)</span></div>
<div class="item"><span>退出</span></div>
</div>
<div class="mockup-caption">托盘菜单:状态一目了然,入口极简</div>
</div>
<div>
<div class="window-mock">
<div class="titlebar">
<span class="dot" style="background:#ff5f57"></span>
<span class="dot" style="background:#febc2e"></span>
<span class="dot" style="background:#28c840"></span>
<span class="title">dudu 设置</span>
</div>
<div class="body">
<div class="row"><span>说话快捷键</span><span><span class="kbd"></span> <span class="kbd"></span> <span class="kbd">Space</span>(按住说话)</span></div>
<div class="row"><span>麦克风</span><span>MacBook Pro 麦克风 ▾</span></div>
<div class="row"><span>开机自启</span><span></span></div>
<div class="row"><span>登录状态</span><span>微信已登录 · wang***</span></div>
<div class="row"><span>当前套餐</span><span>年度会员(2027-06-10 到期)<span class="btn-mock ghost">续费</span></span></div>
</div>
</div>
<div class="mockup-caption">设置窗口:单页搞定全部配置</div>
</div>
</div>
<h4>④ 登录 / 购买窗口</h4>
<ul>
<li>登录:窗口内展示微信二维码(后端 <code class="inline">/v1/auth/qr</code>),手机扫码授权后自动登录并关闭;</li>
<li>购买:选套餐(月/年)→ 展示微信 Native 支付二维码 → 支付完成自动刷新订阅状态;</li>
<li>首次启动流程:欢迎页 → 授予权限(macOS 辅助功能 + 麦克风)→ 微信登录 → 试一试(引导用户在示例输入框里按快捷键说一句话)。</li>
</ul>
<!-- ---- 8.2 移动端键盘 ---- -->
<h3>8.2 移动端键盘扩展(iOS / Android 布局一致)</h3>
<p>界面清单:<span class="tag blue">① 键盘面板(核心)</span><span class="tag blue">② 候选/partial 文本条</span><span class="tag blue">③ 未登录/超额提示态</span></p>
<p>MVP 键盘策略:<strong>以语音为主键盘</strong>——大麦克风按键 + 一行常用功能(切回系统键盘、退格、回车、标点),不做完整 26 键(降低开发量,用户在语音和系统键盘间一键切换)。</p>
<div class="mockup-row">
<div>
<div class="phone-mock">
<div class="screen">
<div class="statusbar"><div class="notch"></div></div>
<div class="app-area">
<div style="color:var(--text-light);font-size:12px;margin-bottom:6px">微信 · 与张三聊天中</div>
<div class="fake-input">说点什么…<span class="cursor"></span></div>
</div>
<div class="keyboard-mock">
<div class="toolbar"><span>dudu 语音输入</span><span>已订阅 ✓</span></div>
<div class="keys">
<div class="keyrow">
<div class="key fn">🌐 切换键盘</div>
<div class="key fn"></div>
<div class="key fn"></div>
<div class="key fn">⌫ 退格</div>
<div class="key fn">↵ 发送</div>
</div>
</div>
<div class="mic-bar">🎙️ 按住说话</div>
</div>
</div>
</div>
<div class="mockup-caption">态 A:默认态(大按键 + 一行功能键)</div>
</div>
<div>
<div class="phone-mock">
<div class="screen">
<div class="statusbar"><div class="notch"></div></div>
<div class="app-area">
<div style="color:var(--text-light);font-size:12px;margin-bottom:6px">微信 · 与张三聊天中</div>
<div class="fake-input">好的,我马上把文件发给你<span class="cursor"></span></div>
</div>
<div class="keyboard-mock">
<div class="toolbar"><span>dudu 语音输入</span><span>00:04</span></div>
<div class="kb-partial">好的,我马上把文件发给你,<span class="partial">大概十分钟之</span></div>
<div class="mic-bar active">🟢 松开完成 · 上滑取消</div>
</div>
</div>
</div>
<div class="mockup-caption">态 B:按住说话中(partial 实时显示,final 即时上屏)</div>
</div>
</div>
<h4>键盘交互细节</h4>
<ul>
<li><strong>按住说话,松开上屏</strong>;上滑取消本次输入(微信语音的肌肉记忆);</li>
<li>流式上屏策略:<strong>final 句子即时 <code class="inline">insertText</code> 上屏</strong>,partial 显示在键盘自带的文本条里(不直接写入宿主输入框,避免修正抖动);</li>
<li>未登录态:麦克风按键置灰,点击提示"打开 dudu App 登录"deep link 跳主 App);</li>
<li>超额态:按键变橙色提示"今日免费额度已用完,升级会员"。</li>
</ul>
<!-- ---- 8.3 移动主 App ---- -->
<h3>8.3 移动端主 AppiOS / Android</h3>
<p>界面清单:<span class="tag blue">① 启用引导页</span><span class="tag blue">② 登录页</span><span class="tag blue">③ 套餐购买页</span><span class="tag blue">④ 我的(用量/订阅管理)</span></p>
<table>
<tr><th style="width:22%">页面</th><th>内容与交互</th></tr>
<tr><td>① 启用引导页</td><td>三步卡片式引导:添加键盘 →(iOS)开启完全访问 → 试一试(内置输入框练习)。每步带系统设置深链与动图示意</td></tr>
<tr><td>② 登录页</td><td>单按钮"微信一键登录"(拉起微信 OpenSDK),登录后 token 写入 App Group 供键盘扩展使用</td></tr>
<tr><td>③ 套餐购买页</td><td>免费版 vs 会员对比表 + 月/年价格卡片,点击拉起微信 APP 支付,支付回调后即时解锁</td></tr>
<tr><td>④ 我的</td><td>本月用量进度条、订阅到期时间、麦克风/键盘权限自检(异常时红点提示并跳设置)</td></tr>
</table>
<!-- ---- 8.4 iPad ---- -->
<h3>8.4 iPad 适配要点</h3>
<ul>
<li>与 iPhone 同一个 Keyboard Extension target,键盘布局按 size class 自适应(横屏时麦克风键加宽、功能键拉开间距);</li>
<li>支持悬浮键盘(floating keyboard)形态下的紧凑布局;</li>
<li>主 App 用 SwiftUI 自适应布局,无需单独开发。</li>
</ul>
</section>
<!-- ============ 9. 延迟 ============ -->
<section class="card" id="latency">
<h2>九、延迟优化清单(贯穿各端)</h2>
<ol>
<li>客户端 WS 启动即预连接 + 心跳保活,按键时零握手;</li>
<li>100ms 帧直推,客户端不做缓冲聚合;</li>
<li>后端与 ASR 同区域部署,中继 goroutine 零拷贝转发;</li>
<li>浮层/键盘 UI 在按键瞬间先进入"聆听中"态,不等首个结果;</li>
<li>埋点指标:按键 → 首个 partial(目标 <strong>P95 &lt; 500ms</strong>)、松开 → final 注入(目标 <strong>&lt; 300ms</strong>)。</li>
</ol>
</section>
<!-- ============ 10. 仓库 ============ -->
<section class="card" id="repo">
<h2>十、仓库结构(monorepo</h2>
<pre class="code">dudu/
server/ Go 后端
desktop/ Tauri 2Mac + Windows
android/ Kotlin:主 App + IME Service
ios/ Swift:主 App + Keyboard Extension
docs/ 协议文档、架构决策记录</pre>
</section>
<!-- ============ 11. 里程碑 ============ -->
<section class="card" id="milestones">
<h2>十一、里程碑</h2>
<table>
<tr><th style="width:18%">阶段</th><th>内容</th><th style="width:12%">周期</th></tr>
<tr>
<td><span class="tag red">M0 风险验证</span></td>
<td>① iOS 键盘扩展录音 spike;② Gummy 流式接入 demo;③ macOS 注入 + 不抢焦点浮层 demo;④ <strong>立即启动微信开放平台(网站应用 + 移动应用)和商户号申请</strong>(周期数周,是关键路径;输入法上架需软著)</td>
<td>1 周</td>
</tr>
<tr>
<td><span class="tag blue">M1 核心链路</span></td>
<td>后端 auth / gateway / quota + 桌面端完整链路(快捷键 → 采集 → 流式 → 浮层 → 注入)</td>
<td>23 周</td>
</tr>
<tr>
<td><span class="tag blue">M2 商业化</span></td>
<td>微信登录(扫码 + App)、微信支付、时长包与配额</td>
<td>2 周</td>
</tr>
<tr>
<td><span class="tag blue">M3 移动端</span></td>
<td>先 Android IME,后 iOS 键盘扩展</td>
<td>34 周</td>
</tr>
<tr>
<td><span class="tag green">M4 打磨</span></td>
<td>断网重连、错误兜底、延迟调优、内测分发</td>
<td>持续</td>
</tr>
</table>
</section>
<!-- ============ 12. 验证 ============ -->
<section class="card" id="verify">
<h2>十二、风险与验证</h2>
<h3>主要风险</h3>
<table>
<tr><th style="width:34%">风险</th><th style="width:12%">等级</th><th>缓解措施</th></tr>
<tr><td>iOS 键盘扩展麦克风限制</td><td><span class="tag red"></span></td><td>M0 第一周 spike 验证;降级方案:deep link 跳主 App 录音回传</td></tr>
<tr><td>微信开放平台 / 商户号申请周期长</td><td><span class="tag orange"></span></td><td>第一天就启动申请(需企业资质、软著),与开发并行</td></tr>
<tr><td>macOS 文字注入在部分应用不兼容</td><td><span class="tag orange"></span></td><td>剪贴板 + 粘贴方案兼容面最广;M0 在微信/浏览器/IDE 实测</td></tr>
<tr><td>ASR 厂商限流 / 故障</td><td><span class="tag green"></span></td><td>Provider 抽象保证可热切到火山引擎备选</td></tr>
</table>
<h3>验证方式</h3>
<ul>
<li><strong>后端</strong><code class="inline">go test ./...</code>;测试脚本推送 wav 文件模拟音频流,断言收到 partial/final 序列;微信支付用沙箱/模拟回调测试订单状态机;</li>
<li><strong>桌面端</strong>:本地起 server 真实说话验证全链路;在备忘录/浏览器/IDE 等不同应用验证注入兼容性;记录 P95 首字延迟;</li>
<li><strong>移动端</strong>:Android 真机在微信/备忘录中切换到本输入法验证上屏;iOS TestFlight 验证完全访问权限流程;</li>
<li><strong>计费</strong>:构造超额场景验证 <code class="inline">QUOTA_EXCEEDED</code> 下发与客户端提示。</li>
</ul>
</section>
<footer>dudu 语音输入法 · MVP 设计方案 v0.1 · 2026-06-11</footer>
</div>
</body>
</html>