@peterwangze/claude-trigger-router 1.1.1 → 1.1.2

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
@@ -124,6 +124,389 @@ var init_constants = __esm({
124
124
  }
125
125
  });
126
126
 
127
+ // src/auth/api-keys.ts
128
+ function createSecret() {
129
+ const token = (0, import_crypto.randomBytes)(24).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
130
+ return `ctr_${token}`;
131
+ }
132
+ function createKeyId() {
133
+ return `key_${(0, import_crypto.randomBytes)(8).toString("hex")}`;
134
+ }
135
+ function hashApiKey(secret) {
136
+ return (0, import_crypto.createHash)("sha256").update(secret).digest("hex");
137
+ }
138
+ function safeEqual(left, right) {
139
+ const leftBuffer = Buffer.from(left);
140
+ const rightBuffer = Buffer.from(right);
141
+ if (leftBuffer.length !== rightBuffer.length) {
142
+ return false;
143
+ }
144
+ return (0, import_crypto.timingSafeEqual)(leftBuffer, rightBuffer);
145
+ }
146
+ function normalizeManagedApiKeyScopes(input3) {
147
+ if (!Array.isArray(input3) || input3.length === 0) {
148
+ return ["client"];
149
+ }
150
+ const scopes = Array.from(new Set(
151
+ input3.map((item) => String(item).trim()).filter((item) => VALID_SCOPES.includes(item))
152
+ ));
153
+ return scopes.length ? scopes : ["client"];
154
+ }
155
+ function validateManagedApiKeyScopes(input3) {
156
+ if (input3 === void 0) {
157
+ return [];
158
+ }
159
+ if (!Array.isArray(input3) || input3.length === 0) {
160
+ return ["scopes must be a non-empty array when provided"];
161
+ }
162
+ return input3.map((item) => String(item).trim()).filter((item) => !VALID_SCOPES.includes(item)).map((item) => `unsupported scope: ${item}`);
163
+ }
164
+ function validateManagedApiKeyQuota(input3) {
165
+ if (input3 === void 0) {
166
+ return [];
167
+ }
168
+ if (!input3 || typeof input3 !== "object" || Array.isArray(input3)) {
169
+ return ["quota must be an object when provided"];
170
+ }
171
+ const quota = input3;
172
+ return ["request_limit", "token_limit", "window_seconds"].filter((field) => quota[field] !== void 0).filter((field) => !Number.isInteger(quota[field]) || Number(quota[field]) <= 0).map((field) => `quota.${field} must be a positive integer`);
173
+ }
174
+ function createManagedApiKey(input3 = {}, now = /* @__PURE__ */ new Date()) {
175
+ const secret = createSecret();
176
+ const label = typeof input3.label === "string" && input3.label.trim() ? input3.label.trim() : "client key";
177
+ const record = {
178
+ id: createKeyId(),
179
+ label,
180
+ key_hash: hashApiKey(secret),
181
+ key_prefix: secret.slice(0, 8),
182
+ key_suffix: secret.slice(-6),
183
+ scopes: normalizeManagedApiKeyScopes(input3.scopes),
184
+ created_at: now.toISOString(),
185
+ ...typeof input3.expiresAt === "string" && input3.expiresAt.trim() ? { expires_at: input3.expiresAt.trim() } : {},
186
+ ...input3.quota ? { quota: input3.quota } : {}
187
+ };
188
+ return { secret, record };
189
+ }
190
+ function isManagedApiKeyActive(record, now = /* @__PURE__ */ new Date()) {
191
+ if (record.revoked_at) {
192
+ return false;
193
+ }
194
+ if (record.expires_at) {
195
+ const expiresAt = Date.parse(record.expires_at);
196
+ if (Number.isFinite(expiresAt) && expiresAt <= now.getTime()) {
197
+ return false;
198
+ }
199
+ }
200
+ return true;
201
+ }
202
+ function sanitizeManagedApiKey(record, now = /* @__PURE__ */ new Date()) {
203
+ return {
204
+ id: record.id,
205
+ label: record.label,
206
+ keyPrefix: record.key_prefix,
207
+ keySuffix: record.key_suffix,
208
+ scopes: record.scopes,
209
+ createdAt: record.created_at,
210
+ expiresAt: record.expires_at,
211
+ revokedAt: record.revoked_at,
212
+ active: isManagedApiKeyActive(record, now),
213
+ quota: record.quota
214
+ };
215
+ }
216
+ function listManagedApiKeys(config, now = /* @__PURE__ */ new Date()) {
217
+ return (config.Auth?.managed_keys ?? []).map((record) => sanitizeManagedApiKey(record, now));
218
+ }
219
+ function managedApiKeySummary(config, now = /* @__PURE__ */ new Date()) {
220
+ const keys = listManagedApiKeys(config, now);
221
+ return {
222
+ total: keys.length,
223
+ active: keys.filter((item) => item.active).length,
224
+ revoked: keys.filter((item) => item.revokedAt).length,
225
+ expired: keys.filter((item) => !item.active && !item.revokedAt).length
226
+ };
227
+ }
228
+ function scopeAllows(scopes, required) {
229
+ if (scopes.includes("admin")) {
230
+ return true;
231
+ }
232
+ if (required === "read-only") {
233
+ return scopes.includes("read-only");
234
+ }
235
+ if (required === "client") {
236
+ return scopes.includes("client");
237
+ }
238
+ return false;
239
+ }
240
+ function verifyApiKey(config, providedKey, required = "client", now = /* @__PURE__ */ new Date()) {
241
+ if (!providedKey) {
242
+ return { ok: false, reason: "missing" };
243
+ }
244
+ if (config.APIKEY && safeEqual(providedKey, config.APIKEY)) {
245
+ return {
246
+ ok: true,
247
+ source: "bootstrap",
248
+ scopes: ["admin"]
249
+ };
250
+ }
251
+ const providedHash = hashApiKey(providedKey);
252
+ const record = (config.Auth?.managed_keys ?? []).find((item) => safeEqual(item.key_hash, providedHash));
253
+ if (!record) {
254
+ return { ok: false, reason: "invalid" };
255
+ }
256
+ if (record.revoked_at) {
257
+ return { ok: false, source: "managed", keyId: record.id, reason: "revoked" };
258
+ }
259
+ if (!isManagedApiKeyActive(record, now)) {
260
+ return { ok: false, source: "managed", keyId: record.id, reason: "expired" };
261
+ }
262
+ if (!scopeAllows(record.scopes, required)) {
263
+ return { ok: false, source: "managed", keyId: record.id, reason: "insufficient_scope" };
264
+ }
265
+ return {
266
+ ok: true,
267
+ source: "managed",
268
+ keyId: record.id,
269
+ scopes: record.scopes,
270
+ quota: record.quota
271
+ };
272
+ }
273
+ function extractApiKeyFromHeaders(headers) {
274
+ const authHeader = headers?.authorization ?? headers?.Authorization;
275
+ const xApiKey = headers?.["x-api-key"] ?? headers?.["X-Api-Key"];
276
+ if (typeof authHeader === "string" && authHeader.startsWith("Bearer ")) {
277
+ return authHeader.slice(7);
278
+ }
279
+ if (Array.isArray(xApiKey)) {
280
+ return xApiKey[0];
281
+ }
282
+ return typeof xApiKey === "string" ? xApiKey : void 0;
283
+ }
284
+ var import_crypto, VALID_SCOPES, AuthAuditStore, authAuditStore, AuthQuotaUsageStore, authQuotaUsageStore;
285
+ var init_api_keys = __esm({
286
+ "src/auth/api-keys.ts"() {
287
+ "use strict";
288
+ import_crypto = require("crypto");
289
+ VALID_SCOPES = ["admin", "client", "read-only"];
290
+ AuthAuditStore = class {
291
+ constructor(max = 200) {
292
+ this.max = max;
293
+ }
294
+ events = [];
295
+ add(event2) {
296
+ const recorded = {
297
+ ...event2,
298
+ timestamp: event2.timestamp ?? (/* @__PURE__ */ new Date()).toISOString()
299
+ };
300
+ this.events.unshift(recorded);
301
+ if (this.events.length > this.max) {
302
+ this.events = this.events.slice(0, this.max);
303
+ }
304
+ return recorded;
305
+ }
306
+ list(limit = 50) {
307
+ return this.events.slice(0, Math.max(0, Math.min(limit, this.max))).map((event2) => ({
308
+ ...event2,
309
+ scopes: event2.scopes ? [...event2.scopes] : void 0
310
+ }));
311
+ }
312
+ summary() {
313
+ const total = this.events.length;
314
+ const denied = this.events.filter((event2) => event2.outcome === "denied").length;
315
+ const allowed = this.events.filter((event2) => event2.outcome === "allowed").length;
316
+ const skipped = this.events.filter((event2) => event2.outcome === "skipped").length;
317
+ const managed = this.events.filter((event2) => event2.source === "managed").length;
318
+ const bootstrap = this.events.filter((event2) => event2.source === "bootstrap").length;
319
+ const byReason = this.events.reduce((acc, event2) => {
320
+ const reason = event2.reason ?? event2.outcome;
321
+ acc[reason] = (acc[reason] ?? 0) + 1;
322
+ return acc;
323
+ }, {});
324
+ return {
325
+ total,
326
+ allowed,
327
+ denied,
328
+ skipped,
329
+ managed,
330
+ bootstrap,
331
+ byReason,
332
+ latestAt: this.events[0]?.timestamp
333
+ };
334
+ }
335
+ clear() {
336
+ this.events = [];
337
+ }
338
+ };
339
+ authAuditStore = new AuthAuditStore();
340
+ AuthQuotaUsageStore = class {
341
+ usage = /* @__PURE__ */ new Map();
342
+ hydrate(input3) {
343
+ if (!input3 || typeof input3 !== "object" || Array.isArray(input3)) {
344
+ return;
345
+ }
346
+ Object.entries(input3).forEach(([keyId, item]) => {
347
+ if (!item || typeof item !== "object") {
348
+ return;
349
+ }
350
+ const requests = Number(item.requests);
351
+ const tokens = Number(item.tokens);
352
+ const windowStartedAt = Date.parse(item.window_started_at);
353
+ const windowSeconds = Number(item.window_seconds);
354
+ if (!keyId || !Number.isFinite(requests) || requests < 0 || !Number.isFinite(tokens) || tokens < 0 || !Number.isFinite(windowStartedAt)) {
355
+ return;
356
+ }
357
+ const existing = this.usage.get(keyId);
358
+ if (existing && existing.windowStartedAt === windowStartedAt && existing.requests >= requests && existing.tokens >= tokens) {
359
+ return;
360
+ }
361
+ this.usage.set(keyId, {
362
+ requests: Math.floor(requests),
363
+ tokens: Math.floor(tokens),
364
+ windowStartedAt,
365
+ windowSeconds: Number.isInteger(windowSeconds) && windowSeconds > 0 ? windowSeconds : void 0
366
+ });
367
+ });
368
+ }
369
+ exportForConfig(now = /* @__PURE__ */ new Date()) {
370
+ const nowMs = now.getTime();
371
+ return Object.fromEntries(
372
+ Array.from(this.usage.entries()).map(([keyId, item]) => [
373
+ keyId,
374
+ {
375
+ requests: item.requests,
376
+ tokens: item.tokens,
377
+ window_started_at: new Date(item.windowStartedAt).toISOString(),
378
+ ...item.windowSeconds !== void 0 ? { window_seconds: item.windowSeconds } : {},
379
+ updated_at: new Date(nowMs).toISOString()
380
+ }
381
+ ])
382
+ );
383
+ }
384
+ resolveLimits(quota) {
385
+ const requestLimit = Number.isInteger(quota?.request_limit) && Number(quota?.request_limit) > 0 ? Number(quota?.request_limit) : void 0;
386
+ const tokenLimit = Number.isInteger(quota?.token_limit) && Number(quota?.token_limit) > 0 ? Number(quota?.token_limit) : void 0;
387
+ const windowSeconds = Number.isInteger(quota?.window_seconds) && Number(quota?.window_seconds) > 0 ? Number(quota?.window_seconds) : void 0;
388
+ return { requestLimit, tokenLimit, windowSeconds };
389
+ }
390
+ normalizeEntry(keyId, windowSeconds, now) {
391
+ const existing = this.usage.get(keyId);
392
+ if (!existing) {
393
+ return void 0;
394
+ }
395
+ const nowMs = now.getTime();
396
+ if (windowSeconds !== void 0 && (existing.windowSeconds !== windowSeconds || nowMs - existing.windowStartedAt >= windowSeconds * 1e3)) {
397
+ const reset = {
398
+ requests: 0,
399
+ tokens: 0,
400
+ windowStartedAt: nowMs,
401
+ windowSeconds
402
+ };
403
+ this.usage.set(keyId, reset);
404
+ return reset;
405
+ }
406
+ const current = { ...existing, windowSeconds };
407
+ if (existing.windowSeconds !== windowSeconds) {
408
+ this.usage.set(keyId, current);
409
+ }
410
+ return current;
411
+ }
412
+ toSnapshot(entry, quota, estimatedTokens) {
413
+ const { requestLimit, tokenLimit, windowSeconds } = this.resolveLimits(quota);
414
+ if (requestLimit === void 0 && tokenLimit === void 0) {
415
+ return void 0;
416
+ }
417
+ return {
418
+ requestLimit,
419
+ requestsUsed: entry?.requests ?? 0,
420
+ tokenLimit,
421
+ tokensUsed: entry?.tokens ?? 0,
422
+ windowSeconds,
423
+ windowStartedAt: entry?.windowStartedAt !== void 0 ? new Date(entry.windowStartedAt).toISOString() : void 0,
424
+ windowResetAt: entry?.windowStartedAt !== void 0 && windowSeconds !== void 0 ? new Date(entry.windowStartedAt + windowSeconds * 1e3).toISOString() : void 0,
425
+ estimatedTokens
426
+ };
427
+ }
428
+ consume(keyId, quota, estimatedTokens = 0, now = /* @__PURE__ */ new Date()) {
429
+ if (!keyId || !quota) {
430
+ return { ok: true };
431
+ }
432
+ const { requestLimit, tokenLimit, windowSeconds } = this.resolveLimits(quota);
433
+ if (requestLimit === void 0 && tokenLimit === void 0) {
434
+ return { ok: true };
435
+ }
436
+ const nowMs = now.getTime();
437
+ const current = this.normalizeEntry(keyId, windowSeconds, now) ?? { requests: 0, tokens: 0, windowStartedAt: nowMs, windowSeconds };
438
+ const tokensToAdd = Math.max(0, Math.ceil(estimatedTokens));
439
+ const currentSnapshot = this.toSnapshot(current, quota, tokensToAdd);
440
+ if (requestLimit !== void 0 && current.requests >= requestLimit) {
441
+ return {
442
+ ok: false,
443
+ reason: "request_quota_exceeded",
444
+ usage: currentSnapshot
445
+ };
446
+ }
447
+ if (tokenLimit !== void 0 && current.tokens + tokensToAdd > tokenLimit) {
448
+ return {
449
+ ok: false,
450
+ reason: "token_quota_exceeded",
451
+ usage: currentSnapshot
452
+ };
453
+ }
454
+ const next = {
455
+ requests: current.requests + 1,
456
+ tokens: current.tokens + tokensToAdd,
457
+ windowStartedAt: current.windowStartedAt,
458
+ windowSeconds
459
+ };
460
+ this.usage.set(keyId, next);
461
+ return {
462
+ ok: true,
463
+ usage: this.toSnapshot(next, quota, tokensToAdd)
464
+ };
465
+ }
466
+ snapshotForKey(keyId, quota, now = /* @__PURE__ */ new Date()) {
467
+ if (!keyId) {
468
+ return void 0;
469
+ }
470
+ const { requestLimit, tokenLimit, windowSeconds } = this.resolveLimits(quota);
471
+ if (requestLimit === void 0 && tokenLimit === void 0) {
472
+ return void 0;
473
+ }
474
+ return this.toSnapshot(this.normalizeEntry(keyId, windowSeconds, now), quota);
475
+ }
476
+ summary(now = /* @__PURE__ */ new Date()) {
477
+ const nowMs = now.getTime();
478
+ const entries = Array.from(this.usage.entries()).map(([key, item]) => {
479
+ if (item.windowSeconds !== void 0 && nowMs - item.windowStartedAt >= item.windowSeconds * 1e3) {
480
+ const reset = {
481
+ requests: 0,
482
+ tokens: 0,
483
+ windowStartedAt: nowMs,
484
+ windowSeconds: item.windowSeconds
485
+ };
486
+ this.usage.set(key, reset);
487
+ return [key, reset];
488
+ }
489
+ return [key, item];
490
+ });
491
+ const windowResetAts = entries.map(([, item]) => item.windowSeconds !== void 0 ? item.windowStartedAt + item.windowSeconds * 1e3 : void 0).filter((value) => value !== void 0);
492
+ const result = {
493
+ trackedKeys: entries.length,
494
+ requestsUsed: entries.reduce((total, [, item]) => total + item.requests, 0),
495
+ tokensUsed: entries.reduce((total, [, item]) => total + item.tokens, 0)
496
+ };
497
+ if (windowResetAts.length > 0) {
498
+ result.windowResetAt = new Date(Math.min(...windowResetAts)).toISOString();
499
+ }
500
+ return result;
501
+ }
502
+ clear() {
503
+ this.usage.clear();
504
+ }
505
+ };
506
+ authQuotaUsageStore = new AuthQuotaUsageStore();
507
+ }
508
+ });
509
+
127
510
  // src/models/schema.ts
128
511
  function trimTrailingSlash(value) {
129
512
  return value.replace(/\/+$/, "");
@@ -165,7 +548,7 @@ function normalizeEndpointPath(pathname, modelInterface) {
165
548
  if (!normalizedPath) {
166
549
  return "/v1/messages";
167
550
  }
168
- return `${normalizedPath}/messages`;
551
+ return normalizedPath;
169
552
  }
170
553
  if (lowerPath.endsWith("/chat/completions")) {
171
554
  return normalizedPath || "/chat/completions";
@@ -176,7 +559,7 @@ function normalizeEndpointPath(pathname, modelInterface) {
176
559
  if (!normalizedPath) {
177
560
  return "/v1/chat/completions";
178
561
  }
179
- return `${normalizedPath}/chat/completions`;
562
+ return normalizedPath;
180
563
  }
181
564
  function normalizeApiEndpoint(api, explicitInterface) {
182
565
  const trimmed = api?.trim() || "";
@@ -362,6 +745,160 @@ function buildCompiledCapabilities(item, modelInterface) {
362
745
  systemMessageStyle: modelInterface
363
746
  };
364
747
  }
748
+ function readMetadataString(metadata, key) {
749
+ const value = metadata?.[key];
750
+ return typeof value === "string" ? value.trim() : "";
751
+ }
752
+ function readMetadataNumber(metadata, key) {
753
+ const value = metadata?.[key];
754
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
755
+ }
756
+ function readMetadataBoolean(metadata, key) {
757
+ const value = metadata?.[key];
758
+ return typeof value === "boolean" ? value : void 0;
759
+ }
760
+ function buildRegistrationUpstreamIndex(config) {
761
+ const services = Array.isArray(config.Registration?.upstream_services) ? config.Registration?.upstream_services : [];
762
+ return new Map(
763
+ services.filter((service) => typeof service?.id === "string" && service.id.trim()).map((service) => [service.id.trim(), service])
764
+ );
765
+ }
766
+ function createUniqueEndpointId(preferredId, usedEndpointIds) {
767
+ let endpointId = preferredId;
768
+ let suffix = 2;
769
+ while (usedEndpointIds.has(endpointId)) {
770
+ endpointId = `${preferredId}-${suffix}`;
771
+ suffix += 1;
772
+ }
773
+ usedEndpointIds.add(endpointId);
774
+ return endpointId;
775
+ }
776
+ function createUniqueName(preferredName, usedNames) {
777
+ let name = preferredName;
778
+ let suffix = 2;
779
+ while (usedNames.has(name)) {
780
+ name = `${preferredName}_${suffix}`;
781
+ suffix += 1;
782
+ }
783
+ usedNames.add(name);
784
+ return name;
785
+ }
786
+ function sanitizeProviderName(value) {
787
+ const sanitized = value.trim().replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
788
+ return sanitized || "endpoint";
789
+ }
790
+ function buildCompiledModelRefFromPoolEndpoint(endpoint, thinking, compatibilityProfile) {
791
+ return {
792
+ id: endpoint.modelId,
793
+ providerName: endpoint.providerName,
794
+ modelName: endpoint.modelName,
795
+ interface: endpoint.interface,
796
+ protocol: endpoint.protocol,
797
+ compatibilityProfile,
798
+ dispatchFormat: getDispatchFormatForProfile(endpoint.protocol, compatibilityProfile),
799
+ thinking,
800
+ capabilities: endpoint.capabilities,
801
+ source: "registration",
802
+ modelPool: {
803
+ modelId: endpoint.modelId,
804
+ endpointId: endpoint.id,
805
+ strategy: "priority"
806
+ }
807
+ };
808
+ }
809
+ function buildRegistrationModelPools(config) {
810
+ const registration = config.Registration;
811
+ if (!registration?.enabled || !Array.isArray(registration.models) || registration.models.length === 0) {
812
+ return {
813
+ providers: [],
814
+ modelMap: {},
815
+ modelPools: {}
816
+ };
817
+ }
818
+ const upstreamServices = buildRegistrationUpstreamIndex(config);
819
+ const usedEndpointIds = /* @__PURE__ */ new Set();
820
+ const usedProviderNames = /* @__PURE__ */ new Set();
821
+ const providers = [];
822
+ const modelMap = {};
823
+ const pools = {};
824
+ registration.models.forEach((rawItem, index) => {
825
+ const item = normalizeModelEndpointConfig(rawItem);
826
+ if (!item.id) {
827
+ return;
828
+ }
829
+ const upstreamServiceId = readMetadataString(item.metadata, "upstream_service_id");
830
+ const upstreamService = upstreamServiceId ? upstreamServices.get(upstreamServiceId) : void 0;
831
+ const warnings = [];
832
+ if (upstreamServiceId && !upstreamService) {
833
+ warnings.push(`Registration.models[${index}].metadata.upstream_service_id references missing upstream service "${upstreamServiceId}".`);
834
+ }
835
+ const modelInterface = getModelInterface(item) || "openai";
836
+ const endpointId = createUniqueEndpointId(
837
+ readMetadataString(item.metadata, "pool_endpoint_id") || (upstreamServiceId ? `${item.id}@${upstreamServiceId}` : `${item.id}@registration-${index + 1}`),
838
+ usedEndpointIds
839
+ );
840
+ const providerName = createUniqueName(
841
+ `registration__${sanitizeProviderName(endpointId)}`,
842
+ usedProviderNames
843
+ );
844
+ const poolPriority = readMetadataNumber(item.metadata, "pool_priority") ?? index + 1;
845
+ const enabled = readMetadataBoolean(item.metadata, "pool_enabled") ?? true;
846
+ const compatibilityProfile = inferCompatibilityProfile(item, modelInterface);
847
+ const capabilities = buildCompiledCapabilities(item, modelInterface);
848
+ const endpoint = {
849
+ id: endpointId,
850
+ modelId: item.id,
851
+ modelName: item.model,
852
+ providerName,
853
+ legacyRef: `${providerName},${item.model}`,
854
+ interface: modelInterface,
855
+ protocol: modelInterface,
856
+ api: getModelApi(item) || void 0,
857
+ keyConfigured: Boolean(getModelKey(item)),
858
+ upstreamServiceId: upstreamServiceId || void 0,
859
+ upstreamBaseUrl: upstreamService?.base_url,
860
+ upstreamAuthConfigured: Boolean(upstreamService?.auth_token),
861
+ priority: poolPriority,
862
+ enabled,
863
+ capabilities,
864
+ source: "registration"
865
+ };
866
+ providers.push({
867
+ name: providerName,
868
+ api_base_url: getModelApi(item),
869
+ api_key: getModelKey(item),
870
+ models: [item.model],
871
+ transformer: inferTransformer(modelInterface)
872
+ });
873
+ modelMap[endpoint.legacyRef] = buildCompiledModelRefFromPoolEndpoint(
874
+ endpoint,
875
+ item.thinking,
876
+ compatibilityProfile
877
+ );
878
+ const pool = pools[item.id] ?? {
879
+ modelId: item.id,
880
+ strategy: "priority",
881
+ endpoints: [],
882
+ warnings: []
883
+ };
884
+ pool.endpoints.push(endpoint);
885
+ pool.warnings.push(...warnings);
886
+ pools[item.id] = pool;
887
+ });
888
+ Object.values(pools).forEach((pool) => {
889
+ pool.endpoints.sort((a, b) => a.priority - b.priority || a.id.localeCompare(b.id));
890
+ pool.activeEndpointId = pool.endpoints.find((endpoint) => endpoint.enabled)?.id;
891
+ const activeEndpoint = pool.endpoints.find((endpoint) => endpoint.id === pool.activeEndpointId);
892
+ if (activeEndpoint) {
893
+ modelMap[pool.modelId] = modelMap[activeEndpoint.legacyRef];
894
+ }
895
+ });
896
+ return {
897
+ providers,
898
+ modelMap,
899
+ modelPools: pools
900
+ };
901
+ }
365
902
  function compileModelsToProviders(models) {
366
903
  return models.map((rawItem) => {
367
904
  const item = normalizeModelEndpointConfig(rawItem);
@@ -376,8 +913,12 @@ function compileModelsToProviders(models) {
376
913
  });
377
914
  }
378
915
  function buildModelRegistry(config) {
916
+ const registrationPools = buildRegistrationModelPools(config);
379
917
  if (Array.isArray(config.Models) && config.Models.length > 0) {
380
- const providers2 = compileModelsToProviders(config.Models);
918
+ const providers2 = [
919
+ ...compileModelsToProviders(config.Models),
920
+ ...registrationPools.providers
921
+ ];
381
922
  const modelMap2 = config.Models.reduce((result, rawItem) => {
382
923
  const item = normalizeModelEndpointConfig(rawItem);
383
924
  const modelInterface = getModelInterface(item) || "openai";
@@ -395,21 +936,32 @@ function buildModelRegistry(config) {
395
936
  source: "models"
396
937
  };
397
938
  return result;
398
- }, {});
939
+ }, {
940
+ ...registrationPools.modelMap
941
+ });
399
942
  return {
400
943
  providers: providers2,
401
- modelMap: modelMap2
944
+ modelMap: modelMap2,
945
+ modelPools: registrationPools.modelPools
402
946
  };
403
947
  }
404
- const providers = config.Providers ?? [];
948
+ const providers = [
949
+ ...config.Providers ?? [],
950
+ ...registrationPools.providers
951
+ ];
405
952
  const modelMap = providers.reduce((result, provider) => {
406
953
  for (const model of provider.models ?? []) {
954
+ const legacyRef = `${provider.name},${model}`;
955
+ if (registrationPools.modelMap[legacyRef]) {
956
+ result[legacyRef] = registrationPools.modelMap[legacyRef];
957
+ continue;
958
+ }
407
959
  const compatibilityProfile = inferCompatibilityProfile(
408
960
  { api_base_url: provider.api_base_url },
409
961
  "openai"
410
962
  );
411
- result[`${provider.name},${model}`] = {
412
- id: `${provider.name},${model}`,
963
+ result[legacyRef] = {
964
+ id: legacyRef,
413
965
  providerName: provider.name,
414
966
  modelName: model,
415
967
  interface: "openai",
@@ -428,10 +980,13 @@ function buildModelRegistry(config) {
428
980
  };
429
981
  }
430
982
  return result;
431
- }, {});
983
+ }, {
984
+ ...registrationPools.modelMap
985
+ });
432
986
  return {
433
987
  providers,
434
- modelMap
988
+ modelMap,
989
+ modelPools: registrationPools.modelPools
435
990
  };
436
991
  }
437
992
  function resolveModelReference(config, ref) {
@@ -470,6 +1025,10 @@ function isKnownModelReference(config, ref) {
470
1025
  return false;
471
1026
  }
472
1027
  if (ref.includes(",")) {
1028
+ const registry2 = buildModelRegistry(config);
1029
+ if (registry2.modelMap[ref]) {
1030
+ return true;
1031
+ }
473
1032
  const [provider, model] = ref.split(",");
474
1033
  return Boolean(
475
1034
  config.Providers?.find(
@@ -733,7 +1292,7 @@ function validateSemanticRoutingConfig(semantic, config, validProviders, prefix,
733
1292
  }
734
1293
  }
735
1294
  }
736
- function validateModelEndpointList(models, prefix, errors) {
1295
+ function validateModelEndpointList(models, prefix, errors, options = {}) {
737
1296
  const ids = /* @__PURE__ */ new Set();
738
1297
  models.forEach((item, index) => {
739
1298
  if (!item || typeof item !== "object" || Array.isArray(item)) {
@@ -742,7 +1301,7 @@ function validateModelEndpointList(models, prefix, errors) {
742
1301
  }
743
1302
  if (!item.id?.trim()) {
744
1303
  errors.push(`${prefix}[${index}].id is required`);
745
- } else if (ids.has(item.id.trim())) {
1304
+ } else if (!options.allowDuplicateIds && ids.has(item.id.trim())) {
746
1305
  errors.push(`${prefix}[${index}].id must be unique`);
747
1306
  } else {
748
1307
  ids.add(item.id.trim());
@@ -798,6 +1357,74 @@ function validateRegistrationUpstreamServices(services, errors) {
798
1357
  }
799
1358
  });
800
1359
  }
1360
+ function validateManagedApiKeys(keys, errors) {
1361
+ const ids = /* @__PURE__ */ new Set();
1362
+ keys.forEach((key, index) => {
1363
+ if (!key || typeof key !== "object" || Array.isArray(key)) {
1364
+ errors.push(`Auth.managed_keys[${index}] must be an object`);
1365
+ return;
1366
+ }
1367
+ const id = typeof key.id === "string" ? key.id.trim() : "";
1368
+ if (!id) {
1369
+ errors.push(`Auth.managed_keys[${index}].id is required`);
1370
+ } else if (ids.has(id)) {
1371
+ errors.push(`Auth.managed_keys[${index}].id must be unique`);
1372
+ } else {
1373
+ ids.add(id);
1374
+ }
1375
+ if (!key.label || typeof key.label !== "string") {
1376
+ errors.push(`Auth.managed_keys[${index}].label is required`);
1377
+ }
1378
+ if (!key.key_hash || typeof key.key_hash !== "string") {
1379
+ errors.push(`Auth.managed_keys[${index}].key_hash is required`);
1380
+ }
1381
+ if (!key.key_prefix || typeof key.key_prefix !== "string") {
1382
+ errors.push(`Auth.managed_keys[${index}].key_prefix is required`);
1383
+ }
1384
+ if (!key.key_suffix || typeof key.key_suffix !== "string") {
1385
+ errors.push(`Auth.managed_keys[${index}].key_suffix is required`);
1386
+ }
1387
+ if (!key.created_at || typeof key.created_at !== "string") {
1388
+ errors.push(`Auth.managed_keys[${index}].created_at is required`);
1389
+ }
1390
+ validateManagedApiKeyScopes(key.scopes).forEach((message) => {
1391
+ errors.push(`Auth.managed_keys[${index}].${message}`);
1392
+ });
1393
+ if (key.expires_at !== void 0 && typeof key.expires_at !== "string") {
1394
+ errors.push(`Auth.managed_keys[${index}].expires_at must be a string when provided`);
1395
+ }
1396
+ if (key.revoked_at !== void 0 && typeof key.revoked_at !== "string") {
1397
+ errors.push(`Auth.managed_keys[${index}].revoked_at must be a string when provided`);
1398
+ }
1399
+ });
1400
+ }
1401
+ function validateAuthQuotaUsage(usage, errors) {
1402
+ if (usage === void 0) {
1403
+ return;
1404
+ }
1405
+ if (!usage || typeof usage !== "object" || Array.isArray(usage)) {
1406
+ errors.push("Auth.quota_usage must be an object when provided");
1407
+ return;
1408
+ }
1409
+ Object.entries(usage).forEach(([keyId, item]) => {
1410
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
1411
+ errors.push(`Auth.quota_usage.${keyId} must be an object`);
1412
+ return;
1413
+ }
1414
+ if (!Number.isInteger(item.requests) || item.requests < 0) {
1415
+ errors.push(`Auth.quota_usage.${keyId}.requests must be a non-negative integer`);
1416
+ }
1417
+ if (!Number.isInteger(item.tokens) || item.tokens < 0) {
1418
+ errors.push(`Auth.quota_usage.${keyId}.tokens must be a non-negative integer`);
1419
+ }
1420
+ if (!item.window_started_at || typeof item.window_started_at !== "string" || Number.isNaN(Date.parse(item.window_started_at))) {
1421
+ errors.push(`Auth.quota_usage.${keyId}.window_started_at must be an ISO date string`);
1422
+ }
1423
+ if (item.window_seconds !== void 0 && (!Number.isInteger(item.window_seconds) || item.window_seconds <= 0)) {
1424
+ errors.push(`Auth.quota_usage.${keyId}.window_seconds must be a positive integer when provided`);
1425
+ }
1426
+ });
1427
+ }
801
1428
  function trimTrailingSlash2(value) {
802
1429
  return value.replace(/\/+$/, "");
803
1430
  }
@@ -862,13 +1489,21 @@ function validateConfig(config) {
862
1489
  if (config.Registration?.models !== void 0 && !Array.isArray(config.Registration.models)) {
863
1490
  errors.push("Registration.models must be an array when provided");
864
1491
  } else if (Array.isArray(config.Registration?.models)) {
865
- validateModelEndpointList(config.Registration.models, "Registration.models", errors);
1492
+ validateModelEndpointList(config.Registration.models, "Registration.models", errors, {
1493
+ allowDuplicateIds: true
1494
+ });
866
1495
  }
867
1496
  if (config.Registration?.upstream_services !== void 0 && !Array.isArray(config.Registration.upstream_services)) {
868
1497
  errors.push("Registration.upstream_services must be an array when provided");
869
1498
  } else if (Array.isArray(config.Registration?.upstream_services)) {
870
1499
  validateRegistrationUpstreamServices(config.Registration.upstream_services, errors);
871
1500
  }
1501
+ if (config.Auth?.managed_keys !== void 0 && !Array.isArray(config.Auth.managed_keys)) {
1502
+ errors.push("Auth.managed_keys must be an array when provided");
1503
+ } else if (Array.isArray(config.Auth?.managed_keys)) {
1504
+ validateManagedApiKeys(config.Auth.managed_keys, errors);
1505
+ }
1506
+ validateAuthQuotaUsage(config.Auth?.quota_usage, errors);
872
1507
  const validProviders = config.Providers?.filter((p) => p.name && p.models?.length) ?? [];
873
1508
  const runtimeSmartRouter = deriveRuntimeSmartRouterConfig(config, config);
874
1509
  if (validProviders.length > 0) {
@@ -1290,6 +1925,7 @@ var init_config = __esm({
1290
1925
  import_path2 = require("path");
1291
1926
  yaml = __toESM(require("js-yaml"));
1292
1927
  init_constants();
1928
+ init_api_keys();
1293
1929
  init_compile();
1294
1930
  init_schema();
1295
1931
  init_log();
@@ -1319,6 +1955,10 @@ var init_utils = __esm({
1319
1955
  });
1320
1956
 
1321
1957
  // src/service-health.ts
1958
+ function buildServiceHealthHeaders(options = {}) {
1959
+ const apiKey = options.apiKey?.trim();
1960
+ return apiKey ? { Authorization: `Bearer ${apiKey}` } : void 0;
1961
+ }
1322
1962
  function isExpectedServiceHealth(payload) {
1323
1963
  if (!payload || typeof payload !== "object") {
1324
1964
  return false;
@@ -1326,9 +1966,10 @@ function isExpectedServiceHealth(payload) {
1326
1966
  const health = payload;
1327
1967
  return health.service === SERVICE_NAME && health.ready === true;
1328
1968
  }
1329
- async function probeServiceHealth(port, timeoutMs = 500) {
1969
+ async function probeServiceHealth(port, timeoutMs = 500, options = {}) {
1330
1970
  try {
1331
1971
  const res = await fetch(`http://127.0.0.1:${port}${SERVICE_HEALTH_PATH}`, {
1972
+ headers: buildServiceHealthHeaders(options),
1332
1973
  signal: AbortSignal.timeout(timeoutMs)
1333
1974
  });
1334
1975
  if (!res.ok) {
@@ -1391,7 +2032,9 @@ async function probeRemoteServiceStatus(remoteService, timeoutMs = 800, fetchFn
1391
2032
  baseUrl: normalizedBaseUrl,
1392
2033
  service: info.service,
1393
2034
  runtimeMode: info.runtimeMode,
1394
- remoteEnabled: info.remoteEnabled
2035
+ remoteEnabled: info.remoteEnabled,
2036
+ ...info.auth !== void 0 ? { auth: info.auth } : {},
2037
+ ...info.security !== void 0 ? { security: info.security } : {}
1395
2038
  };
1396
2039
  } catch (error) {
1397
2040
  return {
@@ -1429,10 +2072,10 @@ async function isTcpPortOccupied(port, timeoutMs = 500) {
1429
2072
  socket.connect(port, "127.0.0.1");
1430
2073
  });
1431
2074
  }
1432
- async function waitForService(port, timeoutMs = 5e3) {
2075
+ async function waitForService(port, timeoutMs = 5e3, options = {}) {
1433
2076
  const start = Date.now();
1434
2077
  while (Date.now() - start < timeoutMs) {
1435
- if (await probeServiceHealth(port, 500)) {
2078
+ if (await probeServiceHealth(port, 500, options)) {
1436
2079
  return true;
1437
2080
  }
1438
2081
  await new Promise((resolve) => setTimeout(resolve, 300));
@@ -1460,7 +2103,7 @@ var init_types = __esm({
1460
2103
  // src/governance/trace.ts
1461
2104
  function createGovernanceTrace(input3 = {}) {
1462
2105
  return {
1463
- requestId: input3.requestId ?? (0, import_crypto.randomUUID)(),
2106
+ requestId: input3.requestId ?? (0, import_crypto2.randomUUID)(),
1464
2107
  sessionKey: input3.sessionKey,
1465
2108
  initialModel: input3.initialModel,
1466
2109
  finalModel: input3.finalModel,
@@ -1499,11 +2142,11 @@ function recordGovernanceTrace(trace) {
1499
2142
  governanceTraceStore.add(trace);
1500
2143
  return trace;
1501
2144
  }
1502
- var import_crypto, import_fs2, import_lru_cache, import_path3, import_zlib, GovernanceTraceStore, governanceTraceStore;
2145
+ var import_crypto2, import_fs2, import_lru_cache, import_path3, import_zlib, GovernanceTraceStore, governanceTraceStore;
1503
2146
  var init_trace = __esm({
1504
2147
  "src/governance/trace.ts"() {
1505
2148
  "use strict";
1506
- import_crypto = require("crypto");
2149
+ import_crypto2 = require("crypto");
1507
2150
  import_fs2 = require("fs");
1508
2151
  import_lru_cache = require("lru-cache");
1509
2152
  import_path3 = require("path");
@@ -2888,6 +3531,122 @@ function buildTopEntries(distribution, total, limit = 5) {
2888
3531
  rate: rate(count, total)
2889
3532
  }));
2890
3533
  }
3534
+ function buildTopSwitchEntries(distribution, total, limit = 5) {
3535
+ return Object.values(distribution).sort((left, right) => {
3536
+ if (right.count !== left.count) {
3537
+ return right.count - left.count;
3538
+ }
3539
+ return `${left.from ?? ""}->${left.to ?? ""}`.localeCompare(`${right.from ?? ""}->${right.to ?? ""}`);
3540
+ }).slice(0, limit).map((entry) => ({
3541
+ key: `${entry.from ?? "-"} -> ${entry.to ?? "-"}`,
3542
+ from: entry.from,
3543
+ to: entry.to,
3544
+ count: entry.count,
3545
+ rate: rate(entry.count, total)
3546
+ }));
3547
+ }
3548
+ function addOutcomeGroup(groups, key, trace) {
3549
+ if (!key) {
3550
+ return;
3551
+ }
3552
+ const switched = isModelSwitch(trace);
3553
+ const group = groups[key] ?? {
3554
+ key,
3555
+ totalTraces: 0,
3556
+ modelSwitchCount: 0,
3557
+ alignmentOnSwitchCount: 0,
3558
+ cascadeAfterSwitchCount: 0,
3559
+ latencyValues: []
3560
+ };
3561
+ group.totalTraces += 1;
3562
+ group.modelSwitchCount += switched ? 1 : 0;
3563
+ group.alignmentOnSwitchCount += switched && trace.alignmentUsed ? 1 : 0;
3564
+ group.cascadeAfterSwitchCount += switched && trace.cascadeTriggered ? 1 : 0;
3565
+ if (typeof trace.latencyMs === "number" && Number.isFinite(trace.latencyMs)) {
3566
+ group.latencyValues.push(trace.latencyMs);
3567
+ }
3568
+ groups[key] = group;
3569
+ }
3570
+ function buildOutcomeGroupEntries(groups, totalTraces, limit = 5) {
3571
+ return Object.values(groups).sort((left, right) => {
3572
+ if (right.totalTraces !== left.totalTraces) {
3573
+ return right.totalTraces - left.totalTraces;
3574
+ }
3575
+ return left.key.localeCompare(right.key);
3576
+ }).slice(0, limit).map((group) => ({
3577
+ key: group.key,
3578
+ totalTraces: group.totalTraces,
3579
+ rate: rate(group.totalTraces, totalTraces),
3580
+ modelSwitchCount: group.modelSwitchCount,
3581
+ modelSwitchRate: rate(group.modelSwitchCount, group.totalTraces),
3582
+ alignmentOnSwitchCount: group.alignmentOnSwitchCount,
3583
+ alignmentOnSwitchRate: rate(group.alignmentOnSwitchCount, group.modelSwitchCount),
3584
+ cascadeAfterSwitchCount: group.cascadeAfterSwitchCount,
3585
+ cascadeAfterSwitchRate: rate(group.cascadeAfterSwitchCount, group.modelSwitchCount),
3586
+ averageLatencyMs: average(group.latencyValues)
3587
+ }));
3588
+ }
3589
+ function isRoutedTrace(trace) {
3590
+ return trace.routeReason.some((reason) => reason !== "request_received");
3591
+ }
3592
+ function isModelSwitch(trace) {
3593
+ return Boolean(trace.initialModel && trace.finalModel && trace.initialModel !== trace.finalModel);
3594
+ }
3595
+ function summarizeRoutingOutcomes(traces) {
3596
+ const routedTraces = traces.filter(isRoutedTrace);
3597
+ const switchedTraces = traces.filter(isModelSwitch);
3598
+ const stableModelCount = traces.filter(
3599
+ (trace) => Boolean(trace.initialModel && trace.finalModel && trace.initialModel === trace.finalModel)
3600
+ ).length;
3601
+ const alignmentOnSwitchCount = switchedTraces.filter((trace) => trace.alignmentUsed).length;
3602
+ const cascadeAfterSwitchCount = switchedTraces.filter((trace) => trace.cascadeTriggered).length;
3603
+ const switchDistribution = {};
3604
+ const routeLatencyValues = {};
3605
+ const routeReasonGroups = {};
3606
+ const finalModelGroups = {};
3607
+ const semanticIntentGroups = {};
3608
+ for (const trace of traces) {
3609
+ if (isModelSwitch(trace)) {
3610
+ const key = `${trace.initialModel} -> ${trace.finalModel}`;
3611
+ switchDistribution[key] = {
3612
+ from: trace.initialModel,
3613
+ to: trace.finalModel,
3614
+ count: (switchDistribution[key]?.count ?? 0) + 1
3615
+ };
3616
+ }
3617
+ if (typeof trace.latencyMs === "number" && Number.isFinite(trace.latencyMs)) {
3618
+ for (const reason of trace.routeReason.filter((item) => item !== "request_received")) {
3619
+ routeLatencyValues[reason] = [...routeLatencyValues[reason] ?? [], trace.latencyMs];
3620
+ }
3621
+ }
3622
+ for (const reason of trace.routeReason.filter((item) => item !== "request_received")) {
3623
+ addOutcomeGroup(routeReasonGroups, reason, trace);
3624
+ }
3625
+ addOutcomeGroup(finalModelGroups, trace.finalModel, trace);
3626
+ addOutcomeGroup(semanticIntentGroups, trace.semanticIntent, trace);
3627
+ }
3628
+ const averageLatencyByRouteReason = Object.fromEntries(
3629
+ Object.entries(routeLatencyValues).sort(([left], [right]) => left.localeCompare(right)).map(([reason, values]) => [reason, average(values)])
3630
+ );
3631
+ return {
3632
+ totalTraces: traces.length,
3633
+ routedTraces: routedTraces.length,
3634
+ routedRate: rate(routedTraces.length, traces.length),
3635
+ modelSwitchCount: switchedTraces.length,
3636
+ modelSwitchRate: rate(switchedTraces.length, traces.length),
3637
+ stableModelCount,
3638
+ stableModelRate: rate(stableModelCount, traces.length),
3639
+ alignmentOnSwitchCount,
3640
+ alignmentOnSwitchRate: rate(alignmentOnSwitchCount, switchedTraces.length),
3641
+ cascadeAfterSwitchCount,
3642
+ cascadeAfterSwitchRate: rate(cascadeAfterSwitchCount, switchedTraces.length),
3643
+ averageLatencyByRouteReason,
3644
+ topModelSwitches: buildTopSwitchEntries(switchDistribution, switchedTraces.length),
3645
+ byRouteReason: buildOutcomeGroupEntries(routeReasonGroups, traces.length),
3646
+ byFinalModel: buildOutcomeGroupEntries(finalModelGroups, traces.length),
3647
+ bySemanticIntent: buildOutcomeGroupEntries(semanticIntentGroups, traces.length)
3648
+ };
3649
+ }
2891
3650
  function averageRate(values) {
2892
3651
  if (!values.length) {
2893
3652
  return 0;
@@ -2995,6 +3754,8 @@ function buildGovernanceHealthSummary(input3) {
2995
3754
  cascadeTriggeredRate: 0,
2996
3755
  shadowCheckedRate: 0,
2997
3756
  alignmentUsedRate: 0,
3757
+ modelSwitchRate: 0,
3758
+ alignmentOnSwitchRate: 0,
2998
3759
  averageLatencyMs: 0,
2999
3760
  topRouteReason: input3.topRouteReasons?.[0],
3000
3761
  topFinalModel: input3.topFinalModels?.[0]
@@ -3017,6 +3778,8 @@ function buildGovernanceHealthSummary(input3) {
3017
3778
  cascadeTriggeredRate: metrics.cascadeTriggeredRate,
3018
3779
  shadowCheckedRate: metrics.shadowCheckedRate,
3019
3780
  alignmentUsedRate: metrics.alignmentUsedRate,
3781
+ modelSwitchRate: input3.outcome?.modelSwitchRate ?? 0,
3782
+ alignmentOnSwitchRate: input3.outcome?.alignmentOnSwitchRate ?? 0,
3020
3783
  averageLatencyMs: metrics.averageLatencyMs,
3021
3784
  topRouteReason: input3.topRouteReasons?.[0],
3022
3785
  topFinalModel: input3.topFinalModels?.[0]
@@ -3113,6 +3876,7 @@ function getGovernanceMetricsReport(options = {}) {
3113
3876
  const limitedTraces = options.limit && options.limit > 0 ? windowed.traces.slice(0, options.limit) : windowed.traces;
3114
3877
  const bucketCount = options.bucketCount && options.bucketCount > 0 ? options.bucketCount : 6;
3115
3878
  const metrics = summarizeGovernanceMetrics(limitedTraces);
3879
+ const outcome = summarizeRoutingOutcomes(limitedTraces);
3116
3880
  const buckets = buildBuckets(limitedTraces, windowed.windowStart, windowed.windowEnd, bucketCount);
3117
3881
  const thresholds = normalizeAnomalyThresholds(options.anomalyThresholds);
3118
3882
  const topRouteReasons = buildTopEntries(metrics.routeReasonDistribution, limitedTraces.length);
@@ -3125,6 +3889,7 @@ function getGovernanceMetricsReport(options = {}) {
3125
3889
  windowStart: windowed.windowStart,
3126
3890
  windowEnd: windowed.windowEnd,
3127
3891
  metrics,
3892
+ outcome,
3128
3893
  buckets,
3129
3894
  topRouteReasons,
3130
3895
  topFinalModels,
@@ -3134,7 +3899,8 @@ function getGovernanceMetricsReport(options = {}) {
3134
3899
  metrics,
3135
3900
  anomalies,
3136
3901
  topRouteReasons,
3137
- topFinalModels
3902
+ topFinalModels,
3903
+ outcome
3138
3904
  })
3139
3905
  };
3140
3906
  }
@@ -3150,7 +3916,11 @@ function exportGovernanceMetricsReport(report, format = "json") {
3150
3916
  `summary,shadowCheckedRate,${report.metrics.shadowCheckedRate}`,
3151
3917
  `summary,alignmentUsedRate,${report.metrics.alignmentUsedRate}`,
3152
3918
  `summary,averageLatencyMs,${report.metrics.averageLatencyMs}`,
3153
- `summary,averageEstimatedCost,${report.metrics.averageEstimatedCost}`
3919
+ `summary,averageEstimatedCost,${report.metrics.averageEstimatedCost}`,
3920
+ `outcome,routedRate,${report.outcome.routedRate}`,
3921
+ `outcome,modelSwitchRate,${report.outcome.modelSwitchRate}`,
3922
+ `outcome,alignmentOnSwitchRate,${report.outcome.alignmentOnSwitchRate}`,
3923
+ `outcome,cascadeAfterSwitchRate,${report.outcome.cascadeAfterSwitchRate}`
3154
3924
  ];
3155
3925
  if (report.health) {
3156
3926
  lines.push(`summary,healthStatus,${report.health.status}`);
@@ -3168,6 +3938,18 @@ function exportGovernanceMetricsReport(report, format = "json") {
3168
3938
  for (const item of report.topSemanticIntents) {
3169
3939
  lines.push(`topSemanticIntent,${item.key},${item.count}:${item.rate}`);
3170
3940
  }
3941
+ for (const item of report.outcome.topModelSwitches) {
3942
+ lines.push(`topModelSwitch,${item.key},${item.count}:${item.rate}`);
3943
+ }
3944
+ for (const item of report.outcome.byRouteReason) {
3945
+ lines.push(`outcomeByRouteReason,${item.key},${item.totalTraces}:${item.modelSwitchRate}:${item.averageLatencyMs}`);
3946
+ }
3947
+ for (const item of report.outcome.byFinalModel) {
3948
+ lines.push(`outcomeByFinalModel,${item.key},${item.totalTraces}:${item.modelSwitchRate}:${item.averageLatencyMs}`);
3949
+ }
3950
+ for (const item of report.outcome.bySemanticIntent) {
3951
+ lines.push(`outcomeBySemanticIntent,${item.key},${item.totalTraces}:${item.modelSwitchRate}:${item.averageLatencyMs}`);
3952
+ }
3171
3953
  for (const bucket of report.buckets) {
3172
3954
  lines.push(
3173
3955
  `bucket,${bucket.label},${[
@@ -3620,6 +4402,17 @@ var init_provider_presets = __esm({
3620
4402
  }
3621
4403
  });
3622
4404
 
4405
+ // src/runtime-role-guidance.ts
4406
+ var LOCAL_USER_ROLE_GUIDE, SERVER_MAINTAINER_ROLE_GUIDE, REMOTE_CLIENT_ROLE_GUIDE;
4407
+ var init_runtime_role_guidance = __esm({
4408
+ "src/runtime-role-guidance.ts"() {
4409
+ "use strict";
4410
+ LOCAL_USER_ROLE_GUIDE = "\u672C\u5730\u4F7F\u7528\u8005\uFF1A\u5148\u8DD1\u901A Models + Router.default\uFF0C\u518D\u7528 ctr start / ctr status / ctr code \u8FDB\u5165 Claude Code\u3002";
4411
+ SERVER_MAINTAINER_ROLE_GUIDE = "\u670D\u52A1\u7EF4\u62A4\u8005\uFF1A\u7528 ctr deploy init --target server \u751F\u6210 server \u914D\u7F6E\uFF0C\u4FDD\u7559 bootstrap/admin key \u7BA1\u7406\u670D\u52A1\uFF0C\u5E76\u7ED9\u8FDC\u7A0B\u4F7F\u7528\u8005\u53D1\u653E managed client + read-only key\u3002";
4412
+ REMOTE_CLIENT_ROLE_GUIDE = "\u8FDC\u7A0B\u4F7F\u7528\u8005\uFF1A\u62FF\u5230\u670D\u52A1\u5730\u5740\u548C managed client + read-only key\uFF1BRuntime.remote_service \u8D1F\u8D23\u8FDE\u63A5\u914D\u7F6E\u4E0E ready/status \u68C0\u67E5\uFF0C\u76F4\u8FDE Claude Code \u65F6\u8BBE\u7F6E ANTHROPIC_BASE_URL \u4E0E ANTHROPIC_AUTH_TOKEN\u3002";
4413
+ }
4414
+ });
4415
+
3623
4416
  // src/ui/workbench.ts
3624
4417
  function toInlineScriptJson(value) {
3625
4418
  return JSON.stringify(value).replace(/</g, "\\u003c");
@@ -3637,27 +4430,53 @@ function renderWorkbenchHtml(rawInitialConfig, configuredThresholds = {}) {
3637
4430
  const remoteService = initialConfig.Runtime?.remote_service ?? {};
3638
4431
  const remoteBaseUrl = typeof remoteService.base_url === "string" ? remoteService.base_url.trim().replace(/\/+$/, "") : "";
3639
4432
  const remoteSummary = remoteService.enabled ? `${remoteBaseUrl || "-"} (checking)` : "disabled";
4433
+ const configuredHost = String(initialConfig.HOST ?? "127.0.0.1").trim() || "127.0.0.1";
4434
+ const publicHost = ["0.0.0.0", "::", "[::]"].includes(configuredHost);
4435
+ const advertisedUrl = publicHost ? `http://<server-host>:${displayPort}` : `http://${configuredHost}:${displayPort}`;
4436
+ const clientConnectionSummary = runtimeMode === "local" && remoteService.enabled ? `${remoteBaseUrl || "-"} \xB7 client + read-only token` : runtimeMode === "local" ? `local only \xB7 http://127.0.0.1:${displayPort}` : `${advertisedUrl} \xB7 client + read-only token`;
3640
4437
  const registration = initialConfig.Registration ?? {};
3641
4438
  const registrationModels = Array.isArray(registration.models) ? registration.models.length : 0;
3642
4439
  const registrationUpstreamServices = Array.isArray(registration.upstream_services) ? registration.upstream_services.length : 0;
3643
4440
  const registrationSummary = registration.enabled ? `${registrationModels} models / ${registrationUpstreamServices} upstream` : "disabled";
4441
+ const initialManagedKeys = Array.isArray(initialConfig.Auth?.managed_keys) ? initialConfig.Auth.managed_keys : [];
4442
+ const nowMs = Date.now();
4443
+ const initialActiveManagedKeys = initialManagedKeys.filter((record) => {
4444
+ if (record?.revoked_at) {
4445
+ return false;
4446
+ }
4447
+ if (!record?.expires_at) {
4448
+ return true;
4449
+ }
4450
+ const expiresAt = Date.parse(record.expires_at);
4451
+ return !Number.isFinite(expiresAt) || expiresAt > nowMs;
4452
+ }).length;
4453
+ const authSummary = initialConfig.APIKEY || initialManagedKeys.length > 0 ? `configured \xB7 ${initialActiveManagedKeys} active` : "not configured";
4454
+ const securitySummary = !initialConfig.APIKEY && initialManagedKeys.length === 0 && (runtimeMode !== "local" || publicHost) ? "critical" : !initialConfig.APIKEY && initialManagedKeys.length > 0 && initialActiveManagedKeys === 0 ? "warning" : "ok";
3644
4455
  const escapedDisplayPort = escapeHtml(displayPort);
3645
4456
  const escapedModelsCount = escapeHtml(modelsCount);
3646
4457
  const escapedRouterDefault = escapeHtml(routerDefault);
3647
4458
  const escapedRuntimeMode = escapeHtml(runtimeMode);
3648
4459
  const escapedServiceRole = escapeHtml(serviceRole);
4460
+ const escapedListenerSummary = escapeHtml(`${configuredHost}:${displayPort}${publicHost ? " (public)" : " (local)"}`);
4461
+ const escapedClientConnectionSummary = escapeHtml(clientConnectionSummary);
3649
4462
  const escapedRemoteSummary = escapeHtml(remoteSummary);
3650
4463
  const escapedRegistrationSummary = escapeHtml(registrationSummary);
4464
+ const escapedAuthSummary = escapeHtml(authSummary);
4465
+ const escapedSecuritySummary = escapeHtml(securitySummary);
4466
+ const escapedLocalUserRoleGuide = escapeHtml(LOCAL_USER_ROLE_GUIDE);
4467
+ const escapedServerMaintainerRoleGuide = escapeHtml(SERVER_MAINTAINER_ROLE_GUIDE);
4468
+ const escapedRemoteClientRoleGuide = escapeHtml(REMOTE_CLIENT_ROLE_GUIDE);
3651
4469
  const escapedMinSampleSize = escapeHtml(configuredThresholds.min_sample_size ?? 3);
3652
4470
  const escapedCascadeWarnRate = escapeHtml(configuredThresholds.cascade_warn_rate ?? 0.4);
3653
4471
  const escapedShadowWarnRate = escapeHtml(configuredThresholds.shadow_warn_rate ?? 0.5);
3654
4472
  const escapedLatencyWarnMs = escapeHtml(configuredThresholds.latency_warn_ms ?? 1500);
3655
- return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Claude Trigger Router</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.hero{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(260px,.8fr);gap:1rem;align-items:stretch;margin-bottom:1rem}.hero h2{margin:.2rem 0 .5rem;font-size:1.55rem}.hero-copy{display:flex;flex-direction:column;justify-content:center}.status-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.status-tile{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem;min-width:0}.status-tile strong{display:block;margin-top:.2rem;word-break:break-word}@media (max-width:760px){.hero{grid-template-columns:1fr}.status-grid{grid-template-columns:1fr}}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:1rem;padding:.45rem 0;border-bottom:1px dashed #e5e7eb}.mini-list li:last-child{border-bottom:none}.action-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center;margin-top:.75rem}.management-table{width:100%;margin-top:.75rem}.management-table th,.management-table td{padding:.5rem;border-bottom:1px solid #e5e7eb;font-size:.92rem;vertical-align:top}.alert-list{display:grid;gap:.75rem;margin-top:1rem}.alert{border-radius:12px;padding:.85rem 1rem;border:1px solid}.alert.warn{background:#fff7ed;border-color:#fdba74;color:#9a3412}.alert.critical{background:#fef2f2;border-color:#fca5a5;color:#991b1b}.alert.info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.diff-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-top:.75rem}.diff-chip{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.75rem}.diff-chip strong{display:block;font-size:1rem;margin-top:.2rem}.models-form-grid{display:grid;gap:.75rem;margin-top:.75rem}.model-card{border:1px solid #e5e7eb;border-radius:12px;padding:1rem;background:#fcfcfd}.model-card-header{display:flex;justify-content:space-between;gap:1rem;align-items:center;margin-bottom:.75rem}.model-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.model-card-grid textarea{min-height:84px;resize:vertical}.list-editor{display:grid;gap:.75rem;margin-top:.75rem}.list-item{border:1px solid #e5e7eb;border-radius:12px;padding:.85rem;background:#fcfcfd}.list-item-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.jump-highlight{outline:3px solid #f59e0b;box-shadow:0 0 0 6px rgba(245,158,11,.15);transition:box-shadow .25s ease,outline-color .25s ease}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.control-grid label{display:block;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}.trend-table{width:100%;margin-top:.75rem}.trend-table th,.trend-table td{padding:.45rem;border-bottom:1px solid #e5e7eb;font-size:.92rem}.row{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}input,select,button{font:inherit;padding:.55rem .75rem;border-radius:8px;border:1px solid #d1d5db}button{background:#111827;color:#fff;border-color:#111827;cursor:pointer}table{width:100%;border-collapse:collapse;margin-top:1rem}th,td{text-align:left;padding:.65rem .5rem;border-bottom:1px solid #e5e7eb;vertical-align:top}code,pre{font-family:ui-monospace,SFMono-Regular,monospace}pre{white-space:pre-wrap;background:#0f172a;color:#e2e8f0;padding:1rem;border-radius:12px;overflow:auto}.pill{display:inline-block;padding:.2rem .5rem;border-radius:999px;background:#eef2ff;color:#3730a3;font-size:.8rem}.surface-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin:1rem 0}.surface-tab{background:#fff;color:#1f2328;border-color:#d1d5db}.surface-tab.active{background:#111827;color:#fff;border-color:#111827}.surface-panel[hidden]{display:none}.surface-heading{display:flex;gap:1rem;flex-wrap:wrap;align-items:center;margin-bottom:.75rem}</style></head><body><div class="hero"><div class="panel hero-copy"><h2>\u914D\u7F6E\u4E0E\u72B6\u6001\u5DE5\u4F5C\u53F0</h2><p class="muted">\u67E5\u770B\u5F53\u524D\u8DEF\u7531\u670D\u52A1\u3001\u6A21\u578B\u914D\u7F6E\u548C\u9ED8\u8BA4\u53BB\u5411\uFF1B\u9700\u8981\u6392\u67E5\u65F6\uFF0C\u4E0B\u65B9\u7EF4\u62A4\u8005\u533A\u57DF\u53EF\u7EE7\u7EED\u67E5\u770B Governance Trace\u3001metrics \u548C\u5F52\u6863\u3002</p><div class="action-row"><button id="loadConfigDraftHeroBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="previewConfigDraftHeroBtn" type="button">\u9884\u89C8 compiled models</button><button id="refreshStatusHeroBtn" type="button">\u5237\u65B0\u72B6\u6001</button></div></div><div class="panel"><div class="status-grid"><div class="status-tile"><span class="muted">Service</span><strong id="serviceReadyStatus">ready</strong></div><div class="status-tile"><span class="muted">Port</span><strong id="servicePortStatus">${escapedDisplayPort}</strong></div><div class="status-tile"><span class="muted">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">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">Registration</span><strong id="registrationStatusSummary">${escapedRegistrationSummary}</strong></div></div></div></div><div class="surface-tabs" role="tablist" aria-label="\u5DE5\u4F5C\u53F0\u5207\u6362"><button id="userSurfaceTab" class="surface-tab active" type="button" role="tab" aria-selected="true" data-surface-target="user">\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</button><button id="maintainerSurfaceTab" class="surface-tab" type="button" role="tab" aria-selected="false" data-surface-target="maintainer">\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</button></div><section id="userSurface" class="surface-panel" data-surface="user"><div class="panel"><div class="surface-heading"><strong>\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u914D\u7F6E\u3001\u6A21\u578B\u3001\u8DEF\u7531\u3001\u670D\u52A1\u72B6\u6001\u4E0E\u4E0B\u4E00\u6B65\u4FDD\u5B58\u52A8\u4F5C\u3002</span></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Routing rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u56F4\u7ED5 SmartRouter \u7EDF\u4E00\u8DEF\u7531\u5F15\u64CE\u7F16\u8F91\u89C4\u5219\u3001\u5019\u9009\u4E0E\u6CBB\u7406\u589E\u5F3A\u517C\u5BB9\u914D\u7F6E</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Routing rules</strong><span class="muted">\u663E\u5F0F\u89C4\u5219\u3001\u8BED\u4E49\u63D0\u793A\u4E0E\u517C\u5BB9\u8F93\u5165</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5F71\u5B50\u6821\u9A8C\u3001\u7EA7\u8054\u4E0E\u89C2\u6D4B\u76F8\u5173\u914D\u7F6E</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div 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></div></div></section><section id="maintainerSurface" class="surface-panel" data-surface="maintainer" hidden><div class="panel"><div class="surface-heading"><strong>\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u8FD0\u884C\u89C2\u6D4B\u3001Governance Trace\u3001metrics\u3001\u5F52\u6863\u4E0E\u7EF4\u62A4\u64CD\u4F5C\u3002</span></div><div class="row"><strong>\u7EF4\u62A4\u8005\u89C2\u6D4B</strong><span class="muted">\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4 Governance Trace\uFF0C\u5E76\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u6307\u6807\u3002</span></div><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/health</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">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">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>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${escapedMinSampleSize}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${escapedCascadeWarnRate}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${escapedShadowWarnRate}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${escapedLatencyWarnMs}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Trend detail</strong><span class="muted">\u6BCF\u4E2A bucket \u7684\u8BE6\u7EC6\u547D\u4E2D\u7387</span></div><table id="trendTable" class="trend-table"><thead><tr><th>Bucket</th><th>Traces</th><th>Sticky</th><th>Cascade</th><th>Shadow</th><th>Alignment</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading...</td></tr></tbody></table></div></div><table id="traceTable"><thead><tr><th>Request</th><th>Session</th><th>Final Model</th><th>Reasons</th><th>Latency</th><th>Inspect</th></tr></thead><tbody><tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Trace Detail</strong><span id="detailHint" class="muted">\u70B9\u51FB\u4E0A\u8868\u4E2D\u7684 View \u67E5\u770B\u8BE6\u60C5</span></div><pre id="traceDetail">{}</pre></div><div class="panel"><div class="row"><strong>Snapshot Management</strong><span class="muted">\u67E5\u770B\u5BFC\u51FA\u5386\u53F2\u3001\u5B9A\u65F6\u4EFB\u52A1\uFF0C\u5E76\u624B\u52A8\u521B\u5EFA\u5FEB\u7167</span></div><div class="action-row"><select id="snapshotFormat"><option value="json">snapshot json</option><option value="csv">snapshot csv</option></select><button id="createSnapshotBtn" type="button">\u751F\u6210\u5FEB\u7167</button><span id="snapshotStatus" class="muted">\u5C1A\u672A\u521B\u5EFA\u5FEB\u7167</span></div><table id="exportTable" class="management-table"><thead><tr><th>Export</th><th>Kind</th><th>Format</th><th>Created</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading exports...</td></tr></tbody></table><table id="scheduleTable" class="management-table"><thead><tr><th>Schedule</th><th>Interval</th><th>Format</th><th>Last run</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading schedules...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Archive Management</strong><span class="muted">\u6D4F\u89C8\u538B\u7F29\u5F52\u6863\u5E76\u67E5\u770B\u5206\u9875\u7ED3\u679C</span></div><div class="action-row"><input id="archiveDate" placeholder="YYYY-MM-DD"><input id="archivePage" placeholder="page" value="1"><input id="archivePageSize" placeholder="pageSize" value="5"><button id="loadArchivesBtn" type="button">\u52A0\u8F7D\u5F52\u6863</button><span id="archiveStatus" class="muted">\u5C1A\u672A\u52A0\u8F7D\u5F52\u6863</span></div><table id="archiveTable" class="management-table"><thead><tr><th>Archive</th><th>Range</th><th>Count</th><th>Compressed</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading archives...</td></tr></tbody></table></div><div class="panel"><p>\u5176\u4ED6\u7BA1\u7406 API\uFF1A</p><ul><li><code>GET /api/config</code> \u2014 \u8BFB\u53D6\u5F53\u524D\u914D\u7F6E</li><li><code>GET /api/models/compiled</code> \u2014 \u67E5\u770B Models \u7F16\u8BD1\u540E\u7684\u5185\u90E8 provider / model \u6620\u5C04</li><li><code>POST /api/models/compiled/preview</code> \u2014 \u7528\u914D\u7F6E\u8349\u7A3F\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u5199\u56DE\u6587\u4EF6</li><li><code>POST /api/config</code> \u2014 \u4FDD\u5B58\u914D\u7F6E</li><li><code>GET /api/transformers</code> \u2014 \u67E5\u770B\u5DF2\u52A0\u8F7D transformer</li><li><code>POST /api/restart</code> \u2014 \u91CD\u542F\u670D\u52A1</li><li><code>GET /api/governance/archives</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5F52\u6863\u5217\u8868</li><li><code>GET /api/governance/archives/:file</code> \u2014 \u67E5\u770B\u5F52\u6863\u5185 traces</li><li><code>POST /api/governance/archives/:file/delete</code> \u2014 \u5220\u9664\u6307\u5B9A\u5F52\u6863</li><li><code>GET /api/governance/health</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5065\u5EB7\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><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const serviceReadyStatus=document.getElementById('serviceReadyStatus');const servicePortStatus=document.getElementById('servicePortStatus');const serviceModeStatus=document.getElementById('serviceModeStatus');const serviceRoleStatus=document.getElementById('serviceRoleStatus');const remoteStatusSummary=document.getElementById('remoteStatusSummary');const registrationStatusSummary=document.getElementById('registrationStatusSummary');const modelCountStatus=document.getElementById('modelCountStatus');const routerDefaultStatus=document.getElementById('routerDefaultStatus');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const metricsGrid=document.getElementById('metricsGrid');const bucketGrid=document.getElementById('bucketGrid');const bucketHint=document.getElementById('bucketHint');const routeRanking=document.getElementById('routeRanking');const modelRanking=document.getElementById('modelRanking');const intentRanking=document.getElementById('intentRanking');const healthSummary=document.getElementById('healthSummary');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');const surfaceTabs=Array.from(document.querySelectorAll('[data-surface-target]'));const surfacePanels=Array.from(document.querySelectorAll('[data-surface]'));let currentDraftConfig={};let knownModelIds=[];let activeValidationHighlight=null;const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94\u8DEF\u7531\u89C4\u5219\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u589E\u5F3A\u4E0E\u6821\u9A8C\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','SmartRouter.sticky.alignment','SmartRouter.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function setActiveSurface(surfaceName){ surfacePanels.forEach((panel)=>{ panel.hidden=panel.dataset.surface !== surfaceName; }); surfaceTabs.forEach((tab)=>{ const active=tab.dataset.surfaceTarget === surfaceName; tab.classList.toggle('active',active); tab.setAttribute('aria-selected', active ? 'true' : 'false'); });}function inferProviderTemplateKey(model){ const explicit=String(model?.provider_template || '').trim(); if(explicit && modelProviderTemplates[explicit]){ return explicit; } const api=String(model?.api || model?.api_base_url || '').trim().toLowerCase(); const modelInterface=String(model?.interface || model?.protocol || '').trim().toLowerCase(); const exactMatch=Object.entries(modelProviderTemplates).find(([,item])=>String(item.api || '').trim().toLowerCase()===api && String(item.interface || '').trim().toLowerCase()===modelInterface); if(exactMatch){ return exactMatch[0]; } if(api.includes('api.anthropic.com/v1/messages') || modelInterface === 'anthropic'){ return 'anthropic'; } if(api.includes('openrouter.ai')){ return 'openrouter'; } if(api.includes('deepseek.com')){ return 'deepseek'; } if(api.includes('siliconflow.cn')){ return 'siliconflow'; } if(api.includes('api.openai.com')){ return 'openai-compatible'; } return '';}function getProviderTemplateContext(model){ const templateKey=inferProviderTemplateKey(model) || defaultProviderTemplateKey; return { templateKey, template:modelProviderTemplates[templateKey] || modelProviderTemplates[defaultProviderTemplateKey] || {} };}function createDraftModelFromTemplate(templateKey){ const resolvedKey=(templateKey && modelProviderTemplates[templateKey]) ? templateKey : defaultProviderTemplateKey; const template=modelProviderTemplates[resolvedKey] || {}; return { provider_template:resolvedKey, id:template.suggested_id || '', api:template.api || '', interface:template.interface || 'openai', model:template.default_model || '', thinking:template.default_thinking || 'auto' };}function getModelIdSuggestionsMarkup(idPrefix){ return '<datalist id="'+idPrefix+'">'+knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join('')+'</datalist>';}function resolvePresetModelId(seed){ const source=String(seed || '').trim().toLowerCase(); if(!source || !knownModelIds.length){ return seed; } if(knownModelIds.includes(seed)){ return seed; } const ranked=knownModelIds.map((modelId)=>{ const target=String(modelId || '').toLowerCase(); let score=0; if(target===source){ score+=100; } if(target.includes(source) || source.includes(target)){ score+=40; } source.split(/[^a-z0-9]+/).filter(Boolean).forEach((part)=>{ if(target.includes(part)){ score+=Math.min(part.length * 4, 24); } }); return { modelId, score }; }).filter((item)=>item.score>0).sort((a,b)=>b.score-a.score || a.modelId.localeCompare(b.modelId)); return ranked.length ? ranked[0].modelId : seed;}function getTriggerPatternValidationHint(pattern){ if((pattern?.type || 'exact') === 'regex'){ return pattern?.pattern ? { level:'ok', message:'regex pattern \u5DF2\u914D\u7F6E' } : { level:'warn', message:'regex \u6A21\u5F0F\u9700\u8981\u586B\u5199 pattern' }; } return Array.isArray(pattern?.keywords) && pattern.keywords.some((keyword)=>String(keyword || '').trim()) ? { level:'ok', message:'exact keywords \u5DF2\u914D\u7F6E' } : { level:'warn', message:'exact \u6A21\u5F0F\u81F3\u5C11\u9700\u8981\u4E00\u4E2A keyword' };}function getDraftSmartRouterConfig(config){ const smart={ ...((config && config.SmartRouter) || {}) }; const smartExplicit=config && Object.prototype.hasOwnProperty.call(config,'SmartRouter'); const legacyIntentEnabled=Boolean(config?.TriggerRouter?.llm_intent_recognition); const legacyIntentModel=config?.TriggerRouter?.intent_model || ''; if(!smart.analysis_scope && config?.TriggerRouter?.analysis_scope){ smart.analysis_scope=config.TriggerRouter.analysis_scope; } if((!Array.isArray(smart.rules) || !smart.rules.length) && Array.isArray(config?.TriggerRouter?.rules)){ smart.rules=config.TriggerRouter.rules; } if(!smart.semantic && (config?.Governance?.semantic || config?.TriggerRouter?.llm_intent_recognition)){ smart.semantic={ ...((config && config.Governance && config.Governance.semantic) || {}) }; if(config?.TriggerRouter?.llm_intent_recognition){ smart.semantic.enabled=true; smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || config.TriggerRouter.intent_model || ''; } } if(!smart.sticky && config?.Governance?.sticky){ smart.sticky={ ...(config.Governance.sticky || {}) }; } if(!smartExplicit && !smart.enabled && (config?.TriggerRouter?.enabled || smart.rules?.length || smart.router_model || smart.candidates?.length || smart.semantic || smart.sticky)){ smart.enabled=true; } if(smart.enabled){ smart.analysis_scope=smart.analysis_scope || 'last_message'; smart.semantic={ ...(smart.semantic || {}) }; smart.semantic.enabled=smart.semantic.enabled !== undefined ? smart.semantic.enabled : true; smart.semantic.threshold=smart.semantic.threshold !== undefined ? smart.semantic.threshold : 0.2; if(legacyIntentEnabled){ smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || legacyIntentModel; } smart.sticky={ ...(smart.sticky || {}) }; smart.sticky.enabled=smart.sticky.enabled !== undefined ? smart.sticky.enabled : true; smart.sticky.alignment={ ...((smart.sticky && smart.sticky.alignment) || {}) }; smart.sticky.alignment.enabled=smart.sticky.alignment.enabled !== undefined ? smart.sticky.alignment.enabled : true; smart.sticky.alignment.summarizer_model=smart.sticky.alignment.summarizer_model || smart.router_model || config?.Router?.default || legacyIntentModel || ''; } return smart;}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const smart=getDraftSmartRouterConfig(config); const triggerRules=Array.isArray(smart?.rules) ? smart.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(smart?.candidates) ? smart.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, smart?.router_model, smart?.sticky?.alignment?.summarizer_model, smart?.semantic?.classifier_model, config?.Governance?.shadow?.verifier_model].filter(v=>typeof v === 'string' && v.trim()).length + triggerRules.filter(rule=>rule?.model).length + smartCandidates.filter(item=>item?.model).length + cascadeLevels.reduce((sum,level)=>sum + (level?.from ? 1 : 0) + (level?.to ? 1 : 0), 0); draftSummaryGrid.innerHTML=[ ['Models', models.length], ['Routing rules', triggerRules.length], ['Patterns', patternCount], ['Smart candidates', smartCandidates.length], ['Cascade levels', cascadeLevels.length], ['Model refs', modelRefCount] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function updateStatusSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; modelCountStatus.textContent=String(models.length); routerDefaultStatus.textContent=config?.Router?.default || '-';}function renderDraftValidation(errors,warnings,issueReport){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; const contractIssues=Array.isArray(issueReport?.issues) ? issueReport.issues : []; if(!errorList.length && !warningList.length && !contractIssues.length){ draftValidationList.innerHTML='<div class="alert info"><strong>No validation issues</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u672A\u53D1\u73B0\u96C6\u4E2D\u5C55\u793A\u7684\u95EE\u9898</div></div>'; return; } const extractPath=(text)=>{ const match=String(text).match(/^(Models(?:\\[[0-9]+\\])?(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Router(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|TriggerRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|SmartRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Governance(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?)/); return match ? match[1] : ''; }; const sourceItems=contractIssues.length ? contractIssues.map(item=>({ text:String(item.message || ''), severity:item.severity==='error' ? 'error' : 'warning', path:item.path || '', action:item.action || '' })) : [...errorList.map(item=>({ text:String(item), severity:'error', path:'', action:'' })), ...warningList.map(item=>({ text:String(item), severity:'warning', path:'', action:'' }))]; const grouped=sourceItems.reduce((acc,item)=>{ const text=item.text; const path=item.path || extractPath(text); const bucket=path.startsWith('Models') || text.startsWith('Models') ? 'Models' : path.startsWith('Router') || text.startsWith('Router') ? 'Router' : path.startsWith('TriggerRouter') || text.startsWith('TriggerRouter') ? 'SmartRouter' : path.startsWith('SmartRouter') || text.startsWith('SmartRouter') ? 'SmartRouter' : (path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic') || text.startsWith('Governance.sticky') || text.startsWith('Governance.semantic')) ? 'SmartRouter' : path.startsWith('Governance') || text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path, severity:item.severity, action:item.action || '' }); return acc; }, {}); const errorCount=contractIssues.length ? contractIssues.filter(item=>item.severity==='error').length : errorList.length; const warningCount=contractIssues.length ? contractIssues.filter(item=>item.severity!=='error').length : warningList.length; const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorCount)+' errors / '+esc(warningCount)+' warnings</span></div><div class="muted">'+(errorCount ? '\u8BF7\u4F18\u5148\u4FEE\u590D errors\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u63A5\u53D7 warnings\u3002' : '\u5F53\u524D\u65E0\u963B\u65AD\u9519\u8BEF\uFF0C\u53EF\u6309\u9700\u5904\u7406 warnings\u3002')+'</div></div>'; draftValidationList.innerHTML=summary + Object.entries(grouped).map(([bucket,items])=>{ const hasError=items.some(item=>item.severity==='error'); const levelClass=hasError ? 'warn' : 'info'; const actionLabel=hasError ? 'repair first' : 'review before save'; return '<div class="alert '+levelClass+'"><div class="row"><strong>'+esc(bucket)+'</strong><span class="pill">'+esc(items.length)+' issues</span></div><div class="muted">'+esc(actionLabel)+'</div><div>'+items.slice(0,4).map(item=>'<div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+'<span class="pill">'+esc(item.severity==='error' ? 'error' : 'warning')+'</span> '+esc(item.text)+(item.action ? ('<div class="muted">Action: '+esc(item.action)+'</div>') : '')+'</div>').join('')+'</div></div>'; }).join('');}function getCapabilityWarningActionLabel(code){ if(code==='thinking_ignored'){ return '\u79FB\u9664 thinking'; } if(code==='tools_text_fallback' || code==='images_text_fallback'){ return '\u6062\u590D\u9ED8\u8BA4 capability'; } return '';}function renderCapabilityWarnings(report){ const entries=Array.isArray(report?.entries) ? report.entries : []; if(!entries.length){ capabilityWarningsList.innerHTML='<div class="alert info"><strong>No capability warnings</strong><div class="muted">\u5F53\u524D compiled models \u672A\u53D1\u73B0\u9700\u8981\u989D\u5916\u63D0\u793A\u7684\u80FD\u529B\u964D\u7EA7</div></div>'; return; } const summary=report?.summary || {}; capabilityWarningsList.innerHTML='<div class="alert info"><strong>Capability warning summary</strong><div class="muted">warn '+esc(summary.warn ?? 0)+' / info '+esc(summary.info ?? 0)+' / total '+esc(summary.total ?? entries.length)+'</div></div>' + entries.map(item=>{ const actionLabel=getCapabilityWarningActionLabel(item.code); return '<div class="alert '+esc(item.level === 'warn' ? 'warn' : 'info')+'"><div class="row"><strong>'+esc(item.code || item.level || 'warning')+'</strong><span class="pill">'+esc(item.modelId || '-').trim()+'</span></div><div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+esc(item.message || '')+'</div>'+(actionLabel ? ('<div class="row" style="margin-top:.5rem"><button type="button" data-apply-warning-path="'+esc(item.path || '')+'" data-apply-warning-code="'+esc(item.code || '')+'">'+esc(actionLabel)+'</button></div>') : '')+'</div>'; }).join('');}function findValidationTarget(path){ if(!path){ return null; } if(path.startsWith('Models')){ return modelsFormGrid; } if(path === 'Router.default'){ return draftRouterDefault; } if(path.startsWith('TriggerRouter.intent_model')){ return triggerIntentModel; } if(path.startsWith('TriggerRouter.rules[')){ return triggerRulesList; } if(path.startsWith('SmartRouter.router_model')){ return smartRouterModel; } if(path.startsWith('SmartRouter.candidates[')){ return smartCandidatesList; } if(path.startsWith('Governance.cascade.levels[')){ return governanceCascadeLevelsList; } if(path.startsWith('Governance.sticky.alignment')){ return governanceSummarizerModel; } if(path.startsWith('Governance.semantic')){ return governanceClassifierModel; } if(path.startsWith('Governance.shadow')){ return governanceVerifierModel; } if(path.startsWith('Governance')){ return governanceEnabled; } return null;}function jumpToValidationPath(path){ const target=findValidationTarget(path); if(!target || typeof target.scrollIntoView !== 'function'){ return; } if(activeValidationHighlight && activeValidationHighlight.classList){ activeValidationHighlight.classList.remove('jump-highlight'); } target.scrollIntoView({ behavior:'smooth', block:'center' }); if(target.classList){ target.classList.add('jump-highlight'); activeValidationHighlight=target; setTimeout(()=>{ if(target.classList){ target.classList.remove('jump-highlight'); if(activeValidationHighlight===target){ activeValidationHighlight=null; } } }, 1800); } if(typeof target.focus === 'function'){ target.focus({ preventScroll:true }); }}function renderDraftPresetModeHint(){ const overwriteMode=draftPresetMode.value === 'replace'; draftPresetModeHint.textContent=overwriteMode ? 'overwrite \u4F1A\u91CD\u7F6E SmartRouter / Governance \u76F8\u5173\u8868\u5355\uFF0C\u518D\u5E94\u7528\u9884\u8BBE' : 'append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145 SmartRouter / Governance \u76F8\u5173\u5B57\u6BB5';}function deriveActualAffectedAreas(preview){ const areas=new Set(); const diff=preview?.diff || {}; const impact=preview?.referenceImpact || {}; if((diff.providerChanges || []).length || (diff.modelChanges || []).length){ areas.add('Models'); } (impact.entries || []).forEach((entry)=>{ const path=String(entry.path || ''); if(path.startsWith('Router.')){ areas.add('Router'); } else if(path.startsWith('TriggerRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.')){ areas.add('Governance'); } }); return Array.from(areas);}function renderDraftPreviewMeta(meta){ if(!meta){ draftPreviewMeta.innerHTML='<div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div>'; return; } draftPreviewMeta.innerHTML='<div class="alert info"><strong>'+esc(meta.title || 'Preset dry-run')+'</strong><div>'+esc(meta.description || '')+'</div><div class="muted">\u6A21\u5F0F\uFF1A'+esc(meta.mode || '-')+' \xB7 \u9884\u8BBE\u58F0\u660E\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((meta.affects || []).join(' / ') || '-')</div><div class="muted">\u5B9E\u9645\u9884\u89C8\u547D\u4E2D\u533A\u57DF\uFF1A'+esc((meta.actualAffects || []).join(' / ') || '-')</div></div>';}function renderDraftPresetGuide(){ draftPresetList.innerHTML=Object.entries(draftPresets).map(([key,preset])=>'<div class="alert info"><strong>'+esc(preset.label || key)+'</strong><div>'+esc(preset.description || '')+'</div><div class="muted">\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((preset.affects || []).join(' / '))+'</div></div>').join('');}function updateTopLevelModelSuggestionLists(){ const markup=knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join(''); [topLevelTriggerIntentSuggestions,topLevelSmartRouterSuggestions,topLevelGovernanceSummarizerSuggestions,topLevelGovernanceClassifierSuggestions,topLevelGovernanceVerifierSuggestions].forEach(node=>{ if(node){ node.innerHTML=markup; } });}function renderModelsForm(models){ const list=Array.isArray(models) ? models : []; draftModelsCount.value=String(list.length); if(!list.length){ modelsFormGrid.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div>'; return; } modelsFormGrid.innerHTML=list.map((model,index)=>{ const templateContext=getProviderTemplateContext(model); const template=templateContext.template; return '<div class="model-card" data-model-card="'+index+'">' + '<div class="model-card-header"><strong>Model #'+(index+1)+'</strong><button type="button" data-remove-model="'+index+'">\u5220\u9664</button></div>' + '<div class="model-card-grid">' + '<div><label>Provider template</label><div class="row"><select data-field="provider_template" data-index="'+index+'"><option value="">custom</option>'+Object.entries(modelProviderTemplates).map(([key,item])=>'<option value="'+esc(key)+'"'+(model.provider_template === key ? ' selected' : '')+'>'+esc(item.label)+'</option>').join('')+'</select><button type="button" data-apply-template="'+index+'">\u5E94\u7528</button></div></div>' + '<div><label>ID</label><input data-field="id" data-index="'+index+'" value="'+esc(model.id || '')+'" placeholder="'+esc(template.suggested_id || 'sonnet')+'"><div class="muted">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>Reasoning support</label><select data-field="supports_reasoning" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_reasoning === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_reasoning === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Tool support</label><select data-field="supports_tools" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_tools === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_tools === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Image support</label><select data-field="supports_images" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_images === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_images === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div style="grid-column:1/-1"><label>Metadata (advanced JSON)</label><textarea data-field="metadata" data-index="'+index+'" placeholder="{\\"label\\":\\"Balanced profile\\"}">'+esc(model.metadata ? JSON.stringify(model.metadata, null, 2) : '')+'</textarea><div class="muted">\u666E\u901A capability \u5EFA\u8BAE\u4F18\u5148\u4F7F\u7528\u4E0A\u9762\u7684\u663E\u5F0F\u5B57\u6BB5\uFF1B\u8FD9\u91CC\u4FDD\u7559\u7ED9\u9AD8\u7EA7\u6269\u5C55\u5143\u6570\u636E\u3002</div></div>' + '</div>' + '</div>'; }).join('');}function extractModelsFromForm(){ const cards=Array.from(modelsFormGrid.querySelectorAll('[data-model-card]')); return cards.map((card,index)=>{ const read=(field)=>card.querySelector('[data-field="'+field+'"][data-index="'+index+'"]'); const providerTemplate=(read('provider_template')?.value || '').trim(); const metadataRaw=(read('metadata')?.value || '').trim(); let metadata; if(metadataRaw){ metadata=JSON.parse(metadataRaw); } else { metadata={}; } const thinkingProfile=(read('thinking_profile')?.value || '').trim(); const vendorHint=(read('vendor_hint')?.value || '').trim(); const supportsReasoning=(read('supports_reasoning')?.value || '').trim(); const supportsTools=(read('supports_tools')?.value || '').trim(); const supportsImages=(read('supports_images')?.value || '').trim(); const thinking={}; const mode=(read('thinking_mode')?.value || '').trim(); const effort=(read('thinking_effort')?.value || '').trim(); const budget=(read('thinking_budget_tokens')?.value || '').trim(); if(mode) thinking.mode=mode; if(effort) thinking.effort=effort; if(budget) thinking.budget_tokens=Number(budget); const model={ id:(read('id')?.value || '').trim(), api:(read('api')?.value || '').trim(), key:(read('key')?.value || '').trim(), interface:(read('interface')?.value || '').trim(), model:(read('model')?.value || '').trim(), }; if(vendorHint){ metadata.vendor_hint=vendorHint; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'vendor_hint')){ delete metadata.vendor_hint; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); const smartCandidates=extractSmartCandidatesFromForm(); const smartRouterEnabled=Boolean(smartEnabled.checked || triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim() || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim()); if(smartRouterEnabled){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: true, analysis_scope: triggerAnalysisScope.value || payload.SmartRouter?.analysis_scope || 'last_message', router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined, rules: triggerRules, semantic:(governanceSemanticEnabled.checked || triggerIntentEnabled.checked || governanceClassifierModel.value.trim() || triggerIntentModel.value.trim()) ? { ...(((payload.SmartRouter || {}).semantic) || {}), enabled:Boolean(governanceSemanticEnabled.checked || triggerIntentEnabled.checked), mode:'classifier', classifier_model: governanceClassifierModel.value.trim() || triggerIntentModel.value.trim() } : undefined, sticky:(governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim()) ? { ...(((payload.SmartRouter || {}).sticky) || {}), enabled:true, alignment:{ ...((((payload.SmartRouter || {}).sticky || {}).alignment) || {}), enabled:Boolean(governanceAlignmentEnabled.checked), summarizer_model: governanceSummarizerModel.value.trim() } } : undefined }; } else { delete payload.SmartRouter; } delete payload.TriggerRouter; const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const smart=getDraftSmartRouterConfig(config); const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(smart.enabled); triggerIntentEnabled.checked=Boolean(smart.semantic?.enabled && smart.semantic?.mode === 'classifier'); triggerAnalysisScope.value=smart.analysis_scope || 'last_message'; triggerIntentModel.value=smart.semantic?.classifier_model || trigger.intent_model || ''; renderTriggerRulesList(smart.rules || trigger.rules || []); smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(smart.sticky?.alignment?.enabled); governanceSummarizerModel.value=smart.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(smart.semantic?.enabled); governanceClassifierModel.value=smart.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u540C\u6B65 Models \u8868\u5355\u5230 JSON \u8349\u7A3F'; } catch (error) { draftPreviewStatus.textContent='\u540C\u6B65\u5931\u8D25\uFF1A'+error.message; }}function applyReferenceSuggestion(path,modelId){ if(!modelId){ return; } if(path==='Router.default'){ draftRouterDefault.value=modelId; syncDraftEditorFromForm(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 Router.default'; return; } const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const pathMatch=path.match(/^([^.[]+)(?:.(.+))?$/); if(!pathMatch){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\uFF1A'+path; return; } const tokens=path.replace(/[(d+)]/g,'.$1').split('.'); let cursor=payload; for(let i=0;i<tokens.length-1;i++){ const token=tokens[i]; const nextToken=tokens[i+1]; if(cursor[token] === undefined){ cursor[token]=String(Number(nextToken))===nextToken ? [] : {}; } cursor=cursor[token]; } cursor[tokens[tokens.length-1]]=modelId; currentDraftConfig=payload; if(payload.Router?.default){ draftRouterDefault.value=payload.Router.default; } renderConfigControlForms(payload); configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyCapabilityWarningSuggestion(path,code){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const tokens=String(path || '').replace(/[(d+)]/g,'.$1').split('.').filter(Boolean); if(!tokens.length){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } let cursor=payload; for(let i=0;i<tokens.length-1;i++){ if(cursor == null){ break; } cursor=cursor[tokens[i]]; } const lastToken=tokens[tokens.length-1]; if(code==='thinking_ignored'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } } else if(code==='tools_text_fallback' || code==='images_text_fallback'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } if(cursor && !Object.keys(cursor).length){ const parentTokens=tokens.slice(0,-1); const maybeMetadataKey=parentTokens[parentTokens.length-1]; if(maybeMetadataKey==='metadata'){ let parentCursor=payload; for(let i=0;i<parentTokens.length-1;i++){ if(parentCursor == null){ break; } parentCursor=parentCursor[parentTokens[i]]; } if(parentCursor && Object.prototype.hasOwnProperty.call(parentCursor,'metadata')){ delete parentCursor.metadata; } } } } else { draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528 warning \u4FEE\u6B63\uFF1A'+code+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function renderCompiledDiff(diff){ const summary=diff?.summary || {}; compiledDiffSummary.innerHTML=[ ['Added providers', summary.addedProviders ?? 0], ['Removed providers', summary.removedProviders ?? 0], ['Changed providers', summary.changedProviders ?? 0], ['Added models', summary.addedModels ?? 0], ['Removed models', summary.removedModels ?? 0], ['Changed models', summary.changedModels ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const rows=[ ...((diff?.providerChanges || []).map(item=>({ scope:'provider', key:item.name, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ...((diff?.modelChanges || []).map(item=>({ scope:'model', key:item.modelId, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ]; compiledDiffTableBody.innerHTML=rows.length ? rows.map(item=>'<tr>' + '<td>'+esc(item.scope)+'</td>' + '<td>'+esc(item.type)+'</td>' + '<td><code>'+esc(item.key)+'</code></td>' + '<td>'+esc(item.fields.join(', ') || '-')+'</td>' + '<td><code>'+esc(item.target.providerName || item.target.name || '-')+'</code><div class="muted">'+esc(item.target.modelName || (item.target.models || []).join(', ') || '-')}</div></td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled registry changes</td></tr>';}function renderReferenceImpact(impact){ const summary=impact?.summary || {}; referenceImpactSummary.innerHTML=[ ['Total refs', summary.total ?? 0], ['modelId refs', summary.modelIdRefs ?? 0], ['Legacy refs', summary.legacyRefs ?? 0], ['Valid modelIds', summary.validModelIds ?? 0], ['Missing modelIds', summary.missingModelIds ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const entries=impact?.entries || []; referenceImpactTableBody.innerHTML=entries.length ? entries.map(item=>'<tr>' + '<td><code>'+esc(item.path)+'</code></td>' + '<td><code>'+esc(item.value)+'</code></td>' + '<td>'+esc(item.referenceType)+'</td>' + '<td>'+esc(item.status)+'</td>' + '<td><code>'+esc(item.resolvedTarget?.providerName || '-')+'</code><div class="muted">'+esc(item.resolvedTarget?.modelName || '-')}</div></td>' + '<td>'+((item.suggestions || []).length ? item.suggestions.map(s=>'<div><code>'+esc(s.modelId)+'</code><div class="muted">'+esc(s.modelName || '-')+'</div><button type="button" data-apply-reference-path="'+esc(item.path)+'" data-apply-reference-model="'+esc(s.modelId)+'">\u5E94\u7528\u5EFA\u8BAE</button></div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No model references found</td></tr>';}function renderCompiledModels(data){ const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04'; compiledProvidersTableBody.innerHTML=providers.length ? providers.map(provider=>'<tr>' + '<td><code>'+esc(provider.name)+'</code><div class="muted">'+esc(provider.api_base_url || '-')+'</div></td>' + '<td>'+esc(provider.transformer?.use?.[0] || '-')+'</td>' + '<td>'+esc((provider.models || []).join(', ') || '-')+'</td>' + '<td>'+esc(JSON.stringify(provider.transformer || {}))+'</td>' + '<td>'+esc(provider.has_api_key ? 'configured' : 'missing')+'</td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled providers</td></tr>'; compiledModelMapTableBody.innerHTML=modelMapEntries.length ? modelMapEntries.map(([modelId,item])=>'<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td><code>'+esc(item.providerName || '-')+'</code><div class="muted">'+esc(item.modelName || '-')+'</div></td>' + '<td>'+esc(item.protocol || '-')+'</td>' + '<td>'+esc(item.compatibilityProfile || '-')+'</td>' + '<td>'+esc(item.dispatchFormat || '-')+'</td>' + '<td><code>'+esc(JSON.stringify(item.thinking || { mode: 'off' }))+'</code></td>' + '<td><code>'+esc(JSON.stringify(item.capabilities || {}))+'</code></td>' + '<td>'+esc(item.source || '-')+'</td>' + '</tr>').join('') : '<tr><td colspan="8" class="muted">No compiled model map</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); updateStatusSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function loadServiceStatus(){ serviceReadyStatus.textContent='checking'; try { const [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 || '-'; 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'; if(remoteData.compiledModels){ modelCountStatus.textContent=remoteData.compiledModels.modelCount ?? modelCountStatus.textContent; } } catch (_error) { serviceReadyStatus.textContent='unreachable'; remoteStatusSummary.textContent='unknown'; }}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || [], data.issueReport); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addCascadeLevel(){ const next=extractCascadeLevelsFromForm(); next.push({ from:'', to:'' }); renderCascadeLevelsList(next); syncDraftEditorFromForm(); }modelsFormGrid.addEventListener('input',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('change',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-template]'); if(applyBtn){ const applyIndex=Number(applyBtn.dataset.applyTemplate); applyProviderTemplate(applyIndex); syncDraftEditorFromForm(); return; } const btn=e.target.closest('button[data-remove-model]'); if(!btn){ return; } const removeIndex=Number(btn.dataset.removeModel); const nextModels=extractModelsFromForm().filter((_,index)=>index!==removeIndex); renderModelsForm(nextModels); syncDraftEditorFromForm(); });triggerRulesList.addEventListener('input',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('change',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('click',(e)=>{ const addKeywordBtn=e.target.closest('button[data-add-trigger-keyword]'); if(addKeywordBtn){ addTriggerKeyword(Number(addKeywordBtn.dataset.addTriggerKeyword), Number(addKeywordBtn.dataset.patternIndex)); return; } const removeKeywordBtn=e.target.closest('button[data-remove-trigger-keyword]'); if(removeKeywordBtn){ const ruleIndex=Number(removeKeywordBtn.dataset.removeTriggerKeyword); const patternIndex=Number(removeKeywordBtn.dataset.patternIndex); const keywordIndex=Number(removeKeywordBtn.dataset.keywordIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex] && next[ruleIndex].patterns && next[ruleIndex].patterns[patternIndex]){ const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=(pattern.keywords || []).filter((_,index)=>index!==keywordIndex); if(!pattern.keywords.length){ pattern.keywords=['']; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const addBtn=e.target.closest('button[data-add-trigger-pattern]'); if(addBtn){ addTriggerPattern(Number(addBtn.dataset.addTriggerPattern)); return; } const removePatternBtn=e.target.closest('button[data-remove-trigger-pattern]'); if(removePatternBtn){ const ruleIndex=Number(removePatternBtn.dataset.removeTriggerPattern); const patternIndex=Number(removePatternBtn.dataset.patternIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex]){ next[ruleIndex].patterns=(next[ruleIndex].patterns || []).filter((_,index)=>index!==patternIndex); if(!next[ruleIndex].patterns.length){ next[ruleIndex].patterns=[{ type:'exact', keywords:[] }]; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const btn=e.target.closest('button[data-remove-trigger-rule]'); if(!btn){ return; } const next=extractTriggerRulesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeTriggerRule)); renderTriggerRulesList(next); syncDraftEditorFromForm(); });smartCandidatesList.addEventListener('input',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('change',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-smart-candidate]'); if(!btn){ return; } const next=extractSmartCandidatesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeSmartCandidate)); renderSmartCandidatesList(next); syncDraftEditorFromForm(); });governanceCascadeLevelsList.addEventListener('input',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('change',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-cascade-level]'); if(!btn){ return; } const next=extractCascadeLevelsFromForm().filter((_,index)=>index!==Number(btn.dataset.removeCascadeLevel)); renderCascadeLevelsList(next); syncDraftEditorFromForm(); });referenceImpactTableBody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-apply-reference-path]'); if(!btn){ return; } applyReferenceSuggestion(btn.dataset.applyReferencePath, btn.dataset.applyReferenceModel); });draftValidationList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });capabilityWarningsList.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-warning-path]'); if(applyBtn){ applyCapabilityWarningSuggestion(applyBtn.dataset.applyWarningPath, applyBtn.dataset.applyWarningCode); return; } const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });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){ 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)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.SmartRouter?.enabled), analysis_scope: payload.SmartRouter?.analysis_scope || 'last_message', router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: payload.SmartRouter?.candidates || [], cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.SmartRouter?.rules || []) }; delete payload.TriggerRouter; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: payload.SmartRouter?.rules || [] }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: payload.SmartRouter?.enabled !== undefined ? Boolean(payload.SmartRouter?.enabled) : Boolean(preset.governanceEnabled), sticky:{ ...((payload.SmartRouter && payload.SmartRouter.sticky) || {}), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.enabled), alignment:{ ...(((payload.SmartRouter && payload.SmartRouter.sticky && payload.SmartRouter.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.SmartRouter?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.SmartRouter && payload.SmartRouter.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.SmartRouter?.semantic?.enabled), mode:(payload.SmartRouter?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.SmartRouter?.semantic?.classifier_model || '') } }; payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderAnomalies(anomalies,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); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || [],health); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderTrendTable(metricsData || {}); const traces=data.traces || []; if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftHeroBtn').addEventListener('click',loadConfigDraft);document.getElementById('previewConfigDraftHeroBtn').addEventListener('click',previewConfigDraft);document.getElementById('refreshStatusHeroBtn').addEventListener('click',loadServiceStatus);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadServiceStatus();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`;
4473
+ return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Claude Trigger Router</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.hero{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(260px,.8fr);gap:1rem;align-items:stretch;margin-bottom:1rem}.hero h2{margin:.2rem 0 .5rem;font-size:1.55rem}.hero-copy{display:flex;flex-direction:column;justify-content:center}.status-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.status-tile{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem;min-width:0}.status-tile strong{display:block;margin-top:.2rem;word-break:break-word}@media (max-width:760px){.hero{grid-template-columns:1fr}.status-grid{grid-template-columns:1fr}}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:.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">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><div class="surface-tabs" role="tablist" aria-label="\u5DE5\u4F5C\u53F0\u5207\u6362"><button id="userSurfaceTab" class="surface-tab active" type="button" role="tab" aria-selected="true" data-surface-target="user">\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</button><button id="maintainerSurfaceTab" class="surface-tab" type="button" role="tab" aria-selected="false" data-surface-target="maintainer">\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</button></div><section id="userSurface" class="surface-panel" data-surface="user"><div class="panel"><div class="surface-heading"><strong>\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u914D\u7F6E\u3001\u6A21\u578B\u3001\u8DEF\u7531\u3001\u670D\u52A1\u72B6\u6001\u4E0E\u4E0B\u4E00\u6B65\u4FDD\u5B58\u52A8\u4F5C\u3002</span></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Routing rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u56F4\u7ED5 SmartRouter \u7EDF\u4E00\u8DEF\u7531\u5F15\u64CE\u7F16\u8F91\u89C4\u5219\u3001\u5019\u9009\u4E0E\u6CBB\u7406\u589E\u5F3A\u517C\u5BB9\u914D\u7F6E</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Routing rules</strong><span class="muted">\u663E\u5F0F\u89C4\u5219\u3001\u8BED\u4E49\u63D0\u793A\u4E0E\u517C\u5BB9\u8F93\u5165</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5F71\u5B50\u6821\u9A8C\u3001\u7EA7\u8054\u4E0E\u89C2\u6D4B\u76F8\u5173\u914D\u7F6E</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div 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\u4E3A priority \u8C03\u5EA6\u5951\u7EA6</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">\u7EF4\u62A4\u8005\u4F7F\u7528\uFF1A/ui\u3001\u914D\u7F6E\u4FDD\u5B58\u3001\u91CD\u542F\u3001auth \u7BA1\u7406\u548C\u6CBB\u7406\u5199\u64CD\u4F5C\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\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/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><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="row"><strong>\u7EF4\u62A4\u8005\u89C2\u6D4B</strong><span class="muted">\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4 Governance Trace\uFF0C\u5E76\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u6307\u6807\u3002</span></div><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/health</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">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>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><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><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const serviceReadyStatus=document.getElementById('serviceReadyStatus');const servicePortStatus=document.getElementById('servicePortStatus');const 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 securitySummary=document.getElementById('securitySummary');const authQuotaTableBody=document.querySelector('#authQuotaTable 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 activeValidationHighlight=null;const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94\u8DEF\u7531\u89C4\u5219\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u589E\u5F3A\u4E0E\u6821\u9A8C\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','SmartRouter.sticky.alignment','SmartRouter.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function 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 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>Reasoning support</label><select data-field="supports_reasoning" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_reasoning === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_reasoning === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Tool support</label><select data-field="supports_tools" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_tools === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_tools === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Image support</label><select data-field="supports_images" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_images === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_images === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div style="grid-column:1/-1"><label>Metadata (advanced JSON)</label><textarea data-field="metadata" data-index="'+index+'" placeholder="{\\"label\\":\\"Balanced profile\\"}">'+esc(model.metadata ? JSON.stringify(model.metadata, null, 2) : '')+'</textarea><div class="muted">\u666E\u901A capability \u5EFA\u8BAE\u4F18\u5148\u4F7F\u7528\u4E0A\u9762\u7684\u663E\u5F0F\u5B57\u6BB5\uFF1B\u8FD9\u91CC\u4FDD\u7559\u7ED9\u9AD8\u7EA7\u6269\u5C55\u5143\u6570\u636E\u3002</div></div>' + '</div>' + '</div>'; }).join('');}function extractModelsFromForm(){ const cards=Array.from(modelsFormGrid.querySelectorAll('[data-model-card]')); return cards.map((card,index)=>{ const read=(field)=>card.querySelector('[data-field="'+field+'"][data-index="'+index+'"]'); const providerTemplate=(read('provider_template')?.value || '').trim(); const metadataRaw=(read('metadata')?.value || '').trim(); let metadata; if(metadataRaw){ metadata=JSON.parse(metadataRaw); } else { metadata={}; } const thinkingProfile=(read('thinking_profile')?.value || '').trim(); const vendorHint=(read('vendor_hint')?.value || '').trim(); const supportsReasoning=(read('supports_reasoning')?.value || '').trim(); const supportsTools=(read('supports_tools')?.value || '').trim(); const supportsImages=(read('supports_images')?.value || '').trim(); const thinking={}; const mode=(read('thinking_mode')?.value || '').trim(); const effort=(read('thinking_effort')?.value || '').trim(); const budget=(read('thinking_budget_tokens')?.value || '').trim(); if(mode) thinking.mode=mode; if(effort) thinking.effort=effort; if(budget) thinking.budget_tokens=Number(budget); const model={ id:(read('id')?.value || '').trim(), api:(read('api')?.value || '').trim(), key:(read('key')?.value || '').trim(), interface:(read('interface')?.value || '').trim(), model:(read('model')?.value || '').trim(), }; if(vendorHint){ metadata.vendor_hint=vendorHint; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'vendor_hint')){ delete metadata.vendor_hint; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); const smartCandidates=extractSmartCandidatesFromForm(); const smartRouterEnabled=Boolean(smartEnabled.checked || triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim() || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim()); if(smartRouterEnabled){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: true, analysis_scope: triggerAnalysisScope.value || payload.SmartRouter?.analysis_scope || 'last_message', router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined, rules: triggerRules, semantic:(governanceSemanticEnabled.checked || triggerIntentEnabled.checked || governanceClassifierModel.value.trim() || triggerIntentModel.value.trim()) ? { ...(((payload.SmartRouter || {}).semantic) || {}), enabled:Boolean(governanceSemanticEnabled.checked || triggerIntentEnabled.checked), mode:'classifier', classifier_model: governanceClassifierModel.value.trim() || triggerIntentModel.value.trim() } : undefined, sticky:(governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim()) ? { ...(((payload.SmartRouter || {}).sticky) || {}), enabled:true, alignment:{ ...((((payload.SmartRouter || {}).sticky || {}).alignment) || {}), enabled:Boolean(governanceAlignmentEnabled.checked), summarizer_model: governanceSummarizerModel.value.trim() } } : undefined }; } else { delete payload.SmartRouter; } delete payload.TriggerRouter; const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const smart=getDraftSmartRouterConfig(config); const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(smart.enabled); triggerIntentEnabled.checked=Boolean(smart.semantic?.enabled && smart.semantic?.mode === 'classifier'); triggerAnalysisScope.value=smart.analysis_scope || 'last_message'; triggerIntentModel.value=smart.semantic?.classifier_model || trigger.intent_model || ''; renderTriggerRulesList(smart.rules || trigger.rules || []); smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(smart.sticky?.alignment?.enabled); governanceSummarizerModel.value=smart.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(smart.semantic?.enabled); governanceClassifierModel.value=smart.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u540C\u6B65 Models \u8868\u5355\u5230 JSON \u8349\u7A3F'; } catch (error) { draftPreviewStatus.textContent='\u540C\u6B65\u5931\u8D25\uFF1A'+error.message; }}function applyReferenceSuggestion(path,modelId){ if(!modelId){ return; } if(path==='Router.default'){ draftRouterDefault.value=modelId; syncDraftEditorFromForm(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 Router.default'; return; } const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const pathMatch=path.match(/^([^.[]+)(?:.(.+))?$/); if(!pathMatch){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\uFF1A'+path; return; } const tokens=path.replace(/[(d+)]/g,'.$1').split('.'); let cursor=payload; for(let i=0;i<tokens.length-1;i++){ const token=tokens[i]; const nextToken=tokens[i+1]; if(cursor[token] === undefined){ cursor[token]=String(Number(nextToken))===nextToken ? [] : {}; } cursor=cursor[token]; } cursor[tokens[tokens.length-1]]=modelId; currentDraftConfig=payload; if(payload.Router?.default){ draftRouterDefault.value=payload.Router.default; } renderConfigControlForms(payload); configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyCapabilityWarningSuggestion(path,code){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const tokens=String(path || '').replace(/[(d+)]/g,'.$1').split('.').filter(Boolean); if(!tokens.length){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } let cursor=payload; for(let i=0;i<tokens.length-1;i++){ if(cursor == null){ break; } cursor=cursor[tokens[i]]; } const lastToken=tokens[tokens.length-1]; if(code==='thinking_ignored'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } } else if(code==='tools_text_fallback' || code==='images_text_fallback'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } if(cursor && !Object.keys(cursor).length){ const parentTokens=tokens.slice(0,-1); const maybeMetadataKey=parentTokens[parentTokens.length-1]; if(maybeMetadataKey==='metadata'){ let parentCursor=payload; for(let i=0;i<parentTokens.length-1;i++){ if(parentCursor == null){ break; } parentCursor=parentCursor[parentTokens[i]]; } if(parentCursor && Object.prototype.hasOwnProperty.call(parentCursor,'metadata')){ delete parentCursor.metadata; } } } } else { draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528 warning \u4FEE\u6B63\uFF1A'+code+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function renderCompiledDiff(diff){ const summary=diff?.summary || {}; compiledDiffSummary.innerHTML=[ ['Added providers', summary.addedProviders ?? 0], ['Removed providers', summary.removedProviders ?? 0], ['Changed providers', summary.changedProviders ?? 0], ['Added models', summary.addedModels ?? 0], ['Removed models', summary.removedModels ?? 0], ['Changed models', summary.changedModels ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const rows=[ ...((diff?.providerChanges || []).map(item=>({ scope:'provider', key:item.name, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ...((diff?.modelChanges || []).map(item=>({ scope:'model', key:item.modelId, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ]; compiledDiffTableBody.innerHTML=rows.length ? rows.map(item=>'<tr>' + '<td>'+esc(item.scope)+'</td>' + '<td>'+esc(item.type)+'</td>' + '<td><code>'+esc(item.key)+'</code></td>' + '<td>'+esc(item.fields.join(', ') || '-')+'</td>' + '<td><code>'+esc(item.target.providerName || item.target.name || '-')+'</code><div class="muted">'+esc(item.target.modelName || (item.target.models || []).join(', ') || '-')}</div></td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled registry changes</td></tr>';}function renderReferenceImpact(impact){ const summary=impact?.summary || {}; referenceImpactSummary.innerHTML=[ ['Total refs', summary.total ?? 0], ['modelId refs', summary.modelIdRefs ?? 0], ['Legacy refs', summary.legacyRefs ?? 0], ['Valid modelIds', summary.validModelIds ?? 0], ['Missing modelIds', summary.missingModelIds ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const entries=impact?.entries || []; referenceImpactTableBody.innerHTML=entries.length ? entries.map(item=>'<tr>' + '<td><code>'+esc(item.path)+'</code></td>' + '<td><code>'+esc(item.value)+'</code></td>' + '<td>'+esc(item.referenceType)+'</td>' + '<td>'+esc(item.status)+'</td>' + '<td><code>'+esc(item.resolvedTarget?.providerName || '-')+'</code><div class="muted">'+esc(item.resolvedTarget?.modelName || '-')}</div></td>' + '<td>'+((item.suggestions || []).length ? item.suggestions.map(s=>'<div><code>'+esc(s.modelId)+'</code><div class="muted">'+esc(s.modelName || '-')+'</div><button type="button" data-apply-reference-path="'+esc(item.path)+'" data-apply-reference-model="'+esc(s.modelId)+'">\u5E94\u7528\u5EFA\u8BAE</button></div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No model references found</td></tr>';}function renderCompiledModels(data){ const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); 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); 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=>'<div><code>'+esc(endpoint.id)+'</code><span class="muted"> priority '+esc(endpoint.priority)+' / '+esc(endpoint.enabled ? 'enabled' : 'disabled')+'</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(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function loadServiceStatus(){ serviceReadyStatus.textContent='checking'; try { const [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'; if(remoteData.compiledModels){ modelCountStatus.textContent=remoteData.compiledModels.modelCount ?? modelCountStatus.textContent; } } catch (_error) { serviceReadyStatus.textContent='unreachable'; remoteStatusSummary.textContent='unknown'; securityStatusSummary.textContent='unknown'; }}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || [], data.issueReport); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addCascadeLevel(){ const next=extractCascadeLevelsFromForm(); next.push({ from:'', to:'' }); renderCascadeLevelsList(next); syncDraftEditorFromForm(); }modelsFormGrid.addEventListener('input',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('change',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-template]'); if(applyBtn){ const applyIndex=Number(applyBtn.dataset.applyTemplate); applyProviderTemplate(applyIndex); syncDraftEditorFromForm(); return; } const btn=e.target.closest('button[data-remove-model]'); if(!btn){ return; } const removeIndex=Number(btn.dataset.removeModel); const nextModels=extractModelsFromForm().filter((_,index)=>index!==removeIndex); renderModelsForm(nextModels); syncDraftEditorFromForm(); });triggerRulesList.addEventListener('input',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('change',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('click',(e)=>{ const addKeywordBtn=e.target.closest('button[data-add-trigger-keyword]'); if(addKeywordBtn){ addTriggerKeyword(Number(addKeywordBtn.dataset.addTriggerKeyword), Number(addKeywordBtn.dataset.patternIndex)); return; } const removeKeywordBtn=e.target.closest('button[data-remove-trigger-keyword]'); if(removeKeywordBtn){ const ruleIndex=Number(removeKeywordBtn.dataset.removeTriggerKeyword); const patternIndex=Number(removeKeywordBtn.dataset.patternIndex); const keywordIndex=Number(removeKeywordBtn.dataset.keywordIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex] && next[ruleIndex].patterns && next[ruleIndex].patterns[patternIndex]){ const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=(pattern.keywords || []).filter((_,index)=>index!==keywordIndex); if(!pattern.keywords.length){ pattern.keywords=['']; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const addBtn=e.target.closest('button[data-add-trigger-pattern]'); if(addBtn){ addTriggerPattern(Number(addBtn.dataset.addTriggerPattern)); return; } const removePatternBtn=e.target.closest('button[data-remove-trigger-pattern]'); if(removePatternBtn){ const ruleIndex=Number(removePatternBtn.dataset.removeTriggerPattern); const patternIndex=Number(removePatternBtn.dataset.patternIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex]){ next[ruleIndex].patterns=(next[ruleIndex].patterns || []).filter((_,index)=>index!==patternIndex); if(!next[ruleIndex].patterns.length){ next[ruleIndex].patterns=[{ type:'exact', keywords:[] }]; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const btn=e.target.closest('button[data-remove-trigger-rule]'); if(!btn){ return; } const next=extractTriggerRulesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeTriggerRule)); renderTriggerRulesList(next); syncDraftEditorFromForm(); });smartCandidatesList.addEventListener('input',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('change',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-smart-candidate]'); if(!btn){ return; } const next=extractSmartCandidatesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeSmartCandidate)); renderSmartCandidatesList(next); syncDraftEditorFromForm(); });governanceCascadeLevelsList.addEventListener('input',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('change',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-cascade-level]'); if(!btn){ return; } const next=extractCascadeLevelsFromForm().filter((_,index)=>index!==Number(btn.dataset.removeCascadeLevel)); renderCascadeLevelsList(next); syncDraftEditorFromForm(); });referenceImpactTableBody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-apply-reference-path]'); if(!btn){ return; } applyReferenceSuggestion(btn.dataset.applyReferencePath, btn.dataset.applyReferenceModel); });draftValidationList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });capabilityWarningsList.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-warning-path]'); if(applyBtn){ applyCapabilityWarningSuggestion(applyBtn.dataset.applyWarningPath, applyBtn.dataset.applyWarningCode); return; } const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });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)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.SmartRouter?.enabled), analysis_scope: payload.SmartRouter?.analysis_scope || 'last_message', router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: payload.SmartRouter?.candidates || [], cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.SmartRouter?.rules || []) }; delete payload.TriggerRouter; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: payload.SmartRouter?.rules || [] }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: payload.SmartRouter?.enabled !== undefined ? Boolean(payload.SmartRouter?.enabled) : Boolean(preset.governanceEnabled), sticky:{ ...((payload.SmartRouter && payload.SmartRouter.sticky) || {}), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.enabled), alignment:{ ...(((payload.SmartRouter && payload.SmartRouter.sticky && payload.SmartRouter.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.SmartRouter?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.SmartRouter && payload.SmartRouter.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.SmartRouter?.semantic?.enabled), mode:(payload.SmartRouter?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.SmartRouter?.semantic?.classifier_model || '') } }; payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function 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 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); 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 || []; if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftHeroBtn').addEventListener('click',loadConfigDraft);document.getElementById('previewConfigDraftHeroBtn').addEventListener('click',previewConfigDraft);document.getElementById('refreshStatusHeroBtn').addEventListener('click',loadServiceStatus);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadServiceStatus();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`;
3656
4474
  }
3657
4475
  var init_workbench = __esm({
3658
4476
  "src/ui/workbench.ts"() {
3659
4477
  "use strict";
3660
4478
  init_provider_presets();
4479
+ init_runtime_role_guidance();
3661
4480
  }
3662
4481
  });
3663
4482
 
@@ -3672,7 +4491,8 @@ function toCompiledRegistryView(config) {
3672
4491
  transformer: provider.transformer,
3673
4492
  has_api_key: Boolean(provider.api_key)
3674
4493
  })),
3675
- modelMap: registry.modelMap
4494
+ modelMap: registry.modelMap,
4495
+ modelPools: registry.modelPools
3676
4496
  };
3677
4497
  }
3678
4498
  function collectModelReferences(config) {
@@ -3712,23 +4532,120 @@ function buildServiceInfo(rawConfig) {
3712
4532
  const remoteService = runtime.remote_service ?? {};
3713
4533
  const registration = normalized.Registration ?? {};
3714
4534
  const runtimeMode = runtime.mode ?? "local";
4535
+ const managedKeys = listManagedApiKeys(normalized);
4536
+ const authSummary = managedApiKeySummary(normalized);
4537
+ const host = rawConfig?.HOST ?? normalized.HOST;
4538
+ const port = rawConfig?.PORT ?? normalized.PORT;
4539
+ const listenerHost = String(host ?? "").trim() || "127.0.0.1";
4540
+ const publicHost = ["0.0.0.0", "::", "[::]"].includes(String(host ?? "").trim());
4541
+ const hasBootstrapAuth = Boolean(normalized.APIKEY);
4542
+ const hasManagedAuthRecords = authSummary.total > 0;
4543
+ const hasActiveManagedAuth = authSummary.active > 0;
4544
+ const authRequired = hasBootstrapAuth || hasManagedAuthRecords;
4545
+ const listenerBaseUrl = publicHost ? `http://<server-host>:${port}` : `http://${listenerHost}:${port}`;
4546
+ const localBaseUrl = `http://127.0.0.1:${port}`;
4547
+ const securityIssues = [];
4548
+ if (!authRequired && (publicHost || runtimeMode !== "local")) {
4549
+ securityIssues.push({
4550
+ code: "server_without_auth",
4551
+ severity: "critical",
4552
+ message: "Server/cloud or public listener is running without API key authentication.",
4553
+ action: "Set APIKEY or create an active managed admin/client key before exposing this service."
4554
+ });
4555
+ }
4556
+ if (authRequired && hasBootstrapAuth && authSummary.total === 0 && runtimeMode !== "local") {
4557
+ securityIssues.push({
4558
+ code: "bootstrap_only_auth",
4559
+ severity: "warning",
4560
+ message: "Only the bootstrap APIKEY is configured for a server/cloud role.",
4561
+ action: "Create managed client keys for remote users and keep APIKEY for administration."
4562
+ });
4563
+ }
4564
+ if (!hasBootstrapAuth && hasManagedAuthRecords && !hasActiveManagedAuth) {
4565
+ securityIssues.push({
4566
+ code: "managed_auth_without_active_key",
4567
+ severity: "warning",
4568
+ message: "Managed API key records exist, but none are active.",
4569
+ action: "Create an active managed admin/client key or configure APIKEY before relying on this service."
4570
+ });
4571
+ }
4572
+ const quotaSummary = authQuotaUsageStore.summary();
4573
+ const quotaKeys = managedKeys.map((key) => {
4574
+ const usage = authQuotaUsageStore.snapshotForKey(key.id, key.quota);
4575
+ const requestLimit = usage?.requestLimit;
4576
+ const tokenLimit = usage?.tokenLimit;
4577
+ const requestRatio = requestLimit ? usage.requestsUsed / requestLimit : 0;
4578
+ const tokenRatio = tokenLimit ? usage.tokensUsed / tokenLimit : 0;
4579
+ const exhausted = requestLimit !== void 0 && usage.requestsUsed >= requestLimit || tokenLimit !== void 0 && usage.tokensUsed >= tokenLimit;
4580
+ const nearLimit = !exhausted && (requestRatio >= 0.8 || tokenRatio >= 0.8);
4581
+ return {
4582
+ id: key.id,
4583
+ label: key.label,
4584
+ scopes: key.scopes,
4585
+ active: key.active,
4586
+ quota: key.quota,
4587
+ usage,
4588
+ status: !usage ? "unlimited" : !key.active ? "inactive" : exhausted ? "exhausted" : nearLimit ? "watch" : "ok"
4589
+ };
4590
+ });
3715
4591
  return {
3716
4592
  service: SERVICE_NAME,
3717
4593
  ready: true,
3718
- host: rawConfig?.HOST ?? normalized.HOST,
3719
- port: rawConfig?.PORT ?? normalized.PORT,
4594
+ host,
4595
+ port,
3720
4596
  runtimeMode,
3721
4597
  serviceRole: runtimeMode === "local" ? "local_agent" : "router_service",
4598
+ listener: {
4599
+ host: listenerHost,
4600
+ port,
4601
+ public: publicHost,
4602
+ localUrl: localBaseUrl,
4603
+ advertisedUrl: listenerBaseUrl
4604
+ },
3722
4605
  remoteEnabled: Boolean(remoteService.enabled),
3723
4606
  remoteService: {
3724
4607
  enabled: Boolean(remoteService.enabled),
3725
4608
  baseUrl: remoteService.base_url || "",
3726
4609
  authTokenConfigured: Boolean(remoteService.auth_token)
3727
4610
  },
4611
+ clientConnection: runtimeMode === "local" && remoteService.enabled ? {
4612
+ role: "remote_client",
4613
+ baseUrl: remoteService.base_url || "",
4614
+ authTokenConfigured: Boolean(remoteService.auth_token),
4615
+ recommendedScopes: ["client", "read-only"],
4616
+ guidance: "Use Runtime.remote_service.base_url and a managed client + read-only key from the server maintainer."
4617
+ } : runtimeMode === "local" ? {
4618
+ role: "local_user",
4619
+ baseUrl: localBaseUrl,
4620
+ authTokenConfigured: authRequired,
4621
+ recommendedScopes: [],
4622
+ guidance: "Local Claude Code can use the local router URL; authentication is optional unless configured."
4623
+ } : {
4624
+ role: "remote_user",
4625
+ baseUrl: listenerBaseUrl,
4626
+ authTokenConfigured: authRequired,
4627
+ recommendedScopes: ["client", "read-only"],
4628
+ guidance: "Remote clients should set ANTHROPIC_BASE_URL to this service and use a managed client + read-only key."
4629
+ },
3728
4630
  registration: {
3729
4631
  enabled: Boolean(registration.enabled),
3730
4632
  models: Array.isArray(registration.models) ? registration.models.length : 0,
3731
4633
  upstreamServices: Array.isArray(registration.upstream_services) ? registration.upstream_services.length : 0
4634
+ },
4635
+ auth: {
4636
+ required: authRequired,
4637
+ bootstrapConfigured: Boolean(normalized.APIKEY),
4638
+ managedKeys: authSummary,
4639
+ audit: authAuditStore.summary(),
4640
+ quota: {
4641
+ ...quotaSummary,
4642
+ keys: quotaKeys
4643
+ }
4644
+ },
4645
+ security: {
4646
+ status: securityIssues.some((issue) => issue.severity === "critical") ? "critical" : securityIssues.length > 0 ? "warning" : "ok",
4647
+ publicHost,
4648
+ issues: securityIssues
3732
4649
  }
3733
4650
  };
3734
4651
  }
@@ -3765,9 +4682,13 @@ function summarizeCompiledModels(normalized) {
3765
4682
  const compiled = toCompiledRegistryView(normalized);
3766
4683
  const capabilityWarnings = collectCapabilityWarnings(normalized);
3767
4684
  const modelEntries = Object.values(compiled.modelMap ?? {});
4685
+ const modelPoolEntries = Object.values(compiled.modelPools ?? {});
4686
+ const modelPoolEndpoints = modelPoolEntries.flatMap((pool) => pool.endpoints ?? []);
3768
4687
  return {
3769
4688
  providerCount: compiled.providers.length,
3770
4689
  modelCount: modelEntries.length,
4690
+ modelPoolCount: modelPoolEntries.length,
4691
+ modelPoolEndpointCount: modelPoolEndpoints.length,
3771
4692
  capabilities: {
3772
4693
  reasoning: modelEntries.filter((item) => item.capabilities?.thinking?.supported !== false).length,
3773
4694
  tools: modelEntries.filter((item) => item.capabilities?.tools !== false).length,
@@ -3909,6 +4830,10 @@ function buildPersistedConfig(rawConfig, normalizedConfig) {
3909
4830
  if (registrationProjection && typeof registrationProjection === "object" && Object.keys(registrationProjection).length > 0) {
3910
4831
  persisted.Registration = registrationProjection;
3911
4832
  }
4833
+ const authProjection = projectConfiguredBranch(rawConfig?.Auth, normalizedConfig.Auth);
4834
+ if (authProjection && typeof authProjection === "object" && Object.keys(authProjection).length > 0) {
4835
+ persisted.Auth = authProjection;
4836
+ }
3912
4837
  if (rawConfig?.TriggerRouter) {
3913
4838
  smartRouterProjection = mergeSmartRouterProjection(smartRouterProjection, {
3914
4839
  ...rawConfig.TriggerRouter.enabled !== void 0 ? { enabled: runtimeSmartRouter.enabled } : {},
@@ -3948,6 +4873,51 @@ function buildPersistedConfig(rawConfig, normalizedConfig) {
3948
4873
  }
3949
4874
  return persisted;
3950
4875
  }
4876
+ function denyAuth(reply, statusCode, reason) {
4877
+ reply.code(statusCode);
4878
+ return {
4879
+ success: false,
4880
+ message: statusCode === 403 ? "Forbidden" : "Unauthorized",
4881
+ reason
4882
+ };
4883
+ }
4884
+ function requireAdminAuth(req, reply, authConfig) {
4885
+ const verification = verifyApiKey(
4886
+ authConfig ?? {},
4887
+ extractApiKeyFromHeaders(req?.headers ?? {}),
4888
+ "admin"
4889
+ );
4890
+ const auditBase = {
4891
+ required: "admin",
4892
+ method: req?.method,
4893
+ path: req?.url,
4894
+ requestId: req?.id
4895
+ };
4896
+ if (verification.ok) {
4897
+ authAuditStore.add({
4898
+ ...auditBase,
4899
+ outcome: "allowed",
4900
+ source: verification.source,
4901
+ keyId: verification.keyId,
4902
+ scopes: verification.scopes,
4903
+ statusCode: 200
4904
+ });
4905
+ return null;
4906
+ }
4907
+ authAuditStore.add({
4908
+ ...auditBase,
4909
+ outcome: "denied",
4910
+ source: verification.source,
4911
+ keyId: verification.keyId,
4912
+ reason: verification.reason ?? "invalid",
4913
+ statusCode: verification.reason === "insufficient_scope" ? 403 : 401
4914
+ });
4915
+ return denyAuth(
4916
+ reply,
4917
+ verification.reason === "insufficient_scope" ? 403 : 401,
4918
+ verification.reason ?? "invalid"
4919
+ );
4920
+ }
3951
4921
  function buildDraftConfigView(config) {
3952
4922
  const normalizedConfig = normalizeAndValidateConfig(config ?? {}).config;
3953
4923
  const runtimeSmartRouterConfig = deriveRuntimeSmartRouterConfig(normalizedConfig);
@@ -4052,6 +5022,7 @@ var init_server = __esm({
4052
5022
  init_schema();
4053
5023
  init_validation_contract();
4054
5024
  init_workbench();
5025
+ init_api_keys();
4055
5026
  createServer = (config) => {
4056
5027
  const server = new import_llms.default(config);
4057
5028
  const configuredThresholds = config.initialConfig?.Governance?.observability?.anomaly_thresholds ?? {};
@@ -4137,6 +5108,7 @@ var init_server = __esm({
4137
5108
  success: true,
4138
5109
  providers: previewCompiled.providers,
4139
5110
  modelMap: previewCompiled.modelMap,
5111
+ modelPools: previewCompiled.modelPools,
4140
5112
  normalizedConfig: buildDraftConfigView(result.config),
4141
5113
  diff: diffCompiledRegistry(currentCompiled, previewCompiled),
4142
5114
  referenceImpact: analyzeModelReferenceImpact(result.config, previewCompiled),
@@ -4156,11 +5128,149 @@ var init_server = __esm({
4156
5128
  };
4157
5129
  });
4158
5130
  server.app.get("/api/service-info", async () => {
4159
- return buildServiceInfo(config.initialConfig ?? {});
5131
+ let currentConfig;
5132
+ try {
5133
+ currentConfig = await readConfigFile();
5134
+ } catch {
5135
+ currentConfig = void 0;
5136
+ }
5137
+ const serviceInfoConfig = currentConfig && Object.keys(currentConfig).length > 0 ? { ...config.initialConfig ?? {}, ...currentConfig } : config.initialConfig ?? {};
5138
+ return buildServiceInfo(serviceInfoConfig);
4160
5139
  });
4161
5140
  server.app.get("/api/registration", async () => {
4162
5141
  return buildRegistrationInfo(config.initialConfig ?? {});
4163
5142
  });
5143
+ server.app.get("/api/auth/keys", async (req, reply) => {
5144
+ const currentConfig = await readConfigFile();
5145
+ const denied = requireAdminAuth(req, reply, currentConfig);
5146
+ if (denied) {
5147
+ return denied;
5148
+ }
5149
+ const normalized = normalizeAndValidateConfig(currentConfig ?? {}).config;
5150
+ return {
5151
+ keys: listManagedApiKeys(normalized),
5152
+ summary: managedApiKeySummary(normalized)
5153
+ };
5154
+ });
5155
+ server.app.get("/api/auth/audit", async (req, reply) => {
5156
+ const currentConfig = await readConfigFile();
5157
+ const denied = requireAdminAuth(req, reply, currentConfig);
5158
+ if (denied) {
5159
+ return denied;
5160
+ }
5161
+ const limit = Number(req.query?.limit ?? 50);
5162
+ return {
5163
+ events: authAuditStore.list(Number.isFinite(limit) ? limit : 50),
5164
+ summary: authAuditStore.summary()
5165
+ };
5166
+ });
5167
+ server.app.post("/api/auth/keys", async (req, reply) => {
5168
+ const currentConfig = await readConfigFile();
5169
+ const denied = requireAdminAuth(req, reply, currentConfig);
5170
+ if (denied) {
5171
+ return denied;
5172
+ }
5173
+ const scopeErrors = validateManagedApiKeyScopes(req.body?.scopes);
5174
+ const quotaErrors = validateManagedApiKeyQuota(req.body?.quota);
5175
+ const inputErrors = [...scopeErrors, ...quotaErrors];
5176
+ if (inputErrors.length > 0) {
5177
+ reply.code(400);
5178
+ return {
5179
+ success: false,
5180
+ message: "Invalid managed API key input",
5181
+ errors: inputErrors
5182
+ };
5183
+ }
5184
+ if (req.body?.expiresAt !== void 0 && Number.isNaN(Date.parse(String(req.body.expiresAt)))) {
5185
+ reply.code(400);
5186
+ return {
5187
+ success: false,
5188
+ message: "expiresAt must be an ISO date string when provided"
5189
+ };
5190
+ }
5191
+ const created = createManagedApiKey({
5192
+ label: req.body?.label,
5193
+ scopes: req.body?.scopes,
5194
+ expiresAt: req.body?.expiresAt,
5195
+ quota: req.body?.quota
5196
+ });
5197
+ const nextConfig = {
5198
+ ...currentConfig ?? {},
5199
+ Auth: {
5200
+ ...currentConfig?.Auth ?? {},
5201
+ managed_keys: [
5202
+ ...currentConfig?.Auth?.managed_keys ?? [],
5203
+ created.record
5204
+ ]
5205
+ }
5206
+ };
5207
+ const result = normalizeAndValidateConfig(nextConfig);
5208
+ if (result.errors.length > 0) {
5209
+ reply.code(400);
5210
+ return {
5211
+ success: false,
5212
+ message: "Invalid auth key configuration",
5213
+ errors: result.errors
5214
+ };
5215
+ }
5216
+ const backupPath = await backupConfigFile();
5217
+ if (backupPath) {
5218
+ log(`Backed up existing configuration file to ${backupPath}`);
5219
+ }
5220
+ await writeConfigFile(buildPersistedConfig(nextConfig, result.config));
5221
+ return {
5222
+ success: true,
5223
+ key: sanitizeManagedApiKey(created.record),
5224
+ secret: created.secret,
5225
+ message: "Managed API key created. Store the secret now; it will not be shown again."
5226
+ };
5227
+ });
5228
+ server.app.post("/api/auth/keys/:id/revoke", async (req, reply) => {
5229
+ const currentConfig = await readConfigFile();
5230
+ const denied = requireAdminAuth(req, reply, currentConfig);
5231
+ if (denied) {
5232
+ return denied;
5233
+ }
5234
+ const keyId = String(req.params?.id ?? "").trim();
5235
+ const managedKeys = currentConfig?.Auth?.managed_keys ?? [];
5236
+ const keyIndex = managedKeys.findIndex((key) => key.id === keyId);
5237
+ if (keyIndex < 0) {
5238
+ reply.code(404);
5239
+ return {
5240
+ success: false,
5241
+ message: "Managed API key not found"
5242
+ };
5243
+ }
5244
+ const revokedAt = (/* @__PURE__ */ new Date()).toISOString();
5245
+ const nextKeys = managedKeys.map(
5246
+ (key, index) => index === keyIndex ? { ...key, revoked_at: key.revoked_at ?? revokedAt } : key
5247
+ );
5248
+ const nextConfig = {
5249
+ ...currentConfig ?? {},
5250
+ Auth: {
5251
+ ...currentConfig?.Auth ?? {},
5252
+ managed_keys: nextKeys
5253
+ }
5254
+ };
5255
+ const result = normalizeAndValidateConfig(nextConfig);
5256
+ if (result.errors.length > 0) {
5257
+ reply.code(400);
5258
+ return {
5259
+ success: false,
5260
+ message: "Invalid auth key configuration",
5261
+ errors: result.errors
5262
+ };
5263
+ }
5264
+ const backupPath = await backupConfigFile();
5265
+ if (backupPath) {
5266
+ log(`Backed up existing configuration file to ${backupPath}`);
5267
+ }
5268
+ await writeConfigFile(buildPersistedConfig(nextConfig, result.config));
5269
+ return {
5270
+ success: true,
5271
+ key: sanitizeManagedApiKey(nextKeys[keyIndex])
5272
+ };
5273
+ });
4164
5274
  server.app.get("/api/remote-status", async (req) => {
4165
5275
  const normalizedResult = normalizeAndValidateConfig(config.initialConfig ?? {});
4166
5276
  const normalized = normalizedResult.config;
@@ -4206,9 +5316,11 @@ var init_server = __esm({
4206
5316
  metrics: report.metrics,
4207
5317
  anomalies: report.anomalies,
4208
5318
  topRouteReasons: report.topRouteReasons,
4209
- topFinalModels: report.topFinalModels
5319
+ topFinalModels: report.topFinalModels,
5320
+ outcome: report.outcome
4210
5321
  }),
4211
5322
  metrics: report.metrics,
5323
+ outcome: report.outcome,
4212
5324
  anomalies: report.anomalies,
4213
5325
  topRouteReasons: report.topRouteReasons,
4214
5326
  topFinalModels: report.topFinalModels,
@@ -4398,8 +5510,8 @@ var init_server = __esm({
4398
5510
  reply.send({ success: true, message: "Service restart initiated" });
4399
5511
  setTimeout(() => {
4400
5512
  const { spawn: spawn3 } = require("child_process");
4401
- const { join: join8 } = require("path");
4402
- const cliPath = join8(__dirname, "cli.js");
5513
+ const { join: join9 } = require("path");
5514
+ const cliPath = join9(__dirname, "cli.js");
4403
5515
  const currentPort = config.initialConfig?.PORT;
4404
5516
  const restartArgs = [cliPath, "start", "--daemon"];
4405
5517
  if (currentPort) {
@@ -4430,6 +5542,7 @@ var init_router = __esm({
4430
5542
  init_cache();
4431
5543
  init_log();
4432
5544
  init_compile();
5545
+ init_governance();
4433
5546
  enc = (0, import_tiktoken.get_encoding)("cl100k_base");
4434
5547
  calculateTokenCount = (messages, system, tools) => {
4435
5548
  let tokenCount = 0;
@@ -4582,6 +5695,17 @@ var init_router = __esm({
4582
5695
  }
4583
5696
  req.body.model = model ?? req.body.model;
4584
5697
  applyModelThinking(req, config, req.body.model);
5698
+ const compiledModel = getCompiledModelRef(config, req.body.model);
5699
+ if (compiledModel?.source === "registration" && compiledModel.modelPool) {
5700
+ req.modelPoolSelection = compiledModel.modelPool;
5701
+ if (req.governanceTrace) {
5702
+ req.governanceTrace.finalModel = req.body.model;
5703
+ appendTraceReason(
5704
+ req.governanceTrace,
5705
+ `model_pool:${compiledModel.modelPool.modelId}:${compiledModel.modelPool.endpointId}`
5706
+ );
5707
+ }
5708
+ }
4585
5709
  req.tokenCount = tokenCount;
4586
5710
  } catch (error) {
4587
5711
  logError("Error in router middleware:", error.message);
@@ -4593,31 +5717,166 @@ var init_router = __esm({
4593
5717
  });
4594
5718
 
4595
5719
  // src/middleware/auth.ts
4596
- function apiKeyAuth(config) {
5720
+ function estimateRequestTokens(body) {
5721
+ if (body === void 0 || body === null) {
5722
+ return 0;
5723
+ }
5724
+ const text = typeof body === "string" ? body : JSON.stringify(body);
5725
+ return Math.ceil(text.length / 4);
5726
+ }
5727
+ function authRequirementForRequest(req) {
5728
+ const method = String(req.method ?? "").toUpperCase();
5729
+ const path = String(req.url ?? "").split("?")[0];
5730
+ const readOnlyPaths = /* @__PURE__ */ new Set([
5731
+ "/api/health",
5732
+ "/api/service-info",
5733
+ "/api/remote-status",
5734
+ "/api/registration",
5735
+ "/api/models/compiled",
5736
+ "/api/transformers",
5737
+ "/api/governance/health",
5738
+ "/api/governance/metrics",
5739
+ "/api/governance/metrics/export",
5740
+ "/api/governance/metrics/exports"
5741
+ ]);
5742
+ const modelCallPaths = /* @__PURE__ */ new Set([
5743
+ "/v1/messages",
5744
+ "/v1/chat/completions"
5745
+ ]);
5746
+ if (method === "GET" && (readOnlyPaths.has(path) || path === "/api/governance/traces" || path.startsWith("/api/governance/traces/") || path === "/api/governance/archives" || path.startsWith("/api/governance/archives/"))) {
5747
+ return "read-only";
5748
+ }
5749
+ if (modelCallPaths.has(path)) {
5750
+ return "client";
5751
+ }
5752
+ return path.startsWith("/api/") || path === "/ui" ? "admin" : "client";
5753
+ }
5754
+ function isQuotaMeteredRequest(req) {
5755
+ const method = String(req.method ?? "").toUpperCase();
5756
+ const path = String(req.url ?? "").split("?")[0];
5757
+ return method === "POST" && (path === "/v1/messages" || path === "/v1/chat/completions");
5758
+ }
5759
+ function apiKeyAuth(configInput, options = {}) {
4597
5760
  return (req, reply, done) => {
4598
- if (!config.APIKEY) {
5761
+ Promise.resolve(typeof configInput === "function" ? configInput() : configInput).then(async (config) => {
5762
+ authQuotaUsageStore.hydrate(config.Auth?.quota_usage);
5763
+ const required = authRequirementForRequest(req);
5764
+ const auditBase = {
5765
+ required,
5766
+ method: req.method,
5767
+ path: req.url,
5768
+ requestId: req.id
5769
+ };
5770
+ if (!config.APIKEY && !config.Auth?.managed_keys?.length) {
5771
+ authAuditStore.add({
5772
+ ...auditBase,
5773
+ outcome: "skipped",
5774
+ reason: "no_auth_config"
5775
+ });
5776
+ done();
5777
+ return;
5778
+ }
5779
+ const result = verifyApiKey(config, extractApiKeyFromHeaders(req.headers), required);
5780
+ if (!result.ok) {
5781
+ const statusCode = result.reason === "insufficient_scope" ? 403 : 401;
5782
+ authAuditStore.add({
5783
+ ...auditBase,
5784
+ outcome: "denied",
5785
+ source: result.source,
5786
+ keyId: result.keyId,
5787
+ reason: result.reason,
5788
+ statusCode
5789
+ });
5790
+ reply.code(statusCode).send({
5791
+ error: statusCode === 403 ? "Forbidden" : "Unauthorized",
5792
+ reason: result.reason
5793
+ });
5794
+ done(new Error(statusCode === 403 ? "Forbidden" : "Unauthorized"));
5795
+ return;
5796
+ }
5797
+ const quotaResult = isQuotaMeteredRequest(req) ? authQuotaUsageStore.consume(
5798
+ result.keyId,
5799
+ result.quota,
5800
+ estimateRequestTokens(req.body)
5801
+ ) : { ok: true };
5802
+ if (!quotaResult.ok) {
5803
+ const retryAfterSeconds = quotaResult.usage.windowResetAt ? Math.max(0, Math.ceil((Date.parse(quotaResult.usage.windowResetAt) - Date.now()) / 1e3)) : void 0;
5804
+ authAuditStore.add({
5805
+ ...auditBase,
5806
+ outcome: "denied",
5807
+ source: result.source,
5808
+ keyId: result.keyId,
5809
+ scopes: result.scopes,
5810
+ reason: quotaResult.reason,
5811
+ statusCode: 429,
5812
+ quota: quotaResult.usage
5813
+ });
5814
+ if (retryAfterSeconds !== void 0) {
5815
+ reply.header("Retry-After", String(retryAfterSeconds));
5816
+ }
5817
+ reply.code(429).send({
5818
+ error: "Too Many Requests",
5819
+ reason: quotaResult.reason,
5820
+ quota: quotaResult.usage
5821
+ });
5822
+ done(new Error("Too Many Requests"));
5823
+ return;
5824
+ }
5825
+ authAuditStore.add({
5826
+ ...auditBase,
5827
+ outcome: "allowed",
5828
+ source: result.source,
5829
+ keyId: result.keyId,
5830
+ scopes: result.scopes,
5831
+ statusCode: 200,
5832
+ quota: quotaResult.usage
5833
+ });
5834
+ if (quotaResult.usage && options.persistQuotaUsage) {
5835
+ Promise.resolve().then(() => options.persistQuotaUsage?.(authQuotaUsageStore.exportForConfig())).catch(() => void 0);
5836
+ }
4599
5837
  done();
4600
- return;
4601
- }
4602
- const authHeader = req.headers.authorization;
4603
- const xApiKey = req.headers["x-api-key"];
4604
- let providedKey;
4605
- if (authHeader?.startsWith("Bearer ")) {
4606
- providedKey = authHeader.slice(7);
4607
- } else if (xApiKey) {
4608
- providedKey = xApiKey;
4609
- }
4610
- if (!providedKey || providedKey !== config.APIKEY) {
4611
- reply.code(401).send({ error: "Unauthorized" });
4612
- done(new Error("Unauthorized"));
4613
- return;
4614
- }
4615
- done();
5838
+ }).catch((error) => {
5839
+ done(error instanceof Error ? error : new Error(String(error)));
5840
+ });
4616
5841
  };
4617
5842
  }
4618
5843
  var init_auth = __esm({
4619
5844
  "src/middleware/auth.ts"() {
4620
5845
  "use strict";
5846
+ init_api_keys();
5847
+ }
5848
+ });
5849
+
5850
+ // src/auth/quota-persistence.ts
5851
+ async function loadPersistedAuthQuotaUsage() {
5852
+ if (!(0, import_fs4.existsSync)(QUOTA_USAGE_FILE)) {
5853
+ return void 0;
5854
+ }
5855
+ const content = await (0, import_promises2.readFile)(QUOTA_USAGE_FILE, "utf-8");
5856
+ const parsed = JSON.parse(content);
5857
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
5858
+ }
5859
+ async function savePersistedAuthQuotaUsage(usage) {
5860
+ if (!(0, import_fs4.existsSync)(HOME_DIR)) {
5861
+ (0, import_fs4.mkdirSync)(HOME_DIR, { recursive: true });
5862
+ }
5863
+ const tempFile = `${QUOTA_USAGE_FILE}.tmp`;
5864
+ quotaUsageWriteQueue = quotaUsageWriteQueue.catch(() => void 0).then(async () => {
5865
+ await (0, import_promises2.writeFile)(tempFile, JSON.stringify(usage, null, 2), "utf-8");
5866
+ await (0, import_promises2.rename)(tempFile, QUOTA_USAGE_FILE);
5867
+ });
5868
+ await quotaUsageWriteQueue;
5869
+ }
5870
+ var import_fs4, import_promises2, import_path5, QUOTA_USAGE_FILE, quotaUsageWriteQueue;
5871
+ var init_quota_persistence = __esm({
5872
+ "src/auth/quota-persistence.ts"() {
5873
+ "use strict";
5874
+ import_fs4 = require("fs");
5875
+ import_promises2 = require("fs/promises");
5876
+ import_path5 = require("path");
5877
+ init_constants();
5878
+ QUOTA_USAGE_FILE = (0, import_path5.join)(HOME_DIR, "auth-quota-usage.json");
5879
+ quotaUsageWriteQueue = Promise.resolve();
4621
5880
  }
4622
5881
  });
4623
5882
 
@@ -4676,12 +5935,12 @@ function savePid(pid, port) {
4676
5935
  port: port ?? DEFAULT_CONFIG2.PORT,
4677
5936
  startTime: (/* @__PURE__ */ new Date()).toISOString()
4678
5937
  };
4679
- (0, import_fs4.writeFileSync)(PID_FILE, JSON.stringify(info, null, 2), "utf-8");
5938
+ (0, import_fs5.writeFileSync)(PID_FILE, JSON.stringify(info, null, 2), "utf-8");
4680
5939
  }
4681
5940
  function readServiceInfo() {
4682
- if (!(0, import_fs4.existsSync)(PID_FILE)) return null;
5941
+ if (!(0, import_fs5.existsSync)(PID_FILE)) return null;
4683
5942
  try {
4684
- const content = (0, import_fs4.readFileSync)(PID_FILE, "utf-8").trim();
5943
+ const content = (0, import_fs5.readFileSync)(PID_FILE, "utf-8").trim();
4685
5944
  if (/^\d+$/.test(content)) {
4686
5945
  return { pid: parseInt(content, 10), port: DEFAULT_CONFIG2.PORT, startTime: "" };
4687
5946
  }
@@ -4691,19 +5950,19 @@ function readServiceInfo() {
4691
5950
  }
4692
5951
  }
4693
5952
  function cleanupPidFile() {
4694
- if ((0, import_fs4.existsSync)(PID_FILE)) {
5953
+ if ((0, import_fs5.existsSync)(PID_FILE)) {
4695
5954
  try {
4696
- (0, import_fs4.unlinkSync)(PID_FILE);
5955
+ (0, import_fs5.unlinkSync)(PID_FILE);
4697
5956
  } catch (error) {
4698
5957
  logError("Failed to cleanup PID file:", error);
4699
5958
  }
4700
5959
  }
4701
5960
  }
4702
- var import_fs4, import_child_process;
5961
+ var import_fs5, import_child_process;
4703
5962
  var init_processCheck = __esm({
4704
5963
  "src/utils/processCheck.ts"() {
4705
5964
  "use strict";
4706
- import_fs4 = require("fs");
5965
+ import_fs5 = require("fs");
4707
5966
  import_child_process = require("child_process");
4708
5967
  init_constants();
4709
5968
  init_log();
@@ -6541,8 +7800,8 @@ function cloneRequestBody(value) {
6541
7800
  }
6542
7801
  async function initializeClaudeConfig() {
6543
7802
  const homeDir = (0, import_os2.homedir)();
6544
- const configPath = (0, import_path5.join)(homeDir, ".claude.json");
6545
- if (!(0, import_fs5.existsSync)(configPath)) {
7803
+ const configPath = (0, import_path6.join)(homeDir, ".claude.json");
7804
+ if (!(0, import_fs6.existsSync)(configPath)) {
6546
7805
  log(`Creating ${configPath} for Claude Code compatibility (onboarding bypass)`);
6547
7806
  const userID = Array.from(
6548
7807
  { length: 64 },
@@ -6556,7 +7815,7 @@ async function initializeClaudeConfig() {
6556
7815
  lastOnboardingVersion: "1.0.17",
6557
7816
  projects: {}
6558
7817
  };
6559
- await (0, import_promises2.writeFile)(configPath, JSON.stringify(configContent, null, 2));
7818
+ await (0, import_promises3.writeFile)(configPath, JSON.stringify(configContent, null, 2));
6560
7819
  }
6561
7820
  }
6562
7821
  function buildServerInitialConfig(config, registry, host, servicePort) {
@@ -6565,7 +7824,7 @@ function buildServerInitialConfig(config, registry, host, servicePort) {
6565
7824
  providers: registry.providers,
6566
7825
  HOST: host,
6567
7826
  PORT: servicePort,
6568
- LOG_FILE: (0, import_path5.join)(
7827
+ LOG_FILE: (0, import_path6.join)(
6569
7828
  (0, import_os2.homedir)(),
6570
7829
  ".claude-trigger-router",
6571
7830
  "claude-trigger-router.log"
@@ -6579,11 +7838,19 @@ async function run(options = {}) {
6579
7838
  }
6580
7839
  await initDir();
6581
7840
  const config = await initConfig();
7841
+ authQuotaUsageStore.hydrate(config.Auth?.quota_usage);
7842
+ try {
7843
+ authQuotaUsageStore.hydrate(await loadPersistedAuthQuotaUsage());
7844
+ } catch (error) {
7845
+ logWarn(`[AuthQuota] Failed to load persisted quota usage: ${error instanceof Error ? error.message : String(error)}`);
7846
+ }
6582
7847
  configureLogging(config);
6583
7848
  let HOST = config.HOST || "127.0.0.1";
6584
- if (config.HOST && !config.APIKEY) {
7849
+ const managedKeySummary = managedApiKeySummary(config);
7850
+ const hasPublicAuth = Boolean(config.APIKEY || managedKeySummary.active > 0);
7851
+ if (config.HOST && !hasPublicAuth) {
6585
7852
  HOST = "127.0.0.1";
6586
- logWarn("\u26A0\uFE0F API key is not set. HOST is forced to 127.0.0.1.");
7853
+ logWarn("\u26A0\uFE0F API key or active managed key is not set. HOST is forced to 127.0.0.1.");
6587
7854
  }
6588
7855
  const port = options.port ?? config.PORT ?? DEFAULT_CONFIG.PORT;
6589
7856
  savePid(process.pid, port);
@@ -6626,13 +7893,34 @@ async function run(options = {}) {
6626
7893
  initialConfig: buildServerInitialConfig(config, registry, HOST, servicePort),
6627
7894
  logger: loggerConfig
6628
7895
  });
7896
+ const authMiddleware = apiKeyAuth(async () => {
7897
+ try {
7898
+ const currentConfig = await readConfigFile();
7899
+ return {
7900
+ ...config,
7901
+ APIKEY: currentConfig.APIKEY,
7902
+ Auth: currentConfig.Auth
7903
+ };
7904
+ } catch (error) {
7905
+ logWarn(`[Auth] Failed to refresh auth config, using startup auth config: ${error instanceof Error ? error.message : String(error)}`);
7906
+ return config;
7907
+ }
7908
+ }, {
7909
+ persistQuotaUsage: async (usage) => {
7910
+ try {
7911
+ await savePersistedAuthQuotaUsage(usage);
7912
+ } catch (error) {
7913
+ logWarn(`[AuthQuota] Failed to persist quota usage: ${error instanceof Error ? error.message : String(error)}`);
7914
+ }
7915
+ }
7916
+ });
6629
7917
  server.addHook("preHandler", async (req, reply) => {
6630
7918
  return new Promise((resolve, reject) => {
6631
7919
  const done = (err) => {
6632
7920
  if (err) reject(err);
6633
7921
  else resolve();
6634
7922
  };
6635
- apiKeyAuth(config)(req, reply, done);
7923
+ authMiddleware(req, reply, done);
6636
7924
  });
6637
7925
  });
6638
7926
  triggerRouter.init(config);
@@ -6911,18 +8199,20 @@ async function run(options = {}) {
6911
8199
  });
6912
8200
  await server.start();
6913
8201
  }
6914
- var import_fs5, import_promises2, import_os2, import_path5, import_json5, import_node_events, import_rotating_file_stream, event;
8202
+ var import_fs6, import_promises3, import_os2, import_path6, import_json5, import_node_events, import_rotating_file_stream, event;
6915
8203
  var init_index = __esm({
6916
8204
  "src/index.ts"() {
6917
8205
  "use strict";
6918
- import_fs5 = require("fs");
6919
- import_promises2 = require("fs/promises");
8206
+ import_fs6 = require("fs");
8207
+ import_promises3 = require("fs/promises");
6920
8208
  import_os2 = require("os");
6921
- import_path5 = require("path");
8209
+ import_path6 = require("path");
6922
8210
  init_utils();
6923
8211
  init_server();
6924
8212
  init_router();
6925
8213
  init_auth();
8214
+ init_api_keys();
8215
+ init_quota_persistence();
6926
8216
  init_processCheck();
6927
8217
  init_constants();
6928
8218
  init_log();
@@ -7400,6 +8690,17 @@ function buildRemoteServiceConfig(input3) {
7400
8690
  Router: {}
7401
8691
  };
7402
8692
  }
8693
+ function buildServerDeploymentConfig(input3) {
8694
+ const template = buildUsableMinimalTemplateConfig();
8695
+ return {
8696
+ ...template,
8697
+ HOST: "0.0.0.0",
8698
+ APIKEY: input3.apiKey,
8699
+ Runtime: {
8700
+ mode: "server"
8701
+ }
8702
+ };
8703
+ }
7403
8704
  function buildUsableMinimalTemplateConfig() {
7404
8705
  const openRouterPreset = getProviderPreset("openrouter");
7405
8706
  const modelId = openRouterPreset?.suggested_id ?? "sonnet";
@@ -7590,6 +8891,16 @@ function getMigratedModelCount(draft) {
7590
8891
  }
7591
8892
  return 0;
7592
8893
  }
8894
+ function isRouterServiceDeploymentDraft(draft) {
8895
+ return draft?.Runtime?.mode === "server" || draft?.Runtime?.mode === "cloud";
8896
+ }
8897
+ function getRouterServiceDeploymentLabel(draft) {
8898
+ return draft?.Runtime?.mode === "cloud" ? "cloud" : "server";
8899
+ }
8900
+ function printRouterServiceDeploymentNextSteps(io, draft, message = "\u5DF2\u751F\u6210 {mode} \u90E8\u7F72\u914D\u7F6E\uFF1Bsetup \u4E0D\u4F1A\u81EA\u52A8\u542F\u52A8\u8FDC\u7A0B\u670D\u52A1\u3002") {
8901
+ io.info(message.replace("{mode}", getRouterServiceDeploymentLabel(draft)));
8902
+ io.info("\u4E0B\u4E00\u6B65\uFF1A\u7F16\u8F91 Models[].key / Models[].model\uFF0C\u8FD0\u884C ctr doctor\uFF0C\u7136\u540E\u8FD0\u884C ctr start --daemon\u3002");
8903
+ }
7593
8904
  async function runSetup(deps) {
7594
8905
  const detection = await deps.detectSetupEnvironment();
7595
8906
  const currentConfigAction = await deps.chooseCurrentConfigAction({
@@ -7613,6 +8924,7 @@ async function runSetup(deps) {
7613
8924
  return;
7614
8925
  }
7615
8926
  let configChanged = false;
8927
+ let finalDraft;
7616
8928
  if (branch.kind === "repair_current") {
7617
8929
  if (detection.currentConfig.kind !== "invalid" && detection.currentConfig.kind !== "valid") {
7618
8930
  throw new Error("repair_current requires current config");
@@ -7632,6 +8944,7 @@ async function runSetup(deps) {
7632
8944
  draft: baseDraft,
7633
8945
  fields: repairPlan.fields
7634
8946
  });
8947
+ finalDraft = completedDraft;
7635
8948
  const persistResult = await deps.persistConfig({
7636
8949
  config: completedDraft,
7637
8950
  currentConfigPath: detection.currentConfig.path,
@@ -7640,16 +8953,28 @@ async function runSetup(deps) {
7640
8953
  configChanged = persistResult.configChanged;
7641
8954
  }
7642
8955
  if (branch.kind === "reuse_current") {
7643
- await deps.ensureServiceReady({
8956
+ if (detection.currentConfig.kind === "valid" && isRouterServiceDeploymentDraft(detection.currentConfig.config)) {
8957
+ printRouterServiceDeploymentNextSteps(
8958
+ deps.io,
8959
+ detection.currentConfig.config,
8960
+ "\u5F53\u524D\u914D\u7F6E\u662F {mode} \u90E8\u7F72\u914D\u7F6E\uFF1Bsetup \u4E0D\u4F1A\u81EA\u52A8\u542F\u52A8\u8FDC\u7A0B\u670D\u52A1\u3002"
8961
+ );
8962
+ return;
8963
+ }
8964
+ const service = await deps.ensureServiceReady({
7644
8965
  configChanged: false,
7645
8966
  detectedService: detection.detectedService,
7646
8967
  reloadSupported: deps.reloadSupported
7647
8968
  });
7648
- await deps.enterClaudeCode();
8969
+ await deps.enterClaudeCode({
8970
+ config: detection.currentConfig.config,
8971
+ service
8972
+ });
7649
8973
  return;
7650
8974
  }
7651
8975
  if (branch.kind === "unparseable_current") {
7652
8976
  const draft = await deps.buildFreshConfig();
8977
+ finalDraft = draft;
7653
8978
  const persistResult = await deps.persistConfig({
7654
8979
  config: draft,
7655
8980
  currentConfigPath: detection.currentConfig.path,
@@ -7659,6 +8984,7 @@ async function runSetup(deps) {
7659
8984
  }
7660
8985
  if (branch.kind === "fresh_init") {
7661
8986
  const draft = await deps.buildFreshConfig();
8987
+ finalDraft = draft;
7662
8988
  const persistResult = await deps.persistConfig({
7663
8989
  config: draft,
7664
8990
  currentConfigPath: getTargetConfigPath(detection),
@@ -7681,27 +9007,38 @@ async function runSetup(deps) {
7681
9007
  if (migrated.skippedFields.length > 0) {
7682
9008
  deps.io.info(`\u4EE5\u4E0B\u65E7\u5B57\u6BB5\u672A\u81EA\u52A8\u8FC1\u79FB\uFF1A${migrated.skippedFields.join(", ")}`);
7683
9009
  }
7684
- let finalDraft = migrated.draft;
9010
+ let migratedFinalDraft = migrated.draft;
7685
9011
  if (migrated.needsCompletion) {
7686
- finalDraft = await deps.completeDraft({
9012
+ migratedFinalDraft = await deps.completeDraft({
7687
9013
  draft: migrated.draft,
7688
9014
  fields: migrated.missingFields
7689
9015
  });
7690
9016
  }
9017
+ finalDraft = migratedFinalDraft;
7691
9018
  const persistResult = await deps.persistConfig({
7692
- config: finalDraft,
9019
+ config: migratedFinalDraft,
7693
9020
  currentConfigPath: getTargetConfigPath(detection),
7694
9021
  hasExistingConfig: detection.currentConfig.kind !== "missing"
7695
9022
  });
7696
9023
  configChanged = persistResult.configChanged;
7697
9024
  }
7698
9025
  if (branch.kind === "fresh_init" || branch.kind === "repair_current" || branch.kind === "unparseable_current" || branch.kind === "migrate_legacy") {
7699
- await deps.ensureServiceReady({
9026
+ if (isRouterServiceDeploymentDraft(finalDraft)) {
9027
+ printRouterServiceDeploymentNextSteps(deps.io, finalDraft);
9028
+ return;
9029
+ }
9030
+ const service = await deps.ensureServiceReady({
7700
9031
  configChanged,
7701
9032
  detectedService: detection.detectedService,
7702
9033
  reloadSupported: deps.reloadSupported
7703
9034
  });
7704
- await deps.enterClaudeCode();
9035
+ if (!finalDraft) {
9036
+ throw new Error("setup finished without a final config draft");
9037
+ }
9038
+ await deps.enterClaudeCode({
9039
+ config: finalDraft,
9040
+ service
9041
+ });
7705
9042
  return;
7706
9043
  }
7707
9044
  }
@@ -7716,7 +9053,7 @@ var init_setup = __esm({
7716
9053
  // src/setup/index.ts
7717
9054
  function createConsoleIO() {
7718
9055
  if (process.env.CTR_SETUP_FORCE_SCRIPTED_INPUT === "1") {
7719
- const scriptedInput = (0, import_fs6.readFileSync)(0, "utf-8");
9056
+ const scriptedInput = (0, import_fs7.readFileSync)(0, "utf-8");
7720
9057
  const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter((item) => item.length > 0);
7721
9058
  let cursor = 0;
7722
9059
  const nextAnswer = async () => answers[cursor++] ?? "";
@@ -7805,7 +9142,7 @@ function createConsoleIO() {
7805
9142
  }
7806
9143
  };
7807
9144
  }
7808
- const rl = (0, import_promises3.createInterface)({ input: import_process.stdin, output: import_process.stdout });
9145
+ const rl = (0, import_promises4.createInterface)({ input: import_process.stdin, output: import_process.stdout });
7809
9146
  const ask = async (message) => {
7810
9147
  const answer = await rl.question(message);
7811
9148
  return answer.trim();
@@ -7846,7 +9183,7 @@ function createConsoleIO() {
7846
9183
  };
7847
9184
  }
7848
9185
  function readStructuredConfigFile(filePath) {
7849
- const content = (0, import_fs6.readFileSync)(filePath, "utf-8");
9186
+ const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
7850
9187
  if (filePath.endsWith(".json")) {
7851
9188
  return JSON.parse(content);
7852
9189
  }
@@ -7854,7 +9191,7 @@ function readStructuredConfigFile(filePath) {
7854
9191
  }
7855
9192
  function getCurrentRuntimeFields() {
7856
9193
  const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
7857
- const currentPath = candidates.find((filePath) => (0, import_fs6.existsSync)(filePath));
9194
+ const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
7858
9195
  if (!currentPath) {
7859
9196
  return {};
7860
9197
  }
@@ -7876,7 +9213,7 @@ function getCurrentRuntimeFields() {
7876
9213
  }
7877
9214
  function getConfiguredPortFromCurrentFiles() {
7878
9215
  const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
7879
- const currentPath = candidates.find((filePath) => (0, import_fs6.existsSync)(filePath));
9216
+ const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
7880
9217
  if (!currentPath) {
7881
9218
  return DEFAULT_CONFIG2.PORT;
7882
9219
  }
@@ -7910,7 +9247,7 @@ async function getAvailablePort() {
7910
9247
  }
7911
9248
  }
7912
9249
  function readLegacyConfigFile(filePath) {
7913
- const content = (0, import_fs6.readFileSync)(filePath, "utf-8");
9250
+ const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
7914
9251
  if (filePath.endsWith(".json")) {
7915
9252
  return import_json52.default.parse(content);
7916
9253
  }
@@ -7918,13 +9255,13 @@ function readLegacyConfigFile(filePath) {
7918
9255
  }
7919
9256
  async function readLegacyConfig(deps = {}) {
7920
9257
  const baseHomeDir = deps.homeDir || (0, import_os3.homedir)();
7921
- const exists = deps.exists || import_fs6.existsSync;
9258
+ const exists = deps.exists || import_fs7.existsSync;
7922
9259
  const readConfig = deps.readConfig || readLegacyConfigFile;
7923
9260
  const overridePath = process.env.CTR_SETUP_LEGACY_CONFIG_PATH;
7924
9261
  const candidatePaths = overridePath ? [overridePath] : [
7925
- (0, import_path6.join)(baseHomeDir, ".ccr", "config.yaml"),
7926
- (0, import_path6.join)(baseHomeDir, ".claude-code-router", "config.yaml"),
7927
- (0, import_path6.join)(baseHomeDir, ".claude-code-router", "config.json")
9262
+ (0, import_path7.join)(baseHomeDir, ".ccr", "config.yaml"),
9263
+ (0, import_path7.join)(baseHomeDir, ".claude-code-router", "config.yaml"),
9264
+ (0, import_path7.join)(baseHomeDir, ".claude-code-router", "config.json")
7928
9265
  ];
7929
9266
  const legacyPath = candidatePaths.find((filePath) => exists(filePath));
7930
9267
  if (!legacyPath) {
@@ -7946,7 +9283,7 @@ async function readLegacyConfig(deps = {}) {
7946
9283
  }
7947
9284
  async function readCurrentConfig() {
7948
9285
  const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
7949
- const currentPath = candidates.find((filePath) => (0, import_fs6.existsSync)(filePath));
9286
+ const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
7950
9287
  if (!currentPath) {
7951
9288
  return { kind: "missing" };
7952
9289
  }
@@ -8321,6 +9658,9 @@ function applyRoutingBootstrap(draft, choice, specializedModelId) {
8321
9658
  };
8322
9659
  return nextDraft;
8323
9660
  }
9661
+ function createSetupBootstrapApiKey() {
9662
+ return `ctr_bootstrap_${(0, import_crypto3.randomBytes)(24).toString("base64url")}`;
9663
+ }
8324
9664
  async function promptModelConnection(io, input3) {
8325
9665
  if (input3.intro) {
8326
9666
  io.info(input3.intro);
@@ -8354,18 +9694,30 @@ async function promptModelConnection(io, input3) {
8354
9694
  };
8355
9695
  }
8356
9696
  async function buildFreshConfig(io) {
8357
- const setupEntryChoice = await io.choose("\u5F53\u524D\u8981\u672C\u5730\u4F7F\u7528\uFF0C\u8FD8\u662F\u8FDE\u63A5\u8FDC\u7A0B\u670D\u52A1\uFF1F", [
9697
+ const setupEntryChoice = await io.choose("\u5F53\u524D\u8981\u672C\u5730\u4F7F\u7528\u3001\u8FDE\u63A5\u8FDC\u7A0B\u670D\u52A1\uFF0C\u8FD8\u662F\u90E8\u7F72\u4E3A\u8FDC\u7A0B\u670D\u52A1\u7AEF\uFF1F", [
8358
9698
  "\u672C\u5730\u4F7F\u7528\uFF08\u63A8\u8350\uFF09",
8359
- "\u8FDE\u63A5\u8FDC\u7A0B\u670D\u52A1"
9699
+ "\u8FDE\u63A5\u8FDC\u7A0B\u670D\u52A1",
9700
+ "\u90E8\u7F72\u4E3A\u8FDC\u7A0B\u670D\u52A1\u7AEF"
8360
9701
  ]);
8361
9702
  if (setupEntryChoice === "\u8FDE\u63A5\u8FDC\u7A0B\u670D\u52A1") {
8362
9703
  const baseUrl = await io.input("\u8FDC\u7A0B\u670D\u52A1 URL");
8363
9704
  const authToken = await io.input("\u8FDC\u7A0B\u670D\u52A1 Auth Token\uFF08\u53EF\u9009\uFF09", "${CTR_REMOTE_AUTH_TOKEN}");
8364
9705
  io.info("\u5DF2\u751F\u6210\u8FDC\u7A0B\u670D\u52A1\u8FDE\u63A5\u914D\u7F6E\uFF0C\u672C\u673A\u4E0D\u4F1A\u8981\u6C42\u4F60\u5148\u586B\u5199 provider/model\u3002");
9706
+ io.info(REMOTE_CLIENT_ROLE_GUIDE);
9707
+ io.info("\u5982\u679C\u4F60\u5176\u5B9E\u8981\u628A\u672C\u673A\u90E8\u7F72\u6210\u670D\u52A1\u7AEF\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C setup \u9009\u62E9\u201C\u90E8\u7F72\u4E3A\u8FDC\u7A0B\u670D\u52A1\u7AEF\u201D\uFF0C\u6216\u8FD0\u884C\uFF1Actr deploy init --target server");
9708
+ io.info(SERVER_MAINTAINER_ROLE_GUIDE);
8365
9709
  return buildRemoteServiceConfig({ baseUrl, authToken });
8366
9710
  }
9711
+ if (setupEntryChoice === "\u90E8\u7F72\u4E3A\u8FDC\u7A0B\u670D\u52A1\u7AEF") {
9712
+ io.info(SERVER_MAINTAINER_ROLE_GUIDE);
9713
+ io.info("setup \u5C06\u751F\u6210 server profile \u548C bootstrap admin APIKEY\uFF0C\u4F46\u4E0D\u4F1A\u81EA\u52A8\u542F\u52A8\u670D\u52A1\u3002");
9714
+ io.info("\u4FDD\u5B58\u540E\u8BF7\u5148\u7F16\u8F91 Models[].key / Models[].model\uFF0C\u518D\u8FD0\u884C\uFF1Actr doctor && ctr start --daemon");
9715
+ return buildServerDeploymentConfig({
9716
+ apiKey: createSetupBootstrapApiKey()
9717
+ });
9718
+ }
8367
9719
  const primaryModel = await promptModelConnection(io, {
8368
- intro: "\u6211\u4EEC\u5148\u521B\u5EFA\u4E00\u4EFD\u6700\u5C0F\u53EF\u7528\u914D\u7F6E\u3002",
9720
+ intro: `\u6211\u4EEC\u5148\u521B\u5EFA\u4E00\u4EFD\u6700\u5C0F\u53EF\u7528\u914D\u7F6E\u3002${LOCAL_USER_ROLE_GUIDE}`,
8369
9721
  modelIdPrompt: "\u9ED8\u8BA4\u6A21\u578B\u7684 model id\uFF08Router.default \u4F1A\u5F15\u7528\u5B83\uFF09",
8370
9722
  suggestedModelId: "sonnet"
8371
9723
  });
@@ -8466,6 +9818,31 @@ function printRoutingNextSteps(io) {
8466
9818
  io.info(" - SmartRouter candidates\uFF1A\u9002\u5408\u6A21\u7CCA\u4EFB\u52A1\uFF0C\u5728\u5019\u9009\u6A21\u578B\u4E4B\u95F4\u81EA\u52A8\u9009\u62E9\u66F4\u5408\u9002\u7684\u6A21\u578B");
8467
9819
  io.info(" - \u914D\u7F6E\u6A21\u677F\u53C2\u8003\uFF1Aconfig/trigger.advanced.yaml");
8468
9820
  }
9821
+ function formatSetupServiceReadyMessage(action) {
9822
+ if (action === "start") {
9823
+ return "\u672C\u5730\u4EE3\u7406\u5DF2\u542F\u52A8\u5E76\u901A\u8FC7\u5065\u5EB7\u68C0\u67E5\u3002";
9824
+ }
9825
+ if (action === "reload") {
9826
+ return "\u672C\u5730\u4EE3\u7406\u5DF2\u91CD\u8F7D\u914D\u7F6E\u5E76\u901A\u8FC7\u5065\u5EB7\u68C0\u67E5\u3002";
9827
+ }
9828
+ if (action === "restart") {
9829
+ return "\u672C\u5730\u4EE3\u7406\u5DF2\u91CD\u542F\u5E76\u901A\u8FC7\u5065\u5EB7\u68C0\u67E5\u3002";
9830
+ }
9831
+ return "\u672C\u5730\u4EE3\u7406\u5DF2\u5728\u8FD0\u884C\u5E76\u901A\u8FC7\u5065\u5EB7\u68C0\u67E5\u3002";
9832
+ }
9833
+ function printRemoteClientNextSteps(io, action) {
9834
+ io.info(`${formatSetupServiceReadyMessage(action)}\u8FDC\u7A0B\u670D\u52A1\u8FDE\u63A5\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u7528\u4E8E\u68C0\u67E5\u8FDC\u7AEF ready/status\u3002`);
9835
+ io.info("\u4E0B\u4E00\u6B65\uFF1A\u8FD0\u884C ctr status \u67E5\u770B\u672C\u5730\u4EE3\u7406\u4E0E\u8FDC\u7A0B\u670D\u52A1 ready \u72B6\u6001\u3002");
9836
+ io.info("\u65E5\u5E38\u76F4\u8FDE\u8FDC\u7A0B\u670D\u52A1\u65F6\uFF0C\u8BF7\u6309\u670D\u52A1\u7EF4\u62A4\u8005\u63D0\u4F9B\u7684 ANTHROPIC_BASE_URL \u548C ANTHROPIC_AUTH_TOKEN \u914D\u7F6E Claude Code\u3002");
9837
+ io.info("\u5982\u679C\u8FDC\u7AEF\u4E0D\u53EF\u7528\uFF0C\u8BF7\u786E\u8BA4 Runtime.remote_service.base_url \u548C managed client + read-only key\u3002");
9838
+ }
9839
+ function printLocalClientNextSteps(io, action) {
9840
+ io.info(`${formatSetupServiceReadyMessage(action)}\u65E5\u5E38\u4F7F\u7528\u8FD0\u884C\uFF1Actr code`);
9841
+ printRoutingNextSteps(io);
9842
+ }
9843
+ function isRemoteServiceClientConfig(config) {
9844
+ return config.Runtime?.mode === "local" && Boolean(config.Runtime.remote_service?.enabled);
9845
+ }
8469
9846
  async function runSetupCli(customDeps) {
8470
9847
  const defaults = createDefaultDeps(customDeps?.io);
8471
9848
  const deps = { ...defaults, ...customDeps };
@@ -8569,8 +9946,13 @@ async function runSetupCli(customDeps) {
8569
9946
  healthChecked: true
8570
9947
  };
8571
9948
  },
8572
- enterClaudeCode: async () => {
8573
- printRoutingNextSteps(deps.io);
9949
+ enterClaudeCode: async ({ config, service }) => {
9950
+ if (isRemoteServiceClientConfig(config)) {
9951
+ printRemoteClientNextSteps(deps.io, service.action);
9952
+ return;
9953
+ } else {
9954
+ printLocalClientNextSteps(deps.io, service.action);
9955
+ }
8574
9956
  if (!shouldAutoEnterClaudeCodeAfterSetup()) {
8575
9957
  deps.io.info("\u4E3A\u907F\u514D setup \u7ED3\u675F\u540E\u63A5\u7BA1\u5F53\u524D\u7EC8\u7AEF\uFF0C\u8BF7\u624B\u52A8\u8FD0\u884C\uFF1Actr code");
8576
9958
  deps.io.info("\u5982\u679C\u4F60\u660E\u786E\u9700\u8981 setup \u7ED3\u675F\u540E\u81EA\u52A8\u8FDB\u5165 Claude Code\uFF0C\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF CTR_SETUP_AUTO_ENTER_CODE=1");
@@ -8585,15 +9967,16 @@ async function runSetupCli(customDeps) {
8585
9967
  deps.io.close?.();
8586
9968
  }
8587
9969
  }
8588
- var import_fs6, import_net2, import_os3, import_path6, import_promises3, import_process, import_json52, import_js_yaml;
9970
+ var import_fs7, import_crypto3, import_net2, import_os3, import_path7, import_promises4, import_process, import_json52, import_js_yaml;
8589
9971
  var init_setup2 = __esm({
8590
9972
  "src/setup/index.ts"() {
8591
9973
  "use strict";
8592
- import_fs6 = require("fs");
9974
+ import_fs7 = require("fs");
9975
+ import_crypto3 = require("crypto");
8593
9976
  import_net2 = require("net");
8594
9977
  import_os3 = require("os");
8595
- import_path6 = require("path");
8596
- import_promises3 = require("readline/promises");
9978
+ import_path7 = require("path");
9979
+ import_promises4 = require("readline/promises");
8597
9980
  import_process = require("process");
8598
9981
  import_json52 = __toESM(require("json5"));
8599
9982
  import_js_yaml = __toESM(require("js-yaml"));
@@ -8611,6 +9994,7 @@ var init_setup2 = __esm({
8611
9994
  init_templates();
8612
9995
  init_persist();
8613
9996
  init_setup();
9997
+ init_runtime_role_guidance();
8614
9998
  }
8615
9999
  });
8616
10000
 
@@ -8683,7 +10067,7 @@ function hasArg(flag) {
8683
10067
  }
8684
10068
  function createConsoleIO2() {
8685
10069
  if (process.env.CTR_DOCTOR_FORCE_SCRIPTED_INPUT === "1") {
8686
- const scriptedInput = (0, import_fs7.readFileSync)(0, "utf-8");
10070
+ const scriptedInput = (0, import_fs8.readFileSync)(0, "utf-8");
8687
10071
  const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
8688
10072
  let cursor = 0;
8689
10073
  const nextAnswer = async () => answers[cursor++] ?? "";
@@ -8726,8 +10110,17 @@ function createConsoleIO2() {
8726
10110
  }
8727
10111
  };
8728
10112
  }
8729
- const rl = (0, import_promises4.createInterface)({ input: import_process2.stdin, output: import_process2.stdout });
8730
- const ask = async (message) => (await rl.question(message)).trim();
10113
+ const rl = (0, import_promises5.createInterface)({ input: import_process2.stdin, output: import_process2.stdout });
10114
+ const ask = async (message) => {
10115
+ try {
10116
+ return (await rl.question(message)).trim();
10117
+ } catch (error) {
10118
+ if (error?.code === "ERR_USE_AFTER_CLOSE") {
10119
+ return void 0;
10120
+ }
10121
+ throw error;
10122
+ }
10123
+ };
8731
10124
  return {
8732
10125
  info(message) {
8733
10126
  import_process2.stdout.write(`${message}
@@ -8744,6 +10137,9 @@ function createConsoleIO2() {
8744
10137
  `));
8745
10138
  while (true) {
8746
10139
  const answer = await ask("> ");
10140
+ if (answer === void 0) {
10141
+ return options[0];
10142
+ }
8747
10143
  const index = Number(answer);
8748
10144
  if (Number.isInteger(index) && index >= 1 && index <= options.length) {
8749
10145
  return options[index - 1];
@@ -8760,7 +10156,7 @@ function createConsoleIO2() {
8760
10156
  return answer || defaultValue || "";
8761
10157
  },
8762
10158
  async confirm(message, defaultValue = true) {
8763
- const answer = (await ask(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"}: `)).toLowerCase();
10159
+ const answer = (await ask(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"}: `))?.toLowerCase();
8764
10160
  if (!answer) {
8765
10161
  return defaultValue;
8766
10162
  }
@@ -8807,7 +10203,7 @@ function tryLoadStructuredConfig(filePath, content) {
8807
10203
  }
8808
10204
  }
8809
10205
  function loadCurrentConfig() {
8810
- const existingPath = getConfigCandidates().find((filePath) => (0, import_fs7.existsSync)(filePath));
10206
+ const existingPath = getConfigCandidates().find((filePath) => (0, import_fs8.existsSync)(filePath));
8811
10207
  const path = existingPath ?? CONFIG_FILE;
8812
10208
  if (!existingPath) {
8813
10209
  return {
@@ -8817,7 +10213,7 @@ function loadCurrentConfig() {
8817
10213
  messages: ["\u672A\u68C0\u6D4B\u5230\u5F53\u524D Claude Trigger Router \u914D\u7F6E\u3002"]
8818
10214
  };
8819
10215
  }
8820
- const content = (0, import_fs7.readFileSync)(existingPath, "utf-8");
10216
+ const content = (0, import_fs8.readFileSync)(existingPath, "utf-8");
8821
10217
  const loaded = tryLoadStructuredConfig(existingPath, content);
8822
10218
  return {
8823
10219
  path,
@@ -9094,7 +10490,8 @@ function explainProbeFailure(category) {
9094
10490
  }
9095
10491
  async function ensureServiceUsable(config, deps, configChanged) {
9096
10492
  const port = config.PORT ?? DEFAULT_CONFIG2.PORT;
9097
- const healthy = await deps.probeServiceHealth(port, 500);
10493
+ const serviceHealthOptions = config.APIKEY ? { apiKey: config.APIKEY } : {};
10494
+ const healthy = await deps.probeServiceHealth(port, 500, serviceHealthOptions);
9098
10495
  const occupied = await deps.isTcpPortOccupied(port, 500);
9099
10496
  const running = deps.isServiceRunning();
9100
10497
  if (healthy && !configChanged) {
@@ -9114,7 +10511,7 @@ async function ensureServiceUsable(config, deps, configChanged) {
9114
10511
  }
9115
10512
  }
9116
10513
  await deps.startDaemon();
9117
- const verified = await deps.waitForService(port, 5e3);
10514
+ const verified = await deps.waitForService(port, 5e3, serviceHealthOptions);
9118
10515
  if (!verified) {
9119
10516
  throw new Error(`doctor \u81EA\u52A8\u542F\u52A8\u540E\u5065\u5EB7\u68C0\u67E5\u4ECD\u672A\u901A\u8FC7\uFF08\u7AEF\u53E3 ${port}\uFF09\u3002`);
9120
10517
  }
@@ -9124,16 +10521,48 @@ async function reportRuntimeServiceContext(config, deps) {
9124
10521
  const runtimeMode = config.Runtime?.mode ?? "local";
9125
10522
  const serviceRole = runtimeMode === "local" ? "local_agent" : "router_service";
9126
10523
  const remoteService = config.Runtime?.remote_service;
10524
+ const managedKeys = managedApiKeySummary(config);
10525
+ const hasBootstrapAuth = Boolean(config.APIKEY);
10526
+ const hasManagedAuthRecords = managedKeys.total > 0;
10527
+ const authRequired = hasBootstrapAuth || hasManagedAuthRecords;
10528
+ const host = String(config.HOST ?? DEFAULT_CONFIG2.HOST ?? "127.0.0.1").trim() || "127.0.0.1";
10529
+ const port = config.PORT ?? DEFAULT_CONFIG2.PORT;
10530
+ const publicHost = ["0.0.0.0", "::", "[::]"].includes(host);
10531
+ const listenerUrl = publicHost ? `http://<server-host>:${port}` : `http://${host}:${port}`;
9127
10532
  deps.io.info(`\u670D\u52A1\u4E0A\u4E0B\u6587\uFF1A${runtimeMode}\uFF08${serviceRole}\uFF09`);
10533
+ deps.io.info(`\u76D1\u542C\u5730\u5740\uFF1A${host}:${port}${publicHost ? "\uFF08\u5BF9\u5916\u76D1\u542C\uFF09" : "\uFF08\u672C\u673A\u76D1\u542C\uFF09"}`);
10534
+ deps.io.info(`\u9274\u6743\u72B6\u6001\uFF1A${authRequired ? "enabled" : "disabled"}\uFF08bootstrap=${hasBootstrapAuth}, managed_active=${managedKeys.active}\uFF09`);
10535
+ deps.io.info("Scope \u6307\u5F15\uFF1Aadmin \u7528\u4E8E /ui\u3001\u914D\u7F6E\u4FDD\u5B58\u3001\u91CD\u542F\u3001auth \u7BA1\u7406\u548C\u6CBB\u7406\u5199\u64CD\u4F5C\uFF1Bclient \u53EA\u7528\u4E8E\u6A21\u578B\u8C03\u7528\uFF1Bread-only \u53EA\u7528\u4E8E health/status/compiled/governance \u89C2\u6D4B\u3002");
10536
+ deps.io.info("Key \u64CD\u4F5C\u6307\u5F15\uFF1A\u4F7F\u7528 admin key \u8C03\u7528 GET /api/auth/keys \u67E5\u770B\u5217\u8868\u3001POST /api/auth/keys \u751F\u6210 key\u3001POST /api/auth/keys/:id/revoke \u540A\u9500 key\uFF1B\u751F\u6210\u7684 secret \u53EA\u8FD4\u56DE\u4E00\u6B21\u3002");
10537
+ if (runtimeMode !== "local") {
10538
+ deps.io.info(`\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u63A5\u5165\uFF1AANTHROPIC_BASE_URL=${listenerUrl}\uFF0CANTHROPIC_AUTH_TOKEN \u4F7F\u7528 managed client + read-only key\u3002`);
10539
+ deps.io.info(`\u7EF4\u62A4\u5165\u53E3\uFF1Ahttp://127.0.0.1:${port}/ui\uFF1B\u516C\u7F51\u8BBF\u95EE\u8BF7\u653E\u5728 HTTPS \u53CD\u5411\u4EE3\u7406\u6216\u5185\u7F51\u4E4B\u540E\u3002`);
10540
+ }
10541
+ if (!authRequired && (runtimeMode !== "local" || publicHost)) {
10542
+ deps.io.error("\u5B89\u5168\u98CE\u9669\uFF1A\u5F53\u524D server/cloud \u6216\u516C\u7F51\u76D1\u542C\u672A\u914D\u7F6E API key\uFF1B\u66B4\u9732\u670D\u52A1\u524D\u8BF7\u8BBE\u7F6E APIKEY \u6216\u521B\u5EFA managed client/admin key\u3002");
10543
+ } else if (!hasBootstrapAuth && hasManagedAuthRecords && managedKeys.active === 0) {
10544
+ deps.io.error("\u5B89\u5168\u98CE\u9669\uFF1A\u5F53\u524D\u4EC5\u4FDD\u7559 managed key \u8BB0\u5F55\u4F46\u6CA1\u6709 active key\uFF1B\u670D\u52A1\u4F1A\u62D2\u7EDD\u8BF7\u6C42\uFF0C\u8BF7\u8BBE\u7F6E APIKEY \u6216\u521B\u5EFA active managed key\u3002");
10545
+ } else if (authRequired && hasBootstrapAuth && managedKeys.total === 0 && runtimeMode !== "local") {
10546
+ deps.io.info("\u5B89\u5168\u63D0\u793A\uFF1A\u5F53\u524D\u4EC5\u914D\u7F6E bootstrap APIKEY\uFF1B\u5EFA\u8BAE\u4E3A\u8FDC\u7A0B\u4F7F\u7528\u8005\u751F\u6210 managed client key\uFF0C\u5E76\u4FDD\u7559 APIKEY \u53EA\u505A\u7BA1\u7406\u7528\u9014\u3002");
10547
+ }
9128
10548
  if (!remoteService?.enabled) {
9129
10549
  deps.io.info("\u8FDC\u7A0B\u670D\u52A1\u68C0\u67E5\uFF1A\u672A\u542F\u7528\uFF0C\u672C\u673A\u4F7F\u7528\u672C\u5730\u914D\u7F6E\u548C\u672C\u5730\u670D\u52A1\u5065\u5EB7\u68C0\u67E5\u3002");
9130
10550
  return;
9131
10551
  }
9132
10552
  const baseUrl = remoteService.base_url?.trim().replace(/\/+$/, "") || "<missing>";
9133
10553
  deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u68C0\u67E5\uFF1A${baseUrl}`);
10554
+ deps.io.info("\u8FDC\u7A0B token \u6307\u5F15\uFF1ARuntime.remote_service.auth_token \u5982\u679C\u540C\u65F6\u8981\u63A2\u6D4B ready/status \u5E76\u8C03\u7528\u6A21\u578B\uFF0C\u8BF7\u901A\u8FC7 POST /api/auth/keys \u751F\u6210 client + read-only key\uFF1B\u907F\u514D\u590D\u7528 admin key\u3002");
9134
10555
  const remoteStatus = await probeRemoteServiceStatus(remoteService);
9135
10556
  const statusLabel = remoteStatus.ready ? "ready" : remoteStatus.reachable ? "reachable" : "unreachable";
9136
10557
  deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u72B6\u6001\uFF1A${statusLabel}\uFF08reachable=${remoteStatus.reachable}, ready=${remoteStatus.ready}\uFF09`);
10558
+ const remoteSecurity = remoteStatus.security && typeof remoteStatus.security === "object" ? remoteStatus.security : void 0;
10559
+ if (remoteSecurity?.status) {
10560
+ deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u5B89\u5168\u72B6\u6001\uFF1A${String(remoteSecurity.status)}`);
10561
+ const firstIssue = Array.isArray(remoteSecurity.issues) ? remoteSecurity.issues[0] : void 0;
10562
+ if (firstIssue?.message) {
10563
+ deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u5B89\u5168\u63D0\u793A\uFF1A${firstIssue.message}${firstIssue.action ? `\uFF1B${firstIssue.action}` : ""}`);
10564
+ }
10565
+ }
9137
10566
  if (remoteStatus.error) {
9138
10567
  deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u63D0\u793A\uFF1A${remoteStatus.error}`);
9139
10568
  }
@@ -9262,12 +10691,12 @@ async function runDoctorCli(customDeps) {
9262
10691
  deps.io.close?.();
9263
10692
  }
9264
10693
  }
9265
- var import_fs7, import_promises4, import_process2, import_child_process2, import_json53, import_js_yaml2;
10694
+ var import_fs8, import_promises5, import_process2, import_child_process2, import_json53, import_js_yaml2;
9266
10695
  var init_doctor = __esm({
9267
10696
  "src/doctor/index.ts"() {
9268
10697
  "use strict";
9269
- import_fs7 = require("fs");
9270
- import_promises4 = require("readline/promises");
10698
+ import_fs8 = require("fs");
10699
+ import_promises5 = require("readline/promises");
9271
10700
  import_process2 = require("process");
9272
10701
  import_child_process2 = require("child_process");
9273
10702
  import_json53 = __toESM(require("json5"));
@@ -9283,6 +10712,7 @@ var init_doctor = __esm({
9283
10712
  init_processCheck();
9284
10713
  init_service_health();
9285
10714
  init_templates();
10715
+ init_api_keys();
9286
10716
  }
9287
10717
  });
9288
10718
 
@@ -9295,7 +10725,7 @@ __export(cli_exports, {
9295
10725
  });
9296
10726
  module.exports = __toCommonJS(cli_exports);
9297
10727
  function getPackageInfo() {
9298
- const content = (0, import_fs8.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
10728
+ const content = (0, import_fs9.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
9299
10729
  const pkg = JSON.parse(content);
9300
10730
  return {
9301
10731
  name: pkg.name ?? "@peterwangze/claude-trigger-router",
@@ -9335,16 +10765,16 @@ function getPort() {
9335
10765
  }
9336
10766
  try {
9337
10767
  const yaml4 = require("js-yaml");
9338
- if ((0, import_fs8.existsSync)(CONFIG_FILE)) {
9339
- const content = (0, import_fs8.readFileSync)(CONFIG_FILE, "utf-8");
10768
+ if ((0, import_fs9.existsSync)(CONFIG_FILE)) {
10769
+ const content = (0, import_fs9.readFileSync)(CONFIG_FILE, "utf-8");
9340
10770
  const config = yaml4.load(content);
9341
10771
  if (config?.PORT) return config.PORT;
9342
- } else if ((0, import_fs8.existsSync)(CONFIG_FILE_YML)) {
9343
- const content = (0, import_fs8.readFileSync)(CONFIG_FILE_YML, "utf-8");
10772
+ } else if ((0, import_fs9.existsSync)(CONFIG_FILE_YML)) {
10773
+ const content = (0, import_fs9.readFileSync)(CONFIG_FILE_YML, "utf-8");
9344
10774
  const config = yaml4.load(content);
9345
10775
  if (config?.PORT) return config.PORT;
9346
- } else if ((0, import_fs8.existsSync)(CONFIG_FILE_JSON)) {
9347
- const content = (0, import_fs8.readFileSync)(CONFIG_FILE_JSON, "utf-8");
10776
+ } else if ((0, import_fs9.existsSync)(CONFIG_FILE_JSON)) {
10777
+ const content = (0, import_fs9.readFileSync)(CONFIG_FILE_JSON, "utf-8");
9348
10778
  const config = JSON.parse(content);
9349
10779
  if (config?.PORT) return config.PORT;
9350
10780
  }
@@ -9365,6 +10795,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
9365
10795
  setup \u68C0\u6D4B\u5E76\u590D\u7528\u5DF2\u6709\u914D\u7F6E\uFF0C\u5FC5\u8981\u65F6\u8FC1\u79FB\u65E7\u914D\u7F6E\u6216\u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
9366
10796
  doctor \u8BCA\u65AD\u5E76\u4FEE\u590D\u5F53\u524D\u914D\u7F6E\uFF0C\u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
9367
10797
  init \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
10798
+ deploy \u751F\u6210\u90E8\u7F72\u5165\u53E3\u914D\u7F6E\uFF08\u5F53\u524D\u652F\u6301 deploy init --target server\uFF09
9368
10799
  start \u542F\u52A8\u8DEF\u7531\u670D\u52A1\uFF08\u9ED8\u8BA4\u524D\u53F0\u8FD0\u884C\uFF09
9369
10800
  stop \u505C\u6B62\u540E\u53F0\u670D\u52A1
9370
10801
  restart \u91CD\u542F\u540E\u53F0\u670D\u52A1
@@ -9378,12 +10809,13 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
9378
10809
  \u9009\u9879\uFF1A
9379
10810
  --port, -p \u6307\u5B9A\u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4\uFF1A5678\uFF09
9380
10811
  --daemon, -d \u4EE5\u540E\u53F0\u65B9\u5F0F\u8FD0\u884C\uFF08\u914D\u5408 start/restart \u4F7F\u7528\uFF09
9381
- --force \u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u914D\u7F6E\uFF08\u914D\u5408 init \u4F7F\u7528\uFF09
10812
+ --force \u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u914D\u7F6E\uFF08\u914D\u5408 init/deploy init \u4F7F\u7528\uFF09
9382
10813
 
9383
10814
  \u4F7F\u7528\u793A\u4F8B\uFF1A
9384
10815
  ctr setup # \u590D\u7528\u5F53\u524D\u914D\u7F6E / \u8FC1\u79FB\u65E7\u914D\u7F6E / \u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
9385
10816
  ctr doctor # \u8BCA\u65AD\u914D\u7F6E / \u4FEE\u590D\u683C\u5F0F\u95EE\u9898 / \u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
9386
10817
  ctr init # \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
10818
+ ctr deploy init --target server # \u751F\u6210\u5B89\u5168\u9ED8\u8BA4\u7684 server \u90E8\u7F72\u914D\u7F6E
9387
10819
  ctr version # \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C
9388
10820
  ctr upgrade # \u67E5\u770B\u5347\u7EA7\u5230\u6700\u65B0\u7248\u672C\u7684\u547D\u4EE4
9389
10821
  ctr start # \u524D\u53F0\u542F\u52A8\uFF08\u63A8\u8350\u9996\u6B21\u4F7F\u7528\uFF0C\u4FBF\u4E8E\u67E5\u770B\u65E5\u5FD7\uFF09
@@ -9406,6 +10838,73 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
9406
10838
  \u66F4\u591A\u4FE1\u606F\uFF1Ahttps://github.com/peterwangze/claude-trigger-router
9407
10839
  `);
9408
10840
  }
10841
+ function readConfigForCliStatus() {
10842
+ const yaml4 = require("js-yaml");
10843
+ for (const configFile of [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON]) {
10844
+ if (!(0, import_fs9.existsSync)(configFile)) {
10845
+ continue;
10846
+ }
10847
+ const content = (0, import_fs9.readFileSync)(configFile, "utf-8");
10848
+ return configFile.endsWith(".json") ? JSON.parse(content) : yaml4.load(content);
10849
+ }
10850
+ return {};
10851
+ }
10852
+ function getLocalClaudeProxyToken(config) {
10853
+ const bootstrapKey = typeof config?.APIKEY === "string" ? config.APIKEY.trim() : "";
10854
+ return bootstrapKey || "ctr-local-proxy";
10855
+ }
10856
+ async function fetchLiveServiceInfo(port, apiKey) {
10857
+ try {
10858
+ const headers = apiKey?.trim() ? { Authorization: `Bearer ${apiKey.trim()}` } : void 0;
10859
+ const response = await fetch(`http://127.0.0.1:${port}${SERVICE_INFO_PATH}`, {
10860
+ headers,
10861
+ signal: AbortSignal.timeout(500)
10862
+ });
10863
+ if (!response.ok) {
10864
+ return null;
10865
+ }
10866
+ const payload = await response.json();
10867
+ if (!payload || typeof payload !== "object" || payload.service !== SERVICE_NAME) {
10868
+ return null;
10869
+ }
10870
+ return payload;
10871
+ } catch {
10872
+ return null;
10873
+ }
10874
+ }
10875
+ function printRuntimeStatus(config, port, liveInfo) {
10876
+ const normalized = normalizeAndValidateConfig(config ?? {}).config;
10877
+ const runtimeMode = liveInfo?.runtimeMode ?? normalized.Runtime?.mode ?? "local";
10878
+ const serviceRole = liveInfo?.serviceRole ?? (runtimeMode === "local" ? "local_agent" : "router_service");
10879
+ const listener = liveInfo?.listener && typeof liveInfo.listener === "object" ? liveInfo.listener : null;
10880
+ const host = String(listener?.host ?? normalized.HOST ?? DEFAULT_CONFIG2.HOST ?? "127.0.0.1").trim() || "127.0.0.1";
10881
+ const publicHost = typeof listener?.public === "boolean" ? listener.public : ["0.0.0.0", "::", "[::]"].includes(host);
10882
+ const listenerPort = Number(listener?.port ?? port) || port;
10883
+ const managedKeys = managedApiKeySummary(normalized);
10884
+ const liveAuth = liveInfo?.auth && typeof liveInfo.auth === "object" ? liveInfo.auth : null;
10885
+ const hasBootstrapAuth = Boolean(liveAuth?.bootstrapConfigured ?? normalized.APIKEY);
10886
+ const managedActive = Number(liveAuth?.managedKeys?.active ?? managedKeys.active) || 0;
10887
+ const authRequired = Boolean(liveAuth?.required ?? (hasBootstrapAuth || managedKeys.total > 0));
10888
+ const listenerUrl = String(listener?.advertisedUrl ?? (publicHost ? `http://<server-host>:${listenerPort}` : `http://${host}:${listenerPort}`));
10889
+ const remoteService = normalized.Runtime?.remote_service;
10890
+ const clientConnection = liveInfo?.clientConnection && typeof liveInfo.clientConnection === "object" ? liveInfo.clientConnection : null;
10891
+ console.log(` \u6A21\u5F0F\uFF1A${runtimeMode}\uFF08${serviceRole}\uFF09`);
10892
+ console.log(` \u76D1\u542C\uFF1A${host}:${listenerPort}${publicHost ? "\uFF08\u5BF9\u5916\u76D1\u542C\uFF09" : "\uFF08\u672C\u673A\u76D1\u542C\uFF09"}`);
10893
+ console.log(` \u9274\u6743\uFF1A${authRequired ? "enabled" : "disabled"}\uFF08bootstrap=${hasBootstrapAuth}, managed_active=${managedActive}\uFF09`);
10894
+ if (runtimeMode !== "local") {
10895
+ console.log(` \u8FDC\u7A0B\u5BA2\u6237\u7AEF\u63A5\u5165\uFF1AANTHROPIC_BASE_URL=${clientConnection?.baseUrl || listenerUrl}`);
10896
+ console.log(" \u63A8\u8350\u5BA2\u6237\u7AEF key\uFF1Amanaged client + read-only\uFF1B\u4E0D\u8981\u628A admin/bootstrap key \u53D1\u7ED9\u8FDC\u7A0B\u4F7F\u7528\u8005\u3002");
10897
+ console.log(` \u7EF4\u62A4\u5165\u53E3\uFF1Ahttp://127.0.0.1:${listenerPort}/ui\uFF08\u9700\u8981 admin key\uFF09`);
10898
+ return;
10899
+ }
10900
+ if (clientConnection?.role === "remote_client" || remoteService?.enabled) {
10901
+ const baseUrl = String(clientConnection?.baseUrl || remoteService?.base_url || "").trim().replace(/\/+$/, "") || "<missing>";
10902
+ console.log(` \u8FDC\u7A0B\u670D\u52A1\uFF1A${baseUrl}`);
10903
+ console.log(" \u63A8\u8350\u8FDC\u7A0B token\uFF1Amanaged client + read-only\uFF0C\u7528\u4E8E ready/status \u63A2\u6D4B\u548C\u6A21\u578B\u8C03\u7528\u3002");
10904
+ return;
10905
+ }
10906
+ console.log(` \u672C\u5730\u63A5\u5165\uFF1A${clientConnection?.baseUrl || `http://127.0.0.1:${listenerPort}`}`);
10907
+ }
9409
10908
  function getLatestPackageVersionViaNpm(packageName, timeoutMs = 5e3) {
9410
10909
  try {
9411
10910
  const result = (0, import_child_process3.spawnSync)("npm", ["view", packageName, "version", "--registry", PACKAGE_REGISTRY_URL], {
@@ -9492,16 +10991,19 @@ function isClaudeCommandAvailable(timeoutMs = 3e3) {
9492
10991
  return false;
9493
10992
  }
9494
10993
  }
10994
+ function createBootstrapApiKey() {
10995
+ return `ctr_bootstrap_${(0, import_crypto4.randomBytes)(24).toString("hex")}`;
10996
+ }
9495
10997
  function initConfig2() {
9496
10998
  const force = hasArg2("--force");
9497
- const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs8.existsSync);
10999
+ const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs9.existsSync);
9498
11000
  if (existingConfig && !force) {
9499
11001
  console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
9500
11002
  console.log(" \u5982\u9700\u8986\u76D6\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
9501
11003
  return;
9502
11004
  }
9503
- if (!(0, import_fs8.existsSync)(CONFIG_DIR)) {
9504
- (0, import_fs8.mkdirSync)(CONFIG_DIR, { recursive: true });
11005
+ if (!(0, import_fs9.existsSync)(CONFIG_DIR)) {
11006
+ (0, import_fs9.mkdirSync)(CONFIG_DIR, { recursive: true });
9505
11007
  }
9506
11008
  try {
9507
11009
  const yaml4 = require("js-yaml");
@@ -9511,7 +11013,7 @@ function initConfig2() {
9511
11013
  lineWidth: -1,
9512
11014
  noRefs: true
9513
11015
  });
9514
- (0, import_fs8.writeFileSync)(CONFIG_FILE, content, "utf-8");
11016
+ (0, import_fs9.writeFileSync)(CONFIG_FILE, content, "utf-8");
9515
11017
  const action = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
9516
11018
  console.log(`\u2705 \u914D\u7F6E\u6587\u4EF6${action}\uFF1A${CONFIG_FILE}`);
9517
11019
  console.log("");
@@ -9526,6 +11028,65 @@ function initConfig2() {
9526
11028
  process.exit(1);
9527
11029
  }
9528
11030
  }
11031
+ function printDeployHelp() {
11032
+ console.log("\u7528\u6CD5\uFF1Actr deploy init --target server [--force]");
11033
+ console.log("");
11034
+ console.log("\u5F53\u524D\u652F\u6301\uFF1A");
11035
+ console.log(" server \u751F\u6210\u5E26 HOST/APIKEY/Runtime.mode/Models/Router \u7684\u81EA\u6258\u7BA1\u670D\u52A1\u7AEF\u914D\u7F6E");
11036
+ console.log("");
11037
+ console.log("\u4E0B\u4E00\u6B65\uFF1A");
11038
+ console.log(" 1. \u7F16\u8F91 Models[].key \u548C Models[].model");
11039
+ console.log(" 2. \u8FD0\u884C ctr doctor \u68C0\u67E5\u914D\u7F6E\u548C\u9274\u6743\u72B6\u6001");
11040
+ console.log(" 3. \u8FD0\u884C ctr start --daemon \u542F\u52A8\u670D\u52A1");
11041
+ }
11042
+ function initDeployConfig() {
11043
+ const action = getArgs()[1];
11044
+ const target = getArgValue("--target") ?? "server";
11045
+ const force = hasArg2("--force");
11046
+ if (action !== "init") {
11047
+ printDeployHelp();
11048
+ return;
11049
+ }
11050
+ if (target !== "server") {
11051
+ console.error(`\u274C \u5F53\u524D\u4E0D\u652F\u6301\u7684\u90E8\u7F72\u76EE\u6807\uFF1A${target}`);
11052
+ printDeployHelp();
11053
+ process.exit(1);
11054
+ }
11055
+ const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs9.existsSync);
11056
+ if (existingConfig && !force) {
11057
+ console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
11058
+ console.log(" \u5982\u9700\u8986\u76D6\u90E8\u7F72\u6A21\u677F\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
11059
+ return;
11060
+ }
11061
+ if (!(0, import_fs9.existsSync)(CONFIG_DIR)) {
11062
+ (0, import_fs9.mkdirSync)(CONFIG_DIR, { recursive: true });
11063
+ }
11064
+ try {
11065
+ const yaml4 = require("js-yaml");
11066
+ const templateConfig = buildServerDeploymentConfig({
11067
+ apiKey: createBootstrapApiKey()
11068
+ });
11069
+ const content = yaml4.dump(templateConfig, {
11070
+ indent: 2,
11071
+ lineWidth: -1,
11072
+ noRefs: true
11073
+ });
11074
+ (0, import_fs9.writeFileSync)(CONFIG_FILE, content, "utf-8");
11075
+ const actionLabel = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
11076
+ console.log(`\u2705 Server \u90E8\u7F72\u914D\u7F6E${actionLabel}\uFF1A${CONFIG_FILE}`);
11077
+ console.log("");
11078
+ console.log("\u5DF2\u751F\u6210 bootstrap admin APIKEY\uFF1B\u8BF7\u53EA\u7528\u4E8E\u7EF4\u62A4\u8005\u7BA1\u7406\uFF0C\u4E0D\u8981\u53D1\u7ED9\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u3002");
11079
+ console.log("");
11080
+ console.log("\u4E0B\u4E00\u6B65\uFF1A");
11081
+ console.log(" 1. \u7F16\u8F91 Models[].key \u548C Models[].model\uFF0C\u586B\u5165\u670D\u52A1\u7AEF\u8981\u4EE3\u7406\u7684\u4E0A\u6E38\u6A21\u578B");
11082
+ console.log(" 2. \u8FD0\u884C\uFF1Actr doctor");
11083
+ console.log(" 3. \u8FD0\u884C\uFF1Actr start --daemon");
11084
+ console.log(" 4. \u7528 admin key \u8C03\u7528 POST /api/auth/keys \u751F\u6210 client + read-only \u8FDC\u7A0B\u5BA2\u6237\u7AEF key");
11085
+ } catch (error) {
11086
+ console.error("\u274C \u521B\u5EFA\u90E8\u7F72\u914D\u7F6E\u5931\u8D25:", error.message);
11087
+ process.exit(1);
11088
+ }
11089
+ }
9529
11090
  async function startForeground(port) {
9530
11091
  const targetPort = port ?? getPort();
9531
11092
  const healthy = await waitForService(targetPort, 500);
@@ -9613,16 +11174,28 @@ async function startDaemon(port) {
9613
11174
  console.log(` Run 'ctr stop' to stop it.`);
9614
11175
  }
9615
11176
  async function showStatus() {
11177
+ const config = readConfigForCliStatus();
11178
+ const configuredPort = getPort();
11179
+ const healthOptions = config?.APIKEY ? { apiKey: config.APIKEY } : {};
9616
11180
  const info = readServiceInfo();
9617
11181
  if (!info || !isServiceRunning()) {
9618
- const targetPort = getPort();
9619
- const healthy = await waitForService(targetPort, 500);
11182
+ const targetPort = configuredPort;
11183
+ const healthy = await waitForService(targetPort, 500, healthOptions);
9620
11184
  const occupied = await isTcpPortOccupied(targetPort, 500);
11185
+ if (healthy) {
11186
+ const liveInfo2 = await fetchLiveServiceInfo(targetPort, config?.APIKEY);
11187
+ console.log("\u2705 \u670D\u52A1\u8FD0\u884C\u4E2D");
11188
+ console.log(` \u7AEF\u53E3\uFF1A${targetPort}`);
11189
+ console.log(` \u63A5\u5165\u5730\u5740\uFF1Ahttp://127.0.0.1:${targetPort}`);
11190
+ printRuntimeStatus(config, targetPort, liveInfo2);
11191
+ return;
11192
+ }
9621
11193
  if (!healthy && occupied) {
9622
11194
  console.log(`\u26A0\uFE0F \u7AEF\u53E3 ${targetPort} \u5DF2\u88AB\u5176\u4ED6\u670D\u52A1\u5360\u7528\uFF0C\u5F53\u524D\u4E0D\u662F claude-trigger-router\u3002`);
9623
11195
  return;
9624
11196
  }
9625
11197
  console.log("\u23F9 \u670D\u52A1\u672A\u8FD0\u884C");
11198
+ printRuntimeStatus(config, targetPort);
9626
11199
  return;
9627
11200
  }
9628
11201
  const startTime = info.startTime ? new Date(info.startTime).toLocaleString() : "\u672A\u77E5";
@@ -9631,6 +11204,8 @@ async function showStatus() {
9631
11204
  console.log(` \u7AEF\u53E3\uFF1A${info.port}`);
9632
11205
  console.log(` \u542F\u52A8\u65F6\u95F4\uFF1A${startTime}`);
9633
11206
  console.log(` \u63A5\u5165\u5730\u5740\uFF1Ahttp://127.0.0.1:${info.port}`);
11207
+ const liveInfo = await fetchLiveServiceInfo(info.port, config?.APIKEY);
11208
+ printRuntimeStatus(config, info.port, liveInfo);
9634
11209
  }
9635
11210
  function stopService() {
9636
11211
  const info = readServiceInfo();
@@ -9653,10 +11228,12 @@ async function restartService() {
9653
11228
  }
9654
11229
  async function runClaudeCode() {
9655
11230
  const port = getPort();
11231
+ const config = readConfigForCliStatus();
11232
+ const proxyToken = getLocalClaudeProxyToken(config);
9656
11233
  await initializeClaudeConfig();
9657
11234
  const running = isServiceRunning();
9658
11235
  console.log(`\u{1F50D} Checking if service is available on port ${port}...`);
9659
- const reachable = await waitForService(port, 2e3);
11236
+ const reachable = await waitForService(port, 2e3, { apiKey: proxyToken });
9660
11237
  if (!reachable) {
9661
11238
  console.log(`\u26A0\uFE0F Trigger Router service is not running on port ${port}.`);
9662
11239
  console.log("");
@@ -9673,14 +11250,16 @@ async function runClaudeCode() {
9673
11250
  process.exit(1);
9674
11251
  }
9675
11252
  const isWindows = process.platform === "win32";
11253
+ const claudeEnv = {
11254
+ ...process.env,
11255
+ ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
11256
+ ANTHROPIC_AUTH_TOKEN: proxyToken
11257
+ };
11258
+ delete claudeEnv.ANTHROPIC_API_KEY;
9676
11259
  const claude = (0, import_child_process3.spawn)("claude", [], {
9677
11260
  stdio: "inherit",
9678
11261
  shell: isWindows,
9679
- env: {
9680
- ...process.env,
9681
- ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
9682
- ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "ctr-local-proxy"
9683
- }
11262
+ env: claudeEnv
9684
11263
  });
9685
11264
  claude.on("error", (error) => {
9686
11265
  console.error("\u274C \u542F\u52A8 Claude Code \u5931\u8D25:", error.message);
@@ -9723,6 +11302,9 @@ async function main() {
9723
11302
  case "init":
9724
11303
  initConfig2();
9725
11304
  break;
11305
+ case "deploy":
11306
+ initDeployConfig();
11307
+ break;
9726
11308
  case "start":
9727
11309
  if (isDaemonMode()) {
9728
11310
  await startDaemon(getPort());
@@ -9765,13 +11347,14 @@ async function main() {
9765
11347
  process.exit(command ? 1 : 0);
9766
11348
  }
9767
11349
  }
9768
- var import_child_process3, import_path7, import_openurl, import_fs8, PACKAGE_JSON_PATH, PACKAGE_PAGE_URL, PACKAGE_REGISTRY_LATEST_URL, PACKAGE_REGISTRY_URL;
11350
+ var import_child_process3, import_crypto4, import_path8, import_openurl, import_fs9, PACKAGE_JSON_PATH, PACKAGE_PAGE_URL, PACKAGE_REGISTRY_LATEST_URL, PACKAGE_REGISTRY_URL;
9769
11351
  var init_cli = __esm({
9770
11352
  "src/cli.ts"() {
9771
11353
  import_child_process3 = require("child_process");
9772
- import_path7 = require("path");
11354
+ import_crypto4 = require("crypto");
11355
+ import_path8 = require("path");
9773
11356
  import_openurl = __toESM(require("openurl"));
9774
- import_fs8 = require("fs");
11357
+ import_fs9 = require("fs");
9775
11358
  init_index();
9776
11359
  init_processCheck();
9777
11360
  init_constants();
@@ -9779,7 +11362,9 @@ var init_cli = __esm({
9779
11362
  init_setup2();
9780
11363
  init_templates();
9781
11364
  init_doctor();
9782
- PACKAGE_JSON_PATH = (0, import_path7.join)(__dirname, "..", "package.json");
11365
+ init_api_keys();
11366
+ init_config();
11367
+ PACKAGE_JSON_PATH = (0, import_path8.join)(__dirname, "..", "package.json");
9783
11368
  PACKAGE_PAGE_URL = "https://www.npmjs.com/package/@peterwangze/claude-trigger-router";
9784
11369
  PACKAGE_REGISTRY_LATEST_URL = "https://registry.npmjs.org/@peterwangze%2Fclaude-trigger-router/latest";
9785
11370
  PACKAGE_REGISTRY_URL = "https://registry.npmjs.org/";