dsh-mobile 0.1.0-alpha.31 → 0.1.0-alpha.33
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 +26 -48
- package/README.md +27 -52
- package/SECURITY.md +2 -0
- package/lib/cli.js +92 -0
- package/lib/client.js +353 -55
- package/lib/client.js.map +1 -1
- package/lib/index.d.mts +161 -4
- package/lib/index.mjs +884 -16
- package/package.json +18 -3
package/README.en.md
CHANGED
|
@@ -19,15 +19,19 @@
|
|
|
19
19
|
|
|
20
20
|
<p align="center"><a href="https://github.com/saya-ch/dsh-mobile/releases"><strong>Download the Android app</strong></a></p>
|
|
21
21
|
|
|
22
|
-
DSH Mobile is a DeepSeek Harness plugin that lets a mobile browser or the Android app connect over a protected LAN and keep using the same sessions, Workspaces, messages, and tools. It is a mobile entry point only; the DeepSeek Harness source is not modified.
|
|
22
|
+
DSH Mobile is a DeepSeek Harness plugin that lets a mobile browser or the Android app connect over a protected LAN and keep using the same sessions, Workspaces, messages, and tools. It is a mobile entry point only; the DeepSeek Harness source is not modified and no public-Internet tunneling is needed.
|
|
23
|
+
|
|
24
|
+
It also lets you customize the phone from a DSH conversation: `/mobile <what you want>`.
|
|
23
25
|
|
|
24
26
|
## What it does
|
|
25
27
|
|
|
26
|
-
- Continue DSH work from a phone
|
|
27
|
-
-
|
|
28
|
-
- A dedicated touch layout
|
|
29
|
-
- Auto-discovery
|
|
30
|
-
-
|
|
28
|
+
- **Continue DSH work from a phone**: the same sessions, Workspaces, messages, and tools, in real time.
|
|
29
|
+
- **Customize the phone UI by talking to DSH**: change the mobile layout, interactions, and features from a conversation; open pages refresh within seconds.
|
|
30
|
+
- **A dedicated touch layout**: session drawer, tool details, settings, and composer reorganized for phones.
|
|
31
|
+
- **Auto-discovery, no re-pairing**: Wi-Fi, hotspot, or IP changes normally recover automatically.
|
|
32
|
+
- **Three pairing options**: scan a QR code, paste a pairing link, or enter a key.
|
|
33
|
+
|
|
34
|
+
A paired device is fully trusted and can operate the DSH on the computer. Use this only on a trusted home or office LAN, or a trusted VPN.
|
|
31
35
|
|
|
32
36
|
## Quick start
|
|
33
37
|
|
|
@@ -48,17 +52,25 @@ pnpm dsh plugin --profile web exec dsh-mobile setup
|
|
|
48
52
|
pnpm dsh --profile web
|
|
49
53
|
```
|
|
50
54
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
After starting DSH, open **Mobile Access** in the lower-left sidebar, enable it, then:
|
|
55
|
+
After starting DSH, open **Mobile Access** in the lower-left sidebar, then:
|
|
54
56
|
|
|
55
57
|
1. Select **Create and copy key** or **Copy pairing link**; the panel shows a pairing QR code.
|
|
56
58
|
2. In the Android app, tap **Scan QR code** and point the camera at the screen — or tap **Scan**, select the computer, and paste the key or pairing link.
|
|
57
59
|
3. Pairing establishes persistent device trust; later launches do not ask again.
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
`setup` automatically selects and remembers the current LAN; Wi-Fi, hotspot, and IP changes normally recover without re-pairing. Use `--address 192.168.x.x` only when automatic selection fails. Settings, certificates, devices, and customization files live under `$DSH_HOME/mobile-access/`.
|
|
62
|
+
|
|
63
|
+
## Extend and customize
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
Type `/mobile <what you want>` in a DSH conversation, and DSH edits the phone client's files for you; changes apply within a few seconds. For example:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
/mobile turn the phone UI into an old CRT terminal, with messages scrolling like terminal output
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Two kinds of changes are supported: the phone UI itself (theme, layout, buttons), and computer capabilities the phone can use (browsing computer files, running programs on the computer). `/mobile` hands the request to the DSH agent, which edits files under the local DSH configuration directory (`$DSH_HOME/mobile-access/`); the phone client applies them automatically. UI changes live in `mobile.css`/`mobile.js`. Computer capabilities come from extensions under `extensions/`, whose `host.mjs` runs with the local user's privileges on the computer. DeepSeek Harness source is not modified.
|
|
72
|
+
|
|
73
|
+
> When using computer-side capabilities, use only content you trust.
|
|
62
74
|
|
|
63
75
|
## App or mobile browser
|
|
64
76
|
|
|
@@ -67,42 +79,8 @@ The plugin does not modify the DeepSeek Harness source. Settings, certificates,
|
|
|
67
79
|
| Android app | Everyday use | Auto-discovery; private certificate pinning inside the app, no manual browser trust step |
|
|
68
80
|
| Mobile browser | Temporary or cross-platform | Open the HTTPS origin shown by Mobile Access; trust the certificate manually on first visit |
|
|
69
81
|
|
|
70
|
-
Discovery uses mDNS/NSD, UDP announcements and queries, plus HTTPS probing. It publishes only the device name, address, port, protocol version, and stable `instanceId` — never keys or device tokens. IP changes do not require another pairing.
|
|
71
|
-
|
|
72
|
-
For a browser's first connection, open pairing on the computer, then select **Copy pairing link** and open that link on the phone — the pairing code is prefilled. Alternatively, visit `/mobile-access/pair` on the shown HTTPS origin and enter the 43-character pairing code after the key's final dot. The browser stores a revocable device credential after pairing.
|
|
73
|
-
|
|
74
|
-
## Mobile UI
|
|
75
|
-
|
|
76
|
-
Phones use a dedicated layout shell that no longer depends on the desktop three-column DOM, while native components keep a small touch-adaptation layer:
|
|
77
|
-
|
|
78
|
-
- A workspace-and-session drawer opens from the top-left.
|
|
79
|
-
- Conversations, traces, tool details, and Session logs keep their full capabilities.
|
|
80
|
-
- Settings use top-level tabs and a single column.
|
|
81
|
-
- The composer keeps command, permission, model, context, image, and send controls.
|
|
82
|
-
- **Add Workspace** browses computer folders inside the phone page instead of opening a system picker on the computer.
|
|
83
|
-
|
|
84
82
|
The Android app is a thin Kotlin WebView shell and contains no frontend copy; mobile browsers load the same page. For compatibility diagnosis, append `?frontend=stock` to the browser URL to temporarily use the previous desktop-page adaptation.
|
|
85
83
|
|
|
86
|
-
## Customize from DeepSeek Harness
|
|
87
|
-
|
|
88
|
-
Default files:
|
|
89
|
-
|
|
90
|
-
```text
|
|
91
|
-
$DSH_HOME/mobile-access/mobile.css
|
|
92
|
-
$DSH_HOME/mobile-access/mobile.js
|
|
93
|
-
```
|
|
94
|
-
|
|
95
|
-
Ask DeepSeek Harness to edit them, for example:
|
|
96
|
-
|
|
97
|
-
```text
|
|
98
|
-
Edit $DSH_HOME/mobile-access/mobile.css and mobile.js to turn the mobile
|
|
99
|
-
client into a one-handed development console: add a bottom shortcut bar,
|
|
100
|
-
a session status panel, and a press-and-hold voice entry. Narrow screens only;
|
|
101
|
-
do not modify the DSH source.
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
Changes are applied to open Android and browser pages within a few seconds. Customization is not limited to colors: `mobile.js` can add navigation, shortcuts, dashboards, camera, voice, scanning, and complete interactions with same-origin DeepSeek Harness APIs.
|
|
105
|
-
|
|
106
84
|
## How it works
|
|
107
85
|
|
|
108
86
|
```mermaid
|
|
@@ -111,7 +89,7 @@ flowchart LR
|
|
|
111
89
|
Gateway -->|"loopback proxy"| DSH["Stock DSH Web and Host"]
|
|
112
90
|
```
|
|
113
91
|
|
|
114
|
-
|
|
92
|
+
Three layers: the Host face for discovery, pairing, HTTPS, loopback proxying, and extension registration; the Client face for the dedicated mobile layout and extension SDK; and the Android app for a narrow native bridge. Neither the DeepSeek Harness source nor its desktop page on port 3080 is modified.
|
|
115
93
|
|
|
116
94
|
## Security
|
|
117
95
|
|
|
@@ -125,9 +103,9 @@ See [SECURITY.md](SECURITY.md).
|
|
|
125
103
|
|
|
126
104
|
| DSH Mobile | Verified DeepSeek Harness releases |
|
|
127
105
|
| --- | --- |
|
|
128
|
-
| `0.1.0-alpha.
|
|
106
|
+
| `0.1.0-alpha.33` | `0.1.0-rc.5`, `0.1.0-rc.6`, `0.1.0-rc.7` |
|
|
129
107
|
|
|
130
|
-
At startup, the plugin verifies the DSH Host version and the frontend dependencies required by the mobile layout
|
|
108
|
+
At startup, the plugin verifies the DSH Host version and the frontend dependencies required by the mobile layout; an unverified release fails with a clear error instead of serving a broken page. CI also tracks the DSH main branch layout contract. If a DSH upgrade reports an incompatibility, update DSH Mobile first.
|
|
131
109
|
|
|
132
110
|
## Uninstall
|
|
133
111
|
|
package/README.md
CHANGED
|
@@ -14,9 +14,9 @@
|
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
|
17
|
+
<a href="#能做什么">能做什么</a> ·
|
|
17
18
|
<a href="#快速开始">快速开始</a> ·
|
|
18
|
-
<a href="
|
|
19
|
-
<a href="#在-dsh-对话中自定义">自定义</a> ·
|
|
19
|
+
<a href="#扩展与自定义">扩展与自定义</a> ·
|
|
20
20
|
<a href="README.en.md">English</a>
|
|
21
21
|
</p>
|
|
22
22
|
|
|
@@ -26,13 +26,17 @@
|
|
|
26
26
|
|
|
27
27
|
DSH Mobile 是一个 DeepSeek Harness 插件,让手机浏览器或 Android App 在受保护的局域网内连接电脑,继续使用同一份会话、工作区、消息和工具。它只作为移动入口接入,不修改 DeepSeek Harness 源码。
|
|
28
28
|
|
|
29
|
+
它还能在 DSH 对话里用 `/mobile <需求>` 定制手机端。
|
|
30
|
+
|
|
29
31
|
## 能做什么
|
|
30
32
|
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
33
|
+
- **在手机上继续电脑端的工作**:同一份会话、工作区、消息和工具,实时同步。
|
|
34
|
+
- **用对话定制手机端**:直接在 DSH 对话里改手机页面的布局、交互和功能,几秒内刷新。
|
|
35
|
+
- **专属触屏布局**:会话抽屉、工具详情、设置和输入栏都按手机重新组织。
|
|
36
|
+
- **自动发现、无需重新配对**:切换 Wi-Fi、热点或 IP 后通常自动恢复。
|
|
37
|
+
- **三种配对方式**:扫码、配对链接、密钥。
|
|
38
|
+
|
|
39
|
+
配对设备被视为完全信任,可以操作电脑上的 DSH;建议只在可信的家庭、办公局域网或可信 VPN 中使用。
|
|
36
40
|
|
|
37
41
|
## 快速开始
|
|
38
42
|
|
|
@@ -53,17 +57,23 @@ pnpm dsh plugin --profile web exec dsh-mobile setup
|
|
|
53
57
|
pnpm dsh --profile web
|
|
54
58
|
```
|
|
55
59
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
启动后,在 DeepSeek Harness 左下角打开“移动访问”并确认已开启,然后:
|
|
60
|
+
启动后,在 DeepSeek Harness 左下角打开“移动访问”,然后:
|
|
59
61
|
|
|
60
62
|
1. 点击“生成并复制密钥”或“复制配对链接”,面板会显示配对二维码。
|
|
61
|
-
2. Android App
|
|
63
|
+
2. Android App 点击“扫码配对”,把手机对准电脑屏幕上的二维码即可;也可以点击“扫描”选择电脑后粘贴密钥或配对链接。
|
|
62
64
|
3. 配对完成后会建立持久设备信任,以后打开 App 无需重复输入。
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
`setup` 会自动选择并记住当前局域网,切换 Wi-Fi、热点或 IP 后通常自动恢复;仅在自动选择失败时使用 `--address 192.168.x.x`。设置、证书、设备和自定义文件保存在 `$DSH_HOME/mobile-access/`。
|
|
67
|
+
|
|
68
|
+
## 扩展与自定义
|
|
65
69
|
|
|
66
|
-
|
|
70
|
+
在 DSH 对话里输入 `/mobile <需求>`,DSH 会直接修改手机端的文件,几秒内生效。例如:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
/mobile 把手机端做成老式终端的样子,让消息像终端输出一样逐行滚动
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`/mobile` 把需求交给 DSH 对话中的 agent,由它直接修改本机 `$DSH_HOME/mobile-access/` 下的文件,保存后手机端自动生效。改动分两类:界面和交互在 `mobile.css`/`mobile.js`;需要电脑能力时用 `extensions/` 下的扩展,其 `host.mjs` 以本机用户权限在电脑上运行。不修改 DeepSeek Harness 源码。
|
|
67
77
|
|
|
68
78
|
## App 与手机浏览器
|
|
69
79
|
|
|
@@ -73,40 +83,7 @@ pnpm dsh --profile web
|
|
|
73
83
|
| Android App | 日常使用 | 自动发现;App 内保存私有证书信任,无需在浏览器手动信任证书 |
|
|
74
84
|
| 手机浏览器 | 临时或跨平台访问 | 打开“移动访问”卡片显示的 HTTPS 地址;首次连接需在浏览器手动信任该证书 |
|
|
75
85
|
|
|
76
|
-
Android
|
|
77
|
-
|
|
78
|
-
手机浏览器首次连接:在电脑端开启配对后,点击“复制配对链接”并把链接发到手机浏览器打开,配对码会自动填入;也可以打开 HTTPS 地址中的 `/mobile-access/pair`,手动输入密钥最后一段的 43 位配对码。配对完成后,浏览器会保存可撤销的设备凭据。
|
|
79
|
-
|
|
80
|
-
## 移动端界面
|
|
81
|
-
|
|
82
|
-
手机端使用独立的移动布局外壳,不再依赖桌面三栏 DOM;原生的对话、工具、设置等组件保留触屏适配:
|
|
83
|
-
|
|
84
|
-
- 左上角打开工作区与会话抽屉。
|
|
85
|
-
- 对话、轨迹、工具详情和 Session log 保持原有能力。
|
|
86
|
-
- 设置页使用顶部分类和单列内容。
|
|
87
|
-
- 输入栏保留命令、权限、模型、上下文、图片和发送控件。
|
|
88
|
-
- “添加工作区”在手机上展示电脑目录,不会在电脑上弹系统选择器。
|
|
89
|
-
|
|
90
|
-
Android App 只是 Kotlin WebView 薄壳,不内置另一份网页;手机浏览器访问的也是同一页面。需要排查兼容性时,可在浏览器地址后追加 `?frontend=stock`,临时回到旧的桌面页面适配模式。
|
|
91
|
-
|
|
92
|
-
## 在 DSH 对话中自定义
|
|
93
|
-
|
|
94
|
-
默认文件:
|
|
95
|
-
|
|
96
|
-
```text
|
|
97
|
-
$DSH_HOME/mobile-access/mobile.css
|
|
98
|
-
$DSH_HOME/mobile-access/mobile.js
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
直接在 DeepSeek Harness 对话中提出修改即可,例如:
|
|
102
|
-
|
|
103
|
-
```text
|
|
104
|
-
请编辑 $DSH_HOME/mobile-access/mobile.css 和 mobile.js,
|
|
105
|
-
把移动端改成适合单手操作的开发控制台:增加底部快捷指令、
|
|
106
|
-
会话状态面板和长按语音入口。只影响窄屏,不修改 DSH 源码。
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
保存后,已打开的 App 和浏览器通常会在几秒内应用变化。自定义不限于配色:`mobile.js` 可以添加导航、快捷操作、状态面板、相机、语音、扫码,以及调用同源 DSH API 的完整交互。
|
|
86
|
+
Android App 只是 Kotlin WebView 薄壳,不内置另一份网页;手机浏览器访问的是同一页面。需要排查兼容性时,可在浏览器地址后追加 `?frontend=stock`,临时回到旧的桌面页面适配模式。
|
|
110
87
|
|
|
111
88
|
## 工作原理
|
|
112
89
|
|
|
@@ -117,7 +94,7 @@ flowchart LR
|
|
|
117
94
|
DSH -->|"同一工作区、会话和事件流"| Phone
|
|
118
95
|
```
|
|
119
96
|
|
|
120
|
-
|
|
97
|
+
插件包含三层:Host face 负责发现、配对、HTTPS、回环代理和扩展注册表;Client face 提供独立的移动布局与扩展 SDK;Android App 提供受限的原生 Bridge。DeepSeek Harness 的源码和 3080 桌面页面都不会被修改,安装和卸载完全通过插件机制完成。
|
|
121
98
|
|
|
122
99
|
## 安全
|
|
123
100
|
|
|
@@ -132,14 +109,12 @@ flowchart LR
|
|
|
132
109
|
|
|
133
110
|
| DSH Mobile | 已验证的 DeepSeek Harness |
|
|
134
111
|
| ------------------ | ------------------------------------------ |
|
|
135
|
-
| `0.1.0-alpha.
|
|
112
|
+
| `0.1.0-alpha.33` | `0.1.0-rc.5`、`0.1.0-rc.6`、`0.1.0-rc.7` |
|
|
136
113
|
|
|
137
|
-
|
|
114
|
+
插件启动时会检查 DSH Host 版本和移动布局所需的前端依赖,遇到未经验证的版本会直接报错而不是带病启动;CI 也会持续跟踪 DSH 主分支的布局契约。升级 DSH 后如遇兼容提示,请先升级 DSH Mobile。
|
|
138
115
|
|
|
139
116
|
## 卸载
|
|
140
117
|
|
|
141
|
-
保留设备和自定义数据:
|
|
142
|
-
|
|
143
118
|
```powershell
|
|
144
119
|
dsh plugin --profile web remove dsh-mobile
|
|
145
120
|
```
|
package/SECURITY.md
CHANGED
|
@@ -24,6 +24,8 @@ The maintainer will acknowledge a complete report within seven days. Publication
|
|
|
24
24
|
- Do not expose the gateway directly to the public Internet.
|
|
25
25
|
- Treat every paired device as a fully trusted operator. Stock DSH methods reached through the authenticated loopback proxy may read configuration or run tools with the desktop user's authority.
|
|
26
26
|
- Treat `mobile.js` as application code with the paired page's same-origin authority. Restrict write access to trusted host-side DSH sessions and review generated API calls or browser-permission use.
|
|
27
|
+
- Treat every extension `host.mjs` as a local program with the desktop user's Node.js privileges. It is never sandboxed and is not editable through the mobile gateway; only place code there that you trust.
|
|
28
|
+
- Extension Actions and Routes receive filtered request data, a device identifier, and an abort signal. They cannot set proxy security headers or access the gateway's cookies, device tokens, CSRF tokens, or internal request headers.
|
|
27
29
|
|
|
28
30
|
## Known alpha limitation
|
|
29
31
|
|
package/lib/cli.js
CHANGED
|
@@ -6,6 +6,8 @@ import { basename, dirname, join, resolve } from "node:path";
|
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { X509Certificate, createPrivateKey, createPublicKey } from "node:crypto";
|
|
8
8
|
import { generate } from "selfsigned";
|
|
9
|
+
import "@deepseek-ai/cordis";
|
|
10
|
+
import "@deepseek-ai/schemastery";
|
|
9
11
|
//#region src/managed-setup.ts
|
|
10
12
|
const execFile$2 = promisify(execFile);
|
|
11
13
|
const VIRTUAL_INTERFACE_MARKERS = [
|
|
@@ -253,6 +255,28 @@ async function refreshManagedServerCertificate(setup, address) {
|
|
|
253
255
|
});
|
|
254
256
|
await Promise.all([atomicWrite(setup.tls.certFile, server.cert), atomicWrite(setup.tls.keyFile, server.private)]);
|
|
255
257
|
}
|
|
258
|
+
Object.freeze({
|
|
259
|
+
manifest: 65536,
|
|
260
|
+
script: 1048576,
|
|
261
|
+
css: 524288,
|
|
262
|
+
asset: 8388608
|
|
263
|
+
});
|
|
264
|
+
/** A controlled business failure returned by an extension action or route. */
|
|
265
|
+
var MobileExtensionError = class extends Error {
|
|
266
|
+
code;
|
|
267
|
+
status;
|
|
268
|
+
constructor(code, message, status = 400) {
|
|
269
|
+
super(message);
|
|
270
|
+
this.code = code;
|
|
271
|
+
this.status = status;
|
|
272
|
+
this.name = "MobileExtensionError";
|
|
273
|
+
}
|
|
274
|
+
};
|
|
275
|
+
/** Validate a stable extension id. */
|
|
276
|
+
function assertExtensionId(value) {
|
|
277
|
+
if (typeof value !== "string" || !/^[a-z][a-z0-9-]{0,63}$/u.test(value)) throw new MobileExtensionError("invalid_manifest", "extension id is invalid");
|
|
278
|
+
return value;
|
|
279
|
+
}
|
|
256
280
|
//#endregion
|
|
257
281
|
//#region src/cli.ts
|
|
258
282
|
const execFile$1 = promisify(execFile);
|
|
@@ -407,6 +431,12 @@ async function setup(args) {
|
|
|
407
431
|
""
|
|
408
432
|
].join("\n"), { mode: 384 });
|
|
409
433
|
}
|
|
434
|
+
const extensions = join(directory, "extensions");
|
|
435
|
+
await mkdir(extensions, {
|
|
436
|
+
recursive: true,
|
|
437
|
+
mode: 448
|
|
438
|
+
});
|
|
439
|
+
await createExtensionScaffold(extensions, "custom", "自定义移动扩展", false);
|
|
410
440
|
const origin = `https://${network.address}:${String(options.port)}`;
|
|
411
441
|
await Promise.all([writeFile(join(directory, "setup.json"), `${JSON.stringify({
|
|
412
442
|
...managedSetup,
|
|
@@ -415,9 +445,69 @@ async function setup(args) {
|
|
|
415
445
|
console.log(`DSH Mobile follows ${network.name} and is currently configured for ${origin}`);
|
|
416
446
|
console.log(`Install this CA certificate on Android once: ${androidCertificate}`);
|
|
417
447
|
console.log(`Ask DSH to customize the mobile Web UI and features in: ${customCss} and ${customScript}`);
|
|
448
|
+
console.log(`Additional extensions live in: ${extensions}`);
|
|
418
449
|
console.log("Start DSH with: dsh --profile web");
|
|
419
450
|
console.log("Then open the Mobile card in the lower-left corner and create a pairing key.");
|
|
420
451
|
}
|
|
452
|
+
async function createExtensionScaffold(root, id, name, refuseExisting = true) {
|
|
453
|
+
assertExtensionId(id);
|
|
454
|
+
await mkdir(root, {
|
|
455
|
+
recursive: true,
|
|
456
|
+
mode: 448
|
|
457
|
+
});
|
|
458
|
+
const directory = join(root, id);
|
|
459
|
+
try {
|
|
460
|
+
await mkdir(directory, {
|
|
461
|
+
recursive: false,
|
|
462
|
+
mode: 448
|
|
463
|
+
});
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (error.code === "EEXIST" && !refuseExisting) return;
|
|
466
|
+
if (error.code === "EEXIST") throw new Error(`extension directory already exists: ${id}`);
|
|
467
|
+
throw error;
|
|
468
|
+
}
|
|
469
|
+
const files = {
|
|
470
|
+
"extension.json": `${JSON.stringify({
|
|
471
|
+
schemaVersion: 1,
|
|
472
|
+
id,
|
|
473
|
+
name,
|
|
474
|
+
version: "0.1.0",
|
|
475
|
+
description: "在手机端扩展 DSH"
|
|
476
|
+
}, null, 2)}\n`,
|
|
477
|
+
"host.mjs": `export default async function activate(api) {\n api.action('hello', {\n input: api.schema.object({ name: api.schema.string().max(80) }),\n async run({ signal, deviceId }, input) {\n void signal; void deviceId\n return { message: \`Hello, \${input.name}\` }\n },\n })\n}\n`,
|
|
478
|
+
"mobile.js": `window.dshMobile?.define?.({\n apiVersion: 1,\n id: '${id}',\n activate(api) {\n return api.ui.registerSurface({\n id: '${id}-page', placement: 'page', label: ${JSON.stringify(name)},\n mount(container) {\n container.textContent = ${JSON.stringify(`这是 ${name} 的移动页面。`)}\n return () => container.replaceChildren()\n },\n })\n },\n})\n`,
|
|
479
|
+
"mobile.css": `/* ${name.replaceAll("*/", "* /")} 的移动端样式。保存后通常会在几秒内刷新。 */\n`
|
|
480
|
+
};
|
|
481
|
+
try {
|
|
482
|
+
for (const [file, contents] of Object.entries(files)) await writeFile(join(directory, file), contents, {
|
|
483
|
+
encoding: "utf8",
|
|
484
|
+
flag: "wx",
|
|
485
|
+
mode: 384
|
|
486
|
+
});
|
|
487
|
+
} catch (error) {
|
|
488
|
+
await rm(directory, {
|
|
489
|
+
recursive: true,
|
|
490
|
+
force: true
|
|
491
|
+
});
|
|
492
|
+
throw error;
|
|
493
|
+
}
|
|
494
|
+
console.log(`Created extension: ${directory}`);
|
|
495
|
+
}
|
|
496
|
+
async function extensionCommand(args) {
|
|
497
|
+
const [subcommand, id, ...rest] = args;
|
|
498
|
+
if (subcommand !== "create" || id === void 0) throw new Error("usage: extension create <id> [--name <name>]");
|
|
499
|
+
let name = id;
|
|
500
|
+
for (let index = 0; index < rest.length; index += 1) {
|
|
501
|
+
if (rest[index] === "--name" && rest[index + 1] !== void 0) {
|
|
502
|
+
name = rest[index + 1];
|
|
503
|
+
index += 1;
|
|
504
|
+
continue;
|
|
505
|
+
}
|
|
506
|
+
throw new Error(`unknown extension option: ${rest[index] ?? ""}`);
|
|
507
|
+
}
|
|
508
|
+
if (name.length === 0 || name.length > 120 || /[\u0000-\u001f\u007f]/u.test(name)) throw new Error("--name is invalid");
|
|
509
|
+
await createExtensionScaffold(join(dshHome(), "mobile-access", "extensions"), id, name);
|
|
510
|
+
}
|
|
421
511
|
async function purge(args) {
|
|
422
512
|
if (args.length !== 1 || args[0] !== "--yes") throw new Error("purge requires --yes");
|
|
423
513
|
const home = dshHome();
|
|
@@ -431,6 +521,7 @@ async function purge(args) {
|
|
|
431
521
|
function help() {
|
|
432
522
|
console.log([
|
|
433
523
|
"dsh-mobile setup [--address 192.168.x.x] [--port 3443] [--dsh-port 3080] [--no-firewall]",
|
|
524
|
+
"dsh-mobile extension create <id> [--name <name>]",
|
|
434
525
|
"dsh-mobile purge --yes",
|
|
435
526
|
"",
|
|
436
527
|
"Run through the DSH profile:",
|
|
@@ -440,6 +531,7 @@ function help() {
|
|
|
440
531
|
async function main() {
|
|
441
532
|
const [command = "help", ...args] = process.argv.slice(2);
|
|
442
533
|
if (command === "setup") await setup(args);
|
|
534
|
+
else if (command === "extension") await extensionCommand(args);
|
|
443
535
|
else if (command === "purge") await purge(args);
|
|
444
536
|
else if (command === "help" || command === "--help" || command === "-h") help();
|
|
445
537
|
else throw new Error(`unknown command: ${command}`);
|