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 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. 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.
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 owns discovery, pairing, HTTPS, and the proxy. The Client face replaces only the phone layout entry while reusing native feature plugins. Neither the DeepSeek Harness source nor its desktop page on port 3080 is modified.
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.30` | `0.1.0-rc.5`, `0.1.0-rc.6`, `0.1.0-rc.7` |
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 和浏览器通常会在几秒内应用变化。自定义不限于配色:`mobile.js` 可以添加导航、快捷操作、状态面板、相机、语音、扫码,以及调用同源 DSH API 的完整交互。
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
- 插件包含两部分:Host face 提供发现、配对、HTTPS 和回环代理;Client face 替换移动端的布局入口,并复用原生功能插件。DeepSeek Harness 的源码和 3080 桌面页面都不会被修改,安装和卸载完全通过插件机制完成。
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.30` | `0.1.0-rc.5`、`0.1.0-rc.6`、`0.1.0-rc.7` |
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}`);