oh-my-agent-web 0.4.0 → 0.5.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 +29 -2
- package/dist/server/engine/builtin.js +23 -0
- package/dist/server/engine/jsonrpc-stdio.js +1 -1
- package/dist/server/engine/narrate-guidance.js +22 -0
- package/dist/server/engine/opencode-map.js +499 -0
- package/dist/server/engine/opencode-projects.js +61 -0
- package/dist/server/engine/opencode-sessions.js +195 -0
- package/dist/server/engine/opencode-transport.js +48 -0
- package/dist/server/engine/opencode.js +394 -0
- package/dist/server/engine/pi-plugins.js +310 -0
- package/dist/server/engine/pi-sdk.js +163 -3
- package/dist/server/engine/pi-subagent-tool.js +166 -0
- package/dist/server/engine/pi-subagents.js +372 -0
- package/dist/server/engine/registry.js +1 -1
- package/dist/server/engine/subagent-guidance.js +22 -0
- package/dist/server/engine/wiring.js +59 -0
- package/dist/server/files/files-routes.js +29 -1
- package/dist/server/files/git-info.js +176 -1
- package/dist/server/http/aux-routes.js +16 -0
- package/dist/server/http/compress.js +2 -0
- package/dist/server/index.js +22 -1
- package/dist/server/mcp/mcp-auth.js +184 -0
- package/dist/server/mcp/mcp-command.js +94 -0
- package/dist/server/mcp/mcp-import.js +290 -0
- package/dist/server/mcp/mcp-market-sources.js +192 -0
- package/dist/server/mcp/mcp-market-types.js +5 -0
- package/dist/server/mcp/mcp-market.js +175 -0
- package/dist/server/mcp/mcp-probe.js +134 -0
- package/dist/server/mcp/mcp-routes.js +70 -0
- package/dist/server/mcp/mcp-store.js +286 -0
- package/dist/server/mcp/pi-mcp-config.js +280 -0
- package/dist/server/plugins/plugin-routes.js +21 -0
- package/dist/server/protocol-parse.js +64 -3
- package/dist/server/providers/codex-models.js +96 -0
- package/dist/server/providers/codex-test.js +124 -0
- package/dist/server/providers/import-routes.js +99 -32
- package/dist/server/providers/provider-handlers.js +26 -6
- package/dist/server/providers/provider-store.js +49 -17
- package/dist/server/providers/subscription-routes.js +181 -0
- package/dist/server/session/model-controller.js +9 -1
- package/dist/server/session/runtime.js +23 -1
- package/dist/server/session/session-stats.js +3 -0
- package/dist/server/session/subagents.js +116 -0
- package/dist/server/session/turn-queue.js +1 -1
- package/dist/server/session/turn-send.js +23 -3
- package/dist/server/skills/skill-install.js +186 -0
- package/dist/server/skills/skill-routes.js +107 -1
- package/dist/server/skills/skill-store.js +33 -4
- package/dist/server/skills/skillhub.js +214 -0
- package/dist/server/skills/zip.js +121 -0
- package/dist/server/snapshot/state.js +10 -1
- package/dist/server/subagents/profile-store.js +287 -0
- package/dist/server/util/env.js +6 -0
- package/dist/server/ws/handle-message.js +12 -0
- package/package.json +2 -1
- package/web/dist/assets/{abnfDiagram-WEYOIA3C-NLp09iEC.js → abnfDiagram-WEYOIA3C-VUKLHHSn.js} +1 -1
- package/web/dist/assets/{arc-DviL9o3c.js → arc-BeUkaHhk.js} +1 -1
- package/web/dist/assets/{architecture-WOLXFQ4H-fjsM_Sy3.js → architecture-WOLXFQ4H-B9I4s0NV.js} +1 -1
- package/web/dist/assets/{architectureDiagram-ZHVYKT53-Cn3B4TE8.js → architectureDiagram-ZHVYKT53-CaEkLhfq.js} +1 -1
- package/web/dist/assets/{blockDiagram-RJAE7QA3-CIuvxWvi.js → blockDiagram-RJAE7QA3-IR49UNE_.js} +1 -1
- package/web/dist/assets/{c4Diagram-XDPPPE54-DvdKmEpF.js → c4Diagram-XDPPPE54-B-81CloB.js} +1 -1
- package/web/dist/assets/channel-B_mTTjGt.js +1 -0
- package/web/dist/assets/{chunk-2E7WHVMD-HNNASGCS.js → chunk-2E7WHVMD-CaWVF7qv.js} +1 -1
- package/web/dist/assets/{chunk-4EMNKR2T-Cg-eRwBE.js → chunk-4EMNKR2T-BvXf2tq-.js} +1 -1
- package/web/dist/assets/{chunk-55XCU76K-BweiisZL.js → chunk-55XCU76K-Bz_d2e2D.js} +1 -1
- package/web/dist/assets/{chunk-7S6XSTFO-BCzIiP-6.js → chunk-7S6XSTFO-B_SPMiU8.js} +1 -1
- package/web/dist/assets/{chunk-AQDTSKZB-Dl0NRFuU.js → chunk-AQDTSKZB-Cen-_xej.js} +1 -1
- package/web/dist/assets/{chunk-DSNLSHJZ-zhHCjjN9.js → chunk-DSNLSHJZ-DEw1kFwj.js} +1 -1
- package/web/dist/assets/{chunk-HAGFOTED-BDQqU2EH.js → chunk-HAGFOTED-BwpOOcw8.js} +1 -1
- package/web/dist/assets/{chunk-HNE4Q36P-B-qRzMjX.js → chunk-HNE4Q36P-BjCwi2uW.js} +1 -1
- package/web/dist/assets/{chunk-NQOKRVIF-CNcvhMn_.js → chunk-NQOKRVIF-WcqhWFS5.js} +1 -1
- package/web/dist/assets/{chunk-UHW7PFBP-Dwr8LUzr.js → chunk-UHW7PFBP-quDvQby8.js} +1 -1
- package/web/dist/assets/{chunk-VQ5W62QI-O8sN6jJu.js → chunk-VQ5W62QI-DZBwmhzq.js} +1 -1
- package/web/dist/assets/{chunk-WQQL4CDK-B6-59Zg2.js → chunk-WQQL4CDK-wYyHYuui.js} +1 -1
- package/web/dist/assets/{chunk-XXDRQBXY-DvSbeUlZ.js → chunk-XXDRQBXY-DfJzC7nt.js} +1 -1
- package/web/dist/assets/{chunk-YZO67HFG-BXpKYKsw.js → chunk-YZO67HFG-CWLraGk_.js} +1 -1
- package/web/dist/assets/{chunk-ZAUX2LTX-B1Z5VSkz.js → chunk-ZAUX2LTX-ChBUt_FJ.js} +1 -1
- package/web/dist/assets/{chunk-ZCHMQSOR-h_5mBLIA.js → chunk-ZCHMQSOR-C57UFT60.js} +1 -1
- package/web/dist/assets/{classDiagram-v2-4MYWR3UA-Dr6JnaaV.js → classDiagram-v2-4MYWR3UA-Bo4i7SNY.js} +1 -1
- package/web/dist/assets/{cose-bilkent-JH36ORCC-36ShkR7R.js → cose-bilkent-JH36ORCC-CK360NdH.js} +1 -1
- package/web/dist/assets/{cynefin-EF2NZ3EQ-Dg6bI7pC.js → cynefin-EF2NZ3EQ-DlXFjIFm.js} +1 -1
- package/web/dist/assets/{cynefinDiagram-WF24UDRP-B0YHpBlR.js → cynefinDiagram-WF24UDRP-zvZIqA1c.js} +1 -1
- package/web/dist/assets/{dagre-NK4GIZVX-Cx3mn7YS.js → dagre-NK4GIZVX-CnKhCu-v.js} +1 -1
- package/web/dist/assets/{diagram-CBK3MWTC-l8aZG_Bm.js → diagram-CBK3MWTC-3QTR8Fjx.js} +1 -1
- package/web/dist/assets/{diagram-I5XIIV2D-Bd5f14_h.js → diagram-I5XIIV2D-D9lSd7z6.js} +1 -1
- package/web/dist/assets/{diagram-ODIL2UCW-hBsxCYUj.js → diagram-ODIL2UCW-DWJD6lPM.js} +1 -1
- package/web/dist/assets/{diagram-PFBEJUXE-BgpIBUYl.js → diagram-PFBEJUXE-CWJmoUg4.js} +1 -1
- package/web/dist/assets/{diagram-RJA3I4VV-CDU1gUVD.js → diagram-RJA3I4VV-D1SZHQcQ.js} +1 -1
- package/web/dist/assets/{diagram-TYUNFSNR-Iuxh7Tqf.js → diagram-TYUNFSNR-DCCjGi1x.js} +1 -1
- package/web/dist/assets/{dist-C86cKTuE.js → dist-VOk_Iv17.js} +1 -1
- package/web/dist/assets/{ebnfDiagram-V4P3TTPT-Cms0HFKh.js → ebnfDiagram-V4P3TTPT-CQLdqEiW.js} +1 -1
- package/web/dist/assets/{elk-QONTBCTQ-B7yEy0PQ.js → elk-QONTBCTQ-Dp2pEp6b.js} +1 -1
- package/web/dist/assets/{erDiagram-KYQIGQAA-CL48j4aU.js → erDiagram-KYQIGQAA-BFGNoFQ-.js} +1 -1
- package/web/dist/assets/{eventmodeling-K75KTNOO-CFvkaejh.js → eventmodeling-K75KTNOO-CFfAemGw.js} +1 -1
- package/web/dist/assets/flowDiagram-IUGLLWBU-DmSp3cbW.js +1 -0
- package/web/dist/assets/{ganttDiagram-R7GPNTJY-vdMr-0ZV.js → ganttDiagram-R7GPNTJY-vK3jPGDq.js} +1 -1
- package/web/dist/assets/{gitGraph-VSP46ZUC-2z85Wuwo.js → gitGraph-VSP46ZUC-rXBgHTdq.js} +1 -1
- package/web/dist/assets/{gitGraphDiagram-PDYL7XHY-BY4GPiqC.js → gitGraphDiagram-PDYL7XHY-CBbGhago.js} +1 -1
- package/web/dist/assets/index-DBOF_OJe.js +104 -0
- package/web/dist/assets/index-nlY_-OaR.css +1 -0
- package/web/dist/assets/{info-OHQRW6UA-ECzkYTx-.js → info-OHQRW6UA-Cd9vp-6C.js} +1 -1
- package/web/dist/assets/{infoDiagram-MVDRKAX5-DDk5EAeb.js → infoDiagram-MVDRKAX5-Ce-Sg_qu.js} +1 -1
- package/web/dist/assets/{ishikawaDiagram-HPEHV2GE-9yPH4J94.js → ishikawaDiagram-HPEHV2GE-DWlmR6BY.js} +1 -1
- package/web/dist/assets/{journeyDiagram-BOSHW7O7-BPTIap3l.js → journeyDiagram-BOSHW7O7-DmDG97ae.js} +1 -1
- package/web/dist/assets/{kanban-definition-FNSA4HC5-BbADHrA8.js → kanban-definition-FNSA4HC5-1Z4sDrgV.js} +1 -1
- package/web/dist/assets/{line-CHV9_4lL.js → line-DGM1-9KW.js} +1 -1
- package/web/dist/assets/{linear-C-eZsBTv.js → linear-B6hyeGHu.js} +1 -1
- package/web/dist/assets/{mermaid-parser.core-BiAhX1l1.js → mermaid-parser.core-D1HmdkA4.js} +3 -3
- package/web/dist/assets/{mermaid.core-BxP_A_zj.js → mermaid.core-DeUJrhO1.js} +4 -4
- package/web/dist/assets/{mindmap-definition-6OY57UG3-BMWlit95.js → mindmap-definition-6OY57UG3-C3-FByy8.js} +1 -1
- package/web/dist/assets/{packet-JDAUHWVQ-FVApuKOp.js → packet-JDAUHWVQ-BoOr7DaW.js} +1 -1
- package/web/dist/assets/{pegDiagram-ECIUJNBX-WjWJvDHM.js → pegDiagram-ECIUJNBX-Bn7m75TZ.js} +1 -1
- package/web/dist/assets/{pie-XZMESJXO-BGCZnvhp.js → pie-XZMESJXO-CGuMTDBp.js} +1 -1
- package/web/dist/assets/{pieDiagram-GNMPY74T-DJpAC_JC.js → pieDiagram-GNMPY74T-DHhaPwZe.js} +1 -1
- package/web/dist/assets/{quadrantDiagram-D57K4MC3-hxx5c7oq.js → quadrantDiagram-D57K4MC3-DxTgFHLK.js} +1 -1
- package/web/dist/assets/{radar-ABXABTNO-BU9Shc-O.js → radar-ABXABTNO-BiKxmALL.js} +1 -1
- package/web/dist/assets/{railroad-I3PHUGI6-pWj8d-G4.js → railroad-I3PHUGI6-BKYufgg4.js} +1 -1
- package/web/dist/assets/{railroad-abnf-LNEFI6M7-D7HZBTAj.js → railroad-abnf-LNEFI6M7-DmOrjlyL.js} +1 -1
- package/web/dist/assets/{railroad-ebnf-DWYD2UWJ-CKiXJSuM.js → railroad-ebnf-DWYD2UWJ-BgMyVDd1.js} +1 -1
- package/web/dist/assets/{railroad-peg-JR7BVNR7-DB3VJHgR.js → railroad-peg-JR7BVNR7-BN6NL8S-.js} +1 -1
- package/web/dist/assets/{railroadDiagram-Y72NRBL7-Cev7Y65k.js → railroadDiagram-Y72NRBL7-BsbA-9gP.js} +1 -1
- package/web/dist/assets/{requirementDiagram-EQYSW5RA-CqbwMLD8.js → requirementDiagram-EQYSW5RA-HUw4YqWd.js} +1 -1
- package/web/dist/assets/{sankeyDiagram-SY24JHO5-V2cyVi8q.js → sankeyDiagram-SY24JHO5-Dx1MOFRu.js} +1 -1
- package/web/dist/assets/{sequenceDiagram-BLEHNHQU-By6NJOPM.js → sequenceDiagram-BLEHNHQU-C0-Rzg3g.js} +1 -1
- package/web/dist/assets/{src-Da91fG3g.js → src-CuHygcpj.js} +1 -1
- package/web/dist/assets/{stateDiagram-v2-A6IESKWZ-BnPGX5UI.js → stateDiagram-v2-A6IESKWZ-BnEICGtg.js} +1 -1
- package/web/dist/assets/{swimlanes-B3ENRKFQ-C58BpIpm.js → swimlanes-B3ENRKFQ-CwDRCHZD.js} +1 -1
- package/web/dist/assets/swimlanesDiagram-AYQRV4AK-DZ_aAXmG.js +8 -0
- package/web/dist/assets/{timeline-definition-JXYKGLGB-Hgo1DhnS.js → timeline-definition-JXYKGLGB-C4BVpgzc.js} +1 -1
- package/web/dist/assets/{treeView-D4JQ5SDB-DF0dfjqB.js → treeView-D4JQ5SDB-BBsvEMP3.js} +1 -1
- package/web/dist/assets/{treemap-SAJKECNS-CSSM5FqB.js → treemap-SAJKECNS-BYw6cxC8.js} +1 -1
- package/web/dist/assets/{usecaseDiagram-WPOLBNNX-BkBBD1NY.js → usecaseDiagram-WPOLBNNX-BkOXLgwP.js} +1 -1
- package/web/dist/assets/{vennDiagram-Q3Y3IDCW-Bm28keX_.js → vennDiagram-Q3Y3IDCW-Kgjk_n6B.js} +1 -1
- package/web/dist/assets/{wardley-7MLQ67FV-By7KgEQX.js → wardley-7MLQ67FV-BGVObhvJ.js} +1 -1
- package/web/dist/assets/{wardleyDiagram-IVAIDKWA-oVtIrnGa.js → wardleyDiagram-IVAIDKWA-i97uD6tL.js} +1 -1
- package/web/dist/assets/{xychartDiagram-4URNHDU4-aI8FH4g2.js → xychartDiagram-4URNHDU4-awJi5TfJ.js} +1 -1
- package/web/dist/index.html +15 -2
- package/web/dist/assets/channel-C3AQ6nAs.js +0 -1
- package/web/dist/assets/flowDiagram-IUGLLWBU-6UyKXpqG.js +0 -1
- package/web/dist/assets/index-Bjn56a2i.js +0 -76
- package/web/dist/assets/index-Drp6e5iU.css +0 -1
- package/web/dist/assets/swimlanesDiagram-AYQRV4AK-RaivxSsc.js +0 -8
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
import { isScratchCwd } from "./scratch.js";
|
|
2
|
+
import { currentModelId, currentThinkingLevel, exportStats, exportToTranscript, exportToUiMessages, friendlyOpencodeError, hasPendingUsage, hasUsableToolFacts, isFinalToolStatus, modelOptions, modelValueFor, parseModelValue, permissionApprovalRequest, permissionOptionId, stopReasonToTurnEnd, textEventOf, thinkingConfigId, thinkingLevels, toolEndEvent, toolStartEvent, usageEventOf, } from "./opencode-map.js";
|
|
3
|
+
import { deleteOpencodeSession, readOpencodeExport } from "./opencode-sessions.js";
|
|
4
|
+
import { createOpencodeClient } from "./opencode-transport.js";
|
|
5
|
+
const MAX_TOOL_FACTS = 200;
|
|
6
|
+
/** 历史导出缓存时长:`sessionStats` 会被快照频繁调用,不能每次都起子进程 */
|
|
7
|
+
const EXPORT_CACHE_MS = 3000;
|
|
8
|
+
/**
|
|
9
|
+
* 回合刚结束后多久内,「读统计」要等一下再读。
|
|
10
|
+
*
|
|
11
|
+
* 为什么必须有它:`session/prompt` 一返回,宿主立刻调 `sessionStats`;而 opencode
|
|
12
|
+
* 是**先返回再落库**,这一瞬间 `opencode export` 还看不到最后一条 assistant
|
|
13
|
+
* (实测:续聊一轮后数字停在前一轮,要等一会儿才对)。所以这一小段窗口里
|
|
14
|
+
* 先让出 600ms 再读,并且不吃缓存。
|
|
15
|
+
*/
|
|
16
|
+
const EXPORT_SETTLE_MS = 3000;
|
|
17
|
+
export function createOpencodeEngine(options = {}) {
|
|
18
|
+
const bin = options.bin ?? "opencode";
|
|
19
|
+
const client = options.client ?? createOpencodeClient({ bin, args: options.args, log: options.log ?? (() => { }) });
|
|
20
|
+
const bindings = new Map();
|
|
21
|
+
const byEngineSession = new Map();
|
|
22
|
+
const active = new Map();
|
|
23
|
+
/** toolCallId → 工具事实(权限请求兜底 / 延迟开卡片用) */
|
|
24
|
+
const toolFacts = new Map();
|
|
25
|
+
const exportCache = new Map();
|
|
26
|
+
/** conversationId → 最近一次回合结束的时刻(用于 EXPORT_SETTLE_MS 的等待) */
|
|
27
|
+
const settledAt = new Map();
|
|
28
|
+
let approvalHandler = null;
|
|
29
|
+
let lastError = null;
|
|
30
|
+
/**
|
|
31
|
+
* 最近一次拿到的 configOptions(模型 / 思考档目录)。
|
|
32
|
+
* 历史会话在 resume 之前没有 config,但选择器不该是空的 —— 用它兜底;
|
|
33
|
+
* 真正切模型时仍会先 resume(拿不到就报可读错误)。
|
|
34
|
+
*/
|
|
35
|
+
let lastConfig = [];
|
|
36
|
+
const rememberTool = (facts) => {
|
|
37
|
+
const previous = toolFacts.get(facts.toolCallId);
|
|
38
|
+
// 后到的 update 更详细(title 从 "bash" 变成真命令),合并而不是覆盖
|
|
39
|
+
toolFacts.set(facts.toolCallId, { ...previous, ...facts });
|
|
40
|
+
if (toolFacts.size > MAX_TOOL_FACTS) {
|
|
41
|
+
const oldest = toolFacts.keys().next().value;
|
|
42
|
+
if (oldest)
|
|
43
|
+
toolFacts.delete(oldest);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
const applyConfig = (binding, config) => {
|
|
47
|
+
if (!Array.isArray(config) || config.length === 0)
|
|
48
|
+
return;
|
|
49
|
+
lastConfig = config;
|
|
50
|
+
if (binding)
|
|
51
|
+
binding.config = lastConfig;
|
|
52
|
+
};
|
|
53
|
+
const bindingOf = (conversationId) => bindings.get(conversationId) ?? null;
|
|
54
|
+
const anyConfig = (conversationId) => {
|
|
55
|
+
const binding = conversationId ? bindingOf(conversationId) : undefined;
|
|
56
|
+
if (binding && binding.config.length > 0)
|
|
57
|
+
return binding.config;
|
|
58
|
+
return [...bindings.values()].find((item) => item.config.length > 0)?.config ?? lastConfig;
|
|
59
|
+
};
|
|
60
|
+
const invalidateExport = (conversationId) => {
|
|
61
|
+
const binding = bindingOf(conversationId);
|
|
62
|
+
if (binding)
|
|
63
|
+
exportCache.delete(binding.engineSessionId);
|
|
64
|
+
};
|
|
65
|
+
/** 回合结束:先失效缓存(下一次读必须重新取),并记下时刻供 sessionStats 等待 */
|
|
66
|
+
const markTurnSettled = (conversationId) => {
|
|
67
|
+
invalidateExport(conversationId);
|
|
68
|
+
settledAt.set(conversationId, Date.now());
|
|
69
|
+
};
|
|
70
|
+
/** 历史会话首次使用时才 resume(ACP 的 resume 会拿写句柄,所以按需) */
|
|
71
|
+
const ensureLive = async (conversationId) => {
|
|
72
|
+
const binding = bindingOf(conversationId);
|
|
73
|
+
if (!binding)
|
|
74
|
+
return null;
|
|
75
|
+
if (binding.live)
|
|
76
|
+
return binding;
|
|
77
|
+
const result = await client.request("session/resume", {
|
|
78
|
+
sessionId: binding.engineSessionId,
|
|
79
|
+
cwd: binding.cwd,
|
|
80
|
+
mcpServers: [],
|
|
81
|
+
});
|
|
82
|
+
binding.live = true;
|
|
83
|
+
applyConfig(binding, result?.configOptions);
|
|
84
|
+
return binding;
|
|
85
|
+
};
|
|
86
|
+
const setOption = async (conversationId, configId, value) => {
|
|
87
|
+
lastError = null;
|
|
88
|
+
try {
|
|
89
|
+
const binding = await ensureLive(conversationId);
|
|
90
|
+
if (!binding) {
|
|
91
|
+
lastError = "先打开一个会话";
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
const result = await client.request("session/set_config_option", {
|
|
95
|
+
sessionId: binding.engineSessionId,
|
|
96
|
+
configId,
|
|
97
|
+
value,
|
|
98
|
+
});
|
|
99
|
+
applyConfig(binding, result?.configOptions);
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
lastError = friendlyOpencodeError(error);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const handleToolUpdate = (conversationId, update) => {
|
|
108
|
+
const toolCallId = update.toolCallId;
|
|
109
|
+
if (typeof toolCallId !== "string" || toolCallId === "")
|
|
110
|
+
return;
|
|
111
|
+
const facts = { toolCallId };
|
|
112
|
+
if (typeof update.title === "string")
|
|
113
|
+
facts.title = update.title;
|
|
114
|
+
if (typeof update.kind === "string")
|
|
115
|
+
facts.kind = update.kind;
|
|
116
|
+
if (update.rawInput !== undefined)
|
|
117
|
+
facts.rawInput = update.rawInput;
|
|
118
|
+
rememberTool(facts);
|
|
119
|
+
const merged = toolFacts.get(toolCallId) ?? facts;
|
|
120
|
+
const turn = active.get(conversationId);
|
|
121
|
+
if (!turn || turn.ended)
|
|
122
|
+
return;
|
|
123
|
+
// 延迟开卡片:等「有真参数」那一条(否则先显示成 `bash {"cwd":…}`)
|
|
124
|
+
if (!turn.started.has(toolCallId)) {
|
|
125
|
+
const final = isFinalToolStatus(update.status);
|
|
126
|
+
if (!hasUsableToolFacts(merged) && !final)
|
|
127
|
+
return;
|
|
128
|
+
turn.started.add(toolCallId);
|
|
129
|
+
turn.onEvent(toolStartEvent(merged));
|
|
130
|
+
}
|
|
131
|
+
if (!isFinalToolStatus(update.status) || turn.finished.has(toolCallId))
|
|
132
|
+
return;
|
|
133
|
+
turn.finished.add(toolCallId);
|
|
134
|
+
turn.onEvent(toolEndEvent(update));
|
|
135
|
+
};
|
|
136
|
+
const handleNotification = (notification) => {
|
|
137
|
+
if (notification.method !== "session/update")
|
|
138
|
+
return;
|
|
139
|
+
const params = notification.params;
|
|
140
|
+
const engineSessionId = typeof params.sessionId === "string" ? params.sessionId : "";
|
|
141
|
+
const conversationId = byEngineSession.get(engineSessionId);
|
|
142
|
+
if (!conversationId)
|
|
143
|
+
return;
|
|
144
|
+
const update = (params.update ?? {});
|
|
145
|
+
if (update.sessionUpdate === "config_option_update") {
|
|
146
|
+
applyConfig(bindingOf(conversationId) ?? undefined, update.configOptions);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") {
|
|
150
|
+
handleToolUpdate(conversationId, update);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
const event = textEventOf(update);
|
|
154
|
+
if (!event)
|
|
155
|
+
return;
|
|
156
|
+
const turn = active.get(conversationId);
|
|
157
|
+
if (turn && !turn.ended)
|
|
158
|
+
turn.onEvent(event);
|
|
159
|
+
};
|
|
160
|
+
const handlePermission = async (id, params) => {
|
|
161
|
+
const facts = params.toolCall?.toolCallId ? toolFacts.get(params.toolCall.toolCallId) : undefined;
|
|
162
|
+
const request = permissionApprovalRequest(params, facts);
|
|
163
|
+
let decision = "deny";
|
|
164
|
+
if (approvalHandler) {
|
|
165
|
+
try {
|
|
166
|
+
decision = await approvalHandler(request);
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
decision = "deny";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
client.respond(id, { outcome: { outcome: "selected", optionId: permissionOptionId(params.options, decision) } });
|
|
173
|
+
};
|
|
174
|
+
client.onNotification(handleNotification);
|
|
175
|
+
client.onServerRequest((request) => {
|
|
176
|
+
if (request.method === "session/request_permission") {
|
|
177
|
+
void handlePermission(request.id, request.params);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
// 认不出的服务端请求必须回包,否则 opencode 那一轮会一直等
|
|
181
|
+
client.respondError(request.id, -32601, `agent-web 未实现的服务端请求:${request.method}`);
|
|
182
|
+
});
|
|
183
|
+
/** 读一条会话的导出(带缓存):历史回放与统计共用 */
|
|
184
|
+
const exportOf = async (conversationId, readOptions = {}) => {
|
|
185
|
+
const binding = bindingOf(conversationId);
|
|
186
|
+
const engineSessionId = binding?.engineSessionId ?? conversationId;
|
|
187
|
+
if (!readOptions.fresh) {
|
|
188
|
+
const cached = exportCache.get(engineSessionId);
|
|
189
|
+
if (cached && Date.now() - cached.at < EXPORT_CACHE_MS)
|
|
190
|
+
return cached.value;
|
|
191
|
+
}
|
|
192
|
+
const value = await readOpencodeExport(bin, engineSessionId, { log: options.log });
|
|
193
|
+
// 读失败(opencode 忙 / 会话刚建还没落库)不写缓存,否则 3 秒内都拿不到真数据
|
|
194
|
+
if (value)
|
|
195
|
+
exportCache.set(engineSessionId, { at: Date.now(), value });
|
|
196
|
+
return value;
|
|
197
|
+
};
|
|
198
|
+
return {
|
|
199
|
+
client,
|
|
200
|
+
version: () => client.version(),
|
|
201
|
+
async prewarm() {
|
|
202
|
+
await client.ensureStarted();
|
|
203
|
+
},
|
|
204
|
+
async createSession(sessionOptions) {
|
|
205
|
+
const created = await client
|
|
206
|
+
.request("session/new", {
|
|
207
|
+
cwd: sessionOptions.workspace,
|
|
208
|
+
mcpServers: [],
|
|
209
|
+
})
|
|
210
|
+
.catch((error) => {
|
|
211
|
+
throw new Error(friendlyOpencodeError(error));
|
|
212
|
+
});
|
|
213
|
+
const engineSessionId = created?.sessionId;
|
|
214
|
+
if (!engineSessionId)
|
|
215
|
+
throw new Error("opencode 没有返回 sessionId");
|
|
216
|
+
const binding = {
|
|
217
|
+
engineSessionId,
|
|
218
|
+
cwd: sessionOptions.workspace,
|
|
219
|
+
live: true,
|
|
220
|
+
config: created.configOptions ?? [],
|
|
221
|
+
};
|
|
222
|
+
bindings.set(sessionOptions.sessionId, binding);
|
|
223
|
+
byEngineSession.set(engineSessionId, sessionOptions.sessionId);
|
|
224
|
+
applyConfig(binding, created.configOptions);
|
|
225
|
+
exportCache.delete(engineSessionId);
|
|
226
|
+
// 默认模型:只尊重显式配置(AGENT_WEB_OPENCODE_MODEL),否则用 opencode 自己的默认
|
|
227
|
+
const wanted = options.model?.trim();
|
|
228
|
+
if (wanted) {
|
|
229
|
+
const value = modelValueFor(binding.config, wanted);
|
|
230
|
+
if (value && value !== currentModelId(binding.config)) {
|
|
231
|
+
await setOption(sessionOptions.sessionId, "model", value);
|
|
232
|
+
}
|
|
233
|
+
else if (!value) {
|
|
234
|
+
options.log?.(`opencode:默认模型不在可选列表里,已忽略:${wanted}`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { sessionPath: null, engineSessionId };
|
|
238
|
+
},
|
|
239
|
+
async openSession(openOptions) {
|
|
240
|
+
const engineSessionId = openOptions.engineSessionId ?? openOptions.sessionPath ?? openOptions.sessionId;
|
|
241
|
+
const binding = {
|
|
242
|
+
engineSessionId,
|
|
243
|
+
cwd: openOptions.cwd ?? "",
|
|
244
|
+
live: false,
|
|
245
|
+
config: [],
|
|
246
|
+
};
|
|
247
|
+
bindings.set(openOptions.sessionId, binding);
|
|
248
|
+
byEngineSession.set(engineSessionId, openOptions.sessionId);
|
|
249
|
+
},
|
|
250
|
+
async sendMessage(sessionId, text, onEvent, sendOptions) {
|
|
251
|
+
const binding = await ensureLive(sessionId).catch((error) => {
|
|
252
|
+
throw new Error(friendlyOpencodeError(error));
|
|
253
|
+
});
|
|
254
|
+
if (!binding)
|
|
255
|
+
throw new Error("opencode 会话还没准备好(先新建一个会话)");
|
|
256
|
+
const turn = { onEvent, ended: false, started: new Set(), finished: new Set() };
|
|
257
|
+
active.set(sessionId, turn);
|
|
258
|
+
const model = currentModelId(binding.config);
|
|
259
|
+
if (model)
|
|
260
|
+
onEvent({ type: "model", model });
|
|
261
|
+
// ACP 的图片注入:promptCapabilities.image = true(实测声明里有)
|
|
262
|
+
const prompt = [];
|
|
263
|
+
if (text.trim() !== "")
|
|
264
|
+
prompt.push({ type: "text", text });
|
|
265
|
+
for (const image of sendOptions?.images ?? []) {
|
|
266
|
+
prompt.push({ type: "image", data: image.data, mimeType: image.mimeType });
|
|
267
|
+
}
|
|
268
|
+
if (prompt.length === 0) {
|
|
269
|
+
active.delete(sessionId);
|
|
270
|
+
throw new Error("这条消息是空的(没有文本也没有图片)");
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
const result = await client.request("session/prompt", { sessionId: binding.engineSessionId, prompt },
|
|
274
|
+
// 一轮跑很久是正常的(有工具调用):给 30 分钟,别用默认 30 秒掐掉
|
|
275
|
+
{ timeoutMs: 30 * 60 * 1000 });
|
|
276
|
+
markTurnSettled(sessionId);
|
|
277
|
+
const usage = usageEventOf(result);
|
|
278
|
+
if (usage && !turn.ended)
|
|
279
|
+
onEvent(usage);
|
|
280
|
+
if (!turn.ended)
|
|
281
|
+
onEvent({ type: "turn_end", reason: stopReasonToTurnEnd(result?.stopReason) });
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
invalidateExport(sessionId);
|
|
285
|
+
throw new Error(friendlyOpencodeError(error));
|
|
286
|
+
}
|
|
287
|
+
finally {
|
|
288
|
+
turn.ended = true;
|
|
289
|
+
active.delete(sessionId);
|
|
290
|
+
}
|
|
291
|
+
},
|
|
292
|
+
async interrupt(sessionId) {
|
|
293
|
+
const binding = bindingOf(sessionId);
|
|
294
|
+
if (!binding)
|
|
295
|
+
return;
|
|
296
|
+
// ACP 里 cancel 是**通知**(无 id):发成请求会被当成未知方法丢掉 → 停止按钮没反应
|
|
297
|
+
client.notify("session/cancel", { sessionId: binding.engineSessionId });
|
|
298
|
+
},
|
|
299
|
+
/** 不看了:关掉会话释放写句柄(opencode 自己的 TUI / CLI 才能打开它) */
|
|
300
|
+
async parkSession(sessionId) {
|
|
301
|
+
const binding = bindingOf(sessionId);
|
|
302
|
+
if (!binding || !binding.live)
|
|
303
|
+
return;
|
|
304
|
+
binding.live = false;
|
|
305
|
+
await client.request("session/close", { sessionId: binding.engineSessionId }).catch(() => undefined);
|
|
306
|
+
},
|
|
307
|
+
async closeSession(sessionId) {
|
|
308
|
+
await this.parkSession?.(sessionId);
|
|
309
|
+
const binding = bindingOf(sessionId);
|
|
310
|
+
bindings.delete(sessionId);
|
|
311
|
+
if (binding) {
|
|
312
|
+
byEngineSession.delete(binding.engineSessionId);
|
|
313
|
+
exportCache.delete(binding.engineSessionId);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
async dispose() {
|
|
317
|
+
// 注意:**不清 lastConfig** —— 它只是「opencode 有哪些模型/档位」的目录快照,
|
|
318
|
+
// 切走再切回时模型选择器不该空着;真正切模型时会先 resume 拿最新 configOptions。
|
|
319
|
+
bindings.clear();
|
|
320
|
+
byEngineSession.clear();
|
|
321
|
+
active.clear();
|
|
322
|
+
toolFacts.clear();
|
|
323
|
+
exportCache.clear();
|
|
324
|
+
settledAt.clear();
|
|
325
|
+
client.close();
|
|
326
|
+
},
|
|
327
|
+
setApprovalHandler(handler) {
|
|
328
|
+
approvalHandler = handler;
|
|
329
|
+
},
|
|
330
|
+
async listModels(sessionId) {
|
|
331
|
+
return modelOptions(anyConfig(sessionId));
|
|
332
|
+
},
|
|
333
|
+
async switchModel(sessionId, modelId) {
|
|
334
|
+
const value = modelValueFor(anyConfig(sessionId), modelId);
|
|
335
|
+
if (!value) {
|
|
336
|
+
lastError = `这个模型不在 opencode 的可选列表里:${modelId}`;
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
return setOption(sessionId, "model", value);
|
|
340
|
+
},
|
|
341
|
+
async currentModel(sessionId) {
|
|
342
|
+
return parseModelValue(currentModelId(anyConfig(sessionId)));
|
|
343
|
+
},
|
|
344
|
+
async sessionModel(sessionId) {
|
|
345
|
+
return this.currentModel?.(sessionId) ?? null;
|
|
346
|
+
},
|
|
347
|
+
async listThinkingLevels(sessionId) {
|
|
348
|
+
return thinkingLevels(anyConfig(sessionId));
|
|
349
|
+
},
|
|
350
|
+
async getThinkingLevel(sessionId) {
|
|
351
|
+
return currentThinkingLevel(anyConfig(sessionId));
|
|
352
|
+
},
|
|
353
|
+
async setThinkingLevel(sessionId, level) {
|
|
354
|
+
// configId 随引擎/模型而变(opencode = effort),从当前 configOptions 里取
|
|
355
|
+
return setOption(sessionId, thinkingConfigId(anyConfig(sessionId)), level);
|
|
356
|
+
},
|
|
357
|
+
lastError: () => lastError,
|
|
358
|
+
/** 历史回放:走 `opencode export`(ACP 的 load 会重放事件但会占写句柄,不适合只读) */
|
|
359
|
+
async readUiMessages(sessionId) {
|
|
360
|
+
return exportToUiMessages(await exportOf(sessionId));
|
|
361
|
+
},
|
|
362
|
+
async readTranscript(sessionId) {
|
|
363
|
+
return exportToTranscript(await exportOf(sessionId));
|
|
364
|
+
},
|
|
365
|
+
/** 用量统计:四桶来自每条 assistant 的 `info.tokens`(opencode 自己按次记账) */
|
|
366
|
+
async sessionStats(sessionId) {
|
|
367
|
+
// 回合刚结束的一小段时间:opencode 可能「消息已存、tokens 未存」→ 验证 + 重试,
|
|
368
|
+
// 直到拿到已记账的那一版(理由见 EXPORT_SETTLE_MS / hasPendingUsage)
|
|
369
|
+
const at = settledAt.get(sessionId);
|
|
370
|
+
const settling = at !== undefined && Date.now() - at < EXPORT_SETTLE_MS;
|
|
371
|
+
if (!settling) {
|
|
372
|
+
const exported = await exportOf(sessionId);
|
|
373
|
+
return exported ? exportStats(exported) : null;
|
|
374
|
+
}
|
|
375
|
+
let last = null;
|
|
376
|
+
for (let attempt = 0; attempt < 4; attempt += 1) {
|
|
377
|
+
await new Promise((resolve) => setTimeout(resolve, attempt === 0 ? 400 : 600));
|
|
378
|
+
invalidateExport(sessionId);
|
|
379
|
+
last = await exportOf(sessionId, { fresh: true });
|
|
380
|
+
if (last && !hasPendingUsage(last))
|
|
381
|
+
return exportStats(last);
|
|
382
|
+
}
|
|
383
|
+
// 还没落完(或这一轮真的没有任何用量):给最后一次的读数,不编数字
|
|
384
|
+
return last ? exportStats(last) : null;
|
|
385
|
+
},
|
|
386
|
+
/** opencode 原生删除(`opencode session delete <sid>`);改名没接(ACP 无此方法) */
|
|
387
|
+
async deleteSession(handle) {
|
|
388
|
+
await deleteOpencodeSession(bin, handle);
|
|
389
|
+
exportCache.delete(handle);
|
|
390
|
+
},
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
/** 侧栏过滤的判定也对外暴露一份:会话清单与「刚建的会话在不在侧栏」要用同一套规则 */
|
|
394
|
+
export { isScratchCwd };
|