6db460eb89
Memind CI / Test, build, and release guards (push) Failing after 4m34s
When DeepSeek returns 402, try other chat providers before giving up, and cancel the sole active task only during LLM outages so user 123 can cancel without Goose. Co-authored-by: Cursor <cursoragent@cursor.com>
206 lines
6.2 KiB
JavaScript
206 lines
6.2 KiB
JavaScript
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' };
|
|
}
|
|
}
|