diff --git a/packages/agentlab/src/catalog.ts b/packages/agentlab/src/catalog.ts index 9369845..a0e25da 100644 --- a/packages/agentlab/src/catalog.ts +++ b/packages/agentlab/src/catalog.ts @@ -26,6 +26,8 @@ export const AGENTLAB_PROVIDER_CATALOG: AgentLabProviderCatalogEntry[] = [ models: [ { id: 'deepseek-chat', label: 'deepseek-chat', capabilities: ['chat'] }, { id: 'deepseek-reasoner', label: 'deepseek-reasoner', capabilities: ['chat'] }, + { id: 'deepseek-v4-flash', label: 'deepseek-v4-flash', capabilities: ['chat'] }, + { id: 'deepseek-v4-pro', label: 'deepseek-v4-pro', capabilities: ['chat'] }, ], }, { diff --git a/packages/agentlab/src/extract.ts b/packages/agentlab/src/extract.ts index b629cb4..77a25be 100644 --- a/packages/agentlab/src/extract.ts +++ b/packages/agentlab/src/extract.ts @@ -12,7 +12,7 @@ import type { AgentLabExpression, AgentLabFewShotPair, } from './types'; -import { reportUsage } from './http'; +import { reportUsage, pickMessageText } from './http'; function coerceString(value: unknown): string { if (typeof value === 'string') return value.trim(); @@ -63,9 +63,9 @@ async function chatCompletion( if (!res.ok) { throw new Error(`AgentLab 提炼接口调用失败: HTTP ${res.status}`); } - const data = (await res.json()) as { choices?: Array<{ message?: { content?: string } }> }; + const data = (await res.json()) as { choices?: Array<{ message?: { content?: string; reasoning_content?: string } }> }; reportUsage(endpoint, data); - return data.choices?.[0]?.message?.content ?? ''; + return pickMessageText(data.choices?.[0]?.message); } /** @@ -103,9 +103,9 @@ async function visionCompletion( if (!res.ok) { throw new Error(`AgentLab 视觉接口调用失败: HTTP ${res.status}`); } - const data = (await res.json()) as { choices?: Array<{ message?: { content?: string } }> }; + const data = (await res.json()) as { choices?: Array<{ message?: { content?: string; reasoning_content?: string } }> }; reportUsage(endpoint, data); - return data.choices?.[0]?.message?.content ?? ''; + return pickMessageText(data.choices?.[0]?.message); } /** generateText + 宽松解析 + 失败重试一次。 */ diff --git a/packages/agentlab/src/http.ts b/packages/agentlab/src/http.ts index 442b6fb..5c86391 100644 --- a/packages/agentlab/src/http.ts +++ b/packages/agentlab/src/http.ts @@ -345,20 +345,31 @@ async function postJson( return (await res.json()) as T; } +/** OpenAI 兼容消息里同时可能存在 content 和 reasoning_content(推理模型专用),取首个非空。 */ +export function pickMessageText(msg: { content?: unknown; reasoning_content?: unknown } | undefined): string { + if (!msg) return ''; + const c = typeof msg.content === 'string' ? msg.content : ''; + const r = typeof msg.reasoning_content === 'string' ? msg.reasoning_content : ''; + return (c || r).trim(); +} + /** * 设置页「测试连通性」:用最小的 chat 请求探活,返回 ok + 详细错误(含 HTTP 状态码与响应体)。 * 不抛错——把失败包装成 { ok:false, error },让前端直接展示「模型不对 / key 不对」。 + * 推理模型(如 deepseek-v4 系列)会把思考过程放在 reasoning_content 中, + * 当 max_tokens 较小时 content 可能为空——因此同时检查两个字段。 */ export async function testChatEndpoint( endpoint: AgentLabEndpoint, ): Promise<{ ok: boolean; error?: string; reply?: string }> { try { - const data = await postJson<{ choices?: Array<{ message?: { content?: string } }> }>( + const data = await postJson<{ choices?: Array<{ message?: { content?: string; reasoning_content?: string } }> }>( endpoint, '/chat/completions', - { model: endpoint.model, messages: [{ role: 'user', content: '你好' }], max_tokens: 16 }, + // 推理模型需要更多 token 预算才能产出 content;64 足够短路"你好"又不浪费。 + { model: endpoint.model, messages: [{ role: 'user', content: '你好' }], max_tokens: 64 }, ); - const reply = data.choices?.[0]?.message?.content?.trim(); + const reply = pickMessageText(data.choices?.[0]?.message); if (!reply) return { ok: false, error: '接口可达,但返回内容为空(模型可能不支持 chat/completions)。' }; return { ok: true, reply }; } catch (e) { @@ -564,7 +575,7 @@ export async function runPersonaChat( ]; const data = await postJson<{ - choices?: Array<{ message?: { content?: string } }>; + choices?: Array<{ message?: { content?: string; reasoning_content?: string } }>; usage?: OpenAiUsage; }>(chat, '/chat/completions', { model: chat.model, @@ -573,7 +584,7 @@ export async function runPersonaChat( messages, }); reportUsage(chat, data); - const raw = data.choices?.[0]?.message?.content?.trim(); + const raw = pickMessageText(data.choices?.[0]?.message); if (!raw) { throw new Error('AgentLab chat 返回为空'); } diff --git a/packages/agentlab/src/index.ts b/packages/agentlab/src/index.ts index 2b6176b..4c61ea2 100644 --- a/packages/agentlab/src/index.ts +++ b/packages/agentlab/src/index.ts @@ -5,7 +5,7 @@ export { modelsWithCapability, resolveEndpoint, } from './provider'; -export { embedTexts, runPersonaChat, reportUsage, keywordsOf, testChatEndpoint } from './http'; +export { embedTexts, runPersonaChat, reportUsage, keywordsOf, testChatEndpoint, pickMessageText } from './http'; export { selectStickerByEmotion } from './sticker'; export { humanizeText, DEFAULT_TYPO_INTENSITY } from './typo'; export { scoreReplyWillingness } from './willing'; diff --git a/packages/service/src/account/assistant.ts b/packages/service/src/account/assistant.ts index 86723b2..4289ef4 100644 --- a/packages/service/src/account/assistant.ts +++ b/packages/service/src/account/assistant.ts @@ -13,7 +13,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'; import { randomUUID } from 'node:crypto'; import { basename, dirname, extname, join, resolve, sep } from 'node:path'; -import { reportUsage, type AgentLabEndpoint, type AgentLabModelRef, type AgentLabUsage } from '@weq/agentlab'; +import { reportUsage, pickMessageText, type AgentLabEndpoint, type AgentLabModelRef, type AgentLabUsage } from '@weq/agentlab'; import type { TokenUsageStore } from './agentlab_usage'; import type { ConversationStore, ConversationTurn } from './agentlab_conversation'; @@ -384,8 +384,8 @@ export class AssistantService { ], [], ); - const raw = data.choices?.[0]?.message?.content; - return cleanTitle(typeof raw === 'string' ? raw : ''); + const raw = pickMessageText(data.choices?.[0]?.message); + return cleanTitle(raw); } /** 核心多轮循环:返回最终文本。中途只 emit 过程 step,不 emit final。 */ @@ -424,7 +424,7 @@ export class AssistantService { const msg = data.choices?.[0]?.message; if (!msg) throw new Error('助手返回为空'); - const content = typeof msg.content === 'string' ? msg.content.trim() : ''; + const content = pickMessageText(msg); if (allowTools && msg.tool_calls?.length) { // 模型在调用工具前给出的思路 → 作为"思考"展示。