oh-my-agent-web 0.3.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 -455
- 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 -782
- 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-5LJ5fRcO.js +0 -42
- package/web/dist/assets/index-QiDfPKFo.css +0 -1
- package/web/dist/assets/markdown-CYXe6JZx.js +0 -55
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex app-server 的**纯映射**(不碰网络/子进程,单测直接覆盖)。
|
|
3
|
+
*
|
|
4
|
+
* 三件事:
|
|
5
|
+
* 1. thread items → 工作台的 `UiMessage`(对话区与历史都靠它);
|
|
6
|
+
* 2. model catalog → 模型下拉的条目 + 思考档位;
|
|
7
|
+
* 3. 审批请求 → 工作台的 ApprovalRequest(审批弹窗的文字与作用域)。
|
|
8
|
+
*
|
|
9
|
+
* 为什么单独一个文件:这些规则又多又碎(item 有 18 种),混进引擎里会很快破 400 行,
|
|
10
|
+
* 也没法测。引擎只负责「收通知 → 按 threadId 分发」。
|
|
11
|
+
*/
|
|
12
|
+
import { TOOL_EXCERPT_CHARS } from "../protocol.js";
|
|
13
|
+
const EXCERPT = TOOL_EXCERPT_CHARS;
|
|
14
|
+
function clip(text) {
|
|
15
|
+
if (text.length <= EXCERPT)
|
|
16
|
+
return { outputExcerpt: text, truncated: false };
|
|
17
|
+
return { outputExcerpt: text.slice(0, EXCERPT), truncated: true };
|
|
18
|
+
}
|
|
19
|
+
function textOf(content) {
|
|
20
|
+
if (!Array.isArray(content))
|
|
21
|
+
return "";
|
|
22
|
+
return content
|
|
23
|
+
.map((part) => (typeof part === "string" ? part : typeof part.text === "string" ? part.text : ""))
|
|
24
|
+
.join("");
|
|
25
|
+
}
|
|
26
|
+
/** codex 的工具状态 → 工作台的三态 */
|
|
27
|
+
function toolStatus(status) {
|
|
28
|
+
if (status === undefined || status === "inProgress" || status === "running")
|
|
29
|
+
return "running";
|
|
30
|
+
if (status === "completed" || status === "ok")
|
|
31
|
+
return "ok";
|
|
32
|
+
return "failed";
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 一轮 turn 的 items → 界面消息。
|
|
36
|
+
*
|
|
37
|
+
* 规则:
|
|
38
|
+
* - `userMessage` 开一条用户消息;`agentMessage` 开一条助手消息;
|
|
39
|
+
* - `reasoning` / `plan` 接到当前助手消息上(折叠块);
|
|
40
|
+
* - 工具类(commandExecution / fileChange / mcpToolCall)也接在当前助手消息上,
|
|
41
|
+
* 没有助手消息就先开一条(否则工具卡会丢)。
|
|
42
|
+
*/
|
|
43
|
+
export function itemsToUiMessages(items, ts) {
|
|
44
|
+
const messages = [];
|
|
45
|
+
let assistant = null;
|
|
46
|
+
const ensureAssistant = () => {
|
|
47
|
+
if (assistant)
|
|
48
|
+
return assistant;
|
|
49
|
+
assistant = { id: `codex-asst-${ts}-${messages.length}`, role: "assistant", ts, blocks: [] };
|
|
50
|
+
messages.push(assistant);
|
|
51
|
+
return assistant;
|
|
52
|
+
};
|
|
53
|
+
for (const item of items) {
|
|
54
|
+
const type = item.type;
|
|
55
|
+
if (type === "userMessage") {
|
|
56
|
+
const text = textOf(item.content);
|
|
57
|
+
if (text.trim() === "")
|
|
58
|
+
continue;
|
|
59
|
+
messages.push({ id: item.id ?? `codex-user-${ts}-${messages.length}`, role: "user", ts, blocks: [{ kind: "text", text }] });
|
|
60
|
+
assistant = null;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (type === "agentMessage") {
|
|
64
|
+
const text = item.text ?? "";
|
|
65
|
+
if (text === "")
|
|
66
|
+
continue;
|
|
67
|
+
// 连续的 agentMessage(分段回复)接到同一条助手消息上
|
|
68
|
+
const target = ensureAssistant();
|
|
69
|
+
target.blocks = [...target.blocks, { kind: "text", text }];
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (type === "reasoning" || type === "plan") {
|
|
73
|
+
const text = type === "plan" ? (item.text ?? "") : [...(item.summary ?? []), ...(Array.isArray(item.content) ? item.content.filter((c) => typeof c === "string") : [])].join("\n");
|
|
74
|
+
if (text.trim() === "")
|
|
75
|
+
continue;
|
|
76
|
+
const target = ensureAssistant();
|
|
77
|
+
target.blocks = [...target.blocks, { kind: "thinking", text }];
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (type === "commandExecution") {
|
|
81
|
+
const target = ensureAssistant();
|
|
82
|
+
const output = clip(item.aggregatedOutput ?? "");
|
|
83
|
+
target.blocks = [
|
|
84
|
+
...target.blocks,
|
|
85
|
+
{
|
|
86
|
+
kind: "tool",
|
|
87
|
+
toolCallId: item.id ?? `codex-exec-${ts}`,
|
|
88
|
+
name: "shell",
|
|
89
|
+
argsSummary: item.command ?? "",
|
|
90
|
+
argsFull: item.command ?? "",
|
|
91
|
+
status: toolStatus(item.status),
|
|
92
|
+
outputExcerpt: output.outputExcerpt,
|
|
93
|
+
truncated: output.truncated,
|
|
94
|
+
...(typeof item.exitCode === "number" ? { exitCode: item.exitCode } : {}),
|
|
95
|
+
startedAt: ts,
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (type === "fileChange") {
|
|
101
|
+
const target = ensureAssistant();
|
|
102
|
+
const paths = (item.changes ?? []).map((change) => change.path ?? "").filter(Boolean);
|
|
103
|
+
target.blocks = [
|
|
104
|
+
...target.blocks,
|
|
105
|
+
{
|
|
106
|
+
kind: "tool",
|
|
107
|
+
toolCallId: item.id ?? `codex-patch-${ts}`,
|
|
108
|
+
name: "apply_patch",
|
|
109
|
+
argsSummary: paths.length > 0 ? paths.join(", ") : "改动文件",
|
|
110
|
+
status: toolStatus(item.status),
|
|
111
|
+
outputExcerpt: "",
|
|
112
|
+
truncated: false,
|
|
113
|
+
startedAt: ts,
|
|
114
|
+
},
|
|
115
|
+
];
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (type === "mcpToolCall" || type === "dynamicToolCall") {
|
|
119
|
+
const target = ensureAssistant();
|
|
120
|
+
const name = item.server ? `${item.server}.${item.tool ?? "tool"}` : (item.tool ?? "tool");
|
|
121
|
+
const outputText = item.error?.message ?? (item.result === undefined ? "" : JSON.stringify(item.result));
|
|
122
|
+
const output = clip(outputText);
|
|
123
|
+
target.blocks = [
|
|
124
|
+
...target.blocks,
|
|
125
|
+
{
|
|
126
|
+
kind: "tool",
|
|
127
|
+
toolCallId: item.id ?? `codex-tool-${ts}`,
|
|
128
|
+
name,
|
|
129
|
+
argsSummary: item.arguments === undefined ? "" : JSON.stringify(item.arguments).slice(0, 200),
|
|
130
|
+
argsFull: item.arguments === undefined ? "" : JSON.stringify(item.arguments),
|
|
131
|
+
status: toolStatus(item.status),
|
|
132
|
+
outputExcerpt: output.outputExcerpt,
|
|
133
|
+
truncated: output.truncated,
|
|
134
|
+
startedAt: ts,
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (type === "imageView" || type === "imageGeneration" || type === "webSearch") {
|
|
140
|
+
const target = ensureAssistant();
|
|
141
|
+
const text = type === "webSearch" ? "(联网搜索)" : type === "imageView" ? `(查看图片:${item.path ?? ""})` : "(生成图片)";
|
|
142
|
+
target.blocks = [...target.blocks, { kind: "text", text }];
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return messages;
|
|
146
|
+
}
|
|
147
|
+
/** 一轮 turn 的 items 拍平成转录(`readTranscript` 用) */
|
|
148
|
+
export function itemsToTranscript(items, ts) {
|
|
149
|
+
return itemsToUiMessages(items, ts)
|
|
150
|
+
.filter((message) => message.blocks.some((block) => block.kind === "text"))
|
|
151
|
+
.map((message) => ({
|
|
152
|
+
role: message.role,
|
|
153
|
+
text: message.blocks
|
|
154
|
+
.map((block) => (block.kind === "text" ? block.text : ""))
|
|
155
|
+
.join("")
|
|
156
|
+
.trim(),
|
|
157
|
+
ts,
|
|
158
|
+
}))
|
|
159
|
+
.filter((message) => message.text !== "");
|
|
160
|
+
}
|
|
161
|
+
/** turn 的起始时间(秒 → 毫秒);没有就用兜底值 */
|
|
162
|
+
export function turnTs(turn, fallback) {
|
|
163
|
+
return typeof turn.startedAt === "number" ? turn.startedAt * 1000 : fallback;
|
|
164
|
+
}
|
|
165
|
+
/** 模型目录 → 模型下拉(隐藏模型不显示;id 用 model slug,切模型直接回传它) */
|
|
166
|
+
export function mapModels(models) {
|
|
167
|
+
return models
|
|
168
|
+
.filter((model) => model.hidden !== true)
|
|
169
|
+
.map((model) => ({
|
|
170
|
+
id: model.model || model.id,
|
|
171
|
+
label: model.displayName && model.displayName !== model.model ? `${model.displayName}` : model.model || model.id,
|
|
172
|
+
...(model.isDefault ? { current: true } : {}),
|
|
173
|
+
}));
|
|
174
|
+
}
|
|
175
|
+
/** 某个模型支持的思考档位(codex 的 reasoning efforts) */
|
|
176
|
+
export function thinkingLevelsOf(models, modelId) {
|
|
177
|
+
const model = models.find((item) => (item.model || item.id) === modelId) ?? models.find((item) => item.id === modelId);
|
|
178
|
+
if (!model?.supportedReasoningEfforts)
|
|
179
|
+
return [];
|
|
180
|
+
return model.supportedReasoningEfforts
|
|
181
|
+
.map((effort) => effort.reasoningEffort)
|
|
182
|
+
.filter((effort) => typeof effort === "string" && effort !== "");
|
|
183
|
+
}
|
|
184
|
+
/** 命令行 → 「这类」的作用域键(与 pi 侧的 bash 规则同构,`总是允许这类` 才有意义) */
|
|
185
|
+
export function commandScope(command) {
|
|
186
|
+
const first = command.trim().split(/\s+/)[0] ?? "";
|
|
187
|
+
const clean = first.replace(/^.*\//, "");
|
|
188
|
+
return { scopeKey: `bash|${clean}`, scopeHint: clean === "" ? command.trim().slice(0, 60) : clean };
|
|
189
|
+
}
|
|
190
|
+
/** 命令执行审批 → 工作台的审批请求 */
|
|
191
|
+
export function commandApprovalRequest(params) {
|
|
192
|
+
const command = typeof params.command === "string" ? params.command : "";
|
|
193
|
+
const scope = commandScope(command);
|
|
194
|
+
const reason = typeof params.reason === "string" && params.reason !== "" ? params.reason : null;
|
|
195
|
+
return {
|
|
196
|
+
toolName: "shell",
|
|
197
|
+
argsSummary: command === "" ? "(未提供命令)" : command.slice(0, 300),
|
|
198
|
+
scopeKey: scope.scopeKey,
|
|
199
|
+
scopeHint: scope.scopeHint,
|
|
200
|
+
riskNote: reason ?? "codex 请求执行命令;是否放行由你决定",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
/** 文件改动审批 → 工作台的审批请求 */
|
|
204
|
+
export function fileChangeApprovalRequest(params) {
|
|
205
|
+
const root = typeof params.grantRoot === "string" ? params.grantRoot : "";
|
|
206
|
+
const reason = typeof params.reason === "string" && params.reason !== "" ? params.reason : null;
|
|
207
|
+
return {
|
|
208
|
+
toolName: "apply_patch",
|
|
209
|
+
argsSummary: root === "" ? "改动工作区文件" : `允许写入 ${root}`,
|
|
210
|
+
scopeKey: `write|${root}`,
|
|
211
|
+
scopeHint: root === "" ? "工作区文件" : root,
|
|
212
|
+
riskNote: reason ?? "codex 想改动工作区文件,确认后才继续",
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
/** 审批决定 → codex 的 decision 字符串 */
|
|
216
|
+
export function approvalDecision(decision, kind) {
|
|
217
|
+
if (decision === "deny")
|
|
218
|
+
return "decline";
|
|
219
|
+
if (decision === "always")
|
|
220
|
+
return "acceptForSession";
|
|
221
|
+
return kind === "command" ? "accept" : "accept";
|
|
222
|
+
}
|
|
223
|
+
/** 模型下拉里显示的当前模型(codex 的 provider 只有一个,给个稳定标签) */
|
|
224
|
+
export function modelDisplay(provider, model) {
|
|
225
|
+
if (!model)
|
|
226
|
+
return null;
|
|
227
|
+
return `${provider ?? "codex"}/${model}`;
|
|
228
|
+
}
|
|
229
|
+
/** 给 `ModelOption` 用:把引擎模型表转成快照里的形状(保留顺序,去重) */
|
|
230
|
+
export function toModelOptions(models) {
|
|
231
|
+
const seen = new Set();
|
|
232
|
+
const options = [];
|
|
233
|
+
for (const model of models) {
|
|
234
|
+
if (seen.has(model.id))
|
|
235
|
+
continue;
|
|
236
|
+
seen.add(model.id);
|
|
237
|
+
options.push({ id: model.id, label: model.label });
|
|
238
|
+
}
|
|
239
|
+
return options;
|
|
240
|
+
}
|
|
241
|
+
export function toolStartOf(item) {
|
|
242
|
+
const id = item.id ?? `codex-tool-${item.type}`;
|
|
243
|
+
if (item.type === "commandExecution") {
|
|
244
|
+
return {
|
|
245
|
+
type: "tool_start",
|
|
246
|
+
toolCallId: id,
|
|
247
|
+
name: "shell",
|
|
248
|
+
argsSummary: item.command ?? "",
|
|
249
|
+
argsFull: item.command ?? "",
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
if (item.type === "fileChange") {
|
|
253
|
+
return { type: "tool_start", toolCallId: id, name: "apply_patch", argsSummary: "改动工作区文件" };
|
|
254
|
+
}
|
|
255
|
+
if (item.type === "mcpToolCall" || item.type === "dynamicToolCall") {
|
|
256
|
+
const name = item.server ? `${item.server}.${item.tool ?? "tool"}` : (item.tool ?? "tool");
|
|
257
|
+
return { type: "tool_start", toolCallId: id, name, argsSummary: "" };
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
export function toolEndOf(item) {
|
|
262
|
+
const id = item.id ?? `codex-tool-${item.type}`;
|
|
263
|
+
const ok = item.status === "completed" || item.status === "ok";
|
|
264
|
+
if (item.type === "commandExecution") {
|
|
265
|
+
return {
|
|
266
|
+
type: "tool_end",
|
|
267
|
+
toolCallId: id,
|
|
268
|
+
status: ok ? "ok" : "failed",
|
|
269
|
+
output: item.aggregatedOutput ?? "",
|
|
270
|
+
...(typeof item.exitCode === "number" ? { exitCode: item.exitCode } : {}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
if (item.type === "fileChange") {
|
|
274
|
+
return { type: "tool_end", toolCallId: id, status: ok ? "ok" : "failed", output: "" };
|
|
275
|
+
}
|
|
276
|
+
if (item.type === "mcpToolCall" || item.type === "dynamicToolCall") {
|
|
277
|
+
const output = item.error?.message ?? (item.result === undefined ? "" : JSON.stringify(item.result));
|
|
278
|
+
return { type: "tool_end", toolCallId: id, status: ok ? "ok" : "failed", output };
|
|
279
|
+
}
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* codex 的错误 → 人话。
|
|
284
|
+
* 最常见的一条是**写入锁**:同一个 thread 同时被 Codex 桌面端/TUI 打开时,app-server 会拒绝
|
|
285
|
+
* resume(`thread-store conflict` / `active writer`)—— 这不是 bug,是原生行为,
|
|
286
|
+
* 必须让用户知道该关哪一边,而不是只看到「切换模型失败」。
|
|
287
|
+
*/
|
|
288
|
+
export function friendlyCodexError(error) {
|
|
289
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
290
|
+
if (/active writer|thread-store conflict/i.test(message)) {
|
|
291
|
+
return "这个会话正被 Codex(桌面端/终端)打开着,写入锁被占用:先在那里关掉它,或新建一条会话";
|
|
292
|
+
}
|
|
293
|
+
if (/no rollout found/i.test(message))
|
|
294
|
+
return "这个会话还没有落盘(先发一条消息,或换一条有历史的会话)";
|
|
295
|
+
if (/not found/i.test(message))
|
|
296
|
+
return "Codex 里找不到这个会话(可能已被删除或归档)";
|
|
297
|
+
return message;
|
|
298
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex 的「项目 / 会话清单」提供者(对齐 codex 原生:项目就是 codex 的 project,会话就是 thread)。
|
|
3
|
+
*
|
|
4
|
+
* 数据全部来自 app-server:
|
|
5
|
+
* - `project/list`(原生项目:名字 + 多个 root + 原生排序 position);
|
|
6
|
+
* - `thread/list`(原生会话:名字、cwd、projectId、模型、更新时间)。
|
|
7
|
+
*
|
|
8
|
+
* 分组规则(**只读,不改 codex 的任何东西**):
|
|
9
|
+
* 1. thread.projectId 有值 → 归到那个项目;
|
|
10
|
+
* 2. 没有 projectId 但有 cwd → 归到「root 是 cwd 前缀」的项目(最长前缀胜出);
|
|
11
|
+
* 3. 都没匹配上 → 按 cwd 单独成组(不丢会话,也不硬塞进别人的项目)。
|
|
12
|
+
*
|
|
13
|
+
* 这条兜底很重要:本机实测 codex 的 threads.project_id 大量为 NULL(Desktop 只在某些路径下写入),
|
|
14
|
+
* 只按 projectId 归类会让侧栏空掉。
|
|
15
|
+
*/
|
|
16
|
+
import { basename } from "node:path";
|
|
17
|
+
import { isScratchCwd } from "./scratch.js";
|
|
18
|
+
/**
|
|
19
|
+
* 拉会话时必须显式声明这两项,否则会「看不见大部分历史」(实测 0.162/0.163):
|
|
20
|
+
* - `modelProviders: []` → 缺省时服务端只返回**当前配置的 provider** 的会话
|
|
21
|
+
* (本机 35 条非归档会话里,缺省只回了 13 条);
|
|
22
|
+
* - `sourceKinds` 全给 → 缺省只回「交互式来源」,`exec` 起的会话会被吞掉。
|
|
23
|
+
*/
|
|
24
|
+
const ALL_SOURCES = ["cli", "vscode", "exec", "appServer", "subAgent", "unknown"];
|
|
25
|
+
const cwdOf = (thread) => thread.cwd ?? "";
|
|
26
|
+
/** cwd 是否落在某个 root 里(等于或在其下;避免 `/a/bc` 冒充 `/a/b`) */
|
|
27
|
+
function underRoot(cwd, root) {
|
|
28
|
+
if (cwd === "" || root === "")
|
|
29
|
+
return false;
|
|
30
|
+
if (cwd === root)
|
|
31
|
+
return true;
|
|
32
|
+
return cwd.startsWith(root.endsWith("/") ? root : `${root}/`);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 纯分组:projects + threads → 侧栏形状(单测直接覆盖)。
|
|
36
|
+
* 会话按 updatedAt 倒序;项目之间保持 codex 的 position 顺序。
|
|
37
|
+
*/
|
|
38
|
+
export function groupThreads(projects, threads) {
|
|
39
|
+
const byProjectId = new Map(projects.map((project) => [project.id, project]));
|
|
40
|
+
const roots = projects.flatMap((project) => project.roots.map((root) => ({ projectId: project.id, path: root.path ?? "" })));
|
|
41
|
+
const groups = new Map();
|
|
42
|
+
const ensureGroup = (key, project, cwd) => {
|
|
43
|
+
const known = groups.get(key);
|
|
44
|
+
if (known)
|
|
45
|
+
return known;
|
|
46
|
+
const created = { project, cwd, sessions: [] };
|
|
47
|
+
groups.set(key, created);
|
|
48
|
+
return created;
|
|
49
|
+
};
|
|
50
|
+
// 先把所有项目建出来(没有会话的项目也要显示:原生侧栏就是这样的)
|
|
51
|
+
for (const project of projects) {
|
|
52
|
+
ensureGroup(`project:${project.id}`, project, project.roots[0]?.path ?? "");
|
|
53
|
+
}
|
|
54
|
+
for (const thread of threads) {
|
|
55
|
+
if (thread.archived === true)
|
|
56
|
+
continue;
|
|
57
|
+
const cwd = cwdOf(thread);
|
|
58
|
+
const explicit = thread.projectId ? byProjectId.get(thread.projectId) : undefined;
|
|
59
|
+
let project = explicit ?? null;
|
|
60
|
+
if (!project && cwd !== "") {
|
|
61
|
+
// 最长前缀胜出:嵌套 root(monorepo)时归到最具体的那个项目
|
|
62
|
+
const match = roots
|
|
63
|
+
.filter((root) => underRoot(cwd, root.path))
|
|
64
|
+
.sort((left, right) => right.path.length - left.path.length)[0];
|
|
65
|
+
if (match)
|
|
66
|
+
project = byProjectId.get(match.projectId) ?? null;
|
|
67
|
+
}
|
|
68
|
+
// 临时目录里的随手会话(`codex exec` 的一次性跑)不进侧栏;当前正在聊的那条
|
|
69
|
+
// 仍然看得到 —— 侧栏会把 state.conversations 合进来(见 Sidebar)。
|
|
70
|
+
if (!project && isScratchCwd(cwd))
|
|
71
|
+
continue;
|
|
72
|
+
const key = project ? `project:${project.id}` : `cwd:${cwd || "unknown"}`;
|
|
73
|
+
const group = ensureGroup(key, project, project?.roots[0]?.path ?? cwd);
|
|
74
|
+
group.sessions.push({
|
|
75
|
+
id: thread.id,
|
|
76
|
+
engineSessionId: thread.id,
|
|
77
|
+
title: (thread.name ?? "").trim() || (thread.preview ?? "").trim() || "(未命名会话)",
|
|
78
|
+
startedAt: typeof thread.createdAt === "number" ? thread.createdAt * 1000 : 0,
|
|
79
|
+
updatedAt: typeof thread.updatedAt === "number" ? thread.updatedAt * 1000 : 0,
|
|
80
|
+
cwd: cwd || group.cwd,
|
|
81
|
+
model: thread.model ?? null,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return [...groups.values()]
|
|
85
|
+
.map((group) => ({
|
|
86
|
+
cwd: group.cwd,
|
|
87
|
+
name: group.project?.name?.trim() || basename(group.cwd) || "未分项目",
|
|
88
|
+
sessions: [...group.sessions].sort((left, right) => right.updatedAt - left.updatedAt),
|
|
89
|
+
}))
|
|
90
|
+
// 有会话的排前面;同组内按最近更新时间
|
|
91
|
+
.sort((left, right) => {
|
|
92
|
+
const leftAt = left.sessions[0]?.updatedAt ?? 0;
|
|
93
|
+
const rightAt = right.sessions[0]?.updatedAt ?? 0;
|
|
94
|
+
return rightAt - leftAt;
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
/** 分页拉全部会话(跟随 nextCursor;上限 5 页,避免超大库把首屏拖死) */
|
|
98
|
+
export async function listAllThreads(client, pageSize = 200) {
|
|
99
|
+
const rows = [];
|
|
100
|
+
let cursor = null;
|
|
101
|
+
for (let page = 0; page < 5; page += 1) {
|
|
102
|
+
const result = await client.request("thread/list", {
|
|
103
|
+
limit: pageSize,
|
|
104
|
+
modelProviders: [],
|
|
105
|
+
sourceKinds: [...ALL_SOURCES],
|
|
106
|
+
...(cursor ? { cursor } : {}),
|
|
107
|
+
});
|
|
108
|
+
rows.push(...(result?.data ?? []));
|
|
109
|
+
cursor = result?.nextCursor ?? null;
|
|
110
|
+
if (!cursor)
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
return rows;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* 保证某个目录在 codex 里有一个原生 project(新建会话时用)。
|
|
117
|
+
*
|
|
118
|
+
* - 已有覆盖它的项目(root 相同或是其前缀)→ 直接用(取最长前缀,嵌套目录不会归错);
|
|
119
|
+
* - 没有 → `project/create`,`idempotencyKey` 按目录固定,重复点不会建出两个;
|
|
120
|
+
* - 老版本 codex 没有 `project/*` → 返回 null,退回「只有 cwd」的老行为(不阻塞建会话)。
|
|
121
|
+
*/
|
|
122
|
+
export async function ensureCodexProject(client, workspace) {
|
|
123
|
+
if (!workspace)
|
|
124
|
+
return null;
|
|
125
|
+
try {
|
|
126
|
+
const list = await client.request("project/list", {});
|
|
127
|
+
const match = (list?.data ?? [])
|
|
128
|
+
.flatMap((project) => (project.roots ?? []).map((root) => ({ id: project.id, path: root.path ?? "" })))
|
|
129
|
+
.filter((root) => root.path !== "" && (workspace === root.path || workspace.startsWith(`${root.path}/`)))
|
|
130
|
+
.sort((left, right) => right.path.length - left.path.length)[0];
|
|
131
|
+
if (match)
|
|
132
|
+
return match.id;
|
|
133
|
+
const created = await client.request("project/create", {
|
|
134
|
+
name: workspace.split("/").filter(Boolean).pop() ?? workspace,
|
|
135
|
+
roots: [{ path: workspace }],
|
|
136
|
+
idempotencyKey: `agent-web:${workspace}`,
|
|
137
|
+
});
|
|
138
|
+
return created?.project?.id ?? null;
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
export function createCodexSessionsProvider(options) {
|
|
145
|
+
return async () => {
|
|
146
|
+
const [projects, threads] = await Promise.all([
|
|
147
|
+
options.client
|
|
148
|
+
.request("project/list", { sortKey: "position" })
|
|
149
|
+
.then((result) => result?.data ?? [])
|
|
150
|
+
.catch((error) => {
|
|
151
|
+
// 老版本 codex 没有 project/*:退化成「只按 cwd 分组」,不要让侧栏整体失败
|
|
152
|
+
options.log?.(`codex project/list 不可用:${error instanceof Error ? error.message : String(error)}`);
|
|
153
|
+
return [];
|
|
154
|
+
}),
|
|
155
|
+
listAllThreads(options.client, options.limit ?? 200),
|
|
156
|
+
]);
|
|
157
|
+
return groupThreads(projects, threads);
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex app-server 客户端(JSONL over stdio)。
|
|
3
|
+
*
|
|
4
|
+
* 为什么是「子进程 + stdio」而不是自建协议:codex 自己就有本地服务(`codex app-server`),
|
|
5
|
+
* 项目 / 会话 / 回合 / 审批全走它的 JSON-RPC。我们只当客户端,**不碰它的存储格式**
|
|
6
|
+
* (`state_5.sqlite`、rollout jsonl 都不直写),这样 Codex Desktop / CLI 与工作台看到的是
|
|
7
|
+
* 同一份会话。
|
|
8
|
+
*
|
|
9
|
+
* 生命周期:懒启动(第一次 request 才 spawn)+ 按需重建(进程挂了下次请求再拉起)。
|
|
10
|
+
* 这样「机器上没装 codex」不会让工作台起不来 —— 只有在真正用这个引擎时才报错。
|
|
11
|
+
*
|
|
12
|
+
* 帧格式(实测 0.162/0.163 一致):
|
|
13
|
+
* 请求 → `{"id":1,"method":"thread/list","params":{...}}\n`
|
|
14
|
+
* 响应 → `{"id":1,"result":{...}}` / `{"id":1,"error":{"code":-32602,"message":"..."}}`
|
|
15
|
+
* 通知 → `{"method":"item/agentMessage/delta","params":{...},"emittedAtMs":...}`
|
|
16
|
+
* 服务端请求(审批)→ `{"id":9,"method":"item/commandExecution/requestApproval","params":{...}}`
|
|
17
|
+
* 客户端必须回 `{"id":9,"result":{...}}`,否则那一轮会一直等。
|
|
18
|
+
*/
|
|
19
|
+
import { createJsonRpcStdioClient } from "./jsonrpc-stdio.js";
|
|
20
|
+
/**
|
|
21
|
+
* codex app-server 客户端 = 通用 JSON-RPC stdio 客户端 + codex 的握手。
|
|
22
|
+
* 握手结果里的 userAgent 形如 `agent-web/0.163.0 (Mac OS …)`,取中间的版本号。
|
|
23
|
+
*/
|
|
24
|
+
export function createCodexClient(options = {}) {
|
|
25
|
+
const log = options.log ?? (() => { });
|
|
26
|
+
return createJsonRpcStdioClient({
|
|
27
|
+
bin: options.bin ?? "codex",
|
|
28
|
+
args: ["app-server"],
|
|
29
|
+
env: options.home ? { CODEX_HOME: options.home } : undefined,
|
|
30
|
+
name: "codex",
|
|
31
|
+
log,
|
|
32
|
+
timeoutMs: options.timeoutMs,
|
|
33
|
+
// codex 不带 jsonrpc 信封(实测 0.162/0.163 都接受裸 {id,method,params})
|
|
34
|
+
jsonrpc: false,
|
|
35
|
+
async handshake(call) {
|
|
36
|
+
const result = (await call("initialize", {
|
|
37
|
+
clientInfo: {
|
|
38
|
+
name: options.clientName ?? "agent-web",
|
|
39
|
+
title: "agent-web",
|
|
40
|
+
version: options.clientVersion ?? "0.1.0",
|
|
41
|
+
},
|
|
42
|
+
// experimentalApi 打开隐藏方法(project/* 等);实测 0.162/0.163 都支持
|
|
43
|
+
capabilities: { experimentalApi: true, requestAttestation: false },
|
|
44
|
+
}));
|
|
45
|
+
const match = /^[^/]+\/([0-9][^\s]*)/.exec(result?.userAgent ?? "");
|
|
46
|
+
return { version: match?.[1] ?? null };
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
}
|