feat: Kimi k2.5参数适配+AI日志导出完善(20260419_2249)

- Kimi k2.5 强制传参拦截: 当 provider=kimi 且 model 包含 k2.5 时,
  从请求体中 delete temperature/top_p/presence_penalty/frequency_penalty,
  彻底解决 HTTP 400 报错
- 完善导出AI日志: 新增 lastExchangeLog 状态, 记录每次调用的
  完整请求体(requestPayload)、原始响应(responsePayload)、
  错误详情(errorDetail含status/statusText/responseText)、模型配置
- 更新导出按钮 JSON 结构, 包含 lastExchange 字段
This commit is contained in:
2026-04-19 22:54:00 +08:00
parent 2e634ff832
commit 3bec69986e
4 changed files with 269 additions and 9 deletions

View File

@@ -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. 段落必须使用 <p> 标签包裹,段落间绝对不要使用 <br> 标签,也不要使用换行符 (\\n)\n5. 输出的 HTML 必须紧凑,标签之间不要有空格或换行\n6. 绝对不要包含任何 Markdown 标记(如 ```json'
: '你是一名专业的外科医生助理。当前处于【对话模式】。\n请仔细阅读我提供的【全局手术报告参考内容】并根据【医生指令】进行专业解答。\n重要指令\n1. 必须返回合法的 JSON 对象\n2. 仅包含 "reply"(你的专业回答)一个字段\n3. 不要返回任何 HTML 代码\n4. 绝对不要包含任何 Markdown 标记';
const response = await fetch(`${apiEndpoint}/chat/completions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`
},
body: JSON.stringify({
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(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>('systemSettings', {} as SystemSettings); return s.activeAiProvider || 'kimi'; })(),

View File

@@ -0,0 +1,117 @@
# 功能变更实现方案文档20260419_2249
## 实现方案 AKimi 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`

View File

@@ -0,0 +1,64 @@
# 功能变更测试文档20260419_2249
## 测试项 1Kimi k2.5 参数拦截
### 测试场景 AKimi + 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`
### 测试场景 BKimi + 非 k2.5 模型
1. 系统设置中模型名改为 `moonshot-v1-32k`
2. 发送 AI 消息
3. **预期结果**
- 请求体中**包含** `temperature: 0.3`
- 调用正常
### 测试场景 CDeepSeek / OpenAI / Custom
1. 切换供应商为 DeepSeek模型 `deepseek-chat`
2. 发送 AI 消息
3. **预期结果**
- 请求体中**包含** `temperature: 0.3`
- 调用正常
---
## 测试项 2AI 日志导出完善
### 测试场景 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调用正常

View File

@@ -0,0 +1,44 @@
# 功能变更需求文档20260419_2249
## 需求 1Kimi 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`(主要修改文件)
- 无新增依赖