chainlesschain 0.162.157 → 0.162.159
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 +1 -1
- package/package.json +1 -1
- package/src/assets/web-panel/.build-hash +1 -1
- package/src/assets/web-panel/assets/{AIOps-CfMP2Oud.js → AIOps-ByIiF5bT.js} +1 -1
- package/src/assets/web-panel/assets/{ActionButton-Zu0N8BQL.js → ActionButton-NlgSFZq5.js} +1 -1
- package/src/assets/web-panel/assets/{Analytics-DZTwj5T_.js → Analytics-D17cOUcb.js} +3 -3
- package/src/assets/web-panel/assets/{AppLayout-CVFJh47v.js → AppLayout-B4mEZcbl.js} +5 -5
- package/src/assets/web-panel/assets/Artifacts-CASsmyQw.js +1 -0
- package/src/assets/web-panel/assets/Artifacts-CTVt96zW.css +1 -0
- package/src/assets/web-panel/assets/{Audit-BMfiLj7T.js → Audit-DYAia2e2.js} +1 -1
- package/src/assets/web-panel/assets/{BackgroundAgents-BslPCdGF.js → BackgroundAgents-C1lvYUSu.js} +1 -1
- package/src/assets/web-panel/assets/{Backup-CQ_fMC1r.js → Backup-B6agK6bA.js} +1 -1
- package/src/assets/web-panel/assets/{BaseInput-CsFsARiC.js → BaseInput-DM43R2vy.js} +1 -1
- package/src/assets/web-panel/assets/{Chat-CwfBobco.js → Chat-Dtdp3eXp.js} +6 -6
- package/src/assets/web-panel/assets/Chat-KSGPtU0L.css +1 -0
- package/src/assets/web-panel/assets/{ChatBubbleRenderer-K6Nf3xeN.js → ChatBubbleRenderer-BL5c_OQ1.js} +1 -1
- package/src/assets/web-panel/assets/{Checkbox-DkSyjjPp.js → Checkbox-D2xf7uZt.js} +1 -1
- package/src/assets/web-panel/assets/{Codegen-B7Fx3zZ6.js → Codegen-D3j45jJL.js} +1 -1
- package/src/assets/web-panel/assets/{Col-Dzfqn8Ep.js → Col-voPJ9fUZ.js} +1 -1
- package/src/assets/web-panel/assets/{Community-DhHPdzMM.js → Community-DEsyTESF.js} +1 -1
- package/src/assets/web-panel/assets/{Compact-Ct18_8y7.js → Compact-OHV_QXLD.js} +1 -1
- package/src/assets/web-panel/assets/{Compliance-CY64IqBi.js → Compliance-eDTJWC-J.js} +1 -1
- package/src/assets/web-panel/assets/{Cowork-BYSmWlUl.js → Cowork-BBvy-6d1.js} +2 -2
- package/src/assets/web-panel/assets/{Cron-_4xtP3Ug.js → Cron-Dw2eoJxu.js} +2 -2
- package/src/assets/web-panel/assets/{Crosschain-B4qY-Ycr.js → Crosschain-DQHms_Bh.js} +1 -1
- package/src/assets/web-panel/assets/{DID-CkCzWkx1.js → DID-Cyu7Id1c.js} +2 -2
- package/src/assets/web-panel/assets/{Dashboard-C1scT7Ke.js → Dashboard-sIDzkG92.js} +2 -2
- package/src/assets/web-panel/assets/{Dropdown-DBnQ2EWd.js → Dropdown-DUW3T2ey.js} +1 -1
- package/src/assets/web-panel/assets/{EmailListRenderer-C9JhOTq0.js → EmailListRenderer-CECELXW8.js} +1 -1
- package/src/assets/web-panel/assets/{FamilyGuardDashboard-CWXv5RUv.js → FamilyGuardDashboard-BIZR37ov.js} +1 -1
- package/src/assets/web-panel/assets/{Federation-fYdcbz9W.js → Federation-BEZIQX84.js} +1 -1
- package/src/assets/web-panel/assets/{FormItemContext-CJqOT3Yj.js → FormItemContext-BcdUMznL.js} +1 -1
- package/src/assets/web-panel/assets/{GenericCardRenderer-DGCsHc5K.js → GenericCardRenderer-DsN_fr5H.js} +1 -1
- package/src/assets/web-panel/assets/{Git-EH2K-H0Q.js → Git-63LhjBx2.js} +2 -2
- package/src/assets/web-panel/assets/{Governance-DJn1X2pR.js → Governance-CYtQtIcH.js} +1 -1
- package/src/assets/web-panel/assets/{Inference-Dch1E8qd.js → Inference-BwR6UUox.js} +1 -1
- package/src/assets/web-panel/assets/{KnowledgeGraph-QIur8j8-.js → KnowledgeGraph-D9lVNTUA.js} +1 -1
- package/src/assets/web-panel/assets/{Logs-DUgHldPg.js → Logs-C4q5h7Ut.js} +2 -2
- package/src/assets/web-panel/assets/MarkdownRenderer-DbzhFwvz.css +1 -0
- package/src/assets/web-panel/assets/MarkdownRenderer-DexLlXfN.js +1 -0
- package/src/assets/web-panel/assets/{Marketplace-DafrSfvd.js → Marketplace-Plksliuw.js} +1 -1
- package/src/assets/web-panel/assets/{McpTools-D2xRbfJx.js → McpTools-DZUFfeuG.js} +3 -3
- package/src/assets/web-panel/assets/{Memory-C6RbVHAo.js → Memory-NoDYt29V.js} +2 -2
- package/src/assets/web-panel/assets/{MobileBridge-BPtVBKqn.js → MobileBridge-CEXGTkdh.js} +1 -1
- package/src/assets/web-panel/assets/{MobileProjects-Cuw832cr.js → MobileProjects-DnbJkbZo.js} +1 -1
- package/src/assets/web-panel/assets/{Mtc-D6O9fWFa.js → Mtc-DTAa0cwy.js} +5 -5
- package/src/assets/web-panel/assets/{MtcAudit-CWZcXIH1.js → MtcAudit-CVV7LnNS.js} +4 -4
- package/src/assets/web-panel/assets/{Multisig-DhIBxTFd.js → Multisig-BqqGrjt_.js} +3 -3
- package/src/assets/web-panel/assets/{NLProgramming-wkBFS33e.js → NLProgramming-C4WP8DRp.js} +1 -1
- package/src/assets/web-panel/assets/{Notes-B09FxncA.js → Notes-Zo4DJUVI.js} +3 -3
- package/src/assets/web-panel/assets/{NotificationSettings-oQwtH6-9.js → NotificationSettings-DBcjRaz2.js} +1 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-BCQel9bD.js +1 -0
- package/src/assets/web-panel/assets/{Organization-CsF-NIjz.js → Organization-BT9WPBOX.js} +4 -4
- package/src/assets/web-panel/assets/{Overflow-bLxv9Dqi.js → Overflow-D6b4bZ3q.js} +1 -1
- package/src/assets/web-panel/assets/{P2P-DVwQtm4q.js → P2P-Cw3OsYx-.js} +2 -2
- package/src/assets/web-panel/assets/{PdhVaultBrowser-Bgic3QKm.js → PdhVaultBrowser-C0nyiK2b.js} +4 -4
- package/src/assets/web-panel/assets/{Permissions-as80-9ep.js → Permissions-zVBZEB_C.js} +3 -3
- package/src/assets/web-panel/assets/{PersonalDataHub-BGmUw36a.js → PersonalDataHub-BOspsFew.js} +2 -2
- package/src/assets/web-panel/assets/{Pipeline-BM96zBxG.js → Pipeline-CQWfZ4XH.js} +1 -1
- package/src/assets/web-panel/assets/{Privacy-BBOzBSWe.js → Privacy-C2oGJ_WL.js} +1 -1
- package/src/assets/web-panel/assets/{ProjectInit-GHX41V28.js → ProjectInit-BZMR4qY1.js} +2 -2
- package/src/assets/web-panel/assets/{ProjectSettings-DraqzxKD.js → ProjectSettings-DAujoP63.js} +2 -2
- package/src/assets/web-panel/assets/{Projects-BzzdUml4.js → Projects-DvH_P2Uq.js} +1 -1
- package/src/assets/web-panel/assets/{Providers-CSeYNAtW.js → Providers-D_RGHM-v.js} +1 -1
- package/src/assets/web-panel/assets/{QrScannerModal-DlzBCdXh.js → QrScannerModal-Mf6KfL2J.js} +1 -1
- package/src/assets/web-panel/assets/{QuickAsk-p8iB7od_.js → QuickAsk-5D8URoI_.js} +1 -1
- package/src/assets/web-panel/assets/{Recommend-DjOVXacd.js → Recommend-Df4kC8Gy.js} +1 -1
- package/src/assets/web-panel/assets/RemoteSession-DGi3f3vO.js +4 -0
- package/src/assets/web-panel/assets/{Reputation-CjT3M0Di.js → Reputation-jmkGFNWb.js} +1 -1
- package/src/assets/web-panel/assets/{Row-tw8m6T0c.js → Row-DP6Pcu3m.js} +1 -1
- package/src/assets/web-panel/assets/{RssFeed-B47KBx_f.js → RssFeed-D60hSuPO.js} +3 -3
- package/src/assets/web-panel/assets/{Search-D2sx1PBe.js → Search-0mf1BQUK.js} +1 -1
- package/src/assets/web-panel/assets/{Security-JmtL8g7D.js → Security-CfZtLFYV.js} +3 -3
- package/src/assets/web-panel/assets/{Services-BxrNqjin.js → Services-D9zpZLWz.js} +2 -2
- package/src/assets/web-panel/assets/{Skeleton-udk8L18t.js → Skeleton-D1_44eMD.js} +1 -1
- package/src/assets/web-panel/assets/{Skills-DooLT9of.js → Skills-cVFa8zkz.js} +1 -1
- package/src/assets/web-panel/assets/{Sla-ITVZE1xI.js → Sla-Cinpzc3L.js} +1 -1
- package/src/assets/web-panel/assets/{SpeechSettings-DCkfqY1n.js → SpeechSettings-bqdiJE_P.js} +1 -1
- package/src/assets/web-panel/assets/{SyncSettings-CkNlgFjM.js → SyncSettings-Dgl_SLgj.js} +2 -2
- package/src/assets/web-panel/assets/{Tasks-BMAoyCM_.js → Tasks-DawWAEhy.js} +1 -1
- package/src/assets/web-panel/assets/{Templates-BnrFTlrz.js → Templates-Ca0nY651.js} +1 -1
- package/src/assets/web-panel/assets/{Tenant-BjNsy3-i.js → Tenant-CgaGEhLm.js} +1 -1
- package/src/assets/web-panel/assets/{Terminal-C0gxLL7V.js → Terminal-D-9EnpRx.js} +2 -2
- package/src/assets/web-panel/assets/{TimelineRenderer-CUtj_KHA.js → TimelineRenderer-CSOxDHZ6.js} +1 -1
- package/src/assets/web-panel/assets/{Tokens-DOg61zlq.js → Tokens-BpwLZpL3.js} +1 -1
- package/src/assets/web-panel/assets/{Trigger-D4ldCewj.js → Trigger-DgowwBK5.js} +1 -1
- package/src/assets/web-panel/assets/{Trust-3lk_Zd8j.js → Trust-BZutapjE.js} +1 -1
- package/src/assets/web-panel/assets/{UkeySign-kWhFR6Li.js → UkeySign-QZPt8_Sp.js} +1 -1
- package/src/assets/web-panel/assets/{VideoEditing-D3GOFVWg.js → VideoEditing-DlQx5UDJ.js} +1 -1
- package/src/assets/web-panel/assets/{Wallet-DzrZfBuh.js → Wallet-ZfdTgtWA.js} +4 -4
- package/src/assets/web-panel/assets/{WebAuthn-CyVJVp_g.js → WebAuthn-CAp0y_Cr.js} +5 -5
- package/src/assets/web-panel/assets/{WorkflowEditor-i_b0wi9b.js → WorkflowEditor-BVITZEmr.js} +1 -1
- package/src/assets/web-panel/assets/{chat-DwPCK8I7.js → chat-CC2tPaNg.js} +1 -1
- package/src/assets/web-panel/assets/{colors-D0vaVqZU.js → colors-31brc9CA.js} +1 -1
- package/src/assets/web-panel/assets/{compact-item-Don7nGW2.js → compact-item-DLMkC9C-.js} +1 -1
- package/src/assets/web-panel/assets/{createContext-D84R5qy2.js → createContext-BMtveI6F.js} +1 -1
- package/src/assets/web-panel/assets/devWarning-CB8pPOtd.js +1 -0
- package/src/assets/web-panel/assets/{hasIn-BeUqVVKU.js → hasIn-DbD6WHW2.js} +1 -1
- package/src/assets/web-panel/assets/{index-m9bn0F1p.js → index-3oBsXZmF.js} +1 -1
- package/src/assets/web-panel/assets/{index-CfYRv-zA.js → index-ALvoNTRF.js} +1 -1
- package/src/assets/web-panel/assets/{index-4MkfPLGv.js → index-AaIKG60c.js} +1 -1
- package/src/assets/web-panel/assets/{index-LMo9bBLJ.js → index-BFRLmCR3.js} +1 -1
- package/src/assets/web-panel/assets/{index-wesQQ2cH.js → index-BKAB6-6V.js} +1 -1
- package/src/assets/web-panel/assets/{index-CzT56x-_.js → index-BOM-VFlv.js} +1 -1
- package/src/assets/web-panel/assets/{index-L73NrmlW.js → index-BPpS-lGb.js} +1 -1
- package/src/assets/web-panel/assets/{index-WRw1pM5E.js → index-Bbc2JeQG.js} +1 -1
- package/src/assets/web-panel/assets/index-BhXiaiev.js +1 -0
- package/src/assets/web-panel/assets/{index-Br3SOdER.js → index-BiRQUoyE.js} +1 -1
- package/src/assets/web-panel/assets/{index-D8r7WKk-.js → index-BnFLzejT.js} +1 -1
- package/src/assets/web-panel/assets/{index-CAdCTTBC.js → index-BoO4WEJB.js} +1 -1
- package/src/assets/web-panel/assets/{index-BlfTf0ka.js → index-C-cGlCDW.js} +1 -1
- package/src/assets/web-panel/assets/{index-B57al0Z5.js → index-C1PfeI88.js} +1 -1
- package/src/assets/web-panel/assets/{index-D4b5XqIy.js → index-C21pdBqs.js} +1 -1
- package/src/assets/web-panel/assets/{index-CnnxzIx0.js → index-C7LHNw_N.js} +1 -1
- package/src/assets/web-panel/assets/{index-BNpJf-sf.js → index-CEXa-4zd.js} +1 -1
- package/src/assets/web-panel/assets/{index-DT40zd7K.js → index-CEwjBA9q.js} +1 -1
- package/src/assets/web-panel/assets/{index-CL71rIwo.js → index-CFXKbOek.js} +1 -1
- package/src/assets/web-panel/assets/{index-BZ1N07Sp.js → index-CJhREOlG.js} +1 -1
- package/src/assets/web-panel/assets/{index-C0_sKVi7.js → index-COHHQ9cU.js} +1 -1
- package/src/assets/web-panel/assets/{index-DBzBLX-K.js → index-CUmLMFsy.js} +1 -1
- package/src/assets/web-panel/assets/{index-De85D9Pc.js → index-CWgfipcC.js} +1 -1
- package/src/assets/web-panel/assets/{index-x7VYlV41.js → index-C_eUKGYP.js} +1 -1
- package/src/assets/web-panel/assets/{index-hjgrIX5I.js → index-CcVszn2b.js} +1 -1
- package/src/assets/web-panel/assets/{index-DMT8n9vB.js → index-Cod97Urs.js} +3 -3
- package/src/assets/web-panel/assets/{index-DFFzwrNs.js → index-Cwbqh0KZ.js} +1 -1
- package/src/assets/web-panel/assets/{index-YKLCotoo.js → index-D67kIagl.js} +1 -1
- package/src/assets/web-panel/assets/{index-IGGttsCv.js → index-D7cV0tu0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DQor_7NF.js → index-DJvxFyIz.js} +1 -1
- package/src/assets/web-panel/assets/{index-D84BnuFg.js → index-DZl-UXB6.js} +1 -1
- package/src/assets/web-panel/assets/{index-CrnVEEQO.js → index-Dm0byjm1.js} +1 -1
- package/src/assets/web-panel/assets/{index-BER98o4P.js → index-DndNnYc0.js} +1 -1
- package/src/assets/web-panel/assets/{index-DA0-EJKx.js → index-WQUO2vvK.js} +1 -1
- package/src/assets/web-panel/assets/index-WpaYaE3D.js +1 -0
- package/src/assets/web-panel/assets/{index-CExWtm98.js → index-bdaJwTXJ.js} +1 -1
- package/src/assets/web-panel/assets/{index-MKZrJrP_.js → index-ep6JchNG.js} +1 -1
- package/src/assets/web-panel/assets/{index-7QUwcGTg.js → index-jG6h22Qg.js} +1 -1
- package/src/assets/web-panel/assets/{index-BohrJpBf.js → index-ntVQqSML.js} +1 -1
- package/src/assets/web-panel/assets/{initDefaultProps-DGeU4vPS.js → initDefaultProps-D5UoBRhN.js} +1 -1
- package/src/assets/web-panel/assets/{motion-Dgqj9ab3.js → motion-DltOw0CR.js} +1 -1
- package/src/assets/web-panel/assets/{move-BFyEU6Lr.js → move-_oPkk5zj.js} +1 -1
- package/src/assets/web-panel/assets/{mtc-parser-CueCXXPC.js → mtc-parser-EES8ROCS.js} +1 -1
- package/src/assets/web-panel/assets/{omit-D2pU5OyE.js → omit-B-_39x_M.js} +1 -1
- package/src/assets/web-panel/assets/{pickAttrs-CKTf6zD6.js → pickAttrs-l6D0yJHz.js} +1 -1
- package/src/assets/web-panel/assets/{placementArrow-DieJaGCN.js → placementArrow-DIeUK31L.js} +1 -1
- package/src/assets/web-panel/assets/{responsiveObserve-Bxr-rmU6.js → responsiveObserve-dG4EzUQo.js} +1 -1
- package/src/assets/web-panel/assets/{slide-CDEDk3Se.js → slide-DWFzYGdH.js} +1 -1
- package/src/assets/web-panel/assets/{statusUtils-icf2eCTx.js → statusUtils-CECdTA5H.js} +1 -1
- package/src/assets/web-panel/assets/{styleChecker-qKvgmBhE.js → styleChecker-D6kP-mMK.js} +1 -1
- package/src/assets/web-panel/assets/{useFlexGapSupport-1_UhiyJZ.js → useFlexGapSupport-BWfna_rd.js} +1 -1
- package/src/assets/web-panel/assets/{useFs-CFGLXDWh.js → useFs-Bb-j6rzo.js} +1 -1
- package/src/assets/web-panel/assets/{usePersonalDataHub-UafOXvQ6.js → usePersonalDataHub-D5OIsYM4.js} +1 -1
- package/src/assets/web-panel/assets/{vnode-BfPqSx79.js → vnode-DNpwzFTM.js} +1 -1
- package/src/assets/web-panel/assets/{zoom-CXsVcRB6.js → zoom-vNLpFQBW.js} +1 -1
- package/src/assets/web-panel/index.html +1 -1
- package/src/command-manifest.json +17 -3
- package/src/commands/agent.js +11 -1
- package/src/commands/background-session.js +143 -3
- package/src/commands/browse.js +86 -0
- package/src/commands/cloud.js +232 -0
- package/src/commands/doctor.js +61 -6
- package/src/commands/mcp.js +61 -0
- package/src/commands/plugin.js +63 -1
- package/src/commands/routine.js +367 -0
- package/src/commands/session.js +326 -0
- package/src/data/changelog.json +12 -2
- package/src/harness/jsonl-session-store.js +82 -2
- package/src/harness/session-index.js +295 -0
- package/src/harness/session-mirror.js +142 -0
- package/src/harness/transcript-integrity.js +182 -0
- package/src/index.js +8 -0
- package/src/lib/background-agent-supervisor.js +126 -2
- package/src/lib/channels/channel-manager.js +126 -0
- package/src/lib/channels/telegram-channel.js +112 -0
- package/src/lib/channels/webhook-channel.js +140 -0
- package/src/lib/chrome-connector.js +425 -4
- package/src/lib/cloud/bundle.js +110 -0
- package/src/lib/cloud/cloud-client.js +152 -0
- package/src/lib/computer-use/authorization.js +173 -0
- package/src/lib/computer-use/computer-use-server.js +276 -0
- package/src/lib/computer-use/control-backend.js +150 -0
- package/src/lib/doctor-checkup.js +652 -0
- package/src/lib/jsonl-session-store.js +4 -0
- package/src/lib/lsp/lsp-server-registry.js +48 -21
- package/src/lib/plugin-runtime/governance.js +174 -0
- package/src/lib/pr-link-ledger.js +221 -0
- package/src/lib/remote-control.js +35 -0
- package/src/lib/routine-store.js +352 -0
- package/src/lib/session-store-guard.js +40 -0
- package/src/lib/session-usage.js +318 -2
- package/src/lib/sub-agent-context.js +23 -0
- package/src/lib/web-ui-server.js +120 -7
- package/src/repl/agent-repl.js +592 -61
- package/src/repl/bg-dashboard.js +355 -0
- package/src/repl/fast-mode.js +103 -0
- package/src/repl/fullscreen-tui.js +197 -0
- package/src/repl/remote-approval.js +111 -0
- package/src/repl/slash-command-registry.js +11 -3
- package/src/repl/voice-dictation.js +132 -0
- package/src/runtime/agent-core.js +278 -1
- package/src/runtime/coding-agent-contract-shared.cjs +141 -0
- package/src/runtime/coding-agent-policy.cjs +33 -0
- package/src/runtime/headless-runner.js +71 -7
- package/src/runtime/headless-stream.js +14 -8
- package/src/runtime/policies/agent-policy.js +6 -0
- package/src/assets/web-panel/assets/Artifacts-BbN5V7au.js +0 -1
- package/src/assets/web-panel/assets/Artifacts-CRkepl4t.css +0 -1
- package/src/assets/web-panel/assets/Chat-DDh58PCf.css +0 -1
- package/src/assets/web-panel/assets/OrderTableRenderer-iFKeNf6Y.js +0 -1
- package/src/assets/web-panel/assets/RemoteSession-CIGk7gk-.js +0 -4
- package/src/assets/web-panel/assets/devWarning-BBwWehQ7.js +0 -1
- package/src/assets/web-panel/assets/index-DJ8etkBT.js +0 -1
- package/src/assets/web-panel/assets/index-OeaK-zXZ.js +0 -1
package/src/lib/session-usage.js
CHANGED
|
@@ -31,6 +31,30 @@ function toNumber(v) {
|
|
|
31
31
|
return Number.isFinite(n) ? n : 0;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Extract the optional usage-attribution frame (用量归因) persisted alongside
|
|
36
|
+
* a token_usage event's data: `{ origin, skill?, subagentId?, role?,
|
|
37
|
+
* parentSessionId?, depth? }`. Written by the agent drivers for sub-agent /
|
|
38
|
+
* isolated-skill usage; absent on legacy events and on plain main-loop usage
|
|
39
|
+
* (absence ⇒ origin "main"). Returns a normalized copy or null.
|
|
40
|
+
*/
|
|
41
|
+
export function extractAttribution(event) {
|
|
42
|
+
const a = event?.data?.attribution;
|
|
43
|
+
if (!a || typeof a !== "object") return null;
|
|
44
|
+
const origin = typeof a.origin === "string" && a.origin ? a.origin : null;
|
|
45
|
+
if (!origin) return null;
|
|
46
|
+
const out = { origin };
|
|
47
|
+
if (typeof a.skill === "string" && a.skill) out.skill = a.skill;
|
|
48
|
+
if (typeof a.subagentId === "string" && a.subagentId)
|
|
49
|
+
out.subagentId = a.subagentId;
|
|
50
|
+
if (typeof a.role === "string" && a.role) out.role = a.role;
|
|
51
|
+
if (typeof a.parentSessionId === "string" && a.parentSessionId)
|
|
52
|
+
out.parentSessionId = a.parentSessionId;
|
|
53
|
+
const depth = Number(a.depth);
|
|
54
|
+
if (Number.isFinite(depth)) out.depth = depth;
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
34
58
|
/**
|
|
35
59
|
* Extract a normalized usage record from a single event, or null if none.
|
|
36
60
|
* Accepts both snake_case (OpenAI/Anthropic) and camelCase variants.
|
|
@@ -75,6 +99,9 @@ export function extractUsage(event) {
|
|
|
75
99
|
return null;
|
|
76
100
|
}
|
|
77
101
|
|
|
102
|
+
// Attribution rides along ONLY when present, so records built from legacy
|
|
103
|
+
// events stay byte-identical (callers using exact-equality keep passing).
|
|
104
|
+
const attribution = extractAttribution(event);
|
|
78
105
|
return {
|
|
79
106
|
provider: d.provider || raw.provider || null,
|
|
80
107
|
model: d.model || raw.model || null,
|
|
@@ -84,6 +111,223 @@ export function extractUsage(event) {
|
|
|
84
111
|
cacheReadTokens,
|
|
85
112
|
cacheCreationTokens,
|
|
86
113
|
timestamp: event.timestamp || null,
|
|
114
|
+
...(attribution ? { attribution } : {}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Usage attribution roll-ups (用量归因) ───────────────────────────────────
|
|
119
|
+
|
|
120
|
+
function _newSums() {
|
|
121
|
+
return {
|
|
122
|
+
inputTokens: 0,
|
|
123
|
+
outputTokens: 0,
|
|
124
|
+
totalTokens: 0,
|
|
125
|
+
cacheReadTokens: 0,
|
|
126
|
+
cacheCreationTokens: 0,
|
|
127
|
+
calls: 0,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function _addSums(sums, u) {
|
|
132
|
+
sums.inputTokens += u.inputTokens || 0;
|
|
133
|
+
sums.outputTokens += u.outputTokens || 0;
|
|
134
|
+
sums.totalTokens += u.totalTokens || 0;
|
|
135
|
+
sums.cacheReadTokens += u.cacheReadTokens || 0;
|
|
136
|
+
sums.cacheCreationTokens += u.cacheCreationTokens || 0;
|
|
137
|
+
sums.calls += u.calls != null ? u.calls : 1;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const _byTokensDesc = (a, b) => b.totalTokens - a.totalTokens;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Roll usage events up by attribution: origin (main | subagent | skill —
|
|
144
|
+
* events without an attribution frame count as "main", which keeps every
|
|
145
|
+
* pre-attribution transcript aggregating exactly as before), by skill name,
|
|
146
|
+
* and by individual sub-agent.
|
|
147
|
+
*/
|
|
148
|
+
export function aggregateUsageAttribution(events) {
|
|
149
|
+
const byOrigin = new Map();
|
|
150
|
+
const bySkill = new Map();
|
|
151
|
+
const bySubagent = new Map();
|
|
152
|
+
|
|
153
|
+
for (const evt of events || []) {
|
|
154
|
+
const u = extractUsage(evt);
|
|
155
|
+
if (!u) continue;
|
|
156
|
+
const attr = u.attribution || null;
|
|
157
|
+
const origin = attr?.origin || "main";
|
|
158
|
+
|
|
159
|
+
const o = byOrigin.get(origin) || { origin, ..._newSums() };
|
|
160
|
+
_addSums(o, u);
|
|
161
|
+
byOrigin.set(origin, o);
|
|
162
|
+
|
|
163
|
+
if (origin === "skill") {
|
|
164
|
+
const key = attr?.skill || "?";
|
|
165
|
+
const s = bySkill.get(key) || { skill: key, ..._newSums() };
|
|
166
|
+
_addSums(s, u);
|
|
167
|
+
bySkill.set(key, s);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (attr?.subagentId) {
|
|
171
|
+
const key = attr.subagentId;
|
|
172
|
+
const r = bySubagent.get(key) || {
|
|
173
|
+
subagentId: key,
|
|
174
|
+
role: attr.role || null,
|
|
175
|
+
origin,
|
|
176
|
+
..._newSums(),
|
|
177
|
+
};
|
|
178
|
+
_addSums(r, u);
|
|
179
|
+
bySubagent.set(key, r);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
byOrigin: Array.from(byOrigin.values()).sort(_byTokensDesc),
|
|
185
|
+
bySkill: Array.from(bySkill.values()).sort(_byTokensDesc),
|
|
186
|
+
bySubagent: Array.from(bySubagent.values()).sort(_byTokensDesc),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* MCP tools reach the loop as `mcp__<server>__<tool>`; returns the server
|
|
192
|
+
* segment or null for a non-MCP tool name.
|
|
193
|
+
*/
|
|
194
|
+
export function mcpServerOf(toolName) {
|
|
195
|
+
if (typeof toolName !== "string" || !toolName.startsWith("mcp__"))
|
|
196
|
+
return null;
|
|
197
|
+
const rest = toolName.slice(5);
|
|
198
|
+
const sep = rest.indexOf("__");
|
|
199
|
+
return sep > 0 ? rest.slice(0, sep) : null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Lenient tool_result error detection (mirrors session-insights). */
|
|
203
|
+
function _isToolResultError(event) {
|
|
204
|
+
const r = event?.data?.result;
|
|
205
|
+
return Boolean(
|
|
206
|
+
event?.data?.error ||
|
|
207
|
+
(r && typeof r === "object" && (r.error || r.is_error || r.isError)),
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Aggregate the transcript's tool events: per-tool call/error counts, an MCP
|
|
213
|
+
* server bucket (`mcp__<server>__*`), and a turn-associated token figure.
|
|
214
|
+
*
|
|
215
|
+
* `turnTokens` is an APPROXIMATION: turns are delimited by `user_message`
|
|
216
|
+
* events, and every token_usage event inside a turn is attributed to EVERY
|
|
217
|
+
* tool that ran in that turn ("tokens of turns that used tool X"). A turn's
|
|
218
|
+
* tokens therefore count once per distinct tool — do NOT sum turnTokens
|
|
219
|
+
* across tools/servers, it is not a partition. Headless sessions persist one
|
|
220
|
+
* aggregated usage event per run, so there the whole run's tokens associate
|
|
221
|
+
* with each tool the run used.
|
|
222
|
+
*
|
|
223
|
+
* Accepts both the compact `tool_call` shape ({tool, is_error, skill?}) the
|
|
224
|
+
* drivers write now and the legacy `tool_call`({tool, args}) + `tool_result`
|
|
225
|
+
* pair (errors read off the result, calls only off tool_call — no double
|
|
226
|
+
* count). Sessions without tool events aggregate to zeros.
|
|
227
|
+
*/
|
|
228
|
+
export function aggregateToolCalls(events) {
|
|
229
|
+
const byTool = new Map();
|
|
230
|
+
const byServer = new Map();
|
|
231
|
+
let totalCalls = 0;
|
|
232
|
+
let totalErrors = 0;
|
|
233
|
+
|
|
234
|
+
const bumpTool = (name) => {
|
|
235
|
+
let t = byTool.get(name);
|
|
236
|
+
if (!t) {
|
|
237
|
+
t = {
|
|
238
|
+
tool: name,
|
|
239
|
+
mcpServer: mcpServerOf(name),
|
|
240
|
+
calls: 0,
|
|
241
|
+
errors: 0,
|
|
242
|
+
turnTokens: 0,
|
|
243
|
+
};
|
|
244
|
+
byTool.set(name, t);
|
|
245
|
+
}
|
|
246
|
+
return t;
|
|
247
|
+
};
|
|
248
|
+
const bumpServer = (server) => {
|
|
249
|
+
let s = byServer.get(server);
|
|
250
|
+
if (!s) {
|
|
251
|
+
s = { server, calls: 0, errors: 0, turnTokens: 0 };
|
|
252
|
+
byServer.set(server, s);
|
|
253
|
+
}
|
|
254
|
+
return s;
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
let turnTools = new Set();
|
|
258
|
+
let turnServers = new Set();
|
|
259
|
+
let turnTokens = 0;
|
|
260
|
+
const flushTurn = () => {
|
|
261
|
+
if (turnTokens > 0) {
|
|
262
|
+
for (const name of turnTools) bumpTool(name).turnTokens += turnTokens;
|
|
263
|
+
for (const server of turnServers)
|
|
264
|
+
bumpServer(server).turnTokens += turnTokens;
|
|
265
|
+
}
|
|
266
|
+
turnTools = new Set();
|
|
267
|
+
turnServers = new Set();
|
|
268
|
+
turnTokens = 0;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
for (const evt of events || []) {
|
|
272
|
+
if (!evt || typeof evt !== "object") continue;
|
|
273
|
+
if (evt.type === "user_message") {
|
|
274
|
+
flushTurn();
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (evt.type === "tool_call") {
|
|
278
|
+
const name = evt.data?.tool || "?";
|
|
279
|
+
totalCalls++;
|
|
280
|
+
const t = bumpTool(name);
|
|
281
|
+
t.calls++;
|
|
282
|
+
const isErr = evt.data?.is_error === true;
|
|
283
|
+
if (isErr) {
|
|
284
|
+
totalErrors++;
|
|
285
|
+
t.errors++;
|
|
286
|
+
}
|
|
287
|
+
turnTools.add(name);
|
|
288
|
+
const server = mcpServerOf(name);
|
|
289
|
+
if (server) {
|
|
290
|
+
const s = bumpServer(server);
|
|
291
|
+
s.calls++;
|
|
292
|
+
if (isErr) s.errors++;
|
|
293
|
+
turnServers.add(server);
|
|
294
|
+
}
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
if (evt.type === "tool_result") {
|
|
298
|
+
const name = evt.data?.tool || "?";
|
|
299
|
+
turnTools.add(name);
|
|
300
|
+
const server = mcpServerOf(name);
|
|
301
|
+
if (server) turnServers.add(server);
|
|
302
|
+
if (_isToolResultError(evt)) {
|
|
303
|
+
totalErrors++;
|
|
304
|
+
bumpTool(name).errors++;
|
|
305
|
+
if (server) bumpServer(server).errors++;
|
|
306
|
+
}
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
const u = extractUsage(evt);
|
|
310
|
+
if (u) turnTokens += u.totalTokens;
|
|
311
|
+
}
|
|
312
|
+
flushTurn();
|
|
313
|
+
|
|
314
|
+
const byCallsDesc = (a, b) => b.calls - a.calls;
|
|
315
|
+
return {
|
|
316
|
+
totalCalls,
|
|
317
|
+
totalErrors,
|
|
318
|
+
byTool: Array.from(byTool.values()).sort(byCallsDesc),
|
|
319
|
+
byMcpServer: Array.from(byServer.values()).sort(byCallsDesc),
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Full attribution section for a transcript: origin/skill/subagent usage
|
|
325
|
+
* roll-ups + the tool/MCP call aggregation. Pure.
|
|
326
|
+
*/
|
|
327
|
+
export function sessionAttribution(events) {
|
|
328
|
+
return {
|
|
329
|
+
...aggregateUsageAttribution(events),
|
|
330
|
+
tools: aggregateToolCalls(events),
|
|
87
331
|
};
|
|
88
332
|
}
|
|
89
333
|
|
|
@@ -141,12 +385,13 @@ export function aggregateUsage(events) {
|
|
|
141
385
|
}
|
|
142
386
|
|
|
143
387
|
/**
|
|
144
|
-
* Roll up usage for a single JSONL session.
|
|
388
|
+
* Roll up usage for a single JSONL session. The pre-attribution keys
|
|
389
|
+
* (total/byModel) are unchanged; `attribution` is additive.
|
|
145
390
|
*/
|
|
146
391
|
export function sessionUsage(sessionId) {
|
|
147
392
|
const events = _deps.readEvents(sessionId);
|
|
148
393
|
const agg = aggregateUsage(events);
|
|
149
|
-
return { sessionId, ...agg };
|
|
394
|
+
return { sessionId, ...agg, attribution: sessionAttribution(events) };
|
|
150
395
|
}
|
|
151
396
|
|
|
152
397
|
/**
|
|
@@ -206,6 +451,66 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
|
|
|
206
451
|
}
|
|
207
452
|
}
|
|
208
453
|
|
|
454
|
+
// Global attribution roll-up: merge each session's attribution rows by key.
|
|
455
|
+
const gOrigin = new Map();
|
|
456
|
+
const gSkill = new Map();
|
|
457
|
+
const gSubagent = new Map();
|
|
458
|
+
const gTool = new Map();
|
|
459
|
+
const gServer = new Map();
|
|
460
|
+
let gToolCalls = 0;
|
|
461
|
+
let gToolErrors = 0;
|
|
462
|
+
const mergeRow = (map, key, row, seed) => {
|
|
463
|
+
const entry = map.get(key) || { ...seed, ..._newSums() };
|
|
464
|
+
_addSums(entry, row);
|
|
465
|
+
map.set(key, entry);
|
|
466
|
+
};
|
|
467
|
+
for (const s of perSession) {
|
|
468
|
+
const a = s.attribution;
|
|
469
|
+
if (!a) continue;
|
|
470
|
+
for (const row of a.byOrigin || []) {
|
|
471
|
+
mergeRow(gOrigin, row.origin, row, { origin: row.origin });
|
|
472
|
+
}
|
|
473
|
+
for (const row of a.bySkill || []) {
|
|
474
|
+
mergeRow(gSkill, row.skill, row, { skill: row.skill });
|
|
475
|
+
}
|
|
476
|
+
for (const row of a.bySubagent || []) {
|
|
477
|
+
mergeRow(gSubagent, row.subagentId, row, {
|
|
478
|
+
subagentId: row.subagentId,
|
|
479
|
+
role: row.role || null,
|
|
480
|
+
origin: row.origin,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
const tools = a.tools || {};
|
|
484
|
+
gToolCalls += tools.totalCalls || 0;
|
|
485
|
+
gToolErrors += tools.totalErrors || 0;
|
|
486
|
+
for (const row of tools.byTool || []) {
|
|
487
|
+
const entry = gTool.get(row.tool) || {
|
|
488
|
+
tool: row.tool,
|
|
489
|
+
mcpServer: row.mcpServer || null,
|
|
490
|
+
calls: 0,
|
|
491
|
+
errors: 0,
|
|
492
|
+
turnTokens: 0,
|
|
493
|
+
};
|
|
494
|
+
entry.calls += row.calls || 0;
|
|
495
|
+
entry.errors += row.errors || 0;
|
|
496
|
+
entry.turnTokens += row.turnTokens || 0;
|
|
497
|
+
gTool.set(row.tool, entry);
|
|
498
|
+
}
|
|
499
|
+
for (const row of tools.byMcpServer || []) {
|
|
500
|
+
const entry = gServer.get(row.server) || {
|
|
501
|
+
server: row.server,
|
|
502
|
+
calls: 0,
|
|
503
|
+
errors: 0,
|
|
504
|
+
turnTokens: 0,
|
|
505
|
+
};
|
|
506
|
+
entry.calls += row.calls || 0;
|
|
507
|
+
entry.errors += row.errors || 0;
|
|
508
|
+
entry.turnTokens += row.turnTokens || 0;
|
|
509
|
+
gServer.set(row.server, entry);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
const byCallsDesc = (a, b) => b.calls - a.calls;
|
|
513
|
+
|
|
209
514
|
return {
|
|
210
515
|
sessions: perSession,
|
|
211
516
|
skipped,
|
|
@@ -213,6 +518,17 @@ export function allSessionsUsage({ limit = 1000 } = {}) {
|
|
|
213
518
|
byModel: Array.from(byKey.values()).sort(
|
|
214
519
|
(a, b) => b.totalTokens - a.totalTokens,
|
|
215
520
|
),
|
|
521
|
+
attribution: {
|
|
522
|
+
byOrigin: Array.from(gOrigin.values()).sort(_byTokensDesc),
|
|
523
|
+
bySkill: Array.from(gSkill.values()).sort(_byTokensDesc),
|
|
524
|
+
bySubagent: Array.from(gSubagent.values()).sort(_byTokensDesc),
|
|
525
|
+
tools: {
|
|
526
|
+
totalCalls: gToolCalls,
|
|
527
|
+
totalErrors: gToolErrors,
|
|
528
|
+
byTool: Array.from(gTool.values()).sort(byCallsDesc),
|
|
529
|
+
byMcpServer: Array.from(gServer.values()).sort(byCallsDesc),
|
|
530
|
+
},
|
|
531
|
+
},
|
|
216
532
|
};
|
|
217
533
|
}
|
|
218
534
|
|
|
@@ -115,6 +115,14 @@ export class SubAgentContext {
|
|
|
115
115
|
// Optional progress callback for streaming events to consumers
|
|
116
116
|
this._onProgress = options.onProgress || null;
|
|
117
117
|
|
|
118
|
+
// Usage-attribution seam (用量归因): forwards the child loop's REAL
|
|
119
|
+
// provider-reported token usage to the spawner (the ~4-chars/token
|
|
120
|
+
// estimate kept in _tokenCount below is only a budget heuristic and never
|
|
121
|
+
// leaves this context). Wired by spawn_sub_agent / isolated run_skill so
|
|
122
|
+
// the parent loop can re-yield attributed `token-usage` events.
|
|
123
|
+
this._onUsage =
|
|
124
|
+
typeof options.onUsage === "function" ? options.onUsage : null;
|
|
125
|
+
|
|
118
126
|
// Optional abort signal for cancellation
|
|
119
127
|
this._signal = options.signal || null;
|
|
120
128
|
|
|
@@ -295,6 +303,21 @@ export class SubAgentContext {
|
|
|
295
303
|
for await (const event of gen) {
|
|
296
304
|
this._iterationCount++;
|
|
297
305
|
|
|
306
|
+
if (event.type === "token-usage" && this._onUsage) {
|
|
307
|
+
// Forward real usage to the spawner. A nested child's event already
|
|
308
|
+
// carries its own attribution frame — preserve it (deepest wins).
|
|
309
|
+
try {
|
|
310
|
+
this._onUsage({
|
|
311
|
+
provider: event.provider || null,
|
|
312
|
+
model: event.model || null,
|
|
313
|
+
usage: event.usage || null,
|
|
314
|
+
attribution: event.attribution || null,
|
|
315
|
+
});
|
|
316
|
+
} catch (_e) {
|
|
317
|
+
// Usage forwarding is best-effort — never break the child loop
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
298
321
|
if (event.type === "tool-executing") {
|
|
299
322
|
this._toolsUsed.push(event.tool);
|
|
300
323
|
}
|
package/src/lib/web-ui-server.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import http from "http";
|
|
20
20
|
import fs from "fs";
|
|
21
21
|
import path from "path";
|
|
22
|
+
import crypto from "crypto";
|
|
22
23
|
import { fileURLToPath } from "url";
|
|
23
24
|
import { getInlineSource as getEnvelopeInlineSource } from "./web-ui-envelope.js";
|
|
24
25
|
import { CLISkillLoader } from "./skill-loader.js";
|
|
@@ -1224,28 +1225,140 @@ function findWebPanelDist(staticDir) {
|
|
|
1224
1225
|
return null;
|
|
1225
1226
|
}
|
|
1226
1227
|
|
|
1228
|
+
/** Constant-time token check (same construction as ws-server's): hash both
|
|
1229
|
+
* sides to a fixed length so length differences don't leak, then compare in
|
|
1230
|
+
* constant time. `null`/absent configured token → no auth required. */
|
|
1231
|
+
function downloadTokenOk(configuredToken, req) {
|
|
1232
|
+
if (!configuredToken) return true;
|
|
1233
|
+
let supplied = "";
|
|
1234
|
+
const auth = String(req.headers?.authorization || "");
|
|
1235
|
+
if (auth.toLowerCase().startsWith("bearer ")) {
|
|
1236
|
+
supplied = auth.slice(7).trim();
|
|
1237
|
+
} else {
|
|
1238
|
+
try {
|
|
1239
|
+
supplied =
|
|
1240
|
+
new URL(req.url, "http://localhost").searchParams.get("token") || "";
|
|
1241
|
+
} catch {
|
|
1242
|
+
supplied = "";
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
const a = crypto.createHash("sha256").update(String(supplied)).digest();
|
|
1246
|
+
const b = crypto
|
|
1247
|
+
.createHash("sha256")
|
|
1248
|
+
.update(String(configuredToken))
|
|
1249
|
+
.digest();
|
|
1250
|
+
return crypto.timingSafeEqual(a, b);
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
/** RFC 5987 filename* value for a Content-Disposition header. */
|
|
1254
|
+
function encodeDispositionFilename(name) {
|
|
1255
|
+
const base = String(name || "artifact").replace(/[\r\n"]/g, "_");
|
|
1256
|
+
return `attachment; filename*=UTF-8''${encodeURIComponent(base)}`;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1227
1259
|
/**
|
|
1228
1260
|
* Handle `/api/*` routes. Returns `true` if the request was handled (caller
|
|
1229
1261
|
* must stop further processing) and `false` for everything else (caller
|
|
1230
1262
|
* continues with the static/SPA handler).
|
|
1231
1263
|
*
|
|
1232
|
-
*
|
|
1264
|
+
* Endpoints:
|
|
1233
1265
|
*
|
|
1234
1266
|
* GET /api/skills → 200 { schema: 1, skills: [{name, source, category,
|
|
1235
1267
|
* description, version}] }
|
|
1268
|
+
* GET /api/artifacts/<id>/download → the artifact's stored copy as an
|
|
1269
|
+
* attachment (full file — the WS artifact-content preview
|
|
1270
|
+
* route is capped; this is the uncapped download path the
|
|
1271
|
+
* web-panel's 下载 button uses). When the server was
|
|
1272
|
+
* created with a wsToken, the same token must accompany
|
|
1273
|
+
* the request (`Authorization: Bearer` or `?token=`).
|
|
1236
1274
|
*
|
|
1237
|
-
* The smoke-runner's project-mode probe (packer/smoke-runner.js) hits
|
|
1238
|
-
* to verify bundled skills were materialized and registered. The
|
|
1239
|
-
* deliberately minimal — the Web UI's richer
|
|
1275
|
+
* The smoke-runner's project-mode probe (packer/smoke-runner.js) hits
|
|
1276
|
+
* /api/skills to verify bundled skills were materialized and registered. The
|
|
1277
|
+
* shapes are deliberately minimal — the Web UI's richer views use WS.
|
|
1240
1278
|
*
|
|
1241
1279
|
* @param {import("http").IncomingMessage} req
|
|
1242
1280
|
* @param {import("http").ServerResponse} res
|
|
1281
|
+
* @param {{ wsToken?: string|null }} [ctx] server context (auth for downloads)
|
|
1243
1282
|
* @returns {boolean} true if handled, false if the caller should continue
|
|
1244
1283
|
*/
|
|
1245
|
-
export function handleApiRequest(req, res) {
|
|
1284
|
+
export function handleApiRequest(req, res, ctx = {}) {
|
|
1246
1285
|
const urlPath = req.url.split("?")[0];
|
|
1247
1286
|
if (!urlPath.startsWith("/api/")) return false;
|
|
1248
1287
|
|
|
1288
|
+
const artifactMatch = urlPath.match(
|
|
1289
|
+
/^\/api\/artifacts\/([A-Za-z0-9_]+)\/download$/,
|
|
1290
|
+
);
|
|
1291
|
+
if (artifactMatch) {
|
|
1292
|
+
if (req.method !== "GET") {
|
|
1293
|
+
res.writeHead(405, { "Content-Type": "application/json" });
|
|
1294
|
+
res.end(JSON.stringify({ error: "Method Not Allowed" }));
|
|
1295
|
+
return true;
|
|
1296
|
+
}
|
|
1297
|
+
// The panel authenticates its WS with this token; the download endpoint
|
|
1298
|
+
// serves artifact FILE CONTENT, so it must not be weaker than the WS.
|
|
1299
|
+
if (!downloadTokenOk(ctx.wsToken || null, req)) {
|
|
1300
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
1301
|
+
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
1302
|
+
return true;
|
|
1303
|
+
}
|
|
1304
|
+
// Async body in a sync router: the route is claimed (return true) and the
|
|
1305
|
+
// response completes when the store import resolves.
|
|
1306
|
+
(async () => {
|
|
1307
|
+
try {
|
|
1308
|
+
const { ArtifactStore } = await import("./artifact-store.js");
|
|
1309
|
+
const store = new ArtifactStore();
|
|
1310
|
+
const entry = store.get(artifactMatch[1]);
|
|
1311
|
+
if (!entry) {
|
|
1312
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1313
|
+
res.end(JSON.stringify({ error: "Not Found" }));
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
// Same tamper guard as the WS content route: the stored filename must
|
|
1317
|
+
// be a plain basename or the index row is refused.
|
|
1318
|
+
if (
|
|
1319
|
+
!entry.file ||
|
|
1320
|
+
entry.file !== path.basename(entry.file) ||
|
|
1321
|
+
entry.file.includes("..")
|
|
1322
|
+
) {
|
|
1323
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1324
|
+
res.end(JSON.stringify({ error: "Bad artifact index row" }));
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
const storedPath = store.storedPath(entry);
|
|
1328
|
+
let size;
|
|
1329
|
+
try {
|
|
1330
|
+
size = fs.statSync(storedPath).size;
|
|
1331
|
+
} catch {
|
|
1332
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1333
|
+
res.end(JSON.stringify({ error: "Stored copy missing" }));
|
|
1334
|
+
return;
|
|
1335
|
+
}
|
|
1336
|
+
const downloadName =
|
|
1337
|
+
entry.title && path.extname(entry.title)
|
|
1338
|
+
? entry.title
|
|
1339
|
+
: path.basename(entry.sourcePath || entry.file);
|
|
1340
|
+
res.writeHead(200, {
|
|
1341
|
+
"Content-Type": entry.mime || "application/octet-stream",
|
|
1342
|
+
"Content-Length": size,
|
|
1343
|
+
"Content-Disposition": encodeDispositionFilename(downloadName),
|
|
1344
|
+
"Cache-Control": "no-store",
|
|
1345
|
+
"X-Content-Type-Options": "nosniff",
|
|
1346
|
+
});
|
|
1347
|
+
const stream = fs.createReadStream(storedPath);
|
|
1348
|
+
stream.on("error", () => res.destroy());
|
|
1349
|
+
stream.pipe(res);
|
|
1350
|
+
} catch (err) {
|
|
1351
|
+
try {
|
|
1352
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1353
|
+
res.end(JSON.stringify({ error: err.message }));
|
|
1354
|
+
} catch {
|
|
1355
|
+
/* headers already sent — connection is torn down by the stream */
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
})();
|
|
1359
|
+
return true;
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1249
1362
|
if (urlPath === "/api/skills") {
|
|
1250
1363
|
if (req.method !== "GET") {
|
|
1251
1364
|
res.writeHead(405, { "Content-Type": "application/json" });
|
|
@@ -1328,7 +1441,7 @@ export function createWebUIServer(opts) {
|
|
|
1328
1441
|
return http.createServer((req, res) => {
|
|
1329
1442
|
// /api/* routes are handled before the static file logic so GET /api/skills
|
|
1330
1443
|
// doesn't accidentally resolve to index.html via the SPA fallback.
|
|
1331
|
-
if (handleApiRequest(req, res)) return;
|
|
1444
|
+
if (handleApiRequest(req, res, { wsToken: opts.wsToken || null })) return;
|
|
1332
1445
|
|
|
1333
1446
|
if (req.method !== "GET") {
|
|
1334
1447
|
res.writeHead(405, { "Content-Type": "text/plain" });
|
|
@@ -1415,7 +1528,7 @@ export function createWebUIServer(opts) {
|
|
|
1415
1528
|
// ── Fallback: embedded classic single-page HTML ─────────────────────────
|
|
1416
1529
|
const html = buildHtml(opts);
|
|
1417
1530
|
return http.createServer((req, res) => {
|
|
1418
|
-
if (handleApiRequest(req, res)) return;
|
|
1531
|
+
if (handleApiRequest(req, res, { wsToken: opts.wsToken || null })) return;
|
|
1419
1532
|
|
|
1420
1533
|
const urlPath = req.url.split("?")[0];
|
|
1421
1534
|
if (
|