duaer-spec 0.25.1 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/.claude/rules/ai-ui-copy.md +2 -2
  2. package/.cursor/rules/ai-ui-copy.mdc +3 -3
  3. package/.cursor/rules/fde-8787-only.mdc +7 -7
  4. package/.duaer/active-job.json +3 -3
  5. package/.duaer/specs/252-release-0251/delivery.json +14 -0
  6. package/.duaer/specs/252-release-0251/tasks.md +1 -1
  7. package/.duaer/specs/253-docs-cursor-claude/delivery.json +9 -0
  8. package/.duaer/specs/253-docs-cursor-claude/spec.md +25 -0
  9. package/.duaer/specs/253-docs-cursor-claude/tasks.md +3 -0
  10. package/.duaer/specs/254-console-rename/delivery.json +13 -0
  11. package/.duaer/specs/254-console-rename/spec.md +27 -0
  12. package/.duaer/specs/254-console-rename/tasks.md +4 -0
  13. package/.duaer/specs/255-dispatch-arch-view/delivery.json +13 -0
  14. package/.duaer/specs/255-dispatch-arch-view/spec.md +16 -0
  15. package/.duaer/specs/255-dispatch-arch-view/tasks.md +4 -0
  16. package/.duaer/specs/256-chat-voice/delivery.json +13 -0
  17. package/.duaer/specs/256-chat-voice/spec.md +15 -0
  18. package/.duaer/specs/256-chat-voice/tasks.md +6 -0
  19. package/.duaer/specs/257-composer-voice-compact/delivery.json +9 -0
  20. package/.duaer/specs/257-composer-voice-compact/spec.md +13 -0
  21. package/.duaer/specs/257-composer-voice-compact/tasks.md +4 -0
  22. package/.duaer/specs/258-chat-stt/delivery.json +14 -0
  23. package/.duaer/specs/258-chat-stt/spec.md +16 -0
  24. package/.duaer/specs/258-chat-stt/tasks.md +5 -0
  25. package/.duaer/specs/259-voice-guide-stt/delivery.json +13 -0
  26. package/.duaer/specs/259-voice-guide-stt/spec.md +13 -0
  27. package/.duaer/specs/259-voice-guide-stt/tasks.md +4 -0
  28. package/.duaer/specs/260-release-026/delivery.json +18 -0
  29. package/.duaer/specs/260-release-026/spec.md +12 -0
  30. package/.duaer/specs/260-release-026/tasks.md +5 -0
  31. package/ADOPT.md +7 -4
  32. package/AGENTS.md +2 -2
  33. package/CHANGELOG.md +38 -0
  34. package/README.md +29 -25
  35. package/README.zh-CN.md +21 -21
  36. package/bin/duaer-live.mjs +186 -8
  37. package/bin/live-bug-context.mjs +1 -1
  38. package/bin/live-deliverables.mjs +1 -1
  39. package/bin/live-verify-gate.mjs +2 -2
  40. package/docs/agent/e2e-test-plan.md +8 -0
  41. package/docs/agent/live-desk.md +9 -6
  42. package/docs/agent/worker-models.md +7 -5
  43. package/docs/agent/worker-models.zh-CN.md +5 -3
  44. package/package.json +5 -5
  45. package/web/live-dev/app.js +386 -0
  46. package/web/live-dev/dispatch-center.html +25 -1
  47. package/web/live-dev/dispatch-center.js +113 -16
  48. package/web/live-dev/employee-catalog.mjs +2 -2
  49. package/web/live-dev/i18n-de.js +32 -8
  50. package/web/live-dev/i18n-es.js +32 -8
  51. package/web/live-dev/i18n-fr.js +32 -8
  52. package/web/live-dev/i18n-ja.js +32 -8
  53. package/web/live-dev/i18n-ko.js +32 -8
  54. package/web/live-dev/i18n-pt-BR.js +32 -8
  55. package/web/live-dev/i18n-ru.js +32 -8
  56. package/web/live-dev/i18n-vi.js +32 -8
  57. package/web/live-dev/i18n-zh-TW.js +34 -10
  58. package/web/live-dev/i18n.js +67 -19
  59. package/web/live-dev/index.html +55 -9
  60. package/web/live-dev/styles.css +59 -3
package/README.zh-CN.md CHANGED
@@ -6,11 +6,11 @@
6
6
 
7
7
  **Duaer** 把 AI 编程助手变成能**满意交付**的**数字员工**:你用自然语言说意图,它先把需求规范成可检查的 Brief(目标 / 边界 / 验收),再按范围实现,对照验收通过才算完成。
8
8
 
9
- 主打的是 **规范 → 满意交付**——不是让人学会操作 Spec 阶段或 slash 命令。
9
+ 主打的是 **规范 → 满意交付**——不是让人学会 Spec 阶段、slash 命令,或一整套专业交付流程。
10
10
 
11
- 你**不用**操作流程。装一次之后,在 **Cursor** / **Claude Code** / **Codex** / **Copilot** / **Windsurf** / **Cline** / **Continue** / **Gemini** / **Aider** 里说话即可。
11
+ 你**不必懂**那些流程。装一次之后,在 **Cursor** 或 **Claude Code** 里用平常话交代即可;流程由数字员工自己跑。
12
12
 
13
- `init` / `update` 会写入各宿主适配文件(详见 [`ADOPT.md`](ADOPT.md))。
13
+ `init` / `update` 还会给其它编辑器写可选适配(见 [`ADOPT.md`](ADOPT.md));控制台派工只启动 **Cursor Agent** 或 **Claude Code**。
14
14
 
15
15
  ## 我们有什么
16
16
 
@@ -21,29 +21,29 @@
21
21
  - **你平常怎么说,就怎么交代。** 不用记 slash 命令、不用自己点「写 Spec / 拆任务」。
22
22
  - **先谈清楚再动手。** 它会把你的想法整理成:要做什么、不做什么、怎样算做完。
23
23
  - **做完才算完。** 对照验收通过才会说「交付好了」;半成品不会假装完成。
24
- - **多宿主可用。** Cursor、Claude Code、Codex、Copilot、Windsurf、Cline、Continue、Gemini、Aider 都行。
24
+ - **支持 Cursor 与 Claude Code。** 装一次后直接在里面说话,不必学专业交付流程。(其它编辑器可选适配见 [`ADOPT.md`](ADOPT.md)。)
25
25
 
26
- ### 在浏览器里:Duaer-spec FDE(现场台面)
26
+ ### 在浏览器里:Duaer-spec FDE(现场控制台)
27
27
 
28
- 本机开一个网页台面(`duaer live`),专门管「从聊需求到派工、看进度、看结果」:
28
+ 本机开一个网页控制台(`duaer live`),专门管「从聊需求到派工、看进度、看结果」:
29
29
 
30
30
  - **项目管理。** 建项目、切换项目;每个项目自己的对话和进度,互不搅在一起。
31
- - **聊着把需求聊清楚。** 左边聊天;中间把目标、边界、验收写成确认卡;空话验收过不了,得能检查才给确认。
31
+ - **聊着把需求聊清楚。** 左边聊天;中间把目标、边界、验收写成确认卡;空话验收过不了,得能检查才给确认。发送旁可点 **录音**;在设置里配置 **语音识别模型**(兼容 OpenAI `/audio/transcriptions`,如 whisper-1)后,录完会解码成文字填入输入框,改完再发送。
32
32
  - **大需求可拆成多个模块。** 一块一块确认,确认完一块不会立刻开工,全部谈妥再一起派。
33
33
  - **系统架构图。** 确认需求后一起看架构;点图可全屏查看;架构确认后才能派工。若对话称已生成但中间栏没有图,同一条气泡会改成提示并提供「重新生成架构图」,不会留下假的「已生成」。
34
34
  - **一键派数字员工写代码。** 选产品目录、选 Cursor Agent 或 Claude Code,点开工;它在隔离目录里改代码,不直接乱动你的主分支。
35
- - **任务进度看得见。** 任务拆成一条条可勾选项;做完勾一项,台面实时刷新;有依赖的任务会排队放行,不会一窝蜂乱做。
35
+ - **任务进度看得见。** 任务拆成一条条可勾选项;做完勾一项,控制台实时刷新;有依赖的任务会排队放行,不会一窝蜂乱做。
36
36
  - **数字员工分工。** 顶部有「数字员工」目录(实现 / 功能回归 / 部署);可开多个并行(同一套 CLI)。详见下一节。
37
- - **中 / 繁中 / 英 / 日 / 韩 / 西 / 葡 / 法 / 德 / 俄 / 越界面。** 台面文案可切换语种。
37
+ - **中 / 繁中 / 英 / 日 / 韩 / 西 / 葡 / 法 / 德 / 俄 / 越界面。** 控制台文案可切换语种。
38
38
  - **看交付物和结果。** 可打开交付物页(需求时间线、架构、任务、交付记录);做完了可打开页面或项目文件夹。
39
39
  - **不满意就继续改。** 左侧继续聊改进 → 再确认 → 同一次任务上续派,不用从头建仓。部署后结果栏仍可「再改一版」和「修 bug」。
40
40
  - **修 bug。** 走缺陷卡(不拆多模块),默认跳过架构,短任务池,开 `fix/` 分支(可选从 `main` 出紧急修复)。交付过的地址/启动信息会自动带入;缺陷和迭代排在同一条项目时间线上。
41
41
  - **快速导航。** 对话栏右缘「导航」可跳到:对话底部 · 确认卡 · 结果 · 进度。
42
- - **调度中心。** 独立页看派工图,顶栏与台面一致;长任务链会换行,节点显示实时状态。
42
+ - **调度中心。** 独立页看派工图与各项目的系统架构,顶栏与控制台一致;长任务链会换行,节点显示实时状态。
43
43
 
44
44
  ### 数字员工:三种角色
45
45
 
46
- 台面顶部点「数字员工」可看目录。运行时仍是 Cursor Agent 或 Claude Code;差别在**任务角色**——谁做实现、谁做回归、谁做部署。
46
+ 控制台顶部点「数字员工」可看目录。运行时仍是 Cursor Agent 或 Claude Code;差别在**任务角色**——谁做实现、谁做回归、谁做部署。
47
47
 
48
48
  | 角色 | 干什么 | 不干什么 |
49
49
  |---|---|---|
@@ -53,19 +53,19 @@
53
53
 
54
54
  多人并行时:默认末位泳道承担**功能回归**(以及部署任务);实现任务在前面的泳道。一个人时:同一 CLI 会话按任务角色依次做完。
55
55
 
56
- 台面在放行前会自己跑产品仓 `.duaer/memory/verify.json` 里的命令。退出码不是 0,或没有这份契约,就不会保持「已交付」。纯文档任务要显式写 `{ "waiver": "docs-only" }`。
56
+ 控制台在放行前会自己跑产品仓 `.duaer/memory/verify.json` 里的命令。退出码不是 0,或没有这份契约,就不会保持「已交付」。纯文档任务要显式写 `{ "waiver": "docs-only" }`。
57
57
 
58
58
  ### 部署:支持的几种方式
59
59
 
60
- 开工前在台面选 **计划托管平台**。选好后,数字员工按该平台约束写代码,并由**部署员工**负责上线(把公网地址写进交付预览)。
60
+ 开工前在控制台选 **计划托管平台**。选好后,数字员工按该平台约束写代码,并由**部署员工**负责上线(把公网地址写进交付预览)。
61
61
 
62
62
  | 方式 | 适合做什么 | 你要准备什么 |
63
63
  |---|---|---|
64
64
  | **暂不部署** | 只在本地做完、先不发公网 | 无 |
65
65
  | **GitHub Pages** | 静态站 / 文档站;未选平台但文案要上线时的默认 | GitHub 仓库与 `gh` 登录 |
66
- | **Cloudflare** | 静态站用 Pages;接口 / 边缘逻辑用 Workers | 在台面设置里保存 API Token 与 Account ID |
67
- | **阿里云** | 静态多用 OSS(+ CDN);接口可用函数计算等 | 在台面设置里保存 AccessKey |
68
- | **AWS** | 静态多用 S3 + CloudFront;也可用 Amplify / Lambda 等 | 在台面设置里保存 Access Key |
66
+ | **Cloudflare** | 静态站用 Pages;接口 / 边缘逻辑用 Workers | 在控制台设置里保存 API Token 与 Account ID |
67
+ | **阿里云** | 静态多用 OSS(+ CDN);接口可用函数计算等 | 在控制台设置里保存 AccessKey |
68
+ | **AWS** | 静态多用 S3 + CloudFront;也可用 Amplify / Lambda 等 | 在控制台设置里保存 Access Key |
69
69
 
70
70
  细则:[`docs/agent/deploy-targets.md`](docs/agent/deploy-targets.md) · GitHub 默认路径:[`docs/agent/deploy-github.md`](docs/agent/deploy-github.md)
71
71
 
@@ -120,7 +120,7 @@ npx duaer-spec update
120
120
  **Duaer-spec FDE** = Field Development Environment(现场开发)。
121
121
 
122
122
  ```bash
123
- # 启动(任意目录都行,不会写入当前项目;未配模型也会打开台面)
123
+ # 启动(任意目录都行,不会写入当前项目;未配模型也会打开控制台)
124
124
  duaer live
125
125
 
126
126
  # 在打开的页面「设置」里填模型,或用 CLI:
@@ -134,17 +134,17 @@ duaer live config --base-url https://api.openai.com/v1 --api-key sk-... --model
134
134
  ```
135
135
 
136
136
  启动后会自动打开浏览器(默认 `http://127.0.0.1:8787`)。未配置模型时请在页面
137
- **设置**里填写并保存。台面**全屏宽**,三列:
137
+ **设置**里填写并保存。控制台**全屏宽**,三列:
138
138
  **对话** | **模块 / 确认 / 架构 / 开工 / 改进** | **任务进度**。细则见
139
139
  [`docs/agent/live-desk.md`](docs/agent/live-desk.md)。
140
140
 
141
141
  **验收(维护者 / 数字员工):** 改 Duaer-spec FDE UI 或确认校验门禁后,跑
142
- `npm run test:live`(L3 冒烟:台面结构标记 + `/api/validate`,内置 mock 模型,
142
+ `npm run test:live`(L3 冒烟:控制台结构标记 + `/api/validate`,内置 mock 模型,
143
143
  不产生付费 API 调用)。
144
144
 
145
145
  **流程**
146
146
 
147
- 1. 打开或新建**项目**,再聊天。右上角「项目」列表会显示每个项目的**交付状态**;有内容时可直接打开**交付物**。话题可以乱跳:台面会演进 **`modules[]`**(模块页签)。气泡支持 **Markdown**(加粗、代码、链接)。
147
+ 1. 打开或新建**项目**,再聊天。右上角「项目」列表会显示每个项目的**交付状态**;有内容时可直接打开**交付物**。话题可以乱跳:控制台会演进 **`modules[]`**(模块页签)。气泡支持 **Markdown**(加粗、代码、链接)。
148
148
  2. **按模块**填确认卡(目标 / 不做 / **可检查验收** / 假设)。空泛验收过不了门禁;须校验通过才能点确认。确认只**锁住该模块卡**,**不会**立刻写 Brief 或开工。模块之间可随时切换。
149
149
  3. 关心的模块都确认完 → 审 / 确认**架构**(Archify)→ **开工(kickoff)**。开工才写 Brief,建带 **`dependsOn` 的任务池**,并分派。默认 **1** 个数字员工;可选 **N** 个同 CLI 并行。共享任务给 1 号;模块任务轮询。每个 worker 独占一条 **Terminal 队列泳道**(`live-terminal` / `live-terminal/w2`…)。
150
150
  4. 浏览 / 扫描 / 点选产品仓,或粘贴路径 / 项目名。路径不存在会自动创建;可先设**产品父目录**,再填短名在其下新建。非 git 目录会 `git init -b develop`;没有 develop/main/master 会建本地 `develop`。
@@ -155,7 +155,7 @@ duaer live config --base-url https://api.openai.com/v1 --api-key sk-... --model
155
155
  6. 编辑「启动命令」(须以 `Duaer` 开头)→ 开工建 `.worktree/feat-*`,并为每条 worker 泳道打开 **Terminal**。
156
156
  7. 右侧进度列先提示**下一步**(阶段条),并轮询 `tasks.md`。进度上方 **查看交付物** 会打开生成的 HTML 页(需求文档时间线、确认书、架构、任务池、交付等)。交付时数字员工先更新产品仓 **README**,再把 `delivery.json` 标为 `accepted`。然后出现 **结果**(有页面时 **查看结果**;否则 **打开项目目录**)。
157
157
  8. 成品不满意:点 **继续改进(左侧对话)** → 确认改进卡(同样须校验)→ 再确认架构(保留或重画)→ 同 worktree 续派(先清残留 Agent 再入队)。进度跟本轮 `R{n}-*`。启动失败会回滚本轮 Brief Revision;状态行按泳道显示 Terminal 忙闲/队列。
158
- 9. 若需要上线 / 公网:在台面选 **计划托管平台**(Cloudflare / 阿里云 / AWS / GitHub Pages),说明见 [`docs/agent/deploy-targets.md`](docs/agent/deploy-targets.md)。未指定时默认 **GitHub CLI(`gh`)+ Actions**([`docs/agent/deploy-github.md`](docs/agent/deploy-github.md))
158
+ 9. 若需要上线 / 公网:在控制台选 **计划托管平台**(Cloudflare / 阿里云 / AWS / GitHub Pages),说明见 [`docs/agent/deploy-targets.md`](docs/agent/deploy-targets.md)。未指定时默认 **GitHub CLI(`gh`)+ Actions**([`docs/agent/deploy-github.md`](docs/agent/deploy-github.md))
159
159
 
160
160
  ### 文档语言
161
161
 
@@ -354,6 +354,9 @@ function emptyLiveConfig() {
354
354
  baseUrl: "",
355
355
  apiKey: "",
356
356
  model: "",
357
+ sttBaseUrl: "",
358
+ sttApiKey: "",
359
+ sttModel: "",
357
360
  preferredAgentId: "",
358
361
  projectsRoot: "",
359
362
  activeProjectPath: "",
@@ -383,6 +386,15 @@ function readConfig() {
383
386
  baseUrl: String(raw.baseUrl || process.env.DUAER_LIVE_BASE_URL || "").trim(),
384
387
  apiKey: String(raw.apiKey || process.env.DUAER_LIVE_API_KEY || "").trim(),
385
388
  model: String(raw.model || process.env.DUAER_LIVE_MODEL || "").trim(),
389
+ sttBaseUrl: String(
390
+ raw.sttBaseUrl || process.env.DUAER_LIVE_STT_BASE_URL || "",
391
+ ).trim(),
392
+ sttApiKey: String(
393
+ raw.sttApiKey || process.env.DUAER_LIVE_STT_API_KEY || "",
394
+ ).trim(),
395
+ sttModel: String(
396
+ raw.sttModel || process.env.DUAER_LIVE_STT_MODEL || "",
397
+ ).trim(),
386
398
  preferredAgentId: String(raw.preferredAgentId || "").trim(),
387
399
  projectsRoot: String(raw.projectsRoot || "").trim(),
388
400
  activeProjectPath: String(raw.activeProjectPath || "").trim(),
@@ -427,6 +439,9 @@ function writeConfig(partial) {
427
439
  baseUrl: pick("baseUrl"),
428
440
  apiKey: pick("apiKey"),
429
441
  model: pick("model"),
442
+ sttBaseUrl: pick("sttBaseUrl"),
443
+ sttApiKey: pick("sttApiKey"),
444
+ sttModel: pick("sttModel"),
430
445
  preferredAgentId: pick("preferredAgentId"),
431
446
  projectsRoot:
432
447
  partial.projectsRoot !== undefined
@@ -452,6 +467,21 @@ function configReady(cfg = readConfig()) {
452
467
  return Boolean(cfg.baseUrl && cfg.apiKey && cfg.model);
453
468
  }
454
469
 
470
+ function sttConfigReady(cfg = readConfig()) {
471
+ return Boolean(cfg.sttBaseUrl && cfg.sttApiKey && cfg.sttModel);
472
+ }
473
+
474
+ /** OpenAI-compatible audio transcriptions URL from a Base URL. */
475
+ function sttTranscriptionsUrl(baseUrl) {
476
+ const base = String(baseUrl || "")
477
+ .trim()
478
+ .replace(/\/$/, "");
479
+ if (!base) return "";
480
+ if (/\/audio\/transcriptions$/i.test(base)) return base;
481
+ if (/\/v1$/i.test(base)) return `${base}/audio/transcriptions`;
482
+ return `${base}/v1/audio/transcriptions`;
483
+ }
484
+
455
485
  /** Both AccessKey ID and Secret present → 阿里云 may appear in deploy UI. */
456
486
  function hasAliyunCredentials(cfg = readConfig()) {
457
487
  return Boolean(
@@ -560,6 +590,10 @@ function publicConfig(cfg = readConfig()) {
560
590
  baseUrl: cfg.baseUrl || "",
561
591
  model: cfg.model || "",
562
592
  hasApiKey: Boolean(cfg.apiKey),
593
+ sttReady: sttConfigReady(cfg),
594
+ sttBaseUrl: cfg.sttBaseUrl || "",
595
+ sttModel: cfg.sttModel || "",
596
+ hasSttApiKey: Boolean(cfg.sttApiKey),
563
597
  hasAliyunCredentials: hasAliyunCredentials(cfg),
564
598
  hasCloudflareCredentials: hasCloudflareCredentials(cfg),
565
599
  hasAwsCredentials: hasAwsCredentials(cfg),
@@ -718,7 +752,7 @@ const BUG_CHAT_PROMPT = `你是「Duaer-spec FDE」缺陷助手。用户要修 b
718
752
  - outOfScope = 本次不改什么
719
753
  - acceptance = 怎么算修好(复现关闭 + 可核对结果/命令)
720
754
  - assumptions = 环境 / 疑似原因 / 是否线上紧急(优先写入系统给的「项目交付上下文」)
721
- 4. **禁止向用户追问**网页/成品 URL、服务启动命令或脚本、机器/端口/依赖等运行环境、疑似原因、是否线上紧急——这些由台面从项目交付自动补齐。疑似原因未知时写「待复现定位」;线上紧急默认「否」(从 develop 出 fix/),仅当用户明确说线上/生产/紧急才标「是」。
755
+ 4. **禁止向用户追问**网页/成品 URL、服务启动命令或脚本、机器/端口/依赖等运行环境、疑似原因、是否线上紧急——这些由控制台从项目交付自动补齐。疑似原因未知时写「待复现定位」;线上紧急默认「否」(从 develop 出 fix/),仅当用户明确说线上/生产/紧急才标「是」。
722
756
  5. ready=true 表示缺陷卡可确认。不要催派工。不要写代码。不要假设用户没给的仓库路径(上下文里有则直接用)。
723
757
  6. 只要问题是让用户做选择,必须在 options 填 2~5 个短选项(≤20字)。
724
758
  7. 输出格式(严格):
@@ -795,7 +829,7 @@ const BUG_ACCEPT_PROMPT = `你是「Duaer-spec FDE」缺陷验收官。用户即
795
829
  1. goal 是否写清现象与复现(可执行,不要堆无关功能)
796
830
  2. acceptance 是否可客观检查:复现关闭 + 打开/看到/命令通过等;禁止仅「修好了/更好用」
797
831
  3. outOfScope 是否划清本次不改什么(可简短)
798
- 4. assumptions:环境/URL/启动/紧急/疑似原因由台面交付上下文补齐——**禁止**因缺少网页地址、启动命令、运行环境、疑似原因、是否线上紧急而 failed
832
+ 4. assumptions:环境/URL/启动/紧急/疑似原因由控制台交付上下文补齐——**禁止**因缺少网页地址、启动命令、运行环境、疑似原因、是否线上紧急而 failed
799
833
 
800
834
  规则:
801
835
  - 若小改即可通过:修订四块,passed=true;assumptions 空缺时写入合理默认(疑似原因=待复现定位;线上紧急=否)
@@ -823,6 +857,69 @@ function llmTimeoutError(ms) {
823
857
  );
824
858
  }
825
859
 
860
+ /**
861
+ * POST audio to OpenAI-compatible /audio/transcriptions.
862
+ * @param {object} cfg
863
+ * @param {{ buffer: Buffer, mimeType?: string, filename?: string, language?: string }} audio
864
+ */
865
+ async function callSttTranscribe(cfg, audio) {
866
+ const url = sttTranscriptionsUrl(cfg.sttBaseUrl);
867
+ if (!url) throw new Error("STT baseUrl missing");
868
+ const mime = String(audio.mimeType || "audio/webm").trim() || "audio/webm";
869
+ const filename =
870
+ String(audio.filename || "").trim() ||
871
+ (mime.includes("mp4") || mime.includes("m4a")
872
+ ? "voice.m4a"
873
+ : mime.includes("mpeg") || mime.includes("mp3")
874
+ ? "voice.mp3"
875
+ : mime.includes("wav")
876
+ ? "voice.wav"
877
+ : mime.includes("ogg")
878
+ ? "voice.ogg"
879
+ : "voice.webm");
880
+ const form = new FormData();
881
+ form.append("file", new Blob([audio.buffer], { type: mime }), filename);
882
+ form.append("model", String(cfg.sttModel).trim());
883
+ const lang = String(audio.language || "").trim();
884
+ if (lang) form.append("language", lang);
885
+ const ac = new AbortController();
886
+ const timer = setTimeout(() => ac.abort(), LLM_TIMEOUT_MS);
887
+ try {
888
+ const res = await fetch(url, {
889
+ method: "POST",
890
+ headers: {
891
+ authorization: `Bearer ${cfg.sttApiKey}`,
892
+ },
893
+ body: form,
894
+ signal: ac.signal,
895
+ });
896
+ const text = await res.text();
897
+ let data = null;
898
+ try {
899
+ data = text ? JSON.parse(text) : null;
900
+ } catch {
901
+ data = null;
902
+ }
903
+ if (!res.ok) {
904
+ const msg =
905
+ (data && (data.error?.message || data.error || data.message)) ||
906
+ text.slice(0, 240) ||
907
+ `HTTP ${res.status}`;
908
+ throw new Error(String(msg));
909
+ }
910
+ const transcript = String(
911
+ data?.text || data?.transcript || data?.result || "",
912
+ ).trim();
913
+ if (!transcript) throw new Error("empty transcript");
914
+ return transcript;
915
+ } catch (err) {
916
+ if (err?.name === "AbortError") throw llmTimeoutError(LLM_TIMEOUT_MS);
917
+ throw err;
918
+ } finally {
919
+ clearTimeout(timer);
920
+ }
921
+ }
922
+
826
923
  async function callChatModel(cfg, messages, systemPrompt = SYSTEM_PROMPT) {
827
924
  const base = cfg.baseUrl.replace(/\/$/, "");
828
925
  const url = `${base}/chat/completions`;
@@ -3817,7 +3914,7 @@ Brief: ${featureDir}
3817
3914
  4. 每完成 tasks.md 中的一步,立刻把该行改成 - [x](Duaer-spec FDE 靠此显示细粒度进度与编排放行)
3818
3915
  4a. 只做当前编排波次已放行的任务;未满足 dependsOn / 未放行的任务不要开工;本波勾完后:若还有后续波次则输出「本波完成,退出等编排器」并立刻结束本次 CLI 会话(不要挂起;编排器用 --continue 放行);若已无后续波次则继续完成 stamp accepted
3819
3916
  4b. 拆任务(强制):必须拆到原子任务且每条可独立验证;每个勾选项只覆盖一个验收点;进度只能用 tasks.md 的 - [ ]/- [x] 监控(立刻勾选,不要攒到最后);不要把多项验收揉进同一条;不要人为限制条数。若仍偏粗,先按 Acceptance 扩成「一条验收一勾选」(仍用 T00x),保存后再做
3820
- 5. 对照 Acceptance 全部满足后,才 stamp ${path.join(featureDir, "delivery.json")} 为 accepted——tasks.md 全部勾完还不够,必须 stamp;禁止停在 Job not accepted yet。台面会按产品仓 .duaer/memory/verify.json 自己跑 commands;退出码非 0 或缺少契约时会把 accepted 打回 open。不要把已有 commands 改成 waiver 来跳过
3917
+ 5. 对照 Acceptance 全部满足后,才 stamp ${path.join(featureDir, "delivery.json")} 为 accepted——tasks.md 全部勾完还不够,必须 stamp;禁止停在 Job not accepted yet。控制台会按产品仓 .duaer/memory/verify.json 自己跑 commands;退出码非 0 或缺少契约时会把 accepted 打回 open。不要把已有 commands 改成 waiver 来跳过
3821
3918
  5b. 交付前必须更新产品仓 README(说明文档):与本次交付一致——做什么、模块/验收要点、如何运行或打开;需求变了就改 README,不要只改代码。英文 README 不得出现中文;若项目是中文说明则用 README.zh-CN.md(或项目既有约定),可夹英文术语
3822
3919
  6. 必须在 delivery.json 写入 preview.url(满意交付的必填证据):必须是可打开的成品入口——HTTP 服务用 http://localhost:…;静态页用 index.html 等 HTML。禁止把 docs/**/*.md 等说明文档当作 preview.url——不要因「没有页面」而省略
3823
3920
  6b. 若交付是 HTTP 服务:验收前必须先把服务跑起来(如 npm start),确认能打开 preview.url 后再 stamp accepted;不要只写地址却不启动
@@ -5884,7 +5981,9 @@ async function handleApi(req, res) {
5884
5981
  "awsRegion",
5885
5982
  "clearAwsCredentials",
5886
5983
  ];
5984
+ const sttKeys = ["sttBaseUrl", "sttApiKey", "sttModel", "clearStt"];
5887
5985
  const noHostCreds = hostCredKeys.every((k) => body[k] === undefined);
5986
+ const noStt = sttKeys.every((k) => body[k] === undefined);
5888
5987
  const onlyProjectsRoot =
5889
5988
  body.projectsRoot !== undefined &&
5890
5989
  body.baseUrl === undefined &&
@@ -5892,7 +5991,8 @@ async function handleApi(req, res) {
5892
5991
  body.model === undefined &&
5893
5992
  body.preferredAgentId === undefined &&
5894
5993
  body.activeProjectPath === undefined &&
5895
- noHostCreds;
5994
+ noHostCreds &&
5995
+ noStt;
5896
5996
  const onlyActiveProject =
5897
5997
  body.activeProjectPath !== undefined &&
5898
5998
  body.baseUrl === undefined &&
@@ -5900,7 +6000,8 @@ async function handleApi(req, res) {
5900
6000
  body.model === undefined &&
5901
6001
  body.preferredAgentId === undefined &&
5902
6002
  body.projectsRoot === undefined &&
5903
- noHostCreds;
6003
+ noHostCreds &&
6004
+ noStt;
5904
6005
  const onlyHostCreds =
5905
6006
  !noHostCreds &&
5906
6007
  body.baseUrl === undefined &&
@@ -5908,7 +6009,17 @@ async function handleApi(req, res) {
5908
6009
  body.model === undefined &&
5909
6010
  body.preferredAgentId === undefined &&
5910
6011
  body.projectsRoot === undefined &&
5911
- body.activeProjectPath === undefined;
6012
+ body.activeProjectPath === undefined &&
6013
+ noStt;
6014
+ const onlyStt =
6015
+ !noStt &&
6016
+ body.baseUrl === undefined &&
6017
+ body.apiKey === undefined &&
6018
+ body.model === undefined &&
6019
+ body.preferredAgentId === undefined &&
6020
+ body.projectsRoot === undefined &&
6021
+ body.activeProjectPath === undefined &&
6022
+ noHostCreds;
5912
6023
  let projectsRoot = body.projectsRoot;
5913
6024
  if (projectsRoot !== undefined) {
5914
6025
  const raw = String(projectsRoot || "").trim();
@@ -5966,6 +6077,23 @@ async function handleApi(req, res) {
5966
6077
  hostPartial.awsRegion = body.awsRegion;
5967
6078
  }
5968
6079
  }
6080
+ const sttPartial = {};
6081
+ if (body.clearStt) {
6082
+ sttPartial.sttBaseUrl = "";
6083
+ sttPartial.sttApiKey = "";
6084
+ sttPartial.sttModel = "";
6085
+ } else {
6086
+ if (body.sttBaseUrl !== undefined) {
6087
+ sttPartial.sttBaseUrl = body.sttBaseUrl;
6088
+ }
6089
+ if (body.sttModel !== undefined) {
6090
+ sttPartial.sttModel = body.sttModel;
6091
+ }
6092
+ if (body.sttApiKey !== undefined) {
6093
+ const sttKey = String(body.sttApiKey || "").trim();
6094
+ if (sttKey) sttPartial.sttApiKey = sttKey;
6095
+ }
6096
+ }
5969
6097
  const next = writeConfig({
5970
6098
  baseUrl: body.baseUrl,
5971
6099
  apiKey: body.apiKey,
@@ -5974,8 +6102,9 @@ async function handleApi(req, res) {
5974
6102
  projectsRoot,
5975
6103
  activeProjectPath,
5976
6104
  ...hostPartial,
6105
+ ...sttPartial,
5977
6106
  });
5978
- if (onlyProjectsRoot || onlyActiveProject || onlyHostCreds) {
6107
+ if (onlyProjectsRoot || onlyActiveProject || onlyHostCreds || onlyStt) {
5979
6108
  send(res, 200, publicConfig(next));
5980
6109
  return;
5981
6110
  }
@@ -5995,6 +6124,55 @@ async function handleApi(req, res) {
5995
6124
  return;
5996
6125
  }
5997
6126
 
6127
+ if (req.method === "POST" && url.pathname === "/api/transcribe") {
6128
+ const cfg = readConfig();
6129
+ if (!sttConfigReady(cfg)) {
6130
+ send(res, 400, {
6131
+ error: "请先在设置中配置语音识别模型(STT Base URL / API Key / Model)",
6132
+ code: "NEED_STT",
6133
+ ...publicConfig(cfg),
6134
+ });
6135
+ return;
6136
+ }
6137
+ try {
6138
+ const body = await readJson(req);
6139
+ const b64 = String(body.audioBase64 || body.audio || "").trim();
6140
+ if (!b64) {
6141
+ send(res, 400, { error: "audioBase64 required" });
6142
+ return;
6143
+ }
6144
+ const raw = b64.replace(/^data:[^;]+;base64,/, "");
6145
+ let buffer;
6146
+ try {
6147
+ buffer = Buffer.from(raw, "base64");
6148
+ } catch {
6149
+ send(res, 400, { error: "invalid audioBase64" });
6150
+ return;
6151
+ }
6152
+ if (!buffer.length) {
6153
+ send(res, 400, { error: "empty audio" });
6154
+ return;
6155
+ }
6156
+ if (buffer.length > 12 * 1024 * 1024) {
6157
+ send(res, 400, { error: "audio too large (max 12MB)" });
6158
+ return;
6159
+ }
6160
+ const text = await callSttTranscribe(cfg, {
6161
+ buffer,
6162
+ mimeType: String(body.mimeType || body.type || "").trim(),
6163
+ filename: String(body.filename || "").trim(),
6164
+ language: String(body.language || "").trim(),
6165
+ });
6166
+ send(res, 200, { text, model: cfg.sttModel });
6167
+ } catch (err) {
6168
+ send(res, 502, {
6169
+ error: err instanceof Error ? err.message : "transcribe failed",
6170
+ code: "STT_FAILED",
6171
+ });
6172
+ }
6173
+ return;
6174
+ }
6175
+
5998
6176
  if (req.method === "POST" && url.pathname === "/api/chat") {
5999
6177
  const cfg = readConfig();
6000
6178
  if (!configReady(cfg)) {
@@ -7358,7 +7536,7 @@ function serve(port) {
7358
7536
  console.log(`Brief 写入 ${jobsRoot()} (不会写入你当前业务仓库)`);
7359
7537
  if (!cfg.ready) {
7360
7538
  console.log(
7361
- `模型尚未配置 — 已打开台面,请在页面「设置」里填写模型后保存。`,
7539
+ `模型尚未配置 — 已打开控制台,请在页面「设置」里填写模型后保存。`,
7362
7540
  );
7363
7541
  } else {
7364
7542
  console.log(`模型 ${cfg.model} @ ${cfg.baseUrl}`);
@@ -127,7 +127,7 @@ export function collectBugProjectContext(input = {}) {
127
127
  */
128
128
  export function formatBugProjectContextBlock(ctx) {
129
129
  const c = ctx && typeof ctx === "object" ? ctx : {};
130
- const lines = ["【项目交付上下文——已从台面/交付得知,禁止再向用户追问这些项】"];
130
+ const lines = ["【项目交付上下文——已从控制台/交付得知,禁止再向用户追问这些项】"];
131
131
  if (c.projectPath) lines.push(`- 仓库路径: ${c.projectPath}`);
132
132
  if (c.previewUrl) lines.push(`- 成品/预览地址: ${c.previewUrl}`);
133
133
  else if (c.inferredPreviewUrl) {
@@ -419,7 +419,7 @@ export function buildDeliverablesModel(session, opts = {}) {
419
419
  ? pickLang(lang, {
420
420
  en: "Architecture confirmed on the desk.",
421
421
  ja: "デスクでアーキテクチャを確認済みです。",
422
- zh: "已在台面确认架构。",
422
+ zh: "已在控制台确认架构。",
423
423
  })
424
424
  : "",
425
425
  },
@@ -360,7 +360,7 @@ Brief: ${featureDir || ""}
360
360
  产品仓需要 \`.duaer/memory/verify.json\`。
361
361
  - 有可跑测试时写入 { "commands": ["<真实命令>"] }(例如 npm test)
362
362
  - 只有纯文档、没有可跑命令时才允许 { "waiver": "docs-only" }
363
- 不要 stamp delivery.json 为 accepted。台面会自己复跑;失败会把 accepted 打回 open。
363
+ 不要 stamp delivery.json 为 accepted。控制台会自己复跑;失败会把 accepted 打回 open。
364
364
  `;
365
365
  }
366
366
  return `Duaer
@@ -373,6 +373,6 @@ Brief: ${featureDir || ""}
373
373
  输出尾部:
374
374
  ${nudge.tail || "(无)"}
375
375
 
376
- 修好并使上述命令退出码为 0。不要把已有 commands 改成 waiver 来跳过。不要 stamp accepted;台面会复跑,通过后才允许 accepted。
376
+ 修好并使上述命令退出码为 0。不要把已有 commands 改成 waiver 来跳过。不要 stamp accepted;控制台会复跑,通过后才允许 accepted。
377
377
  `;
378
378
  }
@@ -165,6 +165,10 @@ the table for their app.
165
165
  | E2E-193 | Chat quick-nav stays visible inside chat column (not clipped/covered) | `npm run test:live` + manual |
166
166
  | E2E-194 | After deploy, result bar still offers 再改一版 and 修 bug | `npm run test:live` + manual |
167
167
  | E2E-195 | Architecture claim-ready without IR rewrites the same chat bubble (no leftover false “diagram ready”) + regenerate | `npm run test:live` + manual |
168
+ | E2E-196 | Dispatch center per-project view switch: Dispatch graph | Architecture (saved architecture.url) | `npm test` + manual |
169
+ | E2E-197 | Composer Record + Send in compact `.composer-actions`; with STT configured, stop → `/api/transcribe` fills `#input`; without STT, Record opens Settings speech-model block | `npm run test:live` + manual |
170
+ | E2E-198 | Settings speech recognition model block saves `sttBaseUrl` / `sttApiKey` / `sttModel`; publicConfig exposes `sttReady` | `npm test` + `npm run test:live` |
171
+ | E2E-199 | Record click with no STT shows bot guide and opens Settings → speech model | `npm run test:live` + manual |
168
172
 
169
173
  | E2E-156 | Accepted chat bubble preview URL is a clickable `<a>` (http(s) or /api/…) | `npm test` + `npm run test:live` |
170
174
  | E2E-157 | `http://host/api/result/…` chat links render as one clean `<a>` (no nested HTML garble) | `npm test` |
@@ -395,4 +399,8 @@ the table for their app.
395
399
  | E2E-193 | `248-quicknav-visible` | quick-nav not clipped by overflow |
396
400
  | E2E-194 | `249-result-cta-after-deploy` | result CTAs remain after deploy |
397
401
  | E2E-195 | `251-arch-claim-no-diagram` | rewrite claim-ready bubble when IR missing |
402
+ | E2E-196 | `255-dispatch-arch-view` | dispatch center per-project architecture view |
403
+ | E2E-197 | `256-chat-voice` / `257-composer-voice-compact` | composer voice / Record beside Send |
404
+ | E2E-198 | `258-chat-stt` | STT model settings + /api/transcribe |
405
+ | E2E-199 | `259-voice-guide-stt` | Record without STT opens Settings |
398
406
 
@@ -1,4 +1,4 @@
1
- # Duaer-spec FDE (live desk)
1
+ # Duaer-spec FDE (live console)
2
2
 
3
3
  Field Development Environment for digital employees. Durable state lives in
4
4
  `~/.duaer/live/desk.sqlite` (config, repos, jobs, project sessions). That
@@ -6,11 +6,11 @@ directory is **per-machine only** — it is never part of the npm package; each
6
6
  user starts with an empty live root. Product work runs in isolated worktrees
7
7
  inside the chosen product repo.
8
8
 
9
- `duaer live` starts the desk on **port 8787** (`http://127.0.0.1:8787`). A missing
9
+ `duaer live` starts the console on **port 8787** (`http://127.0.0.1:8787`). A missing
10
10
  model does not block startup — the Settings drawer opens so the operator can save
11
11
  credentials, then chat.
12
12
 
13
- **Agents:** do not start a second desk or use another port. Prefer LaunchAgent
13
+ **Agents:** do not start a second console or use another port. Prefer LaunchAgent
14
14
  `com.duaer.live8787`. Interactive `duaer live` may open the browser once;
15
15
  `DUAER_LIVE_NO_BROWSER=1` (set on the LaunchAgent) skips auto-open on restart.
16
16
 
@@ -22,7 +22,7 @@ Three columns:
22
22
 
23
23
  | Column | Role |
24
24
  |---|---|
25
- | Chat | Project dialogue; Markdown bubbles; messy multi-topic talk |
25
+ | Chat | Project dialogue; Markdown bubbles; messy multi-topic talk. Composer **Record** captures audio when Settings has an STT model (`sttBaseUrl` / `sttApiKey` / `sttModel`, OpenAI-compatible `/audio/transcriptions`); stop → `/api/transcribe` → text in `#input`. Without STT, Record opens Settings and prompts to configure. |
26
26
  | Center | Module tabs + confirm cards → architecture → kickoff / revise |
27
27
  | Progress | Delivery cockpit (stages + next action) · **View deliverables** · `tasks.md` polling · result bar |
28
28
 
@@ -162,7 +162,9 @@ Additional notes:
162
162
  - **Execution graph:** after worker-count confirm, the desk builds the task
163
163
  path with the **same Archify renderer and layered layout** as the system
164
164
  architecture diagram. **查看派工图** / **Dispatch center** open
165
- `/dispatch-center.html` (100px project rail + selected graph).
165
+ `/dispatch-center.html` (100px project rail + selected graph). Each project
166
+ can switch the stage to **Architecture** to view that project’s saved system
167
+ diagram (`architecture.url`).
166
168
  - Each worker has its own Terminal **queue lane** (`live-terminal`,
167
169
  `live-terminal/w2`, …) so parallel launches do not hit a single lock and exit.
168
170
 
@@ -183,7 +185,8 @@ Additional notes:
183
185
  Touching live desk UI / protocol UX: L0 + **`npm run test:live`** (L3) per
184
186
  [`.duaer/memory/testing.md`](../../.duaer/memory/testing.md). Catalog rows:
185
187
  E2E-135…E2E-139 (modular / pool / README-on-delivery / chat Markdown / lanes),
186
- E2E-145 (multi-worker progress lanes), E2E-146 (wave orchestration).
188
+ E2E-145 (multi-worker progress lanes), E2E-146 (wave orchestration),
189
+ E2E-197 (composer Record / voice input).
187
190
 
188
191
  ## Related
189
192
 
@@ -1,11 +1,11 @@
1
1
  # Worker CLIs and other models
2
2
 
3
3
  FDE launches **digital employees** with Terminal CLIs. That is separate from the
4
- **desk LLM** used for chat / confirm validation.
4
+ **console LLM** used for chat / confirm validation.
5
5
 
6
6
  | Layer | What | Examples |
7
7
  |---|---|---|
8
- | Desk LLM | OpenAI-compatible HTTP API for the FDE UI | DeepSeek API, OpenAI, custom gateway |
8
+ | Console LLM | OpenAI-compatible HTTP API for the FDE UI | DeepSeek API, OpenAI, custom gateway |
9
9
  | Worker CLI | Coding agent that edits the product worktree | **Cursor Agent**, **Claude Code** |
10
10
 
11
11
  On kickoff, FDE can launch **1..N** workers of the **same** CLI (not mixed).
@@ -18,7 +18,7 @@ Claude Code (or another supported agent) at DeepSeek’s official API.
18
18
 
19
19
  The FDE **Configure model** screen includes the same guide below the form.
20
20
 
21
- ## Desk LLM (FDE chat)
21
+ ## Console LLM (FDE chat)
22
22
 
23
23
  On the FDE setup page:
24
24
 
@@ -106,7 +106,9 @@ Official guide:
106
106
  DeepSeek also documents integrations for Codex and other tools:
107
107
  [Agent integrations](https://api-docs.deepseek.com/quick_start/agent_integrations/).
108
108
 
109
- FDE still only **auto-launches** Cursor Agent and Claude Code. Other hosts can
110
- run Duaer Briefs via their own adapters after `npx duaer-spec init --here`.
109
+ FDE still only **auto-launches** Cursor Agent and Claude Code. Day-to-day
110
+ product support is **Cursor** and **Claude Code** — talk in plain language; you
111
+ do not need a professional Spec workflow. Other editors may still receive
112
+ optional adapters from `npx duaer-spec init --here` (see [`ADOPT.md`](../../ADOPT.md)).
111
113
 
112
114
  Chinese version: [worker-models.zh-CN.md](worker-models.zh-CN.md).
@@ -1,13 +1,13 @@
1
1
  # 数字员工 CLI 与其他模型
2
2
 
3
- FDE 派工用的是 **Terminal 里的数字员工 CLI**,和台面聊天用的 **Desk 模型 API** 不是一回事。
3
+ FDE 派工用的是 **Terminal 里的数字员工 CLI**,和控制台聊天用的 **Desk 模型 API** 不是一回事。
4
4
 
5
5
  | 层级 | 作用 | 例子 |
6
6
  |---|---|---|
7
7
  | Desk 模型 | FDE 对话 / 确认卡校验(HTTP) | DeepSeek API、OpenAI、自建网关 |
8
8
  | 数字员工 CLI | 真正改产品仓库的 coding agent | **Cursor Agent**、**Claude Code** |
9
9
 
10
- 开工时可启 **1..N** 个**同一种** CLI(不可混用);并行时各占一条 Terminal 队列泳道。台面流程见英文 [`live-desk.md`](live-desk.md)。
10
+ 开工时可启 **1..N** 个**同一种** CLI(不可混用);并行时各占一条 Terminal 队列泳道。控制台流程见英文 [`live-desk.md`](live-desk.md)。
11
11
 
12
12
  第三方包 `deepseek` / `deepseek-tui`(现已改名 Codewhale)**不是** DeepSeek 官方 CLI,FDE **不再**把它当作数字员工。要用 DeepSeek **模型**写代码:把 Claude Code(或其它支持的工具)接到 DeepSeek 官方接口。
13
13
 
@@ -96,6 +96,8 @@ DeepSeek 提供 Anthropic 兼容接口。配好 Claude Code 后,在 FDE 派工
96
96
  DeepSeek 还写了 Codex 等工具的接入说明:
97
97
  [Agent integrations](https://api-docs.deepseek.com/quick_start/agent_integrations/)。
98
98
 
99
- FDE **自动派工**目前只支持 Cursor Agent 与 Claude Code。其它宿主可在 `npx duaer-spec init --here` 之后,用各自适配器跑 Duaer Brief。
99
+ FDE **自动派工**目前只支持 Cursor Agent 与 Claude Code。日常使用以 **Cursor**、
100
+ **Claude Code** 为准:用平常话交代即可,不必学专业 Spec 流程。其它编辑器仍可
101
+ 从 `npx duaer-spec init --here` 拿到可选适配(见 [`ADOPT.md`](../../ADOPT.md))。
100
102
 
101
103
  English: [worker-models.md](worker-models.md).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "duaer-spec",
3
- "version": "0.25.1",
4
- "description": "Norms → satisfactory delivery: digital employees for Cursor, Claude, Codex, Copilot, Windsurf, Cline, Continue, Gemini, Aider",
3
+ "version": "0.26.0",
4
+ "description": "Norms → satisfactory delivery: digital employees for Cursor and Claude Code — talk in plain language, no Spec workflow required",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "duaer": "bin/duaer.mjs",
@@ -34,12 +34,12 @@
34
34
  },
35
35
  "repository": {
36
36
  "type": "git",
37
- "url": "git+https://github.com/fujiezee/duaer-spec.git"
37
+ "url": "git+https://github.com/Duaer/duaer-spec.git"
38
38
  },
39
39
  "bugs": {
40
- "url": "https://github.com/fujiezee/duaer-spec/issues"
40
+ "url": "https://github.com/Duaer/duaer-spec/issues"
41
41
  },
42
- "homepage": "https://github.com/fujiezee/duaer-spec#readme",
42
+ "homepage": "https://github.com/Duaer/duaer-spec#readme",
43
43
  "keywords": [
44
44
  "duaer",
45
45
  "digital-employee",