(
options: Required<Pick<ChatCommandOptions, 'question'>> &
Pick<ChatCommandOptions, 'sessionId' | 'aiChatId' | 'json' | 'stream' | 'locale' | 'includeEvents'>,
deps: ChatCommandDeps
)
| 147 | } |
| 148 | |
| 149 | export async function runChatTurn( |
| 150 | options: Required<Pick<ChatCommandOptions, 'question'>> & |
| 151 | Pick<ChatCommandOptions, 'sessionId' | 'aiChatId' | 'json' | 'stream' | 'locale' | 'includeEvents'>, |
| 152 | deps: ChatCommandDeps |
| 153 | ): Promise<ChatTurnResult> { |
| 154 | const stdout = deps.stdout ?? process.stdout |
| 155 | const target = resolveAIChatTarget(options, deps) |
| 156 | const startedAt = Date.now() |
| 157 | let answer = '' |
| 158 | let tokenUsage: TokenUsageData | null = null |
| 159 | let streamError: Error | null = null |
| 160 | const events: AgentStreamChunk[] = [] |
| 161 | const contentBlocks: ContentBlock[] = [] |
| 162 | let hasReplayContentBlocks = false |
| 163 | |
| 164 | const runAgentStream = (deps.createRunAgentStream ?? createCliRunAgentStream)(deps.dbManager, deps.aiChatManager) |
| 165 | |
| 166 | // 中文注释:CLI 历史回放依赖 contentBlocks 的时序;只要出现计划/思考块, |
| 167 | // 就同步保留后续正文 text block,避免 UI 使用 blocks 渲染时丢失最终回答。 |
| 168 | const appendTextBlock = (text: string) => { |
| 169 | if (!text) return |
| 170 | const lastBlock = contentBlocks[contentBlocks.length - 1] |
| 171 | if (lastBlock?.type === 'text') { |
| 172 | lastBlock.text += text |
| 173 | } else { |
| 174 | contentBlocks.push({ type: 'text', text }) |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | const appendThinkBlock = (text: string, tag = 'thinking', durationMs?: number) => { |
| 179 | if (!text && durationMs === undefined) return |
| 180 | const lastBlock = contentBlocks[contentBlocks.length - 1] |
| 181 | let targetBlock: ContentBlock | undefined |
| 182 | |
| 183 | if (lastBlock?.type === 'think' && lastBlock.tag === tag) { |
| 184 | lastBlock.text += text |
| 185 | targetBlock = lastBlock |
| 186 | } else if (text.trim().length > 0) { |
| 187 | targetBlock = { type: 'think', tag, text } |
| 188 | contentBlocks.push(targetBlock) |
| 189 | } else if (durationMs !== undefined) { |
| 190 | for (let index = contentBlocks.length - 1; index >= 0; index--) { |
| 191 | const block = contentBlocks[index] |
| 192 | if (block.type === 'think' && block.tag === tag) { |
| 193 | targetBlock = block |
| 194 | break |
| 195 | } |
| 196 | } |
| 197 | } |
| 198 | |
| 199 | if (durationMs !== undefined && targetBlock?.type === 'think') { |
| 200 | targetBlock.durationMs = durationMs |
| 201 | } |
| 202 | if (targetBlock?.type === 'think') { |
| 203 | hasReplayContentBlocks = true |
| 204 | } |
| 205 | } |
| 206 |
no test coverage detected