cc-viewer 1.9.8 → 1.9.10
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/cli.js +5 -3
- package/dist/assets/App-Ci9GYs24.js +2 -0
- package/dist/assets/App-D06hBGII.css +1 -0
- package/dist/assets/{MdxEditorPanel-BnbZpgDp.js → MdxEditorPanel-8fwlNHMz.js} +1 -1
- package/dist/assets/Mobile-DngSOqYr.js +1 -0
- package/dist/assets/ProxyStatsModal-CWSIELLj.js +1 -0
- package/dist/assets/{_baseUniq-CwQykUuc.js → _baseUniq-6ZLcdMmL.js} +1 -1
- package/dist/assets/{arc-EdUdBxtG.js → arc-Dirf6EZP.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-DtBIdElY.js → architectureDiagram-Q4EWVU46-BfIhmu_I.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-guNZIKkZ.js → blockDiagram-DXYQGD6D-D0y4ALi1.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-xV5vB-Z9.js → c4Diagram-AHTNJAMY-Dlfp4SUK.js} +1 -1
- package/dist/assets/{channel-DYEktNap.js → channel-CaNcU8SI.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-ZTkItpvj.js → chunk-4BX2VUAB-ki0vHNS-.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-dYIewyyR.js → chunk-4TB4RGXK-D8yO8Iuv.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-CSsPNgPX.js → chunk-55IACEB6-BvIgaNPO.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-CnMiuqzQ.js → chunk-EDXVE4YY-DF2Of_VO.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-CVpATPZ3.js → chunk-FMBD7UC4-BIw7SXs-.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-Dpq4s8dW.js → chunk-OYMX7WX6-AB76s0A0.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Bnojh6X6.js → chunk-QZHKN3VN-CuN2LCFc.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-BYqK4Dsa.js → chunk-YZCP3GAM-D4DLBzBN.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DLJzWgYm.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-DLJzWgYm.js +1 -0
- package/dist/assets/clone-H_VGsHn9.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-OJ7mmWyV.js → cose-bilkent-S5V4N54A-Bn485PX9.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-B1Kwd_Rk.js → dagre-KV5264BT-e4QVCf17.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-D8qrw8sp.js → diagram-5BDNPKRD-_SBFGgnf.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-ip-N4ho8.js → diagram-G4DWMVQ6-Bz1ovxLs.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-2PZaskUF.js → diagram-MMDJMWI5-B-oojW8K.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-CeAOQUZP.js → diagram-TYMM5635-CTAyNaOj.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-BCBaEYEb.js → erDiagram-SMLLAGMA-BfvKAqq2.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CsGyIqcX.js → flowDiagram-DWJPFMVM-BpTEZEAe.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-COsj3FZ-.js → ganttDiagram-T4ZO3ILL-DUD_vkyE.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-DvQVQGJG.js → gitGraphDiagram-UUTBAWPF-DnlXcTud.js} +1 -1
- package/dist/assets/{graph-BSB9fDSP.js → graph-D4xsUKSn.js} +1 -1
- package/dist/assets/{index-DKufsAzx.js → index-84-BjpGl.js} +1 -1
- package/dist/assets/index-BDKwOnE3.js +2 -0
- package/dist/assets/{index-Bo-12bug.js → index-BniBLRtZ.js} +1 -1
- package/dist/assets/{index-BRw4Iz2O.js → index-C2Zo3KOg.js} +1 -1
- package/dist/assets/{index-BuVComcY.js → index-CWZoKT7e.js} +1 -1
- package/dist/assets/{index-eM2ih5n7.js → index-D-4EsFub.js} +1 -1
- package/dist/assets/{index-CNNAYjkF.js → index-D3KzRmkP.js} +1 -1
- package/dist/assets/{index-DXBfwPYD.js → index-NGgdum2i.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-eNucS4iW.js → infoDiagram-42DDH7IO-BbdvmCDt.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-Dxxj-gUX.js → ishikawaDiagram-UXIWVN3A-oZBOPoTO.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-yJJzVEBb.js → journeyDiagram-VCZTEJTY-DWoH0hs9.js} +1 -1
- package/dist/assets/{jszip.min-ByI0ds5u.js → jszip.min-I5y9RZhq.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-2rfTi2zw.js → kanban-definition-6JOO6SKY-BLHtjKsS.js} +1 -1
- package/dist/assets/{layout-DTmHj0Wm.js → layout-C116SNcQ.js} +1 -1
- package/dist/assets/{linear-BOA3a9FE.js → linear-Br08WCkF.js} +1 -1
- package/dist/assets/{mermaid.core-5H-NR79H.js → mermaid.core-D-4dTrvI.js} +2 -2
- package/dist/assets/{min-CpjFTEec.js → min-khs74HbH.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-B8KCjFgB.js → mindmap-definition-QFDTVHPH-C3_yItFF.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-BARy96g5.js → pieDiagram-DEJITSTG-ByCwM2_P.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-DYoTapuI.js → quadrantDiagram-34T5L4WZ-CjV65X1r.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-EeANLLK2.js → requirementDiagram-MS252O5E-DFJ_dbtV.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-DmQ-YVPj.js → sankeyDiagram-XADWPNL6-BBvXoBfj.js} +1 -1
- package/dist/assets/seqResourceLoaders-C34z9uOa.css +41 -0
- package/dist/assets/seqResourceLoaders-JJzgZZzU.js +2 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-SZtW0rYC.js → sequenceDiagram-FGHM5R23-BJM04Y3N.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-CEygsIld.js → stateDiagram-FHFEXIEX-DI2q_Kkn.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BU1XPhBe.js → stateDiagram-v2-QKLJ7IA2-CrGC6kEY.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-DhPBaQBt.js → timeline-definition-GMOUNBTQ-JcfRUsjD.js} +1 -1
- package/dist/assets/vendor-antd-DxEVqVhD.js +17 -0
- package/dist/assets/vendor-codemirror-CgNlkZva.js +1 -0
- package/dist/assets/{vendor-mdxeditor-Cz09a1_I.js → vendor-mdxeditor-DoreQf4A.js} +2 -2
- package/dist/assets/{vendor-qrcode-qbbntD6-.js → vendor-qrcode-q1CKtOyX.js} +1 -1
- package/dist/assets/{vendor-virtuoso-Sk8fQ4Hx.js → vendor-virtuoso-BXGe9q7E.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-NniTHxDw.js → vennDiagram-DHZGUBPP-mAHR1Ugj.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-D9SNOrDw.js → wardley-RL74JXVD-mlTq_P-4.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-DvHXKNza.js → wardleyDiagram-NUSXRM2D-BDGfajRb.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-CaiwPTQB.js → xychartDiagram-5P7HB3ND-Cf2pR1mV.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +1 -1
- package/plugins/README.md +9 -1
- package/server/interceptor.js +1 -1
- package/server/lib/builtin-model-prompts.js +1 -1
- package/server/lib/chat-queue.js +1 -1
- package/server/lib/credential-access.js +1 -1
- package/server/lib/credential-migrate.js +1 -1
- package/server/lib/error-report.js +26 -0
- package/server/lib/json-store.js +1 -1
- package/server/lib/launch-config.js +1 -1
- package/server/lib/log-watcher.js +1 -1
- package/server/lib/plugin-host.mjs +198 -0
- package/server/lib/plugin-loader.js +346 -90
- package/server/lib/plugin-manager.js +46 -2
- package/server/lib/plugin-manifest.js +329 -0
- package/server/lib/plugin-panels.js +183 -0
- package/server/lib/plugin-proxy.js +408 -0
- package/server/lib/plugin-runtime.js +775 -0
- package/server/lib/plugin-service-manager.js +463 -0
- package/server/lib/proxy/proxy-retry.js +1 -1
- package/server/lib/proxy/proxy-stats.js +1 -1
- package/server/lib/resume-watchdog.js +1 -1
- package/server/lib/same-origin.js +13 -3
- package/server/lib/sdk-manager.js +117 -23
- package/server/lib/sdk-session-index.js +1 -1
- package/server/lib/stats-worker.js +1 -1
- package/server/lib/system-prompt-files.js +1 -1
- package/server/lib/system-prompt-live.js +1 -1
- package/server/lib/system-prompt-snapshots.js +1 -1
- package/server/lib/v2/adapter.js +1 -1
- package/server/lib/v2/convert.js +1 -1
- package/server/lib/v2/jsonl-read.js +1 -1
- package/server/lib/v2/live-feed.js +1 -1
- package/server/lib/v2/meta-rows.js +1 -1
- package/server/lib/v2/resume-list.js +1 -1
- package/server/lib/v2/session-list.js +1 -1
- package/server/lib/v2/session-owner.js +1 -1
- package/server/lib/v2/v2-writer.js +1 -1
- package/server/lib/wire-compress.js +1 -1
- package/server/lib/ws-mux.js +1 -1
- package/server/proxy.js +1 -1
- package/server/pty-manager.js +1 -1
- package/server/routes/ask-perm.js +1 -1
- package/server/routes/events.js +1 -1
- package/server/routes/expert.js +1 -1
- package/server/routes/files-content.js +1 -1
- package/server/routes/files-fs.js +4 -2
- package/server/routes/misc.js +11 -0
- package/server/routes/plugin-services.js +104 -0
- package/server/routes/plugins.js +145 -17
- package/server/routes/preferences.js +1 -1
- package/server/routes/resume.js +1 -1
- package/server/routes/sdk-context.js +1 -1
- package/server/routes/sdk-mcp-status.js +1 -1
- package/server/routes/workspaces.js +1 -1
- package/server/server.js +370 -173
- package/dist/assets/App-D2zrtS4P.js +0 -2
- package/dist/assets/App-DuDeDOuX.css +0 -1
- package/dist/assets/Mobile-CU-g7cTn.js +0 -1
- package/dist/assets/ProxyStatsModal-CavWQfBA.js +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-ClGF33bF.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-ClGF33bF.js +0 -1
- package/dist/assets/clone-CFj55h49.js +0 -1
- package/dist/assets/index-D2maXxkC.js +0 -2
- package/dist/assets/seqResourceLoaders-DZF6ofB7.css +0 -41
- package/dist/assets/seqResourceLoaders-jOSz1x7F.js +0 -2
- package/dist/assets/vendor-antd-CS_S2mBN.js +0 -17
- package/dist/assets/vendor-codemirror-B5l-VbBO.js +0 -1
|
@@ -30,7 +30,7 @@ import { ASK_TIMEOUT_MS } from './ask/ask-constants.js';
|
|
|
30
30
|
import { withDefaultThinkingDisplay, resolveLaunchSystemPrompt, launchArgsToExtraArgs } from './launch-config.js';
|
|
31
31
|
import { evaluateImDeny } from './im-deny.js';
|
|
32
32
|
import { APPROVAL_TOOLS, isOverTrustDir, isPublishCommand } from './approval-policy.js';
|
|
33
|
-
import { reportSwallowed } from '
|
|
33
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
34
34
|
import { resolve as _pathResolve, dirname as _pathDirname, sep as _pathSep } from 'node:path';
|
|
35
35
|
import { createHash } from 'node:crypto';
|
|
36
36
|
import { registerSdkSessionLister } from './sdk-session-index.js';
|
|
@@ -147,7 +147,7 @@ const MAX_QUEUE_ITEMS = 50;
|
|
|
147
147
|
// Callbacks registered by cli.js (one wiring for the whole process; every session
|
|
148
148
|
// reuses the same callbacks — they broadcast to the shared terminal WS).
|
|
149
149
|
let _onTurnEnd = null; // SDK mode has no Stop hook (ensureHooks() skipped) — fire turnEnd directly when the SDK 'result' message arrives
|
|
150
|
-
let _onQueryError = null; // query-level failure → (message: string) => void; cli.js forwards
|
|
150
|
+
let _onQueryError = null; // query-level failure → (message: string, meta: {cwd, sessionId, instanceKey}) => void; cli.js forwards it as a scoped sdk-error WS frame (dialogue row + toast)
|
|
151
151
|
let _broadcastWs = null;
|
|
152
152
|
let _runWaterfallHook = null;
|
|
153
153
|
|
|
@@ -327,6 +327,13 @@ function _createSession(key, cwd) {
|
|
|
327
327
|
let _sessionId = null;
|
|
328
328
|
let _initAnnouncedSid = ''; // sessionId whose slash-command surface was already announced (reset on full reset)
|
|
329
329
|
let _lastInitSnapshot = null; // last sdk-init payload for WS reconnect replay
|
|
330
|
+
// Last sdk-skills frame (supportedCommands() result) for WS reconnect replay, and the
|
|
331
|
+
// resident query it was loaded from (dedupes the once-per-query-generation fetch).
|
|
332
|
+
let _lastSkillsSnapshot = null;
|
|
333
|
+
let _skillsLoadedQuery = null;
|
|
334
|
+
// init 帧的 skills 白名单(name / plugin:skill / :suffix 形式),用于把 supportedCommands()
|
|
335
|
+
// 的全量命令面收敛为纯 skill 面;null = 该 init 帧未提供(不做过滤,保守放行)。
|
|
336
|
+
let _initSkills = null;
|
|
330
337
|
// Last announced session state ('idle' | 'running' | 'requires_action' | null) — the
|
|
331
338
|
// turn-level busy truth for the conversation spinner. Sources, most authoritative
|
|
332
339
|
// first: CLI-emitted session_state_changed (gated behind CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS,
|
|
@@ -690,6 +697,8 @@ function _createSession(key, cwd) {
|
|
|
690
697
|
const inputQueue = _inputQueue;
|
|
691
698
|
_activeQuery = null;
|
|
692
699
|
_inputQueue = null;
|
|
700
|
+
_skillsLoadedQuery = null; // next resident query re-pulls supportedCommands() on init
|
|
701
|
+
_initSkills = null; // re-captured from the next init frame
|
|
693
702
|
_clearGraceTimer();
|
|
694
703
|
_clearTurnWatchdog();
|
|
695
704
|
_lateResultTombstones = 0;
|
|
@@ -701,10 +710,12 @@ function _createSession(key, cwd) {
|
|
|
701
710
|
}
|
|
702
711
|
}
|
|
703
712
|
|
|
704
|
-
/**
|
|
713
|
+
/** Query-level error signal; the routing envelope always comes from this
|
|
714
|
+
* session's closure (callback throw must never break the caller). */
|
|
705
715
|
function _notifyQueryError(err) {
|
|
706
716
|
if (!_onQueryError) return;
|
|
707
|
-
|
|
717
|
+
const context = { cwd: _cwd, sessionId: _sessionId || null, instanceKey };
|
|
718
|
+
try { _onQueryError(String(err?.message || err), context); }
|
|
708
719
|
catch (cbErr) { console.warn('[sdk-manager] onQueryError threw:', cbErr?.message); }
|
|
709
720
|
}
|
|
710
721
|
|
|
@@ -748,20 +759,33 @@ function _createSession(key, cwd) {
|
|
|
748
759
|
// push/result fallbacks keep the spinner correct at result granularity.
|
|
749
760
|
if (msg.subtype === 'session_state_changed') {
|
|
750
761
|
_announceSessionState(msg.state);
|
|
751
|
-
} else if (msg.subtype === 'init'
|
|
752
|
-
&& Array.isArray(msg.slash_commands) && msg.slash_commands.length > 0
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
762
|
+
} else if (msg.subtype === 'init') {
|
|
763
|
+
if (_broadcastWs && Array.isArray(msg.slash_commands) && msg.slash_commands.length > 0
|
|
764
|
+
&& msg.session_id !== _initAnnouncedSid) {
|
|
765
|
+
_initAnnouncedSid = msg.session_id;
|
|
766
|
+
_lastInitSnapshot = {
|
|
767
|
+
type: 'sdk-init',
|
|
768
|
+
sessionId: msg.session_id,
|
|
769
|
+
model: msg.model,
|
|
770
|
+
slashCommands: msg.slash_commands,
|
|
771
|
+
tools: msg.tools,
|
|
772
|
+
cwd: _cwd,
|
|
773
|
+
instanceKey,
|
|
774
|
+
};
|
|
775
|
+
_broadcastWs(_lastInitSnapshot);
|
|
776
|
+
}
|
|
777
|
+
// Skill menu surface: pull the full supportedCommands() (name + description +
|
|
778
|
+
// argumentHint) once per resident-query generation. Done even when slash_commands
|
|
779
|
+
// is empty/absent — a session with no TUI commands can still expose skills.
|
|
780
|
+
// supportedCommands() returns the whole command surface (incl. /clear, /compact,
|
|
781
|
+
// /model, /usage), so keep ONLY real skills: intersect with the init frame's
|
|
782
|
+
// `skills` allowlist (name / plugin:skill / :suffix forms) to honour the
|
|
783
|
+
// "slash menu indexes skills only" decision.
|
|
784
|
+
if (_broadcastWs && _activeQuery && _skillsLoadedQuery !== _activeQuery) {
|
|
785
|
+
_skillsLoadedQuery = _activeQuery;
|
|
786
|
+
_initSkills = Array.isArray(msg.skills) ? msg.skills : null;
|
|
787
|
+
_loadSupportedCommands();
|
|
788
|
+
}
|
|
765
789
|
} else if (msg.subtype === 'compact_boundary' && _broadcastWs) {
|
|
766
790
|
const meta = msg.compact_metadata;
|
|
767
791
|
_broadcastWs({
|
|
@@ -861,7 +885,8 @@ function _createSession(key, cwd) {
|
|
|
861
885
|
|
|
862
886
|
/**
|
|
863
887
|
* Surface a failed turn to the Web UI — the wire path persists the failed entry but
|
|
864
|
-
* never
|
|
888
|
+
* never signals it, so a dead/errored turn would otherwise look like a hang. The
|
|
889
|
+
* sdk-error frame renders as a persistent dialogue row plus a toast client-side.
|
|
865
890
|
*/
|
|
866
891
|
function _notifyTurnError(resultMsg) {
|
|
867
892
|
const isError = resultMsg.is_error === true || (resultMsg.subtype && resultMsg.subtype !== 'success');
|
|
@@ -953,6 +978,58 @@ function _createSession(key, cwd) {
|
|
|
953
978
|
}
|
|
954
979
|
}
|
|
955
980
|
|
|
981
|
+
/**
|
|
982
|
+
* Pull the SDK's supportedCommands() (skill surface: name + description + argumentHint)
|
|
983
|
+
* once per resident-query generation and broadcast it as an `sdk-skills` frame, retained
|
|
984
|
+
* for WS reconnect replay. Fire-and-forget from the init branch — never awaited. Bounded
|
|
985
|
+
* by CTX_USAGE_TIMEOUT_MS; on timeout/rejection/stale generation it reports and broadcasts
|
|
986
|
+
* nothing (clients fall back to /api/skills). Raw commands are passed through; the web
|
|
987
|
+
* client's normalizeSdkCommands does the safe-name filtering.
|
|
988
|
+
*/
|
|
989
|
+
function _loadSupportedCommands() {
|
|
990
|
+
const q = _activeQuery;
|
|
991
|
+
if (!q || typeof q.supportedCommands !== 'function') return;
|
|
992
|
+
let timeoutId;
|
|
993
|
+
const timeout = new Promise((_, reject) => {
|
|
994
|
+
timeoutId = setTimeout(() => reject(new Error('supportedCommands timeout')), CTX_USAGE_TIMEOUT_MS);
|
|
995
|
+
if (timeoutId.unref) timeoutId.unref();
|
|
996
|
+
});
|
|
997
|
+
Promise.race([Promise.resolve().then(() => q.supportedCommands()), timeout]).then((commands) => {
|
|
998
|
+
if (_activeQuery !== q) return; // stale generation (session switch/death) — drop
|
|
999
|
+
// Keep only real skills: supportedCommands() also returns TUI built-ins (/clear,
|
|
1000
|
+
// /compact, /model, /usage). When the init frame supplied a skills allowlist, filter
|
|
1001
|
+
// by it (name, plugin:skill, or :suffix). No allowlist → pass through (conservative).
|
|
1002
|
+
let filtered = Array.isArray(commands) ? commands : [];
|
|
1003
|
+
if (Array.isArray(_initSkills) && _initSkills.length > 0) {
|
|
1004
|
+
const allow = new Set();
|
|
1005
|
+
for (const s of _initSkills) {
|
|
1006
|
+
if (typeof s !== 'string' || !s) continue;
|
|
1007
|
+
allow.add(s); // exact (name or plugin:skill)
|
|
1008
|
+
const idx = s.lastIndexOf(':');
|
|
1009
|
+
if (idx >= 0) allow.add(s.slice(idx + 1)); // :suffix form → bare name
|
|
1010
|
+
}
|
|
1011
|
+
filtered = filtered.filter(c => c && (allow.has(c.name)
|
|
1012
|
+
|| (typeof c.name === 'string' && allow.has(c.name.replace(/^plugin.*:/, '')))));
|
|
1013
|
+
}
|
|
1014
|
+
_lastSkillsSnapshot = {
|
|
1015
|
+
type: 'sdk-skills',
|
|
1016
|
+
sessionId: _sessionId || null,
|
|
1017
|
+
ts: Date.now(),
|
|
1018
|
+
cwd: _cwd,
|
|
1019
|
+
instanceKey,
|
|
1020
|
+
commands: filtered,
|
|
1021
|
+
};
|
|
1022
|
+
if (_broadcastWs) {
|
|
1023
|
+
try { _broadcastWs(_lastSkillsSnapshot); }
|
|
1024
|
+
catch (err) { reportSwallowed('sdk-manager.skills-broadcast', err); }
|
|
1025
|
+
}
|
|
1026
|
+
}).catch((err) => {
|
|
1027
|
+
reportSwallowed('sdk-manager.supported-commands', err);
|
|
1028
|
+
}).finally(() => {
|
|
1029
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
|
|
956
1033
|
/**
|
|
957
1034
|
* Collect an MCP-server-status snapshot via the Query control surface (`mcp_status`).
|
|
958
1035
|
* Returns { ok:true, snapshot } | { ok:false, reason:'session-inactive'|'control-failed' }.
|
|
@@ -1464,6 +1541,7 @@ function _createSession(key, cwd) {
|
|
|
1464
1541
|
_launchResume = null;
|
|
1465
1542
|
_initAnnouncedSid = '';
|
|
1466
1543
|
_lastInitSnapshot = null; // don't replay the OLD session's sdk-init to the new one
|
|
1544
|
+
_lastSkillsSnapshot = null; // same for the skill surface
|
|
1467
1545
|
_lastSessionState = null; // same for the session-state snapshot — new session starts idle
|
|
1468
1546
|
_clearContextUsage('session-switched');
|
|
1469
1547
|
_clearMcpStatus('session-switched');
|
|
@@ -1485,6 +1563,7 @@ function _createSession(key, cwd) {
|
|
|
1485
1563
|
_turnInFlight = false;
|
|
1486
1564
|
_initAnnouncedSid = '';
|
|
1487
1565
|
_lastInitSnapshot = null;
|
|
1566
|
+
_lastSkillsSnapshot = null;
|
|
1488
1567
|
// Tell clients BEFORE nulling the snapshot: a re-init/launch mid-turn would
|
|
1489
1568
|
// otherwise leave the spinner lit with no frame ever clearing it (the new
|
|
1490
1569
|
// session starts idle and the state-change guard would swallow the first
|
|
@@ -1546,6 +1625,7 @@ function _createSession(key, cwd) {
|
|
|
1546
1625
|
},
|
|
1547
1626
|
getContextUsageSnapshot: () => _lastContextUsage,
|
|
1548
1627
|
getMcpStatusSnapshot: () => _lastMcpStatus,
|
|
1628
|
+
getSkillsSnapshot: () => _lastSkillsSnapshot,
|
|
1549
1629
|
fetchContextUsage: async () => {
|
|
1550
1630
|
// On-demand fetch for the HTTP route: refresh the snapshot, broadcast to all clients,
|
|
1551
1631
|
// then reply. Unlike _refreshContextUsage this bypasses the in-flight gate (an explicit
|
|
@@ -1597,9 +1677,9 @@ export function isSdkAvailable() {
|
|
|
1597
1677
|
* @param {string} projectName
|
|
1598
1678
|
* @param {object} deps
|
|
1599
1679
|
* @param {function} deps.onTurnEnd — ({sessionId, ts}) => void, fired on SDK 'result'
|
|
1600
|
-
* @param {function} [deps.onQueryError] — (message: string) => void,
|
|
1601
|
-
* query itself fails (spawn/protocol/iterator errors)
|
|
1602
|
-
*
|
|
1680
|
+
* @param {function} [deps.onQueryError] — (message: string, meta: {cwd, sessionId, instanceKey}) => void,
|
|
1681
|
+
* fired when the query itself fails (spawn/protocol/iterator errors); meta carries the session's
|
|
1682
|
+
* routing envelope so the sdk-error frame scopes to the owning workspace's viewers
|
|
1603
1683
|
* @param {function} deps.broadcastWs — (msg) => void, terminal-WS broadcast for approvals
|
|
1604
1684
|
* @param {string} [deps.permissionMode]
|
|
1605
1685
|
* @param {function} [deps.runWaterfallHook] — plugin waterfall (onPlanRequest/onAskRequest/onPermRequest)
|
|
@@ -1650,7 +1730,11 @@ export async function sendUserMessage(text, anchor) {
|
|
|
1650
1730
|
console.warn(`[sdk-manager] no SDK session registered for "${targetName}" — message dropped`);
|
|
1651
1731
|
reportSwallowed('sdk-manager.route', new Error(`no SDK session for ${targetName}`));
|
|
1652
1732
|
if (_onQueryError) {
|
|
1653
|
-
|
|
1733
|
+
// Module-level fallback has no session closure. Anchors never carry cwd
|
|
1734
|
+
// (server.js builds {project, instanceKey} only) — pass the project key so
|
|
1735
|
+
// broadcastWsMessage can scope the frame; instanceKey narrows further when present.
|
|
1736
|
+
const meta = { cwd: anchor.cwd || null, sessionId: null, instanceKey: anchor.instanceKey || null, project: anchor.project || null };
|
|
1737
|
+
try { _onQueryError(`Workspace "${targetName}" is no longer available — message not sent.`, meta); }
|
|
1654
1738
|
catch (err) { console.warn('[sdk-manager] onQueryError threw:', err?.message); }
|
|
1655
1739
|
}
|
|
1656
1740
|
return;
|
|
@@ -1900,6 +1984,16 @@ export function getSdkInitSnapshot(anchor) {
|
|
|
1900
1984
|
return sess ? sess.getSdkInitSnapshot() : null;
|
|
1901
1985
|
}
|
|
1902
1986
|
|
|
1987
|
+
/**
|
|
1988
|
+
* Last sdk-skills frame (supportedCommands() result) for WS reconnect replay. Null until
|
|
1989
|
+
* the first init-driven pull resolves; an absent frame leaves the client on its /api/skills
|
|
1990
|
+
* fallback. No anchor → active session.
|
|
1991
|
+
*/
|
|
1992
|
+
export function getSkillsSnapshot(anchor) {
|
|
1993
|
+
const sess = anchor ? _sessionForAnchor(anchor) : _active();
|
|
1994
|
+
return sess ? sess.getSkillsSnapshot() : null;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1903
1997
|
/**
|
|
1904
1998
|
* Last announced session-state frame for WS reconnect replay (sdk-init snapshot
|
|
1905
1999
|
* parity). Null until the first state transition — a fresh session is idle, and an
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Synchronous read-only projection of the SDK session registry. Generic view
|
|
2
2
|
// routing must not import sdk-manager (and eagerly load the Agent SDK) in PTY mode.
|
|
3
|
-
import { reportSwallowed } from '
|
|
3
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
4
4
|
|
|
5
5
|
let _listSessions = null;
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ import { join, basename } from 'node:path';
|
|
|
10
10
|
import { readJsonlTolerant, listSessionIds } from './v2/replay.js';
|
|
11
11
|
import { dirSizeSync } from './v2/layout.js';
|
|
12
12
|
import { isDiscardableSession } from './v2/session-select.js';
|
|
13
|
-
import { reportSwallowed } from '
|
|
13
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
14
14
|
import {
|
|
15
15
|
INTER_SESSION_TYPES, isSystemText, extractUserTexts, isSuggestionMode,
|
|
16
16
|
collectPromptsFromEvents, sortEpochFiles,
|
|
@@ -3,7 +3,7 @@ import { join } from 'node:path';
|
|
|
3
3
|
import { MODEL_PROMPT_DIR, matchModelPrompt } from './model-system-prompts.js';
|
|
4
4
|
import { isBuiltinDisabled, matchBuiltinModelPrompt, materializeBuiltinPrompt } from './builtin-model-prompts.js';
|
|
5
5
|
import { isNonEmptyFile } from './file-api.js';
|
|
6
|
-
import { reportSwallowed } from '
|
|
6
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
7
7
|
|
|
8
8
|
// isNonEmptyFile lives in file-api.js (shared leaf, cycle break:
|
|
9
9
|
// model-system-prompts.js must not import this module). Re-exported here so
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync, statSync } from 'node:fs';
|
|
20
20
|
import { join } from 'node:path';
|
|
21
21
|
import { randomBytes } from 'node:crypto';
|
|
22
|
-
import { reportSwallowed } from '
|
|
22
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
23
23
|
import { LOG_DIR } from '../../findcc.js';
|
|
24
24
|
import { renameSyncWithRetry } from './file-api.js';
|
|
25
25
|
import { matchModelPrompt, MODEL_PROMPT_DIR, readModelPrompt } from './model-system-prompts.js';
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, unlinkSync, readdirSync, statSync, realpathSync, openSync, readSync, closeSync } from 'node:fs';
|
|
34
34
|
import { join, basename, dirname } from 'node:path';
|
|
35
35
|
import { randomBytes } from 'node:crypto';
|
|
36
|
-
import { reportSwallowed } from '
|
|
36
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
37
37
|
import { LOG_DIR, getClaudeConfigDir } from '../../findcc.js';
|
|
38
38
|
import { renameSyncWithRetry } from './file-api.js';
|
|
39
39
|
|
package/server/lib/v2/adapter.js
CHANGED
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
|
|
30
30
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
31
31
|
import { join, dirname, basename } from 'node:path';
|
|
32
|
-
import { reportSwallowed } from '
|
|
32
|
+
import { reportSwallowed } from '../../lib/error-report.js';
|
|
33
33
|
import { isMainAgentRequest } from '../interceptor-core.js';
|
|
34
34
|
import { readSession } from './replay.js';
|
|
35
35
|
import { iterateJsonlLines } from './jsonl-read.js';
|
package/server/lib/v2/convert.js
CHANGED
|
@@ -28,7 +28,7 @@ import { join, basename } from 'node:path';
|
|
|
28
28
|
import { iterateRawEntriesAsync } from '../log-stream.js';
|
|
29
29
|
import { createIncrementalReconstructor } from '@ccv/core/delta-reconstructor';
|
|
30
30
|
import { isLogFileName, parseLogTs } from '../log-file-utils.js';
|
|
31
|
-
import { reportSwallowed } from '
|
|
31
|
+
import { reportSwallowed } from '../../lib/error-report.js';
|
|
32
32
|
import { V2Writer } from './v2-writer.js';
|
|
33
33
|
import { verifyV1File } from './verify.js';
|
|
34
34
|
import { parseUserId } from './identity.js';
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
// mirroring readJsonlTolerant's torn-tail tolerance (spec §14).
|
|
15
15
|
|
|
16
16
|
import { existsSync, openSync, readSync, closeSync, statSync } from 'node:fs';
|
|
17
|
-
import { reportSwallowed } from '
|
|
17
|
+
import { reportSwallowed } from '../../lib/error-report.js';
|
|
18
18
|
|
|
19
19
|
const DEFAULT_CHUNK_BYTES = 8 * 1024 * 1024;
|
|
20
20
|
// Node's max string length is 0x1fffffe8 (~512MiB) — a line this long can
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import { watch, existsSync, readdirSync, readFileSync, statSync, openSync, readSync, closeSync } from 'node:fs';
|
|
26
26
|
import { join } from 'node:path';
|
|
27
|
-
import { reportSwallowed } from '
|
|
27
|
+
import { reportSwallowed } from '../../lib/error-report.js';
|
|
28
28
|
import { createIncrementalReconstructor } from '@ccv/core/delta-reconstructor';
|
|
29
29
|
import { processWatchedEntry, sendEventToClients, sendEventRawToClients } from '../log-watcher.js';
|
|
30
30
|
import { filterClientsByInstance } from './view-router.js';
|
|
@@ -21,7 +21,7 @@ import { readJsonlTolerant } from './replay.js';
|
|
|
21
21
|
import { iterateJsonlLines } from './jsonl-read.js';
|
|
22
22
|
import { iterateV2Items, findTeammateSessionDirs } from './adapter.js';
|
|
23
23
|
import { SingleFlight } from './singleflight.js';
|
|
24
|
-
import { reportSwallowed } from '
|
|
24
|
+
import { reportSwallowed } from '../../lib/error-report.js';
|
|
25
25
|
import { classifyRequest, withAgentNameSubType } from '@ccv/core/requestType';
|
|
26
26
|
|
|
27
27
|
// KEEP IN SYNC: server/lib/v2/adapter.js itemKey — (sessionId, seq) identity.
|
|
@@ -25,7 +25,7 @@ import { summarizeSessionPage } from './session-list.js';
|
|
|
25
25
|
import { sessionHasMainOrTeammateReq } from './session-select.js';
|
|
26
26
|
import { readPin } from '../session-pin-store.js';
|
|
27
27
|
import { LIVE_SESSION_MTIME_MS } from '../log-file-utils.js';
|
|
28
|
-
import { reportSwallowed } from '
|
|
28
|
+
import { reportSwallowed } from '../../lib/error-report.js';
|
|
29
29
|
|
|
30
30
|
/**
|
|
31
31
|
* Liveness heuristic for the "running" placeholder column. A session is
|
|
@@ -26,7 +26,7 @@ import { listSessionIds, readJsonlTolerant } from './replay.js';
|
|
|
26
26
|
import { readPromptsHead, collectPromptsFromEvents } from '../user-prompt-extract.js';
|
|
27
27
|
import { isDiscardableSession } from './session-select.js';
|
|
28
28
|
import { isSupportedWireFormat, dirSizeSync } from './layout.js';
|
|
29
|
-
import { reportSwallowed } from '
|
|
29
|
+
import { reportSwallowed } from '../../lib/error-report.js';
|
|
30
30
|
|
|
31
31
|
const MAX_PROJECTS = 32;
|
|
32
32
|
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
import { readFileSync, writeFileSync, unlinkSync, existsSync } from 'node:fs';
|
|
27
27
|
import { join } from 'node:path';
|
|
28
28
|
import { isPidAlive } from '../pid-alive.js';
|
|
29
|
-
import { reportSwallowed } from '
|
|
29
|
+
import { reportSwallowed } from '../../lib/error-report.js';
|
|
30
30
|
|
|
31
31
|
export const OWNER_LOCK_NAME = 'owner.lock';
|
|
32
32
|
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { statfsSync, existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
20
20
|
import { join, basename, sep } from 'node:path';
|
|
21
21
|
import { AsyncWriteQueue } from '../async-write-queue.js';
|
|
22
|
-
import { reportSwallowed } from '
|
|
22
|
+
import { reportSwallowed } from '../../lib/error-report.js';
|
|
23
23
|
import { ensureSessionDirSync, compactLocalTs14, sanitizePathComponent, normalizeSessionCwd } from './layout.js';
|
|
24
24
|
import { resolveSessionDirName, latestMainSession } from './session-select.js';
|
|
25
25
|
import { acquireSessionClaim, releaseSessionClaim, isForeignLiveOwned } from './session-owner.js';
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* knee on large sessions; q5 collapses to ~2.5x there).
|
|
20
20
|
*/
|
|
21
21
|
import zlib from 'node:zlib';
|
|
22
|
-
import { reportSwallowed } from '
|
|
22
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
23
23
|
import { awaitDrainOrClose } from './sse-backpressure.js';
|
|
24
24
|
|
|
25
25
|
const C = zlib.constants;
|
package/server/lib/ws-mux.js
CHANGED
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
*/
|
|
43
43
|
import { randomBytes } from 'node:crypto';
|
|
44
44
|
import { EventEmitter } from 'node:events';
|
|
45
|
-
import { reportSwallowed as defaultReport } from '
|
|
45
|
+
import { reportSwallowed as defaultReport } from '../lib/error-report.js';
|
|
46
46
|
import { createBackpressureGate } from './ws-backpressure.js';
|
|
47
47
|
|
|
48
48
|
const STREAM_ID_RE = /^[A-Za-z0-9_-]{1,64}$/;
|
package/server/proxy.js
CHANGED
|
@@ -13,7 +13,7 @@ import { parseAgentId, findHeader } from './lib/v2/agent-id.js';
|
|
|
13
13
|
import { liveSystemPromptEnabled } from './lib/system-prompt-live.js';
|
|
14
14
|
import { executeRequest, extractModel } from './lib/proxy/proxy-retry.js';
|
|
15
15
|
import { buildRecord, appendRecord, dailyFilePath, todayStr, emitProxyStatsUpdate } from './lib/proxy/proxy-stats.js';
|
|
16
|
-
import { reportSwallowed } from '
|
|
16
|
+
import { reportSwallowed } from './lib/error-report.js';
|
|
17
17
|
import { LOG_DIR } from '../findcc.js';
|
|
18
18
|
|
|
19
19
|
// Setup interceptor to patch fetch
|
package/server/pty-manager.js
CHANGED
|
@@ -17,7 +17,7 @@ import { randomBytes } from 'node:crypto';
|
|
|
17
17
|
// (shared with the SDK link). Re-exported here for existing consumers/tests.
|
|
18
18
|
import { withDefaultThinkingDisplay, resolveLaunchSystemPrompt, insertBeforeDashDash } from './lib/launch-config.js';
|
|
19
19
|
export { withDefaultThinkingDisplay } from './lib/launch-config.js';
|
|
20
|
-
import { reportSwallowed } from '
|
|
20
|
+
import { reportSwallowed } from './lib/error-report.js';
|
|
21
21
|
import { t, tFor } from './i18n.js';
|
|
22
22
|
|
|
23
23
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -6,7 +6,7 @@ import { filterClientsByScope } from '../lib/v2/view-router.js';
|
|
|
6
6
|
import { _projectName } from '../interceptor.js';
|
|
7
7
|
import { readPrefsRaw } from '../lib/prefs-store.js';
|
|
8
8
|
import { getCurrentProjectKey, hasFork, resolveScoped } from '../lib/project-prefs.js';
|
|
9
|
-
import { reportSwallowed } from '
|
|
9
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
10
10
|
|
|
11
11
|
function pendingAsks(req, res, parsedUrl, isLocal, deps) {
|
|
12
12
|
try {
|
package/server/routes/events.js
CHANGED
|
@@ -5,7 +5,7 @@ import { _projectName, getLiveLogSource, isContinuedLaunch } from '../intercepto
|
|
|
5
5
|
import { LOG_DIR } from '../../findcc.js';
|
|
6
6
|
import { streamRawEntriesAsync } from '../lib/log-stream.js';
|
|
7
7
|
import { migrationStatus } from '../lib/v2/migrate-prompt.js';
|
|
8
|
-
import { reportSwallowed } from '
|
|
8
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
9
9
|
import { sseHead, sseWrite, needsDrain, wireEnd, awaitWireDrain, isWireV3Enabled } from '../lib/wire-compress.js';
|
|
10
10
|
import { scopeKeyForProducer } from '../lib/scope-router.js';
|
|
11
11
|
import { readV2ColdBundle } from '../lib/v2/meta-rows.js';
|
package/server/routes/expert.js
CHANGED
|
@@ -19,7 +19,7 @@ import {
|
|
|
19
19
|
matchBuiltinModelPrompt, isBuiltinDisabled,
|
|
20
20
|
readBuiltinDisabled, setBuiltinDisabled, listBuiltinModelPrompts,
|
|
21
21
|
} from '../lib/builtin-model-prompts.js';
|
|
22
|
-
import { reportSwallowed } from '
|
|
22
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
23
23
|
import { resolveSpawnModel } from '../lib/spawn-model-resolver.js';
|
|
24
24
|
import { listSystemPromptPresets, groupPresetsByCategory, getSystemPromptVariablesDoc } from '../lib/system-prompt-presets.js';
|
|
25
25
|
import { LOG_DIR } from '../../findcc.js';
|
|
@@ -6,7 +6,7 @@ import { ERROR_STATUS_MAP } from '../lib/file-api.js';
|
|
|
6
6
|
import { viewRootOrReply } from '../lib/view-root.js';
|
|
7
7
|
import { loadWorkspaces } from '../workspace-registry.js';
|
|
8
8
|
import { listLivePtys } from '../pty-manager.js';
|
|
9
|
-
import { reportSwallowed } from '
|
|
9
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
10
10
|
import { discoverClaudeMdCandidates, readCandidateById } from '../lib/claude-md-discovery.js';
|
|
11
11
|
import { getClaudeConfigDir } from '../../findcc.js';
|
|
12
12
|
import { _projectName } from '../interceptor.js';
|
|
@@ -125,8 +125,10 @@ function importFile(req, res, parsedUrl, isLocal, deps) {
|
|
|
125
125
|
res.end(JSON.stringify({ error: 'Missing boundary' }));
|
|
126
126
|
return;
|
|
127
127
|
}
|
|
128
|
-
|
|
129
|
-
|
|
128
|
+
// Reuse the parsedUrl that handleRequest already validated: req.headers.host may be a
|
|
129
|
+
// malformed value llhttp accepts but URL parsing rejects, and a fresh new URL() here
|
|
130
|
+
// would throw synchronously inside dispatch → unhandled rejection.
|
|
131
|
+
const dir = parsedUrl.searchParams.get('dir') || '';
|
|
130
132
|
// 在 mkdirSync 之前纯字符串校验,防 symlink 副作用目录被创建在项目外
|
|
131
133
|
const dirCheck = validateImportDir(dir);
|
|
132
134
|
if (!dirCheck.ok) {
|
package/server/routes/misc.js
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
import { getUserProfile } from '../lib/user-profile.js';
|
|
3
3
|
import { runWaterfallHook } from '../lib/plugin-loader.js';
|
|
4
4
|
import { normalizeBasePath } from '../lib/base-path.js';
|
|
5
|
+
import { isAdminReq } from '../lib/is-admin.js';
|
|
6
|
+
import { isSameOriginBrowserRequest } from '../lib/same-origin.js';
|
|
5
7
|
|
|
6
8
|
async function userProfile(req, res) {
|
|
7
9
|
const profile = await getUserProfile();
|
|
@@ -10,6 +12,15 @@ async function userProfile(req, res) {
|
|
|
10
12
|
}
|
|
11
13
|
|
|
12
14
|
async function localUrl(req, res, parsedUrl, isLocal, deps) {
|
|
15
|
+
// The response embeds the full ACCESS_TOKEN. Without this gate the permissive global
|
|
16
|
+
// CORS prelude would let any cross-site page read the token from a loopback server
|
|
17
|
+
// (loopback skips auth) and then bypass every Origin/admin check end-to-end.
|
|
18
|
+
// CLI/native clients (no Origin header) and same-origin admin pages stay allowed.
|
|
19
|
+
if (!isAdminReq(req, isLocal) || !isSameOriginBrowserRequest(req, parsedUrl)) {
|
|
20
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
21
|
+
res.end(JSON.stringify({ error: 'Forbidden: admin and same-origin required' }));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
13
24
|
const localIp = deps.getLocalIp();
|
|
14
25
|
// 反代子路径部署时分享/二维码 URL 也要带前缀,否则扫码绕过代理直连源站端口(与
|
|
15
26
|
// server.startedNetwork 启动打印保持一致)。未设 CCV_BASE_PATH 时为空串,行为不变。
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Routes for service-plugin proxies (/plugins/<name>/*) and panel bundles
|
|
2
|
+
// (/plugin-panels/<name>/*). Registered in server.js's _routes after pluginsRoutes.
|
|
3
|
+
// Unknown plugin names return 404 here — requests must NOT fall through to the SPA
|
|
4
|
+
// fallback (it would serve index.html as JS/JSON).
|
|
5
|
+
import { getPluginEntries } from '../lib/plugin-loader.js';
|
|
6
|
+
import { tryProxyHttp } from '../lib/plugin-runtime.js';
|
|
7
|
+
import { servePanelFile } from '../lib/plugin-panels.js';
|
|
8
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
9
|
+
import { isSameOriginBrowserRequest } from '../lib/same-origin.js';
|
|
10
|
+
|
|
11
|
+
function findPanelEntry(name) {
|
|
12
|
+
for (const entry of getPluginEntries()) {
|
|
13
|
+
if (entry.shape === 'dir' && entry.enabled && !entry.loadError && entry.panel && entry.name === name) {
|
|
14
|
+
return entry;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function pluginProxyHandler(req, res, parsedUrl) {
|
|
21
|
+
try {
|
|
22
|
+
// Plugin services are same-machine capabilities reachable through the main origin.
|
|
23
|
+
// A cross-site browser page must not drive them just because loopback trusts the
|
|
24
|
+
// socket peer; CLI/native callers (no Origin) and same-origin pages pass.
|
|
25
|
+
if (!isSameOriginBrowserRequest(req, parsedUrl)) {
|
|
26
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
27
|
+
res.end(JSON.stringify({ error: 'Forbidden: cross-site plugin proxy request' }));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const handled = tryProxyHttp(req, res, parsedUrl);
|
|
31
|
+
if (!handled) {
|
|
32
|
+
res.writeHead(404, { 'Content-Type': 'application/json' });
|
|
33
|
+
res.end(JSON.stringify({ error: 'unknown plugin service' }));
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
reportSwallowed('plugin-services.proxy-route', err);
|
|
37
|
+
try {
|
|
38
|
+
if (!res.headersSent) {
|
|
39
|
+
res.writeHead(502, { 'Content-Type': 'application/json' });
|
|
40
|
+
res.end(JSON.stringify({ error: 'plugin proxy failure' }));
|
|
41
|
+
} else {
|
|
42
|
+
res.destroy();
|
|
43
|
+
}
|
|
44
|
+
} catch { /* noop */ }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function pluginPanelHandler(req, res, parsedUrl) {
|
|
49
|
+
try {
|
|
50
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
51
|
+
res.writeHead(405, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
52
|
+
res.end('method not allowed');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// /plugin-panels/<name>/<rel...>
|
|
56
|
+
const rest = parsedUrl.pathname.slice('/plugin-panels/'.length);
|
|
57
|
+
const slash = rest.indexOf('/');
|
|
58
|
+
const name = slash === -1 ? rest : rest.slice(0, slash);
|
|
59
|
+
const relPath = slash === -1 ? '' : rest.slice(slash + 1);
|
|
60
|
+
let decodedName;
|
|
61
|
+
try {
|
|
62
|
+
decodedName = decodeURIComponent(name);
|
|
63
|
+
} catch {
|
|
64
|
+
res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
65
|
+
res.end('bad plugin name');
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const entry = findPanelEntry(decodedName);
|
|
69
|
+
if (!entry) {
|
|
70
|
+
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
71
|
+
res.end('unknown plugin panel');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
if (!relPath) {
|
|
75
|
+
// Bare directory path -> entry bundle (relative rootDir = drop the first segment of entryRel)
|
|
76
|
+
const entryRel = entry.panel.entryRel;
|
|
77
|
+
const rel = entryRel.slice(entryRel.indexOf('/') + 1);
|
|
78
|
+
servePanelFile(req, res, entry, rel);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
servePanelFile(req, res, entry, relPath);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
reportSwallowed('plugin-services.panel-route', err);
|
|
84
|
+
try {
|
|
85
|
+
if (!res.headersSent) {
|
|
86
|
+
res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
87
|
+
res.end('panel route failure');
|
|
88
|
+
} else {
|
|
89
|
+
res.destroy();
|
|
90
|
+
}
|
|
91
|
+
} catch { /* noop */ }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const pluginServicesRoutes = [
|
|
96
|
+
{
|
|
97
|
+
predicate: (url) => url === '/plugins' || url.startsWith('/plugins/'),
|
|
98
|
+
handler: pluginProxyHandler,
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
predicate: (url) => url === '/plugin-panels' || url.startsWith('/plugin-panels/'),
|
|
102
|
+
handler: pluginPanelHandler,
|
|
103
|
+
},
|
|
104
|
+
];
|