oh-my-agent-web 0.5.0 → 0.7.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 (234) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +385 -0
  3. package/README.md +322 -81
  4. package/dist/server/cli-info.js +4 -1
  5. package/dist/server/cli.js +54 -11
  6. package/dist/server/engine/builtin.js +28 -3
  7. package/dist/server/engine/codex-sessions.js +3 -3
  8. package/dist/server/engine/dsh.js +3 -1
  9. package/dist/server/engine/jsonrpc-stdio.js +33 -3
  10. package/dist/server/engine/opencode-sessions.js +8 -2
  11. package/dist/server/engine/opencode.js +3 -1
  12. package/dist/server/engine/pi-sdk.js +157 -15
  13. package/dist/server/engine/pi-subagent-tool.js +35 -13
  14. package/dist/server/engine/pi-subagents.js +38 -1
  15. package/dist/server/engine/registry.js +2 -1
  16. package/dist/server/engine/wiring.js +1 -0
  17. package/dist/server/files/dir-routes.js +7 -5
  18. package/dist/server/files/file-search.js +4 -3
  19. package/dist/server/files/file-service.js +45 -9
  20. package/dist/server/files/files-routes.js +8 -1
  21. package/dist/server/files/git-info.js +34 -7
  22. package/dist/server/http/auth.js +27 -1
  23. package/dist/server/http/aux-routes.js +6 -1
  24. package/dist/server/http/file-headers.js +75 -0
  25. package/dist/server/index.js +54 -77
  26. package/dist/server/mcp/mcp-auth.js +9 -3
  27. package/dist/server/mcp/mcp-command.js +50 -5
  28. package/dist/server/mcp/mcp-probe.js +43 -7
  29. package/dist/server/mcp/pi-mcp-config.js +30 -2
  30. package/dist/server/protocol-parse.js +11 -1
  31. package/dist/server/providers/catalog-store.js +7 -3
  32. package/dist/server/providers/catalog-writes.js +5 -3
  33. package/dist/server/providers/enabled-models.js +5 -3
  34. package/dist/server/providers/import-routes.js +15 -5
  35. package/dist/server/providers/model-test.js +72 -11
  36. package/dist/server/providers/probe.js +20 -0
  37. package/dist/server/providers/provider-handlers.js +27 -10
  38. package/dist/server/providers/provider-pool.js +63 -0
  39. package/dist/server/providers/provider-store.js +139 -20
  40. package/dist/server/providers/validate.js +13 -4
  41. package/dist/server/push/push-routes.js +3 -3
  42. package/dist/server/push/push.js +74 -12
  43. package/dist/server/session/approval-bridge.js +31 -19
  44. package/dist/server/session/conversation-create.js +1 -1
  45. package/dist/server/session/engine-switch.js +4 -1
  46. package/dist/server/session/model-controller.js +20 -2
  47. package/dist/server/session/notices.js +4 -2
  48. package/dist/server/session/pi-opener.js +44 -4
  49. package/dist/server/session/pi-sessions.js +79 -9
  50. package/dist/server/session/refresh-actions.js +3 -2
  51. package/dist/server/session/runtime-services.js +1 -0
  52. package/dist/server/session/runtime.js +55 -16
  53. package/dist/server/session/settings.js +4 -4
  54. package/dist/server/session/stats-refresher.js +7 -2
  55. package/dist/server/session/switch-conversation.js +1 -2
  56. package/dist/server/session/turn-events.js +1 -1
  57. package/dist/server/session/turn-finish.js +26 -8
  58. package/dist/server/session/turn-markers.js +6 -5
  59. package/dist/server/session/turn-queue.js +171 -23
  60. package/dist/server/session/turn-send.js +38 -10
  61. package/dist/server/skills/skill-install.js +2 -2
  62. package/dist/server/snapshot/fan-out.js +0 -5
  63. package/dist/server/snapshot/sender.js +0 -9
  64. package/dist/server/snapshot/state.js +60 -6
  65. package/dist/server/subagents/profile-store.js +2 -1
  66. package/dist/server/terminal/terminal-routes.js +50 -23
  67. package/dist/server/terminal/terminal-session.js +63 -6
  68. package/dist/server/uploads/upload-routes.js +3 -0
  69. package/dist/server/util/atomic-write.js +18 -0
  70. package/dist/server/util/env.js +58 -14
  71. package/dist/server/util/file-mode.js +44 -0
  72. package/dist/server/util/origin.js +61 -0
  73. package/dist/server/util/paths.js +79 -1
  74. package/dist/server/util/spawn-cli.js +147 -0
  75. package/dist/server/util/upstream-version.js +47 -8
  76. package/dist/server/ws/handle-message.js +7 -1
  77. package/docs/screenshots/01-chat.png +0 -0
  78. package/docs/screenshots/02-answer.png +0 -0
  79. package/docs/screenshots/03-files-diff.png +0 -0
  80. package/docs/screenshots/04-terminal.png +0 -0
  81. package/docs/screenshots/05-models.png +0 -0
  82. package/docs/screenshots/06-skills.png +0 -0
  83. package/docs/screenshots/07-skills-hub.png +0 -0
  84. package/docs/screenshots/08-mcp.png +0 -0
  85. package/docs/screenshots/09-subagents.png +0 -0
  86. package/docs/screenshots/10-settings.png +0 -0
  87. package/docs/screenshots/11-plugins.png +0 -0
  88. package/docs/screenshots/12-voice.png +0 -0
  89. package/docs/screenshots/13-push.png +0 -0
  90. package/docs/screenshots/README.md +45 -0
  91. package/package.json +43 -19
  92. package/scripts/prune-prebuilds.mjs +42 -0
  93. package/web/dist/assets/_omaw-mermaid-pruned_chunk_abnfDiagram-WEYOIA3C-CvNbnJ75.js +1 -0
  94. package/web/dist/assets/_omaw-mermaid-pruned_chunk_architectureDiagram-ZHVYKT53-T7NZZ4ao.js +1 -0
  95. package/web/dist/assets/_omaw-mermaid-pruned_chunk_c4Diagram-XDPPPE54-ByuAPg39.js +1 -0
  96. package/web/dist/assets/_omaw-mermaid-pruned_chunk_cynefinDiagram-WF24UDRP-BY5DHttT.js +1 -0
  97. package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-CBK3MWTC-D7ZYyrF7.js +1 -0
  98. package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-ODIL2UCW-C6kcOZFc.js +1 -0
  99. package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-PFBEJUXE-CzFPFq0D.js +1 -0
  100. package/web/dist/assets/_omaw-mermaid-pruned_chunk_diagram-TYUNFSNR-B4G4T8SL.js +1 -0
  101. package/web/dist/assets/_omaw-mermaid-pruned_chunk_ebnfDiagram-V4P3TTPT-BkQ4xwD0.js +1 -0
  102. package/web/dist/assets/_omaw-mermaid-pruned_chunk_ishikawaDiagram-HPEHV2GE-52xqckJv.js +1 -0
  103. package/web/dist/assets/_omaw-mermaid-pruned_chunk_mindmap-definition-6OY57UG3-7cQksbFn.js +1 -0
  104. package/web/dist/assets/_omaw-mermaid-pruned_chunk_pegDiagram-ECIUJNBX-udgtBpwv.js +1 -0
  105. package/web/dist/assets/_omaw-mermaid-pruned_chunk_railroadDiagram-Y72NRBL7-Cd2dcQWz.js +1 -0
  106. package/web/dist/assets/_omaw-mermaid-pruned_chunk_swimlanes-B3ENRKFQ-p34pL1Yk.js +1 -0
  107. package/web/dist/assets/_omaw-mermaid-pruned_chunk_swimlanesDiagram-AYQRV4AK-BslfPs4r.js +1 -0
  108. package/web/dist/assets/_omaw-mermaid-pruned_chunk_usecaseDiagram-WPOLBNNX-DJZoN9Xk.js +1 -0
  109. package/web/dist/assets/_omaw-mermaid-pruned_chunk_vennDiagram-Q3Y3IDCW-DPXo67cZ.js +1 -0
  110. package/web/dist/assets/_omaw-mermaid-pruned_chunk_wardleyDiagram-IVAIDKWA-BE6ukwkg.js +1 -0
  111. package/web/dist/assets/_omaw-mermaid-pruned_katex_katex-Zojedrce.js +1 -0
  112. package/web/dist/assets/arc-BTAvbR16.js +1 -0
  113. package/web/dist/assets/architecture-WOLXFQ4H-fewLD5_7.js +1 -0
  114. package/web/dist/assets/{blockDiagram-RJAE7QA3-IR49UNE_.js → blockDiagram-RJAE7QA3-ClmgyxZl.js} +5 -5
  115. package/web/dist/assets/channel-DfaYIFFE.js +1 -0
  116. package/web/dist/assets/{chunk-55XCU76K-Bz_d2e2D.js → chunk-55XCU76K-DdC5EhZG.js} +1 -1
  117. package/web/dist/assets/chunk-7S6XSTFO-CgToWHK6.js +2 -0
  118. package/web/dist/assets/chunk-AQDTSKZB-D-rQMnp7.js +72 -0
  119. package/web/dist/assets/{chunk-DSNLSHJZ-DEw1kFwj.js → chunk-DSNLSHJZ-cuZoTGMH.js} +9 -9
  120. package/web/dist/assets/chunk-HNE4Q36P-CiKr9iSN.js +1 -0
  121. package/web/dist/assets/{chunk-NGNAAXSQ-C2Wy9Xmz.js → chunk-NGNAAXSQ-DuHRe0y2.js} +1 -1
  122. package/web/dist/assets/chunk-VQ5W62QI-DbTfXAAT.js +1 -0
  123. package/web/dist/assets/chunk-WQQL4CDK-DHanHfB5.js +10 -0
  124. package/web/dist/assets/chunk-ZAUX2LTX-SrjFXpy2.js +62 -0
  125. package/web/dist/assets/chunk-ZCHMQSOR-7Wy8xXQ5.js +2 -0
  126. package/web/dist/assets/{classDiagram-v2-4MYWR3UA-Bo4i7SNY.js → classDiagram-v2-4MYWR3UA-CBe5ZDvZ.js} +4 -4
  127. package/web/dist/assets/cose-bilkent-JH36ORCC-Dljdyvhw.js +1 -0
  128. package/web/dist/assets/cynefin-EF2NZ3EQ-CcZU7GFP.js +1 -0
  129. package/web/dist/assets/dagre-NK4GIZVX-Bq_6y8qp.js +4 -0
  130. package/web/dist/assets/diagram-I5XIIV2D-C2lnnYVG.js +24 -0
  131. package/web/dist/assets/{diagram-RJA3I4VV-D1SZHQcQ.js → diagram-RJA3I4VV-Bqf8Z6wJ.js} +1 -1
  132. package/web/dist/assets/elk-QONTBCTQ-Cdzq_cJ5.js +27 -0
  133. package/web/dist/assets/{erDiagram-KYQIGQAA-BFGNoFQ-.js → erDiagram-KYQIGQAA-ZJLCjPFC.js} +2 -2
  134. package/web/dist/assets/eventmodeling-K75KTNOO-DHqeIpes.js +1 -0
  135. package/web/dist/assets/flowDiagram-IUGLLWBU-AVgYvXKN.js +213 -0
  136. package/web/dist/assets/ganttDiagram-R7GPNTJY-CR1SG1M7.js +292 -0
  137. package/web/dist/assets/gitGraph-VSP46ZUC-CIZQPK0R.js +1 -0
  138. package/web/dist/assets/gitGraphDiagram-PDYL7XHY-BhOCWEh6.js +106 -0
  139. package/web/dist/assets/index-CZQjJbRg.js +104 -0
  140. package/web/dist/assets/index-ChDUWKZX.css +1 -0
  141. package/web/dist/assets/info-OHQRW6UA-C2wuRxeQ.js +1 -0
  142. package/web/dist/assets/{infoDiagram-MVDRKAX5-Ce-Sg_qu.js → infoDiagram-MVDRKAX5-BP5doAB5.js} +1 -1
  143. package/web/dist/assets/{journeyDiagram-BOSHW7O7-DmDG97ae.js → journeyDiagram-BOSHW7O7-BcIDRTPS.js} +4 -4
  144. package/web/dist/assets/{kanban-definition-FNSA4HC5-1Z4sDrgV.js → kanban-definition-FNSA4HC5-CRH_bcoT.js} +5 -5
  145. package/web/dist/assets/{line-DGM1-9KW.js → line-iA4GPA_S.js} +1 -1
  146. package/web/dist/assets/linear-BGUKwkTH.js +1 -0
  147. package/web/dist/assets/math-DgNW_bVP.js +1 -0
  148. package/web/dist/assets/{mermaid-parser.core-D1HmdkA4.js → mermaid-parser.core-CsqmgpEv.js} +5 -5
  149. package/web/dist/assets/mermaid.core-C1yCGQK6.js +44 -0
  150. package/web/dist/assets/packet-JDAUHWVQ-2ggaoz2g.js +1 -0
  151. package/web/dist/assets/pie-XZMESJXO-h_yslQQs.js +1 -0
  152. package/web/dist/assets/{pieDiagram-GNMPY74T-DHhaPwZe.js → pieDiagram-GNMPY74T-6-JZMFyk.js} +2 -2
  153. package/web/dist/assets/{quadrantDiagram-D57K4MC3-DxTgFHLK.js → quadrantDiagram-D57K4MC3-K5r1HOgL.js} +3 -3
  154. package/web/dist/assets/radar-ABXABTNO-6fOJGGgq.js +1 -0
  155. package/web/dist/assets/railroad-I3PHUGI6-B_gUgQ3d.js +1 -0
  156. package/web/dist/assets/railroad-abnf-LNEFI6M7-CJ_eqiXZ.js +1 -0
  157. package/web/dist/assets/railroad-ebnf-DWYD2UWJ-BTsgjLTU.js +1 -0
  158. package/web/dist/assets/railroad-peg-JR7BVNR7-D91uiLln.js +1 -0
  159. package/web/dist/assets/{requirementDiagram-EQYSW5RA-HUw4YqWd.js → requirementDiagram-EQYSW5RA-uapnV_OO.js} +3 -3
  160. package/web/dist/assets/{sankeyDiagram-SY24JHO5-Dx1MOFRu.js → sankeyDiagram-SY24JHO5-BQsbFgUb.js} +3 -3
  161. package/web/dist/assets/{sequenceDiagram-BLEHNHQU-C0-Rzg3g.js → sequenceDiagram-BLEHNHQU-iRoSYX_e.js} +6 -6
  162. package/web/dist/assets/{src-CuHygcpj.js → src-BBrKcs9r.js} +1 -1
  163. package/web/dist/assets/{stateDiagram-v2-A6IESKWZ-BnEICGtg.js → stateDiagram-v2-A6IESKWZ-Bzj9z5KH.js} +2 -2
  164. package/web/dist/assets/{timeline-definition-JXYKGLGB-C4BVpgzc.js → timeline-definition-JXYKGLGB-DLUUuJX0.js} +2 -2
  165. package/web/dist/assets/treeView-D4JQ5SDB-CphIQ3n5.js +1 -0
  166. package/web/dist/assets/treemap-SAJKECNS-DqU3insw.js +1 -0
  167. package/web/dist/assets/wardley-7MLQ67FV-Ch7d8cmC.js +1 -0
  168. package/web/dist/assets/{xychartDiagram-4URNHDU4-awJi5TfJ.js → xychartDiagram-4URNHDU4-DdxpWBeM.js} +3 -3
  169. package/web/dist/index.html +2 -2
  170. package/web/dist/assets/abnfDiagram-WEYOIA3C-VUKLHHSn.js +0 -1
  171. package/web/dist/assets/arc-BeUkaHhk.js +0 -1
  172. package/web/dist/assets/architecture-WOLXFQ4H-B9I4s0NV.js +0 -1
  173. package/web/dist/assets/architectureDiagram-ZHVYKT53-CaEkLhfq.js +0 -36
  174. package/web/dist/assets/c4Diagram-XDPPPE54-B-81CloB.js +0 -38
  175. package/web/dist/assets/channel-B_mTTjGt.js +0 -1
  176. package/web/dist/assets/chunk-2E7WHVMD-CaWVF7qv.js +0 -88
  177. package/web/dist/assets/chunk-4EMNKR2T-BvXf2tq-.js +0 -1
  178. package/web/dist/assets/chunk-7S6XSTFO-B_SPMiU8.js +0 -2
  179. package/web/dist/assets/chunk-AQDTSKZB-Cen-_xej.js +0 -72
  180. package/web/dist/assets/chunk-HAGFOTED-BwpOOcw8.js +0 -62
  181. package/web/dist/assets/chunk-HNE4Q36P-BjCwi2uW.js +0 -1
  182. package/web/dist/assets/chunk-NQOKRVIF-WcqhWFS5.js +0 -213
  183. package/web/dist/assets/chunk-UHW7PFBP-quDvQby8.js +0 -1
  184. package/web/dist/assets/chunk-VQ5W62QI-DZBwmhzq.js +0 -1
  185. package/web/dist/assets/chunk-WQQL4CDK-wYyHYuui.js +0 -10
  186. package/web/dist/assets/chunk-XXDRQBXY-DfJzC7nt.js +0 -1
  187. package/web/dist/assets/chunk-YZO67HFG-CWLraGk_.js +0 -1
  188. package/web/dist/assets/chunk-ZAUX2LTX-ChBUt_FJ.js +0 -1
  189. package/web/dist/assets/chunk-ZCHMQSOR-C57UFT60.js +0 -2
  190. package/web/dist/assets/cose-bilkent-JH36ORCC-CK360NdH.js +0 -1
  191. package/web/dist/assets/cynefin-EF2NZ3EQ-DlXFjIFm.js +0 -1
  192. package/web/dist/assets/cynefinDiagram-WF24UDRP-zvZIqA1c.js +0 -62
  193. package/web/dist/assets/cytoscape.esm-BegBNzOV.js +0 -321
  194. package/web/dist/assets/dagre-NK4GIZVX-CnKhCu-v.js +0 -4
  195. package/web/dist/assets/defaultLocale-BFoDCU3G.js +0 -1
  196. package/web/dist/assets/diagram-CBK3MWTC-3QTR8Fjx.js +0 -24
  197. package/web/dist/assets/diagram-I5XIIV2D-D9lSd7z6.js +0 -24
  198. package/web/dist/assets/diagram-ODIL2UCW-DWJD6lPM.js +0 -30
  199. package/web/dist/assets/diagram-PFBEJUXE-CWJmoUg4.js +0 -3
  200. package/web/dist/assets/diagram-TYUNFSNR-DCCjGi1x.js +0 -200
  201. package/web/dist/assets/dist-VOk_Iv17.js +0 -1
  202. package/web/dist/assets/ebnfDiagram-V4P3TTPT-CQLdqEiW.js +0 -1
  203. package/web/dist/assets/elk-QONTBCTQ-Dp2pEp6b.js +0 -27
  204. package/web/dist/assets/eventmodeling-K75KTNOO-CFfAemGw.js +0 -1
  205. package/web/dist/assets/flowDiagram-IUGLLWBU-DmSp3cbW.js +0 -1
  206. package/web/dist/assets/ganttDiagram-R7GPNTJY-vK3jPGDq.js +0 -292
  207. package/web/dist/assets/gitGraph-VSP46ZUC-rXBgHTdq.js +0 -1
  208. package/web/dist/assets/gitGraphDiagram-PDYL7XHY-CBbGhago.js +0 -106
  209. package/web/dist/assets/index-DBOF_OJe.js +0 -104
  210. package/web/dist/assets/index-nlY_-OaR.css +0 -1
  211. package/web/dist/assets/info-OHQRW6UA-Cd9vp-6C.js +0 -1
  212. package/web/dist/assets/ishikawaDiagram-HPEHV2GE-DWlmR6BY.js +0 -70
  213. package/web/dist/assets/katex-B_JcIwqj.js +0 -257
  214. package/web/dist/assets/linear-B6hyeGHu.js +0 -1
  215. package/web/dist/assets/mermaid.core-DeUJrhO1.js +0 -44
  216. package/web/dist/assets/mindmap-definition-6OY57UG3-C3-FByy8.js +0 -98
  217. package/web/dist/assets/packet-JDAUHWVQ-BoOr7DaW.js +0 -1
  218. package/web/dist/assets/pegDiagram-ECIUJNBX-Bn7m75TZ.js +0 -1
  219. package/web/dist/assets/pie-XZMESJXO-CGuMTDBp.js +0 -1
  220. package/web/dist/assets/radar-ABXABTNO-BiKxmALL.js +0 -1
  221. package/web/dist/assets/railroad-I3PHUGI6-BKYufgg4.js +0 -1
  222. package/web/dist/assets/railroad-abnf-LNEFI6M7-DmOrjlyL.js +0 -1
  223. package/web/dist/assets/railroad-ebnf-DWYD2UWJ-BgMyVDd1.js +0 -1
  224. package/web/dist/assets/railroad-peg-JR7BVNR7-BN6NL8S-.js +0 -1
  225. package/web/dist/assets/railroadDiagram-Y72NRBL7-BsbA-9gP.js +0 -1
  226. package/web/dist/assets/rough.esm-Dy-Kn_BL.js +0 -1
  227. package/web/dist/assets/swimlanes-B3ENRKFQ-CwDRCHZD.js +0 -1
  228. package/web/dist/assets/swimlanesDiagram-AYQRV4AK-DZ_aAXmG.js +0 -8
  229. package/web/dist/assets/treeView-D4JQ5SDB-BBsvEMP3.js +0 -1
  230. package/web/dist/assets/treemap-SAJKECNS-BYw6cxC8.js +0 -1
  231. package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkOXLgwP.js +0 -328
  232. package/web/dist/assets/vennDiagram-Q3Y3IDCW-Kgjk_n6B.js +0 -34
  233. package/web/dist/assets/wardley-7MLQ67FV-BGVObhvJ.js +0 -1
  234. package/web/dist/assets/wardleyDiagram-IVAIDKWA-i97uD6tL.js +0 -78
package/README.md CHANGED
@@ -1,43 +1,127 @@
1
1
  # oh-my-agent-web
2
2
 
3
- 把编码智能体(pi / Codex / DeepSeek Harness)包进一个**本机网页工作台**:左边是多项目 + 会话列表,中间是对话,右边随时看文件、跑终端。
3
+ **中文** · [English](README.en.md)
4
4
 
5
- 单用户、只听回环、会话事实源是引擎自己的落盘文件 —— 不引数据库、不建账号体系,`agent-web` 起一个进程就是一个人的工作台。
5
+ 一句话:**给你正在跑的那个 agent 配一个外部操作页。** 引擎照旧自己跑在自己电脑上,你在网页里看着它干活、管它的工具和模型、随时插话纠正,需要时直接翻文件和开终端。
6
6
 
7
- ## 它是什么
7
+ 单用户、只听回环、会话事实源是引擎自己的落盘文件 —— 不引数据库、不建账号体系,`omaw` 起一个进程就是一个人的工作台。
8
8
 
9
- - **一个仓库一个项目**:侧栏按目录分组,会话就是引擎的会话文件(pi 的 `.pi/agent/sessions/**/*.jsonl`、Codex 的 thread、DSH 的会话),在 CLI 里看到的名字/内容与网页里一致。
10
- - **对话 + 工具 + 审批**:工具调用卡片、流式输出、命令/文件改动按审批规则放行或逐次询问。
11
- - **文件管理**:目录树、按名搜索、预览(markdown / 图片 / PDF / 音视频流)、上传、拖拽附件;项目在 git 仓库里时右上角多一枚分支标签,点开可切换本地分支(工作区有未提交改动会先确认)。
12
- - **内嵌终端**:基于 `node-pty` 的真 PTY(xterm.js 渲染),和会话共用工作目录。
13
- - **模型与供应商**:在「模型」面板里配置 provider / 模型清单,支持从别的客户端导入、逐个试连。
14
- - **技能面板**:列出 pi 技能并启停,读单个技能的文件。
15
- - **推送**:Web Push 订阅,回合结束时通知(需要 https 或 localhost)。
16
- - **手机可用**:窄屏自适应布局,侧栏可收起。
9
+ ![一次会话里并行开 4 个子代理:左侧项目与会话、中间过程与工具卡、右侧文件预览](docs/screenshots/01-chat.png)
10
+
11
+ ```
12
+ npm ci && npm run build && npm start → http://127.0.0.1:25257
13
+ ```
14
+
15
+ ---
16
+
17
+ ## 模块一览
18
+
19
+ 顶栏那一排就是全部入口;侧栏管项目/会话/文件,右栏是预览与终端。
20
+
21
+ | 模块 | 入口 | 能做什么 |
22
+ |---|---|---|
23
+ | **对话** | 中间 | 流式正文 + 可折叠思考块 + 工具卡(参数 / 完整命令 / 输出 / 耗时 / 成败)+ 「过程 N 步」分块 + 用量尾注(token / 缓存 / 花费)+ 改动文件 chip |
24
+ | **插队与排队** | 输入区 | 回合进行中可以 steer(下一条立刻生效);排队的消息可撤回;发出去的消息可折叠进过程 |
25
+ | **多引擎** | 顶栏「引擎」 | pi / Codex / DeepSeek Harness / opencode 各自的原生 loop(不是换皮的同一套 prompt);每个会话记住自己的引擎、模型、思考档 |
26
+ | **子代理** | 顶栏「子代理」+ 对话下方卡片条 | 主 agent 拿到 `Agent` 工具后可并行开多个**真实会话**;对话下方给运行卡(状态 / 耗时 / 工具次数 / token / 花费 / 就地看结果 / 单独停掉);面板里管模板:工具白名单、模型、思考档、最大轮次、并发上限(1–12) |
27
+ | **模型与供应商** | 顶栏「模型」 | 内置 pi 官方目录里的 41 家:**填一把 key 就能用**,不用抄 base url 与模型 id;自定义 provider 可改 api 形态(openai-completions / responses / anthropic-messages / codex / typesafe)、base url、自定义 headers;逐个勾选模型、批量测速、从 `/models` 拉清单、按勾选保存白名单;**key 不出服务端**(只回尾 4 位) |
28
+ | **技能** | 顶栏「技能」 | 已装列表(工具市场装了 45 个就是 45 行)+ 启停 + 看 `SKILL.md` 与目录文件;**Skill Hub** 分类搜索与一键安装(索引 20 万+)、从 Git 仓库装、手动创建;启停 = 把 `SKILL.md` 改名,不留第二份状态 |
29
+ | **MCP** | 顶栏「MCP」 | 全局与项目两份 `mcp.json` 可视化增删改(项目同名条目覆盖全局)、启停、exposure 四档(codemode / deferred / direct / hidden)、**测试连接**(真握手 + 列工具)、OAuth 登录、市场挑服务器 |
30
+ | **插件** | 顶栏「插件」 | pi package(npm / git / 本地目录)四态一览(loaded / installed / missing / disabled)、按包停用、散装 `extensions/` 单文件扩展;状态来自 pi 自己解析,装/卸/更新才动磁盘 |
31
+ | **文件管理** | 侧栏「文件管理」 | 目录树(可换根)、按文件名搜索、显示隐藏文件、全部收起、上传与下载、在 Finder 中显示;未提交改动在树上打点 |
32
+ | **查看文件** | 右栏预览(多标签) | markdown / 源码带行号 / 图片 / PDF / 音视频流;按行选中后「添加到对话」直接当上下文;可全屏、可拖宽 |
33
+ | **改动(diff)** | 预览页顶「改动」 | 与 HEAD 对比:`+285 −78` 概览、行级红绿高亮、可只看改动也可回到全文;从「改动过的文件」chip 点进来时直接落在 diff |
34
+ | **git** | 文件区顶部 | 分支标签(点开切本地分支,脏工作区先确认)、文件后面标 `M` / `U` / `D` 状态 |
35
+ | **终端** | 文件区 `>_`、右栏标签 | 真 PTY(`node-pty` + xterm.js),与会话共用 cwd,可自动执行初始命令;Linux 缺编译工具时会明确说装什么 |
36
+ | **推送** | 顶栏「推送」 | Web Push 订阅(VAPID),回合结束推一条:标题=项目名、正文=会话名、失败 ❌ / 中断 ⏹ 带前缀;多设备管理、测试推送、一键清空;需要 https 或 localhost |
37
+ | **语音** | 顶栏「语音」 | 四类事件提示音(开始 / 结束 / 出错 / 等待审批)可试听、音量可调;本地 TTS 播报(浏览器内置语音,不联网),可朗读最终回复、调语速与音色 |
38
+ | **设置** | 顶栏「设置」 | 四页:**界面**(顶栏与状态栏显示哪些块)、**通用**(快捷短语等)、**外观**(聊天字号 4 档 / 内容宽度 4 档 / 6 套色板:清亮白、暖纸、雾青、东京夜、摩卡、北境)、**语言**(中文 / English,点了立刻生效) |
39
+ | **会话** | 侧栏 | 按目录分组成「项目」、置顶、重命名、归档(可撤销)、删除(改名 `.deleted-<时间戳>`,文件还在);会话就是引擎自己的文件,CLI 里看到的一致 |
40
+ | **附件** | 输入框 | 拖拽上传、`@` 提及文件、图片粘贴;上传落在数据目录,不写进项目 |
41
+ | **手机** | — | 窄屏自适应:侧栏变抽屉、过程块收成三角箭头、400px 以下自动切手机布局 |
42
+
43
+ > 下面所有截图都来自**真实运行**的实例(就在本项目自己的会话上截的),不是拼的 mock。
44
+
45
+ ---
46
+
47
+ ## 界面与截图
48
+
49
+ | | |
50
+ |---|---|
51
+ | ![回复:markdown 表格 + 改动文件 + 用量](docs/screenshots/02-answer.png) | ![文件预览的改动视图](docs/screenshots/03-files-diff.png) |
52
+ | **回答**:markdown / 表格 / 代码高亮,尾注给 token 与花费 | **改动**:与 HEAD 对比,红绿行级高亮 |
53
+ | ![内嵌终端](docs/screenshots/04-terminal.png) | ![模型与供应商](docs/screenshots/05-models.png) |
54
+ | **终端**:真 PTY,与会话同 cwd | **模型**:内置 41 家填 key 即用,模型勾选即生效 |
55
+ | ![技能面板](docs/screenshots/06-skills.png) | ![Skill Hub](docs/screenshots/07-skills-hub.png) |
56
+ | **技能**:已装列表 + 启停 + 看 SKILL.md | **技能市场**:分类搜索,一键安装 |
57
+ | ![MCP](docs/screenshots/08-mcp.png) | ![子代理](docs/screenshots/09-subagents.png) |
58
+ | **MCP**:全局 / 项目两份配置、暴露方式、测试连接 | **子代理**:跑几个、看结果、模板与工具白名单 |
59
+ | ![设置](docs/screenshots/10-settings.png) | ![插件](docs/screenshots/11-plugins.png) |
60
+ | **设置 → 外观**:字号、内容宽度、6 套色板 | **插件**:pi package 的加载状态与启停 |
61
+
62
+ 另外两张:[语音(提示音 + 本地 TTS)](docs/screenshots/12-voice.png)、[推送(订阅与多设备)](docs/screenshots/13-push.png)。
63
+
64
+ ---
17
65
 
18
66
  ## 快速开始
19
67
 
20
- 要求 **Node.js ≥ 22.19**(macOS / Linux;Windows 未验证,`node-pty` 需要能编译或拿到预编译产物)。
68
+ 要求 **Node.js ≥ 22.19**(`@earendil-works/pi-*` 1.x 的硬要求)与 **npm ≥ 9**。macOS / Linux / Windows 均可。
21
69
 
22
70
  ```bash
23
- npm install
71
+ npm ci # 推荐:按 lock 文件装精确版本(不重新解析版本范围)
24
72
  npm run build
25
73
  npm start # 前台运行;Ctrl-C 退出
26
74
  ```
27
75
 
28
76
  浏览器打开 <http://127.0.0.1:25257>。
29
77
 
30
- 装成全局命令后,可以用简写 `omaw` 或 `agent-web`(`oh-my-agent-web` / `omaw` / `agent-web` 三个命令指向同一入口,完全等价):
78
+ ### 一行命令安装(不碰系统全局 npm)
79
+
80
+ macOS / Linux:
81
+
82
+ ```bash
83
+ curl -fsSL https://raw.githubusercontent.com/duzhenxun/oh-my-agent-web/main/scripts/install.sh | sh
84
+ ```
85
+
86
+ Windows(PowerShell):
87
+
88
+ ```powershell
89
+ powershell -c "irm https://raw.githubusercontent.com/duzhenxun/oh-my-agent-web/main/scripts/install.ps1 | iex"
90
+ ```
91
+
92
+ 脚本做四件事:**找一个够新的 Node**(没有就下官方包到 `~/.omaw/node`,校验 sha256)、
93
+ **`npm install -g --prefix ~/.omaw`**(不写系统全局目录、不需要 sudo)、
94
+ **把 `omaw` 挂到 `~/.local/bin`**(必要时补 PATH)、**冒烟一次**(版本号 + node-pty 能否 require,即终端能不能开)。
95
+
96
+ | 想改的东西 | 用法(`sh` 版;PowerShell 版用环境变量) |
97
+ |---|---|
98
+ | 版本 | `OMAW_VERSION=0.7.0`(或 `--version 0.7.0`) |
99
+ | 装到别处 | `OMAW_HOME=/opt/omaw`(或 `--home /opt/omaw`) |
100
+ | 镜像源 | `OMAW_REGISTRY=https://registry.npmmirror.com`(或 `--registry …`) |
101
+ | 不动 PATH | `OMAW_NO_PATH=1`(或 `--no-modify-path`) |
102
+ | 装编译工具 | `OMAW_BUILD_TOOLS=1`(或 `--with-build-tools`) |
103
+ | 卸载 | `… \| sh -s -- --uninstall`(只删安装目录与软链,数据目录不动) |
104
+
105
+ Linux 上额外查一次 `make` / `g++` / `python3`:`node-pty` 只给 macOS / Windows 带预编译产物,
106
+ Linux 要就地编译 PTY。缺工具时脚本会说清楚装什么,或者直接 `--with-build-tools` 让它代劳。
107
+
108
+ > 装完在家目录里直接敲 `omaw` 也能用:家目录当工作区不安全(文件树 / 搜索 / agent 的 cwd 会是整个 home),
109
+ > 所以这时会自动改用 `<数据目录>/workspace` 并打一行提示;真想指定项目就用 `AGENT_WEB_CWD=/path/to/project`。
110
+
111
+ ### 装成全局命令
112
+
113
+ `oh-my-agent-web` / `omaw` / `agent-web` 三个 bin 指向同一入口,完全等价:
31
114
 
32
115
  ```bash
33
116
  npm i -g .
34
- agent-web -d # 后台启动(日志写到 <dataDir>/daemon.log)
35
- agent-web status # 看 pid / 端口 / 运行时长 / 会话数
36
- agent-web restart -d # 改完代码重启
37
- agent-web stop
117
+ omaw -d # 后台启动(日志写到 <dataDir>/daemon.log)
118
+ omaw status # pid / 端口 / 运行时长 / 引擎 / 会话数
119
+ omaw restart -d # 改完代码重启
120
+ omaw stop
121
+ omaw version # 当前版本;omaw upgrade 一键升级到最新发行版
38
122
  ```
39
123
 
40
- ### 常用参数
124
+ ### 命令行参数
41
125
 
42
126
  | 参数 | 环境变量 | 默认 | 说明 |
43
127
  | --- | --- | --- | --- |
@@ -45,13 +129,156 @@ agent-web stop
45
129
  | `-H, --host` | `AGENT_WEB_HOST` | `127.0.0.1` | 监听地址;对外访问才需要改 |
46
130
  | `-P, --password` | `AGENT_WEB_PASSWORD` | 无 | 访问密码;不设 = 只允许本机 |
47
131
  | `--allow-host` | `AGENT_WEB_ALLOWED_HOSTS` | 只认 loopback | 额外允许的域名 / 局域网 IP(可多次或逗号分隔) |
48
- | `--data-dir` | `AGENT_WEB_DATA_DIR` | `~/.oh-my-agent-web` | 密码、设置、上传、日志、会话缓存都放这 |
49
- | — | `AGENT_WEB_LOG` | `info` | `debug` / `info` / `warn` / `error` |
132
+ | `--data-dir` | `AGENT_WEB_DATA_DIR` | `~/.oh-my-agent-web` | 设置、密码、上传、日志、锁文件都放这 |
50
133
  | — | `AGENT_WEB_CWD` | `process.cwd()` | 默认工作区(还没开会话时用) |
134
+ | — | `AGENT_WEB_ENGINE` | `pi` | 默认引擎 |
135
+ | — | `AGENT_WEB_APPROVAL_MODE` | `allow` | 新会话的默认审批模式;设 `ask` 则逐次确认 |
136
+ | — | `AGENT_WEB_TURN_TIMEOUT_MS` | 20 分钟 | 单回合看门狗超时 |
137
+ | — | `AGENT_WEB_LOG` | `info` | `debug` / `info` / `warn` / `error` |
138
+
139
+ > 想从手机访问:`omaw -d -H 0.0.0.0 -P 你的密码 --allow-host 你的域名`,反代 / 隧道请带上 `X-Forwarded-For`(来源校验会看它)。
140
+
141
+ ---
51
142
 
52
- 数据目录里会有:`password`(设过一次就记住)、`allowed-hosts`、`settings.json`、`pi-sessions-cache.json`、`uploads/`、`logs/`、`server.lock`。
143
+ ## 界面导览
144
+
145
+ ```
146
+ ┌─ 顶栏:品牌 · 新会话 · 引擎选择 · 模型/技能/推送/语音/插件/MCP/子代理/设置 ──┐
147
+ ├────────────┬──────────────────────────────┬──────────────────────┤
148
+ │ 侧栏 │ 对话 │ 右栏 │
149
+ │ · 项目 │ · 消息流(思考段 + 工具卡) │ · 文件预览(多标签) │
150
+ │ · 会话 │ · steer / 排队消息 │ · 终端 │
151
+ │ · 归档 │ · 子代理运行卡片条 │ · 状态栏 │
152
+ │ · 文件区 │ · 输入框(附件 / slash) │ │
153
+ └────────────┴──────────────────────────────┴──────────────────────┘
154
+ ```
53
155
 
54
- > 想从手机访问:`agent-web -d -H 0.0.0.0 -P 你的密码 --allow-host 你的域名`,反代 / 隧道请带上 `X-Forwarded-For`(来源校验会看它)。
156
+ 侧栏与右栏都能拖宽 / 收起,宽度记在 `localStorage`。输入框支持 `/` 快捷指令、`@` 提及文件、拖拽附件与粘贴图片;上方那排 chip 是提示词模板、思考开关与建议动作。
157
+
158
+ 窄屏(手机 UA 或 ≤400px)自动切成覆盖抽屉,点会话后延迟收起;顶栏与状态栏里显示哪些块、过程块展开与否,都在设置里能关掉。
159
+
160
+ ---
161
+
162
+ ## 引擎
163
+
164
+ | 引擎 | 说明 |
165
+ | --- | --- |
166
+ | **pi**(默认) | 完整的插件 / 技能生态、工具延迟加载、审批门禁、目标模式与子代理;模型与 provider 由本工作台管理 |
167
+ | **Codex** | codex 官方 agent(app-server JSON-RPC),独立 loop 与沙箱,会话与 Codex Desktop / CLI 共用;审批是命令 / 文件级的 |
168
+ | **DeepSeek Harness** | DeepSeek 官方 agent(ACP),模型与思考档可切,权限逐次确认 |
169
+ | **opencode** | opencode 官方 agent(ACP),自带工具 / MCP / 插件生态;会话与 opencode CLI / TUI 共用。**审批取决于 opencode 自身的 `permission` 配置**(配成全局 allow 时不会询问) |
170
+ | **fake** | 零 token 的脚本化引擎,只用来验证协议 / 工具 / 审批流程(`AGENT_WEB_ENGINE=fake`,默认不进切换面板) |
171
+
172
+ 能力差异(`server/engine/registry.ts` 的能力矩阵):
173
+
174
+ | 能力 | pi | codex | dsh | opencode |
175
+ | --- | :-: | :-: | :-: | :-: |
176
+ | 审批 | ✅ | ✅ | ✅ | 看配置 |
177
+ | 模型 / 思考档 | ✅ | ✅ | ✅ | ✅ |
178
+ | 恢复历史会话 | ✅ | ✅ | ✅ | ✅ |
179
+ | 插队(steer) | ✅ | — | — | — |
180
+ | 子代理面板 | ✅ | — | — | — |
181
+ | 本工作台管 provider | ✅ | — | — | — |
182
+
183
+ 装没装的引擎不影响启动:只有切到它时才会报「没找到」。
184
+
185
+ 各引擎的逃生阀(都有默认值,通常不用配):
186
+
187
+ | 变量 | 作用 |
188
+ | --- | --- |
189
+ | `AGENT_WEB_OPENCODE_BIN` / `_ARGS` / `_MODEL` | opencode 可执行文件、额外参数(如 `--pure` 跳过外部插件)、新会话优先模型(`provider/model`) |
190
+ | `AGENT_WEB_DSH_BIN` / `_HOME` / `_MODEL` / `_PATCH` | DSH 可执行文件、数据目录、模型、补丁开关 |
191
+ | `AGENT_WEB_CODEX_BIN` / `_HOME` | codex 可执行文件与数据目录 |
192
+
193
+ ---
194
+
195
+ ## 会话与落盘
196
+
197
+ 不引数据库,**会话的事实源永远是引擎自己写的文件**。网页只是读它、改它的名字、必要时删它。
198
+
199
+ | 内容 | 位置 |
200
+ | --- | --- |
201
+ | pi 会话 | `~/.pi/agent/sessions/<编码后的 cwd>/<ISO>_<uuid>.jsonl`(首行 `{type:"session",id,timestamp,cwd}`) |
202
+ | Codex 会话 | Codex 自己的 thread 存储(走 `thread/list \| resume \| delete`) |
203
+ | opencode / DSH 会话 | 各自的数据目录(opencode 按 cwd `session/list`;DSH 读 `~/.dsh` 的 workspace 投影) |
204
+ | 会话标题 | pi 取首条 user 消息(只读头 60 行);改名 = 往 jsonl 末尾追加 `session_info.name` |
205
+ | 工作台状态 | `<dataDir>/settings.json`、`approval-rules.json`、`server.lock`、`logs/`、`uploads/`、`push-subscriptions.json`、`vapid-keys.json` |
206
+
207
+ 「删除会话」是**改名到 `.deleted-<时间戳>`**(引擎自带删除接口时优先用原生的),文件还在磁盘上,能捞回来。
208
+
209
+ ---
210
+
211
+ ## 模型与供应商
212
+
213
+ 「模型」面板左栏分两组:**官方内置服务商**(pi 自带目录里有模型的那批)与**自定义服务商**。
214
+
215
+ - 内置那批只要**填一把 API key** 就能用 —— 端点与模型表都由 pi 的目录提供。
216
+ - 自定义可以逐个加模型、改 base url、从别的客户端**导入**(实际是拉 `GET {baseUrl}/models` 勾选写回)、逐个**试连**。
217
+ - **key 不出服务端**:接口只返回 `hasKey` 与尾 4 位;写操作在进程内串行化,避免并发写坏文件。
218
+
219
+ 配置真相分散在三处 + 引擎目录:`~/.pi/agent/models.json`(pi 自己的 provider 目录,可写)、
220
+ `models-store.json`(历史位置)、`auth.json`(`api_key` 或 oauth 订阅凭据),白名单在 `settings.json.enabledModels`。
221
+
222
+ ---
223
+
224
+ ## 审批与安全模型
225
+
226
+ - 默认只监听 `127.0.0.1`,并校验 `Origin` / `Host`(含 `AGENT_WEB_ALLOWED_HOSTS` 白名单、vite 端口、局域网地址)。
227
+ - 身份判定:对端是回环 **且** Host 是回环 → 放行;未设密码时远程一律 403;设了密码则走 cookie(`agent-web-auth`,7 天 HttpOnly)或 HTTP Basic;登录失败按 20 次 / 10 分钟节流。
228
+ - 工具与命令走审批:**只读工具**(read / grep / find / ls)与只读命令自动放行,其余按 `scopeKey`(工具 + 可执行名或目标目录)询问;点「总是允许」就把规则写进 `approval-rules.json`(原子写,坏文件当空)。
229
+ - 会话回合结束时清掉该会话所有悬置审批,不会挂在那等下一个人点。
230
+ - 上传的临时文件、会话缓存都在数据目录里,不写进你的项目目录。
231
+
232
+ > 远程暴露是显式操作:必须同时给 `-H`、`-P`、`--allow-host`,缺一个都不通。
233
+
234
+ ---
235
+
236
+ ## 子代理(并行 + 可管理)
237
+
238
+ pi 引擎带一套自己的子代理实现(**不依赖 pi-subagents 插件**):
239
+
240
+ - **怎么开**:主 Agent 拿到 `Agent` 工具(`subagent_type` / `prompt` / `description` / `run_in_background`),一次可以并行开好几个;每次委派都是**一个真实的 pi 会话**(自己的上下文、会话文件、工具白名单)。
241
+ - **在哪看**:运行卡片在对话下方(输入区上方那条「子代理」卡片条):状态、耗时、工具次数、token 与花费,点开原地展开结果原文与会话文件路径,随时停掉某一个;切会话自动换成本会话的那批。顶栏徽标保留一眼概览。
242
+ - **怎么管**:面板里管的是**模板**(内置 explore / general-purpose / plan,可停用;可新建 / 编辑 / 删除)。模板落 `~/.pi/agent/agents/*.md`,frontmatter 写 `name / display_name / description / tools / model / thinking / max_turns`,正文就是系统指令 —— 与 pi 的 agent 文件同一套格式,可读可改可进 git。
243
+ - **并发**:面板可设上限(1–12,默认 4),超出的排队;总开关关掉后模型不再看到 `Agent` 工具。
244
+ - **隔离**:子代理默认不加载技能 / 插件 / 主题 / 上下文文件(快、可预期);模型不跟随父会话时沿用父会话的模型。有的模型由扩展注册 provider,这类模型在子代理里会失败并提示「在模板里指定模型」。
245
+
246
+ 子代理会话文件紧挨着父会话归档(`<父会话目录>/<父会话名>/subagents/`),不进侧栏的项目清单。
247
+
248
+ ---
249
+
250
+ ## MCP · 插件 · 技能
251
+
252
+ **MCP**:读写 `~/.pi/agent/mcp.json`(全局)与 `<workspace>/.pi/mcp.json`(项目),项目同名条目覆盖全局;写回保留缩进与 CRLF。
253
+ exposure 四档 `codemode | deferred | direct | hidden`,工具命名空间 `mcp__<name>`。
254
+ **配置即真相** —— pi 会话启动时自行连接,面板改完对**新会话**生效。测试连接走 `@earendil-works/pi-mcp` 真实握手 + `tools/list` 后即断;
255
+ OAuth 走 `pi mcp login`。另有市场入口(registry / smithery / github)与「粘贴命令 / URL / JSON 自动解析」。
256
+
257
+ **插件**:就是 pi package —— `settings.json` 的 `packages`(npm / git / 本地)+ 散装 `~/.pi/agent/extensions/`、`<ws>/.pi/extensions/`。
258
+ 状态由 pi SDK 的 `resolve()` 决定(查询**不**触发 npm install),四态 `loaded / installed / missing / disabled`;停用写 `autoload:false`,不删文件。
259
+
260
+ **技能**:读 `~/.agents/skills/<目录>/SKILL.md`,列表只解析 frontmatter(不引 yaml 依赖),文件清单 ≤ 200 条。
261
+ **启停 = 把 `SKILL.md` 改名成 `.SKILL.md.disabled`**,没有第二份状态文件。Skill Hub 支持分类搜索 + zip 安装,也支持 git clone 指定 `skills/<name>`。
262
+
263
+ ---
264
+
265
+ ## 架构:SSE + RPC,没有 WebSocket
266
+
267
+ 早期版本用 WS,现在**只有两条 HTTP 通道**(ADR-0032):
268
+
269
+ ```
270
+ 浏览器 ──GET /api/events(SSE)──▶ 服务端 快照 · message_delta · notice
271
+ ──POST /api/rpc (JSON)──▶ ClientRequest → { reply } 就地消费
272
+ ```
273
+
274
+ - **下行**:首帧一份完整快照,其后只推增量。正文走 `message_delta` 逐块追加;工具卡、错误、状态这类结构性变化推快照(60ms 窗口合并,快照可丢、事件不可丢)。SSE 25s 心跳,支持 gzip `Z_SYNC_FLUSH`。
275
+ - **上行**:`send_message`(text / attachments / mode)、`interrupt`、`approve`、`new_chat`、`switch_conversation`、`set_engine` / `set_model` / `set_thinking_level` / `set_approval_mode`、`cancel_queued`、`rename_session`、provider 与子代理若干。
276
+ - **协议单源**在 `server/protocol.ts`,前端复用类型,两端靠单测对齐。
277
+ - **快照是唯一事实源**:前端 `store.ts` 用 `useSyncExternalStore`,快照一到整体替换;本地只留 `preview` / `draft` / `pendingActive` 这类乐观态。
278
+
279
+ **进程模型**:`cli.ts` 解析参数 → `session/lock.ts` 抢 `<dataDir>/server.lock`(`{pid,port,startedAt}`;pid 活则拒绝启动,pid 死则接管陈旧锁)→ `runtime.init()` → `listen`。`SIGINT/SIGTERM` 与 `/api/shutdown` 走同一个优雅退出(flush、断开引擎、释放锁)。
280
+
281
+ ---
55
282
 
56
283
  ## 开发
57
284
 
@@ -65,85 +292,99 @@ npm run dev # 一条命令起两个进程
65
292
 
66
293
  | 命令 | 作用 |
67
294
  | --- | --- |
295
+ | `npm test` | `node --test` 跑 `tests/unit/*.test.ts`(22 个文件,零额外依赖) |
68
296
  | `npm run typecheck` | 服务端 + 前端两套 `tsc --noEmit` |
69
297
  | `npm run lint` | oxlint(`server` / `web/src` / `scripts`) |
70
- | `npm run format` | prettier 全仓格式化(`format:check` 只检查) |
298
+ | `npm run format` | prettier 全仓格式化(`format:check` 只检查;`*.md` 故意跳过) |
71
299
  | `npm run build` | 服务端 `tsc` + 前端 `vite build` 到 `web/dist` |
72
- | `npm run icons` | 重新生成 PWA 图标 |
300
+ | `npm run icons` | 用 playwright-core 重新生成 PWA 图标 |
301
+
302
+ CI(`.github/workflows/ci.yml`):`npm ci` → typecheck → lint → test → build,跑在 ubuntu + Node 22。
73
303
 
74
- ## 目录结构
304
+ ### 目录结构
75
305
 
76
306
  ```
77
307
  oh-my-agent-web/
78
- ├─ server/ 后端(Express + ws)
79
- │ ├─ index.ts 进程入口:路由、WS、静态产物、优雅退出
80
- │ ├─ cli.ts agent-web / omaw 命令(start / stop / restart / status / upgrade)
81
- │ ├─ protocol.ts WS 协议单源(前端通过 web/src/types.ts 复用类型)
82
- │ ├─ engine/ 引擎适配层:pi(原生 SDK)/ codex / dsh / opencode(ACP)/ fake(测试)
83
- │ ├─ session/ 会话运行时:切换、改名、删除、模型控制、审批桥
84
- │ ├─ sessions/ 会话文件扫描(标题 / 模型 / 缓存)
85
- │ ├─ files/ 文件树、预览、搜索、上传、git 分支
86
- │ ├─ terminal/ PTY 终端
87
- │ ├─ approval/ 审批规则与作用域判断
88
- │ ├─ providers/ 自定义 provider / 模型清单
89
- │ ├─ push/ Web Push
90
- │ ├─ skills/ 技能发现与启停
91
- │ └─ subagents/ 子代理模板(落 ~/.pi/agent/agents/*.md,与 pi 共用同一套)
92
- ├─ web/ 前端(React 19 + Vite)
93
- │ └─ src/
94
- │ ├─ components/ 侧栏、消息流、输入框、文件树、终端、各类面板
95
- │ ├─ styles/ 按职责拆分的 CSS(无框架,一套 CSS 变量)
96
- │ └─ use-*.ts 搜索 / 分支 / 分栏 / 复制等小 hooks
97
- ├─ scripts/ dev 双进程、图标生成
308
+ ├─ server/ 后端(Express 5,TypeScript ESM)
309
+ │ ├─ cli.ts 命令入口:start / stop / restart / status / version / upgrade
310
+ │ ├─ index.ts 进程装配:中间件、路由、静态产物、优雅退出
311
+ │ ├─ protocol.ts 协议单源(前端复用类型)
312
+ │ ├─ engine/ 引擎适配层:pi(原生 SDK)/ codex / dsh / opencode(ACP)/ fake
313
+ │ │ + 能力矩阵 registry、catalog、wiring、pi-plugins
314
+ │ ├─ session/ 运行时:runtime、turn-*、审批桥、改名删除、锁、settings
315
+ │ ├─ sessions/ 会话删除的 HTTP 路由(扫描逻辑在 session/pi-sessions.ts)
316
+ │ ├─ snapshot/ 服务端状态快照:state、buildSnapshot、fan-out、60ms 合并
317
+ │ ├─ http/ auth、live-routes(SSE + RPC)、aux-routes、压缩、文件响应头
318
+ │ ├─ files/ 文件树、预览、搜索、媒体流、git 分支
319
+ │ ├─ terminal/ PTY 会话与 SSE 流
320
+ │ ├─ approval/ 审批作用域与规则
321
+ │ ├─ providers/ 供应商 / 模型清单 / 试连 / 导入 / 订阅 OAuth
322
+ │ ├─ mcp/ mcp.json 读写、连通性探测、OAuth、市场
323
+ │ ├─ skills/ 技能发现、启停、Hub 安装
324
+ │ ├─ subagents/ 子代理模板存储(`~/.pi/agent/agents/*.md`)
325
+ │ ├─ plugins/ 插件包 HTTP 路由
326
+ │ ├─ push/ Web Push(VAPID)
327
+ │ ├─ markers/ `[[action:suggest:…]]` 标记的解析与流式过滤
328
+ │ └─ util/ env、paths、spawn-cli(跨平台)、origin 等
329
+ ├─ web/ 前端(React 19 + Vite + TypeScript)
330
+ │ ├─ src/components/ ~70 个组件:侧栏、消息流、输入框、文件树、终端、各类面板
331
+ │ ├─ src/styles/ ~45 个分区 CSS + 6 套主题变量(无 CSS 框架)
332
+ │ ├─ src/ws.ts SSE + RPC 客户端(名字是历史遗留)
333
+ │ └─ src/store.ts 快照 store(useSyncExternalStore)
334
+ ├─ scripts/ dev 双进程、安装脚本、prebuild 裁剪、图标生成
335
+ ├─ tests/unit/ node:test 单测
336
+ ├─ docs/adr/README.md ADR 索引
337
+ └─ docs/screenshots/ README 用的真实截图(PNG 256 色,~1.4MB)
98
338
  ```
99
339
 
100
- ## 引擎
340
+ ### 代码约定
101
341
 
102
- | 引擎 | 说明 |
103
- | --- | --- |
104
- | **pi**(默认) | 完整的插件 / 技能生态、工具延迟加载、审批门禁、目标模式与子代理;模型与 provider 由本工作台管理 |
105
- | **Codex** | codex 官方 agent,独立 loop 与沙箱,会话与 Codex Desktop / CLI 共用;审批是命令 / 文件级的 |
106
- | **DeepSeek Harness** | DeepSeek 官方 agent(ACP),模型与思考档可切,权限逐次确认 |
107
- | **opencode** | opencode 官方 agent(ACP),自带工具 / MCP / 插件生态,模型与思考档可切,会话与 opencode CLI / TUI 共用;**审批取决于 opencode 自身的 `permission` 配置**(配成全局 allow 时不会询问) |
108
- | **fake** | 零 token 的脚本化引擎,只用来验证协议 / 工具 / 审批流程(`AGENT_WEB_ENGINE=fake`,默认不进切换面板) |
342
+ - prettier:**Tab 缩进**、`printWidth 120`;oxlint 用默认规则集。
343
+ - 单文件 400 行是护栏,超了就拆(`cli-info.ts`、`snapshot/*` 都是这么来的)。
344
+ - **ADR 正文写在它约束的那段代码的文件头注释里**,不在 `docs/` 单独立文 —— 改代码的人一定会读到;`docs/adr/README.md` 只是按编号索引到文件的表。改代码时顺手更新那张表。
109
345
 
110
- 装没装的引擎不影响启动:只有切到它时才会报「没找到」。
346
+ ---
111
347
 
112
- opencode 相关的环境变量(都有默认值,通常不用配):
348
+ ## 安装体积:按场景选命令
113
349
 
114
- | 变量 | 作用 |
115
- | --- | --- |
116
- | `AGENT_WEB_OPENCODE_BIN` | opencode 可执行文件路径(默认 PATH 里的 `opencode`) |
117
- | `AGENT_WEB_OPENCODE_ARGS` | 传给 `opencode acp` 的额外参数,空格分隔(例如 `--pure` 跳过外部插件) |
118
- | `AGENT_WEB_OPENCODE_MODEL` | 新会话优先用的模型(`provider/model`);不设就沿用 opencode 自己的默认 |
350
+ 依赖树里大头都在**前端构建期**(`mermaid` 121MB、react 等)与**只给本机用的构建工具**,服务器真正运行时只需要
351
+ `express` / `web-push` / `node-pty` / `@earendil-works/pi-*` 这几样 —— 浏览器那一半已经打进 `web/dist` 了。
352
+ 所以 `react` / `mermaid` / `xterm` / `highlight.js` 这类包全部放在 `devDependencies`:
119
353
 
120
- ## 子代理(多开 + 可管理)
354
+ | 场景 | 命令 | 包数 | 磁盘 |
355
+ |---|---|---|---|
356
+ | 开发 / 要自己构建 | `npm ci` | 456 | 547 MB |
357
+ | 已有构建产物,只想跑 | `npm ci --omit=dev` | 192 | **158 MB**(~3 秒) |
358
+ | 构建完顺手瘦身 | `npm ci && npm run build && npm prune --omit=dev` | 192 | **158 MB** |
359
+ | 分发给另一台机器 | `npm pack` → `npm i -g ./oh-my-agent-web-*.tgz` | 193 | 223 MB(tarball 3.1 MB,含截图) |
121
360
 
122
- pi 引擎带一套自己的子代理实现(**不依赖 pi-subagents 插件**):
361
+ 两个自动瘦身动作已经在 `postinstall` 里(`scripts/prune-prebuilds.mjs`):
123
362
 
124
- - **怎么开**:主 Agent 拿到 `Agent` 工具(`subagent_type` / `prompt` / `description` / `run_in_background`),
125
- 一次可以并行开好几个;每次委派都是**一个真实的 pi 会话**(自己的上下文、会话文件、工具白名单)。
126
- - **在哪看**:运行卡片**就在对话下方**(输入区上方那条「子代理」卡片条):状态、耗时、工具次数、token 与花费,点「查看结果」原地展开结果原文与会话文件路径,随时**停掉某一个**;
127
- 卡片条可折叠,切会话自动换成本会话的那批。顶栏「子代理」徽标保留一眼概览(几个在跑 / 合计花销),点开是小浮层。
128
- - **怎么管**:同一个面板只剩**模板**(内置 explore / general-purpose / plan,可停用;可新建 / 编辑 / 删除),运行记录不在面板里。
129
- 模板落 `~/.pi/agent/agents/*.md`,frontmatter 写 `name / description / tools / model / thinking / max_turns`,
130
- 正文就是子代理的系统指令 —— 与 pi / pi-web 的 agent 文件同一套格式,直接可读可改可进 git。
131
- - **并发**:面板右上角设上限(1–12,默认 4),超出的排队;总开关关掉后模型不再看到 `Agent` 工具。
132
- - **隔离**:子代理默认不加载技能 / 插件 / 主题 / 上下文文件(快、可预期);模型不跟随父会话时会沿用父会话的模型。
133
- 有的模型由扩展注册 provider,这类模型在子代理里会失败并提示「在模板里指定模型」。
363
+ - **只留当前平台的 `node-pty` 预编译产物**:它默认带四个平台的二进制,其中 Windows 那两份就占 58MB。macOS / Linux 上剪掉后 62MB → 5MB;`npm ci` 会原样装回来。
364
+ - 修 `spawn-helper` 的可执行位。
134
365
 
135
- 子代理的会话文件紧挨着父会话归档(`<父会话目录>/<父会话名>/subagents/`),不进侧栏的项目清单。
366
+ > 剩下的 158MB 里,基本全是 pi 的模型层:各家 provider 的官方 SDK(`openai` 17.6MB、`@google/genai` 10.8MB、`@anthropic-ai/sdk` 9.1MB、`@aws-sdk/*` + `@smithy/*` 5.2MB)——
367
+ > 这些都是 `@earendil-works/pi-ai` 的硬依赖,不做供应商裁剪就绕不开。
136
368
 
137
- ## 安全模型
369
+ ---
138
370
 
139
- - 默认只监听 `127.0.0.1`,并校验 `Origin` / `Host`;要远程访问必须显式给 `-H` + `-P` + `--allow-host`。
140
- - 工具与命令走审批:只读命令自动放行,其余按规则询问(规则可积累)。
141
- - 删除会话是**改名到 `.deleted-<时间戳>`**,文件还在磁盘上,能捞回来。
142
- - 上传的临时文件、会话缓存都在数据目录里,不写进你的项目目录。
371
+ ## 排错
372
+
373
+ > **为什么用 `npm ci` 而不是 `npm install`**:本项目的直接依赖普遍很新(多数在发布后一个月内)。
374
+ > `npm install` 会拿 `package.json` 里的版本范围去 registry 重新解析,一旦用的镜像源(公司 Nexus、npmmirror 等)还没同步到那些版本,就会报 `npm error notarget No matching version found for …`。
375
+ > `npm ci` 直接用锁文件里的精确版本,完全不走这一步。
376
+
377
+ 按这个顺序排查:
378
+
379
+ 1. `npm error notarget No matching version found` → 镜像没同步。先 `npm config get registry`,再临时用官方源重试:`npm ci --registry=https://registry.npmjs.org/`。
380
+ 2. `EBADENGINE` / pi 启动即报 Node 版本 → 需要 Node ≥ 22.19(`node -v` 确认)。
381
+ 3. `node-pty` 编译失败(多见于 Linux)→ 它的预编译产物只随包提供 macOS / Windows,Linux 上会现场编译:需要 `python3`、`make`、`g++`。
382
+ 4. 起不来但 `omaw status` 说「陈旧锁」→ 之前进程没退干净,下次 `start` 会自动接管;确实僵着就删 `<dataDir>/server.lock`。
383
+ 5. 端口被占 → `omaw -p 25258`,或先 `omaw stop`。
143
384
 
144
- ## 文档
385
+ Windows 上踩过的坑(shell 解析、killTree、路径判断、chmod、CRLF)记在 `WINDOWS-COMPAT.md`,按 P0/P1/P2 分级。
145
386
 
146
- 代码里的 `ADR-00xx` 引用是设计决策编号;具体缘由写在对应文件的头注释里。
387
+ ---
147
388
 
148
389
  ## License
149
390
 
@@ -8,6 +8,7 @@
8
8
  import { spawn } from "node:child_process";
9
9
  import process from "node:process";
10
10
  import { defaultIsAlive, readLock } from "./session/lock.js";
11
+ import { resolveCli } from "./util/spawn-cli.js";
11
12
  import { packageMeta } from "./util/version.js";
12
13
  /** 探活:拿服务端自报的引擎 / 会话数 / 工作区;连不上就 null */
13
14
  export async function fetchHealth(host, port, timeoutMs = 1500) {
@@ -85,7 +86,9 @@ export async function upgrade() {
85
86
  }
86
87
  console.log(`当前 v${meta.version},正在安装 npm 上的最新版 ${meta.name} …`);
87
88
  const code = await new Promise((resolve) => {
88
- const child = spawn("npm", ["install", "-g", `${meta.name}@latest`], { stdio: "inherit" });
89
+ // Windows 上 `npm` 是 npm.cmd:resolveCli 负责改成 cmd.exe /c(否则 EINVAL)
90
+ const command = resolveCli("npm", ["install", "-g", `${meta.name}@latest`]);
91
+ const child = spawn(command.file, command.args, { ...command.options, stdio: "inherit" });
89
92
  child.on("error", (error) => {
90
93
  console.error(`调用 npm 失败:${error.message}`);
91
94
  resolve(1);
@@ -23,7 +23,7 @@
23
23
  * 没给的选项会从 `<dataDir>/password`、`<dataDir>/allowed-hosts` 兜底(见 cli-config.ts):
24
24
  * 这样 `restart` 少带参数也不会把远程访问关掉。
25
25
  */
26
- import { spawn } from "node:child_process";
26
+ import { spawn, spawnSync } from "node:child_process";
27
27
  import { closeSync, mkdirSync, openSync, readFileSync, realpathSync } from "node:fs";
28
28
  import { homedir } from "node:os";
29
29
  import { dirname, join, resolve } from "node:path";
@@ -111,6 +111,35 @@ function dataDir() {
111
111
  return fromEnv && fromEnv !== "" ? resolve(fromEnv) : join(homedir(), DEFAULT_DATA_DIR_NAME);
112
112
  }
113
113
  const sleep = (ms) => new Promise((done) => setTimeout(done, ms));
114
+ /** 等到进程消失或超时;返回是否已经退了 */
115
+ async function waitGone(pid, timeoutMs) {
116
+ const deadline = Date.now() + timeoutMs;
117
+ while (Date.now() < deadline) {
118
+ await sleep(100);
119
+ if (!defaultIsAlive(pid))
120
+ return true;
121
+ }
122
+ return !defaultIsAlive(pid);
123
+ }
124
+ /**
125
+ * 请服务自己收尾(flush 转录 → 收掉引擎子进程 → 还给锁)。
126
+ *
127
+ * 为什么要走 HTTP 而不是发信号:**Windows 上 `SIGTERM` 被映射成 TerminateProcess,
128
+ * 没有任何进程内处理器会跑** —— 发信号就是在回合写到一半时硬断电。
129
+ * 安全上不多开口子:这个端点挂在鉴权之后,而本机回环本来就直接放行。
130
+ */
131
+ async function requestShutdown(host, port) {
132
+ try {
133
+ const response = await fetch(`http://${host}:${port}/api/shutdown`, {
134
+ method: "POST",
135
+ signal: AbortSignal.timeout(3000),
136
+ });
137
+ return response.ok;
138
+ }
139
+ catch {
140
+ return false;
141
+ }
142
+ }
114
143
  /** 停掉锁文件里记录的进程;返回是否真的停了一个 */
115
144
  async function stopServer() {
116
145
  const lock = await readLock(dataDir());
@@ -123,22 +152,34 @@ async function stopServer() {
123
152
  return false;
124
153
  }
125
154
  console.log(`正在停止 pid ${lock.pid}(端口 ${lock.port})…`);
155
+ // ① 优雅路径:请它自己收尾。Windows 上这是唯一可行的优雅方式。
156
+ const asked = await requestShutdown(probeHostOf(), lock.port);
157
+ if (asked && (await waitGone(lock.pid, 8000))) {
158
+ console.log("已停止");
159
+ return true;
160
+ }
161
+ // ② 兜底:动硬的
162
+ console.log(asked ? "8 秒内没退出,强制结束" : "优雅停机没响应,强制结束");
163
+ if (process.platform === "win32") {
164
+ // 必须 /T:node 还拉着 cmd.exe → 引擎子进程,只杀父进程会留一堆孤儿
165
+ try {
166
+ spawnSync("taskkill", ["/pid", String(lock.pid), "/T", "/F"], { stdio: "ignore", windowsHide: true });
167
+ }
168
+ catch {
169
+ // 已经不在了就算了
170
+ }
171
+ return true;
172
+ }
126
173
  try {
127
174
  process.kill(lock.pid, "SIGTERM");
128
175
  }
129
- catch (error) {
130
- console.error(`发送 SIGTERM 失败:${error instanceof Error ? error.message : String(error)}`);
176
+ catch {
131
177
  return false;
132
178
  }
133
- // 给它 8 秒优雅退出(回合可能正在落盘转录)
134
- for (let i = 0; i < 80; i += 1) {
135
- await sleep(100);
136
- if (!defaultIsAlive(lock.pid)) {
137
- console.log("已停止");
138
- return true;
139
- }
179
+ if (await waitGone(lock.pid, 3000)) {
180
+ console.log("已停止");
181
+ return true;
140
182
  }
141
- console.log("8 秒内没退出,强制结束");
142
183
  try {
143
184
  process.kill(lock.pid, "SIGKILL");
144
185
  }
@@ -214,6 +255,8 @@ async function startDetached(rawArgv) {
214
255
  stdio: ["ignore", fd, fd],
215
256
  env: process.env,
216
257
  cwd: process.cwd(),
258
+ // Windows 上 detached 会连带开一个 console:不关掉就会闪一下黑框
259
+ windowsHide: true,
217
260
  });
218
261
  child.unref();
219
262
  closeSync(fd);