@mrrisega/dsh-remote 0.6.4-beta.3 → 0.6.4-beta.4

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.md CHANGED
@@ -33,6 +33,7 @@ dsh-remote 是一个轻量的**隧道模式**远程控制方案:电脑端运
33
33
  - **登录后 bridge 自动连上,全程零刷新**:面板自动补运行环境 → 拉起 bridge → 显示「正在连接中继…」,连上后自动变成「已连接 ✅」并刷新二维码与设备列表;失败有可读原因、自动重试倒计时和「复制诊断信息」。
34
34
  - **手机端自动等待电脑上线**:空设备列表不再让人干等或手动刷新,电脑一上线设备自己出现。
35
35
  - **安装引导改版**:有插件市场入口就搜索 `dsh-remote` 安装(不用敲命令);没有就复制一条命令;两条路都收敛到「登录后稍等,电脑会自己出现」。
36
+ - **安装输出更干净**:不再重复打印同一段引导,结尾只给一份汇总(地址 / 自启动服务 / 下一步);装完当下 dsh web 没开着也会如实说明「打开 dsh web 后 bridge 会自动启动」,不再像报错。
36
37
 
37
38
  **0.6.2 起已有**
38
39
 
@@ -142,6 +143,58 @@ npx @mrrisega/dsh-remote setup --server wss://<你的域名>:端口 --key <访
142
143
  - 建议上线前用**真机回归**一次完整链路:手机登录解锁 → 🔒 加密访问(对话/工具/审批/凭据)→
143
144
  明文回退提示 → 修改密码后旧会话全部失效、重新登录恢复。
144
145
 
146
+ ## 匿名装机统计与隐私
147
+
148
+ 生产诊断发现「注册了但设备一直没连上」的用户没有任何账号数据、无法归因,因此插件内置了一条
149
+ **匿名装机统计**通道,只上报**装机与连接是否成功**:
150
+
151
+ - **采**:事件名(安装开始/失败原因/运行环境就绪/bridge 是否注册成功/首次远程打通/面板打开/重启/更新)、
152
+ 失败码(白名单,如 `npm_unreachable`、`npm_eacces`、`runtime_install_timeout`)、插件版本号、
153
+ 系统平台(`darwin`/`linux`/`win32`)、架构(`arm64`/`x64`)、node 主版本号,
154
+ 以及一个**本机随机 ID**(`crypto.randomUUID()`,非硬件派生、重装即变、不可跨机器关联);
155
+ - **不采**:手机号 / 邮箱 / 账号 ID、任何会话或文件内容、真实 hostname / 用户名 / 文件路径、
156
+ 密码与密钥、设备指纹 `machine_fp`、原始 IP、精确地理位置;请求**不带 Authorization**(匿名、与账号解耦);
157
+ - **可关**:`export DSH_REMOTE_TELEMETRY=0` → 完全关闭(不生成随机 ID、不落任何文件、不发任何请求);
158
+ 也可在中继/Nginx 侧直接丢弃 `POST /api/telemetry/events`;
159
+ - **可查**:面板「关于 dsh-remote」卡片底部有一行说明与链接,完整字段清单与核实方法见
160
+ [docs/telemetry.md](docs/telemetry.md)。
161
+
162
+ ## 数据与隐私
163
+
164
+ 上一节讲的是「匿名装机统计」这条通道本身;这一节回答更常见的问题:**你的数据落在哪里、我们到底统计什么**。
165
+
166
+ **开源部分采集 / 不采集**
167
+
168
+ - **采**(开源部分只有一件事):装机与连接是否成功这条**匿名**统计通道,字段清单见上一节。
169
+ - **不采**:**你的会话内容**(对话、工具执行、审批、凭据、文件正文与 WebSocket 消息——中继侧不采集内容)、
170
+ **你的账号**(匿名通道不接受任何账号/设备关联,请求不带 Authorization)、**原始 IP**
171
+ (匿名通道不上报 IP,也不做任何按 IP 的关联分析)、真实 hostname / 用户名 / 文件路径。
172
+ - **官方云服务额外记录的**:只有**接入事件**(注册 / 设备接入 / 真实登录 / 首次打通 / 首次看到安装引导),
173
+ 同样是事件级、不含任何内容。**开源部分不产生也不上报这类事件。**
174
+ - **装机漏斗统计的粒度**:云服务的漏斗统计**建立在审计日志之上**,因此口径是「注册 / 新增设备 /
175
+ 真实登录 / 首次打通」这类**事件条数**——**不是内容,也不是行为轨迹**。
176
+ 桥接进程每次启动都会重新登记设备,**重复登记不记为新增**,所以「新增设备数」对得上真实装机量,
177
+ 不会被反复重连刷高。
178
+ - 完整字段级清单、可核实方法与自行关闭方式见 [docs/telemetry.md](docs/telemetry.md)。
179
+
180
+ **如何关闭**
181
+
182
+ ```bash
183
+ export DSH_REMOTE_TELEMETRY=0 # 完全关闭匿名装机统计:不生成随机 ID、不落文件、不发请求
184
+ ```
185
+
186
+ 也可在网络侧直接丢弃 `POST /api/telemetry/events`(中继 / Nginx / 防火墙),
187
+ 关闭后不影响面板、bridge 与连接流程的任何功能。
188
+
189
+ **自建模式(self-hosted)**
190
+
191
+ 自己部署 `relay-router` 时,**数据只落到你自己的服务器**:
192
+
193
+ - 匿名统计发往你在 `.dsh-config.json` 里配置的 `api_url`(即你的实例),不经过任何第三方服务;
194
+ - 没有账号体系、也没有管理端后台,不存在「注册 / 接入事件」的云侧统计;
195
+ - 远程操作的内容只经过**你自己的**中继;开启 E2EE 后端到端加密(手机 ↔ 电脑),中继也读不到内容;
196
+ - 是否保留数据、保留多久,完全由你决定;不想留任何统计就 `DSH_REMOTE_TELEMETRY=0`。
197
+
145
198
  ## 自建部署(开源版)
146
199
 
147
200
  1. 在有公网 HTTPS 入口的服务器上部署 `relay-router`(见 [docs/self-hosting.md](docs/self-hosting.md)):
@@ -182,6 +235,7 @@ npx @mrrisega/dsh-remote setup --server wss://<你的域名>:端口 --key <访
182
235
  ## 文档
183
236
 
184
237
  - [docs/self-hosting.md](docs/self-hosting.md) — 开源自建完整指南(含安全提示)
238
+ - [docs/telemetry.md](docs/telemetry.md) — 匿名装机统计:采集/不采集清单与关闭方法
185
239
  - [CONTRIBUTING.md](CONTRIBUTING.md) — 贡献指南
186
240
  - [SECURITY.md](SECURITY.md) — 安全策略与漏洞报告流程
187
241
  - [CHANGELOG.md](CHANGELOG.md) — 版本记录
@@ -0,0 +1,145 @@
1
+ # 匿名装机统计(遥测)与隐私边界
2
+
3
+ 本文面向**用户与审计者**:dsh-remote 的「匿名装机统计」通道到底采集什么、不采集什么、
4
+ 数据长什么样、如何**彻底关闭**,以及你如何自己核实这些说法。
5
+
6
+ - 实现代码(客户端半):[`packages/dsh-remote-web/lib/index.js`](../packages/dsh-remote-web/lib/index.js)
7
+ 中的「匿名装机/连接遥测」段落(唯一的 payload 构造点是 `telemetryEventOf()`)。
8
+ - 关闭开关:环境变量 `DSH_REMOTE_TELEMETRY=0`(见下文「如何关闭」)。
9
+ - README 摘要:[README「匿名装机统计与隐私」](../README.md#匿名装机统计与隐私)。
10
+
11
+ ---
12
+
13
+ ## 1. 为什么要做这个统计
14
+
15
+ 2026-09 的生产诊断显示:11 个新注册用户里只有 3 人最终把设备连上——**6 人电脑端从未装上**、
16
+ **2 人装了但 bridge 没连上**。而「装不上」的机器**没有任何账号、也就没有任何数据**,
17
+ 于是「我本地好好的,新机器上失败」这类问题在服务端永远无法归因。
18
+
19
+ 这条通道只补这一段事实:**装机与连接是否成功、卡在哪一步**,用来:
20
+
21
+ - 判断某个版本的补装成功率是否下降(回归预警);
22
+ - 判断失败集中在哪一类原因(没装 node / npm 源不通 / 权限不足 / 平台不支持 / 安装超时);
23
+ - 判断「注册了但手机端看不到设备」的用户,是卡在补装、卡在 bridge 拉起,还是卡在中继注册。
24
+
25
+ 它**不**用于、也**不能**用于:识别具体用户、分析使用内容、投放或画像。
26
+
27
+ ## 2. 采集什么(全部字段,无其他)
28
+
29
+ 一次请求(批量)的 body:
30
+
31
+ ```json
32
+ {
33
+ "install_id": "3f2b1c8e-9a4d-4c1e-8b77-0d5f6a2e9c31",
34
+ "source": "plugin",
35
+ "events": [
36
+ { "name": "install_started", "at": 1789000000000, "version": "0.6.4-beta.4", "os": "darwin", "arch": "arm64", "node": "22" },
37
+ { "name": "install_failed", "fail_code": "npm_unreachable", "at": 1789000000000, "version": "0.6.4-beta.4", "os": "darwin", "arch": "arm64", "node": "22" }
38
+ ]
39
+ }
40
+ ```
41
+
42
+ | 字段 | 取值 | 说明 |
43
+ | --- | --- | --- |
44
+ | `install_id` | 本机随机 UUID | **本机生成**的随机 ID(`crypto.randomUUID()`),非硬件派生、不含机器信息;换机或删除后重装即变,**不可跨机器关联同一个人** |
45
+ | `source` | `plugin` | 目前只由插件(node 半)上报 |
46
+ | `name` | 事件名白名单 | 见下表 |
47
+ | `fail_code` | 失败码白名单 | 仅 `install_failed` / `update_failed` 附带 |
48
+ | `at` | 毫秒时间戳 | 事件发生时间 |
49
+ | `version` | 插件版本号 | 例如 `0.6.4-beta.4` |
50
+ | `os` | `process.platform` | 仅平台名:`darwin` / `linux` / `win32` |
51
+ | `arch` | `process.arch` | 仅架构:`arm64` / `x64` |
52
+ | `node` | 主版本号字符串 | 例如 `"22"`(**不含**次版本、补丁、路径) |
53
+
54
+ ### 事件名白名单(只发这些,其它一律不发)
55
+
56
+ `install_started`(开始补装运行环境)· `install_failed`(附 `fail_code`)· `runtime_ready`(运行环境就绪)·
57
+ `bridge_started`(bridge 进程拉起)· `bridge_registered`(设备已在中继注册成功 = 真正可用)·
58
+ `tunnel_disconnected` · `first_remote_ok`(首次远程打通)· `plugin_loaded` · `panel_opened` ·
59
+ `harness_restart`(自动重启触发)· `update_started` · `update_failed`(附 `fail_code`)
60
+
61
+ ### 失败码白名单(`fail_code`)
62
+
63
+ `node_missing` · `node_too_old` · `npm_unreachable` · `npm_eacces` · `platform_unsupported` ·
64
+ `runtime_install_timeout` · `launchd_failed` · `bridge_exit` · `bind_conflict` · `bind_device_limit` · `unknown`
65
+
66
+ > 原始错误文本**绝不外发**(它可能含文件路径、用户名、主机名):只做白名单归类,
67
+ > 归不进去的一律记 `unknown`。
68
+
69
+ ## 3. 不采集什么(硬边界)
70
+
71
+ 以下内容**不会**出现在遥测里,代码里也不存在对应字段(见 `telemetryEventOf()`):
72
+
73
+ - ❌ 手机号、邮箱、账号 ID、设备 `device_id`、访问密钥、任何登录凭据
74
+ - ❌ 任何会话内容与文件内容(对话、工具执行、审批、凭据、文件正文、WebSocket 消息)
75
+ - ❌ 真实 **hostname**、系统用户名、家目录、任何文件路径
76
+ - ❌ 密码、JWT、`bridge_secret`、E2EE 密钥或口令
77
+ - ❌ 设备指纹 `machine_fp`(同机识别用,**只**上报给账号 API 用于顶替旧设备,不进遥测)
78
+ - ❌ 原始 IP(服务端只看到 TCP 来源,客户端不上报 IP)、精确地理位置、GPS
79
+ - ❌ 用户行为轨迹、页面浏览路径、点击流、崩溃堆栈原文
80
+
81
+ 遥测请求**不带 `Authorization` 头**:它是一个与账号体系解耦的匿名通道,
82
+ 服务端也不接受用账号凭据关联这些事件。
83
+
84
+ ## 4. 如何关闭
85
+
86
+ ```bash
87
+ # 完全关闭:不生成 install_id、不落任何遥测文件、不发任何请求
88
+ export DSH_REMOTE_TELEMETRY=0
89
+ ```
90
+
91
+ - 判定规则:`DSH_REMOTE_TELEMETRY` 取值 `0` / `false` / `off` / `no` → 关闭;
92
+ 未设置或 `1` / `true` → 开启(**默认开启**)。判定在每次记录/发送时读取,改完重启 dsh web 生效。
93
+ - 诊断/测试隔离开关 `DSH_RELAY_SKIP_SERVICE=1` 同样会让遥测完全不发送
94
+ (它本来就是「不要碰外部世界」的隔离开关,本仓库测试脚本全局置位它)。
95
+ - 关闭后插件**连本机队列都不读**,`<relayDir>/.telemetry-*.json` 不会新增或发送。
96
+ - 也可以在**网络侧**屏蔽:中继/Nginx/防火墙里丢弃 `POST /api/telemetry/events`
97
+ (插件对任何失败都静默退避,不会影响你的正常使用与面板)。
98
+ - 已经产生的本机文件可以随时删除(它们只是待发队列):
99
+
100
+ ```bash
101
+ rm -f ~/.dsh-remote/.telemetry-install-id ~/.dsh-remote/.telemetry-queue.json ~/.dsh-remote/.telemetry-once.json
102
+ ```
103
+
104
+ ## 5. 本机都有哪些文件、怎么发
105
+
106
+ | 文件(`<relayDir>` 默认 `~/.dsh-remote`) | 权限 | 内容 |
107
+ | --- | --- | --- |
108
+ | `.telemetry-install-id` | `0600` | 一行随机 UUID(首次生成后复用) |
109
+ | `.telemetry-queue.json` | `0600` | 待发事件队列(上限 200 条,超出**丢最旧**) |
110
+ | `.telemetry-once.json` | `0600` | 一次性事件标记(如 `first_remote_ok` 只发一次) |
111
+
112
+ 发送行为:
113
+
114
+ - 端点:`POST <api_url>/api/telemetry/events`(`api_url` 取本机 `.dsh-config.json` 的 `api_url`,
115
+ 与安装上报同源;自建部署即你自己的服务地址);
116
+ - 请求头:`content-type: application/json`、`x-dsh-client: dsh-remote/<version>`,**无 Authorization**;
117
+ - 批量:单批 ≤ 20 条、body ≤ 32KB;队列 ≥ 5 条立即发送,否则每 60s 一次;
118
+ - 失败退避:30s → 2m → 10m → 1h,累计 6 次仍失败则丢弃该批(不永久堆积);
119
+ - 全程静默:任何异常都被吞掉,**不会**影响面板、bridge、连接流程或任何用户可见行为。
120
+
121
+ > 仓库测试/诊断用的加速开关 `DSH_REMOTE_TELEMETRY_MS` 会把上面的心跳与退避**按比例缩放**
122
+ > (默认不设置;生产环境请勿设置,生产值就是本文写明的这串)。
123
+
124
+ ## 6. 你可以自己核实
125
+
126
+ 1. 读代码:`packages/dsh-remote-web/lib/index.js` 搜 `匿名装机/连接遥测`;
127
+ 事件与失败码白名单是 `TELEMETRY_EVENT_NAMES` / `TELEMETRY_FAIL_CODES`,
128
+ payload 唯一构造点是 `telemetryEventOf()`——字段表就是上面第 2 节。
129
+ 2. 跑测试(仓库自带,含「禁止字段不得出现在 payload」的断言):
130
+
131
+ ```bash
132
+ DSH_RELAY_SKIP_SERVICE=1 node --test packages/dsh-remote-web/test/telemetry.test.mjs
133
+ ```
134
+
135
+ 3. 关掉开关后抓包/看日志验证:`DSH_REMOTE_TELEMETRY=0` 时不会有任何 `/api/telemetry/events` 请求,
136
+ 也不会有 `.telemetry-*` 文件生成。
137
+
138
+ ## 7. 服务端怎么处理(约定)
139
+
140
+ - 按 `install_id` + `name` 做**匿名漏斗**统计(装机成功率、失败原因分布、注册→可用转化);
141
+ - 不与会话/账号/设备表做关联分析;不长期保存原始 `at` 精度以外的东西;
142
+ - 未知事件名、未知 `fail_code` 一律丢弃(避免脏数据污染口径)。
143
+
144
+ > 本项目的许可(PolyForm Noncommercial)与隐私承诺都建立在「如实披露」之上:
145
+ > 如果这条通道将来要增加任何字段或事件,必须先改本文与 README,再改代码。
package/dsh-setup.mjs CHANGED
@@ -83,7 +83,7 @@ function ensureRuntimeCopy() {
83
83
  fs.cpSync(wsSrc, path.join(CONFIG_DIR, "node_modules", "ws"), { recursive: true, force: true });
84
84
  }
85
85
  fs.writeFileSync(verFile, ver);
86
- console.log(`✅ 运行时已固化到 ${CONFIG_DIR}(自启动指向稳定路径,不再依赖 npx 缓存)`);
86
+ console.log(`✅ 运行时: ${CONFIG_DIR}`);
87
87
  }
88
88
  try { ensureRuntimeCopy(); } catch (e) { console.warn(`⚠️ 运行时固化跳过: ${e.message}`); }
89
89
 
@@ -219,8 +219,7 @@ function writeAutostartFile() {
219
219
  <key>EnvironmentVariables</key><dict><key>PATH</key><string>/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin</string><key>DSH_BRIDGE_INSTALL_SOURCE</key><string>${INSTALL_SOURCE}</string><key>DSH_BRIDGE_INSTALL_VERSION</key><string>${INSTALL_VERSION}</string></dict>
220
220
  </dict></plist>`;
221
221
  fs.writeFileSync(plistPath, plist);
222
- console.log(`✅ 已创建自启动服务: ${plistPath}`);
223
- return plistPath;
222
+ return plistPath; // 路径在最终汇总里统一展示(避免同一信息打印两遍)
224
223
  }
225
224
  if (process.platform === "linux") {
226
225
  const dir = path.join(os.homedir(), ".config/systemd/user");
@@ -228,8 +227,7 @@ function writeAutostartFile() {
228
227
  const unit = `[Unit]\nDescription=dsh-remote bridge (auto-starts with dsh web)\n\n[Service]\nExecStart=${runCmd}\nRestart=on-failure\nRestartSec=5\nEnvironment=PATH=/usr/local/bin:/usr/bin:/bin\nEnvironment=DSH_BRIDGE_INSTALL_SOURCE=${INSTALL_SOURCE}\nEnvironment=DSH_BRIDGE_INSTALL_VERSION=${INSTALL_VERSION}\n\n[Install]\nWantedBy=default.target\n`;
229
228
  const unitPath = autostartFilePath();
230
229
  fs.writeFileSync(unitPath, unit);
231
- console.log(`✅ 已创建自启动服务: ${unitPath}`);
232
- return unitPath;
230
+ return unitPath; // 路径在最终汇总里统一展示
233
231
  }
234
232
  console.log("⚠️ 当前平台暂不支持自启动,请手动运行 `dsh-remote run`");
235
233
  return null;
@@ -238,8 +236,7 @@ function writeAutostartFile() {
238
236
  function restartBridgeService() {
239
237
  const svcFile = autostartFilePath();
240
238
  if (svcFile && !fs.existsSync(svcFile)) {
241
- console.log("[dsh-remote] 未检测到自启动服务,自动生成...");
242
- writeAutostartFile();
239
+ writeAutostartFile(); // 静默补生成(调用方会汇总展示路径)
243
240
  }
244
241
  if (process.platform === "darwin") {
245
242
  const plistPath = autostartFilePath();
@@ -279,14 +276,25 @@ function restartBridgeService() {
279
276
  return { ok: false, status: "unsupported", detail: `平台 ${process.platform} 不支持自启动` };
280
277
  }
281
278
 
279
+ /**
280
+ * dsh web 是否正在运行(127.0.0.1:3080)。
281
+ * bridge 本身依赖 dsh web 才工作:dsh web 没开时 bridge 起来也会立刻退出,
282
+ * 所以「装完当下 bridge 没在跑」是**正常状态**,不能当失败吓用户(见 install 汇总)。
283
+ */
284
+ async function isDshWebUp(timeoutMs = 1200) {
285
+ try {
286
+ const ac = new AbortController();
287
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
288
+ await fetch("http://127.0.0.1:3080/", { signal: ac.signal });
289
+ clearTimeout(timer);
290
+ return true;
291
+ } catch { return false; }
292
+ }
293
+
282
294
  function installAutostart() {
283
295
  const svcPath = writeAutostartFile();
284
296
  const r = restartBridgeService();
285
- if (r.ok) {
286
- console.log(`✅ 自启动服务已加载并运行${r.pid ? ` (pid=${r.pid})` : ""}`);
287
- } else {
288
- console.log(`⚠️ 自启动服务启动失败: ${r.detail || r.status}`);
289
- }
297
+ // 不再在这里直接打印(信息在 install 汇总里给一次),身份由调用方持有
290
298
  return { path: svcPath, status: r };
291
299
  }
292
300
 
@@ -458,20 +466,40 @@ async function setup(argv) {
458
466
  }
459
467
 
460
468
  const pub = await fetchPublicConfig(cfg.api_url || DEFAULT_API);
461
- console.log("\n══════════════════════════════════════");
462
- console.log("✅ 安装完成!");
469
+ const webUp = await isDshWebUp();
470
+ // 服务状态字符串是给插件面板解析的稳定契约(running / 未运行),不要改口径
471
+ const svcState = st.ok
472
+ ? `✅ 运行中${st.pid ? ` (pid=${st.pid})` : ""}`
473
+ : `未运行 (${st.detail || st.status})`;
474
+ const L = [];
475
+ L.push("✅ 安装完成");
476
+ if (selfHosted) {
477
+ L.push(` 服务器: ${cfg.tunnel_url}`);
478
+ L.push(` 手机端: 打开 ${cfg.tunnel_url.replace(/^ws/, "https")}/app/ ,用访问密钥登录`);
479
+ } else {
480
+ L.push(` 远程控制地址: ${pub.app_url || DEFAULT_APP_URL}`);
481
+ }
482
+ L.push(` 自启动服务: ${svc.path || "(当前平台不支持,可用 dsh-remote run 手动运行)"} — ${svcState}`);
483
+ // 唯一一处"下一步"引导(上面各阶段不再重复打印同样的话)
484
+ if (!st.ok && !webUp) {
485
+ // dsh web 没开时 bridge 起来即退,这是正常状态;要说清"什么时候会自己好",而不是甩一句"启动失败"
486
+ L.push("");
487
+ L.push("ℹ 检测到 dsh web 当前没有运行,所以 bridge 还没接上(正常,不是安装出错)。");
488
+ L.push(" 打开 dsh web 后 bridge 会自动启动,无需任何命令。");
489
+ L.push(" 如果 dsh web 已经开着但看不到本机,先重启 dsh web 让插件生效。");
490
+ } else if (!st.ok) {
491
+ L.push("");
492
+ L.push(`⚠️ bridge 未能启动: ${st.detail || st.status}`);
493
+ L.push(` 可查看日志: ${CONFIG_DIR}/.dsh-bridge.log`);
494
+ }
463
495
  if (selfHosted) {
464
- console.log(` 服务器地址: ${cfg.tunnel_url}`);
465
- console.log(` 手机端: 打开 ${cfg.tunnel_url.replace(/^ws/, "https")}/app/ ,用访问密钥登录即可。`);
496
+ L.push("");
497
+ L.push(" 下一步: 手机端用访问密钥登录即可(bridge 登录后自动启动)。");
466
498
  } else {
467
- console.log(` 远程控制地址: ${pub.app_url || DEFAULT_APP_URL}`);
468
- console.log(` 下一步(小白/无需任何命令): 打开 dsh web → 设置 → 「远程控制」→ 注册/登录手机号即可。`);
469
- console.log(` 若你是从 DeepSeek App/插件市场 安装:请【完全退出并重开 App】(或刷新 profile),插件即生效。`);
470
- console.log(` 登录后 bridge 会自动启动,手机端即可看到本机。`);
499
+ L.push("");
500
+ L.push(" 下一步: 打开 dsh web → 设置 → 「远程控制」→ 注册/登录手机号即可(无需任何命令)。");
471
501
  }
472
- if (svc.path) console.log(` 自启动服务: ${svc.path}`);
473
- console.log(` 服务状态: ${st.ok ? "✅ 运行中" + (st.pid ? ` (pid=${st.pid})` : "") : "❌ 未运行(" + (st.detail || st.status) + ")"}`);
474
- console.log("══════════════════════════════════════");
502
+ console.log("\n" + L.join("\n") + "\n");
475
503
  }
476
504
 
477
505
  // ---------- plugin:安装/卸载 dsh web 远程控制插件 ----------
@@ -688,18 +716,13 @@ function convergePluginActivation(profileDir, pkgFile, patchFile, pluginDir, pat
688
716
  console.error(`❌ 插件入口缺失:${entryFile}(本包不完整?请用官方源重装:npx --registry=https://registry.npmjs.org @mrrisega/dsh-remote@latest)`);
689
717
  process.exit(1);
690
718
  }
691
- console.log(`✅ 插件已拷贝到 ${pluginLocalDir}`);
692
719
  declarePluginDep(pkgFile); // file: 依赖(包管理器 install 不误删)
693
720
  const addedBundle = ensureBundleEntry(pkgFile); // bundles 声明 → 插件自带 patch 自动激活
694
- const linked = ensurePluginLinked(profileDir, pluginLocalDir); // 自建 node_modules 链接
721
+ ensurePluginLinked(profileDir, pluginLocalDir); // 自建 node_modules 链接
695
722
  stripInclude("bundle patch 已是唯一激活点"); // 清历史 include
696
- console.log(addedBundle
697
- ? `✅ 已加入 dsh.profile.bundles(${PLUGIN_ID} 自带 patch 自动生效)`
698
- : `ℹ dsh.profile.bundles 已含 ${PLUGIN_ID}`);
699
- console.log(linked
700
- ? `✅ 已建立 node_modules/${PLUGIN_ID} → ${pluginLocalDir}(免包管理器即可解析)`
701
- : `✅ node_modules/${PLUGIN_ID} 已就绪`);
702
- console.log(`✅ 插件安装完成(bundle 形态,无用户 include)。配置目录: ${CONFIG_DIR}`);
723
+ // 只报结果不报过程:bundle 是否新加、node_modules 是否新建链接都属于实现细节
724
+ // (失败会各自抛错/提示),用户只需要知道"插件装好了、装在哪"。
725
+ console.log(`✅ 插件已就绪: ${pluginLocalDir}${addedBundle ? "" : "(此前已激活)"}`);
703
726
  return;
704
727
  }
705
728
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrrisega/dsh-remote",
3
- "version": "0.6.4-beta.3",
3
+ "version": "0.6.4-beta.4",
4
4
  "description": "手机远程控制 DeepSeek Harness · Remote control DeepSeek Harness (dsh web) from any phone browser — 100% 全功能 App 级体验:发消息、看工具执行、审批权限、改设置、管凭据,含特权操作,免内网穿透。一条命令安装 npx @mrrisega/dsh-remote。Mobile remote control for DSH, self-host or SaaS, no server needed on LAN.",
5
5
  "keywords": [
6
6
  "deepseek-harness",
@@ -38,7 +38,8 @@
38
38
  "dsh-setup.mjs",
39
39
  "clients/dsh-remote",
40
40
  "packages/dsh-remote-web",
41
- "docs/self-hosting.md"
41
+ "docs/self-hosting.md",
42
+ "docs/telemetry.md"
42
43
  ],
43
44
  "workspaces": [
44
45
  "packages/*",
@@ -51,7 +52,7 @@
51
52
  "test": "npm run test:router && npm run test:plugin && npm run test:bridge",
52
53
  "test:router": "node --test packages/relay-router/test/native-device-selection.test.mjs packages/relay-router/test/quotas.test.mjs packages/relay-router/test/presence-report.test.mjs",
53
54
  "_comment:test:plugin": "DSH_RELAY_SKIP_SERVICE=1 = 测试隔离:插件加载时不做真实系统操作(npx 补装 / launchctl / 重启 harness);需要真实分支的用例自行 unset(见 account-switch / runtime-provision / harness-restart 用例注释)",
54
- "test:plugin": "DSH_RELAY_SKIP_SERVICE=1 node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
55
+ "test:plugin": "DSH_REMOTE_TELEMETRY=0 DSH_RELAY_SKIP_SERVICE=1 node --test --test-concurrency=1 packages/dsh-remote-web/test/*.test.mjs",
55
56
  "test:bridge": "node --test clients/dsh-remote/test/*.test.mjs",
56
57
  "check": "node --check clients/dsh-remote/dsh-bridge.mjs && node --check clients/dsh-remote/e2ee-shim.mjs && node --check dsh-setup.mjs && node --check packages/dsh-remote-web/lib/index.js && node --check packages/dsh-remote-web/lib/client.js && bash -n deploy/install-open.sh && npm run check:alias",
57
58
  "sync:alias": "node scripts/sync-legacy-alias.mjs",
@@ -1131,6 +1131,8 @@ window.__ModuleLoader__.load({
1131
1131
  }
1132
1132
 
1133
1133
  // ── 面板主体(渲染于设置页 settings.section 栏目内) ─────────────────────
1134
+ // 匿名装机统计的「面板打开」只上报一次/每页(node 半还会每进程去重)。
1135
+ var panelOpenedSent = false;
1134
1136
  function RemoteControlSection(props) {
1135
1137
 
1136
1138
  var statusArr = useState(null); var st = statusArr[0]; var setSt = statusArr[1];
@@ -1557,6 +1559,16 @@ window.__ModuleLoader__.load({
1557
1559
  };
1558
1560
  }, [view, mode, loggedIn, phoneKey, serviceRunning]);
1559
1561
 
1562
+ // ── 匿名装机统计(面板打开) ──────────────────────────────────────────────
1563
+ // 只上报「面板被打开过」这一个事件名(node 半每进程去重、DSH_REMOTE_TELEMETRY=0 直接丢弃);
1564
+ // 不含任何账号、手机号、设备指纹、会话内容或文件内容——隐私边界见 docs/telemetry.md。
1565
+ useEffect(function () {
1566
+ if (panelOpenedSent) return undefined;
1567
+ panelOpenedSent = true;
1568
+ post("/dsh-remote/telemetry/panel-opened", {}).catch(function () { /* 静默:统计失败绝不影响面板 */ });
1569
+ return undefined;
1570
+ }, []);
1571
+
1560
1572
  // ── 登录后自动闭环(0.6.4):连接阶段短轮询 ──────────────────────────────
1561
1573
  // 登录成功(含从别处已登录、被面板读到)后立刻开始推进连接,之后自调度:
1562
1574
  // 非 online → 2.5s 一次(阶段自动前进,用户零操作);online → 15s 一次(退避,只做保活/新设备发现)。
@@ -2410,6 +2422,26 @@ window.__ModuleLoader__.load({
2410
2422
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🛠 电脑端一键安装:bridge 与「远程访问」面板一次到位——云端/自建切换、账号登录、bridge 启停、一次性扫码访问、已授权设备管理、意见反馈都在这里。"),
2411
2423
  h("div", { className: "dru-hint", style: { marginBottom: 6 } }, "🔒 安全与通道:HTTP / WebSocket 全量透传,一次性访问密钥认证,面板实时显示设备与已授权设备列表;服务端可配置流量配额。"),
2412
2424
  h("div", { className: "dru-hint" }, "🛡 端到端加密:手机↔电脑之间的消息内容用「你的账号密码派生密钥」端到端加密——密钥与密码不落服务端(仅存校验值),中继只可见路径/大小/时间(详见 README「安全与隐私」)。"),
2425
+ // 匿名装机统计(隐私披露):开源项目必须把「采什么 / 不采什么 / 怎么关」写在用户看得到的地方。
2426
+ h("div", { className: "dru-hint", style: { marginTop: 6 } },
2427
+ "📊 匿名装机统计:只上报「装机/连接是否成功」这类事件(安装开始与失败原因、bridge 是否注册成功、是否首次远程打通)——"
2428
+ + "不含任何账号、手机号、会话或文件内容、主机名、路径、设备指纹与 IP;标识是本机随机 ID(重装即变)。"
2429
+ + "用环境变量 DSH_REMOTE_TELEMETRY=0 可完全关闭(不生成 ID、不发任何请求)。"),
2430
+ h("div", { className: "dru-hint", style: { marginTop: 4 } },
2431
+ "隐私说明:",
2432
+ h("a", {
2433
+ className: "dru-linkbtn",
2434
+ href: "https://github.com/mrRisega/dsh-remote/blob/main/README.md#匿名装机统计与隐私",
2435
+ target: "_blank",
2436
+ rel: "noreferrer"
2437
+ }, "README「匿名装机统计与隐私」"),
2438
+ " · ",
2439
+ h("a", {
2440
+ className: "dru-linkbtn",
2441
+ href: "https://github.com/mrRisega/dsh-remote/blob/main/docs/telemetry.md",
2442
+ target: "_blank",
2443
+ rel: "noreferrer"
2444
+ }, "完整字段清单 docs/telemetry.md")),
2413
2445
  // 加入交流群(后台上传二维码后出现;点击弹出二维码大图便于扫码)
2414
2446
  community && community.qrcode
2415
2447
  ? h("div", { className: "dru-actions", style: { marginTop: 10 } },