botmux 2.101.1 → 2.103.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/adapters/backend/sandbox.d.ts +13 -1
- package/dist/adapters/backend/sandbox.d.ts.map +1 -1
- package/dist/adapters/backend/sandbox.js +33 -12
- package/dist/adapters/backend/sandbox.js.map +1 -1
- package/dist/adapters/cli/coco.d.ts.map +1 -1
- package/dist/adapters/cli/coco.js +6 -1
- package/dist/adapters/cli/coco.js.map +1 -1
- package/dist/adapters/cli/codex-app.d.ts.map +1 -1
- package/dist/adapters/cli/codex-app.js +3 -1
- package/dist/adapters/cli/codex-app.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +7 -1
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/adapters/cli/mtr.d.ts.map +1 -1
- package/dist/adapters/cli/mtr.js +4 -1
- package/dist/adapters/cli/mtr.js.map +1 -1
- package/dist/adapters/cli/opencode.d.ts.map +1 -1
- package/dist/adapters/cli/opencode.js +4 -1
- package/dist/adapters/cli/opencode.js.map +1 -1
- package/dist/adapters/cli/traex.d.ts.map +1 -1
- package/dist/adapters/cli/traex.js +4 -1
- package/dist/adapters/cli/traex.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +4 -1
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/bot-registry.d.ts +14 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +9 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts +9 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +141 -6
- package/dist/cli.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +22 -8
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/cost-calculator.d.ts.map +1 -1
- package/dist/core/cost-calculator.js +6 -0
- package/dist/core/cost-calculator.js.map +1 -1
- package/dist/core/dashboard-ipc-server.d.ts +9 -0
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +96 -6
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/local-terminal-opener.d.ts +43 -0
- package/dist/core/local-terminal-opener.d.ts.map +1 -0
- package/dist/core/local-terminal-opener.js +221 -0
- package/dist/core/local-terminal-opener.js.map +1 -0
- package/dist/core/working-dir.d.ts +12 -1
- package/dist/core/working-dir.d.ts.map +1 -1
- package/dist/core/working-dir.js +20 -3
- package/dist/core/working-dir.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +52 -8
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/bot-onboarding.d.ts +28 -0
- package/dist/dashboard/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/bot-onboarding.js +20 -0
- package/dist/dashboard/bot-onboarding.js.map +1 -1
- package/dist/dashboard/bot-payload.d.ts +2 -0
- package/dist/dashboard/bot-payload.d.ts.map +1 -1
- package/dist/dashboard/bot-payload.js +4 -0
- package/dist/dashboard/bot-payload.js.map +1 -1
- package/dist/dashboard/feishu-login.d.ts +64 -0
- package/dist/dashboard/feishu-login.d.ts.map +1 -0
- package/dist/dashboard/feishu-login.js +99 -0
- package/dist/dashboard/feishu-login.js.map +1 -0
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +243 -1
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/bot-onboarding.d.ts.map +1 -1
- package/dist/dashboard/web/bot-onboarding.js +4 -1
- package/dist/dashboard/web/bot-onboarding.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +52 -8
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard-web/app.js +6 -6
- package/dist/dashboard-web/chunks/bot-defaults-page-ZVBKVYIG.js +380 -0
- package/dist/dashboard-web/chunks/chunk-HC7ANVX7.js +31 -0
- package/dist/dashboard-web/chunks/{chunk-ECXPBQ3J.js → chunk-NNXPEJ7S.js} +1 -1
- package/dist/dashboard-web/chunks/{connectors-UGTF4Q4I.js → connectors-POZADN75.js} +1 -1
- package/dist/dashboard-web/chunks/{groups-page-IKTCMQ4A.js → groups-page-UQFQ6DRE.js} +1 -1
- package/dist/dashboard-web/chunks/{insights-page-7CCSHEKM.js → insights-page-ATW2W5KP.js} +1 -1
- package/dist/dashboard-web/chunks/{overview-page-JOJW65P5.js → overview-page-Z7OV54XU.js} +1 -1
- package/dist/dashboard-web/chunks/{roles-page-E2VN2DLV.js → roles-page-4BJVXK2E.js} +1 -1
- package/dist/dashboard-web/chunks/{schedules-6QIHJIHX.js → schedules-XLETZ2EP.js} +1 -1
- package/dist/dashboard-web/chunks/{sessions-page-QN2TARF2.js → sessions-page-RMMQUGXJ.js} +1 -1
- package/dist/dashboard-web/chunks/{settings-page-WWTXB577.js → settings-page-PWKI4TWF.js} +1 -1
- package/dist/dashboard-web/chunks/{skills-page-2FAUPQ6H.js → skills-page-SH7KUD45.js} +1 -1
- package/dist/dashboard-web/chunks/{team-federation-page-PTBQQSFS.js → team-federation-page-JAJXFLXA.js} +1 -1
- package/dist/dashboard-web/chunks/{workflows-ZNBB5KFV.js → workflows-FRJ2DJAO.js} +1 -1
- package/dist/dashboard-web/style.css +99 -0
- package/dist/dashboard.js +170 -17
- package/dist/dashboard.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +13 -4
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +13 -4
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +20 -0
- package/dist/im/lark/card-builder.js.map +1 -1
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +36 -12
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/client.d.ts.map +1 -1
- package/dist/im/lark/client.js +25 -5
- package/dist/im/lark/client.js.map +1 -1
- package/dist/im/lark/event-dispatcher.d.ts.map +1 -1
- package/dist/im/lark/event-dispatcher.js +36 -3
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/services/bot-config-store.d.ts +4 -1
- package/dist/services/bot-config-store.d.ts.map +1 -1
- package/dist/services/bot-config-store.js +19 -1
- package/dist/services/bot-config-store.js.map +1 -1
- package/dist/services/bot-union-ids-store.d.ts +15 -0
- package/dist/services/bot-union-ids-store.d.ts.map +1 -1
- package/dist/services/bot-union-ids-store.js +25 -5
- package/dist/services/bot-union-ids-store.js.map +1 -1
- package/dist/services/open-platform-rename.d.ts +45 -0
- package/dist/services/open-platform-rename.d.ts.map +1 -0
- package/dist/services/open-platform-rename.js +182 -0
- package/dist/services/open-platform-rename.js.map +1 -0
- package/dist/services/platform-team-store.d.ts +5 -0
- package/dist/services/platform-team-store.d.ts.map +1 -1
- package/dist/services/platform-team-store.js +16 -0
- package/dist/services/platform-team-store.js.map +1 -1
- package/dist/services/sandbox-land.d.ts.map +1 -1
- package/dist/services/sandbox-land.js +16 -4
- package/dist/services/sandbox-land.js.map +1 -1
- package/dist/services/usage-ledger.d.ts.map +1 -1
- package/dist/services/usage-ledger.js +14 -2
- package/dist/services/usage-ledger.js.map +1 -1
- package/dist/setup/open-platform-automation.d.ts +8 -0
- package/dist/setup/open-platform-automation.d.ts.map +1 -1
- package/dist/setup/open-platform-automation.js +5 -3
- package/dist/setup/open-platform-automation.js.map +1 -1
- package/dist/utils/bot-routing.d.ts +26 -3
- package/dist/utils/bot-routing.d.ts.map +1 -1
- package/dist/utils/bot-routing.js +38 -4
- package/dist/utils/bot-routing.js.map +1 -1
- package/dist/workflows/events/payloads.d.ts +6 -6
- package/dist/workflows/events/schema.d.ts +12 -12
- package/package.json +1 -1
- package/dist/dashboard-web/chunks/bot-defaults-page-HOGLABBC.js +0 -358
- package/dist/dashboard-web/chunks/chunk-MR57RUVP.js +0 -31
package/dist/dashboard.js
CHANGED
|
@@ -26,12 +26,13 @@ import { handleFederationApi } from './dashboard/federation-api.js';
|
|
|
26
26
|
import { handleFederationSpokeApi, syncAllMemberships, autoBindOwnerIfUnambiguous } from './dashboard/federation-spoke-api.js';
|
|
27
27
|
import { getRunsDir } from './workflows/runs-dir.js';
|
|
28
28
|
import { BotOnboardingManager } from './dashboard/bot-onboarding.js';
|
|
29
|
+
import { FeishuLoginManager } from './dashboard/feishu-login.js';
|
|
29
30
|
import { CLI_SELECT_OPTIONS, resolveCliSelection, isTtadkWrapper, ttadkAcceptsModel, TTADK_DEFAULT_MODEL, TTADK_MODEL_SUGGESTIONS, } from './setup/cli-selection.js';
|
|
30
31
|
import { invalidWorkingDirs } from './utils/working-dir.js';
|
|
31
32
|
import { invalidateGlobalConfigCache, mergeGlobalConfig, readGlobalConfig } from './global-config.js';
|
|
32
33
|
import { buildDashboardUrl } from './core/dashboard-url.js';
|
|
33
34
|
import { deleteWhiteboard, listWhiteboards, readWhiteboard, whiteboardEnabled } from './services/whiteboard-store.js';
|
|
34
|
-
import { isLocalDevInstall, botmuxVersion } from './utils/install-info.js';
|
|
35
|
+
import { isLocalDevInstall, botmuxVersion, botmuxCliEntry } from './utils/install-info.js';
|
|
35
36
|
import { checkNode, detectBotmuxInstalls, resolveCurrentVersion } from './utils/install-diagnostics.js';
|
|
36
37
|
import { fetchLatestVersion, fetchReleasesSince, isNewerVersion } from './core/update-check.js';
|
|
37
38
|
import { GITHUB_REPO } from './core/restart-report.js';
|
|
@@ -168,7 +169,73 @@ function signDaemonTokenHeaders() {
|
|
|
168
169
|
mkdirSync(REGISTRY_DIR, { recursive: true });
|
|
169
170
|
const registry = new DaemonRegistry(REGISTRY_DIR);
|
|
170
171
|
const aggregator = new Aggregator();
|
|
171
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Bring a freshly-onboarded bot online without a fleet-wide restart by spawning
|
|
174
|
+
* `botmux start-bot <appId> --json` (see cli.ts:ensureBotDaemonStarted). The new
|
|
175
|
+
* daemon is forked+supervised by pm2 (reparented off this process), self-registers
|
|
176
|
+
* and opens its Feishu WSClient, then publishes a descriptor the DaemonRegistry
|
|
177
|
+
* auto-discovers — so no dashboard reload is needed either. Runs `botmux` on the
|
|
178
|
+
* SAME host as the dashboard (shared pm2 home / bots.json — the documented
|
|
179
|
+
* dashboard↔daemon co-location assumption). Resolves best-effort; the caller
|
|
180
|
+
* falls back to the restart hint on failure.
|
|
181
|
+
*/
|
|
182
|
+
function spawnStartBotLive(appId) {
|
|
183
|
+
return new Promise((resolve) => {
|
|
184
|
+
let out = '';
|
|
185
|
+
let err = '';
|
|
186
|
+
let settled = false;
|
|
187
|
+
const done = (r) => { if (!settled) {
|
|
188
|
+
settled = true;
|
|
189
|
+
resolve(r);
|
|
190
|
+
} };
|
|
191
|
+
try {
|
|
192
|
+
const child = spawn(process.execPath, [botmuxCliEntry(), 'start-bot', appId, '--json'], {
|
|
193
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
194
|
+
env: process.env,
|
|
195
|
+
});
|
|
196
|
+
const timer = setTimeout(() => {
|
|
197
|
+
try {
|
|
198
|
+
child.kill('SIGKILL');
|
|
199
|
+
}
|
|
200
|
+
catch { /* already gone */ }
|
|
201
|
+
done({ ok: false, message: 'start-bot 超时(30s)' });
|
|
202
|
+
}, 30_000);
|
|
203
|
+
timer.unref?.();
|
|
204
|
+
child.stdout?.on('data', (d) => { out += String(d); });
|
|
205
|
+
child.stderr?.on('data', (d) => { err += String(d); });
|
|
206
|
+
child.on('error', (e) => {
|
|
207
|
+
clearTimeout(timer);
|
|
208
|
+
done({ ok: false, message: e instanceof Error ? e.message : String(e) });
|
|
209
|
+
});
|
|
210
|
+
child.on('exit', (code) => {
|
|
211
|
+
clearTimeout(timer);
|
|
212
|
+
// `start-bot --json` prints a single StartBotLiveResult object; prefer its
|
|
213
|
+
// own message/processName over the raw exit code.
|
|
214
|
+
let parsed;
|
|
215
|
+
try {
|
|
216
|
+
parsed = JSON.parse(out.trim());
|
|
217
|
+
}
|
|
218
|
+
catch { /* non-JSON → fall through */ }
|
|
219
|
+
if (code === 0) {
|
|
220
|
+
done({ ok: true, message: parsed?.processName ? `${parsed.processName} 已上线` : undefined });
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
done({ ok: false, message: parsed?.message || err.trim() || `start-bot 退出码 ${code}` });
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
catch (e) {
|
|
228
|
+
done({ ok: false, message: e instanceof Error ? e.message : String(e) });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
const botOnboarding = new BotOnboardingManager({
|
|
233
|
+
botsJsonPath: BOTS_JSON_PATH,
|
|
234
|
+
startBotLive: spawnStartBotLive,
|
|
235
|
+
});
|
|
236
|
+
// 飞书 Web 登录态刷新(机器人改名缺登录态时的 dashboard 扫码入口)。机器级单例,
|
|
237
|
+
// 写 ~/.botmux/feishu-session.json,与 setup / onboarding 复用同一份登录态。
|
|
238
|
+
const feishuLogin = new FeishuLoginManager();
|
|
172
239
|
const subs = new Map();
|
|
173
240
|
const attaching = new Set(); // dedup concurrent attaches per appId
|
|
174
241
|
function resolveDashboardSettings() {
|
|
@@ -1521,6 +1588,15 @@ const server = createServer(async (req, res) => {
|
|
|
1521
1588
|
return jsonRes(res, 404, { ok: false, error: 'unknown_onboarding_job' });
|
|
1522
1589
|
return jsonRes(res, 200, { job });
|
|
1523
1590
|
}
|
|
1591
|
+
// 飞书 Web 登录态刷新(改名缺登录态 → dashboard 扫码)。POST 受 dashboard 的
|
|
1592
|
+
// 写操作 auth 闸保护(非 GET 需 owner cookie);GET 仅暴露二维码+状态,扫码
|
|
1593
|
+
// 授权的是扫码人自己的账号,风险模型与 onboarding 第二个二维码一致。
|
|
1594
|
+
if (req.method === 'POST' && url.pathname === '/api/feishu-login/start') {
|
|
1595
|
+
return jsonRes(res, 202, { login: feishuLogin.start() });
|
|
1596
|
+
}
|
|
1597
|
+
if (req.method === 'GET' && url.pathname === '/api/feishu-login/status') {
|
|
1598
|
+
return jsonRes(res, 200, { login: feishuLogin.get() });
|
|
1599
|
+
}
|
|
1524
1600
|
let m;
|
|
1525
1601
|
if (req.method === 'POST' && (m = url.pathname.match(/^\/api\/sessions\/([^/]+)\/(close|locate|resume|restart|start)$/))) {
|
|
1526
1602
|
const sid = decodeURIComponent(m[1]);
|
|
@@ -2204,6 +2280,26 @@ const server = createServer(async (req, res) => {
|
|
|
2204
2280
|
res.end(await upstream.text());
|
|
2205
2281
|
return;
|
|
2206
2282
|
}
|
|
2283
|
+
// PUT /api/bots/:appId/rename — proxy to that bot's daemon. Body
|
|
2284
|
+
// `{ name: string }`. Daemon tries the Open Platform automation first
|
|
2285
|
+
// (really renames the Feishu app + publishes a version); on failure it
|
|
2286
|
+
// falls back to the botmux-side display name and reports `warning`.
|
|
2287
|
+
let mBotRename;
|
|
2288
|
+
if (req.method === 'PUT' && (mBotRename = url.pathname.match(/^\/api\/bots\/([^/]+)\/rename$/))) {
|
|
2289
|
+
const appId = decodeURIComponent(mBotRename[1]);
|
|
2290
|
+
const chunks = [];
|
|
2291
|
+
for await (const c of req)
|
|
2292
|
+
chunks.push(c);
|
|
2293
|
+
const raw = Buffer.concat(chunks).toString('utf8') || '{}';
|
|
2294
|
+
const upstream = await proxyToDaemon(appId, `/api/bot-rename`, {
|
|
2295
|
+
method: 'PUT',
|
|
2296
|
+
headers: { 'content-type': 'application/json' },
|
|
2297
|
+
body: raw,
|
|
2298
|
+
});
|
|
2299
|
+
res.writeHead(upstream.status, { 'content-type': 'application/json' });
|
|
2300
|
+
res.end(await upstream.text());
|
|
2301
|
+
return;
|
|
2302
|
+
}
|
|
2207
2303
|
// PUT /api/bots/:appId/max-live-workers — proxy to that bot's daemon. Body
|
|
2208
2304
|
// `{ maxLiveWorkers: number | null }` (null = clear → fall back to the
|
|
2209
2305
|
// built-in default of 30; a positive integer overrides it).
|
|
@@ -2703,17 +2799,52 @@ function handlePlatformTeamSync(payload) {
|
|
|
2703
2799
|
logger.info(`[platform-tunnel] team-sync 已应用 rev=${applied.rev} teams=${applied.teams.length}`);
|
|
2704
2800
|
void maybeAnnounceHallPresence();
|
|
2705
2801
|
}
|
|
2706
|
-
//
|
|
2707
|
-
//
|
|
2708
|
-
|
|
2802
|
+
// 大厅打卡节流:按「发送 bot ×大厅」记最小间隔与尝试上限——按 bot 记会让多团队
|
|
2803
|
+
// bot 在第一个大厅烧光预算后,新加入的大厅永远轮空(实测踩过)。只有真正发出
|
|
2804
|
+
// 消息才消耗次数;状态落盘,重启不重发(否则每次重启都往大厅刷一轮)。
|
|
2709
2805
|
const HALL_ANNOUNCE_MIN_INTERVAL_MS = 10 * 60 * 1000;
|
|
2710
2806
|
const HALL_ANNOUNCE_MAX_TRIES = 6;
|
|
2807
|
+
const hallAnnounceStatePath = () => join(config.session.dataDir, 'hall-announce-state.json');
|
|
2808
|
+
function readHallAnnounceState() {
|
|
2809
|
+
try {
|
|
2810
|
+
return JSON.parse(readFileSync(hallAnnounceStatePath(), 'utf-8'));
|
|
2811
|
+
}
|
|
2812
|
+
catch {
|
|
2813
|
+
return {};
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
/** 记录一次打卡尝试。consumeTry=false 只刷新 lastAt(发送失败:保住 10 分钟退避
|
|
2817
|
+
* 但不烧预算——否则 daemon 掉线期间就把 6 次上限烧光、恢复后永久跳过,Codex review)。 */
|
|
2818
|
+
function bumpHallAnnounceState(key, consumeTry) {
|
|
2819
|
+
const all = readHallAnnounceState();
|
|
2820
|
+
const cur = all[key];
|
|
2821
|
+
all[key] = { lastAt: Date.now(), tries: (cur?.tries ?? 0) + (consumeTry ? 1 : 0) };
|
|
2822
|
+
try {
|
|
2823
|
+
atomicWriteFileSync(hallAnnounceStatePath(), JSON.stringify(all, null, 2) + '\n');
|
|
2824
|
+
}
|
|
2825
|
+
catch { /* 尽力而为 */ }
|
|
2826
|
+
}
|
|
2827
|
+
/** 发送方 daemon 的 mention cross-ref(name → 本 app 视角 open_id)。 */
|
|
2828
|
+
function readBotCrossRef(appId) {
|
|
2829
|
+
try {
|
|
2830
|
+
return JSON.parse(readFileSync(join(config.session.dataDir, `bot-openids-${appId}.json`), 'utf-8'));
|
|
2831
|
+
}
|
|
2832
|
+
catch {
|
|
2833
|
+
return {};
|
|
2834
|
+
}
|
|
2835
|
+
}
|
|
2711
2836
|
/**
|
|
2712
|
-
*
|
|
2713
|
-
*
|
|
2714
|
-
*
|
|
2715
|
-
*
|
|
2716
|
-
*
|
|
2837
|
+
* 大厅打卡编排(union_id 自学)。实测大厅(bot-only 群)只有「直接点名 @」会
|
|
2838
|
+
* 投递事件——普通消息、自 @、@all 全部静默,自家回声在大多数应用上永远等不来。
|
|
2839
|
+
* 机制(与 event-dispatcher 的 hall 分支对偶):
|
|
2840
|
+
* - 有未入册成员的大厅里,每个本机 bot 点名 @ 自己 cross-ref 能解析到的未入册
|
|
2841
|
+
* 成员(含别的机器的——mention 跨机器投递,对方跑新版即可学);被点到的直接
|
|
2842
|
+
* 从 mentions[] 学到自己的 union_id。已入册 bot 也参与——纯教学。
|
|
2843
|
+
* - 自己未入册时消息带 #hall-echo,被点到的 bot 回 @ 一次(open_id 取事件
|
|
2844
|
+
* sender_id,无需 cross-ref)→ 打卡者从回执学到自己。任一方向可解析即收敛。
|
|
2845
|
+
* 消息只在有意义时才发:解析不到任何目标时不发不计次(唯一例外:未入册 bot 的
|
|
2846
|
+
* 首次尝试发一条裸打卡,给有 receive-all scope 的应用留回声机会)。状态落盘,
|
|
2847
|
+
* 重启不重发——解析不到目标反复裸发刷屏这个坑踩过了(自动review 实测)。
|
|
2717
2848
|
*/
|
|
2718
2849
|
async function maybeAnnounceHallPresence() {
|
|
2719
2850
|
try {
|
|
@@ -2723,36 +2854,58 @@ async function maybeAnnounceHallPresence() {
|
|
|
2723
2854
|
return;
|
|
2724
2855
|
const localBotIds = new Set(readPlatformBotsInfo().map(b => b.appId));
|
|
2725
2856
|
const now = Date.now();
|
|
2857
|
+
const state = readHallAnnounceState();
|
|
2726
2858
|
for (const team of teams) {
|
|
2727
2859
|
const hallChatId = team.groupChatIds[0];
|
|
2728
2860
|
if (!hallChatId)
|
|
2729
2861
|
continue;
|
|
2862
|
+
// 未入册成员(全大厅,含别的机器):本机的以本地 store 为准(比 roster 新鲜),
|
|
2863
|
+
// 远端的以 roster 的 unionId 为准。
|
|
2864
|
+
const isLearned = (b) => localBotIds.has(b.appId) ? !!getBotUnionId(dataDir, b.appId) : !!b.unionId;
|
|
2865
|
+
const unlearned = team.bots.filter(b => !isLearned(b));
|
|
2866
|
+
if (unlearned.length === 0)
|
|
2867
|
+
continue;
|
|
2868
|
+
const unlearnedNames = new Set(unlearned.map(b => b.name).filter(Boolean));
|
|
2730
2869
|
for (const bot of team.bots) {
|
|
2731
2870
|
if (!localBotIds.has(bot.appId))
|
|
2732
|
-
continue; //
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
const st =
|
|
2871
|
+
continue; // 只编排本机 bot
|
|
2872
|
+
const selfLearned = isLearned(bot);
|
|
2873
|
+
const throttleKey = `${bot.appId}::${hallChatId}`;
|
|
2874
|
+
const st = state[throttleKey];
|
|
2736
2875
|
if (st && (now - st.lastAt < HALL_ANNOUNCE_MIN_INTERVAL_MS || st.tries >= HALL_ANNOUNCE_MAX_TRIES))
|
|
2737
2876
|
continue;
|
|
2738
|
-
|
|
2877
|
+
// 点名目标 = 自己 cross-ref 能解析到的未入册成员(发不出 @ 的目标点了也白点)。
|
|
2878
|
+
const crossRef = readBotCrossRef(bot.appId);
|
|
2879
|
+
const targets = [...unlearnedNames].filter(n => n !== bot.name && typeof crossRef[n] === 'string').slice(0, 4);
|
|
2880
|
+
// 没有可教的目标:已入册 → 无事可做;未入册 → 仅首次发裸打卡碰回声运气,
|
|
2881
|
+
// 之后静默等别人教(不发不计次,cross-ref 或 roster 变化后自然恢复)。
|
|
2882
|
+
if (targets.length === 0 && (selfLearned || (st?.tries ?? 0) > 0))
|
|
2883
|
+
continue;
|
|
2884
|
+
// 成功发出才消耗预算;失败只刷新 lastAt 保住退避间隔(见 bumpHallAnnounceState)。
|
|
2885
|
+
let sent = false;
|
|
2739
2886
|
try {
|
|
2740
2887
|
const r = await proxyToDaemon(bot.appId, '/api/platform/hall-announce', {
|
|
2741
2888
|
method: 'POST',
|
|
2742
2889
|
headers: { 'content-type': 'application/json' },
|
|
2743
|
-
body: JSON.stringify({ chatId: hallChatId }),
|
|
2890
|
+
body: JSON.stringify({ chatId: hallChatId, mentionNames: targets }),
|
|
2744
2891
|
});
|
|
2745
2892
|
const j = await r.json().catch(() => ({}));
|
|
2746
2893
|
if (!r.ok || !j.ok) {
|
|
2747
2894
|
logger.warn(`[platform-tunnel] 大厅打卡失败 bot=${bot.appId} chat=${hallChatId.substring(0, 12)}: ${j.error ?? r.status}`);
|
|
2748
2895
|
}
|
|
2749
2896
|
else {
|
|
2750
|
-
|
|
2897
|
+
sent = !j.skipped;
|
|
2898
|
+
const mentioned = j.mentioned ?? [];
|
|
2899
|
+
const unresolved = j.unresolved ?? [];
|
|
2900
|
+
if (sent)
|
|
2901
|
+
logger.info(`[platform-tunnel] 大厅打卡已发 bot=${bot.appId} chat=${hallChatId.substring(0, 12)}${mentioned.length ? ` 点名=[${mentioned.join(',')}]` : ''}${unresolved.length ? ` 未解析=[${unresolved.join(',')}]` : ''}`);
|
|
2751
2902
|
}
|
|
2752
2903
|
}
|
|
2753
2904
|
catch (e) {
|
|
2754
2905
|
logger.warn(`[platform-tunnel] 大厅打卡请求异常 bot=${bot.appId}: ${e.message}`);
|
|
2755
2906
|
}
|
|
2907
|
+
bumpHallAnnounceState(throttleKey, sent);
|
|
2908
|
+
state[throttleKey] = { lastAt: now, tries: (st?.tries ?? 0) + (sent ? 1 : 0) };
|
|
2756
2909
|
}
|
|
2757
2910
|
}
|
|
2758
2911
|
}
|