@peterwangze/claude-trigger-router 1.9.0 → 1.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -2746,6 +2746,7 @@ function summarizeRouteDecisionTrace(trace) {
2746
2746
  const confidenceLabel = formatPercent(confidence);
2747
2747
  const finalModel = trace.finalModel ?? trace.routeDecision?.model;
2748
2748
  const fallbackReason = inferFallbackReason(trace);
2749
+ const collaborationMode = trace.routeDecision?.collaborationMode;
2749
2750
  const sourceLabels = {
2750
2751
  smart_rule: ruleName ? `SmartRouter rule "${ruleName}"` : "SmartRouter rule",
2751
2752
  semantic_match: ruleName ? `Semantic match "${ruleName}"` : "Semantic match",
@@ -2770,6 +2771,9 @@ function summarizeRouteDecisionTrace(trace) {
2770
2771
  semanticIntent: trace.semanticIntent,
2771
2772
  confidence,
2772
2773
  confidenceLabel,
2774
+ routingMode: trace.routeDecision?.routingMode,
2775
+ collaborationMode,
2776
+ routingEvidence: trace.routeDecision?.routingEvidence ?? [],
2773
2777
  initialModel: trace.initialModel,
2774
2778
  finalModel,
2775
2779
  fallbackReason,
@@ -5584,6 +5588,213 @@ var init_metrics_export = __esm({
5584
5588
  }
5585
5589
  });
5586
5590
 
5591
+ // src/governance/routing-advisor.ts
5592
+ function round(value, digits = 4) {
5593
+ return Number(value.toFixed(digits));
5594
+ }
5595
+ function average2(weight, total, digits = 4) {
5596
+ if (!total) {
5597
+ return 0;
5598
+ }
5599
+ return round(weight / total, digits);
5600
+ }
5601
+ function compact(value) {
5602
+ return value.replace(/\s+/g, " ").trim();
5603
+ }
5604
+ function createAccumulator(candidate) {
5605
+ return {
5606
+ model: candidate.model,
5607
+ description: candidate.description,
5608
+ totalTraces: 0,
5609
+ failureWeight: 0,
5610
+ latencySampleCount: 0,
5611
+ latencyWeight: 0,
5612
+ alignmentWeight: 0,
5613
+ cascadeWeight: 0,
5614
+ bestQualityCount: 0,
5615
+ fastestCount: 0,
5616
+ taskKeys: /* @__PURE__ */ new Set()
5617
+ };
5618
+ }
5619
+ function toCandidateProfile(acc) {
5620
+ const failureRate = average2(acc.failureWeight, acc.totalTraces);
5621
+ const averageLatencyMs = acc.latencySampleCount ? round(acc.latencyWeight / acc.latencySampleCount, 2) : 0;
5622
+ const alignmentUsedRate = average2(acc.alignmentWeight, acc.totalTraces);
5623
+ const cascadeTriggeredRate = average2(acc.cascadeWeight, acc.totalTraces);
5624
+ const latencyPenalty = averageLatencyMs ? Math.min(2, averageLatencyMs / 3e3) : 0;
5625
+ const score = round(
5626
+ acc.bestQualityCount * 4 + acc.fastestCount * 2 + (1 - failureRate) * 2 + alignmentUsedRate - cascadeTriggeredRate * 2 - latencyPenalty,
5627
+ 4
5628
+ );
5629
+ const taskKeys = Array.from(acc.taskKeys).sort();
5630
+ const evidence = [
5631
+ acc.bestQualityCount ? `best quality on ${acc.bestQualityCount} compared task(s)` : void 0,
5632
+ acc.fastestCount ? `fastest on ${acc.fastestCount} compared task(s)` : void 0,
5633
+ acc.totalTraces ? `failure ${(failureRate * 100).toFixed(1)}% over ${acc.totalTraces} trace(s)` : void 0,
5634
+ averageLatencyMs ? `average latency ${averageLatencyMs}ms` : void 0
5635
+ ].filter(Boolean);
5636
+ return {
5637
+ model: acc.model,
5638
+ configuredDescription: acc.description,
5639
+ profileSource: acc.totalTraces > 0 ? "history" : "configured",
5640
+ totalTraces: acc.totalTraces,
5641
+ failureRate,
5642
+ latencySampleCount: acc.latencySampleCount,
5643
+ averageLatencyMs,
5644
+ alignmentUsedRate,
5645
+ cascadeTriggeredRate,
5646
+ bestQualityCount: acc.bestQualityCount,
5647
+ fastestCount: acc.fastestCount,
5648
+ taskKeys,
5649
+ evidence,
5650
+ score
5651
+ };
5652
+ }
5653
+ function profileRank(left, right) {
5654
+ if (right.score !== left.score) {
5655
+ return right.score - left.score;
5656
+ }
5657
+ if (left.failureRate !== right.failureRate) {
5658
+ return left.failureRate - right.failureRate;
5659
+ }
5660
+ if (left.averageLatencyMs !== right.averageLatencyMs) {
5661
+ return left.averageLatencyMs - right.averageLatencyMs;
5662
+ }
5663
+ return left.model.localeCompare(right.model);
5664
+ }
5665
+ function buildSignature(summary) {
5666
+ return [
5667
+ summary.totalTraces,
5668
+ summary.totalComparedTasks,
5669
+ summary.routeMode,
5670
+ summary.candidateProfiles.map((profile) => `${profile.model}:${profile.totalTraces}:${profile.score}`).join("|")
5671
+ ].join(":");
5672
+ }
5673
+ function buildRoutingAdvisorSummary(input3) {
5674
+ const candidates = input3.candidates.filter((candidate) => candidate.model);
5675
+ if (candidates.length < 2) {
5676
+ return void 0;
5677
+ }
5678
+ const report2 = getGovernanceMetricsReport({
5679
+ limit: input3.historyLimit && input3.historyLimit > 0 ? input3.historyLimit : 120
5680
+ });
5681
+ const candidateModels = new Set(candidates.map((candidate) => candidate.model));
5682
+ const accumulators = new Map(candidates.map((candidate) => [candidate.model, createAccumulator(candidate)]));
5683
+ const comparisons = report2.taskComparison?.comparisons ?? [];
5684
+ for (const comparison of comparisons) {
5685
+ for (const modelEntry of comparison.models) {
5686
+ if (!candidateModels.has(modelEntry.model)) {
5687
+ continue;
5688
+ }
5689
+ const acc = accumulators.get(modelEntry.model);
5690
+ if (!acc) {
5691
+ continue;
5692
+ }
5693
+ acc.totalTraces += modelEntry.totalTraces;
5694
+ acc.failureWeight += modelEntry.failureRate * modelEntry.totalTraces;
5695
+ if (modelEntry.latencySampleCount > 0) {
5696
+ acc.latencySampleCount += modelEntry.latencySampleCount;
5697
+ acc.latencyWeight += modelEntry.averageLatencyMs * modelEntry.latencySampleCount;
5698
+ }
5699
+ acc.alignmentWeight += modelEntry.alignmentUsedRate * modelEntry.totalTraces;
5700
+ acc.cascadeWeight += modelEntry.cascadeTriggeredRate * modelEntry.totalTraces;
5701
+ if (comparison.bestModel === modelEntry.model) {
5702
+ acc.bestQualityCount += 1;
5703
+ }
5704
+ if (comparison.fastestModel === modelEntry.model) {
5705
+ acc.fastestCount += 1;
5706
+ }
5707
+ acc.taskKeys.add(comparison.taskKey);
5708
+ }
5709
+ }
5710
+ const candidateProfiles = Array.from(accumulators.values()).map(toCandidateProfile).sort(profileRank);
5711
+ const historicalProfiles = candidateProfiles.filter((profile) => profile.profileSource === "history");
5712
+ const bestQuality = [...historicalProfiles].sort((left, right) => {
5713
+ if (right.bestQualityCount !== left.bestQualityCount) {
5714
+ return right.bestQualityCount - left.bestQualityCount;
5715
+ }
5716
+ return profileRank(left, right);
5717
+ })[0];
5718
+ const fastest = [...historicalProfiles].sort((left, right) => {
5719
+ if (right.fastestCount !== left.fastestCount) {
5720
+ return right.fastestCount - left.fastestCount;
5721
+ }
5722
+ if (left.averageLatencyMs !== right.averageLatencyMs) {
5723
+ return left.averageLatencyMs - right.averageLatencyMs;
5724
+ }
5725
+ return profileRank(left, right);
5726
+ })[0];
5727
+ const preferred = historicalProfiles[0];
5728
+ const qualityLift = report2.taskComparison?.bestQualityLiftTask;
5729
+ const speedLift = report2.taskComparison?.bestSpeedLiftTask;
5730
+ const routeMode = speedLift?.fastestModel && fastest?.model === speedLift.fastestModel ? "speed" : qualityLift?.bestModel && bestQuality?.model === qualityLift.bestModel ? "quality" : "balanced";
5731
+ const evidence = [
5732
+ qualityLift ? `quality lift ${qualityLift.taskKey}: best=${qualityLift.bestModel}, baseline=${qualityLift.baselineModel}, failureDelta=${qualityLift.failureRateDelta}` : void 0,
5733
+ speedLift ? `speed lift ${speedLift.taskKey}: fastest=${speedLift.fastestModel}, latencyDeltaMs=${speedLift.latencyDeltaMs}` : void 0,
5734
+ report2.qualityEvidence?.totalSamples ? `quality evidence samples=${report2.qualityEvidence.totalSamples}, risks=${report2.qualityEvidence.failureSamples}, speedRisks=${report2.qualityEvidence.speedRiskSamples}` : void 0,
5735
+ preferred ? `preferred candidate ${preferred.model}: score=${preferred.score}, ${preferred.evidence.join("; ")}` : void 0
5736
+ ].filter(Boolean).map((item) => compact(item));
5737
+ if (!historicalProfiles.length && evidence.length === 0) {
5738
+ return void 0;
5739
+ }
5740
+ const summary = {
5741
+ totalTraces: report2.metrics.totalTraces,
5742
+ totalComparedTasks: report2.taskComparison?.totalComparedTasks ?? 0,
5743
+ totalEvidenceSamples: report2.qualityEvidence?.totalSamples ?? 0,
5744
+ routeMode,
5745
+ preferredModel: preferred?.model,
5746
+ qualityModel: bestQuality?.model,
5747
+ fastestModel: fastest?.model,
5748
+ candidateProfiles,
5749
+ evidence,
5750
+ signature: ""
5751
+ };
5752
+ return {
5753
+ ...summary,
5754
+ signature: buildSignature(summary)
5755
+ };
5756
+ }
5757
+ function orderCandidatesByRoutingAdvisor(candidates, summary) {
5758
+ if (!summary?.candidateProfiles.length) {
5759
+ return candidates;
5760
+ }
5761
+ const rank = new Map(summary.candidateProfiles.map((profile, index) => [profile.model, index]));
5762
+ return [...candidates].sort((left, right) => {
5763
+ const leftRank = rank.get(left.model);
5764
+ const rightRank = rank.get(right.model);
5765
+ if (leftRank !== void 0 && rightRank !== void 0) {
5766
+ return leftRank - rightRank;
5767
+ }
5768
+ if (leftRank !== void 0) {
5769
+ return -1;
5770
+ }
5771
+ if (rightRank !== void 0) {
5772
+ return 1;
5773
+ }
5774
+ return 0;
5775
+ });
5776
+ }
5777
+ function formatRoutingAdvisorPromptSection(summary) {
5778
+ const lines = [
5779
+ `Mode: ${summary.routeMode}`,
5780
+ summary.preferredModel ? `Preferred model from recent evidence: ${summary.preferredModel}` : void 0,
5781
+ summary.qualityModel ? `Best quality model: ${summary.qualityModel}` : void 0,
5782
+ summary.fastestModel ? `Fastest model: ${summary.fastestModel}` : void 0,
5783
+ `Compared tasks: ${summary.totalComparedTasks}; traces: ${summary.totalTraces}; evidence samples: ${summary.totalEvidenceSamples}`,
5784
+ ...summary.evidence.slice(0, 4).map((item) => `- ${item}`),
5785
+ ...summary.candidateProfiles.filter((profile) => profile.profileSource === "history").slice(0, 5).map(
5786
+ (profile) => `- ${profile.model}: score=${profile.score}, ${profile.evidence.join("; ") || "no strong historical signal"}`
5787
+ )
5788
+ ].filter(Boolean);
5789
+ return lines.join("\n");
5790
+ }
5791
+ var init_routing_advisor = __esm({
5792
+ "src/governance/routing-advisor.ts"() {
5793
+ "use strict";
5794
+ init_metrics();
5795
+ }
5796
+ });
5797
+
5587
5798
  // src/governance/index.ts
5588
5799
  var init_governance = __esm({
5589
5800
  "src/governance/index.ts"() {
@@ -5600,6 +5811,7 @@ var init_governance = __esm({
5600
5811
  init_metrics();
5601
5812
  init_metrics_export();
5602
5813
  init_io_guardrail();
5814
+ init_routing_advisor();
5603
5815
  }
5604
5816
  });
5605
5817
 
@@ -5949,7 +6161,7 @@ function renderWorkbenchHtml(rawInitialConfig, configuredThresholds = {}) {
5949
6161
  const escapedCascadeWarnRate = escapeHtml(configuredThresholds.cascade_warn_rate ?? 0.4);
5950
6162
  const escapedShadowWarnRate = escapeHtml(configuredThresholds.shadow_warn_rate ?? 0.5);
5951
6163
  const escapedLatencyWarnMs = escapeHtml(configuredThresholds.latency_warn_ms ?? 1500);
5952
- return renderWorkbenchDocumentStart() + `body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.hero{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(260px,.8fr);gap:1rem;align-items:stretch;margin-bottom:1rem}.hero h2{margin:.2rem 0 .5rem;font-size:1.55rem}.hero-copy{display:flex;flex-direction:column;justify-content:center}.status-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.status-tile{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem;min-width:0}.status-tile strong{display:block;margin-top:.2rem;word-break:break-word}@media (max-width:760px){.hero{grid-template-columns:1fr}.status-grid{grid-template-columns:1fr}}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:.75rem 1rem;flex-wrap:wrap;align-items:flex-start;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}.scope-guide{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:.75rem;margin-top:.75rem}.scope-guide div{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem}.scope-guide strong{display:block;margin-bottom:.35rem}.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}.pill.info{background:#eff6ff;color:#1d4ed8}.pill.warn{background:#fff7ed;color:#9a3412}.pill.critical{background:#fef2f2;color:#991b1b}.surface-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin:1rem 0}.surface-tab{background:#fff;color:#1f2328;border-color:#d1d5db}.surface-tab.active{background:#111827;color:#fff;border-color:#111827}.surface-panel[hidden]{display:none}.surface-heading{display:flex;gap:1rem;flex-wrap:wrap;align-items:center;margin-bottom:.75rem}</style></head><body><div class="hero"><div class="panel hero-copy"><h2>\u914D\u7F6E\u4E0E\u72B6\u6001\u5DE5\u4F5C\u53F0</h2><p class="muted">\u67E5\u770B\u5F53\u524D\u8DEF\u7531\u670D\u52A1\u3001\u6A21\u578B\u914D\u7F6E\u548C\u9ED8\u8BA4\u53BB\u5411\uFF1B\u9700\u8981\u6392\u67E5\u65F6\uFF0C\u4E0B\u65B9\u7EF4\u62A4\u8005\u533A\u57DF\u53EF\u7EE7\u7EED\u67E5\u770B Governance Trace\u3001metrics \u548C\u5F52\u6863\u3002</p><div class="action-row"><button id="loadConfigDraftHeroBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="previewConfigDraftHeroBtn" type="button">\u9884\u89C8 compiled models</button><button id="refreshStatusHeroBtn" type="button">\u5237\u65B0\u72B6\u6001</button></div></div><div class="panel"><div class="status-grid"><div class="status-tile"><span class="muted">Service</span><strong id="serviceReadyStatus">ready</strong></div><div class="status-tile"><span class="muted">Port</span><strong id="servicePortStatus">${escapedDisplayPort}</strong></div><div class="status-tile"><span class="muted">Mode</span><strong id="serviceModeStatus">${escapedRuntimeMode}</strong></div><div class="status-tile"><span class="muted">Role</span><strong id="serviceRoleStatus">${escapedServiceRole}</strong></div><div class="status-tile"><span class="muted">Listener</span><strong id="listenerStatusSummary">${escapedListenerSummary}</strong></div><div class="status-tile"><span class="muted">Models</span><strong id="modelCountStatus">${escapedModelsCount}</strong></div><div class="status-tile"><span class="muted">Router.default</span><strong id="routerDefaultStatus">${escapedRouterDefault}</strong></div><div class="status-tile"><span class="muted">Remote service</span><strong id="remoteStatusSummary">${escapedRemoteSummary}</strong></div><div class="status-tile"><span class="muted">Remote registration</span><strong id="remoteRegistrationStatusSummary">checking</strong></div><div class="status-tile"><span class="muted">Registration</span><strong id="registrationStatusSummary">${escapedRegistrationSummary}</strong></div><div class="status-tile"><span class="muted">Auth</span><strong id="authStatusSummary">${escapedAuthSummary}</strong></div><div class="status-tile"><span class="muted">Security</span><strong id="securityStatusSummary">${escapedSecuritySummary}</strong></div></div></div></div>` + renderSurfaceTabs() + `<section id="userSurface" class="surface-panel" data-surface="user"><div class="panel"><div class="surface-heading"><strong>\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u914D\u7F6E\u3001\u6A21\u578B\u3001\u8DEF\u7531\u3001\u670D\u52A1\u72B6\u6001\u4E0E\u4E0B\u4E00\u6B65\u4FDD\u5B58\u52A8\u4F5C\u3002</span></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Routing rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="subpanel"><div class="row"><strong>Current Router slots</strong><span class="muted">\u89E3\u91CA\u57FA\u7840\u8DEF\u7531\u69FD\u4F4D\u5F15\u7528\u7684 modelId\u3001\u4E0A\u6E38\u6A21\u578B\u3001\u80FD\u529B\u548C\u6F5C\u5728\u914D\u7F6E\u98CE\u9669</span></div><div id="routerSlotSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Configured slots</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Resolved slots</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Warnings</span><strong>0</strong></div></div><table id="routerSlotTable" class="management-table"><thead><tr><th>Slot</th><th>When used</th><th>Model ref</th><th>Resolved target</th><th>Capabilities</th><th>Warning</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading router slot explanation...</td></tr></tbody></table><div id="contextWindowGuide" class="alert-list" style="margin-top:.75rem"><div class="alert info"><strong>Context window guide</strong><div class="muted">\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u4E0A\u4E0B\u6587\u7A97\u53E3\u4E0E Router.longContext \u5EFA\u8BAE</div></div></div></div><div class="subpanel"><div class="row"><strong>SmartRouter explanation</strong><span class="muted">\u5C55\u793A\u89C4\u5219\u547D\u4E2D\u987A\u5E8F\u3001\u5019\u9009\u6A21\u578B\u3001router_model\u3001semantic/sticky \u5F00\u5173\u4E0E fallback</span></div><div id="smartRouterExplanationSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Enabled</span><strong>-</strong></div><div class="diff-chip"><span class="muted">Rules</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Candidates</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Warnings</span><strong>0</strong></div></div><div id="smartRouterRouteOrder" class="alert-list" style="margin-top:.75rem"><div class="alert info"><strong>Route order</strong><div class="muted">\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A SmartRouter \u51B3\u7B56\u987A\u5E8F\u3002</div></div></div><table id="smartRouterRulesTable" class="management-table"><thead><tr><th>Order</th><th>Rule</th><th>Model</th><th>Patterns</th><th>Semantic</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading SmartRouter rules...</td></tr></tbody></table><table id="smartRouterCandidatesTable" class="management-table"><thead><tr><th>Order</th><th>Candidate</th><th>Description</th><th>Status</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading SmartRouter candidates...</td></tr></tbody></table><div id="smartCandidateGuide" class="alert-list" style="margin-top:.75rem"><div class="alert info"><strong>Candidate guide</strong><div class="muted">\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u63D0\u793A fast / balanced / deep / long-context \u5019\u9009\u8986\u76D6\u3002</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u56F4\u7ED5 SmartRouter \u7EDF\u4E00\u8DEF\u7531\u5F15\u64CE\u7F16\u8F91\u89C4\u5219\u3001\u5019\u9009\u4E0E\u6CBB\u7406\u589E\u5F3A\u517C\u5BB9\u914D\u7F6E</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Routing rules</strong><span class="muted">\u663E\u5F0F\u89C4\u5219\u3001\u8BED\u4E49\u63D0\u793A\u4E0E\u517C\u5BB9\u8F93\u5165</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5F71\u5B50\u6821\u9A8C\u3001\u7EA7\u8054\u4E0E\u89C2\u6D4B\u76F8\u5173\u914D\u7F6E</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div class="alert info"><strong>Models field guide</strong><div class="muted">\u65B0\u914D\u7F6E\u8BF7\u4F7F\u7528\u5165\u53E3\u5B57\u6BB5\uFF1Aid / api / key / interface / model / thinking / metadata\uFF1Bapi_key / api_base_url / protocol \u4EC5\u4F5C\u4E3A\u65E7\u914D\u7F6E\u517C\u5BB9\u8BFB\u53D6\u3002</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" aria-label="JSON config draft" 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","thinking":"auto","metadata":{"vendor_hint":"openrouter"}}],"Router":{"default":"sonnet"}}'></textarea><div class="muted">JSON \u8349\u7A3F\u540C\u6837\u5EFA\u8BAE\u53EA\u5199\u5165\u53E3\u5B57\u6BB5\uFF1B\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u5F52\u4E00\uFF0C\u65E7\u5B57\u6BB5\u522B\u540D\u65E0\u9700\u624B\u52A8\u8865\u5145\u3002</div><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / SmartRouter / Governance\uFF08shadow/cascade\uFF09\u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Compatibility profile</th><th>Dispatch format</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="8" class="muted">Loading model map...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model pools</strong><span class="muted">Registration.models \u7F16\u8BD1\u51FA\u7684\u540C\u6A21\u578B\u591A\u6E90\u6C60\uFF0C\u5F53\u524D\u652F\u6301 priority / least-latency / round-robin / health-aware / cost-aware active endpoint\u3001\u975E\u6D41\u5F0F\u9519\u8BEF fallback\u3001health/cooldown\u3001\u7194\u65AD\u72B6\u6001\u3001\u5EF6\u8FDF\u7A97\u53E3\u4E0E\u6210\u672C/\u901F\u7387\u5143\u6570\u636E</span></div><table id="compiledModelPoolsTable" class="management-table"><thead><tr><th>Pool</th><th>Strategy</th><th>Active endpoint</th><th>Endpoints</th><th>Warnings</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading model pools...</td></tr></tbody></table></div></div></div></div></section><section id="maintainerSurface" class="surface-panel" data-surface="maintainer" hidden><div class="panel"><div class="surface-heading"><strong>\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u8FD0\u884C\u89C2\u6D4B\u3001Governance Trace\u3001metrics\u3001\u5F52\u6863\u4E0E\u7EF4\u62A4\u64CD\u4F5C\u3002</span></div><div id="securitySummary" class="alert info"><strong>Security pending</strong><div class="muted">\u7B49\u5F85\u670D\u52A1\u5B89\u5168\u72B6\u6001\u52A0\u8F7D</div></div><div class="subpanel" id="roleConnectionGuide"><div class="row"><strong>Role & connection guide</strong><span class="muted">\u6309\u5F53\u524D local / server / cloud \u89D2\u8272\u786E\u8BA4\u76D1\u542C\u5730\u5740\u3001\u7EF4\u62A4\u5165\u53E3\u548C\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u63A5\u5165\u65B9\u5F0F\u3002</span></div><div class="scope-guide"><div><strong>current role</strong><span id="roleConnectionSummary" class="muted">${escapedRuntimeMode} / ${escapedServiceRole}</span></div><div><strong>listener</strong><span id="listenerConnectionSummary" class="muted">${escapedListenerSummary}</span></div><div><strong>remote clients</strong><span id="clientConnectionSummary" class="muted">${escapedClientConnectionSummary}</span></div></div><div class="muted" style="margin-top:.75rem">${escapedLocalUserRoleGuide}</div><div class="muted" style="margin-top:.5rem">${escapedServerMaintainerRoleGuide}</div><div class="muted" style="margin-top:.5rem">${escapedRemoteClientRoleGuide}</div></div><div class="subpanel" id="authScopeGuide"><div class="row"><strong>Auth scope guide</strong><span class="muted">\u6309\u7528\u9014\u53D1\u653E\u6700\u5C0F\u6743\u9650 key\uFF0C\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u4E0D\u8981\u590D\u7528 admin key\u3002</span></div><div class="scope-guide"><div><strong>admin</strong><span class="muted">\u670D\u52A1\u6240\u6709\u8005\u4F7F\u7528\uFF1A/ui\u3001\u914D\u7F6E\u4FDD\u5B58\u3001auth \u7BA1\u7406\uFF0C\u4EE5\u53CA\u6240\u6709\u8FD0\u7EF4\u5199\u64CD\u4F5C\u3002</span></div><div><strong>operator</strong><span class="muted">\u65E5\u5E38\u8FD0\u7EF4\u4F7F\u7528\uFF1A\u91CD\u542F\u3001\u6CBB\u7406\u5FEB\u7167\u3001\u5B9A\u65F6\u5FEB\u7167\u3001\u5F02\u5E38\u9608\u503C\u548C\u5F52\u6863\u5220\u9664\uFF1B\u4E0D\u80FD\u67E5\u770B\u914D\u7F6E\u6216\u7BA1\u7406 auth\u3002</span></div><div><strong>client</strong><span class="muted">\u5BA2\u6237\u7AEF\u6A21\u578B\u8C03\u7528\uFF1A/v1/messages\u3001/v1/chat/completions\uFF1B\u6A21\u578B\u8C03\u7528\u914D\u989D\u53EA\u8BA1\u5165\u8FD9\u91CC\u3002</span></div><div><strong>read-only</strong><span class="muted">\u53EA\u8BFB\u89C2\u6D4B\uFF1Ahealth\u3001service-info\u3001compiled models\u3001model pool health\u3001transformers \u548C governance GET\u3002</span></div><div><strong>client + read-only</strong><span class="muted">\u8FDC\u7A0B token \u540C\u65F6\u9700\u8981 ready/status \u63A2\u6D4B\u4E0E\u6A21\u578B\u8C03\u7528\u65F6\u4F7F\u7528\u8BE5\u7EC4\u5408\u3002</span></div></div><div class="muted" style="margin-top:.75rem">\u7BA1\u7406\u5165\u53E3\uFF1A\u7528 admin key \u8C03\u7528 <code>GET /api/auth/keys</code> \u67E5\u770B\u5217\u8868\uFF0C<code>POST /api/auth/keys</code> \u751F\u6210 key\uFF0C<code>POST /api/auth/keys/:id/rotate</code> \u8F6E\u6362 key\uFF0C<code>POST /api/auth/keys/:id/revoke</code> \u540A\u9500 key\uFF1B\u751F\u6210\u7684 secret \u53EA\u8FD4\u56DE\u4E00\u6B21\uFF0C\u8BF7\u76F4\u63A5\u4EA4\u7ED9\u5BF9\u5E94\u5BA2\u6237\u7AEF\u4FDD\u5B58\u3002</div><div class="muted" style="margin-top:.5rem">\u5982\u679C\u670D\u52A1\u542F\u7528\u4E86\u9274\u6743\uFF0C\u6D4F\u89C8\u5668\u76F4\u63A5\u6253\u5F00 <code>/ui</code> \u4E0D\u80FD\u81EA\u52A8\u643A\u5E26 <code>Authorization</code> header\uFF1B\u7EF4\u62A4\u8005\u5E94\u901A\u8FC7\u5185\u7F51\u3001\u672C\u5730\u96A7\u9053\u6216\u53CD\u5411\u4EE3\u7406\u6CE8\u5165 <code>Authorization: Bearer &lt;admin-key&gt;</code> \u540E\u8BBF\u95EE\uFF0C\u4E0D\u8981\u628A admin key \u653E\u8FDB URL\u3002</div></div><div class="subpanel"><div class="row"><strong>Auth quota</strong><span class="muted">\u6309 managed key \u67E5\u770B\u6A21\u578B\u8C03\u7528\u914D\u989D\u3001\u5F53\u524D\u7528\u91CF\u4E0E\u7A97\u53E3\u91CD\u7F6E\u65F6\u95F4</span></div><table id="authQuotaTable" class="management-table"><thead><tr><th>Key</th><th>Scope</th><th>Status</th><th>Requests</th><th>Tokens</th><th>Window</th></tr></thead><tbody><tr><td colspan="6" class="muted">Waiting for service status...</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Model pool health</strong><span class="muted">\u67E5\u770B\u540C\u6A21\u578B\u591A\u6E90\u6C60\u7684 active endpoint\u3001\u6301\u4E45\u5316\u72B6\u6001\u3001cooldown\u3001\u7194\u65AD\u4E0E\u5EF6\u8FDF\u7A97\u53E3\u3002</span><button id="probeModelPoolBtn" type="button">\u4E3B\u52A8\u63A2\u6D4B</button></div><div id="modelPoolHealthSummary" class="alert info"><strong>Pool health pending</strong><div class="muted">\u7B49\u5F85\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001\u52A0\u8F7D</div></div><table id="modelPoolHealthTable" class="management-table"><thead><tr><th>Pool</th><th>Endpoint</th><th>Status</th><th>Latency</th><th>Failures</th><th>Last success</th><th>Recovery</th></tr></thead><tbody><tr><td colspan="7" class="muted">Waiting for model pool health...</td></tr></tbody></table></div><div class="row"><strong>\u7EF4\u62A4\u8005\u89C2\u6D4B</strong><span class="muted">\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4 Governance Trace\uFF0C\u5E76\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u6307\u6807\u3002</span></div><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/pool-health</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/health</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code>\u3001<code>/api/benchmark/history</code></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Health</span><strong>-</strong></div><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">Model switch rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment on switch</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="healthSummary" class="alert info"><strong>Health pending</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u5065\u5EB7\u6458\u8981\u52A0\u8F7D</div></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>Routing tuning</strong><span class="muted">\u57FA\u4E8E outcome \u8BC1\u636E\u7ED9\u51FA SmartRouter \u8C03\u4F18\u5EFA\u8BAE</span></div><ul id="routingTuningList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Quality evidence</strong><span class="muted">\u771F\u5B9E trace \u4E2D\u7684\u5931\u8D25\u3001\u8FDE\u7EED\u6027\u548C\u901F\u5EA6\u98CE\u9669\u6837\u672C</span></div><div id="qualityEvidenceSummary" class="stats"><div class="stat"><span class="muted">Samples</span><strong>-</strong></div><div class="stat"><span class="muted">Risk</span><strong>-</strong></div><div class="stat"><span class="muted">Improvement</span><strong>-</strong></div><div class="stat"><span class="muted">Speed risk</span><strong>-</strong></div></div><ul id="qualityEvidenceList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Task comparison</strong><span class="muted">\u540C\u7C7B\u4EFB\u52A1\u4E0B\u4E0D\u540C\u6700\u7EC8\u6A21\u578B\u7684\u5931\u8D25\u7387\u548C\u901F\u5EA6\u5BF9\u6BD4</span></div><div id="taskComparisonSummary" class="stats"><div class="stat"><span class="muted">Tasks</span><strong>-</strong></div><div class="stat"><span class="muted">Traces</span><strong>-</strong></div></div><ul id="taskComparisonList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Benchmark summary</strong><span class="muted">\u628A\u6CBB\u7406 trace \u4E0E\u56FA\u5B9A\u4EFB\u52A1\u8BC4\u6D4B\u5165\u53E3\u5408\u5E76\u6210\u7EF4\u62A4\u8005 A/B \u95ED\u73AF</span></div><div id="benchmarkSummary" class="stats"><div class="stat"><span class="muted">Comparable tasks</span><strong>-</strong></div><div class="stat"><span class="muted">Evidence samples</span><strong>-</strong></div><div class="stat"><span class="muted">Best quality lift</span><strong>-</strong></div><div class="stat"><span class="muted">Best speed lift</span><strong>-</strong></div></div><ul id="benchmarkActionList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Benchmark history</strong><span id="benchmarkHistoryFile" class="muted">\u7B49\u5F85\u5386\u53F2\u52A0\u8F7D</span></div><div id="benchmarkHistorySummary" class="stats"><div class="stat"><span class="muted">Entries</span><strong>-</strong></div><div class="stat"><span class="muted">Quality</span><strong>-</strong></div><div class="stat"><span class="muted">Trace tasks</span><strong>-</strong></div><div class="stat"><span class="muted">Latency</span><strong>-</strong></div></div><ul id="benchmarkHistoryList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Human calibration</strong><span id="benchmarkCalibrationStatus" class="muted">\u628A\u4EBA\u5DE5\u590D\u6838\u6837\u672C\u8FFD\u52A0\u5230 benchmark history</span></div><div class="control-grid"><div><label>Task ID</label><input id="calibrationTaskId" value="quick_status"></div><div><label>Model</label><input id="calibrationModel" placeholder="sonnet"></div><div><label>Human score</label><input id="calibrationHumanScore" placeholder="0.9"></div><div><label>Latency ms</label><input id="calibrationLatencyMs" placeholder="1200"></div></div><div style="margin-top:.75rem"><label>Output</label><textarea id="calibrationOutput" rows="4" placeholder="Paste the model output to score"></textarea></div><div style="margin-top:.75rem"><label>Calibration notes</label><textarea id="calibrationNotes" rows="2" placeholder="Why did the human score differ from the rubric?"></textarea></div><div class="row" style="margin-top:.75rem"><button id="saveCalibrationBtn" type="button">\u4FDD\u5B58\u6821\u51C6\u6837\u672C</button><span class="muted">\u4FDD\u5B58\u6458\u8981\uFF0C\u4E0D\u6301\u4E45\u5316\u539F\u59CB output</span></div></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${escapedMinSampleSize}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${escapedCascadeWarnRate}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${escapedShadowWarnRate}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${escapedLatencyWarnMs}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by route</strong><span class="muted">\u5207\u6362\u3001alignment\u3001cascade \u4E0E\u5EF6\u8FDF</span></div><ul id="routeOutcomeRanking" 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>Outcome by model</strong><span class="muted">\u6700\u7EC8\u6A21\u578B\u5207\u6362\u4E0E\u5EF6\u8FDF\u8868\u73B0</span></div><ul id="modelOutcomeRanking" 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>Outcome by intent</strong><span class="muted">\u4EFB\u52A1\u610F\u56FE\u5207\u6362\u4E0E\u5EF6\u8FDF\u8868\u73B0</span></div><ul id="intentOutcomeRanking" 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><div class="subpanel"><div class="row"><strong>Recent route decisions</strong><span class="muted">\u628A\u6700\u8FD1\u8BF7\u6C42\u7684 route source\u3001\u89C4\u5219\u3001\u8BED\u4E49\u610F\u56FE\u3001\u7F6E\u4FE1\u5EA6\u548C fallback \u539F\u56E0\u7FFB\u8BD1\u6210\u53EF\u8BFB\u6458\u8981\u3002</span></div><ul id="routeDecisionSummaryList" class="mini-list"><li><span class="muted">Loading route decisions</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Recent switch continuity</strong><span class="muted">\u89E3\u91CA\u6700\u8FD1\u8BF7\u6C42\u662F\u5426\u5207\u6362\u6A21\u578B\u3001\u662F\u5426\u8865\u4E0A\u4E0B\u6587\uFF0C\u4EE5\u53CA\u5207\u6362\u540E\u662F\u5426\u89E6\u53D1 cascade\u3002</span></div><ul id="switchContinuitySummaryList" class="mini-list"><li><span class="muted">Loading switch continuity</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Route handoff</strong><span class="muted">\u8DDF\u8E2A\u8BF7\u6C42\u4ECE\u8DEF\u7531\u3001agent/tool\u3001\u534F\u8BAE\u5206\u53D1\u5230\u54CD\u5E94\u6CBB\u7406\u7684\u4EA4\u63A5\u94FE\u8DEF\u3002</span></div><ul id="routeHandoffSummaryList" class="mini-list"><li><span class="muted">Loading route handoff</span><strong>-</strong></li></ul></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>GET /api/governance/health</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5065\u5EB7\u6458\u8981</li><li><code>GET /api/auth/audit</code> \u2014 \u67E5\u770B\u9274\u6743\u5BA1\u8BA1\u6458\u8981</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div></section>` + renderWorkbenchScriptStart() + `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 routerSlotSummary=document.getElementById('routerSlotSummary');const routerSlotTableBody=document.querySelector('#routerSlotTable tbody');const contextWindowGuide=document.getElementById('contextWindowGuide');const smartRouterExplanationSummary=document.getElementById('smartRouterExplanationSummary');const smartRouterRouteOrder=document.getElementById('smartRouterRouteOrder');const smartRouterRulesTableBody=document.querySelector('#smartRouterRulesTable tbody');const smartRouterCandidatesTableBody=document.querySelector('#smartRouterCandidatesTable tbody');const smartCandidateGuide=document.getElementById('smartCandidateGuide');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const serviceReadyStatus=document.getElementById('serviceReadyStatus');const servicePortStatus=document.getElementById('servicePortStatus');const serviceModeStatus=document.getElementById('serviceModeStatus');const serviceRoleStatus=document.getElementById('serviceRoleStatus');const listenerStatusSummary=document.getElementById('listenerStatusSummary');const roleConnectionSummary=document.getElementById('roleConnectionSummary');const listenerConnectionSummary=document.getElementById('listenerConnectionSummary');const clientConnectionSummary=document.getElementById('clientConnectionSummary');const remoteStatusSummary=document.getElementById('remoteStatusSummary');const registrationStatusSummary=document.getElementById('registrationStatusSummary');const authStatusSummary=document.getElementById('authStatusSummary');const securityStatusSummary=document.getElementById('securityStatusSummary');const modelCountStatus=document.getElementById('modelCountStatus');const routerDefaultStatus=document.getElementById('routerDefaultStatus');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const compiledModelPoolsTableBody=document.querySelector('#compiledModelPoolsTable 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 routeOutcomeRanking=document.getElementById('routeOutcomeRanking');const modelOutcomeRanking=document.getElementById('modelOutcomeRanking');const intentOutcomeRanking=document.getElementById('intentOutcomeRanking');const healthSummary=document.getElementById('healthSummary');const routingTuningList=document.getElementById('routingTuningList');const routeDecisionSummaryList=document.getElementById('routeDecisionSummaryList');const switchContinuitySummaryList=document.getElementById('switchContinuitySummaryList');const routeHandoffSummaryList=document.getElementById('routeHandoffSummaryList');const qualityEvidenceSummary=document.getElementById('qualityEvidenceSummary');const qualityEvidenceList=document.getElementById('qualityEvidenceList');const taskComparisonSummary=document.getElementById('taskComparisonSummary');const taskComparisonList=document.getElementById('taskComparisonList');const benchmarkSummary=document.getElementById('benchmarkSummary');const benchmarkActionList=document.getElementById('benchmarkActionList');const benchmarkHistoryFile=document.getElementById('benchmarkHistoryFile');const benchmarkHistorySummary=document.getElementById('benchmarkHistorySummary');const benchmarkHistoryList=document.getElementById('benchmarkHistoryList');const benchmarkCalibrationStatus=document.getElementById('benchmarkCalibrationStatus');const securitySummary=document.getElementById('securitySummary');const authQuotaTableBody=document.querySelector('#authQuotaTable tbody');const modelPoolHealthSummary=document.getElementById('modelPoolHealthSummary');const modelPoolHealthTableBody=document.querySelector('#modelPoolHealthTable tbody');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');const surfaceTabs=Array.from(document.querySelectorAll('[data-surface-target]'));const surfacePanels=Array.from(document.querySelectorAll('[data-surface]'));let currentDraftConfig={};let knownModelIds=[];let lastCompiledModelsData=null;let activeValidationHighlight=null;function withDraftCompiledData(payload){ return { ...(lastCompiledModelsData || {}), normalizedConfig: payload || currentDraftConfig || {} }; }const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94\u8DEF\u7531\u89C4\u5219\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u589E\u5F3A\u4E0E\u6821\u9A8C\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','SmartRouter.sticky.alignment','SmartRouter.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function limitText(used,limit){ return Number.isFinite(limit) ? (String(used ?? 0)+' / '+String(limit)) : String(used ?? 0); }function renderAuthQuotaTable(quota){ const keys=Array.isArray(quota?.keys) ? quota.keys : []; if(!keys.length){ authQuotaTableBody.innerHTML='<tr><td colspan="6" class="muted">No managed keys configured</td></tr>'; return; } authQuotaTableBody.innerHTML=keys.map(item=>{ const usage=item.usage || {}; const quotaCfg=item.quota || {}; const keyName=esc(item.label || item.id || '-')+'<div class="muted"><code>'+esc(item.id || '-')+'</code></div>'; const statusClass=item.status === 'exhausted' ? 'critical' : (item.status === 'watch' ? 'warn' : 'info'); const windowText=quotaCfg.window_seconds ? (esc(quotaCfg.window_seconds)+'s'+(usage.windowResetAt ? '<div class="muted">reset '+esc(String(usage.windowResetAt).replace('T',' ').replace('.000Z','Z'))+'</div>' : '<div class="muted">not started</div>')) : '-'; return '<tr><td>'+keyName+'</td><td>'+esc((item.scopes || []).join(', ') || '-')+'</td><td><span class="pill '+statusClass+'">'+esc(item.status || '-')+'</span></td><td>'+esc(limitText(usage.requestsUsed,usage.requestLimit))+'</td><td>'+esc(limitText(usage.tokensUsed,usage.tokenLimit))+'</td><td>'+windowText+'</td></tr>'; }).join('');}function renderModelPoolHealth(data){ const summary=data?.summary || {}; const pools=Array.isArray(data?.pools) ? data.pools : []; const statusClass=summary.open ? 'critical' : (summary.cooldown ? 'warn' : 'info'); const averageLatency=Number.isFinite(summary.averageLatencyMs) ? (Number(summary.averageLatencyMs).toFixed(0)+' ms avg') : 'no latency samples'; modelPoolHealthSummary.className='alert '+statusClass; modelPoolHealthSummary.innerHTML='<strong>Pool health: '+esc(summary.healthy || 0)+' healthy / '+esc(summary.cooldown || 0)+' cooldown / '+esc(summary.open || 0)+' open</strong><div class="muted">'+esc(summary.pools || 0)+' pools \xB7 '+esc(summary.endpoints || 0)+' endpoints \xB7 '+esc(averageLatency)+' \xB7 persisted endpoints '+esc(data?.persistedState?.endpoints || 0)+'</div>'; const rows=[]; pools.forEach(pool=>{ (pool.endpoints || []).forEach(endpoint=>{ const recovery=endpoint.circuitOpenUntil ? ('circuit opens until '+new Date(endpoint.circuitOpenUntil).toISOString()) : endpoint.cooldownUntil ? ('cooldown until '+new Date(endpoint.cooldownUntil).toISOString()) : '-'; const latency=endpoint.latency ? (Number(endpoint.latency.averageMs || 0).toFixed(0)+' ms avg / '+esc(endpoint.latency.sampleCount || 0)+' samples') : '-'; const cost=endpoint.cost ? ('cost '+esc(endpoint.cost.inputPer1MTokens ?? '-')+'/'+esc(endpoint.cost.outputPer1MTokens ?? '-')+' '+esc(endpoint.cost.currency || 'USD')+'/1M') : 'cost -'; const rate=endpoint.rateLimit ? ('rate '+esc(endpoint.rateLimit.requestsPerMinute ?? '-')+' rpm / '+esc(endpoint.rateLimit.tokensPerMinute ?? '-')+' tpm') : 'rate -'; const endpointLabel='<code>'+esc(endpoint.id || '-')+'</code>'+(endpoint.active ? ' <span class="pill info">active</span>' : '')+'<div class="muted">'+esc(endpoint.providerName || '-')+' / '+esc(endpoint.upstreamServiceId || endpoint.upstreamBaseUrl || 'local')+'</div><div class="muted">'+cost+' \xB7 '+rate+'</div>'; const statusCls=endpoint.status === 'open' ? 'critical' : (endpoint.status === 'cooldown' ? 'warn' : 'info'); rows.push('<tr><td><code>'+esc(pool.modelId || '-')+'</code><div class="muted">'+esc(pool.strategy || '-')+'</div></td><td>'+endpointLabel+'</td><td><span class="pill '+statusCls+'">'+esc(endpoint.status || '-')+'</span></td><td>'+esc(latency)+'</td><td>'+esc(endpoint.failureCount || 0)+'<div class="muted">success '+esc(endpoint.successCount || 0)+'</div></td><td>'+esc(endpoint.lastSuccessAt ? new Date(endpoint.lastSuccessAt).toISOString() : '-')+'</td><td>'+esc(recovery)+'</td></tr>'); }); }); modelPoolHealthTableBody.innerHTML=rows.length ? rows.join('') : '<tr><td colspan="7" class="muted">No registration model pools configured</td></tr>';}async function loadModelPoolHealth(){ const res=await fetch('/api/models/pool-health'); const data=await res.json(); renderModelPoolHealth(data);}async function probeModelPoolHealth(){ modelPoolHealthSummary.className='alert info'; modelPoolHealthSummary.innerHTML='<strong>Probing pool health...</strong><div class="muted">\u6B63\u5728\u8F7B\u91CF\u63A2\u6D4B enabled endpoint</div>'; const res=await fetch('/api/models/pool-health/probe',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({}) }); const data=await res.json(); renderModelPoolHealth(data.health || data);}function renderRoleConnectionGuide(data){ const listener=data.listener || {}; const connection=data.clientConnection || {}; const mode=data.runtimeMode || '-'; const role=data.serviceRole || '-'; const listenerText=listener.host ? (listener.host+':'+(listener.port || '-')+(listener.public ? ' (public)' : ' (local)')) : '-'; const connectionText=connection.baseUrl ? (connection.baseUrl+' \xB7 '+(Array.isArray(connection.recommendedScopes) ? connection.recommendedScopes.join(' + ') : '')) : (connection.guidance || '-'); listenerStatusSummary.textContent=listenerText; roleConnectionSummary.textContent=mode+' / '+role; listenerConnectionSummary.textContent=listenerText; clientConnectionSummary.textContent=connectionText || '-';}function setActiveSurface(surfaceName){ surfacePanels.forEach((panel)=>{ panel.hidden=panel.dataset.surface !== surfaceName; }); surfaceTabs.forEach((tab)=>{ const active=tab.dataset.surfaceTarget === surfaceName; tab.classList.toggle('active',active); tab.setAttribute('aria-selected', active ? 'true' : 'false'); });}function inferProviderTemplateKey(model){ const explicit=String(model?.provider_template || '').trim(); if(explicit && modelProviderTemplates[explicit]){ return explicit; } const api=String(model?.api || model?.api_base_url || '').trim().toLowerCase(); const modelInterface=String(model?.interface || model?.protocol || '').trim().toLowerCase(); const exactMatch=Object.entries(modelProviderTemplates).find(([,item])=>String(item.api || '').trim().toLowerCase()===api && String(item.interface || '').trim().toLowerCase()===modelInterface); if(exactMatch){ return exactMatch[0]; } if(api.includes('api.anthropic.com/v1/messages') || modelInterface === 'anthropic'){ return 'anthropic'; } if(api.includes('openrouter.ai')){ return 'openrouter'; } if(api.includes('deepseek.com')){ return 'deepseek'; } if(api.includes('siliconflow.cn')){ return 'siliconflow'; } if(api.includes('api.openai.com')){ return 'openai-compatible'; } return '';}function getProviderTemplateContext(model){ const templateKey=inferProviderTemplateKey(model) || defaultProviderTemplateKey; return { templateKey, template:modelProviderTemplates[templateKey] || modelProviderTemplates[defaultProviderTemplateKey] || {} };}function createDraftModelFromTemplate(templateKey){ const resolvedKey=(templateKey && modelProviderTemplates[templateKey]) ? templateKey : defaultProviderTemplateKey; const template=modelProviderTemplates[resolvedKey] || {}; return { provider_template:resolvedKey, id:template.suggested_id || '', api:template.api || '', interface:template.interface || 'openai', model:template.default_model || '', thinking:template.default_thinking || 'auto' };}function getModelIdSuggestionsMarkup(idPrefix){ return '<datalist id="'+idPrefix+'">'+knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join('')+'</datalist>';}function resolvePresetModelId(seed){ const source=String(seed || '').trim().toLowerCase(); if(!source || !knownModelIds.length){ return seed; } if(knownModelIds.includes(seed)){ return seed; } const ranked=knownModelIds.map((modelId)=>{ const target=String(modelId || '').toLowerCase(); let score=0; if(target===source){ score+=100; } if(target.includes(source) || source.includes(target)){ score+=40; } source.split(/[^a-z0-9]+/).filter(Boolean).forEach((part)=>{ if(target.includes(part)){ score+=Math.min(part.length * 4, 24); } }); return { modelId, score }; }).filter((item)=>item.score>0).sort((a,b)=>b.score-a.score || a.modelId.localeCompare(b.modelId)); return ranked.length ? ranked[0].modelId : seed;}function getTriggerPatternValidationHint(pattern){ if((pattern?.type || 'exact') === 'regex'){ return pattern?.pattern ? { level:'ok', message:'regex pattern \u5DF2\u914D\u7F6E' } : { level:'warn', message:'regex \u6A21\u5F0F\u9700\u8981\u586B\u5199 pattern' }; } return Array.isArray(pattern?.keywords) && pattern.keywords.some((keyword)=>String(keyword || '').trim()) ? { level:'ok', message:'exact keywords \u5DF2\u914D\u7F6E' } : { level:'warn', message:'exact \u6A21\u5F0F\u81F3\u5C11\u9700\u8981\u4E00\u4E2A keyword' };}function getDraftSmartRouterConfig(config){ const smart={ ...((config && config.SmartRouter) || {}) }; const smartExplicit=config && Object.prototype.hasOwnProperty.call(config,'SmartRouter'); const legacyIntentEnabled=Boolean(config?.TriggerRouter?.llm_intent_recognition); const legacyIntentModel=config?.TriggerRouter?.intent_model || ''; if(!smart.analysis_scope && config?.TriggerRouter?.analysis_scope){ smart.analysis_scope=config.TriggerRouter.analysis_scope; } if((!Array.isArray(smart.rules) || !smart.rules.length) && Array.isArray(config?.TriggerRouter?.rules)){ smart.rules=config.TriggerRouter.rules; } if(!smart.semantic && (config?.Governance?.semantic || config?.TriggerRouter?.llm_intent_recognition)){ smart.semantic={ ...((config && config.Governance && config.Governance.semantic) || {}) }; if(config?.TriggerRouter?.llm_intent_recognition){ smart.semantic.enabled=true; smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || config.TriggerRouter.intent_model || ''; } } if(!smart.sticky && config?.Governance?.sticky){ smart.sticky={ ...(config.Governance.sticky || {}) }; } if(!smartExplicit && !smart.enabled && (config?.TriggerRouter?.enabled || smart.rules?.length || smart.router_model || smart.candidates?.length || smart.semantic || smart.sticky)){ smart.enabled=true; } if(smart.enabled){ smart.analysis_scope=smart.analysis_scope || 'last_message'; smart.semantic={ ...(smart.semantic || {}) }; smart.semantic.enabled=smart.semantic.enabled !== undefined ? smart.semantic.enabled : true; smart.semantic.threshold=smart.semantic.threshold !== undefined ? smart.semantic.threshold : 0.2; if(legacyIntentEnabled){ smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || legacyIntentModel; } smart.sticky={ ...(smart.sticky || {}) }; smart.sticky.enabled=smart.sticky.enabled !== undefined ? smart.sticky.enabled : true; smart.sticky.alignment={ ...((smart.sticky && smart.sticky.alignment) || {}) }; smart.sticky.alignment.enabled=smart.sticky.alignment.enabled !== undefined ? smart.sticky.alignment.enabled : true; smart.sticky.alignment.summarizer_model=smart.sticky.alignment.summarizer_model || smart.router_model || config?.Router?.default || legacyIntentModel || ''; } return smart;}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const smart=getDraftSmartRouterConfig(config); const triggerRules=Array.isArray(smart?.rules) ? smart.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(smart?.candidates) ? smart.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, smart?.router_model, smart?.sticky?.alignment?.summarizer_model, smart?.semantic?.classifier_model, config?.Governance?.shadow?.verifier_model].filter(v=>typeof v === 'string' && v.trim()).length + triggerRules.filter(rule=>rule?.model).length + smartCandidates.filter(item=>item?.model).length + cascadeLevels.reduce((sum,level)=>sum + (level?.from ? 1 : 0) + (level?.to ? 1 : 0), 0); draftSummaryGrid.innerHTML=[ ['Models', models.length], ['Routing rules', triggerRules.length], ['Patterns', patternCount], ['Smart candidates', smartCandidates.length], ['Cascade levels', cascadeLevels.length], ['Model refs', modelRefCount] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function updateStatusSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; modelCountStatus.textContent=String(models.length); routerDefaultStatus.textContent=config?.Router?.default || '-';}function renderDraftValidation(errors,warnings,issueReport){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; const contractIssues=Array.isArray(issueReport?.issues) ? issueReport.issues : []; if(!errorList.length && !warningList.length && !contractIssues.length){ draftValidationList.innerHTML='<div class="alert info"><strong>No validation issues</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u672A\u53D1\u73B0\u96C6\u4E2D\u5C55\u793A\u7684\u95EE\u9898</div></div>'; return; } const extractPath=(text)=>{ const match=String(text).match(/^(Models(?:\\[[0-9]+\\])?(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Router(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|TriggerRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|SmartRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Governance(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?)/); return match ? match[1] : ''; }; const sourceItems=contractIssues.length ? contractIssues.map(item=>({ text:String(item.message || ''), severity:item.severity==='error' ? 'error' : 'warning', path:item.path || '', action:item.action || '' })) : [...errorList.map(item=>({ text:String(item), severity:'error', path:'', action:'' })), ...warningList.map(item=>({ text:String(item), severity:'warning', path:'', action:'' }))]; const grouped=sourceItems.reduce((acc,item)=>{ const text=item.text; const path=item.path || extractPath(text); const bucket=path.startsWith('Models') || text.startsWith('Models') ? 'Models' : path.startsWith('Router') || text.startsWith('Router') ? 'Router' : path.startsWith('TriggerRouter') || text.startsWith('TriggerRouter') ? 'SmartRouter' : path.startsWith('SmartRouter') || text.startsWith('SmartRouter') ? 'SmartRouter' : (path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic') || text.startsWith('Governance.sticky') || text.startsWith('Governance.semantic')) ? 'SmartRouter' : path.startsWith('Governance') || text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path, severity:item.severity, action:item.action || '' }); return acc; }, {}); const errorCount=contractIssues.length ? contractIssues.filter(item=>item.severity==='error').length : errorList.length; const warningCount=contractIssues.length ? contractIssues.filter(item=>item.severity!=='error').length : warningList.length; const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorCount)+' errors / '+esc(warningCount)+' warnings</span></div><div class="muted">'+(errorCount ? '\u8BF7\u4F18\u5148\u4FEE\u590D errors\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u63A5\u53D7 warnings\u3002' : '\u5F53\u524D\u65E0\u963B\u65AD\u9519\u8BEF\uFF0C\u53EF\u6309\u9700\u5904\u7406 warnings\u3002')+'</div></div>'; draftValidationList.innerHTML=summary + Object.entries(grouped).map(([bucket,items])=>{ const hasError=items.some(item=>item.severity==='error'); const levelClass=hasError ? 'warn' : 'info'; const actionLabel=hasError ? 'repair first' : 'review before save'; return '<div class="alert '+levelClass+'"><div class="row"><strong>'+esc(bucket)+'</strong><span class="pill">'+esc(items.length)+' issues</span></div><div class="muted">'+esc(actionLabel)+'</div><div>'+items.slice(0,4).map(item=>'<div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+'<span class="pill">'+esc(item.severity==='error' ? 'error' : 'warning')+'</span> '+esc(item.text)+(item.action ? ('<div class="muted">Action: '+esc(item.action)+'</div>') : '')+'</div>').join('')+'</div></div>'; }).join('');}function getCapabilityWarningActionLabel(code){ if(code==='thinking_ignored'){ return '\u79FB\u9664 thinking'; } if(code==='tools_text_fallback' || code==='images_text_fallback'){ return '\u6062\u590D\u9ED8\u8BA4 capability'; } return '';}function renderCapabilityWarnings(report){ const entries=Array.isArray(report?.entries) ? report.entries : []; if(!entries.length){ capabilityWarningsList.innerHTML='<div class="alert info"><strong>No capability warnings</strong><div class="muted">\u5F53\u524D compiled models \u672A\u53D1\u73B0\u9700\u8981\u989D\u5916\u63D0\u793A\u7684\u80FD\u529B\u964D\u7EA7</div></div>'; return; } const summary=report?.summary || {}; capabilityWarningsList.innerHTML='<div class="alert info"><strong>Capability warning summary</strong><div class="muted">warn '+esc(summary.warn ?? 0)+' / info '+esc(summary.info ?? 0)+' / total '+esc(summary.total ?? entries.length)+'</div></div>' + entries.map(item=>{ const actionLabel=getCapabilityWarningActionLabel(item.code); return '<div class="alert '+esc(item.level === 'warn' ? 'warn' : 'info')+'"><div class="row"><strong>'+esc(item.code || item.level || 'warning')+'</strong><span class="pill">'+esc(item.modelId || '-').trim()+'</span></div><div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+esc(item.message || '')+'</div>'+(actionLabel ? ('<div class="row" style="margin-top:.5rem"><button type="button" data-apply-warning-path="'+esc(item.path || '')+'" data-apply-warning-code="'+esc(item.code || '')+'">'+esc(actionLabel)+'</button></div>') : '')+'</div>'; }).join('');}function findValidationTarget(path){ if(!path){ return null; } if(path.startsWith('Models')){ return modelsFormGrid; } if(path === 'Router.default'){ return draftRouterDefault; } if(path.startsWith('TriggerRouter.intent_model')){ return triggerIntentModel; } if(path.startsWith('TriggerRouter.rules[')){ return triggerRulesList; } if(path.startsWith('SmartRouter.router_model')){ return smartRouterModel; } if(path.startsWith('SmartRouter.candidates[')){ return smartCandidatesList; } if(path.startsWith('Governance.cascade.levels[')){ return governanceCascadeLevelsList; } if(path.startsWith('Governance.sticky.alignment')){ return governanceSummarizerModel; } if(path.startsWith('Governance.semantic')){ return governanceClassifierModel; } if(path.startsWith('Governance.shadow')){ return governanceVerifierModel; } if(path.startsWith('Governance')){ return governanceEnabled; } return null;}function jumpToValidationPath(path){ const target=findValidationTarget(path); if(!target || typeof target.scrollIntoView !== 'function'){ return; } if(activeValidationHighlight && activeValidationHighlight.classList){ activeValidationHighlight.classList.remove('jump-highlight'); } target.scrollIntoView({ behavior:'smooth', block:'center' }); if(target.classList){ target.classList.add('jump-highlight'); activeValidationHighlight=target; setTimeout(()=>{ if(target.classList){ target.classList.remove('jump-highlight'); if(activeValidationHighlight===target){ activeValidationHighlight=null; } } }, 1800); } if(typeof target.focus === 'function'){ target.focus({ preventScroll:true }); }}function renderDraftPresetModeHint(){ const overwriteMode=draftPresetMode.value === 'replace'; draftPresetModeHint.textContent=overwriteMode ? 'overwrite \u4F1A\u91CD\u7F6E SmartRouter / Governance \u76F8\u5173\u8868\u5355\uFF0C\u518D\u5E94\u7528\u9884\u8BBE' : 'append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145 SmartRouter / Governance \u76F8\u5173\u5B57\u6BB5';}function deriveActualAffectedAreas(preview){ const areas=new Set(); const diff=preview?.diff || {}; const impact=preview?.referenceImpact || {}; if((diff.providerChanges || []).length || (diff.modelChanges || []).length){ areas.add('Models'); } (impact.entries || []).forEach((entry)=>{ const path=String(entry.path || ''); if(path.startsWith('Router.')){ areas.add('Router'); } else if(path.startsWith('TriggerRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.')){ areas.add('Governance'); } }); return Array.from(areas);}function renderDraftPreviewMeta(meta){ if(!meta){ draftPreviewMeta.innerHTML='<div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div>'; return; } draftPreviewMeta.innerHTML='<div class="alert info"><strong>'+esc(meta.title || 'Preset dry-run')+'</strong><div>'+esc(meta.description || '')+'</div><div class="muted">\u6A21\u5F0F\uFF1A'+esc(meta.mode || '-')+' \xB7 \u9884\u8BBE\u58F0\u660E\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((meta.affects || []).join(' / ') || '-')+'</div><div class="muted">\u5B9E\u9645\u9884\u89C8\u547D\u4E2D\u533A\u57DF\uFF1A'+esc((meta.actualAffects || []).join(' / ') || '-')+'</div></div>';}function renderDraftPresetGuide(){ draftPresetList.innerHTML=Object.entries(draftPresets).map(([key,preset])=>'<div class="alert info"><strong>'+esc(preset.label || key)+'</strong><div>'+esc(preset.description || '')+'</div><div class="muted">\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((preset.affects || []).join(' / '))+'</div></div>').join('');}function updateTopLevelModelSuggestionLists(){ const markup=knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join(''); [topLevelTriggerIntentSuggestions,topLevelSmartRouterSuggestions,topLevelGovernanceSummarizerSuggestions,topLevelGovernanceClassifierSuggestions,topLevelGovernanceVerifierSuggestions].forEach(node=>{ if(node){ node.innerHTML=markup; } });}function renderModelsForm(models){ const list=Array.isArray(models) ? models : []; draftModelsCount.value=String(list.length); if(!list.length){ modelsFormGrid.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div>'; return; } modelsFormGrid.innerHTML=list.map((model,index)=>{ const templateContext=getProviderTemplateContext(model); const template=templateContext.template; return '<div class="model-card" data-model-card="'+index+'">' + '<div class="model-card-header"><strong>Model #'+(index+1)+'</strong><button type="button" data-remove-model="'+index+'">\u5220\u9664</button></div>' + '<div class="model-card-grid">' + '<div><label>Provider template</label><div class="row"><select data-field="provider_template" data-index="'+index+'"><option value="">custom</option>'+Object.entries(modelProviderTemplates).map(([key,item])=>'<option value="'+esc(key)+'"'+(model.provider_template === key ? ' selected' : '')+'>'+esc(item.label)+'</option>').join('')+'</select><button type="button" data-apply-template="'+index+'">\u5E94\u7528</button></div></div>' + '<div><label>ID</label><input data-field="id" data-index="'+index+'" value="'+esc(model.id || '')+'" placeholder="'+esc(template.suggested_id || 'sonnet')+'"><div class="muted">Router.default \u548C\u8DEF\u7531\u89C4\u5219\u5F15\u7528\u8FD9\u4E2A model id\uFF1B\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 class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 interface\uFF1B\u65E7 protocol \u4F1A\u81EA\u52A8\u8BFB\u53D6\u4E3A\u517C\u5BB9\u503C\u3002</div></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">\u4E0A\u6E38\u771F\u5B9E\u6A21\u578B\u540D\uFF0C\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 class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 api\uFF1B\u65E7 api_base_url \u4EC5\u7528\u4E8E\u517C\u5BB9\u8BFB\u53D6\u3002</div></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 class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 key\uFF1B\u65E7 api_key \u4EC5\u7528\u4E8E\u517C\u5BB9\u8BFB\u53D6\u3002</div></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>Context window</label><input data-field="context_window_tokens" data-index="'+index+'" value="'+esc(model.metadata?.context_window_tokens || '')+'" placeholder="200000"></div>' + '<div><label>Safe input</label><input data-field="safe_input_tokens" data-index="'+index+'" value="'+esc(model.metadata?.safe_input_tokens || '')+'" placeholder="180000"></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 contextWindowTokens=(read('context_window_tokens')?.value || '').trim(); const safeInputTokens=(read('safe_input_tokens')?.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(contextWindowTokens){ metadata.context_window_tokens=Number(contextWindowTokens); } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'context_window_tokens')){ delete metadata.context_window_tokens; } if(safeInputTokens){ metadata.safe_input_tokens=Number(safeInputTokens); } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'safe_input_tokens')){ delete metadata.safe_input_tokens; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); const smartCandidates=extractSmartCandidatesFromForm(); const smartRouterEnabled=Boolean(smartEnabled.checked || triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim() || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim()); if(smartRouterEnabled){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: true, analysis_scope: triggerAnalysisScope.value || payload.SmartRouter?.analysis_scope || 'last_message', router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined, rules: triggerRules, semantic:(governanceSemanticEnabled.checked || triggerIntentEnabled.checked || governanceClassifierModel.value.trim() || triggerIntentModel.value.trim()) ? { ...(((payload.SmartRouter || {}).semantic) || {}), enabled:Boolean(governanceSemanticEnabled.checked || triggerIntentEnabled.checked), mode:'classifier', classifier_model: governanceClassifierModel.value.trim() || triggerIntentModel.value.trim() } : undefined, sticky:(governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim()) ? { ...(((payload.SmartRouter || {}).sticky) || {}), enabled:true, alignment:{ ...((((payload.SmartRouter || {}).sticky || {}).alignment) || {}), enabled:Boolean(governanceAlignmentEnabled.checked), summarizer_model: governanceSummarizerModel.value.trim() } } : undefined }; } else { delete payload.SmartRouter; } delete payload.TriggerRouter; const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const smart=getDraftSmartRouterConfig(config); const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(smart.enabled); triggerIntentEnabled.checked=Boolean(smart.semantic?.enabled && smart.semantic?.mode === 'classifier'); triggerAnalysisScope.value=smart.analysis_scope || 'last_message'; triggerIntentModel.value=smart.semantic?.classifier_model || trigger.intent_model || ''; renderTriggerRulesList(smart.rules || trigger.rules || []); smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(smart.sticky?.alignment?.enabled); governanceSummarizerModel.value=smart.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(smart.semantic?.enabled); governanceClassifierModel.value=smart.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); 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([],[]); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyContextWindowAction(action,modelId){ if(action!=='set-long-context' || !modelId){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u8BE5\u4E0A\u4E0B\u6587\u7A97\u53E3\u64CD\u4F5C'; return; } const payload=buildDraftPayloadFromForm(); payload.Router={ ...(payload.Router || {}), longContext:modelId }; currentDraftConfig=payload; renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06 Router.longContext \u8BBE\u7F6E\u4E3A '+modelId+'\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(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); 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 getRouterSlotDefinitions(){ return [ { key:'default', label:'Default', when:'\u666E\u901A\u8BF7\u6C42\u3001\u89C4\u5219\u672A\u547D\u4E2D\u6216\u5176\u4ED6\u69FD\u4F4D\u672A\u914D\u7F6E\u65F6\u4F7F\u7528', required:true }, { key:'think', label:'Thinking', when:'\u8BF7\u6C42\u5305\u542B thinking \u65F6\u4F18\u5148\u4F7F\u7528', required:false }, { key:'longContext', label:'Long context', when:'\u8F93\u5165\u8D85\u8FC7\u9608\u503C\uFF0C\u6216\u5F53\u524D\u6A21\u578B safe_input_tokens \u4E0D\u591F\u65F6\u4F7F\u7528', required:false }, { key:'background', label:'Background', when:'Claude Code \u8F7B\u91CF\u540E\u53F0\u6A21\u578B\u8BF7\u6C42\u65F6\u4F7F\u7528', required:false }, { key:'webSearch', label:'Web search', when:'\u8BF7\u6C42\u5305\u542B web_search \u5DE5\u5177\u65F6\u4F7F\u7528', required:false }, ];}function renderRouterSlotExplanation(data){ const config=data?.normalizedConfig || { Router:(currentDraftConfig.Router && Object.keys(currentDraftConfig.Router).length ? currentDraftConfig.Router : (data?.router || {})) }; const router=config.Router || {}; const modelMap=data?.modelMap || {}; const slots=getRouterSlotDefinitions(); let configured=0; let resolved=0; let warnings=0; const defaultRef=String(router.default || '').trim(); const defaultModel=defaultRef ? modelMap[defaultRef] : null; const rows=slots.map(slot=>{ const ref=String(router[slot.key] || '').trim(); const model=ref ? modelMap[ref] : null; const caps=model?.capabilities || {}; const slotWarnings=[]; if(ref){ configured+=1; } if(ref && model){ resolved+=1; } if(slot.required && !ref){ slotWarnings.push('\u5FC5\u586B\u69FD\u4F4D\u672A\u914D\u7F6E'); } if(ref && !model){ slotWarnings.push('\u5F15\u7528\u672A\u89E3\u6790\u5230 Models[].id'); } if(slot.key==='think' && model && caps.thinking?.supported === false){ slotWarnings.push('\u76EE\u6807\u6A21\u578B\u58F0\u660E\u4E0D\u652F\u6301 reasoning'); } if(slot.key==='longContext' && model){ if(!caps.contextWindowTokens){ slotWarnings.push('\u7F3A\u5C11 context_window_tokens'); } if(!caps.safeInputTokens){ slotWarnings.push('\u7F3A\u5C11 safe_input_tokens'); } if(defaultModel?.capabilities?.contextWindowTokens && caps.contextWindowTokens && caps.contextWindowTokens <= defaultModel.capabilities.contextWindowTokens){ slotWarnings.push('\u7A97\u53E3\u4E0D\u9AD8\u4E8E default'); } } if(model && slot.key!=='longContext' && (!caps.contextWindowTokens || !caps.safeInputTokens)){ slotWarnings.push('\u7F3A\u5C11\u4E0A\u4E0B\u6587\u7A97\u53E3\u5143\u6570\u636E'); } warnings+=slotWarnings.length; const target=model ? ('<code>'+esc(model.providerName || '-')+'</code><div class="muted">'+esc(model.modelName || '-')+'</div>') : '<span class="muted">-</span>'; const capabilityParts=model ? [ 'thinking '+(caps.thinking?.supported === false ? 'off' : 'on'), 'tools '+(caps.tools === false ? 'off' : 'on'), 'images '+(caps.images === false ? 'off' : 'on'), caps.contextWindowTokens ? ('ctx '+caps.contextWindowTokens) : 'ctx ?', caps.safeInputTokens ? ('safe '+caps.safeInputTokens) : 'safe ?', ] : []; const warningText=slotWarnings.length ? slotWarnings.join('\uFF1B') : (ref ? 'ok' : '\u672A\u914D\u7F6E\u65F6\u56DE\u5230 default'); const warningClass=slotWarnings.length ? 'warn' : 'info'; return '<tr>' + '<td><strong>'+esc(slot.label)+'</strong><div class="muted">Router.'+esc(slot.key)+'</div></td>' + '<td>'+esc(slot.when)+'</td>' + '<td>'+(ref ? '<code>'+esc(ref)+'</code>' : '<span class="muted">not configured</span>')+'</td>' + '<td>'+target+'</td>' + '<td>'+(capabilityParts.length ? capabilityParts.map(item=>'<span class="pill">'+esc(item)+'</span>').join(' ') : '<span class="muted">-</span>')+'</td>' + '<td><span class="pill '+warningClass+'">'+esc(warningText)+'</span></td>' + '</tr>'; }); routerSlotSummary.innerHTML=[['Configured slots',configured],['Resolved slots',resolved],['Warnings',warnings]].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); routerSlotTableBody.innerHTML=rows.join('');}function readModelMetadataNumber(model,key){ const value=model?.metadata?.[key]; return Number.isFinite(Number(value)) && Number(value)>0 ? Number(value) : undefined;}function getContextWindowEntries(data,config){ const modelMap=data?.modelMap || {}; const draftModels=Array.isArray(config?.Models) ? config.Models : []; if(draftModels.length){ return draftModels.map(model=>{ const id=String(model?.id || '').trim(); const compiled=id ? modelMap[id] : null; const caps=compiled?.capabilities || {}; return { id, modelName:model?.model || compiled?.modelName || '-', contextWindowTokens:readModelMetadataNumber(model,'context_window_tokens') || caps.contextWindowTokens, safeInputTokens:readModelMetadataNumber(model,'safe_input_tokens') || caps.safeInputTokens }; }).filter(item=>item.id); } return Object.entries(modelMap).map(([id,model])=>({ id, modelName:model?.modelName || '-', contextWindowTokens:model?.capabilities?.contextWindowTokens, safeInputTokens:model?.capabilities?.safeInputTokens }));}function renderContextWindowGuide(data){ const config=data?.normalizedConfig || currentDraftConfig || {}; const router=config.Router || {}; const entries=getContextWindowEntries(data,config); if(!entries.length){ contextWindowGuide.innerHTML='<div class="alert info"><strong>Context window guide</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u8FD8\u6CA1\u6709\u53EF\u89E3\u6790\u7684 Models\u3002</div></div>'; return; } const defaultRef=String(router.default || '').trim(); const longRef=String(router.longContext || '').trim(); const defaultEntry=entries.find(item=>item.id===defaultRef); const longEntry=entries.find(item=>item.id===longRef); const ranked=entries.filter(item=>item.contextWindowTokens).sort((a,b)=>(b.contextWindowTokens || 0)-(a.contextWindowTokens || 0)); const best=ranked[0]; const missingCount=entries.filter(item=>!item.contextWindowTokens || !item.safeInputTokens).length; const messages=[]; let level='info'; if(missingCount){ level='warn'; messages.push('\u6709 '+missingCount+' \u4E2A\u6A21\u578B\u7F3A\u5C11 context_window_tokens \u6216 safe_input_tokens\uFF0C\u8D85\u5927\u8BF7\u6C42\u53EF\u80FD\u65E0\u6CD5\u63D0\u524D\u964D\u7EA7/\u5207\u6362\u3002'); } if(entries.length>1 && !longRef){ level='warn'; messages.push('\u591A\u6A21\u578B\u914D\u7F6E\u672A\u8BBE\u7F6E Router.longContext\uFF0C\u5927\u4E0A\u4E0B\u6587\u8BF7\u6C42\u4F1A\u7EE7\u7EED\u4F7F\u7528\u5DF2\u9009\u6A21\u578B\u3002'); } if(longRef && !longEntry){ level='warn'; messages.push('Router.longContext \u5F15\u7528\u672A\u89E3\u6790\u5230 Models[].id\u3002'); } if(longEntry && (!longEntry.contextWindowTokens || !longEntry.safeInputTokens)){ level='warn'; messages.push('Router.longContext \u7F3A\u5C11\u4E0A\u4E0B\u6587\u7A97\u53E3\u6216\u5B89\u5168\u8F93\u5165\u5143\u6570\u636E\u3002'); } if(defaultEntry?.contextWindowTokens && longEntry?.contextWindowTokens && longEntry.contextWindowTokens <= defaultEntry.contextWindowTokens){ level='warn'; messages.push('Router.longContext \u7684\u7A97\u53E3\u4E0D\u9AD8\u4E8E Router.default\uFF0C\u53EF\u80FD\u65E0\u6CD5\u63D0\u5347\u5927\u4E0A\u4E0B\u6587\u4F53\u9A8C\u3002'); } if(!messages.length){ messages.push('\u5F53\u524D\u4E0A\u4E0B\u6587\u7A97\u53E3\u5143\u6570\u636E\u548C Router.longContext \u914D\u7F6E\u53EF\u7528\u4E8E\u5927\u4E0A\u4E0B\u6587 fallback\u3002'); } const canApplyBest=best?.id && best.id!==longRef && (!defaultEntry?.contextWindowTokens || (best.contextWindowTokens || 0)>defaultEntry.contextWindowTokens); const summaryRows=[['Default', defaultRef || '-'],['Default ctx', defaultEntry?.contextWindowTokens || '?'],['Long context', longRef || '-'],['Long ctx', longEntry?.contextWindowTokens || '?'],['Largest ctx', best ? (best.id+' / '+best.contextWindowTokens) : '-'],['Missing metadata', missingCount]]; contextWindowGuide.innerHTML='<div class="alert '+level+'"><div class="row"><strong>Context window guide</strong>'+(best ? '<span class="pill">largest '+esc(best.id)+'</span>' : '')+'</div><div class="diff-summary">'+summaryRows.map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('')+'</div><ul>'+messages.map(message=>'<li>'+esc(message)+'</li>').join('')+'</ul>'+(canApplyBest ? '<div class="row" style="margin-top:.5rem"><button type="button" data-context-action="set-long-context" data-model-id="'+esc(best.id)+'">\u8BBE\u4E3A Router.longContext</button><span class="muted">'+esc(best.modelName || '')+'</span></div>' : '')+'</div>';}function getSmartCandidateGuideEntries(data,config){ const modelMap=data?.modelMap || {}; const draftModels=Array.isArray(config?.Models) ? config.Models : []; if(draftModels.length){ return draftModels.map(model=>{ const id=String(model?.id || '').trim(); const compiled=id ? modelMap[id] : null; const caps=compiled?.capabilities || {}; return { id, modelName:model?.model || compiled?.modelName || '-', contextWindowTokens:readModelMetadataNumber(model,'context_window_tokens') || caps.contextWindowTokens || 0, thinkingSupported:caps.thinking?.supported !== false }; }).filter(item=>item.id); } return Object.entries(modelMap).map(([id,model])=>({ id, modelName:model?.modelName || '-', contextWindowTokens:model?.capabilities?.contextWindowTokens || 0, thinkingSupported:model?.capabilities?.thinking?.supported !== false }));}function pickSmartCandidate(entries,role){ const list=[...entries]; const score=(item)=>{ const text=(item.id+' '+item.modelName).toLowerCase(); let value=0; if(role==='fast'){ if(/haiku|mini|flash|fast|lite|small/.test(text)){ value+=80; } value+=Math.max(0,50-Math.log10((item.contextWindowTokens || 1))*10); } else if(role==='deep'){ if(/opus|reasoner|thinking|o1|o3|gpt-5|sonnet/.test(text)){ value+=80; } if(item.thinkingSupported){ value+=20; } value+=Math.log10((item.contextWindowTokens || 1))*5; } else if(role==='long_context'){ value+=item.contextWindowTokens || 0; } else { if(/sonnet|gpt-4|gpt-5|default|balanced/.test(text)){ value+=80; } value+=Math.log10((item.contextWindowTokens || 1))*8; } return value; }; return list.sort((a,b)=>score(b)-score(a) || a.id.localeCompare(b.id))[0];}function renderSmartCandidateGuide(data,summary){ const config=data?.normalizedConfig || currentDraftConfig || {}; const entries=getSmartCandidateGuideEntries(data,config); const candidates=Array.isArray(summary?.candidates) ? summary.candidates : []; const configured=new Set(candidates.map(candidate=>candidate.model?.ref).filter(Boolean)); if(!entries.length){ smartCandidateGuide.innerHTML='<div class="alert info"><strong>Candidate guide</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u8FD8\u6CA1\u6709\u53EF\u89E3\u6790\u7684 Models\uFF0C\u5148\u6DFB\u52A0\u6A21\u578B\u540E\u518D\u914D\u7F6E\u5019\u9009\u3002</div></div>'; return; } const roles=[ { key:'fast', label:'Fast', description:'\u9AD8\u9891\u8F7B\u91CF\u4EFB\u52A1\u3001\u540E\u53F0\u8BF7\u6C42\u548C\u4F4E\u5EF6\u8FDF\u5019\u9009' }, { key:'balanced', label:'Balanced', description:'\u9ED8\u8BA4\u65E5\u5E38\u7F16\u7801\u3001\u89E3\u91CA\u548C\u4E2D\u7B49\u590D\u6742\u5EA6\u4EFB\u52A1' }, { key:'deep', label:'Deep', description:'\u590D\u6742\u63A8\u7406\u3001\u67B6\u6784\u8BBE\u8BA1\u548C\u9700\u8981\u66F4\u5F3A\u6A21\u578B\u7684\u4EFB\u52A1' }, { key:'long_context', label:'Long context', description:'\u5927\u4E0A\u4E0B\u6587\u3001\u957F\u6587\u4EF6\u548C\u8D85\u957F\u4F1A\u8BDD\u515C\u5E95\u5019\u9009' }, ]; const rows=roles.map(role=>{ const picked=pickSmartCandidate(entries,role.key); const configuredRole=picked && configured.has(picked.id); const cls=configuredRole ? 'info' : 'warn'; const button=(!configuredRole && picked) ? '<button type="button" data-add-smart-candidate-suggestion="'+esc(picked.id)+'" data-description="'+esc(role.key+' candidate')+'">Add candidate</button>' : ''; return '<div class="alert '+cls+'"><div class="row"><strong>'+esc(role.label)+'</strong><span class="pill '+cls+'">'+esc(configuredRole ? 'configured' : 'suggested')+'</span></div><div>'+esc(role.description)+'</div><div class="muted">'+(picked ? ('<code>'+esc(picked.id)+'</code> \xB7 '+esc(picked.modelName || '-')+' \xB7 ctx '+esc(picked.contextWindowTokens || '?')) : 'no model suggestion')+'</div>'+(button ? '<div class="row" style="margin-top:.5rem">'+button+'</div>' : '')+'</div>'; }); smartCandidateGuide.innerHTML='<div class="alert info"><strong>Candidate guide</strong><div class="muted">\u5EFA\u8BAE\u81F3\u5C11\u8986\u76D6 fast / balanced / deep\uFF0C\u9700\u8981\u5927\u4E0A\u4E0B\u6587\u65F6\u518D\u52A0\u5165 long-context \u5019\u9009\u3002</div></div>'+rows.join('');}function renderSmartRouterExplanation(data){ const summary=data?.smartRouterExplanation || {}; const rules=Array.isArray(summary.rules) ? summary.rules : []; const candidates=Array.isArray(summary.candidates) ? summary.candidates : []; const warnings=Array.isArray(summary.warnings) ? summary.warnings : []; const refLabel=(model)=>model?.ref ? ('<code>'+esc(model.ref)+'</code><div class="muted">'+esc(model.target?.providerName || model.status || '-')+' / '+esc(model.target?.modelName || '-')+'</div>') : '<span class="muted">-</span>'; const switchRows=[['Enabled', summary.enabled ? 'true' : 'false'],['Rules', rules.length],['Candidates', candidates.length],['Router model', summary.routerModel?.ref || '-'],['Semantic', (summary.semantic?.enabled ? 'on' : 'off')+' / '+(summary.semantic?.mode || '-')],['Sticky', summary.sticky?.enabled ? 'on' : 'off'],['Alignment', summary.sticky?.alignment?.enabled ? 'on' : 'off'],['Fallback', summary.fallback || 'default'],['Warnings', warnings.length]]; smartRouterExplanationSummary.innerHTML=switchRows.map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const order=Array.isArray(summary.routeOrder) ? summary.routeOrder : []; smartRouterRouteOrder.innerHTML='<div class="alert '+(warnings.length ? 'warn' : 'info')+'"><strong>Route order</strong><ol>'+order.map(item=>'<li>'+esc(item)+'</li>').join('')+'</ol>'+(warnings.length ? '<ul>'+warnings.map(item=>'<li>'+esc(item)+'</li>').join('')+'</ul>' : '<div class="muted">SmartRouter \u914D\u7F6E\u5F15\u7528\u5DF2\u89E3\u6790\u3002</div>')+'</div>'; smartRouterRulesTableBody.innerHTML=rules.length ? rules.map(rule=>{ const patternText=(rule.patterns || []).map(pattern=>pattern.type==='exact' ? ('exact: '+(pattern.keywords || []).join(', ')) : ('regex: '+(pattern.pattern || '-'))).join('; '); const statusClass=rule.model?.status === 'resolved' ? 'info' : (rule.model?.status === 'legacy' ? 'warn' : 'critical'); return '<tr>' + '<td>'+esc(rule.order || '-')+'<div class="muted">priority '+esc(rule.priority ?? 0)+'</div></td>' + '<td><strong>'+esc(rule.name || '-')+'</strong><div class="muted">'+esc(rule.description || '-')+'</div><span class="pill '+(rule.enabled ? 'info' : 'warn')+'">'+esc(rule.enabled ? 'enabled' : 'disabled')+'</span></td>' + '<td>'+refLabel(rule.model)+'<span class="pill '+statusClass+'">'+esc(rule.model?.status || '-')+'</span></td>' + '<td>'+esc(patternText || '-')+'</td>' + '<td>'+esc(rule.semantic?.enabled ? 'on' : 'off')+'<div class="muted">'+esc(rule.semantic?.prototype || '-')+'</div></td>' + '</tr>'; }).join('') : '<tr><td colspan="5" class="muted">No SmartRouter rules configured</td></tr>'; smartRouterCandidatesTableBody.innerHTML=candidates.length ? candidates.map(candidate=>{ const statusClass=candidate.model?.status === 'resolved' ? 'info' : (candidate.model?.status === 'legacy' ? 'warn' : 'critical'); return '<tr><td>'+esc(candidate.order || '-')+'</td><td>'+refLabel(candidate.model)+'</td><td>'+esc(candidate.description || '-')+'</td><td><span class="pill '+statusClass+'">'+esc(candidate.model?.status || '-')+'</span></td></tr>'; }).join('') : '<tr><td colspan="4" class="muted">No SmartRouter candidates configured</td></tr>'; renderSmartCandidateGuide(data,summary);}function renderCompiledModels(data){ lastCompiledModelsData=data || null; const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); const modelPoolEntries=Object.entries(data.modelPools || {}); const modelPoolEndpointCount=modelPoolEntries.reduce((sum,[_modelId,pool])=>sum+((pool.endpoints || []).length),0); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); renderRouterSlotExplanation(data); renderContextWindowGuide(data); renderSmartRouterExplanation(data); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04 / '+modelPoolEntries.length+' \u4E2A model pool / '+modelPoolEndpointCount+' \u4E2A pool endpoint'; 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>'; compiledModelPoolsTableBody.innerHTML=modelPoolEntries.length ? modelPoolEntries.map(([modelId,pool])=>{ const endpoints=pool.endpoints || []; return '<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td>'+esc(pool.strategy || '-')+'</td>' + '<td><code>'+esc(pool.activeEndpointId || '-')+'</code></td>' + '<td>'+endpoints.map(endpoint=>{ const latency=endpoint.health?.latency; return '<div><code>'+esc(endpoint.id)+'</code><span class="muted"> priority '+esc(endpoint.priority)+' / '+esc(endpoint.enabled ? 'enabled' : 'disabled')+' / '+esc(endpoint.health?.status || 'healthy')+(latency ? ' / avg '+esc(Math.round(latency.averageMs))+'ms' : '')+'</span><div class="muted">'+esc(endpoint.upstreamServiceId || endpoint.api || '-')+'</div></div>'; }).join('')+'</td>' + '<td>'+((pool.warnings || []).length ? pool.warnings.map(w=>'<div class="warning-text">'+esc(w)+'</div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>'; }).join('') : '<tr><td colspan="5" class="muted">No compiled model pools</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); updateStatusSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(currentDraftConfig)); renderContextWindowGuide(withDraftCompiledData(currentDraftConfig)); 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(); renderContextWindowGuide(lastCompiledModelsData); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderContextWindowGuide(withDraftCompiledData(payload)); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function loadServiceStatus(){ serviceReadyStatus.textContent='checking'; try { const [serviceRes,remoteRes]=await Promise.all([fetch('/api/service-info'),fetch('/api/remote-status')]); const data=await serviceRes.json(); const remoteData=await remoteRes.json(); serviceReadyStatus.textContent=data.ready ? 'ready' : 'not ready'; servicePortStatus.textContent=data.port || '-'; serviceModeStatus.textContent=data.runtimeMode || '-'; serviceRoleStatus.textContent=data.serviceRole || '-'; renderRoleConnectionGuide(data); const auth=data.auth || {}; const managed=auth.managedKeys || {}; const quota=auth.quota || {}; const quotaText=Number.isFinite(quota.requestsUsed) ? (' \xB7 quota '+quota.requestsUsed+' req'+(quota.windowResetAt ? ' \xB7 reset '+String(quota.windowResetAt).replace('T',' ').replace('.000Z','Z') : '')) : ''; authStatusSummary.textContent=auth.required ? ((auth.bootstrapConfigured ? 'bootstrap' : 'managed')+' \xB7 '+(managed.active ?? 0)+' active'+quotaText) : 'not configured'; renderAuthQuotaTable(quota); const security=data.security || {}; const issues=Array.isArray(security.issues) ? security.issues : []; securityStatusSummary.textContent=security.status || '-'; securitySummary.className='alert '+((security.status === 'critical') ? 'critical' : (security.status === 'warning' ? 'warn' : 'info')); securitySummary.innerHTML='<strong>Security: '+esc(security.status || '-')+'</strong><div>'+esc(issues[0]?.message || '\u5F53\u524D\u670D\u52A1\u672A\u53D1\u73B0\u660E\u663E\u9274\u6743\u66B4\u9732\u98CE\u9669')+'</div>'+ (issues.length ? '<ul class="mini-list">'+issues.map(issue=>'<li>'+esc(issue.action || issue.code)+'</li>').join('')+'</ul>' : ''); const registration=data.registration || {}; registrationStatusSummary.textContent=registration.enabled ? ((registration.models ?? 0)+' models / '+(registration.upstreamServices ?? 0)+' upstream') : 'disabled'; const remote=remoteData.remote || {}; remoteStatusSummary.textContent=remote.enabled ? ((remote.ready ? 'ready' : (remote.reachable ? 'reachable' : 'unreachable'))+' \xB7 '+(remote.baseUrl || '-')) : 'disabled'; const remoteRegistration=remoteData.remoteRegistration || {}; const remoteRegistrationSummary=remoteRegistration.summary || {}; remoteRegistrationStatusSummary.textContent=remoteRegistration.enabled ? (remoteRegistration.available ? (remoteRegistration.registrationEnabled ? ((remoteRegistrationSummary.models ?? 0)+' remote models / '+(remoteRegistrationSummary.upstreamServices ?? 0)+' upstream') : 'remote registration disabled') : ('unavailable \xB7 '+(remoteRegistration.error || remoteRegistration.baseUrl || '-'))) : 'disabled'; if(remoteData.compiledModels){ modelCountStatus.textContent=remoteData.compiledModels.modelCount ?? modelCountStatus.textContent; } try { await loadModelPoolHealth(); } catch (_poolError) { modelPoolHealthSummary.className='alert warn'; modelPoolHealthSummary.innerHTML='<strong>Pool health unavailable</strong><div class="muted">\u65E0\u6CD5\u52A0\u8F7D\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001</div>'; } } catch (_error) { serviceReadyStatus.textContent='unreachable'; remoteStatusSummary.textContent='unknown'; securityStatusSummary.textContent='unknown'; modelPoolHealthSummary.className='alert warn'; modelPoolHealthSummary.innerHTML='<strong>Pool health unavailable</strong><div class="muted">\u65E0\u6CD5\u52A0\u8F7D\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001</div>'; }}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderContextWindowGuide(lastCompiledModelsData); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || [], data.issueReport); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addSmartCandidateSuggestion(modelId,description){ const id=String(modelId || '').trim(); if(!id){ return; } const next=extractSmartCandidatesFromForm(); if(!next.some(item=>item.model===id)){ next.push({ model:id, description:description || 'guided candidate' }); } renderSmartCandidatesList(next); syncDraftEditorFromForm(); renderSmartCandidateGuide(withDraftCompiledData(currentDraftConfig), { candidates: next.map((item,index)=>({ order:index+1, description:item.description, model:{ ref:item.model, status:'resolved' } })) }); }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(); });smartCandidateGuide.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-add-smart-candidate-suggestion]'); if(!btn){ return; } addSmartCandidateSuggestion(btn.dataset.addSmartCandidateSuggestion, btn.dataset.description); });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); });contextWindowGuide.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-context-action]'); if(!btn){ return; } applyContextWindowAction(btn.dataset.contextAction, btn.dataset.modelId); });healthSummary.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-health-action]'); if(btn){ applyHealthAction(btn.dataset.healthAction); } });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });surfaceTabs.forEach((tab)=>tab.addEventListener('click',()=>setActiveSurface(tab.dataset.surfaceTarget || 'user')));setActiveSurface('user');function renderMetrics(metrics,health,outcome){ metricsGrid.innerHTML=[ ['Health', health?.status || 'idle'], ['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)], ['Model switch rate', pct(outcome?.modelSwitchRate)], ['Alignment on switch', pct(outcome?.alignmentOnSwitchRate)], ['Context fallback', pct(outcome?.contextWindowFallbackRate)], ['Context exceeded', pct(outcome?.contextWindowExceededRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.SmartRouter?.enabled), analysis_scope: payload.SmartRouter?.analysis_scope || 'last_message', router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: payload.SmartRouter?.candidates || [], cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.SmartRouter?.rules || []) }; delete payload.TriggerRouter; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: payload.SmartRouter?.rules || [] }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: payload.SmartRouter?.enabled !== undefined ? Boolean(payload.SmartRouter?.enabled) : Boolean(preset.governanceEnabled), sticky:{ ...((payload.SmartRouter && payload.SmartRouter.sticky) || {}), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.enabled), alignment:{ ...(((payload.SmartRouter && payload.SmartRouter.sticky && payload.SmartRouter.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.SmartRouter?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.SmartRouter && payload.SmartRouter.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.SmartRouter?.semantic?.enabled), mode:(payload.SmartRouter?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.SmartRouter?.semantic?.classifier_model || '') } }; payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderOutcomeGroups(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 class="muted"> \xB7 '+esc(item.totalTraces)+' traces</span></span><strong>switch '+esc(pct(item.modelSwitchRate))+' \xB7 align '+esc(pct(item.alignmentOnSwitchRate))+' \xB7 cascade '+esc(pct(item.cascadeAfterSwitchRate))+' \xB7 '+esc(fmt(item.averageLatencyMs))+' ms</strong></li>').join('');}function renderRoutingTuning(items){ if(!items || !items.length){ routingTuningList.innerHTML='<li><span class="muted">No routing tuning recommendations</span><strong>healthy</strong></li>'; return; } routingTuningList.innerHTML=items.map(item=>{ const suggestions=Array.isArray(item.configSuggestions) ? item.configSuggestions : []; const suggestionHtml=suggestions.length ? '<div class="muted">config: '+suggestions.map(s=>'<code>'+esc(s.path || '-')+'</code>'+(s.suggestedValue !== undefined ? ' = '+esc(s.suggestedValue) : '')+' \u2014 '+esc(s.reason || '')).join('<br>')+'</div>' : ''; return '<li><span><span class="pill '+esc(item.severity === 'critical' ? 'critical' : (item.severity === 'warn' ? 'warn' : 'info'))+'">'+esc(item.severity || 'info')+'</span> <strong>'+esc(item.code || '-')+'</strong><div class="muted">'+esc(item.message || '')+'</div><div class="muted">'+esc(item.evidence || '')+'</div>'+suggestionHtml+'</span><strong>'+esc(item.action || '')+'</strong></li>'; }).join('');}function renderQualityEvidence(summary){ const items=summary?.samples || []; qualityEvidenceSummary.innerHTML=[['Samples',summary?.totalSamples || 0],['Risk',summary?.failureSamples || 0],['Improvement',summary?.improvementSamples || 0],['Speed risk',summary?.speedRiskSamples || 0]].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); if(!items.length){ qualityEvidenceList.innerHTML='<li><span class="muted">No quality evidence samples</span><strong>0</strong></li>'; return; } qualityEvidenceList.innerHTML=items.map(item=>'<li><span><span class="pill '+esc(item.severity === 'critical' ? 'critical' : (item.severity === 'warn' ? 'warn' : 'info'))+'">'+esc(item.severity || 'info')+'</span> <strong>'+esc(item.type || '-')+'</strong><div class="muted">'+esc(item.requestId || '')+' \xB7 '+esc((item.routeReason || []).join(' / '))+'</div><div class="muted">'+esc(item.evidence || '')+'</div></span><strong>'+esc(item.action || '')+'</strong></li>').join('');}function renderTaskComparison(summary){ const items=summary?.comparisons || []; taskComparisonSummary.innerHTML=[['Tasks',summary?.totalComparedTasks || 0],['Traces',summary?.totalComparedTraces || 0]].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); if(!items.length){ taskComparisonList.innerHTML='<li><span class="muted">No comparable task samples</span><strong>0</strong></li>'; return; } taskComparisonList.innerHTML=items.map(item=>'<li><span><strong>'+esc(item.taskKey || '-')+'</strong><div class="muted">best '+esc(item.bestModel || '-')+' \xB7 baseline '+esc(item.baselineModel || '-')+' \xB7 fastest '+esc(item.fastestModel || '-')+'</div><div class="muted">failure lift '+esc(pct(item.failureRateDelta || 0))+' \xB7 latency lift '+esc(fmt(item.latencyDeltaMs || 0))+' ms \xB7 models '+esc(item.modelCount || 0)+'</div></span><strong>'+esc(item.totalTraces || 0)+' traces</strong></li>').join('');}function renderBenchmarkSummary(taskComparison,qualityEvidence){ const bestQuality=taskComparison?.bestQualityLiftTask; const bestSpeed=taskComparison?.bestSpeedLiftTask; benchmarkSummary.innerHTML=[ ['Comparable tasks',taskComparison?.totalComparedTasks || 0], ['Evidence samples',qualityEvidence?.totalSamples || 0], ['Best quality lift',bestQuality ? pct(bestQuality.failureRateDelta || 0) : '-'], ['Best speed lift',bestSpeed ? (fmt(bestSpeed.latencyDeltaMs || 0)+' ms') : '-'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const actions=[]; if((taskComparison?.totalComparedTasks || 0)===0){ actions.push(['Collect comparable traces','Send the same task class through at least two final models, then refresh metrics.']); } if((qualityEvidence?.totalSamples || 0)===0){ actions.push(['Collect quality evidence','Enable cascade, shadow, context-window or model-pool signals so routing wins and risks become visible.']); } actions.push(['Run fixed benchmark','ctr eval --tasks && ctr eval --run --models "sonnet;haiku" --json']); actions.push(['Add calibration','Attach humanScore or judgeScore to ctr eval input results before treating rubric scores as release evidence.']); benchmarkActionList.innerHTML=actions.map(([title,detail])=>'<li><span><strong>'+esc(title)+'</strong><div class="muted">'+esc(detail)+'</div></span><strong>benchmark</strong></li>').join('');}function signed(v,digits){ const n=Number(v || 0); return (n>0?'+':'')+n.toFixed(digits ?? 2); }function renderBenchmarkHistory(data){ const summary=data?.summary || {}; const latest=summary.latest || null; const trends=summary.trends || {}; const alignment=data?.traceAlignment || {}; const traceTasks=alignment.taskComparison?.totalComparedTasks || 0; benchmarkHistoryFile.textContent=data?.historyFile || 'benchmark-history.json'; benchmarkHistorySummary.innerHTML=[ ['Entries',summary.totalEntries || 0], ['Quality',latest ? fmt(latest.averageQualityScore) : '-'], ['Trace tasks',traceTasks], ['Latency',latest ? (fmt(latest.averageLatencyMs)+' ms') : '-'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); if(!latest){ benchmarkHistoryList.innerHTML='<li><span class="muted">No saved benchmark history</span><strong>0</strong></li>'; return; } const entries=Array.isArray(summary.entries) ? summary.entries.slice(-5).reverse() : []; const rows=entries.map(entry=>'<li><span><strong>'+esc(entry.label || entry.id || '-')+'</strong><div class="muted">'+esc(entry.createdAt || '-')+' \xB7 '+esc(entry.source || '-')+' \xB7 runs '+esc(entry.evaluatedRuns || 0)+'/'+esc(entry.totalRuns || entry.evaluatedRuns || 0)+'</div><div class="muted">pass '+esc(pct(entry.passRate || 0))+' \xB7 quality '+esc(fmt(entry.averageQualityScore || 0))+' \xB7 calibration '+esc(fmt(entry.averageCalibrationScore || 0))+'</div></span><strong>'+esc(fmt(entry.averageLatencyMs || 0))+' ms</strong></li>'); const modelRows=(summary.topModels || []).map(model=>'<li><span><strong>'+esc(model.model || '-')+'</strong><div class="muted">quality '+esc(fmt(model.averageQualityScore || 0))+' \xB7 pass '+esc(pct(model.passRate || 0))+' \xB7 latency '+esc(fmt(model.averageLatencyMs || 0))+' ms</div></span><strong>top</strong></li>'); const alignmentRows=[ '<li><span><strong>Trace alignment</strong><div class="muted">task comparison '+esc(traceTasks)+' \xB7 quality evidence '+esc(alignment.qualityEvidence?.totalSamples || 0)+' \xB7 quality trend '+esc(summary.previous ? signed(trends.qualityDelta || 0,2) : 'waiting')+'</div></span><strong>trace</strong></li>' ]; benchmarkHistoryList.innerHTML=[...rows,...modelRows,...alignmentRows].join('') || '<li><span class="muted">No saved benchmark history</span><strong>0</strong></li>';}async function loadBenchmarkHistory(){ const res=await fetch('/api/benchmark/history'); renderBenchmarkHistory(await res.json());}async function saveBenchmarkCalibration(){ benchmarkCalibrationStatus.textContent='\u4FDD\u5B58\u6821\u51C6\u4E2D...'; const payload={ taskId:document.getElementById('calibrationTaskId').value.trim(), model:document.getElementById('calibrationModel').value.trim(), humanScore:document.getElementById('calibrationHumanScore').value.trim() ? Number(document.getElementById('calibrationHumanScore').value.trim()) : undefined, latencyMs:document.getElementById('calibrationLatencyMs').value.trim() ? Number(document.getElementById('calibrationLatencyMs').value.trim()) : undefined, output:document.getElementById('calibrationOutput').value, calibrationNotes:document.getElementById('calibrationNotes').value, label:'ui-calibration' }; const res=await fetch('/api/benchmark/calibration',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ benchmarkCalibrationStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } benchmarkCalibrationStatus.textContent='\u5DF2\u4FDD\u5B58\u6821\u51C6\uFF1A'+(data.entry?.id || 'benchmark history'); renderBenchmarkHistory({ historyFile:data.historyFile, summary:data.summary });}function renderRouteDecisionSummaries(items){ const decisions=Array.isArray(items) ? items.slice(0,5) : []; if(!decisions.length){ routeDecisionSummaryList.innerHTML='<li><span class="muted">No recent route decisions</span><strong>0</strong></li>'; return; } routeDecisionSummaryList.innerHTML=decisions.map(item=>{ const meta=[item.sourceLabel || item.source || '-', item.ruleName ? ('rule '+item.ruleName) : '', item.semanticIntent ? ('intent '+item.semanticIntent) : '', item.confidenceLabel || '', item.latencyMs !== undefined ? (fmt(item.latencyMs)+' ms') : ''].filter(Boolean).join(' \xB7 '); const fallback=item.fallbackReason ? '<div class="muted">fallback: '+esc(item.fallbackReason)+'</div>' : ''; return '<li><span><strong>'+esc(item.headline || item.requestId || '-')+'</strong><div class="muted">'+esc(meta)+'</div>'+fallback+'</span><button type="button" data-request="'+esc(item.requestId || '')+'">View</button></li>'; }).join('');}function renderSwitchContinuitySummaries(items){ const summaries=Array.isArray(items) ? items.slice(0,5) : []; if(!summaries.length){ switchContinuitySummaryList.innerHTML='<li><span class="muted">No recent switch continuity</span><strong>0</strong></li>'; return; } switchContinuitySummaryList.innerHTML=summaries.map(item=>{ const cls=item.status === 'critical' ? 'critical' : (item.status === 'watch' ? 'warn' : 'info'); const meta=[item.transition || (item.finalModel || '-'), item.sourceLabel || item.source || '-', item.alignmentUsed ? 'aligned' : '', item.cascadeTriggered ? 'cascade' : '', item.latencyMs !== undefined ? (fmt(item.latencyMs)+' ms') : ''].filter(Boolean).join(' \xB7 '); const action=item.action ? '<div class="muted">'+esc(item.action)+'</div>' : ''; return '<li><span><span class="pill '+esc(cls)+'">'+esc(item.status || 'unknown')+'</span> <strong>'+esc(item.headline || item.requestId || '-')+'</strong><div class="muted">'+esc(meta)+'</div>'+action+'</span><button type="button" data-request="'+esc(item.requestId || '')+'">View</button></li>'; }).join('');}function renderRouteHandoffSummaries(items){ const handoffs=Array.isArray(items) ? items.slice(0,5) : []; if(!handoffs.length){ routeHandoffSummaryList.innerHTML='<li><span class="muted">No recent route handoff</span><strong>0</strong></li>'; return; } routeHandoffSummaryList.innerHTML=handoffs.map(item=>{ const stages=Array.isArray(item.stages) ? item.stages.map(stage=>stage.stage+':'+stage.status).join(' \xB7 ') : ''; const status=item.blocked ? 'watch' : 'ok'; return '<li><span><strong>'+esc(item.headline || item.requestId || '-')+'</strong><div class="muted">'+esc(stages || item.action || '-')+'</div></span><button type="button" data-request="'+esc(item.requestId || '')+'">'+esc(status)+'</button></li>'; }).join('');}function renderAnomalies(anomalies,health){ const status=health?.status || 'idle'; const message=health?.message || 'No governance traces yet.'; const actions=Array.isArray(health?.actions) ? health.actions : []; healthSummary.className='alert '+esc(status === 'critical' ? 'critical' : (status === 'watch' ? 'warn' : 'info')); healthSummary.innerHTML='<strong>Health: '+esc(status)+'</strong><div>'+esc(message)+'</div>'+ (actions.length ? '<ul class="mini-list">'+actions.map(action=>'<li><button type="button" data-health-action="'+esc(action)+'">'+esc(action)+'</button></li>').join('')+'</ul>' : ''); 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 applyHealthAction(action){ const text=String(action || '').toLowerCase(); const routeReasonInput=document.getElementById('routeReason'); const cascadeSelect=document.getElementById('cascadeTriggered'); const shadowSelect=document.getElementById('shadowChecked'); if(text.includes('cascade')){ cascadeSelect.value='true'; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered cascade traces'; } else if(text.includes('shadow')){ shadowSelect.value='true'; cascadeSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered shadow traces'; } else { cascadeSelect.value=''; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: showing recent traces'; } loadTraces(); document.getElementById('traceTable').scrollIntoView({ behavior:'smooth', block:'start' });}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes,healthRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query), fetch('/api/governance/health'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); const healthData=await healthRes.json(); const health=healthData.health || metricsData.health; renderMetrics(metricsData.metrics || {},health,metricsData.outcome || {}); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || [],health); renderRoutingTuning(health?.routingTuning || []); renderQualityEvidence(metricsData.qualityEvidence || {}); renderTaskComparison(metricsData.taskComparison || {}); renderBenchmarkSummary(metricsData.taskComparison || {},metricsData.qualityEvidence || {}); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderOutcomeGroups(routeOutcomeRanking,metricsData.outcome?.byRouteReason || [],'No route outcomes'); renderOutcomeGroups(modelOutcomeRanking,metricsData.outcome?.byFinalModel || [],'No model outcomes'); renderOutcomeGroups(intentOutcomeRanking,metricsData.outcome?.bySemanticIntent || [],'No intent outcomes'); renderTrendTable(metricsData || {}); const traces=data.traces || []; renderRouteDecisionSummaries(data.routeDecisions || traces.map(t=>t.decisionSummary).filter(Boolean)); renderSwitchContinuitySummaries(data.switchContinuity || traces.map(t=>t.switchSummary).filter(Boolean)); renderRouteHandoffSummaries(data.routeHandoffs || traces.map(t=>t.handoffSummary ? { requestId:t.requestId, sessionKey:t.sessionKey, ...t.handoffSummary } : null).filter(Boolean)); if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftHeroBtn').addEventListener('click',loadConfigDraft);document.getElementById('previewConfigDraftHeroBtn').addEventListener('click',previewConfigDraft);document.getElementById('refreshStatusHeroBtn').addEventListener('click',loadServiceStatus);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);document.getElementById('saveCalibrationBtn').addEventListener('click',saveBenchmarkCalibration);document.getElementById('probeModelPoolBtn').addEventListener('click',probeModelPoolHealth);routeDecisionSummaryList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn && btn.dataset.request){ loadDetail(btn.dataset.request); } });switchContinuitySummaryList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn && btn.dataset.request){ loadDetail(btn.dataset.request); } });routeHandoffSummaryList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn && btn.dataset.request){ loadDetail(btn.dataset.request); } });tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadServiceStatus();loadConfigDraft();loadCompiledModels();loadBenchmarkHistory();loadExports();loadArchives();loadTraces();</script>` + renderWorkbenchDocumentEnd();
6164
+ return renderWorkbenchDocumentStart() + `body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.hero{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(260px,.8fr);gap:1rem;align-items:stretch;margin-bottom:1rem}.hero h2{margin:.2rem 0 .5rem;font-size:1.55rem}.hero-copy{display:flex;flex-direction:column;justify-content:center}.status-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.status-tile{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem;min-width:0}.status-tile strong{display:block;margin-top:.2rem;word-break:break-word}@media (max-width:760px){.hero{grid-template-columns:1fr}.status-grid{grid-template-columns:1fr}}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:.75rem 1rem;flex-wrap:wrap;align-items:flex-start;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}.scope-guide{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:.75rem;margin-top:.75rem}.scope-guide div{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem}.scope-guide strong{display:block;margin-bottom:.35rem}.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}.pill.info{background:#eff6ff;color:#1d4ed8}.pill.warn{background:#fff7ed;color:#9a3412}.pill.critical{background:#fef2f2;color:#991b1b}.surface-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin:1rem 0}.surface-tab{background:#fff;color:#1f2328;border-color:#d1d5db}.surface-tab.active{background:#111827;color:#fff;border-color:#111827}.surface-panel[hidden]{display:none}.surface-heading{display:flex;gap:1rem;flex-wrap:wrap;align-items:center;margin-bottom:.75rem}</style></head><body><div class="hero"><div class="panel hero-copy"><h2>\u914D\u7F6E\u4E0E\u72B6\u6001\u5DE5\u4F5C\u53F0</h2><p class="muted">\u67E5\u770B\u5F53\u524D\u8DEF\u7531\u670D\u52A1\u3001\u6A21\u578B\u914D\u7F6E\u548C\u9ED8\u8BA4\u53BB\u5411\uFF1B\u9700\u8981\u6392\u67E5\u65F6\uFF0C\u4E0B\u65B9\u7EF4\u62A4\u8005\u533A\u57DF\u53EF\u7EE7\u7EED\u67E5\u770B Governance Trace\u3001metrics \u548C\u5F52\u6863\u3002</p><div class="action-row"><button id="loadConfigDraftHeroBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="previewConfigDraftHeroBtn" type="button">\u9884\u89C8 compiled models</button><button id="refreshStatusHeroBtn" type="button">\u5237\u65B0\u72B6\u6001</button></div></div><div class="panel"><div class="status-grid"><div class="status-tile"><span class="muted">Service</span><strong id="serviceReadyStatus">ready</strong></div><div class="status-tile"><span class="muted">Port</span><strong id="servicePortStatus">${escapedDisplayPort}</strong></div><div class="status-tile"><span class="muted">Mode</span><strong id="serviceModeStatus">${escapedRuntimeMode}</strong></div><div class="status-tile"><span class="muted">Role</span><strong id="serviceRoleStatus">${escapedServiceRole}</strong></div><div class="status-tile"><span class="muted">Listener</span><strong id="listenerStatusSummary">${escapedListenerSummary}</strong></div><div class="status-tile"><span class="muted">Models</span><strong id="modelCountStatus">${escapedModelsCount}</strong></div><div class="status-tile"><span class="muted">Router.default</span><strong id="routerDefaultStatus">${escapedRouterDefault}</strong></div><div class="status-tile"><span class="muted">Remote service</span><strong id="remoteStatusSummary">${escapedRemoteSummary}</strong></div><div class="status-tile"><span class="muted">Remote registration</span><strong id="remoteRegistrationStatusSummary">checking</strong></div><div class="status-tile"><span class="muted">Registration</span><strong id="registrationStatusSummary">${escapedRegistrationSummary}</strong></div><div class="status-tile"><span class="muted">Auth</span><strong id="authStatusSummary">${escapedAuthSummary}</strong></div><div class="status-tile"><span class="muted">Security</span><strong id="securityStatusSummary">${escapedSecuritySummary}</strong></div></div></div></div>` + renderSurfaceTabs() + `<section id="userSurface" class="surface-panel" data-surface="user"><div class="panel"><div class="surface-heading"><strong>\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u914D\u7F6E\u3001\u6A21\u578B\u3001\u8DEF\u7531\u3001\u670D\u52A1\u72B6\u6001\u4E0E\u4E0B\u4E00\u6B65\u4FDD\u5B58\u52A8\u4F5C\u3002</span></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Routing rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="subpanel"><div class="row"><strong>Current Router slots</strong><span class="muted">\u89E3\u91CA\u57FA\u7840\u8DEF\u7531\u69FD\u4F4D\u5F15\u7528\u7684 modelId\u3001\u4E0A\u6E38\u6A21\u578B\u3001\u80FD\u529B\u548C\u6F5C\u5728\u914D\u7F6E\u98CE\u9669</span></div><div id="routerSlotSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Configured slots</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Resolved slots</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Warnings</span><strong>0</strong></div></div><table id="routerSlotTable" class="management-table"><thead><tr><th>Slot</th><th>When used</th><th>Model ref</th><th>Resolved target</th><th>Capabilities</th><th>Warning</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading router slot explanation...</td></tr></tbody></table><div id="contextWindowGuide" class="alert-list" style="margin-top:.75rem"><div class="alert info"><strong>Context window guide</strong><div class="muted">\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u4E0A\u4E0B\u6587\u7A97\u53E3\u4E0E Router.longContext \u5EFA\u8BAE</div></div></div></div><div class="subpanel"><div class="row"><strong>SmartRouter explanation</strong><span class="muted">\u5C55\u793A\u89C4\u5219\u547D\u4E2D\u987A\u5E8F\u3001\u5019\u9009\u6A21\u578B\u3001router_model\u3001semantic/sticky \u5F00\u5173\u4E0E fallback</span></div><div id="smartRouterExplanationSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Enabled</span><strong>-</strong></div><div class="diff-chip"><span class="muted">Rules</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Candidates</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Warnings</span><strong>0</strong></div></div><div id="smartRouterRouteOrder" class="alert-list" style="margin-top:.75rem"><div class="alert info"><strong>Route order</strong><div class="muted">\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A SmartRouter \u51B3\u7B56\u987A\u5E8F\u3002</div></div></div><table id="smartRouterRulesTable" class="management-table"><thead><tr><th>Order</th><th>Rule</th><th>Model</th><th>Patterns</th><th>Semantic</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading SmartRouter rules...</td></tr></tbody></table><table id="smartRouterCandidatesTable" class="management-table"><thead><tr><th>Order</th><th>Candidate</th><th>Description</th><th>Status</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading SmartRouter candidates...</td></tr></tbody></table><div id="smartCandidateGuide" class="alert-list" style="margin-top:.75rem"><div class="alert info"><strong>Candidate guide</strong><div class="muted">\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u63D0\u793A fast / balanced / deep / long-context \u5019\u9009\u8986\u76D6\u3002</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u56F4\u7ED5 SmartRouter \u7EDF\u4E00\u8DEF\u7531\u5F15\u64CE\u7F16\u8F91\u89C4\u5219\u3001\u5019\u9009\u4E0E\u6CBB\u7406\u589E\u5F3A\u517C\u5BB9\u914D\u7F6E</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Routing rules</strong><span class="muted">\u663E\u5F0F\u89C4\u5219\u3001\u8BED\u4E49\u63D0\u793A\u4E0E\u517C\u5BB9\u8F93\u5165</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5F71\u5B50\u6821\u9A8C\u3001\u7EA7\u8054\u4E0E\u89C2\u6D4B\u76F8\u5173\u914D\u7F6E</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div class="alert info"><strong>Models field guide</strong><div class="muted">\u65B0\u914D\u7F6E\u8BF7\u4F7F\u7528\u5165\u53E3\u5B57\u6BB5\uFF1Aid / api / key / interface / model / thinking / metadata\uFF1Bapi_key / api_base_url / protocol \u4EC5\u4F5C\u4E3A\u65E7\u914D\u7F6E\u517C\u5BB9\u8BFB\u53D6\u3002</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" aria-label="JSON config draft" 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","thinking":"auto","metadata":{"vendor_hint":"openrouter"}}],"Router":{"default":"sonnet"}}'></textarea><div class="muted">JSON \u8349\u7A3F\u540C\u6837\u5EFA\u8BAE\u53EA\u5199\u5165\u53E3\u5B57\u6BB5\uFF1B\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u5F52\u4E00\uFF0C\u65E7\u5B57\u6BB5\u522B\u540D\u65E0\u9700\u624B\u52A8\u8865\u5145\u3002</div><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / SmartRouter / Governance\uFF08shadow/cascade\uFF09\u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Compatibility profile</th><th>Dispatch format</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="8" class="muted">Loading model map...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model pools</strong><span class="muted">Registration.models \u7F16\u8BD1\u51FA\u7684\u540C\u6A21\u578B\u591A\u6E90\u6C60\uFF0C\u5F53\u524D\u652F\u6301 priority / least-latency / round-robin / health-aware / cost-aware active endpoint\u3001\u975E\u6D41\u5F0F\u9519\u8BEF fallback\u3001health/cooldown\u3001\u7194\u65AD\u72B6\u6001\u3001\u5EF6\u8FDF\u7A97\u53E3\u4E0E\u6210\u672C/\u901F\u7387\u5143\u6570\u636E</span></div><table id="compiledModelPoolsTable" class="management-table"><thead><tr><th>Pool</th><th>Strategy</th><th>Active endpoint</th><th>Endpoints</th><th>Warnings</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading model pools...</td></tr></tbody></table></div></div></div></div></section><section id="maintainerSurface" class="surface-panel" data-surface="maintainer" hidden><div class="panel"><div class="surface-heading"><strong>\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u8FD0\u884C\u89C2\u6D4B\u3001Governance Trace\u3001metrics\u3001\u5F52\u6863\u4E0E\u7EF4\u62A4\u64CD\u4F5C\u3002</span></div><div id="securitySummary" class="alert info"><strong>Security pending</strong><div class="muted">\u7B49\u5F85\u670D\u52A1\u5B89\u5168\u72B6\u6001\u52A0\u8F7D</div></div><div class="subpanel" id="roleConnectionGuide"><div class="row"><strong>Role & connection guide</strong><span class="muted">\u6309\u5F53\u524D local / server / cloud \u89D2\u8272\u786E\u8BA4\u76D1\u542C\u5730\u5740\u3001\u7EF4\u62A4\u5165\u53E3\u548C\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u63A5\u5165\u65B9\u5F0F\u3002</span></div><div class="scope-guide"><div><strong>current role</strong><span id="roleConnectionSummary" class="muted">${escapedRuntimeMode} / ${escapedServiceRole}</span></div><div><strong>listener</strong><span id="listenerConnectionSummary" class="muted">${escapedListenerSummary}</span></div><div><strong>remote clients</strong><span id="clientConnectionSummary" class="muted">${escapedClientConnectionSummary}</span></div></div><div class="muted" style="margin-top:.75rem">${escapedLocalUserRoleGuide}</div><div class="muted" style="margin-top:.5rem">${escapedServerMaintainerRoleGuide}</div><div class="muted" style="margin-top:.5rem">${escapedRemoteClientRoleGuide}</div></div><div class="subpanel" id="authScopeGuide"><div class="row"><strong>Auth scope guide</strong><span class="muted">\u6309\u7528\u9014\u53D1\u653E\u6700\u5C0F\u6743\u9650 key\uFF0C\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u4E0D\u8981\u590D\u7528 admin key\u3002</span></div><div class="scope-guide"><div><strong>admin</strong><span class="muted">\u670D\u52A1\u6240\u6709\u8005\u4F7F\u7528\uFF1A/ui\u3001\u914D\u7F6E\u4FDD\u5B58\u3001auth \u7BA1\u7406\uFF0C\u4EE5\u53CA\u6240\u6709\u8FD0\u7EF4\u5199\u64CD\u4F5C\u3002</span></div><div><strong>operator</strong><span class="muted">\u65E5\u5E38\u8FD0\u7EF4\u4F7F\u7528\uFF1A\u91CD\u542F\u3001\u6CBB\u7406\u5FEB\u7167\u3001\u5B9A\u65F6\u5FEB\u7167\u3001\u5F02\u5E38\u9608\u503C\u548C\u5F52\u6863\u5220\u9664\uFF1B\u4E0D\u80FD\u67E5\u770B\u914D\u7F6E\u6216\u7BA1\u7406 auth\u3002</span></div><div><strong>client</strong><span class="muted">\u5BA2\u6237\u7AEF\u6A21\u578B\u8C03\u7528\uFF1A/v1/messages\u3001/v1/chat/completions\uFF1B\u6A21\u578B\u8C03\u7528\u914D\u989D\u53EA\u8BA1\u5165\u8FD9\u91CC\u3002</span></div><div><strong>read-only</strong><span class="muted">\u53EA\u8BFB\u89C2\u6D4B\uFF1Ahealth\u3001service-info\u3001compiled models\u3001model pool health\u3001transformers \u548C governance GET\u3002</span></div><div><strong>client + read-only</strong><span class="muted">\u8FDC\u7A0B token \u540C\u65F6\u9700\u8981 ready/status \u63A2\u6D4B\u4E0E\u6A21\u578B\u8C03\u7528\u65F6\u4F7F\u7528\u8BE5\u7EC4\u5408\u3002</span></div></div><div class="muted" style="margin-top:.75rem">\u7BA1\u7406\u5165\u53E3\uFF1A\u7528 admin key \u8C03\u7528 <code>GET /api/auth/keys</code> \u67E5\u770B\u5217\u8868\uFF0C<code>POST /api/auth/keys</code> \u751F\u6210 key\uFF0C<code>POST /api/auth/keys/:id/rotate</code> \u8F6E\u6362 key\uFF0C<code>POST /api/auth/keys/:id/revoke</code> \u540A\u9500 key\uFF1B\u751F\u6210\u7684 secret \u53EA\u8FD4\u56DE\u4E00\u6B21\uFF0C\u8BF7\u76F4\u63A5\u4EA4\u7ED9\u5BF9\u5E94\u5BA2\u6237\u7AEF\u4FDD\u5B58\u3002</div><div class="muted" style="margin-top:.5rem">\u5982\u679C\u670D\u52A1\u542F\u7528\u4E86\u9274\u6743\uFF0C\u6D4F\u89C8\u5668\u76F4\u63A5\u6253\u5F00 <code>/ui</code> \u4E0D\u80FD\u81EA\u52A8\u643A\u5E26 <code>Authorization</code> header\uFF1B\u7EF4\u62A4\u8005\u5E94\u901A\u8FC7\u5185\u7F51\u3001\u672C\u5730\u96A7\u9053\u6216\u53CD\u5411\u4EE3\u7406\u6CE8\u5165 <code>Authorization: Bearer &lt;admin-key&gt;</code> \u540E\u8BBF\u95EE\uFF0C\u4E0D\u8981\u628A admin key \u653E\u8FDB URL\u3002</div></div><div class="subpanel"><div class="row"><strong>Auth quota</strong><span class="muted">\u6309 managed key \u67E5\u770B\u6A21\u578B\u8C03\u7528\u914D\u989D\u3001\u5F53\u524D\u7528\u91CF\u4E0E\u7A97\u53E3\u91CD\u7F6E\u65F6\u95F4</span></div><table id="authQuotaTable" class="management-table"><thead><tr><th>Key</th><th>Scope</th><th>Status</th><th>Requests</th><th>Tokens</th><th>Window</th></tr></thead><tbody><tr><td colspan="6" class="muted">Waiting for service status...</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Model pool health</strong><span class="muted">\u67E5\u770B\u540C\u6A21\u578B\u591A\u6E90\u6C60\u7684 active endpoint\u3001\u6301\u4E45\u5316\u72B6\u6001\u3001cooldown\u3001\u7194\u65AD\u4E0E\u5EF6\u8FDF\u7A97\u53E3\u3002</span><button id="probeModelPoolBtn" type="button">\u4E3B\u52A8\u63A2\u6D4B</button></div><div id="modelPoolHealthSummary" class="alert info"><strong>Pool health pending</strong><div class="muted">\u7B49\u5F85\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001\u52A0\u8F7D</div></div><table id="modelPoolHealthTable" class="management-table"><thead><tr><th>Pool</th><th>Endpoint</th><th>Status</th><th>Latency</th><th>Failures</th><th>Last success</th><th>Recovery</th></tr></thead><tbody><tr><td colspan="7" class="muted">Waiting for model pool health...</td></tr></tbody></table></div><div class="row"><strong>\u7EF4\u62A4\u8005\u89C2\u6D4B</strong><span class="muted">\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4 Governance Trace\uFF0C\u5E76\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u6307\u6807\u3002</span></div><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/pool-health</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/health</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code>\u3001<code>/api/benchmark/history</code></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Health</span><strong>-</strong></div><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">Model switch rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment on switch</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="healthSummary" class="alert info"><strong>Health pending</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u5065\u5EB7\u6458\u8981\u52A0\u8F7D</div></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>Routing tuning</strong><span class="muted">\u57FA\u4E8E outcome \u8BC1\u636E\u7ED9\u51FA SmartRouter \u8C03\u4F18\u5EFA\u8BAE</span></div><ul id="routingTuningList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Quality evidence</strong><span class="muted">\u771F\u5B9E trace \u4E2D\u7684\u5931\u8D25\u3001\u8FDE\u7EED\u6027\u548C\u901F\u5EA6\u98CE\u9669\u6837\u672C</span></div><div id="qualityEvidenceSummary" class="stats"><div class="stat"><span class="muted">Samples</span><strong>-</strong></div><div class="stat"><span class="muted">Risk</span><strong>-</strong></div><div class="stat"><span class="muted">Improvement</span><strong>-</strong></div><div class="stat"><span class="muted">Speed risk</span><strong>-</strong></div></div><ul id="qualityEvidenceList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Task comparison</strong><span class="muted">\u540C\u7C7B\u4EFB\u52A1\u4E0B\u4E0D\u540C\u6700\u7EC8\u6A21\u578B\u7684\u5931\u8D25\u7387\u548C\u901F\u5EA6\u5BF9\u6BD4</span></div><div id="taskComparisonSummary" class="stats"><div class="stat"><span class="muted">Tasks</span><strong>-</strong></div><div class="stat"><span class="muted">Traces</span><strong>-</strong></div></div><ul id="taskComparisonList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Benchmark summary</strong><span class="muted">\u628A\u6CBB\u7406 trace \u4E0E\u56FA\u5B9A\u4EFB\u52A1\u8BC4\u6D4B\u5165\u53E3\u5408\u5E76\u6210\u7EF4\u62A4\u8005 A/B \u95ED\u73AF</span></div><div id="benchmarkSummary" class="stats"><div class="stat"><span class="muted">Comparable tasks</span><strong>-</strong></div><div class="stat"><span class="muted">Evidence samples</span><strong>-</strong></div><div class="stat"><span class="muted">Best quality lift</span><strong>-</strong></div><div class="stat"><span class="muted">Best speed lift</span><strong>-</strong></div></div><ul id="benchmarkActionList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Benchmark history</strong><span id="benchmarkHistoryFile" class="muted">\u7B49\u5F85\u5386\u53F2\u52A0\u8F7D</span></div><div id="benchmarkHistorySummary" class="stats"><div class="stat"><span class="muted">Entries</span><strong>-</strong></div><div class="stat"><span class="muted">Quality</span><strong>-</strong></div><div class="stat"><span class="muted">Trace tasks</span><strong>-</strong></div><div class="stat"><span class="muted">Latency</span><strong>-</strong></div></div><ul id="benchmarkHistoryList" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Human calibration</strong><span id="benchmarkCalibrationStatus" class="muted">\u628A\u4EBA\u5DE5\u590D\u6838\u6837\u672C\u8FFD\u52A0\u5230 benchmark history</span></div><div class="control-grid"><div><label>Task ID</label><input id="calibrationTaskId" value="quick_status"></div><div><label>Model</label><input id="calibrationModel" placeholder="sonnet"></div><div><label>Human score</label><input id="calibrationHumanScore" placeholder="0.9"></div><div><label>Latency ms</label><input id="calibrationLatencyMs" placeholder="1200"></div></div><div style="margin-top:.75rem"><label>Output</label><textarea id="calibrationOutput" rows="4" placeholder="Paste the model output to score"></textarea></div><div style="margin-top:.75rem"><label>Calibration notes</label><textarea id="calibrationNotes" rows="2" placeholder="Why did the human score differ from the rubric?"></textarea></div><div class="row" style="margin-top:.75rem"><button id="saveCalibrationBtn" type="button">\u4FDD\u5B58\u6821\u51C6\u6837\u672C</button><span class="muted">\u4FDD\u5B58\u6458\u8981\uFF0C\u4E0D\u6301\u4E45\u5316\u539F\u59CB output</span></div></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${escapedMinSampleSize}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${escapedCascadeWarnRate}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${escapedShadowWarnRate}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${escapedLatencyWarnMs}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by route</strong><span class="muted">\u5207\u6362\u3001alignment\u3001cascade \u4E0E\u5EF6\u8FDF</span></div><ul id="routeOutcomeRanking" 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>Outcome by model</strong><span class="muted">\u6700\u7EC8\u6A21\u578B\u5207\u6362\u4E0E\u5EF6\u8FDF\u8868\u73B0</span></div><ul id="modelOutcomeRanking" 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>Outcome by intent</strong><span class="muted">\u4EFB\u52A1\u610F\u56FE\u5207\u6362\u4E0E\u5EF6\u8FDF\u8868\u73B0</span></div><ul id="intentOutcomeRanking" 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><div class="subpanel"><div class="row"><strong>Recent route decisions</strong><span class="muted">\u628A\u6700\u8FD1\u8BF7\u6C42\u7684 route source\u3001\u89C4\u5219\u3001\u8BED\u4E49\u610F\u56FE\u3001\u7F6E\u4FE1\u5EA6\u548C fallback \u539F\u56E0\u7FFB\u8BD1\u6210\u53EF\u8BFB\u6458\u8981\u3002</span></div><ul id="routeDecisionSummaryList" class="mini-list"><li><span class="muted">Loading route decisions</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Recent switch continuity</strong><span class="muted">\u89E3\u91CA\u6700\u8FD1\u8BF7\u6C42\u662F\u5426\u5207\u6362\u6A21\u578B\u3001\u662F\u5426\u8865\u4E0A\u4E0B\u6587\uFF0C\u4EE5\u53CA\u5207\u6362\u540E\u662F\u5426\u89E6\u53D1 cascade\u3002</span></div><ul id="switchContinuitySummaryList" class="mini-list"><li><span class="muted">Loading switch continuity</span><strong>-</strong></li></ul></div><div class="subpanel"><div class="row"><strong>Route handoff</strong><span class="muted">\u8DDF\u8E2A\u8BF7\u6C42\u4ECE\u8DEF\u7531\u3001agent/tool\u3001\u534F\u8BAE\u5206\u53D1\u5230\u54CD\u5E94\u6CBB\u7406\u7684\u4EA4\u63A5\u94FE\u8DEF\u3002</span></div><ul id="routeHandoffSummaryList" class="mini-list"><li><span class="muted">Loading route handoff</span><strong>-</strong></li></ul></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>GET /api/governance/health</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5065\u5EB7\u6458\u8981</li><li><code>GET /api/auth/audit</code> \u2014 \u67E5\u770B\u9274\u6743\u5BA1\u8BA1\u6458\u8981</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div></section>` + renderWorkbenchScriptStart() + `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 routerSlotSummary=document.getElementById('routerSlotSummary');const routerSlotTableBody=document.querySelector('#routerSlotTable tbody');const contextWindowGuide=document.getElementById('contextWindowGuide');const smartRouterExplanationSummary=document.getElementById('smartRouterExplanationSummary');const smartRouterRouteOrder=document.getElementById('smartRouterRouteOrder');const smartRouterRulesTableBody=document.querySelector('#smartRouterRulesTable tbody');const smartRouterCandidatesTableBody=document.querySelector('#smartRouterCandidatesTable tbody');const smartCandidateGuide=document.getElementById('smartCandidateGuide');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const serviceReadyStatus=document.getElementById('serviceReadyStatus');const servicePortStatus=document.getElementById('servicePortStatus');const serviceModeStatus=document.getElementById('serviceModeStatus');const serviceRoleStatus=document.getElementById('serviceRoleStatus');const listenerStatusSummary=document.getElementById('listenerStatusSummary');const roleConnectionSummary=document.getElementById('roleConnectionSummary');const listenerConnectionSummary=document.getElementById('listenerConnectionSummary');const clientConnectionSummary=document.getElementById('clientConnectionSummary');const remoteStatusSummary=document.getElementById('remoteStatusSummary');const registrationStatusSummary=document.getElementById('registrationStatusSummary');const authStatusSummary=document.getElementById('authStatusSummary');const securityStatusSummary=document.getElementById('securityStatusSummary');const modelCountStatus=document.getElementById('modelCountStatus');const routerDefaultStatus=document.getElementById('routerDefaultStatus');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const compiledModelPoolsTableBody=document.querySelector('#compiledModelPoolsTable 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 routeOutcomeRanking=document.getElementById('routeOutcomeRanking');const modelOutcomeRanking=document.getElementById('modelOutcomeRanking');const intentOutcomeRanking=document.getElementById('intentOutcomeRanking');const healthSummary=document.getElementById('healthSummary');const routingTuningList=document.getElementById('routingTuningList');const routeDecisionSummaryList=document.getElementById('routeDecisionSummaryList');const switchContinuitySummaryList=document.getElementById('switchContinuitySummaryList');const routeHandoffSummaryList=document.getElementById('routeHandoffSummaryList');const qualityEvidenceSummary=document.getElementById('qualityEvidenceSummary');const qualityEvidenceList=document.getElementById('qualityEvidenceList');const taskComparisonSummary=document.getElementById('taskComparisonSummary');const taskComparisonList=document.getElementById('taskComparisonList');const benchmarkSummary=document.getElementById('benchmarkSummary');const benchmarkActionList=document.getElementById('benchmarkActionList');const benchmarkHistoryFile=document.getElementById('benchmarkHistoryFile');const benchmarkHistorySummary=document.getElementById('benchmarkHistorySummary');const benchmarkHistoryList=document.getElementById('benchmarkHistoryList');const benchmarkCalibrationStatus=document.getElementById('benchmarkCalibrationStatus');const securitySummary=document.getElementById('securitySummary');const authQuotaTableBody=document.querySelector('#authQuotaTable tbody');const modelPoolHealthSummary=document.getElementById('modelPoolHealthSummary');const modelPoolHealthTableBody=document.querySelector('#modelPoolHealthTable tbody');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');const surfaceTabs=Array.from(document.querySelectorAll('[data-surface-target]'));const surfacePanels=Array.from(document.querySelectorAll('[data-surface]'));let currentDraftConfig={};let knownModelIds=[];let lastCompiledModelsData=null;let activeValidationHighlight=null;function withDraftCompiledData(payload){ return { ...(lastCompiledModelsData || {}), normalizedConfig: payload || currentDraftConfig || {} }; }const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94\u8DEF\u7531\u89C4\u5219\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u589E\u5F3A\u4E0E\u6821\u9A8C\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','SmartRouter.sticky.alignment','SmartRouter.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function limitText(used,limit){ return Number.isFinite(limit) ? (String(used ?? 0)+' / '+String(limit)) : String(used ?? 0); }function renderAuthQuotaTable(quota){ const keys=Array.isArray(quota?.keys) ? quota.keys : []; if(!keys.length){ authQuotaTableBody.innerHTML='<tr><td colspan="6" class="muted">No managed keys configured</td></tr>'; return; } authQuotaTableBody.innerHTML=keys.map(item=>{ const usage=item.usage || {}; const quotaCfg=item.quota || {}; const keyName=esc(item.label || item.id || '-')+'<div class="muted"><code>'+esc(item.id || '-')+'</code></div>'; const statusClass=item.status === 'exhausted' ? 'critical' : (item.status === 'watch' ? 'warn' : 'info'); const windowText=quotaCfg.window_seconds ? (esc(quotaCfg.window_seconds)+'s'+(usage.windowResetAt ? '<div class="muted">reset '+esc(String(usage.windowResetAt).replace('T',' ').replace('.000Z','Z'))+'</div>' : '<div class="muted">not started</div>')) : '-'; return '<tr><td>'+keyName+'</td><td>'+esc((item.scopes || []).join(', ') || '-')+'</td><td><span class="pill '+statusClass+'">'+esc(item.status || '-')+'</span></td><td>'+esc(limitText(usage.requestsUsed,usage.requestLimit))+'</td><td>'+esc(limitText(usage.tokensUsed,usage.tokenLimit))+'</td><td>'+windowText+'</td></tr>'; }).join('');}function renderModelPoolHealth(data){ const summary=data?.summary || {}; const pools=Array.isArray(data?.pools) ? data.pools : []; const statusClass=summary.open ? 'critical' : (summary.cooldown ? 'warn' : 'info'); const averageLatency=Number.isFinite(summary.averageLatencyMs) ? (Number(summary.averageLatencyMs).toFixed(0)+' ms avg') : 'no latency samples'; modelPoolHealthSummary.className='alert '+statusClass; modelPoolHealthSummary.innerHTML='<strong>Pool health: '+esc(summary.healthy || 0)+' healthy / '+esc(summary.cooldown || 0)+' cooldown / '+esc(summary.open || 0)+' open</strong><div class="muted">'+esc(summary.pools || 0)+' pools \xB7 '+esc(summary.endpoints || 0)+' endpoints \xB7 '+esc(averageLatency)+' \xB7 persisted endpoints '+esc(data?.persistedState?.endpoints || 0)+'</div>'; const rows=[]; pools.forEach(pool=>{ (pool.endpoints || []).forEach(endpoint=>{ const recovery=endpoint.circuitOpenUntil ? ('circuit opens until '+new Date(endpoint.circuitOpenUntil).toISOString()) : endpoint.cooldownUntil ? ('cooldown until '+new Date(endpoint.cooldownUntil).toISOString()) : '-'; const latency=endpoint.latency ? (Number(endpoint.latency.averageMs || 0).toFixed(0)+' ms avg / '+esc(endpoint.latency.sampleCount || 0)+' samples') : '-'; const cost=endpoint.cost ? ('cost '+esc(endpoint.cost.inputPer1MTokens ?? '-')+'/'+esc(endpoint.cost.outputPer1MTokens ?? '-')+' '+esc(endpoint.cost.currency || 'USD')+'/1M') : 'cost -'; const rate=endpoint.rateLimit ? ('rate '+esc(endpoint.rateLimit.requestsPerMinute ?? '-')+' rpm / '+esc(endpoint.rateLimit.tokensPerMinute ?? '-')+' tpm') : 'rate -'; const endpointLabel='<code>'+esc(endpoint.id || '-')+'</code>'+(endpoint.active ? ' <span class="pill info">active</span>' : '')+'<div class="muted">'+esc(endpoint.providerName || '-')+' / '+esc(endpoint.upstreamServiceId || endpoint.upstreamBaseUrl || 'local')+'</div><div class="muted">'+cost+' \xB7 '+rate+'</div>'; const statusCls=endpoint.status === 'open' ? 'critical' : (endpoint.status === 'cooldown' ? 'warn' : 'info'); rows.push('<tr><td><code>'+esc(pool.modelId || '-')+'</code><div class="muted">'+esc(pool.strategy || '-')+'</div></td><td>'+endpointLabel+'</td><td><span class="pill '+statusCls+'">'+esc(endpoint.status || '-')+'</span></td><td>'+esc(latency)+'</td><td>'+esc(endpoint.failureCount || 0)+'<div class="muted">success '+esc(endpoint.successCount || 0)+'</div></td><td>'+esc(endpoint.lastSuccessAt ? new Date(endpoint.lastSuccessAt).toISOString() : '-')+'</td><td>'+esc(recovery)+'</td></tr>'); }); }); modelPoolHealthTableBody.innerHTML=rows.length ? rows.join('') : '<tr><td colspan="7" class="muted">No registration model pools configured</td></tr>';}async function loadModelPoolHealth(){ const res=await fetch('/api/models/pool-health'); const data=await res.json(); renderModelPoolHealth(data);}async function probeModelPoolHealth(){ modelPoolHealthSummary.className='alert info'; modelPoolHealthSummary.innerHTML='<strong>Probing pool health...</strong><div class="muted">\u6B63\u5728\u8F7B\u91CF\u63A2\u6D4B enabled endpoint</div>'; const res=await fetch('/api/models/pool-health/probe',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({}) }); const data=await res.json(); renderModelPoolHealth(data.health || data);}function renderRoleConnectionGuide(data){ const listener=data.listener || {}; const connection=data.clientConnection || {}; const mode=data.runtimeMode || '-'; const role=data.serviceRole || '-'; const listenerText=listener.host ? (listener.host+':'+(listener.port || '-')+(listener.public ? ' (public)' : ' (local)')) : '-'; const connectionText=connection.baseUrl ? (connection.baseUrl+' \xB7 '+(Array.isArray(connection.recommendedScopes) ? connection.recommendedScopes.join(' + ') : '')) : (connection.guidance || '-'); listenerStatusSummary.textContent=listenerText; roleConnectionSummary.textContent=mode+' / '+role; listenerConnectionSummary.textContent=listenerText; clientConnectionSummary.textContent=connectionText || '-';}function setActiveSurface(surfaceName){ surfacePanels.forEach((panel)=>{ panel.hidden=panel.dataset.surface !== surfaceName; }); surfaceTabs.forEach((tab)=>{ const active=tab.dataset.surfaceTarget === surfaceName; tab.classList.toggle('active',active); tab.setAttribute('aria-selected', active ? 'true' : 'false'); });}function inferProviderTemplateKey(model){ const explicit=String(model?.provider_template || '').trim(); if(explicit && modelProviderTemplates[explicit]){ return explicit; } const api=String(model?.api || model?.api_base_url || '').trim().toLowerCase(); const modelInterface=String(model?.interface || model?.protocol || '').trim().toLowerCase(); const exactMatch=Object.entries(modelProviderTemplates).find(([,item])=>String(item.api || '').trim().toLowerCase()===api && String(item.interface || '').trim().toLowerCase()===modelInterface); if(exactMatch){ return exactMatch[0]; } if(api.includes('api.anthropic.com/v1/messages') || modelInterface === 'anthropic'){ return 'anthropic'; } if(api.includes('openrouter.ai')){ return 'openrouter'; } if(api.includes('deepseek.com')){ return 'deepseek'; } if(api.includes('siliconflow.cn')){ return 'siliconflow'; } if(api.includes('api.openai.com')){ return 'openai-compatible'; } return '';}function getProviderTemplateContext(model){ const templateKey=inferProviderTemplateKey(model) || defaultProviderTemplateKey; return { templateKey, template:modelProviderTemplates[templateKey] || modelProviderTemplates[defaultProviderTemplateKey] || {} };}function createDraftModelFromTemplate(templateKey){ const resolvedKey=(templateKey && modelProviderTemplates[templateKey]) ? templateKey : defaultProviderTemplateKey; const template=modelProviderTemplates[resolvedKey] || {}; return { provider_template:resolvedKey, id:template.suggested_id || '', api:template.api || '', interface:template.interface || 'openai', model:template.default_model || '', thinking:template.default_thinking || 'auto' };}function getModelIdSuggestionsMarkup(idPrefix){ return '<datalist id="'+idPrefix+'">'+knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join('')+'</datalist>';}function resolvePresetModelId(seed){ const source=String(seed || '').trim().toLowerCase(); if(!source || !knownModelIds.length){ return seed; } if(knownModelIds.includes(seed)){ return seed; } const ranked=knownModelIds.map((modelId)=>{ const target=String(modelId || '').toLowerCase(); let score=0; if(target===source){ score+=100; } if(target.includes(source) || source.includes(target)){ score+=40; } source.split(/[^a-z0-9]+/).filter(Boolean).forEach((part)=>{ if(target.includes(part)){ score+=Math.min(part.length * 4, 24); } }); return { modelId, score }; }).filter((item)=>item.score>0).sort((a,b)=>b.score-a.score || a.modelId.localeCompare(b.modelId)); return ranked.length ? ranked[0].modelId : seed;}function getTriggerPatternValidationHint(pattern){ if((pattern?.type || 'exact') === 'regex'){ return pattern?.pattern ? { level:'ok', message:'regex pattern \u5DF2\u914D\u7F6E' } : { level:'warn', message:'regex \u6A21\u5F0F\u9700\u8981\u586B\u5199 pattern' }; } return Array.isArray(pattern?.keywords) && pattern.keywords.some((keyword)=>String(keyword || '').trim()) ? { level:'ok', message:'exact keywords \u5DF2\u914D\u7F6E' } : { level:'warn', message:'exact \u6A21\u5F0F\u81F3\u5C11\u9700\u8981\u4E00\u4E2A keyword' };}function getDraftSmartRouterConfig(config){ const smart={ ...((config && config.SmartRouter) || {}) }; const smartExplicit=config && Object.prototype.hasOwnProperty.call(config,'SmartRouter'); const legacyIntentEnabled=Boolean(config?.TriggerRouter?.llm_intent_recognition); const legacyIntentModel=config?.TriggerRouter?.intent_model || ''; if(!smart.analysis_scope && config?.TriggerRouter?.analysis_scope){ smart.analysis_scope=config.TriggerRouter.analysis_scope; } if((!Array.isArray(smart.rules) || !smart.rules.length) && Array.isArray(config?.TriggerRouter?.rules)){ smart.rules=config.TriggerRouter.rules; } if(!smart.semantic && (config?.Governance?.semantic || config?.TriggerRouter?.llm_intent_recognition)){ smart.semantic={ ...((config && config.Governance && config.Governance.semantic) || {}) }; if(config?.TriggerRouter?.llm_intent_recognition){ smart.semantic.enabled=true; smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || config.TriggerRouter.intent_model || ''; } } if(!smart.sticky && config?.Governance?.sticky){ smart.sticky={ ...(config.Governance.sticky || {}) }; } if(!smartExplicit && !smart.enabled && (config?.TriggerRouter?.enabled || smart.rules?.length || smart.router_model || smart.candidates?.length || smart.semantic || smart.sticky)){ smart.enabled=true; } if(smart.enabled){ smart.analysis_scope=smart.analysis_scope || 'last_message'; smart.semantic={ ...(smart.semantic || {}) }; smart.semantic.enabled=smart.semantic.enabled !== undefined ? smart.semantic.enabled : true; smart.semantic.threshold=smart.semantic.threshold !== undefined ? smart.semantic.threshold : 0.2; if(legacyIntentEnabled){ smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || legacyIntentModel; } smart.sticky={ ...(smart.sticky || {}) }; smart.sticky.enabled=smart.sticky.enabled !== undefined ? smart.sticky.enabled : true; smart.sticky.alignment={ ...((smart.sticky && smart.sticky.alignment) || {}) }; smart.sticky.alignment.enabled=smart.sticky.alignment.enabled !== undefined ? smart.sticky.alignment.enabled : true; smart.sticky.alignment.summarizer_model=smart.sticky.alignment.summarizer_model || smart.router_model || config?.Router?.default || legacyIntentModel || ''; } return smart;}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const smart=getDraftSmartRouterConfig(config); const triggerRules=Array.isArray(smart?.rules) ? smart.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(smart?.candidates) ? smart.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, smart?.router_model, smart?.sticky?.alignment?.summarizer_model, smart?.semantic?.classifier_model, config?.Governance?.shadow?.verifier_model].filter(v=>typeof v === 'string' && v.trim()).length + triggerRules.filter(rule=>rule?.model).length + smartCandidates.filter(item=>item?.model).length + cascadeLevels.reduce((sum,level)=>sum + (level?.from ? 1 : 0) + (level?.to ? 1 : 0), 0); draftSummaryGrid.innerHTML=[ ['Models', models.length], ['Routing rules', triggerRules.length], ['Patterns', patternCount], ['Smart candidates', smartCandidates.length], ['Cascade levels', cascadeLevels.length], ['Model refs', modelRefCount] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function updateStatusSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; modelCountStatus.textContent=String(models.length); routerDefaultStatus.textContent=config?.Router?.default || '-';}function renderDraftValidation(errors,warnings,issueReport){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; const contractIssues=Array.isArray(issueReport?.issues) ? issueReport.issues : []; if(!errorList.length && !warningList.length && !contractIssues.length){ draftValidationList.innerHTML='<div class="alert info"><strong>No validation issues</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u672A\u53D1\u73B0\u96C6\u4E2D\u5C55\u793A\u7684\u95EE\u9898</div></div>'; return; } const extractPath=(text)=>{ const match=String(text).match(/^(Models(?:\\[[0-9]+\\])?(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Router(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|TriggerRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|SmartRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Governance(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?)/); return match ? match[1] : ''; }; const sourceItems=contractIssues.length ? contractIssues.map(item=>({ text:String(item.message || ''), severity:item.severity==='error' ? 'error' : 'warning', path:item.path || '', action:item.action || '' })) : [...errorList.map(item=>({ text:String(item), severity:'error', path:'', action:'' })), ...warningList.map(item=>({ text:String(item), severity:'warning', path:'', action:'' }))]; const grouped=sourceItems.reduce((acc,item)=>{ const text=item.text; const path=item.path || extractPath(text); const bucket=path.startsWith('Models') || text.startsWith('Models') ? 'Models' : path.startsWith('Router') || text.startsWith('Router') ? 'Router' : path.startsWith('TriggerRouter') || text.startsWith('TriggerRouter') ? 'SmartRouter' : path.startsWith('SmartRouter') || text.startsWith('SmartRouter') ? 'SmartRouter' : (path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic') || text.startsWith('Governance.sticky') || text.startsWith('Governance.semantic')) ? 'SmartRouter' : path.startsWith('Governance') || text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path, severity:item.severity, action:item.action || '' }); return acc; }, {}); const errorCount=contractIssues.length ? contractIssues.filter(item=>item.severity==='error').length : errorList.length; const warningCount=contractIssues.length ? contractIssues.filter(item=>item.severity!=='error').length : warningList.length; const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorCount)+' errors / '+esc(warningCount)+' warnings</span></div><div class="muted">'+(errorCount ? '\u8BF7\u4F18\u5148\u4FEE\u590D errors\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u63A5\u53D7 warnings\u3002' : '\u5F53\u524D\u65E0\u963B\u65AD\u9519\u8BEF\uFF0C\u53EF\u6309\u9700\u5904\u7406 warnings\u3002')+'</div></div>'; draftValidationList.innerHTML=summary + Object.entries(grouped).map(([bucket,items])=>{ const hasError=items.some(item=>item.severity==='error'); const levelClass=hasError ? 'warn' : 'info'; const actionLabel=hasError ? 'repair first' : 'review before save'; return '<div class="alert '+levelClass+'"><div class="row"><strong>'+esc(bucket)+'</strong><span class="pill">'+esc(items.length)+' issues</span></div><div class="muted">'+esc(actionLabel)+'</div><div>'+items.slice(0,4).map(item=>'<div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+'<span class="pill">'+esc(item.severity==='error' ? 'error' : 'warning')+'</span> '+esc(item.text)+(item.action ? ('<div class="muted">Action: '+esc(item.action)+'</div>') : '')+'</div>').join('')+'</div></div>'; }).join('');}function getCapabilityWarningActionLabel(code){ if(code==='thinking_ignored'){ return '\u79FB\u9664 thinking'; } if(code==='tools_text_fallback' || code==='images_text_fallback'){ return '\u6062\u590D\u9ED8\u8BA4 capability'; } return '';}function renderCapabilityWarnings(report){ const entries=Array.isArray(report?.entries) ? report.entries : []; if(!entries.length){ capabilityWarningsList.innerHTML='<div class="alert info"><strong>No capability warnings</strong><div class="muted">\u5F53\u524D compiled models \u672A\u53D1\u73B0\u9700\u8981\u989D\u5916\u63D0\u793A\u7684\u80FD\u529B\u964D\u7EA7</div></div>'; return; } const summary=report?.summary || {}; capabilityWarningsList.innerHTML='<div class="alert info"><strong>Capability warning summary</strong><div class="muted">warn '+esc(summary.warn ?? 0)+' / info '+esc(summary.info ?? 0)+' / total '+esc(summary.total ?? entries.length)+'</div></div>' + entries.map(item=>{ const actionLabel=getCapabilityWarningActionLabel(item.code); return '<div class="alert '+esc(item.level === 'warn' ? 'warn' : 'info')+'"><div class="row"><strong>'+esc(item.code || item.level || 'warning')+'</strong><span class="pill">'+esc(item.modelId || '-').trim()+'</span></div><div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+esc(item.message || '')+'</div>'+(actionLabel ? ('<div class="row" style="margin-top:.5rem"><button type="button" data-apply-warning-path="'+esc(item.path || '')+'" data-apply-warning-code="'+esc(item.code || '')+'">'+esc(actionLabel)+'</button></div>') : '')+'</div>'; }).join('');}function findValidationTarget(path){ if(!path){ return null; } if(path.startsWith('Models')){ return modelsFormGrid; } if(path === 'Router.default'){ return draftRouterDefault; } if(path.startsWith('TriggerRouter.intent_model')){ return triggerIntentModel; } if(path.startsWith('TriggerRouter.rules[')){ return triggerRulesList; } if(path.startsWith('SmartRouter.router_model')){ return smartRouterModel; } if(path.startsWith('SmartRouter.candidates[')){ return smartCandidatesList; } if(path.startsWith('Governance.cascade.levels[')){ return governanceCascadeLevelsList; } if(path.startsWith('Governance.sticky.alignment')){ return governanceSummarizerModel; } if(path.startsWith('Governance.semantic')){ return governanceClassifierModel; } if(path.startsWith('Governance.shadow')){ return governanceVerifierModel; } if(path.startsWith('Governance')){ return governanceEnabled; } return null;}function jumpToValidationPath(path){ const target=findValidationTarget(path); if(!target || typeof target.scrollIntoView !== 'function'){ return; } if(activeValidationHighlight && activeValidationHighlight.classList){ activeValidationHighlight.classList.remove('jump-highlight'); } target.scrollIntoView({ behavior:'smooth', block:'center' }); if(target.classList){ target.classList.add('jump-highlight'); activeValidationHighlight=target; setTimeout(()=>{ if(target.classList){ target.classList.remove('jump-highlight'); if(activeValidationHighlight===target){ activeValidationHighlight=null; } } }, 1800); } if(typeof target.focus === 'function'){ target.focus({ preventScroll:true }); }}function renderDraftPresetModeHint(){ const overwriteMode=draftPresetMode.value === 'replace'; draftPresetModeHint.textContent=overwriteMode ? 'overwrite \u4F1A\u91CD\u7F6E SmartRouter / Governance \u76F8\u5173\u8868\u5355\uFF0C\u518D\u5E94\u7528\u9884\u8BBE' : 'append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145 SmartRouter / Governance \u76F8\u5173\u5B57\u6BB5';}function deriveActualAffectedAreas(preview){ const areas=new Set(); const diff=preview?.diff || {}; const impact=preview?.referenceImpact || {}; if((diff.providerChanges || []).length || (diff.modelChanges || []).length){ areas.add('Models'); } (impact.entries || []).forEach((entry)=>{ const path=String(entry.path || ''); if(path.startsWith('Router.')){ areas.add('Router'); } else if(path.startsWith('TriggerRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.')){ areas.add('Governance'); } }); return Array.from(areas);}function renderDraftPreviewMeta(meta){ if(!meta){ draftPreviewMeta.innerHTML='<div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div>'; return; } draftPreviewMeta.innerHTML='<div class="alert info"><strong>'+esc(meta.title || 'Preset dry-run')+'</strong><div>'+esc(meta.description || '')+'</div><div class="muted">\u6A21\u5F0F\uFF1A'+esc(meta.mode || '-')+' \xB7 \u9884\u8BBE\u58F0\u660E\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((meta.affects || []).join(' / ') || '-')+'</div><div class="muted">\u5B9E\u9645\u9884\u89C8\u547D\u4E2D\u533A\u57DF\uFF1A'+esc((meta.actualAffects || []).join(' / ') || '-')+'</div></div>';}function renderDraftPresetGuide(){ draftPresetList.innerHTML=Object.entries(draftPresets).map(([key,preset])=>'<div class="alert info"><strong>'+esc(preset.label || key)+'</strong><div>'+esc(preset.description || '')+'</div><div class="muted">\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((preset.affects || []).join(' / '))+'</div></div>').join('');}function updateTopLevelModelSuggestionLists(){ const markup=knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join(''); [topLevelTriggerIntentSuggestions,topLevelSmartRouterSuggestions,topLevelGovernanceSummarizerSuggestions,topLevelGovernanceClassifierSuggestions,topLevelGovernanceVerifierSuggestions].forEach(node=>{ if(node){ node.innerHTML=markup; } });}function renderModelsForm(models){ const list=Array.isArray(models) ? models : []; draftModelsCount.value=String(list.length); if(!list.length){ modelsFormGrid.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div>'; return; } modelsFormGrid.innerHTML=list.map((model,index)=>{ const templateContext=getProviderTemplateContext(model); const template=templateContext.template; return '<div class="model-card" data-model-card="'+index+'">' + '<div class="model-card-header"><strong>Model #'+(index+1)+'</strong><button type="button" data-remove-model="'+index+'">\u5220\u9664</button></div>' + '<div class="model-card-grid">' + '<div><label>Provider template</label><div class="row"><select data-field="provider_template" data-index="'+index+'"><option value="">custom</option>'+Object.entries(modelProviderTemplates).map(([key,item])=>'<option value="'+esc(key)+'"'+(model.provider_template === key ? ' selected' : '')+'>'+esc(item.label)+'</option>').join('')+'</select><button type="button" data-apply-template="'+index+'">\u5E94\u7528</button></div></div>' + '<div><label>ID</label><input data-field="id" data-index="'+index+'" value="'+esc(model.id || '')+'" placeholder="'+esc(template.suggested_id || 'sonnet')+'"><div class="muted">Router.default \u548C\u8DEF\u7531\u89C4\u5219\u5F15\u7528\u8FD9\u4E2A model id\uFF1B\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 class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 interface\uFF1B\u65E7 protocol \u4F1A\u81EA\u52A8\u8BFB\u53D6\u4E3A\u517C\u5BB9\u503C\u3002</div></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">\u4E0A\u6E38\u771F\u5B9E\u6A21\u578B\u540D\uFF0C\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 class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 api\uFF1B\u65E7 api_base_url \u4EC5\u7528\u4E8E\u517C\u5BB9\u8BFB\u53D6\u3002</div></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 class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 key\uFF1B\u65E7 api_key \u4EC5\u7528\u4E8E\u517C\u5BB9\u8BFB\u53D6\u3002</div></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>Context window</label><input data-field="context_window_tokens" data-index="'+index+'" value="'+esc(model.metadata?.context_window_tokens || '')+'" placeholder="200000"></div>' + '<div><label>Safe input</label><input data-field="safe_input_tokens" data-index="'+index+'" value="'+esc(model.metadata?.safe_input_tokens || '')+'" placeholder="180000"></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 contextWindowTokens=(read('context_window_tokens')?.value || '').trim(); const safeInputTokens=(read('safe_input_tokens')?.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(contextWindowTokens){ metadata.context_window_tokens=Number(contextWindowTokens); } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'context_window_tokens')){ delete metadata.context_window_tokens; } if(safeInputTokens){ metadata.safe_input_tokens=Number(safeInputTokens); } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'safe_input_tokens')){ delete metadata.safe_input_tokens; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); const smartCandidates=extractSmartCandidatesFromForm(); const smartRouterEnabled=Boolean(smartEnabled.checked || triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim() || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim()); if(smartRouterEnabled){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: true, analysis_scope: triggerAnalysisScope.value || payload.SmartRouter?.analysis_scope || 'last_message', router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined, rules: triggerRules, semantic:(governanceSemanticEnabled.checked || triggerIntentEnabled.checked || governanceClassifierModel.value.trim() || triggerIntentModel.value.trim()) ? { ...(((payload.SmartRouter || {}).semantic) || {}), enabled:Boolean(governanceSemanticEnabled.checked || triggerIntentEnabled.checked), mode:'classifier', classifier_model: governanceClassifierModel.value.trim() || triggerIntentModel.value.trim() } : undefined, sticky:(governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim()) ? { ...(((payload.SmartRouter || {}).sticky) || {}), enabled:true, alignment:{ ...((((payload.SmartRouter || {}).sticky || {}).alignment) || {}), enabled:Boolean(governanceAlignmentEnabled.checked), summarizer_model: governanceSummarizerModel.value.trim() } } : undefined }; } else { delete payload.SmartRouter; } delete payload.TriggerRouter; const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const smart=getDraftSmartRouterConfig(config); const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(smart.enabled); triggerIntentEnabled.checked=Boolean(smart.semantic?.enabled && smart.semantic?.mode === 'classifier'); triggerAnalysisScope.value=smart.analysis_scope || 'last_message'; triggerIntentModel.value=smart.semantic?.classifier_model || trigger.intent_model || ''; renderTriggerRulesList(smart.rules || trigger.rules || []); smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(smart.sticky?.alignment?.enabled); governanceSummarizerModel.value=smart.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(smart.semantic?.enabled); governanceClassifierModel.value=smart.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); 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([],[]); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyContextWindowAction(action,modelId){ if(action!=='set-long-context' || !modelId){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u8BE5\u4E0A\u4E0B\u6587\u7A97\u53E3\u64CD\u4F5C'; return; } const payload=buildDraftPayloadFromForm(); payload.Router={ ...(payload.Router || {}), longContext:modelId }; currentDraftConfig=payload; renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06 Router.longContext \u8BBE\u7F6E\u4E3A '+modelId+'\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(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); 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 getRouterSlotDefinitions(){ return [ { key:'default', label:'Default', when:'\u666E\u901A\u8BF7\u6C42\u3001\u89C4\u5219\u672A\u547D\u4E2D\u6216\u5176\u4ED6\u69FD\u4F4D\u672A\u914D\u7F6E\u65F6\u4F7F\u7528', required:true }, { key:'think', label:'Thinking', when:'\u8BF7\u6C42\u5305\u542B thinking \u65F6\u4F18\u5148\u4F7F\u7528', required:false }, { key:'longContext', label:'Long context', when:'\u8F93\u5165\u8D85\u8FC7\u9608\u503C\uFF0C\u6216\u5F53\u524D\u6A21\u578B safe_input_tokens \u4E0D\u591F\u65F6\u4F7F\u7528', required:false }, { key:'background', label:'Background', when:'Claude Code \u8F7B\u91CF\u540E\u53F0\u6A21\u578B\u8BF7\u6C42\u65F6\u4F7F\u7528', required:false }, { key:'webSearch', label:'Web search', when:'\u8BF7\u6C42\u5305\u542B web_search \u5DE5\u5177\u65F6\u4F7F\u7528', required:false }, ];}function renderRouterSlotExplanation(data){ const config=data?.normalizedConfig || { Router:(currentDraftConfig.Router && Object.keys(currentDraftConfig.Router).length ? currentDraftConfig.Router : (data?.router || {})) }; const router=config.Router || {}; const modelMap=data?.modelMap || {}; const slots=getRouterSlotDefinitions(); let configured=0; let resolved=0; let warnings=0; const defaultRef=String(router.default || '').trim(); const defaultModel=defaultRef ? modelMap[defaultRef] : null; const rows=slots.map(slot=>{ const ref=String(router[slot.key] || '').trim(); const model=ref ? modelMap[ref] : null; const caps=model?.capabilities || {}; const slotWarnings=[]; if(ref){ configured+=1; } if(ref && model){ resolved+=1; } if(slot.required && !ref){ slotWarnings.push('\u5FC5\u586B\u69FD\u4F4D\u672A\u914D\u7F6E'); } if(ref && !model){ slotWarnings.push('\u5F15\u7528\u672A\u89E3\u6790\u5230 Models[].id'); } if(slot.key==='think' && model && caps.thinking?.supported === false){ slotWarnings.push('\u76EE\u6807\u6A21\u578B\u58F0\u660E\u4E0D\u652F\u6301 reasoning'); } if(slot.key==='longContext' && model){ if(!caps.contextWindowTokens){ slotWarnings.push('\u7F3A\u5C11 context_window_tokens'); } if(!caps.safeInputTokens){ slotWarnings.push('\u7F3A\u5C11 safe_input_tokens'); } if(defaultModel?.capabilities?.contextWindowTokens && caps.contextWindowTokens && caps.contextWindowTokens <= defaultModel.capabilities.contextWindowTokens){ slotWarnings.push('\u7A97\u53E3\u4E0D\u9AD8\u4E8E default'); } } if(model && slot.key!=='longContext' && (!caps.contextWindowTokens || !caps.safeInputTokens)){ slotWarnings.push('\u7F3A\u5C11\u4E0A\u4E0B\u6587\u7A97\u53E3\u5143\u6570\u636E'); } warnings+=slotWarnings.length; const target=model ? ('<code>'+esc(model.providerName || '-')+'</code><div class="muted">'+esc(model.modelName || '-')+'</div>') : '<span class="muted">-</span>'; const capabilityParts=model ? [ 'thinking '+(caps.thinking?.supported === false ? 'off' : 'on'), 'tools '+(caps.tools === false ? 'off' : 'on'), 'images '+(caps.images === false ? 'off' : 'on'), caps.contextWindowTokens ? ('ctx '+caps.contextWindowTokens) : 'ctx ?', caps.safeInputTokens ? ('safe '+caps.safeInputTokens) : 'safe ?', ] : []; const warningText=slotWarnings.length ? slotWarnings.join('\uFF1B') : (ref ? 'ok' : '\u672A\u914D\u7F6E\u65F6\u56DE\u5230 default'); const warningClass=slotWarnings.length ? 'warn' : 'info'; return '<tr>' + '<td><strong>'+esc(slot.label)+'</strong><div class="muted">Router.'+esc(slot.key)+'</div></td>' + '<td>'+esc(slot.when)+'</td>' + '<td>'+(ref ? '<code>'+esc(ref)+'</code>' : '<span class="muted">not configured</span>')+'</td>' + '<td>'+target+'</td>' + '<td>'+(capabilityParts.length ? capabilityParts.map(item=>'<span class="pill">'+esc(item)+'</span>').join(' ') : '<span class="muted">-</span>')+'</td>' + '<td><span class="pill '+warningClass+'">'+esc(warningText)+'</span></td>' + '</tr>'; }); routerSlotSummary.innerHTML=[['Configured slots',configured],['Resolved slots',resolved],['Warnings',warnings]].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); routerSlotTableBody.innerHTML=rows.join('');}function readModelMetadataNumber(model,key){ const value=model?.metadata?.[key]; return Number.isFinite(Number(value)) && Number(value)>0 ? Number(value) : undefined;}function getContextWindowEntries(data,config){ const modelMap=data?.modelMap || {}; const draftModels=Array.isArray(config?.Models) ? config.Models : []; if(draftModels.length){ return draftModels.map(model=>{ const id=String(model?.id || '').trim(); const compiled=id ? modelMap[id] : null; const caps=compiled?.capabilities || {}; return { id, modelName:model?.model || compiled?.modelName || '-', contextWindowTokens:readModelMetadataNumber(model,'context_window_tokens') || caps.contextWindowTokens, safeInputTokens:readModelMetadataNumber(model,'safe_input_tokens') || caps.safeInputTokens }; }).filter(item=>item.id); } return Object.entries(modelMap).map(([id,model])=>({ id, modelName:model?.modelName || '-', contextWindowTokens:model?.capabilities?.contextWindowTokens, safeInputTokens:model?.capabilities?.safeInputTokens }));}function renderContextWindowGuide(data){ const config=data?.normalizedConfig || currentDraftConfig || {}; const router=config.Router || {}; const entries=getContextWindowEntries(data,config); if(!entries.length){ contextWindowGuide.innerHTML='<div class="alert info"><strong>Context window guide</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u8FD8\u6CA1\u6709\u53EF\u89E3\u6790\u7684 Models\u3002</div></div>'; return; } const defaultRef=String(router.default || '').trim(); const longRef=String(router.longContext || '').trim(); const defaultEntry=entries.find(item=>item.id===defaultRef); const longEntry=entries.find(item=>item.id===longRef); const ranked=entries.filter(item=>item.contextWindowTokens).sort((a,b)=>(b.contextWindowTokens || 0)-(a.contextWindowTokens || 0)); const best=ranked[0]; const missingCount=entries.filter(item=>!item.contextWindowTokens || !item.safeInputTokens).length; const messages=[]; let level='info'; if(missingCount){ level='warn'; messages.push('\u6709 '+missingCount+' \u4E2A\u6A21\u578B\u7F3A\u5C11 context_window_tokens \u6216 safe_input_tokens\uFF0C\u8D85\u5927\u8BF7\u6C42\u53EF\u80FD\u65E0\u6CD5\u63D0\u524D\u964D\u7EA7/\u5207\u6362\u3002'); } if(entries.length>1 && !longRef){ level='warn'; messages.push('\u591A\u6A21\u578B\u914D\u7F6E\u672A\u8BBE\u7F6E Router.longContext\uFF0C\u5927\u4E0A\u4E0B\u6587\u8BF7\u6C42\u4F1A\u7EE7\u7EED\u4F7F\u7528\u5DF2\u9009\u6A21\u578B\u3002'); } if(longRef && !longEntry){ level='warn'; messages.push('Router.longContext \u5F15\u7528\u672A\u89E3\u6790\u5230 Models[].id\u3002'); } if(longEntry && (!longEntry.contextWindowTokens || !longEntry.safeInputTokens)){ level='warn'; messages.push('Router.longContext \u7F3A\u5C11\u4E0A\u4E0B\u6587\u7A97\u53E3\u6216\u5B89\u5168\u8F93\u5165\u5143\u6570\u636E\u3002'); } if(defaultEntry?.contextWindowTokens && longEntry?.contextWindowTokens && longEntry.contextWindowTokens <= defaultEntry.contextWindowTokens){ level='warn'; messages.push('Router.longContext \u7684\u7A97\u53E3\u4E0D\u9AD8\u4E8E Router.default\uFF0C\u53EF\u80FD\u65E0\u6CD5\u63D0\u5347\u5927\u4E0A\u4E0B\u6587\u4F53\u9A8C\u3002'); } if(!messages.length){ messages.push('\u5F53\u524D\u4E0A\u4E0B\u6587\u7A97\u53E3\u5143\u6570\u636E\u548C Router.longContext \u914D\u7F6E\u53EF\u7528\u4E8E\u5927\u4E0A\u4E0B\u6587 fallback\u3002'); } const canApplyBest=best?.id && best.id!==longRef && (!defaultEntry?.contextWindowTokens || (best.contextWindowTokens || 0)>defaultEntry.contextWindowTokens); const summaryRows=[['Default', defaultRef || '-'],['Default ctx', defaultEntry?.contextWindowTokens || '?'],['Long context', longRef || '-'],['Long ctx', longEntry?.contextWindowTokens || '?'],['Largest ctx', best ? (best.id+' / '+best.contextWindowTokens) : '-'],['Missing metadata', missingCount]]; contextWindowGuide.innerHTML='<div class="alert '+level+'"><div class="row"><strong>Context window guide</strong>'+(best ? '<span class="pill">largest '+esc(best.id)+'</span>' : '')+'</div><div class="diff-summary">'+summaryRows.map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('')+'</div><ul>'+messages.map(message=>'<li>'+esc(message)+'</li>').join('')+'</ul>'+(canApplyBest ? '<div class="row" style="margin-top:.5rem"><button type="button" data-context-action="set-long-context" data-model-id="'+esc(best.id)+'">\u8BBE\u4E3A Router.longContext</button><span class="muted">'+esc(best.modelName || '')+'</span></div>' : '')+'</div>';}function getSmartCandidateGuideEntries(data,config){ const modelMap=data?.modelMap || {}; const draftModels=Array.isArray(config?.Models) ? config.Models : []; if(draftModels.length){ return draftModels.map(model=>{ const id=String(model?.id || '').trim(); const compiled=id ? modelMap[id] : null; const caps=compiled?.capabilities || {}; return { id, modelName:model?.model || compiled?.modelName || '-', contextWindowTokens:readModelMetadataNumber(model,'context_window_tokens') || caps.contextWindowTokens || 0, thinkingSupported:caps.thinking?.supported !== false }; }).filter(item=>item.id); } return Object.entries(modelMap).map(([id,model])=>({ id, modelName:model?.modelName || '-', contextWindowTokens:model?.capabilities?.contextWindowTokens || 0, thinkingSupported:model?.capabilities?.thinking?.supported !== false }));}function pickSmartCandidate(entries,role){ const list=[...entries]; const score=(item)=>{ const text=(item.id+' '+item.modelName).toLowerCase(); let value=0; if(role==='fast'){ if(/haiku|mini|flash|fast|lite|small/.test(text)){ value+=80; } value+=Math.max(0,50-Math.log10((item.contextWindowTokens || 1))*10); } else if(role==='deep'){ if(/opus|reasoner|thinking|o1|o3|gpt-5|sonnet/.test(text)){ value+=80; } if(item.thinkingSupported){ value+=20; } value+=Math.log10((item.contextWindowTokens || 1))*5; } else if(role==='long_context'){ value+=item.contextWindowTokens || 0; } else { if(/sonnet|gpt-4|gpt-5|default|balanced/.test(text)){ value+=80; } value+=Math.log10((item.contextWindowTokens || 1))*8; } return value; }; return list.sort((a,b)=>score(b)-score(a) || a.id.localeCompare(b.id))[0];}function renderSmartCandidateGuide(data,summary){ const config=data?.normalizedConfig || currentDraftConfig || {}; const entries=getSmartCandidateGuideEntries(data,config); const candidates=Array.isArray(summary?.candidates) ? summary.candidates : []; const configured=new Set(candidates.map(candidate=>candidate.model?.ref).filter(Boolean)); if(!entries.length){ smartCandidateGuide.innerHTML='<div class="alert info"><strong>Candidate guide</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u8FD8\u6CA1\u6709\u53EF\u89E3\u6790\u7684 Models\uFF0C\u5148\u6DFB\u52A0\u6A21\u578B\u540E\u518D\u914D\u7F6E\u5019\u9009\u3002</div></div>'; return; } const roles=[ { key:'fast', label:'Fast', description:'\u9AD8\u9891\u8F7B\u91CF\u4EFB\u52A1\u3001\u540E\u53F0\u8BF7\u6C42\u548C\u4F4E\u5EF6\u8FDF\u5019\u9009' }, { key:'balanced', label:'Balanced', description:'\u9ED8\u8BA4\u65E5\u5E38\u7F16\u7801\u3001\u89E3\u91CA\u548C\u4E2D\u7B49\u590D\u6742\u5EA6\u4EFB\u52A1' }, { key:'deep', label:'Deep', description:'\u590D\u6742\u63A8\u7406\u3001\u67B6\u6784\u8BBE\u8BA1\u548C\u9700\u8981\u66F4\u5F3A\u6A21\u578B\u7684\u4EFB\u52A1' }, { key:'long_context', label:'Long context', description:'\u5927\u4E0A\u4E0B\u6587\u3001\u957F\u6587\u4EF6\u548C\u8D85\u957F\u4F1A\u8BDD\u515C\u5E95\u5019\u9009' }, ]; const rows=roles.map(role=>{ const picked=pickSmartCandidate(entries,role.key); const configuredRole=picked && configured.has(picked.id); const cls=configuredRole ? 'info' : 'warn'; const button=(!configuredRole && picked) ? '<button type="button" data-add-smart-candidate-suggestion="'+esc(picked.id)+'" data-description="'+esc(role.key+' candidate')+'">Add candidate</button>' : ''; return '<div class="alert '+cls+'"><div class="row"><strong>'+esc(role.label)+'</strong><span class="pill '+cls+'">'+esc(configuredRole ? 'configured' : 'suggested')+'</span></div><div>'+esc(role.description)+'</div><div class="muted">'+(picked ? ('<code>'+esc(picked.id)+'</code> \xB7 '+esc(picked.modelName || '-')+' \xB7 ctx '+esc(picked.contextWindowTokens || '?')) : 'no model suggestion')+'</div>'+(button ? '<div class="row" style="margin-top:.5rem">'+button+'</div>' : '')+'</div>'; }); smartCandidateGuide.innerHTML='<div class="alert info"><strong>Candidate guide</strong><div class="muted">\u5EFA\u8BAE\u81F3\u5C11\u8986\u76D6 fast / balanced / deep\uFF0C\u9700\u8981\u5927\u4E0A\u4E0B\u6587\u65F6\u518D\u52A0\u5165 long-context \u5019\u9009\u3002</div></div>'+rows.join('');}function renderSmartRouterExplanation(data){ const summary=data?.smartRouterExplanation || {}; const rules=Array.isArray(summary.rules) ? summary.rules : []; const candidates=Array.isArray(summary.candidates) ? summary.candidates : []; const warnings=Array.isArray(summary.warnings) ? summary.warnings : []; const refLabel=(model)=>model?.ref ? ('<code>'+esc(model.ref)+'</code><div class="muted">'+esc(model.target?.providerName || model.status || '-')+' / '+esc(model.target?.modelName || '-')+'</div>') : '<span class="muted">-</span>'; const switchRows=[['Enabled', summary.enabled ? 'true' : 'false'],['Rules', rules.length],['Candidates', candidates.length],['Router model', summary.routerModel?.ref || '-'],['Semantic', (summary.semantic?.enabled ? 'on' : 'off')+' / '+(summary.semantic?.mode || '-')],['Sticky', summary.sticky?.enabled ? 'on' : 'off'],['Alignment', summary.sticky?.alignment?.enabled ? 'on' : 'off'],['Fallback', summary.fallback || 'default'],['Warnings', warnings.length]]; smartRouterExplanationSummary.innerHTML=switchRows.map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const order=Array.isArray(summary.routeOrder) ? summary.routeOrder : []; smartRouterRouteOrder.innerHTML='<div class="alert '+(warnings.length ? 'warn' : 'info')+'"><strong>Route order</strong><ol>'+order.map(item=>'<li>'+esc(item)+'</li>').join('')+'</ol>'+(warnings.length ? '<ul>'+warnings.map(item=>'<li>'+esc(item)+'</li>').join('')+'</ul>' : '<div class="muted">SmartRouter \u914D\u7F6E\u5F15\u7528\u5DF2\u89E3\u6790\u3002</div>')+'</div>'; smartRouterRulesTableBody.innerHTML=rules.length ? rules.map(rule=>{ const patternText=(rule.patterns || []).map(pattern=>pattern.type==='exact' ? ('exact: '+(pattern.keywords || []).join(', ')) : ('regex: '+(pattern.pattern || '-'))).join('; '); const statusClass=rule.model?.status === 'resolved' ? 'info' : (rule.model?.status === 'legacy' ? 'warn' : 'critical'); return '<tr>' + '<td>'+esc(rule.order || '-')+'<div class="muted">priority '+esc(rule.priority ?? 0)+'</div></td>' + '<td><strong>'+esc(rule.name || '-')+'</strong><div class="muted">'+esc(rule.description || '-')+'</div><span class="pill '+(rule.enabled ? 'info' : 'warn')+'">'+esc(rule.enabled ? 'enabled' : 'disabled')+'</span></td>' + '<td>'+refLabel(rule.model)+'<span class="pill '+statusClass+'">'+esc(rule.model?.status || '-')+'</span></td>' + '<td>'+esc(patternText || '-')+'</td>' + '<td>'+esc(rule.semantic?.enabled ? 'on' : 'off')+'<div class="muted">'+esc(rule.semantic?.prototype || '-')+'</div></td>' + '</tr>'; }).join('') : '<tr><td colspan="5" class="muted">No SmartRouter rules configured</td></tr>'; smartRouterCandidatesTableBody.innerHTML=candidates.length ? candidates.map(candidate=>{ const statusClass=candidate.model?.status === 'resolved' ? 'info' : (candidate.model?.status === 'legacy' ? 'warn' : 'critical'); return '<tr><td>'+esc(candidate.order || '-')+'</td><td>'+refLabel(candidate.model)+'</td><td>'+esc(candidate.description || '-')+'</td><td><span class="pill '+statusClass+'">'+esc(candidate.model?.status || '-')+'</span></td></tr>'; }).join('') : '<tr><td colspan="4" class="muted">No SmartRouter candidates configured</td></tr>'; renderSmartCandidateGuide(data,summary);}function renderCompiledModels(data){ lastCompiledModelsData=data || null; const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); const modelPoolEntries=Object.entries(data.modelPools || {}); const modelPoolEndpointCount=modelPoolEntries.reduce((sum,[_modelId,pool])=>sum+((pool.endpoints || []).length),0); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); renderRouterSlotExplanation(data); renderContextWindowGuide(data); renderSmartRouterExplanation(data); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04 / '+modelPoolEntries.length+' \u4E2A model pool / '+modelPoolEndpointCount+' \u4E2A pool endpoint'; 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>'; compiledModelPoolsTableBody.innerHTML=modelPoolEntries.length ? modelPoolEntries.map(([modelId,pool])=>{ const endpoints=pool.endpoints || []; return '<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td>'+esc(pool.strategy || '-')+'</td>' + '<td><code>'+esc(pool.activeEndpointId || '-')+'</code></td>' + '<td>'+endpoints.map(endpoint=>{ const latency=endpoint.health?.latency; return '<div><code>'+esc(endpoint.id)+'</code><span class="muted"> priority '+esc(endpoint.priority)+' / '+esc(endpoint.enabled ? 'enabled' : 'disabled')+' / '+esc(endpoint.health?.status || 'healthy')+(latency ? ' / avg '+esc(Math.round(latency.averageMs))+'ms' : '')+'</span><div class="muted">'+esc(endpoint.upstreamServiceId || endpoint.api || '-')+'</div></div>'; }).join('')+'</td>' + '<td>'+((pool.warnings || []).length ? pool.warnings.map(w=>'<div class="warning-text">'+esc(w)+'</div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>'; }).join('') : '<tr><td colspan="5" class="muted">No compiled model pools</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); updateStatusSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(currentDraftConfig)); renderContextWindowGuide(withDraftCompiledData(currentDraftConfig)); 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(); renderContextWindowGuide(lastCompiledModelsData); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderContextWindowGuide(withDraftCompiledData(payload)); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function loadServiceStatus(){ serviceReadyStatus.textContent='checking'; try { const [serviceRes,remoteRes]=await Promise.all([fetch('/api/service-info'),fetch('/api/remote-status')]); const data=await serviceRes.json(); const remoteData=await remoteRes.json(); serviceReadyStatus.textContent=data.ready ? 'ready' : 'not ready'; servicePortStatus.textContent=data.port || '-'; serviceModeStatus.textContent=data.runtimeMode || '-'; serviceRoleStatus.textContent=data.serviceRole || '-'; renderRoleConnectionGuide(data); const auth=data.auth || {}; const managed=auth.managedKeys || {}; const quota=auth.quota || {}; const quotaText=Number.isFinite(quota.requestsUsed) ? (' \xB7 quota '+quota.requestsUsed+' req'+(quota.windowResetAt ? ' \xB7 reset '+String(quota.windowResetAt).replace('T',' ').replace('.000Z','Z') : '')) : ''; authStatusSummary.textContent=auth.required ? ((auth.bootstrapConfigured ? 'bootstrap' : 'managed')+' \xB7 '+(managed.active ?? 0)+' active'+quotaText) : 'not configured'; renderAuthQuotaTable(quota); const security=data.security || {}; const issues=Array.isArray(security.issues) ? security.issues : []; securityStatusSummary.textContent=security.status || '-'; securitySummary.className='alert '+((security.status === 'critical') ? 'critical' : (security.status === 'warning' ? 'warn' : 'info')); securitySummary.innerHTML='<strong>Security: '+esc(security.status || '-')+'</strong><div>'+esc(issues[0]?.message || '\u5F53\u524D\u670D\u52A1\u672A\u53D1\u73B0\u660E\u663E\u9274\u6743\u66B4\u9732\u98CE\u9669')+'</div>'+ (issues.length ? '<ul class="mini-list">'+issues.map(issue=>'<li>'+esc(issue.action || issue.code)+'</li>').join('')+'</ul>' : ''); const registration=data.registration || {}; registrationStatusSummary.textContent=registration.enabled ? ((registration.models ?? 0)+' models / '+(registration.upstreamServices ?? 0)+' upstream') : 'disabled'; const remote=remoteData.remote || {}; remoteStatusSummary.textContent=remote.enabled ? ((remote.ready ? 'ready' : (remote.reachable ? 'reachable' : 'unreachable'))+' \xB7 '+(remote.baseUrl || '-')) : 'disabled'; const remoteRegistration=remoteData.remoteRegistration || {}; const remoteRegistrationSummary=remoteRegistration.summary || {}; remoteRegistrationStatusSummary.textContent=remoteRegistration.enabled ? (remoteRegistration.available ? (remoteRegistration.registrationEnabled ? ((remoteRegistrationSummary.models ?? 0)+' remote models / '+(remoteRegistrationSummary.upstreamServices ?? 0)+' upstream') : 'remote registration disabled') : ('unavailable \xB7 '+(remoteRegistration.error || remoteRegistration.baseUrl || '-'))) : 'disabled'; if(remoteData.compiledModels){ modelCountStatus.textContent=remoteData.compiledModels.modelCount ?? modelCountStatus.textContent; } try { await loadModelPoolHealth(); } catch (_poolError) { modelPoolHealthSummary.className='alert warn'; modelPoolHealthSummary.innerHTML='<strong>Pool health unavailable</strong><div class="muted">\u65E0\u6CD5\u52A0\u8F7D\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001</div>'; } } catch (_error) { serviceReadyStatus.textContent='unreachable'; remoteStatusSummary.textContent='unknown'; securityStatusSummary.textContent='unknown'; modelPoolHealthSummary.className='alert warn'; modelPoolHealthSummary.innerHTML='<strong>Pool health unavailable</strong><div class="muted">\u65E0\u6CD5\u52A0\u8F7D\u6A21\u578B\u6C60\u5065\u5EB7\u72B6\u6001</div>'; }}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderContextWindowGuide(lastCompiledModelsData); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || [], data.issueReport); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addSmartCandidateSuggestion(modelId,description){ const id=String(modelId || '').trim(); if(!id){ return; } const next=extractSmartCandidatesFromForm(); if(!next.some(item=>item.model===id)){ next.push({ model:id, description:description || 'guided candidate' }); } renderSmartCandidatesList(next); syncDraftEditorFromForm(); renderSmartCandidateGuide(withDraftCompiledData(currentDraftConfig), { candidates: next.map((item,index)=>({ order:index+1, description:item.description, model:{ ref:item.model, status:'resolved' } })) }); }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(); });smartCandidateGuide.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-add-smart-candidate-suggestion]'); if(!btn){ return; } addSmartCandidateSuggestion(btn.dataset.addSmartCandidateSuggestion, btn.dataset.description); });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); });contextWindowGuide.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-context-action]'); if(!btn){ return; } applyContextWindowAction(btn.dataset.contextAction, btn.dataset.modelId); });healthSummary.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-health-action]'); if(btn){ applyHealthAction(btn.dataset.healthAction); } });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });surfaceTabs.forEach((tab)=>tab.addEventListener('click',()=>setActiveSurface(tab.dataset.surfaceTarget || 'user')));setActiveSurface('user');function renderMetrics(metrics,health,outcome){ metricsGrid.innerHTML=[ ['Health', health?.status || 'idle'], ['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)], ['Model switch rate', pct(outcome?.modelSwitchRate)], ['Alignment on switch', pct(outcome?.alignmentOnSwitchRate)], ['Context fallback', pct(outcome?.contextWindowFallbackRate)], ['Context exceeded', pct(outcome?.contextWindowExceededRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.SmartRouter?.enabled), analysis_scope: payload.SmartRouter?.analysis_scope || 'last_message', router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: payload.SmartRouter?.candidates || [], cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.SmartRouter?.rules || []) }; delete payload.TriggerRouter; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: payload.SmartRouter?.rules || [] }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: payload.SmartRouter?.enabled !== undefined ? Boolean(payload.SmartRouter?.enabled) : Boolean(preset.governanceEnabled), sticky:{ ...((payload.SmartRouter && payload.SmartRouter.sticky) || {}), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.enabled), alignment:{ ...(((payload.SmartRouter && payload.SmartRouter.sticky && payload.SmartRouter.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.SmartRouter?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.SmartRouter && payload.SmartRouter.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.SmartRouter?.semantic?.enabled), mode:(payload.SmartRouter?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.SmartRouter?.semantic?.classifier_model || '') } }; payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderRouterSlotExplanation(withDraftCompiledData(payload)); renderContextWindowGuide(withDraftCompiledData(payload)); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderOutcomeGroups(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 class="muted"> \xB7 '+esc(item.totalTraces)+' traces</span></span><strong>switch '+esc(pct(item.modelSwitchRate))+' \xB7 align '+esc(pct(item.alignmentOnSwitchRate))+' \xB7 cascade '+esc(pct(item.cascadeAfterSwitchRate))+' \xB7 '+esc(fmt(item.averageLatencyMs))+' ms</strong></li>').join('');}function renderRoutingTuning(items){ if(!items || !items.length){ routingTuningList.innerHTML='<li><span class="muted">No routing tuning recommendations</span><strong>healthy</strong></li>'; return; } routingTuningList.innerHTML=items.map(item=>{ const suggestions=Array.isArray(item.configSuggestions) ? item.configSuggestions : []; const suggestionHtml=suggestions.length ? '<div class="muted">config: '+suggestions.map(s=>'<code>'+esc(s.path || '-')+'</code>'+(s.suggestedValue !== undefined ? ' = '+esc(s.suggestedValue) : '')+' \u2014 '+esc(s.reason || '')).join('<br>')+'</div>' : ''; return '<li><span><span class="pill '+esc(item.severity === 'critical' ? 'critical' : (item.severity === 'warn' ? 'warn' : 'info'))+'">'+esc(item.severity || 'info')+'</span> <strong>'+esc(item.code || '-')+'</strong><div class="muted">'+esc(item.message || '')+'</div><div class="muted">'+esc(item.evidence || '')+'</div>'+suggestionHtml+'</span><strong>'+esc(item.action || '')+'</strong></li>'; }).join('');}function renderQualityEvidence(summary){ const items=summary?.samples || []; qualityEvidenceSummary.innerHTML=[['Samples',summary?.totalSamples || 0],['Risk',summary?.failureSamples || 0],['Improvement',summary?.improvementSamples || 0],['Speed risk',summary?.speedRiskSamples || 0]].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); if(!items.length){ qualityEvidenceList.innerHTML='<li><span class="muted">No quality evidence samples</span><strong>0</strong></li>'; return; } qualityEvidenceList.innerHTML=items.map(item=>'<li><span><span class="pill '+esc(item.severity === 'critical' ? 'critical' : (item.severity === 'warn' ? 'warn' : 'info'))+'">'+esc(item.severity || 'info')+'</span> <strong>'+esc(item.type || '-')+'</strong><div class="muted">'+esc(item.requestId || '')+' \xB7 '+esc((item.routeReason || []).join(' / '))+'</div><div class="muted">'+esc(item.evidence || '')+'</div></span><strong>'+esc(item.action || '')+'</strong></li>').join('');}function renderTaskComparison(summary){ const items=summary?.comparisons || []; taskComparisonSummary.innerHTML=[['Tasks',summary?.totalComparedTasks || 0],['Traces',summary?.totalComparedTraces || 0]].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); if(!items.length){ taskComparisonList.innerHTML='<li><span class="muted">No comparable task samples</span><strong>0</strong></li>'; return; } taskComparisonList.innerHTML=items.map(item=>'<li><span><strong>'+esc(item.taskKey || '-')+'</strong><div class="muted">best '+esc(item.bestModel || '-')+' \xB7 baseline '+esc(item.baselineModel || '-')+' \xB7 fastest '+esc(item.fastestModel || '-')+'</div><div class="muted">failure lift '+esc(pct(item.failureRateDelta || 0))+' \xB7 latency lift '+esc(fmt(item.latencyDeltaMs || 0))+' ms \xB7 models '+esc(item.modelCount || 0)+'</div></span><strong>'+esc(item.totalTraces || 0)+' traces</strong></li>').join('');}function renderBenchmarkSummary(taskComparison,qualityEvidence){ const bestQuality=taskComparison?.bestQualityLiftTask; const bestSpeed=taskComparison?.bestSpeedLiftTask; benchmarkSummary.innerHTML=[ ['Comparable tasks',taskComparison?.totalComparedTasks || 0], ['Evidence samples',qualityEvidence?.totalSamples || 0], ['Best quality lift',bestQuality ? pct(bestQuality.failureRateDelta || 0) : '-'], ['Best speed lift',bestSpeed ? (fmt(bestSpeed.latencyDeltaMs || 0)+' ms') : '-'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const actions=[]; if((taskComparison?.totalComparedTasks || 0)===0){ actions.push(['Collect comparable traces','Send the same task class through at least two final models, then refresh metrics.']); } if((qualityEvidence?.totalSamples || 0)===0){ actions.push(['Collect quality evidence','Enable cascade, shadow, context-window or model-pool signals so routing wins and risks become visible.']); } actions.push(['Run fixed benchmark','ctr eval --tasks && ctr eval --run --models "sonnet;haiku" --json']); actions.push(['Add calibration','Attach humanScore or judgeScore to ctr eval input results before treating rubric scores as release evidence.']); benchmarkActionList.innerHTML=actions.map(([title,detail])=>'<li><span><strong>'+esc(title)+'</strong><div class="muted">'+esc(detail)+'</div></span><strong>benchmark</strong></li>').join('');}function signed(v,digits){ const n=Number(v || 0); return (n>0?'+':'')+n.toFixed(digits ?? 2); }function renderBenchmarkHistory(data){ const summary=data?.summary || {}; const latest=summary.latest || null; const trends=summary.trends || {}; const alignment=data?.traceAlignment || {}; const traceTasks=alignment.taskComparison?.totalComparedTasks || 0; benchmarkHistoryFile.textContent=data?.historyFile || 'benchmark-history.json'; benchmarkHistorySummary.innerHTML=[ ['Entries',summary.totalEntries || 0], ['Quality',latest ? fmt(latest.averageQualityScore) : '-'], ['Trace tasks',traceTasks], ['Latency',latest ? (fmt(latest.averageLatencyMs)+' ms') : '-'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); if(!latest){ benchmarkHistoryList.innerHTML='<li><span class="muted">No saved benchmark history</span><strong>0</strong></li>'; return; } const entries=Array.isArray(summary.entries) ? summary.entries.slice(-5).reverse() : []; const rows=entries.map(entry=>'<li><span><strong>'+esc(entry.label || entry.id || '-')+'</strong><div class="muted">'+esc(entry.createdAt || '-')+' \xB7 '+esc(entry.source || '-')+' \xB7 runs '+esc(entry.evaluatedRuns || 0)+'/'+esc(entry.totalRuns || entry.evaluatedRuns || 0)+'</div><div class="muted">pass '+esc(pct(entry.passRate || 0))+' \xB7 quality '+esc(fmt(entry.averageQualityScore || 0))+' \xB7 calibration '+esc(fmt(entry.averageCalibrationScore || 0))+'</div></span><strong>'+esc(fmt(entry.averageLatencyMs || 0))+' ms</strong></li>'); const modelRows=(summary.topModels || []).map(model=>'<li><span><strong>'+esc(model.model || '-')+'</strong><div class="muted">quality '+esc(fmt(model.averageQualityScore || 0))+' \xB7 pass '+esc(pct(model.passRate || 0))+' \xB7 latency '+esc(fmt(model.averageLatencyMs || 0))+' ms</div></span><strong>top</strong></li>'); const alignmentRows=[ '<li><span><strong>Trace alignment</strong><div class="muted">task comparison '+esc(traceTasks)+' \xB7 quality evidence '+esc(alignment.qualityEvidence?.totalSamples || 0)+' \xB7 quality trend '+esc(summary.previous ? signed(trends.qualityDelta || 0,2) : 'waiting')+'</div></span><strong>trace</strong></li>' ]; benchmarkHistoryList.innerHTML=[...rows,...modelRows,...alignmentRows].join('') || '<li><span class="muted">No saved benchmark history</span><strong>0</strong></li>';}async function loadBenchmarkHistory(){ const res=await fetch('/api/benchmark/history'); renderBenchmarkHistory(await res.json());}async function saveBenchmarkCalibration(){ benchmarkCalibrationStatus.textContent='\u4FDD\u5B58\u6821\u51C6\u4E2D...'; const payload={ taskId:document.getElementById('calibrationTaskId').value.trim(), model:document.getElementById('calibrationModel').value.trim(), humanScore:document.getElementById('calibrationHumanScore').value.trim() ? Number(document.getElementById('calibrationHumanScore').value.trim()) : undefined, latencyMs:document.getElementById('calibrationLatencyMs').value.trim() ? Number(document.getElementById('calibrationLatencyMs').value.trim()) : undefined, output:document.getElementById('calibrationOutput').value, calibrationNotes:document.getElementById('calibrationNotes').value, label:'ui-calibration' }; const res=await fetch('/api/benchmark/calibration',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ benchmarkCalibrationStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } benchmarkCalibrationStatus.textContent='\u5DF2\u4FDD\u5B58\u6821\u51C6\uFF1A'+(data.entry?.id || 'benchmark history'); renderBenchmarkHistory({ historyFile:data.historyFile, summary:data.summary });}function renderRouteDecisionSummaries(items){ const decisions=Array.isArray(items) ? items.slice(0,5) : []; if(!decisions.length){ routeDecisionSummaryList.innerHTML='<li><span class="muted">No recent route decisions</span><strong>0</strong></li>'; return; } routeDecisionSummaryList.innerHTML=decisions.map(item=>{ const meta=[item.sourceLabel || item.source || '-', item.ruleName ? ('rule '+item.ruleName) : '', item.semanticIntent ? ('intent '+item.semanticIntent) : '', item.routingMode ? ('mode '+item.routingMode) : '', item.collaborationMode ? ('collab '+item.collaborationMode) : '', item.confidenceLabel || '', item.latencyMs !== undefined ? (fmt(item.latencyMs)+' ms') : ''].filter(Boolean).join(' \xB7 '); const fallback=item.fallbackReason ? '<div class="muted">fallback: '+esc(item.fallbackReason)+'</div>' : ''; const evidence=Array.isArray(item.routingEvidence) && item.routingEvidence.length ? '<div class="muted">'+esc(item.routingEvidence.slice(0,2).join(' / '))+'</div>' : ''; return '<li><span><strong>'+esc(item.headline || item.requestId || '-')+'</strong><div class="muted">'+esc(meta)+'</div>'+fallback+evidence+'</span><button type="button" data-request="'+esc(item.requestId || '')+'">View</button></li>'; }).join('');}function renderSwitchContinuitySummaries(items){ const summaries=Array.isArray(items) ? items.slice(0,5) : []; if(!summaries.length){ switchContinuitySummaryList.innerHTML='<li><span class="muted">No recent switch continuity</span><strong>0</strong></li>'; return; } switchContinuitySummaryList.innerHTML=summaries.map(item=>{ const cls=item.status === 'critical' ? 'critical' : (item.status === 'watch' ? 'warn' : 'info'); const meta=[item.transition || (item.finalModel || '-'), item.sourceLabel || item.source || '-', item.alignmentUsed ? 'aligned' : '', item.cascadeTriggered ? 'cascade' : '', item.latencyMs !== undefined ? (fmt(item.latencyMs)+' ms') : ''].filter(Boolean).join(' \xB7 '); const action=item.action ? '<div class="muted">'+esc(item.action)+'</div>' : ''; return '<li><span><span class="pill '+esc(cls)+'">'+esc(item.status || 'unknown')+'</span> <strong>'+esc(item.headline || item.requestId || '-')+'</strong><div class="muted">'+esc(meta)+'</div>'+action+'</span><button type="button" data-request="'+esc(item.requestId || '')+'">View</button></li>'; }).join('');}function renderRouteHandoffSummaries(items){ const handoffs=Array.isArray(items) ? items.slice(0,5) : []; if(!handoffs.length){ routeHandoffSummaryList.innerHTML='<li><span class="muted">No recent route handoff</span><strong>0</strong></li>'; return; } routeHandoffSummaryList.innerHTML=handoffs.map(item=>{ const stages=Array.isArray(item.stages) ? item.stages.map(stage=>stage.stage+':'+stage.status).join(' \xB7 ') : ''; const status=item.blocked ? 'watch' : 'ok'; return '<li><span><strong>'+esc(item.headline || item.requestId || '-')+'</strong><div class="muted">'+esc(stages || item.action || '-')+'</div></span><button type="button" data-request="'+esc(item.requestId || '')+'">'+esc(status)+'</button></li>'; }).join('');}function renderAnomalies(anomalies,health){ const status=health?.status || 'idle'; const message=health?.message || 'No governance traces yet.'; const actions=Array.isArray(health?.actions) ? health.actions : []; healthSummary.className='alert '+esc(status === 'critical' ? 'critical' : (status === 'watch' ? 'warn' : 'info')); healthSummary.innerHTML='<strong>Health: '+esc(status)+'</strong><div>'+esc(message)+'</div>'+ (actions.length ? '<ul class="mini-list">'+actions.map(action=>'<li><button type="button" data-health-action="'+esc(action)+'">'+esc(action)+'</button></li>').join('')+'</ul>' : ''); 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 applyHealthAction(action){ const text=String(action || '').toLowerCase(); const routeReasonInput=document.getElementById('routeReason'); const cascadeSelect=document.getElementById('cascadeTriggered'); const shadowSelect=document.getElementById('shadowChecked'); if(text.includes('cascade')){ cascadeSelect.value='true'; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered cascade traces'; } else if(text.includes('shadow')){ shadowSelect.value='true'; cascadeSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered shadow traces'; } else { cascadeSelect.value=''; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: showing recent traces'; } loadTraces(); document.getElementById('traceTable').scrollIntoView({ behavior:'smooth', block:'start' });}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes,healthRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query), fetch('/api/governance/health'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); const healthData=await healthRes.json(); const health=healthData.health || metricsData.health; renderMetrics(metricsData.metrics || {},health,metricsData.outcome || {}); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || [],health); renderRoutingTuning(health?.routingTuning || []); renderQualityEvidence(metricsData.qualityEvidence || {}); renderTaskComparison(metricsData.taskComparison || {}); renderBenchmarkSummary(metricsData.taskComparison || {},metricsData.qualityEvidence || {}); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderOutcomeGroups(routeOutcomeRanking,metricsData.outcome?.byRouteReason || [],'No route outcomes'); renderOutcomeGroups(modelOutcomeRanking,metricsData.outcome?.byFinalModel || [],'No model outcomes'); renderOutcomeGroups(intentOutcomeRanking,metricsData.outcome?.bySemanticIntent || [],'No intent outcomes'); renderTrendTable(metricsData || {}); const traces=data.traces || []; renderRouteDecisionSummaries(data.routeDecisions || traces.map(t=>t.decisionSummary).filter(Boolean)); renderSwitchContinuitySummaries(data.switchContinuity || traces.map(t=>t.switchSummary).filter(Boolean)); renderRouteHandoffSummaries(data.routeHandoffs || traces.map(t=>t.handoffSummary ? { requestId:t.requestId, sessionKey:t.sessionKey, ...t.handoffSummary } : null).filter(Boolean)); if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftHeroBtn').addEventListener('click',loadConfigDraft);document.getElementById('previewConfigDraftHeroBtn').addEventListener('click',previewConfigDraft);document.getElementById('refreshStatusHeroBtn').addEventListener('click',loadServiceStatus);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);document.getElementById('saveCalibrationBtn').addEventListener('click',saveBenchmarkCalibration);document.getElementById('probeModelPoolBtn').addEventListener('click',probeModelPoolHealth);routeDecisionSummaryList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn && btn.dataset.request){ loadDetail(btn.dataset.request); } });switchContinuitySummaryList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn && btn.dataset.request){ loadDetail(btn.dataset.request); } });routeHandoffSummaryList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn && btn.dataset.request){ loadDetail(btn.dataset.request); } });tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadServiceStatus();loadConfigDraft();loadCompiledModels();loadBenchmarkHistory();loadExports();loadArchives();loadTraces();</script>` + renderWorkbenchDocumentEnd();
5953
6165
  }
5954
6166
  var init_workbench = __esm({
5955
6167
  "src/ui/workbench.ts"() {
@@ -6015,7 +6227,7 @@ function parseOfflineEvaluationInputs(payload) {
6015
6227
  function clamp(value) {
6016
6228
  return Math.max(0, Math.min(1, Number(value.toFixed(4))));
6017
6229
  }
6018
- function average2(values) {
6230
+ function average3(values) {
6019
6231
  if (!values.length) {
6020
6232
  return 0;
6021
6233
  }
@@ -6236,7 +6448,7 @@ function averageDimensionScores(runs) {
6236
6448
  }
6237
6449
  }
6238
6450
  return Object.fromEntries(
6239
- Object.entries(grouped).map(([id, values]) => [id, average2(values)]).sort(([left], [right]) => left.localeCompare(right))
6451
+ Object.entries(grouped).map(([id, values]) => [id, average3(values)]).sort(([left], [right]) => left.localeCompare(right))
6240
6452
  );
6241
6453
  }
6242
6454
  function buildCalibration(input3, qualityScore) {
@@ -6245,7 +6457,7 @@ function buildCalibration(input3, qualityScore) {
6245
6457
  if (!scores.length && !input3.calibrationNotes && !findings.length) {
6246
6458
  return void 0;
6247
6459
  }
6248
- const averageScore = scores.length ? average2(scores) : void 0;
6460
+ const averageScore = scores.length ? average3(scores) : void 0;
6249
6461
  const deltaFromQuality = averageScore === void 0 ? void 0 : Number((averageScore - qualityScore).toFixed(4));
6250
6462
  if (deltaFromQuality !== void 0 && Math.abs(deltaFromQuality) >= 0.25) {
6251
6463
  findings.push(`calibration_disagreement:${deltaFromQuality}`);
@@ -6338,9 +6550,9 @@ function summarizeGroup(key, runs) {
6338
6550
  totalRuns: runs.length,
6339
6551
  passedRuns: runs.filter((run2) => run2.passed).length,
6340
6552
  passRate: rate2(runs.filter((run2) => run2.passed).length, runs.length),
6341
- averageQualityScore: average2(runs.map((run2) => run2.qualityScore)),
6342
- averageSpeedScore: average2(runs.map((run2) => run2.speedScore)),
6343
- averageLatencyMs: latencies.length ? Number(average2(latencies).toFixed(2)) : 0,
6553
+ averageQualityScore: average3(runs.map((run2) => run2.qualityScore)),
6554
+ averageSpeedScore: average3(runs.map((run2) => run2.speedScore)),
6555
+ averageLatencyMs: latencies.length ? Number(average3(latencies).toFixed(2)) : 0,
6344
6556
  averageDimensionScores: averageDimensionScores(runs)
6345
6557
  };
6346
6558
  }
@@ -6391,10 +6603,10 @@ function summarizeCalibration(runs) {
6391
6603
  }));
6392
6604
  return {
6393
6605
  calibratedRuns: calibratedRuns.length,
6394
- averageHumanScore: average2(humanScores),
6395
- averageJudgeScore: average2(judgeScores),
6396
- averageCalibrationScore: average2(averageScores),
6397
- averageRubricDelta: average2(deltas),
6606
+ averageHumanScore: average3(humanScores),
6607
+ averageJudgeScore: average3(judgeScores),
6608
+ averageCalibrationScore: average3(averageScores),
6609
+ averageRubricDelta: average3(deltas),
6398
6610
  highDisagreementRuns
6399
6611
  };
6400
6612
  }
@@ -6413,9 +6625,9 @@ function runOfflineTaskEvaluation(inputs, tasks = DEFAULT_OFFLINE_EVALUATION_TAS
6413
6625
  evaluatedRuns: runs.length,
6414
6626
  missingTaskIds,
6415
6627
  passRate: rate2(runs.filter((run2) => run2.passed).length, runs.length),
6416
- averageQualityScore: average2(runs.map((run2) => run2.qualityScore)),
6417
- averageSpeedScore: average2(runs.map((run2) => run2.speedScore)),
6418
- averageLatencyMs: latencies.length ? Number(average2(latencies).toFixed(2)) : 0,
6628
+ averageQualityScore: average3(runs.map((run2) => run2.qualityScore)),
6629
+ averageSpeedScore: average3(runs.map((run2) => run2.speedScore)),
6630
+ averageLatencyMs: latencies.length ? Number(average3(latencies).toFixed(2)) : 0,
6419
6631
  averageDimensionScores: averageDimensionScores(runs),
6420
6632
  calibrationSummary: summarizeCalibration(runs),
6421
6633
  bestRunsByTask,
@@ -8262,6 +8474,22 @@ var init_server = __esm({
8262
8474
  ...getGovernanceMetricsReport(readGovernanceMetricsQuery(req.query))
8263
8475
  };
8264
8476
  });
8477
+ server.app.get("/api/governance/routing-advisor", async (req) => {
8478
+ const rawConfig = await readActiveConfig();
8479
+ const normalized = normalizeAndValidateConfig(rawConfig ?? {}).config;
8480
+ const smartRouter = deriveRuntimeSmartRouterConfig(normalized, rawConfig);
8481
+ const historyLimit = req.query?.limit ? Number(req.query.limit) : void 0;
8482
+ const advisor = buildRoutingAdvisorSummary({
8483
+ candidates: smartRouter?.candidates ?? [],
8484
+ historyLimit: Number.isFinite(historyLimit) && historyLimit > 0 ? historyLimit : smartRouter?.adaptive?.history_limit
8485
+ });
8486
+ return {
8487
+ enabled: Boolean(smartRouter?.enabled),
8488
+ candidateCount: smartRouter?.candidates?.length ?? 0,
8489
+ generatedAt: Date.now(),
8490
+ advisor: advisor ?? null
8491
+ };
8492
+ });
8265
8493
  server.app.get("/api/governance/health", async (req) => {
8266
8494
  const report2 = getGovernanceMetricsReport(readGovernanceMetricsQuery(req.query));
8267
8495
  return {
@@ -8868,6 +9096,7 @@ var init_management_routes = __esm({
8868
9096
  { method: "GET", path: "/api/transformers", domain: "models", requiredScope: "read-only" },
8869
9097
  { method: "GET", path: "/api/governance/health", domain: "governance", requiredScope: "read-only" },
8870
9098
  { method: "GET", path: "/api/governance/metrics", domain: "governance", requiredScope: "read-only" },
9099
+ { method: "GET", path: "/api/governance/routing-advisor", domain: "governance", requiredScope: "read-only" },
8871
9100
  { method: "GET", path: "/api/governance/metrics/export", domain: "governance", requiredScope: "read-only" },
8872
9101
  { method: "GET", path: "/api/governance/metrics/exports", domain: "governance", requiredScope: "read-only" },
8873
9102
  { method: "GET", path: "/api/governance/traces", domain: "governance", requiredScope: "read-only" },
@@ -9916,7 +10145,7 @@ Important: Respond ONLY with the JSON, no additional text.`;
9916
10145
  });
9917
10146
 
9918
10147
  // src/trigger/smart-router.ts
9919
- var import_lru_cache7, SmartRouterSelector, smartRouterSelector;
10148
+ var import_lru_cache7, COLLABORATION_MODES, SmartRouterSelector, smartRouterSelector;
9920
10149
  var init_smart_router = __esm({
9921
10150
  "src/trigger/smart-router.ts"() {
9922
10151
  "use strict";
@@ -9925,6 +10154,13 @@ var init_smart_router = __esm({
9925
10154
  init_log();
9926
10155
  init_message_ir();
9927
10156
  init_anthropic();
10157
+ init_routing_advisor();
10158
+ COLLABORATION_MODES = [
10159
+ "route_only",
10160
+ "verify_only",
10161
+ "compare_then_arbiter",
10162
+ "cascade_on_evidence"
10163
+ ];
9928
10164
  SmartRouterSelector = class {
9929
10165
  cache;
9930
10166
  constructor() {
@@ -9938,8 +10174,8 @@ var init_smart_router = __esm({
9938
10174
  * 生成缓存 key(基于请求文本 + router_model + 候选模型列表)
9939
10175
  * 注意:包含 router_model 以防止切换路由模型后命中旧缓存
9940
10176
  */
9941
- generateCacheKey(text, routerModel, candidates) {
9942
- const content = `${text}:${routerModel}:${candidates.map((c) => c.model).join(",")}`;
10177
+ generateCacheKey(text, routerModel, candidates, advisorSignature, budgetSignature) {
10178
+ const content = `${text}:${routerModel}:${candidates.map((c) => c.model).join(",")}:${advisorSignature ?? "static"}:${budgetSignature ?? "no-budget"}`;
9943
10179
  let hash = 0;
9944
10180
  for (let i = 0; i < content.length; i++) {
9945
10181
  const char = content.charCodeAt(i);
@@ -9951,8 +10187,13 @@ var init_smart_router = __esm({
9951
10187
  /**
9952
10188
  * 构建候选模型列表字符串
9953
10189
  */
9954
- buildCandidatesList(candidates) {
9955
- return candidates.map((c, i) => `${i + 1}. ${c.model} - ${c.description}`).join("\n");
10190
+ buildCandidatesList(candidates, advisor) {
10191
+ const profiles = new Map(advisor?.candidateProfiles.map((profile) => [profile.model, profile]));
10192
+ return orderCandidatesByRoutingAdvisor(candidates, advisor).map((c, i) => {
10193
+ const profile = profiles.get(c.model);
10194
+ const evidence = profile?.profileSource === "history" && profile.evidence.length ? ` | recent profile: ${profile.evidence.join("; ")}` : "";
10195
+ return `${i + 1}. ${c.model} - ${c.description}${evidence}`;
10196
+ }).join("\n");
9956
10197
  }
9957
10198
  /**
9958
10199
  * 构建完整 prompt
@@ -9974,22 +10215,54 @@ ${hint.taskSummary}
9974
10215
  ).join("\n")
9975
10216
  );
9976
10217
  }
10218
+ if (hint?.routingAdvisor) {
10219
+ sections.push(
10220
+ "Adaptive routing evidence from recent governance traces:\n" + formatRoutingAdvisorPromptSection(hint.routingAdvisor)
10221
+ );
10222
+ }
10223
+ if (hint?.routingBudget?.latencyBudgetMs || hint?.routingBudget?.confidenceThreshold) {
10224
+ sections.push(
10225
+ "Routing budget:\n" + [
10226
+ hint.routingBudget.latencyBudgetMs ? `- latency budget: ${hint.routingBudget.latencyBudgetMs}ms` : void 0,
10227
+ hint.routingBudget.confidenceThreshold ? `- confidence threshold: ${hint.routingBudget.confidenceThreshold}` : void 0,
10228
+ hint.routingBudget.source ? `- source: ${hint.routingBudget.source}` : void 0,
10229
+ "- If recent evidence shows a candidate repeatedly exceeds the latency budget, prefer a faster candidate unless the task clearly needs a deep model.",
10230
+ "- If your confidence would fall below the threshold, choose the safer or higher-quality model and explain why."
10231
+ ].filter(Boolean).join("\n")
10232
+ );
10233
+ }
10234
+ if (hint?.collaboration) {
10235
+ sections.push(
10236
+ "Collaboration mode policy:\n" + [
10237
+ `- default mode: ${hint.collaboration.mode}`,
10238
+ `- allowed modes: ${hint.collaboration.allowedModes.join(", ")}`,
10239
+ hint.collaboration.confidenceThreshold ? `- collaboration confidence threshold: ${hint.collaboration.confidenceThreshold}` : void 0,
10240
+ `- source: ${hint.collaboration.source}`,
10241
+ "- route_only means use one model only.",
10242
+ "- verify_only means route to one model and ask downstream governance/verifier to pay extra attention.",
10243
+ "- compare_then_arbiter and cascade_on_evidence are planning modes in this release; do not assume parallel execution unless explicitly enabled by future runtime support."
10244
+ ].filter(Boolean).join("\n")
10245
+ );
10246
+ }
9977
10247
  sections.push(
9978
10248
  `User request:
9979
10249
  """
9980
10250
  ${text}
9981
10251
  """`,
9982
10252
  `Available models:
9983
- ${this.buildCandidatesList(candidates)}`,
10253
+ ${this.buildCandidatesList(candidates, hint?.routingAdvisor)}`,
9984
10254
  `Select the most appropriate model and respond in the following JSON format ONLY:
9985
10255
  {
9986
10256
  "model": "<exact model identifier from the list>",
9987
10257
  "confidence": <0.0-1.0>,
10258
+ "routingMode": "balanced|quality|speed",
10259
+ "collaborationMode": "route_only|verify_only|compare_then_arbiter|cascade_on_evidence",
9988
10260
  "reasoning": "<brief explanation>"
9989
10261
  }
9990
10262
 
9991
10263
  Important:
9992
10264
  - The "model" field MUST be one of the exact identifiers listed above
10265
+ - If adaptive evidence is present, use it to balance user intent, recent quality, recent latency, and reliability
9993
10266
  - Respond ONLY with the JSON, no additional text`
9994
10267
  );
9995
10268
  return sections.join("\n\n");
@@ -10013,14 +10286,40 @@ Important:
10013
10286
  if (!config.candidates || config.candidates.length < 2) {
10014
10287
  return null;
10015
10288
  }
10016
- const cacheKey = this.generateCacheKey(text, config.router_model, config.candidates);
10289
+ const routingAdvisor = config.adaptive?.enabled === false ? void 0 : hint?.routingAdvisor ?? buildRoutingAdvisorSummary({
10290
+ candidates: config.candidates,
10291
+ historyLimit: config.adaptive?.history_limit
10292
+ });
10293
+ const routingBudget = hint?.routingBudget ?? {
10294
+ latencyBudgetMs: config.routing_budget?.latency_budget_ms,
10295
+ confidenceThreshold: config.routing_budget?.confidence_threshold,
10296
+ source: config.routing_budget ? "config" : void 0
10297
+ };
10298
+ const collaboration = this.buildCollaborationHint(config, hint);
10299
+ const effectiveHint = routingAdvisor ? {
10300
+ ...hint ?? {},
10301
+ routingAdvisor,
10302
+ routingBudget,
10303
+ collaboration
10304
+ } : {
10305
+ ...hint ?? {},
10306
+ routingBudget,
10307
+ collaboration
10308
+ };
10309
+ const cacheKey = this.generateCacheKey(
10310
+ text,
10311
+ config.router_model,
10312
+ config.candidates,
10313
+ routingAdvisor?.signature,
10314
+ this.buildBudgetSignature(routingBudget, collaboration)
10315
+ );
10017
10316
  const cached = this.cache.get(cacheKey);
10018
10317
  if (cached) {
10019
10318
  return cached;
10020
10319
  }
10021
10320
  try {
10022
10321
  const fetchImpl = fetchFn || fetch;
10023
- const prompt = this.buildPrompt(text, config.candidates, hint);
10322
+ const prompt = this.buildPrompt(text, config.candidates, effectiveHint);
10024
10323
  const response = await fetchImpl(`http://127.0.0.1:${port}/v1/messages`, {
10025
10324
  method: "POST",
10026
10325
  headers: {
@@ -10054,8 +10353,14 @@ Important:
10054
10353
  logWarn(`[SmartRouter] LLM returned unknown model: "${parsed.model}"`);
10055
10354
  return null;
10056
10355
  }
10057
- this.cache.set(cacheKey, parsed, { ttl: config.cache_ttl ?? 6e5 });
10058
- return parsed;
10356
+ const result = this.applyRoutingBudgetGuard({
10357
+ ...parsed,
10358
+ routingMode: parsed.routingMode ?? routingAdvisor?.routeMode,
10359
+ collaborationMode: this.normalizeCollaborationMode(parsed.collaborationMode, collaboration),
10360
+ routingEvidence: routingAdvisor?.evidence.slice(0, 6)
10361
+ }, routingAdvisor, routingBudget, collaboration, validModels);
10362
+ this.cache.set(cacheKey, result, { ttl: config.cache_ttl ?? 6e5 });
10363
+ return result;
10059
10364
  } catch (error) {
10060
10365
  logError("[SmartRouter] Error selecting model:", error);
10061
10366
  return null;
@@ -10067,6 +10372,94 @@ Important:
10067
10372
  clearCache() {
10068
10373
  this.cache.clear();
10069
10374
  }
10375
+ buildBudgetSignature(budget, collaboration) {
10376
+ if (!budget?.latencyBudgetMs && !budget?.confidenceThreshold && !collaboration) {
10377
+ return void 0;
10378
+ }
10379
+ return [
10380
+ budget.latencyBudgetMs ?? "-",
10381
+ budget.confidenceThreshold ?? "-",
10382
+ budget.source ?? "config",
10383
+ collaboration?.mode ?? "route_only",
10384
+ collaboration?.allowedModes.join("+") ?? "route_only"
10385
+ ].join(":");
10386
+ }
10387
+ buildCollaborationHint(config, hint) {
10388
+ const configured = config.collaboration;
10389
+ const existing = hint?.collaboration;
10390
+ if (existing) {
10391
+ return existing;
10392
+ }
10393
+ const allowedModes = (configured?.allowed_modes?.length ? configured.allowed_modes : configured?.mode ? [configured.mode] : ["route_only"]).filter((mode2) => COLLABORATION_MODES.includes(mode2));
10394
+ const normalizedAllowedModes = allowedModes.length ? Array.from(new Set(allowedModes)) : ["route_only"];
10395
+ const mode = configured?.mode && normalizedAllowedModes.includes(configured.mode) ? configured.mode : normalizedAllowedModes[0] ?? "route_only";
10396
+ return {
10397
+ mode,
10398
+ allowedModes: normalizedAllowedModes,
10399
+ confidenceThreshold: configured?.confidence_threshold,
10400
+ source: configured ? "config" : "default"
10401
+ };
10402
+ }
10403
+ normalizeCollaborationMode(mode, collaboration) {
10404
+ const requested = typeof mode === "string" && COLLABORATION_MODES.includes(mode) ? mode : void 0;
10405
+ if (requested && collaboration?.allowedModes.includes(requested)) {
10406
+ return requested;
10407
+ }
10408
+ return collaboration?.mode ?? "route_only";
10409
+ }
10410
+ applyRoutingBudgetGuard(result, advisor, budget, collaboration, validModels) {
10411
+ const evidence = [...result.routingEvidence ?? []];
10412
+ let next = { ...result };
10413
+ if (advisor && (budget?.latencyBudgetMs || budget?.confidenceThreshold)) {
10414
+ const profiles = new Map(advisor.candidateProfiles.map((profile) => [profile.model, profile]));
10415
+ const selectedProfile = profiles.get(next.model);
10416
+ const fastestProfile = advisor.fastestModel ? profiles.get(advisor.fastestModel) : void 0;
10417
+ if (budget.latencyBudgetMs && selectedProfile?.averageLatencyMs && selectedProfile.averageLatencyMs > budget.latencyBudgetMs && fastestProfile?.averageLatencyMs && fastestProfile.averageLatencyMs <= budget.latencyBudgetMs && validModels.includes(fastestProfile.model)) {
10418
+ evidence.push(
10419
+ `latency budget guard: ${selectedProfile.model} avg ${selectedProfile.averageLatencyMs}ms > ${budget.latencyBudgetMs}ms; using ${fastestProfile.model} avg ${fastestProfile.averageLatencyMs}ms`
10420
+ );
10421
+ next = {
10422
+ ...next,
10423
+ model: fastestProfile.model,
10424
+ routingMode: "speed",
10425
+ reasoning: [
10426
+ next.reasoning,
10427
+ `Latency budget favored ${fastestProfile.model}.`
10428
+ ].filter(Boolean).join(" ")
10429
+ };
10430
+ }
10431
+ const confidenceThreshold = budget.confidenceThreshold;
10432
+ const qualityProfile = advisor.qualityModel ? profiles.get(advisor.qualityModel) : void 0;
10433
+ if (confidenceThreshold && next.confidence < confidenceThreshold && qualityProfile && qualityProfile.model !== next.model && validModels.includes(qualityProfile.model)) {
10434
+ evidence.push(
10435
+ `confidence guard: ${next.confidence} < ${confidenceThreshold}; using quality-backed ${qualityProfile.model}`
10436
+ );
10437
+ next = {
10438
+ ...next,
10439
+ model: qualityProfile.model,
10440
+ routingMode: "quality",
10441
+ reasoning: [
10442
+ next.reasoning,
10443
+ `Confidence threshold favored quality-backed ${qualityProfile.model}.`
10444
+ ].filter(Boolean).join(" ")
10445
+ };
10446
+ }
10447
+ }
10448
+ return {
10449
+ ...next,
10450
+ collaborationMode: this.applyCollaborationMode(next, collaboration, evidence),
10451
+ routingEvidence: evidence
10452
+ };
10453
+ }
10454
+ applyCollaborationMode(result, collaboration, evidence) {
10455
+ const mode = this.normalizeCollaborationMode(result.collaborationMode, collaboration);
10456
+ const threshold = collaboration?.confidenceThreshold;
10457
+ if (threshold && result.confidence < threshold && mode === "route_only" && collaboration.allowedModes.includes("verify_only")) {
10458
+ evidence.push(`collaboration guard: confidence ${result.confidence} < ${threshold}; using verify_only`);
10459
+ return "verify_only";
10460
+ }
10461
+ return mode;
10462
+ }
10070
10463
  };
10071
10464
  smartRouterSelector = new SmartRouterSelector();
10072
10465
  }
@@ -10187,7 +10580,34 @@ var init_selector = __esm({
10187
10580
  routeSource: "sticky_correction"
10188
10581
  };
10189
10582
  }
10190
- buildSmartRouterHint(text, rules) {
10583
+ readPositiveNumber(value) {
10584
+ if (typeof value === "number" && Number.isFinite(value) && value > 0) {
10585
+ return value;
10586
+ }
10587
+ if (typeof value === "string" && value.trim()) {
10588
+ const parsed = Number(value);
10589
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : void 0;
10590
+ }
10591
+ return void 0;
10592
+ }
10593
+ buildRoutingBudgetHint(req, smartRouterConfig) {
10594
+ const metadata = req.body?.metadata ?? {};
10595
+ const metadataLatency = this.readPositiveNumber(metadata.ctr_latency_budget_ms);
10596
+ const metadataConfidence = this.readPositiveNumber(metadata.ctr_confidence_threshold);
10597
+ const configLatency = this.readPositiveNumber(smartRouterConfig?.routing_budget?.latency_budget_ms);
10598
+ const configConfidence = this.readPositiveNumber(smartRouterConfig?.routing_budget?.confidence_threshold);
10599
+ const latencyBudgetMs = metadataLatency ?? configLatency;
10600
+ const confidenceThreshold = metadataConfidence ?? configConfidence;
10601
+ if (!latencyBudgetMs && !confidenceThreshold) {
10602
+ return void 0;
10603
+ }
10604
+ return {
10605
+ latencyBudgetMs,
10606
+ confidenceThreshold,
10607
+ source: metadataLatency || metadataConfidence ? "metadata" : "config"
10608
+ };
10609
+ }
10610
+ buildSmartRouterHint(text, rules, req, smartRouterConfig) {
10191
10611
  return {
10192
10612
  taskSummary: text.slice(0, 240),
10193
10613
  topRouteCandidates: this.sortRulesByPriority(rules).filter((rule) => rule.description).slice(0, 3).map((rule) => ({
@@ -10195,7 +10615,8 @@ var init_selector = __esm({
10195
10615
  model: rule.model,
10196
10616
  description: rule.description,
10197
10617
  confidence: void 0
10198
- }))
10618
+ })),
10619
+ routingBudget: this.buildRoutingBudgetHint(req, smartRouterConfig)
10199
10620
  };
10200
10621
  }
10201
10622
  /**
@@ -10350,7 +10771,7 @@ var init_selector = __esm({
10350
10771
  void 0,
10351
10772
  apiKey,
10352
10773
  timeoutMs,
10353
- this.buildSmartRouterHint(text, routingRules)
10774
+ this.buildSmartRouterHint(text, routingRules, req, smartRouterConfig)
10354
10775
  );
10355
10776
  if (smartResult) {
10356
10777
  log(`[SmartRouter] Selected model "${smartResult.model}" (confidence: ${smartResult.confidence})`);
@@ -10360,7 +10781,11 @@ var init_selector = __esm({
10360
10781
  confidence: smartResult.confidence,
10361
10782
  analysisTime: Date.now() - startTime,
10362
10783
  analyzedText: text,
10363
- routeSource: "smart_router"
10784
+ routeSource: "smart_router",
10785
+ routingMode: smartResult.routingMode,
10786
+ collaborationMode: smartResult.collaborationMode,
10787
+ reasoning: smartResult.reasoning,
10788
+ routingEvidence: smartResult.routingEvidence
10364
10789
  };
10365
10790
  return this.applyStickyCorrection(smartSelection, stickyCorrection, appConfig) ?? smartSelection;
10366
10791
  }
@@ -10606,6 +11031,10 @@ var init_trigger = __esm({
10606
11031
  ruleName: result.rule?.name,
10607
11032
  confidence: result.confidence,
10608
11033
  model: result.model,
11034
+ reasoning: result.reasoning,
11035
+ routingMode: result.routingMode,
11036
+ collaborationMode: result.collaborationMode,
11037
+ routingEvidence: result.routingEvidence,
10609
11038
  fallbackReason: result.matched ? void 0 : "SmartRouter did not match; request continued to the basic Router fallback path."
10610
11039
  };
10611
11040
  if (result.routeSource === "smart_rule" && result.rule?.name) {
@@ -10617,6 +11046,15 @@ var init_trigger = __esm({
10617
11046
  appendTraceReason(req.governanceTrace, "sticky_correction");
10618
11047
  } else if (result.routeSource === "smart_router") {
10619
11048
  appendTraceReason(req.governanceTrace, "smart_router");
11049
+ if (result.routingMode) {
11050
+ appendTraceReason(req.governanceTrace, `smart_router_mode:${result.routingMode}`);
11051
+ }
11052
+ if (result.collaborationMode) {
11053
+ appendTraceReason(req.governanceTrace, `smart_router_collaboration:${result.collaborationMode}`);
11054
+ }
11055
+ if (result.routingEvidence?.length) {
11056
+ appendTraceReason(req.governanceTrace, "smart_router_adaptive_feedback");
11057
+ }
10620
11058
  } else {
10621
11059
  appendTraceReason(req.governanceTrace, "smart_router:no_match");
10622
11060
  }