botmux 2.2.7 → 2.3.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.en.md +63 -13
- package/README.md +52 -14
- package/dist/adapters/backend/tmux-backend.d.ts +8 -0
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +18 -0
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/cli/aiden.d.ts.map +1 -1
- package/dist/adapters/cli/aiden.js +0 -40
- package/dist/adapters/cli/aiden.js.map +1 -1
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +21 -67
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/coco.d.ts.map +1 -1
- package/dist/adapters/cli/coco.js +0 -33
- package/dist/adapters/cli/coco.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +0 -27
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/gemini.d.ts.map +1 -1
- package/dist/adapters/cli/gemini.js +1 -29
- package/dist/adapters/cli/gemini.js.map +1 -1
- package/dist/adapters/cli/opencode.d.ts.map +1 -1
- package/dist/adapters/cli/opencode.js +1 -44
- package/dist/adapters/cli/opencode.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +11 -8
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/cli.js +737 -16
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +16 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +30 -0
- package/dist/config.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +8 -4
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/scheduler.d.ts +38 -16
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +335 -149
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/session-manager.d.ts +6 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +105 -16
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +26 -4
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +6 -0
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +15 -3
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +233 -31
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +49 -10
- package/dist/daemon.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +29 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +241 -55
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts +1 -0
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +195 -40
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/services/schedule-store.d.ts +20 -3
- package/dist/services/schedule-store.d.ts.map +1 -1
- package/dist/services/schedule-store.js +140 -16
- package/dist/services/schedule-store.js.map +1 -1
- package/dist/skills/definitions.d.ts +17 -0
- package/dist/skills/definitions.d.ts.map +1 -0
- package/dist/skills/definitions.js +254 -0
- package/dist/skills/definitions.js.map +1 -0
- package/dist/skills/installer.d.ts +9 -0
- package/dist/skills/installer.d.ts.map +1 -0
- package/dist/skills/installer.js +42 -0
- package/dist/skills/installer.js.map +1 -0
- package/dist/types.d.ts +84 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +1 -5
- package/dist/types.js.map +1 -1
- package/dist/utils/lark-upload.d.ts +2 -0
- package/dist/utils/lark-upload.d.ts.map +1 -0
- package/dist/utils/lark-upload.js +27 -0
- package/dist/utils/lark-upload.js.map +1 -0
- package/dist/utils/screen-analyzer.d.ts +67 -0
- package/dist/utils/screen-analyzer.d.ts.map +1 -0
- package/dist/utils/screen-analyzer.js +279 -0
- package/dist/utils/screen-analyzer.js.map +1 -0
- package/dist/utils/screenshot-renderer.d.ts +11 -0
- package/dist/utils/screenshot-renderer.d.ts.map +1 -0
- package/dist/utils/screenshot-renderer.js +225 -0
- package/dist/utils/screenshot-renderer.js.map +1 -0
- package/dist/utils/terminal-renderer.d.ts +30 -0
- package/dist/utils/terminal-renderer.d.ts.map +1 -1
- package/dist/utils/terminal-renderer.js +25 -0
- package/dist/utils/terminal-renderer.js.map +1 -1
- package/dist/worker.js +372 -14
- package/dist/worker.js.map +1 -1
- package/package.json +2 -5
- package/dist/index.d.ts +0 -3
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js +0 -16
- package/dist/index.js.map +0 -1
- package/dist/server.d.ts +0 -3
- package/dist/server.d.ts.map +0 -1
- package/dist/server.js +0 -133
- package/dist/server.js.map +0 -1
- package/dist/tools/get-thread-messages.d.ts +0 -26
- package/dist/tools/get-thread-messages.d.ts.map +0 -1
- package/dist/tools/get-thread-messages.js +0 -38
- package/dist/tools/get-thread-messages.js.map +0 -1
- package/dist/tools/index.d.ts +0 -9
- package/dist/tools/index.d.ts.map +0 -1
- package/dist/tools/index.js +0 -10
- package/dist/tools/index.js.map +0 -1
- package/dist/tools/list-bots.d.ts +0 -40
- package/dist/tools/list-bots.d.ts.map +0 -1
- package/dist/tools/list-bots.js +0 -77
- package/dist/tools/list-bots.js.map +0 -1
- package/dist/tools/send-to-thread.d.ts +0 -46
- package/dist/tools/send-to-thread.d.ts.map +0 -1
- package/dist/tools/send-to-thread.js +0 -275
- package/dist/tools/send-to-thread.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -21,8 +21,10 @@ import { join, dirname } from 'node:path';
|
|
|
21
21
|
import { homedir } from 'node:os';
|
|
22
22
|
import { fileURLToPath } from 'node:url';
|
|
23
23
|
import { createInterface } from 'node:readline';
|
|
24
|
+
import { createRequire } from 'node:module';
|
|
24
25
|
const __filename = fileURLToPath(import.meta.url);
|
|
25
26
|
const __dirname = dirname(__filename);
|
|
27
|
+
const require = createRequire(import.meta.url);
|
|
26
28
|
// Package root is one level up from dist/
|
|
27
29
|
const PKG_ROOT = dirname(__dirname);
|
|
28
30
|
const CONFIG_DIR = join(homedir(), '.botmux');
|
|
@@ -31,25 +33,48 @@ const DATA_DIR = join(CONFIG_DIR, 'data');
|
|
|
31
33
|
const LOG_DIR = join(CONFIG_DIR, 'logs');
|
|
32
34
|
const BOTS_JSON_FILE = join(CONFIG_DIR, 'bots.json');
|
|
33
35
|
const PM2_NAME = 'botmux';
|
|
36
|
+
/**
|
|
37
|
+
* Dedicated PM2_HOME for botmux. Isolates our pm2 daemon state from any
|
|
38
|
+
* other pm2 installation on the machine (e.g. the one bundled in IDE
|
|
39
|
+
* remote-ssh extensions). Prevents stale ProcessContainerFork.js paths
|
|
40
|
+
* when those external pm2 installations get moved or removed.
|
|
41
|
+
*/
|
|
42
|
+
const PM2_HOME = join(CONFIG_DIR, 'pm2');
|
|
34
43
|
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
35
44
|
function ensureConfigDir() {
|
|
36
|
-
for (const dir of [CONFIG_DIR, DATA_DIR, LOG_DIR]) {
|
|
45
|
+
for (const dir of [CONFIG_DIR, DATA_DIR, LOG_DIR, PM2_HOME]) {
|
|
37
46
|
if (!existsSync(dir))
|
|
38
47
|
mkdirSync(dir, { recursive: true });
|
|
39
48
|
}
|
|
40
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the pm2 CLI script path. Uses require.resolve so it always lands
|
|
52
|
+
* on the pm2 bundled with this package, never on a PATH-resolved pm2 that
|
|
53
|
+
* may belong to an unrelated installation (e.g. IDE remote extensions).
|
|
54
|
+
*/
|
|
41
55
|
function pm2Bin() {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
//
|
|
56
|
+
try {
|
|
57
|
+
return require.resolve('pm2/bin/pm2');
|
|
58
|
+
}
|
|
59
|
+
catch { /* fall through */ }
|
|
60
|
+
// Fallbacks for unusual installation layouts
|
|
61
|
+
const direct = join(PKG_ROOT, 'node_modules', 'pm2', 'bin', 'pm2');
|
|
62
|
+
if (existsSync(direct))
|
|
63
|
+
return direct;
|
|
64
|
+
const symlink = join(PKG_ROOT, 'node_modules', '.bin', 'pm2');
|
|
65
|
+
if (existsSync(symlink))
|
|
66
|
+
return symlink;
|
|
47
67
|
return 'pm2';
|
|
48
68
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
69
|
+
/** Env for pm2 invocations with an isolated PM2_HOME. */
|
|
70
|
+
function pm2Env(home = PM2_HOME) {
|
|
71
|
+
return { ...process.env, PM2_HOME: home };
|
|
72
|
+
}
|
|
73
|
+
function runPm2(args, inherit = true, home = PM2_HOME) {
|
|
74
|
+
execSync(`${pm2Bin()} ${args.join(' ')}`, {
|
|
75
|
+
stdio: inherit ? 'inherit' : 'pipe',
|
|
76
|
+
env: pm2Env(home),
|
|
77
|
+
});
|
|
53
78
|
}
|
|
54
79
|
function loadBotsJson() {
|
|
55
80
|
if (existsSync(BOTS_JSON_FILE)) {
|
|
@@ -248,6 +273,7 @@ function cmdStart() {
|
|
|
248
273
|
process.exit(1);
|
|
249
274
|
}
|
|
250
275
|
ensureConfigDir();
|
|
276
|
+
cleanupLegacyPm2();
|
|
251
277
|
const cfg = ecosystemConfig();
|
|
252
278
|
runPm2(['start', cfg]);
|
|
253
279
|
const bots = loadBotsJson();
|
|
@@ -256,15 +282,24 @@ function cmdStart() {
|
|
|
256
282
|
console.log(` 日志: botmux logs`);
|
|
257
283
|
console.log(` 状态: botmux status`);
|
|
258
284
|
}
|
|
259
|
-
/** Delete all pm2 processes matching botmux / botmux-* */
|
|
260
|
-
function deleteAllBotmuxProcesses() {
|
|
285
|
+
/** Delete all pm2 processes matching botmux / botmux-* under the given PM2_HOME. */
|
|
286
|
+
function deleteAllBotmuxProcesses(home = PM2_HOME) {
|
|
261
287
|
try {
|
|
262
|
-
const output = execSync(`${pm2Bin()} jlist`, {
|
|
288
|
+
const output = execSync(`${pm2Bin()} jlist`, {
|
|
289
|
+
encoding: 'utf-8',
|
|
290
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
291
|
+
env: pm2Env(home),
|
|
292
|
+
timeout: 10_000,
|
|
293
|
+
});
|
|
263
294
|
const apps = JSON.parse(output);
|
|
264
295
|
for (const app of apps) {
|
|
265
296
|
if (app.name === PM2_NAME || app.name.startsWith(`${PM2_NAME}-`)) {
|
|
266
297
|
try {
|
|
267
|
-
execSync(`${pm2Bin()} delete ${app.name}`, {
|
|
298
|
+
execSync(`${pm2Bin()} delete ${app.name}`, {
|
|
299
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
300
|
+
env: pm2Env(home),
|
|
301
|
+
timeout: 10_000,
|
|
302
|
+
});
|
|
268
303
|
}
|
|
269
304
|
catch { /* */ }
|
|
270
305
|
}
|
|
@@ -272,10 +307,49 @@ function deleteAllBotmuxProcesses() {
|
|
|
272
307
|
}
|
|
273
308
|
catch { /* pm2 not running or no apps */ }
|
|
274
309
|
}
|
|
310
|
+
/**
|
|
311
|
+
* One-time migration for users upgrading from versions that used the default
|
|
312
|
+
* ~/.pm2 directory. Removes any lingering botmux-* processes registered under
|
|
313
|
+
* the legacy home so the new dedicated PM2_HOME becomes the sole source of
|
|
314
|
+
* truth. Only touches processes named `botmux` or `botmux-*` — the user's
|
|
315
|
+
* unrelated pm2 apps are left untouched. No-op on fresh installs.
|
|
316
|
+
*/
|
|
317
|
+
function cleanupLegacyPm2() {
|
|
318
|
+
const legacyHome = join(homedir(), '.pm2');
|
|
319
|
+
if (legacyHome === PM2_HOME)
|
|
320
|
+
return false;
|
|
321
|
+
const legacyPidFile = join(legacyHome, 'pm2.pid');
|
|
322
|
+
if (!existsSync(legacyPidFile))
|
|
323
|
+
return false;
|
|
324
|
+
let legacyPid = 0;
|
|
325
|
+
try {
|
|
326
|
+
legacyPid = parseInt(readFileSync(legacyPidFile, 'utf-8').trim(), 10);
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
if (!legacyPid)
|
|
332
|
+
return false;
|
|
333
|
+
// If the legacy daemon isn't alive anymore there's nothing to clean.
|
|
334
|
+
try {
|
|
335
|
+
process.kill(legacyPid, 0);
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
deleteAllBotmuxProcesses(legacyHome);
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
275
343
|
function cmdStop() {
|
|
344
|
+
cleanupLegacyPm2();
|
|
276
345
|
let stopped = false;
|
|
277
346
|
try {
|
|
278
|
-
const output = execSync(`${pm2Bin()} jlist`, {
|
|
347
|
+
const output = execSync(`${pm2Bin()} jlist`, {
|
|
348
|
+
encoding: 'utf-8',
|
|
349
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
350
|
+
env: pm2Env(),
|
|
351
|
+
timeout: 10_000,
|
|
352
|
+
});
|
|
279
353
|
const apps = JSON.parse(output);
|
|
280
354
|
for (const app of apps) {
|
|
281
355
|
if (app.name === PM2_NAME || app.name.startsWith(`${PM2_NAME}-`)) {
|
|
@@ -298,12 +372,56 @@ function cmdRestart() {
|
|
|
298
372
|
process.exit(1);
|
|
299
373
|
}
|
|
300
374
|
ensureConfigDir();
|
|
375
|
+
cleanupLegacyPm2();
|
|
301
376
|
// Delete all botmux processes (handles both old single-process and new multi-process)
|
|
302
377
|
deleteAllBotmuxProcesses();
|
|
303
378
|
const cfg = ecosystemConfig();
|
|
304
379
|
runPm2(['start', cfg]);
|
|
305
380
|
}
|
|
381
|
+
/**
|
|
382
|
+
* If a legacy ~/.pm2 daemon with botmux processes still exists alongside our
|
|
383
|
+
* new PM2_HOME, warn the user so read-only commands (status/logs) don't
|
|
384
|
+
* silently show an empty new home while the old daemon keeps running.
|
|
385
|
+
*/
|
|
386
|
+
function warnIfLegacyBotmuxAlive() {
|
|
387
|
+
const legacyHome = join(homedir(), '.pm2');
|
|
388
|
+
if (legacyHome === PM2_HOME)
|
|
389
|
+
return;
|
|
390
|
+
const legacyPidFile = join(legacyHome, 'pm2.pid');
|
|
391
|
+
if (!existsSync(legacyPidFile))
|
|
392
|
+
return;
|
|
393
|
+
let legacyPid = 0;
|
|
394
|
+
try {
|
|
395
|
+
legacyPid = parseInt(readFileSync(legacyPidFile, 'utf-8').trim(), 10);
|
|
396
|
+
}
|
|
397
|
+
catch {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (!legacyPid)
|
|
401
|
+
return;
|
|
402
|
+
try {
|
|
403
|
+
process.kill(legacyPid, 0);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
const output = execSync(`${pm2Bin()} jlist`, {
|
|
410
|
+
encoding: 'utf-8',
|
|
411
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
412
|
+
env: pm2Env(legacyHome),
|
|
413
|
+
timeout: 10_000,
|
|
414
|
+
});
|
|
415
|
+
const apps = JSON.parse(output);
|
|
416
|
+
const hasBotmux = apps.some(a => a.name === PM2_NAME || a.name.startsWith(`${PM2_NAME}-`));
|
|
417
|
+
if (hasBotmux) {
|
|
418
|
+
console.warn('⚠️ 检测到旧版 PM2_HOME (~/.pm2) 下仍有 botmux 进程,运行 `botmux restart` 完成迁移。\n');
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch { /* ignore */ }
|
|
422
|
+
}
|
|
306
423
|
function cmdLogs() {
|
|
424
|
+
warnIfLegacyBotmuxAlive();
|
|
307
425
|
const lines = process.argv.includes('--lines')
|
|
308
426
|
? process.argv[process.argv.indexOf('--lines') + 1] || '50'
|
|
309
427
|
: '50';
|
|
@@ -323,11 +441,12 @@ function cmdLogs() {
|
|
|
323
441
|
// Use spawn for streaming output
|
|
324
442
|
const child = spawn(pm2Bin(), ['logs', target, '--lines', lines], {
|
|
325
443
|
stdio: 'inherit',
|
|
326
|
-
env:
|
|
444
|
+
env: pm2Env(),
|
|
327
445
|
});
|
|
328
446
|
child.on('exit', code => process.exit(code ?? 0));
|
|
329
447
|
}
|
|
330
448
|
function cmdStatus() {
|
|
449
|
+
warnIfLegacyBotmuxAlive();
|
|
331
450
|
runPm2(['status']);
|
|
332
451
|
}
|
|
333
452
|
function cmdUpgrade() {
|
|
@@ -957,10 +1076,593 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
|
|
|
957
1076
|
delete all 关闭所有活跃会话
|
|
958
1077
|
delete stopped 清理所有进程已退出的僵尸会话
|
|
959
1078
|
|
|
1079
|
+
定时任务(可在 CLI 会话内自动推断 chat):
|
|
1080
|
+
schedule list 列出所有任务
|
|
1081
|
+
schedule add <schedule> <prompt> 添加任务(ex: "30m" / "every 2h" / "每日9:00" / "0 9 * * *")
|
|
1082
|
+
schedule remove <id> 删除任务
|
|
1083
|
+
schedule pause|resume <id> 暂停/恢复
|
|
1084
|
+
schedule run <id> 标记立即执行
|
|
1085
|
+
|
|
1086
|
+
飞书消息(在 CLI 会话内自动推断 session):
|
|
1087
|
+
send [content] 发消息到当前话题(支持 stdin / --content-file)
|
|
1088
|
+
--images <path> 内联图片(可重复)
|
|
1089
|
+
--files <path> 附件(可重复)
|
|
1090
|
+
--mention <open_id:name> @提及(可重复)
|
|
1091
|
+
bots list 列出当前群聊中的机器人(含 open_id)
|
|
1092
|
+
thread messages [--limit N] 拉取当前话题的消息历史 (JSON)
|
|
1093
|
+
|
|
960
1094
|
配置目录: ~/.botmux/
|
|
961
1095
|
文档: https://github.com/deepcoldy/botmux
|
|
962
1096
|
`);
|
|
963
1097
|
}
|
|
1098
|
+
// ─── Schedule subcommands ────────────────────────────────────────────────────
|
|
1099
|
+
/**
|
|
1100
|
+
* Walk the process tree looking for a CLI-pid marker written by the botmux
|
|
1101
|
+
* worker. Returns the sessionId stored in the marker (or '' if empty/legacy).
|
|
1102
|
+
*
|
|
1103
|
+
* This mirrors server.ts:findAncestorCliMarker but is local to cli.ts so
|
|
1104
|
+
* subcommands invoked from inside an agent session can auto-detect which
|
|
1105
|
+
* session they belong to.
|
|
1106
|
+
*/
|
|
1107
|
+
function findAncestorSessionId() {
|
|
1108
|
+
const dataDir = resolveDataDir();
|
|
1109
|
+
const markersDir = join(dataDir, '.botmux-cli-pids');
|
|
1110
|
+
if (!existsSync(markersDir))
|
|
1111
|
+
return null;
|
|
1112
|
+
let pid = process.ppid;
|
|
1113
|
+
for (let depth = 0; depth < 8 && pid > 1; depth++) {
|
|
1114
|
+
const markerPath = join(markersDir, String(pid));
|
|
1115
|
+
if (existsSync(markerPath)) {
|
|
1116
|
+
try {
|
|
1117
|
+
return readFileSync(markerPath, 'utf-8').trim();
|
|
1118
|
+
}
|
|
1119
|
+
catch {
|
|
1120
|
+
return '';
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
try {
|
|
1124
|
+
const out = execSync(`ps -o ppid= -p ${pid}`, { encoding: 'utf-8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
1125
|
+
pid = parseInt(out, 10);
|
|
1126
|
+
if (isNaN(pid))
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
catch {
|
|
1130
|
+
break;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return null;
|
|
1134
|
+
}
|
|
1135
|
+
/** Detect current session info from ancestor marker + session files. */
|
|
1136
|
+
function detectCurrentSession() {
|
|
1137
|
+
const sid = findAncestorSessionId();
|
|
1138
|
+
if (!sid)
|
|
1139
|
+
return null;
|
|
1140
|
+
const sessions = loadSessions();
|
|
1141
|
+
const s = sessions.get(sid);
|
|
1142
|
+
if (!s)
|
|
1143
|
+
return null;
|
|
1144
|
+
return {
|
|
1145
|
+
sessionId: s.sessionId,
|
|
1146
|
+
chatId: s.chatId,
|
|
1147
|
+
rootMessageId: s.rootMessageId,
|
|
1148
|
+
workingDir: s.workingDir,
|
|
1149
|
+
larkAppId: s.larkAppId,
|
|
1150
|
+
chatType: s.chatType,
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
/** Pick a value from --flag <value> or --flag=value style args. */
|
|
1154
|
+
function argValue(args, ...flags) {
|
|
1155
|
+
for (let i = 0; i < args.length; i++) {
|
|
1156
|
+
const a = args[i];
|
|
1157
|
+
for (const f of flags) {
|
|
1158
|
+
if (a === f && i + 1 < args.length)
|
|
1159
|
+
return args[i + 1];
|
|
1160
|
+
if (a.startsWith(f + '='))
|
|
1161
|
+
return a.slice(f.length + 1);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
return undefined;
|
|
1165
|
+
}
|
|
1166
|
+
function argFlag(args, flag) {
|
|
1167
|
+
return args.includes(flag);
|
|
1168
|
+
}
|
|
1169
|
+
/** Extract positional args, skipping --flag and the value that follows it
|
|
1170
|
+
* (for --flag <value> style). --flag=value style is self-contained. */
|
|
1171
|
+
function positionals(args) {
|
|
1172
|
+
const out = [];
|
|
1173
|
+
for (let i = 0; i < args.length; i++) {
|
|
1174
|
+
const a = args[i];
|
|
1175
|
+
if (a.startsWith('--')) {
|
|
1176
|
+
if (!a.includes('=') && i + 1 < args.length)
|
|
1177
|
+
i++; // skip value
|
|
1178
|
+
continue;
|
|
1179
|
+
}
|
|
1180
|
+
out.push(a);
|
|
1181
|
+
}
|
|
1182
|
+
return out;
|
|
1183
|
+
}
|
|
1184
|
+
async function cmdSchedule(sub, rest) {
|
|
1185
|
+
// Ensure SESSION_DATA_DIR points at the daemon's data dir so schedule-store
|
|
1186
|
+
// writes to the right file even when invoked outside the daemon env.
|
|
1187
|
+
process.env.SESSION_DATA_DIR ??= resolveDataDir();
|
|
1188
|
+
const scheduler = await import('./core/scheduler.js');
|
|
1189
|
+
const scheduleStore = await import('./services/schedule-store.js');
|
|
1190
|
+
if (!sub || sub === 'list' || sub === 'ls') {
|
|
1191
|
+
const tasks = scheduleStore.listTasks();
|
|
1192
|
+
if (tasks.length === 0) {
|
|
1193
|
+
console.log('暂无定时任务。\n\n用法:\n botmux schedule add "每日17:50" "帮我看AI新闻"\n botmux schedule add "every 2h" "检查构建"\n botmux schedule add "0 9 * * *" "每天早安"');
|
|
1194
|
+
return;
|
|
1195
|
+
}
|
|
1196
|
+
const filter = argValue(rest, '--chat-id');
|
|
1197
|
+
const filtered = filter ? tasks.filter(t => t.chatId === filter) : tasks;
|
|
1198
|
+
console.log(`定时任务 (${filtered.length}${filter ? '/' + tasks.length : ''}):\n`);
|
|
1199
|
+
for (const t of filtered) {
|
|
1200
|
+
const status = t.enabled ? '✅' : '⏸️';
|
|
1201
|
+
const next = t.nextRunAt ? new Date(t.nextRunAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) : '—';
|
|
1202
|
+
const last = t.lastRunAt ? new Date(t.lastRunAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) : '—';
|
|
1203
|
+
const display = t.parsed?.display ?? t.schedule;
|
|
1204
|
+
const prompt = t.prompt ?? '';
|
|
1205
|
+
const chatId = t.chatId ?? '—';
|
|
1206
|
+
const rootId = t.rootMessageId ?? '—';
|
|
1207
|
+
console.log(`${status} [${t.id}] ${display} | ${t.name}`);
|
|
1208
|
+
console.log(` prompt: ${prompt.length > 60 ? prompt.slice(0, 60) + '…' : prompt}`);
|
|
1209
|
+
console.log(` chat: ${chatId.slice(0, 12)}… thread: ${rootId.slice(0, 16)}…`);
|
|
1210
|
+
console.log(` next: ${next} last: ${last}${t.lastStatus === 'error' ? ' ❌' : ''}`);
|
|
1211
|
+
console.log('');
|
|
1212
|
+
}
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (sub === 'add') {
|
|
1216
|
+
const [rawSchedule, ...promptParts] = positionals(rest);
|
|
1217
|
+
if (!rawSchedule) {
|
|
1218
|
+
console.error('用法: botmux schedule add <schedule> <prompt> [--name NAME] [--chat-id CHAT] [--root-msg-id ROOT] [--lark-app-id APP] [--workdir DIR]');
|
|
1219
|
+
process.exit(1);
|
|
1220
|
+
}
|
|
1221
|
+
// prompt may come from positional or --prompt flag
|
|
1222
|
+
const promptArg = argValue(rest, '--prompt') ?? promptParts.join(' ');
|
|
1223
|
+
if (!promptArg) {
|
|
1224
|
+
console.error('缺少 prompt。用法: botmux schedule add <schedule> <prompt>');
|
|
1225
|
+
process.exit(1);
|
|
1226
|
+
}
|
|
1227
|
+
const cur = detectCurrentSession();
|
|
1228
|
+
const chatId = argValue(rest, '--chat-id') ?? cur?.chatId;
|
|
1229
|
+
const rootMessageId = argValue(rest, '--root-msg-id') ?? cur?.rootMessageId;
|
|
1230
|
+
const larkAppId = argValue(rest, '--lark-app-id') ?? cur?.larkAppId;
|
|
1231
|
+
const workingDir = argValue(rest, '--workdir') ?? cur?.workingDir ?? process.cwd();
|
|
1232
|
+
const name = argValue(rest, '--name') ?? (promptArg.length > 20 ? promptArg.slice(0, 20) + '…' : promptArg);
|
|
1233
|
+
const deliver = argValue(rest, '--deliver') ?? 'origin';
|
|
1234
|
+
if (!chatId) {
|
|
1235
|
+
console.error('无法推断 chat-id。请加上 --chat-id <CHAT_ID>,或从 Lark 话题内的 CLI 会话中运行本命令。');
|
|
1236
|
+
process.exit(1);
|
|
1237
|
+
}
|
|
1238
|
+
let parsed;
|
|
1239
|
+
try {
|
|
1240
|
+
parsed = scheduler.parseSchedule(rawSchedule);
|
|
1241
|
+
}
|
|
1242
|
+
catch (err) {
|
|
1243
|
+
console.error(`无法解析 schedule "${rawSchedule}": ${err.message}`);
|
|
1244
|
+
process.exit(1);
|
|
1245
|
+
}
|
|
1246
|
+
const task = scheduler.addTask({
|
|
1247
|
+
name,
|
|
1248
|
+
schedule: rawSchedule,
|
|
1249
|
+
parsed,
|
|
1250
|
+
prompt: promptArg,
|
|
1251
|
+
workingDir,
|
|
1252
|
+
chatId,
|
|
1253
|
+
rootMessageId,
|
|
1254
|
+
larkAppId,
|
|
1255
|
+
chatType: cur?.chatType === 'p2p' ? 'p2p' : 'topic_group',
|
|
1256
|
+
deliver,
|
|
1257
|
+
});
|
|
1258
|
+
const next = task.nextRunAt ? new Date(task.nextRunAt).toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' }) : '—';
|
|
1259
|
+
console.log(`✅ 已创建定时任务 [${task.id}] ${task.name}`);
|
|
1260
|
+
console.log(` 规则: ${parsed.display}`);
|
|
1261
|
+
console.log(` 下次执行: ${next}`);
|
|
1262
|
+
console.log(` 工作目录: ${workingDir}`);
|
|
1263
|
+
console.log(` 话题: ${rootMessageId ?? '(将新开)'}`);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const id = positionals(rest)[0];
|
|
1267
|
+
if (!id) {
|
|
1268
|
+
console.error(`用法: botmux schedule ${sub} <id>`);
|
|
1269
|
+
process.exit(1);
|
|
1270
|
+
}
|
|
1271
|
+
switch (sub) {
|
|
1272
|
+
case 'remove':
|
|
1273
|
+
case 'rm':
|
|
1274
|
+
case 'delete':
|
|
1275
|
+
case 'del':
|
|
1276
|
+
if (scheduler.removeTask(id))
|
|
1277
|
+
console.log(`已删除任务 ${id}`);
|
|
1278
|
+
else {
|
|
1279
|
+
console.error(`未找到任务 ${id}`);
|
|
1280
|
+
process.exit(1);
|
|
1281
|
+
}
|
|
1282
|
+
break;
|
|
1283
|
+
case 'pause':
|
|
1284
|
+
case 'disable':
|
|
1285
|
+
if (scheduler.disableTask(id))
|
|
1286
|
+
console.log(`已暂停任务 ${id}`);
|
|
1287
|
+
else {
|
|
1288
|
+
console.error(`未找到任务 ${id}`);
|
|
1289
|
+
process.exit(1);
|
|
1290
|
+
}
|
|
1291
|
+
break;
|
|
1292
|
+
case 'resume':
|
|
1293
|
+
case 'enable':
|
|
1294
|
+
if (scheduler.enableTask(id))
|
|
1295
|
+
console.log(`已恢复任务 ${id}`);
|
|
1296
|
+
else {
|
|
1297
|
+
console.error(`未找到任务 ${id}`);
|
|
1298
|
+
process.exit(1);
|
|
1299
|
+
}
|
|
1300
|
+
break;
|
|
1301
|
+
case 'run':
|
|
1302
|
+
// Running requires the daemon (executeCallback is daemon-side).
|
|
1303
|
+
// CLI can only mark a task to run ASAP; daemon's next tick picks it up.
|
|
1304
|
+
{
|
|
1305
|
+
const task = scheduleStore.getTask(id);
|
|
1306
|
+
if (!task) {
|
|
1307
|
+
console.error(`未找到任务 ${id}`);
|
|
1308
|
+
process.exit(1);
|
|
1309
|
+
}
|
|
1310
|
+
scheduleStore.updateTask(id, { nextRunAt: new Date().toISOString() });
|
|
1311
|
+
console.log(`已标记任务 ${id} 下次 tick 立即执行(< 30s)`);
|
|
1312
|
+
}
|
|
1313
|
+
break;
|
|
1314
|
+
default:
|
|
1315
|
+
console.error(`未知子命令: ${sub}\n可用: list | add | remove | pause | resume | run`);
|
|
1316
|
+
process.exit(1);
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
async function cmdThreadMessages(rest) {
|
|
1320
|
+
process.env.SESSION_DATA_DIR ??= resolveDataDir();
|
|
1321
|
+
const limit = parseInt(argValue(rest, '--limit') ?? '50', 10);
|
|
1322
|
+
const sessionIdArg = argValue(rest, '--session-id');
|
|
1323
|
+
const sid = sessionIdArg ?? findAncestorSessionId();
|
|
1324
|
+
if (!sid) {
|
|
1325
|
+
console.error('无法推断 session-id。请在 Lark 话题内的 CLI 会话中运行,或传 --session-id <id>。');
|
|
1326
|
+
process.exit(1);
|
|
1327
|
+
}
|
|
1328
|
+
const sessions = loadSessions();
|
|
1329
|
+
const s = sessions.get(sid);
|
|
1330
|
+
if (!s) {
|
|
1331
|
+
console.error(`未找到 session ${sid}`);
|
|
1332
|
+
process.exit(1);
|
|
1333
|
+
}
|
|
1334
|
+
if (!s.larkAppId) {
|
|
1335
|
+
console.error(`session ${sid} 缺少 larkAppId,无法获取消息`);
|
|
1336
|
+
process.exit(1);
|
|
1337
|
+
}
|
|
1338
|
+
// Ensure bot is registered so getBotClient works
|
|
1339
|
+
const { registerBot, loadBotConfigs } = await import('./bot-registry.js');
|
|
1340
|
+
try {
|
|
1341
|
+
for (const cfg of loadBotConfigs())
|
|
1342
|
+
registerBot(cfg);
|
|
1343
|
+
}
|
|
1344
|
+
catch { /* ignore */ }
|
|
1345
|
+
const { listThreadMessages } = await import('./im/lark/client.js');
|
|
1346
|
+
const { parseApiMessage } = await import('./im/lark/message-parser.js');
|
|
1347
|
+
try {
|
|
1348
|
+
const raw = await listThreadMessages(s.larkAppId, s.chatId, s.rootMessageId, limit);
|
|
1349
|
+
const messages = raw.map(parseApiMessage);
|
|
1350
|
+
console.log(JSON.stringify({ sessionId: sid, threadId: s.rootMessageId, messages, total: messages.length }, null, 2));
|
|
1351
|
+
}
|
|
1352
|
+
catch (err) {
|
|
1353
|
+
console.error(`获取话题消息失败: ${err.message}`);
|
|
1354
|
+
process.exit(1);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
// ─── Send subcommand ─────────────────────────────────────────────────────────
|
|
1358
|
+
/** Read all of stdin until EOF. Returns '' if stdin is a TTY (no piped data). */
|
|
1359
|
+
function readStdin() {
|
|
1360
|
+
return new Promise((resolve) => {
|
|
1361
|
+
if (process.stdin.isTTY) {
|
|
1362
|
+
resolve('');
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
const chunks = [];
|
|
1366
|
+
process.stdin.on('data', (c) => chunks.push(c));
|
|
1367
|
+
process.stdin.on('end', () => resolve(Buffer.concat(chunks).toString('utf-8')));
|
|
1368
|
+
process.stdin.on('error', () => resolve(''));
|
|
1369
|
+
});
|
|
1370
|
+
}
|
|
1371
|
+
/** Collect all values for a repeatable flag: --flag v1 --flag v2 */
|
|
1372
|
+
function argValues(args, ...flags) {
|
|
1373
|
+
const out = [];
|
|
1374
|
+
for (let i = 0; i < args.length; i++) {
|
|
1375
|
+
for (const f of flags) {
|
|
1376
|
+
if (args[i] === f && i + 1 < args.length) {
|
|
1377
|
+
out.push(args[++i]);
|
|
1378
|
+
break;
|
|
1379
|
+
}
|
|
1380
|
+
if (args[i].startsWith(f + '=')) {
|
|
1381
|
+
out.push(args[i].slice(f.length + 1));
|
|
1382
|
+
break;
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
return out;
|
|
1387
|
+
}
|
|
1388
|
+
async function cmdSend(rest) {
|
|
1389
|
+
process.env.SESSION_DATA_DIR ??= resolveDataDir();
|
|
1390
|
+
const sessionIdArg = argValue(rest, '--session-id');
|
|
1391
|
+
const images = argValues(rest, '--image', '--images');
|
|
1392
|
+
const files = argValues(rest, '--file', '--files');
|
|
1393
|
+
const mentionArgs = argValues(rest, '--mention'); // "open_id:Display Name"
|
|
1394
|
+
const contentFile = argValue(rest, '--content-file');
|
|
1395
|
+
const sid = sessionIdArg ?? findAncestorSessionId();
|
|
1396
|
+
if (!sid) {
|
|
1397
|
+
console.error('无法推断 session-id。请在 Lark 话题内的 CLI 会话中运行,或传 --session-id <id>。');
|
|
1398
|
+
process.exit(1);
|
|
1399
|
+
}
|
|
1400
|
+
const sessions = loadSessions();
|
|
1401
|
+
const s = sessions.get(sid);
|
|
1402
|
+
if (!s) {
|
|
1403
|
+
console.error(`未找到 session ${sid}`);
|
|
1404
|
+
process.exit(1);
|
|
1405
|
+
}
|
|
1406
|
+
if (!s.larkAppId) {
|
|
1407
|
+
console.error(`session ${sid} 缺少 larkAppId`);
|
|
1408
|
+
process.exit(1);
|
|
1409
|
+
}
|
|
1410
|
+
// Read content from: --content-file > positional arg > stdin
|
|
1411
|
+
let content = '';
|
|
1412
|
+
if (contentFile) {
|
|
1413
|
+
if (!existsSync(contentFile)) {
|
|
1414
|
+
console.error(`文件不存在: ${contentFile}`);
|
|
1415
|
+
process.exit(1);
|
|
1416
|
+
}
|
|
1417
|
+
content = readFileSync(contentFile, 'utf-8');
|
|
1418
|
+
}
|
|
1419
|
+
else {
|
|
1420
|
+
const pos = positionals(rest);
|
|
1421
|
+
if (pos.length > 0) {
|
|
1422
|
+
content = pos.join(' ');
|
|
1423
|
+
}
|
|
1424
|
+
else {
|
|
1425
|
+
content = await readStdin();
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
if (!content.trim() && images.length === 0 && files.length === 0) {
|
|
1429
|
+
console.error('没有内容可发送。用法:\n echo "消息" | botmux send\n botmux send "消息"\n botmux send --content-file /tmp/msg.md --images /tmp/chart.png');
|
|
1430
|
+
process.exit(1);
|
|
1431
|
+
}
|
|
1432
|
+
// Parse mentions: "open_id:Display Name"
|
|
1433
|
+
const mentions = [];
|
|
1434
|
+
for (const m of mentionArgs) {
|
|
1435
|
+
const idx = m.indexOf(':');
|
|
1436
|
+
if (idx > 0) {
|
|
1437
|
+
mentions.push({ open_id: m.slice(0, idx), name: m.slice(idx + 1) });
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
// Validate file paths
|
|
1441
|
+
for (const p of [...images, ...files]) {
|
|
1442
|
+
if (!existsSync(p)) {
|
|
1443
|
+
console.error(`文件不存在: ${p}`);
|
|
1444
|
+
process.exit(1);
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
// Register bots so Lark client works
|
|
1448
|
+
const { registerBot, loadBotConfigs } = await import('./bot-registry.js');
|
|
1449
|
+
try {
|
|
1450
|
+
for (const cfg of loadBotConfigs())
|
|
1451
|
+
registerBot(cfg);
|
|
1452
|
+
}
|
|
1453
|
+
catch { /* */ }
|
|
1454
|
+
const { replyMessage, uploadImage, uploadFile } = await import('./im/lark/client.js');
|
|
1455
|
+
const appId = s.larkAppId;
|
|
1456
|
+
try {
|
|
1457
|
+
// Upload images in parallel
|
|
1458
|
+
const imageKeys = [];
|
|
1459
|
+
if (images.length > 0) {
|
|
1460
|
+
const results = await Promise.all(images.map(p => uploadImage(appId, p)));
|
|
1461
|
+
imageKeys.push(...results);
|
|
1462
|
+
}
|
|
1463
|
+
// Try to extract plain text if Claude accidentally sent post JSON as content
|
|
1464
|
+
let text = content;
|
|
1465
|
+
try {
|
|
1466
|
+
const parsed = JSON.parse(text);
|
|
1467
|
+
const inner = parsed.zh_cn ?? parsed.en_us ?? parsed;
|
|
1468
|
+
if (Array.isArray(inner?.content)) {
|
|
1469
|
+
const lines = [];
|
|
1470
|
+
for (const para of inner.content) {
|
|
1471
|
+
if (!Array.isArray(para))
|
|
1472
|
+
continue;
|
|
1473
|
+
lines.push(para.filter((n) => n.tag === 'text').map((n) => n.text).join(''));
|
|
1474
|
+
}
|
|
1475
|
+
text = lines.join('\n').trim();
|
|
1476
|
+
}
|
|
1477
|
+
}
|
|
1478
|
+
catch { /* not JSON, use as-is */ }
|
|
1479
|
+
// Build post content: text → paragraphs, with @mention replacement
|
|
1480
|
+
let mentionPattern = null;
|
|
1481
|
+
const mentionMap = new Map();
|
|
1482
|
+
if (mentions.length > 0) {
|
|
1483
|
+
const patterns = mentions.map(m => m.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
1484
|
+
mentionPattern = new RegExp(`@(${patterns.join('|')})\\b`, 'gi');
|
|
1485
|
+
for (const m of mentions)
|
|
1486
|
+
mentionMap.set(m.name.toLowerCase(), m.open_id);
|
|
1487
|
+
}
|
|
1488
|
+
const postContent = text ? text.split('\n').map((line) => {
|
|
1489
|
+
if (!mentionPattern)
|
|
1490
|
+
return [{ tag: 'text', text: line }];
|
|
1491
|
+
const nodes = [];
|
|
1492
|
+
let lastIndex = 0;
|
|
1493
|
+
for (const match of line.matchAll(mentionPattern)) {
|
|
1494
|
+
const openId = mentionMap.get(match[1].toLowerCase());
|
|
1495
|
+
if (!openId)
|
|
1496
|
+
continue;
|
|
1497
|
+
if (match.index > lastIndex)
|
|
1498
|
+
nodes.push({ tag: 'text', text: line.slice(lastIndex, match.index) });
|
|
1499
|
+
nodes.push({ tag: 'at', user_id: openId });
|
|
1500
|
+
lastIndex = match.index + match[0].length;
|
|
1501
|
+
}
|
|
1502
|
+
if (lastIndex < line.length)
|
|
1503
|
+
nodes.push({ tag: 'text', text: line.slice(lastIndex) });
|
|
1504
|
+
return nodes.length > 0 ? nodes : [{ tag: 'text', text: line }];
|
|
1505
|
+
}) : [];
|
|
1506
|
+
for (const key of imageKeys)
|
|
1507
|
+
postContent.push([{ tag: 'img', image_key: key }]);
|
|
1508
|
+
// Unused mentions → append at end
|
|
1509
|
+
if (mentions.length > 0) {
|
|
1510
|
+
const usedIds = new Set();
|
|
1511
|
+
for (const para of postContent)
|
|
1512
|
+
for (const n of para)
|
|
1513
|
+
if (n.tag === 'at')
|
|
1514
|
+
usedIds.add(n.user_id);
|
|
1515
|
+
const unused = mentions.filter(m => !usedIds.has(m.open_id));
|
|
1516
|
+
if (unused.length > 0) {
|
|
1517
|
+
if (postContent.length === 0)
|
|
1518
|
+
postContent.push([]);
|
|
1519
|
+
for (const m of unused)
|
|
1520
|
+
postContent[postContent.length - 1].push({ tag: 'at', user_id: m.open_id });
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
// Append @mention to session owner
|
|
1524
|
+
if (s.ownerOpenId) {
|
|
1525
|
+
if (postContent.length === 0)
|
|
1526
|
+
postContent.push([]);
|
|
1527
|
+
postContent[postContent.length - 1].push({ tag: 'at', user_id: s.ownerOpenId });
|
|
1528
|
+
}
|
|
1529
|
+
const postJson = JSON.stringify({ zh_cn: { title: '', content: postContent } });
|
|
1530
|
+
const messageId = await replyMessage(appId, s.rootMessageId, postJson, 'post', true);
|
|
1531
|
+
// Send file attachments as separate messages
|
|
1532
|
+
const fileIds = [];
|
|
1533
|
+
for (const fp of files) {
|
|
1534
|
+
const fileKey = await uploadFile(appId, fp);
|
|
1535
|
+
const fid = await replyMessage(appId, s.rootMessageId, JSON.stringify({ file_key: fileKey }), 'file', true);
|
|
1536
|
+
fileIds.push(fid);
|
|
1537
|
+
}
|
|
1538
|
+
// Bot-to-bot mention signals
|
|
1539
|
+
const dataDir = resolveDataDir();
|
|
1540
|
+
const botInfoPath = join(dataDir, 'bots-info.json');
|
|
1541
|
+
let botEntries = [];
|
|
1542
|
+
try {
|
|
1543
|
+
if (existsSync(botInfoPath))
|
|
1544
|
+
botEntries = JSON.parse(readFileSync(botInfoPath, 'utf-8'));
|
|
1545
|
+
}
|
|
1546
|
+
catch { /* */ }
|
|
1547
|
+
const openIdToAppId = new Map();
|
|
1548
|
+
for (const e of botEntries)
|
|
1549
|
+
if (e.botOpenId)
|
|
1550
|
+
openIdToAppId.set(e.botOpenId, e.larkAppId);
|
|
1551
|
+
try {
|
|
1552
|
+
for (const file of readdirSync(dataDir)) {
|
|
1553
|
+
if (!file.startsWith('bot-openids-') || !file.endsWith('.json'))
|
|
1554
|
+
continue;
|
|
1555
|
+
try {
|
|
1556
|
+
const crossRef = JSON.parse(readFileSync(join(dataDir, file), 'utf-8'));
|
|
1557
|
+
for (const [botName, crossOpenId] of Object.entries(crossRef)) {
|
|
1558
|
+
const entry = botEntries.find(e => e.botName?.toLowerCase() === botName.toLowerCase());
|
|
1559
|
+
if (entry)
|
|
1560
|
+
openIdToAppId.set(crossOpenId, entry.larkAppId);
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
catch { /* */ }
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
catch { /* */ }
|
|
1567
|
+
const targetAppIds = new Set();
|
|
1568
|
+
for (const m of mentions) {
|
|
1569
|
+
const ta = openIdToAppId.get(m.open_id);
|
|
1570
|
+
if (ta && ta !== appId)
|
|
1571
|
+
targetAppIds.add(ta);
|
|
1572
|
+
}
|
|
1573
|
+
if (text && botEntries.length > 0) {
|
|
1574
|
+
for (const entry of botEntries) {
|
|
1575
|
+
if (!entry.botOpenId || entry.larkAppId === appId)
|
|
1576
|
+
continue;
|
|
1577
|
+
const names = [entry.botName, entry.cliId].filter(Boolean);
|
|
1578
|
+
for (const name of names) {
|
|
1579
|
+
if (new RegExp(`@${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i').test(text)) {
|
|
1580
|
+
targetAppIds.add(entry.larkAppId);
|
|
1581
|
+
break;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
if (targetAppIds.size > 0) {
|
|
1587
|
+
const signalDir = join(dataDir, 'bot-mentions');
|
|
1588
|
+
if (!existsSync(signalDir))
|
|
1589
|
+
mkdirSync(signalDir, { recursive: true });
|
|
1590
|
+
for (const targetApp of targetAppIds) {
|
|
1591
|
+
const te = botEntries.find(e => e.larkAppId === targetApp);
|
|
1592
|
+
const signal = {
|
|
1593
|
+
rootMessageId: s.rootMessageId, chatId: s.chatId, chatType: s.chatType,
|
|
1594
|
+
senderAppId: appId, targetBotOpenId: te?.botOpenId ?? targetApp,
|
|
1595
|
+
content: text, messageId, timestamp: Date.now(),
|
|
1596
|
+
};
|
|
1597
|
+
writeFileSync(join(signalDir, `${Date.now()}-${(te?.botOpenId ?? targetApp).slice(-8)}.json`), JSON.stringify(signal));
|
|
1598
|
+
}
|
|
1599
|
+
}
|
|
1600
|
+
console.log(JSON.stringify({ success: true, messageId, sessionId: sid }));
|
|
1601
|
+
}
|
|
1602
|
+
catch (err) {
|
|
1603
|
+
console.error(`发送失败: ${err.message}`);
|
|
1604
|
+
process.exit(1);
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
// ─── Bots subcommand ─────────────────────────────────────────────────────────
|
|
1608
|
+
async function cmdBots(sub, rest) {
|
|
1609
|
+
process.env.SESSION_DATA_DIR ??= resolveDataDir();
|
|
1610
|
+
if (sub !== 'list' && sub !== 'ls' && sub !== '') {
|
|
1611
|
+
console.error('用法: botmux bots list [--session-id ID]');
|
|
1612
|
+
process.exit(1);
|
|
1613
|
+
}
|
|
1614
|
+
const sessionIdArg = argValue(rest, '--session-id');
|
|
1615
|
+
const sid = sessionIdArg ?? findAncestorSessionId();
|
|
1616
|
+
if (!sid) {
|
|
1617
|
+
console.error('无法推断 session-id。请在 Lark 话题内的 CLI 会话中运行,或传 --session-id <id>。');
|
|
1618
|
+
process.exit(1);
|
|
1619
|
+
}
|
|
1620
|
+
const sessions = loadSessions();
|
|
1621
|
+
const s = sessions.get(sid);
|
|
1622
|
+
if (!s) {
|
|
1623
|
+
console.error(`未找到 session ${sid}`);
|
|
1624
|
+
process.exit(1);
|
|
1625
|
+
}
|
|
1626
|
+
if (!s.larkAppId) {
|
|
1627
|
+
console.error(`session ${sid} 缺少 larkAppId`);
|
|
1628
|
+
process.exit(1);
|
|
1629
|
+
}
|
|
1630
|
+
// Register bots
|
|
1631
|
+
const { registerBot, loadBotConfigs } = await import('./bot-registry.js');
|
|
1632
|
+
try {
|
|
1633
|
+
for (const cfg of loadBotConfigs())
|
|
1634
|
+
registerBot(cfg);
|
|
1635
|
+
}
|
|
1636
|
+
catch { /* */ }
|
|
1637
|
+
const appId = s.larkAppId;
|
|
1638
|
+
const dataDir = resolveDataDir();
|
|
1639
|
+
const botInfoPath = join(dataDir, 'bots-info.json');
|
|
1640
|
+
let botEntries = [];
|
|
1641
|
+
try {
|
|
1642
|
+
if (existsSync(botInfoPath))
|
|
1643
|
+
botEntries = JSON.parse(readFileSync(botInfoPath, 'utf-8'));
|
|
1644
|
+
}
|
|
1645
|
+
catch { /* */ }
|
|
1646
|
+
const botByCli = new Map();
|
|
1647
|
+
for (const b of botEntries)
|
|
1648
|
+
botByCli.set(b.cliId, b);
|
|
1649
|
+
try {
|
|
1650
|
+
const { listChatBotMembers } = await import('./im/lark/client.js');
|
|
1651
|
+
const chatBots = await listChatBotMembers(appId, s.chatId);
|
|
1652
|
+
const result = chatBots.map(cb => {
|
|
1653
|
+
const info = botByCli.get(cb.name);
|
|
1654
|
+
return { name: cb.displayName, openId: cb.openId, isSelf: info?.larkAppId === appId };
|
|
1655
|
+
});
|
|
1656
|
+
console.log(JSON.stringify({ sessionId: sid, chatId: s.chatId, bots: result, total: result.length }, null, 2));
|
|
1657
|
+
}
|
|
1658
|
+
catch (err) {
|
|
1659
|
+
// Fallback to bots-info.json
|
|
1660
|
+
const result = botEntries.filter(b => b.botOpenId).map(b => ({
|
|
1661
|
+
name: b.botName ?? b.cliId, openId: b.botOpenId, isSelf: b.larkAppId === appId,
|
|
1662
|
+
}));
|
|
1663
|
+
console.log(JSON.stringify({ sessionId: sid, bots: result, total: result.length, note: `chat query failed: ${err.message}` }, null, 2));
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
964
1666
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
965
1667
|
function getVersion() {
|
|
966
1668
|
const pkgPath = join(PKG_ROOT, 'package.json');
|
|
@@ -1008,6 +1710,25 @@ switch (command) {
|
|
|
1008
1710
|
case 'rm':
|
|
1009
1711
|
cmdDelete();
|
|
1010
1712
|
break;
|
|
1713
|
+
case 'schedule':
|
|
1714
|
+
await cmdSchedule(process.argv[3] ?? '', process.argv.slice(4));
|
|
1715
|
+
break;
|
|
1716
|
+
case 'send':
|
|
1717
|
+
await cmdSend(process.argv.slice(3));
|
|
1718
|
+
break;
|
|
1719
|
+
case 'bots':
|
|
1720
|
+
await cmdBots(process.argv[3] ?? 'list', process.argv.slice(4));
|
|
1721
|
+
break;
|
|
1722
|
+
case 'thread': {
|
|
1723
|
+
const sub = process.argv[3] ?? '';
|
|
1724
|
+
if (sub === 'messages' || sub === 'msgs')
|
|
1725
|
+
await cmdThreadMessages(process.argv.slice(4));
|
|
1726
|
+
else {
|
|
1727
|
+
console.error(`用法: botmux thread messages [--limit N] [--session-id ID]`);
|
|
1728
|
+
process.exit(1);
|
|
1729
|
+
}
|
|
1730
|
+
break;
|
|
1731
|
+
}
|
|
1011
1732
|
default:
|
|
1012
1733
|
showHelp();
|
|
1013
1734
|
break;
|