80 lines
3.2 KiB
JavaScript
80 lines
3.2 KiB
JavaScript
function contentText(payload) {
|
||
return payload?.content?.map((part) => part.text || '').join('') || ''
|
||
}
|
||
|
||
function parseJson(text) {
|
||
const cleaned = text.replace(/^```(?:json)?/i, '').replace(/```$/, '').trim()
|
||
return JSON.parse(cleaned)
|
||
}
|
||
|
||
export async function deepseek(system, user, jsonOrOptions = false) {
|
||
const options = typeof jsonOrOptions === 'boolean'
|
||
? { json: jsonOrOptions }
|
||
: { ...(jsonOrOptions || {}) }
|
||
const json = Boolean(options.json)
|
||
const apiKey = process.env.DEEPSEEK_API_KEY
|
||
if (!apiKey) throw Object.assign(new Error('DEEPSEEK_API_KEY 尚未配置'), { status: 503 })
|
||
const endpoint = `${(process.env.DEEPSEEK_BASE_URL || 'https://api.deepseek.com/anthropic').replace(/\/$/, '')}/v1/messages`
|
||
const attempts = json ? 2 : 1
|
||
let prompt = user
|
||
let lastError
|
||
|
||
for (let attempt = 0; attempt < attempts; attempt += 1) {
|
||
const controller = new AbortController()
|
||
const timeout = setTimeout(
|
||
() => controller.abort(),
|
||
Number(options.timeoutMs || process.env.AI_TIMEOUT_MS || 30000),
|
||
)
|
||
try {
|
||
const response = await fetch(endpoint, {
|
||
method: 'POST',
|
||
signal: controller.signal,
|
||
headers: {
|
||
'content-type': 'application/json',
|
||
'x-api-key': apiKey,
|
||
'anthropic-version': '2023-06-01',
|
||
},
|
||
body: JSON.stringify({
|
||
model: process.env.DEEPSEEK_MODEL || 'deepseek-v4-flash',
|
||
max_tokens: Number(options.maxTokens || process.env.AI_MAX_TOKENS || 4096),
|
||
temperature: options.temperature ?? (json ? 0 : 0.3),
|
||
thinking: options.thinking === true
|
||
? { type: 'enabled' }
|
||
: { type: 'disabled' },
|
||
system,
|
||
messages: [{ role: 'user', content: [{ type: 'text', text: prompt }] }],
|
||
}),
|
||
})
|
||
const body = await response.json()
|
||
if (!response.ok) {
|
||
throw Object.assign(new Error(body?.error?.message || `AI 请求失败 (${response.status})`), { status: 502 })
|
||
}
|
||
const text = contentText(body)
|
||
if (!json) return text
|
||
try {
|
||
return parseJson(text)
|
||
} catch (error) {
|
||
lastError = error
|
||
prompt = `${user}\n\n上一次响应不是合法 JSON。请重新生成完整结果:只允许输出一个可被 JSON.parse 直接解析的 JSON 值;不得使用 Markdown;字符串中的双引号、换行和反斜杠必须正确转义。`
|
||
}
|
||
} catch (error) {
|
||
if (error?.name === 'AbortError') {
|
||
throw Object.assign(new Error('AI 响应超时,请稍后重试'), { status: 504 })
|
||
}
|
||
throw error
|
||
} finally {
|
||
clearTimeout(timeout)
|
||
}
|
||
}
|
||
throw Object.assign(new Error('AI 返回格式异常,自动重试后仍无法解析'), { status: 502, cause: lastError })
|
||
}
|
||
|
||
export function conversationContext(messages, limit = 30) {
|
||
return messages.slice(-limit).map((m) => {
|
||
const role = m.sender === 'agent' ? '我方' : m.sender === 'system' ? '系统' : '对方'
|
||
const content = m.originalText || m.attachmentName || `[${m.type || '消息'}]`
|
||
const translation = m.translatedText && m.translatedText !== content ? `(译文:${m.translatedText})` : ''
|
||
return `${m.createdAt || ''} ${role}: ${content}${translation}`
|
||
}).join('\n')
|
||
}
|