botmux 2.12.2 → 2.13.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 +29 -0
- package/README.md +29 -0
- package/dist/cli.js +104 -8
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -0
- package/dist/config.js.map +1 -1
- package/dist/core/dashboard-events.d.ts +57 -0
- package/dist/core/dashboard-events.d.ts.map +1 -0
- package/dist/core/dashboard-events.js +23 -0
- package/dist/core/dashboard-events.js.map +1 -0
- package/dist/core/dashboard-ipc-server.d.ts +43 -0
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -0
- package/dist/core/dashboard-ipc-server.js +232 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -0
- package/dist/core/dashboard-locate.d.ts +20 -0
- package/dist/core/dashboard-locate.d.ts.map +1 -0
- package/dist/core/dashboard-locate.js +26 -0
- package/dist/core/dashboard-locate.js.map +1 -0
- package/dist/core/dashboard-rows.d.ts +30 -0
- package/dist/core/dashboard-rows.d.ts.map +1 -0
- package/dist/core/dashboard-rows.js +48 -0
- package/dist/core/dashboard-rows.js.map +1 -0
- package/dist/core/scheduler.d.ts +20 -0
- package/dist/core/scheduler.d.ts.map +1 -1
- package/dist/core/scheduler.js +89 -2
- package/dist/core/scheduler.js.map +1 -1
- package/dist/core/types.d.ts +5 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts +19 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +146 -0
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +87 -4
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/aggregator.d.ts +41 -0
- package/dist/dashboard/aggregator.d.ts.map +1 -0
- package/dist/dashboard/aggregator.js +125 -0
- package/dist/dashboard/aggregator.js.map +1 -0
- package/dist/dashboard/auth.d.ts +23 -0
- package/dist/dashboard/auth.d.ts.map +1 -0
- package/dist/dashboard/auth.js +66 -0
- package/dist/dashboard/auth.js.map +1 -0
- package/dist/dashboard/registry.d.ts +28 -0
- package/dist/dashboard/registry.d.ts.map +1 -0
- package/dist/dashboard/registry.js +74 -0
- package/dist/dashboard/registry.js.map +1 -0
- package/dist/dashboard/web/app.d.ts +2 -0
- package/dist/dashboard/web/app.d.ts.map +1 -0
- package/dist/dashboard/web/app.js +42 -0
- package/dist/dashboard/web/app.js.map +1 -0
- package/dist/dashboard/web/groups.d.ts +2 -0
- package/dist/dashboard/web/groups.d.ts.map +1 -0
- package/dist/dashboard/web/groups.js +152 -0
- package/dist/dashboard/web/groups.js.map +1 -0
- package/dist/dashboard/web/schedules.d.ts +2 -0
- package/dist/dashboard/web/schedules.d.ts.map +1 -0
- package/dist/dashboard/web/schedules.js +105 -0
- package/dist/dashboard/web/schedules.js.map +1 -0
- package/dist/dashboard/web/sessions.d.ts +2 -0
- package/dist/dashboard/web/sessions.d.ts.map +1 -0
- package/dist/dashboard/web/sessions.js +184 -0
- package/dist/dashboard/web/sessions.js.map +1 -0
- package/dist/dashboard/web/store.d.ts +23 -0
- package/dist/dashboard/web/store.d.ts.map +1 -0
- package/dist/dashboard/web/store.js +82 -0
- package/dist/dashboard/web/store.js.map +1 -0
- package/dist/dashboard-web/app.js +129 -0
- package/dist/dashboard-web/index.html +22 -0
- package/dist/dashboard-web/style.css +50 -0
- package/dist/dashboard.d.ts +2 -0
- package/dist/dashboard.d.ts.map +1 -0
- package/dist/dashboard.js +308 -0
- package/dist/dashboard.js.map +1 -0
- package/dist/services/bridge-turn-queue.d.ts.map +1 -1
- package/dist/services/bridge-turn-queue.js +12 -7
- package/dist/services/bridge-turn-queue.js.map +1 -1
- package/dist/services/groups-store.d.ts +35 -0
- package/dist/services/groups-store.d.ts.map +1 -0
- package/dist/services/groups-store.js +104 -0
- package/dist/services/groups-store.js.map +1 -0
- package/dist/services/schedule-store.d.ts +1 -0
- package/dist/services/schedule-store.d.ts.map +1 -1
- package/dist/services/schedule-store.js +70 -1
- package/dist/services/schedule-store.js.map +1 -1
- package/dist/services/session-store.d.ts.map +1 -1
- package/dist/services/session-store.js +1 -0
- package/dist/services/session-store.js.map +1 -1
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +34 -0
- package/dist/skills/definitions.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/worker.js +47 -2
- package/dist/worker.js.map +1 -1
- package/package.json +4 -2
package/README.en.md
CHANGED
|
@@ -424,6 +424,7 @@ botmux setup
|
|
|
424
424
|
| `botmux autostart enable` | Register boot-time autostart (macOS launchd / Linux user systemd, no sudo) |
|
|
425
425
|
| `botmux autostart disable` | Unregister boot-time autostart |
|
|
426
426
|
| `botmux autostart status` | Show autostart status |
|
|
427
|
+
| `botmux dashboard` | Print a fresh Web Dashboard URL (rotates the token; previous URL becomes invalid) |
|
|
427
428
|
|
|
428
429
|
### Boot-time Autostart
|
|
429
430
|
|
|
@@ -453,6 +454,34 @@ These require the `~/.botmux/bin/botmux` wrapper, which the daemon writes at sta
|
|
|
453
454
|
|
|
454
455
|
---
|
|
455
456
|
|
|
457
|
+
## Web Dashboard
|
|
458
|
+
|
|
459
|
+
botmux ships a LAN-accessible Web Dashboard for managing all sessions and scheduled tasks across every configured bot.
|
|
460
|
+
|
|
461
|
+
```bash
|
|
462
|
+
botmux dashboard
|
|
463
|
+
# prints: http://<lan-ip>:7891/?t=<token>
|
|
464
|
+
```
|
|
465
|
+
|
|
466
|
+
Each invocation rotates the token — previous URLs are invalidated immediately. This is by design, so a leaked link stops working as soon as you fetch a new one.
|
|
467
|
+
|
|
468
|
+
v1 features:
|
|
469
|
+
- **Sessions board** — every active and closed session across every bot, filterable by CLI / status / adopt / free-text. The detail drawer exposes a "📍 定位到飞书话题" button that posts a marker into the original thread (workaround for Feishu having no public topic deep-link), then opens AppLink to the chat. Also: copy IDs, close session, open xterm.
|
|
470
|
+
- **Schedules board** — every scheduled task across every bot, with Run-now / Pause / Resume actions.
|
|
471
|
+
|
|
472
|
+
Environment variables (set in `~/.botmux/.env`):
|
|
473
|
+
|
|
474
|
+
| Variable | Default | Purpose |
|
|
475
|
+
|----------|---------|---------|
|
|
476
|
+
| `BOTMUX_DASHBOARD_HOST` | `0.0.0.0` | Dashboard HTTP bind address |
|
|
477
|
+
| `BOTMUX_DASHBOARD_PORT` | `7891` | Dashboard HTTP port |
|
|
478
|
+
| `BOTMUX_DASHBOARD_EXTERNAL_HOST` | `WEB_EXTERNAL_HOST` or LAN-IP autodetect | Host used in the printed URL |
|
|
479
|
+
| `BOTMUX_DAEMON_IPC_BASE_PORT` | `7892` | Per-daemon IPC port = base + botIndex |
|
|
480
|
+
|
|
481
|
+
The dashboard runs as its own pm2 process (`botmux-dashboard`) — `pnpm daemon:restart` brings it up alongside every bot daemon. Each daemon exposes a localhost-only IPC at `127.0.0.1:7892+botIndex`; the dashboard process is a thin reverse proxy + token gate. The HMAC secret at `~/.botmux/.dashboard-secret` (mode `0600`) is generated on first start and is used only to sign `botmux dashboard` rotation requests — it never reaches the browser.
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
456
485
|
## Contributing
|
|
457
486
|
|
|
458
487
|
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
package/README.md
CHANGED
|
@@ -357,6 +357,7 @@ botmux autostart enable
|
|
|
357
357
|
| `botmux autostart enable` | 注册开机自启(macOS launchd / Linux user systemd,无需 sudo) |
|
|
358
358
|
| `botmux autostart disable` | 注销开机自启 |
|
|
359
359
|
| `botmux autostart status` | 查看自启状态 |
|
|
360
|
+
| `botmux dashboard` | 输出一次 Web Dashboard URL(每次刷 token,旧链接立即失效) |
|
|
360
361
|
|
|
361
362
|
### 开机自启
|
|
362
363
|
|
|
@@ -386,6 +387,34 @@ botmux autostart enable
|
|
|
386
387
|
|
|
387
388
|
---
|
|
388
389
|
|
|
390
|
+
## Web Dashboard
|
|
391
|
+
|
|
392
|
+
botmux 启动后会自带一个 Web Dashboard 用来管理所有会话和定时任务。
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
botmux dashboard
|
|
396
|
+
# 输出: http://<lan-ip>:7891/?t=<token>
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
每次跑 `botmux dashboard` 都会换一个 token,老 URL 立即失效——这是有意为之,符合一次一密的取链方式。
|
|
400
|
+
|
|
401
|
+
页面功能(v1):
|
|
402
|
+
- **Sessions**:跨所有 bot 列出活跃和已关闭会话,支持按 CLI / 状态 / adopt / 文本搜索过滤。点进 detail drawer 后可以「定位到飞书话题」(机器人在原话题发一条 📍 标记 + 浏览器自动开 chat AppLink,规避飞书没有公开 topic deep-link 的限制)、复制各种 ID、关闭会话。
|
|
403
|
+
- **Schedules**:列出所有定时任务,可以 Run now / Pause / Resume。
|
|
404
|
+
|
|
405
|
+
环境变量(写在 `~/.botmux/.env`):
|
|
406
|
+
|
|
407
|
+
| 变量 | 默认 | 说明 |
|
|
408
|
+
|------|------|------|
|
|
409
|
+
| `BOTMUX_DASHBOARD_HOST` | `0.0.0.0` | dashboard HTTP 绑定地址 |
|
|
410
|
+
| `BOTMUX_DASHBOARD_PORT` | `7891` | dashboard HTTP 端口 |
|
|
411
|
+
| `BOTMUX_DASHBOARD_EXTERNAL_HOST` | `WEB_EXTERNAL_HOST` 或 LAN IP 自动探测 | CLI 输出 URL 用的 host |
|
|
412
|
+
| `BOTMUX_DAEMON_IPC_BASE_PORT` | `7892` | 每个 daemon 的 IPC 端口 = base + botIndex |
|
|
413
|
+
|
|
414
|
+
dashboard 走单独 pm2 进程 `botmux-dashboard`,跟着 `pnpm daemon:restart` 一起起停。每个 daemon 在 127.0.0.1 暴露内部 IPC(仅本机),dashboard 进程做反向代理 + 鉴权。`.dashboard-secret` 在首次启动时生成(`~/.botmux/.dashboard-secret`,mode 0600),仅用于 `botmux dashboard` 命令的 HMAC 鉴权,不下发给浏览器。
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
389
418
|
## 配置
|
|
390
419
|
|
|
391
420
|
通过 `~/.botmux/bots.json` 配置机器人。运行 `botmux setup` 交互式创建,或手动编辑。
|
package/dist/cli.js
CHANGED
|
@@ -17,12 +17,13 @@
|
|
|
17
17
|
* botmux autostart enable|disable|status — manage boot-time autostart (launchd / user systemd)
|
|
18
18
|
*/
|
|
19
19
|
import { execSync, spawnSync, spawn } from 'node:child_process';
|
|
20
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, readlinkSync, appendFileSync } from 'node:fs';
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync, renameSync, readdirSync, readlinkSync, appendFileSync, statSync, unlinkSync } from 'node:fs';
|
|
21
21
|
import { join, dirname } from 'node:path';
|
|
22
22
|
import { homedir } from 'node:os';
|
|
23
23
|
import { fileURLToPath } from 'node:url';
|
|
24
24
|
import { createInterface } from 'node:readline';
|
|
25
25
|
import { createRequire } from 'node:module';
|
|
26
|
+
import { createHmac, randomBytes } from 'node:crypto';
|
|
26
27
|
import { enableAutostart, disableAutostart, autostartStatus, refreshAutostart } from './autostart.js';
|
|
27
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
28
29
|
const __dirname = dirname(__filename);
|
|
@@ -108,6 +109,21 @@ function ecosystemConfig() {
|
|
|
108
109
|
out_file: join(LOG_DIR, `daemon-${i}-out.log`),
|
|
109
110
|
env: { SESSION_DATA_DIR: DATA_DIR, BOTMUX_BOT_INDEX: String(i) },
|
|
110
111
|
}));
|
|
112
|
+
apps.push({
|
|
113
|
+
name: 'botmux-dashboard',
|
|
114
|
+
script: join(PKG_ROOT, 'dist', 'dashboard.js'),
|
|
115
|
+
cwd: PKG_ROOT,
|
|
116
|
+
autorestart: true,
|
|
117
|
+
max_restarts: 10,
|
|
118
|
+
restart_delay: 3000,
|
|
119
|
+
error_file: join(LOG_DIR, 'dashboard-error.log'),
|
|
120
|
+
out_file: join(LOG_DIR, 'dashboard-out.log'),
|
|
121
|
+
merge_logs: true,
|
|
122
|
+
env: {
|
|
123
|
+
BOTMUX_DASHBOARD_HOST: process.env.BOTMUX_DASHBOARD_HOST ?? '0.0.0.0',
|
|
124
|
+
BOTMUX_DASHBOARD_PORT: process.env.BOTMUX_DASHBOARD_PORT ?? '7891',
|
|
125
|
+
},
|
|
126
|
+
});
|
|
111
127
|
const cfg = { apps };
|
|
112
128
|
const tmpFile = join(CONFIG_DIR, 'ecosystem.config.json');
|
|
113
129
|
writeFileSync(tmpFile, JSON.stringify(cfg, null, 2));
|
|
@@ -368,6 +384,28 @@ function cmdStart() {
|
|
|
368
384
|
console.log(` autostart unit 已同步到当前 Node/cli.js 路径`);
|
|
369
385
|
}
|
|
370
386
|
}
|
|
387
|
+
/**
|
|
388
|
+
* Wipe stale dashboard-daemon descriptors (mtime older than 5 minutes).
|
|
389
|
+
* Live daemons refresh their descriptor every 30s via heartbeat; anything
|
|
390
|
+
* older is from a daemon that exited without cleaning up. Called as part of
|
|
391
|
+
* the pm2 zombie-cleanup flow so the dashboard registry stays consistent.
|
|
392
|
+
*/
|
|
393
|
+
function cleanupStaleDaemonDescriptors() {
|
|
394
|
+
const regDir = join(DATA_DIR, 'dashboard-daemons');
|
|
395
|
+
if (!existsSync(regDir))
|
|
396
|
+
return;
|
|
397
|
+
for (const f of readdirSync(regDir)) {
|
|
398
|
+
if (!f.endsWith('.json'))
|
|
399
|
+
continue;
|
|
400
|
+
const fp = join(regDir, f);
|
|
401
|
+
try {
|
|
402
|
+
const stat = statSync(fp);
|
|
403
|
+
if (Date.now() - stat.mtimeMs > 5 * 60_000)
|
|
404
|
+
unlinkSync(fp);
|
|
405
|
+
}
|
|
406
|
+
catch { /* ignore */ }
|
|
407
|
+
}
|
|
408
|
+
}
|
|
371
409
|
/** Delete all pm2 processes matching botmux / botmux-* under the given PM2_HOME. */
|
|
372
410
|
function deleteAllBotmuxProcesses(home = PM2_HOME) {
|
|
373
411
|
try {
|
|
@@ -448,6 +486,8 @@ function cmdStop() {
|
|
|
448
486
|
}
|
|
449
487
|
}
|
|
450
488
|
catch { /* */ }
|
|
489
|
+
// Wipe abandoned dashboard-daemon descriptors left behind by stopped daemons.
|
|
490
|
+
cleanupStaleDaemonDescriptors();
|
|
451
491
|
if (!stopped)
|
|
452
492
|
console.log('daemon 未在运行。');
|
|
453
493
|
}
|
|
@@ -462,6 +502,8 @@ function cmdRestart() {
|
|
|
462
502
|
cleanupLegacyPm2();
|
|
463
503
|
// Delete all botmux processes (handles both old single-process and new multi-process)
|
|
464
504
|
deleteAllBotmuxProcesses();
|
|
505
|
+
// Wipe abandoned dashboard-daemon descriptors left behind by killed daemons.
|
|
506
|
+
cleanupStaleDaemonDescriptors();
|
|
465
507
|
const cfg = ecosystemConfig();
|
|
466
508
|
runPm2(['start', cfg]);
|
|
467
509
|
if (refreshAutostart({ pkgRoot: PKG_ROOT, configDir: CONFIG_DIR, logDir: LOG_DIR })) {
|
|
@@ -550,6 +592,44 @@ function cmdUpgrade() {
|
|
|
550
592
|
process.exit(1);
|
|
551
593
|
}
|
|
552
594
|
}
|
|
595
|
+
/**
|
|
596
|
+
* Print a fresh dashboard URL by HMAC-authing to the dashboard process's
|
|
597
|
+
* loopback rotation endpoint. Each call invalidates the previously-issued
|
|
598
|
+
* token, so sharing a URL is the same as sharing a one-shot session.
|
|
599
|
+
*/
|
|
600
|
+
async function cmdDashboard() {
|
|
601
|
+
const SECRET_PATH = join(CONFIG_DIR, '.dashboard-secret');
|
|
602
|
+
if (!existsSync(SECRET_PATH)) {
|
|
603
|
+
console.error('Dashboard not initialised. Run `pnpm daemon:restart` first.');
|
|
604
|
+
process.exit(1);
|
|
605
|
+
}
|
|
606
|
+
const secret = readFileSync(SECRET_PATH, 'utf8').trim();
|
|
607
|
+
const ts = Math.floor(Date.now() / 1000).toString();
|
|
608
|
+
const nonce = randomBytes(8).toString('hex');
|
|
609
|
+
const sig = createHmac('sha256', secret).update(`${ts}:${nonce}`).digest('base64url');
|
|
610
|
+
const port = process.env.BOTMUX_DASHBOARD_PORT ?? '7891';
|
|
611
|
+
let res;
|
|
612
|
+
try {
|
|
613
|
+
res = await fetch(`http://127.0.0.1:${port}/__cli/rotate`, {
|
|
614
|
+
method: 'POST',
|
|
615
|
+
headers: {
|
|
616
|
+
'X-Botmux-Cli-Ts': ts,
|
|
617
|
+
'X-Botmux-Cli-Nonce': nonce,
|
|
618
|
+
'X-Botmux-Cli-Auth': sig,
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
console.error(`dashboard process not reachable on 127.0.0.1:${port} — \`pnpm daemon:restart\` will start it`);
|
|
624
|
+
process.exit(1);
|
|
625
|
+
}
|
|
626
|
+
if (!res.ok) {
|
|
627
|
+
console.error('Rotation failed:', res.status, await res.text());
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
630
|
+
const body = await res.json();
|
|
631
|
+
console.log(body.url);
|
|
632
|
+
}
|
|
553
633
|
/**
|
|
554
634
|
* Resolve the session data directory.
|
|
555
635
|
* Priority: SESSION_DATA_DIR env > daemon breadcrumb (~/.botmux/.data-dir) > default (~/.botmux/data)
|
|
@@ -1160,6 +1240,7 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
|
|
|
1160
1240
|
logs 查看 daemon 日志(--lines N, --bot <index>)
|
|
1161
1241
|
status 查看 daemon 状态
|
|
1162
1242
|
upgrade 升级到最新版本
|
|
1243
|
+
dashboard 打印新的 Web Dashboard 一次性登录 URL(旧 token 同时失效)
|
|
1163
1244
|
list 列出活跃会话(交互式选择并连接 tmux)
|
|
1164
1245
|
--plain 纯文本表格输出(管道/脚本场景)
|
|
1165
1246
|
delete <id> 关闭指定会话(支持 ID 前缀匹配)
|
|
@@ -1263,13 +1344,18 @@ function argFlag(args, flag) {
|
|
|
1263
1344
|
return args.includes(flag);
|
|
1264
1345
|
}
|
|
1265
1346
|
/** Extract positional args, skipping --flag and the value that follows it
|
|
1266
|
-
* (for --flag <value> style). --flag=value style is self-contained.
|
|
1267
|
-
|
|
1347
|
+
* (for --flag <value> style). --flag=value style is self-contained.
|
|
1348
|
+
* `booleanFlags` lists flags that take no value — without this hint the
|
|
1349
|
+
* parser swallows the *next* arg as the flag's value, which silently eats
|
|
1350
|
+
* positional content (or, worse, a following --flag's value). */
|
|
1351
|
+
function positionals(args, booleanFlags = []) {
|
|
1268
1352
|
const out = [];
|
|
1269
1353
|
for (let i = 0; i < args.length; i++) {
|
|
1270
1354
|
const a = args[i];
|
|
1271
1355
|
if (a.startsWith('--')) {
|
|
1272
|
-
|
|
1356
|
+
const flagName = a.includes('=') ? a.slice(0, a.indexOf('=')) : a;
|
|
1357
|
+
const isBoolean = booleanFlags.includes(flagName);
|
|
1358
|
+
if (!a.includes('=') && !isBoolean && i + 1 < args.length)
|
|
1273
1359
|
i++; // skip value
|
|
1274
1360
|
continue;
|
|
1275
1361
|
}
|
|
@@ -1669,7 +1755,7 @@ async function cmdSend(rest) {
|
|
|
1669
1755
|
content = readFileSync(contentFile, 'utf-8');
|
|
1670
1756
|
}
|
|
1671
1757
|
else {
|
|
1672
|
-
const pos = positionals(rest);
|
|
1758
|
+
const pos = positionals(rest, ['--card', '--text', '--top-level']);
|
|
1673
1759
|
if (pos.length > 0) {
|
|
1674
1760
|
content = pos.join(' ');
|
|
1675
1761
|
}
|
|
@@ -1844,7 +1930,11 @@ async function cmdSend(rest) {
|
|
|
1844
1930
|
// be the session owner), plus a `cc` line listing oncall owners so they
|
|
1845
1931
|
// stay informed. Non-oncall: keep owner-only behaviour.
|
|
1846
1932
|
const footerParts = ['[botmux](https://github.com/deepcoldy/botmux)'];
|
|
1847
|
-
|
|
1933
|
+
// Top-level publish has no specific recipient — drop "发送给/cc" addressing
|
|
1934
|
+
// so the message doesn't @ the session owner who isn't even in the target chat.
|
|
1935
|
+
const addressing = sendTopLevel
|
|
1936
|
+
? { sendTo: undefined, cc: [] }
|
|
1937
|
+
: buildFooterAddressing(s, oncallEntry);
|
|
1848
1938
|
if (addressing.sendTo)
|
|
1849
1939
|
footerParts.push(`发送给:<at id=${addressing.sendTo}></at>`);
|
|
1850
1940
|
if (addressing.cc.length > 0) {
|
|
@@ -1901,8 +1991,11 @@ async function cmdSend(rest) {
|
|
|
1901
1991
|
}
|
|
1902
1992
|
// Footer: mirror the card layout — a blank paragraph separates the body
|
|
1903
1993
|
// from the addressing line(s). `发送给: @<caller>` always; oncall groups
|
|
1904
|
-
// additionally get `cc: @<owners>` on the next line.
|
|
1905
|
-
|
|
1994
|
+
// additionally get `cc: @<owners>` on the next line. Top-level publish
|
|
1995
|
+
// has no specific recipient — skip addressing entirely.
|
|
1996
|
+
const addressing = sendTopLevel
|
|
1997
|
+
? { sendTo: undefined, cc: [] }
|
|
1998
|
+
: buildFooterAddressing(s, oncallEntry);
|
|
1906
1999
|
if (addressing.sendTo || addressing.cc.length > 0) {
|
|
1907
2000
|
if (postContent.length > 0)
|
|
1908
2001
|
postContent.push([{ tag: 'text', text: '' }]);
|
|
@@ -2106,6 +2199,9 @@ switch (command) {
|
|
|
2106
2199
|
case 'upgrade':
|
|
2107
2200
|
cmdUpgrade();
|
|
2108
2201
|
break;
|
|
2202
|
+
case 'dashboard':
|
|
2203
|
+
await cmdDashboard();
|
|
2204
|
+
break;
|
|
2109
2205
|
case 'list':
|
|
2110
2206
|
case 'ls':
|
|
2111
2207
|
await cmdList();
|