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/README.md +94 -10
- package/package.json +6 -1
- package/src/claude-config-file.mjs +172 -0
- package/src/cli.mjs +6 -1
- package/src/codex-catalog.mjs +13 -4
- package/src/config.mjs +44 -64
- package/src/instance-lock.mjs +129 -0
- package/src/model-resolver.mjs +6 -8
- package/src/process-identity.mjs +72 -0
- package/src/server.mjs +142 -76
- package/src/target-artifacts.mjs +40 -3
- package/src/validation.mjs +26 -20
- package/src/windows-codex.ps1 +90 -0
- package/web/app.js +310 -170
- package/web/claude-models.js +39 -0
- package/web/styles.css +0 -56
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
|
-
|
|
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
|
-
:
|
|
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">→</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
|
-
|
|
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", "",
|
|
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
|
|
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
|
|
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"
|
|
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 === "
|
|
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
|
|
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",
|
|
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"
|
|
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
|
|
934
|
-
|
|
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
|
|
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
|
|
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
|
-
${
|
|
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-
|
|
959
|
-
"new-
|
|
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
|
-
|
|
1115
|
+
syncClaudeCreateModels(form);
|
|
1116
|
+
} else if (action === "delete-model") {
|
|
1035
1117
|
if (!window.confirm("删除此配置项?删除后立即生效。")) return;
|
|
1036
|
-
await saveChanges(() =>
|
|
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 === "
|
|
1046
|
-
|
|
1047
|
-
|
|
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
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
for (const
|
|
1158
|
-
|
|
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
|
-
|
|
1174
|
-
|
|
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
|
-
|
|
1235
|
-
|
|
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 ||
|
|
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
|
|
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:
|
|
1384
|
-
ingressProtocol:
|
|
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
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
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:
|
|
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:
|
|
1694
|
+
bindingId: suite.bindingId,
|
|
1553
1695
|
mode: result.report.mode || "managed_proxy",
|
|
1554
1696
|
} : null;
|
|
1555
|
-
toast(
|
|
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
|
-
|
|
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", {
|