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,358 @@
|
|
|
1
|
+
/** 会话运行时:引擎事件 → UI 状态 / 协议事件,并把转录落盘。规则见 ADR。 */
|
|
2
|
+
import { createMarkerStreamFilter } from "../markers/actions.js";
|
|
3
|
+
import { createRefreshActions } from "./refresh-actions.js";
|
|
4
|
+
import { appendText, createMessage } from "../snapshot/messages.js";
|
|
5
|
+
import { createRuntimeServices } from "./runtime-services.js";
|
|
6
|
+
import { createStatsRefresher } from "./stats-refresher.js";
|
|
7
|
+
import { createThrottle } from "./throttle.js";
|
|
8
|
+
import { createRuntimeTurnFinisher, setRunning } from "./turn-finish.js";
|
|
9
|
+
import { cancelQueuedWith, createTurnPlumbing } from "./turn-queue.js";
|
|
10
|
+
import { createConversationFactory } from "./conversation-create.js";
|
|
11
|
+
import { createClientView } from "./client-view.js";
|
|
12
|
+
import { webVersion } from "../util/version.js";
|
|
13
|
+
import { startUpstreamVersionWatch } from "../util/upstream-version.js";
|
|
14
|
+
import { newConversationWith } from "./conversation-create.js";
|
|
15
|
+
import { renameSessionWith } from "./rename-session.js";
|
|
16
|
+
import { createSnapshotFor } from "./snapshot-for.js";
|
|
17
|
+
import { createSwitchConversation } from "./switch-conversation.js";
|
|
18
|
+
import { createModelController, createModelRefresher } from "./model-controller.js";
|
|
19
|
+
import { createPiSessionOpener } from "./pi-opener.js";
|
|
20
|
+
import { createSessionDeleter } from "./delete-session.js";
|
|
21
|
+
import { createTurnSender } from "./turn-send.js";
|
|
22
|
+
import { createSessionsRefresher } from "./pi-refresh.js";
|
|
23
|
+
import { applySettingsPatch, defaultSettings } from "./settings.js";
|
|
24
|
+
import { createTurnContext } from "./turn-events.js";
|
|
25
|
+
import { createEngineSwitcher, engineOptionsFor } from "./engine-switch.js";
|
|
26
|
+
import { createSwitchableEngine } from "../engine/switchable.js";
|
|
27
|
+
import { conversationId as newConversationId, makeIdFactory } from "../util/ids.js";
|
|
28
|
+
import { buildSnapshot, createServerState, emptyRunning, touchConversationIn } from "../snapshot/state.js";
|
|
29
|
+
export function createRuntime(options) {
|
|
30
|
+
const now = options.now ?? (() => Date.now());
|
|
31
|
+
const settings = options.settings;
|
|
32
|
+
const titleChars = options.titleChars ?? 24;
|
|
33
|
+
/** 「下一步」生成建议的超时(默认 20s;测试可调小) */
|
|
34
|
+
const actionsTimeoutMs = options.actionsTimeoutMs ?? 20_000;
|
|
35
|
+
// 引擎槽:下游模块拿到的都是这个稳定代理,切引擎时不用重建任何一个
|
|
36
|
+
const slot = createSwitchableEngine(options.engine);
|
|
37
|
+
const engine = slot.port;
|
|
38
|
+
const state = createServerState(options.workspace, options.engineName ?? "pi");
|
|
39
|
+
if (options.engineCapabilities)
|
|
40
|
+
state.engineCapabilities = { ...options.engineCapabilities };
|
|
41
|
+
state.engines = engineOptionsFor(options.engineName ?? "pi", state.engineCapabilities, options.engines?.descriptors());
|
|
42
|
+
state.files = { root: options.workspace, treeVersion: 0 };
|
|
43
|
+
// 默认真实运行用全局唯一 id(ADR-0019);测试注入确定性 id
|
|
44
|
+
const nextId = options.idSeq ?? makeIdFactory();
|
|
45
|
+
const turns = new Map();
|
|
46
|
+
const clientView = createClientView(); // 每客户端在看哪个会话(ADR-0033)
|
|
47
|
+
state.versions = { web: webVersion(), engine: engine.version?.() ?? null, latest: null }; // 抬头显示的版本号
|
|
48
|
+
/** sendMessage 自引用:队列排空时要能再次调用自己(对象字面量还没构建完成 → 用 ref) */
|
|
49
|
+
let apiRef = null;
|
|
50
|
+
const pushSnapshot = () => options.emit.snapshot(buildSnapshot(state));
|
|
51
|
+
// 上游有没有新版本(抬头那行标红用):后台查,拿到值才推一帧;
|
|
52
|
+
// 离线/超时一直拿不到就一直是 null —— 界面不标红,功能一个不受影响
|
|
53
|
+
startUpstreamVersionWatch((latest) => {
|
|
54
|
+
state.versions = { ...state.versions, latest };
|
|
55
|
+
pushSnapshot();
|
|
56
|
+
});
|
|
57
|
+
// 历史会话清单的刷新节奏(ADR-0021 §1):来源可换(切引擎),节奏与缓存只有一份
|
|
58
|
+
const sessionsRefresher = createSessionsRefresher({
|
|
59
|
+
initial: options.piSessions ?? null,
|
|
60
|
+
state,
|
|
61
|
+
minIntervalMs: options.piSessionsMinIntervalMs,
|
|
62
|
+
now,
|
|
63
|
+
remember: (projects) => piOpener.remember(projects),
|
|
64
|
+
onChanged: pushSnapshot,
|
|
65
|
+
onError: (message) => notice(message, "warn"),
|
|
66
|
+
});
|
|
67
|
+
const piRefresher = { refresh: sessionsRefresher.refresh };
|
|
68
|
+
const services = createRuntimeServices({
|
|
69
|
+
options,
|
|
70
|
+
now,
|
|
71
|
+
nextId,
|
|
72
|
+
onNotice: (entry) => {
|
|
73
|
+
// 按 id upsert:同 key 的提示是「替换」原行,不能当新行追加(否则同一 id 出现两次)
|
|
74
|
+
state.notices = [...state.notices.filter((item) => item.id !== entry.id), entry].slice(-(options.maxNotices ?? 20));
|
|
75
|
+
options.emit.event({ type: "event", event: "notice", notice: entry });
|
|
76
|
+
pushSnapshot();
|
|
77
|
+
},
|
|
78
|
+
onPending: (nextPending) => {
|
|
79
|
+
state.pendingApproval = nextPending;
|
|
80
|
+
state.running.status = nextPending ? "awaiting_approval" : "streaming";
|
|
81
|
+
pushSnapshot();
|
|
82
|
+
},
|
|
83
|
+
isWaitingForUser: () => state.pendingApproval !== null,
|
|
84
|
+
});
|
|
85
|
+
const { notice, approval, watchdog } = services;
|
|
86
|
+
/** 更新会话元数据(只在内存里,不再有自建索引) */
|
|
87
|
+
const touchConversation = (conversationId, patch = {}) => touchConversationIn(state, conversationId, now(), patch);
|
|
88
|
+
const piOpener = createPiSessionOpener({
|
|
89
|
+
engine,
|
|
90
|
+
onModel: (model) => {
|
|
91
|
+
state.model = model;
|
|
92
|
+
pushSnapshot();
|
|
93
|
+
},
|
|
94
|
+
onError: (message) => notice(message, "error"),
|
|
95
|
+
});
|
|
96
|
+
const conversations = createConversationFactory({
|
|
97
|
+
state,
|
|
98
|
+
engine,
|
|
99
|
+
opener: piOpener,
|
|
100
|
+
nextId,
|
|
101
|
+
// 会话 id 与 pi 一致用 UUID;测试注入确定性 id 时仍走 `conv-N`(断言好写)
|
|
102
|
+
newConversationId: options.idSeq ? () => nextId("conv") : newConversationId,
|
|
103
|
+
now,
|
|
104
|
+
titleChars,
|
|
105
|
+
defaultModelFor: (workspace) => state.settings.defaultModels[workspace] ?? piOpener.projectModel(workspace), // 项目默认 > 该项目最近会话,见 conversation-create
|
|
106
|
+
onReady: (id) => {
|
|
107
|
+
void models.refresh(id);
|
|
108
|
+
void refreshStats();
|
|
109
|
+
void models.refreshThinking(id);
|
|
110
|
+
// 新会话可能刚让引擎创建了一个项目(codex):立刻重扫,侧栏不用等下一轮
|
|
111
|
+
void piRefresher.refresh(true);
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
const models = createModelController({
|
|
115
|
+
engine,
|
|
116
|
+
state,
|
|
117
|
+
notice: (text, level) => notice(text, level),
|
|
118
|
+
onChanged: pushSnapshot,
|
|
119
|
+
});
|
|
120
|
+
const turnContext = createTurnContext({
|
|
121
|
+
state,
|
|
122
|
+
now,
|
|
123
|
+
pushSnapshot,
|
|
124
|
+
emitEvent: (event) => options.emit.event(event),
|
|
125
|
+
notice,
|
|
126
|
+
onThinkingDelta: createThrottle(250, pushSnapshot), // 思考增量节流
|
|
127
|
+
appendRawText: (chunk, conversationId) => {
|
|
128
|
+
const turn = turns.get(conversationId);
|
|
129
|
+
if (!turn)
|
|
130
|
+
return "";
|
|
131
|
+
return ((turn.rawText += chunk), turn.filter.push(chunk));
|
|
132
|
+
},
|
|
133
|
+
// 收尾全过程都是同步的状态更新(settle 立刻写内存、刷新另起),所以返回的 Promise
|
|
134
|
+
// 不需要再被 await —— 以前留过一条「等落盘」的通道,但它把 Promise 挂在一个已经被
|
|
135
|
+
// 删除的 turns 条目上,永远读不到(死代码,已删)。
|
|
136
|
+
finish: (conversationId, reason) => void finishTurn(conversationId, reason),
|
|
137
|
+
});
|
|
138
|
+
const refreshStats = createStatsRefresher({
|
|
139
|
+
state,
|
|
140
|
+
pathOf: (id) => piOpener.pathOf(id),
|
|
141
|
+
// 引擎自带统计(dsh 的日志);pi / codex 走下面的文件路径兜底
|
|
142
|
+
statsOf: async (id) => (await engine.sessionStats?.(id)) ?? null,
|
|
143
|
+
onChanged: pushSnapshot,
|
|
144
|
+
});
|
|
145
|
+
const { queue, busy } = createTurnPlumbing({
|
|
146
|
+
state,
|
|
147
|
+
engine,
|
|
148
|
+
nextId,
|
|
149
|
+
appendUser: (id, value, steer) => {
|
|
150
|
+
const message = appendText(createMessage(nextId("msg"), "user", now()), value);
|
|
151
|
+
if (steer)
|
|
152
|
+
message.steer = true;
|
|
153
|
+
state.messages[id] = [...(state.messages[id] ?? []), message];
|
|
154
|
+
},
|
|
155
|
+
notice: (text, level) => notice(text, level),
|
|
156
|
+
pushSnapshot,
|
|
157
|
+
});
|
|
158
|
+
const finishTurn = createRuntimeTurnFinisher({
|
|
159
|
+
state,
|
|
160
|
+
turns,
|
|
161
|
+
nextId,
|
|
162
|
+
now,
|
|
163
|
+
shiftQueued: (conversationId) => queue.shift(conversationId),
|
|
164
|
+
sendNext: (message, conversationId) => setTimeout(() => void apiRef?.sendMessage(message.text, message.attachments, "now", conversationId), 0),
|
|
165
|
+
cancelPending: () => approval.cancelPending(),
|
|
166
|
+
onTurnFinished: options.onTurnFinished,
|
|
167
|
+
touchConversation: (conversationId) => touchConversation(conversationId),
|
|
168
|
+
refreshStats: () => void refreshStats(),
|
|
169
|
+
refreshProjects: () => void piRefresher.refresh(true),
|
|
170
|
+
notice: (text, level) => notice(text, level),
|
|
171
|
+
pushSnapshot,
|
|
172
|
+
});
|
|
173
|
+
const switchTo = createSwitchConversation({
|
|
174
|
+
state,
|
|
175
|
+
clientView,
|
|
176
|
+
infoOf: (id) => piOpener.infoOf(id),
|
|
177
|
+
open: (id, refresh) => piOpener.open(id, (force) => refresh(force ?? false)),
|
|
178
|
+
refreshSessions: (force) => piRefresher.refresh(force),
|
|
179
|
+
refreshModels: (id) => models.refresh(id),
|
|
180
|
+
refreshThinking: (id) => models.refreshThinking(id),
|
|
181
|
+
refreshStats: () => void refreshStats(),
|
|
182
|
+
// 切走的那条会话放开引擎写锁(codex):否则 Codex 桌面端打不开它
|
|
183
|
+
parkPrevious: (id) => {
|
|
184
|
+
if (turns.has(id))
|
|
185
|
+
return;
|
|
186
|
+
void engine.parkSession?.(id).catch(() => undefined);
|
|
187
|
+
},
|
|
188
|
+
pushSnapshot,
|
|
189
|
+
report: (text) => options.onStartupInfo?.(text), // 只写日志,不进对话流(切会话慢时靠它定位)
|
|
190
|
+
});
|
|
191
|
+
const refreshModels = createModelRefresher({ engine, state });
|
|
192
|
+
const refreshActions = createRefreshActions({
|
|
193
|
+
state,
|
|
194
|
+
turns,
|
|
195
|
+
engine,
|
|
196
|
+
notice,
|
|
197
|
+
pushSnapshot,
|
|
198
|
+
sendTurn: (prompt, conv) => sendTurn(prompt, undefined, "now", conv, undefined, { hidden: true }),
|
|
199
|
+
// 设置里的「最大超时」优先;没有/非法就退回启动参数(默认 20s)
|
|
200
|
+
timeoutMs: () => {
|
|
201
|
+
const seconds = state.settings.thinkTimeoutSec;
|
|
202
|
+
return Number.isFinite(seconds) && seconds > 0 ? seconds * 1000 : actionsTimeoutMs;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
const sendTurn = createTurnSender({
|
|
206
|
+
state,
|
|
207
|
+
engine,
|
|
208
|
+
now,
|
|
209
|
+
nextId,
|
|
210
|
+
titleChars,
|
|
211
|
+
turns,
|
|
212
|
+
watchdog,
|
|
213
|
+
turnContext,
|
|
214
|
+
notice,
|
|
215
|
+
pushSnapshot,
|
|
216
|
+
finishTurn: (id, reason) => finishTurn(id, reason),
|
|
217
|
+
busy,
|
|
218
|
+
createConversation: (seed) => conversations.create(seed),
|
|
219
|
+
openConversation: (id, refresh) => piOpener.open(id, refresh),
|
|
220
|
+
isOpen: (id) => piOpener.isOpen(id),
|
|
221
|
+
refreshProjects: (force) => piRefresher.refresh(force),
|
|
222
|
+
refreshModels: (id) => models.refresh(id),
|
|
223
|
+
refreshThinking: (id) => models.refreshThinking(id),
|
|
224
|
+
clientView,
|
|
225
|
+
touchConversation,
|
|
226
|
+
uploadStore: options.uploadStore,
|
|
227
|
+
});
|
|
228
|
+
/**
|
|
229
|
+
* 删除会话后的**本地清场**。
|
|
230
|
+
*
|
|
231
|
+
* 只把磁盘文件改名不够:内存里的会话/消息/排队项、pi-opener 的「已打开」记录、
|
|
232
|
+
* 引擎会话句柄都还在 —— 表现就是「删了还在侧栏、点进去还能读、还能发消息」。
|
|
233
|
+
*/
|
|
234
|
+
const discardConversation = (conversationId) => {
|
|
235
|
+
clientView.forget(conversationId);
|
|
236
|
+
piOpener.forget(conversationId);
|
|
237
|
+
state.conversations = state.conversations.filter((conversation) => conversation.id !== conversationId);
|
|
238
|
+
const { [conversationId]: _messages, ...remainingMessages } = state.messages;
|
|
239
|
+
state.messages = remainingMessages;
|
|
240
|
+
state.queued = state.queued.filter((item) => item.conversationId !== conversationId);
|
|
241
|
+
setRunning(state, conversationId, null);
|
|
242
|
+
turns.delete(conversationId);
|
|
243
|
+
if (state.activeId === conversationId) {
|
|
244
|
+
state.activeId = state.conversations[0]?.id ?? null;
|
|
245
|
+
state.running = emptyRunning();
|
|
246
|
+
state.actions = [];
|
|
247
|
+
}
|
|
248
|
+
// 引擎侧的句柄也放开:否则它继续占着会话(还能被写进那个已改名的文件)
|
|
249
|
+
Promise.resolve(engine.closeSession?.(conversationId)).catch(() => undefined);
|
|
250
|
+
};
|
|
251
|
+
const deleteSession = createSessionDeleter({
|
|
252
|
+
engine,
|
|
253
|
+
projects: () => state.piProjects,
|
|
254
|
+
handleOf: (id) => piOpener.handleOf(id),
|
|
255
|
+
discard: discardConversation,
|
|
256
|
+
refresh: () => piRefresher.refresh(true),
|
|
257
|
+
pushSnapshot,
|
|
258
|
+
});
|
|
259
|
+
const switchEngine = createEngineSwitcher({
|
|
260
|
+
state,
|
|
261
|
+
slot,
|
|
262
|
+
engines: options.engines ?? null,
|
|
263
|
+
sessionsSlot: sessionsRefresher.slot,
|
|
264
|
+
clientView,
|
|
265
|
+
clearSessions: () => piOpener.clear(),
|
|
266
|
+
approvalHandler: approval.handler,
|
|
267
|
+
refreshModels,
|
|
268
|
+
refreshSessions: (force) => piRefresher.refresh(force),
|
|
269
|
+
loadProviders: options.loadProviders ?? null,
|
|
270
|
+
isBusy: () => turns.size > 0,
|
|
271
|
+
notice,
|
|
272
|
+
pushSnapshot,
|
|
273
|
+
});
|
|
274
|
+
const api = {
|
|
275
|
+
async init() {
|
|
276
|
+
const ruleCount = await approval.loadRules(); // 「总是允许」规则只记日志,不占对话流
|
|
277
|
+
if (ruleCount > 0)
|
|
278
|
+
options.onStartupInfo?.(`已装载 ${ruleCount} 条审批规则(这些操作不会再询问)`);
|
|
279
|
+
state.settings = settings ? await settings.load() : defaultSettings();
|
|
280
|
+
await piRefresher.refresh(true);
|
|
281
|
+
await apiRef?.refreshProviders();
|
|
282
|
+
pushSnapshot();
|
|
283
|
+
},
|
|
284
|
+
async applySettings(patch) {
|
|
285
|
+
await applySettingsPatch({
|
|
286
|
+
state,
|
|
287
|
+
store: settings,
|
|
288
|
+
patch,
|
|
289
|
+
onError: (message) => notice(message, "warn"),
|
|
290
|
+
});
|
|
291
|
+
pushSnapshot();
|
|
292
|
+
return { ok: true };
|
|
293
|
+
},
|
|
294
|
+
refreshPiSessions: (force) => piRefresher.refresh(force),
|
|
295
|
+
refreshProviders: async () => {
|
|
296
|
+
await refreshModels(); // 模型表挂在会话上:先保证有会话,再重读
|
|
297
|
+
// provider 配置是 pi 独有的(models-store.json):别的引擎不碰这张表
|
|
298
|
+
const ownConfig = options.engines?.get(state.engine)?.descriptor.providerConfig !== false;
|
|
299
|
+
if (options.loadProviders && ownConfig)
|
|
300
|
+
state.providers = await options.loadProviders();
|
|
301
|
+
},
|
|
302
|
+
snapshot: () => buildSnapshot(state),
|
|
303
|
+
snapshotFor: createSnapshotFor({ state, clientView, now }),
|
|
304
|
+
clientView,
|
|
305
|
+
state: () => state,
|
|
306
|
+
isBusy() {
|
|
307
|
+
return turns.size > 0;
|
|
308
|
+
},
|
|
309
|
+
notify(text, level = "info") {
|
|
310
|
+
notice(text, level);
|
|
311
|
+
},
|
|
312
|
+
sendMessage: sendTurn,
|
|
313
|
+
async interrupt(requested) {
|
|
314
|
+
// 优先停用户点的那条(前端会把当前会话带上来),否则用当前会话,最后兜底任意一条
|
|
315
|
+
const fallback = (requested && turns.has(requested) ? requested : undefined) ??
|
|
316
|
+
(state.activeId !== null && turns.has(state.activeId) ? state.activeId : [...turns.keys()][0]);
|
|
317
|
+
const target = fallback ?? null;
|
|
318
|
+
if (!target) {
|
|
319
|
+
return { ok: false, error: { code: "session.idle", message: "当前没有在跑的回合" } };
|
|
320
|
+
}
|
|
321
|
+
await engine.interrupt(target);
|
|
322
|
+
return { ok: true };
|
|
323
|
+
},
|
|
324
|
+
approve(requestId, decision) {
|
|
325
|
+
const result = approval.approve(requestId, decision);
|
|
326
|
+
if (result.ok)
|
|
327
|
+
pushSnapshot();
|
|
328
|
+
return result;
|
|
329
|
+
},
|
|
330
|
+
// 没显式指定条数时用设置里的「一次生成几条」(手机端仍会自己传 3)
|
|
331
|
+
refreshActions: (conversationId, count) => refreshActions(conversationId, count ?? state.settings.thinkCount),
|
|
332
|
+
setModel: (modelId) => models.switchTo(state.activeId, modelId),
|
|
333
|
+
setThinkingLevel: (level) => models.setThinking(state.activeId, level),
|
|
334
|
+
reloadModels: () => models.reload(state.activeId),
|
|
335
|
+
cancelQueued: (queueId) => cancelQueuedWith(queue, queueId, notice),
|
|
336
|
+
pushSnapshot, // 供 provider 等模块「只推帧、不造提示」
|
|
337
|
+
setApprovalMode(mode) {
|
|
338
|
+
approval.setMode(mode);
|
|
339
|
+
state.approvalMode = mode;
|
|
340
|
+
pushSnapshot();
|
|
341
|
+
return { ok: true };
|
|
342
|
+
},
|
|
343
|
+
setEngine: (name) => switchEngine(name),
|
|
344
|
+
newConversation: newConversationWith({ create: conversations.create, clientView, notice, pushSnapshot }),
|
|
345
|
+
deleteSession: (conversationId) => deleteSession(conversationId),
|
|
346
|
+
renameSession: (conversationId, title) => renameSessionWith({
|
|
347
|
+
engine,
|
|
348
|
+
handleOf: (id) => piOpener.handleOf(id),
|
|
349
|
+
refresh: () => piRefresher.refresh(true),
|
|
350
|
+
onRenamed: (id, title) => touchConversation(id, { title }),
|
|
351
|
+
notice,
|
|
352
|
+
pushSnapshot,
|
|
353
|
+
})(conversationId, title),
|
|
354
|
+
switchConversation: (conversationId, clientId) => switchTo(conversationId, clientId),
|
|
355
|
+
};
|
|
356
|
+
apiRef = api; // 队列排空时要能再调 sendMessage
|
|
357
|
+
return api;
|
|
358
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 会话统计(顶部「会话信息」用):直接解析 pi 的会话 JSONL。
|
|
3
|
+
*
|
|
4
|
+
* 为什么从这里算而不是从内存事件累加:会话可能是**在 pi CLI 里开的**(ADR-0021),
|
|
5
|
+
* 或者是上次进程留下的历史。JSONL 是唯一的事实源,内存里没有那些轮的 usage。
|
|
6
|
+
*
|
|
7
|
+
* 只读、有上限(20MB):一个会话文件不该把服务端读卡。
|
|
8
|
+
*/
|
|
9
|
+
import { readFile, stat } from "node:fs/promises";
|
|
10
|
+
import { parseUsage } from "../util/usage.js";
|
|
11
|
+
export const MAX_STATS_BYTES = 20 * 1024 * 1024;
|
|
12
|
+
export function emptyStats() {
|
|
13
|
+
return {
|
|
14
|
+
userMessages: 0,
|
|
15
|
+
assistantMessages: 0,
|
|
16
|
+
toolCalls: 0,
|
|
17
|
+
toolResults: 0,
|
|
18
|
+
totalMessages: 0,
|
|
19
|
+
inputTokens: 0,
|
|
20
|
+
outputTokens: 0,
|
|
21
|
+
cacheRead: 0,
|
|
22
|
+
cacheWrite: 0,
|
|
23
|
+
totalTokens: 0,
|
|
24
|
+
contextTokens: 0,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** 纯函数:把若干行 JSONL 文本聚合成统计(坏行跳过,绝不抛) */
|
|
28
|
+
export function summarizeSession(lines) {
|
|
29
|
+
const stats = emptyStats();
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
if (line.trim() === "")
|
|
32
|
+
continue;
|
|
33
|
+
let record;
|
|
34
|
+
try {
|
|
35
|
+
record = JSON.parse(line);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const message = (record.message ?? record);
|
|
41
|
+
const role = typeof message.role === "string" ? message.role : "";
|
|
42
|
+
if (role === "user")
|
|
43
|
+
stats.userMessages += 1;
|
|
44
|
+
else if (role === "assistant")
|
|
45
|
+
stats.assistantMessages += 1;
|
|
46
|
+
else if (role === "toolResult" || role === "tool")
|
|
47
|
+
stats.toolResults += 1;
|
|
48
|
+
else
|
|
49
|
+
continue; // header / 其它事件不计
|
|
50
|
+
stats.totalMessages += 1;
|
|
51
|
+
const content = message.content;
|
|
52
|
+
if (Array.isArray(content)) {
|
|
53
|
+
for (const block of content) {
|
|
54
|
+
if (typeof block === "object" && block !== null && block.type === "toolCall")
|
|
55
|
+
stats.toolCalls += 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
if (role === "assistant") {
|
|
59
|
+
// 字段名各家不一样 → 统一口径解析(server/util/usage.ts)
|
|
60
|
+
const usage = parseUsage(message.usage);
|
|
61
|
+
if (usage) {
|
|
62
|
+
stats.inputTokens += usage.input;
|
|
63
|
+
stats.outputTokens += usage.output;
|
|
64
|
+
stats.cacheRead += usage.cacheRead;
|
|
65
|
+
stats.cacheWrite += usage.cacheWrite;
|
|
66
|
+
// 最近一条 assistant 的输入侧大小 ≈ 当前上下文占用
|
|
67
|
+
stats.contextTokens = usage.input + usage.cacheRead + usage.cacheWrite;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
stats.totalTokens = stats.inputTokens + stats.outputTokens + stats.cacheRead + stats.cacheWrite;
|
|
72
|
+
return stats;
|
|
73
|
+
}
|
|
74
|
+
/** 读一个会话文件并聚合;文件不存在 / 过大都返回 null(界面显示"未知"即可) */
|
|
75
|
+
export async function readSessionStats(sessionPath) {
|
|
76
|
+
if (!sessionPath)
|
|
77
|
+
return null;
|
|
78
|
+
try {
|
|
79
|
+
const info = await stat(sessionPath);
|
|
80
|
+
if (!info.isFile() || info.size > MAX_STATS_BYTES)
|
|
81
|
+
return null;
|
|
82
|
+
const text = await readFile(sessionPath, "utf8");
|
|
83
|
+
return summarizeSession(text.split("\n"));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
}
|