botmux 2.91.0 → 2.92.0
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/dist/bot-registry.d.ts +1 -1
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +9 -7
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli/dashboard-endpoint.d.ts +9 -6
- package/dist/cli/dashboard-endpoint.d.ts.map +1 -1
- package/dist/cli/dashboard-endpoint.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +11 -1
- package/dist/config.js.map +1 -1
- package/dist/daemon.d.ts +3 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +19 -2
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-payload.js +1 -1
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +3 -2
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +4 -2
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard-web/app.js +4 -3
- package/dist/dashboard.js +118 -3
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.js +3 -3
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.js +3 -3
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +12 -7
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/im/lark/relay-target-routing.d.ts.map +1 -1
- package/dist/im/lark/relay-target-routing.js +7 -2
- package/dist/im/lark/relay-target-routing.js.map +1 -1
- package/dist/im/lark/reply-mode-command.d.ts.map +1 -1
- package/dist/im/lark/reply-mode-command.js +3 -1
- package/dist/im/lark/reply-mode-command.js.map +1 -1
- package/dist/platform/bind.d.ts.map +1 -1
- package/dist/platform/bind.js +2 -1
- package/dist/platform/bind.js.map +1 -1
- package/dist/platform/tunnel-client.d.ts +10 -0
- package/dist/platform/tunnel-client.d.ts.map +1 -1
- package/dist/platform/tunnel-client.js +2 -0
- package/dist/platform/tunnel-client.js.map +1 -1
- package/dist/services/card-prefs-store.d.ts +1 -1
- package/dist/services/card-prefs-store.d.ts.map +1 -1
- package/dist/services/card-prefs-store.js +5 -5
- package/dist/services/card-prefs-store.js.map +1 -1
- package/dist/services/chat-reply-mode-store.d.ts +1 -1
- package/dist/services/chat-reply-mode-store.d.ts.map +1 -1
- package/dist/services/chat-reply-mode-store.js +17 -3
- package/dist/services/chat-reply-mode-store.js.map +1 -1
- package/dist/setup/cli-selection.d.ts +26 -3
- package/dist/setup/cli-selection.d.ts.map +1 -1
- package/dist/setup/cli-selection.js +53 -4
- package/dist/setup/cli-selection.js.map +1 -1
- package/dist/utils/listen-with-probe.d.ts +20 -1
- package/dist/utils/listen-with-probe.d.ts.map +1 -1
- package/dist/utils/listen-with-probe.js +48 -6
- package/dist/utils/listen-with-probe.js.map +1 -1
- package/package.json +1 -1
package/dist/dashboard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/dashboard.ts
|
|
2
|
-
import { createServer } from 'node:http';
|
|
2
|
+
import { createServer, get as httpGet } from 'node:http';
|
|
3
3
|
import { createServer as createTcpServer } from 'node:net';
|
|
4
4
|
import { readFileSync, existsSync, mkdirSync, statSync, createReadStream, } from 'node:fs';
|
|
5
5
|
import { atomicWriteFileSync } from './utils/atomic-write.js';
|
|
@@ -52,7 +52,8 @@ import { installDashboardSkill, parseDashboardSkillInstallRequest, parseInstallL
|
|
|
52
52
|
import { botDefaultsPayload, botSummaryPayload } from './dashboard/bot-payload.js';
|
|
53
53
|
import { isValidRoleProfileId } from './services/role-profile-store.js';
|
|
54
54
|
import { mergeSafeInsightOverviews } from './services/insight/report.js';
|
|
55
|
-
import {
|
|
55
|
+
import { watch as fsWatch } from 'node:fs';
|
|
56
|
+
import { readPlatformBinding, PLATFORM_BINDING_PATH } from './platform/binding.js';
|
|
56
57
|
import { startPlatformTunnelClient } from './platform/tunnel-client.js';
|
|
57
58
|
import { cleanupIdleSessions, parseIdleCleanupHours } from './dashboard/session-cleanup.js';
|
|
58
59
|
const SECRET_PATH = join(homedir(), '.botmux', '.dashboard-secret');
|
|
@@ -118,6 +119,37 @@ function dashboardPortAvailable(port) {
|
|
|
118
119
|
// wildcard bind still succeeds, causing CLI HMAC calls to hit that process.
|
|
119
120
|
return tcpPortAvailable('127.0.0.1', port);
|
|
120
121
|
}
|
|
122
|
+
// Per-process random marker served at /__selfcheck. Lets verifyDashboardBinding
|
|
123
|
+
// confirm a loopback request to our just-bound wildcard port reaches THIS
|
|
124
|
+
// process and not a shadow holding 127.0.0.1:port. The value is meaningless to
|
|
125
|
+
// anyone else, so exposing it is safe.
|
|
126
|
+
const DASHBOARD_SELF_NONCE = randomBytes(16).toString('hex');
|
|
127
|
+
/**
|
|
128
|
+
* Post-bind loopback identity check handed to listenWithProbe (verifyBound).
|
|
129
|
+
* dashboardPortAvailable is a PRE-bind gate, but on macOS a loopback occupant
|
|
130
|
+
* can appear in the race window between that check and the wildcard listen, and
|
|
131
|
+
* a 0.0.0.0 bind succeeds anyway while loopback routing favours the occupant —
|
|
132
|
+
* so the dashboard would advertise a port it doesn't actually own on loopback.
|
|
133
|
+
* This runs AFTER listen: dial 127.0.0.1:port/__selfcheck and require OUR nonce
|
|
134
|
+
* back. A shadow answers with its own body/404 → reject → listenWithProbe steps
|
|
135
|
+
* up. Number-independent: it works no matter which port or who is shadowing.
|
|
136
|
+
* Loopback-host binds can't be shadowed, so they short-circuit to true.
|
|
137
|
+
*/
|
|
138
|
+
function verifyDashboardBinding(port) {
|
|
139
|
+
if (!isWildcardBindHost(config.dashboard.host))
|
|
140
|
+
return Promise.resolve(true);
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
const req = httpGet({ host: '127.0.0.1', port, path: '/__selfcheck', agent: false }, (res) => {
|
|
143
|
+
let body = '';
|
|
144
|
+
res.setEncoding('utf8');
|
|
145
|
+
res.on('data', (c) => { body += c; if (body.length > 128)
|
|
146
|
+
req.destroy(); });
|
|
147
|
+
res.on('end', () => resolve(res.statusCode === 200 && body === DASHBOARD_SELF_NONCE));
|
|
148
|
+
});
|
|
149
|
+
req.setTimeout(2000, () => { req.destroy(); resolve(false); });
|
|
150
|
+
req.on('error', () => resolve(false));
|
|
151
|
+
});
|
|
152
|
+
}
|
|
121
153
|
/** Sign a loopback request to a daemon's write-link route. The daemon verifies
|
|
122
154
|
* with the same .dashboard-secret, so only a caller that can read the secret —
|
|
123
155
|
* the dashboard — can mint write tokens; a bare local process that only knows
|
|
@@ -807,6 +839,13 @@ const server = createServer(async (req, res) => {
|
|
|
807
839
|
if (url.pathname === '/__health') {
|
|
808
840
|
return jsonRes(res, 200, { ok: true });
|
|
809
841
|
}
|
|
842
|
+
// Loopback self-identification (no auth): echoes this process's nonce so the
|
|
843
|
+
// post-bind shadow check (listen-with-probe verifyBound) can distinguish our
|
|
844
|
+
// server from a process shadowing 127.0.0.1:port. Returns only the nonce.
|
|
845
|
+
if (url.pathname === '/__selfcheck') {
|
|
846
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
847
|
+
return res.end(DASHBOARD_SELF_NONCE);
|
|
848
|
+
}
|
|
810
849
|
if (await handleWebhookRoute(req, res, url, {
|
|
811
850
|
proxyToDaemon,
|
|
812
851
|
createLifecycleGroup: createLifecycleGroupForWebhook,
|
|
@@ -2026,6 +2065,11 @@ const server = createServer(async (req, res) => {
|
|
|
2026
2065
|
}
|
|
2027
2066
|
const creator = registry.getByAppId(pick.creatorLarkAppId);
|
|
2028
2067
|
const merged = new Set([...explicit, ...pick.userOpenIds]);
|
|
2068
|
+
// 跨 app 邀请通道:按 union_id 加人(open_id 是 app 作用域的,union_id 稳定,
|
|
2069
|
+
// 由 creator daemon 解析成本 app 的 open_id 再加)。平台「拉群」即走这条。
|
|
2070
|
+
const ownerUnionIds = Array.isArray(parsed.ownerUnionIds)
|
|
2071
|
+
? parsed.ownerUnionIds.filter((x) => typeof x === 'string')
|
|
2072
|
+
: [];
|
|
2029
2073
|
// Auto-invite/transfer/notify target: prefer the explicit open_id passed
|
|
2030
2074
|
// by the caller (rare API consumer use), else the creator bot's first
|
|
2031
2075
|
// resolved allowlist entry.
|
|
@@ -2034,6 +2078,7 @@ const server = createServer(async (req, res) => {
|
|
|
2034
2078
|
name: typeof parsed.name === 'string' ? parsed.name : undefined,
|
|
2035
2079
|
larkAppIds: selectedIds,
|
|
2036
2080
|
userOpenIds: [...merged],
|
|
2081
|
+
ownerUnionIds,
|
|
2037
2082
|
// Auto-transfer ownership to the auto-invited operator. Scope-safe
|
|
2038
2083
|
// because the open_id was sourced from the creator bot's own allowlist.
|
|
2039
2084
|
transferOwnerTo: autoInvited ?? undefined,
|
|
@@ -2249,6 +2294,7 @@ listenWithProbe({
|
|
|
2249
2294
|
port: config.dashboard.port,
|
|
2250
2295
|
host: config.dashboard.host,
|
|
2251
2296
|
portAvailable: dashboardPortAvailable,
|
|
2297
|
+
verifyBound: verifyDashboardBinding,
|
|
2252
2298
|
log: (m) => logger.warn(`[dashboard] ${m}`),
|
|
2253
2299
|
}).then((port) => {
|
|
2254
2300
|
boundDashboardPort = port;
|
|
@@ -2260,6 +2306,7 @@ listenWithProbe({
|
|
|
2260
2306
|
}
|
|
2261
2307
|
logger.info(`[dashboard] listening on ${config.dashboard.host}:${port}`);
|
|
2262
2308
|
startPlatformTunnelIfBound();
|
|
2309
|
+
watchPlatformBinding(); // bind 后无需重启 daemon,自动连接平台
|
|
2263
2310
|
}).catch((err) => {
|
|
2264
2311
|
logger.error(`[dashboard] could not bind near ${config.dashboard.host}:${config.dashboard.port} after probing — set BOTMUX_DASHBOARD_PORT to a free port. ${err.message}`);
|
|
2265
2312
|
process.exit(1);
|
|
@@ -2276,6 +2323,8 @@ const federationSync = setInterval(() => {
|
|
|
2276
2323
|
federationSync.unref();
|
|
2277
2324
|
// 中心化平台隧道(已绑定才启动;每台机器一个,跑在 dashboard 进程里)
|
|
2278
2325
|
let platformTunnel = null;
|
|
2326
|
+
// 当前隧道对应的绑定身份(platformUrl|machineId|machineToken);用于判断 bind 变化是否需要重连
|
|
2327
|
+
let platformBindingKey = null;
|
|
2279
2328
|
function readBotmuxVersion() {
|
|
2280
2329
|
try {
|
|
2281
2330
|
const pkg = JSON.parse(readFileSync(join(dirname(__dirname), 'package.json'), 'utf8'));
|
|
@@ -2285,25 +2334,91 @@ function readBotmuxVersion() {
|
|
|
2285
2334
|
return 'unknown';
|
|
2286
2335
|
}
|
|
2287
2336
|
}
|
|
2337
|
+
/** 读本机 bots-info.json,转成上报给平台的 bot 概要(人→机器→bot + 拉群用)。 */
|
|
2338
|
+
function readPlatformBotsInfo() {
|
|
2339
|
+
try {
|
|
2340
|
+
const fp = join(config.session.dataDir, 'bots-info.json');
|
|
2341
|
+
if (!existsSync(fp))
|
|
2342
|
+
return [];
|
|
2343
|
+
const entries = JSON.parse(readFileSync(fp, 'utf8'));
|
|
2344
|
+
if (!Array.isArray(entries))
|
|
2345
|
+
return [];
|
|
2346
|
+
return entries
|
|
2347
|
+
.map((e) => ({
|
|
2348
|
+
appId: e.larkAppId || '',
|
|
2349
|
+
openId: e.botOpenId ?? null,
|
|
2350
|
+
name: e.botName || e.larkAppId || 'bot',
|
|
2351
|
+
avatar: e.botAvatarUrl || undefined,
|
|
2352
|
+
cli: e.cliId,
|
|
2353
|
+
}))
|
|
2354
|
+
.filter((b) => b.appId);
|
|
2355
|
+
}
|
|
2356
|
+
catch {
|
|
2357
|
+
return [];
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2288
2360
|
function startPlatformTunnelIfBound() {
|
|
2289
2361
|
try {
|
|
2290
2362
|
const binding = readPlatformBinding();
|
|
2291
|
-
if (!binding)
|
|
2363
|
+
if (!binding) {
|
|
2364
|
+
platformBindingKey = null;
|
|
2292
2365
|
return;
|
|
2366
|
+
}
|
|
2293
2367
|
const version = readBotmuxVersion();
|
|
2294
2368
|
platformTunnel = startPlatformTunnelClient({
|
|
2295
2369
|
binding,
|
|
2296
2370
|
getDashboardPort: () => boundDashboardPort,
|
|
2297
2371
|
getDashboardToken: () => activeToken,
|
|
2298
2372
|
getVersion: () => version,
|
|
2373
|
+
getBots: () => readPlatformBotsInfo(),
|
|
2299
2374
|
log: (msg, extra) => logger.info(`[platform-tunnel] ${msg}${extra ? ' ' + JSON.stringify(extra) : ''}`),
|
|
2300
2375
|
});
|
|
2376
|
+
platformBindingKey = `${binding.platformUrl}|${binding.machineId}|${binding.machineToken}`;
|
|
2301
2377
|
logger.info(`[platform-tunnel] 绑定到 ${binding.platformUrl},启动隧道`);
|
|
2302
2378
|
}
|
|
2303
2379
|
catch (e) {
|
|
2304
2380
|
logger.warn(`[platform-tunnel] 启动失败: ${e.message}`);
|
|
2305
2381
|
}
|
|
2306
2382
|
}
|
|
2383
|
+
/**
|
|
2384
|
+
* 监听绑定文件变化:`botmux bind` 写入后无需重启 daemon,自动连接平台。
|
|
2385
|
+
* 只在绑定「身份」(platformUrl/machineId/machineToken) 变化时重连——团队成员变化也会改写该文件
|
|
2386
|
+
* (tunnel-client 自己写的),那种不重连,避免反复重启。
|
|
2387
|
+
*/
|
|
2388
|
+
function watchPlatformBinding() {
|
|
2389
|
+
let timer = null;
|
|
2390
|
+
const onChange = () => {
|
|
2391
|
+
if (timer)
|
|
2392
|
+
clearTimeout(timer);
|
|
2393
|
+
timer = setTimeout(() => {
|
|
2394
|
+
const b = readPlatformBinding();
|
|
2395
|
+
const key = b ? `${b.platformUrl}|${b.machineId}|${b.machineToken}` : null;
|
|
2396
|
+
if (key === platformBindingKey)
|
|
2397
|
+
return; // 没变(或仅团队变化)→ 不重连
|
|
2398
|
+
logger.info('[platform-tunnel] 检测到绑定变化,重连平台');
|
|
2399
|
+
try {
|
|
2400
|
+
platformTunnel?.stop();
|
|
2401
|
+
}
|
|
2402
|
+
catch {
|
|
2403
|
+
/* ignore */
|
|
2404
|
+
}
|
|
2405
|
+
platformTunnel = null;
|
|
2406
|
+
startPlatformTunnelIfBound();
|
|
2407
|
+
}, 800);
|
|
2408
|
+
};
|
|
2409
|
+
try {
|
|
2410
|
+
// 监听所在目录而非文件本身——绑定走原子写(临时文件 + rename),直接 watch 文件会在 rename 后失效
|
|
2411
|
+
const dir = dirname(PLATFORM_BINDING_PATH);
|
|
2412
|
+
const base = PLATFORM_BINDING_PATH.slice(dir.length + 1);
|
|
2413
|
+
fsWatch(dir, (_event, filename) => {
|
|
2414
|
+
if (!filename || filename === base)
|
|
2415
|
+
onChange();
|
|
2416
|
+
});
|
|
2417
|
+
}
|
|
2418
|
+
catch (e) {
|
|
2419
|
+
logger.warn(`[platform-tunnel] 绑定文件监听启动失败: ${e.message}`);
|
|
2420
|
+
}
|
|
2421
|
+
}
|
|
2307
2422
|
// Graceful shutdown
|
|
2308
2423
|
function shutdown() {
|
|
2309
2424
|
for (const off of subs.values())
|