cabletidy 0.1.0 → 0.2.0

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/web/app.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { configurationId, providerIdForConfiguration, normalizeConfigurationIdentities, configurationBaseUrl } from "./config-identity.js";
2
+ import { CLAUDE_MODEL_CATALOG, CLAUDE_MODEL_ALIASES } from "./claude-models.js";
2
3
 
3
4
  const pageContent = document.querySelector("#page-content");
4
5
  const pageTitle = document.querySelector("#page-title");
@@ -26,7 +27,6 @@ const PAGE_META = {
26
27
  overview: "配置套装",
27
28
  "suite-create": "新建配置",
28
29
  "suite-detail": "配置详情",
29
- upstreams: "上游管理",
30
30
  models: "模型管理",
31
31
  diagnostics: "诊断",
32
32
  };
@@ -38,15 +38,14 @@ const state = {
38
38
  runtime: null,
39
39
  events: [],
40
40
  codexCatalog: null,
41
+ createTarget: "codex",
41
42
  busy: false,
42
43
  pendingSecrets: {
43
44
  upstreamSecrets: {},
44
45
  },
45
46
  selected: {
46
- upstream: null,
47
47
  model: null,
48
48
  virtualProvider: null,
49
- binding: null,
50
49
  suite: null,
51
50
  },
52
51
  artifactPreview: null,
@@ -119,10 +118,8 @@ async function bootstrap() {
119
118
  state.runtime = runtime;
120
119
  state.codexCatalog = codexCatalog;
121
120
  state.events = events.events || [];
122
- state.selected.upstream = firstKey(state.candidate.upstreams);
123
121
  state.selected.model = firstKey(state.candidate.models);
124
122
  state.selected.virtualProvider = firstKey(state.candidate.virtualProviders);
125
- state.selected.binding = firstKey(state.candidate.bindings);
126
123
  state.selected.suite = firstKey(state.candidate.bindings);
127
124
  railStatus.textContent = "服务运行中";
128
125
  railStatus.parentElement.classList.remove("is-offline");
@@ -162,7 +159,6 @@ function render(preservedForms = []) {
162
159
  overview: renderOverview,
163
160
  "suite-create": renderSuiteCreate,
164
161
  "suite-detail": renderSuiteDetail,
165
- upstreams: renderUpstreams,
166
162
  models: renderModels,
167
163
  diagnostics: renderDiagnostics,
168
164
  };
@@ -242,7 +238,8 @@ function restoreFormChanges(form, replacement) {
242
238
  const current = formSnapshot(form);
243
239
  const sameContext = form.getAttribute("data-suite-context") === replacement.getAttribute("data-suite-context");
244
240
  if (sameContext && current === latest) return;
245
- if (sameContext && baseline === latest) {
241
+ // Creation drafts have no remote record to merge, even after switching CLI.
242
+ if (form.getAttribute("id") === "suite-create-form" || (sameContext && baseline === latest)) {
246
243
  if (formConflicts.has(form)) form.querySelector("[data-form-feedback]")?.remove();
247
244
  formConflicts.delete(form);
248
245
  replacement.replaceWith(form);
@@ -385,10 +382,9 @@ function selectedSuite() {
385
382
  function selectSuite(id) {
386
383
  const suite = configurationSuites(state.candidate || {}).find((item) => item.id === id);
387
384
  state.selected.suite = id || null;
385
+ if (state.artifactPreview?.bindingId !== id) state.artifactPreview = null;
388
386
  if (!suite) return;
389
- state.selected.binding = suite.bindingId;
390
387
  state.selected.virtualProvider = suite.binding.virtualProvider;
391
- state.selected.upstream = suite.upstreamId;
392
388
  state.selected.model = suite.modelIds[0] || null;
393
389
  }
394
390
 
@@ -406,7 +402,7 @@ function suiteProviderStatus(suite) {
406
402
  }
407
403
 
408
404
  function suiteEndpoint(suite) {
409
- return `${configurationBaseUrl(state.candidate, suite.bindingId)}/v1`;
405
+ return `${configurationBaseUrl(state.candidate, suite.bindingId)}${suite.target === "claude-code" ? "" : "/v1"}`;
410
406
  }
411
407
 
412
408
  function suiteRow(suite) {
@@ -449,14 +445,16 @@ function suiteModelEditor(suite, modelId, profile, upstreamId) {
449
445
  <div class="suite-model-mapping">
450
446
  ${isCodex
451
447
  ? officialModelSelect("data-suite-model-client", model.clientModelId || model.aliases?.[0] || "")
452
- : `<label class="field"><span>客户端模型 ID</span><input data-suite-model-client value="${esc(model.clientModelId || model.aliases?.[0] || modelId)}" placeholder="客户端模型 ID" required /></label>`}
448
+ : suite.target === "claude-code"
449
+ ? claudeModelInput("data-suite-model-client", model.clientModelId || model.aliases?.[0] || modelId)
450
+ : `<label class="field"><span>客户端模型 ID</span><input data-suite-model-client value="${esc(model.clientModelId || model.aliases?.[0] || modelId)}" placeholder="客户端模型 ID" required /></label>`}
453
451
  <span class="suite-mapping-arrow" aria-hidden="true">&rarr;</span>
454
452
  ${upstreamId
455
453
  ? `<label class="field"><span>上游模型 ID(可选)</span><input data-suite-model-upstream="${esc(upstreamId)}" value="${esc(binding.upstreamModelId || "")}" placeholder="留空使用请求中的模型名" /></label>`
456
454
  : `<div class="notice warning">请先配置上游连接</div>`}
457
455
  <button class="mini-button" type="button" data-action="remove-suite-model" aria-label="移除模型 ${esc(model.clientModelId || modelId || "映射")}">移除</button>
458
456
  </div>
459
- <details class="suite-model-settings" ${expanded ? "open" : ""}>
457
+ ${suite.target === "claude-code" ? "" : `<details class="suite-model-settings" ${expanded ? "open" : ""}>
460
458
  <summary><span>模型能力与上下文</span><span class="field-hint" data-model-policy-summary>${isCodex ? model.codex?.metadataMode === "override" ? "覆盖上游限制" : "沿用官方定义" : "自定义设置"}</span></summary>
461
459
  <div class="form-grid suite-model-policy">
462
460
  ${isCodex ? codexMetadataFields(model) : `
@@ -467,7 +465,7 @@ function suiteModelEditor(suite, modelId, profile, upstreamId) {
467
465
  ${upstreamId ? `<label class="field"><span>上游能力覆盖</span><input data-suite-model-capabilities="${esc(upstreamId)}" value="${esc((binding.capabilityOverrides || []).join(", "))}" placeholder="可留空" /></label>` : ""}
468
466
  `}
469
467
  </div>
470
- </details>
468
+ </details>`}
471
469
  </article>
472
470
  `;
473
471
  }
@@ -554,6 +552,25 @@ function syncCodexMetadata(row, resetOverrides = false) {
554
552
  }
555
553
 
556
554
  pageContent.addEventListener("change", (event) => {
555
+ if (event.target.matches('#suite-create-form [name="target"]')) {
556
+ state.createTarget = event.target.value;
557
+ const form = event.target.closest("form");
558
+ form.querySelector(".catalog-status-wrapper").hidden = state.createTarget !== "codex";
559
+ form.querySelector("[data-claude-create-auth]").hidden = state.createTarget !== "claude-code";
560
+ form.querySelector("[data-claude-model-suggestions]").hidden = state.createTarget !== "claude-code";
561
+ form.querySelectorAll("[data-claude-create-models]").forEach((section) => {
562
+ section.hidden = state.createTarget !== "claude-code";
563
+ });
564
+ form.querySelectorAll("[data-create-model]").forEach((row) => {
565
+ const wrapper = document.createElement("div");
566
+ wrapper.innerHTML = createModelRow({
567
+ clientModelId: row.querySelector('[data-create-field="clientModelId"]').value,
568
+ upstreamModelId: row.querySelector('[data-create-field="upstreamModelId"]').value,
569
+ });
570
+ row.replaceWith(wrapper.firstElementChild);
571
+ });
572
+ syncClaudeCreateModels(form);
573
+ }
557
574
  const modelChanged = event.target.matches("[data-official-model]");
558
575
  if (modelChanged) updateOfficialModelHint(event.target);
559
576
  if (modelChanged || event.target.matches("[data-codex-metadata-mode]")) {
@@ -561,6 +578,12 @@ pageContent.addEventListener("change", (event) => {
561
578
  }
562
579
  });
563
580
 
581
+ pageContent.addEventListener("input", (event) => {
582
+ if (event.target.matches('#suite-create-form [data-create-field="clientModelId"]')) {
583
+ syncClaudeCreateModels(event.target.closest("form"));
584
+ }
585
+ });
586
+
564
587
  pageContent.addEventListener("invalid", (event) => {
565
588
  // Reveal invalid controls before the browser tries to focus them.
566
589
  const settings = event.target.closest(".suite-model-settings");
@@ -619,21 +642,23 @@ function renderSuiteDetail() {
619
642
  <div class="form-grid suite-upstream-fields">
620
643
  ${field("上游名称", "name", upstream.name || upstreamId, "例如 Relay A")}
621
644
  ${field("上游地址", "baseUrl", upstream.baseUrl || "", "https://relay.example.com/v1", false, "url")}
622
- ${field("API Key", "secret", "", upstreamSecretConfigured(upstreamId, upstream) ? "已配置,留空保留现有密钥" : "粘贴上游 API Key", false, "password")}
645
+ ${field(suite.target === "claude-code" ? "上游凭据" : "API Key", "secret", "", upstream.secretConfigured ? "已配置,留空保留现有密钥" : "粘贴上游凭据", false, "password")}
646
+ ${suite.target === "claude-code" ? upstreamAuthField("authHeader", upstream.auth?.header || upstream.authHeader || "x-api-key") : ""}
623
647
  </div>
624
648
  <div class="suite-section-footer">
625
649
  <span class="field-hint">保存后生效,测试使用已保存的连接。</span>
626
650
  <div class="form-actions"><button class="button" type="submit">保存上游</button><button class="button" type="button" data-action="test-upstream" data-id="${esc(upstreamId)}">测试连通性</button></div>
627
651
  </div>
628
652
  </form>
629
- ` : `<div class="empty"><strong>未配置上游</strong><div class="form-actions"><button class="button" data-action="new-upstream">添加上游</button></div></div>`}
653
+ ` : `<div class="empty"><strong>未配置上游</strong></div>`}
630
654
  </section>
631
655
  <section class="suite-section" aria-labelledby="suite-models-title">
632
656
  <div class="suite-section-heading suite-models-heading">
633
- <div><h2 id="suite-models-title">模型设置(可选)</h2><p>默认透传请求中的模型名;需要改名或覆盖上下文等参数时再添加设置。</p></div>
657
+ <div><h2 id="suite-models-title">模型设置(可选)</h2><p>${suite.target === "claude-code" ? "默认透传模型名;改名时填写 Claude Code 请求中的完整模型 ID。模型能力、上下文和压缩由客户端与上游决定。" : "默认透传请求中的模型名;需要改名或覆盖上下文等参数时再添加设置。"}</p></div>
634
658
  <button class="button" type="button" data-action="add-suite-model">添加模型设置</button>
635
659
  </div>
636
660
  ${suite.target === "codex" ? codexCatalogStatus() : ""}
661
+ ${suite.target === "claude-code" ? claudeModelSuggestions() : ""}
637
662
  <form id="suite-models-form" data-suite-context="${esc(JSON.stringify([suite.bindingId, suite.target, binding.virtualProvider, suite.route?.id, upstreamId]))}">
638
663
  <div id="suite-model-list" class="suite-model-list">
639
664
  ${suite.models.length
@@ -641,14 +666,15 @@ function renderSuiteDetail() {
641
666
  : `<div class="empty">已启用模型名直接透传,无需添加模型设置。</div>`}
642
667
  </div>
643
668
  <div class="suite-section-footer">
644
- <span class="field-hint">按需展开模型能力与上下文设置。</span>
669
+ <span class="field-hint">${suite.target === "claude-code" ? "上游模型 ID 留空时原样透传。" : "按需展开模型能力与上下文设置。"}</span>
645
670
  <div class="form-actions">
646
671
  <button class="button" type="submit">保存模型设置</button>
647
- ${suite.target === "codex" ? "" : `<button class="button" type="button" data-action="open-advanced-models">高级模型设置</button>`}
672
+ ${suite.target === "generic-env" ? `<button class="button" type="button" data-action="open-advanced-models">高级模型设置</button>` : ""}
648
673
  </div>
649
674
  </div>
650
675
  </form>
651
676
  </section>
677
+ ${suite.target === "claude-code" ? claudeClientForm(suite) : ""}
652
678
  </div>
653
679
  <section class="panel suite-connection" aria-labelledby="suite-connection-title">
654
680
  <div class="panel-header">
@@ -656,7 +682,8 @@ function renderSuiteDetail() {
656
682
  <div class="suite-connection-actions">
657
683
  <button class="button button-quiet" type="button" data-action="toggle-vp" data-id="${esc(provider.id)}">${provider.enabled === false ? "启动服务" : "暂停服务"}</button>
658
684
  <button class="button" type="button" data-action="preview-artifacts">预览配置</button>
659
- ${binding.target === "codex" ? `<button class="button button-primary" type="button" data-action="apply-target">应用到 Codex</button>` : ""}
685
+ ${["codex", "claude-code"].includes(binding.target) ? `<button class="button button-primary" type="button" data-action="apply-target">应用到 ${esc(targetLabel(binding.target))}</button>` : ""}
686
+ ${binding.target === "claude-code" ? `<button class="button" type="button" data-action="restore-target">撤销接入</button>` : ""}
660
687
  </div>
661
688
  </div>
662
689
  <div class="panel-body">
@@ -670,6 +697,107 @@ function renderSuiteDetail() {
670
697
  `;
671
698
  }
672
699
 
700
+ function upstreamAuthField(name, header = "x-api-key") {
701
+ const options = [
702
+ ["x-api-key", "API Key (x-api-key / ANTHROPIC_API_KEY)"],
703
+ ["authorization", "Bearer Token (Authorization / ANTHROPIC_AUTH_TOKEN)"],
704
+ ];
705
+ if (!options.some(([value]) => value === header)) options.push([header, `保留现有认证 (${header})`]);
706
+ return `<label class="field"><span>上游认证方式</span><select name="${esc(name)}">${options.map(([value, label]) => `<option value="${esc(value)}" ${value === header ? "selected" : ""}>${esc(label)}</option>`).join("")}</select><span class="field-hint">环境变量名用于对照 Claude 直连上游的配置;这里选择的是 CableTidy 访问上游的认证方式。</span></label>`;
707
+ }
708
+
709
+ function claudeDefaultModel(suite) {
710
+ if (suite.binding.claude?.setModel === false) return "";
711
+ return suite.binding.defaultModel || suite.virtualProvider.defaultModel || "";
712
+ }
713
+
714
+ function claudeModelInput(attributes, value) {
715
+ return `<label class="field"><span>客户端模型 ID</span><input ${attributes} list="claude-model-suggestions" value="${esc(value)}" placeholder="搜索官方建议或手动填写" autocomplete="off" required /></label>`;
716
+ }
717
+
718
+ function claudeModelSuggestions() {
719
+ return `<p class="field-hint">官方建议来自 <a href="${esc(CLAUDE_MODEL_CATALOG.sources[0])}" target="_blank" rel="noreferrer">Anthropic 文档</a>(${esc(CLAUDE_MODEL_CATALOG.updatedAt)}),随 CableTidy 发版更新,无需额外 API Key;可手动填写其他模型 ID。</p>
720
+ <datalist id="claude-model-suggestions">${CLAUDE_MODEL_CATALOG.models.map((id) => `<option value="${esc(id)}"></option>`).join("")}</datalist>`;
721
+ }
722
+
723
+ function claudeAliasSelect(suite, family) {
724
+ const selected = suite?.binding.claude?.models?.[family] || "";
725
+ const ids = (suite?.models || []).map(({ id, profile }) => profile.clientModelId || profile.aliases?.[0] || id);
726
+ return `<label class="field"><span>${family[0].toUpperCase() + family.slice(1)} 别名指向</span><select name="${family}">
727
+ ${claudeAliasOptions(ids, selected)}
728
+ </select></label>`;
729
+ }
730
+
731
+ function claudeAliasOptions(ids, selected = "") {
732
+ return `<option value="" ${selected ? "" : "selected"}>由 Claude 默认决定</option>
733
+ ${selected && !ids.includes(selected) ? `<option value="${esc(selected)}" selected disabled>${esc(selected)}(未配置,请重新选择)</option>` : ""}
734
+ ${optionList(ids, selected)}`;
735
+ }
736
+
737
+ function claudeModelChoice(name, ids, value) {
738
+ const selected = String(value || "").trim();
739
+ return CLAUDE_MODEL_ALIASES[name].includes(selected) || ids.includes(selected) ? selected : "";
740
+ }
741
+
742
+ function claudeDefaultModelOptions(name, ids, value = "") {
743
+ const selected = claudeModelChoice(name, ids, value);
744
+ const aliases = CLAUDE_MODEL_ALIASES[name];
745
+ const clientIds = [...new Set(ids)].filter((id) => !aliases.includes(id));
746
+ return `<optgroup label="默认行为"><option value="" ${selected ? "" : "selected"}>${name === "defaultModel" ? "由 Claude 默认决定" : "沿用 Claude 默认行为"}</option></optgroup>
747
+ <optgroup label="模型别名">${optionList(aliases, selected)}</optgroup>
748
+ ${clientIds.length ? `<optgroup label="当前配置的客户端模型 ID">${optionList(clientIds, selected)}</optgroup>` : ""}`;
749
+ }
750
+
751
+ function claudeDefaultModelSelect(name, ids, value = "") {
752
+ return `<label class="field"><span>${name === "defaultModel" ? "启动模型(可选)" : "子代理模型(可选)"}</span>
753
+ <select name="${name}" data-claude-default-model>${claudeDefaultModelOptions(name, ids, value)}</select></label>`;
754
+ }
755
+
756
+ function syncClaudeCreateModels(form) {
757
+ if (!form || state.createTarget !== "claude-code") return;
758
+ const ids = [...new Set([...form.querySelectorAll('[data-create-field="clientModelId"]')]
759
+ .map((input) => input.value.trim()).filter(Boolean))];
760
+ for (const select of form.querySelectorAll("[data-claude-create-models] select")) {
761
+ select.innerHTML = select.hasAttribute("data-claude-default-model")
762
+ ? claudeDefaultModelOptions(select.name, ids, select.value)
763
+ : claudeAliasOptions(ids, ids.includes(select.value) ? select.value : "");
764
+ }
765
+ }
766
+
767
+ function claudeModelGroups(suite = null) {
768
+ const ids = (suite?.models || []).map(({ id, profile }) => profile.clientModelId || profile.aliases?.[0] || id);
769
+ return `<section class="subsection" aria-labelledby="claude-aliases-title">
770
+ <div class="subsection-header"><h3 id="claude-aliases-title">模型别名</h3></div>
771
+ <p class="field-hint">${suite ? "从当前配置已保存的" : "可选。从本次填写的"}客户端模型 ID 中选择,或由 Claude 默认决定。</p>
772
+ <div class="form-grid">
773
+ ${["opus", "sonnet", "fable", "haiku"].map((family) => claudeAliasSelect(suite, family)).join("")}
774
+ </div>
775
+ </section>
776
+ <section class="subsection" aria-labelledby="claude-defaults-title">
777
+ <div class="subsection-header"><h3 id="claude-defaults-title">默认模型</h3></div>
778
+ <div class="form-grid">
779
+ ${claudeDefaultModelSelect("defaultModel", ids, suite ? claudeDefaultModel(suite) : "")}
780
+ ${claudeDefaultModelSelect("subagent", ids, suite?.binding.claude?.models?.subagent || "")}
781
+ </div>
782
+ <p class="field-hint">可选。选择适用的官方别名或当前配置的客户端模型 ID;默认行为不主动覆盖 Claude 的模型选择。</p>
783
+ </section>`;
784
+ }
785
+
786
+ function claudeClientForm(suite) {
787
+ const options = suite.binding.claude || {};
788
+ return `<section class="suite-section">
789
+ <div class="suite-section-heading"><h2>Claude Code 模型选择</h2><p>设置模型别名和默认模型。上游改名在模型设置中配置。</p></div>
790
+ <form id="claude-client-form" data-suite-context="${esc(JSON.stringify([suite.bindingId, suite.models.map(({ id, profile }) => profile.clientModelId || profile.aliases?.[0] || id)]))}">
791
+ ${claudeModelGroups(suite)}
792
+ <div class="form-actions">
793
+ ${checkboxField("在 /model 中发现此配置的模型", "discoverModels", options.discoverModels === true)}
794
+ </div>
795
+ <p class="field-hint">模型发现只列出已配置模型,不限制其他模型请求。Claude Code 可能过滤不含 claude 或 anthropic 的模型 ID。保存后需重新应用客户端配置。</p>
796
+ <div class="form-actions"><button class="button" type="submit">保存客户端设置</button></div>
797
+ </form>
798
+ </section>`;
799
+ }
800
+
673
801
  function renderSuiteCreate() {
674
802
  return `
675
803
  <button class="text-button" data-action="back-overview">返回列表</button>
@@ -678,7 +806,7 @@ function renderSuiteCreate() {
678
806
  <h2 id="config-basics">基本信息</h2>
679
807
  <div class="form-grid">
680
808
  ${field("配置名称(可选)", "suiteName", "", "例如 my-relay;留空自动生成")}
681
- ${selectField("CLI", "target", "codex", ["codex"])}
809
+ ${selectField("CLI", "target", state.createTarget, ["codex", "claude-code"])}
682
810
  </div>
683
811
  </section>
684
812
  <section class="config-section" aria-labelledby="config-upstream">
@@ -686,6 +814,7 @@ function renderSuiteCreate() {
686
814
  <div class="form-grid">
687
815
  ${field("上游地址", "upstreamBaseUrl", "", "https://relay.example.com/v1", false, "url", false, true)}
688
816
  ${field("API Key", "upstreamSecret", "", "", false, "password", false, true)}
817
+ <div data-claude-create-auth ${state.createTarget === "claude-code" ? "" : "hidden"}>${upstreamAuthField("upstreamAuth")}</div>
689
818
  </div>
690
819
  </section>
691
820
  <section class="config-section" aria-labelledby="config-models">
@@ -693,10 +822,16 @@ function renderSuiteCreate() {
693
822
  <h2 id="config-models">模型设置(可选)</h2>
694
823
  <button class="mini-button" type="button" data-action="add-create-model">添加模型设置</button>
695
824
  </div>
696
- <p class="field-hint">默认透传请求中的模型名。需要改名时添加设置,上下文等参数可在创建后调整。</p>
697
- ${codexCatalogStatus()}
825
+ <p class="field-hint">默认透传请求中的模型名。需要改名时添加设置,客户端选项可在创建后调整。</p>
826
+ <div ${state.createTarget === "codex" ? "" : "hidden"} class="catalog-status-wrapper">${codexCatalogStatus()}</div>
827
+ <div data-claude-model-suggestions ${state.createTarget === "claude-code" ? "" : "hidden"}>${claudeModelSuggestions()}</div>
698
828
  <div id="create-model-list" class="create-model-list"></div>
699
829
  </section>
830
+ <section class="config-section" data-claude-create-models aria-labelledby="config-claude-models" ${state.createTarget === "claude-code" ? "" : "hidden"}>
831
+ <h2 id="config-claude-models">Claude Code 模型选择</h2>
832
+ <p class="field-hint">以下两组均为可选;创建后需应用到 Claude Code 才会写入客户端配置。</p>
833
+ <div>${claudeModelGroups()}</div>
834
+ </section>
700
835
  <div class="config-form-footer">
701
836
  <button class="button" type="button" data-action="back-overview">取消</button>
702
837
  <button class="button button-primary" type="submit">创建配置</button>
@@ -705,23 +840,23 @@ function renderSuiteCreate() {
705
840
  `;
706
841
  }
707
842
 
708
- function createModelRow(values = {}) {
843
+ function createModelRow(values = {}, target = state.createTarget) {
709
844
  const model = {
710
845
  clientModelId: values.clientModelId || "",
711
846
  upstreamModelId: values.upstreamModelId || "",
712
847
  };
713
848
  return `
714
849
  <div class="create-model-row" data-create-model>
715
- ${officialModelSelect('data-create-field="clientModelId"', model.clientModelId)}
850
+ ${target === "claude-code" ? claudeModelInput('data-create-field="clientModelId"', model.clientModelId) : officialModelSelect('data-create-field="clientModelId"', model.clientModelId)}
716
851
  <label class="field"><span>上游模型 ID(可选)</span><input data-create-field="upstreamModelId" value="${esc(model.upstreamModelId)}" placeholder="留空使用请求中的模型名" /></label>
717
852
  <button class="mini-button" type="button" data-action="remove-create-model" aria-label="移除模型映射">移除</button>
718
853
  </div>
719
854
  `;
720
855
  }
721
856
 
722
- function suiteDefaults() {
857
+ function suiteDefaults(target = "codex") {
723
858
  const upstreamId = nextConfigId(state.candidate?.upstreams, "relay-main");
724
- const bindingId = nextConfigId(state.candidate?.bindings, "codex-main");
859
+ const bindingId = nextConfigId(state.candidate?.bindings, target === "claude-code" ? "claude-main" : "codex-main");
725
860
  const routeId = nextConfigId(state.candidate?.routes, `${upstreamId}-route`);
726
861
  return { upstreamId, bindingId, routeId };
727
862
  }
@@ -758,52 +893,6 @@ function upstreamDisplayName(baseUrl, fallback) {
758
893
  }
759
894
  }
760
895
 
761
- function renderUpstreams() {
762
- const config = state.candidate;
763
- const selectedId = state.selected.upstream;
764
- const selected = selectedId ? config.upstreams[selectedId] : null;
765
- const integrationLabel = selected?.integration === "codex-native-provider"
766
- ? "Codex Native Provider Integration"
767
- : "Native upstream connection";
768
- return `
769
- <button class="text-button" data-action="back-overview">返回列表</button>
770
- <div class="form-layout">
771
- <div class="panel">
772
- <div class="panel-header">
773
- <h2>上游列表</h2>
774
- <button class="button" data-action="new-upstream">新增上游</button>
775
- </div>
776
- <div class="panel-body">
777
- ${
778
- Object.keys(config.upstreams).length
779
- ? `<div class="list">${entries(config.upstreams).map(([id, item]) => upstreamRow(id, item, id === selectedId)).join("")}</div>`
780
- : `<div class="empty">暂无上游</div>`
781
- }
782
- </div>
783
- </div>
784
- <div class="panel">
785
- <div class="panel-header">
786
- <h2>${selected ? esc(selected.name || selectedId) : "添加上游"}</h2>
787
- </div>
788
- <div class="panel-body">
789
- <form id="upstream-form">
790
- <div class="form-grid">
791
- ${field("Upstream ID", "id", selectedId || "", "例如 relay-main", false, "text", Boolean(selectedId))}
792
- ${field("显示名称", "name", selected?.name || "", "例如 xxx")}
793
- <div class="field"><span>上游接入方式</span><div class="static-field">${integrationLabel}</div></div>
794
- ${selectField("上游协议", "protocol", selected?.protocol || "openai.responses", ["openai.responses", "openai.chat_completions", "anthropic.messages"])}
795
- ${field("真实 base URL", "baseUrl", selected?.baseUrl || "", "https://relay.example.com/v1", true)}
796
- ${field("API key", "secret", "", upstreamSecretConfigured(selectedId, selected) ? "已配置,留空表示不修改" : "粘贴上游 API key", false, "password")}
797
- ${field("上游认证 header", "authHeader", selected?.auth?.header || selected?.authHeader || (selected?.protocol === "anthropic.messages" ? "x-api-key" : "authorization"), "authorization 或 x-api-key")}
798
- </div>
799
- <div class="form-actions"><button class="button button-primary" type="submit">保存</button>${selected ? `<button class="button" type="button" data-action="test-upstream" data-id="${esc(selectedId)}">测试连通性</button><button class="button button-danger" type="button" data-action="delete-upstream" data-id="${esc(selectedId)}">删除</button>` : ""}</div>
800
- </form>
801
- </div>
802
- </div>
803
- </div>
804
- `;
805
- }
806
-
807
896
  function renderModels() {
808
897
  const config = state.candidate;
809
898
  const selectedId = state.selected.model;
@@ -883,21 +972,6 @@ function renderDiagnostics() {
883
972
  `;
884
973
  }
885
974
 
886
- function upstreamRow(id, item, active = false) {
887
- const health = state.runtime?.health?.[id];
888
- const healthClass = health?.outcome === "failure" ? "danger" : health ? "" : "warning";
889
- const status = health?.outcome || (
890
- upstreamSecretConfigured(id, item)
891
- ? "ready"
892
- : "needs key"
893
- );
894
- return `<button class="list-row ${active ? "is-selected" : ""}" data-action="select-upstream" data-id="${esc(id)}"><div><h3>${esc(item.name || id)}</h3><p>${esc(item.protocol || "protocol")} · ${esc(item.baseUrl || "未设置")}</p></div><span class="status-badge ${healthClass}">${status}</span></button>`;
895
- }
896
-
897
- function upstreamSecretConfigured(id, item) {
898
- return Boolean(item?.secretConfigured);
899
- }
900
-
901
975
  function modelRow(id, item, active = false) {
902
976
  const context = item.contextWindow ? `${item.contextWindow.toLocaleString()} ctx` : "context unset";
903
977
  const compact = item.compact?.tokenLimit ? `${item.compact.tokenLimit.toLocaleString()} compact` : "compact unset";
@@ -930,24 +1004,37 @@ function resolveResultPanel(result) {
930
1004
  }
931
1005
 
932
1006
  function artifactPanel(artifacts) {
933
- const title = artifacts.target === "claude-code"
934
- ? "Claude Code environment preview"
1007
+ const isClaude = artifacts.target === "claude-code";
1008
+ const title = isClaude
1009
+ ? "Claude Code settings.json 应用预览"
935
1010
  : artifacts.target === "generic-env"
936
1011
  ? "Generic environment preview"
937
1012
  : "Codex local config.toml preview";
938
- const variables = Object.keys(artifacts.environment?.vars || {}).length
1013
+ const settingsPath = artifacts.settingsPath || (isClaude ? artifacts.applied?.[0] : null);
1014
+ const instructions = isClaude
1015
+ ? ["以下内容合并到 settings.json 的 env 中,保留其他设置;原值保存以便撤销接入。"]
1016
+ : artifacts.instructions || [];
1017
+ const files = isClaude
1018
+ ? [{ path: "settings.json", contents: JSON.stringify({ env: artifacts.environment?.vars || {} }, null, 2) }]
1019
+ : artifacts.files || [];
1020
+ const variables = !isClaude && Object.keys(artifacts.environment?.vars || {}).length
939
1021
  ? `<div class="subsection"><div class="table-meta">ENVIRONMENT</div><pre class="code-preview">${esc(Object.entries(artifacts.environment.vars).map(([key, value]) => `${key}=${value}`).join("\n"))}</pre></div>`
940
1022
  : "";
941
1023
  return `
942
1024
  <div class="subsection">
943
- <div class="subsection-header"><h3>${title}</h3><span class="field-hint">${esc(artifacts.mode || "managed_proxy")}</span></div>
1025
+ <div class="subsection-header"><h3>${title}</h3>${isClaude ? "" : `<span class="field-hint">${esc(artifacts.mode || "managed_proxy")}</span>`}</div>
944
1026
  ${artifacts.catalogSummary ? `<div class="field-hint">${esc(artifacts.catalogSummary.sourceVersion)} / ${artifacts.catalogSummary.mode === "managed" ? "生成模型目录" : "沿用当前 Codex 目录"}</div>` : ""}
945
1027
  ${(artifacts.warnings || []).map((message) => `<div class="notice warning">${esc(message)}</div>`).join("")}
946
- ${(artifacts.files || []).map((file) => file.kind === "json"
1028
+ ${settingsPath ? `<p>应用位置:<code>${esc(settingsPath)}</code>,仅合并管理字段。</p>` : ""}
1029
+ ${instructions.map((message) => `<p class="field-hint">${esc(message)}</p>`).join("")}
1030
+ ${artifacts.changes?.length ? `<p class="field-hint">修改字段:${esc(artifacts.changes.join(", "))}</p>` : ""}
1031
+ ${artifacts.conflicts?.length ? `<div class="notice warning">应用已阻止:这些字段在上次应用后被手工修改:${esc(artifacts.conflicts.join(", "))}</div>` : ""}
1032
+ ${files.map((file) => file.kind === "json"
947
1033
  ? `<details class="subsection"><summary>${esc(file.path)}</summary><pre class="code-preview">${esc(file.contents)}</pre></details>`
948
1034
  : `<div class="subsection"><div class="table-meta">${esc(file.path)}</div><pre class="code-preview">${esc(file.contents)}</pre></div>`).join("")}
949
1035
  ${variables}
950
- ${artifacts.environment?.shell ? `<div class="subsection"><div class="table-meta">SETUP COMMAND</div><pre class="code-preview">${esc(artifacts.environment.shell)}</pre></div>` : ""}
1036
+ ${!isClaude && artifacts.environment?.shell ? `<div class="subsection"><div class="table-meta">SETUP COMMAND</div><pre class="code-preview">${esc(artifacts.environment.shell)}</pre></div>` : ""}
1037
+ ${!isClaude && artifacts.environment?.powershell ? `<details class="subsection"><summary>PowerShell</summary><pre class="code-preview">${esc(artifacts.environment.powershell)}</pre></details>` : ""}
951
1038
  </div>
952
1039
  `;
953
1040
  }
@@ -955,8 +1042,8 @@ function artifactPanel(artifacts) {
955
1042
  async function handleAction(action, element) {
956
1043
  if (state.busy) return;
957
1044
  if ([
958
- "create-suite", "open-suite", "back-overview", "select-upstream", "select-model",
959
- "new-upstream", "new-model", "focus-upstream", "open-advanced-models",
1045
+ "create-suite", "open-suite", "back-overview", "select-model",
1046
+ "new-model", "open-advanced-models",
960
1047
  ].includes(action)) {
961
1048
  if (!confirmPageLeave()) return;
962
1049
  }
@@ -980,6 +1067,7 @@ async function handleAction(action, element) {
980
1067
  if (!window.confirm("放弃此表单的本地修改并加载最新配置?")) return;
981
1068
  render(captureEditedForms(element.closest("form")));
982
1069
  } else if (action === "create-suite") {
1070
+ state.createTarget = "codex";
983
1071
  state.selected.suite = null;
984
1072
  state.page = "suite-create";
985
1073
  render();
@@ -990,18 +1078,10 @@ async function handleAction(action, element) {
990
1078
  } else if (action === "back-overview") {
991
1079
  state.page = "overview";
992
1080
  render();
993
- } else if (action === "select-upstream") {
994
- state.selected.upstream = element.dataset.id;
995
- state.page = "upstreams";
996
- render();
997
1081
  } else if (action === "select-model") {
998
1082
  state.selected.model = element.dataset.id;
999
1083
  state.page = "models";
1000
1084
  render();
1001
- } else if (action === "new-upstream") {
1002
- state.selected.upstream = null;
1003
- state.page = "upstreams";
1004
- render();
1005
1085
  } else if (action === "new-model") {
1006
1086
  state.selected.model = null;
1007
1087
  state.page = "models";
@@ -1014,7 +1094,7 @@ async function handleAction(action, element) {
1014
1094
  const upstreamId = suite?.upstreamId || null;
1015
1095
  wrapper.innerHTML = suiteModelEditor(suite || {}, "", {
1016
1096
  clientModelId: "",
1017
- ...(suite?.target === "codex" ? { codex: { metadataMode: "official" } } : { capabilities: ["streaming", "tools", "reasoning"] }),
1097
+ ...(suite?.target === "codex" ? { codex: { metadataMode: "official" } } : suite?.target === "claude-code" ? {} : { capabilities: ["streaming", "tools", "reasoning"] }),
1018
1098
  upstreams: {},
1019
1099
  }, upstreamId);
1020
1100
  const row = wrapper.firstElementChild;
@@ -1030,10 +1110,12 @@ async function handleAction(action, element) {
1030
1110
  const row = wrapper.firstElementChild;
1031
1111
  list.appendChild(row);
1032
1112
  } else if (action === "remove-create-model") {
1113
+ const form = element.closest("form");
1033
1114
  element.closest("[data-create-model]")?.remove();
1034
- } else if (["delete-upstream", "delete-model"].includes(action)) {
1115
+ syncClaudeCreateModels(form);
1116
+ } else if (action === "delete-model") {
1035
1117
  if (!window.confirm("删除此配置项?删除后立即生效。")) return;
1036
- await saveChanges(() => removeConfigItem(action, element.dataset.id), element.closest("form"));
1118
+ await saveChanges(() => removeModel(element.dataset.id), element.closest("form"));
1037
1119
  } else if (action === "test-upstream") {
1038
1120
  await testUpstream(element.dataset.id);
1039
1121
  } else if (action === "toggle-vp") {
@@ -1042,9 +1124,13 @@ async function handleAction(action, element) {
1042
1124
  await previewArtifacts();
1043
1125
  } else if (action === "apply-target") {
1044
1126
  await applyTarget();
1045
- } else if (action === "focus-upstream") {
1046
- state.page = "upstreams";
1047
- state.selected.upstream = element.dataset.id;
1127
+ } else if (action === "restore-target") {
1128
+ if (hasUnsavedChanges()) throw new Error("请先保存当前编辑,再撤销接入。");
1129
+ const suite = selectedSuite();
1130
+ if (suite?.target !== "claude-code") throw new Error("请选择 Claude Code 配置");
1131
+ await api("/targets/restore", { method: "POST", body: JSON.stringify({ bindingId: suite.bindingId }) });
1132
+ state.artifactPreview = null;
1133
+ toast("Claude Code 原接入配置已恢复,请重启客户端。");
1048
1134
  render();
1049
1135
  } else if (action === "open-advanced-models") {
1050
1136
  state.page = "models";
@@ -1131,9 +1217,9 @@ async function handleFormSubmit(event, form) {
1131
1217
  if (submitButton) submitButton.textContent = "保存中...";
1132
1218
  const handlers = {
1133
1219
  "suite-create-form": () => saveSuiteCreate(data, form),
1134
- "upstream-form": () => saveUpstream(data),
1135
1220
  "suite-upstream-form": () => saveSuiteUpstream(data),
1136
1221
  "suite-models-form": () => saveSuiteModels(form),
1222
+ "claude-client-form": () => saveClaudeClient(data),
1137
1223
  "model-form": () => saveModel(data, form),
1138
1224
  };
1139
1225
  if (!handlers[formId]) throw new Error("无法保存此表单,请刷新页面后重试。");
@@ -1147,32 +1233,21 @@ async function handleFormSubmit(event, form) {
1147
1233
  }
1148
1234
  }
1149
1235
 
1150
- function removeConfigItem(action, id) {
1151
- if (action === "delete-upstream") {
1152
- delete state.candidate.upstreams[id];
1153
- for (const model of values(state.candidate.models)) delete model.upstreams?.[id];
1154
- state.selected.upstream = firstKey(state.candidate.upstreams);
1155
- } else if (action === "delete-model") {
1156
- delete state.candidate.models[id];
1157
- for (const provider of values(state.candidate.virtualProviders)) {
1158
- provider.allowedModels = (provider.allowedModels || []).filter((modelId) => modelId !== id);
1159
- if (provider.defaultModel === id) delete provider.defaultModel;
1160
- }
1161
- for (const route of values(state.candidate.routes)) {
1162
- for (const backend of route.backends || []) {
1163
- backend.models = (backend.models || []).filter((modelId) => modelId !== id);
1164
- }
1165
- }
1166
- for (const binding of values(state.candidate.bindings)) {
1167
- if (binding.defaultModel === id) delete binding.defaultModel;
1236
+ function removeModel(id) {
1237
+ delete state.candidate.models[id];
1238
+ for (const provider of values(state.candidate.virtualProviders)) {
1239
+ provider.allowedModels = (provider.allowedModels || []).filter((modelId) => modelId !== id);
1240
+ if (provider.defaultModel === id) delete provider.defaultModel;
1241
+ }
1242
+ for (const route of values(state.candidate.routes)) {
1243
+ for (const backend of route.backends || []) {
1244
+ backend.models = (backend.models || []).filter((modelId) => modelId !== id);
1168
1245
  }
1169
- state.selected.model = firstKey(state.candidate.models);
1170
1246
  }
1171
- }
1172
-
1173
- function saveSuiteUpstream(data) {
1174
- saveUpstream(data, true);
1175
- state.page = "suite-detail";
1247
+ for (const binding of values(state.candidate.bindings)) {
1248
+ if (binding.defaultModel === id) delete binding.defaultModel;
1249
+ }
1250
+ state.selected.model = firstKey(state.candidate.models);
1176
1251
  }
1177
1252
 
1178
1253
  function saveSuiteModels(form) {
@@ -1230,9 +1305,11 @@ function saveSuiteModels(form) {
1230
1305
  name: clientModelId,
1231
1306
  clientModelId,
1232
1307
  aliases,
1233
- family: existing.family || "codex",
1234
- capabilities: capabilities.length ? capabilities : ["streaming", "tools", "reasoning"],
1235
- ...(suite.target === "codex" ? codexModelFields(clientModelId, existing, row) : {
1308
+ family: existing.family || (suite.target === "claude-code" ? "claude" : "codex"),
1309
+ ...(suite.target === "claude-code" ? {
1310
+ description: undefined,
1311
+ } : { capabilities: capabilities.length ? capabilities : ["streaming", "tools", "reasoning"] }),
1312
+ ...(suite.target === "codex" ? codexModelFields(clientModelId, existing, row) : suite.target === "claude-code" ? {} : {
1236
1313
  contextWindow: Number(row.querySelector("[data-suite-model-context]")?.value || 1000000),
1237
1314
  compact: {
1238
1315
  strategy: row.querySelector("[data-suite-model-compact]")?.value || "auto",
@@ -1287,15 +1364,62 @@ function saveSuiteModels(form) {
1287
1364
  defaultModel: oldModelIds.has(suite.binding.defaultModel) && !nextModelIds.has(suite.binding.defaultModel)
1288
1365
  ? undefined : suite.binding.defaultModel,
1289
1366
  };
1367
+ if (suite.target === "claude-code") {
1368
+ // Keep client-ID selections attached to their row; Claude resolves aliases itself.
1369
+ const syncSelection = (value, aliases = []) => {
1370
+ if (!value) return undefined;
1371
+ if (aliases.includes(value)) return value;
1372
+ const previous = suite.models.find(({ id, profile }) => (profile.clientModelId || profile.aliases?.[0] || id) === value);
1373
+ if (previous && nextModels[previous.id]) return nextModels[previous.id].clientModelId;
1374
+ return Object.values(nextModels).some((model) => model.clientModelId === value) ? value : undefined;
1375
+ };
1376
+ const models = { ...suite.binding.claude?.models };
1377
+ for (const family of ["opus", "sonnet", "fable", "haiku"]) {
1378
+ const value = syncSelection(models[family]);
1379
+ if (value) models[family] = value;
1380
+ else delete models[family];
1381
+ }
1382
+ const subagent = syncSelection(models.subagent, CLAUDE_MODEL_ALIASES.subagent);
1383
+ if (subagent) models.subagent = subagent;
1384
+ else delete models.subagent;
1385
+ const defaultModel = syncSelection(claudeDefaultModel(suite), CLAUDE_MODEL_ALIASES.defaultModel);
1386
+ state.candidate.virtualProviders[suite.binding.virtualProvider].defaultModel = defaultModel;
1387
+ state.candidate.bindings[suite.bindingId].defaultModel = defaultModel;
1388
+ state.candidate.bindings[suite.bindingId].claude = { ...suite.binding.claude, models, setModel: Boolean(defaultModel) };
1389
+ }
1290
1390
  selectSuite(suite.bindingId);
1291
1391
 
1292
1392
  }
1293
1393
 
1394
+ function saveClaudeClient(data) {
1395
+ const suite = selectedSuite();
1396
+ if (suite?.target !== "claude-code") throw new Error("请选择 Claude Code 配置");
1397
+ const clientIds = suite.models.map(({ id, profile }) => profile.clientModelId || profile.aliases?.[0] || id);
1398
+ const defaultModel = claudeModelChoice("defaultModel", clientIds, data.get("defaultModel"));
1399
+ const models = Object.fromEntries(["opus", "sonnet", "fable", "haiku", "subagent"].map((family) =>
1400
+ [family, String(data.get(family) || "").trim()]).filter(([, value]) => value));
1401
+ const subagent = claudeModelChoice("subagent", clientIds, models.subagent);
1402
+ if (subagent) models.subagent = subagent;
1403
+ else delete models.subagent;
1404
+ const ids = new Set(clientIds);
1405
+ for (const family of ["opus", "sonnet", "fable", "haiku"]) {
1406
+ const previous = suite.binding.claude?.models?.[family];
1407
+ if (data.get(family) == null && previous && !ids.has(previous)) throw new Error(`${family} 别名指向未配置的模型,请重新选择`);
1408
+ if (models[family] && !ids.has(models[family])) throw new Error(`${family} 别名请选择当前配置中已保存的客户端模型 ID`);
1409
+ }
1410
+ suite.binding.defaultModel = defaultModel || undefined;
1411
+ suite.virtualProvider.defaultModel = defaultModel || undefined;
1412
+ suite.binding.claude = { ...suite.binding.claude, setModel: Boolean(defaultModel), models, discoverModels: data.get("discoverModels") === "on" };
1413
+ }
1414
+
1294
1415
  function saveSuiteCreate(data, form) {
1295
- const defaults = suiteDefaults();
1296
1416
  const upstreamBaseUrl = String(data.get("upstreamBaseUrl") || "").trim();
1297
1417
  const upstreamSecret = String(data.get("upstreamSecret") || "").trim();
1298
1418
  const target = String(data.get("target") || "codex").trim();
1419
+ if (!["codex", "claude-code"].includes(target)) throw new Error("请选择 Codex 或 Claude Code");
1420
+ const defaults = suiteDefaults(target);
1421
+ const isClaude = target === "claude-code";
1422
+ const protocol = isClaude ? "anthropic.messages" : "openai.responses";
1299
1423
  const suiteName = String(data.get("suiteName") || "").trim();
1300
1424
  const modelEntries = [...form.querySelectorAll("[data-create-model]")].map((row) => {
1301
1425
  const read = (key) =>
@@ -1318,11 +1442,22 @@ function saveSuiteCreate(data, form) {
1318
1442
  if (new Set(clientModelIds).size !== clientModelIds.length) {
1319
1443
  throw new Error("Client model ID 不能重复");
1320
1444
  }
1445
+ const defaultModel = isClaude ? claudeModelChoice("defaultModel", clientModelIds, data.get("defaultModel")) : "";
1446
+ const claudeModels = isClaude ? Object.fromEntries(["opus", "sonnet", "fable", "haiku", "subagent"]
1447
+ .map((family) => [family, String(data.get(family) || "").trim()]).filter(([, value]) => value)) : {};
1448
+ const subagent = claudeModelChoice("subagent", clientModelIds, claudeModels.subagent);
1449
+ if (subagent) claudeModels.subagent = subagent;
1450
+ else delete claudeModels.subagent;
1451
+ for (const family of ["opus", "sonnet", "fable", "haiku"]) {
1452
+ if (claudeModels[family] && !clientModelIds.includes(claudeModels[family])) {
1453
+ throw new Error(`${family} 别名请选择本次填写的客户端模型 ID`);
1454
+ }
1455
+ }
1321
1456
 
1322
1457
  const upstreamId = defaults.upstreamId;
1323
1458
  const upstreamName = upstreamDisplayName(upstreamBaseUrl, upstreamId);
1324
1459
  const routeId = defaults.routeId;
1325
- const name = suiteName || `Codex / ${upstreamName}`;
1460
+ const name = suiteName || `${targetLabel(target)} / ${upstreamName}`;
1326
1461
  const bindingId = configurationId(name, defaults.bindingId);
1327
1462
  const virtualProviderId = providerIdForConfiguration(bindingId);
1328
1463
  if (Object.hasOwn(state.candidate.bindings, bindingId) || Object.hasOwn(state.candidate.virtualProviders, virtualProviderId)) {
@@ -1343,8 +1478,8 @@ function saveSuiteCreate(data, form) {
1343
1478
  name: model.clientModelId,
1344
1479
  clientModelId: model.clientModelId,
1345
1480
  aliases: [model.clientModelId],
1346
- family: "codex",
1347
- ...codexModelFields(model.clientModelId),
1481
+ family: isClaude ? "claude" : "codex",
1482
+ ...(isClaude ? {} : codexModelFields(model.clientModelId)),
1348
1483
  upstreams: {
1349
1484
  [upstreamId]: {
1350
1485
  ...(model.upstreamModelId ? { upstreamModelId: model.upstreamModelId } : {}),
@@ -1359,8 +1494,8 @@ function saveSuiteCreate(data, form) {
1359
1494
  state.candidate.upstreams[upstreamId] = {
1360
1495
  id: upstreamId,
1361
1496
  name: upstreamName,
1362
- integration: "codex-native-provider",
1363
- protocol: "openai.responses",
1497
+ ...(isClaude ? { auth: { header: data.get("upstreamAuth") || "x-api-key" } } : { integration: "codex-native-provider" }),
1498
+ protocol,
1364
1499
  baseUrl: upstreamBaseUrl,
1365
1500
  secretRef: `secret://upstreams/${upstreamId}`,
1366
1501
  enabled: true,
@@ -1380,35 +1515,40 @@ function saveSuiteCreate(data, form) {
1380
1515
  };
1381
1516
  state.candidate.virtualProviders[virtualProviderId] = {
1382
1517
  id: virtualProviderId,
1383
- name: `Codex via ${upstreamName}`,
1384
- ingressProtocol: "openai.responses",
1518
+ name: `${targetLabel(target)} via ${upstreamName}`,
1519
+ ingressProtocol: protocol,
1385
1520
  route: routeId,
1386
1521
  allowedModels: modelIds,
1522
+ ...(defaultModel ? { defaultModel } : {}),
1387
1523
  enabled: true,
1388
1524
  };
1389
1525
  state.candidate.bindings[bindingId] = {
1390
1526
  id: bindingId,
1391
1527
  name,
1392
1528
  target,
1393
- integration: "codex-native-provider",
1394
- targetFormat: "codex.config.toml.v1",
1529
+ ...(isClaude ? {} : { integration: "codex-native-provider" }),
1530
+ targetFormat: isClaude ? "claude.settings.json.v1" : "codex.config.toml.v1",
1395
1531
  mode: "config",
1396
1532
  virtualProvider: virtualProviderId,
1397
- codex: {},
1533
+ ...(isClaude ? {
1534
+ ...(defaultModel ? { defaultModel } : {}),
1535
+ claude: {
1536
+ ...(defaultModel ? { setModel: true } : {}),
1537
+ ...(Object.keys(claudeModels).length ? { models: claudeModels } : {}),
1538
+ },
1539
+ } : { codex: {} }),
1398
1540
  };
1399
1541
 
1400
1542
  state.pendingSecrets.upstreamSecrets[upstreamId] = upstreamSecret;
1401
1543
 
1402
- state.selected.upstream = upstreamId;
1403
1544
  state.selected.model = modelIds[0] || null;
1404
1545
  state.selected.virtualProvider = virtualProviderId;
1405
- state.selected.binding = bindingId;
1406
1546
  state.selected.suite = bindingId;
1407
1547
  state.page = "suite-detail";
1408
1548
 
1409
1549
  }
1410
1550
 
1411
- function saveUpstream(data, connectionOnly = false) {
1551
+ function saveSuiteUpstream(data) {
1412
1552
  const id = String(data.get("id") || "").trim();
1413
1553
  if (!id) throw new Error("upstream ID 不能为空");
1414
1554
  const existing = state.candidate.upstreams[id] || {};
@@ -1422,23 +1562,24 @@ function saveUpstream(data, connectionOnly = false) {
1422
1562
  protocol: data.get("protocol"),
1423
1563
  baseUrl: String(data.get("baseUrl") || "").trim(),
1424
1564
  secretRef: existing.secretRef || `secret://upstreams/${id}`,
1425
- ...(!connectionOnly ? {
1426
- auth: {
1427
- ...(existing.auth || {}),
1428
- header: String(data.get("authHeader") || "").trim().toLowerCase() || undefined,
1429
- },
1430
- } : {}),
1431
1565
  enabled: existing.enabled !== false,
1432
1566
  };
1433
1567
  if (data.get("protocol") !== "openai.responses") {
1434
1568
  delete state.candidate.upstreams[id].integration;
1435
1569
  }
1570
+ if (data.get("protocol") === "anthropic.messages" && data.get("authHeader")) {
1571
+ const header = String(data.get("authHeader"));
1572
+ const previous = existing.auth?.header || existing.authHeader || "x-api-key";
1573
+ if (header !== previous) {
1574
+ state.candidate.upstreams[id].auth = { header, ...(header === "authorization" ? { scheme: "Bearer" } : {}) };
1575
+ delete state.candidate.upstreams[id].authHeader;
1576
+ }
1577
+ }
1436
1578
  const secret = String(data.get("secret") || "").trim();
1437
1579
  if (secret) {
1438
1580
  state.pendingSecrets.upstreamSecrets[id] = secret;
1439
1581
  }
1440
- state.selected.upstream = id;
1441
-
1582
+ state.page = "suite-detail";
1442
1583
  }
1443
1584
 
1444
1585
  function saveModel(data, form) {
@@ -1523,12 +1664,12 @@ async function previewArtifacts() {
1523
1664
  method: "POST",
1524
1665
  body: JSON.stringify({
1525
1666
  config: state.config,
1526
- bindingId: state.selected.binding,
1667
+ bindingId: state.selected.suite,
1527
1668
  }),
1528
1669
  });
1529
1670
  state.artifactPreview = result.artifacts;
1530
1671
  const target = result.artifacts?.target;
1531
- toast(target === "codex" ? "已生成 Codex 本地 config.toml 预览。" : "已生成环境配置预览;请将这些变量注入对应客户端。");
1672
+ toast(target === "codex" ? "已生成 Codex 本地 config.toml 预览。" : target === "claude-code" ? "已生成 Claude Code 配置预览;预览不会写入文件。" : "已生成环境配置预览;请将这些变量注入对应客户端。");
1532
1673
  render(preservedForms);
1533
1674
  }
1534
1675
 
@@ -1536,23 +1677,24 @@ async function applyTarget() {
1536
1677
  if (hasUnsavedChanges()) {
1537
1678
  throw new Error("请先保存当前编辑,再应用到 CLI。");
1538
1679
  }
1539
- if (!state.selected.binding) throw new Error("请先选择一个 Target binding。");
1540
- const binding = state.candidate.bindings[state.selected.binding];
1541
- if (binding?.target !== "codex") {
1680
+ const suite = selectedSuite();
1681
+ if (!suite) throw new Error("请先选择一个配置套装。");
1682
+ const binding = suite.binding;
1683
+ if (!["codex", "claude-code"].includes(binding?.target)) {
1542
1684
  await previewArtifacts();
1543
1685
  return;
1544
1686
  }
1545
1687
  const result = await api("/targets/apply", {
1546
1688
  method: "POST",
1547
- body: JSON.stringify({ bindingId: state.selected.binding }),
1689
+ body: JSON.stringify({ bindingId: suite.bindingId }),
1548
1690
  });
1549
1691
  state.artifactPreview = result.report ? {
1550
1692
  ...result.report,
1551
1693
  target: result.target,
1552
- bindingId: state.selected.binding,
1694
+ bindingId: suite.bindingId,
1553
1695
  mode: result.report.mode || "managed_proxy",
1554
1696
  } : null;
1555
- toast("Codex 配置已应用。");
1697
+ toast(`${targetLabel(binding.target)} 配置已应用,请重启客户端。`);
1556
1698
  render();
1557
1699
  }
1558
1700
 
@@ -1567,9 +1709,7 @@ async function saveChanges(update, form) {
1567
1709
  try {
1568
1710
  update();
1569
1711
  const identities = normalizeConfigurationIdentities(state.candidate);
1570
- for (const field of ["binding", "suite"]) {
1571
- state.selected[field] = identities.bindingIds.get(state.selected[field]) ?? state.selected[field];
1572
- }
1712
+ state.selected.suite = identities.bindingIds.get(state.selected.suite) ?? state.selected.suite;
1573
1713
  state.selected.virtualProvider = identities.providerIds.get(state.selected.virtualProvider) ?? state.selected.virtualProvider;
1574
1714
  // The commit endpoint validates and applies the change atomically.
1575
1715
  const result = await api("/config/commit", {