@peterwangze/claude-trigger-router 1.0.4 → 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -224,6 +224,63 @@ function inferTransformer(protocol) {
224
224
  }
225
225
  return void 0;
226
226
  }
227
+ function inferCompatibilityProfile(item, modelInterface) {
228
+ if (modelInterface === "anthropic") {
229
+ return "anthropic-native";
230
+ }
231
+ return "openai-compatible-anthropic-dispatch";
232
+ }
233
+ function getDispatchFormatForProfile(modelInterface, compatibilityProfile) {
234
+ if (modelInterface === "anthropic") {
235
+ return "anthropic_messages";
236
+ }
237
+ switch (compatibilityProfile) {
238
+ case "openai-compatible-anthropic-dispatch":
239
+ return "anthropic_messages";
240
+ case "anthropic-native":
241
+ return "anthropic_messages";
242
+ default:
243
+ return "anthropic_messages";
244
+ }
245
+ }
246
+ function describeCompatibilityProfile(profile) {
247
+ switch (profile) {
248
+ case "anthropic-native":
249
+ return {
250
+ label: "Anthropic native",
251
+ summary: "\u76EE\u6807\u63A5\u53E3\u539F\u751F\u63A5\u53D7 Anthropic messages \u5F62\u6001\uFF0C\u8BF7\u6C42\u65E0\u9700\u505A OpenAI-compatible \u517C\u5BB9\u8F6C\u6362\u3002"
252
+ };
253
+ case "openai-compatible-anthropic-dispatch":
254
+ return {
255
+ label: "OpenAI-compatible / Anthropic dispatch",
256
+ summary: "\u76EE\u6807\u63A5\u53E3\u5C5E\u4E8E OpenAI-compatible \u517C\u5BB9\u65CF\uFF0C\u8FD0\u884C\u65F6\u4F1A\u81EA\u52A8\u4F7F\u7528 Anthropic-style dispatch \u5904\u7406 tools\u3001messages \u4E0E\u63A7\u5236\u5B57\u6BB5\u5DEE\u5F02\u3002"
257
+ };
258
+ default:
259
+ return {
260
+ label: profile,
261
+ summary: "\u672A\u77E5\u517C\u5BB9\u753B\u50CF\u3002"
262
+ };
263
+ }
264
+ }
265
+ function describeDispatchFormat(format) {
266
+ switch (format) {
267
+ case "anthropic_messages":
268
+ return {
269
+ label: "Anthropic-style messages",
270
+ summary: "\u8FD0\u884C\u65F6\u4F1A\u628A\u7EDF\u4E00\u8BF7\u6C42\u7F16\u8BD1\u6210 Anthropic messages \u5F62\u6001\u540E\u518D\u53D1\u5F80\u76EE\u6807\u63A5\u53E3\u3002"
271
+ };
272
+ case "openai_chat":
273
+ return {
274
+ label: "OpenAI chat completions",
275
+ summary: "\u8FD0\u884C\u65F6\u4F1A\u628A\u7EDF\u4E00\u8BF7\u6C42\u7F16\u8BD1\u6210 OpenAI chat completions \u5F62\u6001\u540E\u518D\u53D1\u5F80\u76EE\u6807\u63A5\u53E3\u3002"
276
+ };
277
+ default:
278
+ return {
279
+ label: format,
280
+ summary: "\u672A\u77E5 dispatch \u5F62\u6001\u3002"
281
+ };
282
+ }
283
+ }
227
284
  function buildCompiledCapabilities(item, modelInterface) {
228
285
  const reasoningSupported = item.metadata?.supports_reasoning !== false;
229
286
  return {
@@ -255,12 +312,15 @@ function buildModelRegistry(config) {
255
312
  const modelMap2 = config.Models.reduce((result, rawItem) => {
256
313
  const item = normalizeModelEndpointConfig(rawItem);
257
314
  const modelInterface = getModelInterface(item) || "openai";
315
+ const compatibilityProfile = inferCompatibilityProfile(item, modelInterface);
258
316
  result[item.id] = {
259
317
  id: item.id,
260
318
  providerName: `model__${item.id}`,
261
319
  modelName: item.model,
262
320
  interface: modelInterface,
263
321
  protocol: modelInterface,
322
+ compatibilityProfile,
323
+ dispatchFormat: getDispatchFormatForProfile(modelInterface, compatibilityProfile),
264
324
  thinking: item.thinking,
265
325
  capabilities: buildCompiledCapabilities(item, modelInterface),
266
326
  source: "models"
@@ -275,12 +335,18 @@ function buildModelRegistry(config) {
275
335
  const providers = config.Providers ?? [];
276
336
  const modelMap = providers.reduce((result, provider) => {
277
337
  for (const model of provider.models ?? []) {
338
+ const compatibilityProfile = inferCompatibilityProfile(
339
+ { api_base_url: provider.api_base_url },
340
+ "openai"
341
+ );
278
342
  result[`${provider.name},${model}`] = {
279
343
  id: `${provider.name},${model}`,
280
344
  providerName: provider.name,
281
345
  modelName: model,
282
346
  interface: "openai",
283
347
  protocol: "openai",
348
+ compatibilityProfile,
349
+ dispatchFormat: getDispatchFormatForProfile("openai", compatibilityProfile),
284
350
  capabilities: {
285
351
  thinking: {
286
352
  supported: true
@@ -452,6 +518,9 @@ async function initDir() {
452
518
  if (!(0, import_fs.existsSync)(CONFIG_DIR)) {
453
519
  (0, import_fs.mkdirSync)(CONFIG_DIR, { recursive: true });
454
520
  }
521
+ if (!(0, import_fs.existsSync)(HOME_DIR)) {
522
+ (0, import_fs.mkdirSync)(HOME_DIR, { recursive: true });
523
+ }
455
524
  }
456
525
  async function loadYamlConfig(path) {
457
526
  if (!(0, import_fs.existsSync)(path)) {
@@ -525,6 +594,12 @@ function validateModelRef(ref, providers, fieldName) {
525
594
  }
526
595
  return null;
527
596
  }
597
+ function validateKnownModelRef(ref, config, providers, fieldName) {
598
+ if (isKnownModelReference(config, ref)) {
599
+ return null;
600
+ }
601
+ return validateModelRef(ref, providers, fieldName);
602
+ }
528
603
  function validateConfig(config) {
529
604
  const errors = [];
530
605
  if (config.Models !== void 0) {
@@ -601,7 +676,7 @@ function validateConfig(config) {
601
676
  ];
602
677
  for (const [ref, field] of routerModelFields) {
603
678
  if (ref) {
604
- const err = validateModelRef(ref, validProviders, field);
679
+ const err = validateKnownModelRef(ref, config, validProviders, field);
605
680
  if (err) errors.push(err);
606
681
  }
607
682
  }
@@ -611,7 +686,7 @@ function validateConfig(config) {
611
686
  if (config.TriggerRouter.llm_intent_recognition && !config.TriggerRouter.intent_model) {
612
687
  errors.push("TriggerRouter.intent_model is required when llm_intent_recognition is enabled");
613
688
  } else if (config.TriggerRouter.intent_model && validProviders.length > 0) {
614
- const err = validateModelRef(config.TriggerRouter.intent_model, validProviders, "TriggerRouter.intent_model");
689
+ const err = validateKnownModelRef(config.TriggerRouter.intent_model, config, validProviders, "TriggerRouter.intent_model");
615
690
  if (err) errors.push(err);
616
691
  }
617
692
  if (config.TriggerRouter.rules) {
@@ -622,10 +697,13 @@ function validateConfig(config) {
622
697
  if (!rule.model) {
623
698
  errors.push(`TriggerRouter.rules[${index}].model is required`);
624
699
  } else if (validProviders.length > 0) {
625
- const err = validateModelRef(rule.model, validProviders, `TriggerRouter.rules[${index}].model`);
700
+ const err = validateKnownModelRef(rule.model, config, validProviders, `TriggerRouter.rules[${index}].model`);
626
701
  if (err) errors.push(err);
627
702
  }
628
- if (!rule.patterns || rule.patterns.length === 0) {
703
+ const hasSemanticOnlyMatch = Boolean(
704
+ rule.description || rule.semantic_profile?.prototype || rule.semantic_profile?.enabled
705
+ );
706
+ if ((!rule.patterns || rule.patterns.length === 0) && !hasSemanticOnlyMatch) {
629
707
  errors.push(`TriggerRouter.rules[${index}].patterns must be a non-empty array`);
630
708
  }
631
709
  });
@@ -635,7 +713,7 @@ function validateConfig(config) {
635
713
  if (!config.SmartRouter.router_model) {
636
714
  errors.push("SmartRouter.router_model is required when SmartRouter is enabled");
637
715
  } else if (validProviders.length > 0) {
638
- const err = validateModelRef(config.SmartRouter.router_model, validProviders, "SmartRouter.router_model");
716
+ const err = validateKnownModelRef(config.SmartRouter.router_model, config, validProviders, "SmartRouter.router_model");
639
717
  if (err) errors.push(err);
640
718
  }
641
719
  if (!config.SmartRouter.candidates || config.SmartRouter.candidates.length < 2) {
@@ -645,7 +723,7 @@ function validateConfig(config) {
645
723
  if (!candidate.model) {
646
724
  errors.push(`SmartRouter.candidates[${index}].model is required`);
647
725
  } else if (validProviders.length > 0) {
648
- const err = validateModelRef(candidate.model, validProviders, `SmartRouter.candidates[${index}].model`);
726
+ const err = validateKnownModelRef(candidate.model, config, validProviders, `SmartRouter.candidates[${index}].model`);
649
727
  if (err) errors.push(err);
650
728
  }
651
729
  if (!candidate.description) {
@@ -772,7 +850,104 @@ function validateConfig(config) {
772
850
  }
773
851
  return errors;
774
852
  }
853
+ function normalizeUnifiedRouterInput(config) {
854
+ const routes = config.Router?.routes;
855
+ const decision = config.Router?.decision;
856
+ const defaults = config.Router?.defaults;
857
+ const hasUnifiedRouterInput = Boolean(
858
+ Array.isArray(routes) && routes.length || decision || defaults
859
+ );
860
+ if (!hasUnifiedRouterInput) {
861
+ return config;
862
+ }
863
+ const nextConfig = {
864
+ ...config,
865
+ Router: {
866
+ ...config.Router ?? {}
867
+ }
868
+ };
869
+ if (Array.isArray(routes) && routes.length > 0) {
870
+ const semanticExplicitlyEnabled = defaults?.semantic?.enabled;
871
+ nextConfig.TriggerRouter = {
872
+ ...config.TriggerRouter ?? DEFAULT_TRIGGER_CONFIG,
873
+ enabled: true,
874
+ rules: routes.map((route) => ({
875
+ name: route.name,
876
+ priority: route.priority ?? 0,
877
+ enabled: route.enabled ?? true,
878
+ model: route.model,
879
+ description: route.description,
880
+ semantic_profile: route.match?.semantic || route.match?.semantic_profile ? {
881
+ enabled: route.match?.semantic ?? true,
882
+ prototype: route.match?.semantic_profile?.prototype,
883
+ threshold: route.match?.semantic_profile?.threshold
884
+ } : void 0,
885
+ patterns: [
886
+ ...Array.isArray(route.match?.keywords) && route.match?.keywords.length ? [{
887
+ type: "exact",
888
+ keywords: route.match?.keywords
889
+ }] : [],
890
+ ...typeof route.match?.regex === "string" && route.match.regex.trim().length ? [{
891
+ type: "regex",
892
+ pattern: route.match.regex
893
+ }] : []
894
+ ]
895
+ }))
896
+ };
897
+ const semanticPrototypes = Object.fromEntries(
898
+ routes.filter((route) => route.match?.semantic || route.match?.semantic_profile?.prototype || route.description).map((route) => [
899
+ route.name,
900
+ route.match?.semantic_profile?.prototype ?? route.description ?? ""
901
+ ]).filter(([, prototype]) => typeof prototype === "string" && prototype.trim().length > 0)
902
+ );
903
+ if (Object.keys(semanticPrototypes).length > 0 || defaults?.semantic) {
904
+ nextConfig.Governance = {
905
+ ...config.Governance ?? DEFAULT_GOVERNANCE_CONFIG,
906
+ enabled: config.Governance?.enabled ?? (semanticExplicitlyEnabled !== void 0 ? semanticExplicitlyEnabled : Object.keys(semanticPrototypes).length > 0),
907
+ semantic: {
908
+ ...config.Governance?.semantic ?? {},
909
+ ...defaults?.semantic ?? {},
910
+ enabled: defaults?.semantic?.enabled ?? true,
911
+ prototypes: {
912
+ ...config.Governance?.semantic?.prototypes ?? {},
913
+ ...semanticPrototypes
914
+ }
915
+ }
916
+ };
917
+ }
918
+ }
919
+ if (decision) {
920
+ nextConfig.SmartRouter = {
921
+ ...config.SmartRouter ?? DEFAULT_SMART_ROUTER_CONFIG,
922
+ enabled: decision.smart_fallback ?? true,
923
+ router_model: decision.router_model ?? config.SmartRouter?.router_model ?? "",
924
+ candidates: decision.candidates ?? config.SmartRouter?.candidates ?? [],
925
+ cache_ttl: decision.cache_ttl ?? config.SmartRouter?.cache_ttl,
926
+ max_tokens: decision.max_tokens ?? config.SmartRouter?.max_tokens,
927
+ fallback: decision.fallback ?? config.SmartRouter?.fallback,
928
+ router_hint: decision.router_hint ?? config.SmartRouter?.router_hint
929
+ };
930
+ }
931
+ if (defaults?.sticky || defaults?.semantic) {
932
+ nextConfig.Governance = {
933
+ ...nextConfig.Governance ?? config.Governance ?? DEFAULT_GOVERNANCE_CONFIG,
934
+ enabled: config.Governance?.enabled ?? Boolean(
935
+ defaults?.sticky?.enabled || defaults?.semantic?.enabled || nextConfig.Governance?.semantic?.enabled
936
+ ),
937
+ sticky: defaults?.sticky ? {
938
+ ...config.Governance?.sticky ?? {},
939
+ ...defaults.sticky
940
+ } : config.Governance?.sticky,
941
+ semantic: defaults?.semantic ? {
942
+ ...nextConfig.Governance?.semantic ?? config.Governance?.semantic ?? {},
943
+ ...defaults.semantic
944
+ } : nextConfig.Governance?.semantic ?? config.Governance?.semantic
945
+ };
946
+ }
947
+ return nextConfig;
948
+ }
775
949
  function normalizeAndValidateConfig(config = {}) {
950
+ const normalizedInput = normalizeUnifiedRouterInput(config);
776
951
  const normalizedConfig = deepMerge(
777
952
  {
778
953
  ...DEFAULT_CONFIG2,
@@ -782,16 +957,16 @@ function normalizeAndValidateConfig(config = {}) {
782
957
  Providers: [],
783
958
  SmartRouter: DEFAULT_SMART_ROUTER_CONFIG
784
959
  },
785
- config
960
+ normalizedInput
786
961
  );
787
- if (config.TriggerRouter) {
788
- normalizedConfig.TriggerRouter = deepMerge(DEFAULT_TRIGGER_CONFIG, config.TriggerRouter);
962
+ if (normalizedInput.TriggerRouter) {
963
+ normalizedConfig.TriggerRouter = deepMerge(DEFAULT_TRIGGER_CONFIG, normalizedInput.TriggerRouter);
789
964
  }
790
- if (config.Governance) {
791
- normalizedConfig.Governance = deepMerge(DEFAULT_GOVERNANCE_CONFIG, config.Governance);
965
+ if (normalizedInput.Governance) {
966
+ normalizedConfig.Governance = deepMerge(DEFAULT_GOVERNANCE_CONFIG, normalizedInput.Governance);
792
967
  }
793
- if (config.Models) {
794
- normalizedConfig.Models = config.Models.map((item) => normalizeModelEndpointConfig(item));
968
+ if (normalizedInput.Models) {
969
+ normalizedConfig.Models = normalizedInput.Models.map((item) => normalizeModelEndpointConfig(item));
795
970
  }
796
971
  return {
797
972
  config: normalizedConfig,
@@ -930,6 +1105,31 @@ async function probeServiceHealth(port, timeoutMs = 500) {
930
1105
  return false;
931
1106
  }
932
1107
  }
1108
+ async function isTcpPortOccupied(port, timeoutMs = 500) {
1109
+ return new Promise((resolve) => {
1110
+ const socket = new import_net.Socket();
1111
+ let settled = false;
1112
+ const finish = (value) => {
1113
+ if (settled) {
1114
+ return;
1115
+ }
1116
+ settled = true;
1117
+ socket.destroy();
1118
+ resolve(value);
1119
+ };
1120
+ socket.setTimeout(timeoutMs);
1121
+ socket.once("connect", () => finish(true));
1122
+ socket.once("timeout", () => finish(false));
1123
+ socket.once("error", (error) => {
1124
+ if (error.code === "ECONNREFUSED") {
1125
+ finish(false);
1126
+ return;
1127
+ }
1128
+ finish(false);
1129
+ });
1130
+ socket.connect(port, "127.0.0.1");
1131
+ });
1132
+ }
933
1133
  async function waitForService(port, timeoutMs = 5e3) {
934
1134
  const start = Date.now();
935
1135
  while (Date.now() - start < timeoutMs) {
@@ -940,10 +1140,11 @@ async function waitForService(port, timeoutMs = 5e3) {
940
1140
  }
941
1141
  return false;
942
1142
  }
943
- var SERVICE_NAME, SERVICE_HEALTH_PATH;
1143
+ var import_net, SERVICE_NAME, SERVICE_HEALTH_PATH;
944
1144
  var init_service_health = __esm({
945
1145
  "src/service-health.ts"() {
946
1146
  "use strict";
1147
+ import_net = require("net");
947
1148
  SERVICE_NAME = "claude-trigger-router";
948
1149
  SERVICE_HEALTH_PATH = "/api/health";
949
1150
  }
@@ -957,25 +1158,25 @@ var init_types = __esm({
957
1158
  });
958
1159
 
959
1160
  // src/governance/trace.ts
960
- function createGovernanceTrace(input2 = {}) {
1161
+ function createGovernanceTrace(input3 = {}) {
961
1162
  return {
962
- requestId: input2.requestId ?? (0, import_crypto.randomUUID)(),
963
- sessionKey: input2.sessionKey,
964
- initialModel: input2.initialModel,
965
- finalModel: input2.finalModel,
966
- routeReason: input2.routeReason ? [...input2.routeReason] : [],
967
- stickyHit: input2.stickyHit ?? false,
968
- alignmentUsed: input2.alignmentUsed ?? false,
969
- semanticIntent: input2.semanticIntent,
970
- cascadeTriggered: input2.cascadeTriggered ?? false,
971
- cascadeEvidence: input2.cascadeEvidence ? [...input2.cascadeEvidence] : [],
972
- cascadeNextModel: input2.cascadeNextModel,
973
- shadowChecked: input2.shadowChecked ?? false,
974
- verificationResult: input2.verificationResult,
975
- latencyMs: input2.latencyMs,
976
- estimatedCost: input2.estimatedCost,
977
- startedAt: input2.startedAt ?? Date.now(),
978
- completedAt: input2.completedAt
1163
+ requestId: input3.requestId ?? (0, import_crypto.randomUUID)(),
1164
+ sessionKey: input3.sessionKey,
1165
+ initialModel: input3.initialModel,
1166
+ finalModel: input3.finalModel,
1167
+ routeReason: input3.routeReason ? [...input3.routeReason] : [],
1168
+ stickyHit: input3.stickyHit ?? false,
1169
+ alignmentUsed: input3.alignmentUsed ?? false,
1170
+ semanticIntent: input3.semanticIntent,
1171
+ cascadeTriggered: input3.cascadeTriggered ?? false,
1172
+ cascadeEvidence: input3.cascadeEvidence ? [...input3.cascadeEvidence] : [],
1173
+ cascadeNextModel: input3.cascadeNextModel,
1174
+ shadowChecked: input3.shadowChecked ?? false,
1175
+ verificationResult: input3.verificationResult,
1176
+ latencyMs: input3.latencyMs,
1177
+ estimatedCost: input3.estimatedCost,
1178
+ startedAt: input3.startedAt ?? Date.now(),
1179
+ completedAt: input3.completedAt
979
1180
  };
980
1181
  }
981
1182
  function appendTraceReason(trace, reason) {
@@ -1290,7 +1491,13 @@ function normalizeContentParts(content) {
1290
1491
  return typeof item.text === "string" ? [{ type: "text", text: item.text }] : [];
1291
1492
  case "image":
1292
1493
  case "image_url":
1293
- return [{ type: "image", source: item.source ?? item.image_url ?? item.url }];
1494
+ return [{
1495
+ type: "image",
1496
+ source: item.source ?? (item.image_url ? {
1497
+ ...item.image_url,
1498
+ ...item.media_type ? { media_type: item.media_type } : {}
1499
+ } : item.url)
1500
+ }];
1294
1501
  case "tool_use":
1295
1502
  return item.id && item.name ? [{ type: "tool_call", id: item.id, name: item.name, arguments: JSON.stringify(item.input ?? {}) }] : [];
1296
1503
  case "tool_result":
@@ -1300,16 +1507,58 @@ function normalizeContentParts(content) {
1300
1507
  }
1301
1508
  });
1302
1509
  }
1303
- function createMessageIR(input2) {
1304
- const system = typeof input2.system === "string" ? [input2.system] : Array.isArray(input2.system) ? input2.system.flatMap((item) => item?.type === "text" && typeof item.text === "string" ? [item.text] : []) : [];
1305
- const messages = Array.isArray(input2.messages) ? input2.messages.filter((item) => item?.role).map((item) => ({
1306
- role: item.role,
1307
- parts: normalizeContentParts(item.content)
1308
- })) : [];
1309
- const thinking = input2.thinking ? {
1310
- enabled: input2.thinking?.type === "enabled" || input2.thinking?.enabled === true,
1311
- effort: input2.thinking?.effort,
1312
- budget_tokens: input2.thinking?.budget_tokens
1510
+ function normalizeOpenAIToolCalls(toolCalls) {
1511
+ if (!Array.isArray(toolCalls)) {
1512
+ return [];
1513
+ }
1514
+ return toolCalls.flatMap((toolCall) => {
1515
+ const id = toolCall?.id;
1516
+ const name = toolCall?.function?.name;
1517
+ const args = toolCall?.function?.arguments;
1518
+ if (!id || !name) {
1519
+ return [];
1520
+ }
1521
+ return [{
1522
+ type: "tool_call",
1523
+ id,
1524
+ name,
1525
+ arguments: typeof args === "string" ? args : JSON.stringify(args ?? {})
1526
+ }];
1527
+ });
1528
+ }
1529
+ function createMessageIR(input3) {
1530
+ const system = typeof input3.system === "string" ? [input3.system] : Array.isArray(input3.system) ? input3.system.flatMap(
1531
+ (item) => typeof item === "string" ? [item] : item?.type === "text" && typeof item.text === "string" ? [item.text] : []
1532
+ ) : [];
1533
+ const messages = Array.isArray(input3.messages) ? input3.messages.filter((item) => item?.role).flatMap((item) => {
1534
+ if (item.role === "system") {
1535
+ const systemParts = typeof item.content === "string" ? [item.content] : normalizeContentParts(item.content).filter((part) => part.type === "text").map((part) => part.text);
1536
+ system.push(...systemParts);
1537
+ return [];
1538
+ }
1539
+ if (item.role === "tool" && item.tool_call_id) {
1540
+ return [{
1541
+ role: "user",
1542
+ parts: [{
1543
+ type: "tool_result",
1544
+ tool_call_id: item.tool_call_id,
1545
+ content: item.content
1546
+ }]
1547
+ }];
1548
+ }
1549
+ const parts = [
1550
+ ...normalizeContentParts(item.content),
1551
+ ...item.role === "assistant" ? normalizeOpenAIToolCalls(item.tool_calls) : []
1552
+ ];
1553
+ return [{
1554
+ role: item.role,
1555
+ parts
1556
+ }];
1557
+ }) : [];
1558
+ const thinking = input3.thinking ? {
1559
+ enabled: input3.thinking?.type === "enabled" || input3.thinking?.enabled === true,
1560
+ effort: input3.thinking?.effort,
1561
+ budget_tokens: input3.thinking?.budget_tokens
1313
1562
  } : void 0;
1314
1563
  return {
1315
1564
  system,
@@ -1361,34 +1610,53 @@ function toAnthropicContent(parts) {
1361
1610
  }
1362
1611
  });
1363
1612
  }
1364
- function toAnthropicMessagesRequest(input2) {
1613
+ function toAnthropicMessagesRequest(input3) {
1365
1614
  const body = {
1366
- model: input2.model,
1367
- messages: input2.ir.messages.map((message) => ({
1615
+ model: input3.model,
1616
+ messages: input3.ir.messages.map((message) => ({
1368
1617
  role: message.role,
1369
1618
  content: toAnthropicContent(message.parts)
1370
1619
  }))
1371
1620
  };
1372
- if (input2.max_tokens !== void 0) {
1373
- body.max_tokens = input2.max_tokens;
1621
+ if (input3.max_tokens !== void 0) {
1622
+ body.max_tokens = input3.max_tokens;
1374
1623
  }
1375
- if (input2.stream !== void 0) {
1376
- body.stream = input2.stream;
1624
+ if (input3.stream !== void 0) {
1625
+ body.stream = input3.stream;
1377
1626
  }
1378
- if (input2.metadata) {
1379
- body.metadata = input2.metadata;
1627
+ if (input3.metadata) {
1628
+ body.metadata = input3.metadata;
1629
+ }
1630
+ if (input3.tools) {
1631
+ body.tools = input3.tools.map((tool) => ({
1632
+ name: tool?.name ?? tool?.function?.name,
1633
+ description: tool?.description ?? tool?.function?.description,
1634
+ input_schema: tool?.input_schema ?? tool?.function?.parameters
1635
+ }));
1380
1636
  }
1381
- if (input2.tools) {
1382
- body.tools = input2.tools;
1637
+ if (input3.tool_choice) {
1638
+ if (typeof input3.tool_choice === "string") {
1639
+ body.tool_choice = input3.tool_choice;
1640
+ } else if (input3.tool_choice?.type === "tool" && input3.tool_choice?.name) {
1641
+ body.tool_choice = {
1642
+ type: "tool",
1643
+ name: input3.tool_choice.name
1644
+ };
1645
+ } else if (input3.tool_choice?.type === "function" && input3.tool_choice?.function?.name) {
1646
+ body.tool_choice = {
1647
+ type: "tool",
1648
+ name: input3.tool_choice.function.name
1649
+ };
1650
+ }
1383
1651
  }
1384
- if (input2.ir.system.length) {
1385
- body.system = input2.ir.system.map((text) => ({ type: "text", text }));
1652
+ if (input3.ir.system.length) {
1653
+ body.system = input3.ir.system.map((text) => ({ type: "text", text }));
1386
1654
  }
1387
- if (input2.ir.options?.thinking?.enabled) {
1655
+ if (input3.ir.options?.thinking?.enabled) {
1388
1656
  body.thinking = {
1389
1657
  type: "enabled",
1390
- ...input2.ir.options.thinking.effort ? { effort: input2.ir.options.thinking.effort } : {},
1391
- ...input2.ir.options.thinking.budget_tokens ? { budget_tokens: input2.ir.options.thinking.budget_tokens } : {}
1658
+ ...input3.ir.options.thinking.effort ? { effort: input3.ir.options.thinking.effort } : {},
1659
+ ...input3.ir.options.thinking.budget_tokens ? { budget_tokens: input3.ir.options.thinking.budget_tokens } : {}
1392
1660
  };
1393
1661
  }
1394
1662
  return body;
@@ -1713,6 +1981,37 @@ Return JSON only:
1713
1981
  }
1714
1982
  return best;
1715
1983
  }
1984
+ analyzeCandidates(text, candidates, defaultThreshold = 0.85) {
1985
+ const inputTokens = tokenize(text);
1986
+ if (inputTokens.length === 0 || !candidates.length) {
1987
+ return null;
1988
+ }
1989
+ const inputVector = buildVector(inputTokens);
1990
+ let best = null;
1991
+ let bestThreshold = defaultThreshold;
1992
+ for (const candidate of candidates) {
1993
+ const prototypeTokens = tokenize(candidate.prototype);
1994
+ if (prototypeTokens.length === 0) {
1995
+ continue;
1996
+ }
1997
+ const prototypeVector = buildVector(prototypeTokens);
1998
+ const matched = prototypeTokens.filter((token) => inputTokens.includes(token));
1999
+ const confidence = cosineSimilarity(inputVector, prototypeVector);
2000
+ if (!best || confidence > best.confidence) {
2001
+ best = {
2002
+ intent: candidate.intent,
2003
+ confidence,
2004
+ matchedPrototype: candidate.prototype,
2005
+ evidence: matched
2006
+ };
2007
+ bestThreshold = candidate.threshold ?? defaultThreshold;
2008
+ }
2009
+ }
2010
+ if (!best || best.confidence < bestThreshold) {
2011
+ return null;
2012
+ }
2013
+ return best;
2014
+ }
1716
2015
  analyze(text, config) {
1717
2016
  return this.analyzeEmbedding(text, config);
1718
2017
  }
@@ -2121,17 +2420,17 @@ var init_SSEParser_transform = __esm({
2121
2420
 
2122
2421
  // src/governance/stream-response-governance.ts
2123
2422
  function serializeEvent(event2) {
2124
- let output2 = "";
2423
+ let output3 = "";
2125
2424
  if (event2.event) {
2126
- output2 += `event: ${event2.event}
2425
+ output3 += `event: ${event2.event}
2127
2426
  `;
2128
2427
  }
2129
2428
  if (event2.data !== void 0) {
2130
- output2 += `data: ${typeof event2.data === "string" ? event2.data : JSON.stringify(event2.data)}
2429
+ output3 += `data: ${typeof event2.data === "string" ? event2.data : JSON.stringify(event2.data)}
2131
2430
  `;
2132
2431
  }
2133
- output2 += "\n";
2134
- return new TextEncoder().encode(output2);
2432
+ output3 += "\n";
2433
+ return new TextEncoder().encode(output3);
2135
2434
  }
2136
2435
  async function collectSSE(stream) {
2137
2436
  const parser = new SSEParserTransform();
@@ -2241,17 +2540,17 @@ var init_stream_response_governance = __esm({
2241
2540
  });
2242
2541
 
2243
2542
  // src/governance/metrics.ts
2244
- function normalizeAnomalyThresholds(input2) {
2543
+ function normalizeAnomalyThresholds(input3) {
2245
2544
  return {
2246
- minSampleSize: input2?.minSampleSize ?? DEFAULT_ANOMALY_THRESHOLDS.minSampleSize,
2247
- cascadeWarnRate: input2?.cascadeWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.cascadeWarnRate,
2248
- cascadeCriticalRate: input2?.cascadeCriticalRate ?? DEFAULT_ANOMALY_THRESHOLDS.cascadeCriticalRate,
2249
- shadowWarnRate: input2?.shadowWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.shadowWarnRate,
2250
- shadowCriticalRate: input2?.shadowCriticalRate ?? DEFAULT_ANOMALY_THRESHOLDS.shadowCriticalRate,
2251
- latencyWarnMs: input2?.latencyWarnMs ?? DEFAULT_ANOMALY_THRESHOLDS.latencyWarnMs,
2252
- latencyCriticalMs: input2?.latencyCriticalMs ?? DEFAULT_ANOMALY_THRESHOLDS.latencyCriticalMs,
2253
- spikeWarnRate: input2?.spikeWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.spikeWarnRate,
2254
- spikeDeltaRate: input2?.spikeDeltaRate ?? DEFAULT_ANOMALY_THRESHOLDS.spikeDeltaRate
2545
+ minSampleSize: input3?.minSampleSize ?? DEFAULT_ANOMALY_THRESHOLDS.minSampleSize,
2546
+ cascadeWarnRate: input3?.cascadeWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.cascadeWarnRate,
2547
+ cascadeCriticalRate: input3?.cascadeCriticalRate ?? DEFAULT_ANOMALY_THRESHOLDS.cascadeCriticalRate,
2548
+ shadowWarnRate: input3?.shadowWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.shadowWarnRate,
2549
+ shadowCriticalRate: input3?.shadowCriticalRate ?? DEFAULT_ANOMALY_THRESHOLDS.shadowCriticalRate,
2550
+ latencyWarnMs: input3?.latencyWarnMs ?? DEFAULT_ANOMALY_THRESHOLDS.latencyWarnMs,
2551
+ latencyCriticalMs: input3?.latencyCriticalMs ?? DEFAULT_ANOMALY_THRESHOLDS.latencyCriticalMs,
2552
+ spikeWarnRate: input3?.spikeWarnRate ?? DEFAULT_ANOMALY_THRESHOLDS.spikeWarnRate,
2553
+ spikeDeltaRate: input3?.spikeDeltaRate ?? DEFAULT_ANOMALY_THRESHOLDS.spikeDeltaRate
2255
2554
  };
2256
2555
  }
2257
2556
  function rate(count, total) {
@@ -2956,6 +3255,8 @@ function diffCompiledRegistry(base, next) {
2956
3255
  before?.providerName !== after?.providerName ? "providerName" : null,
2957
3256
  before?.modelName !== after?.modelName ? "modelName" : null,
2958
3257
  before?.protocol !== after?.protocol ? "protocol" : null,
3258
+ before?.compatibilityProfile !== after?.compatibilityProfile ? "compatibilityProfile" : null,
3259
+ before?.dispatchFormat !== after?.dispatchFormat ? "dispatchFormat" : null,
2959
3260
  JSON.stringify(before?.thinking ?? {}) !== JSON.stringify(after?.thinking ?? {}) ? "thinking" : null,
2960
3261
  JSON.stringify(before?.capabilities ?? {}) !== JSON.stringify(after?.capabilities ?? {}) ? "capabilities" : null,
2961
3262
  before?.source !== after?.source ? "source" : null
@@ -3262,7 +3563,7 @@ var init_server = __esm({
3262
3563
  server.app.post("/api/restart", async (req, reply) => {
3263
3564
  reply.send({ success: true, message: "Service restart initiated" });
3264
3565
  setTimeout(() => {
3265
- const { spawn: spawn2 } = require("child_process");
3566
+ const { spawn: spawn3 } = require("child_process");
3266
3567
  const { join: join8 } = require("path");
3267
3568
  const cliPath = join8(__dirname, "cli.js");
3268
3569
  const currentPort = config.initialConfig?.PORT;
@@ -3270,7 +3571,7 @@ var init_server = __esm({
3270
3571
  if (currentPort) {
3271
3572
  restartArgs.push("--port", String(currentPort));
3272
3573
  }
3273
- spawn2(process.execPath, restartArgs, {
3574
+ spawn3(process.execPath, restartArgs, {
3274
3575
  detached: true,
3275
3576
  stdio: "ignore"
3276
3577
  }).unref();
@@ -3280,7 +3581,7 @@ var init_server = __esm({
3280
3581
  server.app.get("/ui", async (_, reply) => {
3281
3582
  reply.header("Content-Type", "text/html; charset=utf-8");
3282
3583
  return reply.send(
3283
- `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Claude Trigger Router</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:1rem;padding:.45rem 0;border-bottom:1px dashed #e5e7eb}.mini-list li:last-child{border-bottom:none}.action-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center;margin-top:.75rem}.management-table{width:100%;margin-top:.75rem}.management-table th,.management-table td{padding:.5rem;border-bottom:1px solid #e5e7eb;font-size:.92rem;vertical-align:top}.alert-list{display:grid;gap:.75rem;margin-top:1rem}.alert{border-radius:12px;padding:.85rem 1rem;border:1px solid}.alert.warn{background:#fff7ed;border-color:#fdba74;color:#9a3412}.alert.critical{background:#fef2f2;border-color:#fca5a5;color:#991b1b}.alert.info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.diff-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-top:.75rem}.diff-chip{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.75rem}.diff-chip strong{display:block;font-size:1rem;margin-top:.2rem}.models-form-grid{display:grid;gap:.75rem;margin-top:.75rem}.model-card{border:1px solid #e5e7eb;border-radius:12px;padding:1rem;background:#fcfcfd}.model-card-header{display:flex;justify-content:space-between;gap:1rem;align-items:center;margin-bottom:.75rem}.model-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.model-card-grid textarea{min-height:84px;resize:vertical}.list-editor{display:grid;gap:.75rem;margin-top:.75rem}.list-item{border:1px solid #e5e7eb;border-radius:12px;padding:.85rem;background:#fcfcfd}.list-item-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.jump-highlight{outline:3px solid #f59e0b;box-shadow:0 0 0 6px rgba(245,158,11,.15);transition:box-shadow .25s ease,outline-color .25s ease}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.control-grid label{display:block;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}.trend-table{width:100%;margin-top:.75rem}.trend-table th,.trend-table td{padding:.45rem;border-bottom:1px solid #e5e7eb;font-size:.92rem}.row{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}input,select,button{font:inherit;padding:.55rem .75rem;border-radius:8px;border:1px solid #d1d5db}button{background:#111827;color:#fff;border-color:#111827;cursor:pointer}table{width:100%;border-collapse:collapse;margin-top:1rem}th,td{text-align:left;padding:.65rem .5rem;border-bottom:1px solid #e5e7eb;vertical-align:top}code,pre{font-family:ui-monospace,SFMono-Regular,monospace}pre{white-space:pre-wrap;background:#0f172a;color:#e2e8f0;padding:1rem;border-radius:12px;overflow:auto}.pill{display:inline-block;padding:.2rem .5rem;border-radius:999px;background:#eef2ff;color:#3730a3;font-size:.8rem}</style></head><body><h2>Claude Trigger Router</h2><p class="muted">\u7B80\u6613 Governance Trace \u8C03\u8BD5\u9875\u3002\u53EF\u67E5\u770B\u6700\u8FD1\u6CBB\u7406\u94FE\u8DEF\uFF0C\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4\uFF0C\u5E76\u6309 cascade / shadow \u72B6\u6001\u7B5B\u9009\uFF1B\u6CBB\u7406 trace \u73B0\u5DF2\u652F\u6301\u672C\u5730\u6301\u4E45\u5316\uFF0C\u91CD\u542F\u540E\u53EF\u7EE7\u7EED\u67E5\u770B\u8FD1\u671F\u7A97\u53E3\u3002</p><div class="panel"><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Trigger rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u9996\u6279\u8868\u5355\u5316\u7F16\u8F91 TriggerRouter / SmartRouter / Governance \u7684\u6838\u5FC3\u5F15\u7528</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>TriggerRouter</strong><span class="muted">\u89C4\u5219\u8DEF\u7531\u4E0E\u610F\u56FE\u8BC6\u522B</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No trigger rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5BF9\u9F50\u3001\u8BED\u4E49\u3001\u5F71\u5B50\u6821\u9A8C\u4E0E\u7EA7\u8054</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div id="modelsFormGrid" class="models-form-grid"><div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div></div><textarea id="configDraftEditor" style="width:100%;min-height:240px;margin-top:.75rem;padding:.75rem;border-radius:12px;border:1px solid #d1d5db;font:12px/1.5 ui-monospace,SFMono-Regular,monospace" spellcheck="false" placeholder='{"Models":[{"id":"sonnet","api":"https://...","key":"sk-...","interface":"openai","model":"anthropic/claude-sonnet-4"}]}'></textarea><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / TriggerRouter / Governance \u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading model map...</td></tr></tbody></table></div></div></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Recent traces</span><strong>-</strong></div><div class="stat"><span class="muted">Sticky hit rate</span><strong>-</strong></div><div class="stat"><span class="muted">Cascade rate</span><strong>-</strong></div><div class="stat"><span class="muted">Shadow rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment rate</span><strong>-</strong></div><div class="stat"><span class="muted">Avg latency</span><strong>-</strong></div></div><div class="subpanel"><div class="row"><strong>Anomaly alerts</strong><span class="muted">\u68C0\u6D4B\u8FD1\u671F\u6CBB\u7406\u5F02\u5E38\u4E0E\u7A81\u589E</span></div><div id="anomalyList" class="alert-list"><div class="alert info"><strong>No alerts yet</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u6307\u6807\u52A0\u8F7D</div></div></div></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${configuredThresholds.min_sample_size ?? 3}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${configuredThresholds.cascade_warn_rate ?? 0.4}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${configuredThresholds.shadow_warn_rate ?? 0.5}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${configuredThresholds.latency_warn_ms ?? 1500}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Trend detail</strong><span class="muted">\u6BCF\u4E2A bucket \u7684\u8BE6\u7EC6\u547D\u4E2D\u7387</span></div><table id="trendTable" class="trend-table"><thead><tr><th>Bucket</th><th>Traces</th><th>Sticky</th><th>Cascade</th><th>Shadow</th><th>Alignment</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading...</td></tr></tbody></table></div></div><table id="traceTable"><thead><tr><th>Request</th><th>Session</th><th>Final Model</th><th>Reasons</th><th>Latency</th><th>Inspect</th></tr></thead><tbody><tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Trace Detail</strong><span id="detailHint" class="muted">\u70B9\u51FB\u4E0A\u8868\u4E2D\u7684 View \u67E5\u770B\u8BE6\u60C5</span></div><pre id="traceDetail">{}</pre></div><div class="panel"><div class="row"><strong>Snapshot Management</strong><span class="muted">\u67E5\u770B\u5BFC\u51FA\u5386\u53F2\u3001\u5B9A\u65F6\u4EFB\u52A1\uFF0C\u5E76\u624B\u52A8\u521B\u5EFA\u5FEB\u7167</span></div><div class="action-row"><select id="snapshotFormat"><option value="json">snapshot json</option><option value="csv">snapshot csv</option></select><button id="createSnapshotBtn" type="button">\u751F\u6210\u5FEB\u7167</button><span id="snapshotStatus" class="muted">\u5C1A\u672A\u521B\u5EFA\u5FEB\u7167</span></div><table id="exportTable" class="management-table"><thead><tr><th>Export</th><th>Kind</th><th>Format</th><th>Created</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading exports...</td></tr></tbody></table><table id="scheduleTable" class="management-table"><thead><tr><th>Schedule</th><th>Interval</th><th>Format</th><th>Last run</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading schedules...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Archive Management</strong><span class="muted">\u6D4F\u89C8\u538B\u7F29\u5F52\u6863\u5E76\u67E5\u770B\u5206\u9875\u7ED3\u679C</span></div><div class="action-row"><input id="archiveDate" placeholder="YYYY-MM-DD"><input id="archivePage" placeholder="page" value="1"><input id="archivePageSize" placeholder="pageSize" value="5"><button id="loadArchivesBtn" type="button">\u52A0\u8F7D\u5F52\u6863</button><span id="archiveStatus" class="muted">\u5C1A\u672A\u52A0\u8F7D\u5F52\u6863</span></div><table id="archiveTable" class="management-table"><thead><tr><th>Archive</th><th>Range</th><th>Count</th><th>Compressed</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading archives...</td></tr></tbody></table></div><div class="panel"><p>\u5176\u4ED6\u7BA1\u7406 API\uFF1A</p><ul><li><code>GET /api/config</code> \u2014 \u8BFB\u53D6\u5F53\u524D\u914D\u7F6E</li><li><code>GET /api/models/compiled</code> \u2014 \u67E5\u770B Models \u7F16\u8BD1\u540E\u7684\u5185\u90E8 provider / model \u6620\u5C04</li><li><code>POST /api/models/compiled/preview</code> \u2014 \u7528\u914D\u7F6E\u8349\u7A3F\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u5199\u56DE\u6587\u4EF6</li><li><code>POST /api/config</code> \u2014 \u4FDD\u5B58\u914D\u7F6E</li><li><code>GET /api/transformers</code> \u2014 \u67E5\u770B\u5DF2\u52A0\u8F7D transformer</li><li><code>POST /api/restart</code> \u2014 \u91CD\u542F\u670D\u52A1</li><li><code>GET /api/governance/archives</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5F52\u6863\u5217\u8868</li><li><code>GET /api/governance/archives/:file</code> \u2014 \u67E5\u770B\u5F52\u6863\u5185 traces</li><li><code>POST /api/governance/archives/:file/delete</code> \u2014 \u5220\u9664\u6307\u5B9A\u5F52\u6863</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const metricsGrid=document.getElementById('metricsGrid');const bucketGrid=document.getElementById('bucketGrid');const bucketHint=document.getElementById('bucketHint');const routeRanking=document.getElementById('routeRanking');const modelRanking=document.getElementById('modelRanking');const intentRanking=document.getElementById('intentRanking');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');let currentDraftConfig={};let knownModelIds=[];let activeValidationHighlight=null;const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94 TriggerRule\u3002', affects:['Router.default','TriggerRouter.enabled','TriggerRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u6838\u5FC3\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','Governance.sticky.alignment','Governance.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function inferProviderTemplateKey(model){ const explicit=String(model?.provider_template || '').trim(); if(explicit && modelProviderTemplates[explicit]){ return explicit; } const api=String(model?.api || model?.api_base_url || '').trim().toLowerCase(); const modelInterface=String(model?.interface || model?.protocol || '').trim().toLowerCase(); const exactMatch=Object.entries(modelProviderTemplates).find(([,item])=>String(item.api || '').trim().toLowerCase()===api && String(item.interface || '').trim().toLowerCase()===modelInterface); if(exactMatch){ return exactMatch[0]; } if(api.includes('api.anthropic.com/v1/messages') || modelInterface === 'anthropic'){ return 'anthropic'; } if(api.includes('openrouter.ai')){ return 'openrouter'; } if(api.includes('deepseek.com')){ return 'deepseek'; } if(api.includes('siliconflow.cn')){ return 'siliconflow'; } if(api.includes('api.openai.com')){ return 'openai-compatible'; } return '';}function getProviderTemplateContext(model){ const templateKey=inferProviderTemplateKey(model) || defaultProviderTemplateKey; return { templateKey, template:modelProviderTemplates[templateKey] || modelProviderTemplates[defaultProviderTemplateKey] || {} };}function createDraftModelFromTemplate(templateKey){ const resolvedKey=(templateKey && modelProviderTemplates[templateKey]) ? templateKey : defaultProviderTemplateKey; const template=modelProviderTemplates[resolvedKey] || {}; return { provider_template:resolvedKey, id:template.suggested_id || '', api:template.api || '', interface:template.interface || 'openai', model:template.default_model || '', thinking:template.default_thinking || 'auto' };}function getModelIdSuggestionsMarkup(idPrefix){ return '<datalist id="'+idPrefix+'">'+knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join('')+'</datalist>';}function resolvePresetModelId(seed){ const source=String(seed || '').trim().toLowerCase(); if(!source || !knownModelIds.length){ return seed; } if(knownModelIds.includes(seed)){ return seed; } const ranked=knownModelIds.map((modelId)=>{ const target=String(modelId || '').toLowerCase(); let score=0; if(target===source){ score+=100; } if(target.includes(source) || source.includes(target)){ score+=40; } source.split(/[^a-z0-9]+/).filter(Boolean).forEach((part)=>{ if(target.includes(part)){ score+=Math.min(part.length * 4, 24); } }); return { modelId, score }; }).filter((item)=>item.score>0).sort((a,b)=>b.score-a.score || a.modelId.localeCompare(b.modelId)); return ranked.length ? ranked[0].modelId : seed;}function getTriggerPatternValidationHint(pattern){ if((pattern?.type || 'exact') === 'regex'){ return pattern?.pattern ? { level:'ok', message:'regex pattern \u5DF2\u914D\u7F6E' } : { level:'warn', message:'regex \u6A21\u5F0F\u9700\u8981\u586B\u5199 pattern' }; } return Array.isArray(pattern?.keywords) && pattern.keywords.some((keyword)=>String(keyword || '').trim()) ? { level:'ok', message:'exact keywords \u5DF2\u914D\u7F6E' } : { level:'warn', message:'exact \u6A21\u5F0F\u81F3\u5C11\u9700\u8981\u4E00\u4E2A keyword' };}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const triggerRules=Array.isArray(config?.TriggerRouter?.rules) ? config.TriggerRouter.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(config?.SmartRouter?.candidates) ? config.SmartRouter.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, config?.TriggerRouter?.intent_model, config?.SmartRouter?.router_model, config?.Governance?.sticky?.alignment?.summarizer_model, config?.Governance?.semantic?.classifier_model, config?.Governance?.shadow?.verifier_model].filter(v=>typeof v === 'string' && v.trim()).length + triggerRules.filter(rule=>rule?.model).length + smartCandidates.filter(item=>item?.model).length + cascadeLevels.reduce((sum,level)=>sum + (level?.from ? 1 : 0) + (level?.to ? 1 : 0), 0); draftSummaryGrid.innerHTML=[ ['Models', models.length], ['Trigger rules', triggerRules.length], ['Patterns', patternCount], ['Smart candidates', smartCandidates.length], ['Cascade levels', cascadeLevels.length], ['Model refs', modelRefCount] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function renderDraftValidation(errors,warnings){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; if(!errorList.length && !warningList.length){ draftValidationList.innerHTML='<div class="alert info"><strong>No validation issues</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u672A\u53D1\u73B0\u96C6\u4E2D\u5C55\u793A\u7684\u95EE\u9898</div></div>'; return; } const extractPath=(text)=>{ const match=String(text).match(/^(Models(?:\\[[0-9]+\\])?(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Router(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|TriggerRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|SmartRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Governance(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?)/); return match ? match[1] : ''; }; const grouped=[...errorList.map(item=>({ text:String(item), severity:'error' })), ...warningList.map(item=>({ text:String(item), severity:'warning' }))].reduce((acc,item)=>{ const text=item.text; const bucket=text.startsWith('Models') ? 'Models' : text.startsWith('Router') ? 'Router' : text.startsWith('TriggerRouter') ? 'TriggerRouter' : text.startsWith('SmartRouter') ? 'SmartRouter' : text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path: extractPath(text), severity:item.severity }); return acc; }, {}); const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorList.length)+' errors / '+esc(warningList.length)+' warnings</span></div><div class="muted">'+(errorList.length ? '\u8BF7\u4F18\u5148\u4FEE\u590D errors\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u63A5\u53D7 warnings\u3002' : '\u5F53\u524D\u65E0\u963B\u65AD\u9519\u8BEF\uFF0C\u53EF\u6309\u9700\u5904\u7406 warnings\u3002')+'</div></div>'; draftValidationList.innerHTML=summary + Object.entries(grouped).map(([bucket,items])=>{ const hasError=items.some(item=>item.severity==='error'); const levelClass=hasError ? 'warn' : 'info'; const actionLabel=hasError ? 'repair first' : 'review before save'; return '<div class="alert '+levelClass+'"><div class="row"><strong>'+esc(bucket)+'</strong><span class="pill">'+esc(items.length)+' issues</span></div><div class="muted">'+esc(actionLabel)+'</div><div>'+items.slice(0,4).map(item=>'<div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+'<span class="pill">'+esc(item.severity==='error' ? 'error' : 'warning')+'</span> '+esc(item.text)+'</div>').join('')+'</div></div>'; }).join('');}function getCapabilityWarningActionLabel(code){ if(code==='thinking_ignored'){ return '\u79FB\u9664 thinking'; } if(code==='tools_text_fallback' || code==='images_text_fallback'){ return '\u6062\u590D\u9ED8\u8BA4 capability'; } return '';}function renderCapabilityWarnings(report){ const entries=Array.isArray(report?.entries) ? report.entries : []; if(!entries.length){ capabilityWarningsList.innerHTML='<div class="alert info"><strong>No capability warnings</strong><div class="muted">\u5F53\u524D compiled models \u672A\u53D1\u73B0\u9700\u8981\u989D\u5916\u63D0\u793A\u7684\u80FD\u529B\u964D\u7EA7</div></div>'; return; } const summary=report?.summary || {}; capabilityWarningsList.innerHTML='<div class="alert info"><strong>Capability warning summary</strong><div class="muted">warn '+esc(summary.warn ?? 0)+' / info '+esc(summary.info ?? 0)+' / total '+esc(summary.total ?? entries.length)+'</div></div>' + entries.map(item=>{ const actionLabel=getCapabilityWarningActionLabel(item.code); return '<div class="alert '+esc(item.level === 'warn' ? 'warn' : 'info')+'"><div class="row"><strong>'+esc(item.code || item.level || 'warning')+'</strong><span class="pill">'+esc(item.modelId || '-').trim()+'</span></div><div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+esc(item.message || '')+'</div>'+(actionLabel ? ('<div class="row" style="margin-top:.5rem"><button type="button" data-apply-warning-path="'+esc(item.path || '')+'" data-apply-warning-code="'+esc(item.code || '')+'">'+esc(actionLabel)+'</button></div>') : '')+'</div>'; }).join('');}function findValidationTarget(path){ if(!path){ return null; } if(path.startsWith('Models')){ return modelsFormGrid; } if(path === 'Router.default'){ return draftRouterDefault; } if(path.startsWith('TriggerRouter.intent_model')){ return triggerIntentModel; } if(path.startsWith('TriggerRouter.rules[')){ return triggerRulesList; } if(path.startsWith('SmartRouter.router_model')){ return smartRouterModel; } if(path.startsWith('SmartRouter.candidates[')){ return smartCandidatesList; } if(path.startsWith('Governance.cascade.levels[')){ return governanceCascadeLevelsList; } if(path.startsWith('Governance.sticky.alignment')){ return governanceSummarizerModel; } if(path.startsWith('Governance.semantic')){ return governanceClassifierModel; } if(path.startsWith('Governance.shadow')){ return governanceVerifierModel; } if(path.startsWith('Governance')){ return governanceEnabled; } return null;}function jumpToValidationPath(path){ const target=findValidationTarget(path); if(!target || typeof target.scrollIntoView !== 'function'){ return; } if(activeValidationHighlight && activeValidationHighlight.classList){ activeValidationHighlight.classList.remove('jump-highlight'); } target.scrollIntoView({ behavior:'smooth', block:'center' }); if(target.classList){ target.classList.add('jump-highlight'); activeValidationHighlight=target; setTimeout(()=>{ if(target.classList){ target.classList.remove('jump-highlight'); if(activeValidationHighlight===target){ activeValidationHighlight=null; } } }, 1800); } if(typeof target.focus === 'function'){ target.focus({ preventScroll:true }); }}function renderDraftPresetModeHint(){ const overwriteMode=draftPresetMode.value === 'replace'; draftPresetModeHint.textContent=overwriteMode ? 'overwrite \u4F1A\u91CD\u7F6E TriggerRouter / SmartRouter / Governance \u76F8\u5173\u8868\u5355\uFF0C\u518D\u5E94\u7528\u9884\u8BBE' : 'append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5';}function deriveActualAffectedAreas(preview){ const areas=new Set(); const diff=preview?.diff || {}; const impact=preview?.referenceImpact || {}; if((diff.providerChanges || []).length || (diff.modelChanges || []).length){ areas.add('Models'); } (impact.entries || []).forEach((entry)=>{ const path=String(entry.path || ''); if(path.startsWith('Router.')){ areas.add('Router'); } else if(path.startsWith('TriggerRouter.')){ areas.add('TriggerRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.')){ areas.add('Governance'); } }); return Array.from(areas);}function renderDraftPreviewMeta(meta){ if(!meta){ draftPreviewMeta.innerHTML='<div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div>'; return; } draftPreviewMeta.innerHTML='<div class="alert info"><strong>'+esc(meta.title || 'Preset dry-run')+'</strong><div>'+esc(meta.description || '')+'</div><div class="muted">\u6A21\u5F0F\uFF1A'+esc(meta.mode || '-')+' \xB7 \u9884\u8BBE\u58F0\u660E\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((meta.affects || []).join(' / ') || '-')</div><div class="muted">\u5B9E\u9645\u9884\u89C8\u547D\u4E2D\u533A\u57DF\uFF1A'+esc((meta.actualAffects || []).join(' / ') || '-')</div></div>';}function renderDraftPresetGuide(){ draftPresetList.innerHTML=Object.entries(draftPresets).map(([key,preset])=>'<div class="alert info"><strong>'+esc(preset.label || key)+'</strong><div>'+esc(preset.description || '')+'</div><div class="muted">\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((preset.affects || []).join(' / '))+'</div></div>').join('');}function updateTopLevelModelSuggestionLists(){ const markup=knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join(''); [topLevelTriggerIntentSuggestions,topLevelSmartRouterSuggestions,topLevelGovernanceSummarizerSuggestions,topLevelGovernanceClassifierSuggestions,topLevelGovernanceVerifierSuggestions].forEach(node=>{ if(node){ node.innerHTML=markup; } });}function renderModelsForm(models){ const list=Array.isArray(models) ? models : []; draftModelsCount.value=String(list.length); if(!list.length){ modelsFormGrid.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div>'; return; } modelsFormGrid.innerHTML=list.map((model,index)=>{ const templateContext=getProviderTemplateContext(model); const template=templateContext.template; return '<div class="model-card" data-model-card="'+index+'">' + '<div class="model-card-header"><strong>Model #'+(index+1)+'</strong><button type="button" data-remove-model="'+index+'">\u5220\u9664</button></div>' + '<div class="model-card-grid">' + '<div><label>Provider template</label><div class="row"><select data-field="provider_template" data-index="'+index+'"><option value="">custom</option>'+Object.entries(modelProviderTemplates).map(([key,item])=>'<option value="'+esc(key)+'"'+(model.provider_template === key ? ' selected' : '')+'>'+esc(item.label)+'</option>').join('')+'</select><button type="button" data-apply-template="'+index+'">\u5E94\u7528</button></div></div>' + '<div><label>ID</label><input data-field="id" data-index="'+index+'" value="'+esc(model.id || '')+'" placeholder="'+esc(template.suggested_id || 'sonnet')+'"><div class="muted">\u5EFA\u8BAE\u6A21\u677F\uFF1A'+esc(template.label || templateContext.templateKey || 'custom')+'</div></div>' + '<div><label>Interface</label><select data-field="interface" data-index="'+index+'"><option value="openai"'+(((model.interface || model.protocol || 'openai') === 'openai') ? ' selected' : '')+'>openai</option><option value="anthropic"'+(((model.interface || model.protocol) === 'anthropic') ? ' selected' : '')+'>anthropic</option></select></div>' + '<div><label>Model</label><input data-field="model" data-index="'+index+'" list="modelSuggestions'+index+'" value="'+esc(model.model || '')+'" placeholder="'+esc(template.default_model || 'anthropic/claude-sonnet-4')+'"><datalist id="modelSuggestions'+index+'">'+((template.model_examples || []).map(item=>'<option value="'+esc(item)+'"></option>').join(''))+'</datalist><div class="muted">\u4F8B\u5982\uFF1A'+esc((template.model_examples || ['anthropic/claude-sonnet-4']).join(' / '))+'</div></div>' + '<div><label>API</label><input data-field="api" data-index="'+index+'" value="'+esc(model.api || model.api_base_url || '')+'" placeholder="'+esc(template.api || 'https://...')+'"></div>' + '<div><label>Key</label><input data-field="key" data-index="'+index+'" value="'+esc(model.key || model.api_key || '')+'" placeholder="'+esc(template.key_placeholder || 'sk-...')+'"></div>' + '<div><label>Thinking</label><select data-field="thinking_profile" data-index="'+index+'"><option value="">default</option><option value="off"'+(((model.thinking === 'off') || model.thinking?.mode === 'off') ? ' selected' : '')+'>off</option><option value="auto"'+(((model.thinking === 'auto') || model.thinking?.mode === 'auto') ? ' selected' : '')+'>auto</option><option value="on"'+(((model.thinking === 'on') || (model.thinking?.mode === 'on' && !model.thinking?.effort)) ? ' selected' : '')+'>on</option><option value="low"'+(((model.thinking === 'low') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'low' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>low</option><option value="medium"'+(((model.thinking === 'medium') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'medium' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>medium</option><option value="high"'+(((model.thinking === 'high') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'high' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>high</option><option value="custom"'+(((typeof model.thinking === 'object') && model.thinking && model.thinking.budget_tokens) ? ' selected' : '')+'>custom</option></select></div>' + '<div><label>Thinking mode</label><select data-field="thinking_mode" data-index="'+index+'"><option value="">default</option><option value="off"'+(model.thinking?.mode === 'off' ? ' selected' : '')+'>off</option><option value="auto"'+(model.thinking?.mode === 'auto' ? ' selected' : '')+'>auto</option><option value="on"'+(model.thinking?.mode === 'on' ? ' selected' : '')+'>on</option></select></div>' + '<div><label>Thinking effort</label><select data-field="thinking_effort" data-index="'+index+'"><option value="">default</option><option value="low"'+(model.thinking?.effort === 'low' ? ' selected' : '')+'>low</option><option value="medium"'+(model.thinking?.effort === 'medium' ? ' selected' : '')+'>medium</option><option value="high"'+(model.thinking?.effort === 'high' ? ' selected' : '')+'>high</option></select></div>' + '<div><label>Thinking budget</label><input data-field="thinking_budget_tokens" data-index="'+index+'" value="'+esc(model.thinking?.budget_tokens || '')+'" placeholder="1024"></div>' + '<div><label>Vendor hint</label><input data-field="vendor_hint" data-index="'+index+'" value="'+esc(model.metadata?.vendor_hint || '')+'" placeholder="'+esc(template.vendor_hint || 'openrouter')+'"></div>' + '<div><label>Reasoning support</label><select data-field="supports_reasoning" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_reasoning === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_reasoning === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Tool support</label><select data-field="supports_tools" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_tools === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_tools === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Image support</label><select data-field="supports_images" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_images === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_images === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div style="grid-column:1/-1"><label>Metadata (advanced JSON)</label><textarea data-field="metadata" data-index="'+index+'" placeholder="{\\"label\\":\\"Balanced profile\\"}">'+esc(model.metadata ? JSON.stringify(model.metadata, null, 2) : '')+'</textarea><div class="muted">\u666E\u901A capability \u5EFA\u8BAE\u4F18\u5148\u4F7F\u7528\u4E0A\u9762\u7684\u663E\u5F0F\u5B57\u6BB5\uFF1B\u8FD9\u91CC\u4FDD\u7559\u7ED9\u9AD8\u7EA7\u6269\u5C55\u5143\u6570\u636E\u3002</div></div>' + '</div>' + '</div>'; }).join('');}function extractModelsFromForm(){ const cards=Array.from(modelsFormGrid.querySelectorAll('[data-model-card]')); return cards.map((card,index)=>{ const read=(field)=>card.querySelector('[data-field="'+field+'"][data-index="'+index+'"]'); const providerTemplate=(read('provider_template')?.value || '').trim(); const metadataRaw=(read('metadata')?.value || '').trim(); let metadata; if(metadataRaw){ metadata=JSON.parse(metadataRaw); } else { metadata={}; } const thinkingProfile=(read('thinking_profile')?.value || '').trim(); const vendorHint=(read('vendor_hint')?.value || '').trim(); const supportsReasoning=(read('supports_reasoning')?.value || '').trim(); const supportsTools=(read('supports_tools')?.value || '').trim(); const supportsImages=(read('supports_images')?.value || '').trim(); const thinking={}; const mode=(read('thinking_mode')?.value || '').trim(); const effort=(read('thinking_effort')?.value || '').trim(); const budget=(read('thinking_budget_tokens')?.value || '').trim(); if(mode) thinking.mode=mode; if(effort) thinking.effort=effort; if(budget) thinking.budget_tokens=Number(budget); const model={ id:(read('id')?.value || '').trim(), api:(read('api')?.value || '').trim(), key:(read('key')?.value || '').trim(), interface:(read('interface')?.value || '').trim(), model:(read('model')?.value || '').trim(), }; if(vendorHint){ metadata.vendor_hint=vendorHint; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'vendor_hint')){ delete metadata.vendor_hint; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No trigger rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); if(triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length){ payload.TriggerRouter={ ...(payload.TriggerRouter || {}), enabled: triggerEnabled.checked, analysis_scope: triggerAnalysisScope.value || 'last_message', llm_intent_recognition: triggerIntentEnabled.checked, intent_model: triggerIntentModel.value.trim(), rules: triggerRules }; } else { delete payload.TriggerRouter; } const smartCandidates=extractSmartCandidatesFromForm(); if(smartEnabled.checked || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim()){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: smartEnabled.checked, router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined }; } else { delete payload.SmartRouter; } const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim() || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, sticky:{ ...((payload.Governance && payload.Governance.sticky) || {}), enabled: Boolean(governanceEnabled.checked || governanceAlignmentEnabled.checked), alignment:{ ...(((payload.Governance && payload.Governance.sticky && payload.Governance.sticky.alignment) || {})), enabled: governanceAlignmentEnabled.checked, summarizer_model: governanceSummarizerModel.value.trim() } }, semantic:{ ...((payload.Governance && payload.Governance.semantic) || {}), enabled: governanceSemanticEnabled.checked, mode:'classifier', classifier_model: governanceClassifierModel.value.trim() }, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(trigger.enabled); triggerIntentEnabled.checked=Boolean(trigger.llm_intent_recognition); triggerAnalysisScope.value=trigger.analysis_scope || 'last_message'; triggerIntentModel.value=trigger.intent_model || ''; renderTriggerRulesList(trigger.rules || []); const smart=config?.SmartRouter || {}; smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(governance.sticky?.alignment?.enabled); governanceSummarizerModel.value=governance.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(governance.semantic?.enabled); governanceClassifierModel.value=governance.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u540C\u6B65 Models \u8868\u5355\u5230 JSON \u8349\u7A3F'; } catch (error) { draftPreviewStatus.textContent='\u540C\u6B65\u5931\u8D25\uFF1A'+error.message; }}function applyReferenceSuggestion(path,modelId){ if(!modelId){ return; } if(path==='Router.default'){ draftRouterDefault.value=modelId; syncDraftEditorFromForm(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 Router.default'; return; } const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const pathMatch=path.match(/^([^.[]+)(?:.(.+))?$/); if(!pathMatch){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\uFF1A'+path; return; } const tokens=path.replace(/[(d+)]/g,'.$1').split('.'); let cursor=payload; for(let i=0;i<tokens.length-1;i++){ const token=tokens[i]; const nextToken=tokens[i+1]; if(cursor[token] === undefined){ cursor[token]=String(Number(nextToken))===nextToken ? [] : {}; } cursor=cursor[token]; } cursor[tokens[tokens.length-1]]=modelId; currentDraftConfig=payload; if(payload.Router?.default){ draftRouterDefault.value=payload.Router.default; } renderConfigControlForms(payload); configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyCapabilityWarningSuggestion(path,code){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const tokens=String(path || '').replace(/[(d+)]/g,'.$1').split('.').filter(Boolean); if(!tokens.length){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } let cursor=payload; for(let i=0;i<tokens.length-1;i++){ if(cursor == null){ break; } cursor=cursor[tokens[i]]; } const lastToken=tokens[tokens.length-1]; if(code==='thinking_ignored'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } } else if(code==='tools_text_fallback' || code==='images_text_fallback'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } if(cursor && !Object.keys(cursor).length){ const parentTokens=tokens.slice(0,-1); const maybeMetadataKey=parentTokens[parentTokens.length-1]; if(maybeMetadataKey==='metadata'){ let parentCursor=payload; for(let i=0;i<parentTokens.length-1;i++){ if(parentCursor == null){ break; } parentCursor=parentCursor[parentTokens[i]]; } if(parentCursor && Object.prototype.hasOwnProperty.call(parentCursor,'metadata')){ delete parentCursor.metadata; } } } } else { draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528 warning \u4FEE\u6B63\uFF1A'+code+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function renderCompiledDiff(diff){ const summary=diff?.summary || {}; compiledDiffSummary.innerHTML=[ ['Added providers', summary.addedProviders ?? 0], ['Removed providers', summary.removedProviders ?? 0], ['Changed providers', summary.changedProviders ?? 0], ['Added models', summary.addedModels ?? 0], ['Removed models', summary.removedModels ?? 0], ['Changed models', summary.changedModels ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const rows=[ ...((diff?.providerChanges || []).map(item=>({ scope:'provider', key:item.name, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ...((diff?.modelChanges || []).map(item=>({ scope:'model', key:item.modelId, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ]; compiledDiffTableBody.innerHTML=rows.length ? rows.map(item=>'<tr>' + '<td>'+esc(item.scope)+'</td>' + '<td>'+esc(item.type)+'</td>' + '<td><code>'+esc(item.key)+'</code></td>' + '<td>'+esc(item.fields.join(', ') || '-')+'</td>' + '<td><code>'+esc(item.target.providerName || item.target.name || '-')+'</code><div class="muted">'+esc(item.target.modelName || (item.target.models || []).join(', ') || '-')}</div></td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled registry changes</td></tr>';}function renderReferenceImpact(impact){ const summary=impact?.summary || {}; referenceImpactSummary.innerHTML=[ ['Total refs', summary.total ?? 0], ['modelId refs', summary.modelIdRefs ?? 0], ['Legacy refs', summary.legacyRefs ?? 0], ['Valid modelIds', summary.validModelIds ?? 0], ['Missing modelIds', summary.missingModelIds ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const entries=impact?.entries || []; referenceImpactTableBody.innerHTML=entries.length ? entries.map(item=>'<tr>' + '<td><code>'+esc(item.path)+'</code></td>' + '<td><code>'+esc(item.value)+'</code></td>' + '<td>'+esc(item.referenceType)+'</td>' + '<td>'+esc(item.status)+'</td>' + '<td><code>'+esc(item.resolvedTarget?.providerName || '-')+'</code><div class="muted">'+esc(item.resolvedTarget?.modelName || '-')}</div></td>' + '<td>'+((item.suggestions || []).length ? item.suggestions.map(s=>'<div><code>'+esc(s.modelId)+'</code><div class="muted">'+esc(s.modelName || '-')+'</div><button type="button" data-apply-reference-path="'+esc(item.path)+'" data-apply-reference-model="'+esc(s.modelId)+'">\u5E94\u7528\u5EFA\u8BAE</button></div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No model references found</td></tr>';}function renderCompiledModels(data){ const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04'; compiledProvidersTableBody.innerHTML=providers.length ? providers.map(provider=>'<tr>' + '<td><code>'+esc(provider.name)+'</code><div class="muted">'+esc(provider.api_base_url || '-')+'</div></td>' + '<td>'+esc(provider.transformer?.use?.[0] || '-')+'</td>' + '<td>'+esc((provider.models || []).join(', ') || '-')+'</td>' + '<td>'+esc(JSON.stringify(provider.transformer || {}))+'</td>' + '<td>'+esc(provider.has_api_key ? 'configured' : 'missing')+'</td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled providers</td></tr>'; compiledModelMapTableBody.innerHTML=modelMapEntries.length ? modelMapEntries.map(([modelId,item])=>'<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td><code>'+esc(item.providerName || '-')+'</code><div class="muted">'+esc(item.modelName || '-')+'</div></td>' + '<td>'+esc(item.protocol || '-')+'</td>' + '<td><code>'+esc(JSON.stringify(item.thinking || { mode: 'off' }))+'</code></td>' + '<td><code>'+esc(JSON.stringify(item.capabilities || {}))+'</code></td>' + '<td>'+esc(item.source || '-')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No compiled model map</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || []); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || []); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addCascadeLevel(){ const next=extractCascadeLevelsFromForm(); next.push({ from:'', to:'' }); renderCascadeLevelsList(next); syncDraftEditorFromForm(); }modelsFormGrid.addEventListener('input',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('change',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-template]'); if(applyBtn){ const applyIndex=Number(applyBtn.dataset.applyTemplate); applyProviderTemplate(applyIndex); syncDraftEditorFromForm(); return; } const btn=e.target.closest('button[data-remove-model]'); if(!btn){ return; } const removeIndex=Number(btn.dataset.removeModel); const nextModels=extractModelsFromForm().filter((_,index)=>index!==removeIndex); renderModelsForm(nextModels); syncDraftEditorFromForm(); });triggerRulesList.addEventListener('input',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('change',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('click',(e)=>{ const addKeywordBtn=e.target.closest('button[data-add-trigger-keyword]'); if(addKeywordBtn){ addTriggerKeyword(Number(addKeywordBtn.dataset.addTriggerKeyword), Number(addKeywordBtn.dataset.patternIndex)); return; } const removeKeywordBtn=e.target.closest('button[data-remove-trigger-keyword]'); if(removeKeywordBtn){ const ruleIndex=Number(removeKeywordBtn.dataset.removeTriggerKeyword); const patternIndex=Number(removeKeywordBtn.dataset.patternIndex); const keywordIndex=Number(removeKeywordBtn.dataset.keywordIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex] && next[ruleIndex].patterns && next[ruleIndex].patterns[patternIndex]){ const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=(pattern.keywords || []).filter((_,index)=>index!==keywordIndex); if(!pattern.keywords.length){ pattern.keywords=['']; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const addBtn=e.target.closest('button[data-add-trigger-pattern]'); if(addBtn){ addTriggerPattern(Number(addBtn.dataset.addTriggerPattern)); return; } const removePatternBtn=e.target.closest('button[data-remove-trigger-pattern]'); if(removePatternBtn){ const ruleIndex=Number(removePatternBtn.dataset.removeTriggerPattern); const patternIndex=Number(removePatternBtn.dataset.patternIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex]){ next[ruleIndex].patterns=(next[ruleIndex].patterns || []).filter((_,index)=>index!==patternIndex); if(!next[ruleIndex].patterns.length){ next[ruleIndex].patterns=[{ type:'exact', keywords:[] }]; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const btn=e.target.closest('button[data-remove-trigger-rule]'); if(!btn){ return; } const next=extractTriggerRulesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeTriggerRule)); renderTriggerRulesList(next); syncDraftEditorFromForm(); });smartCandidatesList.addEventListener('input',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('change',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-smart-candidate]'); if(!btn){ return; } const next=extractSmartCandidatesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeSmartCandidate)); renderSmartCandidatesList(next); syncDraftEditorFromForm(); });governanceCascadeLevelsList.addEventListener('input',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('change',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-cascade-level]'); if(!btn){ return; } const next=extractCascadeLevelsFromForm().filter((_,index)=>index!==Number(btn.dataset.removeCascadeLevel)); renderCascadeLevelsList(next); syncDraftEditorFromForm(); });referenceImpactTableBody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-apply-reference-path]'); if(!btn){ return; } applyReferenceSuggestion(btn.dataset.applyReferencePath, btn.dataset.applyReferenceModel); });draftValidationList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });capabilityWarningsList.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-warning-path]'); if(applyBtn){ applyCapabilityWarningSuggestion(applyBtn.dataset.applyWarningPath, applyBtn.dataset.applyWarningCode); return; } const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });function renderMetrics(metrics){ metricsGrid.innerHTML=[ ['Recent traces', metrics.totalTraces ?? 0], ['Sticky hit rate', pct(metrics.stickyHitRate)], ['Cascade rate', pct(metrics.cascadeTriggeredRate)], ['Shadow rate', pct(metrics.shadowCheckedRate)], ['Alignment rate', pct(metrics.alignmentUsedRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.TriggerRouter={ ...(payload.TriggerRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.TriggerRouter?.enabled), analysis_scope: payload.TriggerRouter?.analysis_scope || 'last_message', llm_intent_recognition: payload.TriggerRouter?.llm_intent_recognition || false, intent_model: payload.TriggerRouter?.intent_model || '', rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.TriggerRouter?.rules || []) }; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), sticky:{ ...((payload.Governance && payload.Governance.sticky) || {}), alignment:{ ...(((payload.Governance && payload.Governance.sticky && payload.Governance.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.Governance?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.Governance?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.Governance && payload.Governance.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.Governance?.semantic?.enabled), mode:(payload.Governance?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.Governance?.semantic?.classifier_model || '') }, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || []); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderAnomalies(anomalies){ if(!anomalies || !anomalies.length){ anomalyList.innerHTML='<div class="alert info"><strong>No active alerts</strong><div class="muted">\u5F53\u524D\u7A97\u53E3\u672A\u53D1\u73B0\u660E\u663E\u6CBB\u7406\u5F02\u5E38</div></div>'; return; } anomalyList.innerHTML=anomalies.map(item=>'<div class="alert '+esc(item.severity || 'info')+'"><strong>'+esc(item.type)+'</strong><div>'+esc(item.message)+'</div></div>').join('');}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')}</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); renderMetrics(metricsData.metrics || {}); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || []); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderTrendTable(metricsData || {}); const traces=data.traces || []; if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`
3584
+ `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Claude Trigger Router</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:1rem;padding:.45rem 0;border-bottom:1px dashed #e5e7eb}.mini-list li:last-child{border-bottom:none}.action-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center;margin-top:.75rem}.management-table{width:100%;margin-top:.75rem}.management-table th,.management-table td{padding:.5rem;border-bottom:1px solid #e5e7eb;font-size:.92rem;vertical-align:top}.alert-list{display:grid;gap:.75rem;margin-top:1rem}.alert{border-radius:12px;padding:.85rem 1rem;border:1px solid}.alert.warn{background:#fff7ed;border-color:#fdba74;color:#9a3412}.alert.critical{background:#fef2f2;border-color:#fca5a5;color:#991b1b}.alert.info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.diff-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-top:.75rem}.diff-chip{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.75rem}.diff-chip strong{display:block;font-size:1rem;margin-top:.2rem}.models-form-grid{display:grid;gap:.75rem;margin-top:.75rem}.model-card{border:1px solid #e5e7eb;border-radius:12px;padding:1rem;background:#fcfcfd}.model-card-header{display:flex;justify-content:space-between;gap:1rem;align-items:center;margin-bottom:.75rem}.model-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.model-card-grid textarea{min-height:84px;resize:vertical}.list-editor{display:grid;gap:.75rem;margin-top:.75rem}.list-item{border:1px solid #e5e7eb;border-radius:12px;padding:.85rem;background:#fcfcfd}.list-item-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.jump-highlight{outline:3px solid #f59e0b;box-shadow:0 0 0 6px rgba(245,158,11,.15);transition:box-shadow .25s ease,outline-color .25s ease}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.control-grid label{display:block;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}.trend-table{width:100%;margin-top:.75rem}.trend-table th,.trend-table td{padding:.45rem;border-bottom:1px solid #e5e7eb;font-size:.92rem}.row{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}input,select,button{font:inherit;padding:.55rem .75rem;border-radius:8px;border:1px solid #d1d5db}button{background:#111827;color:#fff;border-color:#111827;cursor:pointer}table{width:100%;border-collapse:collapse;margin-top:1rem}th,td{text-align:left;padding:.65rem .5rem;border-bottom:1px solid #e5e7eb;vertical-align:top}code,pre{font-family:ui-monospace,SFMono-Regular,monospace}pre{white-space:pre-wrap;background:#0f172a;color:#e2e8f0;padding:1rem;border-radius:12px;overflow:auto}.pill{display:inline-block;padding:.2rem .5rem;border-radius:999px;background:#eef2ff;color:#3730a3;font-size:.8rem}</style></head><body><h2>Claude Trigger Router</h2><p class="muted">\u7B80\u6613 Governance Trace \u8C03\u8BD5\u9875\u3002\u53EF\u67E5\u770B\u6700\u8FD1\u6CBB\u7406\u94FE\u8DEF\uFF0C\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4\uFF0C\u5E76\u6309 cascade / shadow \u72B6\u6001\u7B5B\u9009\uFF1B\u6CBB\u7406 trace \u73B0\u5DF2\u652F\u6301\u672C\u5730\u6301\u4E45\u5316\uFF0C\u91CD\u542F\u540E\u53EF\u7EE7\u7EED\u67E5\u770B\u8FD1\u671F\u7A97\u53E3\u3002</p><div class="panel"><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Trigger rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u9996\u6279\u8868\u5355\u5316\u7F16\u8F91 TriggerRouter / SmartRouter / Governance \u7684\u6838\u5FC3\u5F15\u7528</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>TriggerRouter</strong><span class="muted">\u89C4\u5219\u8DEF\u7531\u4E0E\u610F\u56FE\u8BC6\u522B</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No trigger rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5BF9\u9F50\u3001\u8BED\u4E49\u3001\u5F71\u5B50\u6821\u9A8C\u4E0E\u7EA7\u8054</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div id="modelsFormGrid" class="models-form-grid"><div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div></div><textarea id="configDraftEditor" style="width:100%;min-height:240px;margin-top:.75rem;padding:.75rem;border-radius:12px;border:1px solid #d1d5db;font:12px/1.5 ui-monospace,SFMono-Regular,monospace" spellcheck="false" placeholder='{"Models":[{"id":"sonnet","api":"https://...","key":"sk-...","interface":"openai","model":"anthropic/claude-sonnet-4"}]}'></textarea><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / TriggerRouter / Governance \u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Compatibility profile</th><th>Dispatch format</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="8" class="muted">Loading model map...</td></tr></tbody></table></div></div></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Recent traces</span><strong>-</strong></div><div class="stat"><span class="muted">Sticky hit rate</span><strong>-</strong></div><div class="stat"><span class="muted">Cascade rate</span><strong>-</strong></div><div class="stat"><span class="muted">Shadow rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment rate</span><strong>-</strong></div><div class="stat"><span class="muted">Avg latency</span><strong>-</strong></div></div><div class="subpanel"><div class="row"><strong>Anomaly alerts</strong><span class="muted">\u68C0\u6D4B\u8FD1\u671F\u6CBB\u7406\u5F02\u5E38\u4E0E\u7A81\u589E</span></div><div id="anomalyList" class="alert-list"><div class="alert info"><strong>No alerts yet</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u6307\u6807\u52A0\u8F7D</div></div></div></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${configuredThresholds.min_sample_size ?? 3}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${configuredThresholds.cascade_warn_rate ?? 0.4}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${configuredThresholds.shadow_warn_rate ?? 0.5}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${configuredThresholds.latency_warn_ms ?? 1500}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Trend detail</strong><span class="muted">\u6BCF\u4E2A bucket \u7684\u8BE6\u7EC6\u547D\u4E2D\u7387</span></div><table id="trendTable" class="trend-table"><thead><tr><th>Bucket</th><th>Traces</th><th>Sticky</th><th>Cascade</th><th>Shadow</th><th>Alignment</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading...</td></tr></tbody></table></div></div><table id="traceTable"><thead><tr><th>Request</th><th>Session</th><th>Final Model</th><th>Reasons</th><th>Latency</th><th>Inspect</th></tr></thead><tbody><tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Trace Detail</strong><span id="detailHint" class="muted">\u70B9\u51FB\u4E0A\u8868\u4E2D\u7684 View \u67E5\u770B\u8BE6\u60C5</span></div><pre id="traceDetail">{}</pre></div><div class="panel"><div class="row"><strong>Snapshot Management</strong><span class="muted">\u67E5\u770B\u5BFC\u51FA\u5386\u53F2\u3001\u5B9A\u65F6\u4EFB\u52A1\uFF0C\u5E76\u624B\u52A8\u521B\u5EFA\u5FEB\u7167</span></div><div class="action-row"><select id="snapshotFormat"><option value="json">snapshot json</option><option value="csv">snapshot csv</option></select><button id="createSnapshotBtn" type="button">\u751F\u6210\u5FEB\u7167</button><span id="snapshotStatus" class="muted">\u5C1A\u672A\u521B\u5EFA\u5FEB\u7167</span></div><table id="exportTable" class="management-table"><thead><tr><th>Export</th><th>Kind</th><th>Format</th><th>Created</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading exports...</td></tr></tbody></table><table id="scheduleTable" class="management-table"><thead><tr><th>Schedule</th><th>Interval</th><th>Format</th><th>Last run</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading schedules...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Archive Management</strong><span class="muted">\u6D4F\u89C8\u538B\u7F29\u5F52\u6863\u5E76\u67E5\u770B\u5206\u9875\u7ED3\u679C</span></div><div class="action-row"><input id="archiveDate" placeholder="YYYY-MM-DD"><input id="archivePage" placeholder="page" value="1"><input id="archivePageSize" placeholder="pageSize" value="5"><button id="loadArchivesBtn" type="button">\u52A0\u8F7D\u5F52\u6863</button><span id="archiveStatus" class="muted">\u5C1A\u672A\u52A0\u8F7D\u5F52\u6863</span></div><table id="archiveTable" class="management-table"><thead><tr><th>Archive</th><th>Range</th><th>Count</th><th>Compressed</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading archives...</td></tr></tbody></table></div><div class="panel"><p>\u5176\u4ED6\u7BA1\u7406 API\uFF1A</p><ul><li><code>GET /api/config</code> \u2014 \u8BFB\u53D6\u5F53\u524D\u914D\u7F6E</li><li><code>GET /api/models/compiled</code> \u2014 \u67E5\u770B Models \u7F16\u8BD1\u540E\u7684\u5185\u90E8 provider / model \u6620\u5C04</li><li><code>POST /api/models/compiled/preview</code> \u2014 \u7528\u914D\u7F6E\u8349\u7A3F\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u5199\u56DE\u6587\u4EF6</li><li><code>POST /api/config</code> \u2014 \u4FDD\u5B58\u914D\u7F6E</li><li><code>GET /api/transformers</code> \u2014 \u67E5\u770B\u5DF2\u52A0\u8F7D transformer</li><li><code>POST /api/restart</code> \u2014 \u91CD\u542F\u670D\u52A1</li><li><code>GET /api/governance/archives</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5F52\u6863\u5217\u8868</li><li><code>GET /api/governance/archives/:file</code> \u2014 \u67E5\u770B\u5F52\u6863\u5185 traces</li><li><code>POST /api/governance/archives/:file/delete</code> \u2014 \u5220\u9664\u6307\u5B9A\u5F52\u6863</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const metricsGrid=document.getElementById('metricsGrid');const bucketGrid=document.getElementById('bucketGrid');const bucketHint=document.getElementById('bucketHint');const routeRanking=document.getElementById('routeRanking');const modelRanking=document.getElementById('modelRanking');const intentRanking=document.getElementById('intentRanking');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');let currentDraftConfig={};let knownModelIds=[];let activeValidationHighlight=null;const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94 TriggerRule\u3002', affects:['Router.default','TriggerRouter.enabled','TriggerRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u6838\u5FC3\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','Governance.sticky.alignment','Governance.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function inferProviderTemplateKey(model){ const explicit=String(model?.provider_template || '').trim(); if(explicit && modelProviderTemplates[explicit]){ return explicit; } const api=String(model?.api || model?.api_base_url || '').trim().toLowerCase(); const modelInterface=String(model?.interface || model?.protocol || '').trim().toLowerCase(); const exactMatch=Object.entries(modelProviderTemplates).find(([,item])=>String(item.api || '').trim().toLowerCase()===api && String(item.interface || '').trim().toLowerCase()===modelInterface); if(exactMatch){ return exactMatch[0]; } if(api.includes('api.anthropic.com/v1/messages') || modelInterface === 'anthropic'){ return 'anthropic'; } if(api.includes('openrouter.ai')){ return 'openrouter'; } if(api.includes('deepseek.com')){ return 'deepseek'; } if(api.includes('siliconflow.cn')){ return 'siliconflow'; } if(api.includes('api.openai.com')){ return 'openai-compatible'; } return '';}function getProviderTemplateContext(model){ const templateKey=inferProviderTemplateKey(model) || defaultProviderTemplateKey; return { templateKey, template:modelProviderTemplates[templateKey] || modelProviderTemplates[defaultProviderTemplateKey] || {} };}function createDraftModelFromTemplate(templateKey){ const resolvedKey=(templateKey && modelProviderTemplates[templateKey]) ? templateKey : defaultProviderTemplateKey; const template=modelProviderTemplates[resolvedKey] || {}; return { provider_template:resolvedKey, id:template.suggested_id || '', api:template.api || '', interface:template.interface || 'openai', model:template.default_model || '', thinking:template.default_thinking || 'auto' };}function getModelIdSuggestionsMarkup(idPrefix){ return '<datalist id="'+idPrefix+'">'+knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join('')+'</datalist>';}function resolvePresetModelId(seed){ const source=String(seed || '').trim().toLowerCase(); if(!source || !knownModelIds.length){ return seed; } if(knownModelIds.includes(seed)){ return seed; } const ranked=knownModelIds.map((modelId)=>{ const target=String(modelId || '').toLowerCase(); let score=0; if(target===source){ score+=100; } if(target.includes(source) || source.includes(target)){ score+=40; } source.split(/[^a-z0-9]+/).filter(Boolean).forEach((part)=>{ if(target.includes(part)){ score+=Math.min(part.length * 4, 24); } }); return { modelId, score }; }).filter((item)=>item.score>0).sort((a,b)=>b.score-a.score || a.modelId.localeCompare(b.modelId)); return ranked.length ? ranked[0].modelId : seed;}function getTriggerPatternValidationHint(pattern){ if((pattern?.type || 'exact') === 'regex'){ return pattern?.pattern ? { level:'ok', message:'regex pattern \u5DF2\u914D\u7F6E' } : { level:'warn', message:'regex \u6A21\u5F0F\u9700\u8981\u586B\u5199 pattern' }; } return Array.isArray(pattern?.keywords) && pattern.keywords.some((keyword)=>String(keyword || '').trim()) ? { level:'ok', message:'exact keywords \u5DF2\u914D\u7F6E' } : { level:'warn', message:'exact \u6A21\u5F0F\u81F3\u5C11\u9700\u8981\u4E00\u4E2A keyword' };}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const triggerRules=Array.isArray(config?.TriggerRouter?.rules) ? config.TriggerRouter.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(config?.SmartRouter?.candidates) ? config.SmartRouter.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, config?.TriggerRouter?.intent_model, config?.SmartRouter?.router_model, config?.Governance?.sticky?.alignment?.summarizer_model, config?.Governance?.semantic?.classifier_model, config?.Governance?.shadow?.verifier_model].filter(v=>typeof v === 'string' && v.trim()).length + triggerRules.filter(rule=>rule?.model).length + smartCandidates.filter(item=>item?.model).length + cascadeLevels.reduce((sum,level)=>sum + (level?.from ? 1 : 0) + (level?.to ? 1 : 0), 0); draftSummaryGrid.innerHTML=[ ['Models', models.length], ['Trigger rules', triggerRules.length], ['Patterns', patternCount], ['Smart candidates', smartCandidates.length], ['Cascade levels', cascadeLevels.length], ['Model refs', modelRefCount] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function renderDraftValidation(errors,warnings){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; if(!errorList.length && !warningList.length){ draftValidationList.innerHTML='<div class="alert info"><strong>No validation issues</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u672A\u53D1\u73B0\u96C6\u4E2D\u5C55\u793A\u7684\u95EE\u9898</div></div>'; return; } const extractPath=(text)=>{ const match=String(text).match(/^(Models(?:\\[[0-9]+\\])?(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Router(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|TriggerRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|SmartRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Governance(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?)/); return match ? match[1] : ''; }; const grouped=[...errorList.map(item=>({ text:String(item), severity:'error' })), ...warningList.map(item=>({ text:String(item), severity:'warning' }))].reduce((acc,item)=>{ const text=item.text; const bucket=text.startsWith('Models') ? 'Models' : text.startsWith('Router') ? 'Router' : text.startsWith('TriggerRouter') ? 'TriggerRouter' : text.startsWith('SmartRouter') ? 'SmartRouter' : text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path: extractPath(text), severity:item.severity }); return acc; }, {}); const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorList.length)+' errors / '+esc(warningList.length)+' warnings</span></div><div class="muted">'+(errorList.length ? '\u8BF7\u4F18\u5148\u4FEE\u590D errors\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u63A5\u53D7 warnings\u3002' : '\u5F53\u524D\u65E0\u963B\u65AD\u9519\u8BEF\uFF0C\u53EF\u6309\u9700\u5904\u7406 warnings\u3002')+'</div></div>'; draftValidationList.innerHTML=summary + Object.entries(grouped).map(([bucket,items])=>{ const hasError=items.some(item=>item.severity==='error'); const levelClass=hasError ? 'warn' : 'info'; const actionLabel=hasError ? 'repair first' : 'review before save'; return '<div class="alert '+levelClass+'"><div class="row"><strong>'+esc(bucket)+'</strong><span class="pill">'+esc(items.length)+' issues</span></div><div class="muted">'+esc(actionLabel)+'</div><div>'+items.slice(0,4).map(item=>'<div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+'<span class="pill">'+esc(item.severity==='error' ? 'error' : 'warning')+'</span> '+esc(item.text)+'</div>').join('')+'</div></div>'; }).join('');}function getCapabilityWarningActionLabel(code){ if(code==='thinking_ignored'){ return '\u79FB\u9664 thinking'; } if(code==='tools_text_fallback' || code==='images_text_fallback'){ return '\u6062\u590D\u9ED8\u8BA4 capability'; } return '';}function renderCapabilityWarnings(report){ const entries=Array.isArray(report?.entries) ? report.entries : []; if(!entries.length){ capabilityWarningsList.innerHTML='<div class="alert info"><strong>No capability warnings</strong><div class="muted">\u5F53\u524D compiled models \u672A\u53D1\u73B0\u9700\u8981\u989D\u5916\u63D0\u793A\u7684\u80FD\u529B\u964D\u7EA7</div></div>'; return; } const summary=report?.summary || {}; capabilityWarningsList.innerHTML='<div class="alert info"><strong>Capability warning summary</strong><div class="muted">warn '+esc(summary.warn ?? 0)+' / info '+esc(summary.info ?? 0)+' / total '+esc(summary.total ?? entries.length)+'</div></div>' + entries.map(item=>{ const actionLabel=getCapabilityWarningActionLabel(item.code); return '<div class="alert '+esc(item.level === 'warn' ? 'warn' : 'info')+'"><div class="row"><strong>'+esc(item.code || item.level || 'warning')+'</strong><span class="pill">'+esc(item.modelId || '-').trim()+'</span></div><div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+esc(item.message || '')+'</div>'+(actionLabel ? ('<div class="row" style="margin-top:.5rem"><button type="button" data-apply-warning-path="'+esc(item.path || '')+'" data-apply-warning-code="'+esc(item.code || '')+'">'+esc(actionLabel)+'</button></div>') : '')+'</div>'; }).join('');}function findValidationTarget(path){ if(!path){ return null; } if(path.startsWith('Models')){ return modelsFormGrid; } if(path === 'Router.default'){ return draftRouterDefault; } if(path.startsWith('TriggerRouter.intent_model')){ return triggerIntentModel; } if(path.startsWith('TriggerRouter.rules[')){ return triggerRulesList; } if(path.startsWith('SmartRouter.router_model')){ return smartRouterModel; } if(path.startsWith('SmartRouter.candidates[')){ return smartCandidatesList; } if(path.startsWith('Governance.cascade.levels[')){ return governanceCascadeLevelsList; } if(path.startsWith('Governance.sticky.alignment')){ return governanceSummarizerModel; } if(path.startsWith('Governance.semantic')){ return governanceClassifierModel; } if(path.startsWith('Governance.shadow')){ return governanceVerifierModel; } if(path.startsWith('Governance')){ return governanceEnabled; } return null;}function jumpToValidationPath(path){ const target=findValidationTarget(path); if(!target || typeof target.scrollIntoView !== 'function'){ return; } if(activeValidationHighlight && activeValidationHighlight.classList){ activeValidationHighlight.classList.remove('jump-highlight'); } target.scrollIntoView({ behavior:'smooth', block:'center' }); if(target.classList){ target.classList.add('jump-highlight'); activeValidationHighlight=target; setTimeout(()=>{ if(target.classList){ target.classList.remove('jump-highlight'); if(activeValidationHighlight===target){ activeValidationHighlight=null; } } }, 1800); } if(typeof target.focus === 'function'){ target.focus({ preventScroll:true }); }}function renderDraftPresetModeHint(){ const overwriteMode=draftPresetMode.value === 'replace'; draftPresetModeHint.textContent=overwriteMode ? 'overwrite \u4F1A\u91CD\u7F6E TriggerRouter / SmartRouter / Governance \u76F8\u5173\u8868\u5355\uFF0C\u518D\u5E94\u7528\u9884\u8BBE' : 'append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5';}function deriveActualAffectedAreas(preview){ const areas=new Set(); const diff=preview?.diff || {}; const impact=preview?.referenceImpact || {}; if((diff.providerChanges || []).length || (diff.modelChanges || []).length){ areas.add('Models'); } (impact.entries || []).forEach((entry)=>{ const path=String(entry.path || ''); if(path.startsWith('Router.')){ areas.add('Router'); } else if(path.startsWith('TriggerRouter.')){ areas.add('TriggerRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.')){ areas.add('Governance'); } }); return Array.from(areas);}function renderDraftPreviewMeta(meta){ if(!meta){ draftPreviewMeta.innerHTML='<div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div>'; return; } draftPreviewMeta.innerHTML='<div class="alert info"><strong>'+esc(meta.title || 'Preset dry-run')+'</strong><div>'+esc(meta.description || '')+'</div><div class="muted">\u6A21\u5F0F\uFF1A'+esc(meta.mode || '-')+' \xB7 \u9884\u8BBE\u58F0\u660E\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((meta.affects || []).join(' / ') || '-')</div><div class="muted">\u5B9E\u9645\u9884\u89C8\u547D\u4E2D\u533A\u57DF\uFF1A'+esc((meta.actualAffects || []).join(' / ') || '-')</div></div>';}function renderDraftPresetGuide(){ draftPresetList.innerHTML=Object.entries(draftPresets).map(([key,preset])=>'<div class="alert info"><strong>'+esc(preset.label || key)+'</strong><div>'+esc(preset.description || '')+'</div><div class="muted">\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((preset.affects || []).join(' / '))+'</div></div>').join('');}function updateTopLevelModelSuggestionLists(){ const markup=knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join(''); [topLevelTriggerIntentSuggestions,topLevelSmartRouterSuggestions,topLevelGovernanceSummarizerSuggestions,topLevelGovernanceClassifierSuggestions,topLevelGovernanceVerifierSuggestions].forEach(node=>{ if(node){ node.innerHTML=markup; } });}function renderModelsForm(models){ const list=Array.isArray(models) ? models : []; draftModelsCount.value=String(list.length); if(!list.length){ modelsFormGrid.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div>'; return; } modelsFormGrid.innerHTML=list.map((model,index)=>{ const templateContext=getProviderTemplateContext(model); const template=templateContext.template; return '<div class="model-card" data-model-card="'+index+'">' + '<div class="model-card-header"><strong>Model #'+(index+1)+'</strong><button type="button" data-remove-model="'+index+'">\u5220\u9664</button></div>' + '<div class="model-card-grid">' + '<div><label>Provider template</label><div class="row"><select data-field="provider_template" data-index="'+index+'"><option value="">custom</option>'+Object.entries(modelProviderTemplates).map(([key,item])=>'<option value="'+esc(key)+'"'+(model.provider_template === key ? ' selected' : '')+'>'+esc(item.label)+'</option>').join('')+'</select><button type="button" data-apply-template="'+index+'">\u5E94\u7528</button></div></div>' + '<div><label>ID</label><input data-field="id" data-index="'+index+'" value="'+esc(model.id || '')+'" placeholder="'+esc(template.suggested_id || 'sonnet')+'"><div class="muted">\u5EFA\u8BAE\u6A21\u677F\uFF1A'+esc(template.label || templateContext.templateKey || 'custom')+'</div></div>' + '<div><label>Interface</label><select data-field="interface" data-index="'+index+'"><option value="openai"'+(((model.interface || model.protocol || 'openai') === 'openai') ? ' selected' : '')+'>openai</option><option value="anthropic"'+(((model.interface || model.protocol) === 'anthropic') ? ' selected' : '')+'>anthropic</option></select></div>' + '<div><label>Model</label><input data-field="model" data-index="'+index+'" list="modelSuggestions'+index+'" value="'+esc(model.model || '')+'" placeholder="'+esc(template.default_model || 'anthropic/claude-sonnet-4')+'"><datalist id="modelSuggestions'+index+'">'+((template.model_examples || []).map(item=>'<option value="'+esc(item)+'"></option>').join(''))+'</datalist><div class="muted">\u4F8B\u5982\uFF1A'+esc((template.model_examples || ['anthropic/claude-sonnet-4']).join(' / '))+'</div></div>' + '<div><label>API</label><input data-field="api" data-index="'+index+'" value="'+esc(model.api || model.api_base_url || '')+'" placeholder="'+esc(template.api || 'https://...')+'"></div>' + '<div><label>Key</label><input data-field="key" data-index="'+index+'" value="'+esc(model.key || model.api_key || '')+'" placeholder="'+esc(template.key_placeholder || 'sk-...')+'"></div>' + '<div><label>Thinking</label><select data-field="thinking_profile" data-index="'+index+'"><option value="">default</option><option value="off"'+(((model.thinking === 'off') || model.thinking?.mode === 'off') ? ' selected' : '')+'>off</option><option value="auto"'+(((model.thinking === 'auto') || model.thinking?.mode === 'auto') ? ' selected' : '')+'>auto</option><option value="on"'+(((model.thinking === 'on') || (model.thinking?.mode === 'on' && !model.thinking?.effort)) ? ' selected' : '')+'>on</option><option value="low"'+(((model.thinking === 'low') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'low' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>low</option><option value="medium"'+(((model.thinking === 'medium') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'medium' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>medium</option><option value="high"'+(((model.thinking === 'high') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'high' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>high</option><option value="custom"'+(((typeof model.thinking === 'object') && model.thinking && model.thinking.budget_tokens) ? ' selected' : '')+'>custom</option></select></div>' + '<div><label>Thinking mode</label><select data-field="thinking_mode" data-index="'+index+'"><option value="">default</option><option value="off"'+(model.thinking?.mode === 'off' ? ' selected' : '')+'>off</option><option value="auto"'+(model.thinking?.mode === 'auto' ? ' selected' : '')+'>auto</option><option value="on"'+(model.thinking?.mode === 'on' ? ' selected' : '')+'>on</option></select></div>' + '<div><label>Thinking effort</label><select data-field="thinking_effort" data-index="'+index+'"><option value="">default</option><option value="low"'+(model.thinking?.effort === 'low' ? ' selected' : '')+'>low</option><option value="medium"'+(model.thinking?.effort === 'medium' ? ' selected' : '')+'>medium</option><option value="high"'+(model.thinking?.effort === 'high' ? ' selected' : '')+'>high</option></select></div>' + '<div><label>Thinking budget</label><input data-field="thinking_budget_tokens" data-index="'+index+'" value="'+esc(model.thinking?.budget_tokens || '')+'" placeholder="1024"></div>' + '<div><label>Vendor hint</label><input data-field="vendor_hint" data-index="'+index+'" value="'+esc(model.metadata?.vendor_hint || '')+'" placeholder="'+esc(template.vendor_hint || 'openrouter')+'"></div>' + '<div><label>Reasoning support</label><select data-field="supports_reasoning" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_reasoning === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_reasoning === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Tool support</label><select data-field="supports_tools" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_tools === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_tools === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Image support</label><select data-field="supports_images" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_images === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_images === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div style="grid-column:1/-1"><label>Metadata (advanced JSON)</label><textarea data-field="metadata" data-index="'+index+'" placeholder="{\\"label\\":\\"Balanced profile\\"}">'+esc(model.metadata ? JSON.stringify(model.metadata, null, 2) : '')+'</textarea><div class="muted">\u666E\u901A capability \u5EFA\u8BAE\u4F18\u5148\u4F7F\u7528\u4E0A\u9762\u7684\u663E\u5F0F\u5B57\u6BB5\uFF1B\u8FD9\u91CC\u4FDD\u7559\u7ED9\u9AD8\u7EA7\u6269\u5C55\u5143\u6570\u636E\u3002</div></div>' + '</div>' + '</div>'; }).join('');}function extractModelsFromForm(){ const cards=Array.from(modelsFormGrid.querySelectorAll('[data-model-card]')); return cards.map((card,index)=>{ const read=(field)=>card.querySelector('[data-field="'+field+'"][data-index="'+index+'"]'); const providerTemplate=(read('provider_template')?.value || '').trim(); const metadataRaw=(read('metadata')?.value || '').trim(); let metadata; if(metadataRaw){ metadata=JSON.parse(metadataRaw); } else { metadata={}; } const thinkingProfile=(read('thinking_profile')?.value || '').trim(); const vendorHint=(read('vendor_hint')?.value || '').trim(); const supportsReasoning=(read('supports_reasoning')?.value || '').trim(); const supportsTools=(read('supports_tools')?.value || '').trim(); const supportsImages=(read('supports_images')?.value || '').trim(); const thinking={}; const mode=(read('thinking_mode')?.value || '').trim(); const effort=(read('thinking_effort')?.value || '').trim(); const budget=(read('thinking_budget_tokens')?.value || '').trim(); if(mode) thinking.mode=mode; if(effort) thinking.effort=effort; if(budget) thinking.budget_tokens=Number(budget); const model={ id:(read('id')?.value || '').trim(), api:(read('api')?.value || '').trim(), key:(read('key')?.value || '').trim(), interface:(read('interface')?.value || '').trim(), model:(read('model')?.value || '').trim(), }; if(vendorHint){ metadata.vendor_hint=vendorHint; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'vendor_hint')){ delete metadata.vendor_hint; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No trigger rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); if(triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length){ payload.TriggerRouter={ ...(payload.TriggerRouter || {}), enabled: triggerEnabled.checked, analysis_scope: triggerAnalysisScope.value || 'last_message', llm_intent_recognition: triggerIntentEnabled.checked, intent_model: triggerIntentModel.value.trim(), rules: triggerRules }; } else { delete payload.TriggerRouter; } const smartCandidates=extractSmartCandidatesFromForm(); if(smartEnabled.checked || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim()){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: smartEnabled.checked, router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined }; } else { delete payload.SmartRouter; } const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim() || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, sticky:{ ...((payload.Governance && payload.Governance.sticky) || {}), enabled: Boolean(governanceEnabled.checked || governanceAlignmentEnabled.checked), alignment:{ ...(((payload.Governance && payload.Governance.sticky && payload.Governance.sticky.alignment) || {})), enabled: governanceAlignmentEnabled.checked, summarizer_model: governanceSummarizerModel.value.trim() } }, semantic:{ ...((payload.Governance && payload.Governance.semantic) || {}), enabled: governanceSemanticEnabled.checked, mode:'classifier', classifier_model: governanceClassifierModel.value.trim() }, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(trigger.enabled); triggerIntentEnabled.checked=Boolean(trigger.llm_intent_recognition); triggerAnalysisScope.value=trigger.analysis_scope || 'last_message'; triggerIntentModel.value=trigger.intent_model || ''; renderTriggerRulesList(trigger.rules || []); const smart=config?.SmartRouter || {}; smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(governance.sticky?.alignment?.enabled); governanceSummarizerModel.value=governance.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(governance.semantic?.enabled); governanceClassifierModel.value=governance.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u540C\u6B65 Models \u8868\u5355\u5230 JSON \u8349\u7A3F'; } catch (error) { draftPreviewStatus.textContent='\u540C\u6B65\u5931\u8D25\uFF1A'+error.message; }}function applyReferenceSuggestion(path,modelId){ if(!modelId){ return; } if(path==='Router.default'){ draftRouterDefault.value=modelId; syncDraftEditorFromForm(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 Router.default'; return; } const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const pathMatch=path.match(/^([^.[]+)(?:.(.+))?$/); if(!pathMatch){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\uFF1A'+path; return; } const tokens=path.replace(/[(d+)]/g,'.$1').split('.'); let cursor=payload; for(let i=0;i<tokens.length-1;i++){ const token=tokens[i]; const nextToken=tokens[i+1]; if(cursor[token] === undefined){ cursor[token]=String(Number(nextToken))===nextToken ? [] : {}; } cursor=cursor[token]; } cursor[tokens[tokens.length-1]]=modelId; currentDraftConfig=payload; if(payload.Router?.default){ draftRouterDefault.value=payload.Router.default; } renderConfigControlForms(payload); configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyCapabilityWarningSuggestion(path,code){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const tokens=String(path || '').replace(/[(d+)]/g,'.$1').split('.').filter(Boolean); if(!tokens.length){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } let cursor=payload; for(let i=0;i<tokens.length-1;i++){ if(cursor == null){ break; } cursor=cursor[tokens[i]]; } const lastToken=tokens[tokens.length-1]; if(code==='thinking_ignored'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } } else if(code==='tools_text_fallback' || code==='images_text_fallback'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } if(cursor && !Object.keys(cursor).length){ const parentTokens=tokens.slice(0,-1); const maybeMetadataKey=parentTokens[parentTokens.length-1]; if(maybeMetadataKey==='metadata'){ let parentCursor=payload; for(let i=0;i<parentTokens.length-1;i++){ if(parentCursor == null){ break; } parentCursor=parentCursor[parentTokens[i]]; } if(parentCursor && Object.prototype.hasOwnProperty.call(parentCursor,'metadata')){ delete parentCursor.metadata; } } } } else { draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528 warning \u4FEE\u6B63\uFF1A'+code+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function renderCompiledDiff(diff){ const summary=diff?.summary || {}; compiledDiffSummary.innerHTML=[ ['Added providers', summary.addedProviders ?? 0], ['Removed providers', summary.removedProviders ?? 0], ['Changed providers', summary.changedProviders ?? 0], ['Added models', summary.addedModels ?? 0], ['Removed models', summary.removedModels ?? 0], ['Changed models', summary.changedModels ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const rows=[ ...((diff?.providerChanges || []).map(item=>({ scope:'provider', key:item.name, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ...((diff?.modelChanges || []).map(item=>({ scope:'model', key:item.modelId, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ]; compiledDiffTableBody.innerHTML=rows.length ? rows.map(item=>'<tr>' + '<td>'+esc(item.scope)+'</td>' + '<td>'+esc(item.type)+'</td>' + '<td><code>'+esc(item.key)+'</code></td>' + '<td>'+esc(item.fields.join(', ') || '-')+'</td>' + '<td><code>'+esc(item.target.providerName || item.target.name || '-')+'</code><div class="muted">'+esc(item.target.modelName || (item.target.models || []).join(', ') || '-')}</div></td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled registry changes</td></tr>';}function renderReferenceImpact(impact){ const summary=impact?.summary || {}; referenceImpactSummary.innerHTML=[ ['Total refs', summary.total ?? 0], ['modelId refs', summary.modelIdRefs ?? 0], ['Legacy refs', summary.legacyRefs ?? 0], ['Valid modelIds', summary.validModelIds ?? 0], ['Missing modelIds', summary.missingModelIds ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const entries=impact?.entries || []; referenceImpactTableBody.innerHTML=entries.length ? entries.map(item=>'<tr>' + '<td><code>'+esc(item.path)+'</code></td>' + '<td><code>'+esc(item.value)+'</code></td>' + '<td>'+esc(item.referenceType)+'</td>' + '<td>'+esc(item.status)+'</td>' + '<td><code>'+esc(item.resolvedTarget?.providerName || '-')+'</code><div class="muted">'+esc(item.resolvedTarget?.modelName || '-')}</div></td>' + '<td>'+((item.suggestions || []).length ? item.suggestions.map(s=>'<div><code>'+esc(s.modelId)+'</code><div class="muted">'+esc(s.modelName || '-')+'</div><button type="button" data-apply-reference-path="'+esc(item.path)+'" data-apply-reference-model="'+esc(s.modelId)+'">\u5E94\u7528\u5EFA\u8BAE</button></div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No model references found</td></tr>';}function renderCompiledModels(data){ const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04'; compiledProvidersTableBody.innerHTML=providers.length ? providers.map(provider=>'<tr>' + '<td><code>'+esc(provider.name)+'</code><div class="muted">'+esc(provider.api_base_url || '-')+'</div></td>' + '<td>'+esc(provider.transformer?.use?.[0] || '-')+'</td>' + '<td>'+esc((provider.models || []).join(', ') || '-')+'</td>' + '<td>'+esc(JSON.stringify(provider.transformer || {}))+'</td>' + '<td>'+esc(provider.has_api_key ? 'configured' : 'missing')+'</td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled providers</td></tr>'; compiledModelMapTableBody.innerHTML=modelMapEntries.length ? modelMapEntries.map(([modelId,item])=>'<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td><code>'+esc(item.providerName || '-')+'</code><div class="muted">'+esc(item.modelName || '-')+'</div></td>' + '<td>'+esc(item.protocol || '-')+'</td>' + '<td>'+esc(item.compatibilityProfile || '-')+'</td>' + '<td>'+esc(item.dispatchFormat || '-')+'</td>' + '<td><code>'+esc(JSON.stringify(item.thinking || { mode: 'off' }))+'</code></td>' + '<td><code>'+esc(JSON.stringify(item.capabilities || {}))+'</code></td>' + '<td>'+esc(item.source || '-')+'</td>' + '</tr>').join('') : '<tr><td colspan="8" class="muted">No compiled model map</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || []); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || []); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addCascadeLevel(){ const next=extractCascadeLevelsFromForm(); next.push({ from:'', to:'' }); renderCascadeLevelsList(next); syncDraftEditorFromForm(); }modelsFormGrid.addEventListener('input',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('change',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-template]'); if(applyBtn){ const applyIndex=Number(applyBtn.dataset.applyTemplate); applyProviderTemplate(applyIndex); syncDraftEditorFromForm(); return; } const btn=e.target.closest('button[data-remove-model]'); if(!btn){ return; } const removeIndex=Number(btn.dataset.removeModel); const nextModels=extractModelsFromForm().filter((_,index)=>index!==removeIndex); renderModelsForm(nextModels); syncDraftEditorFromForm(); });triggerRulesList.addEventListener('input',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('change',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('click',(e)=>{ const addKeywordBtn=e.target.closest('button[data-add-trigger-keyword]'); if(addKeywordBtn){ addTriggerKeyword(Number(addKeywordBtn.dataset.addTriggerKeyword), Number(addKeywordBtn.dataset.patternIndex)); return; } const removeKeywordBtn=e.target.closest('button[data-remove-trigger-keyword]'); if(removeKeywordBtn){ const ruleIndex=Number(removeKeywordBtn.dataset.removeTriggerKeyword); const patternIndex=Number(removeKeywordBtn.dataset.patternIndex); const keywordIndex=Number(removeKeywordBtn.dataset.keywordIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex] && next[ruleIndex].patterns && next[ruleIndex].patterns[patternIndex]){ const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=(pattern.keywords || []).filter((_,index)=>index!==keywordIndex); if(!pattern.keywords.length){ pattern.keywords=['']; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const addBtn=e.target.closest('button[data-add-trigger-pattern]'); if(addBtn){ addTriggerPattern(Number(addBtn.dataset.addTriggerPattern)); return; } const removePatternBtn=e.target.closest('button[data-remove-trigger-pattern]'); if(removePatternBtn){ const ruleIndex=Number(removePatternBtn.dataset.removeTriggerPattern); const patternIndex=Number(removePatternBtn.dataset.patternIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex]){ next[ruleIndex].patterns=(next[ruleIndex].patterns || []).filter((_,index)=>index!==patternIndex); if(!next[ruleIndex].patterns.length){ next[ruleIndex].patterns=[{ type:'exact', keywords:[] }]; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const btn=e.target.closest('button[data-remove-trigger-rule]'); if(!btn){ return; } const next=extractTriggerRulesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeTriggerRule)); renderTriggerRulesList(next); syncDraftEditorFromForm(); });smartCandidatesList.addEventListener('input',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('change',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-smart-candidate]'); if(!btn){ return; } const next=extractSmartCandidatesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeSmartCandidate)); renderSmartCandidatesList(next); syncDraftEditorFromForm(); });governanceCascadeLevelsList.addEventListener('input',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('change',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-cascade-level]'); if(!btn){ return; } const next=extractCascadeLevelsFromForm().filter((_,index)=>index!==Number(btn.dataset.removeCascadeLevel)); renderCascadeLevelsList(next); syncDraftEditorFromForm(); });referenceImpactTableBody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-apply-reference-path]'); if(!btn){ return; } applyReferenceSuggestion(btn.dataset.applyReferencePath, btn.dataset.applyReferenceModel); });draftValidationList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });capabilityWarningsList.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-warning-path]'); if(applyBtn){ applyCapabilityWarningSuggestion(applyBtn.dataset.applyWarningPath, applyBtn.dataset.applyWarningCode); return; } const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });function renderMetrics(metrics){ metricsGrid.innerHTML=[ ['Recent traces', metrics.totalTraces ?? 0], ['Sticky hit rate', pct(metrics.stickyHitRate)], ['Cascade rate', pct(metrics.cascadeTriggeredRate)], ['Shadow rate', pct(metrics.shadowCheckedRate)], ['Alignment rate', pct(metrics.alignmentUsedRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.TriggerRouter={ ...(payload.TriggerRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.TriggerRouter?.enabled), analysis_scope: payload.TriggerRouter?.analysis_scope || 'last_message', llm_intent_recognition: payload.TriggerRouter?.llm_intent_recognition || false, intent_model: payload.TriggerRouter?.intent_model || '', rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.TriggerRouter?.rules || []) }; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), sticky:{ ...((payload.Governance && payload.Governance.sticky) || {}), alignment:{ ...(((payload.Governance && payload.Governance.sticky && payload.Governance.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.Governance?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.Governance?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.Governance && payload.Governance.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.Governance?.semantic?.enabled), mode:(payload.Governance?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.Governance?.semantic?.classifier_model || '') }, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || []); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderAnomalies(anomalies){ if(!anomalies || !anomalies.length){ anomalyList.innerHTML='<div class="alert info"><strong>No active alerts</strong><div class="muted">\u5F53\u524D\u7A97\u53E3\u672A\u53D1\u73B0\u660E\u663E\u6CBB\u7406\u5F02\u5E38</div></div>'; return; } anomalyList.innerHTML=anomalies.map(item=>'<div class="alert '+esc(item.severity || 'info')+'"><strong>'+esc(item.type)+'</strong><div>'+esc(item.message)+'</div></div>').join('');}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')}</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || []); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); renderMetrics(metricsData.metrics || {}); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || []); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderTrendTable(metricsData || {}); const traces=data.traces || []; if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`
3284
3585
  );
3285
3586
  });
3286
3587
  return server;
@@ -3505,6 +3806,16 @@ function isProcessAlive(pid) {
3505
3806
  return false;
3506
3807
  }
3507
3808
  }
3809
+ async function waitForProcessExit(pid, timeoutMs = 5e3) {
3810
+ const startedAt = Date.now();
3811
+ while (Date.now() - startedAt < timeoutMs) {
3812
+ if (!isProcessAlive(pid)) {
3813
+ return true;
3814
+ }
3815
+ await new Promise((resolve) => setTimeout(resolve, 100));
3816
+ }
3817
+ return !isProcessAlive(pid);
3818
+ }
3508
3819
  function killProcess(pid) {
3509
3820
  if (process.platform === "win32") {
3510
3821
  (0, import_child_process.spawnSync)("taskkill", ["/F", "/PID", String(pid)], { timeout: 5e3 });
@@ -3576,18 +3887,18 @@ var init_SSESerializer_transform = __esm({
3576
3887
  constructor() {
3577
3888
  const transformStream = new TransformStream({
3578
3889
  transform: (event2, controller) => {
3579
- let output2 = "";
3890
+ let output3 = "";
3580
3891
  if (event2.event) {
3581
- output2 += `event: ${event2.event}
3892
+ output3 += `event: ${event2.event}
3582
3893
  `;
3583
3894
  }
3584
3895
  if (event2.data) {
3585
3896
  const dataStr = typeof event2.data === "string" ? event2.data : JSON.stringify(event2.data);
3586
- output2 += `data: ${dataStr}
3897
+ output3 += `data: ${dataStr}
3587
3898
  `;
3588
3899
  }
3589
- output2 += "\n";
3590
- controller.enqueue(new TextEncoder().encode(output2));
3900
+ output3 += "\n";
3901
+ controller.enqueue(new TextEncoder().encode(output3));
3591
3902
  }
3592
3903
  });
3593
3904
  this.readable = transformStream.readable;
@@ -4285,7 +4596,7 @@ Important: Respond ONLY with the JSON, no additional text.`;
4285
4596
  });
4286
4597
 
4287
4598
  // src/trigger/smart-router.ts
4288
- var import_lru_cache7, SMART_ROUTER_PROMPT, SmartRouterSelector, smartRouterSelector;
4599
+ var import_lru_cache7, SmartRouterSelector, smartRouterSelector;
4289
4600
  var init_smart_router = __esm({
4290
4601
  "src/trigger/smart-router.ts"() {
4291
4602
  "use strict";
@@ -4294,26 +4605,6 @@ var init_smart_router = __esm({
4294
4605
  init_log();
4295
4606
  init_message_ir();
4296
4607
  init_anthropic();
4297
- SMART_ROUTER_PROMPT = `You are a model routing assistant. Your job is to select the most appropriate AI model from the given candidates to handle the user's request.
4298
-
4299
- User request:
4300
- """
4301
- {request}
4302
- """
4303
-
4304
- Available models:
4305
- {candidates}
4306
-
4307
- Select the most appropriate model and respond in the following JSON format ONLY:
4308
- {
4309
- "model": "<exact model identifier from the list>",
4310
- "confidence": <0.0-1.0>,
4311
- "reasoning": "<brief explanation>"
4312
- }
4313
-
4314
- Important:
4315
- - The "model" field MUST be one of the exact identifiers listed above
4316
- - Respond ONLY with the JSON, no additional text`;
4317
4608
  SmartRouterSelector = class {
4318
4609
  cache;
4319
4610
  constructor() {
@@ -4346,8 +4637,42 @@ Important:
4346
4637
  /**
4347
4638
  * 构建完整 prompt
4348
4639
  */
4349
- buildPrompt(text, candidates) {
4350
- return SMART_ROUTER_PROMPT.replace("{request}", text).replace("{candidates}", this.buildCandidatesList(candidates));
4640
+ buildPrompt(text, candidates, hint) {
4641
+ const sections = [
4642
+ "You are a model routing assistant. Your job is to select the most appropriate AI model from the given candidates to handle the user's request."
4643
+ ];
4644
+ if (hint?.taskSummary) {
4645
+ sections.push(`Task summary:
4646
+ """
4647
+ ${hint.taskSummary}
4648
+ """`);
4649
+ }
4650
+ if (hint?.topRouteCandidates?.length) {
4651
+ sections.push(
4652
+ "Pre-filtered route candidates:\n" + hint.topRouteCandidates.map(
4653
+ (candidate, index) => `${index + 1}. ${candidate.name} -> ${candidate.model}${candidate.description ? ` (${candidate.description})` : ""}${candidate.confidence !== void 0 ? ` [confidence=${candidate.confidence}]` : ""}`
4654
+ ).join("\n")
4655
+ );
4656
+ }
4657
+ sections.push(
4658
+ `User request:
4659
+ """
4660
+ ${text}
4661
+ """`,
4662
+ `Available models:
4663
+ ${this.buildCandidatesList(candidates)}`,
4664
+ `Select the most appropriate model and respond in the following JSON format ONLY:
4665
+ {
4666
+ "model": "<exact model identifier from the list>",
4667
+ "confidence": <0.0-1.0>,
4668
+ "reasoning": "<brief explanation>"
4669
+ }
4670
+
4671
+ Important:
4672
+ - The "model" field MUST be one of the exact identifiers listed above
4673
+ - Respond ONLY with the JSON, no additional text`
4674
+ );
4675
+ return sections.join("\n\n");
4351
4676
  }
4352
4677
  /**
4353
4678
  * 使用 LLM 选择最优模型
@@ -4358,7 +4683,7 @@ Important:
4358
4683
  * @param fetchFn 可注入的 fetch 函数(用于测试)
4359
4684
  * @returns 选择结果,失败时返回 null
4360
4685
  */
4361
- async selectModel(text, config, port = DEFAULT_CONFIG2.PORT, fetchFn, apiKey, timeoutMs) {
4686
+ async selectModel(text, config, port = DEFAULT_CONFIG2.PORT, fetchFn, apiKey, timeoutMs, hint) {
4362
4687
  if (!config.enabled) {
4363
4688
  return null;
4364
4689
  }
@@ -4372,11 +4697,12 @@ Important:
4372
4697
  }
4373
4698
  try {
4374
4699
  const fetchImpl = fetchFn || fetch;
4375
- const prompt = this.buildPrompt(text, config.candidates);
4700
+ const prompt = this.buildPrompt(text, config.candidates, hint);
4376
4701
  const response = await fetchImpl(`http://127.0.0.1:${port}/v1/messages`, {
4377
4702
  method: "POST",
4378
4703
  headers: {
4379
4704
  "Content-Type": "application/json",
4705
+ "x-ctr-smart-router": "1",
4380
4706
  ...apiKey ? { "x-api-key": apiKey } : {}
4381
4707
  },
4382
4708
  body: JSON.stringify(
@@ -4438,6 +4764,82 @@ var init_selector = __esm({
4438
4764
  init_semantic_router();
4439
4765
  init_compile();
4440
4766
  ModelSelector = class {
4767
+ resolveRouteModel(appConfig, ref) {
4768
+ if (!ref) {
4769
+ return void 0;
4770
+ }
4771
+ return appConfig ? resolveModelReference(appConfig, ref) ?? ref : ref;
4772
+ }
4773
+ buildSemanticCandidates(rules, governanceConfig) {
4774
+ const defaultThreshold = governanceConfig?.semantic?.threshold;
4775
+ const legacyPrototypes = governanceConfig?.semantic?.prototypes ?? {};
4776
+ return this.sortRulesByPriority(rules).map((rule) => {
4777
+ const prototype = rule.semantic_profile?.prototype ?? legacyPrototypes[rule.name] ?? rule.description;
4778
+ const semanticEnabled = rule.semantic_profile?.enabled !== false && Boolean(prototype);
4779
+ if (!semanticEnabled || !prototype) {
4780
+ return null;
4781
+ }
4782
+ return {
4783
+ rule,
4784
+ prototype,
4785
+ threshold: rule.semantic_profile?.threshold ?? defaultThreshold
4786
+ };
4787
+ }).filter(Boolean);
4788
+ }
4789
+ getStickyCorrection(text, req, governanceConfig) {
4790
+ if (!governanceConfig?.enabled || !governanceConfig.sticky?.enabled || !req.sessionId) {
4791
+ return {};
4792
+ }
4793
+ const fingerprint = createTaskFingerprint(text);
4794
+ const sessionState = sessionStateStore.get(req.sessionId);
4795
+ if (!fingerprint || sessionState?.lastTaskFingerprint !== fingerprint || !(sessionState.preferredModel || sessionState.lastSuccessfulModel)) {
4796
+ return {};
4797
+ }
4798
+ return {
4799
+ fingerprint,
4800
+ sessionModel: sessionState.preferredModel || sessionState.lastSuccessfulModel
4801
+ };
4802
+ }
4803
+ applyStickyCorrection(candidate, sticky, appConfig) {
4804
+ if (!sticky.sessionModel) {
4805
+ return candidate;
4806
+ }
4807
+ const stickyModel = this.resolveRouteModel(appConfig, sticky.sessionModel);
4808
+ if (!stickyModel) {
4809
+ return candidate;
4810
+ }
4811
+ if (!candidate) {
4812
+ log(`[StickyRouting] Reusing model "${stickyModel}" as unified router correction`);
4813
+ return {
4814
+ matched: true,
4815
+ model: stickyModel,
4816
+ confidence: 0.95,
4817
+ analysisTime: 0,
4818
+ routeSource: "sticky_correction"
4819
+ };
4820
+ }
4821
+ if (candidate.model === stickyModel) {
4822
+ return candidate;
4823
+ }
4824
+ log(`[StickyRouting] Correcting selected model "${candidate.model}" -> "${stickyModel}"`);
4825
+ return {
4826
+ ...candidate,
4827
+ model: stickyModel,
4828
+ confidence: Math.max(candidate.confidence, 0.95),
4829
+ routeSource: "sticky_correction"
4830
+ };
4831
+ }
4832
+ buildSmartRouterHint(text, rules) {
4833
+ return {
4834
+ taskSummary: text.slice(0, 240),
4835
+ topRouteCandidates: this.sortRulesByPriority(rules).filter((rule) => rule.description).slice(0, 3).map((rule) => ({
4836
+ name: rule.name,
4837
+ model: rule.model,
4838
+ description: rule.description,
4839
+ confidence: void 0
4840
+ }))
4841
+ };
4842
+ }
4441
4843
  /**
4442
4844
  * 按优先级排序规则
4443
4845
  * 优先级数值越大,优先级越高
@@ -4522,33 +4924,29 @@ var init_selector = __esm({
4522
4924
  routeSource: "trigger_rule"
4523
4925
  };
4524
4926
  }
4525
- if (governanceConfig?.enabled && governanceConfig.sticky?.enabled && req.sessionId) {
4526
- const fingerprint = createTaskFingerprint(text);
4527
- const sessionState = sessionStateStore.get(req.sessionId);
4528
- if (fingerprint && sessionState?.lastTaskFingerprint === fingerprint && (sessionState.preferredModel || sessionState.lastSuccessfulModel)) {
4529
- const stickyModel = sessionState.preferredModel || sessionState.lastSuccessfulModel;
4530
- if (stickyModel) {
4531
- log(`[StickyRouting] Reusing model "${stickyModel}" for session "${req.sessionId}"`);
4532
- return {
4533
- matched: true,
4534
- model: appConfig ? resolveModelReference(appConfig, stickyModel) ?? stickyModel : stickyModel,
4535
- confidence: 0.95,
4536
- analysisTime: Date.now() - startTime,
4537
- analyzedText: text,
4538
- routeSource: "sticky"
4539
- };
4540
- }
4541
- }
4542
- }
4543
- if (governanceConfig?.enabled && governanceConfig.semantic?.enabled) {
4544
- const semanticResult = governanceConfig.semantic.mode === "classifier" ? await semanticRouter.analyzeWithClassifier(
4927
+ const stickyCorrection = this.getStickyCorrection(text, req, governanceConfig);
4928
+ const semanticCandidates = this.buildSemanticCandidates(config.rules, governanceConfig);
4929
+ if (governanceConfig?.enabled && governanceConfig.semantic?.enabled && semanticCandidates.length > 0) {
4930
+ const semanticConfig = {
4931
+ ...governanceConfig.semantic,
4932
+ prototypes: Object.fromEntries(semanticCandidates.map((candidate) => [candidate.rule.name, candidate.prototype]))
4933
+ };
4934
+ const semanticResult = semanticConfig.mode === "classifier" ? await semanticRouter.analyzeWithClassifier(
4545
4935
  text,
4546
- governanceConfig.semantic,
4936
+ semanticConfig,
4547
4937
  port,
4548
4938
  void 0,
4549
4939
  apiKey,
4550
4940
  timeoutMs
4551
- ) : semanticRouter.analyze(text, governanceConfig.semantic);
4941
+ ) : semanticRouter.analyzeCandidates(
4942
+ text,
4943
+ semanticCandidates.map((candidate) => ({
4944
+ intent: candidate.rule.name,
4945
+ prototype: candidate.prototype,
4946
+ threshold: candidate.threshold
4947
+ })),
4948
+ semanticConfig.threshold
4949
+ );
4552
4950
  if (semanticResult) {
4553
4951
  const matchedRule = config.rules.find(
4554
4952
  (rule) => rule.enabled !== false && rule.name.toLowerCase() === semanticResult.intent.toLowerCase()
@@ -4558,15 +4956,16 @@ var init_selector = __esm({
4558
4956
  if (req.governanceTrace) {
4559
4957
  req.governanceTrace.semanticIntent = semanticResult.intent;
4560
4958
  }
4561
- return {
4959
+ const semanticSelection = {
4562
4960
  matched: true,
4563
4961
  rule: matchedRule,
4564
- model: appConfig ? resolveModelReference(appConfig, matchedRule.model) ?? matchedRule.model : matchedRule.model,
4962
+ model: this.resolveRouteModel(appConfig, matchedRule.model),
4565
4963
  confidence: semanticResult.confidence,
4566
4964
  analysisTime: Date.now() - startTime,
4567
4965
  analyzedText: text,
4568
- routeSource: "intent"
4966
+ routeSource: "semantic_match"
4569
4967
  };
4968
+ return this.applyStickyCorrection(semanticSelection, stickyCorrection, appConfig) ?? semanticSelection;
4570
4969
  }
4571
4970
  }
4572
4971
  }
@@ -4574,16 +4973,24 @@ var init_selector = __esm({
4574
4973
  try {
4575
4974
  const resolvedSmartRouterConfig = appConfig ? {
4576
4975
  ...smartRouterConfig,
4577
- router_model: resolveModelReference(appConfig, smartRouterConfig.router_model) ?? smartRouterConfig.router_model,
4976
+ router_model: this.resolveRouteModel(appConfig, smartRouterConfig.router_model) ?? smartRouterConfig.router_model,
4578
4977
  candidates: smartRouterConfig.candidates.map((candidate) => ({
4579
4978
  ...candidate,
4580
- model: resolveModelReference(appConfig, candidate.model) ?? candidate.model
4979
+ model: this.resolveRouteModel(appConfig, candidate.model) ?? candidate.model
4581
4980
  }))
4582
4981
  } : smartRouterConfig;
4583
- const smartResult = await smartRouterSelector.selectModel(text, resolvedSmartRouterConfig, port, void 0, apiKey, timeoutMs);
4982
+ const smartResult = await smartRouterSelector.selectModel(
4983
+ text,
4984
+ resolvedSmartRouterConfig,
4985
+ port,
4986
+ void 0,
4987
+ apiKey,
4988
+ timeoutMs,
4989
+ this.buildSmartRouterHint(text, config.rules)
4990
+ );
4584
4991
  if (smartResult) {
4585
4992
  log(`[SmartRouter] Selected model "${smartResult.model}" (confidence: ${smartResult.confidence})`);
4586
- return {
4993
+ const smartSelection = {
4587
4994
  matched: true,
4588
4995
  model: smartResult.model,
4589
4996
  confidence: smartResult.confidence,
@@ -4591,6 +4998,7 @@ var init_selector = __esm({
4591
4998
  analyzedText: text,
4592
4999
  routeSource: "smart_router"
4593
5000
  };
5001
+ return this.applyStickyCorrection(smartSelection, stickyCorrection, appConfig) ?? smartSelection;
4594
5002
  }
4595
5003
  } catch (error) {
4596
5004
  logError("[ModelSelector] SmartRouter error:", error);
@@ -4602,21 +5010,30 @@ var init_selector = __esm({
4602
5010
  if (intentResult.confidence > 0.5 && intentResult.intent !== "general") {
4603
5011
  const matchedRule = intentDetector.findRuleByIntent(intentResult.intent, config.rules);
4604
5012
  if (matchedRule) {
4605
- return {
5013
+ const intentSelection = {
4606
5014
  matched: true,
4607
5015
  rule: matchedRule,
4608
- model: appConfig ? resolveModelReference(appConfig, matchedRule.model) ?? matchedRule.model : matchedRule.model,
5016
+ model: this.resolveRouteModel(appConfig, matchedRule.model),
4609
5017
  confidence: intentResult.confidence,
4610
5018
  analysisTime: Date.now() - startTime,
4611
5019
  analyzedText: text,
4612
- routeSource: "intent"
5020
+ routeSource: "intent_fallback"
4613
5021
  };
5022
+ return this.applyStickyCorrection(intentSelection, stickyCorrection, appConfig) ?? intentSelection;
4614
5023
  }
4615
5024
  }
4616
5025
  } catch (error) {
4617
5026
  logError("[ModelSelector] Intent detection error:", error);
4618
5027
  }
4619
5028
  }
5029
+ const stickyOnlySelection = this.applyStickyCorrection(null, stickyCorrection, appConfig);
5030
+ if (stickyOnlySelection) {
5031
+ return {
5032
+ ...stickyOnlySelection,
5033
+ analysisTime: Date.now() - startTime,
5034
+ analyzedText: text
5035
+ };
5036
+ }
4620
5037
  return {
4621
5038
  matched: false,
4622
5039
  confidence: 0,
@@ -4773,13 +5190,15 @@ var init_trigger = __esm({
4773
5190
  if (req.governanceTrace) {
4774
5191
  if (result.routeSource === "trigger_rule" && result.rule?.name) {
4775
5192
  appendTraceReason(req.governanceTrace, `trigger_rule:${result.rule.name}`);
4776
- } else if (result.routeSource === "sticky") {
5193
+ } else if (result.routeSource === "semantic_match" && result.rule?.name) {
5194
+ appendTraceReason(req.governanceTrace, `semantic_match:${result.rule.name}`);
5195
+ } else if (result.routeSource === "sticky_correction") {
4777
5196
  req.governanceTrace.stickyHit = true;
4778
- appendTraceReason(req.governanceTrace, "sticky_routing");
5197
+ appendTraceReason(req.governanceTrace, "sticky_correction");
4779
5198
  } else if (result.routeSource === "smart_router") {
4780
- appendTraceReason(req.governanceTrace, "smart_router");
4781
- } else if (result.routeSource === "intent") {
4782
- appendTraceReason(req.governanceTrace, "intent_detection");
5199
+ appendTraceReason(req.governanceTrace, "smart_decision");
5200
+ } else if (result.routeSource === "intent_fallback") {
5201
+ appendTraceReason(req.governanceTrace, "intent_fallback");
4783
5202
  } else {
4784
5203
  appendTraceReason(req.governanceTrace, "trigger_router:no_match");
4785
5204
  }
@@ -4837,7 +5256,7 @@ var init_trigger = __esm({
4837
5256
  req.body.model = result.model;
4838
5257
  req.triggerResult = result;
4839
5258
  log(
4840
- `[TriggerRouter] ${result.routeSource === "sticky" ? "Sticky routing selected" : result.rule ? `Matched rule "${result.rule.name}"` : "SmartRouter selected"} -> model "${result.model}" (confidence: ${result.confidence}, time: ${result.analysisTime}ms)`
5259
+ `[TriggerRouter] ${result.routeSource === "sticky_correction" ? "Sticky correction selected" : result.routeSource === "semantic_match" ? `Semantic match "${result.rule?.name}"` : result.routeSource === "smart_router" ? "Smart fallback selected" : result.routeSource === "intent_fallback" ? `Intent fallback "${result.rule?.name}"` : result.rule ? `Matched rule "${result.rule.name}"` : "Unified router selected"} -> model "${result.model}" (confidence: ${result.confidence}, time: ${result.analysisTime}ms)`
4841
5260
  );
4842
5261
  }
4843
5262
  } catch (error) {
@@ -4889,6 +5308,15 @@ function toOpenAIToolResultMessages(parts) {
4889
5308
  content: typeof part.content === "string" ? part.content : JSON.stringify(part.content)
4890
5309
  }));
4891
5310
  }
5311
+ function getToolName(tool) {
5312
+ return tool?.name ?? tool?.function?.name;
5313
+ }
5314
+ function getToolDescription(tool) {
5315
+ return tool?.description ?? tool?.function?.description;
5316
+ }
5317
+ function getToolInputSchema(tool) {
5318
+ return tool?.input_schema ?? tool?.function?.parameters;
5319
+ }
4892
5320
  function toOpenAITools(tools) {
4893
5321
  if (!Array.isArray(tools) || !tools.length) {
4894
5322
  return void 0;
@@ -4896,9 +5324,9 @@ function toOpenAITools(tools) {
4896
5324
  return tools.map((tool) => ({
4897
5325
  type: "function",
4898
5326
  function: {
4899
- name: tool.name,
4900
- description: tool.description,
4901
- parameters: tool.input_schema
5327
+ name: getToolName(tool),
5328
+ description: getToolDescription(tool),
5329
+ parameters: getToolInputSchema(tool)
4902
5330
  }
4903
5331
  }));
4904
5332
  }
@@ -4925,10 +5353,10 @@ function toOpenAIToolChoice(toolChoice) {
4925
5353
  }
4926
5354
  return toolChoice;
4927
5355
  }
4928
- function toOpenAIChatRequest(input2) {
5356
+ function toOpenAIChatRequest(input3) {
4929
5357
  const messages = [
4930
- ...input2.ir.system.map((text) => ({ role: "system", content: text })),
4931
- ...input2.ir.messages.flatMap((message) => {
5358
+ ...input3.ir.system.map((text) => ({ role: "system", content: text })),
5359
+ ...input3.ir.messages.flatMap((message) => {
4932
5360
  const content = toOpenAIContent(message.parts);
4933
5361
  const toolCalls = message.role === "assistant" ? toOpenAIToolCalls(message.parts) : [];
4934
5362
  const toolResults = toOpenAIToolResultMessages(message.parts);
@@ -4947,26 +5375,26 @@ function toOpenAIChatRequest(input2) {
4947
5375
  })
4948
5376
  ];
4949
5377
  const body = {
4950
- model: input2.model,
5378
+ model: input3.model,
4951
5379
  messages
4952
5380
  };
4953
- if (input2.max_completion_tokens !== void 0) {
4954
- body.max_completion_tokens = input2.max_completion_tokens;
5381
+ if (input3.max_completion_tokens !== void 0) {
5382
+ body.max_completion_tokens = input3.max_completion_tokens;
4955
5383
  }
4956
- if (input2.stream !== void 0) {
4957
- body.stream = input2.stream;
5384
+ if (input3.stream !== void 0) {
5385
+ body.stream = input3.stream;
4958
5386
  }
4959
- const tools = toOpenAITools(input2.tools);
5387
+ const tools = toOpenAITools(input3.tools);
4960
5388
  if (tools) {
4961
5389
  body.tools = tools;
4962
5390
  }
4963
- const toolChoice = toOpenAIToolChoice(input2.tool_choice);
5391
+ const toolChoice = toOpenAIToolChoice(input3.tool_choice);
4964
5392
  if (toolChoice !== void 0) {
4965
5393
  body.tool_choice = toolChoice;
4966
5394
  }
4967
- if (input2.ir.options?.thinking?.enabled) {
5395
+ if (input3.ir.options?.thinking?.enabled) {
4968
5396
  body.reasoning = {
4969
- ...input2.ir.options.thinking.effort ? { effort: input2.ir.options.thinking.effort } : {}
5397
+ ...input3.ir.options.thinking.effort ? { effort: input3.ir.options.thinking.effort } : {}
4970
5398
  };
4971
5399
  }
4972
5400
  return body;
@@ -4991,19 +5419,19 @@ function stringifyFallbackContent(value) {
4991
5419
  return String(value);
4992
5420
  }
4993
5421
  }
4994
- function applyCapabilityFallbacks(input2) {
5422
+ function applyCapabilityFallbacks(input3) {
4995
5423
  const diagnostics = [];
4996
- const nextRequest = { ...input2.request };
5424
+ const nextRequest = { ...input3.request };
4997
5425
  const nextIR = {
4998
- ...input2.ir,
4999
- system: [...input2.ir.system],
5000
- messages: input2.ir.messages.map((message) => ({
5426
+ ...input3.ir,
5427
+ system: [...input3.ir.system],
5428
+ messages: input3.ir.messages.map((message) => ({
5001
5429
  ...message,
5002
5430
  parts: message.parts.map((part) => ({ ...part }))
5003
5431
  })),
5004
- options: input2.ir.options ? { ...input2.ir.options } : void 0
5432
+ options: input3.ir.options ? { ...input3.ir.options } : void 0
5005
5433
  };
5006
- if (input2.capabilities?.thinking.supported === false && nextIR.options?.thinking) {
5434
+ if (input3.capabilities?.thinking.supported === false && nextIR.options?.thinking) {
5007
5435
  diagnostics.push("thinking_ignored");
5008
5436
  delete nextIR.options.thinking;
5009
5437
  delete nextRequest.thinking;
@@ -5014,7 +5442,7 @@ function applyCapabilityFallbacks(input2) {
5014
5442
  const hasImageParts = nextIR.messages.some(
5015
5443
  (message) => message.parts.some((part) => part.type === "image")
5016
5444
  );
5017
- if (input2.capabilities?.images === false && hasImageParts) {
5445
+ if (input3.capabilities?.images === false && hasImageParts) {
5018
5446
  diagnostics.push("images_text_fallback");
5019
5447
  nextIR.messages = nextIR.messages.map((message) => ({
5020
5448
  ...message,
@@ -5034,7 +5462,7 @@ function applyCapabilityFallbacks(input2) {
5034
5462
  const hasToolParts = nextIR.messages.some(
5035
5463
  (message) => message.parts.some((part) => part.type === "tool_call" || part.type === "tool_result")
5036
5464
  );
5037
- if (input2.capabilities?.tools === false && (Array.isArray(nextRequest.tools) && nextRequest.tools.length || hasToolParts)) {
5465
+ if (input3.capabilities?.tools === false && (Array.isArray(nextRequest.tools) && nextRequest.tools.length || hasToolParts)) {
5038
5466
  diagnostics.push("tools_text_fallback");
5039
5467
  delete nextRequest.tools;
5040
5468
  delete nextRequest.tool_choice;
@@ -5067,6 +5495,42 @@ function applyCapabilityFallbacks(input2) {
5067
5495
  request: nextRequest
5068
5496
  };
5069
5497
  }
5498
+ function describeProtocolDiagnostic(code) {
5499
+ switch (code) {
5500
+ case "thinking_ignored":
5501
+ return {
5502
+ code,
5503
+ severity: "info",
5504
+ label: "thinking \u5DF2\u5FFD\u7565",
5505
+ summary: "\u5F53\u524D\u6A21\u578B\u6216\u63A5\u53E3\u672A\u542F\u7528 reasoning \u80FD\u529B\uFF0C\u8BF7\u6C42\u4E2D\u7684 thinking \u8BBE\u7F6E\u4E0D\u4F1A\u7EE7\u7EED\u4F20\u7ED9\u4E0A\u6E38\u3002",
5506
+ action: "\u5982\u9700\u4FDD\u7559 thinking\uFF0C\u8BF7\u5207\u56DE\u652F\u6301 reasoning \u7684\u6A21\u578B\uFF0C\u6216\u79FB\u9664\u5F53\u524D\u6A21\u578B\u4E0A\u7684 thinking \u914D\u7F6E\u3002"
5507
+ };
5508
+ case "images_text_fallback":
5509
+ return {
5510
+ code,
5511
+ severity: "warn",
5512
+ label: "\u56FE\u7247\u5DF2\u964D\u7EA7\u4E3A\u6587\u672C",
5513
+ summary: "\u5F53\u524D\u6A21\u578B\u672A\u58F0\u660E\u56FE\u7247\u8F93\u5165\u80FD\u529B\uFF0C\u8BF7\u6C42\u4E2D\u7684\u56FE\u7247\u5185\u5BB9\u4F1A\u9000\u5316\u4E3A\u6587\u672C\u63D0\u793A\uFF0C\u4E0D\u4F1A\u539F\u6837\u53D1\u9001\u5230\u4E0A\u6E38\u3002",
5514
+ action: "\u5982\u9700\u4FDD\u7559\u56FE\u7247\u8F93\u5165\uFF0C\u8BF7\u542F\u7528 supports_images \u6216\u5207\u56DE\u652F\u6301\u56FE\u7247\u7684\u6A21\u578B\u3002"
5515
+ };
5516
+ case "tools_text_fallback":
5517
+ return {
5518
+ code,
5519
+ severity: "warn",
5520
+ label: "\u5DE5\u5177\u8C03\u7528\u5DF2\u964D\u7EA7\u4E3A\u6587\u672C",
5521
+ summary: "\u5F53\u524D\u6A21\u578B\u672A\u58F0\u660E\u5DE5\u5177\u80FD\u529B\uFF0Ctool definitions \u4E0E tool call/result \u4F1A\u9000\u5316\u4E3A\u666E\u901A\u6587\u672C\u5185\u5BB9\u3002",
5522
+ action: "\u5982\u9700\u4FDD\u7559\u5DE5\u5177\u8C03\u7528\uFF0C\u8BF7\u542F\u7528 supports_tools \u6216\u5207\u56DE\u652F\u6301\u5DE5\u5177\u7684\u6A21\u578B\u3002"
5523
+ };
5524
+ default:
5525
+ return {
5526
+ code,
5527
+ severity: "info",
5528
+ label: code,
5529
+ summary: "\u672A\u77E5\u534F\u8BAE\u8BCA\u65AD\u3002",
5530
+ action: "\u8BF7\u7ED3\u5408\u539F\u59CB\u8BF7\u6C42\u548C\u4E0A\u6E38\u54CD\u5E94\u7EE7\u7EED\u6392\u67E5\u3002"
5531
+ };
5532
+ }
5533
+ }
5070
5534
  function omitRequestFields(body) {
5071
5535
  const {
5072
5536
  model,
@@ -5076,57 +5540,64 @@ function omitRequestFields(body) {
5076
5540
  thinking,
5077
5541
  metadata,
5078
5542
  max_tokens,
5543
+ max_completion_tokens,
5079
5544
  ...rest
5080
5545
  } = body;
5081
5546
  return rest;
5082
5547
  }
5083
- function buildUpstreamRequestFromIR(input2) {
5548
+ function buildProviderDispatchRequestFromIR(input3) {
5084
5549
  const fallback = applyCapabilityFallbacks({
5085
- ir: input2.ir,
5086
- request: input2.request,
5087
- capabilities: input2.capabilities
5550
+ ir: input3.ir,
5551
+ request: input3.request,
5552
+ capabilities: input3.capabilities
5088
5553
  });
5089
5554
  const passthrough = omitRequestFields(fallback.request);
5090
- if (input2.interface === "anthropic") {
5555
+ const dispatchFormat = getDispatchFormatForProfile(input3.interface, input3.compatibilityProfile);
5556
+ if (dispatchFormat === "openai_chat") {
5091
5557
  return {
5558
+ dispatchFormat,
5092
5559
  diagnostics: fallback.diagnostics,
5093
5560
  ...passthrough,
5094
- ...toAnthropicMessagesRequest({
5095
- model: input2.model,
5096
- max_tokens: fallback.request.max_tokens,
5561
+ ...toOpenAIChatRequest({
5562
+ model: input3.model,
5563
+ max_completion_tokens: fallback.request.max_tokens ?? fallback.request.max_completion_tokens,
5097
5564
  stream: fallback.request.stream,
5098
- metadata: fallback.request.metadata,
5099
5565
  tools: fallback.request.tools,
5566
+ tool_choice: fallback.request.tool_choice,
5100
5567
  ir: fallback.ir
5101
5568
  })
5102
5569
  };
5103
5570
  }
5104
5571
  return {
5572
+ dispatchFormat,
5105
5573
  diagnostics: fallback.diagnostics,
5106
5574
  ...passthrough,
5107
- ...toOpenAIChatRequest({
5108
- model: input2.model,
5109
- max_completion_tokens: fallback.request.max_tokens ?? fallback.request.max_completion_tokens,
5575
+ ...toAnthropicMessagesRequest({
5576
+ model: input3.model,
5577
+ max_tokens: fallback.request.max_tokens ?? fallback.request.max_completion_tokens,
5110
5578
  stream: fallback.request.stream,
5579
+ metadata: fallback.request.metadata,
5111
5580
  tools: fallback.request.tools,
5112
5581
  tool_choice: fallback.request.tool_choice,
5113
5582
  ir: fallback.ir
5114
5583
  })
5115
5584
  };
5116
5585
  }
5117
- function buildUpstreamRequest(input2) {
5118
- const ir = createMessageIR(input2.request);
5119
- const { diagnostics, ...body } = buildUpstreamRequestFromIR({
5120
- model: input2.model,
5121
- interface: input2.interface,
5122
- request: input2.request,
5586
+ function buildProviderDispatchRequest(input3) {
5587
+ const ir = createMessageIR(input3.request);
5588
+ const { diagnostics, dispatchFormat, ...body } = buildProviderDispatchRequestFromIR({
5589
+ model: input3.model,
5590
+ interface: input3.interface,
5591
+ compatibilityProfile: input3.compatibilityProfile,
5592
+ request: input3.request,
5123
5593
  ir,
5124
- capabilities: input2.capabilities
5594
+ capabilities: input3.capabilities
5125
5595
  });
5126
5596
  return {
5127
5597
  ir,
5128
5598
  body,
5129
- diagnostics
5599
+ diagnostics,
5600
+ dispatchFormat
5130
5601
  };
5131
5602
  }
5132
5603
  var init_protocols = __esm({
@@ -5135,6 +5606,7 @@ var init_protocols = __esm({
5135
5606
  init_message_ir();
5136
5607
  init_anthropic();
5137
5608
  init_openai();
5609
+ init_compile();
5138
5610
  }
5139
5611
  });
5140
5612
 
@@ -5202,7 +5674,7 @@ async function run(options = {}) {
5202
5674
  const hour = pad(date.getHours());
5203
5675
  const minute = pad(date.getMinutes());
5204
5676
  const seconds = pad(date.getSeconds());
5205
- return `./logs/ctr-${month}${day}${hour}${minute}${seconds}${index ? `_${index}` : ""}.log`;
5677
+ return `ctr-${month}${day}${hour}${minute}${seconds}${index ? `_${index}` : ""}.log`;
5206
5678
  };
5207
5679
  const loggerConfig = config.LOG !== false ? {
5208
5680
  level: config.LOG_LEVEL || "debug",
@@ -5213,10 +5685,11 @@ async function run(options = {}) {
5213
5685
  compress: "gzip"
5214
5686
  })
5215
5687
  } : false;
5688
+ const registry = buildModelRegistry(config);
5216
5689
  const server = createServer({
5217
- jsonPath: CONFIG_FILE,
5690
+ useJsonFile: false,
5218
5691
  initialConfig: {
5219
- providers: config.Providers,
5692
+ providers: registry.providers,
5220
5693
  HOST,
5221
5694
  PORT: servicePort,
5222
5695
  LOG_FILE: (0, import_path5.join)(
@@ -5252,9 +5725,10 @@ async function run(options = {}) {
5252
5725
  initialModel: req.body?.model
5253
5726
  });
5254
5727
  appendTraceReason(req.governanceTrace, "request_received");
5255
- const triggerResult = await triggerRouter.route(req);
5728
+ const bypassTriggerRouter = req.headers["x-ctr-smart-router"] === "1";
5729
+ const triggerResult = bypassTriggerRouter ? { matched: false, confidence: 0, analysisTime: 0 } : await triggerRouter.route(req);
5256
5730
  req.triggerResult = triggerResult;
5257
- if (triggerResult.matched && triggerResult.model) {
5731
+ if (!bypassTriggerRouter && triggerResult.matched && triggerResult.model) {
5258
5732
  const previousSessionState = req.sessionId ? sessionStateStore.get(req.sessionId) : void 0;
5259
5733
  const previousModel = previousSessionState?.lastSuccessfulModel;
5260
5734
  const alignmentConfig = config.Governance?.sticky?.alignment;
@@ -5319,9 +5793,10 @@ async function run(options = {}) {
5319
5793
  const compiledModel = getCompiledModelRef(config, req.body?.model);
5320
5794
  if (compiledModel?.interface && req.body?.messages) {
5321
5795
  const originalBody = cloneRequestBody(req.body);
5322
- const upstream = buildUpstreamRequest({
5796
+ const upstream = buildProviderDispatchRequest({
5323
5797
  model: compiledModel.modelName,
5324
5798
  interface: compiledModel.interface,
5799
+ compatibilityProfile: compiledModel.compatibilityProfile,
5325
5800
  request: originalBody,
5326
5801
  capabilities: compiledModel.capabilities
5327
5802
  });
@@ -5508,7 +5983,7 @@ async function run(options = {}) {
5508
5983
  event.emit("onSend", req, reply, payload);
5509
5984
  return payload;
5510
5985
  });
5511
- server.start();
5986
+ await server.start();
5512
5987
  }
5513
5988
  var import_fs5, import_promises2, import_os2, import_path5, import_json5, import_node_events, import_rotating_file_stream, event;
5514
5989
  var init_index = __esm({
@@ -5542,34 +6017,43 @@ var init_index = __esm({
5542
6017
  });
5543
6018
 
5544
6019
  // src/setup/service.ts
5545
- function decideServiceAction(input2) {
5546
- if (input2.detectedService.kind === "non_self_occupied") {
6020
+ function decideServiceAction(input3) {
6021
+ if (input3.detectedService.kind === "non_self_occupied") {
5547
6022
  throw new Error("target port is occupied by another service");
5548
6023
  }
5549
- if (input2.detectedService.kind === "none") {
6024
+ if (input3.detectedService.kind === "none") {
5550
6025
  return { kind: "start" };
5551
6026
  }
5552
- if (input2.detectedService.kind === "self_unhealthy") {
6027
+ if (input3.detectedService.kind === "self_unhealthy") {
5553
6028
  return { kind: "restart" };
5554
6029
  }
5555
- if (input2.configChanged && input2.detectedService.kind === "self_healthy") {
5556
- return input2.reloadSupported ? { kind: "reload" } : { kind: "restart" };
6030
+ if (input3.configChanged && input3.detectedService.kind === "self_healthy") {
6031
+ return input3.reloadSupported ? { kind: "reload" } : { kind: "restart" };
5557
6032
  }
5558
6033
  return { kind: "reuse" };
5559
6034
  }
5560
- async function applyServiceAction(input2) {
5561
- if (input2.action.kind === "start") {
5562
- await input2.executeStart();
6035
+ async function applyServiceAction(input3) {
6036
+ if (input3.action.kind === "start") {
6037
+ await input3.executeStart();
5563
6038
  }
5564
- if (input2.action.kind === "reload") {
5565
- await input2.executeReload();
6039
+ if (input3.action.kind === "reload") {
6040
+ await input3.executeReload();
5566
6041
  }
5567
- if (input2.action.kind === "restart") {
5568
- await input2.executeRestart();
6042
+ if (input3.action.kind === "restart") {
6043
+ await input3.executeRestart();
5569
6044
  }
5570
- const healthy = await input2.verifyHealth();
6045
+ const healthy = await input3.verifyHealth();
5571
6046
  if (!healthy) {
5572
- throw new Error("service health check failed");
6047
+ if (input3.action.kind === "restart") {
6048
+ throw new Error("service health check failed after restart; the previous ctr service may still be shutting down. Please wait a moment and retry, or run `ctr stop` first.");
6049
+ }
6050
+ if (input3.action.kind === "start") {
6051
+ throw new Error("service health check failed after start; please check whether the target port is already occupied or the configuration is still invalid.");
6052
+ }
6053
+ if (input3.action.kind === "reload") {
6054
+ throw new Error("service health check failed after reload; please retry or run `ctr restart` / `ctr stop` first.");
6055
+ }
6056
+ throw new Error("service health check failed while reusing the current service; please run `ctr status` or `ctr restart` to verify it.");
5573
6057
  }
5574
6058
  }
5575
6059
  var init_service = __esm({
@@ -5617,9 +6101,12 @@ function inferProtocolFromApiBaseUrl(apiBaseUrl) {
5617
6101
  }
5618
6102
  return "openai";
5619
6103
  }
6104
+ function normalizeSegment(value) {
6105
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
6106
+ }
5620
6107
  function toModelId(name, model, index) {
5621
- const normalizedName = name.trim() || `provider_${index + 1}`;
5622
- const normalizedModel = model.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
6108
+ const normalizedName = normalizeSegment(name) || `provider_${index + 1}`;
6109
+ const normalizedModel = normalizeSegment(model);
5623
6110
  return normalizedModel ? `${normalizedName}_${normalizedModel}` : normalizedName;
5624
6111
  }
5625
6112
  function createEmptyDraft() {
@@ -5634,23 +6121,124 @@ function createNonMigratableResult() {
5634
6121
  draft: createEmptyDraft(),
5635
6122
  skippedFields: [],
5636
6123
  needsCompletion: true,
5637
- missingFields: ["defaultModel", "apiKey"]
6124
+ missingFields: ["defaultModel", "apiKey", "apiBaseUrl"]
5638
6125
  };
5639
6126
  }
5640
6127
  function isLegacyProviderInput(value) {
5641
6128
  return typeof value === "object" && value !== null;
5642
6129
  }
5643
- function migrateLegacyConfig(input2) {
5644
- if (!Array.isArray(input2.providers)) {
5645
- return createNonMigratableResult();
6130
+ function isLegacyRouterInput(value) {
6131
+ return typeof value === "object" && value !== null;
6132
+ }
6133
+ function pushUnique(target, value) {
6134
+ if (!target.includes(value)) {
6135
+ target.push(value);
5646
6136
  }
5647
- if (!input2.providers.every(isLegacyProviderInput)) {
5648
- return createNonMigratableResult();
6137
+ }
6138
+ function readString(value) {
6139
+ return typeof value === "string" ? value.trim() : void 0;
6140
+ }
6141
+ function readBoolean(value) {
6142
+ if (typeof value === "boolean") {
6143
+ return value;
6144
+ }
6145
+ if (typeof value === "string") {
6146
+ if (value.toLowerCase() === "true") {
6147
+ return true;
6148
+ }
6149
+ if (value.toLowerCase() === "false") {
6150
+ return false;
6151
+ }
6152
+ }
6153
+ return void 0;
6154
+ }
6155
+ function readFiniteNumber(value) {
6156
+ if (typeof value === "number" && Number.isFinite(value)) {
6157
+ return value;
6158
+ }
6159
+ if (typeof value === "string" && value.trim().length > 0) {
6160
+ const parsed = Number(value);
6161
+ if (Number.isFinite(parsed)) {
6162
+ return parsed;
6163
+ }
6164
+ }
6165
+ return void 0;
6166
+ }
6167
+ function extractSupportedTopLevelConfig(input3, consumedTopLevelFields) {
6168
+ const nextConfig = {};
6169
+ const hasOwn = (key) => Object.prototype.hasOwnProperty.call(input3, key);
6170
+ const host = readString(input3.HOST);
6171
+ if (hasOwn("HOST")) {
6172
+ nextConfig.HOST = host;
6173
+ consumedTopLevelFields.add("HOST");
6174
+ }
6175
+ const port = readFiniteNumber(input3.PORT);
6176
+ if (hasOwn("PORT") && port !== void 0) {
6177
+ nextConfig.PORT = port;
6178
+ consumedTopLevelFields.add("PORT");
6179
+ }
6180
+ const log2 = readBoolean(input3.LOG);
6181
+ if (hasOwn("LOG") && log2 !== void 0) {
6182
+ nextConfig.LOG = log2;
6183
+ consumedTopLevelFields.add("LOG");
6184
+ }
6185
+ const logLevel = readString(input3.LOG_LEVEL);
6186
+ if (hasOwn("LOG_LEVEL")) {
6187
+ nextConfig.LOG_LEVEL = logLevel;
6188
+ consumedTopLevelFields.add("LOG_LEVEL");
6189
+ }
6190
+ const apiTimeoutMs = readFiniteNumber(input3.API_TIMEOUT_MS);
6191
+ if (hasOwn("API_TIMEOUT_MS") && apiTimeoutMs !== void 0) {
6192
+ nextConfig.API_TIMEOUT_MS = apiTimeoutMs;
6193
+ consumedTopLevelFields.add("API_TIMEOUT_MS");
6194
+ }
6195
+ const proxyUrl = readString(input3.PROXY_URL);
6196
+ if (hasOwn("PROXY_URL")) {
6197
+ nextConfig.PROXY_URL = proxyUrl;
6198
+ consumedTopLevelFields.add("PROXY_URL");
6199
+ }
6200
+ const apiKey = readString(input3.APIKEY);
6201
+ if (hasOwn("APIKEY")) {
6202
+ nextConfig.APIKEY = apiKey;
6203
+ consumedTopLevelFields.add("APIKEY");
6204
+ }
6205
+ const customRouterPath = readString(input3.CUSTOM_ROUTER_PATH);
6206
+ if (hasOwn("CUSTOM_ROUTER_PATH")) {
6207
+ nextConfig.CUSTOM_ROUTER_PATH = customRouterPath;
6208
+ consumedTopLevelFields.add("CUSTOM_ROUTER_PATH");
6209
+ }
6210
+ return nextConfig;
6211
+ }
6212
+ function normalizeLegacyConfig(input3) {
6213
+ const lowerProviders = Array.isArray(input3.providers) && input3.providers.every(isLegacyProviderInput) ? input3.providers : null;
6214
+ const upperProviders = Array.isArray(input3.Providers) && input3.Providers.every(isLegacyProviderInput) ? input3.Providers : null;
6215
+ const providerKey = lowerProviders && lowerProviders.length > 0 ? "providers" : upperProviders && upperProviders.length > 0 ? "Providers" : lowerProviders ? "providers" : upperProviders ? "Providers" : null;
6216
+ if (!providerKey) {
6217
+ return null;
6218
+ }
6219
+ const rawProviders = providerKey === "providers" ? lowerProviders : upperProviders;
6220
+ if (!rawProviders) {
6221
+ return null;
5649
6222
  }
5650
6223
  const skippedFields = [];
5651
- const providers = input2.providers.map((provider, index) => {
6224
+ const alternateProviderKey = providerKey === "providers" ? "Providers" : "providers";
6225
+ const alternateDefaultKey = providerKey === "providers" ? "Router" : "default";
6226
+ const alternateDefaultValue = providerKey === "providers" ? input3.Router : input3.default;
6227
+ const alternateProviders = providerKey === "providers" ? upperProviders : lowerProviders;
6228
+ if (alternateProviders !== null) {
6229
+ pushUnique(skippedFields, alternateProviderKey);
6230
+ }
6231
+ if (alternateDefaultValue !== void 0) {
6232
+ pushUnique(skippedFields, alternateDefaultKey);
6233
+ }
6234
+ const consumedTopLevelFields = /* @__PURE__ */ new Set([providerKey]);
6235
+ const supportedTopLevelConfig = extractSupportedTopLevelConfig(input3, consumedTopLevelFields);
6236
+ const providers = rawProviders.map((provider, index) => {
5652
6237
  if (provider.transformer !== void 0) {
5653
- skippedFields.push(`providers[${index}].transformer`);
6238
+ pushUnique(skippedFields, `${providerKey}[${index}].transformer`);
6239
+ }
6240
+ if (provider.headers !== void 0) {
6241
+ pushUnique(skippedFields, `${providerKey}[${index}].headers`);
5654
6242
  }
5655
6243
  return {
5656
6244
  name: provider.name ?? "",
@@ -5659,41 +6247,119 @@ function migrateLegacyConfig(input2) {
5659
6247
  models: Array.isArray(provider.models) ? provider.models : []
5660
6248
  };
5661
6249
  });
5662
- const models = providers.flatMap(
6250
+ let defaultRoute;
6251
+ const routeSlots = {};
6252
+ if (providerKey === "providers") {
6253
+ consumedTopLevelFields.add("default");
6254
+ defaultRoute = typeof input3.default === "string" ? input3.default : void 0;
6255
+ } else {
6256
+ consumedTopLevelFields.add("Router");
6257
+ if (isLegacyRouterInput(input3.Router)) {
6258
+ defaultRoute = typeof input3.Router.default === "string" ? input3.Router.default : void 0;
6259
+ routeSlots.background = readString(input3.Router.background);
6260
+ routeSlots.think = readString(input3.Router.think);
6261
+ routeSlots.longContext = readString(input3.Router.longContext);
6262
+ routeSlots.webSearch = readString(input3.Router.webSearch);
6263
+ routeSlots.longContextThreshold = readFiniteNumber(input3.Router.longContextThreshold);
6264
+ }
6265
+ }
6266
+ for (const key of Object.keys(input3)) {
6267
+ if (consumedTopLevelFields.has(key)) {
6268
+ continue;
6269
+ }
6270
+ pushUnique(skippedFields, key);
6271
+ }
6272
+ return {
6273
+ providers,
6274
+ defaultRoute,
6275
+ routeSlots,
6276
+ supportedTopLevelConfig,
6277
+ skippedFields
6278
+ };
6279
+ }
6280
+ function migrateLegacyConfig(input3) {
6281
+ const normalized = normalizeLegacyConfig(input3);
6282
+ if (!normalized) {
6283
+ return createNonMigratableResult();
6284
+ }
6285
+ const rawEntries = normalized.providers.flatMap(
5663
6286
  (provider, providerIndex) => (provider.models.length ? provider.models : [""]).map((model) => ({
5664
- id: toModelId(provider.name, model, providerIndex),
6287
+ candidateId: toModelId(provider.name, model, providerIndex),
5665
6288
  api: provider.api_base_url,
5666
6289
  api_base_url: provider.api_base_url,
5667
6290
  key: provider.api_key,
5668
6291
  api_key: provider.api_key,
5669
6292
  interface: inferProtocolFromApiBaseUrl(provider.api_base_url),
5670
6293
  protocol: inferProtocolFromApiBaseUrl(provider.api_base_url),
5671
- model
5672
- }))
5673
- ).filter((item) => item.model);
5674
- if (input2.trigger_router !== void 0) {
5675
- skippedFields.push("trigger_router");
5676
- }
5677
- const hasDefaultModel = typeof input2.default === "string" && input2.default.length > 0;
5678
- const defaultModelId = hasDefaultModel ? (() => {
5679
- const [providerName, modelName] = String(input2.default).split(",");
5680
- return models.find((item) => item.id === toModelId(providerName, modelName, 0) || item.id.startsWith(`${providerName}_`) && item.model === modelName)?.id;
5681
- })() : void 0;
5682
- const hasMissingApiKey = providers.some((provider) => provider.api_key.length === 0);
6294
+ model,
6295
+ providerName: provider.name
6296
+ })).filter((item) => item.model)
6297
+ );
6298
+ const seenIds = /* @__PURE__ */ new Map();
6299
+ const routeLookup = /* @__PURE__ */ new Map();
6300
+ const models = rawEntries.map((entry) => {
6301
+ const count = seenIds.get(entry.candidateId) ?? 0;
6302
+ seenIds.set(entry.candidateId, count + 1);
6303
+ const finalId = count === 0 ? entry.candidateId : `${entry.candidateId}_${count + 1}`;
6304
+ routeLookup.set(`${entry.providerName.trim()},${entry.model}`, finalId);
6305
+ return {
6306
+ id: finalId,
6307
+ api: entry.api,
6308
+ api_base_url: entry.api_base_url,
6309
+ key: entry.key,
6310
+ api_key: entry.api_key,
6311
+ interface: entry.interface,
6312
+ protocol: entry.protocol,
6313
+ model: entry.model
6314
+ };
6315
+ });
6316
+ const resolveLegacyRoute = (ref, fieldName) => {
6317
+ if (!ref) {
6318
+ return void 0;
6319
+ }
6320
+ const [rawProviderName, rawModelName] = String(ref).split(",");
6321
+ const providerName = (rawProviderName ?? "").trim();
6322
+ const modelName = (rawModelName ?? "").trim();
6323
+ const fromLookup = routeLookup.get(`${providerName},${modelName}`);
6324
+ if (fromLookup) {
6325
+ return fromLookup;
6326
+ }
6327
+ pushUnique(normalized.skippedFields, fieldName);
6328
+ return void 0;
6329
+ };
6330
+ const hasLegacyDefaultRoute = typeof normalized.defaultRoute === "string" && normalized.defaultRoute.length > 0;
6331
+ const defaultModelId = hasLegacyDefaultRoute ? resolveLegacyRoute(normalized.defaultRoute, "Router.default") : void 0;
6332
+ const backgroundModelId = resolveLegacyRoute(normalized.routeSlots.background, "Router.background");
6333
+ const thinkModelId = resolveLegacyRoute(normalized.routeSlots.think, "Router.think");
6334
+ const longContextModelId = resolveLegacyRoute(normalized.routeSlots.longContext, "Router.longContext");
6335
+ const webSearchModelId = resolveLegacyRoute(normalized.routeSlots.webSearch, "Router.webSearch");
6336
+ const hasMissingApiKey = normalized.providers.some((provider) => provider.api_key.length === 0);
6337
+ const hasMissingApiBaseUrl = normalized.providers.some((provider) => (provider.api_base_url?.trim() ?? "").length === 0);
5683
6338
  const missingFields = [];
5684
- if (!hasDefaultModel) {
6339
+ if (!defaultModelId) {
5685
6340
  missingFields.push("defaultModel");
5686
6341
  }
5687
6342
  if (hasMissingApiKey) {
5688
6343
  missingFields.push("apiKey");
5689
6344
  }
6345
+ if (hasMissingApiBaseUrl) {
6346
+ missingFields.push("apiBaseUrl");
6347
+ }
5690
6348
  return {
5691
6349
  draft: {
6350
+ ...normalized.supportedTopLevelConfig,
5692
6351
  Providers: [],
5693
6352
  Models: models,
5694
- Router: hasDefaultModel && defaultModelId ? { default: defaultModelId } : {}
6353
+ Router: {
6354
+ ...defaultModelId ? { default: defaultModelId } : {},
6355
+ ...backgroundModelId ? { background: backgroundModelId } : {},
6356
+ ...thinkModelId ? { think: thinkModelId } : {},
6357
+ ...longContextModelId ? { longContext: longContextModelId } : {},
6358
+ ...normalized.routeSlots.longContextThreshold !== void 0 ? { longContextThreshold: normalized.routeSlots.longContextThreshold } : {},
6359
+ ...webSearchModelId ? { webSearch: webSearchModelId } : {}
6360
+ }
5695
6361
  },
5696
- skippedFields,
6362
+ skippedFields: normalized.skippedFields,
5697
6363
  needsCompletion: missingFields.length > 0,
5698
6364
  missingFields
5699
6365
  };
@@ -5758,11 +6424,12 @@ function getProviderPreset2(key) {
5758
6424
  api: preset.api,
5759
6425
  api_base_url: preset.api_base_url,
5760
6426
  interface: preset.interface,
5761
- protocol: preset.protocol
6427
+ protocol: preset.protocol,
6428
+ default_thinking: preset.default_thinking
5762
6429
  };
5763
6430
  }
5764
- function buildMinimalConfig(input2) {
5765
- const providers = input2.providers;
6431
+ function buildMinimalConfig(input3) {
6432
+ const providers = input3.providers;
5766
6433
  const models = providers.map((p) => {
5767
6434
  const preset = p.preset ? getProviderPreset2(p.preset) : void 0;
5768
6435
  const modelDraft = {
@@ -5782,15 +6449,15 @@ function buildMinimalConfig(input2) {
5782
6449
  }
5783
6450
  return modelDraft;
5784
6451
  });
5785
- let defaultModel = input2.defaultModel?.trim();
5786
- if (input2.defaultModel && input2.defaultModel.includes(",")) {
5787
- const [providerName, modelName] = input2.defaultModel.split(",");
6452
+ let defaultModel = input3.defaultModel?.trim();
6453
+ if (input3.defaultModel && input3.defaultModel.includes(",")) {
6454
+ const [providerName, modelName] = input3.defaultModel.split(",");
5788
6455
  const matched = models.find((item) => item.id === providerName && item.model === modelName);
5789
6456
  if (matched) {
5790
6457
  defaultModel = matched.id;
5791
6458
  }
5792
6459
  }
5793
- if (input2.defaultModel === void 0 && models.length > 0) {
6460
+ if (input3.defaultModel === void 0 && models.length > 0) {
5794
6461
  const firstModelId = models[0].id;
5795
6462
  if (firstModelId && models[0].model) {
5796
6463
  defaultModel = firstModelId;
@@ -5804,31 +6471,68 @@ function buildMinimalConfig(input2) {
5804
6471
  Router: defaultModel ? { default: defaultModel } : {}
5805
6472
  };
5806
6473
  }
6474
+ function buildUsableMinimalTemplateConfig() {
6475
+ const openRouterPreset = getProviderPreset("openrouter");
6476
+ const modelId = openRouterPreset?.suggested_id ?? "sonnet";
6477
+ const modelName = openRouterPreset?.default_model ?? "anthropic/claude-sonnet-4";
6478
+ const thinking = openRouterPreset?.default_thinking ?? "auto";
6479
+ const draft = buildMinimalConfig({
6480
+ providers: [
6481
+ {
6482
+ name: "openrouter",
6483
+ model_id: modelId,
6484
+ preset: "openrouter",
6485
+ api_key: "sk-xxx",
6486
+ models: [modelName]
6487
+ }
6488
+ ],
6489
+ defaultModel: modelId
6490
+ });
6491
+ const primaryModel = draft.Models?.[0];
6492
+ return {
6493
+ HOST: DEFAULT_CONFIG2.HOST,
6494
+ PORT: DEFAULT_CONFIG2.PORT,
6495
+ LOG: DEFAULT_CONFIG2.LOG,
6496
+ LOG_LEVEL: DEFAULT_CONFIG2.LOG_LEVEL,
6497
+ Models: primaryModel ? [
6498
+ {
6499
+ id: primaryModel.id,
6500
+ api: primaryModel.api,
6501
+ key: primaryModel.key,
6502
+ interface: primaryModel.interface,
6503
+ model: primaryModel.model,
6504
+ thinking
6505
+ }
6506
+ ] : [],
6507
+ Router: draft.Router
6508
+ };
6509
+ }
5807
6510
  var init_templates = __esm({
5808
6511
  "src/setup/templates.ts"() {
5809
6512
  "use strict";
6513
+ init_constants();
5810
6514
  init_provider_presets();
5811
6515
  }
5812
6516
  });
5813
6517
 
5814
6518
  // src/setup/persist.ts
5815
- async function persistSetupConfig(input2) {
5816
- const errors = input2.validateConfig(input2.config);
6519
+ async function persistSetupConfig(input3) {
6520
+ const errors = input3.validateConfig(input3.config);
5817
6521
  if (errors.length > 0) {
5818
6522
  throw new Error("config validation failed");
5819
6523
  }
5820
6524
  let backupPath;
5821
- if (input2.hasExistingConfig) {
5822
- const createdBackupPath = await input2.backupCurrentConfig();
6525
+ if (input3.hasExistingConfig) {
6526
+ const createdBackupPath = await input3.backupCurrentConfig();
5823
6527
  if (!createdBackupPath) {
5824
6528
  throw new Error("failed to back up existing config");
5825
6529
  }
5826
6530
  backupPath = createdBackupPath;
5827
6531
  }
5828
- await input2.writeConfig(input2.config);
6532
+ await input3.writeConfig(input3.config);
5829
6533
  return {
5830
6534
  configChanged: true,
5831
- configPath: input2.currentConfigPath,
6535
+ configPath: input3.currentConfigPath,
5832
6536
  backupPath
5833
6537
  };
5834
6538
  }
@@ -5877,8 +6581,8 @@ function ensureLegacyFlow(detection, legacyConfigAction) {
5877
6581
  function invalidAction() {
5878
6582
  return invalidCurrentAction();
5879
6583
  }
5880
- function decideSetupBranch(input2) {
5881
- const { detection, currentConfigAction, legacyConfigAction } = input2;
6584
+ function decideSetupBranch(input3) {
6585
+ const { detection, currentConfigAction, legacyConfigAction } = input3;
5882
6586
  if (currentConfigAction === "cancel") {
5883
6587
  ensureNoLegacyAction(legacyConfigAction);
5884
6588
  return { kind: "cancelled" };
@@ -5931,14 +6635,36 @@ function getTargetConfigPath(detection) {
5931
6635
  }
5932
6636
  return CONFIG_FILE;
5933
6637
  }
5934
- async function runSetup(deps) {
5935
- const detection = await deps.detectSetupEnvironment();
6638
+ function getLegacyProviderCount(input3) {
6639
+ if (typeof input3 !== "object" || input3 === null) {
6640
+ return 0;
6641
+ }
6642
+ const legacyConfig = input3;
6643
+ if (Array.isArray(legacyConfig.providers)) {
6644
+ return legacyConfig.providers.length;
6645
+ }
6646
+ if (Array.isArray(legacyConfig.Providers)) {
6647
+ return legacyConfig.Providers.length;
6648
+ }
6649
+ return 0;
6650
+ }
6651
+ function getMigratedModelCount(draft) {
6652
+ if (Array.isArray(draft.Models)) {
6653
+ return draft.Models.length;
6654
+ }
6655
+ if (Array.isArray(draft.Providers)) {
6656
+ return draft.Providers.reduce((total, provider) => total + (provider.models?.length ?? 0), 0);
6657
+ }
6658
+ return 0;
6659
+ }
6660
+ async function runSetup(deps) {
6661
+ const detection = await deps.detectSetupEnvironment();
5936
6662
  const currentConfigAction = await deps.chooseCurrentConfigAction({
5937
6663
  currentConfig: detection.currentConfig,
5938
6664
  legacyConfig: detection.legacyConfig
5939
6665
  });
5940
6666
  let legacyConfigAction;
5941
- if (currentConfigAction === "create" || currentConfigAction === "overwrite") {
6667
+ if (currentConfigAction === "create" || currentConfigAction === "overwrite" || currentConfigAction === "fresh") {
5942
6668
  if (detection.legacyConfig.kind === "found" || detection.legacyConfig.kind === "read_error") {
5943
6669
  legacyConfigAction = await deps.chooseLegacyConfigAction({
5944
6670
  legacyConfig: detection.legacyConfig
@@ -6012,6 +6738,16 @@ async function runSetup(deps) {
6012
6738
  throw new Error("migrate_legacy requires legacy config");
6013
6739
  }
6014
6740
  const migrated = deps.migrateLegacyConfig(detection.legacyConfig.config);
6741
+ deps.io.info(`\u5DF2\u8BC6\u522B\u65E7\u914D\u7F6E\u4E2D\u7684 ${getLegacyProviderCount(detection.legacyConfig.config)} \u4E2A provider\u3002`);
6742
+ deps.io.info(`\u5DF2\u4ECE\u65E7\u914D\u7F6E\u8FC1\u79FB ${getMigratedModelCount(migrated.draft)} \u4E2A\u6A21\u578B\u3002`);
6743
+ if (migrated.draft.Router.default) {
6744
+ deps.io.info(`\u8FC1\u79FB\u540E\u7684\u9ED8\u8BA4\u6A21\u578B\uFF1A${migrated.draft.Router.default}`);
6745
+ } else {
6746
+ deps.io.info("\u8FC1\u79FB\u540E\u7684\u9ED8\u8BA4\u6A21\u578B\u4ECD\u9700\u8865\u5168\u3002");
6747
+ }
6748
+ if (migrated.skippedFields.length > 0) {
6749
+ deps.io.info(`\u4EE5\u4E0B\u65E7\u5B57\u6BB5\u672A\u81EA\u52A8\u8FC1\u79FB\uFF1A${migrated.skippedFields.join(", ")}`);
6750
+ }
6015
6751
  let finalDraft = migrated.draft;
6016
6752
  if (migrated.needsCompletion) {
6017
6753
  finalDraft = await deps.completeDraft({
@@ -6046,6 +6782,96 @@ var init_setup = __esm({
6046
6782
 
6047
6783
  // src/setup/index.ts
6048
6784
  function createConsoleIO() {
6785
+ if (process.env.CTR_SETUP_FORCE_SCRIPTED_INPUT === "1") {
6786
+ const scriptedInput = (0, import_fs6.readFileSync)(0, "utf-8");
6787
+ const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter((item) => item.length > 0);
6788
+ let cursor = 0;
6789
+ const nextAnswer = async () => answers[cursor++] ?? "";
6790
+ return {
6791
+ async choose(message, options) {
6792
+ import_process.stdout.write(`${message}
6793
+ `);
6794
+ options.forEach((option, index) => {
6795
+ import_process.stdout.write(` ${index + 1}. ${option}
6796
+ `);
6797
+ });
6798
+ const answer = await nextAnswer();
6799
+ const pickedIndex = Number(answer);
6800
+ if (Number.isInteger(pickedIndex) && pickedIndex >= 1 && pickedIndex <= options.length) {
6801
+ return options[pickedIndex - 1];
6802
+ }
6803
+ const matched = options.find((option) => option === answer);
6804
+ if (matched) {
6805
+ return matched;
6806
+ }
6807
+ throw new Error(`invalid scripted answer for "${message}": ${answer || "<empty>"}`);
6808
+ },
6809
+ async input(message, defaultValue) {
6810
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
6811
+ import_process.stdout.write(`${message}${suffix}: `);
6812
+ const answer = await nextAnswer();
6813
+ return answer || defaultValue || "";
6814
+ },
6815
+ info(message) {
6816
+ import_process.stdout.write(`${message}
6817
+ `);
6818
+ },
6819
+ close() {
6820
+ }
6821
+ };
6822
+ }
6823
+ if (!import_process.stdin.isTTY) {
6824
+ let loaded = false;
6825
+ let answers = [];
6826
+ let cursor = 0;
6827
+ const loadAnswers = async () => {
6828
+ if (loaded) {
6829
+ return;
6830
+ }
6831
+ loaded = true;
6832
+ const chunks = [];
6833
+ for await (const chunk of import_process.stdin) {
6834
+ chunks.push(String(chunk));
6835
+ }
6836
+ answers = chunks.join("").split(/\r?\n/).map((item) => item.trim()).filter((item) => item.length > 0);
6837
+ };
6838
+ const nextAnswer = async () => {
6839
+ await loadAnswers();
6840
+ return answers[cursor++] ?? "";
6841
+ };
6842
+ return {
6843
+ async choose(message, options) {
6844
+ import_process.stdout.write(`${message}
6845
+ `);
6846
+ options.forEach((option, index) => {
6847
+ import_process.stdout.write(` ${index + 1}. ${option}
6848
+ `);
6849
+ });
6850
+ const answer = await nextAnswer();
6851
+ const pickedIndex = Number(answer);
6852
+ if (Number.isInteger(pickedIndex) && pickedIndex >= 1 && pickedIndex <= options.length) {
6853
+ return options[pickedIndex - 1];
6854
+ }
6855
+ const matched = options.find((option) => option === answer);
6856
+ if (matched) {
6857
+ return matched;
6858
+ }
6859
+ throw new Error(`invalid scripted answer for "${message}": ${answer || "<empty>"}`);
6860
+ },
6861
+ async input(message, defaultValue) {
6862
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
6863
+ import_process.stdout.write(`${message}${suffix}: `);
6864
+ const answer = await nextAnswer();
6865
+ return answer || defaultValue || "";
6866
+ },
6867
+ info(message) {
6868
+ import_process.stdout.write(`${message}
6869
+ `);
6870
+ },
6871
+ close() {
6872
+ }
6873
+ };
6874
+ }
6049
6875
  const rl = (0, import_promises3.createInterface)({ input: import_process.stdin, output: import_process.stdout });
6050
6876
  const ask = async (message) => {
6051
6877
  const answer = await rl.question(message);
@@ -6080,6 +6906,9 @@ function createConsoleIO() {
6080
6906
  info(message) {
6081
6907
  import_process.stdout.write(`${message}
6082
6908
  `);
6909
+ },
6910
+ close() {
6911
+ rl.close();
6083
6912
  }
6084
6913
  };
6085
6914
  }
@@ -6090,14 +6919,79 @@ function readStructuredConfigFile(filePath) {
6090
6919
  }
6091
6920
  return import_js_yaml.default.load(content);
6092
6921
  }
6922
+ function getCurrentRuntimeFields() {
6923
+ const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
6924
+ const currentPath = candidates.find((filePath) => (0, import_fs6.existsSync)(filePath));
6925
+ if (!currentPath) {
6926
+ return {};
6927
+ }
6928
+ try {
6929
+ const config = readStructuredConfigFile(currentPath);
6930
+ if (!config || typeof config !== "object") {
6931
+ return {};
6932
+ }
6933
+ const fields = {};
6934
+ for (const key of ["HOST", "PORT", "LOG", "LOG_LEVEL", "API_TIMEOUT_MS"]) {
6935
+ if (config[key] !== void 0) {
6936
+ fields[key] = config[key];
6937
+ }
6938
+ }
6939
+ return fields;
6940
+ } catch {
6941
+ return {};
6942
+ }
6943
+ }
6944
+ function getConfiguredPortFromCurrentFiles() {
6945
+ const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
6946
+ const currentPath = candidates.find((filePath) => (0, import_fs6.existsSync)(filePath));
6947
+ if (!currentPath) {
6948
+ return DEFAULT_CONFIG2.PORT;
6949
+ }
6950
+ try {
6951
+ const config = readStructuredConfigFile(currentPath);
6952
+ if (config && typeof config.PORT === "number" && Number.isFinite(config.PORT) && config.PORT > 0) {
6953
+ return config.PORT;
6954
+ }
6955
+ } catch {
6956
+ }
6957
+ return DEFAULT_CONFIG2.PORT;
6958
+ }
6959
+ async function getAvailablePort() {
6960
+ const server = (0, import_net2.createServer)();
6961
+ try {
6962
+ return await new Promise((resolve, reject) => {
6963
+ server.once("error", reject);
6964
+ server.listen(0, "127.0.0.1", () => {
6965
+ const address = server.address();
6966
+ if (!address || typeof address === "string") {
6967
+ reject(new Error("failed to resolve available port"));
6968
+ return;
6969
+ }
6970
+ resolve(address.port);
6971
+ });
6972
+ });
6973
+ } finally {
6974
+ if (server.listening) {
6975
+ await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve()));
6976
+ }
6977
+ }
6978
+ }
6979
+ function readLegacyConfigFile(filePath) {
6980
+ const content = (0, import_fs6.readFileSync)(filePath, "utf-8");
6981
+ if (filePath.endsWith(".json")) {
6982
+ return import_json52.default.parse(content);
6983
+ }
6984
+ return import_js_yaml.default.load(content);
6985
+ }
6093
6986
  async function readLegacyConfig(deps = {}) {
6094
6987
  const baseHomeDir = deps.homeDir || (0, import_os3.homedir)();
6095
6988
  const exists = deps.exists || import_fs6.existsSync;
6096
- const readConfig = deps.readConfig || readStructuredConfigFile;
6989
+ const readConfig = deps.readConfig || readLegacyConfigFile;
6097
6990
  const overridePath = process.env.CTR_SETUP_LEGACY_CONFIG_PATH;
6098
6991
  const candidatePaths = overridePath ? [overridePath] : [
6099
6992
  (0, import_path6.join)(baseHomeDir, ".ccr", "config.yaml"),
6100
- (0, import_path6.join)(baseHomeDir, ".claude-code-router", "config.yaml")
6993
+ (0, import_path6.join)(baseHomeDir, ".claude-code-router", "config.yaml"),
6994
+ (0, import_path6.join)(baseHomeDir, ".claude-code-router", "config.json")
6101
6995
  ];
6102
6996
  const legacyPath = candidatePaths.find((filePath) => exists(filePath));
6103
6997
  if (!legacyPath) {
@@ -6140,16 +7034,30 @@ async function readCurrentConfig() {
6140
7034
  }
6141
7035
  }
6142
7036
  async function probeService() {
6143
- const healthy = await waitForService(DEFAULT_CONFIG2.PORT, 500);
6144
- return healthy ? { kind: "self_healthy", port: DEFAULT_CONFIG2.PORT } : { kind: "none" };
7037
+ const port = getConfiguredPortFromCurrentFiles();
7038
+ const healthy = await waitForService(port, 500);
7039
+ if (healthy) {
7040
+ return isServiceRunning() ? { kind: "self_healthy", port } : { kind: "non_self_occupied", port };
7041
+ }
7042
+ const occupied = await isTcpPortOccupied(port, 500);
7043
+ if (!occupied) {
7044
+ return { kind: "none" };
7045
+ }
7046
+ return isServiceRunning() ? { kind: "self_unhealthy", port } : { kind: "non_self_occupied", port };
6145
7047
  }
6146
7048
  async function enterClaudeCode() {
7049
+ if (process.env.CTR_SETUP_SKIP_ENTER_CODE === "1") {
7050
+ return;
7051
+ }
6147
7052
  const cliModule = await Promise.resolve().then(() => (init_cli(), cli_exports));
6148
7053
  await cliModule.runClaudeCode();
6149
7054
  }
7055
+ function shouldAutoEnterClaudeCodeAfterSetup() {
7056
+ return process.env.CTR_SETUP_AUTO_ENTER_CODE === "1";
7057
+ }
6150
7058
  async function executeStart() {
6151
7059
  const childProcess = await import("child_process");
6152
- childProcess.spawn(process.execPath, [process.argv[1], "start", "--daemon"], {
7060
+ childProcess.spawn(process.execPath, [process.argv[1], "start"], {
6153
7061
  detached: true,
6154
7062
  stdio: "ignore",
6155
7063
  env: { ...process.env, CTR_DAEMON: "1" }
@@ -6160,6 +7068,7 @@ async function executeRestart() {
6160
7068
  if (info) {
6161
7069
  try {
6162
7070
  killProcess(info.pid);
7071
+ await waitForProcessExit(info.pid, 5e3);
6163
7072
  } catch {
6164
7073
  }
6165
7074
  }
@@ -6339,12 +7248,12 @@ async function buildFreshConfig(io) {
6339
7248
  }
6340
7249
  return draft;
6341
7250
  }
6342
- async function completeDraft(input2) {
6343
- const draft = toDraftFromConfig(input2.draft);
6344
- if (input2.fields.includes("defaultModel")) {
7251
+ async function completeDraft(input3) {
7252
+ const draft = toDraftFromConfig(input3.draft);
7253
+ if (input3.fields.includes("defaultModel")) {
6345
7254
  const defaultProvider = draft.Models?.[0]?.id ?? draft.Providers?.[0]?.name ?? "provider";
6346
7255
  const defaultModel = draft.Models?.[0]?.model ?? draft.Providers?.[0]?.models?.[0] ?? "";
6347
- const model = await input2.io.input("\u9ED8\u8BA4\u6A21\u578B", defaultModel);
7256
+ const model = await input3.io.input("\u9ED8\u8BA4\u6A21\u578B", defaultModel);
6348
7257
  if (draft.Models?.[0]) {
6349
7258
  draft.Models[0].model = model;
6350
7259
  draft.Router.default = defaultProvider;
@@ -6353,16 +7262,16 @@ async function completeDraft(input2) {
6353
7262
  draft.Router.default = `${defaultProvider},${model}`;
6354
7263
  }
6355
7264
  }
6356
- if (input2.fields.includes("apiKey")) {
6357
- const apiKey = await input2.io.input("API Key");
7265
+ if (input3.fields.includes("apiKey")) {
7266
+ const apiKey = await input3.io.input("API Key");
6358
7267
  if (draft.Models?.length) {
6359
7268
  draft.Models = draft.Models.map((model) => ({ ...model, key: model.key || apiKey, api_key: model.api_key || apiKey }));
6360
7269
  } else {
6361
7270
  draft.Providers = draft.Providers?.map((provider) => ({ ...provider, api_key: provider.api_key || apiKey }));
6362
7271
  }
6363
7272
  }
6364
- if (input2.fields.includes("apiBaseUrl")) {
6365
- const apiBaseUrl = await input2.io.input("API Base URL");
7273
+ if (input3.fields.includes("apiBaseUrl")) {
7274
+ const apiBaseUrl = await input3.io.input("API Base URL");
6366
7275
  if (draft.Models?.length) {
6367
7276
  draft.Models = draft.Models.map((model) => ({
6368
7277
  ...model,
@@ -6376,8 +7285,8 @@ async function completeDraft(input2) {
6376
7285
  }));
6377
7286
  }
6378
7287
  }
6379
- if (input2.fields.includes("capabilityHints") && draft.Models?.[0]) {
6380
- await promptCapabilityMetadataForDraft(draft, input2.io);
7288
+ if (input3.fields.includes("capabilityHints") && draft.Models?.[0]) {
7289
+ await promptCapabilityMetadataForDraft(draft, input3.io);
6381
7290
  }
6382
7291
  return draft;
6383
7292
  }
@@ -6391,112 +7300,150 @@ function createDefaultDeps(io = createConsoleIO()) {
6391
7300
  executeStart,
6392
7301
  executeReload: executeRestart,
6393
7302
  executeRestart,
6394
- verifyHealth: () => waitForService(DEFAULT_CONFIG2.PORT, 5e3),
7303
+ verifyHealth: () => waitForService(getConfiguredPortFromCurrentFiles(), 5e3),
6395
7304
  enterClaudeCode,
6396
7305
  io
6397
7306
  };
6398
7307
  }
7308
+ function printRoutingNextSteps(io) {
7309
+ io.info("\u4F60\u53EF\u4EE5\u6309\u9700\u7EE7\u7EED\u914D\u7F6E\u8DEF\u7531\u80FD\u529B\uFF1A");
7310
+ io.info(" - TriggerRouter\uFF1A\u9002\u5408\u9AD8\u786E\u5B9A\u6027\u4EFB\u52A1\uFF0C\u628A\u67B6\u6784\u8BBE\u8BA1\u3001\u4EE3\u7801\u5BA1\u67E5\u7B49\u8BF7\u6C42\u56FA\u5B9A\u5207\u5230\u6307\u5B9A\u6A21\u578B");
7311
+ io.info(" - SmartRouter\uFF1A\u9002\u5408\u6A21\u7CCA\u4EFB\u52A1\uFF0C\u5728\u5019\u9009\u6A21\u578B\u4E4B\u95F4\u81EA\u52A8\u9009\u62E9\u66F4\u5408\u9002\u7684\u6A21\u578B");
7312
+ io.info(" - \u914D\u7F6E\u6A21\u677F\u53C2\u8003\uFF1Aconfig/trigger.advanced.yaml");
7313
+ }
6399
7314
  async function runSetupCli(customDeps) {
6400
7315
  const defaults = createDefaultDeps(customDeps?.io);
6401
7316
  const deps = { ...defaults, ...customDeps };
6402
- await runSetup({
6403
- detectSetupEnvironment: () => detectSetupEnvironment({
6404
- readCurrentConfig: deps.readCurrentConfig,
6405
- readLegacyConfig: deps.readLegacyConfig,
6406
- probeService: deps.probeService
6407
- }),
6408
- chooseCurrentConfigAction: async ({ currentConfig }) => {
6409
- if (currentConfig.kind === "missing") {
6410
- return "create";
6411
- }
6412
- if (currentConfig.kind === "valid") {
6413
- deps.io.info("\u68C0\u6D4B\u5230\u5F53\u524D claude-trigger-router \u914D\u7F6E\u5DF2\u53EF\u7528\u3002");
6414
- if (currentConfig.warnings.length > 0) {
6415
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
6416
- }
6417
- return mapValidCurrentConfigChoice(
6418
- await deps.io.choose("\u4F60\u60F3\u76F4\u63A5\u4F7F\u7528\u5B83\uFF0C\u8FD8\u662F\u91CD\u65B0\u8C03\u6574\uFF1F", [
6419
- "\u76F4\u63A5\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
6420
- "\u68C0\u67E5\u5E76\u8C03\u6574\u5F53\u524D\u914D\u7F6E",
6421
- "\u653E\u5F03\u5F53\u524D\u914D\u7F6E\uFF0C\u91CD\u65B0\u5F00\u59CB"
6422
- ])
6423
- );
6424
- }
6425
- if (currentConfig.kind === "invalid") {
6426
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A${currentConfig.errors.join("; ")}`);
6427
- if (currentConfig.warnings.length > 0) {
6428
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
7317
+ try {
7318
+ await runSetup({
7319
+ detectSetupEnvironment: () => detectSetupEnvironment({
7320
+ readCurrentConfig: deps.readCurrentConfig,
7321
+ readLegacyConfig: deps.readLegacyConfig,
7322
+ probeService: deps.probeService
7323
+ }),
7324
+ chooseCurrentConfigAction: async ({ currentConfig }) => {
7325
+ if (currentConfig.kind === "missing") {
7326
+ return "create";
7327
+ }
7328
+ if (currentConfig.kind === "valid") {
7329
+ deps.io.info("\u68C0\u6D4B\u5230\u5F53\u524D claude-trigger-router \u914D\u7F6E\u5DF2\u53EF\u7528\u3002");
7330
+ if (currentConfig.warnings.length > 0) {
7331
+ deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
7332
+ }
7333
+ return mapValidCurrentConfigChoice(
7334
+ await deps.io.choose("\u4F60\u60F3\u76F4\u63A5\u4F7F\u7528\u5B83\uFF0C\u8FD8\u662F\u91CD\u65B0\u8C03\u6574\uFF1F", [
7335
+ "\u76F4\u63A5\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
7336
+ "\u68C0\u67E5\u5E76\u8C03\u6574\u5F53\u524D\u914D\u7F6E",
7337
+ "\u653E\u5F03\u5F53\u524D\u914D\u7F6E\uFF0C\u91CD\u65B0\u5F00\u59CB"
7338
+ ])
7339
+ );
6429
7340
  }
6430
- return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["repair", "overwrite", "cancel"]);
6431
- }
6432
- deps.io.info(`\u5F53\u524D\u914D\u7F6E\u65E0\u6CD5\u89E3\u6790\uFF1A${currentConfig.error}`);
6433
- return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["rebuild", "cancel"]);
6434
- },
6435
- chooseLegacyConfigAction: async ({ legacyConfig }) => {
6436
- if (legacyConfig.kind === "found") {
6437
- return mapLegacyConfigChoice(
6438
- await deps.io.choose("\u68C0\u6D4B\u5230\u65E7 claude-code-router \u914D\u7F6E\u3002\u662F\u5426\u8FC1\u79FB\u4E3A\u5F53\u524D\u63A8\u8350\u914D\u7F6E\uFF1F", [
6439
- "\u8FC1\u79FB\u65E7\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
6440
- "\u8DF3\u8FC7\u8FC1\u79FB\uFF0C\u624B\u52A8\u65B0\u5EFA"
6441
- ])
6442
- );
6443
- }
6444
- if (legacyConfig.kind === "read_error") {
6445
- deps.io.info(`\u65E7 ccr \u914D\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF1A${legacyConfig.error}`);
6446
- }
6447
- return "skip";
6448
- },
6449
- buildFreshConfig: () => buildFreshConfig(deps.io),
6450
- buildRepairConfig: async ({ currentConfig }) => toDraftFromConfig(currentConfig),
6451
- completeDraft: ({ draft, fields }) => completeDraft({ draft, fields, io: deps.io }),
6452
- migrateLegacyConfig,
6453
- mapConfigErrorsToRepairFields,
6454
- persistConfig: async ({ config, currentConfigPath, hasExistingConfig }) => {
6455
- const normalized = normalizeAndValidateConfig(config);
6456
- const persisted = await persistSetupConfig({
6457
- config: normalized.config,
6458
- currentConfigPath,
6459
- hasExistingConfig,
6460
- validateConfig: (inputConfig) => normalizeAndValidateConfig(inputConfig).errors,
6461
- backupCurrentConfig: deps.backupCurrentConfig,
6462
- writeConfig: deps.writeConfig
6463
- });
6464
- if (normalized.warnings.length > 0) {
6465
- deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${normalized.warnings.join("; ")}`);
6466
- }
6467
- return persisted;
6468
- },
6469
- ensureServiceReady: async ({ configChanged, detectedService, reloadSupported }) => {
6470
- const action = decideServiceAction({
6471
- configChanged,
6472
- detectedService,
6473
- reloadSupported
6474
- });
6475
- await applyServiceAction({
6476
- action,
6477
- executeStart: deps.executeStart,
6478
- executeReload: deps.executeReload,
6479
- executeRestart: deps.executeRestart,
6480
- verifyHealth: deps.verifyHealth
6481
- });
6482
- return {
6483
- action: action.kind,
6484
- healthChecked: true
6485
- };
6486
- },
6487
- enterClaudeCode: deps.enterClaudeCode,
6488
- reloadSupported: false
6489
- });
7341
+ if (currentConfig.kind === "invalid") {
7342
+ deps.io.info(`\u5F53\u524D\u914D\u7F6E\u6821\u9A8C\u5931\u8D25\uFF1A${currentConfig.errors.join("; ")}`);
7343
+ if (currentConfig.warnings.length > 0) {
7344
+ deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${currentConfig.warnings.join("; ")}`);
7345
+ }
7346
+ return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["repair", "overwrite", "cancel"]);
7347
+ }
7348
+ deps.io.info(`\u5F53\u524D\u914D\u7F6E\u65E0\u6CD5\u89E3\u6790\uFF1A${currentConfig.error}`);
7349
+ return await deps.io.choose("\u9009\u62E9\u4E0B\u4E00\u6B65", ["rebuild", "cancel"]);
7350
+ },
7351
+ chooseLegacyConfigAction: async ({ legacyConfig }) => {
7352
+ if (legacyConfig.kind === "found") {
7353
+ return mapLegacyConfigChoice(
7354
+ await deps.io.choose("\u68C0\u6D4B\u5230\u65E7 claude-code-router \u914D\u7F6E\u3002\u662F\u5426\u8FC1\u79FB\u4E3A\u5F53\u524D\u63A8\u8350\u914D\u7F6E\uFF1F", [
7355
+ "\u8FC1\u79FB\u65E7\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
7356
+ "\u8DF3\u8FC7\u8FC1\u79FB\uFF0C\u624B\u52A8\u65B0\u5EFA"
7357
+ ])
7358
+ );
7359
+ }
7360
+ if (legacyConfig.kind === "read_error") {
7361
+ deps.io.info(`\u65E7 ccr \u914D\u7F6E\u8BFB\u53D6\u5931\u8D25\uFF1A${legacyConfig.error}`);
7362
+ }
7363
+ return "skip";
7364
+ },
7365
+ buildFreshConfig: () => buildFreshConfig(deps.io),
7366
+ buildRepairConfig: async ({ currentConfig }) => toDraftFromConfig(currentConfig),
7367
+ completeDraft: ({ draft, fields }) => completeDraft({ draft, fields, io: deps.io }),
7368
+ migrateLegacyConfig,
7369
+ mapConfigErrorsToRepairFields,
7370
+ io: deps.io,
7371
+ persistConfig: async ({ config, currentConfigPath, hasExistingConfig }) => {
7372
+ let normalized = normalizeAndValidateConfig({
7373
+ ...hasExistingConfig ? getCurrentRuntimeFields() : {},
7374
+ ...config
7375
+ });
7376
+ {
7377
+ const targetPort = normalized.config.PORT ?? DEFAULT_CONFIG2.PORT;
7378
+ const occupied = await isTcpPortOccupied(targetPort, 500);
7379
+ if (occupied && !isServiceRunning()) {
7380
+ const fallbackPort = await getAvailablePort();
7381
+ deps.io.info(`\u68C0\u6D4B\u5230\u9ED8\u8BA4\u7AEF\u53E3 ${targetPort} \u5DF2\u88AB\u5360\u7528\uFF0Csetup \u5DF2\u81EA\u52A8\u6539\u7528\u53EF\u7528\u7AEF\u53E3 ${fallbackPort}\u3002`);
7382
+ normalized = normalizeAndValidateConfig({
7383
+ ...normalized.config,
7384
+ PORT: fallbackPort
7385
+ });
7386
+ }
7387
+ }
7388
+ const persisted = await persistSetupConfig({
7389
+ config: normalized.config,
7390
+ currentConfigPath,
7391
+ hasExistingConfig,
7392
+ validateConfig: (inputConfig) => normalizeAndValidateConfig(inputConfig).errors,
7393
+ backupCurrentConfig: deps.backupCurrentConfig,
7394
+ writeConfig: deps.writeConfig
7395
+ });
7396
+ if (normalized.warnings.length > 0) {
7397
+ deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${normalized.warnings.join("; ")}`);
7398
+ }
7399
+ return persisted;
7400
+ },
7401
+ ensureServiceReady: async ({ configChanged, detectedService, reloadSupported }) => {
7402
+ const effectiveDetectedService = configChanged ? await deps.probeService() : detectedService;
7403
+ const action = decideServiceAction({
7404
+ configChanged,
7405
+ detectedService: effectiveDetectedService,
7406
+ reloadSupported
7407
+ });
7408
+ await applyServiceAction({
7409
+ action,
7410
+ executeStart: deps.executeStart,
7411
+ executeReload: deps.executeReload,
7412
+ executeRestart: deps.executeRestart,
7413
+ verifyHealth: deps.verifyHealth
7414
+ });
7415
+ return {
7416
+ action: action.kind,
7417
+ healthChecked: true
7418
+ };
7419
+ },
7420
+ enterClaudeCode: async () => {
7421
+ printRoutingNextSteps(deps.io);
7422
+ if (!shouldAutoEnterClaudeCodeAfterSetup()) {
7423
+ deps.io.info("\u4E3A\u907F\u514D setup \u7ED3\u675F\u540E\u63A5\u7BA1\u5F53\u524D\u7EC8\u7AEF\uFF0C\u8BF7\u624B\u52A8\u8FD0\u884C\uFF1Actr code");
7424
+ deps.io.info("\u5982\u679C\u4F60\u660E\u786E\u9700\u8981 setup \u7ED3\u675F\u540E\u81EA\u52A8\u8FDB\u5165 Claude Code\uFF0C\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF CTR_SETUP_AUTO_ENTER_CODE=1");
7425
+ return;
7426
+ }
7427
+ deps.io.close?.();
7428
+ await deps.enterClaudeCode();
7429
+ },
7430
+ reloadSupported: false
7431
+ });
7432
+ } finally {
7433
+ deps.io.close?.();
7434
+ }
6490
7435
  }
6491
- var import_fs6, import_os3, import_path6, import_promises3, import_process, import_js_yaml;
7436
+ var import_fs6, import_net2, import_os3, import_path6, import_promises3, import_process, import_json52, import_js_yaml;
6492
7437
  var init_setup2 = __esm({
6493
7438
  "src/setup/index.ts"() {
6494
7439
  "use strict";
6495
7440
  import_fs6 = require("fs");
7441
+ import_net2 = require("net");
6496
7442
  import_os3 = require("os");
6497
7443
  import_path6 = require("path");
6498
7444
  import_promises3 = require("readline/promises");
6499
7445
  import_process = require("process");
7446
+ import_json52 = __toESM(require("json5"));
6500
7447
  import_js_yaml = __toESM(require("js-yaml"));
6501
7448
  init_constants();
6502
7449
  init_provider_presets();
@@ -6513,6 +7460,657 @@ var init_setup2 = __esm({
6513
7460
  }
6514
7461
  });
6515
7462
 
7463
+ // src/doctor/index.ts
7464
+ function collectCompatibilityPreviewDiagnostics(model) {
7465
+ const registry = buildModelRegistry({
7466
+ Providers: [],
7467
+ Models: [model],
7468
+ Router: {
7469
+ default: model.id
7470
+ }
7471
+ });
7472
+ const compiledModel = registry.modelMap[model.id];
7473
+ if (!compiledModel) {
7474
+ return [];
7475
+ }
7476
+ const preview = buildProviderDispatchRequest({
7477
+ model: compiledModel.modelName,
7478
+ interface: compiledModel.interface ?? "openai",
7479
+ compatibilityProfile: compiledModel.compatibilityProfile,
7480
+ capabilities: compiledModel.capabilities,
7481
+ request: {
7482
+ model: compiledModel.id,
7483
+ max_tokens: 32,
7484
+ messages: [
7485
+ {
7486
+ role: "user",
7487
+ content: [
7488
+ {
7489
+ type: "text",
7490
+ text: "compatibility preview"
7491
+ },
7492
+ {
7493
+ type: "image",
7494
+ source: {
7495
+ type: "base64",
7496
+ media_type: "image/png",
7497
+ data: "preview"
7498
+ }
7499
+ }
7500
+ ]
7501
+ }
7502
+ ],
7503
+ tools: [
7504
+ {
7505
+ name: "preview_tool",
7506
+ description: "Preview tool",
7507
+ input_schema: {
7508
+ type: "object",
7509
+ properties: {
7510
+ query: { type: "string" }
7511
+ }
7512
+ }
7513
+ }
7514
+ ],
7515
+ tool_choice: {
7516
+ type: "tool",
7517
+ name: "preview_tool"
7518
+ },
7519
+ thinking: {
7520
+ type: "enabled",
7521
+ effort: "medium"
7522
+ }
7523
+ }
7524
+ });
7525
+ return preview.diagnostics.map((code) => describeProtocolDiagnostic(code));
7526
+ }
7527
+ function hasArg(flag) {
7528
+ return process.argv.slice(2).includes(flag);
7529
+ }
7530
+ function createConsoleIO2() {
7531
+ if (process.env.CTR_DOCTOR_FORCE_SCRIPTED_INPUT === "1") {
7532
+ const scriptedInput = (0, import_fs7.readFileSync)(0, "utf-8");
7533
+ const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
7534
+ let cursor = 0;
7535
+ const nextAnswer = async () => answers[cursor++] ?? "";
7536
+ return {
7537
+ info(message) {
7538
+ import_process2.stdout.write(`${message}
7539
+ `);
7540
+ },
7541
+ error(message) {
7542
+ import_process2.stdout.write(`${message}
7543
+ `);
7544
+ },
7545
+ async choose(message, options) {
7546
+ import_process2.stdout.write(`${message}
7547
+ `);
7548
+ options.forEach((option, index2) => import_process2.stdout.write(` ${index2 + 1}. ${option}
7549
+ `));
7550
+ const answer = await nextAnswer();
7551
+ const index = Number(answer);
7552
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) {
7553
+ return options[index - 1];
7554
+ }
7555
+ return options.find((option) => option === answer) ?? options[0];
7556
+ },
7557
+ async input(message, defaultValue) {
7558
+ import_process2.stdout.write(`${message}${defaultValue ? ` (${defaultValue})` : ""}: `);
7559
+ const answer = await nextAnswer();
7560
+ return answer || defaultValue || "";
7561
+ },
7562
+ async confirm(message, defaultValue = true) {
7563
+ import_process2.stdout.write(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"}
7564
+ `);
7565
+ const answer = (await nextAnswer()).toLowerCase();
7566
+ if (!answer) {
7567
+ return defaultValue;
7568
+ }
7569
+ return ["y", "yes", "1", "true"].includes(answer);
7570
+ },
7571
+ close() {
7572
+ }
7573
+ };
7574
+ }
7575
+ const rl = (0, import_promises4.createInterface)({ input: import_process2.stdin, output: import_process2.stdout });
7576
+ const ask = async (message) => (await rl.question(message)).trim();
7577
+ return {
7578
+ info(message) {
7579
+ import_process2.stdout.write(`${message}
7580
+ `);
7581
+ },
7582
+ error(message) {
7583
+ import_process2.stdout.write(`${message}
7584
+ `);
7585
+ },
7586
+ async choose(message, options) {
7587
+ import_process2.stdout.write(`${message}
7588
+ `);
7589
+ options.forEach((option, index) => import_process2.stdout.write(` ${index + 1}. ${option}
7590
+ `));
7591
+ while (true) {
7592
+ const answer = await ask("> ");
7593
+ const index = Number(answer);
7594
+ if (Number.isInteger(index) && index >= 1 && index <= options.length) {
7595
+ return options[index - 1];
7596
+ }
7597
+ const matched = options.find((option) => option === answer);
7598
+ if (matched) {
7599
+ return matched;
7600
+ }
7601
+ import_process2.stdout.write("\u8BF7\u8F93\u5165\u9009\u9879\u7F16\u53F7\u3002\n");
7602
+ }
7603
+ },
7604
+ async input(message, defaultValue) {
7605
+ const answer = await ask(`${message}${defaultValue ? ` (${defaultValue})` : ""}: `);
7606
+ return answer || defaultValue || "";
7607
+ },
7608
+ async confirm(message, defaultValue = true) {
7609
+ const answer = (await ask(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"}: `)).toLowerCase();
7610
+ if (!answer) {
7611
+ return defaultValue;
7612
+ }
7613
+ return ["y", "yes"].includes(answer);
7614
+ },
7615
+ close() {
7616
+ rl.close();
7617
+ }
7618
+ };
7619
+ }
7620
+ function getConfigCandidates() {
7621
+ return [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
7622
+ }
7623
+ function inferInterfaceFromApi(api) {
7624
+ const trimmed = api?.trim();
7625
+ if (!trimmed) {
7626
+ return void 0;
7627
+ }
7628
+ return trimmed.includes("/v1/messages") ? "anthropic" : "openai";
7629
+ }
7630
+ function sanitizeModelId(value) {
7631
+ return value.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "") || "model";
7632
+ }
7633
+ function tryLoadStructuredConfig(filePath, content) {
7634
+ if (filePath.endsWith(".json")) {
7635
+ try {
7636
+ return { config: JSON.parse(content), repairedParse: false, messages: [] };
7637
+ } catch {
7638
+ return {
7639
+ config: import_json53.default.parse(content),
7640
+ repairedParse: true,
7641
+ messages: ["\u68C0\u6D4B\u5230 JSON \u914D\u7F6E\u5305\u542B\u5BBD\u677E\u8BED\u6CD5\uFF0Cdoctor \u5DF2\u6309\u6807\u51C6 JSON \u7ED3\u6784\u91CD\u65B0\u5F52\u4E00\u5316\u3002"]
7642
+ };
7643
+ }
7644
+ }
7645
+ try {
7646
+ return { config: import_js_yaml2.default.load(content), repairedParse: false, messages: [] };
7647
+ } catch (error) {
7648
+ const sanitized = content.replace(/\t/g, " ");
7649
+ if (sanitized !== content) {
7650
+ return {
7651
+ config: import_js_yaml2.default.load(sanitized),
7652
+ repairedParse: true,
7653
+ messages: ["\u68C0\u6D4B\u5230 YAML \u4F7F\u7528\u4E86 Tab \u7F29\u8FDB\uFF0Cdoctor \u5DF2\u81EA\u52A8\u4FEE\u590D\u4E3A\u7A7A\u683C\u7F29\u8FDB\u3002"]
7654
+ };
7655
+ }
7656
+ throw error;
7657
+ }
7658
+ }
7659
+ function loadCurrentConfig() {
7660
+ const existingPath = getConfigCandidates().find((filePath) => (0, import_fs7.existsSync)(filePath));
7661
+ const path = existingPath ?? CONFIG_FILE;
7662
+ if (!existingPath) {
7663
+ return {
7664
+ path,
7665
+ existed: false,
7666
+ repairedParse: false,
7667
+ messages: ["\u672A\u68C0\u6D4B\u5230\u5F53\u524D Claude Trigger Router \u914D\u7F6E\u3002"]
7668
+ };
7669
+ }
7670
+ const content = (0, import_fs7.readFileSync)(existingPath, "utf-8");
7671
+ const loaded = tryLoadStructuredConfig(existingPath, content);
7672
+ return {
7673
+ path,
7674
+ existed: true,
7675
+ repairedParse: loaded.repairedParse,
7676
+ messages: loaded.messages,
7677
+ config: loaded.config
7678
+ };
7679
+ }
7680
+ function getModelLookupId(model) {
7681
+ return model.id?.trim() || sanitizeModelId(model.model ?? "");
7682
+ }
7683
+ function repairDeterministicConfig(config) {
7684
+ const nextConfig = {
7685
+ ...config,
7686
+ HOST: config.HOST ?? DEFAULT_CONFIG2.HOST,
7687
+ PORT: config.PORT ?? DEFAULT_CONFIG2.PORT,
7688
+ LOG: config.LOG ?? DEFAULT_CONFIG2.LOG,
7689
+ LOG_LEVEL: config.LOG_LEVEL ?? DEFAULT_CONFIG2.LOG_LEVEL
7690
+ };
7691
+ const changes = [];
7692
+ if (Array.isArray(config.Models) && config.Models.length > 0) {
7693
+ nextConfig.Models = config.Models.map((item, index) => {
7694
+ const api = getModelApi(item);
7695
+ const key = getModelKey(item);
7696
+ const inferredInterface = getModelInterface(item) ?? inferInterfaceFromApi(api);
7697
+ const id = item.id?.trim() || (item.model ? sanitizeModelId(item.model) : `model_${index + 1}`);
7698
+ if (!item.id?.trim()) {
7699
+ changes.push(`\u5DF2\u8865\u5168 Models[${index}].id -> ${id}`);
7700
+ }
7701
+ if (inferredInterface && !getModelInterface(item)) {
7702
+ changes.push(`\u5DF2\u8865\u5168 Models[${index}].interface -> ${inferredInterface}`);
7703
+ }
7704
+ if (api && item.api !== api) {
7705
+ changes.push(`\u5DF2\u5F52\u4E00 Models[${index}].api`);
7706
+ }
7707
+ if (key && item.key !== key) {
7708
+ changes.push(`\u5DF2\u5F52\u4E00 Models[${index}].key`);
7709
+ }
7710
+ return {
7711
+ ...item,
7712
+ id,
7713
+ api: api || void 0,
7714
+ api_base_url: api || void 0,
7715
+ key: key || void 0,
7716
+ api_key: key || void 0,
7717
+ interface: inferredInterface,
7718
+ protocol: inferredInterface
7719
+ };
7720
+ });
7721
+ if (!nextConfig.Router?.default) {
7722
+ if (nextConfig.Models.length === 1) {
7723
+ nextConfig.Router = {
7724
+ ...nextConfig.Router ?? {},
7725
+ default: getModelLookupId(nextConfig.Models[0])
7726
+ };
7727
+ changes.push(`\u5DF2\u8865\u5168 Router.default -> ${nextConfig.Router.default}`);
7728
+ } else if (typeof config.Router?.default === "string" && config.Router.default.includes(",")) {
7729
+ const [providerName, modelName] = config.Router.default.split(",").map((item) => item.trim());
7730
+ const matched = nextConfig.Models.find(
7731
+ (item) => item.model === modelName && (item.id === providerName || item.id.startsWith(`${sanitizeModelId(providerName)}_`))
7732
+ );
7733
+ if (matched) {
7734
+ nextConfig.Router = {
7735
+ ...nextConfig.Router ?? {},
7736
+ default: matched.id
7737
+ };
7738
+ changes.push(`\u5DF2\u5F52\u4E00 Router.default -> ${matched.id}`);
7739
+ }
7740
+ }
7741
+ }
7742
+ } else if (Array.isArray(config.Providers) && config.Providers.length > 0) {
7743
+ const migrated = migrateLegacyConfig(config);
7744
+ nextConfig.Models = migrated.draft.Models;
7745
+ nextConfig.Router = {
7746
+ ...nextConfig.Router ?? {},
7747
+ ...migrated.draft.Router
7748
+ };
7749
+ nextConfig.Providers = [];
7750
+ changes.push("\u5DF2\u5C06 legacy Providers \u7ED3\u6784\u5F52\u4E00\u4E3A Models \u7ED3\u6784\u3002");
7751
+ }
7752
+ return { config: nextConfig, changes };
7753
+ }
7754
+ async function completeMissingModelFields(config, io) {
7755
+ const changes = [];
7756
+ const nextConfig = {
7757
+ ...config,
7758
+ Models: Array.isArray(config.Models) ? config.Models.map((item) => ({ ...item })) : [],
7759
+ Router: { ...config.Router ?? {} }
7760
+ };
7761
+ for (let index = 0; index < (nextConfig.Models?.length ?? 0); index += 1) {
7762
+ const model = nextConfig.Models[index];
7763
+ const label = model.id || model.model || `Models[${index}]`;
7764
+ if (!model.id?.trim()) {
7765
+ model.id = sanitizeModelId(await io.input(`\u8865\u5168 ${label} \u7684\u6A21\u578B ID`, sanitizeModelId(model.model || `model_${index + 1}`)));
7766
+ changes.push(`\u5DF2\u8865\u5168 ${label} \u7684\u6A21\u578B ID -> ${model.id}`);
7767
+ }
7768
+ if (!getModelApi(model)) {
7769
+ const api = await io.input(`\u8865\u5168 ${label} \u7684 API Base URL`);
7770
+ model.api = api;
7771
+ model.api_base_url = api;
7772
+ changes.push(`\u5DF2\u8865\u5168 ${label} \u7684 API Base URL`);
7773
+ }
7774
+ if (!getModelKey(model)) {
7775
+ const key = await io.input(`\u8865\u5168 ${label} \u7684 API Key`);
7776
+ model.key = key;
7777
+ model.api_key = key;
7778
+ changes.push(`\u5DF2\u8865\u5168 ${label} \u7684 API Key`);
7779
+ }
7780
+ if (!getModelInterface(model)) {
7781
+ const interfaceChoice = await io.choose(`\u8865\u5168 ${label} \u7684\u63A5\u53E3\u7C7B\u578B`, ["openai", "anthropic"]);
7782
+ model.interface = interfaceChoice;
7783
+ model.protocol = model.interface;
7784
+ changes.push(`\u5DF2\u8865\u5168 ${label} \u7684\u63A5\u53E3\u7C7B\u578B -> ${model.interface}`);
7785
+ }
7786
+ if (!model.model?.trim()) {
7787
+ model.model = await io.input(`\u8865\u5168 ${label} \u7684\u4E0A\u6E38\u6A21\u578B\u540D`);
7788
+ changes.push(`\u5DF2\u8865\u5168 ${label} \u7684\u4E0A\u6E38\u6A21\u578B\u540D`);
7789
+ }
7790
+ }
7791
+ if (!nextConfig.Router?.default) {
7792
+ if ((nextConfig.Models?.length ?? 0) === 1) {
7793
+ nextConfig.Router.default = nextConfig.Models[0].id;
7794
+ changes.push(`\u5DF2\u8865\u5168 Router.default -> ${nextConfig.Router.default}`);
7795
+ } else if ((nextConfig.Models?.length ?? 0) > 1) {
7796
+ const choice = await io.choose("\u8865\u5168\u9ED8\u8BA4\u6A21\u578B", nextConfig.Models.map((item) => item.id));
7797
+ nextConfig.Router.default = choice;
7798
+ changes.push(`\u5DF2\u8865\u5168 Router.default -> ${choice}`);
7799
+ }
7800
+ }
7801
+ return { config: nextConfig, changes };
7802
+ }
7803
+ async function probeModelAvailability(model) {
7804
+ const api = getModelApi(model);
7805
+ const key = getModelKey(model);
7806
+ const modelInterface = getModelInterface(model);
7807
+ if (!api || !key || !modelInterface || !model.model) {
7808
+ return {
7809
+ kind: "failure",
7810
+ category: "protocol_mismatch",
7811
+ message: "\u6A21\u578B\u914D\u7F6E\u7F3A\u5C11 api/key/interface/model\uFF0C\u65E0\u6CD5\u53D1\u8D77\u63A2\u6D4B\u3002"
7812
+ };
7813
+ }
7814
+ try {
7815
+ const registry = buildModelRegistry({
7816
+ Providers: [],
7817
+ Models: [model],
7818
+ Router: {
7819
+ default: model.id
7820
+ }
7821
+ });
7822
+ const compiledModel = registry.modelMap[model.id];
7823
+ const dispatchRequest = compiledModel ? buildProviderDispatchRequest({
7824
+ model: compiledModel.modelName,
7825
+ interface: compiledModel.interface ?? modelInterface,
7826
+ compatibilityProfile: compiledModel.compatibilityProfile,
7827
+ capabilities: compiledModel.capabilities,
7828
+ request: {
7829
+ model: compiledModel.id,
7830
+ max_tokens: 1,
7831
+ stream: true,
7832
+ messages: [
7833
+ {
7834
+ role: "user",
7835
+ content: [
7836
+ {
7837
+ type: "text",
7838
+ text: "ok"
7839
+ }
7840
+ ]
7841
+ }
7842
+ ]
7843
+ }
7844
+ }) : null;
7845
+ const response = await fetch(api, {
7846
+ method: "POST",
7847
+ signal: AbortSignal.timeout(1e4),
7848
+ headers: modelInterface === "anthropic" ? {
7849
+ "content-type": "application/json",
7850
+ "x-api-key": key,
7851
+ "anthropic-version": "2023-06-01"
7852
+ } : {
7853
+ "content-type": "application/json",
7854
+ authorization: `Bearer ${key}`
7855
+ },
7856
+ body: JSON.stringify(dispatchRequest?.body ?? (modelInterface === "anthropic" ? {
7857
+ model: model.model,
7858
+ max_tokens: 1,
7859
+ stream: true,
7860
+ messages: [
7861
+ {
7862
+ role: "user",
7863
+ content: [
7864
+ {
7865
+ type: "text",
7866
+ text: "ok"
7867
+ }
7868
+ ]
7869
+ }
7870
+ ]
7871
+ } : {
7872
+ model: model.model,
7873
+ max_tokens: 1,
7874
+ stream: true,
7875
+ messages: [
7876
+ {
7877
+ role: "user",
7878
+ content: "ok"
7879
+ }
7880
+ ]
7881
+ }))
7882
+ });
7883
+ if (response.ok) {
7884
+ return { kind: "success" };
7885
+ }
7886
+ const body = await response.text();
7887
+ if (response.status === 401 || response.status === 403) {
7888
+ return { kind: "failure", category: "auth_error", message: `${response.status} ${body}` };
7889
+ }
7890
+ if (response.status === 404) {
7891
+ return { kind: "failure", category: "model_not_found", message: `${response.status} ${body}` };
7892
+ }
7893
+ if (response.status === 400) {
7894
+ return { kind: "failure", category: "protocol_mismatch", message: `${response.status} ${body}` };
7895
+ }
7896
+ return { kind: "failure", category: "remote_error", message: `${response.status} ${body}` };
7897
+ } catch (error) {
7898
+ return {
7899
+ kind: "failure",
7900
+ category: "endpoint_unreachable",
7901
+ message: error?.message || String(error)
7902
+ };
7903
+ }
7904
+ }
7905
+ function explainProbeFailure(category) {
7906
+ switch (category) {
7907
+ case "auth_error":
7908
+ return {
7909
+ label: "\u9274\u6743\u5931\u8D25",
7910
+ summary: "\u4E0A\u6E38\u63A5\u53E3\u62D2\u7EDD\u4E86\u5F53\u524D API Key\uFF0C\u6216\u5F53\u524D\u8D26\u53F7\u6CA1\u6709\u8BBF\u95EE\u8BE5\u6A21\u578B\u7684\u6743\u9650\u3002",
7911
+ action: "\u8BF7\u68C0\u67E5 API Key\u3001\u8D26\u53F7\u8BA2\u9605\u72B6\u6001\uFF0C\u4EE5\u53CA\u5F53\u524D\u8D26\u53F7\u662F\u5426\u5177\u5907\u76EE\u6807\u6A21\u578B\u6743\u9650\u3002"
7912
+ };
7913
+ case "model_not_found":
7914
+ return {
7915
+ label: "\u6A21\u578B\u4E0D\u5B58\u5728\u6216\u65E0\u6743\u9650",
7916
+ summary: "\u4E0A\u6E38\u63A5\u53E3\u65E0\u6CD5\u8BC6\u522B\u5F53\u524D\u6A21\u578B\u540D\uFF0C\u6216\u5F53\u524D\u8D26\u53F7\u6CA1\u6709\u8BE5\u6A21\u578B\u7684\u8BBF\u95EE\u6743\u9650\u3002",
7917
+ action: "\u8BF7\u68C0\u67E5\u6A21\u578B\u540D\u662F\u5426\u6B63\u786E\uFF0C\u4EE5\u53CA\u5F53\u524D\u8D26\u53F7\u662F\u5426\u5DF2\u5F00\u901A\u8BE5\u6A21\u578B\u3002"
7918
+ };
7919
+ case "endpoint_unreachable":
7920
+ return {
7921
+ label: "\u63A5\u53E3\u4E0D\u53EF\u8FBE",
7922
+ summary: "doctor \u65E0\u6CD5\u8FDE\u63A5\u5230\u5F53\u524D API \u5730\u5740\uFF0C\u53EF\u80FD\u662F\u5730\u5740\u3001\u7F51\u7EDC\u3001TLS \u6216\u4EE3\u7406\u914D\u7F6E\u95EE\u9898\u3002",
7923
+ action: "\u8BF7\u68C0\u67E5 API Base URL\u3001\u7F51\u7EDC\u8FDE\u901A\u6027\u3001TLS \u8BC1\u4E66\u94FE\uFF0C\u4EE5\u53CA\u662F\u5426\u9700\u8981\u4EE3\u7406\u3002"
7924
+ };
7925
+ case "protocol_mismatch":
7926
+ return {
7927
+ label: "\u534F\u8BAE\u517C\u5BB9\u5931\u8D25",
7928
+ summary: "\u5F53\u524D\u4E0A\u6E38\u63A5\u53E3\u4E0E\u7EDF\u4E00\u6D88\u606F\u62BD\u8C61\u5728 messages\u3001tools\u3001stream \u6216\u63A7\u5236\u5B57\u6BB5\u4E0A\u5B58\u5728\u517C\u5BB9\u5DEE\u5F02\u3002",
7929
+ action: "\u8BF7\u5148\u786E\u8BA4 API Base URL \u548C interface \u662F\u5426\u914D\u7F6E\u6B63\u786E\uFF1B\u5982\u679C\u6587\u672C\u8BF7\u6C42\u6B63\u5E38\u4F46\u5DE5\u5177\u8C03\u7528\u5931\u8D25\uFF0C\u8BF7\u4FDD\u7559\u539F\u59CB\u62A5\u9519\u7EE7\u7EED\u6536\u655B\u517C\u5BB9\u5C42\u3002"
7930
+ };
7931
+ case "remote_error":
7932
+ return {
7933
+ label: "\u4E0A\u6E38\u8FD4\u56DE\u9519\u8BEF",
7934
+ summary: "\u8BF7\u6C42\u5DF2\u7ECF\u5230\u8FBE\u4E0A\u6E38\uFF0C\u4F46\u4E0A\u6E38\u8FD4\u56DE\u4E86\u5176\u4ED6\u4E1A\u52A1\u6216\u670D\u52A1\u7AEF\u9519\u8BEF\u3002",
7935
+ action: "\u8BF7\u7ED3\u5408\u539F\u59CB\u9519\u8BEF\u4FE1\u606F\u68C0\u67E5\u4E0A\u6E38\u670D\u52A1\u72B6\u6001\u3001\u6A21\u578B\u914D\u989D\u6216\u8D26\u53F7\u9650\u5236\u3002"
7936
+ };
7937
+ default:
7938
+ return {
7939
+ label: category,
7940
+ summary: "\u672A\u77E5\u8FDC\u7AEF\u9519\u8BEF\u3002",
7941
+ action: "\u8BF7\u4FDD\u7559\u539F\u59CB\u9519\u8BEF\u4FE1\u606F\u540E\u7EE7\u7EED\u6392\u67E5\u3002"
7942
+ };
7943
+ }
7944
+ }
7945
+ async function ensureServiceUsable(config, deps, configChanged) {
7946
+ const port = config.PORT ?? DEFAULT_CONFIG2.PORT;
7947
+ const healthy = await deps.probeServiceHealth(port, 500);
7948
+ const occupied = await deps.isTcpPortOccupied(port, 500);
7949
+ const running = deps.isServiceRunning();
7950
+ if (healthy && !configChanged) {
7951
+ deps.io.info(`\u670D\u52A1\u5065\u5EB7\u68C0\u67E5\u901A\u8FC7\uFF1Ahttp://127.0.0.1:${port}`);
7952
+ return;
7953
+ }
7954
+ if (occupied && !healthy && !running) {
7955
+ throw new Error(`\u7AEF\u53E3 ${port} \u5DF2\u88AB\u5176\u4ED6\u670D\u52A1\u5360\u7528\uFF0Cdoctor \u65E0\u6CD5\u81EA\u52A8\u542F\u52A8\u5F53\u524D\u670D\u52A1\u3002`);
7956
+ }
7957
+ if (running) {
7958
+ const info = deps.readServiceInfo();
7959
+ if (info) {
7960
+ try {
7961
+ deps.killProcess(info.pid);
7962
+ } catch {
7963
+ }
7964
+ }
7965
+ }
7966
+ await deps.startDaemon();
7967
+ const verified = await deps.waitForService(port, 5e3);
7968
+ if (!verified) {
7969
+ throw new Error(`doctor \u81EA\u52A8\u542F\u52A8\u540E\u5065\u5EB7\u68C0\u67E5\u4ECD\u672A\u901A\u8FC7\uFF08\u7AEF\u53E3 ${port}\uFF09\u3002`);
7970
+ }
7971
+ deps.io.info(`\u670D\u52A1\u5DF2\u5C31\u7EEA\uFF1Ahttp://127.0.0.1:${port}`);
7972
+ }
7973
+ function createDefaultDeps2(io = createConsoleIO2()) {
7974
+ return {
7975
+ readLegacyConfig,
7976
+ backupCurrentConfig: backupConfigFile,
7977
+ writeConfig: writeConfigFile,
7978
+ isServiceRunning,
7979
+ readServiceInfo,
7980
+ killProcess,
7981
+ probeServiceHealth,
7982
+ isTcpPortOccupied,
7983
+ waitForService,
7984
+ io,
7985
+ startDaemon: async () => {
7986
+ (0, import_child_process2.spawn)(process.execPath, [process.argv[1], "start", "--daemon"], {
7987
+ detached: true,
7988
+ stdio: "ignore",
7989
+ env: { ...process.env, CTR_DAEMON: "1" }
7990
+ }).unref();
7991
+ }
7992
+ };
7993
+ }
7994
+ async function runDoctorCli(customDeps) {
7995
+ const defaults = createDefaultDeps2(customDeps?.io);
7996
+ const deps = { ...defaults, ...customDeps };
7997
+ let configChanged = false;
7998
+ try {
7999
+ deps.io.info("\u5F00\u59CB\u8BCA\u65AD\u5F53\u524D Claude Trigger Router \u914D\u7F6E...");
8000
+ const current = loadCurrentConfig();
8001
+ current.messages.forEach((message) => deps.io.info(message));
8002
+ let workingConfig = current.config;
8003
+ if (!workingConfig) {
8004
+ const legacy = await deps.readLegacyConfig();
8005
+ if (legacy.kind === "found") {
8006
+ deps.io.info("\u672A\u68C0\u6D4B\u5230\u5F53\u524D\u914D\u7F6E\uFF0C\u4F46\u53D1\u73B0\u65E7 claude-code-router \u914D\u7F6E\uFF0Cdoctor \u5C06\u5148\u5C1D\u8BD5\u8FC1\u79FB\u3002");
8007
+ const migrated = migrateLegacyConfig(legacy.config);
8008
+ workingConfig = {
8009
+ ...buildUsableMinimalTemplateConfig(),
8010
+ ...migrated.draft
8011
+ };
8012
+ } else {
8013
+ throw new Error("\u672A\u68C0\u6D4B\u5230\u53EF\u8BCA\u65AD\u7684\u5F53\u524D\u914D\u7F6E\uFF1B\u8BF7\u5148\u8FD0\u884C ctr setup \u6216 ctr init --force\u3002");
8014
+ }
8015
+ }
8016
+ const deterministic = repairDeterministicConfig(workingConfig);
8017
+ workingConfig = deterministic.config;
8018
+ deterministic.changes.forEach((message) => deps.io.info(message));
8019
+ const completed = await completeMissingModelFields(workingConfig, deps.io);
8020
+ workingConfig = completed.config;
8021
+ completed.changes.forEach((message) => deps.io.info(message));
8022
+ const normalized = normalizeAndValidateConfig(workingConfig);
8023
+ if (normalized.errors.length > 0) {
8024
+ deps.io.error(`doctor \u4ECD\u53D1\u73B0\u65E0\u6CD5\u81EA\u52A8\u4FEE\u590D\u7684\u914D\u7F6E\u9519\u8BEF\uFF1A${normalized.errors.join("; ")}`);
8025
+ throw new Error("doctor could not fully repair config");
8026
+ }
8027
+ if (normalized.warnings.length > 0) {
8028
+ deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${normalized.warnings.join("; ")}`);
8029
+ }
8030
+ const registry = buildModelRegistry(normalized.config);
8031
+ for (const model of normalized.config.Models ?? []) {
8032
+ const compiledModel = registry.modelMap[model.id];
8033
+ if (!compiledModel) {
8034
+ continue;
8035
+ }
8036
+ const compatibility = describeCompatibilityProfile(compiledModel.compatibilityProfile);
8037
+ const dispatch = describeDispatchFormat(compiledModel.dispatchFormat);
8038
+ deps.io.info(
8039
+ `\u6A21\u578B\u517C\u5BB9\u7B56\u7565\uFF1A${model.id} -> ${compatibility.label}`
8040
+ );
8041
+ deps.io.info(`\u517C\u5BB9\u8BF4\u660E\uFF1A${compatibility.summary}`);
8042
+ deps.io.info(`\u8BF7\u6C42\u7F16\u8BD1\uFF1A${dispatch.label}\u3002${dispatch.summary}`);
8043
+ const previewDiagnostics = collectCompatibilityPreviewDiagnostics(model);
8044
+ for (const diagnostic of previewDiagnostics) {
8045
+ deps.io.info(`\u8FD0\u884C\u65F6\u517C\u5BB9\u63D0\u793A\uFF1A${diagnostic.label}`);
8046
+ deps.io.info(`\u8FD0\u884C\u65F6\u8BF4\u660E\uFF1A${diagnostic.summary}`);
8047
+ deps.io.info(`\u8FD0\u884C\u65F6\u5EFA\u8BAE\uFF1A${diagnostic.action}`);
8048
+ }
8049
+ }
8050
+ const needWrite = current.repairedParse || deterministic.changes.length > 0 || completed.changes.length > 0 || !current.existed;
8051
+ if (needWrite) {
8052
+ if (current.existed) {
8053
+ const backupPath = await deps.backupCurrentConfig();
8054
+ if (backupPath) {
8055
+ deps.io.info(`\u5DF2\u5907\u4EFD\u5F53\u524D\u914D\u7F6E\uFF1A${backupPath}`);
8056
+ }
8057
+ }
8058
+ await deps.writeConfig(normalized.config);
8059
+ deps.io.info(`\u5DF2\u5199\u56DE\u4FEE\u590D\u540E\u7684\u914D\u7F6E\uFF1A${current.path}`);
8060
+ configChanged = true;
8061
+ }
8062
+ await ensureServiceUsable(normalized.config, deps, configChanged);
8063
+ const shouldProbeModels = hasArg("--check-models") ? await deps.io.confirm(`\u5373\u5C06\u5411 ${normalized.config.Models?.length ?? 0} \u4E2A\u6A21\u578B\u53D1\u9001\u6700\u5C0F\u63A2\u6D4B\u8BF7\u6C42\uFF0C\u53EF\u80FD\u6D88\u8017\u5C11\u91CF\u989D\u5EA6\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F`, true) : await deps.io.confirm(`\u662F\u5426\u7EE7\u7EED\u63A2\u6D4B ${normalized.config.Models?.length ?? 0} \u4E2A\u6A21\u578B\u7684\u53EF\u7528\u6027\uFF1F\u8FD9\u4F1A\u6D88\u8017\u5C11\u91CF\u989D\u5EA6\u3002`, false);
8064
+ if (!shouldProbeModels) {
8065
+ deps.io.info("\u5DF2\u8DF3\u8FC7\u6A21\u578B\u63A2\u6D4B\u3002\u914D\u7F6E\u548C\u670D\u52A1\u8BCA\u65AD\u5DF2\u5B8C\u6210\u3002");
8066
+ return;
8067
+ }
8068
+ let probeSuccess = 0;
8069
+ let probeFailure = 0;
8070
+ for (const model of normalized.config.Models ?? []) {
8071
+ const result = await probeModelAvailability(model);
8072
+ if (result.kind === "success") {
8073
+ deps.io.info(`\u6A21\u578B\u63A2\u6D4B\u6210\u529F\uFF1A${model.id}`);
8074
+ probeSuccess += 1;
8075
+ continue;
8076
+ }
8077
+ const explanation = explainProbeFailure(result.category);
8078
+ probeFailure += 1;
8079
+ deps.io.error(`\u6A21\u578B\u63A2\u6D4B\u5931\u8D25\uFF1A${model.id} -> ${explanation.label}`);
8080
+ deps.io.info(`\u5931\u8D25\u8BF4\u660E\uFF1A${explanation.summary}`);
8081
+ deps.io.info(`\u5904\u7406\u5EFA\u8BAE\uFF1A${explanation.action}`);
8082
+ deps.io.info(`\u8FDC\u7AEF\u539F\u59CB\u4FE1\u606F\uFF1A${result.message}`);
8083
+ deps.io.info("\u8FD9\u7C7B\u8FDC\u7AEF\u5931\u8D25\u9700\u8981\u4F60\u786E\u8BA4\u5E76\u624B\u52A8\u5904\u7406\uFF1Bdoctor \u4E0D\u4F1A\u81EA\u52A8\u4FEE\u6539\u6A21\u578B\u8BED\u4E49\u6216\u8FDC\u7AEF\u8D26\u53F7\u914D\u7F6E\u3002");
8084
+ }
8085
+ deps.io.info(`\u6A21\u578B\u63A2\u6D4B\u5B8C\u6210\uFF1A\u6210\u529F ${probeSuccess}\uFF0C\u5931\u8D25 ${probeFailure}\u3002`);
8086
+ deps.io.info("doctor \u8BCA\u65AD\u5B8C\u6210\u3002");
8087
+ } finally {
8088
+ deps.io.close?.();
8089
+ }
8090
+ }
8091
+ var import_fs7, import_promises4, import_process2, import_child_process2, import_json53, import_js_yaml2;
8092
+ var init_doctor = __esm({
8093
+ "src/doctor/index.ts"() {
8094
+ "use strict";
8095
+ import_fs7 = require("fs");
8096
+ import_promises4 = require("readline/promises");
8097
+ import_process2 = require("process");
8098
+ import_child_process2 = require("child_process");
8099
+ import_json53 = __toESM(require("json5"));
8100
+ import_js_yaml2 = __toESM(require("js-yaml"));
8101
+ init_constants();
8102
+ init_utils();
8103
+ init_migrate();
8104
+ init_setup2();
8105
+ init_schema();
8106
+ init_compile();
8107
+ init_protocols();
8108
+ init_processCheck();
8109
+ init_service_health();
8110
+ init_templates();
8111
+ }
8112
+ });
8113
+
6516
8114
  // src/cli.ts
6517
8115
  var cli_exports = {};
6518
8116
  __export(cli_exports, {
@@ -6522,7 +8120,7 @@ __export(cli_exports, {
6522
8120
  });
6523
8121
  module.exports = __toCommonJS(cli_exports);
6524
8122
  function getPackageInfo() {
6525
- const content = (0, import_fs7.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
8123
+ const content = (0, import_fs8.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
6526
8124
  const pkg = JSON.parse(content);
6527
8125
  return {
6528
8126
  name: pkg.name ?? "@peterwangze/claude-trigger-router",
@@ -6535,7 +8133,7 @@ function getArgs() {
6535
8133
  function getCommand() {
6536
8134
  return getArgs()[0];
6537
8135
  }
6538
- function hasArg(flag, shortFlag) {
8136
+ function hasArg2(flag, shortFlag) {
6539
8137
  const args = getArgs();
6540
8138
  return args.includes(flag) || (shortFlag ? args.includes(shortFlag) : false);
6541
8139
  }
@@ -6544,23 +8142,34 @@ function getArgValue(flag, shortFlag) {
6544
8142
  const index = args.indexOf(flag) !== -1 ? args.indexOf(flag) : shortFlag ? args.indexOf(shortFlag) : -1;
6545
8143
  return index !== -1 ? args[index + 1] : void 0;
6546
8144
  }
8145
+ function parsePortValue(portValue, sourceLabel) {
8146
+ const trimmed = portValue.trim();
8147
+ if (!/^\d+$/.test(trimmed)) {
8148
+ throw new Error(`${sourceLabel} \u4E0D\u662F\u5408\u6CD5\u7AEF\u53E3\uFF1A${portValue}`);
8149
+ }
8150
+ const port = Number.parseInt(trimmed, 10);
8151
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
8152
+ throw new Error(`${sourceLabel} \u8D85\u51FA\u5408\u6CD5\u8303\u56F4\uFF081-65535\uFF09\uFF1A${portValue}`);
8153
+ }
8154
+ return port;
8155
+ }
6547
8156
  function getPort() {
6548
8157
  const portValue = getArgValue("--port", "-p");
6549
8158
  if (portValue) {
6550
- return parseInt(portValue, 10);
8159
+ return parsePortValue(portValue, "\u547D\u4EE4\u884C\u7AEF\u53E3\u53C2\u6570");
6551
8160
  }
6552
8161
  try {
6553
- const yaml3 = require("js-yaml");
6554
- if ((0, import_fs7.existsSync)(CONFIG_FILE)) {
6555
- const content = (0, import_fs7.readFileSync)(CONFIG_FILE, "utf-8");
6556
- const config = yaml3.load(content);
8162
+ const yaml4 = require("js-yaml");
8163
+ if ((0, import_fs8.existsSync)(CONFIG_FILE)) {
8164
+ const content = (0, import_fs8.readFileSync)(CONFIG_FILE, "utf-8");
8165
+ const config = yaml4.load(content);
6557
8166
  if (config?.PORT) return config.PORT;
6558
- } else if ((0, import_fs7.existsSync)(CONFIG_FILE_YML)) {
6559
- const content = (0, import_fs7.readFileSync)(CONFIG_FILE_YML, "utf-8");
6560
- const config = yaml3.load(content);
8167
+ } else if ((0, import_fs8.existsSync)(CONFIG_FILE_YML)) {
8168
+ const content = (0, import_fs8.readFileSync)(CONFIG_FILE_YML, "utf-8");
8169
+ const config = yaml4.load(content);
6561
8170
  if (config?.PORT) return config.PORT;
6562
- } else if ((0, import_fs7.existsSync)(CONFIG_FILE_JSON)) {
6563
- const content = (0, import_fs7.readFileSync)(CONFIG_FILE_JSON, "utf-8");
8171
+ } else if ((0, import_fs8.existsSync)(CONFIG_FILE_JSON)) {
8172
+ const content = (0, import_fs8.readFileSync)(CONFIG_FILE_JSON, "utf-8");
6564
8173
  const config = JSON.parse(content);
6565
8174
  if (config?.PORT) return config.PORT;
6566
8175
  }
@@ -6569,7 +8178,7 @@ function getPort() {
6569
8178
  return DEFAULT_CONFIG2.PORT;
6570
8179
  }
6571
8180
  function isDaemonMode() {
6572
- return hasArg("--daemon", "-d");
8181
+ return hasArg2("--daemon", "-d");
6573
8182
  }
6574
8183
  function printHelp() {
6575
8184
  console.log(`
@@ -6579,6 +8188,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6579
8188
 
6580
8189
  \u547D\u4EE4\uFF1A
6581
8190
  setup \u68C0\u6D4B\u5E76\u590D\u7528\u5DF2\u6709\u914D\u7F6E\uFF0C\u5FC5\u8981\u65F6\u8FC1\u79FB\u65E7\u914D\u7F6E\u6216\u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
8191
+ doctor \u8BCA\u65AD\u5E76\u4FEE\u590D\u5F53\u524D\u914D\u7F6E\uFF0C\u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
6582
8192
  init \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
6583
8193
  start \u542F\u52A8\u8DEF\u7531\u670D\u52A1\uFF08\u9ED8\u8BA4\u524D\u53F0\u8FD0\u884C\uFF09
6584
8194
  stop \u505C\u6B62\u540E\u53F0\u670D\u52A1
@@ -6587,7 +8197,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6587
8197
  version \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C\u4E0E\u5305\u4FE1\u606F
6588
8198
  upgrade \u67E5\u770B\u5347\u7EA7\u5230\u6700\u65B0 npm \u7248\u672C\u7684\u6307\u5F15
6589
8199
  code \u901A\u8FC7\u8DEF\u7531\u5668\u8FD0\u884C Claude Code\uFF08\u9700\u5148\u542F\u52A8\u670D\u52A1\uFF09
6590
- ui \u6253\u5F00\u7BA1\u7406 API \u8BF4\u660E\u9875\uFF08Web UI \u5F00\u53D1\u4E2D\uFF09
8200
+ ui \u6253\u5F00\u672C\u5730\u7BA1\u7406\u9875\uFF08\u914D\u7F6E\u9884\u89C8\u4E0E\u8C03\u8BD5\uFF09
6591
8201
  help \u663E\u793A\u6B64\u5E2E\u52A9\u4FE1\u606F
6592
8202
 
6593
8203
  \u9009\u9879\uFF1A
@@ -6597,6 +8207,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6597
8207
 
6598
8208
  \u4F7F\u7528\u793A\u4F8B\uFF1A
6599
8209
  ctr setup # \u590D\u7528\u5F53\u524D\u914D\u7F6E / \u8FC1\u79FB\u65E7\u914D\u7F6E / \u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
8210
+ ctr doctor # \u8BCA\u65AD\u914D\u7F6E / \u4FEE\u590D\u683C\u5F0F\u95EE\u9898 / \u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
6600
8211
  ctr init # \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
6601
8212
  ctr version # \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C
6602
8213
  ctr upgrade # \u67E5\u770B\u5347\u7EA7\u5230\u6700\u65B0\u7248\u672C\u7684\u547D\u4EE4
@@ -6604,6 +8215,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6604
8215
  ctr start --daemon # \u540E\u53F0\u542F\u52A8
6605
8216
  ctr status # \u67E5\u770B\u670D\u52A1\u72B6\u6001
6606
8217
  ctr code # \u542F\u52A8 Claude Code\uFF08\u9700\u5148\u8FD0\u884C ctr start\uFF09
8218
+ ctr ui # \u6253\u5F00\u672C\u5730\u7BA1\u7406\u9875\uFF08\u53EF\u9009\uFF09
6607
8219
  ctr stop # \u505C\u6B62\u540E\u53F0\u670D\u52A1
6608
8220
  ctr restart --daemon # \u91CD\u542F\u540E\u53F0\u670D\u52A1
6609
8221
 
@@ -6613,10 +8225,29 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
6613
8225
 
6614
8226
  \u914D\u7F6E\u76EE\u5F55\uFF1A${CONFIG_DIR}
6615
8227
 
8228
+ \u8865\u5145\u8BF4\u660E\uFF1A
8229
+ ctr restart \u5F53\u524D\u9ED8\u8BA4\u6309\u540E\u53F0\u6A21\u5F0F\u91CD\u542F\uFF1B\u53EF\u5199 ctr restart \u6216 ctr restart --daemon
8230
+
6616
8231
  \u66F4\u591A\u4FE1\u606F\uFF1Ahttps://github.com/peterwangze/claude-trigger-router
6617
8232
  `);
6618
8233
  }
6619
- async function getLatestPackageVersion(timeoutMs = 1500) {
8234
+ function getLatestPackageVersionViaNpm(packageName, timeoutMs = 5e3) {
8235
+ try {
8236
+ const result = (0, import_child_process3.spawnSync)("npm", ["view", packageName, "version", "--registry", PACKAGE_REGISTRY_URL], {
8237
+ encoding: "utf-8",
8238
+ timeout: timeoutMs,
8239
+ shell: process.platform === "win32"
8240
+ });
8241
+ if (result.status !== 0) {
8242
+ return null;
8243
+ }
8244
+ const value = result.stdout?.trim();
8245
+ return value ? value : null;
8246
+ } catch {
8247
+ return null;
8248
+ }
8249
+ }
8250
+ async function getLatestPackageVersion(packageName, timeoutMs = 4e3) {
6620
8251
  try {
6621
8252
  const response = await fetch(PACKAGE_REGISTRY_LATEST_URL, {
6622
8253
  signal: AbortSignal.timeout(timeoutMs)
@@ -6625,10 +8256,12 @@ async function getLatestPackageVersion(timeoutMs = 1500) {
6625
8256
  return null;
6626
8257
  }
6627
8258
  const payload = await response.json();
6628
- return typeof payload.version === "string" ? payload.version : null;
8259
+ if (typeof payload.version === "string") {
8260
+ return payload.version;
8261
+ }
6629
8262
  } catch {
6630
- return null;
6631
8263
  }
8264
+ return getLatestPackageVersionViaNpm(packageName);
6632
8265
  }
6633
8266
  function isNewerVersion(current, latest) {
6634
8267
  const currentParts = current.split(".").map((part) => Number.parseInt(part, 10));
@@ -6648,7 +8281,7 @@ function isNewerVersion(current, latest) {
6648
8281
  }
6649
8282
  async function printVersion() {
6650
8283
  const pkg = getPackageInfo();
6651
- const latestVersion = await getLatestPackageVersion();
8284
+ const latestVersion = await getLatestPackageVersion(pkg.name);
6652
8285
  console.log(`Package: ${pkg.name}`);
6653
8286
  console.log(`Version: ${pkg.version}`);
6654
8287
  console.log(`Latest: ${latestVersion ?? "unavailable"}`);
@@ -6668,30 +8301,42 @@ function printUpgradeGuidance() {
6668
8301
  console.log("\u5168\u5C40\u5B89\u88C5\u5728\u67D0\u4E9B\u73AF\u5883\u4E0B\u53EF\u80FD\u9700\u8981\u7BA1\u7406\u5458/root \u6743\u9650\u3002");
6669
8302
  console.log(`NPM: ${PACKAGE_PAGE_URL}`);
6670
8303
  }
8304
+ function printRestartGuidanceHint() {
8305
+ console.log("\u8BF4\u660E\uFF1A`ctr restart` \u5F53\u524D\u9ED8\u8BA4\u6309\u540E\u53F0\u6A21\u5F0F\u91CD\u542F\u670D\u52A1\uFF0C`--daemon` \u53EA\u662F\u663E\u5F0F\u5199\u6CD5\u3002");
8306
+ }
8307
+ function isClaudeCommandAvailable(timeoutMs = 3e3) {
8308
+ try {
8309
+ const result = (0, import_child_process3.spawnSync)("claude", ["--version"], {
8310
+ encoding: "utf-8",
8311
+ timeout: timeoutMs,
8312
+ stdio: "ignore",
8313
+ shell: process.platform === "win32"
8314
+ });
8315
+ return result.status === 0;
8316
+ } catch {
8317
+ return false;
8318
+ }
8319
+ }
6671
8320
  function initConfig2() {
6672
- const force = hasArg("--force");
6673
- const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs7.existsSync);
8321
+ const force = hasArg2("--force");
8322
+ const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs8.existsSync);
6674
8323
  if (existingConfig && !force) {
6675
8324
  console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
6676
8325
  console.log(" \u5982\u9700\u8986\u76D6\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
6677
8326
  return;
6678
8327
  }
6679
- if (!(0, import_fs7.existsSync)(CONFIG_DIR)) {
6680
- (0, import_fs7.mkdirSync)(CONFIG_DIR, { recursive: true });
6681
- }
6682
- const examplePaths = [
6683
- (0, import_path7.join)(__dirname, "..", "config", "trigger.example.yaml"),
6684
- (0, import_path7.join)((0, import_path7.dirname)(process.argv[1]), "..", "config", "trigger.example.yaml")
6685
- ];
6686
- const exampleFile = examplePaths.find((p) => (0, import_fs7.existsSync)(p));
6687
- if (!exampleFile) {
6688
- console.error("\u274C \u627E\u4E0D\u5230\u793A\u4F8B\u914D\u7F6E\u6587\u4EF6\u3002");
6689
- console.log(` \u8BF7\u624B\u52A8\u521B\u5EFA ${CONFIG_FILE}`);
6690
- console.log(" \u53C2\u8003\u6587\u6863\uFF1Ahttps://github.com/peterwangze/claude-trigger-router#configuration");
6691
- process.exit(1);
8328
+ if (!(0, import_fs8.existsSync)(CONFIG_DIR)) {
8329
+ (0, import_fs8.mkdirSync)(CONFIG_DIR, { recursive: true });
6692
8330
  }
6693
8331
  try {
6694
- (0, import_fs7.copyFileSync)(exampleFile, CONFIG_FILE);
8332
+ const yaml4 = require("js-yaml");
8333
+ const templateConfig = buildUsableMinimalTemplateConfig();
8334
+ const content = yaml4.dump(templateConfig, {
8335
+ indent: 2,
8336
+ lineWidth: -1,
8337
+ noRefs: true
8338
+ });
8339
+ (0, import_fs8.writeFileSync)(CONFIG_FILE, content, "utf-8");
6695
8340
  const action = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
6696
8341
  console.log(`\u2705 \u914D\u7F6E\u6587\u4EF6${action}\uFF1A${CONFIG_FILE}`);
6697
8342
  console.log("");
@@ -6707,10 +8352,22 @@ function initConfig2() {
6707
8352
  }
6708
8353
  }
6709
8354
  async function startForeground(port) {
8355
+ const targetPort = port ?? getPort();
8356
+ const healthy = await waitForService(targetPort, 500);
8357
+ const occupied = await isTcpPortOccupied(targetPort, 500);
8358
+ if (healthy && occupied && isServiceRunning()) {
8359
+ console.log(`\u2705 Service is already running on port ${targetPort}.`);
8360
+ console.log(" Use 'ctr status' to inspect it or 'ctr stop' before starting again.");
8361
+ return;
8362
+ }
8363
+ if (!healthy && occupied && !isServiceRunning()) {
8364
+ console.error(`\u274C Port ${targetPort} is already occupied by another service.`);
8365
+ process.exit(1);
8366
+ }
6710
8367
  console.log("\u{1F680} Starting Claude Trigger Router (foreground)...");
6711
8368
  console.log(" Press Ctrl+C to stop");
6712
8369
  try {
6713
- await run({ port });
8370
+ await run({ port: targetPort });
6714
8371
  } catch (error) {
6715
8372
  if (error.message?.includes("Invalid configuration")) {
6716
8373
  console.error("\n\u274C Configuration error. Run 'ctr init' to create a config file.");
@@ -6720,7 +8377,14 @@ async function startForeground(port) {
6720
8377
  process.exit(1);
6721
8378
  }
6722
8379
  }
6723
- function startDaemon(port) {
8380
+ async function startDaemon(port) {
8381
+ const targetPort = port ?? getPort();
8382
+ const healthy = await waitForService(targetPort, 500);
8383
+ const occupied = await isTcpPortOccupied(targetPort, 500);
8384
+ if (!healthy && occupied && !isServiceRunning()) {
8385
+ console.log(`\u274C Port ${targetPort} is already occupied by another service.`);
8386
+ return;
8387
+ }
6724
8388
  if (isServiceRunning()) {
6725
8389
  console.log("\u2705 Service is already running in the background.");
6726
8390
  return;
@@ -6731,30 +8395,58 @@ function startDaemon(port) {
6731
8395
  if (port) {
6732
8396
  childArgs.push("--port", String(port));
6733
8397
  }
6734
- const child = (0, import_child_process2.spawn)(nodeExec, childArgs, {
8398
+ const child = (0, import_child_process3.spawn)(nodeExec, childArgs, {
6735
8399
  detached: true,
6736
8400
  stdio: "ignore",
6737
8401
  env: { ...process.env, CTR_DAEMON: "1" }
6738
8402
  });
6739
8403
  child.unref();
6740
- const targetPort = port ?? getPort();
6741
- let waited = 0;
6742
- const interval = setInterval(() => {
6743
- waited += 500;
6744
- if (isServiceRunning()) {
6745
- clearInterval(interval);
6746
- console.log(`\u2705 Service started in background (port: ${targetPort})`);
6747
- console.log(` Run 'ctr stop' to stop it.`);
6748
- } else if (waited >= 5e3) {
6749
- clearInterval(interval);
6750
- console.log(`\u2705 Service launched in background (port: ${targetPort})`);
6751
- console.log(` If it fails to start, run 'ctr start' (without --daemon) to see errors.`);
6752
- }
6753
- }, 500);
6754
- }
6755
- function showStatus() {
8404
+ const startConfirmed = await new Promise((resolve, reject) => {
8405
+ let settled = false;
8406
+ const deadline = Date.now() + 5e3;
8407
+ const finish = (value) => {
8408
+ if (settled) {
8409
+ return;
8410
+ }
8411
+ settled = true;
8412
+ resolve(value);
8413
+ };
8414
+ child.once("error", reject);
8415
+ child.once("exit", () => finish(false));
8416
+ const poll = () => {
8417
+ if (settled) {
8418
+ return;
8419
+ }
8420
+ if (isServiceRunning()) {
8421
+ finish(true);
8422
+ return;
8423
+ }
8424
+ if (Date.now() >= deadline) {
8425
+ finish(false);
8426
+ return;
8427
+ }
8428
+ setTimeout(poll, 250);
8429
+ };
8430
+ poll();
8431
+ });
8432
+ if (!startConfirmed) {
8433
+ console.error(`\u274C Service failed to start in background (port: ${targetPort}).`);
8434
+ console.error(" Run 'ctr start' (without --daemon) to inspect the startup error.");
8435
+ process.exit(1);
8436
+ }
8437
+ console.log(`\u2705 Service started in background (port: ${targetPort})`);
8438
+ console.log(` Run 'ctr stop' to stop it.`);
8439
+ }
8440
+ async function showStatus() {
6756
8441
  const info = readServiceInfo();
6757
8442
  if (!info || !isServiceRunning()) {
8443
+ const targetPort = getPort();
8444
+ const healthy = await waitForService(targetPort, 500);
8445
+ const occupied = await isTcpPortOccupied(targetPort, 500);
8446
+ if (!healthy && occupied) {
8447
+ console.log(`\u26A0\uFE0F \u7AEF\u53E3 ${targetPort} \u5DF2\u88AB\u5176\u4ED6\u670D\u52A1\u5360\u7528\uFF0C\u5F53\u524D\u4E0D\u662F claude-trigger-router\u3002`);
8448
+ return;
8449
+ }
6758
8450
  console.log("\u23F9 \u670D\u52A1\u672A\u8FD0\u884C");
6759
8451
  return;
6760
8452
  }
@@ -6779,9 +8471,10 @@ function stopService() {
6779
8471
  console.error("\u274C \u505C\u6B62\u670D\u52A1\u5931\u8D25:", error.message);
6780
8472
  }
6781
8473
  }
6782
- function restartService() {
8474
+ async function restartService() {
6783
8475
  stopService();
6784
- setTimeout(() => startDaemon(getPort()), 1500);
8476
+ await new Promise((resolve) => setTimeout(resolve, 1500));
8477
+ await startDaemon(getPort());
6785
8478
  }
6786
8479
  async function runClaudeCode() {
6787
8480
  const port = getPort();
@@ -6796,14 +8489,16 @@ async function runClaudeCode() {
6796
8489
  console.log(" 1. Start service first: ctr start --daemon");
6797
8490
  console.log(" 2. Or start interactively in another terminal: ctr start");
6798
8491
  console.log("");
6799
- const proceed = process.env.CTR_AUTO_START === "1";
6800
- if (!proceed) {
6801
- process.exit(1);
6802
- }
8492
+ process.exit(1);
6803
8493
  }
6804
8494
  console.log(`\u{1F680} Starting Claude Code with Trigger Router (port: ${port})...`);
8495
+ if (!isClaudeCommandAvailable()) {
8496
+ console.error("\u274C \u672A\u68C0\u6D4B\u5230 Claude Code CLI\u3002");
8497
+ console.log(" \u8BF7\u5148\u5B89\u88C5\uFF1Anpm install -g @anthropic-ai/claude-code");
8498
+ process.exit(1);
8499
+ }
6805
8500
  const isWindows = process.platform === "win32";
6806
- const claude = (0, import_child_process2.spawn)("claude", [], {
8501
+ const claude = (0, import_child_process3.spawn)("claude", [], {
6807
8502
  stdio: "inherit",
6808
8503
  shell: isWindows,
6809
8504
  env: {
@@ -6822,10 +8517,18 @@ async function runClaudeCode() {
6822
8517
  process.exit(code || 0);
6823
8518
  });
6824
8519
  }
6825
- function openUI() {
8520
+ async function openUI() {
6826
8521
  const port = getPort();
6827
8522
  const url = `http://127.0.0.1:${port}/ui`;
8523
+ const healthy = await waitForService(port, 800);
6828
8524
  console.log(`\u{1F310} Opening UI at ${url}`);
8525
+ if (!healthy) {
8526
+ console.log("\u26A0\uFE0F \u5F53\u524D UI \u670D\u52A1\u672A\u5C31\u7EEA\uFF1B\u5982\u679C\u9875\u9762\u65E0\u6CD5\u6253\u5F00\uFF0C\u8BF7\u5148\u8FD0\u884C ctr start \u6216 ctr start --daemon\u3002");
8527
+ }
8528
+ if (process.env.CTR_UI_SKIP_OPEN === "1") {
8529
+ console.log(" Browser launch skipped by CTR_UI_SKIP_OPEN=1");
8530
+ return;
8531
+ }
6829
8532
  try {
6830
8533
  (0, import_openurl.default)(url);
6831
8534
  } catch (error) {
@@ -6838,12 +8541,15 @@ async function main() {
6838
8541
  case "setup":
6839
8542
  await runSetupCli();
6840
8543
  break;
8544
+ case "doctor":
8545
+ await runDoctorCli();
8546
+ break;
6841
8547
  case "init":
6842
8548
  initConfig2();
6843
8549
  break;
6844
8550
  case "start":
6845
8551
  if (isDaemonMode()) {
6846
- startDaemon(getPort());
8552
+ await startDaemon(getPort());
6847
8553
  } else {
6848
8554
  await startForeground(getPort());
6849
8555
  }
@@ -6852,7 +8558,7 @@ async function main() {
6852
8558
  stopService();
6853
8559
  break;
6854
8560
  case "status":
6855
- showStatus();
8561
+ await showStatus();
6856
8562
  break;
6857
8563
  case "version":
6858
8564
  await printVersion();
@@ -6861,13 +8567,14 @@ async function main() {
6861
8567
  printUpgradeGuidance();
6862
8568
  break;
6863
8569
  case "restart":
6864
- restartService();
8570
+ printRestartGuidanceHint();
8571
+ await restartService();
6865
8572
  break;
6866
8573
  case "code":
6867
8574
  await runClaudeCode();
6868
8575
  break;
6869
8576
  case "ui":
6870
- openUI();
8577
+ await openUI();
6871
8578
  break;
6872
8579
  case "help":
6873
8580
  case "--help":
@@ -6882,21 +8589,24 @@ async function main() {
6882
8589
  process.exit(command ? 1 : 0);
6883
8590
  }
6884
8591
  }
6885
- var import_child_process2, import_path7, import_openurl, import_fs7, PACKAGE_JSON_PATH, PACKAGE_PAGE_URL, PACKAGE_REGISTRY_LATEST_URL;
8592
+ var import_child_process3, import_path7, import_openurl, import_fs8, PACKAGE_JSON_PATH, PACKAGE_PAGE_URL, PACKAGE_REGISTRY_LATEST_URL, PACKAGE_REGISTRY_URL;
6886
8593
  var init_cli = __esm({
6887
8594
  "src/cli.ts"() {
6888
- import_child_process2 = require("child_process");
8595
+ import_child_process3 = require("child_process");
6889
8596
  import_path7 = require("path");
6890
8597
  import_openurl = __toESM(require("openurl"));
6891
- import_fs7 = require("fs");
8598
+ import_fs8 = require("fs");
6892
8599
  init_index();
6893
8600
  init_processCheck();
6894
8601
  init_constants();
6895
8602
  init_service_health();
6896
8603
  init_setup2();
8604
+ init_templates();
8605
+ init_doctor();
6897
8606
  PACKAGE_JSON_PATH = (0, import_path7.join)(__dirname, "..", "package.json");
6898
8607
  PACKAGE_PAGE_URL = "https://www.npmjs.com/package/@peterwangze/claude-trigger-router";
6899
8608
  PACKAGE_REGISTRY_LATEST_URL = "https://registry.npmjs.org/@peterwangze%2Fclaude-trigger-router/latest";
8609
+ PACKAGE_REGISTRY_URL = "https://registry.npmjs.org/";
6900
8610
  if (process.env.CTR_SKIP_MAIN !== "1") {
6901
8611
  main().catch((error) => {
6902
8612
  console.error("Error:", error);