@peterwangze/claude-trigger-router 1.0.6 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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,14 +125,87 @@ 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 trimString(value) {
132
+ return typeof value === "string" ? value.trim() : "";
133
+ }
134
+ function inferInterfaceFromApiEndpoint(api, modelName) {
135
+ const trimmed = api?.trim().toLowerCase();
136
+ if (!trimmed) {
137
+ return void 0;
138
+ }
139
+ if (trimmed.includes("/chat/completions")) {
140
+ return "openai";
141
+ }
142
+ if (trimmed.includes("api.anthropic.com")) {
143
+ return "anthropic";
144
+ }
145
+ if (trimmed.includes("/messages")) {
146
+ return "anthropic";
147
+ }
148
+ const normalizedModelName = modelName?.trim().toLowerCase() || "";
149
+ if (normalizedModelName.startsWith("claude") && !trimmed.includes("/v1/chat/completions") && (trimmed.endsWith("/v1") || /^https?:\/\/[^/]+\/?$/.test(trimmed))) {
150
+ return "anthropic";
151
+ }
152
+ return trimmed.includes("/v1/messages") ? "anthropic" : "openai";
153
+ }
154
+ function normalizeEndpointPath(pathname, modelInterface) {
155
+ const trimmedPath = trimTrailingSlash(pathname || "");
156
+ const normalizedPath = trimmedPath || "";
157
+ const lowerPath = normalizedPath.toLowerCase();
158
+ if (modelInterface === "anthropic") {
159
+ if (lowerPath.endsWith("/v1/messages") || lowerPath.endsWith("/messages")) {
160
+ return normalizedPath || "/v1/messages";
161
+ }
162
+ if (lowerPath.endsWith("/v1")) {
163
+ return `${normalizedPath}/messages`;
164
+ }
165
+ if (!normalizedPath) {
166
+ return "/v1/messages";
167
+ }
168
+ return `${normalizedPath}/messages`;
169
+ }
170
+ if (lowerPath.endsWith("/chat/completions")) {
171
+ return normalizedPath || "/chat/completions";
172
+ }
173
+ if (lowerPath.endsWith("/v1")) {
174
+ return `${normalizedPath}/chat/completions`;
175
+ }
176
+ if (!normalizedPath) {
177
+ return "/v1/chat/completions";
178
+ }
179
+ return `${normalizedPath}/chat/completions`;
180
+ }
181
+ function normalizeApiEndpoint(api, explicitInterface) {
182
+ const trimmed = api?.trim() || "";
183
+ if (!trimmed) {
184
+ return "";
185
+ }
186
+ const modelInterface = explicitInterface ?? inferInterfaceFromApiEndpoint(trimmed) ?? "openai";
187
+ try {
188
+ const url = new URL(trimmed);
189
+ url.pathname = normalizeEndpointPath(url.pathname, modelInterface);
190
+ return url.toString();
191
+ } catch {
192
+ const [base, suffix = ""] = trimmed.split(/([?#].*)/, 2);
193
+ const normalizedBase = trimTrailingSlash(base);
194
+ const normalizedPath = normalizeEndpointPath(normalizedBase, modelInterface);
195
+ return `${normalizedPath}${suffix}`;
196
+ }
197
+ }
133
198
  function getModelApi(item) {
134
- return item.api?.trim() || item.api_base_url?.trim() || "";
199
+ const rawApi = trimString(item.api) || trimString(item.api_base_url) || "";
200
+ const explicitInterface = item.interface === "openai" || item.interface === "anthropic" ? item.interface : item.protocol === "openai" || item.protocol === "anthropic" ? item.protocol : void 0;
201
+ return normalizeApiEndpoint(rawApi, explicitInterface);
135
202
  }
136
203
  function getModelKey(item) {
137
- return item.key?.trim() || item.api_key?.trim() || "";
204
+ return trimString(item.key) || trimString(item.api_key) || "";
138
205
  }
139
206
  function getModelInterface(item) {
140
- return item.interface || item.protocol;
207
+ const modelInterface = item.interface || item.protocol;
208
+ return typeof modelInterface === "string" ? modelInterface : void 0;
141
209
  }
142
210
  function normalizeThinkingConfig(thinking) {
143
211
  if (!thinking) {
@@ -178,22 +246,23 @@ function toThinkingAlias(thinking) {
178
246
  };
179
247
  }
180
248
  function normalizeModelEndpointConfig(item) {
181
- const api = getModelApi(item);
182
- const key = getModelKey(item);
183
- const modelInterface = getModelInterface(item);
249
+ const source = item && typeof item === "object" && !Array.isArray(item) ? item : {};
250
+ const api = getModelApi(source);
251
+ const key = getModelKey(source);
252
+ const modelInterface = getModelInterface(source);
184
253
  return {
185
- ...item,
186
- id: item.id?.trim() ?? "",
254
+ ...source,
255
+ id: trimString(source.id),
187
256
  api,
188
257
  api_base_url: api,
189
258
  key,
190
259
  api_key: key,
191
260
  interface: modelInterface,
192
261
  protocol: modelInterface,
193
- model: item.model?.trim() ?? "",
194
- thinking: normalizeThinkingConfig(item.thinking),
195
- metadata: item.metadata ? {
196
- ...item.metadata
262
+ model: trimString(source.model),
263
+ thinking: normalizeThinkingConfig(source.thinking),
264
+ metadata: source.metadata ? {
265
+ ...source.metadata
197
266
  } : void 0
198
267
  };
199
268
  }
@@ -600,51 +669,163 @@ function validateKnownModelRef(ref, config, providers, fieldName) {
600
669
  }
601
670
  return validateModelRef(ref, providers, fieldName);
602
671
  }
672
+ function validateRoutingRule(rule, index, containerName, config, validProviders, errors) {
673
+ if (!rule.name) {
674
+ errors.push(`${containerName}[${index}].name is required`);
675
+ }
676
+ if (!rule.model) {
677
+ errors.push(`${containerName}[${index}].model is required`);
678
+ } else if (validProviders.length > 0) {
679
+ const err = validateKnownModelRef(rule.model, config, validProviders, `${containerName}[${index}].model`);
680
+ if (err) errors.push(err);
681
+ }
682
+ const hasSemanticOnlyMatch = Boolean(
683
+ rule.description || rule.semantic_profile?.prototype || rule.semantic_profile?.enabled
684
+ );
685
+ if ((!rule.patterns || rule.patterns.length === 0) && !hasSemanticOnlyMatch) {
686
+ errors.push(`${containerName}[${index}].patterns must be a non-empty array`);
687
+ }
688
+ }
689
+ function validateStickyRoutingConfig(sticky, config, validProviders, prefix, errors) {
690
+ if (!sticky?.enabled) {
691
+ return;
692
+ }
693
+ if ((sticky.session_ttl_ms ?? 0) <= 0) {
694
+ errors.push(`${prefix}.session_ttl_ms must be greater than 0 when sticky routing is enabled`);
695
+ }
696
+ const threshold = sticky.fingerprint_similarity_threshold;
697
+ if (threshold !== void 0 && (threshold < 0 || threshold > 1)) {
698
+ errors.push(`${prefix}.fingerprint_similarity_threshold must be between 0 and 1`);
699
+ }
700
+ if (sticky.alignment?.enabled) {
701
+ if (!sticky.alignment.summarizer_model) {
702
+ errors.push(`${prefix}.alignment.summarizer_model is required when alignment is enabled`);
703
+ } else if (!isKnownModelReference(config, sticky.alignment.summarizer_model)) {
704
+ const err = validateModelRef(
705
+ sticky.alignment.summarizer_model,
706
+ validProviders,
707
+ `${prefix}.alignment.summarizer_model`
708
+ );
709
+ if (err) errors.push(err);
710
+ }
711
+ if ((sticky.alignment.max_summary_tokens ?? 0) <= 0) {
712
+ errors.push(`${prefix}.alignment.max_summary_tokens must be greater than 0 when alignment is enabled`);
713
+ }
714
+ }
715
+ }
716
+ function validateSemanticRoutingConfig(semantic, config, validProviders, prefix, errors) {
717
+ if (!semantic?.enabled) {
718
+ return;
719
+ }
720
+ const threshold = semantic.threshold;
721
+ if (threshold !== void 0 && (threshold < 0 || threshold > 1)) {
722
+ errors.push(`${prefix}.threshold must be between 0 and 1`);
723
+ }
724
+ if (semantic.mode && !["embedding", "classifier"].includes(semantic.mode)) {
725
+ errors.push(`${prefix}.mode must be either "embedding" or "classifier"`);
726
+ }
727
+ if (semantic.mode === "classifier") {
728
+ if (!semantic.classifier_model) {
729
+ errors.push(`${prefix}.classifier_model is required when semantic mode is "classifier"`);
730
+ } else if (!isKnownModelReference(config, semantic.classifier_model)) {
731
+ const err = validateModelRef(semantic.classifier_model, validProviders, `${prefix}.classifier_model`);
732
+ if (err) errors.push(err);
733
+ }
734
+ }
735
+ }
736
+ function validateModelEndpointList(models, prefix, errors) {
737
+ const ids = /* @__PURE__ */ new Set();
738
+ models.forEach((item, index) => {
739
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
740
+ errors.push(`${prefix}[${index}] must be an object`);
741
+ return;
742
+ }
743
+ if (!item.id?.trim()) {
744
+ errors.push(`${prefix}[${index}].id is required`);
745
+ } else if (ids.has(item.id.trim())) {
746
+ errors.push(`${prefix}[${index}].id must be unique`);
747
+ } else {
748
+ ids.add(item.id.trim());
749
+ }
750
+ if (!getModelApi(item)) {
751
+ errors.push(`${prefix}[${index}].api is required`);
752
+ }
753
+ if (!getModelKey(item)) {
754
+ errors.push(`${prefix}[${index}].key is required`);
755
+ }
756
+ const modelInterface = getModelInterface(item);
757
+ if (!modelInterface) {
758
+ errors.push(`${prefix}[${index}].interface is required`);
759
+ } else if (!["openai", "anthropic"].includes(modelInterface)) {
760
+ errors.push(`${prefix}[${index}].interface must be either "openai" or "anthropic"`);
761
+ }
762
+ if (!item.model?.trim()) {
763
+ errors.push(`${prefix}[${index}].model is required`);
764
+ }
765
+ const thinking = item.thinking;
766
+ if (thinking?.mode && !["off", "auto", "on"].includes(thinking.mode)) {
767
+ errors.push(`${prefix}[${index}].thinking.mode must be one of "off", "auto", "on"`);
768
+ }
769
+ if (thinking?.effort && !["low", "medium", "high"].includes(thinking.effort)) {
770
+ errors.push(`${prefix}[${index}].thinking.effort must be one of "low", "medium", "high"`);
771
+ }
772
+ if (thinking?.budget_tokens !== void 0 && thinking.budget_tokens <= 0) {
773
+ errors.push(`${prefix}[${index}].thinking.budget_tokens must be greater than 0`);
774
+ }
775
+ });
776
+ }
777
+ function validateRegistrationUpstreamServices(services, errors) {
778
+ const ids = /* @__PURE__ */ new Set();
779
+ services.forEach((service, index) => {
780
+ if (!service || typeof service !== "object" || Array.isArray(service)) {
781
+ errors.push(`Registration.upstream_services[${index}] must be an object`);
782
+ return;
783
+ }
784
+ const id = typeof service.id === "string" ? service.id.trim() : "";
785
+ const baseUrl = typeof service.base_url === "string" ? service.base_url.trim() : "";
786
+ if (!id) {
787
+ errors.push(`Registration.upstream_services[${index}].id is required`);
788
+ } else if (ids.has(id)) {
789
+ errors.push(`Registration.upstream_services[${index}].id must be unique`);
790
+ } else {
791
+ ids.add(id);
792
+ }
793
+ if (!baseUrl) {
794
+ errors.push(`Registration.upstream_services[${index}].base_url is required`);
795
+ }
796
+ if (service.auth_token !== void 0 && typeof service.auth_token !== "string") {
797
+ errors.push(`Registration.upstream_services[${index}].auth_token must be a string when provided`);
798
+ }
799
+ });
800
+ }
801
+ function trimTrailingSlash2(value) {
802
+ return value.replace(/\/+$/, "");
803
+ }
804
+ function normalizeRegistrationUpstreamService(service) {
805
+ if (!service || typeof service !== "object" || Array.isArray(service)) {
806
+ return service;
807
+ }
808
+ const normalized = {
809
+ id: typeof service.id === "string" ? service.id.trim() : service.id,
810
+ base_url: typeof service.base_url === "string" ? trimTrailingSlash2(service.base_url.trim()) : service.base_url
811
+ };
812
+ if (service.auth_token !== void 0) {
813
+ normalized.auth_token = typeof service.auth_token === "string" ? service.auth_token.trim() : service.auth_token;
814
+ }
815
+ return normalized;
816
+ }
603
817
  function validateConfig(config) {
604
818
  const errors = [];
605
819
  if (config.Models !== void 0) {
606
820
  if (!Array.isArray(config.Models)) {
607
821
  errors.push("Models must be an array when provided");
608
822
  } else {
609
- const ids = /* @__PURE__ */ new Set();
610
- config.Models.forEach((item, index) => {
611
- if (!item.id?.trim()) {
612
- errors.push(`Models[${index}].id is required`);
613
- } else if (ids.has(item.id.trim())) {
614
- errors.push(`Models[${index}].id must be unique`);
615
- } else {
616
- ids.add(item.id.trim());
617
- }
618
- if (!getModelApi(item)) {
619
- errors.push(`Models[${index}].api is required`);
620
- }
621
- if (!getModelKey(item)) {
622
- errors.push(`Models[${index}].key is required`);
623
- }
624
- const modelInterface = getModelInterface(item);
625
- if (!modelInterface) {
626
- errors.push(`Models[${index}].interface is required`);
627
- } else if (!["openai", "anthropic"].includes(modelInterface)) {
628
- errors.push(`Models[${index}].interface must be either "openai" or "anthropic"`);
629
- }
630
- if (!item.model?.trim()) {
631
- errors.push(`Models[${index}].model is required`);
632
- }
633
- const thinking = item.thinking;
634
- if (thinking?.mode && !["off", "auto", "on"].includes(thinking.mode)) {
635
- errors.push(`Models[${index}].thinking.mode must be one of "off", "auto", "on"`);
636
- }
637
- if (thinking?.effort && !["low", "medium", "high"].includes(thinking.effort)) {
638
- errors.push(`Models[${index}].thinking.effort must be one of "low", "medium", "high"`);
639
- }
640
- if (thinking?.budget_tokens !== void 0 && thinking.budget_tokens <= 0) {
641
- errors.push(`Models[${index}].thinking.budget_tokens must be greater than 0`);
642
- }
643
- });
823
+ validateModelEndpointList(config.Models, "Models", errors);
644
824
  }
645
825
  }
646
826
  const hasModels = Array.isArray(config.Models) && config.Models.length > 0;
647
- if (!hasModels && (!config.Providers || !Array.isArray(config.Providers) || config.Providers.length === 0)) {
827
+ const remoteServiceEnabled = Boolean(config.Runtime?.remote_service?.enabled);
828
+ if (!remoteServiceEnabled && !hasModels && (!config.Providers || !Array.isArray(config.Providers) || config.Providers.length === 0)) {
648
829
  errors.push("Providers is required and must be a non-empty array");
649
830
  } else if (config.Providers && Array.isArray(config.Providers)) {
650
831
  config.Providers.forEach((provider, index) => {
@@ -659,10 +840,37 @@ function validateConfig(config) {
659
840
  }
660
841
  });
661
842
  }
662
- if (!config.Router?.default) {
843
+ if (!remoteServiceEnabled && !config.Router?.default) {
663
844
  errors.push("Router.default is required");
664
845
  }
846
+ if (config.Runtime?.mode && !["local", "server", "cloud"].includes(config.Runtime.mode)) {
847
+ errors.push('Runtime.mode must be one of "local", "server", or "cloud"');
848
+ }
849
+ if (config.Runtime?.remote_service?.enabled && !config.Runtime.remote_service.base_url?.trim()) {
850
+ errors.push("Runtime.remote_service.base_url is required when remote_service is enabled");
851
+ }
852
+ const registration = config.Registration;
853
+ if (registration?.nodes !== void 0) {
854
+ errors.push("Registration.nodes is not supported yet; use Registration.models or Registration.upstream_services");
855
+ }
856
+ if (registration?.node_id !== void 0) {
857
+ errors.push("Registration.node_id is not supported yet; use Registration.models or Registration.upstream_services");
858
+ }
859
+ if (registration?.cluster !== void 0 || registration?.cluster_id !== void 0) {
860
+ errors.push("Registration cluster fields are not supported yet; use Registration.models or Registration.upstream_services");
861
+ }
862
+ if (config.Registration?.models !== void 0 && !Array.isArray(config.Registration.models)) {
863
+ errors.push("Registration.models must be an array when provided");
864
+ } else if (Array.isArray(config.Registration?.models)) {
865
+ validateModelEndpointList(config.Registration.models, "Registration.models", errors);
866
+ }
867
+ if (config.Registration?.upstream_services !== void 0 && !Array.isArray(config.Registration.upstream_services)) {
868
+ errors.push("Registration.upstream_services must be an array when provided");
869
+ } else if (Array.isArray(config.Registration?.upstream_services)) {
870
+ validateRegistrationUpstreamServices(config.Registration.upstream_services, errors);
871
+ }
665
872
  const validProviders = config.Providers?.filter((p) => p.name && p.models?.length) ?? [];
873
+ const runtimeSmartRouter = deriveRuntimeSmartRouterConfig(config, config);
666
874
  if (validProviders.length > 0) {
667
875
  const router2 = config.Router;
668
876
  if (router2) {
@@ -682,44 +890,18 @@ function validateConfig(config) {
682
890
  }
683
891
  }
684
892
  }
685
- if (config.TriggerRouter) {
686
- if (config.TriggerRouter.llm_intent_recognition && !config.TriggerRouter.intent_model) {
687
- errors.push("TriggerRouter.intent_model is required when llm_intent_recognition is enabled");
688
- } else if (config.TriggerRouter.intent_model && validProviders.length > 0) {
689
- const err = validateKnownModelRef(config.TriggerRouter.intent_model, config, validProviders, "TriggerRouter.intent_model");
893
+ if (runtimeSmartRouter?.enabled) {
894
+ if (runtimeSmartRouter.router_model && validProviders.length > 0) {
895
+ const err = validateKnownModelRef(runtimeSmartRouter.router_model, config, validProviders, "SmartRouter.router_model");
690
896
  if (err) errors.push(err);
691
897
  }
692
- if (config.TriggerRouter.rules) {
693
- config.TriggerRouter.rules.forEach((rule, index) => {
694
- if (!rule.name) {
695
- errors.push(`TriggerRouter.rules[${index}].name is required`);
696
- }
697
- if (!rule.model) {
698
- errors.push(`TriggerRouter.rules[${index}].model is required`);
699
- } else if (validProviders.length > 0) {
700
- const err = validateKnownModelRef(rule.model, config, validProviders, `TriggerRouter.rules[${index}].model`);
701
- if (err) errors.push(err);
702
- }
703
- const hasSemanticOnlyMatch = Boolean(
704
- rule.description || rule.semantic_profile?.prototype || rule.semantic_profile?.enabled
705
- );
706
- if ((!rule.patterns || rule.patterns.length === 0) && !hasSemanticOnlyMatch) {
707
- errors.push(`TriggerRouter.rules[${index}].patterns must be a non-empty array`);
708
- }
709
- });
710
- }
711
- }
712
- if (config.SmartRouter?.enabled) {
713
- if (!config.SmartRouter.router_model) {
714
- errors.push("SmartRouter.router_model is required when SmartRouter is enabled");
715
- } else if (validProviders.length > 0) {
716
- const err = validateKnownModelRef(config.SmartRouter.router_model, config, validProviders, "SmartRouter.router_model");
717
- if (err) errors.push(err);
898
+ if (runtimeSmartRouter.router_model) {
899
+ if (!runtimeSmartRouter.candidates || runtimeSmartRouter.candidates.length < 2) {
900
+ errors.push("SmartRouter.candidates must have at least 2 entries when SmartRouter.router_model is configured");
901
+ }
718
902
  }
719
- if (!config.SmartRouter.candidates || config.SmartRouter.candidates.length < 2) {
720
- errors.push("SmartRouter.candidates must have at least 2 entries when SmartRouter is enabled");
721
- } else {
722
- config.SmartRouter.candidates.forEach((candidate, index) => {
903
+ if (runtimeSmartRouter.candidates && runtimeSmartRouter.candidates.length > 0) {
904
+ runtimeSmartRouter.candidates.forEach((candidate, index) => {
723
905
  if (!candidate.model) {
724
906
  errors.push(`SmartRouter.candidates[${index}].model is required`);
725
907
  } else if (validProviders.length > 0) {
@@ -731,33 +913,15 @@ function validateConfig(config) {
731
913
  }
732
914
  });
733
915
  }
916
+ if (runtimeSmartRouter.rules) {
917
+ runtimeSmartRouter.rules.forEach((rule, index) => {
918
+ validateRoutingRule(rule, index, "SmartRouter.rules", config, validProviders, errors);
919
+ });
920
+ }
921
+ validateStickyRoutingConfig(runtimeSmartRouter.sticky, config, validProviders, "SmartRouter.sticky", errors);
922
+ validateSemanticRoutingConfig(runtimeSmartRouter.semantic, config, validProviders, "SmartRouter.semantic", errors);
734
923
  }
735
924
  if (config.Governance?.enabled) {
736
- const sticky = config.Governance.sticky;
737
- if (sticky?.enabled) {
738
- if ((sticky.session_ttl_ms ?? 0) <= 0) {
739
- errors.push("Governance.sticky.session_ttl_ms must be greater than 0 when sticky routing is enabled");
740
- }
741
- const threshold = sticky.fingerprint_similarity_threshold;
742
- if (threshold !== void 0 && (threshold < 0 || threshold > 1)) {
743
- errors.push("Governance.sticky.fingerprint_similarity_threshold must be between 0 and 1");
744
- }
745
- if (sticky.alignment?.enabled) {
746
- if (!sticky.alignment.summarizer_model) {
747
- errors.push("Governance.sticky.alignment.summarizer_model is required when alignment is enabled");
748
- } else if (!isKnownModelReference(config, sticky.alignment.summarizer_model)) {
749
- const err = validateModelRef(
750
- sticky.alignment.summarizer_model,
751
- validProviders,
752
- "Governance.sticky.alignment.summarizer_model"
753
- );
754
- if (err) errors.push(err);
755
- }
756
- if ((sticky.alignment.max_summary_tokens ?? 0) <= 0) {
757
- errors.push("Governance.sticky.alignment.max_summary_tokens must be greater than 0 when alignment is enabled");
758
- }
759
- }
760
- }
761
925
  const cascade = config.Governance.cascade;
762
926
  if (cascade?.enabled) {
763
927
  if ((cascade.max_attempts ?? 0) < 1) {
@@ -778,24 +942,6 @@ function validateConfig(config) {
778
942
  }
779
943
  });
780
944
  }
781
- const semantic = config.Governance.semantic;
782
- if (semantic?.enabled) {
783
- const threshold = semantic.threshold;
784
- if (threshold !== void 0 && (threshold < 0 || threshold > 1)) {
785
- errors.push("Governance.semantic.threshold must be between 0 and 1");
786
- }
787
- if (semantic.mode && !["embedding", "classifier"].includes(semantic.mode)) {
788
- errors.push('Governance.semantic.mode must be either "embedding" or "classifier"');
789
- }
790
- if (semantic.mode === "classifier") {
791
- if (!semantic.classifier_model) {
792
- errors.push('Governance.semantic.classifier_model is required when semantic mode is "classifier"');
793
- } else if (!isKnownModelReference(config, semantic.classifier_model)) {
794
- const err = validateModelRef(semantic.classifier_model, validProviders, "Governance.semantic.classifier_model");
795
- if (err) errors.push(err);
796
- }
797
- }
798
- }
799
945
  const shadow = config.Governance.shadow;
800
946
  if (shadow?.enabled) {
801
947
  const sampleRate = shadow.sample_rate;
@@ -866,86 +1012,123 @@ function normalizeUnifiedRouterInput(config) {
866
1012
  ...config.Router ?? {}
867
1013
  }
868
1014
  };
869
- if (Array.isArray(routes) && routes.length > 0) {
870
- const semanticExplicitlyEnabled = defaults?.semantic?.enabled;
871
- nextConfig.TriggerRouter = {
872
- ...config.TriggerRouter ?? DEFAULT_TRIGGER_CONFIG,
873
- enabled: true,
874
- rules: routes.map((route) => ({
875
- name: route.name,
876
- priority: route.priority ?? 0,
877
- enabled: route.enabled ?? true,
878
- model: route.model,
879
- description: route.description,
880
- semantic_profile: route.match?.semantic || route.match?.semantic_profile ? {
881
- enabled: route.match?.semantic ?? true,
882
- prototype: route.match?.semantic_profile?.prototype,
883
- threshold: route.match?.semantic_profile?.threshold
884
- } : void 0,
885
- patterns: [
886
- ...Array.isArray(route.match?.keywords) && route.match?.keywords.length ? [{
887
- type: "exact",
888
- keywords: route.match?.keywords
889
- }] : [],
890
- ...typeof route.match?.regex === "string" && route.match.regex.trim().length ? [{
891
- type: "regex",
892
- pattern: route.match.regex
893
- }] : []
894
- ]
895
- }))
896
- };
897
- const semanticPrototypes = Object.fromEntries(
898
- routes.filter((route) => route.match?.semantic || route.match?.semantic_profile?.prototype || route.description).map((route) => [
899
- route.name,
900
- route.match?.semantic_profile?.prototype ?? route.description ?? ""
901
- ]).filter(([, prototype]) => typeof prototype === "string" && prototype.trim().length > 0)
902
- );
903
- if (Object.keys(semanticPrototypes).length > 0 || defaults?.semantic) {
904
- nextConfig.Governance = {
905
- ...config.Governance ?? DEFAULT_GOVERNANCE_CONFIG,
906
- enabled: config.Governance?.enabled ?? (semanticExplicitlyEnabled !== void 0 ? semanticExplicitlyEnabled : Object.keys(semanticPrototypes).length > 0),
907
- semantic: {
908
- ...config.Governance?.semantic ?? {},
909
- ...defaults?.semantic ?? {},
910
- enabled: defaults?.semantic?.enabled ?? true,
911
- prototypes: {
912
- ...config.Governance?.semantic?.prototypes ?? {},
913
- ...semanticPrototypes
914
- }
915
- }
916
- };
917
- }
918
- }
919
- if (decision) {
1015
+ const normalizedRules = Array.isArray(routes) && routes.length > 0 ? routes.map((route) => ({
1016
+ name: route.name,
1017
+ priority: route.priority ?? 0,
1018
+ enabled: route.enabled ?? true,
1019
+ model: route.model,
1020
+ description: route.description,
1021
+ semantic_profile: route.match?.semantic || route.match?.semantic_profile ? {
1022
+ enabled: route.match?.semantic ?? true,
1023
+ prototype: route.match?.semantic_profile?.prototype,
1024
+ threshold: route.match?.semantic_profile?.threshold
1025
+ } : void 0,
1026
+ patterns: [
1027
+ ...Array.isArray(route.match?.keywords) && route.match?.keywords.length ? [{
1028
+ type: "exact",
1029
+ keywords: route.match?.keywords
1030
+ }] : [],
1031
+ ...typeof route.match?.regex === "string" && route.match.regex.trim().length ? [{
1032
+ type: "regex",
1033
+ pattern: route.match.regex
1034
+ }] : []
1035
+ ]
1036
+ })) : [];
1037
+ const semanticPrototypes = Object.fromEntries(
1038
+ normalizedRules.filter((rule) => rule.semantic_profile?.enabled !== false && (rule.semantic_profile?.prototype || rule.description)).map((rule) => [
1039
+ rule.name,
1040
+ rule.semantic_profile?.prototype ?? rule.description ?? ""
1041
+ ]).filter(([, prototype]) => typeof prototype === "string" && prototype.trim().length > 0)
1042
+ );
1043
+ if (decision || normalizedRules.length > 0 || defaults?.sticky || defaults?.semantic || config.SmartRouter) {
920
1044
  nextConfig.SmartRouter = {
921
1045
  ...config.SmartRouter ?? DEFAULT_SMART_ROUTER_CONFIG,
922
- enabled: decision.smart_fallback ?? true,
923
- router_model: decision.router_model ?? config.SmartRouter?.router_model ?? "",
924
- candidates: decision.candidates ?? config.SmartRouter?.candidates ?? [],
925
- cache_ttl: decision.cache_ttl ?? config.SmartRouter?.cache_ttl,
926
- max_tokens: decision.max_tokens ?? config.SmartRouter?.max_tokens,
927
- fallback: decision.fallback ?? config.SmartRouter?.fallback,
928
- router_hint: decision.router_hint ?? config.SmartRouter?.router_hint
929
- };
930
- }
931
- if (defaults?.sticky || defaults?.semantic) {
932
- nextConfig.Governance = {
933
- ...nextConfig.Governance ?? config.Governance ?? DEFAULT_GOVERNANCE_CONFIG,
934
- enabled: config.Governance?.enabled ?? Boolean(
935
- defaults?.sticky?.enabled || defaults?.semantic?.enabled || nextConfig.Governance?.semantic?.enabled
936
- ),
1046
+ enabled: decision?.smart_fallback ?? config.SmartRouter?.enabled ?? true,
1047
+ router_model: decision?.router_model ?? config.SmartRouter?.router_model ?? "",
1048
+ candidates: decision?.candidates ?? config.SmartRouter?.candidates ?? [],
1049
+ cache_ttl: decision?.cache_ttl ?? config.SmartRouter?.cache_ttl,
1050
+ max_tokens: decision?.max_tokens ?? config.SmartRouter?.max_tokens,
1051
+ fallback: decision?.fallback ?? config.SmartRouter?.fallback,
1052
+ router_hint: decision?.router_hint ?? config.SmartRouter?.router_hint,
1053
+ rules: normalizedRules.length > 0 ? normalizedRules : config.SmartRouter?.rules,
1054
+ semantic: Object.keys(semanticPrototypes).length > 0 || defaults?.semantic || config.SmartRouter?.semantic ? {
1055
+ ...config.SmartRouter?.semantic ?? config.Governance?.semantic ?? {},
1056
+ ...defaults?.semantic ?? {},
1057
+ prototypes: {
1058
+ ...config.Governance?.semantic?.prototypes ?? {},
1059
+ ...config.SmartRouter?.semantic?.prototypes ?? {},
1060
+ ...semanticPrototypes
1061
+ }
1062
+ } : config.SmartRouter?.semantic,
937
1063
  sticky: defaults?.sticky ? {
938
- ...config.Governance?.sticky ?? {},
1064
+ ...config.SmartRouter?.sticky ?? config.Governance?.sticky ?? {},
939
1065
  ...defaults.sticky
940
- } : config.Governance?.sticky,
941
- semantic: defaults?.semantic ? {
942
- ...nextConfig.Governance?.semantic ?? config.Governance?.semantic ?? {},
943
- ...defaults.semantic
944
- } : nextConfig.Governance?.semantic ?? config.Governance?.semantic
1066
+ } : config.SmartRouter?.sticky ?? config.Governance?.sticky
945
1067
  };
946
1068
  }
947
1069
  return nextConfig;
948
1070
  }
1071
+ function deriveRuntimeSmartRouterConfig(config, source) {
1072
+ const smartRouterInput = source?.SmartRouter ?? config.SmartRouter;
1073
+ const baseSmartRouterConfig = smartRouterInput ?? DEFAULT_SMART_ROUTER_CONFIG;
1074
+ const legacyIntentEnabled = Boolean(config.TriggerRouter?.llm_intent_recognition);
1075
+ const legacyIntentModel = config.TriggerRouter?.intent_model;
1076
+ const legacySemanticPrototypes = Object.fromEntries(
1077
+ (config.TriggerRouter?.rules ?? []).filter((rule) => rule.enabled !== false && rule.description).map((rule) => [rule.name, rule.description])
1078
+ );
1079
+ const hasExplicitSmartRouterConfig = Boolean(
1080
+ source?.SmartRouter !== void 0 || baseSmartRouterConfig.enabled || baseSmartRouterConfig.router_model || baseSmartRouterConfig.rules?.length || baseSmartRouterConfig.candidates?.length || baseSmartRouterConfig.semantic || baseSmartRouterConfig.sticky
1081
+ );
1082
+ const defaultSummarizerModel = baseSmartRouterConfig.router_model || config.Router?.default || legacyIntentModel || "";
1083
+ const derivedSemantic = deepMerge(
1084
+ DEFAULT_GOVERNANCE_CONFIG.semantic,
1085
+ baseSmartRouterConfig.semantic ?? (legacyIntentEnabled || config.Governance?.semantic ? {
1086
+ ...config.Governance?.semantic ?? {},
1087
+ ...legacyIntentEnabled ? {
1088
+ enabled: true,
1089
+ mode: "classifier",
1090
+ classifier_model: legacyIntentModel,
1091
+ prototypes: {
1092
+ ...config.Governance?.semantic?.prototypes ?? {},
1093
+ ...legacySemanticPrototypes
1094
+ }
1095
+ } : {}
1096
+ } : {})
1097
+ );
1098
+ const derivedSticky = deepMerge(
1099
+ DEFAULT_GOVERNANCE_CONFIG.sticky,
1100
+ baseSmartRouterConfig.sticky ?? config.Governance?.sticky ?? {}
1101
+ );
1102
+ const smartRouterEnabled = hasExplicitSmartRouterConfig ? baseSmartRouterConfig.enabled : Boolean(config.TriggerRouter?.enabled);
1103
+ const hasExplicitSemanticToggle = Boolean(
1104
+ baseSmartRouterConfig.semantic || config.Governance?.semantic || legacyIntentEnabled
1105
+ );
1106
+ const hasExplicitStickyToggle = Boolean(
1107
+ baseSmartRouterConfig.sticky || config.Governance?.sticky
1108
+ );
1109
+ const semantic = smartRouterEnabled ? {
1110
+ ...derivedSemantic,
1111
+ enabled: hasExplicitSemanticToggle ? baseSmartRouterConfig.semantic?.enabled ?? derivedSemantic.enabled : true,
1112
+ threshold: hasExplicitSemanticToggle ? derivedSemantic.threshold : 0.2
1113
+ } : derivedSemantic;
1114
+ const sticky = smartRouterEnabled ? {
1115
+ ...derivedSticky,
1116
+ enabled: hasExplicitStickyToggle ? baseSmartRouterConfig.sticky?.enabled ?? derivedSticky.enabled : true,
1117
+ alignment: {
1118
+ ...derivedSticky.alignment,
1119
+ enabled: hasExplicitStickyToggle && (baseSmartRouterConfig.sticky?.alignment || config.Governance?.sticky?.alignment) ? baseSmartRouterConfig.sticky?.alignment?.enabled ?? derivedSticky.alignment?.enabled : true,
1120
+ summarizer_model: baseSmartRouterConfig.sticky?.alignment?.summarizer_model || derivedSticky.alignment?.summarizer_model || defaultSummarizerModel
1121
+ }
1122
+ } : derivedSticky;
1123
+ return {
1124
+ ...baseSmartRouterConfig,
1125
+ enabled: smartRouterEnabled,
1126
+ analysis_scope: baseSmartRouterConfig.analysis_scope ?? config.TriggerRouter?.analysis_scope ?? "last_message",
1127
+ rules: baseSmartRouterConfig.rules?.length ? baseSmartRouterConfig.rules : config.TriggerRouter?.rules ?? [],
1128
+ semantic,
1129
+ sticky
1130
+ };
1131
+ }
949
1132
  function normalizeAndValidateConfig(config = {}) {
950
1133
  const normalizedInput = normalizeUnifiedRouterInput(config);
951
1134
  const normalizedConfig = deepMerge(
@@ -959,15 +1142,53 @@ function normalizeAndValidateConfig(config = {}) {
959
1142
  },
960
1143
  normalizedInput
961
1144
  );
962
- if (normalizedInput.TriggerRouter) {
963
- normalizedConfig.TriggerRouter = deepMerge(DEFAULT_TRIGGER_CONFIG, normalizedInput.TriggerRouter);
1145
+ if (normalizedInput.Runtime) {
1146
+ normalizedConfig.Runtime = deepMerge(
1147
+ DEFAULT_RUNTIME_CONFIG,
1148
+ normalizedInput.Runtime
1149
+ );
1150
+ normalizedConfig.Runtime.remote_service = deepMerge(
1151
+ DEFAULT_RUNTIME_CONFIG.remote_service ?? {},
1152
+ normalizedInput.Runtime.remote_service ?? {}
1153
+ );
1154
+ }
1155
+ if (normalizedInput.Registration) {
1156
+ normalizedConfig.Registration = deepMerge(
1157
+ DEFAULT_REGISTRATION_CONFIG,
1158
+ normalizedInput.Registration
1159
+ );
964
1160
  }
965
1161
  if (normalizedInput.Governance) {
966
1162
  normalizedConfig.Governance = deepMerge(DEFAULT_GOVERNANCE_CONFIG, normalizedInput.Governance);
967
1163
  }
1164
+ normalizedConfig.SmartRouter = deepMerge(
1165
+ DEFAULT_SMART_ROUTER_CONFIG,
1166
+ deriveRuntimeSmartRouterConfig(normalizedConfig, normalizedInput)
1167
+ );
1168
+ if (normalizedInput.TriggerRouter || normalizedInput.SmartRouter || normalizedInput.Router?.routes || normalizedInput.Router?.decision || normalizedInput.Router?.defaults) {
1169
+ delete normalizedConfig.TriggerRouter;
1170
+ }
1171
+ if (normalizedConfig.SmartRouter?.sticky) {
1172
+ normalizedConfig.SmartRouter.sticky = deepMerge(
1173
+ DEFAULT_GOVERNANCE_CONFIG.sticky,
1174
+ normalizedConfig.SmartRouter.sticky
1175
+ );
1176
+ }
1177
+ if (normalizedConfig.SmartRouter?.semantic) {
1178
+ normalizedConfig.SmartRouter.semantic = deepMerge(
1179
+ DEFAULT_GOVERNANCE_CONFIG.semantic,
1180
+ normalizedConfig.SmartRouter.semantic
1181
+ );
1182
+ }
968
1183
  if (normalizedInput.Models) {
969
1184
  normalizedConfig.Models = normalizedInput.Models.map((item) => normalizeModelEndpointConfig(item));
970
1185
  }
1186
+ if (normalizedInput.Registration?.models && normalizedConfig.Registration) {
1187
+ normalizedConfig.Registration.models = normalizedInput.Registration.models.map((item) => normalizeModelEndpointConfig(item));
1188
+ }
1189
+ if (normalizedInput.Registration?.upstream_services && normalizedConfig.Registration) {
1190
+ normalizedConfig.Registration.upstream_services = normalizedInput.Registration.upstream_services.map((item) => normalizeRegistrationUpstreamService(item));
1191
+ }
971
1192
  return {
972
1193
  config: normalizedConfig,
973
1194
  errors: validateConfig(normalizedConfig),
@@ -1060,7 +1281,7 @@ async function backupConfigFile() {
1060
1281
  return null;
1061
1282
  }
1062
1283
  }
1063
- var import_fs, import_promises, import_path2, yaml;
1284
+ var import_fs, import_promises, import_path2, yaml, DEFAULT_RUNTIME_CONFIG, DEFAULT_REGISTRATION_CONFIG;
1064
1285
  var init_config = __esm({
1065
1286
  "src/utils/config.ts"() {
1066
1287
  "use strict";
@@ -1072,6 +1293,19 @@ var init_config = __esm({
1072
1293
  init_compile();
1073
1294
  init_schema();
1074
1295
  init_log();
1296
+ DEFAULT_RUNTIME_CONFIG = {
1297
+ mode: "local",
1298
+ remote_service: {
1299
+ enabled: false,
1300
+ base_url: "",
1301
+ auth_token: ""
1302
+ }
1303
+ };
1304
+ DEFAULT_REGISTRATION_CONFIG = {
1305
+ enabled: false,
1306
+ models: [],
1307
+ upstream_services: []
1308
+ };
1075
1309
  }
1076
1310
  });
1077
1311
 
@@ -1105,6 +1339,71 @@ async function probeServiceHealth(port, timeoutMs = 500) {
1105
1339
  return false;
1106
1340
  }
1107
1341
  }
1342
+ async function probeRemoteServiceStatus(remoteService, timeoutMs = 800, fetchFn = fetch) {
1343
+ const enabled = Boolean(remoteService?.enabled);
1344
+ const baseUrl = remoteService?.base_url?.trim() ?? "";
1345
+ const normalizedBaseUrl = baseUrl.replace(/\/+$/, "");
1346
+ if (!enabled) {
1347
+ return {
1348
+ enabled: false,
1349
+ configured: false,
1350
+ reachable: false,
1351
+ ready: false,
1352
+ baseUrl: normalizedBaseUrl
1353
+ };
1354
+ }
1355
+ if (!baseUrl) {
1356
+ return {
1357
+ enabled: true,
1358
+ configured: false,
1359
+ reachable: false,
1360
+ ready: false,
1361
+ baseUrl: normalizedBaseUrl,
1362
+ error: "Runtime.remote_service.base_url is required when remote_service is enabled"
1363
+ };
1364
+ }
1365
+ try {
1366
+ const headers = {};
1367
+ if (remoteService?.auth_token) {
1368
+ headers.Authorization = `Bearer ${remoteService.auth_token}`;
1369
+ }
1370
+ const res = await fetchFn(`${normalizedBaseUrl}${SERVICE_INFO_PATH}`, {
1371
+ headers,
1372
+ signal: AbortSignal.timeout(timeoutMs)
1373
+ });
1374
+ if (!res.ok) {
1375
+ return {
1376
+ enabled: true,
1377
+ configured: true,
1378
+ reachable: false,
1379
+ ready: false,
1380
+ baseUrl: normalizedBaseUrl,
1381
+ error: `HTTP ${res.status}`
1382
+ };
1383
+ }
1384
+ const payload = await res.json();
1385
+ const info = payload && typeof payload === "object" ? payload : {};
1386
+ return {
1387
+ enabled: true,
1388
+ configured: true,
1389
+ reachable: true,
1390
+ ready: isExpectedServiceHealth(payload),
1391
+ baseUrl: normalizedBaseUrl,
1392
+ service: info.service,
1393
+ runtimeMode: info.runtimeMode,
1394
+ remoteEnabled: info.remoteEnabled
1395
+ };
1396
+ } catch (error) {
1397
+ return {
1398
+ enabled: true,
1399
+ configured: true,
1400
+ reachable: false,
1401
+ ready: false,
1402
+ baseUrl: normalizedBaseUrl,
1403
+ error: error?.message || String(error)
1404
+ };
1405
+ }
1406
+ }
1108
1407
  async function isTcpPortOccupied(port, timeoutMs = 500) {
1109
1408
  return new Promise((resolve) => {
1110
1409
  const socket = new import_net.Socket();
@@ -1140,13 +1439,14 @@ async function waitForService(port, timeoutMs = 5e3) {
1140
1439
  }
1141
1440
  return false;
1142
1441
  }
1143
- var import_net, SERVICE_NAME, SERVICE_HEALTH_PATH;
1442
+ var import_net, SERVICE_NAME, SERVICE_HEALTH_PATH, SERVICE_INFO_PATH;
1144
1443
  var init_service_health = __esm({
1145
1444
  "src/service-health.ts"() {
1146
1445
  "use strict";
1147
1446
  import_net = require("net");
1148
1447
  SERVICE_NAME = "claude-trigger-router";
1149
1448
  SERVICE_HEALTH_PATH = "/api/health";
1449
+ SERVICE_INFO_PATH = "/api/service-info";
1150
1450
  }
1151
1451
  });
1152
1452
 
@@ -2212,6 +2512,10 @@ async function applyResponseGovernance({
2212
2512
  deps
2213
2513
  }) {
2214
2514
  let nextPayload = payload;
2515
+ const effectiveStickyConfig = config.SmartRouter?.sticky ? {
2516
+ ...config.Governance?.sticky ?? {},
2517
+ ...config.SmartRouter.sticky
2518
+ } : config.Governance?.sticky;
2215
2519
  const resolvedCascadeConfig = config.Governance?.cascade ? {
2216
2520
  ...config.Governance.cascade,
2217
2521
  levels: config.Governance.cascade.levels?.map((level) => ({
@@ -2259,7 +2563,7 @@ async function applyResponseGovernance({
2259
2563
  }
2260
2564
  }
2261
2565
  }
2262
- if (config.Governance?.enabled && config.Governance.sticky?.enabled && req.sessionId && req.body?.model) {
2566
+ if (effectiveStickyConfig?.enabled && req.sessionId && req.body?.model) {
2263
2567
  const fingerprint = createTaskFingerprint(req.triggerResult?.analyzedText);
2264
2568
  if (fingerprint) {
2265
2569
  sessionStateStore.put(req.sessionId, {
@@ -2996,6 +3300,131 @@ var init_governance = __esm({
2996
3300
  }
2997
3301
  });
2998
3302
 
3303
+ // src/utils/validation-contract.ts
3304
+ function inferPath(message) {
3305
+ const directPath = message.match(/^([A-Za-z][A-Za-z0-9]*(?:\[\d+\])?(?:\.[A-Za-z_][A-Za-z0-9_]*)*)\b/);
3306
+ if (directPath) {
3307
+ return directPath[1];
3308
+ }
3309
+ return void 0;
3310
+ }
3311
+ function inferSchemaAction(message) {
3312
+ if (message === "Router.default is required") {
3313
+ return "Set Router.default to one of the configured Models ids.";
3314
+ }
3315
+ if (message === "Providers is required and must be a non-empty array") {
3316
+ return "Add at least one model through Models or provide a legacy Providers entry.";
3317
+ }
3318
+ if (/^(Providers\[\d+\]\.api_key|Models\[\d+\]\.key) is required$/.test(message)) {
3319
+ return "Fill in the API key for the referenced provider or model.";
3320
+ }
3321
+ if (/^(Providers\[\d+\]\.api_base_url|Models\[\d+\]\.api) is required$/.test(message)) {
3322
+ return "Fill in the API endpoint URL for the referenced provider or model.";
3323
+ }
3324
+ if (/^Models\[\d+\]\.model is required$/.test(message)) {
3325
+ return "Set the upstream model name used by this Models entry.";
3326
+ }
3327
+ if (/^Models\[\d+\]\.id is required$/.test(message)) {
3328
+ return "Give this Models entry a stable model id, then reference it from Router.default.";
3329
+ }
3330
+ if (/is not a known model id/.test(message) || /references missing model/.test(message)) {
3331
+ return "Change the reference to an existing Models id, or add the missing model entry.";
3332
+ }
3333
+ return "Review the field, then repair the config before saving or starting the service.";
3334
+ }
3335
+ function inferCapabilityAction(code, message) {
3336
+ if (code === "thinking_ignored" || /^Models\[\d+\]\.thinking is configured/.test(message)) {
3337
+ return "Remove the thinking setting for this model, or change metadata.supports_reasoning to true only if the endpoint supports reasoning.";
3338
+ }
3339
+ if (code === "tools_text_fallback" || /^Models\[\d+\]\.metadata\.supports_tools/.test(message)) {
3340
+ return "Accept text fallback behavior, or set metadata.supports_tools to true only for a tool-capable endpoint.";
3341
+ }
3342
+ if (code === "images_text_fallback" || /^Models\[\d+\]\.metadata\.supports_images/.test(message)) {
3343
+ return "Accept text fallback behavior, or set metadata.supports_images to true only for an image-capable endpoint.";
3344
+ }
3345
+ return "Review the capability hint and decide whether the fallback behavior is acceptable.";
3346
+ }
3347
+ function inferCapabilitySeverity(message) {
3348
+ if (/^Models\[\d+\]\.metadata\.supports_tools disables tools/.test(message) || /^Models\[\d+\]\.metadata\.supports_images disables image input/.test(message)) {
3349
+ return "info";
3350
+ }
3351
+ return "warning";
3352
+ }
3353
+ function buildValidationIssueReport(input3) {
3354
+ const issues = [];
3355
+ const capabilityEntries = input3.capabilityWarnings?.entries ?? [];
3356
+ const capabilityKeys = new Set(
3357
+ capabilityEntries.map((warning) => `${warning.path ?? ""}
3358
+ ${warning.message}`)
3359
+ );
3360
+ for (const message of input3.errors ?? []) {
3361
+ if (!message) {
3362
+ continue;
3363
+ }
3364
+ issues.push({
3365
+ severity: "error",
3366
+ source: "schema",
3367
+ message,
3368
+ path: inferPath(message),
3369
+ action: inferSchemaAction(message)
3370
+ });
3371
+ }
3372
+ for (const message of input3.warnings ?? []) {
3373
+ if (!message) {
3374
+ continue;
3375
+ }
3376
+ const path = inferPath(message);
3377
+ if (capabilityKeys.has(`${path ?? ""}
3378
+ ${message}`)) {
3379
+ continue;
3380
+ }
3381
+ issues.push({
3382
+ severity: inferCapabilitySeverity(message),
3383
+ source: "capability",
3384
+ message,
3385
+ path,
3386
+ action: inferCapabilityAction(void 0, message)
3387
+ });
3388
+ }
3389
+ for (const warning of capabilityEntries) {
3390
+ if (!warning?.message) {
3391
+ continue;
3392
+ }
3393
+ if (issues.some((issue) => issue.message === warning.message && issue.path === warning.path)) {
3394
+ continue;
3395
+ }
3396
+ issues.push({
3397
+ severity: warning.level === "warn" ? "warning" : "info",
3398
+ source: "capability",
3399
+ message: warning.message,
3400
+ path: warning.path,
3401
+ code: warning.code,
3402
+ action: inferCapabilityAction(warning.code, warning.message)
3403
+ });
3404
+ }
3405
+ return {
3406
+ issues,
3407
+ summary: {
3408
+ total: issues.length,
3409
+ error: issues.filter((issue) => issue.severity === "error").length,
3410
+ warning: issues.filter((issue) => issue.severity === "warning").length,
3411
+ info: issues.filter((issue) => issue.severity === "info").length
3412
+ }
3413
+ };
3414
+ }
3415
+ function formatValidationIssue(issue) {
3416
+ const path = issue.path ? `${issue.path}: ` : "";
3417
+ return `[${issue.severity}] ${path}${issue.message} Action: ${issue.action}`;
3418
+ }
3419
+ function formatValidationIssueReport(report) {
3420
+ return report.issues.map((issue) => formatValidationIssue(issue));
3421
+ }
3422
+ var init_validation_contract = __esm({
3423
+ "src/utils/validation-contract.ts"() {
3424
+ "use strict";
3425
+ }
3426
+ });
3427
+
2999
3428
  // src/provider-presets.ts
3000
3429
  function getProviderPreset(key) {
3001
3430
  const preset = PROVIDER_PRESETS[key];
@@ -3113,6 +3542,34 @@ var init_provider_presets = __esm({
3113
3542
  }
3114
3543
  });
3115
3544
 
3545
+ // src/ui/workbench.ts
3546
+ function toInlineScriptJson(value) {
3547
+ return JSON.stringify(value).replace(/</g, "\\u003c");
3548
+ }
3549
+ function escapeHtml(value) {
3550
+ return String(value).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
3551
+ }
3552
+ function renderWorkbenchHtml(rawInitialConfig, configuredThresholds = {}) {
3553
+ const initialConfig = rawInitialConfig ?? {};
3554
+ const modelsCount = Array.isArray(initialConfig.Models) ? initialConfig.Models.length : 0;
3555
+ const routerDefault = initialConfig.Router?.default ?? "-";
3556
+ const displayPort = initialConfig.PORT ?? "-";
3557
+ const escapedDisplayPort = escapeHtml(displayPort);
3558
+ const escapedModelsCount = escapeHtml(modelsCount);
3559
+ const escapedRouterDefault = escapeHtml(routerDefault);
3560
+ const escapedMinSampleSize = escapeHtml(configuredThresholds.min_sample_size ?? 3);
3561
+ const escapedCascadeWarnRate = escapeHtml(configuredThresholds.cascade_warn_rate ?? 0.4);
3562
+ const escapedShadowWarnRate = escapeHtml(configuredThresholds.shadow_warn_rate ?? 0.5);
3563
+ const escapedLatencyWarnMs = escapeHtml(configuredThresholds.latency_warn_ms ?? 1500);
3564
+ return `<!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}.hero{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(260px,.8fr);gap:1rem;align-items:stretch;margin-bottom:1rem}.hero h2{margin:.2rem 0 .5rem;font-size:1.55rem}.hero-copy{display:flex;flex-direction:column;justify-content:center}.status-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.status-tile{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem;min-width:0}.status-tile strong{display:block;margin-top:.2rem;word-break:break-word}@media (max-width:760px){.hero{grid-template-columns:1fr}.status-grid{grid-template-columns:1fr}}.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}.surface-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin:1rem 0}.surface-tab{background:#fff;color:#1f2328;border-color:#d1d5db}.surface-tab.active{background:#111827;color:#fff;border-color:#111827}.surface-panel[hidden]{display:none}.surface-heading{display:flex;gap:1rem;flex-wrap:wrap;align-items:center;margin-bottom:.75rem}</style></head><body><div class="hero"><div class="panel hero-copy"><h2>\u914D\u7F6E\u4E0E\u72B6\u6001\u5DE5\u4F5C\u53F0</h2><p class="muted">\u67E5\u770B\u5F53\u524D\u8DEF\u7531\u670D\u52A1\u3001\u6A21\u578B\u914D\u7F6E\u548C\u9ED8\u8BA4\u53BB\u5411\uFF1B\u9700\u8981\u6392\u67E5\u65F6\uFF0C\u4E0B\u65B9\u7EF4\u62A4\u8005\u533A\u57DF\u53EF\u7EE7\u7EED\u67E5\u770B Governance Trace\u3001metrics \u548C\u5F52\u6863\u3002</p><div class="action-row"><button id="loadConfigDraftHeroBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="previewConfigDraftHeroBtn" type="button">\u9884\u89C8 compiled models</button><button id="refreshStatusHeroBtn" type="button">\u5237\u65B0\u72B6\u6001</button></div></div><div class="panel"><div class="status-grid"><div class="status-tile"><span class="muted">Service</span><strong id="serviceReadyStatus">ready</strong></div><div class="status-tile"><span class="muted">Port</span><strong id="servicePortStatus">${escapedDisplayPort}</strong></div><div class="status-tile"><span class="muted">Models</span><strong id="modelCountStatus">${escapedModelsCount}</strong></div><div class="status-tile"><span class="muted">Router.default</span><strong id="routerDefaultStatus">${escapedRouterDefault}</strong></div></div></div></div><div class="surface-tabs" role="tablist" aria-label="\u5DE5\u4F5C\u53F0\u5207\u6362"><button id="userSurfaceTab" class="surface-tab active" type="button" role="tab" aria-selected="true" data-surface-target="user">\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</button><button id="maintainerSurfaceTab" class="surface-tab" type="button" role="tab" aria-selected="false" data-surface-target="maintainer">\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</button></div><section id="userSurface" class="surface-panel" data-surface="user"><div class="panel"><div class="surface-heading"><strong>\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u914D\u7F6E\u3001\u6A21\u578B\u3001\u8DEF\u7531\u3001\u670D\u52A1\u72B6\u6001\u4E0E\u4E0B\u4E00\u6B65\u4FDD\u5B58\u52A8\u4F5C\u3002</span></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></section><section id="maintainerSurface" class="surface-panel" data-surface="maintainer" hidden><div class="panel"><div class="surface-heading"><strong>\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u8FD0\u884C\u89C2\u6D4B\u3001Governance Trace\u3001metrics\u3001\u5F52\u6863\u4E0E\u7EF4\u62A4\u64CD\u4F5C\u3002</span></div><div class="row"><strong>\u7EF4\u62A4\u8005\u89C2\u6D4B</strong><span class="muted">\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4 Governance Trace\uFF0C\u5E76\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u6307\u6807\u3002</span></div><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 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="${escapedMinSampleSize}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${escapedCascadeWarnRate}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${escapedShadowWarnRate}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${escapedLatencyWarnMs}"></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></section><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 serviceReadyStatus=document.getElementById('serviceReadyStatus');const servicePortStatus=document.getElementById('servicePortStatus');const modelCountStatus=document.getElementById('modelCountStatus');const routerDefaultStatus=document.getElementById('routerDefaultStatus');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');const surfaceTabs=Array.from(document.querySelectorAll('[data-surface-target]'));const surfacePanels=Array.from(document.querySelectorAll('[data-surface]'));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 setActiveSurface(surfaceName){ surfacePanels.forEach((panel)=>{ panel.hidden=panel.dataset.surface !== surfaceName; }); surfaceTabs.forEach((tab)=>{ const active=tab.dataset.surfaceTarget === surfaceName; tab.classList.toggle('active',active); tab.setAttribute('aria-selected', active ? 'true' : 'false'); });}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 updateStatusSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; modelCountStatus.textContent=String(models.length); routerDefaultStatus.textContent=config?.Router?.default || '-';}function renderDraftValidation(errors,warnings,issueReport){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; const contractIssues=Array.isArray(issueReport?.issues) ? issueReport.issues : []; if(!errorList.length && !warningList.length && !contractIssues.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 sourceItems=contractIssues.length ? contractIssues.map(item=>({ text:String(item.message || ''), severity:item.severity==='error' ? 'error' : 'warning', path:item.path || '', action:item.action || '' })) : [...errorList.map(item=>({ text:String(item), severity:'error', path:'', action:'' })), ...warningList.map(item=>({ text:String(item), severity:'warning', path:'', action:'' }))]; const grouped=sourceItems.reduce((acc,item)=>{ const text=item.text; const path=item.path || extractPath(text); const bucket=path.startsWith('Models') || text.startsWith('Models') ? 'Models' : path.startsWith('Router') || text.startsWith('Router') ? 'Router' : path.startsWith('TriggerRouter') || text.startsWith('TriggerRouter') ? 'SmartRouter' : path.startsWith('SmartRouter') || text.startsWith('SmartRouter') ? 'SmartRouter' : (path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic') || text.startsWith('Governance.sticky') || text.startsWith('Governance.semantic')) ? 'SmartRouter' : path.startsWith('Governance') || text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path, severity:item.severity, action:item.action || '' }); return acc; }, {}); const errorCount=contractIssues.length ? contractIssues.filter(item=>item.severity==='error').length : errorList.length; const warningCount=contractIssues.length ? contractIssues.filter(item=>item.severity!=='error').length : warningList.length; const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorCount)+' errors / '+esc(warningCount)+' warnings</span></div><div class="muted">'+(errorCount ? '\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)+(item.action ? ('<div class="muted">Action: '+esc(item.action)+'</div>') : '')+'</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); updateStatusSummary(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 || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function loadServiceStatus(){ serviceReadyStatus.textContent='checking'; try { const res=await fetch('/api/health'); const data=await res.json(); serviceReadyStatus.textContent=data.ready ? 'ready' : 'not ready'; servicePortStatus.textContent=data.port || '-'; } catch (_error) { serviceReadyStatus.textContent='unreachable'; }}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 || [], data.issueReport); 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); });surfaceTabs.forEach((tab)=>tab.addEventListener('click',()=>setActiveSurface(tab.dataset.surfaceTarget || 'user')));setActiveSurface('user');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 || [], data.issueReport); 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 || [], data.issueReport); 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 || [], data.issueReport); 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('loadConfigDraftHeroBtn').addEventListener('click',loadConfigDraft);document.getElementById('previewConfigDraftHeroBtn').addEventListener('click',previewConfigDraft);document.getElementById('refreshStatusHeroBtn').addEventListener('click',loadServiceStatus);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();loadServiceStatus();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`;
3565
+ }
3566
+ var init_workbench = __esm({
3567
+ "src/ui/workbench.ts"() {
3568
+ "use strict";
3569
+ init_provider_presets();
3570
+ }
3571
+ });
3572
+
3116
3573
  // src/server.ts
3117
3574
  function toCompiledRegistryView(config) {
3118
3575
  const registry = buildModelRegistry(config ?? {});
@@ -3127,11 +3584,10 @@ function toCompiledRegistryView(config) {
3127
3584
  modelMap: registry.modelMap
3128
3585
  };
3129
3586
  }
3130
- function toInlineScriptJson(value) {
3131
- return JSON.stringify(value).replace(/</g, "\\u003c");
3132
- }
3133
3587
  function collectModelReferences(config) {
3134
3588
  const refs = [];
3589
+ const normalizedConfig = normalizeAndValidateConfig(config ?? {}).config;
3590
+ const runtimeSmartRouterConfig = deriveRuntimeSmartRouterConfig(normalizedConfig);
3135
3591
  const pushRef = (path, value) => {
3136
3592
  if (typeof value !== "string" || !value.trim()) {
3137
3593
  return;
@@ -3142,24 +3598,103 @@ function collectModelReferences(config) {
3142
3598
  referenceType: value.includes(",") ? "legacy" : "modelId"
3143
3599
  });
3144
3600
  };
3145
- pushRef("Router.default", config?.Router?.default);
3146
- pushRef("TriggerRouter.intent_model", config?.TriggerRouter?.intent_model);
3147
- config?.TriggerRouter?.rules?.forEach((rule, index) => {
3148
- pushRef(`TriggerRouter.rules[${index}].model`, rule?.model);
3601
+ pushRef("Router.default", normalizedConfig?.Router?.default);
3602
+ pushRef("SmartRouter.router_model", runtimeSmartRouterConfig?.router_model);
3603
+ runtimeSmartRouterConfig?.rules?.forEach((rule, index) => {
3604
+ pushRef(`SmartRouter.rules[${index}].model`, rule?.model);
3149
3605
  });
3150
- pushRef("SmartRouter.router_model", config?.SmartRouter?.router_model);
3151
- config?.SmartRouter?.candidates?.forEach((candidate, index) => {
3606
+ runtimeSmartRouterConfig?.candidates?.forEach((candidate, index) => {
3152
3607
  pushRef(`SmartRouter.candidates[${index}].model`, candidate?.model);
3153
3608
  });
3154
- pushRef("Governance.sticky.alignment.summarizer_model", config?.Governance?.sticky?.alignment?.summarizer_model);
3155
- config?.Governance?.cascade?.levels?.forEach((level, index) => {
3609
+ pushRef("SmartRouter.sticky.alignment.summarizer_model", runtimeSmartRouterConfig?.sticky?.alignment?.summarizer_model);
3610
+ pushRef("SmartRouter.semantic.classifier_model", runtimeSmartRouterConfig?.semantic?.classifier_model);
3611
+ normalizedConfig?.Governance?.cascade?.levels?.forEach((level, index) => {
3156
3612
  pushRef(`Governance.cascade.levels[${index}].from`, level?.from);
3157
3613
  pushRef(`Governance.cascade.levels[${index}].to`, level?.to);
3158
3614
  });
3159
- pushRef("Governance.semantic.classifier_model", config?.Governance?.semantic?.classifier_model);
3160
- pushRef("Governance.shadow.verifier_model", config?.Governance?.shadow?.verifier_model);
3615
+ pushRef("Governance.shadow.verifier_model", normalizedConfig?.Governance?.shadow?.verifier_model);
3161
3616
  return refs;
3162
3617
  }
3618
+ function buildServiceInfo(rawConfig) {
3619
+ const normalized = normalizeAndValidateConfig(rawConfig ?? {}).config;
3620
+ const runtime = normalized.Runtime ?? {};
3621
+ const remoteService = runtime.remote_service ?? {};
3622
+ const registration = normalized.Registration ?? {};
3623
+ const runtimeMode = runtime.mode ?? "local";
3624
+ return {
3625
+ service: SERVICE_NAME,
3626
+ ready: true,
3627
+ host: rawConfig?.HOST ?? normalized.HOST,
3628
+ port: rawConfig?.PORT ?? normalized.PORT,
3629
+ runtimeMode,
3630
+ serviceRole: runtimeMode === "local" ? "local_agent" : "router_service",
3631
+ remoteEnabled: Boolean(remoteService.enabled),
3632
+ remoteService: {
3633
+ enabled: Boolean(remoteService.enabled),
3634
+ baseUrl: remoteService.base_url || "",
3635
+ authTokenConfigured: Boolean(remoteService.auth_token)
3636
+ },
3637
+ registration: {
3638
+ enabled: Boolean(registration.enabled),
3639
+ models: Array.isArray(registration.models) ? registration.models.length : 0,
3640
+ upstreamServices: Array.isArray(registration.upstream_services) ? registration.upstream_services.length : 0
3641
+ }
3642
+ };
3643
+ }
3644
+ function buildRegistrationInfo(rawConfig) {
3645
+ const normalizedResult = normalizeAndValidateConfig(rawConfig ?? {});
3646
+ const registration = normalizedResult.config.Registration ?? {};
3647
+ const models = Array.isArray(registration.models) ? registration.models : [];
3648
+ const upstreamServices = Array.isArray(registration.upstream_services) ? registration.upstream_services : [];
3649
+ return {
3650
+ enabled: Boolean(registration.enabled),
3651
+ summary: {
3652
+ models: models.length,
3653
+ upstreamServices: upstreamServices.length
3654
+ },
3655
+ models: models.map((model) => ({
3656
+ id: model.id,
3657
+ model: model.model,
3658
+ interface: model.interface ?? model.protocol,
3659
+ apiConfigured: Boolean(model.api ?? model.api_base_url),
3660
+ keyConfigured: Boolean(model.key ?? model.api_key)
3661
+ })),
3662
+ upstreamServices: upstreamServices.map((service) => ({
3663
+ id: service.id,
3664
+ baseUrl: service.base_url,
3665
+ authTokenConfigured: Boolean(service.auth_token)
3666
+ })),
3667
+ issueReport: buildValidationIssueReport({
3668
+ errors: normalizedResult.errors,
3669
+ warnings: normalizedResult.warnings
3670
+ })
3671
+ };
3672
+ }
3673
+ function summarizeCompiledModels(normalized) {
3674
+ const compiled = toCompiledRegistryView(normalized);
3675
+ const capabilityWarnings = collectCapabilityWarnings(normalized);
3676
+ const modelEntries = Object.values(compiled.modelMap ?? {});
3677
+ return {
3678
+ providerCount: compiled.providers.length,
3679
+ modelCount: modelEntries.length,
3680
+ capabilities: {
3681
+ reasoning: modelEntries.filter((item) => item.capabilities?.thinking?.supported !== false).length,
3682
+ tools: modelEntries.filter((item) => item.capabilities?.tools !== false).length,
3683
+ images: modelEntries.filter((item) => item.capabilities?.images !== false).length,
3684
+ warningCount: capabilityWarnings.summary.total,
3685
+ warnCount: capabilityWarnings.summary.warn,
3686
+ infoCount: capabilityWarnings.summary.info
3687
+ }
3688
+ };
3689
+ }
3690
+ function summarizeGovernanceAlerts(report) {
3691
+ return {
3692
+ totalTraces: report.metrics.totalTraces,
3693
+ alertCount: report.anomalies.length,
3694
+ warnCount: report.anomalies.filter((item) => item.severity === "warn").length,
3695
+ criticalCount: report.anomalies.filter((item) => item.severity === "critical").length
3696
+ };
3697
+ }
3163
3698
  function scoreModelIdSuggestion(source, candidateId, candidate) {
3164
3699
  const sourceText = String(source || "").toLowerCase();
3165
3700
  const candidateText = `${candidateId} ${candidate?.modelName || ""}`.toLowerCase();
@@ -3214,6 +3749,141 @@ function analyzeModelReferenceImpact(config, nextCompiled) {
3214
3749
  }
3215
3750
  };
3216
3751
  }
3752
+ function projectConfiguredBranch(raw, normalized) {
3753
+ if (raw === void 0) {
3754
+ return void 0;
3755
+ }
3756
+ if (raw === null || normalized === null) {
3757
+ return normalized;
3758
+ }
3759
+ if (Array.isArray(raw)) {
3760
+ return normalized;
3761
+ }
3762
+ if (typeof raw !== "object" || typeof normalized !== "object") {
3763
+ return normalized;
3764
+ }
3765
+ const result = {};
3766
+ Object.keys(raw).forEach((key) => {
3767
+ if (normalized[key] === void 0) {
3768
+ return;
3769
+ }
3770
+ result[key] = projectConfiguredBranch(raw[key], normalized[key]);
3771
+ });
3772
+ return result;
3773
+ }
3774
+ function mergeSmartRouterProjection(target, patch) {
3775
+ if (!patch || !Object.keys(patch).length) {
3776
+ return target;
3777
+ }
3778
+ return {
3779
+ ...target,
3780
+ ...patch,
3781
+ semantic: patch.semantic ? {
3782
+ ...target.semantic || {},
3783
+ ...patch.semantic
3784
+ } : target.semantic,
3785
+ sticky: patch.sticky ? {
3786
+ ...target.sticky || {},
3787
+ ...patch.sticky,
3788
+ alignment: patch.sticky?.alignment ? {
3789
+ ...target.sticky?.alignment || {},
3790
+ ...patch.sticky.alignment
3791
+ } : target.sticky?.alignment
3792
+ } : target.sticky
3793
+ };
3794
+ }
3795
+ function buildPersistedConfig(rawConfig, normalizedConfig) {
3796
+ const persisted = {
3797
+ HOST: normalizedConfig.HOST,
3798
+ PORT: normalizedConfig.PORT,
3799
+ LOG: normalizedConfig.LOG,
3800
+ LOG_LEVEL: normalizedConfig.LOG_LEVEL,
3801
+ API_TIMEOUT_MS: normalizedConfig.API_TIMEOUT_MS,
3802
+ NON_INTERACTIVE_MODE: normalizedConfig.NON_INTERACTIVE_MODE,
3803
+ APIKEY: normalizedConfig.APIKEY,
3804
+ PROXY_URL: normalizedConfig.PROXY_URL,
3805
+ CUSTOM_ROUTER_PATH: normalizedConfig.CUSTOM_ROUTER_PATH,
3806
+ Providers: normalizedConfig.Providers,
3807
+ Models: normalizedConfig.Models,
3808
+ Router: normalizedConfig.Router
3809
+ };
3810
+ const runtimeSmartRouter = deriveRuntimeSmartRouterConfig(normalizedConfig, rawConfig);
3811
+ let smartRouterProjection = projectConfiguredBranch(rawConfig?.SmartRouter, runtimeSmartRouter) ?? {};
3812
+ const runtimeProjection = projectConfiguredBranch(rawConfig?.Runtime, normalizedConfig.Runtime);
3813
+ if (runtimeProjection && typeof runtimeProjection === "object" && Object.keys(runtimeProjection).length > 0) {
3814
+ persisted.Runtime = runtimeProjection;
3815
+ }
3816
+ const registrationProjection = projectConfiguredBranch(rawConfig?.Registration, normalizedConfig.Registration);
3817
+ if (registrationProjection && typeof registrationProjection === "object" && Object.keys(registrationProjection).length > 0) {
3818
+ persisted.Registration = registrationProjection;
3819
+ }
3820
+ if (rawConfig?.TriggerRouter) {
3821
+ smartRouterProjection = mergeSmartRouterProjection(smartRouterProjection, {
3822
+ ...rawConfig.TriggerRouter.enabled !== void 0 ? { enabled: runtimeSmartRouter.enabled } : {},
3823
+ ...rawConfig.TriggerRouter.analysis_scope !== void 0 ? { analysis_scope: runtimeSmartRouter.analysis_scope } : {},
3824
+ ...rawConfig.TriggerRouter.rules !== void 0 ? { rules: runtimeSmartRouter.rules } : {},
3825
+ ...rawConfig.TriggerRouter.llm_intent_recognition !== void 0 || rawConfig.TriggerRouter.intent_model !== void 0 ? {
3826
+ semantic: {
3827
+ enabled: runtimeSmartRouter.semantic?.enabled,
3828
+ mode: runtimeSmartRouter.semantic?.mode,
3829
+ classifier_model: runtimeSmartRouter.semantic?.classifier_model
3830
+ }
3831
+ } : {}
3832
+ });
3833
+ }
3834
+ if (rawConfig?.Governance?.sticky) {
3835
+ smartRouterProjection = mergeSmartRouterProjection(
3836
+ smartRouterProjection,
3837
+ { sticky: projectConfiguredBranch(rawConfig.Governance.sticky, runtimeSmartRouter.sticky) }
3838
+ );
3839
+ }
3840
+ if (rawConfig?.Governance?.semantic) {
3841
+ smartRouterProjection = mergeSmartRouterProjection(
3842
+ smartRouterProjection,
3843
+ { semantic: projectConfiguredBranch(rawConfig.Governance.semantic, runtimeSmartRouter.semantic) }
3844
+ );
3845
+ }
3846
+ if (Object.keys(smartRouterProjection).length > 0) {
3847
+ persisted.SmartRouter = smartRouterProjection;
3848
+ }
3849
+ const governanceProjection = projectConfiguredBranch(rawConfig?.Governance, normalizedConfig?.Governance);
3850
+ if (governanceProjection && typeof governanceProjection === "object") {
3851
+ delete governanceProjection.sticky;
3852
+ delete governanceProjection.semantic;
3853
+ if (Object.keys(governanceProjection).length > 0) {
3854
+ persisted.Governance = governanceProjection;
3855
+ }
3856
+ }
3857
+ return persisted;
3858
+ }
3859
+ function buildDraftConfigView(config) {
3860
+ const normalizedConfig = normalizeAndValidateConfig(config ?? {}).config;
3861
+ const runtimeSmartRouterConfig = deriveRuntimeSmartRouterConfig(normalizedConfig);
3862
+ const draftConfig = {
3863
+ ...normalizedConfig,
3864
+ SmartRouter: runtimeSmartRouterConfig
3865
+ };
3866
+ delete draftConfig.TriggerRouter;
3867
+ if (draftConfig.Governance) {
3868
+ const projectedGovernance = {
3869
+ ...draftConfig.Governance
3870
+ };
3871
+ delete projectedGovernance.sticky;
3872
+ delete projectedGovernance.semantic;
3873
+ const hasResidualGovernance = Boolean(
3874
+ projectedGovernance.shadow || projectedGovernance.cascade || projectedGovernance.observability
3875
+ );
3876
+ if (!hasResidualGovernance) {
3877
+ delete draftConfig.Governance;
3878
+ } else {
3879
+ projectedGovernance.enabled = Boolean(
3880
+ projectedGovernance.enabled && hasResidualGovernance
3881
+ );
3882
+ draftConfig.Governance = projectedGovernance;
3883
+ }
3884
+ }
3885
+ return draftConfig;
3886
+ }
3217
3887
  function diffCompiledRegistry(base, next) {
3218
3888
  const providerNames = Array.from(/* @__PURE__ */ new Set([
3219
3889
  ...base.providers.map((item) => item.name),
@@ -3286,7 +3956,8 @@ var init_server = __esm({
3286
3956
  init_service_health();
3287
3957
  init_governance();
3288
3958
  init_compile();
3289
- init_provider_presets();
3959
+ init_validation_contract();
3960
+ init_workbench();
3290
3961
  createServer = (config) => {
3291
3962
  const server = new import_llms.default(config);
3292
3963
  const configuredThresholds = config.initialConfig?.Governance?.observability?.anomaly_thresholds ?? {};
@@ -3321,16 +3992,22 @@ var init_server = __esm({
3321
3992
  };
3322
3993
  };
3323
3994
  server.app.get("/api/config", async (req, reply) => {
3324
- return await readConfigFile();
3995
+ return buildDraftConfigView(await readConfigFile());
3325
3996
  });
3326
3997
  server.app.get("/api/models/compiled", async () => {
3327
3998
  const normalizedResult = normalizeAndValidateConfig(config.initialConfig ?? {});
3328
3999
  const normalized = normalizedResult.config;
3329
4000
  const compiled = toCompiledRegistryView(normalized);
4001
+ const capabilityWarnings = collectCapabilityWarnings(normalized);
3330
4002
  return {
3331
4003
  ...compiled,
3332
- capabilityWarnings: collectCapabilityWarnings(normalized),
3333
- warnings: normalizedResult.warnings
4004
+ capabilityWarnings,
4005
+ warnings: normalizedResult.warnings,
4006
+ issueReport: buildValidationIssueReport({
4007
+ errors: normalizedResult.errors,
4008
+ warnings: normalizedResult.warnings,
4009
+ capabilityWarnings
4010
+ })
3334
4011
  };
3335
4012
  });
3336
4013
  server.app.post("/api/models/compiled/preview", async (req, reply) => {
@@ -3342,6 +4019,7 @@ var init_server = __esm({
3342
4019
  rawCompiled = null;
3343
4020
  }
3344
4021
  const result = normalizeAndValidateConfig(rawConfig);
4022
+ const capabilityWarnings = rawCompiled ? collectCapabilityWarnings(rawConfig) : void 0;
3345
4023
  if (result.errors.length > 0) {
3346
4024
  reply.code(400);
3347
4025
  return {
@@ -3349,21 +4027,31 @@ var init_server = __esm({
3349
4027
  message: "Invalid configuration preview",
3350
4028
  errors: result.errors,
3351
4029
  referenceImpact: rawCompiled ? analyzeModelReferenceImpact(rawConfig, rawCompiled) : void 0,
3352
- capabilityWarnings: rawCompiled ? collectCapabilityWarnings(rawConfig) : void 0,
3353
- warnings: result.warnings
4030
+ capabilityWarnings,
4031
+ warnings: result.warnings,
4032
+ issueReport: buildValidationIssueReport({
4033
+ errors: result.errors,
4034
+ warnings: result.warnings,
4035
+ capabilityWarnings
4036
+ })
3354
4037
  };
3355
4038
  }
3356
4039
  const currentCompiled = toCompiledRegistryView(config.initialConfig ?? {});
3357
4040
  const previewCompiled = toCompiledRegistryView(result.config);
4041
+ const previewCapabilityWarnings = collectCapabilityWarnings(result.config);
3358
4042
  return {
3359
4043
  success: true,
3360
4044
  providers: previewCompiled.providers,
3361
4045
  modelMap: previewCompiled.modelMap,
3362
- normalizedConfig: result.config,
4046
+ normalizedConfig: buildDraftConfigView(result.config),
3363
4047
  diff: diffCompiledRegistry(currentCompiled, previewCompiled),
3364
4048
  referenceImpact: analyzeModelReferenceImpact(result.config, previewCompiled),
3365
- capabilityWarnings: collectCapabilityWarnings(result.config),
3366
- warnings: result.warnings
4049
+ capabilityWarnings: previewCapabilityWarnings,
4050
+ warnings: result.warnings,
4051
+ issueReport: buildValidationIssueReport({
4052
+ warnings: result.warnings,
4053
+ capabilityWarnings: previewCapabilityWarnings
4054
+ })
3367
4055
  };
3368
4056
  });
3369
4057
  server.app.get("/api/health", async () => {
@@ -3373,6 +4061,30 @@ var init_server = __esm({
3373
4061
  port: config.initialConfig?.PORT
3374
4062
  };
3375
4063
  });
4064
+ server.app.get("/api/service-info", async () => {
4065
+ return buildServiceInfo(config.initialConfig ?? {});
4066
+ });
4067
+ server.app.get("/api/registration", async () => {
4068
+ return buildRegistrationInfo(config.initialConfig ?? {});
4069
+ });
4070
+ server.app.get("/api/remote-status", async (req) => {
4071
+ const normalizedResult = normalizeAndValidateConfig(config.initialConfig ?? {});
4072
+ const normalized = normalizedResult.config;
4073
+ const remote = await probeRemoteServiceStatus(normalized.Runtime?.remote_service);
4074
+ const governanceReport = getGovernanceMetricsReport(readGovernanceMetricsQuery(req.query ?? {}));
4075
+ return {
4076
+ service: SERVICE_NAME,
4077
+ ready: true,
4078
+ runtimeMode: normalized.Runtime?.mode ?? "local",
4079
+ remote,
4080
+ compiledModels: summarizeCompiledModels(normalized),
4081
+ governance: summarizeGovernanceAlerts(governanceReport),
4082
+ issueReport: buildValidationIssueReport({
4083
+ errors: normalizedResult.errors,
4084
+ warnings: normalizedResult.warnings
4085
+ })
4086
+ };
4087
+ });
3376
4088
  server.app.get("/api/governance/traces", async (req) => {
3377
4089
  const limit = req.query?.limit ? Number(req.query.limit) : void 0;
3378
4090
  const cascadeTriggered = req.query?.cascadeTriggered === void 0 ? void 0 : String(req.query.cascadeTriggered).toLowerCase() === "true";
@@ -3550,15 +4262,24 @@ var init_server = __esm({
3550
4262
  success: false,
3551
4263
  message: "Invalid configuration",
3552
4264
  errors: result.errors,
3553
- warnings: result.warnings
4265
+ warnings: result.warnings,
4266
+ issueReport: buildValidationIssueReport({
4267
+ errors: result.errors,
4268
+ warnings: result.warnings
4269
+ })
3554
4270
  };
3555
4271
  }
3556
4272
  const backupPath = await backupConfigFile();
3557
4273
  if (backupPath) {
3558
4274
  log(`Backed up existing configuration file to ${backupPath}`);
3559
4275
  }
3560
- await writeConfigFile(result.config);
3561
- return { success: true, message: "Config saved successfully", warnings: result.warnings };
4276
+ await writeConfigFile(buildPersistedConfig(req.body ?? {}, result.config));
4277
+ return {
4278
+ success: true,
4279
+ message: "Config saved successfully",
4280
+ warnings: result.warnings,
4281
+ issueReport: buildValidationIssueReport({ warnings: result.warnings })
4282
+ };
3562
4283
  });
3563
4284
  server.app.post("/api/restart", async (req, reply) => {
3564
4285
  reply.send({ success: true, message: "Service restart initiated" });
@@ -3580,9 +4301,7 @@ var init_server = __esm({
3580
4301
  });
3581
4302
  server.app.get("/ui", async (_, reply) => {
3582
4303
  reply.header("Content-Type", "text/html; charset=utf-8");
3583
- return reply.send(
3584
- `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Claude Trigger Router</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:1rem;padding:.45rem 0;border-bottom:1px dashed #e5e7eb}.mini-list li:last-child{border-bottom:none}.action-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center;margin-top:.75rem}.management-table{width:100%;margin-top:.75rem}.management-table th,.management-table td{padding:.5rem;border-bottom:1px solid #e5e7eb;font-size:.92rem;vertical-align:top}.alert-list{display:grid;gap:.75rem;margin-top:1rem}.alert{border-radius:12px;padding:.85rem 1rem;border:1px solid}.alert.warn{background:#fff7ed;border-color:#fdba74;color:#9a3412}.alert.critical{background:#fef2f2;border-color:#fca5a5;color:#991b1b}.alert.info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.diff-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-top:.75rem}.diff-chip{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.75rem}.diff-chip strong{display:block;font-size:1rem;margin-top:.2rem}.models-form-grid{display:grid;gap:.75rem;margin-top:.75rem}.model-card{border:1px solid #e5e7eb;border-radius:12px;padding:1rem;background:#fcfcfd}.model-card-header{display:flex;justify-content:space-between;gap:1rem;align-items:center;margin-bottom:.75rem}.model-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.model-card-grid textarea{min-height:84px;resize:vertical}.list-editor{display:grid;gap:.75rem;margin-top:.75rem}.list-item{border:1px solid #e5e7eb;border-radius:12px;padding:.85rem;background:#fcfcfd}.list-item-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.jump-highlight{outline:3px solid #f59e0b;box-shadow:0 0 0 6px rgba(245,158,11,.15);transition:box-shadow .25s ease,outline-color .25s ease}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.control-grid label{display:block;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}.trend-table{width:100%;margin-top:.75rem}.trend-table th,.trend-table td{padding:.45rem;border-bottom:1px solid #e5e7eb;font-size:.92rem}.row{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}input,select,button{font:inherit;padding:.55rem .75rem;border-radius:8px;border:1px solid #d1d5db}button{background:#111827;color:#fff;border-color:#111827;cursor:pointer}table{width:100%;border-collapse:collapse;margin-top:1rem}th,td{text-align:left;padding:.65rem .5rem;border-bottom:1px solid #e5e7eb;vertical-align:top}code,pre{font-family:ui-monospace,SFMono-Regular,monospace}pre{white-space:pre-wrap;background:#0f172a;color:#e2e8f0;padding:1rem;border-radius:12px;overflow:auto}.pill{display:inline-block;padding:.2rem .5rem;border-radius:999px;background:#eef2ff;color:#3730a3;font-size:.8rem}</style></head><body><h2>Claude Trigger Router</h2><p class="muted">\u7B80\u6613 Governance Trace \u8C03\u8BD5\u9875\u3002\u53EF\u67E5\u770B\u6700\u8FD1\u6CBB\u7406\u94FE\u8DEF\uFF0C\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4\uFF0C\u5E76\u6309 cascade / shadow \u72B6\u6001\u7B5B\u9009\uFF1B\u6CBB\u7406 trace \u73B0\u5DF2\u652F\u6301\u672C\u5730\u6301\u4E45\u5316\uFF0C\u91CD\u542F\u540E\u53EF\u7EE7\u7EED\u67E5\u770B\u8FD1\u671F\u7A97\u53E3\u3002</p><div class="panel"><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Trigger rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u9996\u6279\u8868\u5355\u5316\u7F16\u8F91 TriggerRouter / SmartRouter / Governance \u7684\u6838\u5FC3\u5F15\u7528</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>TriggerRouter</strong><span class="muted">\u89C4\u5219\u8DEF\u7531\u4E0E\u610F\u56FE\u8BC6\u522B</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No trigger rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5BF9\u9F50\u3001\u8BED\u4E49\u3001\u5F71\u5B50\u6821\u9A8C\u4E0E\u7EA7\u8054</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div id="modelsFormGrid" class="models-form-grid"><div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div></div><textarea id="configDraftEditor" style="width:100%;min-height:240px;margin-top:.75rem;padding:.75rem;border-radius:12px;border:1px solid #d1d5db;font:12px/1.5 ui-monospace,SFMono-Regular,monospace" spellcheck="false" placeholder='{"Models":[{"id":"sonnet","api":"https://...","key":"sk-...","interface":"openai","model":"anthropic/claude-sonnet-4"}]}'></textarea><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / TriggerRouter / Governance \u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Compatibility profile</th><th>Dispatch format</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="8" class="muted">Loading model map...</td></tr></tbody></table></div></div></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Recent traces</span><strong>-</strong></div><div class="stat"><span class="muted">Sticky hit rate</span><strong>-</strong></div><div class="stat"><span class="muted">Cascade rate</span><strong>-</strong></div><div class="stat"><span class="muted">Shadow rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment rate</span><strong>-</strong></div><div class="stat"><span class="muted">Avg latency</span><strong>-</strong></div></div><div class="subpanel"><div class="row"><strong>Anomaly alerts</strong><span class="muted">\u68C0\u6D4B\u8FD1\u671F\u6CBB\u7406\u5F02\u5E38\u4E0E\u7A81\u589E</span></div><div id="anomalyList" class="alert-list"><div class="alert info"><strong>No alerts yet</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u6307\u6807\u52A0\u8F7D</div></div></div></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${configuredThresholds.min_sample_size ?? 3}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${configuredThresholds.cascade_warn_rate ?? 0.4}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${configuredThresholds.shadow_warn_rate ?? 0.5}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${configuredThresholds.latency_warn_ms ?? 1500}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Trend detail</strong><span class="muted">\u6BCF\u4E2A bucket \u7684\u8BE6\u7EC6\u547D\u4E2D\u7387</span></div><table id="trendTable" class="trend-table"><thead><tr><th>Bucket</th><th>Traces</th><th>Sticky</th><th>Cascade</th><th>Shadow</th><th>Alignment</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading...</td></tr></tbody></table></div></div><table id="traceTable"><thead><tr><th>Request</th><th>Session</th><th>Final Model</th><th>Reasons</th><th>Latency</th><th>Inspect</th></tr></thead><tbody><tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Trace Detail</strong><span id="detailHint" class="muted">\u70B9\u51FB\u4E0A\u8868\u4E2D\u7684 View \u67E5\u770B\u8BE6\u60C5</span></div><pre id="traceDetail">{}</pre></div><div class="panel"><div class="row"><strong>Snapshot Management</strong><span class="muted">\u67E5\u770B\u5BFC\u51FA\u5386\u53F2\u3001\u5B9A\u65F6\u4EFB\u52A1\uFF0C\u5E76\u624B\u52A8\u521B\u5EFA\u5FEB\u7167</span></div><div class="action-row"><select id="snapshotFormat"><option value="json">snapshot json</option><option value="csv">snapshot csv</option></select><button id="createSnapshotBtn" type="button">\u751F\u6210\u5FEB\u7167</button><span id="snapshotStatus" class="muted">\u5C1A\u672A\u521B\u5EFA\u5FEB\u7167</span></div><table id="exportTable" class="management-table"><thead><tr><th>Export</th><th>Kind</th><th>Format</th><th>Created</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading exports...</td></tr></tbody></table><table id="scheduleTable" class="management-table"><thead><tr><th>Schedule</th><th>Interval</th><th>Format</th><th>Last run</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading schedules...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Archive Management</strong><span class="muted">\u6D4F\u89C8\u538B\u7F29\u5F52\u6863\u5E76\u67E5\u770B\u5206\u9875\u7ED3\u679C</span></div><div class="action-row"><input id="archiveDate" placeholder="YYYY-MM-DD"><input id="archivePage" placeholder="page" value="1"><input id="archivePageSize" placeholder="pageSize" value="5"><button id="loadArchivesBtn" type="button">\u52A0\u8F7D\u5F52\u6863</button><span id="archiveStatus" class="muted">\u5C1A\u672A\u52A0\u8F7D\u5F52\u6863</span></div><table id="archiveTable" class="management-table"><thead><tr><th>Archive</th><th>Range</th><th>Count</th><th>Compressed</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading archives...</td></tr></tbody></table></div><div class="panel"><p>\u5176\u4ED6\u7BA1\u7406 API\uFF1A</p><ul><li><code>GET /api/config</code> \u2014 \u8BFB\u53D6\u5F53\u524D\u914D\u7F6E</li><li><code>GET /api/models/compiled</code> \u2014 \u67E5\u770B Models \u7F16\u8BD1\u540E\u7684\u5185\u90E8 provider / model \u6620\u5C04</li><li><code>POST /api/models/compiled/preview</code> \u2014 \u7528\u914D\u7F6E\u8349\u7A3F\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u5199\u56DE\u6587\u4EF6</li><li><code>POST /api/config</code> \u2014 \u4FDD\u5B58\u914D\u7F6E</li><li><code>GET /api/transformers</code> \u2014 \u67E5\u770B\u5DF2\u52A0\u8F7D transformer</li><li><code>POST /api/restart</code> \u2014 \u91CD\u542F\u670D\u52A1</li><li><code>GET /api/governance/archives</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5F52\u6863\u5217\u8868</li><li><code>GET /api/governance/archives/:file</code> \u2014 \u67E5\u770B\u5F52\u6863\u5185 traces</li><li><code>POST /api/governance/archives/:file/delete</code> \u2014 \u5220\u9664\u6307\u5B9A\u5F52\u6863</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const metricsGrid=document.getElementById('metricsGrid');const bucketGrid=document.getElementById('bucketGrid');const bucketHint=document.getElementById('bucketHint');const routeRanking=document.getElementById('routeRanking');const modelRanking=document.getElementById('modelRanking');const intentRanking=document.getElementById('intentRanking');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');let currentDraftConfig={};let knownModelIds=[];let activeValidationHighlight=null;const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94 TriggerRule\u3002', affects:['Router.default','TriggerRouter.enabled','TriggerRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u6838\u5FC3\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','Governance.sticky.alignment','Governance.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&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>'+esc(item.compatibilityProfile || '-')+'</td>' + '<td>'+esc(item.dispatchFormat || '-')+'</td>' + '<td><code>'+esc(JSON.stringify(item.thinking || { mode: 'off' }))+'</code></td>' + '<td><code>'+esc(JSON.stringify(item.capabilities || {}))+'</code></td>' + '<td>'+esc(item.source || '-')+'</td>' + '</tr>').join('') : '<tr><td colspan="8" class="muted">No compiled model map</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || []); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || []); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addCascadeLevel(){ const next=extractCascadeLevelsFromForm(); next.push({ from:'', to:'' }); renderCascadeLevelsList(next); syncDraftEditorFromForm(); }modelsFormGrid.addEventListener('input',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('change',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-template]'); if(applyBtn){ const applyIndex=Number(applyBtn.dataset.applyTemplate); applyProviderTemplate(applyIndex); syncDraftEditorFromForm(); return; } const btn=e.target.closest('button[data-remove-model]'); if(!btn){ return; } const removeIndex=Number(btn.dataset.removeModel); const nextModels=extractModelsFromForm().filter((_,index)=>index!==removeIndex); renderModelsForm(nextModels); syncDraftEditorFromForm(); });triggerRulesList.addEventListener('input',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('change',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('click',(e)=>{ const addKeywordBtn=e.target.closest('button[data-add-trigger-keyword]'); if(addKeywordBtn){ addTriggerKeyword(Number(addKeywordBtn.dataset.addTriggerKeyword), Number(addKeywordBtn.dataset.patternIndex)); return; } const removeKeywordBtn=e.target.closest('button[data-remove-trigger-keyword]'); if(removeKeywordBtn){ const ruleIndex=Number(removeKeywordBtn.dataset.removeTriggerKeyword); const patternIndex=Number(removeKeywordBtn.dataset.patternIndex); const keywordIndex=Number(removeKeywordBtn.dataset.keywordIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex] && next[ruleIndex].patterns && next[ruleIndex].patterns[patternIndex]){ const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=(pattern.keywords || []).filter((_,index)=>index!==keywordIndex); if(!pattern.keywords.length){ pattern.keywords=['']; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const addBtn=e.target.closest('button[data-add-trigger-pattern]'); if(addBtn){ addTriggerPattern(Number(addBtn.dataset.addTriggerPattern)); return; } const removePatternBtn=e.target.closest('button[data-remove-trigger-pattern]'); if(removePatternBtn){ const ruleIndex=Number(removePatternBtn.dataset.removeTriggerPattern); const patternIndex=Number(removePatternBtn.dataset.patternIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex]){ next[ruleIndex].patterns=(next[ruleIndex].patterns || []).filter((_,index)=>index!==patternIndex); if(!next[ruleIndex].patterns.length){ next[ruleIndex].patterns=[{ type:'exact', keywords:[] }]; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const btn=e.target.closest('button[data-remove-trigger-rule]'); if(!btn){ return; } const next=extractTriggerRulesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeTriggerRule)); renderTriggerRulesList(next); syncDraftEditorFromForm(); });smartCandidatesList.addEventListener('input',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('change',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-smart-candidate]'); if(!btn){ return; } const next=extractSmartCandidatesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeSmartCandidate)); renderSmartCandidatesList(next); syncDraftEditorFromForm(); });governanceCascadeLevelsList.addEventListener('input',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('change',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-cascade-level]'); if(!btn){ return; } const next=extractCascadeLevelsFromForm().filter((_,index)=>index!==Number(btn.dataset.removeCascadeLevel)); renderCascadeLevelsList(next); syncDraftEditorFromForm(); });referenceImpactTableBody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-apply-reference-path]'); if(!btn){ return; } applyReferenceSuggestion(btn.dataset.applyReferencePath, btn.dataset.applyReferenceModel); });draftValidationList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });capabilityWarningsList.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-warning-path]'); if(applyBtn){ applyCapabilityWarningSuggestion(applyBtn.dataset.applyWarningPath, applyBtn.dataset.applyWarningCode); return; } const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });function renderMetrics(metrics){ metricsGrid.innerHTML=[ ['Recent traces', metrics.totalTraces ?? 0], ['Sticky hit rate', pct(metrics.stickyHitRate)], ['Cascade rate', pct(metrics.cascadeTriggeredRate)], ['Shadow rate', pct(metrics.shadowCheckedRate)], ['Alignment rate', pct(metrics.alignmentUsedRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.TriggerRouter={ ...(payload.TriggerRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.TriggerRouter?.enabled), analysis_scope: payload.TriggerRouter?.analysis_scope || 'last_message', llm_intent_recognition: payload.TriggerRouter?.llm_intent_recognition || false, intent_model: payload.TriggerRouter?.intent_model || '', rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.TriggerRouter?.rules || []) }; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), sticky:{ ...((payload.Governance && payload.Governance.sticky) || {}), alignment:{ ...(((payload.Governance && payload.Governance.sticky && payload.Governance.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.Governance?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.Governance?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.Governance && payload.Governance.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.Governance?.semantic?.enabled), mode:(payload.Governance?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.Governance?.semantic?.classifier_model || '') }, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || []); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderAnomalies(anomalies){ if(!anomalies || !anomalies.length){ anomalyList.innerHTML='<div class="alert info"><strong>No active alerts</strong><div class="muted">\u5F53\u524D\u7A97\u53E3\u672A\u53D1\u73B0\u660E\u663E\u6CBB\u7406\u5F02\u5E38</div></div>'; return; } anomalyList.innerHTML=anomalies.map(item=>'<div class="alert '+esc(item.severity || 'info')+'"><strong>'+esc(item.type)+'</strong><div>'+esc(item.message)+'</div></div>').join('');}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')}</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); renderMetrics(metricsData.metrics || {}); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || []); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderTrendTable(metricsData || {}); const traces=data.traces || []; if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`
3585
- );
4304
+ return reply.send(renderWorkbenchHtml(config.initialConfig, configuredThresholds));
3586
4305
  });
3587
4306
  return server;
3588
4307
  };
@@ -4687,6 +5406,9 @@ Important:
4687
5406
  if (!config.enabled) {
4688
5407
  return null;
4689
5408
  }
5409
+ if (!config.router_model) {
5410
+ return null;
5411
+ }
4690
5412
  if (!config.candidates || config.candidates.length < 2) {
4691
5413
  return null;
4692
5414
  }
@@ -4764,6 +5486,41 @@ var init_selector = __esm({
4764
5486
  init_semantic_router();
4765
5487
  init_compile();
4766
5488
  ModelSelector = class {
5489
+ isRoutingEnabled(config, smartRouterConfig) {
5490
+ if (smartRouterConfig) {
5491
+ return Boolean(smartRouterConfig.enabled);
5492
+ }
5493
+ return Boolean(config.enabled);
5494
+ }
5495
+ getRoutingRules(config, smartRouterConfig) {
5496
+ return smartRouterConfig?.rules?.length ? smartRouterConfig.rules : config.rules;
5497
+ }
5498
+ getEffectiveGovernanceConfig(smartRouterConfig, governanceConfig) {
5499
+ if (!smartRouterConfig?.semantic && !smartRouterConfig?.sticky) {
5500
+ return governanceConfig;
5501
+ }
5502
+ return {
5503
+ ...governanceConfig ?? {},
5504
+ enabled: Boolean(
5505
+ governanceConfig?.enabled || smartRouterConfig.semantic?.enabled || smartRouterConfig.sticky?.enabled
5506
+ ),
5507
+ sticky: smartRouterConfig.sticky ? {
5508
+ ...governanceConfig?.sticky ?? {},
5509
+ ...smartRouterConfig.sticky
5510
+ } : governanceConfig?.sticky,
5511
+ semantic: smartRouterConfig.semantic ? {
5512
+ ...governanceConfig?.semantic ?? {},
5513
+ ...smartRouterConfig.semantic,
5514
+ prototypes: {
5515
+ ...governanceConfig?.semantic?.prototypes ?? {},
5516
+ ...smartRouterConfig.semantic?.prototypes ?? {}
5517
+ }
5518
+ } : governanceConfig?.semantic,
5519
+ cascade: governanceConfig?.cascade,
5520
+ shadow: governanceConfig?.shadow,
5521
+ observability: governanceConfig?.observability
5522
+ };
5523
+ }
4767
5524
  resolveRouteModel(appConfig, ref) {
4768
5525
  if (!ref) {
4769
5526
  return void 0;
@@ -4895,14 +5652,20 @@ var init_selector = __esm({
4895
5652
  async selectModel(req, config, port = DEFAULT_CONFIG2.PORT, smartRouterConfig, governanceConfig, apiKey, timeoutMs) {
4896
5653
  const startTime = Date.now();
4897
5654
  const appConfig = req.appConfig;
4898
- if (!config.enabled) {
5655
+ const effectiveGovernanceConfig = this.getEffectiveGovernanceConfig(smartRouterConfig, governanceConfig);
5656
+ const routingRules = this.getRoutingRules(config, smartRouterConfig);
5657
+ const analysisConfig = smartRouterConfig?.analysis_scope ? {
5658
+ ...config,
5659
+ analysis_scope: smartRouterConfig.analysis_scope
5660
+ } : config;
5661
+ if (!this.isRoutingEnabled(config, smartRouterConfig)) {
4899
5662
  return {
4900
5663
  matched: false,
4901
5664
  confidence: 0,
4902
5665
  analysisTime: Date.now() - startTime
4903
5666
  };
4904
5667
  }
4905
- const text = contextAnalyzer.analyze(req, config);
5668
+ const text = contextAnalyzer.analyze(req, analysisConfig);
4906
5669
  if (!text) {
4907
5670
  return {
4908
5671
  matched: false,
@@ -4911,7 +5674,7 @@ var init_selector = __esm({
4911
5674
  analyzedText: ""
4912
5675
  };
4913
5676
  }
4914
- const matchResult = this.matchRuleFromText(text, config.rules);
5677
+ const matchResult = this.matchRuleFromText(text, routingRules);
4915
5678
  if (matchResult) {
4916
5679
  return {
4917
5680
  matched: true,
@@ -4921,14 +5684,14 @@ var init_selector = __esm({
4921
5684
  // 关键词匹配置信度为 1
4922
5685
  analysisTime: Date.now() - startTime,
4923
5686
  analyzedText: text,
4924
- routeSource: "trigger_rule"
5687
+ routeSource: "smart_rule"
4925
5688
  };
4926
5689
  }
4927
- const stickyCorrection = this.getStickyCorrection(text, req, governanceConfig);
4928
- const semanticCandidates = this.buildSemanticCandidates(config.rules, governanceConfig);
4929
- if (governanceConfig?.enabled && governanceConfig.semantic?.enabled && semanticCandidates.length > 0) {
5690
+ const stickyCorrection = this.getStickyCorrection(text, req, effectiveGovernanceConfig);
5691
+ const semanticCandidates = this.buildSemanticCandidates(routingRules, effectiveGovernanceConfig);
5692
+ if (effectiveGovernanceConfig?.enabled && effectiveGovernanceConfig.semantic?.enabled && semanticCandidates.length > 0) {
4930
5693
  const semanticConfig = {
4931
- ...governanceConfig.semantic,
5694
+ ...effectiveGovernanceConfig.semantic,
4932
5695
  prototypes: Object.fromEntries(semanticCandidates.map((candidate) => [candidate.rule.name, candidate.prototype]))
4933
5696
  };
4934
5697
  const semanticResult = semanticConfig.mode === "classifier" ? await semanticRouter.analyzeWithClassifier(
@@ -4948,7 +5711,7 @@ var init_selector = __esm({
4948
5711
  semanticConfig.threshold
4949
5712
  );
4950
5713
  if (semanticResult) {
4951
- const matchedRule = config.rules.find(
5714
+ const matchedRule = routingRules.find(
4952
5715
  (rule) => rule.enabled !== false && rule.name.toLowerCase() === semanticResult.intent.toLowerCase()
4953
5716
  );
4954
5717
  if (matchedRule) {
@@ -4969,7 +5732,7 @@ var init_selector = __esm({
4969
5732
  }
4970
5733
  }
4971
5734
  }
4972
- if (smartRouterConfig?.enabled && smartRouterConfig.candidates?.length >= 2) {
5735
+ if (smartRouterConfig?.enabled && smartRouterConfig.router_model && smartRouterConfig.candidates?.length >= 2) {
4973
5736
  try {
4974
5737
  const resolvedSmartRouterConfig = appConfig ? {
4975
5738
  ...smartRouterConfig,
@@ -4986,7 +5749,7 @@ var init_selector = __esm({
4986
5749
  void 0,
4987
5750
  apiKey,
4988
5751
  timeoutMs,
4989
- this.buildSmartRouterHint(text, config.rules)
5752
+ this.buildSmartRouterHint(text, routingRules)
4990
5753
  );
4991
5754
  if (smartResult) {
4992
5755
  log(`[SmartRouter] Selected model "${smartResult.model}" (confidence: ${smartResult.confidence})`);
@@ -5004,11 +5767,11 @@ var init_selector = __esm({
5004
5767
  logError("[ModelSelector] SmartRouter error:", error);
5005
5768
  }
5006
5769
  }
5007
- if (config.llm_intent_recognition && config.intent_model) {
5770
+ if (!smartRouterConfig?.enabled && config.llm_intent_recognition && config.intent_model) {
5008
5771
  try {
5009
5772
  const intentResult = await intentDetector.detectIntent(text, config, port, void 0, apiKey, timeoutMs);
5010
5773
  if (intentResult.confidence > 0.5 && intentResult.intent !== "general") {
5011
- const matchedRule = intentDetector.findRuleByIntent(intentResult.intent, config.rules);
5774
+ const matchedRule = intentDetector.findRuleByIntent(intentResult.intent, routingRules);
5012
5775
  if (matchedRule) {
5013
5776
  const intentSelection = {
5014
5777
  matched: true,
@@ -5017,7 +5780,7 @@ var init_selector = __esm({
5017
5780
  confidence: intentResult.confidence,
5018
5781
  analysisTime: Date.now() - startTime,
5019
5782
  analyzedText: text,
5020
- routeSource: "intent_fallback"
5783
+ routeSource: "semantic_match"
5021
5784
  };
5022
5785
  return this.applyStickyCorrection(intentSelection, stickyCorrection, appConfig) ?? intentSelection;
5023
5786
  }
@@ -5049,17 +5812,22 @@ var init_selector = __esm({
5049
5812
  * @param config 触发配置
5050
5813
  * @returns 分析结果
5051
5814
  */
5052
- selectModelSync(req, config) {
5815
+ selectModelSync(req, config, smartRouterConfig) {
5053
5816
  const startTime = Date.now();
5054
5817
  const appConfig = req.appConfig;
5055
- if (!config.enabled) {
5818
+ const effectiveGovernanceConfig = this.getEffectiveGovernanceConfig(smartRouterConfig, void 0);
5819
+ const analysisConfig = smartRouterConfig?.analysis_scope ? {
5820
+ ...config,
5821
+ analysis_scope: smartRouterConfig.analysis_scope
5822
+ } : config;
5823
+ if (!this.isRoutingEnabled(config, smartRouterConfig)) {
5056
5824
  return {
5057
5825
  matched: false,
5058
5826
  confidence: 0,
5059
5827
  analysisTime: Date.now() - startTime
5060
5828
  };
5061
5829
  }
5062
- const text = contextAnalyzer.analyze(req, config);
5830
+ const text = contextAnalyzer.analyze(req, analysisConfig);
5063
5831
  if (!text) {
5064
5832
  return {
5065
5833
  matched: false,
@@ -5068,7 +5836,9 @@ var init_selector = __esm({
5068
5836
  analyzedText: ""
5069
5837
  };
5070
5838
  }
5071
- const matchResult = this.matchRuleFromText(text, config.rules);
5839
+ const routingRules = this.getRoutingRules(config, smartRouterConfig);
5840
+ const stickyCorrection = this.getStickyCorrection(text, req, effectiveGovernanceConfig);
5841
+ const matchResult = this.matchRuleFromText(text, routingRules);
5072
5842
  if (matchResult) {
5073
5843
  return {
5074
5844
  matched: true,
@@ -5076,6 +5846,44 @@ var init_selector = __esm({
5076
5846
  model: appConfig ? resolveModelReference(appConfig, matchResult.rule.model) ?? matchResult.rule.model : matchResult.rule.model,
5077
5847
  confidence: 1,
5078
5848
  analysisTime: Date.now() - startTime,
5849
+ analyzedText: text,
5850
+ routeSource: "smart_rule"
5851
+ };
5852
+ }
5853
+ const semanticCandidates = this.buildSemanticCandidates(routingRules, effectiveGovernanceConfig);
5854
+ if (effectiveGovernanceConfig?.enabled && effectiveGovernanceConfig.semantic?.enabled && semanticCandidates.length > 0) {
5855
+ const semanticResult = semanticRouter.analyzeCandidates(
5856
+ text,
5857
+ semanticCandidates.map((candidate) => ({
5858
+ intent: candidate.rule.name,
5859
+ prototype: candidate.prototype,
5860
+ threshold: candidate.threshold
5861
+ })),
5862
+ effectiveGovernanceConfig.semantic.threshold
5863
+ );
5864
+ if (semanticResult) {
5865
+ const matchedRule = routingRules.find(
5866
+ (rule) => rule.enabled !== false && rule.name.toLowerCase() === semanticResult.intent.toLowerCase()
5867
+ );
5868
+ if (matchedRule) {
5869
+ const semanticSelection = {
5870
+ matched: true,
5871
+ rule: matchedRule,
5872
+ model: this.resolveRouteModel(appConfig, matchedRule.model),
5873
+ confidence: semanticResult.confidence,
5874
+ analysisTime: Date.now() - startTime,
5875
+ analyzedText: text,
5876
+ routeSource: "semantic_match"
5877
+ };
5878
+ return this.applyStickyCorrection(semanticSelection, stickyCorrection, appConfig) ?? semanticSelection;
5879
+ }
5880
+ }
5881
+ }
5882
+ const stickyOnlySelection = this.applyStickyCorrection(null, stickyCorrection, appConfig);
5883
+ if (stickyOnlySelection) {
5884
+ return {
5885
+ ...stickyOnlySelection,
5886
+ analysisTime: Date.now() - startTime,
5079
5887
  analyzedText: text
5080
5888
  };
5081
5889
  }
@@ -5107,6 +5915,7 @@ var init_trigger = __esm({
5107
5915
  init_analyzer();
5108
5916
  init_log();
5109
5917
  init_constants();
5918
+ init_config();
5110
5919
  TriggerRouter = class {
5111
5920
  config = null;
5112
5921
  appConfig = null;
@@ -5116,7 +5925,7 @@ var init_trigger = __esm({
5116
5925
  apiKey;
5117
5926
  apiTimeoutMs;
5118
5927
  /**
5119
- * 初始化触发路由器
5928
+ * 初始化 SmartRouter 运行时
5120
5929
  *
5121
5930
  * @param appConfig 应用配置
5122
5931
  */
@@ -5124,7 +5933,7 @@ var init_trigger = __esm({
5124
5933
  this.appConfig = appConfig;
5125
5934
  this.config = appConfig.TriggerRouter || this.getDefaultConfig();
5126
5935
  this.port = appConfig.PORT || DEFAULT_CONFIG2.PORT;
5127
- this.smartRouterConfig = appConfig.SmartRouter;
5936
+ this.smartRouterConfig = deriveRuntimeSmartRouterConfig(appConfig, appConfig);
5128
5937
  this.governanceConfig = appConfig.Governance;
5129
5938
  this.apiKey = appConfig.APIKEY;
5130
5939
  this.apiTimeoutMs = appConfig.API_TIMEOUT_MS;
@@ -5141,10 +5950,10 @@ var init_trigger = __esm({
5141
5950
  };
5142
5951
  }
5143
5952
  /**
5144
- * 检查触发路由是否启用
5953
+ * 检查 SmartRouter 运行时是否启用
5145
5954
  */
5146
5955
  isEnabled() {
5147
- return this.config?.enabled ?? false;
5956
+ return Boolean(this.smartRouterConfig?.enabled);
5148
5957
  }
5149
5958
  /**
5150
5959
  * 获取当前配置
@@ -5152,15 +5961,18 @@ var init_trigger = __esm({
5152
5961
  getConfig() {
5153
5962
  return this.config;
5154
5963
  }
5964
+ getSmartRouterConfig() {
5965
+ return this.smartRouterConfig;
5966
+ }
5155
5967
  /**
5156
- * 执行触发路由
5968
+ * 执行 SmartRouter 统一路由
5157
5969
  * 分析请求并返回匹配的模型
5158
5970
  *
5159
5971
  * @param req 请求对象
5160
5972
  * @returns 分析结果
5161
5973
  */
5162
5974
  async route(req) {
5163
- if (!this.config || !this.config.enabled) {
5975
+ if (!this.config || !this.isEnabled()) {
5164
5976
  return {
5165
5977
  matched: false,
5166
5978
  confidence: 0,
@@ -5188,32 +6000,29 @@ var init_trigger = __esm({
5188
6000
  this.apiTimeoutMs
5189
6001
  );
5190
6002
  if (req.governanceTrace) {
5191
- if (result.routeSource === "trigger_rule" && result.rule?.name) {
5192
- appendTraceReason(req.governanceTrace, `trigger_rule:${result.rule.name}`);
6003
+ if (result.routeSource === "smart_rule" && result.rule?.name) {
6004
+ appendTraceReason(req.governanceTrace, `smart_rule:${result.rule.name}`);
5193
6005
  } else if (result.routeSource === "semantic_match" && result.rule?.name) {
5194
6006
  appendTraceReason(req.governanceTrace, `semantic_match:${result.rule.name}`);
5195
6007
  } else if (result.routeSource === "sticky_correction") {
5196
6008
  req.governanceTrace.stickyHit = true;
5197
6009
  appendTraceReason(req.governanceTrace, "sticky_correction");
5198
6010
  } else if (result.routeSource === "smart_router") {
5199
- appendTraceReason(req.governanceTrace, "smart_decision");
5200
- } else if (result.routeSource === "intent_fallback") {
5201
- appendTraceReason(req.governanceTrace, "intent_fallback");
6011
+ appendTraceReason(req.governanceTrace, "smart_router");
5202
6012
  } else {
5203
- appendTraceReason(req.governanceTrace, "trigger_router:no_match");
6013
+ appendTraceReason(req.governanceTrace, "smart_router:no_match");
5204
6014
  }
5205
6015
  }
5206
6016
  return result;
5207
6017
  }
5208
6018
  /**
5209
- * 同步版本的触发路由
5210
- * 仅使用关键词匹配
6019
+ * 同步版本的 SmartRouter 统一路由
5211
6020
  *
5212
6021
  * @param req 请求对象
5213
6022
  * @returns 分析结果
5214
6023
  */
5215
6024
  routeSync(req) {
5216
- if (!this.config || !this.config.enabled) {
6025
+ if (!this.config || !this.isEnabled()) {
5217
6026
  return {
5218
6027
  matched: false,
5219
6028
  confidence: 0,
@@ -5231,11 +6040,11 @@ var init_trigger = __esm({
5231
6040
  return modelSelector.selectModelSync({
5232
6041
  ...req,
5233
6042
  appConfig: this.appConfig ?? void 0
5234
- }, this.config);
6043
+ }, this.config, this.smartRouterConfig);
5235
6044
  }
5236
6045
  /**
5237
6046
  * 创建 Fastify 中间件
5238
- * 用于在请求处理前执行触发路由
6047
+ * 用于在请求处理前执行 SmartRouter 统一路由
5239
6048
  *
5240
6049
  * @param appConfig 应用配置
5241
6050
  * @returns Fastify 中间件函数
@@ -5256,11 +6065,11 @@ var init_trigger = __esm({
5256
6065
  req.body.model = result.model;
5257
6066
  req.triggerResult = result;
5258
6067
  log(
5259
- `[TriggerRouter] ${result.routeSource === "sticky_correction" ? "Sticky correction selected" : result.routeSource === "semantic_match" ? `Semantic match "${result.rule?.name}"` : result.routeSource === "smart_router" ? "Smart fallback selected" : result.routeSource === "intent_fallback" ? `Intent fallback "${result.rule?.name}"` : result.rule ? `Matched rule "${result.rule.name}"` : "Unified router selected"} -> model "${result.model}" (confidence: ${result.confidence}, time: ${result.analysisTime}ms)`
6068
+ `[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)`
5260
6069
  );
5261
6070
  }
5262
6071
  } catch (error) {
5263
- logError("[TriggerRouter] Error in trigger routing:", error);
6072
+ logError("[SmartRouter] Error in routing:", error);
5264
6073
  }
5265
6074
  };
5266
6075
  }
@@ -5637,6 +6446,19 @@ async function initializeClaudeConfig() {
5637
6446
  await (0, import_promises2.writeFile)(configPath, JSON.stringify(configContent, null, 2));
5638
6447
  }
5639
6448
  }
6449
+ function buildServerInitialConfig(config, registry, host, servicePort) {
6450
+ return {
6451
+ ...config,
6452
+ providers: registry.providers,
6453
+ HOST: host,
6454
+ PORT: servicePort,
6455
+ LOG_FILE: (0, import_path5.join)(
6456
+ (0, import_os2.homedir)(),
6457
+ ".claude-trigger-router",
6458
+ "claude-trigger-router.log"
6459
+ )
6460
+ };
6461
+ }
5640
6462
  async function run(options = {}) {
5641
6463
  if (isServiceRunning()) {
5642
6464
  log("\u2705 Service is already running in the background.");
@@ -5688,16 +6510,7 @@ async function run(options = {}) {
5688
6510
  const registry = buildModelRegistry(config);
5689
6511
  const server = createServer({
5690
6512
  useJsonFile: false,
5691
- initialConfig: {
5692
- providers: registry.providers,
5693
- HOST,
5694
- PORT: servicePort,
5695
- LOG_FILE: (0, import_path5.join)(
5696
- (0, import_os2.homedir)(),
5697
- ".claude-trigger-router",
5698
- "claude-trigger-router.log"
5699
- )
5700
- },
6513
+ initialConfig: buildServerInitialConfig(config, registry, HOST, servicePort),
5701
6514
  logger: loggerConfig
5702
6515
  });
5703
6516
  server.addHook("preHandler", async (req, reply) => {
@@ -5710,7 +6523,7 @@ async function run(options = {}) {
5710
6523
  });
5711
6524
  });
5712
6525
  triggerRouter.init(config);
5713
- log(`[TriggerRouter] Initialized, enabled: ${triggerRouter.isEnabled()}`);
6526
+ log(`[SmartRouter] Initialized, enabled: ${triggerRouter.isEnabled()}`);
5714
6527
  server.addHook("preHandler", async (req, reply) => {
5715
6528
  if (req.url.startsWith("/v1/messages")) {
5716
6529
  if (req.body.metadata?.user_id) {
@@ -5725,14 +6538,14 @@ async function run(options = {}) {
5725
6538
  initialModel: req.body?.model
5726
6539
  });
5727
6540
  appendTraceReason(req.governanceTrace, "request_received");
5728
- const bypassTriggerRouter = req.headers["x-ctr-smart-router"] === "1";
5729
- const triggerResult = bypassTriggerRouter ? { matched: false, confidence: 0, analysisTime: 0 } : await triggerRouter.route(req);
6541
+ const bypassSmartRouter = req.headers["x-ctr-smart-router"] === "1";
6542
+ const triggerResult = bypassSmartRouter ? { matched: false, confidence: 0, analysisTime: 0 } : await triggerRouter.route(req);
5730
6543
  req.triggerResult = triggerResult;
5731
- if (!bypassTriggerRouter && triggerResult.matched && triggerResult.model) {
6544
+ if (!bypassSmartRouter && triggerResult.matched && triggerResult.model) {
5732
6545
  const previousSessionState = req.sessionId ? sessionStateStore.get(req.sessionId) : void 0;
5733
6546
  const previousModel = previousSessionState?.lastSuccessfulModel;
5734
- const alignmentConfig = config.Governance?.sticky?.alignment;
5735
- if (config.Governance?.enabled && alignmentConfig?.enabled && previousModel && previousModel !== triggerResult.model && triggerResult.analyzedText) {
6547
+ const alignmentConfig = triggerRouter.getSmartRouterConfig()?.sticky?.alignment ?? config.Governance?.sticky?.alignment;
6548
+ if (triggerRouter.getSmartRouterConfig()?.enabled && alignmentConfig?.enabled && previousModel && previousModel !== triggerResult.model && triggerResult.analyzedText) {
5736
6549
  const resolvedAlignmentConfig = {
5737
6550
  ...alignmentConfig,
5738
6551
  summarizer_model: resolveModelReference(config, alignmentConfig.summarizer_model) ?? alignmentConfig.summarizer_model
@@ -5761,7 +6574,7 @@ async function run(options = {}) {
5761
6574
  req.body.model = triggerResult.model;
5762
6575
  req.governanceTrace.finalModel = triggerResult.model;
5763
6576
  log(
5764
- `[TriggerRouter] Matched rule "${triggerResult.rule?.name}" -> "${triggerResult.model}"`
6577
+ `[SmartRouter] Selected "${triggerResult.rule?.name ?? triggerResult.routeSource ?? "route"}" -> "${triggerResult.model}"`
5765
6578
  );
5766
6579
  }
5767
6580
  const useAgents = [];
@@ -6095,11 +6908,8 @@ var init_repair = __esm({
6095
6908
  });
6096
6909
 
6097
6910
  // src/setup/migrate.ts
6098
- function inferProtocolFromApiBaseUrl(apiBaseUrl) {
6099
- if (apiBaseUrl?.includes("/v1/messages")) {
6100
- return "anthropic";
6101
- }
6102
- return "openai";
6911
+ function inferProtocolFromApiBaseUrl(apiBaseUrl, modelName) {
6912
+ return inferInterfaceFromApiEndpoint(apiBaseUrl, modelName) ?? "openai";
6103
6913
  }
6104
6914
  function normalizeSegment(value) {
6105
6915
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
@@ -6285,12 +7095,12 @@ function migrateLegacyConfig(input3) {
6285
7095
  const rawEntries = normalized.providers.flatMap(
6286
7096
  (provider, providerIndex) => (provider.models.length ? provider.models : [""]).map((model) => ({
6287
7097
  candidateId: toModelId(provider.name, model, providerIndex),
6288
- api: provider.api_base_url,
6289
- api_base_url: provider.api_base_url,
7098
+ api: provider.api_base_url ? normalizeApiEndpoint(provider.api_base_url, inferProtocolFromApiBaseUrl(provider.api_base_url, model)) : void 0,
7099
+ api_base_url: provider.api_base_url ? normalizeApiEndpoint(provider.api_base_url, inferProtocolFromApiBaseUrl(provider.api_base_url, model)) : void 0,
6290
7100
  key: provider.api_key,
6291
7101
  api_key: provider.api_key,
6292
- interface: inferProtocolFromApiBaseUrl(provider.api_base_url),
6293
- protocol: inferProtocolFromApiBaseUrl(provider.api_base_url),
7102
+ interface: inferProtocolFromApiBaseUrl(provider.api_base_url, model),
7103
+ protocol: inferProtocolFromApiBaseUrl(provider.api_base_url, model),
6294
7104
  model,
6295
7105
  providerName: provider.name
6296
7106
  })).filter((item) => item.model)
@@ -6367,6 +7177,7 @@ function migrateLegacyConfig(input3) {
6367
7177
  var init_migrate = __esm({
6368
7178
  "src/setup/migrate.ts"() {
6369
7179
  "use strict";
7180
+ init_schema();
6370
7181
  }
6371
7182
  });
6372
7183
 
@@ -6437,8 +7248,8 @@ function buildMinimalConfig(input3) {
6437
7248
  key: p.api_key,
6438
7249
  api_key: p.api_key,
6439
7250
  model: p.models[0] ?? "",
6440
- interface: preset?.interface ?? "openai",
6441
- protocol: preset?.protocol ?? "openai"
7251
+ interface: p.interface ?? preset?.interface ?? "openai",
7252
+ protocol: p.interface ?? preset?.protocol ?? "openai"
6442
7253
  };
6443
7254
  const explicitApiBaseUrl = p.api_base_url?.trim();
6444
7255
  const presetApiBaseUrl = preset?.api_base_url?.trim();
@@ -7078,6 +7889,9 @@ function mapConfigErrorsToRepairFields(errors) {
7078
7889
  const fields = getRepairFields(errors);
7079
7890
  return fields.includes("manualReview") ? { mode: "manualReview", fields } : { mode: "repair", fields };
7080
7891
  }
7892
+ function formatConfigIssues(input3) {
7893
+ return formatValidationIssueReport(buildValidationIssueReport(input3)).join("; ");
7894
+ }
7081
7895
  function mapValidCurrentConfigChoice(choice) {
7082
7896
  if (choice === "reuse" || choice === "\u76F4\u63A5\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\uFF08\u63A8\u8350\uFF09") {
7083
7897
  return "reuse";
@@ -7203,15 +8017,114 @@ function toDraftFromConfig(config) {
7203
8017
  }
7204
8018
  };
7205
8019
  }
7206
- function toSuggestedModelId(providerName, model, preset) {
7207
- const presetDefinition = getProviderPreset(preset);
7208
- if (presetDefinition?.suggested_id) {
7209
- return presetDefinition.suggested_id;
8020
+ function toUniqueSuggestedModelId(preferredId, existingIds) {
8021
+ const normalizedPreferredId = preferredId.trim() || "model";
8022
+ if (!existingIds.includes(normalizedPreferredId)) {
8023
+ return normalizedPreferredId;
8024
+ }
8025
+ let suffix = 2;
8026
+ while (existingIds.includes(`${normalizedPreferredId}_${suffix}`)) {
8027
+ suffix += 1;
7210
8028
  }
7211
- const source = model || providerName || "model";
7212
- return source.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "model";
8029
+ return `${normalizedPreferredId}_${suffix}`;
7213
8030
  }
7214
- async function buildFreshConfig(io) {
8031
+ function appendModelToDraft(draft, modelInput, options = {}) {
8032
+ const fragment = buildMinimalConfig({
8033
+ providers: [modelInput],
8034
+ defaultModel: options.setAsDefault ? modelInput.model_id : void 0
8035
+ });
8036
+ const nextDraft = {
8037
+ ...draft,
8038
+ Models: [...draft.Models ?? []],
8039
+ Router: { ...draft.Router ?? {} }
8040
+ };
8041
+ if (fragment.Models?.[0]) {
8042
+ nextDraft.Models?.push(fragment.Models[0]);
8043
+ }
8044
+ if (options.setAsDefault) {
8045
+ nextDraft.Router.default = modelInput.model_id;
8046
+ } else if (!nextDraft.Router.default) {
8047
+ nextDraft.Router.default = modelInput.model_id;
8048
+ }
8049
+ return nextDraft;
8050
+ }
8051
+ function createComplexTaskRules(modelId) {
8052
+ return [
8053
+ {
8054
+ name: "architecture",
8055
+ priority: 90,
8056
+ enabled: true,
8057
+ description: "\u67B6\u6784\u8BBE\u8BA1\u3001\u7CFB\u7EDF\u89C4\u5212\u548C\u5927\u8303\u56F4\u91CD\u6784\u4EFB\u52A1",
8058
+ patterns: [
8059
+ { type: "exact", keywords: ["\u67B6\u6784\u8BBE\u8BA1", "\u7CFB\u7EDF\u8BBE\u8BA1", "\u6280\u672F\u65B9\u6848", "architecture", "system design"] },
8060
+ { type: "regex", pattern: "(\u67B6\u6784|\u7CFB\u7EDF\u8BBE\u8BA1|\u6280\u672F\u65B9\u6848|architecture|system design)" }
8061
+ ],
8062
+ model: modelId
8063
+ },
8064
+ {
8065
+ name: "code_review",
8066
+ priority: 80,
8067
+ enabled: true,
8068
+ description: "\u4EE3\u7801\u5BA1\u67E5\u3001\u98CE\u9669\u8BC4\u4F30\u548C\u8D28\u91CF\u5206\u6790\u4EFB\u52A1",
8069
+ patterns: [
8070
+ { type: "exact", keywords: ["\u4EE3\u7801\u5BA1\u67E5", "code review", "review code", "\u98CE\u9669\u8BC4\u4F30"] },
8071
+ { type: "regex", pattern: "(\u4EE3\u7801|code).{0,6}(\u5BA1\u67E5|review|\u5BA1\u6838|\u68C0\u67E5)" }
8072
+ ],
8073
+ model: modelId
8074
+ },
8075
+ {
8076
+ name: "deep_reasoning",
8077
+ priority: 70,
8078
+ enabled: true,
8079
+ description: "\u590D\u6742\u63A8\u7406\u3001\u6DF1\u5165\u5206\u6790\u548C\u591A\u6B65\u51B3\u7B56\u4EFB\u52A1",
8080
+ patterns: [
8081
+ { type: "exact", keywords: ["\u6DF1\u5165\u5206\u6790", "\u590D\u6742\u63A8\u7406", "\u4E25\u8C28\u5206\u6790", "deep analysis", "reasoning"] },
8082
+ { type: "regex", pattern: "(\u6DF1\u5165|\u590D\u6742|\u4E25\u8C28).{0,6}(\u5206\u6790|\u63A8\u7406|\u8BBA\u8BC1)" }
8083
+ ],
8084
+ model: modelId
8085
+ }
8086
+ ];
8087
+ }
8088
+ function applyRoutingBootstrap(draft, choice, specializedModelId) {
8089
+ if (choice === "\u5148\u4FDD\u6301\u6700\u5C0F\u914D\u7F6E") {
8090
+ return draft;
8091
+ }
8092
+ const defaultModelId = draft.Router.default;
8093
+ if (!defaultModelId) {
8094
+ return draft;
8095
+ }
8096
+ const specializedModel = draft.Models?.find((item) => item.id === specializedModelId);
8097
+ if (!specializedModel) {
8098
+ return draft;
8099
+ }
8100
+ const nextDraft = {
8101
+ ...draft,
8102
+ SmartRouter: {
8103
+ enabled: true,
8104
+ analysis_scope: "last_message",
8105
+ rules: createComplexTaskRules(specializedModelId),
8106
+ ...choice === "\u5F00\u542F\u590D\u6742\u4EFB\u52A1\u89C4\u5219 + \u667A\u80FD\u515C\u5E95" ? {
8107
+ router_model: defaultModelId,
8108
+ candidates: [
8109
+ {
8110
+ model: defaultModelId,
8111
+ description: "\u9ED8\u8BA4\u6A21\u578B\uFF0C\u9002\u5408\u901A\u7528\u7F16\u7A0B\u3001\u65E5\u5E38\u4FEE\u590D\u548C\u5FEB\u901F\u54CD\u5E94\u4EFB\u52A1"
8112
+ },
8113
+ {
8114
+ model: specializedModelId,
8115
+ 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`
8116
+ }
8117
+ ]
8118
+ } : {}
8119
+ }
8120
+ };
8121
+ return nextDraft;
8122
+ }
8123
+ async function promptModelConnection(io, input3) {
8124
+ if (input3.intro) {
8125
+ io.info(input3.intro);
8126
+ }
8127
+ const modelId = await io.input(input3.modelIdPrompt, input3.suggestedModelId);
7215
8128
  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"]);
7216
8129
  let preset = "custom";
7217
8130
  let providerName = "provider";
@@ -7228,21 +8141,50 @@ async function buildFreshConfig(io) {
7228
8141
  const apiKey = await io.input("API Key");
7229
8142
  const presetDefinition = getProviderPreset(preset);
7230
8143
  const model = await io.input("\u4E0A\u6E38\u6A21\u578B\u540D", presetDefinition?.default_model ?? "");
7231
- const modelId = await io.input("\u9ED8\u8BA4\u6A21\u578B ID", toSuggestedModelId(providerName, model, preset));
7232
- const capabilityMode = await io.choose("\u662F\u5426\u914D\u7F6E capability \u63D0\u793A", ["\u4FDD\u6301\u9ED8\u8BA4", "\u914D\u7F6E capability \u63D0\u793A"]);
7233
- const draft = buildMinimalConfig({
7234
- providers: [
7235
- {
7236
- name: providerName,
7237
- model_id: modelId,
7238
- api_key: apiKey,
7239
- models: [model],
7240
- preset,
7241
- api_base_url: apiBaseUrl
7242
- }
7243
- ],
7244
- defaultModel: modelId
8144
+ const interfaceChoice = connectMode === "\u624B\u52A8\u586B\u5199\u63A5\u53E3" ? await io.choose("\u63A5\u53E3\u7C7B\u578B", ["openai", "anthropic"]) : presetDefinition?.interface;
8145
+ return {
8146
+ name: providerName,
8147
+ model_id: modelId,
8148
+ api_key: apiKey,
8149
+ interface: interfaceChoice,
8150
+ models: [model],
8151
+ preset,
8152
+ api_base_url: apiBaseUrl
8153
+ };
8154
+ }
8155
+ async function buildFreshConfig(io) {
8156
+ const primaryModel = await promptModelConnection(io, {
8157
+ intro: "\u6211\u4EEC\u5148\u521B\u5EFA\u4E00\u4EFD\u6700\u5C0F\u53EF\u7528\u914D\u7F6E\u3002",
8158
+ modelIdPrompt: "\u8FD9\u4E2A\u9ED8\u8BA4\u6A21\u578B\u5728\u672C\u5730\u8981\u53EB\u4EC0\u4E48\u540D\u5B57\uFF1F",
8159
+ suggestedModelId: "sonnet"
7245
8160
  });
8161
+ let draft = buildMinimalConfig({
8162
+ providers: [primaryModel],
8163
+ defaultModel: primaryModel.model_id
8164
+ });
8165
+ 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", [
8166
+ "\u5148\u4E0D\u6DFB\u52A0",
8167
+ "\u6DFB\u52A0\u4E00\u4E2A\u590D\u6742\u4EFB\u52A1\u4E13\u7528\u6A21\u578B"
8168
+ ]);
8169
+ if (addSecondModelChoice === "\u6DFB\u52A0\u4E00\u4E2A\u590D\u6742\u4EFB\u52A1\u4E13\u7528\u6A21\u578B") {
8170
+ const suggestedSecondModelId = toUniqueSuggestedModelId("reasoner", draft.Models?.map((item) => item.id) ?? []);
8171
+ const specializedModel = await promptModelConnection(io, {
8172
+ 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",
8173
+ modelIdPrompt: "\u8FD9\u4E2A\u590D\u6742\u4EFB\u52A1\u6A21\u578B\u5728\u672C\u5730\u8981\u53EB\u4EC0\u4E48\u540D\u5B57\uFF1F",
8174
+ suggestedModelId: suggestedSecondModelId
8175
+ });
8176
+ draft = appendModelToDraft(draft, specializedModel);
8177
+ const routingChoice = await io.choose("\u73B0\u5728\u8981\u4E0D\u8981\u5F00\u542F\u9AD8\u7EA7\u8DEF\u7531\uFF1F", [
8178
+ "\u5148\u4FDD\u6301\u6700\u5C0F\u914D\u7F6E",
8179
+ "\u5F00\u542F\u590D\u6742\u4EFB\u52A1\u89C4\u5219\u6A21\u677F",
8180
+ "\u5F00\u542F\u590D\u6742\u4EFB\u52A1\u89C4\u5219 + \u667A\u80FD\u515C\u5E95"
8181
+ ]);
8182
+ draft = applyRoutingBootstrap(draft, routingChoice, specializedModel.model_id);
8183
+ if (routingChoice !== "\u5148\u4FDD\u6301\u6700\u5C0F\u914D\u7F6E") {
8184
+ 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`);
8185
+ }
8186
+ }
8187
+ const capabilityMode = await io.choose("\u662F\u5426\u914D\u7F6E capability \u63D0\u793A", ["\u4FDD\u6301\u9ED8\u8BA4", "\u914D\u7F6E capability \u63D0\u793A"]);
7246
8188
  if (capabilityMode === "\u914D\u7F6E capability \u63D0\u793A" && draft.Models?.[0]) {
7247
8189
  await promptCapabilityMetadataForDraft(draft, io);
7248
8190
  }
@@ -7307,8 +8249,8 @@ function createDefaultDeps(io = createConsoleIO()) {
7307
8249
  }
7308
8250
  function printRoutingNextSteps(io) {
7309
8251
  io.info("\u4F60\u53EF\u4EE5\u6309\u9700\u7EE7\u7EED\u914D\u7F6E\u8DEF\u7531\u80FD\u529B\uFF1A");
7310
- io.info(" - TriggerRouter\uFF1A\u9002\u5408\u9AD8\u786E\u5B9A\u6027\u4EFB\u52A1\uFF0C\u628A\u67B6\u6784\u8BBE\u8BA1\u3001\u4EE3\u7801\u5BA1\u67E5\u7B49\u8BF7\u6C42\u56FA\u5B9A\u5207\u5230\u6307\u5B9A\u6A21\u578B");
7311
- io.info(" - SmartRouter\uFF1A\u9002\u5408\u6A21\u7CCA\u4EFB\u52A1\uFF0C\u5728\u5019\u9009\u6A21\u578B\u4E4B\u95F4\u81EA\u52A8\u9009\u62E9\u66F4\u5408\u9002\u7684\u6A21\u578B");
8252
+ 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");
8253
+ 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");
7312
8254
  io.info(" - \u914D\u7F6E\u6A21\u677F\u53C2\u8003\uFF1Aconfig/trigger.advanced.yaml");
7313
8255
  }
7314
8256
  async function runSetupCli(customDeps) {
@@ -7328,7 +8270,7 @@ async function runSetupCli(customDeps) {
7328
8270
  if (currentConfig.kind === "valid") {
7329
8271
  deps.io.info("\u68C0\u6D4B\u5230\u5F53\u524D claude-trigger-router \u914D\u7F6E\u5DF2\u53EF\u7528\u3002");
7330
8272
  if (currentConfig.warnings.length > 0) {
7331
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
8273
+ deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${formatConfigIssues({ warnings: currentConfig.warnings })}`);
7332
8274
  }
7333
8275
  return mapValidCurrentConfigChoice(
7334
8276
  await deps.io.choose("\u4F60\u60F3\u76F4\u63A5\u4F7F\u7528\u5B83\uFF0C\u8FD8\u662F\u91CD\u65B0\u8C03\u6574\uFF1F", [
@@ -7339,9 +8281,9 @@ async function runSetupCli(customDeps) {
7339
8281
  );
7340
8282
  }
7341
8283
  if (currentConfig.kind === "invalid") {
7342
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A${currentConfig.errors.join("; ")}`);
8284
+ deps.io.info(`\u5F53\u524D\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A${formatConfigIssues({ errors: currentConfig.errors })}`);
7343
8285
  if (currentConfig.warnings.length > 0) {
7344
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
8286
+ deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${formatConfigIssues({ warnings: currentConfig.warnings })}`);
7345
8287
  }
7346
8288
  return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["repair", "overwrite", "cancel"]);
7347
8289
  }
@@ -7394,7 +8336,7 @@ async function runSetupCli(customDeps) {
7394
8336
  writeConfig: deps.writeConfig
7395
8337
  });
7396
8338
  if (normalized.warnings.length > 0) {
7397
- deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${normalized.warnings.join("; ")}`);
8339
+ deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${formatConfigIssues({ warnings: normalized.warnings })}`);
7398
8340
  }
7399
8341
  return persisted;
7400
8342
  },
@@ -7449,6 +8391,7 @@ var init_setup2 = __esm({
7449
8391
  init_provider_presets();
7450
8392
  init_service_health();
7451
8393
  init_utils();
8394
+ init_validation_contract();
7452
8395
  init_processCheck();
7453
8396
  init_service();
7454
8397
  init_repair();
@@ -7620,12 +8563,8 @@ function createConsoleIO2() {
7620
8563
  function getConfigCandidates() {
7621
8564
  return [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
7622
8565
  }
7623
- function inferInterfaceFromApi(api) {
7624
- const trimmed = api?.trim();
7625
- if (!trimmed) {
7626
- return void 0;
7627
- }
7628
- return trimmed.includes("/v1/messages") ? "anthropic" : "openai";
8566
+ function inferInterfaceFromApi(api, modelName) {
8567
+ return inferInterfaceFromApiEndpoint(api, modelName);
7629
8568
  }
7630
8569
  function sanitizeModelId(value) {
7631
8570
  return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "model";
@@ -7693,7 +8632,7 @@ function repairDeterministicConfig(config) {
7693
8632
  nextConfig.Models = config.Models.map((item, index) => {
7694
8633
  const api = getModelApi(item);
7695
8634
  const key = getModelKey(item);
7696
- const inferredInterface = getModelInterface(item) ?? inferInterfaceFromApi(api);
8635
+ const inferredInterface = getModelInterface(item) ?? inferInterfaceFromApi(api, item.model);
7697
8636
  const id = item.id?.trim() || (item.model ? sanitizeModelId(item.model) : `model_${index + 1}`);
7698
8637
  if (!item.id?.trim()) {
7699
8638
  changes.push(`\u5DF2\u8865\u5168 Models[${index}].id -> ${id}`);
@@ -8021,11 +8960,11 @@ async function runDoctorCli(customDeps) {
8021
8960
  completed.changes.forEach((message) => deps.io.info(message));
8022
8961
  const normalized = normalizeAndValidateConfig(workingConfig);
8023
8962
  if (normalized.errors.length > 0) {
8024
- deps.io.error(`doctor \u4ECD\u53D1\u73B0\u65E0\u6CD5\u81EA\u52A8\u4FEE\u590D\u7684\u914D\u7F6E\u9519\u8BEF\uFF1A${normalized.errors.join("; ")}`);
8963
+ deps.io.error(`doctor \u4ECD\u53D1\u73B0\u65E0\u6CD5\u81EA\u52A8\u4FEE\u590D\u7684\u914D\u7F6E\u9519\u8BEF\uFF1A${formatValidationIssueReport(buildValidationIssueReport({ errors: normalized.errors })).join("; ")}`);
8025
8964
  throw new Error("doctor could not fully repair config");
8026
8965
  }
8027
8966
  if (normalized.warnings.length > 0) {
8028
- deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${normalized.warnings.join("; ")}`);
8967
+ deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${formatValidationIssueReport(buildValidationIssueReport({ warnings: normalized.warnings })).join("; ")}`);
8029
8968
  }
8030
8969
  const registry = buildModelRegistry(normalized.config);
8031
8970
  for (const model of normalized.config.Models ?? []) {
@@ -8100,6 +9039,7 @@ var init_doctor = __esm({
8100
9039
  import_js_yaml2 = __toESM(require("js-yaml"));
8101
9040
  init_constants();
8102
9041
  init_utils();
9042
+ init_validation_contract();
8103
9043
  init_migrate();
8104
9044
  init_setup2();
8105
9045
  init_schema();
@@ -8503,7 +9443,8 @@ async function runClaudeCode() {
8503
9443
  shell: isWindows,
8504
9444
  env: {
8505
9445
  ...process.env,
8506
- ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`
9446
+ ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
9447
+ ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "ctr-local-proxy"
8507
9448
  }
8508
9449
  });
8509
9450
  claude.on("error", (error) => {