From 931d35a26e1f745684895e52dde5f5e1a92af0e1 Mon Sep 17 00:00:00 2001 From: zjdznl Date: Mon, 8 Jun 2026 15:38:26 +0800 Subject: [PATCH 1/5] =?UTF-8?q?feat(cards):=20botmux=20send=20=E5=8D=A1?= =?UTF-8?q?=E7=89=87=E5=A2=9E=E5=8A=A0=E5=AF=BC=E5=87=BA=E9=A3=9E=E4=B9=A6?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E5=92=8C=E5=8E=9F=E5=A7=8BMarkdown=E6=8C=89?= =?UTF-8?q?=E9=92=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 reply-content-cache 基于文件系统的跨进程缓存 - 新增 doc-import 调用 lark-cli drive +import 导入飞书 Docx - 卡片底部加两列按钮:导出飞书文档 + 原始 Markdown - 异步导出避免飞书回调超时 Co-Authored-By: Claude Opus 4.8 --- src/cli.ts | 26 +++++++++ src/i18n/en.ts | 1 + src/i18n/zh.ts | 1 + src/im/lark/card-handler.ts | 47 +++++++++++++++- src/im/lark/reply-content-cache.ts | 85 +++++++++++++++++++++++++++++ src/services/doc-import.ts | 86 ++++++++++++++++++++++++++++++ 6 files changed, 245 insertions(+), 1 deletion(-) create mode 100644 src/im/lark/reply-content-cache.ts create mode 100644 src/services/doc-import.ts diff --git a/src/cli.ts b/src/cli.ts index 2bca01ce..ddef7e09 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -260,6 +260,7 @@ function ecosystemConfig(): string { // ad-hoc (e.g. `BOTMUX_MEMORY_DIAG_INTERVAL_MS=5000`) when chasing an // RSS regression — turned off in master so logs stay quiet. BOTMUX_MEMORY_DIAG_INTERVAL_MS: process.env.BOTMUX_MEMORY_DIAG_INTERVAL_MS ?? '0', + ...(process.env.WEB_TERMINAL_PASSWORD ? { WEB_TERMINAL_PASSWORD: process.env.WEB_TERMINAL_PASSWORD } : {}), }, })); @@ -2811,6 +2812,7 @@ function argValues(args: string[], ...flags: string[]): string[] { // keeps using `buildCardBodyElements` from there. import { buildMentionedPendingResponseCard } from './im/lark/card-builder.js'; import { buildCardBodyElements, brandFooterSegment } from './im/lark/md-card.js'; +import { storeReplyContent } from './im/lark/reply-content-cache.js'; import { COMPLETED_REACTION_EMOJI_TYPE, claimPendingResponseCard, isPendingResponseCardOpen, markPendingResponseCardPatchedIfCurrent, mergePendingResponseState, shouldMarkPendingAsMentionedSend, shouldPatchPendingOnExplicitSend } from './core/pending-response.js'; import { resolveBrandLabel } from './bot-registry.js'; import { config } from './config.js'; @@ -3398,6 +3400,30 @@ async function cmdSend(rest: string[]): Promise { } } + // Reply action buttons: export to Feishu doc + send raw markdown + const contentKey = storeReplyContent(text); + const actionRootId = (isChatScope ? s.chatId : s.rootMessageId) ?? s.chatId; + elements.push({ tag: 'hr' }); + const rpcButton = (label: string, action: string) => ({ + tag: 'button', + text: { tag: 'plain_text', content: label }, + type: 'default', + width: 'fill', + behaviors: [{ + type: 'callback', + value: { action, content_key: contentKey, root_id: actionRootId, session_id: sid }, + }], + }); + elements.push({ + tag: 'column_set', + flex_mode: 'none', + horizontal_spacing: 'default', + columns: [ + { tag: 'column', width: 'weighted', weight: 1, vertical_align: 'center', elements: [rpcButton('📄 导出飞书文档', 'export_to_doc')] }, + { tag: 'column', width: 'weighted', weight: 1, vertical_align: 'center', elements: [rpcButton('📝 原始 Markdown', 'send_raw_md')] }, + ], + }); + const cardJson = JSON.stringify({ schema: '2.0', config: { update_multi: true }, diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 72a568ad..075e78de 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -489,6 +489,7 @@ export const messages: Record = { 'card.action.takeover_retired': '⚠️ The old "Take Over" button is retired. In bridge mode, botmux bridges the original CLI so replies still come back to Lark — no takeover needed. Full takeover (`/adopt --takeover`) is on the roadmap.', 'card.action.terminal_not_ready': '⚠️ Terminal is not ready yet, please try again shortly.', 'card.action.no_output': '(no output yet)', + 'card.action.content_expired': 'Content expired, unable to retrieve original text.', 'card.action.tui_select_title': 'Select options', 'card.action.tui_custom_input': 'Custom input', 'card.action.tui_done': 'Done', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 0b4be412..cc19b322 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -492,6 +492,7 @@ export const messages: Record = { 'card.action.takeover_retired': '⚠️ 旧版"接管"按钮已停用。bridge 模式下原 CLI 由 botmux 桥接,无需接管即可在飞书中收到回答。如需 /resume 完整接管能力,请等待 /adopt --takeover 命令上线。', 'card.action.terminal_not_ready': '⚠️ 终端尚未就绪,请稍后再试。', 'card.action.no_output': '(当前无输出内容)', + 'card.action.content_expired': '内容已过期,无法获取原文。', 'card.action.tui_select_title': 'Select options', 'card.action.tui_custom_input': 'Custom input', 'card.action.tui_done': 'Done', diff --git a/src/im/lark/card-handler.ts b/src/im/lark/card-handler.ts index 15eb81e4..0c9ee91c 100644 --- a/src/im/lark/card-handler.ts +++ b/src/im/lark/card-handler.ts @@ -572,7 +572,7 @@ export async function handleCardAction(data: CardActionData, deps: CardHandlerDe return { toast: { type: 'success', content: t('card.relay.toast_success', undefined, loc) } }; } - const isSensitive = value?.action && ['restart', 'close', 'resume', 'skip_repo', 'retry_last_task', 'get_write_link', 'toggle_stream', 'toggle_display', 'export_text', 'term_action', 'refresh_screenshot', 'takeover', 'disconnect', 'tui_keys', 'tui_text_input', 'wf_approve', 'wf_reject', 'wf_cancel'].includes(value.action); + const isSensitive = value?.action && ['restart', 'close', 'resume', 'skip_repo', 'retry_last_task', 'get_write_link', 'toggle_stream', 'toggle_display', 'export_text', 'term_action', 'refresh_screenshot', 'takeover', 'disconnect', 'tui_keys', 'tui_text_input', 'wf_approve', 'wf_reject', 'wf_cancel', 'export_to_doc', 'send_raw_md'].includes(value.action); if (isSensitive) { const rootId = value?.root_id; // activeSessions is keyed by sessionKey(anchor, larkAppId) — `${anchor}::${larkAppId}` @@ -1154,6 +1154,51 @@ export async function handleCardAction(data: CardActionData, deps: CardHandlerDe return; } + // Export AI reply as a Feishu Docx document. Runs async in background so + // the Feishu callback doesn't time out (lark-cli takes ~3-6s). + if (actionType === 'export_to_doc' && value?.content_key) { + const locDs = localeForBot(ds?.larkAppId ?? larkAppId); + const appId = larkAppId; + if (!appId) { + return { toast: { type: 'error', content: '无法确定 Lark 应用' } }; + } + const { getReplyContent, deriveTitleFromMarkdown } = await import('../../im/lark/reply-content-cache.js'); + const md = getReplyContent(value.content_key); + if (!md) { + return { toast: { type: 'error', content: t('card.action.content_expired', undefined, locDs) } }; + } + const title = deriveTitleFromMarkdown(md); + // Fire-and-forget: return toast immediately so Feishu doesn't time out, + // then do the import asynchronously in the background. + const replyRootId = rootId; + Promise.resolve().then(async () => { + const { importMarkdownAsDoc } = await import('../../services/doc-import.js'); + const result = await importMarkdownAsDoc(appId, md, title); + if (result.ok && result.docUrl) { + await sessionReply(replyRootId, `📄 已导出飞书文档:[${result.docTitle ?? title}](${result.docUrl})`); + } else { + await sessionReply(replyRootId, `❌ 导出失败:${result.error ?? '未知错误'}`); + } + logger.info(`[${tag(ds ?? { session: { sessionId: value.session_id } } as any)}] export_to_doc: ${result.ok ? 'ok' : 'failed'} (${md.length} chars)`); + }).catch((err: any) => { + logger.error(`[export_to_doc] background import failed: ${err?.message ?? err}`); + }); + return { toast: { type: 'info', content: '正在导出飞书文档…' } }; + } + + // Send the raw markdown content as a plain text reply. + if (actionType === 'send_raw_md' && value?.content_key) { + const locDs = localeForBot(ds?.larkAppId ?? larkAppId); + const { getReplyContent } = await import('../../im/lark/reply-content-cache.js'); + const md = getReplyContent(value.content_key); + if (!md) { + return { toast: { type: 'error', content: t('card.action.content_expired', undefined, locDs) } }; + } + await sessionReply(rootId, md); + logger.info(`[${tag(ds ?? { session: { sessionId: value.session_id } } as any)}] send_raw_md: ${md.length} chars`); + return; + } + // Manual screenshot refresh — force immediate capture bypassing 10s interval + hash dedup. if (actionType === 'refresh_screenshot' && ds) { if (ds.worker) { diff --git a/src/im/lark/reply-content-cache.ts b/src/im/lark/reply-content-cache.ts new file mode 100644 index 00000000..ef63c77b --- /dev/null +++ b/src/im/lark/reply-content-cache.ts @@ -0,0 +1,85 @@ +/** + * File-based cache for AI reply content, keyed by a short unique string. + * Card action buttons carry this key instead of the full content (which + * can be 10k+ chars and would exceed Feishu's value size limit). + * + * Uses the filesystem instead of an in-memory Map because `botmux send` + * runs as a short-lived CLI process while the card callback is handled by + * the long-lived daemon — they are different processes with separate + * memory spaces. + * + * TTL + max-count bounded so a leak doesn't grow unbounded across a + * long-running daemon. + */ + +import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, readdirSync, statSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; + +const TTL_MS = 60 * 60 * 1000; // 1 hour +const MAX_FILES = 2000; + +// Use os.tmpdir() so botmux send (short-lived CLI) and the daemon +// (long-lived pm2 process) share the same cache — an in-memory Map +// would be per-process and invisible to the other side. +function cacheDir(): string { + const dir = join(tmpdir(), 'botmux-reply-cache'); + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }); + return dir; +} + +/** Store reply content and return a lookup key for card buttons. */ +export function storeReplyContent(content: string): string { + const dir = cacheDir(); + + // Lazy eviction: if over max, delete expired files + let files: string[] = []; + try { files = readdirSync(dir); } catch { /* ignore */ } + if (files.length >= MAX_FILES) { + const now = Date.now(); + for (const f of files) { + if (f === '.' || f === '..') continue; + try { + const st = statSync(join(dir, f)); + if (now - st.mtimeMs > TTL_MS) unlinkSync(join(dir, f)); + } catch { /* ignore */ } + } + } + + const key = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; + writeFileSync(join(dir, `${key}.txt`), content, 'utf-8'); + return key; +} + +/** Retrieve cached reply content. Returns undefined if expired or missing. */ +export function getReplyContent(key: string): string | undefined { + // Basic sanitisation: disallow path traversal + if (key.includes('/') || key.includes('\\') || key.includes('..')) return undefined; + const filePath = join(cacheDir(), `${key}.txt`); + try { + const st = statSync(filePath); + if (Date.now() - st.mtimeMs > TTL_MS) { + try { unlinkSync(filePath); } catch { /* ignore */ } + return undefined; + } + return readFileSync(filePath, 'utf-8'); + } catch { + return undefined; + } +} + +/** Derive a human-readable doc title from markdown: first non-empty, + * non-code-fence line, truncated to 50 chars. */ +export function deriveTitleFromMarkdown(md: string): string { + const lines = md.split('\n'); + let inFence = false; + for (const line of lines) { + const trimmed = line.trim(); + if (/^```/.test(trimmed)) { inFence = !inFence; continue; } + if (inFence) continue; + // Skip headings markers, blockquotes, list markers for the title + const cleaned = trimmed.replace(/^[#>*-]+ /, '').trim(); + if (cleaned) return cleaned.length > 50 ? cleaned.slice(0, 47) + '...' : cleaned; + } + return 'AI 回复'; +} diff --git a/src/services/doc-import.ts b/src/services/doc-import.ts new file mode 100644 index 00000000..8d3a9a5a --- /dev/null +++ b/src/services/doc-import.ts @@ -0,0 +1,86 @@ +/** + * Import a markdown string as a Feishu Docx document via lark-cli. + * + * Uses `lark-cli drive +import` which handles the multipart upload + + * import task + polling in a single CLI invocation. Much simpler than + * the SDK/curl approach. + */ + +import { writeFileSync, unlinkSync, existsSync, mkdirSync } from 'node:fs'; +import { join } from 'node:path'; +import { randomBytes } from 'node:crypto'; +import { exec } from 'node:child_process'; +import { logger } from '../utils/logger.js'; +import { config } from '../config.js'; + +const MAX_FILE_BYTES = 20 * 1024 * 1024; // 20 MB limit for .md import + +interface ImportResult { + ok: boolean; + docUrl?: string; + docTitle?: string; + error?: string; +} + +/** Run lark-cli asynchronously so the event loop stays unblocked. */ +function execLarkCli(cmd: string): Promise { + return new Promise((resolve, reject) => { + exec(cmd, { maxBuffer: 10 * 1024 * 1024, timeout: 120_000 }, (err, stdout, stderr) => { + if (err) { + reject(new Error(stderr?.trim() || err.message)); + } else { + resolve(stdout); + } + }); + }); +} + +/** + * Import markdown content as a Feishu Docx document. + * Returns the document URL on success. + */ +export async function importMarkdownAsDoc( + _larkAppId: string, + markdown: string, + title: string, +): Promise { + const buf = Buffer.from(markdown, 'utf-8'); + if (buf.length > MAX_FILE_BYTES) { + return { ok: false, error: '内容超过 20 MB,无法导入为飞书文档' }; + } + + const tmpName = `botmux-import-${randomBytes(6).toString('hex')}.md`; + const tmpPath = join(config.session.dataDir, 'tmp', tmpName); + const tmpDir = join(config.session.dataDir, 'tmp'); + if (!existsSync(tmpDir)) mkdirSync(tmpDir, { recursive: true }); + + try { + writeFileSync(tmpPath, markdown, 'utf-8'); + + const result = await execLarkCli( + `cd '${tmpDir}' && lark-cli drive +import --file './${tmpName}' --type docx --name '${title.replace(/'/g, "'\\''")}' --json`, + ); + + const json: any = JSON.parse(result.trim()); + if (!json?.ok) { + const msg = json?.error?.message ?? 'unknown error'; + logger.error(`[doc-import] lark-cli import failed: ${msg}`); + return { ok: false, error: msg }; + } + + const docUrl: string | undefined = json?.data?.url; + const docTitle: string | undefined = json?.data?.job_error_msg === 'success' ? title : undefined; + if (!docUrl) { + logger.error(`[doc-import] lark-cli import returned no url: ${JSON.stringify(json?.data)}`); + return { ok: false, error: '导入成功但未返回文档链接' }; + } + + logger.info(`[doc-import] Imported markdown → ${docUrl} (title: ${docTitle ?? title})`); + return { ok: true, docUrl, docTitle: docTitle ?? title }; + } catch (err: any) { + logger.error(`[doc-import] Import failed: ${err?.message ?? err}`); + return { ok: false, error: err?.message ?? '导入失败' }; + } finally { + try { unlinkSync(tmpPath); } catch { /* ignore */ } + } +} From 9d7bf328e9a4dcaad866a98610f89d7d7452482b Mon Sep 17 00:00:00 2001 From: zjdznl Date: Mon, 8 Jun 2026 15:43:28 +0800 Subject: [PATCH 2/5] =?UTF-8?q?fix(doc-import):=20=E5=A2=9E=E5=8A=A0=20lar?= =?UTF-8?q?k-cli=20=E5=8F=AF=E7=94=A8=E6=80=A7=E6=A3=80=E6=9F=A5=EF=BC=8C?= =?UTF-8?q?=E6=9C=AA=E5=AE=89=E8=A3=85=E6=97=B6=E8=BF=94=E5=9B=9E=E6=98=8E?= =?UTF-8?q?=E7=A1=AE=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/doc-import.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/services/doc-import.ts b/src/services/doc-import.ts index 8d3a9a5a..13b835ff 100644 --- a/src/services/doc-import.ts +++ b/src/services/doc-import.ts @@ -22,6 +22,20 @@ interface ImportResult { error?: string; } +/** Check whether `lark-cli` is available in PATH. Cached so we only probe once. */ +let larkCliAvailable: boolean | null = null; +function checkLarkCli(): boolean { + if (larkCliAvailable !== null) return larkCliAvailable; + try { + const { execSync } = require('node:child_process'); + execSync('lark-cli --version', { stdio: 'ignore', timeout: 5000 }); + larkCliAvailable = true; + } catch { + larkCliAvailable = false; + } + return larkCliAvailable; +} + /** Run lark-cli asynchronously so the event loop stays unblocked. */ function execLarkCli(cmd: string): Promise { return new Promise((resolve, reject) => { @@ -44,6 +58,10 @@ export async function importMarkdownAsDoc( markdown: string, title: string, ): Promise { + if (!checkLarkCli()) { + return { ok: false, error: 'lark-cli 未安装,无法导入飞书文档。请运行 npm i -g @anthropic/lark-cli' }; + } + const buf = Buffer.from(markdown, 'utf-8'); if (buf.length > MAX_FILE_BYTES) { return { ok: false, error: '内容超过 20 MB,无法导入为飞书文档' }; From 6f45cc159d0a365dfd6fc4ba046527e5cc4fbc47 Mon Sep 17 00:00:00 2001 From: zjdznl Date: Mon, 8 Jun 2026 15:46:30 +0800 Subject: [PATCH 3/5] =?UTF-8?q?fix(doc-import):=20=E6=9B=B4=E6=AD=A3=20lar?= =?UTF-8?q?k-cli=20=E5=8C=85=E5=90=8D=E4=B8=BA=20@larksuite/cli=EF=BC=8C?= =?UTF-8?q?=E5=8D=A1=E7=89=87=E5=AF=BC=E5=87=BA=E6=8C=89=E9=92=AE=E5=8A=A0?= =?UTF-8?q?=E5=BC=80=E5=85=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 错误提示中安装命令改为 npx @larksuite/cli@latest install - 新增 BOTMUX_CARD_EXPORT_ENABLED 开关,默认关闭 - 未设置开关时卡片不显示导出/原始MD按钮 Co-Authored-By: Claude Opus 4.8 --- src/cli.ts | 47 ++++++++++++++++++++------------------ src/config.ts | 5 ++++ src/services/doc-import.ts | 2 +- 3 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index ddef7e09..4744ecf8 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -3401,28 +3401,31 @@ async function cmdSend(rest: string[]): Promise { } // Reply action buttons: export to Feishu doc + send raw markdown - const contentKey = storeReplyContent(text); - const actionRootId = (isChatScope ? s.chatId : s.rootMessageId) ?? s.chatId; - elements.push({ tag: 'hr' }); - const rpcButton = (label: string, action: string) => ({ - tag: 'button', - text: { tag: 'plain_text', content: label }, - type: 'default', - width: 'fill', - behaviors: [{ - type: 'callback', - value: { action, content_key: contentKey, root_id: actionRootId, session_id: sid }, - }], - }); - elements.push({ - tag: 'column_set', - flex_mode: 'none', - horizontal_spacing: 'default', - columns: [ - { tag: 'column', width: 'weighted', weight: 1, vertical_align: 'center', elements: [rpcButton('📄 导出飞书文档', 'export_to_doc')] }, - { tag: 'column', width: 'weighted', weight: 1, vertical_align: 'center', elements: [rpcButton('📝 原始 Markdown', 'send_raw_md')] }, - ], - }); + // Controlled by BOTMUX_CARD_EXPORT_ENABLED (default off) — requires lark-cli. + if (config.send.cardExportEnabled) { + const contentKey = storeReplyContent(text); + const actionRootId = (isChatScope ? s.chatId : s.rootMessageId) ?? s.chatId; + elements.push({ tag: 'hr' }); + const rpcButton = (label: string, action: string) => ({ + tag: 'button', + text: { tag: 'plain_text', content: label }, + type: 'default', + width: 'fill', + behaviors: [{ + type: 'callback', + value: { action, content_key: contentKey, root_id: actionRootId, session_id: sid }, + }], + }); + elements.push({ + tag: 'column_set', + flex_mode: 'none', + horizontal_spacing: 'default', + columns: [ + { tag: 'column', width: 'weighted', weight: 1, vertical_align: 'center', elements: [rpcButton('📄 导出飞书文档', 'export_to_doc')] }, + { tag: 'column', width: 'weighted', weight: 1, vertical_align: 'center', elements: [rpcButton('📝 原始 Markdown', 'send_raw_md')] }, + ], + }); + } const cardJson = JSON.stringify({ schema: '2.0', diff --git a/src/config.ts b/src/config.ts index f2ee3055..d106da68 100644 --- a/src/config.ts +++ b/src/config.ts @@ -64,6 +64,11 @@ export const config = { * BOTMUX_REQUIRE_MENTION_DECISION=false to disable (kill-switch if the * gate misfires in production). */ requireMentionDecision: (process.env.BOTMUX_REQUIRE_MENTION_DECISION ?? 'true').toLowerCase() !== 'false', + /** Show "Export to Doc" / "Raw Markdown" buttons on send cards. + * Requires lark-cli to be installed on the host. Off by default so + * users without lark-cli don't see buttons that would always fail. + * Set BOTMUX_CARD_EXPORT_ENABLED=true to enable. */ + cardExportEnabled: (process.env.BOTMUX_CARD_EXPORT_ENABLED ?? '').toLowerCase() === 'true', }, daemon: { cliId: (process.env.CLI_ID ?? 'claude-code') as import('./adapters/cli/types.js').CliId, diff --git a/src/services/doc-import.ts b/src/services/doc-import.ts index 13b835ff..fa5fc39e 100644 --- a/src/services/doc-import.ts +++ b/src/services/doc-import.ts @@ -59,7 +59,7 @@ export async function importMarkdownAsDoc( title: string, ): Promise { if (!checkLarkCli()) { - return { ok: false, error: 'lark-cli 未安装,无法导入飞书文档。请运行 npm i -g @anthropic/lark-cli' }; + return { ok: false, error: 'lark-cli 未安装,无法导入飞书文档。请运行 npx @larksuite/cli@latest install' }; } const buf = Buffer.from(markdown, 'utf-8'); From 61884d25d9145bee8390bafac0ff79d7ad906010 Mon Sep 17 00:00:00 2001 From: zjdznl Date: Mon, 8 Jun 2026 15:57:24 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(doc-import):=20=E6=9C=AA=E5=AE=89?= =?UTF-8?q?=E8=A3=85=20lark-cli=20=E6=97=B6=E6=8F=90=E7=A4=BA=20GitHub=20?= =?UTF-8?q?=E9=93=BE=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/services/doc-import.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/doc-import.ts b/src/services/doc-import.ts index fa5fc39e..fe28a0ae 100644 --- a/src/services/doc-import.ts +++ b/src/services/doc-import.ts @@ -59,7 +59,7 @@ export async function importMarkdownAsDoc( title: string, ): Promise { if (!checkLarkCli()) { - return { ok: false, error: 'lark-cli 未安装,无法导入飞书文档。请运行 npx @larksuite/cli@latest install' }; + return { ok: false, error: 'lark-cli 未安装,无法导入飞书文档。\n安装:npx @larksuite/cli@latest install\n详见:https://github.com/larksuite/cli' }; } const buf = Buffer.from(markdown, 'utf-8'); From 08ce47404af7b7f4dd2c2e2ac20a40640c15eb12 Mon Sep 17 00:00:00 2001 From: zjdznl Date: Mon, 8 Jun 2026 16:00:16 +0800 Subject: [PATCH 5/5] =?UTF-8?q?fix(cli):=20=E5=B0=86=20BOTMUX=5FCARD=5FEXP?= =?UTF-8?q?ORT=5FENABLED=20=E5=86=99=E5=85=A5=20PM2=20ecosystem=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/cli.ts b/src/cli.ts index 4744ecf8..f2f2c5bd 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -261,6 +261,7 @@ function ecosystemConfig(): string { // RSS regression — turned off in master so logs stay quiet. BOTMUX_MEMORY_DIAG_INTERVAL_MS: process.env.BOTMUX_MEMORY_DIAG_INTERVAL_MS ?? '0', ...(process.env.WEB_TERMINAL_PASSWORD ? { WEB_TERMINAL_PASSWORD: process.env.WEB_TERMINAL_PASSWORD } : {}), + ...(process.env.BOTMUX_CARD_EXPORT_ENABLED ? { BOTMUX_CARD_EXPORT_ENABLED: process.env.BOTMUX_CARD_EXPORT_ENABLED } : {}), }, }));