@peterwangze/claude-trigger-router 1.1.1 → 1.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -9
- package/config/deploy/README.md +36 -0
- package/config/deploy/docker-compose.server.yaml +29 -0
- package/config/deploy/systemd/claude-trigger-router.service +33 -0
- package/dist/cli.js +1713 -128
- package/dist/cli.js.map +4 -4
- package/docs/cli-test-matrix.md +175 -0
- package/docs/configuration-guide.md +390 -0
- package/docs/configuration-roles.md +42 -0
- package/docs/models-migration-guide.md +266 -0
- package/docs/releasing.md +311 -0
- package/docs/remote-client-guide.md +68 -0
- package/docs/server-maintainer-guide.md +81 -0
- package/package.json +2 -1
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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[
|
|
412
|
-
id:
|
|
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,
|
|
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
|
|
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
|
-
|
|
2149
|
+
import_crypto2 = require("crypto");
|
|
1507
2150
|
import_fs2 = require("fs");
|
|
1508
2151
|
import_lru_cache = require("lru-cache");
|
|
1509
2152
|
import_path3 = require("path");
|
|
@@ -2888,6 +3531,122 @@ function buildTopEntries(distribution, total, limit = 5) {
|
|
|
2888
3531
|
rate: rate(count, total)
|
|
2889
3532
|
}));
|
|
2890
3533
|
}
|
|
3534
|
+
function buildTopSwitchEntries(distribution, total, limit = 5) {
|
|
3535
|
+
return Object.values(distribution).sort((left, right) => {
|
|
3536
|
+
if (right.count !== left.count) {
|
|
3537
|
+
return right.count - left.count;
|
|
3538
|
+
}
|
|
3539
|
+
return `${left.from ?? ""}->${left.to ?? ""}`.localeCompare(`${right.from ?? ""}->${right.to ?? ""}`);
|
|
3540
|
+
}).slice(0, limit).map((entry) => ({
|
|
3541
|
+
key: `${entry.from ?? "-"} -> ${entry.to ?? "-"}`,
|
|
3542
|
+
from: entry.from,
|
|
3543
|
+
to: entry.to,
|
|
3544
|
+
count: entry.count,
|
|
3545
|
+
rate: rate(entry.count, total)
|
|
3546
|
+
}));
|
|
3547
|
+
}
|
|
3548
|
+
function addOutcomeGroup(groups, key, trace) {
|
|
3549
|
+
if (!key) {
|
|
3550
|
+
return;
|
|
3551
|
+
}
|
|
3552
|
+
const switched = isModelSwitch(trace);
|
|
3553
|
+
const group = groups[key] ?? {
|
|
3554
|
+
key,
|
|
3555
|
+
totalTraces: 0,
|
|
3556
|
+
modelSwitchCount: 0,
|
|
3557
|
+
alignmentOnSwitchCount: 0,
|
|
3558
|
+
cascadeAfterSwitchCount: 0,
|
|
3559
|
+
latencyValues: []
|
|
3560
|
+
};
|
|
3561
|
+
group.totalTraces += 1;
|
|
3562
|
+
group.modelSwitchCount += switched ? 1 : 0;
|
|
3563
|
+
group.alignmentOnSwitchCount += switched && trace.alignmentUsed ? 1 : 0;
|
|
3564
|
+
group.cascadeAfterSwitchCount += switched && trace.cascadeTriggered ? 1 : 0;
|
|
3565
|
+
if (typeof trace.latencyMs === "number" && Number.isFinite(trace.latencyMs)) {
|
|
3566
|
+
group.latencyValues.push(trace.latencyMs);
|
|
3567
|
+
}
|
|
3568
|
+
groups[key] = group;
|
|
3569
|
+
}
|
|
3570
|
+
function buildOutcomeGroupEntries(groups, totalTraces, limit = 5) {
|
|
3571
|
+
return Object.values(groups).sort((left, right) => {
|
|
3572
|
+
if (right.totalTraces !== left.totalTraces) {
|
|
3573
|
+
return right.totalTraces - left.totalTraces;
|
|
3574
|
+
}
|
|
3575
|
+
return left.key.localeCompare(right.key);
|
|
3576
|
+
}).slice(0, limit).map((group) => ({
|
|
3577
|
+
key: group.key,
|
|
3578
|
+
totalTraces: group.totalTraces,
|
|
3579
|
+
rate: rate(group.totalTraces, totalTraces),
|
|
3580
|
+
modelSwitchCount: group.modelSwitchCount,
|
|
3581
|
+
modelSwitchRate: rate(group.modelSwitchCount, group.totalTraces),
|
|
3582
|
+
alignmentOnSwitchCount: group.alignmentOnSwitchCount,
|
|
3583
|
+
alignmentOnSwitchRate: rate(group.alignmentOnSwitchCount, group.modelSwitchCount),
|
|
3584
|
+
cascadeAfterSwitchCount: group.cascadeAfterSwitchCount,
|
|
3585
|
+
cascadeAfterSwitchRate: rate(group.cascadeAfterSwitchCount, group.modelSwitchCount),
|
|
3586
|
+
averageLatencyMs: average(group.latencyValues)
|
|
3587
|
+
}));
|
|
3588
|
+
}
|
|
3589
|
+
function isRoutedTrace(trace) {
|
|
3590
|
+
return trace.routeReason.some((reason) => reason !== "request_received");
|
|
3591
|
+
}
|
|
3592
|
+
function isModelSwitch(trace) {
|
|
3593
|
+
return Boolean(trace.initialModel && trace.finalModel && trace.initialModel !== trace.finalModel);
|
|
3594
|
+
}
|
|
3595
|
+
function summarizeRoutingOutcomes(traces) {
|
|
3596
|
+
const routedTraces = traces.filter(isRoutedTrace);
|
|
3597
|
+
const switchedTraces = traces.filter(isModelSwitch);
|
|
3598
|
+
const stableModelCount = traces.filter(
|
|
3599
|
+
(trace) => Boolean(trace.initialModel && trace.finalModel && trace.initialModel === trace.finalModel)
|
|
3600
|
+
).length;
|
|
3601
|
+
const alignmentOnSwitchCount = switchedTraces.filter((trace) => trace.alignmentUsed).length;
|
|
3602
|
+
const cascadeAfterSwitchCount = switchedTraces.filter((trace) => trace.cascadeTriggered).length;
|
|
3603
|
+
const switchDistribution = {};
|
|
3604
|
+
const routeLatencyValues = {};
|
|
3605
|
+
const routeReasonGroups = {};
|
|
3606
|
+
const finalModelGroups = {};
|
|
3607
|
+
const semanticIntentGroups = {};
|
|
3608
|
+
for (const trace of traces) {
|
|
3609
|
+
if (isModelSwitch(trace)) {
|
|
3610
|
+
const key = `${trace.initialModel} -> ${trace.finalModel}`;
|
|
3611
|
+
switchDistribution[key] = {
|
|
3612
|
+
from: trace.initialModel,
|
|
3613
|
+
to: trace.finalModel,
|
|
3614
|
+
count: (switchDistribution[key]?.count ?? 0) + 1
|
|
3615
|
+
};
|
|
3616
|
+
}
|
|
3617
|
+
if (typeof trace.latencyMs === "number" && Number.isFinite(trace.latencyMs)) {
|
|
3618
|
+
for (const reason of trace.routeReason.filter((item) => item !== "request_received")) {
|
|
3619
|
+
routeLatencyValues[reason] = [...routeLatencyValues[reason] ?? [], trace.latencyMs];
|
|
3620
|
+
}
|
|
3621
|
+
}
|
|
3622
|
+
for (const reason of trace.routeReason.filter((item) => item !== "request_received")) {
|
|
3623
|
+
addOutcomeGroup(routeReasonGroups, reason, trace);
|
|
3624
|
+
}
|
|
3625
|
+
addOutcomeGroup(finalModelGroups, trace.finalModel, trace);
|
|
3626
|
+
addOutcomeGroup(semanticIntentGroups, trace.semanticIntent, trace);
|
|
3627
|
+
}
|
|
3628
|
+
const averageLatencyByRouteReason = Object.fromEntries(
|
|
3629
|
+
Object.entries(routeLatencyValues).sort(([left], [right]) => left.localeCompare(right)).map(([reason, values]) => [reason, average(values)])
|
|
3630
|
+
);
|
|
3631
|
+
return {
|
|
3632
|
+
totalTraces: traces.length,
|
|
3633
|
+
routedTraces: routedTraces.length,
|
|
3634
|
+
routedRate: rate(routedTraces.length, traces.length),
|
|
3635
|
+
modelSwitchCount: switchedTraces.length,
|
|
3636
|
+
modelSwitchRate: rate(switchedTraces.length, traces.length),
|
|
3637
|
+
stableModelCount,
|
|
3638
|
+
stableModelRate: rate(stableModelCount, traces.length),
|
|
3639
|
+
alignmentOnSwitchCount,
|
|
3640
|
+
alignmentOnSwitchRate: rate(alignmentOnSwitchCount, switchedTraces.length),
|
|
3641
|
+
cascadeAfterSwitchCount,
|
|
3642
|
+
cascadeAfterSwitchRate: rate(cascadeAfterSwitchCount, switchedTraces.length),
|
|
3643
|
+
averageLatencyByRouteReason,
|
|
3644
|
+
topModelSwitches: buildTopSwitchEntries(switchDistribution, switchedTraces.length),
|
|
3645
|
+
byRouteReason: buildOutcomeGroupEntries(routeReasonGroups, traces.length),
|
|
3646
|
+
byFinalModel: buildOutcomeGroupEntries(finalModelGroups, traces.length),
|
|
3647
|
+
bySemanticIntent: buildOutcomeGroupEntries(semanticIntentGroups, traces.length)
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
2891
3650
|
function averageRate(values) {
|
|
2892
3651
|
if (!values.length) {
|
|
2893
3652
|
return 0;
|
|
@@ -2995,6 +3754,8 @@ function buildGovernanceHealthSummary(input3) {
|
|
|
2995
3754
|
cascadeTriggeredRate: 0,
|
|
2996
3755
|
shadowCheckedRate: 0,
|
|
2997
3756
|
alignmentUsedRate: 0,
|
|
3757
|
+
modelSwitchRate: 0,
|
|
3758
|
+
alignmentOnSwitchRate: 0,
|
|
2998
3759
|
averageLatencyMs: 0,
|
|
2999
3760
|
topRouteReason: input3.topRouteReasons?.[0],
|
|
3000
3761
|
topFinalModel: input3.topFinalModels?.[0]
|
|
@@ -3017,6 +3778,8 @@ function buildGovernanceHealthSummary(input3) {
|
|
|
3017
3778
|
cascadeTriggeredRate: metrics.cascadeTriggeredRate,
|
|
3018
3779
|
shadowCheckedRate: metrics.shadowCheckedRate,
|
|
3019
3780
|
alignmentUsedRate: metrics.alignmentUsedRate,
|
|
3781
|
+
modelSwitchRate: input3.outcome?.modelSwitchRate ?? 0,
|
|
3782
|
+
alignmentOnSwitchRate: input3.outcome?.alignmentOnSwitchRate ?? 0,
|
|
3020
3783
|
averageLatencyMs: metrics.averageLatencyMs,
|
|
3021
3784
|
topRouteReason: input3.topRouteReasons?.[0],
|
|
3022
3785
|
topFinalModel: input3.topFinalModels?.[0]
|
|
@@ -3113,6 +3876,7 @@ function getGovernanceMetricsReport(options = {}) {
|
|
|
3113
3876
|
const limitedTraces = options.limit && options.limit > 0 ? windowed.traces.slice(0, options.limit) : windowed.traces;
|
|
3114
3877
|
const bucketCount = options.bucketCount && options.bucketCount > 0 ? options.bucketCount : 6;
|
|
3115
3878
|
const metrics = summarizeGovernanceMetrics(limitedTraces);
|
|
3879
|
+
const outcome = summarizeRoutingOutcomes(limitedTraces);
|
|
3116
3880
|
const buckets = buildBuckets(limitedTraces, windowed.windowStart, windowed.windowEnd, bucketCount);
|
|
3117
3881
|
const thresholds = normalizeAnomalyThresholds(options.anomalyThresholds);
|
|
3118
3882
|
const topRouteReasons = buildTopEntries(metrics.routeReasonDistribution, limitedTraces.length);
|
|
@@ -3125,6 +3889,7 @@ function getGovernanceMetricsReport(options = {}) {
|
|
|
3125
3889
|
windowStart: windowed.windowStart,
|
|
3126
3890
|
windowEnd: windowed.windowEnd,
|
|
3127
3891
|
metrics,
|
|
3892
|
+
outcome,
|
|
3128
3893
|
buckets,
|
|
3129
3894
|
topRouteReasons,
|
|
3130
3895
|
topFinalModels,
|
|
@@ -3134,7 +3899,8 @@ function getGovernanceMetricsReport(options = {}) {
|
|
|
3134
3899
|
metrics,
|
|
3135
3900
|
anomalies,
|
|
3136
3901
|
topRouteReasons,
|
|
3137
|
-
topFinalModels
|
|
3902
|
+
topFinalModels,
|
|
3903
|
+
outcome
|
|
3138
3904
|
})
|
|
3139
3905
|
};
|
|
3140
3906
|
}
|
|
@@ -3150,7 +3916,11 @@ function exportGovernanceMetricsReport(report, format = "json") {
|
|
|
3150
3916
|
`summary,shadowCheckedRate,${report.metrics.shadowCheckedRate}`,
|
|
3151
3917
|
`summary,alignmentUsedRate,${report.metrics.alignmentUsedRate}`,
|
|
3152
3918
|
`summary,averageLatencyMs,${report.metrics.averageLatencyMs}`,
|
|
3153
|
-
`summary,averageEstimatedCost,${report.metrics.averageEstimatedCost}
|
|
3919
|
+
`summary,averageEstimatedCost,${report.metrics.averageEstimatedCost}`,
|
|
3920
|
+
`outcome,routedRate,${report.outcome.routedRate}`,
|
|
3921
|
+
`outcome,modelSwitchRate,${report.outcome.modelSwitchRate}`,
|
|
3922
|
+
`outcome,alignmentOnSwitchRate,${report.outcome.alignmentOnSwitchRate}`,
|
|
3923
|
+
`outcome,cascadeAfterSwitchRate,${report.outcome.cascadeAfterSwitchRate}`
|
|
3154
3924
|
];
|
|
3155
3925
|
if (report.health) {
|
|
3156
3926
|
lines.push(`summary,healthStatus,${report.health.status}`);
|
|
@@ -3168,6 +3938,18 @@ function exportGovernanceMetricsReport(report, format = "json") {
|
|
|
3168
3938
|
for (const item of report.topSemanticIntents) {
|
|
3169
3939
|
lines.push(`topSemanticIntent,${item.key},${item.count}:${item.rate}`);
|
|
3170
3940
|
}
|
|
3941
|
+
for (const item of report.outcome.topModelSwitches) {
|
|
3942
|
+
lines.push(`topModelSwitch,${item.key},${item.count}:${item.rate}`);
|
|
3943
|
+
}
|
|
3944
|
+
for (const item of report.outcome.byRouteReason) {
|
|
3945
|
+
lines.push(`outcomeByRouteReason,${item.key},${item.totalTraces}:${item.modelSwitchRate}:${item.averageLatencyMs}`);
|
|
3946
|
+
}
|
|
3947
|
+
for (const item of report.outcome.byFinalModel) {
|
|
3948
|
+
lines.push(`outcomeByFinalModel,${item.key},${item.totalTraces}:${item.modelSwitchRate}:${item.averageLatencyMs}`);
|
|
3949
|
+
}
|
|
3950
|
+
for (const item of report.outcome.bySemanticIntent) {
|
|
3951
|
+
lines.push(`outcomeBySemanticIntent,${item.key},${item.totalTraces}:${item.modelSwitchRate}:${item.averageLatencyMs}`);
|
|
3952
|
+
}
|
|
3171
3953
|
for (const bucket of report.buckets) {
|
|
3172
3954
|
lines.push(
|
|
3173
3955
|
`bucket,${bucket.label},${[
|
|
@@ -3620,6 +4402,17 @@ var init_provider_presets = __esm({
|
|
|
3620
4402
|
}
|
|
3621
4403
|
});
|
|
3622
4404
|
|
|
4405
|
+
// src/runtime-role-guidance.ts
|
|
4406
|
+
var LOCAL_USER_ROLE_GUIDE, SERVER_MAINTAINER_ROLE_GUIDE, REMOTE_CLIENT_ROLE_GUIDE;
|
|
4407
|
+
var init_runtime_role_guidance = __esm({
|
|
4408
|
+
"src/runtime-role-guidance.ts"() {
|
|
4409
|
+
"use strict";
|
|
4410
|
+
LOCAL_USER_ROLE_GUIDE = "\u672C\u5730\u4F7F\u7528\u8005\uFF1A\u5148\u8DD1\u901A Models + Router.default\uFF0C\u518D\u7528 ctr start / ctr status / ctr code \u8FDB\u5165 Claude Code\u3002";
|
|
4411
|
+
SERVER_MAINTAINER_ROLE_GUIDE = "\u670D\u52A1\u7EF4\u62A4\u8005\uFF1A\u7528 ctr deploy init --target server \u751F\u6210 server \u914D\u7F6E\uFF0C\u4FDD\u7559 bootstrap/admin key \u7BA1\u7406\u670D\u52A1\uFF0C\u5E76\u7ED9\u8FDC\u7A0B\u4F7F\u7528\u8005\u53D1\u653E managed client + read-only key\u3002";
|
|
4412
|
+
REMOTE_CLIENT_ROLE_GUIDE = "\u8FDC\u7A0B\u4F7F\u7528\u8005\uFF1A\u62FF\u5230\u670D\u52A1\u5730\u5740\u548C managed client + read-only key\uFF1BRuntime.remote_service \u8D1F\u8D23\u8FDE\u63A5\u914D\u7F6E\u4E0E ready/status \u68C0\u67E5\uFF0C\u76F4\u8FDE Claude Code \u65F6\u8BBE\u7F6E ANTHROPIC_BASE_URL \u4E0E ANTHROPIC_AUTH_TOKEN\u3002";
|
|
4413
|
+
}
|
|
4414
|
+
});
|
|
4415
|
+
|
|
3623
4416
|
// src/ui/workbench.ts
|
|
3624
4417
|
function toInlineScriptJson(value) {
|
|
3625
4418
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
@@ -3637,27 +4430,53 @@ function renderWorkbenchHtml(rawInitialConfig, configuredThresholds = {}) {
|
|
|
3637
4430
|
const remoteService = initialConfig.Runtime?.remote_service ?? {};
|
|
3638
4431
|
const remoteBaseUrl = typeof remoteService.base_url === "string" ? remoteService.base_url.trim().replace(/\/+$/, "") : "";
|
|
3639
4432
|
const remoteSummary = remoteService.enabled ? `${remoteBaseUrl || "-"} (checking)` : "disabled";
|
|
4433
|
+
const configuredHost = String(initialConfig.HOST ?? "127.0.0.1").trim() || "127.0.0.1";
|
|
4434
|
+
const publicHost = ["0.0.0.0", "::", "[::]"].includes(configuredHost);
|
|
4435
|
+
const advertisedUrl = publicHost ? `http://<server-host>:${displayPort}` : `http://${configuredHost}:${displayPort}`;
|
|
4436
|
+
const clientConnectionSummary = runtimeMode === "local" && remoteService.enabled ? `${remoteBaseUrl || "-"} \xB7 client + read-only token` : runtimeMode === "local" ? `local only \xB7 http://127.0.0.1:${displayPort}` : `${advertisedUrl} \xB7 client + read-only token`;
|
|
3640
4437
|
const registration = initialConfig.Registration ?? {};
|
|
3641
4438
|
const registrationModels = Array.isArray(registration.models) ? registration.models.length : 0;
|
|
3642
4439
|
const registrationUpstreamServices = Array.isArray(registration.upstream_services) ? registration.upstream_services.length : 0;
|
|
3643
4440
|
const registrationSummary = registration.enabled ? `${registrationModels} models / ${registrationUpstreamServices} upstream` : "disabled";
|
|
4441
|
+
const initialManagedKeys = Array.isArray(initialConfig.Auth?.managed_keys) ? initialConfig.Auth.managed_keys : [];
|
|
4442
|
+
const nowMs = Date.now();
|
|
4443
|
+
const initialActiveManagedKeys = initialManagedKeys.filter((record) => {
|
|
4444
|
+
if (record?.revoked_at) {
|
|
4445
|
+
return false;
|
|
4446
|
+
}
|
|
4447
|
+
if (!record?.expires_at) {
|
|
4448
|
+
return true;
|
|
4449
|
+
}
|
|
4450
|
+
const expiresAt = Date.parse(record.expires_at);
|
|
4451
|
+
return !Number.isFinite(expiresAt) || expiresAt > nowMs;
|
|
4452
|
+
}).length;
|
|
4453
|
+
const authSummary = initialConfig.APIKEY || initialManagedKeys.length > 0 ? `configured \xB7 ${initialActiveManagedKeys} active` : "not configured";
|
|
4454
|
+
const securitySummary = !initialConfig.APIKEY && initialManagedKeys.length === 0 && (runtimeMode !== "local" || publicHost) ? "critical" : !initialConfig.APIKEY && initialManagedKeys.length > 0 && initialActiveManagedKeys === 0 ? "warning" : "ok";
|
|
3644
4455
|
const escapedDisplayPort = escapeHtml(displayPort);
|
|
3645
4456
|
const escapedModelsCount = escapeHtml(modelsCount);
|
|
3646
4457
|
const escapedRouterDefault = escapeHtml(routerDefault);
|
|
3647
4458
|
const escapedRuntimeMode = escapeHtml(runtimeMode);
|
|
3648
4459
|
const escapedServiceRole = escapeHtml(serviceRole);
|
|
4460
|
+
const escapedListenerSummary = escapeHtml(`${configuredHost}:${displayPort}${publicHost ? " (public)" : " (local)"}`);
|
|
4461
|
+
const escapedClientConnectionSummary = escapeHtml(clientConnectionSummary);
|
|
3649
4462
|
const escapedRemoteSummary = escapeHtml(remoteSummary);
|
|
3650
4463
|
const escapedRegistrationSummary = escapeHtml(registrationSummary);
|
|
4464
|
+
const escapedAuthSummary = escapeHtml(authSummary);
|
|
4465
|
+
const escapedSecuritySummary = escapeHtml(securitySummary);
|
|
4466
|
+
const escapedLocalUserRoleGuide = escapeHtml(LOCAL_USER_ROLE_GUIDE);
|
|
4467
|
+
const escapedServerMaintainerRoleGuide = escapeHtml(SERVER_MAINTAINER_ROLE_GUIDE);
|
|
4468
|
+
const escapedRemoteClientRoleGuide = escapeHtml(REMOTE_CLIENT_ROLE_GUIDE);
|
|
3651
4469
|
const escapedMinSampleSize = escapeHtml(configuredThresholds.min_sample_size ?? 3);
|
|
3652
4470
|
const escapedCascadeWarnRate = escapeHtml(configuredThresholds.cascade_warn_rate ?? 0.4);
|
|
3653
4471
|
const escapedShadowWarnRate = escapeHtml(configuredThresholds.shadow_warn_rate ?? 0.5);
|
|
3654
4472
|
const escapedLatencyWarnMs = escapeHtml(configuredThresholds.latency_warn_ms ?? 1500);
|
|
3655
|
-
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Claude Trigger Router</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.hero{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(260px,.8fr);gap:1rem;align-items:stretch;margin-bottom:1rem}.hero h2{margin:.2rem 0 .5rem;font-size:1.55rem}.hero-copy{display:flex;flex-direction:column;justify-content:center}.status-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.status-tile{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem;min-width:0}.status-tile strong{display:block;margin-top:.2rem;word-break:break-word}@media (max-width:760px){.hero{grid-template-columns:1fr}.status-grid{grid-template-columns:1fr}}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:1rem;padding:.45rem 0;border-bottom:1px dashed #e5e7eb}.mini-list li:last-child{border-bottom:none}.action-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center;margin-top:.75rem}.management-table{width:100%;margin-top:.75rem}.management-table th,.management-table td{padding:.5rem;border-bottom:1px solid #e5e7eb;font-size:.92rem;vertical-align:top}.alert-list{display:grid;gap:.75rem;margin-top:1rem}.alert{border-radius:12px;padding:.85rem 1rem;border:1px solid}.alert.warn{background:#fff7ed;border-color:#fdba74;color:#9a3412}.alert.critical{background:#fef2f2;border-color:#fca5a5;color:#991b1b}.alert.info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.diff-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-top:.75rem}.diff-chip{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.75rem}.diff-chip strong{display:block;font-size:1rem;margin-top:.2rem}.models-form-grid{display:grid;gap:.75rem;margin-top:.75rem}.model-card{border:1px solid #e5e7eb;border-radius:12px;padding:1rem;background:#fcfcfd}.model-card-header{display:flex;justify-content:space-between;gap:1rem;align-items:center;margin-bottom:.75rem}.model-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.model-card-grid textarea{min-height:84px;resize:vertical}.list-editor{display:grid;gap:.75rem;margin-top:.75rem}.list-item{border:1px solid #e5e7eb;border-radius:12px;padding:.85rem;background:#fcfcfd}.list-item-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.jump-highlight{outline:3px solid #f59e0b;box-shadow:0 0 0 6px rgba(245,158,11,.15);transition:box-shadow .25s ease,outline-color .25s ease}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.control-grid label{display:block;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}.trend-table{width:100%;margin-top:.75rem}.trend-table th,.trend-table td{padding:.45rem;border-bottom:1px solid #e5e7eb;font-size:.92rem}.row{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}input,select,button{font:inherit;padding:.55rem .75rem;border-radius:8px;border:1px solid #d1d5db}button{background:#111827;color:#fff;border-color:#111827;cursor:pointer}table{width:100%;border-collapse:collapse;margin-top:1rem}th,td{text-align:left;padding:.65rem .5rem;border-bottom:1px solid #e5e7eb;vertical-align:top}code,pre{font-family:ui-monospace,SFMono-Regular,monospace}pre{white-space:pre-wrap;background:#0f172a;color:#e2e8f0;padding:1rem;border-radius:12px;overflow:auto}.pill{display:inline-block;padding:.2rem .5rem;border-radius:999px;background:#eef2ff;color:#3730a3;font-size:.8rem}.surface-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin:1rem 0}.surface-tab{background:#fff;color:#1f2328;border-color:#d1d5db}.surface-tab.active{background:#111827;color:#fff;border-color:#111827}.surface-panel[hidden]{display:none}.surface-heading{display:flex;gap:1rem;flex-wrap:wrap;align-items:center;margin-bottom:.75rem}</style></head><body><div class="hero"><div class="panel hero-copy"><h2>\u914D\u7F6E\u4E0E\u72B6\u6001\u5DE5\u4F5C\u53F0</h2><p class="muted">\u67E5\u770B\u5F53\u524D\u8DEF\u7531\u670D\u52A1\u3001\u6A21\u578B\u914D\u7F6E\u548C\u9ED8\u8BA4\u53BB\u5411\uFF1B\u9700\u8981\u6392\u67E5\u65F6\uFF0C\u4E0B\u65B9\u7EF4\u62A4\u8005\u533A\u57DF\u53EF\u7EE7\u7EED\u67E5\u770B Governance Trace\u3001metrics \u548C\u5F52\u6863\u3002</p><div class="action-row"><button id="loadConfigDraftHeroBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="previewConfigDraftHeroBtn" type="button">\u9884\u89C8 compiled models</button><button id="refreshStatusHeroBtn" type="button">\u5237\u65B0\u72B6\u6001</button></div></div><div class="panel"><div class="status-grid"><div class="status-tile"><span class="muted">Service</span><strong id="serviceReadyStatus">ready</strong></div><div class="status-tile"><span class="muted">Port</span><strong id="servicePortStatus">${escapedDisplayPort}</strong></div><div class="status-tile"><span class="muted">Mode</span><strong id="serviceModeStatus">${escapedRuntimeMode}</strong></div><div class="status-tile"><span class="muted">Role</span><strong id="serviceRoleStatus">${escapedServiceRole}</strong></div><div class="status-tile"><span class="muted">Models</span><strong id="modelCountStatus">${escapedModelsCount}</strong></div><div class="status-tile"><span class="muted">Router.default</span><strong id="routerDefaultStatus">${escapedRouterDefault}</strong></div><div class="status-tile"><span class="muted">Remote service</span><strong id="remoteStatusSummary">${escapedRemoteSummary}</strong></div><div class="status-tile"><span class="muted">Registration</span><strong id="registrationStatusSummary">${escapedRegistrationSummary}</strong></div></div></div></div><div class="surface-tabs" role="tablist" aria-label="\u5DE5\u4F5C\u53F0\u5207\u6362"><button id="userSurfaceTab" class="surface-tab active" type="button" role="tab" aria-selected="true" data-surface-target="user">\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</button><button id="maintainerSurfaceTab" class="surface-tab" type="button" role="tab" aria-selected="false" data-surface-target="maintainer">\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</button></div><section id="userSurface" class="surface-panel" data-surface="user"><div class="panel"><div class="surface-heading"><strong>\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u914D\u7F6E\u3001\u6A21\u578B\u3001\u8DEF\u7531\u3001\u670D\u52A1\u72B6\u6001\u4E0E\u4E0B\u4E00\u6B65\u4FDD\u5B58\u52A8\u4F5C\u3002</span></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Routing rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u56F4\u7ED5 SmartRouter \u7EDF\u4E00\u8DEF\u7531\u5F15\u64CE\u7F16\u8F91\u89C4\u5219\u3001\u5019\u9009\u4E0E\u6CBB\u7406\u589E\u5F3A\u517C\u5BB9\u914D\u7F6E</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Routing rules</strong><span class="muted">\u663E\u5F0F\u89C4\u5219\u3001\u8BED\u4E49\u63D0\u793A\u4E0E\u517C\u5BB9\u8F93\u5165</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5F71\u5B50\u6821\u9A8C\u3001\u7EA7\u8054\u4E0E\u89C2\u6D4B\u76F8\u5173\u914D\u7F6E</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div class="alert info"><strong>Models field guide</strong><div class="muted">\u65B0\u914D\u7F6E\u8BF7\u4F7F\u7528\u5165\u53E3\u5B57\u6BB5\uFF1Aid / api / key / interface / model / thinking / metadata\uFF1Bapi_key / api_base_url / protocol \u4EC5\u4F5C\u4E3A\u65E7\u914D\u7F6E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div><div id="modelsFormGrid" class="models-form-grid"><div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div></div><textarea id="configDraftEditor" aria-label="JSON config draft" style="width:100%;min-height:240px;margin-top:.75rem;padding:.75rem;border-radius:12px;border:1px solid #d1d5db;font:12px/1.5 ui-monospace,SFMono-Regular,monospace" spellcheck="false" placeholder='{"Models":[{"id":"sonnet","api":"https://...","key":"sk-...","interface":"openai","model":"anthropic/claude-sonnet-4","thinking":"auto","metadata":{"vendor_hint":"openrouter"}}],"Router":{"default":"sonnet"}}'></textarea><div class="muted">JSON \u8349\u7A3F\u540C\u6837\u5EFA\u8BAE\u53EA\u5199\u5165\u53E3\u5B57\u6BB5\uFF1B\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u5F52\u4E00\uFF0C\u65E7\u5B57\u6BB5\u522B\u540D\u65E0\u9700\u624B\u52A8\u8865\u5145\u3002</div><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / SmartRouter / Governance\uFF08shadow/cascade\uFF09\u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Compatibility profile</th><th>Dispatch format</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="8" class="muted">Loading model map...</td></tr></tbody></table></div></div></div></div></section><section id="maintainerSurface" class="surface-panel" data-surface="maintainer" hidden><div class="panel"><div class="surface-heading"><strong>\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u8FD0\u884C\u89C2\u6D4B\u3001Governance Trace\u3001metrics\u3001\u5F52\u6863\u4E0E\u7EF4\u62A4\u64CD\u4F5C\u3002</span></div><div class="row"><strong>\u7EF4\u62A4\u8005\u89C2\u6D4B</strong><span class="muted">\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4 Governance Trace\uFF0C\u5E76\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u6307\u6807\u3002</span></div><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/health</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Health</span><strong>-</strong></div><div class="stat"><span class="muted">Recent traces</span><strong>-</strong></div><div class="stat"><span class="muted">Sticky hit rate</span><strong>-</strong></div><div class="stat"><span class="muted">Cascade rate</span><strong>-</strong></div><div class="stat"><span class="muted">Shadow rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment rate</span><strong>-</strong></div><div class="stat"><span class="muted">Avg latency</span><strong>-</strong></div></div><div class="subpanel"><div class="row"><strong>Anomaly alerts</strong><span class="muted">\u68C0\u6D4B\u8FD1\u671F\u6CBB\u7406\u5F02\u5E38\u4E0E\u7A81\u589E</span></div><div id="healthSummary" class="alert info"><strong>Health pending</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u5065\u5EB7\u6458\u8981\u52A0\u8F7D</div></div><div id="anomalyList" class="alert-list"><div class="alert info"><strong>No alerts yet</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u6307\u6807\u52A0\u8F7D</div></div></div></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${escapedMinSampleSize}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${escapedCascadeWarnRate}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${escapedShadowWarnRate}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${escapedLatencyWarnMs}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Trend detail</strong><span class="muted">\u6BCF\u4E2A bucket \u7684\u8BE6\u7EC6\u547D\u4E2D\u7387</span></div><table id="trendTable" class="trend-table"><thead><tr><th>Bucket</th><th>Traces</th><th>Sticky</th><th>Cascade</th><th>Shadow</th><th>Alignment</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading...</td></tr></tbody></table></div></div><table id="traceTable"><thead><tr><th>Request</th><th>Session</th><th>Final Model</th><th>Reasons</th><th>Latency</th><th>Inspect</th></tr></thead><tbody><tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Trace Detail</strong><span id="detailHint" class="muted">\u70B9\u51FB\u4E0A\u8868\u4E2D\u7684 View \u67E5\u770B\u8BE6\u60C5</span></div><pre id="traceDetail">{}</pre></div><div class="panel"><div class="row"><strong>Snapshot Management</strong><span class="muted">\u67E5\u770B\u5BFC\u51FA\u5386\u53F2\u3001\u5B9A\u65F6\u4EFB\u52A1\uFF0C\u5E76\u624B\u52A8\u521B\u5EFA\u5FEB\u7167</span></div><div class="action-row"><select id="snapshotFormat"><option value="json">snapshot json</option><option value="csv">snapshot csv</option></select><button id="createSnapshotBtn" type="button">\u751F\u6210\u5FEB\u7167</button><span id="snapshotStatus" class="muted">\u5C1A\u672A\u521B\u5EFA\u5FEB\u7167</span></div><table id="exportTable" class="management-table"><thead><tr><th>Export</th><th>Kind</th><th>Format</th><th>Created</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading exports...</td></tr></tbody></table><table id="scheduleTable" class="management-table"><thead><tr><th>Schedule</th><th>Interval</th><th>Format</th><th>Last run</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading schedules...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Archive Management</strong><span class="muted">\u6D4F\u89C8\u538B\u7F29\u5F52\u6863\u5E76\u67E5\u770B\u5206\u9875\u7ED3\u679C</span></div><div class="action-row"><input id="archiveDate" placeholder="YYYY-MM-DD"><input id="archivePage" placeholder="page" value="1"><input id="archivePageSize" placeholder="pageSize" value="5"><button id="loadArchivesBtn" type="button">\u52A0\u8F7D\u5F52\u6863</button><span id="archiveStatus" class="muted">\u5C1A\u672A\u52A0\u8F7D\u5F52\u6863</span></div><table id="archiveTable" class="management-table"><thead><tr><th>Archive</th><th>Range</th><th>Count</th><th>Compressed</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading archives...</td></tr></tbody></table></div><div class="panel"><p>\u5176\u4ED6\u7BA1\u7406 API\uFF1A</p><ul><li><code>GET /api/config</code> \u2014 \u8BFB\u53D6\u5F53\u524D\u914D\u7F6E</li><li><code>GET /api/models/compiled</code> \u2014 \u67E5\u770B Models \u7F16\u8BD1\u540E\u7684\u5185\u90E8 provider / model \u6620\u5C04</li><li><code>POST /api/models/compiled/preview</code> \u2014 \u7528\u914D\u7F6E\u8349\u7A3F\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u5199\u56DE\u6587\u4EF6</li><li><code>POST /api/config</code> \u2014 \u4FDD\u5B58\u914D\u7F6E</li><li><code>GET /api/transformers</code> \u2014 \u67E5\u770B\u5DF2\u52A0\u8F7D transformer</li><li><code>POST /api/restart</code> \u2014 \u91CD\u542F\u670D\u52A1</li><li><code>GET /api/governance/archives</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5F52\u6863\u5217\u8868</li><li><code>GET /api/governance/archives/:file</code> \u2014 \u67E5\u770B\u5F52\u6863\u5185 traces</li><li><code>POST /api/governance/archives/:file/delete</code> \u2014 \u5220\u9664\u6307\u5B9A\u5F52\u6863</li><li><code>GET /api/governance/health</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5065\u5EB7\u6458\u8981</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div></section><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const serviceReadyStatus=document.getElementById('serviceReadyStatus');const servicePortStatus=document.getElementById('servicePortStatus');const serviceModeStatus=document.getElementById('serviceModeStatus');const serviceRoleStatus=document.getElementById('serviceRoleStatus');const remoteStatusSummary=document.getElementById('remoteStatusSummary');const registrationStatusSummary=document.getElementById('registrationStatusSummary');const modelCountStatus=document.getElementById('modelCountStatus');const routerDefaultStatus=document.getElementById('routerDefaultStatus');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const metricsGrid=document.getElementById('metricsGrid');const bucketGrid=document.getElementById('bucketGrid');const bucketHint=document.getElementById('bucketHint');const routeRanking=document.getElementById('routeRanking');const modelRanking=document.getElementById('modelRanking');const intentRanking=document.getElementById('intentRanking');const healthSummary=document.getElementById('healthSummary');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');const surfaceTabs=Array.from(document.querySelectorAll('[data-surface-target]'));const surfacePanels=Array.from(document.querySelectorAll('[data-surface]'));let currentDraftConfig={};let knownModelIds=[];let activeValidationHighlight=null;const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94\u8DEF\u7531\u89C4\u5219\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u589E\u5F3A\u4E0E\u6821\u9A8C\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','SmartRouter.sticky.alignment','SmartRouter.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&','<':'<','>':'>','"':'"' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function setActiveSurface(surfaceName){ surfacePanels.forEach((panel)=>{ panel.hidden=panel.dataset.surface !== surfaceName; }); surfaceTabs.forEach((tab)=>{ const active=tab.dataset.surfaceTarget === surfaceName; tab.classList.toggle('active',active); tab.setAttribute('aria-selected', active ? 'true' : 'false'); });}function inferProviderTemplateKey(model){ const explicit=String(model?.provider_template || '').trim(); if(explicit && modelProviderTemplates[explicit]){ return explicit; } const api=String(model?.api || model?.api_base_url || '').trim().toLowerCase(); const modelInterface=String(model?.interface || model?.protocol || '').trim().toLowerCase(); const exactMatch=Object.entries(modelProviderTemplates).find(([,item])=>String(item.api || '').trim().toLowerCase()===api && String(item.interface || '').trim().toLowerCase()===modelInterface); if(exactMatch){ return exactMatch[0]; } if(api.includes('api.anthropic.com/v1/messages') || modelInterface === 'anthropic'){ return 'anthropic'; } if(api.includes('openrouter.ai')){ return 'openrouter'; } if(api.includes('deepseek.com')){ return 'deepseek'; } if(api.includes('siliconflow.cn')){ return 'siliconflow'; } if(api.includes('api.openai.com')){ return 'openai-compatible'; } return '';}function getProviderTemplateContext(model){ const templateKey=inferProviderTemplateKey(model) || defaultProviderTemplateKey; return { templateKey, template:modelProviderTemplates[templateKey] || modelProviderTemplates[defaultProviderTemplateKey] || {} };}function createDraftModelFromTemplate(templateKey){ const resolvedKey=(templateKey && modelProviderTemplates[templateKey]) ? templateKey : defaultProviderTemplateKey; const template=modelProviderTemplates[resolvedKey] || {}; return { provider_template:resolvedKey, id:template.suggested_id || '', api:template.api || '', interface:template.interface || 'openai', model:template.default_model || '', thinking:template.default_thinking || 'auto' };}function getModelIdSuggestionsMarkup(idPrefix){ return '<datalist id="'+idPrefix+'">'+knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join('')+'</datalist>';}function resolvePresetModelId(seed){ const source=String(seed || '').trim().toLowerCase(); if(!source || !knownModelIds.length){ return seed; } if(knownModelIds.includes(seed)){ return seed; } const ranked=knownModelIds.map((modelId)=>{ const target=String(modelId || '').toLowerCase(); let score=0; if(target===source){ score+=100; } if(target.includes(source) || source.includes(target)){ score+=40; } source.split(/[^a-z0-9]+/).filter(Boolean).forEach((part)=>{ if(target.includes(part)){ score+=Math.min(part.length * 4, 24); } }); return { modelId, score }; }).filter((item)=>item.score>0).sort((a,b)=>b.score-a.score || a.modelId.localeCompare(b.modelId)); return ranked.length ? ranked[0].modelId : seed;}function getTriggerPatternValidationHint(pattern){ if((pattern?.type || 'exact') === 'regex'){ return pattern?.pattern ? { level:'ok', message:'regex pattern \u5DF2\u914D\u7F6E' } : { level:'warn', message:'regex \u6A21\u5F0F\u9700\u8981\u586B\u5199 pattern' }; } return Array.isArray(pattern?.keywords) && pattern.keywords.some((keyword)=>String(keyword || '').trim()) ? { level:'ok', message:'exact keywords \u5DF2\u914D\u7F6E' } : { level:'warn', message:'exact \u6A21\u5F0F\u81F3\u5C11\u9700\u8981\u4E00\u4E2A keyword' };}function getDraftSmartRouterConfig(config){ const smart={ ...((config && config.SmartRouter) || {}) }; const smartExplicit=config && Object.prototype.hasOwnProperty.call(config,'SmartRouter'); const legacyIntentEnabled=Boolean(config?.TriggerRouter?.llm_intent_recognition); const legacyIntentModel=config?.TriggerRouter?.intent_model || ''; if(!smart.analysis_scope && config?.TriggerRouter?.analysis_scope){ smart.analysis_scope=config.TriggerRouter.analysis_scope; } if((!Array.isArray(smart.rules) || !smart.rules.length) && Array.isArray(config?.TriggerRouter?.rules)){ smart.rules=config.TriggerRouter.rules; } if(!smart.semantic && (config?.Governance?.semantic || config?.TriggerRouter?.llm_intent_recognition)){ smart.semantic={ ...((config && config.Governance && config.Governance.semantic) || {}) }; if(config?.TriggerRouter?.llm_intent_recognition){ smart.semantic.enabled=true; smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || config.TriggerRouter.intent_model || ''; } } if(!smart.sticky && config?.Governance?.sticky){ smart.sticky={ ...(config.Governance.sticky || {}) }; } if(!smartExplicit && !smart.enabled && (config?.TriggerRouter?.enabled || smart.rules?.length || smart.router_model || smart.candidates?.length || smart.semantic || smart.sticky)){ smart.enabled=true; } if(smart.enabled){ smart.analysis_scope=smart.analysis_scope || 'last_message'; smart.semantic={ ...(smart.semantic || {}) }; smart.semantic.enabled=smart.semantic.enabled !== undefined ? smart.semantic.enabled : true; smart.semantic.threshold=smart.semantic.threshold !== undefined ? smart.semantic.threshold : 0.2; if(legacyIntentEnabled){ smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || legacyIntentModel; } smart.sticky={ ...(smart.sticky || {}) }; smart.sticky.enabled=smart.sticky.enabled !== undefined ? smart.sticky.enabled : true; smart.sticky.alignment={ ...((smart.sticky && smart.sticky.alignment) || {}) }; smart.sticky.alignment.enabled=smart.sticky.alignment.enabled !== undefined ? smart.sticky.alignment.enabled : true; smart.sticky.alignment.summarizer_model=smart.sticky.alignment.summarizer_model || smart.router_model || config?.Router?.default || legacyIntentModel || ''; } return smart;}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const smart=getDraftSmartRouterConfig(config); const triggerRules=Array.isArray(smart?.rules) ? smart.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(smart?.candidates) ? smart.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, smart?.router_model, smart?.sticky?.alignment?.summarizer_model, smart?.semantic?.classifier_model, config?.Governance?.shadow?.verifier_model].filter(v=>typeof v === 'string' && v.trim()).length + triggerRules.filter(rule=>rule?.model).length + smartCandidates.filter(item=>item?.model).length + cascadeLevels.reduce((sum,level)=>sum + (level?.from ? 1 : 0) + (level?.to ? 1 : 0), 0); draftSummaryGrid.innerHTML=[ ['Models', models.length], ['Routing rules', triggerRules.length], ['Patterns', patternCount], ['Smart candidates', smartCandidates.length], ['Cascade levels', cascadeLevels.length], ['Model refs', modelRefCount] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function updateStatusSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; modelCountStatus.textContent=String(models.length); routerDefaultStatus.textContent=config?.Router?.default || '-';}function renderDraftValidation(errors,warnings,issueReport){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; const contractIssues=Array.isArray(issueReport?.issues) ? issueReport.issues : []; if(!errorList.length && !warningList.length && !contractIssues.length){ draftValidationList.innerHTML='<div class="alert info"><strong>No validation issues</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u672A\u53D1\u73B0\u96C6\u4E2D\u5C55\u793A\u7684\u95EE\u9898</div></div>'; return; } const extractPath=(text)=>{ const match=String(text).match(/^(Models(?:\\[[0-9]+\\])?(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Router(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|TriggerRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|SmartRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Governance(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?)/); return match ? match[1] : ''; }; const sourceItems=contractIssues.length ? contractIssues.map(item=>({ text:String(item.message || ''), severity:item.severity==='error' ? 'error' : 'warning', path:item.path || '', action:item.action || '' })) : [...errorList.map(item=>({ text:String(item), severity:'error', path:'', action:'' })), ...warningList.map(item=>({ text:String(item), severity:'warning', path:'', action:'' }))]; const grouped=sourceItems.reduce((acc,item)=>{ const text=item.text; const path=item.path || extractPath(text); const bucket=path.startsWith('Models') || text.startsWith('Models') ? 'Models' : path.startsWith('Router') || text.startsWith('Router') ? 'Router' : path.startsWith('TriggerRouter') || text.startsWith('TriggerRouter') ? 'SmartRouter' : path.startsWith('SmartRouter') || text.startsWith('SmartRouter') ? 'SmartRouter' : (path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic') || text.startsWith('Governance.sticky') || text.startsWith('Governance.semantic')) ? 'SmartRouter' : path.startsWith('Governance') || text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path, severity:item.severity, action:item.action || '' }); return acc; }, {}); const errorCount=contractIssues.length ? contractIssues.filter(item=>item.severity==='error').length : errorList.length; const warningCount=contractIssues.length ? contractIssues.filter(item=>item.severity!=='error').length : warningList.length; const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorCount)+' errors / '+esc(warningCount)+' warnings</span></div><div class="muted">'+(errorCount ? '\u8BF7\u4F18\u5148\u4FEE\u590D errors\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u63A5\u53D7 warnings\u3002' : '\u5F53\u524D\u65E0\u963B\u65AD\u9519\u8BEF\uFF0C\u53EF\u6309\u9700\u5904\u7406 warnings\u3002')+'</div></div>'; draftValidationList.innerHTML=summary + Object.entries(grouped).map(([bucket,items])=>{ const hasError=items.some(item=>item.severity==='error'); const levelClass=hasError ? 'warn' : 'info'; const actionLabel=hasError ? 'repair first' : 'review before save'; return '<div class="alert '+levelClass+'"><div class="row"><strong>'+esc(bucket)+'</strong><span class="pill">'+esc(items.length)+' issues</span></div><div class="muted">'+esc(actionLabel)+'</div><div>'+items.slice(0,4).map(item=>'<div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+'<span class="pill">'+esc(item.severity==='error' ? 'error' : 'warning')+'</span> '+esc(item.text)+(item.action ? ('<div class="muted">Action: '+esc(item.action)+'</div>') : '')+'</div>').join('')+'</div></div>'; }).join('');}function getCapabilityWarningActionLabel(code){ if(code==='thinking_ignored'){ return '\u79FB\u9664 thinking'; } if(code==='tools_text_fallback' || code==='images_text_fallback'){ return '\u6062\u590D\u9ED8\u8BA4 capability'; } return '';}function renderCapabilityWarnings(report){ const entries=Array.isArray(report?.entries) ? report.entries : []; if(!entries.length){ capabilityWarningsList.innerHTML='<div class="alert info"><strong>No capability warnings</strong><div class="muted">\u5F53\u524D compiled models \u672A\u53D1\u73B0\u9700\u8981\u989D\u5916\u63D0\u793A\u7684\u80FD\u529B\u964D\u7EA7</div></div>'; return; } const summary=report?.summary || {}; capabilityWarningsList.innerHTML='<div class="alert info"><strong>Capability warning summary</strong><div class="muted">warn '+esc(summary.warn ?? 0)+' / info '+esc(summary.info ?? 0)+' / total '+esc(summary.total ?? entries.length)+'</div></div>' + entries.map(item=>{ const actionLabel=getCapabilityWarningActionLabel(item.code); return '<div class="alert '+esc(item.level === 'warn' ? 'warn' : 'info')+'"><div class="row"><strong>'+esc(item.code || item.level || 'warning')+'</strong><span class="pill">'+esc(item.modelId || '-').trim()+'</span></div><div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+esc(item.message || '')+'</div>'+(actionLabel ? ('<div class="row" style="margin-top:.5rem"><button type="button" data-apply-warning-path="'+esc(item.path || '')+'" data-apply-warning-code="'+esc(item.code || '')+'">'+esc(actionLabel)+'</button></div>') : '')+'</div>'; }).join('');}function findValidationTarget(path){ if(!path){ return null; } if(path.startsWith('Models')){ return modelsFormGrid; } if(path === 'Router.default'){ return draftRouterDefault; } if(path.startsWith('TriggerRouter.intent_model')){ return triggerIntentModel; } if(path.startsWith('TriggerRouter.rules[')){ return triggerRulesList; } if(path.startsWith('SmartRouter.router_model')){ return smartRouterModel; } if(path.startsWith('SmartRouter.candidates[')){ return smartCandidatesList; } if(path.startsWith('Governance.cascade.levels[')){ return governanceCascadeLevelsList; } if(path.startsWith('Governance.sticky.alignment')){ return governanceSummarizerModel; } if(path.startsWith('Governance.semantic')){ return governanceClassifierModel; } if(path.startsWith('Governance.shadow')){ return governanceVerifierModel; } if(path.startsWith('Governance')){ return governanceEnabled; } return null;}function jumpToValidationPath(path){ const target=findValidationTarget(path); if(!target || typeof target.scrollIntoView !== 'function'){ return; } if(activeValidationHighlight && activeValidationHighlight.classList){ activeValidationHighlight.classList.remove('jump-highlight'); } target.scrollIntoView({ behavior:'smooth', block:'center' }); if(target.classList){ target.classList.add('jump-highlight'); activeValidationHighlight=target; setTimeout(()=>{ if(target.classList){ target.classList.remove('jump-highlight'); if(activeValidationHighlight===target){ activeValidationHighlight=null; } } }, 1800); } if(typeof target.focus === 'function'){ target.focus({ preventScroll:true }); }}function renderDraftPresetModeHint(){ const overwriteMode=draftPresetMode.value === 'replace'; draftPresetModeHint.textContent=overwriteMode ? 'overwrite \u4F1A\u91CD\u7F6E SmartRouter / Governance \u76F8\u5173\u8868\u5355\uFF0C\u518D\u5E94\u7528\u9884\u8BBE' : 'append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145 SmartRouter / Governance \u76F8\u5173\u5B57\u6BB5';}function deriveActualAffectedAreas(preview){ const areas=new Set(); const diff=preview?.diff || {}; const impact=preview?.referenceImpact || {}; if((diff.providerChanges || []).length || (diff.modelChanges || []).length){ areas.add('Models'); } (impact.entries || []).forEach((entry)=>{ const path=String(entry.path || ''); if(path.startsWith('Router.')){ areas.add('Router'); } else if(path.startsWith('TriggerRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.')){ areas.add('Governance'); } }); return Array.from(areas);}function renderDraftPreviewMeta(meta){ if(!meta){ draftPreviewMeta.innerHTML='<div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div>'; return; } draftPreviewMeta.innerHTML='<div class="alert info"><strong>'+esc(meta.title || 'Preset dry-run')+'</strong><div>'+esc(meta.description || '')+'</div><div class="muted">\u6A21\u5F0F\uFF1A'+esc(meta.mode || '-')+' \xB7 \u9884\u8BBE\u58F0\u660E\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((meta.affects || []).join(' / ') || '-')</div><div class="muted">\u5B9E\u9645\u9884\u89C8\u547D\u4E2D\u533A\u57DF\uFF1A'+esc((meta.actualAffects || []).join(' / ') || '-')</div></div>';}function renderDraftPresetGuide(){ draftPresetList.innerHTML=Object.entries(draftPresets).map(([key,preset])=>'<div class="alert info"><strong>'+esc(preset.label || key)+'</strong><div>'+esc(preset.description || '')+'</div><div class="muted">\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((preset.affects || []).join(' / '))+'</div></div>').join('');}function updateTopLevelModelSuggestionLists(){ const markup=knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join(''); [topLevelTriggerIntentSuggestions,topLevelSmartRouterSuggestions,topLevelGovernanceSummarizerSuggestions,topLevelGovernanceClassifierSuggestions,topLevelGovernanceVerifierSuggestions].forEach(node=>{ if(node){ node.innerHTML=markup; } });}function renderModelsForm(models){ const list=Array.isArray(models) ? models : []; draftModelsCount.value=String(list.length); if(!list.length){ modelsFormGrid.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div>'; return; } modelsFormGrid.innerHTML=list.map((model,index)=>{ const templateContext=getProviderTemplateContext(model); const template=templateContext.template; return '<div class="model-card" data-model-card="'+index+'">' + '<div class="model-card-header"><strong>Model #'+(index+1)+'</strong><button type="button" data-remove-model="'+index+'">\u5220\u9664</button></div>' + '<div class="model-card-grid">' + '<div><label>Provider template</label><div class="row"><select data-field="provider_template" data-index="'+index+'"><option value="">custom</option>'+Object.entries(modelProviderTemplates).map(([key,item])=>'<option value="'+esc(key)+'"'+(model.provider_template === key ? ' selected' : '')+'>'+esc(item.label)+'</option>').join('')+'</select><button type="button" data-apply-template="'+index+'">\u5E94\u7528</button></div></div>' + '<div><label>ID</label><input data-field="id" data-index="'+index+'" value="'+esc(model.id || '')+'" placeholder="'+esc(template.suggested_id || 'sonnet')+'"><div class="muted">Router.default \u548C\u8DEF\u7531\u89C4\u5219\u5F15\u7528\u8FD9\u4E2A model id\uFF1B\u5EFA\u8BAE\u6A21\u677F\uFF1A'+esc(template.label || templateContext.templateKey || 'custom')+'</div></div>' + '<div><label>Interface</label><select data-field="interface" data-index="'+index+'"><option value="openai"'+(((model.interface || model.protocol || 'openai') === 'openai') ? ' selected' : '')+'>openai</option><option value="anthropic"'+(((model.interface || model.protocol) === 'anthropic') ? ' selected' : '')+'>anthropic</option></select><div class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 interface\uFF1B\u65E7 protocol \u4F1A\u81EA\u52A8\u8BFB\u53D6\u4E3A\u517C\u5BB9\u503C\u3002</div></div>' + '<div><label>Model</label><input data-field="model" data-index="'+index+'" list="modelSuggestions'+index+'" value="'+esc(model.model || '')+'" placeholder="'+esc(template.default_model || 'anthropic/claude-sonnet-4')+'"><datalist id="modelSuggestions'+index+'">'+((template.model_examples || []).map(item=>'<option value="'+esc(item)+'"></option>').join(''))+'</datalist><div class="muted">\u4E0A\u6E38\u771F\u5B9E\u6A21\u578B\u540D\uFF0C\u4F8B\u5982\uFF1A'+esc((template.model_examples || ['anthropic/claude-sonnet-4']).join(' / '))+'</div></div>' + '<div><label>API</label><input data-field="api" data-index="'+index+'" value="'+esc(model.api || model.api_base_url || '')+'" placeholder="'+esc(template.api || 'https://...')+'"><div class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 api\uFF1B\u65E7 api_base_url \u4EC5\u7528\u4E8E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div>' + '<div><label>Key</label><input data-field="key" data-index="'+index+'" value="'+esc(model.key || model.api_key || '')+'" placeholder="'+esc(template.key_placeholder || 'sk-...')+'"><div class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 key\uFF1B\u65E7 api_key \u4EC5\u7528\u4E8E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div>' + '<div><label>Thinking</label><select data-field="thinking_profile" data-index="'+index+'"><option value="">default</option><option value="off"'+(((model.thinking === 'off') || model.thinking?.mode === 'off') ? ' selected' : '')+'>off</option><option value="auto"'+(((model.thinking === 'auto') || model.thinking?.mode === 'auto') ? ' selected' : '')+'>auto</option><option value="on"'+(((model.thinking === 'on') || (model.thinking?.mode === 'on' && !model.thinking?.effort)) ? ' selected' : '')+'>on</option><option value="low"'+(((model.thinking === 'low') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'low' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>low</option><option value="medium"'+(((model.thinking === 'medium') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'medium' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>medium</option><option value="high"'+(((model.thinking === 'high') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'high' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>high</option><option value="custom"'+(((typeof model.thinking === 'object') && model.thinking && model.thinking.budget_tokens) ? ' selected' : '')+'>custom</option></select></div>' + '<div><label>Thinking mode</label><select data-field="thinking_mode" data-index="'+index+'"><option value="">default</option><option value="off"'+(model.thinking?.mode === 'off' ? ' selected' : '')+'>off</option><option value="auto"'+(model.thinking?.mode === 'auto' ? ' selected' : '')+'>auto</option><option value="on"'+(model.thinking?.mode === 'on' ? ' selected' : '')+'>on</option></select></div>' + '<div><label>Thinking effort</label><select data-field="thinking_effort" data-index="'+index+'"><option value="">default</option><option value="low"'+(model.thinking?.effort === 'low' ? ' selected' : '')+'>low</option><option value="medium"'+(model.thinking?.effort === 'medium' ? ' selected' : '')+'>medium</option><option value="high"'+(model.thinking?.effort === 'high' ? ' selected' : '')+'>high</option></select></div>' + '<div><label>Thinking budget</label><input data-field="thinking_budget_tokens" data-index="'+index+'" value="'+esc(model.thinking?.budget_tokens || '')+'" placeholder="1024"></div>' + '<div><label>Vendor hint</label><input data-field="vendor_hint" data-index="'+index+'" value="'+esc(model.metadata?.vendor_hint || '')+'" placeholder="'+esc(template.vendor_hint || 'openrouter')+'"></div>' + '<div><label>Reasoning support</label><select data-field="supports_reasoning" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_reasoning === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_reasoning === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Tool support</label><select data-field="supports_tools" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_tools === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_tools === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Image support</label><select data-field="supports_images" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_images === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_images === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div style="grid-column:1/-1"><label>Metadata (advanced JSON)</label><textarea data-field="metadata" data-index="'+index+'" placeholder="{\\"label\\":\\"Balanced profile\\"}">'+esc(model.metadata ? JSON.stringify(model.metadata, null, 2) : '')+'</textarea><div class="muted">\u666E\u901A capability \u5EFA\u8BAE\u4F18\u5148\u4F7F\u7528\u4E0A\u9762\u7684\u663E\u5F0F\u5B57\u6BB5\uFF1B\u8FD9\u91CC\u4FDD\u7559\u7ED9\u9AD8\u7EA7\u6269\u5C55\u5143\u6570\u636E\u3002</div></div>' + '</div>' + '</div>'; }).join('');}function extractModelsFromForm(){ const cards=Array.from(modelsFormGrid.querySelectorAll('[data-model-card]')); return cards.map((card,index)=>{ const read=(field)=>card.querySelector('[data-field="'+field+'"][data-index="'+index+'"]'); const providerTemplate=(read('provider_template')?.value || '').trim(); const metadataRaw=(read('metadata')?.value || '').trim(); let metadata; if(metadataRaw){ metadata=JSON.parse(metadataRaw); } else { metadata={}; } const thinkingProfile=(read('thinking_profile')?.value || '').trim(); const vendorHint=(read('vendor_hint')?.value || '').trim(); const supportsReasoning=(read('supports_reasoning')?.value || '').trim(); const supportsTools=(read('supports_tools')?.value || '').trim(); const supportsImages=(read('supports_images')?.value || '').trim(); const thinking={}; const mode=(read('thinking_mode')?.value || '').trim(); const effort=(read('thinking_effort')?.value || '').trim(); const budget=(read('thinking_budget_tokens')?.value || '').trim(); if(mode) thinking.mode=mode; if(effort) thinking.effort=effort; if(budget) thinking.budget_tokens=Number(budget); const model={ id:(read('id')?.value || '').trim(), api:(read('api')?.value || '').trim(), key:(read('key')?.value || '').trim(), interface:(read('interface')?.value || '').trim(), model:(read('model')?.value || '').trim(), }; if(vendorHint){ metadata.vendor_hint=vendorHint; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'vendor_hint')){ delete metadata.vendor_hint; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); const smartCandidates=extractSmartCandidatesFromForm(); const smartRouterEnabled=Boolean(smartEnabled.checked || triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim() || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim()); if(smartRouterEnabled){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: true, analysis_scope: triggerAnalysisScope.value || payload.SmartRouter?.analysis_scope || 'last_message', router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined, rules: triggerRules, semantic:(governanceSemanticEnabled.checked || triggerIntentEnabled.checked || governanceClassifierModel.value.trim() || triggerIntentModel.value.trim()) ? { ...(((payload.SmartRouter || {}).semantic) || {}), enabled:Boolean(governanceSemanticEnabled.checked || triggerIntentEnabled.checked), mode:'classifier', classifier_model: governanceClassifierModel.value.trim() || triggerIntentModel.value.trim() } : undefined, sticky:(governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim()) ? { ...(((payload.SmartRouter || {}).sticky) || {}), enabled:true, alignment:{ ...((((payload.SmartRouter || {}).sticky || {}).alignment) || {}), enabled:Boolean(governanceAlignmentEnabled.checked), summarizer_model: governanceSummarizerModel.value.trim() } } : undefined }; } else { delete payload.SmartRouter; } delete payload.TriggerRouter; const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const smart=getDraftSmartRouterConfig(config); const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(smart.enabled); triggerIntentEnabled.checked=Boolean(smart.semantic?.enabled && smart.semantic?.mode === 'classifier'); triggerAnalysisScope.value=smart.analysis_scope || 'last_message'; triggerIntentModel.value=smart.semantic?.classifier_model || trigger.intent_model || ''; renderTriggerRulesList(smart.rules || trigger.rules || []); smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(smart.sticky?.alignment?.enabled); governanceSummarizerModel.value=smart.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(smart.semantic?.enabled); governanceClassifierModel.value=smart.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u540C\u6B65 Models \u8868\u5355\u5230 JSON \u8349\u7A3F'; } catch (error) { draftPreviewStatus.textContent='\u540C\u6B65\u5931\u8D25\uFF1A'+error.message; }}function applyReferenceSuggestion(path,modelId){ if(!modelId){ return; } if(path==='Router.default'){ draftRouterDefault.value=modelId; syncDraftEditorFromForm(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 Router.default'; return; } const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const pathMatch=path.match(/^([^.[]+)(?:.(.+))?$/); if(!pathMatch){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\uFF1A'+path; return; } const tokens=path.replace(/[(d+)]/g,'.$1').split('.'); let cursor=payload; for(let i=0;i<tokens.length-1;i++){ const token=tokens[i]; const nextToken=tokens[i+1]; if(cursor[token] === undefined){ cursor[token]=String(Number(nextToken))===nextToken ? [] : {}; } cursor=cursor[token]; } cursor[tokens[tokens.length-1]]=modelId; currentDraftConfig=payload; if(payload.Router?.default){ draftRouterDefault.value=payload.Router.default; } renderConfigControlForms(payload); configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyCapabilityWarningSuggestion(path,code){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const tokens=String(path || '').replace(/[(d+)]/g,'.$1').split('.').filter(Boolean); if(!tokens.length){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } let cursor=payload; for(let i=0;i<tokens.length-1;i++){ if(cursor == null){ break; } cursor=cursor[tokens[i]]; } const lastToken=tokens[tokens.length-1]; if(code==='thinking_ignored'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } } else if(code==='tools_text_fallback' || code==='images_text_fallback'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } if(cursor && !Object.keys(cursor).length){ const parentTokens=tokens.slice(0,-1); const maybeMetadataKey=parentTokens[parentTokens.length-1]; if(maybeMetadataKey==='metadata'){ let parentCursor=payload; for(let i=0;i<parentTokens.length-1;i++){ if(parentCursor == null){ break; } parentCursor=parentCursor[parentTokens[i]]; } if(parentCursor && Object.prototype.hasOwnProperty.call(parentCursor,'metadata')){ delete parentCursor.metadata; } } } } else { draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528 warning \u4FEE\u6B63\uFF1A'+code+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function renderCompiledDiff(diff){ const summary=diff?.summary || {}; compiledDiffSummary.innerHTML=[ ['Added providers', summary.addedProviders ?? 0], ['Removed providers', summary.removedProviders ?? 0], ['Changed providers', summary.changedProviders ?? 0], ['Added models', summary.addedModels ?? 0], ['Removed models', summary.removedModels ?? 0], ['Changed models', summary.changedModels ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const rows=[ ...((diff?.providerChanges || []).map(item=>({ scope:'provider', key:item.name, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ...((diff?.modelChanges || []).map(item=>({ scope:'model', key:item.modelId, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ]; compiledDiffTableBody.innerHTML=rows.length ? rows.map(item=>'<tr>' + '<td>'+esc(item.scope)+'</td>' + '<td>'+esc(item.type)+'</td>' + '<td><code>'+esc(item.key)+'</code></td>' + '<td>'+esc(item.fields.join(', ') || '-')+'</td>' + '<td><code>'+esc(item.target.providerName || item.target.name || '-')+'</code><div class="muted">'+esc(item.target.modelName || (item.target.models || []).join(', ') || '-')}</div></td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled registry changes</td></tr>';}function renderReferenceImpact(impact){ const summary=impact?.summary || {}; referenceImpactSummary.innerHTML=[ ['Total refs', summary.total ?? 0], ['modelId refs', summary.modelIdRefs ?? 0], ['Legacy refs', summary.legacyRefs ?? 0], ['Valid modelIds', summary.validModelIds ?? 0], ['Missing modelIds', summary.missingModelIds ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const entries=impact?.entries || []; referenceImpactTableBody.innerHTML=entries.length ? entries.map(item=>'<tr>' + '<td><code>'+esc(item.path)+'</code></td>' + '<td><code>'+esc(item.value)+'</code></td>' + '<td>'+esc(item.referenceType)+'</td>' + '<td>'+esc(item.status)+'</td>' + '<td><code>'+esc(item.resolvedTarget?.providerName || '-')+'</code><div class="muted">'+esc(item.resolvedTarget?.modelName || '-')}</div></td>' + '<td>'+((item.suggestions || []).length ? item.suggestions.map(s=>'<div><code>'+esc(s.modelId)+'</code><div class="muted">'+esc(s.modelName || '-')+'</div><button type="button" data-apply-reference-path="'+esc(item.path)+'" data-apply-reference-model="'+esc(s.modelId)+'">\u5E94\u7528\u5EFA\u8BAE</button></div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No model references found</td></tr>';}function renderCompiledModels(data){ const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04'; compiledProvidersTableBody.innerHTML=providers.length ? providers.map(provider=>'<tr>' + '<td><code>'+esc(provider.name)+'</code><div class="muted">'+esc(provider.api_base_url || '-')+'</div></td>' + '<td>'+esc(provider.transformer?.use?.[0] || '-')+'</td>' + '<td>'+esc((provider.models || []).join(', ') || '-')+'</td>' + '<td>'+esc(JSON.stringify(provider.transformer || {}))+'</td>' + '<td>'+esc(provider.has_api_key ? 'configured' : 'missing')+'</td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled providers</td></tr>'; compiledModelMapTableBody.innerHTML=modelMapEntries.length ? modelMapEntries.map(([modelId,item])=>'<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td><code>'+esc(item.providerName || '-')+'</code><div class="muted">'+esc(item.modelName || '-')+'</div></td>' + '<td>'+esc(item.protocol || '-')+'</td>' + '<td>'+esc(item.compatibilityProfile || '-')+'</td>' + '<td>'+esc(item.dispatchFormat || '-')+'</td>' + '<td><code>'+esc(JSON.stringify(item.thinking || { mode: 'off' }))+'</code></td>' + '<td><code>'+esc(JSON.stringify(item.capabilities || {}))+'</code></td>' + '<td>'+esc(item.source || '-')+'</td>' + '</tr>').join('') : '<tr><td colspan="8" class="muted">No compiled model map</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); updateStatusSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function loadServiceStatus(){ serviceReadyStatus.textContent='checking'; try { const [serviceRes,remoteRes]=await Promise.all([fetch('/api/service-info'),fetch('/api/remote-status')]); const data=await serviceRes.json(); const remoteData=await remoteRes.json(); serviceReadyStatus.textContent=data.ready ? 'ready' : 'not ready'; servicePortStatus.textContent=data.port || '-'; serviceModeStatus.textContent=data.runtimeMode || '-'; serviceRoleStatus.textContent=data.serviceRole || '-'; const registration=data.registration || {}; registrationStatusSummary.textContent=registration.enabled ? ((registration.models ?? 0)+' models / '+(registration.upstreamServices ?? 0)+' upstream') : 'disabled'; const remote=remoteData.remote || {}; remoteStatusSummary.textContent=remote.enabled ? ((remote.ready ? 'ready' : (remote.reachable ? 'reachable' : 'unreachable'))+' \xB7 '+(remote.baseUrl || '-')) : 'disabled'; if(remoteData.compiledModels){ modelCountStatus.textContent=remoteData.compiledModels.modelCount ?? modelCountStatus.textContent; } } catch (_error) { serviceReadyStatus.textContent='unreachable'; remoteStatusSummary.textContent='unknown'; }}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || [], data.issueReport); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addCascadeLevel(){ const next=extractCascadeLevelsFromForm(); next.push({ from:'', to:'' }); renderCascadeLevelsList(next); syncDraftEditorFromForm(); }modelsFormGrid.addEventListener('input',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('change',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-template]'); if(applyBtn){ const applyIndex=Number(applyBtn.dataset.applyTemplate); applyProviderTemplate(applyIndex); syncDraftEditorFromForm(); return; } const btn=e.target.closest('button[data-remove-model]'); if(!btn){ return; } const removeIndex=Number(btn.dataset.removeModel); const nextModels=extractModelsFromForm().filter((_,index)=>index!==removeIndex); renderModelsForm(nextModels); syncDraftEditorFromForm(); });triggerRulesList.addEventListener('input',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('change',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('click',(e)=>{ const addKeywordBtn=e.target.closest('button[data-add-trigger-keyword]'); if(addKeywordBtn){ addTriggerKeyword(Number(addKeywordBtn.dataset.addTriggerKeyword), Number(addKeywordBtn.dataset.patternIndex)); return; } const removeKeywordBtn=e.target.closest('button[data-remove-trigger-keyword]'); if(removeKeywordBtn){ const ruleIndex=Number(removeKeywordBtn.dataset.removeTriggerKeyword); const patternIndex=Number(removeKeywordBtn.dataset.patternIndex); const keywordIndex=Number(removeKeywordBtn.dataset.keywordIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex] && next[ruleIndex].patterns && next[ruleIndex].patterns[patternIndex]){ const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=(pattern.keywords || []).filter((_,index)=>index!==keywordIndex); if(!pattern.keywords.length){ pattern.keywords=['']; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const addBtn=e.target.closest('button[data-add-trigger-pattern]'); if(addBtn){ addTriggerPattern(Number(addBtn.dataset.addTriggerPattern)); return; } const removePatternBtn=e.target.closest('button[data-remove-trigger-pattern]'); if(removePatternBtn){ const ruleIndex=Number(removePatternBtn.dataset.removeTriggerPattern); const patternIndex=Number(removePatternBtn.dataset.patternIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex]){ next[ruleIndex].patterns=(next[ruleIndex].patterns || []).filter((_,index)=>index!==patternIndex); if(!next[ruleIndex].patterns.length){ next[ruleIndex].patterns=[{ type:'exact', keywords:[] }]; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const btn=e.target.closest('button[data-remove-trigger-rule]'); if(!btn){ return; } const next=extractTriggerRulesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeTriggerRule)); renderTriggerRulesList(next); syncDraftEditorFromForm(); });smartCandidatesList.addEventListener('input',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('change',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-smart-candidate]'); if(!btn){ return; } const next=extractSmartCandidatesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeSmartCandidate)); renderSmartCandidatesList(next); syncDraftEditorFromForm(); });governanceCascadeLevelsList.addEventListener('input',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('change',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-cascade-level]'); if(!btn){ return; } const next=extractCascadeLevelsFromForm().filter((_,index)=>index!==Number(btn.dataset.removeCascadeLevel)); renderCascadeLevelsList(next); syncDraftEditorFromForm(); });referenceImpactTableBody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-apply-reference-path]'); if(!btn){ return; } applyReferenceSuggestion(btn.dataset.applyReferencePath, btn.dataset.applyReferenceModel); });draftValidationList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });capabilityWarningsList.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-warning-path]'); if(applyBtn){ applyCapabilityWarningSuggestion(applyBtn.dataset.applyWarningPath, applyBtn.dataset.applyWarningCode); return; } const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });healthSummary.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-health-action]'); if(btn){ applyHealthAction(btn.dataset.healthAction); } });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });surfaceTabs.forEach((tab)=>tab.addEventListener('click',()=>setActiveSurface(tab.dataset.surfaceTarget || 'user')));setActiveSurface('user');function renderMetrics(metrics,health){ metricsGrid.innerHTML=[ ['Health', health?.status || 'idle'], ['Recent traces', metrics.totalTraces ?? 0], ['Sticky hit rate', pct(metrics.stickyHitRate)], ['Cascade rate', pct(metrics.cascadeTriggeredRate)], ['Shadow rate', pct(metrics.shadowCheckedRate)], ['Alignment rate', pct(metrics.alignmentUsedRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.SmartRouter?.enabled), analysis_scope: payload.SmartRouter?.analysis_scope || 'last_message', router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: payload.SmartRouter?.candidates || [], cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.SmartRouter?.rules || []) }; delete payload.TriggerRouter; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: payload.SmartRouter?.rules || [] }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: payload.SmartRouter?.enabled !== undefined ? Boolean(payload.SmartRouter?.enabled) : Boolean(preset.governanceEnabled), sticky:{ ...((payload.SmartRouter && payload.SmartRouter.sticky) || {}), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.enabled), alignment:{ ...(((payload.SmartRouter && payload.SmartRouter.sticky && payload.SmartRouter.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.SmartRouter?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.SmartRouter && payload.SmartRouter.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.SmartRouter?.semantic?.enabled), mode:(payload.SmartRouter?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.SmartRouter?.semantic?.classifier_model || '') } }; payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderAnomalies(anomalies,health){ const status=health?.status || 'idle'; const message=health?.message || 'No governance traces yet.'; const actions=Array.isArray(health?.actions) ? health.actions : []; healthSummary.className='alert '+esc(status === 'critical' ? 'critical' : (status === 'watch' ? 'warn' : 'info')); healthSummary.innerHTML='<strong>Health: '+esc(status)+'</strong><div>'+esc(message)+'</div>'+ (actions.length ? '<ul class="mini-list">'+actions.map(action=>'<li><button type="button" data-health-action="'+esc(action)+'">'+esc(action)+'</button></li>').join('')+'</ul>' : ''); if(!anomalies || !anomalies.length){ anomalyList.innerHTML='<div class="alert info"><strong>No active alerts</strong><div class="muted">\u5F53\u524D\u7A97\u53E3\u672A\u53D1\u73B0\u660E\u663E\u6CBB\u7406\u5F02\u5E38</div></div>'; return; } anomalyList.innerHTML=anomalies.map(item=>'<div class="alert '+esc(item.severity || 'info')+'"><strong>'+esc(item.type)+'</strong><div>'+esc(item.message)+'</div></div>').join('');}function applyHealthAction(action){ const text=String(action || '').toLowerCase(); const routeReasonInput=document.getElementById('routeReason'); const cascadeSelect=document.getElementById('cascadeTriggered'); const shadowSelect=document.getElementById('shadowChecked'); if(text.includes('cascade')){ cascadeSelect.value='true'; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered cascade traces'; } else if(text.includes('shadow')){ shadowSelect.value='true'; cascadeSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered shadow traces'; } else { cascadeSelect.value=''; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: showing recent traces'; } loadTraces(); document.getElementById('traceTable').scrollIntoView({ behavior:'smooth', block:'start' });}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')}</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes,healthRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query), fetch('/api/governance/health'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); const healthData=await healthRes.json(); const health=healthData.health || metricsData.health; renderMetrics(metricsData.metrics || {},health); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || [],health); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderTrendTable(metricsData || {}); const traces=data.traces || []; if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftHeroBtn').addEventListener('click',loadConfigDraft);document.getElementById('previewConfigDraftHeroBtn').addEventListener('click',previewConfigDraft);document.getElementById('refreshStatusHeroBtn').addEventListener('click',loadServiceStatus);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadServiceStatus();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`;
|
|
4473
|
+
return `<!DOCTYPE html><html><head><meta charset="utf-8"><title>Claude Trigger Router</title><style>body{font-family:ui-sans-serif,system-ui,sans-serif;padding:2rem;max-width:1100px;margin:0 auto;background:#f7f7f5;color:#1f2328}.panel{background:#fff;border:1px solid #e5e7eb;border-radius:12px;padding:1rem 1.25rem;margin-bottom:1rem}.muted{color:#6b7280}.hero{display:grid;grid-template-columns:minmax(0,1.2fr) minmax(260px,.8fr);gap:1rem;align-items:stretch;margin-bottom:1rem}.hero h2{margin:.2rem 0 .5rem;font-size:1.55rem}.hero-copy{display:flex;flex-direction:column;justify-content:center}.status-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:.75rem}.status-tile{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem;min-width:0}.status-tile strong{display:block;margin-top:.2rem;word-break:break-word}@media (max-width:760px){.hero{grid-template-columns:1fr}.status-grid{grid-template-columns:1fr}}.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.stat{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.85rem}.stat strong{display:block;font-size:1.1rem;margin-top:.25rem}.subpanel{margin-top:1rem;padding-top:1rem;border-top:1px solid #e5e7eb}.bucket-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem;margin-top:.75rem}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1rem;margin-top:1rem}.mini-list{list-style:none;padding:0;margin:.75rem 0 0}.mini-list li{display:flex;justify-content:space-between;gap:.75rem 1rem;flex-wrap:wrap;align-items:flex-start;padding:.45rem 0;border-bottom:1px dashed #e5e7eb}.mini-list li:last-child{border-bottom:none}.action-row{display:flex;gap:.75rem;flex-wrap:wrap;align-items:center;margin-top:.75rem}.management-table{width:100%;margin-top:.75rem}.management-table th,.management-table td{padding:.5rem;border-bottom:1px solid #e5e7eb;font-size:.92rem;vertical-align:top}.scope-guide{display:grid;grid-template-columns:repeat(auto-fit,minmax(190px,1fr));gap:.75rem;margin-top:.75rem}.scope-guide div{background:#f8fafc;border:1px solid #e5e7eb;border-radius:8px;padding:.75rem}.scope-guide strong{display:block;margin-bottom:.35rem}.alert-list{display:grid;gap:.75rem;margin-top:1rem}.alert{border-radius:12px;padding:.85rem 1rem;border:1px solid}.alert.warn{background:#fff7ed;border-color:#fdba74;color:#9a3412}.alert.critical{background:#fef2f2;border-color:#fca5a5;color:#991b1b}.alert.info{background:#eff6ff;border-color:#93c5fd;color:#1d4ed8}.diff-summary{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:.75rem;margin-top:.75rem}.diff-chip{background:#f8fafc;border:1px solid #e5e7eb;border-radius:12px;padding:.75rem}.diff-chip strong{display:block;font-size:1rem;margin-top:.2rem}.models-form-grid{display:grid;gap:.75rem;margin-top:.75rem}.model-card{border:1px solid #e5e7eb;border-radius:12px;padding:1rem;background:#fcfcfd}.model-card-header{display:flex;justify-content:space-between;gap:1rem;align-items:center;margin-bottom:.75rem}.model-card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.model-card-grid textarea{min-height:84px;resize:vertical}.list-editor{display:grid;gap:.75rem;margin-top:.75rem}.list-item{border:1px solid #e5e7eb;border-radius:12px;padding:.85rem;background:#fcfcfd}.list-item-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:.75rem}.jump-highlight{outline:3px solid #f59e0b;box-shadow:0 0 0 6px rgba(245,158,11,.15);transition:box-shadow .25s ease,outline-color .25s ease}.control-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));gap:.75rem;margin-top:1rem}.control-grid label{display:block;font-size:.85rem;color:#6b7280;margin-bottom:.35rem}.trend-table{width:100%;margin-top:.75rem}.trend-table th,.trend-table td{padding:.45rem;border-bottom:1px solid #e5e7eb;font-size:.92rem}.row{display:flex;gap:1rem;flex-wrap:wrap;align-items:center}input,select,button{font:inherit;padding:.55rem .75rem;border-radius:8px;border:1px solid #d1d5db}button{background:#111827;color:#fff;border-color:#111827;cursor:pointer}table{width:100%;border-collapse:collapse;margin-top:1rem}th,td{text-align:left;padding:.65rem .5rem;border-bottom:1px solid #e5e7eb;vertical-align:top}code,pre{font-family:ui-monospace,SFMono-Regular,monospace}pre{white-space:pre-wrap;background:#0f172a;color:#e2e8f0;padding:1rem;border-radius:12px;overflow:auto}.pill{display:inline-block;padding:.2rem .5rem;border-radius:999px;background:#eef2ff;color:#3730a3;font-size:.8rem}.pill.info{background:#eff6ff;color:#1d4ed8}.pill.warn{background:#fff7ed;color:#9a3412}.pill.critical{background:#fef2f2;color:#991b1b}.surface-tabs{display:flex;gap:.5rem;flex-wrap:wrap;margin:1rem 0}.surface-tab{background:#fff;color:#1f2328;border-color:#d1d5db}.surface-tab.active{background:#111827;color:#fff;border-color:#111827}.surface-panel[hidden]{display:none}.surface-heading{display:flex;gap:1rem;flex-wrap:wrap;align-items:center;margin-bottom:.75rem}</style></head><body><div class="hero"><div class="panel hero-copy"><h2>\u914D\u7F6E\u4E0E\u72B6\u6001\u5DE5\u4F5C\u53F0</h2><p class="muted">\u67E5\u770B\u5F53\u524D\u8DEF\u7531\u670D\u52A1\u3001\u6A21\u578B\u914D\u7F6E\u548C\u9ED8\u8BA4\u53BB\u5411\uFF1B\u9700\u8981\u6392\u67E5\u65F6\uFF0C\u4E0B\u65B9\u7EF4\u62A4\u8005\u533A\u57DF\u53EF\u7EE7\u7EED\u67E5\u770B Governance Trace\u3001metrics \u548C\u5F52\u6863\u3002</p><div class="action-row"><button id="loadConfigDraftHeroBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="previewConfigDraftHeroBtn" type="button">\u9884\u89C8 compiled models</button><button id="refreshStatusHeroBtn" type="button">\u5237\u65B0\u72B6\u6001</button></div></div><div class="panel"><div class="status-grid"><div class="status-tile"><span class="muted">Service</span><strong id="serviceReadyStatus">ready</strong></div><div class="status-tile"><span class="muted">Port</span><strong id="servicePortStatus">${escapedDisplayPort}</strong></div><div class="status-tile"><span class="muted">Mode</span><strong id="serviceModeStatus">${escapedRuntimeMode}</strong></div><div class="status-tile"><span class="muted">Role</span><strong id="serviceRoleStatus">${escapedServiceRole}</strong></div><div class="status-tile"><span class="muted">Listener</span><strong id="listenerStatusSummary">${escapedListenerSummary}</strong></div><div class="status-tile"><span class="muted">Models</span><strong id="modelCountStatus">${escapedModelsCount}</strong></div><div class="status-tile"><span class="muted">Router.default</span><strong id="routerDefaultStatus">${escapedRouterDefault}</strong></div><div class="status-tile"><span class="muted">Remote service</span><strong id="remoteStatusSummary">${escapedRemoteSummary}</strong></div><div class="status-tile"><span class="muted">Registration</span><strong id="registrationStatusSummary">${escapedRegistrationSummary}</strong></div><div class="status-tile"><span class="muted">Auth</span><strong id="authStatusSummary">${escapedAuthSummary}</strong></div><div class="status-tile"><span class="muted">Security</span><strong id="securityStatusSummary">${escapedSecuritySummary}</strong></div></div></div></div><div class="surface-tabs" role="tablist" aria-label="\u5DE5\u4F5C\u53F0\u5207\u6362"><button id="userSurfaceTab" class="surface-tab active" type="button" role="tab" aria-selected="true" data-surface-target="user">\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</button><button id="maintainerSurfaceTab" class="surface-tab" type="button" role="tab" aria-selected="false" data-surface-target="maintainer">\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</button></div><section id="userSurface" class="surface-panel" data-surface="user"><div class="panel"><div class="surface-heading"><strong>\u4F7F\u7528\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u914D\u7F6E\u3001\u6A21\u578B\u3001\u8DEF\u7531\u3001\u670D\u52A1\u72B6\u6001\u4E0E\u4E0B\u4E00\u6B65\u4FDD\u5B58\u52A8\u4F5C\u3002</span></div><div class="subpanel"><div class="row"><strong>Draft Config Preview</strong><span class="muted">\u7F16\u8F91\u5F53\u524D\u914D\u7F6E\u8349\u7A3F\u5E76\u5373\u65F6\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u843D\u76D8</span></div><div class="action-row"><button id="loadConfigDraftBtn" type="button">\u8F7D\u5165\u5F53\u524D\u914D\u7F6E</button><button id="addModelDraftBtn" type="button">\u65B0\u589E Model</button><button id="applyBalancedPresetBtn" type="button">\u5E94\u7528\u5E73\u8861\u9884\u8BBE</button><button id="previewBalancedPresetBtn" type="button">\u9884\u89C8\u5E73\u8861\u9884\u8BBE</button><button id="applyFastPresetBtn" type="button">\u5E94\u7528\u5FEB\u901F\u9884\u8BBE</button><button id="previewFastPresetBtn" type="button">\u9884\u89C8\u5FEB\u901F\u9884\u8BBE</button><button id="applyGovernancePresetBtn" type="button">\u5E94\u7528\u6CBB\u7406\u9884\u8BBE</button><button id="previewGovernancePresetBtn" type="button">\u9884\u89C8\u6CBB\u7406\u9884\u8BBE</button><button id="syncDraftJsonBtn" type="button">\u540C\u6B65 JSON \u8349\u7A3F</button><button id="previewConfigDraftBtn" type="button">\u9884\u89C8 compiled models</button><button id="saveConfigDraftBtn" type="button">\u4FDD\u5B58\u914D\u7F6E</button><span id="draftPreviewStatus" class="muted">\u5C1A\u672A\u9884\u89C8\u914D\u7F6E\u8349\u7A3F</span></div><div class="control-grid"><div><label>Preset mode</label><select id="draftPresetMode"><option value="merge" selected>append / merge</option><option value="replace">overwrite</option></select></div><div><label>Mode guide</label><div id="draftPresetModeHint" class="muted">append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145\u9884\u8BBE\u76F8\u5173\u5B57\u6BB5</div></div></div><div id="draftPresetList" class="alert-list"><div class="alert info"><strong>Preset guide</strong><div class="muted">\u9009\u62E9\u9884\u8BBE\u524D\u53EF\u5148\u67E5\u770B\u5176\u4F1A\u8986\u76D6\u7684\u533A\u57DF\u4E0E\u63A8\u8350\u7528\u9014</div></div></div><div id="draftPreviewMeta" class="alert-list"><div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div></div><div id="draftSummaryGrid" class="stats"><div class="stat"><span class="muted">Models</span><strong>0</strong></div><div class="stat"><span class="muted">Routing rules</span><strong>0</strong></div><div class="stat"><span class="muted">Patterns</span><strong>0</strong></div><div class="stat"><span class="muted">Smart candidates</span><strong>0</strong></div><div class="stat"><span class="muted">Cascade levels</span><strong>0</strong></div><div class="stat"><span class="muted">Model refs</span><strong>0</strong></div></div><div class="subpanel"><div class="row"><strong>Validation Summary</strong><span class="muted">\u96C6\u4E2D\u663E\u793A\u5F53\u524D\u8349\u7A3F\u7684\u9519\u8BEF\u4E0E warning\uFF0C\u5E76\u533A\u5206\u4FEE\u590D\u4F18\u5148\u7EA7</span></div><div id="draftValidationList" class="alert-list"><div class="alert info"><strong>No validation issues</strong><div class="muted">\u9884\u89C8\u524D\u4F1A\u5728\u8FD9\u91CC\u6C47\u603B\u8349\u7A3F\u95EE\u9898</div></div></div></div><div class="subpanel"><div class="row"><strong>Capability Warnings</strong><span class="muted">\u663E\u793A\u6A21\u578B capability hint \u53EF\u80FD\u5E26\u6765\u7684\u8FD0\u884C\u65F6\u964D\u7EA7\u884C\u4E3A</span></div><div id="capabilityWarningsList" class="alert-list"><div class="alert info"><strong>No capability warnings</strong><div class="muted">\u9884\u89C8\u6216\u52A0\u8F7D compiled models \u540E\u4F1A\u5728\u8FD9\u91CC\u663E\u793A\u80FD\u529B\u964D\u7EA7\u63D0\u793A</div></div></div></div><div class="control-grid"><div><label>Router default (modelId)</label><input id="draftRouterDefault" placeholder="\u4F8B\u5982 sonnet"></div><div><label>Models count</label><input id="draftModelsCount" value="0" readonly></div></div><div class="subpanel"><div class="row"><strong>Routing Controls</strong><span class="muted">\u56F4\u7ED5 SmartRouter \u7EDF\u4E00\u8DEF\u7531\u5F15\u64CE\u7F16\u8F91\u89C4\u5219\u3001\u5019\u9009\u4E0E\u6CBB\u7406\u589E\u5F3A\u517C\u5BB9\u914D\u7F6E</span></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Routing rules</strong><span class="muted">\u663E\u5F0F\u89C4\u5219\u3001\u8BED\u4E49\u63D0\u793A\u4E0E\u517C\u5BB9\u8F93\u5165</span></div><div class="control-grid"><div><label><input id="triggerEnabled" type="checkbox"> Enabled</label></div><div><label><input id="triggerIntentEnabled" type="checkbox"> Intent recognition</label></div><div><label>Analysis scope</label><select id="triggerAnalysisScope"><option value="last_message">last_message</option><option value="full_context">full_context</option></select></div><div><label>Intent model</label><input id="triggerIntentModel" list="topLevelTriggerIntentSuggestions" placeholder="modelId"><datalist id="topLevelTriggerIntentSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Rules</label><button id="addTriggerRuleBtn" type="button">\u65B0\u589E Rule</button></div><div id="triggerRulesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>SmartRouter</strong><span class="muted">\u667A\u80FD\u5019\u9009\u9009\u62E9</span></div><div class="control-grid"><div><label><input id="smartEnabled" type="checkbox"> Enabled</label></div><div><label>Router model</label><input id="smartRouterModel" list="topLevelSmartRouterSuggestions" placeholder="modelId"><datalist id="topLevelSmartRouterSuggestions"></datalist></div><div><label>Fallback</label><select id="smartFallback"><option value="default">default</option><option value="skip">skip</option></select></div><div><label>Cache TTL</label><input id="smartCacheTtl" placeholder="600000"></div><div><label>Max tokens</label><input id="smartMaxTokens" placeholder="256"></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Candidates</label><button id="addSmartCandidateBtn" type="button">\u65B0\u589E Candidate</button></div><div id="smartCandidatesList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div></div></div></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Governance</strong><span class="muted">\u5F71\u5B50\u6821\u9A8C\u3001\u7EA7\u8054\u4E0E\u89C2\u6D4B\u76F8\u5173\u914D\u7F6E</span></div><div class="control-grid"><div><label><input id="governanceEnabled" type="checkbox"> Enabled</label></div><div><label><input id="governanceAlignmentEnabled" type="checkbox"> Alignment</label></div><div><label>Summarizer model</label><input id="governanceSummarizerModel" list="topLevelGovernanceSummarizerSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceSummarizerSuggestions"></datalist></div><div><label><input id="governanceSemanticEnabled" type="checkbox"> Semantic</label></div><div><label>Classifier model</label><input id="governanceClassifierModel" list="topLevelGovernanceClassifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceClassifierSuggestions"></datalist></div><div><label><input id="governanceShadowEnabled" type="checkbox"> Shadow</label></div><div><label>Verifier model</label><input id="governanceVerifierModel" list="topLevelGovernanceVerifierSuggestions" placeholder="modelId"><datalist id="topLevelGovernanceVerifierSuggestions"></datalist></div></div><div style="margin-top:.75rem"><div class="action-row"><label>Cascade levels</label><button id="addCascadeLevelBtn" type="button">\u65B0\u589E Level</button></div><div id="governanceCascadeLevelsList" class="list-editor"><div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div></div></div></div></div></div><div class="alert info"><strong>Models field guide</strong><div class="muted">\u65B0\u914D\u7F6E\u8BF7\u4F7F\u7528\u5165\u53E3\u5B57\u6BB5\uFF1Aid / api / key / interface / model / thinking / metadata\uFF1Bapi_key / api_base_url / protocol \u4EC5\u4F5C\u4E3A\u65E7\u914D\u7F6E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div><div id="modelsFormGrid" class="models-form-grid"><div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div></div><textarea id="configDraftEditor" aria-label="JSON config draft" style="width:100%;min-height:240px;margin-top:.75rem;padding:.75rem;border-radius:12px;border:1px solid #d1d5db;font:12px/1.5 ui-monospace,SFMono-Regular,monospace" spellcheck="false" placeholder='{"Models":[{"id":"sonnet","api":"https://...","key":"sk-...","interface":"openai","model":"anthropic/claude-sonnet-4","thinking":"auto","metadata":{"vendor_hint":"openrouter"}}],"Router":{"default":"sonnet"}}'></textarea><div class="muted">JSON \u8349\u7A3F\u540C\u6837\u5EFA\u8BAE\u53EA\u5199\u5165\u53E3\u5B57\u6BB5\uFF1B\u4FDD\u5B58\u65F6\u4F1A\u81EA\u52A8\u5F52\u4E00\uFF0C\u65E7\u5B57\u6BB5\u522B\u540D\u65E0\u9700\u624B\u52A8\u8865\u5145\u3002</div><div class="subpanel"><div class="row"><strong>Preview Diff</strong><span class="muted">\u5BF9\u6BD4\u5F53\u524D\u8FD0\u884C\u914D\u7F6E\u4E0E\u8349\u7A3F\u914D\u7F6E\u7684 compiled model \u53D8\u5316</span></div><div id="compiledDiffSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Added providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed providers</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Added models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Removed models</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Changed models</span><strong>0</strong></div></div><table id="compiledDiffTable" class="management-table"><thead><tr><th>Scope</th><th>Type</th><th>Key</th><th>Changed fields</th><th>Target</th></tr></thead><tbody><tr><td colspan="5" class="muted">Preview a draft to inspect compiled registry changes</td></tr></tbody></table></div><div class="subpanel"><div class="row"><strong>Reference Impact</strong><span class="muted">\u5206\u6790 Router / SmartRouter / Governance\uFF08shadow/cascade\uFF09\u7B49 modelId \u5F15\u7528\u662F\u5426\u4ECD\u7136\u6709\u6548</span></div><div id="referenceImpactSummary" class="diff-summary"><div class="diff-chip"><span class="muted">Total refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">modelId refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Legacy refs</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Valid modelIds</span><strong>0</strong></div><div class="diff-chip"><span class="muted">Missing modelIds</span><strong>0</strong></div></div><table id="referenceImpactTable" class="management-table"><thead><tr><th>Path</th><th>Ref</th><th>Type</th><th>Status</th><th>Resolved target</th><th>Suggestions</th></tr></thead><tbody><tr><td colspan="6" class="muted">Preview a draft to inspect model reference impact</td></tr></tbody></table></div></div><div class="subpanel"><div class="row"><strong>Compiled Models</strong><span class="muted">\u67E5\u770B Models \u7F16\u8BD1\u540E\u7684 provider \u4E0E\u8DEF\u7531\u6620\u5C04</span></div><div id="compiledModelsStatus" class="muted" style="margin-top:.75rem">\u52A0\u8F7D compiled models \u4E2D...</div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Compiled providers</strong><span class="muted">\u5185\u90E8 provider\u3001\u6A21\u578B\u5217\u8868\u4E0E transformer</span></div><table id="compiledProvidersTable" class="management-table"><thead><tr><th>Provider</th><th>Interface</th><th>Models</th><th>Transformer</th><th>API key</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading compiled providers...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model map</strong><span class="muted">modelId \u5230\u5185\u90E8 provider/model\u3001thinking \u4E0E capability \u914D\u7F6E</span></div><table id="compiledModelMapTable" class="management-table"><thead><tr><th>Model ID</th><th>Internal target</th><th>Protocol</th><th>Compatibility profile</th><th>Dispatch format</th><th>Thinking</th><th>Capabilities</th><th>Source</th></tr></thead><tbody><tr><td colspan="8" class="muted">Loading model map...</td></tr></tbody></table></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model pools</strong><span class="muted">Registration.models \u7F16\u8BD1\u51FA\u7684\u540C\u6A21\u578B\u591A\u6E90\u6C60\uFF0C\u5F53\u524D\u4E3A priority \u8C03\u5EA6\u5951\u7EA6</span></div><table id="compiledModelPoolsTable" class="management-table"><thead><tr><th>Pool</th><th>Strategy</th><th>Active endpoint</th><th>Endpoints</th><th>Warnings</th></tr></thead><tbody><tr><td colspan="5" class="muted">Loading model pools...</td></tr></tbody></table></div></div></div></div></section><section id="maintainerSurface" class="surface-panel" data-surface="maintainer" hidden><div class="panel"><div class="surface-heading"><strong>\u7EF4\u62A4\u8005\u5DE5\u4F5C\u53F0</strong><span class="muted">\u8FD0\u884C\u89C2\u6D4B\u3001Governance Trace\u3001metrics\u3001\u5F52\u6863\u4E0E\u7EF4\u62A4\u64CD\u4F5C\u3002</span></div><div id="securitySummary" class="alert info"><strong>Security pending</strong><div class="muted">\u7B49\u5F85\u670D\u52A1\u5B89\u5168\u72B6\u6001\u52A0\u8F7D</div></div><div class="subpanel" id="roleConnectionGuide"><div class="row"><strong>Role & connection guide</strong><span class="muted">\u6309\u5F53\u524D local / server / cloud \u89D2\u8272\u786E\u8BA4\u76D1\u542C\u5730\u5740\u3001\u7EF4\u62A4\u5165\u53E3\u548C\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u63A5\u5165\u65B9\u5F0F\u3002</span></div><div class="scope-guide"><div><strong>current role</strong><span id="roleConnectionSummary" class="muted">${escapedRuntimeMode} / ${escapedServiceRole}</span></div><div><strong>listener</strong><span id="listenerConnectionSummary" class="muted">${escapedListenerSummary}</span></div><div><strong>remote clients</strong><span id="clientConnectionSummary" class="muted">${escapedClientConnectionSummary}</span></div></div><div class="muted" style="margin-top:.75rem">${escapedLocalUserRoleGuide}</div><div class="muted" style="margin-top:.5rem">${escapedServerMaintainerRoleGuide}</div><div class="muted" style="margin-top:.5rem">${escapedRemoteClientRoleGuide}</div></div><div class="subpanel" id="authScopeGuide"><div class="row"><strong>Auth scope guide</strong><span class="muted">\u6309\u7528\u9014\u53D1\u653E\u6700\u5C0F\u6743\u9650 key\uFF0C\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u4E0D\u8981\u590D\u7528 admin key\u3002</span></div><div class="scope-guide"><div><strong>admin</strong><span class="muted">\u7EF4\u62A4\u8005\u4F7F\u7528\uFF1A/ui\u3001\u914D\u7F6E\u4FDD\u5B58\u3001\u91CD\u542F\u3001auth \u7BA1\u7406\u548C\u6CBB\u7406\u5199\u64CD\u4F5C\u3002</span></div><div><strong>client</strong><span class="muted">\u5BA2\u6237\u7AEF\u6A21\u578B\u8C03\u7528\uFF1A/v1/messages\u3001/v1/chat/completions\uFF1B\u6A21\u578B\u8C03\u7528\u914D\u989D\u53EA\u8BA1\u5165\u8FD9\u91CC\u3002</span></div><div><strong>read-only</strong><span class="muted">\u53EA\u8BFB\u89C2\u6D4B\uFF1Ahealth\u3001service-info\u3001compiled models\u3001transformers \u548C governance GET\u3002</span></div><div><strong>client + read-only</strong><span class="muted">\u8FDC\u7A0B token \u540C\u65F6\u9700\u8981 ready/status \u63A2\u6D4B\u4E0E\u6A21\u578B\u8C03\u7528\u65F6\u4F7F\u7528\u8BE5\u7EC4\u5408\u3002</span></div></div><div class="muted" style="margin-top:.75rem">\u7BA1\u7406\u5165\u53E3\uFF1A\u7528 admin key \u8C03\u7528 <code>GET /api/auth/keys</code> \u67E5\u770B\u5217\u8868\uFF0C<code>POST /api/auth/keys</code> \u751F\u6210 key\uFF0C<code>POST /api/auth/keys/:id/revoke</code> \u540A\u9500 key\uFF1B\u751F\u6210\u7684 secret \u53EA\u8FD4\u56DE\u4E00\u6B21\uFF0C\u8BF7\u76F4\u63A5\u4EA4\u7ED9\u5BF9\u5E94\u5BA2\u6237\u7AEF\u4FDD\u5B58\u3002</div></div><div class="subpanel"><div class="row"><strong>Auth quota</strong><span class="muted">\u6309 managed key \u67E5\u770B\u6A21\u578B\u8C03\u7528\u914D\u989D\u3001\u5F53\u524D\u7528\u91CF\u4E0E\u7A97\u53E3\u91CD\u7F6E\u65F6\u95F4</span></div><table id="authQuotaTable" class="management-table"><thead><tr><th>Key</th><th>Scope</th><th>Status</th><th>Requests</th><th>Tokens</th><th>Window</th></tr></thead><tbody><tr><td colspan="6" class="muted">Waiting for service status...</td></tr></tbody></table></div><div class="row"><strong>\u7EF4\u62A4\u8005\u89C2\u6D4B</strong><span class="muted">\u6309 requestId / sessionKey / routeReason \u8FC7\u6EE4 Governance Trace\uFF0C\u5E76\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u6307\u6807\u3002</span></div><div class="row"><input id="requestId" placeholder="requestId"><input id="sessionKey" placeholder="sessionKey"><input id="routeReason" placeholder="routeReason"><select id="cascadeTriggered"><option value="">cascadeTriggered</option><option value="true">cascade=true</option><option value="false">cascade=false</option></select><select id="shadowChecked"><option value="">shadowChecked</option><option value="true">shadow=true</option><option value="false">shadow=false</option></select><select id="windowMs"><option value="900000">15m window</option><option value="3600000" selected>1h window</option><option value="21600000">6h window</option><option value="86400000">24h window</option></select><input id="limit" placeholder="limit" value="20"><button id="refreshBtn">\u5237\u65B0</button></div><div class="muted" style="margin-top:.75rem">\u6570\u636E\u6E90\uFF1A<code>/api/models/compiled</code>\u3001<code>/api/models/compiled/preview</code>\u3001<code>/api/governance/traces</code>\u3001<code>/api/governance/traces/:requestId</code>\u3001<code>/api/governance/archives</code>\u3001<code>/api/governance/metrics</code>\u3001<code>/api/governance/health</code>\u3001<code>/api/governance/metrics/export</code>\u3001<code>/api/governance/metrics/exports</code></div><div id="metricsGrid" class="stats"><div class="stat"><span class="muted">Health</span><strong>-</strong></div><div class="stat"><span class="muted">Recent traces</span><strong>-</strong></div><div class="stat"><span class="muted">Sticky hit rate</span><strong>-</strong></div><div class="stat"><span class="muted">Cascade rate</span><strong>-</strong></div><div class="stat"><span class="muted">Shadow rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment rate</span><strong>-</strong></div><div class="stat"><span class="muted">Model switch rate</span><strong>-</strong></div><div class="stat"><span class="muted">Alignment on switch</span><strong>-</strong></div><div class="stat"><span class="muted">Avg latency</span><strong>-</strong></div></div><div class="subpanel"><div class="row"><strong>Anomaly alerts</strong><span class="muted">\u68C0\u6D4B\u8FD1\u671F\u6CBB\u7406\u5F02\u5E38\u4E0E\u7A81\u589E</span></div><div id="healthSummary" class="alert info"><strong>Health pending</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u5065\u5EB7\u6458\u8981\u52A0\u8F7D</div></div><div id="anomalyList" class="alert-list"><div class="alert info"><strong>No alerts yet</strong><div class="muted">\u7B49\u5F85\u6CBB\u7406\u6307\u6807\u52A0\u8F7D</div></div></div></div><div class="subpanel"><div class="row"><strong>Anomaly tuning</strong><span class="muted">\u6765\u81EA\u914D\u7F6E\u6587\u4EF6\uFF0C\u53EF\u5728\u6B64\u4E34\u65F6\u8986\u76D6\u5F53\u524D\u9875\u9762\u67E5\u8BE2</span></div><div class="control-grid"><div><label>Min sample</label><input id="minSampleSize" value="${escapedMinSampleSize}"></div><div><label>Cascade warn</label><input id="cascadeWarnRate" value="${escapedCascadeWarnRate}"></div><div><label>Shadow warn</label><input id="shadowWarnRate" value="${escapedShadowWarnRate}"></div><div><label>Latency warn ms</label><input id="latencyWarnMs" value="${escapedLatencyWarnMs}"></div></div><div class="row" style="margin-top:.75rem"><button id="saveThresholdsBtn" type="button">\u4FDD\u5B58\u9608\u503C\u5230\u914D\u7F6E</button><span id="saveThresholdsStatus" class="muted">\u5F53\u524D\u4EC5\u4F5C\u4E3A\u9875\u9762\u67E5\u8BE2\u53C2\u6570\uFF1B\u70B9\u51FB\u53EF\u5199\u56DE\u914D\u7F6E\u6587\u4EF6</span></div></div><div class="subpanel"><div class="row"><strong>Window buckets</strong><span id="bucketHint" class="muted">\u6309\u65F6\u95F4\u7A97\u67E5\u770B\u8FD1\u671F\u6CBB\u7406\u8D8B\u52BF</span></div><div id="bucketGrid" class="bucket-grid"><div class="stat"><span class="muted">Loading buckets</span><strong>-</strong></div></div></div><div class="detail-grid"><div class="panel" style="margin-bottom:0"><div class="row"><strong>Route ranking</strong><span class="muted">\u8FD1\u671F\u547D\u4E2D\u539F\u56E0 Top 5</span></div><ul id="routeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Model ranking</strong><span class="muted">\u8FD1\u671F\u6700\u7EC8\u6A21\u578B Top 5</span></div><ul id="modelRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Intent ranking</strong><span class="muted">\u8FD1\u671F\u8BED\u4E49\u610F\u56FE Top 5</span></div><ul id="intentRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by route</strong><span class="muted">\u5207\u6362\u3001alignment\u3001cascade \u4E0E\u5EF6\u8FDF</span></div><ul id="routeOutcomeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by model</strong><span class="muted">\u6700\u7EC8\u6A21\u578B\u5207\u6362\u4E0E\u5EF6\u8FDF\u8868\u73B0</span></div><ul id="modelOutcomeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Outcome by intent</strong><span class="muted">\u4EFB\u52A1\u610F\u56FE\u5207\u6362\u4E0E\u5EF6\u8FDF\u8868\u73B0</span></div><ul id="intentOutcomeRanking" class="mini-list"><li><span class="muted">Loading</span><strong>-</strong></li></ul></div><div class="panel" style="margin-bottom:0"><div class="row"><strong>Trend detail</strong><span class="muted">\u6BCF\u4E2A bucket \u7684\u8BE6\u7EC6\u547D\u4E2D\u7387</span></div><table id="trendTable" class="trend-table"><thead><tr><th>Bucket</th><th>Traces</th><th>Sticky</th><th>Cascade</th><th>Shadow</th><th>Alignment</th></tr></thead><tbody><tr><td colspan="6" class="muted">Loading...</td></tr></tbody></table></div></div><table id="traceTable"><thead><tr><th>Request</th><th>Session</th><th>Final Model</th><th>Reasons</th><th>Latency</th><th>Inspect</th></tr></thead><tbody><tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Trace Detail</strong><span id="detailHint" class="muted">\u70B9\u51FB\u4E0A\u8868\u4E2D\u7684 View \u67E5\u770B\u8BE6\u60C5</span></div><pre id="traceDetail">{}</pre></div><div class="panel"><div class="row"><strong>Snapshot Management</strong><span class="muted">\u67E5\u770B\u5BFC\u51FA\u5386\u53F2\u3001\u5B9A\u65F6\u4EFB\u52A1\uFF0C\u5E76\u624B\u52A8\u521B\u5EFA\u5FEB\u7167</span></div><div class="action-row"><select id="snapshotFormat"><option value="json">snapshot json</option><option value="csv">snapshot csv</option></select><button id="createSnapshotBtn" type="button">\u751F\u6210\u5FEB\u7167</button><span id="snapshotStatus" class="muted">\u5C1A\u672A\u521B\u5EFA\u5FEB\u7167</span></div><table id="exportTable" class="management-table"><thead><tr><th>Export</th><th>Kind</th><th>Format</th><th>Created</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading exports...</td></tr></tbody></table><table id="scheduleTable" class="management-table"><thead><tr><th>Schedule</th><th>Interval</th><th>Format</th><th>Last run</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading schedules...</td></tr></tbody></table></div><div class="panel"><div class="row"><strong>Archive Management</strong><span class="muted">\u6D4F\u89C8\u538B\u7F29\u5F52\u6863\u5E76\u67E5\u770B\u5206\u9875\u7ED3\u679C</span></div><div class="action-row"><input id="archiveDate" placeholder="YYYY-MM-DD"><input id="archivePage" placeholder="page" value="1"><input id="archivePageSize" placeholder="pageSize" value="5"><button id="loadArchivesBtn" type="button">\u52A0\u8F7D\u5F52\u6863</button><span id="archiveStatus" class="muted">\u5C1A\u672A\u52A0\u8F7D\u5F52\u6863</span></div><table id="archiveTable" class="management-table"><thead><tr><th>Archive</th><th>Range</th><th>Count</th><th>Compressed</th></tr></thead><tbody><tr><td colspan="4" class="muted">Loading archives...</td></tr></tbody></table></div><div class="panel"><p>\u5176\u4ED6\u7BA1\u7406 API\uFF1A</p><ul><li><code>GET /api/config</code> \u2014 \u8BFB\u53D6\u5F53\u524D\u914D\u7F6E</li><li><code>GET /api/models/compiled</code> \u2014 \u67E5\u770B Models \u7F16\u8BD1\u540E\u7684\u5185\u90E8 provider / model \u6620\u5C04</li><li><code>POST /api/models/compiled/preview</code> \u2014 \u7528\u914D\u7F6E\u8349\u7A3F\u9884\u89C8 compiled models \u7ED3\u679C\uFF0C\u4E0D\u5199\u56DE\u6587\u4EF6</li><li><code>POST /api/config</code> \u2014 \u4FDD\u5B58\u914D\u7F6E</li><li><code>GET /api/transformers</code> \u2014 \u67E5\u770B\u5DF2\u52A0\u8F7D transformer</li><li><code>POST /api/restart</code> \u2014 \u91CD\u542F\u670D\u52A1</li><li><code>GET /api/governance/archives</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5F52\u6863\u5217\u8868</li><li><code>GET /api/governance/archives/:file</code> \u2014 \u67E5\u770B\u5F52\u6863\u5185 traces</li><li><code>POST /api/governance/archives/:file/delete</code> \u2014 \u5220\u9664\u6307\u5B9A\u5F52\u6863</li><li><code>GET /api/governance/health</code> \u2014 \u67E5\u770B\u6CBB\u7406\u5065\u5EB7\u6458\u8981</li><li><code>GET /api/auth/audit</code> \u2014 \u67E5\u770B\u9274\u6743\u5BA1\u8BA1\u6458\u8981</li><li><code>POST /api/governance/metrics/snapshots</code> \u2014 \u751F\u6210\u4E00\u6B21\u6CBB\u7406\u6307\u6807\u5FEB\u7167</li><li><code>POST /api/governance/metrics/schedules</code> \u2014 \u6CE8\u518C\u5B9A\u65F6\u5FEB\u7167\u4EFB\u52A1</li></ul></div></section><script>const tbody=document.querySelector('#traceTable tbody');const detail=document.getElementById('traceDetail');const detailHint=document.getElementById('detailHint');const draftPreviewStatus=document.getElementById('draftPreviewStatus');const draftPresetMode=document.getElementById('draftPresetMode');const draftPresetModeHint=document.getElementById('draftPresetModeHint');const draftPresetList=document.getElementById('draftPresetList');const draftPreviewMeta=document.getElementById('draftPreviewMeta');const draftValidationList=document.getElementById('draftValidationList');const capabilityWarningsList=document.getElementById('capabilityWarningsList');const configDraftEditor=document.getElementById('configDraftEditor');const draftSummaryGrid=document.getElementById('draftSummaryGrid');const modelsFormGrid=document.getElementById('modelsFormGrid');const draftRouterDefault=document.getElementById('draftRouterDefault');const draftModelsCount=document.getElementById('draftModelsCount');const serviceReadyStatus=document.getElementById('serviceReadyStatus');const servicePortStatus=document.getElementById('servicePortStatus');const serviceModeStatus=document.getElementById('serviceModeStatus');const serviceRoleStatus=document.getElementById('serviceRoleStatus');const listenerStatusSummary=document.getElementById('listenerStatusSummary');const roleConnectionSummary=document.getElementById('roleConnectionSummary');const listenerConnectionSummary=document.getElementById('listenerConnectionSummary');const clientConnectionSummary=document.getElementById('clientConnectionSummary');const remoteStatusSummary=document.getElementById('remoteStatusSummary');const registrationStatusSummary=document.getElementById('registrationStatusSummary');const authStatusSummary=document.getElementById('authStatusSummary');const securityStatusSummary=document.getElementById('securityStatusSummary');const modelCountStatus=document.getElementById('modelCountStatus');const routerDefaultStatus=document.getElementById('routerDefaultStatus');const triggerEnabled=document.getElementById('triggerEnabled');const triggerIntentEnabled=document.getElementById('triggerIntentEnabled');const triggerAnalysisScope=document.getElementById('triggerAnalysisScope');const triggerIntentModel=document.getElementById('triggerIntentModel');const triggerRulesList=document.getElementById('triggerRulesList');const smartEnabled=document.getElementById('smartEnabled');const smartRouterModel=document.getElementById('smartRouterModel');const smartFallback=document.getElementById('smartFallback');const smartCacheTtl=document.getElementById('smartCacheTtl');const smartMaxTokens=document.getElementById('smartMaxTokens');const smartCandidatesList=document.getElementById('smartCandidatesList');const governanceEnabled=document.getElementById('governanceEnabled');const governanceAlignmentEnabled=document.getElementById('governanceAlignmentEnabled');const governanceSummarizerModel=document.getElementById('governanceSummarizerModel');const governanceSemanticEnabled=document.getElementById('governanceSemanticEnabled');const governanceClassifierModel=document.getElementById('governanceClassifierModel');const governanceShadowEnabled=document.getElementById('governanceShadowEnabled');const governanceVerifierModel=document.getElementById('governanceVerifierModel');const governanceCascadeLevelsList=document.getElementById('governanceCascadeLevelsList');const topLevelTriggerIntentSuggestions=document.getElementById('topLevelTriggerIntentSuggestions');const topLevelSmartRouterSuggestions=document.getElementById('topLevelSmartRouterSuggestions');const topLevelGovernanceSummarizerSuggestions=document.getElementById('topLevelGovernanceSummarizerSuggestions');const topLevelGovernanceClassifierSuggestions=document.getElementById('topLevelGovernanceClassifierSuggestions');const topLevelGovernanceVerifierSuggestions=document.getElementById('topLevelGovernanceVerifierSuggestions');const compiledModelsStatus=document.getElementById('compiledModelsStatus');const compiledDiffSummary=document.getElementById('compiledDiffSummary');const compiledDiffTableBody=document.querySelector('#compiledDiffTable tbody');const referenceImpactSummary=document.getElementById('referenceImpactSummary');const referenceImpactTableBody=document.querySelector('#referenceImpactTable tbody');const compiledProvidersTableBody=document.querySelector('#compiledProvidersTable tbody');const compiledModelMapTableBody=document.querySelector('#compiledModelMapTable tbody');const compiledModelPoolsTableBody=document.querySelector('#compiledModelPoolsTable tbody');const metricsGrid=document.getElementById('metricsGrid');const bucketGrid=document.getElementById('bucketGrid');const bucketHint=document.getElementById('bucketHint');const routeRanking=document.getElementById('routeRanking');const modelRanking=document.getElementById('modelRanking');const intentRanking=document.getElementById('intentRanking');const routeOutcomeRanking=document.getElementById('routeOutcomeRanking');const modelOutcomeRanking=document.getElementById('modelOutcomeRanking');const intentOutcomeRanking=document.getElementById('intentOutcomeRanking');const healthSummary=document.getElementById('healthSummary');const securitySummary=document.getElementById('securitySummary');const authQuotaTableBody=document.querySelector('#authQuotaTable tbody');const anomalyList=document.getElementById('anomalyList');const saveThresholdsStatus=document.getElementById('saveThresholdsStatus');const snapshotStatus=document.getElementById('snapshotStatus');const archiveStatus=document.getElementById('archiveStatus');const exportTableBody=document.querySelector('#exportTable tbody');const scheduleTableBody=document.querySelector('#scheduleTable tbody');const archiveTableBody=document.querySelector('#archiveTable tbody');const trendTableBody=document.querySelector('#trendTable tbody');const surfaceTabs=Array.from(document.querySelectorAll('[data-surface-target]'));const surfacePanels=Array.from(document.querySelectorAll('[data-surface]'));let currentDraftConfig={};let knownModelIds=[];let activeValidationHighlight=null;const draftPresets={ balanced:{ label:'\u5E73\u8861\u9884\u8BBE', description:'\u542F\u7528 SmartRouter\uFF0C\u5E76\u586B\u5145\u5E73\u8861/\u5FEB\u901F\u5019\u9009\u6A21\u578B\u7EC4\u5408\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.candidates'], routerDefault:'sonnet', smartEnabled:true, smartCandidates:[{ model:'sonnet', description:'balanced default' },{ model:'haiku', description:'fast lightweight' }] }, fast:{ label:'\u5FEB\u901F\u9884\u8BBE', description:'\u9ED8\u8BA4\u8D70\u8F7B\u91CF\u6A21\u578B\uFF0C\u5E76\u6DFB\u52A0\u4E00\u6761\u5FEB\u901F\u54CD\u5E94\u8DEF\u7531\u89C4\u5219\u3002', affects:['Router.default','SmartRouter.enabled','SmartRouter.rules'], routerDefault:'haiku', triggerEnabled:true, triggerRules:[{ name:'quick-response', enabled:true, priority:20, model:'haiku', patterns:[{ type:'exact', keywords:['\u5FEB\u901F\u5904\u7406','\u5FEB\u901F\u56DE\u7B54'] }] }] }, governance:{ label:'\u6CBB\u7406\u9884\u8BBE', description:'\u6253\u5F00\u6CBB\u7406\u589E\u5F3A\u4E0E\u6821\u9A8C\u80FD\u529B\uFF0C\u5E76\u586B\u5165 summarizer/classifier/verifier \u793A\u4F8B\u6A21\u578B\u3002', affects:['Governance.enabled','SmartRouter.sticky.alignment','SmartRouter.semantic','Governance.shadow'], governanceEnabled:true, governanceAlignmentEnabled:true, governanceSemanticEnabled:true, governanceShadowEnabled:true, governanceSummarizerModel:'sonnet', governanceClassifierModel:'sonnet', governanceVerifierModel:'haiku' }};const modelProviderTemplates=${toInlineScriptJson(getUiProviderTemplates())};const defaultProviderTemplateKey='openrouter';function esc(v){return String(v ?? '').replace(/[&<>"]/g,m=>({ '&':'&','<':'<','>':'>','"':'"' }[m]));}function pct(v){return (Number(v || 0) * 100).toFixed(1)+'%';}function fmt(v){return Number(v || 0).toFixed(2);}function shortTime(v){ const d=new Date(v); return d.toISOString().slice(11,16); }function limitText(used,limit){ return Number.isFinite(limit) ? (String(used ?? 0)+' / '+String(limit)) : String(used ?? 0); }function renderAuthQuotaTable(quota){ const keys=Array.isArray(quota?.keys) ? quota.keys : []; if(!keys.length){ authQuotaTableBody.innerHTML='<tr><td colspan="6" class="muted">No managed keys configured</td></tr>'; return; } authQuotaTableBody.innerHTML=keys.map(item=>{ const usage=item.usage || {}; const quotaCfg=item.quota || {}; const keyName=esc(item.label || item.id || '-')+'<div class="muted"><code>'+esc(item.id || '-')+'</code></div>'; const statusClass=item.status === 'exhausted' ? 'critical' : (item.status === 'watch' ? 'warn' : 'info'); const windowText=quotaCfg.window_seconds ? (esc(quotaCfg.window_seconds)+'s'+(usage.windowResetAt ? '<div class="muted">reset '+esc(String(usage.windowResetAt).replace('T',' ').replace('.000Z','Z'))+'</div>' : '<div class="muted">not started</div>')) : '-'; return '<tr><td>'+keyName+'</td><td>'+esc((item.scopes || []).join(', ') || '-')+'</td><td><span class="pill '+statusClass+'">'+esc(item.status || '-')+'</span></td><td>'+esc(limitText(usage.requestsUsed,usage.requestLimit))+'</td><td>'+esc(limitText(usage.tokensUsed,usage.tokenLimit))+'</td><td>'+windowText+'</td></tr>'; }).join('');}function renderRoleConnectionGuide(data){ const listener=data.listener || {}; const connection=data.clientConnection || {}; const mode=data.runtimeMode || '-'; const role=data.serviceRole || '-'; const listenerText=listener.host ? (listener.host+':'+(listener.port || '-')+(listener.public ? ' (public)' : ' (local)')) : '-'; const connectionText=connection.baseUrl ? (connection.baseUrl+' \xB7 '+(Array.isArray(connection.recommendedScopes) ? connection.recommendedScopes.join(' + ') : '')) : (connection.guidance || '-'); listenerStatusSummary.textContent=listenerText; roleConnectionSummary.textContent=mode+' / '+role; listenerConnectionSummary.textContent=listenerText; clientConnectionSummary.textContent=connectionText || '-';}function setActiveSurface(surfaceName){ surfacePanels.forEach((panel)=>{ panel.hidden=panel.dataset.surface !== surfaceName; }); surfaceTabs.forEach((tab)=>{ const active=tab.dataset.surfaceTarget === surfaceName; tab.classList.toggle('active',active); tab.setAttribute('aria-selected', active ? 'true' : 'false'); });}function inferProviderTemplateKey(model){ const explicit=String(model?.provider_template || '').trim(); if(explicit && modelProviderTemplates[explicit]){ return explicit; } const api=String(model?.api || model?.api_base_url || '').trim().toLowerCase(); const modelInterface=String(model?.interface || model?.protocol || '').trim().toLowerCase(); const exactMatch=Object.entries(modelProviderTemplates).find(([,item])=>String(item.api || '').trim().toLowerCase()===api && String(item.interface || '').trim().toLowerCase()===modelInterface); if(exactMatch){ return exactMatch[0]; } if(api.includes('api.anthropic.com/v1/messages') || modelInterface === 'anthropic'){ return 'anthropic'; } if(api.includes('openrouter.ai')){ return 'openrouter'; } if(api.includes('deepseek.com')){ return 'deepseek'; } if(api.includes('siliconflow.cn')){ return 'siliconflow'; } if(api.includes('api.openai.com')){ return 'openai-compatible'; } return '';}function getProviderTemplateContext(model){ const templateKey=inferProviderTemplateKey(model) || defaultProviderTemplateKey; return { templateKey, template:modelProviderTemplates[templateKey] || modelProviderTemplates[defaultProviderTemplateKey] || {} };}function createDraftModelFromTemplate(templateKey){ const resolvedKey=(templateKey && modelProviderTemplates[templateKey]) ? templateKey : defaultProviderTemplateKey; const template=modelProviderTemplates[resolvedKey] || {}; return { provider_template:resolvedKey, id:template.suggested_id || '', api:template.api || '', interface:template.interface || 'openai', model:template.default_model || '', thinking:template.default_thinking || 'auto' };}function getModelIdSuggestionsMarkup(idPrefix){ return '<datalist id="'+idPrefix+'">'+knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join('')+'</datalist>';}function resolvePresetModelId(seed){ const source=String(seed || '').trim().toLowerCase(); if(!source || !knownModelIds.length){ return seed; } if(knownModelIds.includes(seed)){ return seed; } const ranked=knownModelIds.map((modelId)=>{ const target=String(modelId || '').toLowerCase(); let score=0; if(target===source){ score+=100; } if(target.includes(source) || source.includes(target)){ score+=40; } source.split(/[^a-z0-9]+/).filter(Boolean).forEach((part)=>{ if(target.includes(part)){ score+=Math.min(part.length * 4, 24); } }); return { modelId, score }; }).filter((item)=>item.score>0).sort((a,b)=>b.score-a.score || a.modelId.localeCompare(b.modelId)); return ranked.length ? ranked[0].modelId : seed;}function getTriggerPatternValidationHint(pattern){ if((pattern?.type || 'exact') === 'regex'){ return pattern?.pattern ? { level:'ok', message:'regex pattern \u5DF2\u914D\u7F6E' } : { level:'warn', message:'regex \u6A21\u5F0F\u9700\u8981\u586B\u5199 pattern' }; } return Array.isArray(pattern?.keywords) && pattern.keywords.some((keyword)=>String(keyword || '').trim()) ? { level:'ok', message:'exact keywords \u5DF2\u914D\u7F6E' } : { level:'warn', message:'exact \u6A21\u5F0F\u81F3\u5C11\u9700\u8981\u4E00\u4E2A keyword' };}function getDraftSmartRouterConfig(config){ const smart={ ...((config && config.SmartRouter) || {}) }; const smartExplicit=config && Object.prototype.hasOwnProperty.call(config,'SmartRouter'); const legacyIntentEnabled=Boolean(config?.TriggerRouter?.llm_intent_recognition); const legacyIntentModel=config?.TriggerRouter?.intent_model || ''; if(!smart.analysis_scope && config?.TriggerRouter?.analysis_scope){ smart.analysis_scope=config.TriggerRouter.analysis_scope; } if((!Array.isArray(smart.rules) || !smart.rules.length) && Array.isArray(config?.TriggerRouter?.rules)){ smart.rules=config.TriggerRouter.rules; } if(!smart.semantic && (config?.Governance?.semantic || config?.TriggerRouter?.llm_intent_recognition)){ smart.semantic={ ...((config && config.Governance && config.Governance.semantic) || {}) }; if(config?.TriggerRouter?.llm_intent_recognition){ smart.semantic.enabled=true; smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || config.TriggerRouter.intent_model || ''; } } if(!smart.sticky && config?.Governance?.sticky){ smart.sticky={ ...(config.Governance.sticky || {}) }; } if(!smartExplicit && !smart.enabled && (config?.TriggerRouter?.enabled || smart.rules?.length || smart.router_model || smart.candidates?.length || smart.semantic || smart.sticky)){ smart.enabled=true; } if(smart.enabled){ smart.analysis_scope=smart.analysis_scope || 'last_message'; smart.semantic={ ...(smart.semantic || {}) }; smart.semantic.enabled=smart.semantic.enabled !== undefined ? smart.semantic.enabled : true; smart.semantic.threshold=smart.semantic.threshold !== undefined ? smart.semantic.threshold : 0.2; if(legacyIntentEnabled){ smart.semantic.mode=smart.semantic.mode || 'classifier'; smart.semantic.classifier_model=smart.semantic.classifier_model || legacyIntentModel; } smart.sticky={ ...(smart.sticky || {}) }; smart.sticky.enabled=smart.sticky.enabled !== undefined ? smart.sticky.enabled : true; smart.sticky.alignment={ ...((smart.sticky && smart.sticky.alignment) || {}) }; smart.sticky.alignment.enabled=smart.sticky.alignment.enabled !== undefined ? smart.sticky.alignment.enabled : true; smart.sticky.alignment.summarizer_model=smart.sticky.alignment.summarizer_model || smart.router_model || config?.Router?.default || legacyIntentModel || ''; } return smart;}function renderDraftSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; const smart=getDraftSmartRouterConfig(config); const triggerRules=Array.isArray(smart?.rules) ? smart.rules : []; const patternCount=triggerRules.reduce((sum,rule)=>sum + (Array.isArray(rule.patterns) ? rule.patterns.length : 0),0); const smartCandidates=Array.isArray(smart?.candidates) ? smart.candidates : []; const cascadeLevels=Array.isArray(config?.Governance?.cascade?.levels) ? config.Governance.cascade.levels : []; const modelRefCount=[config?.Router?.default, smart?.router_model, smart?.sticky?.alignment?.summarizer_model, smart?.semantic?.classifier_model, config?.Governance?.shadow?.verifier_model].filter(v=>typeof v === 'string' && v.trim()).length + triggerRules.filter(rule=>rule?.model).length + smartCandidates.filter(item=>item?.model).length + cascadeLevels.reduce((sum,level)=>sum + (level?.from ? 1 : 0) + (level?.to ? 1 : 0), 0); draftSummaryGrid.innerHTML=[ ['Models', models.length], ['Routing rules', triggerRules.length], ['Patterns', patternCount], ['Smart candidates', smartCandidates.length], ['Cascade levels', cascadeLevels.length], ['Model refs', modelRefCount] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function updateStatusSummary(config){ const models=Array.isArray(config?.Models) ? config.Models : []; modelCountStatus.textContent=String(models.length); routerDefaultStatus.textContent=config?.Router?.default || '-';}function renderDraftValidation(errors,warnings,issueReport){ const errorList=Array.isArray(errors) ? errors.filter(Boolean) : []; const warningList=Array.isArray(warnings) ? warnings.filter(Boolean) : []; const contractIssues=Array.isArray(issueReport?.issues) ? issueReport.issues : []; if(!errorList.length && !warningList.length && !contractIssues.length){ draftValidationList.innerHTML='<div class="alert info"><strong>No validation issues</strong><div class="muted">\u5F53\u524D\u8349\u7A3F\u672A\u53D1\u73B0\u96C6\u4E2D\u5C55\u793A\u7684\u95EE\u9898</div></div>'; return; } const extractPath=(text)=>{ const match=String(text).match(/^(Models(?:\\[[0-9]+\\])?(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Router(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|TriggerRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|SmartRouter(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?|Governance(?:\\.[A-Za-z0-9_\\[\\]\\.]+)?)/); return match ? match[1] : ''; }; const sourceItems=contractIssues.length ? contractIssues.map(item=>({ text:String(item.message || ''), severity:item.severity==='error' ? 'error' : 'warning', path:item.path || '', action:item.action || '' })) : [...errorList.map(item=>({ text:String(item), severity:'error', path:'', action:'' })), ...warningList.map(item=>({ text:String(item), severity:'warning', path:'', action:'' }))]; const grouped=sourceItems.reduce((acc,item)=>{ const text=item.text; const path=item.path || extractPath(text); const bucket=path.startsWith('Models') || text.startsWith('Models') ? 'Models' : path.startsWith('Router') || text.startsWith('Router') ? 'Router' : path.startsWith('TriggerRouter') || text.startsWith('TriggerRouter') ? 'SmartRouter' : path.startsWith('SmartRouter') || text.startsWith('SmartRouter') ? 'SmartRouter' : (path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic') || text.startsWith('Governance.sticky') || text.startsWith('Governance.semantic')) ? 'SmartRouter' : path.startsWith('Governance') || text.startsWith('Governance') ? 'Governance' : text.startsWith('JSON parse error') ? 'Draft JSON' : 'Other'; acc[bucket]=acc[bucket] || []; acc[bucket].push({ text, path, severity:item.severity, action:item.action || '' }); return acc; }, {}); const errorCount=contractIssues.length ? contractIssues.filter(item=>item.severity==='error').length : errorList.length; const warningCount=contractIssues.length ? contractIssues.filter(item=>item.severity!=='error').length : warningList.length; const summary='<div class="alert info"><div class="row"><strong>Validation summary</strong><span class="pill">'+esc(errorCount)+' errors / '+esc(warningCount)+' warnings</span></div><div class="muted">'+(errorCount ? '\u8BF7\u4F18\u5148\u4FEE\u590D errors\uFF0C\u518D\u51B3\u5B9A\u662F\u5426\u63A5\u53D7 warnings\u3002' : '\u5F53\u524D\u65E0\u963B\u65AD\u9519\u8BEF\uFF0C\u53EF\u6309\u9700\u5904\u7406 warnings\u3002')+'</div></div>'; draftValidationList.innerHTML=summary + Object.entries(grouped).map(([bucket,items])=>{ const hasError=items.some(item=>item.severity==='error'); const levelClass=hasError ? 'warn' : 'info'; const actionLabel=hasError ? 'repair first' : 'review before save'; return '<div class="alert '+levelClass+'"><div class="row"><strong>'+esc(bucket)+'</strong><span class="pill">'+esc(items.length)+' issues</span></div><div class="muted">'+esc(actionLabel)+'</div><div>'+items.slice(0,4).map(item=>'<div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+'<span class="pill">'+esc(item.severity==='error' ? 'error' : 'warning')+'</span> '+esc(item.text)+(item.action ? ('<div class="muted">Action: '+esc(item.action)+'</div>') : '')+'</div>').join('')+'</div></div>'; }).join('');}function getCapabilityWarningActionLabel(code){ if(code==='thinking_ignored'){ return '\u79FB\u9664 thinking'; } if(code==='tools_text_fallback' || code==='images_text_fallback'){ return '\u6062\u590D\u9ED8\u8BA4 capability'; } return '';}function renderCapabilityWarnings(report){ const entries=Array.isArray(report?.entries) ? report.entries : []; if(!entries.length){ capabilityWarningsList.innerHTML='<div class="alert info"><strong>No capability warnings</strong><div class="muted">\u5F53\u524D compiled models \u672A\u53D1\u73B0\u9700\u8981\u989D\u5916\u63D0\u793A\u7684\u80FD\u529B\u964D\u7EA7</div></div>'; return; } const summary=report?.summary || {}; capabilityWarningsList.innerHTML='<div class="alert info"><strong>Capability warning summary</strong><div class="muted">warn '+esc(summary.warn ?? 0)+' / info '+esc(summary.info ?? 0)+' / total '+esc(summary.total ?? entries.length)+'</div></div>' + entries.map(item=>{ const actionLabel=getCapabilityWarningActionLabel(item.code); return '<div class="alert '+esc(item.level === 'warn' ? 'warn' : 'info')+'"><div class="row"><strong>'+esc(item.code || item.level || 'warning')+'</strong><span class="pill">'+esc(item.modelId || '-').trim()+'</span></div><div>'+(item.path ? ('<button type="button" class="pill" data-validation-path="'+esc(item.path)+'">'+esc(item.path)+'</button> ') : '')+esc(item.message || '')+'</div>'+(actionLabel ? ('<div class="row" style="margin-top:.5rem"><button type="button" data-apply-warning-path="'+esc(item.path || '')+'" data-apply-warning-code="'+esc(item.code || '')+'">'+esc(actionLabel)+'</button></div>') : '')+'</div>'; }).join('');}function findValidationTarget(path){ if(!path){ return null; } if(path.startsWith('Models')){ return modelsFormGrid; } if(path === 'Router.default'){ return draftRouterDefault; } if(path.startsWith('TriggerRouter.intent_model')){ return triggerIntentModel; } if(path.startsWith('TriggerRouter.rules[')){ return triggerRulesList; } if(path.startsWith('SmartRouter.router_model')){ return smartRouterModel; } if(path.startsWith('SmartRouter.candidates[')){ return smartCandidatesList; } if(path.startsWith('Governance.cascade.levels[')){ return governanceCascadeLevelsList; } if(path.startsWith('Governance.sticky.alignment')){ return governanceSummarizerModel; } if(path.startsWith('Governance.semantic')){ return governanceClassifierModel; } if(path.startsWith('Governance.shadow')){ return governanceVerifierModel; } if(path.startsWith('Governance')){ return governanceEnabled; } return null;}function jumpToValidationPath(path){ const target=findValidationTarget(path); if(!target || typeof target.scrollIntoView !== 'function'){ return; } if(activeValidationHighlight && activeValidationHighlight.classList){ activeValidationHighlight.classList.remove('jump-highlight'); } target.scrollIntoView({ behavior:'smooth', block:'center' }); if(target.classList){ target.classList.add('jump-highlight'); activeValidationHighlight=target; setTimeout(()=>{ if(target.classList){ target.classList.remove('jump-highlight'); if(activeValidationHighlight===target){ activeValidationHighlight=null; } } }, 1800); } if(typeof target.focus === 'function'){ target.focus({ preventScroll:true }); }}function renderDraftPresetModeHint(){ const overwriteMode=draftPresetMode.value === 'replace'; draftPresetModeHint.textContent=overwriteMode ? 'overwrite \u4F1A\u91CD\u7F6E SmartRouter / Governance \u76F8\u5173\u8868\u5355\uFF0C\u518D\u5E94\u7528\u9884\u8BBE' : 'append / merge \u4F1A\u5C3D\u91CF\u4FDD\u7559\u5F53\u524D\u8349\u7A3F\uFF0C\u4EC5\u8865\u5145 SmartRouter / Governance \u76F8\u5173\u5B57\u6BB5';}function deriveActualAffectedAreas(preview){ const areas=new Set(); const diff=preview?.diff || {}; const impact=preview?.referenceImpact || {}; if((diff.providerChanges || []).length || (diff.modelChanges || []).length){ areas.add('Models'); } (impact.entries || []).forEach((entry)=>{ const path=String(entry.path || ''); if(path.startsWith('Router.')){ areas.add('Router'); } else if(path.startsWith('TriggerRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('SmartRouter.')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.sticky') || path.startsWith('Governance.semantic')){ areas.add('SmartRouter'); } else if(path.startsWith('Governance.')){ areas.add('Governance'); } }); return Array.from(areas);}function renderDraftPreviewMeta(meta){ if(!meta){ draftPreviewMeta.innerHTML='<div class="alert info"><strong>Draft preview mode</strong><div class="muted">\u5F53\u524D\u663E\u793A\u4E3A\u8349\u7A3F\u7F16\u8F91\u89C6\u56FE\uFF0C\u9884\u8BBE dry-run \u4F1A\u5728\u8FD9\u91CC\u63D0\u793A\u5F71\u54CD\u8303\u56F4\u3002</div></div>'; return; } draftPreviewMeta.innerHTML='<div class="alert info"><strong>'+esc(meta.title || 'Preset dry-run')+'</strong><div>'+esc(meta.description || '')+'</div><div class="muted">\u6A21\u5F0F\uFF1A'+esc(meta.mode || '-')+' \xB7 \u9884\u8BBE\u58F0\u660E\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((meta.affects || []).join(' / ') || '-')</div><div class="muted">\u5B9E\u9645\u9884\u89C8\u547D\u4E2D\u533A\u57DF\uFF1A'+esc((meta.actualAffects || []).join(' / ') || '-')</div></div>';}function renderDraftPresetGuide(){ draftPresetList.innerHTML=Object.entries(draftPresets).map(([key,preset])=>'<div class="alert info"><strong>'+esc(preset.label || key)+'</strong><div>'+esc(preset.description || '')+'</div><div class="muted">\u5F71\u54CD\u8303\u56F4\uFF1A'+esc((preset.affects || []).join(' / '))+'</div></div>').join('');}function updateTopLevelModelSuggestionLists(){ const markup=knownModelIds.map(modelId=>'<option value="'+esc(modelId)+'"></option>').join(''); [topLevelTriggerIntentSuggestions,topLevelSmartRouterSuggestions,topLevelGovernanceSummarizerSuggestions,topLevelGovernanceClassifierSuggestions,topLevelGovernanceVerifierSuggestions].forEach(node=>{ if(node){ node.innerHTML=markup; } });}function renderModelsForm(models){ const list=Array.isArray(models) ? models : []; draftModelsCount.value=String(list.length); if(!list.length){ modelsFormGrid.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No draft models loaded yet</span></div>'; return; } modelsFormGrid.innerHTML=list.map((model,index)=>{ const templateContext=getProviderTemplateContext(model); const template=templateContext.template; return '<div class="model-card" data-model-card="'+index+'">' + '<div class="model-card-header"><strong>Model #'+(index+1)+'</strong><button type="button" data-remove-model="'+index+'">\u5220\u9664</button></div>' + '<div class="model-card-grid">' + '<div><label>Provider template</label><div class="row"><select data-field="provider_template" data-index="'+index+'"><option value="">custom</option>'+Object.entries(modelProviderTemplates).map(([key,item])=>'<option value="'+esc(key)+'"'+(model.provider_template === key ? ' selected' : '')+'>'+esc(item.label)+'</option>').join('')+'</select><button type="button" data-apply-template="'+index+'">\u5E94\u7528</button></div></div>' + '<div><label>ID</label><input data-field="id" data-index="'+index+'" value="'+esc(model.id || '')+'" placeholder="'+esc(template.suggested_id || 'sonnet')+'"><div class="muted">Router.default \u548C\u8DEF\u7531\u89C4\u5219\u5F15\u7528\u8FD9\u4E2A model id\uFF1B\u5EFA\u8BAE\u6A21\u677F\uFF1A'+esc(template.label || templateContext.templateKey || 'custom')+'</div></div>' + '<div><label>Interface</label><select data-field="interface" data-index="'+index+'"><option value="openai"'+(((model.interface || model.protocol || 'openai') === 'openai') ? ' selected' : '')+'>openai</option><option value="anthropic"'+(((model.interface || model.protocol) === 'anthropic') ? ' selected' : '')+'>anthropic</option></select><div class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 interface\uFF1B\u65E7 protocol \u4F1A\u81EA\u52A8\u8BFB\u53D6\u4E3A\u517C\u5BB9\u503C\u3002</div></div>' + '<div><label>Model</label><input data-field="model" data-index="'+index+'" list="modelSuggestions'+index+'" value="'+esc(model.model || '')+'" placeholder="'+esc(template.default_model || 'anthropic/claude-sonnet-4')+'"><datalist id="modelSuggestions'+index+'">'+((template.model_examples || []).map(item=>'<option value="'+esc(item)+'"></option>').join(''))+'</datalist><div class="muted">\u4E0A\u6E38\u771F\u5B9E\u6A21\u578B\u540D\uFF0C\u4F8B\u5982\uFF1A'+esc((template.model_examples || ['anthropic/claude-sonnet-4']).join(' / '))+'</div></div>' + '<div><label>API</label><input data-field="api" data-index="'+index+'" value="'+esc(model.api || model.api_base_url || '')+'" placeholder="'+esc(template.api || 'https://...')+'"><div class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 api\uFF1B\u65E7 api_base_url \u4EC5\u7528\u4E8E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div>' + '<div><label>Key</label><input data-field="key" data-index="'+index+'" value="'+esc(model.key || model.api_key || '')+'" placeholder="'+esc(template.key_placeholder || 'sk-...')+'"><div class="muted">\u65B0\u914D\u7F6E\u4F7F\u7528 key\uFF1B\u65E7 api_key \u4EC5\u7528\u4E8E\u517C\u5BB9\u8BFB\u53D6\u3002</div></div>' + '<div><label>Thinking</label><select data-field="thinking_profile" data-index="'+index+'"><option value="">default</option><option value="off"'+(((model.thinking === 'off') || model.thinking?.mode === 'off') ? ' selected' : '')+'>off</option><option value="auto"'+(((model.thinking === 'auto') || model.thinking?.mode === 'auto') ? ' selected' : '')+'>auto</option><option value="on"'+(((model.thinking === 'on') || (model.thinking?.mode === 'on' && !model.thinking?.effort)) ? ' selected' : '')+'>on</option><option value="low"'+(((model.thinking === 'low') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'low' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>low</option><option value="medium"'+(((model.thinking === 'medium') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'medium' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>medium</option><option value="high"'+(((model.thinking === 'high') || (model.thinking?.mode === 'on' && model.thinking?.effort === 'high' && !model.thinking?.budget_tokens)) ? ' selected' : '')+'>high</option><option value="custom"'+(((typeof model.thinking === 'object') && model.thinking && model.thinking.budget_tokens) ? ' selected' : '')+'>custom</option></select></div>' + '<div><label>Thinking mode</label><select data-field="thinking_mode" data-index="'+index+'"><option value="">default</option><option value="off"'+(model.thinking?.mode === 'off' ? ' selected' : '')+'>off</option><option value="auto"'+(model.thinking?.mode === 'auto' ? ' selected' : '')+'>auto</option><option value="on"'+(model.thinking?.mode === 'on' ? ' selected' : '')+'>on</option></select></div>' + '<div><label>Thinking effort</label><select data-field="thinking_effort" data-index="'+index+'"><option value="">default</option><option value="low"'+(model.thinking?.effort === 'low' ? ' selected' : '')+'>low</option><option value="medium"'+(model.thinking?.effort === 'medium' ? ' selected' : '')+'>medium</option><option value="high"'+(model.thinking?.effort === 'high' ? ' selected' : '')+'>high</option></select></div>' + '<div><label>Thinking budget</label><input data-field="thinking_budget_tokens" data-index="'+index+'" value="'+esc(model.thinking?.budget_tokens || '')+'" placeholder="1024"></div>' + '<div><label>Vendor hint</label><input data-field="vendor_hint" data-index="'+index+'" value="'+esc(model.metadata?.vendor_hint || '')+'" placeholder="'+esc(template.vendor_hint || 'openrouter')+'"></div>' + '<div><label>Reasoning support</label><select data-field="supports_reasoning" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_reasoning === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_reasoning === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Tool support</label><select data-field="supports_tools" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_tools === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_tools === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div><label>Image support</label><select data-field="supports_images" data-index="'+index+'"><option value="">default</option><option value="true"'+(model.metadata?.supports_images === true ? ' selected' : '')+'>supported</option><option value="false"'+(model.metadata?.supports_images === false ? ' selected' : '')+'>disabled</option></select></div>' + '<div style="grid-column:1/-1"><label>Metadata (advanced JSON)</label><textarea data-field="metadata" data-index="'+index+'" placeholder="{\\"label\\":\\"Balanced profile\\"}">'+esc(model.metadata ? JSON.stringify(model.metadata, null, 2) : '')+'</textarea><div class="muted">\u666E\u901A capability \u5EFA\u8BAE\u4F18\u5148\u4F7F\u7528\u4E0A\u9762\u7684\u663E\u5F0F\u5B57\u6BB5\uFF1B\u8FD9\u91CC\u4FDD\u7559\u7ED9\u9AD8\u7EA7\u6269\u5C55\u5143\u6570\u636E\u3002</div></div>' + '</div>' + '</div>'; }).join('');}function extractModelsFromForm(){ const cards=Array.from(modelsFormGrid.querySelectorAll('[data-model-card]')); return cards.map((card,index)=>{ const read=(field)=>card.querySelector('[data-field="'+field+'"][data-index="'+index+'"]'); const providerTemplate=(read('provider_template')?.value || '').trim(); const metadataRaw=(read('metadata')?.value || '').trim(); let metadata; if(metadataRaw){ metadata=JSON.parse(metadataRaw); } else { metadata={}; } const thinkingProfile=(read('thinking_profile')?.value || '').trim(); const vendorHint=(read('vendor_hint')?.value || '').trim(); const supportsReasoning=(read('supports_reasoning')?.value || '').trim(); const supportsTools=(read('supports_tools')?.value || '').trim(); const supportsImages=(read('supports_images')?.value || '').trim(); const thinking={}; const mode=(read('thinking_mode')?.value || '').trim(); const effort=(read('thinking_effort')?.value || '').trim(); const budget=(read('thinking_budget_tokens')?.value || '').trim(); if(mode) thinking.mode=mode; if(effort) thinking.effort=effort; if(budget) thinking.budget_tokens=Number(budget); const model={ id:(read('id')?.value || '').trim(), api:(read('api')?.value || '').trim(), key:(read('key')?.value || '').trim(), interface:(read('interface')?.value || '').trim(), model:(read('model')?.value || '').trim(), }; if(vendorHint){ metadata.vendor_hint=vendorHint; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'vendor_hint')){ delete metadata.vendor_hint; } if(supportsReasoning){ metadata.supports_reasoning=supportsReasoning === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_reasoning')){ delete metadata.supports_reasoning; } if(supportsTools){ metadata.supports_tools=supportsTools === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_tools')){ delete metadata.supports_tools; } if(supportsImages){ metadata.supports_images=supportsImages === 'true'; } else if(metadata && Object.prototype.hasOwnProperty.call(metadata,'supports_images')){ delete metadata.supports_images; } if(providerTemplate){ model.provider_template=providerTemplate; } if(thinkingProfile && thinkingProfile !== 'custom'){ model.thinking=thinkingProfile; } else if(Object.keys(thinking).length){ model.thinking=thinking; } if(metadata !== undefined && Object.keys(metadata).length){ model.metadata=metadata; } return model; });}function applyProviderTemplate(index){ const card=modelsFormGrid.querySelector('[data-model-card="'+index+'"]'); if(!card){ return; } const templateKey=(card.querySelector('[data-field="provider_template"][data-index="'+index+'"]')?.value || '').trim(); const template=modelProviderTemplates[templateKey]; if(!template){ return; } const modelInterface=card.querySelector('[data-field="interface"][data-index="'+index+'"]'); const apiBaseUrl=card.querySelector('[data-field="api"][data-index="'+index+'"]'); const modelInput=card.querySelector('[data-field="model"][data-index="'+index+'"]'); if(modelInterface){ modelInterface.value=template.interface || template.protocol; } if(apiBaseUrl && !apiBaseUrl.value.trim()){ apiBaseUrl.value=template.api || template.api_base_url; } else if(apiBaseUrl){ apiBaseUrl.value=template.api || template.api_base_url; } if(modelInput){ modelInput.placeholder=template.default_model || modelInput.placeholder; if(!modelInput.value.trim() && template.default_model){ modelInput.value=template.default_model; } } const modelIdInput=card.querySelector('[data-field="id"][data-index="'+index+'"]'); if(modelIdInput){ modelIdInput.placeholder=template.suggested_id || modelIdInput.placeholder; if(!modelIdInput.value.trim() && template.suggested_id){ modelIdInput.value=template.suggested_id; } } const keyInput=card.querySelector('[data-field="key"][data-index="'+index+'"]'); if(keyInput && template.key_placeholder){ keyInput.placeholder=template.key_placeholder; } const vendorHintInput=card.querySelector('[data-field="vendor_hint"][data-index="'+index+'"]'); if(vendorHintInput && template.vendor_hint){ vendorHintInput.placeholder=template.vendor_hint; } const thinkingProfile=card.querySelector('[data-field="thinking_profile"][data-index="'+index+'"]'); if(thinkingProfile && !thinkingProfile.value && template.default_thinking){ thinkingProfile.value=template.default_thinking; } const nextModels=extractModelsFromForm(); if(nextModels[index]){ nextModels[index]={ ...nextModels[index], provider_template: templateKey }; } renderModelsForm(nextModels);}function renderTriggerRulesList(rules){ const list=Array.isArray(rules) ? rules : []; if(!list.length){ triggerRulesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No routing rules yet</span></div>'; return; } triggerRulesList.innerHTML=list.map((rule,index)=>'<div class="list-item" data-trigger-rule="'+index+'">' + '<div class="action-row"><strong>Rule #'+(index+1)+'</strong><button type="button" data-remove-trigger-rule="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Name</label><input data-trigger-field="name" data-index="'+index+'" value="'+esc(rule.name || '')+'"></div>' + '<div><label>Model</label><input data-trigger-field="model" data-index="'+index+'" list="triggerModelSuggestions'+index+'" value="'+esc(rule.model || '')+'">'+getModelIdSuggestionsMarkup('triggerModelSuggestions'+index)+'</div>' + '<div><label>Priority</label><input data-trigger-field="priority" data-index="'+index+'" value="'+esc(rule.priority ?? 10)+'"></div>' + '<div><label><input type="checkbox" data-trigger-field="enabled" data-index="'+index+'"'+(rule.enabled === false ? '' : ' checked')+'> Enabled</label></div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-trigger-field="description" data-index="'+index+'" value="'+esc(rule.description || '')+'"></div>' + '</div>' + '<div class="action-row" style="margin-top:.75rem"><strong>Patterns</strong><button type="button" data-add-trigger-pattern="'+index+'">\u65B0\u589E Pattern</button></div>' + '<div class="list-editor">'+(((rule.patterns || []).length ? rule.patterns : [{ type:'exact', keywords:[] }]).map((pattern,patternIndex)=>'<div class="list-item" data-trigger-pattern="'+index+'-'+patternIndex+'">' + '<div class="action-row"><span class="muted">Pattern #'+(patternIndex+1)+'</span><span class="pill">'+esc(pattern.type || 'exact')+'</span><span class="muted">'+esc(getTriggerPatternValidationHint(pattern).message)+'</span><button type="button" data-remove-trigger-pattern="'+index+'" data-pattern-index="'+patternIndex+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Type</label><select data-trigger-pattern-field="type" data-index="'+index+'" data-pattern-index="'+patternIndex+'"><option value="exact"'+(pattern.type !== 'regex' ? ' selected' : '')+'>exact</option><option value="regex"'+(pattern.type === 'regex' ? ' selected' : '')+'>regex</option></select></div>' + '<div><label><input type="checkbox" data-trigger-pattern-field="caseSensitive" data-index="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.caseSensitive ? ' checked' : '')+'> Case sensitive</label></div>' + '<div style="grid-column:1/-1"><div class="action-row"><label>Keywords</label><button type="button" data-add-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u65B0\u589E Keyword</button></div><div class="list-editor">'+((((pattern.keywords || []).length ? pattern.keywords : ['']).map((keyword,keywordIndex)=>'<div class="list-item" data-trigger-keyword="'+index+'-'+patternIndex+'-'+keywordIndex+'"><div class="action-row"><span class="muted">Keyword #'+(keywordIndex+1)+'</span><button type="button" data-remove-trigger-keyword="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'"'+(pattern.type === 'regex' ? ' disabled' : '')+'>\u5220\u9664</button></div><input data-trigger-pattern-field="keyword_item" data-index="'+index+'" data-pattern-index="'+patternIndex+'" data-keyword-index="'+keywordIndex+'" value="'+esc(keyword || '')+'" placeholder="keyword"'+(pattern.type === 'regex' ? ' disabled' : '')+'></div>')).join(''))+'</div><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u5FFD\u7565 keywords' : 'exact \u6A21\u5F0F\u4E0B\u6309\u5173\u952E\u8BCD\u5217\u8868\u5339\u914D')+'</div></div>' + '<div style="grid-column:1/-1"><label>Regex pattern</label><input data-trigger-pattern-field="pattern" data-index="'+index+'" data-pattern-index="'+patternIndex+'" value="'+esc(pattern.pattern || '')+'" placeholder="error|exception"'+(pattern.type === 'regex' ? '' : ' disabled')+'><div class="muted">'+(pattern.type === 'regex' ? 'regex \u6A21\u5F0F\u4E0B\u4F7F\u7528\u6B63\u5219\u8868\u8FBE\u5F0F\u5339\u914D' : 'exact \u6A21\u5F0F\u4E0B\u5FFD\u7565 regex pattern')+'</div></div>' + '</div>' + '</div>').join(''))+'</div>' + '</div>').join('');}function extractTriggerRulesFromForm(){ return Array.from(triggerRulesList.querySelectorAll('[data-trigger-rule]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-trigger-field="'+field+'"][data-index="'+index+'"]'); const patterns=Array.from(card.querySelectorAll('[data-trigger-pattern]')).map((patternCard,patternIndex)=>{ const patternRead=(field)=>patternCard.querySelector('[data-trigger-pattern-field="'+field+'"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]'); const type=(patternRead('type')?.value || 'exact').trim(); const pattern={ type, caseSensitive:Boolean(patternRead('caseSensitive')?.checked) }; const keywords=Array.from(patternCard.querySelectorAll('[data-trigger-pattern-field="keyword_item"][data-index="'+index+'"][data-pattern-index="'+patternIndex+'"]')).map((node)=>node.value.trim()).filter(Boolean); const regexPattern=(patternRead('pattern')?.value || '').trim(); if(type === 'regex'){ if(regexPattern){ pattern.pattern=regexPattern; } } else if(keywords.length){ pattern.keywords=keywords; } return pattern; }); const rule={ name:(read('name')?.value || '').trim(), model:(read('model')?.value || '').trim(), priority:Number(read('priority')?.value || 10), enabled:Boolean(read('enabled')?.checked), patterns }; const description=(read('description')?.value || '').trim(); if(description){ rule.description=description; } return rule; });}function renderSmartCandidatesList(candidates){ const list=Array.isArray(candidates) ? candidates : []; if(!list.length){ smartCandidatesList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No smart candidates yet</span></div>'; return; } smartCandidatesList.innerHTML=list.map((candidate,index)=>'<div class="list-item" data-smart-candidate="'+index+'">' + '<div class="action-row"><strong>Candidate #'+(index+1)+'</strong><button type="button" data-remove-smart-candidate="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>Model</label><input data-smart-field="model" data-index="'+index+'" list="smartModelSuggestions'+index+'" value="'+esc(candidate.model || '')+'">'+getModelIdSuggestionsMarkup('smartModelSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Description</label><input data-smart-field="description" data-index="'+index+'" value="'+esc(candidate.description || '')+'"></div>' + '</div>' + '</div>').join('');}function extractSmartCandidatesFromForm(){ return Array.from(smartCandidatesList.querySelectorAll('[data-smart-candidate]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-smart-field="'+field+'"][data-index="'+index+'"]'); return { model:(read('model')?.value || '').trim(), description:(read('description')?.value || '').trim() }; });}function renderCascadeLevelsList(levels){ const list=Array.isArray(levels) ? levels : []; if(!list.length){ governanceCascadeLevelsList.innerHTML='<div class="panel" style="margin-bottom:0"><span class="muted">No cascade levels yet</span></div>'; return; } governanceCascadeLevelsList.innerHTML=list.map((level,index)=>'<div class="list-item" data-cascade-level="'+index+'">' + '<div class="action-row"><strong>Level #'+(index+1)+'</strong><button type="button" data-remove-cascade-level="'+index+'">\u5220\u9664</button></div>' + '<div class="list-item-grid">' + '<div><label>From</label><input data-cascade-field="from" data-index="'+index+'" list="cascadeFromSuggestions'+index+'" value="'+esc(level.from || '')+'">'+getModelIdSuggestionsMarkup('cascadeFromSuggestions'+index)+'</div>' + '<div><label>To</label><input data-cascade-field="to" data-index="'+index+'" list="cascadeToSuggestions'+index+'" value="'+esc(level.to || '')+'">'+getModelIdSuggestionsMarkup('cascadeToSuggestions'+index)+'</div>' + '<div style="grid-column:1/-1"><label>Reason</label><input data-cascade-field="reason" data-index="'+index+'" value="'+esc(level.reason || '')+'"></div>' + '</div>' + '</div>').join('');}function extractCascadeLevelsFromForm(){ return Array.from(governanceCascadeLevelsList.querySelectorAll('[data-cascade-level]')).map((card,index)=>{ const read=(field)=>card.querySelector('[data-cascade-field="'+field+'"][data-index="'+index+'"]'); const level={ from:(read('from')?.value || '').trim(), to:(read('to')?.value || '').trim() }; const reason=(read('reason')?.value || '').trim(); if(reason){ level.reason=reason; } return level; });}function buildDraftPayloadFromForm(){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); payload.Models=extractModelsFromForm(); const routerDefault=(draftRouterDefault.value || '').trim(); if(routerDefault){ payload.Router={ ...(payload.Router || {}), default: routerDefault }; } else if(payload.Router){ delete payload.Router.default; if(!Object.keys(payload.Router).length){ delete payload.Router; } } const triggerRules=extractTriggerRulesFromForm(); const smartCandidates=extractSmartCandidatesFromForm(); const smartRouterEnabled=Boolean(smartEnabled.checked || triggerEnabled.checked || triggerIntentEnabled.checked || triggerIntentModel.value.trim() || triggerRules.length || smartRouterModel.value.trim() || smartCandidates.length || smartCacheTtl.value.trim() || smartMaxTokens.value.trim() || governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim() || governanceSemanticEnabled.checked || governanceClassifierModel.value.trim()); if(smartRouterEnabled){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: true, analysis_scope: triggerAnalysisScope.value || payload.SmartRouter?.analysis_scope || 'last_message', router_model: smartRouterModel.value.trim(), fallback: smartFallback.value || 'default', candidates: smartCandidates, cache_ttl: smartCacheTtl.value.trim() ? Number(smartCacheTtl.value.trim()) : undefined, max_tokens: smartMaxTokens.value.trim() ? Number(smartMaxTokens.value.trim()) : undefined, rules: triggerRules, semantic:(governanceSemanticEnabled.checked || triggerIntentEnabled.checked || governanceClassifierModel.value.trim() || triggerIntentModel.value.trim()) ? { ...(((payload.SmartRouter || {}).semantic) || {}), enabled:Boolean(governanceSemanticEnabled.checked || triggerIntentEnabled.checked), mode:'classifier', classifier_model: governanceClassifierModel.value.trim() || triggerIntentModel.value.trim() } : undefined, sticky:(governanceAlignmentEnabled.checked || governanceSummarizerModel.value.trim()) ? { ...(((payload.SmartRouter || {}).sticky) || {}), enabled:true, alignment:{ ...((((payload.SmartRouter || {}).sticky || {}).alignment) || {}), enabled:Boolean(governanceAlignmentEnabled.checked), summarizer_model: governanceSummarizerModel.value.trim() } } : undefined }; } else { delete payload.SmartRouter; } delete payload.TriggerRouter; const cascadeLevels=extractCascadeLevelsFromForm(); if(governanceEnabled.checked || governanceShadowEnabled.checked || governanceVerifierModel.value.trim() || cascadeLevels.length){ payload.Governance={ ...(payload.Governance || {}), enabled: governanceEnabled.checked, shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: governanceShadowEnabled.checked, verifier_model: governanceVerifierModel.value.trim() }, cascade:{ ...((payload.Governance && payload.Governance.cascade) || {}), enabled: Boolean(cascadeLevels.length), levels: cascadeLevels } }; } else { delete payload.Governance; } return payload;}function renderConfigControlForms(config){ const smart=getDraftSmartRouterConfig(config); const trigger=config?.TriggerRouter || {}; triggerEnabled.checked=Boolean(smart.enabled); triggerIntentEnabled.checked=Boolean(smart.semantic?.enabled && smart.semantic?.mode === 'classifier'); triggerAnalysisScope.value=smart.analysis_scope || 'last_message'; triggerIntentModel.value=smart.semantic?.classifier_model || trigger.intent_model || ''; renderTriggerRulesList(smart.rules || trigger.rules || []); smartEnabled.checked=Boolean(smart.enabled); smartRouterModel.value=smart.router_model || ''; smartFallback.value=smart.fallback || 'default'; smartCacheTtl.value=smart.cache_ttl ?? ''; smartMaxTokens.value=smart.max_tokens ?? ''; renderSmartCandidatesList(smart.candidates || []); const governance=config?.Governance || {}; governanceEnabled.checked=Boolean(governance.enabled); governanceAlignmentEnabled.checked=Boolean(smart.sticky?.alignment?.enabled); governanceSummarizerModel.value=smart.sticky?.alignment?.summarizer_model || ''; governanceSemanticEnabled.checked=Boolean(smart.semantic?.enabled); governanceClassifierModel.value=smart.semantic?.classifier_model || ''; governanceShadowEnabled.checked=Boolean(governance.shadow?.enabled); governanceVerifierModel.value=governance.shadow?.verifier_model || ''; renderCascadeLevelsList(governance.cascade?.levels || []);}function syncDraftEditorFromForm(){ try { const payload=buildDraftPayloadFromForm(); currentDraftConfig=payload; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u540C\u6B65 Models \u8868\u5355\u5230 JSON \u8349\u7A3F'; } catch (error) { draftPreviewStatus.textContent='\u540C\u6B65\u5931\u8D25\uFF1A'+error.message; }}function applyReferenceSuggestion(path,modelId){ if(!modelId){ return; } if(path==='Router.default'){ draftRouterDefault.value=modelId; syncDraftEditorFromForm(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 Router.default'; return; } const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const pathMatch=path.match(/^([^.[]+)(?:.(.+))?$/); if(!pathMatch){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\uFF1A'+path; return; } const tokens=path.replace(/[(d+)]/g,'.$1').split('.'); let cursor=payload; for(let i=0;i<tokens.length-1;i++){ const token=tokens[i]; const nextToken=tokens[i+1]; if(cursor[token] === undefined){ cursor[token]=String(Number(nextToken))===nextToken ? [] : {}; } cursor=cursor[token]; } cursor[tokens[tokens.length-1]]=modelId; currentDraftConfig=payload; if(payload.Router?.default){ draftRouterDefault.value=payload.Router.default; } renderConfigControlForms(payload); configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5C06\u5EFA\u8BAE\u6A21\u578B\u5E94\u7528\u5230 '+path+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function applyCapabilityWarningSuggestion(path,code){ const payload=JSON.parse(JSON.stringify(currentDraftConfig || {})); const tokens=String(path || '').replace(/[(d+)]/g,'.$1').split('.').filter(Boolean); if(!tokens.length){ draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } let cursor=payload; for(let i=0;i<tokens.length-1;i++){ if(cursor == null){ break; } cursor=cursor[tokens[i]]; } const lastToken=tokens[tokens.length-1]; if(code==='thinking_ignored'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } } else if(code==='tools_text_fallback' || code==='images_text_fallback'){ if(cursor && Object.prototype.hasOwnProperty.call(cursor,lastToken)){ delete cursor[lastToken]; } if(cursor && !Object.keys(cursor).length){ const parentTokens=tokens.slice(0,-1); const maybeMetadataKey=parentTokens[parentTokens.length-1]; if(maybeMetadataKey==='metadata'){ let parentCursor=payload; for(let i=0;i<parentTokens.length-1;i++){ if(parentCursor == null){ break; } parentCursor=parentCursor[parentTokens[i]]; } if(parentCursor && Object.prototype.hasOwnProperty.call(parentCursor,'metadata')){ delete parentCursor.metadata; } } } } else { draftPreviewStatus.textContent='\u6682\u4E0D\u652F\u6301\u81EA\u52A8\u4FEE\u590D\u8BE5 warning'; return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528 warning \u4FEE\u6B63\uFF1A'+code+'\uFF0C\u53EF\u91CD\u65B0\u9884\u89C8\u9A8C\u8BC1';}function renderCompiledDiff(diff){ const summary=diff?.summary || {}; compiledDiffSummary.innerHTML=[ ['Added providers', summary.addedProviders ?? 0], ['Removed providers', summary.removedProviders ?? 0], ['Changed providers', summary.changedProviders ?? 0], ['Added models', summary.addedModels ?? 0], ['Removed models', summary.removedModels ?? 0], ['Changed models', summary.changedModels ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const rows=[ ...((diff?.providerChanges || []).map(item=>({ scope:'provider', key:item.name, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ...((diff?.modelChanges || []).map(item=>({ scope:'model', key:item.modelId, type:item.type, fields:item.fields || [], target:item.after || item.before || {} }))), ]; compiledDiffTableBody.innerHTML=rows.length ? rows.map(item=>'<tr>' + '<td>'+esc(item.scope)+'</td>' + '<td>'+esc(item.type)+'</td>' + '<td><code>'+esc(item.key)+'</code></td>' + '<td>'+esc(item.fields.join(', ') || '-')+'</td>' + '<td><code>'+esc(item.target.providerName || item.target.name || '-')+'</code><div class="muted">'+esc(item.target.modelName || (item.target.models || []).join(', ') || '-')}</div></td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled registry changes</td></tr>';}function renderReferenceImpact(impact){ const summary=impact?.summary || {}; referenceImpactSummary.innerHTML=[ ['Total refs', summary.total ?? 0], ['modelId refs', summary.modelIdRefs ?? 0], ['Legacy refs', summary.legacyRefs ?? 0], ['Valid modelIds', summary.validModelIds ?? 0], ['Missing modelIds', summary.missingModelIds ?? 0] ].map(([label,value])=>'<div class="diff-chip"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join(''); const entries=impact?.entries || []; referenceImpactTableBody.innerHTML=entries.length ? entries.map(item=>'<tr>' + '<td><code>'+esc(item.path)+'</code></td>' + '<td><code>'+esc(item.value)+'</code></td>' + '<td>'+esc(item.referenceType)+'</td>' + '<td>'+esc(item.status)+'</td>' + '<td><code>'+esc(item.resolvedTarget?.providerName || '-')+'</code><div class="muted">'+esc(item.resolvedTarget?.modelName || '-')}</div></td>' + '<td>'+((item.suggestions || []).length ? item.suggestions.map(s=>'<div><code>'+esc(s.modelId)+'</code><div class="muted">'+esc(s.modelName || '-')+'</div><button type="button" data-apply-reference-path="'+esc(item.path)+'" data-apply-reference-model="'+esc(s.modelId)+'">\u5E94\u7528\u5EFA\u8BAE</button></div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>').join('') : '<tr><td colspan="6" class="muted">No model references found</td></tr>';}function renderCompiledModels(data){ const providers=Array.isArray(data.providers) ? data.providers : []; const modelMapEntries=Object.entries(data.modelMap || {}); const modelPoolEntries=Object.entries(data.modelPools || {}); const modelPoolEndpointCount=modelPoolEntries.reduce((sum,[_modelId,pool])=>sum+((pool.endpoints || []).length),0); knownModelIds=modelMapEntries.map(([modelId])=>modelId).sort(); updateTopLevelModelSuggestionLists(); renderCapabilityWarnings(data.capabilityWarnings); compiledModelsStatus.textContent='\u5DF2\u52A0\u8F7D '+providers.length+' \u4E2A compiled provider / '+modelMapEntries.length+' \u4E2A modelId \u6620\u5C04 / '+modelPoolEntries.length+' \u4E2A model pool / '+modelPoolEndpointCount+' \u4E2A pool endpoint'; compiledProvidersTableBody.innerHTML=providers.length ? providers.map(provider=>'<tr>' + '<td><code>'+esc(provider.name)+'</code><div class="muted">'+esc(provider.api_base_url || '-')+'</div></td>' + '<td>'+esc(provider.transformer?.use?.[0] || '-')+'</td>' + '<td>'+esc((provider.models || []).join(', ') || '-')+'</td>' + '<td>'+esc(JSON.stringify(provider.transformer || {}))+'</td>' + '<td>'+esc(provider.has_api_key ? 'configured' : 'missing')+'</td>' + '</tr>').join('') : '<tr><td colspan="5" class="muted">No compiled providers</td></tr>'; compiledModelMapTableBody.innerHTML=modelMapEntries.length ? modelMapEntries.map(([modelId,item])=>'<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td><code>'+esc(item.providerName || '-')+'</code><div class="muted">'+esc(item.modelName || '-')+'</div></td>' + '<td>'+esc(item.protocol || '-')+'</td>' + '<td>'+esc(item.compatibilityProfile || '-')+'</td>' + '<td>'+esc(item.dispatchFormat || '-')+'</td>' + '<td><code>'+esc(JSON.stringify(item.thinking || { mode: 'off' }))+'</code></td>' + '<td><code>'+esc(JSON.stringify(item.capabilities || {}))+'</code></td>' + '<td>'+esc(item.source || '-')+'</td>' + '</tr>').join('') : '<tr><td colspan="8" class="muted">No compiled model map</td></tr>'; compiledModelPoolsTableBody.innerHTML=modelPoolEntries.length ? modelPoolEntries.map(([modelId,pool])=>{ const endpoints=pool.endpoints || []; return '<tr>' + '<td><code>'+esc(modelId)+'</code></td>' + '<td>'+esc(pool.strategy || '-')+'</td>' + '<td><code>'+esc(pool.activeEndpointId || '-')+'</code></td>' + '<td>'+endpoints.map(endpoint=>'<div><code>'+esc(endpoint.id)+'</code><span class="muted"> priority '+esc(endpoint.priority)+' / '+esc(endpoint.enabled ? 'enabled' : 'disabled')+'</span><div class="muted">'+esc(endpoint.upstreamServiceId || endpoint.api || '-')+'</div></div>').join('')+'</td>' + '<td>'+((pool.warnings || []).length ? pool.warnings.map(w=>'<div class="warning-text">'+esc(w)+'</div>').join('') : '<span class="muted">-</span>')+'</td>' + '</tr>'; }).join('') : '<tr><td colspan="5" class="muted">No compiled model pools</td></tr>'; if(data.diff){ renderCompiledDiff(data.diff); } if(data.referenceImpact){ renderReferenceImpact(data.referenceImpact); } renderConfigControlForms(currentDraftConfig);}async function loadConfigDraft(){ draftPreviewStatus.textContent='\u52A0\u8F7D\u5F53\u524D\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config'); const data=await res.json(); currentDraftConfig=data || {}; renderModelsForm(currentDraftConfig.Models || []); renderConfigControlForms(currentDraftConfig); draftRouterDefault.value=currentDraftConfig.Router?.default || ''; configDraftEditor.value=JSON.stringify(data,null,2); renderDraftSummary(currentDraftConfig); updateStatusSummary(currentDraftConfig); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u8F7D\u5165\u5F53\u524D\u914D\u7F6E\uFF0C\u53EF\u901A\u8FC7 Models \u8868\u5355\u6216 JSON \u8349\u7A3F\u7F16\u8F91';}async function previewConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u8349\u7A3F\u89E3\u6790\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u9884\u89C8\u7F16\u8BD1\u7ED3\u679C\u4E2D...'; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ draftPreviewStatus.textContent='\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta(); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u9884\u89C8\u5B8C\u6210\uFF1A\u5DF2\u6309\u8349\u7A3F\u914D\u7F6E\u5237\u65B0 compiled models';}async function loadServiceStatus(){ serviceReadyStatus.textContent='checking'; try { const [serviceRes,remoteRes]=await Promise.all([fetch('/api/service-info'),fetch('/api/remote-status')]); const data=await serviceRes.json(); const remoteData=await remoteRes.json(); serviceReadyStatus.textContent=data.ready ? 'ready' : 'not ready'; servicePortStatus.textContent=data.port || '-'; serviceModeStatus.textContent=data.runtimeMode || '-'; serviceRoleStatus.textContent=data.serviceRole || '-'; renderRoleConnectionGuide(data); const auth=data.auth || {}; const managed=auth.managedKeys || {}; const quota=auth.quota || {}; const quotaText=Number.isFinite(quota.requestsUsed) ? (' \xB7 quota '+quota.requestsUsed+' req'+(quota.windowResetAt ? ' \xB7 reset '+String(quota.windowResetAt).replace('T',' ').replace('.000Z','Z') : '')) : ''; authStatusSummary.textContent=auth.required ? ((auth.bootstrapConfigured ? 'bootstrap' : 'managed')+' \xB7 '+(managed.active ?? 0)+' active'+quotaText) : 'not configured'; renderAuthQuotaTable(quota); const security=data.security || {}; const issues=Array.isArray(security.issues) ? security.issues : []; securityStatusSummary.textContent=security.status || '-'; securitySummary.className='alert '+((security.status === 'critical') ? 'critical' : (security.status === 'warning' ? 'warn' : 'info')); securitySummary.innerHTML='<strong>Security: '+esc(security.status || '-')+'</strong><div>'+esc(issues[0]?.message || '\u5F53\u524D\u670D\u52A1\u672A\u53D1\u73B0\u660E\u663E\u9274\u6743\u66B4\u9732\u98CE\u9669')+'</div>'+ (issues.length ? '<ul class="mini-list">'+issues.map(issue=>'<li>'+esc(issue.action || issue.code)+'</li>').join('')+'</ul>' : ''); const registration=data.registration || {}; registrationStatusSummary.textContent=registration.enabled ? ((registration.models ?? 0)+' models / '+(registration.upstreamServices ?? 0)+' upstream') : 'disabled'; const remote=remoteData.remote || {}; remoteStatusSummary.textContent=remote.enabled ? ((remote.ready ? 'ready' : (remote.reachable ? 'reachable' : 'unreachable'))+' \xB7 '+(remote.baseUrl || '-')) : 'disabled'; if(remoteData.compiledModels){ modelCountStatus.textContent=remoteData.compiledModels.modelCount ?? modelCountStatus.textContent; } } catch (_error) { serviceReadyStatus.textContent='unreachable'; remoteStatusSummary.textContent='unknown'; securityStatusSummary.textContent='unknown'; }}async function saveConfigDraft(){ let payload; try { payload=buildDraftPayloadFromForm(); configDraftEditor.value=JSON.stringify(payload,null,2); } catch (error) { renderDraftValidation(['JSON parse error: '+error.message],[]); renderCapabilityWarnings(); draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+error.message; return; } draftPreviewStatus.textContent='\u4FDD\u5B58\u914D\u7F6E\u4E2D...'; const res=await fetch('/api/config',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); renderDraftValidation(data.errors || [], data.warnings || [], data.issueReport); if(!res.ok){ draftPreviewStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } currentDraftConfig=payload; await loadCompiledModels(); draftPreviewStatus.textContent='\u5DF2\u4FDD\u5B58\u914D\u7F6E'+((data.warnings || []).length ? ('\uFF08\u542B '+data.warnings.length+' \u6761 warning\uFF09') : '');}function addDraftModel(){ const nextModels=extractModelsFromForm(); nextModels.push(createDraftModelFromTemplate(defaultProviderTemplateKey)); renderModelsForm(nextModels); syncDraftEditorFromForm();}function addTriggerRule(){ const next=extractTriggerRulesFromForm(); next.push({ name:'', enabled:true, priority:10, model:'', patterns:[{ type:'exact', keywords:[] }] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerPattern(ruleIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex]){ return; } next[ruleIndex].patterns = Array.isArray(next[ruleIndex].patterns) ? next[ruleIndex].patterns : []; next[ruleIndex].patterns.push({ type:'exact', keywords:[] }); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addTriggerKeyword(ruleIndex,patternIndex){ const next=extractTriggerRulesFromForm(); if(!next[ruleIndex] || !next[ruleIndex].patterns || !next[ruleIndex].patterns[patternIndex]){ return; } const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=Array.isArray(pattern.keywords) ? pattern.keywords : []; pattern.keywords.push(''); renderTriggerRulesList(next); syncDraftEditorFromForm(); }function addSmartCandidate(){ const next=extractSmartCandidatesFromForm(); next.push({ model:'', description:'' }); renderSmartCandidatesList(next); syncDraftEditorFromForm(); }function addCascadeLevel(){ const next=extractCascadeLevelsFromForm(); next.push({ from:'', to:'' }); renderCascadeLevelsList(next); syncDraftEditorFromForm(); }modelsFormGrid.addEventListener('input',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('change',()=>syncDraftEditorFromForm());modelsFormGrid.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-template]'); if(applyBtn){ const applyIndex=Number(applyBtn.dataset.applyTemplate); applyProviderTemplate(applyIndex); syncDraftEditorFromForm(); return; } const btn=e.target.closest('button[data-remove-model]'); if(!btn){ return; } const removeIndex=Number(btn.dataset.removeModel); const nextModels=extractModelsFromForm().filter((_,index)=>index!==removeIndex); renderModelsForm(nextModels); syncDraftEditorFromForm(); });triggerRulesList.addEventListener('input',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('change',()=>syncDraftEditorFromForm());triggerRulesList.addEventListener('click',(e)=>{ const addKeywordBtn=e.target.closest('button[data-add-trigger-keyword]'); if(addKeywordBtn){ addTriggerKeyword(Number(addKeywordBtn.dataset.addTriggerKeyword), Number(addKeywordBtn.dataset.patternIndex)); return; } const removeKeywordBtn=e.target.closest('button[data-remove-trigger-keyword]'); if(removeKeywordBtn){ const ruleIndex=Number(removeKeywordBtn.dataset.removeTriggerKeyword); const patternIndex=Number(removeKeywordBtn.dataset.patternIndex); const keywordIndex=Number(removeKeywordBtn.dataset.keywordIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex] && next[ruleIndex].patterns && next[ruleIndex].patterns[patternIndex]){ const pattern=next[ruleIndex].patterns[patternIndex]; pattern.keywords=(pattern.keywords || []).filter((_,index)=>index!==keywordIndex); if(!pattern.keywords.length){ pattern.keywords=['']; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const addBtn=e.target.closest('button[data-add-trigger-pattern]'); if(addBtn){ addTriggerPattern(Number(addBtn.dataset.addTriggerPattern)); return; } const removePatternBtn=e.target.closest('button[data-remove-trigger-pattern]'); if(removePatternBtn){ const ruleIndex=Number(removePatternBtn.dataset.removeTriggerPattern); const patternIndex=Number(removePatternBtn.dataset.patternIndex); const next=extractTriggerRulesFromForm(); if(next[ruleIndex]){ next[ruleIndex].patterns=(next[ruleIndex].patterns || []).filter((_,index)=>index!==patternIndex); if(!next[ruleIndex].patterns.length){ next[ruleIndex].patterns=[{ type:'exact', keywords:[] }]; } renderTriggerRulesList(next); syncDraftEditorFromForm(); } return; } const btn=e.target.closest('button[data-remove-trigger-rule]'); if(!btn){ return; } const next=extractTriggerRulesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeTriggerRule)); renderTriggerRulesList(next); syncDraftEditorFromForm(); });smartCandidatesList.addEventListener('input',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('change',()=>syncDraftEditorFromForm());smartCandidatesList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-smart-candidate]'); if(!btn){ return; } const next=extractSmartCandidatesFromForm().filter((_,index)=>index!==Number(btn.dataset.removeSmartCandidate)); renderSmartCandidatesList(next); syncDraftEditorFromForm(); });governanceCascadeLevelsList.addEventListener('input',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('change',()=>syncDraftEditorFromForm());governanceCascadeLevelsList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-remove-cascade-level]'); if(!btn){ return; } const next=extractCascadeLevelsFromForm().filter((_,index)=>index!==Number(btn.dataset.removeCascadeLevel)); renderCascadeLevelsList(next); syncDraftEditorFromForm(); });referenceImpactTableBody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-apply-reference-path]'); if(!btn){ return; } applyReferenceSuggestion(btn.dataset.applyReferencePath, btn.dataset.applyReferenceModel); });draftValidationList.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });capabilityWarningsList.addEventListener('click',(e)=>{ const applyBtn=e.target.closest('button[data-apply-warning-path]'); if(applyBtn){ applyCapabilityWarningSuggestion(applyBtn.dataset.applyWarningPath, applyBtn.dataset.applyWarningCode); return; } const btn=e.target.closest('button[data-validation-path]'); if(!btn){ return; } jumpToValidationPath(btn.dataset.validationPath); });healthSummary.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-health-action]'); if(btn){ applyHealthAction(btn.dataset.healthAction); } });draftRouterDefault.addEventListener('input',syncDraftEditorFromForm);[triggerEnabled,triggerIntentEnabled,triggerAnalysisScope,triggerIntentModel,smartEnabled,smartRouterModel,smartFallback,smartCacheTtl,smartMaxTokens,governanceEnabled,governanceAlignmentEnabled,governanceSummarizerModel,governanceSemanticEnabled,governanceClassifierModel,governanceShadowEnabled,governanceVerifierModel].forEach(el=>{ el.addEventListener('input',syncDraftEditorFromForm); el.addEventListener('change',syncDraftEditorFromForm); });surfaceTabs.forEach((tab)=>tab.addEventListener('click',()=>setActiveSurface(tab.dataset.surfaceTarget || 'user')));setActiveSurface('user');function renderMetrics(metrics,health,outcome){ metricsGrid.innerHTML=[ ['Health', health?.status || 'idle'], ['Recent traces', metrics.totalTraces ?? 0], ['Sticky hit rate', pct(metrics.stickyHitRate)], ['Cascade rate', pct(metrics.cascadeTriggeredRate)], ['Shadow rate', pct(metrics.shadowCheckedRate)], ['Alignment rate', pct(metrics.alignmentUsedRate)], ['Model switch rate', pct(outcome?.modelSwitchRate)], ['Alignment on switch', pct(outcome?.alignmentOnSwitchRate)], ['Avg latency', fmt(metrics.averageLatencyMs)+' ms'] ].map(([label,value])=>'<div class="stat"><span class="muted">'+esc(label)+'</span><strong>'+esc(value)+'</strong></div>').join('');}function buildPresetPayload(presetName){ const preset=draftPresets[presetName]; if(!preset){ return null; } const overwriteMode=draftPresetMode.value === 'replace'; const payload=buildDraftPayloadFromForm(); if(overwriteMode){ delete payload.TriggerRouter; delete payload.SmartRouter; delete payload.Governance; } if(preset.routerDefault){ payload.Router={ ...(payload.Router || {}), default: resolvePresetModelId(preset.routerDefault) }; } if(preset.triggerEnabled !== undefined || preset.triggerRules){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.triggerEnabled !== undefined ? Boolean(preset.triggerEnabled) : Boolean(payload.SmartRouter?.enabled), analysis_scope: payload.SmartRouter?.analysis_scope || 'last_message', router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: payload.SmartRouter?.candidates || [], cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: preset.triggerRules ? preset.triggerRules.map(rule=>({ ...rule, model: resolvePresetModelId(rule.model) })) : (payload.SmartRouter?.rules || []) }; delete payload.TriggerRouter; } if(preset.smartEnabled !== undefined || preset.smartCandidates){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: preset.smartEnabled !== undefined ? Boolean(preset.smartEnabled) : Boolean(payload.SmartRouter?.enabled), router_model: payload.SmartRouter?.router_model || '', fallback: payload.SmartRouter?.fallback || 'default', candidates: preset.smartCandidates ? preset.smartCandidates.map(item=>({ ...item, model: resolvePresetModelId(item.model) })) : (payload.SmartRouter?.candidates || []), cache_ttl: payload.SmartRouter?.cache_ttl, max_tokens: payload.SmartRouter?.max_tokens, rules: payload.SmartRouter?.rules || [] }; } if(preset.governanceEnabled !== undefined || preset.governanceAlignmentEnabled !== undefined || preset.governanceSemanticEnabled !== undefined || preset.governanceShadowEnabled !== undefined || preset.governanceSummarizerModel !== undefined || preset.governanceClassifierModel !== undefined || preset.governanceVerifierModel !== undefined){ payload.SmartRouter={ ...(payload.SmartRouter || {}), enabled: payload.SmartRouter?.enabled !== undefined ? Boolean(payload.SmartRouter?.enabled) : Boolean(preset.governanceEnabled), sticky:{ ...((payload.SmartRouter && payload.SmartRouter.sticky) || {}), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.enabled), alignment:{ ...(((payload.SmartRouter && payload.SmartRouter.sticky && payload.SmartRouter.sticky.alignment) || {})), enabled: preset.governanceAlignmentEnabled !== undefined ? Boolean(preset.governanceAlignmentEnabled) : Boolean(payload.SmartRouter?.sticky?.alignment?.enabled), summarizer_model: preset.governanceSummarizerModel !== undefined ? resolvePresetModelId(preset.governanceSummarizerModel) : (payload.SmartRouter?.sticky?.alignment?.summarizer_model || '') } }, semantic:{ ...((payload.SmartRouter && payload.SmartRouter.semantic) || {}), enabled: preset.governanceSemanticEnabled !== undefined ? Boolean(preset.governanceSemanticEnabled) : Boolean(payload.SmartRouter?.semantic?.enabled), mode:(payload.SmartRouter?.semantic?.mode || 'classifier'), classifier_model: preset.governanceClassifierModel !== undefined ? resolvePresetModelId(preset.governanceClassifierModel) : (payload.SmartRouter?.semantic?.classifier_model || '') } }; payload.Governance={ ...(payload.Governance || {}), enabled: preset.governanceEnabled !== undefined ? Boolean(preset.governanceEnabled) : Boolean(payload.Governance?.enabled), shadow:{ ...((payload.Governance && payload.Governance.shadow) || {}), enabled: preset.governanceShadowEnabled !== undefined ? Boolean(preset.governanceShadowEnabled) : Boolean(payload.Governance?.shadow?.enabled), verifier_model: preset.governanceVerifierModel !== undefined ? resolvePresetModelId(preset.governanceVerifierModel) : (payload.Governance?.shadow?.verifier_model || '') } }; } return payload;}function applyDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } currentDraftConfig=payload; renderModelsForm(payload.Models || []); renderConfigControlForms(payload); draftRouterDefault.value=payload.Router?.default || ''; configDraftEditor.value=JSON.stringify(payload,null,2); renderDraftSummary(payload); renderDraftValidation([],[]); renderCapabilityWarnings(); renderDraftPreviewMeta(); draftPreviewStatus.textContent='\u5DF2\u5E94\u7528\u9884\u8BBE\uFF1A'+presetName+'\uFF08'+(draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge')+'\uFF09';}async function previewDraftPreset(presetName){ const payload=buildPresetPayload(presetName); if(!payload){ return; } const preset=draftPresets[presetName]; const modeLabel=draftPresetMode.value === 'replace' ? 'overwrite' : 'append / merge'; renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:[], mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u89C8\u9884\u8BBE\u4E2D\uFF1A'+presetName; const res=await fetch('/api/models/compiled/preview',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ renderDraftValidation(data.errors || [data.message || 'unknown error'], data.warnings || [], data.issueReport); renderCapabilityWarnings(data.capabilityWarnings); renderCompiledDiff(); renderReferenceImpact(data.referenceImpact); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u9884\u89C8\u5931\u8D25\uFF0C\u4EE5\u4E0B\u4E3A\u5F53\u524D\u9884\u89C8\u5C1D\u8BD5\u547D\u4E2D\u7684\u533A\u57DF\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u9884\u8BBE\u9884\u89C8\u5931\u8D25\uFF1A'+((data.errors || []).join('; ') || data.message || 'unknown error'); return; } renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderDraftPreviewMeta({ title:'Preset dry-run', description:(preset?.label || presetName)+' \u4EC5\u9884\u89C8\uFF0C\u4E0D\u4F1A\u5199\u56DE\u5F53\u524D\u8349\u7A3F\u3002', affects:preset?.affects || [], actualAffects:deriveActualAffectedAreas(data), mode:modeLabel }); draftPreviewStatus.textContent='\u5DF2\u9884\u89C8\u9884\u8BBE\uFF1A'+presetName+'\uFF08\u672A\u5199\u56DE\u8349\u7A3F\uFF09';}function renderRanking(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code></span><strong>'+esc(item.count)+' \xB7 '+esc(pct(item.rate))+'</strong></li>').join('');}function renderOutcomeGroups(target,entries,emptyLabel){ if(!entries || !entries.length){ target.innerHTML='<li><span class="muted">'+esc(emptyLabel)+'</span><strong>0</strong></li>'; return; } target.innerHTML=entries.map(item=>'<li><span><code>'+esc(item.key)+'</code><span class="muted"> \xB7 '+esc(item.totalTraces)+' traces</span></span><strong>switch '+esc(pct(item.modelSwitchRate))+' \xB7 align '+esc(pct(item.alignmentOnSwitchRate))+' \xB7 cascade '+esc(pct(item.cascadeAfterSwitchRate))+' \xB7 '+esc(fmt(item.averageLatencyMs))+' ms</strong></li>').join('');}function renderAnomalies(anomalies,health){ const status=health?.status || 'idle'; const message=health?.message || 'No governance traces yet.'; const actions=Array.isArray(health?.actions) ? health.actions : []; healthSummary.className='alert '+esc(status === 'critical' ? 'critical' : (status === 'watch' ? 'warn' : 'info')); healthSummary.innerHTML='<strong>Health: '+esc(status)+'</strong><div>'+esc(message)+'</div>'+ (actions.length ? '<ul class="mini-list">'+actions.map(action=>'<li><button type="button" data-health-action="'+esc(action)+'">'+esc(action)+'</button></li>').join('')+'</ul>' : ''); if(!anomalies || !anomalies.length){ anomalyList.innerHTML='<div class="alert info"><strong>No active alerts</strong><div class="muted">\u5F53\u524D\u7A97\u53E3\u672A\u53D1\u73B0\u660E\u663E\u6CBB\u7406\u5F02\u5E38</div></div>'; return; } anomalyList.innerHTML=anomalies.map(item=>'<div class="alert '+esc(item.severity || 'info')+'"><strong>'+esc(item.type)+'</strong><div>'+esc(item.message)+'</div></div>').join('');}function applyHealthAction(action){ const text=String(action || '').toLowerCase(); const routeReasonInput=document.getElementById('routeReason'); const cascadeSelect=document.getElementById('cascadeTriggered'); const shadowSelect=document.getElementById('shadowChecked'); if(text.includes('cascade')){ cascadeSelect.value='true'; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered cascade traces'; } else if(text.includes('shadow')){ shadowSelect.value='true'; cascadeSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: filtered shadow traces'; } else { cascadeSelect.value=''; shadowSelect.value=''; routeReasonInput.value=''; detailHint.textContent='Health action: showing recent traces'; } loadTraces(); document.getElementById('traceTable').scrollIntoView({ behavior:'smooth', block:'start' });}function renderBuckets(report){ const buckets=report.buckets || []; const windowMs=Number(report.windowMs || 0); bucketHint.textContent=windowMs ? ('\u6700\u8FD1 '+Math.round(windowMs / 60000)+' \u5206\u949F\uFF0C\u5171 '+(report.bucketCount || buckets.length || 0)+' \u6876') : '\u5F53\u524D\u672A\u542F\u7528\u65F6\u95F4\u7A97'; if(!buckets.length){ bucketGrid.innerHTML='<div class="stat"><span class="muted">No bucket data</span><strong>0</strong></div>'; return; } bucketGrid.innerHTML=buckets.map(bucket=> '<div class="stat">'+'<span class="muted">'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</span>'+'<strong>'+esc(bucket.metrics.totalTraces)+'</strong>'+'<div class="muted">sticky '+esc(pct(bucket.metrics.stickyHitRate))+' / cascade '+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</div>'+'</div>').join('');}function renderTrendTable(report){ const buckets=report.buckets || []; if(!buckets.length){ trendTableBody.innerHTML='<tr><td colspan="6" class="muted">No trend data</td></tr>'; return; } trendTableBody.innerHTML=buckets.map(bucket=>'<tr>' + '<td>'+esc(shortTime(bucket.bucketStart))+' - '+esc(shortTime(bucket.bucketEnd))+'</td>' + '<td>'+esc(bucket.metrics.totalTraces)+'</td>' + '<td>'+esc(pct(bucket.metrics.stickyHitRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.cascadeTriggeredRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.shadowCheckedRate))+'</td>' + '<td>'+esc(pct(bucket.metrics.alignmentUsedRate))+'</td>' + '</tr>').join('');}function renderExportHistory(data){ const exports=(data.exports || []); const schedules=(data.schedules || []); exportTableBody.innerHTML=exports.length ? exports.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.kind)+'</td><td>'+esc(item.format)+'</td><td>'+esc(new Date(item.createdAt).toISOString())+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No exports yet</td></tr>'; scheduleTableBody.innerHTML=schedules.length ? schedules.map(item=>'<tr><td><code>'+esc(item.id)+'</code></td><td>'+esc(item.intervalMs)+' ms</td><td>'+esc(item.format)+'</td><td>'+esc(item.lastRunAt ? new Date(item.lastRunAt).toISOString() : '-')}</td></tr>').join('') : '<tr><td colspan="4" class="muted">No schedules yet</td></tr>';}function renderArchives(data){ const archives=(data.archives || []); archiveTableBody.innerHTML=archives.length ? archives.map(item=>'<tr><td><code>'+esc(item.file)+'</code></td><td>'+esc(item.startedAt ? new Date(item.startedAt).toISOString().slice(0,10) : '-')+' ~ '+esc(item.endedAt ? new Date(item.endedAt).toISOString().slice(0,10) : '-')+'</td><td>'+esc(item.traceCount)+'</td><td>'+esc(item.compressed ? 'yes' : 'no')+'</td></tr>').join('') : '<tr><td colspan="4" class="muted">No archives found</td></tr>';}async function loadCompiledModels(){ compiledModelsStatus.textContent='\u52A0\u8F7D compiled models \u4E2D...'; const res=await fetch('/api/models/compiled'); const data=await res.json(); renderDraftValidation([], data.warnings || [], data.issueReport); renderCompiledModels(data); renderCompiledDiff(); renderReferenceImpact();}async function loadTraces(){ const requestId=document.getElementById('requestId').value.trim(); const sessionKey=document.getElementById('sessionKey').value.trim(); const routeReason=document.getElementById('routeReason').value.trim(); const cascadeTriggered=document.getElementById('cascadeTriggered').value; const shadowChecked=document.getElementById('shadowChecked').value; const windowMs=document.getElementById('windowMs').value; const minSampleSize=document.getElementById('minSampleSize').value.trim(); const cascadeWarnRate=document.getElementById('cascadeWarnRate').value.trim(); const shadowWarnRate=document.getElementById('shadowWarnRate').value.trim(); const latencyWarnMs=document.getElementById('latencyWarnMs').value.trim(); const limit=document.getElementById('limit').value.trim(); const params=new URLSearchParams(); if(requestId) params.set('requestId',requestId); if(sessionKey) params.set('sessionKey',sessionKey); if(routeReason) params.set('routeReason',routeReason); if(cascadeTriggered) params.set('cascadeTriggered',cascadeTriggered); if(shadowChecked) params.set('shadowChecked',shadowChecked); if(windowMs) params.set('windowMs',windowMs); if(minSampleSize) params.set('minSampleSize',minSampleSize); if(cascadeWarnRate) params.set('cascadeWarnRate',cascadeWarnRate); if(shadowWarnRate) params.set('shadowWarnRate',shadowWarnRate); if(latencyWarnMs) params.set('latencyWarnMs',latencyWarnMs); params.set('bucketCount','6'); if(limit) params.set('limit',limit); tbody.innerHTML='<tr><td colspan="6" class="muted">\u52A0\u8F7D\u4E2D...</td></tr>'; const query=params.toString()?('?'+params.toString()):''; const [traceRes,metricsRes,healthRes]=await Promise.all([ fetch('/api/governance/traces'+query), fetch('/api/governance/metrics'+query), fetch('/api/governance/health'+query) ]); const data=await traceRes.json(); const metricsData=await metricsRes.json(); const healthData=await healthRes.json(); const health=healthData.health || metricsData.health; renderMetrics(metricsData.metrics || {},health,metricsData.outcome || {}); renderBuckets(metricsData || {}); renderAnomalies(metricsData.anomalies || [],health); renderRanking(routeRanking,metricsData.topRouteReasons || [],'No routes'); renderRanking(modelRanking,metricsData.topFinalModels || [],'No models'); renderRanking(intentRanking,metricsData.topSemanticIntents || [],'No intents'); renderOutcomeGroups(routeOutcomeRanking,metricsData.outcome?.byRouteReason || [],'No route outcomes'); renderOutcomeGroups(modelOutcomeRanking,metricsData.outcome?.byFinalModel || [],'No model outcomes'); renderOutcomeGroups(intentOutcomeRanking,metricsData.outcome?.bySemanticIntent || [],'No intent outcomes'); renderTrendTable(metricsData || {}); const traces=data.traces || []; if(!traces.length){ tbody.innerHTML='<tr><td colspan="6" class="muted">\u6682\u65E0 trace</td></tr>'; return; } tbody.innerHTML=traces.map(t=> \`<tr>\`+ \`<td><code>\${esc(t.requestId)}</code></td>\`+ \`<td>\${t.sessionKey ? \`<span class="pill">\${esc(t.sessionKey)}</span>\` : '<span class="muted">-</span>'}</td>\`+ \`<td><code>\${esc(t.finalModel || '')}</code></td>\`+ \`<td>\${(t.routeReason || []).map(r=>\`<span class="pill">\${esc(r)}</span>\`).join(' ')}</td>\`+ \`<td>\${esc(t.latencyMs ?? '')}</td>\`+ \`<td><button data-request="\${esc(t.requestId)}">View</button></td>\`+ \`</tr>\` ).join('');}async function loadDetail(requestId){ const res=await fetch('/api/governance/traces/'+encodeURIComponent(requestId)); const data=await res.json(); detailHint.textContent='\u5F53\u524D\u67E5\u770B\uFF1A'+requestId; detail.textContent=JSON.stringify(data,null,2);}async function loadExports(){ const res=await fetch('/api/governance/metrics/exports'); renderExportHistory(await res.json());}async function createSnapshot(){ snapshotStatus.textContent='\u521B\u5EFA\u5FEB\u7167\u4E2D...'; const res=await fetch('/api/governance/metrics/snapshots',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify({ format: document.getElementById('snapshotFormat').value, windowMs: Number(document.getElementById('windowMs').value || 0) || undefined }) }); const data=await res.json(); snapshotStatus.textContent=res.ok ? ('\u5DF2\u521B\u5EFA\uFF1A'+data.export.id) : ('\u521B\u5EFA\u5931\u8D25\uFF1A'+(data.message || 'unknown error')); if(res.ok) await loadExports();}async function loadArchives(){ archiveStatus.textContent='\u52A0\u8F7D\u5F52\u6863\u4E2D...'; const params=new URLSearchParams(); const archiveDate=document.getElementById('archiveDate').value.trim(); const archivePage=document.getElementById('archivePage').value.trim(); const archivePageSize=document.getElementById('archivePageSize').value.trim(); if(archiveDate) params.set('date',archiveDate); if(archivePage) params.set('page',archivePage); if(archivePageSize) params.set('pageSize',archivePageSize); const res=await fetch('/api/governance/archives'+(params.toString()?('?'+params.toString()):'')); const data=await res.json(); renderArchives(data); archiveStatus.textContent='\u5F52\u6863\u52A0\u8F7D\u5B8C\u6210';}async function saveThresholds(){ const payload={ min_sample_size:Number(document.getElementById('minSampleSize').value || 0), cascade_warn_rate:Number(document.getElementById('cascadeWarnRate').value || 0), shadow_warn_rate:Number(document.getElementById('shadowWarnRate').value || 0), latency_warn_ms:Number(document.getElementById('latencyWarnMs').value || 0) }; saveThresholdsStatus.textContent='\u4FDD\u5B58\u4E2D...'; const res=await fetch('/api/governance/observability/anomaly-thresholds',{ method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(payload) }); const data=await res.json(); if(!res.ok){ saveThresholdsStatus.textContent='\u4FDD\u5B58\u5931\u8D25\uFF1A'+(data.message || 'unknown error'); return; } saveThresholdsStatus.textContent='\u5DF2\u4FDD\u5B58\u5230\u914D\u7F6E\u6587\u4EF6';}document.getElementById('refreshBtn').addEventListener('click',loadTraces);document.getElementById('loadConfigDraftHeroBtn').addEventListener('click',loadConfigDraft);document.getElementById('previewConfigDraftHeroBtn').addEventListener('click',previewConfigDraft);document.getElementById('refreshStatusHeroBtn').addEventListener('click',loadServiceStatus);document.getElementById('loadConfigDraftBtn').addEventListener('click',loadConfigDraft);document.getElementById('addModelDraftBtn').addEventListener('click',addDraftModel);document.getElementById('applyBalancedPresetBtn').addEventListener('click',()=>applyDraftPreset('balanced'));document.getElementById('previewBalancedPresetBtn').addEventListener('click',()=>previewDraftPreset('balanced'));document.getElementById('applyFastPresetBtn').addEventListener('click',()=>applyDraftPreset('fast'));document.getElementById('previewFastPresetBtn').addEventListener('click',()=>previewDraftPreset('fast'));document.getElementById('applyGovernancePresetBtn').addEventListener('click',()=>applyDraftPreset('governance'));document.getElementById('previewGovernancePresetBtn').addEventListener('click',()=>previewDraftPreset('governance'));document.getElementById('addTriggerRuleBtn').addEventListener('click',addTriggerRule);document.getElementById('addSmartCandidateBtn').addEventListener('click',addSmartCandidate);document.getElementById('addCascadeLevelBtn').addEventListener('click',addCascadeLevel);document.getElementById('syncDraftJsonBtn').addEventListener('click',syncDraftEditorFromForm);document.getElementById('previewConfigDraftBtn').addEventListener('click',previewConfigDraft);document.getElementById('saveConfigDraftBtn').addEventListener('click',saveConfigDraft);draftPresetMode.addEventListener('change',renderDraftPresetModeHint);document.getElementById('createSnapshotBtn').addEventListener('click',createSnapshot);document.getElementById('loadArchivesBtn').addEventListener('click',loadArchives);document.getElementById('saveThresholdsBtn').addEventListener('click',saveThresholds);tbody.addEventListener('click',(e)=>{ const btn=e.target.closest('button[data-request]'); if(btn){ loadDetail(btn.dataset.request); } });renderDraftPresetGuide();renderDraftPresetModeHint();renderDraftPreviewMeta();loadServiceStatus();loadConfigDraft();loadCompiledModels();loadExports();loadArchives();loadTraces();</script></body></html>`;
|
|
3656
4474
|
}
|
|
3657
4475
|
var init_workbench = __esm({
|
|
3658
4476
|
"src/ui/workbench.ts"() {
|
|
3659
4477
|
"use strict";
|
|
3660
4478
|
init_provider_presets();
|
|
4479
|
+
init_runtime_role_guidance();
|
|
3661
4480
|
}
|
|
3662
4481
|
});
|
|
3663
4482
|
|
|
@@ -3672,7 +4491,8 @@ function toCompiledRegistryView(config) {
|
|
|
3672
4491
|
transformer: provider.transformer,
|
|
3673
4492
|
has_api_key: Boolean(provider.api_key)
|
|
3674
4493
|
})),
|
|
3675
|
-
modelMap: registry.modelMap
|
|
4494
|
+
modelMap: registry.modelMap,
|
|
4495
|
+
modelPools: registry.modelPools
|
|
3676
4496
|
};
|
|
3677
4497
|
}
|
|
3678
4498
|
function collectModelReferences(config) {
|
|
@@ -3712,23 +4532,120 @@ function buildServiceInfo(rawConfig) {
|
|
|
3712
4532
|
const remoteService = runtime.remote_service ?? {};
|
|
3713
4533
|
const registration = normalized.Registration ?? {};
|
|
3714
4534
|
const runtimeMode = runtime.mode ?? "local";
|
|
4535
|
+
const managedKeys = listManagedApiKeys(normalized);
|
|
4536
|
+
const authSummary = managedApiKeySummary(normalized);
|
|
4537
|
+
const host = rawConfig?.HOST ?? normalized.HOST;
|
|
4538
|
+
const port = rawConfig?.PORT ?? normalized.PORT;
|
|
4539
|
+
const listenerHost = String(host ?? "").trim() || "127.0.0.1";
|
|
4540
|
+
const publicHost = ["0.0.0.0", "::", "[::]"].includes(String(host ?? "").trim());
|
|
4541
|
+
const hasBootstrapAuth = Boolean(normalized.APIKEY);
|
|
4542
|
+
const hasManagedAuthRecords = authSummary.total > 0;
|
|
4543
|
+
const hasActiveManagedAuth = authSummary.active > 0;
|
|
4544
|
+
const authRequired = hasBootstrapAuth || hasManagedAuthRecords;
|
|
4545
|
+
const listenerBaseUrl = publicHost ? `http://<server-host>:${port}` : `http://${listenerHost}:${port}`;
|
|
4546
|
+
const localBaseUrl = `http://127.0.0.1:${port}`;
|
|
4547
|
+
const securityIssues = [];
|
|
4548
|
+
if (!authRequired && (publicHost || runtimeMode !== "local")) {
|
|
4549
|
+
securityIssues.push({
|
|
4550
|
+
code: "server_without_auth",
|
|
4551
|
+
severity: "critical",
|
|
4552
|
+
message: "Server/cloud or public listener is running without API key authentication.",
|
|
4553
|
+
action: "Set APIKEY or create an active managed admin/client key before exposing this service."
|
|
4554
|
+
});
|
|
4555
|
+
}
|
|
4556
|
+
if (authRequired && hasBootstrapAuth && authSummary.total === 0 && runtimeMode !== "local") {
|
|
4557
|
+
securityIssues.push({
|
|
4558
|
+
code: "bootstrap_only_auth",
|
|
4559
|
+
severity: "warning",
|
|
4560
|
+
message: "Only the bootstrap APIKEY is configured for a server/cloud role.",
|
|
4561
|
+
action: "Create managed client keys for remote users and keep APIKEY for administration."
|
|
4562
|
+
});
|
|
4563
|
+
}
|
|
4564
|
+
if (!hasBootstrapAuth && hasManagedAuthRecords && !hasActiveManagedAuth) {
|
|
4565
|
+
securityIssues.push({
|
|
4566
|
+
code: "managed_auth_without_active_key",
|
|
4567
|
+
severity: "warning",
|
|
4568
|
+
message: "Managed API key records exist, but none are active.",
|
|
4569
|
+
action: "Create an active managed admin/client key or configure APIKEY before relying on this service."
|
|
4570
|
+
});
|
|
4571
|
+
}
|
|
4572
|
+
const quotaSummary = authQuotaUsageStore.summary();
|
|
4573
|
+
const quotaKeys = managedKeys.map((key) => {
|
|
4574
|
+
const usage = authQuotaUsageStore.snapshotForKey(key.id, key.quota);
|
|
4575
|
+
const requestLimit = usage?.requestLimit;
|
|
4576
|
+
const tokenLimit = usage?.tokenLimit;
|
|
4577
|
+
const requestRatio = requestLimit ? usage.requestsUsed / requestLimit : 0;
|
|
4578
|
+
const tokenRatio = tokenLimit ? usage.tokensUsed / tokenLimit : 0;
|
|
4579
|
+
const exhausted = requestLimit !== void 0 && usage.requestsUsed >= requestLimit || tokenLimit !== void 0 && usage.tokensUsed >= tokenLimit;
|
|
4580
|
+
const nearLimit = !exhausted && (requestRatio >= 0.8 || tokenRatio >= 0.8);
|
|
4581
|
+
return {
|
|
4582
|
+
id: key.id,
|
|
4583
|
+
label: key.label,
|
|
4584
|
+
scopes: key.scopes,
|
|
4585
|
+
active: key.active,
|
|
4586
|
+
quota: key.quota,
|
|
4587
|
+
usage,
|
|
4588
|
+
status: !usage ? "unlimited" : !key.active ? "inactive" : exhausted ? "exhausted" : nearLimit ? "watch" : "ok"
|
|
4589
|
+
};
|
|
4590
|
+
});
|
|
3715
4591
|
return {
|
|
3716
4592
|
service: SERVICE_NAME,
|
|
3717
4593
|
ready: true,
|
|
3718
|
-
host
|
|
3719
|
-
port
|
|
4594
|
+
host,
|
|
4595
|
+
port,
|
|
3720
4596
|
runtimeMode,
|
|
3721
4597
|
serviceRole: runtimeMode === "local" ? "local_agent" : "router_service",
|
|
4598
|
+
listener: {
|
|
4599
|
+
host: listenerHost,
|
|
4600
|
+
port,
|
|
4601
|
+
public: publicHost,
|
|
4602
|
+
localUrl: localBaseUrl,
|
|
4603
|
+
advertisedUrl: listenerBaseUrl
|
|
4604
|
+
},
|
|
3722
4605
|
remoteEnabled: Boolean(remoteService.enabled),
|
|
3723
4606
|
remoteService: {
|
|
3724
4607
|
enabled: Boolean(remoteService.enabled),
|
|
3725
4608
|
baseUrl: remoteService.base_url || "",
|
|
3726
4609
|
authTokenConfigured: Boolean(remoteService.auth_token)
|
|
3727
4610
|
},
|
|
4611
|
+
clientConnection: runtimeMode === "local" && remoteService.enabled ? {
|
|
4612
|
+
role: "remote_client",
|
|
4613
|
+
baseUrl: remoteService.base_url || "",
|
|
4614
|
+
authTokenConfigured: Boolean(remoteService.auth_token),
|
|
4615
|
+
recommendedScopes: ["client", "read-only"],
|
|
4616
|
+
guidance: "Use Runtime.remote_service.base_url and a managed client + read-only key from the server maintainer."
|
|
4617
|
+
} : runtimeMode === "local" ? {
|
|
4618
|
+
role: "local_user",
|
|
4619
|
+
baseUrl: localBaseUrl,
|
|
4620
|
+
authTokenConfigured: authRequired,
|
|
4621
|
+
recommendedScopes: [],
|
|
4622
|
+
guidance: "Local Claude Code can use the local router URL; authentication is optional unless configured."
|
|
4623
|
+
} : {
|
|
4624
|
+
role: "remote_user",
|
|
4625
|
+
baseUrl: listenerBaseUrl,
|
|
4626
|
+
authTokenConfigured: authRequired,
|
|
4627
|
+
recommendedScopes: ["client", "read-only"],
|
|
4628
|
+
guidance: "Remote clients should set ANTHROPIC_BASE_URL to this service and use a managed client + read-only key."
|
|
4629
|
+
},
|
|
3728
4630
|
registration: {
|
|
3729
4631
|
enabled: Boolean(registration.enabled),
|
|
3730
4632
|
models: Array.isArray(registration.models) ? registration.models.length : 0,
|
|
3731
4633
|
upstreamServices: Array.isArray(registration.upstream_services) ? registration.upstream_services.length : 0
|
|
4634
|
+
},
|
|
4635
|
+
auth: {
|
|
4636
|
+
required: authRequired,
|
|
4637
|
+
bootstrapConfigured: Boolean(normalized.APIKEY),
|
|
4638
|
+
managedKeys: authSummary,
|
|
4639
|
+
audit: authAuditStore.summary(),
|
|
4640
|
+
quota: {
|
|
4641
|
+
...quotaSummary,
|
|
4642
|
+
keys: quotaKeys
|
|
4643
|
+
}
|
|
4644
|
+
},
|
|
4645
|
+
security: {
|
|
4646
|
+
status: securityIssues.some((issue) => issue.severity === "critical") ? "critical" : securityIssues.length > 0 ? "warning" : "ok",
|
|
4647
|
+
publicHost,
|
|
4648
|
+
issues: securityIssues
|
|
3732
4649
|
}
|
|
3733
4650
|
};
|
|
3734
4651
|
}
|
|
@@ -3765,9 +4682,13 @@ function summarizeCompiledModels(normalized) {
|
|
|
3765
4682
|
const compiled = toCompiledRegistryView(normalized);
|
|
3766
4683
|
const capabilityWarnings = collectCapabilityWarnings(normalized);
|
|
3767
4684
|
const modelEntries = Object.values(compiled.modelMap ?? {});
|
|
4685
|
+
const modelPoolEntries = Object.values(compiled.modelPools ?? {});
|
|
4686
|
+
const modelPoolEndpoints = modelPoolEntries.flatMap((pool) => pool.endpoints ?? []);
|
|
3768
4687
|
return {
|
|
3769
4688
|
providerCount: compiled.providers.length,
|
|
3770
4689
|
modelCount: modelEntries.length,
|
|
4690
|
+
modelPoolCount: modelPoolEntries.length,
|
|
4691
|
+
modelPoolEndpointCount: modelPoolEndpoints.length,
|
|
3771
4692
|
capabilities: {
|
|
3772
4693
|
reasoning: modelEntries.filter((item) => item.capabilities?.thinking?.supported !== false).length,
|
|
3773
4694
|
tools: modelEntries.filter((item) => item.capabilities?.tools !== false).length,
|
|
@@ -3909,6 +4830,10 @@ function buildPersistedConfig(rawConfig, normalizedConfig) {
|
|
|
3909
4830
|
if (registrationProjection && typeof registrationProjection === "object" && Object.keys(registrationProjection).length > 0) {
|
|
3910
4831
|
persisted.Registration = registrationProjection;
|
|
3911
4832
|
}
|
|
4833
|
+
const authProjection = projectConfiguredBranch(rawConfig?.Auth, normalizedConfig.Auth);
|
|
4834
|
+
if (authProjection && typeof authProjection === "object" && Object.keys(authProjection).length > 0) {
|
|
4835
|
+
persisted.Auth = authProjection;
|
|
4836
|
+
}
|
|
3912
4837
|
if (rawConfig?.TriggerRouter) {
|
|
3913
4838
|
smartRouterProjection = mergeSmartRouterProjection(smartRouterProjection, {
|
|
3914
4839
|
...rawConfig.TriggerRouter.enabled !== void 0 ? { enabled: runtimeSmartRouter.enabled } : {},
|
|
@@ -3948,6 +4873,51 @@ function buildPersistedConfig(rawConfig, normalizedConfig) {
|
|
|
3948
4873
|
}
|
|
3949
4874
|
return persisted;
|
|
3950
4875
|
}
|
|
4876
|
+
function denyAuth(reply, statusCode, reason) {
|
|
4877
|
+
reply.code(statusCode);
|
|
4878
|
+
return {
|
|
4879
|
+
success: false,
|
|
4880
|
+
message: statusCode === 403 ? "Forbidden" : "Unauthorized",
|
|
4881
|
+
reason
|
|
4882
|
+
};
|
|
4883
|
+
}
|
|
4884
|
+
function requireAdminAuth(req, reply, authConfig) {
|
|
4885
|
+
const verification = verifyApiKey(
|
|
4886
|
+
authConfig ?? {},
|
|
4887
|
+
extractApiKeyFromHeaders(req?.headers ?? {}),
|
|
4888
|
+
"admin"
|
|
4889
|
+
);
|
|
4890
|
+
const auditBase = {
|
|
4891
|
+
required: "admin",
|
|
4892
|
+
method: req?.method,
|
|
4893
|
+
path: req?.url,
|
|
4894
|
+
requestId: req?.id
|
|
4895
|
+
};
|
|
4896
|
+
if (verification.ok) {
|
|
4897
|
+
authAuditStore.add({
|
|
4898
|
+
...auditBase,
|
|
4899
|
+
outcome: "allowed",
|
|
4900
|
+
source: verification.source,
|
|
4901
|
+
keyId: verification.keyId,
|
|
4902
|
+
scopes: verification.scopes,
|
|
4903
|
+
statusCode: 200
|
|
4904
|
+
});
|
|
4905
|
+
return null;
|
|
4906
|
+
}
|
|
4907
|
+
authAuditStore.add({
|
|
4908
|
+
...auditBase,
|
|
4909
|
+
outcome: "denied",
|
|
4910
|
+
source: verification.source,
|
|
4911
|
+
keyId: verification.keyId,
|
|
4912
|
+
reason: verification.reason ?? "invalid",
|
|
4913
|
+
statusCode: verification.reason === "insufficient_scope" ? 403 : 401
|
|
4914
|
+
});
|
|
4915
|
+
return denyAuth(
|
|
4916
|
+
reply,
|
|
4917
|
+
verification.reason === "insufficient_scope" ? 403 : 401,
|
|
4918
|
+
verification.reason ?? "invalid"
|
|
4919
|
+
);
|
|
4920
|
+
}
|
|
3951
4921
|
function buildDraftConfigView(config) {
|
|
3952
4922
|
const normalizedConfig = normalizeAndValidateConfig(config ?? {}).config;
|
|
3953
4923
|
const runtimeSmartRouterConfig = deriveRuntimeSmartRouterConfig(normalizedConfig);
|
|
@@ -4052,6 +5022,7 @@ var init_server = __esm({
|
|
|
4052
5022
|
init_schema();
|
|
4053
5023
|
init_validation_contract();
|
|
4054
5024
|
init_workbench();
|
|
5025
|
+
init_api_keys();
|
|
4055
5026
|
createServer = (config) => {
|
|
4056
5027
|
const server = new import_llms.default(config);
|
|
4057
5028
|
const configuredThresholds = config.initialConfig?.Governance?.observability?.anomaly_thresholds ?? {};
|
|
@@ -4137,6 +5108,7 @@ var init_server = __esm({
|
|
|
4137
5108
|
success: true,
|
|
4138
5109
|
providers: previewCompiled.providers,
|
|
4139
5110
|
modelMap: previewCompiled.modelMap,
|
|
5111
|
+
modelPools: previewCompiled.modelPools,
|
|
4140
5112
|
normalizedConfig: buildDraftConfigView(result.config),
|
|
4141
5113
|
diff: diffCompiledRegistry(currentCompiled, previewCompiled),
|
|
4142
5114
|
referenceImpact: analyzeModelReferenceImpact(result.config, previewCompiled),
|
|
@@ -4156,11 +5128,149 @@ var init_server = __esm({
|
|
|
4156
5128
|
};
|
|
4157
5129
|
});
|
|
4158
5130
|
server.app.get("/api/service-info", async () => {
|
|
4159
|
-
|
|
5131
|
+
let currentConfig;
|
|
5132
|
+
try {
|
|
5133
|
+
currentConfig = await readConfigFile();
|
|
5134
|
+
} catch {
|
|
5135
|
+
currentConfig = void 0;
|
|
5136
|
+
}
|
|
5137
|
+
const serviceInfoConfig = currentConfig && Object.keys(currentConfig).length > 0 ? { ...config.initialConfig ?? {}, ...currentConfig } : config.initialConfig ?? {};
|
|
5138
|
+
return buildServiceInfo(serviceInfoConfig);
|
|
4160
5139
|
});
|
|
4161
5140
|
server.app.get("/api/registration", async () => {
|
|
4162
5141
|
return buildRegistrationInfo(config.initialConfig ?? {});
|
|
4163
5142
|
});
|
|
5143
|
+
server.app.get("/api/auth/keys", async (req, reply) => {
|
|
5144
|
+
const currentConfig = await readConfigFile();
|
|
5145
|
+
const denied = requireAdminAuth(req, reply, currentConfig);
|
|
5146
|
+
if (denied) {
|
|
5147
|
+
return denied;
|
|
5148
|
+
}
|
|
5149
|
+
const normalized = normalizeAndValidateConfig(currentConfig ?? {}).config;
|
|
5150
|
+
return {
|
|
5151
|
+
keys: listManagedApiKeys(normalized),
|
|
5152
|
+
summary: managedApiKeySummary(normalized)
|
|
5153
|
+
};
|
|
5154
|
+
});
|
|
5155
|
+
server.app.get("/api/auth/audit", async (req, reply) => {
|
|
5156
|
+
const currentConfig = await readConfigFile();
|
|
5157
|
+
const denied = requireAdminAuth(req, reply, currentConfig);
|
|
5158
|
+
if (denied) {
|
|
5159
|
+
return denied;
|
|
5160
|
+
}
|
|
5161
|
+
const limit = Number(req.query?.limit ?? 50);
|
|
5162
|
+
return {
|
|
5163
|
+
events: authAuditStore.list(Number.isFinite(limit) ? limit : 50),
|
|
5164
|
+
summary: authAuditStore.summary()
|
|
5165
|
+
};
|
|
5166
|
+
});
|
|
5167
|
+
server.app.post("/api/auth/keys", async (req, reply) => {
|
|
5168
|
+
const currentConfig = await readConfigFile();
|
|
5169
|
+
const denied = requireAdminAuth(req, reply, currentConfig);
|
|
5170
|
+
if (denied) {
|
|
5171
|
+
return denied;
|
|
5172
|
+
}
|
|
5173
|
+
const scopeErrors = validateManagedApiKeyScopes(req.body?.scopes);
|
|
5174
|
+
const quotaErrors = validateManagedApiKeyQuota(req.body?.quota);
|
|
5175
|
+
const inputErrors = [...scopeErrors, ...quotaErrors];
|
|
5176
|
+
if (inputErrors.length > 0) {
|
|
5177
|
+
reply.code(400);
|
|
5178
|
+
return {
|
|
5179
|
+
success: false,
|
|
5180
|
+
message: "Invalid managed API key input",
|
|
5181
|
+
errors: inputErrors
|
|
5182
|
+
};
|
|
5183
|
+
}
|
|
5184
|
+
if (req.body?.expiresAt !== void 0 && Number.isNaN(Date.parse(String(req.body.expiresAt)))) {
|
|
5185
|
+
reply.code(400);
|
|
5186
|
+
return {
|
|
5187
|
+
success: false,
|
|
5188
|
+
message: "expiresAt must be an ISO date string when provided"
|
|
5189
|
+
};
|
|
5190
|
+
}
|
|
5191
|
+
const created = createManagedApiKey({
|
|
5192
|
+
label: req.body?.label,
|
|
5193
|
+
scopes: req.body?.scopes,
|
|
5194
|
+
expiresAt: req.body?.expiresAt,
|
|
5195
|
+
quota: req.body?.quota
|
|
5196
|
+
});
|
|
5197
|
+
const nextConfig = {
|
|
5198
|
+
...currentConfig ?? {},
|
|
5199
|
+
Auth: {
|
|
5200
|
+
...currentConfig?.Auth ?? {},
|
|
5201
|
+
managed_keys: [
|
|
5202
|
+
...currentConfig?.Auth?.managed_keys ?? [],
|
|
5203
|
+
created.record
|
|
5204
|
+
]
|
|
5205
|
+
}
|
|
5206
|
+
};
|
|
5207
|
+
const result = normalizeAndValidateConfig(nextConfig);
|
|
5208
|
+
if (result.errors.length > 0) {
|
|
5209
|
+
reply.code(400);
|
|
5210
|
+
return {
|
|
5211
|
+
success: false,
|
|
5212
|
+
message: "Invalid auth key configuration",
|
|
5213
|
+
errors: result.errors
|
|
5214
|
+
};
|
|
5215
|
+
}
|
|
5216
|
+
const backupPath = await backupConfigFile();
|
|
5217
|
+
if (backupPath) {
|
|
5218
|
+
log(`Backed up existing configuration file to ${backupPath}`);
|
|
5219
|
+
}
|
|
5220
|
+
await writeConfigFile(buildPersistedConfig(nextConfig, result.config));
|
|
5221
|
+
return {
|
|
5222
|
+
success: true,
|
|
5223
|
+
key: sanitizeManagedApiKey(created.record),
|
|
5224
|
+
secret: created.secret,
|
|
5225
|
+
message: "Managed API key created. Store the secret now; it will not be shown again."
|
|
5226
|
+
};
|
|
5227
|
+
});
|
|
5228
|
+
server.app.post("/api/auth/keys/:id/revoke", async (req, reply) => {
|
|
5229
|
+
const currentConfig = await readConfigFile();
|
|
5230
|
+
const denied = requireAdminAuth(req, reply, currentConfig);
|
|
5231
|
+
if (denied) {
|
|
5232
|
+
return denied;
|
|
5233
|
+
}
|
|
5234
|
+
const keyId = String(req.params?.id ?? "").trim();
|
|
5235
|
+
const managedKeys = currentConfig?.Auth?.managed_keys ?? [];
|
|
5236
|
+
const keyIndex = managedKeys.findIndex((key) => key.id === keyId);
|
|
5237
|
+
if (keyIndex < 0) {
|
|
5238
|
+
reply.code(404);
|
|
5239
|
+
return {
|
|
5240
|
+
success: false,
|
|
5241
|
+
message: "Managed API key not found"
|
|
5242
|
+
};
|
|
5243
|
+
}
|
|
5244
|
+
const revokedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5245
|
+
const nextKeys = managedKeys.map(
|
|
5246
|
+
(key, index) => index === keyIndex ? { ...key, revoked_at: key.revoked_at ?? revokedAt } : key
|
|
5247
|
+
);
|
|
5248
|
+
const nextConfig = {
|
|
5249
|
+
...currentConfig ?? {},
|
|
5250
|
+
Auth: {
|
|
5251
|
+
...currentConfig?.Auth ?? {},
|
|
5252
|
+
managed_keys: nextKeys
|
|
5253
|
+
}
|
|
5254
|
+
};
|
|
5255
|
+
const result = normalizeAndValidateConfig(nextConfig);
|
|
5256
|
+
if (result.errors.length > 0) {
|
|
5257
|
+
reply.code(400);
|
|
5258
|
+
return {
|
|
5259
|
+
success: false,
|
|
5260
|
+
message: "Invalid auth key configuration",
|
|
5261
|
+
errors: result.errors
|
|
5262
|
+
};
|
|
5263
|
+
}
|
|
5264
|
+
const backupPath = await backupConfigFile();
|
|
5265
|
+
if (backupPath) {
|
|
5266
|
+
log(`Backed up existing configuration file to ${backupPath}`);
|
|
5267
|
+
}
|
|
5268
|
+
await writeConfigFile(buildPersistedConfig(nextConfig, result.config));
|
|
5269
|
+
return {
|
|
5270
|
+
success: true,
|
|
5271
|
+
key: sanitizeManagedApiKey(nextKeys[keyIndex])
|
|
5272
|
+
};
|
|
5273
|
+
});
|
|
4164
5274
|
server.app.get("/api/remote-status", async (req) => {
|
|
4165
5275
|
const normalizedResult = normalizeAndValidateConfig(config.initialConfig ?? {});
|
|
4166
5276
|
const normalized = normalizedResult.config;
|
|
@@ -4206,9 +5316,11 @@ var init_server = __esm({
|
|
|
4206
5316
|
metrics: report.metrics,
|
|
4207
5317
|
anomalies: report.anomalies,
|
|
4208
5318
|
topRouteReasons: report.topRouteReasons,
|
|
4209
|
-
topFinalModels: report.topFinalModels
|
|
5319
|
+
topFinalModels: report.topFinalModels,
|
|
5320
|
+
outcome: report.outcome
|
|
4210
5321
|
}),
|
|
4211
5322
|
metrics: report.metrics,
|
|
5323
|
+
outcome: report.outcome,
|
|
4212
5324
|
anomalies: report.anomalies,
|
|
4213
5325
|
topRouteReasons: report.topRouteReasons,
|
|
4214
5326
|
topFinalModels: report.topFinalModels,
|
|
@@ -4398,8 +5510,8 @@ var init_server = __esm({
|
|
|
4398
5510
|
reply.send({ success: true, message: "Service restart initiated" });
|
|
4399
5511
|
setTimeout(() => {
|
|
4400
5512
|
const { spawn: spawn3 } = require("child_process");
|
|
4401
|
-
const { join:
|
|
4402
|
-
const cliPath =
|
|
5513
|
+
const { join: join9 } = require("path");
|
|
5514
|
+
const cliPath = join9(__dirname, "cli.js");
|
|
4403
5515
|
const currentPort = config.initialConfig?.PORT;
|
|
4404
5516
|
const restartArgs = [cliPath, "start", "--daemon"];
|
|
4405
5517
|
if (currentPort) {
|
|
@@ -4430,6 +5542,7 @@ var init_router = __esm({
|
|
|
4430
5542
|
init_cache();
|
|
4431
5543
|
init_log();
|
|
4432
5544
|
init_compile();
|
|
5545
|
+
init_governance();
|
|
4433
5546
|
enc = (0, import_tiktoken.get_encoding)("cl100k_base");
|
|
4434
5547
|
calculateTokenCount = (messages, system, tools) => {
|
|
4435
5548
|
let tokenCount = 0;
|
|
@@ -4582,6 +5695,17 @@ var init_router = __esm({
|
|
|
4582
5695
|
}
|
|
4583
5696
|
req.body.model = model ?? req.body.model;
|
|
4584
5697
|
applyModelThinking(req, config, req.body.model);
|
|
5698
|
+
const compiledModel = getCompiledModelRef(config, req.body.model);
|
|
5699
|
+
if (compiledModel?.source === "registration" && compiledModel.modelPool) {
|
|
5700
|
+
req.modelPoolSelection = compiledModel.modelPool;
|
|
5701
|
+
if (req.governanceTrace) {
|
|
5702
|
+
req.governanceTrace.finalModel = req.body.model;
|
|
5703
|
+
appendTraceReason(
|
|
5704
|
+
req.governanceTrace,
|
|
5705
|
+
`model_pool:${compiledModel.modelPool.modelId}:${compiledModel.modelPool.endpointId}`
|
|
5706
|
+
);
|
|
5707
|
+
}
|
|
5708
|
+
}
|
|
4585
5709
|
req.tokenCount = tokenCount;
|
|
4586
5710
|
} catch (error) {
|
|
4587
5711
|
logError("Error in router middleware:", error.message);
|
|
@@ -4593,31 +5717,166 @@ var init_router = __esm({
|
|
|
4593
5717
|
});
|
|
4594
5718
|
|
|
4595
5719
|
// src/middleware/auth.ts
|
|
4596
|
-
function
|
|
5720
|
+
function estimateRequestTokens(body) {
|
|
5721
|
+
if (body === void 0 || body === null) {
|
|
5722
|
+
return 0;
|
|
5723
|
+
}
|
|
5724
|
+
const text = typeof body === "string" ? body : JSON.stringify(body);
|
|
5725
|
+
return Math.ceil(text.length / 4);
|
|
5726
|
+
}
|
|
5727
|
+
function authRequirementForRequest(req) {
|
|
5728
|
+
const method = String(req.method ?? "").toUpperCase();
|
|
5729
|
+
const path = String(req.url ?? "").split("?")[0];
|
|
5730
|
+
const readOnlyPaths = /* @__PURE__ */ new Set([
|
|
5731
|
+
"/api/health",
|
|
5732
|
+
"/api/service-info",
|
|
5733
|
+
"/api/remote-status",
|
|
5734
|
+
"/api/registration",
|
|
5735
|
+
"/api/models/compiled",
|
|
5736
|
+
"/api/transformers",
|
|
5737
|
+
"/api/governance/health",
|
|
5738
|
+
"/api/governance/metrics",
|
|
5739
|
+
"/api/governance/metrics/export",
|
|
5740
|
+
"/api/governance/metrics/exports"
|
|
5741
|
+
]);
|
|
5742
|
+
const modelCallPaths = /* @__PURE__ */ new Set([
|
|
5743
|
+
"/v1/messages",
|
|
5744
|
+
"/v1/chat/completions"
|
|
5745
|
+
]);
|
|
5746
|
+
if (method === "GET" && (readOnlyPaths.has(path) || path === "/api/governance/traces" || path.startsWith("/api/governance/traces/") || path === "/api/governance/archives" || path.startsWith("/api/governance/archives/"))) {
|
|
5747
|
+
return "read-only";
|
|
5748
|
+
}
|
|
5749
|
+
if (modelCallPaths.has(path)) {
|
|
5750
|
+
return "client";
|
|
5751
|
+
}
|
|
5752
|
+
return path.startsWith("/api/") || path === "/ui" ? "admin" : "client";
|
|
5753
|
+
}
|
|
5754
|
+
function isQuotaMeteredRequest(req) {
|
|
5755
|
+
const method = String(req.method ?? "").toUpperCase();
|
|
5756
|
+
const path = String(req.url ?? "").split("?")[0];
|
|
5757
|
+
return method === "POST" && (path === "/v1/messages" || path === "/v1/chat/completions");
|
|
5758
|
+
}
|
|
5759
|
+
function apiKeyAuth(configInput, options = {}) {
|
|
4597
5760
|
return (req, reply, done) => {
|
|
4598
|
-
|
|
5761
|
+
Promise.resolve(typeof configInput === "function" ? configInput() : configInput).then(async (config) => {
|
|
5762
|
+
authQuotaUsageStore.hydrate(config.Auth?.quota_usage);
|
|
5763
|
+
const required = authRequirementForRequest(req);
|
|
5764
|
+
const auditBase = {
|
|
5765
|
+
required,
|
|
5766
|
+
method: req.method,
|
|
5767
|
+
path: req.url,
|
|
5768
|
+
requestId: req.id
|
|
5769
|
+
};
|
|
5770
|
+
if (!config.APIKEY && !config.Auth?.managed_keys?.length) {
|
|
5771
|
+
authAuditStore.add({
|
|
5772
|
+
...auditBase,
|
|
5773
|
+
outcome: "skipped",
|
|
5774
|
+
reason: "no_auth_config"
|
|
5775
|
+
});
|
|
5776
|
+
done();
|
|
5777
|
+
return;
|
|
5778
|
+
}
|
|
5779
|
+
const result = verifyApiKey(config, extractApiKeyFromHeaders(req.headers), required);
|
|
5780
|
+
if (!result.ok) {
|
|
5781
|
+
const statusCode = result.reason === "insufficient_scope" ? 403 : 401;
|
|
5782
|
+
authAuditStore.add({
|
|
5783
|
+
...auditBase,
|
|
5784
|
+
outcome: "denied",
|
|
5785
|
+
source: result.source,
|
|
5786
|
+
keyId: result.keyId,
|
|
5787
|
+
reason: result.reason,
|
|
5788
|
+
statusCode
|
|
5789
|
+
});
|
|
5790
|
+
reply.code(statusCode).send({
|
|
5791
|
+
error: statusCode === 403 ? "Forbidden" : "Unauthorized",
|
|
5792
|
+
reason: result.reason
|
|
5793
|
+
});
|
|
5794
|
+
done(new Error(statusCode === 403 ? "Forbidden" : "Unauthorized"));
|
|
5795
|
+
return;
|
|
5796
|
+
}
|
|
5797
|
+
const quotaResult = isQuotaMeteredRequest(req) ? authQuotaUsageStore.consume(
|
|
5798
|
+
result.keyId,
|
|
5799
|
+
result.quota,
|
|
5800
|
+
estimateRequestTokens(req.body)
|
|
5801
|
+
) : { ok: true };
|
|
5802
|
+
if (!quotaResult.ok) {
|
|
5803
|
+
const retryAfterSeconds = quotaResult.usage.windowResetAt ? Math.max(0, Math.ceil((Date.parse(quotaResult.usage.windowResetAt) - Date.now()) / 1e3)) : void 0;
|
|
5804
|
+
authAuditStore.add({
|
|
5805
|
+
...auditBase,
|
|
5806
|
+
outcome: "denied",
|
|
5807
|
+
source: result.source,
|
|
5808
|
+
keyId: result.keyId,
|
|
5809
|
+
scopes: result.scopes,
|
|
5810
|
+
reason: quotaResult.reason,
|
|
5811
|
+
statusCode: 429,
|
|
5812
|
+
quota: quotaResult.usage
|
|
5813
|
+
});
|
|
5814
|
+
if (retryAfterSeconds !== void 0) {
|
|
5815
|
+
reply.header("Retry-After", String(retryAfterSeconds));
|
|
5816
|
+
}
|
|
5817
|
+
reply.code(429).send({
|
|
5818
|
+
error: "Too Many Requests",
|
|
5819
|
+
reason: quotaResult.reason,
|
|
5820
|
+
quota: quotaResult.usage
|
|
5821
|
+
});
|
|
5822
|
+
done(new Error("Too Many Requests"));
|
|
5823
|
+
return;
|
|
5824
|
+
}
|
|
5825
|
+
authAuditStore.add({
|
|
5826
|
+
...auditBase,
|
|
5827
|
+
outcome: "allowed",
|
|
5828
|
+
source: result.source,
|
|
5829
|
+
keyId: result.keyId,
|
|
5830
|
+
scopes: result.scopes,
|
|
5831
|
+
statusCode: 200,
|
|
5832
|
+
quota: quotaResult.usage
|
|
5833
|
+
});
|
|
5834
|
+
if (quotaResult.usage && options.persistQuotaUsage) {
|
|
5835
|
+
Promise.resolve().then(() => options.persistQuotaUsage?.(authQuotaUsageStore.exportForConfig())).catch(() => void 0);
|
|
5836
|
+
}
|
|
4599
5837
|
done();
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
const xApiKey = req.headers["x-api-key"];
|
|
4604
|
-
let providedKey;
|
|
4605
|
-
if (authHeader?.startsWith("Bearer ")) {
|
|
4606
|
-
providedKey = authHeader.slice(7);
|
|
4607
|
-
} else if (xApiKey) {
|
|
4608
|
-
providedKey = xApiKey;
|
|
4609
|
-
}
|
|
4610
|
-
if (!providedKey || providedKey !== config.APIKEY) {
|
|
4611
|
-
reply.code(401).send({ error: "Unauthorized" });
|
|
4612
|
-
done(new Error("Unauthorized"));
|
|
4613
|
-
return;
|
|
4614
|
-
}
|
|
4615
|
-
done();
|
|
5838
|
+
}).catch((error) => {
|
|
5839
|
+
done(error instanceof Error ? error : new Error(String(error)));
|
|
5840
|
+
});
|
|
4616
5841
|
};
|
|
4617
5842
|
}
|
|
4618
5843
|
var init_auth = __esm({
|
|
4619
5844
|
"src/middleware/auth.ts"() {
|
|
4620
5845
|
"use strict";
|
|
5846
|
+
init_api_keys();
|
|
5847
|
+
}
|
|
5848
|
+
});
|
|
5849
|
+
|
|
5850
|
+
// src/auth/quota-persistence.ts
|
|
5851
|
+
async function loadPersistedAuthQuotaUsage() {
|
|
5852
|
+
if (!(0, import_fs4.existsSync)(QUOTA_USAGE_FILE)) {
|
|
5853
|
+
return void 0;
|
|
5854
|
+
}
|
|
5855
|
+
const content = await (0, import_promises2.readFile)(QUOTA_USAGE_FILE, "utf-8");
|
|
5856
|
+
const parsed = JSON.parse(content);
|
|
5857
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : void 0;
|
|
5858
|
+
}
|
|
5859
|
+
async function savePersistedAuthQuotaUsage(usage) {
|
|
5860
|
+
if (!(0, import_fs4.existsSync)(HOME_DIR)) {
|
|
5861
|
+
(0, import_fs4.mkdirSync)(HOME_DIR, { recursive: true });
|
|
5862
|
+
}
|
|
5863
|
+
const tempFile = `${QUOTA_USAGE_FILE}.tmp`;
|
|
5864
|
+
quotaUsageWriteQueue = quotaUsageWriteQueue.catch(() => void 0).then(async () => {
|
|
5865
|
+
await (0, import_promises2.writeFile)(tempFile, JSON.stringify(usage, null, 2), "utf-8");
|
|
5866
|
+
await (0, import_promises2.rename)(tempFile, QUOTA_USAGE_FILE);
|
|
5867
|
+
});
|
|
5868
|
+
await quotaUsageWriteQueue;
|
|
5869
|
+
}
|
|
5870
|
+
var import_fs4, import_promises2, import_path5, QUOTA_USAGE_FILE, quotaUsageWriteQueue;
|
|
5871
|
+
var init_quota_persistence = __esm({
|
|
5872
|
+
"src/auth/quota-persistence.ts"() {
|
|
5873
|
+
"use strict";
|
|
5874
|
+
import_fs4 = require("fs");
|
|
5875
|
+
import_promises2 = require("fs/promises");
|
|
5876
|
+
import_path5 = require("path");
|
|
5877
|
+
init_constants();
|
|
5878
|
+
QUOTA_USAGE_FILE = (0, import_path5.join)(HOME_DIR, "auth-quota-usage.json");
|
|
5879
|
+
quotaUsageWriteQueue = Promise.resolve();
|
|
4621
5880
|
}
|
|
4622
5881
|
});
|
|
4623
5882
|
|
|
@@ -4676,12 +5935,12 @@ function savePid(pid, port) {
|
|
|
4676
5935
|
port: port ?? DEFAULT_CONFIG2.PORT,
|
|
4677
5936
|
startTime: (/* @__PURE__ */ new Date()).toISOString()
|
|
4678
5937
|
};
|
|
4679
|
-
(0,
|
|
5938
|
+
(0, import_fs5.writeFileSync)(PID_FILE, JSON.stringify(info, null, 2), "utf-8");
|
|
4680
5939
|
}
|
|
4681
5940
|
function readServiceInfo() {
|
|
4682
|
-
if (!(0,
|
|
5941
|
+
if (!(0, import_fs5.existsSync)(PID_FILE)) return null;
|
|
4683
5942
|
try {
|
|
4684
|
-
const content = (0,
|
|
5943
|
+
const content = (0, import_fs5.readFileSync)(PID_FILE, "utf-8").trim();
|
|
4685
5944
|
if (/^\d+$/.test(content)) {
|
|
4686
5945
|
return { pid: parseInt(content, 10), port: DEFAULT_CONFIG2.PORT, startTime: "" };
|
|
4687
5946
|
}
|
|
@@ -4691,19 +5950,19 @@ function readServiceInfo() {
|
|
|
4691
5950
|
}
|
|
4692
5951
|
}
|
|
4693
5952
|
function cleanupPidFile() {
|
|
4694
|
-
if ((0,
|
|
5953
|
+
if ((0, import_fs5.existsSync)(PID_FILE)) {
|
|
4695
5954
|
try {
|
|
4696
|
-
(0,
|
|
5955
|
+
(0, import_fs5.unlinkSync)(PID_FILE);
|
|
4697
5956
|
} catch (error) {
|
|
4698
5957
|
logError("Failed to cleanup PID file:", error);
|
|
4699
5958
|
}
|
|
4700
5959
|
}
|
|
4701
5960
|
}
|
|
4702
|
-
var
|
|
5961
|
+
var import_fs5, import_child_process;
|
|
4703
5962
|
var init_processCheck = __esm({
|
|
4704
5963
|
"src/utils/processCheck.ts"() {
|
|
4705
5964
|
"use strict";
|
|
4706
|
-
|
|
5965
|
+
import_fs5 = require("fs");
|
|
4707
5966
|
import_child_process = require("child_process");
|
|
4708
5967
|
init_constants();
|
|
4709
5968
|
init_log();
|
|
@@ -6541,8 +7800,8 @@ function cloneRequestBody(value) {
|
|
|
6541
7800
|
}
|
|
6542
7801
|
async function initializeClaudeConfig() {
|
|
6543
7802
|
const homeDir = (0, import_os2.homedir)();
|
|
6544
|
-
const configPath = (0,
|
|
6545
|
-
if (!(0,
|
|
7803
|
+
const configPath = (0, import_path6.join)(homeDir, ".claude.json");
|
|
7804
|
+
if (!(0, import_fs6.existsSync)(configPath)) {
|
|
6546
7805
|
log(`Creating ${configPath} for Claude Code compatibility (onboarding bypass)`);
|
|
6547
7806
|
const userID = Array.from(
|
|
6548
7807
|
{ length: 64 },
|
|
@@ -6556,7 +7815,7 @@ async function initializeClaudeConfig() {
|
|
|
6556
7815
|
lastOnboardingVersion: "1.0.17",
|
|
6557
7816
|
projects: {}
|
|
6558
7817
|
};
|
|
6559
|
-
await (0,
|
|
7818
|
+
await (0, import_promises3.writeFile)(configPath, JSON.stringify(configContent, null, 2));
|
|
6560
7819
|
}
|
|
6561
7820
|
}
|
|
6562
7821
|
function buildServerInitialConfig(config, registry, host, servicePort) {
|
|
@@ -6565,7 +7824,7 @@ function buildServerInitialConfig(config, registry, host, servicePort) {
|
|
|
6565
7824
|
providers: registry.providers,
|
|
6566
7825
|
HOST: host,
|
|
6567
7826
|
PORT: servicePort,
|
|
6568
|
-
LOG_FILE: (0,
|
|
7827
|
+
LOG_FILE: (0, import_path6.join)(
|
|
6569
7828
|
(0, import_os2.homedir)(),
|
|
6570
7829
|
".claude-trigger-router",
|
|
6571
7830
|
"claude-trigger-router.log"
|
|
@@ -6579,11 +7838,19 @@ async function run(options = {}) {
|
|
|
6579
7838
|
}
|
|
6580
7839
|
await initDir();
|
|
6581
7840
|
const config = await initConfig();
|
|
7841
|
+
authQuotaUsageStore.hydrate(config.Auth?.quota_usage);
|
|
7842
|
+
try {
|
|
7843
|
+
authQuotaUsageStore.hydrate(await loadPersistedAuthQuotaUsage());
|
|
7844
|
+
} catch (error) {
|
|
7845
|
+
logWarn(`[AuthQuota] Failed to load persisted quota usage: ${error instanceof Error ? error.message : String(error)}`);
|
|
7846
|
+
}
|
|
6582
7847
|
configureLogging(config);
|
|
6583
7848
|
let HOST = config.HOST || "127.0.0.1";
|
|
6584
|
-
|
|
7849
|
+
const managedKeySummary = managedApiKeySummary(config);
|
|
7850
|
+
const hasPublicAuth = Boolean(config.APIKEY || managedKeySummary.active > 0);
|
|
7851
|
+
if (config.HOST && !hasPublicAuth) {
|
|
6585
7852
|
HOST = "127.0.0.1";
|
|
6586
|
-
logWarn("\u26A0\uFE0F API key is not set. HOST is forced to 127.0.0.1.");
|
|
7853
|
+
logWarn("\u26A0\uFE0F API key or active managed key is not set. HOST is forced to 127.0.0.1.");
|
|
6587
7854
|
}
|
|
6588
7855
|
const port = options.port ?? config.PORT ?? DEFAULT_CONFIG.PORT;
|
|
6589
7856
|
savePid(process.pid, port);
|
|
@@ -6626,13 +7893,34 @@ async function run(options = {}) {
|
|
|
6626
7893
|
initialConfig: buildServerInitialConfig(config, registry, HOST, servicePort),
|
|
6627
7894
|
logger: loggerConfig
|
|
6628
7895
|
});
|
|
7896
|
+
const authMiddleware = apiKeyAuth(async () => {
|
|
7897
|
+
try {
|
|
7898
|
+
const currentConfig = await readConfigFile();
|
|
7899
|
+
return {
|
|
7900
|
+
...config,
|
|
7901
|
+
APIKEY: currentConfig.APIKEY,
|
|
7902
|
+
Auth: currentConfig.Auth
|
|
7903
|
+
};
|
|
7904
|
+
} catch (error) {
|
|
7905
|
+
logWarn(`[Auth] Failed to refresh auth config, using startup auth config: ${error instanceof Error ? error.message : String(error)}`);
|
|
7906
|
+
return config;
|
|
7907
|
+
}
|
|
7908
|
+
}, {
|
|
7909
|
+
persistQuotaUsage: async (usage) => {
|
|
7910
|
+
try {
|
|
7911
|
+
await savePersistedAuthQuotaUsage(usage);
|
|
7912
|
+
} catch (error) {
|
|
7913
|
+
logWarn(`[AuthQuota] Failed to persist quota usage: ${error instanceof Error ? error.message : String(error)}`);
|
|
7914
|
+
}
|
|
7915
|
+
}
|
|
7916
|
+
});
|
|
6629
7917
|
server.addHook("preHandler", async (req, reply) => {
|
|
6630
7918
|
return new Promise((resolve, reject) => {
|
|
6631
7919
|
const done = (err) => {
|
|
6632
7920
|
if (err) reject(err);
|
|
6633
7921
|
else resolve();
|
|
6634
7922
|
};
|
|
6635
|
-
|
|
7923
|
+
authMiddleware(req, reply, done);
|
|
6636
7924
|
});
|
|
6637
7925
|
});
|
|
6638
7926
|
triggerRouter.init(config);
|
|
@@ -6911,18 +8199,20 @@ async function run(options = {}) {
|
|
|
6911
8199
|
});
|
|
6912
8200
|
await server.start();
|
|
6913
8201
|
}
|
|
6914
|
-
var
|
|
8202
|
+
var import_fs6, import_promises3, import_os2, import_path6, import_json5, import_node_events, import_rotating_file_stream, event;
|
|
6915
8203
|
var init_index = __esm({
|
|
6916
8204
|
"src/index.ts"() {
|
|
6917
8205
|
"use strict";
|
|
6918
|
-
|
|
6919
|
-
|
|
8206
|
+
import_fs6 = require("fs");
|
|
8207
|
+
import_promises3 = require("fs/promises");
|
|
6920
8208
|
import_os2 = require("os");
|
|
6921
|
-
|
|
8209
|
+
import_path6 = require("path");
|
|
6922
8210
|
init_utils();
|
|
6923
8211
|
init_server();
|
|
6924
8212
|
init_router();
|
|
6925
8213
|
init_auth();
|
|
8214
|
+
init_api_keys();
|
|
8215
|
+
init_quota_persistence();
|
|
6926
8216
|
init_processCheck();
|
|
6927
8217
|
init_constants();
|
|
6928
8218
|
init_log();
|
|
@@ -7400,6 +8690,17 @@ function buildRemoteServiceConfig(input3) {
|
|
|
7400
8690
|
Router: {}
|
|
7401
8691
|
};
|
|
7402
8692
|
}
|
|
8693
|
+
function buildServerDeploymentConfig(input3) {
|
|
8694
|
+
const template = buildUsableMinimalTemplateConfig();
|
|
8695
|
+
return {
|
|
8696
|
+
...template,
|
|
8697
|
+
HOST: "0.0.0.0",
|
|
8698
|
+
APIKEY: input3.apiKey,
|
|
8699
|
+
Runtime: {
|
|
8700
|
+
mode: "server"
|
|
8701
|
+
}
|
|
8702
|
+
};
|
|
8703
|
+
}
|
|
7403
8704
|
function buildUsableMinimalTemplateConfig() {
|
|
7404
8705
|
const openRouterPreset = getProviderPreset("openrouter");
|
|
7405
8706
|
const modelId = openRouterPreset?.suggested_id ?? "sonnet";
|
|
@@ -7590,6 +8891,16 @@ function getMigratedModelCount(draft) {
|
|
|
7590
8891
|
}
|
|
7591
8892
|
return 0;
|
|
7592
8893
|
}
|
|
8894
|
+
function isRouterServiceDeploymentDraft(draft) {
|
|
8895
|
+
return draft?.Runtime?.mode === "server" || draft?.Runtime?.mode === "cloud";
|
|
8896
|
+
}
|
|
8897
|
+
function getRouterServiceDeploymentLabel(draft) {
|
|
8898
|
+
return draft?.Runtime?.mode === "cloud" ? "cloud" : "server";
|
|
8899
|
+
}
|
|
8900
|
+
function printRouterServiceDeploymentNextSteps(io, draft, message = "\u5DF2\u751F\u6210 {mode} \u90E8\u7F72\u914D\u7F6E\uFF1Bsetup \u4E0D\u4F1A\u81EA\u52A8\u542F\u52A8\u8FDC\u7A0B\u670D\u52A1\u3002") {
|
|
8901
|
+
io.info(message.replace("{mode}", getRouterServiceDeploymentLabel(draft)));
|
|
8902
|
+
io.info("\u4E0B\u4E00\u6B65\uFF1A\u7F16\u8F91 Models[].key / Models[].model\uFF0C\u8FD0\u884C ctr doctor\uFF0C\u7136\u540E\u8FD0\u884C ctr start --daemon\u3002");
|
|
8903
|
+
}
|
|
7593
8904
|
async function runSetup(deps) {
|
|
7594
8905
|
const detection = await deps.detectSetupEnvironment();
|
|
7595
8906
|
const currentConfigAction = await deps.chooseCurrentConfigAction({
|
|
@@ -7613,6 +8924,7 @@ async function runSetup(deps) {
|
|
|
7613
8924
|
return;
|
|
7614
8925
|
}
|
|
7615
8926
|
let configChanged = false;
|
|
8927
|
+
let finalDraft;
|
|
7616
8928
|
if (branch.kind === "repair_current") {
|
|
7617
8929
|
if (detection.currentConfig.kind !== "invalid" && detection.currentConfig.kind !== "valid") {
|
|
7618
8930
|
throw new Error("repair_current requires current config");
|
|
@@ -7632,6 +8944,7 @@ async function runSetup(deps) {
|
|
|
7632
8944
|
draft: baseDraft,
|
|
7633
8945
|
fields: repairPlan.fields
|
|
7634
8946
|
});
|
|
8947
|
+
finalDraft = completedDraft;
|
|
7635
8948
|
const persistResult = await deps.persistConfig({
|
|
7636
8949
|
config: completedDraft,
|
|
7637
8950
|
currentConfigPath: detection.currentConfig.path,
|
|
@@ -7640,16 +8953,28 @@ async function runSetup(deps) {
|
|
|
7640
8953
|
configChanged = persistResult.configChanged;
|
|
7641
8954
|
}
|
|
7642
8955
|
if (branch.kind === "reuse_current") {
|
|
7643
|
-
|
|
8956
|
+
if (detection.currentConfig.kind === "valid" && isRouterServiceDeploymentDraft(detection.currentConfig.config)) {
|
|
8957
|
+
printRouterServiceDeploymentNextSteps(
|
|
8958
|
+
deps.io,
|
|
8959
|
+
detection.currentConfig.config,
|
|
8960
|
+
"\u5F53\u524D\u914D\u7F6E\u662F {mode} \u90E8\u7F72\u914D\u7F6E\uFF1Bsetup \u4E0D\u4F1A\u81EA\u52A8\u542F\u52A8\u8FDC\u7A0B\u670D\u52A1\u3002"
|
|
8961
|
+
);
|
|
8962
|
+
return;
|
|
8963
|
+
}
|
|
8964
|
+
const service = await deps.ensureServiceReady({
|
|
7644
8965
|
configChanged: false,
|
|
7645
8966
|
detectedService: detection.detectedService,
|
|
7646
8967
|
reloadSupported: deps.reloadSupported
|
|
7647
8968
|
});
|
|
7648
|
-
await deps.enterClaudeCode(
|
|
8969
|
+
await deps.enterClaudeCode({
|
|
8970
|
+
config: detection.currentConfig.config,
|
|
8971
|
+
service
|
|
8972
|
+
});
|
|
7649
8973
|
return;
|
|
7650
8974
|
}
|
|
7651
8975
|
if (branch.kind === "unparseable_current") {
|
|
7652
8976
|
const draft = await deps.buildFreshConfig();
|
|
8977
|
+
finalDraft = draft;
|
|
7653
8978
|
const persistResult = await deps.persistConfig({
|
|
7654
8979
|
config: draft,
|
|
7655
8980
|
currentConfigPath: detection.currentConfig.path,
|
|
@@ -7659,6 +8984,7 @@ async function runSetup(deps) {
|
|
|
7659
8984
|
}
|
|
7660
8985
|
if (branch.kind === "fresh_init") {
|
|
7661
8986
|
const draft = await deps.buildFreshConfig();
|
|
8987
|
+
finalDraft = draft;
|
|
7662
8988
|
const persistResult = await deps.persistConfig({
|
|
7663
8989
|
config: draft,
|
|
7664
8990
|
currentConfigPath: getTargetConfigPath(detection),
|
|
@@ -7681,27 +9007,38 @@ async function runSetup(deps) {
|
|
|
7681
9007
|
if (migrated.skippedFields.length > 0) {
|
|
7682
9008
|
deps.io.info(`\u4EE5\u4E0B\u65E7\u5B57\u6BB5\u672A\u81EA\u52A8\u8FC1\u79FB\uFF1A${migrated.skippedFields.join(", ")}`);
|
|
7683
9009
|
}
|
|
7684
|
-
let
|
|
9010
|
+
let migratedFinalDraft = migrated.draft;
|
|
7685
9011
|
if (migrated.needsCompletion) {
|
|
7686
|
-
|
|
9012
|
+
migratedFinalDraft = await deps.completeDraft({
|
|
7687
9013
|
draft: migrated.draft,
|
|
7688
9014
|
fields: migrated.missingFields
|
|
7689
9015
|
});
|
|
7690
9016
|
}
|
|
9017
|
+
finalDraft = migratedFinalDraft;
|
|
7691
9018
|
const persistResult = await deps.persistConfig({
|
|
7692
|
-
config:
|
|
9019
|
+
config: migratedFinalDraft,
|
|
7693
9020
|
currentConfigPath: getTargetConfigPath(detection),
|
|
7694
9021
|
hasExistingConfig: detection.currentConfig.kind !== "missing"
|
|
7695
9022
|
});
|
|
7696
9023
|
configChanged = persistResult.configChanged;
|
|
7697
9024
|
}
|
|
7698
9025
|
if (branch.kind === "fresh_init" || branch.kind === "repair_current" || branch.kind === "unparseable_current" || branch.kind === "migrate_legacy") {
|
|
7699
|
-
|
|
9026
|
+
if (isRouterServiceDeploymentDraft(finalDraft)) {
|
|
9027
|
+
printRouterServiceDeploymentNextSteps(deps.io, finalDraft);
|
|
9028
|
+
return;
|
|
9029
|
+
}
|
|
9030
|
+
const service = await deps.ensureServiceReady({
|
|
7700
9031
|
configChanged,
|
|
7701
9032
|
detectedService: detection.detectedService,
|
|
7702
9033
|
reloadSupported: deps.reloadSupported
|
|
7703
9034
|
});
|
|
7704
|
-
|
|
9035
|
+
if (!finalDraft) {
|
|
9036
|
+
throw new Error("setup finished without a final config draft");
|
|
9037
|
+
}
|
|
9038
|
+
await deps.enterClaudeCode({
|
|
9039
|
+
config: finalDraft,
|
|
9040
|
+
service
|
|
9041
|
+
});
|
|
7705
9042
|
return;
|
|
7706
9043
|
}
|
|
7707
9044
|
}
|
|
@@ -7716,7 +9053,7 @@ var init_setup = __esm({
|
|
|
7716
9053
|
// src/setup/index.ts
|
|
7717
9054
|
function createConsoleIO() {
|
|
7718
9055
|
if (process.env.CTR_SETUP_FORCE_SCRIPTED_INPUT === "1") {
|
|
7719
|
-
const scriptedInput = (0,
|
|
9056
|
+
const scriptedInput = (0, import_fs7.readFileSync)(0, "utf-8");
|
|
7720
9057
|
const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter((item) => item.length > 0);
|
|
7721
9058
|
let cursor = 0;
|
|
7722
9059
|
const nextAnswer = async () => answers[cursor++] ?? "";
|
|
@@ -7805,7 +9142,7 @@ function createConsoleIO() {
|
|
|
7805
9142
|
}
|
|
7806
9143
|
};
|
|
7807
9144
|
}
|
|
7808
|
-
const rl = (0,
|
|
9145
|
+
const rl = (0, import_promises4.createInterface)({ input: import_process.stdin, output: import_process.stdout });
|
|
7809
9146
|
const ask = async (message) => {
|
|
7810
9147
|
const answer = await rl.question(message);
|
|
7811
9148
|
return answer.trim();
|
|
@@ -7846,7 +9183,7 @@ function createConsoleIO() {
|
|
|
7846
9183
|
};
|
|
7847
9184
|
}
|
|
7848
9185
|
function readStructuredConfigFile(filePath) {
|
|
7849
|
-
const content = (0,
|
|
9186
|
+
const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
|
|
7850
9187
|
if (filePath.endsWith(".json")) {
|
|
7851
9188
|
return JSON.parse(content);
|
|
7852
9189
|
}
|
|
@@ -7854,7 +9191,7 @@ function readStructuredConfigFile(filePath) {
|
|
|
7854
9191
|
}
|
|
7855
9192
|
function getCurrentRuntimeFields() {
|
|
7856
9193
|
const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
|
|
7857
|
-
const currentPath = candidates.find((filePath) => (0,
|
|
9194
|
+
const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
|
|
7858
9195
|
if (!currentPath) {
|
|
7859
9196
|
return {};
|
|
7860
9197
|
}
|
|
@@ -7876,7 +9213,7 @@ function getCurrentRuntimeFields() {
|
|
|
7876
9213
|
}
|
|
7877
9214
|
function getConfiguredPortFromCurrentFiles() {
|
|
7878
9215
|
const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
|
|
7879
|
-
const currentPath = candidates.find((filePath) => (0,
|
|
9216
|
+
const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
|
|
7880
9217
|
if (!currentPath) {
|
|
7881
9218
|
return DEFAULT_CONFIG2.PORT;
|
|
7882
9219
|
}
|
|
@@ -7910,7 +9247,7 @@ async function getAvailablePort() {
|
|
|
7910
9247
|
}
|
|
7911
9248
|
}
|
|
7912
9249
|
function readLegacyConfigFile(filePath) {
|
|
7913
|
-
const content = (0,
|
|
9250
|
+
const content = (0, import_fs7.readFileSync)(filePath, "utf-8");
|
|
7914
9251
|
if (filePath.endsWith(".json")) {
|
|
7915
9252
|
return import_json52.default.parse(content);
|
|
7916
9253
|
}
|
|
@@ -7918,13 +9255,13 @@ function readLegacyConfigFile(filePath) {
|
|
|
7918
9255
|
}
|
|
7919
9256
|
async function readLegacyConfig(deps = {}) {
|
|
7920
9257
|
const baseHomeDir = deps.homeDir || (0, import_os3.homedir)();
|
|
7921
|
-
const exists = deps.exists ||
|
|
9258
|
+
const exists = deps.exists || import_fs7.existsSync;
|
|
7922
9259
|
const readConfig = deps.readConfig || readLegacyConfigFile;
|
|
7923
9260
|
const overridePath = process.env.CTR_SETUP_LEGACY_CONFIG_PATH;
|
|
7924
9261
|
const candidatePaths = overridePath ? [overridePath] : [
|
|
7925
|
-
(0,
|
|
7926
|
-
(0,
|
|
7927
|
-
(0,
|
|
9262
|
+
(0, import_path7.join)(baseHomeDir, ".ccr", "config.yaml"),
|
|
9263
|
+
(0, import_path7.join)(baseHomeDir, ".claude-code-router", "config.yaml"),
|
|
9264
|
+
(0, import_path7.join)(baseHomeDir, ".claude-code-router", "config.json")
|
|
7928
9265
|
];
|
|
7929
9266
|
const legacyPath = candidatePaths.find((filePath) => exists(filePath));
|
|
7930
9267
|
if (!legacyPath) {
|
|
@@ -7946,7 +9283,7 @@ async function readLegacyConfig(deps = {}) {
|
|
|
7946
9283
|
}
|
|
7947
9284
|
async function readCurrentConfig() {
|
|
7948
9285
|
const candidates = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON];
|
|
7949
|
-
const currentPath = candidates.find((filePath) => (0,
|
|
9286
|
+
const currentPath = candidates.find((filePath) => (0, import_fs7.existsSync)(filePath));
|
|
7950
9287
|
if (!currentPath) {
|
|
7951
9288
|
return { kind: "missing" };
|
|
7952
9289
|
}
|
|
@@ -8321,6 +9658,9 @@ function applyRoutingBootstrap(draft, choice, specializedModelId) {
|
|
|
8321
9658
|
};
|
|
8322
9659
|
return nextDraft;
|
|
8323
9660
|
}
|
|
9661
|
+
function createSetupBootstrapApiKey() {
|
|
9662
|
+
return `ctr_bootstrap_${(0, import_crypto3.randomBytes)(24).toString("base64url")}`;
|
|
9663
|
+
}
|
|
8324
9664
|
async function promptModelConnection(io, input3) {
|
|
8325
9665
|
if (input3.intro) {
|
|
8326
9666
|
io.info(input3.intro);
|
|
@@ -8354,18 +9694,30 @@ async function promptModelConnection(io, input3) {
|
|
|
8354
9694
|
};
|
|
8355
9695
|
}
|
|
8356
9696
|
async function buildFreshConfig(io) {
|
|
8357
|
-
const setupEntryChoice = await io.choose("\u5F53\u524D\u8981\u672C\u5730\u4F7F\u7528\uFF0C\u8FD8\u662F\
|
|
9697
|
+
const setupEntryChoice = await io.choose("\u5F53\u524D\u8981\u672C\u5730\u4F7F\u7528\u3001\u8FDE\u63A5\u8FDC\u7A0B\u670D\u52A1\uFF0C\u8FD8\u662F\u90E8\u7F72\u4E3A\u8FDC\u7A0B\u670D\u52A1\u7AEF\uFF1F", [
|
|
8358
9698
|
"\u672C\u5730\u4F7F\u7528\uFF08\u63A8\u8350\uFF09",
|
|
8359
|
-
"\u8FDE\u63A5\u8FDC\u7A0B\u670D\u52A1"
|
|
9699
|
+
"\u8FDE\u63A5\u8FDC\u7A0B\u670D\u52A1",
|
|
9700
|
+
"\u90E8\u7F72\u4E3A\u8FDC\u7A0B\u670D\u52A1\u7AEF"
|
|
8360
9701
|
]);
|
|
8361
9702
|
if (setupEntryChoice === "\u8FDE\u63A5\u8FDC\u7A0B\u670D\u52A1") {
|
|
8362
9703
|
const baseUrl = await io.input("\u8FDC\u7A0B\u670D\u52A1 URL");
|
|
8363
9704
|
const authToken = await io.input("\u8FDC\u7A0B\u670D\u52A1 Auth Token\uFF08\u53EF\u9009\uFF09", "${CTR_REMOTE_AUTH_TOKEN}");
|
|
8364
9705
|
io.info("\u5DF2\u751F\u6210\u8FDC\u7A0B\u670D\u52A1\u8FDE\u63A5\u914D\u7F6E\uFF0C\u672C\u673A\u4E0D\u4F1A\u8981\u6C42\u4F60\u5148\u586B\u5199 provider/model\u3002");
|
|
9706
|
+
io.info(REMOTE_CLIENT_ROLE_GUIDE);
|
|
9707
|
+
io.info("\u5982\u679C\u4F60\u5176\u5B9E\u8981\u628A\u672C\u673A\u90E8\u7F72\u6210\u670D\u52A1\u7AEF\uFF0C\u8BF7\u91CD\u65B0\u8FD0\u884C setup \u9009\u62E9\u201C\u90E8\u7F72\u4E3A\u8FDC\u7A0B\u670D\u52A1\u7AEF\u201D\uFF0C\u6216\u8FD0\u884C\uFF1Actr deploy init --target server");
|
|
9708
|
+
io.info(SERVER_MAINTAINER_ROLE_GUIDE);
|
|
8365
9709
|
return buildRemoteServiceConfig({ baseUrl, authToken });
|
|
8366
9710
|
}
|
|
9711
|
+
if (setupEntryChoice === "\u90E8\u7F72\u4E3A\u8FDC\u7A0B\u670D\u52A1\u7AEF") {
|
|
9712
|
+
io.info(SERVER_MAINTAINER_ROLE_GUIDE);
|
|
9713
|
+
io.info("setup \u5C06\u751F\u6210 server profile \u548C bootstrap admin APIKEY\uFF0C\u4F46\u4E0D\u4F1A\u81EA\u52A8\u542F\u52A8\u670D\u52A1\u3002");
|
|
9714
|
+
io.info("\u4FDD\u5B58\u540E\u8BF7\u5148\u7F16\u8F91 Models[].key / Models[].model\uFF0C\u518D\u8FD0\u884C\uFF1Actr doctor && ctr start --daemon");
|
|
9715
|
+
return buildServerDeploymentConfig({
|
|
9716
|
+
apiKey: createSetupBootstrapApiKey()
|
|
9717
|
+
});
|
|
9718
|
+
}
|
|
8367
9719
|
const primaryModel = await promptModelConnection(io, {
|
|
8368
|
-
intro:
|
|
9720
|
+
intro: `\u6211\u4EEC\u5148\u521B\u5EFA\u4E00\u4EFD\u6700\u5C0F\u53EF\u7528\u914D\u7F6E\u3002${LOCAL_USER_ROLE_GUIDE}`,
|
|
8369
9721
|
modelIdPrompt: "\u9ED8\u8BA4\u6A21\u578B\u7684 model id\uFF08Router.default \u4F1A\u5F15\u7528\u5B83\uFF09",
|
|
8370
9722
|
suggestedModelId: "sonnet"
|
|
8371
9723
|
});
|
|
@@ -8466,6 +9818,31 @@ function printRoutingNextSteps(io) {
|
|
|
8466
9818
|
io.info(" - SmartRouter candidates\uFF1A\u9002\u5408\u6A21\u7CCA\u4EFB\u52A1\uFF0C\u5728\u5019\u9009\u6A21\u578B\u4E4B\u95F4\u81EA\u52A8\u9009\u62E9\u66F4\u5408\u9002\u7684\u6A21\u578B");
|
|
8467
9819
|
io.info(" - \u914D\u7F6E\u6A21\u677F\u53C2\u8003\uFF1Aconfig/trigger.advanced.yaml");
|
|
8468
9820
|
}
|
|
9821
|
+
function formatSetupServiceReadyMessage(action) {
|
|
9822
|
+
if (action === "start") {
|
|
9823
|
+
return "\u672C\u5730\u4EE3\u7406\u5DF2\u542F\u52A8\u5E76\u901A\u8FC7\u5065\u5EB7\u68C0\u67E5\u3002";
|
|
9824
|
+
}
|
|
9825
|
+
if (action === "reload") {
|
|
9826
|
+
return "\u672C\u5730\u4EE3\u7406\u5DF2\u91CD\u8F7D\u914D\u7F6E\u5E76\u901A\u8FC7\u5065\u5EB7\u68C0\u67E5\u3002";
|
|
9827
|
+
}
|
|
9828
|
+
if (action === "restart") {
|
|
9829
|
+
return "\u672C\u5730\u4EE3\u7406\u5DF2\u91CD\u542F\u5E76\u901A\u8FC7\u5065\u5EB7\u68C0\u67E5\u3002";
|
|
9830
|
+
}
|
|
9831
|
+
return "\u672C\u5730\u4EE3\u7406\u5DF2\u5728\u8FD0\u884C\u5E76\u901A\u8FC7\u5065\u5EB7\u68C0\u67E5\u3002";
|
|
9832
|
+
}
|
|
9833
|
+
function printRemoteClientNextSteps(io, action) {
|
|
9834
|
+
io.info(`${formatSetupServiceReadyMessage(action)}\u8FDC\u7A0B\u670D\u52A1\u8FDE\u63A5\u914D\u7F6E\u5DF2\u4FDD\u5B58\uFF0C\u53EF\u7528\u4E8E\u68C0\u67E5\u8FDC\u7AEF ready/status\u3002`);
|
|
9835
|
+
io.info("\u4E0B\u4E00\u6B65\uFF1A\u8FD0\u884C ctr status \u67E5\u770B\u672C\u5730\u4EE3\u7406\u4E0E\u8FDC\u7A0B\u670D\u52A1 ready \u72B6\u6001\u3002");
|
|
9836
|
+
io.info("\u65E5\u5E38\u76F4\u8FDE\u8FDC\u7A0B\u670D\u52A1\u65F6\uFF0C\u8BF7\u6309\u670D\u52A1\u7EF4\u62A4\u8005\u63D0\u4F9B\u7684 ANTHROPIC_BASE_URL \u548C ANTHROPIC_AUTH_TOKEN \u914D\u7F6E Claude Code\u3002");
|
|
9837
|
+
io.info("\u5982\u679C\u8FDC\u7AEF\u4E0D\u53EF\u7528\uFF0C\u8BF7\u786E\u8BA4 Runtime.remote_service.base_url \u548C managed client + read-only key\u3002");
|
|
9838
|
+
}
|
|
9839
|
+
function printLocalClientNextSteps(io, action) {
|
|
9840
|
+
io.info(`${formatSetupServiceReadyMessage(action)}\u65E5\u5E38\u4F7F\u7528\u8FD0\u884C\uFF1Actr code`);
|
|
9841
|
+
printRoutingNextSteps(io);
|
|
9842
|
+
}
|
|
9843
|
+
function isRemoteServiceClientConfig(config) {
|
|
9844
|
+
return config.Runtime?.mode === "local" && Boolean(config.Runtime.remote_service?.enabled);
|
|
9845
|
+
}
|
|
8469
9846
|
async function runSetupCli(customDeps) {
|
|
8470
9847
|
const defaults = createDefaultDeps(customDeps?.io);
|
|
8471
9848
|
const deps = { ...defaults, ...customDeps };
|
|
@@ -8569,8 +9946,13 @@ async function runSetupCli(customDeps) {
|
|
|
8569
9946
|
healthChecked: true
|
|
8570
9947
|
};
|
|
8571
9948
|
},
|
|
8572
|
-
enterClaudeCode: async () => {
|
|
8573
|
-
|
|
9949
|
+
enterClaudeCode: async ({ config, service }) => {
|
|
9950
|
+
if (isRemoteServiceClientConfig(config)) {
|
|
9951
|
+
printRemoteClientNextSteps(deps.io, service.action);
|
|
9952
|
+
return;
|
|
9953
|
+
} else {
|
|
9954
|
+
printLocalClientNextSteps(deps.io, service.action);
|
|
9955
|
+
}
|
|
8574
9956
|
if (!shouldAutoEnterClaudeCodeAfterSetup()) {
|
|
8575
9957
|
deps.io.info("\u4E3A\u907F\u514D setup \u7ED3\u675F\u540E\u63A5\u7BA1\u5F53\u524D\u7EC8\u7AEF\uFF0C\u8BF7\u624B\u52A8\u8FD0\u884C\uFF1Actr code");
|
|
8576
9958
|
deps.io.info("\u5982\u679C\u4F60\u660E\u786E\u9700\u8981 setup \u7ED3\u675F\u540E\u81EA\u52A8\u8FDB\u5165 Claude Code\uFF0C\u53EF\u8BBE\u7F6E\u73AF\u5883\u53D8\u91CF CTR_SETUP_AUTO_ENTER_CODE=1");
|
|
@@ -8585,15 +9967,16 @@ async function runSetupCli(customDeps) {
|
|
|
8585
9967
|
deps.io.close?.();
|
|
8586
9968
|
}
|
|
8587
9969
|
}
|
|
8588
|
-
var
|
|
9970
|
+
var import_fs7, import_crypto3, import_net2, import_os3, import_path7, import_promises4, import_process, import_json52, import_js_yaml;
|
|
8589
9971
|
var init_setup2 = __esm({
|
|
8590
9972
|
"src/setup/index.ts"() {
|
|
8591
9973
|
"use strict";
|
|
8592
|
-
|
|
9974
|
+
import_fs7 = require("fs");
|
|
9975
|
+
import_crypto3 = require("crypto");
|
|
8593
9976
|
import_net2 = require("net");
|
|
8594
9977
|
import_os3 = require("os");
|
|
8595
|
-
|
|
8596
|
-
|
|
9978
|
+
import_path7 = require("path");
|
|
9979
|
+
import_promises4 = require("readline/promises");
|
|
8597
9980
|
import_process = require("process");
|
|
8598
9981
|
import_json52 = __toESM(require("json5"));
|
|
8599
9982
|
import_js_yaml = __toESM(require("js-yaml"));
|
|
@@ -8611,6 +9994,7 @@ var init_setup2 = __esm({
|
|
|
8611
9994
|
init_templates();
|
|
8612
9995
|
init_persist();
|
|
8613
9996
|
init_setup();
|
|
9997
|
+
init_runtime_role_guidance();
|
|
8614
9998
|
}
|
|
8615
9999
|
});
|
|
8616
10000
|
|
|
@@ -8683,7 +10067,7 @@ function hasArg(flag) {
|
|
|
8683
10067
|
}
|
|
8684
10068
|
function createConsoleIO2() {
|
|
8685
10069
|
if (process.env.CTR_DOCTOR_FORCE_SCRIPTED_INPUT === "1") {
|
|
8686
|
-
const scriptedInput = (0,
|
|
10070
|
+
const scriptedInput = (0, import_fs8.readFileSync)(0, "utf-8");
|
|
8687
10071
|
const answers = scriptedInput.split(/\r?\n/).map((item) => item.trim()).filter(Boolean);
|
|
8688
10072
|
let cursor = 0;
|
|
8689
10073
|
const nextAnswer = async () => answers[cursor++] ?? "";
|
|
@@ -8726,8 +10110,17 @@ function createConsoleIO2() {
|
|
|
8726
10110
|
}
|
|
8727
10111
|
};
|
|
8728
10112
|
}
|
|
8729
|
-
const rl = (0,
|
|
8730
|
-
const ask = async (message) =>
|
|
10113
|
+
const rl = (0, import_promises5.createInterface)({ input: import_process2.stdin, output: import_process2.stdout });
|
|
10114
|
+
const ask = async (message) => {
|
|
10115
|
+
try {
|
|
10116
|
+
return (await rl.question(message)).trim();
|
|
10117
|
+
} catch (error) {
|
|
10118
|
+
if (error?.code === "ERR_USE_AFTER_CLOSE") {
|
|
10119
|
+
return void 0;
|
|
10120
|
+
}
|
|
10121
|
+
throw error;
|
|
10122
|
+
}
|
|
10123
|
+
};
|
|
8731
10124
|
return {
|
|
8732
10125
|
info(message) {
|
|
8733
10126
|
import_process2.stdout.write(`${message}
|
|
@@ -8744,6 +10137,9 @@ function createConsoleIO2() {
|
|
|
8744
10137
|
`));
|
|
8745
10138
|
while (true) {
|
|
8746
10139
|
const answer = await ask("> ");
|
|
10140
|
+
if (answer === void 0) {
|
|
10141
|
+
return options[0];
|
|
10142
|
+
}
|
|
8747
10143
|
const index = Number(answer);
|
|
8748
10144
|
if (Number.isInteger(index) && index >= 1 && index <= options.length) {
|
|
8749
10145
|
return options[index - 1];
|
|
@@ -8760,7 +10156,7 @@ function createConsoleIO2() {
|
|
|
8760
10156
|
return answer || defaultValue || "";
|
|
8761
10157
|
},
|
|
8762
10158
|
async confirm(message, defaultValue = true) {
|
|
8763
|
-
const answer = (await ask(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"}: `))
|
|
10159
|
+
const answer = (await ask(`${message} ${defaultValue ? "[Y/n]" : "[y/N]"}: `))?.toLowerCase();
|
|
8764
10160
|
if (!answer) {
|
|
8765
10161
|
return defaultValue;
|
|
8766
10162
|
}
|
|
@@ -8807,7 +10203,7 @@ function tryLoadStructuredConfig(filePath, content) {
|
|
|
8807
10203
|
}
|
|
8808
10204
|
}
|
|
8809
10205
|
function loadCurrentConfig() {
|
|
8810
|
-
const existingPath = getConfigCandidates().find((filePath) => (0,
|
|
10206
|
+
const existingPath = getConfigCandidates().find((filePath) => (0, import_fs8.existsSync)(filePath));
|
|
8811
10207
|
const path = existingPath ?? CONFIG_FILE;
|
|
8812
10208
|
if (!existingPath) {
|
|
8813
10209
|
return {
|
|
@@ -8817,7 +10213,7 @@ function loadCurrentConfig() {
|
|
|
8817
10213
|
messages: ["\u672A\u68C0\u6D4B\u5230\u5F53\u524D Claude Trigger Router \u914D\u7F6E\u3002"]
|
|
8818
10214
|
};
|
|
8819
10215
|
}
|
|
8820
|
-
const content = (0,
|
|
10216
|
+
const content = (0, import_fs8.readFileSync)(existingPath, "utf-8");
|
|
8821
10217
|
const loaded = tryLoadStructuredConfig(existingPath, content);
|
|
8822
10218
|
return {
|
|
8823
10219
|
path,
|
|
@@ -9094,7 +10490,8 @@ function explainProbeFailure(category) {
|
|
|
9094
10490
|
}
|
|
9095
10491
|
async function ensureServiceUsable(config, deps, configChanged) {
|
|
9096
10492
|
const port = config.PORT ?? DEFAULT_CONFIG2.PORT;
|
|
9097
|
-
const
|
|
10493
|
+
const serviceHealthOptions = config.APIKEY ? { apiKey: config.APIKEY } : {};
|
|
10494
|
+
const healthy = await deps.probeServiceHealth(port, 500, serviceHealthOptions);
|
|
9098
10495
|
const occupied = await deps.isTcpPortOccupied(port, 500);
|
|
9099
10496
|
const running = deps.isServiceRunning();
|
|
9100
10497
|
if (healthy && !configChanged) {
|
|
@@ -9114,7 +10511,7 @@ async function ensureServiceUsable(config, deps, configChanged) {
|
|
|
9114
10511
|
}
|
|
9115
10512
|
}
|
|
9116
10513
|
await deps.startDaemon();
|
|
9117
|
-
const verified = await deps.waitForService(port, 5e3);
|
|
10514
|
+
const verified = await deps.waitForService(port, 5e3, serviceHealthOptions);
|
|
9118
10515
|
if (!verified) {
|
|
9119
10516
|
throw new Error(`doctor \u81EA\u52A8\u542F\u52A8\u540E\u5065\u5EB7\u68C0\u67E5\u4ECD\u672A\u901A\u8FC7\uFF08\u7AEF\u53E3 ${port}\uFF09\u3002`);
|
|
9120
10517
|
}
|
|
@@ -9124,16 +10521,48 @@ async function reportRuntimeServiceContext(config, deps) {
|
|
|
9124
10521
|
const runtimeMode = config.Runtime?.mode ?? "local";
|
|
9125
10522
|
const serviceRole = runtimeMode === "local" ? "local_agent" : "router_service";
|
|
9126
10523
|
const remoteService = config.Runtime?.remote_service;
|
|
10524
|
+
const managedKeys = managedApiKeySummary(config);
|
|
10525
|
+
const hasBootstrapAuth = Boolean(config.APIKEY);
|
|
10526
|
+
const hasManagedAuthRecords = managedKeys.total > 0;
|
|
10527
|
+
const authRequired = hasBootstrapAuth || hasManagedAuthRecords;
|
|
10528
|
+
const host = String(config.HOST ?? DEFAULT_CONFIG2.HOST ?? "127.0.0.1").trim() || "127.0.0.1";
|
|
10529
|
+
const port = config.PORT ?? DEFAULT_CONFIG2.PORT;
|
|
10530
|
+
const publicHost = ["0.0.0.0", "::", "[::]"].includes(host);
|
|
10531
|
+
const listenerUrl = publicHost ? `http://<server-host>:${port}` : `http://${host}:${port}`;
|
|
9127
10532
|
deps.io.info(`\u670D\u52A1\u4E0A\u4E0B\u6587\uFF1A${runtimeMode}\uFF08${serviceRole}\uFF09`);
|
|
10533
|
+
deps.io.info(`\u76D1\u542C\u5730\u5740\uFF1A${host}:${port}${publicHost ? "\uFF08\u5BF9\u5916\u76D1\u542C\uFF09" : "\uFF08\u672C\u673A\u76D1\u542C\uFF09"}`);
|
|
10534
|
+
deps.io.info(`\u9274\u6743\u72B6\u6001\uFF1A${authRequired ? "enabled" : "disabled"}\uFF08bootstrap=${hasBootstrapAuth}, managed_active=${managedKeys.active}\uFF09`);
|
|
10535
|
+
deps.io.info("Scope \u6307\u5F15\uFF1Aadmin \u7528\u4E8E /ui\u3001\u914D\u7F6E\u4FDD\u5B58\u3001\u91CD\u542F\u3001auth \u7BA1\u7406\u548C\u6CBB\u7406\u5199\u64CD\u4F5C\uFF1Bclient \u53EA\u7528\u4E8E\u6A21\u578B\u8C03\u7528\uFF1Bread-only \u53EA\u7528\u4E8E health/status/compiled/governance \u89C2\u6D4B\u3002");
|
|
10536
|
+
deps.io.info("Key \u64CD\u4F5C\u6307\u5F15\uFF1A\u4F7F\u7528 admin key \u8C03\u7528 GET /api/auth/keys \u67E5\u770B\u5217\u8868\u3001POST /api/auth/keys \u751F\u6210 key\u3001POST /api/auth/keys/:id/revoke \u540A\u9500 key\uFF1B\u751F\u6210\u7684 secret \u53EA\u8FD4\u56DE\u4E00\u6B21\u3002");
|
|
10537
|
+
if (runtimeMode !== "local") {
|
|
10538
|
+
deps.io.info(`\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u63A5\u5165\uFF1AANTHROPIC_BASE_URL=${listenerUrl}\uFF0CANTHROPIC_AUTH_TOKEN \u4F7F\u7528 managed client + read-only key\u3002`);
|
|
10539
|
+
deps.io.info(`\u7EF4\u62A4\u5165\u53E3\uFF1Ahttp://127.0.0.1:${port}/ui\uFF1B\u516C\u7F51\u8BBF\u95EE\u8BF7\u653E\u5728 HTTPS \u53CD\u5411\u4EE3\u7406\u6216\u5185\u7F51\u4E4B\u540E\u3002`);
|
|
10540
|
+
}
|
|
10541
|
+
if (!authRequired && (runtimeMode !== "local" || publicHost)) {
|
|
10542
|
+
deps.io.error("\u5B89\u5168\u98CE\u9669\uFF1A\u5F53\u524D server/cloud \u6216\u516C\u7F51\u76D1\u542C\u672A\u914D\u7F6E API key\uFF1B\u66B4\u9732\u670D\u52A1\u524D\u8BF7\u8BBE\u7F6E APIKEY \u6216\u521B\u5EFA managed client/admin key\u3002");
|
|
10543
|
+
} else if (!hasBootstrapAuth && hasManagedAuthRecords && managedKeys.active === 0) {
|
|
10544
|
+
deps.io.error("\u5B89\u5168\u98CE\u9669\uFF1A\u5F53\u524D\u4EC5\u4FDD\u7559 managed key \u8BB0\u5F55\u4F46\u6CA1\u6709 active key\uFF1B\u670D\u52A1\u4F1A\u62D2\u7EDD\u8BF7\u6C42\uFF0C\u8BF7\u8BBE\u7F6E APIKEY \u6216\u521B\u5EFA active managed key\u3002");
|
|
10545
|
+
} else if (authRequired && hasBootstrapAuth && managedKeys.total === 0 && runtimeMode !== "local") {
|
|
10546
|
+
deps.io.info("\u5B89\u5168\u63D0\u793A\uFF1A\u5F53\u524D\u4EC5\u914D\u7F6E bootstrap APIKEY\uFF1B\u5EFA\u8BAE\u4E3A\u8FDC\u7A0B\u4F7F\u7528\u8005\u751F\u6210 managed client key\uFF0C\u5E76\u4FDD\u7559 APIKEY \u53EA\u505A\u7BA1\u7406\u7528\u9014\u3002");
|
|
10547
|
+
}
|
|
9128
10548
|
if (!remoteService?.enabled) {
|
|
9129
10549
|
deps.io.info("\u8FDC\u7A0B\u670D\u52A1\u68C0\u67E5\uFF1A\u672A\u542F\u7528\uFF0C\u672C\u673A\u4F7F\u7528\u672C\u5730\u914D\u7F6E\u548C\u672C\u5730\u670D\u52A1\u5065\u5EB7\u68C0\u67E5\u3002");
|
|
9130
10550
|
return;
|
|
9131
10551
|
}
|
|
9132
10552
|
const baseUrl = remoteService.base_url?.trim().replace(/\/+$/, "") || "<missing>";
|
|
9133
10553
|
deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u68C0\u67E5\uFF1A${baseUrl}`);
|
|
10554
|
+
deps.io.info("\u8FDC\u7A0B token \u6307\u5F15\uFF1ARuntime.remote_service.auth_token \u5982\u679C\u540C\u65F6\u8981\u63A2\u6D4B ready/status \u5E76\u8C03\u7528\u6A21\u578B\uFF0C\u8BF7\u901A\u8FC7 POST /api/auth/keys \u751F\u6210 client + read-only key\uFF1B\u907F\u514D\u590D\u7528 admin key\u3002");
|
|
9134
10555
|
const remoteStatus = await probeRemoteServiceStatus(remoteService);
|
|
9135
10556
|
const statusLabel = remoteStatus.ready ? "ready" : remoteStatus.reachable ? "reachable" : "unreachable";
|
|
9136
10557
|
deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u72B6\u6001\uFF1A${statusLabel}\uFF08reachable=${remoteStatus.reachable}, ready=${remoteStatus.ready}\uFF09`);
|
|
10558
|
+
const remoteSecurity = remoteStatus.security && typeof remoteStatus.security === "object" ? remoteStatus.security : void 0;
|
|
10559
|
+
if (remoteSecurity?.status) {
|
|
10560
|
+
deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u5B89\u5168\u72B6\u6001\uFF1A${String(remoteSecurity.status)}`);
|
|
10561
|
+
const firstIssue = Array.isArray(remoteSecurity.issues) ? remoteSecurity.issues[0] : void 0;
|
|
10562
|
+
if (firstIssue?.message) {
|
|
10563
|
+
deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u5B89\u5168\u63D0\u793A\uFF1A${firstIssue.message}${firstIssue.action ? `\uFF1B${firstIssue.action}` : ""}`);
|
|
10564
|
+
}
|
|
10565
|
+
}
|
|
9137
10566
|
if (remoteStatus.error) {
|
|
9138
10567
|
deps.io.info(`\u8FDC\u7A0B\u670D\u52A1\u63D0\u793A\uFF1A${remoteStatus.error}`);
|
|
9139
10568
|
}
|
|
@@ -9262,12 +10691,12 @@ async function runDoctorCli(customDeps) {
|
|
|
9262
10691
|
deps.io.close?.();
|
|
9263
10692
|
}
|
|
9264
10693
|
}
|
|
9265
|
-
var
|
|
10694
|
+
var import_fs8, import_promises5, import_process2, import_child_process2, import_json53, import_js_yaml2;
|
|
9266
10695
|
var init_doctor = __esm({
|
|
9267
10696
|
"src/doctor/index.ts"() {
|
|
9268
10697
|
"use strict";
|
|
9269
|
-
|
|
9270
|
-
|
|
10698
|
+
import_fs8 = require("fs");
|
|
10699
|
+
import_promises5 = require("readline/promises");
|
|
9271
10700
|
import_process2 = require("process");
|
|
9272
10701
|
import_child_process2 = require("child_process");
|
|
9273
10702
|
import_json53 = __toESM(require("json5"));
|
|
@@ -9283,6 +10712,7 @@ var init_doctor = __esm({
|
|
|
9283
10712
|
init_processCheck();
|
|
9284
10713
|
init_service_health();
|
|
9285
10714
|
init_templates();
|
|
10715
|
+
init_api_keys();
|
|
9286
10716
|
}
|
|
9287
10717
|
});
|
|
9288
10718
|
|
|
@@ -9295,7 +10725,7 @@ __export(cli_exports, {
|
|
|
9295
10725
|
});
|
|
9296
10726
|
module.exports = __toCommonJS(cli_exports);
|
|
9297
10727
|
function getPackageInfo() {
|
|
9298
|
-
const content = (0,
|
|
10728
|
+
const content = (0, import_fs9.readFileSync)(PACKAGE_JSON_PATH, "utf-8");
|
|
9299
10729
|
const pkg = JSON.parse(content);
|
|
9300
10730
|
return {
|
|
9301
10731
|
name: pkg.name ?? "@peterwangze/claude-trigger-router",
|
|
@@ -9335,16 +10765,16 @@ function getPort() {
|
|
|
9335
10765
|
}
|
|
9336
10766
|
try {
|
|
9337
10767
|
const yaml4 = require("js-yaml");
|
|
9338
|
-
if ((0,
|
|
9339
|
-
const content = (0,
|
|
10768
|
+
if ((0, import_fs9.existsSync)(CONFIG_FILE)) {
|
|
10769
|
+
const content = (0, import_fs9.readFileSync)(CONFIG_FILE, "utf-8");
|
|
9340
10770
|
const config = yaml4.load(content);
|
|
9341
10771
|
if (config?.PORT) return config.PORT;
|
|
9342
|
-
} else if ((0,
|
|
9343
|
-
const content = (0,
|
|
10772
|
+
} else if ((0, import_fs9.existsSync)(CONFIG_FILE_YML)) {
|
|
10773
|
+
const content = (0, import_fs9.readFileSync)(CONFIG_FILE_YML, "utf-8");
|
|
9344
10774
|
const config = yaml4.load(content);
|
|
9345
10775
|
if (config?.PORT) return config.PORT;
|
|
9346
|
-
} else if ((0,
|
|
9347
|
-
const content = (0,
|
|
10776
|
+
} else if ((0, import_fs9.existsSync)(CONFIG_FILE_JSON)) {
|
|
10777
|
+
const content = (0, import_fs9.readFileSync)(CONFIG_FILE_JSON, "utf-8");
|
|
9348
10778
|
const config = JSON.parse(content);
|
|
9349
10779
|
if (config?.PORT) return config.PORT;
|
|
9350
10780
|
}
|
|
@@ -9365,6 +10795,7 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
|
|
|
9365
10795
|
setup \u68C0\u6D4B\u5E76\u590D\u7528\u5DF2\u6709\u914D\u7F6E\uFF0C\u5FC5\u8981\u65F6\u8FC1\u79FB\u65E7\u914D\u7F6E\u6216\u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
|
|
9366
10796
|
doctor \u8BCA\u65AD\u5E76\u4FEE\u590D\u5F53\u524D\u914D\u7F6E\uFF0C\u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
|
|
9367
10797
|
init \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
|
|
10798
|
+
deploy \u751F\u6210\u90E8\u7F72\u5165\u53E3\u914D\u7F6E\uFF08\u5F53\u524D\u652F\u6301 deploy init --target server\uFF09
|
|
9368
10799
|
start \u542F\u52A8\u8DEF\u7531\u670D\u52A1\uFF08\u9ED8\u8BA4\u524D\u53F0\u8FD0\u884C\uFF09
|
|
9369
10800
|
stop \u505C\u6B62\u540E\u53F0\u670D\u52A1
|
|
9370
10801
|
restart \u91CD\u542F\u540E\u53F0\u670D\u52A1
|
|
@@ -9378,12 +10809,13 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
|
|
|
9378
10809
|
\u9009\u9879\uFF1A
|
|
9379
10810
|
--port, -p \u6307\u5B9A\u76D1\u542C\u7AEF\u53E3\uFF08\u9ED8\u8BA4\uFF1A5678\uFF09
|
|
9380
10811
|
--daemon, -d \u4EE5\u540E\u53F0\u65B9\u5F0F\u8FD0\u884C\uFF08\u914D\u5408 start/restart \u4F7F\u7528\uFF09
|
|
9381
|
-
--force \u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u914D\u7F6E\uFF08\u914D\u5408 init \u4F7F\u7528\uFF09
|
|
10812
|
+
--force \u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u914D\u7F6E\uFF08\u914D\u5408 init/deploy init \u4F7F\u7528\uFF09
|
|
9382
10813
|
|
|
9383
10814
|
\u4F7F\u7528\u793A\u4F8B\uFF1A
|
|
9384
10815
|
ctr setup # \u590D\u7528\u5F53\u524D\u914D\u7F6E / \u8FC1\u79FB\u65E7\u914D\u7F6E / \u65B0\u5EFA\u6700\u5C0F\u914D\u7F6E
|
|
9385
10816
|
ctr doctor # \u8BCA\u65AD\u914D\u7F6E / \u4FEE\u590D\u683C\u5F0F\u95EE\u9898 / \u6309\u9700\u63A2\u6D4B\u6A21\u578B\u53EF\u7528\u6027
|
|
9386
10817
|
ctr init # \u521D\u59CB\u5316\u6700\u5C0F\u914D\u7F6E\u6A21\u677F
|
|
10818
|
+
ctr deploy init --target server # \u751F\u6210\u5B89\u5168\u9ED8\u8BA4\u7684 server \u90E8\u7F72\u914D\u7F6E
|
|
9387
10819
|
ctr version # \u67E5\u770B\u5F53\u524D\u5B89\u88C5\u7248\u672C
|
|
9388
10820
|
ctr upgrade # \u67E5\u770B\u5347\u7EA7\u5230\u6700\u65B0\u7248\u672C\u7684\u547D\u4EE4
|
|
9389
10821
|
ctr start # \u524D\u53F0\u542F\u52A8\uFF08\u63A8\u8350\u9996\u6B21\u4F7F\u7528\uFF0C\u4FBF\u4E8E\u67E5\u770B\u65E5\u5FD7\uFF09
|
|
@@ -9406,6 +10838,73 @@ Claude Trigger Router - \u667A\u80FD\u89E6\u53D1\u8DEF\u7531\u5668
|
|
|
9406
10838
|
\u66F4\u591A\u4FE1\u606F\uFF1Ahttps://github.com/peterwangze/claude-trigger-router
|
|
9407
10839
|
`);
|
|
9408
10840
|
}
|
|
10841
|
+
function readConfigForCliStatus() {
|
|
10842
|
+
const yaml4 = require("js-yaml");
|
|
10843
|
+
for (const configFile of [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON]) {
|
|
10844
|
+
if (!(0, import_fs9.existsSync)(configFile)) {
|
|
10845
|
+
continue;
|
|
10846
|
+
}
|
|
10847
|
+
const content = (0, import_fs9.readFileSync)(configFile, "utf-8");
|
|
10848
|
+
return configFile.endsWith(".json") ? JSON.parse(content) : yaml4.load(content);
|
|
10849
|
+
}
|
|
10850
|
+
return {};
|
|
10851
|
+
}
|
|
10852
|
+
function getLocalClaudeProxyToken(config) {
|
|
10853
|
+
const bootstrapKey = typeof config?.APIKEY === "string" ? config.APIKEY.trim() : "";
|
|
10854
|
+
return bootstrapKey || "ctr-local-proxy";
|
|
10855
|
+
}
|
|
10856
|
+
async function fetchLiveServiceInfo(port, apiKey) {
|
|
10857
|
+
try {
|
|
10858
|
+
const headers = apiKey?.trim() ? { Authorization: `Bearer ${apiKey.trim()}` } : void 0;
|
|
10859
|
+
const response = await fetch(`http://127.0.0.1:${port}${SERVICE_INFO_PATH}`, {
|
|
10860
|
+
headers,
|
|
10861
|
+
signal: AbortSignal.timeout(500)
|
|
10862
|
+
});
|
|
10863
|
+
if (!response.ok) {
|
|
10864
|
+
return null;
|
|
10865
|
+
}
|
|
10866
|
+
const payload = await response.json();
|
|
10867
|
+
if (!payload || typeof payload !== "object" || payload.service !== SERVICE_NAME) {
|
|
10868
|
+
return null;
|
|
10869
|
+
}
|
|
10870
|
+
return payload;
|
|
10871
|
+
} catch {
|
|
10872
|
+
return null;
|
|
10873
|
+
}
|
|
10874
|
+
}
|
|
10875
|
+
function printRuntimeStatus(config, port, liveInfo) {
|
|
10876
|
+
const normalized = normalizeAndValidateConfig(config ?? {}).config;
|
|
10877
|
+
const runtimeMode = liveInfo?.runtimeMode ?? normalized.Runtime?.mode ?? "local";
|
|
10878
|
+
const serviceRole = liveInfo?.serviceRole ?? (runtimeMode === "local" ? "local_agent" : "router_service");
|
|
10879
|
+
const listener = liveInfo?.listener && typeof liveInfo.listener === "object" ? liveInfo.listener : null;
|
|
10880
|
+
const host = String(listener?.host ?? normalized.HOST ?? DEFAULT_CONFIG2.HOST ?? "127.0.0.1").trim() || "127.0.0.1";
|
|
10881
|
+
const publicHost = typeof listener?.public === "boolean" ? listener.public : ["0.0.0.0", "::", "[::]"].includes(host);
|
|
10882
|
+
const listenerPort = Number(listener?.port ?? port) || port;
|
|
10883
|
+
const managedKeys = managedApiKeySummary(normalized);
|
|
10884
|
+
const liveAuth = liveInfo?.auth && typeof liveInfo.auth === "object" ? liveInfo.auth : null;
|
|
10885
|
+
const hasBootstrapAuth = Boolean(liveAuth?.bootstrapConfigured ?? normalized.APIKEY);
|
|
10886
|
+
const managedActive = Number(liveAuth?.managedKeys?.active ?? managedKeys.active) || 0;
|
|
10887
|
+
const authRequired = Boolean(liveAuth?.required ?? (hasBootstrapAuth || managedKeys.total > 0));
|
|
10888
|
+
const listenerUrl = String(listener?.advertisedUrl ?? (publicHost ? `http://<server-host>:${listenerPort}` : `http://${host}:${listenerPort}`));
|
|
10889
|
+
const remoteService = normalized.Runtime?.remote_service;
|
|
10890
|
+
const clientConnection = liveInfo?.clientConnection && typeof liveInfo.clientConnection === "object" ? liveInfo.clientConnection : null;
|
|
10891
|
+
console.log(` \u6A21\u5F0F\uFF1A${runtimeMode}\uFF08${serviceRole}\uFF09`);
|
|
10892
|
+
console.log(` \u76D1\u542C\uFF1A${host}:${listenerPort}${publicHost ? "\uFF08\u5BF9\u5916\u76D1\u542C\uFF09" : "\uFF08\u672C\u673A\u76D1\u542C\uFF09"}`);
|
|
10893
|
+
console.log(` \u9274\u6743\uFF1A${authRequired ? "enabled" : "disabled"}\uFF08bootstrap=${hasBootstrapAuth}, managed_active=${managedActive}\uFF09`);
|
|
10894
|
+
if (runtimeMode !== "local") {
|
|
10895
|
+
console.log(` \u8FDC\u7A0B\u5BA2\u6237\u7AEF\u63A5\u5165\uFF1AANTHROPIC_BASE_URL=${clientConnection?.baseUrl || listenerUrl}`);
|
|
10896
|
+
console.log(" \u63A8\u8350\u5BA2\u6237\u7AEF key\uFF1Amanaged client + read-only\uFF1B\u4E0D\u8981\u628A admin/bootstrap key \u53D1\u7ED9\u8FDC\u7A0B\u4F7F\u7528\u8005\u3002");
|
|
10897
|
+
console.log(` \u7EF4\u62A4\u5165\u53E3\uFF1Ahttp://127.0.0.1:${listenerPort}/ui\uFF08\u9700\u8981 admin key\uFF09`);
|
|
10898
|
+
return;
|
|
10899
|
+
}
|
|
10900
|
+
if (clientConnection?.role === "remote_client" || remoteService?.enabled) {
|
|
10901
|
+
const baseUrl = String(clientConnection?.baseUrl || remoteService?.base_url || "").trim().replace(/\/+$/, "") || "<missing>";
|
|
10902
|
+
console.log(` \u8FDC\u7A0B\u670D\u52A1\uFF1A${baseUrl}`);
|
|
10903
|
+
console.log(" \u63A8\u8350\u8FDC\u7A0B token\uFF1Amanaged client + read-only\uFF0C\u7528\u4E8E ready/status \u63A2\u6D4B\u548C\u6A21\u578B\u8C03\u7528\u3002");
|
|
10904
|
+
return;
|
|
10905
|
+
}
|
|
10906
|
+
console.log(` \u672C\u5730\u63A5\u5165\uFF1A${clientConnection?.baseUrl || `http://127.0.0.1:${listenerPort}`}`);
|
|
10907
|
+
}
|
|
9409
10908
|
function getLatestPackageVersionViaNpm(packageName, timeoutMs = 5e3) {
|
|
9410
10909
|
try {
|
|
9411
10910
|
const result = (0, import_child_process3.spawnSync)("npm", ["view", packageName, "version", "--registry", PACKAGE_REGISTRY_URL], {
|
|
@@ -9492,16 +10991,19 @@ function isClaudeCommandAvailable(timeoutMs = 3e3) {
|
|
|
9492
10991
|
return false;
|
|
9493
10992
|
}
|
|
9494
10993
|
}
|
|
10994
|
+
function createBootstrapApiKey() {
|
|
10995
|
+
return `ctr_bootstrap_${(0, import_crypto4.randomBytes)(24).toString("hex")}`;
|
|
10996
|
+
}
|
|
9495
10997
|
function initConfig2() {
|
|
9496
10998
|
const force = hasArg2("--force");
|
|
9497
|
-
const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(
|
|
10999
|
+
const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs9.existsSync);
|
|
9498
11000
|
if (existingConfig && !force) {
|
|
9499
11001
|
console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
|
|
9500
11002
|
console.log(" \u5982\u9700\u8986\u76D6\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
|
|
9501
11003
|
return;
|
|
9502
11004
|
}
|
|
9503
|
-
if (!(0,
|
|
9504
|
-
(0,
|
|
11005
|
+
if (!(0, import_fs9.existsSync)(CONFIG_DIR)) {
|
|
11006
|
+
(0, import_fs9.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
9505
11007
|
}
|
|
9506
11008
|
try {
|
|
9507
11009
|
const yaml4 = require("js-yaml");
|
|
@@ -9511,7 +11013,7 @@ function initConfig2() {
|
|
|
9511
11013
|
lineWidth: -1,
|
|
9512
11014
|
noRefs: true
|
|
9513
11015
|
});
|
|
9514
|
-
(0,
|
|
11016
|
+
(0, import_fs9.writeFileSync)(CONFIG_FILE, content, "utf-8");
|
|
9515
11017
|
const action = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
|
|
9516
11018
|
console.log(`\u2705 \u914D\u7F6E\u6587\u4EF6${action}\uFF1A${CONFIG_FILE}`);
|
|
9517
11019
|
console.log("");
|
|
@@ -9526,6 +11028,65 @@ function initConfig2() {
|
|
|
9526
11028
|
process.exit(1);
|
|
9527
11029
|
}
|
|
9528
11030
|
}
|
|
11031
|
+
function printDeployHelp() {
|
|
11032
|
+
console.log("\u7528\u6CD5\uFF1Actr deploy init --target server [--force]");
|
|
11033
|
+
console.log("");
|
|
11034
|
+
console.log("\u5F53\u524D\u652F\u6301\uFF1A");
|
|
11035
|
+
console.log(" server \u751F\u6210\u5E26 HOST/APIKEY/Runtime.mode/Models/Router \u7684\u81EA\u6258\u7BA1\u670D\u52A1\u7AEF\u914D\u7F6E");
|
|
11036
|
+
console.log("");
|
|
11037
|
+
console.log("\u4E0B\u4E00\u6B65\uFF1A");
|
|
11038
|
+
console.log(" 1. \u7F16\u8F91 Models[].key \u548C Models[].model");
|
|
11039
|
+
console.log(" 2. \u8FD0\u884C ctr doctor \u68C0\u67E5\u914D\u7F6E\u548C\u9274\u6743\u72B6\u6001");
|
|
11040
|
+
console.log(" 3. \u8FD0\u884C ctr start --daemon \u542F\u52A8\u670D\u52A1");
|
|
11041
|
+
}
|
|
11042
|
+
function initDeployConfig() {
|
|
11043
|
+
const action = getArgs()[1];
|
|
11044
|
+
const target = getArgValue("--target") ?? "server";
|
|
11045
|
+
const force = hasArg2("--force");
|
|
11046
|
+
if (action !== "init") {
|
|
11047
|
+
printDeployHelp();
|
|
11048
|
+
return;
|
|
11049
|
+
}
|
|
11050
|
+
if (target !== "server") {
|
|
11051
|
+
console.error(`\u274C \u5F53\u524D\u4E0D\u652F\u6301\u7684\u90E8\u7F72\u76EE\u6807\uFF1A${target}`);
|
|
11052
|
+
printDeployHelp();
|
|
11053
|
+
process.exit(1);
|
|
11054
|
+
}
|
|
11055
|
+
const existingConfig = [CONFIG_FILE, CONFIG_FILE_YML, CONFIG_FILE_JSON].find(import_fs9.existsSync);
|
|
11056
|
+
if (existingConfig && !force) {
|
|
11057
|
+
console.log(`\u26A0\uFE0F \u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728\uFF1A${existingConfig}`);
|
|
11058
|
+
console.log(" \u5982\u9700\u8986\u76D6\u90E8\u7F72\u6A21\u677F\uFF0C\u8BF7\u4F7F\u7528 --force \u53C2\u6570\u3002");
|
|
11059
|
+
return;
|
|
11060
|
+
}
|
|
11061
|
+
if (!(0, import_fs9.existsSync)(CONFIG_DIR)) {
|
|
11062
|
+
(0, import_fs9.mkdirSync)(CONFIG_DIR, { recursive: true });
|
|
11063
|
+
}
|
|
11064
|
+
try {
|
|
11065
|
+
const yaml4 = require("js-yaml");
|
|
11066
|
+
const templateConfig = buildServerDeploymentConfig({
|
|
11067
|
+
apiKey: createBootstrapApiKey()
|
|
11068
|
+
});
|
|
11069
|
+
const content = yaml4.dump(templateConfig, {
|
|
11070
|
+
indent: 2,
|
|
11071
|
+
lineWidth: -1,
|
|
11072
|
+
noRefs: true
|
|
11073
|
+
});
|
|
11074
|
+
(0, import_fs9.writeFileSync)(CONFIG_FILE, content, "utf-8");
|
|
11075
|
+
const actionLabel = force ? "\u5DF2\u8986\u76D6" : "\u5DF2\u521B\u5EFA";
|
|
11076
|
+
console.log(`\u2705 Server \u90E8\u7F72\u914D\u7F6E${actionLabel}\uFF1A${CONFIG_FILE}`);
|
|
11077
|
+
console.log("");
|
|
11078
|
+
console.log("\u5DF2\u751F\u6210 bootstrap admin APIKEY\uFF1B\u8BF7\u53EA\u7528\u4E8E\u7EF4\u62A4\u8005\u7BA1\u7406\uFF0C\u4E0D\u8981\u53D1\u7ED9\u8FDC\u7A0B\u5BA2\u6237\u7AEF\u3002");
|
|
11079
|
+
console.log("");
|
|
11080
|
+
console.log("\u4E0B\u4E00\u6B65\uFF1A");
|
|
11081
|
+
console.log(" 1. \u7F16\u8F91 Models[].key \u548C Models[].model\uFF0C\u586B\u5165\u670D\u52A1\u7AEF\u8981\u4EE3\u7406\u7684\u4E0A\u6E38\u6A21\u578B");
|
|
11082
|
+
console.log(" 2. \u8FD0\u884C\uFF1Actr doctor");
|
|
11083
|
+
console.log(" 3. \u8FD0\u884C\uFF1Actr start --daemon");
|
|
11084
|
+
console.log(" 4. \u7528 admin key \u8C03\u7528 POST /api/auth/keys \u751F\u6210 client + read-only \u8FDC\u7A0B\u5BA2\u6237\u7AEF key");
|
|
11085
|
+
} catch (error) {
|
|
11086
|
+
console.error("\u274C \u521B\u5EFA\u90E8\u7F72\u914D\u7F6E\u5931\u8D25:", error.message);
|
|
11087
|
+
process.exit(1);
|
|
11088
|
+
}
|
|
11089
|
+
}
|
|
9529
11090
|
async function startForeground(port) {
|
|
9530
11091
|
const targetPort = port ?? getPort();
|
|
9531
11092
|
const healthy = await waitForService(targetPort, 500);
|
|
@@ -9613,16 +11174,28 @@ async function startDaemon(port) {
|
|
|
9613
11174
|
console.log(` Run 'ctr stop' to stop it.`);
|
|
9614
11175
|
}
|
|
9615
11176
|
async function showStatus() {
|
|
11177
|
+
const config = readConfigForCliStatus();
|
|
11178
|
+
const configuredPort = getPort();
|
|
11179
|
+
const healthOptions = config?.APIKEY ? { apiKey: config.APIKEY } : {};
|
|
9616
11180
|
const info = readServiceInfo();
|
|
9617
11181
|
if (!info || !isServiceRunning()) {
|
|
9618
|
-
const targetPort =
|
|
9619
|
-
const healthy = await waitForService(targetPort, 500);
|
|
11182
|
+
const targetPort = configuredPort;
|
|
11183
|
+
const healthy = await waitForService(targetPort, 500, healthOptions);
|
|
9620
11184
|
const occupied = await isTcpPortOccupied(targetPort, 500);
|
|
11185
|
+
if (healthy) {
|
|
11186
|
+
const liveInfo2 = await fetchLiveServiceInfo(targetPort, config?.APIKEY);
|
|
11187
|
+
console.log("\u2705 \u670D\u52A1\u8FD0\u884C\u4E2D");
|
|
11188
|
+
console.log(` \u7AEF\u53E3\uFF1A${targetPort}`);
|
|
11189
|
+
console.log(` \u63A5\u5165\u5730\u5740\uFF1Ahttp://127.0.0.1:${targetPort}`);
|
|
11190
|
+
printRuntimeStatus(config, targetPort, liveInfo2);
|
|
11191
|
+
return;
|
|
11192
|
+
}
|
|
9621
11193
|
if (!healthy && occupied) {
|
|
9622
11194
|
console.log(`\u26A0\uFE0F \u7AEF\u53E3 ${targetPort} \u5DF2\u88AB\u5176\u4ED6\u670D\u52A1\u5360\u7528\uFF0C\u5F53\u524D\u4E0D\u662F claude-trigger-router\u3002`);
|
|
9623
11195
|
return;
|
|
9624
11196
|
}
|
|
9625
11197
|
console.log("\u23F9 \u670D\u52A1\u672A\u8FD0\u884C");
|
|
11198
|
+
printRuntimeStatus(config, targetPort);
|
|
9626
11199
|
return;
|
|
9627
11200
|
}
|
|
9628
11201
|
const startTime = info.startTime ? new Date(info.startTime).toLocaleString() : "\u672A\u77E5";
|
|
@@ -9631,6 +11204,8 @@ async function showStatus() {
|
|
|
9631
11204
|
console.log(` \u7AEF\u53E3\uFF1A${info.port}`);
|
|
9632
11205
|
console.log(` \u542F\u52A8\u65F6\u95F4\uFF1A${startTime}`);
|
|
9633
11206
|
console.log(` \u63A5\u5165\u5730\u5740\uFF1Ahttp://127.0.0.1:${info.port}`);
|
|
11207
|
+
const liveInfo = await fetchLiveServiceInfo(info.port, config?.APIKEY);
|
|
11208
|
+
printRuntimeStatus(config, info.port, liveInfo);
|
|
9634
11209
|
}
|
|
9635
11210
|
function stopService() {
|
|
9636
11211
|
const info = readServiceInfo();
|
|
@@ -9653,10 +11228,12 @@ async function restartService() {
|
|
|
9653
11228
|
}
|
|
9654
11229
|
async function runClaudeCode() {
|
|
9655
11230
|
const port = getPort();
|
|
11231
|
+
const config = readConfigForCliStatus();
|
|
11232
|
+
const proxyToken = getLocalClaudeProxyToken(config);
|
|
9656
11233
|
await initializeClaudeConfig();
|
|
9657
11234
|
const running = isServiceRunning();
|
|
9658
11235
|
console.log(`\u{1F50D} Checking if service is available on port ${port}...`);
|
|
9659
|
-
const reachable = await waitForService(port, 2e3);
|
|
11236
|
+
const reachable = await waitForService(port, 2e3, { apiKey: proxyToken });
|
|
9660
11237
|
if (!reachable) {
|
|
9661
11238
|
console.log(`\u26A0\uFE0F Trigger Router service is not running on port ${port}.`);
|
|
9662
11239
|
console.log("");
|
|
@@ -9673,14 +11250,16 @@ async function runClaudeCode() {
|
|
|
9673
11250
|
process.exit(1);
|
|
9674
11251
|
}
|
|
9675
11252
|
const isWindows = process.platform === "win32";
|
|
11253
|
+
const claudeEnv = {
|
|
11254
|
+
...process.env,
|
|
11255
|
+
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
|
|
11256
|
+
ANTHROPIC_AUTH_TOKEN: proxyToken
|
|
11257
|
+
};
|
|
11258
|
+
delete claudeEnv.ANTHROPIC_API_KEY;
|
|
9676
11259
|
const claude = (0, import_child_process3.spawn)("claude", [], {
|
|
9677
11260
|
stdio: "inherit",
|
|
9678
11261
|
shell: isWindows,
|
|
9679
|
-
env:
|
|
9680
|
-
...process.env,
|
|
9681
|
-
ANTHROPIC_BASE_URL: `http://127.0.0.1:${port}`,
|
|
9682
|
-
ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY || "ctr-local-proxy"
|
|
9683
|
-
}
|
|
11262
|
+
env: claudeEnv
|
|
9684
11263
|
});
|
|
9685
11264
|
claude.on("error", (error) => {
|
|
9686
11265
|
console.error("\u274C \u542F\u52A8 Claude Code \u5931\u8D25:", error.message);
|
|
@@ -9723,6 +11302,9 @@ async function main() {
|
|
|
9723
11302
|
case "init":
|
|
9724
11303
|
initConfig2();
|
|
9725
11304
|
break;
|
|
11305
|
+
case "deploy":
|
|
11306
|
+
initDeployConfig();
|
|
11307
|
+
break;
|
|
9726
11308
|
case "start":
|
|
9727
11309
|
if (isDaemonMode()) {
|
|
9728
11310
|
await startDaemon(getPort());
|
|
@@ -9765,13 +11347,14 @@ async function main() {
|
|
|
9765
11347
|
process.exit(command ? 1 : 0);
|
|
9766
11348
|
}
|
|
9767
11349
|
}
|
|
9768
|
-
var import_child_process3,
|
|
11350
|
+
var import_child_process3, import_crypto4, import_path8, import_openurl, import_fs9, PACKAGE_JSON_PATH, PACKAGE_PAGE_URL, PACKAGE_REGISTRY_LATEST_URL, PACKAGE_REGISTRY_URL;
|
|
9769
11351
|
var init_cli = __esm({
|
|
9770
11352
|
"src/cli.ts"() {
|
|
9771
11353
|
import_child_process3 = require("child_process");
|
|
9772
|
-
|
|
11354
|
+
import_crypto4 = require("crypto");
|
|
11355
|
+
import_path8 = require("path");
|
|
9773
11356
|
import_openurl = __toESM(require("openurl"));
|
|
9774
|
-
|
|
11357
|
+
import_fs9 = require("fs");
|
|
9775
11358
|
init_index();
|
|
9776
11359
|
init_processCheck();
|
|
9777
11360
|
init_constants();
|
|
@@ -9779,7 +11362,9 @@ var init_cli = __esm({
|
|
|
9779
11362
|
init_setup2();
|
|
9780
11363
|
init_templates();
|
|
9781
11364
|
init_doctor();
|
|
9782
|
-
|
|
11365
|
+
init_api_keys();
|
|
11366
|
+
init_config();
|
|
11367
|
+
PACKAGE_JSON_PATH = (0, import_path8.join)(__dirname, "..", "package.json");
|
|
9783
11368
|
PACKAGE_PAGE_URL = "https://www.npmjs.com/package/@peterwangze/claude-trigger-router";
|
|
9784
11369
|
PACKAGE_REGISTRY_LATEST_URL = "https://registry.npmjs.org/@peterwangze%2Fclaude-trigger-router/latest";
|
|
9785
11370
|
PACKAGE_REGISTRY_URL = "https://registry.npmjs.org/";
|