botmux 2.55.0 → 2.56.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/herdr-backend.d.ts +80 -0
- package/dist/adapters/backend/herdr-backend.d.ts.map +1 -0
- package/dist/adapters/backend/herdr-backend.js +519 -0
- package/dist/adapters/backend/herdr-backend.js.map +1 -0
- package/dist/adapters/backend/session-backend-selector.d.ts +2 -2
- package/dist/adapters/backend/session-backend-selector.d.ts.map +1 -1
- package/dist/adapters/backend/session-backend-selector.js +19 -1
- package/dist/adapters/backend/session-backend-selector.js.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.d.ts +29 -0
- package/dist/adapters/backend/tmux-pipe-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-pipe-backend.js +51 -4
- package/dist/adapters/backend/tmux-pipe-backend.js.map +1 -1
- package/dist/adapters/backend/types.d.ts +7 -0
- package/dist/adapters/backend/types.d.ts.map +1 -1
- package/dist/adapters/backend/types.js.map +1 -1
- package/dist/adapters/cli/codex.d.ts.map +1 -1
- package/dist/adapters/cli/codex.js +11 -0
- package/dist/adapters/cli/codex.js.map +1 -1
- package/dist/bot-registry.d.ts +2 -1
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +262 -15
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +2 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +13 -3
- package/dist/config.js.map +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +12 -7
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/pending-response.d.ts +16 -1
- package/dist/core/pending-response.d.ts.map +1 -1
- package/dist/core/pending-response.js +19 -1
- package/dist/core/pending-response.js.map +1 -1
- package/dist/core/session-discovery.d.ts +20 -4
- package/dist/core/session-discovery.d.ts.map +1 -1
- package/dist/core/session-discovery.js +228 -72
- package/dist/core/session-discovery.js.map +1 -1
- package/dist/core/session-manager.d.ts +2 -1
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +77 -45
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/types.d.ts +8 -1
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +74 -62
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.js +4 -4
- package/dist/daemon.js.map +1 -1
- package/dist/i18n/en.d.ts.map +1 -1
- package/dist/i18n/en.js +1 -0
- package/dist/i18n/en.js.map +1 -1
- package/dist/i18n/zh.d.ts.map +1 -1
- package/dist/i18n/zh.js +1 -0
- package/dist/i18n/zh.js.map +1 -1
- package/dist/im/lark/card-builder.d.ts +2 -1
- package/dist/im/lark/card-builder.d.ts.map +1 -1
- package/dist/im/lark/card-builder.js +19 -2
- 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 +5 -3
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/event-dispatcher.js +3 -3
- package/dist/im/lark/event-dispatcher.js.map +1 -1
- package/dist/setup/agent-preset.d.ts +78 -0
- package/dist/setup/agent-preset.d.ts.map +1 -0
- package/dist/setup/agent-preset.js +127 -0
- package/dist/setup/agent-preset.js.map +1 -0
- package/dist/setup/bot-config-editor.js +2 -2
- package/dist/setup/bot-config-editor.js.map +1 -1
- package/dist/setup/ensure-herdr-integrations.d.ts +26 -0
- package/dist/setup/ensure-herdr-integrations.d.ts.map +1 -0
- package/dist/setup/ensure-herdr-integrations.js +127 -0
- package/dist/setup/ensure-herdr-integrations.js.map +1 -0
- package/dist/setup/ensure-herdr.d.ts +12 -0
- package/dist/setup/ensure-herdr.d.ts.map +1 -0
- package/dist/setup/ensure-herdr.js +70 -0
- package/dist/setup/ensure-herdr.js.map +1 -0
- package/dist/setup/ensure-opus.d.ts +13 -0
- package/dist/setup/ensure-opus.d.ts.map +1 -0
- package/dist/setup/ensure-opus.js +64 -0
- package/dist/setup/ensure-opus.js.map +1 -0
- package/dist/setup/ensure-tmux.d.ts +13 -0
- package/dist/setup/ensure-tmux.d.ts.map +1 -1
- package/dist/setup/ensure-tmux.js +4 -4
- package/dist/setup/ensure-tmux.js.map +1 -1
- package/dist/setup/index.d.ts +4 -0
- package/dist/setup/index.d.ts.map +1 -1
- package/dist/setup/index.js +78 -1
- package/dist/setup/index.js.map +1 -1
- package/dist/types.d.ts +14 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/transient-snapshot.d.ts +3 -3
- package/dist/utils/transient-snapshot.js +3 -3
- package/dist/worker.js +251 -179
- package/dist/worker.js.map +1 -1
- package/dist/workflows/attempt-resume.d.ts +2 -1
- package/dist/workflows/attempt-resume.d.ts.map +1 -1
- package/dist/workflows/attempt-resume.js.map +1 -1
- package/dist/workflows/definition.d.ts +22 -22
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -31,13 +31,14 @@ import { enableAutostart, disableAutostart, autostartStatus, refreshAutostart }
|
|
|
31
31
|
import { tmuxEnv } from './setup/ensure-tmux.js';
|
|
32
32
|
import { writeBotsJsonAtomic as writeBotsAtomic } from './setup/bots-store.js';
|
|
33
33
|
import { applyBotConfigEdits, assertUniqueBotProcessNames, botProcessName, normalizeBotConfig, parseBotConfigsJson, parseBotSelection, removeBotConfig, resolveCliId, assertOwnerWhenChatGroups, findInvalidAllowedUserEntries, hasOwnerEntry, } from './setup/bot-config-editor.js';
|
|
34
|
+
import { buildPreset, serializePreset, presetFilename } from './setup/agent-preset.js';
|
|
34
35
|
import { createCliAdapterSync } from './adapters/cli/registry.js';
|
|
35
36
|
import { logger } from './utils/logger.js';
|
|
36
37
|
import { invalidWorkingDirs } from './utils/working-dir.js';
|
|
37
38
|
import { firstPositional } from './cli/arg-utils.js';
|
|
38
39
|
import { formatBotInfoEntriesForCli, formatChatBotsForCli, } from './cli/bots-list-output.js';
|
|
39
40
|
import { buildFooterAddressing, hasKnownBotMention, knownBotOpenIdsFromCrossRef, orderedFooterRecipients, } from './utils/bot-routing.js';
|
|
40
|
-
import { isLocale, setDefaultLocale, SUPPORTED_LOCALES } from './i18n/index.js';
|
|
41
|
+
import { isLocale, localeForBot, setDefaultLocale, SUPPORTED_LOCALES } from './i18n/index.js';
|
|
41
42
|
import { readGlobalConfig, setGlobalLocale, globalConfigPath } from './global-config.js';
|
|
42
43
|
// Resolve the CLI's UI locale once from the global config file, so subsequent
|
|
43
44
|
// CLI output (and any t() callers that don't pass an explicit locale) honour
|
|
@@ -748,8 +749,8 @@ async function promptEditBotConfig(rl, bot) {
|
|
|
748
749
|
input.model = cliChanged && result === undefined ? null : result;
|
|
749
750
|
}
|
|
750
751
|
printInputHelp('会话后端 backendType', [
|
|
751
|
-
'可选。pty 更轻量;tmux 支持 adopt 和 Web Terminal 附着;
|
|
752
|
-
'留空保留当前值;输入 - 回到自动检测;接受 pty / tmux / zellij。',
|
|
752
|
+
'可选。pty 更轻量;tmux 支持 adopt 和 Web Terminal 附着;herdr 支持托管持久会话;zellij 为实验后端(需 zellij >= 0.44)。',
|
|
753
|
+
'留空保留当前值;输入 - 回到自动检测;接受 pty / tmux / herdr / zellij。',
|
|
753
754
|
]);
|
|
754
755
|
input.backendType = await ask(rl, `会话后端 backendType [${formatOptionalValue(bot.backendType)}]: `);
|
|
755
756
|
printInputHelp('默认工作目录', [
|
|
@@ -2210,6 +2211,12 @@ botmux v${getVersion()} — IM ↔ AI 编程 CLI 桥接
|
|
|
2210
2211
|
create-group --bot <name> [--bot ...] [--name "群名"]
|
|
2211
2212
|
用指定 bot 起新群;详见 \`botmux create-group --help\`
|
|
2212
2213
|
|
|
2214
|
+
预设分享(导出某 bot 的可分享配置给同事,绝不含密钥):
|
|
2215
|
+
preset export <bot> [--from-chat <chatId>] [--out <file>] [--yes]
|
|
2216
|
+
导出 cliId/model/角色/能力标签 + 接入指引;
|
|
2217
|
+
默认 team 级角色,--from-chat 取某群角色内容;
|
|
2218
|
+
缺省写 ./<name或appid>.botmux-preset.json,--out - 走 stdout
|
|
2219
|
+
|
|
2213
2220
|
配置目录: ~/.botmux/
|
|
2214
2221
|
文档: https://github.com/deepcoldy/botmux
|
|
2215
2222
|
`);
|
|
@@ -2285,6 +2292,26 @@ function argValue(args, ...flags) {
|
|
|
2285
2292
|
function argFlag(args, flag) {
|
|
2286
2293
|
return args.includes(flag);
|
|
2287
2294
|
}
|
|
2295
|
+
/**
|
|
2296
|
+
* True when `flag` is present but lacks a usable value — i.e. it's the last
|
|
2297
|
+
* token, is followed by another flag, or was given as `--flag=` (empty). Lets
|
|
2298
|
+
* callers surface a friendly error instead of silently falling back to a
|
|
2299
|
+
* default (e.g. treating a value-less `--from-chat` as "no chat"). `allowDash`
|
|
2300
|
+
* permits a bare `-` value (used by `--out -` to mean stdout).
|
|
2301
|
+
*/
|
|
2302
|
+
function flagPresentButValueMissing(args, flag, allowDash = false) {
|
|
2303
|
+
const i = args.findIndex(a => a === flag || a.startsWith(flag + '='));
|
|
2304
|
+
if (i < 0)
|
|
2305
|
+
return false; // absent entirely — not "missing a value"
|
|
2306
|
+
if (args[i].startsWith(flag + '='))
|
|
2307
|
+
return args[i].slice(flag.length + 1) === '';
|
|
2308
|
+
const next = args[i + 1];
|
|
2309
|
+
if (next === undefined)
|
|
2310
|
+
return true;
|
|
2311
|
+
if (next.startsWith('-'))
|
|
2312
|
+
return !(allowDash && next === '-');
|
|
2313
|
+
return false;
|
|
2314
|
+
}
|
|
2288
2315
|
/** Extract positional args, skipping --flag and the value that follows it
|
|
2289
2316
|
* (for --flag <value> style). --flag=value style is self-contained.
|
|
2290
2317
|
* `booleanFlags` lists flags that take no value — without this hint the
|
|
@@ -2643,9 +2670,10 @@ function argValues(args, ...flags) {
|
|
|
2643
2670
|
}
|
|
2644
2671
|
// Card v2 body builder helpers — extracted to im/lark/md-card.ts so the
|
|
2645
2672
|
// daemon's bridge fallback path can produce identical cards. cmdSend
|
|
2646
|
-
// keeps using `buildCardBodyElements`
|
|
2673
|
+
// keeps using `buildCardBodyElements` from there.
|
|
2674
|
+
import { buildMentionedPendingResponseCard } from './im/lark/card-builder.js';
|
|
2647
2675
|
import { buildCardBodyElements, brandFooterSegment } from './im/lark/md-card.js';
|
|
2648
|
-
import { claimPendingResponseCard, markPendingResponseCardPatchedIfCurrent, mergePendingResponseState } from './core/pending-response.js';
|
|
2676
|
+
import { COMPLETED_REACTION_EMOJI_TYPE, claimPendingResponseCard, markPendingResponseCardPatchedIfCurrent, mergePendingResponseState, shouldMarkPendingAsMentionedSend, shouldPatchPendingOnExplicitSend } from './core/pending-response.js';
|
|
2649
2677
|
import { resolveBrandLabel } from './bot-registry.js';
|
|
2650
2678
|
import { config } from './config.js';
|
|
2651
2679
|
import { resolveQuoteTarget, validateMentionDecision } from './services/send-policy.js';
|
|
@@ -2843,7 +2871,7 @@ async function cmdSend(rest) {
|
|
|
2843
2871
|
registerBot(cfg);
|
|
2844
2872
|
}
|
|
2845
2873
|
catch { /* */ }
|
|
2846
|
-
const { sendMessage, replyMessage, uploadImage, uploadFile,
|
|
2874
|
+
const { sendMessage, replyMessage, uploadImage, uploadFile, updateMessage, addReaction, MessageWithdrawnError } = await import('./im/lark/client.js');
|
|
2847
2875
|
const appId = s.larkAppId;
|
|
2848
2876
|
// Effective target chat for top-level mode (defaults to session's chat)
|
|
2849
2877
|
const targetChatId = overrideChatId ?? s.chatId;
|
|
@@ -2914,16 +2942,50 @@ async function cmdSend(rest) {
|
|
|
2914
2942
|
catch { /* best-effort: marker miss only causes a redundant fallback message */ }
|
|
2915
2943
|
};
|
|
2916
2944
|
const shouldRecordBridgeMarker = !sendTopLevel && !overrideChatId && !sendInto;
|
|
2945
|
+
let hasNotificationMentionsForPending = mentionArgs.length > 0;
|
|
2917
2946
|
const dispatchOrPatchPending = async (content, msgType) => {
|
|
2918
|
-
const pendingCardId = msgType
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
if (pendingCardId
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2947
|
+
const pendingCardId = shouldPatchPendingOnExplicitSend(s, { msgType, sendTopLevel, overrideChatId: !!overrideChatId, sendInto: !!sendInto, hasNotificationMentions: hasNotificationMentionsForPending })
|
|
2948
|
+
? claimPendingResponseCard(s)
|
|
2949
|
+
: undefined;
|
|
2950
|
+
if (!pendingCardId) {
|
|
2951
|
+
const sentId = await dispatchPrimary(content, msgType);
|
|
2952
|
+
if (shouldMarkPendingAsMentionedSend({ msgType, sendTopLevel, overrideChatId: !!overrideChatId, sendInto: !!sendInto, hasNotificationMentions: hasNotificationMentionsForPending })) {
|
|
2953
|
+
const stalePendingCardId = claimPendingResponseCard(s);
|
|
2954
|
+
const latest = stalePendingCardId ? loadSessionFresh(s) : undefined;
|
|
2955
|
+
if (stalePendingCardId && latest?.pendingResponseCardId === stalePendingCardId) {
|
|
2956
|
+
updateMessage(appId, stalePendingCardId, buildMentionedPendingResponseCard(localeForBot(appId)))
|
|
2957
|
+
.then(() => {
|
|
2958
|
+
if (markPendingResponseCardPatchedIfCurrent(latest, stalePendingCardId))
|
|
2959
|
+
saveSession(latest);
|
|
2960
|
+
})
|
|
2961
|
+
.catch((err) => logger.warn(`[send:${sid.substring(0, 8)}] failed to mark pending card after mentioned send: ${err?.message ?? err}`));
|
|
2962
|
+
}
|
|
2963
|
+
}
|
|
2964
|
+
return sentId;
|
|
2965
|
+
}
|
|
2966
|
+
const latest = loadSessionFresh(s);
|
|
2967
|
+
if (latest?.pendingResponseCardId !== pendingCardId)
|
|
2968
|
+
return dispatchPrimary(content, msgType);
|
|
2969
|
+
try {
|
|
2970
|
+
await updateMessage(appId, pendingCardId, content);
|
|
2971
|
+
if (markPendingResponseCardPatchedIfCurrent(latest, pendingCardId)) {
|
|
2972
|
+
saveSession(latest);
|
|
2973
|
+
if (latest.quoteTargetId) {
|
|
2974
|
+
addReaction(appId, latest.quoteTargetId, COMPLETED_REACTION_EMOJI_TYPE)
|
|
2975
|
+
.catch((err) => logger.warn(`[send:${sid.substring(0, 8)}] failed to add completion reaction to ${latest.quoteTargetId}: ${err?.message ?? err}`));
|
|
2976
|
+
}
|
|
2977
|
+
}
|
|
2978
|
+
return pendingCardId;
|
|
2979
|
+
}
|
|
2980
|
+
catch (err) {
|
|
2981
|
+
if (err instanceof MessageWithdrawnError) {
|
|
2982
|
+
logger.warn(`[send:${sid.substring(0, 8)}] pending card withdrawn before explicit send patch; sending a new reply`);
|
|
2983
|
+
markPendingResponseCardPatchedIfCurrent(latest, pendingCardId);
|
|
2984
|
+
saveSession(latest);
|
|
2985
|
+
return dispatchPrimary(content, msgType);
|
|
2986
|
+
}
|
|
2987
|
+
throw err;
|
|
2988
|
+
}
|
|
2927
2989
|
};
|
|
2928
2990
|
// Quote chain (普通群): the primary message replies to the turn's target so
|
|
2929
2991
|
// Lark renders a 引用 chain. --quote overrides, --no-quote opts out. Thread
|
|
@@ -3066,6 +3128,7 @@ async function cmdSend(rest) {
|
|
|
3066
3128
|
}
|
|
3067
3129
|
catch { /* best-effort */ }
|
|
3068
3130
|
const explicitKnownBotMention = hasKnownBotMention(text, mentions, botEntries, crossRef, appId);
|
|
3131
|
+
hasNotificationMentionsForPending ||= explicitKnownBotMention;
|
|
3069
3132
|
const knownBotOpenIds = knownBotOpenIdsFromCrossRef(crossRef, botEntries, appId);
|
|
3070
3133
|
// --no-mention 显式不 @ 任何人 → 连 footer 的"发送给/cc"寻址 <at> 也清空,
|
|
3071
3134
|
// 否则 footer 仍会 @ 人,与 --no-mention 语义和"未@任何人"输出自相矛盾
|
|
@@ -4190,6 +4253,168 @@ function cmdLang(args) {
|
|
|
4190
4253
|
console.log(`✅ Set global lang → ${target}.`);
|
|
4191
4254
|
console.log(`Run \`botmux restart\` for changes to take effect.`);
|
|
4192
4255
|
}
|
|
4256
|
+
// ─── botmux preset ────────────────────────────────────────────────────────────
|
|
4257
|
+
/**
|
|
4258
|
+
* `botmux preset <sub>` dispatcher. Currently only `export`.
|
|
4259
|
+
*/
|
|
4260
|
+
async function cmdPreset(sub, rest) {
|
|
4261
|
+
switch (sub) {
|
|
4262
|
+
case 'export':
|
|
4263
|
+
await cmdPresetExport(rest);
|
|
4264
|
+
break;
|
|
4265
|
+
default:
|
|
4266
|
+
console.error('用法: botmux preset export <bot> [--from-chat <chatId>] [--out <file>] [--yes]');
|
|
4267
|
+
process.exit(1);
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
/**
|
|
4271
|
+
* `botmux preset export <bot> [--from-chat <chatId>] [--out <file>] [--yes]`
|
|
4272
|
+
*
|
|
4273
|
+
* Export a bot's **shareable, secret-free** preset (cliId / model / team role /
|
|
4274
|
+
* capability + an embedded guide) so a teammate's agent can self-configure a
|
|
4275
|
+
* matching bot. Never emits credentials or deployment fields — see
|
|
4276
|
+
* agent-preset.ts:buildPreset for the allow-list guarantee.
|
|
4277
|
+
*
|
|
4278
|
+
* Role source: team-level by default; `--from-chat <chatId>` exports that
|
|
4279
|
+
* group's role content instead (the chatId itself is dropped). Both role and
|
|
4280
|
+
* capability resolve under the effective data dir: this fn sets
|
|
4281
|
+
* `SESSION_DATA_DIR ??= resolveDataDir()` (SESSION_DATA_DIR → ~/.botmux
|
|
4282
|
+
* breadcrumb → default), and reads it via config.session.dataDir's lazy getter —
|
|
4283
|
+
* correct in agent sessions and bare-shell runs alike.
|
|
4284
|
+
*/
|
|
4285
|
+
async function cmdPresetExport(rest) {
|
|
4286
|
+
process.env.SESSION_DATA_DIR ??= resolveDataDir();
|
|
4287
|
+
const USAGE = '用法: botmux preset export <bot> [--from-chat <chatId>] [--out <file>] [--yes]';
|
|
4288
|
+
const selection = firstPositional(rest, ['--from-chat', '--out']);
|
|
4289
|
+
if (!selection) {
|
|
4290
|
+
console.error(USAGE);
|
|
4291
|
+
console.error(' <bot> 进程名 (botmux-xxx) 或 larkAppId');
|
|
4292
|
+
process.exit(1);
|
|
4293
|
+
return;
|
|
4294
|
+
}
|
|
4295
|
+
const bots = loadBotsJson();
|
|
4296
|
+
if (bots.length === 0) {
|
|
4297
|
+
console.error('❌ 没有可用的 bot:未找到 bots.json 或其中为空。先跑 `botmux setup`。');
|
|
4298
|
+
process.exit(1);
|
|
4299
|
+
return;
|
|
4300
|
+
}
|
|
4301
|
+
const idx = parseBotSelection(selection, bots);
|
|
4302
|
+
if (idx === undefined) {
|
|
4303
|
+
console.error(`❌ 找不到 bot "${selection}"。可选:`);
|
|
4304
|
+
bots.forEach((b, i) => {
|
|
4305
|
+
const appId = typeof b.larkAppId === 'string' ? b.larkAppId : '(无 larkAppId)';
|
|
4306
|
+
console.error(` - ${botProcessName(b, i)} (${appId})`);
|
|
4307
|
+
});
|
|
4308
|
+
process.exit(1);
|
|
4309
|
+
return;
|
|
4310
|
+
}
|
|
4311
|
+
const bot = bots[idx];
|
|
4312
|
+
const appId = typeof bot.larkAppId === 'string' ? bot.larkAppId : '';
|
|
4313
|
+
if (!appId) {
|
|
4314
|
+
console.error(`❌ bot "${selection}" 缺少 larkAppId,无法解析角色/能力。`);
|
|
4315
|
+
process.exit(1);
|
|
4316
|
+
return;
|
|
4317
|
+
}
|
|
4318
|
+
if (!bot.cliId || typeof bot.cliId !== 'string') {
|
|
4319
|
+
console.error(`❌ bot "${selection}" 缺少 cliId,无法导出预设。`);
|
|
4320
|
+
process.exit(1);
|
|
4321
|
+
return;
|
|
4322
|
+
}
|
|
4323
|
+
// Fail loudly when a flag was given without a value, instead of silently
|
|
4324
|
+
// exporting as if it weren't passed (e.g. a value-less `--from-chat` would
|
|
4325
|
+
// otherwise quietly fall back to the team role).
|
|
4326
|
+
if (flagPresentButValueMissing(rest, '--from-chat')) {
|
|
4327
|
+
console.error('❌ --from-chat 需要一个 chatId(如 oc_xxx)。');
|
|
4328
|
+
console.error(USAGE);
|
|
4329
|
+
process.exit(1);
|
|
4330
|
+
return;
|
|
4331
|
+
}
|
|
4332
|
+
if (flagPresentButValueMissing(rest, '--out', true)) {
|
|
4333
|
+
console.error('❌ --out 需要一个文件路径,或用 `--out -` 输出到 stdout。');
|
|
4334
|
+
console.error(USAGE);
|
|
4335
|
+
process.exit(1);
|
|
4336
|
+
return;
|
|
4337
|
+
}
|
|
4338
|
+
const fromChat = argValue(rest, '--from-chat');
|
|
4339
|
+
const out = argValue(rest, '--out');
|
|
4340
|
+
const skipConfirm = argFlag(rest, '--yes') || argFlag(rest, '-y');
|
|
4341
|
+
// capability + role read the SAME data dir. config.session.dataDir is a lazy
|
|
4342
|
+
// getter, so the SESSION_DATA_DIR set at the top of this fn (= resolveDataDir())
|
|
4343
|
+
// is honored — correct for both agent sessions AND bare-shell runs (no longer
|
|
4344
|
+
// the frozen packaged default).
|
|
4345
|
+
const dataDir = config.session.dataDir;
|
|
4346
|
+
const { resolveTeamRoleFile, resolveRoleFile } = await import('./core/role-resolver.js');
|
|
4347
|
+
const { getBotCapability } = await import('./services/bot-profile-store.js');
|
|
4348
|
+
let teamRole;
|
|
4349
|
+
if (fromChat) {
|
|
4350
|
+
teamRole = resolveRoleFile(appId, fromChat);
|
|
4351
|
+
if (teamRole === null) {
|
|
4352
|
+
console.error(`⚠️ 群 ${fromChat} 下没有为该 bot 配置角色;导出将不含 teamRole(仍含 cliId/model/capability)。`);
|
|
4353
|
+
}
|
|
4354
|
+
}
|
|
4355
|
+
else {
|
|
4356
|
+
teamRole = resolveTeamRoleFile(appId);
|
|
4357
|
+
if (teamRole === null) {
|
|
4358
|
+
console.error('⚠️ 该 bot 没有 team 级角色;导出将不含 teamRole。可加 `--from-chat <chatId>` 导出某群的角色内容。');
|
|
4359
|
+
}
|
|
4360
|
+
}
|
|
4361
|
+
const capability = getBotCapability(dataDir, appId);
|
|
4362
|
+
const sourceName = typeof bot.name === 'string' && bot.name.trim() ? bot.name.trim() : undefined;
|
|
4363
|
+
const preset = buildPreset({
|
|
4364
|
+
cliId: bot.cliId,
|
|
4365
|
+
model: typeof bot.model === 'string' ? bot.model : undefined,
|
|
4366
|
+
teamRole,
|
|
4367
|
+
capability,
|
|
4368
|
+
sourceName,
|
|
4369
|
+
});
|
|
4370
|
+
const json = serializePreset(preset);
|
|
4371
|
+
// Confirm before writing — the role may carry internal info. --yes skips.
|
|
4372
|
+
if (!skipConfirm) {
|
|
4373
|
+
if (!process.stdin.isTTY) {
|
|
4374
|
+
console.error('❌ 角色内容可能含内部信息,导出前需确认;非交互环境(如 agent 调用)请加 `--yes` 跳过确认。');
|
|
4375
|
+
process.exit(1);
|
|
4376
|
+
return;
|
|
4377
|
+
}
|
|
4378
|
+
if (teamRole || capability) {
|
|
4379
|
+
console.error('\n即将导出以下内容,请确认不含敏感/内部信息:');
|
|
4380
|
+
console.error('────────────────────────────────────────');
|
|
4381
|
+
if (teamRole)
|
|
4382
|
+
console.error(`[角色 teamRole]\n${teamRole}`);
|
|
4383
|
+
if (capability)
|
|
4384
|
+
console.error(`[能力标签 capability] ${capability}`);
|
|
4385
|
+
console.error('────────────────────────────────────────');
|
|
4386
|
+
}
|
|
4387
|
+
else {
|
|
4388
|
+
console.error('\n(无角色 / 能力标签内容,仅导出 cliId/model)');
|
|
4389
|
+
}
|
|
4390
|
+
// Prompt on stderr so a piped stdout (--out -) stays clean.
|
|
4391
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
4392
|
+
const answer = (await ask(rl, '确认导出?输入 y 继续,其它取消: ')).trim().toLowerCase();
|
|
4393
|
+
rl.close();
|
|
4394
|
+
if (answer !== 'y' && answer !== 'yes') {
|
|
4395
|
+
console.error('已取消,未写入任何文件。');
|
|
4396
|
+
process.exit(1);
|
|
4397
|
+
return;
|
|
4398
|
+
}
|
|
4399
|
+
}
|
|
4400
|
+
// stdout mode: the JSON must own stdout; all chatter goes to stderr.
|
|
4401
|
+
if (out === '-') {
|
|
4402
|
+
process.stdout.write(json);
|
|
4403
|
+
console.error('✅ 已输出到 stdout。本文件不含任何密钥(larkAppId/secret/allowedUsers 等均未包含)。');
|
|
4404
|
+
return;
|
|
4405
|
+
}
|
|
4406
|
+
const outPath = out ?? `./${presetFilename(sourceName, appId)}`;
|
|
4407
|
+
try {
|
|
4408
|
+
writeFileSync(outPath, json, 'utf-8');
|
|
4409
|
+
}
|
|
4410
|
+
catch (err) {
|
|
4411
|
+
console.error(`❌ 写入 ${outPath} 失败: ${err?.message ?? String(err)}`);
|
|
4412
|
+
process.exit(1);
|
|
4413
|
+
return;
|
|
4414
|
+
}
|
|
4415
|
+
console.error(`✅ 已导出预设到 ${outPath}`);
|
|
4416
|
+
console.error(' 本文件不含任何密钥(larkAppId/secret/allowedUsers/workingDir 等均未包含),可安全分享。');
|
|
4417
|
+
}
|
|
4193
4418
|
// ─── Main ────────────────────────────────────────────────────────────────────
|
|
4194
4419
|
function getVersion() {
|
|
4195
4420
|
const pkgPath = join(PKG_ROOT, 'package.json');
|
|
@@ -4318,6 +4543,25 @@ async function cmdVoiceSetup(args) {
|
|
|
4318
4543
|
mergeGlobalConfig({ voice: voice });
|
|
4319
4544
|
console.log('\n✅ 已写入 voice 配置。`botmux restart` 后,配了语音的机器人回复卡片底部会出现「🔊 语音总结」按钮。');
|
|
4320
4545
|
console.log(' 查看:`botmux voice status` 关闭:`botmux voice disable`');
|
|
4546
|
+
// 语音合成产物要编码成飞书语音气泡用的 opus,依赖系统的 opusenc(opus-tools)。
|
|
4547
|
+
// 缺了就当场帮用户装(沿用 ensure-tmux 的包管理器/sudo 机制)。
|
|
4548
|
+
const { ensureOpusTools, probeOpusenc } = await import('./setup/ensure-opus.js');
|
|
4549
|
+
if (!probeOpusenc()) {
|
|
4550
|
+
console.log('\n⚠️ 未检测到 opus 编码器(opus-tools)——语音合成需要它把音频转成飞书语音格式。');
|
|
4551
|
+
const yes = (await ask(rl, '现在自动安装 opus-tools?(Y/n): ')).trim().toLowerCase();
|
|
4552
|
+
if (yes === '' || yes === 'y' || yes === 'yes') {
|
|
4553
|
+
const r = await ensureOpusTools();
|
|
4554
|
+
if (r.installed)
|
|
4555
|
+
console.log(`✅ opus-tools 就绪${r.version ? `(${r.version})` : ''}`);
|
|
4556
|
+
else {
|
|
4557
|
+
console.log(`未能自动安装:${r.reason ?? ''}`);
|
|
4558
|
+
console.log(`请手动安装后再用语音:${r.manualCommand ?? 'apt-get install -y opus-tools / brew install opus-tools'}`);
|
|
4559
|
+
}
|
|
4560
|
+
}
|
|
4561
|
+
else {
|
|
4562
|
+
console.log('已跳过。记得手动安装:Debian/Ubuntu `sudo apt-get install -y opus-tools`,macOS `brew install opus-tools`。');
|
|
4563
|
+
}
|
|
4564
|
+
}
|
|
4321
4565
|
}
|
|
4322
4566
|
finally {
|
|
4323
4567
|
rl.close();
|
|
@@ -4401,6 +4645,9 @@ switch (command) {
|
|
|
4401
4645
|
case 'bots':
|
|
4402
4646
|
await cmdBots(process.argv[3] ?? 'list', process.argv.slice(4));
|
|
4403
4647
|
break;
|
|
4648
|
+
case 'preset':
|
|
4649
|
+
await cmdPreset(process.argv[3] ?? '', process.argv.slice(4));
|
|
4650
|
+
break;
|
|
4404
4651
|
case 'history':
|
|
4405
4652
|
await cmdHistory(process.argv.slice(3));
|
|
4406
4653
|
break;
|