@dmh2002/dsh-connect-qoder 0.3.0 → 0.4.0
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/CHANGELOG.md +63 -0
- package/README.en.md +3 -3
- package/README.md +4 -4
- package/lib/bin.js +1 -1
- package/lib/{host-heartbeat-Cx880iUX.js → host-heartbeat-BNuj6Lt_.js} +76 -5
- package/lib/index.d.ts +71 -1
- package/lib/index.js +90 -24
- package/lib/{transfer-BepnYpWY.js → transfer-CUNaVvmi.js} +1 -1
- package/package.json +29 -27
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,69 @@
|
|
|
2
2
|
|
|
3
3
|
格式参照 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循 [语义化版本](https://semver.org/lang/zh-CN/)。
|
|
4
4
|
|
|
5
|
+
## 0.4.0 (2026-10-13)
|
|
6
|
+
|
|
7
|
+
0.3.0 发布后挖出的四条修复,外加一条有意的旧行为移除。四条里两条是 0.3.0 自己带出来的伤:**npm 上 0.3.0 的 peer 范围装不进当前 DSH**(装上即 `ERESOLVE`,必须尽快覆盖掉),以及**国际版需要的第二族令牌**;另两条是用户实拍回来的(思考强度间歇性消失、签到倒计时)。其中思考强度那条经历了一次返工 —— 第一版修复在生产路径上是**死代码**,险些带着假说明发布,经过与返工都记在条目里。
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **国际版积分与签到恒为 `credential-rejected` —— 国际版需要【两族并存】的令牌,而插件只有一族**(用户实拍;cn 侧完全正常,所以这个问题只在国际版显形)。
|
|
12
|
+
- **实测(本机两个真实 PAT,只读;唯一变动的是「怎么换令牌」)**:
|
|
13
|
+
|
|
14
|
+
| 面 | 认哪一族 | 国际版 + 旧令牌 | 国际版 + 新令牌 |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| chat / cosy(模型对话) | `/algo/api/v3/user/jobToken` 的 `securityOauthToken` | ✅ got-delta | ❌ 403 code 105 登录态失效 |
|
|
17
|
+
| sash / 额度(积分、签到) | `/api/v1/jobToken/exchange` 的 `token` | ❌ 401 TOKEN_EXPIRE | ✅ 200 |
|
|
18
|
+
|
|
19
|
+
国内版两族**等价**,所以这个差异在国内版永远看不出来 —— 这就是它反复查不出来的原因,也是「模型能用、只有积分读不到」这个奇怪组合的来源。
|
|
20
|
+
- **更早的两处误判**(记在这里,因为它们塑造了排查路径):先把 401 归因为「PAT 失效」,再归因为「国际版 sash 不开放该账号」。两者都被矩阵证伪 —— 截图显示网页端有 300 套餐 + 100 资源包积分,且换一枚新令牌后 sash 全线 200。
|
|
21
|
+
- **修法**:`QoderSessionHolder` 增加 `sashToken` / `sashExpireAtMs`;`exchangeJobToken` 里**额外**换一次 `/api/v1/jobToken/exchange`,`jobTokenGet`(用量)与 `sashRequest`(签到/活动)的 Authorization 改用它,chat / cosy 那一族原样不动。
|
|
22
|
+
- **必须在交换处 `await`**:放后台的话,紧接着的第一次读积分会带着空令牌去,拿到一个**与「真被拒」无法区分**的 401(实测:第一次 401、第二次 200)。这个坑本轮真实踩到了。
|
|
23
|
+
- **body 键是 `personal_token`**:`personal_access_token`(eghrhegpe/dsh-connect-qoder 用的拼写)在当前上游回 **400 BadRequest**,两个区域都是。
|
|
24
|
+
- **sash 交换失败不抛**:只让「积分读不到」,不让 chat / 对话跟着挂 —— 那是两条独立链路。
|
|
25
|
+
- **测试**:新增 `tests/live-intl-session.spec.ts`(**打真实上游,默认跳过**,`QODER_LIVE_TESTS=1` 打开)钉住 7 条事实:两区域各自的积分、签到、对话,外加两个区域不共用 baseUrl。改动前它精确失败在「global 积分 / 签到」两条而 chat 两条仍绿;改动后 7 条全绿。
|
|
26
|
+
- 另有 6 条桩测固定契约(两族都换、sash 用新令牌、失败不抛、body 键、以及**第一发就带对令牌**的竞态守卫),全部做过变异测试:回落旧令牌 / 换回 fire-and-forget 各自被接住。
|
|
27
|
+
- 参考实现:Liki4/qodercli2api、shuishuipingan/qoder2api-hub、eghrhegpe/dsh-connect-qoder(三者都走 `/api/v1/jobToken/exchange`)。
|
|
28
|
+
|
|
29
|
+
- **peer 范围静默排除整条 `0.2.0-rc.*` 宿主线 —— 从 npm 装到当前 DSH 就会 `ERESOLVE`。**
|
|
30
|
+
- **现象**:13 条 `@deepseek-ai/dsh-*` peer 写的是 `>=0.1.7-rc.1 <0.3.0-0`。这条范围**看着很宽,却匹配不到任何 `0.2.0-rc.*`**:node-semver 只有当范围里*某个*比较符与该版本的 `major.minor.patch` 元组完全一致、且自身也带预发布标签时,才放行预发布版本;`0.1.7-rc.1` 与 `0.3.0-0` 都不落在 `0.2.0` 元组上,于是 `0.2.0-rc.1` / `0.2.0-rc.2` 被**静默**排除。而 `@deepseek-ai/dsh` 的 `latest` **正是 `0.2.0-rc.2`** —— 从 npm 安装本插件的用户会撞上 `ERESOLVE`,还得自己手工绕过。
|
|
31
|
+
- **为什么本地与 CI 全绿**:本机 profile 是 `link:` 安装,**完全不走 peer 解析**;而 `tests/dsh-line.spec.ts` 当时自带一个只认「两个比较符」的简易实现,它对预发布版本的判断与 node-semver **相反** —— 一条**假绿**测试,而且它断言的恰好是错误的那一侧(用例写着「0.2.x 线仍放行」,实际并不放行)。
|
|
32
|
+
- **修法**:范围改为**两条分支** `>=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0`(peer 与 dev 共 26 处一并改),`tests/dsh-line.spec.ts` 的比较器换成**真实的 node-semver**(`semver` 提为显式 devDependency —— 此前只作为传递依赖存在),删掉已无用的手写比较器;新增两条用例:已发布的 `0.2.0-rc.*` 必须被放行(回归)、有意排除的 `0.1.7-alpha.*` / `0.2.1-alpha.*` / `0.3.0-rc.1` 仍须被挡在外;并让 peer 与 dev 的口径一致。两条 README 的「受支持的宿主范围」表与 `RELEASING.md` 的发布核对清单同步更新。
|
|
33
|
+
- **依据**:`awesome-dsh-plugin/awesome-dsh-plugin` 的 `contributing.md`「A peer range without an explicit prerelease branch silently excludes every prerelease build of the harness」。
|
|
34
|
+
- **思考强度「有时候有、有时候没有」—— 目录只在启动时拉一次,而保存的快照全是空档位**(用户实拍)。
|
|
35
|
+
- **实测本机状态**(只读检查,未发起任何领取):保存的 `regions.cn.lastCatalog` 里 **14 个模型的 `thinkingEfforts` 全部是 `[]`**,`defaultThinkingEffort` 一个都没有;而同一时刻上游实时目录里 **9 个模型明确声明了档位**(`qmodel_38max` / `qfmodel` → `low, medium, xhigh` 默认 medium;`dmodel` → `high, max` 默认 max;`dfmodel` / `gmodel` / `kmodel` / `gfmodel` / `gm51model` / `kmodel_latest` 各自不同)。`[]` 是**旧 schema 把档位字段剥掉时**留下的产物 —— 也就是说这份快照根本不是「上游没声明」,而是「它来自一个没拉到档位的时刻」。
|
|
36
|
+
- **为什么是间歇性的**:`fullCatalog`(唯一带档位的目录)是**纯内存**的,而目录只在**启动时**拉一次。于是:点一次「从 Qoder 刷新」→ 实时目录装上 → 档位出现;重启 / 重新读取 → 实时目录空 → 退回那份全空快照 → 档位消失。「有没有点过刷新、进程活了多久」就是「有时候有、有时候没有」的全部来源。
|
|
37
|
+
- **修法**:`displayModels`(卡片 `/usage` 路由读的就是它)读到「本进程还没成功拉过实时目录 + 手上的目录一个档位都没声明」时,触发一次后台重拉(fire-and-forget + 在飞标记去重,**不阻塞**这次读 —— `displayModels` 是被 `/usage` 路由同步调用的读路径,在里面 await 网络会让每次读卡片都等一次上游),**继续服务手上的目录**:模型不能从卡片上消失,缺的只是档位,拉回来之后的下一次读取自然补上。「本进程还没拉到实时目录」这个守卫不可省 —— 上游**真的**没声明档位是合法状态,去掉守卫后每 60 秒一次的卡片轮询都会拖一发模型列表请求,永不停。
|
|
38
|
+
- **返工记录(第一版修复是死代码,险些带着假说明发布)**:第一版把重拉挂在 `displayModels` 的「运行时目录为空」分支上,三层缺陷叠加,使它在生产路径上**一次都不会执行**:
|
|
39
|
+
1. 调用处 `if (!serve.models) return serve.fallback` —— `serve.models` 恒为数组,而 `[]` 是 truthy,这个分支永远不走;
|
|
40
|
+
2. `serveSavedCatalog` 的 stale 路径返回 `fallback: []`,兜底目录从未被填上(与它自己的文档注释「返回 `{ models: [], fallback: [...] }`」直接矛盾);
|
|
41
|
+
3. 根因:运行时目录 `catalog` 构造时就种了 9 个静态兜底模型、`set()` 拒绝空数组,所以 `live.length > 0` 恒成立、永远提前返回 —— 挂在它后面的整个 lastCatalog 分支(`serveSavedCatalog` 唯一的调用处)不可达。
|
|
42
|
+
当时的集成测试只断言 `ctx.llm.listModels`(适配器路径,读运行时目录),恰好绕开 `displayModels` —— 于是测试全绿、修复无效,CHANGELOG 差一点写下一个没发生的修复。这轮把空档位判据挪到**真正被服务的 live 列表**上、删掉不可达分支,并把测试改成走**真实的 `/usage` 路由**(假 webServer 捕获路由,见「签到积分不刷新」那条)。
|
|
43
|
+
- **判据用 `length > 0` 而不是「字段在不在」**:旧 schema 留下的是**空数组**而不是缺席,所以「字段在」恰恰不能作为「声明过」的证据。两种「没有档位」(旧快照的空数组 vs 上游真的没声明)从数据上分不开,**要不要重拉由目录来源决定**(见修法里的守卫);判据与动作继续打包在 `serveSavedCatalog(models, onStale)` 里 —— 拆开接线时,把调用处变异掉单测照样全绿。
|
|
44
|
+
- **测试**(`tests/index.spec.ts`,+2 走真实 `/usage` 路由):一条钉**自愈** —— 启动拉取失败后,第一次读卡片**继续服务旧快照里的模型**(模型不消失、无档位),同时触发一次重拉;落地后下一次读,档位来自实时目录。另一条钉**守卫** —— 上游诚实声明「无档位」时,实时目录落地后的卡片轮询不再打模型列表端点。三组变异测试:删掉触发(复现原死代码)/ 去掉守卫(读路径变打网络循环)/ 恢复 `models: []` 的旧契约 —— 各自被对应测试接住。
|
|
45
|
+
- **每日签到:领取之后不说「还有多久刷新」—— 而「已领取」这句话离开它是残缺的**(用户实拍官网页面上的 `09:22:23` 倒计时,并指出领取成功要判断是否有倒计时)。
|
|
46
|
+
- **先去看接口,而不是猜**:用本地那个能用的 PAT 只读拉了一次 `GET /sash/api/v1/me/campaigns`(**没有领取任何东西**)。实测活动行是:
|
|
47
|
+
```json
|
|
48
|
+
{ "campaignId": "01a12102-…", "campaignKey": "act-20261009-057",
|
|
49
|
+
"actionType": "CLAIM_BENEFIT", "claimStatus": "CLAIMED",
|
|
50
|
+
"startAt": 1791770400, "endAt": 1791856740,
|
|
51
|
+
"benefit": { "kind": "CREDITS", "amount": 100,
|
|
52
|
+
"validity": { "mode": "RELATIVE_DAYS", "days": 30 } } }
|
|
53
|
+
```
|
|
54
|
+
换算(UTC+8):`startAt` = 当天 **10:00:00**,`endAt` = 次日 **09:59:00** —— 与活动文案「每日 10:00(UTC+8)刷新」一致。所以页面上那个 `09:22:23` 就是 **`endAt − 当前时间`**。
|
|
55
|
+
- **领取不是「今天到此为止」**:领取只是把这一窗从 `CLAIMABLE` 变成 `CLAIMED`,而下一窗什么时候开,上游早就写在 `endAt` 里了。此前插件只回一个 `already-claimed` / `claimed`,UI 只能说「已签到」—— 用户会以为要等到第二天零点。
|
|
56
|
+
- **修法**(`endAt` 是 epoch **秒**,转毫秒只发生在它变成时间戳的那一刻;解析层保持秒,否则 `startAt`/`endAt` 单位会不一致):
|
|
57
|
+
- `QoderCampaign` 解析 `startAt` / `endAt`;`QoderCheckinOutcome` 在 `claimed` **与 `already-claimed` 两条路**(含 409 幂等)都带上 `nextRefreshAtMs`;
|
|
58
|
+
- 宿主快照从 `Record<id, boolean>` 升级为 `{ claimed, refreshAtMs? }`,写进成员行的 `checkinRefreshAtMs`;
|
|
59
|
+
- 客户端新增 `countdownText()`(`HH:MM:SS`,秒级向上取整,小时不封顶),由既有的 20 秒 `nowMs` 心跳重绘;样式用等宽数字(tabular-nums),否则位数一变整格宽度就抖、表格列跟着晃。
|
|
60
|
+
- **上游没给 `endAt` 就不显示倒计时**,绝不用本地零点或「上次 +24h」编一个。
|
|
61
|
+
- **顺带修掉一个真缺陷**:客户端最初把「已领取但上游没给时间」与「倒计时走完了」都表达成 `countdownText(...) === ''`,于是前者被渲染成「现在可以领取」—— 把「不知道下一窗什么时候开」说成了「已经可以领了」。改成四态(`off` / `unknown` / `ticking` / `due`)。测试接住了它:先写成两态时那条断言立刻失败。
|
|
62
|
+
- 测试 +17(`tests/checkin-countdown.spec.ts` 新建 7 条、`tests/upstream.spec.ts` +5、`tests/pool-render.spec.tsx` +5),**用实测值锚定**(`endAt = 1791856740`,倒计时断言精确到 `09:22:23`)。四组变异测试:`already-claimed` 丢掉刷新时刻 / `unknown` 塌成 `due` / 宿主快照丢掉 `refreshAtMs` / 上游缺字段时仍然编一个 —— 各自被对应测试接住。
|
|
63
|
+
|
|
64
|
+
### Changed
|
|
65
|
+
|
|
66
|
+
- **回复流不再注入「将由账号 … 回答本次问题」。** 账号池成员接管请求时,此前会在回复正文最前面带一行这个提示(`poolServingNotice`,账号标识打码)。宿主侧不再提供该注入点,回复正文从模型自己的输出开始;shim 的注入能力仍保留,只是本插件不再用它。
|
|
67
|
+
|
|
5
68
|
## 0.3.0 (2026-10-12)
|
|
6
69
|
|
|
7
70
|
三条主线:**每日签到真的接通了**(我早期把它判成「Qoder 没有签到」,那是没实测就下的结论)、**与 dsh-connect-workbuddy 的逐模块对等审计**,以及收尾的**国际版(global)对等审计**(结论见 [`docs/audit/AUDIT-REPORT.md`](docs/audit/AUDIT-REPORT.md))。对等审计的每条都以 workbuddy 的已验证行为为基准,并对照 pi-ai / DSH 宿主源码逐条核实。
|
package/README.en.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">dsh-connect-qoder</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center"><b>Connect Qoder (Lingma
|
|
7
|
+
<p align="center"><b>Connect Qoder (Lingma CN / international) models to DeepSeek Harness, with an account pool, daily check-in, credit readouts and model management.</b></p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="README.md">中文</a> ·
|
|
@@ -81,7 +81,7 @@ dsh plugin --profile web add @dmh2002/dsh-connect-qoder # Web
|
|
|
81
81
|
|
|
82
82
|
Then **restart that profile's DSH process** (the host half of a bundle loads at startup). You can also search for `dsh-connect-qoder` in DSH's "Plugin market / Bundles" and install it in one click.
|
|
83
83
|
|
|
84
|
-
> The npm package name carries the `@dmh2002/` scope
|
|
84
|
+
> The npm package name carries the `@dmh2002/` scope. The plugin's **configuration identity inside DSH does not** — the settings namespace, the `/plugins/dsh-connect-qoder/…` routes and the CLI program name all stay `dsh-connect-qoder`, so anyone upgrading from 0.2.0 keeps their saved configuration with no need to re-enter PATs. But the **browser bundle's module id and the settings card's slot keys are both keyed by the scoped package name** (`plugins.bundle.config` takes the bare package name, `plugins.row.config` takes `<package name>#<row id>`, and the row id stays unscoped); these are injected from `package.json` at build time, so a rename cannot leave them behind.
|
|
85
85
|
|
|
86
86
|
### From source (development)
|
|
87
87
|
|
|
@@ -207,7 +207,7 @@ pnpm exec dsh-connect-qoder doctor --json # machine-readable output
|
|
|
207
207
|
|
|
208
208
|
| Host | Peer range |
|
|
209
209
|
| --- | --- |
|
|
210
|
-
| DSH (`@deepseek-ai/*`) | `>=0.1.7-rc.1 <0.3.0-0` |
|
|
210
|
+
| DSH (`@deepseek-ai/*`) | `>=0.1.7-rc.1 <0.2.0-0 \|\| >=0.2.0-rc.1 <0.3.0-0` |
|
|
211
211
|
| Node.js | `^22.19.0 \|\| >=24.0.0` |
|
|
212
212
|
|
|
213
213
|
> Enforced by `tests/dsh-line.spec.ts`: the upper bound must carry `-0` (DSH only ships prereleases; `<0.3.0` would block nothing).
|
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
<h1 align="center">dsh-connect-qoder</h1>
|
|
6
6
|
|
|
7
|
-
<p align="center"><b>把 Qoder
|
|
7
|
+
<p align="center"><b>把 Qoder(通义灵码国内版 / 国际版)的模型接入 DeepSeek Harness,并提供账号池、每日签到、积分读数与模型管理。</b></p>
|
|
8
8
|
|
|
9
9
|
<p align="center">
|
|
10
10
|
<a href="README.en.md">English</a> ·
|
|
@@ -81,7 +81,7 @@ dsh plugin --profile web add @dmh2002/dsh-connect-qoder # Web
|
|
|
81
81
|
|
|
82
82
|
装完**重启该 profile 的 DSH 进程**(bundle 的 host 半边在启动时加载)。也可以在 DSH 的「插件市场 / Bundles」里搜 `dsh-connect-qoder` 一键安装。
|
|
83
83
|
|
|
84
|
-
> npm 包名带 `@dmh2002/`
|
|
84
|
+
> npm 包名带 `@dmh2002/` 作用域。插件在 DSH 内的**配置标识不带作用域**——设置命名空间、`/plugins/dsh-connect-qoder/…` 路由与命令行程序名都仍是 `dsh-connect-qoder`,所以 0.2.0 之前装过的用户升级后配置照旧生效,不用重填 PAT。但**浏览器 bundle 的模块 id、以及设置卡片两个槽位的 key 都按带作用域的包名建**(`plugins.bundle.config` 用裸包名,`plugins.row.config` 用 `<包名>#<行 id>`,行 id 仍不带作用域),这几处由构建期从 `package.json` 注入,改包名不会漏下它们。
|
|
85
85
|
|
|
86
86
|
### 从源码构建(开发)
|
|
87
87
|
|
|
@@ -207,10 +207,10 @@ pnpm exec dsh-connect-qoder doctor --json # 机器可读输出
|
|
|
207
207
|
|
|
208
208
|
| 宿主 | peer 范围 |
|
|
209
209
|
| --- | --- |
|
|
210
|
-
| DSH(`@deepseek-ai/*`) | `>=0.1.7-rc.1 <0.3.0-0` |
|
|
210
|
+
| DSH(`@deepseek-ai/*`) | `>=0.1.7-rc.1 <0.2.0-0 \|\| >=0.2.0-rc.1 <0.3.0-0` |
|
|
211
211
|
| Node.js | `^22.19.0 \|\| >=24.0.0` |
|
|
212
212
|
|
|
213
|
-
> 范围由 `tests/dsh-line.spec.ts`
|
|
213
|
+
> 范围由 `tests/dsh-line.spec.ts` 强制,且**必须写成两条分支**:DSH 至今只发预发布,而 node-semver 只有当范围里*某个*比较符与该版本的 `major.minor.patch` 元组完全一致、且自身也带预发布标签时,才会放行预发布版本。只写 `>=0.1.7-rc.1 <0.3.0-0` 会**静默排除整条 `0.2.0-rc.*` 线**——而 `0.2.0-rc.2` 正是 `@deepseek-ai/dsh` 当前的 `latest`,用户装到就会 `ERESOLVE`。上界仍须带 `-0`(`<0.3.0` 不挡任何东西)。
|
|
214
214
|
|
|
215
215
|
## 安全说明
|
|
216
216
|
|
package/lib/bin.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { S as deriveFingerprint, _ as piAiRuntimeInfo, d as qoderAccountsPath, f as QODER_PAT_ENV_VARS, h as patFromEnv, i as readHostHeartbeat, n as isHeartbeatProcessAlive, o as qoderPoolStorePath, r as qoderHostHeartbeatPath, u as QoderAccountStore, v as QODER_REGIONS, x as isQoderUpstreamError, y as QoderUpstreamClient } from "./host-heartbeat-
|
|
2
|
+
import { S as deriveFingerprint, _ as piAiRuntimeInfo, d as qoderAccountsPath, f as QODER_PAT_ENV_VARS, h as patFromEnv, i as readHostHeartbeat, n as isHeartbeatProcessAlive, o as qoderPoolStorePath, r as qoderHostHeartbeatPath, u as QoderAccountStore, v as QODER_REGIONS, x as isQoderUpstreamError, y as QoderUpstreamClient } from "./host-heartbeat-BNuj6Lt_.js";
|
|
3
3
|
import { rm } from "node:fs/promises";
|
|
4
4
|
//#region src/bin.ts
|
|
5
5
|
/**
|
|
@@ -564,6 +564,14 @@ const CHAT_PATH = "/algo/api/v2/service/pro/sse/agent_chat_generation?FetchKeys=
|
|
|
564
564
|
* 注意是 **v1**(用量那套是 v2)。
|
|
565
565
|
*/
|
|
566
566
|
const CAMPAIGNS_PATH = "/sash/api/v1/me/campaigns";
|
|
567
|
+
/**
|
|
568
|
+
* sash 面专用的令牌交换端点(CLI / 网页那一族)。
|
|
569
|
+
*
|
|
570
|
+
* 与 {@link JOB_TOKEN_PATH}(桌面客户端那一族)**并存**,不是替代 —— 实测国际版
|
|
571
|
+
* 的 chat 面只认前者换来的 `securityOauthToken`,而 sash / 额度面只认这里换来的
|
|
572
|
+
* `token`;国内版两族等价。见 `QoderSessionHolder.sashToken`。
|
|
573
|
+
*/
|
|
574
|
+
const SASH_TOKEN_PATH = "/api/v1/jobToken/exchange";
|
|
567
575
|
const CAMPAIGNS_QUERY = "?forceRefresh=true";
|
|
568
576
|
/** sash 接口要求的客户端标识(参考实现如此,缺了会被拒)。 */
|
|
569
577
|
const SASH_CLIENT_TYPE = "10";
|
|
@@ -717,12 +725,67 @@ var QoderUpstreamClient = class {
|
|
|
717
725
|
jobToken,
|
|
718
726
|
expireAtMs: jobToken.expireTime > 0 ? jobToken.expireTime : 0
|
|
719
727
|
};
|
|
728
|
+
await this.refreshSashToken(credential, holder);
|
|
720
729
|
this.holders.set(credential, holder);
|
|
721
730
|
this.currentKey = credential;
|
|
722
731
|
this.log(`jobToken 已交换: ${jobToken.name} (${jobToken.id})`);
|
|
723
732
|
return jobToken;
|
|
724
733
|
}
|
|
725
734
|
/**
|
|
735
|
+
* 换一枚 sash 面专用的令牌(`POST /api/v1/jobToken/exchange`)。
|
|
736
|
+
*
|
|
737
|
+
* **不抛**:这一族换不到时 sash 那一支会回落到旧令牌(国内版本来就够用),
|
|
738
|
+
* 而整条交换失败只该让「积分读不到」,不该让 chat / 对话跟着挂 —— 那是完全
|
|
739
|
+
* 不同的两条链路。
|
|
740
|
+
*
|
|
741
|
+
* body 键是 `personal_token`:实测 `personal_access_token`(eghrhegpe 那个 fork
|
|
742
|
+
* 用的拼写)在当前上游回 **400 BadRequest**,两个区域都是。
|
|
743
|
+
*/
|
|
744
|
+
async refreshSashToken(credential, holder) {
|
|
745
|
+
const url = `${this.region.openapi}${SASH_TOKEN_PATH}`;
|
|
746
|
+
try {
|
|
747
|
+
const resp = await this.fetchImpl(url, {
|
|
748
|
+
method: "POST",
|
|
749
|
+
headers: {
|
|
750
|
+
"content-type": "application/json",
|
|
751
|
+
accept: "application/json"
|
|
752
|
+
},
|
|
753
|
+
body: JSON.stringify({ personal_token: credential })
|
|
754
|
+
});
|
|
755
|
+
const text = await resp.text();
|
|
756
|
+
if (resp.status !== 200) {
|
|
757
|
+
this.log(`sash 令牌交换失败(HTTP ${resp.status}),sash 面将回落到旧令牌: ${truncate(text, 120)}`);
|
|
758
|
+
return;
|
|
759
|
+
}
|
|
760
|
+
const data = parseJson(text);
|
|
761
|
+
const raw = data === void 0 ? {} : data;
|
|
762
|
+
const token = typeof raw["token"] === "string" && raw["token"] !== "" ? raw["token"] : typeof raw["job_token"] === "string" && raw["job_token"] !== "" ? raw["job_token"] : typeof raw["jobToken"] === "string" && raw["jobToken"] !== "" ? raw["jobToken"] : "";
|
|
763
|
+
if (token === "") {
|
|
764
|
+
this.log(`sash 令牌交换响应里没有 token 字段: ${truncate(text, 120)}`);
|
|
765
|
+
return;
|
|
766
|
+
}
|
|
767
|
+
holder.sashToken = token;
|
|
768
|
+
const expiry = typeof raw["expires_at"] === "string" ? Date.parse(raw["expires_at"]) : NaN;
|
|
769
|
+
holder.sashExpireAtMs = Number.isFinite(expiry) ? expiry : 0;
|
|
770
|
+
this.log(`sash 令牌已交换(有效期至 ${holder.sashExpireAtMs > 0 ? new Date(holder.sashExpireAtMs).toISOString() : "未知"})`);
|
|
771
|
+
} catch (error) {
|
|
772
|
+
this.log(`sash 令牌交换异常,sash 面将回落到旧令牌: ${String(error?.message ?? error).slice(0, 120)}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* 取该会话用于 **sash 面** 的 Authorization 值。
|
|
777
|
+
*
|
|
778
|
+
* 新令牌优先(国际版必需);没有就回落到旧的 `securityOauthToken`(国内版
|
|
779
|
+
* 两个面都认它,而且它是 chat 在用的那一个 —— 回落不会破坏任何东西)。
|
|
780
|
+
*
|
|
781
|
+
* 返回 `''` 表示两族都没有:那一支必须报「读不到」,绝不能带上一个空 bearer
|
|
782
|
+
* 去换一个 401 回来(那样分不清是没令牌还是令牌被拒)。
|
|
783
|
+
*/
|
|
784
|
+
sashBearerOf(holder) {
|
|
785
|
+
if (typeof holder.sashToken === "string" && holder.sashToken !== "") return holder.sashToken;
|
|
786
|
+
return holder.jobToken.securityOauthToken;
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
726
789
|
* 确保存在未过期会话。credential 提供 PAT 或 refreshToken(`drt-` 前缀)。
|
|
727
790
|
* 已有有效会话时直接复用(不重复交换)。
|
|
728
791
|
*/
|
|
@@ -888,8 +951,8 @@ var QoderUpstreamClient = class {
|
|
|
888
951
|
* 但**设备风控三件套属于 sash 面**,按 `requiresDeviceRisk` 附加 —— 见下。
|
|
889
952
|
*/
|
|
890
953
|
async jobTokenGet(holder, url, signal) {
|
|
891
|
-
const oauthToken = holder
|
|
892
|
-
if (oauthToken === "") throw new QoderUpstreamError("auth", "
|
|
954
|
+
const oauthToken = this.sashBearerOf(holder);
|
|
955
|
+
if (oauthToken === "") throw new QoderUpstreamError("auth", "两次令牌交换都没有拿到可用令牌,无法查询用量", 401);
|
|
893
956
|
const resp = await this.fetchImpl(url, {
|
|
894
957
|
headers: {
|
|
895
958
|
accept: "application/json",
|
|
@@ -995,8 +1058,8 @@ var QoderUpstreamClient = class {
|
|
|
995
1058
|
* region 条件附加 —— 不要无条件发,多余的头在另一侧是噪音。
|
|
996
1059
|
*/
|
|
997
1060
|
async sashRequest(method, holder, url, body, signal) {
|
|
998
|
-
const oauthToken = holder
|
|
999
|
-
if (oauthToken === "") throw new QoderUpstreamError("auth", "
|
|
1061
|
+
const oauthToken = this.sashBearerOf(holder);
|
|
1062
|
+
if (oauthToken === "") throw new QoderUpstreamError("auth", "两次令牌交换都没有拿到可用令牌,无法访问签到接口", 401);
|
|
1000
1063
|
const resp = await this.fetchImpl(url, {
|
|
1001
1064
|
method,
|
|
1002
1065
|
headers: {
|
|
@@ -1233,6 +1296,14 @@ const CONTENT_POLICY_MARKERS = [
|
|
|
1233
1296
|
function isContentPolicy(detail) {
|
|
1234
1297
|
return CONTENT_POLICY_MARKERS.some((marker) => detail.includes(marker));
|
|
1235
1298
|
}
|
|
1299
|
+
/** Parse JSON, or undefined when the body is not JSON at all. Never throws. */
|
|
1300
|
+
function parseJson(text) {
|
|
1301
|
+
try {
|
|
1302
|
+
return JSON.parse(text);
|
|
1303
|
+
} catch {
|
|
1304
|
+
return;
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1236
1307
|
function truncate(text, max) {
|
|
1237
1308
|
return text.length <= max ? text : `${text.slice(0, max)}…`;
|
|
1238
1309
|
}
|
|
@@ -1657,7 +1728,7 @@ var QoderAccountStore = class {
|
|
|
1657
1728
|
//#endregion
|
|
1658
1729
|
//#region src/version.ts
|
|
1659
1730
|
/** The npm package version this build was produced from. */
|
|
1660
|
-
const QODER_CONNECT_VERSION = "0.
|
|
1731
|
+
const QODER_CONNECT_VERSION = "0.4.0";
|
|
1661
1732
|
//#endregion
|
|
1662
1733
|
//#region src/account-pool-store.ts
|
|
1663
1734
|
/**
|
package/lib/index.d.ts
CHANGED
|
@@ -3,6 +3,40 @@ import { Context } from "@deepseek-ai/cordis";
|
|
|
3
3
|
//#region src/upstream.d.ts
|
|
4
4
|
type QoderRegion = 'cn' | 'global';
|
|
5
5
|
//#endregion
|
|
6
|
+
//#region src/catalog.d.ts
|
|
7
|
+
interface QoderModelInfo {
|
|
8
|
+
key: string;
|
|
9
|
+
displayName: string;
|
|
10
|
+
/** 积分倍率(模型名内嵌 `· xN.NN`)。 */
|
|
11
|
+
priceFactor: number;
|
|
12
|
+
/** 原生输入窗口(由 context_config 首档或 max_input_tokens 决定)。 */
|
|
13
|
+
contextWindow: number;
|
|
14
|
+
maxOutputTokens: number;
|
|
15
|
+
supportsImages: boolean;
|
|
16
|
+
supportsThinking: boolean;
|
|
17
|
+
/** 可完全关闭思考(thinking_config.disabled 存在)。 */
|
|
18
|
+
canDisableThinking: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* 上游声明的思考强度档位(`thinking_config.enabled.efforts` 的键,按强度
|
|
21
|
+
* 升序)。不存在时该模型没有可选档位,卡片不渲染这一项。
|
|
22
|
+
*
|
|
23
|
+
* 实测 Qoder 的档位名不统一:Qwen 系用 `low / medium / xhigh`,DeepSeek/GLM
|
|
24
|
+
* 用 `low / high / max`——所以只做展示,不做跨模型比较。上游给出的键序也不
|
|
25
|
+
* 可靠(JSON 里可能先出现 `xhigh`),必须按语义排序后再显示。
|
|
26
|
+
*/
|
|
27
|
+
thinkingEfforts?: string[];
|
|
28
|
+
/** 上游标为默认的档位(`efforts[x].is_default`)。 */
|
|
29
|
+
defaultThinkingEffort?: string;
|
|
30
|
+
isDefault: boolean;
|
|
31
|
+
isFree: boolean;
|
|
32
|
+
isNew: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* 网关声明可用的上下文档位(`context_config` 各项的 token_count,升序)。
|
|
35
|
+
* 不存在时无档位可选,卡片不渲染预算选择器。
|
|
36
|
+
*/
|
|
37
|
+
contextTiers?: number[];
|
|
38
|
+
}
|
|
39
|
+
//#endregion
|
|
6
40
|
//#region src/status-paths.d.ts
|
|
7
41
|
/**
|
|
8
42
|
* The project's public repository.
|
|
@@ -39,6 +73,42 @@ interface Config {
|
|
|
39
73
|
regions: Record<string, unknown>;
|
|
40
74
|
}
|
|
41
75
|
declare const Config: z<Config>;
|
|
76
|
+
/**
|
|
77
|
+
* 一份目录是否**声明了**思考档位。
|
|
78
|
+
*
|
|
79
|
+
* 「一个模型都没有档位」有两种完全不同的含义,必须分开:
|
|
80
|
+
* 1. 上游当前没声明(`thinkingEfforts` 字段缺席或为空)—— 那它是「上游没
|
|
81
|
+
* 档位」的诚实答案;
|
|
82
|
+
* 2. 这份目录来自一次没拉到档位的时刻 —— 本机实测 14 个 cn 模型的
|
|
83
|
+
* `thinkingEfforts` 全是 `[]`,是旧 schema 把字段剥掉时的产物。用它当答案,
|
|
84
|
+
* 卡片就不渲染档位选择器(用户实拍「思考强度有时候有、有时候没有」:点一次
|
|
85
|
+
* 「从 Qoder 刷新」就有,重启后就没)。
|
|
86
|
+
*
|
|
87
|
+
* 两种情况从数据上分不开(空数组与缺席都判「没声明」),要不要重拉由**目录的
|
|
88
|
+
* 来源**决定:调用方只在「本进程还没成功拉到实时目录」时才据此触发重拉(见
|
|
89
|
+
* displayModels)—— 实时目录没档位是上游的诚实答案,重拉多少次都一样。
|
|
90
|
+
*
|
|
91
|
+
* 判据用 `length > 0` 而不是「字段在不在」:旧 schema 留下的是**空数组**而不是
|
|
92
|
+
* 缺席,所以「字段在」恰恰不能作为「声明过」的证据。
|
|
93
|
+
*/
|
|
94
|
+
declare function catalogSnapshotDeclaresEfforts(models: readonly {
|
|
95
|
+
thinkingEfforts?: readonly string[];
|
|
96
|
+
}[]): boolean;
|
|
97
|
+
/**
|
|
98
|
+
* 「手上的目录能不能当档位答案」——**判据与动作打包在一起**。
|
|
99
|
+
*
|
|
100
|
+
* 拆开成两个函数(判据一个、调用处自己判)会留下一条没被测试覆盖的接线:把调用处
|
|
101
|
+
* 变异掉,单测仍然全绿(第一版就是这么自我欺骗的)。这里让规则自己执行动作并
|
|
102
|
+
* 返回结论,于是「动作有没有真的发生」也能被断言。
|
|
103
|
+
*
|
|
104
|
+
* 检测到「一个档位都没声明」时**不改动要服务的模型列表**:模型不能从卡片上
|
|
105
|
+
* 消失,缺的只是档位 —— 后台拉回来之后的下一次读取自然补上。返回 `stale`
|
|
106
|
+
* 标记这次调用有没有触发过拉取。
|
|
107
|
+
*/
|
|
108
|
+
declare function serveSavedCatalog(models: readonly QoderModelInfo[], onStale: () => void): {
|
|
109
|
+
models: readonly QoderModelInfo[];
|
|
110
|
+
stale: boolean;
|
|
111
|
+
};
|
|
42
112
|
/** 某区域的配置状态(缺省返回空对象)。 */
|
|
43
113
|
declare function regionStateOf(config: {
|
|
44
114
|
regions?: Record<string, unknown> | null;
|
|
@@ -53,4 +123,4 @@ declare function regionHasCredential(config: {
|
|
|
53
123
|
}, region: QoderRegion): boolean;
|
|
54
124
|
declare function apply(ctx: Context, config: Config): void;
|
|
55
125
|
//#endregion
|
|
56
|
-
export { Config, QODER_CONNECT_VERSION, QODER_GITHUB_URL, apply, asVolatile, inject, name, regionEnabled, regionHasCredential, regionStateOf, settingsNamespaceOf, unwrapVolatile };
|
|
126
|
+
export { Config, QODER_CONNECT_VERSION, QODER_GITHUB_URL, apply, asVolatile, catalogSnapshotDeclaresEfforts, inject, name, regionEnabled, regionHasCredential, regionStateOf, serveSavedCatalog, settingsNamespaceOf, unwrapVolatile };
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { _ as piAiRuntimeInfo, a as writeHostHeartbeat, b as QoderUpstreamError, c as writePoolProbes, h as patFromEnv, l as QODER_CONNECT_VERSION, m as maskPat, p as isQoderAccountId, s as readPoolProbes, t as clearHostHeartbeat, u as QoderAccountStore, x as isQoderUpstreamError, y as QoderUpstreamClient } from "./host-heartbeat-
|
|
1
|
+
import { _ as piAiRuntimeInfo, a as writeHostHeartbeat, b as QoderUpstreamError, c as writePoolProbes, h as patFromEnv, l as QODER_CONNECT_VERSION, m as maskPat, p as isQoderAccountId, s as readPoolProbes, t as clearHostHeartbeat, u as QoderAccountStore, x as isQoderUpstreamError, y as QoderUpstreamClient } from "./host-heartbeat-BNuj6Lt_.js";
|
|
2
2
|
import { _ as regionOfStatusUrl, a as QODER_MODELS_REFRESH_PATH, c as QODER_REGIONS, d as QODER_SETTINGS_NS, f as QODER_TRANSFER_PATH, g as poolActionOf, h as poolAccountIdOf, i as QODER_GITHUB_URL, m as accountActionOf, n as QODER_ACCOUNTS_REFRESH_PATH, o as QODER_POOL_PATH, p as QODER_USAGE_PATH, r as QODER_ACCOUNT_CREDITS_PATH, s as QODER_PROBE_PATH, t as QODER_ACCOUNTS_PATH, u as QODER_SAVE_PATH, v as transferActionOf, y as unwrapVolatileDeep } from "./status-paths-w2WDUPVS.js";
|
|
3
3
|
import z from "@deepseek-ai/schemastery";
|
|
4
4
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
|
@@ -2077,6 +2077,49 @@ const regionConfig = z.object({
|
|
|
2077
2077
|
lastCatalog: z.array(modelConfig).default([])
|
|
2078
2078
|
});
|
|
2079
2079
|
const Config = z.object({ regions: asVolatile(z.dict(regionConfig).default({}).description("Per-region Qoder settings, keyed cn | global")) });
|
|
2080
|
+
/**
|
|
2081
|
+
* 一份目录是否**声明了**思考档位。
|
|
2082
|
+
*
|
|
2083
|
+
* 「一个模型都没有档位」有两种完全不同的含义,必须分开:
|
|
2084
|
+
* 1. 上游当前没声明(`thinkingEfforts` 字段缺席或为空)—— 那它是「上游没
|
|
2085
|
+
* 档位」的诚实答案;
|
|
2086
|
+
* 2. 这份目录来自一次没拉到档位的时刻 —— 本机实测 14 个 cn 模型的
|
|
2087
|
+
* `thinkingEfforts` 全是 `[]`,是旧 schema 把字段剥掉时的产物。用它当答案,
|
|
2088
|
+
* 卡片就不渲染档位选择器(用户实拍「思考强度有时候有、有时候没有」:点一次
|
|
2089
|
+
* 「从 Qoder 刷新」就有,重启后就没)。
|
|
2090
|
+
*
|
|
2091
|
+
* 两种情况从数据上分不开(空数组与缺席都判「没声明」),要不要重拉由**目录的
|
|
2092
|
+
* 来源**决定:调用方只在「本进程还没成功拉到实时目录」时才据此触发重拉(见
|
|
2093
|
+
* displayModels)—— 实时目录没档位是上游的诚实答案,重拉多少次都一样。
|
|
2094
|
+
*
|
|
2095
|
+
* 判据用 `length > 0` 而不是「字段在不在」:旧 schema 留下的是**空数组**而不是
|
|
2096
|
+
* 缺席,所以「字段在」恰恰不能作为「声明过」的证据。
|
|
2097
|
+
*/
|
|
2098
|
+
function catalogSnapshotDeclaresEfforts(models) {
|
|
2099
|
+
return models.some((model) => (model.thinkingEfforts?.length ?? 0) > 0);
|
|
2100
|
+
}
|
|
2101
|
+
/**
|
|
2102
|
+
* 「手上的目录能不能当档位答案」——**判据与动作打包在一起**。
|
|
2103
|
+
*
|
|
2104
|
+
* 拆开成两个函数(判据一个、调用处自己判)会留下一条没被测试覆盖的接线:把调用处
|
|
2105
|
+
* 变异掉,单测仍然全绿(第一版就是这么自我欺骗的)。这里让规则自己执行动作并
|
|
2106
|
+
* 返回结论,于是「动作有没有真的发生」也能被断言。
|
|
2107
|
+
*
|
|
2108
|
+
* 检测到「一个档位都没声明」时**不改动要服务的模型列表**:模型不能从卡片上
|
|
2109
|
+
* 消失,缺的只是档位 —— 后台拉回来之后的下一次读取自然补上。返回 `stale`
|
|
2110
|
+
* 标记这次调用有没有触发过拉取。
|
|
2111
|
+
*/
|
|
2112
|
+
function serveSavedCatalog(models, onStale) {
|
|
2113
|
+
if (catalogSnapshotDeclaresEfforts(models)) return {
|
|
2114
|
+
models,
|
|
2115
|
+
stale: false
|
|
2116
|
+
};
|
|
2117
|
+
onStale();
|
|
2118
|
+
return {
|
|
2119
|
+
models,
|
|
2120
|
+
stale: true
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
2080
2123
|
/** 某区域的配置状态(缺省返回空对象)。 */
|
|
2081
2124
|
function regionStateOf(config, region) {
|
|
2082
2125
|
const stored = unwrapVolatile(config.regions)?.[region];
|
|
@@ -2302,21 +2345,47 @@ function apply(ctx, config) {
|
|
|
2302
2345
|
* **运行时目录只含用户勾选的模型**(DSH 的选择器读它),而卡片必须看到**完整
|
|
2303
2346
|
* 目录**,否则未勾选的模型根本不出现在卡片里,用户再也勾不上它们。
|
|
2304
2347
|
*
|
|
2305
|
-
*
|
|
2306
|
-
*
|
|
2307
|
-
*
|
|
2308
|
-
*
|
|
2348
|
+
* 实时目录(`fullCatalog`)优先于运行时目录:实时目录反映上游**当前**声明了
|
|
2349
|
+
* 什么(含思考档位),而运行时目录是启动时由保存的 lastCatalog 装配的 ——
|
|
2350
|
+
* 那是上一次保存时的旧快照(issue #32 的教训:保存槽位丢了目录时必须显示
|
|
2351
|
+
* 实时目录,否则卡片会把用户正在用的模型报成「已下架」)。
|
|
2352
|
+
*
|
|
2353
|
+
* 「思考强度有时候有、有时候没有」(用户实拍)的机制也在这条读路径上:
|
|
2354
|
+
* `fullCatalog` 是纯内存的、只在启动时拉一次;启动那次失败的话,卡片每次
|
|
2355
|
+
* 读到的都是启动装配的旧快照 —— 一个档位都没有,直到用户手动点一次
|
|
2356
|
+
* 「从 Qoder 刷新」。所以读到「本进程还没成功拉过实时目录 + 手上的目录一个
|
|
2357
|
+
* 档位都没声明」时,**触发一次后台重拉**(fire-and-forget,见
|
|
2358
|
+
* ensureCatalogRefresh)并且**继续服务手上的目录** —— 模型不能从卡片上消失,
|
|
2359
|
+
* 缺的只是档位,拉回来之后下一次读取自然补上。
|
|
2309
2360
|
*/
|
|
2310
|
-
function displayModels(
|
|
2311
|
-
const
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
}
|
|
2318
|
-
return QODER_FALLBACK_MODELS.map(fallbackModelInfo);
|
|
2361
|
+
function displayModels(region) {
|
|
2362
|
+
const full = fullCatalog[region];
|
|
2363
|
+
const live = full.length > 0 ? full : stacks[region].catalog.current();
|
|
2364
|
+
if (full.length === 0) serveSavedCatalog(live, () => {
|
|
2365
|
+
ensureCatalogRefresh(region);
|
|
2366
|
+
});
|
|
2367
|
+
return live;
|
|
2319
2368
|
}
|
|
2369
|
+
/**
|
|
2370
|
+
* 幂等地触发一次该区域的目录拉取(**不等待**)。
|
|
2371
|
+
*
|
|
2372
|
+
* 为什么不等待:`displayModels` 是**读**路径,被 `/usage` 路由同步调用;在里面
|
|
2373
|
+
* await 网络会让「卡片读一次快照」这个纯读动作每次都等一次上游。改为
|
|
2374
|
+
* fire-and-forget + 在飞标记去重,拉取完成后下一次读取自然拿到新目录。
|
|
2375
|
+
*
|
|
2376
|
+
* 为什么需要它:目录此前**只在启动时拉一次**(`refreshModels`),而 `fullCatalog`
|
|
2377
|
+
* 是纯内存的。于是「思考强度」在重启后必然退化成那份空快照 —— 除非用户恰好
|
|
2378
|
+
* 去点一次「从 Qoder 刷新」。有没有点过刷新、以及进程活多久,就是「有时候有、
|
|
2379
|
+
* 有时候没有」的全部来源。
|
|
2380
|
+
*/
|
|
2381
|
+
const catalogRefreshInFlight = {};
|
|
2382
|
+
const ensureCatalogRefresh = (region) => {
|
|
2383
|
+
if (catalogRefreshInFlight[region] === true) return;
|
|
2384
|
+
catalogRefreshInFlight[region] = true;
|
|
2385
|
+
refreshModels(region).finally(() => {
|
|
2386
|
+
catalogRefreshInFlight[region] = false;
|
|
2387
|
+
});
|
|
2388
|
+
};
|
|
2320
2389
|
let invalidateCatalog = () => {};
|
|
2321
2390
|
for (const region of QODER_REGIONS) {
|
|
2322
2391
|
const upstream = new QoderUpstreamClient(region, { logger: log });
|
|
@@ -2360,9 +2429,6 @@ function apply(ctx, config) {
|
|
|
2360
2429
|
writePoolProbes(region, { [accountId]: poolRuntime[accountId] });
|
|
2361
2430
|
log(`pool ${region}: ${maskPat(accountId)} marked ${outcome} by live request`);
|
|
2362
2431
|
}
|
|
2363
|
-
},
|
|
2364
|
-
poolServingNotice: (accountId) => {
|
|
2365
|
-
return selectedAccountId(config, region) === accountId ? void 0 : `将由账号 ${maskPat(accountId)} 回答本次问题`;
|
|
2366
2432
|
}
|
|
2367
2433
|
}),
|
|
2368
2434
|
store,
|
|
@@ -2651,7 +2717,7 @@ function apply(ctx, config) {
|
|
|
2651
2717
|
return models;
|
|
2652
2718
|
}
|
|
2653
2719
|
const refreshModels = (region) => {
|
|
2654
|
-
(async () => {
|
|
2720
|
+
return (async () => {
|
|
2655
2721
|
const serving = await resolveServing(region);
|
|
2656
2722
|
if (!serving) {
|
|
2657
2723
|
log(`models refresh skipped for ${region}: no PAT configured`);
|
|
@@ -2793,15 +2859,15 @@ function apply(ctx, config) {
|
|
|
2793
2859
|
/** Build the transfer deps once (region-dispatched). */
|
|
2794
2860
|
const transferDeps = {
|
|
2795
2861
|
exportAccounts: async (region) => {
|
|
2796
|
-
const { transferRecordsFor } = await import("./transfer-
|
|
2862
|
+
const { transferRecordsFor } = await import("./transfer-CUNaVvmi.js");
|
|
2797
2863
|
return { accounts: transferRecordsFor(region, await stacks[region].store.vaultAccounts()) };
|
|
2798
2864
|
},
|
|
2799
2865
|
previewImport: async (_region, text) => {
|
|
2800
|
-
const { previewTransferFile } = await import("./transfer-
|
|
2866
|
+
const { previewTransferFile } = await import("./transfer-CUNaVvmi.js");
|
|
2801
2867
|
return previewTransferFile(text);
|
|
2802
2868
|
},
|
|
2803
2869
|
importAccounts: async (region, text, indexes) => {
|
|
2804
|
-
const { mergeTransferRecords } = await import("./transfer-
|
|
2870
|
+
const { mergeTransferRecords } = await import("./transfer-CUNaVvmi.js");
|
|
2805
2871
|
const { changes, imported, skipped } = mergeTransferRecords(region, text, indexes);
|
|
2806
2872
|
for (const entry of changes.values()) await stacks[region].store.add(entry.pat, entry.accountName);
|
|
2807
2873
|
return {
|
|
@@ -2911,8 +2977,8 @@ function apply(ctx, config) {
|
|
|
2911
2977
|
submitRegionSettings(region, { accountId: "" });
|
|
2912
2978
|
},
|
|
2913
2979
|
catalogs: {
|
|
2914
|
-
cn: { current: () => [...displayModels(
|
|
2915
|
-
global: { current: () => [...displayModels(
|
|
2980
|
+
cn: { current: () => [...displayModels("cn")] },
|
|
2981
|
+
global: { current: () => [...displayModels("global")] }
|
|
2916
2982
|
},
|
|
2917
2983
|
contextBudgets: (region) => contextBudgetsOf(config, region),
|
|
2918
2984
|
thinkingEfforts: (region) => thinkingEffortsOf(config, region),
|
|
@@ -3024,4 +3090,4 @@ const FAIL_OVER_OUTCOME = {
|
|
|
3024
3090
|
transient: "unavailable"
|
|
3025
3091
|
};
|
|
3026
3092
|
//#endregion
|
|
3027
|
-
export { Config, QODER_CONNECT_VERSION, QODER_GITHUB_URL, apply, asVolatile, inject, name, regionEnabled, regionHasCredential, regionStateOf, settingsNamespaceOf, unwrapVolatile };
|
|
3093
|
+
export { Config, QODER_CONNECT_VERSION, QODER_GITHUB_URL, apply, asVolatile, catalogSnapshotDeclaresEfforts, inject, name, regionEnabled, regionHasCredential, regionStateOf, serveSavedCatalog, settingsNamespaceOf, unwrapVolatile };
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@dmh2002/dsh-connect-qoder",
|
|
3
3
|
"displayName": "DSH Connect Qoder",
|
|
4
4
|
"description": "把 Qoder(通义灵码国内版 / 国际版)接入 DeepSeek Harness:PAT 多账号池、每日签到、积分读数与模型管理。",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.4.0",
|
|
6
6
|
"author": "dingminhua",
|
|
7
7
|
"copyright": "Copyright (c) 2026 LaoDing",
|
|
8
8
|
"license": "MIT",
|
|
@@ -88,46 +88,48 @@
|
|
|
88
88
|
},
|
|
89
89
|
"peerDependencies": {
|
|
90
90
|
"@deepseek-ai/cordis": ">=4.0.1-rc.1 <5.0.0 || >=4.0.2 <5.0.0",
|
|
91
|
-
"@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1 <0.3.0-0",
|
|
92
|
-
"@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1 <0.3.0-0",
|
|
93
|
-
"@deepseek-ai/dsh-client-
|
|
94
|
-
"@deepseek-ai/dsh-client-ui-
|
|
95
|
-
"@deepseek-ai/dsh-client-ui-
|
|
96
|
-
"@deepseek-ai/dsh-client-ui-settings
|
|
97
|
-
"@deepseek-ai/dsh-client-ui-
|
|
98
|
-
"@deepseek-ai/dsh-client-
|
|
99
|
-
"@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1 <0.3.0-0",
|
|
100
|
-
"@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.3.0-0",
|
|
101
|
-
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.3.0-0",
|
|
102
|
-
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.3.0-0",
|
|
103
|
-
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.3.0-0",
|
|
91
|
+
"@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
92
|
+
"@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
93
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
94
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
95
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
96
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
97
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
98
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
99
|
+
"@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
100
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
101
|
+
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
102
|
+
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
103
|
+
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
104
104
|
"@deepseek-ai/schemastery": ">=3.18.1-rc.1 <4.0.0 || >=3.18.2 <4.0.0",
|
|
105
105
|
"@earendil-works/pi-ai": ">=0.85.0 <0.88.0",
|
|
106
106
|
"react": "^18.2.0"
|
|
107
107
|
},
|
|
108
108
|
"devDependencies": {
|
|
109
109
|
"@deepseek-ai/cordis": ">=4.0.1 <5.0.0 || >=4.0.2 <5.0.0",
|
|
110
|
-
"@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1 <0.3.0-0",
|
|
111
|
-
"@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1 <0.3.0-0",
|
|
112
|
-
"@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.1 <0.3.0-0",
|
|
113
|
-
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.1 <0.3.0-0",
|
|
114
|
-
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-rc.1 <0.3.0-0",
|
|
115
|
-
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1 <0.3.0-0",
|
|
116
|
-
"@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1.7-rc.1 <0.3.0-0",
|
|
117
|
-
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.1 <0.3.0-0",
|
|
118
|
-
"@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1 <0.3.0-0",
|
|
119
|
-
"@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.3.0-0",
|
|
120
|
-
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.3.0-0",
|
|
121
|
-
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.3.0-0",
|
|
122
|
-
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.3.0-0",
|
|
110
|
+
"@deepseek-ai/dsh-atomic-write": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
111
|
+
"@deepseek-ai/dsh-attachment": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
112
|
+
"@deepseek-ai/dsh-client-locale": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
113
|
+
"@deepseek-ai/dsh-client-ui-primitives": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
114
|
+
"@deepseek-ai/dsh-client-ui-renderer": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
115
|
+
"@deepseek-ai/dsh-client-ui-settings": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
116
|
+
"@deepseek-ai/dsh-client-ui-settings-plugins": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
117
|
+
"@deepseek-ai/dsh-client-ui-slots": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
118
|
+
"@deepseek-ai/dsh-home-paths": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
119
|
+
"@deepseek-ai/dsh-host-webserver": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
120
|
+
"@deepseek-ai/dsh-llm": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
121
|
+
"@deepseek-ai/dsh-llm-pi-ai": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
122
|
+
"@deepseek-ai/dsh-settings": ">=0.1.7-rc.1 <0.2.0-0 || >=0.2.0-rc.1 <0.3.0-0",
|
|
123
123
|
"@deepseek-ai/schemastery": ">=3.18.1 <4.0.0 || >=3.18.2 <4.0.0",
|
|
124
124
|
"@earendil-works/pi-ai": ">=0.85.0 <0.88.0",
|
|
125
125
|
"@types/node": "^22.20.0",
|
|
126
126
|
"@types/react": "~18.3.31",
|
|
127
127
|
"@types/react-dom": "18.3.7",
|
|
128
|
+
"@types/semver": "^7.8.0",
|
|
128
129
|
"jsdom": "^29.1.1",
|
|
129
130
|
"react": "^18.3.1",
|
|
130
131
|
"react-dom": "18.3.1",
|
|
132
|
+
"semver": "^7.8.5",
|
|
131
133
|
"tsdown": "^0.22.2",
|
|
132
134
|
"typescript": "^5.9.3",
|
|
133
135
|
"vitest": "^4.1.8"
|