dsh-scnet-connect 1.0.1 → 1.1.1
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 +74 -6
- package/cordis.patch.yml +3 -2
- package/lib/client.js +40 -11
- package/lib/index.js +178 -38
- package/lib/scnet-catalog.js +54 -11
- package/lib/scnet-credential.js +5 -15
- package/lib/scnet-fetch.js +153 -0
- package/lib/scnet-login.js +0 -23
- package/lib/scnet-models.js +54 -24
- package/lib/scnet-platform.js +258 -329
- package/lib/scnet-web-login.js +387 -404
- package/package.json +6 -3
- package/screenshots.json +4 -0
- package/tools/check-classify.mjs +63 -0
- package/tools/check-fetch.mjs +91 -0
- package/tools/check.mjs +156 -0
- package/tools/fake-gateway.mjs +73 -0
- package/tools/repro/asar-extract.mjs +84 -0
- package/tools/repro/direct-capabilities.mjs +77 -0
- package/tools/repro/direct-effort.mjs +59 -0
- package/tools/repro/direct-probe.mjs +82 -0
- package/tools/repro/direct-verify.mjs +229 -0
- package/tools/repro/effort-burst.mjs +25 -0
- package/tools/repro/effort-effect.mjs +29 -0
- package/tools/repro/image-tools-burst.mjs +28 -0
- package/tools/repro/pixel.png +0 -0
- package/tools/repro/probe-scnet.mjs +61 -0
- package/tools/repro/scnet-chat.mjs +77 -0
- package/tools/repro/scnet-llm-call.mjs +186 -0
- package/tools/repro/search-asar.mjs +58 -0
- package/tools/repro/slice-asar.mjs +13 -0
package/README.md
CHANGED
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
- **零客户端**:不装 SCNet 桌面客户端,用手机短信登录即可使用
|
|
10
10
|
- **零配置**:装了客户端的话,自动读凭据、自动发现免费模型
|
|
11
11
|
- **设置页管理**:账号、登录、模型开关都在 DSH 设置里
|
|
12
|
+
- **1M 上下文**:`SCNet-Max` 按平台真实能力声明 `contextWindow: 1048576` / `maxTokens: 131072`(实测 104.5 万 token 输入通过、1.06M 被拒;同样的直连调用方式见 [`tools/repro/scnet-chat.mjs`](tools/repro/scnet-chat.mjs))
|
|
13
|
+
- **抖动自愈**:平台偶发的 `503 + 10008`(路由切换)会被归一化成可重试错误,自动退避重试而不是整轮失败
|
|
12
14
|
|
|
13
|
-

|
|
15
|
+

|
|
14
16
|
|
|
15
17
|
---
|
|
16
18
|
|
|
@@ -159,7 +161,7 @@ dsh plugin --profile desktop add link:.
|
|
|
159
161
|
| 只显示免费模型 | 默认开启。关闭后会把目录里所有模型都注册出来(可能产生费用) |
|
|
160
162
|
| 刷新模型 | 重新读模型目录并用网关校验可用性 |
|
|
161
163
|
|
|
162
|
-

|
|
164
|
+

|
|
163
165
|
|
|
164
166
|
---
|
|
165
167
|
|
|
@@ -194,9 +196,49 @@ dsh plugin --profile desktop add link:.
|
|
|
194
196
|
| `inputImages` | `true` | 是否声明接受图片 |
|
|
195
197
|
| `reasoning` | `true` | 是否声明支持思考等级 |
|
|
196
198
|
| `verifyWithGateway` | `true` | 刷新时用 `GET /models` 校验 |
|
|
199
|
+
| `retryOnTransientErrors` | `true` | 把 SCNet「5xx 却标成 `invalid_request_error`」的瞬时抖动改写成可重试的服务端错误(见下) |
|
|
197
200
|
|
|
198
201
|
</details>
|
|
199
202
|
|
|
203
|
+
### 运行时开关与清单位置
|
|
204
|
+
|
|
205
|
+
| 状态 | 文件 | 说明 |
|
|
206
|
+
| --- | --- | --- |
|
|
207
|
+
| 「只显示免费模型」 | `$DSH_HOME/scnet-connect-preferences.json` | 设置页切换后会记住;**优先级高于配置文件里的 `freeOnly`**。想回到配置文件的值,删掉这个文件 |
|
|
208
|
+
| 「退出登录 / 恢复使用」 | `$DSH_HOME/scnet-connect-suspended.json` | 退出后连客户端凭据一并停用,重启 DSH 仍生效 |
|
|
209
|
+
| 免客户端登录会话 | `$DSH_HOME/scnet-session.json` | 含 Cookie 与 `sk-` key(创建时按 `0600`),退出即删 |
|
|
210
|
+
| 诊断快照 | `$DSH_HOME/scnet-connect-status[.<profile>].json` | 只读排障用,`registered` / 模型 / 掩码 key;文件名带 profile,多个 profile 不会互相覆盖 |
|
|
211
|
+
|
|
212
|
+
### 关于模型元数据
|
|
213
|
+
|
|
214
|
+
模型描述符的字段来源是**配置 > 客户端目录 > 内置默认**,但有两处按目录的真实语义读:
|
|
215
|
+
|
|
216
|
+
- **输入能力**:目录里没写 `input` 就按纯文本处理(不会凭空给陌生模型加图片能力);
|
|
217
|
+
- **思考等级**:目录里 `thinkingMode: "levels"` 或 `reasoning: true` 才算支持思考。当前 `SCNet-Max` 的目录条目是 `reasoning: false` + `thinkingMode: "levels"` —— 客户端 UI 也是按分等级展示的,所以插件判为支持;
|
|
218
|
+
- **工具调用**:目录标 `toolCalling: false` 的模型仍会注册(DSH 是 agent,无法在描述符里表达「不下发工具」),但设置页会给它打「不支持工具」标,选中前请留意。
|
|
219
|
+
|
|
220
|
+
### 关于 `retryOnTransientErrors`
|
|
221
|
+
|
|
222
|
+
SCNet 在推理实例切换/回收时会返回这种响应:
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
HTTP 503
|
|
226
|
+
{"error":{"message":"The model's routing or availability changed while processing the request. Please try again.","type":"invalid_request_error","code":"10008"}}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
状态码是 5xx,但响应体 `type` 写的是 `invalid_request_error`。DSH 的错误归类按正则顺序扫描文本,`invalid.?request` 排在 `5xx → SERVER` 前面,于是把它判成 **`INVALID_REQUEST`(不可重试)**,一次退避都不做,本轮直接失败。
|
|
230
|
+
|
|
231
|
+
插件默认把这层归一化打开:只针对「状态码 ≥ 500 且 `type` 是 `invalid_request_error`」的响应,把 `type` 改写为 `api_error`(状态码与 message 原样保留),让归类落到 `SERVER`,交给 DSH 的 5 次指数退避重试。
|
|
232
|
+
|
|
233
|
+
刻意不碰的:真正的 `400`(如参数错误 `10013`)、`401/403/402/429`、以及 2xx 流式响应。
|
|
234
|
+
|
|
235
|
+
实测(本地假网关,前 2 次返回上述 503):
|
|
236
|
+
|
|
237
|
+
| 配置 | chat 请求数 | 结果 |
|
|
238
|
+
| --- | --- | --- |
|
|
239
|
+
| `retryOnTransientErrors: false` | 2(无重试) | 失败,`code: INVALID_REQUEST` |
|
|
240
|
+
| `retryOnTransientErrors: true`(默认) | 4(2 次 503 后成功) | 正常完成 |
|
|
241
|
+
|
|
200
242
|
---
|
|
201
243
|
|
|
202
244
|
## 故障排查
|
|
@@ -210,15 +252,17 @@ dsh plugin --profile desktop add link:.
|
|
|
210
252
|
| 提示「平台会话已过期」 | 在设置页重新登录一次 |
|
|
211
253
|
| 短信收不到 | 确认手机号正确、图形验证码没填错;平台有 60 秒与每小时次数限制 |
|
|
212
254
|
| 上游 429 | 插件不屏蔽限流,降低并发或稍等重试 |
|
|
255
|
+
| 报 `503` + `code: 10008` / `10009` | 平台侧路由抖动。默认会自动重试;若已被手动关掉 `retryOnTransientErrors`,打开它或稍后重发 |
|
|
213
256
|
|
|
214
257
|
---
|
|
215
258
|
|
|
216
259
|
## 安全
|
|
217
260
|
|
|
218
261
|
- **不接收账号密码**:只走短信验证码,密码从不经过插件。
|
|
219
|
-
- 登录会话存于 `$DSH_HOME/scnet-session.json
|
|
262
|
+
- 登录会话存于 `$DSH_HOME/scnet-session.json`,创建时按 `0600` 打开、退出登录即删除。Windows 上 `chmod` 只切只读位,实际可读性由该目录的 ACL 决定,请自行确认 ACL 没有放开。
|
|
220
263
|
- **这两样东西等价于你的账号权限**:不要复制给他人,不要提交进仓库。
|
|
221
|
-
-
|
|
264
|
+
- 插件只在本机回环地址上暴露设置页路由,响应里**不返回** Key / Cookie / 验证码明文(只给掩码 `sk-NTM4L…MjM=`)。
|
|
265
|
+
- 这些路由当前**没有额外的来源校验**:能访问本机 `127.0.0.1:<port>` 的进程(含本机其他程序)可以读取状态、触发刷新/登出。不要把这个端口通过反向代理暴露到局域网。
|
|
222
266
|
- 若用 `apiKey` 配置项,它以明文存在 profile 的 `cordis.patch.yml` 里,请自行控制权限。
|
|
223
267
|
|
|
224
268
|
> `sk-` Key 是账号级凭据、与 SCNet 客户端共用,平台没有吊销接口。
|
|
@@ -226,12 +270,33 @@ dsh plugin --profile desktop add link:.
|
|
|
226
270
|
|
|
227
271
|
---
|
|
228
272
|
|
|
273
|
+
## 卸载
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
dsh plugin --profile desktop remove dsh-scnet-connect
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
插件自己的状态文件不会跟着删掉(它们是本机排障/偏好数据,不是包内容),需要时手动清理:
|
|
280
|
+
|
|
281
|
+
```powershell
|
|
282
|
+
Remove-Item "$env:USERPROFILE\.dsh\scnet-connect-status*.json" -ErrorAction SilentlyContinue
|
|
283
|
+
Remove-Item "$env:USERPROFILE\.dsh\scnet-connect-suspended.json" -ErrorAction SilentlyContinue
|
|
284
|
+
Remove-Item "$env:USERPROFILE\.dsh\scnet-connect-preferences.json" -ErrorAction SilentlyContinue
|
|
285
|
+
Remove-Item "$env:USERPROFILE\.dsh\scnet-session.json" -ErrorAction SilentlyContinue
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
`scnet-session.json` 里有 `sk-` key 与平台 Cookie,不再使用本插件时建议一并删除。
|
|
289
|
+
|
|
290
|
+
---
|
|
291
|
+
|
|
229
292
|
## 已知限制
|
|
230
293
|
|
|
231
294
|
- 免费模型取决于平台:目前只有 `SCNet-Max` 标记为 `free`。
|
|
232
295
|
- 「免费」是平台侧的**限时**政策,不是永久免费;本插件不提供额度,也不绕过计费。
|
|
233
296
|
- 插件内**不提供微信扫码**(平台二维码接口对非微信客户端恒返回 402)。
|
|
234
297
|
- 用的是未公开的客户端内部接口,SCNet 客户端升级后可能变化。
|
|
298
|
+
- 模型功能要求 DSH `>=0.2.0-rc.1`;**设置页**需要 `0.2.2` 及以上(`settings.section` slot)。低版本上模型照常可用,只是没有设置页卡片,登录要在配置里指定 `apiKey`。
|
|
299
|
+
- 上下文窗口按平台实测值声明 1M,但单次请求的 `prompt + max_tokens` 仍受 1,048,576 总量约束;DSH 的自动压缩阈值通常留有余量,若手动调大压缩阈值,接近上限的长对话可能被上游以 `10013` 拒绝。
|
|
235
300
|
|
|
236
301
|
---
|
|
237
302
|
|
|
@@ -239,19 +304,22 @@ dsh plugin --profile desktop add link:.
|
|
|
239
304
|
|
|
240
305
|
```bash
|
|
241
306
|
git clone https://github.com/Phoeky/dsh-scnet-connect.git && cd dsh-scnet-connect
|
|
242
|
-
node tools/check.mjs
|
|
243
|
-
node tools/check.mjs --live
|
|
307
|
+
node tools/check.mjs # 自检:凭据、模型目录、账号
|
|
308
|
+
node tools/check.mjs --live # 额外请求一次 /models
|
|
309
|
+
npm run check:fetch # 错误归一化 + 归类(离线,无需凭据)
|
|
244
310
|
```
|
|
245
311
|
|
|
246
312
|
```text
|
|
247
313
|
lib/index.js 插件入口:注册 provider、设置页路由
|
|
248
314
|
lib/client.js 浏览器侧:设置页卡片
|
|
315
|
+
lib/scnet-fetch.js 上游 5xx 抖动的错误归一化(可重试)
|
|
249
316
|
lib/scnet-login.js 免客户端登录状态机
|
|
250
317
|
lib/scnet-platform.js 平台接口封装
|
|
251
318
|
lib/scnet-web-login.js 短信登录与验证码
|
|
252
319
|
lib/scnet-catalog.js 模型目录发现与过滤
|
|
253
320
|
lib/scnet-credential.js 客户端凭据读取
|
|
254
321
|
lib/scnet-models.js 模型描述符
|
|
322
|
+
tools/fake-gateway.mjs 本地假网关(前 N 次 chat 返回 503/10008,用于复现重试)
|
|
255
323
|
```
|
|
256
324
|
|
|
257
325
|
## License
|
package/cordis.patch.yml
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
# maxTokens: 0 # 0 = 跟随客户端目录 / 内置默认
|
|
25
25
|
# inputImages: true
|
|
26
26
|
# reasoning: true
|
|
27
|
-
# verifyWithGateway: true
|
|
28
|
-
#
|
|
27
|
+
# verifyWithGateway: true # 刷新模型时用 GET /models 校验可用性
|
|
28
|
+
# retryOnTransientErrors: true # 把 5xx 却自称 invalid_request 的抖动改写成可重试错误
|
|
29
|
+
# freeOnly: true # 只注册免费模型(默认开;设置页切换会覆盖它)
|
|
29
30
|
# credentialSource: auto # auto | client | login
|
package/lib/client.js
CHANGED
|
@@ -247,6 +247,9 @@ window.__ModuleLoader__.load({
|
|
|
247
247
|
function AccountGroup({ data }) {
|
|
248
248
|
const account = data && data.account;
|
|
249
249
|
const credential = data && data.credential;
|
|
250
|
+
// 账号可能来自 SCNet 客户端(带 loggedIn),也可能来自设置页的免客户端登录
|
|
251
|
+
// (没有 loggedIn)。不区分来源就会把「已登录」显示成「未检测到登录态」。
|
|
252
|
+
const viaLogin = Boolean(account) && (account.source === 'login' || account.fromLogin === true);
|
|
250
253
|
const rows = [];
|
|
251
254
|
|
|
252
255
|
if (account) {
|
|
@@ -281,8 +284,8 @@ window.__ModuleLoader__.load({
|
|
|
281
284
|
h(
|
|
282
285
|
'span',
|
|
283
286
|
{ style: styles.value },
|
|
284
|
-
account.loggedIn ? 'SCNet 客户端已登录' : '未检测到登录态',
|
|
285
|
-
account.tokenExpiresAt
|
|
287
|
+
viaLogin ? '免客户端登录已登录' : account.loggedIn ? 'SCNet 客户端已登录' : '未检测到登录态',
|
|
288
|
+
!viaLogin && account.tokenExpiresAt
|
|
286
289
|
? h('span', { style: styles.muted }, `(平台 token ${formatExpiry(account.tokenExpiresAt)})`)
|
|
287
290
|
: null,
|
|
288
291
|
),
|
|
@@ -395,6 +398,11 @@ window.__ModuleLoader__.load({
|
|
|
395
398
|
Array.isArray(model.input) && model.input.includes('image')
|
|
396
399
|
? h('span', { style: styles.badge }, '图片')
|
|
397
400
|
: null,
|
|
401
|
+
// 目录明确说「不支持工具调用」时标出来:DSH 是 agent,仍然会下发
|
|
402
|
+
// 工具定义,用户选它之前值得知道这个模型可能忽略工具。
|
|
403
|
+
model.toolCalling === false
|
|
404
|
+
? h('span', { style: { ...styles.badge, ...styles.warn } }, '不支持工具')
|
|
405
|
+
: null,
|
|
398
406
|
),
|
|
399
407
|
h(
|
|
400
408
|
'span',
|
|
@@ -590,7 +598,10 @@ window.__ModuleLoader__.load({
|
|
|
590
598
|
];
|
|
591
599
|
|
|
592
600
|
// 已退出登录:本插件整体停用(连客户端凭据也不再供 key)。
|
|
593
|
-
|
|
601
|
+
// 但仍然把登录表单留在下面 —— 退出后想直接换个账号登录是常见动作,
|
|
602
|
+
// 逼用户先点「恢复使用」再登录只是多余的一步(登录成功本身就会恢复供 key)。
|
|
603
|
+
const suspendedView = Boolean(data && data.suspended);
|
|
604
|
+
if (suspendedView) {
|
|
594
605
|
children.push(
|
|
595
606
|
h(
|
|
596
607
|
'div',
|
|
@@ -607,10 +618,13 @@ window.__ModuleLoader__.load({
|
|
|
607
618
|
{ type: 'button', style: buttonStyle(busy !== null), disabled: busy !== null, onClick: resume },
|
|
608
619
|
busy === 'resume' ? '恢复中…' : '恢复使用',
|
|
609
620
|
),
|
|
610
|
-
h(
|
|
621
|
+
h(
|
|
622
|
+
'span',
|
|
623
|
+
{ style: styles.hint },
|
|
624
|
+
'恢复后按「当前凭据来源」重新供 key;也可以直接在下面重新登录。',
|
|
625
|
+
),
|
|
611
626
|
),
|
|
612
627
|
);
|
|
613
|
-
return h('div', { style: styles.group }, h('div', { style: styles.groupTitle }, '免客户端登录'), children);
|
|
614
628
|
}
|
|
615
629
|
|
|
616
630
|
if (login.maskedKey) {
|
|
@@ -645,7 +659,8 @@ window.__ModuleLoader__.load({
|
|
|
645
659
|
);
|
|
646
660
|
}
|
|
647
661
|
|
|
648
|
-
|
|
662
|
+
// 停用状态下优先展示登录表单(重新登录即恢复供 key),不展示「检查/退出」。
|
|
663
|
+
if (authorized && !suspendedView) {
|
|
649
664
|
children.push(
|
|
650
665
|
h(
|
|
651
666
|
'div',
|
|
@@ -681,9 +696,11 @@ window.__ModuleLoader__.load({
|
|
|
681
696
|
h(
|
|
682
697
|
'div',
|
|
683
698
|
{ style: styles.hint, key: 'intro' },
|
|
684
|
-
|
|
685
|
-
? '
|
|
686
|
-
:
|
|
699
|
+
suspendedView
|
|
700
|
+
? '重新登录会同时恢复本插件供 key。'
|
|
701
|
+
: expired
|
|
702
|
+
? '平台会话已过期,重新用手机验证码登录一次即可(不需要客户端)。'
|
|
703
|
+
: '不安装 SCNet 客户端也能用:用手机号收一条短信验证码登录平台,插件会自己换到模型 Key。',
|
|
687
704
|
),
|
|
688
705
|
);
|
|
689
706
|
|
|
@@ -777,9 +794,21 @@ window.__ModuleLoader__.load({
|
|
|
777
794
|
const status = useStatus();
|
|
778
795
|
const { loading, data, error, refreshing, refresh } = status;
|
|
779
796
|
const [override, setOverride] = react.useState(null);
|
|
780
|
-
//
|
|
797
|
+
// 登录/切换开关会带回新的 status;优先显示它,直到下一次 load/refresh。
|
|
781
798
|
const view = override || data;
|
|
782
799
|
|
|
800
|
+
/**
|
|
801
|
+
* 「刷新模型」必须先把 override 清掉。
|
|
802
|
+
*
|
|
803
|
+
* 否则一旦本页做过登录/退出/切换开关(`override` 被写入),
|
|
804
|
+
* 之后每次刷新都只是更新 `data`,而 `view` 仍然取 `override` ——
|
|
805
|
+
* 请求确实发出去了,界面却停在旧状态(连「状态更新于」的时间都不动)。
|
|
806
|
+
*/
|
|
807
|
+
const doRefresh = react.useCallback(async () => {
|
|
808
|
+
setOverride(null);
|
|
809
|
+
await refresh();
|
|
810
|
+
}, [refresh]);
|
|
811
|
+
|
|
783
812
|
const buttonStyle = refreshing ? { ...styles.button, ...styles.buttonDisabled } : styles.button;
|
|
784
813
|
|
|
785
814
|
return h(
|
|
@@ -804,7 +833,7 @@ window.__ModuleLoader__.load({
|
|
|
804
833
|
type: 'button',
|
|
805
834
|
style: buttonStyle,
|
|
806
835
|
disabled: refreshing,
|
|
807
|
-
onClick:
|
|
836
|
+
onClick: doRefresh,
|
|
808
837
|
title: '重新读取模型目录,并用网关校验可用性',
|
|
809
838
|
},
|
|
810
839
|
refreshing ? '刷新中…' : '刷新模型',
|