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,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 通用「子进程 + stdio JSON-RPC」客户端(codex / dsh 共用)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要它:codex 的 `app-server` 和 dsh 的 `--profile acp` 是**同一种东西**——
|
|
5
|
+
* 每行一个 JSON-RPC 帧、stdout 只走协议、服务端会反过来向我们发请求(审批/权限)。
|
|
6
|
+
* 差异只有三处,全部参数化:
|
|
7
|
+
* 1. 启动命令(`codex app-server` / `dsh --profile acp`);
|
|
8
|
+
* 2. 握手方法(都是 `initialize`,但参数/返回值不同);
|
|
9
|
+
* 3. 帧里带不带 `"jsonrpc":"2.0"`(ACP 要求;codex 不带)。
|
|
10
|
+
*
|
|
11
|
+
* 生命周期纪律(两个引擎一致):
|
|
12
|
+
* - **懒启动**:第一次 request 才 spawn —— 机器上没装某个 CLI 不该让工作台起不来;
|
|
13
|
+
* - **挂了就重来**:子进程退出 → 拒掉所有在飞请求 → 下次请求重新拉起;
|
|
14
|
+
* - **退出即收**:`close()` 杀掉子进程(codex 由此释放它持有的全部 thread 写锁)。
|
|
15
|
+
*
|
|
16
|
+
* 帧格式:
|
|
17
|
+
* 请求 → `{"id":1,"method":"…","params":{…}}\n`
|
|
18
|
+
* 响应 → `{"id":1,"result":{…}}` / `{"id":1,"error":{"code":-32602,"message":"…"}}`
|
|
19
|
+
* 通知 → `{"method":"…","params":{…}}`(无 id)
|
|
20
|
+
* 服务端请求 → `{"id":9,"method":"…","params":{…}}`(有 id + method,**必须回包**否则对端一直等)
|
|
21
|
+
*/
|
|
22
|
+
import { spawn } from "node:child_process";
|
|
23
|
+
export function createJsonRpcStdioClient(options) {
|
|
24
|
+
const baseTimeout = options.timeoutMs ?? 30_000;
|
|
25
|
+
const prefix = options.name;
|
|
26
|
+
let child = null;
|
|
27
|
+
let starting = null;
|
|
28
|
+
let initialized = false;
|
|
29
|
+
let seq = 0;
|
|
30
|
+
let version = null;
|
|
31
|
+
let buffer = "";
|
|
32
|
+
const pending = new Map();
|
|
33
|
+
const notificationListeners = new Set();
|
|
34
|
+
const serverRequestListeners = new Set();
|
|
35
|
+
const exitListeners = new Set();
|
|
36
|
+
const rejectAll = (error) => {
|
|
37
|
+
for (const [id, entry] of pending) {
|
|
38
|
+
clearTimeout(entry.timer);
|
|
39
|
+
entry.reject(error);
|
|
40
|
+
pending.delete(id);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
const send = (payload) => {
|
|
44
|
+
if (!child)
|
|
45
|
+
throw new Error(`${prefix} 未连接`);
|
|
46
|
+
child.stdin.write(`${JSON.stringify(options.jsonrpc ? { jsonrpc: "2.0", ...payload } : payload)}\n`);
|
|
47
|
+
};
|
|
48
|
+
const respond = (id, result) => {
|
|
49
|
+
try {
|
|
50
|
+
send({ id, result });
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
options.log?.(`${prefix}: 回包失败(${id}):${error instanceof Error ? error.message : String(error)}`);
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
const handleLine = (line) => {
|
|
57
|
+
if (line.trim() === "")
|
|
58
|
+
return;
|
|
59
|
+
let message;
|
|
60
|
+
try {
|
|
61
|
+
message = JSON.parse(line);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
options.log?.(`${prefix}: 收到非 JSON 帧(已忽略):${line.slice(0, 120)}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
const id = typeof message.id === "number" ? message.id : null;
|
|
68
|
+
const method = typeof message.method === "string" ? message.method : null;
|
|
69
|
+
if (method && id !== null) {
|
|
70
|
+
const request = { id, method, params: (message.params ?? {}) };
|
|
71
|
+
if (serverRequestListeners.size === 0) {
|
|
72
|
+
// 没人接也得回一个:不回包对端会一直等下去
|
|
73
|
+
respond(id, { decision: "decline" });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
for (const listener of serverRequestListeners)
|
|
77
|
+
listener(request);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (method) {
|
|
81
|
+
const notification = { method, params: (message.params ?? {}) };
|
|
82
|
+
for (const listener of notificationListeners)
|
|
83
|
+
listener(notification);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (id === null)
|
|
87
|
+
return;
|
|
88
|
+
const entry = pending.get(id);
|
|
89
|
+
if (!entry)
|
|
90
|
+
return;
|
|
91
|
+
pending.delete(id);
|
|
92
|
+
clearTimeout(entry.timer);
|
|
93
|
+
const error = message.error;
|
|
94
|
+
if (error) {
|
|
95
|
+
const detail = error.data && typeof error.data === "object" && "details" in error.data
|
|
96
|
+
? `:${String(error.data.details)}`
|
|
97
|
+
: "";
|
|
98
|
+
entry.reject(new Error(`${error.message ?? `${prefix} 请求失败`}${detail}`));
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
entry.resolve(message.result);
|
|
102
|
+
};
|
|
103
|
+
const wire = (process) => {
|
|
104
|
+
process.stdout.setEncoding("utf8");
|
|
105
|
+
process.stdout.on("data", (chunk) => {
|
|
106
|
+
buffer += chunk;
|
|
107
|
+
let index;
|
|
108
|
+
while ((index = buffer.indexOf("\n")) >= 0) {
|
|
109
|
+
const line = buffer.slice(0, index);
|
|
110
|
+
buffer = buffer.slice(index + 1);
|
|
111
|
+
handleLine(line);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
process.stderr.setEncoding("utf8");
|
|
115
|
+
process.stderr.on("data", (chunk) => {
|
|
116
|
+
const text = chunk.trim();
|
|
117
|
+
// 两边都会在 stderr 打启动信息,别把日志刷满
|
|
118
|
+
if (text !== "" && !/^(WARNING|note:)/.test(text))
|
|
119
|
+
options.log?.(`${prefix}: ${text.slice(0, 400)}`);
|
|
120
|
+
});
|
|
121
|
+
const onGone = (reason) => {
|
|
122
|
+
if (child !== process)
|
|
123
|
+
return;
|
|
124
|
+
child = null;
|
|
125
|
+
initialized = false;
|
|
126
|
+
version = null;
|
|
127
|
+
starting = null;
|
|
128
|
+
buffer = "";
|
|
129
|
+
const error = new Error(reason);
|
|
130
|
+
rejectAll(error);
|
|
131
|
+
for (const listener of exitListeners)
|
|
132
|
+
listener(error);
|
|
133
|
+
};
|
|
134
|
+
process.on("exit", (code) => onGone(`${prefix} 已退出(code ${code ?? "?"})`));
|
|
135
|
+
process.on("error", (error) => onGone(`${prefix} 启动失败:${error.message}`));
|
|
136
|
+
};
|
|
137
|
+
const requestRaw = (method, params, timeoutMs = baseTimeout) => {
|
|
138
|
+
const id = ++seq;
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const timer = setTimeout(() => {
|
|
141
|
+
pending.delete(id);
|
|
142
|
+
reject(new Error(`${prefix} 请求超时:${method}`));
|
|
143
|
+
}, timeoutMs);
|
|
144
|
+
pending.set(id, { method, resolve: (value) => resolve(value), reject, timer });
|
|
145
|
+
try {
|
|
146
|
+
send({ id, method, params: params ?? {} });
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
pending.delete(id);
|
|
150
|
+
clearTimeout(timer);
|
|
151
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
};
|
|
155
|
+
const ensureStarted = async () => {
|
|
156
|
+
if (initialized && child)
|
|
157
|
+
return;
|
|
158
|
+
if (starting)
|
|
159
|
+
return starting;
|
|
160
|
+
starting = (async () => {
|
|
161
|
+
if (!child) {
|
|
162
|
+
const env = { ...process.env };
|
|
163
|
+
for (const [key, value] of Object.entries(options.env ?? {})) {
|
|
164
|
+
if (value === undefined)
|
|
165
|
+
delete env[key];
|
|
166
|
+
else
|
|
167
|
+
env[key] = value;
|
|
168
|
+
}
|
|
169
|
+
const next = spawn(options.bin, options.args, { env, stdio: ["pipe", "pipe", "pipe"] });
|
|
170
|
+
child = next;
|
|
171
|
+
wire(next);
|
|
172
|
+
}
|
|
173
|
+
const result = await options.handshake((method, params) => requestRaw(method, params));
|
|
174
|
+
version = result.version ?? null;
|
|
175
|
+
initialized = true;
|
|
176
|
+
})();
|
|
177
|
+
try {
|
|
178
|
+
await starting;
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
const process = child;
|
|
182
|
+
child = null;
|
|
183
|
+
initialized = false;
|
|
184
|
+
version = null;
|
|
185
|
+
process?.kill("SIGTERM");
|
|
186
|
+
throw error instanceof Error ? error : new Error(String(error));
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
starting = null;
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
return {
|
|
193
|
+
async request(method, params, requestOptions) {
|
|
194
|
+
await ensureStarted();
|
|
195
|
+
return requestRaw(method, params, requestOptions?.timeoutMs);
|
|
196
|
+
},
|
|
197
|
+
notify: (method, params) => {
|
|
198
|
+
try {
|
|
199
|
+
// 通知也要求「已连接」:没连上就没人收,静默丢掉即可
|
|
200
|
+
if (!child)
|
|
201
|
+
return;
|
|
202
|
+
send({ method, params: params ?? {} });
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
options.log?.(`${prefix}: 通知发送失败(${method}):${error instanceof Error ? error.message : String(error)}`);
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
respond,
|
|
209
|
+
respondError: (id, code, message) => {
|
|
210
|
+
try {
|
|
211
|
+
send({ id, error: { code, message } });
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
/* 进程没了就算了 */
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
onNotification: (listener) => {
|
|
218
|
+
notificationListeners.add(listener);
|
|
219
|
+
return () => notificationListeners.delete(listener);
|
|
220
|
+
},
|
|
221
|
+
onServerRequest: (listener) => {
|
|
222
|
+
serverRequestListeners.add(listener);
|
|
223
|
+
return () => serverRequestListeners.delete(listener);
|
|
224
|
+
},
|
|
225
|
+
onExit: (listener) => {
|
|
226
|
+
exitListeners.add(listener);
|
|
227
|
+
return () => exitListeners.delete(listener);
|
|
228
|
+
},
|
|
229
|
+
ensureStarted,
|
|
230
|
+
version: () => version,
|
|
231
|
+
isReady: () => initialized && child !== null,
|
|
232
|
+
close: () => {
|
|
233
|
+
const process = child;
|
|
234
|
+
child = null;
|
|
235
|
+
initialized = false;
|
|
236
|
+
version = null;
|
|
237
|
+
rejectAll(new Error(`${prefix} 已关闭`));
|
|
238
|
+
process?.kill("SIGTERM");
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 让「刚加进去的自定义 provider / 模型」立刻对 pi 生效。
|
|
3
|
+
*
|
|
4
|
+
* 为什么需要这一层(实测出来的):
|
|
5
|
+
* - pi 的模型注册表是**进程内缓存**;
|
|
6
|
+
* - `ModelRuntime.refresh()` 只重新拉「动态 provider 的远端目录」,
|
|
7
|
+
* 不会重新读 `models.json` 里的 provider 定义;
|
|
8
|
+
* - 于是「界面上加了 abc,但模型选择器里没有」—— 因为活着的会话里根本没有这个 provider。
|
|
9
|
+
*
|
|
10
|
+
* 所以除了重建预热会话,还要对**每个活着的会话**调 `registerProvider()` 把配置推过去。
|
|
11
|
+
* 这些都是 pi 的公开 API(registerProvider / unregisterProvider)。
|
|
12
|
+
*/
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { setPriceIndex } from "../util/pricing-index.js";
|
|
16
|
+
async function readJson(path, fallback) {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return fallback;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** 把 pi 的两份配置读成「可直接 registerProvider 的形状」 */
|
|
25
|
+
export async function readProviderConfigs(agentDir) {
|
|
26
|
+
const catalog = await readJson(join(agentDir, "models.json"), {});
|
|
27
|
+
const store = await readJson(join(agentDir, "models-store.json"), {});
|
|
28
|
+
const auth = await readJson(join(agentDir, "auth.json"), {});
|
|
29
|
+
const configs = [];
|
|
30
|
+
for (const [id, entry] of Object.entries(catalog.providers ?? {})) {
|
|
31
|
+
configs.push({
|
|
32
|
+
id,
|
|
33
|
+
...(typeof entry.api === "string" ? { api: entry.api } : {}),
|
|
34
|
+
...(typeof entry.baseUrl === "string" ? { baseUrl: entry.baseUrl } : {}),
|
|
35
|
+
...(typeof entry.apiKey === "string" ? { apiKey: entry.apiKey } : {}),
|
|
36
|
+
...(entry.headers ? { headers: entry.headers } : {}),
|
|
37
|
+
models: entry.models ?? [],
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
for (const [id, entry] of Object.entries(store)) {
|
|
41
|
+
const models = Array.isArray(entry.models) ? entry.models : [];
|
|
42
|
+
const template = models.find((model) => model.api && model.baseUrl);
|
|
43
|
+
if (!template)
|
|
44
|
+
continue;
|
|
45
|
+
configs.push({
|
|
46
|
+
id,
|
|
47
|
+
api: template.api,
|
|
48
|
+
baseUrl: template.baseUrl,
|
|
49
|
+
...(auth[id]?.key ? { apiKey: auth[id].key } : {}),
|
|
50
|
+
...(template.headers ? { headers: template.headers } : {}),
|
|
51
|
+
models,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return configs;
|
|
55
|
+
}
|
|
56
|
+
/** pi 认的模型字段(缺省给安全值,避免 provider 被拒) */
|
|
57
|
+
function toProviderModel(model) {
|
|
58
|
+
return {
|
|
59
|
+
id: model.id,
|
|
60
|
+
name: model.name ?? model.id,
|
|
61
|
+
reasoning: model.reasoning === true,
|
|
62
|
+
input: Array.isArray(model.input) ? model.input : ["text"],
|
|
63
|
+
contextWindow: model.contextWindow ?? 128_000,
|
|
64
|
+
maxTokens: model.maxTokens ?? 8_192,
|
|
65
|
+
cost: model.cost ?? { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
66
|
+
...(model.headers ? { headers: model.headers } : {}),
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/** 我们通过 registerProvider 推给运行时的 provider(用来在改名/删除后注销旧的) */
|
|
70
|
+
const pushed = new Set();
|
|
71
|
+
export async function reloadModelRegistry(sessions, prewarmId, prewarm, agentDir) {
|
|
72
|
+
const warm = sessions.get(prewarmId);
|
|
73
|
+
if (warm) {
|
|
74
|
+
try {
|
|
75
|
+
warm.unsubscribe();
|
|
76
|
+
warm.session.dispose();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// 预热会话无关紧要,dispose 失败忽略
|
|
80
|
+
}
|
|
81
|
+
sessions.delete(prewarmId);
|
|
82
|
+
}
|
|
83
|
+
await prewarm();
|
|
84
|
+
const configs = await readProviderConfigs(agentDir);
|
|
85
|
+
// 分时价格索引(ADR-0026 D 期):反正这两份配置刚读过,顺手把「时段 → 价格」也刷一遍,
|
|
86
|
+
// 不然用户刚改完价格,下一轮的消息花费还是按旧价算的。
|
|
87
|
+
setPriceIndex(configs.map((config) => ({ provider: config.id, models: config.models })));
|
|
88
|
+
for (const handle of sessions.values()) {
|
|
89
|
+
const runtime = handle.session.modelRuntime;
|
|
90
|
+
await runtime?.refresh?.({ allowNetwork: false }).catch(() => undefined);
|
|
91
|
+
// 配置里已经没有的(改名 / 删除):从运行时注销,否则旧名字会一直挂在模型表里。
|
|
92
|
+
// 先收集再删:在遍历 `pushed` 时 delete 会让后续会话跳过这些 id(旧的注销不干净)。
|
|
93
|
+
const stale = [...pushed].filter((id) => !configs.some((config) => config.id === id));
|
|
94
|
+
for (const id of stale)
|
|
95
|
+
runtime?.unregisterProvider?.(id);
|
|
96
|
+
if (sessions.size > 0)
|
|
97
|
+
for (const id of stale)
|
|
98
|
+
pushed.delete(id);
|
|
99
|
+
for (const config of configs) {
|
|
100
|
+
try {
|
|
101
|
+
runtime?.registerProvider?.(config.id, {
|
|
102
|
+
...(config.api ? { api: config.api } : {}),
|
|
103
|
+
...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
|
104
|
+
...(config.apiKey ? { apiKey: config.apiKey } : {}),
|
|
105
|
+
...(config.headers ? { headers: config.headers } : {}),
|
|
106
|
+
models: config.models.map(toProviderModel),
|
|
107
|
+
});
|
|
108
|
+
pushed.add(config.id);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// 单个 provider 注册失败不该拖垮整次刷新(它只是不出现在列表里)
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK 会话事件 → 端口事件的映射(ADR-0003 的端口实现里最容易出错的一层)。
|
|
3
|
+
*
|
|
4
|
+
* 从 `pi-sdk.ts` 拆出(400 行护栏):这是**纯映射**,与「怎么建会话、怎么订阅」无关,
|
|
5
|
+
* 所以可以脱离 SDK 用合成事件直接测(单测覆盖整张映射表,不烧 token)。
|
|
6
|
+
*/
|
|
7
|
+
import { parseUsage } from "../util/usage.js";
|
|
8
|
+
function asRecord(value) {
|
|
9
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
10
|
+
}
|
|
11
|
+
/** 把工具结果里的文本抽出来(content 是 {type:"text",text} 数组) */
|
|
12
|
+
export function resultText(result) {
|
|
13
|
+
const content = asRecord(result).content;
|
|
14
|
+
if (!Array.isArray(content))
|
|
15
|
+
return "";
|
|
16
|
+
return content
|
|
17
|
+
.map((part) => {
|
|
18
|
+
const record = asRecord(part);
|
|
19
|
+
return typeof record.text === "string" ? record.text : "";
|
|
20
|
+
})
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.join("\n");
|
|
23
|
+
}
|
|
24
|
+
/** 把 bash 工具结果的退出码抠出来(不同工具 details 形状不同,取不到就不给) */
|
|
25
|
+
export function resultExitCode(result) {
|
|
26
|
+
const details = asRecord(asRecord(result).details);
|
|
27
|
+
const value = details.exitCode ?? asRecord(asRecord(result).structuredContent).exitCode;
|
|
28
|
+
return typeof value === "number" ? value : undefined;
|
|
29
|
+
}
|
|
30
|
+
/** 一行预览的长度:超过就截(参数再长,抬头也只有一行) */
|
|
31
|
+
const SUMMARY_CHARS = 200;
|
|
32
|
+
/** 完整参数的上限:不能让一条快照因参数膨胀(正常命令远小于这个数) */
|
|
33
|
+
export const FULL_ARGS_CHARS = 4000;
|
|
34
|
+
/** 把任意参数变成文本(字符串原样返回:有的引擎直接给命令串,不是 JSON) */
|
|
35
|
+
function argsText(args) {
|
|
36
|
+
if (typeof args === "string")
|
|
37
|
+
return args;
|
|
38
|
+
try {
|
|
39
|
+
return JSON.stringify(args ?? {});
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return "(无法序列化的参数)";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** 参数摘要(给界面一行预览与审批用) */
|
|
46
|
+
export function summarizeArgs(args) {
|
|
47
|
+
const text = argsText(args);
|
|
48
|
+
return text.length > SUMMARY_CHARS ? `${text.slice(0, SUMMARY_CHARS)}…` : text;
|
|
49
|
+
}
|
|
50
|
+
/** 完整参数(给界面「展开了看全 + 复制」);跟摘要一样短时调用方可以丢掉它 */
|
|
51
|
+
export function fullArgs(args) {
|
|
52
|
+
const text = argsText(args);
|
|
53
|
+
return text.length > FULL_ARGS_CHARS ? `${text.slice(0, FULL_ARGS_CHARS)}…` : text;
|
|
54
|
+
}
|
|
55
|
+
/** 同一类失败的统一出口:能把原因带给用户就带,拿不到给一句可读的兜底 */
|
|
56
|
+
function providerError(raw) {
|
|
57
|
+
const text = typeof raw === "string" && raw.trim() !== ""
|
|
58
|
+
? raw.trim()
|
|
59
|
+
: typeof asRecord(raw).message === "string" && asRecord(raw).message !== ""
|
|
60
|
+
? String(asRecord(raw).message)
|
|
61
|
+
: "模型调用失败(provider 没有给出原因)";
|
|
62
|
+
return { type: "error", code: "engine.provider_error", message: text };
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 纯函数:把 SDK 会话事件翻译成端口事件。返回 null 表示这个事件不影响 UI。
|
|
66
|
+
* (单测用合成事件直接覆盖这张映射表 —— 不需要真的连模型。)
|
|
67
|
+
*/
|
|
68
|
+
export function mapSessionEvent(event) {
|
|
69
|
+
const record = asRecord(event);
|
|
70
|
+
const type = record.type;
|
|
71
|
+
if (type === "message_update") {
|
|
72
|
+
const message = asRecord(record.message);
|
|
73
|
+
const assistantEvent = asRecord(record.assistantMessageEvent);
|
|
74
|
+
// ⚠️ 正文 / 思考的增量**必须优先**:SDK 的 message_update 里一旦带上 usage,
|
|
75
|
+
// 之前那种「先看 usage 就 return」会把后面的每一个正文增量全吞掉 ——
|
|
76
|
+
// 表现就是「只调工具、不回复」。所以先在前面处理 delta,usage 只在非 delta 更新上取。
|
|
77
|
+
if (assistantEvent.type === "text_delta" &&
|
|
78
|
+
typeof assistantEvent.delta === "string" &&
|
|
79
|
+
assistantEvent.delta !== "") {
|
|
80
|
+
return { type: "text_delta", text: assistantEvent.delta };
|
|
81
|
+
}
|
|
82
|
+
// 模型的思考过程:单独一类事件,界面上折叠展示(不是正文)
|
|
83
|
+
if (assistantEvent.type === "thinking_delta" &&
|
|
84
|
+
typeof assistantEvent.delta === "string" &&
|
|
85
|
+
assistantEvent.delta !== "") {
|
|
86
|
+
return { type: "thinking_delta", text: assistantEvent.delta };
|
|
87
|
+
}
|
|
88
|
+
// 思考 / 正文之外的另一种增量:provider 直接报错(有的 provider 走这条,不走 message_end)
|
|
89
|
+
if (assistantEvent.type === "error")
|
|
90
|
+
return providerError(assistantEvent.error ?? assistantEvent.message);
|
|
91
|
+
// 用量:SDK 的 message 上带 usage(各 provider 字段名不一样,统一解析)。
|
|
92
|
+
// 这是**过程值**(final:false):宿主只拿 message_end 那份求和,不能两边都算。
|
|
93
|
+
const usage = parseUsage(message.usage);
|
|
94
|
+
if (usage)
|
|
95
|
+
return { type: "usage", ...usage, final: false };
|
|
96
|
+
// 模型:**放最后**。有的 provider 每一帧 message 上都带 provider/model,
|
|
97
|
+
// 一旦先返回它,后面的正文增量就全被吞了 —— 表现就是「助手不回复,只有用量」。
|
|
98
|
+
if (typeof message.model === "string" && message.model !== "") {
|
|
99
|
+
const provider = typeof message.provider === "string" ? message.provider : "";
|
|
100
|
+
return { type: "model", model: provider ? `${provider}/${message.model}` : message.model };
|
|
101
|
+
}
|
|
102
|
+
return null; // tool_use 等:v1 不展示
|
|
103
|
+
}
|
|
104
|
+
// 一条消息开始:这一帧上带着 provider/model(拿它当「当前模型」的来源之一)
|
|
105
|
+
if (type === "message_start") {
|
|
106
|
+
const message = asRecord(record.message);
|
|
107
|
+
if (typeof message.model === "string" && message.model !== "") {
|
|
108
|
+
const provider = typeof message.provider === "string" ? message.provider : "";
|
|
109
|
+
return { type: "model", model: provider ? `${provider}/${message.model}` : message.model };
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
// 一条消息结束:**最终**用量在这里(output 才统计全),同一条 message_update 上的是过程值
|
|
114
|
+
if (type === "message_end") {
|
|
115
|
+
const message = asRecord(record.message);
|
|
116
|
+
// provider / 鉴权 / 网络的失败**不会抛异常**:SDK 会合成一条 stopReason=error 的消息
|
|
117
|
+
// (带 errorMessage)然后正常 resolve。不在这里翻译成 error 事件,界面就是
|
|
118
|
+
// 「空回复 + 没有任何报错 + 回合算完成」—— 用户根本不知道出了什么事。
|
|
119
|
+
if (message.stopReason === "error")
|
|
120
|
+
return providerError(message.errorMessage);
|
|
121
|
+
// 一次模型调用的**最终**用量:宿主把一轮里的这些加起来 = 本轮总量(含工具轮次)
|
|
122
|
+
const usage = parseUsage(message.usage);
|
|
123
|
+
return usage ? { type: "usage", ...usage, final: true } : null;
|
|
124
|
+
}
|
|
125
|
+
if (type === "tool_execution_start") {
|
|
126
|
+
const toolCallId = String(record.toolCallId ?? "");
|
|
127
|
+
const name = String(record.toolName ?? "");
|
|
128
|
+
return {
|
|
129
|
+
type: "tool_start",
|
|
130
|
+
toolCallId,
|
|
131
|
+
name,
|
|
132
|
+
argsSummary: summarizeArgs(record.args),
|
|
133
|
+
argsFull: fullArgs(record.args),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
if (type === "tool_execution_end") {
|
|
137
|
+
const toolCallId = String(record.toolCallId ?? "");
|
|
138
|
+
const exitCode = resultExitCode(record.result);
|
|
139
|
+
return {
|
|
140
|
+
type: "tool_end",
|
|
141
|
+
toolCallId,
|
|
142
|
+
status: record.isError === true ? "failed" : "ok",
|
|
143
|
+
output: resultText(record.result),
|
|
144
|
+
...(exitCode === undefined ? {} : { exitCode }),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 改会话显示名:往会话文件追加一条 `session_info`(pi 自己的「会话名」条目)。
|
|
3
|
+
*
|
|
4
|
+
* 已打开的会话直接用它的 sessionManager(同一个文件句柄);没打开的临时 open 一次再追加。
|
|
5
|
+
* 单独成文件:ADR-0003 规定只有 `server/engine` 能碰 SDK,而 pi-sdk.ts 已贴近行数上限。
|
|
6
|
+
*/
|
|
7
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
8
|
+
export async function renameSessionIn(sessions, sessionPath, name, sessionDir) {
|
|
9
|
+
const opened = [...sessions.values()].find((handle) => handle.sessionManager.getSessionFile() === sessionPath);
|
|
10
|
+
const manager = opened
|
|
11
|
+
? opened.sessionManager
|
|
12
|
+
: SessionManager.open(sessionPath, sessionDir);
|
|
13
|
+
manager.appendSessionInfo(name);
|
|
14
|
+
}
|