diff --git a/AGENTS.md b/AGENTS.md index 3e54038..e12a4f5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,7 +1,7 @@ # LEARN-LANGUAGES 知识库 **生成时间:** 2026-03-08 -**提交:** 91c59c3 +**提交:** 6ba5ae9 **分支:** dev ## 概述 @@ -112,6 +112,7 @@ log.error("Failed to fetch folders", { error }); - ❌ Server Component 可行时用 Client Component - ❌ npm 或 yarn (使用 pnpm) - ❌ 生产代码中使用 `console.log` (使用 winston logger) +- ❌ 擅自运行 `pnpm dev` (不需要,用 `pnpm build` 验证即可) ## 独特风格 diff --git a/src/lib/bigmodel/dictionary/orchestrator.ts b/src/lib/bigmodel/dictionary/orchestrator.ts index 782d154..fe57196 100644 --- a/src/lib/bigmodel/dictionary/orchestrator.ts +++ b/src/lib/bigmodel/dictionary/orchestrator.ts @@ -53,7 +53,7 @@ export async function executeDictionaryLookup( if (!standardFormResult.standardForm) { log.error("[Stage 3] Standard form is empty"); - throw "无法生成标准形式"; + throw new LookUpError("无法生成标准形式"); } log.debug("[Stage 3] Standard form complete", { standardFormResult }); diff --git a/src/lib/bigmodel/dictionary/stage1-inputAnalysis.ts b/src/lib/bigmodel/dictionary/stage1-inputAnalysis.ts index 409e0b3..fe028e3 100644 --- a/src/lib/bigmodel/dictionary/stage1-inputAnalysis.ts +++ b/src/lib/bigmodel/dictionary/stage1-inputAnalysis.ts @@ -55,6 +55,14 @@ export async function analyzeInput(text: string): Promise { throw new Error("阶段1:isValid 字段类型错误"); } + if (typeof result.isEmpty !== "boolean") { + throw new Error("阶段1:isEmpty 字段类型错误"); + } + + if (typeof result.isNaturalLanguage !== "boolean") { + throw new Error("阶段1:isNaturalLanguage 字段类型错误"); + } + // 确保 reason 字段存在 if (typeof result.reason !== "string") { result.reason = ""; diff --git a/src/lib/bigmodel/dictionary/stage2-semanticMapping.ts b/src/lib/bigmodel/dictionary/stage2-semanticMapping.ts index 7052b5b..263e719 100644 --- a/src/lib/bigmodel/dictionary/stage2-semanticMapping.ts +++ b/src/lib/bigmodel/dictionary/stage2-semanticMapping.ts @@ -9,7 +9,7 @@ const log = createLogger("dictionary-stage2"); * 阶段 2:跨语言语义映射决策 * * 独立的 LLM 调用,决定是否需要语义映射 - * 如果输入不符合"明确、基础、可词典化的语义概念"且语言不一致,直接返回失败 + * 如果输入不符合"明确、基础、可词典化的语义概念"且语言不一致,则降级使用原始输入 */ export async function determineSemanticMapping( @@ -86,9 +86,17 @@ b) 输入是明确、基础、可词典化的语义概念 result.reason = ""; } - // 如果不应该映射,返回错误 + // 如果不应该映射,返回降级结果(不抛出错误) + // 这样可以让后续阶段使用原始输入继续处理 if (!result.shouldMap) { - throw new Error(result.reason || "输入不符合可词典化的语义概念,无法进行跨语言查询"); + log.debug("Semantic mapping not applicable, using original input", { + reason: result.reason + }); + return { + shouldMap: false, + canMap: result.canMap ?? false, + reason: result.reason, + }; } if (!result.mappedQuery || result.mappedQuery.trim().length === 0) { @@ -97,6 +105,7 @@ b) 输入是明确、基础、可词典化的语义概念 return { shouldMap: result.shouldMap, + canMap: result.canMap ?? true, coreSemantic: result.coreSemantic, mappedQuery: result.mappedQuery, reason: result.reason, diff --git a/src/lib/bigmodel/dictionary/types.ts b/src/lib/bigmodel/dictionary/types.ts index 38a8bdb..e94eef2 100644 --- a/src/lib/bigmodel/dictionary/types.ts +++ b/src/lib/bigmodel/dictionary/types.ts @@ -20,6 +20,7 @@ export interface InputAnalysisResult { // 阶段2:语义映射结果 export interface SemanticMappingResult { shouldMap: boolean; + canMap?: boolean; coreSemantic?: string; mappedQuery?: string; reason: string; diff --git a/src/lib/bigmodel/zhipu.ts b/src/lib/bigmodel/zhipu.ts index e2eb659..e617750 100644 --- a/src/lib/bigmodel/zhipu.ts +++ b/src/lib/bigmodel/zhipu.ts @@ -2,10 +2,15 @@ type Messages = { role: string; content: string; }[]; +const LLM_TIMEOUT_MS = 30000; + async function callZhipuAPI( messages: Messages, model = process.env.ZHIPU_MODEL_NAME, ) { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), LLM_TIMEOUT_MS); + const url = "https://open.bigmodel.cn/api/paas/v4/chat/completions"; const response = await fetch(url, { @@ -22,8 +27,11 @@ async function callZhipuAPI( type: "disabled", }, }), + signal: controller.signal, }); + clearTimeout(timeoutId); + if (!response.ok) { throw new Error(`API 调用失败: ${response.status} ${response.statusText}`); } @@ -42,4 +50,4 @@ async function getAnswer(prompt: string | Messages): Promise { return response.choices[0].message.content.trim() as string; } -export { getAnswer }; \ No newline at end of file +export { getAnswer };