chatccc 0.2.103 → 0.2.107

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/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(normalized),
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 Agent SDK<br>官方/第三方 API 均可</div>
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" style="background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px;padding:12px 14px">
683
- <label style="margin-bottom:8px;font-weight:600;color:#334155">API 来源</label>
684
- <div style="display:flex;flex-direction:column;gap:6px">
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
- <div id="claude-thirdparty-fields" class="hidden">
698
- <div class="form-group">
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>模型</label>
712
- <input type="text" id="field-CHATCCC_ANTHROPIC_MODEL" placeholder="留空表示不向 SDK 传 model">
641
+ <label>思考深度 Effort(选填)</label>
642
+ <input type="text" id="field-CHATCCC_ANTHROPIC_EFFORT" placeholder="留空使用默认值">
713
643
  </div>
714
- <div class="form-group" id="claude-subagent-model-field">
715
- <label>Subagent 模型</label>
716
- <input type="text" id="field-CHATCCC_ANTHROPIC_SUBAGENT_MODEL" placeholder="留空表示不向 SDK CLAUDE_CODE_SUBAGENT_MODEL">
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>思考深度 (Effort)</label>
720
- <input type="text" id="field-CHATCCC_ANTHROPIC_EFFORT" placeholder="留空表示不向 SDK 传 effort">
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" id="cfg-row-ANTHROPIC_SUBAGENT_MODEL"><span class="key">Subagent 模型</span><span class="val" id="cfg-ANTHROPIC_SUBAGENT_MODEL">-</span></div>
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: ['CLAUDE_API_KEY','CLAUDE_BASE_URL','CHATCCC_ANTHROPIC_MODEL','CHATCCC_ANTHROPIC_SUBAGENT_MODEL','CHATCCC_ANTHROPIC_EFFORT'],
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, claudeApiMode: detectClaudeApiModeFromConfig(state.config && state.config.claude) });
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 && isClaudeFieldsValid()) validCount++;
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 = ['apiKey','baseUrl','model','subagentModel','effort'];
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
- // 字段输入时实时刷新"下一步"按钮(Claude 第三方模式 apiKey/baseUrl 必填)
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
- // 同时把当前 claudeApiMode 传给服务端:服务端 applyClaudeApiMode() 会按
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, claudeApiMode: detectClaudeApiModeFromConfig(state.config && state.config.claude) });
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 === 'CLAUDE_API_KEY') val = state.config.claude.apiKey || '';
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
- var isClaudeThirdPartyField = section === 'claude' && (key === 'CLAUDE_API_KEY' || key === 'CLAUDE_BASE_URL');
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,'&quot;') + '">';
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 / claudeApiMode 都留给 renderStep2() 按已有 config 重新判定
1523
+ // agentsEnabled 留给 renderStep2() 按已有 config 重新判定
1766
1524
  showWizard().catch(function(){});
1767
1525
  }
1768
1526