cc-viewer 1.7.2 → 1.7.4
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/cli.js +109 -20
- package/dist/assets/App-CKvCavmF.js +2 -0
- package/dist/assets/App-Dj37fluO.css +1 -0
- package/dist/assets/{MdxEditorPanel-BUubiE6Y.js → MdxEditorPanel-DeSJp_ct.js} +1 -1
- package/dist/assets/{Mobile-CKmfRRvo.js → Mobile-CWou-jtF.js} +1 -1
- package/dist/assets/{_baseUniq-D9_E5ZDp.js → _baseUniq-BK7Nhp_x.js} +1 -1
- package/dist/assets/{arc-D4iFrlhb.js → arc-Cm4HMXz6.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-f1neo7hg.js → architectureDiagram-Q4EWVU46-Co8QCowN.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-DWi9e9tG.js → blockDiagram-DXYQGD6D-Cudecf7x.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-DAdj49A2.js → c4Diagram-AHTNJAMY-4JeFoCHX.js} +1 -1
- package/dist/assets/{channel-B3VthT0m.js → channel-K7eT7Hjr.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-BrPcAHV3.js → chunk-4BX2VUAB-BBjl6EVW.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-Bvopc1V9.js → chunk-4TB4RGXK-DBP9YnwO.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-Juw-7Z3i.js → chunk-55IACEB6--A_i9gwR.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-DVYP9tpB.js → chunk-EDXVE4YY-UBsL-3qr.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-VQ7VOXl3.js → chunk-FMBD7UC4-DBO0asmI.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-F8Rj4ZUZ.js → chunk-OYMX7WX6-DOCcEaE6.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-Dn7K3yi9.js → chunk-QZHKN3VN-B-j8QbQh.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-UjO-KTsn.js → chunk-YZCP3GAM-B0WO1U-U.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-Ba3tOmih.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-Ba3tOmih.js +1 -0
- package/dist/assets/clone-DjURvTeN.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BDhNj8rq.js → cose-bilkent-S5V4N54A-DQgpss_a.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-BzdcDuyt.js → dagre-KV5264BT-CCJBKmKv.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-DP_SX65N.js → diagram-5BDNPKRD-DgUcnfkU.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-B-pBFw8Q.js → diagram-G4DWMVQ6-CRpVvexw.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-6XSfN1eN.js → diagram-MMDJMWI5-C3IhpsM-.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-DJvGxvpE.js → diagram-TYMM5635-BaVbsrwt.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-CpqdpGYj.js → erDiagram-SMLLAGMA-BxRoBUR8.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-C9m7wjwI.js → flowDiagram-DWJPFMVM-Du2lIzoM.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CU0XOfY3.js → ganttDiagram-T4ZO3ILL-DR1pDqLK.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-ipyGULnP.js → gitGraphDiagram-UUTBAWPF-DHAmE20O.js} +1 -1
- package/dist/assets/{graph-BR2KmES9.js → graph-D8Yv0IU1.js} +1 -1
- package/dist/assets/{index-C-be29ey.js → index-AOmFG_I_.js} +1 -1
- package/dist/assets/{index-D5tSeMU7.js → index-B6ZWIxY0.js} +1 -1
- package/dist/assets/{index-D5YruOKD.js → index-Bq95HjYM.js} +1 -1
- package/dist/assets/{index--wgRW_OB.js → index-D0pvxpcl.js} +1 -1
- package/dist/assets/{index-DuZQ6FqR.js → index-D2c77__8.js} +1 -1
- package/dist/assets/{index-C16fiVhv.js → index-DgtUyP57.js} +1 -1
- package/dist/assets/{index-CjsQEQc2.js → index-Dpu92rw_.js} +2 -2
- package/dist/assets/{index-OWzSX2T8.js → index-cCB-lhEO.js} +1 -1
- package/dist/assets/{infoDiagram-42DDH7IO-BQWQjfFX.js → infoDiagram-42DDH7IO-BAqRIm2G.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-Df-KH_Hz.js → ishikawaDiagram-UXIWVN3A-CzH4tezb.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-CMbB_4G9.js → journeyDiagram-VCZTEJTY-Qs4eiyuI.js} +1 -1
- package/dist/assets/{jszip.min-B6k2t9Ch.js → jszip.min-C0OccDsk.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-CImdlKN3.js → kanban-definition-6JOO6SKY-BAjl0gpV.js} +1 -1
- package/dist/assets/{layout-DQl4aw09.js → layout-Cf6cd9xE.js} +1 -1
- package/dist/assets/{linear-CLD3aPlK.js → linear-PzdCkji6.js} +1 -1
- package/dist/assets/mermaid.core-DV9745Lq.js +7 -0
- package/dist/assets/{min-_D86henJ.js → min-CHgWxbzS.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-2JVcu1nk.js → mindmap-definition-QFDTVHPH-CHUjMdpi.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-Dik-m5u7.js → pieDiagram-DEJITSTG-D_r2QC0f.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-Bm5l40Jq.js → quadrantDiagram-34T5L4WZ-DPzGygdN.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-Ck9w5rTj.js → requirementDiagram-MS252O5E-BA2ynnGb.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-BbGVCFEf.js → sankeyDiagram-XADWPNL6-XHVvXuqU.js} +1 -1
- package/dist/assets/{seqResourceLoaders-DFrHmgnK.css → seqResourceLoaders-0RXZfUKp.css} +1 -1
- package/dist/assets/seqResourceLoaders-kBEzcKLh.js +2 -0
- package/dist/assets/{sequenceDiagram-FGHM5R23-vjvEdksk.js → sequenceDiagram-FGHM5R23-C47sA8YH.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-DTqMoiB2.js → stateDiagram-FHFEXIEX-BOmer839.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-CNbAwnhu.js → stateDiagram-v2-QKLJ7IA2-D-kVvTHA.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-Cw8O-52d.js → timeline-definition-GMOUNBTQ-CTAKVZgs.js} +1 -1
- package/dist/assets/{vendor-antd-DI7JL-mE.js → vendor-antd-BuJ6oz45.js} +2 -2
- package/dist/assets/{vendor-codemirror-B9c49dtM.js → vendor-codemirror-D2KAW4ty.js} +1 -1
- package/dist/assets/{vendor-mdxeditor-B6cpBtIE.js → vendor-mdxeditor-BsFVVUcj.js} +2 -2
- package/dist/assets/{vendor-qrcode-Dn90-s-u.js → vendor-qrcode-BpxSq04x.js} +1 -1
- package/dist/assets/{vendor-virtuoso-CpdSKD_6.js → vendor-virtuoso-an4WvAvS.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-DCuqymb7.js → vennDiagram-DHZGUBPP-CuXzoroh.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-BJ20wJUU.js → wardley-RL74JXVD-CFCmxnck.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-Vz6US3qt.js → wardleyDiagram-NUSXRM2D-gfhDB9SR.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-CzkLKrmi.js → xychartDiagram-5P7HB3ND-X-pj66qI.js} +1 -1
- package/dist/index.html +4 -4
- package/package.json +1 -1
- package/server/i18n.js +120 -55
- package/server/interceptor.js +72 -2
- package/server/lib/ccswitch-import.js +251 -0
- package/server/lib/ensure-hooks.js +28 -2
- package/server/lib/log-watcher.js +8 -0
- package/server/lib/pid-alive.js +15 -0
- package/server/lib/proxy-retry.js +716 -0
- package/server/lib/session-start-bridge.js +121 -0
- package/server/lib/settings-merge.js +170 -0
- package/server/lib/v2/live-feed.js +189 -48
- package/server/lib/v2/session-owner.js +135 -0
- package/server/lib/v2/session-select.js +15 -2
- package/server/lib/v2/v2-writer.js +181 -6
- package/server/proxy.js +115 -11
- package/server/pty-manager.js +28 -4
- package/server/routes/events.js +44 -0
- package/server/routes/preferences.js +184 -1
- package/server/server.js +15 -1
- package/src/utils/contentFilter.js +17 -1
- package/dist/assets/App-B224r-8M.js +0 -2
- package/dist/assets/App-dYPa5-df.css +0 -1
- package/dist/assets/classDiagram-6PBFFD2Q-DmSozxoO.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-DmSozxoO.js +0 -1
- package/dist/assets/clone-BQtvy9Ae.js +0 -1
- package/dist/assets/mermaid.core-CN35dvKJ.js +0 -7
- package/dist/assets/seqResourceLoaders-C-T_EMiO.js +0 -2
package/server/routes/events.js
CHANGED
|
@@ -52,6 +52,49 @@ function turnEndNotify(req, res, parsedUrl, isLocal, deps) {
|
|
|
52
52
|
});
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// SessionStart hook notify (session-start-bridge.js): the conversation-switch
|
|
56
|
+
// signal for an in-terminal /resume. Same security shape as turnEndNotify
|
|
57
|
+
// (loopback-only + internal token + 16KB cap); the actual gating on
|
|
58
|
+
// payload.source and the V2Writer re-bind live behind deps.onSessionStartNotify
|
|
59
|
+
// (server.js → interceptor.markSessionStart).
|
|
60
|
+
function sessionStartNotify(req, res, parsedUrl, isLocal, deps) {
|
|
61
|
+
if (!isLocal) {
|
|
62
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
63
|
+
res.end(JSON.stringify({ error: 'Loopback only' }));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (req.headers['x-ccviewer-internal'] !== deps.INTERNAL_TOKEN) {
|
|
67
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
68
|
+
res.end(JSON.stringify({ error: 'Invalid bridge token' }));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
let body = '';
|
|
72
|
+
let truncated = false;
|
|
73
|
+
req.on('data', chunk => {
|
|
74
|
+
body += chunk;
|
|
75
|
+
if (body.length > 16384) { truncated = true; req.destroy(); }
|
|
76
|
+
});
|
|
77
|
+
req.on('end', () => {
|
|
78
|
+
if (truncated) {
|
|
79
|
+
console.warn('[session-start-notify] body exceeded 16KB cap — request destroyed');
|
|
80
|
+
return; // socket already closed by destroy()
|
|
81
|
+
}
|
|
82
|
+
let payload = {};
|
|
83
|
+
let badJson = false;
|
|
84
|
+
try { payload = body ? JSON.parse(body) : {}; }
|
|
85
|
+
catch { badJson = true; console.warn('[session-start-notify] malformed JSON body'); }
|
|
86
|
+
if (badJson) {
|
|
87
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
88
|
+
res.end(JSON.stringify({ error: 'malformed JSON body' }));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
try { deps.onSessionStartNotify(payload); }
|
|
92
|
+
catch (err) { reportSwallowed('session-start-notify.dispatch', err); }
|
|
93
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
94
|
+
res.end(JSON.stringify({ ok: true }));
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
55
98
|
// SSE endpoint
|
|
56
99
|
// 构造「有新版」徽标的 SSE 帧(pending = {version, source})。pending 为空返回 null。
|
|
57
100
|
// 抽成纯函数便于单测帧格式;events() 在新连接上调用它补推,使版本徽标跨刷新持续显示。
|
|
@@ -358,6 +401,7 @@ async function requests(req, res) {
|
|
|
358
401
|
|
|
359
402
|
export const eventsRoutes = [
|
|
360
403
|
{ method: 'POST', match: 'exact', path: '/api/turn-end-notify', handler: turnEndNotify },
|
|
404
|
+
{ method: 'POST', match: 'exact', path: '/api/session-start-notify', handler: sessionStartNotify },
|
|
361
405
|
{ method: 'GET', match: 'exact', path: '/events', handler: events },
|
|
362
406
|
{ method: 'GET', match: 'exact', path: '/api/requests', handler: requests },
|
|
363
407
|
];
|
|
@@ -3,8 +3,11 @@ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
|
3
3
|
import { join, dirname } from 'node:path';
|
|
4
4
|
import { homedir } from 'node:os';
|
|
5
5
|
import { LOG_DIR, setLogDir, getClaudeConfigDir } from '../../findcc.js';
|
|
6
|
-
import { PROFILE_PATH, _defaultConfig, getActiveProfileId, setActiveProfileForWorkspace, _loadProxyProfile } from '../interceptor.js';
|
|
6
|
+
import { PROFILE_PATH, _defaultConfig, getActiveProfileId, setActiveProfileForWorkspace, _loadProxyProfile, RETRY_CONFIG_PATH, _retryConfigState, _loadRetryConfigState } from '../interceptor.js';
|
|
7
7
|
import { migrateProxyProfileList } from '../lib/interceptor-core.js';
|
|
8
|
+
import { DEFAULT_RETRY_CONFIG, validateRetryConfig, resolveRetryConfig } from '../lib/proxy-retry.js';
|
|
9
|
+
import { discoverCcSwitchProviders, mergeImportedProfiles } from '../lib/ccswitch-import.js';
|
|
10
|
+
import { reportSwallowed } from '../lib/error-report.js';
|
|
8
11
|
import { setLang } from '../i18n.js';
|
|
9
12
|
import { reconcileVoicePackPrefs as vpReconcile } from '../lib/voice-pack-manager.js';
|
|
10
13
|
import { readClaudeProjectModel } from '../lib/context-watcher.js';
|
|
@@ -299,6 +302,182 @@ function proxyProfilesPost(req, res, parsedUrl, isLocal, deps) {
|
|
|
299
302
|
});
|
|
300
303
|
}
|
|
301
304
|
|
|
305
|
+
// ── 代理重试配置(GET/POST,仿 proxyProfilesGet/Post,但无密钥脱敏、无 workspace 隔离)──
|
|
306
|
+
// 返回 { config, defaults }:config = 当前生效(env 基础 + retry-config.json 覆盖后的 live binding);
|
|
307
|
+
// defaults = DEFAULT_RETRY_CONFIG,供 UI「恢复默认」按钮参照。
|
|
308
|
+
|
|
309
|
+
function retryConfigGet(req, res, _parsedUrl, _isLocal, _deps) {
|
|
310
|
+
try {
|
|
311
|
+
// _retryConfigState 是 live binding(watchFile 刷新),即当前生效配置。
|
|
312
|
+
// 缺省兜底:若拦截器尚未初始化(理论上不会,但防御性),即时解析一次。
|
|
313
|
+
const config = _retryConfigState || resolveRetryConfig();
|
|
314
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
315
|
+
res.end(JSON.stringify({ config, defaults: DEFAULT_RETRY_CONFIG }));
|
|
316
|
+
} catch {
|
|
317
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
318
|
+
res.end(JSON.stringify({ config: DEFAULT_RETRY_CONFIG, defaults: DEFAULT_RETRY_CONFIG }));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function retryConfigPost(req, res, _parsedUrl, isLocal, deps) {
|
|
323
|
+
// Local-only, mirroring ccswitchImportPost: retry config is written to a
|
|
324
|
+
// cross-process file every ccv instance hot-reloads, and race/stagger with
|
|
325
|
+
// high concurrency multiplies the HOST's paid upstream requests — a LAN
|
|
326
|
+
// client must not be able to amplify the host's API spend machine-wide.
|
|
327
|
+
if (!isLocal) {
|
|
328
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
329
|
+
res.end(JSON.stringify({ ok: false, error: 'retry config is local-only' }));
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
let body = '';
|
|
333
|
+
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
334
|
+
req.on('end', () => {
|
|
335
|
+
try {
|
|
336
|
+
const incoming = JSON.parse(body);
|
|
337
|
+
if (!incoming || typeof incoming !== 'object' || !incoming.config || typeof incoming.config !== 'object') {
|
|
338
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
339
|
+
res.end(JSON.stringify({ error: 'Invalid retry config: expected { config: {...} }' }));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// 校验 + 归一化(与 resolveRetryConfig 共用 validateRetryConfig,单一真源)
|
|
343
|
+
const validated = validateRetryConfig(incoming.config);
|
|
344
|
+
// 合并默认值,保证写盘的是完整配置(缺字段回落默认,避免读到半截配置)
|
|
345
|
+
const toWrite = { ...DEFAULT_RETRY_CONFIG, ...validated };
|
|
346
|
+
const dir = dirname(RETRY_CONFIG_PATH);
|
|
347
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
348
|
+
writeFileSync(RETRY_CONFIG_PATH, JSON.stringify(toWrite, null, 2), { mode: 0o600 });
|
|
349
|
+
// 立即刷新本进程 live binding(不等 watchFile 1.5s 轮询)
|
|
350
|
+
_loadRetryConfigState();
|
|
351
|
+
// SSE 广播给本进程所有客户端(同 proxy_profile 模式)
|
|
352
|
+
sendEventToClients(deps.clients, 'retry_config', { config: _retryConfigState });
|
|
353
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
354
|
+
res.end(JSON.stringify({ ok: true }));
|
|
355
|
+
} catch {
|
|
356
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
357
|
+
res.end(JSON.stringify({ error: 'Invalid JSON' }));
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// ── cc-switch 导入 ─────────────────────────────────────
|
|
363
|
+
// cc-switch 是 Tauri 桌面应用,把供应商凭证存 SQLite(cc-switch.db providers 表)。
|
|
364
|
+
// 本路由跨平台探测 cc-switch 数据目录,只读查 claude 类型供应商,映射成 cc-viewer profile。
|
|
365
|
+
// GET=预览(不落盘);POST=执行导入(merge 进 profile.json + SSE 广播刷新)。
|
|
366
|
+
|
|
367
|
+
async function ccswitchProvidersGet(req, res, _parsedUrl, isLocal, _deps) {
|
|
368
|
+
try {
|
|
369
|
+
const result = await discoverCcSwitchProviders();
|
|
370
|
+
// 脱敏:非本机不下发明文 apiKey(与 proxyProfilesGet 的 maskProfiles 策略一致)
|
|
371
|
+
const profiles = isLocal ? result.profiles : (result.profiles || []).map(p => ({
|
|
372
|
+
...p,
|
|
373
|
+
apiKey: p.apiKey ? `${p.apiKey.slice(0, 4)}****` : '',
|
|
374
|
+
}));
|
|
375
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
376
|
+
res.end(JSON.stringify({
|
|
377
|
+
profiles,
|
|
378
|
+
currentId: result.currentId || null,
|
|
379
|
+
dbPath: result.dbPath,
|
|
380
|
+
error: result.error,
|
|
381
|
+
}));
|
|
382
|
+
} catch (err) {
|
|
383
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
384
|
+
res.end(JSON.stringify({ profiles: [], error: String(err && err.message || err) }));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// POST body 可选 { setActive: true } —— 是否把 cc-switch 的 current 设为 cc-viewer active。
|
|
389
|
+
// 默认不设(避免覆盖用户当前选择)。导入只 merge 列表,凭证刷新幂等。
|
|
390
|
+
async function ccswitchImportPost(req, res, _parsedUrl, isLocal, deps) {
|
|
391
|
+
if (!isLocal) {
|
|
392
|
+
res.writeHead(403, { 'Content-Type': 'application/json' });
|
|
393
|
+
res.end(JSON.stringify({ ok: false, error: 'cc-switch import is local-only' }));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
let body = '';
|
|
397
|
+
req.on('data', chunk => { body += chunk; if (body.length > deps.MAX_POST_BODY) req.destroy(); });
|
|
398
|
+
req.on('end', async () => {
|
|
399
|
+
try {
|
|
400
|
+
const incoming = body ? JSON.parse(body) : {};
|
|
401
|
+
const setActive = incoming && incoming.setActive === true;
|
|
402
|
+
const result = await discoverCcSwitchProviders();
|
|
403
|
+
if (result.error && result.profiles.length === 0) {
|
|
404
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
405
|
+
res.end(JSON.stringify({ ok: false, error: result.error, imported: 0, updated: 0 }));
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
// Read existing profile.json. A missing file is a legitimate first import,
|
|
409
|
+
// but a file that exists and fails to parse must ABORT: merging into an
|
|
410
|
+
// empty base and writing back would silently wipe every user-created
|
|
411
|
+
// proxy_ profile (existing IS the source of truth here, unlike
|
|
412
|
+
// proxyProfilesPost where the client sends the full desired list).
|
|
413
|
+
let existing = { profiles: [] };
|
|
414
|
+
if (existsSync(PROFILE_PATH)) {
|
|
415
|
+
let parsed = null;
|
|
416
|
+
try {
|
|
417
|
+
parsed = JSON.parse(readFileSync(PROFILE_PATH, 'utf-8'));
|
|
418
|
+
} catch (err) {
|
|
419
|
+
reportSwallowed('ccswitch-import.read-existing', err);
|
|
420
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
421
|
+
res.end(JSON.stringify({
|
|
422
|
+
ok: false,
|
|
423
|
+
error: 'existing profile.json is unreadable; import aborted to avoid overwriting your profiles',
|
|
424
|
+
imported: 0, updated: 0,
|
|
425
|
+
}));
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// JSON that parses to a non-object (null, array, number) is just as
|
|
429
|
+
// destructive to merge into — treat it the same as a parse failure.
|
|
430
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
431
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
432
|
+
res.end(JSON.stringify({
|
|
433
|
+
ok: false,
|
|
434
|
+
error: 'existing profile.json has an unexpected shape; import aborted to avoid overwriting your profiles',
|
|
435
|
+
imported: 0, updated: 0,
|
|
436
|
+
}));
|
|
437
|
+
return;
|
|
438
|
+
}
|
|
439
|
+
existing = parsed;
|
|
440
|
+
}
|
|
441
|
+
// merge
|
|
442
|
+
const merged = mergeImportedProfiles(existing.profiles || [], result.profiles);
|
|
443
|
+
const toWrite = { ...existing, profiles: merged.profiles };
|
|
444
|
+
// 落盘
|
|
445
|
+
const dir = dirname(PROFILE_PATH);
|
|
446
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
447
|
+
writeFileSync(PROFILE_PATH, JSON.stringify(toWrite, null, 2), { mode: 0o600 });
|
|
448
|
+
// active 处理:setActive=true 且 cc-switch 有 current → 切换;否则保持现状
|
|
449
|
+
let activeChanged = false;
|
|
450
|
+
if (setActive && result.currentId) {
|
|
451
|
+
setActiveProfileForWorkspace(result.currentId);
|
|
452
|
+
activeChanged = true;
|
|
453
|
+
} else {
|
|
454
|
+
_loadProxyProfile(); // 刷新列表(可能增删了)
|
|
455
|
+
}
|
|
456
|
+
// SSE 广播给前端刷新(profile: 'refresh' 是 truthy 哨兵,
|
|
457
|
+
// 触发 AppBase SSE handler 的 if(data.profile) 重新 GET 全量列表)
|
|
458
|
+
const effectiveActive = getActiveProfileId();
|
|
459
|
+
sendEventToClients(deps.clients, 'proxy_profile', {
|
|
460
|
+
active: effectiveActive,
|
|
461
|
+
profile: 'refresh',
|
|
462
|
+
});
|
|
463
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
464
|
+
res.end(JSON.stringify({
|
|
465
|
+
ok: true,
|
|
466
|
+
imported: merged.imported,
|
|
467
|
+
updated: merged.updated,
|
|
468
|
+
total: merged.profiles.length,
|
|
469
|
+
activeChanged,
|
|
470
|
+
dbPath: result.dbPath,
|
|
471
|
+
}));
|
|
472
|
+
} catch (err) {
|
|
473
|
+
// ok:false keeps the response contract uniform — the client decides
|
|
474
|
+
// success strictly on data.ok, never on the presence of counter fields.
|
|
475
|
+
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
476
|
+
res.end(JSON.stringify({ ok: false, error: String(err && err.message || err) }));
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
|
|
302
481
|
export const preferencesRoutes = [
|
|
303
482
|
{ method: 'GET', match: 'exact', path: '/api/preferences', handler: preferencesGet },
|
|
304
483
|
{ method: 'POST', match: 'exact', path: '/api/preferences', handler: preferencesPost },
|
|
@@ -306,4 +485,8 @@ export const preferencesRoutes = [
|
|
|
306
485
|
{ method: 'POST', match: 'exact', path: '/api/claude-settings', handler: claudeSettingsPost },
|
|
307
486
|
{ method: 'GET', match: 'exact', path: '/api/proxy-profiles', handler: proxyProfilesGet },
|
|
308
487
|
{ method: 'POST', match: 'exact', path: '/api/proxy-profiles', handler: proxyProfilesPost },
|
|
488
|
+
{ method: 'GET', match: 'exact', path: '/api/retry-config', handler: retryConfigGet },
|
|
489
|
+
{ method: 'POST', match: 'exact', path: '/api/retry-config', handler: retryConfigPost },
|
|
490
|
+
{ method: 'GET', match: 'exact', path: '/api/ccswitch-providers', handler: ccswitchProvidersGet },
|
|
491
|
+
{ method: 'POST', match: 'exact', path: '/api/ccswitch-import', handler: ccswitchImportPost },
|
|
309
492
|
];
|
package/server/server.js
CHANGED
|
@@ -76,11 +76,20 @@ function execWithStdin(cmd, args, input, options) {
|
|
|
76
76
|
child.stdin.end();
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
|
-
import { _initPromise, _projectName, _logDir, _v2Writer, streamingState, resetStreamingState, PROFILE_PATH, setLivePort, getImLiveText, resetImLiveText } from './interceptor.js';
|
|
79
|
+
import { _initPromise, _projectName, _logDir, _v2Writer, streamingState, resetStreamingState, PROFILE_PATH, RETRY_CONFIG_PATH, _retryConfigState, setLivePort, getImLiveText, resetImLiveText, markSessionStart } from './interceptor.js';
|
|
80
80
|
import { V2LiveFeed } from './lib/v2/live-feed.js';
|
|
81
81
|
import { sanitizePathComponent } from './lib/v2/layout.js';
|
|
82
82
|
import { maybeResumeConvert } from './lib/v2/convert-manager.js';
|
|
83
83
|
import { LOG_DIR, setLogDir, getClaudeConfigDir, isBrowserOpenSuppressed } from '../findcc.js';
|
|
84
|
+
import { loadRetryConfig } from './lib/proxy-retry.js';
|
|
85
|
+
|
|
86
|
+
// 代理重试配置:由 interceptor._retryConfigState 提供(live binding,watchFile 热刷新)。
|
|
87
|
+
// mode 默认 off(向后兼容,不重试)。env 是启动默认/兜底,retry-config.json 覆盖 env。
|
|
88
|
+
// 启动日志读一次首屏值;运行时 proxy.js 每请求读 interceptor._retryConfigState 取最新。
|
|
89
|
+
const _startupRetryConfig = _retryConfigState || loadRetryConfig();
|
|
90
|
+
if (_startupRetryConfig.mode !== 'off' && process.env.CCV_DEBUG) {
|
|
91
|
+
console.error(`[CC Viewer] Proxy retry: mode=${_startupRetryConfig.mode}, maxRetries=${_startupRetryConfig.maxRetries}, interval=${_startupRetryConfig.retryIntervalMs}ms`);
|
|
92
|
+
}
|
|
84
93
|
import { t, getLang, setLang } from './i18n.js';
|
|
85
94
|
import { loadAuthConfig, loadAuthState, saveAuthConfig, clearProjectOverride, generatePassword, decideAuth, parseCookies, renderLoginPage, localeFromAcceptLanguage } from './lib/auth.js';
|
|
86
95
|
import { checkAndUpdate } from './lib/updater.js';
|
|
@@ -520,6 +529,7 @@ const deps = {
|
|
|
520
529
|
get writeToPty() { return _writeToPty; },
|
|
521
530
|
get onPtyData() { return _onPtyData; },
|
|
522
531
|
get statsWorker() { return statsWorker; },
|
|
532
|
+
get retryConfig() { return _retryConfigState; },
|
|
523
533
|
get workspaceLaunched() { return _workspaceLaunched; },
|
|
524
534
|
setWorkspaceLaunched(v) { _workspaceLaunched = v; },
|
|
525
535
|
get launchCallback() { return _launchCallback; },
|
|
@@ -554,6 +564,9 @@ const deps = {
|
|
|
554
564
|
startLogWatch,
|
|
555
565
|
stopLogWatch,
|
|
556
566
|
scheduleTurnEndBroadcast: _scheduleTurnEndBroadcast,
|
|
567
|
+
// SessionStart hook notify → conversation-switch signal (in-terminal
|
|
568
|
+
// /resume). The source gate + V2Writer re-bind live in the interceptor.
|
|
569
|
+
onSessionStartNotify: markSessionStart,
|
|
557
570
|
ensureImWatch: _ensureImWatch,
|
|
558
571
|
maskProfiles: _maskProfiles,
|
|
559
572
|
maskApiKey: _maskApiKey,
|
|
@@ -2303,6 +2316,7 @@ async function _doStop() {
|
|
|
2303
2316
|
// 早期请求向已关闭的端口 POST 丢包。新 startViewer 的 listen 回调会再次 setLivePort
|
|
2304
2317
|
setLivePort(null);
|
|
2305
2318
|
try { unwatchFile(PROFILE_PATH); } catch {} // 清理 interceptor 的 StatWatcher
|
|
2319
|
+
try { unwatchFile(RETRY_CONFIG_PATH); } catch {} // 清理 retry-config 的 StatWatcher
|
|
2306
2320
|
}
|
|
2307
2321
|
|
|
2308
2322
|
// ─── SDK Mode Exports ──────────────────────────────────────────
|
|
@@ -285,6 +285,17 @@ export function isSyntheticPromptText(text) {
|
|
|
285
285
|
return false;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
// UltraPlan 输入识别:UltraPlanModal / CLI /ultraplan 都会把 ultraplanTemplates.js
|
|
289
|
+
// 组装的 <system-reminder>[SCOPED INSTRUCTION]…</system-reminder> 模板前置到用户 prompt。
|
|
290
|
+
// 该 marker 仅由 ultraplanTemplates.js 产出,故可作为「本轮是 UltraPlan 输入」的可靠信号。
|
|
291
|
+
// stripSystemTags 会剥掉该块,所以必须在原始(未剥离)文本上判断;用负向前瞻确保 marker
|
|
292
|
+
// 出现在同一个 <system-reminder>…</system-reminder> 块内部(不跨过闭合标签),避免用户正文里
|
|
293
|
+
// 在某个无关 reminder 之后只是「提到」该短语时误判。
|
|
294
|
+
export function isUltraplanText(text) {
|
|
295
|
+
if (!text || typeof text !== 'string') return false;
|
|
296
|
+
return /<system-reminder>(?:(?!<\/system-reminder>)[\s\S])*?\[SCOPED INSTRUCTION\]/i.test(text);
|
|
297
|
+
}
|
|
298
|
+
|
|
288
299
|
export function isSystemText(text) {
|
|
289
300
|
if (!text) return true;
|
|
290
301
|
const trimmed = text.trim();
|
|
@@ -469,7 +480,12 @@ export function classifyUserContent(content) {
|
|
|
469
480
|
// 出现在 textBlocks 中;保留 skillBlocks 键以维持返回 shape(ChatView/ImConversationModal 消费)。
|
|
470
481
|
const skillBlocks = [];
|
|
471
482
|
|
|
472
|
-
|
|
483
|
+
// 本轮是否为 UltraPlan 输入(原始文本仍含 <system-reminder>[SCOPED INSTRUCTION] marker,
|
|
484
|
+
// 此时尚未被 stripSystemTags 剥离)。消费方(ChatView)据此在用户气泡上方渲染 [UltraPlan] 标签。
|
|
485
|
+
const ultraplan = Array.isArray(content)
|
|
486
|
+
&& content.some(b => b && b.type === 'text' && isUltraplanText(b.text));
|
|
487
|
+
|
|
488
|
+
return { commands, textBlocks, skillBlocks, teammateBlocks, taskNotificationBlocks, ultraplan };
|
|
473
489
|
}
|
|
474
490
|
|
|
475
491
|
/**
|