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
10 changes: 10 additions & 0 deletions src/agent/codex/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createInterface } from 'node:readline';
import type { Readable, Writable } from 'node:stream';
import { homedir } from 'node:os';
import { join } from 'node:path';
import type { SandboxMode } from '../../config/profile-schema';
import { log } from '../../core/logger';
Expand Down Expand Up @@ -63,6 +64,15 @@ export class CodexAdapter implements AgentAdapter {
this.botIdentity = identity;
}

getGeneratedImagesDir(): string {
const home =
this.codexHome ??
(!this.inheritCodexHome
? join(this.profileStateDir, 'codex-home')
: process.env.CODEX_HOME || join(homedir(), '.codex'));
return join(home, 'generated_images');
}

async isAvailable(): Promise<boolean> {
return (await this.checkAvailability()).ok;
}
Expand Down
5 changes: 5 additions & 0 deletions src/agent/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,4 +88,9 @@ export interface AgentAdapter {
* Adapters that don't bake identity into their prompts may omit it.
*/
setBotIdentity?(identity: AgentBotIdentity): void;
/**
* Directory where this adapter writes generated image artifacts. The bridge
* uses this optional hint for fast-path delivery while a run is still active.
*/
getGeneratedImagesDir?(): string | undefined;
}
38 changes: 33 additions & 5 deletions src/bot/channel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import type { WorkspaceStore } from '../workspace/store';
import { ActiveRuns, type RunHandle } from './active-runs';
import { ChatModeCache, type ChatMode } from './chat-mode-cache';
import { handleCommentMention } from './comments';
import { GeneratedImageDelivery } from './generated-image-delivery';
import { recordRunSessionEvent, startRunFlow } from './run-flow';
import { commandSessionCatalogIdentity } from './session-catalog-identity';
import { startKeepalive } from './keepalive';
Expand All @@ -76,6 +77,8 @@ import {
const DEBOUNCE_MS = 600;
const STREAM_TERMINAL_GRACE_MS = 3000;
const REACTION_CLEANUP_GRACE_MS = 1000;
const CODEX_GENERATED_IMAGE_INSTRUCTION =
'当你通过 imagegen 生成或编辑图片时,只负责完成图片生成;不要读取飞书发送技能,也不要调用 lark-cli 发送该图片。bridge 会监控 Codex generated_images 目录并自动把新图片回复到当前消息。';

const BRIDGE_AGENT_INSTRUCTIONS = [
'你在 bridge 进程中运行,普通 lark-cli 会继承 LARK_CHANNEL=1 并进入 bridge-bound 模式。',
Expand Down Expand Up @@ -189,6 +192,7 @@ export async function startChannel(deps: StartChannelDeps): Promise<BridgeChanne
// so /config bumps take effect for the next run.
const pool = new ProcessPool(() => getMaxConcurrentRuns(controls.cfg));
const executor = new RunExecutor({ agent, pool, activeRuns });
const generatedImagesDir = agent.getGeneratedImagesDir?.();

// Resolve the App Secret to plaintext. The config field can be a literal
// string, a "${VAR}" template, or a {source, id} SecretRef referencing
Expand Down Expand Up @@ -250,6 +254,7 @@ export async function startChannel(deps: StartChannelDeps): Promise<BridgeChanne
includeRawEvent: true,
outbound: {
streamThrottleMs: 400,
...(generatedImagesDir ? { allowedFileDirs: [generatedImagesDir] } : {}),
},
// SDK 1.65.0-alpha.3+ knobs.
wsConfig: {
Expand Down Expand Up @@ -315,6 +320,7 @@ export async function startChannel(deps: StartChannelDeps): Promise<BridgeChanne
callbackAuth,
activePolicyFingerprints,
lastRunModelByScope,
generatedImagesDir,
scope,
mode,
});
Expand Down Expand Up @@ -699,6 +705,7 @@ interface RunBatchDeps {
callbackAuth?: CallbackAuth;
activePolicyFingerprints: Map<string, string>;
lastRunModelByScope: Map<string, string>;
generatedImagesDir?: string;
scope: string;
mode: ChatMode;
}
Expand All @@ -717,6 +724,7 @@ async function runAgentBatch(deps: RunBatchDeps): Promise<void> {
callbackAuth,
activePolicyFingerprints,
lastRunModelByScope,
generatedImagesDir,
scope,
mode,
} = deps;
Expand Down Expand Up @@ -806,20 +814,23 @@ async function runAgentBatch(deps: RunBatchDeps): Promise<void> {
const prevModel = lastRunModelByScope.get(scope);
const modelSwitched = prevModel !== undefined && prevModel !== modelSelection;
lastRunModelByScope.set(scope, modelSelection);
const extraInstructions = modelSwitched
? [
const extraInstructions = [
...(modelSwitched
? [
`用户刚把本会话使用的模型切换为「${modelLabel(agentKind, modelPref)}」。` +
'之前的对话里可能提到别的模型,请以当前模型为准;若被问到你用的是什么模型,据此回答。',
]
: undefined;
]
: []),
...(agentKind === 'codex' && generatedImagesDir ? [CODEX_GENERATED_IMAGE_INSTRUCTION] : []),
];

const prompt = buildPrompt(
batch,
attachments,
quotes,
topicContext,
channel.botIdentity,
extraInstructions,
extraInstructions.length > 0 ? extraInstructions : undefined,
);
log.info('prompt', 'built', {
promptChars: prompt.length,
Expand Down Expand Up @@ -858,6 +869,7 @@ async function runAgentBatch(deps: RunBatchDeps): Promise<void> {
controls.profileConfig.agentKind === 'codex'
? codexCapability(controls.profileConfig)
: claudeCapability(controls.profileConfig);
const runRequestedAt = Date.now();
const flow = await startRunFlow({
scopeId: scope,
scope: scopeContext,
Expand Down Expand Up @@ -894,6 +906,20 @@ async function runAgentBatch(deps: RunBatchDeps): Promise<void> {
activePolicyFingerprints.set(scope, flow.policy.policyFingerprint);
const handle = execution.handle;
const eventStream = execution.subscribe();
const generatedImageDelivery =
agentKind === 'codex' && generatedImagesDir
? new GeneratedImageDelivery({
channel,
chatId,
sendOpts,
rootDir: generatedImagesDir,
runId: execution.runId,
startedAt: runRequestedAt,
imageMaxBytes: controls.profileConfig.attachments.imageMaxBytes,
initialThreadId: flow.resumeFrom,
})
: undefined;
generatedImageDelivery?.start();
if (flow.resumeFrom) {
log.info('session', 'resume', { sessionId: flow.resumeFrom, cwd });
} else {
Expand Down Expand Up @@ -923,6 +949,7 @@ async function runAgentBatch(deps: RunBatchDeps): Promise<void> {
}
if (evt.type === 'system' && evt.threadId) {
log.info('session', 'set-thread', { threadId: evt.threadId });
generatedImageDelivery?.setThreadId(evt.threadId);
}
};

Expand Down Expand Up @@ -1139,6 +1166,7 @@ async function runAgentBatch(deps: RunBatchDeps): Promise<void> {
} catch (err) {
log.fail('stream', err);
} finally {
await generatedImageDelivery?.stop();
activePolicyFingerprints.delete(scope);
scheduleWorkingReactionCleanup(channel, lastMsg.messageId, reactionPromise);
}
Expand Down
Loading