dsh-pocket 1.0.9 → 1.0.11

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
@@ -18,12 +18,15 @@
18
18
 
19
19
  ## What is this
20
20
 
21
- `dsh web` only allows local access by default (`127.0.0.1:3080`). DSH Pocket puts a **header-rewriting proxy** in front of it and hands you QR codes right in the DSH web settings page:
21
+ **You want to use DeepSeek Harness on your computer, even when you're not at the computer.**
22
22
 
23
- ```
24
- Phone ──scan──> dsh-pocket proxy ──> dsh web :3080
25
- (rewrites Host/Origin to loopback; passes DSH's trust fence)
26
- ```
23
+ - On your way home, the agent is running a task on your computer — pull out your phone and see where it is, what it produced.
24
+ - Out and about, you want the agent on your computer to look something up or write a snippet — no remote desktop, no SSH.
25
+ - The computer is at home or in the office, you're elsewhere, and you want to **drive your DeepSeek Harness from your phone** — send tasks, watch the output, tap approvals.
26
+
27
+ That's what DSH Pocket does: **install it, scan a QR code, and your phone shows and controls the DeepSeek Harness UI in real time — from anywhere.**
28
+
29
+ > One-liner: `dsh web` only allows local access by default; DSH Pocket puts a proxy in front of it that safely bridges your phone (see [How it works](#-how-it-works)).
27
30
 
28
31
  ## ✨ Features
29
32
 
@@ -39,6 +42,12 @@ Phone ──scan──> dsh-pocket proxy ──> dsh web :3080
39
42
 
40
43
  ## 🚀 Usage
41
44
 
45
+ **Where the entry is**: after installing and restarting `dsh web`, open **Settings** — the left sidebar shows **"Phone access"** at the top level (same level as General / Models):
46
+
47
+ <p align="center">
48
+ <img src="docs/entry.jpg" alt="Phone access entry" width="70%">
49
+ </p>
50
+
42
51
  **Prerequisite**: [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) installed. If your terminal says `dsh: command not found`, install it first:
43
52
 
44
53
  ```sh
@@ -139,3 +148,7 @@ npm test # proxy rewrite / WS passthrough / tunnel / service / RP
139
148
  [GPL-2.0](LICENSE) — copyleft: free to use, modify, and redistribute, but **derivatives must stay GPL** and keep the copyright notice; commercial use included.
140
149
 
141
150
  > Note: the mobile-adaptation portion is ported from [dsh-web-mobile](https://github.com/mexiaosqwq/dsh-web-mobile) (MIT, GPL-compatible); its copyright notice stays in `client/mobile/LICENSE.dsh-web-mobile`.
151
+
152
+ ---
153
+
154
+ **Questions? Feedback welcome**: bugs, ideas, or feature requests — open an issue at [GitHub Issues](https://github.com/shaobeichen/dsh-pocket/issues) 🙏
package/README.md CHANGED
@@ -18,12 +18,15 @@
18
18
 
19
19
  ## 这是什么
20
20
 
21
- `dsh web` 默认只允许本机访问(`127.0.0.1:3080`)。DSH Pocket 在它前面加一层**改头代理**,并直接在 **DSH 网页设置页**里给你二维码:
21
+ **你不在电脑前,也想用电脑上的 DeepSeek Harness。**
22
22
 
23
- ```
24
- 手机 ──扫码──> dsh-pocket 代理 ──> dsh web :3080
25
- (改写 Host/Origin loopback,DSH 信任栅栏放行)
26
- ```
23
+ - 下班路上,agent 在电脑上跑任务,你想掏出手机看看它干到哪了、结果如何
24
+ - 出门在外,突然想让电脑上的 agent 查点资料、写段代码,但没有远程桌面、没有 SSH
25
+ - 电脑在宿舍/办公室,你人在外面,想随时"操控你的 DeepSeek Harness"——发任务、看输出、点审批
26
+
27
+ DSH Pocket 就是干这个的:**装上它,手机扫个码,就能实时看到并操控电脑上的 DeepSeek Harness 界面**——人在外面也能用。
28
+
29
+ > 原理一句话:`dsh web` 默认只允许本机访问,DSH Pocket 在它前面加一层代理,把手机访问安全地接入(详见[「原理」](#-原理))。
27
30
 
28
31
  ## ✨ 特性
29
32
 
@@ -39,6 +42,12 @@
39
42
 
40
43
  ## 🚀 怎么用
41
44
 
45
+ **入口在哪**:安装完成并重启 `dsh web` 后,打开 **设置**,左侧边栏就能看到 **「手机访问」** 入口(和「通用设置」「模型」同级):
46
+
47
+ <p align="center">
48
+ <img src="docs/entry.jpg" alt="手机访问入口" width="70%">
49
+ </p>
50
+
42
51
  **前提**:电脑上已装好 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)。如果终端提示 `dsh: command not found`(找不到 dsh 命令),先安装:
43
52
 
44
53
  ```sh
@@ -140,3 +149,7 @@ npm test # 代理改写 / WS 透传 / 隧道 / 服务 / RPC(7
140
149
  [GPL-2.0](LICENSE) —— 自由软件许可:可自由使用、修改、分发,但**修改版必须同样以 GPL 开源**并保留版权声明;商用同样适用。
141
150
 
142
151
  > 说明:移动端适配部分移植自 [dsh-web-mobile](https://github.com/mexiaosqwq/dsh-web-mobile)(MIT 许可,兼容 GPL),其版权声明保留在 `client/mobile/LICENSE.dsh-web-mobile`。
152
+
153
+ ---
154
+
155
+ **有问题?欢迎反馈**:遇到 Bug、有想法、想提需求,请到 [GitHub Issues](https://github.com/shaobeichen/dsh-pocket/issues) 告诉我们 🙏
package/client/api.js CHANGED
@@ -12,6 +12,7 @@ export const POCKET_ENDPOINTS = Object.freeze({
12
12
  pushSetEnabled: 'push.setEnabled',
13
13
  version: 'pocket.version',
14
14
  update: 'pocket.update',
15
+ restart: 'pocket.restart',
15
16
  });
16
17
 
17
18
  /** 语义化版本比较:a > b 返回正数,相等 0,a < b 负数(仅数字段;带预发布后缀的更旧)。 */
package/client/client.js CHANGED
@@ -44,7 +44,8 @@ var POCKET_ENDPOINTS = Object.freeze({
44
44
  pushStatus: "push.status",
45
45
  pushSetEnabled: "push.setEnabled",
46
46
  version: "pocket.version",
47
- update: "pocket.update"
47
+ update: "pocket.update",
48
+ restart: "pocket.restart"
48
49
  });
49
50
  function compareVersions(a, b) {
50
51
  const pa = String(a).replace(/^v/, "").split(".");
@@ -1346,6 +1347,14 @@ function PocketSettingsTab({ rpcCall }) {
1346
1347
  alive = false;
1347
1348
  };
1348
1349
  }, []);
1350
+ const restartPocket = async () => {
1351
+ setUpdateInfo((u) => ({ ...u, restarting: true }));
1352
+ try {
1353
+ await call(POCKET_ENDPOINTS.restart, {});
1354
+ } catch (err) {
1355
+ setUpdateInfo((u) => ({ ...u, restarting: false, result: "fail", output: err.message }));
1356
+ }
1357
+ };
1349
1358
  const runUpdate = async () => {
1350
1359
  setUpdateInfo((u) => ({ ...u, updating: true, result: null }));
1351
1360
  try {
@@ -1406,9 +1415,18 @@ function PocketSettingsTab({ rpcCall }) {
1406
1415
  { style: styles.card },
1407
1416
  (0, import_react2.createElement)(
1408
1417
  "div",
1409
- null,
1410
- (0, import_react2.createElement)("strong", null, "\u{1F4F1} \u624B\u673A\u8BBF\u95EE | Phone access"),
1411
- (0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u626B\u7801\u6253\u5F00\u7684\u5C31\u662F\u7535\u8111\u4E0A\u7684\u8FD9\u4E2A\u754C\u9762\uFF0C\u5B9E\u65F6\u540C\u6B65 | the phone shows this exact screen, live")
1418
+ { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
1419
+ (0, import_react2.createElement)(
1420
+ "div",
1421
+ null,
1422
+ (0, import_react2.createElement)("strong", null, "\u{1F4F1} \u624B\u673A\u8BBF\u95EE | Phone access"),
1423
+ (0, import_react2.createElement)("div", { style: styles.muted }, "\u624B\u673A\u626B\u7801\u6253\u5F00\u7684\u5C31\u662F\u7535\u8111\u4E0A\u7684\u8FD9\u4E2A\u754C\u9762\uFF0C\u5B9E\u65F6\u540C\u6B65 | the phone shows this exact screen, live")
1424
+ ),
1425
+ (0, import_react2.createElement)(
1426
+ "div",
1427
+ { style: { fontSize: 12, color: "var(--dsw-alias-label-tertiary,#8b93a1)", whiteSpace: "nowrap" } },
1428
+ "\u5F00\u53D1\u8005\uFF1A\u7A0B\u5E8F\u5458\u5C11\u5317\u6668"
1429
+ )
1412
1430
  ),
1413
1431
  // 更新提示
1414
1432
  updateInfo ? (0, import_react2.createElement)(
@@ -1418,7 +1436,7 @@ function PocketSettingsTab({ rpcCall }) {
1418
1436
  "div",
1419
1437
  { style: { display: "flex", alignItems: "center", justifyContent: "space-between", gap: 8 } },
1420
1438
  (0, import_react2.createElement)("div", { style: { fontWeight: 600, fontSize: 13 } }, `\u{1F4E6} \u65B0\u7248\u672C v${updateInfo.latest} | Update available`),
1421
- updateInfo.result !== "ok" ? (0, import_react2.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? "\u66F4\u65B0\u4E2D\u2026" : `\u66F4\u65B0\u5230 v${updateInfo.latest} | Update`) : (0, import_react2.createElement)("button", { style: styles.btn, onClick: () => window.location.reload() }, "\u{1F504} \u5237\u65B0\u9875\u9762 | Refresh")
1439
+ updateInfo.result !== "ok" ? (0, import_react2.createElement)("button", { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? "\u66F4\u65B0\u4E2D\u2026" : `\u66F4\u65B0\u5230 v${updateInfo.latest} | Update`) : (0, import_react2.createElement)("button", { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? "\u91CD\u542F\u4E2D\u2026" : "\u{1F504} \u91CD\u542F dsh web \u751F\u6548 | Restart now")
1422
1440
  ),
1423
1441
  (0, import_react2.createElement)(
1424
1442
  "div",
@@ -1478,7 +1496,17 @@ function PocketSettingsTab({ rpcCall }) {
1478
1496
  !pushEnabled ? "\u5DF2\u5173\u95ED\uFF1Aagent \u8DD1\u5B8C\u4E0D\u4F1A\u63A8\u9001 | off: no notifications" : pushState === "on" ? "\u5DF2\u5F00\u542F\uFF1Aagent \u8DD1\u5B8C/\u51FA\u9519\u65F6\u624B\u673A\u6536\u5230\u901A\u77E5 | on: notified when tasks finish or fail" : pushState === "unsupported" ? "\u5DF2\u5F00\u542F\uFF08\u4F46\u5F53\u524D\u6D4F\u89C8\u5668\u4E0D\u652F\u6301\u63A8\u9001\uFF09| on, but this browser does not support push" : pushState === "insecure" ? "\u5DF2\u5F00\u542F\uFF0C\u4F46\u5F53\u524D\u8DEF\u5F84\u4E0D\u662F HTTPS\u2014\u2014\u63A8\u9001\u9700\u8981\u516C\u7F51\u96A7\u9053\u6216 localhost | on, but push needs HTTPS (public tunnel) or localhost" : pushState === "checking" ? "\u68C0\u67E5\u4E2D\u2026 | checking\u2026" : "\u63A8\u9001\u672A\u751F\u6548 | push not active"
1479
1497
  )
1480
1498
  ),
1481
- error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${error}`) : null
1499
+ error ? (0, import_react2.createElement)("div", { style: { color: "var(--dsw-alias-state-error-primary,#dc2626)", fontSize: 12, marginTop: 8 } }, `\u274C ${error}`) : null,
1500
+ // 页面最底部:反馈入口
1501
+ (0, import_react2.createElement)(
1502
+ "div",
1503
+ { style: { ...styles.block, textAlign: "center" } },
1504
+ (0, import_react2.createElement)(
1505
+ "a",
1506
+ { href: "https://github.com/shaobeichen/dsh-pocket/issues", target: "_blank", rel: "noreferrer", style: { fontSize: 12, color: "var(--dsw-alias-label-secondary,#6b7280)", textDecoration: "none" } },
1507
+ "\u6709\u95EE\u9898\uFF1F\u6B22\u8FCE\u5230 GitHub Issues \u53CD\u9988 \u{1F64F} | Questions? Open an issue on GitHub"
1508
+ )
1509
+ )
1482
1510
  );
1483
1511
  }
1484
1512
  function apply(ctx) {
package/client/index.jsx CHANGED
@@ -105,6 +105,17 @@ function PocketSettingsTab({ rpcCall }) {
105
105
  return () => { alive = false; };
106
106
  }, []);
107
107
 
108
+ // 重启宿主(更新生效必需:刷新页面不会重载服务端代码)
109
+ const restartPocket = async () => {
110
+ setUpdateInfo((u) => ({ ...u, restarting: true }));
111
+ try {
112
+ await call(POCKET_ENDPOINTS.restart, {});
113
+ // 宿主即将退出重启,无需更多处理
114
+ } catch (err) {
115
+ setUpdateInfo((u) => ({ ...u, restarting: false, result: 'fail', output: err.message }));
116
+ }
117
+ };
118
+
108
119
  // 一键更新:调宿主 dsh plugin update
109
120
  const runUpdate = async () => {
110
121
  setUpdateInfo((u) => ({ ...u, updating: true, result: null }));
@@ -166,9 +177,13 @@ function PocketSettingsTab({ rpcCall }) {
166
177
  const tunnelStateStarted = tunnelState?.startedAt ?? null;
167
178
 
168
179
  return h('div', { style: styles.card },
169
- h('div', null,
170
- h('strong', null, '📱 手机访问 | Phone access'),
171
- h('div', { style: styles.muted }, '手机扫码打开的就是电脑上的这个界面,实时同步 | the phone shows this exact screen, live'),
180
+ h('div', { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 } },
181
+ h('div', null,
182
+ h('strong', null, '📱 手机访问 | Phone access'),
183
+ h('div', { style: styles.muted }, '手机扫码打开的就是电脑上的这个界面,实时同步 | the phone shows this exact screen, live'),
184
+ ),
185
+ h('div', { style: { fontSize: 12, color: 'var(--dsw-alias-label-tertiary,#8b93a1)', whiteSpace: 'nowrap' } },
186
+ '开发者:程序员少北晨'),
172
187
  ),
173
188
 
174
189
  // 更新提示
@@ -177,7 +192,7 @@ function PocketSettingsTab({ rpcCall }) {
177
192
  h('div', { style: { fontWeight: 600, fontSize: 13 } }, `📦 新版本 v${updateInfo.latest} | Update available`),
178
193
  updateInfo.result !== 'ok'
179
194
  ? h('button', { style: styles.primary, onClick: runUpdate, disabled: updateInfo.updating }, updateInfo.updating ? '更新中…' : `更新到 v${updateInfo.latest} | Update`)
180
- : h('button', { style: styles.btn, onClick: () => window.location.reload() }, '🔄 刷新页面 | Refresh'),
195
+ : h('button', { style: styles.primary, onClick: restartPocket, disabled: updateInfo.restarting }, updateInfo.restarting ? '重启中…' : '🔄 重启 dsh web 生效 | Restart now'),
181
196
  ),
182
197
  h('div', { style: styles.muted, marginTop: 4 },
183
198
  updateInfo.result === 'ok' ? '✅ 已更新,重启 dsh web 生效 | updated — restart dsh web'
@@ -234,6 +249,12 @@ function PocketSettingsTab({ rpcCall }) {
234
249
  ),
235
250
 
236
251
  error ? h('div', { style: { color: 'var(--dsw-alias-state-error-primary,#dc2626)', fontSize: 12, marginTop: 8 } }, `❌ ${error}`) : null,
252
+
253
+ // 页面最底部:反馈入口
254
+ h('div', { style: { ...styles.block, textAlign: 'center' } },
255
+ h('a', { href: 'https://github.com/shaobeichen/dsh-pocket/issues', target: '_blank', rel: 'noreferrer', style: { fontSize: 12, color: 'var(--dsw-alias-label-secondary,#6b7280)', textDecoration: 'none' } },
256
+ '有问题?欢迎到 GitHub Issues 反馈 🙏 | Questions? Open an issue on GitHub'),
257
+ ),
237
258
  );
238
259
  }
239
260
 
package/lib/index.js CHANGED
@@ -15,6 +15,7 @@ import { createRequire } from 'node:module';
15
15
  import { createPocketService } from './service.mjs';
16
16
  import { installPocketRpc } from './web-rpc.js';
17
17
  import { createPushService } from './push.mjs';
18
+ import { restartHost } from './restart.js';
18
19
 
19
20
  const name = 'dsh-pocket';
20
21
  const inject = ['connection', 'webServer'];
@@ -83,6 +84,7 @@ export function apply(ctx, config = {}, internals = {}) {
83
84
  service,
84
85
  push: internals.push ?? { vapidPublicKey: () => '', count: () => 0, subscribe: async () => false, unsubscribe: async () => false, isEnabled: () => true, setEnabled: async () => true },
85
86
  runUpdate: internals.runUpdate ?? { currentVersion, perform: performUpdate },
87
+ restart: internals.restart ?? (() => restartHost({ internals })),
86
88
  log: logger,
87
89
  });
88
90
  disposers.push(disposeRpc);
package/lib/restart.js ADDED
@@ -0,0 +1,45 @@
1
+ // dsh-pocket 自重启:重新拉起启动本宿主的确切 dsh 调用(脱离父进程),
2
+ // 让更新后的插件代码生效——用户无需离开界面手动重启(借鉴 dshmarket 的
3
+ // self-restart 方案,见 https://github.com/dsh-market/dsh-market lib/restart.js)。
4
+ //
5
+ // 原理:本插件运行在 dsh web 进程内,process.argv 即 dsh CLI 的启动参数
6
+ // (如 node <bin.js> web [flags])。用相同参数 detached 拉起新进程,
7
+ // 短暂等待后旧进程退出,新进程接管端口。
8
+
9
+ import { spawn } from 'node:child_process';
10
+
11
+ /**
12
+ * 重建启动调用(与当前宿主相同的命令)。
13
+ * @returns {{file:string, args:string[]}}
14
+ */
15
+ export function restartLaunch() {
16
+ return {
17
+ file: process.argv[0], // node
18
+ args: [process.argv[1], ...process.argv.slice(2)], // <bin.js> + web [flags]
19
+ };
20
+ }
21
+
22
+ /**
23
+ * 拉起替代宿主(detached),随后退出当前进程。
24
+ * @param {object} opts
25
+ * @param {number} [opts.exitDelayMs] 等待新进程接管后再退出(默认 800ms)
26
+ * @param {object} [opts.internals] 测试注入:spawnFn / exitFn
27
+ * @returns {{spawned:boolean, file:string}}
28
+ */
29
+ export function restartHost({ exitDelayMs = 800, internals = {} } = {}) {
30
+ const spawnFn = internals.spawn ?? spawn;
31
+ const exitFn = internals.exit ?? ((code) => process.exit(code));
32
+ const { file, args } = restartLaunch();
33
+ try {
34
+ const child = spawnFn(file, args, {
35
+ detached: true,
36
+ stdio: 'ignore',
37
+ env: process.env,
38
+ });
39
+ child.unref?.();
40
+ setTimeout(() => exitFn(0), exitDelayMs);
41
+ return { spawned: true, file };
42
+ } catch (err) {
43
+ return { spawned: false, error: err?.message ?? String(err) };
44
+ }
45
+ }
package/lib/web-rpc.js CHANGED
@@ -17,7 +17,7 @@ function fail(code, message) {
17
17
  }
18
18
 
19
19
  /** 注册 /dsh-pocket 逻辑通道(仅本机 loopback 可调)。 */
20
- export function installPocketRpc(ctx, { service, push, log = console, runUpdate = null }) {
20
+ export function installPocketRpc(ctx, { service, push, log = console, runUpdate = null, restart = null }) {
21
21
  if (!ctx?.connection?.rpc?.handle) {
22
22
  log.warn?.('dsh-pocket: DSH Host Connection RPC unavailable — settings tab disabled | 无 Connection RPC,设置页不可用');
23
23
  return () => {};
@@ -63,6 +63,11 @@ export function installPocketRpc(ctx, { service, push, log = console, runUpdate
63
63
  const result = await runUpdate.perform(payload?.profile ?? 'web');
64
64
  return ok(result);
65
65
  }
66
+ if (endpoint === POCKET_ENDPOINTS.restart) {
67
+ if (!restart) return fail('bad-request', '重启不可用 | restart unavailable');
68
+ const result = restart();
69
+ return ok(result);
70
+ }
66
71
  return fail('bad-request', `Unknown endpoint: ${endpoint}`);
67
72
  } catch (err) {
68
73
  log.error?.('dsh-pocket: rpc %s failed | RPC 失败: %s', endpoint, err?.message ?? err);
package/package.json CHANGED
@@ -77,5 +77,5 @@
77
77
  "access": "public",
78
78
  "registry": "https://registry.npmjs.org/"
79
79
  },
80
- "version": "1.0.9"
80
+ "version": "1.0.11"
81
81
  }