@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/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, DEFAULT_TRIGGER_CONFIG, DEFAULT_SMART_ROUTER_CONFIG, DEFAULT_GOVERNANCE_CONFIG;
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
- return item.api?.trim() || item.api_base_url?.trim() || "";
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
- const api = getModelApi(item);
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 "openrouter-like":
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 = validateModelRef(ref, validProviders, field);
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 (config.TriggerRouter) {
662
- if (config.TriggerRouter.llm_intent_recognition && !config.TriggerRouter.intent_model) {
663
- errors.push("TriggerRouter.intent_model is required when llm_intent_recognition is enabled");
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 (config.TriggerRouter.rules) {
669
- config.TriggerRouter.rules.forEach((rule, index) => {
670
- if (!rule.name) {
671
- errors.push(`TriggerRouter.rules[${index}].name is required`);
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 (!config.SmartRouter.candidates || config.SmartRouter.candidates.length < 2) {
693
- errors.push("SmartRouter.candidates must have at least 2 entries when SmartRouter is enabled");
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 = validateModelRef(candidate.model, validProviders, `SmartRouter.candidates[${index}].model`);
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
- config
1064
+ normalizedInput
837
1065
  );
838
- if (config.TriggerRouter) {
839
- normalizedConfig.TriggerRouter = deepMerge(DEFAULT_TRIGGER_CONFIG, config.TriggerRouter);
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 (config.Governance) {
842
- normalizedConfig.Governance = deepMerge(DEFAULT_GOVERNANCE_CONFIG, config.Governance);
1082
+ if (normalizedConfig.SmartRouter?.semantic) {
1083
+ normalizedConfig.SmartRouter.semantic = deepMerge(
1084
+ DEFAULT_GOVERNANCE_CONFIG.semantic,
1085
+ normalizedConfig.SmartRouter.semantic
1086
+ );
843
1087
  }
844
- if (config.Models) {
845
- normalizedConfig.Models = config.Models.map((item) => normalizeModelEndpointConfig(item));
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 [{ type: "image", source: item.source ?? item.image_url ?? item.url }];
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((item) => item?.type === "text" && typeof item.text === "string" ? [item.text] : []) : [];
1382
- const messages = Array.isArray(input3.messages) ? input3.messages.filter((item) => item?.role).map((item) => ({
1383
- role: item.role,
1384
- parts: normalizeContentParts(item.content)
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 (config.Governance?.enabled && config.Governance.sticky?.enabled && req.sessionId && req.body?.model) {
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", config?.Router?.default);
2928
- pushRef("TriggerRouter.intent_model", config?.TriggerRouter?.intent_model);
2929
- config?.TriggerRouter?.rules?.forEach((rule, index) => {
2930
- pushRef(`TriggerRouter.rules[${index}].model`, rule?.model);
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
- pushRef("SmartRouter.router_model", config?.SmartRouter?.router_model);
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("Governance.sticky.alignment.summarizer_model", config?.Governance?.sticky?.alignment?.summarizer_model);
2937
- config?.Governance?.cascade?.levels?.forEach((level, index) => {
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.semantic.classifier_model", config?.Governance?.semantic?.classifier_model);
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=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[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=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[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, SMART_ROUTER_PROMPT, SmartRouterSelector, smartRouterSelector;
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
- return SMART_ROUTER_PROMPT.replace("{request}", text).replace("{candidates}", this.buildCandidatesList(candidates));
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
- if (!config.enabled) {
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, config);
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, config.rules);
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: "trigger_rule"
5220
+ routeSource: "smart_rule"
4605
5221
  };
4606
5222
  }
4607
- if (governanceConfig?.enabled && governanceConfig.sticky?.enabled && req.sessionId) {
4608
- const fingerprint = createTaskFingerprint(text);
4609
- const sessionState = sessionStateStore.get(req.sessionId);
4610
- if (fingerprint && sessionState?.lastTaskFingerprint === fingerprint && (sessionState.preferredModel || sessionState.lastSuccessfulModel)) {
4611
- const stickyModel = sessionState.preferredModel || sessionState.lastSuccessfulModel;
4612
- if (stickyModel) {
4613
- log(`[StickyRouting] Reusing model "${stickyModel}" for session "${req.sessionId}"`);
4614
- return {
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
- governanceConfig.semantic,
5232
+ semanticConfig,
4629
5233
  port,
4630
5234
  void 0,
4631
5235
  apiKey,
4632
5236
  timeoutMs
4633
- ) : semanticRouter.analyze(text, governanceConfig.semantic);
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 = config.rules.find(
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
- return {
5255
+ const semanticSelection = {
4644
5256
  matched: true,
4645
5257
  rule: matchedRule,
4646
- model: appConfig ? resolveModelReference(appConfig, matchedRule.model) ?? matchedRule.model : matchedRule.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: "intent"
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: resolveModelReference(appConfig, smartRouterConfig.router_model) ?? smartRouterConfig.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: resolveModelReference(appConfig, candidate.model) ?? candidate.model
5275
+ model: this.resolveRouteModel(appConfig, candidate.model) ?? candidate.model
4663
5276
  }))
4664
5277
  } : smartRouterConfig;
4665
- const smartResult = await smartRouterSelector.selectModel(text, resolvedSmartRouterConfig, port, void 0, apiKey, timeoutMs);
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
- return {
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, config.rules);
5307
+ const matchedRule = intentDetector.findRuleByIntent(intentResult.intent, routingRules);
4686
5308
  if (matchedRule) {
4687
- return {
5309
+ const intentSelection = {
4688
5310
  matched: true,
4689
5311
  rule: matchedRule,
4690
- model: appConfig ? resolveModelReference(appConfig, matchedRule.model) ?? matchedRule.model : matchedRule.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: "intent"
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
- if (!config.enabled) {
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, config);
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 matchResult = this.matchRuleFromText(text, config.rules);
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.SmartRouter;
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.config?.enabled ?? false;
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.config.enabled) {
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 === "trigger_rule" && result.rule?.name) {
4857
- appendTraceReason(req.governanceTrace, `trigger_rule:${result.rule.name}`);
4858
- } else if (result.routeSource === "sticky") {
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, "sticky_routing");
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, "trigger_router:no_match");
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.config.enabled) {
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
- `[TriggerRouter] ${result.routeSource === "sticky" ? "Sticky routing selected" : result.rule ? `Matched rule "${result.rule.name}"` : "SmartRouter selected"} -> model "${result.model}" (confidence: ${result.confidence}, time: ${result.analysisTime}ms)`
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("[TriggerRouter] Error in trigger routing:", error);
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(`[TriggerRouter] Initialized, enabled: ${triggerRouter.isEnabled()}`);
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 bypassTriggerRouter = req.headers["x-ctr-smart-router"] === "1";
5354
- const triggerResult = bypassTriggerRouter ? { matched: false, confidence: 0, analysisTime: 0 } : await triggerRouter.route(req);
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 (!bypassTriggerRouter && triggerResult.matched && triggerResult.model) {
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 (config.Governance?.enabled && alignmentConfig?.enabled && previousModel && previousModel !== triggerResult.model && triggerResult.analyzedText) {
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
- `[TriggerRouter] Matched rule "${triggerResult.rule?.name}" -> "${triggerResult.model}"`
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
- throw new Error("service health check failed");
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
- if (apiBaseUrl?.includes("/v1/messages")) {
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
- if (input3.Router.background !== void 0) {
5815
- pushUnique(skippedFields, "Router.background");
5816
- }
5817
- if (input3.Router.think !== void 0) {
5818
- pushUnique(skippedFields, "Router.think");
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 hasLegacyDefaultRoute = typeof normalized.defaultRoute === "string" && normalized.defaultRoute.length > 0;
5881
- const defaultModelId = hasLegacyDefaultRoute ? (() => {
5882
- const [rawProviderName, rawModelName] = String(normalized.defaultRoute).split(",");
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) return fromLookup;
5887
- return models.find(
5888
- (item) => item.id === toModelId(providerName, modelName, 0) || item.id.startsWith(`${normalizeSegment(providerName)}_`) && item.model === modelName
5889
- )?.id;
5890
- })() : void 0;
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: defaultModelId ? { default: defaultModelId } : {}
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 toSuggestedModelId(providerName, model, preset) {
6753
- const presetDefinition = getProviderPreset(preset);
6754
- if (presetDefinition?.suggested_id) {
6755
- return presetDefinition.suggested_id;
7546
+ function toUniqueSuggestedModelId(preferredId, existingIds) {
7547
+ const normalizedPreferredId = preferredId.trim() || "model";
7548
+ if (!existingIds.includes(normalizedPreferredId)) {
7549
+ return normalizedPreferredId;
6756
7550
  }
6757
- const source = model || providerName || "model";
6758
- return source.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "model";
7551
+ let suffix = 2;
7552
+ while (existingIds.includes(`${normalizedPreferredId}_${suffix}`)) {
7553
+ suffix += 1;
7554
+ }
7555
+ return `${normalizedPreferredId}_${suffix}`;
6759
7556
  }
6760
- async function buildFreshConfig(io) {
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 modelId = await io.input("\u9ED8\u8BA4\u6A21\u578B ID", toSuggestedModelId(providerName, model, preset));
6778
- const capabilityMode = await io.choose("\u662F\u5426\u914D\u7F6E capability \u63D0\u793A", ["\u4FDD\u6301\u9ED8\u8BA4", "\u914D\u7F6E capability \u63D0\u793A"]);
6779
- const draft = buildMinimalConfig({
6780
- providers: [
6781
- {
6782
- name: providerName,
6783
- model_id: modelId,
6784
- api_key: apiKey,
6785
- models: [model],
6786
- preset,
6787
- api_base_url: apiBaseUrl
6788
- }
6789
- ],
6790
- defaultModel: modelId
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(" - TriggerRouter\uFF1A\u9002\u5408\u9AD8\u786E\u5B9A\u6027\u4EFB\u52A1\uFF0C\u628A\u67B6\u6784\u8BBE\u8BA1\u3001\u4EE3\u7801\u5BA1\u67E5\u7B49\u8BF7\u6C42\u56FA\u5B9A\u5207\u5230\u6307\u5B9A\u6A21\u578B");
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
- const trimmed = api?.trim();
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
- deps.io.error(`\u6A21\u578B\u63A2\u6D4B\u5931\u8D25\uFF1A${model.id} -> ${result.category} -> ${result.message}`);
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) => {