2026-04-19-03-03-55 修复AI撰写体验:API endpoint斜杠净化、模型列表下拉栏、聊天记录持久化存储
This commit is contained in:
118
工程分析/实现方案-2026-04-19-03-03-55.md
Normal file
118
工程分析/实现方案-2026-04-19-03-03-55.md
Normal file
@@ -0,0 +1,118 @@
|
||||
# 实现方案 — 2026-04-19-03-03-55
|
||||
|
||||
## 1. 方案概述
|
||||
三处修补:① endpoint 尾部斜杠净化防止 404;② testApi 捕获模型列表并动态切换 Model Name 输入框为下拉栏;③ 将 `chatMessages` 纳入现有草稿持久化生命周期。
|
||||
|
||||
## 2. 详细步骤
|
||||
|
||||
### 步骤 1:`src/pages/ReportEditor.tsx` — endpoint 净化
|
||||
**目标文件**:`src/pages/ReportEditor.tsx`
|
||||
**修改内容**:
|
||||
在 `handleAIGenerate` 中,将:
|
||||
```ts
|
||||
const apiEndpoint = provider?.endpoint || 'https://api.moonshot.cn/v1';
|
||||
```
|
||||
改为:
|
||||
```ts
|
||||
const apiEndpoint = (provider?.endpoint || 'https://api.moonshot.cn/v1').replace(/\/+$/, '');
|
||||
```
|
||||
|
||||
### 步骤 2:`src/pages/ReportEditor.tsx` — 聊天记录持久化
|
||||
**目标文件**:`src/pages/ReportEditor.tsx`
|
||||
**修改内容**:
|
||||
1. `stateRef` 增加 `chatMessages`:
|
||||
```ts
|
||||
const stateRef = useRef({ reportData, videos, capturedFrames, activeTab, loadedTemplateId, chatMessages });
|
||||
```
|
||||
2. `saveDraftToStorage` 增加 `chatMessages`:
|
||||
```ts
|
||||
storage.set(key, {
|
||||
content: currentContent,
|
||||
draftReportId: reportId || null,
|
||||
reportData: stateRef.current.reportData,
|
||||
videos: stateRef.current.videos,
|
||||
capturedFrames: stateRef.current.capturedFrames,
|
||||
activeTab: stateRef.current.activeTab,
|
||||
loadedTemplateId: stateRef.current.loadedTemplateId,
|
||||
chatMessages: stateRef.current.chatMessages
|
||||
});
|
||||
```
|
||||
3. 在 `setChatMessages` 的调用处同步更新 `stateRef.current.chatMessages`:
|
||||
- `handleAIGenerate` 中发送 user 消息时
|
||||
- `handleAIGenerate` 中收到 model 消息时
|
||||
- 也可在 `setChatInput('')` 之后统一用 `useEffect` 监听 `chatMessages` 变化来同步 ref
|
||||
|
||||
更简单的方案:增加一个 `useEffect` 监听 `chatMessages`:
|
||||
```ts
|
||||
useEffect(() => {
|
||||
stateRef.current.chatMessages = chatMessages;
|
||||
}, [chatMessages]);
|
||||
```
|
||||
4. 初始化 `useEffect`(draft 恢复分支)中恢复 `chatMessages`:
|
||||
```ts
|
||||
if (draft.chatMessages) {
|
||||
setChatMessages(draft.chatMessages);
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 3:`src/pages/SystemSettings.tsx` — 模型名称下拉栏
|
||||
**目标文件**:`src/pages/SystemSettings.tsx`
|
||||
**修改内容**:
|
||||
1. 新增 state:
|
||||
```ts
|
||||
const [availableModels, setAvailableModels] = useState<string[]>([]);
|
||||
```
|
||||
2. 修改 `testApi`:
|
||||
```ts
|
||||
const testApi = async () => {
|
||||
const provider = settings.aiProviders[settings.activeAiProvider];
|
||||
if (!provider?.apiKey) { alert('请先输入 API 密钥'); return; }
|
||||
try {
|
||||
const res = await fetch(`${provider.endpoint.replace(/\/+$/, '')}/models`, {
|
||||
method: 'GET',
|
||||
headers: { 'Authorization': `Bearer ${provider.apiKey}`, 'Content-Type': 'application/json' }
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
const models = data.data?.map((m: any) => m.id).filter((id: string) => id) || [];
|
||||
setAvailableModels(models);
|
||||
if (models.length > 0 && !provider.modelName) {
|
||||
const next = { ...settings.aiProviders };
|
||||
next[settings.activeAiProvider] = { ...next[settings.activeAiProvider], modelName: models[0] };
|
||||
setSettings({ ...settings, aiProviders: next });
|
||||
}
|
||||
alert(`连接成功!可用模型数: ${models.length}`);
|
||||
} else {
|
||||
alert(`连接失败: ${res.status} ${res.statusText}`);
|
||||
setAvailableModels([]);
|
||||
}
|
||||
} catch (e: any) {
|
||||
alert(`连接失败: ${e.message}`);
|
||||
setAvailableModels([]);
|
||||
}
|
||||
};
|
||||
```
|
||||
3. Model Name UI 改为条件渲染:
|
||||
```tsx
|
||||
{availableModels.length > 0 ? (
|
||||
<select value={settings.aiProviders[settings.activeAiProvider]?.modelName || ''}
|
||||
onChange={(e) => {
|
||||
const next = { ...settings.aiProviders };
|
||||
next[settings.activeAiProvider] = { ...next[settings.activeAiProvider], modelName: e.target.value };
|
||||
setSettings({ ...settings, aiProviders: next });
|
||||
}}
|
||||
className="input-minimal bg-white">
|
||||
{availableModels.map(m => <option key={m} value={m}>{m}</option>)}
|
||||
</select>
|
||||
) : (
|
||||
<input type="text" ... />
|
||||
)}
|
||||
```
|
||||
|
||||
## 3. 依赖关系
|
||||
- 步骤 1 和步骤 2 可并行(都在 ReportEditor.tsx)
|
||||
- 步骤 3 独立(SystemSettings.tsx)
|
||||
|
||||
## 4. 风险预案
|
||||
- 若 `/models` 接口返回格式非标准 OpenAI 格式(无 `data` 数组),`models` 列表为空,自动回退到 input 输入框
|
||||
- 若 draft 中没有 `chatMessages`(旧 draft),`setChatMessages` 不执行,保持空数组
|
||||
38
工程分析/测试方案-2026-04-19-03-03-55.md
Normal file
38
工程分析/测试方案-2026-04-19-03-03-55.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# 测试方案 — 2026-04-19-03-03-55
|
||||
|
||||
## 1. 测试范围
|
||||
- API 404 修复(endpoint 尾部斜杠)
|
||||
- 模型名称下拉栏动态切换
|
||||
- AI 聊天记录路由切换持久化
|
||||
|
||||
## 2. 测试步骤与预期结果
|
||||
|
||||
### 场景 1:Endpoint 尾部斜杠修复
|
||||
1. 在 SystemSettings 中故意将 Base URL 末尾多加一个 `/`:`https://api.moonshot.cn/v1/`
|
||||
2. 保存,进入 ReportEditor,发送 AI 消息
|
||||
预期:请求 URL 应为 `https://api.moonshot.cn/v1/chat/completions`(只有 1 个斜杠),不应 404
|
||||
|
||||
### 场景 2:模型名称下拉栏
|
||||
1. 在 SystemSettings 中填写正确的 Base URL 和 API Key
|
||||
2. 点击"测试连接"
|
||||
预期:alert 显示连接成功,下方模型名称自动变为下拉栏,列出所有可用模型
|
||||
3. 选择其中一个模型,保存
|
||||
4. 刷新页面
|
||||
预期:模型名称仍为下拉栏,选中值保留
|
||||
|
||||
### 场景 3:聊天记录持久化
|
||||
1. 进入 ReportEditor,切换到 AI撰写 Tab
|
||||
2. 发送 2-3 条消息(user + model)
|
||||
3. 切换到 `/report-manage`,再返回 `/report-editor`
|
||||
预期:AI撰写 Tab 中聊天记录仍然存在,与离开前一致
|
||||
4. 刷新页面(模拟完全重载)
|
||||
预期:聊天记录仍然从 draft 中恢复
|
||||
|
||||
### 场景 4:类型检查与构建
|
||||
1. `npm run lint`
|
||||
预期:0 errors
|
||||
2. `npm run build`
|
||||
预期:成功
|
||||
|
||||
## 3. 回滚检查
|
||||
- 若测试失败,执行 `git checkout main` 恢复到上一个 commit
|
||||
44
工程分析/经验记录.md
44
工程分析/经验记录.md
@@ -544,3 +544,47 @@ SystemSettings 需要支持 4 个服务商(Kimi/DeepSeek/OpenAI/自定义)
|
||||
**D. 后续如何避免问题**
|
||||
- 当一组配置具有"同构多实例"特征时(多个服务商、多个环境、多个账号),优先使用 `Record<string, Config>` 而非平铺字段。
|
||||
- 动态表单的 `onChange` 必须注意不可变更新:先浅拷贝外层字典,再浅拷贝当前项,最后修改目标字段。直接 `settings.aiProviders[k].endpoint = x` 会触发 React 引用比较优化导致不刷新。
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 记录 26:API Endpoint 尾部斜杠导致 404
|
||||
|
||||
**A. 具体问题**
|
||||
SystemSettings 中测试连接成功(`/models` 返回 200),但 ReportEditor 中调用 `/chat/completions` 报 404。用户输入的 Base URL 末尾带有 `/`,导致拼接后路径为 `https://api.xxx.com/v1//chat/completions`。
|
||||
|
||||
**B. 产生问题原因**
|
||||
用户从文档复制 Base URL 时,末尾可能带斜杠;代码中直接做字符串拼接 `${apiEndpoint}/chat/completions`,未做净化处理。
|
||||
|
||||
**C. 解决问题方案**
|
||||
在 `handleAIGenerate` 和 `testApi` 中统一对 endpoint 做尾部斜杠移除:
|
||||
```ts
|
||||
const apiEndpoint = (provider?.endpoint || 'https://api.moonshot.cn/v1').replace(/\/+$/, '');
|
||||
```
|
||||
|
||||
**D. 后续如何避免问题**
|
||||
- 任何从用户输入拼接 URL 的场景,都必须先对基础路径做 `.replace(/\/+$/, '')` 或 `new URL(path, base)` 标准化处理。
|
||||
- 测试连通性(`/models`)和实际业务调用(`/chat/completions`)应使用同一套 endpoint 净化逻辑,避免"测试通过、调用失败"的认知落差。
|
||||
|
||||
---
|
||||
|
||||
## 记录 27:State 未纳入 Ref 导致自动保存遗漏
|
||||
|
||||
**A. 具体问题**
|
||||
AI 撰写面板的 `chatMessages` 在路由切换后全部丢失。因为 `saveDraftToStorage` 从 `stateRef.current` 读取数据快照,而 `chatMessages` 从未被同步到 `stateRef`。
|
||||
|
||||
**B. 产生问题原因**
|
||||
ReportEditor 采用 `useRef` 作为自动保存的数据快照机制(避免 React state 闭包陷阱)。新增 `chatMessages` state 时,只关注了 UI 渲染,遗漏了与 `stateRef` 的同步。
|
||||
|
||||
**C. 解决问题方案**
|
||||
1. `stateRef` 初始化时包含 `chatMessages`
|
||||
2. `saveDraftToStorage` 保存对象中增加 `chatMessages: stateRef.current.chatMessages`
|
||||
3. 增加 `useEffect` 监听 `chatMessages` 变化,实时同步到 `stateRef.current.chatMessages`
|
||||
4. 所有草稿恢复分支(初始化 useEffect 的 2 处 + useLayoutEffect 的 2 处)均增加 `chatMessages` 的恢复和 ref 同步
|
||||
|
||||
**D. 后续如何避免问题**
|
||||
- 在 `ReportEditor.tsx` 中新增任何 `useState` 时,必须同时问自己三个问题:
|
||||
1. 这个 state 是否需要持久化到 draft?
|
||||
2. 若需要,是否已加入 `stateRef` 初始化?
|
||||
3. 若需要,是否已在 `saveDraftToStorage`、所有恢复分支、以及 state→ref 同步 effect 中补齐?
|
||||
- 建议维护一份 "Draft 持久化字段清单" 注释在 `stateRef` 定义附近,作为新增 state 时的检查单。
|
||||
|
||||
37
工程分析/需求分析-2026-04-19-03-03-55.md
Normal file
37
工程分析/需求分析-2026-04-19-03-03-55.md
Normal file
@@ -0,0 +1,37 @@
|
||||
# 需求分析 — 2026-04-19-03-03-55
|
||||
|
||||
## 1. 需求背景
|
||||
AI 撰写功能上线后出现三个体验问题:
|
||||
1. **SystemSettings 测试连接成功(返回 13 个模型),但 ReportEditor 调用报 404**:`POST https://api.moonshot.cn/v1/chat/completions 404`
|
||||
2. **模型名称只能手动输入**:用户希望测试连接成功后,模型名称自动变为下拉选择(从 `/models` 返回的列表中选择)
|
||||
3. **AI 聊天记录不持久**:从 ReportEditor 切换到其他页面再返回,AI 撰写面板中的聊天记录全部丢失
|
||||
|
||||
## 2. 需求拆解
|
||||
- [ ] **Task 1:修复 404 错误**
|
||||
- 原因分析:测试按钮读取的是 React state(界面临时值),ReportEditor 读取的是 localStorage。若测试后未点击"保存",两者不一致。另外 `apiEndpoint` 末尾多余斜杠可能导致路径拼接错误(`v1//chat/completions`)
|
||||
- 修复:在 `handleAIGenerate` 中对 `apiEndpoint` 做 `.replace(/\/+$/, '')` 净化
|
||||
- [ ] **Task 2:模型名称下拉栏**
|
||||
- SystemSettings.tsx 增加 `availableModels` 状态
|
||||
- `testApi` 成功后解析 `/models` 响应,填充 `availableModels`
|
||||
- Model Name 输入框在有 availableModels 时自动变为 `<select>` 下拉
|
||||
- [ ] **Task 3:AI 聊天记录持久化**
|
||||
- `stateRef` 增加 `chatMessages` 字段
|
||||
- `saveDraftToStorage` 将 `chatMessages` 存入 draft
|
||||
- 初始化 `useEffect` 恢复 draft 时,同步恢复 `chatMessages`
|
||||
|
||||
## 3. 影响范围
|
||||
| 文件 | 修改类型 | 风险等级 |
|
||||
|------|----------|----------|
|
||||
| `src/pages/ReportEditor.tsx` | 修改(endpoint 净化 + 草稿持久化) | 中 |
|
||||
| `src/pages/SystemSettings.tsx` | 修改(testApi + UI 动态切换) | 低 |
|
||||
|
||||
## 4. 优先级
|
||||
- P0:404 修复(功能不可用)
|
||||
- P1:聊天记录持久化(体验问题)
|
||||
- P1:模型名称下拉栏(体验优化)
|
||||
|
||||
## 5. 验收标准
|
||||
- [ ] ReportEditor 中 AI 调用不再因尾部斜杠导致 404
|
||||
- [ ] SystemSettings 测试连接成功后,模型名称自动变为下拉栏,可选模型列表
|
||||
- [ ] 切换页面后返回 ReportEditor,AI 聊天记录保留
|
||||
- [ ] `npm run lint` 无类型错误
|
||||
Reference in New Issue
Block a user