dsh-router-core 0.3.1 → 0.3.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/LICENSE +21 -0
- package/README.md +77 -47
- package/docs/screenshot.png +0 -0
- package/docs/suppliers.md +19 -3
- package/lib/client-registry.js +21 -10
- package/lib/client.js +21 -10
- package/lib/index.js +611 -215
- 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 +5 -0
- package/lib/types/router/usage-store.d.ts +20 -3
- package/lib/types/shared.d.ts +2 -0
- package/package.json +1 -1
- package/src/client/RouterView.tsx +10 -2
- package/src/client/SupplierDetail.tsx +17 -9
- 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 +137 -38
- package/src/router/models-cache.test.ts +87 -4
- package/src/router/precise-dispatch.test.ts +110 -7
- package/src/router/usage-store.ts +174 -74
- package/src/router/usage.test.ts +115 -0
- package/src/shared.ts +2 -0
- package/src/suppliers/loader.ts +2 -1
- package/src/suppliers/nvidia/plugin.ts +8 -2
- package/src/suppliers/registry.test.ts +23 -3
- package/src/suppliers/registry.ts +7 -2
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 CARVIN
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
|
-
|
|
1
|
+
<h1 align="center">dsh-router</h1>
|
|
2
|
+
|
|
3
|
+
<p align="center">DeepSeek Harness 的 OpenAI 兼容路由插件</p>
|
|
4
|
+
|
|
5
|
+
<p align="center">
|
|
6
|
+
<a href="https://www.npmjs.com/package/dsh-router-core"><img src="https://img.shields.io/npm/v/dsh-router-core?style=flat-square&logo=npm&label=npm" alt="npm version"></a>
|
|
7
|
+
<a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-10b981?style=flat-square" alt="MIT license"></a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="#快速安装">快速安装</a> ·
|
|
12
|
+
<a href="#面板设置--路由">面板</a> ·
|
|
13
|
+
<a href="#api-端点openai-兼容3080v1">API 端点</a> ·
|
|
14
|
+
<a href="docs/suppliers.md">供应商开发</a>
|
|
15
|
+
</p>
|
|
2
16
|
|
|
3
17
|
**插件版的 9router** —— 不是另开一个网关服务,而是直接作为 DSH 插件嵌进 DSH web,
|
|
4
18
|
在 `http://localhost:3080/v1` 上原生暴露 OpenAI 兼容端点,把请求路由到内部供应商。
|
|
@@ -7,30 +21,44 @@
|
|
|
7
21
|
|
|
8
22
|

|
|
9
23
|
|
|
10
|
-
##
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- **凭证 SQLite 单库**:`auths/credentials.sqlite`,供应商凭证不透明 blob,
|
|
27
|
-
核心统一生命周期,干净可备份;
|
|
28
|
-
- **复用 9router 思路**:面板布局、组合 fallback、连接池/账号池、API key
|
|
29
|
-
管理都贴近 9router,但按 DSH「一切皆插件」的方式重组得更轻。
|
|
24
|
+
## 快速安装
|
|
25
|
+
|
|
26
|
+
需要支持 `dsh plugin` profile 插件机制的 DeepSeek Harness、Node.js `>= 20`,以及 `web` profile。
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
dsh plugin --profile web add dsh-router-core
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
然后**重启 `dsh web`**。打开设置面板,左侧导航「模型」下面会出现 **路由**。
|
|
33
|
+
|
|
34
|
+
> 更多供应商:DSH 插件形态的供应商各自发 npm 包,同样
|
|
35
|
+
> `dsh plugin --profile web add <包名>` 即可;供应商接入与开发见
|
|
36
|
+
> [`docs/suppliers.md`](docs/suppliers.md)。
|
|
37
|
+
>
|
|
38
|
+
> 本地开发版:不用 npm,直接 `dependencies` 加
|
|
39
|
+
> `"dsh-router-core": "link:/path/to/dsh-router"` 指向本地仓库。
|
|
30
40
|
|
|
41
|
+
## 它解决什么问题
|
|
42
|
+
|
|
43
|
+
| 能力 | 说明 |
|
|
44
|
+
|---|---|
|
|
45
|
+
| 零额外进程 | 就是 DSH 插件,随 `dsh web` 启停,天然同源(`/router/api/*` 无 CORS、面板嵌在设置里)。 |
|
|
46
|
+
| 供应商即插即拔 | 内置供应商随插件分发;更多供应商 = 装一个 DSH 插件(`dsh-router-*`)或放一个 js 文件到 `~/.dsh/profiles/web/suppliers/`。 |
|
|
47
|
+
| 模型不内置 | 供应商只实现差异化能力,模型拉取与缓存由核心统一管,不写死、不过时。 |
|
|
48
|
+
| 策略只写一次 | 组合回退、账号池(选号/冷却/禁用/连续错误累计)、响应写入、凭证存储、模型管理都由核心提供。供应商 js 只对**单个账号**调一次上游并报告成败,不自己遍历账号、不维护冷却表——否则每个插件都会长出一份互相不一致的实现,而核心也就无从判断「该不该换号」。 |
|
|
49
|
+
| 凭证单库 | `auths/credentials.sqlite`,供应商凭证不透明 blob,核心统一生命周期,干净可备份。 |
|
|
50
|
+
| 组合即模型 | 建好的组合自动带出为 DSH 模型目录里的 `router` provider 选项,设置 → 模型直接选组合名即可。 |
|
|
51
|
+
| 用量可观测 | 面板概览看板:周期切换、汇总卡、趋势折线、Top 榜、最近请求。 |
|
|
52
|
+
|
|
53
|
+
> 面板布局、组合 fallback、连接池/账号池、API key 管理都贴近
|
|
54
|
+
> [9router](https://github.com/decolua/9router),但按 DSH「一切皆插件」的方式
|
|
55
|
+
> 重组得更轻。
|
|
56
|
+
>
|
|
31
57
|
> 供应商开发与接入规范见 [`docs/suppliers.md`](docs/suppliers.md)
|
|
32
58
|
> (契约 / 加载顺序 / 模型统一策略 / 内置供应商参考实现)。
|
|
33
59
|
|
|
60
|
+
## 面板(设置 → 路由)
|
|
61
|
+
|
|
34
62
|
面板挂在 **设置 → 路由**(官方 `settings.section` 座位,排在「模型」下面):
|
|
35
63
|
|
|
36
64
|
- **概览** — 用量看板(默认页):
|
|
@@ -43,7 +71,11 @@
|
|
|
43
71
|
- Top 榜:按供应商 / 按模型(请求数带失败计数);
|
|
44
72
|
- 最近请求:时间 / 模型 / 供应商 / in↑ out↓ / 耗时,显示最近 10 条;
|
|
45
73
|
- **清空** — 清掉全部用量统计(不影响供应商、账号、组合配置);
|
|
46
|
-
- 数据落盘 `data/usage.json`(按天聚合 + 最近 500 条明细 +
|
|
74
|
+
- 数据落盘 `data/usage.json`(按天聚合 + 每天 24 个小时桶 + 最近 500 条明细 +
|
|
75
|
+
累计计数)。今日/7 天/30 天读天桶、24 小时读小时桶,都不受明细环容量限制;
|
|
76
|
+
明细环只服务「最近请求」列表。
|
|
77
|
+
小时桶从新数据开始累积,升级前那几天的天内分布查不到(明细环只剩 500 条
|
|
78
|
+
回溯不回去),那段历史的小时柱状图留空、24 小时口径按整桶计入 —— 不编数据。
|
|
47
79
|
token 口径:上游返回 `usage` 就用真值(分散在多帧时按字段取最大值合并);
|
|
48
80
|
上游不发时按 ~4 字符/token 估算,面板上标 `~`。**失败请求不估算**——
|
|
49
81
|
它没到上游,编造输入 token 只会把总量灌水;
|
|
@@ -162,32 +194,14 @@ curl -X POST http://localhost:3080/v1/chat/completions \
|
|
|
162
194
|
| `TW2A_STATE_FILE` | `data/state.json` | 状态持久化(同目录放 keys.json / combos.json / supplier-config.json) |
|
|
163
195
|
| `TW2A_API_KEY` | 空 | /v1 Bearer 鉴权(与库内 key 等效) |
|
|
164
196
|
|
|
165
|
-
##
|
|
166
|
-
|
|
167
|
-
```bash
|
|
168
|
-
pnpm install
|
|
169
|
-
pnpm build # lib/index.js(host) + lib/client.js / lib/client-registry.js(browser)
|
|
170
|
-
pnpm typecheck
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
## 安装(DSH)
|
|
174
|
-
|
|
175
|
-
用 dsh CLI 装到 profile(web 是 DSH 插件宿主):
|
|
176
|
-
|
|
177
|
-
```bash
|
|
178
|
-
dsh plugin --profile web add dsh-router-core
|
|
179
|
-
```
|
|
197
|
+
## 与 DSH 的边界
|
|
180
198
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
> 即可;供应商接入与开发见 [`docs/suppliers.md`](docs/suppliers.md)。
|
|
188
|
-
>
|
|
189
|
-
> 本地开发版:不用 npm,直接 `dependencies` 加
|
|
190
|
-
> `"dsh-router-core": "link:/path/to/dsh-router"` 指向本地仓库。
|
|
199
|
+
- dsh-router 复用 DSH 的 Web Server 与设置面板座位,不启动第二个应用或代理系统。
|
|
200
|
+
- 供应商 js 不改 DSH 的 prompt、工具 schema 或权限;它只负责「把上游协议翻译成
|
|
201
|
+
OpenAI 形态」,路由/回退/存储归核心。
|
|
202
|
+
- 数据分两处:`data/` 下的状态与用量 JSON(删了只是没统计了),以及
|
|
203
|
+
`auths/credentials.sqlite`(删了要重新登录所有供应商)。
|
|
204
|
+
- 内置 patch 仅支持 DSH 的 `web` profile。
|
|
191
205
|
|
|
192
206
|
## 前提
|
|
193
207
|
|
|
@@ -195,6 +209,18 @@ dsh plugin --profile web add dsh-router-core
|
|
|
195
209
|
- 供应商接入与开发见 [`docs/suppliers.md`](docs/suppliers.md);
|
|
196
210
|
- 重启 DSH 后 `/v1/*` 即生效;面板管理账号、模型与密钥。
|
|
197
211
|
|
|
212
|
+
## 开发
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
pnpm install
|
|
216
|
+
pnpm build # lib/index.js(host) + lib/client.js / lib/client-registry.js(browser)
|
|
217
|
+
pnpm typecheck
|
|
218
|
+
pnpm test # node --test "src/**/*.test.ts"
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
需要一个供应商最小实现作参考时,看 [`examples/suppliers/echo.js`](examples/suppliers/echo.js);
|
|
222
|
+
完整契约、加载顺序与模型策略见 [`docs/suppliers.md`](docs/suppliers.md)。
|
|
223
|
+
|
|
198
224
|
## 致谢
|
|
199
225
|
|
|
200
226
|
感谢以下项目给的灵感:
|
|
@@ -202,3 +228,7 @@ dsh plugin --profile web add dsh-router-core
|
|
|
202
228
|
- [decolua/9router](https://github.com/decolua/9router) —— 本地 AI 路由网关,面板/组合/连接池/凭证等思路的来源;
|
|
203
229
|
- [deepseek-ai/deepseek-harness](https://github.com/deepseek-ai/deepseek-harness) —— DSH「一切皆插件」的宿主框架;
|
|
204
230
|
- [omdsh-dev/dsh-mnemon](https://github.com/omdsh-dev/dsh-mnemon) —— DSH 插件形态与侧边栏入口的参考。
|
|
231
|
+
|
|
232
|
+
## 许可证
|
|
233
|
+
|
|
234
|
+
[MIT](LICENSE)
|
package/docs/screenshot.png
CHANGED
|
Binary file
|
package/docs/suppliers.md
CHANGED
|
@@ -76,7 +76,7 @@ js 报告**此刻**观察到的账户状况,而不是把上游原始响应丢给
|
|
|
76
76
|
|
|
77
77
|
**`no_such_model` 是必须的,不能拿 `unavailable` 顶替。** 后者在核心策略里
|
|
78
78
|
会计一次连续错误——组合里每有一个别人家的模型,就会给无关账号攒一次错误,
|
|
79
|
-
攒够 3 次把它冷却
|
|
79
|
+
攒够 3 次把它冷却 5 分钟,表现为「组合越用越没号可用」。所以「这个模型不属于我」
|
|
80
80
|
一定要报 `no_such_model`,核心才知道该换供应商而不是换账号。
|
|
81
81
|
|
|
82
82
|
**js 只说「现在怎么了」,不说「该怎么办」。** 冷却多久、是否禁用、要不要换号
|
|
@@ -108,7 +108,7 @@ type ChatOnceResult =
|
|
|
108
108
|
| `rate_limit` | 冷却 1 分钟 |
|
|
109
109
|
| `quota` | 冷却 10 分钟 |
|
|
110
110
|
| `session_dead` | **永久禁用**(需重新登录才能恢复) |
|
|
111
|
-
| `unavailable` / `transport` / `unknown` | 计一次连续错误,攒够 3 次冷却
|
|
111
|
+
| `unavailable` / `transport` / `unknown` | 计一次连续错误,攒够 3 次冷却 5 分钟 |
|
|
112
112
|
| `no_such_model` | **不惩罚账号**,核心直接跳过整个供应商换下一个 |
|
|
113
113
|
|
|
114
114
|
- 非流式若聚合后发现**空响应**(无 content / reasoning / tool_calls),请返回
|
|
@@ -128,6 +128,22 @@ type ChatOnceResult =
|
|
|
128
128
|
- 要让「上游吐到一半才断」也能回退,必须在写响应头前缓冲判定,代价是
|
|
129
129
|
首字节延迟,目前没做
|
|
130
130
|
|
|
131
|
+
### 终止帧 `[DONE]` 由核心兜底
|
|
132
|
+
|
|
133
|
+
客户端(dsh-llm adapter)严格等 `data: [DONE]` 才认为流正常结束;缺了就抛
|
|
134
|
+
`SSE payload stream ended without [DONE]`,整轮判失败——已经流式吐出去的内容
|
|
135
|
+
**全部作废**。
|
|
136
|
+
|
|
137
|
+
核心是 SSE 的唯一写入方,所以这个兜底由核心做,不要求 js 合成:
|
|
138
|
+
|
|
139
|
+
- 上游流结束时**没发过** `[DONE]` → 核心补发一个(降级为「拿到截断内容并
|
|
140
|
+
正常收尾」,而不是整轮白干)
|
|
141
|
+
- 上游**已经发了** → 不再重复发(重复 `[DONE]` 同样坑客户端)
|
|
142
|
+
- **一个字节都没写出就断** → **不补**,留给换号/换模型回退(补了就把这次
|
|
143
|
+
失败坐实成一个空响应,组合回退就没了)
|
|
144
|
+
|
|
145
|
+
js 因此不必自己合成终止帧(合成也不算错,核心会认)。
|
|
146
|
+
|
|
131
147
|
所以 js 的流转换要**尽早产出第一个字节**,别攒着。攒着不仅延迟高,还会把
|
|
132
148
|
自己的失败伪装成「可回退」,让组合以为没成功而反复重试别的模型。
|
|
133
149
|
|
|
@@ -296,7 +312,7 @@ export function dispose() {}
|
|
|
296
312
|
|
|
297
313
|
存裸 id 时,核心只能遍历所有供应商逐个试。插件用 `unavailable` 回答
|
|
298
314
|
「不是我的」,而这个状态在池策略里**计一次连续错误**——组合里每有一个别人家的
|
|
299
|
-
模型就给无关账号攒一次,攒够 3 次冷却
|
|
315
|
+
模型就给无关账号攒一次,攒够 3 次冷却 5 分钟,表现为「组合越用越没号可用」。
|
|
300
316
|
|
|
301
317
|
存了供应商 id 就不需要问了:直接调那一个。查不到供应商(拼错/供应商没加载)
|
|
302
318
|
直接失败,**不兜底遍历**——兜底会让拼错的 id 静默落到别的供应商上,更难查。
|
package/lib/client-registry.js
CHANGED
|
@@ -243,16 +243,21 @@ window.__ModuleLoader__.load({
|
|
|
243
243
|
cache: "no-store"
|
|
244
244
|
})).json();
|
|
245
245
|
if (data.ok && data.total > 0) {
|
|
246
|
-
const failed = (data.results ?? []).filter((r) => r.status !== "ok" && r.status !== "already");
|
|
247
246
|
let msg = `签到完成:${data.succeeded}/${data.total} 成功`;
|
|
248
247
|
const already = data.already ?? 0;
|
|
249
248
|
if (already > 0) msg += ` · ${already} 今日已签`;
|
|
250
|
-
if (failed.length > 0) {
|
|
251
|
-
const first = failed[0];
|
|
252
|
-
msg += ` · ${first?.message ?? "有失败"}`;
|
|
253
|
-
}
|
|
254
249
|
showToast(msg);
|
|
255
|
-
} else
|
|
250
|
+
} else {
|
|
251
|
+
const firstFail = (data.results ?? []).find((r) => r.status !== "ok" && r.status !== "already");
|
|
252
|
+
const reason = firstFail ? `${firstFail.message ?? "有链接签到失败"}` : "";
|
|
253
|
+
const already = data.already ?? 0;
|
|
254
|
+
const head = data.total > 0 ? `签到未完成:${data.succeeded}/${data.total} 成功` : "没有可签到的链接";
|
|
255
|
+
showToast(data.error ?? [
|
|
256
|
+
head,
|
|
257
|
+
already > 0 ? `${already} 今日已签` : "",
|
|
258
|
+
reason
|
|
259
|
+
].filter(Boolean).join(" · "));
|
|
260
|
+
}
|
|
256
261
|
onRefresh();
|
|
257
262
|
} catch (err) {
|
|
258
263
|
showToast(`签到失败:${err.message}`);
|
|
@@ -607,7 +612,10 @@ window.__ModuleLoader__.load({
|
|
|
607
612
|
children: supplier.icon !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
608
613
|
className: "dshr-providerImg",
|
|
609
614
|
src: supplier.icon,
|
|
610
|
-
alt: ""
|
|
615
|
+
alt: "",
|
|
616
|
+
onError: (e) => {
|
|
617
|
+
e.currentTarget.style.display = "none";
|
|
618
|
+
}
|
|
611
619
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
612
620
|
d: I$4.link,
|
|
613
621
|
size: 22
|
|
@@ -3376,7 +3384,10 @@ window.__ModuleLoader__.load({
|
|
|
3376
3384
|
children: [supplier.icon !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
3377
3385
|
className: "dshr-supplierIcon",
|
|
3378
3386
|
src: supplier.icon,
|
|
3379
|
-
alt: ""
|
|
3387
|
+
alt: "",
|
|
3388
|
+
onError: (e) => {
|
|
3389
|
+
e.currentTarget.style.display = "none";
|
|
3390
|
+
}
|
|
3380
3391
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3381
3392
|
className: "dshr-supplierName",
|
|
3382
3393
|
children: supplier.name
|
|
@@ -3386,9 +3397,9 @@ window.__ModuleLoader__.load({
|
|
|
3386
3397
|
children: [supplierAccounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3387
3398
|
className: "dshr-muted",
|
|
3388
3399
|
children: [
|
|
3389
|
-
supplierAccounts.length,
|
|
3390
|
-
"/",
|
|
3391
3400
|
healthy,
|
|
3401
|
+
"/",
|
|
3402
|
+
supplierAccounts.length,
|
|
3392
3403
|
" 健康"
|
|
3393
3404
|
]
|
|
3394
3405
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
package/lib/client.js
CHANGED
|
@@ -243,16 +243,21 @@ window.__ModuleLoader__.load({
|
|
|
243
243
|
cache: "no-store"
|
|
244
244
|
})).json();
|
|
245
245
|
if (data.ok && data.total > 0) {
|
|
246
|
-
const failed = (data.results ?? []).filter((r) => r.status !== "ok" && r.status !== "already");
|
|
247
246
|
let msg = `签到完成:${data.succeeded}/${data.total} 成功`;
|
|
248
247
|
const already = data.already ?? 0;
|
|
249
248
|
if (already > 0) msg += ` · ${already} 今日已签`;
|
|
250
|
-
if (failed.length > 0) {
|
|
251
|
-
const first = failed[0];
|
|
252
|
-
msg += ` · ${first?.message ?? "有失败"}`;
|
|
253
|
-
}
|
|
254
249
|
showToast(msg);
|
|
255
|
-
} else
|
|
250
|
+
} else {
|
|
251
|
+
const firstFail = (data.results ?? []).find((r) => r.status !== "ok" && r.status !== "already");
|
|
252
|
+
const reason = firstFail ? `${firstFail.message ?? "有链接签到失败"}` : "";
|
|
253
|
+
const already = data.already ?? 0;
|
|
254
|
+
const head = data.total > 0 ? `签到未完成:${data.succeeded}/${data.total} 成功` : "没有可签到的链接";
|
|
255
|
+
showToast(data.error ?? [
|
|
256
|
+
head,
|
|
257
|
+
already > 0 ? `${already} 今日已签` : "",
|
|
258
|
+
reason
|
|
259
|
+
].filter(Boolean).join(" · "));
|
|
260
|
+
}
|
|
256
261
|
onRefresh();
|
|
257
262
|
} catch (err) {
|
|
258
263
|
showToast(`签到失败:${err.message}`);
|
|
@@ -607,7 +612,10 @@ window.__ModuleLoader__.load({
|
|
|
607
612
|
children: supplier.icon !== void 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
608
613
|
className: "dshr-providerImg",
|
|
609
614
|
src: supplier.icon,
|
|
610
|
-
alt: ""
|
|
615
|
+
alt: "",
|
|
616
|
+
onError: (e) => {
|
|
617
|
+
e.currentTarget.style.display = "none";
|
|
618
|
+
}
|
|
611
619
|
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon$4, {
|
|
612
620
|
d: I$4.link,
|
|
613
621
|
size: 22
|
|
@@ -3376,7 +3384,10 @@ window.__ModuleLoader__.load({
|
|
|
3376
3384
|
children: [supplier.icon !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
|
|
3377
3385
|
className: "dshr-supplierIcon",
|
|
3378
3386
|
src: supplier.icon,
|
|
3379
|
-
alt: ""
|
|
3387
|
+
alt: "",
|
|
3388
|
+
onError: (e) => {
|
|
3389
|
+
e.currentTarget.style.display = "none";
|
|
3390
|
+
}
|
|
3380
3391
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
3381
3392
|
className: "dshr-supplierName",
|
|
3382
3393
|
children: supplier.name
|
|
@@ -3386,9 +3397,9 @@ window.__ModuleLoader__.load({
|
|
|
3386
3397
|
children: [supplierAccounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
3387
3398
|
className: "dshr-muted",
|
|
3388
3399
|
children: [
|
|
3389
|
-
supplierAccounts.length,
|
|
3390
|
-
"/",
|
|
3391
3400
|
healthy,
|
|
3401
|
+
"/",
|
|
3402
|
+
supplierAccounts.length,
|
|
3392
3403
|
" 健康"
|
|
3393
3404
|
]
|
|
3394
3405
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|