diff --git a/src/pages/ReportEditor.tsx b/src/pages/ReportEditor.tsx index e5364ee..7a2c0b6 100644 --- a/src/pages/ReportEditor.tsx +++ b/src/pages/ReportEditor.tsx @@ -73,6 +73,13 @@ export default function ReportEditor() { ]); const [isEditingPrompts, setIsEditingPrompts] = useState(false); const [diffModal, setDiffModal] = useState<{isOpen: boolean, originalHtml: string, newHtml: string, targetId: string} | null>(null); + const [lastExchangeLog, setLastExchangeLog] = useState<{ + startTime: string; + modelConfig: { provider: string; endpoint: string; modelName: string }; + requestPayload: any; + responsePayload: any | null; + errorDetail: { status: number; statusText: string; responseText: string; message: string } | null; + } | null>(null); useEffect(() => { stateRef.current.chatMessages = chatMessages; @@ -945,23 +952,50 @@ export default function ReportEditor() { const systemPrompt = aiModifyEnabled ? '你是一名专业的外科医生助理。当前处于【修改模式】。\n我为你提供了当前手术报告的【全局参考内容】作为背景知识,以及你需要修改的【目标区域 HTML 源码】。\n请根据用户的【医生指令】,直接重写并输出目标区域的 HTML。\n\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 必须包含 "reply"(简短回复)和 "updatedHtml"(修改后的完整 HTML 代码)两个字段\n3. 【内容边界】:全局参考内容仅供你理解上下文。你的 updatedHtml 只能包含目标区域本身的内容(例如:如果目标区域是"手术步骤",你就只写步骤)。严禁输出签名、落款、术后总结等属于报告其他部分的结构!\n4. 段落必须使用

标签包裹,段落间绝对不要使用
标签,也不要使用换行符 (\\n)\n5. 输出的 HTML 必须紧凑,标签之间不要有空格或换行\n6. 绝对不要包含任何 Markdown 标记(如 ```json)' : '你是一名专业的外科医生助理。当前处于【对话模式】。\n请仔细阅读我提供的【全局手术报告参考内容】,并根据【医生指令】进行专业解答。\n重要指令:\n1. 必须返回合法的 JSON 对象\n2. 仅包含 "reply"(你的专业回答)一个字段\n3. 不要返回任何 HTML 代码\n4. 绝对不要包含任何 Markdown 标记'; + const payload: any = { + model: modelName, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: messageContent } + ], + temperature: 0.3 + }; + const isKimiK25 = settings.activeAiProvider === 'kimi' && /k2\.5/i.test(modelName); + if (isKimiK25) { + delete payload.temperature; + delete payload.top_p; + delete payload.presence_penalty; + delete payload.frequency_penalty; + } + const logEntry = { + startTime: new Date().toISOString(), + modelConfig: { provider: settings.activeAiProvider || 'kimi', endpoint: apiEndpoint, modelName }, + requestPayload: JSON.parse(JSON.stringify(payload)), + responsePayload: null as any | null, + errorDetail: null as { status: number; statusText: string; responseText: string; message: string } | null + }; const response = await fetch(`${apiEndpoint}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, - body: JSON.stringify({ - model: modelName, - messages: [ - { role: 'system', content: systemPrompt }, - { role: 'user', content: messageContent } - ], - temperature: 0.3 - }) + body: JSON.stringify(payload) }); - if (!response.ok) throw new Error(`API 请求失败: ${response.status}`); + if (!response.ok) { + const errorText = await response.text().catch(() => ''); + logEntry.errorDetail = { + status: response.status, + statusText: response.statusText, + responseText: errorText, + message: `API 请求失败: ${response.status}` + }; + setLastExchangeLog(logEntry); + throw new Error(`API 请求失败: ${response.status}${errorText ? ' - ' + errorText : ''}`); + } const data = await response.json(); + logEntry.responsePayload = data; + setLastExchangeLog(logEntry); const responseText = data.choices[0].message.content.trim(); const cleanedText = responseText.replace(/```json\n?|```/g, ''); let responseJson: any = {}; @@ -2386,6 +2420,7 @@ export default function ReportEditor() { exportAt: new Date().toISOString(), url: window.location.href, messages: chatMessages, + lastExchange: lastExchangeLog, metadata: { user: currentUser?.username || 'anonymous', activeProvider: (() => { const s = storage.get('systemSettings', {} as SystemSettings); return s.activeAiProvider || 'kimi'; })(), diff --git a/工程分析/20260419_2249/功能变更实现方案文档.md b/工程分析/20260419_2249/功能变更实现方案文档.md new file mode 100644 index 0000000..88ec93e --- /dev/null +++ b/工程分析/20260419_2249/功能变更实现方案文档.md @@ -0,0 +1,117 @@ +# 功能变更实现方案文档(20260419_2249) + +## 实现方案 A:Kimi k2.5 参数拦截 + +### 变更点 +`ReportEditor.tsx` 中 `handleAIGenerate` 函数的请求体构建部分(当前行号约 948-962)。 + +### 具体实现 +1. 在构建 `body` 前,先创建一个 `payload` 对象: + ```ts + const payload: any = { + model: modelName, + messages: [ + { role: 'system', content: systemPrompt }, + { role: 'user', content: messageContent } + ], + temperature: 0.3 + }; + ``` +2. 增加条件判断: + ```ts + const isKimiK25 = settings.activeAiProvider === 'kimi' && /k2\.5/i.test(modelName); + if (isKimiK25) { + delete payload.temperature; + delete payload.top_p; + delete payload.presence_penalty; + delete payload.frequency_penalty; + } + ``` +3. `fetch` 的 `body: JSON.stringify(payload)` + +### 设计理由 +- 使用 `delete` 而非覆盖为固定值,是因为 Kimi 官方文档明确禁止这些参数出现;即使设为 `1.0` 仍可能触发校验失败 +- 条件判断放在 `settings` 读取后立即执行,确保使用用户实际配置的 provider 和 modelName +- 正则 `/k2\.5/i` 兼容可能的模型 ID 变体(如 `kimi-k2.5`、`kimi-k2.5-preview` 等) + +--- + +## 实现方案 B:完善 AI 日志导出 + +### 变更点 1:新增 `lastExchangeLog` 状态 +在 `ReportEditor.tsx` 的 state 定义区(`chatMessages` 附近)新增: +```ts +const [lastExchangeLog, setLastExchangeLog] = useState<{ + startTime: string; + modelConfig: { provider: string; endpoint: string; modelName: string }; + requestPayload: any; + responsePayload: any | null; + errorDetail: { status: number; statusText: string; responseText: string; message: string } | null; +} | null>(null); +``` + +### 变更点 2:在 `handleAIGenerate` 中记录日志 +将函数重构为在关键节点捕获数据: + +**A. 请求前记录:** +```ts +const logEntry = { + startTime: new Date().toISOString(), + modelConfig: { provider: settings.activeAiProvider || 'kimi', endpoint: apiEndpoint, modelName }, + requestPayload: { ...payload }, + responsePayload: null, + errorDetail: null +}; +``` + +**B. 错误处理增强:** +当前错误处理只有: +```ts +if (!response.ok) throw new Error(`API 请求失败: ${response.status}`); +``` +改为: +```ts +if (!response.ok) { + const errorText = await response.text(); + logEntry.errorDetail = { + status: response.status, + statusText: response.statusText, + responseText: errorText, + message: `API 请求失败: ${response.status}` + }; + setLastExchangeLog(logEntry); + throw new Error(`API 请求失败: ${response.status} - ${errorText}`); +} +``` + +**C. 成功响应记录:** +在解析 `responseJson` 后: +```ts +logEntry.responsePayload = responseJson; +setLastExchangeLog(logEntry); +``` + +### 变更点 3:更新导出按钮 +在「导出 AI 日志」按钮的 `onClick` 中,将 `lastExchangeLog` 加入导出数据: +```ts +const data = { + exportAt: new Date().toISOString(), + url: window.location.href, + messages: chatMessages, + lastExchange: lastExchangeLog, + metadata: { ... } +}; +``` + +### 设计理由 +- `lastExchangeLog` 只记录最后一次调用,避免无限增长导致内存/状态膨胀 +- 错误时捕获 `response.text()` 获取 Kimi 官方返回的详细错误 JSON(通常包含 `error.code` 和 `error.message`) +- `requestPayload` 深拷贝防止后续 `delete` 操作污染日志记录 +- 使用 `useState` 而非 `useRef`,因为导出按钮需要读取最新值并触发重渲染显示状态 + +--- + +## 依赖与兼容性 +- 无新增 npm 依赖 +- TypeScript 类型在组件内部定义,不影响 `src/types.ts` +- 向后兼容:旧数据无 `lastExchangeLog`,导出时字段为 `null` diff --git a/工程分析/20260419_2249/功能变更测试文档.md b/工程分析/20260419_2249/功能变更测试文档.md new file mode 100644 index 0000000..7bbd15f --- /dev/null +++ b/工程分析/20260419_2249/功能变更测试文档.md @@ -0,0 +1,64 @@ +# 功能变更测试文档(20260419_2249) + +## 测试项 1:Kimi k2.5 参数拦截 + +### 测试场景 A:Kimi + k2.5 模型 +1. 进入系统设置 → AI 接口集成 +2. 供应商选择「Kimi (Moonshot)」,模型名填写 `kimi-k2.5` +3. 填写有效 API Key,点击「测试连接」确认配置有效 +4. 进入报告编辑器,打开 AI 面板,发送任意消息 +5. **预期结果**: + - 网络请求成功(HTTP 200),无 400 报错 + - 浏览器 DevTools → Network → 请求体中**不包含** `temperature`、`top_p`、`presence_penalty`、`frequency_penalty` + +### 测试场景 B:Kimi + 非 k2.5 模型 +1. 系统设置中模型名改为 `moonshot-v1-32k` +2. 发送 AI 消息 +3. **预期结果**: + - 请求体中**包含** `temperature: 0.3` + - 调用正常 + +### 测试场景 C:DeepSeek / OpenAI / Custom +1. 切换供应商为 DeepSeek,模型 `deepseek-chat` +2. 发送 AI 消息 +3. **预期结果**: + - 请求体中**包含** `temperature: 0.3` + - 调用正常 + +--- + +## 测试项 2:AI 日志导出完善 + +### 测试场景 A:成功调用后的导出 +1. 发送一条 AI 消息并等待成功返回 +2. 点击「导出 AI 日志」 +3. **预期结果**: + - 下载的 JSON 中 `lastExchange` 字段非空 + - `lastExchange.requestPayload` 包含完整的 `model`、`messages`(system + user) + - `lastExchange.requestPayload.messages[0].content` 包含系统提示词全文 + - `lastExchange.responsePayload` 包含 AI 返回的原始 JSON(含 `reply` 和 `updatedHtml`) + - `lastExchange.modelConfig` 包含 provider、endpoint、modelName + - `lastExchange.errorDetail` 为 `null` + +### 测试场景 B:失败调用后的导出 +1. 故意填写错误的 API Key 或断开网络 +2. 发送 AI 消息,等待报错 +3. 点击「导出 AI 日志」 +4. **预期结果**: + - `lastExchange.errorDetail` 非空 + - 包含 `status`(如 401/400/403)、`statusText`、`responseText`(服务端返回的原始错误 JSON)、`message` + - `lastExchange.responsePayload` 为 `null` + +### 测试场景 C:未进行任何 AI 调用时的导出 +1. 刷新页面后直接点击「导出 AI 日志」 +2. **预期结果**: + - `lastExchange` 为 `null` + - 其他字段(messages、metadata)正常导出 + +--- + +## 回归测试 +- `tsc --noEmit` 零错误 +- `npm run build` 构建成功 +- 预览服务器正常启动并可访问 +- 现有 AI 对话功能不受影响的供应商(deepseek/openai)调用正常 diff --git a/工程分析/20260419_2249/功能变更需求文档.md b/工程分析/20260419_2249/功能变更需求文档.md new file mode 100644 index 0000000..a7f4b6d --- /dev/null +++ b/工程分析/20260419_2249/功能变更需求文档.md @@ -0,0 +1,44 @@ +# 功能变更需求文档(20260419_2249) + +## 需求 1:Kimi k2.5 模型强制传参规则适配 + +### 问题背景 +Kimi 最新版 `kimi-k2.5` 模型对 API 请求体有极其苛刻的要求,不允许出现非标准的温度和概率参数。当前系统在向所有模型发送请求时均硬编码了 `temperature: 0.3`,导致调用 `kimi-k2.5` 时返回 HTTP 400 错误。 + +### 需求描述 +在封装向大模型发起 `fetch` 请求的地方,增加条件判断: +- **触发条件**:当前激活供应商为 `kimi` 且模型名包含 `k2.5`(大小写不敏感) +- **数据处理**:强制从请求体中 `delete` 移除 `temperature`、`top_p`、`presence_penalty`、`frequency_penalty` 等可选参数,让 Kimi 官方服务器使用其默认安全值 +- **兼容性**:其他供应商(deepseek/openai/custom)及 Kimi 非 k2.5 模型不受影响,继续保留 `temperature: 0.3` + +### 参考文档 +https://platform.kimi.com/docs/guide/kimi-k2-5-quickstart + +--- + +## 需求 2:完善「导出 AI 日志」功能 + +### 问题背景 +当前「导出 AI 日志」按钮仅导出 `chatMessages`(UI 对话历史)和少量元数据,缺少: +- 实际发往 AI 的完整请求体(System Prompt + Messages + Parameters) +- AI 返回的原始 JSON 响应 +- API 调用失败时的具体错误信息(HTTP 状态码 + 响应体) +- 当前生效的模型完整配置 + +这些信息对于排查大模型幻觉、优化提示词、定位网络/接口故障至关重要。 + +### 需求描述 +1. 在 `ReportEditor.tsx` 中建立 `lastExchangeLog` 状态,每次 `handleAIGenerate` 调用时记录: + - `startTime`:请求发起时间 + - `requestPayload`:完整请求体(model、messages、实际发送的参数) + - `responsePayload`:AI 原始响应 JSON + - `errorDetail`:失败时的完整错误信息(含 HTTP 状态码、错误响应体文本) + - `modelConfig`:当前 provider、endpoint、modelName +2. 更新「导出 AI 日志」按钮,将 `lastExchangeLog` 一并写入导出的 JSON +3. 保持向后兼容:无 AI 调用记录时,`lastExchangeLog` 为 `null`,导出时不影响其他字段 + +--- + +## 影响范围 +- `src/pages/ReportEditor.tsx`(主要修改文件) +- 无新增依赖