Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,8 @@ lark-channel-bridge profile export <name> --include-secrets --yes
- **工具调用显示**:控制最终回复卡片 / markdown 中是否展示工具块。
- **COT 过程消息**:`关闭` 只发送最终回复;`简略` 先用 COT 消息展示 agent 的过程文本和工具摘要;`详细` 还会展示工具参数和截断后的输出。

新建 Codex profile 默认只交付最终结果:纯文本一次性发送、隐藏工具调用、关闭 COT。已有 profile 如果明确设置过展示偏好,会继续保留原选择;Claude 仍默认使用流式消息卡片。

开启 COT 后,bridge 会把过程消息和最终答案拆成两条消息。过程消息用于追踪 agent 做了什么;最终答案仍由 agent 原始文本生成,bridge 不做启发式过滤。若 agent 把最终答案也作为普通流式文本输出,COT 过程消息中可能会出现对应片段。

## lark-cli 身份策略
Expand Down
4 changes: 2 additions & 2 deletions src/card/config-card.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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' },
],
},
{
Expand All @@ -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' },
],
},
Expand Down
18 changes: 12 additions & 6 deletions src/config/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
/**
Expand Down Expand Up @@ -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;
};
Expand Down Expand Up @@ -200,20 +203,23 @@ 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;
if (raw === 'text' && cfg.preferences?.messageReplyMigrated !== true) {
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 {
Expand Down
25 changes: 24 additions & 1 deletion tests/integration/bot/claude-regression.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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[] }> = [];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down