dsh-remote-bridge 2.5.2 → 2.6.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/README.en.md +8 -7
- package/README.md +55 -9
- package/docs/api/asyncapi.yaml +1 -1
- package/docs/api/openapi.yaml +141 -2
- package/lib/client.cjs +57 -53
- package/lib/index.d.ts +138 -2
- package/lib/index.js +603 -56
- 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.
|
|
12
|
+
<img alt="Version" src="https://img.shields.io/badge/version-v2.6.0-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">
|
|
@@ -102,10 +102,10 @@ dsh plugin --profile web add dsh-remote-bridge
|
|
|
102
102
|
dsh plugin --profile web add dsh-remote-bridge --registry https://registry.npmmirror.com
|
|
103
103
|
|
|
104
104
|
# 2. tarball from the GitHub release
|
|
105
|
-
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.
|
|
105
|
+
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.6.0/dsh-remote-bridge-v2.6.0.tgz
|
|
106
106
|
|
|
107
107
|
# 3. tarball from the Gitee release (faster from mainland China)
|
|
108
|
-
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.
|
|
108
|
+
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.6.0/dsh-remote-bridge-v2.6.0.tgz
|
|
109
109
|
|
|
110
110
|
npx @deepseek-ai/dsh web
|
|
111
111
|
```
|
|
@@ -122,7 +122,7 @@ Restart DSH after installation, then open settings — The section is titled **
|
|
|
122
122
|
To install a downloaded release archive:
|
|
123
123
|
|
|
124
124
|
```sh
|
|
125
|
-
dsh plugin --profile web add ./dsh-remote-bridge-v2.
|
|
125
|
+
dsh plugin --profile web add ./dsh-remote-bridge-v2.6.0.tgz
|
|
126
126
|
```
|
|
127
127
|
|
|
128
128
|
## First connection
|
|
@@ -354,11 +354,11 @@ pnpm pack
|
|
|
354
354
|
|
|
355
355
|
`pnpm check` covers type-checking, documentation consistency, and tests. It currently passes **67 test files / 557 cases**, plus **8 browser-layer cases** (Playwright measuring real computed styles, including the 375px narrow-viewport contract).
|
|
356
356
|
|
|
357
|
-
`npm pack` produces roughly a **0.61 MB** archive (v2.5.2 measured 635,
|
|
357
|
+
`npm pack` produces roughly a **0.61 MB** archive (v2.5.2 measured 635,325 bytes, SHA-256 `18d36904…c8095`): 2.2.0 dropped the standalone page and its editor dependencies and turned source maps off, down from 1.72 MB; it has since grown slightly with projection passthrough, the permission BFF, and the five-dimension hardening.
|
|
358
358
|
|
|
359
359
|
v2.5.0 was verified on DSH **0.2.1-rc.2** on real hardware: all 14/14 host services injected, the three jobs endpoints exercised, and the permission catalog returned the kernel's real three presets. The development baseline is upstream `dsh-v0.2.1-rc.2` (commit `687f282e8625c6b8d11ff58211a90524be2e4317`); see `engines.dsh` above for the supported range. CI covers Windows, Ubuntu, and macOS.
|
|
360
360
|
|
|
361
|
-
**v2.
|
|
361
|
+
**v2.6.0 verification status**: 70 test files / 593 cases green, plus 8 Playwright browser cases
|
|
362
362
|
(including the 375px narrow-viewport contract); `typecheck`, `docs:check`, and the build all pass.
|
|
363
363
|
All three rounds of real-hardware tunnel regressions were reproduced and verified on a
|
|
364
364
|
React18 + Playwright click harness (one-click token issuance, the stop-tunnel escape hatch, and the
|
|
@@ -366,7 +366,8 @@ locked state when re-entering settings), then confirmed by manual testing on the
|
|
|
366
366
|
|
|
367
367
|
## Project
|
|
368
368
|
|
|
369
|
-
- Version: `v2.
|
|
369
|
+
- Version: `v2.6.0`
|
|
370
|
+
- **v2.6.0 is the plugin-settings + IM-entry release**: it adds the per-plugin settings namespaces (`GET /api/v1/settings/namespaces`, `PATCH .../{ns}`) so the app can build the form from the kernel schema, and two entry points for "I am out and the desktop is still locked" — the host command `/tunnel` and the tool `open_tunnel_config`; plus one-tap pruning of unused devices, the audit page removed, and a guard so a failing optional registration cannot tear down the whole plugin context.
|
|
370
371
|
- **v2.5.2 is a comprehensive hardening release**: it lands the five-dimension audit
|
|
371
372
|
(security, features, performance, UI, button feedback) — including the unauthenticated
|
|
372
373
|
`debug/host-services` information disclosure fix, dual-bucket rate limiting, broadcast backpressure,
|
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.
|
|
12
|
+
<img alt="Version" src="https://img.shields.io/badge/version-v2.6.0-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">
|
|
@@ -93,10 +93,10 @@ dsh plugin --profile web add dsh-remote-bridge
|
|
|
93
93
|
dsh plugin --profile web add dsh-remote-bridge --registry https://registry.npmmirror.com
|
|
94
94
|
|
|
95
95
|
# ② GitHub Release 的 tarball
|
|
96
|
-
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.
|
|
96
|
+
dsh plugin --profile web add https://github.com/westanke/dsh-remote-bridge/releases/download/v2.6.0/dsh-remote-bridge-v2.6.0.tgz
|
|
97
97
|
|
|
98
98
|
# ③ Gitee Release 的 tarball(国内访问更快)
|
|
99
|
-
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.
|
|
99
|
+
dsh plugin --profile web add https://gitee.com/westanke/dsh-remote-bridge/releases/download/v2.6.0/dsh-remote-bridge-v2.6.0.tgz
|
|
100
100
|
|
|
101
101
|
npx @deepseek-ai/dsh web
|
|
102
102
|
```
|
|
@@ -112,7 +112,7 @@ npx @deepseek-ai/dsh web
|
|
|
112
112
|
从 GitHub Release 下载离线安装包时,可直接安装 tarball:
|
|
113
113
|
|
|
114
114
|
```sh
|
|
115
|
-
dsh plugin --profile web add ./dsh-remote-bridge-v2.
|
|
115
|
+
dsh plugin --profile web add ./dsh-remote-bridge-v2.6.0.tgz
|
|
116
116
|
```
|
|
117
117
|
|
|
118
118
|
## 第一次使用
|
|
@@ -182,14 +182,59 @@ dsh plugin --profile web add ./dsh-remote-bridge-v2.5.2.tgz
|
|
|
182
182
|
装好 App 后,在「手机设置 → 远程访问」里生成配置文本或配对码即可连上本机。
|
|
183
183
|
公开 API 不依赖 DSH 私有协议,也可用于其他客户端:
|
|
184
184
|
|
|
185
|
-
- 查看、新建、重命名和移除 DSH
|
|
186
|
-
- 创建、重命名、分叉和归档会话。
|
|
185
|
+
- 查看、新建、重命名和移除 DSH 工作区登记。- 创建、重命名、分叉和归档会话。
|
|
187
186
|
- 读取消息、流式接收正文和思考、追加引导或取消运行。
|
|
188
187
|
- 查看 TODO、斜杠命令、权限模式和待审批操作。
|
|
189
188
|
- 选择 Agent、模型和思考强度。
|
|
190
189
|
- 查看或修改模型供应商,支持完全自定义供应商。
|
|
191
190
|
- 管理设备获准访问的文件和回收站项目。
|
|
192
191
|
|
|
192
|
+
### 远程开隧道:人已经出门、电脑还锁着
|
|
193
|
+
|
|
194
|
+
常见场景:**人已经出门,才发现电脑上的 Cloudflare 隧道没开,配置文本也没发给自己**。
|
|
195
|
+
此时公网入口根本不存在,WebUI 和 App 都够不着这台机器。能用的只剩「本机主动连出去」的通道,
|
|
196
|
+
装了 `@xmanrui/dsh-im`(微信 / 飞书 / 钉钉 / 企业微信 / Telegram …)或任何带命令栏的客户端时,
|
|
197
|
+
有两条入口可以做这件事:
|
|
198
|
+
|
|
199
|
+
**① 桌面 WebUI 的命令栏(立即可用)**
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
/tunnel
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
回的是这样一段:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
云隧道已开启。
|
|
209
|
+
地址:https://<random>.trycloudflare.com
|
|
210
|
+
设备名:Android 设备
|
|
211
|
+
配置文本(一行,复制后粘贴到 App 的「导入配置」):
|
|
212
|
+
DSH1:eyJ2IjoxLCJlbmRwb2ludHMiOlt7ImxhYmVsIjoi…
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**② IM 聊天窗口里用一句话(配合智能体)**
|
|
216
|
+
|
|
217
|
+
在微信/飞书里直接说「**帮我开隧道,把配置文本发我**」,智能体会调用本插件的
|
|
218
|
+
`open_tunnel_config` 工具,把同样的地址与 `DSH1:` 文本回到聊天里。
|
|
219
|
+
|
|
220
|
+
> 为什么 IM 里不是发 `/tunnel`?因为 `@xmanrui/dsh-im` 的斜杠命令表是**封闭的**:
|
|
221
|
+
> 内置 31 条,另外只把 `/compact` `/clear` `/permission` 三条委派给宿主,没有
|
|
222
|
+
> 「未知命令转交宿主命令表」的通用派发,也从不查询宿主的命令列表。所以 `/tunnel`
|
|
223
|
+
> 在 IM 里会当成一句普通消息交给智能体 —— 这也正是上面第 ② 条走自然语言 + 工具的原因。
|
|
224
|
+
> 如果 dsh-im 以后加上通用派发,`/tunnel` 会立刻可用,桥这边不用改。
|
|
225
|
+
|
|
226
|
+
隧道**已经在跑**时再触发一次不会重复开,而是复用当前地址再给一份配置文本
|
|
227
|
+
(每次调用都会新建一台设备,方便在面板里单独吊销)。
|
|
228
|
+
|
|
229
|
+
能力边界刻意压得很小:**只能开,不能停、不能改配置、不读任何文件**。
|
|
230
|
+
带参数一律拒绝,不提供 `/tunnel stop` 这类第二入口。就算口令泄露,损失上限也只是
|
|
231
|
+
「隧道被人开了一下」—— 隧道本身不授予任何数据面权限(读文件、发消息仍要设备令牌)。
|
|
232
|
+
|
|
233
|
+
> ⚠️ **必要前提:IM 机器人必须是白名单模式,且白名单里只有你自己。**
|
|
234
|
+
> 这条通道的身份校验**不在本插件里**,而在 IM 插件自己的访问策略(`canExecuteCommands` +
|
|
235
|
+
> allowlist)。如果机器人设成「允许所有用户私聊执行命令」,那等于把开机按钮放到公共群里 ——
|
|
236
|
+
> 这是部署问题,代码替你兜不住。详见 [SECURITY.md](docs/project/SECURITY.md#im-命令tunnel的信任边界)。
|
|
237
|
+
|
|
193
238
|
## 远程访问与安全
|
|
194
239
|
|
|
195
240
|
默认远程地址为 `0.0.0.0:3090`,IP 和端口都可修改。客户端可以使用能到达服务器的 IP 或域名连接。
|
|
@@ -340,20 +385,21 @@ pnpm pack
|
|
|
340
385
|
|
|
341
386
|
`pnpm check` 覆盖类型检查、文档一致性与测试;当前 **67 个测试文件 / 557 个用例**,外加 **8 个浏览器层用例**(Playwright 实测量出的计算样式,含 375px 窄屏契约),全部通过。
|
|
342
387
|
|
|
343
|
-
`npm pack` 的产物约 **0.61 MB**(v2.5.2 实测 635,
|
|
388
|
+
`npm pack` 的产物约 **0.61 MB**(v2.5.2 实测 635,325 字节,SHA-256 `18d36904…c8095`):2.2.0 删除独立页及其编辑器依赖、并关闭 source map 之后从 1.72 MB 降下来,之后随投影透传、权限 BFF 与五维加固小幅增长。
|
|
344
389
|
|
|
345
390
|
v2.5.0 在 DSH **0.2.1-rc.2** 上完成真机验证:14/14 宿主服务全部可注入、三个 jobs 端点实测通过、
|
|
346
391
|
权限档位目录返回内核真实三档。开发基线为上游 `dsh-v0.2.1-rc.2`(commit `687f282e8625c6b8d11ff58211a90524be2e4317`),
|
|
347
392
|
兼容范围见上文 `engines.dsh`。CI 覆盖 Windows、Ubuntu 和 macOS。
|
|
348
393
|
|
|
349
|
-
**v2.
|
|
394
|
+
**v2.6.0 的验证状态**:70 个测试文件 / 593 个用例全绿、8 个 Playwright 浏览器用例
|
|
350
395
|
(含 375px 窄屏契约)、`typecheck`/`docs:check`/构建通过;隧道管理面三轮真机回归均用
|
|
351
396
|
React18 + Playwright 真点击台复现并验证(一键出令牌、停止出口、重进设置页锁定态),
|
|
352
397
|
用户真机手工复测通过。
|
|
353
398
|
|
|
354
399
|
## 项目信息
|
|
355
400
|
|
|
356
|
-
- 当前版本:`v2.
|
|
401
|
+
- 当前版本:`v2.6.0`
|
|
402
|
+
- **v2.6.0 是通用设置面 + IM 入口版**:新增「按插件划分的设置命名空间」两个端点(手机端照 schema 自建表单,新装插件无需改 App),以及「人已出门、电脑还锁着」时用 IM 一句话开隧道并收回配对配置的双入口(宿主命令 `/tunnel` + 工具 `open_tunnel_config`);另加一键清理 N 天未使用设备、移除审计页,并把「激活期降级保护」补上(apply 尾部抛错不再拆掉整条插件上下文)。
|
|
357
403
|
- **v2.5.2 是全量完善版**:落地五维审计(安全/功能/性能/界面/按钮反馈)——含高危的
|
|
358
404
|
`debug/host-services` 免鉴权信息泄露修复、限流双桶、广播背压、弱 ETag、审计保留策略等,
|
|
359
405
|
并把「一键开隧道并生成令牌」做成同一次请求内搭车完成;隧道运行时管理面安全锁定,
|
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.
|
|
4
|
+
version: 2.6.0
|
|
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.
|
|
27
|
+
pluginVersion: { type: string, const: 2.6.0 }
|
|
28
28
|
/debug/host-services:
|
|
29
29
|
get:
|
|
30
30
|
operationId: hostServices
|
|
@@ -579,6 +579,68 @@ paths:
|
|
|
579
579
|
content:
|
|
580
580
|
application/json:
|
|
581
581
|
schema: { $ref: '#/components/schemas/ModelCatalog' }
|
|
582
|
+
/settings/namespaces:
|
|
583
|
+
get:
|
|
584
|
+
operationId: listSettingsNamespaces
|
|
585
|
+
summary: Read every DSH plugin settings namespace
|
|
586
|
+
description: |
|
|
587
|
+
Returns the host's per-plugin settings document as one entry per namespace, so a
|
|
588
|
+
mobile client can build a form from `schema` without a dedicated endpoint per
|
|
589
|
+
plugin. Requires `settings.read`.
|
|
590
|
+
|
|
591
|
+
The top-level `writable` mirrors the host settings document. Each entry's
|
|
592
|
+
`writable` is that value AND the bridge's write policy: namespaces starting with
|
|
593
|
+
`ui-` or ending in `locale` are writable by default; high-risk ones (`llm-*`,
|
|
594
|
+
`permission`, `agent-default-model`, `subagent-*`, …) are read-only unless the
|
|
595
|
+
deployment explicitly allows them through `settingsWriteNamespaces`. When an
|
|
596
|
+
entry is not writable, `reason` says why: `HOST_READONLY` (the whole settings
|
|
597
|
+
document is read-only) or `HIGH_RISK_NAMESPACE` (write policy).
|
|
598
|
+
|
|
599
|
+
Secret values never leave the host: the kernel already redacts fields declared as
|
|
600
|
+
secrets, and the bridge additionally masks string values whose key matches
|
|
601
|
+
`apiKey|secret|token|password|credential|apiKeyEnv` with `***`.
|
|
602
|
+
responses:
|
|
603
|
+
'200':
|
|
604
|
+
description: All namespaces, with secret values redacted and per-namespace writability.
|
|
605
|
+
content:
|
|
606
|
+
application/json:
|
|
607
|
+
schema: { $ref: '#/components/schemas/SettingsNamespaces' }
|
|
608
|
+
'403': { $ref: '#/components/responses/Error' }
|
|
609
|
+
/settings/namespaces/{ns}:
|
|
610
|
+
parameters:
|
|
611
|
+
- name: ns
|
|
612
|
+
in: path
|
|
613
|
+
required: true
|
|
614
|
+
schema: { type: string }
|
|
615
|
+
description: Namespace id (a plugin name such as `ui-chat`).
|
|
616
|
+
patch:
|
|
617
|
+
operationId: mutateSettingsNamespace
|
|
618
|
+
description: |
|
|
619
|
+
Applies path-addressed `set` / `unset` operations to one namespace, mirroring the
|
|
620
|
+
kernel's `settings.mutate`. Requires `settings.write`; at most 32 operations per
|
|
621
|
+
request. A successful mutation is audited as `settings.namespace.mutate` with the
|
|
622
|
+
touched field paths only — never the values.
|
|
623
|
+
|
|
624
|
+
Only namespaces allowed by the write policy can be changed; others answer
|
|
625
|
+
403 `SETTINGS_NAMESPACE_READONLY`. `expectedRevision` is optimistic concurrency:
|
|
626
|
+
a mismatch answers 409 `SETTINGS_CONFLICT`; an invalid ops body answers
|
|
627
|
+
400 `SETTINGS_OPS_INVALID`; a missing host settings service answers
|
|
628
|
+
503 `SETTINGS_UNAVAILABLE`.
|
|
629
|
+
requestBody:
|
|
630
|
+
required: true
|
|
631
|
+
content:
|
|
632
|
+
application/json:
|
|
633
|
+
schema: { $ref: '#/components/schemas/SettingsNamespaceOps' }
|
|
634
|
+
responses:
|
|
635
|
+
'200':
|
|
636
|
+
description: The mutated namespace, in the same shape as one entry of `SettingsNamespaces.namespaces`.
|
|
637
|
+
content:
|
|
638
|
+
application/json:
|
|
639
|
+
schema: { $ref: '#/components/schemas/SettingsNamespaceSummary' }
|
|
640
|
+
'400': { $ref: '#/components/responses/Error' }
|
|
641
|
+
'403': { $ref: '#/components/responses/Error' }
|
|
642
|
+
'409': { $ref: '#/components/responses/Error' }
|
|
643
|
+
'503': { $ref: '#/components/responses/Error' }
|
|
582
644
|
/settings/providers/{providerId}:
|
|
583
645
|
parameters:
|
|
584
646
|
- name: providerId
|
|
@@ -1624,6 +1686,83 @@ components:
|
|
|
1624
1686
|
providers:
|
|
1625
1687
|
type: array
|
|
1626
1688
|
items: { $ref: '#/components/schemas/Provider' }
|
|
1689
|
+
SettingsNamespaceSecret:
|
|
1690
|
+
type: object
|
|
1691
|
+
description: One secret slot reported by the host. Only its path and whether it is set are exposed, never the value.
|
|
1692
|
+
required: [path, set]
|
|
1693
|
+
properties:
|
|
1694
|
+
path:
|
|
1695
|
+
type: array
|
|
1696
|
+
items: { type: string }
|
|
1697
|
+
set: { type: boolean }
|
|
1698
|
+
SettingsNamespaceSummary:
|
|
1699
|
+
type: object
|
|
1700
|
+
description: One DSH settings namespace as seen by a remote client, with secret values redacted.
|
|
1701
|
+
required: [ns, revision, value, writable]
|
|
1702
|
+
properties:
|
|
1703
|
+
ns: { type: string, examples: [ui-chat, ui-theme] }
|
|
1704
|
+
revision: { type: integer, minimum: 0 }
|
|
1705
|
+
schema:
|
|
1706
|
+
type: object
|
|
1707
|
+
additionalProperties: true
|
|
1708
|
+
description: Kernel schema tree for form generation; its shape is kernel-defined.
|
|
1709
|
+
value:
|
|
1710
|
+
description: Effective value (base merged with user overrides), recursively redacted.
|
|
1711
|
+
base:
|
|
1712
|
+
description: Built-in default value, recursively redacted.
|
|
1713
|
+
user:
|
|
1714
|
+
description: User overrides only, recursively redacted.
|
|
1715
|
+
autoGenerate:
|
|
1716
|
+
type: object
|
|
1717
|
+
additionalProperties: true
|
|
1718
|
+
description: Kernel-reported fields that are generated automatically.
|
|
1719
|
+
applies:
|
|
1720
|
+
type: object
|
|
1721
|
+
additionalProperties: true
|
|
1722
|
+
description: Kernel-reported applicability conditions for this namespace.
|
|
1723
|
+
secrets:
|
|
1724
|
+
type: array
|
|
1725
|
+
items: { $ref: '#/components/schemas/SettingsNamespaceSecret' }
|
|
1726
|
+
writable:
|
|
1727
|
+
type: boolean
|
|
1728
|
+
description: The host document is writable AND this namespace passes the bridge's write policy.
|
|
1729
|
+
reason:
|
|
1730
|
+
type: string
|
|
1731
|
+
enum: [HOST_READONLY, HIGH_RISK_NAMESPACE]
|
|
1732
|
+
description: Present only when `writable` is false.
|
|
1733
|
+
SettingsNamespaces:
|
|
1734
|
+
type: object
|
|
1735
|
+
required: [writable, namespaces]
|
|
1736
|
+
properties:
|
|
1737
|
+
writable:
|
|
1738
|
+
type: boolean
|
|
1739
|
+
description: Whether the host settings document itself is writable.
|
|
1740
|
+
namespaces:
|
|
1741
|
+
type: array
|
|
1742
|
+
items: { $ref: '#/components/schemas/SettingsNamespaceSummary' }
|
|
1743
|
+
SettingsNamespaceOps:
|
|
1744
|
+
type: object
|
|
1745
|
+
description: Path-addressed operations and optional optimistic-concurrency revision.
|
|
1746
|
+
required: [ops]
|
|
1747
|
+
properties:
|
|
1748
|
+
ops:
|
|
1749
|
+
type: array
|
|
1750
|
+
minItems: 1
|
|
1751
|
+
maxItems: 32
|
|
1752
|
+
items:
|
|
1753
|
+
oneOf:
|
|
1754
|
+
- type: object
|
|
1755
|
+
required: [op, path, value]
|
|
1756
|
+
properties:
|
|
1757
|
+
op: { type: string, const: set }
|
|
1758
|
+
path: { type: array, items: { type: string } }
|
|
1759
|
+
value: {}
|
|
1760
|
+
- type: object
|
|
1761
|
+
required: [op, path]
|
|
1762
|
+
properties:
|
|
1763
|
+
op: { type: string, const: unset }
|
|
1764
|
+
path: { type: array, items: { type: string } }
|
|
1765
|
+
expectedRevision: { type: integer, minimum: 0 }
|
|
1627
1766
|
ProviderPatch:
|
|
1628
1767
|
type: object
|
|
1629
1768
|
properties:
|
package/lib/client.cjs
CHANGED
|
@@ -152,6 +152,17 @@ window.__ModuleLoader__.load({
|
|
|
152
152
|
async revokeDevice(deviceId) {
|
|
153
153
|
await this.manageJson(`/devices/${encodeURIComponent(deviceId)}`, { method: "DELETE" });
|
|
154
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* 一键清理「N 天没动过」的设备。天数由用户填,服务端再夹一次 1..3650。
|
|
157
|
+
*
|
|
158
|
+
* 返回真正删掉的条数 —— 0 也是一次合法结果(「没有可清理的」),界面不该把它当失败。
|
|
159
|
+
*/
|
|
160
|
+
async pruneInactiveDevices(olderThanDays) {
|
|
161
|
+
return this.manageJson("/devices/prune", {
|
|
162
|
+
method: "POST",
|
|
163
|
+
body: JSON.stringify({ olderThanDays })
|
|
164
|
+
});
|
|
165
|
+
}
|
|
155
166
|
async trashItems() {
|
|
156
167
|
return this.json("/trash");
|
|
157
168
|
}
|
|
@@ -22831,7 +22842,7 @@ window.__ModuleLoader__.load({
|
|
|
22831
22842
|
key: "1o3tu8"
|
|
22832
22843
|
}]
|
|
22833
22844
|
]);
|
|
22834
|
-
|
|
22845
|
+
createLucideIcon("ScrollText", [
|
|
22835
22846
|
["path", {
|
|
22836
22847
|
d: "M15 12h-5",
|
|
22837
22848
|
key: "r7krc0"
|
|
@@ -29780,7 +29791,6 @@ window.__ModuleLoader__.load({
|
|
|
29780
29791
|
exports.HardDrive = HardDrive;
|
|
29781
29792
|
exports.Image = Image;
|
|
29782
29793
|
exports.Languages = Languages;
|
|
29783
|
-
exports.ScrollText = ScrollText;
|
|
29784
29794
|
exports.Smartphone = Smartphone;
|
|
29785
29795
|
exports.Trash2 = Trash2;
|
|
29786
29796
|
exports.Upload = Upload;
|
|
@@ -29984,6 +29994,12 @@ window.__ModuleLoader__.load({
|
|
|
29984
29994
|
pairingCreated: "配对码已生成",
|
|
29985
29995
|
configCreated: "配置文本已生成",
|
|
29986
29996
|
revokeConfirm: "吊销后这台手机的令牌立即失效,必须重新配对才能访问。确定吊销「{name}」吗?",
|
|
29997
|
+
pruneInactiveDays: "清理超过(天)未使用",
|
|
29998
|
+
pruneInactiveAction: "清理 {days} 天未使用设备",
|
|
29999
|
+
pruneInactiveBusy: "正在清理…",
|
|
30000
|
+
pruneInactiveConfirm: "将删除 {days} 天没有使用过的设备(它们需要重新配对才能访问)。最近用过的设备不受影响。继续吗?",
|
|
30001
|
+
pruneInactiveDone: "已清理 {count} 台设备",
|
|
30002
|
+
pruneInactiveNone: "没有需要清理的设备",
|
|
29987
30003
|
removeRootConfirm: "移除根目录「{label}」不会删除磁盘上的文件,但所有手机将立刻失去对它的访问权。确定移除吗?",
|
|
29988
30004
|
disableRemoteConfirm: "关闭远程访问后,所有手机连接立即断开,需要重新启用并配对。确定关闭吗?",
|
|
29989
30005
|
rootAdded: "根目录已添加",
|
|
@@ -30204,6 +30220,12 @@ window.__ModuleLoader__.load({
|
|
|
30204
30220
|
pairingCreated: "Pairing code generated",
|
|
30205
30221
|
configCreated: "Config text generated",
|
|
30206
30222
|
revokeConfirm: "Revoking invalidates this phone’s token at once; it must pair again to regain access. Revoke “{name}”?",
|
|
30223
|
+
pruneInactiveDays: "Unused for more than (days)",
|
|
30224
|
+
pruneInactiveAction: "Prune devices unused for {days} days",
|
|
30225
|
+
pruneInactiveBusy: "Pruning…",
|
|
30226
|
+
pruneInactiveConfirm: "This deletes devices unused for {days} days (they must pair again to regain access). Recently used devices are untouched. Continue?",
|
|
30227
|
+
pruneInactiveDone: "Pruned {count} device(s)",
|
|
30228
|
+
pruneInactiveNone: "No devices to prune",
|
|
30207
30229
|
removeRootConfirm: "Removing “{label}” does not delete files on disk, but every phone immediately loses access to it. Remove it?",
|
|
30208
30230
|
disableRemoteConfirm: "Disabling remote access drops every phone connection right away; it must be re-enabled and re-paired. Disable it?",
|
|
30209
30231
|
rootAdded: "Root added",
|
|
@@ -30944,7 +30966,6 @@ window.__ModuleLoader__.load({
|
|
|
30944
30966
|
devices: "devicesTabDesc",
|
|
30945
30967
|
image: "imageTabDesc",
|
|
30946
30968
|
trash: "trashTabDesc",
|
|
30947
|
-
audit: "auditTabDesc",
|
|
30948
30969
|
"app-download": "appDownloadTabDesc"
|
|
30949
30970
|
};
|
|
30950
30971
|
function tabDescKey(tab) {
|
|
@@ -31050,7 +31071,6 @@ window.__ModuleLoader__.load({
|
|
|
31050
31071
|
const { locale, t } = useWorkspaceI18n();
|
|
31051
31072
|
const [status, setStatus] = (0, react.useState)();
|
|
31052
31073
|
const [trash, setTrash] = (0, react.useState)([]);
|
|
31053
|
-
const [audit, setAudit] = (0, react.useState)([]);
|
|
31054
31074
|
const [path, setPath] = (0, react.useState)("");
|
|
31055
31075
|
const [label, setLabel] = (0, react.useState)("");
|
|
31056
31076
|
const [listenerHost, setListenerHost] = (0, react.useState)("0.0.0.0");
|
|
@@ -31069,8 +31089,8 @@ window.__ModuleLoader__.load({
|
|
|
31069
31089
|
const [tunnel, setTunnel] = (0, react.useState)();
|
|
31070
31090
|
const [tunnelBusy, setTunnelBusy] = (0, react.useState)(false);
|
|
31071
31091
|
const [activity, setActivity] = (0, react.useState)();
|
|
31072
|
-
const [
|
|
31073
|
-
const [
|
|
31092
|
+
const [pruneDays, setPruneDays] = (0, react.useState)(30);
|
|
31093
|
+
const [pruneBusy, setPruneBusy] = (0, react.useState)(false);
|
|
31074
31094
|
const [now, setNow] = (0, react.useState)(() => Date.now());
|
|
31075
31095
|
const notify = (0, react.useCallback)((key, params) => {
|
|
31076
31096
|
setNotice(params === void 0 ? {
|
|
@@ -31127,7 +31147,6 @@ window.__ModuleLoader__.load({
|
|
|
31127
31147
|
const next = await props.api.status();
|
|
31128
31148
|
setStatus(next);
|
|
31129
31149
|
if (openCards.has("trash")) setTrash((await props.api.trashItems()).items);
|
|
31130
|
-
if (openCards.has("audit")) setAudit((await props.api.audit()).items);
|
|
31131
31150
|
if (openCards.has("image")) {
|
|
31132
31151
|
const settings = await props.api.imageSettings();
|
|
31133
31152
|
setImage(settings);
|
|
@@ -31707,13 +31726,41 @@ window.__ModuleLoader__.load({
|
|
|
31707
31726
|
configText !== void 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ConfigTextPanel, { result: configText })
|
|
31708
31727
|
]
|
|
31709
31728
|
}, "remote"),
|
|
31710
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
31729
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
31711
31730
|
tab: "devices",
|
|
31712
31731
|
nameKey: tabLabelKey("devices"),
|
|
31713
31732
|
descKey: tabDescKey("devices"),
|
|
31714
31733
|
open: isOpen("devices"),
|
|
31715
31734
|
onToggle: () => toggleCard("devices"),
|
|
31716
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.
|
|
31735
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31736
|
+
className: "daw-inline-command",
|
|
31737
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
31738
|
+
className: "daw-field",
|
|
31739
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("pruneInactiveDays") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
|
|
31740
|
+
className: "daw-input",
|
|
31741
|
+
type: "number",
|
|
31742
|
+
min: 1,
|
|
31743
|
+
max: 3650,
|
|
31744
|
+
value: pruneDays,
|
|
31745
|
+
disabled: managementLocked || pruneBusy,
|
|
31746
|
+
onChange: (event) => setPruneDays(Math.max(1, Math.min(3650, Number(event.target.value) || 1)))
|
|
31747
|
+
})]
|
|
31748
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31749
|
+
className: "daw-command",
|
|
31750
|
+
disabled: managementLocked || pruneBusy,
|
|
31751
|
+
onClick: () => {
|
|
31752
|
+
if (!confirm(t("pruneInactiveConfirm", { days: String(pruneDays) }))) return;
|
|
31753
|
+
setPruneBusy(true);
|
|
31754
|
+
setError(void 0);
|
|
31755
|
+
props.api.pruneInactiveDevices(pruneDays).then((result) => {
|
|
31756
|
+
if (result.pruned === 0) notify("pruneInactiveNone");
|
|
31757
|
+
else notify("pruneInactiveDone", { count: String(result.pruned) });
|
|
31758
|
+
return refresh();
|
|
31759
|
+
}).catch(setError).finally(() => setPruneBusy(false));
|
|
31760
|
+
},
|
|
31761
|
+
children: pruneBusy ? t("pruneInactiveBusy") : t("pruneInactiveAction", { days: String(pruneDays) })
|
|
31762
|
+
})]
|
|
31763
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31717
31764
|
className: "daw-list",
|
|
31718
31765
|
children: (status?.devices ?? []).map((device) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(DeviceRow, {
|
|
31719
31766
|
api: props.api,
|
|
@@ -31723,7 +31770,7 @@ window.__ModuleLoader__.load({
|
|
|
31723
31770
|
onError: setError,
|
|
31724
31771
|
onNotice: notify
|
|
31725
31772
|
}, device.id))
|
|
31726
|
-
})
|
|
31773
|
+
})]
|
|
31727
31774
|
}, "devices"),
|
|
31728
31775
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)(SettingsCard, {
|
|
31729
31776
|
tab: "image",
|
|
@@ -31869,48 +31916,6 @@ window.__ModuleLoader__.load({
|
|
|
31869
31916
|
}, item.id))
|
|
31870
31917
|
})]
|
|
31871
31918
|
}, "trash"),
|
|
31872
|
-
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SettingsCard, {
|
|
31873
|
-
tab: "audit",
|
|
31874
|
-
nameKey: tabLabelKey("audit"),
|
|
31875
|
-
descKey: tabDescKey("audit"),
|
|
31876
|
-
open: isOpen("audit"),
|
|
31877
|
-
onToggle: () => toggleCard("audit"),
|
|
31878
|
-
children: audit.length === 0 ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31879
|
-
className: "daw-list-meta daw-empty",
|
|
31880
|
-
children: t("auditEmpty")
|
|
31881
|
-
}) : /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31882
|
-
className: "daw-list",
|
|
31883
|
-
children: audit.slice(0, auditLimit).map((item, index) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31884
|
-
className: "daw-list-row",
|
|
31885
|
-
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
31886
|
-
className: "daw-list-title",
|
|
31887
|
-
children: String(item.action ?? t("event"))
|
|
31888
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
31889
|
-
className: "daw-list-meta",
|
|
31890
|
-
children: [
|
|
31891
|
-
new Date(Number(item.time ?? 0)).toLocaleString(locale),
|
|
31892
|
-
" · ",
|
|
31893
|
-
String(item.actorType ?? ""),
|
|
31894
|
-
":",
|
|
31895
|
-
String(item.actorId ?? ""),
|
|
31896
|
-
" · ",
|
|
31897
|
-
String(item.relativePath ?? "")
|
|
31898
|
-
]
|
|
31899
|
-
})] })
|
|
31900
|
-
}, String(item.id ?? index)))
|
|
31901
|
-
}), auditLimit < audit.length && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
31902
|
-
className: "daw-command daw-inline-command",
|
|
31903
|
-
disabled: auditLoading,
|
|
31904
|
-
onClick: () => {
|
|
31905
|
-
setAuditLoading(true);
|
|
31906
|
-
window.setTimeout(() => {
|
|
31907
|
-
setAuditLimit((value) => value + 50);
|
|
31908
|
-
setAuditLoading(false);
|
|
31909
|
-
}, 0);
|
|
31910
|
-
},
|
|
31911
|
-
children: auditLoading ? t("auditLoading") : t("auditLoadMore")
|
|
31912
|
-
})] })
|
|
31913
|
-
}, "audit"),
|
|
31914
31919
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AppDownloadSection, {
|
|
31915
31920
|
open: isOpen("app-download"),
|
|
31916
31921
|
onToggle: () => toggleCard("app-download")
|
|
@@ -32183,7 +32188,6 @@ window.__ModuleLoader__.load({
|
|
|
32183
32188
|
if (tab === "devices") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Smartphone, { size: 15 });
|
|
32184
32189
|
if (tab === "image") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Image, { size: 15 });
|
|
32185
32190
|
if (tab === "trash") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Trash2, { size: 15 });
|
|
32186
|
-
if (tab === "audit") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.ScrollText, { size: 15 });
|
|
32187
32191
|
if (tab === "remote") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Upload, { size: 15 });
|
|
32188
32192
|
if (tab === "app-download") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.Download, { size: 15 });
|
|
32189
32193
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(import_lucide_react.File, { size: 15 });
|