diff --git a/README.md b/README.md index 6c5c6689..152e6625 100644 --- a/README.md +++ b/README.md @@ -169,6 +169,10 @@ DMs do not require an @ mention. Groups and topic groups require `@bot` by defau - **Tool-call display**: controls whether tool blocks appear in the final card / markdown reply. - **COT process message**: `off` sends only the final reply; `brief` first sends a COT message with agent progress text and tool summaries; `detailed` also includes tool args and truncated output. +Fresh Codex profiles default to final-only delivery: plain text, hidden tool +calls, and COT off. Existing profiles with explicit display preferences keep +their choices. Claude keeps the streaming message-card default. + When COT is enabled, the bridge splits the process view and final answer into two messages. The COT message is for tracing what the agent did; the final answer is still generated from the agent's raw text, without heuristic bridge-side filtering. If an agent emits final-answer text as ordinary stream text, that text can also appear in the COT process message. ## lark-cli identity policy diff --git a/README.zh.md b/README.zh.md index fae27156..fc263158 100644 --- a/README.zh.md +++ b/README.zh.md @@ -169,6 +169,8 @@ lark-channel-bridge profile export --include-secrets --yes - **工具调用显示**:控制最终回复卡片 / markdown 中是否展示工具块。 - **COT 过程消息**:`关闭` 只发送最终回复;`简略` 先用 COT 消息展示 agent 的过程文本和工具摘要;`详细` 还会展示工具参数和截断后的输出。 +新建 Codex profile 默认只交付最终结果:纯文本一次性发送、隐藏工具调用、关闭 COT。已有 profile 如果明确设置过展示偏好,会继续保留原选择;Claude 仍默认使用流式消息卡片。 + 开启 COT 后,bridge 会把过程消息和最终答案拆成两条消息。过程消息用于追踪 agent 做了什么;最终答案仍由 agent 原始文本生成,bridge 不做启发式过滤。若 agent 把最终答案也作为普通流式文本输出,COT 过程消息中可能会出现对应片段。 ## lark-cli 身份策略 diff --git a/src/card/config-card.ts b/src/card/config-card.ts index a20b8af9..d84c4713 100644 --- a/src/card/config-card.ts +++ b/src/card/config-card.ts @@ -174,7 +174,7 @@ export function configFormCard(opts: ConfigFormOpts): object { initial_option: opts.messageReply === 'card' ? 'markdown' : opts.messageReply, options: [ { text: { tag: 'plain_text', content: '纯文本' }, value: 'text' }, - { text: { tag: 'plain_text', content: '消息卡片(默认)' }, value: 'markdown' }, + { text: { tag: 'plain_text', content: '消息卡片' }, value: 'markdown' }, ], }, { @@ -189,7 +189,7 @@ export function configFormCard(opts: ConfigFormOpts): object { name: 'show_tool_calls', initial_option: opts.showToolCalls ? 'show' : 'hide', options: [ - { text: { tag: 'plain_text', content: '显示(默认)' }, value: 'show' }, + { text: { tag: 'plain_text', content: '显示' }, value: 'show' }, { text: { tag: 'plain_text', content: '隐藏' }, value: 'hide' }, ], }, diff --git a/src/config/schema.ts b/src/config/schema.ts index ed00c178..e231d4dc 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -87,7 +87,7 @@ export interface AppAccess { } export interface AppPreferences { - /** Reply rendering mode for IM (group/p2p) messages. Default 'card'. */ + /** Reply rendering mode for IM (group/p2p) messages. */ messageReply?: MessageReplyMode; /** * Internal marker: pre-0.1.27 the value `'text'` meant "lightweight @@ -100,8 +100,9 @@ export interface AppPreferences { messageReplyMigrated?: boolean; /** * Whether to render tool-call blocks (Bash / Read / Edit / ...) in the - * output. Default true. Turn off if you only care about Claude's final - * text answer and want to hide the "工具调用过程". + * output. Defaults to false for Codex and true for Claude. Turn off if you + * only care about the agent's final text answer and want to hide the + * "工具调用过程". */ showToolCalls?: boolean; /** @@ -163,6 +164,8 @@ export interface AppPreferences { * belong at this top level alongside them. */ export interface AppConfig { + /** Present on v2 profile configs; absent on legacy single-profile configs. */ + agentKind?: 'claude' | 'codex'; accounts: { app: AppCredentials; }; @@ -200,7 +203,8 @@ export function secretKeyForApp(appId: string): string { * (which sets `messageReplyMigrated: true`), we map their `text` → * `markdown` so the behavior stays the same after upgrade. * - * Default for fresh configs (no `messageReply` set) is `'markdown'`. + * Fresh Codex profiles default to `'text'` so verbose progress and command + * execution stay out of Lark. Claude and legacy configs keep `'markdown'`. */ export function getMessageReplyMode(cfg: AppConfig): MessageReplyMode { const raw = cfg.preferences?.messageReply; @@ -208,12 +212,14 @@ export function getMessageReplyMode(cfg: AppConfig): MessageReplyMode { return 'markdown'; } if (raw === 'card' || raw === 'markdown' || raw === 'text') return raw; - return 'markdown'; + return cfg.agentKind === 'codex' ? 'text' : 'markdown'; } /** Resolve the show-tool-calls preference with default fallback. */ export function getShowToolCalls(cfg: AppConfig): boolean { - return cfg.preferences?.showToolCalls !== false; + const configured = cfg.preferences?.showToolCalls; + if (configured !== undefined) return configured; + return cfg.agentKind !== 'codex'; } export function getCotMessages(cfg: AppConfig): CotMessagesMode { diff --git a/tests/integration/bot/claude-regression.test.ts b/tests/integration/bot/claude-regression.test.ts index eab1edd5..df234290 100644 --- a/tests/integration/bot/claude-regression.test.ts +++ b/tests/integration/bot/claude-regression.test.ts @@ -1,7 +1,11 @@ import { readFile } from 'node:fs/promises'; import { join } from 'node:path'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { getMessageReplyMode, getRequireMentionInGroup } from '../../../src/config/schema.js'; +import { + getMessageReplyMode, + getRequireMentionInGroup, + getShowToolCalls, +} from '../../../src/config/schema.js'; import { PendingQueue } from '../../../src/bot/pending-queue.js'; import type { NormalizedMessage } from '@larksuite/channel'; @@ -31,6 +35,25 @@ describe('Claude IM regression boundaries', () => { expect(getMessageReplyMode(cardCfg)).toBe('card'); }); + it('defaults Codex to a final-only reply while preserving explicit display choices', () => { + const codexCfg = { + agentKind: 'codex' as const, + accounts: { app: { id: 'app-id', secret: 'secret', tenant: 'feishu' as const } }, + }; + + expect(getMessageReplyMode(codexCfg)).toBe('text'); + expect(getShowToolCalls(codexCfg)).toBe(false); + expect( + getMessageReplyMode({ + ...codexCfg, + preferences: { messageReply: 'markdown' as const }, + }), + ).toBe('markdown'); + expect( + getShowToolCalls({ ...codexCfg, preferences: { showToolCalls: true } }), + ).toBe(true); + }); + it('queues messages that arrive while a run is active and flushes them as the next batch', () => { vi.useFakeTimers(); const flushed: Array<{ scope: string; batch: NormalizedMessage[] }> = []; diff --git a/tests/integration/bot/markdown-stream-startup-failure.test.ts b/tests/integration/bot/markdown-stream-startup-failure.test.ts index 7769523f..70a4faaf 100644 --- a/tests/integration/bot/markdown-stream-startup-failure.test.ts +++ b/tests/integration/bot/markdown-stream-startup-failure.test.ts @@ -324,7 +324,12 @@ async function createHarness(options: { codex: { binaryPath: '/usr/local/bin/codex', }, - ...(options.messageReply ? { preferences: { messageReply: options.messageReply } } : {}), + preferences: { + // This suite exercises stream behavior explicitly. Codex profiles now + // default to final-only text delivery, so opt into a streaming mode. + messageReply: options.messageReply ?? 'markdown', + messageReplyMigrated: true, + }, }); const profileConfig = { ...baseProfileConfig,