@jsonstudio/rcc 0.89.942 → 0.89.1083

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.
Files changed (91) hide show
  1. package/README.md +1 -42
  2. package/dist/build-info.js +2 -2
  3. package/dist/build-info.js.map +1 -1
  4. package/dist/cli.js +106 -10
  5. package/dist/cli.js.map +1 -1
  6. package/dist/commands/quota-daemon.d.ts +2 -0
  7. package/dist/commands/quota-daemon.js +89 -0
  8. package/dist/commands/quota-daemon.js.map +1 -0
  9. package/dist/docs/daemon-admin-ui.html +958 -0
  10. package/dist/index.js +5 -1
  11. package/dist/index.js.map +1 -1
  12. package/dist/manager/modules/quota/index.d.ts +34 -0
  13. package/dist/manager/modules/quota/index.js +291 -0
  14. package/dist/manager/modules/quota/index.js.map +1 -1
  15. package/dist/manager/modules/token/index.js +13 -2
  16. package/dist/manager/modules/token/index.js.map +1 -1
  17. package/dist/manager/quota/provider-quota-center.d.ts +48 -0
  18. package/dist/manager/quota/provider-quota-center.js +239 -0
  19. package/dist/manager/quota/provider-quota-center.js.map +1 -0
  20. package/dist/manager/quota/provider-quota-store.d.ts +17 -0
  21. package/dist/manager/quota/provider-quota-store.js +88 -0
  22. package/dist/manager/quota/provider-quota-store.js.map +1 -0
  23. package/dist/providers/auth/token-scanner/index.js +11 -3
  24. package/dist/providers/auth/token-scanner/index.js.map +1 -1
  25. package/dist/providers/core/runtime/http-request-executor.js +24 -7
  26. package/dist/providers/core/runtime/http-request-executor.js.map +1 -1
  27. package/dist/providers/core/runtime/http-transport-provider.js +11 -3
  28. package/dist/providers/core/runtime/http-transport-provider.js.map +1 -1
  29. package/dist/providers/core/runtime/responses-provider.js +9 -3
  30. package/dist/providers/core/runtime/responses-provider.js.map +1 -1
  31. package/dist/providers/core/utils/http-client.d.ts +1 -0
  32. package/dist/providers/core/utils/http-client.js +139 -4
  33. package/dist/providers/core/utils/http-client.js.map +1 -1
  34. package/dist/providers/core/utils/snapshot-writer.d.ts +12 -0
  35. package/dist/providers/core/utils/snapshot-writer.js +99 -18
  36. package/dist/providers/core/utils/snapshot-writer.js.map +1 -1
  37. package/dist/providers/mock/mock-provider-runtime.d.ts +3 -0
  38. package/dist/providers/mock/mock-provider-runtime.js +176 -4
  39. package/dist/providers/mock/mock-provider-runtime.js.map +1 -1
  40. package/dist/server/handlers/chat-handler.js +13 -1
  41. package/dist/server/handlers/chat-handler.js.map +1 -1
  42. package/dist/server/handlers/handler-utils.js +5 -0
  43. package/dist/server/handlers/handler-utils.js.map +1 -1
  44. package/dist/server/handlers/messages-handler.js +13 -1
  45. package/dist/server/handlers/messages-handler.js.map +1 -1
  46. package/dist/server/handlers/responses-handler.js +73 -1
  47. package/dist/server/handlers/responses-handler.js.map +1 -1
  48. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js +174 -2
  49. package/dist/server/runtime/http-server/daemon-admin/credentials-handler.js.map +1 -1
  50. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js +519 -0
  51. package/dist/server/runtime/http-server/daemon-admin/providers-handler.js.map +1 -1
  52. package/dist/server/runtime/http-server/executor-response.js +6 -0
  53. package/dist/server/runtime/http-server/executor-response.js.map +1 -1
  54. package/dist/server/runtime/http-server/index.d.ts +5 -0
  55. package/dist/server/runtime/http-server/index.js +205 -4
  56. package/dist/server/runtime/http-server/index.js.map +1 -1
  57. package/dist/server/runtime/http-server/middleware.d.ts +2 -0
  58. package/dist/server/runtime/http-server/middleware.js +63 -0
  59. package/dist/server/runtime/http-server/middleware.js.map +1 -1
  60. package/dist/server/runtime/http-server/request-executor.d.ts +2 -0
  61. package/dist/server/runtime/http-server/request-executor.js +57 -10
  62. package/dist/server/runtime/http-server/request-executor.js.map +1 -1
  63. package/dist/server/runtime/http-server/routes.js +38 -1
  64. package/dist/server/runtime/http-server/routes.js.map +1 -1
  65. package/dist/server/runtime/http-server/stats-manager.d.ts +55 -0
  66. package/dist/server/runtime/http-server/stats-manager.js +462 -4
  67. package/dist/server/runtime/http-server/stats-manager.js.map +1 -1
  68. package/dist/server/runtime/http-server/types.d.ts +1 -0
  69. package/dist/token-daemon/token-daemon.js +70 -25
  70. package/dist/token-daemon/token-daemon.js.map +1 -1
  71. package/dist/token-daemon/token-utils.d.ts +1 -0
  72. package/dist/token-daemon/token-utils.js +9 -1
  73. package/dist/token-daemon/token-utils.js.map +1 -1
  74. package/dist/tools/semantic-replay.js +29 -0
  75. package/dist/tools/semantic-replay.js.map +1 -1
  76. package/dist/utils/snapshot-writer.d.ts +2 -0
  77. package/dist/utils/snapshot-writer.js +47 -4
  78. package/dist/utils/snapshot-writer.js.map +1 -1
  79. package/package.json +2 -3
  80. package/scripts/analyze-codex-error-failures.mjs +24 -14
  81. package/scripts/classify-codex-samples.mjs +0 -35
  82. package/scripts/copy-modules-config.mjs +17 -1
  83. package/scripts/generate-snapshot-data.mjs +41 -11
  84. package/scripts/mock-provider/extract.mjs +239 -21
  85. package/scripts/mock-provider/run-regressions.mjs +79 -16
  86. package/scripts/quota-dryrun.mjs +124 -0
  87. package/scripts/tests/apply-patch-loop.mjs +5 -1
  88. package/scripts/tests/exec-command-loop.mjs +16 -19
  89. package/scripts/verify-apply-patch.mjs +335 -5
  90. package/scripts/verify-e2e-toolcall.mjs +49 -10
  91. package/scripts/toon-suite.mjs +0 -141
@@ -0,0 +1,958 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <title>RouteCodex Daemon Admin</title>
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+ <style>
8
+ :root {
9
+ --bg: #070a14;
10
+ --panel: rgba(255, 255, 255, 0.05);
11
+ --panel-2: rgba(255, 255, 255, 0.03);
12
+ --border: rgba(255, 255, 255, 0.08);
13
+ --text: rgba(255, 255, 255, 0.92);
14
+ --muted: rgba(255, 255, 255, 0.62);
15
+ --accent: #4ea1ff;
16
+ --ok: #4cd964;
17
+ --warn: #ffb547;
18
+ --err: #ff5f5f;
19
+ --radius: 12px;
20
+ }
21
+
22
+ * {
23
+ box-sizing: border-box;
24
+ }
25
+
26
+ body {
27
+ margin: 0;
28
+ font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
29
+ sans-serif;
30
+ background: radial-gradient(circle at 25% 0, #131a36 0, var(--bg) 50%, #03040a 100%);
31
+ color: var(--text);
32
+ }
33
+
34
+ .container {
35
+ max-width: 1120px;
36
+ margin: 22px auto 40px;
37
+ padding: 0 16px;
38
+ }
39
+
40
+ .card {
41
+ border: 1px solid var(--border);
42
+ background: linear-gradient(180deg, var(--panel), var(--panel-2));
43
+ border-radius: var(--radius);
44
+ padding: 14px;
45
+ box-shadow: 0 12px 30px rgba(0, 0, 0, 0.35);
46
+ }
47
+
48
+ header {
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: space-between;
52
+ gap: 14px;
53
+ margin-bottom: 14px;
54
+ }
55
+
56
+ .title h1 {
57
+ margin: 0;
58
+ font-size: 16px;
59
+ font-weight: 650;
60
+ }
61
+
62
+ .title p {
63
+ margin: 3px 0 0;
64
+ font-size: 12px;
65
+ color: var(--muted);
66
+ }
67
+
68
+ .statusline {
69
+ display: flex;
70
+ align-items: center;
71
+ gap: 10px;
72
+ flex-wrap: wrap;
73
+ }
74
+
75
+ .pill {
76
+ display: inline-flex;
77
+ align-items: center;
78
+ gap: 8px;
79
+ border: 1px solid var(--border);
80
+ background: rgba(255, 255, 255, 0.03);
81
+ border-radius: 999px;
82
+ padding: 4px 10px;
83
+ font-size: 12px;
84
+ color: var(--muted);
85
+ }
86
+
87
+ .dot {
88
+ width: 8px;
89
+ height: 8px;
90
+ border-radius: 999px;
91
+ background: var(--warn);
92
+ }
93
+
94
+ .dot.ok {
95
+ background: var(--ok);
96
+ }
97
+
98
+ .dot.err {
99
+ background: var(--err);
100
+ }
101
+
102
+ .row {
103
+ display: flex;
104
+ gap: 10px;
105
+ flex-wrap: wrap;
106
+ align-items: center;
107
+ }
108
+
109
+ label {
110
+ font-size: 12px;
111
+ color: var(--muted);
112
+ }
113
+
114
+ input[type="text"],
115
+ input[type="password"],
116
+ select,
117
+ textarea {
118
+ border: 1px solid var(--border);
119
+ background: rgba(0, 0, 0, 0.25);
120
+ color: var(--text);
121
+ border-radius: 10px;
122
+ padding: 8px 10px;
123
+ font-size: 12px;
124
+ outline: none;
125
+ }
126
+
127
+ textarea {
128
+ width: 100%;
129
+ min-height: 220px;
130
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
131
+ "Courier New", monospace;
132
+ line-height: 1.45;
133
+ }
134
+
135
+ button {
136
+ border: 1px solid var(--border);
137
+ background: rgba(255, 255, 255, 0.04);
138
+ color: var(--text);
139
+ border-radius: 10px;
140
+ padding: 8px 10px;
141
+ font-size: 12px;
142
+ cursor: pointer;
143
+ }
144
+
145
+ button.primary {
146
+ border-color: rgba(78, 161, 255, 0.55);
147
+ background: rgba(78, 161, 255, 0.14);
148
+ }
149
+
150
+ button.danger {
151
+ border-color: rgba(255, 95, 95, 0.55);
152
+ background: rgba(255, 95, 95, 0.14);
153
+ }
154
+
155
+ .tabs {
156
+ display: flex;
157
+ gap: 6px;
158
+ margin: 14px 0 10px;
159
+ }
160
+
161
+ .tab {
162
+ padding: 8px 12px;
163
+ border-radius: 999px;
164
+ border: 1px solid var(--border);
165
+ background: rgba(255, 255, 255, 0.02);
166
+ color: var(--muted);
167
+ }
168
+
169
+ .tab.active {
170
+ color: var(--text);
171
+ border-color: rgba(78, 161, 255, 0.55);
172
+ background: rgba(78, 161, 255, 0.12);
173
+ }
174
+
175
+ .grid {
176
+ display: grid;
177
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
178
+ gap: 12px;
179
+ }
180
+
181
+ @media (max-width: 980px) {
182
+ .grid {
183
+ grid-template-columns: minmax(0, 1fr);
184
+ }
185
+ }
186
+
187
+ .section-title {
188
+ font-size: 13px;
189
+ font-weight: 650;
190
+ margin: 0 0 6px;
191
+ }
192
+
193
+ .section-sub {
194
+ margin: 0 0 10px;
195
+ font-size: 12px;
196
+ color: var(--muted);
197
+ }
198
+
199
+ .table {
200
+ width: 100%;
201
+ border-collapse: collapse;
202
+ border-radius: 12px;
203
+ overflow: hidden;
204
+ border: 1px solid var(--border);
205
+ }
206
+
207
+ .table th,
208
+ .table td {
209
+ padding: 8px 10px;
210
+ font-size: 12px;
211
+ border-bottom: 1px solid rgba(255, 255, 255, 0.06);
212
+ vertical-align: top;
213
+ }
214
+
215
+ .table th {
216
+ text-align: left;
217
+ color: var(--muted);
218
+ font-weight: 600;
219
+ background: rgba(255, 255, 255, 0.03);
220
+ }
221
+
222
+ .mono {
223
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
224
+ "Courier New", monospace;
225
+ color: var(--muted);
226
+ }
227
+
228
+ .notice {
229
+ border: 1px solid rgba(255, 181, 71, 0.35);
230
+ background: rgba(255, 181, 71, 0.08);
231
+ border-radius: 12px;
232
+ padding: 10px 12px;
233
+ font-size: 12px;
234
+ color: rgba(255, 255, 255, 0.82);
235
+ }
236
+
237
+ .log {
238
+ white-space: pre-wrap;
239
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono",
240
+ "Courier New", monospace;
241
+ font-size: 12px;
242
+ line-height: 1.45;
243
+ color: rgba(255, 255, 255, 0.84);
244
+ background: rgba(0, 0, 0, 0.25);
245
+ border: 1px solid rgba(255, 255, 255, 0.08);
246
+ border-radius: 12px;
247
+ padding: 10px 12px;
248
+ }
249
+ </style>
250
+ </head>
251
+ <body>
252
+ <div class="container">
253
+ <div class="card">
254
+ <header>
255
+ <div class="title">
256
+ <h1>RouteCodex Daemon Admin</h1>
257
+ <p>
258
+ Local-only UI. Writes to <span class="mono">~/.routecodex/config.json</span>; restart
259
+ required to apply routing/provider changes.
260
+ </p>
261
+ </div>
262
+ <div class="statusline">
263
+ <div class="pill"><span id="statusDot" class="dot"></span><span id="statusText">connecting…</span></div>
264
+ <div class="pill"><span class="mono" id="serverId">serverId: —</span></div>
265
+ </div>
266
+ </header>
267
+
268
+ <div class="row" style="margin-bottom: 10px;">
269
+ <label for="apiKeyInput">Server API Key (optional)</label>
270
+ <input
271
+ id="apiKeyInput"
272
+ type="password"
273
+ placeholder="x-api-key / Authorization: Bearer …"
274
+ style="flex: 1; min-width: 260px;"
275
+ />
276
+ <button id="saveApiKeyBtn" class="primary">Save</button>
277
+ <button id="clearApiKeyBtn">Clear</button>
278
+ <span class="mono" id="apiKeyHint"></span>
279
+ </div>
280
+
281
+ <div class="tabs">
282
+ <button class="tab active" data-tab="providers">Provider Pool</button>
283
+ <button class="tab" data-tab="credentials">Auth Provider Pool</button>
284
+ <button class="tab" data-tab="routing">Runtime Routing Pool</button>
285
+ </div>
286
+
287
+ <section id="panelProviders" data-panel="providers">
288
+ <div class="grid">
289
+ <div class="card" style="box-shadow: none;">
290
+ <p class="section-title">Providers in <span class="mono">virtualrouter.providers</span></p>
291
+ <p class="section-sub">
292
+ API keys are stored as authfiles and referenced with <span class="mono">authfile-…</span>. The UI never reads or returns secrets.
293
+ </p>
294
+ <div class="row" style="margin-bottom: 10px;">
295
+ <button id="refreshProvidersBtn" class="primary">Refresh</button>
296
+ <button id="newProviderBtn">New provider…</button>
297
+ </div>
298
+ <table class="table">
299
+ <thead>
300
+ <tr>
301
+ <th>id</th>
302
+ <th>type</th>
303
+ <th>enabled</th>
304
+ <th>baseURL</th>
305
+ <th>models</th>
306
+ <th>auth</th>
307
+ <th></th>
308
+ </tr>
309
+ </thead>
310
+ <tbody id="providersTbody"></tbody>
311
+ </table>
312
+ </div>
313
+
314
+ <div class="card" style="box-shadow: none;">
315
+ <p class="section-title" id="providerEditorTitle">Provider editor</p>
316
+ <p class="section-sub">
317
+ Edit provider JSON (secrets are not allowed inline). Save writes to disk and creates a backup.
318
+ </p>
319
+
320
+ <div class="notice" style="margin-bottom: 10px;">
321
+ If you use <span class="mono">httpserver.apikey</span>, set it above so API calls don’t return 401. The HTML page is always local-only.
322
+ </div>
323
+
324
+ <div class="row" style="margin-bottom: 10px;">
325
+ <label for="providerIdInput">provider id</label>
326
+ <input id="providerIdInput" type="text" placeholder="e.g. tab, glm, qwen" style="width: 240px;" />
327
+ <label for="providerPreset">preset</label>
328
+ <select id="providerPreset">
329
+ <option value="responses">responses (OpenAI /v1/responses)</option>
330
+ <option value="openai">openai (OpenAI /v1/chat/completions)</option>
331
+ <option value="openai-standard">openai-standard</option>
332
+ <option value="iflow">iflow (cookieFile)</option>
333
+ <option value="custom" selected>custom (start empty)</option>
334
+ </select>
335
+ </div>
336
+
337
+ <div class="row" style="margin-bottom: 10px;">
338
+ <label for="authMode">auth</label>
339
+ <select id="authMode">
340
+ <option value="apikey">apikey (store as authfile)</option>
341
+ <option value="oauth">oauth (alias-only + authorize in Auth tab)</option>
342
+ <option value="cookie">cookieFile</option>
343
+ <option value="none">none</option>
344
+ </select>
345
+ </div>
346
+
347
+ <div id="authApikeyBox" class="card" style="box-shadow:none; padding: 10px; margin-bottom: 10px;">
348
+ <p class="section-title" style="margin-bottom: 8px;">API key</p>
349
+ <div class="row" style="margin-bottom: 8px;">
350
+ <label for="apikeyAliasInput">alias</label>
351
+ <input id="apikeyAliasInput" type="text" placeholder="default" style="width: 220px;" />
352
+ <label for="apikeyValueInput">apiKey</label>
353
+ <input
354
+ id="apikeyValueInput"
355
+ type="password"
356
+ placeholder="will be written to ~/.routecodex/auth/*.key"
357
+ style="flex: 1; min-width: 260px;"
358
+ />
359
+ </div>
360
+ <div class="row">
361
+ <span class="mono" id="apikeySecretRefOut"></span>
362
+ <button id="createApiKeyCredentialBtn">Create authfile</button>
363
+ </div>
364
+ </div>
365
+
366
+ <div id="authOauthBox" class="card" style="box-shadow:none; padding: 10px; margin-bottom: 10px; display:none;">
367
+ <p class="section-title" style="margin-bottom: 8px;">OAuth</p>
368
+ <p class="section-sub" style="margin-bottom: 8px;">
369
+ Use Auth tab to authorize. Here we only reference <span class="mono">tokenFile</span> as an alias.
370
+ </p>
371
+ <div class="row">
372
+ <label for="oauthTypeInput">auth.type</label>
373
+ <input
374
+ id="oauthTypeInput"
375
+ type="text"
376
+ placeholder="qwen-oauth / iflow-oauth / gemini-cli-oauth / antigravity-oauth"
377
+ style="flex: 1; min-width: 260px;"
378
+ />
379
+ <label for="oauthAliasInput">tokenFile alias</label>
380
+ <input id="oauthAliasInput" type="text" placeholder="default" style="width: 240px;" />
381
+ </div>
382
+ </div>
383
+
384
+ <div id="authCookieBox" class="card" style="box-shadow:none; padding: 10px; margin-bottom: 10px; display:none;">
385
+ <p class="section-title" style="margin-bottom: 8px;">Cookie file</p>
386
+ <div class="row">
387
+ <label for="cookieFileInput">cookieFile</label>
388
+ <input
389
+ id="cookieFileInput"
390
+ type="text"
391
+ placeholder="~/.routecodex/auth/iflow-work.cookie"
392
+ style="flex: 1; min-width: 280px;"
393
+ />
394
+ </div>
395
+ </div>
396
+
397
+ <textarea
398
+ id="providerJsonEditor"
399
+ spellcheck="false"
400
+ placeholder="{\n \"enabled\": true,\n \"type\": \"responses\",\n \"baseURL\": \"https://...\",\n \"auth\": { \"type\": \"apikey\", \"apiKey\": \"authfile-...\" }\n}"
401
+ ></textarea>
402
+
403
+ <div class="row" style="margin-top: 10px;">
404
+ <button id="loadProviderBtn">Load</button>
405
+ <button id="applyPresetBtn">Apply preset</button>
406
+ <button id="saveProviderBtn" class="primary">Save</button>
407
+ <button id="deleteProviderBtn" class="danger">Delete</button>
408
+ </div>
409
+
410
+ <div id="providerOpLog" class="log" style="margin-top: 10px; display:none;"></div>
411
+ </div>
412
+ </div>
413
+ </section>
414
+
415
+ <section id="panelCredentials" data-panel="credentials" style="display:none;">
416
+ <div class="grid">
417
+ <div class="card" style="box-shadow:none;">
418
+ <p class="section-title">Credentials</p>
419
+ <p class="section-sub">
420
+ Token files + API key authfiles in <span class="mono">~/.routecodex/auth</span>.
421
+ </p>
422
+ <div class="row" style="margin-bottom: 10px;">
423
+ <button id="refreshCredentialsBtn" class="primary">Refresh</button>
424
+ </div>
425
+ <table class="table">
426
+ <thead>
427
+ <tr>
428
+ <th>kind</th>
429
+ <th>provider</th>
430
+ <th>alias</th>
431
+ <th>status</th>
432
+ <th>expires</th>
433
+ <th>secretRef</th>
434
+ </tr>
435
+ </thead>
436
+ <tbody id="credentialsTbody"></tbody>
437
+ </table>
438
+ </div>
439
+
440
+ <div class="card" style="box-shadow:none;">
441
+ <p class="section-title">OAuth / Browser settings</p>
442
+ <p class="section-sub">
443
+ Set <span class="mono">ROUTECODEX_OAUTH_BROWSER</span> via config so “Authorize” can auto-open with Camoufox.
444
+ </p>
445
+ <div class="row" style="margin-bottom: 10px;">
446
+ <label for="oauthBrowserSelect">oauthBrowser</label>
447
+ <select id="oauthBrowserSelect">
448
+ <option value="default">default</option>
449
+ <option value="camoufox">camoufox</option>
450
+ </select>
451
+ <button id="saveOauthBrowserBtn" class="primary">Save</button>
452
+ </div>
453
+
454
+ <p class="section-title" style="margin-top: 10px;">Authorize OAuth</p>
455
+ <div class="row" style="margin-bottom: 10px;">
456
+ <label for="oauthProviderSelect">provider</label>
457
+ <select id="oauthProviderSelect">
458
+ <option value="qwen">qwen</option>
459
+ <option value="iflow">iflow</option>
460
+ <option value="gemini-cli">gemini-cli</option>
461
+ <option value="antigravity">antigravity</option>
462
+ </select>
463
+ <label for="oauthAuthAliasInput">alias</label>
464
+ <input id="oauthAuthAliasInput" type="text" placeholder="default" style="width: 240px;" />
465
+ </div>
466
+ <div class="row" style="margin-bottom: 10px;">
467
+ <label><input id="oauthOpenBrowser" type="checkbox" checked /> open browser</label>
468
+ <label><input id="oauthForceReauth" type="checkbox" /> force reauthorize</label>
469
+ <button id="oauthAuthorizeBtn" class="primary">Authorize</button>
470
+ </div>
471
+
472
+ <div id="credentialOpLog" class="log" style="display:none;"></div>
473
+ </div>
474
+ </div>
475
+ </section>
476
+
477
+ <section id="panelRouting" data-panel="routing" style="display:none;">
478
+ <div class="grid">
479
+ <div class="card" style="box-shadow:none;">
480
+ <p class="section-title">Routing editor</p>
481
+ <p class="section-sub">Edits <span class="mono">virtualrouter.routing</span> in user config.</p>
482
+ <div class="row" style="margin-bottom: 10px;">
483
+ <button id="loadRoutingBtn" class="primary">Load</button>
484
+ <button id="saveRoutingBtn" class="primary">Save</button>
485
+ </div>
486
+ <textarea id="routingEditor" spellcheck="false" placeholder="{\n \"default\": [...]\n}"></textarea>
487
+ <div id="routingOpLog" class="log" style="margin-top: 10px; display:none;"></div>
488
+ </div>
489
+
490
+ <div class="card" style="box-shadow:none;">
491
+ <p class="section-title">Runtime providers</p>
492
+ <p class="section-sub">
493
+ What the running process currently has loaded (restart required after edits).
494
+ </p>
495
+ <div class="row" style="margin-bottom: 10px;">
496
+ <button id="refreshRuntimesBtn" class="primary">Refresh</button>
497
+ </div>
498
+ <table class="table">
499
+ <thead>
500
+ <tr>
501
+ <th>providerKey</th>
502
+ <th>runtimeKey</th>
503
+ <th>family</th>
504
+ <th>protocol</th>
505
+ <th>series</th>
506
+ </tr>
507
+ </thead>
508
+ <tbody id="runtimesTbody"></tbody>
509
+ </table>
510
+ </div>
511
+ </div>
512
+ </section>
513
+ </div>
514
+ </div>
515
+
516
+ <script>
517
+ const $ = (id) => document.getElementById(id);
518
+
519
+ function setLog(id, value) {
520
+ const el = $(id);
521
+ if (!el) return;
522
+ el.style.display = value ? "block" : "none";
523
+ el.textContent = value || "";
524
+ }
525
+
526
+ function getApiKey() {
527
+ try {
528
+ return sessionStorage.getItem("routecodex:apikey") || "";
529
+ } catch {
530
+ return "";
531
+ }
532
+ }
533
+
534
+ function setApiKey(value) {
535
+ try {
536
+ if (!value) sessionStorage.removeItem("routecodex:apikey");
537
+ else sessionStorage.setItem("routecodex:apikey", value);
538
+ } catch {}
539
+ }
540
+
541
+ async function apiFetch(path, opts = {}) {
542
+ const headers = new Headers(opts.headers || {});
543
+ const apiKey = getApiKey();
544
+ if (apiKey) headers.set("x-api-key", apiKey);
545
+ if (!headers.has("content-type") && opts.body) headers.set("content-type", "application/json");
546
+ const res = await fetch(path, { ...opts, headers });
547
+ const text = await res.text();
548
+ let json = null;
549
+ try {
550
+ json = text ? JSON.parse(text) : null;
551
+ } catch {
552
+ json = null;
553
+ }
554
+ if (!res.ok) {
555
+ const msg =
556
+ (json && json.error && (json.error.message || json.error.code)) ||
557
+ `HTTP ${res.status} ${res.statusText}`;
558
+ const err = new Error(msg);
559
+ err.status = res.status;
560
+ err.payload = json;
561
+ throw err;
562
+ }
563
+ return json;
564
+ }
565
+
566
+ function selectTab(name) {
567
+ document.querySelectorAll(".tab").forEach((btn) => {
568
+ btn.classList.toggle("active", btn.getAttribute("data-tab") === name);
569
+ });
570
+ const panels = [
571
+ { name: "providers", el: $("panelProviders") },
572
+ { name: "credentials", el: $("panelCredentials") },
573
+ { name: "routing", el: $("panelRouting") }
574
+ ];
575
+ for (const p of panels) p.el.style.display = p.name === name ? "block" : "none";
576
+ }
577
+
578
+ function presetFor(type) {
579
+ if (type === "responses") {
580
+ return {
581
+ enabled: true,
582
+ type: "responses",
583
+ baseURL: "https://api.openai.com/v1",
584
+ auth: { type: "apikey", apiKey: "authfile-REPLACE_ME" },
585
+ responses: { process: "chat", streaming: "always" },
586
+ config: { responses: { process: "chat", streaming: "always" } },
587
+ models: {}
588
+ };
589
+ }
590
+ if (type === "openai") {
591
+ return {
592
+ enabled: true,
593
+ type: "openai",
594
+ baseURL: "https://api.openai.com/v1",
595
+ auth: { type: "apikey", apiKey: "authfile-REPLACE_ME" },
596
+ models: {}
597
+ };
598
+ }
599
+ if (type === "openai-standard") {
600
+ return {
601
+ enabled: true,
602
+ type: "openai-standard",
603
+ baseURL: "https://api.openai.com/v1",
604
+ auth: { type: "apikey", apiKey: "authfile-REPLACE_ME" },
605
+ models: {}
606
+ };
607
+ }
608
+ if (type === "iflow") {
609
+ return {
610
+ enabled: true,
611
+ type: "iflow",
612
+ baseURL: "https://apis.iflow.cn/v1",
613
+ compatibilityProfile: "chat:iflow",
614
+ auth: { type: "iflow-cookie", cookieFile: "~/.routecodex/auth/iflow-work.cookie" },
615
+ models: {}
616
+ };
617
+ }
618
+ return {
619
+ enabled: true,
620
+ type: "responses",
621
+ baseURL: "",
622
+ auth: { type: "apikey", apiKey: "authfile-REPLACE_ME" },
623
+ models: {}
624
+ };
625
+ }
626
+
627
+ async function refreshStatus() {
628
+ try {
629
+ const data = await apiFetch("/daemon/status");
630
+ $("serverId").textContent = `serverId: ${data.serverId || "—"}`;
631
+ $("statusText").textContent = "connected";
632
+ $("statusDot").classList.add("ok");
633
+ $("statusDot").classList.remove("err");
634
+ } catch (e) {
635
+ $("statusText").textContent = e && e.status === 401 ? "401 (set API key above)" : "disconnected";
636
+ $("statusDot").classList.remove("ok");
637
+ $("statusDot").classList.add("err");
638
+ }
639
+ }
640
+
641
+ async function refreshProviders() {
642
+ const body = $("providersTbody");
643
+ body.innerHTML = "";
644
+ try {
645
+ const data = await apiFetch("/config/providers");
646
+ for (const p of data.providers || []) {
647
+ const tr = document.createElement("tr");
648
+ tr.innerHTML = `
649
+ <td class="mono">${p.id || ""}</td>
650
+ <td>${p.type || ""}</td>
651
+ <td>${String(p.enabled)}</td>
652
+ <td class="mono">${p.baseURL || ""}</td>
653
+ <td>${p.modelCount || 0}</td>
654
+ <td>${p.authType || ""}</td>
655
+ <td>
656
+ <button data-action="edit" data-id="${p.id}">Edit</button>
657
+ <button data-action="delete" data-id="${p.id}" class="danger">Delete</button>
658
+ </td>
659
+ `;
660
+ body.appendChild(tr);
661
+ }
662
+ } catch (e) {
663
+ const tr = document.createElement("tr");
664
+ tr.innerHTML = `<td colspan="7" class="mono">Failed to load: ${e.message}</td>`;
665
+ body.appendChild(tr);
666
+ }
667
+ }
668
+
669
+ async function loadProvider(id) {
670
+ setLog("providerOpLog", "");
671
+ try {
672
+ const data = await apiFetch(`/config/providers/${encodeURIComponent(id)}`);
673
+ $("providerIdInput").value = id;
674
+ $("providerJsonEditor").value = JSON.stringify(data.provider || {}, null, 2);
675
+ $("providerEditorTitle").textContent = `Provider editor: ${id}`;
676
+ } catch (e) {
677
+ setLog("providerOpLog", `Load failed: ${e.message}`);
678
+ }
679
+ }
680
+
681
+ async function saveProvider() {
682
+ setLog("providerOpLog", "");
683
+ const id = ($("providerIdInput").value || "").trim();
684
+ if (!id) {
685
+ setLog("providerOpLog", "provider id is required");
686
+ return;
687
+ }
688
+ try {
689
+ const provider = JSON.parse($("providerJsonEditor").value || "{}");
690
+ const result = await apiFetch(`/config/providers/${encodeURIComponent(id)}`, {
691
+ method: "PUT",
692
+ body: JSON.stringify({ provider })
693
+ });
694
+ setLog("providerOpLog", `Saved. Path: ${result.path || "—"}\nRestart required to apply.`);
695
+ await refreshProviders();
696
+ } catch (e) {
697
+ setLog("providerOpLog", `Save failed: ${e.message}`);
698
+ }
699
+ }
700
+
701
+ async function deleteProvider(id) {
702
+ setLog("providerOpLog", "");
703
+ if (!id) {
704
+ setLog("providerOpLog", "provider id is required");
705
+ return;
706
+ }
707
+ if (!confirm(`Delete provider "${id}" from user config?`)) return;
708
+ try {
709
+ const result = await apiFetch(`/config/providers/${encodeURIComponent(id)}`, { method: "DELETE" });
710
+ setLog("providerOpLog", `Deleted. Path: ${result.path || "—"}\nRestart required to apply.`);
711
+ await refreshProviders();
712
+ } catch (e) {
713
+ setLog("providerOpLog", `Delete failed: ${e.message}`);
714
+ }
715
+ }
716
+
717
+ async function createApiKeyCredential() {
718
+ setLog("providerOpLog", "");
719
+ $("apikeySecretRefOut").textContent = "";
720
+ const provider = ($("providerIdInput").value || "").trim();
721
+ const alias = ($("apikeyAliasInput").value || "default").trim() || "default";
722
+ const apiKey = ($("apikeyValueInput").value || "").trim();
723
+ if (!provider) {
724
+ setLog("providerOpLog", "provider id is required before creating an authfile");
725
+ return null;
726
+ }
727
+ if (!apiKey) {
728
+ setLog("providerOpLog", "apiKey is required");
729
+ return null;
730
+ }
731
+ try {
732
+ const out = await apiFetch("/daemon/credentials/apikey", {
733
+ method: "POST",
734
+ body: JSON.stringify({ provider, alias, apiKey })
735
+ });
736
+ $("apikeySecretRefOut").textContent = `secretRef: ${out.secretRef}`;
737
+ return out.secretRef;
738
+ } catch (e) {
739
+ setLog("providerOpLog", `Create authfile failed: ${e.message}`);
740
+ return null;
741
+ }
742
+ }
743
+
744
+ function applyPresetToEditor() {
745
+ const preset = $("providerPreset").value;
746
+ const base = presetFor(preset);
747
+ const authMode = $("authMode").value;
748
+ if (authMode === "none") {
749
+ delete base.auth;
750
+ }
751
+ if (authMode === "cookie") {
752
+ base.auth = {
753
+ type: "iflow-cookie",
754
+ cookieFile: ($("cookieFileInput").value || "").trim() || "~/.routecodex/auth/iflow.cookie"
755
+ };
756
+ }
757
+ if (authMode === "oauth") {
758
+ const t = ($("oauthTypeInput").value || "").trim() || "qwen-oauth";
759
+ const alias = ($("oauthAliasInput").value || "default").trim() || "default";
760
+ base.auth = { type: t, tokenFile: alias };
761
+ }
762
+ if (authMode === "apikey") {
763
+ const secretRef = ($("apikeySecretRefOut").textContent || "").replace(/^secretRef:\\s*/i, "").trim();
764
+ base.auth = { type: "apikey", apiKey: secretRef || "authfile-REPLACE_ME" };
765
+ }
766
+ $("providerJsonEditor").value = JSON.stringify(base, null, 2);
767
+ }
768
+
769
+ function updateAuthModeUi() {
770
+ const mode = $("authMode").value;
771
+ $("authApikeyBox").style.display = mode === "apikey" ? "block" : "none";
772
+ $("authOauthBox").style.display = mode === "oauth" ? "block" : "none";
773
+ $("authCookieBox").style.display = mode === "cookie" ? "block" : "none";
774
+ }
775
+
776
+ async function refreshCredentials() {
777
+ const body = $("credentialsTbody");
778
+ body.innerHTML = "";
779
+ try {
780
+ const items = await apiFetch("/daemon/credentials");
781
+ for (const c of items || []) {
782
+ const tr = document.createElement("tr");
783
+ const exp = c.expiresInSec == null ? "—" : `${c.expiresInSec}s`;
784
+ tr.innerHTML = `
785
+ <td>${c.kind}</td>
786
+ <td class="mono">${c.provider}</td>
787
+ <td class="mono">${c.alias}</td>
788
+ <td>${c.status}</td>
789
+ <td class="mono">${exp}</td>
790
+ <td class="mono">${c.secretRef || "—"}</td>
791
+ `;
792
+ body.appendChild(tr);
793
+ }
794
+ } catch (e) {
795
+ const tr = document.createElement("tr");
796
+ tr.innerHTML = `<td colspan="6" class="mono">Failed to load: ${e.message}</td>`;
797
+ body.appendChild(tr);
798
+ }
799
+ }
800
+
801
+ async function loadSettings() {
802
+ try {
803
+ const data = await apiFetch("/config/settings");
804
+ const v = (data.oauthBrowser || "default").toLowerCase();
805
+ $("oauthBrowserSelect").value = v === "camoufox" ? "camoufox" : "default";
806
+ } catch {}
807
+ }
808
+
809
+ async function saveSettings() {
810
+ setLog("credentialOpLog", "");
811
+ const oauthBrowser = $("oauthBrowserSelect").value;
812
+ try {
813
+ const out = await apiFetch("/config/settings", {
814
+ method: "PUT",
815
+ body: JSON.stringify({ oauthBrowser })
816
+ });
817
+ setLog("credentialOpLog", `Saved oauthBrowser=${out.oauthBrowser}.`);
818
+ } catch (e) {
819
+ setLog("credentialOpLog", `Save failed: ${e.message}`);
820
+ }
821
+ }
822
+
823
+ async function authorizeOauth() {
824
+ setLog("credentialOpLog", "");
825
+ const provider = $("oauthProviderSelect").value;
826
+ const alias = ($("oauthAuthAliasInput").value || "default").trim() || "default";
827
+ const openBrowser = $("oauthOpenBrowser").checked;
828
+ const forceReauthorize = $("oauthForceReauth").checked;
829
+ try {
830
+ const out = await apiFetch("/daemon/oauth/authorize", {
831
+ method: "POST",
832
+ body: JSON.stringify({ provider, alias, openBrowser, forceReauthorize })
833
+ });
834
+ setLog("credentialOpLog", `OK. tokenFile: ${out.tokenFile || "—"}`);
835
+ await refreshCredentials();
836
+ } catch (e) {
837
+ setLog("credentialOpLog", `Authorize failed: ${e.message}`);
838
+ }
839
+ }
840
+
841
+ async function loadRouting() {
842
+ setLog("routingOpLog", "");
843
+ try {
844
+ const out = await apiFetch("/config/routing");
845
+ $("routingEditor").value = JSON.stringify(out.routing || {}, null, 2);
846
+ setLog("routingOpLog", `Loaded. Path: ${out.path || "—"}`);
847
+ } catch (e) {
848
+ setLog("routingOpLog", `Load failed: ${e.message}`);
849
+ }
850
+ }
851
+
852
+ async function saveRouting() {
853
+ setLog("routingOpLog", "");
854
+ try {
855
+ const routing = JSON.parse($("routingEditor").value || "{}");
856
+ const out = await apiFetch("/config/routing", {
857
+ method: "PUT",
858
+ body: JSON.stringify({ routing })
859
+ });
860
+ setLog("routingOpLog", `Saved. Path: ${out.path || "—"}\nRestart required to apply.`);
861
+ } catch (e) {
862
+ setLog("routingOpLog", `Save failed: ${e.message}`);
863
+ }
864
+ }
865
+
866
+ async function refreshRuntimes() {
867
+ const body = $("runtimesTbody");
868
+ body.innerHTML = "";
869
+ try {
870
+ const items = await apiFetch("/providers/runtimes");
871
+ for (const r of items || []) {
872
+ const tr = document.createElement("tr");
873
+ tr.innerHTML = `
874
+ <td class="mono">${r.providerKey || ""}</td>
875
+ <td class="mono">${r.runtimeKey || ""}</td>
876
+ <td>${r.family || ""}</td>
877
+ <td>${r.protocol || ""}</td>
878
+ <td>${r.series || ""}</td>
879
+ `;
880
+ body.appendChild(tr);
881
+ }
882
+ } catch (e) {
883
+ const tr = document.createElement("tr");
884
+ tr.innerHTML = `<td colspan="5" class="mono">Failed to load: ${e.message}</td>`;
885
+ body.appendChild(tr);
886
+ }
887
+ }
888
+
889
+ // Bind events
890
+ document.querySelectorAll(".tab").forEach((btn) => {
891
+ btn.addEventListener("click", () => selectTab(btn.getAttribute("data-tab")));
892
+ });
893
+
894
+ $("saveApiKeyBtn").addEventListener("click", () => {
895
+ const value = ($("apiKeyInput").value || "").trim();
896
+ setApiKey(value);
897
+ $("apiKeyHint").textContent = value ? "saved (session only)" : "";
898
+ });
899
+ $("clearApiKeyBtn").addEventListener("click", () => {
900
+ setApiKey("");
901
+ $("apiKeyInput").value = "";
902
+ $("apiKeyHint").textContent = "";
903
+ });
904
+
905
+ $("refreshProvidersBtn").addEventListener("click", refreshProviders);
906
+ $("newProviderBtn").addEventListener("click", () => {
907
+ $("providerEditorTitle").textContent = "Provider editor (new)";
908
+ $("providerIdInput").value = "";
909
+ $("providerJsonEditor").value = JSON.stringify(presetFor($("providerPreset").value), null, 2);
910
+ setLog("providerOpLog", "");
911
+ });
912
+ $("providersTbody").addEventListener("click", async (ev) => {
913
+ const btn = ev.target.closest("button");
914
+ if (!btn) return;
915
+ const id = btn.getAttribute("data-id");
916
+ const action = btn.getAttribute("data-action");
917
+ if (action === "edit") await loadProvider(id);
918
+ if (action === "delete") await deleteProvider(id);
919
+ });
920
+ $("loadProviderBtn").addEventListener("click", async () => {
921
+ const id = ($("providerIdInput").value || "").trim();
922
+ if (id) await loadProvider(id);
923
+ });
924
+ $("applyPresetBtn").addEventListener("click", applyPresetToEditor);
925
+ $("saveProviderBtn").addEventListener("click", saveProvider);
926
+ $("deleteProviderBtn").addEventListener("click", async () => {
927
+ const id = ($("providerIdInput").value || "").trim();
928
+ await deleteProvider(id);
929
+ });
930
+ $("createApiKeyCredentialBtn").addEventListener("click", createApiKeyCredential);
931
+
932
+ $("authMode").addEventListener("change", updateAuthModeUi);
933
+ updateAuthModeUi();
934
+
935
+ $("refreshCredentialsBtn").addEventListener("click", refreshCredentials);
936
+ $("saveOauthBrowserBtn").addEventListener("click", saveSettings);
937
+ $("oauthAuthorizeBtn").addEventListener("click", authorizeOauth);
938
+
939
+ $("loadRoutingBtn").addEventListener("click", loadRouting);
940
+ $("saveRoutingBtn").addEventListener("click", saveRouting);
941
+ $("refreshRuntimesBtn").addEventListener("click", refreshRuntimes);
942
+
943
+ // Init
944
+ (async () => {
945
+ const savedKey = getApiKey();
946
+ if (savedKey) {
947
+ $("apiKeyHint").textContent = "saved (session only)";
948
+ $("apiKeyInput").value = savedKey;
949
+ }
950
+ await refreshStatus();
951
+ await refreshProviders();
952
+ await refreshCredentials();
953
+ await loadSettings();
954
+ })();
955
+ </script>
956
+ </body>
957
+ </html>
958
+