@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/README.md +198 -177
- package/config/trigger.advanced.yaml +16 -20
- package/dist/cli.js +1282 -341
- package/dist/cli.js.map +4 -4
- package/package.json +74 -74
package/dist/cli.js
CHANGED
|
@@ -32,7 +32,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
32
32
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
33
33
|
|
|
34
34
|
// src/constants.ts
|
|
35
|
-
var import_os, import_path, CONFIG_DIR, CONFIG_FILE, CONFIG_FILE_JSON, CONFIG_FILE_YML, HOME_DIR, PID_FILE, GOVERNANCE_TRACE_FILE, GOVERNANCE_TRACE_ARCHIVE_DIR, GOVERNANCE_EXPORT_HISTORY_FILE, GOVERNANCE_SNAPSHOT_DIR, GOVERNANCE_SCHEDULE_FILE, DEFAULT_CONFIG2,
|
|
35
|
+
var import_os, import_path, CONFIG_DIR, CONFIG_FILE, CONFIG_FILE_JSON, CONFIG_FILE_YML, HOME_DIR, PID_FILE, GOVERNANCE_TRACE_FILE, GOVERNANCE_TRACE_ARCHIVE_DIR, GOVERNANCE_EXPORT_HISTORY_FILE, GOVERNANCE_SNAPSHOT_DIR, GOVERNANCE_SCHEDULE_FILE, DEFAULT_CONFIG2, DEFAULT_SMART_ROUTER_CONFIG, DEFAULT_GOVERNANCE_CONFIG;
|
|
36
36
|
var init_constants = __esm({
|
|
37
37
|
"src/constants.ts"() {
|
|
38
38
|
"use strict";
|
|
@@ -57,14 +57,9 @@ var init_constants = __esm({
|
|
|
57
57
|
API_TIMEOUT_MS: 6e5,
|
|
58
58
|
NON_INTERACTIVE_MODE: false
|
|
59
59
|
};
|
|
60
|
-
DEFAULT_TRIGGER_CONFIG = {
|
|
61
|
-
enabled: true,
|
|
62
|
-
analysis_scope: "last_message",
|
|
63
|
-
llm_intent_recognition: false,
|
|
64
|
-
rules: []
|
|
65
|
-
};
|
|
66
60
|
DEFAULT_SMART_ROUTER_CONFIG = {
|
|
67
61
|
enabled: false,
|
|
62
|
+
analysis_scope: "last_message",
|
|
68
63
|
router_model: "",
|
|
69
64
|
candidates: [],
|
|
70
65
|
cache_ttl: 6e5,
|
|
@@ -130,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
|
-
|
|
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
|
|
204
|
+
return trimString(item.key) || trimString(item.api_key) || "";
|
|
138
205
|
}
|
|
139
206
|
function getModelInterface(item) {
|
|
140
|
-
|
|
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
|
|
182
|
-
const
|
|
183
|
-
const
|
|
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
|
-
...
|
|
186
|
-
id:
|
|
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:
|
|
194
|
-
thinking: normalizeThinkingConfig(
|
|
195
|
-
metadata:
|
|
196
|
-
...
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
686
|
-
if (
|
|
687
|
-
|
|
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 (
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
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 (
|
|
720
|
-
|
|
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
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
|
923
|
-
router_model: decision
|
|
924
|
-
candidates: decision
|
|
925
|
-
cache_ttl: decision
|
|
926
|
-
max_tokens: decision
|
|
927
|
-
fallback: decision
|
|
928
|
-
router_hint: decision
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
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.
|
|
963
|
-
normalizedConfig.
|
|
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 (
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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=>({ '&':'&','<':'<','>':'>','"':'"' }[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",
|
|
3146
|
-
pushRef("
|
|
3147
|
-
|
|
3148
|
-
pushRef(`
|
|
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
|
-
|
|
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("
|
|
3155
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
|
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:
|
|
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 {
|
|
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=>({ '&':'&','<':'<','>':'>','"':'"' }[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
|
-
|
|
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,
|
|
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,
|
|
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: "
|
|
5687
|
+
routeSource: "smart_rule"
|
|
4925
5688
|
};
|
|
4926
5689
|
}
|
|
4927
|
-
const stickyCorrection = this.getStickyCorrection(text, req,
|
|
4928
|
-
const semanticCandidates = this.buildSemanticCandidates(
|
|
4929
|
-
if (
|
|
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
|
-
...
|
|
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 =
|
|
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,
|
|
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,
|
|
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: "
|
|
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
|
-
|
|
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,
|
|
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
|
|
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
|
|
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.
|
|
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.
|
|
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 === "
|
|
5192
|
-
appendTraceReason(req.governanceTrace, `
|
|
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, "
|
|
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, "
|
|
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.
|
|
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
|
-
`[
|
|
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("[
|
|
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(`[
|
|
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
|
|
5729
|
-
const triggerResult =
|
|
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 (!
|
|
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 (
|
|
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
|
-
`[
|
|
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
|
-
|
|
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
|
|
7207
|
-
const
|
|
7208
|
-
if (
|
|
7209
|
-
return
|
|
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
|
-
|
|
7212
|
-
return source.toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "model";
|
|
8029
|
+
return `${normalizedPreferredId}_${suffix}`;
|
|
7213
8030
|
}
|
|
7214
|
-
|
|
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
|
|
7232
|
-
|
|
7233
|
-
|
|
7234
|
-
|
|
7235
|
-
|
|
7236
|
-
|
|
7237
|
-
|
|
7238
|
-
|
|
7239
|
-
|
|
7240
|
-
|
|
7241
|
-
|
|
7242
|
-
|
|
7243
|
-
|
|
7244
|
-
|
|
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(" -
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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) => {
|