function parseJsonReply(reply) { const text = String(reply ?? '').trim(); if (!text) return null; const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i); const candidate = fenced?.[1] ?? text; try { return JSON.parse(candidate); } catch { return null; } } const IMAGE_ONLY_MODEL_HINTS = /(?:image|ocr|vision|vl-ocr|文生图|图生文)/iu; export function isLlmProviderUnavailable(result) { if (result?.ok) return false; if (Number(result?.status) === 402 || Number(result?.status) === 429) return true; const message = String(result?.message ?? '').toLowerCase(); return /insufficient balance|余额不足|quota|rate limit|too many requests/u.test(message); } function isLikelyChatProviderKey(key) { if (!key || key.status === 'disabled') return false; const label = `${key.name ?? ''} ${key.defaultModel ?? ''} ${(key.models ?? []).join(' ')}`; return !IMAGE_ONLY_MODEL_HINTS.test(label); } function buildManageLlmMessages(text, taskList) { return [ { role: 'system', content: [ '你是微信服务号定时自动任务管理助手,只做任务选择,不做页面生成。', '输入包含用户原话和当前 active 定时任务列表(JSON)。', '请严格输出 JSON,不要输出解释。', '允许 action:', '- cancel:用户明确要取消某个任务,且能从列表中唯一确定 taskId', '- clarify:用户想取消但无法唯一确定是哪一个,message 用中文简短追问', '- none:不是取消任务,或信息不足且不应猜测', 'cancel 时必须返回列表里存在的 taskId;禁止编造 taskId。', '如果只有一条任务且用户明确要取消/停止定时任务,可以返回该 taskId。', '如果用户在要求生成/修改 HTML 页面,而不是管理已有定时任务,返回 none。', ].join('\n'), }, { role: 'user', content: JSON.stringify({ userMessage: String(text ?? '').trim(), activeTasks: taskList, }), }, ]; } async function createManageLlmChatCompletion({ llmProviderService, modelProviderKeyId = null, model = null, messages, logger = console, } = {}) { const candidates = []; const seen = new Set(); const pushCandidate = (providerKeyId, candidateModel) => { const key = providerKeyId ?? '__default__'; if (seen.has(key)) return; seen.add(key); candidates.push({ providerKeyId, model: candidateModel ?? model ?? null, }); }; pushCandidate(modelProviderKeyId, model); pushCandidate(null, model); if (typeof llmProviderService.listKeys === 'function') { try { const keys = await llmProviderService.listKeys(); for (const key of keys ?? []) { if (!isLikelyChatProviderKey(key)) continue; pushCandidate(key.id, model ?? key.defaultModel ?? null); } } catch (err) { logger?.warn?.( '[wechat-scheduled-task-manage-llm] provider list failed:', err instanceof Error ? err.message : err, ); } } let lastResult = null; let sawUnavailable = false; for (const candidate of candidates) { const result = await llmProviderService.createChatCompletion({ ...(candidate.providerKeyId ? { providerKeyId: candidate.providerKeyId } : {}), ...(candidate.model ? { model: candidate.model } : {}), temperature: 0, messages, }); lastResult = result; if (result?.ok) return result; if (isLlmProviderUnavailable(result)) { sawUnavailable = true; logger?.warn?.( '[wechat-scheduled-task-manage-llm] provider unavailable:', candidate.providerKeyId ?? 'default', result?.message ?? 'unknown', ); continue; } logger?.warn?.( '[wechat-scheduled-task-manage-llm] provider failed:', candidate.providerKeyId ?? 'default', result?.message ?? 'unknown', ); } return { ...(lastResult ?? { ok: false, message: '聊天模型不可用' }), llmUnavailable: sawUnavailable, }; } export function buildScheduledTaskListPayload(tasks = []) { return tasks.map((task) => ({ taskId: task.id, title: task.title, recurrence: task.recurrence, hour: task.hour, minute: task.minute ?? 0, taskSpec: String(task.taskSpec ?? '').slice(0, 240), })); } export function normalizeScheduledTaskManageLlmDecision(payload, tasks = []) { const action = String(payload?.action ?? 'none').trim(); const taskIds = new Set(tasks.map((task) => String(task.id ?? '').trim()).filter(Boolean)); if (action === 'clarify') { const message = String(payload?.message ?? '').trim() || '我找到多个定时任务,请告诉我要取消哪一个。'; return { action: 'clarify', message }; } if (action === 'cancel') { const taskId = String(payload?.taskId ?? '').trim(); if (taskId && taskIds.has(taskId)) { return { action: 'cancel', taskId }; } return { action: 'none' }; } return { action: 'none' }; } export async function resolveScheduledTaskCancelWithLlm({ text, tasks = [], llmProviderService, modelProviderKeyId = null, model = null, logger = console, } = {}) { if (!llmProviderService || typeof llmProviderService.createChatCompletion !== 'function') { return { action: 'none' }; } if (!Array.isArray(tasks) || tasks.length === 0) { return { action: 'none' }; } const taskList = buildScheduledTaskListPayload(tasks); const messages = buildManageLlmMessages(text, taskList); try { const result = await createManageLlmChatCompletion({ llmProviderService, modelProviderKeyId, model, messages, logger, }); if (!result?.ok) { logger?.warn?.( '[wechat-scheduled-task-manage-llm] parse skipped:', result?.message ?? 'unknown', ); return { action: 'none', ...(result?.llmUnavailable ? { llmUnavailable: true } : {}), }; } return normalizeScheduledTaskManageLlmDecision(parseJsonReply(result.reply), tasks); } catch (err) { logger?.warn?.( '[wechat-scheduled-task-manage-llm] parse skipped:', err instanceof Error ? err.message : err, ); return { action: 'none' }; } }