dsh-remote-bridge 2.3.0 → 2.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.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.0-087f8c">
12
+ <img alt="Version" src="https://img.shields.io/badge/version-v2.3.3-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">
@@ -30,7 +30,7 @@ The interface is bilingual and defaults to Chinese. Prebuilt packages run on Win
30
30
 
31
31
  ## What it looks like
32
32
 
33
- The desktop entry lives **inside your DSH settings**: under **📱 Phone access** you will find **📱 Phone settings**, which renders seven collapsible cards inline — no second dialog. It is settings-only and contains **no file tree**: the standalone page `/dsh-workspace` and its file tree and CodeMirror editor were deleted in 2.2.0. The reason and the cost are covered in "No file-management UI on the desktop" below.
33
+ The desktop entry lives **inside your DSH settings**: look for the section titled **📱 Phone settings**, which renders seven collapsible cards inline — no second dialog. It is settings-only and contains **no file tree**: the standalone page `/dsh-workspace` and its file tree and CodeMirror editor were deleted in 2.2.0. The reason and the cost are covered in "No file-management UI on the desktop" below.
34
34
 
35
35
  Remote access remains disabled until a local administrator chooses a bind address, port, and initial device permissions.
36
36
 
@@ -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.0/dsh-remote-bridge-v2.3.0.tgz
76
+ dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.3.3/dsh-remote-bridge-v2.3.3.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.0/dsh-remote-bridge-v2.3.0.tgz
79
+ dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.3.3/dsh-remote-bridge-v2.3.3.tgz
80
80
 
81
81
  npx @deepseek-ai/dsh web
82
82
  ```
@@ -88,17 +88,17 @@ npx @deepseek-ai/dsh web
88
88
  > are reading on, but `/releases/download/...` has no relative form. The npm package name is the
89
89
  > same as the GitHub repository, so it reads identically on both hosts.
90
90
 
91
- Restart DSH after installation, then open settings — **Phone settings** sits right below **Phone access**. It is settings-only and does **not** contain the file tree. The in-conversation **Files** tab was removed in 2.0.4. **The desktop no longer offers a UI for browsing or editing files**: the standalone page `/dsh-workspace` was deleted in 2.2.0 — use the phone app, or ask the agent to use its tools in a conversation.
91
+ Restart DSH after installation, then open settings — The section is titled **📱 Phone settings**. It is settings-only and does **not** contain the file tree. The in-conversation **Files** tab was removed in 2.0.4. **The desktop no longer offers a UI for browsing or editing files**: the standalone page `/dsh-workspace` was deleted in 2.2.0 — use the phone app, or ask the agent to use its tools in a conversation.
92
92
 
93
93
  To install a downloaded release archive:
94
94
 
95
95
  ```sh
96
- dsh plugin --profile web add ./dsh-remote-bridge-v2.3.0.tgz
96
+ dsh plugin --profile web add ./dsh-remote-bridge-v2.3.3.tgz
97
97
  ```
98
98
 
99
99
  ## First connection
100
100
 
101
- 1. Open DSH settings and go to **Phone settings** (below **Phone access**; it lands on **Roots**).
101
+ 1. Open DSH settings and go to **Phone settings** (it lands on **Roots**).
102
102
  2. Add a local directory under **Roots** and give it a recognizable label.
103
103
  3. For mobile access, go to **Remote**, enter the bind IP and port, and save them.
104
104
  4. Select **Enable and create pairing**, then enter the ten-minute one-time code in the app.
@@ -297,7 +297,7 @@ The user-visible consequence: **if this DSH build provides no attachment store,
297
297
 
298
298
  **Phone settings is missing**
299
299
 
300
- The entry is inside DSH settings, right below **Phone access**. If you cannot see it, make sure the plugin was installed into the `web` profile and **restart DSH** — plugins load in-process and a restart is required.
300
+ The entry is inside DSH settings, under the title **📱 Phone settings**. If you cannot see it, make sure the plugin was installed into the `web` profile and **restart DSH** — plugins load in-process and a restart is required.
301
301
 
302
302
  **There is no UI for browsing or editing files on the desktop**
303
303
 
@@ -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.0 was developed against DSH `master@47f943859bef60e4160492346772ded9b24f765a`. CI covers Windows, Ubuntu, and macOS.
330
+ v2.3.3 was developed against DSH `master@47f943859bef60e4160492346772ded9b24f765a`. CI covers Windows, Ubuntu, and macOS.
331
331
 
332
332
  ## Project
333
333
 
334
- - Version: `v2.3.0`
334
+ - Version: `v2.3.3`
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,18 +9,17 @@
9
9
  </p>
10
10
 
11
11
  <p align="center">
12
- <img alt="Version" src="https://img.shields.io/badge/version-v2.3.0-087f8c">
12
+ <img alt="Version" src="https://img.shields.io/badge/version-v2.3.3-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">
16
16
  </p>
17
17
 
18
- `dsh-remote-bridge` 是 DeepSeek Harness(DSH)的远程访问桥接插件:它把 DSH 内核的私有 remote 协议翻译成版本化的 REST 接口,让 dsh-companion([GitHub](https://github.com/westanke/dsh-companion) · [Gitee](https://gitee.com/westanke/dsh-companion))等受信任客户端在离开电脑后仍能使用同一套聊天、工作区与文件能力;桌面端在你的 DSH 设置里提供一个「手机设置」,内联七张设置卡片(根目录 / 远程访问 / 设备 / 识图 / 回收站 / 审计 / App 下载),每张卡片各自折叠、可多张同时展开。**桌面端不再提供文件管理界面**:文件树与编辑器已于 2.2.0 删除,电脑上管理文件请走手机 App,或在会话里让 agent 用工具完成(见「桌面端不再有文件管理界面」)。
18
+ `dsh-remote-bridge` 是 DeepSeek Harness(DSH)的远程访问桥接插件:它把 DSH 内核的私有 remote 协议翻译成版本化的 REST 接口,让 dsh-companion([GitHub](https://github.com/westanke/dsh-companion) · [Gitee](https://gitee.com/westanke/dsh-companion))等受信任客户端在离开电脑后仍能使用同一套聊天、工作区与文件能力;桌面端在你的 DSH 设置里提供一个「手机设置」,内联七张设置卡片(根目录 / 远程访问 / 设备 / 识图 / 回收站 / 审计 / App 下载),每张卡片各自折叠、可多张同时展开。)。
19
19
 
20
20
  界面默认中文,可随时切换 English。插件支持 Windows、macOS 和 Linux,安装包已经包含编译产物,不要求用户在服务器上重新构建。
21
21
 
22
- > **版本警告**:已发布的 `v2.0.0` 与 `v2.0.1` 存在严重缺陷(`v2.0.0` 历史图片读取全部失败,`v2.0.1` 插件无法激活)。请务必使用 **`v2.2.0`**(或 ≥ `v2.0.4`)。
23
- > `v2.0.2` 与 `v2.0.3` **从未发布**,请不要按这两个版本号寻找安装包。
22
+ > **版本警告**:请务必使用 ≥ `v2.0.4`版本。
24
23
 
25
24
  > **DSH 0.2.x 兼容性**:本分支在上游 `v1.0.0` 的基础上增加了对 DSH `0.2.0-rc.2` 的兼容层。
26
25
  > 0.2.x 用 `dsh-api-gateway` 替换了提供 `apiProxy` 服务的 `dsh-host-apiproxy`,导致上游版本在
@@ -29,11 +28,10 @@
29
28
 
30
29
  ## 实际效果
31
30
 
32
- 桌面端的入口**就在你的 DSH 设置里**:打开设置,在「通用设置」下面就是「📱 手机设置」,点进去直接内联渲染七张折叠卡片,不再另开弹窗。它只做设置,**不含文件树**——独立页 `/dsh-workspace` 以及其中的文件树、CodeMirror 编辑器已于 2.2.0 一并删除,理由与代价见下文「桌面端不再有文件管理界面」。
31
+ 桌面端的入口**就在你的 DSH 设置里**:打开设置,找到标题为「📱 手机设置」的一节,点进去直接内联渲染七张折叠卡片,不再另开弹窗。它只做设置。
33
32
 
34
33
  远程访问默认关闭。管理员可在本机 WebUI 中设置绑定 IP、端口和新设备的初始权限。
35
34
 
36
- > 本节原先配有两张界面截图,但那是改名前的旧界面(标题仍是「DSH 文件工作区」;远程访问那张还标着 v1.0.0,且是文件树与设置两个弹窗叠加的形态),与上面描述的当前形态完全不符,已删除以免误导。缓存的旧截图不会自动更新,请以本节文字与 `docs/project/ARCHITECTURE.md` 为准。
37
35
 
38
36
  ## 关于改名(2.0.4)
39
37
 
@@ -72,10 +70,10 @@ dsh plugin --profile web add dsh-remote-bridge
72
70
  dsh plugin --profile web add dsh-remote-bridge --registry https://registry.npmmirror.com
73
71
 
74
72
  # ② GitHub Release 的 tarball
75
- dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.3.0/dsh-remote-bridge-v2.3.0.tgz
73
+ dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.3.3/dsh-remote-bridge-v2.3.3.tgz
76
74
 
77
75
  # ③ Gitee Release 的 tarball(国内访问更快)
78
- dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.3.0/dsh-remote-bridge-v2.3.0.tgz
76
+ dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.3.3/dsh-remote-bridge-v2.3.3.tgz
79
77
 
80
78
  npx @deepseek-ai/dsh web
81
79
  ```
@@ -91,7 +89,7 @@ npx @deepseek-ai/dsh web
91
89
  从 GitHub Release 下载离线安装包时,可直接安装 tarball:
92
90
 
93
91
  ```sh
94
- dsh plugin --profile web add ./dsh-remote-bridge-v2.3.0.tgz
92
+ dsh plugin --profile web add ./dsh-remote-bridge-v2.3.3.tgz
95
93
  ```
96
94
 
97
95
  ## 第一次使用
@@ -142,7 +140,7 @@ dsh plugin --profile web add ./dsh-remote-bridge-v2.3.0.tgz
142
140
  - **识图路径**默认 `.dsh-images`,在设置页「识图」里可以改,改完立即生效、不需要重启;**「当前默认」那一栏是只读的** —— 历史交接消息和识图工具都按它写过路径,它不跟着你的修改走。旁边还列出解析不出会话工作目录时用的兜底目录。
143
141
  - **只认交接句式**,而且只认**你自己发的那条消息**:助手正文、上下文注入、工具输出里出现的 `.dsh-images/…` 不会被渲染成图(对话里到处都是这种引用,读它们只会把别人的话切成碎图)。
144
142
 
145
- > 如果你此前装过 `dsh-image-paths`,请把它那一行**停用**:两个插件的图集会挂在同一个位置,同一张图会被画两遍。本插件已自带这套能力。
143
+ > 如果你此前装过 [dsh-image-paths](https://github.com/lunar-tear/dsh-plugins),请把它那一行**停用**:两个插件的图集会挂在同一个位置,同一张图会被画两遍。本插件已自带这套能力。
146
144
 
147
145
  ### 连接 Android App 或其他客户端
148
146
 
@@ -291,7 +289,7 @@ DSH 的 cordis 注入属性(`ctx.attachments` / `ctx.fileUploads`)是惰性
291
289
 
292
290
  **安装后看不到「手机设置」**
293
291
 
294
- 入口在 DSH 设置里,位于「手机访问」下面。如果看不到,确认插件安装在 `web` profile,并在安装后**重启 DSH**(插件为进程内加载,不重启不生效)。
292
+ 入口在 DSH 设置里,标题为「📱 手机设置」。如果看不到,确认插件安装在 `web` profile,并在安装后**重启 DSH**(插件为进程内加载,不重启不生效)。
295
293
 
296
294
  **桌面上找不到浏览或编辑文件的界面**
297
295
 
@@ -321,11 +319,11 @@ pnpm pack
321
319
 
322
320
  `npm pack` 的产物约 **0.25 MB**:2.2.0 删除独立页及其编辑器依赖、并关闭 source map 之后,从 1.72 MB 降下来。
323
321
 
324
- v2.3.0 基于 DSH `master@47f943859bef60e4160492346772ded9b24f765a` 开发,CI 覆盖 Windows、Ubuntu 和 macOS。
322
+ v2.3.3 基于 DSH `master@47f943859bef60e4160492346772ded9b24f765a` 开发,CI 覆盖 Windows、Ubuntu 和 macOS。
325
323
 
326
324
  ## 项目信息
327
325
 
328
- - 当前版本:`v2.3.0`
326
+ - 当前版本:`v2.3.3`
329
327
  - 作者:上游 [Hakunm](https://github.com/Hakunm),本 fork 维护 [westanke](https://github.com/westanke)
330
328
  - 仓库:[GitHub](https://github.com/westanke/dsh-remote-bridge) · [Gitee](https://gitee.com/westanke/dsh-remote-bridge)
331
329
  - npm:[`dsh-remote-bridge`](https://www.npmjs.com/package/dsh-remote-bridge)([国内镜像](https://registry.npmmirror.com/dsh-remote-bridge))
@@ -1,7 +1,7 @@
1
1
  asyncapi: 3.0.0
2
2
  info:
3
3
  title: DeepSeek Harness Mobile Events
4
- version: 2.3.0
4
+ version: 2.3.3
5
5
  description: |
6
6
  Live file and authorized DSH chat events. Reconnect clients must refresh
7
7
  REST state; unbounded replay is not provided.
@@ -1,7 +1,7 @@
1
1
  openapi: 3.1.0
2
2
  info:
3
3
  title: DeepSeek Harness Mobile API
4
- version: 2.3.0
4
+ version: 2.3.3
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.0 }
27
+ pluginVersion: { type: string, const: 2.3.3 }
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.
@@ -4,7 +4,7 @@ plugins {
4
4
  }
5
5
 
6
6
  group = "ai.deepseek.dsh"
7
- version = "2.3.0"
7
+ version = "2.3.3"
8
8
 
9
9
  repositories { mavenCentral() }
10
10
 
package/lib/index.d.ts CHANGED
@@ -512,6 +512,25 @@ interface SessionControllerLike {
512
512
  }): Promise<{
513
513
  sessionId: string;
514
514
  }>;
515
+ /**
516
+ * Read one message-aligned history page without activating an Agent.
517
+ *
518
+ * `throughSeq` must be obtained from the corresponding follow opening frame's
519
+ * `cursor`; hard-coding one returns a well-formed but empty page.
520
+ * `beforeSeq` is optional and acts as an earlier boundary.
521
+ */
522
+ page(request: {
523
+ address: {
524
+ kind: 'session';
525
+ sessionId: string;
526
+ };
527
+ throughSeq: number;
528
+ beforeSeq?: number;
529
+ maxMessages?: number;
530
+ }, signal: AbortSignal): Promise<{
531
+ records: readonly unknown[];
532
+ hasMore: boolean;
533
+ }>;
515
534
  /**
516
535
  * 取一张**被这个会话引用过**的图片字节(内核主机端方法,名字就叫 `attachment`)。
517
536
  *
package/lib/index.js CHANGED
@@ -2475,7 +2475,7 @@ function createDsh02ApiProxy(host, options = {}) {
2475
2475
  }),
2476
2476
  create: (request) => call(() => host.sessionController.create(request.payload)),
2477
2477
  history: (request) => call(async () => {
2478
- const { sessionId, maxMessages } = request.payload;
2478
+ const { sessionId, maxMessages, beforeSeq } = request.payload;
2479
2479
  const controller = new AbortController();
2480
2480
  try {
2481
2481
  const snapshot = (await host.sessionController.follow({
@@ -2489,10 +2489,23 @@ function createDsh02ApiProxy(host, options = {}) {
2489
2489
  events: [],
2490
2490
  hasMore: false
2491
2491
  };
2492
- return {
2492
+ if (beforeSeq === void 0) return {
2493
2493
  events: Array.isArray(snapshot.records) ? [...snapshot.records] : [],
2494
2494
  hasMore: snapshot.hasMore === true
2495
2495
  };
2496
+ const page = await host.sessionController.page({
2497
+ address: {
2498
+ kind: "session",
2499
+ sessionId
2500
+ },
2501
+ throughSeq: snapshot.cursor,
2502
+ beforeSeq,
2503
+ ...maxMessages === void 0 ? {} : { maxMessages }
2504
+ }, controller.signal);
2505
+ return {
2506
+ events: Array.isArray(page.records) ? [...page.records] : [],
2507
+ hasMore: page.hasMore === true
2508
+ };
2496
2509
  } finally {
2497
2510
  controller.abort();
2498
2511
  }
@@ -3228,8 +3241,18 @@ function defaultDisplayName() {
3228
3241
  /**
3229
3242
  * 解析当前 profile 名。
3230
3243
  *
3231
- * 依次尝试:`DSH_PROFILE` 环境变量 → `--profile=<name>` → `--profile <name>`。
3244
+ * 依次尝试:`DSH_PROFILE` 环境变量 → `--profile=<name>` → `--profile <name>`
3245
+ * → **子命令简写**(`dsh web` 等价于 `dsh --profile web`)。
3232
3246
  * 都拿不到时返回 null(调用方不要猜,猜错会读到另一个 profile 的清单)。
3247
+ *
3248
+ * 最后那条简写路径是补上的一个真 bug:内核的启动器文档写得很清楚 ——
3249
+ *
3250
+ * > `dsh <name>` abbreviates `dsh --profile <name>`;`dsh web` boots the web
3251
+ * > profile (same as: `dsh --profile web`)
3252
+ *
3253
+ * 而此前这里只认 `--profile` 两种写法。于是**用 `dsh web` 启动的用户会看到
3254
+ * 「服务器判定不出当前使用的插件档案」** —— 明明 profile 就在那儿、插件也正常
3255
+ * 加载着,只有这个页面读不出来。这不是「用户没带参数」,是插件漏了一条启动形式。
3233
3256
  */
3234
3257
  function resolveProfileName(input = {}) {
3235
3258
  const env = input.env ?? process.env;
@@ -3244,6 +3267,39 @@ function resolveProfileName(input = {}) {
3244
3267
  if (index >= 0) {
3245
3268
  const value = argv[index + 1]?.trim();
3246
3269
  if (value && !value.startsWith("-")) return value;
3270
+ return null;
3271
+ }
3272
+ return profileFromSubcommand(argv);
3273
+ }
3274
+ /**
3275
+ * 从子命令简写里取 profile 名(`dsh web` → `web`)。
3276
+ *
3277
+ * 判据照抄内核启动器的解析规则:**跳过所有选项,第一个非选项 token 就是子命令**。
3278
+ * 需要绕开两类 token:
3279
+ *
3280
+ * - 选项本身(以 `-` 开头)—— 例如 `dsh --port 3080 web` 里要先跳过 `--port`;
3281
+ * - **选项的取值** —— 上面那个 `3080` 不是选项也不是子命令,只是 `--port` 的值。
3282
+ * 不跳它的话会把 `3080` 当成 profile 名。内核的选项都是「一个旗标接一个值」的
3283
+ * 单值形式(`--port 3080` / `--patch a.yml`),所以看到选项就把它的下一个 token
3284
+ * 一起跳过,是安全的。
3285
+ *
3286
+ * `plugin` 单独排除:`dsh plugin add xxx` 是管理插件依赖的命令,
3287
+ * 它**不启动任何 profile**,把它当 profile 名会去读一个不存在的目录。
3288
+ */
3289
+ function profileFromSubcommand(argv) {
3290
+ const start = argv.findIndex((arg) => /(^|[/\\])dsh(\.js|\.cjs|\.mjs)?$/.test(arg));
3291
+ let i = start >= 0 ? start + 1 : 0;
3292
+ while (i < argv.length) {
3293
+ const arg = argv[i];
3294
+ if (arg === void 0 || arg === "") {
3295
+ i += 1;
3296
+ continue;
3297
+ }
3298
+ if (arg.startsWith("-")) {
3299
+ i += arg.includes("=") ? 1 : 2;
3300
+ continue;
3301
+ }
3302
+ return arg === "plugin" ? null : arg;
3247
3303
  }
3248
3304
  return null;
3249
3305
  }
@@ -3289,10 +3345,13 @@ async function readPluginInventory(input = {}) {
3289
3345
  const names = [.../* @__PURE__ */ new Set([...Object.keys(dependencies), ...bundles])].sort();
3290
3346
  const items = [];
3291
3347
  for (const name of names) {
3292
- const installed = await readInstalledVersion(profilePath, name);
3348
+ const pkgManifest = await readPackageManifest(profilePath, name);
3349
+ const installedRaw = pkgManifest?.version;
3350
+ const installed = typeof installedRaw === "string" && installedRaw.trim() ? installedRaw : null;
3293
3351
  const loaded = bundles.includes(name);
3294
3352
  const declared = dependencies[name] ?? null;
3295
3353
  const official = name.startsWith("@deepseek-ai/");
3354
+ const description = typeof pkgManifest?.description === "string" && pkgManifest.description.trim() ? pkgManifest.description.trim() : null;
3296
3355
  items.push({
3297
3356
  name,
3298
3357
  declared,
@@ -3303,7 +3362,12 @@ async function readPluginInventory(input = {}) {
3303
3362
  loaded,
3304
3363
  installed,
3305
3364
  official
3306
- })
3365
+ }),
3366
+ description,
3367
+ homepage: readHomepage(pkgManifest),
3368
+ updateAvailable: null,
3369
+ latestVersion: null,
3370
+ toggleable: !official && installed !== null
3307
3371
  });
3308
3372
  }
3309
3373
  return {
@@ -3317,6 +3381,68 @@ async function readPluginInventory(input = {}) {
3317
3381
  };
3318
3382
  }
3319
3383
  /**
3384
+ * 查 npm registry 上的最新版本(可选功能,默认不开)。
3385
+ *
3386
+ * 为什么默认关:每个包一次 HTTP 请求,清单会从毫秒级变成好几秒,而用户点进
3387
+ * 「插件」页十次里九次只是想看看「装了啥、哪个没启用」。要查更新得显式开。
3388
+ *
3389
+ * 两个刻意的取舍:
3390
+ * - **超时短**(5 秒):一个包查不动不该拖住整张清单,超时就当「查不到」。
3391
+ * - **失败即 null**,不重试:这是锦上添花的信息,为了它排队重试不值当。
3392
+ * 界面拿到 null 会说「未检查」,而不是谎称「已是最新」。
3393
+ */
3394
+ async function fetchLatestVersion(packageName) {
3395
+ const controller = new AbortController();
3396
+ const timer = setTimeout(() => controller.abort(), 5e3);
3397
+ try {
3398
+ const response = await fetch(`https://registry.npmjs.org/${packageName.replace("/", "%2F")}/latest`, {
3399
+ signal: controller.signal,
3400
+ headers: { accept: "application/json" }
3401
+ });
3402
+ if (!response.ok) return null;
3403
+ const body = await response.json();
3404
+ return typeof body.version === "string" && body.version.trim() ? body.version.trim() : null;
3405
+ } catch {
3406
+ return null;
3407
+ } finally {
3408
+ clearTimeout(timer);
3409
+ }
3410
+ }
3411
+ /**
3412
+ * 给清单里的插件补上「有没有新版本」。
3413
+ *
3414
+ * 只查**确实装在 profile 里**的第三方包:官方包随运行时发布、用户装不了(查了也没用),
3415
+ * 缺失包连本地版本都没有(无从比较)。
3416
+ *
3417
+ * 并发度限制在 6:十几个包全放出去对 registry 不礼貌,也容易触发限流 ——
3418
+ * 而限流之后拿到的 null 会被界面显示成「未检查」,白跑一趟。
3419
+ */
3420
+ async function withUpdateCheck(inventory) {
3421
+ const targets = inventory.items.filter((item) => !item.official && item.installed !== null);
3422
+ const latest = /* @__PURE__ */ new Map();
3423
+ const queue = [...targets];
3424
+ const workers = Array.from({ length: Math.min(6, queue.length) }, async () => {
3425
+ for (;;) {
3426
+ const next = queue.shift();
3427
+ if (next === void 0) return;
3428
+ latest.set(next.name, await fetchLatestVersion(next.name));
3429
+ }
3430
+ });
3431
+ await Promise.all(workers);
3432
+ return {
3433
+ ...inventory,
3434
+ items: inventory.items.map((item) => {
3435
+ if (!latest.has(item.name)) return item;
3436
+ const newest = latest.get(item.name) ?? null;
3437
+ return {
3438
+ ...item,
3439
+ latestVersion: newest,
3440
+ updateAvailable: newest === null || item.installed === null ? null : newest !== item.installed
3441
+ };
3442
+ })
3443
+ };
3444
+ }
3445
+ /**
3320
3446
  * 判定条目状态。
3321
3447
  *
3322
3448
  * 顺序很重要:**先判「官方包 + 未落盘」**,否则它们会被判成 `declared-missing` ——
@@ -3328,11 +3454,37 @@ function resolveState(input) {
3328
3454
  if (input.official) return "runtime-provided";
3329
3455
  return "declared-missing";
3330
3456
  }
3331
- /** 读 `node_modules/<name>/package.json` 的 version;包名含 scope 时要按路径展开。 */
3332
- async function readInstalledVersion(profilePath, packageName) {
3457
+ /**
3458
+ * 读 `node_modules/<name>/package.json` 的整份内容。
3459
+ *
3460
+ * 读不到就返回 null(包没装、或装了一半)。刻意不抛:这是展示用的元数据,
3461
+ * 某个包读不出来不该让整张清单失败。
3462
+ */
3463
+ async function readPackageManifest(profilePath, packageName) {
3333
3464
  const modulePath = path.join(profilePath, "node_modules", ...packageName.split("/"));
3334
- const version = (await readJson$1(path.join(modulePath, "package.json")))?.version;
3335
- return typeof version === "string" && version.trim() ? version : null;
3465
+ return readJson$1(path.join(modulePath, "package.json"));
3466
+ }
3467
+ /**
3468
+ * 从 package.json 里取一个人类可读的主页地址。
3469
+ *
3470
+ * 三种写法都见过:`homepage` 是字符串、`repository` 是字符串、`repository` 是
3471
+ * `{ type, url }` 对象。GitHub 那种 `git+https://…git` 前缀与 `.git` 后缀要剥掉,
3472
+ * 否则直接点开是个 git 协议地址,浏览器打不开。
3473
+ */
3474
+ function readHomepage(manifest) {
3475
+ if (!manifest) return null;
3476
+ const raw = typeof manifest.homepage === "string" ? manifest.homepage : (() => {
3477
+ const repo = manifest.repository;
3478
+ if (typeof repo === "string") return repo;
3479
+ if (typeof repo === "object" && repo !== null) {
3480
+ const url = repo.url;
3481
+ if (typeof url === "string") return url;
3482
+ }
3483
+ return null;
3484
+ })();
3485
+ if (!raw) return null;
3486
+ const cleaned = raw.trim().replace(/^git\+/, "").replace(/\.git$/, "");
3487
+ return cleaned.startsWith("http") ? cleaned : null;
3336
3488
  }
3337
3489
  function unavailable(profile, profilePath, reason) {
3338
3490
  return {
@@ -3345,6 +3497,73 @@ function unavailable(profile, profilePath, reason) {
3345
3497
  items: []
3346
3498
  };
3347
3499
  }
3500
+ var PluginToggleError = class extends Error {
3501
+ code;
3502
+ constructor(code, message) {
3503
+ super(message);
3504
+ this.code = code;
3505
+ this.name = "PluginToggleError";
3506
+ }
3507
+ };
3508
+ /** 包名合法性白名单:npm 包名允许的字符集,外加 scope 的 `/`。 */
3509
+ const PACKAGE_NAME = /^(?:@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i;
3510
+ /**
3511
+ * 启用(`enable=true`)或停用(`enable=false`)一个插件。
3512
+ *
3513
+ * @throws PluginToggleError 带着稳定的 `code`,供路由层映射成 HTTP 状态与错误码。
3514
+ */
3515
+ async function togglePlugin(name, enable, input = {}) {
3516
+ const trimmed = name.trim();
3517
+ if (!PACKAGE_NAME.test(trimmed)) throw new PluginToggleError("PLUGIN_NAME_INVALID", `Not a valid package name: ${name}`);
3518
+ 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.");
3519
+ const profile = resolveProfileName(input);
3520
+ if (!profile) throw new PluginToggleError("PROFILE_UNKNOWN", "Cannot determine the active profile.");
3521
+ const profilePath = path.join(resolveDshHome(input), "profiles", profile);
3522
+ const manifestPath = path.join(profilePath, "package.json");
3523
+ const raw = await readFile(manifestPath, "utf8").catch(() => null);
3524
+ if (raw === null) throw new PluginToggleError("MANIFEST_UNREADABLE", `Cannot read ${manifestPath}`);
3525
+ let manifest;
3526
+ try {
3527
+ manifest = JSON.parse(raw);
3528
+ } catch {
3529
+ throw new PluginToggleError("MANIFEST_UNREADABLE", "The profile manifest is not valid JSON.");
3530
+ }
3531
+ const dependencies = stringRecord(manifest.dependencies);
3532
+ const dshSection = typeof manifest.dsh === "object" && manifest.dsh !== null ? manifest.dsh : {};
3533
+ const profileSection = typeof dshSection.profile === "object" && dshSection.profile !== null ? dshSection.profile : {};
3534
+ const bundles = stringArray(profileSection.bundles);
3535
+ 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.");
3536
+ const next = enable ? bundles.includes(trimmed) ? bundles : [...bundles, trimmed] : bundles.filter((item) => item !== trimmed);
3537
+ if (next.length === bundles.length) return {
3538
+ name: trimmed,
3539
+ loaded: enable,
3540
+ backupName: "",
3541
+ bundleCount: bundles.length,
3542
+ restartRequired: true
3543
+ };
3544
+ const backupName = `package.json.bak-${timestamp()}`;
3545
+ await writeFile(path.join(profilePath, backupName), raw, { mode: 384 });
3546
+ profileSection.bundles = next;
3547
+ dshSection.profile = profileSection;
3548
+ manifest.dsh = dshSection;
3549
+ const serialized = `${JSON.stringify(manifest, null, 2)}\n`;
3550
+ const tempPath = path.join(profilePath, `.package.json.tmp-${process.pid}`);
3551
+ await writeFile(tempPath, serialized, { mode: 384 });
3552
+ await rename(tempPath, manifestPath);
3553
+ return {
3554
+ name: trimmed,
3555
+ loaded: enable,
3556
+ backupName,
3557
+ bundleCount: next.length,
3558
+ restartRequired: true
3559
+ };
3560
+ }
3561
+ /** 备份文件名用的时间戳:`20261013-133045`,本地时区、可直接读。 */
3562
+ function timestamp() {
3563
+ const d = /* @__PURE__ */ new Date();
3564
+ const p = (n) => String(n).padStart(2, "0");
3565
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}-${p(d.getHours())}${p(d.getMinutes())}${p(d.getSeconds())}`;
3566
+ }
3348
3567
  //#endregion
3349
3568
  //#region src/host/root-resolver.ts
3350
3569
  /**
@@ -3422,7 +3641,7 @@ async function inspectResolvedPath(root, wirePath) {
3422
3641
  }
3423
3642
  //#endregion
3424
3643
  //#region src/shared/version.ts
3425
- const PLUGIN_VERSION = "2.3.0";
3644
+ const PLUGIN_VERSION = "2.3.3";
3426
3645
  //#endregion
3427
3646
  //#region src/host/router.ts
3428
3647
  var ApiRouter = class {
@@ -3621,7 +3840,27 @@ var ApiRouter = class {
3621
3840
  }
3622
3841
  if (method === "GET" && pathname === "/api/v1/settings/plugins") {
3623
3842
  this.options.auth.requireScope(principal, "settings.read");
3624
- sendJson(res, 200, await readPluginInventory());
3843
+ const inventory = await readPluginInventory();
3844
+ sendJson(res, 200, url.searchParams.get("checkUpdates") === "1" ? await withUpdateCheck(inventory) : inventory);
3845
+ return;
3846
+ }
3847
+ if (method === "PATCH" && pathname === "/api/v1/settings/plugins") {
3848
+ this.options.auth.requireScope(principal, "settings.write");
3849
+ let payload;
3850
+ try {
3851
+ payload = await readJson(req, 8e3);
3852
+ } catch {
3853
+ throw new ApiError(400, "REQUEST_INVALID", "The request body must be valid JSON.");
3854
+ }
3855
+ const name = typeof payload.name === "string" ? payload.name : "";
3856
+ const loaded = payload.loaded;
3857
+ if (name.trim() === "" || typeof loaded !== "boolean") throw new ApiError(400, "REQUEST_INVALID", "Expected { name: string, loaded: boolean }.");
3858
+ try {
3859
+ sendJson(res, 200, await togglePlugin(name, loaded));
3860
+ } catch (error) {
3861
+ if (error instanceof PluginToggleError) throw new ApiError(error.code === "PROFILE_UNKNOWN" || error.code === "MANIFEST_UNREADABLE" ? 503 : 400, error.code, error.message);
3862
+ throw error;
3863
+ }
3625
3864
  return;
3626
3865
  }
3627
3866
  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.0",
3
+ "version": "2.3.3",
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",