chatccc 0.2.102 → 0.2.106
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 +3 -3
- package/config.sample.json +1 -2
- package/package.json +63 -63
- package/src/__tests__/claude-adapter.test.ts +104 -574
- package/src/__tests__/config-reload.test.ts +6 -6
- package/src/__tests__/web-ui.test.ts +15 -127
- package/src/adapters/claude-adapter.ts +253 -150
- package/src/adapters/claude-session-meta-store.ts +120 -0
- package/src/cards.ts +0 -31
- package/src/config.ts +5 -17
- package/src/orchestrator.ts +0 -134
- package/src/session.ts +1 -1
- package/src/web-ui.ts +43 -285
package/src/web-ui.ts
CHANGED
|
@@ -37,51 +37,6 @@ interface AppConfig {
|
|
|
37
37
|
codex?: { enabled?: boolean; defaultAgent?: boolean; path?: string; command?: string; model?: string; effort?: string };
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
// ---------------------------------------------------------------------------
|
|
41
|
-
// Claude API 模式("official" / "thirdparty")
|
|
42
|
-
// ---------------------------------------------------------------------------
|
|
43
|
-
// 用户在 Wizard / Dashboard 上选择的"官方 API(Anthropic 直连)"或"第三方 API
|
|
44
|
-
// (自定义网关)"模式:
|
|
45
|
-
// - "official" → 强制把 claude.apiKey / claude.baseUrl 视为空,永远写入 ""
|
|
46
|
-
// (即使前端漏传也按此处理,作为服务端兌底);
|
|
47
|
-
// - "thirdparty" → 保留前端提交的 apiKey / baseUrl 原值,由用户填写。
|
|
48
|
-
//
|
|
49
|
-
// 这两个纯函数被前端 JS 与服务端 handlePostConfig 同时使用,单测在
|
|
50
|
-
// src/__tests__/web-ui.test.ts 里锁定它们的契约。
|
|
51
|
-
|
|
52
|
-
export type ClaudeApiMode = "official" | "thirdparty";
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* 加载已有 config.json 时,决定 UI 应初始展现哪种模式:
|
|
56
|
-
* 只要 apiKey 或 baseUrl 任一非空(trim 后),即视为第三方模式;否则官方。
|
|
57
|
-
*/
|
|
58
|
-
export function detectClaudeApiMode(
|
|
59
|
-
claude: { apiKey?: string; baseUrl?: string } | undefined,
|
|
60
|
-
): ClaudeApiMode {
|
|
61
|
-
const apiKey = (claude?.apiKey ?? "").trim();
|
|
62
|
-
const baseUrl = (claude?.baseUrl ?? "").trim();
|
|
63
|
-
if (apiKey || baseUrl) return "thirdparty";
|
|
64
|
-
return "official";
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 服务端在写入 config.json 前按用户选择的模式归一化扁平 vars:
|
|
69
|
-
* - mode = "thirdparty":保留 vars 中的 CLAUDE_API_KEY / CLAUDE_BASE_URL 原值
|
|
70
|
-
* (包括 ""——用户也可能主动清空一项)。
|
|
71
|
-
* - mode = "official" 或未传 mode:强制把 CLAUDE_API_KEY / CLAUDE_BASE_URL
|
|
72
|
-
* 设为 "",即使前端漏传这两个键也要主动加上 "",覆盖 config.json 里旧值。
|
|
73
|
-
*
|
|
74
|
-
* 这里"未传 mode 默认按 official 处理"是有意为之的兌底:旧版前端可能不传
|
|
75
|
-
* mode 字段,按更安全的方向(不保留可能误填的密钥)落地。
|
|
76
|
-
*/
|
|
77
|
-
export function applyClaudeApiMode(
|
|
78
|
-
vars: Record<string, unknown>,
|
|
79
|
-
mode: string | undefined,
|
|
80
|
-
): Record<string, unknown> {
|
|
81
|
-
if (mode === "thirdparty") return vars;
|
|
82
|
-
return { ...vars, CLAUDE_API_KEY: "", CLAUDE_BASE_URL: "" };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
40
|
// ---------------------------------------------------------------------------
|
|
86
41
|
// /api/start 路径选择(纯函数,便于单测护栏)
|
|
87
42
|
// ---------------------------------------------------------------------------
|
|
@@ -293,24 +248,19 @@ async function handleGetConfig(_req: IncomingMessage, res: ServerResponse): Prom
|
|
|
293
248
|
async function handlePostConfig(req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
294
249
|
const body = await readRequestBody(req);
|
|
295
250
|
let updates: Record<string, unknown>;
|
|
296
|
-
let claudeApiMode: string | undefined;
|
|
297
251
|
try {
|
|
298
252
|
const parsed = JSON.parse(body);
|
|
299
253
|
updates = parsed.vars ?? {};
|
|
300
|
-
// 前端可选传 claudeApiMode:"official" / "thirdparty";未传时 applyClaudeApiMode
|
|
301
|
-
// 会按 official 兌底清空 apiKey/baseUrl,避免把误填的密钥落到 config.json。
|
|
302
|
-
claudeApiMode = typeof parsed.claudeApiMode === "string" ? parsed.claudeApiMode : undefined;
|
|
303
254
|
} catch {
|
|
304
255
|
jsonReply(res, 400, { ok: false, error: "Invalid JSON" });
|
|
305
256
|
return;
|
|
306
257
|
}
|
|
307
|
-
const normalized = applyClaudeApiMode(updates, claudeApiMode);
|
|
308
258
|
const existing = loadConfig();
|
|
309
259
|
// AppConfig 是闭合接口(无 index signature),但运行时本质就是 JSON 对象,
|
|
310
260
|
// 这里通过 unknown 桥接到 Record 让 deepMerge 能复用;写回前断言回 AppConfig。
|
|
311
261
|
const merged = deepMerge(
|
|
312
262
|
existing as unknown as Record<string, unknown>,
|
|
313
|
-
unflattenConfig(
|
|
263
|
+
unflattenConfig(updates),
|
|
314
264
|
) as AppConfig;
|
|
315
265
|
try {
|
|
316
266
|
saveConfig(merged);
|
|
@@ -354,12 +304,6 @@ export function unflattenConfig(flat: Record<string, unknown>): Record<string, u
|
|
|
354
304
|
result.port = parseInt(val as string, 10) || 18080;
|
|
355
305
|
} else if (key === "CHATCCC_GIT_TIMEOUT_SECONDS") {
|
|
356
306
|
result.gitTimeoutSeconds = parseInt(val as string, 10) || 180;
|
|
357
|
-
} else if (key === "CLAUDE_API_KEY") {
|
|
358
|
-
result.claude = result.claude || {};
|
|
359
|
-
(result.claude as Record<string, unknown>).apiKey = val;
|
|
360
|
-
} else if (key === "CLAUDE_BASE_URL") {
|
|
361
|
-
result.claude = result.claude || {};
|
|
362
|
-
(result.claude as Record<string, unknown>).baseUrl = val;
|
|
363
307
|
} else if (key === "CHATCCC_ANTHROPIC_MODEL") {
|
|
364
308
|
result.claude = result.claude || {};
|
|
365
309
|
(result.claude as Record<string, unknown>).model = val;
|
|
@@ -369,6 +313,12 @@ export function unflattenConfig(flat: Record<string, unknown>): Record<string, u
|
|
|
369
313
|
} else if (key === "CHATCCC_ANTHROPIC_EFFORT") {
|
|
370
314
|
result.claude = result.claude || {};
|
|
371
315
|
(result.claude as Record<string, unknown>).effort = val;
|
|
316
|
+
} else if (key === "CHATCCC_ANTHROPIC_API_KEY") {
|
|
317
|
+
result.claude = result.claude || {};
|
|
318
|
+
(result.claude as Record<string, unknown>).apiKey = val;
|
|
319
|
+
} else if (key === "CHATCCC_ANTHROPIC_BASE_URL") {
|
|
320
|
+
result.claude = result.claude || {};
|
|
321
|
+
(result.claude as Record<string, unknown>).baseUrl = val;
|
|
372
322
|
} else if (key === "CHATCCC_CLAUDE_ENABLED") {
|
|
373
323
|
result.claude = result.claude || {};
|
|
374
324
|
(result.claude as Record<string, unknown>).enabled = val === true || val === "true";
|
|
@@ -671,7 +621,7 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
671
621
|
<input type="checkbox" class="agent-toggle" id="agent-enable-claude" onchange="onAgentToggle('claude', this.checked)">
|
|
672
622
|
<div class="meta">
|
|
673
623
|
<div class="name">Claude Code</div>
|
|
674
|
-
<div class="desc">Anthropic Claude
|
|
624
|
+
<div class="desc">Anthropic Claude Code CLI<br>模型、effort 均为选填</div>
|
|
675
625
|
</div>
|
|
676
626
|
</div>
|
|
677
627
|
<label class="agent-default-row">
|
|
@@ -679,45 +629,25 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
679
629
|
设为默认 Agent
|
|
680
630
|
</label>
|
|
681
631
|
<fieldset class="agent-body" id="agent-body-claude" disabled>
|
|
682
|
-
<div class="form-group"
|
|
683
|
-
<label
|
|
684
|
-
<
|
|
685
|
-
<label style="display:flex;align-items:center;gap:6px;font-weight:500;cursor:pointer">
|
|
686
|
-
<input type="radio" name="claude-api-mode" value="official" id="claude-api-mode-official" checked onchange="onClaudeApiModeChange('official')">
|
|
687
|
-
官方 API(Anthropic 直连)
|
|
688
|
-
</label>
|
|
689
|
-
<label style="display:flex;align-items:center;gap:6px;font-weight:500;cursor:pointer">
|
|
690
|
-
<input type="radio" name="claude-api-mode" value="thirdparty" id="claude-api-mode-thirdparty" onchange="onClaudeApiModeChange('thirdparty')">
|
|
691
|
-
第三方 API(自定义网关)
|
|
692
|
-
</label>
|
|
693
|
-
</div>
|
|
694
|
-
<div class="hint" style="margin-top:6px">官方模式不需 API Key / Base URL;第三方模式需填写。<strong>切回官方后保存会清空已填的密钥。</strong></div>
|
|
632
|
+
<div class="form-group">
|
|
633
|
+
<label>模型(选填)</label>
|
|
634
|
+
<input type="text" id="field-CHATCCC_ANTHROPIC_MODEL" placeholder="留空使用默认模型">
|
|
695
635
|
</div>
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
<
|
|
699
|
-
<label>API Key</label>
|
|
700
|
-
<input type="password" id="field-CLAUDE_API_KEY" placeholder="sk-...">
|
|
701
|
-
<div class="hint">第三方网关 API 密钥,对应 <code>claude.apiKey</code></div>
|
|
702
|
-
</div>
|
|
703
|
-
<div class="form-group">
|
|
704
|
-
<label>Base URL</label>
|
|
705
|
-
<input type="text" id="field-CLAUDE_BASE_URL" placeholder="https://api.deepseek.com/anthropic">
|
|
706
|
-
<div class="hint">Anthropic 兼容端点,对应 <code>claude.baseUrl</code></div>
|
|
707
|
-
</div>
|
|
636
|
+
<div class="form-group">
|
|
637
|
+
<label>Subagent 模型(选填)</label>
|
|
638
|
+
<input type="text" id="field-CHATCCC_ANTHROPIC_SUBAGENT_MODEL" placeholder="留空使用默认 subagent 模型">
|
|
708
639
|
</div>
|
|
709
|
-
|
|
710
640
|
<div class="form-group">
|
|
711
|
-
<label
|
|
712
|
-
<input type="text" id="field-
|
|
641
|
+
<label>思考深度 Effort(选填)</label>
|
|
642
|
+
<input type="text" id="field-CHATCCC_ANTHROPIC_EFFORT" placeholder="留空使用默认值">
|
|
713
643
|
</div>
|
|
714
|
-
<div class="form-group"
|
|
715
|
-
<label>
|
|
716
|
-
<input type="
|
|
644
|
+
<div class="form-group">
|
|
645
|
+
<label>API Key(选填)</label>
|
|
646
|
+
<input type="password" id="field-CHATCCC_ANTHROPIC_API_KEY" placeholder="留空使用 Claude CLI 默认认证">
|
|
717
647
|
</div>
|
|
718
648
|
<div class="form-group">
|
|
719
|
-
<label
|
|
720
|
-
<input type="text" id="field-
|
|
649
|
+
<label>Base URL(选填)</label>
|
|
650
|
+
<input type="text" id="field-CHATCCC_ANTHROPIC_BASE_URL" placeholder="留空使用默认端点">
|
|
721
651
|
</div>
|
|
722
652
|
</fieldset>
|
|
723
653
|
</div>
|
|
@@ -850,12 +780,11 @@ header .badge{font-size:13px;padding:4px 12px;border-radius:12px;font-weight:500
|
|
|
850
780
|
<details class="card config-section" id="dash-claude">
|
|
851
781
|
<summary>Claude Agent</summary>
|
|
852
782
|
<div class="section-detail">
|
|
853
|
-
<div class="config-row"><span class="key">API 来源</span><span class="val" id="cfg-CLAUDE_API_MODE">-</span></div>
|
|
854
|
-
<div class="config-row" id="cfg-row-CLAUDE_API_KEY"><span class="key">API Key</span><span class="val" id="cfg-CLAUDE_API_KEY">-</span></div>
|
|
855
|
-
<div class="config-row" id="cfg-row-CLAUDE_BASE_URL"><span class="key">Base URL</span><span class="val" id="cfg-CLAUDE_BASE_URL">-</span></div>
|
|
856
783
|
<div class="config-row"><span class="key">模型</span><span class="val" id="cfg-ANTHROPIC_MODEL">-</span></div>
|
|
857
|
-
<div class="config-row"
|
|
784
|
+
<div class="config-row"><span class="key">Subagent 模型</span><span class="val" id="cfg-ANTHROPIC_SUBAGENT_MODEL">-</span></div>
|
|
858
785
|
<div class="config-row"><span class="key">Effort</span><span class="val" id="cfg-ANTHROPIC_EFFORT">-</span></div>
|
|
786
|
+
<div class="config-row"><span class="key">API Key</span><span class="val" id="cfg-ANTHROPIC_API_KEY">-</span></div>
|
|
787
|
+
<div class="config-row"><span class="key">Base URL</span><span class="val" id="cfg-ANTHROPIC_BASE_URL">-</span></div>
|
|
859
788
|
<label class="agent-default-row" style="margin-top:10px"><input type="checkbox" id="dash-default-claude" onchange="setDashboardDefaultAgent('claude', this.checked)"> 设为默认 Agent</label>
|
|
860
789
|
<button class="btn btn-outline" style="margin-top:8px" onclick="editSection('claude')">编辑</button>
|
|
861
790
|
</div>
|
|
@@ -922,67 +851,12 @@ let state = {
|
|
|
922
851
|
var step2InputBound = false;
|
|
923
852
|
|
|
924
853
|
const AGENT_FIELDS = {
|
|
925
|
-
claude: ['
|
|
854
|
+
claude: ['CHATCCC_ANTHROPIC_MODEL','CHATCCC_ANTHROPIC_SUBAGENT_MODEL','CHATCCC_ANTHROPIC_EFFORT','CHATCCC_ANTHROPIC_API_KEY','CHATCCC_ANTHROPIC_BASE_URL'],
|
|
926
855
|
cursor: ['CHATCCC_CURSOR_PATH','CHATCCC_CURSOR_MODEL'],
|
|
927
856
|
codex: ['CHATCCC_CODEX_PATH','CHATCCC_CODEX_MODEL','CHATCCC_CODEX_EFFORT']
|
|
928
857
|
};
|
|
929
858
|
const FEISHU_FIELDS = ['CHATCCC_APP_ID','CHATCCC_APP_SECRET'];
|
|
930
859
|
|
|
931
|
-
// 当前选中的 Claude API 模式("official" / "thirdparty")
|
|
932
|
-
// Wizard / Dashboard 都通过这个变量驱动 UI 显隐和提交时的 mode 字段
|
|
933
|
-
var claudeApiMode = 'official';
|
|
934
|
-
|
|
935
|
-
// model / effort 输入框 placeholder:仅第三方模式下举例典型取值(网关五花八门,
|
|
936
|
-
// 示例只是引导);官方模式不列举任何具体值,避免给出过期/不支持的取值产生误导。
|
|
937
|
-
var CLAUDE_MODEL_PLACEHOLDER = {
|
|
938
|
-
official: '留空表示不向 SDK 传 model',
|
|
939
|
-
thirdparty: 'claude-sonnet-4-6'
|
|
940
|
-
};
|
|
941
|
-
var CLAUDE_SUBAGENT_MODEL_PLACEHOLDER = {
|
|
942
|
-
official: '留空表示不向 SDK 传 CLAUDE_CODE_SUBAGENT_MODEL',
|
|
943
|
-
thirdparty: 'claude-haiku-4-5-20251001'
|
|
944
|
-
};
|
|
945
|
-
var CLAUDE_EFFORT_PLACEHOLDER = {
|
|
946
|
-
official: '留空表示不向 SDK 传 effort',
|
|
947
|
-
thirdparty: 'low / medium / high / max(留空不向 SDK 传 effort)'
|
|
948
|
-
};
|
|
949
|
-
|
|
950
|
-
function onClaudeApiModeChange(mode) {
|
|
951
|
-
claudeApiMode = mode;
|
|
952
|
-
var thirdPartyEl = document.getElementById('claude-thirdparty-fields');
|
|
953
|
-
if (thirdPartyEl) {
|
|
954
|
-
thirdPartyEl.classList.toggle('hidden', mode !== 'thirdparty');
|
|
955
|
-
}
|
|
956
|
-
// Edit Modal 内的同名容器(如果当前打开的是 Claude 编辑模态框)
|
|
957
|
-
var editThirdPartyEl = document.getElementById('edit-claude-thirdparty-fields');
|
|
958
|
-
if (editThirdPartyEl) {
|
|
959
|
-
editThirdPartyEl.classList.toggle('hidden', mode !== 'thirdparty');
|
|
960
|
-
}
|
|
961
|
-
var wizardSubagentField = document.getElementById('claude-subagent-model-field');
|
|
962
|
-
if (wizardSubagentField) wizardSubagentField.classList.toggle('hidden', mode !== 'thirdparty');
|
|
963
|
-
document.querySelectorAll('[data-claude-subagent-field]').forEach(function(el){
|
|
964
|
-
el.classList.toggle('hidden', mode !== 'thirdparty');
|
|
965
|
-
});
|
|
966
|
-
// 同步 model / effort 输入框的 placeholder(Wizard + Edit Modal 两处都更新)
|
|
967
|
-
var modelPlaceholder = CLAUDE_MODEL_PLACEHOLDER[mode] || CLAUDE_MODEL_PLACEHOLDER.official;
|
|
968
|
-
var subagentModelPlaceholder = CLAUDE_SUBAGENT_MODEL_PLACEHOLDER[mode] || CLAUDE_SUBAGENT_MODEL_PLACEHOLDER.official;
|
|
969
|
-
var effortPlaceholder = CLAUDE_EFFORT_PLACEHOLDER[mode] || CLAUDE_EFFORT_PLACEHOLDER.official;
|
|
970
|
-
var wizardModelEl = document.getElementById('field-CHATCCC_ANTHROPIC_MODEL');
|
|
971
|
-
if (wizardModelEl) wizardModelEl.placeholder = modelPlaceholder;
|
|
972
|
-
var editModelEl = document.getElementById('edit-CHATCCC_ANTHROPIC_MODEL');
|
|
973
|
-
if (editModelEl) editModelEl.placeholder = modelPlaceholder;
|
|
974
|
-
var wizardSubagentModelEl = document.getElementById('field-CHATCCC_ANTHROPIC_SUBAGENT_MODEL');
|
|
975
|
-
if (wizardSubagentModelEl) wizardSubagentModelEl.placeholder = subagentModelPlaceholder;
|
|
976
|
-
var editSubagentModelEl = document.getElementById('edit-CHATCCC_ANTHROPIC_SUBAGENT_MODEL');
|
|
977
|
-
if (editSubagentModelEl) editSubagentModelEl.placeholder = subagentModelPlaceholder;
|
|
978
|
-
var wizardEffortEl = document.getElementById('field-CHATCCC_ANTHROPIC_EFFORT');
|
|
979
|
-
if (wizardEffortEl) wizardEffortEl.placeholder = effortPlaceholder;
|
|
980
|
-
var editEffortEl = document.getElementById('edit-CHATCCC_ANTHROPIC_EFFORT');
|
|
981
|
-
if (editEffortEl) editEffortEl.placeholder = effortPlaceholder;
|
|
982
|
-
// Claude 切换 API 模式可能改变"是否填对"的判定(第三方需要 apiKey+baseUrl)
|
|
983
|
-
updateStep2NextBtn();
|
|
984
|
-
}
|
|
985
|
-
|
|
986
860
|
function onWizardPlatformToggle(platform, enabled) {
|
|
987
861
|
state.platformsEnabled[platform] = enabled;
|
|
988
862
|
// 飞书凭证字段跟随开关显示/隐藏
|
|
@@ -995,7 +869,7 @@ async function onPlatformToggle(platform, enabled) {
|
|
|
995
869
|
var vars = {};
|
|
996
870
|
if (platform === 'feishu') vars.CHATCCC_FEISHU_ENABLED = enabled;
|
|
997
871
|
else vars.CHATCCC_ILINK_ENABLED = enabled;
|
|
998
|
-
var result = await api('/api/config', 'POST', { vars: vars
|
|
872
|
+
var result = await api('/api/config', 'POST', { vars: vars });
|
|
999
873
|
if (result.ok) {
|
|
1000
874
|
state.config.platforms = state.config.platforms || {};
|
|
1001
875
|
state.config.platforms[platform] = state.config.platforms[platform] || {};
|
|
@@ -1075,24 +949,15 @@ function onDefaultAgentToggle(agent, enabled) {
|
|
|
1075
949
|
updateStep2NextBtn();
|
|
1076
950
|
}
|
|
1077
951
|
|
|
1078
|
-
/** Claude 启用时"填对"的判定:第三方模式需 apiKey + baseUrl 都非空,其他情况一律算填对 */
|
|
1079
|
-
function isClaudeFieldsValid() {
|
|
1080
|
-
if (claudeApiMode !== 'thirdparty') return true;
|
|
1081
|
-
var apiKey = (document.getElementById('field-CLAUDE_API_KEY') || {}).value || '';
|
|
1082
|
-
var baseUrl = (document.getElementById('field-CLAUDE_BASE_URL') || {}).value || '';
|
|
1083
|
-
return Boolean(apiKey.trim() && baseUrl.trim());
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
952
|
/**
|
|
1087
|
-
* "下一步"按钮启用条件:至少一个 Agent
|
|
1088
|
-
* - claude:第三方模式必须 apiKey + baseUrl 都填;官方模式视为已填对
|
|
953
|
+
* "下一步"按钮启用条件:至少一个 Agent 开关打开
|
|
1089
954
|
* - cursor / codex:开关打开即视为填对(path 留空时运行时会自动探测/退回 PATH)
|
|
1090
955
|
*/
|
|
1091
956
|
function updateStep2NextBtn() {
|
|
1092
957
|
var btn = document.getElementById('btn-step2-next');
|
|
1093
958
|
if (!btn) return;
|
|
1094
959
|
var validCount = 0;
|
|
1095
|
-
if (state.agentsEnabled.claude
|
|
960
|
+
if (state.agentsEnabled.claude) validCount++;
|
|
1096
961
|
if (state.agentsEnabled.cursor) validCount++;
|
|
1097
962
|
if (state.agentsEnabled.codex) validCount++;
|
|
1098
963
|
if (validCount > 0 && !state.defaultAgent) {
|
|
@@ -1102,14 +967,6 @@ function updateStep2NextBtn() {
|
|
|
1102
967
|
btn.disabled = validCount === 0;
|
|
1103
968
|
}
|
|
1104
969
|
|
|
1105
|
-
/** 加载已有 config 时按 detectClaudeApiMode 契约判定初始模式 */
|
|
1106
|
-
function detectClaudeApiModeFromConfig(claude) {
|
|
1107
|
-
if (!claude) return 'official';
|
|
1108
|
-
var apiKey = (claude.apiKey || '').toString().trim();
|
|
1109
|
-
var baseUrl = (claude.baseUrl || '').toString().trim();
|
|
1110
|
-
return (apiKey || baseUrl) ? 'thirdparty' : 'official';
|
|
1111
|
-
}
|
|
1112
|
-
|
|
1113
970
|
// ---- API helpers ----
|
|
1114
971
|
async function api(path, method, body) {
|
|
1115
972
|
const opts = { method, headers: {} };
|
|
@@ -1230,24 +1087,17 @@ function isAgentEnabled(node, keys) {
|
|
|
1230
1087
|
return false;
|
|
1231
1088
|
}
|
|
1232
1089
|
|
|
1233
|
-
var CLAUDE_FALLBACK_KEYS = ['
|
|
1090
|
+
var CLAUDE_FALLBACK_KEYS = ['model','subagentModel','effort'];
|
|
1234
1091
|
var CURSOR_FALLBACK_KEYS = ['path','command','model'];
|
|
1235
1092
|
var CODEX_FALLBACK_KEYS = ['path','command','model','effort'];
|
|
1236
1093
|
|
|
1237
1094
|
function renderStep2() {
|
|
1238
1095
|
var c = state.config || {};
|
|
1239
1096
|
if (c.claude) {
|
|
1240
|
-
prefillNested('field-CLAUDE_API_KEY', c.claude.apiKey);
|
|
1241
|
-
prefillNested('field-CLAUDE_BASE_URL', c.claude.baseUrl);
|
|
1242
1097
|
prefillNested('field-CHATCCC_ANTHROPIC_MODEL', c.claude.model);
|
|
1243
1098
|
prefillNested('field-CHATCCC_ANTHROPIC_SUBAGENT_MODEL', c.claude.subagentModel);
|
|
1244
1099
|
prefillNested('field-CHATCCC_ANTHROPIC_EFFORT', c.claude.effort);
|
|
1245
1100
|
}
|
|
1246
|
-
// 按已有 apiKey/baseUrl 判定初始 API 模式,并相应显示/隐藏字段
|
|
1247
|
-
var initialMode = detectClaudeApiModeFromConfig(c.claude);
|
|
1248
|
-
var radio = document.getElementById('claude-api-mode-' + initialMode);
|
|
1249
|
-
if (radio) radio.checked = true;
|
|
1250
|
-
onClaudeApiModeChange(initialMode);
|
|
1251
1101
|
if (c.cursor) {
|
|
1252
1102
|
prefillNested('field-CHATCCC_CURSOR_PATH', c.cursor.path || c.cursor.command);
|
|
1253
1103
|
prefillNested('field-CHATCCC_CURSOR_MODEL', c.cursor.model);
|
|
@@ -1280,7 +1130,7 @@ function renderStep2() {
|
|
|
1280
1130
|
if (hint) hint.textContent = '已自动探测到';
|
|
1281
1131
|
}
|
|
1282
1132
|
|
|
1283
|
-
// 字段输入时实时刷新"下一步"
|
|
1133
|
+
// 字段输入时实时刷新"下一步"按钮
|
|
1284
1134
|
if (!step2InputBound) {
|
|
1285
1135
|
document.getElementById('step-2').addEventListener('input', updateStep2NextBtn);
|
|
1286
1136
|
step2InputBound = true;
|
|
@@ -1295,7 +1145,6 @@ function renderStep2() {
|
|
|
1295
1145
|
* - 三个 Agent 的 enabled 状态都显式下发,让 config.json 持久化用户的最新开关偏好
|
|
1296
1146
|
* - Agent 字段仅在该 Agent 开关启用时收集;未启用的 Agent 不下发其它字段,
|
|
1297
1147
|
* 服务端 deepMerge 会保留 config.json 中已有值(避免关闭开关时误清空旧配置)
|
|
1298
|
-
* - Claude 启用且当前 mode=official 时显式置空 apiKey/baseUrl,覆盖 config.json 旧值
|
|
1299
1148
|
*/
|
|
1300
1149
|
function collectAllFields() {
|
|
1301
1150
|
var vars = {};
|
|
@@ -1316,14 +1165,9 @@ function collectAllFields() {
|
|
|
1316
1165
|
vars.CHATCCC_CODEX_DEFAULT_AGENT = state.defaultAgent === 'codex';
|
|
1317
1166
|
if (state.agentsEnabled.claude) {
|
|
1318
1167
|
AGENT_FIELDS.claude.forEach(function(key){
|
|
1319
|
-
if (claudeApiMode !== 'thirdparty' && key === 'CHATCCC_ANTHROPIC_SUBAGENT_MODEL') return;
|
|
1320
1168
|
var el = document.getElementById('field-' + key);
|
|
1321
1169
|
if (el && el.value.trim()) vars[key] = el.value.trim();
|
|
1322
1170
|
});
|
|
1323
|
-
if (claudeApiMode !== 'thirdparty') {
|
|
1324
|
-
vars.CLAUDE_API_KEY = '';
|
|
1325
|
-
vars.CLAUDE_BASE_URL = '';
|
|
1326
|
-
}
|
|
1327
1171
|
}
|
|
1328
1172
|
if (state.agentsEnabled.cursor) {
|
|
1329
1173
|
AGENT_FIELDS.cursor.forEach(function(key){
|
|
@@ -1372,15 +1216,11 @@ function renderStep3() {
|
|
|
1372
1216
|
enabledList.forEach(function(t){
|
|
1373
1217
|
if (t === 'claude') {
|
|
1374
1218
|
lines.push('<h4 style="margin:10px 0 4px;color:#334155">Claude Code</h4>');
|
|
1375
|
-
var modeLabel = claudeApiMode === 'thirdparty' ? '第三方 API(自定义网关)' : '官方 API(Anthropic 直连)';
|
|
1376
|
-
lines.push('<div class="config-row"><span class="key">API 来源</span><span class="val">' + modeLabel + '</span></div>');
|
|
1377
|
-
if (claudeApiMode === 'thirdparty') {
|
|
1378
|
-
lines.push('<div class="config-row"><span class="key">API Key</span><span class="val">' + (vars.CLAUDE_API_KEY ? '***已设置***' : '(未设置)') + '</span></div>');
|
|
1379
|
-
if (vars.CLAUDE_BASE_URL) lines.push('<div class="config-row"><span class="key">Base URL</span><span class="val">' + vars.CLAUDE_BASE_URL + '</span></div>');
|
|
1380
|
-
lines.push('<div class="config-row"><span class="key">Subagent 模型</span><span class="val">' + (vars.CHATCCC_ANTHROPIC_SUBAGENT_MODEL || '(留空)') + '</span></div>');
|
|
1381
|
-
}
|
|
1382
1219
|
lines.push('<div class="config-row"><span class="key">模型</span><span class="val">' + (vars.CHATCCC_ANTHROPIC_MODEL || '(留空)') + '</span></div>');
|
|
1220
|
+
lines.push('<div class="config-row"><span class="key">Subagent 模型</span><span class="val">' + (vars.CHATCCC_ANTHROPIC_SUBAGENT_MODEL || '(留空)') + '</span></div>');
|
|
1383
1221
|
lines.push('<div class="config-row"><span class="key">Effort</span><span class="val">' + (vars.CHATCCC_ANTHROPIC_EFFORT || '(留空)') + '</span></div>');
|
|
1222
|
+
lines.push('<div class="config-row"><span class="key">API Key</span><span class="val">' + (vars.CHATCCC_ANTHROPIC_API_KEY ? '***已设置***' : '(留空)') + '</span></div>');
|
|
1223
|
+
lines.push('<div class="config-row"><span class="key">Base URL</span><span class="val">' + (vars.CHATCCC_ANTHROPIC_BASE_URL || '(留空)') + '</span></div>');
|
|
1384
1224
|
} else if (t === 'cursor') {
|
|
1385
1225
|
lines.push('<h4 style="margin:10px 0 4px;color:#334155">Cursor</h4>');
|
|
1386
1226
|
if (vars.CHATCCC_CURSOR_PATH) lines.push('<div class="config-row"><span class="key">CLI 路径</span><span class="val">' + vars.CHATCCC_CURSOR_PATH + '</span></div>');
|
|
@@ -1396,17 +1236,7 @@ function renderStep3() {
|
|
|
1396
1236
|
}
|
|
1397
1237
|
|
|
1398
1238
|
async function saveConfig(vars) {
|
|
1399
|
-
|
|
1400
|
-
// mode 强制清空 apiKey/baseUrl(即使前端 vars 没传也会兌底,详见 web-ui.test.ts)。
|
|
1401
|
-
//
|
|
1402
|
-
// 当 wizard 中 claude 开关未启用时,前端不打算修改 claude 的任何字段——但服务端
|
|
1403
|
-
// 默认按 official 兌底会清空 apiKey/baseUrl。这里按"当前 config 的模式"上送,
|
|
1404
|
-
// 让已存在的 thirdparty 凭证保留不被清空。
|
|
1405
|
-
var modeToSend = claudeApiMode;
|
|
1406
|
-
if (state.view === 'wizard' && state.agentsEnabled && state.agentsEnabled.claude === false) {
|
|
1407
|
-
modeToSend = detectClaudeApiModeFromConfig(state.config && state.config.claude);
|
|
1408
|
-
}
|
|
1409
|
-
var result = await api('/api/config', 'POST', { vars: vars, claudeApiMode: modeToSend });
|
|
1239
|
+
var result = await api('/api/config', 'POST', { vars: vars });
|
|
1410
1240
|
if (result.ok) {
|
|
1411
1241
|
state.config = Object.assign({}, state.config, vars);
|
|
1412
1242
|
toast('配置已保存');
|
|
@@ -1427,7 +1257,7 @@ async function setDashboardDefaultAgent(agent, enabled) {
|
|
|
1427
1257
|
CHATCCC_CURSOR_DEFAULT_AGENT: agent === 'cursor',
|
|
1428
1258
|
CHATCCC_CODEX_DEFAULT_AGENT: agent === 'codex'
|
|
1429
1259
|
};
|
|
1430
|
-
var result = await api('/api/config', 'POST', { vars: vars
|
|
1260
|
+
var result = await api('/api/config', 'POST', { vars: vars });
|
|
1431
1261
|
if (result.ok) {
|
|
1432
1262
|
state.config.claude = state.config.claude || {};
|
|
1433
1263
|
state.config.cursor = state.config.cursor || {};
|
|
@@ -1564,20 +1394,11 @@ function updateDashboardUI() {
|
|
|
1564
1394
|
var emptyHint = document.getElementById('dash-no-agent-hint');
|
|
1565
1395
|
if (emptyHint) emptyHint.style.display = (!claudeOn && !cursorOn && !codexOn) ? '' : 'none';
|
|
1566
1396
|
|
|
1567
|
-
// 按 detectClaudeApiMode 的契约判定 API 来源(apiKey/baseUrl 任一非空 → 第三方)
|
|
1568
|
-
var claudeApiKey = (c.claude && c.claude.apiKey ? String(c.claude.apiKey) : '').trim();
|
|
1569
|
-
var claudeBaseUrl = (c.claude && c.claude.baseUrl ? String(c.claude.baseUrl) : '').trim();
|
|
1570
|
-
var isThirdPartyClaude = !!(claudeApiKey || claudeBaseUrl);
|
|
1571
|
-
document.getElementById('cfg-CLAUDE_API_MODE').textContent = isThirdPartyClaude ? '第三方 API(自定义网关)' : '官方 API(Anthropic 直连)';
|
|
1572
|
-
// 官方模式下 API Key / Base URL 行整体隐藏,避免显示无意义的 "-"
|
|
1573
|
-
document.getElementById('cfg-row-CLAUDE_API_KEY').style.display = isThirdPartyClaude ? '' : 'none';
|
|
1574
|
-
document.getElementById('cfg-row-CLAUDE_BASE_URL').style.display = isThirdPartyClaude ? '' : 'none';
|
|
1575
|
-
document.getElementById('cfg-CLAUDE_API_KEY').textContent = claudeApiKey ? '***已设置***' : '-';
|
|
1576
|
-
document.getElementById('cfg-CLAUDE_BASE_URL').textContent = claudeBaseUrl || '-';
|
|
1577
1397
|
document.getElementById('cfg-ANTHROPIC_MODEL').textContent = (c.claude && c.claude.model) || '(留空)';
|
|
1578
|
-
document.getElementById('cfg-row-ANTHROPIC_SUBAGENT_MODEL').style.display = isThirdPartyClaude ? '' : 'none';
|
|
1579
1398
|
document.getElementById('cfg-ANTHROPIC_SUBAGENT_MODEL').textContent = (c.claude && c.claude.subagentModel) || '(留空)';
|
|
1580
1399
|
document.getElementById('cfg-ANTHROPIC_EFFORT').textContent = (c.claude && c.claude.effort) || '(留空)';
|
|
1400
|
+
document.getElementById('cfg-ANTHROPIC_API_KEY').textContent = (c.claude && c.claude.apiKey) ? '***已设置***' : '(留空)';
|
|
1401
|
+
document.getElementById('cfg-ANTHROPIC_BASE_URL').textContent = (c.claude && c.claude.baseUrl) || '(留空)';
|
|
1581
1402
|
document.getElementById('cfg-CURSOR_PATH').textContent = (c.cursor && (c.cursor.path || c.cursor.command)) || '-';
|
|
1582
1403
|
document.getElementById('cfg-CURSOR_MODEL').textContent = (c.cursor && c.cursor.model) || '(留空)';
|
|
1583
1404
|
document.getElementById('cfg-CODEX_PATH').textContent = (c.codex && (c.codex.path || c.codex.command)) || 'codex';
|
|
@@ -1637,35 +1458,12 @@ function editSection(section) {
|
|
|
1637
1458
|
var html = '';
|
|
1638
1459
|
var labelMap = {
|
|
1639
1460
|
'CHATCCC_APP_ID': 'App ID', 'CHATCCC_APP_SECRET': 'App Secret',
|
|
1640
|
-
'CLAUDE_API_KEY': 'API Key', 'CLAUDE_BASE_URL': 'Base URL',
|
|
1641
1461
|
'CHATCCC_ANTHROPIC_MODEL': '模型', 'CHATCCC_ANTHROPIC_SUBAGENT_MODEL': 'Subagent 模型', 'CHATCCC_ANTHROPIC_EFFORT': 'Effort',
|
|
1462
|
+
'CHATCCC_ANTHROPIC_API_KEY': 'API Key', 'CHATCCC_ANTHROPIC_BASE_URL': 'Base URL',
|
|
1642
1463
|
'CHATCCC_CURSOR_PATH': 'CLI 路径', 'CHATCCC_CURSOR_MODEL': '模型',
|
|
1643
1464
|
'CHATCCC_CODEX_PATH': 'CLI 路径', 'CHATCCC_CODEX_MODEL': '模型', 'CHATCCC_CODEX_EFFORT': 'Effort'
|
|
1644
1465
|
};
|
|
1645
1466
|
|
|
1646
|
-
// Claude Agent 编辑:先按当前 config 决定初始 mode,并在 modal 顶部插入切换器;
|
|
1647
|
-
// 把 API Key / Base URL 的输入框装进同一个 .hidden 容器,按 mode 显隐与 Wizard 一致。
|
|
1648
|
-
if (section === 'claude') {
|
|
1649
|
-
var initialMode = detectClaudeApiModeFromConfig(state.config && state.config.claude);
|
|
1650
|
-
claudeApiMode = initialMode;
|
|
1651
|
-
html += '<div class="form-group" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:12px 14px">';
|
|
1652
|
-
html += ' <label style="margin-bottom:8px;font-weight:600;color:#334155">API 来源</label>';
|
|
1653
|
-
html += ' <div style="display:flex;gap:18px;flex-wrap:wrap">';
|
|
1654
|
-
html += ' <label style="display:flex;align-items:center;gap:6px;font-weight:500;cursor:pointer">';
|
|
1655
|
-
html += ' <input type="radio" name="edit-claude-api-mode" value="official"' + (initialMode === 'official' ? ' checked' : '') + ' onchange="onClaudeApiModeChange(\\'official\\')">';
|
|
1656
|
-
html += ' 官方 API(Anthropic 直连)';
|
|
1657
|
-
html += ' </label>';
|
|
1658
|
-
html += ' <label style="display:flex;align-items:center;gap:6px;font-weight:500;cursor:pointer">';
|
|
1659
|
-
html += ' <input type="radio" name="edit-claude-api-mode" value="thirdparty"' + (initialMode === 'thirdparty' ? ' checked' : '') + ' onchange="onClaudeApiModeChange(\\'thirdparty\\')">';
|
|
1660
|
-
html += ' 第三方 API(自定义网关)';
|
|
1661
|
-
html += ' </label>';
|
|
1662
|
-
html += ' </div>';
|
|
1663
|
-
html += ' <div class="hint" style="margin-top:6px">切回官方模式后保存,已填的密钥会被清空。</div>';
|
|
1664
|
-
html += '</div>';
|
|
1665
|
-
}
|
|
1666
|
-
|
|
1667
|
-
var thirdPartyOpened = false;
|
|
1668
|
-
var thirdPartyClosed = false;
|
|
1669
1467
|
fields.forEach(function(key){
|
|
1670
1468
|
var val = state.config[key] || '';
|
|
1671
1469
|
// Also check nested config
|
|
@@ -1674,11 +1472,11 @@ function editSection(section) {
|
|
|
1674
1472
|
if (key === 'CHATCCC_APP_ID' && state.config.feishu) val = state.config.feishu.appId || '';
|
|
1675
1473
|
else if (key === 'CHATCCC_APP_SECRET' && state.config.feishu) val = state.config.feishu.appSecret || '';
|
|
1676
1474
|
} else if (section === 'claude' && state.config.claude) {
|
|
1677
|
-
if (key === '
|
|
1678
|
-
else if (key === 'CLAUDE_BASE_URL') val = state.config.claude.baseUrl || '';
|
|
1679
|
-
else if (key === 'CHATCCC_ANTHROPIC_MODEL') val = state.config.claude.model || '';
|
|
1475
|
+
if (key === 'CHATCCC_ANTHROPIC_MODEL') val = state.config.claude.model || '';
|
|
1680
1476
|
else if (key === 'CHATCCC_ANTHROPIC_SUBAGENT_MODEL') val = state.config.claude.subagentModel || '';
|
|
1681
1477
|
else if (key === 'CHATCCC_ANTHROPIC_EFFORT') val = state.config.claude.effort || '';
|
|
1478
|
+
else if (key === 'CHATCCC_ANTHROPIC_API_KEY') val = state.config.claude.apiKey || '';
|
|
1479
|
+
else if (key === 'CHATCCC_ANTHROPIC_BASE_URL') val = state.config.claude.baseUrl || '';
|
|
1682
1480
|
} else if (section === 'cursor' && state.config.cursor) {
|
|
1683
1481
|
if (key === 'CHATCCC_CURSOR_PATH') val = state.config.cursor.path || state.config.cursor.command || '';
|
|
1684
1482
|
else if (key === 'CHATCCC_CURSOR_MODEL') val = state.config.cursor.model || '';
|
|
@@ -1689,37 +1487,10 @@ function editSection(section) {
|
|
|
1689
1487
|
}
|
|
1690
1488
|
}
|
|
1691
1489
|
var isSecret = key.includes('SECRET') || key.includes('API_KEY');
|
|
1692
|
-
|
|
1693
|
-
var isClaudeSubagentField = section === 'claude' && key === 'CHATCCC_ANTHROPIC_SUBAGENT_MODEL';
|
|
1694
|
-
|
|
1695
|
-
if (isClaudeThirdPartyField && !thirdPartyOpened) {
|
|
1696
|
-
html += '<div id="edit-claude-thirdparty-fields"' + (claudeApiMode === 'thirdparty' ? '' : ' class="hidden"') + '>';
|
|
1697
|
-
thirdPartyOpened = true;
|
|
1698
|
-
}
|
|
1699
|
-
|
|
1700
|
-
// 在第三方字段之后、第一个通用字段之前闭合 wrapper,
|
|
1701
|
-
// 避免 model / subagentModel / effort 被包裹在 hidden div 中。
|
|
1702
|
-
if (thirdPartyOpened && !thirdPartyClosed && !isClaudeThirdPartyField) {
|
|
1703
|
-
html += '</div>';
|
|
1704
|
-
thirdPartyClosed = true;
|
|
1705
|
-
}
|
|
1706
|
-
|
|
1707
|
-
var groupClass = 'form-group' + (isClaudeSubagentField && claudeApiMode !== 'thirdparty' ? ' hidden' : '');
|
|
1708
|
-
var subagentAttr = isClaudeSubagentField ? ' data-claude-subagent-field="1"' : '';
|
|
1709
|
-
html += '<div class="' + groupClass + '"' + subagentAttr + '><label>' + (labelMap[key] || key) + '</label>';
|
|
1490
|
+
html += '<div class="form-group"><label>' + (labelMap[key] || key) + '</label>';
|
|
1710
1491
|
html += '<input type="' + (isSecret ? 'password' : 'text') + '" id="edit-' + key + '" value="' + String(val).replace(/"/g,'"') + '">';
|
|
1711
1492
|
html += '</div>';
|
|
1712
1493
|
});
|
|
1713
|
-
|
|
1714
|
-
if (thirdPartyOpened && !thirdPartyClosed) html += '</div>';
|
|
1715
|
-
|
|
1716
|
-
document.getElementById('edit-modal-fields').innerHTML = html;
|
|
1717
|
-
document.getElementById('edit-modal').classList.remove('hidden');
|
|
1718
|
-
document.getElementById('edit-overlay').classList.remove('hidden');
|
|
1719
|
-
|
|
1720
|
-
// Edit Modal 的 input 是 innerHTML 现写入的,需要在 DOM 就绪后再触发一次同步:
|
|
1721
|
-
// 让 model 输入框的 placeholder / 第三方字段容器的显隐状态,跟当前 claudeApiMode 对齐。
|
|
1722
|
-
if (section === 'claude') onClaudeApiModeChange(claudeApiMode);
|
|
1723
1494
|
}
|
|
1724
1495
|
|
|
1725
1496
|
function closeEditModal() {
|
|
@@ -1735,24 +1506,11 @@ async function saveEdit() {
|
|
|
1735
1506
|
|
|
1736
1507
|
var vars = {};
|
|
1737
1508
|
fields.forEach(function(key){
|
|
1738
|
-
if (editSectionType === 'claude' && claudeApiMode !== 'thirdparty' && key === 'CHATCCC_ANTHROPIC_SUBAGENT_MODEL') return;
|
|
1739
1509
|
var el = document.getElementById('edit-' + key);
|
|
1740
1510
|
if (el) vars[key] = el.value.trim();
|
|
1741
1511
|
});
|
|
1742
|
-
// 编辑 Claude 时按当前 mode 强制清空 apiKey/baseUrl(与 collectAllFields 保持一致);
|
|
1743
|
-
// 服务端 applyClaudeApiMode 也会兌底,但这里前端先做能让 state.config 立即同步。
|
|
1744
|
-
if (editSectionType === 'claude' && claudeApiMode !== 'thirdparty') {
|
|
1745
|
-
vars.CLAUDE_API_KEY = '';
|
|
1746
|
-
vars.CLAUDE_BASE_URL = '';
|
|
1747
|
-
}
|
|
1748
1512
|
await saveConfig(vars);
|
|
1749
1513
|
closeEditModal();
|
|
1750
|
-
// 本地 state.config.claude 也按 mode 同步清空,避免 dashboard UI 残留旧值
|
|
1751
|
-
if (editSectionType === 'claude' && claudeApiMode !== 'thirdparty') {
|
|
1752
|
-
state.config.claude = state.config.claude || {};
|
|
1753
|
-
state.config.claude.apiKey = '';
|
|
1754
|
-
state.config.claude.baseUrl = '';
|
|
1755
|
-
}
|
|
1756
1514
|
updateDashboardUI();
|
|
1757
1515
|
toast('修改已保存。若服务正在运行,需重启生效。');
|
|
1758
1516
|
}
|
|
@@ -1762,7 +1520,7 @@ function reconfigure() {
|
|
|
1762
1520
|
if (!confirm('这将重新打开配置向导。现有配置不会丢失。')) return;
|
|
1763
1521
|
state.view = 'wizard';
|
|
1764
1522
|
state.wizardStep = 1;
|
|
1765
|
-
// agentsEnabled
|
|
1523
|
+
// agentsEnabled 留给 renderStep2() 按已有 config 重新判定
|
|
1766
1524
|
showWizard().catch(function(){});
|
|
1767
1525
|
}
|
|
1768
1526
|
|