@mrrisega/dsh-remote 0.7.9 → 0.7.10-beta.3
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 +9 -2
- package/clients/dsh-remote/dsh-bridge.mjs +88 -1
- package/clients/dsh-remote/mobile-adapter.mjs +775 -11
- package/clients/dsh-remote/push-reporter.mjs +479 -0
- package/clients/dsh-remote/test/bridge-e2ee-reject-observable.test.mjs +90 -0
- package/clients/dsh-remote/test/mobile-adapter-guards.test.mjs +172 -0
- package/clients/dsh-remote/test/mobile-adapter-runtime.test.mjs +188 -4
- package/clients/dsh-remote/test/push-reporter.test.mjs +380 -0
- package/dsh-setup.mjs +5 -0
- package/package.json +1 -1
- package/packages/dsh-remote-web/lib/client.js +350 -8
- package/packages/dsh-remote-web/lib/index.js +26 -0
- package/packages/dsh-remote-web/package.json +1 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +43 -1
- package/packages/dsh-remote-web/runtime/clients/dsh-remote/mobile-adapter.mjs +775 -11
- package/packages/dsh-remote-web/runtime/dsh-setup.mjs +5 -0
- package/packages/dsh-remote-web/test/activation-entry.test.mjs +407 -0
- package/packages/dsh-remote-web/test/e2ee-reject-diag.test.mjs +70 -0
- package/packages/dsh-remote-web/test/setup-output.test.mjs +16 -0
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
createPushReporter,
|
|
6
|
+
toNotifyPayload,
|
|
7
|
+
NOTIFY_KINDS,
|
|
8
|
+
MAX_QUEUE,
|
|
9
|
+
MAX_BODY_LEN
|
|
10
|
+
} from "../push-reporter.mjs";
|
|
11
|
+
import { NODE_KINDS } from "../dsh-events.mjs";
|
|
12
|
+
|
|
13
|
+
/** 构造一个审批节点(与 dsh-events classifyWaterfall 的形状一致)。 */
|
|
14
|
+
function approvalNode(over = {}) {
|
|
15
|
+
return {
|
|
16
|
+
kind: NODE_KINDS.APPROVAL_REQUEST,
|
|
17
|
+
specNode: 1,
|
|
18
|
+
answerable: true,
|
|
19
|
+
sessionId: "s_1",
|
|
20
|
+
agentId: "a_1",
|
|
21
|
+
eventId: "evt_approval_1",
|
|
22
|
+
toolName: "Bash",
|
|
23
|
+
reason: "npm run deploy",
|
|
24
|
+
options: [
|
|
25
|
+
{ value: "allowed-once", label: "允许一次" },
|
|
26
|
+
{ value: "rejected", label: "拒绝" }
|
|
27
|
+
],
|
|
28
|
+
at: 1_700_000_000_000,
|
|
29
|
+
...over
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function turnEndNode(over = {}) {
|
|
34
|
+
return {
|
|
35
|
+
kind: NODE_KINDS.TURN_END,
|
|
36
|
+
specNode: 4,
|
|
37
|
+
sessionId: "s_1",
|
|
38
|
+
turn: 3,
|
|
39
|
+
reason: "completed",
|
|
40
|
+
known: true,
|
|
41
|
+
seq: 42,
|
|
42
|
+
at: 1_700_000_000_000,
|
|
43
|
+
...over
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 极简 fetch 假实现:按脚本返回,记录每次调用。 */
|
|
48
|
+
function makeFetch(script) {
|
|
49
|
+
const calls = [];
|
|
50
|
+
const fn = async (url, init) => {
|
|
51
|
+
calls.push({ url, init });
|
|
52
|
+
const step = script.shift();
|
|
53
|
+
if (!step) throw new Error("fetch script exhausted");
|
|
54
|
+
if (step instanceof Error) throw step;
|
|
55
|
+
const status = step.status ?? 200;
|
|
56
|
+
const body = step.body ?? { ok: true };
|
|
57
|
+
return {
|
|
58
|
+
status,
|
|
59
|
+
ok: status >= 200 && status < 300,
|
|
60
|
+
async json() { return body; }
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
fn.calls = calls;
|
|
64
|
+
return fn;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ───────────────────────── 映射(事件 → 上报体) ─────────────────────────
|
|
68
|
+
|
|
69
|
+
test("① 审批节点 → approval_request,event_id 稳定、带选项、正文不含完整命令(默认隐私模式)", () => {
|
|
70
|
+
const p = toNotifyPayload(approvalNode(), { deviceId: "dev-x", detail: false });
|
|
71
|
+
assert.equal(p.kind, NOTIFY_KINDS.APPROVAL_REQUEST);
|
|
72
|
+
assert.match(p.event_id, /^dev-x:approval:/);
|
|
73
|
+
assert.equal(p.approval.request_id, "evt_approval_1");
|
|
74
|
+
assert.equal(p.approval.options.length, 2);
|
|
75
|
+
assert.equal(p.session.id, "s_1");
|
|
76
|
+
// 默认模式:summary 只留动作名(工具名),绝不外发 "npm run deploy" 这种完整命令
|
|
77
|
+
assert.equal(p.approval.summary, "Bash");
|
|
78
|
+
assert.ok(!JSON.stringify(p).includes("npm run deploy"), "默认模式不得把完整命令外发");
|
|
79
|
+
assert.ok(p.body.length <= MAX_BODY_LEN);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("② detail 模式才带上完整命令摘要(服务端规则引擎据此判风险)", () => {
|
|
83
|
+
const p = toNotifyPayload(approvalNode(), { deviceId: "dev-x", detail: true });
|
|
84
|
+
assert.equal(p.approval.summary, "npm run deploy");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("③ 会话标题进 session.title(超长截断),正文只写一句人话", () => {
|
|
88
|
+
const p = toNotifyPayload(approvalNode(), {
|
|
89
|
+
deviceId: "dev-x",
|
|
90
|
+
sessionTitle: () => "重构登录模块".repeat(20)
|
|
91
|
+
});
|
|
92
|
+
assert.ok(p.session.title.length <= 60);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("④ turn/end 各 reason 映射:completed→turn_done、error→turn_error、blocked→turn_blocked、未知原因如实写", () => {
|
|
96
|
+
const done = toNotifyPayload(turnEndNode(), { deviceId: "dev-x", sessionTitle: () => "重构登录模块" });
|
|
97
|
+
assert.equal(done.kind, NOTIFY_KINDS.TURN_DONE);
|
|
98
|
+
assert.match(done.body, /重构登录模块/);
|
|
99
|
+
|
|
100
|
+
const err = toNotifyPayload(turnEndNode({ reason: "error", known: true }), { deviceId: "dev-x" });
|
|
101
|
+
assert.equal(err.kind, NOTIFY_KINDS.TURN_ERROR);
|
|
102
|
+
|
|
103
|
+
const blocked = toNotifyPayload(turnEndNode({ reason: "blocked", known: true }), { deviceId: "dev-x" });
|
|
104
|
+
assert.equal(blocked.kind, NOTIFY_KINDS.TURN_BLOCKED);
|
|
105
|
+
|
|
106
|
+
const weird = toNotifyPayload(turnEndNode({ reason: "some-new-reason", known: false }), { deviceId: "dev-x" });
|
|
107
|
+
assert.equal(weird.kind, NOTIFY_KINDS.TURN_DONE);
|
|
108
|
+
assert.match(weird.body, /some-new-reason/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("⑤ gap/fault/简报等节点不上报(返回 null),缺 deviceId 也不上报", () => {
|
|
112
|
+
assert.equal(toNotifyPayload({ kind: NODE_KINDS.GAP, at: 1 }, { deviceId: "dev-x" }), null);
|
|
113
|
+
assert.equal(toNotifyPayload({ kind: NODE_KINDS.FAULT, at: 1 }, { deviceId: "dev-x" }), null);
|
|
114
|
+
assert.equal(toNotifyPayload({ kind: NODE_KINDS.DIGEST_DUE, at: 1 }, { deviceId: "dev-x" }), null);
|
|
115
|
+
assert.equal(toNotifyPayload(approvalNode(), { deviceId: "" }), null);
|
|
116
|
+
assert.equal(toNotifyPayload(null, { deviceId: "dev-x" }), null);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("⑥ 审批节点没有 eventId(无法回执)时不上报", () => {
|
|
120
|
+
assert.equal(toNotifyPayload(approvalNode({ eventId: "" }), { deviceId: "dev-x" }), null);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ───────────────────────── 队列与投递 ─────────────────────────
|
|
124
|
+
|
|
125
|
+
test("⑦ 同一 event_id 重复入队只算一条(幂等第一道)", async () => {
|
|
126
|
+
const fetchImpl = makeFetch([{ status: 200 }]);
|
|
127
|
+
const r = createPushReporter({
|
|
128
|
+
apiBase: "https://api.example.com",
|
|
129
|
+
deviceId: "dev-x",
|
|
130
|
+
tokenProvider: () => "tok",
|
|
131
|
+
upstream: "http://127.0.0.1:3080",
|
|
132
|
+
fetchImpl
|
|
133
|
+
});
|
|
134
|
+
const p = toNotifyPayload(turnEndNode(), { deviceId: "dev-x" });
|
|
135
|
+
assert.equal(r._enqueue(p), true);
|
|
136
|
+
assert.equal(r._enqueue({ ...p }), false);
|
|
137
|
+
assert.equal(r.status().duplicates, 1);
|
|
138
|
+
await r._flush();
|
|
139
|
+
assert.equal(r.status().sent, 1);
|
|
140
|
+
await r.stop();
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("⑧ 队列有界:超过上限丢最旧并计数(不让内存无界增长)", async () => {
|
|
144
|
+
const fetchImpl = makeFetch([]);
|
|
145
|
+
const r = createPushReporter({
|
|
146
|
+
apiBase: "https://api.example.com",
|
|
147
|
+
deviceId: "dev-x",
|
|
148
|
+
tokenProvider: () => "tok",
|
|
149
|
+
upstream: "http://127.0.0.1:3080",
|
|
150
|
+
fetchImpl,
|
|
151
|
+
maxQueue: 3
|
|
152
|
+
});
|
|
153
|
+
for (let i = 0; i < 5; i++) {
|
|
154
|
+
r._enqueue({
|
|
155
|
+
event_id: `dev-x:turn:${i}`,
|
|
156
|
+
device_id: "dev-x",
|
|
157
|
+
kind: NOTIFY_KINDS.TURN_DONE,
|
|
158
|
+
at: 1,
|
|
159
|
+
session: { id: "s" },
|
|
160
|
+
title: "任务完成",
|
|
161
|
+
body: "x"
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
assert.equal(r.status().queued, 3);
|
|
165
|
+
assert.equal(r.status().dropped, 2);
|
|
166
|
+
await r.stop();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("⑨ 投递成功:带 Bearer、POST 到 /api/notify/event、JSON body", async () => {
|
|
170
|
+
const fetchImpl = makeFetch([{ status: 200, body: { ok: true, notified: 1 } }]);
|
|
171
|
+
const r = createPushReporter({
|
|
172
|
+
apiBase: "https://api.example.com/",
|
|
173
|
+
deviceId: "dev-x",
|
|
174
|
+
tokenProvider: () => "tok-1",
|
|
175
|
+
upstream: "http://127.0.0.1:3080",
|
|
176
|
+
fetchImpl
|
|
177
|
+
});
|
|
178
|
+
r._enqueue(toNotifyPayload(turnEndNode(), { deviceId: "dev-x" }));
|
|
179
|
+
await r._flush();
|
|
180
|
+
const call = fetchImpl.calls[0];
|
|
181
|
+
assert.equal(call.url, "https://api.example.com/api/notify/event");
|
|
182
|
+
assert.equal(call.init.method, "POST");
|
|
183
|
+
assert.equal(call.init.headers.authorization, "Bearer tok-1");
|
|
184
|
+
assert.equal(JSON.parse(call.init.body).device_id, "dev-x");
|
|
185
|
+
assert.equal(r.status().sent, 1);
|
|
186
|
+
await r.stop();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("⑩ 401:丢弃本批(等下次登录刷新),不无限重试", async () => {
|
|
190
|
+
const fetchImpl = makeFetch([{ status: 401 }]);
|
|
191
|
+
const r = createPushReporter({
|
|
192
|
+
apiBase: "https://api.example.com",
|
|
193
|
+
deviceId: "dev-x",
|
|
194
|
+
tokenProvider: () => "expired",
|
|
195
|
+
upstream: "http://127.0.0.1:3080",
|
|
196
|
+
fetchImpl
|
|
197
|
+
});
|
|
198
|
+
r._enqueue(toNotifyPayload(turnEndNode(), { deviceId: "dev-x" }));
|
|
199
|
+
await r._flush();
|
|
200
|
+
assert.equal(r.status().queued, 0, "401 后不应把条目留在队列里反复打");
|
|
201
|
+
assert.equal(r.status().lastError, "auth_failed");
|
|
202
|
+
await r.stop();
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test("⑪ 400 契约错误:丢弃该条(重试没意义)", async () => {
|
|
206
|
+
const fetchImpl = makeFetch([{ status: 400 }]);
|
|
207
|
+
const r = createPushReporter({
|
|
208
|
+
apiBase: "https://api.example.com",
|
|
209
|
+
deviceId: "dev-x",
|
|
210
|
+
tokenProvider: () => "tok",
|
|
211
|
+
upstream: "http://127.0.0.1:3080",
|
|
212
|
+
fetchImpl
|
|
213
|
+
});
|
|
214
|
+
r._enqueue(toNotifyPayload(turnEndNode(), { deviceId: "dev-x" }));
|
|
215
|
+
await r._flush();
|
|
216
|
+
assert.equal(r.status().queued, 0);
|
|
217
|
+
assert.match(r.status().lastError, /http_400/);
|
|
218
|
+
await r.stop();
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
test("⑫ 5xx:保留条目等待退避重试(不丢事件)", async () => {
|
|
222
|
+
const fetchImpl = makeFetch([{ status: 503 }]);
|
|
223
|
+
const r = createPushReporter({
|
|
224
|
+
apiBase: "https://api.example.com",
|
|
225
|
+
deviceId: "dev-x",
|
|
226
|
+
tokenProvider: () => "tok",
|
|
227
|
+
upstream: "http://127.0.0.1:3080",
|
|
228
|
+
fetchImpl,
|
|
229
|
+
retryDelaysMs: [1, 1, 1]
|
|
230
|
+
});
|
|
231
|
+
r._enqueue(toNotifyPayload(turnEndNode(), { deviceId: "dev-x" }));
|
|
232
|
+
await r._flush();
|
|
233
|
+
assert.equal(r.status().queued, 1, "5xx 应保留重试");
|
|
234
|
+
await r.stop();
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("⑬ 没有 token:不发请求、条目留在队列", async () => {
|
|
238
|
+
const fetchImpl = makeFetch([]);
|
|
239
|
+
const r = createPushReporter({
|
|
240
|
+
apiBase: "https://api.example.com",
|
|
241
|
+
deviceId: "dev-x",
|
|
242
|
+
tokenProvider: () => "",
|
|
243
|
+
upstream: "http://127.0.0.1:3080",
|
|
244
|
+
fetchImpl
|
|
245
|
+
});
|
|
246
|
+
r._enqueue(toNotifyPayload(turnEndNode(), { deviceId: "dev-x" }));
|
|
247
|
+
await r._flush();
|
|
248
|
+
assert.equal(fetchImpl.calls.length, 0);
|
|
249
|
+
assert.equal(r.status().queued, 1);
|
|
250
|
+
assert.equal(r.status().lastError, "no_token");
|
|
251
|
+
await r.stop();
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// ───────────────────────── 启动门禁 ─────────────────────────
|
|
255
|
+
|
|
256
|
+
test("⑭ start():服务端明确关闭推送时**不启动**(默认关)", async () => {
|
|
257
|
+
const fetchImpl = makeFetch([{ status: 200, body: { ok: true, notify: { enabled: false } } }]);
|
|
258
|
+
const r = createPushReporter({
|
|
259
|
+
apiBase: "https://api.example.com",
|
|
260
|
+
deviceId: "dev-x",
|
|
261
|
+
tokenProvider: () => "tok",
|
|
262
|
+
upstream: "http://127.0.0.1:3080",
|
|
263
|
+
fetchImpl
|
|
264
|
+
});
|
|
265
|
+
const st = await r.start();
|
|
266
|
+
assert.equal(st.started, false);
|
|
267
|
+
assert.equal(st.reason, "disabled");
|
|
268
|
+
await r.stop();
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("⑮ start():账号没有推送订阅时不启动(没订阅推了也没人收)", async () => {
|
|
272
|
+
const fetchImpl = makeFetch([
|
|
273
|
+
{ status: 200, body: { ok: true, notify: { enabled: true } } },
|
|
274
|
+
{ status: 200, body: { ok: true, enabled: true, subscribed: false } }
|
|
275
|
+
]);
|
|
276
|
+
const r = createPushReporter({
|
|
277
|
+
apiBase: "https://api.example.com",
|
|
278
|
+
deviceId: "dev-x",
|
|
279
|
+
tokenProvider: () => "tok",
|
|
280
|
+
upstream: "http://127.0.0.1:3080",
|
|
281
|
+
fetchImpl
|
|
282
|
+
});
|
|
283
|
+
const st = await r.start();
|
|
284
|
+
assert.equal(st.started, false);
|
|
285
|
+
assert.equal(st.reason, "no_subscription");
|
|
286
|
+
await r.stop();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test("⑯ start():没有 token 时不启动、不抛异常", async () => {
|
|
290
|
+
const fetchImpl = makeFetch([]);
|
|
291
|
+
const r = createPushReporter({
|
|
292
|
+
apiBase: "https://api.example.com",
|
|
293
|
+
deviceId: "dev-x",
|
|
294
|
+
tokenProvider: () => "",
|
|
295
|
+
upstream: "http://127.0.0.1:3080",
|
|
296
|
+
fetchImpl
|
|
297
|
+
});
|
|
298
|
+
const st = await r.start();
|
|
299
|
+
assert.equal(st.started, false);
|
|
300
|
+
assert.equal(st.reason, "no_token");
|
|
301
|
+
assert.equal(fetchImpl.calls.length, 0);
|
|
302
|
+
await r.stop();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
test("⑰ 探测失败(网络异常)不抛、不启动(可选增强绝不能拖垮主链路)", async () => {
|
|
306
|
+
const fetchImpl = makeFetch([new Error("ECONNREFUSED")]);
|
|
307
|
+
const r = createPushReporter({
|
|
308
|
+
apiBase: "https://api.example.com",
|
|
309
|
+
deviceId: "dev-x",
|
|
310
|
+
tokenProvider: () => "tok",
|
|
311
|
+
upstream: "http://127.0.0.1:3080",
|
|
312
|
+
fetchImpl
|
|
313
|
+
});
|
|
314
|
+
const st = await r.start();
|
|
315
|
+
assert.equal(st.started, false);
|
|
316
|
+
assert.equal(st.reason, "ECONNREFUSED");
|
|
317
|
+
await r.stop();
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("⑱ 事件归一化抛错也不影响(畸形节点被忽略,不冒泡)", async () => {
|
|
321
|
+
const r = createPushReporter({
|
|
322
|
+
apiBase: "https://api.example.com",
|
|
323
|
+
deviceId: "dev-x",
|
|
324
|
+
tokenProvider: () => "tok",
|
|
325
|
+
upstream: "http://127.0.0.1:3080",
|
|
326
|
+
fetchImpl: makeFetch([])
|
|
327
|
+
});
|
|
328
|
+
// 直接喂畸形对象给映射函数:必须返回 null 而不是抛
|
|
329
|
+
assert.equal(toNotifyPayload({ kind: NODE_KINDS.APPROVAL_REQUEST, eventId: null, at: "x" }, { deviceId: "dev-x" }), null);
|
|
330
|
+
await r.stop();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("⑲ 正文长度硬上限:超长标题/正文被截断", () => {
|
|
334
|
+
const p = toNotifyPayload(turnEndNode(), {
|
|
335
|
+
deviceId: "dev-x",
|
|
336
|
+
sessionTitle: () => "很长的标题".repeat(100)
|
|
337
|
+
});
|
|
338
|
+
assert.ok(p.body.length <= MAX_BODY_LEN);
|
|
339
|
+
assert.ok(p.session.title.length <= 60);
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
test("⑳ MAX_QUEUE 常量与文档一致(50)", () => {
|
|
343
|
+
assert.equal(MAX_QUEUE, 50);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
test("㉑ event_id 必须只含 [A-Za-z0-9._:-](服务端契约,问号/空格会被 400)", () => {
|
|
347
|
+
const p = toNotifyPayload(
|
|
348
|
+
{ kind: NODE_KINDS.TURN_END, sessionId: "s/1", turn: undefined, seq: undefined, reason: "completed", at: 1 },
|
|
349
|
+
{ deviceId: "dev-x" }
|
|
350
|
+
);
|
|
351
|
+
assert.ok(p, "应生成上报体");
|
|
352
|
+
assert.match(p.event_id, /^[A-Za-z0-9._:-]+$/, `event_id 含非法字符:${p.event_id}`);
|
|
353
|
+
// 会话 id 里有斜杠、turn/seq 缺失产生问号 —— 都必须被归一化成安全字符
|
|
354
|
+
const p2 = toNotifyPayload(
|
|
355
|
+
{ kind: NODE_KINDS.APPROVAL_REQUEST, sessionId: "s/1", eventId: "evt 1?x", at: 1, toolName: "Bash" },
|
|
356
|
+
{ deviceId: "dev-x" }
|
|
357
|
+
);
|
|
358
|
+
assert.match(p2.event_id, /^[A-Za-z0-9._:-]+$/, `event_id 含非法字符:${p2.event_id}`);
|
|
359
|
+
assert.ok(p2.event_id.length <= 128);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test("㉒ start():服务端开启且已订阅时才会继续(门禁用 /api/me.notify.enabled)", async () => {
|
|
363
|
+
const fetchImpl = makeFetch([
|
|
364
|
+
{ status: 200, body: { ok: true, notify: { enabled: true } } },
|
|
365
|
+
{ status: 200, body: { ok: true, enabled: true, subscribed: true } }
|
|
366
|
+
]);
|
|
367
|
+
const r = createPushReporter({
|
|
368
|
+
apiBase: "https://api.example.com",
|
|
369
|
+
deviceId: "dev-x",
|
|
370
|
+
tokenProvider: () => "tok",
|
|
371
|
+
upstream: "http://127.0.0.1:3080",
|
|
372
|
+
fetchImpl
|
|
373
|
+
});
|
|
374
|
+
const st = await r.start();
|
|
375
|
+
// 订阅器在测试环境没有真实 WS,启动可能失败;关键断言是"门禁通过了"(reason 不再是 disabled/no_subscription)
|
|
376
|
+
assert.notEqual(st.reason, "disabled");
|
|
377
|
+
assert.notEqual(st.reason, "no_subscription");
|
|
378
|
+
assert.equal(fetchImpl.calls[0].url, "https://api.example.com/api/me");
|
|
379
|
+
await r.stop();
|
|
380
|
+
});
|
package/dsh-setup.mjs
CHANGED
|
@@ -2408,6 +2408,11 @@ async function setup(argv) {
|
|
|
2408
2408
|
L.push(` 手机端: 打开 ${cfg.tunnel_url.replace(/^ws/, "https")}/app/ ,用访问密钥登录`);
|
|
2409
2409
|
} else {
|
|
2410
2410
|
L.push(` 远程访问地址: ${pub.app_url || DEFAULT_APP_URL}`);
|
|
2411
|
+
// 【0.7.10 / W1-T1】安装引导原来停在「注册/登录手机号」—— 用户以为到此结束,
|
|
2412
|
+
// 于是设备在线了、人却从没去过手机端(生产复盘:129 人 C1)。这里把**手机端下一步**一次说全,
|
|
2413
|
+
// 两条路都写清楚(扫码免登录 / 直接打开地址用同一手机号登录),任一都能到同一台电脑。
|
|
2414
|
+
L.push(" 手机端下一步: 用手机浏览器打开上面这个地址 → 用同一手机号登录 → 在「设备列表」里选这台电脑");
|
|
2415
|
+
L.push(" (或在电脑端登录后,回到 设置 → 「远程访问」,用手机扫那里的二维码:免登录直接进入,30 分钟有效)");
|
|
2411
2416
|
}
|
|
2412
2417
|
L.push(svcSkipped
|
|
2413
2418
|
? ` 自启动服务: ${svcState},可用 \`dsh-remote run\` 手动运行 bridge`
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrrisega/dsh-remote",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.10-beta.3",
|
|
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",
|