cc-viewer 1.6.199 → 1.6.201
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/dist/assets/App-B0Cfe4hA.js +1 -0
- package/dist/assets/{App-fG2r3jFq.css → App-DOYmReD4.css} +1 -1
- package/dist/assets/{AppHeader-pu5K74SB.css → AppHeader-auOOOaQm.css} +2 -2
- package/dist/assets/AppHeader.module-CDgQaFoO.js +2 -0
- package/dist/assets/{Mobile-DnvC6H2O.js → Mobile-CsKFJYE2.js} +1 -1
- package/dist/assets/{_baseUniq-Cnd-_Qe2.js → _baseUniq-C73FOqig.js} +1 -1
- package/dist/assets/{arc-CPVCmYmr.js → arc-DAWkd4Du.js} +1 -1
- package/dist/assets/{architectureDiagram-Q4EWVU46-CTmPWolC.js → architectureDiagram-Q4EWVU46-CXA8GRiC.js} +1 -1
- package/dist/assets/{blockDiagram-DXYQGD6D-BYUa2ljj.js → blockDiagram-DXYQGD6D-BH7SCCtR.js} +1 -1
- package/dist/assets/{c4Diagram-AHTNJAMY-BBfrYhDN.js → c4Diagram-AHTNJAMY-DjidAThD.js} +1 -1
- package/dist/assets/{channel-D1pwh2Yq.js → channel-fMMU54GK.js} +1 -1
- package/dist/assets/{chunk-4BX2VUAB-DYCV-6PI.js → chunk-4BX2VUAB-C7VtouVc.js} +1 -1
- package/dist/assets/{chunk-4TB4RGXK-DZWVouv1.js → chunk-4TB4RGXK-E8ZyU2MN.js} +1 -1
- package/dist/assets/{chunk-55IACEB6-BnetqbRo.js → chunk-55IACEB6-kKpXz88M.js} +1 -1
- package/dist/assets/{chunk-EDXVE4YY-CL4WCxSM.js → chunk-EDXVE4YY-DGxEwZ4m.js} +1 -1
- package/dist/assets/{chunk-FMBD7UC4-DLmQmJbs.js → chunk-FMBD7UC4-CD8bIwZQ.js} +1 -1
- package/dist/assets/{chunk-OYMX7WX6-B7jkMtrd.js → chunk-OYMX7WX6-gYD4Acaz.js} +1 -1
- package/dist/assets/{chunk-QZHKN3VN-tscrjv7L.js → chunk-QZHKN3VN-BjwG7rZm.js} +1 -1
- package/dist/assets/{chunk-YZCP3GAM-BreqZYYH.js → chunk-YZCP3GAM-QsYUvoBQ.js} +1 -1
- package/dist/assets/classDiagram-6PBFFD2Q-BIdWZE2B.js +1 -0
- package/dist/assets/classDiagram-v2-HSJHXN6E-BIdWZE2B.js +1 -0
- package/dist/assets/clone-DUPgDT2x.js +1 -0
- package/dist/assets/{cose-bilkent-S5V4N54A-BqETFkWe.js → cose-bilkent-S5V4N54A-9ctKMOYC.js} +1 -1
- package/dist/assets/{dagre-KV5264BT-C1fQhqsm.js → dagre-KV5264BT-By2HwLaW.js} +1 -1
- package/dist/assets/{diagram-5BDNPKRD-BQ_AArZ4.js → diagram-5BDNPKRD-BpgOHENA.js} +1 -1
- package/dist/assets/{diagram-G4DWMVQ6-ETR1atoi.js → diagram-G4DWMVQ6-B2x2muJb.js} +1 -1
- package/dist/assets/{diagram-MMDJMWI5-BS5YlnF7.js → diagram-MMDJMWI5-un9PMQTJ.js} +1 -1
- package/dist/assets/{diagram-TYMM5635-DgEDTcSl.js → diagram-TYMM5635-CoSUmrbF.js} +1 -1
- package/dist/assets/{erDiagram-SMLLAGMA-BfJzSJyz.js → erDiagram-SMLLAGMA-D9ycUUrR.js} +1 -1
- package/dist/assets/{flowDiagram-DWJPFMVM-CmZyhVxI.js → flowDiagram-DWJPFMVM-CMwSrS0j.js} +1 -1
- package/dist/assets/{ganttDiagram-T4ZO3ILL-CyCgF8EH.js → ganttDiagram-T4ZO3ILL-dNzPWJ2v.js} +1 -1
- package/dist/assets/{gitGraphDiagram-UUTBAWPF-Ch_lyUYI.js → gitGraphDiagram-UUTBAWPF-5jScxIeV.js} +1 -1
- package/dist/assets/{graph-DONA5GRX.js → graph-CSAV79cR.js} +1 -1
- package/dist/assets/{index-CP_dJQHF.js → index-BnE1kmli.js} +2 -2
- package/dist/assets/{infoDiagram-42DDH7IO-sWbLdJMA.js → infoDiagram-42DDH7IO-BBwe5zCb.js} +1 -1
- package/dist/assets/{ishikawaDiagram-UXIWVN3A-B1-Sul49.js → ishikawaDiagram-UXIWVN3A-DxpzhU4G.js} +1 -1
- package/dist/assets/{journeyDiagram-VCZTEJTY-B2GzVR2v.js → journeyDiagram-VCZTEJTY-BiLOryhx.js} +1 -1
- package/dist/assets/{kanban-definition-6JOO6SKY-BQ369gG9.js → kanban-definition-6JOO6SKY-BwMtkCnE.js} +1 -1
- package/dist/assets/{layout-Dg4txuWO.js → layout-DEZivYZo.js} +1 -1
- package/dist/assets/{linear-CB6_fADN.js → linear-8H25OaNu.js} +1 -1
- package/dist/assets/{mermaid.core-fx56L749.js → mermaid.core-O3WUNQR6.js} +2 -2
- package/dist/assets/{min-Cfm_qmEy.js → min-BDNYdllt.js} +1 -1
- package/dist/assets/{mindmap-definition-QFDTVHPH-CucfYr3R.js → mindmap-definition-QFDTVHPH-DLa1a2PT.js} +1 -1
- package/dist/assets/{pieDiagram-DEJITSTG-B1JuDmET.js → pieDiagram-DEJITSTG-C7a0wkId.js} +1 -1
- package/dist/assets/{quadrantDiagram-34T5L4WZ-D45nu4Jo.js → quadrantDiagram-34T5L4WZ-qaO4n-DR.js} +1 -1
- package/dist/assets/{requirementDiagram-MS252O5E-C4MR71jH.js → requirementDiagram-MS252O5E-B5elAix1.js} +1 -1
- package/dist/assets/{sankeyDiagram-XADWPNL6-BjN93eui.js → sankeyDiagram-XADWPNL6-BxAAO2OT.js} +1 -1
- package/dist/assets/{sequenceDiagram-FGHM5R23-DfNydjTE.js → sequenceDiagram-FGHM5R23-D4epWe89.js} +1 -1
- package/dist/assets/{stateDiagram-FHFEXIEX-Cth1bRU5.js → stateDiagram-FHFEXIEX-DgLET5fE.js} +1 -1
- package/dist/assets/{stateDiagram-v2-QKLJ7IA2-DRKS8TSe.js → stateDiagram-v2-QKLJ7IA2-Ci28Wp9J.js} +1 -1
- package/dist/assets/{timeline-definition-GMOUNBTQ-CGI0k2Go.js → timeline-definition-GMOUNBTQ-8JmSnBLT.js} +1 -1
- package/dist/assets/{vennDiagram-DHZGUBPP-C1-dVDob.js → vennDiagram-DHZGUBPP-DYVoEvAU.js} +1 -1
- package/dist/assets/{wardley-RL74JXVD-Cw6grnZ8.js → wardley-RL74JXVD-DS_huD5U.js} +1 -1
- package/dist/assets/{wardleyDiagram-NUSXRM2D-Dz6uGDTV.js → wardleyDiagram-NUSXRM2D-BldFmHzV.js} +1 -1
- package/dist/assets/{xychartDiagram-5P7HB3ND-B_Pl_Y8E.js → xychartDiagram-5P7HB3ND-DS97a-id.js} +1 -1
- package/dist/index.html +1 -1
- package/interceptor.js +141 -10
- package/package.json +1 -1
- package/proxy.js +8 -0
- package/server.js +20 -7
- package/dist/assets/App-P57OfZoY.js +0 -1
- package/dist/assets/AppHeader.module-BaWqwKOb.js +0 -2
- package/dist/assets/classDiagram-6PBFFD2Q-DNn_1CVl.js +0 -1
- package/dist/assets/classDiagram-v2-HSJHXN6E-DNn_1CVl.js +0 -1
- package/dist/assets/clone-6rNpdByw.js +0 -1
package/interceptor.js
CHANGED
|
@@ -6,7 +6,7 @@ const _ccvSkipArgs = ['--version', '-v', '--v', '--help', '-h', 'doctor', 'insta
|
|
|
6
6
|
const _ccvSkip = _ccvSkipArgs.includes(process.argv[2]);
|
|
7
7
|
|
|
8
8
|
import './lib/proxy-env.js';
|
|
9
|
-
import { appendFileSync, mkdirSync, readFileSync, statSync, renameSync, unlinkSync, existsSync, watchFile } from 'node:fs';
|
|
9
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync, statSync, renameSync, unlinkSync, existsSync, watchFile } from 'node:fs';
|
|
10
10
|
import http from 'node:http';
|
|
11
11
|
import { homedir } from 'node:os';
|
|
12
12
|
import { fileURLToPath } from 'node:url';
|
|
@@ -45,26 +45,134 @@ export let _cachedModel = null;
|
|
|
45
45
|
export let _cachedHaikuModel = null;
|
|
46
46
|
|
|
47
47
|
// Proxy profile hot-switch support
|
|
48
|
+
// 数据模型:
|
|
49
|
+
// profile.json (全局共享): 仅存 profiles 列表,watchFile 跨 ccv 进程同步 CRUD。
|
|
50
|
+
// 兼容老数据:若文件里仍有 active 字段,读为"全局回退默认";但本模块不再写它。
|
|
51
|
+
// <projectDir>/active-profile.json (每 workspace 独占): 仅存 { activeId };
|
|
52
|
+
// 切换 active 只影响当前 ccv 进程的 workspace,不污染其他实例。
|
|
48
53
|
const PROFILE_PATH = join(getClaudeConfigDir(), 'cc-viewer', 'profile.json');
|
|
49
54
|
let _activeProfile = null; // { id, name, baseURL?, apiKey?, models?, activeModel? }
|
|
50
55
|
|
|
51
56
|
// 启动时捕获的原始配置(首次 API 请求时记录,不可变)
|
|
52
57
|
let _defaultConfig = null; // { origin, authType, model }
|
|
53
58
|
|
|
59
|
+
function _getActiveProfileFilePath() {
|
|
60
|
+
// _projectName/_logDir 声明在 ~line 218;本函数只会在这些变量初始化后被调用
|
|
61
|
+
// (_loadProxyProfile 的初始调用被挪到 line ~237 之后;watchFile 回调、HTTP handler 也都在之后)
|
|
62
|
+
if (!_projectName || !_logDir) return null;
|
|
63
|
+
return join(_logDir, 'active-profile.json');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function _readWorkspaceActiveId() {
|
|
67
|
+
const p = _getActiveProfileFilePath();
|
|
68
|
+
if (!p) return null;
|
|
69
|
+
try {
|
|
70
|
+
if (existsSync(p)) {
|
|
71
|
+
const data = JSON.parse(readFileSync(p, 'utf-8'));
|
|
72
|
+
return typeof data?.activeId === 'string' ? data.activeId : null;
|
|
73
|
+
}
|
|
74
|
+
} catch { }
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function _writeWorkspaceActiveId(activeId) {
|
|
79
|
+
const p = _getActiveProfileFilePath();
|
|
80
|
+
if (!p) {
|
|
81
|
+
// 诊断用:能把"为什么 workspace 路径不可用"暴露到启动 ccv 的终端
|
|
82
|
+
console.error('[ccv proxy-profile] skip workspace write: ' +
|
|
83
|
+
`_projectName="${_projectName}" _logDir="${_logDir}" (both required)`);
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
88
|
+
const payload = { activeId: (activeId && typeof activeId === 'string') ? activeId : 'max' };
|
|
89
|
+
writeFileSync(p, JSON.stringify(payload, null, 2), { mode: 0o600 });
|
|
90
|
+
return true;
|
|
91
|
+
} catch (err) {
|
|
92
|
+
console.error('[ccv proxy-profile] workspace write failed:', p, err && err.message);
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
54
97
|
function _loadProxyProfile() {
|
|
55
98
|
try {
|
|
56
99
|
const data = JSON.parse(readFileSync(PROFILE_PATH, 'utf-8'));
|
|
57
|
-
|
|
100
|
+
// active 解析优先级:workspace override > profile.json.active (兼容老数据 / 全局回退) > null
|
|
101
|
+
const wsActive = _readWorkspaceActiveId();
|
|
102
|
+
const activeId = wsActive || data.active;
|
|
103
|
+
const active = data.profiles?.find(p => p.id === activeId);
|
|
58
104
|
_activeProfile = (active && active.id !== 'max') ? active : null;
|
|
59
|
-
} catch {
|
|
105
|
+
} catch (err) {
|
|
60
106
|
_activeProfile = null;
|
|
107
|
+
if (process.env.CCV_DEBUG_HOTSWITCH) {
|
|
108
|
+
console.error('[ccv hotswitch] _loadProxyProfile failed:', err && err.message);
|
|
109
|
+
}
|
|
61
110
|
}
|
|
62
111
|
}
|
|
63
112
|
|
|
64
|
-
|
|
65
|
-
|
|
113
|
+
// 为 server.js::POST /api/proxy-profiles 使用,切换当前 workspace 的 active。
|
|
114
|
+
// 同时写两个位置,彼此互为兜底:
|
|
115
|
+
// (1) <logDir>/active-profile.json —— 每 workspace 独占,读取优先级最高
|
|
116
|
+
// (2) profile.json.active —— 全局默认,watchFile 跨实例同步;用作
|
|
117
|
+
// UI 在 workspace 文件读失败 / 不存在时的回落,避免"切换后立刻回切"的幽灵 revert
|
|
118
|
+
// 回落一致性:其他 ccv 实例如果自己 workspace 文件已存在,_loadProxyProfile 会优先用自己
|
|
119
|
+
// 的,不受这里改动影响;只有"从未切过"的实例会跟随最新全局默认(符合直觉)。
|
|
120
|
+
// 返回 { workspace: bool, profile: bool } 指示两条路径的落盘结果。
|
|
121
|
+
function setActiveProfileForWorkspace(activeId) {
|
|
122
|
+
const normalizedId = (activeId && typeof activeId === 'string') ? activeId : 'max';
|
|
123
|
+
const result = { workspace: false, profile: false };
|
|
124
|
+
|
|
125
|
+
// (1) workspace override
|
|
126
|
+
result.workspace = _writeWorkspaceActiveId(normalizedId);
|
|
127
|
+
|
|
128
|
+
// (2) profile.json.active —— 幂等更新,老数据兼容 + UI GET 回落兜底
|
|
129
|
+
try {
|
|
130
|
+
const data = existsSync(PROFILE_PATH)
|
|
131
|
+
? JSON.parse(readFileSync(PROFILE_PATH, 'utf-8'))
|
|
132
|
+
: { profiles: [{ id: 'max', name: 'Default' }] };
|
|
133
|
+
if (data.active !== normalizedId) {
|
|
134
|
+
data.active = normalizedId;
|
|
135
|
+
mkdirSync(dirname(PROFILE_PATH), { recursive: true });
|
|
136
|
+
writeFileSync(PROFILE_PATH, JSON.stringify(data, null, 2), { mode: 0o600 });
|
|
137
|
+
}
|
|
138
|
+
result.profile = true;
|
|
139
|
+
} catch { /* 双失败场景下 result 全 false,由调用方自行兜底 */ }
|
|
66
140
|
|
|
67
|
-
|
|
141
|
+
_loadProxyProfile(); // 立刻刷新本进程 _activeProfile
|
|
142
|
+
return result;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function getActiveProfileId() {
|
|
146
|
+
// UI 需要知道当前 workspace 的 active(优先 workspace 文件,回退 profile.json.active)
|
|
147
|
+
const ws = _readWorkspaceActiveId();
|
|
148
|
+
if (ws) return ws;
|
|
149
|
+
try {
|
|
150
|
+
const data = JSON.parse(readFileSync(PROFILE_PATH, 'utf-8'));
|
|
151
|
+
return data.active || 'max';
|
|
152
|
+
} catch { return 'max'; }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// _loadProxyProfile 的初始调用 + watchFile 挂载挪到 _projectName/_logDir 初始化之后
|
|
156
|
+
// (见 "初始化日志文件路径" 段后的 _kickoffProxyProfileWatcher 调用),避免 TDZ。
|
|
157
|
+
|
|
158
|
+
// 纯函数:把 headers 里任意大小写的 authorization / x-api-key 替换为 profile 的 apiKey;
|
|
159
|
+
// 两者都不存在时强制植入 x-api-key(第三方代理最常见的鉴权形式)。
|
|
160
|
+
// 返回 { headers, matchedAuthKey, matchedXApiKey },诊断日志据此判断是否真正写入。
|
|
161
|
+
function _replaceProxyAuthHeaders(headers, apiKey) {
|
|
162
|
+
const newHeaders = { ...headers };
|
|
163
|
+
let matchedAuthKey = null, matchedXApiKey = null;
|
|
164
|
+
for (const k of Object.keys(newHeaders)) {
|
|
165
|
+
const lk = k.toLowerCase();
|
|
166
|
+
if (lk === 'authorization') matchedAuthKey = k;
|
|
167
|
+
else if (lk === 'x-api-key') matchedXApiKey = k;
|
|
168
|
+
}
|
|
169
|
+
if (matchedAuthKey) newHeaders[matchedAuthKey] = `Bearer ${apiKey}`;
|
|
170
|
+
if (matchedXApiKey) newHeaders[matchedXApiKey] = apiKey;
|
|
171
|
+
if (!matchedAuthKey && !matchedXApiKey) newHeaders['x-api-key'] = apiKey;
|
|
172
|
+
return { headers: newHeaders, matchedAuthKey, matchedXApiKey };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export { _activeProfile, _defaultConfig, _loadProxyProfile, PROFILE_PATH, setActiveProfileForWorkspace, getActiveProfileId };
|
|
68
176
|
|
|
69
177
|
// 生成新的日志文件路径
|
|
70
178
|
function generateNewLogFilePath() {
|
|
@@ -174,6 +282,11 @@ if (process.env.CCV_WORKSPACE_MODE === '1') {
|
|
|
174
282
|
}
|
|
175
283
|
let LOG_FILE = _newLogFile;
|
|
176
284
|
|
|
285
|
+
// 现在 _projectName/_logDir 已初始化,可以安全加载 proxy profile(含 workspace override)
|
|
286
|
+
// 并挂载 watchFile 同步列表变化。
|
|
287
|
+
_loadProxyProfile();
|
|
288
|
+
try { watchFile(PROFILE_PATH, { interval: 1500 }, _loadProxyProfile); } catch { }
|
|
289
|
+
|
|
177
290
|
const _initPromise = (async () => {
|
|
178
291
|
if (!_logDir || !_projectName) return; // 工作区模式下跳过
|
|
179
292
|
if (_isTeammate) return; // Teammate 已在上方同步初始化,跳过 async resume 流程
|
|
@@ -210,6 +323,8 @@ export function initForWorkspace(projectPath, { forceNew = false } = {}) {
|
|
|
210
323
|
_projectName = projectName;
|
|
211
324
|
_logDir = dir;
|
|
212
325
|
LOG_FILE = recentLog;
|
|
326
|
+
// workspace 切换后,重读该 workspace 的 active-profile.json(可能和上一个 workspace 不同)
|
|
327
|
+
_loadProxyProfile();
|
|
213
328
|
return { filePath: recentLog, dir, projectName, resumed: true };
|
|
214
329
|
}
|
|
215
330
|
|
|
@@ -228,6 +343,7 @@ export function initForWorkspace(projectPath, { forceNew = false } = {}) {
|
|
|
228
343
|
_projectName = projectName;
|
|
229
344
|
_logDir = dir;
|
|
230
345
|
LOG_FILE = filePath;
|
|
346
|
+
_loadProxyProfile(); // 同上
|
|
231
347
|
|
|
232
348
|
return { filePath, dir, projectName, resumed: false };
|
|
233
349
|
}
|
|
@@ -237,6 +353,7 @@ export function resetWorkspace() {
|
|
|
237
353
|
_projectName = '';
|
|
238
354
|
_logDir = '';
|
|
239
355
|
LOG_FILE = '';
|
|
356
|
+
_loadProxyProfile(); // workspace 上下文消失,回落到 profile.json.active
|
|
240
357
|
}
|
|
241
358
|
|
|
242
359
|
const MAX_LOG_SIZE = 300 * 1024 * 1024; // 300MB
|
|
@@ -551,13 +668,27 @@ export function setupInterceptor() {
|
|
|
551
668
|
const _finalPath = (!_basePath || _origPath === _basePath || _origPath.startsWith(_basePath + '/')) ? _origPath : _basePath + _origPath;
|
|
552
669
|
_fetchUrl = _baseUrl.origin + _finalPath + _origUrl.search;
|
|
553
670
|
}
|
|
554
|
-
// 2. Auth 替换
|
|
671
|
+
// 2. Auth 替换 —— 兼容 lowercase / TitleCase,且 x-api-key / Authorization 同时替换以覆盖两种鉴权形式
|
|
555
672
|
if (_activeProfile.apiKey && _fetchOpts?.headers) {
|
|
556
673
|
const h = _fetchOpts.headers;
|
|
557
674
|
if (typeof h === 'object' && !(h instanceof Headers)) {
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
675
|
+
const { headers: newHeaders, matchedAuthKey, matchedXApiKey } =
|
|
676
|
+
_replaceProxyAuthHeaders(h, _activeProfile.apiKey);
|
|
677
|
+
_fetchOpts = { ..._fetchOpts, headers: newHeaders };
|
|
678
|
+
|
|
679
|
+
// 诊断日志:让 stderr 能看到替换是否真的发生
|
|
680
|
+
// 只输出"是否命中/是否写入"布尔,绝不输出任何 apiKey 明文或片段
|
|
681
|
+
// (日志聚合/审计规则会把尾 N 字符一并标记为敏感泄漏)
|
|
682
|
+
if (process.env.CCV_DEBUG_HOTSWITCH) {
|
|
683
|
+
console.error('[ccv hotswitch]', {
|
|
684
|
+
profile: _activeProfile.name,
|
|
685
|
+
url: _fetchUrl,
|
|
686
|
+
matchedAuth: matchedAuthKey || '(none)',
|
|
687
|
+
matchedXApiKey: matchedXApiKey || '(none)',
|
|
688
|
+
authSet: !!(matchedAuthKey && newHeaders[matchedAuthKey]),
|
|
689
|
+
xApiKeySet: !!(newHeaders[matchedXApiKey] || newHeaders['x-api-key']),
|
|
690
|
+
});
|
|
691
|
+
}
|
|
561
692
|
}
|
|
562
693
|
}
|
|
563
694
|
// 3. Model 替换
|
package/package.json
CHANGED
package/proxy.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createServer } from 'node:http';
|
|
|
3
3
|
import { readFileSync, existsSync } from 'node:fs';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
5
|
import { homedir } from 'node:os';
|
|
6
|
+
import * as interceptor from './interceptor.js';
|
|
6
7
|
import { setupInterceptor } from './interceptor.js';
|
|
7
8
|
import { extractApiErrorMessage, formatProxyRequestError } from './lib/proxy-errors.js';
|
|
8
9
|
import { getClaudeConfigDir } from './findcc.js';
|
|
@@ -25,6 +26,13 @@ function getBaseUrlFromSettings(settingsPath) {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
function getOriginalBaseUrl() {
|
|
29
|
+
// 热切换 profile 最高优先:UI 里用户选中的 baseURL 直接作为上游目标,
|
|
30
|
+
// 让 log/UI 显示的 URL 与实际去向一致,且避免 settings.json 残留的本地
|
|
31
|
+
// 代理 URL(如 127.0.0.1:xxxx)导致 ccv proxy 自环 404。
|
|
32
|
+
// Via namespace import to pick up watchFile 刷新(ES module live binding)。
|
|
33
|
+
const ap = interceptor._activeProfile;
|
|
34
|
+
if (ap && ap.baseURL) return ap.baseURL;
|
|
35
|
+
|
|
28
36
|
let cwd;
|
|
29
37
|
try { cwd = process.cwd(); } catch { cwd = null; }
|
|
30
38
|
|
package/server.js
CHANGED
|
@@ -34,7 +34,7 @@ function execWithStdin(cmd, args, input, options) {
|
|
|
34
34
|
child.stdin.end();
|
|
35
35
|
});
|
|
36
36
|
}
|
|
37
|
-
import { LOG_FILE, _initPromise, _resumeState, resolveResumeChoice, _projectName, _logDir, _cachedApiKey, _cachedAuthHeader, _cachedHaikuModel, initForWorkspace, resetWorkspace, streamingState, resetStreamingState, _loadProxyProfile, PROFILE_PATH, _defaultConfig, setLivePort } from './interceptor.js';
|
|
37
|
+
import { LOG_FILE, _initPromise, _resumeState, resolveResumeChoice, _projectName, _logDir, _cachedApiKey, _cachedAuthHeader, _cachedHaikuModel, initForWorkspace, resetWorkspace, streamingState, resetStreamingState, _loadProxyProfile, PROFILE_PATH, _defaultConfig, setLivePort, setActiveProfileForWorkspace, getActiveProfileId } from './interceptor.js';
|
|
38
38
|
import { LOG_DIR, setLogDir, getClaudeConfigDir } from './findcc.js';
|
|
39
39
|
import { t, detectLanguage } from './i18n.js';
|
|
40
40
|
import { checkAndUpdate } from './lib/updater.js';
|
|
@@ -1109,10 +1109,14 @@ async function handleRequest(req, res) {
|
|
|
1109
1109
|
}
|
|
1110
1110
|
|
|
1111
1111
|
// Proxy profile 热切换
|
|
1112
|
+
// 数据拆分:profiles 列表 → profile.json(全局共享,fs.watchFile 跨进程同步)
|
|
1113
|
+
// active → <workspace>/active-profile.json(每 workspace 独占,不污染其他 ccv 实例)
|
|
1112
1114
|
if (url === '/api/proxy-profiles' && method === 'GET') {
|
|
1113
1115
|
try {
|
|
1114
1116
|
const data = existsSync(PROFILE_PATH) ? JSON.parse(readFileSync(PROFILE_PATH, 'utf-8')) : _defaultProxyProfiles;
|
|
1115
|
-
|
|
1117
|
+
// 用 interceptor.getActiveProfileId() 返回 effective active(workspace > profile.json.active > 'max')
|
|
1118
|
+
const effectiveActive = getActiveProfileId();
|
|
1119
|
+
const masked = _maskProfiles({ ...data, active: effectiveActive });
|
|
1116
1120
|
if (_defaultConfig) masked.defaultConfig = { ..._defaultConfig, apiKey: _defaultConfig.apiKey ? _maskApiKey(_defaultConfig.apiKey) : null };
|
|
1117
1121
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1118
1122
|
res.end(JSON.stringify(masked));
|
|
@@ -1148,14 +1152,23 @@ async function handleRequest(req, res) {
|
|
|
1148
1152
|
p.apiKey = existingMap[p.id];
|
|
1149
1153
|
}
|
|
1150
1154
|
}
|
|
1155
|
+
// 只写 profiles 列表到 profile.json;active 不再入文件(避免跨进程串台)
|
|
1156
|
+
// 保留老数据里的 active 字段不变,以便老版本 ccv 或手动编辑者的回退能力
|
|
1157
|
+
const toWrite = { ...existing, profiles: incoming.profiles };
|
|
1151
1158
|
const dir = dirname(PROFILE_PATH);
|
|
1152
1159
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
1153
|
-
writeFileSync(PROFILE_PATH, JSON.stringify(
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1160
|
+
writeFileSync(PROFILE_PATH, JSON.stringify(toWrite, null, 2), { mode: 0o600 });
|
|
1161
|
+
// active 走 workspace 级别存储(当前进程独占)
|
|
1162
|
+
if (typeof incoming.active === 'string' && incoming.active) {
|
|
1163
|
+
setActiveProfileForWorkspace(incoming.active);
|
|
1164
|
+
} else {
|
|
1165
|
+
_loadProxyProfile(); // 仅列表变化时也刷新一次以反映删除 / 重命名
|
|
1166
|
+
}
|
|
1167
|
+
// SSE 广播仅给本进程客户端(sendEventToClients 本就是 per-process;另外 active 不跨进程)
|
|
1168
|
+
const effectiveActive = getActiveProfileId();
|
|
1169
|
+
const activeProfile = incoming.profiles?.find(p => p.id === effectiveActive) || null;
|
|
1157
1170
|
const maskedProfile = activeProfile?.apiKey ? { ...activeProfile, apiKey: _maskApiKey(activeProfile.apiKey) } : activeProfile;
|
|
1158
|
-
sendEventToClients(clients, 'proxy_profile', { active:
|
|
1171
|
+
sendEventToClients(clients, 'proxy_profile', { active: effectiveActive, profile: maskedProfile });
|
|
1159
1172
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
1160
1173
|
res.end(JSON.stringify({ ok: true }));
|
|
1161
1174
|
} catch {
|