cc-viewer 1.9.9 → 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.
Files changed (138) hide show
  1. package/README.md +1 -1
  2. package/cli.js +4 -3
  3. package/dist/assets/App-Ci9GYs24.js +2 -0
  4. package/dist/assets/{MdxEditorPanel-BJVD_bcU.js → MdxEditorPanel-8fwlNHMz.js} +1 -1
  5. package/dist/assets/Mobile-DngSOqYr.js +1 -0
  6. package/dist/assets/ProxyStatsModal-CWSIELLj.js +1 -0
  7. package/dist/assets/{_baseUniq-CwQykUuc.js → _baseUniq-6ZLcdMmL.js} +1 -1
  8. package/dist/assets/{arc-EdUdBxtG.js → arc-Dirf6EZP.js} +1 -1
  9. package/dist/assets/{architectureDiagram-Q4EWVU46-DtBIdElY.js → architectureDiagram-Q4EWVU46-BfIhmu_I.js} +1 -1
  10. package/dist/assets/{blockDiagram-DXYQGD6D-guNZIKkZ.js → blockDiagram-DXYQGD6D-D0y4ALi1.js} +1 -1
  11. package/dist/assets/{c4Diagram-AHTNJAMY-xV5vB-Z9.js → c4Diagram-AHTNJAMY-Dlfp4SUK.js} +1 -1
  12. package/dist/assets/{channel-DYEktNap.js → channel-CaNcU8SI.js} +1 -1
  13. package/dist/assets/{chunk-4BX2VUAB-ZTkItpvj.js → chunk-4BX2VUAB-ki0vHNS-.js} +1 -1
  14. package/dist/assets/{chunk-4TB4RGXK-dYIewyyR.js → chunk-4TB4RGXK-D8yO8Iuv.js} +1 -1
  15. package/dist/assets/{chunk-55IACEB6-CSsPNgPX.js → chunk-55IACEB6-BvIgaNPO.js} +1 -1
  16. package/dist/assets/{chunk-EDXVE4YY-CnMiuqzQ.js → chunk-EDXVE4YY-DF2Of_VO.js} +1 -1
  17. package/dist/assets/{chunk-FMBD7UC4-CVpATPZ3.js → chunk-FMBD7UC4-BIw7SXs-.js} +1 -1
  18. package/dist/assets/{chunk-OYMX7WX6-Dpq4s8dW.js → chunk-OYMX7WX6-AB76s0A0.js} +1 -1
  19. package/dist/assets/{chunk-QZHKN3VN-Bnojh6X6.js → chunk-QZHKN3VN-CuN2LCFc.js} +1 -1
  20. package/dist/assets/{chunk-YZCP3GAM-BYqK4Dsa.js → chunk-YZCP3GAM-D4DLBzBN.js} +1 -1
  21. package/dist/assets/classDiagram-6PBFFD2Q-DLJzWgYm.js +1 -0
  22. package/dist/assets/classDiagram-v2-HSJHXN6E-DLJzWgYm.js +1 -0
  23. package/dist/assets/clone-H_VGsHn9.js +1 -0
  24. package/dist/assets/{cose-bilkent-S5V4N54A-OJ7mmWyV.js → cose-bilkent-S5V4N54A-Bn485PX9.js} +1 -1
  25. package/dist/assets/{dagre-KV5264BT-B1Kwd_Rk.js → dagre-KV5264BT-e4QVCf17.js} +1 -1
  26. package/dist/assets/{diagram-5BDNPKRD-D8qrw8sp.js → diagram-5BDNPKRD-_SBFGgnf.js} +1 -1
  27. package/dist/assets/{diagram-G4DWMVQ6-ip-N4ho8.js → diagram-G4DWMVQ6-Bz1ovxLs.js} +1 -1
  28. package/dist/assets/{diagram-MMDJMWI5-2PZaskUF.js → diagram-MMDJMWI5-B-oojW8K.js} +1 -1
  29. package/dist/assets/{diagram-TYMM5635-CeAOQUZP.js → diagram-TYMM5635-CTAyNaOj.js} +1 -1
  30. package/dist/assets/{erDiagram-SMLLAGMA-BCBaEYEb.js → erDiagram-SMLLAGMA-BfvKAqq2.js} +1 -1
  31. package/dist/assets/{flowDiagram-DWJPFMVM-CsGyIqcX.js → flowDiagram-DWJPFMVM-BpTEZEAe.js} +1 -1
  32. package/dist/assets/{ganttDiagram-T4ZO3ILL-COsj3FZ-.js → ganttDiagram-T4ZO3ILL-DUD_vkyE.js} +1 -1
  33. package/dist/assets/{gitGraphDiagram-UUTBAWPF-DvQVQGJG.js → gitGraphDiagram-UUTBAWPF-DnlXcTud.js} +1 -1
  34. package/dist/assets/{graph-BSB9fDSP.js → graph-D4xsUKSn.js} +1 -1
  35. package/dist/assets/{index-DKufsAzx.js → index-84-BjpGl.js} +1 -1
  36. package/dist/assets/index-BDKwOnE3.js +2 -0
  37. package/dist/assets/{index-Bo-12bug.js → index-BniBLRtZ.js} +1 -1
  38. package/dist/assets/{index-BRw4Iz2O.js → index-C2Zo3KOg.js} +1 -1
  39. package/dist/assets/{index-BuVComcY.js → index-CWZoKT7e.js} +1 -1
  40. package/dist/assets/{index-eM2ih5n7.js → index-D-4EsFub.js} +1 -1
  41. package/dist/assets/{index-CNNAYjkF.js → index-D3KzRmkP.js} +1 -1
  42. package/dist/assets/{index-DXBfwPYD.js → index-NGgdum2i.js} +1 -1
  43. package/dist/assets/{infoDiagram-42DDH7IO-eNucS4iW.js → infoDiagram-42DDH7IO-BbdvmCDt.js} +1 -1
  44. package/dist/assets/{ishikawaDiagram-UXIWVN3A-Dxxj-gUX.js → ishikawaDiagram-UXIWVN3A-oZBOPoTO.js} +1 -1
  45. package/dist/assets/{journeyDiagram-VCZTEJTY-yJJzVEBb.js → journeyDiagram-VCZTEJTY-DWoH0hs9.js} +1 -1
  46. package/dist/assets/{jszip.min-ByI0ds5u.js → jszip.min-I5y9RZhq.js} +1 -1
  47. package/dist/assets/{kanban-definition-6JOO6SKY-2rfTi2zw.js → kanban-definition-6JOO6SKY-BLHtjKsS.js} +1 -1
  48. package/dist/assets/{layout-DTmHj0Wm.js → layout-C116SNcQ.js} +1 -1
  49. package/dist/assets/{linear-BOA3a9FE.js → linear-Br08WCkF.js} +1 -1
  50. package/dist/assets/{mermaid.core-5H-NR79H.js → mermaid.core-D-4dTrvI.js} +2 -2
  51. package/dist/assets/{min-CpjFTEec.js → min-khs74HbH.js} +1 -1
  52. package/dist/assets/{mindmap-definition-QFDTVHPH-B8KCjFgB.js → mindmap-definition-QFDTVHPH-C3_yItFF.js} +1 -1
  53. package/dist/assets/{pieDiagram-DEJITSTG-BARy96g5.js → pieDiagram-DEJITSTG-ByCwM2_P.js} +1 -1
  54. package/dist/assets/{quadrantDiagram-34T5L4WZ-DYoTapuI.js → quadrantDiagram-34T5L4WZ-CjV65X1r.js} +1 -1
  55. package/dist/assets/{requirementDiagram-MS252O5E-EeANLLK2.js → requirementDiagram-MS252O5E-DFJ_dbtV.js} +1 -1
  56. package/dist/assets/{sankeyDiagram-XADWPNL6-DmQ-YVPj.js → sankeyDiagram-XADWPNL6-BBvXoBfj.js} +1 -1
  57. package/dist/assets/{seqResourceLoaders-C0cU2e1o.css → seqResourceLoaders-C34z9uOa.css} +2 -2
  58. package/dist/assets/seqResourceLoaders-JJzgZZzU.js +2 -0
  59. package/dist/assets/{sequenceDiagram-FGHM5R23-SZtW0rYC.js → sequenceDiagram-FGHM5R23-BJM04Y3N.js} +1 -1
  60. package/dist/assets/{stateDiagram-FHFEXIEX-CEygsIld.js → stateDiagram-FHFEXIEX-DI2q_Kkn.js} +1 -1
  61. package/dist/assets/{stateDiagram-v2-QKLJ7IA2-BU1XPhBe.js → stateDiagram-v2-QKLJ7IA2-CrGC6kEY.js} +1 -1
  62. package/dist/assets/{timeline-definition-GMOUNBTQ-DhPBaQBt.js → timeline-definition-GMOUNBTQ-JcfRUsjD.js} +1 -1
  63. package/dist/assets/vendor-antd-DxEVqVhD.js +17 -0
  64. package/dist/assets/vendor-codemirror-CgNlkZva.js +1 -0
  65. package/dist/assets/{vendor-mdxeditor-Cz09a1_I.js → vendor-mdxeditor-DoreQf4A.js} +2 -2
  66. package/dist/assets/{vendor-qrcode-qbbntD6-.js → vendor-qrcode-q1CKtOyX.js} +1 -1
  67. package/dist/assets/{vendor-virtuoso-Sk8fQ4Hx.js → vendor-virtuoso-BXGe9q7E.js} +1 -1
  68. package/dist/assets/{vennDiagram-DHZGUBPP-NniTHxDw.js → vennDiagram-DHZGUBPP-mAHR1Ugj.js} +1 -1
  69. package/dist/assets/{wardley-RL74JXVD-D9SNOrDw.js → wardley-RL74JXVD-mlTq_P-4.js} +1 -1
  70. package/dist/assets/{wardleyDiagram-NUSXRM2D-DvHXKNza.js → wardleyDiagram-NUSXRM2D-BDGfajRb.js} +1 -1
  71. package/dist/assets/{xychartDiagram-5P7HB3ND-CaiwPTQB.js → xychartDiagram-5P7HB3ND-Cf2pR1mV.js} +1 -1
  72. package/dist/index.html +4 -4
  73. package/package.json +1 -1
  74. package/plugins/README.md +9 -1
  75. package/server/interceptor.js +1 -1
  76. package/server/lib/builtin-model-prompts.js +1 -1
  77. package/server/lib/chat-queue.js +1 -1
  78. package/server/lib/credential-access.js +1 -1
  79. package/server/lib/credential-migrate.js +1 -1
  80. package/server/lib/error-report.js +26 -0
  81. package/server/lib/json-store.js +1 -1
  82. package/server/lib/launch-config.js +1 -1
  83. package/server/lib/log-watcher.js +1 -1
  84. package/server/lib/plugin-host.mjs +198 -0
  85. package/server/lib/plugin-loader.js +346 -90
  86. package/server/lib/plugin-manager.js +46 -2
  87. package/server/lib/plugin-manifest.js +329 -0
  88. package/server/lib/plugin-panels.js +183 -0
  89. package/server/lib/plugin-proxy.js +408 -0
  90. package/server/lib/plugin-runtime.js +775 -0
  91. package/server/lib/plugin-service-manager.js +463 -0
  92. package/server/lib/proxy/proxy-retry.js +1 -1
  93. package/server/lib/proxy/proxy-stats.js +1 -1
  94. package/server/lib/resume-watchdog.js +1 -1
  95. package/server/lib/same-origin.js +13 -3
  96. package/server/lib/sdk-manager.js +16 -9
  97. package/server/lib/sdk-session-index.js +1 -1
  98. package/server/lib/stats-worker.js +1 -1
  99. package/server/lib/system-prompt-files.js +1 -1
  100. package/server/lib/system-prompt-live.js +1 -1
  101. package/server/lib/system-prompt-snapshots.js +1 -1
  102. package/server/lib/v2/adapter.js +1 -1
  103. package/server/lib/v2/convert.js +1 -1
  104. package/server/lib/v2/jsonl-read.js +1 -1
  105. package/server/lib/v2/live-feed.js +1 -1
  106. package/server/lib/v2/meta-rows.js +1 -1
  107. package/server/lib/v2/resume-list.js +1 -1
  108. package/server/lib/v2/session-list.js +1 -1
  109. package/server/lib/v2/session-owner.js +1 -1
  110. package/server/lib/v2/v2-writer.js +1 -1
  111. package/server/lib/wire-compress.js +1 -1
  112. package/server/lib/ws-mux.js +1 -1
  113. package/server/proxy.js +1 -1
  114. package/server/pty-manager.js +1 -1
  115. package/server/routes/ask-perm.js +1 -1
  116. package/server/routes/events.js +1 -1
  117. package/server/routes/expert.js +1 -1
  118. package/server/routes/files-content.js +1 -1
  119. package/server/routes/files-fs.js +4 -2
  120. package/server/routes/misc.js +11 -0
  121. package/server/routes/plugin-services.js +104 -0
  122. package/server/routes/plugins.js +145 -17
  123. package/server/routes/preferences.js +1 -1
  124. package/server/routes/resume.js +1 -1
  125. package/server/routes/sdk-context.js +1 -1
  126. package/server/routes/sdk-mcp-status.js +1 -1
  127. package/server/routes/workspaces.js +1 -1
  128. package/server/server.js +328 -171
  129. package/dist/assets/App-CLteNB83.js +0 -2
  130. package/dist/assets/Mobile-CJCQf3MQ.js +0 -1
  131. package/dist/assets/ProxyStatsModal-B5ccoOPy.js +0 -1
  132. package/dist/assets/classDiagram-6PBFFD2Q-ClGF33bF.js +0 -1
  133. package/dist/assets/classDiagram-v2-HSJHXN6E-ClGF33bF.js +0 -1
  134. package/dist/assets/clone-CFj55h49.js +0 -1
  135. package/dist/assets/index-CqX5TkEL.js +0 -2
  136. package/dist/assets/seqResourceLoaders-7lsgbvGa.js +0 -2
  137. package/dist/assets/vendor-antd-CS_S2mBN.js +0 -17
  138. package/dist/assets/vendor-codemirror-B5l-VbBO.js +0 -1
@@ -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 '@ccv/core/error-report';
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 '@ccv/core/error-report';
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 '@ccv/core/error-report';
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 '@ccv/core/error-report';
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 '@ccv/core/error-report';
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 '@ccv/core/error-report';
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 '@ccv/core/error-report';
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 '@ccv/core/error-report';
22
+ import { reportSwallowed } from '../lib/error-report.js';
23
23
  import { awaitDrainOrClose } from './sse-backpressure.js';
24
24
 
25
25
  const C = zlib.constants;
@@ -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 '@ccv/core/error-report';
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 '@ccv/core/error-report';
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
@@ -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 '@ccv/core/error-report';
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 '@ccv/core/error-report';
9
+ import { reportSwallowed } from '../lib/error-report.js';
10
10
 
11
11
  function pendingAsks(req, res, parsedUrl, isLocal, deps) {
12
12
  try {
@@ -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 '@ccv/core/error-report';
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';
@@ -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 '@ccv/core/error-report';
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 '@ccv/core/error-report';
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
- const importUrl = new URL(req.url, `${deps.protocol}://${req.headers.host}`);
129
- const dir = importUrl.searchParams.get('dir') || '';
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) {
@@ -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
+ ];
@@ -1,17 +1,84 @@
1
1
  // Plugin management routes (moved verbatim from server.js handleRequest).
2
+ // Write operations go through the plugin runtime's single-flight queue so service
3
+ // plugins are deactivated/stopped/re-spawned — never a bare loadPlugins() that would
4
+ // leave child processes behind.
2
5
  import { existsSync, unlinkSync } from 'node:fs';
3
6
  import { join } from 'node:path';
4
- import { loadPlugins, getPluginsInfo, getPluginsDir } from '../lib/plugin-loader.js';
5
- import { uploadPlugins, installPluginFromUrl } from '../lib/plugin-manager.js';
7
+ import { getPluginsInfo, getPluginsDir, getPluginEntries } from '../lib/plugin-loader.js';
8
+ import { uploadPlugins, installPluginFromUrl, deletePluginDir } from '../lib/plugin-manager.js';
9
+ import {
10
+ reloadRuntime,
11
+ unloadPlugin,
12
+ restartPlugin,
13
+ getRuntimePluginStatus,
14
+ isRuntimeStopping,
15
+ } from '../lib/plugin-runtime.js';
16
+ import { buildPanelDescriptor } from '../lib/plugin-panels.js';
6
17
  import { SERVER_LIB } from '../_paths.js';
18
+ import { reportSwallowed } from '../lib/error-report.js';
19
+ import { isAdminReq } from '../lib/is-admin.js';
20
+ import { isSameOriginBrowserRequest } from '../lib/same-origin.js';
21
+
22
+ // Write endpoints execute arbitrary plugin code (reload/restart), install remote files,
23
+ // or delete on-disk plugins. They require an admin AND a same-origin browser context:
24
+ // the global ACAO:* prelude would otherwise let a cross-site page drive a localhost
25
+ // server that trusts its socket peer. CLI/native clients (no Origin) stay allowed.
26
+ function rejectWrite(req, res, isLocal, parsedUrl) {
27
+ // Refuse during the shutdown teardown window: a reload/restart accepted here could
28
+ // spawn a child after onServerStopping already swept the runtime, leaking a lock file.
29
+ if (isRuntimeStopping()) {
30
+ res.writeHead(503, { 'Content-Type': 'application/json' });
31
+ res.end(JSON.stringify({ error: 'server is stopping' }));
32
+ return true;
33
+ }
34
+ if (isAdminReq(req, isLocal) && isSameOriginBrowserRequest(req, parsedUrl)) return false;
35
+ res.writeHead(403, { 'Content-Type': 'application/json' });
36
+ res.end(JSON.stringify({ error: 'Forbidden: admin and same-origin required' }));
37
+ return true;
38
+ }
39
+
40
+ function decorateWithRuntime(plugins) {
41
+ return plugins.map(p => {
42
+ // Only service plugins have a service status; lifecycle-only dir plugins also get a
43
+ // runtime entry, but exposing it as serviceStatus would imply a service exists.
44
+ if (p.type !== 'service') return p;
45
+ const status = getRuntimePluginStatus(p.name);
46
+ return status ? { ...p, serviceStatus: status } : p;
47
+ });
48
+ }
7
49
 
8
50
  function pluginsList(req, res) {
9
- const plugins = getPluginsInfo();
51
+ const plugins = decorateWithRuntime(getPluginsInfo());
10
52
  res.writeHead(200, { 'Content-Type': 'application/json' });
11
53
  res.end(JSON.stringify({ plugins, pluginsDir: getPluginsDir() }));
12
54
  }
13
55
 
14
- async function pluginsDelete(req, res, parsedUrl) {
56
+ async function pluginsDelete(req, res, parsedUrl, isLocal) {
57
+ if (rejectWrite(req, res, isLocal, parsedUrl)) return;
58
+ const dir = parsedUrl.searchParams.get('dir');
59
+ if (dir) {
60
+ try {
61
+ const unloaded = await unloadPlugin(dir, 'delete');
62
+ if (!unloaded.ok) {
63
+ res.writeHead(503, { 'Content-Type': 'application/json' });
64
+ res.end(JSON.stringify({ error: unloaded.error || 'plugin runtime is stopping' }));
65
+ return;
66
+ }
67
+ deletePluginDir(getPluginsDir(), dir);
68
+ const result = await reloadRuntime();
69
+ if (!result.ok) {
70
+ res.writeHead(503, { 'Content-Type': 'application/json' });
71
+ res.end(JSON.stringify({ error: result.error || 'plugin runtime is stopping' }));
72
+ return;
73
+ }
74
+ res.writeHead(200, { 'Content-Type': 'application/json' });
75
+ res.end(JSON.stringify({ ok: true, plugins: decorateWithRuntime(getPluginsInfo()), pluginsDir: getPluginsDir(), generation: result.generation }));
76
+ } catch (err) {
77
+ res.writeHead(err.statusCode || 500, { 'Content-Type': 'application/json' });
78
+ res.end(JSON.stringify({ error: err.message }));
79
+ }
80
+ return;
81
+ }
15
82
  const file = parsedUrl.searchParams.get('file');
16
83
  if (!file || file.includes('..') || file.includes('/') || file.includes('\\')) {
17
84
  res.writeHead(400, { 'Content-Type': 'application/json' });
@@ -26,39 +93,93 @@ async function pluginsDelete(req, res, parsedUrl) {
26
93
  return;
27
94
  }
28
95
  unlinkSync(filePath);
29
- await loadPlugins();
30
- const plugins = getPluginsInfo();
96
+ const result = await reloadRuntime();
97
+ if (!result.ok) {
98
+ res.writeHead(503, { 'Content-Type': 'application/json' });
99
+ res.end(JSON.stringify({ error: result.error || 'plugin runtime is stopping' }));
100
+ return;
101
+ }
31
102
  res.writeHead(200, { 'Content-Type': 'application/json' });
32
- res.end(JSON.stringify({ ok: true, plugins, pluginsDir: getPluginsDir() }));
103
+ res.end(JSON.stringify({ ok: true, plugins: decorateWithRuntime(getPluginsInfo()), pluginsDir: getPluginsDir(), generation: result.generation }));
33
104
  } catch (err) {
34
105
  res.writeHead(500, { 'Content-Type': 'application/json' });
35
106
  res.end(JSON.stringify({ error: err.message }));
36
107
  }
37
108
  }
38
109
 
39
- async function pluginsReload(req, res) {
110
+ async function pluginsReload(req, res, parsedUrl, isLocal) {
111
+ if (rejectWrite(req, res, isLocal, parsedUrl)) return;
40
112
  try {
41
- await loadPlugins();
42
- const plugins = getPluginsInfo();
113
+ // ?reason=disable: the UI disable toggle goes through preferences + reload, so deactivate must receive 'disable'
114
+ const reason = parsedUrl && parsedUrl.searchParams.get('reason');
115
+ const result = await reloadRuntime(reason === 'disable' ? { reason: 'disable' } : {});
116
+ if (!result.ok) {
117
+ res.writeHead(503, { 'Content-Type': 'application/json' });
118
+ res.end(JSON.stringify({ error: result.error || 'plugin runtime is stopping' }));
119
+ return;
120
+ }
43
121
  res.writeHead(200, { 'Content-Type': 'application/json' });
44
- res.end(JSON.stringify({ ok: true, plugins, pluginsDir: getPluginsDir() }));
122
+ res.end(JSON.stringify({ ok: true, plugins: decorateWithRuntime(getPluginsInfo()), pluginsDir: getPluginsDir(), generation: result.generation }));
123
+ } catch (err) {
124
+ reportSwallowed('plugins.reload', err);
125
+ res.writeHead(500, { 'Content-Type': 'application/json' });
126
+ res.end(JSON.stringify({ error: err.message }));
127
+ }
128
+ }
129
+
130
+ async function pluginsRestart(req, res, parsedUrl, isLocal) {
131
+ if (rejectWrite(req, res, isLocal, parsedUrl)) return;
132
+ const name = parsedUrl.searchParams.get('name');
133
+ if (!name) {
134
+ res.writeHead(400, { 'Content-Type': 'application/json' });
135
+ res.end(JSON.stringify({ error: 'name is required' }));
136
+ return;
137
+ }
138
+ try {
139
+ const result = await restartPlugin(name);
140
+ res.writeHead(result.ok ? 200 : 400, { 'Content-Type': 'application/json' });
141
+ res.end(JSON.stringify(result));
142
+ } catch (err) {
143
+ reportSwallowed('plugins.restart', err);
144
+ res.writeHead(500, { 'Content-Type': 'application/json' });
145
+ res.end(JSON.stringify({ error: err.message }));
146
+ }
147
+ }
148
+
149
+ function pluginPanelsList(req, res) {
150
+ try {
151
+ const panels = [];
152
+ for (const entry of getPluginEntries()) {
153
+ if (entry.shape !== 'dir' || !entry.enabled || entry.loadError || !entry.panel) continue;
154
+ const status = getRuntimePluginStatus(entry.name);
155
+ panels.push(buildPanelDescriptor(entry, { serviceStatus: status }));
156
+ }
157
+ panels.sort((a, b) => a.name.localeCompare(b.name));
158
+ res.writeHead(200, { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' });
159
+ res.end(JSON.stringify({ ok: true, panels }));
45
160
  } catch (err) {
161
+ reportSwallowed('plugins.panels', err);
46
162
  res.writeHead(500, { 'Content-Type': 'application/json' });
47
163
  res.end(JSON.stringify({ error: err.message }));
48
164
  }
49
165
  }
50
166
 
51
167
  function pluginsUpload(req, res, parsedUrl, isLocal, deps) {
168
+ if (rejectWrite(req, res, isLocal, parsedUrl)) return;
52
169
  let body = '';
53
170
  req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
54
171
  req.on('end', async () => {
55
172
  try {
56
173
  const { files: fileList } = JSON.parse(body);
57
174
  uploadPlugins(getPluginsDir(), fileList);
58
- await loadPlugins();
59
- const plugins = getPluginsInfo();
175
+ const result = await reloadRuntime();
176
+ if (!result.ok) {
177
+ res.writeHead(503, { 'Content-Type': 'application/json' });
178
+ res.end(JSON.stringify({ error: result.error || 'plugin runtime is stopping' }));
179
+ return;
180
+ }
60
181
  res.writeHead(200, { 'Content-Type': 'application/json' });
61
- res.end(JSON.stringify({ ok: true, plugins, pluginsDir: getPluginsDir() }));
182
+ res.end(JSON.stringify({ ok: true, plugins: decorateWithRuntime(getPluginsInfo()), pluginsDir: getPluginsDir(), generation: result.generation }));
62
183
  } catch (err) {
63
184
  const status = err.statusCode || 500;
64
185
  res.writeHead(status, { 'Content-Type': 'application/json' });
@@ -68,6 +189,7 @@ function pluginsUpload(req, res, parsedUrl, isLocal, deps) {
68
189
  }
69
190
 
70
191
  function pluginsInstallFromUrl(req, res, parsedUrl, isLocal, deps) {
192
+ if (rejectWrite(req, res, isLocal, parsedUrl)) return;
71
193
  let body = '';
72
194
  req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
73
195
  req.on('end', async () => {
@@ -75,10 +197,14 @@ function pluginsInstallFromUrl(req, res, parsedUrl, isLocal, deps) {
75
197
  const { url: fileUrl } = JSON.parse(body);
76
198
  const extractScript = join(SERVER_LIB, 'extract-plugin-name.mjs');
77
199
  await installPluginFromUrl(getPluginsDir(), fileUrl, extractScript);
78
- await loadPlugins();
79
- const plugins = getPluginsInfo();
200
+ const result = await reloadRuntime();
201
+ if (!result.ok) {
202
+ res.writeHead(503, { 'Content-Type': 'application/json' });
203
+ res.end(JSON.stringify({ error: result.error || 'plugin runtime is stopping' }));
204
+ return;
205
+ }
80
206
  res.writeHead(200, { 'Content-Type': 'application/json' });
81
- res.end(JSON.stringify({ ok: true, plugins, pluginsDir: getPluginsDir() }));
207
+ res.end(JSON.stringify({ ok: true, plugins: decorateWithRuntime(getPluginsInfo()), pluginsDir: getPluginsDir(), generation: result.generation }));
82
208
  } catch (err) {
83
209
  const status = err.statusCode || 500;
84
210
  res.writeHead(status, { 'Content-Type': 'application/json' });
@@ -91,6 +217,8 @@ export const pluginsRoutes = [
91
217
  { method: 'GET', match: 'exact', path: '/api/plugins', handler: pluginsList },
92
218
  { method: 'DELETE', match: 'exact', path: '/api/plugins', handler: pluginsDelete },
93
219
  { method: 'POST', match: 'exact', path: '/api/plugins/reload', handler: pluginsReload },
220
+ { method: 'POST', match: 'exact', path: '/api/plugins/restart', handler: pluginsRestart },
221
+ { method: 'GET', match: 'exact', path: '/api/plugin-panels', handler: pluginPanelsList },
94
222
  { method: 'POST', match: 'exact', path: '/api/plugins/upload', handler: pluginsUpload },
95
223
  { method: 'POST', match: 'exact', path: '/api/plugins/install-from-url', handler: pluginsInstallFromUrl },
96
224
  ];
@@ -7,7 +7,7 @@ import { PROFILE_PATH, _defaultConfig, getActiveProfileId, getStoredRoles, isOff
7
7
  import { migrateProxyProfileList, isValidRoleValue, PROXY_ROLE_KEYS } from '../lib/interceptor-core.js';
8
8
  import { DEFAULT_RETRY_CONFIG, validateRetryConfig, resolveRetryConfig } from '../lib/proxy/proxy-retry.js';
9
9
  import { discoverCcSwitchProviders, mergeImportedProfiles } from '../lib/ccswitch-import.js';
10
- import { reportSwallowed } from '@ccv/core/error-report';
10
+ import { reportSwallowed } from '../lib/error-report.js';
11
11
  import { setLang } from '../i18n.js';
12
12
  import { reconcileVoicePackPrefs as vpReconcile } from '../lib/voice-pack-manager.js';
13
13
  import { readClaudeProjectModel } from '../lib/context-watcher.js';
@@ -53,7 +53,7 @@ import { bumpWorkspacesVersion } from '../lib/file-access-policy.js';
53
53
  import { isAdminReq } from '../lib/is-admin.js';
54
54
  import { isSameOriginBrowserRequest } from '../lib/same-origin.js';
55
55
  import { sseWrite } from '../lib/wire-compress.js';
56
- import { reportSwallowed } from '@ccv/core/error-report';
56
+ import { reportSwallowed } from '../lib/error-report.js';
57
57
  import { findSdkSessionByInstance, listSdkSessionInstances } from '../lib/sdk-session-index.js';
58
58
 
59
59
  function sendJson(res, code, obj) {
@@ -2,7 +2,7 @@
2
2
  // the automatic turn-end/compact refreshes flow over the WS broadcast instead. A
3
3
  // `session-inactive` outcome is a NORMAL state (no turn run yet / query dead), so it
4
4
  // returns 200 with { ok:false, reason } rather than an HTTP error.
5
- import { reportSwallowed } from '@ccv/core/error-report';
5
+ import { reportSwallowed } from '../lib/error-report.js';
6
6
 
7
7
  async function sdkContextUsage(req, res) {
8
8
  try {
@@ -2,7 +2,7 @@
2
2
  // pull; there is no turn-driven refresh (MCP status is not turn-correlated). A
3
3
  // `session-inactive` outcome is a NORMAL state (no turn run yet / query dead), so it
4
4
  // returns 200 with { ok:false, reason } rather than an HTTP error.
5
- import { reportSwallowed } from '@ccv/core/error-report';
5
+ import { reportSwallowed } from '../lib/error-report.js';
6
6
 
7
7
  async function sdkMcpStatus(req, res) {
8
8
  try {
@@ -4,7 +4,7 @@ import { basename } from 'node:path';
4
4
  import { initForWorkspace, resetWorkspace, getLiveLogSource, markContinuedLaunch, markForkSession, markResumeSession, isContinuedLaunch, clearScopeState } from '../interceptor.js';
5
5
  import { LOG_DIR } from '../../findcc.js';
6
6
  import { migrationStatus } from '../lib/v2/migrate-prompt.js';
7
- import { reportSwallowed } from '@ccv/core/error-report';
7
+ import { reportSwallowed } from '../lib/error-report.js';
8
8
 
9
9
  // P2: claude continuation flags — the workspace launcher injects `-c` itself
10
10
  // (WorkspaceList's logCount heuristic), so argv scanning in cli.js never sees