From b31887cd1261f0997a13a7cb0e47415da37bdd1f Mon Sep 17 00:00:00 2001 From: Simin Yang Date: Fri, 31 Jul 2026 13:49:56 +0800 Subject: [PATCH] fix(agent): preserve lark-cli runtime paths --- src/agent/bridge-system-prompt.ts | 14 ++-- src/agent/lark-channel-env.ts | 65 +++++++++++++++- src/bot/channel.ts | 6 +- tests/process/claude-adapter.test.ts | 14 ++++ tests/process/codex-adapter.test.ts | 15 +++- tests/unit/agent/bridge-system-prompt.test.ts | 13 ++++ tests/unit/agent/lark-channel-env.test.ts | 76 +++++++++++++++++++ tests/unit/agent/prompt-builder.test.ts | 11 +-- 8 files changed, 198 insertions(+), 16 deletions(-) create mode 100644 tests/unit/agent/lark-channel-env.test.ts diff --git a/src/agent/bridge-system-prompt.ts b/src/agent/bridge-system-prompt.ts index ec581476..6ea0f644 100644 --- a/src/agent/bridge-system-prompt.ts +++ b/src/agent/bridge-system-prompt.ts @@ -64,7 +64,7 @@ export const BRIDGE_SYSTEM_PROMPT = `# lark-channel-bridge 运行约定 你想发一张可交互的卡片让用户点选时: 1. 用 \`lark-cli\` 把卡发到 \`bridge_context.chat_id\`: - \`lark-cli im send-card --chat-id --card ''\` + \`"$LARK_CHANNEL_LARK_CLI_BIN" im send-card --chat-id --card ''\` 2. 卡片用 CardKit 2.0 schema(\`schema: "2.0"\`)。 3. **如果你希望用户点按钮后回调到你(让你在同一会话里继续处理)**: - 按钮的 \`value\` 对象**必须**同时包含 \`__bridge_cb: true\` 和 \`bridge_token: ""\`。 @@ -99,8 +99,10 @@ bridge 会给你的子进程注入当前运行 profile 的环境变量: - \`LARK_CHANNEL_PROFILE\`: 当前 bridge profile - \`LARK_CHANNEL_CONFIG\`: 当前 profile 的 lark-cli source projection - \`LARKSUITE_CLI_CONFIG_DIR\`: 当前 profile 的 lark-cli 私有配置目录 +- \`LARKSUITE_CLI_DATA_DIR\`: bridge 启动账号的 lark-cli data 目录(Linux keychain 存储位置) +- \`LARK_CHANNEL_LARK_CLI_BIN\`: lark-cli 的绝对路径 -因此普通 \`lark-cli ...\` 命令会自动进入当前 lark-channel 工作区,读取当前 profile 的私有 lark-cli 配置。不要 unset \`LARK_CHANNEL\` / \`LARK_CHANNEL_HOME\` / \`LARK_CHANNEL_PROFILE\` / \`LARKSUITE_CLI_CONFIG_DIR\`,也不要用 \`env -u LARK_CHANNEL\` 绕回本机普通配置。 +调用 lark-cli 时必须使用 \`"$LARK_CHANNEL_LARK_CLI_BIN" ...\`,不要调用裸的 \`lark-cli ...\`:agent 的 login shell 可能重置 PATH。该命令会自动进入当前 lark-channel 工作区,读取当前 profile 的私有配置和 keychain。不要 unset \`LARK_CHANNEL\` / \`LARK_CHANNEL_HOME\` / \`LARK_CHANNEL_PROFILE\` / \`LARKSUITE_CLI_CONFIG_DIR\` / \`LARKSUITE_CLI_DATA_DIR\` / \`LARK_CHANNEL_LARK_CLI_BIN\`,也不要用 \`env -u LARK_CHANNEL\` 绕回本机普通配置。 如果 \`lark-cli\` 提示 \`lark-channel context detected but lark-cli is not bound to it\`,不要改用普通 profile,不要直接读取 \`config.json\` 里的账号或密钥,也不要自行执行 bind。停止当前操作并请用户重启 bridge 或运行 bridge doctor/preflight。 @@ -115,12 +117,12 @@ bridge 会给你的子进程注入当前运行 profile 的环境变量: - \`chat_type: group\`(含 topic 群)—— **不要**调 \`lark-cli auth login\`。device flow 把 \`verification_url\` 发到群里,谁先点谁拿走 token——会绑定到错的身份。正确做法是回复用户:"授权要在私聊里做,请单独私信我。" 2. **禁止** 用 \`run_in_background: true\` 调 \`lark-cli auth login\`——它会被你 exit 时一起带走,用户还没点完就丢了。 3. **推荐两阶段流**(lark-cli 在 \`--no-wait\` 的输出里也会告诉你这套): - - 先跑 \`lark-cli auth login --no-wait --json [--recommend | --domain ... | --scope ...]\`,**这一步秒返回**,stdout 里有 \`verification_url\` 和 \`device_code\`。 + - 先跑 \`"$LARK_CHANNEL_LARK_CLI_BIN" auth login --no-wait --json [--recommend | --domain ... | --scope ...]\`,**这一步秒返回**,stdout 里有 \`verification_url\` 和 \`device_code\`。 - 把 \`verification_url\` **原样**用代码块发给用户(不要 Markdown 链接化、不要 URL 编码)。 - - 紧接着同一轮里跑 \`lark-cli auth login --device-code \`,**这一步前台阻塞**直到用户点完或 10 分钟超时——这是你应该等的地方,不要丢到后台。 + - 紧接着同一轮里跑 \`"$LARK_CHANNEL_LARK_CLI_BIN" auth login --device-code \`,**这一步前台阻塞**直到用户点完或 10 分钟超时——这是你应该等的地方,不要丢到后台。 4. \`lark-cli auth login --device-code \` 成功后,继续在同一个当前 profile 环境里执行: - - \`lark-cli config strict-mode off\` - - \`lark-cli config default-as auto\` + - \`"$LARK_CHANNEL_LARK_CLI_BIN" config strict-mode off\` + - \`"$LARK_CHANNEL_LARK_CLI_BIN" config default-as auto\` 这会让当前 profile 同时可用应用身份和已授权用户身份。不要重新 bind,不要绕回本机普通配置。 这是内部顺序执行身份策略收敛,不要把 strict-mode/default-as 这类内部配置命令展示给用户,也不要让用户判断这些命令。面向用户只说:"当前 profile 还没有可用的用户身份授权,请打开下面链接完成授权;授权完成后我会继续处理。" 5. 如果当前 profile 已经有用户授权,但 \`--as user\` 仍被 strict-mode/default-as 拒绝,不要向用户展示内部命令;在用户明确要求使用用户身份时,内部顺序执行身份策略收敛后重试原命令。 diff --git a/src/agent/lark-channel-env.ts b/src/agent/lark-channel-env.ts index 591818aa..7d826a62 100644 --- a/src/agent/lark-channel-env.ts +++ b/src/agent/lark-channel-env.ts @@ -1,10 +1,14 @@ -import { join } from 'node:path'; +import { accessSync, constants } from 'node:fs'; +import { userInfo } from 'node:os'; +import { delimiter, dirname, extname, isAbsolute, join, resolve } from 'node:path'; export interface LarkChannelEnvContext { profile?: string; rootDir?: string; configPath?: string; larkCliConfigDir?: string; + larkCliDataDir?: string; + larkCliBinPath?: string; larkCliSourceConfigFile?: string; } @@ -27,9 +31,68 @@ export function buildLarkChannelEnv(context?: LarkChannelEnvContext): NodeJS.Pro const larkCliConfigDir = nonEmpty(context?.larkCliConfigDir); if (larkCliConfigDir) env.LARKSUITE_CLI_CONFIG_DIR = larkCliConfigDir; + const larkCliDataDir = + nonEmpty(context?.larkCliDataDir) ?? + nonEmpty(process.env.LARKSUITE_CLI_DATA_DIR) ?? + defaultLarkCliDataDir(); + if (larkCliDataDir) env.LARKSUITE_CLI_DATA_DIR = larkCliDataDir; + + const larkCliBinPath = resolveLarkCliBinPath(context?.larkCliBinPath); + if (larkCliBinPath) { + env.LARK_CHANNEL_LARK_CLI_BIN = larkCliBinPath; + env.PATH = prependPath(dirname(larkCliBinPath), process.env.PATH); + } + return env; } +function defaultLarkCliDataDir(): string | undefined { + if (process.platform !== 'linux') return undefined; + try { + const home = nonEmpty(userInfo().homedir); + return home ? join(home, '.local', 'share') : undefined; + } catch { + return undefined; + } +} + +function resolveLarkCliBinPath(configuredPath: string | undefined): string | undefined { + const configured = nonEmpty(configuredPath) ?? nonEmpty(process.env.LARK_CHANNEL_LARK_CLI_BIN); + if (configured) return isAbsolute(configured) ? configured : findExecutable(configured); + return findExecutable('lark-cli'); +} + +function findExecutable(command: string): string | undefined { + for (const dir of (process.env.PATH ?? '').split(delimiter)) { + if (!dir) continue; + for (const candidate of executableCandidates(resolve(dir), command)) { + try { + accessSync(candidate, constants.X_OK); + return candidate; + } catch { + // Continue searching PATH. + } + } + } + return undefined; +} + +function executableCandidates(dir: string, command: string): string[] { + const candidates = [join(dir, command)]; + if (extname(command)) return candidates; + for (const ext of (process.env.PATHEXT ?? '').split(';')) { + const trimmed = ext.trim(); + if (trimmed) candidates.push(join(dir, `${command}${trimmed}`)); + } + return candidates; +} + +function prependPath(dir: string, currentPath: string | undefined): string { + const entries = (currentPath ?? '').split(delimiter).filter(Boolean); + if (entries.includes(dir)) return entries.join(delimiter); + return [dir, ...entries].join(delimiter); +} + function nonEmpty(value: string | undefined): string | undefined { const trimmed = value?.trim(); return trimmed ? value : undefined; diff --git a/src/bot/channel.ts b/src/bot/channel.ts index 6ca43ef0..4ef6a633 100644 --- a/src/bot/channel.ts +++ b/src/bot/channel.ts @@ -77,9 +77,9 @@ const STREAM_TERMINAL_GRACE_MS = 3000; const REACTION_CLEANUP_GRACE_MS = 1000; const BRIDGE_AGENT_INSTRUCTIONS = [ - '你在 bridge 进程中运行,普通 lark-cli 会继承 LARK_CHANNEL=1 并进入 bridge-bound 模式。', - '不要 unset LARK_CHANNEL / LARK_CHANNEL_HOME / LARK_CHANNEL_PROFILE / LARKSUITE_CLI_CONFIG_DIR,也不要用 env -u LARK_CHANNEL 绕回本机普通配置。', - 'Codex bridge 默认使用 danger-full-access 对齐 Claude bridge 的 bypassPermissions 行为,因此 lark-cli 应能像用户本机终端一样访问 keychain。', + '你在 bridge 进程中运行,调用 lark-cli 必须使用 "$LARK_CHANNEL_LARK_CLI_BIN",不要依赖可能被 login shell 重置的 PATH。', + '不要 unset LARK_CHANNEL / LARK_CHANNEL_HOME / LARK_CHANNEL_PROFILE / LARKSUITE_CLI_CONFIG_DIR / LARKSUITE_CLI_DATA_DIR / LARK_CHANNEL_LARK_CLI_BIN,也不要用 env -u LARK_CHANNEL 绕回本机普通配置。', + 'bridge 已固定当前账号的 lark-cli data 目录,因此隔离 HOME 不会切断 Linux keychain。', '如果提示 lark-channel context detected but not bound,停止当前操作并请用户重启 bridge 或运行 bridge doctor/preflight;不要改用普通 profile,不要自行 bind,也不要直接读取 config.json 里的账号或密钥。', ]; diff --git a/tests/process/claude-adapter.test.ts b/tests/process/claude-adapter.test.ts index 8a9bed54..82dbb103 100644 --- a/tests/process/claude-adapter.test.ts +++ b/tests/process/claude-adapter.test.ts @@ -60,6 +60,8 @@ describe('ClaudeAdapter process contract', () => { expect(record.systemPrompt).toContain('__bridge_cb'); expect(record.systemPrompt).toContain('LARK_CHANNEL_PROFILE'); expect(record.systemPrompt).toContain('LARKSUITE_CLI_CONFIG_DIR'); + expect(record.systemPrompt).toContain('LARKSUITE_CLI_DATA_DIR'); + expect(record.systemPrompt).toContain('LARK_CHANNEL_LARK_CLI_BIN'); expect(record.systemPrompt).not.toContain('lark-cli config bind --source lark-channel'); expect(record.systemPrompt).not.toContain('__claude_cb'); expect(record.argv).not.toContain('--resume'); @@ -74,6 +76,8 @@ describe('ClaudeAdapter process contract', () => { const rootDir = join(fake.dir, 'channel-home'); const configPath = join(rootDir, 'config.custom.json'); const larkCliConfigDir = join(rootDir, 'profiles', 'codex-dev', 'lark-cli'); + const larkCliDataDir = join(fake.dir, 'lark-cli-data'); + const larkCliBinPath = join(fake.dir, 'bin', 'lark-cli'); const larkCliSourceConfigFile = join(rootDir, 'profiles', 'codex-dev', 'lark-cli-source', 'config.json'); const run = new ClaudeAdapter({ @@ -83,6 +87,8 @@ describe('ClaudeAdapter process contract', () => { rootDir, configPath, larkCliConfigDir, + larkCliDataDir, + larkCliBinPath, larkCliSourceConfigFile, }, }).run({ @@ -100,6 +106,8 @@ describe('ClaudeAdapter process contract', () => { LARK_CHANNEL_HOME: rootDir, LARK_CHANNEL_CONFIG: larkCliSourceConfigFile, LARKSUITE_CLI_CONFIG_DIR: larkCliConfigDir, + LARKSUITE_CLI_DATA_DIR: larkCliDataDir, + LARK_CHANNEL_LARK_CLI_BIN: larkCliBinPath, }); }); @@ -249,6 +257,8 @@ async function createFakeClaude(options: { ' LARK_CHANNEL_HOME: process.env.LARK_CHANNEL_HOME,', ' LARK_CHANNEL_CONFIG: process.env.LARK_CHANNEL_CONFIG,', ' LARKSUITE_CLI_CONFIG_DIR: process.env.LARKSUITE_CLI_CONFIG_DIR,', + ' LARKSUITE_CLI_DATA_DIR: process.env.LARKSUITE_CLI_DATA_DIR,', + ' LARK_CHANNEL_LARK_CLI_BIN: process.env.LARK_CHANNEL_LARK_CLI_BIN,', ' },', ' }));', ` const lines = ${JSON.stringify(options.lines)};`, @@ -274,6 +284,8 @@ async function readRecord(path: string): Promise<{ LARK_CHANNEL_HOME?: string; LARK_CHANNEL_CONFIG?: string; LARKSUITE_CLI_CONFIG_DIR?: string; + LARKSUITE_CLI_DATA_DIR?: string; + LARK_CHANNEL_LARK_CLI_BIN?: string; }; }> { return JSON.parse(await readFile(path, 'utf8')) as { @@ -287,6 +299,8 @@ async function readRecord(path: string): Promise<{ LARK_CHANNEL_HOME?: string; LARK_CHANNEL_CONFIG?: string; LARKSUITE_CLI_CONFIG_DIR?: string; + LARKSUITE_CLI_DATA_DIR?: string; + LARK_CHANNEL_LARK_CLI_BIN?: string; }; }; } diff --git a/tests/process/codex-adapter.test.ts b/tests/process/codex-adapter.test.ts index c86aa036..ef66a7da 100644 --- a/tests/process/codex-adapter.test.ts +++ b/tests/process/codex-adapter.test.ts @@ -73,9 +73,10 @@ describe('CodexAdapter process contract', () => { expect(record.argv).not.toContain('hello from lark'); expect(record.stdin).toContain('lark-channel-bridge 运行约定'); expect(record.stdin).toContain('__bridge_cb'); - expect(record.stdin).toContain('lark-cli auth login'); + expect(record.stdin).toContain('"$LARK_CHANNEL_LARK_CLI_BIN" auth login'); expect(record.stdin).toContain('LARK_CHANNEL_PROFILE'); expect(record.stdin).toContain('LARKSUITE_CLI_CONFIG_DIR'); + expect(record.stdin).toContain('LARKSUITE_CLI_DATA_DIR'); expect(record.stdin).not.toContain('lark-cli config bind --source lark-channel'); expect(record.stdin).toContain('hello from lark'); expect(record.stdin).not.toBe('hello from lark'); @@ -95,6 +96,8 @@ describe('CodexAdapter process contract', () => { const rootDir = join(fake.dir, 'channel-home'); const configPath = join(rootDir, 'config.custom.json'); const larkCliConfigDir = join(rootDir, 'profiles', 'codex-dev', 'lark-cli'); + const larkCliDataDir = join(fake.dir, 'lark-cli-data'); + const larkCliBinPath = join(fake.dir, 'bin', 'lark-cli'); const larkCliSourceConfigFile = join(rootDir, 'profiles', 'codex-dev', 'lark-cli-source', 'config.json'); const run = new CodexAdapter({ @@ -105,6 +108,8 @@ describe('CodexAdapter process contract', () => { rootDir, configPath, larkCliConfigDir, + larkCliDataDir, + larkCliBinPath, larkCliSourceConfigFile, }, }).run({ @@ -122,6 +127,8 @@ describe('CodexAdapter process contract', () => { LARK_CHANNEL_HOME: rootDir, LARK_CHANNEL_CONFIG: larkCliSourceConfigFile, LARKSUITE_CLI_CONFIG_DIR: larkCliConfigDir, + LARKSUITE_CLI_DATA_DIR: larkCliDataDir, + LARK_CHANNEL_LARK_CLI_BIN: larkCliBinPath, CODEX_HOME: '/outer/codex-home', }); }); @@ -452,6 +459,8 @@ async function createFakeCodex(options: { ' LARK_CHANNEL_HOME: process.env.LARK_CHANNEL_HOME,', ' LARK_CHANNEL_CONFIG: process.env.LARK_CHANNEL_CONFIG,', ' LARKSUITE_CLI_CONFIG_DIR: process.env.LARKSUITE_CLI_CONFIG_DIR,', + ' LARKSUITE_CLI_DATA_DIR: process.env.LARKSUITE_CLI_DATA_DIR,', + ' LARK_CHANNEL_LARK_CLI_BIN: process.env.LARK_CHANNEL_LARK_CLI_BIN,', ' CODEX_HOME: process.env.CODEX_HOME,', ' APP_SECRET: process.env.APP_SECRET,', ' PATH: process.env.PATH,', @@ -479,6 +488,8 @@ async function readRecord(path: string): Promise<{ LARK_CHANNEL_HOME?: string; LARK_CHANNEL_CONFIG?: string; LARKSUITE_CLI_CONFIG_DIR?: string; + LARKSUITE_CLI_DATA_DIR?: string; + LARK_CHANNEL_LARK_CLI_BIN?: string; CODEX_HOME?: string; APP_SECRET?: string; PATH?: string; @@ -494,6 +505,8 @@ async function readRecord(path: string): Promise<{ LARK_CHANNEL_HOME?: string; LARK_CHANNEL_CONFIG?: string; LARKSUITE_CLI_CONFIG_DIR?: string; + LARKSUITE_CLI_DATA_DIR?: string; + LARK_CHANNEL_LARK_CLI_BIN?: string; CODEX_HOME?: string; APP_SECRET?: string; PATH?: string; diff --git a/tests/unit/agent/bridge-system-prompt.test.ts b/tests/unit/agent/bridge-system-prompt.test.ts index 00d14a67..c359791a 100644 --- a/tests/unit/agent/bridge-system-prompt.test.ts +++ b/tests/unit/agent/bridge-system-prompt.test.ts @@ -39,6 +39,19 @@ describe('bridge system prompt bot collaboration rules', () => { }); }); +describe('bridge lark-cli runtime rules', () => { + it('uses the injected absolute executable instead of relying on PATH', () => { + expect(BRIDGE_SYSTEM_PROMPT).toContain('LARK_CHANNEL_LARK_CLI_BIN'); + expect(BRIDGE_SYSTEM_PROMPT).toContain('"$LARK_CHANNEL_LARK_CLI_BIN" auth login'); + expect(BRIDGE_SYSTEM_PROMPT).toContain('不要调用裸的'); + }); + + it('keeps Linux keychain lookup independent from an isolated HOME', () => { + expect(BRIDGE_SYSTEM_PROMPT).toContain('LARKSUITE_CLI_DATA_DIR'); + expect(BRIDGE_SYSTEM_PROMPT).toContain('Linux keychain'); + }); +}); + describe('buildBridgeSystemPrompt', () => { it('returns the base prompt unchanged when no identity is available', () => { expect(buildBridgeSystemPrompt(undefined)).toBe(BRIDGE_SYSTEM_PROMPT); diff --git a/tests/unit/agent/lark-channel-env.test.ts b/tests/unit/agent/lark-channel-env.test.ts new file mode 100644 index 00000000..de48f104 --- /dev/null +++ b/tests/unit/agent/lark-channel-env.test.ts @@ -0,0 +1,76 @@ +import { chmod, mkdtemp, rm, writeFile } from 'node:fs/promises'; +import { tmpdir, userInfo } from 'node:os'; +import { delimiter, dirname, join, resolve } from 'node:path'; +import { afterEach, describe, expect, it } from 'vitest'; +import { buildLarkChannelEnv } from '../../../src/agent/lark-channel-env'; + +describe('buildLarkChannelEnv', () => { + const cleanup: string[] = []; + const originalHome = process.env.HOME; + const originalPath = process.env.PATH; + const originalPathExt = process.env.PATHEXT; + const originalDataDir = process.env.LARKSUITE_CLI_DATA_DIR; + const originalBinPath = process.env.LARK_CHANNEL_LARK_CLI_BIN; + + afterEach(async () => { + restoreEnv('HOME', originalHome); + restoreEnv('PATH', originalPath); + restoreEnv('PATHEXT', originalPathExt); + restoreEnv('LARKSUITE_CLI_DATA_DIR', originalDataDir); + restoreEnv('LARK_CHANNEL_LARK_CLI_BIN', originalBinPath); + await Promise.all(cleanup.splice(0).map((dir) => rm(dir, { recursive: true, force: true }))); + }); + + it('injects explicit lark-cli data and executable paths', () => { + const larkCliDataDir = resolve('lark-cli-data'); + const larkCliBinPath = resolve('fake-bin', 'lark-cli'); + const env = buildLarkChannelEnv({ + larkCliDataDir, + larkCliBinPath, + }); + + expect(env.LARKSUITE_CLI_DATA_DIR).toBe(larkCliDataDir); + expect(env.LARK_CHANNEL_LARK_CLI_BIN).toBe(larkCliBinPath); + expect(env.PATH?.split(delimiter)[0]).toBe(dirname(larkCliBinPath)); + }); + + it.runIf(process.platform === 'linux')( + 'uses the account home for keychain data when the process HOME is isolated', + () => { + process.env.HOME = '/opt/isolated-agent/home'; + delete process.env.LARKSUITE_CLI_DATA_DIR; + + const env = buildLarkChannelEnv(); + + expect(env.LARKSUITE_CLI_DATA_DIR).toBe(join(userInfo().homedir, '.local', 'share')); + expect(env.LARKSUITE_CLI_DATA_DIR).not.toContain('/opt/isolated-agent/home'); + }, + ); + + it('resolves lark-cli to an absolute path before spawning the agent', async () => { + const root = await mkdtemp(join(tmpdir(), 'bridge-lark-cli-env-')); + cleanup.push(root); + const binary = join(root, process.platform === 'win32' ? 'lark-cli.CMD' : 'lark-cli'); + await writeFile( + binary, + process.platform === 'win32' ? '@exit /b 0\r\n' : '#!/bin/sh\nexit 0\n', + 'utf8', + ); + await chmod(binary, 0o755); + process.env.PATH = root; + if (process.platform === 'win32') process.env.PATHEXT = '.CMD'; + delete process.env.LARK_CHANNEL_LARK_CLI_BIN; + + const env = buildLarkChannelEnv(); + + expect(env.LARK_CHANNEL_LARK_CLI_BIN).toBe(binary); + }); +}); + +function restoreEnv(name: string, value: string | undefined): void { + if (value === undefined) { + delete process.env[name]; + } else { + process.env[name] = value; + } +} diff --git a/tests/unit/agent/prompt-builder.test.ts b/tests/unit/agent/prompt-builder.test.ts index ac443788..4abe7193 100644 --- a/tests/unit/agent/prompt-builder.test.ts +++ b/tests/unit/agent/prompt-builder.test.ts @@ -104,10 +104,10 @@ describe('agent prompt builder', () => { expect(source).not.toContain('命令必须写成 env -u LARK_CHANNEL'); expect(source).not.toContain('env -u LARK_CHANNEL lark-cli'); - expect(source).toContain('danger-full-access'); - expect(source).toContain('bypassPermissions'); expect(source).toContain('不要 unset LARK_CHANNEL'); expect(source).toContain('LARKSUITE_CLI_CONFIG_DIR'); + expect(source).toContain('LARKSUITE_CLI_DATA_DIR'); + expect(source).toContain('LARK_CHANNEL_LARK_CLI_BIN'); expect(source).not.toContain('lark-cli config bind --source lark-channel'); }); @@ -115,9 +115,10 @@ describe('agent prompt builder', () => { const source = readFileSync(join(process.cwd(), 'src/agent/bridge-system-prompt.ts'), 'utf8'); expect(source).toContain('LARKSUITE_CLI_CONFIG_DIR'); - expect(source).toContain('lark-cli auth login --device-code'); - expect(source).toContain('lark-cli config strict-mode off'); - expect(source).toContain('lark-cli config default-as auto'); + expect(source).toContain('LARKSUITE_CLI_DATA_DIR'); + expect(source).toContain('"$LARK_CHANNEL_LARK_CLI_BIN" auth login --device-code'); + expect(source).toContain('"$LARK_CHANNEL_LARK_CLI_BIN" config strict-mode off'); + expect(source).toContain('"$LARK_CHANNEL_LARK_CLI_BIN" config default-as auto'); expect(source).not.toContain('env -u LARK_CHANNEL lark-cli auth login'); });