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.
- package/README.md +5 -5
- package/client/cli-archives.test.mjs +134 -0
- package/client/cli-group.test.mjs +9 -9
- package/client/cli.mjs +647 -22
- package/client/pairing-baseurl.test.mjs +153 -0
- package/client/pairing.test.mjs +4 -1
- package/client/peer-call.mjs +94 -4
- package/host/server.mjs +1663 -166
- package/host/test/auth.test.mjs +20 -18
- package/host/test/group-applications-admin.test.mjs +204 -0
- package/host/test/group-apply.test.mjs +465 -0
- package/host/test/group-archive.test.mjs +236 -0
- package/host/test/group-auth.test.mjs +8 -5
- package/host/test/group-cluster.mjs +124 -6
- package/host/test/group-commit.test.mjs +55 -16
- package/host/test/group-discovery.test.mjs +480 -0
- package/host/test/group-epoch.test.mjs +268 -0
- package/host/test/group-members-authoritative.test.mjs +119 -0
- package/host/test/group-name-join.test.mjs +229 -0
- package/host/test/group-name-resolution.test.mjs +317 -0
- package/host/test/group-post-auth.test.mjs +256 -0
- package/host/test/group-post-target.test.mjs +158 -0
- package/host/test/group.test.mjs +23 -19
- package/host/test/helpers.mjs +57 -7
- package/host/test/pairing-ecdh.test.mjs +10 -5
- package/host/test/unpair.test.mjs +1 -1
- package/lib/admin-actions.test.mjs +1 -1
- package/lib/admin-archives.test.mjs +297 -0
- package/lib/admin-group-apply.test.mjs +720 -0
- package/lib/admin-group.test.mjs +32 -32
- package/lib/admin-unpair.test.mjs +3 -3
- package/lib/admin.mjs +1242 -19
- package/lib/client-render.test.mjs +1 -1
- package/lib/client.js +1 -1
- package/lib/group-client.mjs +625 -6
- package/lib/panel-archives.test.mjs +332 -0
- package/lib/panel-group-apply.test.mjs +620 -0
- package/lib/panel-group.test.mjs +24 -19
- package/lib/panel-pair-group.test.mjs +256 -0
- package/lib/panel-unpair.test.mjs +9 -9
- package/overlays/web.patch.yml +1 -1
- package/package.json +66 -53
- package/DESIGN.md +0 -398
- 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.
|
|
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.
|
|
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.
|
|
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 问
|
|
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
|
|
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', '
|
|
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, /显示名=
|
|
191
|
-
const joinedB = await runAsync(['group', 'join', '--name', '
|
|
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, /
|
|
201
|
-
assert.match(state.stdout, /
|
|
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, /
|
|
208
|
-
assert.match(history.stdout, /
|
|
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, '
|
|
213
|
-
assert.equal(raw.members[peerB.peerId].name, '
|
|
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]);
|