dsh-lan-peers 0.7.4 → 0.8.1

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 (44) hide show
  1. package/README.md +5 -5
  2. package/client/cli-archives.test.mjs +134 -0
  3. package/client/cli-group.test.mjs +9 -9
  4. package/client/cli.mjs +647 -22
  5. package/client/pairing-baseurl.test.mjs +153 -0
  6. package/client/pairing.test.mjs +4 -1
  7. package/client/peer-call.mjs +94 -4
  8. package/host/server.mjs +1663 -166
  9. package/host/test/auth.test.mjs +20 -18
  10. package/host/test/group-applications-admin.test.mjs +204 -0
  11. package/host/test/group-apply.test.mjs +465 -0
  12. package/host/test/group-archive.test.mjs +236 -0
  13. package/host/test/group-auth.test.mjs +8 -5
  14. package/host/test/group-cluster.mjs +124 -6
  15. package/host/test/group-commit.test.mjs +55 -16
  16. package/host/test/group-discovery.test.mjs +480 -0
  17. package/host/test/group-epoch.test.mjs +268 -0
  18. package/host/test/group-members-authoritative.test.mjs +119 -0
  19. package/host/test/group-name-join.test.mjs +229 -0
  20. package/host/test/group-name-resolution.test.mjs +317 -0
  21. package/host/test/group-post-auth.test.mjs +256 -0
  22. package/host/test/group-post-target.test.mjs +158 -0
  23. package/host/test/group.test.mjs +23 -19
  24. package/host/test/helpers.mjs +57 -7
  25. package/host/test/pairing-ecdh.test.mjs +10 -5
  26. package/host/test/unpair.test.mjs +1 -1
  27. package/lib/admin-actions.test.mjs +1 -1
  28. package/lib/admin-archives.test.mjs +297 -0
  29. package/lib/admin-group-apply.test.mjs +720 -0
  30. package/lib/admin-group.test.mjs +32 -32
  31. package/lib/admin-unpair.test.mjs +3 -3
  32. package/lib/admin.mjs +1242 -19
  33. package/lib/client-render.test.mjs +1 -1
  34. package/lib/client.js +1 -1
  35. package/lib/group-client.mjs +625 -6
  36. package/lib/panel-archives.test.mjs +332 -0
  37. package/lib/panel-group-apply.test.mjs +620 -0
  38. package/lib/panel-group.test.mjs +24 -19
  39. package/lib/panel-pair-group.test.mjs +256 -0
  40. package/lib/panel-unpair.test.mjs +9 -9
  41. package/overlays/web.patch.yml +1 -1
  42. package/package.json +66 -53
  43. package/DESIGN.md +0 -398
  44. package/HANDOVER.md +0 -437
package/README.md CHANGED
@@ -76,7 +76,7 @@ dsh plugin --profile web add dsh-lan-peers
76
76
  workspace: ~/.dsh/lan-peers/workspace # 远端派单会话的工作目录
77
77
  allowRemotePrompts: false # 默认关;打开后才接受别人的派单
78
78
  requireEncryption: true # 默认开;旧版本对端会被拒绝
79
- trustedCidrs: ["192.168.5.0/24"] # 可选,限制来源网段
79
+ trustedCidrs: ["192.168.1.0/24"] # 可选,限制来源网段
80
80
  agentPreset: standard
81
81
  ```
82
82
 
@@ -98,13 +98,13 @@ dsh plugin --profile web add dsh-lan-peers
98
98
 
99
99
  ## 配对
100
100
 
101
- **界面**:左下角 🔗 伙伴 → 「生成配对码」,把 6 位码给对方;对方在「添加伙伴」里填本机地址(如 `192.168.5.51:7788`)+ 码。两个方向各配一次,双方都能主动派单。
101
+ **界面**:左下角 🔗 伙伴 → 「生成配对码」,把 6 位码给对方;对方在「添加伙伴」里填本机地址(如 `192.168.1.10:7788`)+ 码。两个方向各配一次,双方都能主动派单。
102
102
 
103
103
  **命令行**:
104
104
 
105
105
  ```bash
106
106
  npx dsh-lan-peers code --state-dir ~/.dsh/lan-peers
107
- npx dsh-lan-peers pair --url http://192.168.5.51:7788 --code 123456 --name my-nas
107
+ npx dsh-lan-peers pair --url http://192.168.1.10:7788 --code 123456 --name my-nas
108
108
  npx dsh-lan-peers peers --probe
109
109
  ```
110
110
 
@@ -112,12 +112,12 @@ npx dsh-lan-peers peers --probe
112
112
 
113
113
  模型侧(会话里直接说):
114
114
 
115
- > 用 lan_peer_ask 问 fn-302:`/vol5/main` 下 `pwd` 输出什么?
115
+ > 用 lan_peer_ask 问 my-nas:`/data/project` 下 `pwd` 输出什么?
116
116
 
117
117
  命令行:
118
118
 
119
119
  ```bash
120
- npx dsh-lan-peers ask --peer fn-302 "在 /vol5/main 执行 pwd" --timeout 120
120
+ npx dsh-lan-peers ask --peer my-nas "在 /data/project 执行 pwd" --timeout 120
121
121
  ```
122
122
 
123
123
  对端会在自己的 `workspace` 里建真实会话、由对端模型执行,然后把最终回复加密回传。
@@ -0,0 +1,134 @@
1
+ // dsh-lan-peers · v0.8.0-β(DESIGN §18.2)CLI:group archives / group archive-delete。
2
+ //
3
+ // 两个子命令都只打本机回环的 hub 管理接口(/api/lan-peers/group/archives),
4
+ // 所以这里起一个只实现这两条路由的 node:http 桩,真跑 cli.mjs 子进程,
5
+ // 断言请求形状、stdout 文案与退出码(403/409 归「现在做不了」,其余算失败)。
6
+ import test from 'node:test';
7
+ import assert from 'node:assert/strict';
8
+ import http from 'node:http';
9
+ import { execFile } from 'node:child_process';
10
+ import fs from 'node:fs';
11
+ import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
13
+ import { makeTempStateDir } from '../host/test/helpers.mjs';
14
+
15
+ const CLI = path.join(path.dirname(fileURLToPath(import.meta.url)), 'cli.mjs');
16
+ const MB = 1024 * 1024;
17
+
18
+ function runAsync(args) {
19
+ return new Promise((resolve) => {
20
+ execFile(process.execPath, [CLI, ...args], { encoding: 'utf8' }, (error, stdout, stderr) => {
21
+ resolve({ status: error ? error.code : 0, stdout, stderr });
22
+ });
23
+ });
24
+ }
25
+
26
+ /** 起一个只实现 group/archives 的管理接口桩;handler 决定状态码与响应体。 */
27
+ async function startAdmin(t, handler) {
28
+ const seen = [];
29
+ const server = http.createServer((req, res) => {
30
+ let raw = '';
31
+ req.on('data', (chunk) => { raw += chunk; });
32
+ req.on('end', () => {
33
+ const body = raw === '' ? null : JSON.parse(raw);
34
+ const call = { method: req.method, url: req.url, body };
35
+ seen.push(call);
36
+ const reply = handler(call) ?? {};
37
+ res.writeHead(reply.status ?? 200, { 'Content-Type': 'application/json' });
38
+ res.end(JSON.stringify(reply.body ?? {}));
39
+ });
40
+ });
41
+ const port = await new Promise((resolve) => {
42
+ server.listen(0, '127.0.0.1', () => resolve(server.address().port));
43
+ });
44
+ t.after(() => new Promise((resolve) => { server.close(() => resolve()); }));
45
+ const dir = makeTempStateDir('dsh-lan-peers-cli-archives-');
46
+ t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
47
+ return { seen, url: 'http://127.0.0.1:' + port, dir };
48
+ }
49
+
50
+ const ARCHIVES = [
51
+ { file: '101-400.json', fromSeq: 101, toSeq: 400, count: 300, bytes: 12 * MB, createdAt: 1_700_000_000_000 },
52
+ { file: '1-100.json', fromSeq: 1, toSeq: 100, count: 100, bytes: 9 * MB, createdAt: 1_600_000_000_000 },
53
+ ];
54
+
55
+ test('group archives:逐行打印文件名 / seq 区间 / 条数 / 体积 / 时间,末行汇总', async (t) => {
56
+ const admin = await startAdmin(t, () => ({ body: { archives: ARCHIVES, totalBytes: 21 * MB } }));
57
+ const out = await runAsync(['group', 'archives', '--url', admin.url, '--state-dir', admin.dir]);
58
+ assert.equal(out.status, 0, out.stderr);
59
+ const lines = out.stdout.trim().split('\n');
60
+ assert.equal(lines.length, 2);
61
+ assert.match(lines[0], /^101-400\.json\tseq 101-400\t300 条\t12582912B\t1700000000000$/);
62
+ assert.match(lines[1], /^1-100\.json\tseq 1-100\t100 条\t9437184B\t1600000000000$/);
63
+ assert.match(out.stderr, /共 2 个归档 · 22020096 字节/);
64
+
65
+ assert.equal(admin.seen.length, 1);
66
+ assert.equal(admin.seen[0].method, 'GET');
67
+ assert.equal(admin.seen[0].url, '/api/lan-peers/group/archives');
68
+ });
69
+
70
+ test('group archives:没有归档时给一行说明,不打印空表头', async (t) => {
71
+ const admin = await startAdmin(t, () => ({ body: { archives: [], totalBytes: 0 } }));
72
+ const out = await runAsync(['group', 'archives', '--url', admin.url, '--state-dir', admin.dir]);
73
+ assert.equal(out.status, 0, out.stderr);
74
+ assert.equal(out.stdout.trim(), '没有归档(hub 侧还没有归档文件)');
75
+ assert.equal(out.stderr, '');
76
+ });
77
+
78
+ test('group archives:非 hub(403)按「现在做不了」退出 2,未启用(409)同理', async (t) => {
79
+ const forbidden = await startAdmin(t, () => ({ status: 403, body: { error: 'hub-only' } }));
80
+ const out403 = await runAsync(['group', 'archives', '--url', forbidden.url, '--state-dir', forbidden.dir]);
81
+ assert.equal(out403.status, 2);
82
+ assert.match(out403.stderr, /归档操作失败: 403 hub-only/);
83
+
84
+ const disabled = await startAdmin(t, () => ({ status: 409, body: { error: 'group-disabled' } }));
85
+ const out409 = await runAsync(['group', 'archives', '--url', disabled.url, '--state-dir', disabled.dir]);
86
+ assert.equal(out409.status, 2);
87
+ assert.match(out409.stderr, /409 group-disabled/);
88
+ });
89
+
90
+ test('group archives:门面未接线(501)如实报错并退出 1', async (t) => {
91
+ const admin = await startAdmin(t, () => ({ status: 501, body: { error: 'archives-unsupported' } }));
92
+ const out = await runAsync(['group', 'archives', '--url', admin.url, '--state-dir', admin.dir]);
93
+ assert.equal(out.status, 1);
94
+ assert.match(out.stderr, /归档操作失败: 501 archives-unsupported/);
95
+ assert.equal(out.stdout, '');
96
+ });
97
+
98
+ test('group archive-delete:POST { file, action:"delete" } 并打印释放的字节', async (t) => {
99
+ const admin = await startAdmin(t, () => ({ body: { ok: true, removedBytes: 12 * MB } }));
100
+ const out = await runAsync(['group', 'archive-delete', '101-400.json', '--url', admin.url, '--state-dir', admin.dir]);
101
+ assert.equal(out.status, 0, out.stderr);
102
+ assert.equal(out.stdout.trim(), '已删除归档 101-400.json,释放 12582912 字节');
103
+ assert.equal(admin.seen.length, 1);
104
+ assert.equal(admin.seen[0].method, 'POST');
105
+ assert.equal(admin.seen[0].url, '/api/lan-peers/group/archives');
106
+ assert.deepEqual(admin.seen[0].body, { file: '101-400.json', action: 'delete' });
107
+ });
108
+
109
+ test('group archive-delete:未知归档(404)退出 1,权限失败(403)退出 2', async (t) => {
110
+ const unknown = await startAdmin(t, () => ({ status: 404, body: { error: 'unknown-archive' } }));
111
+ const out404 = await runAsync(['group', 'archive-delete', 'nope.json', '--url', unknown.url, '--state-dir', unknown.dir]);
112
+ assert.equal(out404.status, 1);
113
+ assert.match(out404.stderr, /归档操作失败: 404 unknown-archive/);
114
+
115
+ const forbidden = await startAdmin(t, () => ({ status: 403, body: { error: 'hub-only' } }));
116
+ const out403 = await runAsync(['group', 'archive-delete', '1-100.json', '--url', forbidden.url, '--state-dir', forbidden.dir]);
117
+ assert.equal(out403.status, 2);
118
+ assert.match(out403.stderr, /403 hub-only/);
119
+ });
120
+
121
+ test('group archive-delete:缺文件名直接报用法,不发请求', async (t) => {
122
+ const admin = await startAdmin(t, () => ({ body: { ok: true, removedBytes: 0 } }));
123
+ const out = await runAsync(['group', 'archive-delete', '--url', admin.url, '--state-dir', admin.dir]);
124
+ assert.equal(out.status, 1);
125
+ assert.match(out.stderr, /用法: dsh-lan-peers group archive-delete <file>/);
126
+ assert.equal(admin.seen.length, 0);
127
+ });
128
+
129
+ test('group help:两个归档子命令都出现在帮助里', async () => {
130
+ const out = await runAsync(['group', 'help']);
131
+ assert.equal(out.status, 0);
132
+ assert.match(out.stdout, /archives\s+列出 hub 侧归档文件/);
133
+ assert.match(out.stdout, /archive-delete <file>\s+删除一个归档文件/);
134
+ });
@@ -185,10 +185,10 @@ test('group join --name 只改自己;两个同名节点在 state / history 里
185
185
  const dirB = memberStateDir(t, { baseUrl: hub.baseUrl, hubInstanceId: hubId, identity: peerB });
186
186
 
187
187
  // 两台机器的默认主机名相同(真机现场就是这个现象)
188
- const joinedA = await runAsync(['group', 'join', '--name', 'PW-NEW22G6LR3', '--peer', 'hub', '--state-dir', dirA]);
188
+ const joinedA = await runAsync(['group', 'join', '--name', 'desk-alpha', '--peer', 'hub', '--state-dir', dirA]);
189
189
  assert.equal(joinedA.status, 0, joinedA.stderr);
190
- assert.match(joinedA.stdout, /显示名=PW-NEW22G6LR3/);
191
- const joinedB = await runAsync(['group', 'join', '--name', 'PW-NEW22G6LR3', '--peer', 'hub', '--state-dir', dirB]);
190
+ assert.match(joinedA.stdout, /显示名=desk-alpha/);
191
+ const joinedB = await runAsync(['group', 'join', '--name', 'desk-alpha', '--peer', 'hub', '--state-dir', dirB]);
192
192
  assert.equal(joinedB.status, 0, joinedB.stderr);
193
193
 
194
194
  // 不传 --name 再 join 不会把名字抹掉
@@ -197,20 +197,20 @@ test('group join --name 只改自己;两个同名节点在 state / history 里
197
197
 
198
198
  const state = await runAsync(['group', 'state', '--peer', 'hub', '--state-dir', dirA]);
199
199
  assert.equal(state.status, 0, state.stderr);
200
- assert.match(state.stdout, /PW-NEW22G6LR3 \(peer-cli\)/, '同名成员要带短 id 后缀');
201
- assert.match(state.stdout, /PW-NEW22G6LR3 \(bbbbbbbb\)/);
200
+ assert.match(state.stdout, /desk-alpha \(peer-cli\)/, '同名成员要带短 id 后缀');
201
+ assert.match(state.stdout, /desk-alpha \(bbbbbbbb\)/);
202
202
 
203
203
  await runAsync(['group', 'post', '--text', 'A 说的', '--peer', 'hub', '--state-dir', dirA]);
204
204
  await runAsync(['group', 'post', '--text', 'B 说的', '--peer', 'hub', '--state-dir', dirB]);
205
205
  const history = await runAsync(['group', 'history', '--peer', 'hub', '--state-dir', dirA]);
206
206
  assert.equal(history.status, 0, history.stderr);
207
- assert.match(history.stdout, /PW-NEW22G6LR3 \(peer-cli\) \[chat\] A 说的/, 'history 的 from.name 也要能区分');
208
- assert.match(history.stdout, /PW-NEW22G6LR3 \(bbbbbbbb\) \[chat\] B 说的/);
207
+ assert.match(history.stdout, /desk-alpha \(peer-cli\) \[chat\] A 说的/, 'history 的 from.name 也要能区分');
208
+ assert.match(history.stdout, /desk-alpha \(bbbbbbbb\) \[chat\] B 说的/);
209
209
 
210
210
  // hub 侧存的仍是原始 name(渲染层才加后缀)
211
211
  const raw = JSON.parse(fs.readFileSync(path.join(hub.stateDir, 'group.json'), 'utf8'));
212
- assert.equal(raw.members[MEMBER.peerId].name, 'PW-NEW22G6LR3');
213
- assert.equal(raw.members[peerB.peerId].name, 'PW-NEW22G6LR3');
212
+ assert.equal(raw.members[MEMBER.peerId].name, 'desk-alpha');
213
+ assert.equal(raw.members[peerB.peerId].name, 'desk-alpha');
214
214
 
215
215
  // 空 --name 直接拒绝
216
216
  const empty = await runAsync(['group', 'join', '--name', '', '--peer', 'hub', '--state-dir', dirA]);