@peterwangze/claude-trigger-router 1.1.0 → 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;
@@ -2960,6 +3719,74 @@ function buildAnomalies(metrics, buckets, thresholds) {
2960
3719
  }
2961
3720
  return anomalies;
2962
3721
  }
3722
+ function buildHealthActions(anomalies) {
3723
+ const actions = /* @__PURE__ */ new Set();
3724
+ for (const anomaly of anomalies) {
3725
+ if (anomaly.type.includes("cascade")) {
3726
+ actions.add("Review cascade triggers and recent failure evidence.");
3727
+ } else if (anomaly.type.includes("shadow")) {
3728
+ actions.add("Review shadow supervision findings and verifier sampling.");
3729
+ } else if (anomaly.type.includes("latency")) {
3730
+ actions.add("Inspect slow models or upstream latency before widening traffic.");
3731
+ }
3732
+ }
3733
+ if (!actions.size) {
3734
+ actions.add("Continue monitoring route and model distributions.");
3735
+ }
3736
+ return Array.from(actions);
3737
+ }
3738
+ function buildGovernanceHealthSummary(input3) {
3739
+ const metrics = input3.metrics;
3740
+ const anomalies = input3.anomalies ?? [];
3741
+ const criticalCount = anomalies.filter((item) => item.severity === "critical").length;
3742
+ const warnCount = anomalies.filter((item) => item.severity === "warn").length;
3743
+ const alertCount = anomalies.length;
3744
+ if (metrics.totalTraces === 0) {
3745
+ return {
3746
+ status: "idle",
3747
+ message: "No governance traces yet.",
3748
+ sampleSize: 0,
3749
+ alertCount: 0,
3750
+ warnCount: 0,
3751
+ criticalCount: 0,
3752
+ signals: {
3753
+ stickyHitRate: 0,
3754
+ cascadeTriggeredRate: 0,
3755
+ shadowCheckedRate: 0,
3756
+ alignmentUsedRate: 0,
3757
+ modelSwitchRate: 0,
3758
+ alignmentOnSwitchRate: 0,
3759
+ averageLatencyMs: 0,
3760
+ topRouteReason: input3.topRouteReasons?.[0],
3761
+ topFinalModel: input3.topFinalModels?.[0]
3762
+ },
3763
+ actions: ["Send requests through the router to collect governance traces."]
3764
+ };
3765
+ }
3766
+ const status = criticalCount > 0 ? "critical" : warnCount > 0 ? "watch" : "healthy";
3767
+ const alertVerb = alertCount === 1 ? "needs" : "need";
3768
+ const message = status === "healthy" ? `Healthy over ${metrics.totalTraces} traces.` : `${alertCount} governance alert${alertCount === 1 ? "" : "s"} ${alertVerb} attention (${criticalCount} critical / ${warnCount} warning${warnCount === 1 ? "" : "s"}).`;
3769
+ return {
3770
+ status,
3771
+ message,
3772
+ sampleSize: metrics.totalTraces,
3773
+ alertCount,
3774
+ warnCount,
3775
+ criticalCount,
3776
+ signals: {
3777
+ stickyHitRate: metrics.stickyHitRate,
3778
+ cascadeTriggeredRate: metrics.cascadeTriggeredRate,
3779
+ shadowCheckedRate: metrics.shadowCheckedRate,
3780
+ alignmentUsedRate: metrics.alignmentUsedRate,
3781
+ modelSwitchRate: input3.outcome?.modelSwitchRate ?? 0,
3782
+ alignmentOnSwitchRate: input3.outcome?.alignmentOnSwitchRate ?? 0,
3783
+ averageLatencyMs: metrics.averageLatencyMs,
3784
+ topRouteReason: input3.topRouteReasons?.[0],
3785
+ topFinalModel: input3.topFinalModels?.[0]
3786
+ },
3787
+ actions: buildHealthActions(anomalies)
3788
+ };
3789
+ }
2963
3790
  function summarizeGovernanceMetrics(traces) {
2964
3791
  const stickyHitCount = traces.filter((trace) => trace.stickyHit).length;
2965
3792
  const alignmentUsedCount = traces.filter((trace) => trace.alignmentUsed).length;
@@ -3049,19 +3876,32 @@ function getGovernanceMetricsReport(options = {}) {
3049
3876
  const limitedTraces = options.limit && options.limit > 0 ? windowed.traces.slice(0, options.limit) : windowed.traces;
3050
3877
  const bucketCount = options.bucketCount && options.bucketCount > 0 ? options.bucketCount : 6;
3051
3878
  const metrics = summarizeGovernanceMetrics(limitedTraces);
3879
+ const outcome = summarizeRoutingOutcomes(limitedTraces);
3052
3880
  const buckets = buildBuckets(limitedTraces, windowed.windowStart, windowed.windowEnd, bucketCount);
3053
3881
  const thresholds = normalizeAnomalyThresholds(options.anomalyThresholds);
3882
+ const topRouteReasons = buildTopEntries(metrics.routeReasonDistribution, limitedTraces.length);
3883
+ const topFinalModels = buildTopEntries(metrics.finalModelDistribution, limitedTraces.length);
3884
+ const topSemanticIntents = buildTopEntries(metrics.semanticIntentDistribution, limitedTraces.length);
3885
+ const anomalies = buildAnomalies(metrics, buckets, thresholds);
3054
3886
  return {
3055
3887
  windowMs: options.windowMs,
3056
3888
  bucketCount,
3057
3889
  windowStart: windowed.windowStart,
3058
3890
  windowEnd: windowed.windowEnd,
3059
3891
  metrics,
3892
+ outcome,
3060
3893
  buckets,
3061
- topRouteReasons: buildTopEntries(metrics.routeReasonDistribution, limitedTraces.length),
3062
- topFinalModels: buildTopEntries(metrics.finalModelDistribution, limitedTraces.length),
3063
- topSemanticIntents: buildTopEntries(metrics.semanticIntentDistribution, limitedTraces.length),
3064
- anomalies: buildAnomalies(metrics, buckets, thresholds)
3894
+ topRouteReasons,
3895
+ topFinalModels,
3896
+ topSemanticIntents,
3897
+ anomalies,
3898
+ health: buildGovernanceHealthSummary({
3899
+ metrics,
3900
+ anomalies,
3901
+ topRouteReasons,
3902
+ topFinalModels,
3903
+ outcome
3904
+ })
3065
3905
  };
3066
3906
  }
3067
3907
  function exportGovernanceMetricsReport(report, format = "json") {
@@ -3076,8 +3916,16 @@ function exportGovernanceMetricsReport(report, format = "json") {
3076
3916
  `summary,shadowCheckedRate,${report.metrics.shadowCheckedRate}`,
3077
3917
  `summary,alignmentUsedRate,${report.metrics.alignmentUsedRate}`,
3078
3918
  `summary,averageLatencyMs,${report.metrics.averageLatencyMs}`,
3079
- `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}`
3080
3924
  ];
3925
+ if (report.health) {
3926
+ lines.push(`summary,healthStatus,${report.health.status}`);
3927
+ lines.push(`summary,healthMessage,${report.health.message}`);
3928
+ }
3081
3929
  for (const anomaly of report.anomalies) {
3082
3930
  lines.push(`anomaly,${anomaly.type},${anomaly.severity}:${anomaly.value}`);
3083
3931
  }
@@ -3090,6 +3938,18 @@ function exportGovernanceMetricsReport(report, format = "json") {
3090
3938
  for (const item of report.topSemanticIntents) {
3091
3939
  lines.push(`topSemanticIntent,${item.key},${item.count}:${item.rate}`);
3092
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
+ }
3093
3953
  for (const bucket of report.buckets) {
3094
3954
  lines.push(
3095
3955
  `bucket,${bucket.label},${[
@@ -3542,6 +4402,17 @@ var init_provider_presets = __esm({
3542
4402
  }
3543
4403
  });
3544
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
+
3545
4416
  // src/ui/workbench.ts
3546
4417
  function toInlineScriptJson(value) {
3547
4418
  return JSON.stringify(value).replace(/</g, "\\u003c");
@@ -3554,19 +4425,58 @@ function renderWorkbenchHtml(rawInitialConfig, configuredThresholds = {}) {
3554
4425
  const modelsCount = Array.isArray(initialConfig.Models) ? initialConfig.Models.length : 0;
3555
4426
  const routerDefault = initialConfig.Router?.default ?? "-";
3556
4427
  const displayPort = initialConfig.PORT ?? "-";
4428
+ const runtimeMode = initialConfig.Runtime?.mode ?? "local";
4429
+ const serviceRole = runtimeMode === "local" ? "local_agent" : "router_service";
4430
+ const remoteService = initialConfig.Runtime?.remote_service ?? {};
4431
+ const remoteBaseUrl = typeof remoteService.base_url === "string" ? remoteService.base_url.trim().replace(/\/+$/, "") : "";
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`;
4437
+ const registration = initialConfig.Registration ?? {};
4438
+ const registrationModels = Array.isArray(registration.models) ? registration.models.length : 0;
4439
+ const registrationUpstreamServices = Array.isArray(registration.upstream_services) ? registration.upstream_services.length : 0;
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";
3557
4455
  const escapedDisplayPort = escapeHtml(displayPort);
3558
4456
  const escapedModelsCount = escapeHtml(modelsCount);
3559
4457
  const escapedRouterDefault = escapeHtml(routerDefault);
4458
+ const escapedRuntimeMode = escapeHtml(runtimeMode);
4459
+ const escapedServiceRole = escapeHtml(serviceRole);
4460
+ const escapedListenerSummary = escapeHtml(`${configuredHost}:${displayPort}${publicHost ? " (public)" : " (local)"}`);
4461
+ const escapedClientConnectionSummary = escapeHtml(clientConnectionSummary);
4462
+ const escapedRemoteSummary = escapeHtml(remoteSummary);
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);
3560
4469
  const escapedMinSampleSize = escapeHtml(configuredThresholds.min_sample_size ?? 3);
3561
4470
  const escapedCascadeWarnRate = escapeHtml(configuredThresholds.cascade_warn_rate ?? 0.4);
3562
4471
  const escapedShadowWarnRate = escapeHtml(configuredThresholds.shadow_warn_rate ?? 0.5);
3563
4472
  const escapedLatencyWarnMs = escapeHtml(configuredThresholds.latency_warn_ms ?? 1500);
3564
- return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Claude Trigger Router</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.hero{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(260px,.8fr);gap:1rem;align-items:stretch;margin-bottom:1rem}.hero h2{margin:.2rem 0 .5rem;font-size:1.55rem}.hero-copy{display:flex;flex-direction:column;justify-content:center}.status-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.status-tile{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem;min-width:0}.status-tile strong{display:block;margin-top:.2rem;word-break:break-word}@media (max-width:760px){.hero{grid-template-columns:1fr}.status-grid{grid-template-columns:1fr}}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:1rem;padding:.45rem 0;border-bottom:1px dashed #e5e7eb}.mini-list li:last-child{border-bottom:none}.action-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center;margin-top:.75rem}.management-table{width:100%;margin-top:.75rem}.management-table th,.management-table td{padding:.5rem;border-bottom:1px solid #e5e7eb;font-size:.92rem;vertical-align:top}.alert-list{display:grid;gap:.75rem;margin-top:1rem}.alert{border-radius:12px;padding:.85rem 1rem;border:1px solid}.alert.warn{background:#fff7ed;border-color:#fdba74;color:#9a3412}.alert.critical{background:#fef2f2;border-color:#fca5a5;color:#991b1b}.alert.info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.diff-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-top:.75rem}.diff-chip{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.75rem}.diff-chip strong{display:block;font-size:1rem;margin-top:.2rem}.models-form-grid{display:grid;gap:.75rem;margin-top:.75rem}.model-card{border:1px solid #e5e7eb;border-radius:12px;padding:1rem;background:#fcfcfd}.model-card-header{display:flex;justify-content:space-between;gap:1rem;align-items:center;margin-bottom:.75rem}.model-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.model-card-grid textarea{min-height:84px;resize:vertical}.list-editor{display:grid;gap:.75rem;margin-top:.75rem}.list-item{border:1px solid #e5e7eb;border-radius:12px;padding:.85rem;background:#fcfcfd}.list-item-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.jump-highlight{outline:3px solid #f59e0b;box-shadow:0 0 0 6px rgba(245,158,11,.15);transition:box-shadow .25s ease,outline-color .25s ease}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.control-grid label{display:block;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}.trend-table{width:100%;margin-top:.75rem}.trend-table th,.trend-table td{padding:.45rem;border-bottom:1px solid #e5e7eb;font-size:.92rem}.row{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}input,select,button{font:inherit;padding:.55rem .75rem;border-radius:8px;border:1px solid #d1d5db}button{background:#111827;color:#fff;border-color:#111827;cursor:pointer}table{width:100%;border-collapse:collapse;margin-top:1rem}th,td{text-align:left;padding:.65rem .5rem;border-bottom:1px solid #e5e7eb;vertical-align:top}code,pre{font-family:ui-monospace,SFMono-Regular,monospace}pre{white-space:pre-wrap;background:#0f172a;color:#e2e8f0;padding:1rem;border-radius:12px;overflow:auto}.pill{display:inline-block;padding:.2rem .5rem;border-radius:999px;background:#eef2ff;color:#3730a3;font-size:.8rem}.surface-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin:1rem 0}.surface-tab{background:#fff;color:#1f2328;border-color:#d1d5db}.surface-tab.active{background:#111827;color:#fff;border-color:#111827}.surface-panel[hidden]{display:none}.surface-heading{display:flex;gap:1rem;flex-wrap:wrap;align-items:center;margin-bottom:.75rem}</style></head><body><div class="hero"><div class="panel hero-copy"><h2>\u914D\u7F6E\u4E0E\u72B6\u6001\u5DE5\u4F5C\u53F0</h2><p class="muted">\u67E5\u770B\u5F53\u524D\u8DEF\u7531\u670D\u52A1\u3001\u6A21\u578B\u914D\u7F6E\u548C\u9ED8\u8BA4\u53BB\u5411\uFF1B\u9700\u8981\u6392\u67E5\u65F6\uFF0C\u4E0B\u65B9\u7EF4\u62A4\u8005\u533A\u57DF\u53EF\u7EE7\u7EED\u67E5\u770B Governance Trace\u3001metrics \u548C\u5F52\u6863\u3002</p><div class="action-row"><button id="loadConfigDraftHeroBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="previewConfigDraftHeroBtn" type="button">\u9884\u89C8 compiled models</button><button id="refreshStatusHeroBtn" type="button">\u5237\u65B0\u72B6\u6001</button></div></div><div class="panel"><div class="status-grid"><div class="status-tile"><span class="muted">Service</span><strong id="serviceReadyStatus">ready</strong></div><div class="status-tile"><span class="muted">Port</span><strong id="servicePortStatus">${escapedDisplayPort}</strong></div><div class="status-tile"><span class="muted">Models</span><strong id="modelCountStatus">${escapedModelsCount}</strong></div><div class="status-tile"><span class="muted">Router.default</span><strong id="routerDefaultStatus">${escapedRouterDefault}</strong></div></div></div></div><div class="surface-tabs" role="tablist" aria-label="\u5DE5\u4F5C\u53F0\u5207\u6362"><button id="userSurfaceTab" class="surface-tab active" type="button" role="tab" aria-selected="true" data-surface-target="user">\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</button><button id="maintainerSurfaceTab" class="surface-tab" type="button" role="tab" aria-selected="false" data-surface-target="maintainer">\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</button></div><section id="userSurface" class="surface-panel" data-surface="user"><div class="panel"><div class="surface-heading"><strong>\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u914D\u7F6E\u3001\u6A21\u578B\u3001\u8DEF\u7531\u3001\u670D\u52A1\u72B6\u6001\u4E0E\u4E0B\u4E00\u6B65\u4FDD\u5B58\u52A8\u4F5C\u3002</span></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Routing rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u56F4\u7ED5 SmartRouter \u7EDF\u4E00\u8DEF\u7531\u5F15\u64CE\u7F16\u8F91\u89C4\u5219\u3001\u5019\u9009\u4E0E\u6CBB\u7406\u589E\u5F3A\u517C\u5BB9\u914D\u7F6E</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Routing rules</strong><span class="muted">\u663E\u5F0F\u89C4\u5219\u3001\u8BED\u4E49\u63D0\u793A\u4E0E\u517C\u5BB9\u8F93\u5165</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5F71\u5B50\u6821\u9A8C\u3001\u7EA7\u8054\u4E0E\u89C2\u6D4B\u76F8\u5173\u914D\u7F6E</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div id="modelsFormGrid" class="models-form-grid"><div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div></div><textarea id="configDraftEditor" style="width:100%;min-height:240px;margin-top:.75rem;padding:.75rem;border-radius:12px;border:1px solid #d1d5db;font:12px/1.5 ui-monospace,SFMono-Regular,monospace" spellcheck="false" placeholder='{"Models":[{"id":"sonnet","api":"https://...","key":"sk-...","interface":"openai","model":"anthropic/claude-sonnet-4"}]}'></textarea><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / SmartRouter / Governance\uFF08shadow/cascade\uFF09\u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Compatibility profile</th><th>Dispatch format</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="8" class="muted">Loading model map...</td></tr></tbody></table></div></div></div></div></section><section id="maintainerSurface" class="surface-panel" data-surface="maintainer" hidden><div class="panel"><div class="surface-heading"><strong>\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u8FD0\u884C\u89C2\u6D4B\u3001Governance Trace\u3001metrics\u3001\u5F52\u6863\u4E0E\u7EF4\u62A4\u64CD\u4F5C\u3002</span></div><div class="row"><strong>\u7EF4\u62A4\u8005\u89C2\u6D4B</strong><span class="muted">\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4 Governance Trace\uFF0C\u5E76\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u6307\u6807\u3002</span></div><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Recent traces</span><strong>-</strong></div><div class="stat"><span class="muted">Sticky hit rate</span><strong>-</strong></div><div class="stat"><span class="muted">Cascade rate</span><strong>-</strong></div><div class="stat"><span class="muted">Shadow rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment rate</span><strong>-</strong></div><div class="stat"><span class="muted">Avg latency</span><strong>-</strong></div></div><div class="subpanel"><div class="row"><strong>Anomaly alerts</strong><span class="muted">\u68C0\u6D4B\u8FD1\u671F\u6CBB\u7406\u5F02\u5E38\u4E0E\u7A81\u589E</span></div><div id="anomalyList" class="alert-list"><div class="alert info"><strong>No alerts yet</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u6307\u6807\u52A0\u8F7D</div></div></div></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${escapedMinSampleSize}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${escapedCascadeWarnRate}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${escapedShadowWarnRate}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${escapedLatencyWarnMs}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Trend detail</strong><span class="muted">\u6BCF\u4E2A bucket \u7684\u8BE6\u7EC6\u547D\u4E2D\u7387</span></div><table id="trendTable" class="trend-table"><thead><tr><th>Bucket</th><th>Traces</th><th>Sticky</th><th>Cascade</th><th>Shadow</th><th>Alignment</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading...</td></tr></tbody></table></div></div><table id="traceTable"><thead><tr><th>Request</th><th>Session</th><th>Final Model</th><th>Reasons</th><th>Latency</th><th>Inspect</th></tr></thead><tbody><tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Trace Detail</strong><span id="detailHint" class="muted">\u70B9\u51FB\u4E0A\u8868\u4E2D\u7684 View \u67E5\u770B\u8BE6\u60C5</span></div><pre id="traceDetail">{}</pre></div><div class="panel"><div class="row"><strong>Snapshot Management</strong><span class="muted">\u67E5\u770B\u5BFC\u51FA\u5386\u53F2\u3001\u5B9A\u65F6\u4EFB\u52A1\uFF0C\u5E76\u624B\u52A8\u521B\u5EFA\u5FEB\u7167</span></div><div class="action-row"><select id="snapshotFormat"><option value="json">snapshot json</option><option value="csv">snapshot csv</option></select><button id="createSnapshotBtn" type="button">\u751F\u6210\u5FEB\u7167</button><span id="snapshotStatus" class="muted">\u5C1A\u672A\u521B\u5EFA\u5FEB\u7167</span></div><table id="exportTable" class="management-table"><thead><tr><th>Export</th><th>Kind</th><th>Format</th><th>Created</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading exports...</td></tr></tbody></table><table id="scheduleTable" class="management-table"><thead><tr><th>Schedule</th><th>Interval</th><th>Format</th><th>Last run</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading schedules...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Archive Management</strong><span class="muted">\u6D4F\u89C8\u538B\u7F29\u5F52\u6863\u5E76\u67E5\u770B\u5206\u9875\u7ED3\u679C</span></div><div class="action-row"><input id="archiveDate" placeholder="YYYY-MM-DD"><input id="archivePage" placeholder="page" value="1"><input id="archivePageSize" placeholder="pageSize" value="5"><button id="loadArchivesBtn" type="button">\u52A0\u8F7D\u5F52\u6863</button><span id="archiveStatus" class="muted">\u5C1A\u672A\u52A0\u8F7D\u5F52\u6863</span></div><table id="archiveTable" class="management-table"><thead><tr><th>Archive</th><th>Range</th><th>Count</th><th>Compressed</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading archives...</td></tr></tbody></table></div><div class="panel"><p>\u5176\u4ED6\u7BA1\u7406 API\uFF1A</p><ul><li><code>GET /api/config</code> \u2014 \u8BFB\u53D6\u5F53\u524D\u914D\u7F6E</li><li><code>GET /api/models/compiled</code> \u2014 \u67E5\u770B Models \u7F16\u8BD1\u540E\u7684\u5185\u90E8 provider / model \u6620\u5C04</li><li><code>POST /api/models/compiled/preview</code> \u2014 \u7528\u914D\u7F6E\u8349\u7A3F\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u5199\u56DE\u6587\u4EF6</li><li><code>POST /api/config</code> \u2014 \u4FDD\u5B58\u914D\u7F6E</li><li><code>GET /api/transformers</code> \u2014 \u67E5\u770B\u5DF2\u52A0\u8F7D transformer</li><li><code>POST /api/restart</code> \u2014 \u91CD\u542F\u670D\u52A1</li><li><code>GET /api/governance/archives</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5F52\u6863\u5217\u8868</li><li><code>GET /api/governance/archives/:file</code> \u2014 \u67E5\u770B\u5F52\u6863\u5185 traces</li><li><code>POST /api/governance/archives/:file/delete</code> \u2014 \u5220\u9664\u6307\u5B9A\u5F52\u6863</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div></section><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const serviceReadyStatus=document.getElementById('serviceReadyStatus');const servicePortStatus=document.getElementById('servicePortStatus');const modelCountStatus=document.getElementById('modelCountStatus');const routerDefaultStatus=document.getElementById('routerDefaultStatus');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const metricsGrid=document.getElementById('metricsGrid');const bucketGrid=document.getElementById('bucketGrid');const bucketHint=document.getElementById('bucketHint');const routeRanking=document.getElementById('routeRanking');const modelRanking=document.getElementById('modelRanking');const intentRanking=document.getElementById('intentRanking');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');const surfaceTabs=Array.from(document.querySelectorAll('[data-surface-target]'));const surfacePanels=Array.from(document.querySelectorAll('[data-surface]'));let currentDraftConfig={};let knownModelIds=[];let activeValidationHighlight=null;const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94\u8DEF\u7531\u89C4\u5219\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u589E\u5F3A\u4E0E\u6821\u9A8C\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','SmartRouter.sticky.alignment','SmartRouter.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function setActiveSurface(surfaceName){ surfacePanels.forEach((panel)=>{ panel.hidden=panel.dataset.surface !== surfaceName; }); surfaceTabs.forEach((tab)=>{ const active=tab.dataset.surfaceTarget === surfaceName; tab.classList.toggle('active',active); tab.setAttribute('aria-selected', active ? 'true' : 'false'); });}function inferProviderTemplateKey(model){ const explicit=String(model?.provider_template || '').trim(); if(explicit && modelProviderTemplates[explicit]){ return explicit; } const api=String(model?.api || model?.api_base_url || '').trim().toLowerCase(); const modelInterface=String(model?.interface || model?.protocol || '').trim().toLowerCase(); const exactMatch=Object.entries(modelProviderTemplates).find(([,item])=>String(item.api || '').trim().toLowerCase()===api && String(item.interface || '').trim().toLowerCase()===modelInterface); if(exactMatch){ return exactMatch[0]; } if(api.includes('api.anthropic.com/v1/messages') || modelInterface === 'anthropic'){ return 'anthropic'; } if(api.includes('openrouter.ai')){ return 'openrouter'; } if(api.includes('deepseek.com')){ return 'deepseek'; } if(api.includes('siliconflow.cn')){ return 'siliconflow'; } if(api.includes('api.openai.com')){ return 'openai-compatible'; } return '';}function getProviderTemplateContext(model){ const templateKey=inferProviderTemplateKey(model) || defaultProviderTemplateKey; return { templateKey, template:modelProviderTemplates[templateKey] || modelProviderTemplates[defaultProviderTemplateKey] || {} };}function createDraftModelFromTemplate(templateKey){ const resolvedKey=(templateKey && modelProviderTemplates[templateKey]) ? templateKey : defaultProviderTemplateKey; const template=modelProviderTemplates[resolvedKey] || {}; return { provider_template:resolvedKey, id:template.suggested_id || '', api:template.api || '', interface:template.interface || 'openai', model:template.default_model || '', thinking:template.default_thinking || 'auto' };}function getModelIdSuggestionsMarkup(idPrefix){ return '<datalist id="'+idPrefix+'">'+knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join('')+'</datalist>';}function resolvePresetModelId(seed){ const source=String(seed || '').trim().toLowerCase(); if(!source || !knownModelIds.length){ return seed; } if(knownModelIds.includes(seed)){ return seed; } const ranked=knownModelIds.map((modelId)=>{ const target=String(modelId || '').toLowerCase(); let score=0; if(target===source){ score+=100; } if(target.includes(source) || source.includes(target)){ score+=40; } source.split(/[^a-z0-9]+/).filter(Boolean).forEach((part)=>{ if(target.includes(part)){ score+=Math.min(part.length * 4, 24); } }); return { modelId, score }; }).filter((item)=>item.score>0).sort((a,b)=>b.score-a.score || a.modelId.localeCompare(b.modelId)); return ranked.length ? ranked[0].modelId : seed;}function getTriggerPatternValidationHint(pattern){ if((pattern?.type || 'exact') === 'regex'){ return pattern?.pattern ? { level:'ok', message:'regex pattern \u5DF2\u914D\u7F6E' } : { level:'warn', message:'regex \u6A21\u5F0F\u9700\u8981\u586B\u5199 pattern' }; } return Array.isArray(pattern?.keywords) && pattern.keywords.some((keyword)=>String(keyword || '').trim()) ? { level:'ok', message:'exact keywords \u5DF2\u914D\u7F6E' } : { level:'warn', message:'exact \u6A21\u5F0F\u81F3\u5C11\u9700\u8981\u4E00\u4E2A keyword' };}function getDraftSmartRouterConfig(config){ const smart={ ...((config && config.SmartRouter) || {}) }; const smartExplicit=config && Object.prototype.hasOwnProperty.call(config,'SmartRouter'); const legacyIntentEnabled=Boolean(config?.TriggerRouter?.llm_intent_recognition); const legacyIntentModel=config?.TriggerRouter?.intent_model || ''; if(!smart.analysis_scope && config?.TriggerRouter?.analysis_scope){ smart.analysis_scope=config.TriggerRouter.analysis_scope; } if((!Array.isArray(smart.rules) || !smart.rules.length) && Array.isArray(config?.TriggerRouter?.rules)){ smart.rules=config.TriggerRouter.rules; } if(!smart.semantic && (config?.Governance?.semantic || config?.TriggerRouter?.llm_intent_recognition)){ smart.semantic={ ...((config && config.Governance && config.Governance.semantic) || {}) }; if(config?.TriggerRouter?.llm_intent_recognition){ smart.semantic.enabled=true; smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || config.TriggerRouter.intent_model || ''; } } if(!smart.sticky && config?.Governance?.sticky){ smart.sticky={ ...(config.Governance.sticky || {}) }; } if(!smartExplicit && !smart.enabled && (config?.TriggerRouter?.enabled || smart.rules?.length || smart.router_model || smart.candidates?.length || smart.semantic || smart.sticky)){ smart.enabled=true; } if(smart.enabled){ smart.analysis_scope=smart.analysis_scope || 'last_message'; smart.semantic={ ...(smart.semantic || {}) }; smart.semantic.enabled=smart.semantic.enabled !== undefined ? smart.semantic.enabled : true; smart.semantic.threshold=smart.semantic.threshold !== undefined ? smart.semantic.threshold : 0.2; if(legacyIntentEnabled){ smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || legacyIntentModel; } smart.sticky={ ...(smart.sticky || {}) }; smart.sticky.enabled=smart.sticky.enabled !== undefined ? smart.sticky.enabled : true; smart.sticky.alignment={ ...((smart.sticky && smart.sticky.alignment) || {}) }; smart.sticky.alignment.enabled=smart.sticky.alignment.enabled !== undefined ? smart.sticky.alignment.enabled : true; smart.sticky.alignment.summarizer_model=smart.sticky.alignment.summarizer_model || smart.router_model || config?.Router?.default || legacyIntentModel || ''; } return smart;}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const smart=getDraftSmartRouterConfig(config); const triggerRules=Array.isArray(smart?.rules) ? smart.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(smart?.candidates) ? smart.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, smart?.router_model, smart?.sticky?.alignment?.summarizer_model, smart?.semantic?.classifier_model, config?.Governance?.shadow?.verifier_model].filter(v=>typeof v === 'string' && v.trim()).length + triggerRules.filter(rule=>rule?.model).length + smartCandidates.filter(item=>item?.model).length + cascadeLevels.reduce((sum,level)=>sum + (level?.from ? 1 : 0) + (level?.to ? 1 : 0), 0); draftSummaryGrid.innerHTML=[ ['Models', models.length], ['Routing rules', triggerRules.length], ['Patterns', patternCount], ['Smart candidates', smartCandidates.length], ['Cascade levels', cascadeLevels.length], ['Model refs', modelRefCount] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function updateStatusSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; modelCountStatus.textContent=String(models.length); routerDefaultStatus.textContent=config?.Router?.default || '-';}function renderDraftValidation(errors,warnings,issueReport){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; const contractIssues=Array.isArray(issueReport?.issues) ? issueReport.issues : []; if(!errorList.length && !warningList.length && !contractIssues.length){ draftValidationList.innerHTML='<div class="alert info"><strong>No validation issues</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u672A\u53D1\u73B0\u96C6\u4E2D\u5C55\u793A\u7684\u95EE\u9898</div></div>'; return; } const extractPath=(text)=>{ const match=String(text).match(/^(Models(?:\\[[0-9]+\\])?(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Router(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|TriggerRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|SmartRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Governance(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?)/); return match ? match[1] : ''; }; const sourceItems=contractIssues.length ? contractIssues.map(item=>({ text:String(item.message || ''), severity:item.severity==='error' ? 'error' : 'warning', path:item.path || '', action:item.action || '' })) : [...errorList.map(item=>({ text:String(item), severity:'error', path:'', action:'' })), ...warningList.map(item=>({ text:String(item), severity:'warning', path:'', action:'' }))]; const grouped=sourceItems.reduce((acc,item)=>{ const text=item.text; const path=item.path || extractPath(text); const bucket=path.startsWith('Models') || text.startsWith('Models') ? 'Models' : path.startsWith('Router') || text.startsWith('Router') ? 'Router' : path.startsWith('TriggerRouter') || text.startsWith('TriggerRouter') ? 'SmartRouter' : path.startsWith('SmartRouter') || text.startsWith('SmartRouter') ? 'SmartRouter' : (path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic') || text.startsWith('Governance.sticky') || text.startsWith('Governance.semantic')) ? 'SmartRouter' : path.startsWith('Governance') || text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path, severity:item.severity, action:item.action || '' }); return acc; }, {}); const errorCount=contractIssues.length ? contractIssues.filter(item=>item.severity==='error').length : errorList.length; const warningCount=contractIssues.length ? contractIssues.filter(item=>item.severity!=='error').length : warningList.length; const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorCount)+' errors / '+esc(warningCount)+' warnings</span></div><div class="muted">'+(errorCount ? '\u8BF7\u4F18\u5148\u4FEE\u590D errors\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u63A5\u53D7 warnings\u3002' : '\u5F53\u524D\u65E0\u963B\u65AD\u9519\u8BEF\uFF0C\u53EF\u6309\u9700\u5904\u7406 warnings\u3002')+'</div></div>'; draftValidationList.innerHTML=summary + Object.entries(grouped).map(([bucket,items])=>{ const hasError=items.some(item=>item.severity==='error'); const levelClass=hasError ? 'warn' : 'info'; const actionLabel=hasError ? 'repair first' : 'review before save'; return '<div class="alert '+levelClass+'"><div class="row"><strong>'+esc(bucket)+'</strong><span class="pill">'+esc(items.length)+' issues</span></div><div class="muted">'+esc(actionLabel)+'</div><div>'+items.slice(0,4).map(item=>'<div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+'<span class="pill">'+esc(item.severity==='error' ? 'error' : 'warning')+'</span> '+esc(item.text)+(item.action ? ('<div class="muted">Action: '+esc(item.action)+'</div>') : '')+'</div>').join('')+'</div></div>'; }).join('');}function getCapabilityWarningActionLabel(code){ if(code==='thinking_ignored'){ return '\u79FB\u9664 thinking'; } if(code==='tools_text_fallback' || code==='images_text_fallback'){ return '\u6062\u590D\u9ED8\u8BA4 capability'; } return '';}function renderCapabilityWarnings(report){ const entries=Array.isArray(report?.entries) ? report.entries : []; if(!entries.length){ capabilityWarningsList.innerHTML='<div class="alert info"><strong>No capability warnings</strong><div class="muted">\u5F53\u524D compiled models \u672A\u53D1\u73B0\u9700\u8981\u989D\u5916\u63D0\u793A\u7684\u80FD\u529B\u964D\u7EA7</div></div>'; return; } const summary=report?.summary || {}; capabilityWarningsList.innerHTML='<div class="alert info"><strong>Capability warning summary</strong><div class="muted">warn '+esc(summary.warn ?? 0)+' / info '+esc(summary.info ?? 0)+' / total '+esc(summary.total ?? entries.length)+'</div></div>' + entries.map(item=>{ const actionLabel=getCapabilityWarningActionLabel(item.code); return '<div class="alert '+esc(item.level === 'warn' ? 'warn' : 'info')+'"><div class="row"><strong>'+esc(item.code || item.level || 'warning')+'</strong><span class="pill">'+esc(item.modelId || '-').trim()+'</span></div><div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+esc(item.message || '')+'</div>'+(actionLabel ? ('<div class="row" style="margin-top:.5rem"><button type="button" data-apply-warning-path="'+esc(item.path || '')+'" data-apply-warning-code="'+esc(item.code || '')+'">'+esc(actionLabel)+'</button></div>') : '')+'</div>'; }).join('');}function findValidationTarget(path){ if(!path){ return null; } if(path.startsWith('Models')){ return modelsFormGrid; } if(path === 'Router.default'){ return draftRouterDefault; } if(path.startsWith('TriggerRouter.intent_model')){ return triggerIntentModel; } if(path.startsWith('TriggerRouter.rules[')){ return triggerRulesList; } if(path.startsWith('SmartRouter.router_model')){ return smartRouterModel; } if(path.startsWith('SmartRouter.candidates[')){ return smartCandidatesList; } if(path.startsWith('Governance.cascade.levels[')){ return governanceCascadeLevelsList; } if(path.startsWith('Governance.sticky.alignment')){ return governanceSummarizerModel; } if(path.startsWith('Governance.semantic')){ return governanceClassifierModel; } if(path.startsWith('Governance.shadow')){ return governanceVerifierModel; } if(path.startsWith('Governance')){ return governanceEnabled; } return null;}function jumpToValidationPath(path){ const target=findValidationTarget(path); if(!target || typeof target.scrollIntoView !== 'function'){ return; } if(activeValidationHighlight && activeValidationHighlight.classList){ activeValidationHighlight.classList.remove('jump-highlight'); } target.scrollIntoView({ behavior:'smooth', block:'center' }); if(target.classList){ target.classList.add('jump-highlight'); activeValidationHighlight=target; setTimeout(()=>{ if(target.classList){ target.classList.remove('jump-highlight'); if(activeValidationHighlight===target){ activeValidationHighlight=null; } } }, 1800); } if(typeof target.focus === 'function'){ target.focus({ preventScroll:true }); }}function renderDraftPresetModeHint(){ const overwriteMode=draftPresetMode.value === 'replace'; draftPresetModeHint.textContent=overwriteMode ? 'overwrite \u4F1A\u91CD\u7F6E SmartRouter / Governance \u76F8\u5173\u8868\u5355\uFF0C\u518D\u5E94\u7528\u9884\u8BBE' : 'append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145 SmartRouter / Governance \u76F8\u5173\u5B57\u6BB5';}function deriveActualAffectedAreas(preview){ const areas=new Set(); const diff=preview?.diff || {}; const impact=preview?.referenceImpact || {}; if((diff.providerChanges || []).length || (diff.modelChanges || []).length){ areas.add('Models'); } (impact.entries || []).forEach((entry)=>{ const path=String(entry.path || ''); if(path.startsWith('Router.')){ areas.add('Router'); } else if(path.startsWith('TriggerRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.')){ areas.add('Governance'); } }); return Array.from(areas);}function renderDraftPreviewMeta(meta){ if(!meta){ draftPreviewMeta.innerHTML='<div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div>'; return; } draftPreviewMeta.innerHTML='<div class="alert info"><strong>'+esc(meta.title || 'Preset dry-run')+'</strong><div>'+esc(meta.description || '')+'</div><div class="muted">\u6A21\u5F0F\uFF1A'+esc(meta.mode || '-')+' \xB7 \u9884\u8BBE\u58F0\u660E\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((meta.affects || []).join(' / ') || '-')</div><div class="muted">\u5B9E\u9645\u9884\u89C8\u547D\u4E2D\u533A\u57DF\uFF1A'+esc((meta.actualAffects || []).join(' / ') || '-')</div></div>';}function renderDraftPresetGuide(){ draftPresetList.innerHTML=Object.entries(draftPresets).map(([key,preset])=>'<div class="alert info"><strong>'+esc(preset.label || key)+'</strong><div>'+esc(preset.description || '')+'</div><div class="muted">\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((preset.affects || []).join(' / '))+'</div></div>').join('');}function updateTopLevelModelSuggestionLists(){ const markup=knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join(''); [topLevelTriggerIntentSuggestions,topLevelSmartRouterSuggestions,topLevelGovernanceSummarizerSuggestions,topLevelGovernanceClassifierSuggestions,topLevelGovernanceVerifierSuggestions].forEach(node=>{ if(node){ node.innerHTML=markup; } });}function renderModelsForm(models){ const list=Array.isArray(models) ? models : []; draftModelsCount.value=String(list.length); if(!list.length){ modelsFormGrid.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div>'; return; } modelsFormGrid.innerHTML=list.map((model,index)=>{ const templateContext=getProviderTemplateContext(model); const template=templateContext.template; return '<div class="model-card" data-model-card="'+index+'">' + '<div class="model-card-header"><strong>Model #'+(index+1)+'</strong><button type="button" data-remove-model="'+index+'">\u5220\u9664</button></div>' + '<div class="model-card-grid">' + '<div><label>Provider template</label><div class="row"><select data-field="provider_template" data-index="'+index+'"><option value="">custom</option>'+Object.entries(modelProviderTemplates).map(([key,item])=>'<option value="'+esc(key)+'"'+(model.provider_template === key ? ' selected' : '')+'>'+esc(item.label)+'</option>').join('')+'</select><button type="button" data-apply-template="'+index+'">\u5E94\u7528</button></div></div>' + '<div><label>ID</label><input data-field="id" data-index="'+index+'" value="'+esc(model.id || '')+'" placeholder="'+esc(template.suggested_id || 'sonnet')+'"><div class="muted">\u5EFA\u8BAE\u6A21\u677F\uFF1A'+esc(template.label || templateContext.templateKey || 'custom')+'</div></div>' + '<div><label>Interface</label><select data-field="interface" data-index="'+index+'"><option value="openai"'+(((model.interface || model.protocol || 'openai') === 'openai') ? ' selected' : '')+'>openai</option><option value="anthropic"'+(((model.interface || model.protocol) === 'anthropic') ? ' selected' : '')+'>anthropic</option></select></div>' + '<div><label>Model</label><input data-field="model" data-index="'+index+'" list="modelSuggestions'+index+'" value="'+esc(model.model || '')+'" placeholder="'+esc(template.default_model || 'anthropic/claude-sonnet-4')+'"><datalist id="modelSuggestions'+index+'">'+((template.model_examples || []).map(item=>'<option value="'+esc(item)+'"></option>').join(''))+'</datalist><div class="muted">\u4F8B\u5982\uFF1A'+esc((template.model_examples || ['anthropic/claude-sonnet-4']).join(' / '))+'</div></div>' + '<div><label>API</label><input data-field="api" data-index="'+index+'" value="'+esc(model.api || model.api_base_url || '')+'" placeholder="'+esc(template.api || 'https://...')+'"></div>' + '<div><label>Key</label><input data-field="key" data-index="'+index+'" value="'+esc(model.key || model.api_key || '')+'" placeholder="'+esc(template.key_placeholder || 'sk-...')+'"></div>' + '<div><label>Thinking</label><select data-field="thinking_profile" data-index="'+index+'"><option value="">default</option><option value="off"'+(((model.thinking === 'off') || model.thinking?.mode === 'off') ? ' selected' : '')+'>off</option><option value="auto"'+(((model.thinking === 'auto') || model.thinking?.mode === 'auto') ? ' selected' : '')+'>auto</option><option value="on"'+(((model.thinking === 'on') || (model.thinking?.mode === 'on' && !model.thinking?.effort)) ? ' selected' : '')+'>on</option><option value="low"'+(((model.thinking === 'low') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'low' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>low</option><option value="medium"'+(((model.thinking === 'medium') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'medium' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>medium</option><option value="high"'+(((model.thinking === 'high') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'high' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>high</option><option value="custom"'+(((typeof model.thinking === 'object') && model.thinking && model.thinking.budget_tokens) ? ' selected' : '')+'>custom</option></select></div>' + '<div><label>Thinking mode</label><select data-field="thinking_mode" data-index="'+index+'"><option value="">default</option><option value="off"'+(model.thinking?.mode === 'off' ? ' selected' : '')+'>off</option><option value="auto"'+(model.thinking?.mode === 'auto' ? ' selected' : '')+'>auto</option><option value="on"'+(model.thinking?.mode === 'on' ? ' selected' : '')+'>on</option></select></div>' + '<div><label>Thinking effort</label><select data-field="thinking_effort" data-index="'+index+'"><option value="">default</option><option value="low"'+(model.thinking?.effort === 'low' ? ' selected' : '')+'>low</option><option value="medium"'+(model.thinking?.effort === 'medium' ? ' selected' : '')+'>medium</option><option value="high"'+(model.thinking?.effort === 'high' ? ' selected' : '')+'>high</option></select></div>' + '<div><label>Thinking budget</label><input data-field="thinking_budget_tokens" data-index="'+index+'" value="'+esc(model.thinking?.budget_tokens || '')+'" placeholder="1024"></div>' + '<div><label>Vendor hint</label><input data-field="vendor_hint" data-index="'+index+'" value="'+esc(model.metadata?.vendor_hint || '')+'" placeholder="'+esc(template.vendor_hint || 'openrouter')+'"></div>' + '<div><label>Reasoning support</label><select data-field="supports_reasoning" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_reasoning === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_reasoning === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Tool support</label><select data-field="supports_tools" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_tools === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_tools === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Image support</label><select data-field="supports_images" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_images === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_images === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div style="grid-column:1/-1"><label>Metadata (advanced JSON)</label><textarea data-field="metadata" data-index="'+index+'" placeholder="{\\"label\\":\\"Balanced profile\\"}">'+esc(model.metadata ? JSON.stringify(model.metadata, null, 2) : '')+'</textarea><div class="muted">\u666E\u901A capability \u5EFA\u8BAE\u4F18\u5148\u4F7F\u7528\u4E0A\u9762\u7684\u663E\u5F0F\u5B57\u6BB5\uFF1B\u8FD9\u91CC\u4FDD\u7559\u7ED9\u9AD8\u7EA7\u6269\u5C55\u5143\u6570\u636E\u3002</div></div>' + '</div>' + '</div>'; }).join('');}function extractModelsFromForm(){ const cards=Array.from(modelsFormGrid.querySelectorAll('[data-model-card]')); return cards.map((card,index)=>{ const read=(field)=>card.querySelector('[data-field="'+field+'"][data-index="'+index+'"]'); const providerTemplate=(read('provider_template')?.value || '').trim(); const metadataRaw=(read('metadata')?.value || '').trim(); let metadata; if(metadataRaw){ metadata=JSON.parse(metadataRaw); } else { metadata={}; } const thinkingProfile=(read('thinking_profile')?.value || '').trim(); const vendorHint=(read('vendor_hint')?.value || '').trim(); const supportsReasoning=(read('supports_reasoning')?.value || '').trim(); const supportsTools=(read('supports_tools')?.value || '').trim(); const supportsImages=(read('supports_images')?.value || '').trim(); const thinking={}; const mode=(read('thinking_mode')?.value || '').trim(); const effort=(read('thinking_effort')?.value || '').trim(); const budget=(read('thinking_budget_tokens')?.value || '').trim(); if(mode) thinking.mode=mode; if(effort) thinking.effort=effort; if(budget) thinking.budget_tokens=Number(budget); const model={ id:(read('id')?.value || '').trim(), api:(read('api')?.value || '').trim(), key:(read('key')?.value || '').trim(), interface:(read('interface')?.value || '').trim(), model:(read('model')?.value || '').trim(), }; if(vendorHint){ metadata.vendor_hint=vendorHint; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'vendor_hint')){ delete metadata.vendor_hint; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); const smartCandidates=extractSmartCandidatesFromForm(); const smartRouterEnabled=Boolean(smartEnabled.checked || triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim() || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim()); if(smartRouterEnabled){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: true, analysis_scope: triggerAnalysisScope.value || payload.SmartRouter?.analysis_scope || 'last_message', router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined, rules: triggerRules, semantic:(governanceSemanticEnabled.checked || triggerIntentEnabled.checked || governanceClassifierModel.value.trim() || triggerIntentModel.value.trim()) ? { ...(((payload.SmartRouter || {}).semantic) || {}), enabled:Boolean(governanceSemanticEnabled.checked || triggerIntentEnabled.checked), mode:'classifier', classifier_model: governanceClassifierModel.value.trim() || triggerIntentModel.value.trim() } : undefined, sticky:(governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim()) ? { ...(((payload.SmartRouter || {}).sticky) || {}), enabled:true, alignment:{ ...((((payload.SmartRouter || {}).sticky || {}).alignment) || {}), enabled:Boolean(governanceAlignmentEnabled.checked), summarizer_model: governanceSummarizerModel.value.trim() } } : undefined }; } else { delete payload.SmartRouter; } delete payload.TriggerRouter; const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const smart=getDraftSmartRouterConfig(config); const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(smart.enabled); triggerIntentEnabled.checked=Boolean(smart.semantic?.enabled && smart.semantic?.mode === 'classifier'); triggerAnalysisScope.value=smart.analysis_scope || 'last_message'; triggerIntentModel.value=smart.semantic?.classifier_model || trigger.intent_model || ''; renderTriggerRulesList(smart.rules || trigger.rules || []); smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(smart.sticky?.alignment?.enabled); governanceSummarizerModel.value=smart.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(smart.semantic?.enabled); governanceClassifierModel.value=smart.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u540C\u6B65 Models \u8868\u5355\u5230 JSON \u8349\u7A3F'; } catch (error) { draftPreviewStatus.textContent='\u540C\u6B65\u5931\u8D25\uFF1A'+error.message; }}function applyReferenceSuggestion(path,modelId){ if(!modelId){ return; } if(path==='Router.default'){ draftRouterDefault.value=modelId; syncDraftEditorFromForm(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 Router.default'; return; } const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const pathMatch=path.match(/^([^.[]+)(?:.(.+))?$/); if(!pathMatch){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\uFF1A'+path; return; } const tokens=path.replace(/[(d+)]/g,'.$1').split('.'); let cursor=payload; for(let i=0;i<tokens.length-1;i++){ const token=tokens[i]; const nextToken=tokens[i+1]; if(cursor[token] === undefined){ cursor[token]=String(Number(nextToken))===nextToken ? [] : {}; } cursor=cursor[token]; } cursor[tokens[tokens.length-1]]=modelId; currentDraftConfig=payload; if(payload.Router?.default){ draftRouterDefault.value=payload.Router.default; } renderConfigControlForms(payload); configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyCapabilityWarningSuggestion(path,code){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const tokens=String(path || '').replace(/[(d+)]/g,'.$1').split('.').filter(Boolean); if(!tokens.length){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } let cursor=payload; for(let i=0;i<tokens.length-1;i++){ if(cursor == null){ break; } cursor=cursor[tokens[i]]; } const lastToken=tokens[tokens.length-1]; if(code==='thinking_ignored'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } } else if(code==='tools_text_fallback' || code==='images_text_fallback'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } if(cursor && !Object.keys(cursor).length){ const parentTokens=tokens.slice(0,-1); const maybeMetadataKey=parentTokens[parentTokens.length-1]; if(maybeMetadataKey==='metadata'){ let parentCursor=payload; for(let i=0;i<parentTokens.length-1;i++){ if(parentCursor == null){ break; } parentCursor=parentCursor[parentTokens[i]]; } if(parentCursor && Object.prototype.hasOwnProperty.call(parentCursor,'metadata')){ delete parentCursor.metadata; } } } } else { draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528 warning \u4FEE\u6B63\uFF1A'+code+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function renderCompiledDiff(diff){ const summary=diff?.summary || {}; compiledDiffSummary.innerHTML=[ ['Added providers', summary.addedProviders ?? 0], ['Removed providers', summary.removedProviders ?? 0], ['Changed providers', summary.changedProviders ?? 0], ['Added models', summary.addedModels ?? 0], ['Removed models', summary.removedModels ?? 0], ['Changed models', summary.changedModels ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const rows=[ ...((diff?.providerChanges || []).map(item=>({ scope:'provider', key:item.name, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ...((diff?.modelChanges || []).map(item=>({ scope:'model', key:item.modelId, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ]; compiledDiffTableBody.innerHTML=rows.length ? rows.map(item=>'<tr>' + '<td>'+esc(item.scope)+'</td>' + '<td>'+esc(item.type)+'</td>' + '<td><code>'+esc(item.key)+'</code></td>' + '<td>'+esc(item.fields.join(', ') || '-')+'</td>' + '<td><code>'+esc(item.target.providerName || item.target.name || '-')+'</code><div class="muted">'+esc(item.target.modelName || (item.target.models || []).join(', ') || '-')}</div></td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled registry changes</td></tr>';}function renderReferenceImpact(impact){ const summary=impact?.summary || {}; referenceImpactSummary.innerHTML=[ ['Total refs', summary.total ?? 0], ['modelId refs', summary.modelIdRefs ?? 0], ['Legacy refs', summary.legacyRefs ?? 0], ['Valid modelIds', summary.validModelIds ?? 0], ['Missing modelIds', summary.missingModelIds ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const entries=impact?.entries || []; referenceImpactTableBody.innerHTML=entries.length ? entries.map(item=>'<tr>' + '<td><code>'+esc(item.path)+'</code></td>' + '<td><code>'+esc(item.value)+'</code></td>' + '<td>'+esc(item.referenceType)+'</td>' + '<td>'+esc(item.status)+'</td>' + '<td><code>'+esc(item.resolvedTarget?.providerName || '-')+'</code><div class="muted">'+esc(item.resolvedTarget?.modelName || '-')}</div></td>' + '<td>'+((item.suggestions || []).length ? item.suggestions.map(s=>'<div><code>'+esc(s.modelId)+'</code><div class="muted">'+esc(s.modelName || '-')+'</div><button type="button" data-apply-reference-path="'+esc(item.path)+'" data-apply-reference-model="'+esc(s.modelId)+'">\u5E94\u7528\u5EFA\u8BAE</button></div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No model references found</td></tr>';}function renderCompiledModels(data){ const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04'; compiledProvidersTableBody.innerHTML=providers.length ? providers.map(provider=>'<tr>' + '<td><code>'+esc(provider.name)+'</code><div class="muted">'+esc(provider.api_base_url || '-')+'</div></td>' + '<td>'+esc(provider.transformer?.use?.[0] || '-')+'</td>' + '<td>'+esc((provider.models || []).join(', ') || '-')+'</td>' + '<td>'+esc(JSON.stringify(provider.transformer || {}))+'</td>' + '<td>'+esc(provider.has_api_key ? 'configured' : 'missing')+'</td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled providers</td></tr>'; compiledModelMapTableBody.innerHTML=modelMapEntries.length ? modelMapEntries.map(([modelId,item])=>'<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td><code>'+esc(item.providerName || '-')+'</code><div class="muted">'+esc(item.modelName || '-')+'</div></td>' + '<td>'+esc(item.protocol || '-')+'</td>' + '<td>'+esc(item.compatibilityProfile || '-')+'</td>' + '<td>'+esc(item.dispatchFormat || '-')+'</td>' + '<td><code>'+esc(JSON.stringify(item.thinking || { mode: 'off' }))+'</code></td>' + '<td><code>'+esc(JSON.stringify(item.capabilities || {}))+'</code></td>' + '<td>'+esc(item.source || '-')+'</td>' + '</tr>').join('') : '<tr><td colspan="8" class="muted">No compiled model map</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); updateStatusSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function loadServiceStatus(){ serviceReadyStatus.textContent='checking'; try { const res=await fetch('/api/health'); const data=await res.json(); serviceReadyStatus.textContent=data.ready ? 'ready' : 'not ready'; servicePortStatus.textContent=data.port || '-'; } catch (_error) { serviceReadyStatus.textContent='unreachable'; }}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || [], data.issueReport); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addCascadeLevel(){ const next=extractCascadeLevelsFromForm(); next.push({ from:'', to:'' }); renderCascadeLevelsList(next); syncDraftEditorFromForm(); }modelsFormGrid.addEventListener('input',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('change',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-template]'); if(applyBtn){ const applyIndex=Number(applyBtn.dataset.applyTemplate); applyProviderTemplate(applyIndex); syncDraftEditorFromForm(); return; } const btn=e.target.closest('button[data-remove-model]'); if(!btn){ return; } const removeIndex=Number(btn.dataset.removeModel); const nextModels=extractModelsFromForm().filter((_,index)=>index!==removeIndex); renderModelsForm(nextModels); syncDraftEditorFromForm(); });triggerRulesList.addEventListener('input',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('change',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('click',(e)=>{ const addKeywordBtn=e.target.closest('button[data-add-trigger-keyword]'); if(addKeywordBtn){ addTriggerKeyword(Number(addKeywordBtn.dataset.addTriggerKeyword), Number(addKeywordBtn.dataset.patternIndex)); return; } const removeKeywordBtn=e.target.closest('button[data-remove-trigger-keyword]'); if(removeKeywordBtn){ const ruleIndex=Number(removeKeywordBtn.dataset.removeTriggerKeyword); const patternIndex=Number(removeKeywordBtn.dataset.patternIndex); const keywordIndex=Number(removeKeywordBtn.dataset.keywordIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex] && next[ruleIndex].patterns && next[ruleIndex].patterns[patternIndex]){ const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=(pattern.keywords || []).filter((_,index)=>index!==keywordIndex); if(!pattern.keywords.length){ pattern.keywords=['']; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const addBtn=e.target.closest('button[data-add-trigger-pattern]'); if(addBtn){ addTriggerPattern(Number(addBtn.dataset.addTriggerPattern)); return; } const removePatternBtn=e.target.closest('button[data-remove-trigger-pattern]'); if(removePatternBtn){ const ruleIndex=Number(removePatternBtn.dataset.removeTriggerPattern); const patternIndex=Number(removePatternBtn.dataset.patternIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex]){ next[ruleIndex].patterns=(next[ruleIndex].patterns || []).filter((_,index)=>index!==patternIndex); if(!next[ruleIndex].patterns.length){ next[ruleIndex].patterns=[{ type:'exact', keywords:[] }]; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const btn=e.target.closest('button[data-remove-trigger-rule]'); if(!btn){ return; } const next=extractTriggerRulesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeTriggerRule)); renderTriggerRulesList(next); syncDraftEditorFromForm(); });smartCandidatesList.addEventListener('input',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('change',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-smart-candidate]'); if(!btn){ return; } const next=extractSmartCandidatesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeSmartCandidate)); renderSmartCandidatesList(next); syncDraftEditorFromForm(); });governanceCascadeLevelsList.addEventListener('input',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('change',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-cascade-level]'); if(!btn){ return; } const next=extractCascadeLevelsFromForm().filter((_,index)=>index!==Number(btn.dataset.removeCascadeLevel)); renderCascadeLevelsList(next); syncDraftEditorFromForm(); });referenceImpactTableBody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-apply-reference-path]'); if(!btn){ return; } applyReferenceSuggestion(btn.dataset.applyReferencePath, btn.dataset.applyReferenceModel); });draftValidationList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });capabilityWarningsList.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-warning-path]'); if(applyBtn){ applyCapabilityWarningSuggestion(applyBtn.dataset.applyWarningPath, applyBtn.dataset.applyWarningCode); return; } const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });surfaceTabs.forEach((tab)=>tab.addEventListener('click',()=>setActiveSurface(tab.dataset.surfaceTarget || 'user')));setActiveSurface('user');function renderMetrics(metrics){ metricsGrid.innerHTML=[ ['Recent traces', metrics.totalTraces ?? 0], ['Sticky hit rate', pct(metrics.stickyHitRate)], ['Cascade rate', pct(metrics.cascadeTriggeredRate)], ['Shadow rate', pct(metrics.shadowCheckedRate)], ['Alignment rate', pct(metrics.alignmentUsedRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.SmartRouter?.enabled), analysis_scope: payload.SmartRouter?.analysis_scope || 'last_message', router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: payload.SmartRouter?.candidates || [], cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.SmartRouter?.rules || []) }; delete payload.TriggerRouter; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: payload.SmartRouter?.rules || [] }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: payload.SmartRouter?.enabled !== undefined ? Boolean(payload.SmartRouter?.enabled) : Boolean(preset.governanceEnabled), sticky:{ ...((payload.SmartRouter && payload.SmartRouter.sticky) || {}), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.enabled), alignment:{ ...(((payload.SmartRouter && payload.SmartRouter.sticky && payload.SmartRouter.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.SmartRouter?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.SmartRouter && payload.SmartRouter.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.SmartRouter?.semantic?.enabled), mode:(payload.SmartRouter?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.SmartRouter?.semantic?.classifier_model || '') } }; payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderAnomalies(anomalies){ if(!anomalies || !anomalies.length){ anomalyList.innerHTML='<div class="alert info"><strong>No active alerts</strong><div class="muted">\u5F53\u524D\u7A97\u53E3\u672A\u53D1\u73B0\u660E\u663E\u6CBB\u7406\u5F02\u5E38</div></div>'; return; } anomalyList.innerHTML=anomalies.map(item=>'<div class="alert '+esc(item.severity || 'info')+'"><strong>'+esc(item.type)+'</strong><div>'+esc(item.message)+'</div></div>').join('');}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')}</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); renderMetrics(metricsData.metrics || {}); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || []); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderTrendTable(metricsData || {}); const traces=data.traces || []; if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftHeroBtn').addEventListener('click',loadConfigDraft);document.getElementById('previewConfigDraftHeroBtn').addEventListener('click',previewConfigDraft);document.getElementById('refreshStatusHeroBtn').addEventListener('click',loadServiceStatus);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadServiceStatus();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`;
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>`;
3565
4474
  }
3566
4475
  var init_workbench = __esm({
3567
4476
  "src/ui/workbench.ts"() {
3568
4477
  "use strict";
3569
4478
  init_provider_presets();
4479
+ init_runtime_role_guidance();
3570
4480
  }
3571
4481
  });
3572
4482
 
@@ -3581,7 +4491,8 @@ function toCompiledRegistryView(config) {
3581
4491
  transformer: provider.transformer,
3582
4492
  has_api_key: Boolean(provider.api_key)
3583
4493
  })),
3584
- modelMap: registry.modelMap
4494
+ modelMap: registry.modelMap,
4495
+ modelPools: registry.modelPools
3585
4496
  };
3586
4497
  }
3587
4498
  function collectModelReferences(config) {
@@ -3621,23 +4532,120 @@ function buildServiceInfo(rawConfig) {
3621
4532
  const remoteService = runtime.remote_service ?? {};
3622
4533
  const registration = normalized.Registration ?? {};
3623
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
+ });
3624
4591
  return {
3625
4592
  service: SERVICE_NAME,
3626
4593
  ready: true,
3627
- host: rawConfig?.HOST ?? normalized.HOST,
3628
- port: rawConfig?.PORT ?? normalized.PORT,
4594
+ host,
4595
+ port,
3629
4596
  runtimeMode,
3630
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
+ },
3631
4605
  remoteEnabled: Boolean(remoteService.enabled),
3632
4606
  remoteService: {
3633
4607
  enabled: Boolean(remoteService.enabled),
3634
4608
  baseUrl: remoteService.base_url || "",
3635
4609
  authTokenConfigured: Boolean(remoteService.auth_token)
3636
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
+ },
3637
4630
  registration: {
3638
4631
  enabled: Boolean(registration.enabled),
3639
4632
  models: Array.isArray(registration.models) ? registration.models.length : 0,
3640
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
3641
4649
  }
3642
4650
  };
3643
4651
  }
@@ -3674,9 +4682,13 @@ function summarizeCompiledModels(normalized) {
3674
4682
  const compiled = toCompiledRegistryView(normalized);
3675
4683
  const capabilityWarnings = collectCapabilityWarnings(normalized);
3676
4684
  const modelEntries = Object.values(compiled.modelMap ?? {});
4685
+ const modelPoolEntries = Object.values(compiled.modelPools ?? {});
4686
+ const modelPoolEndpoints = modelPoolEntries.flatMap((pool) => pool.endpoints ?? []);
3677
4687
  return {
3678
4688
  providerCount: compiled.providers.length,
3679
4689
  modelCount: modelEntries.length,
4690
+ modelPoolCount: modelPoolEntries.length,
4691
+ modelPoolEndpointCount: modelPoolEndpoints.length,
3680
4692
  capabilities: {
3681
4693
  reasoning: modelEntries.filter((item) => item.capabilities?.thinking?.supported !== false).length,
3682
4694
  tools: modelEntries.filter((item) => item.capabilities?.tools !== false).length,
@@ -3689,6 +4701,7 @@ function summarizeCompiledModels(normalized) {
3689
4701
  }
3690
4702
  function summarizeGovernanceAlerts(report) {
3691
4703
  return {
4704
+ healthStatus: report.health?.status ?? "idle",
3692
4705
  totalTraces: report.metrics.totalTraces,
3693
4706
  alertCount: report.anomalies.length,
3694
4707
  warnCount: report.anomalies.filter((item) => item.severity === "warn").length,
@@ -3817,6 +4830,10 @@ function buildPersistedConfig(rawConfig, normalizedConfig) {
3817
4830
  if (registrationProjection && typeof registrationProjection === "object" && Object.keys(registrationProjection).length > 0) {
3818
4831
  persisted.Registration = registrationProjection;
3819
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
+ }
3820
4837
  if (rawConfig?.TriggerRouter) {
3821
4838
  smartRouterProjection = mergeSmartRouterProjection(smartRouterProjection, {
3822
4839
  ...rawConfig.TriggerRouter.enabled !== void 0 ? { enabled: runtimeSmartRouter.enabled } : {},
@@ -3856,11 +4873,57 @@ function buildPersistedConfig(rawConfig, normalizedConfig) {
3856
4873
  }
3857
4874
  return persisted;
3858
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
+ }
3859
4921
  function buildDraftConfigView(config) {
3860
4922
  const normalizedConfig = normalizeAndValidateConfig(config ?? {}).config;
3861
4923
  const runtimeSmartRouterConfig = deriveRuntimeSmartRouterConfig(normalizedConfig);
3862
4924
  const draftConfig = {
3863
4925
  ...normalizedConfig,
4926
+ Models: Array.isArray(normalizedConfig.Models) ? normalizedConfig.Models.map((item) => toExternalModelConfig(item)) : normalizedConfig.Models,
3864
4927
  SmartRouter: runtimeSmartRouterConfig
3865
4928
  };
3866
4929
  delete draftConfig.TriggerRouter;
@@ -3956,8 +5019,10 @@ var init_server = __esm({
3956
5019
  init_service_health();
3957
5020
  init_governance();
3958
5021
  init_compile();
5022
+ init_schema();
3959
5023
  init_validation_contract();
3960
5024
  init_workbench();
5025
+ init_api_keys();
3961
5026
  createServer = (config) => {
3962
5027
  const server = new import_llms.default(config);
3963
5028
  const configuredThresholds = config.initialConfig?.Governance?.observability?.anomaly_thresholds ?? {};
@@ -4043,6 +5108,7 @@ var init_server = __esm({
4043
5108
  success: true,
4044
5109
  providers: previewCompiled.providers,
4045
5110
  modelMap: previewCompiled.modelMap,
5111
+ modelPools: previewCompiled.modelPools,
4046
5112
  normalizedConfig: buildDraftConfigView(result.config),
4047
5113
  diff: diffCompiledRegistry(currentCompiled, previewCompiled),
4048
5114
  referenceImpact: analyzeModelReferenceImpact(result.config, previewCompiled),
@@ -4053,20 +5119,158 @@ var init_server = __esm({
4053
5119
  capabilityWarnings: previewCapabilityWarnings
4054
5120
  })
4055
5121
  };
4056
- });
4057
- server.app.get("/api/health", async () => {
5122
+ });
5123
+ server.app.get("/api/health", async () => {
5124
+ return {
5125
+ service: SERVICE_NAME,
5126
+ ready: true,
5127
+ port: config.initialConfig?.PORT
5128
+ };
5129
+ });
5130
+ server.app.get("/api/service-info", async () => {
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);
5139
+ });
5140
+ server.app.get("/api/registration", async () => {
5141
+ return buildRegistrationInfo(config.initialConfig ?? {});
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));
4058
5269
  return {
4059
- service: SERVICE_NAME,
4060
- ready: true,
4061
- port: config.initialConfig?.PORT
5270
+ success: true,
5271
+ key: sanitizeManagedApiKey(nextKeys[keyIndex])
4062
5272
  };
4063
5273
  });
4064
- server.app.get("/api/service-info", async () => {
4065
- return buildServiceInfo(config.initialConfig ?? {});
4066
- });
4067
- server.app.get("/api/registration", async () => {
4068
- return buildRegistrationInfo(config.initialConfig ?? {});
4069
- });
4070
5274
  server.app.get("/api/remote-status", async (req) => {
4071
5275
  const normalizedResult = normalizeAndValidateConfig(config.initialConfig ?? {});
4072
5276
  const normalized = normalizedResult.config;
@@ -4105,6 +5309,27 @@ var init_server = __esm({
4105
5309
  ...getGovernanceMetricsReport(readGovernanceMetricsQuery(req.query))
4106
5310
  };
4107
5311
  });
5312
+ server.app.get("/api/governance/health", async (req) => {
5313
+ const report = getGovernanceMetricsReport(readGovernanceMetricsQuery(req.query));
5314
+ return {
5315
+ health: report.health ?? buildGovernanceHealthSummary({
5316
+ metrics: report.metrics,
5317
+ anomalies: report.anomalies,
5318
+ topRouteReasons: report.topRouteReasons,
5319
+ topFinalModels: report.topFinalModels,
5320
+ outcome: report.outcome
5321
+ }),
5322
+ metrics: report.metrics,
5323
+ outcome: report.outcome,
5324
+ anomalies: report.anomalies,
5325
+ topRouteReasons: report.topRouteReasons,
5326
+ topFinalModels: report.topFinalModels,
5327
+ topSemanticIntents: report.topSemanticIntents,
5328
+ windowMs: report.windowMs,
5329
+ windowStart: report.windowStart,
5330
+ windowEnd: report.windowEnd
5331
+ };
5332
+ });
4108
5333
  server.app.get("/api/governance/metrics/export", async (req, reply) => {
4109
5334
  const format = String(req.query?.format || "json").toLowerCase() === "csv" ? "csv" : "json";
4110
5335
  const report = getGovernanceMetricsReport(readGovernanceMetricsQuery(req.query));
@@ -4285,8 +5510,8 @@ var init_server = __esm({
4285
5510
  reply.send({ success: true, message: "Service restart initiated" });
4286
5511
  setTimeout(() => {
4287
5512
  const { spawn: spawn3 } = require("child_process");
4288
- const { join: join8 } = require("path");
4289
- const cliPath = join8(__dirname, "cli.js");
5513
+ const { join: join9 } = require("path");
5514
+ const cliPath = join9(__dirname, "cli.js");
4290
5515
  const currentPort = config.initialConfig?.PORT;
4291
5516
  const restartArgs = [cliPath, "start", "--daemon"];
4292
5517
  if (currentPort) {
@@ -4317,6 +5542,7 @@ var init_router = __esm({
4317
5542
  init_cache();
4318
5543
  init_log();
4319
5544
  init_compile();
5545
+ init_governance();
4320
5546
  enc = (0, import_tiktoken.get_encoding)("cl100k_base");
4321
5547
  calculateTokenCount = (messages, system, tools) => {
4322
5548
  let tokenCount = 0;
@@ -4469,6 +5695,17 @@ var init_router = __esm({
4469
5695
  }
4470
5696
  req.body.model = model ?? req.body.model;
4471
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
+ }
4472
5709
  req.tokenCount = tokenCount;
4473
5710
  } catch (error) {
4474
5711
  logError("Error in router middleware:", error.message);
@@ -4480,31 +5717,166 @@ var init_router = __esm({
4480
5717
  });
4481
5718
 
4482
5719
  // src/middleware/auth.ts
4483
- 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 = {}) {
4484
5760
  return (req, reply, done) => {
4485
- 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
+ }
4486
5837
  done();
4487
- return;
4488
- }
4489
- const authHeader = req.headers.authorization;
4490
- const xApiKey = req.headers["x-api-key"];
4491
- let providedKey;
4492
- if (authHeader?.startsWith("Bearer ")) {
4493
- providedKey = authHeader.slice(7);
4494
- } else if (xApiKey) {
4495
- providedKey = xApiKey;
4496
- }
4497
- if (!providedKey || providedKey !== config.APIKEY) {
4498
- reply.code(401).send({ error: "Unauthorized" });
4499
- done(new Error("Unauthorized"));
4500
- return;
4501
- }
4502
- done();
5838
+ }).catch((error) => {
5839
+ done(error instanceof Error ? error : new Error(String(error)));
5840
+ });
4503
5841
  };
4504
5842
  }
4505
5843
  var init_auth = __esm({
4506
5844
  "src/middleware/auth.ts"() {
4507
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();
4508
5880
  }
4509
5881
  });
4510
5882
 
@@ -4563,12 +5935,12 @@ function savePid(pid, port) {
4563
5935
  port: port ?? DEFAULT_CONFIG2.PORT,
4564
5936
  startTime: (/* @__PURE__ */ new Date()).toISOString()
4565
5937
  };
4566
- (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");
4567
5939
  }
4568
5940
  function readServiceInfo() {
4569
- if (!(0, import_fs4.existsSync)(PID_FILE)) return null;
5941
+ if (!(0, import_fs5.existsSync)(PID_FILE)) return null;
4570
5942
  try {
4571
- const content = (0, import_fs4.readFileSync)(PID_FILE, "utf-8").trim();
5943
+ const content = (0, import_fs5.readFileSync)(PID_FILE, "utf-8").trim();
4572
5944
  if (/^\d+$/.test(content)) {
4573
5945
  return { pid: parseInt(content, 10), port: DEFAULT_CONFIG2.PORT, startTime: "" };
4574
5946
  }
@@ -4578,19 +5950,19 @@ function readServiceInfo() {
4578
5950
  }
4579
5951
  }
4580
5952
  function cleanupPidFile() {
4581
- if ((0, import_fs4.existsSync)(PID_FILE)) {
5953
+ if ((0, import_fs5.existsSync)(PID_FILE)) {
4582
5954
  try {
4583
- (0, import_fs4.unlinkSync)(PID_FILE);
5955
+ (0, import_fs5.unlinkSync)(PID_FILE);
4584
5956
  } catch (error) {
4585
5957
  logError("Failed to cleanup PID file:", error);
4586
5958
  }
4587
5959
  }
4588
5960
  }
4589
- var import_fs4, import_child_process;
5961
+ var import_fs5, import_child_process;
4590
5962
  var init_processCheck = __esm({
4591
5963
  "src/utils/processCheck.ts"() {
4592
5964
  "use strict";
4593
- import_fs4 = require("fs");
5965
+ import_fs5 = require("fs");
4594
5966
  import_child_process = require("child_process");
4595
5967
  init_constants();
4596
5968
  init_log();
@@ -6428,8 +7800,8 @@ function cloneRequestBody(value) {
6428
7800
  }
6429
7801
  async function initializeClaudeConfig() {
6430
7802
  const homeDir = (0, import_os2.homedir)();
6431
- const configPath = (0, import_path5.join)(homeDir, ".claude.json");
6432
- if (!(0, import_fs5.existsSync)(configPath)) {
7803
+ const configPath = (0, import_path6.join)(homeDir, ".claude.json");
7804
+ if (!(0, import_fs6.existsSync)(configPath)) {
6433
7805
  log(`Creating ${configPath} for Claude Code compatibility (onboarding bypass)`);
6434
7806
  const userID = Array.from(
6435
7807
  { length: 64 },
@@ -6443,7 +7815,7 @@ async function initializeClaudeConfig() {
6443
7815
  lastOnboardingVersion: "1.0.17",
6444
7816
  projects: {}
6445
7817
  };
6446
- await (0, import_promises2.writeFile)(configPath, JSON.stringify(configContent, null, 2));
7818
+ await (0, import_promises3.writeFile)(configPath, JSON.stringify(configContent, null, 2));
6447
7819
  }
6448
7820
  }
6449
7821
  function buildServerInitialConfig(config, registry, host, servicePort) {
@@ -6452,7 +7824,7 @@ function buildServerInitialConfig(config, registry, host, servicePort) {
6452
7824
  providers: registry.providers,
6453
7825
  HOST: host,
6454
7826
  PORT: servicePort,
6455
- LOG_FILE: (0, import_path5.join)(
7827
+ LOG_FILE: (0, import_path6.join)(
6456
7828
  (0, import_os2.homedir)(),
6457
7829
  ".claude-trigger-router",
6458
7830
  "claude-trigger-router.log"
@@ -6466,11 +7838,19 @@ async function run(options = {}) {
6466
7838
  }
6467
7839
  await initDir();
6468
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
+ }
6469
7847
  configureLogging(config);
6470
7848
  let HOST = config.HOST || "127.0.0.1";
6471
- if (config.HOST && !config.APIKEY) {
7849
+ const managedKeySummary = managedApiKeySummary(config);
7850
+ const hasPublicAuth = Boolean(config.APIKEY || managedKeySummary.active > 0);
7851
+ if (config.HOST && !hasPublicAuth) {
6472
7852
  HOST = "127.0.0.1";
6473
- 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.");
6474
7854
  }
6475
7855
  const port = options.port ?? config.PORT ?? DEFAULT_CONFIG.PORT;
6476
7856
  savePid(process.pid, port);
@@ -6513,13 +7893,34 @@ async function run(options = {}) {
6513
7893
  initialConfig: buildServerInitialConfig(config, registry, HOST, servicePort),
6514
7894
  logger: loggerConfig
6515
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
+ });
6516
7917
  server.addHook("preHandler", async (req, reply) => {
6517
7918
  return new Promise((resolve, reject) => {
6518
7919
  const done = (err) => {
6519
7920
  if (err) reject(err);
6520
7921
  else resolve();
6521
7922
  };
6522
- apiKeyAuth(config)(req, reply, done);
7923
+ authMiddleware(req, reply, done);
6523
7924
  });
6524
7925
  });
6525
7926
  triggerRouter.init(config);
@@ -6798,18 +8199,20 @@ async function run(options = {}) {
6798
8199
  });
6799
8200
  await server.start();
6800
8201
  }
6801
- 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;
6802
8203
  var init_index = __esm({
6803
8204
  "src/index.ts"() {
6804
8205
  "use strict";
6805
- import_fs5 = require("fs");
6806
- import_promises2 = require("fs/promises");
8206
+ import_fs6 = require("fs");
8207
+ import_promises3 = require("fs/promises");
6807
8208
  import_os2 = require("os");
6808
- import_path5 = require("path");
8209
+ import_path6 = require("path");
6809
8210
  init_utils();
6810
8211
  init_server();
6811
8212
  init_router();
6812
8213
  init_auth();
8214
+ init_api_keys();
8215
+ init_quota_persistence();
6813
8216
  init_processCheck();
6814
8217
  init_constants();
6815
8218
  init_log();
@@ -7096,11 +8499,8 @@ function migrateLegacyConfig(input3) {
7096
8499
  (provider, providerIndex) => (provider.models.length ? provider.models : [""]).map((model) => ({
7097
8500
  candidateId: toModelId(provider.name, model, providerIndex),
7098
8501
  api: provider.api_base_url ? normalizeApiEndpoint(provider.api_base_url, inferProtocolFromApiBaseUrl(provider.api_base_url, model)) : void 0,
7099
- api_base_url: provider.api_base_url ? normalizeApiEndpoint(provider.api_base_url, inferProtocolFromApiBaseUrl(provider.api_base_url, model)) : void 0,
7100
8502
  key: provider.api_key,
7101
- api_key: provider.api_key,
7102
8503
  interface: inferProtocolFromApiBaseUrl(provider.api_base_url, model),
7103
- protocol: inferProtocolFromApiBaseUrl(provider.api_base_url, model),
7104
8504
  model,
7105
8505
  providerName: provider.name
7106
8506
  })).filter((item) => item.model)
@@ -7115,11 +8515,8 @@ function migrateLegacyConfig(input3) {
7115
8515
  return {
7116
8516
  id: finalId,
7117
8517
  api: entry.api,
7118
- api_base_url: entry.api_base_url,
7119
8518
  key: entry.key,
7120
- api_key: entry.api_key,
7121
8519
  interface: entry.interface,
7122
- protocol: entry.protocol,
7123
8520
  model: entry.model
7124
8521
  };
7125
8522
  });
@@ -7246,17 +8643,14 @@ function buildMinimalConfig(input3) {
7246
8643
  const modelDraft = {
7247
8644
  id: p.model_id?.trim() || p.name,
7248
8645
  key: p.api_key,
7249
- api_key: p.api_key,
7250
8646
  model: p.models[0] ?? "",
7251
- interface: p.interface ?? preset?.interface ?? "openai",
7252
- protocol: p.interface ?? preset?.protocol ?? "openai"
8647
+ interface: p.interface ?? preset?.interface ?? "openai"
7253
8648
  };
7254
8649
  const explicitApiBaseUrl = p.api_base_url?.trim();
7255
8650
  const presetApiBaseUrl = preset?.api_base_url?.trim();
7256
8651
  const apiBaseUrl = explicitApiBaseUrl || presetApiBaseUrl;
7257
8652
  if (apiBaseUrl) {
7258
8653
  modelDraft.api = apiBaseUrl;
7259
- modelDraft.api_base_url = apiBaseUrl;
7260
8654
  }
7261
8655
  return modelDraft;
7262
8656
  });
@@ -7282,6 +8676,31 @@ function buildMinimalConfig(input3) {
7282
8676
  Router: defaultModel ? { default: defaultModel } : {}
7283
8677
  };
7284
8678
  }
8679
+ function buildRemoteServiceConfig(input3) {
8680
+ const baseUrl = input3.baseUrl.trim().replace(/\/+$/, "");
8681
+ return {
8682
+ Runtime: {
8683
+ mode: "local",
8684
+ remote_service: {
8685
+ enabled: true,
8686
+ base_url: baseUrl,
8687
+ auth_token: input3.authToken?.trim() || "${CTR_REMOTE_AUTH_TOKEN}"
8688
+ }
8689
+ },
8690
+ Router: {}
8691
+ };
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
+ }
7285
8704
  function buildUsableMinimalTemplateConfig() {
7286
8705
  const openRouterPreset = getProviderPreset("openrouter");
7287
8706
  const modelId = openRouterPreset?.suggested_id ?? "sonnet";
@@ -7404,6 +8823,10 @@ function decideSetupBranch(input3) {
7404
8823
  ensureNoLegacyAction(legacyConfigAction);
7405
8824
  return { kind: "reuse_current" };
7406
8825
  }
8826
+ if (currentConfigAction === "repair") {
8827
+ ensureNoLegacyAction(legacyConfigAction);
8828
+ return { kind: "repair_current" };
8829
+ }
7407
8830
  if (currentConfigAction === "overwrite" || currentConfigAction === "fresh") {
7408
8831
  return ensureLegacyFlow(detection, legacyConfigAction);
7409
8832
  }
@@ -7468,6 +8891,16 @@ function getMigratedModelCount(draft) {
7468
8891
  }
7469
8892
  return 0;
7470
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
+ }
7471
8904
  async function runSetup(deps) {
7472
8905
  const detection = await deps.detectSetupEnvironment();
7473
8906
  const currentConfigAction = await deps.chooseCurrentConfigAction({
@@ -7491,9 +8924,10 @@ async function runSetup(deps) {
7491
8924
  return;
7492
8925
  }
7493
8926
  let configChanged = false;
8927
+ let finalDraft;
7494
8928
  if (branch.kind === "repair_current") {
7495
- if (detection.currentConfig.kind !== "invalid") {
7496
- throw new Error("repair_current requires invalid current config");
8929
+ if (detection.currentConfig.kind !== "invalid" && detection.currentConfig.kind !== "valid") {
8930
+ throw new Error("repair_current requires current config");
7497
8931
  }
7498
8932
  const repairPlan = deps.mapConfigErrorsToRepairFields([
7499
8933
  ...detection.currentConfig.errors,
@@ -7510,6 +8944,7 @@ async function runSetup(deps) {
7510
8944
  draft: baseDraft,
7511
8945
  fields: repairPlan.fields
7512
8946
  });
8947
+ finalDraft = completedDraft;
7513
8948
  const persistResult = await deps.persistConfig({
7514
8949
  config: completedDraft,
7515
8950
  currentConfigPath: detection.currentConfig.path,
@@ -7518,16 +8953,28 @@ async function runSetup(deps) {
7518
8953
  configChanged = persistResult.configChanged;
7519
8954
  }
7520
8955
  if (branch.kind === "reuse_current") {
7521
- 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({
7522
8965
  configChanged: false,
7523
8966
  detectedService: detection.detectedService,
7524
8967
  reloadSupported: deps.reloadSupported
7525
8968
  });
7526
- await deps.enterClaudeCode();
8969
+ await deps.enterClaudeCode({
8970
+ config: detection.currentConfig.config,
8971
+ service
8972
+ });
7527
8973
  return;
7528
8974
  }
7529
8975
  if (branch.kind === "unparseable_current") {
7530
8976
  const draft = await deps.buildFreshConfig();
8977
+ finalDraft = draft;
7531
8978
  const persistResult = await deps.persistConfig({
7532
8979
  config: draft,
7533
8980
  currentConfigPath: detection.currentConfig.path,
@@ -7537,6 +8984,7 @@ async function runSetup(deps) {
7537
8984
  }
7538
8985
  if (branch.kind === "fresh_init") {
7539
8986
  const draft = await deps.buildFreshConfig();
8987
+ finalDraft = draft;
7540
8988
  const persistResult = await deps.persistConfig({
7541
8989
  config: draft,
7542
8990
  currentConfigPath: getTargetConfigPath(detection),
@@ -7559,27 +9007,38 @@ async function runSetup(deps) {
7559
9007
  if (migrated.skippedFields.length > 0) {
7560
9008
  deps.io.info(`\u4EE5\u4E0B\u65E7\u5B57\u6BB5\u672A\u81EA\u52A8\u8FC1\u79FB\uFF1A${migrated.skippedFields.join(", ")}`);
7561
9009
  }
7562
- let finalDraft = migrated.draft;
9010
+ let migratedFinalDraft = migrated.draft;
7563
9011
  if (migrated.needsCompletion) {
7564
- finalDraft = await deps.completeDraft({
9012
+ migratedFinalDraft = await deps.completeDraft({
7565
9013
  draft: migrated.draft,
7566
9014
  fields: migrated.missingFields
7567
9015
  });
7568
9016
  }
9017
+ finalDraft = migratedFinalDraft;
7569
9018
  const persistResult = await deps.persistConfig({
7570
- config: finalDraft,
9019
+ config: migratedFinalDraft,
7571
9020
  currentConfigPath: getTargetConfigPath(detection),
7572
9021
  hasExistingConfig: detection.currentConfig.kind !== "missing"
7573
9022
  });
7574
9023
  configChanged = persistResult.configChanged;
7575
9024
  }
7576
9025
  if (branch.kind === "fresh_init" || branch.kind === "repair_current" || branch.kind === "unparseable_current" || branch.kind === "migrate_legacy") {
7577
- await deps.ensureServiceReady({
9026
+ if (isRouterServiceDeploymentDraft(finalDraft)) {
9027
+ printRouterServiceDeploymentNextSteps(deps.io, finalDraft);
9028
+ return;
9029
+ }
9030
+ const service = await deps.ensureServiceReady({
7578
9031
  configChanged,
7579
9032
  detectedService: detection.detectedService,
7580
9033
  reloadSupported: deps.reloadSupported
7581
9034
  });
7582
- 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
+ });
7583
9042
  return;
7584
9043
  }
7585
9044
  }
@@ -7594,7 +9053,7 @@ var init_setup = __esm({
7594
9053
  // src/setup/index.ts
7595
9054
  function createConsoleIO() {
7596
9055
  if (process.env.CTR_SETUP_FORCE_SCRIPTED_INPUT === "1") {
7597
- const scriptedInput = (0, import_fs6.readFileSync)(0, "utf-8");
9056
+ const scriptedInput = (0, import_fs7.readFileSync)(0, "utf-8");
7598
9057
  const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter((item) => item.length > 0);
7599
9058
  let cursor = 0;
7600
9059
  const nextAnswer = async () => answers[cursor++] ?? "";
@@ -7683,7 +9142,7 @@ function createConsoleIO() {
7683
9142
  }
7684
9143
  };
7685
9144
  }
7686
- 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 });
7687
9146
  const ask = async (message) => {
7688
9147
  const answer = await rl.question(message);
7689
9148
  return answer.trim();
@@ -7724,7 +9183,7 @@ function createConsoleIO() {
7724
9183
  };
7725
9184
  }
7726
9185
  function readStructuredConfigFile(filePath) {
7727
- const content = (0, import_fs6.readFileSync)(filePath, "utf-8");
9186
+ const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
7728
9187
  if (filePath.endsWith(".json")) {
7729
9188
  return JSON.parse(content);
7730
9189
  }
@@ -7732,7 +9191,7 @@ function readStructuredConfigFile(filePath) {
7732
9191
  }
7733
9192
  function getCurrentRuntimeFields() {
7734
9193
  const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
7735
- const currentPath = candidates.find((filePath) => (0, import_fs6.existsSync)(filePath));
9194
+ const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
7736
9195
  if (!currentPath) {
7737
9196
  return {};
7738
9197
  }
@@ -7754,7 +9213,7 @@ function getCurrentRuntimeFields() {
7754
9213
  }
7755
9214
  function getConfiguredPortFromCurrentFiles() {
7756
9215
  const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
7757
- const currentPath = candidates.find((filePath) => (0, import_fs6.existsSync)(filePath));
9216
+ const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
7758
9217
  if (!currentPath) {
7759
9218
  return DEFAULT_CONFIG2.PORT;
7760
9219
  }
@@ -7788,7 +9247,7 @@ async function getAvailablePort() {
7788
9247
  }
7789
9248
  }
7790
9249
  function readLegacyConfigFile(filePath) {
7791
- const content = (0, import_fs6.readFileSync)(filePath, "utf-8");
9250
+ const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
7792
9251
  if (filePath.endsWith(".json")) {
7793
9252
  return import_json52.default.parse(content);
7794
9253
  }
@@ -7796,13 +9255,13 @@ function readLegacyConfigFile(filePath) {
7796
9255
  }
7797
9256
  async function readLegacyConfig(deps = {}) {
7798
9257
  const baseHomeDir = deps.homeDir || (0, import_os3.homedir)();
7799
- const exists = deps.exists || import_fs6.existsSync;
9258
+ const exists = deps.exists || import_fs7.existsSync;
7800
9259
  const readConfig = deps.readConfig || readLegacyConfigFile;
7801
9260
  const overridePath = process.env.CTR_SETUP_LEGACY_CONFIG_PATH;
7802
9261
  const candidatePaths = overridePath ? [overridePath] : [
7803
- (0, import_path6.join)(baseHomeDir, ".ccr", "config.yaml"),
7804
- (0, import_path6.join)(baseHomeDir, ".claude-code-router", "config.yaml"),
7805
- (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")
7806
9265
  ];
7807
9266
  const legacyPath = candidatePaths.find((filePath) => exists(filePath));
7808
9267
  if (!legacyPath) {
@@ -7824,7 +9283,7 @@ async function readLegacyConfig(deps = {}) {
7824
9283
  }
7825
9284
  async function readCurrentConfig() {
7826
9285
  const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
7827
- const currentPath = candidates.find((filePath) => (0, import_fs6.existsSync)(filePath));
9286
+ const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
7828
9287
  if (!currentPath) {
7829
9288
  return { kind: "missing" };
7830
9289
  }
@@ -7896,6 +9355,9 @@ function mapValidCurrentConfigChoice(choice) {
7896
9355
  if (choice === "reuse" || choice === "\u76F4\u63A5\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\uFF08\u63A8\u8350\uFF09") {
7897
9356
  return "reuse";
7898
9357
  }
9358
+ if (choice === "repair" || choice === "\u5FEB\u901F\u4FEE\u6B63\u914D\u7F6E\u63D0\u793A") {
9359
+ return "repair";
9360
+ }
7899
9361
  if (choice === "overwrite" || choice === "\u68C0\u67E5\u5E76\u8C03\u6574\u5F53\u524D\u914D\u7F6E") {
7900
9362
  return "overwrite";
7901
9363
  }
@@ -7930,38 +9392,120 @@ function applyCapabilityMetadata(model, metadata) {
7930
9392
  delete model.metadata;
7931
9393
  return model;
7932
9394
  }
7933
- model.metadata = {
7934
- ...model.metadata ?? {},
7935
- ...metadata
7936
- };
9395
+ model.metadata = { ...metadata };
7937
9396
  if (!Object.keys(model.metadata || {}).length) {
7938
9397
  delete model.metadata;
7939
9398
  }
7940
9399
  return model;
7941
9400
  }
9401
+ function ensureCapabilityMetadata(model) {
9402
+ if (!model.metadata || typeof model.metadata !== "object") {
9403
+ model.metadata = {};
9404
+ }
9405
+ return model.metadata;
9406
+ }
9407
+ function removeCapabilityMetadataField(model, field) {
9408
+ if (!model.metadata || typeof model.metadata !== "object") {
9409
+ return;
9410
+ }
9411
+ delete model.metadata[field];
9412
+ if (!Object.keys(model.metadata).length) {
9413
+ delete model.metadata;
9414
+ }
9415
+ }
7942
9416
  async function promptCapabilityMetadata(io, currentMetadata) {
7943
9417
  const vendorHint = await io.input("Vendor hint\uFF08\u53EF\u9009\uFF09", String(currentMetadata?.vendor_hint ?? ""));
7944
9418
  const reasoningChoice = await io.choose("Reasoning support", ["\u9ED8\u8BA4", "\u652F\u6301", "\u7981\u7528"]);
7945
9419
  const toolChoice = await io.choose("Tool support", ["\u9ED8\u8BA4", "\u652F\u6301", "\u7981\u7528"]);
7946
9420
  const imageChoice = await io.choose("Image support", ["\u9ED8\u8BA4", "\u652F\u6301", "\u7981\u7528"]);
7947
- const nextMetadata = {};
9421
+ const nextMetadata = {
9422
+ ...currentMetadata ?? {}
9423
+ };
7948
9424
  if (vendorHint.trim()) {
7949
9425
  nextMetadata.vendor_hint = vendorHint.trim();
9426
+ } else {
9427
+ delete nextMetadata.vendor_hint;
7950
9428
  }
7951
9429
  const reasoning = toCapabilityBoolean(reasoningChoice);
7952
9430
  if (reasoning !== void 0) {
7953
9431
  nextMetadata.supports_reasoning = reasoning;
9432
+ } else {
9433
+ delete nextMetadata.supports_reasoning;
7954
9434
  }
7955
9435
  const tools = toCapabilityBoolean(toolChoice);
7956
9436
  if (tools !== void 0) {
7957
9437
  nextMetadata.supports_tools = tools;
9438
+ } else {
9439
+ delete nextMetadata.supports_tools;
7958
9440
  }
7959
9441
  const images = toCapabilityBoolean(imageChoice);
7960
9442
  if (images !== void 0) {
7961
9443
  nextMetadata.supports_images = images;
9444
+ } else {
9445
+ delete nextMetadata.supports_images;
7962
9446
  }
7963
9447
  return nextMetadata;
7964
9448
  }
9449
+ async function promptCapabilityWarningFixesForDraft(draft, io) {
9450
+ const report = collectCapabilityWarnings(draft);
9451
+ if (!report.entries.length || !draft.Models?.length) {
9452
+ return false;
9453
+ }
9454
+ for (const entry of report.entries) {
9455
+ const stillActive = collectCapabilityWarnings(draft).entries.some(
9456
+ (item) => item.path === entry.path && item.modelId === entry.modelId && item.code === entry.code
9457
+ );
9458
+ if (!stillActive) {
9459
+ continue;
9460
+ }
9461
+ const model = draft.Models.find((item) => item.id === entry.modelId);
9462
+ if (!model) {
9463
+ continue;
9464
+ }
9465
+ io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${entry.message}`);
9466
+ if (entry.code === "thinking_ignored") {
9467
+ const choice = await io.choose(`\u5982\u4F55\u5904\u7406\u6A21\u578B ${entry.modelId} \u7684 thinking warning\uFF1F`, [
9468
+ "\u79FB\u9664 thinking\uFF08\u63A8\u8350\uFF09",
9469
+ "\u6807\u8BB0\u652F\u6301 reasoning",
9470
+ "\u4FDD\u6301\u5F53\u524D\u914D\u7F6E"
9471
+ ]);
9472
+ if (choice === "\u79FB\u9664 thinking\uFF08\u63A8\u8350\uFF09") {
9473
+ delete model.thinking;
9474
+ } else if (choice === "\u6807\u8BB0\u652F\u6301 reasoning") {
9475
+ ensureCapabilityMetadata(model).supports_reasoning = true;
9476
+ }
9477
+ continue;
9478
+ }
9479
+ if (entry.code === "tools_text_fallback") {
9480
+ const choice = await io.choose(`\u5982\u4F55\u5904\u7406\u6A21\u578B ${entry.modelId} \u7684 tool fallback\uFF1F`, [
9481
+ "\u6062\u590D\u9ED8\u8BA4\u5DE5\u5177\u652F\u6301\uFF08\u63A8\u8350\uFF09",
9482
+ "\u63A5\u53D7\u6587\u672C\u964D\u7EA7",
9483
+ "\u7F16\u8F91 capability"
9484
+ ]);
9485
+ if (choice === "\u6062\u590D\u9ED8\u8BA4\u5DE5\u5177\u652F\u6301\uFF08\u63A8\u8350\uFF09") {
9486
+ removeCapabilityMetadataField(model, "supports_tools");
9487
+ } else if (choice === "\u7F16\u8F91 capability") {
9488
+ const metadata = await promptCapabilityMetadata(io, model.metadata);
9489
+ applyCapabilityMetadata(model, metadata);
9490
+ }
9491
+ continue;
9492
+ }
9493
+ if (entry.code === "images_text_fallback") {
9494
+ const choice = await io.choose(`\u5982\u4F55\u5904\u7406\u6A21\u578B ${entry.modelId} \u7684 image fallback\uFF1F`, [
9495
+ "\u6062\u590D\u9ED8\u8BA4\u56FE\u7247\u652F\u6301\uFF08\u63A8\u8350\uFF09",
9496
+ "\u63A5\u53D7\u6587\u672C\u964D\u7EA7",
9497
+ "\u7F16\u8F91 capability"
9498
+ ]);
9499
+ if (choice === "\u6062\u590D\u9ED8\u8BA4\u56FE\u7247\u652F\u6301\uFF08\u63A8\u8350\uFF09") {
9500
+ removeCapabilityMetadataField(model, "supports_images");
9501
+ } else if (choice === "\u7F16\u8F91 capability") {
9502
+ const metadata = await promptCapabilityMetadata(io, model.metadata);
9503
+ applyCapabilityMetadata(model, metadata);
9504
+ }
9505
+ }
9506
+ }
9507
+ return true;
9508
+ }
7965
9509
  async function promptCapabilityMetadataForDraft(draft, io) {
7966
9510
  if (!draft.Models?.length) {
7967
9511
  return;
@@ -7981,11 +9525,8 @@ function toDraftFromConfig(config) {
7981
9525
  (provider) => (Array.isArray(provider.models) ? provider.models : []).map((model) => ({
7982
9526
  id: `${provider.name}_${String(model).toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "")}`,
7983
9527
  key: provider.api_key ?? "",
7984
- api_key: provider.api_key ?? "",
7985
9528
  api: provider.api_base_url,
7986
- api_base_url: provider.api_base_url,
7987
9529
  interface: provider.api_base_url?.includes("/v1/messages") ? "anthropic" : "openai",
7988
- protocol: provider.api_base_url?.includes("/v1/messages") ? "anthropic" : "openai",
7989
9530
  model
7990
9531
  }))
7991
9532
  ) : [];
@@ -8000,11 +9541,8 @@ function toDraftFromConfig(config) {
8000
9541
  Models: Array.isArray(config?.Models) ? config.Models.map((model) => ({
8001
9542
  id: model.id ?? "",
8002
9543
  key: model.key ?? model.api_key ?? "",
8003
- api_key: model.api_key ?? "",
8004
9544
  api: model.api ?? model.api_base_url,
8005
- api_base_url: model.api_base_url,
8006
9545
  interface: model.interface ?? model.protocol,
8007
- protocol: model.protocol,
8008
9546
  model: model.model ?? "",
8009
9547
  thinking: typeof model.thinking === "string" ? model.thinking : model.thinking ? { ...model.thinking } : void 0,
8010
9548
  metadata: model.metadata ? { ...model.metadata } : void 0
@@ -8120,6 +9658,9 @@ function applyRoutingBootstrap(draft, choice, specializedModelId) {
8120
9658
  };
8121
9659
  return nextDraft;
8122
9660
  }
9661
+ function createSetupBootstrapApiKey() {
9662
+ return `ctr_bootstrap_${(0, import_crypto3.randomBytes)(24).toString("base64url")}`;
9663
+ }
8123
9664
  async function promptModelConnection(io, input3) {
8124
9665
  if (input3.intro) {
8125
9666
  io.info(input3.intro);
@@ -8132,16 +9673,16 @@ async function promptModelConnection(io, input3) {
8132
9673
  if (connectMode === "\u4F7F\u7528\u5E38\u89C1\u63A5\u5165\u6A21\u677F") {
8133
9674
  const presetOptions = listProviderPresetKeys("setup");
8134
9675
  preset = await io.choose("\u9009\u62E9 provider \u9884\u8BBE", presetOptions);
8135
- providerName = await io.input("Provider \u540D\u79F0", preset);
8136
- apiBaseUrl = preset === "custom" ? await io.input("API Base URL") : await io.input("API Base URL\uFF08\u7559\u7A7A\u4F7F\u7528\u9884\u8BBE\uFF09", "");
9676
+ providerName = await io.input("\u63A5\u5165\u540D\u79F0\uFF08\u7528\u4E8E\u9884\u8BBE\u8BC6\u522B\uFF0C\u4E0D\u662F model id\uFF09", preset);
9677
+ apiBaseUrl = preset === "custom" ? await io.input("API URL\uFF08\u5199\u5165 Models[].api\uFF09") : await io.input("API URL\uFF08\u7559\u7A7A\u4F7F\u7528\u9884\u8BBE\uFF0C\u5199\u5165 Models[].api\uFF09", "");
8137
9678
  } else {
8138
- providerName = await io.input("Provider \u540D\u79F0", "provider");
8139
- apiBaseUrl = await io.input("API Base URL");
9679
+ providerName = await io.input("\u63A5\u5165\u540D\u79F0\uFF08\u7528\u4E8E\u9884\u8BBE\u8BC6\u522B\uFF0C\u4E0D\u662F model id\uFF09", "provider");
9680
+ apiBaseUrl = await io.input("API URL\uFF08\u5199\u5165 Models[].api\uFF09");
8140
9681
  }
8141
- const apiKey = await io.input("API Key");
9682
+ const apiKey = await io.input("API Key\uFF08\u5199\u5165 Models[].key\uFF09");
8142
9683
  const presetDefinition = getProviderPreset(preset);
8143
- const model = await io.input("\u4E0A\u6E38\u6A21\u578B\u540D", presetDefinition?.default_model ?? "");
8144
- const interfaceChoice = connectMode === "\u624B\u52A8\u586B\u5199\u63A5\u53E3" ? await io.choose("\u63A5\u53E3\u7C7B\u578B", ["openai", "anthropic"]) : presetDefinition?.interface;
9684
+ const model = await io.input("\u4E0A\u6E38\u6A21\u578B\u540D\uFF08\u5199\u5165 Models[].model\uFF09", presetDefinition?.default_model ?? "");
9685
+ const interfaceChoice = connectMode === "\u624B\u52A8\u586B\u5199\u63A5\u53E3" ? await io.choose("\u63A5\u53E3\u7C7B\u578B\uFF08\u5199\u5165 Models[].interface\uFF09", ["openai", "anthropic"]) : presetDefinition?.interface;
8145
9686
  return {
8146
9687
  name: providerName,
8147
9688
  model_id: modelId,
@@ -8153,9 +9694,31 @@ async function promptModelConnection(io, input3) {
8153
9694
  };
8154
9695
  }
8155
9696
  async function buildFreshConfig(io) {
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", [
9698
+ "\u672C\u5730\u4F7F\u7528\uFF08\u63A8\u8350\uFF09",
9699
+ "\u8FDE\u63A5\u8FDC\u7A0B\u670D\u52A1",
9700
+ "\u90E8\u7F72\u4E3A\u8FDC\u7A0B\u670D\u52A1\u7AEF"
9701
+ ]);
9702
+ if (setupEntryChoice === "\u8FDE\u63A5\u8FDC\u7A0B\u670D\u52A1") {
9703
+ const baseUrl = await io.input("\u8FDC\u7A0B\u670D\u52A1 URL");
9704
+ const authToken = await io.input("\u8FDC\u7A0B\u670D\u52A1 Auth Token\uFF08\u53EF\u9009\uFF09", "${CTR_REMOTE_AUTH_TOKEN}");
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);
9709
+ return buildRemoteServiceConfig({ baseUrl, authToken });
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
+ }
8156
9719
  const primaryModel = await promptModelConnection(io, {
8157
- intro: "\u6211\u4EEC\u5148\u521B\u5EFA\u4E00\u4EFD\u6700\u5C0F\u53EF\u7528\u914D\u7F6E\u3002",
8158
- modelIdPrompt: "\u8FD9\u4E2A\u9ED8\u8BA4\u6A21\u578B\u5728\u672C\u5730\u8981\u53EB\u4EC0\u4E48\u540D\u5B57\uFF1F",
9720
+ intro: `\u6211\u4EEC\u5148\u521B\u5EFA\u4E00\u4EFD\u6700\u5C0F\u53EF\u7528\u914D\u7F6E\u3002${LOCAL_USER_ROLE_GUIDE}`,
9721
+ modelIdPrompt: "\u9ED8\u8BA4\u6A21\u578B\u7684 model id\uFF08Router.default \u4F1A\u5F15\u7528\u5B83\uFF09",
8159
9722
  suggestedModelId: "sonnet"
8160
9723
  });
8161
9724
  let draft = buildMinimalConfig({
@@ -8170,7 +9733,7 @@ async function buildFreshConfig(io) {
8170
9733
  const suggestedSecondModelId = toUniqueSuggestedModelId("reasoner", draft.Models?.map((item) => item.id) ?? []);
8171
9734
  const specializedModel = await promptModelConnection(io, {
8172
9735
  intro: "\u8FD9\u4E2A\u6A21\u578B\u901A\u5E38\u7528\u4E8E\u67B6\u6784\u8BBE\u8BA1\u3001\u4EE3\u7801\u5BA1\u67E5\u6216\u590D\u6742\u63A8\u7406\u7B49\u66F4\u91CD\u7684\u4EFB\u52A1\u3002",
8173
- modelIdPrompt: "\u8FD9\u4E2A\u590D\u6742\u4EFB\u52A1\u6A21\u578B\u5728\u672C\u5730\u8981\u53EB\u4EC0\u4E48\u540D\u5B57\uFF1F",
9736
+ modelIdPrompt: "\u590D\u6742\u4EFB\u52A1\u6A21\u578B\u7684 model id",
8174
9737
  suggestedModelId: suggestedSecondModelId
8175
9738
  });
8176
9739
  draft = appendModelToDraft(draft, specializedModel);
@@ -8195,7 +9758,7 @@ async function completeDraft(input3) {
8195
9758
  if (input3.fields.includes("defaultModel")) {
8196
9759
  const defaultProvider = draft.Models?.[0]?.id ?? draft.Providers?.[0]?.name ?? "provider";
8197
9760
  const defaultModel = draft.Models?.[0]?.model ?? draft.Providers?.[0]?.models?.[0] ?? "";
8198
- const model = await input3.io.input("\u9ED8\u8BA4\u6A21\u578B", defaultModel);
9761
+ const model = await input3.io.input("\u9ED8\u8BA4\u4E0A\u6E38\u6A21\u578B\u540D\uFF08\u5199\u5165 Models[0].model\uFF0CRouter.default \u4F1A\u5F15\u7528 Models[0].id\uFF09", defaultModel);
8199
9762
  if (draft.Models?.[0]) {
8200
9763
  draft.Models[0].model = model;
8201
9764
  draft.Router.default = defaultProvider;
@@ -8205,20 +9768,19 @@ async function completeDraft(input3) {
8205
9768
  }
8206
9769
  }
8207
9770
  if (input3.fields.includes("apiKey")) {
8208
- const apiKey = await input3.io.input("API Key");
9771
+ const apiKey = await input3.io.input("API Key\uFF08\u5199\u5165 Models[].key\uFF09");
8209
9772
  if (draft.Models?.length) {
8210
- draft.Models = draft.Models.map((model) => ({ ...model, key: model.key || apiKey, api_key: model.api_key || apiKey }));
9773
+ draft.Models = draft.Models.map((model) => ({ ...model, key: model.key || apiKey }));
8211
9774
  } else {
8212
9775
  draft.Providers = draft.Providers?.map((provider) => ({ ...provider, api_key: provider.api_key || apiKey }));
8213
9776
  }
8214
9777
  }
8215
9778
  if (input3.fields.includes("apiBaseUrl")) {
8216
- const apiBaseUrl = await input3.io.input("API Base URL");
9779
+ const apiBaseUrl = await input3.io.input("API URL\uFF08\u5199\u5165 Models[].api\uFF09");
8217
9780
  if (draft.Models?.length) {
8218
9781
  draft.Models = draft.Models.map((model) => ({
8219
9782
  ...model,
8220
- api: model.api || apiBaseUrl,
8221
- api_base_url: model.api_base_url || apiBaseUrl
9783
+ api: model.api || apiBaseUrl
8222
9784
  }));
8223
9785
  } else {
8224
9786
  draft.Providers = draft.Providers?.map((provider) => ({
@@ -8228,7 +9790,10 @@ async function completeDraft(input3) {
8228
9790
  }
8229
9791
  }
8230
9792
  if (input3.fields.includes("capabilityHints") && draft.Models?.[0]) {
8231
- await promptCapabilityMetadataForDraft(draft, input3.io);
9793
+ const fixedWarnings = await promptCapabilityWarningFixesForDraft(draft, input3.io);
9794
+ if (!fixedWarnings) {
9795
+ await promptCapabilityMetadataForDraft(draft, input3.io);
9796
+ }
8232
9797
  }
8233
9798
  return draft;
8234
9799
  }
@@ -8253,6 +9818,31 @@ function printRoutingNextSteps(io) {
8253
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");
8254
9819
  io.info(" - \u914D\u7F6E\u6A21\u677F\u53C2\u8003\uFF1Aconfig/trigger.advanced.yaml");
8255
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
+ }
8256
9846
  async function runSetupCli(customDeps) {
8257
9847
  const defaults = createDefaultDeps(customDeps?.io);
8258
9848
  const deps = { ...defaults, ...customDeps };
@@ -8272,12 +9862,9 @@ async function runSetupCli(customDeps) {
8272
9862
  if (currentConfig.warnings.length > 0) {
8273
9863
  deps.io.info(`\u5F53\u524D\u914D\u7F6E\u63D0\u793A\uFF1A${formatConfigIssues({ warnings: currentConfig.warnings })}`);
8274
9864
  }
9865
+ const options = currentConfig.warnings.length > 0 ? ["\u76F4\u63A5\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\uFF08\u63A8\u8350\uFF09", "\u5FEB\u901F\u4FEE\u6B63\u914D\u7F6E\u63D0\u793A", "\u68C0\u67E5\u5E76\u8C03\u6574\u5F53\u524D\u914D\u7F6E", "\u653E\u5F03\u5F53\u524D\u914D\u7F6E\uFF0C\u91CD\u65B0\u5F00\u59CB"] : ["\u76F4\u63A5\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\uFF08\u63A8\u8350\uFF09", "\u68C0\u67E5\u5E76\u8C03\u6574\u5F53\u524D\u914D\u7F6E", "\u653E\u5F03\u5F53\u524D\u914D\u7F6E\uFF0C\u91CD\u65B0\u5F00\u59CB"];
8275
9866
  return mapValidCurrentConfigChoice(
8276
- await deps.io.choose("\u4F60\u60F3\u76F4\u63A5\u4F7F\u7528\u5B83\uFF0C\u8FD8\u662F\u91CD\u65B0\u8C03\u6574\uFF1F", [
8277
- "\u76F4\u63A5\u4F7F\u7528\u5F53\u524D\u914D\u7F6E\uFF08\u63A8\u8350\uFF09",
8278
- "\u68C0\u67E5\u5E76\u8C03\u6574\u5F53\u524D\u914D\u7F6E",
8279
- "\u653E\u5F03\u5F53\u524D\u914D\u7F6E\uFF0C\u91CD\u65B0\u5F00\u59CB"
8280
- ])
9867
+ await deps.io.choose("\u4F60\u60F3\u76F4\u63A5\u4F7F\u7528\u5B83\uFF0C\u8FD8\u662F\u91CD\u65B0\u8C03\u6574\uFF1F", options)
8281
9868
  );
8282
9869
  }
8283
9870
  if (currentConfig.kind === "invalid") {
@@ -8359,8 +9946,13 @@ async function runSetupCli(customDeps) {
8359
9946
  healthChecked: true
8360
9947
  };
8361
9948
  },
8362
- enterClaudeCode: async () => {
8363
- 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
+ }
8364
9956
  if (!shouldAutoEnterClaudeCodeAfterSetup()) {
8365
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");
8366
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");
@@ -8375,21 +9967,23 @@ async function runSetupCli(customDeps) {
8375
9967
  deps.io.close?.();
8376
9968
  }
8377
9969
  }
8378
- 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;
8379
9971
  var init_setup2 = __esm({
8380
9972
  "src/setup/index.ts"() {
8381
9973
  "use strict";
8382
- import_fs6 = require("fs");
9974
+ import_fs7 = require("fs");
9975
+ import_crypto3 = require("crypto");
8383
9976
  import_net2 = require("net");
8384
9977
  import_os3 = require("os");
8385
- import_path6 = require("path");
8386
- import_promises3 = require("readline/promises");
9978
+ import_path7 = require("path");
9979
+ import_promises4 = require("readline/promises");
8387
9980
  import_process = require("process");
8388
9981
  import_json52 = __toESM(require("json5"));
8389
9982
  import_js_yaml = __toESM(require("js-yaml"));
8390
9983
  init_constants();
8391
9984
  init_provider_presets();
8392
9985
  init_service_health();
9986
+ init_compile();
8393
9987
  init_utils();
8394
9988
  init_validation_contract();
8395
9989
  init_processCheck();
@@ -8400,6 +9994,7 @@ var init_setup2 = __esm({
8400
9994
  init_templates();
8401
9995
  init_persist();
8402
9996
  init_setup();
9997
+ init_runtime_role_guidance();
8403
9998
  }
8404
9999
  });
8405
10000
 
@@ -8472,7 +10067,7 @@ function hasArg(flag) {
8472
10067
  }
8473
10068
  function createConsoleIO2() {
8474
10069
  if (process.env.CTR_DOCTOR_FORCE_SCRIPTED_INPUT === "1") {
8475
- const scriptedInput = (0, import_fs7.readFileSync)(0, "utf-8");
10070
+ const scriptedInput = (0, import_fs8.readFileSync)(0, "utf-8");
8476
10071
  const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
8477
10072
  let cursor = 0;
8478
10073
  const nextAnswer = async () => answers[cursor++] ?? "";
@@ -8515,8 +10110,17 @@ function createConsoleIO2() {
8515
10110
  }
8516
10111
  };
8517
10112
  }
8518
- const rl = (0, import_promises4.createInterface)({ input: import_process2.stdin, output: import_process2.stdout });
8519
- 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
+ };
8520
10124
  return {
8521
10125
  info(message) {
8522
10126
  import_process2.stdout.write(`${message}
@@ -8533,6 +10137,9 @@ function createConsoleIO2() {
8533
10137
  `));
8534
10138
  while (true) {
8535
10139
  const answer = await ask("> ");
10140
+ if (answer === void 0) {
10141
+ return options[0];
10142
+ }
8536
10143
  const index = Number(answer);
8537
10144
  if (Number.isInteger(index) && index >= 1 && index <= options.length) {
8538
10145
  return options[index - 1];
@@ -8549,7 +10156,7 @@ function createConsoleIO2() {
8549
10156
  return answer || defaultValue || "";
8550
10157
  },
8551
10158
  async confirm(message, defaultValue = true) {
8552
- const answer = (await ask(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"}: `)).toLowerCase();
10159
+ const answer = (await ask(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"}: `))?.toLowerCase();
8553
10160
  if (!answer) {
8554
10161
  return defaultValue;
8555
10162
  }
@@ -8596,7 +10203,7 @@ function tryLoadStructuredConfig(filePath, content) {
8596
10203
  }
8597
10204
  }
8598
10205
  function loadCurrentConfig() {
8599
- const existingPath = getConfigCandidates().find((filePath) => (0, import_fs7.existsSync)(filePath));
10206
+ const existingPath = getConfigCandidates().find((filePath) => (0, import_fs8.existsSync)(filePath));
8600
10207
  const path = existingPath ?? CONFIG_FILE;
8601
10208
  if (!existingPath) {
8602
10209
  return {
@@ -8606,7 +10213,7 @@ function loadCurrentConfig() {
8606
10213
  messages: ["\u672A\u68C0\u6D4B\u5230\u5F53\u524D Claude Trigger Router \u914D\u7F6E\u3002"]
8607
10214
  };
8608
10215
  }
8609
- const content = (0, import_fs7.readFileSync)(existingPath, "utf-8");
10216
+ const content = (0, import_fs8.readFileSync)(existingPath, "utf-8");
8610
10217
  const loaded = tryLoadStructuredConfig(existingPath, content);
8611
10218
  return {
8612
10219
  path,
@@ -8883,7 +10490,8 @@ function explainProbeFailure(category) {
8883
10490
  }
8884
10491
  async function ensureServiceUsable(config, deps, configChanged) {
8885
10492
  const port = config.PORT ?? DEFAULT_CONFIG2.PORT;
8886
- const healthy = await deps.probeServiceHealth(port, 500);
10493
+ const serviceHealthOptions = config.APIKEY ? { apiKey: config.APIKEY } : {};
10494
+ const healthy = await deps.probeServiceHealth(port, 500, serviceHealthOptions);
8887
10495
  const occupied = await deps.isTcpPortOccupied(port, 500);
8888
10496
  const running = deps.isServiceRunning();
8889
10497
  if (healthy && !configChanged) {
@@ -8903,12 +10511,62 @@ async function ensureServiceUsable(config, deps, configChanged) {
8903
10511
  }
8904
10512
  }
8905
10513
  await deps.startDaemon();
8906
- const verified = await deps.waitForService(port, 5e3);
10514
+ const verified = await deps.waitForService(port, 5e3, serviceHealthOptions);
8907
10515
  if (!verified) {
8908
10516
  throw new Error(`doctor \u81EA\u52A8\u542F\u52A8\u540E\u5065\u5EB7\u68C0\u67E5\u4ECD\u672A\u901A\u8FC7\uFF08\u7AEF\u53E3 ${port}\uFF09\u3002`);
8909
10517
  }
8910
10518
  deps.io.info(`\u670D\u52A1\u5DF2\u5C31\u7EEA\uFF1Ahttp://127.0.0.1:${port}`);
8911
10519
  }
10520
+ async function reportRuntimeServiceContext(config, deps) {
10521
+ const runtimeMode = config.Runtime?.mode ?? "local";
10522
+ const serviceRole = runtimeMode === "local" ? "local_agent" : "router_service";
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}`;
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
+ }
10548
+ if (!remoteService?.enabled) {
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");
10550
+ return;
10551
+ }
10552
+ const baseUrl = remoteService.base_url?.trim().replace(/\/+$/, "") || "<missing>";
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");
10555
+ const remoteStatus = await probeRemoteServiceStatus(remoteService);
10556
+ const statusLabel = remoteStatus.ready ? "ready" : remoteStatus.reachable ? "reachable" : "unreachable";
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
+ }
10566
+ if (remoteStatus.error) {
10567
+ deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u63D0\u793A\uFF1A${remoteStatus.error}`);
10568
+ }
10569
+ }
8912
10570
  function createDefaultDeps2(io = createConsoleIO2()) {
8913
10571
  return {
8914
10572
  readLegacyConfig,
@@ -8966,6 +10624,7 @@ async function runDoctorCli(customDeps) {
8966
10624
  if (normalized.warnings.length > 0) {
8967
10625
  deps.io.info(`\u914D\u7F6E\u63D0\u793A\uFF1A${formatValidationIssueReport(buildValidationIssueReport({ warnings: normalized.warnings })).join("; ")}`);
8968
10626
  }
10627
+ await reportRuntimeServiceContext(normalized.config, deps);
8969
10628
  const registry = buildModelRegistry(normalized.config);
8970
10629
  for (const model of normalized.config.Models ?? []) {
8971
10630
  const compiledModel = registry.modelMap[model.id];
@@ -8999,7 +10658,12 @@ async function runDoctorCli(customDeps) {
8999
10658
  configChanged = true;
9000
10659
  }
9001
10660
  await ensureServiceUsable(normalized.config, deps, configChanged);
9002
- const shouldProbeModels = hasArg("--check-models") ? await deps.io.confirm(`\u5373\u5C06\u5411 ${normalized.config.Models?.length ?? 0} \u4E2A\u6A21\u578B\u53D1\u9001\u6700\u5C0F\u63A2\u6D4B\u8BF7\u6C42\uFF0C\u53EF\u80FD\u6D88\u8017\u5C11\u91CF\u989D\u5EA6\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F`, true) : await deps.io.confirm(`\u662F\u5426\u7EE7\u7EED\u63A2\u6D4B ${normalized.config.Models?.length ?? 0} \u4E2A\u6A21\u578B\u7684\u53EF\u7528\u6027\uFF1F\u8FD9\u4F1A\u6D88\u8017\u5C11\u91CF\u989D\u5EA6\u3002`, false);
10661
+ const modelCount = normalized.config.Models?.length ?? 0;
10662
+ if (modelCount === 0) {
10663
+ deps.io.info("\u5DF2\u8DF3\u8FC7\u6A21\u578B\u63A2\u6D4B\uFF1A\u5F53\u524D\u914D\u7F6E\u6CA1\u6709\u672C\u5730\u6A21\u578B\u3002\u914D\u7F6E\u548C\u670D\u52A1\u8BCA\u65AD\u5DF2\u5B8C\u6210\u3002");
10664
+ return;
10665
+ }
10666
+ const shouldProbeModels = hasArg("--check-models") ? await deps.io.confirm(`\u5373\u5C06\u5411 ${modelCount} \u4E2A\u6A21\u578B\u53D1\u9001\u6700\u5C0F\u63A2\u6D4B\u8BF7\u6C42\uFF0C\u53EF\u80FD\u6D88\u8017\u5C11\u91CF\u989D\u5EA6\uFF0C\u662F\u5426\u7EE7\u7EED\uFF1F`, true) : await deps.io.confirm(`\u662F\u5426\u7EE7\u7EED\u63A2\u6D4B ${modelCount} \u4E2A\u6A21\u578B\u7684\u53EF\u7528\u6027\uFF1F\u8FD9\u4F1A\u6D88\u8017\u5C11\u91CF\u989D\u5EA6\u3002`, false);
9003
10667
  if (!shouldProbeModels) {
9004
10668
  deps.io.info("\u5DF2\u8DF3\u8FC7\u6A21\u578B\u63A2\u6D4B\u3002\u914D\u7F6E\u548C\u670D\u52A1\u8BCA\u65AD\u5DF2\u5B8C\u6210\u3002");
9005
10669
  return;
@@ -9027,12 +10691,12 @@ async function runDoctorCli(customDeps) {
9027
10691
  deps.io.close?.();
9028
10692
  }
9029
10693
  }
9030
- 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;
9031
10695
  var init_doctor = __esm({
9032
10696
  "src/doctor/index.ts"() {
9033
10697
  "use strict";
9034
- import_fs7 = require("fs");
9035
- import_promises4 = require("readline/promises");
10698
+ import_fs8 = require("fs");
10699
+ import_promises5 = require("readline/promises");
9036
10700
  import_process2 = require("process");
9037
10701
  import_child_process2 = require("child_process");
9038
10702
  import_json53 = __toESM(require("json5"));
@@ -9048,6 +10712,7 @@ var init_doctor = __esm({
9048
10712
  init_processCheck();
9049
10713
  init_service_health();
9050
10714
  init_templates();
10715
+ init_api_keys();
9051
10716
  }
9052
10717
  });
9053
10718
 
@@ -9060,7 +10725,7 @@ __export(cli_exports, {
9060
10725
  });
9061
10726
  module.exports = __toCommonJS(cli_exports);
9062
10727
  function getPackageInfo() {
9063
- const content = (0, import_fs8.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
10728
+ const content = (0, import_fs9.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
9064
10729
  const pkg = JSON.parse(content);
9065
10730
  return {
9066
10731
  name: pkg.name ?? "@peterwangze/claude-trigger-router",
@@ -9100,16 +10765,16 @@ function getPort() {
9100
10765
  }
9101
10766
  try {
9102
10767
  const yaml4 = require("js-yaml");
9103
- if ((0, import_fs8.existsSync)(CONFIG_FILE)) {
9104
- 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");
9105
10770
  const config = yaml4.load(content);
9106
10771
  if (config?.PORT) return config.PORT;
9107
- } else if ((0, import_fs8.existsSync)(CONFIG_FILE_YML)) {
9108
- 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");
9109
10774
  const config = yaml4.load(content);
9110
10775
  if (config?.PORT) return config.PORT;
9111
- } else if ((0, import_fs8.existsSync)(CONFIG_FILE_JSON)) {
9112
- 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");
9113
10778
  const config = JSON.parse(content);
9114
10779
  if (config?.PORT) return config.PORT;
9115
10780
  }
@@ -9130,6 +10795,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
9130
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
9131
10796
  doctor \u8BCA\u65AD\u5E76\u4FEE\u590D\u5F53\u524D\u914D\u7F6E\uFF0C\u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
9132
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
9133
10799
  start \u542F\u52A8\u8DEF\u7531\u670D\u52A1\uFF08\u9ED8\u8BA4\u524D\u53F0\u8FD0\u884C\uFF09
9134
10800
  stop \u505C\u6B62\u540E\u53F0\u670D\u52A1
9135
10801
  restart \u91CD\u542F\u540E\u53F0\u670D\u52A1
@@ -9143,12 +10809,13 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
9143
10809
  \u9009\u9879\uFF1A
9144
10810
  --port, -p \u6307\u5B9A\u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4\uFF1A5678\uFF09
9145
10811
  --daemon, -d \u4EE5\u540E\u53F0\u65B9\u5F0F\u8FD0\u884C\uFF08\u914D\u5408 start/restart \u4F7F\u7528\uFF09
9146
- --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
9147
10813
 
9148
10814
  \u4F7F\u7528\u793A\u4F8B\uFF1A
9149
10815
  ctr setup # \u590D\u7528\u5F53\u524D\u914D\u7F6E / \u8FC1\u79FB\u65E7\u914D\u7F6E / \u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
9150
10816
  ctr doctor # \u8BCA\u65AD\u914D\u7F6E / \u4FEE\u590D\u683C\u5F0F\u95EE\u9898 / \u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
9151
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
9152
10819
  ctr version # \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C
9153
10820
  ctr upgrade # \u67E5\u770B\u5347\u7EA7\u5230\u6700\u65B0\u7248\u672C\u7684\u547D\u4EE4
9154
10821
  ctr start # \u524D\u53F0\u542F\u52A8\uFF08\u63A8\u8350\u9996\u6B21\u4F7F\u7528\uFF0C\u4FBF\u4E8E\u67E5\u770B\u65E5\u5FD7\uFF09
@@ -9171,6 +10838,73 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
9171
10838
  \u66F4\u591A\u4FE1\u606F\uFF1Ahttps://github.com/peterwangze/claude-trigger-router
9172
10839
  `);
9173
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
+ }
9174
10908
  function getLatestPackageVersionViaNpm(packageName, timeoutMs = 5e3) {
9175
10909
  try {
9176
10910
  const result = (0, import_child_process3.spawnSync)("npm", ["view", packageName, "version", "--registry", PACKAGE_REGISTRY_URL], {
@@ -9257,16 +10991,19 @@ function isClaudeCommandAvailable(timeoutMs = 3e3) {
9257
10991
  return false;
9258
10992
  }
9259
10993
  }
10994
+ function createBootstrapApiKey() {
10995
+ return `ctr_bootstrap_${(0, import_crypto4.randomBytes)(24).toString("hex")}`;
10996
+ }
9260
10997
  function initConfig2() {
9261
10998
  const force = hasArg2("--force");
9262
- 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);
9263
11000
  if (existingConfig && !force) {
9264
11001
  console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
9265
11002
  console.log(" \u5982\u9700\u8986\u76D6\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
9266
11003
  return;
9267
11004
  }
9268
- if (!(0, import_fs8.existsSync)(CONFIG_DIR)) {
9269
- (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 });
9270
11007
  }
9271
11008
  try {
9272
11009
  const yaml4 = require("js-yaml");
@@ -9276,7 +11013,7 @@ function initConfig2() {
9276
11013
  lineWidth: -1,
9277
11014
  noRefs: true
9278
11015
  });
9279
- (0, import_fs8.writeFileSync)(CONFIG_FILE, content, "utf-8");
11016
+ (0, import_fs9.writeFileSync)(CONFIG_FILE, content, "utf-8");
9280
11017
  const action = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
9281
11018
  console.log(`\u2705 \u914D\u7F6E\u6587\u4EF6${action}\uFF1A${CONFIG_FILE}`);
9282
11019
  console.log("");
@@ -9291,6 +11028,65 @@ function initConfig2() {
9291
11028
  process.exit(1);
9292
11029
  }
9293
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
+ }
9294
11090
  async function startForeground(port) {
9295
11091
  const targetPort = port ?? getPort();
9296
11092
  const healthy = await waitForService(targetPort, 500);
@@ -9378,16 +11174,28 @@ async function startDaemon(port) {
9378
11174
  console.log(` Run 'ctr stop' to stop it.`);
9379
11175
  }
9380
11176
  async function showStatus() {
11177
+ const config = readConfigForCliStatus();
11178
+ const configuredPort = getPort();
11179
+ const healthOptions = config?.APIKEY ? { apiKey: config.APIKEY } : {};
9381
11180
  const info = readServiceInfo();
9382
11181
  if (!info || !isServiceRunning()) {
9383
- const targetPort = getPort();
9384
- const healthy = await waitForService(targetPort, 500);
11182
+ const targetPort = configuredPort;
11183
+ const healthy = await waitForService(targetPort, 500, healthOptions);
9385
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
+ }
9386
11193
  if (!healthy && occupied) {
9387
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`);
9388
11195
  return;
9389
11196
  }
9390
11197
  console.log("\u23F9 \u670D\u52A1\u672A\u8FD0\u884C");
11198
+ printRuntimeStatus(config, targetPort);
9391
11199
  return;
9392
11200
  }
9393
11201
  const startTime = info.startTime ? new Date(info.startTime).toLocaleString() : "\u672A\u77E5";
@@ -9396,6 +11204,8 @@ async function showStatus() {
9396
11204
  console.log(` \u7AEF\u53E3\uFF1A${info.port}`);
9397
11205
  console.log(` \u542F\u52A8\u65F6\u95F4\uFF1A${startTime}`);
9398
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);
9399
11209
  }
9400
11210
  function stopService() {
9401
11211
  const info = readServiceInfo();
@@ -9418,10 +11228,12 @@ async function restartService() {
9418
11228
  }
9419
11229
  async function runClaudeCode() {
9420
11230
  const port = getPort();
11231
+ const config = readConfigForCliStatus();
11232
+ const proxyToken = getLocalClaudeProxyToken(config);
9421
11233
  await initializeClaudeConfig();
9422
11234
  const running = isServiceRunning();
9423
11235
  console.log(`\u{1F50D} Checking if service is available on port ${port}...`);
9424
- const reachable = await waitForService(port, 2e3);
11236
+ const reachable = await waitForService(port, 2e3, { apiKey: proxyToken });
9425
11237
  if (!reachable) {
9426
11238
  console.log(`\u26A0\uFE0F Trigger Router service is not running on port ${port}.`);
9427
11239
  console.log("");
@@ -9438,14 +11250,16 @@ async function runClaudeCode() {
9438
11250
  process.exit(1);
9439
11251
  }
9440
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;
9441
11259
  const claude = (0, import_child_process3.spawn)("claude", [], {
9442
11260
  stdio: "inherit",
9443
11261
  shell: isWindows,
9444
- env: {
9445
- ...process.env,
9446
- ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
9447
- ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "ctr-local-proxy"
9448
- }
11262
+ env: claudeEnv
9449
11263
  });
9450
11264
  claude.on("error", (error) => {
9451
11265
  console.error("\u274C \u542F\u52A8 Claude Code \u5931\u8D25:", error.message);
@@ -9488,6 +11302,9 @@ async function main() {
9488
11302
  case "init":
9489
11303
  initConfig2();
9490
11304
  break;
11305
+ case "deploy":
11306
+ initDeployConfig();
11307
+ break;
9491
11308
  case "start":
9492
11309
  if (isDaemonMode()) {
9493
11310
  await startDaemon(getPort());
@@ -9530,13 +11347,14 @@ async function main() {
9530
11347
  process.exit(command ? 1 : 0);
9531
11348
  }
9532
11349
  }
9533
- 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;
9534
11351
  var init_cli = __esm({
9535
11352
  "src/cli.ts"() {
9536
11353
  import_child_process3 = require("child_process");
9537
- import_path7 = require("path");
11354
+ import_crypto4 = require("crypto");
11355
+ import_path8 = require("path");
9538
11356
  import_openurl = __toESM(require("openurl"));
9539
- import_fs8 = require("fs");
11357
+ import_fs9 = require("fs");
9540
11358
  init_index();
9541
11359
  init_processCheck();
9542
11360
  init_constants();
@@ -9544,7 +11362,9 @@ var init_cli = __esm({
9544
11362
  init_setup2();
9545
11363
  init_templates();
9546
11364
  init_doctor();
9547
- 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");
9548
11368
  PACKAGE_PAGE_URL = "https://www.npmjs.com/package/@peterwangze/claude-trigger-router";
9549
11369
  PACKAGE_REGISTRY_LATEST_URL = "https://registry.npmjs.org/@peterwangze%2Fclaude-trigger-router/latest";
9550
11370
  PACKAGE_REGISTRY_URL = "https://registry.npmjs.org/";