botmux 2.38.0 → 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 +368 -47
- 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/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +10 -6
- package/dist/core/session-manager.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 +104 -8
- 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.d.ts.map +1 -1
- package/dist/im/lark/message-parser.js +39 -1
- 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) {
|
|
@@ -2772,12 +2773,9 @@ async function cmdSend(rest) {
|
|
|
2772
2773
|
return `<at id=${openId}></at>`;
|
|
2773
2774
|
});
|
|
2774
2775
|
}
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
trailingAts.push(`<at id=${m.open_id}></at>`);
|
|
2779
|
-
if (trailingAts.length > 0)
|
|
2780
|
-
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.
|
|
2781
2779
|
// Inline images into the markdown via . If caller used an
|
|
2782
2780
|
// `` placeholder, substitute by 0-based index; any remaining
|
|
2783
2781
|
// images get appended at the end so they flow with the text.
|
|
@@ -2804,21 +2802,34 @@ async function cmdSend(rest) {
|
|
|
2804
2802
|
// Oncall groups usually address whoever triggered this turn (may not be
|
|
2805
2803
|
// the session owner). Bot recipients are filtered out so footer chrome
|
|
2806
2804
|
// cannot accidentally wake a sibling bot.
|
|
2807
|
-
|
|
2808
|
-
//
|
|
2809
|
-
//
|
|
2810
|
-
const
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
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,
|
|
2821
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
|
+
}
|
|
2822
2833
|
const cardJson = JSON.stringify({
|
|
2823
2834
|
schema: '2.0',
|
|
2824
2835
|
config: { update_multi: true },
|
|
@@ -2848,33 +2859,28 @@ async function cmdSend(rest) {
|
|
|
2848
2859
|
}) : [];
|
|
2849
2860
|
for (const key of imageKeys)
|
|
2850
2861
|
postContent.push([{ tag: 'img', image_key: key }]);
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
2857
|
-
const
|
|
2858
|
-
|
|
2859
|
-
|
|
2860
|
-
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
// recipient; bot recipients are filtered out by footerAddressing.
|
|
2868
|
-
const addressing = footerAddressing;
|
|
2869
|
-
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) {
|
|
2870
2878
|
if (postContent.length > 0)
|
|
2871
2879
|
postContent.push([{ tag: 'text', text: '' }]);
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
postContent.push([{ tag: 'text', text: 'cc:' }, ...addressing.cc.map(id => ({ tag: 'at', user_id: id }))]);
|
|
2877
|
-
}
|
|
2880
|
+
postContent.push([
|
|
2881
|
+
{ tag: 'text', text: '发送给:' },
|
|
2882
|
+
...footerRecipients.map(id => ({ tag: 'at', user_id: id })),
|
|
2883
|
+
]);
|
|
2878
2884
|
}
|
|
2879
2885
|
const postJson = JSON.stringify({ zh_cn: { title: '', content: postContent } });
|
|
2880
2886
|
messageId = await dispatchPrimary(postJson, 'post');
|
|
@@ -3100,6 +3106,307 @@ botmux create-group — 用一组机器人新建飞书群
|
|
|
3100
3106
|
}
|
|
3101
3107
|
}
|
|
3102
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
|
+
}
|
|
3103
3410
|
async function cmdBots(sub, rest) {
|
|
3104
3411
|
process.env.SESSION_DATA_DIR ??= resolveDataDir();
|
|
3105
3412
|
if (sub !== 'list' && sub !== 'ls' && sub !== '') {
|
|
@@ -3328,6 +3635,20 @@ switch (command) {
|
|
|
3328
3635
|
case 'schedule':
|
|
3329
3636
|
await cmdSchedule(process.argv[3] ?? '', process.argv.slice(4));
|
|
3330
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
|
+
}
|
|
3331
3652
|
case 'workflow': {
|
|
3332
3653
|
const { cmdWorkflow } = await import('./cli/workflow.js');
|
|
3333
3654
|
await cmdWorkflow(process.argv[3] ?? '', process.argv.slice(4));
|