dsh-router-core 0.3.2 → 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/README.md CHANGED
@@ -71,7 +71,11 @@ dsh plugin --profile web add dsh-router-core
71
71
  - Top 榜:按供应商 / 按模型(请求数带失败计数);
72
72
  - 最近请求:时间 / 模型 / 供应商 / in↑ out↓ / 耗时,显示最近 10 条;
73
73
  - **清空** — 清掉全部用量统计(不影响供应商、账号、组合配置);
74
- - 数据落盘 `data/usage.json`(按天聚合 + 最近 500 条明细 + 累计计数)。
74
+ - 数据落盘 `data/usage.json`(按天聚合 + 每天 24 个小时桶 + 最近 500 条明细 +
75
+ 累计计数)。今日/7 天/30 天读天桶、24 小时读小时桶,都不受明细环容量限制;
76
+ 明细环只服务「最近请求」列表。
77
+ 小时桶从新数据开始累积,升级前那几天的天内分布查不到(明细环只剩 500 条
78
+ 回溯不回去),那段历史的小时柱状图留空、24 小时口径按整桶计入 —— 不编数据。
75
79
  token 口径:上游返回 `usage` 就用真值(分散在多帧时按字段取最大值合并);
76
80
  上游不发时按 ~4 字符/token 估算,面板上标 `~`。**失败请求不估算**——
77
81
  它没到上游,编造输入 token 只会把总量灌水;
package/docs/suppliers.md CHANGED
@@ -76,7 +76,7 @@ js 报告**此刻**观察到的账户状况,而不是把上游原始响应丢给
76
76
 
77
77
  **`no_such_model` 是必须的,不能拿 `unavailable` 顶替。** 后者在核心策略里
78
78
  会计一次连续错误——组合里每有一个别人家的模型,就会给无关账号攒一次错误,
79
- 攒够 3 次把它冷却 10 分钟,表现为「组合越用越没号可用」。所以「这个模型不属于我」
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 次冷却 10 分钟 |
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 次冷却 10 分钟,表现为「组合越用越没号可用」。
315
+ 模型就给无关账号攒一次,攒够 3 次冷却 5 分钟,表现为「组合越用越没号可用」。
300
316
 
301
317
  存了供应商 id 就不需要问了:直接调那一个。查不到供应商(拼错/供应商没加载)
302
318
  直接失败,**不兜底遍历**——兜底会让拼错的 id 静默落到别的供应商上,更难查。
@@ -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 showToast(data.error ?? (data.total === 0 ? "没有可签到的链接" : "签到失败"));
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}`);
@@ -3392,9 +3397,9 @@ window.__ModuleLoader__.load({
3392
3397
  children: [supplierAccounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3393
3398
  className: "dshr-muted",
3394
3399
  children: [
3395
- supplierAccounts.length,
3396
- "/",
3397
3400
  healthy,
3401
+ "/",
3402
+ supplierAccounts.length,
3398
3403
  " 健康"
3399
3404
  ]
3400
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 showToast(data.error ?? (data.total === 0 ? "没有可签到的链接" : "签到失败"));
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}`);
@@ -3392,9 +3397,9 @@ window.__ModuleLoader__.load({
3392
3397
  children: [supplierAccounts.length > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3393
3398
  className: "dshr-muted",
3394
3399
  children: [
3395
- supplierAccounts.length,
3396
- "/",
3397
3400
  healthy,
3401
+ "/",
3402
+ supplierAccounts.length,
3398
3403
  " 健康"
3399
3404
  ]
3400
3405
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {