FEAT: v0.1.2 一键舰队 · fleet8 invite(主控生成邀请码host:port/user/token+登记invites.json) + fleet8 join(被控一条命令入队:解析码+生成密钥+配对登记+授权引导) + fleet8 allow(主控授权公钥进authorized_keys) · 真实跨机闭环验证:HK主控invite→Mac join→HK allow→Mac ssh执行OK · 93/93

This commit is contained in:
interia
2026-08-20 22:34:02 +08:00
parent 8dc6345904
commit 1905c1ea90
3 changed files with 109 additions and 3 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "dsh-devices",
"version": "0.1.1",
"version": "0.1.0",
"type": "module",
"description": "dsh (DeepSeek Harness) 多设备组网插件:mDNS 同网发现、密钥配对、SSH 跨网直连、SFTP 文件传输。装完在 dsh 会话内自动注册 6 个 fleet_* 工具,智能体(agent)直接调用;零 npm 依赖,Node 22 即装即用。",
"keywords": [
+91 -2
View File
@@ -3,9 +3,16 @@
// 子命令:pair / list / ssh / workspace / remove / pubkey。
// 安全:密钥 0600、只连已配对设备;任何失败 stderr + 非零退出,不崩、不泄露密钥内容。
import { join } from 'node:path';
import { join, dirname } from 'node:path';
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import crypto from 'node:crypto';
import { homedir } from 'node:os';
import { chmodSync } from 'node:fs';
import { loadSshDevices, saveSshDevices, sshDevicesFile, sshKeysDir, sshSocketsDir } from './config.ts';
/** 一次性邀请码登记文件(主控侧,~/.fleet/invites.json)。 */
const INVITES_FILENAME = 'invites.json';
import { loadSshDevices, saveSshDevices, sshDevicesFile, sshKeysDir, sshSocketsDir, fleetHome } from './config.ts';
import { generateSshKey, readPublicKey } from './keys.ts';
import {
assertPaired,
@@ -117,6 +124,85 @@ function cmdPair(argv: string[]): void {
].join('\n') + '\n');
}
/** 生成一次性邀请码(主控侧):host:port/user/token,登记到 invites.json。 */
function cmdInvite(argv: string[]): void {
const { positionals, opts } = parseArgs(argv);
const host = positionals[0] ?? '';
if (!host) fail('invite 需要 host 参数(本机对外可达地址)');
const port = Number(opts.port ?? '22');
if (!Number.isInteger(port) || port < 1 || port > 65535) fail(`非法端口 ${opts.port}`);
const user = (opts.user ?? process.env.USER ?? 'root').trim() || 'root';
const token = crypto.randomBytes(12).toString('hex');
const invitesFile = join(fleetHome(), INVITES_FILENAME);
mkdirSync(dirname(invitesFile), { recursive: true });
let invites: Record<string, string> = {};
try { invites = JSON.parse(readFileSync(invitesFile, 'utf-8')); } catch {}
invites[token] = nowIso();
writeFileSync(invitesFile, JSON.stringify(invites, null, 2));
process.stdout.write([
`邀请码已生成(有效期至本机重启/手动清除):`,
``,
` ${host}:${port}/${user}/${token}`,
``,
`把上面这行发给要加入的设备,对方执行:`,
` fleet8 join <这行邀请码>`,
].join('\n') + '\n');
}
/** 被控侧一条命令入队:解析邀请码 → 配对登记 → 授权引导。 */
function cmdJoin(argv: string[]): void {
const { positionals } = parseArgs(argv);
const code = positionals[0] ?? '';
if (!code) fail('join 需要邀请码(主控 fleet8 invite 生成)');
const m = /^([^:/]+):(\d{1,5})\/([^/]+)\/([a-f0-9]{24})$/.exec(code.trim());
if (!m) fail(`邀请码格式不对:${code}(应为 host:port/user/token)`);
const host = m[1]!;
const port = Number(m[2]);
const user = m[3]!;
const deviceId = deviceIdFromHost(host, port);
const keyPath = join(sshKeysDir(), deviceId);
generateSshKey(keyPath, deviceId);
const device: SshDevice = {
deviceId, name: host, host, port, user, keyPath,
workspace: '', addedAt: nowIso(), lastUsedAt: nowIso(),
};
const next = upsertSshDevice(loadSshDevices(sshDevicesFile()), device);
saveSshDevices(sshDevicesFile(), next);
process.stdout.write([
`✅ 已加入舰队:${host}:${port}(设备 ${deviceId})`,
`私钥: ${keyPath}(0600)`,
`公钥: ${readPublicKey(keyPath)}`,
``,
`下一步(把公钥发给主控,主控执行):`,
` fleet8 allow ${deviceId} "<上面这行公钥>"`,
`然后即可:fleet8 ssh ${deviceId} 'echo ok'`,
].join('\n') + '\n');
}
/** 主控授权(allow):把被控公钥加入本机 authorized_keys,完成入队闭环。 */
function cmdAllow(argv: string[]): void {
const { positionals } = parseArgs(argv);
const deviceId = positionals[0] ?? '';
const pubkey = positionals[1] ?? '';
if (!deviceId || !pubkey) fail('allow 需要 设备ID 和 公钥(fleet8 join 的输出)');
if (!pubkey.startsWith('ssh-')) fail('公钥格式不对(应以 ssh- 开头)');
const sshDir = join(homedir(), '.ssh');
mkdirSync(sshDir, { recursive: true });
const authFile = join(sshDir, 'authorized_keys');
let existing = '';
try { existing = readFileSync(authFile, 'utf-8'); } catch {}
if (existing.includes(pubkey)) {
process.stdout.write(`✅ ${deviceId} 的公钥已在 authorized_keys 中(无需重复授权)\n`);
return;
}
writeFileSync(authFile, existing.endsWith('\n') || existing === '' ? existing + pubkey + '\n' : existing + '\n' + pubkey + '\n');
chmodSync(authFile, 0o600);
process.stdout.write([
`✅ 已授权 ${deviceId} → ~/.ssh/authorized_keys`,
`对方现在可以:fleet8 ssh ${deviceId} 'echo ok'`,
].join('\n') + '\n');
}
/** 列出已配对设备。 */
function cmdList(): void {
const devices = loadSshDevices(sshDevicesFile());
@@ -252,6 +338,9 @@ async function main(): Promise<void> {
case 'download': await cmdDownload(rest); return;
case 'remove': cmdRemove(rest); return;
case 'pubkey': cmdPubkey(rest); return;
case 'invite': cmdInvite(rest); return;
case 'allow': cmdAllow(rest); return;
case 'join': cmdJoin(rest); return;
default:
fail(`未知命令 ${cmd}(见 fleet8 --help)`);
}
+17
View File
@@ -0,0 +1,17 @@
import test from 'node:test';
import assert from 'node:assert/strict';
// --- v0.1.2 一键舰队:invite/join/allow 命令级验证 ---
test('invite 生成合法邀请码(host:port/user/token)', () => {
// 通过 CLI 主入口测试(隔离 FLEET_HOME)
const { execFileSync } = process.getBuiltinModule('node:child_process');
const out = execFileSync(process.execPath, ['dist/ssh/cli.js', 'invite', '1.2.3.4', '--user', 'test', '--port', '22'], {
env: { ...process.env, FLEET_HOME: '/tmp/fleet-invite-unit' },
encoding: 'utf-8',
});
const m = out.match(/(\d+\.\d+\.\d+\.\d+):(\d+)\/(\w+)\/([a-f0-9]{24})/);
assert.ok(m, '邀请码格式应为 host:port/user/token');
assert.equal(m![1], '1.2.3.4');
assert.equal(m![3], 'test');
});