botmux 2.91.1 → 2.93.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/README.en.md +1 -1
- package/README.md +1 -1
- package/dist/bot-registry.d.ts +1 -1
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +14 -11
- 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/core/session-manager.js +2 -2
- package/dist/core/session-manager.js.map +1 -1
- package/dist/daemon.d.ts +5 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +24 -6
- 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 +6 -4
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard-web/app.js +6 -5
- package/dist/dashboard.js +82 -23
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.js +9 -9
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.js +9 -9
- 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/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/skills/definitions.js +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,8 +52,7 @@ 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 {
|
|
56
|
-
import { readPlatformBinding, PLATFORM_BINDING_PATH } from './platform/binding.js';
|
|
55
|
+
import { readPlatformBinding } from './platform/binding.js';
|
|
57
56
|
import { startPlatformTunnelClient } from './platform/tunnel-client.js';
|
|
58
57
|
import { cleanupIdleSessions, parseIdleCleanupHours } from './dashboard/session-cleanup.js';
|
|
59
58
|
const SECRET_PATH = join(homedir(), '.botmux', '.dashboard-secret');
|
|
@@ -119,6 +118,37 @@ function dashboardPortAvailable(port) {
|
|
|
119
118
|
// wildcard bind still succeeds, causing CLI HMAC calls to hit that process.
|
|
120
119
|
return tcpPortAvailable('127.0.0.1', port);
|
|
121
120
|
}
|
|
121
|
+
// Per-process random marker served at /__selfcheck. Lets verifyDashboardBinding
|
|
122
|
+
// confirm a loopback request to our just-bound wildcard port reaches THIS
|
|
123
|
+
// process and not a shadow holding 127.0.0.1:port. The value is meaningless to
|
|
124
|
+
// anyone else, so exposing it is safe.
|
|
125
|
+
const DASHBOARD_SELF_NONCE = randomBytes(16).toString('hex');
|
|
126
|
+
/**
|
|
127
|
+
* Post-bind loopback identity check handed to listenWithProbe (verifyBound).
|
|
128
|
+
* dashboardPortAvailable is a PRE-bind gate, but on macOS a loopback occupant
|
|
129
|
+
* can appear in the race window between that check and the wildcard listen, and
|
|
130
|
+
* a 0.0.0.0 bind succeeds anyway while loopback routing favours the occupant —
|
|
131
|
+
* so the dashboard would advertise a port it doesn't actually own on loopback.
|
|
132
|
+
* This runs AFTER listen: dial 127.0.0.1:port/__selfcheck and require OUR nonce
|
|
133
|
+
* back. A shadow answers with its own body/404 → reject → listenWithProbe steps
|
|
134
|
+
* up. Number-independent: it works no matter which port or who is shadowing.
|
|
135
|
+
* Loopback-host binds can't be shadowed, so they short-circuit to true.
|
|
136
|
+
*/
|
|
137
|
+
function verifyDashboardBinding(port) {
|
|
138
|
+
if (!isWildcardBindHost(config.dashboard.host))
|
|
139
|
+
return Promise.resolve(true);
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
const req = httpGet({ host: '127.0.0.1', port, path: '/__selfcheck', agent: false }, (res) => {
|
|
142
|
+
let body = '';
|
|
143
|
+
res.setEncoding('utf8');
|
|
144
|
+
res.on('data', (c) => { body += c; if (body.length > 128)
|
|
145
|
+
req.destroy(); });
|
|
146
|
+
res.on('end', () => resolve(res.statusCode === 200 && body === DASHBOARD_SELF_NONCE));
|
|
147
|
+
});
|
|
148
|
+
req.setTimeout(2000, () => { req.destroy(); resolve(false); });
|
|
149
|
+
req.on('error', () => resolve(false));
|
|
150
|
+
});
|
|
151
|
+
}
|
|
122
152
|
/** Sign a loopback request to a daemon's write-link route. The daemon verifies
|
|
123
153
|
* with the same .dashboard-secret, so only a caller that can read the secret —
|
|
124
154
|
* the dashboard — can mint write tokens; a bare local process that only knows
|
|
@@ -808,6 +838,13 @@ const server = createServer(async (req, res) => {
|
|
|
808
838
|
if (url.pathname === '/__health') {
|
|
809
839
|
return jsonRes(res, 200, { ok: true });
|
|
810
840
|
}
|
|
841
|
+
// Loopback self-identification (no auth): echoes this process's nonce so the
|
|
842
|
+
// post-bind shadow check (listen-with-probe verifyBound) can distinguish our
|
|
843
|
+
// server from a process shadowing 127.0.0.1:port. Returns only the nonce.
|
|
844
|
+
if (url.pathname === '/__selfcheck') {
|
|
845
|
+
res.writeHead(200, { 'content-type': 'text/plain' });
|
|
846
|
+
return res.end(DASHBOARD_SELF_NONCE);
|
|
847
|
+
}
|
|
811
848
|
if (await handleWebhookRoute(req, res, url, {
|
|
812
849
|
proxyToDaemon,
|
|
813
850
|
createLifecycleGroup: createLifecycleGroupForWebhook,
|
|
@@ -2027,6 +2064,11 @@ const server = createServer(async (req, res) => {
|
|
|
2027
2064
|
}
|
|
2028
2065
|
const creator = registry.getByAppId(pick.creatorLarkAppId);
|
|
2029
2066
|
const merged = new Set([...explicit, ...pick.userOpenIds]);
|
|
2067
|
+
// 跨 app 邀请通道:按 union_id 加人(open_id 是 app 作用域的,union_id 稳定,
|
|
2068
|
+
// 由 creator daemon 解析成本 app 的 open_id 再加)。平台「拉群」即走这条。
|
|
2069
|
+
const ownerUnionIds = Array.isArray(parsed.ownerUnionIds)
|
|
2070
|
+
? parsed.ownerUnionIds.filter((x) => typeof x === 'string')
|
|
2071
|
+
: [];
|
|
2030
2072
|
// Auto-invite/transfer/notify target: prefer the explicit open_id passed
|
|
2031
2073
|
// by the caller (rare API consumer use), else the creator bot's first
|
|
2032
2074
|
// resolved allowlist entry.
|
|
@@ -2035,6 +2077,7 @@ const server = createServer(async (req, res) => {
|
|
|
2035
2077
|
name: typeof parsed.name === 'string' ? parsed.name : undefined,
|
|
2036
2078
|
larkAppIds: selectedIds,
|
|
2037
2079
|
userOpenIds: [...merged],
|
|
2080
|
+
ownerUnionIds,
|
|
2038
2081
|
// Auto-transfer ownership to the auto-invited operator. Scope-safe
|
|
2039
2082
|
// because the open_id was sourced from the creator bot's own allowlist.
|
|
2040
2083
|
transferOwnerTo: autoInvited ?? undefined,
|
|
@@ -2250,6 +2293,7 @@ listenWithProbe({
|
|
|
2250
2293
|
port: config.dashboard.port,
|
|
2251
2294
|
host: config.dashboard.host,
|
|
2252
2295
|
portAvailable: dashboardPortAvailable,
|
|
2296
|
+
verifyBound: verifyDashboardBinding,
|
|
2253
2297
|
log: (m) => logger.warn(`[dashboard] ${m}`),
|
|
2254
2298
|
}).then((port) => {
|
|
2255
2299
|
boundDashboardPort = port;
|
|
@@ -2289,6 +2333,29 @@ function readBotmuxVersion() {
|
|
|
2289
2333
|
return 'unknown';
|
|
2290
2334
|
}
|
|
2291
2335
|
}
|
|
2336
|
+
/** 读本机 bots-info.json,转成上报给平台的 bot 概要(人→机器→bot + 拉群用)。 */
|
|
2337
|
+
function readPlatformBotsInfo() {
|
|
2338
|
+
try {
|
|
2339
|
+
const fp = join(config.session.dataDir, 'bots-info.json');
|
|
2340
|
+
if (!existsSync(fp))
|
|
2341
|
+
return [];
|
|
2342
|
+
const entries = JSON.parse(readFileSync(fp, 'utf8'));
|
|
2343
|
+
if (!Array.isArray(entries))
|
|
2344
|
+
return [];
|
|
2345
|
+
return entries
|
|
2346
|
+
.map((e) => ({
|
|
2347
|
+
appId: e.larkAppId || '',
|
|
2348
|
+
openId: e.botOpenId ?? null,
|
|
2349
|
+
name: e.botName || e.larkAppId || 'bot',
|
|
2350
|
+
avatar: e.botAvatarUrl || undefined,
|
|
2351
|
+
cli: e.cliId,
|
|
2352
|
+
}))
|
|
2353
|
+
.filter((b) => b.appId);
|
|
2354
|
+
}
|
|
2355
|
+
catch {
|
|
2356
|
+
return [];
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2292
2359
|
function startPlatformTunnelIfBound() {
|
|
2293
2360
|
try {
|
|
2294
2361
|
const binding = readPlatformBinding();
|
|
@@ -2302,6 +2369,7 @@ function startPlatformTunnelIfBound() {
|
|
|
2302
2369
|
getDashboardPort: () => boundDashboardPort,
|
|
2303
2370
|
getDashboardToken: () => activeToken,
|
|
2304
2371
|
getVersion: () => version,
|
|
2372
|
+
getBots: () => readPlatformBotsInfo(),
|
|
2305
2373
|
log: (msg, extra) => logger.info(`[platform-tunnel] ${msg}${extra ? ' ' + JSON.stringify(extra) : ''}`),
|
|
2306
2374
|
});
|
|
2307
2375
|
platformBindingKey = `${binding.platformUrl}|${binding.machineId}|${binding.machineToken}`;
|
|
@@ -2317,15 +2385,15 @@ function startPlatformTunnelIfBound() {
|
|
|
2317
2385
|
* (tunnel-client 自己写的),那种不重连,避免反复重启。
|
|
2318
2386
|
*/
|
|
2319
2387
|
function watchPlatformBinding() {
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2388
|
+
// 每 3s 轮询绑定文件——`botmux bind` 写入后自动连接,无需重启 daemon。
|
|
2389
|
+
// 用轮询而非 fs.watch:绑定走原子写(临时文件 + rename),fs.watch 对 rename 在不同
|
|
2390
|
+
// 文件系统上不可靠(实测某些机器收不到事件),轮询在任何环境都稳。
|
|
2391
|
+
setInterval(() => {
|
|
2392
|
+
try {
|
|
2325
2393
|
const b = readPlatformBinding();
|
|
2326
2394
|
const key = b ? `${b.platformUrl}|${b.machineId}|${b.machineToken}` : null;
|
|
2327
2395
|
if (key === platformBindingKey)
|
|
2328
|
-
return; //
|
|
2396
|
+
return; // 绑定身份没变(团队成员变化不在此 key 内)→ 不重连
|
|
2329
2397
|
logger.info('[platform-tunnel] 检测到绑定变化,重连平台');
|
|
2330
2398
|
try {
|
|
2331
2399
|
platformTunnel?.stop();
|
|
@@ -2335,20 +2403,11 @@ function watchPlatformBinding() {
|
|
|
2335
2403
|
}
|
|
2336
2404
|
platformTunnel = null;
|
|
2337
2405
|
startPlatformTunnelIfBound();
|
|
2338
|
-
}
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
const base = PLATFORM_BINDING_PATH.slice(dir.length + 1);
|
|
2344
|
-
fsWatch(dir, (_event, filename) => {
|
|
2345
|
-
if (!filename || filename === base)
|
|
2346
|
-
onChange();
|
|
2347
|
-
});
|
|
2348
|
-
}
|
|
2349
|
-
catch (e) {
|
|
2350
|
-
logger.warn(`[platform-tunnel] 绑定文件监听启动失败: ${e.message}`);
|
|
2351
|
-
}
|
|
2406
|
+
}
|
|
2407
|
+
catch {
|
|
2408
|
+
/* ignore */
|
|
2409
|
+
}
|
|
2410
|
+
}, 3000).unref();
|
|
2352
2411
|
}
|
|
2353
2412
|
// Graceful shutdown
|
|
2354
2413
|
function shutdown() {
|