fix(wechat): stop relay bootstrap from hijacking LLM provider selection.
Memind CI / Test, build, and release guards (push) Failing after 2s
Memind CI / Test, build, and release guards (push) Failing after 2s
Schedule LLM now binds an explicit provider key and fails closed on network errors so dead relay endpoints cannot block ordinary chat; relay bootstrap is opt-in only. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -60,40 +60,50 @@ function normalizeLlmScheduleIntent(payload) {
|
||||
return { action: 'none' };
|
||||
}
|
||||
|
||||
async function parseScheduleIntentWithLlm({ text, llmProviderService, logger = console }) {
|
||||
async function parseScheduleIntentWithLlm({ text, llmProviderService, modelProviderKeyId = null, model = null, logger = console }) {
|
||||
if (!llmProviderService || typeof llmProviderService.createChatCompletion !== 'function') {
|
||||
return { action: 'none' };
|
||||
}
|
||||
|
||||
const result = await llmProviderService.createChatCompletion({
|
||||
temperature: 0,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: [
|
||||
'你是公众号提醒助手,只做意图识别。',
|
||||
'请严格输出 JSON,不要输出解释。',
|
||||
'允许 action: create_todo, create_daily_todo_digest, create_balance_alert, query_schedule, schedule_agent, none。',
|
||||
'create_todo 需要 title。',
|
||||
'create_daily_todo_digest 需要 hour(0-23) 和 minute(0-59)。',
|
||||
'create_balance_alert 需要 thresholdYuan 数字。',
|
||||
'如果用户在说具体时间提醒、一次性闹钟、某天某时提醒,返回 schedule_agent。',
|
||||
'不确定时返回 none。',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
});
|
||||
try {
|
||||
const result = await llmProviderService.createChatCompletion({
|
||||
...(modelProviderKeyId ? { providerKeyId: modelProviderKeyId } : {}),
|
||||
...(model ? { model } : {}),
|
||||
temperature: 0,
|
||||
messages: [
|
||||
{
|
||||
role: 'system',
|
||||
content: [
|
||||
'你是公众号提醒助手,只做意图识别。',
|
||||
'请严格输出 JSON,不要输出解释。',
|
||||
'允许 action: create_todo, create_daily_todo_digest, create_balance_alert, query_schedule, schedule_agent, none。',
|
||||
'create_todo 需要 title。',
|
||||
'create_daily_todo_digest 需要 hour(0-23) 和 minute(0-59)。',
|
||||
'create_balance_alert 需要 thresholdYuan 数字。',
|
||||
'如果用户在说具体时间提醒、一次性闹钟、某天某时提醒,返回 schedule_agent。',
|
||||
'不确定时返回 none。',
|
||||
].join('\n'),
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
if (!result?.ok) {
|
||||
logger?.warn?.('[wechat-schedule-llm] parse skipped:', result?.message ?? 'unknown');
|
||||
if (!result?.ok) {
|
||||
logger?.warn?.('[wechat-schedule-llm] parse skipped:', result?.message ?? 'unknown');
|
||||
return { action: 'none' };
|
||||
}
|
||||
|
||||
return normalizeLlmScheduleIntent(parseJsonReply(result.reply));
|
||||
} catch (err) {
|
||||
logger?.warn?.(
|
||||
'[wechat-schedule-llm] parse skipped:',
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
return { action: 'none' };
|
||||
}
|
||||
|
||||
return normalizeLlmScheduleIntent(parseJsonReply(result.reply));
|
||||
}
|
||||
|
||||
async function resolveScheduleIntent({
|
||||
@@ -118,7 +128,26 @@ async function resolveScheduleIntent({
|
||||
}
|
||||
if (!enabled) return ruleIntent;
|
||||
|
||||
return parseScheduleIntentWithLlm({ text, llmProviderService, logger });
|
||||
let modelProviderKeyId = null;
|
||||
let model = null;
|
||||
try {
|
||||
const config = await wechatScheduleLlmConfigService.getConfig();
|
||||
modelProviderKeyId = config.modelProviderKeyId ?? null;
|
||||
model = config.model ?? null;
|
||||
} catch (err) {
|
||||
logger?.warn?.(
|
||||
'[wechat-schedule-llm] provider config load failed:',
|
||||
err instanceof Error ? err.message : err,
|
||||
);
|
||||
}
|
||||
|
||||
return parseScheduleIntentWithLlm({
|
||||
text,
|
||||
llmProviderService,
|
||||
modelProviderKeyId,
|
||||
model,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
export async function handleWechatScheduleIntent({
|
||||
|
||||
@@ -19,13 +19,28 @@ function createScheduleService() {
|
||||
};
|
||||
}
|
||||
|
||||
function createScheduleLlmConfigService(enabled = true) {
|
||||
return {
|
||||
async isScheduleLlmEnabled() {
|
||||
return enabled;
|
||||
},
|
||||
async getConfig() {
|
||||
return {
|
||||
scheduleLlmEnabled: enabled,
|
||||
modelProviderKeyId: 'deepseek-key',
|
||||
model: 'deepseek-v4-pro',
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
test('schedule handler keeps rule-based parsing as first priority', async () => {
|
||||
let llmCalled = false;
|
||||
const reply = await handleWechatScheduleIntent({
|
||||
intent: { agentText: '帮我记一下 跟进合同', msgId: 'msg-1' },
|
||||
user: { userId: 'user-1' },
|
||||
scheduleService: createScheduleService(),
|
||||
wechatScheduleLlmConfigService: { async isScheduleLlmEnabled() { return true; } },
|
||||
wechatScheduleLlmConfigService: createScheduleLlmConfigService(true),
|
||||
llmProviderService: {
|
||||
async createChatCompletion() {
|
||||
llmCalled = true;
|
||||
@@ -42,7 +57,7 @@ test('schedule handler can use llm fallback for ambiguous todo text', async () =
|
||||
intent: { agentText: '下周把合同发给客户,别忘了', msgId: 'msg-2' },
|
||||
user: { userId: 'user-1' },
|
||||
scheduleService: createScheduleService(),
|
||||
wechatScheduleLlmConfigService: { async isScheduleLlmEnabled() { return true; } },
|
||||
wechatScheduleLlmConfigService: createScheduleLlmConfigService(true),
|
||||
llmProviderService: {
|
||||
async createChatCompletion() {
|
||||
return {
|
||||
@@ -71,7 +86,7 @@ test('schedule handler does not call llm when switch is off', async () => {
|
||||
intent: { agentText: '别忘了报销', msgId: 'msg-4' },
|
||||
user: { userId: 'user-1' },
|
||||
scheduleService: createScheduleService(),
|
||||
wechatScheduleLlmConfigService: { async isScheduleLlmEnabled() { return false; } },
|
||||
wechatScheduleLlmConfigService: createScheduleLlmConfigService(false),
|
||||
llmProviderService: {
|
||||
async createChatCompletion() {
|
||||
llmCalled = true;
|
||||
@@ -89,7 +104,7 @@ test('schedule handler falls back quietly when llm request fails', async () => {
|
||||
intent: { agentText: '别忘了报销', msgId: 'msg-5' },
|
||||
user: { userId: 'user-1' },
|
||||
scheduleService: createScheduleService(),
|
||||
wechatScheduleLlmConfigService: { async isScheduleLlmEnabled() { return true; } },
|
||||
wechatScheduleLlmConfigService: createScheduleLlmConfigService(true),
|
||||
llmProviderService: {
|
||||
async createChatCompletion() {
|
||||
return { ok: false, message: 'provider unavailable' };
|
||||
@@ -102,12 +117,31 @@ test('schedule handler falls back quietly when llm request fails', async () => {
|
||||
assert.match(warnings[0], /provider unavailable/);
|
||||
});
|
||||
|
||||
test('schedule handler falls back quietly when llm request throws', async () => {
|
||||
const warnings = [];
|
||||
const reply = await handleWechatScheduleIntent({
|
||||
intent: { agentText: '什么是机器学习', msgId: 'msg-6' },
|
||||
user: { userId: 'user-1' },
|
||||
scheduleService: createScheduleService(),
|
||||
wechatScheduleLlmConfigService: createScheduleLlmConfigService(true),
|
||||
llmProviderService: {
|
||||
async createChatCompletion() {
|
||||
throw new TypeError('fetch failed');
|
||||
},
|
||||
},
|
||||
logger: { warn: (...args) => warnings.push(args.join(' ')) },
|
||||
});
|
||||
assert.equal(reply, null);
|
||||
assert.equal(warnings.length, 1);
|
||||
assert.match(warnings[0], /fetch failed/);
|
||||
});
|
||||
|
||||
test('schedule handler falls through for llm schedule_agent results', async () => {
|
||||
const reply = await handleWechatScheduleIntent({
|
||||
intent: { agentText: '明天下午三点提醒我开会', msgId: 'msg-3' },
|
||||
user: { userId: 'user-1' },
|
||||
scheduleService: createScheduleService(),
|
||||
wechatScheduleLlmConfigService: { async isScheduleLlmEnabled() { return true; } },
|
||||
wechatScheduleLlmConfigService: createScheduleLlmConfigService(true),
|
||||
llmProviderService: {
|
||||
async createChatCompletion() {
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user