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.
- package/README.md +86 -447
- package/dist/server/approval/read-only.js +107 -0
- package/dist/server/approval/rules.js +85 -0
- package/dist/server/approval/scope.js +150 -0
- package/dist/server/brand.js +2 -0
- package/dist/server/cli-config.js +52 -0
- package/dist/server/cli-info.js +101 -0
- package/dist/server/cli.js +304 -0
- package/dist/server/engine/builtin.js +61 -0
- package/dist/server/engine/catalog.js +8 -0
- package/dist/server/engine/codex-approvals.js +45 -0
- package/dist/server/engine/codex-map.js +298 -0
- package/dist/server/engine/codex-sessions.js +159 -0
- package/dist/server/engine/codex-transport.js +49 -0
- package/dist/server/engine/codex.js +314 -0
- package/dist/server/engine/dsh-history.js +195 -0
- package/dist/server/engine/dsh-log.js +245 -0
- package/dist/server/engine/dsh-map.js +221 -0
- package/dist/server/engine/dsh-sessions.js +207 -0
- package/dist/server/engine/dsh-transport.js +37 -0
- package/dist/server/engine/dsh.js +284 -0
- package/dist/server/engine/fake.js +231 -0
- package/dist/server/engine/jsonrpc-stdio.js +241 -0
- package/dist/server/engine/model-registry-reload.js +115 -0
- package/dist/server/engine/pi-events.js +148 -0
- package/dist/server/engine/pi-rename.js +14 -0
- package/dist/server/engine/pi-sdk.js +343 -0
- package/dist/server/engine/pi-session-model.js +23 -0
- package/dist/server/engine/pi-thinking.js +34 -0
- package/dist/server/engine/pi-transcript.js +225 -0
- package/dist/server/engine/pi-version.js +11 -0
- package/dist/server/engine/port.js +1 -0
- package/dist/server/engine/registry.js +47 -0
- package/dist/server/engine/scratch.js +13 -0
- package/dist/server/engine/suggestion-guidance.js +9 -0
- package/dist/server/engine/switchable.js +22 -0
- package/dist/server/engine/wiring.js +107 -0
- package/dist/server/files/dir-routes.js +40 -0
- package/dist/server/files/file-search.js +63 -0
- package/dist/server/files/file-service.js +232 -0
- package/dist/server/files/files-routes.js +182 -0
- package/dist/server/files/git-info.js +186 -0
- package/dist/server/http/auth-routes.js +99 -0
- package/dist/server/http/auth.js +191 -0
- package/dist/server/http/aux-routes.js +24 -0
- package/dist/server/http/compress.js +75 -0
- package/dist/server/http/health-and-static.js +33 -0
- package/dist/server/http/live-routes.js +113 -0
- package/dist/server/http/login-page.js +200 -0
- package/dist/server/index.js +342 -520
- package/dist/server/markers/actions.js +262 -0
- package/dist/server/protocol-parse.js +201 -0
- package/dist/server/protocol-settings.js +1 -0
- package/dist/server/protocol.js +6 -0
- package/dist/server/providers/catalog-store.js +85 -0
- package/dist/server/providers/catalog-writes.js +197 -0
- package/dist/server/providers/catalog.js +77 -0
- package/dist/server/providers/enabled-models.js +118 -0
- package/dist/server/providers/import-models.js +69 -0
- package/dist/server/providers/import-routes.js +136 -0
- package/dist/server/providers/model-test.js +52 -0
- package/dist/server/providers/probe.js +64 -0
- package/dist/server/providers/provider-handlers.js +203 -0
- package/dist/server/providers/provider-store.js +327 -0
- package/dist/server/providers/request-headers.js +7 -0
- package/dist/server/providers/validate.js +241 -0
- package/dist/server/push/push-routes.js +114 -0
- package/dist/server/push/push.js +120 -0
- package/dist/server/session/approval-bridge.js +103 -0
- package/dist/server/session/attachment-resolve.js +21 -0
- package/dist/server/session/client-view.js +82 -0
- package/dist/server/session/conversation-create.js +65 -0
- package/dist/server/session/delete-session.js +84 -0
- package/dist/server/session/engine-switch.js +108 -0
- package/dist/server/session/lock.js +82 -0
- package/dist/server/session/model-controller.js +111 -0
- package/dist/server/session/notices.js +31 -0
- package/dist/server/session/pi-opener.js +123 -0
- package/dist/server/session/pi-refresh.js +51 -0
- package/dist/server/session/pi-sessions-provider.js +46 -0
- package/dist/server/session/pi-sessions.js +272 -0
- package/dist/server/session/refresh-actions.js +42 -0
- package/dist/server/session/rename-session.js +23 -0
- package/dist/server/session/runtime-services.js +28 -0
- package/dist/server/session/runtime-types.js +1 -0
- package/dist/server/session/runtime.js +358 -0
- package/dist/server/session/session-stats.js +88 -0
- package/dist/server/session/settings.js +367 -0
- package/dist/server/session/snapshot-for.js +7 -0
- package/dist/server/session/stats-refresher.js +50 -0
- package/dist/server/session/switch-conversation.js +86 -0
- package/dist/server/session/throttle.js +14 -0
- package/dist/server/session/turn-events.js +126 -0
- package/dist/server/session/turn-finish.js +79 -0
- package/dist/server/session/turn-markers.js +26 -0
- package/dist/server/session/turn-queue.js +111 -0
- package/dist/server/session/turn-send.js +88 -0
- package/dist/server/session/turn-watchdog.js +77 -0
- package/dist/server/sessions/session-routes.js +17 -0
- package/dist/server/skills/skill-routes.js +28 -0
- package/dist/server/skills/skill-store.js +134 -0
- package/dist/server/snapshot/fan-out.js +10 -0
- package/dist/server/snapshot/messages.js +75 -0
- package/dist/server/snapshot/sender.js +62 -0
- package/dist/server/snapshot/state.js +193 -0
- package/dist/server/terminal/terminal-routes.js +176 -0
- package/dist/server/terminal/terminal-session.js +140 -0
- package/dist/server/uploads/upload-routes.js +64 -0
- package/dist/server/uploads/upload-store.js +115 -0
- package/dist/server/util/data-dir.js +36 -0
- package/dist/server/util/env.js +140 -0
- package/dist/server/util/ids.js +28 -0
- package/dist/server/util/log-file.js +67 -0
- package/dist/server/util/log.js +52 -0
- package/dist/server/util/origin.js +75 -0
- package/dist/server/util/paths.js +60 -0
- package/dist/server/util/pricing-index.js +121 -0
- package/dist/server/util/pricing.js +214 -0
- package/dist/server/util/startup.js +34 -0
- package/dist/server/util/upstream-version.js +140 -0
- package/dist/server/util/usage.js +19 -0
- package/dist/server/util/version.js +50 -0
- package/dist/server/ws/handle-message.js +95 -0
- package/package.json +49 -63
- package/web/dist/apple-touch-icon-precomposed.png +0 -0
- package/web/dist/apple-touch-icon.png +0 -0
- package/web/dist/assets/abnfDiagram-WEYOIA3C-NLp09iEC.js +1 -0
- package/web/dist/assets/arc-DviL9o3c.js +1 -0
- package/web/dist/assets/architecture-WOLXFQ4H-fjsM_Sy3.js +1 -0
- package/web/dist/assets/architectureDiagram-ZHVYKT53-Cn3B4TE8.js +36 -0
- package/web/dist/assets/array-BifhSqXX.js +1 -0
- package/web/dist/assets/blockDiagram-RJAE7QA3-CIuvxWvi.js +140 -0
- package/web/dist/assets/c4Diagram-XDPPPE54-DvdKmEpF.js +38 -0
- package/web/dist/assets/channel-C3AQ6nAs.js +1 -0
- package/web/dist/assets/chunk-2E7WHVMD-HNNASGCS.js +88 -0
- package/web/dist/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
- package/web/dist/assets/chunk-4EMNKR2T-Cg-eRwBE.js +1 -0
- package/web/dist/assets/chunk-55XCU76K-BweiisZL.js +1 -0
- package/web/dist/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
- package/web/dist/assets/chunk-7S6XSTFO-BCzIiP-6.js +2 -0
- package/web/dist/assets/chunk-AQDTSKZB-Dl0NRFuU.js +72 -0
- package/web/dist/assets/chunk-DSNLSHJZ-zhHCjjN9.js +129 -0
- package/web/dist/assets/chunk-DUW6YSOI-Dgx8z5s3.js +1 -0
- package/web/dist/assets/chunk-HAGFOTED-BDQqU2EH.js +62 -0
- package/web/dist/assets/chunk-HNE4Q36P-B-qRzMjX.js +1 -0
- package/web/dist/assets/chunk-J5ZVWO5B-oDCd-gWO.js +1 -0
- package/web/dist/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
- package/web/dist/assets/chunk-NGNAAXSQ-C2Wy9Xmz.js +136 -0
- package/web/dist/assets/chunk-NQOKRVIF-CNcvhMn_.js +213 -0
- package/web/dist/assets/chunk-UHW7PFBP-Dwr8LUzr.js +1 -0
- package/web/dist/assets/chunk-VQ5W62QI-O8sN6jJu.js +1 -0
- package/web/dist/assets/chunk-WQQL4CDK-B6-59Zg2.js +10 -0
- package/web/dist/assets/chunk-XXDRQBXY-DvSbeUlZ.js +1 -0
- package/web/dist/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
- package/web/dist/assets/chunk-YZO67HFG-BXpKYKsw.js +1 -0
- package/web/dist/assets/chunk-ZAUX2LTX-B1Z5VSkz.js +1 -0
- package/web/dist/assets/chunk-ZCHMQSOR-h_5mBLIA.js +2 -0
- package/web/dist/assets/classDiagram-v2-4MYWR3UA-Dr6JnaaV.js +217 -0
- package/web/dist/assets/cose-bilkent-JH36ORCC-36ShkR7R.js +1 -0
- package/web/dist/assets/cynefin-EF2NZ3EQ-Dg6bI7pC.js +1 -0
- package/web/dist/assets/cynefinDiagram-WF24UDRP-B0YHpBlR.js +62 -0
- package/web/dist/assets/cytoscape.esm-BegBNzOV.js +321 -0
- package/web/dist/assets/dagre-NK4GIZVX-Cx3mn7YS.js +4 -0
- package/web/dist/assets/defaultLocale-BFoDCU3G.js +1 -0
- package/web/dist/assets/diagram-CBK3MWTC-l8aZG_Bm.js +24 -0
- package/web/dist/assets/diagram-I5XIIV2D-Bd5f14_h.js +24 -0
- package/web/dist/assets/diagram-ODIL2UCW-hBsxCYUj.js +30 -0
- package/web/dist/assets/diagram-PFBEJUXE-BgpIBUYl.js +3 -0
- package/web/dist/assets/diagram-RJA3I4VV-CDU1gUVD.js +41 -0
- package/web/dist/assets/diagram-TYUNFSNR-Iuxh7Tqf.js +200 -0
- package/web/dist/assets/dist-C86cKTuE.js +1 -0
- package/web/dist/assets/ebnfDiagram-V4P3TTPT-Cms0HFKh.js +1 -0
- package/web/dist/assets/elk-QONTBCTQ-B7yEy0PQ.js +27 -0
- package/web/dist/assets/erDiagram-KYQIGQAA-CL48j4aU.js +99 -0
- package/web/dist/assets/eventmodeling-K75KTNOO-CFvkaejh.js +1 -0
- package/web/dist/assets/flowDiagram-IUGLLWBU-6UyKXpqG.js +1 -0
- package/web/dist/assets/ganttDiagram-R7GPNTJY-vdMr-0ZV.js +292 -0
- package/web/dist/assets/gitGraph-VSP46ZUC-2z85Wuwo.js +1 -0
- package/web/dist/assets/gitGraphDiagram-PDYL7XHY-BY4GPiqC.js +106 -0
- package/web/dist/assets/graphlib-DS17s2tU.js +1 -0
- package/web/dist/assets/index-Bjn56a2i.js +76 -0
- package/web/dist/assets/index-Drp6e5iU.css +1 -0
- package/web/dist/assets/info-OHQRW6UA-ECzkYTx-.js +1 -0
- package/web/dist/assets/infoDiagram-MVDRKAX5-DDk5EAeb.js +2 -0
- package/web/dist/assets/init-C-OQMol4.js +1 -0
- package/web/dist/assets/ishikawaDiagram-HPEHV2GE-9yPH4J94.js +70 -0
- package/web/dist/assets/journeyDiagram-BOSHW7O7-BPTIap3l.js +139 -0
- package/web/dist/assets/kanban-definition-FNSA4HC5-BbADHrA8.js +89 -0
- package/web/dist/assets/katex-B_JcIwqj.js +257 -0
- package/web/dist/assets/line-CHV9_4lL.js +1 -0
- package/web/dist/assets/linear-C-eZsBTv.js +1 -0
- package/web/dist/assets/mermaid-parser.core-BiAhX1l1.js +7 -0
- package/web/dist/assets/mermaid.core-BxP_A_zj.js +44 -0
- package/web/dist/assets/mindmap-definition-6OY57UG3-BMWlit95.js +98 -0
- package/web/dist/assets/ordinal-BDEzSJ7C.js +1 -0
- package/web/dist/assets/packet-JDAUHWVQ-FVApuKOp.js +1 -0
- package/web/dist/assets/path-fybaL0A-.js +1 -0
- package/web/dist/assets/pegDiagram-ECIUJNBX-WjWJvDHM.js +1 -0
- package/web/dist/assets/pie-XZMESJXO-BGCZnvhp.js +1 -0
- package/web/dist/assets/pieDiagram-GNMPY74T-DJpAC_JC.js +39 -0
- package/web/dist/assets/quadrantDiagram-D57K4MC3-hxx5c7oq.js +7 -0
- package/web/dist/assets/radar-ABXABTNO-BU9Shc-O.js +1 -0
- package/web/dist/assets/railroad-I3PHUGI6-pWj8d-G4.js +1 -0
- package/web/dist/assets/railroad-abnf-LNEFI6M7-D7HZBTAj.js +1 -0
- package/web/dist/assets/railroad-ebnf-DWYD2UWJ-CKiXJSuM.js +1 -0
- package/web/dist/assets/railroad-peg-JR7BVNR7-DB3VJHgR.js +1 -0
- package/web/dist/assets/railroadDiagram-Y72NRBL7-Cev7Y65k.js +1 -0
- package/web/dist/assets/requirementDiagram-EQYSW5RA-CqbwMLD8.js +84 -0
- package/web/dist/assets/rough.esm-Dy-Kn_BL.js +1 -0
- package/web/dist/assets/sankeyDiagram-SY24JHO5-V2cyVi8q.js +40 -0
- package/web/dist/assets/sequenceDiagram-BLEHNHQU-By6NJOPM.js +169 -0
- package/web/dist/assets/sizeCapture-INFHLROL-3uHXrFqA.js +1 -0
- package/web/dist/assets/src-Da91fG3g.js +1 -0
- package/web/dist/assets/stateDiagram-v2-A6IESKWZ-BnPGX5UI.js +291 -0
- package/web/dist/assets/swimlanes-B3ENRKFQ-C58BpIpm.js +1 -0
- package/web/dist/assets/swimlanesDiagram-AYQRV4AK-RaivxSsc.js +8 -0
- package/web/dist/assets/timeline-definition-JXYKGLGB-Hgo1DhnS.js +120 -0
- package/web/dist/assets/treeView-D4JQ5SDB-DF0dfjqB.js +1 -0
- package/web/dist/assets/treemap-SAJKECNS-CSSM5FqB.js +1 -0
- package/web/dist/assets/usecaseDiagram-WPOLBNNX-BkBBD1NY.js +328 -0
- package/web/dist/assets/vennDiagram-Q3Y3IDCW-Bm28keX_.js +34 -0
- package/web/dist/assets/wardley-7MLQ67FV-By7KgEQX.js +1 -0
- package/web/dist/assets/wardleyDiagram-IVAIDKWA-oVtIrnGa.js +78 -0
- package/web/dist/assets/xychartDiagram-4URNHDU4-aI8FH4g2.js +7 -0
- package/web/dist/favicon-32.png +0 -0
- package/web/dist/favicon.ico +0 -0
- package/web/dist/favicon.svg +20 -8
- package/web/dist/icons/apple-touch-icon-180.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon-maskable-512.png +0 -0
- package/web/dist/index.html +21 -27
- package/web/dist/manifest.webmanifest +17 -0
- package/web/dist/maskable.svg +25 -0
- package/web/dist/sw.js +40 -0
- package/bin/oh-my-agent-web-ps.mjs +0 -203
- package/bin/oh-my-agent-web-ws.mjs +0 -593
- package/bin/oh-my-agent-web.mjs +0 -321
- package/bin/update-check.mjs +0 -142
- package/dist/server/codex-client.js +0 -310
- package/dist/server/codex-supervisor.js +0 -446
- package/dist/server/fs-service.js +0 -144
- package/dist/server/request-inspector.js +0 -458
- package/dist/shared/protocol.js +0 -112
- package/scripts/manage-server.mjs +0 -155
- package/scripts/omaw.mjs +0 -74
- package/shared/protocol.ts +0 -228
- package/web/dist/assets/index-B8-VzDf2.css +0 -1
- package/web/dist/assets/index-DeC2eiiq.js +0 -42
- package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { registerCatalogRoutes, registerImportRoutes } from "../providers/import-routes.js";
|
|
2
|
+
import { createCatalogLookup } from "../providers/catalog.js";
|
|
3
|
+
import { testModel } from "../providers/model-test.js";
|
|
4
|
+
import { registerSkillRoutes } from "../skills/skill-routes.js";
|
|
5
|
+
import { registerProjectActions, registerTerminalRoutes } from "../terminal/terminal-routes.js";
|
|
6
|
+
import { registerDirRoutes } from "../files/dir-routes.js";
|
|
7
|
+
import { registerFileRoutes } from "../files/files-routes.js";
|
|
8
|
+
import { registerSessionRoutes } from "../sessions/session-routes.js";
|
|
9
|
+
import { registerPushRoutes } from "../push/push-routes.js";
|
|
10
|
+
export function registerAuxRoutes(app, options) {
|
|
11
|
+
registerSessionRoutes(app, { deleteSession: options.deleteSession });
|
|
12
|
+
registerPushRoutes(app, { store: options.pushStore });
|
|
13
|
+
registerFileRoutes(app, { workspaceOf: options.workspaceOf });
|
|
14
|
+
registerTerminalRoutes(app, { workspaceOf: options.workspaceOf });
|
|
15
|
+
registerProjectActions(app, { workspaceOf: options.workspaceOf });
|
|
16
|
+
registerDirRoutes(app, { workspaceOf: options.workspaceOf });
|
|
17
|
+
registerSkillRoutes(app);
|
|
18
|
+
registerImportRoutes(app, { store: options.providerStore, onChange: options.onChange });
|
|
19
|
+
registerCatalogRoutes(app, {
|
|
20
|
+
store: options.providerStore,
|
|
21
|
+
catalog: createCatalogLookup(),
|
|
22
|
+
testModel: (input) => testModel(input),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 文本响应压缩(gzip)+ 静态资源长缓存。
|
|
3
|
+
*
|
|
4
|
+
* 为什么自己写而不是引 `compression` 包:依赖预算(ADR-0008/0029)已经很紧,
|
|
5
|
+
* 而这里只需要「把 text/json/js/css/svg 的响应体 gzip 一下」这一件事,zlib 就够。
|
|
6
|
+
*
|
|
7
|
+
* 实测背景:外网打开慢,主包 1.0MB 且**未压缩**、`Cache-Control: public, max-age=0`,
|
|
8
|
+
* 每次访问都要重新拉一遍 —— 这两条一起改,首屏传输量降到 1/3 左右。
|
|
9
|
+
*/
|
|
10
|
+
import { gzipSync } from "node:zlib";
|
|
11
|
+
const TEXT_TYPE = /text|javascript|json|svg|css|xml/i;
|
|
12
|
+
export function compressText() {
|
|
13
|
+
return (request, response, next) => {
|
|
14
|
+
const accept = String(request.headers["accept-encoding"] ?? "");
|
|
15
|
+
// SSE 与文件字节流不能攒也不能压:
|
|
16
|
+
// - SSE 攒着等于永远不发送(终端输出同理);
|
|
17
|
+
// - 视频/音频要支持 Range 拖动,整块进内存会直接把进程吃爆。
|
|
18
|
+
const streamy = request.path === "/api/events" ||
|
|
19
|
+
request.path.startsWith("/api/terminal/stream") ||
|
|
20
|
+
request.path === "/api/files/media" ||
|
|
21
|
+
request.path === "/api/files/raw";
|
|
22
|
+
if (!/gzip/i.test(accept) || streamy) {
|
|
23
|
+
next();
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
const chunks = [];
|
|
27
|
+
const originalEnd = response.end.bind(response);
|
|
28
|
+
let handled = false;
|
|
29
|
+
// 先把响应体攒起来,结束时统一压(单用户工作台,内存里攒一个包没问题)
|
|
30
|
+
response.write = ((chunk, ...rest) => {
|
|
31
|
+
if (chunk)
|
|
32
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
33
|
+
const callback = rest.find((item) => typeof item === "function");
|
|
34
|
+
callback?.();
|
|
35
|
+
return true;
|
|
36
|
+
});
|
|
37
|
+
response.end = ((chunk, ...rest) => {
|
|
38
|
+
if (handled)
|
|
39
|
+
return response;
|
|
40
|
+
handled = true;
|
|
41
|
+
if (chunk && typeof chunk !== "function") {
|
|
42
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
43
|
+
}
|
|
44
|
+
const body = Buffer.concat(chunks);
|
|
45
|
+
const type = String(response.getHeader("content-type") ?? "");
|
|
46
|
+
const alreadyEncoded = response.getHeader("content-encoding") !== undefined;
|
|
47
|
+
// HEAD / 204 / 304 没有实体:不能把 content-length 改成 0(那是在撒谎,
|
|
48
|
+
// 也会让 HEAD 拿不到真实长度);express.static 已经设好的长度要留着。
|
|
49
|
+
const status = response.statusCode;
|
|
50
|
+
const noBody = request.method === "HEAD" || status === 204 || status === 304;
|
|
51
|
+
const hadLength = response.getHeader("content-length") !== undefined;
|
|
52
|
+
if (noBody || alreadyEncoded || !TEXT_TYPE.test(type) || body.length < 1024) {
|
|
53
|
+
if (!noBody && !(body.length === 0 && hadLength)) {
|
|
54
|
+
response.setHeader("content-length", String(body.length));
|
|
55
|
+
}
|
|
56
|
+
return originalEnd(body, ...rest);
|
|
57
|
+
}
|
|
58
|
+
const gzipped = gzipSync(body, { level: 6 });
|
|
59
|
+
response.setHeader("content-encoding", "gzip");
|
|
60
|
+
response.setHeader("vary", "accept-encoding");
|
|
61
|
+
response.setHeader("content-length", String(gzipped.length));
|
|
62
|
+
return originalEnd(gzipped, ...rest);
|
|
63
|
+
});
|
|
64
|
+
next();
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/** 带哈希的构建产物可以长缓存:文件名变了 URL 就变,不会读到旧内容 */
|
|
68
|
+
export function immutableAssets() {
|
|
69
|
+
return (request, response, next) => {
|
|
70
|
+
if (request.path.startsWith("/assets/")) {
|
|
71
|
+
response.setHeader("cache-control", "public, max-age=31536000, immutable");
|
|
72
|
+
}
|
|
73
|
+
next();
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP 的「非 API」部分(从 index.ts 抽出:400 行护栏):健康检查 + 前端静态资源。
|
|
3
|
+
*/
|
|
4
|
+
import express, {} from "express";
|
|
5
|
+
import { PROTOCOL_VERSION } from "../protocol.js";
|
|
6
|
+
import { webVersion } from "../util/version.js";
|
|
7
|
+
export function registerHealthAndStatic(app, options) {
|
|
8
|
+
// 压缩 + 缓存头由 index.ts 在**注册 API 路由之前**挂(见那里的注释):
|
|
9
|
+
// 在这里挂只会覆盖它之后注册的路由,`/api/rpc` 那些大包就永远压不到。
|
|
10
|
+
app.get("/api/health", (_request, response) => {
|
|
11
|
+
const counts = options.counts();
|
|
12
|
+
response.json({
|
|
13
|
+
ok: true,
|
|
14
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
15
|
+
// 跑着的这个进程的版本(omaw status 用它和磁盘上的 package.json 对比)
|
|
16
|
+
version: webVersion(),
|
|
17
|
+
workspace: options.workspace,
|
|
18
|
+
engine: options.engine,
|
|
19
|
+
// 会话事实源是 pi 的会话存储(ADR-0021 §2)→ 报它的数量
|
|
20
|
+
sessions: counts.sessions,
|
|
21
|
+
conversations: counts.conversations,
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
if (options.webDist) {
|
|
25
|
+
app.use(express.static(options.webDist));
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
app.get("/", (_request, response) => {
|
|
29
|
+
response
|
|
30
|
+
.type("text/plain")
|
|
31
|
+
.send(`agent-web 服务已启动\n\n前端开发态请用:npm run dev(vite 在 http://127.0.0.1:${options.devWebPort})\n`);
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 纯 HTTP 的实时通道(SSE + RPC)。ADR-0032 定通道,ADR-0033 定「每客户端一份会话视图」。
|
|
3
|
+
*
|
|
4
|
+
* 两个端点:
|
|
5
|
+
* - `GET /api/events` SSE 长连接:服务端 → 客户端(快照 / 事件)
|
|
6
|
+
* - `POST /api/rpc` 一问一答:客户端 → 服务端(协议不变,还是 ClientRequest)
|
|
7
|
+
*
|
|
8
|
+
* 客户端身份:`agent-web-client` cookie。有了它,快照才能按「这个客户端在看哪个会话」生成
|
|
9
|
+
* (否则两个标签页会互相拽着切换)。cookie 不存在时由 `/api/events` 发一个。
|
|
10
|
+
*/
|
|
11
|
+
import { randomUUID } from "node:crypto";
|
|
12
|
+
import { constants as zlibConstants, createGzip } from "node:zlib";
|
|
13
|
+
export const CLIENT_COOKIE = "agent-web-client";
|
|
14
|
+
/** 每个标签页自己的标识(cookie 是同浏览器共享的,不能用来区分标签页) */
|
|
15
|
+
export const CLIENT_HEADER = "x-agent-client";
|
|
16
|
+
/** 客户端标识:优先请求头 / 查询参数(每个标签页不一样),最后才回落到 cookie */
|
|
17
|
+
export function clientIdOf(input) {
|
|
18
|
+
const fromHeader = Array.isArray(input.header) ? input.header[0] : input.header;
|
|
19
|
+
if (typeof fromHeader === "string" && fromHeader.trim() !== "")
|
|
20
|
+
return fromHeader.trim();
|
|
21
|
+
if (typeof input.query === "string" && input.query.trim() !== "")
|
|
22
|
+
return input.query.trim();
|
|
23
|
+
return clientIdFromCookie(input.cookie);
|
|
24
|
+
}
|
|
25
|
+
/** 从 Cookie 头里取客户端标识 */
|
|
26
|
+
export function clientIdFromCookie(header) {
|
|
27
|
+
if (!header)
|
|
28
|
+
return null;
|
|
29
|
+
for (const part of header.split(";")) {
|
|
30
|
+
const [name, ...rest] = part.trim().split("=");
|
|
31
|
+
if (name === CLIENT_COOKIE) {
|
|
32
|
+
const value = rest.join("=").trim();
|
|
33
|
+
return value === "" ? null : value;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
export function registerLiveRoutes(app, deps) {
|
|
39
|
+
app.get("/api/events", (request, response) => {
|
|
40
|
+
const existing = clientIdOf({
|
|
41
|
+
header: request.headers[CLIENT_HEADER],
|
|
42
|
+
query: request.query?.client,
|
|
43
|
+
cookie: request.headers.cookie,
|
|
44
|
+
});
|
|
45
|
+
const clientId = existing ?? randomUUID();
|
|
46
|
+
if (!existing) {
|
|
47
|
+
response.setHeader("Set-Cookie", `${CLIENT_COOKIE}=${clientId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=31536000`);
|
|
48
|
+
}
|
|
49
|
+
// SSE 首帧快照 ~136KB,wifi/外网下很慢;能 gzip 就流式压(不能攒,必须边压边发)
|
|
50
|
+
const gzip = /gzip/i.test(String(request.headers["accept-encoding"] ?? ""));
|
|
51
|
+
response.writeHead(200, {
|
|
52
|
+
"content-type": "text/event-stream; charset=utf-8",
|
|
53
|
+
// no-store:**不能让任何代理/缓存重放这条响应**。曾经怀疑过反向代理把
|
|
54
|
+
// 首个客户端的 `Set-Cookie` 或首帧快照缓存后重放给别人(那会让两个设备拿到同一个
|
|
55
|
+
// 客户端标识 → 互相拽着切会话)。no-transform 顺带禁掉压缩二次加工。
|
|
56
|
+
"cache-control": "no-store, no-transform",
|
|
57
|
+
connection: "keep-alive",
|
|
58
|
+
// nginx 默认会缓冲响应体,SSE 必须显式关掉,否则事件全卡在缓冲里
|
|
59
|
+
"x-accel-buffering": "no",
|
|
60
|
+
...(gzip ? { "content-encoding": "gzip", vary: "accept-encoding" } : {}),
|
|
61
|
+
});
|
|
62
|
+
response.flushHeaders?.();
|
|
63
|
+
// `flush: Z_SYNC_FLUSH` 是必需的,不是调优项:SSE 每一帧都得**立刻**落地。
|
|
64
|
+
// 默认模式(Z_NO_FLUSH)下 zlib 会把小帧攒在内部缓冲区里,几 KB~几十 KB 的快照
|
|
65
|
+
// 要等下一大块数据才被推出去 —— 表现就是「切会话 / 新建会话页面不跳、快照像停了」。
|
|
66
|
+
const compressor = gzip ? createGzip({ level: 6, flush: zlibConstants.Z_SYNC_FLUSH }) : null;
|
|
67
|
+
compressor?.on("data", (chunk) => response.write(chunk));
|
|
68
|
+
const send = (text) => (compressor ? compressor.write(text) : response.write(text));
|
|
69
|
+
const sink = (payload) => send(`data: ${payload}\n\n`);
|
|
70
|
+
deps.sinks.set(clientId, sink);
|
|
71
|
+
// 能力协商:带 slim=1 的客户端由服务端决定「会话清单只在变化时带」
|
|
72
|
+
deps.clientTracker?.setSlim(clientId, request.query?.slim === "1");
|
|
73
|
+
// 带上客户端标识前缀:排查「两个设备互相拽着切会话」时,一眼能看出它们是不是同一个 id
|
|
74
|
+
deps.logger.info("SSE 已连接", {
|
|
75
|
+
clients: deps.sinks.size,
|
|
76
|
+
slim: request.query?.slim === "1",
|
|
77
|
+
client: clientId.slice(0, 8),
|
|
78
|
+
from: existing ? (request.query?.client ? "query" : "cookie") : "new",
|
|
79
|
+
});
|
|
80
|
+
// 首屏:立刻给这个客户端一份「属于它自己」的快照
|
|
81
|
+
sink(JSON.stringify({ type: "event", event: "snapshot", snapshot: deps.snapshotFor(clientId) }));
|
|
82
|
+
// 心跳:中间代理一般 60s 掐空闲连接,25s 发一次注释行足够保活
|
|
83
|
+
const beat = setInterval(() => send(": ping\n\n"), 25_000);
|
|
84
|
+
request.on("close", () => {
|
|
85
|
+
clearInterval(beat);
|
|
86
|
+
compressor?.end();
|
|
87
|
+
// 只删**自己**那条:同一个 clientId 重连时,新连接会覆盖 map 里的条目,
|
|
88
|
+
// 旧连接的 close 若照删不误,就会把新连接一起干掉(心跳还在、但再也收不到快照/事件)。
|
|
89
|
+
if (deps.sinks.get(clientId) === sink) {
|
|
90
|
+
deps.sinks.delete(clientId);
|
|
91
|
+
deps.clientTracker?.drop(clientId);
|
|
92
|
+
}
|
|
93
|
+
deps.logger.info("SSE 已断开", { clients: deps.sinks.size });
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
app.post("/api/rpc", async (request, response) => {
|
|
97
|
+
// 同上:RPC 的结果里带快照,绝不能被缓存重放
|
|
98
|
+
response.setHeader("cache-control", "no-store");
|
|
99
|
+
const clientId = clientIdOf({ header: request.headers[CLIENT_HEADER], cookie: request.headers.cookie }) ?? "anonymous";
|
|
100
|
+
const frames = [];
|
|
101
|
+
const shim = { send: (payload) => frames.push(payload) };
|
|
102
|
+
try {
|
|
103
|
+
await deps.handleMessage(clientId, shim, JSON.stringify(request.body));
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
deps.logger.info("RPC 处理失败", { message: error instanceof Error ? error.message : String(error) });
|
|
107
|
+
response.status(500).json({ frames, error: "handler_failed" });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
// 同步产生的帧(reply / notice)随响应带回;异步事件走 SSE
|
|
111
|
+
response.json({ frames });
|
|
112
|
+
});
|
|
113
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 非本机访问时的登录页(ADR-0031 修订):自包含单页,不依赖前端构建产物。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不再用浏览器原生 Basic 弹窗:
|
|
5
|
+
* 1. 各处实现不一致(有的不显示中文提示、有的不记住密码、手机上弹出体验很差);
|
|
6
|
+
* 2. 弹窗无法表达「登录状态保存 7 天」这类产品语义;
|
|
7
|
+
* 3. 它只是一张表单 —— 自己渲染 20 行 HTML 就够了,还能顺手控制视觉。
|
|
8
|
+
*
|
|
9
|
+
* 约定:
|
|
10
|
+
* - 无 JS 时也能用:表单 `method=post` 直接打到 `/api/auth/login`,服务端 303 回 `/`;
|
|
11
|
+
* - 页面里不出现任何用户数据(只有品牌名),所以未登录时返回它并不扩大攻击面。
|
|
12
|
+
*/
|
|
13
|
+
import { BRAND_NAME } from "../brand.js";
|
|
14
|
+
/** 极简转义:这里只会插品牌名与错误文案,但仍然按不可信对待 */
|
|
15
|
+
export function escapeHtml(text) {
|
|
16
|
+
return text
|
|
17
|
+
.replace(/&/g, "&")
|
|
18
|
+
.replace(/</g, "<")
|
|
19
|
+
.replace(/>/g, ">")
|
|
20
|
+
.replace(/"/g, """)
|
|
21
|
+
.replace(/'/g, "'");
|
|
22
|
+
}
|
|
23
|
+
const STYLES = `
|
|
24
|
+
:root { color-scheme: light; }
|
|
25
|
+
* { box-sizing: border-box; }
|
|
26
|
+
html, body { height: 100%; margin: 0; }
|
|
27
|
+
body {
|
|
28
|
+
display: flex;
|
|
29
|
+
align-items: center;
|
|
30
|
+
justify-content: center;
|
|
31
|
+
background: #fff;
|
|
32
|
+
color: #1b2530;
|
|
33
|
+
font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
|
|
34
|
+
padding: 24px clamp(20px, 8vw, 48px);
|
|
35
|
+
}
|
|
36
|
+
.login { width: 100%; max-width: 520px; }
|
|
37
|
+
.head { display: flex; align-items: center; gap: 18px; }
|
|
38
|
+
.mark {
|
|
39
|
+
width: 60px;
|
|
40
|
+
height: 60px;
|
|
41
|
+
flex: none;
|
|
42
|
+
border: 1px solid #e8e8e8;
|
|
43
|
+
border-radius: 14px;
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
justify-content: center;
|
|
47
|
+
background: #fff;
|
|
48
|
+
}
|
|
49
|
+
.mark img { width: 44px; height: 44px; display: block; }
|
|
50
|
+
h1 { margin: 0; font-size: 26px; line-height: 1.3; font-weight: 500; }
|
|
51
|
+
.sub { margin: 2px 0 0; font-size: 14px; color: #8b95a1; }
|
|
52
|
+
.field {
|
|
53
|
+
margin-top: 28px;
|
|
54
|
+
display: flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
gap: 8px;
|
|
57
|
+
border: 1px solid #e4e7ec;
|
|
58
|
+
border-radius: 16px;
|
|
59
|
+
padding: 5px 5px 5px 18px;
|
|
60
|
+
background: #fff;
|
|
61
|
+
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
|
62
|
+
}
|
|
63
|
+
.field:focus-within { border-color: #d3d8df; box-shadow: 0 0 0 4px rgba(17, 24, 39, 0.04); }
|
|
64
|
+
input {
|
|
65
|
+
flex: 1 1 auto;
|
|
66
|
+
min-width: 0;
|
|
67
|
+
height: 44px;
|
|
68
|
+
padding: 0;
|
|
69
|
+
border: 0;
|
|
70
|
+
outline: 0;
|
|
71
|
+
background: transparent;
|
|
72
|
+
font: inherit;
|
|
73
|
+
font-size: 17px;
|
|
74
|
+
color: inherit;
|
|
75
|
+
}
|
|
76
|
+
input::placeholder { color: #a8b0ba; }
|
|
77
|
+
button {
|
|
78
|
+
flex: none;
|
|
79
|
+
display: inline-flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
gap: 5px;
|
|
82
|
+
height: 44px;
|
|
83
|
+
padding: 0 16px;
|
|
84
|
+
border: 0;
|
|
85
|
+
border-radius: 12px;
|
|
86
|
+
background: #f1f2f4;
|
|
87
|
+
color: #2b3440;
|
|
88
|
+
font: inherit;
|
|
89
|
+
font-size: 16px;
|
|
90
|
+
cursor: pointer;
|
|
91
|
+
}
|
|
92
|
+
button:hover { background: #e8eaed; }
|
|
93
|
+
button:active { background: #e0e3e7; }
|
|
94
|
+
button[disabled] { opacity: 0.6; cursor: default; }
|
|
95
|
+
.arrow { font-size: 15px; line-height: 1; }
|
|
96
|
+
.error { margin: 10px 4px 0; font-size: 14px; color: #c62828; }
|
|
97
|
+
.error[hidden] { display: none; }
|
|
98
|
+
`;
|
|
99
|
+
/** 登录页脚本:只做「提交 → 成功就原地刷新 → 失败显示原因」,不引入任何依赖 */
|
|
100
|
+
const SCRIPT = `
|
|
101
|
+
(function () {
|
|
102
|
+
var form = document.getElementById("form");
|
|
103
|
+
var input = document.getElementById("password");
|
|
104
|
+
var button = document.getElementById("submit");
|
|
105
|
+
var error = document.getElementById("error");
|
|
106
|
+
if (!form || !input || !button || !error) return;
|
|
107
|
+
|
|
108
|
+
function showError(text) {
|
|
109
|
+
error.textContent = text;
|
|
110
|
+
error.hidden = false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function fail(text) {
|
|
114
|
+
showError(text);
|
|
115
|
+
button.disabled = false;
|
|
116
|
+
input.select();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
form.addEventListener("submit", function (event) {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
if (button.disabled) return;
|
|
122
|
+
var password = input.value;
|
|
123
|
+
if (!password) {
|
|
124
|
+
showError("请输入密码");
|
|
125
|
+
input.focus();
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
error.hidden = true;
|
|
129
|
+
button.disabled = true;
|
|
130
|
+
fetch("/api/auth/login", {
|
|
131
|
+
method: "POST",
|
|
132
|
+
headers: { "content-type": "application/json" },
|
|
133
|
+
body: JSON.stringify({ password: password }),
|
|
134
|
+
credentials: "same-origin",
|
|
135
|
+
})
|
|
136
|
+
.then(function (response) {
|
|
137
|
+
if (response.ok) {
|
|
138
|
+
location.replace(location.pathname + location.search);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
return response
|
|
142
|
+
.json()
|
|
143
|
+
.catch(function () {
|
|
144
|
+
return null;
|
|
145
|
+
})
|
|
146
|
+
.then(function (body) {
|
|
147
|
+
fail((body && body.error) || "密码不正确");
|
|
148
|
+
});
|
|
149
|
+
})
|
|
150
|
+
.catch(function () {
|
|
151
|
+
fail("网络异常,请重试");
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
})();
|
|
155
|
+
`;
|
|
156
|
+
export function renderLoginPage(options = {}) {
|
|
157
|
+
const brand = escapeHtml(options.brandName ?? BRAND_NAME);
|
|
158
|
+
const error = options.error ? escapeHtml(options.error) : "";
|
|
159
|
+
return `<!doctype html>
|
|
160
|
+
<html lang="zh-CN">
|
|
161
|
+
<head>
|
|
162
|
+
<meta charset="utf-8" />
|
|
163
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
164
|
+
<meta name="robots" content="noindex, nofollow" />
|
|
165
|
+
<title>登录 · ${brand}</title>
|
|
166
|
+
<link rel="icon" href="/favicon.ico?v=4" sizes="16x16 32x32 48x48" />
|
|
167
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg?v=4" />
|
|
168
|
+
<link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon-180.png?v=4" />
|
|
169
|
+
<meta name="theme-color" content="#ffffff" />
|
|
170
|
+
<style>${STYLES}</style>
|
|
171
|
+
</head>
|
|
172
|
+
<body>
|
|
173
|
+
<main class="login">
|
|
174
|
+
<div class="head">
|
|
175
|
+
<span class="mark"><img src="/favicon.svg?v=4" alt="" width="44" height="44" /></span>
|
|
176
|
+
<div>
|
|
177
|
+
<h1>${brand}</h1>
|
|
178
|
+
<p class="sub">登录后继续使用</p>
|
|
179
|
+
</div>
|
|
180
|
+
</div>
|
|
181
|
+
<form class="field" id="form" method="post" action="/api/auth/login">
|
|
182
|
+
<input
|
|
183
|
+
id="password"
|
|
184
|
+
name="password"
|
|
185
|
+
type="password"
|
|
186
|
+
placeholder="密码"
|
|
187
|
+
autocomplete="current-password"
|
|
188
|
+
aria-label="访问密码"
|
|
189
|
+
autofocus
|
|
190
|
+
required
|
|
191
|
+
/>
|
|
192
|
+
<button type="submit" id="submit"><span class="arrow" aria-hidden="true">→</span>登录</button>
|
|
193
|
+
</form>
|
|
194
|
+
<p class="error" id="error" role="alert"${error ? "" : " hidden"}>${error}</p>
|
|
195
|
+
</main>
|
|
196
|
+
<script>${SCRIPT}</script>
|
|
197
|
+
</body>
|
|
198
|
+
</html>
|
|
199
|
+
`;
|
|
200
|
+
}
|