dsh-mobile 0.1.0-alpha.30 → 0.1.0-alpha.32
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 +29 -3
- package/README.md +29 -3
- 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 +160 -3
- package/lib/index.mjs +817 -13
- package/package.json +8 -3
package/README.en.md
CHANGED
|
@@ -101,7 +101,33 @@ a session status panel, and a press-and-hold voice entry. Narrow screens only;
|
|
|
101
101
|
do not modify the DSH source.
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
Changes are applied to open Android and browser pages within a few seconds.
|
|
104
|
+
Changes are applied to open Android and browser pages within a few seconds. `mobile.css` and `mobile.js` own the phone UI, interactions, and orchestration of existing APIs; they cannot create computer files, run commands, or access computer hardware by themselves. Browsers fall back to available Web APIs, while the Android app exposes a narrow native bridge for file picking, camera capture, sharing, clipboard, notifications, and speech.
|
|
105
|
+
|
|
106
|
+
### Add computer-side capabilities
|
|
107
|
+
|
|
108
|
+
For a new capability on the computer, create an extension under `$DSH_HOME/mobile-access/extensions/<id>/`:
|
|
109
|
+
|
|
110
|
+
```text
|
|
111
|
+
extension.json # metadata
|
|
112
|
+
host.mjs # trusted local Node.js code
|
|
113
|
+
mobile.js # optional phone-side script
|
|
114
|
+
mobile.css # optional phone-side styles
|
|
115
|
+
assets/ # optional static files
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Generate a complete starter extension with:
|
|
119
|
+
|
|
120
|
+
```powershell
|
|
121
|
+
dsh plugin --profile web exec dsh-mobile extension create media-tools --name "Media tools"
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
`host.mjs` can register schema-validated actions, ordinary HTTP/streaming/SSE routes, and teardown effects; `mobile.js` calls them through `api.host.invoke()` or `api.host.fetch()`. Extension source files can be changed only on the computer by the user or DSH; the phone has no endpoint that writes them. Closing Mobile Access, revoking a device, refreshing an extension, or stopping the gateway aborts its active requests.
|
|
125
|
+
|
|
126
|
+
A published DSH plugin can also call `ctx.mobileAccess.registerExtension(definition)` from a Cordis effect. It shares the same authentication, routes, and client SDK as local directory extensions without modifying DSH core.
|
|
127
|
+
|
|
128
|
+
Extension IDs are unique. Host code, scripts, and CSS switch as one generation; if a new generation fails, the previous one remains active. An empty `extensions/` directory is inert. Paired devices have every registered extension permission, so install and edit only trusted `host.mjs` code.
|
|
129
|
+
|
|
130
|
+
Normal DSH community plugins continue to load through standard `dsh.client` and Slot contributions: conversation nodes, tool cards, settings sections, sidebar items, header actions, composer docks, and overlays remain available in the same session. Plugins that depend on hover, fixed desktop widths, system file pickers, or private DOM selectors need additional mobile adaptation.
|
|
105
131
|
|
|
106
132
|
## How it works
|
|
107
133
|
|
|
@@ -111,7 +137,7 @@ flowchart LR
|
|
|
111
137
|
Gateway -->|"loopback proxy"| DSH["Stock DSH Web and Host"]
|
|
112
138
|
```
|
|
113
139
|
|
|
114
|
-
The Host face
|
|
140
|
+
The three layers are: the Host face for discovery, pairing, HTTPS, loopback proxying, and extension registration; the Client face for the dedicated mobile root, multi-extension SDK, and hot updates; and the Android app for an exact-origin native bridge. Neither the DeepSeek Harness source nor its desktop page on port 3080 is modified.
|
|
115
141
|
|
|
116
142
|
## Security
|
|
117
143
|
|
|
@@ -125,7 +151,7 @@ See [SECURITY.md](SECURITY.md).
|
|
|
125
151
|
|
|
126
152
|
| DSH Mobile | Verified DeepSeek Harness releases |
|
|
127
153
|
| --- | --- |
|
|
128
|
-
| `0.1.0-alpha.
|
|
154
|
+
| `0.1.0-alpha.32` | `0.1.0-rc.5`, `0.1.0-rc.6`, `0.1.0-rc.7` |
|
|
129
155
|
|
|
130
156
|
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 slots and mobile semantic markers. If a DSH upgrade reports an incompatibility, update DSH Mobile first.
|
|
131
157
|
|
package/README.md
CHANGED
|
@@ -106,7 +106,33 @@ $DSH_HOME/mobile-access/mobile.js
|
|
|
106
106
|
会话状态面板和长按语音入口。只影响窄屏,不修改 DSH 源码。
|
|
107
107
|
```
|
|
108
108
|
|
|
109
|
-
保存后,已打开的 App
|
|
109
|
+
保存后,已打开的 App 和浏览器通常会在几秒内应用变化。`mobile.css` 和 `mobile.js` 负责手机页面的样式、交互和已有 API 编排;它们不能单独创建电脑文件、运行命令或访问电脑硬件。浏览器按 Web API 能力降级,Android App 通过受限的原生 Bridge 提供文件选择、拍照、分享、剪贴板、通知和语音等能力。
|
|
110
|
+
|
|
111
|
+
### 扩展电脑端能力
|
|
112
|
+
|
|
113
|
+
需要手机调用新的电脑能力时,在 `$DSH_HOME/mobile-access/extensions/<id>/` 创建扩展:
|
|
114
|
+
|
|
115
|
+
```text
|
|
116
|
+
extension.json # 元数据
|
|
117
|
+
host.mjs # 电脑端 Node.js 代码(可信本地代码)
|
|
118
|
+
mobile.js # 手机端脚本,可选
|
|
119
|
+
mobile.css # 手机端样式,可选
|
|
120
|
+
assets/ # 静态资源,可选
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
可用命令生成模板:
|
|
124
|
+
|
|
125
|
+
```powershell
|
|
126
|
+
dsh plugin --profile web exec dsh-mobile extension create media-tools --name "媒体工具"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`host.mjs` 可以注册经过 Schema 校验的 Action、普通 HTTP/流式/SSE Route 和清理 Effect;`mobile.js` 通过 `api.host.invoke()` 或 `api.host.fetch()` 调用它们。扩展文件只能在电脑端由用户或 DSH 修改,手机没有写入这些文件的接口。Mobile Access 关闭、设备撤销、扩展刷新或网关关闭时,扩展请求会被中止。
|
|
130
|
+
|
|
131
|
+
发布型 DSH 插件也可以在 Cordis effect 中调用 `ctx.mobileAccess.registerExtension(definition)`,与本地目录扩展共用认证、路由和客户端 SDK,不需要修改 DSH 核心。
|
|
132
|
+
|
|
133
|
+
每个扩展的 `id` 必须唯一,Host、脚本和 CSS 会作为同一版本热切换;新版本加载失败时保留上一版本。空的 `extensions/` 目录不产生副作用。配对设备拥有所有已注册扩展的权限,因此只应安装和编辑自己信任的 `host.mjs`。
|
|
134
|
+
|
|
135
|
+
普通 DSH 社区插件仍按标准 `dsh.client` 和 Slot 贡献加载:对话节点、工具卡、设置区、侧栏项、Header Action、Composer Dock 和 Overlay 会随同一会话同步。只有依赖鼠标悬停、固定桌面宽度、系统文件选择器或私有 DOM 的插件需要额外移动适配。
|
|
110
136
|
|
|
111
137
|
## 工作原理
|
|
112
138
|
|
|
@@ -117,7 +143,7 @@ flowchart LR
|
|
|
117
143
|
DSH -->|"同一工作区、会话和事件流"| Phone
|
|
118
144
|
```
|
|
119
145
|
|
|
120
|
-
|
|
146
|
+
插件包含三层:Host face 提供发现、配对、HTTPS、回环代理和扩展注册表;Client face 提供独立的移动根布局、多扩展 SDK 和热更新;Android App 提供精确 Origin 限定的原生 Bridge。DeepSeek Harness 的源码和 3080 桌面页面都不会被修改,安装和卸载完全通过插件机制完成。
|
|
121
147
|
|
|
122
148
|
## 安全
|
|
123
149
|
|
|
@@ -132,7 +158,7 @@ flowchart LR
|
|
|
132
158
|
|
|
133
159
|
| DSH Mobile | 已验证的 DeepSeek Harness |
|
|
134
160
|
| ------------------ | ------------------------------------------ |
|
|
135
|
-
| `0.1.0-alpha.
|
|
161
|
+
| `0.1.0-alpha.32` | `0.1.0-rc.5`、`0.1.0-rc.6`、`0.1.0-rc.7` |
|
|
136
162
|
|
|
137
163
|
插件会在启动时检查 DSH Host 版本和移动布局所需的前端依赖;遇到未经验证的版本会直接给出错误,不会带着不兼容页面继续启动。CI 也会持续检查 DSH 主分支的布局插槽和移动端语义标记。升级 DSH 后如遇兼容提示,请先升级 DSH Mobile。
|
|
138
164
|
|
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}`);
|