oh-my-agent-web 0.2.0 → 0.4.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 (250) hide show
  1. package/README.md +86 -447
  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 +61 -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/pi-events.js +148 -0
  26. package/dist/server/engine/pi-rename.js +14 -0
  27. package/dist/server/engine/pi-sdk.js +343 -0
  28. package/dist/server/engine/pi-session-model.js +23 -0
  29. package/dist/server/engine/pi-thinking.js +34 -0
  30. package/dist/server/engine/pi-transcript.js +225 -0
  31. package/dist/server/engine/pi-version.js +11 -0
  32. package/dist/server/engine/port.js +1 -0
  33. package/dist/server/engine/registry.js +47 -0
  34. package/dist/server/engine/scratch.js +13 -0
  35. package/dist/server/engine/suggestion-guidance.js +9 -0
  36. package/dist/server/engine/switchable.js +22 -0
  37. package/dist/server/engine/wiring.js +107 -0
  38. package/dist/server/files/dir-routes.js +40 -0
  39. package/dist/server/files/file-search.js +63 -0
  40. package/dist/server/files/file-service.js +232 -0
  41. package/dist/server/files/files-routes.js +182 -0
  42. package/dist/server/files/git-info.js +186 -0
  43. package/dist/server/http/auth-routes.js +99 -0
  44. package/dist/server/http/auth.js +191 -0
  45. package/dist/server/http/aux-routes.js +24 -0
  46. package/dist/server/http/compress.js +75 -0
  47. package/dist/server/http/health-and-static.js +33 -0
  48. package/dist/server/http/live-routes.js +113 -0
  49. package/dist/server/http/login-page.js +200 -0
  50. package/dist/server/index.js +342 -520
  51. package/dist/server/markers/actions.js +262 -0
  52. package/dist/server/protocol-parse.js +201 -0
  53. package/dist/server/protocol-settings.js +1 -0
  54. package/dist/server/protocol.js +6 -0
  55. package/dist/server/providers/catalog-store.js +85 -0
  56. package/dist/server/providers/catalog-writes.js +197 -0
  57. package/dist/server/providers/catalog.js +77 -0
  58. package/dist/server/providers/enabled-models.js +118 -0
  59. package/dist/server/providers/import-models.js +69 -0
  60. package/dist/server/providers/import-routes.js +136 -0
  61. package/dist/server/providers/model-test.js +52 -0
  62. package/dist/server/providers/probe.js +64 -0
  63. package/dist/server/providers/provider-handlers.js +203 -0
  64. package/dist/server/providers/provider-store.js +327 -0
  65. package/dist/server/providers/request-headers.js +7 -0
  66. package/dist/server/providers/validate.js +241 -0
  67. package/dist/server/push/push-routes.js +114 -0
  68. package/dist/server/push/push.js +120 -0
  69. package/dist/server/session/approval-bridge.js +103 -0
  70. package/dist/server/session/attachment-resolve.js +21 -0
  71. package/dist/server/session/client-view.js +82 -0
  72. package/dist/server/session/conversation-create.js +65 -0
  73. package/dist/server/session/delete-session.js +84 -0
  74. package/dist/server/session/engine-switch.js +108 -0
  75. package/dist/server/session/lock.js +82 -0
  76. package/dist/server/session/model-controller.js +111 -0
  77. package/dist/server/session/notices.js +31 -0
  78. package/dist/server/session/pi-opener.js +123 -0
  79. package/dist/server/session/pi-refresh.js +51 -0
  80. package/dist/server/session/pi-sessions-provider.js +46 -0
  81. package/dist/server/session/pi-sessions.js +272 -0
  82. package/dist/server/session/refresh-actions.js +42 -0
  83. package/dist/server/session/rename-session.js +23 -0
  84. package/dist/server/session/runtime-services.js +28 -0
  85. package/dist/server/session/runtime-types.js +1 -0
  86. package/dist/server/session/runtime.js +358 -0
  87. package/dist/server/session/session-stats.js +88 -0
  88. package/dist/server/session/settings.js +367 -0
  89. package/dist/server/session/snapshot-for.js +7 -0
  90. package/dist/server/session/stats-refresher.js +50 -0
  91. package/dist/server/session/switch-conversation.js +86 -0
  92. package/dist/server/session/throttle.js +14 -0
  93. package/dist/server/session/turn-events.js +126 -0
  94. package/dist/server/session/turn-finish.js +79 -0
  95. package/dist/server/session/turn-markers.js +26 -0
  96. package/dist/server/session/turn-queue.js +111 -0
  97. package/dist/server/session/turn-send.js +88 -0
  98. package/dist/server/session/turn-watchdog.js +77 -0
  99. package/dist/server/sessions/session-routes.js +17 -0
  100. package/dist/server/skills/skill-routes.js +28 -0
  101. package/dist/server/skills/skill-store.js +134 -0
  102. package/dist/server/snapshot/fan-out.js +10 -0
  103. package/dist/server/snapshot/messages.js +75 -0
  104. package/dist/server/snapshot/sender.js +62 -0
  105. package/dist/server/snapshot/state.js +193 -0
  106. package/dist/server/terminal/terminal-routes.js +176 -0
  107. package/dist/server/terminal/terminal-session.js +140 -0
  108. package/dist/server/uploads/upload-routes.js +64 -0
  109. package/dist/server/uploads/upload-store.js +115 -0
  110. package/dist/server/util/data-dir.js +36 -0
  111. package/dist/server/util/env.js +140 -0
  112. package/dist/server/util/ids.js +28 -0
  113. package/dist/server/util/log-file.js +67 -0
  114. package/dist/server/util/log.js +52 -0
  115. package/dist/server/util/origin.js +75 -0
  116. package/dist/server/util/paths.js +60 -0
  117. package/dist/server/util/pricing-index.js +121 -0
  118. package/dist/server/util/pricing.js +214 -0
  119. package/dist/server/util/startup.js +34 -0
  120. package/dist/server/util/upstream-version.js +140 -0
  121. package/dist/server/util/usage.js +19 -0
  122. package/dist/server/util/version.js +50 -0
  123. package/dist/server/ws/handle-message.js +95 -0
  124. package/package.json +49 -63
  125. package/web/dist/apple-touch-icon-precomposed.png +0 -0
  126. package/web/dist/apple-touch-icon.png +0 -0
  127. package/web/dist/assets/abnfDiagram-WEYOIA3C-NLp09iEC.js +1 -0
  128. package/web/dist/assets/arc-DviL9o3c.js +1 -0
  129. package/web/dist/assets/architecture-WOLXFQ4H-fjsM_Sy3.js +1 -0
  130. package/web/dist/assets/architectureDiagram-ZHVYKT53-Cn3B4TE8.js +36 -0
  131. package/web/dist/assets/array-BifhSqXX.js +1 -0
  132. package/web/dist/assets/blockDiagram-RJAE7QA3-CIuvxWvi.js +140 -0
  133. package/web/dist/assets/c4Diagram-XDPPPE54-DvdKmEpF.js +38 -0
  134. package/web/dist/assets/channel-C3AQ6nAs.js +1 -0
  135. package/web/dist/assets/chunk-2E7WHVMD-HNNASGCS.js +88 -0
  136. package/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  137. package/web/dist/assets/chunk-4EMNKR2T-Cg-eRwBE.js +1 -0
  138. package/web/dist/assets/chunk-55XCU76K-BweiisZL.js +1 -0
  139. package/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  140. package/web/dist/assets/chunk-7S6XSTFO-BCzIiP-6.js +2 -0
  141. package/web/dist/assets/chunk-AQDTSKZB-Dl0NRFuU.js +72 -0
  142. package/web/dist/assets/chunk-DSNLSHJZ-zhHCjjN9.js +129 -0
  143. package/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
  144. package/web/dist/assets/chunk-HAGFOTED-BDQqU2EH.js +62 -0
  145. package/web/dist/assets/chunk-HNE4Q36P-B-qRzMjX.js +1 -0
  146. package/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
  147. package/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  148. package/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
  149. package/web/dist/assets/chunk-NQOKRVIF-CNcvhMn_.js +213 -0
  150. package/web/dist/assets/chunk-UHW7PFBP-Dwr8LUzr.js +1 -0
  151. package/web/dist/assets/chunk-VQ5W62QI-O8sN6jJu.js +1 -0
  152. package/web/dist/assets/chunk-WQQL4CDK-B6-59Zg2.js +10 -0
  153. package/web/dist/assets/chunk-XXDRQBXY-DvSbeUlZ.js +1 -0
  154. package/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  155. package/web/dist/assets/chunk-YZO67HFG-BXpKYKsw.js +1 -0
  156. package/web/dist/assets/chunk-ZAUX2LTX-B1Z5VSkz.js +1 -0
  157. package/web/dist/assets/chunk-ZCHMQSOR-h_5mBLIA.js +2 -0
  158. package/web/dist/assets/classDiagram-v2-4MYWR3UA-Dr6JnaaV.js +217 -0
  159. package/web/dist/assets/cose-bilkent-JH36ORCC-36ShkR7R.js +1 -0
  160. package/web/dist/assets/cynefin-EF2NZ3EQ-Dg6bI7pC.js +1 -0
  161. package/web/dist/assets/cynefinDiagram-WF24UDRP-B0YHpBlR.js +62 -0
  162. package/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
  163. package/web/dist/assets/dagre-NK4GIZVX-Cx3mn7YS.js +4 -0
  164. package/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
  165. package/web/dist/assets/diagram-CBK3MWTC-l8aZG_Bm.js +24 -0
  166. package/web/dist/assets/diagram-I5XIIV2D-Bd5f14_h.js +24 -0
  167. package/web/dist/assets/diagram-ODIL2UCW-hBsxCYUj.js +30 -0
  168. package/web/dist/assets/diagram-PFBEJUXE-BgpIBUYl.js +3 -0
  169. package/web/dist/assets/diagram-RJA3I4VV-CDU1gUVD.js +41 -0
  170. package/web/dist/assets/diagram-TYUNFSNR-Iuxh7Tqf.js +200 -0
  171. package/web/dist/assets/dist-C86cKTuE.js +1 -0
  172. package/web/dist/assets/ebnfDiagram-V4P3TTPT-Cms0HFKh.js +1 -0
  173. package/web/dist/assets/elk-QONTBCTQ-B7yEy0PQ.js +27 -0
  174. package/web/dist/assets/erDiagram-KYQIGQAA-CL48j4aU.js +99 -0
  175. package/web/dist/assets/eventmodeling-K75KTNOO-CFvkaejh.js +1 -0
  176. package/web/dist/assets/flowDiagram-IUGLLWBU-6UyKXpqG.js +1 -0
  177. package/web/dist/assets/ganttDiagram-R7GPNTJY-vdMr-0ZV.js +292 -0
  178. package/web/dist/assets/gitGraph-VSP46ZUC-2z85Wuwo.js +1 -0
  179. package/web/dist/assets/gitGraphDiagram-PDYL7XHY-BY4GPiqC.js +106 -0
  180. package/web/dist/assets/graphlib-DS17s2tU.js +1 -0
  181. package/web/dist/assets/index-Bjn56a2i.js +76 -0
  182. package/web/dist/assets/index-Drp6e5iU.css +1 -0
  183. package/web/dist/assets/info-OHQRW6UA-ECzkYTx-.js +1 -0
  184. package/web/dist/assets/infoDiagram-MVDRKAX5-DDk5EAeb.js +2 -0
  185. package/web/dist/assets/init-C-OQMol4.js +1 -0
  186. package/web/dist/assets/ishikawaDiagram-HPEHV2GE-9yPH4J94.js +70 -0
  187. package/web/dist/assets/journeyDiagram-BOSHW7O7-BPTIap3l.js +139 -0
  188. package/web/dist/assets/kanban-definition-FNSA4HC5-BbADHrA8.js +89 -0
  189. package/web/dist/assets/katex-B_JcIwqj.js +257 -0
  190. package/web/dist/assets/line-CHV9_4lL.js +1 -0
  191. package/web/dist/assets/linear-C-eZsBTv.js +1 -0
  192. package/web/dist/assets/mermaid-parser.core-BiAhX1l1.js +7 -0
  193. package/web/dist/assets/mermaid.core-BxP_A_zj.js +44 -0
  194. package/web/dist/assets/mindmap-definition-6OY57UG3-BMWlit95.js +98 -0
  195. package/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
  196. package/web/dist/assets/packet-JDAUHWVQ-FVApuKOp.js +1 -0
  197. package/web/dist/assets/path-fybaL0A-.js +1 -0
  198. package/web/dist/assets/pegDiagram-ECIUJNBX-WjWJvDHM.js +1 -0
  199. package/web/dist/assets/pie-XZMESJXO-BGCZnvhp.js +1 -0
  200. package/web/dist/assets/pieDiagram-GNMPY74T-DJpAC_JC.js +39 -0
  201. package/web/dist/assets/quadrantDiagram-D57K4MC3-hxx5c7oq.js +7 -0
  202. package/web/dist/assets/radar-ABXABTNO-BU9Shc-O.js +1 -0
  203. package/web/dist/assets/railroad-I3PHUGI6-pWj8d-G4.js +1 -0
  204. package/web/dist/assets/railroad-abnf-LNEFI6M7-D7HZBTAj.js +1 -0
  205. package/web/dist/assets/railroad-ebnf-DWYD2UWJ-CKiXJSuM.js +1 -0
  206. package/web/dist/assets/railroad-peg-JR7BVNR7-DB3VJHgR.js +1 -0
  207. package/web/dist/assets/railroadDiagram-Y72NRBL7-Cev7Y65k.js +1 -0
  208. package/web/dist/assets/requirementDiagram-EQYSW5RA-CqbwMLD8.js +84 -0
  209. package/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
  210. package/web/dist/assets/sankeyDiagram-SY24JHO5-V2cyVi8q.js +40 -0
  211. package/web/dist/assets/sequenceDiagram-BLEHNHQU-By6NJOPM.js +169 -0
  212. package/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
  213. package/web/dist/assets/src-Da91fG3g.js +1 -0
  214. package/web/dist/assets/stateDiagram-v2-A6IESKWZ-BnPGX5UI.js +291 -0
  215. package/web/dist/assets/swimlanes-B3ENRKFQ-C58BpIpm.js +1 -0
  216. package/web/dist/assets/swimlanesDiagram-AYQRV4AK-RaivxSsc.js +8 -0
  217. package/web/dist/assets/timeline-definition-JXYKGLGB-Hgo1DhnS.js +120 -0
  218. package/web/dist/assets/treeView-D4JQ5SDB-DF0dfjqB.js +1 -0
  219. package/web/dist/assets/treemap-SAJKECNS-CSSM5FqB.js +1 -0
  220. package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkBBD1NY.js +328 -0
  221. package/web/dist/assets/vennDiagram-Q3Y3IDCW-Bm28keX_.js +34 -0
  222. package/web/dist/assets/wardley-7MLQ67FV-By7KgEQX.js +1 -0
  223. package/web/dist/assets/wardleyDiagram-IVAIDKWA-oVtIrnGa.js +78 -0
  224. package/web/dist/assets/xychartDiagram-4URNHDU4-aI8FH4g2.js +7 -0
  225. package/web/dist/favicon-32.png +0 -0
  226. package/web/dist/favicon.ico +0 -0
  227. package/web/dist/favicon.svg +20 -8
  228. package/web/dist/icons/apple-touch-icon-180.png +0 -0
  229. package/web/dist/icons/icon-192.png +0 -0
  230. package/web/dist/icons/icon-512.png +0 -0
  231. package/web/dist/icons/icon-maskable-512.png +0 -0
  232. package/web/dist/index.html +21 -27
  233. package/web/dist/manifest.webmanifest +17 -0
  234. package/web/dist/maskable.svg +25 -0
  235. package/web/dist/sw.js +40 -0
  236. package/bin/oh-my-agent-web-ps.mjs +0 -203
  237. package/bin/oh-my-agent-web-ws.mjs +0 -593
  238. package/bin/oh-my-agent-web.mjs +0 -321
  239. package/bin/update-check.mjs +0 -142
  240. package/dist/server/codex-client.js +0 -310
  241. package/dist/server/codex-supervisor.js +0 -446
  242. package/dist/server/fs-service.js +0 -144
  243. package/dist/server/request-inspector.js +0 -458
  244. package/dist/shared/protocol.js +0 -112
  245. package/scripts/manage-server.mjs +0 -155
  246. package/scripts/omaw.mjs +0 -74
  247. package/shared/protocol.ts +0 -228
  248. package/web/dist/assets/index-B8-VzDf2.css +0 -1
  249. package/web/dist/assets/index-DeC2eiiq.js +0 -42
  250. package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
@@ -1,543 +1,365 @@
1
1
  /**
2
- * oh-my-agent-web server — express + WebSocket hub in front of the codex app-server.
3
- *
4
- * browser <--ws /ws--> [this] <--ws--> codex app-server (ws://127.0.0.1:25258)
5
- *
6
- * 我们是「透明 JSON-RPC 代理 + 进程守护」:
7
- * - 浏览器可以按名字调用任意 codex 方法;所有 codex 通知原样广播。
8
- * - `omaw/*` 是本地方法(状态 / 日志 / 文件系统 / 路径),由本进程处理。
9
- * - codex 反向请求(审批 / user input / 动态工具)广播给所有标签页,谁先 reply 谁赢,
10
- * 随后广播 `serverRequest/resolved` 让其他标签页关掉弹窗。
11
- * - 同一 `dist/server/index.js` 既能被 CLI `startServer()` 调用,也能直接 `node` 运行。
2
+ * 入口:一个 HTTP 端口 + 实时通道(WS 或 SSE/RPC,ADR-0032)。
12
3
  */
13
- import { existsSync, readFileSync } from "node:fs";
4
+ import { homedir, networkInterfaces } from "node:os";
14
5
  import { createServer } from "node:http";
15
- import { homedir } from "node:os";
16
- import { dirname, join, resolve } from "node:path";
17
- import { fileURLToPath, pathToFileURL } from "node:url";
6
+ import { join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
18
8
  import express from "express";
19
- import { WebSocket, WebSocketServer } from "ws";
20
- import { CodexClient } from "./codex-client.js";
21
- import { CodexSupervisor } from "./codex-supervisor.js";
22
- import { RequestInspector } from "./request-inspector.js";
23
- import { FsError, listDirectory, paths as resolvePaths, readTextFile } from "./fs-service.js";
24
- import { LOCAL_METHODS, PROTOCOL_VERSION, } from "../shared/protocol.js";
25
- /* ------------------------------------------------------------------ */
26
- /* config */
27
- /* ------------------------------------------------------------------ */
28
- const DEFAULT_OMAW_PORT = 25257;
29
- const DEFAULT_OMAW_HOST = "127.0.0.1";
30
- const DEFAULT_CODEX_PORT = 25258;
31
- const DEFAULT_CODEX_BIN = "codex";
32
- const HEARTBEAT_MS = 30_000;
33
- const SERVER_REQUEST_HISTORY = 2_000;
34
- /**
35
- * How long a proxied `codex/*` call waits for the app-server before giving up
36
- * with `-32001`. Long enough to cover a cold `codex app-server` boot (and a
37
- * reconnect after a crash), short enough that a genuinely dead server still
38
- * reports an error quickly.
39
- */
40
- const CODEX_READY_WAIT_MS = 20_000;
41
- /**
42
- * `OMAW_DEV_WEB=1` —— dev 时前端不经 `web/dist`,而是把 Vite **挂在同一个进程里**
43
- * 当中间件(middleware mode)。
44
- *
45
- * 这样对外只有一个源、一个端口:`http://127.0.0.1:25257`。页面由 Vite 现编
46
- * (源码 + HMR,不用 build),`/ws` 与页面同源,也没有第二个端口要记、要开、
47
- * 或者要配置跨源白名单。
48
- */
49
- const DEV_SOURCE = process.env.OMAW_DEV_WEB === "1";
50
- /**
51
- * Vite 的 HMR 通道挂在同一个 http server 上,得给个专属路径,
52
- * 否则会和下面 `/ws` 的 upgrade 拦截器撞车(非 /ws 一律 destroy)。
53
- */
54
- const DEV_HMR_PATH = "/omaw-hmr";
55
- /* ------------------------------------------------------------------ */
56
- /* package metadata */
57
- /* ------------------------------------------------------------------ */
58
- /** 从当前文件向上找 package.json —— 兼容 tsx(src) 与 dist 两种目录深度。 */
59
- function findPackageRoot(startDir) {
60
- let dir = startDir;
61
- for (let i = 0; i < 8; i += 1) {
62
- if (existsSync(join(dir, "package.json")))
63
- return dir;
64
- const parent = dirname(dir);
65
- if (parent === dir)
66
- break;
67
- dir = parent;
68
- }
69
- return startDir;
70
- }
71
- const __dirname = dirname(fileURLToPath(import.meta.url));
72
- const PACKAGE_ROOT = findPackageRoot(__dirname);
73
- function readOwnVersion() {
74
- try {
75
- const pkg = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8"));
76
- return pkg.version ?? "0.0.0";
77
- }
78
- catch {
79
- return "0.0.0";
80
- }
81
- }
82
- function codexVersionFromHandshake(userAgent) {
83
- if (!userAgent)
84
- return null;
85
- // 形如 `codex-cli 0.156.1` 或 `codex/0.156.1 ...`。
86
- const match = /(\d+\.\d+\.\d+(?:[-+][\w.]+)?)/.exec(userAgent);
87
- return match ? match[1] : userAgent;
88
- }
89
- /* ------------------------------------------------------------------ */
90
- /* origin admission */
91
- /* ------------------------------------------------------------------ */
92
- function parseAllowOrigins() {
93
- return (process.env.OMAW_ALLOW_ORIGINS ?? "")
94
- .split(",")
95
- .map((s) => s.trim().toLowerCase())
96
- .filter(Boolean);
97
- }
98
- /**
99
- * 浏览器 upgrade 的 Origin 校验:
100
- * - 无 Origin(curl / 脚本 / ws 测试)放行;
101
- * - 设置了 OMAW_ALLOW_ORIGINS 就按白名单;
102
- * - 否则放行同源,以及 localhost / 127.0.0.1 / ::1 的任意端口(覆盖 Vite dev server)。
103
- */
104
- function originAllowed(req) {
105
- const origin = req.headers.origin;
106
- if (typeof origin !== "string" || !origin)
107
- return true;
108
- const lower = origin.toLowerCase();
109
- const allow = parseAllowOrigins();
110
- if (allow.length > 0)
111
- return allow.includes(lower);
112
- if (lower === "null")
113
- return false;
114
- let url;
115
- try {
116
- url = new URL(origin);
117
- }
118
- catch {
119
- return false;
120
- }
121
- const hostHeader = req.headers.host;
122
- if (typeof hostHeader === "string" && hostHeader) {
123
- try {
124
- const hostUrl = new URL(`http://${hostHeader}`);
125
- if (url.hostname === hostUrl.hostname && url.port === hostUrl.port)
126
- return true;
127
- }
128
- catch {
129
- /* ignore */
130
- }
9
+ import { WebSocketServer } from "ws";
10
+ import { createEngineWiring } from "./engine/wiring.js";
11
+ import { createFileRuleStore } from "./approval/rules.js";
12
+ import { existsSync } from "node:fs";
13
+ import { PROTOCOL_VERSION } from "./protocol.js";
14
+ import { acquireLock, describeHolder, releaseLock } from "./session/lock.js";
15
+ import { createRuntime } from "./session/runtime.js";
16
+ import { createFileSettingsStore } from "./session/settings.js";
17
+ import { createSender } from "./snapshot/sender.js";
18
+ import { createPiSessionsProvider, piAgentDir } from "./session/pi-sessions-provider.js";
19
+ import { createUploadStore, uploadsDir } from "./uploads/upload-store.js";
20
+ import { builtinProviderGroups, createProviderWiring } from "./providers/provider-handlers.js";
21
+ import { probeProvider } from "./providers/probe.js";
22
+ import { createProviderStore } from "./providers/provider-store.js";
23
+ import { createEnabledStore } from "./providers/enabled-models.js";
24
+ import { migrateStoreToCatalog } from "./providers/catalog-writes.js";
25
+ import { createPushStore, notifyTurnFinished } from "./push/push.js";
26
+ import { registerAuxRoutes } from "./http/aux-routes.js";
27
+ import { handleMessage } from "./ws/handle-message.js";
28
+ import { registerHealthAndStatic } from "./http/health-and-static.js";
29
+ import { registerUploadRoutes } from "./uploads/upload-routes.js";
30
+ import { readConfig, validateConfig } from "./util/env.js";
31
+ import { createLogger } from "./util/log.js";
32
+ import { createRotatingFileSink, logFileName } from "./util/log-file.js";
33
+ import { refreshPriceIndex } from "./util/pricing-index.js";
34
+ import { describePolicy, isAllowedHost, isAllowedOrigin } from "./util/origin.js";
35
+ import { pickExisting, webDistCandidates } from "./util/paths.js";
36
+ import { authMiddleware, decide, effectiveAddress } from "./http/auth.js";
37
+ import { registerAuthRoutes } from "./http/auth-routes.js";
38
+ import { BRAND_NAME } from "./brand.js";
39
+ import { registerLiveRoutes } from "./http/live-routes.js";
40
+ import { compressText, immutableAssets } from "./http/compress.js";
41
+ import { fanOutSnapshot } from "./snapshot/fan-out.js";
42
+ import { checkDataDirWritable, describeListenError } from "./util/startup.js";
43
+ import { migrateLegacyDataDir } from "./util/data-dir.js";
44
+ const HERE = fileURLToPath(new URL(".", import.meta.url));
45
+ /** 开发态对外端口(Vite);dev 脚本用它告诉后端可信来源 */
46
+ const DEV_WEB_PORT = Number(process.env.AGENT_WEB_DEV_PORT ?? 25257);
47
+ /** 快照节流窗口(ADR-0004 §3) */
48
+ const SNAPSHOT_WINDOW_MS = 60;
49
+ const TICK_MS = 20;
50
+ /** socket 积压阈值:超过就只发事件、丢快照(ADR-0004 §4) */
51
+ const BACKPRESSURE_BYTES = 256 * 1024;
52
+ const { config, problems: envProblems } = readConfig(process.env, { home: homedir(), cwd: process.cwd() });
53
+ let fileSink = null;
54
+ const logger = createLogger({
55
+ level: config.logLevel,
56
+ scope: "app",
57
+ sink: (line) => {
58
+ process.stdout.write(`${line}\n`);
59
+ fileSink?.write(line);
60
+ },
61
+ });
62
+ // 单用户工作台:一处 fire-and-forget 的 Promise 拒绝(推送写盘失败、模型热重载失败……)
63
+ // 不该把整个服务带走(Node ≥15 默认会直接退出)。记日志、继续跑。
64
+ process.on("unhandledRejection", (reason) => {
65
+ logger.error("未处理的 Promise 拒绝(已记录,进程继续)", {
66
+ message: reason instanceof Error ? (reason.stack ?? reason.message) : String(reason),
67
+ });
68
+ });
69
+ /** 局域网地址自动放行;域名仍要显式配 */
70
+ const localAddresses = Object.values(networkInterfaces())
71
+ .flat()
72
+ .filter((item) => Boolean(item) && item?.internal === false)
73
+ .map((item) => item.address);
74
+ const policy = {
75
+ port: config.port,
76
+ devPorts: [DEV_WEB_PORT],
77
+ extraHosts: [...new Set([...localAddresses, ...config.allowedHosts])],
78
+ };
79
+ const sockets = new Set();
80
+ let shuttingDown = false;
81
+ const sender = createSender({
82
+ windowMs: SNAPSHOT_WINDOW_MS,
83
+ send: (frame) => {
84
+ if (frame.kind === "snapshot")
85
+ fanOut(frame.snapshot);
86
+ },
87
+ });
88
+ const piSessions = createPiSessionsProvider({ agentDir: piAgentDir(), dataDir: config.dataDir, logger });
89
+ /** 引擎接线:注册表 + codex 客户端 + 每个引擎的会话来源(engine/wiring.ts) */
90
+ const engineWiring = createEngineWiring({
91
+ engineName: config.engine,
92
+ codexBin: config.codexBin,
93
+ codexHome: config.codexHome,
94
+ dshBin: config.dshBin,
95
+ dshHome: config.dshHome,
96
+ dshModel: config.dshModel,
97
+ dshPatches: config.dshPatches,
98
+ dshPatchDir: join(config.dataDir, "dsh-patches"),
99
+ piSessions,
100
+ onNotice: (text, level) => runtimeRef.current?.notify(text, level),
101
+ onLog: (text, meta) => logger.warn(text, meta),
102
+ });
103
+ const engineCatalog = engineWiring.catalog;
104
+ const resolved = engineWiring.resolved;
105
+ const engine = resolved.engine;
106
+ /** 引擎在 runtime 建好之前就会创建,用一个 ref 把「通知」转发给界面 */
107
+ /** provider 配置存储(ADR-0025):复用 pi 自己的 models-store.json / auth.json */
108
+ const providerStore = createProviderStore({ agentDir: piAgentDir(), now: Date.now });
109
+ /** 启动时的配置补齐(main 里会 await 它:避免和价格索引抢着读同一个文件) */
110
+ const catalogMigration = migrateStoreToCatalog(piAgentDir()).then((migrated) => {
111
+ if (migrated.length > 0)
112
+ logger.info("已把老 provider 补进 models.json", { providers: migrated });
113
+ });
114
+ const enabledStore = createEnabledStore({ agentDir: piAgentDir() });
115
+ const pushStore = createPushStore({ dataDir: config.dataDir, now: Date.now });
116
+ const runProviderRequest = createProviderWiring({
117
+ store: providerStore,
118
+ logger,
119
+ probe: probeProvider,
120
+ enabled: enabledStore,
121
+ getRuntime: () => runtime,
122
+ });
123
+ const runtimeRef = {
124
+ current: null,
125
+ };
126
+ /** 懒读:provider 列表里要带上内置 provider 分组,而那需要 runtime 里的模型表 */
127
+ const providerList = async () => {
128
+ const list = await providerStore.list(builtinProviderGroups(runtime?.state().models ?? []));
129
+ const live = runtime?.state();
130
+ if (live)
131
+ live.enabledModels = await enabledStore.read();
132
+ return list;
133
+ };
134
+ const runtime = createRuntime({
135
+ engine,
136
+ // 自定义 provider 来自 models-store.json;不主动读一次,界面上就只剩内置的那几个
137
+ loadProviders: providerList,
138
+ onTurnFinished: (info) => notifyTurnFinished(pushStore, info),
139
+ rules: createFileRuleStore(config.dataDir),
140
+ // 启动信息只进日志,不进对话流
141
+ onStartupInfo: (text) => logger.info(text),
142
+ workspace: config.workspace,
143
+ engineName: resolved.descriptor.name,
144
+ engineCapabilities: resolved.descriptor.capabilities,
145
+ turnTimeoutMs: config.turnTimeoutMs,
146
+ settings: createFileSettingsStore(config.dataDir),
147
+ uploadStore: {
148
+ read: (id) => uploadStore.read(id),
149
+ pathOf: (id) => join(uploadsDir(config.dataDir), id),
150
+ },
151
+ piSessions: engineCatalog.sessionsFor(resolved.descriptor.name) ?? undefined,
152
+ engines: engineCatalog,
153
+ emit: {
154
+ snapshot: (snapshot) => sender.pushSnapshot(snapshot),
155
+ event: (event) => {
156
+ broadcast(JSON.stringify(event));
157
+ },
158
+ },
159
+ });
160
+ runtimeRef.current = runtime;
161
+ /** SSE 连接(纯 HTTP 通道):clientId → 写入函数(每客户端一份快照,ADR-0033) */
162
+ const sinks = new Map();
163
+ const fanOut = (snapshot) => fanOutSnapshot(snapshot, { sockets, sinks, snapshotFor: (id) => runtime.snapshotFor(id) });
164
+ function broadcast(payload) {
165
+ for (const socket of sockets) {
166
+ if (socket.readyState === socket.OPEN)
167
+ socket.send(payload);
131
168
  }
132
- const hn = url.hostname.toLowerCase();
133
- return hn === "localhost" || hn === "127.0.0.1" || hn === "::1" || hn === "[::1]";
169
+ for (const sink of sinks.values())
170
+ sink(payload);
134
171
  }
135
- /* ------------------------------------------------------------------ */
136
- /* helpers */
137
- /* ------------------------------------------------------------------ */
138
- function isRecord(value) {
139
- return typeof value === "object" && value !== null;
172
+ async function dispatchRequest(socket, raw, clientId) {
173
+ await handleMessage(socket, raw, {
174
+ runtime,
175
+ ...(clientId ? { clientId } : {}),
176
+ reply,
177
+ sender,
178
+ logger,
179
+ runProviderRequest: (request, respond) => runProviderRequest(request, (result) => respond(result)),
180
+ });
140
181
  }
141
- function asJsonRpcError(err) {
142
- if (err instanceof FsError)
143
- return { code: err.code, message: err.message };
144
- if (isRecord(err)) {
145
- return {
146
- code: typeof err.code === "number" ? err.code : -32000,
147
- message: typeof err.message === "string" ? err.message : "internal error",
148
- data: err.data,
149
- };
150
- }
151
- return { code: -32000, message: err instanceof Error ? err.message : String(err) };
182
+ function reply(socket, id, result) {
183
+ const message = result.ok
184
+ ? { type: "reply", id, ok: true, ...(result.result === undefined ? {} : { result: result.result }) }
185
+ : { type: "reply", id, ok: false, error: result.error };
186
+ socket.send(JSON.stringify(message));
152
187
  }
153
- /* ------------------------------------------------------------------ */
154
- /* startServer */
155
- /* ------------------------------------------------------------------ */
156
- export async function startServer(options = {}) {
157
- const version = readOwnVersion();
158
- const cwd = resolve(options.cwd ?? process.env.OMAW_CWD ?? process.cwd());
159
- const port = options.port ?? Number(process.env.OMAW_PORT ?? DEFAULT_OMAW_PORT);
160
- const host = options.host ?? process.env.OMAW_HOST ?? DEFAULT_OMAW_HOST;
161
- const codexPort = options.codexPort ?? Number(process.env.OMAW_CODEX_PORT ?? DEFAULT_CODEX_PORT);
162
- const codexBin = options.codexBin ?? process.env.OMAW_CODEX_BIN ?? DEFAULT_CODEX_BIN;
163
- const log = options.quiet
164
- ? () => undefined
165
- : (...args) => {
166
- console.log("[oh-my-agent-web]", ...args);
167
- };
168
- /* -------- supervisor + client (capture/inspector off by default) -------- */
169
- // 抓包(loopback Responses 代理)默认关闭:只有显式设置 OMAW_REQUEST_INSPECTOR=1
170
- // 才会启动 inspector,也才会给 app-server 注入 chatgpt_base_url /
171
- // model_providers.capture.base_url 这两个 -c 覆盖。默认一个都不加,codex 走自己的配置。
172
- const inspector = new RequestInspector({ dataDir: join(PACKAGE_ROOT, "data") });
173
- let inspectorUrl = "";
174
- if (process.env.OMAW_REQUEST_INSPECTOR === "1") {
175
- try {
176
- inspectorUrl = await inspector.start();
177
- log(`request inspector: ${inspectorUrl} -> ${process.env.OMAW_CODEX_UPSTREAM ?? "https://chatgpt.com/backend-api/codex"}`);
178
- }
179
- catch (error) {
180
- log(`request inspector disabled: ${error instanceof Error ? error.message : String(error)}`);
181
- }
182
- }
183
- const supervisor = new CodexSupervisor({ port: codexPort, bin: codexBin, cwd, codexBaseUrl: inspectorUrl || undefined });
184
- const client = new CodexClient(supervisor.url, version);
185
- supervisor.onLog((line) => {
186
- if (!options.quiet)
187
- console.log("[codex]", line);
188
+ // ------------------------------ HTTP ------------------------------
189
+ const app = express();
190
+ // 登录端点必须挂在鉴权**之前**:此刻还没有 cookie,不能要求先过鉴权(ADR-0031 修订)
191
+ registerAuthRoutes(app, { password: config.password, brandName: BRAND_NAME });
192
+ // 鉴权其次:本机放行,其它来源必须过密码;页面请求给登录页,接口请求给 401
193
+ app.use(authMiddleware({ password: config.password, brandName: BRAND_NAME }));
194
+ app.use(express.json()); // 终端/技能/导入要读 body
195
+ const webDist = pickExisting(webDistCandidates(HERE), existsSync);
196
+ // 压缩 + 缓存头必须在 **API 路由注册之前** 挂:Express 按注册顺序匹配中间件,
197
+ // 挂在后面就只影响之后注册的路由 —— `/api/rpc`(快照帧)这类大包会一直裸奔。
198
+ // SSE / 终端流 / Range 媒体在 compress.ts 里显式跳过(不能攒、不能压)。
199
+ app.use(compressText());
200
+ app.use(immutableAssets());
201
+ const uploadStore = createUploadStore({ dataDir: config.dataDir });
202
+ registerUploadRoutes(app, { uploadStore, logger }); // 附件(ADR-0020 §P3)
203
+ const workspaceOf = () => runtime.state().files?.root ?? config.workspace;
204
+ registerAuxRoutes(app, {
205
+ providerStore,
206
+ workspaceOf,
207
+ projects: () => runtime.state().piProjects,
208
+ deleteSession: (id) => runtime.deleteSession(id),
209
+ pushStore,
210
+ onChange: () => void runProviderRequest({ type: "list_providers" }, () => { }),
211
+ });
212
+ registerLiveRoutes(app, {
213
+ sinks,
214
+ snapshotFor: (clientId) => runtime.snapshotFor(clientId),
215
+ clientTracker: { setSlim: runtime.clientView.setSlim, drop: runtime.clientView.drop },
216
+ logger,
217
+ handleMessage: async (clientId, shim, raw) => {
218
+ await dispatchRequest(shim, raw, clientId);
219
+ },
220
+ });
221
+ registerHealthAndStatic(app, {
222
+ workspace: config.workspace,
223
+ engine: config.engine,
224
+ webDist,
225
+ devWebPort: DEV_WEB_PORT,
226
+ counts: () => ({
227
+ sessions: runtime.state().piProjects.reduce((total, project) => total + project.sessions.length, 0),
228
+ conversations: runtime.state().conversations.length,
229
+ }),
230
+ });
231
+ const httpServer = createServer(app);
232
+ // ---------------------------------------------------------------------------
233
+ // WebSocket(握手校验是 ADR-0006 的核心条目)
234
+ // ---------------------------------------------------------------------------
235
+ const wss = new WebSocketServer({ noServer: true });
236
+ httpServer.on("upgrade", (request, socket, head) => {
237
+ // WS 不走 Express 中间件,得在这里单独判一次
238
+ const forwarded = request.headers["x-forwarded-for"];
239
+ const auth = decide({
240
+ password: config.password,
241
+ remoteAddress: effectiveAddress(request.socket.remoteAddress, typeof forwarded === "string" ? forwarded : undefined),
242
+ authorization: request.headers.authorization,
243
+ cookie: request.headers.cookie,
188
244
  });
189
- await supervisor.start();
190
- void client.connect();
191
- /* -------- HTTP app -------- */
192
- const app = express();
193
- app.disable("x-powered-by");
194
- // 先建 http server:Vite 的 HMR 要挂到它上面(见下面的 middleware mode)。
195
- // 路由按注册顺序匹配,所以这里提前 createServer 不影响 /api 的优先级。
196
- const httpServer = createServer(app);
197
- app.get("/api/health", (_req, res) => {
198
- const body = { ok: true, codex: supervisor.status(), version };
199
- res.json(body);
200
- });
201
- app.get("/api/version", (_req, res) => {
202
- const hs = client.handshake();
203
- res.json({
204
- name: "oh-my-agent-web",
205
- version,
206
- protocolVersion: PROTOCOL_VERSION,
207
- codexVersion: codexVersionFromHandshake(hs?.userAgent),
208
- codexUserAgent: hs?.userAgent ?? null,
209
- codexUrl: supervisor.url,
210
- });
211
- });
212
- // 前端:dev 把 Vite 当中间件挂进本进程(源码 + HMR);生产发 web/dist。
213
- // 两种情况下对外都只有 http://127.0.0.1:<port> 一个源,页面与 /ws 同源。
214
- if (DEV_SOURCE) {
215
- // devDependency,且只在源码模式下加载。
216
- const { createServer: createViteServer } = await import("vite");
217
- const vite = await createViteServer({
218
- configFile: join(PACKAGE_ROOT, "web", "vite.config.ts"),
219
- // spa:index.html 的转换 + history fallback 都交给 Vite,
220
- // 替代生产分支里那段 express 的 SPA fallback。
221
- appType: "spa",
222
- server: { middlewareMode: true, hmr: { server: httpServer, path: DEV_HMR_PATH } },
223
- });
224
- app.use(vite.middlewares);
225
- httpServer.once("close", () => void vite.close());
226
- log(`serving web UI from source via Vite (middleware mode, hmr=${DEV_HMR_PATH}, no build needed)`);
245
+ if (auth !== "allow") {
246
+ // 不发 WWW-Authenticate:那会让浏览器弹原生登录框,与自建登录页重复
247
+ socket.write(auth === "forbid" ? "HTTP/1.1 403 Forbidden\r\n\r\n" : "HTTP/1.1 401 Unauthorized\r\n\r\n");
248
+ socket.destroy();
249
+ return;
227
250
  }
228
- else {
229
- const webDist = join(PACKAGE_ROOT, "web", "dist");
230
- if (existsSync(webDist)) {
231
- app.use(express.static(webDist));
232
- app.get(/^\/(?!api\/|ws).*/, (_req, res) => {
233
- res.sendFile(join(webDist, "index.html"));
234
- });
235
- log(`serving web UI from ${webDist}`);
236
- }
237
- else {
238
- log(`web/dist not found — dev 直接跑 \`npm run dev\`(源码模式,无需 build),生产先 \`npm run build\``);
239
- app.get(/^\/(?!api\/|ws).*/, (_req, res) => {
240
- res.status(404).type("text/plain").send("web/dist 不存在。dev 请跑 npm run dev(源码模式,无需 build);生产请先 npm run build。");
241
- });
242
- }
251
+ const origin = request.headers.origin;
252
+ const host = request.headers.host;
253
+ if (!isAllowedOrigin(origin, policy) || !isAllowedHost(host, policy)) {
254
+ logger.warn("拒绝 WS 握手:来源不在白名单", { origin, host, policy: describePolicy(policy) });
255
+ socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\n\r\n");
256
+ socket.destroy();
257
+ return;
243
258
  }
244
- /* -------- WebSocket hub -------- */
245
- const wss = new WebSocketServer({ noServer: true });
246
- const browsers = new Set();
247
- const send = (ws, msg) => {
248
- if (ws.readyState !== WebSocket.OPEN)
249
- return;
250
- try {
251
- ws.send(JSON.stringify(msg));
252
- }
253
- catch {
254
- /* ignore */
255
- }
256
- };
257
- const broadcast = (msg) => {
258
- for (const ws of browsers)
259
- send(ws, msg);
260
- };
261
- const broadcastStatus = () => {
262
- broadcast({ type: "status", codex: supervisor.status() });
263
- };
264
- let prevPhase = supervisor.status().phase;
265
- supervisor.onStatus((status) => {
266
- broadcastStatus();
267
- // supervisor 重新就绪后主动拉一次 codex 连接,避免等满 client 自己的退避。
268
- if (status.phase === "ready" && prevPhase !== "ready" && !client.isReady())
269
- client.reconnect();
270
- prevPhase = status.phase;
271
- });
272
- client.on("state", (state) => {
273
- supervisor.setConnected(state.connected, state.error);
259
+ wss.handleUpgrade(request, socket, head, (websocket) => wss.emit("connection", websocket));
260
+ });
261
+ wss.on("connection", (socket) => {
262
+ sockets.add(socket);
263
+ logger.info("客户端已连接", { clients: sockets.size });
264
+ // 首屏:立刻给一份快照,不等节流窗口
265
+ sender.pushSnapshot(runtime.snapshot());
266
+ sender.flush(Date.now());
267
+ socket.on("message", (raw) => {
268
+ void dispatchRequest(socket, raw.toString());
274
269
  });
275
- client.on("notification", (evt) => {
276
- broadcast({ type: "event", method: evt.method, params: evt.params });
270
+ socket.on("close", () => {
271
+ sockets.delete(socket);
272
+ logger.info("客户端已断开", { clients: sockets.size });
277
273
  });
278
- // codex 反向请求:广播给所有浏览器,记录待答复集合用于去重。
279
- const pendingServerRequests = new Map();
280
- const answeredServerRequests = new Set();
281
- client.on("serverRequest", (evt) => {
282
- const key = String(evt.id);
283
- const threadId = isRecord(evt.params) && typeof evt.params.threadId === "string" ? evt.params.threadId : null;
284
- pendingServerRequests.set(key, { method: evt.method, threadId });
285
- broadcast({ type: "serverRequest", id: evt.id, method: evt.method, params: evt.params });
274
+ socket.on("error", (error) => {
275
+ logger.warn("socket 错误", { message: error.message });
276
+ sockets.delete(socket);
286
277
  });
287
- client.on("state", (state) => {
288
- // 断线后 codex 可能复用 request id,清掉去重历史避免误吞新请求。
289
- if (!state.connected) {
290
- pendingServerRequests.clear();
291
- answeredServerRequests.clear();
292
- }
293
- });
294
- function rememberAnswered(key) {
295
- answeredServerRequests.add(key);
296
- if (answeredServerRequests.size > SERVER_REQUEST_HISTORY) {
297
- const first = answeredServerRequests.values().next().value;
298
- if (first !== undefined)
299
- answeredServerRequests.delete(first);
300
- }
278
+ });
279
+ // 客户端请求分发在 ws/handle-message.ts(index 只负责装配)
280
+ // ---------------------------------------------------------------------------
281
+ // 启动
282
+ // ---------------------------------------------------------------------------
283
+ async function main() {
284
+ // 旧数据目录(~/.agent-web)自动搬到新目录:dev 直接跑本文件时也要搬
285
+ migrateLegacyDataDir({ dataDir: config.dataDir, log: (line) => logger.info(line) });
286
+ const problems = [...envProblems, ...validateConfig(config, { home: homedir() })];
287
+ if (problems.length > 0) {
288
+ for (const problem of problems)
289
+ logger.error(`启动自检失败:${problem}`);
290
+ process.exit(1);
301
291
  }
302
- /* -------- local `omaw/*` methods -------- */
303
- async function handleLocal(method, params) {
304
- const p = isRecord(params) ? params : {};
305
- switch (method) {
306
- case LOCAL_METHODS.codexStatus:
307
- return { status: supervisor.status() };
308
- case LOCAL_METHODS.codexRestart:
309
- await supervisor.restart();
310
- client.reconnect();
311
- return { ok: true };
312
- case LOCAL_METHODS.codexLog:
313
- return { lines: supervisor.logLines() };
314
- case LOCAL_METHODS.requestLogsList:
315
- return inspector.list(typeof p.threadId === "string" ? p.threadId : undefined, typeof p.limit === "number" ? p.limit : undefined);
316
- case LOCAL_METHODS.requestLogsDetail: {
317
- if (typeof p.id !== "string" || !p.id)
318
- throw new FsError(-32602, "omaw/request-logs/detail requires { id: string }");
319
- const entry = await inspector.get(p.id);
320
- if (!entry)
321
- throw new FsError(-32004, "request log not found");
322
- return entry;
323
- }
324
- case LOCAL_METHODS.fsList:
325
- return listDirectory({
326
- path: typeof p.path === "string" ? p.path : undefined,
327
- cwd,
328
- maxEntries: typeof p.maxEntries === "number" ? p.maxEntries : undefined,
329
- });
330
- case LOCAL_METHODS.fsRead:
331
- if (typeof p.path !== "string")
332
- throw new FsError(-32602, "omaw/fs/read requires { path: string }");
333
- return readTextFile({
334
- path: p.path,
335
- cwd,
336
- maxBytes: typeof p.maxBytes === "number" ? p.maxBytes : undefined,
337
- });
338
- case LOCAL_METHODS.paths: {
339
- const codexHome = client.handshake()?.codexHome || process.env.CODEX_HOME || join(homedir(), ".codex");
340
- return resolvePaths(cwd, codexHome);
341
- }
342
- default:
343
- throw { code: -32601, message: `unknown local method: ${method}` };
344
- }
292
+ const writable = await checkDataDirWritable(config.dataDir);
293
+ if (!writable.ok) {
294
+ logger.error(`启动自检失败:${writable.reason}`);
295
+ process.exit(1);
345
296
  }
346
- /* -------- browser message handling -------- */
347
- async function handleRpc(ws, requestId, method, params) {
348
- try {
349
- let result;
350
- if (method.startsWith("omaw/")) {
351
- result = await handleLocal(method, params);
352
- }
353
- else {
354
- // A browser opened during startup races the app-server's boot (and a
355
- // reconnect after a codex crash). Wait briefly instead of failing the
356
- // client's first requests; only report -32001 if it never comes up.
357
- if (!client.isReady() && !(await client.waitUntilReady(CODEX_READY_WAIT_MS))) {
358
- throw { code: -32001, message: "codex app-server not connected" };
359
- }
360
- result = await client.request(method, params);
361
- }
362
- send(ws, { type: "rpcResult", requestId, ok: true, result });
363
- }
364
- catch (err) {
365
- send(ws, { type: "rpcResult", requestId, ok: false, error: asJsonRpcError(err) });
366
- }
297
+ // 日志自己也住在数据目录里 → 确认可写之后再挂文件 sink
298
+ fileSink = createRotatingFileSink({ file: logFileName(config.dataDir), onError: (message) => logger.warn(message) });
299
+ // 单实例锁(ADR-0007):必须在读/写任何会话数据之前拿到
300
+ const acquired = await acquireLock(config.dataDir, { pid: process.pid, port: config.port, startedAt: Date.now() });
301
+ if (!acquired.ok) {
302
+ logger.error("已经有一个 agent-web 在跑,本进程退出", { holder: describeHolder(acquired.holder) });
303
+ console.error(`\n已经有一个 agent-web 在这个数据目录上跑着:\n ${describeHolder(acquired.holder)}\n`);
304
+ console.error(` → 打开它:http://127.0.0.1:${acquired.holder.port}`);
305
+ console.error(" → 或者先停掉它(前台运行时 Ctrl-C 即可)\n");
306
+ process.exit(1);
367
307
  }
368
- function handleReply(id, result, error) {
369
- const key = String(id);
370
- if (answeredServerRequests.has(key))
371
- return; // 其他标签页已答复,忽略后来者
372
- rememberAnswered(key);
373
- const pending = pendingServerRequests.get(key);
374
- pendingServerRequests.delete(key);
375
- if (error)
376
- client.respondError(id, error);
377
- else
378
- client.respond(id, result);
379
- // 让其他标签页关闭审批弹窗;复用 codex 自己的通知名。
380
- broadcast({
381
- type: "event",
382
- method: "serverRequest/resolved",
383
- params: { threadId: pending?.threadId ?? null, requestId: id },
384
- });
308
+ if (acquired.stale && acquired.previous) {
309
+ logger.warn("接管陈旧锁(上一个进程已不在)", { previous: describeHolder(acquired.previous) });
385
310
  }
386
- async function onBrowserMessage(ws, raw) {
387
- let parsed;
388
- try {
389
- parsed = JSON.parse(typeof raw === "string" ? raw : raw.toString("utf8"));
390
- }
391
- catch {
392
- log("忽略非 JSON 的浏览器报文");
393
- return;
394
- }
395
- if (!isRecord(parsed) || typeof parsed.type !== "string")
396
- return;
397
- const msg = parsed;
398
- switch (msg.type) {
399
- case "rpc": {
400
- if (typeof msg.requestId !== "string" || typeof msg.method !== "string")
401
- return;
402
- await handleRpc(ws, msg.requestId, msg.method, msg.params);
403
- return;
404
- }
405
- case "reply": {
406
- if (msg.id === undefined || msg.id === null)
407
- return;
408
- const hasError = "error" in msg && msg.error !== undefined;
409
- const error = hasError ? asJsonRpcError(msg.error) : undefined;
410
- handleReply(msg.id, msg.result, error);
411
- return;
412
- }
413
- case "ping":
414
- send(ws, { type: "pong" });
415
- return;
416
- default:
417
- return;
418
- }
419
- }
420
- wss.on("connection", (ws) => {
421
- ws.__alive = true;
422
- browsers.add(ws);
423
- ws.on("pong", () => {
424
- ws.__alive = true;
425
- });
426
- ws.on("message", (raw) => {
427
- void onBrowserMessage(ws, raw).catch((err) => log("browser message handler error:", err));
428
- });
429
- ws.on("error", (err) => log("browser socket error:", err.message));
430
- ws.on("close", () => browsers.delete(ws));
431
- const info = {
432
- version,
433
- protocolVersion: PROTOCOL_VERSION,
434
- cwd,
435
- codexUrl: supervisor.url,
436
- codexVersion: codexVersionFromHandshake(client.handshake()?.userAgent),
437
- platform: process.platform,
438
- };
439
- send(ws, { type: "welcome", info });
440
- send(ws, { type: "status", codex: supervisor.status() });
311
+ const { conversations } = await runtime.init().then(() => ({ conversations: runtime.state().conversations }));
312
+ // 分时价格索引(ADR-0026 D 期):要在读历史 / 算账之前就绪;读不到就是空表 = 全部走引擎给的 cost
313
+ await catalogMigration.catch(() => undefined);
314
+ await refreshPriceIndex(piAgentDir());
315
+ void runProviderRequest({ type: "list_providers" }, () => { }); // 启动先填一次 provider 列表
316
+ logger.info("会话索引已装载", { conversations: conversations.length });
317
+ httpServer.on("error", (error) => {
318
+ logger.error(describeListenError(error, config.port, `可能是另一个 agent-web 或别的程序占着这个端口`));
319
+ void releaseLock(config.dataDir, process.pid).finally(() => process.exit(1));
441
320
  });
442
- // 升级前做 origin 校验 + 路径校验;非 /ws 直接 destroy(dev 下 Vite 的 HMR 通道除外,
443
- // 它由 Vite 自己在这个 server 上注册的 upgrade 监听器处理)。
444
- httpServer.on("upgrade", (req, socket, head) => {
445
- let pathname = "/";
446
- try {
447
- pathname = new URL(req.url ?? "/", "http://localhost").pathname;
448
- }
449
- catch {
450
- /* fall through */
451
- }
452
- if (DEV_SOURCE && pathname === DEV_HMR_PATH)
453
- return;
454
- if (pathname !== "/ws") {
455
- socket.destroy();
456
- return;
457
- }
458
- if (!originAllowed(req)) {
459
- socket.write("HTTP/1.1 403 Forbidden\r\nConnection: close\r\nContent-Length: 0\r\n\r\n");
460
- socket.destroy();
461
- return;
462
- }
463
- wss.handleUpgrade(req, socket, head, (ws) => wss.emit("connection", ws, req));
321
+ await new Promise((resolve) => httpServer.listen(config.port, config.host, resolve));
322
+ logger.info("agent-web 已启动", {
323
+ url: `http://${config.host}:${config.port}`,
324
+ workspace: config.workspace,
325
+ dataDir: config.dataDir,
326
+ engine: config.engine,
327
+ protocolVersion: PROTOCOL_VERSION,
328
+ conversations: runtime.state().conversations.length,
464
329
  });
465
- // 心跳:30s 一次 ping,未回 pong 的直接 terminate。
466
- const heartbeat = setInterval(() => {
467
- for (const ws of browsers) {
468
- if (ws.__alive === false) {
469
- ws.terminate();
470
- browsers.delete(ws);
471
- continue;
472
- }
473
- ws.__alive = false;
474
- try {
475
- ws.ping();
476
- }
477
- catch {
478
- /* ignore */
479
- }
480
- }
481
- }, HEARTBEAT_MS);
482
- heartbeat.unref?.();
483
- /* -------- listen -------- */
484
- await new Promise((resolvePromise, rejectPromise) => {
485
- httpServer.once("error", rejectPromise);
486
- httpServer.listen(port, host, () => resolvePromise());
487
- });
488
- const address = httpServer.address();
489
- const actualPort = typeof address === "object" && address ? address.port : port;
490
- const url = `http://${host}:${actualPort}`;
491
- log(`listening on ${url} (ws://${host}:${actualPort}/ws)`);
492
- log(`codex app-server: ${supervisor.url} (${supervisor.isExternal() ? "external" : "managed"})`);
493
- /* -------- close -------- */
494
- let closed = false;
495
- async function close() {
496
- if (closed)
497
- return;
498
- closed = true;
499
- clearInterval(heartbeat);
500
- for (const ws of browsers) {
501
- try {
502
- ws.close(1001, "server shutting down");
503
- }
504
- catch {
505
- /* ignore */
506
- }
507
- ws.terminate();
508
- }
509
- browsers.clear();
510
- client.close();
511
- await supervisor.stop();
512
- await inspector.close();
513
- await new Promise((resolvePromise) => {
514
- wss.close(() => resolvePromise());
515
- // 没有活跃连接时 wss.close 回调可能不触发;兜底。
516
- setTimeout(resolvePromise, 500);
517
- });
518
- await new Promise((resolvePromise) => {
519
- httpServer.close(() => resolvePromise());
520
- setTimeout(resolvePromise, 500);
521
- });
330
+ if (!webDist) {
331
+ logger.info("前端未构建,开发态请访问", { vite: `http://127.0.0.1:${DEV_WEB_PORT}` });
522
332
  }
523
- return { port: actualPort, host, url, cwd, version, supervisor, client, close };
333
+ setInterval(() => {
334
+ const buffered = [...sockets].reduce((max, socket) => Math.max(max, socket.bufferedAmount), 0);
335
+ sender.setBusy(buffered > BACKPRESSURE_BYTES);
336
+ sender.tick(Date.now());
337
+ }, TICK_MS);
338
+ setInterval(() => {
339
+ for (const socket of sockets) {
340
+ if (socket.readyState === socket.OPEN)
341
+ socket.ping();
342
+ }
343
+ }, 25_000);
524
344
  }
525
- /* ------------------------------------------------------------------ */
526
- /* direct execution (node dist/server/index.js) */
527
- /* ------------------------------------------------------------------ */
528
- const invokedPath = process.argv[1] ? pathToFileURL(resolve(process.argv[1])).href : "";
529
- if (invokedPath === import.meta.url) {
530
- startServer()
531
- .then((handle) => {
532
- const shutdown = (signal) => {
533
- console.log(`\n[oh-my-agent-web] received ${signal}, shutting down…`);
534
- void handle.close().finally(() => process.exit(0));
535
- };
536
- process.on("SIGINT", () => shutdown("SIGINT"));
537
- process.on("SIGTERM", () => shutdown("SIGTERM"));
538
- })
539
- .catch((err) => {
540
- console.error("[oh-my-agent-web] failed to start:", err);
541
- process.exit(1);
542
- });
345
+ async function shutdown(signal) {
346
+ if (shuttingDown)
347
+ return;
348
+ shuttingDown = true;
349
+ logger.info("收到退出信号,正在关闭", { signal });
350
+ sender.flush(Date.now());
351
+ engineWiring.close(); // 收掉 codex app-server 子进程(不给它留孤儿)
352
+ for (const socket of sockets)
353
+ socket.close(1001, "server shutting down");
354
+ wss.close();
355
+ httpServer.close();
356
+ // 锁要还回去:只有还属于自己时才删(避免删掉接管者的锁)
357
+ await releaseLock(config.dataDir, process.pid);
358
+ process.exit(0);
543
359
  }
360
+ process.on("SIGINT", () => void shutdown("SIGINT"));
361
+ process.on("SIGTERM", () => void shutdown("SIGTERM"));
362
+ main().catch((error) => {
363
+ logger.error("启动失败", { message: error instanceof Error ? error.message : String(error) });
364
+ process.exit(1);
365
+ });