dsh-remote-bridge 2.3.1 → 2.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.en.md +6 -6
- package/README.md +6 -6
- package/docs/api/asyncapi.yaml +1 -1
- package/docs/api/openapi.yaml +110 -2
- package/kotlin-sdk/build.gradle.kts +1 -1
- package/lib/index.js +276 -12
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<img alt="Version" src="https://img.shields.io/badge/version-v2.3.
|
|
12
|
+
<img alt="Version" src="https://img.shields.io/badge/version-v2.3.4-087f8c">
|
|
13
13
|
<img alt="DSH plugin" src="https://img.shields.io/badge/DeepSeek_Harness-plugin-1f2328">
|
|
14
14
|
<img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-586069">
|
|
15
15
|
<img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-2da44e">
|
|
@@ -73,10 +73,10 @@ dsh plugin --profile web add dsh-remote-bridge
|
|
|
73
73
|
dsh plugin --profile web add dsh-remote-bridge --registry https://registry.npmmirror.com
|
|
74
74
|
|
|
75
75
|
# 2. tarball from the GitHub release
|
|
76
|
-
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.3.
|
|
76
|
+
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.3.4/dsh-remote-bridge-v2.3.4.tgz
|
|
77
77
|
|
|
78
78
|
# 3. tarball from the Gitee release (faster from mainland China)
|
|
79
|
-
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.3.
|
|
79
|
+
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.3.4/dsh-remote-bridge-v2.3.4.tgz
|
|
80
80
|
|
|
81
81
|
npx @deepseek-ai/dsh web
|
|
82
82
|
```
|
|
@@ -93,7 +93,7 @@ Restart DSH after installation, then open settings — The section is titled **
|
|
|
93
93
|
To install a downloaded release archive:
|
|
94
94
|
|
|
95
95
|
```sh
|
|
96
|
-
dsh plugin --profile web add ./dsh-remote-bridge-v2.3.
|
|
96
|
+
dsh plugin --profile web add ./dsh-remote-bridge-v2.3.4.tgz
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
## First connection
|
|
@@ -327,11 +327,11 @@ pnpm pack
|
|
|
327
327
|
|
|
328
328
|
`npm pack` produces roughly a **0.25 MB** archive: 2.2.0 dropped the standalone page and its editor dependencies and turned source maps off, down from 1.72 MB.
|
|
329
329
|
|
|
330
|
-
v2.3.
|
|
330
|
+
v2.3.4 was developed against DSH `master@47f943859bef60e4160492346772ded9b24f765a`. CI covers Windows, Ubuntu, and macOS.
|
|
331
331
|
|
|
332
332
|
## Project
|
|
333
333
|
|
|
334
|
-
- Version: `v2.3.
|
|
334
|
+
- Version: `v2.3.4`
|
|
335
335
|
- Author: upstream [Hakunm](https://github.com/Hakunm); this fork maintained by [westanke](https://github.com/westanke)
|
|
336
336
|
- Repository: [GitHub](https://github.com/westanke/dsh-remote-bridge) · [Gitee](https://gitee.com/westanke/dsh-remote-bridge)
|
|
337
337
|
- npm: [`dsh-remote-bridge`](https://www.npmjs.com/package/dsh-remote-bridge) ([mainland-China mirror](https://registry.npmmirror.com/dsh-remote-bridge))
|
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
</p>
|
|
10
10
|
|
|
11
11
|
<p align="center">
|
|
12
|
-
<img alt="Version" src="https://img.shields.io/badge/version-v2.3.
|
|
12
|
+
<img alt="Version" src="https://img.shields.io/badge/version-v2.3.4-087f8c">
|
|
13
13
|
<img alt="DSH plugin" src="https://img.shields.io/badge/DeepSeek_Harness-plugin-1f2328">
|
|
14
14
|
<img alt="Platforms" src="https://img.shields.io/badge/platform-Windows%20%7C%20macOS%20%7C%20Linux-586069">
|
|
15
15
|
<img alt="License" src="https://img.shields.io/badge/license-AGPL--3.0-2da44e">
|
|
@@ -70,10 +70,10 @@ dsh plugin --profile web add dsh-remote-bridge
|
|
|
70
70
|
dsh plugin --profile web add dsh-remote-bridge --registry https://registry.npmmirror.com
|
|
71
71
|
|
|
72
72
|
# ② GitHub Release 的 tarball
|
|
73
|
-
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.3.
|
|
73
|
+
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.3.4/dsh-remote-bridge-v2.3.4.tgz
|
|
74
74
|
|
|
75
75
|
# ③ Gitee Release 的 tarball(国内访问更快)
|
|
76
|
-
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.3.
|
|
76
|
+
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.3.4/dsh-remote-bridge-v2.3.4.tgz
|
|
77
77
|
|
|
78
78
|
npx @deepseek-ai/dsh web
|
|
79
79
|
```
|
|
@@ -89,7 +89,7 @@ npx @deepseek-ai/dsh web
|
|
|
89
89
|
从 GitHub Release 下载离线安装包时,可直接安装 tarball:
|
|
90
90
|
|
|
91
91
|
```sh
|
|
92
|
-
dsh plugin --profile web add ./dsh-remote-bridge-v2.3.
|
|
92
|
+
dsh plugin --profile web add ./dsh-remote-bridge-v2.3.4.tgz
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
## 第一次使用
|
|
@@ -319,11 +319,11 @@ pnpm pack
|
|
|
319
319
|
|
|
320
320
|
`npm pack` 的产物约 **0.25 MB**:2.2.0 删除独立页及其编辑器依赖、并关闭 source map 之后,从 1.72 MB 降下来。
|
|
321
321
|
|
|
322
|
-
v2.3.
|
|
322
|
+
v2.3.4 基于 DSH `master@47f943859bef60e4160492346772ded9b24f765a` 开发,CI 覆盖 Windows、Ubuntu 和 macOS。
|
|
323
323
|
|
|
324
324
|
## 项目信息
|
|
325
325
|
|
|
326
|
-
- 当前版本:`v2.3.
|
|
326
|
+
- 当前版本:`v2.3.4`
|
|
327
327
|
- 作者:上游 [Hakunm](https://github.com/Hakunm),本 fork 维护 [westanke](https://github.com/westanke)
|
|
328
328
|
- 仓库:[GitHub](https://github.com/westanke/dsh-remote-bridge) · [Gitee](https://gitee.com/westanke/dsh-remote-bridge)
|
|
329
329
|
- npm:[`dsh-remote-bridge`](https://www.npmjs.com/package/dsh-remote-bridge)([国内镜像](https://registry.npmmirror.com/dsh-remote-bridge))
|
package/docs/api/asyncapi.yaml
CHANGED
package/docs/api/openapi.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
openapi: 3.1.0
|
|
2
2
|
info:
|
|
3
3
|
title: DeepSeek Harness Mobile API
|
|
4
|
-
version: 2.3.
|
|
4
|
+
version: 2.3.4
|
|
5
5
|
description: Versioned file and chat BFF for mobile and third-party DSH clients. HTTP and HTTPS are both supported.
|
|
6
6
|
servers:
|
|
7
7
|
- url: http://127.0.0.1:3090/api/v1
|
|
@@ -24,7 +24,7 @@ paths:
|
|
|
24
24
|
properties:
|
|
25
25
|
ok: { type: boolean, const: true }
|
|
26
26
|
version: { type: string, const: v1 }
|
|
27
|
-
pluginVersion: { type: string, const: 2.3.
|
|
27
|
+
pluginVersion: { type: string, const: 2.3.4 }
|
|
28
28
|
/pairings/exchange:
|
|
29
29
|
post:
|
|
30
30
|
security: []
|
|
@@ -334,6 +334,56 @@ paths:
|
|
|
334
334
|
schema: { $ref: '#/components/schemas/ProviderSettings' }
|
|
335
335
|
'400': { $ref: '#/components/responses/Error' }
|
|
336
336
|
'409': { $ref: '#/components/responses/Error' }
|
|
337
|
+
/settings/plugins:
|
|
338
|
+
get:
|
|
339
|
+
operationId: listPlugins
|
|
340
|
+
description: Lists every package the active profile declares or loads, with its resolved state.
|
|
341
|
+
parameters:
|
|
342
|
+
- name: checkUpdates
|
|
343
|
+
in: query
|
|
344
|
+
required: false
|
|
345
|
+
schema: { type: string, enum: ['1'] }
|
|
346
|
+
description: 'When `1`, also queries the npm registry for each installed third-party package. Adds seconds to the response, so it is opt-in.'
|
|
347
|
+
responses:
|
|
348
|
+
'200':
|
|
349
|
+
description: 'Plugin inventory. `available: false` plus a `reason` means the profile could not be resolved or read; the endpoint does not fail for that.'
|
|
350
|
+
content:
|
|
351
|
+
application/json:
|
|
352
|
+
schema: { $ref: '#/components/schemas/PluginInventory' }
|
|
353
|
+
patch:
|
|
354
|
+
operationId: setPluginLoaded
|
|
355
|
+
description: |
|
|
356
|
+
Enables or disables a plugin by adding or removing its name in `dsh.profile.bundles`.
|
|
357
|
+
Dependency declarations are left alone (the package stays on disk and can be re-enabled
|
|
358
|
+
without downloading), and no package install runs. A backup of the profile manifest is
|
|
359
|
+
written before any change. **Bundles are read only at startup**, so the running DSH keeps
|
|
360
|
+
the previous set until it restarts.
|
|
361
|
+
requestBody:
|
|
362
|
+
required: true
|
|
363
|
+
content:
|
|
364
|
+
application/json:
|
|
365
|
+
schema:
|
|
366
|
+
type: object
|
|
367
|
+
required: [name, loaded]
|
|
368
|
+
properties:
|
|
369
|
+
name: { type: string, examples: [dsh-ffmpeg] }
|
|
370
|
+
loaded: { type: boolean, description: 'true to enable, false to disable.' }
|
|
371
|
+
responses:
|
|
372
|
+
'200':
|
|
373
|
+
description: 'Applied. The response carries `restartRequired: true`.'
|
|
374
|
+
content:
|
|
375
|
+
application/json:
|
|
376
|
+
schema: { $ref: '#/components/schemas/PluginToggleResult' }
|
|
377
|
+
'400':
|
|
378
|
+
description: 'Bad request, official package, or package not installed. Error codes: REQUEST_INVALID, PLUGIN_NAME_INVALID, PLUGIN_OFFICIAL_IMMUTABLE, PLUGIN_NOT_INSTALLED.'
|
|
379
|
+
content:
|
|
380
|
+
application/json:
|
|
381
|
+
schema: { $ref: '#/components/schemas/Error' }
|
|
382
|
+
'503':
|
|
383
|
+
description: 'The profile could not be resolved or its manifest could not be read. Error codes: PROFILE_UNKNOWN, MANIFEST_UNREADABLE.'
|
|
384
|
+
content:
|
|
385
|
+
application/json:
|
|
386
|
+
schema: { $ref: '#/components/schemas/Error' }
|
|
337
387
|
/settings/models:
|
|
338
388
|
get:
|
|
339
389
|
operationId: listModelCatalog
|
|
@@ -837,6 +887,64 @@ components:
|
|
|
837
887
|
application/json:
|
|
838
888
|
schema: { $ref: '#/components/schemas/ErrorEnvelope' }
|
|
839
889
|
schemas:
|
|
890
|
+
PluginEntry:
|
|
891
|
+
type: object
|
|
892
|
+
description: One package the active profile declares or loads. Contents are limited to these fields; the package manifest itself is never forwarded.
|
|
893
|
+
required: [name, declared, installed, loaded, official, state, description, homepage, updateAvailable, latestVersion, toggleable]
|
|
894
|
+
properties:
|
|
895
|
+
name: { type: string, examples: [dsh-ffmpeg, '@liustack/modlens'] }
|
|
896
|
+
declared:
|
|
897
|
+
type: [string, 'null']
|
|
898
|
+
description: Version range declared in `dependencies`, e.g. `^0.4.5`.
|
|
899
|
+
installed:
|
|
900
|
+
type: [string, 'null']
|
|
901
|
+
description: Version actually present in the profile's node_modules.
|
|
902
|
+
loaded:
|
|
903
|
+
type: boolean
|
|
904
|
+
description: Whether the name appears in `dsh.profile.bundles` (the load list; its order is the load order).
|
|
905
|
+
official: { type: boolean, description: 'True for `@deepseek-ai/` packages, which ship with the runtime.' }
|
|
906
|
+
state:
|
|
907
|
+
type: string
|
|
908
|
+
enum: [loaded, runtime-provided, installed-not-loaded, declared-missing]
|
|
909
|
+
description: '`runtime-provided` is normal, not a problem: those packages ship with DSH and are not installed under the profile.'
|
|
910
|
+
description: { type: [string, 'null'] }
|
|
911
|
+
homepage: { type: [string, 'null'], description: 'Normalized http(s) URL; git+ prefixes and .git suffixes are stripped.' }
|
|
912
|
+
updateAvailable:
|
|
913
|
+
type: [boolean, 'null']
|
|
914
|
+
description: 'Tri-state on purpose: null means not checked, which differs from false ("already latest").'
|
|
915
|
+
latestVersion: { type: [string, 'null'] }
|
|
916
|
+
toggleable:
|
|
917
|
+
type: boolean
|
|
918
|
+
description: 'False for official packages and for packages not installed in the profile.'
|
|
919
|
+
PluginInventory:
|
|
920
|
+
type: object
|
|
921
|
+
required: [profile, profilePath, available, reason, loadedCount, problemCount, items]
|
|
922
|
+
properties:
|
|
923
|
+
profile: { type: [string, 'null'], description: 'Resolved from DSH_PROFILE, `--profile`, or the `dsh <name>` subcommand shorthand.' }
|
|
924
|
+
profilePath: { type: [string, 'null'] }
|
|
925
|
+
available: { type: boolean }
|
|
926
|
+
reason:
|
|
927
|
+
type: [string, 'null']
|
|
928
|
+
enum: [PROFILE_UNKNOWN, MANIFEST_UNREADABLE, null]
|
|
929
|
+
loadedCount: { type: integer }
|
|
930
|
+
problemCount: { type: integer, description: 'Counts installed-not-loaded and declared-missing; runtime-provided is not counted.' }
|
|
931
|
+
items:
|
|
932
|
+
type: array
|
|
933
|
+
items: { $ref: '#/components/schemas/PluginEntry' }
|
|
934
|
+
PluginToggleResult:
|
|
935
|
+
type: object
|
|
936
|
+
description: Result of enabling or disabling a plugin. Only `dsh.profile.bundles` is rewritten; dependency declarations are never touched and no package install runs.
|
|
937
|
+
required: [name, loaded, backupName, bundleCount, restartRequired]
|
|
938
|
+
properties:
|
|
939
|
+
name: { type: string }
|
|
940
|
+
loaded: { type: boolean }
|
|
941
|
+
backupName:
|
|
942
|
+
type: string
|
|
943
|
+
description: 'Name of the backup written before the change (empty for a no-op). A broken profile manifest means DSH will not boot, so a backup is mandatory.'
|
|
944
|
+
bundleCount: { type: integer, description: 'New length of the bundles array.' }
|
|
945
|
+
restartRequired:
|
|
946
|
+
type: boolean
|
|
947
|
+
description: 'Always true: bundles are read only at startup, so the running process keeps the old set until DSH restarts.'
|
|
840
948
|
WirePath:
|
|
841
949
|
type: string
|
|
842
950
|
description: POSIX-style relative path. Absolute paths, backslashes, empty segments, dot segments, drive letters, UNC and null bytes are rejected.
|
package/lib/index.js
CHANGED
|
@@ -3239,24 +3239,105 @@ function defaultDisplayName() {
|
|
|
3239
3239
|
//#endregion
|
|
3240
3240
|
//#region src/host/plugin-inventory.ts
|
|
3241
3241
|
/**
|
|
3242
|
+
* 把 profile 标识归一化成 profile 名。
|
|
3243
|
+
*
|
|
3244
|
+
* 内核接受**两种**写法,两者都合法:
|
|
3245
|
+
*
|
|
3246
|
+
* - `--profile web` —— profile 名;
|
|
3247
|
+
* - `--profile /home/u/.dsh/profiles/web` —— profile 的绝对目录。内核启动器文档里
|
|
3248
|
+
* 写的正是 `--profile ${basename(profileDir)}`,**内核自己会取 basename**。
|
|
3249
|
+
*
|
|
3250
|
+
* 本插件此前只认第一种,于是把第二种的**整条路径**当成了 profile 名。而 `path.join`
|
|
3251
|
+
* 并不会因为后面一段是绝对路径就重置(那是 `path.resolve` 的行为),于是拼出:
|
|
3252
|
+
*
|
|
3253
|
+
* /home/u/.dsh/profiles/home/u/.dsh/profiles/desktop
|
|
3254
|
+
*
|
|
3255
|
+
* —— 必然 ENOENT。**官方 Electron 桌面版恰好用第二种启动内核**,所以在桌面版里,
|
|
3256
|
+
* 手机上只会看到「服务器读不出插件清单:package.json 可能损坏、缺失或没有读取权限」,
|
|
3257
|
+
* 而磁盘上的 package.json 完好无损、权限也对。web 版(`dsh web` / `--profile web`)
|
|
3258
|
+
* 走的是名字形式,不受影响 —— 这就是这个 bug 能潜伏到桌面版才暴露的原因。
|
|
3259
|
+
*
|
|
3260
|
+
* 归一化只做一件事:**含路径分隔符时取最后一段**;不含则原样返回。于是
|
|
3261
|
+
* 「名字」「绝对目录」「相对目录」「Windows 反斜杠路径」「末尾带斜杠」五者同归一条规则。
|
|
3262
|
+
*/
|
|
3263
|
+
function normalizeProfileName(value) {
|
|
3264
|
+
const trimmed = value.trim();
|
|
3265
|
+
if (trimmed === "") return null;
|
|
3266
|
+
if (!/[/\\]/.test(trimmed)) return trimmed;
|
|
3267
|
+
const withoutTrailing = trimmed.replace(/[/\\]+$/, "");
|
|
3268
|
+
if (withoutTrailing === "") return null;
|
|
3269
|
+
const cut = Math.max(withoutTrailing.lastIndexOf("/"), withoutTrailing.lastIndexOf("\\"));
|
|
3270
|
+
const base = withoutTrailing.slice(cut + 1);
|
|
3271
|
+
return base === "" ? null : base;
|
|
3272
|
+
}
|
|
3273
|
+
/**
|
|
3242
3274
|
* 解析当前 profile 名。
|
|
3243
3275
|
*
|
|
3244
|
-
* 依次尝试:`DSH_PROFILE` 环境变量 → `--profile=<name>` → `--profile <name
|
|
3276
|
+
* 依次尝试:`DSH_PROFILE` 环境变量 → `--profile=<name>` → `--profile <name>`
|
|
3277
|
+
* → **子命令简写**(`dsh web` 等价于 `dsh --profile web`)。
|
|
3245
3278
|
* 都拿不到时返回 null(调用方不要猜,猜错会读到另一个 profile 的清单)。
|
|
3279
|
+
*
|
|
3280
|
+
* **四个取值来源一律过 `normalizeProfileName`**:它们都可能装着一个 profile 目录
|
|
3281
|
+
* 而不是名字(官方 Electron 桌面版就是),见该函数的说明。
|
|
3282
|
+
*
|
|
3283
|
+
* 最后那条简写路径是补上的一个真 bug:内核的启动器文档写得很清楚 ——
|
|
3284
|
+
*
|
|
3285
|
+
* > `dsh <name>` abbreviates `dsh --profile <name>`;`dsh web` boots the web
|
|
3286
|
+
* > profile (same as: `dsh --profile web`)
|
|
3287
|
+
*
|
|
3288
|
+
* 而此前这里只认 `--profile` 两种写法。于是**用 `dsh web` 启动的用户会看到
|
|
3289
|
+
* 「服务器判定不出当前使用的插件档案」** —— 明明 profile 就在那儿、插件也正常
|
|
3290
|
+
* 加载着,只有这个页面读不出来。这不是「用户没带参数」,是插件漏了一条启动形式。
|
|
3246
3291
|
*/
|
|
3247
3292
|
function resolveProfileName(input = {}) {
|
|
3248
3293
|
const env = input.env ?? process.env;
|
|
3249
3294
|
const argv = input.argv ?? process.argv;
|
|
3250
3295
|
const fromEnv = env.DSH_PROFILE?.trim();
|
|
3251
|
-
if (fromEnv) return fromEnv;
|
|
3296
|
+
if (fromEnv) return normalizeProfileName(fromEnv);
|
|
3252
3297
|
for (const arg of argv) if (arg.startsWith("--profile=")) {
|
|
3253
3298
|
const value = arg.slice(10).trim();
|
|
3254
|
-
if (value) return value;
|
|
3299
|
+
if (value) return normalizeProfileName(value);
|
|
3255
3300
|
}
|
|
3256
3301
|
const index = argv.indexOf("--profile");
|
|
3257
3302
|
if (index >= 0) {
|
|
3258
3303
|
const value = argv[index + 1]?.trim();
|
|
3259
|
-
if (value && !value.startsWith("-")) return value;
|
|
3304
|
+
if (value && !value.startsWith("-")) return normalizeProfileName(value);
|
|
3305
|
+
return null;
|
|
3306
|
+
}
|
|
3307
|
+
return profileFromSubcommand(argv);
|
|
3308
|
+
}
|
|
3309
|
+
/**
|
|
3310
|
+
* 从子命令简写里取 profile 名(`dsh web` → `web`)。
|
|
3311
|
+
*
|
|
3312
|
+
* 判据照抄内核启动器的解析规则:**跳过所有选项,第一个非选项 token 就是子命令**。
|
|
3313
|
+
* 需要绕开两类 token:
|
|
3314
|
+
*
|
|
3315
|
+
* - 选项本身(以 `-` 开头)—— 例如 `dsh --port 3080 web` 里要先跳过 `--port`;
|
|
3316
|
+
* - **选项的取值** —— 上面那个 `3080` 不是选项也不是子命令,只是 `--port` 的值。
|
|
3317
|
+
* 不跳它的话会把 `3080` 当成 profile 名。内核的选项都是「一个旗标接一个值」的
|
|
3318
|
+
* 单值形式(`--port 3080` / `--patch a.yml`),所以看到选项就把它的下一个 token
|
|
3319
|
+
* 一起跳过,是安全的。
|
|
3320
|
+
*
|
|
3321
|
+
* `plugin` 单独排除:`dsh plugin add xxx` 是管理插件依赖的命令,
|
|
3322
|
+
* 它**不启动任何 profile**,把它当 profile 名会去读一个不存在的目录。
|
|
3323
|
+
*
|
|
3324
|
+
* 取到的 token 同样过 `normalizeProfileName`:桌面版可能把这个位置填成 profile
|
|
3325
|
+
* 目录的绝对路径(`--profile <dir>` 的等价位置参数写法)。
|
|
3326
|
+
*/
|
|
3327
|
+
function profileFromSubcommand(argv) {
|
|
3328
|
+
const start = argv.findIndex((arg) => /(^|[/\\])dsh(\.js|\.cjs|\.mjs)?$/.test(arg));
|
|
3329
|
+
let i = start >= 0 ? start + 1 : 0;
|
|
3330
|
+
while (i < argv.length) {
|
|
3331
|
+
const arg = argv[i];
|
|
3332
|
+
if (arg === void 0 || arg === "") {
|
|
3333
|
+
i += 1;
|
|
3334
|
+
continue;
|
|
3335
|
+
}
|
|
3336
|
+
if (arg.startsWith("-")) {
|
|
3337
|
+
i += arg.includes("=") ? 1 : 2;
|
|
3338
|
+
continue;
|
|
3339
|
+
}
|
|
3340
|
+
return arg === "plugin" ? null : normalizeProfileName(arg);
|
|
3260
3341
|
}
|
|
3261
3342
|
return null;
|
|
3262
3343
|
}
|
|
@@ -3302,10 +3383,13 @@ async function readPluginInventory(input = {}) {
|
|
|
3302
3383
|
const names = [.../* @__PURE__ */ new Set([...Object.keys(dependencies), ...bundles])].sort();
|
|
3303
3384
|
const items = [];
|
|
3304
3385
|
for (const name of names) {
|
|
3305
|
-
const
|
|
3386
|
+
const pkgManifest = await readPackageManifest(profilePath, name);
|
|
3387
|
+
const installedRaw = pkgManifest?.version;
|
|
3388
|
+
const installed = typeof installedRaw === "string" && installedRaw.trim() ? installedRaw : null;
|
|
3306
3389
|
const loaded = bundles.includes(name);
|
|
3307
3390
|
const declared = dependencies[name] ?? null;
|
|
3308
3391
|
const official = name.startsWith("@deepseek-ai/");
|
|
3392
|
+
const description = typeof pkgManifest?.description === "string" && pkgManifest.description.trim() ? pkgManifest.description.trim() : null;
|
|
3309
3393
|
items.push({
|
|
3310
3394
|
name,
|
|
3311
3395
|
declared,
|
|
@@ -3316,7 +3400,12 @@ async function readPluginInventory(input = {}) {
|
|
|
3316
3400
|
loaded,
|
|
3317
3401
|
installed,
|
|
3318
3402
|
official
|
|
3319
|
-
})
|
|
3403
|
+
}),
|
|
3404
|
+
description,
|
|
3405
|
+
homepage: readHomepage(pkgManifest),
|
|
3406
|
+
updateAvailable: null,
|
|
3407
|
+
latestVersion: null,
|
|
3408
|
+
toggleable: !official && installed !== null
|
|
3320
3409
|
});
|
|
3321
3410
|
}
|
|
3322
3411
|
return {
|
|
@@ -3330,6 +3419,68 @@ async function readPluginInventory(input = {}) {
|
|
|
3330
3419
|
};
|
|
3331
3420
|
}
|
|
3332
3421
|
/**
|
|
3422
|
+
* 查 npm registry 上的最新版本(可选功能,默认不开)。
|
|
3423
|
+
*
|
|
3424
|
+
* 为什么默认关:每个包一次 HTTP 请求,清单会从毫秒级变成好几秒,而用户点进
|
|
3425
|
+
* 「插件」页十次里九次只是想看看「装了啥、哪个没启用」。要查更新得显式开。
|
|
3426
|
+
*
|
|
3427
|
+
* 两个刻意的取舍:
|
|
3428
|
+
* - **超时短**(5 秒):一个包查不动不该拖住整张清单,超时就当「查不到」。
|
|
3429
|
+
* - **失败即 null**,不重试:这是锦上添花的信息,为了它排队重试不值当。
|
|
3430
|
+
* 界面拿到 null 会说「未检查」,而不是谎称「已是最新」。
|
|
3431
|
+
*/
|
|
3432
|
+
async function fetchLatestVersion(packageName) {
|
|
3433
|
+
const controller = new AbortController();
|
|
3434
|
+
const timer = setTimeout(() => controller.abort(), 5e3);
|
|
3435
|
+
try {
|
|
3436
|
+
const response = await fetch(`https://registry.npmjs.org/${packageName.replace("/", "%2F")}/latest`, {
|
|
3437
|
+
signal: controller.signal,
|
|
3438
|
+
headers: { accept: "application/json" }
|
|
3439
|
+
});
|
|
3440
|
+
if (!response.ok) return null;
|
|
3441
|
+
const body = await response.json();
|
|
3442
|
+
return typeof body.version === "string" && body.version.trim() ? body.version.trim() : null;
|
|
3443
|
+
} catch {
|
|
3444
|
+
return null;
|
|
3445
|
+
} finally {
|
|
3446
|
+
clearTimeout(timer);
|
|
3447
|
+
}
|
|
3448
|
+
}
|
|
3449
|
+
/**
|
|
3450
|
+
* 给清单里的插件补上「有没有新版本」。
|
|
3451
|
+
*
|
|
3452
|
+
* 只查**确实装在 profile 里**的第三方包:官方包随运行时发布、用户装不了(查了也没用),
|
|
3453
|
+
* 缺失包连本地版本都没有(无从比较)。
|
|
3454
|
+
*
|
|
3455
|
+
* 并发度限制在 6:十几个包全放出去对 registry 不礼貌,也容易触发限流 ——
|
|
3456
|
+
* 而限流之后拿到的 null 会被界面显示成「未检查」,白跑一趟。
|
|
3457
|
+
*/
|
|
3458
|
+
async function withUpdateCheck(inventory) {
|
|
3459
|
+
const targets = inventory.items.filter((item) => !item.official && item.installed !== null);
|
|
3460
|
+
const latest = /* @__PURE__ */ new Map();
|
|
3461
|
+
const queue = [...targets];
|
|
3462
|
+
const workers = Array.from({ length: Math.min(6, queue.length) }, async () => {
|
|
3463
|
+
for (;;) {
|
|
3464
|
+
const next = queue.shift();
|
|
3465
|
+
if (next === void 0) return;
|
|
3466
|
+
latest.set(next.name, await fetchLatestVersion(next.name));
|
|
3467
|
+
}
|
|
3468
|
+
});
|
|
3469
|
+
await Promise.all(workers);
|
|
3470
|
+
return {
|
|
3471
|
+
...inventory,
|
|
3472
|
+
items: inventory.items.map((item) => {
|
|
3473
|
+
if (!latest.has(item.name)) return item;
|
|
3474
|
+
const newest = latest.get(item.name) ?? null;
|
|
3475
|
+
return {
|
|
3476
|
+
...item,
|
|
3477
|
+
latestVersion: newest,
|
|
3478
|
+
updateAvailable: newest === null || item.installed === null ? null : newest !== item.installed
|
|
3479
|
+
};
|
|
3480
|
+
})
|
|
3481
|
+
};
|
|
3482
|
+
}
|
|
3483
|
+
/**
|
|
3333
3484
|
* 判定条目状态。
|
|
3334
3485
|
*
|
|
3335
3486
|
* 顺序很重要:**先判「官方包 + 未落盘」**,否则它们会被判成 `declared-missing` ——
|
|
@@ -3341,11 +3492,37 @@ function resolveState(input) {
|
|
|
3341
3492
|
if (input.official) return "runtime-provided";
|
|
3342
3493
|
return "declared-missing";
|
|
3343
3494
|
}
|
|
3344
|
-
/**
|
|
3345
|
-
|
|
3495
|
+
/**
|
|
3496
|
+
* 读 `node_modules/<name>/package.json` 的整份内容。
|
|
3497
|
+
*
|
|
3498
|
+
* 读不到就返回 null(包没装、或装了一半)。刻意不抛:这是展示用的元数据,
|
|
3499
|
+
* 某个包读不出来不该让整张清单失败。
|
|
3500
|
+
*/
|
|
3501
|
+
async function readPackageManifest(profilePath, packageName) {
|
|
3346
3502
|
const modulePath = path.join(profilePath, "node_modules", ...packageName.split("/"));
|
|
3347
|
-
|
|
3348
|
-
|
|
3503
|
+
return readJson$1(path.join(modulePath, "package.json"));
|
|
3504
|
+
}
|
|
3505
|
+
/**
|
|
3506
|
+
* 从 package.json 里取一个人类可读的主页地址。
|
|
3507
|
+
*
|
|
3508
|
+
* 三种写法都见过:`homepage` 是字符串、`repository` 是字符串、`repository` 是
|
|
3509
|
+
* `{ type, url }` 对象。GitHub 那种 `git+https://…git` 前缀与 `.git` 后缀要剥掉,
|
|
3510
|
+
* 否则直接点开是个 git 协议地址,浏览器打不开。
|
|
3511
|
+
*/
|
|
3512
|
+
function readHomepage(manifest) {
|
|
3513
|
+
if (!manifest) return null;
|
|
3514
|
+
const raw = typeof manifest.homepage === "string" ? manifest.homepage : (() => {
|
|
3515
|
+
const repo = manifest.repository;
|
|
3516
|
+
if (typeof repo === "string") return repo;
|
|
3517
|
+
if (typeof repo === "object" && repo !== null) {
|
|
3518
|
+
const url = repo.url;
|
|
3519
|
+
if (typeof url === "string") return url;
|
|
3520
|
+
}
|
|
3521
|
+
return null;
|
|
3522
|
+
})();
|
|
3523
|
+
if (!raw) return null;
|
|
3524
|
+
const cleaned = raw.trim().replace(/^git\+/, "").replace(/\.git$/, "");
|
|
3525
|
+
return cleaned.startsWith("http") ? cleaned : null;
|
|
3349
3526
|
}
|
|
3350
3527
|
function unavailable(profile, profilePath, reason) {
|
|
3351
3528
|
return {
|
|
@@ -3358,6 +3535,73 @@ function unavailable(profile, profilePath, reason) {
|
|
|
3358
3535
|
items: []
|
|
3359
3536
|
};
|
|
3360
3537
|
}
|
|
3538
|
+
var PluginToggleError = class extends Error {
|
|
3539
|
+
code;
|
|
3540
|
+
constructor(code, message) {
|
|
3541
|
+
super(message);
|
|
3542
|
+
this.code = code;
|
|
3543
|
+
this.name = "PluginToggleError";
|
|
3544
|
+
}
|
|
3545
|
+
};
|
|
3546
|
+
/** 包名合法性白名单:npm 包名允许的字符集,外加 scope 的 `/`。 */
|
|
3547
|
+
const PACKAGE_NAME = /^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i;
|
|
3548
|
+
/**
|
|
3549
|
+
* 启用(`enable=true`)或停用(`enable=false`)一个插件。
|
|
3550
|
+
*
|
|
3551
|
+
* @throws PluginToggleError 带着稳定的 `code`,供路由层映射成 HTTP 状态与错误码。
|
|
3552
|
+
*/
|
|
3553
|
+
async function togglePlugin(name, enable, input = {}) {
|
|
3554
|
+
const trimmed = name.trim();
|
|
3555
|
+
if (!PACKAGE_NAME.test(trimmed)) throw new PluginToggleError("PLUGIN_NAME_INVALID", `Not a valid package name: ${name}`);
|
|
3556
|
+
if (trimmed.startsWith("@deepseek-ai/")) throw new PluginToggleError("PLUGIN_OFFICIAL_IMMUTABLE", "Official DSH packages ship with the runtime and are not managed by the profile.");
|
|
3557
|
+
const profile = resolveProfileName(input);
|
|
3558
|
+
if (!profile) throw new PluginToggleError("PROFILE_UNKNOWN", "Cannot determine the active profile.");
|
|
3559
|
+
const profilePath = path.join(resolveDshHome(input), "profiles", profile);
|
|
3560
|
+
const manifestPath = path.join(profilePath, "package.json");
|
|
3561
|
+
const raw = await readFile(manifestPath, "utf8").catch(() => null);
|
|
3562
|
+
if (raw === null) throw new PluginToggleError("MANIFEST_UNREADABLE", `Cannot read ${manifestPath}`);
|
|
3563
|
+
let manifest;
|
|
3564
|
+
try {
|
|
3565
|
+
manifest = JSON.parse(raw);
|
|
3566
|
+
} catch {
|
|
3567
|
+
throw new PluginToggleError("MANIFEST_UNREADABLE", "The profile manifest is not valid JSON.");
|
|
3568
|
+
}
|
|
3569
|
+
const dependencies = stringRecord(manifest.dependencies);
|
|
3570
|
+
const dshSection = typeof manifest.dsh === "object" && manifest.dsh !== null ? manifest.dsh : {};
|
|
3571
|
+
const profileSection = typeof dshSection.profile === "object" && dshSection.profile !== null ? dshSection.profile : {};
|
|
3572
|
+
const bundles = stringArray(profileSection.bundles);
|
|
3573
|
+
if (enable && dependencies[trimmed] === void 0 && !bundles.includes(trimmed)) throw new PluginToggleError("PLUGIN_NOT_INSTALLED", "This package is not installed in the profile; install it first.");
|
|
3574
|
+
const next = enable ? bundles.includes(trimmed) ? bundles : [...bundles, trimmed] : bundles.filter((item) => item !== trimmed);
|
|
3575
|
+
if (next.length === bundles.length) return {
|
|
3576
|
+
name: trimmed,
|
|
3577
|
+
loaded: enable,
|
|
3578
|
+
backupName: "",
|
|
3579
|
+
bundleCount: bundles.length,
|
|
3580
|
+
restartRequired: true
|
|
3581
|
+
};
|
|
3582
|
+
const backupName = `package.json.bak-${timestamp()}`;
|
|
3583
|
+
await writeFile(path.join(profilePath, backupName), raw, { mode: 384 });
|
|
3584
|
+
profileSection.bundles = next;
|
|
3585
|
+
dshSection.profile = profileSection;
|
|
3586
|
+
manifest.dsh = dshSection;
|
|
3587
|
+
const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
|
|
3588
|
+
const tempPath = path.join(profilePath, `.package.json.tmp-${process.pid}`);
|
|
3589
|
+
await writeFile(tempPath, serialized, { mode: 384 });
|
|
3590
|
+
await rename(tempPath, manifestPath);
|
|
3591
|
+
return {
|
|
3592
|
+
name: trimmed,
|
|
3593
|
+
loaded: enable,
|
|
3594
|
+
backupName,
|
|
3595
|
+
bundleCount: next.length,
|
|
3596
|
+
restartRequired: true
|
|
3597
|
+
};
|
|
3598
|
+
}
|
|
3599
|
+
/** 备份文件名用的时间戳:`20261013-133045`,本地时区、可直接读。 */
|
|
3600
|
+
function timestamp() {
|
|
3601
|
+
const d = /* @__PURE__ */ new Date();
|
|
3602
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
3603
|
+
return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
|
|
3604
|
+
}
|
|
3361
3605
|
//#endregion
|
|
3362
3606
|
//#region src/host/root-resolver.ts
|
|
3363
3607
|
/**
|
|
@@ -3435,7 +3679,7 @@ async function inspectResolvedPath(root, wirePath) {
|
|
|
3435
3679
|
}
|
|
3436
3680
|
//#endregion
|
|
3437
3681
|
//#region src/shared/version.ts
|
|
3438
|
-
const PLUGIN_VERSION = "2.3.
|
|
3682
|
+
const PLUGIN_VERSION = "2.3.4";
|
|
3439
3683
|
//#endregion
|
|
3440
3684
|
//#region src/host/router.ts
|
|
3441
3685
|
var ApiRouter = class {
|
|
@@ -3634,7 +3878,27 @@ var ApiRouter = class {
|
|
|
3634
3878
|
}
|
|
3635
3879
|
if (method === "GET" && pathname === "/api/v1/settings/plugins") {
|
|
3636
3880
|
this.options.auth.requireScope(principal, "settings.read");
|
|
3637
|
-
|
|
3881
|
+
const inventory = await readPluginInventory();
|
|
3882
|
+
sendJson(res, 200, url.searchParams.get("checkUpdates") === "1" ? await withUpdateCheck(inventory) : inventory);
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
if (method === "PATCH" && pathname === "/api/v1/settings/plugins") {
|
|
3886
|
+
this.options.auth.requireScope(principal, "settings.write");
|
|
3887
|
+
let payload;
|
|
3888
|
+
try {
|
|
3889
|
+
payload = await readJson(req, 8e3);
|
|
3890
|
+
} catch {
|
|
3891
|
+
throw new ApiError(400, "REQUEST_INVALID", "The request body must be valid JSON.");
|
|
3892
|
+
}
|
|
3893
|
+
const name = typeof payload.name === "string" ? payload.name : "";
|
|
3894
|
+
const loaded = payload.loaded;
|
|
3895
|
+
if (name.trim() === "" || typeof loaded !== "boolean") throw new ApiError(400, "REQUEST_INVALID", "Expected { name: string, loaded: boolean }.");
|
|
3896
|
+
try {
|
|
3897
|
+
sendJson(res, 200, await togglePlugin(name, loaded));
|
|
3898
|
+
} catch (error) {
|
|
3899
|
+
if (error instanceof PluginToggleError) throw new ApiError(error.code === "PROFILE_UNKNOWN" || error.code === "MANIFEST_UNREADABLE" ? 503 : 400, error.code, error.message);
|
|
3900
|
+
throw error;
|
|
3901
|
+
}
|
|
3638
3902
|
return;
|
|
3639
3903
|
}
|
|
3640
3904
|
const providerRoute = pathname.match(/^\/api\/v1\/settings\/providers\/([^/]+)$/);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-remote-bridge",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.4",
|
|
4
4
|
"description": "Versioned REST bridge for DeepSeek Harness: remote access for trusted clients (chat, files, devices) plus a Phone-settings panel inside the DSH WebUI.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"packageManager": "pnpm@11.19.0",
|