@peterwangze/claude-trigger-router 1.0.5 → 1.0.7
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 +35 -49
- package/config/trigger.advanced.yaml +16 -20
- package/dist/cli.js +1371 -320
- package/dist/cli.js.map +3 -3
- package/package.json +74 -66
package/dist/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
32
32
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
33
|
|
|
34
34
|
// src/constants.ts
|
|
35
|
-
var import_os, import_path, CONFIG_DIR, CONFIG_FILE, CONFIG_FILE_JSON, CONFIG_FILE_YML, HOME_DIR, PID_FILE, GOVERNANCE_TRACE_FILE, GOVERNANCE_TRACE_ARCHIVE_DIR, GOVERNANCE_EXPORT_HISTORY_FILE, GOVERNANCE_SNAPSHOT_DIR, GOVERNANCE_SCHEDULE_FILE, DEFAULT_CONFIG2,
|
|
35
|
+
var import_os, import_path, CONFIG_DIR, CONFIG_FILE, CONFIG_FILE_JSON, CONFIG_FILE_YML, HOME_DIR, PID_FILE, GOVERNANCE_TRACE_FILE, GOVERNANCE_TRACE_ARCHIVE_DIR, GOVERNANCE_EXPORT_HISTORY_FILE, GOVERNANCE_SNAPSHOT_DIR, GOVERNANCE_SCHEDULE_FILE, DEFAULT_CONFIG2, DEFAULT_SMART_ROUTER_CONFIG, DEFAULT_GOVERNANCE_CONFIG;
|
|
36
36
|
var init_constants = __esm({
|
|
37
37
|
"src/constants.ts"() {
|
|
38
38
|
"use strict";
|
|
@@ -57,14 +57,9 @@ var init_constants = __esm({
|
|
|
57
57
|
API_TIMEOUT_MS: 6e5,
|
|
58
58
|
NON_INTERACTIVE_MODE: false
|
|
59
59
|
};
|
|
60
|
-
DEFAULT_TRIGGER_CONFIG = {
|
|
61
|
-
enabled: true,
|
|
62
|
-
analysis_scope: "last_message",
|
|
63
|
-
llm_intent_recognition: false,
|
|
64
|
-
rules: []
|
|
65
|
-
};
|
|
66
60
|
DEFAULT_SMART_ROUTER_CONFIG = {
|
|
67
61
|
enabled: false,
|
|
62
|
+
analysis_scope: "last_message",
|
|
68
63
|
router_model: "",
|
|
69
64
|
candidates: [],
|
|
70
65
|
cache_ttl: 6e5,
|
|
@@ -130,8 +125,77 @@ var init_constants = __esm({
|
|
|
130
125
|
});
|
|
131
126
|
|
|
132
127
|
// src/models/schema.ts
|
|
128
|
+
function trimTrailingSlash(value) {
|
|
129
|
+
return value.replace(/\/+$/, "");
|
|
130
|
+
}
|
|
131
|
+
function inferInterfaceFromApiEndpoint(api, modelName) {
|
|
132
|
+
const trimmed = api?.trim().toLowerCase();
|
|
133
|
+
if (!trimmed) {
|
|
134
|
+
return void 0;
|
|
135
|
+
}
|
|
136
|
+
if (trimmed.includes("/chat/completions")) {
|
|
137
|
+
return "openai";
|
|
138
|
+
}
|
|
139
|
+
if (trimmed.includes("api.anthropic.com")) {
|
|
140
|
+
return "anthropic";
|
|
141
|
+
}
|
|
142
|
+
if (trimmed.includes("/messages")) {
|
|
143
|
+
return "anthropic";
|
|
144
|
+
}
|
|
145
|
+
const normalizedModelName = modelName?.trim().toLowerCase() || "";
|
|
146
|
+
if (normalizedModelName.startsWith("claude") && !trimmed.includes("/v1/chat/completions") && (trimmed.endsWith("/v1") || /^https?:\/\/[^/]+\/?$/.test(trimmed))) {
|
|
147
|
+
return "anthropic";
|
|
148
|
+
}
|
|
149
|
+
return trimmed.includes("/v1/messages") ? "anthropic" : "openai";
|
|
150
|
+
}
|
|
151
|
+
function normalizeEndpointPath(pathname, modelInterface) {
|
|
152
|
+
const trimmedPath = trimTrailingSlash(pathname || "");
|
|
153
|
+
const normalizedPath = trimmedPath || "";
|
|
154
|
+
const lowerPath = normalizedPath.toLowerCase();
|
|
155
|
+
if (modelInterface === "anthropic") {
|
|
156
|
+
if (lowerPath.endsWith("/v1/messages") || lowerPath.endsWith("/messages")) {
|
|
157
|
+
return normalizedPath || "/v1/messages";
|
|
158
|
+
}
|
|
159
|
+
if (lowerPath.endsWith("/v1")) {
|
|
160
|
+
return `${normalizedPath}/messages`;
|
|
161
|
+
}
|
|
162
|
+
if (!normalizedPath) {
|
|
163
|
+
return "/v1/messages";
|
|
164
|
+
}
|
|
165
|
+
return `${normalizedPath}/messages`;
|
|
166
|
+
}
|
|
167
|
+
if (lowerPath.endsWith("/chat/completions")) {
|
|
168
|
+
return normalizedPath || "/chat/completions";
|
|
169
|
+
}
|
|
170
|
+
if (lowerPath.endsWith("/v1")) {
|
|
171
|
+
return `${normalizedPath}/chat/completions`;
|
|
172
|
+
}
|
|
173
|
+
if (!normalizedPath) {
|
|
174
|
+
return "/v1/chat/completions";
|
|
175
|
+
}
|
|
176
|
+
return `${normalizedPath}/chat/completions`;
|
|
177
|
+
}
|
|
178
|
+
function normalizeApiEndpoint(api, explicitInterface) {
|
|
179
|
+
const trimmed = api?.trim() || "";
|
|
180
|
+
if (!trimmed) {
|
|
181
|
+
return "";
|
|
182
|
+
}
|
|
183
|
+
const modelInterface = explicitInterface ?? inferInterfaceFromApiEndpoint(trimmed) ?? "openai";
|
|
184
|
+
try {
|
|
185
|
+
const url = new URL(trimmed);
|
|
186
|
+
url.pathname = normalizeEndpointPath(url.pathname, modelInterface);
|
|
187
|
+
return url.toString();
|
|
188
|
+
} catch {
|
|
189
|
+
const [base, suffix = ""] = trimmed.split(/([?#].*)/, 2);
|
|
190
|
+
const normalizedBase = trimTrailingSlash(base);
|
|
191
|
+
const normalizedPath = normalizeEndpointPath(normalizedBase, modelInterface);
|
|
192
|
+
return `${normalizedPath}${suffix}`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
133
195
|
function getModelApi(item) {
|
|
134
|
-
|
|
196
|
+
const rawApi = item.api?.trim() || item.api_base_url?.trim() || "";
|
|
197
|
+
const explicitInterface = item.interface || item.protocol;
|
|
198
|
+
return normalizeApiEndpoint(rawApi, explicitInterface);
|
|
135
199
|
}
|
|
136
200
|
function getModelKey(item) {
|
|
137
201
|
return item.key?.trim() || item.api_key?.trim() || "";
|
|
@@ -228,36 +292,59 @@ function inferCompatibilityProfile(item, modelInterface) {
|
|
|
228
292
|
if (modelInterface === "anthropic") {
|
|
229
293
|
return "anthropic-native";
|
|
230
294
|
}
|
|
231
|
-
|
|
232
|
-
const vendorHint = item.metadata?.vendor_hint?.trim().toLowerCase();
|
|
233
|
-
if (vendorHint === "openrouter" || api.includes("openrouter.ai")) {
|
|
234
|
-
return "openrouter-like";
|
|
235
|
-
}
|
|
236
|
-
if (vendorHint === "qianfan" || vendorHint === "qianfan-coding" || api.includes("qianfan.baidubce.com/v2/coding")) {
|
|
237
|
-
return "qianfan-coding";
|
|
238
|
-
}
|
|
239
|
-
if (vendorHint === "minimax" || vendorHint === "minimax-chatcompletion-v2" || api.includes("/v1/text/chatcompletion_v2")) {
|
|
240
|
-
return "minimax-chatcompletion-v2";
|
|
241
|
-
}
|
|
242
|
-
return "generic-openai-compatible";
|
|
295
|
+
return "openai-compatible-anthropic-dispatch";
|
|
243
296
|
}
|
|
244
297
|
function getDispatchFormatForProfile(modelInterface, compatibilityProfile) {
|
|
245
298
|
if (modelInterface === "anthropic") {
|
|
246
299
|
return "anthropic_messages";
|
|
247
300
|
}
|
|
248
301
|
switch (compatibilityProfile) {
|
|
249
|
-
case "
|
|
250
|
-
case "qianfan-coding":
|
|
251
|
-
case "minimax-chatcompletion-v2":
|
|
302
|
+
case "openai-compatible-anthropic-dispatch":
|
|
252
303
|
return "anthropic_messages";
|
|
253
304
|
case "anthropic-native":
|
|
254
305
|
return "anthropic_messages";
|
|
255
|
-
case "generic-openai-compatible":
|
|
256
|
-
return "anthropic_messages";
|
|
257
306
|
default:
|
|
258
307
|
return "anthropic_messages";
|
|
259
308
|
}
|
|
260
309
|
}
|
|
310
|
+
function describeCompatibilityProfile(profile) {
|
|
311
|
+
switch (profile) {
|
|
312
|
+
case "anthropic-native":
|
|
313
|
+
return {
|
|
314
|
+
label: "Anthropic native",
|
|
315
|
+
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"
|
|
316
|
+
};
|
|
317
|
+
case "openai-compatible-anthropic-dispatch":
|
|
318
|
+
return {
|
|
319
|
+
label: "OpenAI-compatible / Anthropic dispatch",
|
|
320
|
+
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"
|
|
321
|
+
};
|
|
322
|
+
default:
|
|
323
|
+
return {
|
|
324
|
+
label: profile,
|
|
325
|
+
summary: "\u672A\u77E5\u517C\u5BB9\u753B\u50CF\u3002"
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function describeDispatchFormat(format) {
|
|
330
|
+
switch (format) {
|
|
331
|
+
case "anthropic_messages":
|
|
332
|
+
return {
|
|
333
|
+
label: "Anthropic-style messages",
|
|
334
|
+
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"
|
|
335
|
+
};
|
|
336
|
+
case "openai_chat":
|
|
337
|
+
return {
|
|
338
|
+
label: "OpenAI chat completions",
|
|
339
|
+
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"
|
|
340
|
+
};
|
|
341
|
+
default:
|
|
342
|
+
return {
|
|
343
|
+
label: format,
|
|
344
|
+
summary: "\u672A\u77E5 dispatch \u5F62\u6001\u3002"
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
}
|
|
261
348
|
function buildCompiledCapabilities(item, modelInterface) {
|
|
262
349
|
const reasoningSupported = item.metadata?.supports_reasoning !== false;
|
|
263
350
|
return {
|
|
@@ -313,12 +400,7 @@ function buildModelRegistry(config) {
|
|
|
313
400
|
const modelMap = providers.reduce((result, provider) => {
|
|
314
401
|
for (const model of provider.models ?? []) {
|
|
315
402
|
const compatibilityProfile = inferCompatibilityProfile(
|
|
316
|
-
{
|
|
317
|
-
api_base_url: provider.api_base_url,
|
|
318
|
-
metadata: {
|
|
319
|
-
vendor_hint: provider.transformer?.use?.[0]
|
|
320
|
-
}
|
|
321
|
-
},
|
|
403
|
+
{ api_base_url: provider.api_base_url },
|
|
322
404
|
"openai"
|
|
323
405
|
);
|
|
324
406
|
result[`${provider.name},${model}`] = {
|
|
@@ -576,6 +658,76 @@ function validateModelRef(ref, providers, fieldName) {
|
|
|
576
658
|
}
|
|
577
659
|
return null;
|
|
578
660
|
}
|
|
661
|
+
function validateKnownModelRef(ref, config, providers, fieldName) {
|
|
662
|
+
if (isKnownModelReference(config, ref)) {
|
|
663
|
+
return null;
|
|
664
|
+
}
|
|
665
|
+
return validateModelRef(ref, providers, fieldName);
|
|
666
|
+
}
|
|
667
|
+
function validateRoutingRule(rule, index, containerName, config, validProviders, errors) {
|
|
668
|
+
if (!rule.name) {
|
|
669
|
+
errors.push(`${containerName}[${index}].name is required`);
|
|
670
|
+
}
|
|
671
|
+
if (!rule.model) {
|
|
672
|
+
errors.push(`${containerName}[${index}].model is required`);
|
|
673
|
+
} else if (validProviders.length > 0) {
|
|
674
|
+
const err = validateKnownModelRef(rule.model, config, validProviders, `${containerName}[${index}].model`);
|
|
675
|
+
if (err) errors.push(err);
|
|
676
|
+
}
|
|
677
|
+
const hasSemanticOnlyMatch = Boolean(
|
|
678
|
+
rule.description || rule.semantic_profile?.prototype || rule.semantic_profile?.enabled
|
|
679
|
+
);
|
|
680
|
+
if ((!rule.patterns || rule.patterns.length === 0) && !hasSemanticOnlyMatch) {
|
|
681
|
+
errors.push(`${containerName}[${index}].patterns must be a non-empty array`);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
function validateStickyRoutingConfig(sticky, config, validProviders, prefix, errors) {
|
|
685
|
+
if (!sticky?.enabled) {
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if ((sticky.session_ttl_ms ?? 0) <= 0) {
|
|
689
|
+
errors.push(`${prefix}.session_ttl_ms must be greater than 0 when sticky routing is enabled`);
|
|
690
|
+
}
|
|
691
|
+
const threshold = sticky.fingerprint_similarity_threshold;
|
|
692
|
+
if (threshold !== void 0 && (threshold < 0 || threshold > 1)) {
|
|
693
|
+
errors.push(`${prefix}.fingerprint_similarity_threshold must be between 0 and 1`);
|
|
694
|
+
}
|
|
695
|
+
if (sticky.alignment?.enabled) {
|
|
696
|
+
if (!sticky.alignment.summarizer_model) {
|
|
697
|
+
errors.push(`${prefix}.alignment.summarizer_model is required when alignment is enabled`);
|
|
698
|
+
} else if (!isKnownModelReference(config, sticky.alignment.summarizer_model)) {
|
|
699
|
+
const err = validateModelRef(
|
|
700
|
+
sticky.alignment.summarizer_model,
|
|
701
|
+
validProviders,
|
|
702
|
+
`${prefix}.alignment.summarizer_model`
|
|
703
|
+
);
|
|
704
|
+
if (err) errors.push(err);
|
|
705
|
+
}
|
|
706
|
+
if ((sticky.alignment.max_summary_tokens ?? 0) <= 0) {
|
|
707
|
+
errors.push(`${prefix}.alignment.max_summary_tokens must be greater than 0 when alignment is enabled`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
function validateSemanticRoutingConfig(semantic, config, validProviders, prefix, errors) {
|
|
712
|
+
if (!semantic?.enabled) {
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
const threshold = semantic.threshold;
|
|
716
|
+
if (threshold !== void 0 && (threshold < 0 || threshold > 1)) {
|
|
717
|
+
errors.push(`${prefix}.threshold must be between 0 and 1`);
|
|
718
|
+
}
|
|
719
|
+
if (semantic.mode && !["embedding", "classifier"].includes(semantic.mode)) {
|
|
720
|
+
errors.push(`${prefix}.mode must be either "embedding" or "classifier"`);
|
|
721
|
+
}
|
|
722
|
+
if (semantic.mode === "classifier") {
|
|
723
|
+
if (!semantic.classifier_model) {
|
|
724
|
+
errors.push(`${prefix}.classifier_model is required when semantic mode is "classifier"`);
|
|
725
|
+
} else if (!isKnownModelReference(config, semantic.classifier_model)) {
|
|
726
|
+
const err = validateModelRef(semantic.classifier_model, validProviders, `${prefix}.classifier_model`);
|
|
727
|
+
if (err) errors.push(err);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
579
731
|
function validateConfig(config) {
|
|
580
732
|
const errors = [];
|
|
581
733
|
if (config.Models !== void 0) {
|
|
@@ -639,6 +791,7 @@ function validateConfig(config) {
|
|
|
639
791
|
errors.push("Router.default is required");
|
|
640
792
|
}
|
|
641
793
|
const validProviders = config.Providers?.filter((p) => p.name && p.models?.length) ?? [];
|
|
794
|
+
const runtimeSmartRouter = deriveRuntimeSmartRouterConfig(config, config);
|
|
642
795
|
if (validProviders.length > 0) {
|
|
643
796
|
const router2 = config.Router;
|
|
644
797
|
if (router2) {
|
|
@@ -652,51 +805,28 @@ function validateConfig(config) {
|
|
|
652
805
|
];
|
|
653
806
|
for (const [ref, field] of routerModelFields) {
|
|
654
807
|
if (ref) {
|
|
655
|
-
const err =
|
|
808
|
+
const err = validateKnownModelRef(ref, config, validProviders, field);
|
|
656
809
|
if (err) errors.push(err);
|
|
657
810
|
}
|
|
658
811
|
}
|
|
659
812
|
}
|
|
660
813
|
}
|
|
661
|
-
if (
|
|
662
|
-
if (
|
|
663
|
-
|
|
664
|
-
} else if (config.TriggerRouter.intent_model && validProviders.length > 0) {
|
|
665
|
-
const err = validateModelRef(config.TriggerRouter.intent_model, validProviders, "TriggerRouter.intent_model");
|
|
814
|
+
if (runtimeSmartRouter?.enabled) {
|
|
815
|
+
if (runtimeSmartRouter.router_model && validProviders.length > 0) {
|
|
816
|
+
const err = validateKnownModelRef(runtimeSmartRouter.router_model, config, validProviders, "SmartRouter.router_model");
|
|
666
817
|
if (err) errors.push(err);
|
|
667
818
|
}
|
|
668
|
-
if (
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
if (!rule.model) {
|
|
674
|
-
errors.push(`TriggerRouter.rules[${index}].model is required`);
|
|
675
|
-
} else if (validProviders.length > 0) {
|
|
676
|
-
const err = validateModelRef(rule.model, validProviders, `TriggerRouter.rules[${index}].model`);
|
|
677
|
-
if (err) errors.push(err);
|
|
678
|
-
}
|
|
679
|
-
if (!rule.patterns || rule.patterns.length === 0) {
|
|
680
|
-
errors.push(`TriggerRouter.rules[${index}].patterns must be a non-empty array`);
|
|
681
|
-
}
|
|
682
|
-
});
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
if (config.SmartRouter?.enabled) {
|
|
686
|
-
if (!config.SmartRouter.router_model) {
|
|
687
|
-
errors.push("SmartRouter.router_model is required when SmartRouter is enabled");
|
|
688
|
-
} else if (validProviders.length > 0) {
|
|
689
|
-
const err = validateModelRef(config.SmartRouter.router_model, validProviders, "SmartRouter.router_model");
|
|
690
|
-
if (err) errors.push(err);
|
|
819
|
+
if (runtimeSmartRouter.router_model) {
|
|
820
|
+
if (!runtimeSmartRouter.candidates || runtimeSmartRouter.candidates.length < 2) {
|
|
821
|
+
errors.push("SmartRouter.candidates must have at least 2 entries when SmartRouter.router_model is configured");
|
|
822
|
+
}
|
|
691
823
|
}
|
|
692
|
-
if (
|
|
693
|
-
|
|
694
|
-
} else {
|
|
695
|
-
config.SmartRouter.candidates.forEach((candidate, index) => {
|
|
824
|
+
if (runtimeSmartRouter.candidates && runtimeSmartRouter.candidates.length > 0) {
|
|
825
|
+
runtimeSmartRouter.candidates.forEach((candidate, index) => {
|
|
696
826
|
if (!candidate.model) {
|
|
697
827
|
errors.push(`SmartRouter.candidates[${index}].model is required`);
|
|
698
828
|
} else if (validProviders.length > 0) {
|
|
699
|
-
const err =
|
|
829
|
+
const err = validateKnownModelRef(candidate.model, config, validProviders, `SmartRouter.candidates[${index}].model`);
|
|
700
830
|
if (err) errors.push(err);
|
|
701
831
|
}
|
|
702
832
|
if (!candidate.description) {
|
|
@@ -704,33 +834,15 @@ function validateConfig(config) {
|
|
|
704
834
|
}
|
|
705
835
|
});
|
|
706
836
|
}
|
|
837
|
+
if (runtimeSmartRouter.rules) {
|
|
838
|
+
runtimeSmartRouter.rules.forEach((rule, index) => {
|
|
839
|
+
validateRoutingRule(rule, index, "SmartRouter.rules", config, validProviders, errors);
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
validateStickyRoutingConfig(runtimeSmartRouter.sticky, config, validProviders, "SmartRouter.sticky", errors);
|
|
843
|
+
validateSemanticRoutingConfig(runtimeSmartRouter.semantic, config, validProviders, "SmartRouter.semantic", errors);
|
|
707
844
|
}
|
|
708
845
|
if (config.Governance?.enabled) {
|
|
709
|
-
const sticky = config.Governance.sticky;
|
|
710
|
-
if (sticky?.enabled) {
|
|
711
|
-
if ((sticky.session_ttl_ms ?? 0) <= 0) {
|
|
712
|
-
errors.push("Governance.sticky.session_ttl_ms must be greater than 0 when sticky routing is enabled");
|
|
713
|
-
}
|
|
714
|
-
const threshold = sticky.fingerprint_similarity_threshold;
|
|
715
|
-
if (threshold !== void 0 && (threshold < 0 || threshold > 1)) {
|
|
716
|
-
errors.push("Governance.sticky.fingerprint_similarity_threshold must be between 0 and 1");
|
|
717
|
-
}
|
|
718
|
-
if (sticky.alignment?.enabled) {
|
|
719
|
-
if (!sticky.alignment.summarizer_model) {
|
|
720
|
-
errors.push("Governance.sticky.alignment.summarizer_model is required when alignment is enabled");
|
|
721
|
-
} else if (!isKnownModelReference(config, sticky.alignment.summarizer_model)) {
|
|
722
|
-
const err = validateModelRef(
|
|
723
|
-
sticky.alignment.summarizer_model,
|
|
724
|
-
validProviders,
|
|
725
|
-
"Governance.sticky.alignment.summarizer_model"
|
|
726
|
-
);
|
|
727
|
-
if (err) errors.push(err);
|
|
728
|
-
}
|
|
729
|
-
if ((sticky.alignment.max_summary_tokens ?? 0) <= 0) {
|
|
730
|
-
errors.push("Governance.sticky.alignment.max_summary_tokens must be greater than 0 when alignment is enabled");
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
846
|
const cascade = config.Governance.cascade;
|
|
735
847
|
if (cascade?.enabled) {
|
|
736
848
|
if ((cascade.max_attempts ?? 0) < 1) {
|
|
@@ -751,24 +863,6 @@ function validateConfig(config) {
|
|
|
751
863
|
}
|
|
752
864
|
});
|
|
753
865
|
}
|
|
754
|
-
const semantic = config.Governance.semantic;
|
|
755
|
-
if (semantic?.enabled) {
|
|
756
|
-
const threshold = semantic.threshold;
|
|
757
|
-
if (threshold !== void 0 && (threshold < 0 || threshold > 1)) {
|
|
758
|
-
errors.push("Governance.semantic.threshold must be between 0 and 1");
|
|
759
|
-
}
|
|
760
|
-
if (semantic.mode && !["embedding", "classifier"].includes(semantic.mode)) {
|
|
761
|
-
errors.push('Governance.semantic.mode must be either "embedding" or "classifier"');
|
|
762
|
-
}
|
|
763
|
-
if (semantic.mode === "classifier") {
|
|
764
|
-
if (!semantic.classifier_model) {
|
|
765
|
-
errors.push('Governance.semantic.classifier_model is required when semantic mode is "classifier"');
|
|
766
|
-
} else if (!isKnownModelReference(config, semantic.classifier_model)) {
|
|
767
|
-
const err = validateModelRef(semantic.classifier_model, validProviders, "Governance.semantic.classifier_model");
|
|
768
|
-
if (err) errors.push(err);
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
|
-
}
|
|
772
866
|
const shadow = config.Governance.shadow;
|
|
773
867
|
if (shadow?.enabled) {
|
|
774
868
|
const sampleRate = shadow.sample_rate;
|
|
@@ -823,7 +917,141 @@ function validateConfig(config) {
|
|
|
823
917
|
}
|
|
824
918
|
return errors;
|
|
825
919
|
}
|
|
920
|
+
function normalizeUnifiedRouterInput(config) {
|
|
921
|
+
const routes = config.Router?.routes;
|
|
922
|
+
const decision = config.Router?.decision;
|
|
923
|
+
const defaults = config.Router?.defaults;
|
|
924
|
+
const hasUnifiedRouterInput = Boolean(
|
|
925
|
+
Array.isArray(routes) && routes.length || decision || defaults
|
|
926
|
+
);
|
|
927
|
+
if (!hasUnifiedRouterInput) {
|
|
928
|
+
return config;
|
|
929
|
+
}
|
|
930
|
+
const nextConfig = {
|
|
931
|
+
...config,
|
|
932
|
+
Router: {
|
|
933
|
+
...config.Router ?? {}
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
const normalizedRules = Array.isArray(routes) && routes.length > 0 ? routes.map((route) => ({
|
|
937
|
+
name: route.name,
|
|
938
|
+
priority: route.priority ?? 0,
|
|
939
|
+
enabled: route.enabled ?? true,
|
|
940
|
+
model: route.model,
|
|
941
|
+
description: route.description,
|
|
942
|
+
semantic_profile: route.match?.semantic || route.match?.semantic_profile ? {
|
|
943
|
+
enabled: route.match?.semantic ?? true,
|
|
944
|
+
prototype: route.match?.semantic_profile?.prototype,
|
|
945
|
+
threshold: route.match?.semantic_profile?.threshold
|
|
946
|
+
} : void 0,
|
|
947
|
+
patterns: [
|
|
948
|
+
...Array.isArray(route.match?.keywords) && route.match?.keywords.length ? [{
|
|
949
|
+
type: "exact",
|
|
950
|
+
keywords: route.match?.keywords
|
|
951
|
+
}] : [],
|
|
952
|
+
...typeof route.match?.regex === "string" && route.match.regex.trim().length ? [{
|
|
953
|
+
type: "regex",
|
|
954
|
+
pattern: route.match.regex
|
|
955
|
+
}] : []
|
|
956
|
+
]
|
|
957
|
+
})) : [];
|
|
958
|
+
const semanticPrototypes = Object.fromEntries(
|
|
959
|
+
normalizedRules.filter((rule) => rule.semantic_profile?.enabled !== false && (rule.semantic_profile?.prototype || rule.description)).map((rule) => [
|
|
960
|
+
rule.name,
|
|
961
|
+
rule.semantic_profile?.prototype ?? rule.description ?? ""
|
|
962
|
+
]).filter(([, prototype]) => typeof prototype === "string" && prototype.trim().length > 0)
|
|
963
|
+
);
|
|
964
|
+
if (decision || normalizedRules.length > 0 || defaults?.sticky || defaults?.semantic || config.SmartRouter) {
|
|
965
|
+
nextConfig.SmartRouter = {
|
|
966
|
+
...config.SmartRouter ?? DEFAULT_SMART_ROUTER_CONFIG,
|
|
967
|
+
enabled: decision?.smart_fallback ?? config.SmartRouter?.enabled ?? true,
|
|
968
|
+
router_model: decision?.router_model ?? config.SmartRouter?.router_model ?? "",
|
|
969
|
+
candidates: decision?.candidates ?? config.SmartRouter?.candidates ?? [],
|
|
970
|
+
cache_ttl: decision?.cache_ttl ?? config.SmartRouter?.cache_ttl,
|
|
971
|
+
max_tokens: decision?.max_tokens ?? config.SmartRouter?.max_tokens,
|
|
972
|
+
fallback: decision?.fallback ?? config.SmartRouter?.fallback,
|
|
973
|
+
router_hint: decision?.router_hint ?? config.SmartRouter?.router_hint,
|
|
974
|
+
rules: normalizedRules.length > 0 ? normalizedRules : config.SmartRouter?.rules,
|
|
975
|
+
semantic: Object.keys(semanticPrototypes).length > 0 || defaults?.semantic || config.SmartRouter?.semantic ? {
|
|
976
|
+
...config.SmartRouter?.semantic ?? config.Governance?.semantic ?? {},
|
|
977
|
+
...defaults?.semantic ?? {},
|
|
978
|
+
prototypes: {
|
|
979
|
+
...config.Governance?.semantic?.prototypes ?? {},
|
|
980
|
+
...config.SmartRouter?.semantic?.prototypes ?? {},
|
|
981
|
+
...semanticPrototypes
|
|
982
|
+
}
|
|
983
|
+
} : config.SmartRouter?.semantic,
|
|
984
|
+
sticky: defaults?.sticky ? {
|
|
985
|
+
...config.SmartRouter?.sticky ?? config.Governance?.sticky ?? {},
|
|
986
|
+
...defaults.sticky
|
|
987
|
+
} : config.SmartRouter?.sticky ?? config.Governance?.sticky
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
return nextConfig;
|
|
991
|
+
}
|
|
992
|
+
function deriveRuntimeSmartRouterConfig(config, source) {
|
|
993
|
+
const smartRouterInput = source?.SmartRouter ?? config.SmartRouter;
|
|
994
|
+
const baseSmartRouterConfig = smartRouterInput ?? DEFAULT_SMART_ROUTER_CONFIG;
|
|
995
|
+
const legacyIntentEnabled = Boolean(config.TriggerRouter?.llm_intent_recognition);
|
|
996
|
+
const legacyIntentModel = config.TriggerRouter?.intent_model;
|
|
997
|
+
const legacySemanticPrototypes = Object.fromEntries(
|
|
998
|
+
(config.TriggerRouter?.rules ?? []).filter((rule) => rule.enabled !== false && rule.description).map((rule) => [rule.name, rule.description])
|
|
999
|
+
);
|
|
1000
|
+
const hasExplicitSmartRouterConfig = Boolean(
|
|
1001
|
+
source?.SmartRouter !== void 0 || baseSmartRouterConfig.enabled || baseSmartRouterConfig.router_model || baseSmartRouterConfig.rules?.length || baseSmartRouterConfig.candidates?.length || baseSmartRouterConfig.semantic || baseSmartRouterConfig.sticky
|
|
1002
|
+
);
|
|
1003
|
+
const defaultSummarizerModel = baseSmartRouterConfig.router_model || config.Router?.default || legacyIntentModel || "";
|
|
1004
|
+
const derivedSemantic = deepMerge(
|
|
1005
|
+
DEFAULT_GOVERNANCE_CONFIG.semantic,
|
|
1006
|
+
baseSmartRouterConfig.semantic ?? (legacyIntentEnabled || config.Governance?.semantic ? {
|
|
1007
|
+
...config.Governance?.semantic ?? {},
|
|
1008
|
+
...legacyIntentEnabled ? {
|
|
1009
|
+
enabled: true,
|
|
1010
|
+
mode: "classifier",
|
|
1011
|
+
classifier_model: legacyIntentModel,
|
|
1012
|
+
prototypes: {
|
|
1013
|
+
...config.Governance?.semantic?.prototypes ?? {},
|
|
1014
|
+
...legacySemanticPrototypes
|
|
1015
|
+
}
|
|
1016
|
+
} : {}
|
|
1017
|
+
} : {})
|
|
1018
|
+
);
|
|
1019
|
+
const derivedSticky = deepMerge(
|
|
1020
|
+
DEFAULT_GOVERNANCE_CONFIG.sticky,
|
|
1021
|
+
baseSmartRouterConfig.sticky ?? config.Governance?.sticky ?? {}
|
|
1022
|
+
);
|
|
1023
|
+
const smartRouterEnabled = hasExplicitSmartRouterConfig ? baseSmartRouterConfig.enabled : Boolean(config.TriggerRouter?.enabled);
|
|
1024
|
+
const hasExplicitSemanticToggle = Boolean(
|
|
1025
|
+
baseSmartRouterConfig.semantic || config.Governance?.semantic || legacyIntentEnabled
|
|
1026
|
+
);
|
|
1027
|
+
const hasExplicitStickyToggle = Boolean(
|
|
1028
|
+
baseSmartRouterConfig.sticky || config.Governance?.sticky
|
|
1029
|
+
);
|
|
1030
|
+
const semantic = smartRouterEnabled ? {
|
|
1031
|
+
...derivedSemantic,
|
|
1032
|
+
enabled: hasExplicitSemanticToggle ? baseSmartRouterConfig.semantic?.enabled ?? derivedSemantic.enabled : true,
|
|
1033
|
+
threshold: hasExplicitSemanticToggle ? derivedSemantic.threshold : 0.2
|
|
1034
|
+
} : derivedSemantic;
|
|
1035
|
+
const sticky = smartRouterEnabled ? {
|
|
1036
|
+
...derivedSticky,
|
|
1037
|
+
enabled: hasExplicitStickyToggle ? baseSmartRouterConfig.sticky?.enabled ?? derivedSticky.enabled : true,
|
|
1038
|
+
alignment: {
|
|
1039
|
+
...derivedSticky.alignment,
|
|
1040
|
+
enabled: hasExplicitStickyToggle && (baseSmartRouterConfig.sticky?.alignment || config.Governance?.sticky?.alignment) ? baseSmartRouterConfig.sticky?.alignment?.enabled ?? derivedSticky.alignment?.enabled : true,
|
|
1041
|
+
summarizer_model: baseSmartRouterConfig.sticky?.alignment?.summarizer_model || derivedSticky.alignment?.summarizer_model || defaultSummarizerModel
|
|
1042
|
+
}
|
|
1043
|
+
} : derivedSticky;
|
|
1044
|
+
return {
|
|
1045
|
+
...baseSmartRouterConfig,
|
|
1046
|
+
enabled: smartRouterEnabled,
|
|
1047
|
+
analysis_scope: baseSmartRouterConfig.analysis_scope ?? config.TriggerRouter?.analysis_scope ?? "last_message",
|
|
1048
|
+
rules: baseSmartRouterConfig.rules?.length ? baseSmartRouterConfig.rules : config.TriggerRouter?.rules ?? [],
|
|
1049
|
+
semantic,
|
|
1050
|
+
sticky
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
826
1053
|
function normalizeAndValidateConfig(config = {}) {
|
|
1054
|
+
const normalizedInput = normalizeUnifiedRouterInput(config);
|
|
827
1055
|
const normalizedConfig = deepMerge(
|
|
828
1056
|
{
|
|
829
1057
|
...DEFAULT_CONFIG2,
|
|
@@ -833,16 +1061,32 @@ function normalizeAndValidateConfig(config = {}) {
|
|
|
833
1061
|
Providers: [],
|
|
834
1062
|
SmartRouter: DEFAULT_SMART_ROUTER_CONFIG
|
|
835
1063
|
},
|
|
836
|
-
|
|
1064
|
+
normalizedInput
|
|
837
1065
|
);
|
|
838
|
-
if (
|
|
839
|
-
normalizedConfig.
|
|
1066
|
+
if (normalizedInput.Governance) {
|
|
1067
|
+
normalizedConfig.Governance = deepMerge(DEFAULT_GOVERNANCE_CONFIG, normalizedInput.Governance);
|
|
1068
|
+
}
|
|
1069
|
+
normalizedConfig.SmartRouter = deepMerge(
|
|
1070
|
+
DEFAULT_SMART_ROUTER_CONFIG,
|
|
1071
|
+
deriveRuntimeSmartRouterConfig(normalizedConfig, normalizedInput)
|
|
1072
|
+
);
|
|
1073
|
+
if (normalizedInput.TriggerRouter || normalizedInput.SmartRouter || normalizedInput.Router?.routes || normalizedInput.Router?.decision || normalizedInput.Router?.defaults) {
|
|
1074
|
+
delete normalizedConfig.TriggerRouter;
|
|
1075
|
+
}
|
|
1076
|
+
if (normalizedConfig.SmartRouter?.sticky) {
|
|
1077
|
+
normalizedConfig.SmartRouter.sticky = deepMerge(
|
|
1078
|
+
DEFAULT_GOVERNANCE_CONFIG.sticky,
|
|
1079
|
+
normalizedConfig.SmartRouter.sticky
|
|
1080
|
+
);
|
|
840
1081
|
}
|
|
841
|
-
if (
|
|
842
|
-
normalizedConfig.
|
|
1082
|
+
if (normalizedConfig.SmartRouter?.semantic) {
|
|
1083
|
+
normalizedConfig.SmartRouter.semantic = deepMerge(
|
|
1084
|
+
DEFAULT_GOVERNANCE_CONFIG.semantic,
|
|
1085
|
+
normalizedConfig.SmartRouter.semantic
|
|
1086
|
+
);
|
|
843
1087
|
}
|
|
844
|
-
if (
|
|
845
|
-
normalizedConfig.Models =
|
|
1088
|
+
if (normalizedInput.Models) {
|
|
1089
|
+
normalizedConfig.Models = normalizedInput.Models.map((item) => normalizeModelEndpointConfig(item));
|
|
846
1090
|
}
|
|
847
1091
|
return {
|
|
848
1092
|
config: normalizedConfig,
|
|
@@ -1367,7 +1611,13 @@ function normalizeContentParts(content) {
|
|
|
1367
1611
|
return typeof item.text === "string" ? [{ type: "text", text: item.text }] : [];
|
|
1368
1612
|
case "image":
|
|
1369
1613
|
case "image_url":
|
|
1370
|
-
return [{
|
|
1614
|
+
return [{
|
|
1615
|
+
type: "image",
|
|
1616
|
+
source: item.source ?? (item.image_url ? {
|
|
1617
|
+
...item.image_url,
|
|
1618
|
+
...item.media_type ? { media_type: item.media_type } : {}
|
|
1619
|
+
} : item.url)
|
|
1620
|
+
}];
|
|
1371
1621
|
case "tool_use":
|
|
1372
1622
|
return item.id && item.name ? [{ type: "tool_call", id: item.id, name: item.name, arguments: JSON.stringify(item.input ?? {}) }] : [];
|
|
1373
1623
|
case "tool_result":
|
|
@@ -1377,12 +1627,54 @@ function normalizeContentParts(content) {
|
|
|
1377
1627
|
}
|
|
1378
1628
|
});
|
|
1379
1629
|
}
|
|
1630
|
+
function normalizeOpenAIToolCalls(toolCalls) {
|
|
1631
|
+
if (!Array.isArray(toolCalls)) {
|
|
1632
|
+
return [];
|
|
1633
|
+
}
|
|
1634
|
+
return toolCalls.flatMap((toolCall) => {
|
|
1635
|
+
const id = toolCall?.id;
|
|
1636
|
+
const name = toolCall?.function?.name;
|
|
1637
|
+
const args = toolCall?.function?.arguments;
|
|
1638
|
+
if (!id || !name) {
|
|
1639
|
+
return [];
|
|
1640
|
+
}
|
|
1641
|
+
return [{
|
|
1642
|
+
type: "tool_call",
|
|
1643
|
+
id,
|
|
1644
|
+
name,
|
|
1645
|
+
arguments: typeof args === "string" ? args : JSON.stringify(args ?? {})
|
|
1646
|
+
}];
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1380
1649
|
function createMessageIR(input3) {
|
|
1381
|
-
const system = typeof input3.system === "string" ? [input3.system] : Array.isArray(input3.system) ? input3.system.flatMap(
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1650
|
+
const system = typeof input3.system === "string" ? [input3.system] : Array.isArray(input3.system) ? input3.system.flatMap(
|
|
1651
|
+
(item) => typeof item === "string" ? [item] : item?.type === "text" && typeof item.text === "string" ? [item.text] : []
|
|
1652
|
+
) : [];
|
|
1653
|
+
const messages = Array.isArray(input3.messages) ? input3.messages.filter((item) => item?.role).flatMap((item) => {
|
|
1654
|
+
if (item.role === "system") {
|
|
1655
|
+
const systemParts = typeof item.content === "string" ? [item.content] : normalizeContentParts(item.content).filter((part) => part.type === "text").map((part) => part.text);
|
|
1656
|
+
system.push(...systemParts);
|
|
1657
|
+
return [];
|
|
1658
|
+
}
|
|
1659
|
+
if (item.role === "tool" && item.tool_call_id) {
|
|
1660
|
+
return [{
|
|
1661
|
+
role: "user",
|
|
1662
|
+
parts: [{
|
|
1663
|
+
type: "tool_result",
|
|
1664
|
+
tool_call_id: item.tool_call_id,
|
|
1665
|
+
content: item.content
|
|
1666
|
+
}]
|
|
1667
|
+
}];
|
|
1668
|
+
}
|
|
1669
|
+
const parts = [
|
|
1670
|
+
...normalizeContentParts(item.content),
|
|
1671
|
+
...item.role === "assistant" ? normalizeOpenAIToolCalls(item.tool_calls) : []
|
|
1672
|
+
];
|
|
1673
|
+
return [{
|
|
1674
|
+
role: item.role,
|
|
1675
|
+
parts
|
|
1676
|
+
}];
|
|
1677
|
+
}) : [];
|
|
1386
1678
|
const thinking = input3.thinking ? {
|
|
1387
1679
|
enabled: input3.thinking?.type === "enabled" || input3.thinking?.enabled === true,
|
|
1388
1680
|
effort: input3.thinking?.effort,
|
|
@@ -1462,6 +1754,21 @@ function toAnthropicMessagesRequest(input3) {
|
|
|
1462
1754
|
input_schema: tool?.input_schema ?? tool?.function?.parameters
|
|
1463
1755
|
}));
|
|
1464
1756
|
}
|
|
1757
|
+
if (input3.tool_choice) {
|
|
1758
|
+
if (typeof input3.tool_choice === "string") {
|
|
1759
|
+
body.tool_choice = input3.tool_choice;
|
|
1760
|
+
} else if (input3.tool_choice?.type === "tool" && input3.tool_choice?.name) {
|
|
1761
|
+
body.tool_choice = {
|
|
1762
|
+
type: "tool",
|
|
1763
|
+
name: input3.tool_choice.name
|
|
1764
|
+
};
|
|
1765
|
+
} else if (input3.tool_choice?.type === "function" && input3.tool_choice?.function?.name) {
|
|
1766
|
+
body.tool_choice = {
|
|
1767
|
+
type: "tool",
|
|
1768
|
+
name: input3.tool_choice.function.name
|
|
1769
|
+
};
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1465
1772
|
if (input3.ir.system.length) {
|
|
1466
1773
|
body.system = input3.ir.system.map((text) => ({ type: "text", text }));
|
|
1467
1774
|
}
|
|
@@ -1794,6 +2101,37 @@ Return JSON only:
|
|
|
1794
2101
|
}
|
|
1795
2102
|
return best;
|
|
1796
2103
|
}
|
|
2104
|
+
analyzeCandidates(text, candidates, defaultThreshold = 0.85) {
|
|
2105
|
+
const inputTokens = tokenize(text);
|
|
2106
|
+
if (inputTokens.length === 0 || !candidates.length) {
|
|
2107
|
+
return null;
|
|
2108
|
+
}
|
|
2109
|
+
const inputVector = buildVector(inputTokens);
|
|
2110
|
+
let best = null;
|
|
2111
|
+
let bestThreshold = defaultThreshold;
|
|
2112
|
+
for (const candidate of candidates) {
|
|
2113
|
+
const prototypeTokens = tokenize(candidate.prototype);
|
|
2114
|
+
if (prototypeTokens.length === 0) {
|
|
2115
|
+
continue;
|
|
2116
|
+
}
|
|
2117
|
+
const prototypeVector = buildVector(prototypeTokens);
|
|
2118
|
+
const matched = prototypeTokens.filter((token) => inputTokens.includes(token));
|
|
2119
|
+
const confidence = cosineSimilarity(inputVector, prototypeVector);
|
|
2120
|
+
if (!best || confidence > best.confidence) {
|
|
2121
|
+
best = {
|
|
2122
|
+
intent: candidate.intent,
|
|
2123
|
+
confidence,
|
|
2124
|
+
matchedPrototype: candidate.prototype,
|
|
2125
|
+
evidence: matched
|
|
2126
|
+
};
|
|
2127
|
+
bestThreshold = candidate.threshold ?? defaultThreshold;
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
if (!best || best.confidence < bestThreshold) {
|
|
2131
|
+
return null;
|
|
2132
|
+
}
|
|
2133
|
+
return best;
|
|
2134
|
+
}
|
|
1797
2135
|
analyze(text, config) {
|
|
1798
2136
|
return this.analyzeEmbedding(text, config);
|
|
1799
2137
|
}
|
|
@@ -1994,6 +2332,10 @@ async function applyResponseGovernance({
|
|
|
1994
2332
|
deps
|
|
1995
2333
|
}) {
|
|
1996
2334
|
let nextPayload = payload;
|
|
2335
|
+
const effectiveStickyConfig = config.SmartRouter?.sticky ? {
|
|
2336
|
+
...config.Governance?.sticky ?? {},
|
|
2337
|
+
...config.SmartRouter.sticky
|
|
2338
|
+
} : config.Governance?.sticky;
|
|
1997
2339
|
const resolvedCascadeConfig = config.Governance?.cascade ? {
|
|
1998
2340
|
...config.Governance.cascade,
|
|
1999
2341
|
levels: config.Governance.cascade.levels?.map((level) => ({
|
|
@@ -2041,7 +2383,7 @@ async function applyResponseGovernance({
|
|
|
2041
2383
|
}
|
|
2042
2384
|
}
|
|
2043
2385
|
}
|
|
2044
|
-
if (
|
|
2386
|
+
if (effectiveStickyConfig?.enabled && req.sessionId && req.body?.model) {
|
|
2045
2387
|
const fingerprint = createTaskFingerprint(req.triggerResult?.analyzedText);
|
|
2046
2388
|
if (fingerprint) {
|
|
2047
2389
|
sessionStateStore.put(req.sessionId, {
|
|
@@ -2914,6 +3256,8 @@ function toInlineScriptJson(value) {
|
|
|
2914
3256
|
}
|
|
2915
3257
|
function collectModelReferences(config) {
|
|
2916
3258
|
const refs = [];
|
|
3259
|
+
const normalizedConfig = normalizeAndValidateConfig(config ?? {}).config;
|
|
3260
|
+
const runtimeSmartRouterConfig = deriveRuntimeSmartRouterConfig(normalizedConfig);
|
|
2917
3261
|
const pushRef = (path, value) => {
|
|
2918
3262
|
if (typeof value !== "string" || !value.trim()) {
|
|
2919
3263
|
return;
|
|
@@ -2924,22 +3268,21 @@ function collectModelReferences(config) {
|
|
|
2924
3268
|
referenceType: value.includes(",") ? "legacy" : "modelId"
|
|
2925
3269
|
});
|
|
2926
3270
|
};
|
|
2927
|
-
pushRef("Router.default",
|
|
2928
|
-
pushRef("
|
|
2929
|
-
|
|
2930
|
-
pushRef(`
|
|
3271
|
+
pushRef("Router.default", normalizedConfig?.Router?.default);
|
|
3272
|
+
pushRef("SmartRouter.router_model", runtimeSmartRouterConfig?.router_model);
|
|
3273
|
+
runtimeSmartRouterConfig?.rules?.forEach((rule, index) => {
|
|
3274
|
+
pushRef(`SmartRouter.rules[${index}].model`, rule?.model);
|
|
2931
3275
|
});
|
|
2932
|
-
|
|
2933
|
-
config?.SmartRouter?.candidates?.forEach((candidate, index) => {
|
|
3276
|
+
runtimeSmartRouterConfig?.candidates?.forEach((candidate, index) => {
|
|
2934
3277
|
pushRef(`SmartRouter.candidates[${index}].model`, candidate?.model);
|
|
2935
3278
|
});
|
|
2936
|
-
pushRef("
|
|
2937
|
-
|
|
3279
|
+
pushRef("SmartRouter.sticky.alignment.summarizer_model", runtimeSmartRouterConfig?.sticky?.alignment?.summarizer_model);
|
|
3280
|
+
pushRef("SmartRouter.semantic.classifier_model", runtimeSmartRouterConfig?.semantic?.classifier_model);
|
|
3281
|
+
normalizedConfig?.Governance?.cascade?.levels?.forEach((level, index) => {
|
|
2938
3282
|
pushRef(`Governance.cascade.levels[${index}].from`, level?.from);
|
|
2939
3283
|
pushRef(`Governance.cascade.levels[${index}].to`, level?.to);
|
|
2940
3284
|
});
|
|
2941
|
-
pushRef("Governance.
|
|
2942
|
-
pushRef("Governance.shadow.verifier_model", config?.Governance?.shadow?.verifier_model);
|
|
3285
|
+
pushRef("Governance.shadow.verifier_model", normalizedConfig?.Governance?.shadow?.verifier_model);
|
|
2943
3286
|
return refs;
|
|
2944
3287
|
}
|
|
2945
3288
|
function scoreModelIdSuggestion(source, candidateId, candidate) {
|
|
@@ -2996,6 +3339,133 @@ function analyzeModelReferenceImpact(config, nextCompiled) {
|
|
|
2996
3339
|
}
|
|
2997
3340
|
};
|
|
2998
3341
|
}
|
|
3342
|
+
function projectConfiguredBranch(raw, normalized) {
|
|
3343
|
+
if (raw === void 0) {
|
|
3344
|
+
return void 0;
|
|
3345
|
+
}
|
|
3346
|
+
if (raw === null || normalized === null) {
|
|
3347
|
+
return normalized;
|
|
3348
|
+
}
|
|
3349
|
+
if (Array.isArray(raw)) {
|
|
3350
|
+
return normalized;
|
|
3351
|
+
}
|
|
3352
|
+
if (typeof raw !== "object" || typeof normalized !== "object") {
|
|
3353
|
+
return normalized;
|
|
3354
|
+
}
|
|
3355
|
+
const result = {};
|
|
3356
|
+
Object.keys(raw).forEach((key) => {
|
|
3357
|
+
if (normalized[key] === void 0) {
|
|
3358
|
+
return;
|
|
3359
|
+
}
|
|
3360
|
+
result[key] = projectConfiguredBranch(raw[key], normalized[key]);
|
|
3361
|
+
});
|
|
3362
|
+
return result;
|
|
3363
|
+
}
|
|
3364
|
+
function mergeSmartRouterProjection(target, patch) {
|
|
3365
|
+
if (!patch || !Object.keys(patch).length) {
|
|
3366
|
+
return target;
|
|
3367
|
+
}
|
|
3368
|
+
return {
|
|
3369
|
+
...target,
|
|
3370
|
+
...patch,
|
|
3371
|
+
semantic: patch.semantic ? {
|
|
3372
|
+
...target.semantic || {},
|
|
3373
|
+
...patch.semantic
|
|
3374
|
+
} : target.semantic,
|
|
3375
|
+
sticky: patch.sticky ? {
|
|
3376
|
+
...target.sticky || {},
|
|
3377
|
+
...patch.sticky,
|
|
3378
|
+
alignment: patch.sticky?.alignment ? {
|
|
3379
|
+
...target.sticky?.alignment || {},
|
|
3380
|
+
...patch.sticky.alignment
|
|
3381
|
+
} : target.sticky?.alignment
|
|
3382
|
+
} : target.sticky
|
|
3383
|
+
};
|
|
3384
|
+
}
|
|
3385
|
+
function buildPersistedConfig(rawConfig, normalizedConfig) {
|
|
3386
|
+
const persisted = {
|
|
3387
|
+
HOST: normalizedConfig.HOST,
|
|
3388
|
+
PORT: normalizedConfig.PORT,
|
|
3389
|
+
LOG: normalizedConfig.LOG,
|
|
3390
|
+
LOG_LEVEL: normalizedConfig.LOG_LEVEL,
|
|
3391
|
+
API_TIMEOUT_MS: normalizedConfig.API_TIMEOUT_MS,
|
|
3392
|
+
NON_INTERACTIVE_MODE: normalizedConfig.NON_INTERACTIVE_MODE,
|
|
3393
|
+
APIKEY: normalizedConfig.APIKEY,
|
|
3394
|
+
PROXY_URL: normalizedConfig.PROXY_URL,
|
|
3395
|
+
CUSTOM_ROUTER_PATH: normalizedConfig.CUSTOM_ROUTER_PATH,
|
|
3396
|
+
Providers: normalizedConfig.Providers,
|
|
3397
|
+
Models: normalizedConfig.Models,
|
|
3398
|
+
Router: normalizedConfig.Router
|
|
3399
|
+
};
|
|
3400
|
+
const runtimeSmartRouter = deriveRuntimeSmartRouterConfig(normalizedConfig, rawConfig);
|
|
3401
|
+
let smartRouterProjection = projectConfiguredBranch(rawConfig?.SmartRouter, runtimeSmartRouter) ?? {};
|
|
3402
|
+
if (rawConfig?.TriggerRouter) {
|
|
3403
|
+
smartRouterProjection = mergeSmartRouterProjection(smartRouterProjection, {
|
|
3404
|
+
...rawConfig.TriggerRouter.enabled !== void 0 ? { enabled: runtimeSmartRouter.enabled } : {},
|
|
3405
|
+
...rawConfig.TriggerRouter.analysis_scope !== void 0 ? { analysis_scope: runtimeSmartRouter.analysis_scope } : {},
|
|
3406
|
+
...rawConfig.TriggerRouter.rules !== void 0 ? { rules: runtimeSmartRouter.rules } : {},
|
|
3407
|
+
...rawConfig.TriggerRouter.llm_intent_recognition !== void 0 || rawConfig.TriggerRouter.intent_model !== void 0 ? {
|
|
3408
|
+
semantic: {
|
|
3409
|
+
enabled: runtimeSmartRouter.semantic?.enabled,
|
|
3410
|
+
mode: runtimeSmartRouter.semantic?.mode,
|
|
3411
|
+
classifier_model: runtimeSmartRouter.semantic?.classifier_model
|
|
3412
|
+
}
|
|
3413
|
+
} : {}
|
|
3414
|
+
});
|
|
3415
|
+
}
|
|
3416
|
+
if (rawConfig?.Governance?.sticky) {
|
|
3417
|
+
smartRouterProjection = mergeSmartRouterProjection(
|
|
3418
|
+
smartRouterProjection,
|
|
3419
|
+
{ sticky: projectConfiguredBranch(rawConfig.Governance.sticky, runtimeSmartRouter.sticky) }
|
|
3420
|
+
);
|
|
3421
|
+
}
|
|
3422
|
+
if (rawConfig?.Governance?.semantic) {
|
|
3423
|
+
smartRouterProjection = mergeSmartRouterProjection(
|
|
3424
|
+
smartRouterProjection,
|
|
3425
|
+
{ semantic: projectConfiguredBranch(rawConfig.Governance.semantic, runtimeSmartRouter.semantic) }
|
|
3426
|
+
);
|
|
3427
|
+
}
|
|
3428
|
+
if (Object.keys(smartRouterProjection).length > 0) {
|
|
3429
|
+
persisted.SmartRouter = smartRouterProjection;
|
|
3430
|
+
}
|
|
3431
|
+
const governanceProjection = projectConfiguredBranch(rawConfig?.Governance, normalizedConfig?.Governance);
|
|
3432
|
+
if (governanceProjection && typeof governanceProjection === "object") {
|
|
3433
|
+
delete governanceProjection.sticky;
|
|
3434
|
+
delete governanceProjection.semantic;
|
|
3435
|
+
if (Object.keys(governanceProjection).length > 0) {
|
|
3436
|
+
persisted.Governance = governanceProjection;
|
|
3437
|
+
}
|
|
3438
|
+
}
|
|
3439
|
+
return persisted;
|
|
3440
|
+
}
|
|
3441
|
+
function buildDraftConfigView(config) {
|
|
3442
|
+
const normalizedConfig = normalizeAndValidateConfig(config ?? {}).config;
|
|
3443
|
+
const runtimeSmartRouterConfig = deriveRuntimeSmartRouterConfig(normalizedConfig);
|
|
3444
|
+
const draftConfig = {
|
|
3445
|
+
...normalizedConfig,
|
|
3446
|
+
SmartRouter: runtimeSmartRouterConfig
|
|
3447
|
+
};
|
|
3448
|
+
delete draftConfig.TriggerRouter;
|
|
3449
|
+
if (draftConfig.Governance) {
|
|
3450
|
+
const projectedGovernance = {
|
|
3451
|
+
...draftConfig.Governance
|
|
3452
|
+
};
|
|
3453
|
+
delete projectedGovernance.sticky;
|
|
3454
|
+
delete projectedGovernance.semantic;
|
|
3455
|
+
const hasResidualGovernance = Boolean(
|
|
3456
|
+
projectedGovernance.shadow || projectedGovernance.cascade || projectedGovernance.observability
|
|
3457
|
+
);
|
|
3458
|
+
if (!hasResidualGovernance) {
|
|
3459
|
+
delete draftConfig.Governance;
|
|
3460
|
+
} else {
|
|
3461
|
+
projectedGovernance.enabled = Boolean(
|
|
3462
|
+
projectedGovernance.enabled && hasResidualGovernance
|
|
3463
|
+
);
|
|
3464
|
+
draftConfig.Governance = projectedGovernance;
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
return draftConfig;
|
|
3468
|
+
}
|
|
2999
3469
|
function diffCompiledRegistry(base, next) {
|
|
3000
3470
|
const providerNames = Array.from(/* @__PURE__ */ new Set([
|
|
3001
3471
|
...base.providers.map((item) => item.name),
|
|
@@ -3037,6 +3507,8 @@ function diffCompiledRegistry(base, next) {
|
|
|
3037
3507
|
before?.providerName !== after?.providerName ? "providerName" : null,
|
|
3038
3508
|
before?.modelName !== after?.modelName ? "modelName" : null,
|
|
3039
3509
|
before?.protocol !== after?.protocol ? "protocol" : null,
|
|
3510
|
+
before?.compatibilityProfile !== after?.compatibilityProfile ? "compatibilityProfile" : null,
|
|
3511
|
+
before?.dispatchFormat !== after?.dispatchFormat ? "dispatchFormat" : null,
|
|
3040
3512
|
JSON.stringify(before?.thinking ?? {}) !== JSON.stringify(after?.thinking ?? {}) ? "thinking" : null,
|
|
3041
3513
|
JSON.stringify(before?.capabilities ?? {}) !== JSON.stringify(after?.capabilities ?? {}) ? "capabilities" : null,
|
|
3042
3514
|
before?.source !== after?.source ? "source" : null
|
|
@@ -3101,7 +3573,7 @@ var init_server = __esm({
|
|
|
3101
3573
|
};
|
|
3102
3574
|
};
|
|
3103
3575
|
server.app.get("/api/config", async (req, reply) => {
|
|
3104
|
-
return await readConfigFile();
|
|
3576
|
+
return buildDraftConfigView(await readConfigFile());
|
|
3105
3577
|
});
|
|
3106
3578
|
server.app.get("/api/models/compiled", async () => {
|
|
3107
3579
|
const normalizedResult = normalizeAndValidateConfig(config.initialConfig ?? {});
|
|
@@ -3139,7 +3611,7 @@ var init_server = __esm({
|
|
|
3139
3611
|
success: true,
|
|
3140
3612
|
providers: previewCompiled.providers,
|
|
3141
3613
|
modelMap: previewCompiled.modelMap,
|
|
3142
|
-
normalizedConfig: result.config,
|
|
3614
|
+
normalizedConfig: buildDraftConfigView(result.config),
|
|
3143
3615
|
diff: diffCompiledRegistry(currentCompiled, previewCompiled),
|
|
3144
3616
|
referenceImpact: analyzeModelReferenceImpact(result.config, previewCompiled),
|
|
3145
3617
|
capabilityWarnings: collectCapabilityWarnings(result.config),
|
|
@@ -3337,7 +3809,7 @@ var init_server = __esm({
|
|
|
3337
3809
|
if (backupPath) {
|
|
3338
3810
|
log(`Backed up existing configuration file to ${backupPath}`);
|
|
3339
3811
|
}
|
|
3340
|
-
await writeConfigFile(result.config);
|
|
3812
|
+
await writeConfigFile(buildPersistedConfig(req.body ?? {}, result.config));
|
|
3341
3813
|
return { success: true, message: "Config saved successfully", warnings: result.warnings };
|
|
3342
3814
|
});
|
|
3343
3815
|
server.app.post("/api/restart", async (req, reply) => {
|
|
@@ -3361,7 +3833,7 @@ var init_server = __esm({
|
|
|
3361
3833
|
server.app.get("/ui", async (_, reply) => {
|
|
3362
3834
|
reply.header("Content-Type", "text/html; charset=utf-8");
|
|
3363
3835
|
return reply.send(
|
|
3364
|
-
`<!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>`
|
|
3836
|
+
`<!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">Routing 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">\u56F4\u7ED5 SmartRouter \u7EDF\u4E00\u8DEF\u7531\u5F15\u64CE\u7F16\u8F91\u89C4\u5219\u3001\u5019\u9009\u4E0E\u6CBB\u7406\u589E\u5F3A\u517C\u5BB9\u914D\u7F6E</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Routing rules</strong><span class="muted">\u663E\u5F0F\u89C4\u5219\u3001\u8BED\u4E49\u63D0\u793A\u4E0E\u517C\u5BB9\u8F93\u5165</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 routing 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">\u5F71\u5B50\u6821\u9A8C\u3001\u7EA7\u8054\u4E0E\u89C2\u6D4B\u76F8\u5173\u914D\u7F6E</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 / SmartRouter / Governance\uFF08shadow/cascade\uFF09\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\u8DEF\u7531\u89C4\u5219\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.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\u589E\u5F3A\u4E0E\u6821\u9A8C\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','SmartRouter.sticky.alignment','SmartRouter.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 getDraftSmartRouterConfig(config){ const smart={ ...((config && config.SmartRouter) || {}) }; const smartExplicit=config && Object.prototype.hasOwnProperty.call(config,'SmartRouter'); const legacyIntentEnabled=Boolean(config?.TriggerRouter?.llm_intent_recognition); const legacyIntentModel=config?.TriggerRouter?.intent_model || ''; if(!smart.analysis_scope && config?.TriggerRouter?.analysis_scope){ smart.analysis_scope=config.TriggerRouter.analysis_scope; } if((!Array.isArray(smart.rules) || !smart.rules.length) && Array.isArray(config?.TriggerRouter?.rules)){ smart.rules=config.TriggerRouter.rules; } if(!smart.semantic && (config?.Governance?.semantic || config?.TriggerRouter?.llm_intent_recognition)){ smart.semantic={ ...((config && config.Governance && config.Governance.semantic) || {}) }; if(config?.TriggerRouter?.llm_intent_recognition){ smart.semantic.enabled=true; smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || config.TriggerRouter.intent_model || ''; } } if(!smart.sticky && config?.Governance?.sticky){ smart.sticky={ ...(config.Governance.sticky || {}) }; } if(!smartExplicit && !smart.enabled && (config?.TriggerRouter?.enabled || smart.rules?.length || smart.router_model || smart.candidates?.length || smart.semantic || smart.sticky)){ smart.enabled=true; } if(smart.enabled){ smart.analysis_scope=smart.analysis_scope || 'last_message'; smart.semantic={ ...(smart.semantic || {}) }; smart.semantic.enabled=smart.semantic.enabled !== undefined ? smart.semantic.enabled : true; smart.semantic.threshold=smart.semantic.threshold !== undefined ? smart.semantic.threshold : 0.2; if(legacyIntentEnabled){ smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || legacyIntentModel; } smart.sticky={ ...(smart.sticky || {}) }; smart.sticky.enabled=smart.sticky.enabled !== undefined ? smart.sticky.enabled : true; smart.sticky.alignment={ ...((smart.sticky && smart.sticky.alignment) || {}) }; smart.sticky.alignment.enabled=smart.sticky.alignment.enabled !== undefined ? smart.sticky.alignment.enabled : true; smart.sticky.alignment.summarizer_model=smart.sticky.alignment.summarizer_model || smart.router_model || config?.Router?.default || legacyIntentModel || ''; } return smart;}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const smart=getDraftSmartRouterConfig(config); const triggerRules=Array.isArray(smart?.rules) ? smart.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(smart?.candidates) ? smart.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, smart?.router_model, smart?.sticky?.alignment?.summarizer_model, smart?.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], ['Routing 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') ? 'SmartRouter' : text.startsWith('SmartRouter') ? 'SmartRouter' : (text.startsWith('Governance.sticky') || text.startsWith('Governance.semantic')) ? '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 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 SmartRouter / Governance \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('SmartRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic')){ 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 routing 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(); const smartCandidates=extractSmartCandidatesFromForm(); const smartRouterEnabled=Boolean(smartEnabled.checked || triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim() || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim()); if(smartRouterEnabled){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: true, analysis_scope: triggerAnalysisScope.value || payload.SmartRouter?.analysis_scope || 'last_message', 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, rules: triggerRules, semantic:(governanceSemanticEnabled.checked || triggerIntentEnabled.checked || governanceClassifierModel.value.trim() || triggerIntentModel.value.trim()) ? { ...(((payload.SmartRouter || {}).semantic) || {}), enabled:Boolean(governanceSemanticEnabled.checked || triggerIntentEnabled.checked), mode:'classifier', classifier_model: governanceClassifierModel.value.trim() || triggerIntentModel.value.trim() } : undefined, sticky:(governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim()) ? { ...(((payload.SmartRouter || {}).sticky) || {}), enabled:true, alignment:{ ...((((payload.SmartRouter || {}).sticky || {}).alignment) || {}), enabled:Boolean(governanceAlignmentEnabled.checked), summarizer_model: governanceSummarizerModel.value.trim() } } : undefined }; } else { delete payload.SmartRouter; } delete payload.TriggerRouter; const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, 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 smart=getDraftSmartRouterConfig(config); const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(smart.enabled); triggerIntentEnabled.checked=Boolean(smart.semantic?.enabled && smart.semantic?.mode === 'classifier'); triggerAnalysisScope.value=smart.analysis_scope || 'last_message'; triggerIntentModel.value=smart.semantic?.classifier_model || trigger.intent_model || ''; renderTriggerRulesList(smart.rules || trigger.rules || []); 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(smart.sticky?.alignment?.enabled); governanceSummarizerModel.value=smart.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(smart.semantic?.enabled); governanceClassifierModel.value=smart.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.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.SmartRouter?.enabled), analysis_scope: payload.SmartRouter?.analysis_scope || 'last_message', router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: payload.SmartRouter?.candidates || [], cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.SmartRouter?.rules || []) }; delete payload.TriggerRouter; } 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, rules: payload.SmartRouter?.rules || [] }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: payload.SmartRouter?.enabled !== undefined ? Boolean(payload.SmartRouter?.enabled) : Boolean(preset.governanceEnabled), sticky:{ ...((payload.SmartRouter && payload.SmartRouter.sticky) || {}), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.enabled), alignment:{ ...(((payload.SmartRouter && payload.SmartRouter.sticky && payload.SmartRouter.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.SmartRouter?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.SmartRouter && payload.SmartRouter.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.SmartRouter?.semantic?.enabled), mode:(payload.SmartRouter?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.SmartRouter?.semantic?.classifier_model || '') } }; payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), 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>`
|
|
3365
3837
|
);
|
|
3366
3838
|
});
|
|
3367
3839
|
return server;
|
|
@@ -3586,6 +4058,16 @@ function isProcessAlive(pid) {
|
|
|
3586
4058
|
return false;
|
|
3587
4059
|
}
|
|
3588
4060
|
}
|
|
4061
|
+
async function waitForProcessExit(pid, timeoutMs = 5e3) {
|
|
4062
|
+
const startedAt = Date.now();
|
|
4063
|
+
while (Date.now() - startedAt < timeoutMs) {
|
|
4064
|
+
if (!isProcessAlive(pid)) {
|
|
4065
|
+
return true;
|
|
4066
|
+
}
|
|
4067
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
4068
|
+
}
|
|
4069
|
+
return !isProcessAlive(pid);
|
|
4070
|
+
}
|
|
3589
4071
|
function killProcess(pid) {
|
|
3590
4072
|
if (process.platform === "win32") {
|
|
3591
4073
|
(0, import_child_process.spawnSync)("taskkill", ["/F", "/PID", String(pid)], { timeout: 5e3 });
|
|
@@ -4366,7 +4848,7 @@ Important: Respond ONLY with the JSON, no additional text.`;
|
|
|
4366
4848
|
});
|
|
4367
4849
|
|
|
4368
4850
|
// src/trigger/smart-router.ts
|
|
4369
|
-
var import_lru_cache7,
|
|
4851
|
+
var import_lru_cache7, SmartRouterSelector, smartRouterSelector;
|
|
4370
4852
|
var init_smart_router = __esm({
|
|
4371
4853
|
"src/trigger/smart-router.ts"() {
|
|
4372
4854
|
"use strict";
|
|
@@ -4375,26 +4857,6 @@ var init_smart_router = __esm({
|
|
|
4375
4857
|
init_log();
|
|
4376
4858
|
init_message_ir();
|
|
4377
4859
|
init_anthropic();
|
|
4378
|
-
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.
|
|
4379
|
-
|
|
4380
|
-
User request:
|
|
4381
|
-
"""
|
|
4382
|
-
{request}
|
|
4383
|
-
"""
|
|
4384
|
-
|
|
4385
|
-
Available models:
|
|
4386
|
-
{candidates}
|
|
4387
|
-
|
|
4388
|
-
Select the most appropriate model and respond in the following JSON format ONLY:
|
|
4389
|
-
{
|
|
4390
|
-
"model": "<exact model identifier from the list>",
|
|
4391
|
-
"confidence": <0.0-1.0>,
|
|
4392
|
-
"reasoning": "<brief explanation>"
|
|
4393
|
-
}
|
|
4394
|
-
|
|
4395
|
-
Important:
|
|
4396
|
-
- The "model" field MUST be one of the exact identifiers listed above
|
|
4397
|
-
- Respond ONLY with the JSON, no additional text`;
|
|
4398
4860
|
SmartRouterSelector = class {
|
|
4399
4861
|
cache;
|
|
4400
4862
|
constructor() {
|
|
@@ -4427,8 +4889,42 @@ Important:
|
|
|
4427
4889
|
/**
|
|
4428
4890
|
* 构建完整 prompt
|
|
4429
4891
|
*/
|
|
4430
|
-
buildPrompt(text, candidates) {
|
|
4431
|
-
|
|
4892
|
+
buildPrompt(text, candidates, hint) {
|
|
4893
|
+
const sections = [
|
|
4894
|
+
"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."
|
|
4895
|
+
];
|
|
4896
|
+
if (hint?.taskSummary) {
|
|
4897
|
+
sections.push(`Task summary:
|
|
4898
|
+
"""
|
|
4899
|
+
${hint.taskSummary}
|
|
4900
|
+
"""`);
|
|
4901
|
+
}
|
|
4902
|
+
if (hint?.topRouteCandidates?.length) {
|
|
4903
|
+
sections.push(
|
|
4904
|
+
"Pre-filtered route candidates:\n" + hint.topRouteCandidates.map(
|
|
4905
|
+
(candidate, index) => `${index + 1}. ${candidate.name} -> ${candidate.model}${candidate.description ? ` (${candidate.description})` : ""}${candidate.confidence !== void 0 ? ` [confidence=${candidate.confidence}]` : ""}`
|
|
4906
|
+
).join("\n")
|
|
4907
|
+
);
|
|
4908
|
+
}
|
|
4909
|
+
sections.push(
|
|
4910
|
+
`User request:
|
|
4911
|
+
"""
|
|
4912
|
+
${text}
|
|
4913
|
+
"""`,
|
|
4914
|
+
`Available models:
|
|
4915
|
+
${this.buildCandidatesList(candidates)}`,
|
|
4916
|
+
`Select the most appropriate model and respond in the following JSON format ONLY:
|
|
4917
|
+
{
|
|
4918
|
+
"model": "<exact model identifier from the list>",
|
|
4919
|
+
"confidence": <0.0-1.0>,
|
|
4920
|
+
"reasoning": "<brief explanation>"
|
|
4921
|
+
}
|
|
4922
|
+
|
|
4923
|
+
Important:
|
|
4924
|
+
- The "model" field MUST be one of the exact identifiers listed above
|
|
4925
|
+
- Respond ONLY with the JSON, no additional text`
|
|
4926
|
+
);
|
|
4927
|
+
return sections.join("\n\n");
|
|
4432
4928
|
}
|
|
4433
4929
|
/**
|
|
4434
4930
|
* 使用 LLM 选择最优模型
|
|
@@ -4439,10 +4935,13 @@ Important:
|
|
|
4439
4935
|
* @param fetchFn 可注入的 fetch 函数(用于测试)
|
|
4440
4936
|
* @returns 选择结果,失败时返回 null
|
|
4441
4937
|
*/
|
|
4442
|
-
async selectModel(text, config, port = DEFAULT_CONFIG2.PORT, fetchFn, apiKey, timeoutMs) {
|
|
4938
|
+
async selectModel(text, config, port = DEFAULT_CONFIG2.PORT, fetchFn, apiKey, timeoutMs, hint) {
|
|
4443
4939
|
if (!config.enabled) {
|
|
4444
4940
|
return null;
|
|
4445
4941
|
}
|
|
4942
|
+
if (!config.router_model) {
|
|
4943
|
+
return null;
|
|
4944
|
+
}
|
|
4446
4945
|
if (!config.candidates || config.candidates.length < 2) {
|
|
4447
4946
|
return null;
|
|
4448
4947
|
}
|
|
@@ -4453,7 +4952,7 @@ Important:
|
|
|
4453
4952
|
}
|
|
4454
4953
|
try {
|
|
4455
4954
|
const fetchImpl = fetchFn || fetch;
|
|
4456
|
-
const prompt = this.buildPrompt(text, config.candidates);
|
|
4955
|
+
const prompt = this.buildPrompt(text, config.candidates, hint);
|
|
4457
4956
|
const response = await fetchImpl(`http://127.0.0.1:${port}/v1/messages`, {
|
|
4458
4957
|
method: "POST",
|
|
4459
4958
|
headers: {
|
|
@@ -4520,6 +5019,117 @@ var init_selector = __esm({
|
|
|
4520
5019
|
init_semantic_router();
|
|
4521
5020
|
init_compile();
|
|
4522
5021
|
ModelSelector = class {
|
|
5022
|
+
isRoutingEnabled(config, smartRouterConfig) {
|
|
5023
|
+
if (smartRouterConfig) {
|
|
5024
|
+
return Boolean(smartRouterConfig.enabled);
|
|
5025
|
+
}
|
|
5026
|
+
return Boolean(config.enabled);
|
|
5027
|
+
}
|
|
5028
|
+
getRoutingRules(config, smartRouterConfig) {
|
|
5029
|
+
return smartRouterConfig?.rules?.length ? smartRouterConfig.rules : config.rules;
|
|
5030
|
+
}
|
|
5031
|
+
getEffectiveGovernanceConfig(smartRouterConfig, governanceConfig) {
|
|
5032
|
+
if (!smartRouterConfig?.semantic && !smartRouterConfig?.sticky) {
|
|
5033
|
+
return governanceConfig;
|
|
5034
|
+
}
|
|
5035
|
+
return {
|
|
5036
|
+
...governanceConfig ?? {},
|
|
5037
|
+
enabled: Boolean(
|
|
5038
|
+
governanceConfig?.enabled || smartRouterConfig.semantic?.enabled || smartRouterConfig.sticky?.enabled
|
|
5039
|
+
),
|
|
5040
|
+
sticky: smartRouterConfig.sticky ? {
|
|
5041
|
+
...governanceConfig?.sticky ?? {},
|
|
5042
|
+
...smartRouterConfig.sticky
|
|
5043
|
+
} : governanceConfig?.sticky,
|
|
5044
|
+
semantic: smartRouterConfig.semantic ? {
|
|
5045
|
+
...governanceConfig?.semantic ?? {},
|
|
5046
|
+
...smartRouterConfig.semantic,
|
|
5047
|
+
prototypes: {
|
|
5048
|
+
...governanceConfig?.semantic?.prototypes ?? {},
|
|
5049
|
+
...smartRouterConfig.semantic?.prototypes ?? {}
|
|
5050
|
+
}
|
|
5051
|
+
} : governanceConfig?.semantic,
|
|
5052
|
+
cascade: governanceConfig?.cascade,
|
|
5053
|
+
shadow: governanceConfig?.shadow,
|
|
5054
|
+
observability: governanceConfig?.observability
|
|
5055
|
+
};
|
|
5056
|
+
}
|
|
5057
|
+
resolveRouteModel(appConfig, ref) {
|
|
5058
|
+
if (!ref) {
|
|
5059
|
+
return void 0;
|
|
5060
|
+
}
|
|
5061
|
+
return appConfig ? resolveModelReference(appConfig, ref) ?? ref : ref;
|
|
5062
|
+
}
|
|
5063
|
+
buildSemanticCandidates(rules, governanceConfig) {
|
|
5064
|
+
const defaultThreshold = governanceConfig?.semantic?.threshold;
|
|
5065
|
+
const legacyPrototypes = governanceConfig?.semantic?.prototypes ?? {};
|
|
5066
|
+
return this.sortRulesByPriority(rules).map((rule) => {
|
|
5067
|
+
const prototype = rule.semantic_profile?.prototype ?? legacyPrototypes[rule.name] ?? rule.description;
|
|
5068
|
+
const semanticEnabled = rule.semantic_profile?.enabled !== false && Boolean(prototype);
|
|
5069
|
+
if (!semanticEnabled || !prototype) {
|
|
5070
|
+
return null;
|
|
5071
|
+
}
|
|
5072
|
+
return {
|
|
5073
|
+
rule,
|
|
5074
|
+
prototype,
|
|
5075
|
+
threshold: rule.semantic_profile?.threshold ?? defaultThreshold
|
|
5076
|
+
};
|
|
5077
|
+
}).filter(Boolean);
|
|
5078
|
+
}
|
|
5079
|
+
getStickyCorrection(text, req, governanceConfig) {
|
|
5080
|
+
if (!governanceConfig?.enabled || !governanceConfig.sticky?.enabled || !req.sessionId) {
|
|
5081
|
+
return {};
|
|
5082
|
+
}
|
|
5083
|
+
const fingerprint = createTaskFingerprint(text);
|
|
5084
|
+
const sessionState = sessionStateStore.get(req.sessionId);
|
|
5085
|
+
if (!fingerprint || sessionState?.lastTaskFingerprint !== fingerprint || !(sessionState.preferredModel || sessionState.lastSuccessfulModel)) {
|
|
5086
|
+
return {};
|
|
5087
|
+
}
|
|
5088
|
+
return {
|
|
5089
|
+
fingerprint,
|
|
5090
|
+
sessionModel: sessionState.preferredModel || sessionState.lastSuccessfulModel
|
|
5091
|
+
};
|
|
5092
|
+
}
|
|
5093
|
+
applyStickyCorrection(candidate, sticky, appConfig) {
|
|
5094
|
+
if (!sticky.sessionModel) {
|
|
5095
|
+
return candidate;
|
|
5096
|
+
}
|
|
5097
|
+
const stickyModel = this.resolveRouteModel(appConfig, sticky.sessionModel);
|
|
5098
|
+
if (!stickyModel) {
|
|
5099
|
+
return candidate;
|
|
5100
|
+
}
|
|
5101
|
+
if (!candidate) {
|
|
5102
|
+
log(`[StickyRouting] Reusing model "${stickyModel}" as unified router correction`);
|
|
5103
|
+
return {
|
|
5104
|
+
matched: true,
|
|
5105
|
+
model: stickyModel,
|
|
5106
|
+
confidence: 0.95,
|
|
5107
|
+
analysisTime: 0,
|
|
5108
|
+
routeSource: "sticky_correction"
|
|
5109
|
+
};
|
|
5110
|
+
}
|
|
5111
|
+
if (candidate.model === stickyModel) {
|
|
5112
|
+
return candidate;
|
|
5113
|
+
}
|
|
5114
|
+
log(`[StickyRouting] Correcting selected model "${candidate.model}" -> "${stickyModel}"`);
|
|
5115
|
+
return {
|
|
5116
|
+
...candidate,
|
|
5117
|
+
model: stickyModel,
|
|
5118
|
+
confidence: Math.max(candidate.confidence, 0.95),
|
|
5119
|
+
routeSource: "sticky_correction"
|
|
5120
|
+
};
|
|
5121
|
+
}
|
|
5122
|
+
buildSmartRouterHint(text, rules) {
|
|
5123
|
+
return {
|
|
5124
|
+
taskSummary: text.slice(0, 240),
|
|
5125
|
+
topRouteCandidates: this.sortRulesByPriority(rules).filter((rule) => rule.description).slice(0, 3).map((rule) => ({
|
|
5126
|
+
name: rule.name,
|
|
5127
|
+
model: rule.model,
|
|
5128
|
+
description: rule.description,
|
|
5129
|
+
confidence: void 0
|
|
5130
|
+
}))
|
|
5131
|
+
};
|
|
5132
|
+
}
|
|
4523
5133
|
/**
|
|
4524
5134
|
* 按优先级排序规则
|
|
4525
5135
|
* 优先级数值越大,优先级越高
|
|
@@ -4575,14 +5185,20 @@ var init_selector = __esm({
|
|
|
4575
5185
|
async selectModel(req, config, port = DEFAULT_CONFIG2.PORT, smartRouterConfig, governanceConfig, apiKey, timeoutMs) {
|
|
4576
5186
|
const startTime = Date.now();
|
|
4577
5187
|
const appConfig = req.appConfig;
|
|
4578
|
-
|
|
5188
|
+
const effectiveGovernanceConfig = this.getEffectiveGovernanceConfig(smartRouterConfig, governanceConfig);
|
|
5189
|
+
const routingRules = this.getRoutingRules(config, smartRouterConfig);
|
|
5190
|
+
const analysisConfig = smartRouterConfig?.analysis_scope ? {
|
|
5191
|
+
...config,
|
|
5192
|
+
analysis_scope: smartRouterConfig.analysis_scope
|
|
5193
|
+
} : config;
|
|
5194
|
+
if (!this.isRoutingEnabled(config, smartRouterConfig)) {
|
|
4579
5195
|
return {
|
|
4580
5196
|
matched: false,
|
|
4581
5197
|
confidence: 0,
|
|
4582
5198
|
analysisTime: Date.now() - startTime
|
|
4583
5199
|
};
|
|
4584
5200
|
}
|
|
4585
|
-
const text = contextAnalyzer.analyze(req,
|
|
5201
|
+
const text = contextAnalyzer.analyze(req, analysisConfig);
|
|
4586
5202
|
if (!text) {
|
|
4587
5203
|
return {
|
|
4588
5204
|
matched: false,
|
|
@@ -4591,7 +5207,7 @@ var init_selector = __esm({
|
|
|
4591
5207
|
analyzedText: ""
|
|
4592
5208
|
};
|
|
4593
5209
|
}
|
|
4594
|
-
const matchResult = this.matchRuleFromText(text,
|
|
5210
|
+
const matchResult = this.matchRuleFromText(text, routingRules);
|
|
4595
5211
|
if (matchResult) {
|
|
4596
5212
|
return {
|
|
4597
5213
|
matched: true,
|
|
@@ -4601,38 +5217,34 @@ var init_selector = __esm({
|
|
|
4601
5217
|
// 关键词匹配置信度为 1
|
|
4602
5218
|
analysisTime: Date.now() - startTime,
|
|
4603
5219
|
analyzedText: text,
|
|
4604
|
-
routeSource: "
|
|
5220
|
+
routeSource: "smart_rule"
|
|
4605
5221
|
};
|
|
4606
5222
|
}
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
matched: true,
|
|
4616
|
-
model: appConfig ? resolveModelReference(appConfig, stickyModel) ?? stickyModel : stickyModel,
|
|
4617
|
-
confidence: 0.95,
|
|
4618
|
-
analysisTime: Date.now() - startTime,
|
|
4619
|
-
analyzedText: text,
|
|
4620
|
-
routeSource: "sticky"
|
|
4621
|
-
};
|
|
4622
|
-
}
|
|
4623
|
-
}
|
|
4624
|
-
}
|
|
4625
|
-
if (governanceConfig?.enabled && governanceConfig.semantic?.enabled) {
|
|
4626
|
-
const semanticResult = governanceConfig.semantic.mode === "classifier" ? await semanticRouter.analyzeWithClassifier(
|
|
5223
|
+
const stickyCorrection = this.getStickyCorrection(text, req, effectiveGovernanceConfig);
|
|
5224
|
+
const semanticCandidates = this.buildSemanticCandidates(routingRules, effectiveGovernanceConfig);
|
|
5225
|
+
if (effectiveGovernanceConfig?.enabled && effectiveGovernanceConfig.semantic?.enabled && semanticCandidates.length > 0) {
|
|
5226
|
+
const semanticConfig = {
|
|
5227
|
+
...effectiveGovernanceConfig.semantic,
|
|
5228
|
+
prototypes: Object.fromEntries(semanticCandidates.map((candidate) => [candidate.rule.name, candidate.prototype]))
|
|
5229
|
+
};
|
|
5230
|
+
const semanticResult = semanticConfig.mode === "classifier" ? await semanticRouter.analyzeWithClassifier(
|
|
4627
5231
|
text,
|
|
4628
|
-
|
|
5232
|
+
semanticConfig,
|
|
4629
5233
|
port,
|
|
4630
5234
|
void 0,
|
|
4631
5235
|
apiKey,
|
|
4632
5236
|
timeoutMs
|
|
4633
|
-
) : semanticRouter.
|
|
5237
|
+
) : semanticRouter.analyzeCandidates(
|
|
5238
|
+
text,
|
|
5239
|
+
semanticCandidates.map((candidate) => ({
|
|
5240
|
+
intent: candidate.rule.name,
|
|
5241
|
+
prototype: candidate.prototype,
|
|
5242
|
+
threshold: candidate.threshold
|
|
5243
|
+
})),
|
|
5244
|
+
semanticConfig.threshold
|
|
5245
|
+
);
|
|
4634
5246
|
if (semanticResult) {
|
|
4635
|
-
const matchedRule =
|
|
5247
|
+
const matchedRule = routingRules.find(
|
|
4636
5248
|
(rule) => rule.enabled !== false && rule.name.toLowerCase() === semanticResult.intent.toLowerCase()
|
|
4637
5249
|
);
|
|
4638
5250
|
if (matchedRule) {
|
|
@@ -4640,32 +5252,41 @@ var init_selector = __esm({
|
|
|
4640
5252
|
if (req.governanceTrace) {
|
|
4641
5253
|
req.governanceTrace.semanticIntent = semanticResult.intent;
|
|
4642
5254
|
}
|
|
4643
|
-
|
|
5255
|
+
const semanticSelection = {
|
|
4644
5256
|
matched: true,
|
|
4645
5257
|
rule: matchedRule,
|
|
4646
|
-
model:
|
|
5258
|
+
model: this.resolveRouteModel(appConfig, matchedRule.model),
|
|
4647
5259
|
confidence: semanticResult.confidence,
|
|
4648
5260
|
analysisTime: Date.now() - startTime,
|
|
4649
5261
|
analyzedText: text,
|
|
4650
|
-
routeSource: "
|
|
5262
|
+
routeSource: "semantic_match"
|
|
4651
5263
|
};
|
|
5264
|
+
return this.applyStickyCorrection(semanticSelection, stickyCorrection, appConfig) ?? semanticSelection;
|
|
4652
5265
|
}
|
|
4653
5266
|
}
|
|
4654
5267
|
}
|
|
4655
|
-
if (smartRouterConfig?.enabled && smartRouterConfig.candidates?.length >= 2) {
|
|
5268
|
+
if (smartRouterConfig?.enabled && smartRouterConfig.router_model && smartRouterConfig.candidates?.length >= 2) {
|
|
4656
5269
|
try {
|
|
4657
5270
|
const resolvedSmartRouterConfig = appConfig ? {
|
|
4658
5271
|
...smartRouterConfig,
|
|
4659
|
-
router_model:
|
|
5272
|
+
router_model: this.resolveRouteModel(appConfig, smartRouterConfig.router_model) ?? smartRouterConfig.router_model,
|
|
4660
5273
|
candidates: smartRouterConfig.candidates.map((candidate) => ({
|
|
4661
5274
|
...candidate,
|
|
4662
|
-
model:
|
|
5275
|
+
model: this.resolveRouteModel(appConfig, candidate.model) ?? candidate.model
|
|
4663
5276
|
}))
|
|
4664
5277
|
} : smartRouterConfig;
|
|
4665
|
-
const smartResult = await smartRouterSelector.selectModel(
|
|
5278
|
+
const smartResult = await smartRouterSelector.selectModel(
|
|
5279
|
+
text,
|
|
5280
|
+
resolvedSmartRouterConfig,
|
|
5281
|
+
port,
|
|
5282
|
+
void 0,
|
|
5283
|
+
apiKey,
|
|
5284
|
+
timeoutMs,
|
|
5285
|
+
this.buildSmartRouterHint(text, routingRules)
|
|
5286
|
+
);
|
|
4666
5287
|
if (smartResult) {
|
|
4667
5288
|
log(`[SmartRouter] Selected model "${smartResult.model}" (confidence: ${smartResult.confidence})`);
|
|
4668
|
-
|
|
5289
|
+
const smartSelection = {
|
|
4669
5290
|
matched: true,
|
|
4670
5291
|
model: smartResult.model,
|
|
4671
5292
|
confidence: smartResult.confidence,
|
|
@@ -4673,32 +5294,42 @@ var init_selector = __esm({
|
|
|
4673
5294
|
analyzedText: text,
|
|
4674
5295
|
routeSource: "smart_router"
|
|
4675
5296
|
};
|
|
5297
|
+
return this.applyStickyCorrection(smartSelection, stickyCorrection, appConfig) ?? smartSelection;
|
|
4676
5298
|
}
|
|
4677
5299
|
} catch (error) {
|
|
4678
5300
|
logError("[ModelSelector] SmartRouter error:", error);
|
|
4679
5301
|
}
|
|
4680
5302
|
}
|
|
4681
|
-
if (config.llm_intent_recognition && config.intent_model) {
|
|
5303
|
+
if (!smartRouterConfig?.enabled && config.llm_intent_recognition && config.intent_model) {
|
|
4682
5304
|
try {
|
|
4683
5305
|
const intentResult = await intentDetector.detectIntent(text, config, port, void 0, apiKey, timeoutMs);
|
|
4684
5306
|
if (intentResult.confidence > 0.5 && intentResult.intent !== "general") {
|
|
4685
|
-
const matchedRule = intentDetector.findRuleByIntent(intentResult.intent,
|
|
5307
|
+
const matchedRule = intentDetector.findRuleByIntent(intentResult.intent, routingRules);
|
|
4686
5308
|
if (matchedRule) {
|
|
4687
|
-
|
|
5309
|
+
const intentSelection = {
|
|
4688
5310
|
matched: true,
|
|
4689
5311
|
rule: matchedRule,
|
|
4690
|
-
model:
|
|
5312
|
+
model: this.resolveRouteModel(appConfig, matchedRule.model),
|
|
4691
5313
|
confidence: intentResult.confidence,
|
|
4692
5314
|
analysisTime: Date.now() - startTime,
|
|
4693
5315
|
analyzedText: text,
|
|
4694
|
-
routeSource: "
|
|
5316
|
+
routeSource: "semantic_match"
|
|
4695
5317
|
};
|
|
5318
|
+
return this.applyStickyCorrection(intentSelection, stickyCorrection, appConfig) ?? intentSelection;
|
|
4696
5319
|
}
|
|
4697
5320
|
}
|
|
4698
5321
|
} catch (error) {
|
|
4699
5322
|
logError("[ModelSelector] Intent detection error:", error);
|
|
4700
5323
|
}
|
|
4701
5324
|
}
|
|
5325
|
+
const stickyOnlySelection = this.applyStickyCorrection(null, stickyCorrection, appConfig);
|
|
5326
|
+
if (stickyOnlySelection) {
|
|
5327
|
+
return {
|
|
5328
|
+
...stickyOnlySelection,
|
|
5329
|
+
analysisTime: Date.now() - startTime,
|
|
5330
|
+
analyzedText: text
|
|
5331
|
+
};
|
|
5332
|
+
}
|
|
4702
5333
|
return {
|
|
4703
5334
|
matched: false,
|
|
4704
5335
|
confidence: 0,
|
|
@@ -4714,17 +5345,22 @@ var init_selector = __esm({
|
|
|
4714
5345
|
* @param config 触发配置
|
|
4715
5346
|
* @returns 分析结果
|
|
4716
5347
|
*/
|
|
4717
|
-
selectModelSync(req, config) {
|
|
5348
|
+
selectModelSync(req, config, smartRouterConfig) {
|
|
4718
5349
|
const startTime = Date.now();
|
|
4719
5350
|
const appConfig = req.appConfig;
|
|
4720
|
-
|
|
5351
|
+
const effectiveGovernanceConfig = this.getEffectiveGovernanceConfig(smartRouterConfig, void 0);
|
|
5352
|
+
const analysisConfig = smartRouterConfig?.analysis_scope ? {
|
|
5353
|
+
...config,
|
|
5354
|
+
analysis_scope: smartRouterConfig.analysis_scope
|
|
5355
|
+
} : config;
|
|
5356
|
+
if (!this.isRoutingEnabled(config, smartRouterConfig)) {
|
|
4721
5357
|
return {
|
|
4722
5358
|
matched: false,
|
|
4723
5359
|
confidence: 0,
|
|
4724
5360
|
analysisTime: Date.now() - startTime
|
|
4725
5361
|
};
|
|
4726
5362
|
}
|
|
4727
|
-
const text = contextAnalyzer.analyze(req,
|
|
5363
|
+
const text = contextAnalyzer.analyze(req, analysisConfig);
|
|
4728
5364
|
if (!text) {
|
|
4729
5365
|
return {
|
|
4730
5366
|
matched: false,
|
|
@@ -4733,7 +5369,9 @@ var init_selector = __esm({
|
|
|
4733
5369
|
analyzedText: ""
|
|
4734
5370
|
};
|
|
4735
5371
|
}
|
|
4736
|
-
const
|
|
5372
|
+
const routingRules = this.getRoutingRules(config, smartRouterConfig);
|
|
5373
|
+
const stickyCorrection = this.getStickyCorrection(text, req, effectiveGovernanceConfig);
|
|
5374
|
+
const matchResult = this.matchRuleFromText(text, routingRules);
|
|
4737
5375
|
if (matchResult) {
|
|
4738
5376
|
return {
|
|
4739
5377
|
matched: true,
|
|
@@ -4741,6 +5379,44 @@ var init_selector = __esm({
|
|
|
4741
5379
|
model: appConfig ? resolveModelReference(appConfig, matchResult.rule.model) ?? matchResult.rule.model : matchResult.rule.model,
|
|
4742
5380
|
confidence: 1,
|
|
4743
5381
|
analysisTime: Date.now() - startTime,
|
|
5382
|
+
analyzedText: text,
|
|
5383
|
+
routeSource: "smart_rule"
|
|
5384
|
+
};
|
|
5385
|
+
}
|
|
5386
|
+
const semanticCandidates = this.buildSemanticCandidates(routingRules, effectiveGovernanceConfig);
|
|
5387
|
+
if (effectiveGovernanceConfig?.enabled && effectiveGovernanceConfig.semantic?.enabled && semanticCandidates.length > 0) {
|
|
5388
|
+
const semanticResult = semanticRouter.analyzeCandidates(
|
|
5389
|
+
text,
|
|
5390
|
+
semanticCandidates.map((candidate) => ({
|
|
5391
|
+
intent: candidate.rule.name,
|
|
5392
|
+
prototype: candidate.prototype,
|
|
5393
|
+
threshold: candidate.threshold
|
|
5394
|
+
})),
|
|
5395
|
+
effectiveGovernanceConfig.semantic.threshold
|
|
5396
|
+
);
|
|
5397
|
+
if (semanticResult) {
|
|
5398
|
+
const matchedRule = routingRules.find(
|
|
5399
|
+
(rule) => rule.enabled !== false && rule.name.toLowerCase() === semanticResult.intent.toLowerCase()
|
|
5400
|
+
);
|
|
5401
|
+
if (matchedRule) {
|
|
5402
|
+
const semanticSelection = {
|
|
5403
|
+
matched: true,
|
|
5404
|
+
rule: matchedRule,
|
|
5405
|
+
model: this.resolveRouteModel(appConfig, matchedRule.model),
|
|
5406
|
+
confidence: semanticResult.confidence,
|
|
5407
|
+
analysisTime: Date.now() - startTime,
|
|
5408
|
+
analyzedText: text,
|
|
5409
|
+
routeSource: "semantic_match"
|
|
5410
|
+
};
|
|
5411
|
+
return this.applyStickyCorrection(semanticSelection, stickyCorrection, appConfig) ?? semanticSelection;
|
|
5412
|
+
}
|
|
5413
|
+
}
|
|
5414
|
+
}
|
|
5415
|
+
const stickyOnlySelection = this.applyStickyCorrection(null, stickyCorrection, appConfig);
|
|
5416
|
+
if (stickyOnlySelection) {
|
|
5417
|
+
return {
|
|
5418
|
+
...stickyOnlySelection,
|
|
5419
|
+
analysisTime: Date.now() - startTime,
|
|
4744
5420
|
analyzedText: text
|
|
4745
5421
|
};
|
|
4746
5422
|
}
|
|
@@ -4772,6 +5448,7 @@ var init_trigger = __esm({
|
|
|
4772
5448
|
init_analyzer();
|
|
4773
5449
|
init_log();
|
|
4774
5450
|
init_constants();
|
|
5451
|
+
init_config();
|
|
4775
5452
|
TriggerRouter = class {
|
|
4776
5453
|
config = null;
|
|
4777
5454
|
appConfig = null;
|
|
@@ -4781,7 +5458,7 @@ var init_trigger = __esm({
|
|
|
4781
5458
|
apiKey;
|
|
4782
5459
|
apiTimeoutMs;
|
|
4783
5460
|
/**
|
|
4784
|
-
*
|
|
5461
|
+
* 初始化 SmartRouter 运行时
|
|
4785
5462
|
*
|
|
4786
5463
|
* @param appConfig 应用配置
|
|
4787
5464
|
*/
|
|
@@ -4789,7 +5466,7 @@ var init_trigger = __esm({
|
|
|
4789
5466
|
this.appConfig = appConfig;
|
|
4790
5467
|
this.config = appConfig.TriggerRouter || this.getDefaultConfig();
|
|
4791
5468
|
this.port = appConfig.PORT || DEFAULT_CONFIG2.PORT;
|
|
4792
|
-
this.smartRouterConfig = appConfig
|
|
5469
|
+
this.smartRouterConfig = deriveRuntimeSmartRouterConfig(appConfig, appConfig);
|
|
4793
5470
|
this.governanceConfig = appConfig.Governance;
|
|
4794
5471
|
this.apiKey = appConfig.APIKEY;
|
|
4795
5472
|
this.apiTimeoutMs = appConfig.API_TIMEOUT_MS;
|
|
@@ -4806,10 +5483,10 @@ var init_trigger = __esm({
|
|
|
4806
5483
|
};
|
|
4807
5484
|
}
|
|
4808
5485
|
/**
|
|
4809
|
-
*
|
|
5486
|
+
* 检查 SmartRouter 运行时是否启用
|
|
4810
5487
|
*/
|
|
4811
5488
|
isEnabled() {
|
|
4812
|
-
return this.
|
|
5489
|
+
return Boolean(this.smartRouterConfig?.enabled);
|
|
4813
5490
|
}
|
|
4814
5491
|
/**
|
|
4815
5492
|
* 获取当前配置
|
|
@@ -4817,15 +5494,18 @@ var init_trigger = __esm({
|
|
|
4817
5494
|
getConfig() {
|
|
4818
5495
|
return this.config;
|
|
4819
5496
|
}
|
|
5497
|
+
getSmartRouterConfig() {
|
|
5498
|
+
return this.smartRouterConfig;
|
|
5499
|
+
}
|
|
4820
5500
|
/**
|
|
4821
|
-
*
|
|
5501
|
+
* 执行 SmartRouter 统一路由
|
|
4822
5502
|
* 分析请求并返回匹配的模型
|
|
4823
5503
|
*
|
|
4824
5504
|
* @param req 请求对象
|
|
4825
5505
|
* @returns 分析结果
|
|
4826
5506
|
*/
|
|
4827
5507
|
async route(req) {
|
|
4828
|
-
if (!this.config || !this.
|
|
5508
|
+
if (!this.config || !this.isEnabled()) {
|
|
4829
5509
|
return {
|
|
4830
5510
|
matched: false,
|
|
4831
5511
|
confidence: 0,
|
|
@@ -4853,30 +5533,29 @@ var init_trigger = __esm({
|
|
|
4853
5533
|
this.apiTimeoutMs
|
|
4854
5534
|
);
|
|
4855
5535
|
if (req.governanceTrace) {
|
|
4856
|
-
if (result.routeSource === "
|
|
4857
|
-
appendTraceReason(req.governanceTrace, `
|
|
4858
|
-
} else if (result.routeSource === "
|
|
5536
|
+
if (result.routeSource === "smart_rule" && result.rule?.name) {
|
|
5537
|
+
appendTraceReason(req.governanceTrace, `smart_rule:${result.rule.name}`);
|
|
5538
|
+
} else if (result.routeSource === "semantic_match" && result.rule?.name) {
|
|
5539
|
+
appendTraceReason(req.governanceTrace, `semantic_match:${result.rule.name}`);
|
|
5540
|
+
} else if (result.routeSource === "sticky_correction") {
|
|
4859
5541
|
req.governanceTrace.stickyHit = true;
|
|
4860
|
-
appendTraceReason(req.governanceTrace, "
|
|
5542
|
+
appendTraceReason(req.governanceTrace, "sticky_correction");
|
|
4861
5543
|
} else if (result.routeSource === "smart_router") {
|
|
4862
5544
|
appendTraceReason(req.governanceTrace, "smart_router");
|
|
4863
|
-
} else if (result.routeSource === "intent") {
|
|
4864
|
-
appendTraceReason(req.governanceTrace, "intent_detection");
|
|
4865
5545
|
} else {
|
|
4866
|
-
appendTraceReason(req.governanceTrace, "
|
|
5546
|
+
appendTraceReason(req.governanceTrace, "smart_router:no_match");
|
|
4867
5547
|
}
|
|
4868
5548
|
}
|
|
4869
5549
|
return result;
|
|
4870
5550
|
}
|
|
4871
5551
|
/**
|
|
4872
|
-
*
|
|
4873
|
-
* 仅使用关键词匹配
|
|
5552
|
+
* 同步版本的 SmartRouter 统一路由
|
|
4874
5553
|
*
|
|
4875
5554
|
* @param req 请求对象
|
|
4876
5555
|
* @returns 分析结果
|
|
4877
5556
|
*/
|
|
4878
5557
|
routeSync(req) {
|
|
4879
|
-
if (!this.config || !this.
|
|
5558
|
+
if (!this.config || !this.isEnabled()) {
|
|
4880
5559
|
return {
|
|
4881
5560
|
matched: false,
|
|
4882
5561
|
confidence: 0,
|
|
@@ -4894,11 +5573,11 @@ var init_trigger = __esm({
|
|
|
4894
5573
|
return modelSelector.selectModelSync({
|
|
4895
5574
|
...req,
|
|
4896
5575
|
appConfig: this.appConfig ?? void 0
|
|
4897
|
-
}, this.config);
|
|
5576
|
+
}, this.config, this.smartRouterConfig);
|
|
4898
5577
|
}
|
|
4899
5578
|
/**
|
|
4900
5579
|
* 创建 Fastify 中间件
|
|
4901
|
-
*
|
|
5580
|
+
* 用于在请求处理前执行 SmartRouter 统一路由
|
|
4902
5581
|
*
|
|
4903
5582
|
* @param appConfig 应用配置
|
|
4904
5583
|
* @returns Fastify 中间件函数
|
|
@@ -4919,11 +5598,11 @@ var init_trigger = __esm({
|
|
|
4919
5598
|
req.body.model = result.model;
|
|
4920
5599
|
req.triggerResult = result;
|
|
4921
5600
|
log(
|
|
4922
|
-
`[
|
|
5601
|
+
`[SmartRouter] ${result.routeSource === "sticky_correction" ? "Sticky correction selected" : result.routeSource === "semantic_match" ? `Semantic match "${result.rule?.name}"` : result.routeSource === "smart_router" ? "Smart fallback selected" : result.rule ? `Matched rule "${result.rule.name}"` : "Unified router selected"} -> model "${result.model}" (confidence: ${result.confidence}, time: ${result.analysisTime}ms)`
|
|
4923
5602
|
);
|
|
4924
5603
|
}
|
|
4925
5604
|
} catch (error) {
|
|
4926
|
-
logError("[
|
|
5605
|
+
logError("[SmartRouter] Error in routing:", error);
|
|
4927
5606
|
}
|
|
4928
5607
|
};
|
|
4929
5608
|
}
|
|
@@ -5158,6 +5837,42 @@ function applyCapabilityFallbacks(input3) {
|
|
|
5158
5837
|
request: nextRequest
|
|
5159
5838
|
};
|
|
5160
5839
|
}
|
|
5840
|
+
function describeProtocolDiagnostic(code) {
|
|
5841
|
+
switch (code) {
|
|
5842
|
+
case "thinking_ignored":
|
|
5843
|
+
return {
|
|
5844
|
+
code,
|
|
5845
|
+
severity: "info",
|
|
5846
|
+
label: "thinking \u5DF2\u5FFD\u7565",
|
|
5847
|
+
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",
|
|
5848
|
+
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"
|
|
5849
|
+
};
|
|
5850
|
+
case "images_text_fallback":
|
|
5851
|
+
return {
|
|
5852
|
+
code,
|
|
5853
|
+
severity: "warn",
|
|
5854
|
+
label: "\u56FE\u7247\u5DF2\u964D\u7EA7\u4E3A\u6587\u672C",
|
|
5855
|
+
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",
|
|
5856
|
+
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"
|
|
5857
|
+
};
|
|
5858
|
+
case "tools_text_fallback":
|
|
5859
|
+
return {
|
|
5860
|
+
code,
|
|
5861
|
+
severity: "warn",
|
|
5862
|
+
label: "\u5DE5\u5177\u8C03\u7528\u5DF2\u964D\u7EA7\u4E3A\u6587\u672C",
|
|
5863
|
+
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",
|
|
5864
|
+
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"
|
|
5865
|
+
};
|
|
5866
|
+
default:
|
|
5867
|
+
return {
|
|
5868
|
+
code,
|
|
5869
|
+
severity: "info",
|
|
5870
|
+
label: code,
|
|
5871
|
+
summary: "\u672A\u77E5\u534F\u8BAE\u8BCA\u65AD\u3002",
|
|
5872
|
+
action: "\u8BF7\u7ED3\u5408\u539F\u59CB\u8BF7\u6C42\u548C\u4E0A\u6E38\u54CD\u5E94\u7EE7\u7EED\u6392\u67E5\u3002"
|
|
5873
|
+
};
|
|
5874
|
+
}
|
|
5875
|
+
}
|
|
5161
5876
|
function omitRequestFields(body) {
|
|
5162
5877
|
const {
|
|
5163
5878
|
model,
|
|
@@ -5167,6 +5882,7 @@ function omitRequestFields(body) {
|
|
|
5167
5882
|
thinking,
|
|
5168
5883
|
metadata,
|
|
5169
5884
|
max_tokens,
|
|
5885
|
+
max_completion_tokens,
|
|
5170
5886
|
...rest
|
|
5171
5887
|
} = body;
|
|
5172
5888
|
return rest;
|
|
@@ -5200,10 +5916,11 @@ function buildProviderDispatchRequestFromIR(input3) {
|
|
|
5200
5916
|
...passthrough,
|
|
5201
5917
|
...toAnthropicMessagesRequest({
|
|
5202
5918
|
model: input3.model,
|
|
5203
|
-
max_tokens: fallback.request.max_tokens,
|
|
5919
|
+
max_tokens: fallback.request.max_tokens ?? fallback.request.max_completion_tokens,
|
|
5204
5920
|
stream: fallback.request.stream,
|
|
5205
5921
|
metadata: fallback.request.metadata,
|
|
5206
5922
|
tools: fallback.request.tools,
|
|
5923
|
+
tool_choice: fallback.request.tool_choice,
|
|
5207
5924
|
ir: fallback.ir
|
|
5208
5925
|
})
|
|
5209
5926
|
};
|
|
@@ -5335,7 +6052,7 @@ async function run(options = {}) {
|
|
|
5335
6052
|
});
|
|
5336
6053
|
});
|
|
5337
6054
|
triggerRouter.init(config);
|
|
5338
|
-
log(`[
|
|
6055
|
+
log(`[SmartRouter] Initialized, enabled: ${triggerRouter.isEnabled()}`);
|
|
5339
6056
|
server.addHook("preHandler", async (req, reply) => {
|
|
5340
6057
|
if (req.url.startsWith("/v1/messages")) {
|
|
5341
6058
|
if (req.body.metadata?.user_id) {
|
|
@@ -5350,14 +6067,14 @@ async function run(options = {}) {
|
|
|
5350
6067
|
initialModel: req.body?.model
|
|
5351
6068
|
});
|
|
5352
6069
|
appendTraceReason(req.governanceTrace, "request_received");
|
|
5353
|
-
const
|
|
5354
|
-
const triggerResult =
|
|
6070
|
+
const bypassSmartRouter = req.headers["x-ctr-smart-router"] === "1";
|
|
6071
|
+
const triggerResult = bypassSmartRouter ? { matched: false, confidence: 0, analysisTime: 0 } : await triggerRouter.route(req);
|
|
5355
6072
|
req.triggerResult = triggerResult;
|
|
5356
|
-
if (!
|
|
6073
|
+
if (!bypassSmartRouter && triggerResult.matched && triggerResult.model) {
|
|
5357
6074
|
const previousSessionState = req.sessionId ? sessionStateStore.get(req.sessionId) : void 0;
|
|
5358
6075
|
const previousModel = previousSessionState?.lastSuccessfulModel;
|
|
5359
|
-
const alignmentConfig = config.Governance?.sticky?.alignment;
|
|
5360
|
-
if (
|
|
6076
|
+
const alignmentConfig = triggerRouter.getSmartRouterConfig()?.sticky?.alignment ?? config.Governance?.sticky?.alignment;
|
|
6077
|
+
if (triggerRouter.getSmartRouterConfig()?.enabled && alignmentConfig?.enabled && previousModel && previousModel !== triggerResult.model && triggerResult.analyzedText) {
|
|
5361
6078
|
const resolvedAlignmentConfig = {
|
|
5362
6079
|
...alignmentConfig,
|
|
5363
6080
|
summarizer_model: resolveModelReference(config, alignmentConfig.summarizer_model) ?? alignmentConfig.summarizer_model
|
|
@@ -5386,7 +6103,7 @@ async function run(options = {}) {
|
|
|
5386
6103
|
req.body.model = triggerResult.model;
|
|
5387
6104
|
req.governanceTrace.finalModel = triggerResult.model;
|
|
5388
6105
|
log(
|
|
5389
|
-
`[
|
|
6106
|
+
`[SmartRouter] Selected "${triggerResult.rule?.name ?? triggerResult.routeSource ?? "route"}" -> "${triggerResult.model}"`
|
|
5390
6107
|
);
|
|
5391
6108
|
}
|
|
5392
6109
|
const useAgents = [];
|
|
@@ -5669,7 +6386,16 @@ async function applyServiceAction(input3) {
|
|
|
5669
6386
|
}
|
|
5670
6387
|
const healthy = await input3.verifyHealth();
|
|
5671
6388
|
if (!healthy) {
|
|
5672
|
-
|
|
6389
|
+
if (input3.action.kind === "restart") {
|
|
6390
|
+
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.");
|
|
6391
|
+
}
|
|
6392
|
+
if (input3.action.kind === "start") {
|
|
6393
|
+
throw new Error("service health check failed after start; please check whether the target port is already occupied or the configuration is still invalid.");
|
|
6394
|
+
}
|
|
6395
|
+
if (input3.action.kind === "reload") {
|
|
6396
|
+
throw new Error("service health check failed after reload; please retry or run `ctr restart` / `ctr stop` first.");
|
|
6397
|
+
}
|
|
6398
|
+
throw new Error("service health check failed while reusing the current service; please run `ctr status` or `ctr restart` to verify it.");
|
|
5673
6399
|
}
|
|
5674
6400
|
}
|
|
5675
6401
|
var init_service = __esm({
|
|
@@ -5711,11 +6437,8 @@ var init_repair = __esm({
|
|
|
5711
6437
|
});
|
|
5712
6438
|
|
|
5713
6439
|
// src/setup/migrate.ts
|
|
5714
|
-
function inferProtocolFromApiBaseUrl(apiBaseUrl) {
|
|
5715
|
-
|
|
5716
|
-
return "anthropic";
|
|
5717
|
-
}
|
|
5718
|
-
return "openai";
|
|
6440
|
+
function inferProtocolFromApiBaseUrl(apiBaseUrl, modelName) {
|
|
6441
|
+
return inferInterfaceFromApiEndpoint(apiBaseUrl, modelName) ?? "openai";
|
|
5719
6442
|
}
|
|
5720
6443
|
function normalizeSegment(value) {
|
|
5721
6444
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
@@ -5740,20 +6463,6 @@ function createNonMigratableResult() {
|
|
|
5740
6463
|
missingFields: ["defaultModel", "apiKey", "apiBaseUrl"]
|
|
5741
6464
|
};
|
|
5742
6465
|
}
|
|
5743
|
-
function inferVendorHintFromLegacyProvider(provider) {
|
|
5744
|
-
const transformerUse = Array.isArray(provider.transformer?.use) ? provider.transformer.use.map((item) => String(item).trim().toLowerCase()) : [];
|
|
5745
|
-
const normalizedName = (provider.name ?? "").trim().toLowerCase();
|
|
5746
|
-
if (transformerUse.includes("openrouter")) {
|
|
5747
|
-
return "openrouter";
|
|
5748
|
-
}
|
|
5749
|
-
if (normalizedName.includes("qianfan")) {
|
|
5750
|
-
return "qianfan-coding";
|
|
5751
|
-
}
|
|
5752
|
-
if (normalizedName.includes("minimax")) {
|
|
5753
|
-
return "minimax-chatcompletion-v2";
|
|
5754
|
-
}
|
|
5755
|
-
return void 0;
|
|
5756
|
-
}
|
|
5757
6466
|
function isLegacyProviderInput(value) {
|
|
5758
6467
|
return typeof value === "object" && value !== null;
|
|
5759
6468
|
}
|
|
@@ -5765,6 +6474,80 @@ function pushUnique(target, value) {
|
|
|
5765
6474
|
target.push(value);
|
|
5766
6475
|
}
|
|
5767
6476
|
}
|
|
6477
|
+
function readString(value) {
|
|
6478
|
+
return typeof value === "string" ? value.trim() : void 0;
|
|
6479
|
+
}
|
|
6480
|
+
function readBoolean(value) {
|
|
6481
|
+
if (typeof value === "boolean") {
|
|
6482
|
+
return value;
|
|
6483
|
+
}
|
|
6484
|
+
if (typeof value === "string") {
|
|
6485
|
+
if (value.toLowerCase() === "true") {
|
|
6486
|
+
return true;
|
|
6487
|
+
}
|
|
6488
|
+
if (value.toLowerCase() === "false") {
|
|
6489
|
+
return false;
|
|
6490
|
+
}
|
|
6491
|
+
}
|
|
6492
|
+
return void 0;
|
|
6493
|
+
}
|
|
6494
|
+
function readFiniteNumber(value) {
|
|
6495
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
6496
|
+
return value;
|
|
6497
|
+
}
|
|
6498
|
+
if (typeof value === "string" && value.trim().length > 0) {
|
|
6499
|
+
const parsed = Number(value);
|
|
6500
|
+
if (Number.isFinite(parsed)) {
|
|
6501
|
+
return parsed;
|
|
6502
|
+
}
|
|
6503
|
+
}
|
|
6504
|
+
return void 0;
|
|
6505
|
+
}
|
|
6506
|
+
function extractSupportedTopLevelConfig(input3, consumedTopLevelFields) {
|
|
6507
|
+
const nextConfig = {};
|
|
6508
|
+
const hasOwn = (key) => Object.prototype.hasOwnProperty.call(input3, key);
|
|
6509
|
+
const host = readString(input3.HOST);
|
|
6510
|
+
if (hasOwn("HOST")) {
|
|
6511
|
+
nextConfig.HOST = host;
|
|
6512
|
+
consumedTopLevelFields.add("HOST");
|
|
6513
|
+
}
|
|
6514
|
+
const port = readFiniteNumber(input3.PORT);
|
|
6515
|
+
if (hasOwn("PORT") && port !== void 0) {
|
|
6516
|
+
nextConfig.PORT = port;
|
|
6517
|
+
consumedTopLevelFields.add("PORT");
|
|
6518
|
+
}
|
|
6519
|
+
const log2 = readBoolean(input3.LOG);
|
|
6520
|
+
if (hasOwn("LOG") && log2 !== void 0) {
|
|
6521
|
+
nextConfig.LOG = log2;
|
|
6522
|
+
consumedTopLevelFields.add("LOG");
|
|
6523
|
+
}
|
|
6524
|
+
const logLevel = readString(input3.LOG_LEVEL);
|
|
6525
|
+
if (hasOwn("LOG_LEVEL")) {
|
|
6526
|
+
nextConfig.LOG_LEVEL = logLevel;
|
|
6527
|
+
consumedTopLevelFields.add("LOG_LEVEL");
|
|
6528
|
+
}
|
|
6529
|
+
const apiTimeoutMs = readFiniteNumber(input3.API_TIMEOUT_MS);
|
|
6530
|
+
if (hasOwn("API_TIMEOUT_MS") && apiTimeoutMs !== void 0) {
|
|
6531
|
+
nextConfig.API_TIMEOUT_MS = apiTimeoutMs;
|
|
6532
|
+
consumedTopLevelFields.add("API_TIMEOUT_MS");
|
|
6533
|
+
}
|
|
6534
|
+
const proxyUrl = readString(input3.PROXY_URL);
|
|
6535
|
+
if (hasOwn("PROXY_URL")) {
|
|
6536
|
+
nextConfig.PROXY_URL = proxyUrl;
|
|
6537
|
+
consumedTopLevelFields.add("PROXY_URL");
|
|
6538
|
+
}
|
|
6539
|
+
const apiKey = readString(input3.APIKEY);
|
|
6540
|
+
if (hasOwn("APIKEY")) {
|
|
6541
|
+
nextConfig.APIKEY = apiKey;
|
|
6542
|
+
consumedTopLevelFields.add("APIKEY");
|
|
6543
|
+
}
|
|
6544
|
+
const customRouterPath = readString(input3.CUSTOM_ROUTER_PATH);
|
|
6545
|
+
if (hasOwn("CUSTOM_ROUTER_PATH")) {
|
|
6546
|
+
nextConfig.CUSTOM_ROUTER_PATH = customRouterPath;
|
|
6547
|
+
consumedTopLevelFields.add("CUSTOM_ROUTER_PATH");
|
|
6548
|
+
}
|
|
6549
|
+
return nextConfig;
|
|
6550
|
+
}
|
|
5768
6551
|
function normalizeLegacyConfig(input3) {
|
|
5769
6552
|
const lowerProviders = Array.isArray(input3.providers) && input3.providers.every(isLegacyProviderInput) ? input3.providers : null;
|
|
5770
6553
|
const upperProviders = Array.isArray(input3.Providers) && input3.Providers.every(isLegacyProviderInput) ? input3.Providers : null;
|
|
@@ -5788,6 +6571,7 @@ function normalizeLegacyConfig(input3) {
|
|
|
5788
6571
|
pushUnique(skippedFields, alternateDefaultKey);
|
|
5789
6572
|
}
|
|
5790
6573
|
const consumedTopLevelFields = /* @__PURE__ */ new Set([providerKey]);
|
|
6574
|
+
const supportedTopLevelConfig = extractSupportedTopLevelConfig(input3, consumedTopLevelFields);
|
|
5791
6575
|
const providers = rawProviders.map((provider, index) => {
|
|
5792
6576
|
if (provider.transformer !== void 0) {
|
|
5793
6577
|
pushUnique(skippedFields, `${providerKey}[${index}].transformer`);
|
|
@@ -5799,11 +6583,11 @@ function normalizeLegacyConfig(input3) {
|
|
|
5799
6583
|
name: provider.name ?? "",
|
|
5800
6584
|
api_base_url: provider.api_base_url,
|
|
5801
6585
|
api_key: provider.api_key ?? "",
|
|
5802
|
-
models: Array.isArray(provider.models) ? provider.models : []
|
|
5803
|
-
vendor_hint: inferVendorHintFromLegacyProvider(provider)
|
|
6586
|
+
models: Array.isArray(provider.models) ? provider.models : []
|
|
5804
6587
|
};
|
|
5805
6588
|
});
|
|
5806
6589
|
let defaultRoute;
|
|
6590
|
+
const routeSlots = {};
|
|
5807
6591
|
if (providerKey === "providers") {
|
|
5808
6592
|
consumedTopLevelFields.add("default");
|
|
5809
6593
|
defaultRoute = typeof input3.default === "string" ? input3.default : void 0;
|
|
@@ -5811,18 +6595,11 @@ function normalizeLegacyConfig(input3) {
|
|
|
5811
6595
|
consumedTopLevelFields.add("Router");
|
|
5812
6596
|
if (isLegacyRouterInput(input3.Router)) {
|
|
5813
6597
|
defaultRoute = typeof input3.Router.default === "string" ? input3.Router.default : void 0;
|
|
5814
|
-
|
|
5815
|
-
|
|
5816
|
-
|
|
5817
|
-
|
|
5818
|
-
|
|
5819
|
-
}
|
|
5820
|
-
if (input3.Router.longContext !== void 0) {
|
|
5821
|
-
pushUnique(skippedFields, "Router.longContext");
|
|
5822
|
-
}
|
|
5823
|
-
if (input3.Router.longContextThreshold !== void 0) {
|
|
5824
|
-
pushUnique(skippedFields, "Router.longContextThreshold");
|
|
5825
|
-
}
|
|
6598
|
+
routeSlots.background = readString(input3.Router.background);
|
|
6599
|
+
routeSlots.think = readString(input3.Router.think);
|
|
6600
|
+
routeSlots.longContext = readString(input3.Router.longContext);
|
|
6601
|
+
routeSlots.webSearch = readString(input3.Router.webSearch);
|
|
6602
|
+
routeSlots.longContextThreshold = readFiniteNumber(input3.Router.longContextThreshold);
|
|
5826
6603
|
}
|
|
5827
6604
|
}
|
|
5828
6605
|
for (const key of Object.keys(input3)) {
|
|
@@ -5834,6 +6611,8 @@ function normalizeLegacyConfig(input3) {
|
|
|
5834
6611
|
return {
|
|
5835
6612
|
providers,
|
|
5836
6613
|
defaultRoute,
|
|
6614
|
+
routeSlots,
|
|
6615
|
+
supportedTopLevelConfig,
|
|
5837
6616
|
skippedFields
|
|
5838
6617
|
};
|
|
5839
6618
|
}
|
|
@@ -5845,15 +6624,14 @@ function migrateLegacyConfig(input3) {
|
|
|
5845
6624
|
const rawEntries = normalized.providers.flatMap(
|
|
5846
6625
|
(provider, providerIndex) => (provider.models.length ? provider.models : [""]).map((model) => ({
|
|
5847
6626
|
candidateId: toModelId(provider.name, model, providerIndex),
|
|
5848
|
-
api: provider.api_base_url,
|
|
5849
|
-
api_base_url: provider.api_base_url,
|
|
6627
|
+
api: provider.api_base_url ? normalizeApiEndpoint(provider.api_base_url, inferProtocolFromApiBaseUrl(provider.api_base_url, model)) : void 0,
|
|
6628
|
+
api_base_url: provider.api_base_url ? normalizeApiEndpoint(provider.api_base_url, inferProtocolFromApiBaseUrl(provider.api_base_url, model)) : void 0,
|
|
5850
6629
|
key: provider.api_key,
|
|
5851
6630
|
api_key: provider.api_key,
|
|
5852
|
-
interface: inferProtocolFromApiBaseUrl(provider.api_base_url),
|
|
5853
|
-
protocol: inferProtocolFromApiBaseUrl(provider.api_base_url),
|
|
6631
|
+
interface: inferProtocolFromApiBaseUrl(provider.api_base_url, model),
|
|
6632
|
+
protocol: inferProtocolFromApiBaseUrl(provider.api_base_url, model),
|
|
5854
6633
|
model,
|
|
5855
|
-
providerName: provider.name
|
|
5856
|
-
vendorHint: provider.vendor_hint
|
|
6634
|
+
providerName: provider.name
|
|
5857
6635
|
})).filter((item) => item.model)
|
|
5858
6636
|
);
|
|
5859
6637
|
const seenIds = /* @__PURE__ */ new Map();
|
|
@@ -5871,23 +6649,29 @@ function migrateLegacyConfig(input3) {
|
|
|
5871
6649
|
api_key: entry.api_key,
|
|
5872
6650
|
interface: entry.interface,
|
|
5873
6651
|
protocol: entry.protocol,
|
|
5874
|
-
model: entry.model
|
|
5875
|
-
metadata: entry.vendorHint ? {
|
|
5876
|
-
vendor_hint: entry.vendorHint
|
|
5877
|
-
} : void 0
|
|
6652
|
+
model: entry.model
|
|
5878
6653
|
};
|
|
5879
6654
|
});
|
|
5880
|
-
const
|
|
5881
|
-
|
|
5882
|
-
|
|
6655
|
+
const resolveLegacyRoute = (ref, fieldName) => {
|
|
6656
|
+
if (!ref) {
|
|
6657
|
+
return void 0;
|
|
6658
|
+
}
|
|
6659
|
+
const [rawProviderName, rawModelName] = String(ref).split(",");
|
|
5883
6660
|
const providerName = (rawProviderName ?? "").trim();
|
|
5884
6661
|
const modelName = (rawModelName ?? "").trim();
|
|
5885
6662
|
const fromLookup = routeLookup.get(`${providerName},${modelName}`);
|
|
5886
|
-
if (fromLookup)
|
|
5887
|
-
|
|
5888
|
-
|
|
5889
|
-
)
|
|
5890
|
-
|
|
6663
|
+
if (fromLookup) {
|
|
6664
|
+
return fromLookup;
|
|
6665
|
+
}
|
|
6666
|
+
pushUnique(normalized.skippedFields, fieldName);
|
|
6667
|
+
return void 0;
|
|
6668
|
+
};
|
|
6669
|
+
const hasLegacyDefaultRoute = typeof normalized.defaultRoute === "string" && normalized.defaultRoute.length > 0;
|
|
6670
|
+
const defaultModelId = hasLegacyDefaultRoute ? resolveLegacyRoute(normalized.defaultRoute, "Router.default") : void 0;
|
|
6671
|
+
const backgroundModelId = resolveLegacyRoute(normalized.routeSlots.background, "Router.background");
|
|
6672
|
+
const thinkModelId = resolveLegacyRoute(normalized.routeSlots.think, "Router.think");
|
|
6673
|
+
const longContextModelId = resolveLegacyRoute(normalized.routeSlots.longContext, "Router.longContext");
|
|
6674
|
+
const webSearchModelId = resolveLegacyRoute(normalized.routeSlots.webSearch, "Router.webSearch");
|
|
5891
6675
|
const hasMissingApiKey = normalized.providers.some((provider) => provider.api_key.length === 0);
|
|
5892
6676
|
const hasMissingApiBaseUrl = normalized.providers.some((provider) => (provider.api_base_url?.trim() ?? "").length === 0);
|
|
5893
6677
|
const missingFields = [];
|
|
@@ -5902,9 +6686,17 @@ function migrateLegacyConfig(input3) {
|
|
|
5902
6686
|
}
|
|
5903
6687
|
return {
|
|
5904
6688
|
draft: {
|
|
6689
|
+
...normalized.supportedTopLevelConfig,
|
|
5905
6690
|
Providers: [],
|
|
5906
6691
|
Models: models,
|
|
5907
|
-
Router:
|
|
6692
|
+
Router: {
|
|
6693
|
+
...defaultModelId ? { default: defaultModelId } : {},
|
|
6694
|
+
...backgroundModelId ? { background: backgroundModelId } : {},
|
|
6695
|
+
...thinkModelId ? { think: thinkModelId } : {},
|
|
6696
|
+
...longContextModelId ? { longContext: longContextModelId } : {},
|
|
6697
|
+
...normalized.routeSlots.longContextThreshold !== void 0 ? { longContextThreshold: normalized.routeSlots.longContextThreshold } : {},
|
|
6698
|
+
...webSearchModelId ? { webSearch: webSearchModelId } : {}
|
|
6699
|
+
}
|
|
5908
6700
|
},
|
|
5909
6701
|
skippedFields: normalized.skippedFields,
|
|
5910
6702
|
needsCompletion: missingFields.length > 0,
|
|
@@ -5914,6 +6706,7 @@ function migrateLegacyConfig(input3) {
|
|
|
5914
6706
|
var init_migrate = __esm({
|
|
5915
6707
|
"src/setup/migrate.ts"() {
|
|
5916
6708
|
"use strict";
|
|
6709
|
+
init_schema();
|
|
5917
6710
|
}
|
|
5918
6711
|
});
|
|
5919
6712
|
|
|
@@ -5984,8 +6777,8 @@ function buildMinimalConfig(input3) {
|
|
|
5984
6777
|
key: p.api_key,
|
|
5985
6778
|
api_key: p.api_key,
|
|
5986
6779
|
model: p.models[0] ?? "",
|
|
5987
|
-
interface: preset?.interface ?? "openai",
|
|
5988
|
-
protocol: preset?.protocol ?? "openai"
|
|
6780
|
+
interface: p.interface ?? preset?.interface ?? "openai",
|
|
6781
|
+
protocol: p.interface ?? preset?.protocol ?? "openai"
|
|
5989
6782
|
};
|
|
5990
6783
|
const explicitApiBaseUrl = p.api_base_url?.trim();
|
|
5991
6784
|
const presetApiBaseUrl = preset?.api_base_url?.trim();
|
|
@@ -6615,6 +7408,7 @@ async function executeRestart() {
|
|
|
6615
7408
|
if (info) {
|
|
6616
7409
|
try {
|
|
6617
7410
|
killProcess(info.pid);
|
|
7411
|
+
await waitForProcessExit(info.pid, 5e3);
|
|
6618
7412
|
} catch {
|
|
6619
7413
|
}
|
|
6620
7414
|
}
|
|
@@ -6749,15 +7543,114 @@ function toDraftFromConfig(config) {
|
|
|
6749
7543
|
}
|
|
6750
7544
|
};
|
|
6751
7545
|
}
|
|
6752
|
-
function
|
|
6753
|
-
const
|
|
6754
|
-
if (
|
|
6755
|
-
return
|
|
7546
|
+
function toUniqueSuggestedModelId(preferredId, existingIds) {
|
|
7547
|
+
const normalizedPreferredId = preferredId.trim() || "model";
|
|
7548
|
+
if (!existingIds.includes(normalizedPreferredId)) {
|
|
7549
|
+
return normalizedPreferredId;
|
|
6756
7550
|
}
|
|
6757
|
-
|
|
6758
|
-
|
|
7551
|
+
let suffix = 2;
|
|
7552
|
+
while (existingIds.includes(`${normalizedPreferredId}_${suffix}`)) {
|
|
7553
|
+
suffix += 1;
|
|
7554
|
+
}
|
|
7555
|
+
return `${normalizedPreferredId}_${suffix}`;
|
|
6759
7556
|
}
|
|
6760
|
-
|
|
7557
|
+
function appendModelToDraft(draft, modelInput, options = {}) {
|
|
7558
|
+
const fragment = buildMinimalConfig({
|
|
7559
|
+
providers: [modelInput],
|
|
7560
|
+
defaultModel: options.setAsDefault ? modelInput.model_id : void 0
|
|
7561
|
+
});
|
|
7562
|
+
const nextDraft = {
|
|
7563
|
+
...draft,
|
|
7564
|
+
Models: [...draft.Models ?? []],
|
|
7565
|
+
Router: { ...draft.Router ?? {} }
|
|
7566
|
+
};
|
|
7567
|
+
if (fragment.Models?.[0]) {
|
|
7568
|
+
nextDraft.Models?.push(fragment.Models[0]);
|
|
7569
|
+
}
|
|
7570
|
+
if (options.setAsDefault) {
|
|
7571
|
+
nextDraft.Router.default = modelInput.model_id;
|
|
7572
|
+
} else if (!nextDraft.Router.default) {
|
|
7573
|
+
nextDraft.Router.default = modelInput.model_id;
|
|
7574
|
+
}
|
|
7575
|
+
return nextDraft;
|
|
7576
|
+
}
|
|
7577
|
+
function createComplexTaskRules(modelId) {
|
|
7578
|
+
return [
|
|
7579
|
+
{
|
|
7580
|
+
name: "architecture",
|
|
7581
|
+
priority: 90,
|
|
7582
|
+
enabled: true,
|
|
7583
|
+
description: "\u67B6\u6784\u8BBE\u8BA1\u3001\u7CFB\u7EDF\u89C4\u5212\u548C\u5927\u8303\u56F4\u91CD\u6784\u4EFB\u52A1",
|
|
7584
|
+
patterns: [
|
|
7585
|
+
{ type: "exact", keywords: ["\u67B6\u6784\u8BBE\u8BA1", "\u7CFB\u7EDF\u8BBE\u8BA1", "\u6280\u672F\u65B9\u6848", "architecture", "system design"] },
|
|
7586
|
+
{ type: "regex", pattern: "(\u67B6\u6784|\u7CFB\u7EDF\u8BBE\u8BA1|\u6280\u672F\u65B9\u6848|architecture|system design)" }
|
|
7587
|
+
],
|
|
7588
|
+
model: modelId
|
|
7589
|
+
},
|
|
7590
|
+
{
|
|
7591
|
+
name: "code_review",
|
|
7592
|
+
priority: 80,
|
|
7593
|
+
enabled: true,
|
|
7594
|
+
description: "\u4EE3\u7801\u5BA1\u67E5\u3001\u98CE\u9669\u8BC4\u4F30\u548C\u8D28\u91CF\u5206\u6790\u4EFB\u52A1",
|
|
7595
|
+
patterns: [
|
|
7596
|
+
{ type: "exact", keywords: ["\u4EE3\u7801\u5BA1\u67E5", "code review", "review code", "\u98CE\u9669\u8BC4\u4F30"] },
|
|
7597
|
+
{ type: "regex", pattern: "(\u4EE3\u7801|code).{0,6}(\u5BA1\u67E5|review|\u5BA1\u6838|\u68C0\u67E5)" }
|
|
7598
|
+
],
|
|
7599
|
+
model: modelId
|
|
7600
|
+
},
|
|
7601
|
+
{
|
|
7602
|
+
name: "deep_reasoning",
|
|
7603
|
+
priority: 70,
|
|
7604
|
+
enabled: true,
|
|
7605
|
+
description: "\u590D\u6742\u63A8\u7406\u3001\u6DF1\u5165\u5206\u6790\u548C\u591A\u6B65\u51B3\u7B56\u4EFB\u52A1",
|
|
7606
|
+
patterns: [
|
|
7607
|
+
{ type: "exact", keywords: ["\u6DF1\u5165\u5206\u6790", "\u590D\u6742\u63A8\u7406", "\u4E25\u8C28\u5206\u6790", "deep analysis", "reasoning"] },
|
|
7608
|
+
{ type: "regex", pattern: "(\u6DF1\u5165|\u590D\u6742|\u4E25\u8C28).{0,6}(\u5206\u6790|\u63A8\u7406|\u8BBA\u8BC1)" }
|
|
7609
|
+
],
|
|
7610
|
+
model: modelId
|
|
7611
|
+
}
|
|
7612
|
+
];
|
|
7613
|
+
}
|
|
7614
|
+
function applyRoutingBootstrap(draft, choice, specializedModelId) {
|
|
7615
|
+
if (choice === "\u5148\u4FDD\u6301\u6700\u5C0F\u914D\u7F6E") {
|
|
7616
|
+
return draft;
|
|
7617
|
+
}
|
|
7618
|
+
const defaultModelId = draft.Router.default;
|
|
7619
|
+
if (!defaultModelId) {
|
|
7620
|
+
return draft;
|
|
7621
|
+
}
|
|
7622
|
+
const specializedModel = draft.Models?.find((item) => item.id === specializedModelId);
|
|
7623
|
+
if (!specializedModel) {
|
|
7624
|
+
return draft;
|
|
7625
|
+
}
|
|
7626
|
+
const nextDraft = {
|
|
7627
|
+
...draft,
|
|
7628
|
+
SmartRouter: {
|
|
7629
|
+
enabled: true,
|
|
7630
|
+
analysis_scope: "last_message",
|
|
7631
|
+
rules: createComplexTaskRules(specializedModelId),
|
|
7632
|
+
...choice === "\u5F00\u542F\u590D\u6742\u4EFB\u52A1\u89C4\u5219 + \u667A\u80FD\u515C\u5E95" ? {
|
|
7633
|
+
router_model: defaultModelId,
|
|
7634
|
+
candidates: [
|
|
7635
|
+
{
|
|
7636
|
+
model: defaultModelId,
|
|
7637
|
+
description: "\u9ED8\u8BA4\u6A21\u578B\uFF0C\u9002\u5408\u901A\u7528\u7F16\u7A0B\u3001\u65E5\u5E38\u4FEE\u590D\u548C\u5FEB\u901F\u54CD\u5E94\u4EFB\u52A1"
|
|
7638
|
+
},
|
|
7639
|
+
{
|
|
7640
|
+
model: specializedModelId,
|
|
7641
|
+
description: `\u590D\u6742\u4EFB\u52A1\u6A21\u578B\uFF08${specializedModel.model}\uFF09\uFF0C\u9002\u5408\u67B6\u6784\u8BBE\u8BA1\u3001\u4EE3\u7801\u5BA1\u67E5\u548C\u6DF1\u5165\u63A8\u7406`
|
|
7642
|
+
}
|
|
7643
|
+
]
|
|
7644
|
+
} : {}
|
|
7645
|
+
}
|
|
7646
|
+
};
|
|
7647
|
+
return nextDraft;
|
|
7648
|
+
}
|
|
7649
|
+
async function promptModelConnection(io, input3) {
|
|
7650
|
+
if (input3.intro) {
|
|
7651
|
+
io.info(input3.intro);
|
|
7652
|
+
}
|
|
7653
|
+
const modelId = await io.input(input3.modelIdPrompt, input3.suggestedModelId);
|
|
6761
7654
|
const connectMode = await io.choose("\u8FD9\u4E2A\u6A21\u578B\u63A5\u5230\u54EA\u91CC\uFF1F", ["\u4F7F\u7528\u5E38\u89C1\u63A5\u5165\u6A21\u677F", "\u624B\u52A8\u586B\u5199\u63A5\u53E3"]);
|
|
6762
7655
|
let preset = "custom";
|
|
6763
7656
|
let providerName = "provider";
|
|
@@ -6774,21 +7667,50 @@ async function buildFreshConfig(io) {
|
|
|
6774
7667
|
const apiKey = await io.input("API Key");
|
|
6775
7668
|
const presetDefinition = getProviderPreset(preset);
|
|
6776
7669
|
const model = await io.input("\u4E0A\u6E38\u6A21\u578B\u540D", presetDefinition?.default_model ?? "");
|
|
6777
|
-
const
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
|
|
6782
|
-
|
|
6783
|
-
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
6790
|
-
|
|
7670
|
+
const interfaceChoice = connectMode === "\u624B\u52A8\u586B\u5199\u63A5\u53E3" ? await io.choose("\u63A5\u53E3\u7C7B\u578B", ["openai", "anthropic"]) : presetDefinition?.interface;
|
|
7671
|
+
return {
|
|
7672
|
+
name: providerName,
|
|
7673
|
+
model_id: modelId,
|
|
7674
|
+
api_key: apiKey,
|
|
7675
|
+
interface: interfaceChoice,
|
|
7676
|
+
models: [model],
|
|
7677
|
+
preset,
|
|
7678
|
+
api_base_url: apiBaseUrl
|
|
7679
|
+
};
|
|
7680
|
+
}
|
|
7681
|
+
async function buildFreshConfig(io) {
|
|
7682
|
+
const primaryModel = await promptModelConnection(io, {
|
|
7683
|
+
intro: "\u6211\u4EEC\u5148\u521B\u5EFA\u4E00\u4EFD\u6700\u5C0F\u53EF\u7528\u914D\u7F6E\u3002",
|
|
7684
|
+
modelIdPrompt: "\u8FD9\u4E2A\u9ED8\u8BA4\u6A21\u578B\u5728\u672C\u5730\u8981\u53EB\u4EC0\u4E48\u540D\u5B57\uFF1F",
|
|
7685
|
+
suggestedModelId: "sonnet"
|
|
7686
|
+
});
|
|
7687
|
+
let draft = buildMinimalConfig({
|
|
7688
|
+
providers: [primaryModel],
|
|
7689
|
+
defaultModel: primaryModel.model_id
|
|
6791
7690
|
});
|
|
7691
|
+
const addSecondModelChoice = await io.choose("\u73B0\u5728\u8981\u4E0D\u8981\u7EE7\u7EED\u6DFB\u52A0\u4E00\u4E2A\u201C\u590D\u6742\u4EFB\u52A1\u4E13\u7528\u6A21\u578B\u201D\uFF1F", [
|
|
7692
|
+
"\u5148\u4E0D\u6DFB\u52A0",
|
|
7693
|
+
"\u6DFB\u52A0\u4E00\u4E2A\u590D\u6742\u4EFB\u52A1\u4E13\u7528\u6A21\u578B"
|
|
7694
|
+
]);
|
|
7695
|
+
if (addSecondModelChoice === "\u6DFB\u52A0\u4E00\u4E2A\u590D\u6742\u4EFB\u52A1\u4E13\u7528\u6A21\u578B") {
|
|
7696
|
+
const suggestedSecondModelId = toUniqueSuggestedModelId("reasoner", draft.Models?.map((item) => item.id) ?? []);
|
|
7697
|
+
const specializedModel = await promptModelConnection(io, {
|
|
7698
|
+
intro: "\u8FD9\u4E2A\u6A21\u578B\u901A\u5E38\u7528\u4E8E\u67B6\u6784\u8BBE\u8BA1\u3001\u4EE3\u7801\u5BA1\u67E5\u6216\u590D\u6742\u63A8\u7406\u7B49\u66F4\u91CD\u7684\u4EFB\u52A1\u3002",
|
|
7699
|
+
modelIdPrompt: "\u8FD9\u4E2A\u590D\u6742\u4EFB\u52A1\u6A21\u578B\u5728\u672C\u5730\u8981\u53EB\u4EC0\u4E48\u540D\u5B57\uFF1F",
|
|
7700
|
+
suggestedModelId: suggestedSecondModelId
|
|
7701
|
+
});
|
|
7702
|
+
draft = appendModelToDraft(draft, specializedModel);
|
|
7703
|
+
const routingChoice = await io.choose("\u73B0\u5728\u8981\u4E0D\u8981\u5F00\u542F\u9AD8\u7EA7\u8DEF\u7531\uFF1F", [
|
|
7704
|
+
"\u5148\u4FDD\u6301\u6700\u5C0F\u914D\u7F6E",
|
|
7705
|
+
"\u5F00\u542F\u590D\u6742\u4EFB\u52A1\u89C4\u5219\u6A21\u677F",
|
|
7706
|
+
"\u5F00\u542F\u590D\u6742\u4EFB\u52A1\u89C4\u5219 + \u667A\u80FD\u515C\u5E95"
|
|
7707
|
+
]);
|
|
7708
|
+
draft = applyRoutingBootstrap(draft, routingChoice, specializedModel.model_id);
|
|
7709
|
+
if (routingChoice !== "\u5148\u4FDD\u6301\u6700\u5C0F\u914D\u7F6E") {
|
|
7710
|
+
io.info(`\u5DF2\u4E3A\u4F60\u751F\u6210 SmartRouter \u8DEF\u7531\u6A21\u677F\uFF0C\u9ED8\u8BA4\u6A21\u578B\u4ECD\u662F ${primaryModel.model_id}\uFF0C\u590D\u6742\u4EFB\u52A1\u4F1A\u4F18\u5148\u4F7F\u7528 ${specializedModel.model_id}\u3002`);
|
|
7711
|
+
}
|
|
7712
|
+
}
|
|
7713
|
+
const capabilityMode = await io.choose("\u662F\u5426\u914D\u7F6E capability \u63D0\u793A", ["\u4FDD\u6301\u9ED8\u8BA4", "\u914D\u7F6E capability \u63D0\u793A"]);
|
|
6792
7714
|
if (capabilityMode === "\u914D\u7F6E capability \u63D0\u793A" && draft.Models?.[0]) {
|
|
6793
7715
|
await promptCapabilityMetadataForDraft(draft, io);
|
|
6794
7716
|
}
|
|
@@ -6853,8 +7775,8 @@ function createDefaultDeps(io = createConsoleIO()) {
|
|
|
6853
7775
|
}
|
|
6854
7776
|
function printRoutingNextSteps(io) {
|
|
6855
7777
|
io.info("\u4F60\u53EF\u4EE5\u6309\u9700\u7EE7\u7EED\u914D\u7F6E\u8DEF\u7531\u80FD\u529B\uFF1A");
|
|
6856
|
-
io.info(" -
|
|
6857
|
-
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");
|
|
7778
|
+
io.info(" - SmartRouter.rules\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");
|
|
7779
|
+
io.info(" - SmartRouter candidates\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");
|
|
6858
7780
|
io.info(" - \u914D\u7F6E\u6A21\u677F\u53C2\u8003\uFF1Aconfig/trigger.advanced.yaml");
|
|
6859
7781
|
}
|
|
6860
7782
|
async function runSetupCli(customDeps) {
|
|
@@ -7007,6 +7929,69 @@ var init_setup2 = __esm({
|
|
|
7007
7929
|
});
|
|
7008
7930
|
|
|
7009
7931
|
// src/doctor/index.ts
|
|
7932
|
+
function collectCompatibilityPreviewDiagnostics(model) {
|
|
7933
|
+
const registry = buildModelRegistry({
|
|
7934
|
+
Providers: [],
|
|
7935
|
+
Models: [model],
|
|
7936
|
+
Router: {
|
|
7937
|
+
default: model.id
|
|
7938
|
+
}
|
|
7939
|
+
});
|
|
7940
|
+
const compiledModel = registry.modelMap[model.id];
|
|
7941
|
+
if (!compiledModel) {
|
|
7942
|
+
return [];
|
|
7943
|
+
}
|
|
7944
|
+
const preview = buildProviderDispatchRequest({
|
|
7945
|
+
model: compiledModel.modelName,
|
|
7946
|
+
interface: compiledModel.interface ?? "openai",
|
|
7947
|
+
compatibilityProfile: compiledModel.compatibilityProfile,
|
|
7948
|
+
capabilities: compiledModel.capabilities,
|
|
7949
|
+
request: {
|
|
7950
|
+
model: compiledModel.id,
|
|
7951
|
+
max_tokens: 32,
|
|
7952
|
+
messages: [
|
|
7953
|
+
{
|
|
7954
|
+
role: "user",
|
|
7955
|
+
content: [
|
|
7956
|
+
{
|
|
7957
|
+
type: "text",
|
|
7958
|
+
text: "compatibility preview"
|
|
7959
|
+
},
|
|
7960
|
+
{
|
|
7961
|
+
type: "image",
|
|
7962
|
+
source: {
|
|
7963
|
+
type: "base64",
|
|
7964
|
+
media_type: "image/png",
|
|
7965
|
+
data: "preview"
|
|
7966
|
+
}
|
|
7967
|
+
}
|
|
7968
|
+
]
|
|
7969
|
+
}
|
|
7970
|
+
],
|
|
7971
|
+
tools: [
|
|
7972
|
+
{
|
|
7973
|
+
name: "preview_tool",
|
|
7974
|
+
description: "Preview tool",
|
|
7975
|
+
input_schema: {
|
|
7976
|
+
type: "object",
|
|
7977
|
+
properties: {
|
|
7978
|
+
query: { type: "string" }
|
|
7979
|
+
}
|
|
7980
|
+
}
|
|
7981
|
+
}
|
|
7982
|
+
],
|
|
7983
|
+
tool_choice: {
|
|
7984
|
+
type: "tool",
|
|
7985
|
+
name: "preview_tool"
|
|
7986
|
+
},
|
|
7987
|
+
thinking: {
|
|
7988
|
+
type: "enabled",
|
|
7989
|
+
effort: "medium"
|
|
7990
|
+
}
|
|
7991
|
+
}
|
|
7992
|
+
});
|
|
7993
|
+
return preview.diagnostics.map((code) => describeProtocolDiagnostic(code));
|
|
7994
|
+
}
|
|
7010
7995
|
function hasArg(flag) {
|
|
7011
7996
|
return process.argv.slice(2).includes(flag);
|
|
7012
7997
|
}
|
|
@@ -7103,12 +8088,8 @@ function createConsoleIO2() {
|
|
|
7103
8088
|
function getConfigCandidates() {
|
|
7104
8089
|
return [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
|
|
7105
8090
|
}
|
|
7106
|
-
function inferInterfaceFromApi(api) {
|
|
7107
|
-
|
|
7108
|
-
if (!trimmed) {
|
|
7109
|
-
return void 0;
|
|
7110
|
-
}
|
|
7111
|
-
return trimmed.includes("/v1/messages") ? "anthropic" : "openai";
|
|
8091
|
+
function inferInterfaceFromApi(api, modelName) {
|
|
8092
|
+
return inferInterfaceFromApiEndpoint(api, modelName);
|
|
7112
8093
|
}
|
|
7113
8094
|
function sanitizeModelId(value) {
|
|
7114
8095
|
return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "model";
|
|
@@ -7176,7 +8157,7 @@ function repairDeterministicConfig(config) {
|
|
|
7176
8157
|
nextConfig.Models = config.Models.map((item, index) => {
|
|
7177
8158
|
const api = getModelApi(item);
|
|
7178
8159
|
const key = getModelKey(item);
|
|
7179
|
-
const inferredInterface = getModelInterface(item) ?? inferInterfaceFromApi(api);
|
|
8160
|
+
const inferredInterface = getModelInterface(item) ?? inferInterfaceFromApi(api, item.model);
|
|
7180
8161
|
const id = item.id?.trim() || (item.model ? sanitizeModelId(item.model) : `model_${index + 1}`);
|
|
7181
8162
|
if (!item.id?.trim()) {
|
|
7182
8163
|
changes.push(`\u5DF2\u8865\u5168 Models[${index}].id -> ${id}`);
|
|
@@ -7385,6 +8366,46 @@ async function probeModelAvailability(model) {
|
|
|
7385
8366
|
};
|
|
7386
8367
|
}
|
|
7387
8368
|
}
|
|
8369
|
+
function explainProbeFailure(category) {
|
|
8370
|
+
switch (category) {
|
|
8371
|
+
case "auth_error":
|
|
8372
|
+
return {
|
|
8373
|
+
label: "\u9274\u6743\u5931\u8D25",
|
|
8374
|
+
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",
|
|
8375
|
+
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"
|
|
8376
|
+
};
|
|
8377
|
+
case "model_not_found":
|
|
8378
|
+
return {
|
|
8379
|
+
label: "\u6A21\u578B\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u9650",
|
|
8380
|
+
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",
|
|
8381
|
+
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"
|
|
8382
|
+
};
|
|
8383
|
+
case "endpoint_unreachable":
|
|
8384
|
+
return {
|
|
8385
|
+
label: "\u63A5\u53E3\u4E0D\u53EF\u8FBE",
|
|
8386
|
+
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",
|
|
8387
|
+
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"
|
|
8388
|
+
};
|
|
8389
|
+
case "protocol_mismatch":
|
|
8390
|
+
return {
|
|
8391
|
+
label: "\u534F\u8BAE\u517C\u5BB9\u5931\u8D25",
|
|
8392
|
+
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",
|
|
8393
|
+
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"
|
|
8394
|
+
};
|
|
8395
|
+
case "remote_error":
|
|
8396
|
+
return {
|
|
8397
|
+
label: "\u4E0A\u6E38\u8FD4\u56DE\u9519\u8BEF",
|
|
8398
|
+
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",
|
|
8399
|
+
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"
|
|
8400
|
+
};
|
|
8401
|
+
default:
|
|
8402
|
+
return {
|
|
8403
|
+
label: category,
|
|
8404
|
+
summary: "\u672A\u77E5\u8FDC\u7AEF\u9519\u8BEF\u3002",
|
|
8405
|
+
action: "\u8BF7\u4FDD\u7559\u539F\u59CB\u9519\u8BEF\u4FE1\u606F\u540E\u7EE7\u7EED\u6392\u67E5\u3002"
|
|
8406
|
+
};
|
|
8407
|
+
}
|
|
8408
|
+
}
|
|
7388
8409
|
async function ensureServiceUsable(config, deps, configChanged) {
|
|
7389
8410
|
const port = config.PORT ?? DEFAULT_CONFIG2.PORT;
|
|
7390
8411
|
const healthy = await deps.probeServiceHealth(port, 500);
|
|
@@ -7470,6 +8491,26 @@ async function runDoctorCli(customDeps) {
|
|
|
7470
8491
|
if (normalized.warnings.length > 0) {
|
|
7471
8492
|
deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${normalized.warnings.join("; ")}`);
|
|
7472
8493
|
}
|
|
8494
|
+
const registry = buildModelRegistry(normalized.config);
|
|
8495
|
+
for (const model of normalized.config.Models ?? []) {
|
|
8496
|
+
const compiledModel = registry.modelMap[model.id];
|
|
8497
|
+
if (!compiledModel) {
|
|
8498
|
+
continue;
|
|
8499
|
+
}
|
|
8500
|
+
const compatibility = describeCompatibilityProfile(compiledModel.compatibilityProfile);
|
|
8501
|
+
const dispatch = describeDispatchFormat(compiledModel.dispatchFormat);
|
|
8502
|
+
deps.io.info(
|
|
8503
|
+
`\u6A21\u578B\u517C\u5BB9\u7B56\u7565\uFF1A${model.id} -> ${compatibility.label}`
|
|
8504
|
+
);
|
|
8505
|
+
deps.io.info(`\u517C\u5BB9\u8BF4\u660E\uFF1A${compatibility.summary}`);
|
|
8506
|
+
deps.io.info(`\u8BF7\u6C42\u7F16\u8BD1\uFF1A${dispatch.label}\u3002${dispatch.summary}`);
|
|
8507
|
+
const previewDiagnostics = collectCompatibilityPreviewDiagnostics(model);
|
|
8508
|
+
for (const diagnostic of previewDiagnostics) {
|
|
8509
|
+
deps.io.info(`\u8FD0\u884C\u65F6\u517C\u5BB9\u63D0\u793A\uFF1A${diagnostic.label}`);
|
|
8510
|
+
deps.io.info(`\u8FD0\u884C\u65F6\u8BF4\u660E\uFF1A${diagnostic.summary}`);
|
|
8511
|
+
deps.io.info(`\u8FD0\u884C\u65F6\u5EFA\u8BAE\uFF1A${diagnostic.action}`);
|
|
8512
|
+
}
|
|
8513
|
+
}
|
|
7473
8514
|
const needWrite = current.repairedParse || deterministic.changes.length > 0 || completed.changes.length > 0 || !current.existed;
|
|
7474
8515
|
if (needWrite) {
|
|
7475
8516
|
if (current.existed) {
|
|
@@ -7488,15 +8529,24 @@ async function runDoctorCli(customDeps) {
|
|
|
7488
8529
|
deps.io.info("\u5DF2\u8DF3\u8FC7\u6A21\u578B\u63A2\u6D4B\u3002\u914D\u7F6E\u548C\u670D\u52A1\u8BCA\u65AD\u5DF2\u5B8C\u6210\u3002");
|
|
7489
8530
|
return;
|
|
7490
8531
|
}
|
|
8532
|
+
let probeSuccess = 0;
|
|
8533
|
+
let probeFailure = 0;
|
|
7491
8534
|
for (const model of normalized.config.Models ?? []) {
|
|
7492
8535
|
const result = await probeModelAvailability(model);
|
|
7493
8536
|
if (result.kind === "success") {
|
|
7494
8537
|
deps.io.info(`\u6A21\u578B\u63A2\u6D4B\u6210\u529F\uFF1A${model.id}`);
|
|
8538
|
+
probeSuccess += 1;
|
|
7495
8539
|
continue;
|
|
7496
8540
|
}
|
|
7497
|
-
|
|
8541
|
+
const explanation = explainProbeFailure(result.category);
|
|
8542
|
+
probeFailure += 1;
|
|
8543
|
+
deps.io.error(`\u6A21\u578B\u63A2\u6D4B\u5931\u8D25\uFF1A${model.id} -> ${explanation.label}`);
|
|
8544
|
+
deps.io.info(`\u5931\u8D25\u8BF4\u660E\uFF1A${explanation.summary}`);
|
|
8545
|
+
deps.io.info(`\u5904\u7406\u5EFA\u8BAE\uFF1A${explanation.action}`);
|
|
8546
|
+
deps.io.info(`\u8FDC\u7AEF\u539F\u59CB\u4FE1\u606F\uFF1A${result.message}`);
|
|
7498
8547
|
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");
|
|
7499
8548
|
}
|
|
8549
|
+
deps.io.info(`\u6A21\u578B\u63A2\u6D4B\u5B8C\u6210\uFF1A\u6210\u529F ${probeSuccess}\uFF0C\u5931\u8D25 ${probeFailure}\u3002`);
|
|
7500
8550
|
deps.io.info("doctor \u8BCA\u65AD\u5B8C\u6210\u3002");
|
|
7501
8551
|
} finally {
|
|
7502
8552
|
deps.io.close?.();
|
|
@@ -7917,7 +8967,8 @@ async function runClaudeCode() {
|
|
|
7917
8967
|
shell: isWindows,
|
|
7918
8968
|
env: {
|
|
7919
8969
|
...process.env,
|
|
7920
|
-
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}
|
|
8970
|
+
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
|
|
8971
|
+
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "ctr-local-proxy"
|
|
7921
8972
|
}
|
|
7922
8973
|
});
|
|
7923
8974
|
claude.on("error", (error) => {
|