@peterwangze/claude-trigger-router 1.0.4 → 1.0.6
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 +332 -296
- package/config/trigger.advanced.yaml +210 -0
- package/config/trigger.example.yaml +3 -190
- package/dist/cli.js +2138 -428
- package/dist/cli.js.map +4 -4
- package/package.json +74 -58
package/dist/cli.js
CHANGED
|
@@ -224,6 +224,63 @@ function inferTransformer(protocol) {
|
|
|
224
224
|
}
|
|
225
225
|
return void 0;
|
|
226
226
|
}
|
|
227
|
+
function inferCompatibilityProfile(item, modelInterface) {
|
|
228
|
+
if (modelInterface === "anthropic") {
|
|
229
|
+
return "anthropic-native";
|
|
230
|
+
}
|
|
231
|
+
return "openai-compatible-anthropic-dispatch";
|
|
232
|
+
}
|
|
233
|
+
function getDispatchFormatForProfile(modelInterface, compatibilityProfile) {
|
|
234
|
+
if (modelInterface === "anthropic") {
|
|
235
|
+
return "anthropic_messages";
|
|
236
|
+
}
|
|
237
|
+
switch (compatibilityProfile) {
|
|
238
|
+
case "openai-compatible-anthropic-dispatch":
|
|
239
|
+
return "anthropic_messages";
|
|
240
|
+
case "anthropic-native":
|
|
241
|
+
return "anthropic_messages";
|
|
242
|
+
default:
|
|
243
|
+
return "anthropic_messages";
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function describeCompatibilityProfile(profile) {
|
|
247
|
+
switch (profile) {
|
|
248
|
+
case "anthropic-native":
|
|
249
|
+
return {
|
|
250
|
+
label: "Anthropic native",
|
|
251
|
+
summary: "\u76EE\u6807\u63A5\u53E3\u539F\u751F\u63A5\u53D7 Anthropic messages \u5F62\u6001\uFF0C\u8BF7\u6C42\u65E0\u9700\u505A OpenAI-compatible \u517C\u5BB9\u8F6C\u6362\u3002"
|
|
252
|
+
};
|
|
253
|
+
case "openai-compatible-anthropic-dispatch":
|
|
254
|
+
return {
|
|
255
|
+
label: "OpenAI-compatible / Anthropic dispatch",
|
|
256
|
+
summary: "\u76EE\u6807\u63A5\u53E3\u5C5E\u4E8E OpenAI-compatible \u517C\u5BB9\u65CF\uFF0C\u8FD0\u884C\u65F6\u4F1A\u81EA\u52A8\u4F7F\u7528 Anthropic-style dispatch \u5904\u7406 tools\u3001messages \u4E0E\u63A7\u5236\u5B57\u6BB5\u5DEE\u5F02\u3002"
|
|
257
|
+
};
|
|
258
|
+
default:
|
|
259
|
+
return {
|
|
260
|
+
label: profile,
|
|
261
|
+
summary: "\u672A\u77E5\u517C\u5BB9\u753B\u50CF\u3002"
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function describeDispatchFormat(format) {
|
|
266
|
+
switch (format) {
|
|
267
|
+
case "anthropic_messages":
|
|
268
|
+
return {
|
|
269
|
+
label: "Anthropic-style messages",
|
|
270
|
+
summary: "\u8FD0\u884C\u65F6\u4F1A\u628A\u7EDF\u4E00\u8BF7\u6C42\u7F16\u8BD1\u6210 Anthropic messages \u5F62\u6001\u540E\u518D\u53D1\u5F80\u76EE\u6807\u63A5\u53E3\u3002"
|
|
271
|
+
};
|
|
272
|
+
case "openai_chat":
|
|
273
|
+
return {
|
|
274
|
+
label: "OpenAI chat completions",
|
|
275
|
+
summary: "\u8FD0\u884C\u65F6\u4F1A\u628A\u7EDF\u4E00\u8BF7\u6C42\u7F16\u8BD1\u6210 OpenAI chat completions \u5F62\u6001\u540E\u518D\u53D1\u5F80\u76EE\u6807\u63A5\u53E3\u3002"
|
|
276
|
+
};
|
|
277
|
+
default:
|
|
278
|
+
return {
|
|
279
|
+
label: format,
|
|
280
|
+
summary: "\u672A\u77E5 dispatch \u5F62\u6001\u3002"
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
}
|
|
227
284
|
function buildCompiledCapabilities(item, modelInterface) {
|
|
228
285
|
const reasoningSupported = item.metadata?.supports_reasoning !== false;
|
|
229
286
|
return {
|
|
@@ -255,12 +312,15 @@ function buildModelRegistry(config) {
|
|
|
255
312
|
const modelMap2 = config.Models.reduce((result, rawItem) => {
|
|
256
313
|
const item = normalizeModelEndpointConfig(rawItem);
|
|
257
314
|
const modelInterface = getModelInterface(item) || "openai";
|
|
315
|
+
const compatibilityProfile = inferCompatibilityProfile(item, modelInterface);
|
|
258
316
|
result[item.id] = {
|
|
259
317
|
id: item.id,
|
|
260
318
|
providerName: `model__${item.id}`,
|
|
261
319
|
modelName: item.model,
|
|
262
320
|
interface: modelInterface,
|
|
263
321
|
protocol: modelInterface,
|
|
322
|
+
compatibilityProfile,
|
|
323
|
+
dispatchFormat: getDispatchFormatForProfile(modelInterface, compatibilityProfile),
|
|
264
324
|
thinking: item.thinking,
|
|
265
325
|
capabilities: buildCompiledCapabilities(item, modelInterface),
|
|
266
326
|
source: "models"
|
|
@@ -275,12 +335,18 @@ function buildModelRegistry(config) {
|
|
|
275
335
|
const providers = config.Providers ?? [];
|
|
276
336
|
const modelMap = providers.reduce((result, provider) => {
|
|
277
337
|
for (const model of provider.models ?? []) {
|
|
338
|
+
const compatibilityProfile = inferCompatibilityProfile(
|
|
339
|
+
{ api_base_url: provider.api_base_url },
|
|
340
|
+
"openai"
|
|
341
|
+
);
|
|
278
342
|
result[`${provider.name},${model}`] = {
|
|
279
343
|
id: `${provider.name},${model}`,
|
|
280
344
|
providerName: provider.name,
|
|
281
345
|
modelName: model,
|
|
282
346
|
interface: "openai",
|
|
283
347
|
protocol: "openai",
|
|
348
|
+
compatibilityProfile,
|
|
349
|
+
dispatchFormat: getDispatchFormatForProfile("openai", compatibilityProfile),
|
|
284
350
|
capabilities: {
|
|
285
351
|
thinking: {
|
|
286
352
|
supported: true
|
|
@@ -452,6 +518,9 @@ async function initDir() {
|
|
|
452
518
|
if (!(0, import_fs.existsSync)(CONFIG_DIR)) {
|
|
453
519
|
(0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
454
520
|
}
|
|
521
|
+
if (!(0, import_fs.existsSync)(HOME_DIR)) {
|
|
522
|
+
(0, import_fs.mkdirSync)(HOME_DIR, { recursive: true });
|
|
523
|
+
}
|
|
455
524
|
}
|
|
456
525
|
async function loadYamlConfig(path) {
|
|
457
526
|
if (!(0, import_fs.existsSync)(path)) {
|
|
@@ -525,6 +594,12 @@ function validateModelRef(ref, providers, fieldName) {
|
|
|
525
594
|
}
|
|
526
595
|
return null;
|
|
527
596
|
}
|
|
597
|
+
function validateKnownModelRef(ref, config, providers, fieldName) {
|
|
598
|
+
if (isKnownModelReference(config, ref)) {
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
return validateModelRef(ref, providers, fieldName);
|
|
602
|
+
}
|
|
528
603
|
function validateConfig(config) {
|
|
529
604
|
const errors = [];
|
|
530
605
|
if (config.Models !== void 0) {
|
|
@@ -601,7 +676,7 @@ function validateConfig(config) {
|
|
|
601
676
|
];
|
|
602
677
|
for (const [ref, field] of routerModelFields) {
|
|
603
678
|
if (ref) {
|
|
604
|
-
const err =
|
|
679
|
+
const err = validateKnownModelRef(ref, config, validProviders, field);
|
|
605
680
|
if (err) errors.push(err);
|
|
606
681
|
}
|
|
607
682
|
}
|
|
@@ -611,7 +686,7 @@ function validateConfig(config) {
|
|
|
611
686
|
if (config.TriggerRouter.llm_intent_recognition && !config.TriggerRouter.intent_model) {
|
|
612
687
|
errors.push("TriggerRouter.intent_model is required when llm_intent_recognition is enabled");
|
|
613
688
|
} else if (config.TriggerRouter.intent_model && validProviders.length > 0) {
|
|
614
|
-
const err =
|
|
689
|
+
const err = validateKnownModelRef(config.TriggerRouter.intent_model, config, validProviders, "TriggerRouter.intent_model");
|
|
615
690
|
if (err) errors.push(err);
|
|
616
691
|
}
|
|
617
692
|
if (config.TriggerRouter.rules) {
|
|
@@ -622,10 +697,13 @@ function validateConfig(config) {
|
|
|
622
697
|
if (!rule.model) {
|
|
623
698
|
errors.push(`TriggerRouter.rules[${index}].model is required`);
|
|
624
699
|
} else if (validProviders.length > 0) {
|
|
625
|
-
const err =
|
|
700
|
+
const err = validateKnownModelRef(rule.model, config, validProviders, `TriggerRouter.rules[${index}].model`);
|
|
626
701
|
if (err) errors.push(err);
|
|
627
702
|
}
|
|
628
|
-
|
|
703
|
+
const hasSemanticOnlyMatch = Boolean(
|
|
704
|
+
rule.description || rule.semantic_profile?.prototype || rule.semantic_profile?.enabled
|
|
705
|
+
);
|
|
706
|
+
if ((!rule.patterns || rule.patterns.length === 0) && !hasSemanticOnlyMatch) {
|
|
629
707
|
errors.push(`TriggerRouter.rules[${index}].patterns must be a non-empty array`);
|
|
630
708
|
}
|
|
631
709
|
});
|
|
@@ -635,7 +713,7 @@ function validateConfig(config) {
|
|
|
635
713
|
if (!config.SmartRouter.router_model) {
|
|
636
714
|
errors.push("SmartRouter.router_model is required when SmartRouter is enabled");
|
|
637
715
|
} else if (validProviders.length > 0) {
|
|
638
|
-
const err =
|
|
716
|
+
const err = validateKnownModelRef(config.SmartRouter.router_model, config, validProviders, "SmartRouter.router_model");
|
|
639
717
|
if (err) errors.push(err);
|
|
640
718
|
}
|
|
641
719
|
if (!config.SmartRouter.candidates || config.SmartRouter.candidates.length < 2) {
|
|
@@ -645,7 +723,7 @@ function validateConfig(config) {
|
|
|
645
723
|
if (!candidate.model) {
|
|
646
724
|
errors.push(`SmartRouter.candidates[${index}].model is required`);
|
|
647
725
|
} else if (validProviders.length > 0) {
|
|
648
|
-
const err =
|
|
726
|
+
const err = validateKnownModelRef(candidate.model, config, validProviders, `SmartRouter.candidates[${index}].model`);
|
|
649
727
|
if (err) errors.push(err);
|
|
650
728
|
}
|
|
651
729
|
if (!candidate.description) {
|
|
@@ -772,7 +850,104 @@ function validateConfig(config) {
|
|
|
772
850
|
}
|
|
773
851
|
return errors;
|
|
774
852
|
}
|
|
853
|
+
function normalizeUnifiedRouterInput(config) {
|
|
854
|
+
const routes = config.Router?.routes;
|
|
855
|
+
const decision = config.Router?.decision;
|
|
856
|
+
const defaults = config.Router?.defaults;
|
|
857
|
+
const hasUnifiedRouterInput = Boolean(
|
|
858
|
+
Array.isArray(routes) && routes.length || decision || defaults
|
|
859
|
+
);
|
|
860
|
+
if (!hasUnifiedRouterInput) {
|
|
861
|
+
return config;
|
|
862
|
+
}
|
|
863
|
+
const nextConfig = {
|
|
864
|
+
...config,
|
|
865
|
+
Router: {
|
|
866
|
+
...config.Router ?? {}
|
|
867
|
+
}
|
|
868
|
+
};
|
|
869
|
+
if (Array.isArray(routes) && routes.length > 0) {
|
|
870
|
+
const semanticExplicitlyEnabled = defaults?.semantic?.enabled;
|
|
871
|
+
nextConfig.TriggerRouter = {
|
|
872
|
+
...config.TriggerRouter ?? DEFAULT_TRIGGER_CONFIG,
|
|
873
|
+
enabled: true,
|
|
874
|
+
rules: routes.map((route) => ({
|
|
875
|
+
name: route.name,
|
|
876
|
+
priority: route.priority ?? 0,
|
|
877
|
+
enabled: route.enabled ?? true,
|
|
878
|
+
model: route.model,
|
|
879
|
+
description: route.description,
|
|
880
|
+
semantic_profile: route.match?.semantic || route.match?.semantic_profile ? {
|
|
881
|
+
enabled: route.match?.semantic ?? true,
|
|
882
|
+
prototype: route.match?.semantic_profile?.prototype,
|
|
883
|
+
threshold: route.match?.semantic_profile?.threshold
|
|
884
|
+
} : void 0,
|
|
885
|
+
patterns: [
|
|
886
|
+
...Array.isArray(route.match?.keywords) && route.match?.keywords.length ? [{
|
|
887
|
+
type: "exact",
|
|
888
|
+
keywords: route.match?.keywords
|
|
889
|
+
}] : [],
|
|
890
|
+
...typeof route.match?.regex === "string" && route.match.regex.trim().length ? [{
|
|
891
|
+
type: "regex",
|
|
892
|
+
pattern: route.match.regex
|
|
893
|
+
}] : []
|
|
894
|
+
]
|
|
895
|
+
}))
|
|
896
|
+
};
|
|
897
|
+
const semanticPrototypes = Object.fromEntries(
|
|
898
|
+
routes.filter((route) => route.match?.semantic || route.match?.semantic_profile?.prototype || route.description).map((route) => [
|
|
899
|
+
route.name,
|
|
900
|
+
route.match?.semantic_profile?.prototype ?? route.description ?? ""
|
|
901
|
+
]).filter(([, prototype]) => typeof prototype === "string" && prototype.trim().length > 0)
|
|
902
|
+
);
|
|
903
|
+
if (Object.keys(semanticPrototypes).length > 0 || defaults?.semantic) {
|
|
904
|
+
nextConfig.Governance = {
|
|
905
|
+
...config.Governance ?? DEFAULT_GOVERNANCE_CONFIG,
|
|
906
|
+
enabled: config.Governance?.enabled ?? (semanticExplicitlyEnabled !== void 0 ? semanticExplicitlyEnabled : Object.keys(semanticPrototypes).length > 0),
|
|
907
|
+
semantic: {
|
|
908
|
+
...config.Governance?.semantic ?? {},
|
|
909
|
+
...defaults?.semantic ?? {},
|
|
910
|
+
enabled: defaults?.semantic?.enabled ?? true,
|
|
911
|
+
prototypes: {
|
|
912
|
+
...config.Governance?.semantic?.prototypes ?? {},
|
|
913
|
+
...semanticPrototypes
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
};
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (decision) {
|
|
920
|
+
nextConfig.SmartRouter = {
|
|
921
|
+
...config.SmartRouter ?? DEFAULT_SMART_ROUTER_CONFIG,
|
|
922
|
+
enabled: decision.smart_fallback ?? true,
|
|
923
|
+
router_model: decision.router_model ?? config.SmartRouter?.router_model ?? "",
|
|
924
|
+
candidates: decision.candidates ?? config.SmartRouter?.candidates ?? [],
|
|
925
|
+
cache_ttl: decision.cache_ttl ?? config.SmartRouter?.cache_ttl,
|
|
926
|
+
max_tokens: decision.max_tokens ?? config.SmartRouter?.max_tokens,
|
|
927
|
+
fallback: decision.fallback ?? config.SmartRouter?.fallback,
|
|
928
|
+
router_hint: decision.router_hint ?? config.SmartRouter?.router_hint
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
if (defaults?.sticky || defaults?.semantic) {
|
|
932
|
+
nextConfig.Governance = {
|
|
933
|
+
...nextConfig.Governance ?? config.Governance ?? DEFAULT_GOVERNANCE_CONFIG,
|
|
934
|
+
enabled: config.Governance?.enabled ?? Boolean(
|
|
935
|
+
defaults?.sticky?.enabled || defaults?.semantic?.enabled || nextConfig.Governance?.semantic?.enabled
|
|
936
|
+
),
|
|
937
|
+
sticky: defaults?.sticky ? {
|
|
938
|
+
...config.Governance?.sticky ?? {},
|
|
939
|
+
...defaults.sticky
|
|
940
|
+
} : config.Governance?.sticky,
|
|
941
|
+
semantic: defaults?.semantic ? {
|
|
942
|
+
...nextConfig.Governance?.semantic ?? config.Governance?.semantic ?? {},
|
|
943
|
+
...defaults.semantic
|
|
944
|
+
} : nextConfig.Governance?.semantic ?? config.Governance?.semantic
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
return nextConfig;
|
|
948
|
+
}
|
|
775
949
|
function normalizeAndValidateConfig(config = {}) {
|
|
950
|
+
const normalizedInput = normalizeUnifiedRouterInput(config);
|
|
776
951
|
const normalizedConfig = deepMerge(
|
|
777
952
|
{
|
|
778
953
|
...DEFAULT_CONFIG2,
|
|
@@ -782,16 +957,16 @@ function normalizeAndValidateConfig(config = {}) {
|
|
|
782
957
|
Providers: [],
|
|
783
958
|
SmartRouter: DEFAULT_SMART_ROUTER_CONFIG
|
|
784
959
|
},
|
|
785
|
-
|
|
960
|
+
normalizedInput
|
|
786
961
|
);
|
|
787
|
-
if (
|
|
788
|
-
normalizedConfig.TriggerRouter = deepMerge(DEFAULT_TRIGGER_CONFIG,
|
|
962
|
+
if (normalizedInput.TriggerRouter) {
|
|
963
|
+
normalizedConfig.TriggerRouter = deepMerge(DEFAULT_TRIGGER_CONFIG, normalizedInput.TriggerRouter);
|
|
789
964
|
}
|
|
790
|
-
if (
|
|
791
|
-
normalizedConfig.Governance = deepMerge(DEFAULT_GOVERNANCE_CONFIG,
|
|
965
|
+
if (normalizedInput.Governance) {
|
|
966
|
+
normalizedConfig.Governance = deepMerge(DEFAULT_GOVERNANCE_CONFIG, normalizedInput.Governance);
|
|
792
967
|
}
|
|
793
|
-
if (
|
|
794
|
-
normalizedConfig.Models =
|
|
968
|
+
if (normalizedInput.Models) {
|
|
969
|
+
normalizedConfig.Models = normalizedInput.Models.map((item) => normalizeModelEndpointConfig(item));
|
|
795
970
|
}
|
|
796
971
|
return {
|
|
797
972
|
config: normalizedConfig,
|
|
@@ -930,6 +1105,31 @@ async function probeServiceHealth(port, timeoutMs = 500) {
|
|
|
930
1105
|
return false;
|
|
931
1106
|
}
|
|
932
1107
|
}
|
|
1108
|
+
async function isTcpPortOccupied(port, timeoutMs = 500) {
|
|
1109
|
+
return new Promise((resolve) => {
|
|
1110
|
+
const socket = new import_net.Socket();
|
|
1111
|
+
let settled = false;
|
|
1112
|
+
const finish = (value) => {
|
|
1113
|
+
if (settled) {
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
settled = true;
|
|
1117
|
+
socket.destroy();
|
|
1118
|
+
resolve(value);
|
|
1119
|
+
};
|
|
1120
|
+
socket.setTimeout(timeoutMs);
|
|
1121
|
+
socket.once("connect", () => finish(true));
|
|
1122
|
+
socket.once("timeout", () => finish(false));
|
|
1123
|
+
socket.once("error", (error) => {
|
|
1124
|
+
if (error.code === "ECONNREFUSED") {
|
|
1125
|
+
finish(false);
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
finish(false);
|
|
1129
|
+
});
|
|
1130
|
+
socket.connect(port, "127.0.0.1");
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
933
1133
|
async function waitForService(port, timeoutMs = 5e3) {
|
|
934
1134
|
const start = Date.now();
|
|
935
1135
|
while (Date.now() - start < timeoutMs) {
|
|
@@ -940,10 +1140,11 @@ async function waitForService(port, timeoutMs = 5e3) {
|
|
|
940
1140
|
}
|
|
941
1141
|
return false;
|
|
942
1142
|
}
|
|
943
|
-
var SERVICE_NAME, SERVICE_HEALTH_PATH;
|
|
1143
|
+
var import_net, SERVICE_NAME, SERVICE_HEALTH_PATH;
|
|
944
1144
|
var init_service_health = __esm({
|
|
945
1145
|
"src/service-health.ts"() {
|
|
946
1146
|
"use strict";
|
|
1147
|
+
import_net = require("net");
|
|
947
1148
|
SERVICE_NAME = "claude-trigger-router";
|
|
948
1149
|
SERVICE_HEALTH_PATH = "/api/health";
|
|
949
1150
|
}
|
|
@@ -957,25 +1158,25 @@ var init_types = __esm({
|
|
|
957
1158
|
});
|
|
958
1159
|
|
|
959
1160
|
// src/governance/trace.ts
|
|
960
|
-
function createGovernanceTrace(
|
|
1161
|
+
function createGovernanceTrace(input3 = {}) {
|
|
961
1162
|
return {
|
|
962
|
-
requestId:
|
|
963
|
-
sessionKey:
|
|
964
|
-
initialModel:
|
|
965
|
-
finalModel:
|
|
966
|
-
routeReason:
|
|
967
|
-
stickyHit:
|
|
968
|
-
alignmentUsed:
|
|
969
|
-
semanticIntent:
|
|
970
|
-
cascadeTriggered:
|
|
971
|
-
cascadeEvidence:
|
|
972
|
-
cascadeNextModel:
|
|
973
|
-
shadowChecked:
|
|
974
|
-
verificationResult:
|
|
975
|
-
latencyMs:
|
|
976
|
-
estimatedCost:
|
|
977
|
-
startedAt:
|
|
978
|
-
completedAt:
|
|
1163
|
+
requestId: input3.requestId ?? (0, import_crypto.randomUUID)(),
|
|
1164
|
+
sessionKey: input3.sessionKey,
|
|
1165
|
+
initialModel: input3.initialModel,
|
|
1166
|
+
finalModel: input3.finalModel,
|
|
1167
|
+
routeReason: input3.routeReason ? [...input3.routeReason] : [],
|
|
1168
|
+
stickyHit: input3.stickyHit ?? false,
|
|
1169
|
+
alignmentUsed: input3.alignmentUsed ?? false,
|
|
1170
|
+
semanticIntent: input3.semanticIntent,
|
|
1171
|
+
cascadeTriggered: input3.cascadeTriggered ?? false,
|
|
1172
|
+
cascadeEvidence: input3.cascadeEvidence ? [...input3.cascadeEvidence] : [],
|
|
1173
|
+
cascadeNextModel: input3.cascadeNextModel,
|
|
1174
|
+
shadowChecked: input3.shadowChecked ?? false,
|
|
1175
|
+
verificationResult: input3.verificationResult,
|
|
1176
|
+
latencyMs: input3.latencyMs,
|
|
1177
|
+
estimatedCost: input3.estimatedCost,
|
|
1178
|
+
startedAt: input3.startedAt ?? Date.now(),
|
|
1179
|
+
completedAt: input3.completedAt
|
|
979
1180
|
};
|
|
980
1181
|
}
|
|
981
1182
|
function appendTraceReason(trace, reason) {
|
|
@@ -1290,7 +1491,13 @@ function normalizeContentParts(content) {
|
|
|
1290
1491
|
return typeof item.text === "string" ? [{ type: "text", text: item.text }] : [];
|
|
1291
1492
|
case "image":
|
|
1292
1493
|
case "image_url":
|
|
1293
|
-
return [{
|
|
1494
|
+
return [{
|
|
1495
|
+
type: "image",
|
|
1496
|
+
source: item.source ?? (item.image_url ? {
|
|
1497
|
+
...item.image_url,
|
|
1498
|
+
...item.media_type ? { media_type: item.media_type } : {}
|
|
1499
|
+
} : item.url)
|
|
1500
|
+
}];
|
|
1294
1501
|
case "tool_use":
|
|
1295
1502
|
return item.id && item.name ? [{ type: "tool_call", id: item.id, name: item.name, arguments: JSON.stringify(item.input ?? {}) }] : [];
|
|
1296
1503
|
case "tool_result":
|
|
@@ -1300,16 +1507,58 @@ function normalizeContentParts(content) {
|
|
|
1300
1507
|
}
|
|
1301
1508
|
});
|
|
1302
1509
|
}
|
|
1303
|
-
function
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1510
|
+
function normalizeOpenAIToolCalls(toolCalls) {
|
|
1511
|
+
if (!Array.isArray(toolCalls)) {
|
|
1512
|
+
return [];
|
|
1513
|
+
}
|
|
1514
|
+
return toolCalls.flatMap((toolCall) => {
|
|
1515
|
+
const id = toolCall?.id;
|
|
1516
|
+
const name = toolCall?.function?.name;
|
|
1517
|
+
const args = toolCall?.function?.arguments;
|
|
1518
|
+
if (!id || !name) {
|
|
1519
|
+
return [];
|
|
1520
|
+
}
|
|
1521
|
+
return [{
|
|
1522
|
+
type: "tool_call",
|
|
1523
|
+
id,
|
|
1524
|
+
name,
|
|
1525
|
+
arguments: typeof args === "string" ? args : JSON.stringify(args ?? {})
|
|
1526
|
+
}];
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
function createMessageIR(input3) {
|
|
1530
|
+
const system = typeof input3.system === "string" ? [input3.system] : Array.isArray(input3.system) ? input3.system.flatMap(
|
|
1531
|
+
(item) => typeof item === "string" ? [item] : item?.type === "text" && typeof item.text === "string" ? [item.text] : []
|
|
1532
|
+
) : [];
|
|
1533
|
+
const messages = Array.isArray(input3.messages) ? input3.messages.filter((item) => item?.role).flatMap((item) => {
|
|
1534
|
+
if (item.role === "system") {
|
|
1535
|
+
const systemParts = typeof item.content === "string" ? [item.content] : normalizeContentParts(item.content).filter((part) => part.type === "text").map((part) => part.text);
|
|
1536
|
+
system.push(...systemParts);
|
|
1537
|
+
return [];
|
|
1538
|
+
}
|
|
1539
|
+
if (item.role === "tool" && item.tool_call_id) {
|
|
1540
|
+
return [{
|
|
1541
|
+
role: "user",
|
|
1542
|
+
parts: [{
|
|
1543
|
+
type: "tool_result",
|
|
1544
|
+
tool_call_id: item.tool_call_id,
|
|
1545
|
+
content: item.content
|
|
1546
|
+
}]
|
|
1547
|
+
}];
|
|
1548
|
+
}
|
|
1549
|
+
const parts = [
|
|
1550
|
+
...normalizeContentParts(item.content),
|
|
1551
|
+
...item.role === "assistant" ? normalizeOpenAIToolCalls(item.tool_calls) : []
|
|
1552
|
+
];
|
|
1553
|
+
return [{
|
|
1554
|
+
role: item.role,
|
|
1555
|
+
parts
|
|
1556
|
+
}];
|
|
1557
|
+
}) : [];
|
|
1558
|
+
const thinking = input3.thinking ? {
|
|
1559
|
+
enabled: input3.thinking?.type === "enabled" || input3.thinking?.enabled === true,
|
|
1560
|
+
effort: input3.thinking?.effort,
|
|
1561
|
+
budget_tokens: input3.thinking?.budget_tokens
|
|
1313
1562
|
} : void 0;
|
|
1314
1563
|
return {
|
|
1315
1564
|
system,
|
|
@@ -1361,34 +1610,53 @@ function toAnthropicContent(parts) {
|
|
|
1361
1610
|
}
|
|
1362
1611
|
});
|
|
1363
1612
|
}
|
|
1364
|
-
function toAnthropicMessagesRequest(
|
|
1613
|
+
function toAnthropicMessagesRequest(input3) {
|
|
1365
1614
|
const body = {
|
|
1366
|
-
model:
|
|
1367
|
-
messages:
|
|
1615
|
+
model: input3.model,
|
|
1616
|
+
messages: input3.ir.messages.map((message) => ({
|
|
1368
1617
|
role: message.role,
|
|
1369
1618
|
content: toAnthropicContent(message.parts)
|
|
1370
1619
|
}))
|
|
1371
1620
|
};
|
|
1372
|
-
if (
|
|
1373
|
-
body.max_tokens =
|
|
1621
|
+
if (input3.max_tokens !== void 0) {
|
|
1622
|
+
body.max_tokens = input3.max_tokens;
|
|
1374
1623
|
}
|
|
1375
|
-
if (
|
|
1376
|
-
body.stream =
|
|
1624
|
+
if (input3.stream !== void 0) {
|
|
1625
|
+
body.stream = input3.stream;
|
|
1377
1626
|
}
|
|
1378
|
-
if (
|
|
1379
|
-
body.metadata =
|
|
1627
|
+
if (input3.metadata) {
|
|
1628
|
+
body.metadata = input3.metadata;
|
|
1629
|
+
}
|
|
1630
|
+
if (input3.tools) {
|
|
1631
|
+
body.tools = input3.tools.map((tool) => ({
|
|
1632
|
+
name: tool?.name ?? tool?.function?.name,
|
|
1633
|
+
description: tool?.description ?? tool?.function?.description,
|
|
1634
|
+
input_schema: tool?.input_schema ?? tool?.function?.parameters
|
|
1635
|
+
}));
|
|
1380
1636
|
}
|
|
1381
|
-
if (
|
|
1382
|
-
|
|
1637
|
+
if (input3.tool_choice) {
|
|
1638
|
+
if (typeof input3.tool_choice === "string") {
|
|
1639
|
+
body.tool_choice = input3.tool_choice;
|
|
1640
|
+
} else if (input3.tool_choice?.type === "tool" && input3.tool_choice?.name) {
|
|
1641
|
+
body.tool_choice = {
|
|
1642
|
+
type: "tool",
|
|
1643
|
+
name: input3.tool_choice.name
|
|
1644
|
+
};
|
|
1645
|
+
} else if (input3.tool_choice?.type === "function" && input3.tool_choice?.function?.name) {
|
|
1646
|
+
body.tool_choice = {
|
|
1647
|
+
type: "tool",
|
|
1648
|
+
name: input3.tool_choice.function.name
|
|
1649
|
+
};
|
|
1650
|
+
}
|
|
1383
1651
|
}
|
|
1384
|
-
if (
|
|
1385
|
-
body.system =
|
|
1652
|
+
if (input3.ir.system.length) {
|
|
1653
|
+
body.system = input3.ir.system.map((text) => ({ type: "text", text }));
|
|
1386
1654
|
}
|
|
1387
|
-
if (
|
|
1655
|
+
if (input3.ir.options?.thinking?.enabled) {
|
|
1388
1656
|
body.thinking = {
|
|
1389
1657
|
type: "enabled",
|
|
1390
|
-
...
|
|
1391
|
-
...
|
|
1658
|
+
...input3.ir.options.thinking.effort ? { effort: input3.ir.options.thinking.effort } : {},
|
|
1659
|
+
...input3.ir.options.thinking.budget_tokens ? { budget_tokens: input3.ir.options.thinking.budget_tokens } : {}
|
|
1392
1660
|
};
|
|
1393
1661
|
}
|
|
1394
1662
|
return body;
|
|
@@ -1713,6 +1981,37 @@ Return JSON only:
|
|
|
1713
1981
|
}
|
|
1714
1982
|
return best;
|
|
1715
1983
|
}
|
|
1984
|
+
analyzeCandidates(text, candidates, defaultThreshold = 0.85) {
|
|
1985
|
+
const inputTokens = tokenize(text);
|
|
1986
|
+
if (inputTokens.length === 0 || !candidates.length) {
|
|
1987
|
+
return null;
|
|
1988
|
+
}
|
|
1989
|
+
const inputVector = buildVector(inputTokens);
|
|
1990
|
+
let best = null;
|
|
1991
|
+
let bestThreshold = defaultThreshold;
|
|
1992
|
+
for (const candidate of candidates) {
|
|
1993
|
+
const prototypeTokens = tokenize(candidate.prototype);
|
|
1994
|
+
if (prototypeTokens.length === 0) {
|
|
1995
|
+
continue;
|
|
1996
|
+
}
|
|
1997
|
+
const prototypeVector = buildVector(prototypeTokens);
|
|
1998
|
+
const matched = prototypeTokens.filter((token) => inputTokens.includes(token));
|
|
1999
|
+
const confidence = cosineSimilarity(inputVector, prototypeVector);
|
|
2000
|
+
if (!best || confidence > best.confidence) {
|
|
2001
|
+
best = {
|
|
2002
|
+
intent: candidate.intent,
|
|
2003
|
+
confidence,
|
|
2004
|
+
matchedPrototype: candidate.prototype,
|
|
2005
|
+
evidence: matched
|
|
2006
|
+
};
|
|
2007
|
+
bestThreshold = candidate.threshold ?? defaultThreshold;
|
|
2008
|
+
}
|
|
2009
|
+
}
|
|
2010
|
+
if (!best || best.confidence < bestThreshold) {
|
|
2011
|
+
return null;
|
|
2012
|
+
}
|
|
2013
|
+
return best;
|
|
2014
|
+
}
|
|
1716
2015
|
analyze(text, config) {
|
|
1717
2016
|
return this.analyzeEmbedding(text, config);
|
|
1718
2017
|
}
|
|
@@ -2121,17 +2420,17 @@ var init_SSEParser_transform = __esm({
|
|
|
2121
2420
|
|
|
2122
2421
|
// src/governance/stream-response-governance.ts
|
|
2123
2422
|
function serializeEvent(event2) {
|
|
2124
|
-
let
|
|
2423
|
+
let output3 = "";
|
|
2125
2424
|
if (event2.event) {
|
|
2126
|
-
|
|
2425
|
+
output3 += `event: ${event2.event}
|
|
2127
2426
|
`;
|
|
2128
2427
|
}
|
|
2129
2428
|
if (event2.data !== void 0) {
|
|
2130
|
-
|
|
2429
|
+
output3 += `data: ${typeof event2.data === "string" ? event2.data : JSON.stringify(event2.data)}
|
|
2131
2430
|
`;
|
|
2132
2431
|
}
|
|
2133
|
-
|
|
2134
|
-
return new TextEncoder().encode(
|
|
2432
|
+
output3 += "\n";
|
|
2433
|
+
return new TextEncoder().encode(output3);
|
|
2135
2434
|
}
|
|
2136
2435
|
async function collectSSE(stream) {
|
|
2137
2436
|
const parser = new SSEParserTransform();
|
|
@@ -2241,17 +2540,17 @@ var init_stream_response_governance = __esm({
|
|
|
2241
2540
|
});
|
|
2242
2541
|
|
|
2243
2542
|
// src/governance/metrics.ts
|
|
2244
|
-
function normalizeAnomalyThresholds(
|
|
2543
|
+
function normalizeAnomalyThresholds(input3) {
|
|
2245
2544
|
return {
|
|
2246
|
-
minSampleSize:
|
|
2247
|
-
cascadeWarnRate:
|
|
2248
|
-
cascadeCriticalRate:
|
|
2249
|
-
shadowWarnRate:
|
|
2250
|
-
shadowCriticalRate:
|
|
2251
|
-
latencyWarnMs:
|
|
2252
|
-
latencyCriticalMs:
|
|
2253
|
-
spikeWarnRate:
|
|
2254
|
-
spikeDeltaRate:
|
|
2545
|
+
minSampleSize: input3?.minSampleSize ?? DEFAULT_ANOMALY_THRESHOLDS.minSampleSize,
|
|
2546
|
+
cascadeWarnRate: input3?.cascadeWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.cascadeWarnRate,
|
|
2547
|
+
cascadeCriticalRate: input3?.cascadeCriticalRate ?? DEFAULT_ANOMALY_THRESHOLDS.cascadeCriticalRate,
|
|
2548
|
+
shadowWarnRate: input3?.shadowWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.shadowWarnRate,
|
|
2549
|
+
shadowCriticalRate: input3?.shadowCriticalRate ?? DEFAULT_ANOMALY_THRESHOLDS.shadowCriticalRate,
|
|
2550
|
+
latencyWarnMs: input3?.latencyWarnMs ?? DEFAULT_ANOMALY_THRESHOLDS.latencyWarnMs,
|
|
2551
|
+
latencyCriticalMs: input3?.latencyCriticalMs ?? DEFAULT_ANOMALY_THRESHOLDS.latencyCriticalMs,
|
|
2552
|
+
spikeWarnRate: input3?.spikeWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.spikeWarnRate,
|
|
2553
|
+
spikeDeltaRate: input3?.spikeDeltaRate ?? DEFAULT_ANOMALY_THRESHOLDS.spikeDeltaRate
|
|
2255
2554
|
};
|
|
2256
2555
|
}
|
|
2257
2556
|
function rate(count, total) {
|
|
@@ -2956,6 +3255,8 @@ function diffCompiledRegistry(base, next) {
|
|
|
2956
3255
|
before?.providerName !== after?.providerName ? "providerName" : null,
|
|
2957
3256
|
before?.modelName !== after?.modelName ? "modelName" : null,
|
|
2958
3257
|
before?.protocol !== after?.protocol ? "protocol" : null,
|
|
3258
|
+
before?.compatibilityProfile !== after?.compatibilityProfile ? "compatibilityProfile" : null,
|
|
3259
|
+
before?.dispatchFormat !== after?.dispatchFormat ? "dispatchFormat" : null,
|
|
2959
3260
|
JSON.stringify(before?.thinking ?? {}) !== JSON.stringify(after?.thinking ?? {}) ? "thinking" : null,
|
|
2960
3261
|
JSON.stringify(before?.capabilities ?? {}) !== JSON.stringify(after?.capabilities ?? {}) ? "capabilities" : null,
|
|
2961
3262
|
before?.source !== after?.source ? "source" : null
|
|
@@ -3262,7 +3563,7 @@ var init_server = __esm({
|
|
|
3262
3563
|
server.app.post("/api/restart", async (req, reply) => {
|
|
3263
3564
|
reply.send({ success: true, message: "Service restart initiated" });
|
|
3264
3565
|
setTimeout(() => {
|
|
3265
|
-
const { spawn:
|
|
3566
|
+
const { spawn: spawn3 } = require("child_process");
|
|
3266
3567
|
const { join: join8 } = require("path");
|
|
3267
3568
|
const cliPath = join8(__dirname, "cli.js");
|
|
3268
3569
|
const currentPort = config.initialConfig?.PORT;
|
|
@@ -3270,7 +3571,7 @@ var init_server = __esm({
|
|
|
3270
3571
|
if (currentPort) {
|
|
3271
3572
|
restartArgs.push("--port", String(currentPort));
|
|
3272
3573
|
}
|
|
3273
|
-
|
|
3574
|
+
spawn3(process.execPath, restartArgs, {
|
|
3274
3575
|
detached: true,
|
|
3275
3576
|
stdio: "ignore"
|
|
3276
3577
|
}).unref();
|
|
@@ -3280,7 +3581,7 @@ var init_server = __esm({
|
|
|
3280
3581
|
server.app.get("/ui", async (_, reply) => {
|
|
3281
3582
|
reply.header("Content-Type", "text/html; charset=utf-8");
|
|
3282
3583
|
return reply.send(
|
|
3283
|
-
`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Claude Trigger Router</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:1rem;padding:.45rem 0;border-bottom:1px dashed #e5e7eb}.mini-list li:last-child{border-bottom:none}.action-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center;margin-top:.75rem}.management-table{width:100%;margin-top:.75rem}.management-table th,.management-table td{padding:.5rem;border-bottom:1px solid #e5e7eb;font-size:.92rem;vertical-align:top}.alert-list{display:grid;gap:.75rem;margin-top:1rem}.alert{border-radius:12px;padding:.85rem 1rem;border:1px solid}.alert.warn{background:#fff7ed;border-color:#fdba74;color:#9a3412}.alert.critical{background:#fef2f2;border-color:#fca5a5;color:#991b1b}.alert.info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.diff-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-top:.75rem}.diff-chip{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.75rem}.diff-chip strong{display:block;font-size:1rem;margin-top:.2rem}.models-form-grid{display:grid;gap:.75rem;margin-top:.75rem}.model-card{border:1px solid #e5e7eb;border-radius:12px;padding:1rem;background:#fcfcfd}.model-card-header{display:flex;justify-content:space-between;gap:1rem;align-items:center;margin-bottom:.75rem}.model-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.model-card-grid textarea{min-height:84px;resize:vertical}.list-editor{display:grid;gap:.75rem;margin-top:.75rem}.list-item{border:1px solid #e5e7eb;border-radius:12px;padding:.85rem;background:#fcfcfd}.list-item-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.jump-highlight{outline:3px solid #f59e0b;box-shadow:0 0 0 6px rgba(245,158,11,.15);transition:box-shadow .25s ease,outline-color .25s ease}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.control-grid label{display:block;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}.trend-table{width:100%;margin-top:.75rem}.trend-table th,.trend-table td{padding:.45rem;border-bottom:1px solid #e5e7eb;font-size:.92rem}.row{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}input,select,button{font:inherit;padding:.55rem .75rem;border-radius:8px;border:1px solid #d1d5db}button{background:#111827;color:#fff;border-color:#111827;cursor:pointer}table{width:100%;border-collapse:collapse;margin-top:1rem}th,td{text-align:left;padding:.65rem .5rem;border-bottom:1px solid #e5e7eb;vertical-align:top}code,pre{font-family:ui-monospace,SFMono-Regular,monospace}pre{white-space:pre-wrap;background:#0f172a;color:#e2e8f0;padding:1rem;border-radius:12px;overflow:auto}.pill{display:inline-block;padding:.2rem .5rem;border-radius:999px;background:#eef2ff;color:#3730a3;font-size:.8rem}</style></head><body><h2>Claude Trigger Router</h2><p class="muted">\u7B80\u6613 Governance Trace \u8C03\u8BD5\u9875\u3002\u53EF\u67E5\u770B\u6700\u8FD1\u6CBB\u7406\u94FE\u8DEF\uFF0C\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4\uFF0C\u5E76\u6309 cascade / shadow \u72B6\u6001\u7B5B\u9009\uFF1B\u6CBB\u7406 trace \u73B0\u5DF2\u652F\u6301\u672C\u5730\u6301\u4E45\u5316\uFF0C\u91CD\u542F\u540E\u53EF\u7EE7\u7EED\u67E5\u770B\u8FD1\u671F\u7A97\u53E3\u3002</p><div class="panel"><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Trigger rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u9996\u6279\u8868\u5355\u5316\u7F16\u8F91 TriggerRouter / SmartRouter / Governance \u7684\u6838\u5FC3\u5F15\u7528</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>TriggerRouter</strong><span class="muted">\u89C4\u5219\u8DEF\u7531\u4E0E\u610F\u56FE\u8BC6\u522B</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No trigger rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5BF9\u9F50\u3001\u8BED\u4E49\u3001\u5F71\u5B50\u6821\u9A8C\u4E0E\u7EA7\u8054</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div id="modelsFormGrid" class="models-form-grid"><div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div></div><textarea id="configDraftEditor" style="width:100%;min-height:240px;margin-top:.75rem;padding:.75rem;border-radius:12px;border:1px solid #d1d5db;font:12px/1.5 ui-monospace,SFMono-Regular,monospace" spellcheck="false" placeholder='{"Models":[{"id":"sonnet","api":"https://...","key":"sk-...","interface":"openai","model":"anthropic/claude-sonnet-4"}]}'></textarea><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / TriggerRouter / Governance \u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading model map...</td></tr></tbody></table></div></div></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Recent traces</span><strong>-</strong></div><div class="stat"><span class="muted">Sticky hit rate</span><strong>-</strong></div><div class="stat"><span class="muted">Cascade rate</span><strong>-</strong></div><div class="stat"><span class="muted">Shadow rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment rate</span><strong>-</strong></div><div class="stat"><span class="muted">Avg latency</span><strong>-</strong></div></div><div class="subpanel"><div class="row"><strong>Anomaly alerts</strong><span class="muted">\u68C0\u6D4B\u8FD1\u671F\u6CBB\u7406\u5F02\u5E38\u4E0E\u7A81\u589E</span></div><div id="anomalyList" class="alert-list"><div class="alert info"><strong>No alerts yet</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u6307\u6807\u52A0\u8F7D</div></div></div></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${configuredThresholds.min_sample_size ?? 3}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${configuredThresholds.cascade_warn_rate ?? 0.4}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${configuredThresholds.shadow_warn_rate ?? 0.5}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${configuredThresholds.latency_warn_ms ?? 1500}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Trend detail</strong><span class="muted">\u6BCF\u4E2A bucket \u7684\u8BE6\u7EC6\u547D\u4E2D\u7387</span></div><table id="trendTable" class="trend-table"><thead><tr><th>Bucket</th><th>Traces</th><th>Sticky</th><th>Cascade</th><th>Shadow</th><th>Alignment</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading...</td></tr></tbody></table></div></div><table id="traceTable"><thead><tr><th>Request</th><th>Session</th><th>Final Model</th><th>Reasons</th><th>Latency</th><th>Inspect</th></tr></thead><tbody><tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Trace Detail</strong><span id="detailHint" class="muted">\u70B9\u51FB\u4E0A\u8868\u4E2D\u7684 View \u67E5\u770B\u8BE6\u60C5</span></div><pre id="traceDetail">{}</pre></div><div class="panel"><div class="row"><strong>Snapshot Management</strong><span class="muted">\u67E5\u770B\u5BFC\u51FA\u5386\u53F2\u3001\u5B9A\u65F6\u4EFB\u52A1\uFF0C\u5E76\u624B\u52A8\u521B\u5EFA\u5FEB\u7167</span></div><div class="action-row"><select id="snapshotFormat"><option value="json">snapshot json</option><option value="csv">snapshot csv</option></select><button id="createSnapshotBtn" type="button">\u751F\u6210\u5FEB\u7167</button><span id="snapshotStatus" class="muted">\u5C1A\u672A\u521B\u5EFA\u5FEB\u7167</span></div><table id="exportTable" class="management-table"><thead><tr><th>Export</th><th>Kind</th><th>Format</th><th>Created</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading exports...</td></tr></tbody></table><table id="scheduleTable" class="management-table"><thead><tr><th>Schedule</th><th>Interval</th><th>Format</th><th>Last run</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading schedules...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Archive Management</strong><span class="muted">\u6D4F\u89C8\u538B\u7F29\u5F52\u6863\u5E76\u67E5\u770B\u5206\u9875\u7ED3\u679C</span></div><div class="action-row"><input id="archiveDate" placeholder="YYYY-MM-DD"><input id="archivePage" placeholder="page" value="1"><input id="archivePageSize" placeholder="pageSize" value="5"><button id="loadArchivesBtn" type="button">\u52A0\u8F7D\u5F52\u6863</button><span id="archiveStatus" class="muted">\u5C1A\u672A\u52A0\u8F7D\u5F52\u6863</span></div><table id="archiveTable" class="management-table"><thead><tr><th>Archive</th><th>Range</th><th>Count</th><th>Compressed</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading archives...</td></tr></tbody></table></div><div class="panel"><p>\u5176\u4ED6\u7BA1\u7406 API\uFF1A</p><ul><li><code>GET /api/config</code> \u2014 \u8BFB\u53D6\u5F53\u524D\u914D\u7F6E</li><li><code>GET /api/models/compiled</code> \u2014 \u67E5\u770B Models \u7F16\u8BD1\u540E\u7684\u5185\u90E8 provider / model \u6620\u5C04</li><li><code>POST /api/models/compiled/preview</code> \u2014 \u7528\u914D\u7F6E\u8349\u7A3F\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u5199\u56DE\u6587\u4EF6</li><li><code>POST /api/config</code> \u2014 \u4FDD\u5B58\u914D\u7F6E</li><li><code>GET /api/transformers</code> \u2014 \u67E5\u770B\u5DF2\u52A0\u8F7D transformer</li><li><code>POST /api/restart</code> \u2014 \u91CD\u542F\u670D\u52A1</li><li><code>GET /api/governance/archives</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5F52\u6863\u5217\u8868</li><li><code>GET /api/governance/archives/:file</code> \u2014 \u67E5\u770B\u5F52\u6863\u5185 traces</li><li><code>POST /api/governance/archives/:file/delete</code> \u2014 \u5220\u9664\u6307\u5B9A\u5F52\u6863</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const metricsGrid=document.getElementById('metricsGrid');const bucketGrid=document.getElementById('bucketGrid');const bucketHint=document.getElementById('bucketHint');const routeRanking=document.getElementById('routeRanking');const modelRanking=document.getElementById('modelRanking');const intentRanking=document.getElementById('intentRanking');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');let currentDraftConfig={};let knownModelIds=[];let activeValidationHighlight=null;const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94 TriggerRule\u3002', affects:['Router.default','TriggerRouter.enabled','TriggerRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u6838\u5FC3\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','Governance.sticky.alignment','Governance.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&','<':'<','>':'>','"':'"' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function inferProviderTemplateKey(model){ const explicit=String(model?.provider_template || '').trim(); if(explicit && modelProviderTemplates[explicit]){ return explicit; } const api=String(model?.api || model?.api_base_url || '').trim().toLowerCase(); const modelInterface=String(model?.interface || model?.protocol || '').trim().toLowerCase(); const exactMatch=Object.entries(modelProviderTemplates).find(([,item])=>String(item.api || '').trim().toLowerCase()===api && String(item.interface || '').trim().toLowerCase()===modelInterface); if(exactMatch){ return exactMatch[0]; } if(api.includes('api.anthropic.com/v1/messages') || modelInterface === 'anthropic'){ return 'anthropic'; } if(api.includes('openrouter.ai')){ return 'openrouter'; } if(api.includes('deepseek.com')){ return 'deepseek'; } if(api.includes('siliconflow.cn')){ return 'siliconflow'; } if(api.includes('api.openai.com')){ return 'openai-compatible'; } return '';}function getProviderTemplateContext(model){ const templateKey=inferProviderTemplateKey(model) || defaultProviderTemplateKey; return { templateKey, template:modelProviderTemplates[templateKey] || modelProviderTemplates[defaultProviderTemplateKey] || {} };}function createDraftModelFromTemplate(templateKey){ const resolvedKey=(templateKey && modelProviderTemplates[templateKey]) ? templateKey : defaultProviderTemplateKey; const template=modelProviderTemplates[resolvedKey] || {}; return { provider_template:resolvedKey, id:template.suggested_id || '', api:template.api || '', interface:template.interface || 'openai', model:template.default_model || '', thinking:template.default_thinking || 'auto' };}function getModelIdSuggestionsMarkup(idPrefix){ return '<datalist id="'+idPrefix+'">'+knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join('')+'</datalist>';}function resolvePresetModelId(seed){ const source=String(seed || '').trim().toLowerCase(); if(!source || !knownModelIds.length){ return seed; } if(knownModelIds.includes(seed)){ return seed; } const ranked=knownModelIds.map((modelId)=>{ const target=String(modelId || '').toLowerCase(); let score=0; if(target===source){ score+=100; } if(target.includes(source) || source.includes(target)){ score+=40; } source.split(/[^a-z0-9]+/).filter(Boolean).forEach((part)=>{ if(target.includes(part)){ score+=Math.min(part.length * 4, 24); } }); return { modelId, score }; }).filter((item)=>item.score>0).sort((a,b)=>b.score-a.score || a.modelId.localeCompare(b.modelId)); return ranked.length ? ranked[0].modelId : seed;}function getTriggerPatternValidationHint(pattern){ if((pattern?.type || 'exact') === 'regex'){ return pattern?.pattern ? { level:'ok', message:'regex pattern \u5DF2\u914D\u7F6E' } : { level:'warn', message:'regex \u6A21\u5F0F\u9700\u8981\u586B\u5199 pattern' }; } return Array.isArray(pattern?.keywords) && pattern.keywords.some((keyword)=>String(keyword || '').trim()) ? { level:'ok', message:'exact keywords \u5DF2\u914D\u7F6E' } : { level:'warn', message:'exact \u6A21\u5F0F\u81F3\u5C11\u9700\u8981\u4E00\u4E2A keyword' };}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const triggerRules=Array.isArray(config?.TriggerRouter?.rules) ? config.TriggerRouter.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(config?.SmartRouter?.candidates) ? config.SmartRouter.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, config?.TriggerRouter?.intent_model, config?.SmartRouter?.router_model, config?.Governance?.sticky?.alignment?.summarizer_model, config?.Governance?.semantic?.classifier_model, config?.Governance?.shadow?.verifier_model].filter(v=>typeof v === 'string' && v.trim()).length + triggerRules.filter(rule=>rule?.model).length + smartCandidates.filter(item=>item?.model).length + cascadeLevels.reduce((sum,level)=>sum + (level?.from ? 1 : 0) + (level?.to ? 1 : 0), 0); draftSummaryGrid.innerHTML=[ ['Models', models.length], ['Trigger rules', triggerRules.length], ['Patterns', patternCount], ['Smart candidates', smartCandidates.length], ['Cascade levels', cascadeLevels.length], ['Model refs', modelRefCount] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function renderDraftValidation(errors,warnings){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; if(!errorList.length && !warningList.length){ draftValidationList.innerHTML='<div class="alert info"><strong>No validation issues</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u672A\u53D1\u73B0\u96C6\u4E2D\u5C55\u793A\u7684\u95EE\u9898</div></div>'; return; } const extractPath=(text)=>{ const match=String(text).match(/^(Models(?:\\[[0-9]+\\])?(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Router(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|TriggerRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|SmartRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Governance(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?)/); return match ? match[1] : ''; }; const grouped=[...errorList.map(item=>({ text:String(item), severity:'error' })), ...warningList.map(item=>({ text:String(item), severity:'warning' }))].reduce((acc,item)=>{ const text=item.text; const bucket=text.startsWith('Models') ? 'Models' : text.startsWith('Router') ? 'Router' : text.startsWith('TriggerRouter') ? 'TriggerRouter' : text.startsWith('SmartRouter') ? 'SmartRouter' : text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path: extractPath(text), severity:item.severity }); return acc; }, {}); const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorList.length)+' errors / '+esc(warningList.length)+' warnings</span></div><div class="muted">'+(errorList.length ? '\u8BF7\u4F18\u5148\u4FEE\u590D errors\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u63A5\u53D7 warnings\u3002' : '\u5F53\u524D\u65E0\u963B\u65AD\u9519\u8BEF\uFF0C\u53EF\u6309\u9700\u5904\u7406 warnings\u3002')+'</div></div>'; draftValidationList.innerHTML=summary + Object.entries(grouped).map(([bucket,items])=>{ const hasError=items.some(item=>item.severity==='error'); const levelClass=hasError ? 'warn' : 'info'; const actionLabel=hasError ? 'repair first' : 'review before save'; return '<div class="alert '+levelClass+'"><div class="row"><strong>'+esc(bucket)+'</strong><span class="pill">'+esc(items.length)+' issues</span></div><div class="muted">'+esc(actionLabel)+'</div><div>'+items.slice(0,4).map(item=>'<div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+'<span class="pill">'+esc(item.severity==='error' ? 'error' : 'warning')+'</span> '+esc(item.text)+'</div>').join('')+'</div></div>'; }).join('');}function getCapabilityWarningActionLabel(code){ if(code==='thinking_ignored'){ return '\u79FB\u9664 thinking'; } if(code==='tools_text_fallback' || code==='images_text_fallback'){ return '\u6062\u590D\u9ED8\u8BA4 capability'; } return '';}function renderCapabilityWarnings(report){ const entries=Array.isArray(report?.entries) ? report.entries : []; if(!entries.length){ capabilityWarningsList.innerHTML='<div class="alert info"><strong>No capability warnings</strong><div class="muted">\u5F53\u524D compiled models \u672A\u53D1\u73B0\u9700\u8981\u989D\u5916\u63D0\u793A\u7684\u80FD\u529B\u964D\u7EA7</div></div>'; return; } const summary=report?.summary || {}; capabilityWarningsList.innerHTML='<div class="alert info"><strong>Capability warning summary</strong><div class="muted">warn '+esc(summary.warn ?? 0)+' / info '+esc(summary.info ?? 0)+' / total '+esc(summary.total ?? entries.length)+'</div></div>' + entries.map(item=>{ const actionLabel=getCapabilityWarningActionLabel(item.code); return '<div class="alert '+esc(item.level === 'warn' ? 'warn' : 'info')+'"><div class="row"><strong>'+esc(item.code || item.level || 'warning')+'</strong><span class="pill">'+esc(item.modelId || '-').trim()+'</span></div><div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+esc(item.message || '')+'</div>'+(actionLabel ? ('<div class="row" style="margin-top:.5rem"><button type="button" data-apply-warning-path="'+esc(item.path || '')+'" data-apply-warning-code="'+esc(item.code || '')+'">'+esc(actionLabel)+'</button></div>') : '')+'</div>'; }).join('');}function findValidationTarget(path){ if(!path){ return null; } if(path.startsWith('Models')){ return modelsFormGrid; } if(path === 'Router.default'){ return draftRouterDefault; } if(path.startsWith('TriggerRouter.intent_model')){ return triggerIntentModel; } if(path.startsWith('TriggerRouter.rules[')){ return triggerRulesList; } if(path.startsWith('SmartRouter.router_model')){ return smartRouterModel; } if(path.startsWith('SmartRouter.candidates[')){ return smartCandidatesList; } if(path.startsWith('Governance.cascade.levels[')){ return governanceCascadeLevelsList; } if(path.startsWith('Governance.sticky.alignment')){ return governanceSummarizerModel; } if(path.startsWith('Governance.semantic')){ return governanceClassifierModel; } if(path.startsWith('Governance.shadow')){ return governanceVerifierModel; } if(path.startsWith('Governance')){ return governanceEnabled; } return null;}function jumpToValidationPath(path){ const target=findValidationTarget(path); if(!target || typeof target.scrollIntoView !== 'function'){ return; } if(activeValidationHighlight && activeValidationHighlight.classList){ activeValidationHighlight.classList.remove('jump-highlight'); } target.scrollIntoView({ behavior:'smooth', block:'center' }); if(target.classList){ target.classList.add('jump-highlight'); activeValidationHighlight=target; setTimeout(()=>{ if(target.classList){ target.classList.remove('jump-highlight'); if(activeValidationHighlight===target){ activeValidationHighlight=null; } } }, 1800); } if(typeof target.focus === 'function'){ target.focus({ preventScroll:true }); }}function renderDraftPresetModeHint(){ const overwriteMode=draftPresetMode.value === 'replace'; draftPresetModeHint.textContent=overwriteMode ? 'overwrite \u4F1A\u91CD\u7F6E TriggerRouter / SmartRouter / Governance \u76F8\u5173\u8868\u5355\uFF0C\u518D\u5E94\u7528\u9884\u8BBE' : 'append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5';}function deriveActualAffectedAreas(preview){ const areas=new Set(); const diff=preview?.diff || {}; const impact=preview?.referenceImpact || {}; if((diff.providerChanges || []).length || (diff.modelChanges || []).length){ areas.add('Models'); } (impact.entries || []).forEach((entry)=>{ const path=String(entry.path || ''); if(path.startsWith('Router.')){ areas.add('Router'); } else if(path.startsWith('TriggerRouter.')){ areas.add('TriggerRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.')){ areas.add('Governance'); } }); return Array.from(areas);}function renderDraftPreviewMeta(meta){ if(!meta){ draftPreviewMeta.innerHTML='<div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div>'; return; } draftPreviewMeta.innerHTML='<div class="alert info"><strong>'+esc(meta.title || 'Preset dry-run')+'</strong><div>'+esc(meta.description || '')+'</div><div class="muted">\u6A21\u5F0F\uFF1A'+esc(meta.mode || '-')+' \xB7 \u9884\u8BBE\u58F0\u660E\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((meta.affects || []).join(' / ') || '-')</div><div class="muted">\u5B9E\u9645\u9884\u89C8\u547D\u4E2D\u533A\u57DF\uFF1A'+esc((meta.actualAffects || []).join(' / ') || '-')</div></div>';}function renderDraftPresetGuide(){ draftPresetList.innerHTML=Object.entries(draftPresets).map(([key,preset])=>'<div class="alert info"><strong>'+esc(preset.label || key)+'</strong><div>'+esc(preset.description || '')+'</div><div class="muted">\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((preset.affects || []).join(' / '))+'</div></div>').join('');}function updateTopLevelModelSuggestionLists(){ const markup=knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join(''); [topLevelTriggerIntentSuggestions,topLevelSmartRouterSuggestions,topLevelGovernanceSummarizerSuggestions,topLevelGovernanceClassifierSuggestions,topLevelGovernanceVerifierSuggestions].forEach(node=>{ if(node){ node.innerHTML=markup; } });}function renderModelsForm(models){ const list=Array.isArray(models) ? models : []; draftModelsCount.value=String(list.length); if(!list.length){ modelsFormGrid.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div>'; return; } modelsFormGrid.innerHTML=list.map((model,index)=>{ const templateContext=getProviderTemplateContext(model); const template=templateContext.template; return '<div class="model-card" data-model-card="'+index+'">' + '<div class="model-card-header"><strong>Model #'+(index+1)+'</strong><button type="button" data-remove-model="'+index+'">\u5220\u9664</button></div>' + '<div class="model-card-grid">' + '<div><label>Provider template</label><div class="row"><select data-field="provider_template" data-index="'+index+'"><option value="">custom</option>'+Object.entries(modelProviderTemplates).map(([key,item])=>'<option value="'+esc(key)+'"'+(model.provider_template === key ? ' selected' : '')+'>'+esc(item.label)+'</option>').join('')+'</select><button type="button" data-apply-template="'+index+'">\u5E94\u7528</button></div></div>' + '<div><label>ID</label><input data-field="id" data-index="'+index+'" value="'+esc(model.id || '')+'" placeholder="'+esc(template.suggested_id || 'sonnet')+'"><div class="muted">\u5EFA\u8BAE\u6A21\u677F\uFF1A'+esc(template.label || templateContext.templateKey || 'custom')+'</div></div>' + '<div><label>Interface</label><select data-field="interface" data-index="'+index+'"><option value="openai"'+(((model.interface || model.protocol || 'openai') === 'openai') ? ' selected' : '')+'>openai</option><option value="anthropic"'+(((model.interface || model.protocol) === 'anthropic') ? ' selected' : '')+'>anthropic</option></select></div>' + '<div><label>Model</label><input data-field="model" data-index="'+index+'" list="modelSuggestions'+index+'" value="'+esc(model.model || '')+'" placeholder="'+esc(template.default_model || 'anthropic/claude-sonnet-4')+'"><datalist id="modelSuggestions'+index+'">'+((template.model_examples || []).map(item=>'<option value="'+esc(item)+'"></option>').join(''))+'</datalist><div class="muted">\u4F8B\u5982\uFF1A'+esc((template.model_examples || ['anthropic/claude-sonnet-4']).join(' / '))+'</div></div>' + '<div><label>API</label><input data-field="api" data-index="'+index+'" value="'+esc(model.api || model.api_base_url || '')+'" placeholder="'+esc(template.api || 'https://...')+'"></div>' + '<div><label>Key</label><input data-field="key" data-index="'+index+'" value="'+esc(model.key || model.api_key || '')+'" placeholder="'+esc(template.key_placeholder || 'sk-...')+'"></div>' + '<div><label>Thinking</label><select data-field="thinking_profile" data-index="'+index+'"><option value="">default</option><option value="off"'+(((model.thinking === 'off') || model.thinking?.mode === 'off') ? ' selected' : '')+'>off</option><option value="auto"'+(((model.thinking === 'auto') || model.thinking?.mode === 'auto') ? ' selected' : '')+'>auto</option><option value="on"'+(((model.thinking === 'on') || (model.thinking?.mode === 'on' && !model.thinking?.effort)) ? ' selected' : '')+'>on</option><option value="low"'+(((model.thinking === 'low') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'low' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>low</option><option value="medium"'+(((model.thinking === 'medium') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'medium' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>medium</option><option value="high"'+(((model.thinking === 'high') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'high' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>high</option><option value="custom"'+(((typeof model.thinking === 'object') && model.thinking && model.thinking.budget_tokens) ? ' selected' : '')+'>custom</option></select></div>' + '<div><label>Thinking mode</label><select data-field="thinking_mode" data-index="'+index+'"><option value="">default</option><option value="off"'+(model.thinking?.mode === 'off' ? ' selected' : '')+'>off</option><option value="auto"'+(model.thinking?.mode === 'auto' ? ' selected' : '')+'>auto</option><option value="on"'+(model.thinking?.mode === 'on' ? ' selected' : '')+'>on</option></select></div>' + '<div><label>Thinking effort</label><select data-field="thinking_effort" data-index="'+index+'"><option value="">default</option><option value="low"'+(model.thinking?.effort === 'low' ? ' selected' : '')+'>low</option><option value="medium"'+(model.thinking?.effort === 'medium' ? ' selected' : '')+'>medium</option><option value="high"'+(model.thinking?.effort === 'high' ? ' selected' : '')+'>high</option></select></div>' + '<div><label>Thinking budget</label><input data-field="thinking_budget_tokens" data-index="'+index+'" value="'+esc(model.thinking?.budget_tokens || '')+'" placeholder="1024"></div>' + '<div><label>Vendor hint</label><input data-field="vendor_hint" data-index="'+index+'" value="'+esc(model.metadata?.vendor_hint || '')+'" placeholder="'+esc(template.vendor_hint || 'openrouter')+'"></div>' + '<div><label>Reasoning support</label><select data-field="supports_reasoning" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_reasoning === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_reasoning === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Tool support</label><select data-field="supports_tools" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_tools === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_tools === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Image support</label><select data-field="supports_images" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_images === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_images === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div style="grid-column:1/-1"><label>Metadata (advanced JSON)</label><textarea data-field="metadata" data-index="'+index+'" placeholder="{\\"label\\":\\"Balanced profile\\"}">'+esc(model.metadata ? JSON.stringify(model.metadata, null, 2) : '')+'</textarea><div class="muted">\u666E\u901A capability \u5EFA\u8BAE\u4F18\u5148\u4F7F\u7528\u4E0A\u9762\u7684\u663E\u5F0F\u5B57\u6BB5\uFF1B\u8FD9\u91CC\u4FDD\u7559\u7ED9\u9AD8\u7EA7\u6269\u5C55\u5143\u6570\u636E\u3002</div></div>' + '</div>' + '</div>'; }).join('');}function extractModelsFromForm(){ const cards=Array.from(modelsFormGrid.querySelectorAll('[data-model-card]')); return cards.map((card,index)=>{ const read=(field)=>card.querySelector('[data-field="'+field+'"][data-index="'+index+'"]'); const providerTemplate=(read('provider_template')?.value || '').trim(); const metadataRaw=(read('metadata')?.value || '').trim(); let metadata; if(metadataRaw){ metadata=JSON.parse(metadataRaw); } else { metadata={}; } const thinkingProfile=(read('thinking_profile')?.value || '').trim(); const vendorHint=(read('vendor_hint')?.value || '').trim(); const supportsReasoning=(read('supports_reasoning')?.value || '').trim(); const supportsTools=(read('supports_tools')?.value || '').trim(); const supportsImages=(read('supports_images')?.value || '').trim(); const thinking={}; const mode=(read('thinking_mode')?.value || '').trim(); const effort=(read('thinking_effort')?.value || '').trim(); const budget=(read('thinking_budget_tokens')?.value || '').trim(); if(mode) thinking.mode=mode; if(effort) thinking.effort=effort; if(budget) thinking.budget_tokens=Number(budget); const model={ id:(read('id')?.value || '').trim(), api:(read('api')?.value || '').trim(), key:(read('key')?.value || '').trim(), interface:(read('interface')?.value || '').trim(), model:(read('model')?.value || '').trim(), }; if(vendorHint){ metadata.vendor_hint=vendorHint; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'vendor_hint')){ delete metadata.vendor_hint; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No trigger rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); if(triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length){ payload.TriggerRouter={ ...(payload.TriggerRouter || {}), enabled: triggerEnabled.checked, analysis_scope: triggerAnalysisScope.value || 'last_message', llm_intent_recognition: triggerIntentEnabled.checked, intent_model: triggerIntentModel.value.trim(), rules: triggerRules }; } else { delete payload.TriggerRouter; } const smartCandidates=extractSmartCandidatesFromForm(); if(smartEnabled.checked || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim()){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: smartEnabled.checked, router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined }; } else { delete payload.SmartRouter; } const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim() || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, sticky:{ ...((payload.Governance && payload.Governance.sticky) || {}), enabled: Boolean(governanceEnabled.checked || governanceAlignmentEnabled.checked), alignment:{ ...(((payload.Governance && payload.Governance.sticky && payload.Governance.sticky.alignment) || {})), enabled: governanceAlignmentEnabled.checked, summarizer_model: governanceSummarizerModel.value.trim() } }, semantic:{ ...((payload.Governance && payload.Governance.semantic) || {}), enabled: governanceSemanticEnabled.checked, mode:'classifier', classifier_model: governanceClassifierModel.value.trim() }, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(trigger.enabled); triggerIntentEnabled.checked=Boolean(trigger.llm_intent_recognition); triggerAnalysisScope.value=trigger.analysis_scope || 'last_message'; triggerIntentModel.value=trigger.intent_model || ''; renderTriggerRulesList(trigger.rules || []); const smart=config?.SmartRouter || {}; smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(governance.sticky?.alignment?.enabled); governanceSummarizerModel.value=governance.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(governance.semantic?.enabled); governanceClassifierModel.value=governance.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u540C\u6B65 Models \u8868\u5355\u5230 JSON \u8349\u7A3F'; } catch (error) { draftPreviewStatus.textContent='\u540C\u6B65\u5931\u8D25\uFF1A'+error.message; }}function applyReferenceSuggestion(path,modelId){ if(!modelId){ return; } if(path==='Router.default'){ draftRouterDefault.value=modelId; syncDraftEditorFromForm(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 Router.default'; return; } const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const pathMatch=path.match(/^([^.[]+)(?:.(.+))?$/); if(!pathMatch){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\uFF1A'+path; return; } const tokens=path.replace(/[(d+)]/g,'.$1').split('.'); let cursor=payload; for(let i=0;i<tokens.length-1;i++){ const token=tokens[i]; const nextToken=tokens[i+1]; if(cursor[token] === undefined){ cursor[token]=String(Number(nextToken))===nextToken ? [] : {}; } cursor=cursor[token]; } cursor[tokens[tokens.length-1]]=modelId; currentDraftConfig=payload; if(payload.Router?.default){ draftRouterDefault.value=payload.Router.default; } renderConfigControlForms(payload); configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyCapabilityWarningSuggestion(path,code){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const tokens=String(path || '').replace(/[(d+)]/g,'.$1').split('.').filter(Boolean); if(!tokens.length){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } let cursor=payload; for(let i=0;i<tokens.length-1;i++){ if(cursor == null){ break; } cursor=cursor[tokens[i]]; } const lastToken=tokens[tokens.length-1]; if(code==='thinking_ignored'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } } else if(code==='tools_text_fallback' || code==='images_text_fallback'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } if(cursor && !Object.keys(cursor).length){ const parentTokens=tokens.slice(0,-1); const maybeMetadataKey=parentTokens[parentTokens.length-1]; if(maybeMetadataKey==='metadata'){ let parentCursor=payload; for(let i=0;i<parentTokens.length-1;i++){ if(parentCursor == null){ break; } parentCursor=parentCursor[parentTokens[i]]; } if(parentCursor && Object.prototype.hasOwnProperty.call(parentCursor,'metadata')){ delete parentCursor.metadata; } } } } else { draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528 warning \u4FEE\u6B63\uFF1A'+code+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function renderCompiledDiff(diff){ const summary=diff?.summary || {}; compiledDiffSummary.innerHTML=[ ['Added providers', summary.addedProviders ?? 0], ['Removed providers', summary.removedProviders ?? 0], ['Changed providers', summary.changedProviders ?? 0], ['Added models', summary.addedModels ?? 0], ['Removed models', summary.removedModels ?? 0], ['Changed models', summary.changedModels ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const rows=[ ...((diff?.providerChanges || []).map(item=>({ scope:'provider', key:item.name, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ...((diff?.modelChanges || []).map(item=>({ scope:'model', key:item.modelId, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ]; compiledDiffTableBody.innerHTML=rows.length ? rows.map(item=>'<tr>' + '<td>'+esc(item.scope)+'</td>' + '<td>'+esc(item.type)+'</td>' + '<td><code>'+esc(item.key)+'</code></td>' + '<td>'+esc(item.fields.join(', ') || '-')+'</td>' + '<td><code>'+esc(item.target.providerName || item.target.name || '-')+'</code><div class="muted">'+esc(item.target.modelName || (item.target.models || []).join(', ') || '-')}</div></td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled registry changes</td></tr>';}function renderReferenceImpact(impact){ const summary=impact?.summary || {}; referenceImpactSummary.innerHTML=[ ['Total refs', summary.total ?? 0], ['modelId refs', summary.modelIdRefs ?? 0], ['Legacy refs', summary.legacyRefs ?? 0], ['Valid modelIds', summary.validModelIds ?? 0], ['Missing modelIds', summary.missingModelIds ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const entries=impact?.entries || []; referenceImpactTableBody.innerHTML=entries.length ? entries.map(item=>'<tr>' + '<td><code>'+esc(item.path)+'</code></td>' + '<td><code>'+esc(item.value)+'</code></td>' + '<td>'+esc(item.referenceType)+'</td>' + '<td>'+esc(item.status)+'</td>' + '<td><code>'+esc(item.resolvedTarget?.providerName || '-')+'</code><div class="muted">'+esc(item.resolvedTarget?.modelName || '-')}</div></td>' + '<td>'+((item.suggestions || []).length ? item.suggestions.map(s=>'<div><code>'+esc(s.modelId)+'</code><div class="muted">'+esc(s.modelName || '-')+'</div><button type="button" data-apply-reference-path="'+esc(item.path)+'" data-apply-reference-model="'+esc(s.modelId)+'">\u5E94\u7528\u5EFA\u8BAE</button></div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No model references found</td></tr>';}function renderCompiledModels(data){ const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04'; compiledProvidersTableBody.innerHTML=providers.length ? providers.map(provider=>'<tr>' + '<td><code>'+esc(provider.name)+'</code><div class="muted">'+esc(provider.api_base_url || '-')+'</div></td>' + '<td>'+esc(provider.transformer?.use?.[0] || '-')+'</td>' + '<td>'+esc((provider.models || []).join(', ') || '-')+'</td>' + '<td>'+esc(JSON.stringify(provider.transformer || {}))+'</td>' + '<td>'+esc(provider.has_api_key ? 'configured' : 'missing')+'</td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled providers</td></tr>'; compiledModelMapTableBody.innerHTML=modelMapEntries.length ? modelMapEntries.map(([modelId,item])=>'<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td><code>'+esc(item.providerName || '-')+'</code><div class="muted">'+esc(item.modelName || '-')+'</div></td>' + '<td>'+esc(item.protocol || '-')+'</td>' + '<td><code>'+esc(JSON.stringify(item.thinking || { mode: 'off' }))+'</code></td>' + '<td><code>'+esc(JSON.stringify(item.capabilities || {}))+'</code></td>' + '<td>'+esc(item.source || '-')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No compiled model map</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || []); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || []); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addCascadeLevel(){ const next=extractCascadeLevelsFromForm(); next.push({ from:'', to:'' }); renderCascadeLevelsList(next); syncDraftEditorFromForm(); }modelsFormGrid.addEventListener('input',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('change',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-template]'); if(applyBtn){ const applyIndex=Number(applyBtn.dataset.applyTemplate); applyProviderTemplate(applyIndex); syncDraftEditorFromForm(); return; } const btn=e.target.closest('button[data-remove-model]'); if(!btn){ return; } const removeIndex=Number(btn.dataset.removeModel); const nextModels=extractModelsFromForm().filter((_,index)=>index!==removeIndex); renderModelsForm(nextModels); syncDraftEditorFromForm(); });triggerRulesList.addEventListener('input',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('change',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('click',(e)=>{ const addKeywordBtn=e.target.closest('button[data-add-trigger-keyword]'); if(addKeywordBtn){ addTriggerKeyword(Number(addKeywordBtn.dataset.addTriggerKeyword), Number(addKeywordBtn.dataset.patternIndex)); return; } const removeKeywordBtn=e.target.closest('button[data-remove-trigger-keyword]'); if(removeKeywordBtn){ const ruleIndex=Number(removeKeywordBtn.dataset.removeTriggerKeyword); const patternIndex=Number(removeKeywordBtn.dataset.patternIndex); const keywordIndex=Number(removeKeywordBtn.dataset.keywordIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex] && next[ruleIndex].patterns && next[ruleIndex].patterns[patternIndex]){ const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=(pattern.keywords || []).filter((_,index)=>index!==keywordIndex); if(!pattern.keywords.length){ pattern.keywords=['']; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const addBtn=e.target.closest('button[data-add-trigger-pattern]'); if(addBtn){ addTriggerPattern(Number(addBtn.dataset.addTriggerPattern)); return; } const removePatternBtn=e.target.closest('button[data-remove-trigger-pattern]'); if(removePatternBtn){ const ruleIndex=Number(removePatternBtn.dataset.removeTriggerPattern); const patternIndex=Number(removePatternBtn.dataset.patternIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex]){ next[ruleIndex].patterns=(next[ruleIndex].patterns || []).filter((_,index)=>index!==patternIndex); if(!next[ruleIndex].patterns.length){ next[ruleIndex].patterns=[{ type:'exact', keywords:[] }]; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const btn=e.target.closest('button[data-remove-trigger-rule]'); if(!btn){ return; } const next=extractTriggerRulesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeTriggerRule)); renderTriggerRulesList(next); syncDraftEditorFromForm(); });smartCandidatesList.addEventListener('input',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('change',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-smart-candidate]'); if(!btn){ return; } const next=extractSmartCandidatesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeSmartCandidate)); renderSmartCandidatesList(next); syncDraftEditorFromForm(); });governanceCascadeLevelsList.addEventListener('input',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('change',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-cascade-level]'); if(!btn){ return; } const next=extractCascadeLevelsFromForm().filter((_,index)=>index!==Number(btn.dataset.removeCascadeLevel)); renderCascadeLevelsList(next); syncDraftEditorFromForm(); });referenceImpactTableBody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-apply-reference-path]'); if(!btn){ return; } applyReferenceSuggestion(btn.dataset.applyReferencePath, btn.dataset.applyReferenceModel); });draftValidationList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });capabilityWarningsList.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-warning-path]'); if(applyBtn){ applyCapabilityWarningSuggestion(applyBtn.dataset.applyWarningPath, applyBtn.dataset.applyWarningCode); return; } const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });function renderMetrics(metrics){ metricsGrid.innerHTML=[ ['Recent traces', metrics.totalTraces ?? 0], ['Sticky hit rate', pct(metrics.stickyHitRate)], ['Cascade rate', pct(metrics.cascadeTriggeredRate)], ['Shadow rate', pct(metrics.shadowCheckedRate)], ['Alignment rate', pct(metrics.alignmentUsedRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.TriggerRouter={ ...(payload.TriggerRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.TriggerRouter?.enabled), analysis_scope: payload.TriggerRouter?.analysis_scope || 'last_message', llm_intent_recognition: payload.TriggerRouter?.llm_intent_recognition || false, intent_model: payload.TriggerRouter?.intent_model || '', rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.TriggerRouter?.rules || []) }; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), sticky:{ ...((payload.Governance && payload.Governance.sticky) || {}), alignment:{ ...(((payload.Governance && payload.Governance.sticky && payload.Governance.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.Governance?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.Governance?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.Governance && payload.Governance.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.Governance?.semantic?.enabled), mode:(payload.Governance?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.Governance?.semantic?.classifier_model || '') }, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || []); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderAnomalies(anomalies){ if(!anomalies || !anomalies.length){ anomalyList.innerHTML='<div class="alert info"><strong>No active alerts</strong><div class="muted">\u5F53\u524D\u7A97\u53E3\u672A\u53D1\u73B0\u660E\u663E\u6CBB\u7406\u5F02\u5E38</div></div>'; return; } anomalyList.innerHTML=anomalies.map(item=>'<div class="alert '+esc(item.severity || 'info')+'"><strong>'+esc(item.type)+'</strong><div>'+esc(item.message)+'</div></div>').join('');}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')}</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); renderMetrics(metricsData.metrics || {}); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || []); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderTrendTable(metricsData || {}); const traces=data.traces || []; if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`
|
|
3584
|
+
`<!DOCTYPE html><html><head><meta charset="utf-8"><title>Claude Trigger Router</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:1rem;padding:.45rem 0;border-bottom:1px dashed #e5e7eb}.mini-list li:last-child{border-bottom:none}.action-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center;margin-top:.75rem}.management-table{width:100%;margin-top:.75rem}.management-table th,.management-table td{padding:.5rem;border-bottom:1px solid #e5e7eb;font-size:.92rem;vertical-align:top}.alert-list{display:grid;gap:.75rem;margin-top:1rem}.alert{border-radius:12px;padding:.85rem 1rem;border:1px solid}.alert.warn{background:#fff7ed;border-color:#fdba74;color:#9a3412}.alert.critical{background:#fef2f2;border-color:#fca5a5;color:#991b1b}.alert.info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.diff-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-top:.75rem}.diff-chip{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.75rem}.diff-chip strong{display:block;font-size:1rem;margin-top:.2rem}.models-form-grid{display:grid;gap:.75rem;margin-top:.75rem}.model-card{border:1px solid #e5e7eb;border-radius:12px;padding:1rem;background:#fcfcfd}.model-card-header{display:flex;justify-content:space-between;gap:1rem;align-items:center;margin-bottom:.75rem}.model-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.model-card-grid textarea{min-height:84px;resize:vertical}.list-editor{display:grid;gap:.75rem;margin-top:.75rem}.list-item{border:1px solid #e5e7eb;border-radius:12px;padding:.85rem;background:#fcfcfd}.list-item-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.jump-highlight{outline:3px solid #f59e0b;box-shadow:0 0 0 6px rgba(245,158,11,.15);transition:box-shadow .25s ease,outline-color .25s ease}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.control-grid label{display:block;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}.trend-table{width:100%;margin-top:.75rem}.trend-table th,.trend-table td{padding:.45rem;border-bottom:1px solid #e5e7eb;font-size:.92rem}.row{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}input,select,button{font:inherit;padding:.55rem .75rem;border-radius:8px;border:1px solid #d1d5db}button{background:#111827;color:#fff;border-color:#111827;cursor:pointer}table{width:100%;border-collapse:collapse;margin-top:1rem}th,td{text-align:left;padding:.65rem .5rem;border-bottom:1px solid #e5e7eb;vertical-align:top}code,pre{font-family:ui-monospace,SFMono-Regular,monospace}pre{white-space:pre-wrap;background:#0f172a;color:#e2e8f0;padding:1rem;border-radius:12px;overflow:auto}.pill{display:inline-block;padding:.2rem .5rem;border-radius:999px;background:#eef2ff;color:#3730a3;font-size:.8rem}</style></head><body><h2>Claude Trigger Router</h2><p class="muted">\u7B80\u6613 Governance Trace \u8C03\u8BD5\u9875\u3002\u53EF\u67E5\u770B\u6700\u8FD1\u6CBB\u7406\u94FE\u8DEF\uFF0C\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4\uFF0C\u5E76\u6309 cascade / shadow \u72B6\u6001\u7B5B\u9009\uFF1B\u6CBB\u7406 trace \u73B0\u5DF2\u652F\u6301\u672C\u5730\u6301\u4E45\u5316\uFF0C\u91CD\u542F\u540E\u53EF\u7EE7\u7EED\u67E5\u770B\u8FD1\u671F\u7A97\u53E3\u3002</p><div class="panel"><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Trigger rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u9996\u6279\u8868\u5355\u5316\u7F16\u8F91 TriggerRouter / SmartRouter / Governance \u7684\u6838\u5FC3\u5F15\u7528</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>TriggerRouter</strong><span class="muted">\u89C4\u5219\u8DEF\u7531\u4E0E\u610F\u56FE\u8BC6\u522B</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No trigger rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5BF9\u9F50\u3001\u8BED\u4E49\u3001\u5F71\u5B50\u6821\u9A8C\u4E0E\u7EA7\u8054</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div id="modelsFormGrid" class="models-form-grid"><div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div></div><textarea id="configDraftEditor" style="width:100%;min-height:240px;margin-top:.75rem;padding:.75rem;border-radius:12px;border:1px solid #d1d5db;font:12px/1.5 ui-monospace,SFMono-Regular,monospace" spellcheck="false" placeholder='{"Models":[{"id":"sonnet","api":"https://...","key":"sk-...","interface":"openai","model":"anthropic/claude-sonnet-4"}]}'></textarea><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / TriggerRouter / Governance \u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Compatibility profile</th><th>Dispatch format</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="8" class="muted">Loading model map...</td></tr></tbody></table></div></div></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Recent traces</span><strong>-</strong></div><div class="stat"><span class="muted">Sticky hit rate</span><strong>-</strong></div><div class="stat"><span class="muted">Cascade rate</span><strong>-</strong></div><div class="stat"><span class="muted">Shadow rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment rate</span><strong>-</strong></div><div class="stat"><span class="muted">Avg latency</span><strong>-</strong></div></div><div class="subpanel"><div class="row"><strong>Anomaly alerts</strong><span class="muted">\u68C0\u6D4B\u8FD1\u671F\u6CBB\u7406\u5F02\u5E38\u4E0E\u7A81\u589E</span></div><div id="anomalyList" class="alert-list"><div class="alert info"><strong>No alerts yet</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u6307\u6807\u52A0\u8F7D</div></div></div></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${configuredThresholds.min_sample_size ?? 3}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${configuredThresholds.cascade_warn_rate ?? 0.4}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${configuredThresholds.shadow_warn_rate ?? 0.5}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${configuredThresholds.latency_warn_ms ?? 1500}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Trend detail</strong><span class="muted">\u6BCF\u4E2A bucket \u7684\u8BE6\u7EC6\u547D\u4E2D\u7387</span></div><table id="trendTable" class="trend-table"><thead><tr><th>Bucket</th><th>Traces</th><th>Sticky</th><th>Cascade</th><th>Shadow</th><th>Alignment</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading...</td></tr></tbody></table></div></div><table id="traceTable"><thead><tr><th>Request</th><th>Session</th><th>Final Model</th><th>Reasons</th><th>Latency</th><th>Inspect</th></tr></thead><tbody><tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Trace Detail</strong><span id="detailHint" class="muted">\u70B9\u51FB\u4E0A\u8868\u4E2D\u7684 View \u67E5\u770B\u8BE6\u60C5</span></div><pre id="traceDetail">{}</pre></div><div class="panel"><div class="row"><strong>Snapshot Management</strong><span class="muted">\u67E5\u770B\u5BFC\u51FA\u5386\u53F2\u3001\u5B9A\u65F6\u4EFB\u52A1\uFF0C\u5E76\u624B\u52A8\u521B\u5EFA\u5FEB\u7167</span></div><div class="action-row"><select id="snapshotFormat"><option value="json">snapshot json</option><option value="csv">snapshot csv</option></select><button id="createSnapshotBtn" type="button">\u751F\u6210\u5FEB\u7167</button><span id="snapshotStatus" class="muted">\u5C1A\u672A\u521B\u5EFA\u5FEB\u7167</span></div><table id="exportTable" class="management-table"><thead><tr><th>Export</th><th>Kind</th><th>Format</th><th>Created</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading exports...</td></tr></tbody></table><table id="scheduleTable" class="management-table"><thead><tr><th>Schedule</th><th>Interval</th><th>Format</th><th>Last run</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading schedules...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Archive Management</strong><span class="muted">\u6D4F\u89C8\u538B\u7F29\u5F52\u6863\u5E76\u67E5\u770B\u5206\u9875\u7ED3\u679C</span></div><div class="action-row"><input id="archiveDate" placeholder="YYYY-MM-DD"><input id="archivePage" placeholder="page" value="1"><input id="archivePageSize" placeholder="pageSize" value="5"><button id="loadArchivesBtn" type="button">\u52A0\u8F7D\u5F52\u6863</button><span id="archiveStatus" class="muted">\u5C1A\u672A\u52A0\u8F7D\u5F52\u6863</span></div><table id="archiveTable" class="management-table"><thead><tr><th>Archive</th><th>Range</th><th>Count</th><th>Compressed</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading archives...</td></tr></tbody></table></div><div class="panel"><p>\u5176\u4ED6\u7BA1\u7406 API\uFF1A</p><ul><li><code>GET /api/config</code> \u2014 \u8BFB\u53D6\u5F53\u524D\u914D\u7F6E</li><li><code>GET /api/models/compiled</code> \u2014 \u67E5\u770B Models \u7F16\u8BD1\u540E\u7684\u5185\u90E8 provider / model \u6620\u5C04</li><li><code>POST /api/models/compiled/preview</code> \u2014 \u7528\u914D\u7F6E\u8349\u7A3F\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u5199\u56DE\u6587\u4EF6</li><li><code>POST /api/config</code> \u2014 \u4FDD\u5B58\u914D\u7F6E</li><li><code>GET /api/transformers</code> \u2014 \u67E5\u770B\u5DF2\u52A0\u8F7D transformer</li><li><code>POST /api/restart</code> \u2014 \u91CD\u542F\u670D\u52A1</li><li><code>GET /api/governance/archives</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5F52\u6863\u5217\u8868</li><li><code>GET /api/governance/archives/:file</code> \u2014 \u67E5\u770B\u5F52\u6863\u5185 traces</li><li><code>POST /api/governance/archives/:file/delete</code> \u2014 \u5220\u9664\u6307\u5B9A\u5F52\u6863</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const metricsGrid=document.getElementById('metricsGrid');const bucketGrid=document.getElementById('bucketGrid');const bucketHint=document.getElementById('bucketHint');const routeRanking=document.getElementById('routeRanking');const modelRanking=document.getElementById('modelRanking');const intentRanking=document.getElementById('intentRanking');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');let currentDraftConfig={};let knownModelIds=[];let activeValidationHighlight=null;const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94 TriggerRule\u3002', affects:['Router.default','TriggerRouter.enabled','TriggerRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u6838\u5FC3\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','Governance.sticky.alignment','Governance.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&','<':'<','>':'>','"':'"' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function inferProviderTemplateKey(model){ const explicit=String(model?.provider_template || '').trim(); if(explicit && modelProviderTemplates[explicit]){ return explicit; } const api=String(model?.api || model?.api_base_url || '').trim().toLowerCase(); const modelInterface=String(model?.interface || model?.protocol || '').trim().toLowerCase(); const exactMatch=Object.entries(modelProviderTemplates).find(([,item])=>String(item.api || '').trim().toLowerCase()===api && String(item.interface || '').trim().toLowerCase()===modelInterface); if(exactMatch){ return exactMatch[0]; } if(api.includes('api.anthropic.com/v1/messages') || modelInterface === 'anthropic'){ return 'anthropic'; } if(api.includes('openrouter.ai')){ return 'openrouter'; } if(api.includes('deepseek.com')){ return 'deepseek'; } if(api.includes('siliconflow.cn')){ return 'siliconflow'; } if(api.includes('api.openai.com')){ return 'openai-compatible'; } return '';}function getProviderTemplateContext(model){ const templateKey=inferProviderTemplateKey(model) || defaultProviderTemplateKey; return { templateKey, template:modelProviderTemplates[templateKey] || modelProviderTemplates[defaultProviderTemplateKey] || {} };}function createDraftModelFromTemplate(templateKey){ const resolvedKey=(templateKey && modelProviderTemplates[templateKey]) ? templateKey : defaultProviderTemplateKey; const template=modelProviderTemplates[resolvedKey] || {}; return { provider_template:resolvedKey, id:template.suggested_id || '', api:template.api || '', interface:template.interface || 'openai', model:template.default_model || '', thinking:template.default_thinking || 'auto' };}function getModelIdSuggestionsMarkup(idPrefix){ return '<datalist id="'+idPrefix+'">'+knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join('')+'</datalist>';}function resolvePresetModelId(seed){ const source=String(seed || '').trim().toLowerCase(); if(!source || !knownModelIds.length){ return seed; } if(knownModelIds.includes(seed)){ return seed; } const ranked=knownModelIds.map((modelId)=>{ const target=String(modelId || '').toLowerCase(); let score=0; if(target===source){ score+=100; } if(target.includes(source) || source.includes(target)){ score+=40; } source.split(/[^a-z0-9]+/).filter(Boolean).forEach((part)=>{ if(target.includes(part)){ score+=Math.min(part.length * 4, 24); } }); return { modelId, score }; }).filter((item)=>item.score>0).sort((a,b)=>b.score-a.score || a.modelId.localeCompare(b.modelId)); return ranked.length ? ranked[0].modelId : seed;}function getTriggerPatternValidationHint(pattern){ if((pattern?.type || 'exact') === 'regex'){ return pattern?.pattern ? { level:'ok', message:'regex pattern \u5DF2\u914D\u7F6E' } : { level:'warn', message:'regex \u6A21\u5F0F\u9700\u8981\u586B\u5199 pattern' }; } return Array.isArray(pattern?.keywords) && pattern.keywords.some((keyword)=>String(keyword || '').trim()) ? { level:'ok', message:'exact keywords \u5DF2\u914D\u7F6E' } : { level:'warn', message:'exact \u6A21\u5F0F\u81F3\u5C11\u9700\u8981\u4E00\u4E2A keyword' };}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const triggerRules=Array.isArray(config?.TriggerRouter?.rules) ? config.TriggerRouter.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(config?.SmartRouter?.candidates) ? config.SmartRouter.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, config?.TriggerRouter?.intent_model, config?.SmartRouter?.router_model, config?.Governance?.sticky?.alignment?.summarizer_model, config?.Governance?.semantic?.classifier_model, config?.Governance?.shadow?.verifier_model].filter(v=>typeof v === 'string' && v.trim()).length + triggerRules.filter(rule=>rule?.model).length + smartCandidates.filter(item=>item?.model).length + cascadeLevels.reduce((sum,level)=>sum + (level?.from ? 1 : 0) + (level?.to ? 1 : 0), 0); draftSummaryGrid.innerHTML=[ ['Models', models.length], ['Trigger rules', triggerRules.length], ['Patterns', patternCount], ['Smart candidates', smartCandidates.length], ['Cascade levels', cascadeLevels.length], ['Model refs', modelRefCount] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function renderDraftValidation(errors,warnings){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; if(!errorList.length && !warningList.length){ draftValidationList.innerHTML='<div class="alert info"><strong>No validation issues</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u672A\u53D1\u73B0\u96C6\u4E2D\u5C55\u793A\u7684\u95EE\u9898</div></div>'; return; } const extractPath=(text)=>{ const match=String(text).match(/^(Models(?:\\[[0-9]+\\])?(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Router(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|TriggerRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|SmartRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Governance(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?)/); return match ? match[1] : ''; }; const grouped=[...errorList.map(item=>({ text:String(item), severity:'error' })), ...warningList.map(item=>({ text:String(item), severity:'warning' }))].reduce((acc,item)=>{ const text=item.text; const bucket=text.startsWith('Models') ? 'Models' : text.startsWith('Router') ? 'Router' : text.startsWith('TriggerRouter') ? 'TriggerRouter' : text.startsWith('SmartRouter') ? 'SmartRouter' : text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path: extractPath(text), severity:item.severity }); return acc; }, {}); const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorList.length)+' errors / '+esc(warningList.length)+' warnings</span></div><div class="muted">'+(errorList.length ? '\u8BF7\u4F18\u5148\u4FEE\u590D errors\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u63A5\u53D7 warnings\u3002' : '\u5F53\u524D\u65E0\u963B\u65AD\u9519\u8BEF\uFF0C\u53EF\u6309\u9700\u5904\u7406 warnings\u3002')+'</div></div>'; draftValidationList.innerHTML=summary + Object.entries(grouped).map(([bucket,items])=>{ const hasError=items.some(item=>item.severity==='error'); const levelClass=hasError ? 'warn' : 'info'; const actionLabel=hasError ? 'repair first' : 'review before save'; return '<div class="alert '+levelClass+'"><div class="row"><strong>'+esc(bucket)+'</strong><span class="pill">'+esc(items.length)+' issues</span></div><div class="muted">'+esc(actionLabel)+'</div><div>'+items.slice(0,4).map(item=>'<div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+'<span class="pill">'+esc(item.severity==='error' ? 'error' : 'warning')+'</span> '+esc(item.text)+'</div>').join('')+'</div></div>'; }).join('');}function getCapabilityWarningActionLabel(code){ if(code==='thinking_ignored'){ return '\u79FB\u9664 thinking'; } if(code==='tools_text_fallback' || code==='images_text_fallback'){ return '\u6062\u590D\u9ED8\u8BA4 capability'; } return '';}function renderCapabilityWarnings(report){ const entries=Array.isArray(report?.entries) ? report.entries : []; if(!entries.length){ capabilityWarningsList.innerHTML='<div class="alert info"><strong>No capability warnings</strong><div class="muted">\u5F53\u524D compiled models \u672A\u53D1\u73B0\u9700\u8981\u989D\u5916\u63D0\u793A\u7684\u80FD\u529B\u964D\u7EA7</div></div>'; return; } const summary=report?.summary || {}; capabilityWarningsList.innerHTML='<div class="alert info"><strong>Capability warning summary</strong><div class="muted">warn '+esc(summary.warn ?? 0)+' / info '+esc(summary.info ?? 0)+' / total '+esc(summary.total ?? entries.length)+'</div></div>' + entries.map(item=>{ const actionLabel=getCapabilityWarningActionLabel(item.code); return '<div class="alert '+esc(item.level === 'warn' ? 'warn' : 'info')+'"><div class="row"><strong>'+esc(item.code || item.level || 'warning')+'</strong><span class="pill">'+esc(item.modelId || '-').trim()+'</span></div><div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+esc(item.message || '')+'</div>'+(actionLabel ? ('<div class="row" style="margin-top:.5rem"><button type="button" data-apply-warning-path="'+esc(item.path || '')+'" data-apply-warning-code="'+esc(item.code || '')+'">'+esc(actionLabel)+'</button></div>') : '')+'</div>'; }).join('');}function findValidationTarget(path){ if(!path){ return null; } if(path.startsWith('Models')){ return modelsFormGrid; } if(path === 'Router.default'){ return draftRouterDefault; } if(path.startsWith('TriggerRouter.intent_model')){ return triggerIntentModel; } if(path.startsWith('TriggerRouter.rules[')){ return triggerRulesList; } if(path.startsWith('SmartRouter.router_model')){ return smartRouterModel; } if(path.startsWith('SmartRouter.candidates[')){ return smartCandidatesList; } if(path.startsWith('Governance.cascade.levels[')){ return governanceCascadeLevelsList; } if(path.startsWith('Governance.sticky.alignment')){ return governanceSummarizerModel; } if(path.startsWith('Governance.semantic')){ return governanceClassifierModel; } if(path.startsWith('Governance.shadow')){ return governanceVerifierModel; } if(path.startsWith('Governance')){ return governanceEnabled; } return null;}function jumpToValidationPath(path){ const target=findValidationTarget(path); if(!target || typeof target.scrollIntoView !== 'function'){ return; } if(activeValidationHighlight && activeValidationHighlight.classList){ activeValidationHighlight.classList.remove('jump-highlight'); } target.scrollIntoView({ behavior:'smooth', block:'center' }); if(target.classList){ target.classList.add('jump-highlight'); activeValidationHighlight=target; setTimeout(()=>{ if(target.classList){ target.classList.remove('jump-highlight'); if(activeValidationHighlight===target){ activeValidationHighlight=null; } } }, 1800); } if(typeof target.focus === 'function'){ target.focus({ preventScroll:true }); }}function renderDraftPresetModeHint(){ const overwriteMode=draftPresetMode.value === 'replace'; draftPresetModeHint.textContent=overwriteMode ? 'overwrite \u4F1A\u91CD\u7F6E TriggerRouter / SmartRouter / Governance \u76F8\u5173\u8868\u5355\uFF0C\u518D\u5E94\u7528\u9884\u8BBE' : 'append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5';}function deriveActualAffectedAreas(preview){ const areas=new Set(); const diff=preview?.diff || {}; const impact=preview?.referenceImpact || {}; if((diff.providerChanges || []).length || (diff.modelChanges || []).length){ areas.add('Models'); } (impact.entries || []).forEach((entry)=>{ const path=String(entry.path || ''); if(path.startsWith('Router.')){ areas.add('Router'); } else if(path.startsWith('TriggerRouter.')){ areas.add('TriggerRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.')){ areas.add('Governance'); } }); return Array.from(areas);}function renderDraftPreviewMeta(meta){ if(!meta){ draftPreviewMeta.innerHTML='<div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div>'; return; } draftPreviewMeta.innerHTML='<div class="alert info"><strong>'+esc(meta.title || 'Preset dry-run')+'</strong><div>'+esc(meta.description || '')+'</div><div class="muted">\u6A21\u5F0F\uFF1A'+esc(meta.mode || '-')+' \xB7 \u9884\u8BBE\u58F0\u660E\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((meta.affects || []).join(' / ') || '-')</div><div class="muted">\u5B9E\u9645\u9884\u89C8\u547D\u4E2D\u533A\u57DF\uFF1A'+esc((meta.actualAffects || []).join(' / ') || '-')</div></div>';}function renderDraftPresetGuide(){ draftPresetList.innerHTML=Object.entries(draftPresets).map(([key,preset])=>'<div class="alert info"><strong>'+esc(preset.label || key)+'</strong><div>'+esc(preset.description || '')+'</div><div class="muted">\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((preset.affects || []).join(' / '))+'</div></div>').join('');}function updateTopLevelModelSuggestionLists(){ const markup=knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join(''); [topLevelTriggerIntentSuggestions,topLevelSmartRouterSuggestions,topLevelGovernanceSummarizerSuggestions,topLevelGovernanceClassifierSuggestions,topLevelGovernanceVerifierSuggestions].forEach(node=>{ if(node){ node.innerHTML=markup; } });}function renderModelsForm(models){ const list=Array.isArray(models) ? models : []; draftModelsCount.value=String(list.length); if(!list.length){ modelsFormGrid.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div>'; return; } modelsFormGrid.innerHTML=list.map((model,index)=>{ const templateContext=getProviderTemplateContext(model); const template=templateContext.template; return '<div class="model-card" data-model-card="'+index+'">' + '<div class="model-card-header"><strong>Model #'+(index+1)+'</strong><button type="button" data-remove-model="'+index+'">\u5220\u9664</button></div>' + '<div class="model-card-grid">' + '<div><label>Provider template</label><div class="row"><select data-field="provider_template" data-index="'+index+'"><option value="">custom</option>'+Object.entries(modelProviderTemplates).map(([key,item])=>'<option value="'+esc(key)+'"'+(model.provider_template === key ? ' selected' : '')+'>'+esc(item.label)+'</option>').join('')+'</select><button type="button" data-apply-template="'+index+'">\u5E94\u7528</button></div></div>' + '<div><label>ID</label><input data-field="id" data-index="'+index+'" value="'+esc(model.id || '')+'" placeholder="'+esc(template.suggested_id || 'sonnet')+'"><div class="muted">\u5EFA\u8BAE\u6A21\u677F\uFF1A'+esc(template.label || templateContext.templateKey || 'custom')+'</div></div>' + '<div><label>Interface</label><select data-field="interface" data-index="'+index+'"><option value="openai"'+(((model.interface || model.protocol || 'openai') === 'openai') ? ' selected' : '')+'>openai</option><option value="anthropic"'+(((model.interface || model.protocol) === 'anthropic') ? ' selected' : '')+'>anthropic</option></select></div>' + '<div><label>Model</label><input data-field="model" data-index="'+index+'" list="modelSuggestions'+index+'" value="'+esc(model.model || '')+'" placeholder="'+esc(template.default_model || 'anthropic/claude-sonnet-4')+'"><datalist id="modelSuggestions'+index+'">'+((template.model_examples || []).map(item=>'<option value="'+esc(item)+'"></option>').join(''))+'</datalist><div class="muted">\u4F8B\u5982\uFF1A'+esc((template.model_examples || ['anthropic/claude-sonnet-4']).join(' / '))+'</div></div>' + '<div><label>API</label><input data-field="api" data-index="'+index+'" value="'+esc(model.api || model.api_base_url || '')+'" placeholder="'+esc(template.api || 'https://...')+'"></div>' + '<div><label>Key</label><input data-field="key" data-index="'+index+'" value="'+esc(model.key || model.api_key || '')+'" placeholder="'+esc(template.key_placeholder || 'sk-...')+'"></div>' + '<div><label>Thinking</label><select data-field="thinking_profile" data-index="'+index+'"><option value="">default</option><option value="off"'+(((model.thinking === 'off') || model.thinking?.mode === 'off') ? ' selected' : '')+'>off</option><option value="auto"'+(((model.thinking === 'auto') || model.thinking?.mode === 'auto') ? ' selected' : '')+'>auto</option><option value="on"'+(((model.thinking === 'on') || (model.thinking?.mode === 'on' && !model.thinking?.effort)) ? ' selected' : '')+'>on</option><option value="low"'+(((model.thinking === 'low') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'low' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>low</option><option value="medium"'+(((model.thinking === 'medium') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'medium' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>medium</option><option value="high"'+(((model.thinking === 'high') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'high' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>high</option><option value="custom"'+(((typeof model.thinking === 'object') && model.thinking && model.thinking.budget_tokens) ? ' selected' : '')+'>custom</option></select></div>' + '<div><label>Thinking mode</label><select data-field="thinking_mode" data-index="'+index+'"><option value="">default</option><option value="off"'+(model.thinking?.mode === 'off' ? ' selected' : '')+'>off</option><option value="auto"'+(model.thinking?.mode === 'auto' ? ' selected' : '')+'>auto</option><option value="on"'+(model.thinking?.mode === 'on' ? ' selected' : '')+'>on</option></select></div>' + '<div><label>Thinking effort</label><select data-field="thinking_effort" data-index="'+index+'"><option value="">default</option><option value="low"'+(model.thinking?.effort === 'low' ? ' selected' : '')+'>low</option><option value="medium"'+(model.thinking?.effort === 'medium' ? ' selected' : '')+'>medium</option><option value="high"'+(model.thinking?.effort === 'high' ? ' selected' : '')+'>high</option></select></div>' + '<div><label>Thinking budget</label><input data-field="thinking_budget_tokens" data-index="'+index+'" value="'+esc(model.thinking?.budget_tokens || '')+'" placeholder="1024"></div>' + '<div><label>Vendor hint</label><input data-field="vendor_hint" data-index="'+index+'" value="'+esc(model.metadata?.vendor_hint || '')+'" placeholder="'+esc(template.vendor_hint || 'openrouter')+'"></div>' + '<div><label>Reasoning support</label><select data-field="supports_reasoning" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_reasoning === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_reasoning === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Tool support</label><select data-field="supports_tools" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_tools === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_tools === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Image support</label><select data-field="supports_images" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_images === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_images === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div style="grid-column:1/-1"><label>Metadata (advanced JSON)</label><textarea data-field="metadata" data-index="'+index+'" placeholder="{\\"label\\":\\"Balanced profile\\"}">'+esc(model.metadata ? JSON.stringify(model.metadata, null, 2) : '')+'</textarea><div class="muted">\u666E\u901A capability \u5EFA\u8BAE\u4F18\u5148\u4F7F\u7528\u4E0A\u9762\u7684\u663E\u5F0F\u5B57\u6BB5\uFF1B\u8FD9\u91CC\u4FDD\u7559\u7ED9\u9AD8\u7EA7\u6269\u5C55\u5143\u6570\u636E\u3002</div></div>' + '</div>' + '</div>'; }).join('');}function extractModelsFromForm(){ const cards=Array.from(modelsFormGrid.querySelectorAll('[data-model-card]')); return cards.map((card,index)=>{ const read=(field)=>card.querySelector('[data-field="'+field+'"][data-index="'+index+'"]'); const providerTemplate=(read('provider_template')?.value || '').trim(); const metadataRaw=(read('metadata')?.value || '').trim(); let metadata; if(metadataRaw){ metadata=JSON.parse(metadataRaw); } else { metadata={}; } const thinkingProfile=(read('thinking_profile')?.value || '').trim(); const vendorHint=(read('vendor_hint')?.value || '').trim(); const supportsReasoning=(read('supports_reasoning')?.value || '').trim(); const supportsTools=(read('supports_tools')?.value || '').trim(); const supportsImages=(read('supports_images')?.value || '').trim(); const thinking={}; const mode=(read('thinking_mode')?.value || '').trim(); const effort=(read('thinking_effort')?.value || '').trim(); const budget=(read('thinking_budget_tokens')?.value || '').trim(); if(mode) thinking.mode=mode; if(effort) thinking.effort=effort; if(budget) thinking.budget_tokens=Number(budget); const model={ id:(read('id')?.value || '').trim(), api:(read('api')?.value || '').trim(), key:(read('key')?.value || '').trim(), interface:(read('interface')?.value || '').trim(), model:(read('model')?.value || '').trim(), }; if(vendorHint){ metadata.vendor_hint=vendorHint; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'vendor_hint')){ delete metadata.vendor_hint; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No trigger rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); if(triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length){ payload.TriggerRouter={ ...(payload.TriggerRouter || {}), enabled: triggerEnabled.checked, analysis_scope: triggerAnalysisScope.value || 'last_message', llm_intent_recognition: triggerIntentEnabled.checked, intent_model: triggerIntentModel.value.trim(), rules: triggerRules }; } else { delete payload.TriggerRouter; } const smartCandidates=extractSmartCandidatesFromForm(); if(smartEnabled.checked || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim()){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: smartEnabled.checked, router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined }; } else { delete payload.SmartRouter; } const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim() || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, sticky:{ ...((payload.Governance && payload.Governance.sticky) || {}), enabled: Boolean(governanceEnabled.checked || governanceAlignmentEnabled.checked), alignment:{ ...(((payload.Governance && payload.Governance.sticky && payload.Governance.sticky.alignment) || {})), enabled: governanceAlignmentEnabled.checked, summarizer_model: governanceSummarizerModel.value.trim() } }, semantic:{ ...((payload.Governance && payload.Governance.semantic) || {}), enabled: governanceSemanticEnabled.checked, mode:'classifier', classifier_model: governanceClassifierModel.value.trim() }, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(trigger.enabled); triggerIntentEnabled.checked=Boolean(trigger.llm_intent_recognition); triggerAnalysisScope.value=trigger.analysis_scope || 'last_message'; triggerIntentModel.value=trigger.intent_model || ''; renderTriggerRulesList(trigger.rules || []); const smart=config?.SmartRouter || {}; smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(governance.sticky?.alignment?.enabled); governanceSummarizerModel.value=governance.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(governance.semantic?.enabled); governanceClassifierModel.value=governance.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u540C\u6B65 Models \u8868\u5355\u5230 JSON \u8349\u7A3F'; } catch (error) { draftPreviewStatus.textContent='\u540C\u6B65\u5931\u8D25\uFF1A'+error.message; }}function applyReferenceSuggestion(path,modelId){ if(!modelId){ return; } if(path==='Router.default'){ draftRouterDefault.value=modelId; syncDraftEditorFromForm(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 Router.default'; return; } const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const pathMatch=path.match(/^([^.[]+)(?:.(.+))?$/); if(!pathMatch){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\uFF1A'+path; return; } const tokens=path.replace(/[(d+)]/g,'.$1').split('.'); let cursor=payload; for(let i=0;i<tokens.length-1;i++){ const token=tokens[i]; const nextToken=tokens[i+1]; if(cursor[token] === undefined){ cursor[token]=String(Number(nextToken))===nextToken ? [] : {}; } cursor=cursor[token]; } cursor[tokens[tokens.length-1]]=modelId; currentDraftConfig=payload; if(payload.Router?.default){ draftRouterDefault.value=payload.Router.default; } renderConfigControlForms(payload); configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyCapabilityWarningSuggestion(path,code){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const tokens=String(path || '').replace(/[(d+)]/g,'.$1').split('.').filter(Boolean); if(!tokens.length){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } let cursor=payload; for(let i=0;i<tokens.length-1;i++){ if(cursor == null){ break; } cursor=cursor[tokens[i]]; } const lastToken=tokens[tokens.length-1]; if(code==='thinking_ignored'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } } else if(code==='tools_text_fallback' || code==='images_text_fallback'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } if(cursor && !Object.keys(cursor).length){ const parentTokens=tokens.slice(0,-1); const maybeMetadataKey=parentTokens[parentTokens.length-1]; if(maybeMetadataKey==='metadata'){ let parentCursor=payload; for(let i=0;i<parentTokens.length-1;i++){ if(parentCursor == null){ break; } parentCursor=parentCursor[parentTokens[i]]; } if(parentCursor && Object.prototype.hasOwnProperty.call(parentCursor,'metadata')){ delete parentCursor.metadata; } } } } else { draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528 warning \u4FEE\u6B63\uFF1A'+code+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function renderCompiledDiff(diff){ const summary=diff?.summary || {}; compiledDiffSummary.innerHTML=[ ['Added providers', summary.addedProviders ?? 0], ['Removed providers', summary.removedProviders ?? 0], ['Changed providers', summary.changedProviders ?? 0], ['Added models', summary.addedModels ?? 0], ['Removed models', summary.removedModels ?? 0], ['Changed models', summary.changedModels ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const rows=[ ...((diff?.providerChanges || []).map(item=>({ scope:'provider', key:item.name, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ...((diff?.modelChanges || []).map(item=>({ scope:'model', key:item.modelId, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ]; compiledDiffTableBody.innerHTML=rows.length ? rows.map(item=>'<tr>' + '<td>'+esc(item.scope)+'</td>' + '<td>'+esc(item.type)+'</td>' + '<td><code>'+esc(item.key)+'</code></td>' + '<td>'+esc(item.fields.join(', ') || '-')+'</td>' + '<td><code>'+esc(item.target.providerName || item.target.name || '-')+'</code><div class="muted">'+esc(item.target.modelName || (item.target.models || []).join(', ') || '-')}</div></td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled registry changes</td></tr>';}function renderReferenceImpact(impact){ const summary=impact?.summary || {}; referenceImpactSummary.innerHTML=[ ['Total refs', summary.total ?? 0], ['modelId refs', summary.modelIdRefs ?? 0], ['Legacy refs', summary.legacyRefs ?? 0], ['Valid modelIds', summary.validModelIds ?? 0], ['Missing modelIds', summary.missingModelIds ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const entries=impact?.entries || []; referenceImpactTableBody.innerHTML=entries.length ? entries.map(item=>'<tr>' + '<td><code>'+esc(item.path)+'</code></td>' + '<td><code>'+esc(item.value)+'</code></td>' + '<td>'+esc(item.referenceType)+'</td>' + '<td>'+esc(item.status)+'</td>' + '<td><code>'+esc(item.resolvedTarget?.providerName || '-')+'</code><div class="muted">'+esc(item.resolvedTarget?.modelName || '-')}</div></td>' + '<td>'+((item.suggestions || []).length ? item.suggestions.map(s=>'<div><code>'+esc(s.modelId)+'</code><div class="muted">'+esc(s.modelName || '-')+'</div><button type="button" data-apply-reference-path="'+esc(item.path)+'" data-apply-reference-model="'+esc(s.modelId)+'">\u5E94\u7528\u5EFA\u8BAE</button></div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No model references found</td></tr>';}function renderCompiledModels(data){ const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04'; compiledProvidersTableBody.innerHTML=providers.length ? providers.map(provider=>'<tr>' + '<td><code>'+esc(provider.name)+'</code><div class="muted">'+esc(provider.api_base_url || '-')+'</div></td>' + '<td>'+esc(provider.transformer?.use?.[0] || '-')+'</td>' + '<td>'+esc((provider.models || []).join(', ') || '-')+'</td>' + '<td>'+esc(JSON.stringify(provider.transformer || {}))+'</td>' + '<td>'+esc(provider.has_api_key ? 'configured' : 'missing')+'</td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled providers</td></tr>'; compiledModelMapTableBody.innerHTML=modelMapEntries.length ? modelMapEntries.map(([modelId,item])=>'<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td><code>'+esc(item.providerName || '-')+'</code><div class="muted">'+esc(item.modelName || '-')+'</div></td>' + '<td>'+esc(item.protocol || '-')+'</td>' + '<td>'+esc(item.compatibilityProfile || '-')+'</td>' + '<td>'+esc(item.dispatchFormat || '-')+'</td>' + '<td><code>'+esc(JSON.stringify(item.thinking || { mode: 'off' }))+'</code></td>' + '<td><code>'+esc(JSON.stringify(item.capabilities || {}))+'</code></td>' + '<td>'+esc(item.source || '-')+'</td>' + '</tr>').join('') : '<tr><td colspan="8" class="muted">No compiled model map</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || []); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || []); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addCascadeLevel(){ const next=extractCascadeLevelsFromForm(); next.push({ from:'', to:'' }); renderCascadeLevelsList(next); syncDraftEditorFromForm(); }modelsFormGrid.addEventListener('input',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('change',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-template]'); if(applyBtn){ const applyIndex=Number(applyBtn.dataset.applyTemplate); applyProviderTemplate(applyIndex); syncDraftEditorFromForm(); return; } const btn=e.target.closest('button[data-remove-model]'); if(!btn){ return; } const removeIndex=Number(btn.dataset.removeModel); const nextModels=extractModelsFromForm().filter((_,index)=>index!==removeIndex); renderModelsForm(nextModels); syncDraftEditorFromForm(); });triggerRulesList.addEventListener('input',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('change',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('click',(e)=>{ const addKeywordBtn=e.target.closest('button[data-add-trigger-keyword]'); if(addKeywordBtn){ addTriggerKeyword(Number(addKeywordBtn.dataset.addTriggerKeyword), Number(addKeywordBtn.dataset.patternIndex)); return; } const removeKeywordBtn=e.target.closest('button[data-remove-trigger-keyword]'); if(removeKeywordBtn){ const ruleIndex=Number(removeKeywordBtn.dataset.removeTriggerKeyword); const patternIndex=Number(removeKeywordBtn.dataset.patternIndex); const keywordIndex=Number(removeKeywordBtn.dataset.keywordIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex] && next[ruleIndex].patterns && next[ruleIndex].patterns[patternIndex]){ const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=(pattern.keywords || []).filter((_,index)=>index!==keywordIndex); if(!pattern.keywords.length){ pattern.keywords=['']; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const addBtn=e.target.closest('button[data-add-trigger-pattern]'); if(addBtn){ addTriggerPattern(Number(addBtn.dataset.addTriggerPattern)); return; } const removePatternBtn=e.target.closest('button[data-remove-trigger-pattern]'); if(removePatternBtn){ const ruleIndex=Number(removePatternBtn.dataset.removeTriggerPattern); const patternIndex=Number(removePatternBtn.dataset.patternIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex]){ next[ruleIndex].patterns=(next[ruleIndex].patterns || []).filter((_,index)=>index!==patternIndex); if(!next[ruleIndex].patterns.length){ next[ruleIndex].patterns=[{ type:'exact', keywords:[] }]; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const btn=e.target.closest('button[data-remove-trigger-rule]'); if(!btn){ return; } const next=extractTriggerRulesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeTriggerRule)); renderTriggerRulesList(next); syncDraftEditorFromForm(); });smartCandidatesList.addEventListener('input',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('change',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-smart-candidate]'); if(!btn){ return; } const next=extractSmartCandidatesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeSmartCandidate)); renderSmartCandidatesList(next); syncDraftEditorFromForm(); });governanceCascadeLevelsList.addEventListener('input',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('change',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-cascade-level]'); if(!btn){ return; } const next=extractCascadeLevelsFromForm().filter((_,index)=>index!==Number(btn.dataset.removeCascadeLevel)); renderCascadeLevelsList(next); syncDraftEditorFromForm(); });referenceImpactTableBody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-apply-reference-path]'); if(!btn){ return; } applyReferenceSuggestion(btn.dataset.applyReferencePath, btn.dataset.applyReferenceModel); });draftValidationList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });capabilityWarningsList.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-warning-path]'); if(applyBtn){ applyCapabilityWarningSuggestion(applyBtn.dataset.applyWarningPath, applyBtn.dataset.applyWarningCode); return; } const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });function renderMetrics(metrics){ metricsGrid.innerHTML=[ ['Recent traces', metrics.totalTraces ?? 0], ['Sticky hit rate', pct(metrics.stickyHitRate)], ['Cascade rate', pct(metrics.cascadeTriggeredRate)], ['Shadow rate', pct(metrics.shadowCheckedRate)], ['Alignment rate', pct(metrics.alignmentUsedRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.TriggerRouter={ ...(payload.TriggerRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.TriggerRouter?.enabled), analysis_scope: payload.TriggerRouter?.analysis_scope || 'last_message', llm_intent_recognition: payload.TriggerRouter?.llm_intent_recognition || false, intent_model: payload.TriggerRouter?.intent_model || '', rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.TriggerRouter?.rules || []) }; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), sticky:{ ...((payload.Governance && payload.Governance.sticky) || {}), alignment:{ ...(((payload.Governance && payload.Governance.sticky && payload.Governance.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.Governance?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.Governance?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.Governance && payload.Governance.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.Governance?.semantic?.enabled), mode:(payload.Governance?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.Governance?.semantic?.classifier_model || '') }, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || []); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderAnomalies(anomalies){ if(!anomalies || !anomalies.length){ anomalyList.innerHTML='<div class="alert info"><strong>No active alerts</strong><div class="muted">\u5F53\u524D\u7A97\u53E3\u672A\u53D1\u73B0\u660E\u663E\u6CBB\u7406\u5F02\u5E38</div></div>'; return; } anomalyList.innerHTML=anomalies.map(item=>'<div class="alert '+esc(item.severity || 'info')+'"><strong>'+esc(item.type)+'</strong><div>'+esc(item.message)+'</div></div>').join('');}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')}</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); renderMetrics(metricsData.metrics || {}); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || []); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderTrendTable(metricsData || {}); const traces=data.traces || []; if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`
|
|
3284
3585
|
);
|
|
3285
3586
|
});
|
|
3286
3587
|
return server;
|
|
@@ -3505,6 +3806,16 @@ function isProcessAlive(pid) {
|
|
|
3505
3806
|
return false;
|
|
3506
3807
|
}
|
|
3507
3808
|
}
|
|
3809
|
+
async function waitForProcessExit(pid, timeoutMs = 5e3) {
|
|
3810
|
+
const startedAt = Date.now();
|
|
3811
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
3812
|
+
if (!isProcessAlive(pid)) {
|
|
3813
|
+
return true;
|
|
3814
|
+
}
|
|
3815
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
3816
|
+
}
|
|
3817
|
+
return !isProcessAlive(pid);
|
|
3818
|
+
}
|
|
3508
3819
|
function killProcess(pid) {
|
|
3509
3820
|
if (process.platform === "win32") {
|
|
3510
3821
|
(0, import_child_process.spawnSync)("taskkill", ["/F", "/PID", String(pid)], { timeout: 5e3 });
|
|
@@ -3576,18 +3887,18 @@ var init_SSESerializer_transform = __esm({
|
|
|
3576
3887
|
constructor() {
|
|
3577
3888
|
const transformStream = new TransformStream({
|
|
3578
3889
|
transform: (event2, controller) => {
|
|
3579
|
-
let
|
|
3890
|
+
let output3 = "";
|
|
3580
3891
|
if (event2.event) {
|
|
3581
|
-
|
|
3892
|
+
output3 += `event: ${event2.event}
|
|
3582
3893
|
`;
|
|
3583
3894
|
}
|
|
3584
3895
|
if (event2.data) {
|
|
3585
3896
|
const dataStr = typeof event2.data === "string" ? event2.data : JSON.stringify(event2.data);
|
|
3586
|
-
|
|
3897
|
+
output3 += `data: ${dataStr}
|
|
3587
3898
|
`;
|
|
3588
3899
|
}
|
|
3589
|
-
|
|
3590
|
-
controller.enqueue(new TextEncoder().encode(
|
|
3900
|
+
output3 += "\n";
|
|
3901
|
+
controller.enqueue(new TextEncoder().encode(output3));
|
|
3591
3902
|
}
|
|
3592
3903
|
});
|
|
3593
3904
|
this.readable = transformStream.readable;
|
|
@@ -4285,7 +4596,7 @@ Important: Respond ONLY with the JSON, no additional text.`;
|
|
|
4285
4596
|
});
|
|
4286
4597
|
|
|
4287
4598
|
// src/trigger/smart-router.ts
|
|
4288
|
-
var import_lru_cache7,
|
|
4599
|
+
var import_lru_cache7, SmartRouterSelector, smartRouterSelector;
|
|
4289
4600
|
var init_smart_router = __esm({
|
|
4290
4601
|
"src/trigger/smart-router.ts"() {
|
|
4291
4602
|
"use strict";
|
|
@@ -4294,26 +4605,6 @@ var init_smart_router = __esm({
|
|
|
4294
4605
|
init_log();
|
|
4295
4606
|
init_message_ir();
|
|
4296
4607
|
init_anthropic();
|
|
4297
|
-
SMART_ROUTER_PROMPT = `You are a model routing assistant. Your job is to select the most appropriate AI model from the given candidates to handle the user's request.
|
|
4298
|
-
|
|
4299
|
-
User request:
|
|
4300
|
-
"""
|
|
4301
|
-
{request}
|
|
4302
|
-
"""
|
|
4303
|
-
|
|
4304
|
-
Available models:
|
|
4305
|
-
{candidates}
|
|
4306
|
-
|
|
4307
|
-
Select the most appropriate model and respond in the following JSON format ONLY:
|
|
4308
|
-
{
|
|
4309
|
-
"model": "<exact model identifier from the list>",
|
|
4310
|
-
"confidence": <0.0-1.0>,
|
|
4311
|
-
"reasoning": "<brief explanation>"
|
|
4312
|
-
}
|
|
4313
|
-
|
|
4314
|
-
Important:
|
|
4315
|
-
- The "model" field MUST be one of the exact identifiers listed above
|
|
4316
|
-
- Respond ONLY with the JSON, no additional text`;
|
|
4317
4608
|
SmartRouterSelector = class {
|
|
4318
4609
|
cache;
|
|
4319
4610
|
constructor() {
|
|
@@ -4346,8 +4637,42 @@ Important:
|
|
|
4346
4637
|
/**
|
|
4347
4638
|
* 构建完整 prompt
|
|
4348
4639
|
*/
|
|
4349
|
-
buildPrompt(text, candidates) {
|
|
4350
|
-
|
|
4640
|
+
buildPrompt(text, candidates, hint) {
|
|
4641
|
+
const sections = [
|
|
4642
|
+
"You are a model routing assistant. Your job is to select the most appropriate AI model from the given candidates to handle the user's request."
|
|
4643
|
+
];
|
|
4644
|
+
if (hint?.taskSummary) {
|
|
4645
|
+
sections.push(`Task summary:
|
|
4646
|
+
"""
|
|
4647
|
+
${hint.taskSummary}
|
|
4648
|
+
"""`);
|
|
4649
|
+
}
|
|
4650
|
+
if (hint?.topRouteCandidates?.length) {
|
|
4651
|
+
sections.push(
|
|
4652
|
+
"Pre-filtered route candidates:\n" + hint.topRouteCandidates.map(
|
|
4653
|
+
(candidate, index) => `${index + 1}. ${candidate.name} -> ${candidate.model}${candidate.description ? ` (${candidate.description})` : ""}${candidate.confidence !== void 0 ? ` [confidence=${candidate.confidence}]` : ""}`
|
|
4654
|
+
).join("\n")
|
|
4655
|
+
);
|
|
4656
|
+
}
|
|
4657
|
+
sections.push(
|
|
4658
|
+
`User request:
|
|
4659
|
+
"""
|
|
4660
|
+
${text}
|
|
4661
|
+
"""`,
|
|
4662
|
+
`Available models:
|
|
4663
|
+
${this.buildCandidatesList(candidates)}`,
|
|
4664
|
+
`Select the most appropriate model and respond in the following JSON format ONLY:
|
|
4665
|
+
{
|
|
4666
|
+
"model": "<exact model identifier from the list>",
|
|
4667
|
+
"confidence": <0.0-1.0>,
|
|
4668
|
+
"reasoning": "<brief explanation>"
|
|
4669
|
+
}
|
|
4670
|
+
|
|
4671
|
+
Important:
|
|
4672
|
+
- The "model" field MUST be one of the exact identifiers listed above
|
|
4673
|
+
- Respond ONLY with the JSON, no additional text`
|
|
4674
|
+
);
|
|
4675
|
+
return sections.join("\n\n");
|
|
4351
4676
|
}
|
|
4352
4677
|
/**
|
|
4353
4678
|
* 使用 LLM 选择最优模型
|
|
@@ -4358,7 +4683,7 @@ Important:
|
|
|
4358
4683
|
* @param fetchFn 可注入的 fetch 函数(用于测试)
|
|
4359
4684
|
* @returns 选择结果,失败时返回 null
|
|
4360
4685
|
*/
|
|
4361
|
-
async selectModel(text, config, port = DEFAULT_CONFIG2.PORT, fetchFn, apiKey, timeoutMs) {
|
|
4686
|
+
async selectModel(text, config, port = DEFAULT_CONFIG2.PORT, fetchFn, apiKey, timeoutMs, hint) {
|
|
4362
4687
|
if (!config.enabled) {
|
|
4363
4688
|
return null;
|
|
4364
4689
|
}
|
|
@@ -4372,11 +4697,12 @@ Important:
|
|
|
4372
4697
|
}
|
|
4373
4698
|
try {
|
|
4374
4699
|
const fetchImpl = fetchFn || fetch;
|
|
4375
|
-
const prompt = this.buildPrompt(text, config.candidates);
|
|
4700
|
+
const prompt = this.buildPrompt(text, config.candidates, hint);
|
|
4376
4701
|
const response = await fetchImpl(`http://127.0.0.1:${port}/v1/messages`, {
|
|
4377
4702
|
method: "POST",
|
|
4378
4703
|
headers: {
|
|
4379
4704
|
"Content-Type": "application/json",
|
|
4705
|
+
"x-ctr-smart-router": "1",
|
|
4380
4706
|
...apiKey ? { "x-api-key": apiKey } : {}
|
|
4381
4707
|
},
|
|
4382
4708
|
body: JSON.stringify(
|
|
@@ -4438,6 +4764,82 @@ var init_selector = __esm({
|
|
|
4438
4764
|
init_semantic_router();
|
|
4439
4765
|
init_compile();
|
|
4440
4766
|
ModelSelector = class {
|
|
4767
|
+
resolveRouteModel(appConfig, ref) {
|
|
4768
|
+
if (!ref) {
|
|
4769
|
+
return void 0;
|
|
4770
|
+
}
|
|
4771
|
+
return appConfig ? resolveModelReference(appConfig, ref) ?? ref : ref;
|
|
4772
|
+
}
|
|
4773
|
+
buildSemanticCandidates(rules, governanceConfig) {
|
|
4774
|
+
const defaultThreshold = governanceConfig?.semantic?.threshold;
|
|
4775
|
+
const legacyPrototypes = governanceConfig?.semantic?.prototypes ?? {};
|
|
4776
|
+
return this.sortRulesByPriority(rules).map((rule) => {
|
|
4777
|
+
const prototype = rule.semantic_profile?.prototype ?? legacyPrototypes[rule.name] ?? rule.description;
|
|
4778
|
+
const semanticEnabled = rule.semantic_profile?.enabled !== false && Boolean(prototype);
|
|
4779
|
+
if (!semanticEnabled || !prototype) {
|
|
4780
|
+
return null;
|
|
4781
|
+
}
|
|
4782
|
+
return {
|
|
4783
|
+
rule,
|
|
4784
|
+
prototype,
|
|
4785
|
+
threshold: rule.semantic_profile?.threshold ?? defaultThreshold
|
|
4786
|
+
};
|
|
4787
|
+
}).filter(Boolean);
|
|
4788
|
+
}
|
|
4789
|
+
getStickyCorrection(text, req, governanceConfig) {
|
|
4790
|
+
if (!governanceConfig?.enabled || !governanceConfig.sticky?.enabled || !req.sessionId) {
|
|
4791
|
+
return {};
|
|
4792
|
+
}
|
|
4793
|
+
const fingerprint = createTaskFingerprint(text);
|
|
4794
|
+
const sessionState = sessionStateStore.get(req.sessionId);
|
|
4795
|
+
if (!fingerprint || sessionState?.lastTaskFingerprint !== fingerprint || !(sessionState.preferredModel || sessionState.lastSuccessfulModel)) {
|
|
4796
|
+
return {};
|
|
4797
|
+
}
|
|
4798
|
+
return {
|
|
4799
|
+
fingerprint,
|
|
4800
|
+
sessionModel: sessionState.preferredModel || sessionState.lastSuccessfulModel
|
|
4801
|
+
};
|
|
4802
|
+
}
|
|
4803
|
+
applyStickyCorrection(candidate, sticky, appConfig) {
|
|
4804
|
+
if (!sticky.sessionModel) {
|
|
4805
|
+
return candidate;
|
|
4806
|
+
}
|
|
4807
|
+
const stickyModel = this.resolveRouteModel(appConfig, sticky.sessionModel);
|
|
4808
|
+
if (!stickyModel) {
|
|
4809
|
+
return candidate;
|
|
4810
|
+
}
|
|
4811
|
+
if (!candidate) {
|
|
4812
|
+
log(`[StickyRouting] Reusing model "${stickyModel}" as unified router correction`);
|
|
4813
|
+
return {
|
|
4814
|
+
matched: true,
|
|
4815
|
+
model: stickyModel,
|
|
4816
|
+
confidence: 0.95,
|
|
4817
|
+
analysisTime: 0,
|
|
4818
|
+
routeSource: "sticky_correction"
|
|
4819
|
+
};
|
|
4820
|
+
}
|
|
4821
|
+
if (candidate.model === stickyModel) {
|
|
4822
|
+
return candidate;
|
|
4823
|
+
}
|
|
4824
|
+
log(`[StickyRouting] Correcting selected model "${candidate.model}" -> "${stickyModel}"`);
|
|
4825
|
+
return {
|
|
4826
|
+
...candidate,
|
|
4827
|
+
model: stickyModel,
|
|
4828
|
+
confidence: Math.max(candidate.confidence, 0.95),
|
|
4829
|
+
routeSource: "sticky_correction"
|
|
4830
|
+
};
|
|
4831
|
+
}
|
|
4832
|
+
buildSmartRouterHint(text, rules) {
|
|
4833
|
+
return {
|
|
4834
|
+
taskSummary: text.slice(0, 240),
|
|
4835
|
+
topRouteCandidates: this.sortRulesByPriority(rules).filter((rule) => rule.description).slice(0, 3).map((rule) => ({
|
|
4836
|
+
name: rule.name,
|
|
4837
|
+
model: rule.model,
|
|
4838
|
+
description: rule.description,
|
|
4839
|
+
confidence: void 0
|
|
4840
|
+
}))
|
|
4841
|
+
};
|
|
4842
|
+
}
|
|
4441
4843
|
/**
|
|
4442
4844
|
* 按优先级排序规则
|
|
4443
4845
|
* 优先级数值越大,优先级越高
|
|
@@ -4522,33 +4924,29 @@ var init_selector = __esm({
|
|
|
4522
4924
|
routeSource: "trigger_rule"
|
|
4523
4925
|
};
|
|
4524
4926
|
}
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
matched: true,
|
|
4534
|
-
model: appConfig ? resolveModelReference(appConfig, stickyModel) ?? stickyModel : stickyModel,
|
|
4535
|
-
confidence: 0.95,
|
|
4536
|
-
analysisTime: Date.now() - startTime,
|
|
4537
|
-
analyzedText: text,
|
|
4538
|
-
routeSource: "sticky"
|
|
4539
|
-
};
|
|
4540
|
-
}
|
|
4541
|
-
}
|
|
4542
|
-
}
|
|
4543
|
-
if (governanceConfig?.enabled && governanceConfig.semantic?.enabled) {
|
|
4544
|
-
const semanticResult = governanceConfig.semantic.mode === "classifier" ? await semanticRouter.analyzeWithClassifier(
|
|
4927
|
+
const stickyCorrection = this.getStickyCorrection(text, req, governanceConfig);
|
|
4928
|
+
const semanticCandidates = this.buildSemanticCandidates(config.rules, governanceConfig);
|
|
4929
|
+
if (governanceConfig?.enabled && governanceConfig.semantic?.enabled && semanticCandidates.length > 0) {
|
|
4930
|
+
const semanticConfig = {
|
|
4931
|
+
...governanceConfig.semantic,
|
|
4932
|
+
prototypes: Object.fromEntries(semanticCandidates.map((candidate) => [candidate.rule.name, candidate.prototype]))
|
|
4933
|
+
};
|
|
4934
|
+
const semanticResult = semanticConfig.mode === "classifier" ? await semanticRouter.analyzeWithClassifier(
|
|
4545
4935
|
text,
|
|
4546
|
-
|
|
4936
|
+
semanticConfig,
|
|
4547
4937
|
port,
|
|
4548
4938
|
void 0,
|
|
4549
4939
|
apiKey,
|
|
4550
4940
|
timeoutMs
|
|
4551
|
-
) : semanticRouter.
|
|
4941
|
+
) : semanticRouter.analyzeCandidates(
|
|
4942
|
+
text,
|
|
4943
|
+
semanticCandidates.map((candidate) => ({
|
|
4944
|
+
intent: candidate.rule.name,
|
|
4945
|
+
prototype: candidate.prototype,
|
|
4946
|
+
threshold: candidate.threshold
|
|
4947
|
+
})),
|
|
4948
|
+
semanticConfig.threshold
|
|
4949
|
+
);
|
|
4552
4950
|
if (semanticResult) {
|
|
4553
4951
|
const matchedRule = config.rules.find(
|
|
4554
4952
|
(rule) => rule.enabled !== false && rule.name.toLowerCase() === semanticResult.intent.toLowerCase()
|
|
@@ -4558,15 +4956,16 @@ var init_selector = __esm({
|
|
|
4558
4956
|
if (req.governanceTrace) {
|
|
4559
4957
|
req.governanceTrace.semanticIntent = semanticResult.intent;
|
|
4560
4958
|
}
|
|
4561
|
-
|
|
4959
|
+
const semanticSelection = {
|
|
4562
4960
|
matched: true,
|
|
4563
4961
|
rule: matchedRule,
|
|
4564
|
-
model:
|
|
4962
|
+
model: this.resolveRouteModel(appConfig, matchedRule.model),
|
|
4565
4963
|
confidence: semanticResult.confidence,
|
|
4566
4964
|
analysisTime: Date.now() - startTime,
|
|
4567
4965
|
analyzedText: text,
|
|
4568
|
-
routeSource: "
|
|
4966
|
+
routeSource: "semantic_match"
|
|
4569
4967
|
};
|
|
4968
|
+
return this.applyStickyCorrection(semanticSelection, stickyCorrection, appConfig) ?? semanticSelection;
|
|
4570
4969
|
}
|
|
4571
4970
|
}
|
|
4572
4971
|
}
|
|
@@ -4574,16 +4973,24 @@ var init_selector = __esm({
|
|
|
4574
4973
|
try {
|
|
4575
4974
|
const resolvedSmartRouterConfig = appConfig ? {
|
|
4576
4975
|
...smartRouterConfig,
|
|
4577
|
-
router_model:
|
|
4976
|
+
router_model: this.resolveRouteModel(appConfig, smartRouterConfig.router_model) ?? smartRouterConfig.router_model,
|
|
4578
4977
|
candidates: smartRouterConfig.candidates.map((candidate) => ({
|
|
4579
4978
|
...candidate,
|
|
4580
|
-
model:
|
|
4979
|
+
model: this.resolveRouteModel(appConfig, candidate.model) ?? candidate.model
|
|
4581
4980
|
}))
|
|
4582
4981
|
} : smartRouterConfig;
|
|
4583
|
-
const smartResult = await smartRouterSelector.selectModel(
|
|
4982
|
+
const smartResult = await smartRouterSelector.selectModel(
|
|
4983
|
+
text,
|
|
4984
|
+
resolvedSmartRouterConfig,
|
|
4985
|
+
port,
|
|
4986
|
+
void 0,
|
|
4987
|
+
apiKey,
|
|
4988
|
+
timeoutMs,
|
|
4989
|
+
this.buildSmartRouterHint(text, config.rules)
|
|
4990
|
+
);
|
|
4584
4991
|
if (smartResult) {
|
|
4585
4992
|
log(`[SmartRouter] Selected model "${smartResult.model}" (confidence: ${smartResult.confidence})`);
|
|
4586
|
-
|
|
4993
|
+
const smartSelection = {
|
|
4587
4994
|
matched: true,
|
|
4588
4995
|
model: smartResult.model,
|
|
4589
4996
|
confidence: smartResult.confidence,
|
|
@@ -4591,6 +4998,7 @@ var init_selector = __esm({
|
|
|
4591
4998
|
analyzedText: text,
|
|
4592
4999
|
routeSource: "smart_router"
|
|
4593
5000
|
};
|
|
5001
|
+
return this.applyStickyCorrection(smartSelection, stickyCorrection, appConfig) ?? smartSelection;
|
|
4594
5002
|
}
|
|
4595
5003
|
} catch (error) {
|
|
4596
5004
|
logError("[ModelSelector] SmartRouter error:", error);
|
|
@@ -4602,21 +5010,30 @@ var init_selector = __esm({
|
|
|
4602
5010
|
if (intentResult.confidence > 0.5 && intentResult.intent !== "general") {
|
|
4603
5011
|
const matchedRule = intentDetector.findRuleByIntent(intentResult.intent, config.rules);
|
|
4604
5012
|
if (matchedRule) {
|
|
4605
|
-
|
|
5013
|
+
const intentSelection = {
|
|
4606
5014
|
matched: true,
|
|
4607
5015
|
rule: matchedRule,
|
|
4608
|
-
model:
|
|
5016
|
+
model: this.resolveRouteModel(appConfig, matchedRule.model),
|
|
4609
5017
|
confidence: intentResult.confidence,
|
|
4610
5018
|
analysisTime: Date.now() - startTime,
|
|
4611
5019
|
analyzedText: text,
|
|
4612
|
-
routeSource: "
|
|
5020
|
+
routeSource: "intent_fallback"
|
|
4613
5021
|
};
|
|
5022
|
+
return this.applyStickyCorrection(intentSelection, stickyCorrection, appConfig) ?? intentSelection;
|
|
4614
5023
|
}
|
|
4615
5024
|
}
|
|
4616
5025
|
} catch (error) {
|
|
4617
5026
|
logError("[ModelSelector] Intent detection error:", error);
|
|
4618
5027
|
}
|
|
4619
5028
|
}
|
|
5029
|
+
const stickyOnlySelection = this.applyStickyCorrection(null, stickyCorrection, appConfig);
|
|
5030
|
+
if (stickyOnlySelection) {
|
|
5031
|
+
return {
|
|
5032
|
+
...stickyOnlySelection,
|
|
5033
|
+
analysisTime: Date.now() - startTime,
|
|
5034
|
+
analyzedText: text
|
|
5035
|
+
};
|
|
5036
|
+
}
|
|
4620
5037
|
return {
|
|
4621
5038
|
matched: false,
|
|
4622
5039
|
confidence: 0,
|
|
@@ -4773,13 +5190,15 @@ var init_trigger = __esm({
|
|
|
4773
5190
|
if (req.governanceTrace) {
|
|
4774
5191
|
if (result.routeSource === "trigger_rule" && result.rule?.name) {
|
|
4775
5192
|
appendTraceReason(req.governanceTrace, `trigger_rule:${result.rule.name}`);
|
|
4776
|
-
} else if (result.routeSource === "
|
|
5193
|
+
} else if (result.routeSource === "semantic_match" && result.rule?.name) {
|
|
5194
|
+
appendTraceReason(req.governanceTrace, `semantic_match:${result.rule.name}`);
|
|
5195
|
+
} else if (result.routeSource === "sticky_correction") {
|
|
4777
5196
|
req.governanceTrace.stickyHit = true;
|
|
4778
|
-
appendTraceReason(req.governanceTrace, "
|
|
5197
|
+
appendTraceReason(req.governanceTrace, "sticky_correction");
|
|
4779
5198
|
} else if (result.routeSource === "smart_router") {
|
|
4780
|
-
appendTraceReason(req.governanceTrace, "
|
|
4781
|
-
} else if (result.routeSource === "
|
|
4782
|
-
appendTraceReason(req.governanceTrace, "
|
|
5199
|
+
appendTraceReason(req.governanceTrace, "smart_decision");
|
|
5200
|
+
} else if (result.routeSource === "intent_fallback") {
|
|
5201
|
+
appendTraceReason(req.governanceTrace, "intent_fallback");
|
|
4783
5202
|
} else {
|
|
4784
5203
|
appendTraceReason(req.governanceTrace, "trigger_router:no_match");
|
|
4785
5204
|
}
|
|
@@ -4837,7 +5256,7 @@ var init_trigger = __esm({
|
|
|
4837
5256
|
req.body.model = result.model;
|
|
4838
5257
|
req.triggerResult = result;
|
|
4839
5258
|
log(
|
|
4840
|
-
`[TriggerRouter] ${result.routeSource === "
|
|
5259
|
+
`[TriggerRouter] ${result.routeSource === "sticky_correction" ? "Sticky correction selected" : result.routeSource === "semantic_match" ? `Semantic match "${result.rule?.name}"` : result.routeSource === "smart_router" ? "Smart fallback selected" : result.routeSource === "intent_fallback" ? `Intent fallback "${result.rule?.name}"` : result.rule ? `Matched rule "${result.rule.name}"` : "Unified router selected"} -> model "${result.model}" (confidence: ${result.confidence}, time: ${result.analysisTime}ms)`
|
|
4841
5260
|
);
|
|
4842
5261
|
}
|
|
4843
5262
|
} catch (error) {
|
|
@@ -4889,6 +5308,15 @@ function toOpenAIToolResultMessages(parts) {
|
|
|
4889
5308
|
content: typeof part.content === "string" ? part.content : JSON.stringify(part.content)
|
|
4890
5309
|
}));
|
|
4891
5310
|
}
|
|
5311
|
+
function getToolName(tool) {
|
|
5312
|
+
return tool?.name ?? tool?.function?.name;
|
|
5313
|
+
}
|
|
5314
|
+
function getToolDescription(tool) {
|
|
5315
|
+
return tool?.description ?? tool?.function?.description;
|
|
5316
|
+
}
|
|
5317
|
+
function getToolInputSchema(tool) {
|
|
5318
|
+
return tool?.input_schema ?? tool?.function?.parameters;
|
|
5319
|
+
}
|
|
4892
5320
|
function toOpenAITools(tools) {
|
|
4893
5321
|
if (!Array.isArray(tools) || !tools.length) {
|
|
4894
5322
|
return void 0;
|
|
@@ -4896,9 +5324,9 @@ function toOpenAITools(tools) {
|
|
|
4896
5324
|
return tools.map((tool) => ({
|
|
4897
5325
|
type: "function",
|
|
4898
5326
|
function: {
|
|
4899
|
-
name: tool
|
|
4900
|
-
description: tool
|
|
4901
|
-
parameters: tool
|
|
5327
|
+
name: getToolName(tool),
|
|
5328
|
+
description: getToolDescription(tool),
|
|
5329
|
+
parameters: getToolInputSchema(tool)
|
|
4902
5330
|
}
|
|
4903
5331
|
}));
|
|
4904
5332
|
}
|
|
@@ -4925,10 +5353,10 @@ function toOpenAIToolChoice(toolChoice) {
|
|
|
4925
5353
|
}
|
|
4926
5354
|
return toolChoice;
|
|
4927
5355
|
}
|
|
4928
|
-
function toOpenAIChatRequest(
|
|
5356
|
+
function toOpenAIChatRequest(input3) {
|
|
4929
5357
|
const messages = [
|
|
4930
|
-
...
|
|
4931
|
-
...
|
|
5358
|
+
...input3.ir.system.map((text) => ({ role: "system", content: text })),
|
|
5359
|
+
...input3.ir.messages.flatMap((message) => {
|
|
4932
5360
|
const content = toOpenAIContent(message.parts);
|
|
4933
5361
|
const toolCalls = message.role === "assistant" ? toOpenAIToolCalls(message.parts) : [];
|
|
4934
5362
|
const toolResults = toOpenAIToolResultMessages(message.parts);
|
|
@@ -4947,26 +5375,26 @@ function toOpenAIChatRequest(input2) {
|
|
|
4947
5375
|
})
|
|
4948
5376
|
];
|
|
4949
5377
|
const body = {
|
|
4950
|
-
model:
|
|
5378
|
+
model: input3.model,
|
|
4951
5379
|
messages
|
|
4952
5380
|
};
|
|
4953
|
-
if (
|
|
4954
|
-
body.max_completion_tokens =
|
|
5381
|
+
if (input3.max_completion_tokens !== void 0) {
|
|
5382
|
+
body.max_completion_tokens = input3.max_completion_tokens;
|
|
4955
5383
|
}
|
|
4956
|
-
if (
|
|
4957
|
-
body.stream =
|
|
5384
|
+
if (input3.stream !== void 0) {
|
|
5385
|
+
body.stream = input3.stream;
|
|
4958
5386
|
}
|
|
4959
|
-
const tools = toOpenAITools(
|
|
5387
|
+
const tools = toOpenAITools(input3.tools);
|
|
4960
5388
|
if (tools) {
|
|
4961
5389
|
body.tools = tools;
|
|
4962
5390
|
}
|
|
4963
|
-
const toolChoice = toOpenAIToolChoice(
|
|
5391
|
+
const toolChoice = toOpenAIToolChoice(input3.tool_choice);
|
|
4964
5392
|
if (toolChoice !== void 0) {
|
|
4965
5393
|
body.tool_choice = toolChoice;
|
|
4966
5394
|
}
|
|
4967
|
-
if (
|
|
5395
|
+
if (input3.ir.options?.thinking?.enabled) {
|
|
4968
5396
|
body.reasoning = {
|
|
4969
|
-
...
|
|
5397
|
+
...input3.ir.options.thinking.effort ? { effort: input3.ir.options.thinking.effort } : {}
|
|
4970
5398
|
};
|
|
4971
5399
|
}
|
|
4972
5400
|
return body;
|
|
@@ -4991,19 +5419,19 @@ function stringifyFallbackContent(value) {
|
|
|
4991
5419
|
return String(value);
|
|
4992
5420
|
}
|
|
4993
5421
|
}
|
|
4994
|
-
function applyCapabilityFallbacks(
|
|
5422
|
+
function applyCapabilityFallbacks(input3) {
|
|
4995
5423
|
const diagnostics = [];
|
|
4996
|
-
const nextRequest = { ...
|
|
5424
|
+
const nextRequest = { ...input3.request };
|
|
4997
5425
|
const nextIR = {
|
|
4998
|
-
...
|
|
4999
|
-
system: [...
|
|
5000
|
-
messages:
|
|
5426
|
+
...input3.ir,
|
|
5427
|
+
system: [...input3.ir.system],
|
|
5428
|
+
messages: input3.ir.messages.map((message) => ({
|
|
5001
5429
|
...message,
|
|
5002
5430
|
parts: message.parts.map((part) => ({ ...part }))
|
|
5003
5431
|
})),
|
|
5004
|
-
options:
|
|
5432
|
+
options: input3.ir.options ? { ...input3.ir.options } : void 0
|
|
5005
5433
|
};
|
|
5006
|
-
if (
|
|
5434
|
+
if (input3.capabilities?.thinking.supported === false && nextIR.options?.thinking) {
|
|
5007
5435
|
diagnostics.push("thinking_ignored");
|
|
5008
5436
|
delete nextIR.options.thinking;
|
|
5009
5437
|
delete nextRequest.thinking;
|
|
@@ -5014,7 +5442,7 @@ function applyCapabilityFallbacks(input2) {
|
|
|
5014
5442
|
const hasImageParts = nextIR.messages.some(
|
|
5015
5443
|
(message) => message.parts.some((part) => part.type === "image")
|
|
5016
5444
|
);
|
|
5017
|
-
if (
|
|
5445
|
+
if (input3.capabilities?.images === false && hasImageParts) {
|
|
5018
5446
|
diagnostics.push("images_text_fallback");
|
|
5019
5447
|
nextIR.messages = nextIR.messages.map((message) => ({
|
|
5020
5448
|
...message,
|
|
@@ -5034,7 +5462,7 @@ function applyCapabilityFallbacks(input2) {
|
|
|
5034
5462
|
const hasToolParts = nextIR.messages.some(
|
|
5035
5463
|
(message) => message.parts.some((part) => part.type === "tool_call" || part.type === "tool_result")
|
|
5036
5464
|
);
|
|
5037
|
-
if (
|
|
5465
|
+
if (input3.capabilities?.tools === false && (Array.isArray(nextRequest.tools) && nextRequest.tools.length || hasToolParts)) {
|
|
5038
5466
|
diagnostics.push("tools_text_fallback");
|
|
5039
5467
|
delete nextRequest.tools;
|
|
5040
5468
|
delete nextRequest.tool_choice;
|
|
@@ -5067,6 +5495,42 @@ function applyCapabilityFallbacks(input2) {
|
|
|
5067
5495
|
request: nextRequest
|
|
5068
5496
|
};
|
|
5069
5497
|
}
|
|
5498
|
+
function describeProtocolDiagnostic(code) {
|
|
5499
|
+
switch (code) {
|
|
5500
|
+
case "thinking_ignored":
|
|
5501
|
+
return {
|
|
5502
|
+
code,
|
|
5503
|
+
severity: "info",
|
|
5504
|
+
label: "thinking \u5DF2\u5FFD\u7565",
|
|
5505
|
+
summary: "\u5F53\u524D\u6A21\u578B\u6216\u63A5\u53E3\u672A\u542F\u7528 reasoning \u80FD\u529B\uFF0C\u8BF7\u6C42\u4E2D\u7684 thinking \u8BBE\u7F6E\u4E0D\u4F1A\u7EE7\u7EED\u4F20\u7ED9\u4E0A\u6E38\u3002",
|
|
5506
|
+
action: "\u5982\u9700\u4FDD\u7559 thinking\uFF0C\u8BF7\u5207\u56DE\u652F\u6301 reasoning \u7684\u6A21\u578B\uFF0C\u6216\u79FB\u9664\u5F53\u524D\u6A21\u578B\u4E0A\u7684 thinking \u914D\u7F6E\u3002"
|
|
5507
|
+
};
|
|
5508
|
+
case "images_text_fallback":
|
|
5509
|
+
return {
|
|
5510
|
+
code,
|
|
5511
|
+
severity: "warn",
|
|
5512
|
+
label: "\u56FE\u7247\u5DF2\u964D\u7EA7\u4E3A\u6587\u672C",
|
|
5513
|
+
summary: "\u5F53\u524D\u6A21\u578B\u672A\u58F0\u660E\u56FE\u7247\u8F93\u5165\u80FD\u529B\uFF0C\u8BF7\u6C42\u4E2D\u7684\u56FE\u7247\u5185\u5BB9\u4F1A\u9000\u5316\u4E3A\u6587\u672C\u63D0\u793A\uFF0C\u4E0D\u4F1A\u539F\u6837\u53D1\u9001\u5230\u4E0A\u6E38\u3002",
|
|
5514
|
+
action: "\u5982\u9700\u4FDD\u7559\u56FE\u7247\u8F93\u5165\uFF0C\u8BF7\u542F\u7528 supports_images \u6216\u5207\u56DE\u652F\u6301\u56FE\u7247\u7684\u6A21\u578B\u3002"
|
|
5515
|
+
};
|
|
5516
|
+
case "tools_text_fallback":
|
|
5517
|
+
return {
|
|
5518
|
+
code,
|
|
5519
|
+
severity: "warn",
|
|
5520
|
+
label: "\u5DE5\u5177\u8C03\u7528\u5DF2\u964D\u7EA7\u4E3A\u6587\u672C",
|
|
5521
|
+
summary: "\u5F53\u524D\u6A21\u578B\u672A\u58F0\u660E\u5DE5\u5177\u80FD\u529B\uFF0Ctool definitions \u4E0E tool call/result \u4F1A\u9000\u5316\u4E3A\u666E\u901A\u6587\u672C\u5185\u5BB9\u3002",
|
|
5522
|
+
action: "\u5982\u9700\u4FDD\u7559\u5DE5\u5177\u8C03\u7528\uFF0C\u8BF7\u542F\u7528 supports_tools \u6216\u5207\u56DE\u652F\u6301\u5DE5\u5177\u7684\u6A21\u578B\u3002"
|
|
5523
|
+
};
|
|
5524
|
+
default:
|
|
5525
|
+
return {
|
|
5526
|
+
code,
|
|
5527
|
+
severity: "info",
|
|
5528
|
+
label: code,
|
|
5529
|
+
summary: "\u672A\u77E5\u534F\u8BAE\u8BCA\u65AD\u3002",
|
|
5530
|
+
action: "\u8BF7\u7ED3\u5408\u539F\u59CB\u8BF7\u6C42\u548C\u4E0A\u6E38\u54CD\u5E94\u7EE7\u7EED\u6392\u67E5\u3002"
|
|
5531
|
+
};
|
|
5532
|
+
}
|
|
5533
|
+
}
|
|
5070
5534
|
function omitRequestFields(body) {
|
|
5071
5535
|
const {
|
|
5072
5536
|
model,
|
|
@@ -5076,57 +5540,64 @@ function omitRequestFields(body) {
|
|
|
5076
5540
|
thinking,
|
|
5077
5541
|
metadata,
|
|
5078
5542
|
max_tokens,
|
|
5543
|
+
max_completion_tokens,
|
|
5079
5544
|
...rest
|
|
5080
5545
|
} = body;
|
|
5081
5546
|
return rest;
|
|
5082
5547
|
}
|
|
5083
|
-
function
|
|
5548
|
+
function buildProviderDispatchRequestFromIR(input3) {
|
|
5084
5549
|
const fallback = applyCapabilityFallbacks({
|
|
5085
|
-
ir:
|
|
5086
|
-
request:
|
|
5087
|
-
capabilities:
|
|
5550
|
+
ir: input3.ir,
|
|
5551
|
+
request: input3.request,
|
|
5552
|
+
capabilities: input3.capabilities
|
|
5088
5553
|
});
|
|
5089
5554
|
const passthrough = omitRequestFields(fallback.request);
|
|
5090
|
-
|
|
5555
|
+
const dispatchFormat = getDispatchFormatForProfile(input3.interface, input3.compatibilityProfile);
|
|
5556
|
+
if (dispatchFormat === "openai_chat") {
|
|
5091
5557
|
return {
|
|
5558
|
+
dispatchFormat,
|
|
5092
5559
|
diagnostics: fallback.diagnostics,
|
|
5093
5560
|
...passthrough,
|
|
5094
|
-
...
|
|
5095
|
-
model:
|
|
5096
|
-
|
|
5561
|
+
...toOpenAIChatRequest({
|
|
5562
|
+
model: input3.model,
|
|
5563
|
+
max_completion_tokens: fallback.request.max_tokens ?? fallback.request.max_completion_tokens,
|
|
5097
5564
|
stream: fallback.request.stream,
|
|
5098
|
-
metadata: fallback.request.metadata,
|
|
5099
5565
|
tools: fallback.request.tools,
|
|
5566
|
+
tool_choice: fallback.request.tool_choice,
|
|
5100
5567
|
ir: fallback.ir
|
|
5101
5568
|
})
|
|
5102
5569
|
};
|
|
5103
5570
|
}
|
|
5104
5571
|
return {
|
|
5572
|
+
dispatchFormat,
|
|
5105
5573
|
diagnostics: fallback.diagnostics,
|
|
5106
5574
|
...passthrough,
|
|
5107
|
-
...
|
|
5108
|
-
model:
|
|
5109
|
-
|
|
5575
|
+
...toAnthropicMessagesRequest({
|
|
5576
|
+
model: input3.model,
|
|
5577
|
+
max_tokens: fallback.request.max_tokens ?? fallback.request.max_completion_tokens,
|
|
5110
5578
|
stream: fallback.request.stream,
|
|
5579
|
+
metadata: fallback.request.metadata,
|
|
5111
5580
|
tools: fallback.request.tools,
|
|
5112
5581
|
tool_choice: fallback.request.tool_choice,
|
|
5113
5582
|
ir: fallback.ir
|
|
5114
5583
|
})
|
|
5115
5584
|
};
|
|
5116
5585
|
}
|
|
5117
|
-
function
|
|
5118
|
-
const ir = createMessageIR(
|
|
5119
|
-
const { diagnostics, ...body } =
|
|
5120
|
-
model:
|
|
5121
|
-
interface:
|
|
5122
|
-
|
|
5586
|
+
function buildProviderDispatchRequest(input3) {
|
|
5587
|
+
const ir = createMessageIR(input3.request);
|
|
5588
|
+
const { diagnostics, dispatchFormat, ...body } = buildProviderDispatchRequestFromIR({
|
|
5589
|
+
model: input3.model,
|
|
5590
|
+
interface: input3.interface,
|
|
5591
|
+
compatibilityProfile: input3.compatibilityProfile,
|
|
5592
|
+
request: input3.request,
|
|
5123
5593
|
ir,
|
|
5124
|
-
capabilities:
|
|
5594
|
+
capabilities: input3.capabilities
|
|
5125
5595
|
});
|
|
5126
5596
|
return {
|
|
5127
5597
|
ir,
|
|
5128
5598
|
body,
|
|
5129
|
-
diagnostics
|
|
5599
|
+
diagnostics,
|
|
5600
|
+
dispatchFormat
|
|
5130
5601
|
};
|
|
5131
5602
|
}
|
|
5132
5603
|
var init_protocols = __esm({
|
|
@@ -5135,6 +5606,7 @@ var init_protocols = __esm({
|
|
|
5135
5606
|
init_message_ir();
|
|
5136
5607
|
init_anthropic();
|
|
5137
5608
|
init_openai();
|
|
5609
|
+
init_compile();
|
|
5138
5610
|
}
|
|
5139
5611
|
});
|
|
5140
5612
|
|
|
@@ -5202,7 +5674,7 @@ async function run(options = {}) {
|
|
|
5202
5674
|
const hour = pad(date.getHours());
|
|
5203
5675
|
const minute = pad(date.getMinutes());
|
|
5204
5676
|
const seconds = pad(date.getSeconds());
|
|
5205
|
-
return
|
|
5677
|
+
return `ctr-${month}${day}${hour}${minute}${seconds}${index ? `_${index}` : ""}.log`;
|
|
5206
5678
|
};
|
|
5207
5679
|
const loggerConfig = config.LOG !== false ? {
|
|
5208
5680
|
level: config.LOG_LEVEL || "debug",
|
|
@@ -5213,10 +5685,11 @@ async function run(options = {}) {
|
|
|
5213
5685
|
compress: "gzip"
|
|
5214
5686
|
})
|
|
5215
5687
|
} : false;
|
|
5688
|
+
const registry = buildModelRegistry(config);
|
|
5216
5689
|
const server = createServer({
|
|
5217
|
-
|
|
5690
|
+
useJsonFile: false,
|
|
5218
5691
|
initialConfig: {
|
|
5219
|
-
providers:
|
|
5692
|
+
providers: registry.providers,
|
|
5220
5693
|
HOST,
|
|
5221
5694
|
PORT: servicePort,
|
|
5222
5695
|
LOG_FILE: (0, import_path5.join)(
|
|
@@ -5252,9 +5725,10 @@ async function run(options = {}) {
|
|
|
5252
5725
|
initialModel: req.body?.model
|
|
5253
5726
|
});
|
|
5254
5727
|
appendTraceReason(req.governanceTrace, "request_received");
|
|
5255
|
-
const
|
|
5728
|
+
const bypassTriggerRouter = req.headers["x-ctr-smart-router"] === "1";
|
|
5729
|
+
const triggerResult = bypassTriggerRouter ? { matched: false, confidence: 0, analysisTime: 0 } : await triggerRouter.route(req);
|
|
5256
5730
|
req.triggerResult = triggerResult;
|
|
5257
|
-
if (triggerResult.matched && triggerResult.model) {
|
|
5731
|
+
if (!bypassTriggerRouter && triggerResult.matched && triggerResult.model) {
|
|
5258
5732
|
const previousSessionState = req.sessionId ? sessionStateStore.get(req.sessionId) : void 0;
|
|
5259
5733
|
const previousModel = previousSessionState?.lastSuccessfulModel;
|
|
5260
5734
|
const alignmentConfig = config.Governance?.sticky?.alignment;
|
|
@@ -5319,9 +5793,10 @@ async function run(options = {}) {
|
|
|
5319
5793
|
const compiledModel = getCompiledModelRef(config, req.body?.model);
|
|
5320
5794
|
if (compiledModel?.interface && req.body?.messages) {
|
|
5321
5795
|
const originalBody = cloneRequestBody(req.body);
|
|
5322
|
-
const upstream =
|
|
5796
|
+
const upstream = buildProviderDispatchRequest({
|
|
5323
5797
|
model: compiledModel.modelName,
|
|
5324
5798
|
interface: compiledModel.interface,
|
|
5799
|
+
compatibilityProfile: compiledModel.compatibilityProfile,
|
|
5325
5800
|
request: originalBody,
|
|
5326
5801
|
capabilities: compiledModel.capabilities
|
|
5327
5802
|
});
|
|
@@ -5508,7 +5983,7 @@ async function run(options = {}) {
|
|
|
5508
5983
|
event.emit("onSend", req, reply, payload);
|
|
5509
5984
|
return payload;
|
|
5510
5985
|
});
|
|
5511
|
-
server.start();
|
|
5986
|
+
await server.start();
|
|
5512
5987
|
}
|
|
5513
5988
|
var import_fs5, import_promises2, import_os2, import_path5, import_json5, import_node_events, import_rotating_file_stream, event;
|
|
5514
5989
|
var init_index = __esm({
|
|
@@ -5542,34 +6017,43 @@ var init_index = __esm({
|
|
|
5542
6017
|
});
|
|
5543
6018
|
|
|
5544
6019
|
// src/setup/service.ts
|
|
5545
|
-
function decideServiceAction(
|
|
5546
|
-
if (
|
|
6020
|
+
function decideServiceAction(input3) {
|
|
6021
|
+
if (input3.detectedService.kind === "non_self_occupied") {
|
|
5547
6022
|
throw new Error("target port is occupied by another service");
|
|
5548
6023
|
}
|
|
5549
|
-
if (
|
|
6024
|
+
if (input3.detectedService.kind === "none") {
|
|
5550
6025
|
return { kind: "start" };
|
|
5551
6026
|
}
|
|
5552
|
-
if (
|
|
6027
|
+
if (input3.detectedService.kind === "self_unhealthy") {
|
|
5553
6028
|
return { kind: "restart" };
|
|
5554
6029
|
}
|
|
5555
|
-
if (
|
|
5556
|
-
return
|
|
6030
|
+
if (input3.configChanged && input3.detectedService.kind === "self_healthy") {
|
|
6031
|
+
return input3.reloadSupported ? { kind: "reload" } : { kind: "restart" };
|
|
5557
6032
|
}
|
|
5558
6033
|
return { kind: "reuse" };
|
|
5559
6034
|
}
|
|
5560
|
-
async function applyServiceAction(
|
|
5561
|
-
if (
|
|
5562
|
-
await
|
|
6035
|
+
async function applyServiceAction(input3) {
|
|
6036
|
+
if (input3.action.kind === "start") {
|
|
6037
|
+
await input3.executeStart();
|
|
5563
6038
|
}
|
|
5564
|
-
if (
|
|
5565
|
-
await
|
|
6039
|
+
if (input3.action.kind === "reload") {
|
|
6040
|
+
await input3.executeReload();
|
|
5566
6041
|
}
|
|
5567
|
-
if (
|
|
5568
|
-
await
|
|
6042
|
+
if (input3.action.kind === "restart") {
|
|
6043
|
+
await input3.executeRestart();
|
|
5569
6044
|
}
|
|
5570
|
-
const healthy = await
|
|
6045
|
+
const healthy = await input3.verifyHealth();
|
|
5571
6046
|
if (!healthy) {
|
|
5572
|
-
|
|
6047
|
+
if (input3.action.kind === "restart") {
|
|
6048
|
+
throw new Error("service health check failed after restart; the previous ctr service may still be shutting down. Please wait a moment and retry, or run `ctr stop` first.");
|
|
6049
|
+
}
|
|
6050
|
+
if (input3.action.kind === "start") {
|
|
6051
|
+
throw new Error("service health check failed after start; please check whether the target port is already occupied or the configuration is still invalid.");
|
|
6052
|
+
}
|
|
6053
|
+
if (input3.action.kind === "reload") {
|
|
6054
|
+
throw new Error("service health check failed after reload; please retry or run `ctr restart` / `ctr stop` first.");
|
|
6055
|
+
}
|
|
6056
|
+
throw new Error("service health check failed while reusing the current service; please run `ctr status` or `ctr restart` to verify it.");
|
|
5573
6057
|
}
|
|
5574
6058
|
}
|
|
5575
6059
|
var init_service = __esm({
|
|
@@ -5617,9 +6101,12 @@ function inferProtocolFromApiBaseUrl(apiBaseUrl) {
|
|
|
5617
6101
|
}
|
|
5618
6102
|
return "openai";
|
|
5619
6103
|
}
|
|
6104
|
+
function normalizeSegment(value) {
|
|
6105
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
6106
|
+
}
|
|
5620
6107
|
function toModelId(name, model, index) {
|
|
5621
|
-
const normalizedName = name
|
|
5622
|
-
const normalizedModel = model
|
|
6108
|
+
const normalizedName = normalizeSegment(name) || `provider_${index + 1}`;
|
|
6109
|
+
const normalizedModel = normalizeSegment(model);
|
|
5623
6110
|
return normalizedModel ? `${normalizedName}_${normalizedModel}` : normalizedName;
|
|
5624
6111
|
}
|
|
5625
6112
|
function createEmptyDraft() {
|
|
@@ -5634,23 +6121,124 @@ function createNonMigratableResult() {
|
|
|
5634
6121
|
draft: createEmptyDraft(),
|
|
5635
6122
|
skippedFields: [],
|
|
5636
6123
|
needsCompletion: true,
|
|
5637
|
-
missingFields: ["defaultModel", "apiKey"]
|
|
6124
|
+
missingFields: ["defaultModel", "apiKey", "apiBaseUrl"]
|
|
5638
6125
|
};
|
|
5639
6126
|
}
|
|
5640
6127
|
function isLegacyProviderInput(value) {
|
|
5641
6128
|
return typeof value === "object" && value !== null;
|
|
5642
6129
|
}
|
|
5643
|
-
function
|
|
5644
|
-
|
|
5645
|
-
|
|
6130
|
+
function isLegacyRouterInput(value) {
|
|
6131
|
+
return typeof value === "object" && value !== null;
|
|
6132
|
+
}
|
|
6133
|
+
function pushUnique(target, value) {
|
|
6134
|
+
if (!target.includes(value)) {
|
|
6135
|
+
target.push(value);
|
|
5646
6136
|
}
|
|
5647
|
-
|
|
5648
|
-
|
|
6137
|
+
}
|
|
6138
|
+
function readString(value) {
|
|
6139
|
+
return typeof value === "string" ? value.trim() : void 0;
|
|
6140
|
+
}
|
|
6141
|
+
function readBoolean(value) {
|
|
6142
|
+
if (typeof value === "boolean") {
|
|
6143
|
+
return value;
|
|
6144
|
+
}
|
|
6145
|
+
if (typeof value === "string") {
|
|
6146
|
+
if (value.toLowerCase() === "true") {
|
|
6147
|
+
return true;
|
|
6148
|
+
}
|
|
6149
|
+
if (value.toLowerCase() === "false") {
|
|
6150
|
+
return false;
|
|
6151
|
+
}
|
|
6152
|
+
}
|
|
6153
|
+
return void 0;
|
|
6154
|
+
}
|
|
6155
|
+
function readFiniteNumber(value) {
|
|
6156
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
6157
|
+
return value;
|
|
6158
|
+
}
|
|
6159
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
6160
|
+
const parsed = Number(value);
|
|
6161
|
+
if (Number.isFinite(parsed)) {
|
|
6162
|
+
return parsed;
|
|
6163
|
+
}
|
|
6164
|
+
}
|
|
6165
|
+
return void 0;
|
|
6166
|
+
}
|
|
6167
|
+
function extractSupportedTopLevelConfig(input3, consumedTopLevelFields) {
|
|
6168
|
+
const nextConfig = {};
|
|
6169
|
+
const hasOwn = (key) => Object.prototype.hasOwnProperty.call(input3, key);
|
|
6170
|
+
const host = readString(input3.HOST);
|
|
6171
|
+
if (hasOwn("HOST")) {
|
|
6172
|
+
nextConfig.HOST = host;
|
|
6173
|
+
consumedTopLevelFields.add("HOST");
|
|
6174
|
+
}
|
|
6175
|
+
const port = readFiniteNumber(input3.PORT);
|
|
6176
|
+
if (hasOwn("PORT") && port !== void 0) {
|
|
6177
|
+
nextConfig.PORT = port;
|
|
6178
|
+
consumedTopLevelFields.add("PORT");
|
|
6179
|
+
}
|
|
6180
|
+
const log2 = readBoolean(input3.LOG);
|
|
6181
|
+
if (hasOwn("LOG") && log2 !== void 0) {
|
|
6182
|
+
nextConfig.LOG = log2;
|
|
6183
|
+
consumedTopLevelFields.add("LOG");
|
|
6184
|
+
}
|
|
6185
|
+
const logLevel = readString(input3.LOG_LEVEL);
|
|
6186
|
+
if (hasOwn("LOG_LEVEL")) {
|
|
6187
|
+
nextConfig.LOG_LEVEL = logLevel;
|
|
6188
|
+
consumedTopLevelFields.add("LOG_LEVEL");
|
|
6189
|
+
}
|
|
6190
|
+
const apiTimeoutMs = readFiniteNumber(input3.API_TIMEOUT_MS);
|
|
6191
|
+
if (hasOwn("API_TIMEOUT_MS") && apiTimeoutMs !== void 0) {
|
|
6192
|
+
nextConfig.API_TIMEOUT_MS = apiTimeoutMs;
|
|
6193
|
+
consumedTopLevelFields.add("API_TIMEOUT_MS");
|
|
6194
|
+
}
|
|
6195
|
+
const proxyUrl = readString(input3.PROXY_URL);
|
|
6196
|
+
if (hasOwn("PROXY_URL")) {
|
|
6197
|
+
nextConfig.PROXY_URL = proxyUrl;
|
|
6198
|
+
consumedTopLevelFields.add("PROXY_URL");
|
|
6199
|
+
}
|
|
6200
|
+
const apiKey = readString(input3.APIKEY);
|
|
6201
|
+
if (hasOwn("APIKEY")) {
|
|
6202
|
+
nextConfig.APIKEY = apiKey;
|
|
6203
|
+
consumedTopLevelFields.add("APIKEY");
|
|
6204
|
+
}
|
|
6205
|
+
const customRouterPath = readString(input3.CUSTOM_ROUTER_PATH);
|
|
6206
|
+
if (hasOwn("CUSTOM_ROUTER_PATH")) {
|
|
6207
|
+
nextConfig.CUSTOM_ROUTER_PATH = customRouterPath;
|
|
6208
|
+
consumedTopLevelFields.add("CUSTOM_ROUTER_PATH");
|
|
6209
|
+
}
|
|
6210
|
+
return nextConfig;
|
|
6211
|
+
}
|
|
6212
|
+
function normalizeLegacyConfig(input3) {
|
|
6213
|
+
const lowerProviders = Array.isArray(input3.providers) && input3.providers.every(isLegacyProviderInput) ? input3.providers : null;
|
|
6214
|
+
const upperProviders = Array.isArray(input3.Providers) && input3.Providers.every(isLegacyProviderInput) ? input3.Providers : null;
|
|
6215
|
+
const providerKey = lowerProviders && lowerProviders.length > 0 ? "providers" : upperProviders && upperProviders.length > 0 ? "Providers" : lowerProviders ? "providers" : upperProviders ? "Providers" : null;
|
|
6216
|
+
if (!providerKey) {
|
|
6217
|
+
return null;
|
|
6218
|
+
}
|
|
6219
|
+
const rawProviders = providerKey === "providers" ? lowerProviders : upperProviders;
|
|
6220
|
+
if (!rawProviders) {
|
|
6221
|
+
return null;
|
|
5649
6222
|
}
|
|
5650
6223
|
const skippedFields = [];
|
|
5651
|
-
const
|
|
6224
|
+
const alternateProviderKey = providerKey === "providers" ? "Providers" : "providers";
|
|
6225
|
+
const alternateDefaultKey = providerKey === "providers" ? "Router" : "default";
|
|
6226
|
+
const alternateDefaultValue = providerKey === "providers" ? input3.Router : input3.default;
|
|
6227
|
+
const alternateProviders = providerKey === "providers" ? upperProviders : lowerProviders;
|
|
6228
|
+
if (alternateProviders !== null) {
|
|
6229
|
+
pushUnique(skippedFields, alternateProviderKey);
|
|
6230
|
+
}
|
|
6231
|
+
if (alternateDefaultValue !== void 0) {
|
|
6232
|
+
pushUnique(skippedFields, alternateDefaultKey);
|
|
6233
|
+
}
|
|
6234
|
+
const consumedTopLevelFields = /* @__PURE__ */ new Set([providerKey]);
|
|
6235
|
+
const supportedTopLevelConfig = extractSupportedTopLevelConfig(input3, consumedTopLevelFields);
|
|
6236
|
+
const providers = rawProviders.map((provider, index) => {
|
|
5652
6237
|
if (provider.transformer !== void 0) {
|
|
5653
|
-
skippedFields
|
|
6238
|
+
pushUnique(skippedFields, `${providerKey}[${index}].transformer`);
|
|
6239
|
+
}
|
|
6240
|
+
if (provider.headers !== void 0) {
|
|
6241
|
+
pushUnique(skippedFields, `${providerKey}[${index}].headers`);
|
|
5654
6242
|
}
|
|
5655
6243
|
return {
|
|
5656
6244
|
name: provider.name ?? "",
|
|
@@ -5659,41 +6247,119 @@ function migrateLegacyConfig(input2) {
|
|
|
5659
6247
|
models: Array.isArray(provider.models) ? provider.models : []
|
|
5660
6248
|
};
|
|
5661
6249
|
});
|
|
5662
|
-
|
|
6250
|
+
let defaultRoute;
|
|
6251
|
+
const routeSlots = {};
|
|
6252
|
+
if (providerKey === "providers") {
|
|
6253
|
+
consumedTopLevelFields.add("default");
|
|
6254
|
+
defaultRoute = typeof input3.default === "string" ? input3.default : void 0;
|
|
6255
|
+
} else {
|
|
6256
|
+
consumedTopLevelFields.add("Router");
|
|
6257
|
+
if (isLegacyRouterInput(input3.Router)) {
|
|
6258
|
+
defaultRoute = typeof input3.Router.default === "string" ? input3.Router.default : void 0;
|
|
6259
|
+
routeSlots.background = readString(input3.Router.background);
|
|
6260
|
+
routeSlots.think = readString(input3.Router.think);
|
|
6261
|
+
routeSlots.longContext = readString(input3.Router.longContext);
|
|
6262
|
+
routeSlots.webSearch = readString(input3.Router.webSearch);
|
|
6263
|
+
routeSlots.longContextThreshold = readFiniteNumber(input3.Router.longContextThreshold);
|
|
6264
|
+
}
|
|
6265
|
+
}
|
|
6266
|
+
for (const key of Object.keys(input3)) {
|
|
6267
|
+
if (consumedTopLevelFields.has(key)) {
|
|
6268
|
+
continue;
|
|
6269
|
+
}
|
|
6270
|
+
pushUnique(skippedFields, key);
|
|
6271
|
+
}
|
|
6272
|
+
return {
|
|
6273
|
+
providers,
|
|
6274
|
+
defaultRoute,
|
|
6275
|
+
routeSlots,
|
|
6276
|
+
supportedTopLevelConfig,
|
|
6277
|
+
skippedFields
|
|
6278
|
+
};
|
|
6279
|
+
}
|
|
6280
|
+
function migrateLegacyConfig(input3) {
|
|
6281
|
+
const normalized = normalizeLegacyConfig(input3);
|
|
6282
|
+
if (!normalized) {
|
|
6283
|
+
return createNonMigratableResult();
|
|
6284
|
+
}
|
|
6285
|
+
const rawEntries = normalized.providers.flatMap(
|
|
5663
6286
|
(provider, providerIndex) => (provider.models.length ? provider.models : [""]).map((model) => ({
|
|
5664
|
-
|
|
6287
|
+
candidateId: toModelId(provider.name, model, providerIndex),
|
|
5665
6288
|
api: provider.api_base_url,
|
|
5666
6289
|
api_base_url: provider.api_base_url,
|
|
5667
6290
|
key: provider.api_key,
|
|
5668
6291
|
api_key: provider.api_key,
|
|
5669
6292
|
interface: inferProtocolFromApiBaseUrl(provider.api_base_url),
|
|
5670
6293
|
protocol: inferProtocolFromApiBaseUrl(provider.api_base_url),
|
|
5671
|
-
model
|
|
5672
|
-
|
|
5673
|
-
|
|
5674
|
-
|
|
5675
|
-
|
|
5676
|
-
|
|
5677
|
-
const
|
|
5678
|
-
|
|
5679
|
-
|
|
5680
|
-
|
|
5681
|
-
|
|
5682
|
-
|
|
6294
|
+
model,
|
|
6295
|
+
providerName: provider.name
|
|
6296
|
+
})).filter((item) => item.model)
|
|
6297
|
+
);
|
|
6298
|
+
const seenIds = /* @__PURE__ */ new Map();
|
|
6299
|
+
const routeLookup = /* @__PURE__ */ new Map();
|
|
6300
|
+
const models = rawEntries.map((entry) => {
|
|
6301
|
+
const count = seenIds.get(entry.candidateId) ?? 0;
|
|
6302
|
+
seenIds.set(entry.candidateId, count + 1);
|
|
6303
|
+
const finalId = count === 0 ? entry.candidateId : `${entry.candidateId}_${count + 1}`;
|
|
6304
|
+
routeLookup.set(`${entry.providerName.trim()},${entry.model}`, finalId);
|
|
6305
|
+
return {
|
|
6306
|
+
id: finalId,
|
|
6307
|
+
api: entry.api,
|
|
6308
|
+
api_base_url: entry.api_base_url,
|
|
6309
|
+
key: entry.key,
|
|
6310
|
+
api_key: entry.api_key,
|
|
6311
|
+
interface: entry.interface,
|
|
6312
|
+
protocol: entry.protocol,
|
|
6313
|
+
model: entry.model
|
|
6314
|
+
};
|
|
6315
|
+
});
|
|
6316
|
+
const resolveLegacyRoute = (ref, fieldName) => {
|
|
6317
|
+
if (!ref) {
|
|
6318
|
+
return void 0;
|
|
6319
|
+
}
|
|
6320
|
+
const [rawProviderName, rawModelName] = String(ref).split(",");
|
|
6321
|
+
const providerName = (rawProviderName ?? "").trim();
|
|
6322
|
+
const modelName = (rawModelName ?? "").trim();
|
|
6323
|
+
const fromLookup = routeLookup.get(`${providerName},${modelName}`);
|
|
6324
|
+
if (fromLookup) {
|
|
6325
|
+
return fromLookup;
|
|
6326
|
+
}
|
|
6327
|
+
pushUnique(normalized.skippedFields, fieldName);
|
|
6328
|
+
return void 0;
|
|
6329
|
+
};
|
|
6330
|
+
const hasLegacyDefaultRoute = typeof normalized.defaultRoute === "string" && normalized.defaultRoute.length > 0;
|
|
6331
|
+
const defaultModelId = hasLegacyDefaultRoute ? resolveLegacyRoute(normalized.defaultRoute, "Router.default") : void 0;
|
|
6332
|
+
const backgroundModelId = resolveLegacyRoute(normalized.routeSlots.background, "Router.background");
|
|
6333
|
+
const thinkModelId = resolveLegacyRoute(normalized.routeSlots.think, "Router.think");
|
|
6334
|
+
const longContextModelId = resolveLegacyRoute(normalized.routeSlots.longContext, "Router.longContext");
|
|
6335
|
+
const webSearchModelId = resolveLegacyRoute(normalized.routeSlots.webSearch, "Router.webSearch");
|
|
6336
|
+
const hasMissingApiKey = normalized.providers.some((provider) => provider.api_key.length === 0);
|
|
6337
|
+
const hasMissingApiBaseUrl = normalized.providers.some((provider) => (provider.api_base_url?.trim() ?? "").length === 0);
|
|
5683
6338
|
const missingFields = [];
|
|
5684
|
-
if (!
|
|
6339
|
+
if (!defaultModelId) {
|
|
5685
6340
|
missingFields.push("defaultModel");
|
|
5686
6341
|
}
|
|
5687
6342
|
if (hasMissingApiKey) {
|
|
5688
6343
|
missingFields.push("apiKey");
|
|
5689
6344
|
}
|
|
6345
|
+
if (hasMissingApiBaseUrl) {
|
|
6346
|
+
missingFields.push("apiBaseUrl");
|
|
6347
|
+
}
|
|
5690
6348
|
return {
|
|
5691
6349
|
draft: {
|
|
6350
|
+
...normalized.supportedTopLevelConfig,
|
|
5692
6351
|
Providers: [],
|
|
5693
6352
|
Models: models,
|
|
5694
|
-
Router:
|
|
6353
|
+
Router: {
|
|
6354
|
+
...defaultModelId ? { default: defaultModelId } : {},
|
|
6355
|
+
...backgroundModelId ? { background: backgroundModelId } : {},
|
|
6356
|
+
...thinkModelId ? { think: thinkModelId } : {},
|
|
6357
|
+
...longContextModelId ? { longContext: longContextModelId } : {},
|
|
6358
|
+
...normalized.routeSlots.longContextThreshold !== void 0 ? { longContextThreshold: normalized.routeSlots.longContextThreshold } : {},
|
|
6359
|
+
...webSearchModelId ? { webSearch: webSearchModelId } : {}
|
|
6360
|
+
}
|
|
5695
6361
|
},
|
|
5696
|
-
skippedFields,
|
|
6362
|
+
skippedFields: normalized.skippedFields,
|
|
5697
6363
|
needsCompletion: missingFields.length > 0,
|
|
5698
6364
|
missingFields
|
|
5699
6365
|
};
|
|
@@ -5758,11 +6424,12 @@ function getProviderPreset2(key) {
|
|
|
5758
6424
|
api: preset.api,
|
|
5759
6425
|
api_base_url: preset.api_base_url,
|
|
5760
6426
|
interface: preset.interface,
|
|
5761
|
-
protocol: preset.protocol
|
|
6427
|
+
protocol: preset.protocol,
|
|
6428
|
+
default_thinking: preset.default_thinking
|
|
5762
6429
|
};
|
|
5763
6430
|
}
|
|
5764
|
-
function buildMinimalConfig(
|
|
5765
|
-
const providers =
|
|
6431
|
+
function buildMinimalConfig(input3) {
|
|
6432
|
+
const providers = input3.providers;
|
|
5766
6433
|
const models = providers.map((p) => {
|
|
5767
6434
|
const preset = p.preset ? getProviderPreset2(p.preset) : void 0;
|
|
5768
6435
|
const modelDraft = {
|
|
@@ -5782,15 +6449,15 @@ function buildMinimalConfig(input2) {
|
|
|
5782
6449
|
}
|
|
5783
6450
|
return modelDraft;
|
|
5784
6451
|
});
|
|
5785
|
-
let defaultModel =
|
|
5786
|
-
if (
|
|
5787
|
-
const [providerName, modelName] =
|
|
6452
|
+
let defaultModel = input3.defaultModel?.trim();
|
|
6453
|
+
if (input3.defaultModel && input3.defaultModel.includes(",")) {
|
|
6454
|
+
const [providerName, modelName] = input3.defaultModel.split(",");
|
|
5788
6455
|
const matched = models.find((item) => item.id === providerName && item.model === modelName);
|
|
5789
6456
|
if (matched) {
|
|
5790
6457
|
defaultModel = matched.id;
|
|
5791
6458
|
}
|
|
5792
6459
|
}
|
|
5793
|
-
if (
|
|
6460
|
+
if (input3.defaultModel === void 0 && models.length > 0) {
|
|
5794
6461
|
const firstModelId = models[0].id;
|
|
5795
6462
|
if (firstModelId && models[0].model) {
|
|
5796
6463
|
defaultModel = firstModelId;
|
|
@@ -5804,31 +6471,68 @@ function buildMinimalConfig(input2) {
|
|
|
5804
6471
|
Router: defaultModel ? { default: defaultModel } : {}
|
|
5805
6472
|
};
|
|
5806
6473
|
}
|
|
6474
|
+
function buildUsableMinimalTemplateConfig() {
|
|
6475
|
+
const openRouterPreset = getProviderPreset("openrouter");
|
|
6476
|
+
const modelId = openRouterPreset?.suggested_id ?? "sonnet";
|
|
6477
|
+
const modelName = openRouterPreset?.default_model ?? "anthropic/claude-sonnet-4";
|
|
6478
|
+
const thinking = openRouterPreset?.default_thinking ?? "auto";
|
|
6479
|
+
const draft = buildMinimalConfig({
|
|
6480
|
+
providers: [
|
|
6481
|
+
{
|
|
6482
|
+
name: "openrouter",
|
|
6483
|
+
model_id: modelId,
|
|
6484
|
+
preset: "openrouter",
|
|
6485
|
+
api_key: "sk-xxx",
|
|
6486
|
+
models: [modelName]
|
|
6487
|
+
}
|
|
6488
|
+
],
|
|
6489
|
+
defaultModel: modelId
|
|
6490
|
+
});
|
|
6491
|
+
const primaryModel = draft.Models?.[0];
|
|
6492
|
+
return {
|
|
6493
|
+
HOST: DEFAULT_CONFIG2.HOST,
|
|
6494
|
+
PORT: DEFAULT_CONFIG2.PORT,
|
|
6495
|
+
LOG: DEFAULT_CONFIG2.LOG,
|
|
6496
|
+
LOG_LEVEL: DEFAULT_CONFIG2.LOG_LEVEL,
|
|
6497
|
+
Models: primaryModel ? [
|
|
6498
|
+
{
|
|
6499
|
+
id: primaryModel.id,
|
|
6500
|
+
api: primaryModel.api,
|
|
6501
|
+
key: primaryModel.key,
|
|
6502
|
+
interface: primaryModel.interface,
|
|
6503
|
+
model: primaryModel.model,
|
|
6504
|
+
thinking
|
|
6505
|
+
}
|
|
6506
|
+
] : [],
|
|
6507
|
+
Router: draft.Router
|
|
6508
|
+
};
|
|
6509
|
+
}
|
|
5807
6510
|
var init_templates = __esm({
|
|
5808
6511
|
"src/setup/templates.ts"() {
|
|
5809
6512
|
"use strict";
|
|
6513
|
+
init_constants();
|
|
5810
6514
|
init_provider_presets();
|
|
5811
6515
|
}
|
|
5812
6516
|
});
|
|
5813
6517
|
|
|
5814
6518
|
// src/setup/persist.ts
|
|
5815
|
-
async function persistSetupConfig(
|
|
5816
|
-
const errors =
|
|
6519
|
+
async function persistSetupConfig(input3) {
|
|
6520
|
+
const errors = input3.validateConfig(input3.config);
|
|
5817
6521
|
if (errors.length > 0) {
|
|
5818
6522
|
throw new Error("config validation failed");
|
|
5819
6523
|
}
|
|
5820
6524
|
let backupPath;
|
|
5821
|
-
if (
|
|
5822
|
-
const createdBackupPath = await
|
|
6525
|
+
if (input3.hasExistingConfig) {
|
|
6526
|
+
const createdBackupPath = await input3.backupCurrentConfig();
|
|
5823
6527
|
if (!createdBackupPath) {
|
|
5824
6528
|
throw new Error("failed to back up existing config");
|
|
5825
6529
|
}
|
|
5826
6530
|
backupPath = createdBackupPath;
|
|
5827
6531
|
}
|
|
5828
|
-
await
|
|
6532
|
+
await input3.writeConfig(input3.config);
|
|
5829
6533
|
return {
|
|
5830
6534
|
configChanged: true,
|
|
5831
|
-
configPath:
|
|
6535
|
+
configPath: input3.currentConfigPath,
|
|
5832
6536
|
backupPath
|
|
5833
6537
|
};
|
|
5834
6538
|
}
|
|
@@ -5877,8 +6581,8 @@ function ensureLegacyFlow(detection, legacyConfigAction) {
|
|
|
5877
6581
|
function invalidAction() {
|
|
5878
6582
|
return invalidCurrentAction();
|
|
5879
6583
|
}
|
|
5880
|
-
function decideSetupBranch(
|
|
5881
|
-
const { detection, currentConfigAction, legacyConfigAction } =
|
|
6584
|
+
function decideSetupBranch(input3) {
|
|
6585
|
+
const { detection, currentConfigAction, legacyConfigAction } = input3;
|
|
5882
6586
|
if (currentConfigAction === "cancel") {
|
|
5883
6587
|
ensureNoLegacyAction(legacyConfigAction);
|
|
5884
6588
|
return { kind: "cancelled" };
|
|
@@ -5931,14 +6635,36 @@ function getTargetConfigPath(detection) {
|
|
|
5931
6635
|
}
|
|
5932
6636
|
return CONFIG_FILE;
|
|
5933
6637
|
}
|
|
5934
|
-
|
|
5935
|
-
|
|
6638
|
+
function getLegacyProviderCount(input3) {
|
|
6639
|
+
if (typeof input3 !== "object" || input3 === null) {
|
|
6640
|
+
return 0;
|
|
6641
|
+
}
|
|
6642
|
+
const legacyConfig = input3;
|
|
6643
|
+
if (Array.isArray(legacyConfig.providers)) {
|
|
6644
|
+
return legacyConfig.providers.length;
|
|
6645
|
+
}
|
|
6646
|
+
if (Array.isArray(legacyConfig.Providers)) {
|
|
6647
|
+
return legacyConfig.Providers.length;
|
|
6648
|
+
}
|
|
6649
|
+
return 0;
|
|
6650
|
+
}
|
|
6651
|
+
function getMigratedModelCount(draft) {
|
|
6652
|
+
if (Array.isArray(draft.Models)) {
|
|
6653
|
+
return draft.Models.length;
|
|
6654
|
+
}
|
|
6655
|
+
if (Array.isArray(draft.Providers)) {
|
|
6656
|
+
return draft.Providers.reduce((total, provider) => total + (provider.models?.length ?? 0), 0);
|
|
6657
|
+
}
|
|
6658
|
+
return 0;
|
|
6659
|
+
}
|
|
6660
|
+
async function runSetup(deps) {
|
|
6661
|
+
const detection = await deps.detectSetupEnvironment();
|
|
5936
6662
|
const currentConfigAction = await deps.chooseCurrentConfigAction({
|
|
5937
6663
|
currentConfig: detection.currentConfig,
|
|
5938
6664
|
legacyConfig: detection.legacyConfig
|
|
5939
6665
|
});
|
|
5940
6666
|
let legacyConfigAction;
|
|
5941
|
-
if (currentConfigAction === "create" || currentConfigAction === "overwrite") {
|
|
6667
|
+
if (currentConfigAction === "create" || currentConfigAction === "overwrite" || currentConfigAction === "fresh") {
|
|
5942
6668
|
if (detection.legacyConfig.kind === "found" || detection.legacyConfig.kind === "read_error") {
|
|
5943
6669
|
legacyConfigAction = await deps.chooseLegacyConfigAction({
|
|
5944
6670
|
legacyConfig: detection.legacyConfig
|
|
@@ -6012,6 +6738,16 @@ async function runSetup(deps) {
|
|
|
6012
6738
|
throw new Error("migrate_legacy requires legacy config");
|
|
6013
6739
|
}
|
|
6014
6740
|
const migrated = deps.migrateLegacyConfig(detection.legacyConfig.config);
|
|
6741
|
+
deps.io.info(`\u5DF2\u8BC6\u522B\u65E7\u914D\u7F6E\u4E2D\u7684 ${getLegacyProviderCount(detection.legacyConfig.config)} \u4E2A provider\u3002`);
|
|
6742
|
+
deps.io.info(`\u5DF2\u4ECE\u65E7\u914D\u7F6E\u8FC1\u79FB ${getMigratedModelCount(migrated.draft)} \u4E2A\u6A21\u578B\u3002`);
|
|
6743
|
+
if (migrated.draft.Router.default) {
|
|
6744
|
+
deps.io.info(`\u8FC1\u79FB\u540E\u7684\u9ED8\u8BA4\u6A21\u578B\uFF1A${migrated.draft.Router.default}`);
|
|
6745
|
+
} else {
|
|
6746
|
+
deps.io.info("\u8FC1\u79FB\u540E\u7684\u9ED8\u8BA4\u6A21\u578B\u4ECD\u9700\u8865\u5168\u3002");
|
|
6747
|
+
}
|
|
6748
|
+
if (migrated.skippedFields.length > 0) {
|
|
6749
|
+
deps.io.info(`\u4EE5\u4E0B\u65E7\u5B57\u6BB5\u672A\u81EA\u52A8\u8FC1\u79FB\uFF1A${migrated.skippedFields.join(", ")}`);
|
|
6750
|
+
}
|
|
6015
6751
|
let finalDraft = migrated.draft;
|
|
6016
6752
|
if (migrated.needsCompletion) {
|
|
6017
6753
|
finalDraft = await deps.completeDraft({
|
|
@@ -6046,6 +6782,96 @@ var init_setup = __esm({
|
|
|
6046
6782
|
|
|
6047
6783
|
// src/setup/index.ts
|
|
6048
6784
|
function createConsoleIO() {
|
|
6785
|
+
if (process.env.CTR_SETUP_FORCE_SCRIPTED_INPUT === "1") {
|
|
6786
|
+
const scriptedInput = (0, import_fs6.readFileSync)(0, "utf-8");
|
|
6787
|
+
const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter((item) => item.length > 0);
|
|
6788
|
+
let cursor = 0;
|
|
6789
|
+
const nextAnswer = async () => answers[cursor++] ?? "";
|
|
6790
|
+
return {
|
|
6791
|
+
async choose(message, options) {
|
|
6792
|
+
import_process.stdout.write(`${message}
|
|
6793
|
+
`);
|
|
6794
|
+
options.forEach((option, index) => {
|
|
6795
|
+
import_process.stdout.write(` ${index + 1}. ${option}
|
|
6796
|
+
`);
|
|
6797
|
+
});
|
|
6798
|
+
const answer = await nextAnswer();
|
|
6799
|
+
const pickedIndex = Number(answer);
|
|
6800
|
+
if (Number.isInteger(pickedIndex) && pickedIndex >= 1 && pickedIndex <= options.length) {
|
|
6801
|
+
return options[pickedIndex - 1];
|
|
6802
|
+
}
|
|
6803
|
+
const matched = options.find((option) => option === answer);
|
|
6804
|
+
if (matched) {
|
|
6805
|
+
return matched;
|
|
6806
|
+
}
|
|
6807
|
+
throw new Error(`invalid scripted answer for "${message}": ${answer || "<empty>"}`);
|
|
6808
|
+
},
|
|
6809
|
+
async input(message, defaultValue) {
|
|
6810
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
6811
|
+
import_process.stdout.write(`${message}${suffix}: `);
|
|
6812
|
+
const answer = await nextAnswer();
|
|
6813
|
+
return answer || defaultValue || "";
|
|
6814
|
+
},
|
|
6815
|
+
info(message) {
|
|
6816
|
+
import_process.stdout.write(`${message}
|
|
6817
|
+
`);
|
|
6818
|
+
},
|
|
6819
|
+
close() {
|
|
6820
|
+
}
|
|
6821
|
+
};
|
|
6822
|
+
}
|
|
6823
|
+
if (!import_process.stdin.isTTY) {
|
|
6824
|
+
let loaded = false;
|
|
6825
|
+
let answers = [];
|
|
6826
|
+
let cursor = 0;
|
|
6827
|
+
const loadAnswers = async () => {
|
|
6828
|
+
if (loaded) {
|
|
6829
|
+
return;
|
|
6830
|
+
}
|
|
6831
|
+
loaded = true;
|
|
6832
|
+
const chunks = [];
|
|
6833
|
+
for await (const chunk of import_process.stdin) {
|
|
6834
|
+
chunks.push(String(chunk));
|
|
6835
|
+
}
|
|
6836
|
+
answers = chunks.join("").split(/\r?\n/).map((item) => item.trim()).filter((item) => item.length > 0);
|
|
6837
|
+
};
|
|
6838
|
+
const nextAnswer = async () => {
|
|
6839
|
+
await loadAnswers();
|
|
6840
|
+
return answers[cursor++] ?? "";
|
|
6841
|
+
};
|
|
6842
|
+
return {
|
|
6843
|
+
async choose(message, options) {
|
|
6844
|
+
import_process.stdout.write(`${message}
|
|
6845
|
+
`);
|
|
6846
|
+
options.forEach((option, index) => {
|
|
6847
|
+
import_process.stdout.write(` ${index + 1}. ${option}
|
|
6848
|
+
`);
|
|
6849
|
+
});
|
|
6850
|
+
const answer = await nextAnswer();
|
|
6851
|
+
const pickedIndex = Number(answer);
|
|
6852
|
+
if (Number.isInteger(pickedIndex) && pickedIndex >= 1 && pickedIndex <= options.length) {
|
|
6853
|
+
return options[pickedIndex - 1];
|
|
6854
|
+
}
|
|
6855
|
+
const matched = options.find((option) => option === answer);
|
|
6856
|
+
if (matched) {
|
|
6857
|
+
return matched;
|
|
6858
|
+
}
|
|
6859
|
+
throw new Error(`invalid scripted answer for "${message}": ${answer || "<empty>"}`);
|
|
6860
|
+
},
|
|
6861
|
+
async input(message, defaultValue) {
|
|
6862
|
+
const suffix = defaultValue ? ` (${defaultValue})` : "";
|
|
6863
|
+
import_process.stdout.write(`${message}${suffix}: `);
|
|
6864
|
+
const answer = await nextAnswer();
|
|
6865
|
+
return answer || defaultValue || "";
|
|
6866
|
+
},
|
|
6867
|
+
info(message) {
|
|
6868
|
+
import_process.stdout.write(`${message}
|
|
6869
|
+
`);
|
|
6870
|
+
},
|
|
6871
|
+
close() {
|
|
6872
|
+
}
|
|
6873
|
+
};
|
|
6874
|
+
}
|
|
6049
6875
|
const rl = (0, import_promises3.createInterface)({ input: import_process.stdin, output: import_process.stdout });
|
|
6050
6876
|
const ask = async (message) => {
|
|
6051
6877
|
const answer = await rl.question(message);
|
|
@@ -6080,6 +6906,9 @@ function createConsoleIO() {
|
|
|
6080
6906
|
info(message) {
|
|
6081
6907
|
import_process.stdout.write(`${message}
|
|
6082
6908
|
`);
|
|
6909
|
+
},
|
|
6910
|
+
close() {
|
|
6911
|
+
rl.close();
|
|
6083
6912
|
}
|
|
6084
6913
|
};
|
|
6085
6914
|
}
|
|
@@ -6090,14 +6919,79 @@ function readStructuredConfigFile(filePath) {
|
|
|
6090
6919
|
}
|
|
6091
6920
|
return import_js_yaml.default.load(content);
|
|
6092
6921
|
}
|
|
6922
|
+
function getCurrentRuntimeFields() {
|
|
6923
|
+
const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
|
|
6924
|
+
const currentPath = candidates.find((filePath) => (0, import_fs6.existsSync)(filePath));
|
|
6925
|
+
if (!currentPath) {
|
|
6926
|
+
return {};
|
|
6927
|
+
}
|
|
6928
|
+
try {
|
|
6929
|
+
const config = readStructuredConfigFile(currentPath);
|
|
6930
|
+
if (!config || typeof config !== "object") {
|
|
6931
|
+
return {};
|
|
6932
|
+
}
|
|
6933
|
+
const fields = {};
|
|
6934
|
+
for (const key of ["HOST", "PORT", "LOG", "LOG_LEVEL", "API_TIMEOUT_MS"]) {
|
|
6935
|
+
if (config[key] !== void 0) {
|
|
6936
|
+
fields[key] = config[key];
|
|
6937
|
+
}
|
|
6938
|
+
}
|
|
6939
|
+
return fields;
|
|
6940
|
+
} catch {
|
|
6941
|
+
return {};
|
|
6942
|
+
}
|
|
6943
|
+
}
|
|
6944
|
+
function getConfiguredPortFromCurrentFiles() {
|
|
6945
|
+
const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
|
|
6946
|
+
const currentPath = candidates.find((filePath) => (0, import_fs6.existsSync)(filePath));
|
|
6947
|
+
if (!currentPath) {
|
|
6948
|
+
return DEFAULT_CONFIG2.PORT;
|
|
6949
|
+
}
|
|
6950
|
+
try {
|
|
6951
|
+
const config = readStructuredConfigFile(currentPath);
|
|
6952
|
+
if (config && typeof config.PORT === "number" && Number.isFinite(config.PORT) && config.PORT > 0) {
|
|
6953
|
+
return config.PORT;
|
|
6954
|
+
}
|
|
6955
|
+
} catch {
|
|
6956
|
+
}
|
|
6957
|
+
return DEFAULT_CONFIG2.PORT;
|
|
6958
|
+
}
|
|
6959
|
+
async function getAvailablePort() {
|
|
6960
|
+
const server = (0, import_net2.createServer)();
|
|
6961
|
+
try {
|
|
6962
|
+
return await new Promise((resolve, reject) => {
|
|
6963
|
+
server.once("error", reject);
|
|
6964
|
+
server.listen(0, "127.0.0.1", () => {
|
|
6965
|
+
const address = server.address();
|
|
6966
|
+
if (!address || typeof address === "string") {
|
|
6967
|
+
reject(new Error("failed to resolve available port"));
|
|
6968
|
+
return;
|
|
6969
|
+
}
|
|
6970
|
+
resolve(address.port);
|
|
6971
|
+
});
|
|
6972
|
+
});
|
|
6973
|
+
} finally {
|
|
6974
|
+
if (server.listening) {
|
|
6975
|
+
await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
|
|
6976
|
+
}
|
|
6977
|
+
}
|
|
6978
|
+
}
|
|
6979
|
+
function readLegacyConfigFile(filePath) {
|
|
6980
|
+
const content = (0, import_fs6.readFileSync)(filePath, "utf-8");
|
|
6981
|
+
if (filePath.endsWith(".json")) {
|
|
6982
|
+
return import_json52.default.parse(content);
|
|
6983
|
+
}
|
|
6984
|
+
return import_js_yaml.default.load(content);
|
|
6985
|
+
}
|
|
6093
6986
|
async function readLegacyConfig(deps = {}) {
|
|
6094
6987
|
const baseHomeDir = deps.homeDir || (0, import_os3.homedir)();
|
|
6095
6988
|
const exists = deps.exists || import_fs6.existsSync;
|
|
6096
|
-
const readConfig = deps.readConfig ||
|
|
6989
|
+
const readConfig = deps.readConfig || readLegacyConfigFile;
|
|
6097
6990
|
const overridePath = process.env.CTR_SETUP_LEGACY_CONFIG_PATH;
|
|
6098
6991
|
const candidatePaths = overridePath ? [overridePath] : [
|
|
6099
6992
|
(0, import_path6.join)(baseHomeDir, ".ccr", "config.yaml"),
|
|
6100
|
-
(0, import_path6.join)(baseHomeDir, ".claude-code-router", "config.yaml")
|
|
6993
|
+
(0, import_path6.join)(baseHomeDir, ".claude-code-router", "config.yaml"),
|
|
6994
|
+
(0, import_path6.join)(baseHomeDir, ".claude-code-router", "config.json")
|
|
6101
6995
|
];
|
|
6102
6996
|
const legacyPath = candidatePaths.find((filePath) => exists(filePath));
|
|
6103
6997
|
if (!legacyPath) {
|
|
@@ -6140,16 +7034,30 @@ async function readCurrentConfig() {
|
|
|
6140
7034
|
}
|
|
6141
7035
|
}
|
|
6142
7036
|
async function probeService() {
|
|
6143
|
-
const
|
|
6144
|
-
|
|
7037
|
+
const port = getConfiguredPortFromCurrentFiles();
|
|
7038
|
+
const healthy = await waitForService(port, 500);
|
|
7039
|
+
if (healthy) {
|
|
7040
|
+
return isServiceRunning() ? { kind: "self_healthy", port } : { kind: "non_self_occupied", port };
|
|
7041
|
+
}
|
|
7042
|
+
const occupied = await isTcpPortOccupied(port, 500);
|
|
7043
|
+
if (!occupied) {
|
|
7044
|
+
return { kind: "none" };
|
|
7045
|
+
}
|
|
7046
|
+
return isServiceRunning() ? { kind: "self_unhealthy", port } : { kind: "non_self_occupied", port };
|
|
6145
7047
|
}
|
|
6146
7048
|
async function enterClaudeCode() {
|
|
7049
|
+
if (process.env.CTR_SETUP_SKIP_ENTER_CODE === "1") {
|
|
7050
|
+
return;
|
|
7051
|
+
}
|
|
6147
7052
|
const cliModule = await Promise.resolve().then(() => (init_cli(), cli_exports));
|
|
6148
7053
|
await cliModule.runClaudeCode();
|
|
6149
7054
|
}
|
|
7055
|
+
function shouldAutoEnterClaudeCodeAfterSetup() {
|
|
7056
|
+
return process.env.CTR_SETUP_AUTO_ENTER_CODE === "1";
|
|
7057
|
+
}
|
|
6150
7058
|
async function executeStart() {
|
|
6151
7059
|
const childProcess = await import("child_process");
|
|
6152
|
-
childProcess.spawn(process.execPath, [process.argv[1], "start"
|
|
7060
|
+
childProcess.spawn(process.execPath, [process.argv[1], "start"], {
|
|
6153
7061
|
detached: true,
|
|
6154
7062
|
stdio: "ignore",
|
|
6155
7063
|
env: { ...process.env, CTR_DAEMON: "1" }
|
|
@@ -6160,6 +7068,7 @@ async function executeRestart() {
|
|
|
6160
7068
|
if (info) {
|
|
6161
7069
|
try {
|
|
6162
7070
|
killProcess(info.pid);
|
|
7071
|
+
await waitForProcessExit(info.pid, 5e3);
|
|
6163
7072
|
} catch {
|
|
6164
7073
|
}
|
|
6165
7074
|
}
|
|
@@ -6339,12 +7248,12 @@ async function buildFreshConfig(io) {
|
|
|
6339
7248
|
}
|
|
6340
7249
|
return draft;
|
|
6341
7250
|
}
|
|
6342
|
-
async function completeDraft(
|
|
6343
|
-
const draft = toDraftFromConfig(
|
|
6344
|
-
if (
|
|
7251
|
+
async function completeDraft(input3) {
|
|
7252
|
+
const draft = toDraftFromConfig(input3.draft);
|
|
7253
|
+
if (input3.fields.includes("defaultModel")) {
|
|
6345
7254
|
const defaultProvider = draft.Models?.[0]?.id ?? draft.Providers?.[0]?.name ?? "provider";
|
|
6346
7255
|
const defaultModel = draft.Models?.[0]?.model ?? draft.Providers?.[0]?.models?.[0] ?? "";
|
|
6347
|
-
const model = await
|
|
7256
|
+
const model = await input3.io.input("\u9ED8\u8BA4\u6A21\u578B", defaultModel);
|
|
6348
7257
|
if (draft.Models?.[0]) {
|
|
6349
7258
|
draft.Models[0].model = model;
|
|
6350
7259
|
draft.Router.default = defaultProvider;
|
|
@@ -6353,16 +7262,16 @@ async function completeDraft(input2) {
|
|
|
6353
7262
|
draft.Router.default = `${defaultProvider},${model}`;
|
|
6354
7263
|
}
|
|
6355
7264
|
}
|
|
6356
|
-
if (
|
|
6357
|
-
const apiKey = await
|
|
7265
|
+
if (input3.fields.includes("apiKey")) {
|
|
7266
|
+
const apiKey = await input3.io.input("API Key");
|
|
6358
7267
|
if (draft.Models?.length) {
|
|
6359
7268
|
draft.Models = draft.Models.map((model) => ({ ...model, key: model.key || apiKey, api_key: model.api_key || apiKey }));
|
|
6360
7269
|
} else {
|
|
6361
7270
|
draft.Providers = draft.Providers?.map((provider) => ({ ...provider, api_key: provider.api_key || apiKey }));
|
|
6362
7271
|
}
|
|
6363
7272
|
}
|
|
6364
|
-
if (
|
|
6365
|
-
const apiBaseUrl = await
|
|
7273
|
+
if (input3.fields.includes("apiBaseUrl")) {
|
|
7274
|
+
const apiBaseUrl = await input3.io.input("API Base URL");
|
|
6366
7275
|
if (draft.Models?.length) {
|
|
6367
7276
|
draft.Models = draft.Models.map((model) => ({
|
|
6368
7277
|
...model,
|
|
@@ -6376,8 +7285,8 @@ async function completeDraft(input2) {
|
|
|
6376
7285
|
}));
|
|
6377
7286
|
}
|
|
6378
7287
|
}
|
|
6379
|
-
if (
|
|
6380
|
-
await promptCapabilityMetadataForDraft(draft,
|
|
7288
|
+
if (input3.fields.includes("capabilityHints") && draft.Models?.[0]) {
|
|
7289
|
+
await promptCapabilityMetadataForDraft(draft, input3.io);
|
|
6381
7290
|
}
|
|
6382
7291
|
return draft;
|
|
6383
7292
|
}
|
|
@@ -6391,112 +7300,150 @@ function createDefaultDeps(io = createConsoleIO()) {
|
|
|
6391
7300
|
executeStart,
|
|
6392
7301
|
executeReload: executeRestart,
|
|
6393
7302
|
executeRestart,
|
|
6394
|
-
verifyHealth: () => waitForService(
|
|
7303
|
+
verifyHealth: () => waitForService(getConfiguredPortFromCurrentFiles(), 5e3),
|
|
6395
7304
|
enterClaudeCode,
|
|
6396
7305
|
io
|
|
6397
7306
|
};
|
|
6398
7307
|
}
|
|
7308
|
+
function printRoutingNextSteps(io) {
|
|
7309
|
+
io.info("\u4F60\u53EF\u4EE5\u6309\u9700\u7EE7\u7EED\u914D\u7F6E\u8DEF\u7531\u80FD\u529B\uFF1A");
|
|
7310
|
+
io.info(" - TriggerRouter\uFF1A\u9002\u5408\u9AD8\u786E\u5B9A\u6027\u4EFB\u52A1\uFF0C\u628A\u67B6\u6784\u8BBE\u8BA1\u3001\u4EE3\u7801\u5BA1\u67E5\u7B49\u8BF7\u6C42\u56FA\u5B9A\u5207\u5230\u6307\u5B9A\u6A21\u578B");
|
|
7311
|
+
io.info(" - SmartRouter\uFF1A\u9002\u5408\u6A21\u7CCA\u4EFB\u52A1\uFF0C\u5728\u5019\u9009\u6A21\u578B\u4E4B\u95F4\u81EA\u52A8\u9009\u62E9\u66F4\u5408\u9002\u7684\u6A21\u578B");
|
|
7312
|
+
io.info(" - \u914D\u7F6E\u6A21\u677F\u53C2\u8003\uFF1Aconfig/trigger.advanced.yaml");
|
|
7313
|
+
}
|
|
6399
7314
|
async function runSetupCli(customDeps) {
|
|
6400
7315
|
const defaults = createDefaultDeps(customDeps?.io);
|
|
6401
7316
|
const deps = { ...defaults, ...customDeps };
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6415
|
-
|
|
6416
|
-
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
"\u76F4\u63A5\u4F7F\u7528\
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
if (currentConfig.kind === "invalid") {
|
|
6426
|
-
deps.io.info(`\u5F53\u524D\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A${currentConfig.errors.join("; ")}`);
|
|
6427
|
-
if (currentConfig.warnings.length > 0) {
|
|
6428
|
-
deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
|
|
7317
|
+
try {
|
|
7318
|
+
await runSetup({
|
|
7319
|
+
detectSetupEnvironment: () => detectSetupEnvironment({
|
|
7320
|
+
readCurrentConfig: deps.readCurrentConfig,
|
|
7321
|
+
readLegacyConfig: deps.readLegacyConfig,
|
|
7322
|
+
probeService: deps.probeService
|
|
7323
|
+
}),
|
|
7324
|
+
chooseCurrentConfigAction: async ({ currentConfig }) => {
|
|
7325
|
+
if (currentConfig.kind === "missing") {
|
|
7326
|
+
return "create";
|
|
7327
|
+
}
|
|
7328
|
+
if (currentConfig.kind === "valid") {
|
|
7329
|
+
deps.io.info("\u68C0\u6D4B\u5230\u5F53\u524D claude-trigger-router \u914D\u7F6E\u5DF2\u53EF\u7528\u3002");
|
|
7330
|
+
if (currentConfig.warnings.length > 0) {
|
|
7331
|
+
deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
|
|
7332
|
+
}
|
|
7333
|
+
return mapValidCurrentConfigChoice(
|
|
7334
|
+
await deps.io.choose("\u4F60\u60F3\u76F4\u63A5\u4F7F\u7528\u5B83\uFF0C\u8FD8\u662F\u91CD\u65B0\u8C03\u6574\uFF1F", [
|
|
7335
|
+
"\u76F4\u63A5\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
|
|
7336
|
+
"\u68C0\u67E5\u5E76\u8C03\u6574\u5F53\u524D\u914D\u7F6E",
|
|
7337
|
+
"\u653E\u5F03\u5F53\u524D\u914D\u7F6E\uFF0C\u91CD\u65B0\u5F00\u59CB"
|
|
7338
|
+
])
|
|
7339
|
+
);
|
|
6429
7340
|
}
|
|
6430
|
-
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
|
|
6449
|
-
|
|
6450
|
-
|
|
6451
|
-
|
|
6452
|
-
|
|
6453
|
-
|
|
6454
|
-
|
|
6455
|
-
|
|
6456
|
-
|
|
6457
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6468
|
-
|
|
6469
|
-
|
|
6470
|
-
|
|
6471
|
-
|
|
6472
|
-
|
|
6473
|
-
|
|
6474
|
-
|
|
6475
|
-
|
|
6476
|
-
|
|
6477
|
-
|
|
6478
|
-
|
|
6479
|
-
|
|
6480
|
-
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
7341
|
+
if (currentConfig.kind === "invalid") {
|
|
7342
|
+
deps.io.info(`\u5F53\u524D\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A${currentConfig.errors.join("; ")}`);
|
|
7343
|
+
if (currentConfig.warnings.length > 0) {
|
|
7344
|
+
deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
|
|
7345
|
+
}
|
|
7346
|
+
return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["repair", "overwrite", "cancel"]);
|
|
7347
|
+
}
|
|
7348
|
+
deps.io.info(`\u5F53\u524D\u914D\u7F6E\u65E0\u6CD5\u89E3\u6790\uFF1A${currentConfig.error}`);
|
|
7349
|
+
return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["rebuild", "cancel"]);
|
|
7350
|
+
},
|
|
7351
|
+
chooseLegacyConfigAction: async ({ legacyConfig }) => {
|
|
7352
|
+
if (legacyConfig.kind === "found") {
|
|
7353
|
+
return mapLegacyConfigChoice(
|
|
7354
|
+
await deps.io.choose("\u68C0\u6D4B\u5230\u65E7 claude-code-router \u914D\u7F6E\u3002\u662F\u5426\u8FC1\u79FB\u4E3A\u5F53\u524D\u63A8\u8350\u914D\u7F6E\uFF1F", [
|
|
7355
|
+
"\u8FC1\u79FB\u65E7\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
|
|
7356
|
+
"\u8DF3\u8FC7\u8FC1\u79FB\uFF0C\u624B\u52A8\u65B0\u5EFA"
|
|
7357
|
+
])
|
|
7358
|
+
);
|
|
7359
|
+
}
|
|
7360
|
+
if (legacyConfig.kind === "read_error") {
|
|
7361
|
+
deps.io.info(`\u65E7 ccr \u914D\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF1A${legacyConfig.error}`);
|
|
7362
|
+
}
|
|
7363
|
+
return "skip";
|
|
7364
|
+
},
|
|
7365
|
+
buildFreshConfig: () => buildFreshConfig(deps.io),
|
|
7366
|
+
buildRepairConfig: async ({ currentConfig }) => toDraftFromConfig(currentConfig),
|
|
7367
|
+
completeDraft: ({ draft, fields }) => completeDraft({ draft, fields, io: deps.io }),
|
|
7368
|
+
migrateLegacyConfig,
|
|
7369
|
+
mapConfigErrorsToRepairFields,
|
|
7370
|
+
io: deps.io,
|
|
7371
|
+
persistConfig: async ({ config, currentConfigPath, hasExistingConfig }) => {
|
|
7372
|
+
let normalized = normalizeAndValidateConfig({
|
|
7373
|
+
...hasExistingConfig ? getCurrentRuntimeFields() : {},
|
|
7374
|
+
...config
|
|
7375
|
+
});
|
|
7376
|
+
{
|
|
7377
|
+
const targetPort = normalized.config.PORT ?? DEFAULT_CONFIG2.PORT;
|
|
7378
|
+
const occupied = await isTcpPortOccupied(targetPort, 500);
|
|
7379
|
+
if (occupied && !isServiceRunning()) {
|
|
7380
|
+
const fallbackPort = await getAvailablePort();
|
|
7381
|
+
deps.io.info(`\u68C0\u6D4B\u5230\u9ED8\u8BA4\u7AEF\u53E3 ${targetPort} \u5DF2\u88AB\u5360\u7528\uFF0Csetup \u5DF2\u81EA\u52A8\u6539\u7528\u53EF\u7528\u7AEF\u53E3 ${fallbackPort}\u3002`);
|
|
7382
|
+
normalized = normalizeAndValidateConfig({
|
|
7383
|
+
...normalized.config,
|
|
7384
|
+
PORT: fallbackPort
|
|
7385
|
+
});
|
|
7386
|
+
}
|
|
7387
|
+
}
|
|
7388
|
+
const persisted = await persistSetupConfig({
|
|
7389
|
+
config: normalized.config,
|
|
7390
|
+
currentConfigPath,
|
|
7391
|
+
hasExistingConfig,
|
|
7392
|
+
validateConfig: (inputConfig) => normalizeAndValidateConfig(inputConfig).errors,
|
|
7393
|
+
backupCurrentConfig: deps.backupCurrentConfig,
|
|
7394
|
+
writeConfig: deps.writeConfig
|
|
7395
|
+
});
|
|
7396
|
+
if (normalized.warnings.length > 0) {
|
|
7397
|
+
deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${normalized.warnings.join("; ")}`);
|
|
7398
|
+
}
|
|
7399
|
+
return persisted;
|
|
7400
|
+
},
|
|
7401
|
+
ensureServiceReady: async ({ configChanged, detectedService, reloadSupported }) => {
|
|
7402
|
+
const effectiveDetectedService = configChanged ? await deps.probeService() : detectedService;
|
|
7403
|
+
const action = decideServiceAction({
|
|
7404
|
+
configChanged,
|
|
7405
|
+
detectedService: effectiveDetectedService,
|
|
7406
|
+
reloadSupported
|
|
7407
|
+
});
|
|
7408
|
+
await applyServiceAction({
|
|
7409
|
+
action,
|
|
7410
|
+
executeStart: deps.executeStart,
|
|
7411
|
+
executeReload: deps.executeReload,
|
|
7412
|
+
executeRestart: deps.executeRestart,
|
|
7413
|
+
verifyHealth: deps.verifyHealth
|
|
7414
|
+
});
|
|
7415
|
+
return {
|
|
7416
|
+
action: action.kind,
|
|
7417
|
+
healthChecked: true
|
|
7418
|
+
};
|
|
7419
|
+
},
|
|
7420
|
+
enterClaudeCode: async () => {
|
|
7421
|
+
printRoutingNextSteps(deps.io);
|
|
7422
|
+
if (!shouldAutoEnterClaudeCodeAfterSetup()) {
|
|
7423
|
+
deps.io.info("\u4E3A\u907F\u514D setup \u7ED3\u675F\u540E\u63A5\u7BA1\u5F53\u524D\u7EC8\u7AEF\uFF0C\u8BF7\u624B\u52A8\u8FD0\u884C\uFF1Actr code");
|
|
7424
|
+
deps.io.info("\u5982\u679C\u4F60\u660E\u786E\u9700\u8981 setup \u7ED3\u675F\u540E\u81EA\u52A8\u8FDB\u5165 Claude Code\uFF0C\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF CTR_SETUP_AUTO_ENTER_CODE=1");
|
|
7425
|
+
return;
|
|
7426
|
+
}
|
|
7427
|
+
deps.io.close?.();
|
|
7428
|
+
await deps.enterClaudeCode();
|
|
7429
|
+
},
|
|
7430
|
+
reloadSupported: false
|
|
7431
|
+
});
|
|
7432
|
+
} finally {
|
|
7433
|
+
deps.io.close?.();
|
|
7434
|
+
}
|
|
6490
7435
|
}
|
|
6491
|
-
var import_fs6, import_os3, import_path6, import_promises3, import_process, import_js_yaml;
|
|
7436
|
+
var import_fs6, import_net2, import_os3, import_path6, import_promises3, import_process, import_json52, import_js_yaml;
|
|
6492
7437
|
var init_setup2 = __esm({
|
|
6493
7438
|
"src/setup/index.ts"() {
|
|
6494
7439
|
"use strict";
|
|
6495
7440
|
import_fs6 = require("fs");
|
|
7441
|
+
import_net2 = require("net");
|
|
6496
7442
|
import_os3 = require("os");
|
|
6497
7443
|
import_path6 = require("path");
|
|
6498
7444
|
import_promises3 = require("readline/promises");
|
|
6499
7445
|
import_process = require("process");
|
|
7446
|
+
import_json52 = __toESM(require("json5"));
|
|
6500
7447
|
import_js_yaml = __toESM(require("js-yaml"));
|
|
6501
7448
|
init_constants();
|
|
6502
7449
|
init_provider_presets();
|
|
@@ -6513,6 +7460,657 @@ var init_setup2 = __esm({
|
|
|
6513
7460
|
}
|
|
6514
7461
|
});
|
|
6515
7462
|
|
|
7463
|
+
// src/doctor/index.ts
|
|
7464
|
+
function collectCompatibilityPreviewDiagnostics(model) {
|
|
7465
|
+
const registry = buildModelRegistry({
|
|
7466
|
+
Providers: [],
|
|
7467
|
+
Models: [model],
|
|
7468
|
+
Router: {
|
|
7469
|
+
default: model.id
|
|
7470
|
+
}
|
|
7471
|
+
});
|
|
7472
|
+
const compiledModel = registry.modelMap[model.id];
|
|
7473
|
+
if (!compiledModel) {
|
|
7474
|
+
return [];
|
|
7475
|
+
}
|
|
7476
|
+
const preview = buildProviderDispatchRequest({
|
|
7477
|
+
model: compiledModel.modelName,
|
|
7478
|
+
interface: compiledModel.interface ?? "openai",
|
|
7479
|
+
compatibilityProfile: compiledModel.compatibilityProfile,
|
|
7480
|
+
capabilities: compiledModel.capabilities,
|
|
7481
|
+
request: {
|
|
7482
|
+
model: compiledModel.id,
|
|
7483
|
+
max_tokens: 32,
|
|
7484
|
+
messages: [
|
|
7485
|
+
{
|
|
7486
|
+
role: "user",
|
|
7487
|
+
content: [
|
|
7488
|
+
{
|
|
7489
|
+
type: "text",
|
|
7490
|
+
text: "compatibility preview"
|
|
7491
|
+
},
|
|
7492
|
+
{
|
|
7493
|
+
type: "image",
|
|
7494
|
+
source: {
|
|
7495
|
+
type: "base64",
|
|
7496
|
+
media_type: "image/png",
|
|
7497
|
+
data: "preview"
|
|
7498
|
+
}
|
|
7499
|
+
}
|
|
7500
|
+
]
|
|
7501
|
+
}
|
|
7502
|
+
],
|
|
7503
|
+
tools: [
|
|
7504
|
+
{
|
|
7505
|
+
name: "preview_tool",
|
|
7506
|
+
description: "Preview tool",
|
|
7507
|
+
input_schema: {
|
|
7508
|
+
type: "object",
|
|
7509
|
+
properties: {
|
|
7510
|
+
query: { type: "string" }
|
|
7511
|
+
}
|
|
7512
|
+
}
|
|
7513
|
+
}
|
|
7514
|
+
],
|
|
7515
|
+
tool_choice: {
|
|
7516
|
+
type: "tool",
|
|
7517
|
+
name: "preview_tool"
|
|
7518
|
+
},
|
|
7519
|
+
thinking: {
|
|
7520
|
+
type: "enabled",
|
|
7521
|
+
effort: "medium"
|
|
7522
|
+
}
|
|
7523
|
+
}
|
|
7524
|
+
});
|
|
7525
|
+
return preview.diagnostics.map((code) => describeProtocolDiagnostic(code));
|
|
7526
|
+
}
|
|
7527
|
+
function hasArg(flag) {
|
|
7528
|
+
return process.argv.slice(2).includes(flag);
|
|
7529
|
+
}
|
|
7530
|
+
function createConsoleIO2() {
|
|
7531
|
+
if (process.env.CTR_DOCTOR_FORCE_SCRIPTED_INPUT === "1") {
|
|
7532
|
+
const scriptedInput = (0, import_fs7.readFileSync)(0, "utf-8");
|
|
7533
|
+
const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
|
|
7534
|
+
let cursor = 0;
|
|
7535
|
+
const nextAnswer = async () => answers[cursor++] ?? "";
|
|
7536
|
+
return {
|
|
7537
|
+
info(message) {
|
|
7538
|
+
import_process2.stdout.write(`${message}
|
|
7539
|
+
`);
|
|
7540
|
+
},
|
|
7541
|
+
error(message) {
|
|
7542
|
+
import_process2.stdout.write(`${message}
|
|
7543
|
+
`);
|
|
7544
|
+
},
|
|
7545
|
+
async choose(message, options) {
|
|
7546
|
+
import_process2.stdout.write(`${message}
|
|
7547
|
+
`);
|
|
7548
|
+
options.forEach((option, index2) => import_process2.stdout.write(` ${index2 + 1}. ${option}
|
|
7549
|
+
`));
|
|
7550
|
+
const answer = await nextAnswer();
|
|
7551
|
+
const index = Number(answer);
|
|
7552
|
+
if (Number.isInteger(index) && index >= 1 && index <= options.length) {
|
|
7553
|
+
return options[index - 1];
|
|
7554
|
+
}
|
|
7555
|
+
return options.find((option) => option === answer) ?? options[0];
|
|
7556
|
+
},
|
|
7557
|
+
async input(message, defaultValue) {
|
|
7558
|
+
import_process2.stdout.write(`${message}${defaultValue ? ` (${defaultValue})` : ""}: `);
|
|
7559
|
+
const answer = await nextAnswer();
|
|
7560
|
+
return answer || defaultValue || "";
|
|
7561
|
+
},
|
|
7562
|
+
async confirm(message, defaultValue = true) {
|
|
7563
|
+
import_process2.stdout.write(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"}
|
|
7564
|
+
`);
|
|
7565
|
+
const answer = (await nextAnswer()).toLowerCase();
|
|
7566
|
+
if (!answer) {
|
|
7567
|
+
return defaultValue;
|
|
7568
|
+
}
|
|
7569
|
+
return ["y", "yes", "1", "true"].includes(answer);
|
|
7570
|
+
},
|
|
7571
|
+
close() {
|
|
7572
|
+
}
|
|
7573
|
+
};
|
|
7574
|
+
}
|
|
7575
|
+
const rl = (0, import_promises4.createInterface)({ input: import_process2.stdin, output: import_process2.stdout });
|
|
7576
|
+
const ask = async (message) => (await rl.question(message)).trim();
|
|
7577
|
+
return {
|
|
7578
|
+
info(message) {
|
|
7579
|
+
import_process2.stdout.write(`${message}
|
|
7580
|
+
`);
|
|
7581
|
+
},
|
|
7582
|
+
error(message) {
|
|
7583
|
+
import_process2.stdout.write(`${message}
|
|
7584
|
+
`);
|
|
7585
|
+
},
|
|
7586
|
+
async choose(message, options) {
|
|
7587
|
+
import_process2.stdout.write(`${message}
|
|
7588
|
+
`);
|
|
7589
|
+
options.forEach((option, index) => import_process2.stdout.write(` ${index + 1}. ${option}
|
|
7590
|
+
`));
|
|
7591
|
+
while (true) {
|
|
7592
|
+
const answer = await ask("> ");
|
|
7593
|
+
const index = Number(answer);
|
|
7594
|
+
if (Number.isInteger(index) && index >= 1 && index <= options.length) {
|
|
7595
|
+
return options[index - 1];
|
|
7596
|
+
}
|
|
7597
|
+
const matched = options.find((option) => option === answer);
|
|
7598
|
+
if (matched) {
|
|
7599
|
+
return matched;
|
|
7600
|
+
}
|
|
7601
|
+
import_process2.stdout.write("\u8BF7\u8F93\u5165\u9009\u9879\u7F16\u53F7\u3002\n");
|
|
7602
|
+
}
|
|
7603
|
+
},
|
|
7604
|
+
async input(message, defaultValue) {
|
|
7605
|
+
const answer = await ask(`${message}${defaultValue ? ` (${defaultValue})` : ""}: `);
|
|
7606
|
+
return answer || defaultValue || "";
|
|
7607
|
+
},
|
|
7608
|
+
async confirm(message, defaultValue = true) {
|
|
7609
|
+
const answer = (await ask(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"}: `)).toLowerCase();
|
|
7610
|
+
if (!answer) {
|
|
7611
|
+
return defaultValue;
|
|
7612
|
+
}
|
|
7613
|
+
return ["y", "yes"].includes(answer);
|
|
7614
|
+
},
|
|
7615
|
+
close() {
|
|
7616
|
+
rl.close();
|
|
7617
|
+
}
|
|
7618
|
+
};
|
|
7619
|
+
}
|
|
7620
|
+
function getConfigCandidates() {
|
|
7621
|
+
return [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
|
|
7622
|
+
}
|
|
7623
|
+
function inferInterfaceFromApi(api) {
|
|
7624
|
+
const trimmed = api?.trim();
|
|
7625
|
+
if (!trimmed) {
|
|
7626
|
+
return void 0;
|
|
7627
|
+
}
|
|
7628
|
+
return trimmed.includes("/v1/messages") ? "anthropic" : "openai";
|
|
7629
|
+
}
|
|
7630
|
+
function sanitizeModelId(value) {
|
|
7631
|
+
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "model";
|
|
7632
|
+
}
|
|
7633
|
+
function tryLoadStructuredConfig(filePath, content) {
|
|
7634
|
+
if (filePath.endsWith(".json")) {
|
|
7635
|
+
try {
|
|
7636
|
+
return { config: JSON.parse(content), repairedParse: false, messages: [] };
|
|
7637
|
+
} catch {
|
|
7638
|
+
return {
|
|
7639
|
+
config: import_json53.default.parse(content),
|
|
7640
|
+
repairedParse: true,
|
|
7641
|
+
messages: ["\u68C0\u6D4B\u5230 JSON \u914D\u7F6E\u5305\u542B\u5BBD\u677E\u8BED\u6CD5\uFF0Cdoctor \u5DF2\u6309\u6807\u51C6 JSON \u7ED3\u6784\u91CD\u65B0\u5F52\u4E00\u5316\u3002"]
|
|
7642
|
+
};
|
|
7643
|
+
}
|
|
7644
|
+
}
|
|
7645
|
+
try {
|
|
7646
|
+
return { config: import_js_yaml2.default.load(content), repairedParse: false, messages: [] };
|
|
7647
|
+
} catch (error) {
|
|
7648
|
+
const sanitized = content.replace(/\t/g, " ");
|
|
7649
|
+
if (sanitized !== content) {
|
|
7650
|
+
return {
|
|
7651
|
+
config: import_js_yaml2.default.load(sanitized),
|
|
7652
|
+
repairedParse: true,
|
|
7653
|
+
messages: ["\u68C0\u6D4B\u5230 YAML \u4F7F\u7528\u4E86 Tab \u7F29\u8FDB\uFF0Cdoctor \u5DF2\u81EA\u52A8\u4FEE\u590D\u4E3A\u7A7A\u683C\u7F29\u8FDB\u3002"]
|
|
7654
|
+
};
|
|
7655
|
+
}
|
|
7656
|
+
throw error;
|
|
7657
|
+
}
|
|
7658
|
+
}
|
|
7659
|
+
function loadCurrentConfig() {
|
|
7660
|
+
const existingPath = getConfigCandidates().find((filePath) => (0, import_fs7.existsSync)(filePath));
|
|
7661
|
+
const path = existingPath ?? CONFIG_FILE;
|
|
7662
|
+
if (!existingPath) {
|
|
7663
|
+
return {
|
|
7664
|
+
path,
|
|
7665
|
+
existed: false,
|
|
7666
|
+
repairedParse: false,
|
|
7667
|
+
messages: ["\u672A\u68C0\u6D4B\u5230\u5F53\u524D Claude Trigger Router \u914D\u7F6E\u3002"]
|
|
7668
|
+
};
|
|
7669
|
+
}
|
|
7670
|
+
const content = (0, import_fs7.readFileSync)(existingPath, "utf-8");
|
|
7671
|
+
const loaded = tryLoadStructuredConfig(existingPath, content);
|
|
7672
|
+
return {
|
|
7673
|
+
path,
|
|
7674
|
+
existed: true,
|
|
7675
|
+
repairedParse: loaded.repairedParse,
|
|
7676
|
+
messages: loaded.messages,
|
|
7677
|
+
config: loaded.config
|
|
7678
|
+
};
|
|
7679
|
+
}
|
|
7680
|
+
function getModelLookupId(model) {
|
|
7681
|
+
return model.id?.trim() || sanitizeModelId(model.model ?? "");
|
|
7682
|
+
}
|
|
7683
|
+
function repairDeterministicConfig(config) {
|
|
7684
|
+
const nextConfig = {
|
|
7685
|
+
...config,
|
|
7686
|
+
HOST: config.HOST ?? DEFAULT_CONFIG2.HOST,
|
|
7687
|
+
PORT: config.PORT ?? DEFAULT_CONFIG2.PORT,
|
|
7688
|
+
LOG: config.LOG ?? DEFAULT_CONFIG2.LOG,
|
|
7689
|
+
LOG_LEVEL: config.LOG_LEVEL ?? DEFAULT_CONFIG2.LOG_LEVEL
|
|
7690
|
+
};
|
|
7691
|
+
const changes = [];
|
|
7692
|
+
if (Array.isArray(config.Models) && config.Models.length > 0) {
|
|
7693
|
+
nextConfig.Models = config.Models.map((item, index) => {
|
|
7694
|
+
const api = getModelApi(item);
|
|
7695
|
+
const key = getModelKey(item);
|
|
7696
|
+
const inferredInterface = getModelInterface(item) ?? inferInterfaceFromApi(api);
|
|
7697
|
+
const id = item.id?.trim() || (item.model ? sanitizeModelId(item.model) : `model_${index + 1}`);
|
|
7698
|
+
if (!item.id?.trim()) {
|
|
7699
|
+
changes.push(`\u5DF2\u8865\u5168 Models[${index}].id -> ${id}`);
|
|
7700
|
+
}
|
|
7701
|
+
if (inferredInterface && !getModelInterface(item)) {
|
|
7702
|
+
changes.push(`\u5DF2\u8865\u5168 Models[${index}].interface -> ${inferredInterface}`);
|
|
7703
|
+
}
|
|
7704
|
+
if (api && item.api !== api) {
|
|
7705
|
+
changes.push(`\u5DF2\u5F52\u4E00 Models[${index}].api`);
|
|
7706
|
+
}
|
|
7707
|
+
if (key && item.key !== key) {
|
|
7708
|
+
changes.push(`\u5DF2\u5F52\u4E00 Models[${index}].key`);
|
|
7709
|
+
}
|
|
7710
|
+
return {
|
|
7711
|
+
...item,
|
|
7712
|
+
id,
|
|
7713
|
+
api: api || void 0,
|
|
7714
|
+
api_base_url: api || void 0,
|
|
7715
|
+
key: key || void 0,
|
|
7716
|
+
api_key: key || void 0,
|
|
7717
|
+
interface: inferredInterface,
|
|
7718
|
+
protocol: inferredInterface
|
|
7719
|
+
};
|
|
7720
|
+
});
|
|
7721
|
+
if (!nextConfig.Router?.default) {
|
|
7722
|
+
if (nextConfig.Models.length === 1) {
|
|
7723
|
+
nextConfig.Router = {
|
|
7724
|
+
...nextConfig.Router ?? {},
|
|
7725
|
+
default: getModelLookupId(nextConfig.Models[0])
|
|
7726
|
+
};
|
|
7727
|
+
changes.push(`\u5DF2\u8865\u5168 Router.default -> ${nextConfig.Router.default}`);
|
|
7728
|
+
} else if (typeof config.Router?.default === "string" && config.Router.default.includes(",")) {
|
|
7729
|
+
const [providerName, modelName] = config.Router.default.split(",").map((item) => item.trim());
|
|
7730
|
+
const matched = nextConfig.Models.find(
|
|
7731
|
+
(item) => item.model === modelName && (item.id === providerName || item.id.startsWith(`${sanitizeModelId(providerName)}_`))
|
|
7732
|
+
);
|
|
7733
|
+
if (matched) {
|
|
7734
|
+
nextConfig.Router = {
|
|
7735
|
+
...nextConfig.Router ?? {},
|
|
7736
|
+
default: matched.id
|
|
7737
|
+
};
|
|
7738
|
+
changes.push(`\u5DF2\u5F52\u4E00 Router.default -> ${matched.id}`);
|
|
7739
|
+
}
|
|
7740
|
+
}
|
|
7741
|
+
}
|
|
7742
|
+
} else if (Array.isArray(config.Providers) && config.Providers.length > 0) {
|
|
7743
|
+
const migrated = migrateLegacyConfig(config);
|
|
7744
|
+
nextConfig.Models = migrated.draft.Models;
|
|
7745
|
+
nextConfig.Router = {
|
|
7746
|
+
...nextConfig.Router ?? {},
|
|
7747
|
+
...migrated.draft.Router
|
|
7748
|
+
};
|
|
7749
|
+
nextConfig.Providers = [];
|
|
7750
|
+
changes.push("\u5DF2\u5C06 legacy Providers \u7ED3\u6784\u5F52\u4E00\u4E3A Models \u7ED3\u6784\u3002");
|
|
7751
|
+
}
|
|
7752
|
+
return { config: nextConfig, changes };
|
|
7753
|
+
}
|
|
7754
|
+
async function completeMissingModelFields(config, io) {
|
|
7755
|
+
const changes = [];
|
|
7756
|
+
const nextConfig = {
|
|
7757
|
+
...config,
|
|
7758
|
+
Models: Array.isArray(config.Models) ? config.Models.map((item) => ({ ...item })) : [],
|
|
7759
|
+
Router: { ...config.Router ?? {} }
|
|
7760
|
+
};
|
|
7761
|
+
for (let index = 0; index < (nextConfig.Models?.length ?? 0); index += 1) {
|
|
7762
|
+
const model = nextConfig.Models[index];
|
|
7763
|
+
const label = model.id || model.model || `Models[${index}]`;
|
|
7764
|
+
if (!model.id?.trim()) {
|
|
7765
|
+
model.id = sanitizeModelId(await io.input(`\u8865\u5168 ${label} \u7684\u6A21\u578B ID`, sanitizeModelId(model.model || `model_${index + 1}`)));
|
|
7766
|
+
changes.push(`\u5DF2\u8865\u5168 ${label} \u7684\u6A21\u578B ID -> ${model.id}`);
|
|
7767
|
+
}
|
|
7768
|
+
if (!getModelApi(model)) {
|
|
7769
|
+
const api = await io.input(`\u8865\u5168 ${label} \u7684 API Base URL`);
|
|
7770
|
+
model.api = api;
|
|
7771
|
+
model.api_base_url = api;
|
|
7772
|
+
changes.push(`\u5DF2\u8865\u5168 ${label} \u7684 API Base URL`);
|
|
7773
|
+
}
|
|
7774
|
+
if (!getModelKey(model)) {
|
|
7775
|
+
const key = await io.input(`\u8865\u5168 ${label} \u7684 API Key`);
|
|
7776
|
+
model.key = key;
|
|
7777
|
+
model.api_key = key;
|
|
7778
|
+
changes.push(`\u5DF2\u8865\u5168 ${label} \u7684 API Key`);
|
|
7779
|
+
}
|
|
7780
|
+
if (!getModelInterface(model)) {
|
|
7781
|
+
const interfaceChoice = await io.choose(`\u8865\u5168 ${label} \u7684\u63A5\u53E3\u7C7B\u578B`, ["openai", "anthropic"]);
|
|
7782
|
+
model.interface = interfaceChoice;
|
|
7783
|
+
model.protocol = model.interface;
|
|
7784
|
+
changes.push(`\u5DF2\u8865\u5168 ${label} \u7684\u63A5\u53E3\u7C7B\u578B -> ${model.interface}`);
|
|
7785
|
+
}
|
|
7786
|
+
if (!model.model?.trim()) {
|
|
7787
|
+
model.model = await io.input(`\u8865\u5168 ${label} \u7684\u4E0A\u6E38\u6A21\u578B\u540D`);
|
|
7788
|
+
changes.push(`\u5DF2\u8865\u5168 ${label} \u7684\u4E0A\u6E38\u6A21\u578B\u540D`);
|
|
7789
|
+
}
|
|
7790
|
+
}
|
|
7791
|
+
if (!nextConfig.Router?.default) {
|
|
7792
|
+
if ((nextConfig.Models?.length ?? 0) === 1) {
|
|
7793
|
+
nextConfig.Router.default = nextConfig.Models[0].id;
|
|
7794
|
+
changes.push(`\u5DF2\u8865\u5168 Router.default -> ${nextConfig.Router.default}`);
|
|
7795
|
+
} else if ((nextConfig.Models?.length ?? 0) > 1) {
|
|
7796
|
+
const choice = await io.choose("\u8865\u5168\u9ED8\u8BA4\u6A21\u578B", nextConfig.Models.map((item) => item.id));
|
|
7797
|
+
nextConfig.Router.default = choice;
|
|
7798
|
+
changes.push(`\u5DF2\u8865\u5168 Router.default -> ${choice}`);
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7801
|
+
return { config: nextConfig, changes };
|
|
7802
|
+
}
|
|
7803
|
+
async function probeModelAvailability(model) {
|
|
7804
|
+
const api = getModelApi(model);
|
|
7805
|
+
const key = getModelKey(model);
|
|
7806
|
+
const modelInterface = getModelInterface(model);
|
|
7807
|
+
if (!api || !key || !modelInterface || !model.model) {
|
|
7808
|
+
return {
|
|
7809
|
+
kind: "failure",
|
|
7810
|
+
category: "protocol_mismatch",
|
|
7811
|
+
message: "\u6A21\u578B\u914D\u7F6E\u7F3A\u5C11 api/key/interface/model\uFF0C\u65E0\u6CD5\u53D1\u8D77\u63A2\u6D4B\u3002"
|
|
7812
|
+
};
|
|
7813
|
+
}
|
|
7814
|
+
try {
|
|
7815
|
+
const registry = buildModelRegistry({
|
|
7816
|
+
Providers: [],
|
|
7817
|
+
Models: [model],
|
|
7818
|
+
Router: {
|
|
7819
|
+
default: model.id
|
|
7820
|
+
}
|
|
7821
|
+
});
|
|
7822
|
+
const compiledModel = registry.modelMap[model.id];
|
|
7823
|
+
const dispatchRequest = compiledModel ? buildProviderDispatchRequest({
|
|
7824
|
+
model: compiledModel.modelName,
|
|
7825
|
+
interface: compiledModel.interface ?? modelInterface,
|
|
7826
|
+
compatibilityProfile: compiledModel.compatibilityProfile,
|
|
7827
|
+
capabilities: compiledModel.capabilities,
|
|
7828
|
+
request: {
|
|
7829
|
+
model: compiledModel.id,
|
|
7830
|
+
max_tokens: 1,
|
|
7831
|
+
stream: true,
|
|
7832
|
+
messages: [
|
|
7833
|
+
{
|
|
7834
|
+
role: "user",
|
|
7835
|
+
content: [
|
|
7836
|
+
{
|
|
7837
|
+
type: "text",
|
|
7838
|
+
text: "ok"
|
|
7839
|
+
}
|
|
7840
|
+
]
|
|
7841
|
+
}
|
|
7842
|
+
]
|
|
7843
|
+
}
|
|
7844
|
+
}) : null;
|
|
7845
|
+
const response = await fetch(api, {
|
|
7846
|
+
method: "POST",
|
|
7847
|
+
signal: AbortSignal.timeout(1e4),
|
|
7848
|
+
headers: modelInterface === "anthropic" ? {
|
|
7849
|
+
"content-type": "application/json",
|
|
7850
|
+
"x-api-key": key,
|
|
7851
|
+
"anthropic-version": "2023-06-01"
|
|
7852
|
+
} : {
|
|
7853
|
+
"content-type": "application/json",
|
|
7854
|
+
authorization: `Bearer ${key}`
|
|
7855
|
+
},
|
|
7856
|
+
body: JSON.stringify(dispatchRequest?.body ?? (modelInterface === "anthropic" ? {
|
|
7857
|
+
model: model.model,
|
|
7858
|
+
max_tokens: 1,
|
|
7859
|
+
stream: true,
|
|
7860
|
+
messages: [
|
|
7861
|
+
{
|
|
7862
|
+
role: "user",
|
|
7863
|
+
content: [
|
|
7864
|
+
{
|
|
7865
|
+
type: "text",
|
|
7866
|
+
text: "ok"
|
|
7867
|
+
}
|
|
7868
|
+
]
|
|
7869
|
+
}
|
|
7870
|
+
]
|
|
7871
|
+
} : {
|
|
7872
|
+
model: model.model,
|
|
7873
|
+
max_tokens: 1,
|
|
7874
|
+
stream: true,
|
|
7875
|
+
messages: [
|
|
7876
|
+
{
|
|
7877
|
+
role: "user",
|
|
7878
|
+
content: "ok"
|
|
7879
|
+
}
|
|
7880
|
+
]
|
|
7881
|
+
}))
|
|
7882
|
+
});
|
|
7883
|
+
if (response.ok) {
|
|
7884
|
+
return { kind: "success" };
|
|
7885
|
+
}
|
|
7886
|
+
const body = await response.text();
|
|
7887
|
+
if (response.status === 401 || response.status === 403) {
|
|
7888
|
+
return { kind: "failure", category: "auth_error", message: `${response.status} ${body}` };
|
|
7889
|
+
}
|
|
7890
|
+
if (response.status === 404) {
|
|
7891
|
+
return { kind: "failure", category: "model_not_found", message: `${response.status} ${body}` };
|
|
7892
|
+
}
|
|
7893
|
+
if (response.status === 400) {
|
|
7894
|
+
return { kind: "failure", category: "protocol_mismatch", message: `${response.status} ${body}` };
|
|
7895
|
+
}
|
|
7896
|
+
return { kind: "failure", category: "remote_error", message: `${response.status} ${body}` };
|
|
7897
|
+
} catch (error) {
|
|
7898
|
+
return {
|
|
7899
|
+
kind: "failure",
|
|
7900
|
+
category: "endpoint_unreachable",
|
|
7901
|
+
message: error?.message || String(error)
|
|
7902
|
+
};
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
function explainProbeFailure(category) {
|
|
7906
|
+
switch (category) {
|
|
7907
|
+
case "auth_error":
|
|
7908
|
+
return {
|
|
7909
|
+
label: "\u9274\u6743\u5931\u8D25",
|
|
7910
|
+
summary: "\u4E0A\u6E38\u63A5\u53E3\u62D2\u7EDD\u4E86\u5F53\u524D API Key\uFF0C\u6216\u5F53\u524D\u8D26\u53F7\u6CA1\u6709\u8BBF\u95EE\u8BE5\u6A21\u578B\u7684\u6743\u9650\u3002",
|
|
7911
|
+
action: "\u8BF7\u68C0\u67E5 API Key\u3001\u8D26\u53F7\u8BA2\u9605\u72B6\u6001\uFF0C\u4EE5\u53CA\u5F53\u524D\u8D26\u53F7\u662F\u5426\u5177\u5907\u76EE\u6807\u6A21\u578B\u6743\u9650\u3002"
|
|
7912
|
+
};
|
|
7913
|
+
case "model_not_found":
|
|
7914
|
+
return {
|
|
7915
|
+
label: "\u6A21\u578B\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u9650",
|
|
7916
|
+
summary: "\u4E0A\u6E38\u63A5\u53E3\u65E0\u6CD5\u8BC6\u522B\u5F53\u524D\u6A21\u578B\u540D\uFF0C\u6216\u5F53\u524D\u8D26\u53F7\u6CA1\u6709\u8BE5\u6A21\u578B\u7684\u8BBF\u95EE\u6743\u9650\u3002",
|
|
7917
|
+
action: "\u8BF7\u68C0\u67E5\u6A21\u578B\u540D\u662F\u5426\u6B63\u786E\uFF0C\u4EE5\u53CA\u5F53\u524D\u8D26\u53F7\u662F\u5426\u5DF2\u5F00\u901A\u8BE5\u6A21\u578B\u3002"
|
|
7918
|
+
};
|
|
7919
|
+
case "endpoint_unreachable":
|
|
7920
|
+
return {
|
|
7921
|
+
label: "\u63A5\u53E3\u4E0D\u53EF\u8FBE",
|
|
7922
|
+
summary: "doctor \u65E0\u6CD5\u8FDE\u63A5\u5230\u5F53\u524D API \u5730\u5740\uFF0C\u53EF\u80FD\u662F\u5730\u5740\u3001\u7F51\u7EDC\u3001TLS \u6216\u4EE3\u7406\u914D\u7F6E\u95EE\u9898\u3002",
|
|
7923
|
+
action: "\u8BF7\u68C0\u67E5 API Base URL\u3001\u7F51\u7EDC\u8FDE\u901A\u6027\u3001TLS \u8BC1\u4E66\u94FE\uFF0C\u4EE5\u53CA\u662F\u5426\u9700\u8981\u4EE3\u7406\u3002"
|
|
7924
|
+
};
|
|
7925
|
+
case "protocol_mismatch":
|
|
7926
|
+
return {
|
|
7927
|
+
label: "\u534F\u8BAE\u517C\u5BB9\u5931\u8D25",
|
|
7928
|
+
summary: "\u5F53\u524D\u4E0A\u6E38\u63A5\u53E3\u4E0E\u7EDF\u4E00\u6D88\u606F\u62BD\u8C61\u5728 messages\u3001tools\u3001stream \u6216\u63A7\u5236\u5B57\u6BB5\u4E0A\u5B58\u5728\u517C\u5BB9\u5DEE\u5F02\u3002",
|
|
7929
|
+
action: "\u8BF7\u5148\u786E\u8BA4 API Base URL \u548C interface \u662F\u5426\u914D\u7F6E\u6B63\u786E\uFF1B\u5982\u679C\u6587\u672C\u8BF7\u6C42\u6B63\u5E38\u4F46\u5DE5\u5177\u8C03\u7528\u5931\u8D25\uFF0C\u8BF7\u4FDD\u7559\u539F\u59CB\u62A5\u9519\u7EE7\u7EED\u6536\u655B\u517C\u5BB9\u5C42\u3002"
|
|
7930
|
+
};
|
|
7931
|
+
case "remote_error":
|
|
7932
|
+
return {
|
|
7933
|
+
label: "\u4E0A\u6E38\u8FD4\u56DE\u9519\u8BEF",
|
|
7934
|
+
summary: "\u8BF7\u6C42\u5DF2\u7ECF\u5230\u8FBE\u4E0A\u6E38\uFF0C\u4F46\u4E0A\u6E38\u8FD4\u56DE\u4E86\u5176\u4ED6\u4E1A\u52A1\u6216\u670D\u52A1\u7AEF\u9519\u8BEF\u3002",
|
|
7935
|
+
action: "\u8BF7\u7ED3\u5408\u539F\u59CB\u9519\u8BEF\u4FE1\u606F\u68C0\u67E5\u4E0A\u6E38\u670D\u52A1\u72B6\u6001\u3001\u6A21\u578B\u914D\u989D\u6216\u8D26\u53F7\u9650\u5236\u3002"
|
|
7936
|
+
};
|
|
7937
|
+
default:
|
|
7938
|
+
return {
|
|
7939
|
+
label: category,
|
|
7940
|
+
summary: "\u672A\u77E5\u8FDC\u7AEF\u9519\u8BEF\u3002",
|
|
7941
|
+
action: "\u8BF7\u4FDD\u7559\u539F\u59CB\u9519\u8BEF\u4FE1\u606F\u540E\u7EE7\u7EED\u6392\u67E5\u3002"
|
|
7942
|
+
};
|
|
7943
|
+
}
|
|
7944
|
+
}
|
|
7945
|
+
async function ensureServiceUsable(config, deps, configChanged) {
|
|
7946
|
+
const port = config.PORT ?? DEFAULT_CONFIG2.PORT;
|
|
7947
|
+
const healthy = await deps.probeServiceHealth(port, 500);
|
|
7948
|
+
const occupied = await deps.isTcpPortOccupied(port, 500);
|
|
7949
|
+
const running = deps.isServiceRunning();
|
|
7950
|
+
if (healthy && !configChanged) {
|
|
7951
|
+
deps.io.info(`\u670D\u52A1\u5065\u5EB7\u68C0\u67E5\u901A\u8FC7\uFF1Ahttp://127.0.0.1:${port}`);
|
|
7952
|
+
return;
|
|
7953
|
+
}
|
|
7954
|
+
if (occupied && !healthy && !running) {
|
|
7955
|
+
throw new Error(`\u7AEF\u53E3 ${port} \u5DF2\u88AB\u5176\u4ED6\u670D\u52A1\u5360\u7528\uFF0Cdoctor \u65E0\u6CD5\u81EA\u52A8\u542F\u52A8\u5F53\u524D\u670D\u52A1\u3002`);
|
|
7956
|
+
}
|
|
7957
|
+
if (running) {
|
|
7958
|
+
const info = deps.readServiceInfo();
|
|
7959
|
+
if (info) {
|
|
7960
|
+
try {
|
|
7961
|
+
deps.killProcess(info.pid);
|
|
7962
|
+
} catch {
|
|
7963
|
+
}
|
|
7964
|
+
}
|
|
7965
|
+
}
|
|
7966
|
+
await deps.startDaemon();
|
|
7967
|
+
const verified = await deps.waitForService(port, 5e3);
|
|
7968
|
+
if (!verified) {
|
|
7969
|
+
throw new Error(`doctor \u81EA\u52A8\u542F\u52A8\u540E\u5065\u5EB7\u68C0\u67E5\u4ECD\u672A\u901A\u8FC7\uFF08\u7AEF\u53E3 ${port}\uFF09\u3002`);
|
|
7970
|
+
}
|
|
7971
|
+
deps.io.info(`\u670D\u52A1\u5DF2\u5C31\u7EEA\uFF1Ahttp://127.0.0.1:${port}`);
|
|
7972
|
+
}
|
|
7973
|
+
function createDefaultDeps2(io = createConsoleIO2()) {
|
|
7974
|
+
return {
|
|
7975
|
+
readLegacyConfig,
|
|
7976
|
+
backupCurrentConfig: backupConfigFile,
|
|
7977
|
+
writeConfig: writeConfigFile,
|
|
7978
|
+
isServiceRunning,
|
|
7979
|
+
readServiceInfo,
|
|
7980
|
+
killProcess,
|
|
7981
|
+
probeServiceHealth,
|
|
7982
|
+
isTcpPortOccupied,
|
|
7983
|
+
waitForService,
|
|
7984
|
+
io,
|
|
7985
|
+
startDaemon: async () => {
|
|
7986
|
+
(0, import_child_process2.spawn)(process.execPath, [process.argv[1], "start", "--daemon"], {
|
|
7987
|
+
detached: true,
|
|
7988
|
+
stdio: "ignore",
|
|
7989
|
+
env: { ...process.env, CTR_DAEMON: "1" }
|
|
7990
|
+
}).unref();
|
|
7991
|
+
}
|
|
7992
|
+
};
|
|
7993
|
+
}
|
|
7994
|
+
async function runDoctorCli(customDeps) {
|
|
7995
|
+
const defaults = createDefaultDeps2(customDeps?.io);
|
|
7996
|
+
const deps = { ...defaults, ...customDeps };
|
|
7997
|
+
let configChanged = false;
|
|
7998
|
+
try {
|
|
7999
|
+
deps.io.info("\u5F00\u59CB\u8BCA\u65AD\u5F53\u524D Claude Trigger Router \u914D\u7F6E...");
|
|
8000
|
+
const current = loadCurrentConfig();
|
|
8001
|
+
current.messages.forEach((message) => deps.io.info(message));
|
|
8002
|
+
let workingConfig = current.config;
|
|
8003
|
+
if (!workingConfig) {
|
|
8004
|
+
const legacy = await deps.readLegacyConfig();
|
|
8005
|
+
if (legacy.kind === "found") {
|
|
8006
|
+
deps.io.info("\u672A\u68C0\u6D4B\u5230\u5F53\u524D\u914D\u7F6E\uFF0C\u4F46\u53D1\u73B0\u65E7 claude-code-router \u914D\u7F6E\uFF0Cdoctor \u5C06\u5148\u5C1D\u8BD5\u8FC1\u79FB\u3002");
|
|
8007
|
+
const migrated = migrateLegacyConfig(legacy.config);
|
|
8008
|
+
workingConfig = {
|
|
8009
|
+
...buildUsableMinimalTemplateConfig(),
|
|
8010
|
+
...migrated.draft
|
|
8011
|
+
};
|
|
8012
|
+
} else {
|
|
8013
|
+
throw new Error("\u672A\u68C0\u6D4B\u5230\u53EF\u8BCA\u65AD\u7684\u5F53\u524D\u914D\u7F6E\uFF1B\u8BF7\u5148\u8FD0\u884C ctr setup \u6216 ctr init --force\u3002");
|
|
8014
|
+
}
|
|
8015
|
+
}
|
|
8016
|
+
const deterministic = repairDeterministicConfig(workingConfig);
|
|
8017
|
+
workingConfig = deterministic.config;
|
|
8018
|
+
deterministic.changes.forEach((message) => deps.io.info(message));
|
|
8019
|
+
const completed = await completeMissingModelFields(workingConfig, deps.io);
|
|
8020
|
+
workingConfig = completed.config;
|
|
8021
|
+
completed.changes.forEach((message) => deps.io.info(message));
|
|
8022
|
+
const normalized = normalizeAndValidateConfig(workingConfig);
|
|
8023
|
+
if (normalized.errors.length > 0) {
|
|
8024
|
+
deps.io.error(`doctor \u4ECD\u53D1\u73B0\u65E0\u6CD5\u81EA\u52A8\u4FEE\u590D\u7684\u914D\u7F6E\u9519\u8BEF\uFF1A${normalized.errors.join("; ")}`);
|
|
8025
|
+
throw new Error("doctor could not fully repair config");
|
|
8026
|
+
}
|
|
8027
|
+
if (normalized.warnings.length > 0) {
|
|
8028
|
+
deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${normalized.warnings.join("; ")}`);
|
|
8029
|
+
}
|
|
8030
|
+
const registry = buildModelRegistry(normalized.config);
|
|
8031
|
+
for (const model of normalized.config.Models ?? []) {
|
|
8032
|
+
const compiledModel = registry.modelMap[model.id];
|
|
8033
|
+
if (!compiledModel) {
|
|
8034
|
+
continue;
|
|
8035
|
+
}
|
|
8036
|
+
const compatibility = describeCompatibilityProfile(compiledModel.compatibilityProfile);
|
|
8037
|
+
const dispatch = describeDispatchFormat(compiledModel.dispatchFormat);
|
|
8038
|
+
deps.io.info(
|
|
8039
|
+
`\u6A21\u578B\u517C\u5BB9\u7B56\u7565\uFF1A${model.id} -> ${compatibility.label}`
|
|
8040
|
+
);
|
|
8041
|
+
deps.io.info(`\u517C\u5BB9\u8BF4\u660E\uFF1A${compatibility.summary}`);
|
|
8042
|
+
deps.io.info(`\u8BF7\u6C42\u7F16\u8BD1\uFF1A${dispatch.label}\u3002${dispatch.summary}`);
|
|
8043
|
+
const previewDiagnostics = collectCompatibilityPreviewDiagnostics(model);
|
|
8044
|
+
for (const diagnostic of previewDiagnostics) {
|
|
8045
|
+
deps.io.info(`\u8FD0\u884C\u65F6\u517C\u5BB9\u63D0\u793A\uFF1A${diagnostic.label}`);
|
|
8046
|
+
deps.io.info(`\u8FD0\u884C\u65F6\u8BF4\u660E\uFF1A${diagnostic.summary}`);
|
|
8047
|
+
deps.io.info(`\u8FD0\u884C\u65F6\u5EFA\u8BAE\uFF1A${diagnostic.action}`);
|
|
8048
|
+
}
|
|
8049
|
+
}
|
|
8050
|
+
const needWrite = current.repairedParse || deterministic.changes.length > 0 || completed.changes.length > 0 || !current.existed;
|
|
8051
|
+
if (needWrite) {
|
|
8052
|
+
if (current.existed) {
|
|
8053
|
+
const backupPath = await deps.backupCurrentConfig();
|
|
8054
|
+
if (backupPath) {
|
|
8055
|
+
deps.io.info(`\u5DF2\u5907\u4EFD\u5F53\u524D\u914D\u7F6E\uFF1A${backupPath}`);
|
|
8056
|
+
}
|
|
8057
|
+
}
|
|
8058
|
+
await deps.writeConfig(normalized.config);
|
|
8059
|
+
deps.io.info(`\u5DF2\u5199\u56DE\u4FEE\u590D\u540E\u7684\u914D\u7F6E\uFF1A${current.path}`);
|
|
8060
|
+
configChanged = true;
|
|
8061
|
+
}
|
|
8062
|
+
await ensureServiceUsable(normalized.config, deps, configChanged);
|
|
8063
|
+
const shouldProbeModels = hasArg("--check-models") ? await deps.io.confirm(`\u5373\u5C06\u5411 ${normalized.config.Models?.length ?? 0} \u4E2A\u6A21\u578B\u53D1\u9001\u6700\u5C0F\u63A2\u6D4B\u8BF7\u6C42\uFF0C\u53EF\u80FD\u6D88\u8017\u5C11\u91CF\u989D\u5EA6\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F`, true) : await deps.io.confirm(`\u662F\u5426\u7EE7\u7EED\u63A2\u6D4B ${normalized.config.Models?.length ?? 0} \u4E2A\u6A21\u578B\u7684\u53EF\u7528\u6027\uFF1F\u8FD9\u4F1A\u6D88\u8017\u5C11\u91CF\u989D\u5EA6\u3002`, false);
|
|
8064
|
+
if (!shouldProbeModels) {
|
|
8065
|
+
deps.io.info("\u5DF2\u8DF3\u8FC7\u6A21\u578B\u63A2\u6D4B\u3002\u914D\u7F6E\u548C\u670D\u52A1\u8BCA\u65AD\u5DF2\u5B8C\u6210\u3002");
|
|
8066
|
+
return;
|
|
8067
|
+
}
|
|
8068
|
+
let probeSuccess = 0;
|
|
8069
|
+
let probeFailure = 0;
|
|
8070
|
+
for (const model of normalized.config.Models ?? []) {
|
|
8071
|
+
const result = await probeModelAvailability(model);
|
|
8072
|
+
if (result.kind === "success") {
|
|
8073
|
+
deps.io.info(`\u6A21\u578B\u63A2\u6D4B\u6210\u529F\uFF1A${model.id}`);
|
|
8074
|
+
probeSuccess += 1;
|
|
8075
|
+
continue;
|
|
8076
|
+
}
|
|
8077
|
+
const explanation = explainProbeFailure(result.category);
|
|
8078
|
+
probeFailure += 1;
|
|
8079
|
+
deps.io.error(`\u6A21\u578B\u63A2\u6D4B\u5931\u8D25\uFF1A${model.id} -> ${explanation.label}`);
|
|
8080
|
+
deps.io.info(`\u5931\u8D25\u8BF4\u660E\uFF1A${explanation.summary}`);
|
|
8081
|
+
deps.io.info(`\u5904\u7406\u5EFA\u8BAE\uFF1A${explanation.action}`);
|
|
8082
|
+
deps.io.info(`\u8FDC\u7AEF\u539F\u59CB\u4FE1\u606F\uFF1A${result.message}`);
|
|
8083
|
+
deps.io.info("\u8FD9\u7C7B\u8FDC\u7AEF\u5931\u8D25\u9700\u8981\u4F60\u786E\u8BA4\u5E76\u624B\u52A8\u5904\u7406\uFF1Bdoctor \u4E0D\u4F1A\u81EA\u52A8\u4FEE\u6539\u6A21\u578B\u8BED\u4E49\u6216\u8FDC\u7AEF\u8D26\u53F7\u914D\u7F6E\u3002");
|
|
8084
|
+
}
|
|
8085
|
+
deps.io.info(`\u6A21\u578B\u63A2\u6D4B\u5B8C\u6210\uFF1A\u6210\u529F ${probeSuccess}\uFF0C\u5931\u8D25 ${probeFailure}\u3002`);
|
|
8086
|
+
deps.io.info("doctor \u8BCA\u65AD\u5B8C\u6210\u3002");
|
|
8087
|
+
} finally {
|
|
8088
|
+
deps.io.close?.();
|
|
8089
|
+
}
|
|
8090
|
+
}
|
|
8091
|
+
var import_fs7, import_promises4, import_process2, import_child_process2, import_json53, import_js_yaml2;
|
|
8092
|
+
var init_doctor = __esm({
|
|
8093
|
+
"src/doctor/index.ts"() {
|
|
8094
|
+
"use strict";
|
|
8095
|
+
import_fs7 = require("fs");
|
|
8096
|
+
import_promises4 = require("readline/promises");
|
|
8097
|
+
import_process2 = require("process");
|
|
8098
|
+
import_child_process2 = require("child_process");
|
|
8099
|
+
import_json53 = __toESM(require("json5"));
|
|
8100
|
+
import_js_yaml2 = __toESM(require("js-yaml"));
|
|
8101
|
+
init_constants();
|
|
8102
|
+
init_utils();
|
|
8103
|
+
init_migrate();
|
|
8104
|
+
init_setup2();
|
|
8105
|
+
init_schema();
|
|
8106
|
+
init_compile();
|
|
8107
|
+
init_protocols();
|
|
8108
|
+
init_processCheck();
|
|
8109
|
+
init_service_health();
|
|
8110
|
+
init_templates();
|
|
8111
|
+
}
|
|
8112
|
+
});
|
|
8113
|
+
|
|
6516
8114
|
// src/cli.ts
|
|
6517
8115
|
var cli_exports = {};
|
|
6518
8116
|
__export(cli_exports, {
|
|
@@ -6522,7 +8120,7 @@ __export(cli_exports, {
|
|
|
6522
8120
|
});
|
|
6523
8121
|
module.exports = __toCommonJS(cli_exports);
|
|
6524
8122
|
function getPackageInfo() {
|
|
6525
|
-
const content = (0,
|
|
8123
|
+
const content = (0, import_fs8.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
|
|
6526
8124
|
const pkg = JSON.parse(content);
|
|
6527
8125
|
return {
|
|
6528
8126
|
name: pkg.name ?? "@peterwangze/claude-trigger-router",
|
|
@@ -6535,7 +8133,7 @@ function getArgs() {
|
|
|
6535
8133
|
function getCommand() {
|
|
6536
8134
|
return getArgs()[0];
|
|
6537
8135
|
}
|
|
6538
|
-
function
|
|
8136
|
+
function hasArg2(flag, shortFlag) {
|
|
6539
8137
|
const args = getArgs();
|
|
6540
8138
|
return args.includes(flag) || (shortFlag ? args.includes(shortFlag) : false);
|
|
6541
8139
|
}
|
|
@@ -6544,23 +8142,34 @@ function getArgValue(flag, shortFlag) {
|
|
|
6544
8142
|
const index = args.indexOf(flag) !== -1 ? args.indexOf(flag) : shortFlag ? args.indexOf(shortFlag) : -1;
|
|
6545
8143
|
return index !== -1 ? args[index + 1] : void 0;
|
|
6546
8144
|
}
|
|
8145
|
+
function parsePortValue(portValue, sourceLabel) {
|
|
8146
|
+
const trimmed = portValue.trim();
|
|
8147
|
+
if (!/^\d+$/.test(trimmed)) {
|
|
8148
|
+
throw new Error(`${sourceLabel} \u4E0D\u662F\u5408\u6CD5\u7AEF\u53E3\uFF1A${portValue}`);
|
|
8149
|
+
}
|
|
8150
|
+
const port = Number.parseInt(trimmed, 10);
|
|
8151
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
8152
|
+
throw new Error(`${sourceLabel} \u8D85\u51FA\u5408\u6CD5\u8303\u56F4\uFF081-65535\uFF09\uFF1A${portValue}`);
|
|
8153
|
+
}
|
|
8154
|
+
return port;
|
|
8155
|
+
}
|
|
6547
8156
|
function getPort() {
|
|
6548
8157
|
const portValue = getArgValue("--port", "-p");
|
|
6549
8158
|
if (portValue) {
|
|
6550
|
-
return
|
|
8159
|
+
return parsePortValue(portValue, "\u547D\u4EE4\u884C\u7AEF\u53E3\u53C2\u6570");
|
|
6551
8160
|
}
|
|
6552
8161
|
try {
|
|
6553
|
-
const
|
|
6554
|
-
if ((0,
|
|
6555
|
-
const content = (0,
|
|
6556
|
-
const config =
|
|
8162
|
+
const yaml4 = require("js-yaml");
|
|
8163
|
+
if ((0, import_fs8.existsSync)(CONFIG_FILE)) {
|
|
8164
|
+
const content = (0, import_fs8.readFileSync)(CONFIG_FILE, "utf-8");
|
|
8165
|
+
const config = yaml4.load(content);
|
|
6557
8166
|
if (config?.PORT) return config.PORT;
|
|
6558
|
-
} else if ((0,
|
|
6559
|
-
const content = (0,
|
|
6560
|
-
const config =
|
|
8167
|
+
} else if ((0, import_fs8.existsSync)(CONFIG_FILE_YML)) {
|
|
8168
|
+
const content = (0, import_fs8.readFileSync)(CONFIG_FILE_YML, "utf-8");
|
|
8169
|
+
const config = yaml4.load(content);
|
|
6561
8170
|
if (config?.PORT) return config.PORT;
|
|
6562
|
-
} else if ((0,
|
|
6563
|
-
const content = (0,
|
|
8171
|
+
} else if ((0, import_fs8.existsSync)(CONFIG_FILE_JSON)) {
|
|
8172
|
+
const content = (0, import_fs8.readFileSync)(CONFIG_FILE_JSON, "utf-8");
|
|
6564
8173
|
const config = JSON.parse(content);
|
|
6565
8174
|
if (config?.PORT) return config.PORT;
|
|
6566
8175
|
}
|
|
@@ -6569,7 +8178,7 @@ function getPort() {
|
|
|
6569
8178
|
return DEFAULT_CONFIG2.PORT;
|
|
6570
8179
|
}
|
|
6571
8180
|
function isDaemonMode() {
|
|
6572
|
-
return
|
|
8181
|
+
return hasArg2("--daemon", "-d");
|
|
6573
8182
|
}
|
|
6574
8183
|
function printHelp() {
|
|
6575
8184
|
console.log(`
|
|
@@ -6579,6 +8188,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
|
|
|
6579
8188
|
|
|
6580
8189
|
\u547D\u4EE4\uFF1A
|
|
6581
8190
|
setup \u68C0\u6D4B\u5E76\u590D\u7528\u5DF2\u6709\u914D\u7F6E\uFF0C\u5FC5\u8981\u65F6\u8FC1\u79FB\u65E7\u914D\u7F6E\u6216\u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
|
|
8191
|
+
doctor \u8BCA\u65AD\u5E76\u4FEE\u590D\u5F53\u524D\u914D\u7F6E\uFF0C\u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
|
|
6582
8192
|
init \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
|
|
6583
8193
|
start \u542F\u52A8\u8DEF\u7531\u670D\u52A1\uFF08\u9ED8\u8BA4\u524D\u53F0\u8FD0\u884C\uFF09
|
|
6584
8194
|
stop \u505C\u6B62\u540E\u53F0\u670D\u52A1
|
|
@@ -6587,7 +8197,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
|
|
|
6587
8197
|
version \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C\u4E0E\u5305\u4FE1\u606F
|
|
6588
8198
|
upgrade \u67E5\u770B\u5347\u7EA7\u5230\u6700\u65B0 npm \u7248\u672C\u7684\u6307\u5F15
|
|
6589
8199
|
code \u901A\u8FC7\u8DEF\u7531\u5668\u8FD0\u884C Claude Code\uFF08\u9700\u5148\u542F\u52A8\u670D\u52A1\uFF09
|
|
6590
|
-
ui \u6253\u5F00\u7BA1\u7406
|
|
8200
|
+
ui \u6253\u5F00\u672C\u5730\u7BA1\u7406\u9875\uFF08\u914D\u7F6E\u9884\u89C8\u4E0E\u8C03\u8BD5\uFF09
|
|
6591
8201
|
help \u663E\u793A\u6B64\u5E2E\u52A9\u4FE1\u606F
|
|
6592
8202
|
|
|
6593
8203
|
\u9009\u9879\uFF1A
|
|
@@ -6597,6 +8207,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
|
|
|
6597
8207
|
|
|
6598
8208
|
\u4F7F\u7528\u793A\u4F8B\uFF1A
|
|
6599
8209
|
ctr setup # \u590D\u7528\u5F53\u524D\u914D\u7F6E / \u8FC1\u79FB\u65E7\u914D\u7F6E / \u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
|
|
8210
|
+
ctr doctor # \u8BCA\u65AD\u914D\u7F6E / \u4FEE\u590D\u683C\u5F0F\u95EE\u9898 / \u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
|
|
6600
8211
|
ctr init # \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
|
|
6601
8212
|
ctr version # \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C
|
|
6602
8213
|
ctr upgrade # \u67E5\u770B\u5347\u7EA7\u5230\u6700\u65B0\u7248\u672C\u7684\u547D\u4EE4
|
|
@@ -6604,6 +8215,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
|
|
|
6604
8215
|
ctr start --daemon # \u540E\u53F0\u542F\u52A8
|
|
6605
8216
|
ctr status # \u67E5\u770B\u670D\u52A1\u72B6\u6001
|
|
6606
8217
|
ctr code # \u542F\u52A8 Claude Code\uFF08\u9700\u5148\u8FD0\u884C ctr start\uFF09
|
|
8218
|
+
ctr ui # \u6253\u5F00\u672C\u5730\u7BA1\u7406\u9875\uFF08\u53EF\u9009\uFF09
|
|
6607
8219
|
ctr stop # \u505C\u6B62\u540E\u53F0\u670D\u52A1
|
|
6608
8220
|
ctr restart --daemon # \u91CD\u542F\u540E\u53F0\u670D\u52A1
|
|
6609
8221
|
|
|
@@ -6613,10 +8225,29 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
|
|
|
6613
8225
|
|
|
6614
8226
|
\u914D\u7F6E\u76EE\u5F55\uFF1A${CONFIG_DIR}
|
|
6615
8227
|
|
|
8228
|
+
\u8865\u5145\u8BF4\u660E\uFF1A
|
|
8229
|
+
ctr restart \u5F53\u524D\u9ED8\u8BA4\u6309\u540E\u53F0\u6A21\u5F0F\u91CD\u542F\uFF1B\u53EF\u5199 ctr restart \u6216 ctr restart --daemon
|
|
8230
|
+
|
|
6616
8231
|
\u66F4\u591A\u4FE1\u606F\uFF1Ahttps://github.com/peterwangze/claude-trigger-router
|
|
6617
8232
|
`);
|
|
6618
8233
|
}
|
|
6619
|
-
|
|
8234
|
+
function getLatestPackageVersionViaNpm(packageName, timeoutMs = 5e3) {
|
|
8235
|
+
try {
|
|
8236
|
+
const result = (0, import_child_process3.spawnSync)("npm", ["view", packageName, "version", "--registry", PACKAGE_REGISTRY_URL], {
|
|
8237
|
+
encoding: "utf-8",
|
|
8238
|
+
timeout: timeoutMs,
|
|
8239
|
+
shell: process.platform === "win32"
|
|
8240
|
+
});
|
|
8241
|
+
if (result.status !== 0) {
|
|
8242
|
+
return null;
|
|
8243
|
+
}
|
|
8244
|
+
const value = result.stdout?.trim();
|
|
8245
|
+
return value ? value : null;
|
|
8246
|
+
} catch {
|
|
8247
|
+
return null;
|
|
8248
|
+
}
|
|
8249
|
+
}
|
|
8250
|
+
async function getLatestPackageVersion(packageName, timeoutMs = 4e3) {
|
|
6620
8251
|
try {
|
|
6621
8252
|
const response = await fetch(PACKAGE_REGISTRY_LATEST_URL, {
|
|
6622
8253
|
signal: AbortSignal.timeout(timeoutMs)
|
|
@@ -6625,10 +8256,12 @@ async function getLatestPackageVersion(timeoutMs = 1500) {
|
|
|
6625
8256
|
return null;
|
|
6626
8257
|
}
|
|
6627
8258
|
const payload = await response.json();
|
|
6628
|
-
|
|
8259
|
+
if (typeof payload.version === "string") {
|
|
8260
|
+
return payload.version;
|
|
8261
|
+
}
|
|
6629
8262
|
} catch {
|
|
6630
|
-
return null;
|
|
6631
8263
|
}
|
|
8264
|
+
return getLatestPackageVersionViaNpm(packageName);
|
|
6632
8265
|
}
|
|
6633
8266
|
function isNewerVersion(current, latest) {
|
|
6634
8267
|
const currentParts = current.split(".").map((part) => Number.parseInt(part, 10));
|
|
@@ -6648,7 +8281,7 @@ function isNewerVersion(current, latest) {
|
|
|
6648
8281
|
}
|
|
6649
8282
|
async function printVersion() {
|
|
6650
8283
|
const pkg = getPackageInfo();
|
|
6651
|
-
const latestVersion = await getLatestPackageVersion();
|
|
8284
|
+
const latestVersion = await getLatestPackageVersion(pkg.name);
|
|
6652
8285
|
console.log(`Package: ${pkg.name}`);
|
|
6653
8286
|
console.log(`Version: ${pkg.version}`);
|
|
6654
8287
|
console.log(`Latest: ${latestVersion ?? "unavailable"}`);
|
|
@@ -6668,30 +8301,42 @@ function printUpgradeGuidance() {
|
|
|
6668
8301
|
console.log("\u5168\u5C40\u5B89\u88C5\u5728\u67D0\u4E9B\u73AF\u5883\u4E0B\u53EF\u80FD\u9700\u8981\u7BA1\u7406\u5458/root \u6743\u9650\u3002");
|
|
6669
8302
|
console.log(`NPM: ${PACKAGE_PAGE_URL}`);
|
|
6670
8303
|
}
|
|
8304
|
+
function printRestartGuidanceHint() {
|
|
8305
|
+
console.log("\u8BF4\u660E\uFF1A`ctr restart` \u5F53\u524D\u9ED8\u8BA4\u6309\u540E\u53F0\u6A21\u5F0F\u91CD\u542F\u670D\u52A1\uFF0C`--daemon` \u53EA\u662F\u663E\u5F0F\u5199\u6CD5\u3002");
|
|
8306
|
+
}
|
|
8307
|
+
function isClaudeCommandAvailable(timeoutMs = 3e3) {
|
|
8308
|
+
try {
|
|
8309
|
+
const result = (0, import_child_process3.spawnSync)("claude", ["--version"], {
|
|
8310
|
+
encoding: "utf-8",
|
|
8311
|
+
timeout: timeoutMs,
|
|
8312
|
+
stdio: "ignore",
|
|
8313
|
+
shell: process.platform === "win32"
|
|
8314
|
+
});
|
|
8315
|
+
return result.status === 0;
|
|
8316
|
+
} catch {
|
|
8317
|
+
return false;
|
|
8318
|
+
}
|
|
8319
|
+
}
|
|
6671
8320
|
function initConfig2() {
|
|
6672
|
-
const force =
|
|
6673
|
-
const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(
|
|
8321
|
+
const force = hasArg2("--force");
|
|
8322
|
+
const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs8.existsSync);
|
|
6674
8323
|
if (existingConfig && !force) {
|
|
6675
8324
|
console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
|
|
6676
8325
|
console.log(" \u5982\u9700\u8986\u76D6\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
|
|
6677
8326
|
return;
|
|
6678
8327
|
}
|
|
6679
|
-
if (!(0,
|
|
6680
|
-
(0,
|
|
6681
|
-
}
|
|
6682
|
-
const examplePaths = [
|
|
6683
|
-
(0, import_path7.join)(__dirname, "..", "config", "trigger.example.yaml"),
|
|
6684
|
-
(0, import_path7.join)((0, import_path7.dirname)(process.argv[1]), "..", "config", "trigger.example.yaml")
|
|
6685
|
-
];
|
|
6686
|
-
const exampleFile = examplePaths.find((p) => (0, import_fs7.existsSync)(p));
|
|
6687
|
-
if (!exampleFile) {
|
|
6688
|
-
console.error("\u274C \u627E\u4E0D\u5230\u793A\u4F8B\u914D\u7F6E\u6587\u4EF6\u3002");
|
|
6689
|
-
console.log(` \u8BF7\u624B\u52A8\u521B\u5EFA ${CONFIG_FILE}`);
|
|
6690
|
-
console.log(" \u53C2\u8003\u6587\u6863\uFF1Ahttps://github.com/peterwangze/claude-trigger-router#configuration");
|
|
6691
|
-
process.exit(1);
|
|
8328
|
+
if (!(0, import_fs8.existsSync)(CONFIG_DIR)) {
|
|
8329
|
+
(0, import_fs8.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
6692
8330
|
}
|
|
6693
8331
|
try {
|
|
6694
|
-
|
|
8332
|
+
const yaml4 = require("js-yaml");
|
|
8333
|
+
const templateConfig = buildUsableMinimalTemplateConfig();
|
|
8334
|
+
const content = yaml4.dump(templateConfig, {
|
|
8335
|
+
indent: 2,
|
|
8336
|
+
lineWidth: -1,
|
|
8337
|
+
noRefs: true
|
|
8338
|
+
});
|
|
8339
|
+
(0, import_fs8.writeFileSync)(CONFIG_FILE, content, "utf-8");
|
|
6695
8340
|
const action = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
|
|
6696
8341
|
console.log(`\u2705 \u914D\u7F6E\u6587\u4EF6${action}\uFF1A${CONFIG_FILE}`);
|
|
6697
8342
|
console.log("");
|
|
@@ -6707,10 +8352,22 @@ function initConfig2() {
|
|
|
6707
8352
|
}
|
|
6708
8353
|
}
|
|
6709
8354
|
async function startForeground(port) {
|
|
8355
|
+
const targetPort = port ?? getPort();
|
|
8356
|
+
const healthy = await waitForService(targetPort, 500);
|
|
8357
|
+
const occupied = await isTcpPortOccupied(targetPort, 500);
|
|
8358
|
+
if (healthy && occupied && isServiceRunning()) {
|
|
8359
|
+
console.log(`\u2705 Service is already running on port ${targetPort}.`);
|
|
8360
|
+
console.log(" Use 'ctr status' to inspect it or 'ctr stop' before starting again.");
|
|
8361
|
+
return;
|
|
8362
|
+
}
|
|
8363
|
+
if (!healthy && occupied && !isServiceRunning()) {
|
|
8364
|
+
console.error(`\u274C Port ${targetPort} is already occupied by another service.`);
|
|
8365
|
+
process.exit(1);
|
|
8366
|
+
}
|
|
6710
8367
|
console.log("\u{1F680} Starting Claude Trigger Router (foreground)...");
|
|
6711
8368
|
console.log(" Press Ctrl+C to stop");
|
|
6712
8369
|
try {
|
|
6713
|
-
await run({ port });
|
|
8370
|
+
await run({ port: targetPort });
|
|
6714
8371
|
} catch (error) {
|
|
6715
8372
|
if (error.message?.includes("Invalid configuration")) {
|
|
6716
8373
|
console.error("\n\u274C Configuration error. Run 'ctr init' to create a config file.");
|
|
@@ -6720,7 +8377,14 @@ async function startForeground(port) {
|
|
|
6720
8377
|
process.exit(1);
|
|
6721
8378
|
}
|
|
6722
8379
|
}
|
|
6723
|
-
function startDaemon(port) {
|
|
8380
|
+
async function startDaemon(port) {
|
|
8381
|
+
const targetPort = port ?? getPort();
|
|
8382
|
+
const healthy = await waitForService(targetPort, 500);
|
|
8383
|
+
const occupied = await isTcpPortOccupied(targetPort, 500);
|
|
8384
|
+
if (!healthy && occupied && !isServiceRunning()) {
|
|
8385
|
+
console.log(`\u274C Port ${targetPort} is already occupied by another service.`);
|
|
8386
|
+
return;
|
|
8387
|
+
}
|
|
6724
8388
|
if (isServiceRunning()) {
|
|
6725
8389
|
console.log("\u2705 Service is already running in the background.");
|
|
6726
8390
|
return;
|
|
@@ -6731,30 +8395,58 @@ function startDaemon(port) {
|
|
|
6731
8395
|
if (port) {
|
|
6732
8396
|
childArgs.push("--port", String(port));
|
|
6733
8397
|
}
|
|
6734
|
-
const child = (0,
|
|
8398
|
+
const child = (0, import_child_process3.spawn)(nodeExec, childArgs, {
|
|
6735
8399
|
detached: true,
|
|
6736
8400
|
stdio: "ignore",
|
|
6737
8401
|
env: { ...process.env, CTR_DAEMON: "1" }
|
|
6738
8402
|
});
|
|
6739
8403
|
child.unref();
|
|
6740
|
-
const
|
|
6741
|
-
|
|
6742
|
-
|
|
6743
|
-
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6750
|
-
|
|
6751
|
-
|
|
6752
|
-
|
|
6753
|
-
|
|
6754
|
-
|
|
6755
|
-
|
|
8404
|
+
const startConfirmed = await new Promise((resolve, reject) => {
|
|
8405
|
+
let settled = false;
|
|
8406
|
+
const deadline = Date.now() + 5e3;
|
|
8407
|
+
const finish = (value) => {
|
|
8408
|
+
if (settled) {
|
|
8409
|
+
return;
|
|
8410
|
+
}
|
|
8411
|
+
settled = true;
|
|
8412
|
+
resolve(value);
|
|
8413
|
+
};
|
|
8414
|
+
child.once("error", reject);
|
|
8415
|
+
child.once("exit", () => finish(false));
|
|
8416
|
+
const poll = () => {
|
|
8417
|
+
if (settled) {
|
|
8418
|
+
return;
|
|
8419
|
+
}
|
|
8420
|
+
if (isServiceRunning()) {
|
|
8421
|
+
finish(true);
|
|
8422
|
+
return;
|
|
8423
|
+
}
|
|
8424
|
+
if (Date.now() >= deadline) {
|
|
8425
|
+
finish(false);
|
|
8426
|
+
return;
|
|
8427
|
+
}
|
|
8428
|
+
setTimeout(poll, 250);
|
|
8429
|
+
};
|
|
8430
|
+
poll();
|
|
8431
|
+
});
|
|
8432
|
+
if (!startConfirmed) {
|
|
8433
|
+
console.error(`\u274C Service failed to start in background (port: ${targetPort}).`);
|
|
8434
|
+
console.error(" Run 'ctr start' (without --daemon) to inspect the startup error.");
|
|
8435
|
+
process.exit(1);
|
|
8436
|
+
}
|
|
8437
|
+
console.log(`\u2705 Service started in background (port: ${targetPort})`);
|
|
8438
|
+
console.log(` Run 'ctr stop' to stop it.`);
|
|
8439
|
+
}
|
|
8440
|
+
async function showStatus() {
|
|
6756
8441
|
const info = readServiceInfo();
|
|
6757
8442
|
if (!info || !isServiceRunning()) {
|
|
8443
|
+
const targetPort = getPort();
|
|
8444
|
+
const healthy = await waitForService(targetPort, 500);
|
|
8445
|
+
const occupied = await isTcpPortOccupied(targetPort, 500);
|
|
8446
|
+
if (!healthy && occupied) {
|
|
8447
|
+
console.log(`\u26A0\uFE0F \u7AEF\u53E3 ${targetPort} \u5DF2\u88AB\u5176\u4ED6\u670D\u52A1\u5360\u7528\uFF0C\u5F53\u524D\u4E0D\u662F claude-trigger-router\u3002`);
|
|
8448
|
+
return;
|
|
8449
|
+
}
|
|
6758
8450
|
console.log("\u23F9 \u670D\u52A1\u672A\u8FD0\u884C");
|
|
6759
8451
|
return;
|
|
6760
8452
|
}
|
|
@@ -6779,9 +8471,10 @@ function stopService() {
|
|
|
6779
8471
|
console.error("\u274C \u505C\u6B62\u670D\u52A1\u5931\u8D25:", error.message);
|
|
6780
8472
|
}
|
|
6781
8473
|
}
|
|
6782
|
-
function restartService() {
|
|
8474
|
+
async function restartService() {
|
|
6783
8475
|
stopService();
|
|
6784
|
-
|
|
8476
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
8477
|
+
await startDaemon(getPort());
|
|
6785
8478
|
}
|
|
6786
8479
|
async function runClaudeCode() {
|
|
6787
8480
|
const port = getPort();
|
|
@@ -6796,14 +8489,16 @@ async function runClaudeCode() {
|
|
|
6796
8489
|
console.log(" 1. Start service first: ctr start --daemon");
|
|
6797
8490
|
console.log(" 2. Or start interactively in another terminal: ctr start");
|
|
6798
8491
|
console.log("");
|
|
6799
|
-
|
|
6800
|
-
if (!proceed) {
|
|
6801
|
-
process.exit(1);
|
|
6802
|
-
}
|
|
8492
|
+
process.exit(1);
|
|
6803
8493
|
}
|
|
6804
8494
|
console.log(`\u{1F680} Starting Claude Code with Trigger Router (port: ${port})...`);
|
|
8495
|
+
if (!isClaudeCommandAvailable()) {
|
|
8496
|
+
console.error("\u274C \u672A\u68C0\u6D4B\u5230 Claude Code CLI\u3002");
|
|
8497
|
+
console.log(" \u8BF7\u5148\u5B89\u88C5\uFF1Anpm install -g @anthropic-ai/claude-code");
|
|
8498
|
+
process.exit(1);
|
|
8499
|
+
}
|
|
6805
8500
|
const isWindows = process.platform === "win32";
|
|
6806
|
-
const claude = (0,
|
|
8501
|
+
const claude = (0, import_child_process3.spawn)("claude", [], {
|
|
6807
8502
|
stdio: "inherit",
|
|
6808
8503
|
shell: isWindows,
|
|
6809
8504
|
env: {
|
|
@@ -6822,10 +8517,18 @@ async function runClaudeCode() {
|
|
|
6822
8517
|
process.exit(code || 0);
|
|
6823
8518
|
});
|
|
6824
8519
|
}
|
|
6825
|
-
function openUI() {
|
|
8520
|
+
async function openUI() {
|
|
6826
8521
|
const port = getPort();
|
|
6827
8522
|
const url = `http://127.0.0.1:${port}/ui`;
|
|
8523
|
+
const healthy = await waitForService(port, 800);
|
|
6828
8524
|
console.log(`\u{1F310} Opening UI at ${url}`);
|
|
8525
|
+
if (!healthy) {
|
|
8526
|
+
console.log("\u26A0\uFE0F \u5F53\u524D UI \u670D\u52A1\u672A\u5C31\u7EEA\uFF1B\u5982\u679C\u9875\u9762\u65E0\u6CD5\u6253\u5F00\uFF0C\u8BF7\u5148\u8FD0\u884C ctr start \u6216 ctr start --daemon\u3002");
|
|
8527
|
+
}
|
|
8528
|
+
if (process.env.CTR_UI_SKIP_OPEN === "1") {
|
|
8529
|
+
console.log(" Browser launch skipped by CTR_UI_SKIP_OPEN=1");
|
|
8530
|
+
return;
|
|
8531
|
+
}
|
|
6829
8532
|
try {
|
|
6830
8533
|
(0, import_openurl.default)(url);
|
|
6831
8534
|
} catch (error) {
|
|
@@ -6838,12 +8541,15 @@ async function main() {
|
|
|
6838
8541
|
case "setup":
|
|
6839
8542
|
await runSetupCli();
|
|
6840
8543
|
break;
|
|
8544
|
+
case "doctor":
|
|
8545
|
+
await runDoctorCli();
|
|
8546
|
+
break;
|
|
6841
8547
|
case "init":
|
|
6842
8548
|
initConfig2();
|
|
6843
8549
|
break;
|
|
6844
8550
|
case "start":
|
|
6845
8551
|
if (isDaemonMode()) {
|
|
6846
|
-
startDaemon(getPort());
|
|
8552
|
+
await startDaemon(getPort());
|
|
6847
8553
|
} else {
|
|
6848
8554
|
await startForeground(getPort());
|
|
6849
8555
|
}
|
|
@@ -6852,7 +8558,7 @@ async function main() {
|
|
|
6852
8558
|
stopService();
|
|
6853
8559
|
break;
|
|
6854
8560
|
case "status":
|
|
6855
|
-
showStatus();
|
|
8561
|
+
await showStatus();
|
|
6856
8562
|
break;
|
|
6857
8563
|
case "version":
|
|
6858
8564
|
await printVersion();
|
|
@@ -6861,13 +8567,14 @@ async function main() {
|
|
|
6861
8567
|
printUpgradeGuidance();
|
|
6862
8568
|
break;
|
|
6863
8569
|
case "restart":
|
|
6864
|
-
|
|
8570
|
+
printRestartGuidanceHint();
|
|
8571
|
+
await restartService();
|
|
6865
8572
|
break;
|
|
6866
8573
|
case "code":
|
|
6867
8574
|
await runClaudeCode();
|
|
6868
8575
|
break;
|
|
6869
8576
|
case "ui":
|
|
6870
|
-
openUI();
|
|
8577
|
+
await openUI();
|
|
6871
8578
|
break;
|
|
6872
8579
|
case "help":
|
|
6873
8580
|
case "--help":
|
|
@@ -6882,21 +8589,24 @@ async function main() {
|
|
|
6882
8589
|
process.exit(command ? 1 : 0);
|
|
6883
8590
|
}
|
|
6884
8591
|
}
|
|
6885
|
-
var
|
|
8592
|
+
var import_child_process3, import_path7, import_openurl, import_fs8, PACKAGE_JSON_PATH, PACKAGE_PAGE_URL, PACKAGE_REGISTRY_LATEST_URL, PACKAGE_REGISTRY_URL;
|
|
6886
8593
|
var init_cli = __esm({
|
|
6887
8594
|
"src/cli.ts"() {
|
|
6888
|
-
|
|
8595
|
+
import_child_process3 = require("child_process");
|
|
6889
8596
|
import_path7 = require("path");
|
|
6890
8597
|
import_openurl = __toESM(require("openurl"));
|
|
6891
|
-
|
|
8598
|
+
import_fs8 = require("fs");
|
|
6892
8599
|
init_index();
|
|
6893
8600
|
init_processCheck();
|
|
6894
8601
|
init_constants();
|
|
6895
8602
|
init_service_health();
|
|
6896
8603
|
init_setup2();
|
|
8604
|
+
init_templates();
|
|
8605
|
+
init_doctor();
|
|
6897
8606
|
PACKAGE_JSON_PATH = (0, import_path7.join)(__dirname, "..", "package.json");
|
|
6898
8607
|
PACKAGE_PAGE_URL = "https://www.npmjs.com/package/@peterwangze/claude-trigger-router";
|
|
6899
8608
|
PACKAGE_REGISTRY_LATEST_URL = "https://registry.npmjs.org/@peterwangze%2Fclaude-trigger-router/latest";
|
|
8609
|
+
PACKAGE_REGISTRY_URL = "https://registry.npmjs.org/";
|
|
6900
8610
|
if (process.env.CTR_SKIP_MAIN !== "1") {
|
|
6901
8611
|
main().catch((error) => {
|
|
6902
8612
|
console.error("Error:", error);
|