oh-my-agent-web 0.3.0 → 0.5.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 (279) hide show
  1. package/README.md +108 -450
  2. package/dist/server/approval/read-only.js +107 -0
  3. package/dist/server/approval/rules.js +85 -0
  4. package/dist/server/approval/scope.js +150 -0
  5. package/dist/server/brand.js +2 -0
  6. package/dist/server/cli-config.js +52 -0
  7. package/dist/server/cli-info.js +101 -0
  8. package/dist/server/cli.js +304 -0
  9. package/dist/server/engine/builtin.js +84 -0
  10. package/dist/server/engine/catalog.js +8 -0
  11. package/dist/server/engine/codex-approvals.js +45 -0
  12. package/dist/server/engine/codex-map.js +298 -0
  13. package/dist/server/engine/codex-sessions.js +159 -0
  14. package/dist/server/engine/codex-transport.js +49 -0
  15. package/dist/server/engine/codex.js +314 -0
  16. package/dist/server/engine/dsh-history.js +195 -0
  17. package/dist/server/engine/dsh-log.js +245 -0
  18. package/dist/server/engine/dsh-map.js +221 -0
  19. package/dist/server/engine/dsh-sessions.js +207 -0
  20. package/dist/server/engine/dsh-transport.js +37 -0
  21. package/dist/server/engine/dsh.js +284 -0
  22. package/dist/server/engine/fake.js +231 -0
  23. package/dist/server/engine/jsonrpc-stdio.js +241 -0
  24. package/dist/server/engine/model-registry-reload.js +115 -0
  25. package/dist/server/engine/narrate-guidance.js +22 -0
  26. package/dist/server/engine/opencode-map.js +499 -0
  27. package/dist/server/engine/opencode-projects.js +61 -0
  28. package/dist/server/engine/opencode-sessions.js +195 -0
  29. package/dist/server/engine/opencode-transport.js +48 -0
  30. package/dist/server/engine/opencode.js +394 -0
  31. package/dist/server/engine/pi-events.js +148 -0
  32. package/dist/server/engine/pi-plugins.js +310 -0
  33. package/dist/server/engine/pi-rename.js +14 -0
  34. package/dist/server/engine/pi-sdk.js +503 -0
  35. package/dist/server/engine/pi-session-model.js +23 -0
  36. package/dist/server/engine/pi-subagent-tool.js +166 -0
  37. package/dist/server/engine/pi-subagents.js +372 -0
  38. package/dist/server/engine/pi-thinking.js +34 -0
  39. package/dist/server/engine/pi-transcript.js +225 -0
  40. package/dist/server/engine/pi-version.js +11 -0
  41. package/dist/server/engine/port.js +1 -0
  42. package/dist/server/engine/registry.js +47 -0
  43. package/dist/server/engine/scratch.js +13 -0
  44. package/dist/server/engine/subagent-guidance.js +22 -0
  45. package/dist/server/engine/suggestion-guidance.js +9 -0
  46. package/dist/server/engine/switchable.js +22 -0
  47. package/dist/server/engine/wiring.js +166 -0
  48. package/dist/server/files/dir-routes.js +40 -0
  49. package/dist/server/files/file-search.js +63 -0
  50. package/dist/server/files/file-service.js +232 -0
  51. package/dist/server/files/files-routes.js +210 -0
  52. package/dist/server/files/git-info.js +361 -0
  53. package/dist/server/http/auth-routes.js +99 -0
  54. package/dist/server/http/auth.js +191 -0
  55. package/dist/server/http/aux-routes.js +40 -0
  56. package/dist/server/http/compress.js +77 -0
  57. package/dist/server/http/health-and-static.js +33 -0
  58. package/dist/server/http/live-routes.js +113 -0
  59. package/dist/server/http/login-page.js +200 -0
  60. package/dist/server/index.js +363 -520
  61. package/dist/server/markers/actions.js +262 -0
  62. package/dist/server/mcp/mcp-auth.js +184 -0
  63. package/dist/server/mcp/mcp-command.js +94 -0
  64. package/dist/server/mcp/mcp-import.js +290 -0
  65. package/dist/server/mcp/mcp-market-sources.js +192 -0
  66. package/dist/server/mcp/mcp-market-types.js +5 -0
  67. package/dist/server/mcp/mcp-market.js +175 -0
  68. package/dist/server/mcp/mcp-probe.js +134 -0
  69. package/dist/server/mcp/mcp-routes.js +70 -0
  70. package/dist/server/mcp/mcp-store.js +286 -0
  71. package/dist/server/mcp/pi-mcp-config.js +280 -0
  72. package/dist/server/plugins/plugin-routes.js +21 -0
  73. package/dist/server/protocol-parse.js +262 -0
  74. package/dist/server/protocol-settings.js +1 -0
  75. package/dist/server/protocol.js +6 -0
  76. package/dist/server/providers/catalog-store.js +85 -0
  77. package/dist/server/providers/catalog-writes.js +197 -0
  78. package/dist/server/providers/catalog.js +77 -0
  79. package/dist/server/providers/codex-models.js +96 -0
  80. package/dist/server/providers/codex-test.js +124 -0
  81. package/dist/server/providers/enabled-models.js +118 -0
  82. package/dist/server/providers/import-models.js +69 -0
  83. package/dist/server/providers/import-routes.js +203 -0
  84. package/dist/server/providers/model-test.js +52 -0
  85. package/dist/server/providers/probe.js +64 -0
  86. package/dist/server/providers/provider-handlers.js +223 -0
  87. package/dist/server/providers/provider-store.js +359 -0
  88. package/dist/server/providers/request-headers.js +7 -0
  89. package/dist/server/providers/subscription-routes.js +181 -0
  90. package/dist/server/providers/validate.js +241 -0
  91. package/dist/server/push/push-routes.js +114 -0
  92. package/dist/server/push/push.js +120 -0
  93. package/dist/server/session/approval-bridge.js +103 -0
  94. package/dist/server/session/attachment-resolve.js +21 -0
  95. package/dist/server/session/client-view.js +82 -0
  96. package/dist/server/session/conversation-create.js +65 -0
  97. package/dist/server/session/delete-session.js +84 -0
  98. package/dist/server/session/engine-switch.js +108 -0
  99. package/dist/server/session/lock.js +82 -0
  100. package/dist/server/session/model-controller.js +119 -0
  101. package/dist/server/session/notices.js +31 -0
  102. package/dist/server/session/pi-opener.js +123 -0
  103. package/dist/server/session/pi-refresh.js +51 -0
  104. package/dist/server/session/pi-sessions-provider.js +46 -0
  105. package/dist/server/session/pi-sessions.js +272 -0
  106. package/dist/server/session/refresh-actions.js +42 -0
  107. package/dist/server/session/rename-session.js +23 -0
  108. package/dist/server/session/runtime-services.js +28 -0
  109. package/dist/server/session/runtime-types.js +1 -0
  110. package/dist/server/session/runtime.js +380 -0
  111. package/dist/server/session/session-stats.js +91 -0
  112. package/dist/server/session/settings.js +367 -0
  113. package/dist/server/session/snapshot-for.js +7 -0
  114. package/dist/server/session/stats-refresher.js +50 -0
  115. package/dist/server/session/subagents.js +116 -0
  116. package/dist/server/session/switch-conversation.js +86 -0
  117. package/dist/server/session/throttle.js +14 -0
  118. package/dist/server/session/turn-events.js +126 -0
  119. package/dist/server/session/turn-finish.js +79 -0
  120. package/dist/server/session/turn-markers.js +26 -0
  121. package/dist/server/session/turn-queue.js +111 -0
  122. package/dist/server/session/turn-send.js +108 -0
  123. package/dist/server/session/turn-watchdog.js +77 -0
  124. package/dist/server/sessions/session-routes.js +17 -0
  125. package/dist/server/skills/skill-install.js +186 -0
  126. package/dist/server/skills/skill-routes.js +134 -0
  127. package/dist/server/skills/skill-store.js +163 -0
  128. package/dist/server/skills/skillhub.js +214 -0
  129. package/dist/server/skills/zip.js +121 -0
  130. package/dist/server/snapshot/fan-out.js +10 -0
  131. package/dist/server/snapshot/messages.js +75 -0
  132. package/dist/server/snapshot/sender.js +62 -0
  133. package/dist/server/snapshot/state.js +202 -0
  134. package/dist/server/subagents/profile-store.js +287 -0
  135. package/dist/server/terminal/terminal-routes.js +176 -0
  136. package/dist/server/terminal/terminal-session.js +140 -0
  137. package/dist/server/uploads/upload-routes.js +64 -0
  138. package/dist/server/uploads/upload-store.js +115 -0
  139. package/dist/server/util/data-dir.js +36 -0
  140. package/dist/server/util/env.js +146 -0
  141. package/dist/server/util/ids.js +28 -0
  142. package/dist/server/util/log-file.js +67 -0
  143. package/dist/server/util/log.js +52 -0
  144. package/dist/server/util/origin.js +75 -0
  145. package/dist/server/util/paths.js +60 -0
  146. package/dist/server/util/pricing-index.js +121 -0
  147. package/dist/server/util/pricing.js +214 -0
  148. package/dist/server/util/startup.js +34 -0
  149. package/dist/server/util/upstream-version.js +140 -0
  150. package/dist/server/util/usage.js +19 -0
  151. package/dist/server/util/version.js +50 -0
  152. package/dist/server/ws/handle-message.js +107 -0
  153. package/package.json +50 -63
  154. package/web/dist/apple-touch-icon-precomposed.png +0 -0
  155. package/web/dist/apple-touch-icon.png +0 -0
  156. package/web/dist/assets/abnfDiagram-WEYOIA3C-VUKLHHSn.js +1 -0
  157. package/web/dist/assets/arc-BeUkaHhk.js +1 -0
  158. package/web/dist/assets/architecture-WOLXFQ4H-B9I4s0NV.js +1 -0
  159. package/web/dist/assets/architectureDiagram-ZHVYKT53-CaEkLhfq.js +36 -0
  160. package/web/dist/assets/array-BifhSqXX.js +1 -0
  161. package/web/dist/assets/blockDiagram-RJAE7QA3-IR49UNE_.js +140 -0
  162. package/web/dist/assets/c4Diagram-XDPPPE54-B-81CloB.js +38 -0
  163. package/web/dist/assets/channel-B_mTTjGt.js +1 -0
  164. package/web/dist/assets/chunk-2E7WHVMD-CaWVF7qv.js +88 -0
  165. package/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  166. package/web/dist/assets/chunk-4EMNKR2T-BvXf2tq-.js +1 -0
  167. package/web/dist/assets/chunk-55XCU76K-Bz_d2e2D.js +1 -0
  168. package/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  169. package/web/dist/assets/chunk-7S6XSTFO-B_SPMiU8.js +2 -0
  170. package/web/dist/assets/chunk-AQDTSKZB-Cen-_xej.js +72 -0
  171. package/web/dist/assets/chunk-DSNLSHJZ-DEw1kFwj.js +129 -0
  172. package/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  173. package/web/dist/assets/chunk-HAGFOTED-BwpOOcw8.js +62 -0
  174. package/web/dist/assets/chunk-HNE4Q36P-BjCwi2uW.js +1 -0
  175. package/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  176. package/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  177. package/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  178. package/web/dist/assets/chunk-NQOKRVIF-WcqhWFS5.js +213 -0
  179. package/web/dist/assets/chunk-UHW7PFBP-quDvQby8.js +1 -0
  180. package/web/dist/assets/chunk-VQ5W62QI-DZBwmhzq.js +1 -0
  181. package/web/dist/assets/chunk-WQQL4CDK-wYyHYuui.js +10 -0
  182. package/web/dist/assets/chunk-XXDRQBXY-DfJzC7nt.js +1 -0
  183. package/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  184. package/web/dist/assets/chunk-YZO67HFG-CWLraGk_.js +1 -0
  185. package/web/dist/assets/chunk-ZAUX2LTX-ChBUt_FJ.js +1 -0
  186. package/web/dist/assets/chunk-ZCHMQSOR-C57UFT60.js +2 -0
  187. package/web/dist/assets/classDiagram-v2-4MYWR3UA-Bo4i7SNY.js +217 -0
  188. package/web/dist/assets/cose-bilkent-JH36ORCC-CK360NdH.js +1 -0
  189. package/web/dist/assets/cynefin-EF2NZ3EQ-DlXFjIFm.js +1 -0
  190. package/web/dist/assets/cynefinDiagram-WF24UDRP-zvZIqA1c.js +62 -0
  191. package/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  192. package/web/dist/assets/dagre-NK4GIZVX-CnKhCu-v.js +4 -0
  193. package/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  194. package/web/dist/assets/diagram-CBK3MWTC-3QTR8Fjx.js +24 -0
  195. package/web/dist/assets/diagram-I5XIIV2D-D9lSd7z6.js +24 -0
  196. package/web/dist/assets/diagram-ODIL2UCW-DWJD6lPM.js +30 -0
  197. package/web/dist/assets/diagram-PFBEJUXE-CWJmoUg4.js +3 -0
  198. package/web/dist/assets/diagram-RJA3I4VV-D1SZHQcQ.js +41 -0
  199. package/web/dist/assets/diagram-TYUNFSNR-DCCjGi1x.js +200 -0
  200. package/web/dist/assets/dist-VOk_Iv17.js +1 -0
  201. package/web/dist/assets/ebnfDiagram-V4P3TTPT-CQLdqEiW.js +1 -0
  202. package/web/dist/assets/elk-QONTBCTQ-Dp2pEp6b.js +27 -0
  203. package/web/dist/assets/erDiagram-KYQIGQAA-BFGNoFQ-.js +99 -0
  204. package/web/dist/assets/eventmodeling-K75KTNOO-CFfAemGw.js +1 -0
  205. package/web/dist/assets/flowDiagram-IUGLLWBU-DmSp3cbW.js +1 -0
  206. package/web/dist/assets/ganttDiagram-R7GPNTJY-vK3jPGDq.js +292 -0
  207. package/web/dist/assets/gitGraph-VSP46ZUC-rXBgHTdq.js +1 -0
  208. package/web/dist/assets/gitGraphDiagram-PDYL7XHY-CBbGhago.js +106 -0
  209. package/web/dist/assets/graphlib-DS17s2tU.js +1 -0
  210. package/web/dist/assets/index-DBOF_OJe.js +104 -0
  211. package/web/dist/assets/index-nlY_-OaR.css +1 -0
  212. package/web/dist/assets/info-OHQRW6UA-Cd9vp-6C.js +1 -0
  213. package/web/dist/assets/infoDiagram-MVDRKAX5-Ce-Sg_qu.js +2 -0
  214. package/web/dist/assets/init-C-OQMol4.js +1 -0
  215. package/web/dist/assets/ishikawaDiagram-HPEHV2GE-DWlmR6BY.js +70 -0
  216. package/web/dist/assets/journeyDiagram-BOSHW7O7-DmDG97ae.js +139 -0
  217. package/web/dist/assets/kanban-definition-FNSA4HC5-1Z4sDrgV.js +89 -0
  218. package/web/dist/assets/katex-B_JcIwqj.js +257 -0
  219. package/web/dist/assets/line-DGM1-9KW.js +1 -0
  220. package/web/dist/assets/linear-B6hyeGHu.js +1 -0
  221. package/web/dist/assets/mermaid-parser.core-D1HmdkA4.js +7 -0
  222. package/web/dist/assets/mermaid.core-DeUJrhO1.js +44 -0
  223. package/web/dist/assets/mindmap-definition-6OY57UG3-C3-FByy8.js +98 -0
  224. package/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  225. package/web/dist/assets/packet-JDAUHWVQ-BoOr7DaW.js +1 -0
  226. package/web/dist/assets/path-fybaL0A-.js +1 -0
  227. package/web/dist/assets/pegDiagram-ECIUJNBX-Bn7m75TZ.js +1 -0
  228. package/web/dist/assets/pie-XZMESJXO-CGuMTDBp.js +1 -0
  229. package/web/dist/assets/pieDiagram-GNMPY74T-DHhaPwZe.js +39 -0
  230. package/web/dist/assets/quadrantDiagram-D57K4MC3-DxTgFHLK.js +7 -0
  231. package/web/dist/assets/radar-ABXABTNO-BiKxmALL.js +1 -0
  232. package/web/dist/assets/railroad-I3PHUGI6-BKYufgg4.js +1 -0
  233. package/web/dist/assets/railroad-abnf-LNEFI6M7-DmOrjlyL.js +1 -0
  234. package/web/dist/assets/railroad-ebnf-DWYD2UWJ-BgMyVDd1.js +1 -0
  235. package/web/dist/assets/railroad-peg-JR7BVNR7-BN6NL8S-.js +1 -0
  236. package/web/dist/assets/railroadDiagram-Y72NRBL7-BsbA-9gP.js +1 -0
  237. package/web/dist/assets/requirementDiagram-EQYSW5RA-HUw4YqWd.js +84 -0
  238. package/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  239. package/web/dist/assets/sankeyDiagram-SY24JHO5-Dx1MOFRu.js +40 -0
  240. package/web/dist/assets/sequenceDiagram-BLEHNHQU-C0-Rzg3g.js +169 -0
  241. package/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  242. package/web/dist/assets/src-CuHygcpj.js +1 -0
  243. package/web/dist/assets/stateDiagram-v2-A6IESKWZ-BnEICGtg.js +291 -0
  244. package/web/dist/assets/swimlanes-B3ENRKFQ-CwDRCHZD.js +1 -0
  245. package/web/dist/assets/swimlanesDiagram-AYQRV4AK-DZ_aAXmG.js +8 -0
  246. package/web/dist/assets/timeline-definition-JXYKGLGB-C4BVpgzc.js +120 -0
  247. package/web/dist/assets/treeView-D4JQ5SDB-BBsvEMP3.js +1 -0
  248. package/web/dist/assets/treemap-SAJKECNS-BYw6cxC8.js +1 -0
  249. package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkOXLgwP.js +328 -0
  250. package/web/dist/assets/vennDiagram-Q3Y3IDCW-Kgjk_n6B.js +34 -0
  251. package/web/dist/assets/wardley-7MLQ67FV-BGVObhvJ.js +1 -0
  252. package/web/dist/assets/wardleyDiagram-IVAIDKWA-i97uD6tL.js +78 -0
  253. package/web/dist/assets/xychartDiagram-4URNHDU4-awJi5TfJ.js +7 -0
  254. package/web/dist/favicon-32.png +0 -0
  255. package/web/dist/favicon.ico +0 -0
  256. package/web/dist/favicon.svg +20 -8
  257. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  258. package/web/dist/icons/icon-192.png +0 -0
  259. package/web/dist/icons/icon-512.png +0 -0
  260. package/web/dist/icons/icon-maskable-512.png +0 -0
  261. package/web/dist/index.html +31 -24
  262. package/web/dist/manifest.webmanifest +17 -0
  263. package/web/dist/maskable.svg +25 -0
  264. package/web/dist/sw.js +40 -0
  265. package/bin/oh-my-agent-web-ps.mjs +0 -203
  266. package/bin/oh-my-agent-web-ws.mjs +0 -782
  267. package/bin/oh-my-agent-web.mjs +0 -321
  268. package/bin/update-check.mjs +0 -142
  269. package/dist/server/codex-client.js +0 -310
  270. package/dist/server/codex-supervisor.js +0 -446
  271. package/dist/server/fs-service.js +0 -144
  272. package/dist/server/request-inspector.js +0 -458
  273. package/dist/shared/protocol.js +0 -112
  274. package/scripts/manage-server.mjs +0 -155
  275. package/scripts/omaw.mjs +0 -74
  276. package/shared/protocol.ts +0 -228
  277. package/web/dist/assets/index-5LJ5fRcO.js +0 -42
  278. package/web/dist/assets/index-QiDfPKFo.css +0 -1
  279. package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
@@ -0,0 +1,37 @@
1
+ /**
2
+ * DSH(DeepSeek Harness)ACP 客户端 = 通用 JSON-RPC stdio + ACP 握手。
3
+ *
4
+ * 为什么走 ACP(`dsh --profile acp`)而不是 SDK(`--profile sdk`)或 headless:
5
+ * - SDK 面只有 `initialize / session/prompt / shutdown`,没有会话列表/续接/取消/配置/权限,
6
+ * 这些要么自己读文件、要么自己写插件补(pi-web-ui 就是这么干的,代价是 26 个本地依赖 + 重启换模型);
7
+ * - ACP 原生给了 `session/new|list|resume|close`、`session/set_config_option`(模型/思考档)、
8
+ * `session/cancel`、`session/request_permission`,我们只需当客户端;
9
+ * - headless 是一次性进程,没有权限问询。
10
+ *
11
+ * 协议细节见 ADR-0037;映射规则在 dsh-map.ts(纯函数,单测覆盖)。
12
+ */
13
+ import { createJsonRpcStdioClient } from "./jsonrpc-stdio.js";
14
+ /** ACP 要求初始化的能力协商;我们不用它的 fs/terminal 桥(工具都在 DSH 进程里跑) */
15
+ const CLIENT_CAPABILITIES = {
16
+ fs: { readTextFile: false, writeTextFile: false },
17
+ terminal: false,
18
+ };
19
+ export function createDshClient(options = {}) {
20
+ return createJsonRpcStdioClient({
21
+ bin: options.bin ?? "dsh",
22
+ args: ["--profile", "acp", ...(options.patches ?? []).flatMap((file) => ["--patch", file])],
23
+ env: options.home ? { DSH_HOME: options.home } : undefined,
24
+ name: "dsh",
25
+ log: options.log ?? (() => { }),
26
+ timeoutMs: options.timeoutMs,
27
+ // ACP 是标准 JSON-RPC 2.0:帧里必须带 jsonrpc 字段
28
+ jsonrpc: true,
29
+ async handshake(call) {
30
+ const result = (await call("initialize", {
31
+ protocolVersion: 1,
32
+ clientCapabilities: CLIENT_CAPABILITIES,
33
+ }));
34
+ return { version: result?.agentInfo?.version ?? null };
35
+ },
36
+ });
37
+ }
@@ -0,0 +1,284 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { createDshClient } from "./dsh-transport.js";
4
+ import { locateSessionFile, readSessionHeader, readSessionLog } from "./dsh-log.js";
5
+ import { logToTranscript, logToUiMessages, statsFromLog } from "./dsh-history.js";
6
+ import { sanitizeHistoricalActions } from "../markers/actions.js";
7
+ import { currentModelId, currentThinkingLevel, friendlyDshError, modelOptions, modelValueFor, parseModelValue, permissionApprovalRequest, permissionOptionId, pickDefaultModelValue, stopReasonToTurnEnd, thinkingLevels, updateToEvent, } from "./dsh-map.js";
8
+ const MAX_TOOL_FACTS = 200;
9
+ export function createDshEngine(options = {}) {
10
+ /** DSH_HOME(会话日志与投影缓存都在它下面);测试可覆盖 */
11
+ const home = options.home ?? process.env.DSH_HOME?.trim() ?? join(homedir(), ".dsh");
12
+ const sessionsRoot = join(home, "sessions");
13
+ const client = options.client ?? createDshClient({ bin: options.bin, home: options.home, log: options.log ?? (() => { }) });
14
+ const bindings = new Map();
15
+ const bySession = new Map();
16
+ const active = new Map();
17
+ /** toolCallId → 工具事实(权限请求只带 id,名字/参数得从这里补) */
18
+ const toolFacts = new Map();
19
+ let approvalHandler = null;
20
+ let lastError = null;
21
+ /**
22
+ * 最近一次拿到的 configOptions(模型/思考档目录)。
23
+ * 历史会话在 resume 之前没有 config,但选择器不该是空的 —— 用它兜底;
24
+ * 真正切模型时仍会先 resume(拿不到就报可读错误)。
25
+ */
26
+ let lastConfig = [];
27
+ const rememberTool = (id, update) => {
28
+ toolFacts.set(id, { title: update.title, rawInput: update.rawInput });
29
+ if (toolFacts.size > MAX_TOOL_FACTS) {
30
+ const oldest = toolFacts.keys().next().value;
31
+ if (oldest)
32
+ toolFacts.delete(oldest);
33
+ }
34
+ };
35
+ const applyConfig = (binding, config) => {
36
+ if (!Array.isArray(config) || config.length === 0)
37
+ return;
38
+ lastConfig = config;
39
+ if (binding)
40
+ binding.config = lastConfig;
41
+ };
42
+ const bindingOf = (conversationId) => bindings.get(conversationId) ?? null;
43
+ /** 会话 id → 日志文件;cwd 未知时按 id 扫一层(历史会话第一次打开就是这条路径) */
44
+ const logFileOf = (binding, sessionId) => {
45
+ const id = binding?.sessionId ?? sessionId;
46
+ const file = locateSessionFile(sessionsRoot, id, binding?.cwd || undefined);
47
+ if (!file)
48
+ return null;
49
+ // 顺手把 cwd 补上:`session/resume` 必须带 cwd,而历史会话的 cwd 只在 header 里
50
+ if (binding && binding.cwd === "") {
51
+ const header = readSessionHeader(file);
52
+ if (typeof header?.cwd === "string")
53
+ binding.cwd = header.cwd;
54
+ }
55
+ return file;
56
+ };
57
+ const anyConfig = (conversationId) => {
58
+ const binding = conversationId ? bindingOf(conversationId) : undefined;
59
+ if (binding && binding.config.length > 0)
60
+ return binding.config;
61
+ return [...bindings.values()].find((item) => item.config.length > 0)?.config ?? lastConfig;
62
+ };
63
+ /** 历史会话首次使用时才 resume(ACP 的 resume 会拿到写句柄,所以按需) */
64
+ const ensureLive = async (conversationId) => {
65
+ const binding = bindingOf(conversationId);
66
+ if (!binding)
67
+ return null;
68
+ if (binding.live)
69
+ return binding;
70
+ const result = await client.request("session/resume", {
71
+ sessionId: binding.sessionId,
72
+ cwd: binding.cwd,
73
+ mcpServers: [],
74
+ });
75
+ binding.live = true;
76
+ applyConfig(binding, result?.configOptions);
77
+ return binding;
78
+ };
79
+ const setOption = async (conversationId, configId, value) => {
80
+ lastError = null;
81
+ try {
82
+ const binding = await ensureLive(conversationId);
83
+ if (!binding) {
84
+ lastError = "先打开一个会话";
85
+ return false;
86
+ }
87
+ const result = await client.request("session/set_config_option", {
88
+ sessionId: binding.sessionId,
89
+ configId,
90
+ value,
91
+ });
92
+ applyConfig(binding, result?.configOptions);
93
+ return true;
94
+ }
95
+ catch (error) {
96
+ lastError = friendlyDshError(error);
97
+ return false;
98
+ }
99
+ };
100
+ const handleNotification = (notification) => {
101
+ if (notification.method !== "session/update")
102
+ return;
103
+ const params = notification.params;
104
+ const sessionId = typeof params.sessionId === "string" ? params.sessionId : "";
105
+ const conversationId = bySession.get(sessionId);
106
+ if (!conversationId)
107
+ return;
108
+ const update = (params.update ?? {});
109
+ if (update.sessionUpdate === "tool_call" && typeof update.toolCallId === "string") {
110
+ rememberTool(update.toolCallId, update);
111
+ }
112
+ if (update.sessionUpdate === "config_option_update") {
113
+ applyConfig(bindingOf(conversationId) ?? undefined, update.configOptions);
114
+ }
115
+ const event = updateToEvent(update);
116
+ const turn = active.get(conversationId);
117
+ if (event && turn && !turn.ended)
118
+ turn.onEvent(event);
119
+ };
120
+ const handlePermission = async (id, params) => {
121
+ const facts = params.toolCall?.toolCallId ? toolFacts.get(params.toolCall.toolCallId) : undefined;
122
+ const request = permissionApprovalRequest(params, facts);
123
+ let decision = "deny";
124
+ if (approvalHandler) {
125
+ try {
126
+ decision = await approvalHandler(request);
127
+ }
128
+ catch {
129
+ decision = "deny";
130
+ }
131
+ }
132
+ client.respond(id, { outcome: { outcome: "selected", optionId: permissionOptionId(params.options, decision) } });
133
+ };
134
+ client.onNotification(handleNotification);
135
+ client.onServerRequest((request) => {
136
+ if (request.method === "session/request_permission") {
137
+ void handlePermission(request.id, request.params);
138
+ return;
139
+ }
140
+ // 认不出的服务端请求必须回包,否则 DSH 那一轮会一直等
141
+ client.respondError(request.id, -32601, `agent-web 未实现的服务端请求:${request.method}`);
142
+ });
143
+ return {
144
+ client,
145
+ version: () => client.version(),
146
+ async prewarm() {
147
+ await client.ensureStarted();
148
+ },
149
+ async createSession(sessionOptions) {
150
+ const created = await client.request("session/new", { cwd: sessionOptions.workspace, mcpServers: [] });
151
+ const sessionId = created?.sessionId;
152
+ if (!sessionId)
153
+ throw new Error("dsh 没有返回 sessionId");
154
+ const binding = {
155
+ sessionId,
156
+ cwd: sessionOptions.workspace,
157
+ live: true,
158
+ config: created.configOptions ?? [],
159
+ };
160
+ bindings.set(sessionOptions.sessionId, binding);
161
+ bySession.set(sessionId, sessionOptions.sessionId);
162
+ // 默认模型:优先「账号路由」,否则第一次 prompt 会报 no API key(见 pickDefaultModelValue)
163
+ const wanted = pickDefaultModelValue(binding.config, options.model);
164
+ const current = currentModelId(binding.config);
165
+ if (wanted && parseModelValue(wanted)) {
166
+ const wantedId = modelOptions(binding.config).find((model) => modelValueFor(binding.config, model.id) === wanted);
167
+ if (!current || (wantedId && wantedId.id !== current)) {
168
+ await setOption(sessionOptions.sessionId, "model", wanted);
169
+ }
170
+ }
171
+ return { sessionPath: null, engineSessionId: sessionId };
172
+ },
173
+ async openSession(openOptions) {
174
+ const sessionId = openOptions.engineSessionId ?? openOptions.sessionPath ?? openOptions.sessionId;
175
+ const binding = { sessionId, cwd: openOptions.cwd ?? "", live: false, config: [] };
176
+ bindings.set(openOptions.sessionId, binding);
177
+ bySession.set(sessionId, openOptions.sessionId);
178
+ },
179
+ async sendMessage(sessionId, text, onEvent) {
180
+ const binding = await ensureLive(sessionId).catch((error) => {
181
+ throw new Error(friendlyDshError(error));
182
+ });
183
+ if (!binding)
184
+ throw new Error("DSH 会话还没准备好(先新建一个会话)");
185
+ const turn = { onEvent, ended: false };
186
+ active.set(sessionId, turn);
187
+ try {
188
+ const result = await client.request("session/prompt", { sessionId: binding.sessionId, prompt: [{ type: "text", text }] },
189
+ // 一轮跑很久是正常的(有工具调用):给 30 分钟,别用默认 30 秒掐掉
190
+ { timeoutMs: 30 * 60 * 1000 });
191
+ if (!turn.ended)
192
+ onEvent({ type: "turn_end", reason: stopReasonToTurnEnd(result?.stopReason) });
193
+ }
194
+ finally {
195
+ turn.ended = true;
196
+ active.delete(sessionId);
197
+ }
198
+ },
199
+ async interrupt(sessionId) {
200
+ const binding = bindingOf(sessionId);
201
+ if (!binding)
202
+ return;
203
+ // ACP 里 cancel 是**通知**(无 id):发成请求会被当成未知方法丢掉 → 停止按钮没反应
204
+ client.notify("session/cancel", { sessionId: binding.sessionId });
205
+ },
206
+ /** 不看了:关掉会话释放写句柄(DSH 自己的 web/tui 才能打开它) */
207
+ async parkSession(sessionId) {
208
+ const binding = bindingOf(sessionId);
209
+ if (!binding || !binding.live)
210
+ return;
211
+ binding.live = false;
212
+ await client.request("session/close", { sessionId: binding.sessionId }).catch(() => undefined);
213
+ },
214
+ async closeSession(sessionId) {
215
+ await this.parkSession?.(sessionId);
216
+ const binding = bindingOf(sessionId);
217
+ bindings.delete(sessionId);
218
+ if (binding)
219
+ bySession.delete(binding.sessionId);
220
+ },
221
+ async dispose() {
222
+ lastConfig = [];
223
+ bindings.clear();
224
+ bySession.clear();
225
+ active.clear();
226
+ toolFacts.clear();
227
+ client.close();
228
+ },
229
+ setApprovalHandler(handler) {
230
+ approvalHandler = handler;
231
+ },
232
+ async listModels(sessionId) {
233
+ return modelOptions(anyConfig(sessionId));
234
+ },
235
+ async switchModel(sessionId, modelId) {
236
+ const value = modelValueFor(anyConfig(sessionId), modelId);
237
+ if (!value) {
238
+ lastError = `这个模型不在 DSH 的可选列表里:${modelId}`;
239
+ return false;
240
+ }
241
+ return setOption(sessionId, "model", value);
242
+ },
243
+ async currentModel(sessionId) {
244
+ const parsed = parseModelValue(anyConfig(sessionId).find((option) => option.id === "model")?.currentValue);
245
+ return parsed;
246
+ },
247
+ async sessionModel(sessionId) {
248
+ return this.currentModel?.(sessionId) ?? null;
249
+ },
250
+ async listThinkingLevels(sessionId) {
251
+ return thinkingLevels(anyConfig(sessionId));
252
+ },
253
+ async getThinkingLevel(sessionId) {
254
+ return currentThinkingLevel(anyConfig(sessionId));
255
+ },
256
+ async setThinkingLevel(sessionId, level) {
257
+ return setOption(sessionId, "reasoning_effort", level);
258
+ },
259
+ lastError: () => lastError,
260
+ /**
261
+ * 历史回放:ACP 不支持 `session/load`,所以读 DSH 自己的 v4 日志
262
+ * (多帧 zstd → 事件流 → 消息块;规则见 dsh-history.ts)。
263
+ */
264
+ async readUiMessages(sessionId) {
265
+ const file = logFileOf(bindingOf(sessionId), sessionId);
266
+ if (!file)
267
+ return [];
268
+ return sanitizeHistoricalActions(logToUiMessages(readSessionLog(file)));
269
+ },
270
+ async readTranscript(sessionId) {
271
+ const file = logFileOf(bindingOf(sessionId), sessionId);
272
+ if (!file)
273
+ return [];
274
+ return logToTranscript(readSessionLog(file));
275
+ },
276
+ /** 用量统计:四桶来自 `assistant/message.data.usage`,不依赖 ACP 的 usage_update */
277
+ async sessionStats(sessionId) {
278
+ const file = logFileOf(bindingOf(sessionId), sessionId);
279
+ if (!file)
280
+ return null;
281
+ return statsFromLog(readSessionLog(file));
282
+ },
283
+ };
284
+ }
@@ -0,0 +1,231 @@
1
+ const BIG_OUTPUT_CHARS = 200 * 1024;
2
+ /** 触发词 → 脚本。默认行为:把用户输入原样念一遍,证明「逐字说话」。 */
3
+ export function planFor(text) {
4
+ const key = text.toLowerCase();
5
+ if (key.includes("boom")) {
6
+ return [
7
+ { kind: "text", text: "好,我准备炸一下试试:\n" },
8
+ { kind: "error", code: "fake.boom", message: "假引擎按剧本报错了" },
9
+ ];
10
+ }
11
+ if (key.includes("approve")) {
12
+ return [
13
+ { kind: "text", text: "这一步要写文件,按规矩先问你一下:\n" },
14
+ {
15
+ kind: "approval",
16
+ toolName: "write_file",
17
+ argsSummary: 'path="docs/notes.md", bytes=128',
18
+ scopeKey: "write_file|/Users/me/code/agent-web/docs",
19
+ scopeHint: "目录 /Users/me/code/agent-web/docs",
20
+ riskNote: "写文件会改动磁盘内容,确认后才能执行",
21
+ },
22
+ { kind: "text", text: "(按你的决定,我要么继续、要么收手。)\n" },
23
+ ];
24
+ }
25
+ if (key.includes("huge")) {
26
+ return [
27
+ { kind: "text", text: "准备吐一大坨输出,看看截断提示对不对:\n" },
28
+ {
29
+ kind: "tool",
30
+ name: "run_command",
31
+ argsSummary: "cat very-big.log",
32
+ output: "x".repeat(BIG_OUTPUT_CHARS),
33
+ },
34
+ { kind: "text", text: "上面这段应该被截断了。\n" },
35
+ ];
36
+ }
37
+ if (key.includes("fail")) {
38
+ return [
39
+ { kind: "text", text: "跑一下测试:\n" },
40
+ {
41
+ kind: "tool",
42
+ name: "run_command",
43
+ argsSummary: "npm test",
44
+ output: "1 test failed: 期望 42 实际 43\n",
45
+ exitCode: 1,
46
+ },
47
+ { kind: "text", text: "失败了,退出码 1。\n" },
48
+ ];
49
+ }
50
+ if (key.includes("tool")) {
51
+ return [
52
+ { kind: "text", text: "我先看一下这个文件:\n" },
53
+ { kind: "tool", name: "read_file", argsSummary: 'path="server/index.ts"', output: "export const x = 1;\n" },
54
+ { kind: "text", text: "看完了。\n" },
55
+ ];
56
+ }
57
+ if (key.includes("slow")) {
58
+ const chunks = Array.from({ length: 40 }, (_, i) => `第 ${i + 1} 块 … `);
59
+ return [{ kind: "text", text: "慢慢说:\n" }, ...chunks.map((chunk) => ({ kind: "text", text: chunk }))];
60
+ }
61
+ return [
62
+ { kind: "text", text: `收到:「${text}」。\n这是假引擎在逐字说话:零 token 的测试替身,没有调用任何模型。\n` },
63
+ // 建议操作(ADR-0022):默认剧本就带上,方便零 token 验证这条链路
64
+ { kind: "text", text: "\n[[action:suggest:继续]]\n[[action:suggest:总结一下,prompt=用三句话总结刚才的内容]]\n" },
65
+ ];
66
+ }
67
+ const sleep = (ms) => (ms <= 0 ? Promise.resolve() : new Promise((resolve) => setTimeout(resolve, ms)));
68
+ export function createFakeEngine(options = {}) {
69
+ const chunkDelayMs = options.chunkDelayMs ?? 18;
70
+ const chunkChars = options.chunkChars ?? 6;
71
+ const slowChunkDelayMs = options.slowChunkDelayMs ?? 250;
72
+ /** 会话表:**必须先 createSession**,否则 sendMessage 报 engine.no_session(与真引擎一致) */
73
+ const sessions = new Map();
74
+ const interrupted = new Set();
75
+ /** 假引擎自己的模型状态(只为了让 switchModel 有地方记) */
76
+ const models = new Map();
77
+ /** 测试钩子:宿主交给引擎的图片(ADR-0020 §P3) */
78
+ const receivedImages = [];
79
+ /** switchModel 的调用记录:断言「新会话按项目的默认模型起步」 */
80
+ const switchedModels = [];
81
+ let approvalHandler = null;
82
+ let toolCallSeq = 0;
83
+ async function streamText(sessionId, text, onEvent, slow) {
84
+ const buffer = sessions.get(sessionId)?.transcript;
85
+ for (let index = 0; index < text.length; index += chunkChars) {
86
+ if (interrupted.has(sessionId))
87
+ return false;
88
+ const chunk = text.slice(index, index + chunkChars);
89
+ onEvent({ type: "text_delta", text: chunk });
90
+ buffer?.push({ role: "assistant", text: chunk, ts: Date.now() });
91
+ await sleep(slow ? slowChunkDelayMs : chunkDelayMs);
92
+ }
93
+ return true;
94
+ }
95
+ return {
96
+ version: () => "fake",
97
+ async createSession(sessionOptions) {
98
+ sessions.set(sessionOptions.sessionId, {
99
+ workspace: sessionOptions.workspace,
100
+ transcript: [],
101
+ sessionPath: `fake-sessions/${sessionOptions.sessionId}.jsonl`,
102
+ });
103
+ return { sessionPath: `fake-sessions/${sessionOptions.sessionId}.jsonl` };
104
+ },
105
+ /** 打开已有会话(ADR-0021 §2):假引擎没有真文件,只登记「已打开」 */
106
+ async openSession(openOptions) {
107
+ if (!sessions.has(openOptions.sessionId)) {
108
+ sessions.set(openOptions.sessionId, {
109
+ workspace: options.workspaceHint ?? "/tmp/agent-web-fake",
110
+ transcript: [],
111
+ sessionPath: openOptions.sessionPath ?? null,
112
+ });
113
+ }
114
+ },
115
+ async readUiMessages(sessionId) {
116
+ const session = sessions.get(sessionId);
117
+ if (!session)
118
+ return [];
119
+ return session.transcript.map((message, index) => ({
120
+ id: `fake-${sessionId}-${index}`,
121
+ role: message.role,
122
+ ts: message.ts,
123
+ blocks: [{ kind: "text", text: message.text }],
124
+ }));
125
+ },
126
+ async sendMessage(sessionId, text, onEvent, messageOptions) {
127
+ // 记下宿主交给引擎的图片(测试用;ADR-0020 §P3 的视觉注入)
128
+ if (messageOptions?.images?.length)
129
+ receivedImages.push(...messageOptions.images);
130
+ // 生命周期忠实于真引擎:**没有 createSession 就没有会话**(ADR-0018)
131
+ const session = sessions.get(sessionId);
132
+ if (!session) {
133
+ onEvent({ type: "error", code: "engine.no_session", message: `引擎里没有这个会话:${sessionId}` });
134
+ onEvent({ type: "turn_end", reason: "error" });
135
+ return;
136
+ }
137
+ const transcript = session.transcript;
138
+ transcript.push({ role: "user", text, ts: Date.now() });
139
+ const slow = text.toLowerCase().includes("slow");
140
+ // 中断标志只在**回合结束时**清理(不能在建回合时清):
141
+ // 宿主可能在「回合已开跑但还没进到引擎」的窗口里发来中断,那时它必须仍然生效。
142
+ try {
143
+ for (const step of planFor(text)) {
144
+ if (interrupted.has(sessionId)) {
145
+ onEvent({ type: "turn_end", reason: "interrupted" });
146
+ return;
147
+ }
148
+ if (step.kind === "text") {
149
+ if (!(await streamText(sessionId, step.text, onEvent, slow))) {
150
+ onEvent({ type: "turn_end", reason: "interrupted" });
151
+ return;
152
+ }
153
+ continue;
154
+ }
155
+ if (step.kind === "tool") {
156
+ const toolCallId = `fake-tool-${++toolCallSeq}`;
157
+ onEvent({ type: "tool_start", toolCallId, name: step.name, argsSummary: step.argsSummary });
158
+ await sleep(Math.max(chunkDelayMs, 20));
159
+ onEvent({
160
+ type: "tool_end",
161
+ toolCallId,
162
+ status: step.exitCode ? "failed" : "ok",
163
+ output: step.output,
164
+ ...(step.exitCode === undefined ? {} : { exitCode: step.exitCode }),
165
+ });
166
+ continue;
167
+ }
168
+ if (step.kind === "error") {
169
+ onEvent({ type: "error", code: step.code, message: step.message });
170
+ onEvent({ type: "turn_end", reason: "error" });
171
+ return;
172
+ }
173
+ // 审批:把决定权交回宿主(ADR-0006)
174
+ const decision = approvalHandler
175
+ ? await approvalHandler({
176
+ toolName: step.toolName,
177
+ argsSummary: step.argsSummary,
178
+ scopeKey: step.scopeKey,
179
+ scopeHint: step.scopeHint,
180
+ riskNote: step.riskNote,
181
+ })
182
+ : "deny";
183
+ onEvent({ type: "text_delta", text: `审批结果:${decision}\n` });
184
+ }
185
+ onEvent({ type: "turn_end", reason: "done" });
186
+ }
187
+ finally {
188
+ interrupted.delete(sessionId);
189
+ }
190
+ },
191
+ async listModels() {
192
+ // 假引擎固定给两个模型(零 token 也能验证「选模型」这条链路)
193
+ return [
194
+ { id: "fake/alpha", label: "Fake Alpha", current: true },
195
+ { id: "fake/beta", label: "Fake Beta", current: false },
196
+ ];
197
+ },
198
+ async switchModel(sessionId, modelId) {
199
+ models.set("current", modelId);
200
+ switchedModels.push({ sessionId, modelId });
201
+ return true;
202
+ },
203
+ async closeSession(sessionId) {
204
+ sessions.delete(sessionId);
205
+ },
206
+ async interrupt(sessionId) {
207
+ interrupted.add(sessionId);
208
+ },
209
+ async readTranscript(sessionId) {
210
+ return [...(sessions.get(sessionId)?.transcript ?? [])];
211
+ },
212
+ setApprovalHandler(handler) {
213
+ approvalHandler = handler;
214
+ },
215
+ /** 测试钩子:宿主交给引擎的图片(ADR-0020 §P3) */
216
+ images() {
217
+ return receivedImages;
218
+ },
219
+ /** 测试钩子:看会话表(含 createSession 时收到的历史) */
220
+ sessions() {
221
+ return sessions;
222
+ },
223
+ switchedModels: () => [...switchedModels],
224
+ reset() {
225
+ sessions.clear();
226
+ interrupted.clear();
227
+ approvalHandler = null;
228
+ switchedModels.length = 0;
229
+ },
230
+ };
231
+ }