botmux 2.38.1 → 2.39.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/adopt-route.d.ts +63 -0
- package/dist/adapters/adopt-route.d.ts.map +1 -0
- package/dist/adapters/adopt-route.js +195 -0
- package/dist/adapters/adopt-route.js.map +1 -0
- package/dist/adapters/backend/tmux-backend.d.ts.map +1 -1
- package/dist/adapters/backend/tmux-backend.js +8 -0
- package/dist/adapters/backend/tmux-backend.js.map +1 -1
- package/dist/adapters/cli/claude-code.d.ts.map +1 -1
- package/dist/adapters/cli/claude-code.js +14 -7
- package/dist/adapters/cli/claude-code.js.map +1 -1
- package/dist/adapters/cli/opencode.d.ts.map +1 -1
- package/dist/adapters/cli/opencode.js +7 -0
- package/dist/adapters/cli/opencode.js.map +1 -1
- package/dist/adapters/cli/types.d.ts +11 -0
- package/dist/adapters/cli/types.d.ts.map +1 -1
- package/dist/adapters/hook-command.d.ts +18 -0
- package/dist/adapters/hook-command.d.ts.map +1 -0
- package/dist/adapters/hook-command.js +38 -0
- package/dist/adapters/hook-command.js.map +1 -0
- package/dist/adapters/hook-installer.d.ts +14 -0
- package/dist/adapters/hook-installer.d.ts.map +1 -0
- package/dist/adapters/hook-installer.js +172 -0
- package/dist/adapters/hook-installer.js.map +1 -0
- package/dist/bot-registry.d.ts +17 -0
- package/dist/bot-registry.d.ts.map +1 -1
- package/dist/bot-registry.js +52 -0
- package/dist/bot-registry.js.map +1 -1
- package/dist/cli.d.ts +15 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +369 -57
- package/dist/cli.js.map +1 -1
- package/dist/core/ask-api.d.ts +47 -0
- package/dist/core/ask-api.d.ts.map +1 -0
- package/dist/core/ask-api.js +139 -0
- package/dist/core/ask-api.js.map +1 -0
- package/dist/core/ask-args.d.ts +53 -0
- package/dist/core/ask-args.d.ts.map +1 -0
- package/dist/core/ask-args.js +122 -0
- package/dist/core/ask-args.js.map +1 -0
- package/dist/core/ask-broker.d.ts +98 -0
- package/dist/core/ask-broker.d.ts.map +1 -0
- package/dist/core/ask-broker.js +329 -0
- package/dist/core/ask-broker.js.map +1 -0
- package/dist/core/ask-hook/claude-code.d.ts +50 -0
- package/dist/core/ask-hook/claude-code.d.ts.map +1 -0
- package/dist/core/ask-hook/claude-code.js +145 -0
- package/dist/core/ask-hook/claude-code.js.map +1 -0
- package/dist/core/ask-hook/codex.d.ts +43 -0
- package/dist/core/ask-hook/codex.d.ts.map +1 -0
- package/dist/core/ask-hook/codex.js +69 -0
- package/dist/core/ask-hook/codex.js.map +1 -0
- package/dist/core/ask-hook/opencode.d.ts +41 -0
- package/dist/core/ask-hook/opencode.d.ts.map +1 -0
- package/dist/core/ask-hook/opencode.js +108 -0
- package/dist/core/ask-hook/opencode.js.map +1 -0
- package/dist/core/ask-hook/registry.d.ts +3 -0
- package/dist/core/ask-hook/registry.d.ts.map +1 -0
- package/dist/core/ask-hook/registry.js +12 -0
- package/dist/core/ask-hook/registry.js.map +1 -0
- package/dist/core/ask-hook/types.d.ts +26 -0
- package/dist/core/ask-hook/types.d.ts.map +1 -0
- package/dist/core/ask-hook/types.js +2 -0
- package/dist/core/ask-hook/types.js.map +1 -0
- package/dist/core/ask-types.d.ts +146 -0
- package/dist/core/ask-types.d.ts.map +1 -0
- package/dist/core/ask-types.js +18 -0
- package/dist/core/ask-types.js.map +1 -0
- package/dist/core/dashboard-ipc-server.d.ts.map +1 -1
- package/dist/core/dashboard-ipc-server.js +21 -0
- package/dist/core/dashboard-ipc-server.js.map +1 -1
- package/dist/core/worker-pool.d.ts.map +1 -1
- package/dist/core/worker-pool.js +22 -3
- package/dist/core/worker-pool.js.map +1 -1
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +92 -1
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard/web/bot-defaults.d.ts.map +1 -1
- package/dist/dashboard/web/bot-defaults.js +80 -0
- package/dist/dashboard/web/bot-defaults.js.map +1 -1
- package/dist/dashboard/web/i18n.d.ts.map +1 -1
- package/dist/dashboard/web/i18n.js +16 -0
- package/dist/dashboard/web/i18n.js.map +1 -1
- package/dist/dashboard-web/app.js +195 -180
- package/dist/dashboard.js +19 -0
- package/dist/dashboard.js.map +1 -1
- package/dist/im/lark/ask-card.d.ts +55 -0
- package/dist/im/lark/ask-card.d.ts.map +1 -0
- package/dist/im/lark/ask-card.js +328 -0
- package/dist/im/lark/ask-card.js.map +1 -0
- package/dist/im/lark/card-handler.d.ts.map +1 -1
- package/dist/im/lark/card-handler.js +4 -0
- package/dist/im/lark/card-handler.js.map +1 -1
- package/dist/im/lark/md-card.d.ts +20 -2
- package/dist/im/lark/md-card.d.ts.map +1 -1
- package/dist/im/lark/md-card.js +49 -17
- package/dist/im/lark/md-card.js.map +1 -1
- package/dist/im/lark/message-parser.js +12 -0
- package/dist/im/lark/message-parser.js.map +1 -1
- package/dist/services/brand-store.d.ts +15 -0
- package/dist/services/brand-store.d.ts.map +1 -0
- package/dist/services/brand-store.js +47 -0
- package/dist/services/brand-store.js.map +1 -0
- package/dist/skills/definitions.d.ts +2 -0
- package/dist/skills/definitions.d.ts.map +1 -1
- package/dist/skills/definitions.js +67 -0
- package/dist/skills/definitions.js.map +1 -1
- package/dist/skills/installer.d.ts +11 -0
- package/dist/skills/installer.d.ts.map +1 -1
- package/dist/skills/installer.js +35 -1
- package/dist/skills/installer.js.map +1 -1
- package/dist/utils/bot-routing.d.ts +19 -0
- package/dist/utils/bot-routing.d.ts.map +1 -1
- package/dist/utils/bot-routing.js +29 -0
- package/dist/utils/bot-routing.js.map +1 -1
- package/dist/worker.js +7 -0
- package/dist/worker.js.map +1 -1
- package/dist/workflows/events/payloads.d.ts +2 -2
- package/dist/workflows/events/schema.d.ts +8 -8
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -33,7 +33,7 @@ import { logger } from './utils/logger.js';
|
|
|
33
33
|
import { invalidWorkingDirs } from './utils/working-dir.js';
|
|
34
34
|
import { firstPositional } from './cli/arg-utils.js';
|
|
35
35
|
import { formatBotInfoEntriesForCli, formatChatBotsForCli, } from './cli/bots-list-output.js';
|
|
36
|
-
import { buildFooterAddressing, hasKnownBotMention, knownBotOpenIdsFromCrossRef, } from './utils/bot-routing.js';
|
|
36
|
+
import { buildFooterAddressing, hasKnownBotMention, knownBotOpenIdsFromCrossRef, orderedFooterRecipients, } from './utils/bot-routing.js';
|
|
37
37
|
import { isLocale, setDefaultLocale, SUPPORTED_LOCALES } from './i18n/index.js';
|
|
38
38
|
import { readGlobalConfig, setGlobalLocale, globalConfigPath } from './global-config.js';
|
|
39
39
|
// Resolve the CLI's UI locale once from the global config file, so subsequent
|
|
@@ -2464,7 +2464,8 @@ function argValues(args, ...flags) {
|
|
|
2464
2464
|
// Card v2 body builder helpers — extracted to im/lark/md-card.ts so the
|
|
2465
2465
|
// daemon's bridge fallback path can produce identical cards. cmdSend
|
|
2466
2466
|
// keeps using `buildCardBodyElements` and `hasMarkdown` from there.
|
|
2467
|
-
import { buildCardBodyElements, hasMarkdown } from './im/lark/md-card.js';
|
|
2467
|
+
import { buildCardBodyElements, hasMarkdown, brandFooterSegment } from './im/lark/md-card.js';
|
|
2468
|
+
import { resolveBrandLabel } from './bot-registry.js';
|
|
2468
2469
|
import { config } from './config.js';
|
|
2469
2470
|
import { resolveQuoteTarget, validateMentionDecision } from './services/send-policy.js';
|
|
2470
2471
|
async function cmdSend(rest) {
|
|
@@ -2731,22 +2732,13 @@ async function cmdSend(rest) {
|
|
|
2731
2732
|
// --no-mention 显式不 @ 任何人 → 连 footer 的"发送给/cc"寻址 <at> 也清空,
|
|
2732
2733
|
// 否则 footer 仍会 @ 人,与 --no-mention 语义和"未@任何人"输出自相矛盾
|
|
2733
2734
|
// (Codex review P2)。--top-level 同样无特定收件人。
|
|
2734
|
-
const
|
|
2735
|
+
const footerAddressing = (sendTopLevel || noMention)
|
|
2735
2736
|
? { sendTo: undefined, cc: [] }
|
|
2736
2737
|
: buildFooterAddressing(s, {
|
|
2737
2738
|
isOncall: !!oncallEntry,
|
|
2738
2739
|
hasExplicitBotMention: explicitKnownBotMention,
|
|
2739
2740
|
knownBotOpenIds,
|
|
2740
2741
|
});
|
|
2741
|
-
// De-dupe vs body @: if someone is already @'d in the body (典型是
|
|
2742
|
-
// --mention-back @ 了触发者,而 footer 的"发送给"又指向同一个 owner/caller),
|
|
2743
|
-
// 从 footer 去掉,避免一条消息里出现两个相同的 @。
|
|
2744
|
-
const bodyMentionIds = new Set(mentions.map(m => m.open_id));
|
|
2745
|
-
const footerAddressing = {
|
|
2746
|
-
sendTo: footerAddressingRaw.sendTo && !bodyMentionIds.has(footerAddressingRaw.sendTo)
|
|
2747
|
-
? footerAddressingRaw.sendTo : undefined,
|
|
2748
|
-
cc: footerAddressingRaw.cc.filter(id => !bodyMentionIds.has(id)),
|
|
2749
|
-
};
|
|
2750
2742
|
// Decide: interactive card (renders markdown) vs. post (plain text).
|
|
2751
2743
|
// Explicit --card / --text wins; otherwise auto-detect markdown syntax.
|
|
2752
2744
|
const useCard = forceCard || (!forceText && hasMarkdown(text));
|
|
@@ -2781,12 +2773,9 @@ async function cmdSend(rest) {
|
|
|
2781
2773
|
return `<at id=${openId}></at>`;
|
|
2782
2774
|
});
|
|
2783
2775
|
}
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
trailingAts.push(`<at id=${m.open_id}></at>`);
|
|
2788
|
-
if (trailingAts.length > 0)
|
|
2789
|
-
md = md ? `${md}\n\n${trailingAts.join(' ')}` : trailingAts.join(' ');
|
|
2776
|
+
// Non-inlined mentions are no longer dangled as a trailing @ block at the
|
|
2777
|
+
// body bottom — they're consolidated onto the footer `发送给:` line below
|
|
2778
|
+
// (human addressee first, then explicit targets). See orderedFooterRecipients.
|
|
2790
2779
|
// Inline images into the markdown via . If caller used an
|
|
2791
2780
|
// `` placeholder, substitute by 0-based index; any remaining
|
|
2792
2781
|
// images get appended at the end so they flow with the text.
|
|
@@ -2813,21 +2802,34 @@ async function cmdSend(rest) {
|
|
|
2813
2802
|
// Oncall groups usually address whoever triggered this turn (may not be
|
|
2814
2803
|
// the session owner). Bot recipients are filtered out so footer chrome
|
|
2815
2804
|
// cannot accidentally wake a sibling bot.
|
|
2816
|
-
|
|
2817
|
-
//
|
|
2818
|
-
//
|
|
2819
|
-
const
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2805
|
+
// Brand segment honours this bot's configured brandLabel (unset →
|
|
2806
|
+
// default botmux, '' → suppressed, else custom). Same resolver/rule as
|
|
2807
|
+
// the daemon's card builders so both send paths render identically.
|
|
2808
|
+
const footerParts = [];
|
|
2809
|
+
const brandSeg = brandFooterSegment(resolveBrandLabel(appId));
|
|
2810
|
+
if (brandSeg)
|
|
2811
|
+
footerParts.push(brandSeg);
|
|
2812
|
+
// All real mentions land on one footer line: human addressee first, then
|
|
2813
|
+
// explicit @ targets (incl. handoff bots), then cc. Ids already inlined in
|
|
2814
|
+
// the body prose are skipped. Top-level publish keeps sendTo empty.
|
|
2815
|
+
const footerRecipients = orderedFooterRecipients({
|
|
2816
|
+
sendTo: footerAddressing.sendTo,
|
|
2817
|
+
mentionIds: mentions.map(m => m.open_id),
|
|
2818
|
+
cc: footerAddressing.cc,
|
|
2819
|
+
inlinedIds: usedIds,
|
|
2830
2820
|
});
|
|
2821
|
+
if (footerRecipients.length > 0) {
|
|
2822
|
+
footerParts.push(`发送给:${footerRecipients.map(id => `<at id=${id}></at>`).join(' ')}`);
|
|
2823
|
+
}
|
|
2824
|
+
// Empty brand + no recipients → no footer at all (skip the orphan HR).
|
|
2825
|
+
if (footerParts.length > 0) {
|
|
2826
|
+
elements.push({ tag: 'hr' });
|
|
2827
|
+
elements.push({
|
|
2828
|
+
tag: 'markdown',
|
|
2829
|
+
text_size: 'notation_small_v2',
|
|
2830
|
+
content: `<font color='grey'>${footerParts.join(' · ')}</font>`,
|
|
2831
|
+
});
|
|
2832
|
+
}
|
|
2831
2833
|
const cardJson = JSON.stringify({
|
|
2832
2834
|
schema: '2.0',
|
|
2833
2835
|
config: { update_multi: true },
|
|
@@ -2857,33 +2859,28 @@ async function cmdSend(rest) {
|
|
|
2857
2859
|
}) : [];
|
|
2858
2860
|
for (const key of imageKeys)
|
|
2859
2861
|
postContent.push([{ tag: 'img', image_key: key }]);
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
const
|
|
2867
|
-
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
// recipient; bot recipients are filtered out by footerAddressing.
|
|
2877
|
-
const addressing = footerAddressing;
|
|
2878
|
-
if (addressing.sendTo || addressing.cc.length > 0) {
|
|
2862
|
+
// Footer: mirror the card layout — all real mentions go on one
|
|
2863
|
+
// `发送给:` line (human addressee first, then explicit targets, then cc),
|
|
2864
|
+
// separated from the body by a blank paragraph. Ids already inlined in the
|
|
2865
|
+
// body prose are skipped. Top-level publish keeps sendTo empty.
|
|
2866
|
+
const inlinedIds = new Set();
|
|
2867
|
+
for (const para of postContent)
|
|
2868
|
+
for (const n of para)
|
|
2869
|
+
if (n.tag === 'at')
|
|
2870
|
+
inlinedIds.add(n.user_id);
|
|
2871
|
+
const footerRecipients = orderedFooterRecipients({
|
|
2872
|
+
sendTo: footerAddressing.sendTo,
|
|
2873
|
+
mentionIds: mentions.map(m => m.open_id),
|
|
2874
|
+
cc: footerAddressing.cc,
|
|
2875
|
+
inlinedIds,
|
|
2876
|
+
});
|
|
2877
|
+
if (footerRecipients.length > 0) {
|
|
2879
2878
|
if (postContent.length > 0)
|
|
2880
2879
|
postContent.push([{ tag: 'text', text: '' }]);
|
|
2881
|
-
|
|
2882
|
-
|
|
2883
|
-
|
|
2884
|
-
|
|
2885
|
-
postContent.push([{ tag: 'text', text: 'cc:' }, ...addressing.cc.map(id => ({ tag: 'at', user_id: id }))]);
|
|
2886
|
-
}
|
|
2880
|
+
postContent.push([
|
|
2881
|
+
{ tag: 'text', text: '发送给:' },
|
|
2882
|
+
...footerRecipients.map(id => ({ tag: 'at', user_id: id })),
|
|
2883
|
+
]);
|
|
2887
2884
|
}
|
|
2888
2885
|
const postJson = JSON.stringify({ zh_cn: { title: '', content: postContent } });
|
|
2889
2886
|
messageId = await dispatchPrimary(postJson, 'post');
|
|
@@ -3109,6 +3106,307 @@ botmux create-group — 用一组机器人新建飞书群
|
|
|
3109
3106
|
}
|
|
3110
3107
|
}
|
|
3111
3108
|
// ─── Bots subcommand ─────────────────────────────────────────────────────────
|
|
3109
|
+
// ─── botmux ask v0.1.7 ───────────────────────────────────────────────────────
|
|
3110
|
+
//
|
|
3111
|
+
// CLI agent inside a botmux-spawned session calls `botmux ask buttons
|
|
3112
|
+
// --options "..." "<prompt>"`. Daemon sends a Lark card; user clicks; CLI
|
|
3113
|
+
// process unblocks with the selected key (or exit 124 on timeout, exit 3 if
|
|
3114
|
+
// the daemon dies). See /tmp/botmux-ask.md (or design memory).
|
|
3115
|
+
/**
|
|
3116
|
+
* postAsk: 找到 daemon → POST /api/asks → 返回 AskResult。
|
|
3117
|
+
* 连接失败 / HTTP 错误时抛出带 exitCode 属性的 Error:
|
|
3118
|
+
* - exitCode=3:daemon 不可达或 HTTP 非 400
|
|
3119
|
+
* - exitCode=2:400 + no_approvers
|
|
3120
|
+
*/
|
|
3121
|
+
async function postAsk(body) {
|
|
3122
|
+
const larkAppId = body.larkAppId;
|
|
3123
|
+
const daemon = findDaemon(larkAppId);
|
|
3124
|
+
if (!daemon) {
|
|
3125
|
+
const err = new Error(`botmux ask: 找不到 daemon (larkAppId=${larkAppId})。daemon 已停?exit 3.`);
|
|
3126
|
+
err.exitCode = 3;
|
|
3127
|
+
throw err;
|
|
3128
|
+
}
|
|
3129
|
+
let res;
|
|
3130
|
+
try {
|
|
3131
|
+
res = await fetch(`http://127.0.0.1:${daemon.ipcPort}/api/asks`, {
|
|
3132
|
+
method: 'POST',
|
|
3133
|
+
headers: { 'content-type': 'application/json' },
|
|
3134
|
+
body: JSON.stringify(body),
|
|
3135
|
+
// No client-side timeout — broker enforces `timeoutMs` and will respond
|
|
3136
|
+
// with `kind:'timedOut'` so this fetch always settles.
|
|
3137
|
+
});
|
|
3138
|
+
}
|
|
3139
|
+
catch (fetchErr) {
|
|
3140
|
+
const msg = fetchErr instanceof Error ? fetchErr.message : String(fetchErr);
|
|
3141
|
+
const err = new Error(`botmux ask: 无法连接 daemon (port=${daemon.ipcPort}): ${msg}`);
|
|
3142
|
+
err.exitCode = 3;
|
|
3143
|
+
throw err;
|
|
3144
|
+
}
|
|
3145
|
+
if (!res.ok) {
|
|
3146
|
+
let errBody = '';
|
|
3147
|
+
try {
|
|
3148
|
+
errBody = (await res.text()).slice(0, 200);
|
|
3149
|
+
}
|
|
3150
|
+
catch { /* */ }
|
|
3151
|
+
if (res.status === 400 && /no_approvers/.test(errBody)) {
|
|
3152
|
+
const err = new Error('botmux ask: 当前会话没有可批准者(session.owner 不在 bot.allowedUsers 里,且 --approver 未指定)');
|
|
3153
|
+
err.exitCode = 2;
|
|
3154
|
+
throw err;
|
|
3155
|
+
}
|
|
3156
|
+
const err = new Error(`botmux ask: daemon HTTP ${res.status}: ${errBody}`);
|
|
3157
|
+
err.exitCode = 3;
|
|
3158
|
+
throw err;
|
|
3159
|
+
}
|
|
3160
|
+
try {
|
|
3161
|
+
return (await res.json());
|
|
3162
|
+
}
|
|
3163
|
+
catch (jsonErr) {
|
|
3164
|
+
const err = new Error(`botmux ask: daemon 返回非 JSON: ${jsonErr}`);
|
|
3165
|
+
err.exitCode = 3;
|
|
3166
|
+
throw err;
|
|
3167
|
+
}
|
|
3168
|
+
}
|
|
3169
|
+
async function cmdAsk(sub, rest) {
|
|
3170
|
+
// Workflow-subagent safety gate (same posture as cmdSend): a CLI running
|
|
3171
|
+
// inside a workflow subagent (Slice F) must not surface chat UI. Workflow
|
|
3172
|
+
// approvals belong in humanGate / decision nodes so the choice is part of
|
|
3173
|
+
// the run's event log; an ad-hoc `botmux ask` would bypass that audit
|
|
3174
|
+
// trail entirely.
|
|
3175
|
+
if (process.env.BOTMUX_WORKFLOW === '1') {
|
|
3176
|
+
const runId = process.env.BOTMUX_WORKFLOW_RUN_ID ?? '?';
|
|
3177
|
+
const nodeId = process.env.BOTMUX_WORKFLOW_NODE_ID ?? '?';
|
|
3178
|
+
console.error(`botmux ask refused inside workflow subagent (run=${runId} node=${nodeId}).\n` +
|
|
3179
|
+
`Workflow subagents must surface approvals via humanGate / decision nodes\n` +
|
|
3180
|
+
`so the resolution is recorded in the run's event log; ask would bypass it.`);
|
|
3181
|
+
process.exit(2);
|
|
3182
|
+
}
|
|
3183
|
+
// Only `buttons` shipped in v0.1.7. The bare alias (`botmux ask --options`)
|
|
3184
|
+
// routes here with sub='' — accept it and behave identically. `ask text` /
|
|
3185
|
+
// `ask confirm` are reserved for later versions.
|
|
3186
|
+
if (sub && sub !== 'buttons') {
|
|
3187
|
+
console.error(`botmux ask: 未知 subcommand "${sub}"(v0.1.7 仅支持 \`buttons\` 或省略)`);
|
|
3188
|
+
process.exit(2);
|
|
3189
|
+
}
|
|
3190
|
+
const { findMissingAskEnv, parseAskOptions, parseAskTimeoutSeconds, AskArgsError } = await import('./core/ask-args.js');
|
|
3191
|
+
const { toLegacySelected } = await import('./core/ask-types.js');
|
|
3192
|
+
const missing = findMissingAskEnv(process.env);
|
|
3193
|
+
if (missing) {
|
|
3194
|
+
console.error(`botmux ask: 缺少必需环境变量 ${missing}。` +
|
|
3195
|
+
` 请在 botmux daemon spawn 的 CLI 会话内运行。`);
|
|
3196
|
+
process.exit(2);
|
|
3197
|
+
}
|
|
3198
|
+
const optionsRaw = argValue(rest, '--options');
|
|
3199
|
+
const timeoutRaw = argValue(rest, '--timeout');
|
|
3200
|
+
const useJson = rest.includes('--json');
|
|
3201
|
+
const approverArgs = argValues(rest, '--approver');
|
|
3202
|
+
const positionalArgs = positionals(rest, ['--json']);
|
|
3203
|
+
let options;
|
|
3204
|
+
let timeoutMs;
|
|
3205
|
+
try {
|
|
3206
|
+
options = parseAskOptions(optionsRaw);
|
|
3207
|
+
timeoutMs = parseAskTimeoutSeconds(timeoutRaw);
|
|
3208
|
+
}
|
|
3209
|
+
catch (err) {
|
|
3210
|
+
if (err instanceof AskArgsError) {
|
|
3211
|
+
console.error(`botmux ask: ${err.message}`);
|
|
3212
|
+
process.exit(2);
|
|
3213
|
+
}
|
|
3214
|
+
throw err;
|
|
3215
|
+
}
|
|
3216
|
+
const prompt = positionalArgs.join(' ').trim();
|
|
3217
|
+
if (!prompt) {
|
|
3218
|
+
console.error('botmux ask: 缺少 prompt。用法: botmux ask buttons --options "yes,no" "继续发版吗?"');
|
|
3219
|
+
process.exit(2);
|
|
3220
|
+
}
|
|
3221
|
+
const larkAppId = process.env.BOTMUX_LARK_APP_ID;
|
|
3222
|
+
const body = {
|
|
3223
|
+
sessionId: process.env.BOTMUX_SESSION_ID,
|
|
3224
|
+
chatId: process.env.BOTMUX_CHAT_ID,
|
|
3225
|
+
larkAppId,
|
|
3226
|
+
rootMessageId: process.env.BOTMUX_ROOT_MESSAGE_ID || null,
|
|
3227
|
+
options,
|
|
3228
|
+
prompt,
|
|
3229
|
+
timeoutMs,
|
|
3230
|
+
approvers: approverArgs,
|
|
3231
|
+
};
|
|
3232
|
+
let result;
|
|
3233
|
+
try {
|
|
3234
|
+
result = await postAsk(body);
|
|
3235
|
+
}
|
|
3236
|
+
catch (err) {
|
|
3237
|
+
const code = err.exitCode ?? 3;
|
|
3238
|
+
console.error(err.message);
|
|
3239
|
+
process.exit(code);
|
|
3240
|
+
}
|
|
3241
|
+
// result.kind==='answered' 时用 toLegacySelected 取回旧的 string(单问单选)
|
|
3242
|
+
const selected = toLegacySelected(result);
|
|
3243
|
+
if (useJson) {
|
|
3244
|
+
const out = {
|
|
3245
|
+
selected,
|
|
3246
|
+
answers: result.kind === 'answered' ? result.answers : null,
|
|
3247
|
+
by: result.kind === 'answered' ? result.by : null,
|
|
3248
|
+
comment: null,
|
|
3249
|
+
timedOut: result.kind === 'timedOut',
|
|
3250
|
+
};
|
|
3251
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|
|
3252
|
+
}
|
|
3253
|
+
else if (result.kind === 'answered') {
|
|
3254
|
+
// 非 JSON 模式:输出 selected key(单问单选),多选/多问输出空字符串
|
|
3255
|
+
process.stdout.write((selected ?? '') + '\n');
|
|
3256
|
+
}
|
|
3257
|
+
switch (result.kind) {
|
|
3258
|
+
case 'answered':
|
|
3259
|
+
process.exit(0);
|
|
3260
|
+
case 'timedOut':
|
|
3261
|
+
console.error(`botmux ask: 超时(${timeoutMs / 1000}s),无回复`);
|
|
3262
|
+
process.exit(124);
|
|
3263
|
+
case 'invalidated':
|
|
3264
|
+
console.error(`botmux ask: 已失效 (${result.reason})`);
|
|
3265
|
+
process.exit(3);
|
|
3266
|
+
}
|
|
3267
|
+
}
|
|
3268
|
+
// ─── botmux hook <cliId> ──────────────────────────────────────────────────────
|
|
3269
|
+
//
|
|
3270
|
+
// hook 模式:各 CLI hook 配置调用 `botmux hook <cliId>`,stdin 注入 hook payload,
|
|
3271
|
+
// 本命令解析问题 → POST /api/asks → 等结果 → 写 directive 到 stdout。
|
|
3272
|
+
// 任何失败(daemon 不可达、env 缺失、解析错误)均输出 passthrough directive 并 exit 0,
|
|
3273
|
+
// 绝不挂死,保证 CLI 可以继续原生终端提问。
|
|
3274
|
+
/**
|
|
3275
|
+
* runHook: hook 命令的纯业务逻辑,接受已解析的 payload/env/postAskFn,
|
|
3276
|
+
* 返回应写到 stdout 的字符串。通过依赖注入使单元测试无需真实 daemon/env。
|
|
3277
|
+
*
|
|
3278
|
+
* @param payload 已经 JSON.parse 的 hook payload 对象
|
|
3279
|
+
* @param env 包含 BOTMUX_* 环境变量的字典
|
|
3280
|
+
* @param postAskFn 替代真实 postAsk 的可注入函数(测试用)
|
|
3281
|
+
* @param cliId CLI 适配器 ID
|
|
3282
|
+
* @param resolveAdoptRouteFn 可选:替代真实 adopt 路由解析的注入函数(测试用);
|
|
3283
|
+
* 缺省时使用真实 resolveAdoptRoute(查祖先 PID → daemon)
|
|
3284
|
+
* @returns { stdout: string } 应写到 stdout 的内容
|
|
3285
|
+
*/
|
|
3286
|
+
export async function runHook(payload, env, postAskFn, cliId, resolveAdoptRouteFn) {
|
|
3287
|
+
const { getHookAdapter } = await import('./core/ask-hook/registry.js');
|
|
3288
|
+
// 未知 cliId → 无 adapter,输出空字符串静默放行
|
|
3289
|
+
const adapter = getHookAdapter(cliId);
|
|
3290
|
+
if (!adapter) {
|
|
3291
|
+
return { stdout: '' };
|
|
3292
|
+
}
|
|
3293
|
+
// Workflow-subagent 安全门:workflow 子 agent 内直接 passthrough
|
|
3294
|
+
if (env.BOTMUX_WORKFLOW === '1') {
|
|
3295
|
+
return { stdout: adapter.passthrough(payload) };
|
|
3296
|
+
}
|
|
3297
|
+
// 解析问题:非 askUserQuestion 类事件 → passthrough 放行
|
|
3298
|
+
const parsed = adapter.parseQuestions(payload);
|
|
3299
|
+
if (!parsed) {
|
|
3300
|
+
return { stdout: adapter.passthrough(payload) };
|
|
3301
|
+
}
|
|
3302
|
+
// 检查必需的 BOTMUX_* env
|
|
3303
|
+
const sessionId = env.BOTMUX_SESSION_ID;
|
|
3304
|
+
const chatId = env.BOTMUX_CHAT_ID;
|
|
3305
|
+
const larkAppId = env.BOTMUX_LARK_APP_ID;
|
|
3306
|
+
// 路由变量:优先用 env,env 缺失时尝试 adopt 路由
|
|
3307
|
+
let routeSessionId = sessionId;
|
|
3308
|
+
let routeChatId = chatId;
|
|
3309
|
+
let routeLarkAppId = larkAppId;
|
|
3310
|
+
let routeRoot = env.BOTMUX_ROOT_MESSAGE_ID || null;
|
|
3311
|
+
if (!sessionId || !chatId || !larkAppId) {
|
|
3312
|
+
// env 缺失 → 尝试通过祖先 PID 匹配在线 adopt 会话
|
|
3313
|
+
const resolver = resolveAdoptRouteFn ?? (() => {
|
|
3314
|
+
// 延迟 import 避免冷启动开销
|
|
3315
|
+
return import('./adapters/adopt-route.js').then(({ resolveAdoptRoute, queryAdoptSession }) => resolveAdoptRoute({
|
|
3316
|
+
startPid: process.pid,
|
|
3317
|
+
listDaemons: listOnlineDaemons,
|
|
3318
|
+
queryDaemon: queryAdoptSession,
|
|
3319
|
+
}));
|
|
3320
|
+
});
|
|
3321
|
+
let adopt = null;
|
|
3322
|
+
try {
|
|
3323
|
+
adopt = await resolver();
|
|
3324
|
+
}
|
|
3325
|
+
catch {
|
|
3326
|
+
// 解析失败 → 视作真非 botmux 会话,passthrough 放行
|
|
3327
|
+
}
|
|
3328
|
+
if (!adopt) {
|
|
3329
|
+
// 真非 botmux 会话 → passthrough 放行
|
|
3330
|
+
return { stdout: adapter.passthrough(payload) };
|
|
3331
|
+
}
|
|
3332
|
+
// adopt 命中 → 使用 adopt 路由信息
|
|
3333
|
+
routeSessionId = adopt.sessionId;
|
|
3334
|
+
routeChatId = adopt.chatId;
|
|
3335
|
+
routeLarkAppId = adopt.larkAppId;
|
|
3336
|
+
routeRoot = adopt.rootMessageId;
|
|
3337
|
+
}
|
|
3338
|
+
// 解析 timeoutMs:默认 1 小时,可由 BOTMUX_ASK_TIMEOUT_MS 覆盖
|
|
3339
|
+
const DEFAULT_TIMEOUT_MS = 3_600_000;
|
|
3340
|
+
let timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
3341
|
+
const timeoutEnv = env.BOTMUX_ASK_TIMEOUT_MS;
|
|
3342
|
+
if (timeoutEnv) {
|
|
3343
|
+
const parsed_timeout = parseInt(timeoutEnv, 10);
|
|
3344
|
+
if (Number.isInteger(parsed_timeout) && parsed_timeout > 0) {
|
|
3345
|
+
timeoutMs = parsed_timeout;
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
const body = {
|
|
3349
|
+
sessionId: routeSessionId,
|
|
3350
|
+
chatId: routeChatId,
|
|
3351
|
+
larkAppId: routeLarkAppId,
|
|
3352
|
+
rootMessageId: routeRoot,
|
|
3353
|
+
questions: parsed.questions,
|
|
3354
|
+
timeoutMs,
|
|
3355
|
+
approvers: [],
|
|
3356
|
+
};
|
|
3357
|
+
let result;
|
|
3358
|
+
try {
|
|
3359
|
+
result = await postAskFn(body);
|
|
3360
|
+
}
|
|
3361
|
+
catch {
|
|
3362
|
+
// 任何失败(daemon 不可达、HTTP 错误等)→ passthrough 放行
|
|
3363
|
+
return { stdout: adapter.passthrough(payload) };
|
|
3364
|
+
}
|
|
3365
|
+
if (result.kind === 'answered') {
|
|
3366
|
+
return { stdout: adapter.formatAnswer(result.answers, parsed) };
|
|
3367
|
+
}
|
|
3368
|
+
// timedOut / invalidated → passthrough 放行
|
|
3369
|
+
return { stdout: adapter.passthrough(payload) };
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* cmdHook: `botmux hook <cliId>` 入口。
|
|
3373
|
+
* 读取 stdin 全文 → JSON.parse → runHook → 写 stdout,exit 0。
|
|
3374
|
+
*/
|
|
3375
|
+
async function cmdHook(cliId) {
|
|
3376
|
+
// 读取 stdin 全文
|
|
3377
|
+
let stdinText = '';
|
|
3378
|
+
try {
|
|
3379
|
+
const chunks = [];
|
|
3380
|
+
for await (const chunk of process.stdin) {
|
|
3381
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
3382
|
+
}
|
|
3383
|
+
stdinText = Buffer.concat(chunks).toString('utf-8');
|
|
3384
|
+
}
|
|
3385
|
+
catch {
|
|
3386
|
+
// stdin 读取失败 → 无法处理,静默退出
|
|
3387
|
+
process.exit(0);
|
|
3388
|
+
}
|
|
3389
|
+
// JSON.parse 失败 → 输出空并退出(不挂死)
|
|
3390
|
+
let payload;
|
|
3391
|
+
try {
|
|
3392
|
+
payload = JSON.parse(stdinText);
|
|
3393
|
+
}
|
|
3394
|
+
catch {
|
|
3395
|
+
process.exit(0);
|
|
3396
|
+
}
|
|
3397
|
+
const { getHookAdapter } = await import('./core/ask-hook/registry.js');
|
|
3398
|
+
const adapter = getHookAdapter(cliId);
|
|
3399
|
+
// 未知 cliId → 静默放行
|
|
3400
|
+
if (!adapter) {
|
|
3401
|
+
process.exit(0);
|
|
3402
|
+
}
|
|
3403
|
+
const env = process.env;
|
|
3404
|
+
const result = await runHook(payload, env, postAsk, cliId);
|
|
3405
|
+
if (result.stdout) {
|
|
3406
|
+
console.log(result.stdout);
|
|
3407
|
+
}
|
|
3408
|
+
process.exit(0);
|
|
3409
|
+
}
|
|
3112
3410
|
async function cmdBots(sub, rest) {
|
|
3113
3411
|
process.env.SESSION_DATA_DIR ??= resolveDataDir();
|
|
3114
3412
|
if (sub !== 'list' && sub !== 'ls' && sub !== '') {
|
|
@@ -3337,6 +3635,20 @@ switch (command) {
|
|
|
3337
3635
|
case 'schedule':
|
|
3338
3636
|
await cmdSchedule(process.argv[3] ?? '', process.argv.slice(4));
|
|
3339
3637
|
break;
|
|
3638
|
+
case 'ask': {
|
|
3639
|
+
// `botmux ask buttons --options ...` → sub='buttons', rest=['--options', ...]
|
|
3640
|
+
// `botmux ask --options ...` → sub='', rest=['--options', ...] (bare alias)
|
|
3641
|
+
const { normalizeAskDispatch } = await import('./core/ask-args.js');
|
|
3642
|
+
const { sub, rest } = normalizeAskDispatch(process.argv.slice(3));
|
|
3643
|
+
await cmdAsk(sub, rest);
|
|
3644
|
+
break;
|
|
3645
|
+
}
|
|
3646
|
+
case 'hook': {
|
|
3647
|
+
// `botmux hook <cliId>` — hook 客户端,stdin 读 payload,stdout 写 directive
|
|
3648
|
+
const cliId = process.argv[3] ?? '';
|
|
3649
|
+
await cmdHook(cliId);
|
|
3650
|
+
break;
|
|
3651
|
+
}
|
|
3340
3652
|
case 'workflow': {
|
|
3341
3653
|
const { cmdWorkflow } = await import('./cli/workflow.js');
|
|
3342
3654
|
await cmdWorkflow(process.argv[3] ?? '', process.argv.slice(4));
|