Skip to content

Commit 68dcd1b

Browse files
committed
fix(ai): AI handler 增加 IPC 入参校验与宽松 JSON 解析
- 全部 AI handler 首行以 requireText/requireArray 校验入参,缺参/错型前移为带字段名的可诊断错误,避免主进程裸 TypeError 与无效云端请求 - 新增 jsonParse.ts 宽松解析(直接解析→围栏提取→大括号截取三级策略),本地小模型输出围栏/解释文字时不再误降级到云端,守住本地优先承诺
1 parent 3cc1f91 commit 68dcd1b

16 files changed

Lines changed: 177 additions & 42 deletions

‎client/electron/ai/handlers/anchorPointHandler.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* @ai-context: 记忆锚点生成 IPC handler——AIFeatureDef 注册表模式,经 callWithLocalFallback 支持本地 Ollama 优先/云端网关降级;请求响应契约与网关 Pydantic model 对齐。
77
*/
88

9-
import { safeHandle } from '../../ipcUtils.js';
9+
import { requireText, safeHandle } from '../../ipcUtils.js';
1010
import { logger } from '../../logger.js';
11-
import { callWithLocalFallback, gatewayUrl, type AIFeatureDef } from '../utils.js';
11+
import { callWithLocalFallback, gatewayUrl, parseModelJson, type AIFeatureDef } from '../utils.js';
1212
import { generateText } from '../ollama/OllamaProvider.js';
1313

1414
// ================================================================
@@ -29,6 +29,7 @@ function register(): void {
2929
authToken?: string;
3030
},
3131
) => {
32+
requireText(args?.content, 'content');
3233
const startMs = Date.now();
3334
logger.info(`[AI] [anchor-point] IPC received: content_length=${args.content.length}, title=${args.title ?? '(empty)'}, hasAuth=${!!args.authToken}`);
3435
logger.debug(`[AI] [anchor-point] Content preview: ${args.content.slice(0, 80)}...`);
@@ -58,7 +59,8 @@ function register(): void {
5859
const localHandler = async (): Promise<AnchorPointGenResp> => {
5960
const prompt = `从以下笔记中生成记忆锚点,返回JSON: {"anchor_points": [{"concept": "...", "association": "...", "memory_technique": "...", "importance": 0.8}], "status": "ok"}\n\n笔记:\n${args.content}`;
6061
const result = await generateText(prompt, '你是一个记忆锚点生成助手。请仅返回JSON。', { temperature: 0.6, maxTokens: 1024 });
61-
const parsed = JSON.parse(result.content);
62+
// 宽松解析:本地小模型常输出围栏/解释文字,裸 parse 会误降级到云端
63+
const parsed = parseModelJson<Partial<AnchorPointGenResp>>(result.content, {});
6264
return { anchor_points: parsed.anchor_points ?? [], status: 'ok', model: result.model, tokens_used: result.tokens_used, latency_ms: result.latency_ms };
6365
};
6466

‎client/electron/ai/handlers/conceptPrecheckHandler.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* @ai-context: 概念预检 IPC handler——AIFeatureDef 注册表模式,经 callWithLocalFallback 支持本地 Ollama 优先/云端网关降级;本地降级同样走 JSON 生成。
88
*/
99

10-
import { safeHandle } from '../../ipcUtils.js';
10+
import { requireText, safeHandle } from '../../ipcUtils.js';
1111
import { logger } from '../../logger.js';
12-
import { callWithLocalFallback, gatewayUrl, type AIFeatureDef } from '../utils.js';
12+
import { callWithLocalFallback, gatewayUrl, parseModelJson, type AIFeatureDef } from '../utils.js';
1313
import { generateText } from '../ollama/OllamaProvider.js';
1414

1515
// ================================================================
@@ -30,6 +30,7 @@ function register(): void {
3030
authToken?: string;
3131
},
3232
) => {
33+
requireText(args?.concept, 'concept');
3334
const startMs = Date.now();
3435
logger.info(`[AI] [conceptPrecheck] IPC received: concept=${args.concept.slice(0, 50)}, weak_len=${(args.weakHistory ?? '').length}, hasAuth=${!!args.authToken}`);
3536

@@ -54,7 +55,8 @@ function register(): void {
5455
const localHandler = async (): Promise<ConceptPrecheckResp> => {
5556
const prompt = `针对概念「${reqBody.concept}」设计 1-2 个探测性问题,暴露学习者常见误解(非知识记忆考察),仅返回JSON: {"questions": [{"question": "...", "intent": "..."}]}\n\n【学习者历史薄弱点】\n${reqBody.weakHistory || '(暂无)'}`;
5657
const result = await generateText(prompt, '你是一位善于发现学习误区的热心教练。请仅返回JSON。', { temperature: 0.5, maxTokens: 1000 });
57-
const parsed = JSON.parse(result.content);
58+
// 宽松解析:本地小模型常输出围栏/解释文字,裸 parse 会误降级到云端
59+
const parsed = parseModelJson<Partial<ConceptPrecheckResp>>(result.content, {});
5860
return {
5961
questions: parsed.questions ?? [],
6062
model: result.model,

‎client/electron/ai/handlers/conflictDetectHandler.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* @ai-context: 概念冲突检测 IPC handler——AIFeatureDef 注册表模式,经 callWithLocalFallback 支持本地 Ollama 优先/云端网关降级;本地降级同样走 JSON 生成。
88
*/
99

10-
import { safeHandle } from '../../ipcUtils.js';
10+
import { requireText, safeHandle } from '../../ipcUtils.js';
1111
import { logger } from '../../logger.js';
12-
import { callWithLocalFallback, gatewayUrl, type AIFeatureDef } from '../utils.js';
12+
import { callWithLocalFallback, gatewayUrl, parseModelJson, type AIFeatureDef } from '../utils.js';
1313
import { generateText } from '../ollama/OllamaProvider.js';
1414

1515
// ================================================================
@@ -30,6 +30,8 @@ function register(): void {
3030
authToken?: string;
3131
},
3232
) => {
33+
requireText(args?.newNoteText, 'newNoteText');
34+
requireText(args?.historyText, 'historyText');
3335
const startMs = Date.now();
3436
logger.info(`[AI] [conflictDetect] IPC received: new_len=${args.newNoteText.length}, history_len=${args.historyText.length}, hasAuth=${!!args.authToken}`);
3537

@@ -56,7 +58,8 @@ function register(): void {
5658
const localHandler = async (): Promise<ConflictDetectResp> => {
5759
const prompt = `分析新笔记与历史理解之间的概念冲突(只报告真正的矛盾),仅返回JSON: {"conflicts": [{"old_claim": "...", "new_claim": "...", "topic": "...", "suggestion": "..."}]}\n\n【新笔记】\n${reqBody.newNoteText}\n\n【历史理解】\n${reqBody.historyText}`;
5860
const result = await generateText(prompt, '你是一位概念转变研究专家,擅长识别学习者新旧理解之间的矛盾。请仅返回JSON。', { temperature: 0.3, maxTokens: 2000 });
59-
const parsed = JSON.parse(result.content);
61+
// 宽松解析:本地小模型常输出围栏/解释文字,裸 parse 会误降级到云端
62+
const parsed = parseModelJson<Partial<ConflictDetectResp>>(result.content, {});
6063
return {
6164
conflicts: parsed.conflicts ?? [],
6265
model: result.model,

‎client/electron/ai/handlers/contentTierHandler.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* @ai-context: 内容分层 IPC handler——AIFeatureDef 注册表模式,经 callWithLocalFallback 支持本地 Ollama 优先/云端网关降级;本地降级同样走 JSON 生成。
88
*/
99

10-
import { safeHandle } from '../../ipcUtils.js';
10+
import { requireText, safeHandle } from '../../ipcUtils.js';
1111
import { logger } from '../../logger.js';
12-
import { callWithLocalFallback, gatewayUrl, type AIFeatureDef } from '../utils.js';
12+
import { callWithLocalFallback, gatewayUrl, parseModelJson, type AIFeatureDef } from '../utils.js';
1313
import { generateText } from '../ollama/OllamaProvider.js';
1414

1515
// ================================================================
@@ -29,6 +29,7 @@ function register(): void {
2929
authToken?: string;
3030
},
3131
) => {
32+
requireText(args?.notesText, 'notesText');
3233
const startMs = Date.now();
3334
logger.info(`[AI] [contentTier] IPC received: text_len=${args.notesText.length}, hasAuth=${!!args.authToken}`);
3435

@@ -52,7 +53,8 @@ function register(): void {
5253
const localHandler = async (): Promise<ContentTierResp> => {
5354
const prompt = `将以下笔记内容分为三层:core(核心概念,含 reason)、support(支撑材料)、detail(参考细节),仅返回JSON: {"core": [{"text": "...", "reason": "..."}], "support": [{"text": "..."}], "detail": [{"text": "..."}]}\n\n${reqBody.notesText}`;
5455
const result = await generateText(prompt, '你是一位认知负荷管理专家,擅长从笔记中提炼核心概念。请仅返回JSON。', { temperature: 0.3, maxTokens: 2500 });
55-
const parsed = JSON.parse(result.content);
56+
// 宽松解析:本地小模型常输出围栏/解释文字,裸 parse 会误降级到云端
57+
const parsed = parseModelJson<Partial<ContentTierResp>>(result.content, {});
5658
return {
5759
core: parsed.core ?? [],
5860
support: parsed.support ?? [],

‎client/electron/ai/handlers/errorPatternHandler.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* @ai-context: 错误模式分析 IPC handler——AIFeatureDef 注册表模式,经 callWithLocalFallback 支持本地 Ollama 优先/云端网关降级;本地降级返回空模式列表(前端回退纯本地统计)。
88
*/
99

10-
import { safeHandle } from '../../ipcUtils.js';
10+
import { requireArray, safeHandle } from '../../ipcUtils.js';
1111
import { logger } from '../../logger.js';
12-
import { callWithLocalFallback, gatewayUrl, type AIFeatureDef } from '../utils.js';
12+
import { callWithLocalFallback, gatewayUrl, parseModelJson, type AIFeatureDef } from '../utils.js';
1313
import { generateText } from '../ollama/OllamaProvider.js';
1414

1515
// ================================================================
@@ -29,6 +29,7 @@ function register(): void {
2929
authToken?: string;
3030
},
3131
) => {
32+
requireArray(args?.goldenErrors, 'goldenErrors');
3233
const startMs = Date.now();
3334
logger.info(`[AI] [errorPattern] IPC received: errors_count=${args.goldenErrors.length}, hasAuth=${!!args.authToken}`);
3435

@@ -57,7 +58,8 @@ function register(): void {
5758
.join('\n');
5859
const prompt = `分析以下黄金错误记录,识别错误模式,返回JSON: {"patterns": [{"type": "concept_blind|concept_confusion|overconfidence", "keywords": ["..."], "explanation": "...", "suggestion": "..."}], "top_offenders": [{"flashcardId": "...", "count": 1}], "summary": "不超过50字总结"}\n\n${errorsText}`;
5960
const result = await generateText(prompt, '你是一个教育心理学专家,擅长识别学习者的典型错误模式。请仅返回JSON。', { temperature: 0.3, maxTokens: 2048 });
60-
const parsed = JSON.parse(result.content);
61+
// 宽松解析:本地小模型常输出围栏/解释文字,裸 parse 会误降级到云端
62+
const parsed = parseModelJson<Partial<ErrorPatternResp>>(result.content, {});
6163
return {
6264
patterns: parsed.patterns ?? [],
6365
top_offenders: parsed.top_offenders ?? [],

‎client/electron/ai/handlers/feynmanHandler.ts‎

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
* @ai-context: 费曼提问与答案评估 IPC handler——AIFeatureDef 注册表模式,经 callWithLocalFallback 支持本地 Ollama 优先/云端网关降级;请求响应契约与网关 Pydantic model 对齐。
88
*/
99

10-
import { safeHandle } from '../../ipcUtils.js';
10+
import { requireArray, requireText, safeHandle } from '../../ipcUtils.js';
1111
import { logger } from '../../logger.js';
12-
import { callWithLocalFallback, gatewayUrl, type AIFeatureDef } from '../utils.js';
12+
import { callWithLocalFallback, gatewayUrl, parseModelJson, type AIFeatureDef } from '../utils.js';
1313
import { generateText } from '../ollama/OllamaProvider.js';
1414

1515
// ================================================================
@@ -33,6 +33,8 @@ function register(): void {
3333
authToken?: string;
3434
},
3535
) => {
36+
requireText(args?.concept, 'concept');
37+
requireText(args?.explanation, 'explanation');
3638
const startMs = Date.now();
3739
logger.info(`[AI] [feynman-q] IPC received: concept_length=${args.concept.length}, explanation_length=${args.explanation.length}, hasAuth=${!!args.authToken}`);
3840
logger.debug(`[AI] [feynman-q] Concept: ${args.concept.slice(0, 60)}`);
@@ -55,7 +57,8 @@ function register(): void {
5557
const localHandler = async (): Promise<FeynmanQuestionResp> => {
5658
const prompt = `针对概念“${args.concept}”和解释“${args.explanation}”,生成苏格拉底式追问,返回JSON: {"questions": [{"question": "...", "focus": "..."}]}`;
5759
const result = await generateText(prompt, '你是一个费曼学习法追问助手。请仅返回JSON。', { temperature: 0.7, maxTokens: 1024 });
58-
const parsed = JSON.parse(result.content);
60+
// 宽松解析:本地小模型常输出围栏/解释文字,裸 parse 会误降级到云端
61+
const parsed = parseModelJson<Partial<FeynmanQuestionResp>>(result.content, {});
5962
return { questions: parsed.questions ?? [], model: result.model, tokens_used: result.tokens_used, latency_ms: result.latency_ms };
6063
};
6164

@@ -104,6 +107,9 @@ function register(): void {
104107
authToken?: string;
105108
},
106109
) => {
110+
requireText(args?.concept, 'concept');
111+
requireArray(args?.questions, 'questions');
112+
requireArray(args?.answers, 'answers');
107113
const startMs = Date.now();
108114
logger.info(`[AI] [feynman-eval] IPC received: concept_length=${args.concept.length}, questions=${args.questions.length}, answers=${args.answers.length}, hasAuth=${!!args.authToken}`);
109115
logger.debug(`[AI] [feynman-eval] Concept: ${args.concept.slice(0, 60)}, Q count=${args.questions.length}`);
@@ -130,7 +136,8 @@ function register(): void {
130136
const localHandler = async (): Promise<FeynmanAnswerEvalResp> => {
131137
const prompt = `评估用户对概念“${args.concept}”的回答,返回JSON: {"understanding_score": 70, "feedback": "...", "strong_points": ["..."], "weak_points": ["..."]}`;
132138
const result = await generateText(prompt, '你是一个费曼学习法评估助手。请仅返回JSON。', { temperature: 0.4, maxTokens: 1024 });
133-
const parsed = JSON.parse(result.content);
139+
// 宽松解析:本地小模型常输出围栏/解释文字,裸 parse 会误降级到云端
140+
const parsed = parseModelJson<Partial<FeynmanAnswerEvalResp>>(result.content, {});
134141
return { understanding_score: parsed.understanding_score ?? 60, feedback: parsed.feedback ?? '', strong_points: parsed.strong_points ?? [], weak_points: parsed.weak_points ?? [], model: result.model, tokens_used: result.tokens_used, latency_ms: result.latency_ms };
135142
};
136143

‎client/electron/ai/handlers/optimizeCardHandler.ts‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* @ai-context: 卡片优化 IPC handler——AIFeatureDef 注册表模式,经 callWithLocalFallback 支持本地 Ollama 优先/云端网关降级;请求响应契约与网关 Pydantic model 对齐。
77
*/
88

9-
import { safeHandle } from '../../ipcUtils.js';
9+
import { requireText, safeHandle } from '../../ipcUtils.js';
1010
import { logger } from '../../logger.js';
11-
import { callWithLocalFallback, gatewayUrl, type AIFeatureDef } from '../utils.js';
11+
import { callWithLocalFallback, gatewayUrl, parseModelJson, type AIFeatureDef } from '../utils.js';
1212
import { generateText } from '../ollama/OllamaProvider.js';
1313

1414
// ================================================================
@@ -29,6 +29,8 @@ function register(): void {
2929
authToken?: string;
3030
},
3131
) => {
32+
requireText(args?.front, 'front');
33+
requireText(args?.back, 'back');
3234
const startMs = Date.now();
3335
logger.info(`[AI] [optimize-card] IPC received: front_length=${args.front.length}, back_length=${args.back.length}, hasAuth=${!!args.authToken}`);
3436
logger.debug(`[AI] [optimize-card] Front preview: ${args.front.slice(0, 60)}, Back preview: ${args.back.slice(0, 60)}`);
@@ -55,7 +57,8 @@ function register(): void {
5557
正面:${args.front}
5658
反面:${args.back}`;
5759
const result = await generateText(prompt, '你是一个闪卡优化助手,擅长改进问答对的表述。请仅返回JSON。', { temperature: 0.5, maxTokens: 1024 });
58-
const parsed = JSON.parse(result.content);
60+
// 宽松解析:本地小模型常输出围栏/解释文字,裸 parse 会误降级到云端
61+
const parsed = parseModelJson<Partial<OptimizeCardResp>>(result.content, {});
5962
return { suggested_front: parsed.suggested_front ?? args.front, suggested_back: parsed.suggested_back ?? args.back, improvements: parsed.improvements ?? [], model: result.model, tokens_used: result.tokens_used, latency_ms: result.latency_ms };
6063
};
6164

‎client/electron/ai/handlers/predictHandler.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66
* @ai-context: 预测提问 IPC handler——AIFeatureDef 注册表模式,经 callWithLocalFallback 支持本地 Ollama 优先/云端网关降级;请求响应契约与网关 Pydantic model 对齐。
77
*/
88

9-
import { safeHandle } from '../../ipcUtils.js';
9+
import { requireText, safeHandle } from '../../ipcUtils.js';
1010
import { logger } from '../../logger.js';
11-
import { callWithLocalFallback, gatewayUrl, type AIFeatureDef } from '../utils.js';
11+
import { callWithLocalFallback, gatewayUrl, parseModelJson, type AIFeatureDef } from '../utils.js';
1212
import { generateText } from '../ollama/OllamaProvider.js';
1313

1414
// ================================================================
@@ -28,6 +28,7 @@ function register(): void {
2828
authToken?: string;
2929
},
3030
) => {
31+
requireText(args?.content, 'content');
3132
const startMs = Date.now();
3233
logger.info(`[AI] [predict] IPC received: content_length=${args.content.length}, hasAuth=${!!args.authToken}`);
3334
logger.debug(`[AI] [predict] Content preview: ${args.content.slice(0, 80)}...`);
@@ -54,7 +55,8 @@ function register(): void {
5455
const localHandler = async (): Promise<PredictGenResp> => {
5556
const prompt = `基于以下笔记内容,生成预测性问题,返回JSON: {"predictions": [{"question": "...", "type": "...", "reason": "...", "curiosity_score": 0.8}], "status": "ok"}\n\n笔记:\n${args.content}`;
5657
const result = await generateText(prompt, '你是一个预测驱动学习助手,擅长从笔记中生成引导性问题。请仅返回JSON。', { temperature: 0.6, maxTokens: 1024 });
57-
const parsed = JSON.parse(result.content);
58+
// 宽松解析:本地小模型常输出围栏/解释文字,裸 parse 会误降级到云端
59+
const parsed = parseModelJson<Partial<PredictGenResp>>(result.content, {});
5860
return { predictions: parsed.predictions ?? [], status: 'ok', model: result.model, tokens_used: result.tokens_used, latency_ms: result.latency_ms };
5961
};
6062

‎client/electron/ai/handlers/progressNarratorHandler.ts‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
* 请求响应契约与网关 progress_narrative 路由的 Pydantic model 对齐。
1010
*/
1111

12-
import { safeHandle } from '../../ipcUtils.js';
12+
import { requireText, safeHandle } from '../../ipcUtils.js';
1313
import { logger } from '../../logger.js';
14-
import { callWithLocalFallback, gatewayUrl, type AIFeatureDef } from '../utils.js';
14+
import { callWithLocalFallback, gatewayUrl, parseModelJson, type AIFeatureDef } from '../utils.js';
1515
import { generateText } from '../ollama/OllamaProvider.js';
1616

1717
// ================================================================
@@ -31,6 +31,7 @@ function register(): void {
3131
authToken?: string;
3232
},
3333
) => {
34+
requireText(args?.statsText, 'statsText');
3435
const startMs = Date.now();
3536
logger.info(`[AI] [progress-narrative] IPC received: stats_length=${args.statsText?.length ?? 0}, hasAuth=${!!args.authToken}`);
3637

@@ -54,7 +55,8 @@ function register(): void {
5455
const localHandler = async (): Promise<NarrativeGenResp> => {
5556
const prompt = `以下是用户本周的学习统计:\n${reqBody.stats_text}\n\n请把这段统计写成一句温暖、具体的微进展叙述(不超过两句话,正向语言),返回JSON: {"narrative": "..."}`;
5657
const result = await generateText(prompt, '你是一位温暖的学习教练,擅长把学习统计讲述成看得见的进步。请仅返回JSON。', { temperature: 0.7, maxTokens: 256 });
57-
const parsed = JSON.parse(result.content);
58+
// 宽松解析:本地小模型常输出围栏/解释文字,裸 parse 会误降级到云端
59+
const parsed = parseModelJson<Partial<NarrativeGenResp>>(result.content, {});
5860
return { narrative: parsed.narrative ?? '', status: 'success', model: result.model, tokens_used: result.tokens_used, latency_ms: result.latency_ms };
5961
};
6062

0 commit comments

Comments
 (0)