dsh-router-core 0.3.2 → 0.3.4
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 +6 -2
- package/docs/suppliers.md +87 -30
- package/lib/client-registry.js +20 -11
- package/lib/client.js +20 -11
- package/lib/index.js +751 -264
- package/lib/types/llm/adapter.d.ts +7 -1
- package/lib/types/router/account-pool.d.ts +54 -19
- package/lib/types/router/index.d.ts +28 -5
- package/lib/types/router/types.d.ts +0 -2
- package/lib/types/router/usage-store.d.ts +20 -3
- package/lib/types/shared.d.ts +2 -0
- package/lib/types/supplier-config.d.ts +23 -0
- package/lib/types/suppliers/contract.d.ts +9 -6
- package/package.json +1 -1
- package/src/client/RouterView.tsx +1 -1
- package/src/client/SupplierDetail.tsx +15 -10
- package/src/llm/adapter.test.ts +404 -1
- package/src/llm/adapter.ts +242 -34
- package/src/router/account-pool.test.ts +163 -61
- package/src/router/account-pool.ts +200 -68
- package/src/router/index.ts +225 -66
- package/src/router/models-cache.test.ts +224 -6
- package/src/router/precise-dispatch.test.ts +110 -7
- package/src/router/types.ts +0 -2
- package/src/router/usage-store.ts +174 -74
- package/src/router/usage.test.ts +115 -0
- package/src/shared.ts +2 -0
- package/src/supplier-config.test.ts +98 -0
- package/src/supplier-config.ts +60 -1
- package/src/suppliers/contract.ts +9 -9
- package/src/suppliers/loader.test.ts +118 -0
- package/src/suppliers/loader.ts +21 -5
- package/src/suppliers/nvidia/plugin.ts +12 -15
- package/src/suppliers/opencode/plugin.ts +10 -13
- package/src/suppliers/openrouter/plugin.ts +12 -15
- package/src/suppliers/registry.test.ts +23 -3
- package/src/suppliers/registry.ts +11 -3
|
@@ -15,7 +15,13 @@ export interface RouterAdapterSource {
|
|
|
15
15
|
}>>;
|
|
16
16
|
}
|
|
17
17
|
/** openai SSE payload → DSH StreamChunk 流。导出以便单测锁死 usage 契约。 */
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* 把上游 SSE 的 `data:` 载荷流翻译成 DSH 的 StreamChunk 事件流。
|
|
20
|
+
*
|
|
21
|
+
* 吃 `AsyncIterable`(真流式:边收边吐)或 `Iterable`(测试用数组)。
|
|
22
|
+
* 用 `for await` 是因为它天然兼容两者 —— 不必为流式再写一份。
|
|
23
|
+
*/
|
|
24
|
+
export declare function translateSse(payloads: AsyncIterable<string> | Iterable<string>): AsyncGenerator<StreamChunk>;
|
|
19
25
|
/**
|
|
20
26
|
* DSH adapter:provider `router`。模型目录 = 组合;stream 转发到本插件
|
|
21
27
|
* /v1/chat/completions(组合路由在 /v1 内完成)。
|
|
@@ -1,37 +1,72 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* 账号池 —— dsh-router
|
|
2
|
+
* 账号池 —— dsh-router 核心策略:选号、冷却、禁用、遍历回退。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* - 按 poolOrder 排序(未配置的按插件报告顺序追加)
|
|
6
|
-
* - 按 poolStrategy 选号:fallback = 取第一个健康;round-robin = 轮转健康号
|
|
7
|
-
* - 失败按插件报的 AccountState 处置(冷却/禁用/错误累计),攒够阈值自动冷却
|
|
4
|
+
* 供应商插件只管「对单个账号调通上游」,账号层面的策略全在这里。
|
|
8
5
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* ## 冷却颗粒度 = (供应商, 模型, 连接) 三元组
|
|
7
|
+
*
|
|
8
|
+
* 冷却**不按连接整体记**,而是按「哪家供应商 × 哪个模型 × 哪个连接」单独记。
|
|
9
|
+
* 理由:一个连接可以服务多个模型。若某连接调模型 A 时被限流/瞬时抖动而把
|
|
10
|
+
* **整个连接**冷掉,那它调模型 B 也被殃及——一个付费连接会因一个模型的瞬时
|
|
11
|
+
* 失败而对其它模型也不可用,这是错的。
|
|
12
|
+
*
|
|
13
|
+
* - 冷却/限流退避/瞬时 → 键 = (supplierId, modelId, uid),只冷「这个号 × 这个模型」
|
|
14
|
+
* - 禁用(session_dead) → 键 = uid(登录态问题,该号所有模型都不可用)
|
|
15
|
+
* - 手动冷却(面板) → 键 = uid(管理员整连接暂停)
|
|
16
|
+
*
|
|
17
|
+
* 为什么键里要带 supplierId:不同供应商**会有同名的模型 id**(如 traework 与
|
|
18
|
+
* codebuddy 都有 `deepseek-v4-flash`),它们是不同的实体,单靠 (modelId, uid)
|
|
19
|
+
* 会把跨供应商的同名模型串到一起。尽管本池实例当前是单供应商(loader 每供应商
|
|
20
|
+
* 一个 AccountPool),键仍显式带 supplierId,语义自明且将来若池合并也安全。
|
|
21
|
+
*
|
|
22
|
+
* 天花板:冷却/禁用都是**内存态**,重启归零(与旧插件行为一致)。要跨重启保留
|
|
23
|
+
* 得落盘,届时加 stateFile 即可,接口不用变。
|
|
12
24
|
*/
|
|
13
25
|
import type { AccountState, SupplierAccountNow } from '../suppliers/contract.ts';
|
|
14
26
|
export declare class AccountPool {
|
|
15
|
-
|
|
27
|
+
/** 本池所属供应商(loader 每供应商一个实例;键里带上,跨供应商同名模型不串)。 */
|
|
28
|
+
private supplierId;
|
|
29
|
+
/** (supplier, model, uid) → 冷却/退避。 */
|
|
30
|
+
private cooldowns;
|
|
31
|
+
/** uid → 连接级禁用 / 手动暂停。 */
|
|
32
|
+
private byUid;
|
|
16
33
|
private rrCursor;
|
|
34
|
+
constructor(supplierId?: string);
|
|
35
|
+
private key;
|
|
17
36
|
private entry;
|
|
18
|
-
/**
|
|
37
|
+
/** 某连接整体状态(禁用/手动暂停)。 */
|
|
38
|
+
private uidState;
|
|
39
|
+
/** 该连接是否被禁用 / 手动整连接暂停中(跨模型生效)。 */
|
|
40
|
+
private uidBlocked;
|
|
41
|
+
/**
|
|
42
|
+
* 给定要试的模型,该连接是否可服务:
|
|
43
|
+
* - 未被连接级禁用/手动暂停(跨模型);
|
|
44
|
+
* - 且此 (supplier, model, uid) 冷却单元不在冷却中。
|
|
45
|
+
*/
|
|
19
46
|
private healthy;
|
|
20
47
|
/**
|
|
21
|
-
*
|
|
48
|
+
* 按策略为「某个模型」选一个健康账号。
|
|
22
49
|
* @param accounts 插件报告的「现在状态」(顺序即插件的自然顺序)
|
|
23
50
|
* @param poolOrder 用户在面板拖出来的顺序(核心管)
|
|
51
|
+
* @param strategy fallback 取第一个健康 / round-robin 轮转健康号
|
|
52
|
+
* @param modelId 当前要路由的模型(决定查哪个 (model, uid) 冷却单元)
|
|
24
53
|
* @returns 选中的 uid;无健康账号返回 undefined
|
|
25
54
|
*/
|
|
26
|
-
pick(accounts: SupplierAccountNow[], poolOrder: string[], strategy: string): string | undefined;
|
|
27
|
-
/**
|
|
28
|
-
noteFailure(uid: string, state: AccountState, message: string): void;
|
|
29
|
-
/**
|
|
30
|
-
noteSuccess(uid: string): void;
|
|
31
|
-
/**
|
|
55
|
+
pick(accounts: SupplierAccountNow[], poolOrder: string[], strategy: string, modelId: string): string | undefined;
|
|
56
|
+
/** 记录一次失败:按状态处置(冷却/禁用/退避),落在「(模型, 连接)」这个冷却单元上。 */
|
|
57
|
+
noteFailure(uid: string, modelId: string, state: AccountState, message: string): void;
|
|
58
|
+
/** 记录一次成功:清零「该模型」的限流退避等级(好号不背历史惩罚)。不解除禁用。 */
|
|
59
|
+
noteSuccess(uid: string, modelId: string): void;
|
|
60
|
+
/** 手动暂停整连接(面板/管理用途,跨模型)。ms<=0 表示解除。 */
|
|
32
61
|
cooldown(uid: string, ms: number, reason: string): void;
|
|
33
|
-
/**
|
|
34
|
-
|
|
62
|
+
/** 把核心的冷却/禁用叠加到插件报的「现在状态」上,产出面板态。
|
|
63
|
+
*
|
|
64
|
+
* 冷却现在是 (模型, 连接) 粒度的,而面板状态没有「当前模型」上下文,
|
|
65
|
+
* 这里默认**聚合**:只要该连接在任一模型上有活跃冷却,就显示 cooling=true
|
|
66
|
+
* (reason 带上具体模型),并在 `until` 给最长者。真实路由用 `pick` 的
|
|
67
|
+
* 模型级判定,不受此展示聚合影响。传 modelId 可只按该模型判定。
|
|
68
|
+
*/
|
|
69
|
+
decorate(accounts: SupplierAccountNow[], modelId?: string): Array<SupplierAccountNow & {
|
|
35
70
|
cooling: boolean;
|
|
36
71
|
disabled: boolean;
|
|
37
72
|
err_count: number;
|
|
@@ -13,6 +13,11 @@ export declare class Router {
|
|
|
13
13
|
private store;
|
|
14
14
|
/** 模型列表缓存(supplierId → 模型 + 拉取时间)。插件只管拉,不缓存。 */
|
|
15
15
|
private modelsCache;
|
|
16
|
+
/**
|
|
17
|
+
* 正在拉模型的供应商 → in-flight promise。
|
|
18
|
+
* 组合页会同时从多个入口触发同一个供应商的刷新,不去重上游就被按 N 倍打。
|
|
19
|
+
*/
|
|
20
|
+
private modelsInflight;
|
|
16
21
|
/** 请求日志出口(面板/宿主 logger)。 */
|
|
17
22
|
private log;
|
|
18
23
|
/** 用量统计(概览看板)。 */
|
|
@@ -50,14 +55,27 @@ export declare class Router {
|
|
|
50
55
|
* 取某供应商的模型(核心统一缓存)。
|
|
51
56
|
*
|
|
52
57
|
* 模型列表只在这里缓存一次:`/suppliers/:id/models`(供应商详情)和
|
|
53
|
-
* `/combos
|
|
54
|
-
* 冷启动(缓存还没建)时才拉一次;真正刷新由详情页打开或「获取模型」
|
|
55
|
-
* 按钮(force)触发。
|
|
58
|
+
* `/combos`(组合加模型)共用。
|
|
56
59
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
60
|
+
* **过期后走 stale-while-revalidate**:先返回旧值让面板立刻有内容,后台
|
|
61
|
+
* 重新拉,拉完下次请求就是新的。为什么必须这样:插件的 `listModels` 大多
|
|
62
|
+
* 无条件打上游(实测冷路径 0.3~1.0s/供应商),而组合页要拉全部供应商、
|
|
63
|
+
* 取最慢那家——同步等就是 1s+ 的白屏。旧值只可能「少列了新模型」,
|
|
64
|
+
* 比白屏划算;想立刻看新模型点「获取模型」(force)即可。
|
|
65
|
+
*
|
|
66
|
+
* 只有两种情况会真的等:`force`(用户主动刷新)和**从来没有过缓存值**
|
|
67
|
+
* (冷启动,没有旧值可给)。后者只发生一次。
|
|
59
68
|
*/
|
|
60
69
|
modelsOf(supplierId: string, force?: boolean): Promise<ModelWithEnabled[]>;
|
|
70
|
+
/**
|
|
71
|
+
* 真拉一次上游并写缓存。并发调用共享同一个 in-flight promise——
|
|
72
|
+
* 组合页会同时触发多个入口,不去重会把上游按 N 倍打(各家插件自己没做这层)。
|
|
73
|
+
*
|
|
74
|
+
* 失败时:**有旧值就退回旧值**(宁可显示几分钟前的列表,也别让面板空掉),
|
|
75
|
+
* **没旧值就把错误抛回去**——调用方(`supplierModels`)据此把该供应商整个
|
|
76
|
+
* 剔除,而不是显示一个「有 0 个模型」的空壳。
|
|
77
|
+
*/
|
|
78
|
+
private refreshModels;
|
|
61
79
|
/** 失效某供应商的模型缓存(增删改模型后调用)。 */
|
|
62
80
|
invalidateModels(supplierId: string): void;
|
|
63
81
|
/** 可用模型(按供应商分组,仅启用),面板加模型用。
|
|
@@ -117,6 +135,11 @@ export declare class Router {
|
|
|
117
135
|
* 不再挨个问供应商「这是不是你的模型」——精准调用,也避免同名模型串台。
|
|
118
136
|
* 兼容旧的裸 `modelId`(没有逗号):降级为遍历,行为同以前。
|
|
119
137
|
*/
|
|
138
|
+
/**
|
|
139
|
+
* @param outTrace 可选出参:把这次调用的追踪信息带回去(组合据此判断
|
|
140
|
+
* 要不要「喘口气」再降级)。裸 id 遍历路径不填(多供应商混在一起,
|
|
141
|
+
* 失败状态没有单一归属)。
|
|
142
|
+
*/
|
|
120
143
|
private chatWithModel;
|
|
121
144
|
/** 按别名找供应商(对外模型全名 = alias/model)。别名唯一,故最多命中一个。 */
|
|
122
145
|
supplierByAlias(alias: string): Supplier | undefined;
|
|
@@ -72,8 +72,6 @@ export interface Supplier {
|
|
|
72
72
|
accounts(): SupplierAccountNow[];
|
|
73
73
|
/** 对单个账号调一次上游。不遍历账号、不管冷却、不写 res。 */
|
|
74
74
|
chatOnce(uid: string, req: ChatRequest): Promise<ChatOnceResult>;
|
|
75
|
-
/** 上次 chatOnce 失败原因(诊断用,可选)。 */
|
|
76
|
-
lastError?(): string | undefined;
|
|
77
75
|
/** 删除链接(清理凭证;内部能力,js 契约不要求实现)。 */
|
|
78
76
|
removeLink?(uid: string): Promise<boolean>;
|
|
79
77
|
dispose(): void;
|
|
@@ -41,11 +41,28 @@ export declare class UsageStore {
|
|
|
41
41
|
ts?: number;
|
|
42
42
|
}, usage: Usage): void;
|
|
43
43
|
private bump;
|
|
44
|
-
/**
|
|
45
|
-
|
|
44
|
+
/**
|
|
45
|
+
* 从 `first` 起的 24 个小时格:每格带整点时间戳、所属自然日、那天的数据。
|
|
46
|
+
*
|
|
47
|
+
* `today` 传今天 00:00,`24h` 传「当前整点往前 23 格」。
|
|
48
|
+
* 跨天一律靠 `localDateKey(start)` 现算,不用「减 N 小时」去猜天数:
|
|
49
|
+
* 本地时区(尤其 DST)下小时偏移和自然日不是一回事,算错就漏一整天。
|
|
50
|
+
*/
|
|
51
|
+
private hourSlots;
|
|
52
|
+
/** 今天的小时格起点(自然日零点)。 */
|
|
53
|
+
private static dayStartOf;
|
|
54
|
+
/** 24h 滚动窗口的小时格起点(当前整点往前 23 格)。 */
|
|
55
|
+
private static rollingStartOf;
|
|
56
|
+
/**
|
|
57
|
+
* 一整天的小时桶是否齐(各格之和 >= 天桶计数)。
|
|
58
|
+
*
|
|
59
|
+
* 不齐 = 这天是升级前落的盘(没有小时粒度)→ 24h 窗口踩到它就只能整桶计入。
|
|
60
|
+
* 宁可多算窗口外那截,也不能报一个比「今日」还小的数。
|
|
61
|
+
*/
|
|
62
|
+
private static hoursComplete;
|
|
46
63
|
/** 汇总 + Top 榜。 */
|
|
47
64
|
stats(period: Period, now?: number): StatsResult;
|
|
48
|
-
/** 柱状图数据:today/24h = 24
|
|
65
|
+
/** 柱状图数据:today/24h = 24 个小时桶,7d/30d = 天桶。 */
|
|
49
66
|
chart(period: Period, now?: number): ChartBucket[];
|
|
50
67
|
/** 最近请求明细。 */
|
|
51
68
|
recentList(limit?: number): UsageRecord[];
|
package/lib/types/shared.d.ts
CHANGED
|
@@ -1,10 +1,21 @@
|
|
|
1
1
|
export type PoolStrategy = 'fallback' | 'round-robin';
|
|
2
|
+
/**
|
|
3
|
+
* 积分哨兵:插件还**没拿到过**真实额度(刚重启、积分拉取失败、或压根不支持
|
|
4
|
+
* 积分)时报 `-1`,核心据此保留上一次的缓存值,而不是把 0 写进去。
|
|
5
|
+
*
|
|
6
|
+
* 为什么不能用 0 表示未知:积分 0 是有意义的真值(用完了),核心无法区分
|
|
7
|
+
* 「拿到 0」和「没拿到」。用 0 当哨兵会把缓存冲成 0 —— 这正是 codebuddy
|
|
8
|
+
* 重启后面板永久显示 0 积分的原因(插件只在内存里缓存积分)。
|
|
9
|
+
*/
|
|
10
|
+
export declare const CREDITS_UNKNOWN = -1;
|
|
2
11
|
export interface SupplierConfig {
|
|
3
12
|
alias: string;
|
|
4
13
|
disabled: string[];
|
|
5
14
|
custom: string[];
|
|
6
15
|
poolOrder: string[];
|
|
7
16
|
poolStrategy: PoolStrategy;
|
|
17
|
+
/** 积分缓存:uid → 剩余额度(最后一次从插件拿到的非 -1 值)。 */
|
|
18
|
+
credits: Record<string, number>;
|
|
8
19
|
}
|
|
9
20
|
/** 通用供应商配置存储。 */
|
|
10
21
|
export declare class SupplierConfigStore {
|
|
@@ -34,6 +45,18 @@ export declare class SupplierConfigStore {
|
|
|
34
45
|
removeCustomModel(supplierId: string, modelId: string): void;
|
|
35
46
|
setPoolOrder(supplierId: string, uids: string[]): void;
|
|
36
47
|
setPoolStrategy(supplierId: string, strategy: string): void;
|
|
48
|
+
/**
|
|
49
|
+
* 读缓存的积分。没缓存过返回 `CREDITS_UNKNOWN`(-1),让调用方自己决定
|
|
50
|
+
* 显示什么——不要把 -1 当 0 展示。
|
|
51
|
+
*/
|
|
52
|
+
getCredits(supplierId: string, uid: string): number;
|
|
53
|
+
/**
|
|
54
|
+
* 合并插件报的积分:非 -1 就写入并返回它;-1(未知)则回落到上次缓存值。
|
|
55
|
+
* @returns 面板该显示的积分(仍可能是 -1 = 从来没拿到过)
|
|
56
|
+
*/
|
|
57
|
+
putCredits(supplierId: string, uid: string, reported: number): number;
|
|
58
|
+
/** 删除链接时清掉它的积分缓存(不写盘:下一行 removeLink 会带出保存)。 */
|
|
59
|
+
clearCredits(supplierId: string, uid: string): void;
|
|
37
60
|
private load;
|
|
38
61
|
private saveLocked;
|
|
39
62
|
}
|
|
@@ -46,6 +46,12 @@ export type AccountState = 'ok' | 'rate_limit' | 'quota' | 'session_dead' | 'una
|
|
|
46
46
|
export interface SupplierAccountNow {
|
|
47
47
|
uid: string;
|
|
48
48
|
nickname?: string;
|
|
49
|
+
/**
|
|
50
|
+
* 剩余额度。**拿不到就报 `-1`**,不是 0:
|
|
51
|
+
* 插件刚重启(内存缓存空)、积分拉取失败、或压根不支持积分时都是 `-1`。
|
|
52
|
+
* 核心据此保留上一次持久化下来的值 —— 报 0 会把缓存冲成 0(0 是真值
|
|
53
|
+
* 「用完了」,核心分不清「拿到 0」和「没拿到」)。
|
|
54
|
+
*/
|
|
49
55
|
credits: number;
|
|
50
56
|
/** 插件观察到的上游状态。 */
|
|
51
57
|
state: AccountState;
|
|
@@ -91,19 +97,16 @@ export interface SupplierModule {
|
|
|
91
97
|
status(): SupplierStatusNow;
|
|
92
98
|
/** 模型来源。`force=true` 时强制刷新来源(核心「获取模型」按钮调用)。 */
|
|
93
99
|
listModels(force?: boolean): Promise<ModelInfo[]> | ModelInfo[];
|
|
94
|
-
/** 默认前缀(可被通用层 alias 覆盖)。 */
|
|
95
|
-
getAlias(): string;
|
|
96
100
|
/**
|
|
97
101
|
* 对**单个账号**调一次上游。插件不遍历账号、不管冷却、不写 res——
|
|
98
102
|
* 选号/回退/健康判定全是核心的活。
|
|
99
103
|
* 无账号供应商(如 opencode)传空 uid,忽略即可。
|
|
104
|
+
*
|
|
105
|
+
* 失败原因通过返回值的 `message` 报(核心在测试模型时直接用它做诊断提示),
|
|
106
|
+
* 插件不必自己存一份「上次失败」的状态。
|
|
100
107
|
*/
|
|
101
108
|
chatOnce(uid: string, req: ChatRequest): Promise<ChatOnceResult>;
|
|
102
109
|
dispose(): void;
|
|
103
|
-
/** 上次 chatOnce 失败的原因(诊断用)。
|
|
104
|
-
* 测试模型由 dsh-router 核心统一走 chatOnce 路径(账号池回退/冷却自动生效),
|
|
105
|
-
* 插件只需把失败原因暴露出来供核心汇总;不实现则测试失败时只有通用提示。 */
|
|
106
|
-
lastError?(): string | undefined;
|
|
107
110
|
/** 生成登录链接(加账号)。有它才显示连接池(账号池)。 */
|
|
108
111
|
generateLoginUrl?(): string | {
|
|
109
112
|
ok: boolean;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-router-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
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": {
|
|
@@ -117,7 +117,7 @@ export function RouterView({ onBack }: RouterViewProps): JSX.Element {
|
|
|
117
117
|
</div>
|
|
118
118
|
<div className="dshr-supplierMeta">
|
|
119
119
|
{supplierAccounts.length > 0 && (
|
|
120
|
-
<span className="dshr-muted">{supplierAccounts.length}
|
|
120
|
+
<span className="dshr-muted">{healthy}/{supplierAccounts.length} 健康</span>
|
|
121
121
|
)}
|
|
122
122
|
<span className="dshr-chevron">›</span>
|
|
123
123
|
</div>
|
|
@@ -184,20 +184,20 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
184
184
|
setCheckingIn(true)
|
|
185
185
|
try {
|
|
186
186
|
const response = await fetch(`${ROUTER_API_BASE}/suppliers/${supplier.id}/checkin`, { method: 'POST', cache: 'no-store' })
|
|
187
|
-
const data = await response.json() as { ok: boolean; total: number; succeeded: number; already?: number; error?: string; results?: Array<{ uid: string; ok: boolean; status?: string; message?: string }> }
|
|
187
|
+
const data = await response.json() as { ok: boolean; total: number; succeeded: number; already?: number; failed?: number; error?: string; results?: Array<{ uid: string; ok: boolean; status?: string; message?: string }> }
|
|
188
|
+
// 核心口径(2026-09-02 修正):有任一链接失败 → ok:false + HTTP 400。
|
|
189
|
+
// 别再用「签到完成」打头——部分失败会被 UI 吞成成功。
|
|
188
190
|
if (data.ok && data.total > 0) {
|
|
189
|
-
// 真实语义:succeeded = 真正 claim 成功;already = 今日已签;其余 = 失败/未开放
|
|
190
|
-
const failed = (data.results ?? []).filter(r => r.status !== 'ok' && r.status !== 'already')
|
|
191
191
|
let msg = `签到完成:${data.succeeded}/${data.total} 成功`
|
|
192
192
|
const already = data.already ?? 0
|
|
193
193
|
if (already > 0) msg += ` · ${already} 今日已签`
|
|
194
|
-
if (failed.length > 0) {
|
|
195
|
-
const first = failed[0]
|
|
196
|
-
msg += ` · ${first?.message ?? '有失败'}`
|
|
197
|
-
}
|
|
198
194
|
showToast(msg)
|
|
199
195
|
} else {
|
|
200
|
-
|
|
196
|
+
const firstFail = (data.results ?? []).find(r => r.status !== 'ok' && r.status !== 'already')
|
|
197
|
+
const reason = firstFail ? `${firstFail.message ?? '有链接签到失败'}` : ''
|
|
198
|
+
const already = data.already ?? 0
|
|
199
|
+
const head = data.total > 0 ? `签到未完成:${data.succeeded}/${data.total} 成功` : '没有可签到的链接'
|
|
200
|
+
showToast(data.error ?? [head, already > 0 ? `${already} 今日已签` : '', reason].filter(Boolean).join(' · '))
|
|
201
201
|
}
|
|
202
202
|
onRefresh()
|
|
203
203
|
} catch (err) {
|
|
@@ -673,11 +673,16 @@ export function SupplierDetail({ supplier, accounts, statusLoading, onBack, onRe
|
|
|
673
673
|
<div className="dshr-linkSub">
|
|
674
674
|
<span className="dshr-mono dshr-linkUid">{account.uid}</span>
|
|
675
675
|
<span className="dshr-linkDot">·</span>
|
|
676
|
-
<
|
|
676
|
+
{/* credits < 0 = 插件还没拉到过积分(缓存也没值),
|
|
677
|
+
别显示成「-1 积分」或直接吃掉整段 */}
|
|
678
|
+
<span className="dshr-mono dshr-linkCredits">
|
|
679
|
+
{account.credits < 0 ? '积分未知' : `${Math.ceil(account.credits)} 积分`}
|
|
680
|
+
</span>
|
|
681
|
+
{/* err_count 是限流退避等级(反复被限流的号递增,成功后清零) */}
|
|
677
682
|
{account.err_count !== undefined && account.err_count > 0 && (
|
|
678
683
|
<>
|
|
679
684
|
<span className="dshr-linkDot">·</span>
|
|
680
|
-
<span className="dshr-linkErr"
|
|
685
|
+
<span className="dshr-linkErr">退避 {account.err_count} 级</span>
|
|
681
686
|
</>
|
|
682
687
|
)}
|
|
683
688
|
{coolingText !== '' && <span className="dshr-reason dshr-linkCooling">{coolingText}</span>}
|