@mrrisega/dsh-remote 0.7.10-beta.3 → 0.7.11-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.
Files changed (68) hide show
  1. package/clients/dsh-remote/agent/README.md +412 -0
  2. package/clients/dsh-remote/agent/agent-e2e-codex.mjs +185 -0
  3. package/clients/dsh-remote/agent/agent-e2e-real.mjs +443 -0
  4. package/clients/dsh-remote/agent/codex.mjs +736 -0
  5. package/clients/dsh-remote/agent/detect.mjs +399 -0
  6. package/clients/dsh-remote/agent/events.mjs +511 -0
  7. package/clients/dsh-remote/agent/gemini.mjs +503 -0
  8. package/clients/dsh-remote/agent/index.mjs +339 -0
  9. package/clients/dsh-remote/agent/opencode.mjs +628 -0
  10. package/clients/dsh-remote/agent/permission-bridge.mjs +534 -0
  11. package/clients/dsh-remote/agent/proc.mjs +179 -0
  12. package/clients/dsh-remote/agent/version.mjs +148 -0
  13. package/clients/dsh-remote/approval-coordinator.mjs +150 -0
  14. package/clients/dsh-remote/decision-poller.mjs +235 -0
  15. package/clients/dsh-remote/dsh-bridge.mjs +214 -1
  16. package/clients/dsh-remote/push-reporter.mjs +54 -0
  17. package/clients/dsh-remote/test/agent-codex.test.mjs +535 -0
  18. package/clients/dsh-remote/test/agent-detect.test.mjs +390 -0
  19. package/clients/dsh-remote/test/agent-events.test.mjs +340 -0
  20. package/clients/dsh-remote/test/agent-gemini.test.mjs +355 -0
  21. package/clients/dsh-remote/test/agent-jsonl-driver.test.mjs +165 -0
  22. package/clients/dsh-remote/test/agent-opencode.test.mjs +369 -0
  23. package/clients/dsh-remote/test/agent-permission-bridge.test.mjs +340 -0
  24. package/clients/dsh-remote/test/agent-proc.test.mjs +175 -0
  25. package/clients/dsh-remote/test/agent-runtime.test.mjs +282 -0
  26. package/clients/dsh-remote/test/agent-version.test.mjs +122 -0
  27. package/clients/dsh-remote/test/approval-coordinator.test.mjs +192 -0
  28. package/clients/dsh-remote/test/decision-poller.test.mjs +328 -0
  29. package/clients/dsh-remote/test/wecom-bot.test.mjs +1438 -0
  30. package/clients/dsh-remote/wecom-bot.mjs +2444 -0
  31. package/dsh-setup.mjs +18 -3
  32. package/package.json +2 -2
  33. package/packages/dsh-remote-web/lib/client.js +1048 -223
  34. package/packages/dsh-remote-web/lib/index.js +299 -2
  35. package/packages/dsh-remote-web/package.json +1 -1
  36. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/README.md +412 -0
  37. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/agent-e2e-codex.mjs +185 -0
  38. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/agent-e2e-real.mjs +443 -0
  39. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/codex.mjs +736 -0
  40. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/detect.mjs +399 -0
  41. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/events.mjs +511 -0
  42. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/gemini.mjs +503 -0
  43. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/index.mjs +339 -0
  44. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/opencode.mjs +628 -0
  45. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/permission-bridge.mjs +534 -0
  46. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/proc.mjs +179 -0
  47. package/packages/dsh-remote-web/runtime/clients/dsh-remote/agent/version.mjs +148 -0
  48. package/packages/dsh-remote-web/runtime/clients/dsh-remote/approval-coordinator.mjs +150 -0
  49. package/packages/dsh-remote-web/runtime/clients/dsh-remote/decision-poller.mjs +235 -0
  50. package/packages/dsh-remote-web/runtime/clients/dsh-remote/dsh-bridge.mjs +258 -0
  51. package/packages/dsh-remote-web/runtime/clients/dsh-remote/push-reporter.mjs +533 -0
  52. package/packages/dsh-remote-web/runtime/clients/dsh-remote/wecom-bot.mjs +2444 -0
  53. package/packages/dsh-remote-web/runtime/dsh-setup.mjs +1 -1
  54. package/packages/dsh-remote-web/test/activation-entry.test.mjs +11 -1
  55. package/packages/dsh-remote-web/test/announcements-ui.test.mjs +8 -3
  56. package/packages/dsh-remote-web/test/connect-ui.test.mjs +16 -4
  57. package/packages/dsh-remote-web/test/im-bots.test.mjs +359 -0
  58. package/packages/dsh-remote-web/test/lib-panel-ia.mjs +98 -0
  59. package/packages/dsh-remote-web/test/login-selfheal-ui.test.mjs +13 -5
  60. package/packages/dsh-remote-web/test/panel-ia.test.mjs +798 -0
  61. package/packages/dsh-remote-web/test/password-reset.test.mjs +11 -1
  62. package/packages/dsh-remote-web/test/plugin-link.test.mjs +78 -0
  63. package/packages/dsh-remote-web/test/purchase-agreement-ui.test.mjs +10 -1
  64. package/packages/dsh-remote-web/test/remote-access-ui.test.mjs +20 -8
  65. package/packages/dsh-remote-web/test/settings-entry.test.mjs +30 -21
  66. package/packages/dsh-remote-web/test/sms-captcha-ui.test.mjs +15 -2
  67. package/packages/dsh-remote-web/test/telemetry.test.mjs +5 -3
  68. package/packages/dsh-remote-web/test/wechat-bot-ui.test.mjs +57 -130
@@ -0,0 +1,328 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+
4
+ import { createDecisionPoller, toDshOutcome, MAX_ITEMS_PER_TICK } from "../decision-poller.mjs";
5
+
6
+ /** 按脚本返回的假 fetch;记录每次调用。 */
7
+ function makeFetch(script) {
8
+ const calls = [];
9
+ const fn = async (url, init) => {
10
+ calls.push({ url, init });
11
+ const step = script.shift();
12
+ if (!step) throw new Error("fetch script exhausted: " + url);
13
+ if (step instanceof Error) throw step;
14
+ return {
15
+ status: step.status ?? 200,
16
+ async json() { return step.body ?? { ok: true }; }
17
+ };
18
+ };
19
+ fn.calls = calls;
20
+ return fn;
21
+ }
22
+
23
+ function mk(opts) {
24
+ return createDecisionPoller({
25
+ apiBase: "https://api.example.com",
26
+ deviceId: "dev-x",
27
+ tokenProvider: () => "tok",
28
+ apply: async () => ({ ok: true }),
29
+ fetchImpl: makeFetch([]),
30
+ ...opts
31
+ });
32
+ }
33
+
34
+ test("① 手机 decision → DSH outcome:allow 只能是 allowed-once,未知一律拒绝", () => {
35
+ assert.equal(toDshOutcome("allow"), "allowed-once");
36
+ assert.equal(toDshOutcome("deny"), "rejected");
37
+ assert.equal(toDshOutcome("weird"), "rejected");
38
+ assert.equal(toDshOutcome(""), "rejected");
39
+ assert.equal(toDshOutcome(undefined), "rejected");
40
+ // 绝不把 allow 映射成永久放行
41
+ assert.notEqual(toDshOutcome("allow"), "allowed-always");
42
+ });
43
+
44
+ test("② start 门禁:缺 apiBase / deviceId / apply 都不启动", () => {
45
+ assert.equal(mk({ apiBase: "" }).start().lastError, "no_api_base");
46
+ assert.equal(mk({ deviceId: "" }).start().lastError, "no_device");
47
+ assert.equal(mk({ apply: null }).start().lastError, "no_apply");
48
+ assert.equal(mk().start().started, true);
49
+ mk().stop();
50
+ });
51
+
52
+ test("③ happy path:拉取 → 应用 → ack,参数正确", async () => {
53
+ const applied = [];
54
+ const fetchImpl = makeFetch([
55
+ { status: 200, body: { ok: true, items: [
56
+ { id: "d1", event_id: "dev-x:approval:evt_1", request_id: "evt_1", decision: "allow", decided_at: 1, expires_at: Date.now() + 60000 }
57
+ ] } },
58
+ { status: 200, body: { ok: true, acked: 1 } }
59
+ ]);
60
+ const p = createDecisionPoller({
61
+ apiBase: "https://api.example.com",
62
+ deviceId: "dev-x",
63
+ tokenProvider: () => "tok",
64
+ apply: async (item) => { applied.push(item); },
65
+ fetchImpl,
66
+ intervalMs: 60_000
67
+ });
68
+ await p._tick();
69
+ assert.equal(applied.length, 1);
70
+ assert.equal(applied[0].request_id, "evt_1");
71
+ assert.equal(applied[0].decision, "allow");
72
+ assert.equal(applied[0].outcome, "allowed-once");
73
+ // 第一个请求是 pending(带 device_id),第二个是 ack
74
+ assert.match(fetchImpl.calls[0].url, /\/api\/notify\/pending\?device_id=dev-x/);
75
+ assert.equal(fetchImpl.calls[0].init.headers.authorization, "Bearer tok");
76
+ assert.match(fetchImpl.calls[1].url, /\/api\/notify\/ack$/);
77
+ assert.deepEqual(JSON.parse(fetchImpl.calls[1].init.body).ids, ["d1"]);
78
+ assert.equal(p.status().applied, 1);
79
+ assert.equal(p.status().acked, 1);
80
+ p.stop();
81
+ });
82
+
83
+ test("④ apply 抛错也必须 ack(否则每 3s 重放同一条)", async () => {
84
+ const fetchImpl = makeFetch([
85
+ { status: 200, body: { ok: true, items: [
86
+ { id: "d1", event_id: "e1", request_id: "evt_1", decision: "deny", expires_at: Date.now() + 60000 }
87
+ ] } },
88
+ { status: 200, body: { ok: true } }
89
+ ]);
90
+ const p = createDecisionPoller({
91
+ apiBase: "https://api.example.com",
92
+ deviceId: "dev-x",
93
+ tokenProvider: () => "tok",
94
+ apply: async () => { throw new Error("session gone"); },
95
+ fetchImpl
96
+ });
97
+ await p._tick();
98
+ assert.equal(p.status().applied, 0);
99
+ assert.equal(p.status().failed, 1);
100
+ assert.match(fetchImpl.calls[1].url, /\/api\/notify\/ack$/);
101
+ assert.deepEqual(JSON.parse(fetchImpl.calls[1].init.body).ids, ["d1"]);
102
+ p.stop();
103
+ });
104
+
105
+ test("⑤ 已过期条目:不应用,但要 ack 掉避免反复拉", async () => {
106
+ const applied = [];
107
+ const fetchImpl = makeFetch([
108
+ { status: 200, body: { ok: true, items: [
109
+ { id: "old", event_id: "e", request_id: "evt_old", decision: "allow", expires_at: Date.now() - 1000 }
110
+ ] } },
111
+ { status: 200, body: { ok: true } }
112
+ ]);
113
+ const p = createDecisionPoller({
114
+ apiBase: "https://api.example.com",
115
+ deviceId: "dev-x",
116
+ tokenProvider: () => "tok",
117
+ apply: async (i) => { applied.push(i); },
118
+ fetchImpl
119
+ });
120
+ await p._tick();
121
+ assert.equal(applied.length, 0, "过期决策绝不能应用");
122
+ assert.deepEqual(JSON.parse(fetchImpl.calls[1].init.body).ids, ["old"]);
123
+ p.stop();
124
+ });
125
+
126
+ test("⑥ 401:不应用、不 ack、不抛", async () => {
127
+ const applied = [];
128
+ const fetchImpl = makeFetch([{ status: 401 }]);
129
+ const p = createDecisionPoller({
130
+ apiBase: "https://api.example.com",
131
+ deviceId: "dev-x",
132
+ tokenProvider: () => "expired",
133
+ apply: async (i) => { applied.push(i); },
134
+ fetchImpl
135
+ });
136
+ await p._tick();
137
+ assert.equal(applied.length, 0);
138
+ assert.equal(p.status().lastError, "auth_failed");
139
+ assert.equal(fetchImpl.calls.length, 1, "401 后不应再发 ack");
140
+ p.stop();
141
+ });
142
+
143
+ test("⑦ 没有 token:一个请求都不发", async () => {
144
+ const fetchImpl = makeFetch([]);
145
+ const p = createDecisionPoller({
146
+ apiBase: "https://api.example.com",
147
+ deviceId: "dev-x",
148
+ tokenProvider: () => "",
149
+ apply: async () => {},
150
+ fetchImpl
151
+ });
152
+ await p._tick();
153
+ assert.equal(fetchImpl.calls.length, 0);
154
+ assert.equal(p.status().lastError, "no_token");
155
+ p.stop();
156
+ });
157
+
158
+ test("⑧ ack 失败不影响主流程(幂等,下轮重复拉也没副作用)", async () => {
159
+ const applied = [];
160
+ const fetchImpl = makeFetch([
161
+ { status: 200, body: { ok: true, items: [
162
+ { id: "d1", event_id: "e", request_id: "evt_1", decision: "allow", expires_at: Date.now() + 60000 }
163
+ ] } },
164
+ { status: 500, body: { ok: false } }
165
+ ]);
166
+ const p = createDecisionPoller({
167
+ apiBase: "https://api.example.com",
168
+ deviceId: "dev-x",
169
+ tokenProvider: () => "tok",
170
+ apply: async (i) => { applied.push(i); },
171
+ fetchImpl
172
+ });
173
+ await p._tick();
174
+ assert.equal(applied.length, 1);
175
+ assert.equal(p.status().acked, 0);
176
+ p.stop();
177
+ });
178
+
179
+ test("⑨ 单次最多处理 20 条(有界,避免并发打爆 dsh)", async () => {
180
+ const items = Array.from({ length: 35 }, (_, i) => ({
181
+ id: "d" + i, event_id: "e" + i, request_id: "evt_" + i, decision: "deny", expires_at: Date.now() + 60000
182
+ }));
183
+ const applied = [];
184
+ const fetchImpl = makeFetch([
185
+ { status: 200, body: { ok: true, items } },
186
+ { status: 200, body: { ok: true } }
187
+ ]);
188
+ const p = createDecisionPoller({
189
+ apiBase: "https://api.example.com",
190
+ deviceId: "dev-x",
191
+ tokenProvider: () => "tok",
192
+ apply: async (i) => { applied.push(i); },
193
+ fetchImpl
194
+ });
195
+ await p._tick();
196
+ assert.equal(applied.length, MAX_ITEMS_PER_TICK);
197
+ assert.equal(JSON.parse(fetchImpl.calls[1].init.body).ids.length, MAX_ITEMS_PER_TICK);
198
+ p.stop();
199
+ });
200
+
201
+ test("⑩ 未知 decision 值按 deny 处理(fail-closed)", async () => {
202
+ const applied = [];
203
+ const fetchImpl = makeFetch([
204
+ { status: 200, body: { ok: true, items: [
205
+ { id: "d1", event_id: "e", request_id: "evt_1", decision: "maybe", expires_at: Date.now() + 60000 }
206
+ ] } },
207
+ { status: 200, body: { ok: true } }
208
+ ]);
209
+ const p = createDecisionPoller({
210
+ apiBase: "https://api.example.com",
211
+ deviceId: "dev-x",
212
+ tokenProvider: () => "tok",
213
+ apply: async (i) => { applied.push(i); },
214
+ fetchImpl
215
+ });
216
+ await p._tick();
217
+ assert.equal(applied[0].decision, "deny");
218
+ assert.equal(applied[0].outcome, "rejected");
219
+ p.stop();
220
+ });
221
+
222
+ test("⑪ 网络异常吞掉,不抛、不崩", async () => {
223
+ const fetchImpl = makeFetch([new Error("ECONNRESET")]);
224
+ const p = createDecisionPoller({
225
+ apiBase: "https://api.example.com",
226
+ deviceId: "dev-x",
227
+ tokenProvider: () => "tok",
228
+ apply: async () => {},
229
+ fetchImpl
230
+ });
231
+ await p._tick();
232
+ assert.match(p.status().lastError, /ECONNRESET/);
233
+ p.stop();
234
+ });
235
+
236
+ test("⑫ stop() 后不再轮询", async () => {
237
+ const fetchImpl = makeFetch([]);
238
+ const p = createDecisionPoller({
239
+ apiBase: "https://api.example.com",
240
+ deviceId: "dev-x",
241
+ tokenProvider: () => "tok",
242
+ apply: async () => {},
243
+ fetchImpl,
244
+ intervalMs: 10
245
+ });
246
+ p.start();
247
+ p.stop();
248
+ await new Promise((r) => setTimeout(r, 40));
249
+ assert.equal(fetchImpl.calls.length, 0, "stop 后不应再发任何请求");
250
+ });
251
+
252
+ test("⑬ 403 device_mismatch / 400 invalid_device_id:停止轮询(重试永远不会成功)", async () => {
253
+ const f1 = makeFetch([{ status: 403 }]);
254
+ const p1 = createDecisionPoller({
255
+ apiBase: "https://api.example.com", deviceId: "dev-x", tokenProvider: () => "tok",
256
+ apply: async () => {}, fetchImpl: f1, intervalMs: 10
257
+ });
258
+ p1.start();
259
+ await p1._tick();
260
+ assert.equal(p1.status().lastError, "device_mismatch");
261
+ const before = f1.calls.length;
262
+ await new Promise((r) => setTimeout(r, 40));
263
+ assert.equal(f1.calls.length, before, "403 之后不应再轮询");
264
+ p1.stop();
265
+
266
+ const f2 = makeFetch([{ status: 400 }]);
267
+ const p2 = createDecisionPoller({
268
+ apiBase: "https://api.example.com", deviceId: "dev-x", tokenProvider: () => "tok",
269
+ apply: async () => {}, fetchImpl: f2, intervalMs: 10
270
+ });
271
+ await p2._tick();
272
+ assert.equal(p2.status().lastError, "invalid_device_id");
273
+ p2.stop();
274
+ });
275
+
276
+ test("⑭ 401 之后仍会继续轮询(token 刷新后要能恢复)", async () => {
277
+ const f = makeFetch([{ status: 401 }]);
278
+ const p = createDecisionPoller({
279
+ apiBase: "https://api.example.com", deviceId: "dev-x", tokenProvider: () => "expired",
280
+ apply: async () => {}, fetchImpl: f, intervalMs: 10
281
+ });
282
+ p.start();
283
+ await p._tick();
284
+ assert.equal(p.status().lastError, "auth_failed");
285
+ await new Promise((r) => setTimeout(r, 30));
286
+ assert.ok(f.calls.length >= 2, "401 不应停止轮询(等 token 刷新)");
287
+ p.stop();
288
+ });
289
+
290
+ test("⑮ 至少一次语义:同一条 id 被重投时只 apply 一次、但仍 ack(幂等去重)", async () => {
291
+ const applied = [];
292
+ const item = { id: "dec_same", event_id: "e", request_id: "evt_same", decision: "allow", expires_at: Date.now() + 60000, redeliver_count: 0 };
293
+ const fetchImpl = makeFetch([
294
+ { status: 200, body: { ok: true, items: [item] } },
295
+ { status: 200, body: { ok: true } },
296
+ // 服务端重投同一条(redeliver_count=1)
297
+ { status: 200, body: { ok: true, items: [{ ...item, redeliver_count: 1 }] } },
298
+ { status: 200, body: { ok: true } }
299
+ ]);
300
+ const p = createDecisionPoller({
301
+ apiBase: "https://api.example.com", deviceId: "dev-x", tokenProvider: () => "tok",
302
+ apply: async (i) => { applied.push(i); }, fetchImpl
303
+ });
304
+ await p._tick();
305
+ await p._tick();
306
+ assert.equal(applied.length, 1, "重投不应重复 apply");
307
+ assert.equal(p.status().applied, 1);
308
+ assert.equal(p.status().skipped, 1);
309
+ assert.equal(p.status().acked, 2, "两次都应 ack(否则服务端会一直重投)");
310
+ p.stop();
311
+ });
312
+
313
+ test("⑯ 重投回执会写 warn 日志留痕(便于排查重复审批)", async () => {
314
+ const logs = [];
315
+ const item = { id: "dec_r", event_id: "e", request_id: "evt_r", decision: "deny", expires_at: Date.now() + 60000, redeliver_count: 2 };
316
+ const fetchImpl = makeFetch([
317
+ { status: 200, body: { ok: true, items: [item] } },
318
+ { status: 200, body: { ok: true } }
319
+ ]);
320
+ const p = createDecisionPoller({
321
+ apiBase: "https://api.example.com", deviceId: "dev-x", tokenProvider: () => "tok",
322
+ apply: async () => {}, fetchImpl,
323
+ log: (lvl, msg) => logs.push(lvl + ":" + msg)
324
+ });
325
+ await p._tick();
326
+ assert.ok(logs.some((l) => l.startsWith("warn:") && /重投/.test(l)));
327
+ p.stop();
328
+ });