dsh-router-core 0.4.3 → 0.4.5
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/LICENSE +0 -0
- package/README.md +0 -0
- package/cordis.patch.yml +0 -0
- package/docs/ext.md +0 -0
- package/docs/pool-sticky-block.md +59 -1
- package/docs/screenshot.png +0 -0
- package/docs/suppliers.md +20 -1
- package/lib/client-registry.js +24 -14
- package/lib/client.js +24 -14
- package/lib/ext-prompt/index.js +2 -2
- package/lib/ext-prompt/locale/en.json +0 -0
- package/lib/ext-prompt/locale/zh.json +0 -0
- package/lib/ext-prompt/package.json +0 -0
- package/lib/ext-test/locale/en.json +0 -0
- package/lib/ext-test/locale/zh.json +0 -0
- package/lib/ext-test/package.json +0 -0
- package/lib/index.js +106 -17
- package/lib/suppliers/nvidia/locale/en.json +0 -0
- package/lib/suppliers/nvidia/locale/zh.json +0 -0
- package/lib/suppliers/nvidia/package.json +0 -0
- package/lib/suppliers/opencode/locale/en.json +0 -0
- package/lib/suppliers/opencode/locale/zh.json +0 -0
- package/lib/suppliers/opencode/package.json +0 -0
- package/lib/suppliers/openrouter/locale/en.json +0 -0
- package/lib/suppliers/openrouter/locale/zh.json +0 -0
- package/lib/suppliers/openrouter/package.json +0 -0
- package/lib/types/ext-prompt/content.d.ts +10 -3
- package/lib/types/router/account-pool.d.ts +23 -1
- package/lib/types/router/index.d.ts +25 -0
- package/lib/types/router/types.d.ts +9 -1
- package/lib/types/shared.d.ts +6 -0
- package/package.json +1 -1
- package/src/client/CheckinCard.tsx +0 -0
- package/src/client/CombosTab.tsx +0 -0
- package/src/client/EndpointTab.tsx +0 -0
- package/src/client/ExtDetail.tsx +0 -0
- package/src/client/ExtTab.tsx +0 -0
- package/src/client/ExtTestPanel.tsx +0 -0
- package/src/client/LastHitDock.tsx +0 -0
- package/src/client/Modal.tsx +0 -0
- package/src/client/RouterComponentsSection.tsx +0 -0
- package/src/client/RouterView.tsx +0 -0
- package/src/client/StatsTab.tsx +0 -0
- package/src/client/SupplierDetail.tsx +1 -5
- package/src/client/account-badge.test.ts +37 -0
- package/src/client/account-badge.ts +24 -0
- package/src/client/comp-button-scope.test.ts +0 -0
- package/src/client/comp-panel-layout.test.ts +0 -0
- package/src/client/env.d.ts +0 -0
- package/src/client/ext-panels.test.ts +0 -0
- package/src/client/ext-panels.ts +0 -0
- package/src/client/index.tsx +0 -0
- package/src/client/last-hit-store.ts +0 -0
- package/src/client/model-choice.test.ts +0 -0
- package/src/client/model-choice.ts +0 -0
- package/src/client/model-hint-copy.test.ts +0 -0
- package/src/client/model-hint-copy.ts +0 -0
- package/src/client/router-components.test.ts +0 -0
- package/src/client/router-components.ts +0 -0
- package/src/client/router.css +0 -0
- package/src/client/settings-nav-icon.ts +0 -0
- package/src/client/settings-section.tsx +0 -0
- package/src/client/sidebar-entry.ts +0 -0
- package/src/client/supplier-visibility.test.ts +0 -0
- package/src/client/supplier-visibility.ts +0 -0
- package/src/client/workspace-controller.ts +0 -0
- package/src/client/workspace-mount.tsx +0 -0
- package/src/credential-store.ts +0 -0
- package/src/data-dir.test.ts +0 -0
- package/src/data-dir.ts +0 -0
- package/src/ext/contract.ts +0 -0
- package/src/ext/store.test.ts +0 -0
- package/src/ext/store.ts +0 -0
- package/src/ext-prompt/assembly.test.ts +0 -0
- package/src/ext-prompt/content.ts +14 -7
- package/src/ext-prompt/ext-prompt.test.ts +0 -0
- package/src/ext-prompt/index.ts +0 -0
- package/src/ext-prompt/locale/en.json +0 -0
- package/src/ext-prompt/locale/zh.json +0 -0
- package/src/ext-prompt/logo.svg +0 -0
- package/src/ext-prompt/logo.ts +0 -0
- package/src/ext-prompt/mount.test.ts +0 -0
- package/src/ext-prompt/mount.ts +0 -0
- package/src/ext-prompt/plugin.ts +0 -0
- package/src/ext-prompt/render.test.ts +0 -0
- package/src/ext-prompt/render.ts +0 -0
- package/src/ext-prompt/row.json +0 -0
- package/src/ext-route.test.ts +0 -0
- package/src/ext-test/ext-test.test.ts +0 -0
- package/src/ext-test/index.ts +0 -0
- package/src/ext-test/locale/en.json +0 -0
- package/src/ext-test/locale/zh.json +0 -0
- package/src/ext-test/plugin.ts +0 -0
- package/src/ext-test/row.json +0 -0
- package/src/host-base-url.test.ts +0 -0
- package/src/host-base-url.ts +0 -0
- package/src/host-version.test.ts +0 -0
- package/src/host-version.ts +0 -0
- package/src/index.ts +12 -1
- package/src/keys.ts +0 -0
- package/src/llm/adapter.test.ts +0 -0
- package/src/llm/adapter.ts +0 -0
- package/src/router/account-pool.test.ts +74 -0
- package/src/router/account-pool.ts +37 -5
- package/src/router/combo-change.test.ts +125 -0
- package/src/router/disconnect-leak.test.ts +0 -0
- package/src/router/first-byte-budget.test.ts +0 -0
- package/src/router/index.ts +43 -3
- package/src/router/models-cache.test.ts +0 -0
- package/src/router/precise-dispatch.test.ts +0 -0
- package/src/router/prefix-affinity.ts +0 -0
- package/src/router/supplier-switch.test.ts +91 -0
- package/src/router/types.ts +9 -1
- package/src/router/usage-store.ts +0 -0
- package/src/router/usage-tokens.ts +0 -0
- package/src/router/usage.test.ts +0 -0
- package/src/shared.ts +6 -0
- package/src/supplier-config.test.ts +0 -0
- package/src/supplier-config.ts +0 -0
- package/src/suppliers/contract.ts +0 -0
- package/src/suppliers/http-state.test.ts +0 -0
- package/src/suppliers/http-state.ts +0 -0
- package/src/suppliers/loader.test.ts +53 -0
- package/src/suppliers/loader.ts +0 -0
- package/src/suppliers/nvidia/index.ts +0 -0
- package/src/suppliers/nvidia/locale/en.json +0 -0
- package/src/suppliers/nvidia/locale/zh.json +0 -0
- package/src/suppliers/nvidia/plugin.ts +0 -0
- package/src/suppliers/nvidia/row.json +0 -0
- package/src/suppliers/opencode/index.ts +0 -0
- package/src/suppliers/opencode/locale/en.json +0 -0
- package/src/suppliers/opencode/locale/zh.json +0 -0
- package/src/suppliers/opencode/plugin.test.ts +0 -0
- package/src/suppliers/opencode/plugin.ts +0 -0
- package/src/suppliers/opencode/row.json +0 -0
- package/src/suppliers/openrouter/index.ts +0 -0
- package/src/suppliers/openrouter/locale/en.json +0 -0
- package/src/suppliers/openrouter/locale/zh.json +0 -0
- package/src/suppliers/openrouter/plugin.ts +0 -0
- package/src/suppliers/openrouter/row.json +0 -0
- package/src/suppliers/probe.test.ts +0 -0
- package/src/suppliers/probe.ts +0 -0
- package/src/suppliers/registry.test.ts +56 -0
- package/src/suppliers/registry.ts +40 -10
- package/src/types.d.ts +0 -0
|
@@ -36,10 +36,26 @@ export declare class Router {
|
|
|
36
36
|
private log;
|
|
37
37
|
/** 每条降级腿的首字节预算(ms)。只给测试一个入口,生产恒为 FIRST_BYTE_BUDGET_MS。 */
|
|
38
38
|
private firstByteBudgetMs;
|
|
39
|
+
/**
|
|
40
|
+
* 组合增删改**成功提交后**的通知出口。
|
|
41
|
+
*
|
|
42
|
+
* 为什么必须有:模型选择器(`ui-model-selection`)的目录是**客户端缓存**的,
|
|
43
|
+
* 只在宿主转发过来的事件上失效重载(`llm/adapters-updated` /
|
|
44
|
+
* `settings/document-updated` / 凭据事件 / 重连)。而组合只是本插件
|
|
45
|
+
* `combos.json` 里的一份状态:改完名字若不发通知,选择器会一直拿着旧快照 ——
|
|
46
|
+
* 用户能看到的症状是「面板里明明有这个名字,模型选择器里却找不到、切不过去」,
|
|
47
|
+
* 且新建的组合永远不出现(2026-10-14 实测)。
|
|
48
|
+
*
|
|
49
|
+
* 通知与落盘分开({@link saveCombos} 先 {@link commitCombos} 落盘、再发通知):
|
|
50
|
+
* 落盘失败、或 `stateFile` 为空(单测/独立运行时)时内存已经改了,客户端的
|
|
51
|
+
* 状态同样过期,所以这两条路径都必须照发。
|
|
52
|
+
*/
|
|
53
|
+
private onCombosChanged;
|
|
39
54
|
/** 用量统计(概览看板)。 */
|
|
40
55
|
usage: UsageStore;
|
|
41
56
|
constructor(stateFile?: string, store?: SupplierConfigStore, log?: (msg: string) => void, opts?: {
|
|
42
57
|
firstByteBudgetMs?: number;
|
|
58
|
+
onCombosChanged?: () => void;
|
|
43
59
|
});
|
|
44
60
|
/**
|
|
45
61
|
* 请求追踪:每次 chat 记一行,回答「这个组合到底用的哪个模型和账号」。
|
|
@@ -48,7 +64,16 @@ export declare class Router {
|
|
|
48
64
|
*/
|
|
49
65
|
private logChat;
|
|
50
66
|
private loadCombos;
|
|
67
|
+
/**
|
|
68
|
+
* 提交组合变更:落盘 + 通知拓扑观察者。
|
|
69
|
+
*
|
|
70
|
+
* `onCombosChanged` 的失败必须被吞掉:它是**通知**不是事务参与者,
|
|
71
|
+
* 监听方(宿主事件总线)出问题不能把一次已经生效的增删改报成失败 ——
|
|
72
|
+
* 那会让用户重试一个其实已经成功的操作(同 `schedule/changed` 的口径)。
|
|
73
|
+
*/
|
|
51
74
|
private saveCombos;
|
|
75
|
+
/** 组合落盘(通知由 {@link saveCombos} 单独发,落盘失败不阻断它)。 */
|
|
76
|
+
private commitCombos;
|
|
52
77
|
add(supplier: Supplier): void;
|
|
53
78
|
/**
|
|
54
79
|
* **参与路由的供应商** —— 装载清单里开关为开的那一份,按 priority 排好。
|
|
@@ -3,13 +3,21 @@
|
|
|
3
3
|
* 每个供应商提供 OpenAI 兼容的 chat/models,并暴露面板状态。
|
|
4
4
|
* 仿 9router:供应商 = 一种"免费/付费"通道;dsh-router 在其上做路由。
|
|
5
5
|
*/
|
|
6
|
-
import type { ChatOnceResult, SupplierAccountNow } from '../suppliers/contract.ts';
|
|
6
|
+
import type { AccountState, ChatOnceResult, SupplierAccountNow } from '../suppliers/contract.ts';
|
|
7
7
|
import type { AccountPool } from './account-pool.ts';
|
|
8
8
|
/** 面板展示的账号状态(脱敏)。 */
|
|
9
9
|
export interface SupplierAccount {
|
|
10
10
|
uid: string;
|
|
11
11
|
nickname?: string;
|
|
12
12
|
credits: number;
|
|
13
|
+
/**
|
|
14
|
+
* 插件解读上游信号后的语义状态(`quota` = 额度/积分用完,`session_dead` = 凭证失效)。
|
|
15
|
+
*
|
|
16
|
+
* `decorate()` 一直原样带出插件报的状态,核心选号也读它(`quota`/`session_dead`
|
|
17
|
+
* 的号不进候选)——面板类型里缺了这一格就会自相矛盾:号明明已被排除在路由之外,
|
|
18
|
+
* 界面上却只看得到「0 积分」,看不出它为什么不参与。
|
|
19
|
+
*/
|
|
20
|
+
state?: AccountState;
|
|
13
21
|
cooling: boolean;
|
|
14
22
|
until?: string;
|
|
15
23
|
reason?: string;
|
package/lib/types/shared.d.ts
CHANGED
|
@@ -19,6 +19,12 @@ export interface RouterAccount {
|
|
|
19
19
|
uid: string;
|
|
20
20
|
nickname?: string;
|
|
21
21
|
credits: number;
|
|
22
|
+
/**
|
|
23
|
+
* 插件解读上游信号后的语义状态(`quota` = 额度/积分用完,`session_dead` = 凭证失效)。
|
|
24
|
+
* 核心选号会跳过这些号,所以面板必须显示出来——否则界面写着「健康」,而路由
|
|
25
|
+
* 永远不选它,用户无从知道为什么。
|
|
26
|
+
*/
|
|
27
|
+
state?: string;
|
|
22
28
|
cooling: boolean;
|
|
23
29
|
until?: string;
|
|
24
30
|
reason?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-router-core",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.5",
|
|
4
4
|
"description": "DSH plugin: a simplified 9router (AI routing gateway). Adds a 路由系统 (Routing System) sidebar entry beside 记忆系统; the panel shows suppliers, account pool, combos, and endpoint/API keys.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
File without changes
|
package/src/client/CombosTab.tsx
CHANGED
|
File without changes
|
|
File without changes
|
package/src/client/ExtDetail.tsx
CHANGED
|
File without changes
|
package/src/client/ExtTab.tsx
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/client/Modal.tsx
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/client/StatsTab.tsx
CHANGED
|
File without changes
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type RouterSupplierModel,
|
|
13
13
|
type RouterSupplierModelsResponse,
|
|
14
14
|
} from '../shared.ts'
|
|
15
|
+
import { statusBadge } from './account-badge.ts'
|
|
15
16
|
import { Modal } from './Modal.tsx'
|
|
16
17
|
|
|
17
18
|
/* ---------------- SVG 图标 ---------------- */
|
|
@@ -61,11 +62,6 @@ function untilLabel(account: RouterAccount): string {
|
|
|
61
62
|
return `剩余 ${hours} 小时`
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
function statusBadge(account: RouterAccount): { text: string; cls: string } {
|
|
65
|
-
if (account.cooling) return { text: '冷却中', cls: 'dshr-badge-warn' }
|
|
66
|
-
return { text: '健康', cls: 'dshr-badge-ok' }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
65
|
export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRefresh }: SupplierDetailProps): JSX.Element {
|
|
70
66
|
const hasCap = (name: string): boolean => supplier.capabilities?.includes(name) ?? false
|
|
71
67
|
// 连接池(账号池)由「添加链接」能力控制显示——没有添加链接 = 无账号概念,不显示连接池
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 账号徽章闸门 —— 面板不许和路由说反话。
|
|
3
|
+
*
|
|
4
|
+
* 核心 `pick()` 会跳过插件报 `quota`/`session_dead` 的号。面板若把它们显示成
|
|
5
|
+
* 「健康」,用户看到的就是「一切正常但请求永远不走这个号」——没有线索可查。
|
|
6
|
+
* 这类不一致不会抛异常,只会让人误判,所以要有闸门。
|
|
7
|
+
*/
|
|
8
|
+
import test from 'node:test'
|
|
9
|
+
import assert from 'node:assert/strict'
|
|
10
|
+
import { statusBadge } from './account-badge.ts'
|
|
11
|
+
import type { RouterAccount } from '../shared.ts'
|
|
12
|
+
|
|
13
|
+
const acc = (over: Partial<RouterAccount> = {}): RouterAccount => ({
|
|
14
|
+
uid: 'u1', credits: 100, cooling: false, ...over,
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('额度用完的号显示「额度用完」,不是「健康」', () => {
|
|
18
|
+
const b = statusBadge(acc({ credits: 0, state: 'quota' }))
|
|
19
|
+
assert.equal(b.text, '额度用完')
|
|
20
|
+
assert.notEqual(b.cls, 'dshr-badge-ok', '不能用健康色')
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
test('凭证失效显示「凭证失效」', () => {
|
|
24
|
+
assert.equal(statusBadge(acc({ state: 'session_dead' })).text, '凭证失效')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('冷却优先于状态:冷却带 until/reason,用户更需要先看到', () => {
|
|
28
|
+
const b = statusBadge(acc({ state: 'quota', cooling: true, reason: 'quota: exceeded' }))
|
|
29
|
+
assert.equal(b.text, '冷却中')
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
test('正常号仍显示「健康」(不误伤 openrouter/nvidia 那种 credits: 0 的号)', () => {
|
|
33
|
+
// 这两家没有积分概念,报的是 credits: 0 + state: 'ok' —— 不能因为 0 就报警。
|
|
34
|
+
assert.equal(statusBadge(acc({ credits: 0, state: 'ok' })).text, '健康')
|
|
35
|
+
assert.equal(statusBadge(acc({ credits: 0 })).text, '健康', 'state 缺省(老插件)也不报警')
|
|
36
|
+
assert.equal(statusBadge(acc({ credits: 500, state: 'ok' })).text, '健康')
|
|
37
|
+
})
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 账号行的状态徽章判据 —— 面板必须和路由说同一句话。
|
|
3
|
+
*
|
|
4
|
+
* 核心选号会跳过插件报 `quota`(额度/积分用完)和 `session_dead`(凭证失效)的号
|
|
5
|
+
* (见 `account-pool.servableByReport`)。如果面板把这些号显示成「健康」,用户
|
|
6
|
+
* 就会看到「界面说没问题、可是请求永远不走它」——查不出原因,只能去删链接重登,
|
|
7
|
+
* 而真正的解法是充值。
|
|
8
|
+
*
|
|
9
|
+
* 判据顺序(先冷却后状态)的理由:冷却带 `until` 和 `reason`,比静态状态更具体,
|
|
10
|
+
* 用户更需要先看到「什么时候会好」。
|
|
11
|
+
*/
|
|
12
|
+
import type { RouterAccount } from '../shared.ts'
|
|
13
|
+
|
|
14
|
+
export interface AccountBadge {
|
|
15
|
+
text: string
|
|
16
|
+
cls: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function statusBadge(account: RouterAccount): AccountBadge {
|
|
20
|
+
if (account.cooling) return { text: '冷却中', cls: 'dshr-badge-warn' }
|
|
21
|
+
if (account.state === 'quota') return { text: '额度用完', cls: 'dshr-badge-warn' }
|
|
22
|
+
if (account.state === 'session_dead') return { text: '凭证失效', cls: 'dshr-badge-warn' }
|
|
23
|
+
return { text: '健康', cls: 'dshr-badge-ok' }
|
|
24
|
+
}
|
|
File without changes
|
|
File without changes
|
package/src/client/env.d.ts
CHANGED
|
File without changes
|
|
File without changes
|
package/src/client/ext-panels.ts
CHANGED
|
File without changes
|
package/src/client/index.tsx
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/client/router.css
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/credential-store.ts
CHANGED
|
File without changes
|
package/src/data-dir.test.ts
CHANGED
|
File without changes
|
package/src/data-dir.ts
CHANGED
|
File without changes
|
package/src/ext/contract.ts
CHANGED
|
File without changes
|
package/src/ext/store.test.ts
CHANGED
|
File without changes
|
package/src/ext/store.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 提示词内容层 —— 准则 v5
|
|
2
|
+
* 提示词内容层 —— 准则 v5 的副本,按原分隔拆成分类。
|
|
3
3
|
*
|
|
4
4
|
* ════════════════════════════════════════════════════════════════════
|
|
5
|
-
*
|
|
5
|
+
* 为什么原则上不改措辞
|
|
6
6
|
* ════════════════════════════════════════════════════════════════════
|
|
7
7
|
* 这些字是**用户自己写的准则**,不是本插件的文案。任何"顺手润色"都是擅自
|
|
8
8
|
* 改写用户的规则 —— 改出来的版本和用户心里的版本不一致,而用户看不见差异,
|
|
9
|
-
*
|
|
9
|
+
* 于是准则开始按**另一份**文本执行。所以改动只在**用户点名**时发生,不做
|
|
10
|
+
* 未被要求的措辞优化。
|
|
10
11
|
*
|
|
11
12
|
* 拆分只为「可开关」,不为了重排:顺序与原文出现顺序一致,`order` 也按此递增。
|
|
12
13
|
*
|
|
13
14
|
* ⚠️ **本文件是唯一事实源**。准则原本存放在 Mnemon 记忆系统的 `USER.md`;
|
|
14
15
|
* 那份已经在插件验证生效后删除(否则新旧两份注入,每轮白烧一次 token 且
|
|
15
16
|
* 永远有两份可能漂移的规则)。**改动准则 = 只改这里。**
|
|
17
|
+
*
|
|
18
|
+
* ⚠️ 2026-10-08 用户点名的两处改动(治「提示词导致思考过久」):
|
|
19
|
+
* ① 收口:加"只在任务完成时做",并**删掉第 ④ 条**「前三问都空时处置一件存量」
|
|
20
|
+
* —— 它没有终止条件,每次收口都必然能再找出一件存量,等于无限循环。
|
|
21
|
+
* ② 固化:由"每次用户可见损失都必须固化"降级为"只对数据丢失/安全这类真损失
|
|
22
|
+
* 补检查",并删掉「不固化的教训等于决定再犯一次」这句无界义务。
|
|
16
23
|
*/
|
|
17
24
|
|
|
18
25
|
/** 一个可独立开关的提示词分类。 */
|
|
@@ -69,8 +76,8 @@ export const PROMPT_CATEGORIES: readonly PromptCategory[] = [
|
|
|
69
76
|
title: '全局收口',
|
|
70
77
|
defaultOn: true,
|
|
71
78
|
body:
|
|
72
|
-
'收口(
|
|
73
|
-
'
|
|
79
|
+
'收口(只在任务完成时做,与跑测试同级):①反查过期:改动让哪些注释/文档/结论/代码引用点(凡以名字对齐的调用方、开关名、注册键)不符事实?当场改或删;' +
|
|
80
|
+
'②反查重复:新旧成两份就并成一处;③反查形状:该合的合、该归档的归位。' +
|
|
74
81
|
'做完那块区域该比动手前更整或更少。',
|
|
75
82
|
},
|
|
76
83
|
{
|
|
@@ -78,8 +85,8 @@ export const PROMPT_CATEGORIES: readonly PromptCategory[] = [
|
|
|
78
85
|
title: '固化',
|
|
79
86
|
defaultOn: true,
|
|
80
87
|
body:
|
|
81
|
-
'
|
|
82
|
-
'归属清晰(失败信息指向违规处)
|
|
88
|
+
'固化:只对数据丢失/安全这类真损失补检查——会变红(注入违规必须失败)、可重放(一条命令)、' +
|
|
89
|
+
'归属清晰(失败信息指向违规处);普通 bug 修好即止,不额外造闸门。',
|
|
83
90
|
},
|
|
84
91
|
{
|
|
85
92
|
id: 'verdifiable',
|
|
File without changes
|
package/src/ext-prompt/index.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/ext-prompt/logo.svg
CHANGED
|
File without changes
|
package/src/ext-prompt/logo.ts
CHANGED
|
File without changes
|
|
File without changes
|
package/src/ext-prompt/mount.ts
CHANGED
|
File without changes
|
package/src/ext-prompt/plugin.ts
CHANGED
|
File without changes
|
|
File without changes
|
package/src/ext-prompt/render.ts
CHANGED
|
File without changes
|
package/src/ext-prompt/row.json
CHANGED
|
File without changes
|
package/src/ext-route.test.ts
CHANGED
|
File without changes
|
|
File without changes
|
package/src/ext-test/index.ts
CHANGED
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/src/ext-test/plugin.ts
CHANGED
|
File without changes
|
package/src/ext-test/row.json
CHANGED
|
File without changes
|
|
File without changes
|
package/src/host-base-url.ts
CHANGED
|
File without changes
|
package/src/host-version.test.ts
CHANGED
|
File without changes
|
package/src/host-version.ts
CHANGED
|
File without changes
|
package/src/index.ts
CHANGED
|
@@ -162,6 +162,11 @@ export function apply(rawContext: unknown): void {
|
|
|
162
162
|
get: (key: string) => unknown
|
|
163
163
|
on: (name: string, listener: (...args: unknown[]) => void) => unknown
|
|
164
164
|
provide: (name: string, value: unknown) => unknown
|
|
165
|
+
/**
|
|
166
|
+
* 广播宿主事件。组合增删改后必须用它发 `llm/adapters-updated`,
|
|
167
|
+
* 否则模型选择器一直拿着旧快照(见 `Router` 的 `onCombosChanged` 注释)。
|
|
168
|
+
*/
|
|
169
|
+
emit: (name: string, ...args: unknown[]) => unknown
|
|
165
170
|
webServer: WebServer
|
|
166
171
|
llm?: Llm
|
|
167
172
|
logger: { info: (message: string) => void; warn: (message: string) => void }
|
|
@@ -193,7 +198,13 @@ export function apply(rawContext: unknown): void {
|
|
|
193
198
|
}
|
|
194
199
|
const store = new SupplierConfigStore(stateFile)
|
|
195
200
|
const credentials = new CredentialStore(join(dataDir, 'auths'))
|
|
196
|
-
|
|
201
|
+
// 组合增删改后广播 `llm/adapters-updated`:模型选择器的目录是客户端缓存的,
|
|
202
|
+
// 只在这个(宿主已转发给客户端的)事件上失效重载。不发它就出现「面板里有、
|
|
203
|
+
// 选择器里找不到/切不过去」(2026-10-14 实测)。
|
|
204
|
+
// 该事件在宿主侧是纯通知(payload-free),监听方都是幂等的重读,重复发安全。
|
|
205
|
+
const router = new Router(stateFile, store, log, {
|
|
206
|
+
onCombosChanged: () => { ctx.emit('llm/adapters-updated') },
|
|
207
|
+
})
|
|
197
208
|
|
|
198
209
|
const keys = new KeysStore(stateFile)
|
|
199
210
|
|
package/src/keys.ts
CHANGED
|
File without changes
|
package/src/llm/adapter.test.ts
CHANGED
|
File without changes
|
package/src/llm/adapter.ts
CHANGED
|
File without changes
|
|
@@ -517,6 +517,80 @@ test('状态表:各 AccountState 的处置符合预期', () => {
|
|
|
517
517
|
assert.equal(p.pick(list, [], 'm1'), undefined, '四个号都不可服务')
|
|
518
518
|
})
|
|
519
519
|
|
|
520
|
+
// ---- 插件报的额度/凭证状态直接进选号 ----
|
|
521
|
+
// 分工:插件报「现在怎么了」,核心决定「怎么办」。积分用完(quota)/凭证失效
|
|
522
|
+
// (session_dead)要靠充值/重登才能恢复,选它必是白撞一次——而且 traework 的
|
|
523
|
+
// 积分耗尽失败发生在流已提交之后(HTTP 200 + 流内 4008),这次请求救不回来。
|
|
524
|
+
// 所以必须在**选号阶段**就排除,不能等失败后记账。
|
|
525
|
+
|
|
526
|
+
test('报 quota 的号不进选号(额度用完,选它必白撞)', () => {
|
|
527
|
+
const p = pool()
|
|
528
|
+
const list: SupplierAccountNow[] = [
|
|
529
|
+
{ uid: 'broke', credits: 0, state: 'quota' },
|
|
530
|
+
{ uid: 'rich', credits: 100, state: 'ok' },
|
|
531
|
+
]
|
|
532
|
+
assert.equal(p.pick(list, ['broke', 'rich'], 'm1'), 'rich', '池顺序在前也没用,用不了的号跳过')
|
|
533
|
+
})
|
|
534
|
+
|
|
535
|
+
test('报 session_dead 的号不进选号(凭证失效,要重登)', () => {
|
|
536
|
+
const p = pool()
|
|
537
|
+
const list: SupplierAccountNow[] = [
|
|
538
|
+
{ uid: 'dead', credits: 100, state: 'session_dead' },
|
|
539
|
+
{ uid: 'alive', credits: 1, state: 'ok' },
|
|
540
|
+
]
|
|
541
|
+
assert.equal(p.pick(list, ['dead', 'alive'], 'm1'), 'alive')
|
|
542
|
+
})
|
|
543
|
+
|
|
544
|
+
test('全池都报 quota → 无号可选,且说明原因不是「都在冷却」', () => {
|
|
545
|
+
const p = pool()
|
|
546
|
+
const list: SupplierAccountNow[] = [
|
|
547
|
+
{ uid: 'a', credits: 0, state: 'quota' },
|
|
548
|
+
{ uid: 'b', credits: 0, state: 'quota' },
|
|
549
|
+
]
|
|
550
|
+
assert.equal(p.pick(list, [], 'm1'), undefined)
|
|
551
|
+
assert.match(p.lastWhy, /额度|凭证/, '原因要能区分「没额度」和「冷却中」')
|
|
552
|
+
})
|
|
553
|
+
|
|
554
|
+
test('报 quota 的号恢复成 ok 后立刻回到候选(充值即自愈,无需重启)', () => {
|
|
555
|
+
const p = pool()
|
|
556
|
+
assert.equal(p.pick([{ uid: 'u', credits: 0, state: 'quota' }], [], 'm1'), undefined)
|
|
557
|
+
assert.equal(p.pick([{ uid: 'u', credits: 50, state: 'ok' }], [], 'm1'), 'u')
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
test('积分 0 但状态是 ok 的号照常可选(0 不等于「没额度」)', () => {
|
|
561
|
+
const p = pool()
|
|
562
|
+
// openrouter/nvidia 报的就是 credits: 0 + state: 'ok'(没有积分这个概念)。
|
|
563
|
+
// 核心不能自己拿 credits===0 当「用完了」,否则这些供应商会被整池排除。
|
|
564
|
+
const list: SupplierAccountNow[] = [{ uid: 'api-key', credits: 0, state: 'ok' }]
|
|
565
|
+
assert.equal(p.pick(list, [], 'm1'), 'api-key')
|
|
566
|
+
})
|
|
567
|
+
|
|
568
|
+
test('瞬时状态(transport/unknown/unavailable)不排除号:那是采样抖动,不是账号事实', () => {
|
|
569
|
+
const p = pool()
|
|
570
|
+
for (const state of ['transport', 'unknown', 'unavailable'] as const) {
|
|
571
|
+
const list: SupplierAccountNow[] = [{ uid: 'u', credits: 5, state }]
|
|
572
|
+
assert.equal(p.pick(list, [], 'm1'), 'u', `${state} 不该让号退出候选`)
|
|
573
|
+
}
|
|
574
|
+
})
|
|
575
|
+
|
|
576
|
+
test('会话绑定撞上报 quota 的号 → 换号,且 lastWhy 指出是额度问题', () => {
|
|
577
|
+
const p = pool()
|
|
578
|
+
const msgs = [{ role: 'user', content: 'hello' }]
|
|
579
|
+
const okList: SupplierAccountNow[] = [
|
|
580
|
+
{ uid: 'bound', credits: 10, state: 'ok' },
|
|
581
|
+
{ uid: 'other', credits: 10, state: 'ok' },
|
|
582
|
+
]
|
|
583
|
+
assert.equal(p.pick(okList, [], 'm1', msgs), 'bound', '先绑到 bound')
|
|
584
|
+
// 该号积分耗尽后:同一会话必须换到别的号,不能继续钉在死号上
|
|
585
|
+
const brokeList: SupplierAccountNow[] = [
|
|
586
|
+
{ uid: 'bound', credits: 0, state: 'quota' },
|
|
587
|
+
{ uid: 'other', credits: 10, state: 'ok' },
|
|
588
|
+
]
|
|
589
|
+
assert.equal(p.pick(brokeList, [], 'm1', msgs), 'other')
|
|
590
|
+
assert.match(p.lastWhy, /bound/, '要说清为什么没用原绑定')
|
|
591
|
+
assert.match(p.lastWhy, /额度|凭证/, '原因要指向额度/凭证,不是「冷却」')
|
|
592
|
+
})
|
|
593
|
+
|
|
520
594
|
/**
|
|
521
595
|
* 处置对齐 9router:未知/瞬时错误**每次**短冷却(不再攒够 3 次)。
|
|
522
596
|
*/
|
|
@@ -252,6 +252,25 @@ export class AccountPool {
|
|
|
252
252
|
return c.until <= now
|
|
253
253
|
}
|
|
254
254
|
|
|
255
|
+
/**
|
|
256
|
+
* 插件报的「现在状态」是否意味着这个号此刻不该被选中。
|
|
257
|
+
*
|
|
258
|
+
* 只认两种**账号级持久事实**(要靠外部动作才能恢复,不是抖动):
|
|
259
|
+
* - `quota` —— 额度/积分用完,充值才能恢复(traework 积分归零时报它)
|
|
260
|
+
* - `session_dead` —— 凭证失效,重新登录才能恢复
|
|
261
|
+
*
|
|
262
|
+
* 其余一律不看:`transport`/`unknown`/`unavailable` 是插件某一次采样的瞬时
|
|
263
|
+
* 观察,拿它排除等于「一次抖动就踢号」,而且没有冷却计时器兜底;`rate_limit`
|
|
264
|
+
* 的指数退避由核心的冷却单元管,比状态采样准。
|
|
265
|
+
*
|
|
266
|
+
* 这是「插件只报现在怎么了、核心决定怎么办」的分工落地:插件报出它观察到
|
|
267
|
+
* 的事实,核心决定此刻不选它。恢复同样自动——充值/重登后插件报回 `ok`,
|
|
268
|
+
* 该号立刻回到候选(没有额外状态要清)。
|
|
269
|
+
*/
|
|
270
|
+
private servableByReport(a: SupplierAccountNow | undefined): boolean {
|
|
271
|
+
return a?.state !== 'quota' && a?.state !== 'session_dead'
|
|
272
|
+
}
|
|
273
|
+
|
|
255
274
|
/**
|
|
256
275
|
* 为「某个模型」选一个健康账号 —— **会话亲和选号,无策略旋钮**。
|
|
257
276
|
*
|
|
@@ -267,11 +286,17 @@ export class AccountPool {
|
|
|
267
286
|
* "策略",是无会话身份时的替身。
|
|
268
287
|
* 4. 块统计不驱动轮转(亲和已保证驻留),只保留 `BLOCK_MAX` 次不达标 →
|
|
269
288
|
* 降权该号(缓存无效,可恢复)。
|
|
289
|
+
* 5. 插件报的 `quota`/`session_dead`(额度用完 / 凭证失效)→ **直接不选它**:
|
|
290
|
+
* 这类失败要靠充值/重登才能恢复,选它必是白撞一次(还有的供应商失败在
|
|
291
|
+
* 流已提交之后,这次请求根本救不回来)。见 `servableByReport`。
|
|
270
292
|
*
|
|
271
293
|
* 天花板:号数 < 并发会话数时必然有会话共用号 → 互踩,无法用选号手法消除
|
|
272
294
|
* (缓存空间是账号侧物理限制)。见 docs/pool-sticky-block.md §10。
|
|
273
295
|
*
|
|
274
|
-
* @param accounts
|
|
296
|
+
* @param accounts 插件报告的「现在状态」(顺序即插件的自然顺序)。
|
|
297
|
+
* `state` 会被读:`quota`/`session_dead` 的号不进候选(见 `servableByReport`)。
|
|
298
|
+
* `credits` **不读** —— 不同供应商的 0 含义不同(openrouter/nvidia 的 0 是
|
|
299
|
+
* 「没有积分这个概念」),是否已用完由插件解读成 `state` 后报上来。
|
|
275
300
|
* @param poolOrder 用户在面板拖出来的顺序(核心管)
|
|
276
301
|
* @param modelId 当前要路由的模型(决定查哪个 (model, uid) 冷却单元)
|
|
277
302
|
* @param messages 请求体的 messages(算亲和指纹的内容兜底)
|
|
@@ -286,9 +311,11 @@ export class AccountPool {
|
|
|
286
311
|
...poolOrder.filter((uid) => byUid.has(uid)),
|
|
287
312
|
...accounts.map((a) => a.uid).filter((uid) => !poolOrder.includes(uid)),
|
|
288
313
|
]
|
|
289
|
-
const healthy = ordered.filter(
|
|
314
|
+
const healthy = ordered.filter(
|
|
315
|
+
(uid) => this.healthy(uid, modelId, now) && this.servableByReport(byUid.get(uid)),
|
|
316
|
+
)
|
|
290
317
|
if (healthy.length === 0) {
|
|
291
|
-
this.lastWhy = '
|
|
318
|
+
this.lastWhy = '全池无健康号(都在冷却,或插件报的额度/凭证已不可用)'
|
|
292
319
|
return undefined
|
|
293
320
|
}
|
|
294
321
|
// 缓存无效降权:该号缓存质量样本长期不达标 → 跳过它(可到期恢复)
|
|
@@ -309,8 +336,13 @@ export class AccountPool {
|
|
|
309
336
|
why.push('该指纹无绑定(会话首次出现)')
|
|
310
337
|
} else {
|
|
311
338
|
if (!healthy.includes(bound)) {
|
|
312
|
-
const
|
|
313
|
-
|
|
339
|
+
const ba = byUid.get(bound)
|
|
340
|
+
if (!this.servableByReport(ba)) {
|
|
341
|
+
why.push(`原绑定 ${bound} 插件报额度/凭证不可用(${ba?.state})`)
|
|
342
|
+
} else {
|
|
343
|
+
const c = this.cooldowns.get(this.key(modelId, bound))
|
|
344
|
+
why.push(`原绑定 ${bound} 正在冷却${c?.reason ? `(${c.reason})` : ''}`)
|
|
345
|
+
}
|
|
314
346
|
} else if (!usable.includes(bound)) {
|
|
315
347
|
why.push(`原绑定 ${bound} 被降权(本会话样本不达标)`)
|
|
316
348
|
} else {
|