@jeffusion/bungee-core 4.4.2 → 4.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -189,12 +189,13 @@
189
189
  "X-Codex-Window-Id",
190
190
  "Thread-Id",
191
191
  "Session-Id",
192
+ "X-Codex-Routing-Hint",
192
193
  "X-OpenAI-Internal-Codex-Responses-Lite"
193
194
  ],
194
195
  "set": {
195
196
  "Accept": "text/event-stream",
196
197
  "Content-Type": "application/json",
197
- "User-Agent": "codex-tui/0.153.3 (Mac OS 26.5.1; arm64) iTerm.app/3.6.11 (codex-tui; 0.153.3)",
198
+ "User-Agent": "codex-tui/0.154.0 (Mac OS 26.5.2; arm64) iTerm.app/3.6.11 (codex-tui; 0.154.0)",
198
199
  "Originator": "codex-tui"
199
200
  }
200
201
  }
@@ -266,6 +267,31 @@
266
267
  "ui.accountType": "Account type: {plan}",
267
268
  "ui.credentialExpiry": "Credentials expire: {date}",
268
269
  "ui.useService": "Use with service",
270
+ "ui.useRoute": "Use with route",
271
+ "ui.newRoute": "New route",
272
+ "ui.chooseRoute": "Choose route",
273
+ "ui.routesLoading": "Loading routes…",
274
+ "ui.routesFailed": "Unable to load routes. Try again.",
275
+ "ui.routeMissing": "The selected route no longer exists. Reload the list.",
276
+ "ui.routeDescription": "Add “{label}” to a route draft. No configuration is published now.",
277
+ "ui.routeHelp": "For a new or custom-endpoint route, this account is added to the unsaved target. An existing binding is reused. Review and explicitly save the route to publish.",
278
+ "ui.routeUsesService": "This route uses service “{name}”. Edit that service to add this account; its target will not be switched silently.",
279
+ "ui.editReferencedService": "Edit referenced service",
280
+ "ui.reloadRoutes": "Reload routes",
281
+ "ui.continueRoute": "Continue to route editor",
282
+ "ui.routeDraftAdded": "Account added to the unsaved route target. Check it, then explicitly save the route to publish.",
283
+ "ui.routeDraftExisting": "This account is already used by this route. The existing target is selected; no duplicate was added. Save only if you change the route.",
284
+ "ui.routeDraftError": "The account could not be added. Your current route draft is unchanged. Return to Accounts and try again.",
285
+ "ui.routeHandoffInvalid": "The account handoff link is invalid. Return to Accounts and choose the route again.",
286
+ "ui.routeHandoffChanged": "This route changed while adding the account. Your draft is unchanged; return to Accounts and choose it again.",
287
+ "ui.routeHandoffUnsupported": "This route cannot accept a custom target. Edit the referenced service or change the route target yourself.",
288
+ "ui.routeHandoffSourceUnavailable": "The account source is unavailable. Check that its plugin is enabled, then try again.",
289
+ "ui.routeHandoffAccountUnavailable": "This account is no longer available. Refresh Accounts and choose an available account.",
290
+ "ui.routeSaveError": "The route could not be saved. Your draft remains here; try again.",
291
+ "ui.routeStale": "This route changed elsewhere. No changes were published. Your draft remains here; reload the latest route before saving again.",
292
+ "ui.routeReload": "Reload route",
293
+ "ui.routeReloadTitle": "Reload latest route?",
294
+ "ui.routeReloadConfirm": "This will discard your unsaved route draft. Continue?",
269
295
  "ui.usageLabel": "Usage",
270
296
  "ui.refreshUsage": "Refresh quota usage",
271
297
  "ui.resolveReset": "Resolve",
@@ -434,6 +460,31 @@
434
460
  "ui.accountType": "账号类型:{plan}",
435
461
  "ui.credentialExpiry": "凭据到期:{date}",
436
462
  "ui.useService": "用于服务",
463
+ "ui.useRoute": "用于路由",
464
+ "ui.newRoute": "新建路由",
465
+ "ui.chooseRoute": "选择路由",
466
+ "ui.routesLoading": "正在加载路由…",
467
+ "ui.routesFailed": "路由加载失败,请重试。",
468
+ "ui.routeMissing": "所选路由已不存在,请刷新列表。",
469
+ "ui.routeDescription": "将「{label}」加入路由草稿,当前不会发布配置。",
470
+ "ui.routeHelp": "新建或自定义端点路由会加入未保存的目标;已绑定同一账号时定位已有端点。检查后需手动保存路由才会发布。",
471
+ "ui.routeUsesService": "这条路由引用服务「{name}」。请到服务编辑器添加账号,不会静默切换路由目标。",
472
+ "ui.editReferencedService": "编辑引用的服务",
473
+ "ui.reloadRoutes": "重新加载路由",
474
+ "ui.continueRoute": "继续到路由编辑器",
475
+ "ui.routeDraftAdded": "账号已加入未保存的路由目标。检查后需明确保存路由才会发布。",
476
+ "ui.routeDraftExisting": "此路由已使用该账号,已定位现有目标,不会重复添加。仅在修改路由后才需保存。",
477
+ "ui.routeDraftError": "无法加入账号,当前路由草稿未改变。请返回账号页面重试。",
478
+ "ui.routeHandoffInvalid": "账号交接链接无效,请返回账号页面重新选择路由。",
479
+ "ui.routeHandoffChanged": "添加账号期间路由已发生变化,草稿未修改;请返回账号页面重新选择。",
480
+ "ui.routeHandoffUnsupported": "此路由不能添加自定义目标。请编辑其引用的服务,或自行调整路由目标。",
481
+ "ui.routeHandoffSourceUnavailable": "账号来源不可用,请确认插件已启用后重试。",
482
+ "ui.routeHandoffAccountUnavailable": "此账号目前不可用,请刷新账号页面后选择可用账号。",
483
+ "ui.routeSaveError": "路由保存失败,当前草稿仍保留,请重试。",
484
+ "ui.routeStale": "路由已被其他操作修改,未发布任何更改。当前草稿仍保留;需重新加载最新路由后才能再次保存。",
485
+ "ui.routeReload": "重新加载路由",
486
+ "ui.routeReloadTitle": "重新加载最新路由?",
487
+ "ui.routeReloadConfirm": "这会丢弃当前未保存的路由草稿,是否继续?",
437
488
  "ui.usageLabel": "用量",
438
489
  "ui.refreshUsage": "刷新额度用量",
439
490
  "ui.resolveReset": "确认结果",
@@ -1,15 +1,17 @@
1
1
  <script lang="ts">
2
- import { onMount } from 'svelte';
2
+ import { onMount, tick } from 'svelte';
3
3
  import { push } from 'svelte-spa-router';
4
4
  import { requestPluginControl } from '$api/client';
5
5
  import { getConfigSnapshot } from '$api/config';
6
6
  import { ServicesAPI, type Service } from '$api/services';
7
7
  import { accountReferences } from '$api/upstream-sources';
8
- import { sourceHandoffUrl } from '$api/source-handoff';
8
+ import { sourceHandoffUrl, routeSourceHandoffUrl } from '$api/source-handoff';
9
+ import { RoutesAPI, type Route } from '$api/routes';
9
10
  import { PanelCard, MetricBar, StatusBadge, LoadingIndicator, IndustrialDialog } from '$components/industrial';
10
11
  import RefreshCw from 'lucide-svelte/icons/refresh-cw';
11
12
  import Plus from 'lucide-svelte/icons/plus';
12
13
  import Server from 'lucide-svelte/icons/server';
14
+ import RouteIcon from 'lucide-svelte/icons/route';
13
15
  import Ellipsis from 'lucide-svelte/icons/ellipsis';
14
16
  import Pencil from 'lucide-svelte/icons/pencil';
15
17
  import LogIn from 'lucide-svelte/icons/log-in';
@@ -70,6 +72,11 @@
70
72
  let useOpen = $state(false), useAccount = $state<Account | null>(null), services = $state<Service[]>([]);
71
73
  let serviceChoice = $state('new'), servicesLoading = $state(false), serviceError = $state('');
72
74
  let selectedService = $derived($isLoading ? undefined : { value: serviceChoice, label: serviceChoice === 'new' ? t('ui.newService') : services.find(service => service._uid === serviceChoice)?.name ?? serviceChoice });
75
+ let routeOpen = $state(false), routeAccount = $state<Account | null>(null), routes = $state<Route[]>([]);
76
+ let routeChoice = $state('new'), routesLoading = $state(false), routeError = $state('');
77
+ let routeGeneration = 0;
78
+ let selectedRoute = $derived(routes.find(route => route.path === routeChoice));
79
+ let routeSelection = $derived($isLoading ? undefined : { value: routeChoice, label: routeChoice === 'new' ? t('ui.newRoute') : selectedRoute?.path ?? routeChoice });
73
80
  let resetOpen = $state(false), resetAccount = $state<Account | null>(null), resetCredit = $state<Credit | null>(null);
74
81
  let resetRequestId = $state(''), resetBusy = $state(false), resetNotice = $state('');
75
82
  let pendingResetByAccount = $state<Record<string, PendingReset>>({});
@@ -362,17 +369,50 @@
362
369
  }
363
370
  function openUse(account: Account) {
364
371
  if (!account.available) return;
372
+ routeOpen = false;
365
373
  useAccount = account; serviceChoice = 'new'; services = []; useOpen = true; void loadServices();
366
374
  }
367
- function continueToService() {
375
+ async function continueToService() {
368
376
  if (!useAccount?.available || servicesLoading || serviceError) return;
369
377
  const existing = services.find(service => service._uid === serviceChoice);
370
378
  if (serviceChoice !== 'new' && !existing) { serviceError = 'ui.serviceMissing'; return; }
371
379
  const handoff = { sourcePlugin: pluginName, sourceId: 'chatgpt', accountRef: useAccount.id,
372
380
  mode: existing ? 'existing' as const : 'new' as const, ...(existing ? { serviceId: existing._uid } : {}) };
373
381
  useOpen = false; loginOpen = false; clearSecrets();
382
+ await tick(); // Flush the dialog close before navigation unmounts the account page.
383
+ if (lifetime.signal.aborted) return;
374
384
  void push(sourceHandoffUrl(handoff, existing?.name));
375
385
  }
386
+ async function loadRoutes() {
387
+ const generation = ++routeGeneration;
388
+ routesLoading = true; routeError = '';
389
+ try { const loaded = await RoutesAPI.list(); if (generation === routeGeneration) routes = loaded; }
390
+ catch { if (generation === routeGeneration) routeError = 'ui.routesFailed'; }
391
+ finally { if (generation === routeGeneration) routesLoading = false; }
392
+ }
393
+ function openRoute(account: Account) {
394
+ if (!account.available) return;
395
+ useOpen = false;
396
+ routeAccount = account; routeChoice = 'new'; routes = []; routeOpen = true; void loadRoutes();
397
+ }
398
+ async function continueToRoute() {
399
+ if (!routeAccount?.available || routesLoading || routeError || selectedRoute?.service) return;
400
+ if (routeChoice !== 'new' && !selectedRoute?._uid) { routeError = 'ui.routeMissing'; return; }
401
+ const handoff = { sourcePlugin: pluginName, sourceId: 'chatgpt', accountRef: routeAccount.id,
402
+ mode: selectedRoute ? 'existing' as const : 'new' as const,
403
+ ...(selectedRoute ? { routeId: selectedRoute._uid } : {}) };
404
+ routeOpen = false; loginOpen = false; clearSecrets();
405
+ await tick();
406
+ if (lifetime.signal.aborted) return;
407
+ void push(routeSourceHandoffUrl(handoff, selectedRoute?.path));
408
+ }
409
+ async function editReferencedService() {
410
+ const service = selectedRoute?.service;
411
+ if (!service) return;
412
+ routeOpen = false;
413
+ await tick();
414
+ if (!lifetime.signal.aborted) void push(`/services/edit/${encodeURIComponent(service)}`);
415
+ }
376
416
  function invalidatePolling() { pollGeneration += 1; clearTimeout(timer); timer = undefined; }
377
417
  function clearSession() { invalidatePolling(); clearSecrets(); session = null; status = ''; }
378
418
  onMount(() => { usageTimer = setInterval(() => { clock = Date.now(); }, 30000); void refreshAccounts(); return () => { lifetime.abort(); clearSession(); if (usageTimer) clearInterval(usageTimer); }; });
@@ -440,6 +480,7 @@
440
480
  {:else}<p class="text-sm text-zinc-400">{t(snapshot.error ? 'ui.usageFailed' : 'ui.usageSkipped')}</p>{/if}
441
481
  <div class="flex flex-wrap items-center justify-end gap-2" data-testid="account-actions">
442
482
  <Button variant="secondary" size="sm" disabled={!account.available} onclick={() => openUse(account)}>{@render actionIcon(Server)}{t('ui.useService')}</Button>
483
+ <Button variant="secondary" size="sm" disabled={!account.available} onclick={() => openRoute(account)}>{@render actionIcon(RouteIcon)}{t('ui.useRoute')}</Button>
443
484
  <DropdownMenu.Root>
444
485
  <DropdownMenu.Trigger asChild let:builder><Button variant="ghost" size="sm" class="aspect-square px-0 [&>span]:mr-0" builders={[builder]} disabled={actionBusy} aria-label={`${t('ui.more')}: ${account.label}`} title={t('ui.more')}>{@render actionIcon(Ellipsis)}</Button></DropdownMenu.Trigger>
445
486
  <DropdownMenu.Content class="z-[200]" align="end">
@@ -520,6 +561,28 @@
520
561
  {/snippet}
521
562
  </IndustrialDialog>
522
563
 
564
+ <IndustrialDialog bind:open={routeOpen} closeLabel={t('ui.close')} title={t('ui.useRoute')} description={t('ui.routeDescription', { label: routeAccount?.label ?? '' })}>
565
+ {#snippet body()}
566
+ <div class="space-y-1.5"><span class="nx-field-label">{t('ui.chooseRoute')}</span>
567
+ <Select.Root selected={routeSelection} onSelectedChange={(next) => routeChoice = next?.value ?? 'new'}>
568
+ <Select.Trigger class="w-full" aria-label={t('ui.chooseRoute')} disabled={routesLoading}><Select.Value placeholder={t('ui.chooseRoute')} /></Select.Trigger>
569
+ <Select.Content><Select.Item value="new" label={t('ui.newRoute')}>{t('ui.newRoute')}</Select.Item>{#each routes.filter(route => route._uid) as route (route._uid)}<Select.Item value={route.path} label={route.path}>{route.path}</Select.Item>{/each}</Select.Content>
570
+ </Select.Root>
571
+ </div>
572
+ {#if routesLoading}<p role="status" class="text-sm text-zinc-400">{t('ui.routesLoading')}</p>{/if}
573
+ {#if selectedRoute?.service}
574
+ <div role="alert" class="border-l-2 border-amber-500 bg-amber-500/5 px-3 py-2 text-sm text-amber-300 space-y-2">
575
+ <p>{t('ui.routeUsesService', { name: selectedRoute.service })}</p>
576
+ <Button variant="outline" size="sm" onclick={editReferencedService}>{@render actionIcon(Server)}{t('ui.editReferencedService')}</Button>
577
+ </div>
578
+ {:else}<p class="text-sm text-zinc-400">{t('ui.routeHelp')}</p>{/if}
579
+ {#if routeError}<p role="alert" class="text-sm text-red-300">{t(routeError)}</p><Button variant="outline" disabled={routesLoading} aria-busy={routesLoading} onclick={loadRoutes}>{@render actionIcon(RefreshCw, routesLoading)}{t('ui.reloadRoutes')}</Button>{/if}
580
+ {/snippet}
581
+ {#snippet footer()}
582
+ <Button disabled={routesLoading || !!routeError || !!selectedRoute?.service} aria-busy={routesLoading} onclick={continueToRoute}>{@render actionIcon(ArrowRight, routesLoading)}{t('ui.continueRoute')}</Button>
583
+ {/snippet}
584
+ </IndustrialDialog>
585
+
523
586
  {#snippet accountActionFooter()}
524
587
  <Button form={`${id}-account-action`} type="submit" variant={action === 'delete' ? 'destructive' : 'default'} disabled={actionBusy} aria-busy={actionBusy}>{@render actionIcon(actionIcons[action] ?? Check, actionBusy)}{t(action === 'delete' ? 'ui.confirmDelete' : 'ui.confirm')}</Button>
525
588
  {/snippet}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jeffusion/bungee-core",
3
- "version": "4.4.2",
3
+ "version": "4.5.0",
4
4
  "description": "Bungee reverse proxy core engine",
5
5
  "main": "./dist/main.js",
6
6
  "types": "./dist/main.d.ts",
@@ -12,7 +12,7 @@
12
12
  "test": "bun test"
13
13
  },
14
14
  "dependencies": {
15
- "@jeffusion/bungee-types": "^4.4.2",
15
+ "@jeffusion/bungee-types": "^4.5.0",
16
16
  "winston": "^3.17.0",
17
17
  "winston-daily-rotate-file": "^5.0.0",
18
18
  "lodash-es": "^4.17.21",