diff --git a/.prettierrc.yaml b/.prettierrc.yaml index 35893b3..d955e51 100644 --- a/.prettierrc.yaml +++ b/.prettierrc.yaml @@ -2,3 +2,4 @@ singleQuote: true semi: false printWidth: 100 trailingComma: none +endOfLine: auto diff --git a/.vscode/launch.json b/.vscode/launch.json index 0b6b9a6..d4d8918 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "cwd": "${workspaceRoot}", "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite", "windows": { - "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.cmd" + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/electron-vite.CMD" }, "runtimeArgs": ["--sourcemap"], "env": { diff --git a/.vscode/settings.json b/.vscode/settings.json index d5b5fe1..6eecddd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -7,5 +7,6 @@ }, "[json]": { "editor.defaultFormatter": "vscode.json-language-features" - } + }, + "files.eol": "\n" } \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index aff5d3f..8de9b90 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -24,6 +24,12 @@ export default defineConfig( 'react-refresh': eslintPluginReactRefresh }, rules: { + 'prettier/prettier': [ + 'error', + { + endOfLine: 'auto' + } + ], ...eslintPluginReactHooks.configs.recommended.rules, ...eslintPluginReactRefresh.configs.vite.rules } diff --git a/src/main/services/ai-provider-service.ts b/src/main/services/ai-provider-service.ts index 2770441..40abdb3 100644 --- a/src/main/services/ai-provider-service.ts +++ b/src/main/services/ai-provider-service.ts @@ -26,16 +26,18 @@ type AIMessagePart = { type: 'text'; text: string } | { type: 'image'; dataUrl: type AIMessage = { role: string; content: string | AIMessagePart[] } type AIRequestResult = { data: string + finishReason?: string usage?: { input?: number; output?: number; total?: number; estimated?: boolean } } interface OpenAIResponsePayload { error?: { message?: string } - choices?: Array<{ message?: { content?: string } }> + choices?: Array<{ message?: { content?: string }; finish_reason?: string }> usage?: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number } } interface AnthropicResponsePayload { error?: { message?: string } content?: Array<{ type?: string; text?: string }> + stop_reason?: string usage?: { input_tokens?: number; output_tokens?: number } } @@ -612,6 +614,12 @@ async function requestOpenAICompatible( const endpoint = provider.baseUrl.endsWith('/chat/completions') ? provider.baseUrl : `${provider.baseUrl.replace(/\/+$/, '')}/chat/completions` + let modelMaxTokens = 0 + for (const m of provider.models) { + if (m.id === model && m.maxTokens) { + modelMaxTokens = m.maxTokens + } + } const response = await fetchWithTimeout( endpoint, { @@ -621,7 +629,11 @@ async function requestOpenAICompatible( model, messages: toOpenAIMessages(messages), temperature: provider.advanced.temperature, - max_tokens: testing ? 8 : provider.advanced.maxTokens + max_tokens: testing + ? 8 + : modelMaxTokens > 0 + ? modelMaxTokens + : provider.advanced.maxTokens || 4096 }) }, provider.advanced.timeoutMs, @@ -631,6 +643,7 @@ async function requestOpenAICompatible( if (!response.ok) throw new Error(payload.error?.message || `AI 请求失败 (${response.status})`) return { data: String(payload.choices?.[0]?.message?.content || ''), + finishReason: String(payload.choices?.[0]?.finish_reason || 'unknown'), usage: payload.usage ? { input: payload.usage.prompt_tokens, @@ -667,6 +680,12 @@ async function requestAnthropic( const endpoint = provider.baseUrl.endsWith('/messages') ? provider.baseUrl : `${provider.baseUrl.replace(/\/+$/, '')}/messages` + let modelMaxTokens = 0 + for (const m of provider.models) { + if (m.id === model && m.maxTokens) { + modelMaxTokens = m.maxTokens + } + } const response = await fetchWithTimeout( endpoint, { @@ -677,7 +696,11 @@ async function requestAnthropic( system: system || undefined, messages: anthropicMessages, temperature: provider.advanced.temperature, - max_tokens: testing ? 8 : provider.advanced.maxTokens || 4096 + max_tokens: testing + ? 8 + : modelMaxTokens > 0 + ? modelMaxTokens + : provider.advanced.maxTokens || 4096 }) }, provider.advanced.timeoutMs, @@ -693,6 +716,7 @@ async function requestAnthropic( .map((item) => item.text || '') .join('\n') : '', + finishReason: String(payload.stop_reason || 'unknown'), usage: payload.usage ? { input: payload.usage.input_tokens, diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 6a7cd75..9707820 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -249,6 +249,7 @@ declare global { ) => Promise<{ success: boolean data?: string + finishReason?: string usage?: { input?: number output?: number diff --git a/src/renderer/src/hooks/useGroupReportGeneration.ts b/src/renderer/src/hooks/useGroupReportGeneration.ts index 6eefe08..cbb5711 100644 --- a/src/renderer/src/hooks/useGroupReportGeneration.ts +++ b/src/renderer/src/hooks/useGroupReportGeneration.ts @@ -511,7 +511,19 @@ export function useGroupReportGeneration({ reportTimeoutSeconds * 1000 + REPORT_MODEL_TIMEOUT_BUFFER_MS ) ) - if (!result.success || !result.data) throw new Error(result.error || 'AI 请求失败') + if ( + result.finishReason && + result.finishReason !== 'stop' && + result.finishReason !== 'end_turn' + ) { + let errMsg = `当前模型\`异常结束生成\`,原因:${result.finishReason}` + if (result.finishReason === 'length' || result.finishReason === 'max_tokens') { + errMsg += ',请尝试在`设置-AI 模型-新增/编辑供应商-模型配置/高级设置`中调高`Max Tokens`' + } + throw new Error(errMsg) + } + if (!result.success || !result.data) + throw new Error(result.error || 'AI 请求失败,未返回有效内容') writeReportLog('info', '模型响应完成', { outputLength: result.data.length, usage: result.usage @@ -552,8 +564,23 @@ export function useGroupReportGeneration({ reportTimeoutSeconds * 1000 + REPORT_MODEL_TIMEOUT_BUFFER_MS ) ) + if ( + repairResult.finishReason && + repairResult.finishReason !== 'stop' && + repairResult.finishReason !== 'end_turn' + ) { + let errMsg = `当前模型\`异常结束生成\`,原因:${repairResult.finishReason}` + if ( + repairResult.finishReason === 'length' || + repairResult.finishReason === 'max_tokens' + ) { + errMsg += + ',请尝试在`设置-AI 模型-新增/编辑供应商-模型配置/高级设置`中调高`Max Tokens`' + } + throw new Error(errMsg) + } if (!repairResult.success || !repairResult.data) { - throw new Error(repairResult.error || 'AI 修复日报 JSON 失败', { + throw new Error(repairResult.error || 'AI 修复日报 JSON 失败,未返回有效内容', { cause: parseError }) }