@lynn123411/dsh-llm-agentrouter 0.2.2 → 0.2.3
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 +3 -2
- package/lib/client.js +226 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# @lynn123411/dsh-llm-agentrouter
|
|
2
2
|
|
|
3
|
-
AgentRouter 中转聚合插件:把同一家中转的多个模型收拢为一条 pi-ai 路由(模型选择器只出现一个 AgentRouter 分组),国内 / 国际端点在「设置 -
|
|
3
|
+
AgentRouter 中转聚合插件:把同一家中转的多个模型收拢为一条 pi-ai 路由(模型选择器只出现一个 AgentRouter 分组),国内 / 国际端点在「设置 - API中转 - AgentRouter 中转站」一键切换、下一请求即生效,外加出站请求的 User-Agent 改写与 402 配额耗尽提示围栏。分叉自 `aqiu817/dsh-llm-agentrouter`,适配 DSH `0.1.2-alpha.5` 起的新 settings/slots API,已在 `0.1.5-rc.1` 逐项实证兼容。
|
|
4
4
|
|
|
5
5
|
## 特性
|
|
6
6
|
|
|
7
7
|
- **单路由多模型**:Claude Opus 5、GPT-5.6-sol 等走同一 `agentrouter` 路由,`reasoningEffort` 档位与兼容开关按实测探针手写声明,模型列表不翻倍。
|
|
8
8
|
- **端点一键切换**:设置卡片两个大选项(国内端点 / 国际端点,附带真实 host 展示),点击即写即生效,无需重启;API Key 只存 `$DSH_HOME/.credentials.yaml` 的 `AGENTROUTER_API_KEY` 引用。
|
|
9
|
+
- **共享「API中转」设置页**:本插件的端点卡片与 `dsh-a6api` 的面板挂在同一个设置页里(`settings.section` id `relay` 的页内两个 tab,参与者可增删)。内核不允许一页被多个插件共同声明,因此每个参与者各持一份运行时逻辑一致的页壳,**先加载者当选页面宿主**、其余只注册卡片;当选者被卸载后下次启动自动改选。做法与约束见仓库 `docs/rules/shared-settings-page.md`。
|
|
9
10
|
- **请求围栏**:重写 relay 要求的 `User-Agent`(适配器强制署名下沉到 `fetch` 层替换),哨兵 host(`.internal` 不可解析)兜底防裸奔,402 配额耗尽的 JSON 错误体改写为可读提示。
|
|
10
|
-
- **alpha.5
|
|
11
|
+
- **alpha.5 兼容**:宿主沿用 `ctx.settings.installSection` + `ctx.inject(['settings'])`,客户端卡片改挂共享设置页的子 slot;沿用 `llm-agentrouter` 设置命名空间,老配置无缝继承。
|
|
11
12
|
|
|
12
13
|
## 安装
|
|
13
14
|
|
package/lib/client.js
CHANGED
|
@@ -42,6 +42,7 @@ window.__ModuleLoader__.load({
|
|
|
42
42
|
//#region locales
|
|
43
43
|
/** Simplified Chinese dictionary and key source of truth. */
|
|
44
44
|
const zh = {
|
|
45
|
+
pageNav: "API中转",
|
|
45
46
|
title: "AgentRouter 中转站",
|
|
46
47
|
description: "选择请求发往的端点。切换后立即生效,无需重启;模型列表不受影响。",
|
|
47
48
|
cn: "国内端点",
|
|
@@ -57,6 +58,7 @@ window.__ModuleLoader__.load({
|
|
|
57
58
|
};
|
|
58
59
|
/** English dictionary checked against the Chinese key set. */
|
|
59
60
|
const en = {
|
|
61
|
+
pageNav: "API relay",
|
|
60
62
|
title: "AgentRouter relay",
|
|
61
63
|
description: "Which endpoint requests are sent to. A switch applies to the next request; the model list is unaffected.",
|
|
62
64
|
cn: "Domestic endpoint",
|
|
@@ -210,6 +212,217 @@ window.__ModuleLoader__.load({
|
|
|
210
212
|
}
|
|
211
213
|
//#endregion
|
|
212
214
|
|
|
215
|
+
//#region shared relay settings page shell
|
|
216
|
+
/**
|
|
217
|
+
* The shared 「API中转」 settings page.
|
|
218
|
+
*
|
|
219
|
+
* Several plugins contribute their configuration to ONE settings page, but the
|
|
220
|
+
* kernel cannot declare that page jointly: `settings.section` is a list slot
|
|
221
|
+
* that rejects a duplicate `id` at the same priority ("already has an entry
|
|
222
|
+
* with id"), and a child slot may be declared exactly once ("slot … is already
|
|
223
|
+
* declared"). Composing several cards into one page therefore takes one
|
|
224
|
+
* declarer, so every participating plugin carries this same shell and the
|
|
225
|
+
* FIRST one to activate claims the page; the others register their card into
|
|
226
|
+
* RELAY_ITEM_SLOT and wait for the winner's declaration through
|
|
227
|
+
* `slots.inject`. Uninstalling the winner promotes another participant on the
|
|
228
|
+
* next boot, so no participant is a fixed owner.
|
|
229
|
+
*
|
|
230
|
+
* The page renders ONE TAB PER REGISTERED CARD: the tab roster comes from the
|
|
231
|
+
* child slot's own registrations (id + `label` + `order`, the same shape the
|
|
232
|
+
* kernel's own Plugins page uses for its tabs), and each panel dispatches
|
|
233
|
+
* through `renderSlot(RELAY_ITEM_SLOT, {}, { only: id })`. Every panel stays
|
|
234
|
+
* mounted but hidden, so a card's local state survives a tab switch.
|
|
235
|
+
*
|
|
236
|
+
* Keep the runtime body of this region identical to the one in
|
|
237
|
+
* `dsh-a6api`'s `src/client/relay-settings-page.js`. Participants own their own
|
|
238
|
+
* card component, locale dictionaries, settings namespace and Host half — only
|
|
239
|
+
* the page shell is shared, because cross-plugin value imports are forbidden by
|
|
240
|
+
* the client bundle purity gate. It needs nothing but `react` on purpose, so
|
|
241
|
+
* every participant's build configuration compiles it unchanged.
|
|
242
|
+
*/
|
|
243
|
+
/** Page id claimed by the first participating plugin to activate. */
|
|
244
|
+
const RELAY_PAGE_ID = "relay";
|
|
245
|
+
/** Sidebar position of the shared page; own plugins start at 110. */
|
|
246
|
+
const RELAY_PAGE_ORDER = 120;
|
|
247
|
+
/** The page's one child slot: every participant's card registers here. */
|
|
248
|
+
const RELAY_ITEM_SLOT = "relay.settings.item";
|
|
249
|
+
/**
|
|
250
|
+
* Tab chrome mirrors the kernel's own settings tabs (`.tabs` / `.tab` in
|
|
251
|
+
* `ui-settings-plugins`): tertiary label, 13px, 22px gutter. The marker is the
|
|
252
|
+
* ACTIVE TAB'S OWN bottom border and the bar draws no rail of its own — a
|
|
253
|
+
* shared underline reads as "every tab is selected".
|
|
254
|
+
*/
|
|
255
|
+
const TABLIST_STYLE = {
|
|
256
|
+
display: "flex",
|
|
257
|
+
alignItems: "flex-end",
|
|
258
|
+
gap: "22px",
|
|
259
|
+
marginTop: "2px",
|
|
260
|
+
marginBottom: "16px",
|
|
261
|
+
};
|
|
262
|
+
const TAB_STYLE = {
|
|
263
|
+
appearance: "none",
|
|
264
|
+
background: "transparent",
|
|
265
|
+
// No border on ANY tab: the marker below is the active tab's own element, so
|
|
266
|
+
// an inactive tab has nothing that could render a line.
|
|
267
|
+
border: "none",
|
|
268
|
+
position: "relative",
|
|
269
|
+
padding: "7px 1px 11px",
|
|
270
|
+
cursor: "pointer",
|
|
271
|
+
font: "inherit",
|
|
272
|
+
fontSize: "13px",
|
|
273
|
+
lineHeight: "20px",
|
|
274
|
+
color: "var(--dsw-alias-label-tertiary, inherit)",
|
|
275
|
+
};
|
|
276
|
+
const TAB_ACTIVE_STYLE = Object.assign({}, TAB_STYLE, {
|
|
277
|
+
color: "var(--dsw-alias-label-primary, inherit)",
|
|
278
|
+
});
|
|
279
|
+
/** The kernel's own tab marker: a 2px rounded bar under the active label. */
|
|
280
|
+
const TAB_MARKER_STYLE = {
|
|
281
|
+
position: "absolute",
|
|
282
|
+
left: 0,
|
|
283
|
+
right: 0,
|
|
284
|
+
bottom: 0,
|
|
285
|
+
height: "2px",
|
|
286
|
+
borderRadius: "2px 2px 0 0",
|
|
287
|
+
background: "var(--dsw-alias-label-primary, currentColor)",
|
|
288
|
+
};
|
|
289
|
+
/**
|
|
290
|
+
* Panels stay mounted (hidden) so each card keeps its local state. `display:
|
|
291
|
+
* none` is written explicitly because a card's own styles commonly set
|
|
292
|
+
* `display: flex` while the section's stylesheet loads after this one.
|
|
293
|
+
*/
|
|
294
|
+
const PANEL_STYLE = { margin: 0 };
|
|
295
|
+
const PANEL_HIDDEN_STYLE = { margin: 0, display: "none" };
|
|
296
|
+
/** A registration label is a plain string or a thunk re-read per projection. */
|
|
297
|
+
function readLabel(label) {
|
|
298
|
+
if (typeof label === "function") return label();
|
|
299
|
+
return typeof label === "string" ? label : "";
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Build the live tab roster over the child slot's registrations. `locale` is
|
|
303
|
+
* read through `ctx.get`: a participant needs it only to re-read localized
|
|
304
|
+
* labels on a language switch, and reaching an undeclared service as
|
|
305
|
+
* `ctx.locale` would trip the kernel's inject guard. Every participant passes a
|
|
306
|
+
* bound translator as its `label` thunk, so the label re-reads the active
|
|
307
|
+
* language on each projection.
|
|
308
|
+
*
|
|
309
|
+
* @param {object} ctx - browser context carrying the slot registry.
|
|
310
|
+
* @returns {object} the tab store consumed by the page component.
|
|
311
|
+
*/
|
|
312
|
+
function createRelayTabs(ctx) {
|
|
313
|
+
const locale = ctx.get("locale");
|
|
314
|
+
let version = -1;
|
|
315
|
+
let revision = -1;
|
|
316
|
+
let tabs = [];
|
|
317
|
+
return {
|
|
318
|
+
getSnapshot: () => {
|
|
319
|
+
const nextVersion = ctx.slots.getVersion(RELAY_ITEM_SLOT);
|
|
320
|
+
const nextRevision = locale === undefined ? 0 : locale.getSnapshot().revision;
|
|
321
|
+
if (nextVersion === version && nextRevision === revision) return tabs;
|
|
322
|
+
version = nextVersion;
|
|
323
|
+
revision = nextRevision;
|
|
324
|
+
tabs = ctx.slots.entries(RELAY_ITEM_SLOT)
|
|
325
|
+
.map((entry) => ({
|
|
326
|
+
id: entry.options.id ?? "",
|
|
327
|
+
order: entry.options.order ?? 0,
|
|
328
|
+
label: readLabel(entry.options.label),
|
|
329
|
+
}))
|
|
330
|
+
.sort((left, right) => left.order - right.order);
|
|
331
|
+
return tabs;
|
|
332
|
+
},
|
|
333
|
+
subscribe: (listener) => {
|
|
334
|
+
const offSlots = ctx.slots.subscribe(RELAY_ITEM_SLOT, listener);
|
|
335
|
+
const offLocale = locale === undefined ? undefined : locale.subscribe(listener);
|
|
336
|
+
return () => {
|
|
337
|
+
offSlots();
|
|
338
|
+
if (offLocale !== undefined) offLocale();
|
|
339
|
+
};
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Page body: one tab per registered card, plus the selected card's panel.
|
|
345
|
+
* The shell supplies the section's own seats and `renderSlot` bound to the
|
|
346
|
+
* child slot declared at registration time.
|
|
347
|
+
*
|
|
348
|
+
* @param {object} props - the injected render seat plus the tab roster.
|
|
349
|
+
* @returns {JSX.Element|null} the page body.
|
|
350
|
+
*/
|
|
351
|
+
function RelaySettingsSection({ renderSlot, relayTabs }) {
|
|
352
|
+
const tabs = react.useSyncExternalStore(
|
|
353
|
+
relayTabs.subscribe,
|
|
354
|
+
relayTabs.getSnapshot,
|
|
355
|
+
relayTabs.getSnapshot,
|
|
356
|
+
);
|
|
357
|
+
const [requested, setRequested] = react.useState(null);
|
|
358
|
+
const selected = requested !== null && tabs.some((tab) => tab.id === requested)
|
|
359
|
+
? requested
|
|
360
|
+
: (tabs.length > 0 ? tabs[0].id : null);
|
|
361
|
+
if (selected === null) return null;
|
|
362
|
+
return react.createElement(
|
|
363
|
+
"div",
|
|
364
|
+
null,
|
|
365
|
+
react.createElement(
|
|
366
|
+
"div",
|
|
367
|
+
{ role: "tablist", style: TABLIST_STYLE },
|
|
368
|
+
tabs.map((tab) => react.createElement(
|
|
369
|
+
"button",
|
|
370
|
+
{
|
|
371
|
+
key: tab.id,
|
|
372
|
+
type: "button",
|
|
373
|
+
role: "tab",
|
|
374
|
+
"aria-selected": tab.id === selected,
|
|
375
|
+
style: tab.id === selected ? TAB_ACTIVE_STYLE : TAB_STYLE,
|
|
376
|
+
onClick: () => { setRequested(tab.id); },
|
|
377
|
+
},
|
|
378
|
+
tab.label,
|
|
379
|
+
tab.id === selected ? react.createElement("span", { style: TAB_MARKER_STYLE, "aria-hidden": true }) : null,
|
|
380
|
+
)),
|
|
381
|
+
),
|
|
382
|
+
tabs.map((tab) => react.createElement(
|
|
383
|
+
"div",
|
|
384
|
+
{
|
|
385
|
+
key: tab.id,
|
|
386
|
+
role: "tabpanel",
|
|
387
|
+
hidden: tab.id !== selected,
|
|
388
|
+
style: tab.id === selected ? PANEL_STYLE : PANEL_HIDDEN_STYLE,
|
|
389
|
+
},
|
|
390
|
+
renderSlot(RELAY_ITEM_SLOT, {}, { only: tab.id }),
|
|
391
|
+
)),
|
|
392
|
+
);
|
|
393
|
+
}
|
|
394
|
+
/** Whether a participant already holds the shared page. */
|
|
395
|
+
function relayPageClaimed(ctx) {
|
|
396
|
+
return ctx.slots.entries("settings.section").some((entry) => entry.options.id === RELAY_PAGE_ID);
|
|
397
|
+
}
|
|
398
|
+
/**
|
|
399
|
+
* Claim the shared page when no participant holds it yet. Call inside
|
|
400
|
+
* `ctx.slots.inject('settings.section', …)`: injection order decides the
|
|
401
|
+
* winner, and the losers stay silent instead of colliding with the kernel's
|
|
402
|
+
* duplicate-id and duplicate-declaration guards.
|
|
403
|
+
*
|
|
404
|
+
* @param {object} ctx - browser context carrying the slot registry.
|
|
405
|
+
* @param {Function} label - page label of the claiming participant, re-read by
|
|
406
|
+
* the shell on every projection so a language switch reaches the sidebar too.
|
|
407
|
+
* @returns {Function} the page registration's disposer, or a no-op when
|
|
408
|
+
* another participant already holds the page.
|
|
409
|
+
*/
|
|
410
|
+
function claimRelaySettingsPage(ctx, label) {
|
|
411
|
+
if (relayPageClaimed(ctx)) return () => {};
|
|
412
|
+
const relayTabs = createRelayTabs(ctx);
|
|
413
|
+
const children = {};
|
|
414
|
+
children[RELAY_ITEM_SLOT] = { kind: "list", scope: "root" };
|
|
415
|
+
return ctx.slots.register({
|
|
416
|
+
name: "settings.section",
|
|
417
|
+
id: RELAY_PAGE_ID,
|
|
418
|
+
order: RELAY_PAGE_ORDER,
|
|
419
|
+
label,
|
|
420
|
+
inject: () => ({ relayTabs }),
|
|
421
|
+
children,
|
|
422
|
+
}, RelaySettingsSection);
|
|
423
|
+
}
|
|
424
|
+
//#endregion
|
|
425
|
+
|
|
213
426
|
//#region plugin
|
|
214
427
|
/** Dictionary namespace owned by this plugin. */
|
|
215
428
|
const NS = "settings.agentrouter";
|
|
@@ -223,19 +436,28 @@ window.__ModuleLoader__.load({
|
|
|
223
436
|
const inject = ["slots", "locale", "settingsScope"];
|
|
224
437
|
|
|
225
438
|
/**
|
|
226
|
-
* Register the endpoint card
|
|
439
|
+
* Register the endpoint card inside the shared 「API中转」 page, which this
|
|
440
|
+
* plugin carries the shell for and claims when it activates first.
|
|
227
441
|
* @param {object} ctx - the browser plugin context.
|
|
228
442
|
*/
|
|
229
443
|
function apply(ctx) {
|
|
230
444
|
ctx.effect(() => ctx.locale.register(NS, { zh, en }), "llm-agentrouter: dictionaries");
|
|
231
445
|
const t = ctx.locale.bind(NS);
|
|
232
446
|
const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NS });
|
|
233
|
-
|
|
447
|
+
// The page is shared with dsh-a6api and the kernel cannot declare one page
|
|
448
|
+
// twice, so whichever participant activates first claims it and the other
|
|
449
|
+
// only contributes a card (see the shell region above).
|
|
450
|
+
ctx.slots.inject("settings.section", () =>
|
|
451
|
+
claimRelaySettingsPage(ctx, () => t("pageNav")));
|
|
452
|
+
ctx.slots.inject(RELAY_ITEM_SLOT, () =>
|
|
234
453
|
ctx.slots.register(
|
|
235
454
|
{
|
|
236
|
-
name:
|
|
455
|
+
name: RELAY_ITEM_SLOT,
|
|
456
|
+
// id = the Host settings namespace; the shared page filters its
|
|
457
|
+
// panels by this id, so it must be the plugin's own key.
|
|
237
458
|
id: SETTINGS_NS,
|
|
238
|
-
order:
|
|
459
|
+
order: 20,
|
|
460
|
+
// Rendered as the card's tab title inside the shared page.
|
|
239
461
|
label: () => t("title"),
|
|
240
462
|
locale: NS,
|
|
241
463
|
inject: () => ({ scope, t }),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lynn123411/dsh-llm-agentrouter",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "AgentRouter 中转聚合:一条 pi-ai 路由承载多模型 + 国内/国际端点一键切换(设置卡片)+ 出站请求 User-Agent/402-配额改写围栏。aqiu817/dsh-llm-agentrouter 分叉,适配 0.1.2-alpha.5 起新 settings/slots API,已在 0.1.5-rc.1 逐项实证兼容。",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"deepseek-harness",
|