Переглянути джерело

chore: Clean up documentation and temporary test files

- Remove CHANGES.txt changelog and documentation files (WORKFLOW_INTEGRATION.md, WORKFLOW_INTEGRATION_SUMMARY.md, 阶段性分析报告.md)
- Delete temporary test HTML files from public directory (check-duplication.html, test-api.html, test-dedup.html)
- Remove vitest.config.ts and services/README.md
- Update README.md with comprehensive project summary and architecture documentation
- Refactor App.tsx component and related service modules (documentService, exportService, exportRecordService)
- Improve component implementations in ChatPanel, DocumentManagement, EditorPanel, MarkdownPreview, and WYSIWYGEditor
- Clean up obsolete configuration and documentation to streamline repository
Zhang Yice 2 місяців тому
батько
коміт
d540a9f9dc

+ 0 - 268
CHANGES.txt

@@ -1,268 +0,0 @@
1
-================================================================================
2
-                    布局更新 - 修改文件清单
3
-================================================================================
4
-
5
-更新日期:2026-06-24
6
-版本:v2.0.0
7
-目标:实现图三所示的三栏布局(左侧聊天 + 右侧编辑 + 固定目录)
8
-
9
-================================================================================
10
-                         修改的代码文件(4个)
11
-================================================================================
12
-
13
-1. src/components/MarkdownEditor/MarkdownEditor.tsx
14
-   ✏️ 修改类型:重大更新
15
-   
16
-   变更内容:
17
-   - 将垂直布局(编辑+预览)改为水平布局(编辑+目录)
18
-   - 移除预览面板,改为纯编辑模式
19
-   - 将目录从 Drawer 抽屉改为固定侧边栏
20
-   - 移除 outlineVisible 状态和 toggleOutline 函数
21
-   - 移除 Drawer 和 UnorderedListOutlined 导入
22
-   - Toast UI Editor 设置为 previewStyle="tab" 和 hideModeSwitch={true}
23
-   
24
-   影响范围:
25
-   - 编辑器布局完全改变
26
-   - 用户交互方式改变
27
-   - 目录始终可见
28
-
29
-   代码行数变化:约 ±50 行
30
-
31
---------------------------------------------------------------------------------
32
-
33
-2. src/components/MarkdownEditor/MarkdownEditor.css
34
-   ➕ 文件类型:新增
35
-   
36
-   内容:
37
-   - 目录项样式(基础、悬停、激活)
38
-   - 标题层级样式(H1-H6 不同样式)
39
-   - 滚动条样式
40
-   - 空状态样式
41
-   - 响应式规则(不同屏幕尺寸)
42
-   
43
-   影响范围:
44
-   - 目录视觉效果
45
-   - 交互反馈
46
-   - 响应式显示
47
-
48
-   代码行数:约 80 行
49
-
50
---------------------------------------------------------------------------------
51
-
52
-3. src/stores/chatStore.ts
53
-   🐛 修改类型:Bug 修复
54
-   
55
-   变更内容:
56
-   第 19 行:
57
-   - 错误: import { exportDoc } from '../services/exportService';
58
-   - 正确: import { exportToWord } from '../services/exportService';
59
-   
60
-   第 XX 行(使用处):
61
-   - 错误: await exportDoc({...})
62
-   - 正确: await exportToWord({...})
63
-   
64
-   影响范围:
65
-   - 解决模块导入错误
66
-   - 修复导出功能
67
-
68
-   代码行数变化:2 行
69
-
70
---------------------------------------------------------------------------------
71
-
72
-4. src/App.tsx
73
-   📝 修改类型:文档更新
74
-   
75
-   变更内容:
76
-   - 更新文件头部注释
77
-   - 添加新布局结构的 ASCII 图示
78
-   - 说明三栏布局的实现
79
-   
80
-   影响范围:
81
-   - 仅文档说明,不影响功能
82
-   
83
-   代码行数变化:+15 行
84
-
85
-================================================================================
86
-                         新增的文档文件(5个)
87
-================================================================================
88
-
89
-1. docs/EDITOR_LAYOUT.md
90
-   📄 类型:技术设计文档
91
-   内容:完整的布局设计说明、技术实现、样式规范
92
-   字数:约 3000 字
93
-
94
-2. docs/EDITOR_LAYOUT_TEST.md
95
-   📄 类型:测试文档
96
-   内容:详细的测试用例、步骤、预期结果、兼容性测试
97
-   字数:约 2500 字
98
-
99
-3. docs/LAYOUT_COMPARISON.md
100
-   📄 类型:对比分析
101
-   内容:新旧布局的详细对比、用户场景分析、数据对比
102
-   字数:约 2000 字
103
-
104
-4. CHANGELOG_LAYOUT.md
105
-   📄 类型:变更日志
106
-   内容:完整的更新记录、功能列表、问题修复、未来计划
107
-   字数:约 2200 字
108
-
109
-5. UPDATE_SUMMARY.md
110
-   📄 类型:快速总结
111
-   内容:一页纸的更新总结、核心变化、测试清单
112
-   字数:约 1500 字
113
-
114
-6. QUICK_START.md
115
-   📄 类型:快速指南
116
-   内容:3步启动指南、功能测试、常见问题
117
-   字数:约 1000 字
118
-
119
-7. CHANGES.txt
120
-   📄 类型:修改清单
121
-   内容:本文件,简明的修改列表
122
-   字数:约 500 字
123
-
124
-================================================================================
125
-                            修改统计
126
-================================================================================
127
-
128
-代码文件修改:
129
-  - 修改文件:3 个(MarkdownEditor.tsx, chatStore.ts, App.tsx)
130
-  - 新增文件:1 个(MarkdownEditor.css)
131
-  - 删除文件:0 个
132
-  - 总计:4 个代码文件
133
-
134
-文档文件新增:
135
-  - 技术文档:2 个
136
-  - 测试文档:1 个
137
-  - 对比文档:1 个
138
-  - 总结文档:3 个
139
-  - 总计:7 个文档文件
140
-
141
-代码行数变化:
142
-  - 新增:约 130 行
143
-  - 修改:约 50 行
144
-  - 删除:约 30 行
145
-  - 净增:约 150 行
146
-
147
-================================================================================
148
-                          核心功能变更
149
-================================================================================
150
-
151
-✅ 已移除:
152
-  - Markdown 预览面板
153
-  - 预览/编辑模式切换按钮
154
-  - 目录抽屉(Drawer)
155
-  - 目录切换按钮
156
-
157
-✅ 已新增:
158
-  - 固定目录侧边栏(280px)
159
-  - 目录层级样式
160
-  - 目录悬停效果
161
-  - 响应式目录显示
162
-
163
-✅ 已改进:
164
-  - 编辑空间增加约 80%
165
-  - 目录访问便捷性提升
166
-  - 工作流程简化
167
-  - 用户体验优化
168
-
169
-================================================================================
170
-                          重要提示
171
-================================================================================
172
-
173
-⚠️ 破坏性变更:
174
-  - 无破坏性变更
175
-  - 所有 API 保持兼容
176
-  - 组件属性未改变
177
-
178
-⚠️ 用户影响:
179
-  - 不再有预览功能
180
-  - 目录始终显示(桌面设备)
181
-  - 工具栏布局略有变化
182
-
183
-⚠️ 浏览器要求:
184
-  - 支持 CSS Flexbox
185
-  - 支持 CSS Grid(可选)
186
-  - 支持 ES6+
187
-
188
-⚠️ 性能影响:
189
-  - 移除预览渲染,性能可能提升
190
-  - 目录实时更新,CPU 占用略增
191
-  - 整体性能影响:中性偏正面
192
-
193
-================================================================================
194
-                          验证清单
195
-================================================================================
196
-
197
-编译验证:
198
-  ✅ TypeScript 编译通过(npx tsc --noEmit)
199
-  ✅ 无 ESLint 错误
200
-  ✅ 无 TypeScript 类型错误
201
-
202
-功能验证:
203
-  ⏳ 应用启动测试(需手动)
204
-  ⏳ 布局显示测试(需手动)
205
-  ⏳ 目录功能测试(需手动)
206
-  ⏳ 编辑器功能测试(需手动)
207
-  ⏳ 导出功能测试(需手动)
208
-
209
-兼容性验证:
210
-  ⏳ Chrome 测试(需手动)
211
-  ⏳ Firefox 测试(需手动)
212
-  ⏳ Safari 测试(需手动)
213
-  ⏳ Edge 测试(需手动)
214
-
215
-================================================================================
216
-                          快速启动
217
-================================================================================
218
-
219
-1. 启动开发服务器:
220
-   cd ax-frontend-app
221
-   npm run dev
222
-
223
-2. 打开浏览器:
224
-   http://localhost:5173
225
-
226
-3. 查看布局:
227
-   应该看到左侧聊天 + 中间编辑 + 右侧目录的三栏布局
228
-
229
-4. 测试功能:
230
-   - 输入 Markdown 文本
231
-   - 查看右侧目录自动生成
232
-   - 点击目录项跳转
233
-   - 测试保存和导出
234
-
235
-================================================================================
236
-                          相关资源
237
-================================================================================
238
-
239
-详细文档:
240
-  - 设计文档:docs/EDITOR_LAYOUT.md
241
-  - 测试指南:docs/EDITOR_LAYOUT_TEST.md
242
-  - 对比分析:docs/LAYOUT_COMPARISON.md
243
-
244
-快速文档:
245
-  - 更新总结:UPDATE_SUMMARY.md
246
-  - 快速指南:QUICK_START.md
247
-  - 变更日志:CHANGELOG_LAYOUT.md
248
-
249
-在线资源:
250
-  - Toast UI Editor: https://github.com/nhn/tui.editor
251
-  - Ant Design: https://ant.design
252
-  - React 文档: https://react.dev
253
-
254
-================================================================================
255
-                          联系方式
256
-================================================================================
257
-
258
-技术支持:[待填写]
259
-问题反馈:[待填写]
260
-功能建议:[待填写]
261
-
262
-================================================================================
263
-
264
-最后更新:2026-06-24
265
-文档版本:1.0
266
-状态:✅ 开发完成,等待测试
267
-
268
-================================================================================

Різницю між файлами не показано, бо вона завелика
+ 587 - 355
README.md


+ 0 - 456
docs/WORKFLOW_INTEGRATION.md

@@ -1,456 +0,0 @@
1
-# 工作流集成文档 (Workflow Integration)
2
-
3
-## 概述
4
-
5
-本文档描述了外部AI平台工作流与AX文档编辑器前端的集成。通过此集成,用户可以在聊天界面输入自然语言指令(如"生成一个地质报告"),系统会自动触发工作流,生成文档并返回可下载的URL链接。
6
-
7
-## 架构设计
8
-
9
-### 整体流程图
10
-
11
-```
12
-用户输入(聊天)
13
-    ↓
14
-Chat Store (检查是否触发工作流)
15
-    ↓
16
-Workflow Service (调用外部AI平台 v2 API)
17
-    ↓
18
-外部工作流平台 (http://114.242.25.27:3000/api/v2/chat/completions)
19
-    ↓
20
-本地后端 API (http://192.168.0.200:8000/api/v1/export/records)
21
-    ↓
22
-返回导出记录 (Export Record with downloadUrl)
23
-    ↓
24
-在聊天中显示文档卡片
25
-    ↓
26
-用户点击查看/下载文档
27
-```
28
-
29
-## 核心组件
30
-
31
-### 1. Workflow Service (`src/services/workflowService.ts`)
32
-
33
-**作用**: 处理与外部工作流API的通信。
34
-
35
-**关键函数**:
36
-
37
-#### `shouldTriggerWorkflow(input: string): boolean`
38
-判断用户输入是否应该触发文档生成工作流。
39
-
40
-**触发模式**:
41
-- **生成动作**: 生成、创建、制作、编写、generate、create
42
-- **文档类型**: 报告、文档、方案、总结、分析、report、document、doc
43
-
44
-**示例**:
45
-```typescript
46
-shouldTriggerWorkflow("生成一个地质报告") // true
47
-shouldTriggerWorkflow("创建技术文档")     // true
48
-shouldTriggerWorkflow("你好")           // false
49
-```
50
-
51
-#### `triggerDocumentWorkflow(userInput: string, chatId: string)`
52
-调用工作流API生成文档并返回导出记录。
53
-
54
-**API端点**:
55
-```
56
-POST http://114.242.25.27:3000/api/v2/chat/completions
57
-```
58
-
59
-**请求头**:
60
-```
61
-Content-Type: application/json
62
-Authorization: Bearer XAgent-mWHBqQw06psUYRqx6PrHWiKdfY05ebt7I9drDBHzaG9QQesIkVEICj
63
-```
64
-
65
-**请求体**:
66
-```json
67
-{
68
-  "chatId": "session-uuid",
69
-  "stream": false,
70
-  "detail": false,
71
-  "messages": [
72
-    {
73
-      "role": "user",
74
-      "content": "生成一个地质报告"
75
-    }
76
-  ]
77
-}
78
-```
79
-
80
-**响应格式**:
81
-```json
82
-{
83
-  "content": "已为您生成地质报告",
84
-  "exportRecord": {
85
-    "recordId": "rec-e22bbf07350d",
86
-    "userId": "default-user",
87
-    "fileName": "地质报告_1782369497876.doc",
88
-    "fileSize": 39076,
89
-    "downloadUrl": "http://192.168.0.200:8000/api/v1/export/records/rec-e22bbf07350d/download?userId=default-user",
90
-    "documentId": "doc-d2ef5b701b5c",
91
-    "styleId": "default",
92
-    "createdAt": 1782369497764
93
-  }
94
-}
95
-```
96
-
97
-或者数组格式:
98
-```json
99
-{
100
-  "content": "已为您生成地质报告",
101
-  "records": [
102
-    {
103
-      "recordId": "rec-e22bbf07350d",
104
-      ...
105
-    }
106
-  ]
107
-}
108
-```
109
-
110
-### 2. Chat Store (`src/stores/chatStore.ts`)
111
-
112
-**修改内容**:
113
-
114
-#### 导入工作流服务
115
-```typescript
116
-import { shouldTriggerWorkflow, triggerDocumentWorkflow } from '../services/workflowService';
117
-```
118
-
119
-#### 修改 `sendMessage` 函数
120
-```typescript
121
-// 检查是否需要触发工作流
122
-const shouldUseWorkflow = shouldTriggerWorkflow(content);
123
-
124
-if (shouldUseWorkflow) {
125
-  // 使用工作流生成文档
126
-  const workflowResult = await triggerDocumentWorkflow(content, sessionId);
127
-  aiResponse = workflowResult.content;
128
-  exportRecord = workflowResult.exportRecord;
129
-} else {
130
-  // 使用常规AI服务
131
-  const aiResult = await getAIResponse(content, sessionId, get().messages);
132
-  aiResponse = aiResult.response;
133
-}
134
-
135
-// 创建AI消息并附加导出记录
136
-const aiMessage: ChatMessage = {
137
-  id: uuidv4(),
138
-  role: 'assistant',
139
-  content: aiResponse,
140
-  timestamp: Date.now(),
141
-  exportRecord: exportRecord, // 附加导出记录
142
-};
143
-```
144
-
145
-### 3. Message Item Component (`src/components/ChatPanel/MessageItem.tsx`)
146
-
147
-**功能**: 已有支持渲染导出记录为可点击文档卡片。
148
-
149
-当消息中包含 `exportRecord` 时,会显示一个文档卡片:
150
-- Word图标
151
-- 文件名
152
-- "点击预览文档"文本
153
-- 眼睛图标
154
-
155
-用户点击卡片会触发 `onDocumentClick` 回调。
156
-
157
-### 4. App Component (`src/App.tsx`)
158
-
159
-**功能**: 处理文档点击事件,在编辑器中打开文档。
160
-
161
-```typescript
162
-const handleDocumentClick = useCallback((exportRecord: ExportRecordInfo) => {
163
-  setPreviewDocument({
164
-    downloadUrl: exportRecord.downloadUrl,
165
-    fileName: exportRecord.fileName,
166
-    recordId: exportRecord.recordId,
167
-    userId: exportRecord.userId,
168
-    documentId: exportRecord.documentId,
169
-  });
170
-  setPreviewMode('editor');
171
-}, []);
172
-```
173
-
174
-## 配置
175
-
176
-### 环境变量
177
-
178
-在 `.env` 文件中添加:
179
-
180
-```bash
181
-# 工作流API配置
182
-# 工作流API URL(v2 - 带工作流)
183
-VITE_WORKFLOW_API_URL=http://114.242.25.27:3000/api/v2/chat/completions
184
-
185
-# 工作流API密钥
186
-VITE_WORKFLOW_API_KEY=XAgent-mWHBqQw06psUYRqx6PrHWiKdfY05ebt7I9drDBHzaG9QQesIkVEICj
187
-```
188
-
189
-在 `.env.example` 文件中添加(示例):
190
-
191
-```bash
192
-# 工作流API配置
193
-VITE_WORKFLOW_API_URL=http://114.242.25.27:3000/api/v2/chat/completions
194
-VITE_WORKFLOW_API_KEY=your_workflow_api_key_here
195
-```
196
-
197
-## 使用方法
198
-
199
-### 终端用户使用
200
-
201
-1. **打开聊天面板**
202
-2. **输入文档生成请求**,例如:
203
-   - "生成一个地质报告"
204
-   - "创建技术文档"
205
-   - "制作项目分析报告"
206
-   
207
-3. **系统自动处理**:
208
-   - 检测到文档生成请求
209
-   - 触发工作流
210
-   - 调用外部AI平台
211
-   - 生成文档
212
-   - 返回导出记录
213
-   
214
-4. **查看结果**:
215
-   - AI回复消息
216
-   - 文档卡片出现在消息下方
217
-   - 显示文件名和预览按钮
218
-   
219
-5. **点击文档卡片**:
220
-   - 在编辑器中打开文档
221
-   - 或在新标签页下载文档
222
-
223
-### 触发条件示例
224
-
225
-✅ **会触发工作流**:
226
-- "生成一个地质报告"
227
-- "创建技术文档"
228
-- "制作项目分析报告"
229
-- "编写总结文档"
230
-- "generate a report"
231
-
232
-❌ **不会触发工作流**:
233
-- "你好" (无生成动作或文档类型)
234
-- "生成图片" (无文档类型关键词)
235
-- "打开文档" (无生成动作)
236
-- "分析一下数据" (无生成动作)
237
-
238
-## 错误处理
239
-
240
-### 优雅降级
241
-
242
-工作流集成设计为优雅降级:
243
-
244
-1. **API密钥未配置**:
245
-   - 跳过工作流
246
-   - 控制台警告
247
-   - 返回提示消息
248
-
249
-2. **工作流API调用失败**:
250
-   - 错误记录到控制台
251
-   - 返回错误消息给用户
252
-   - 不中断聊天体验
253
-
254
-3. **工作流未返回导出记录**:
255
-   - 控制台警告
256
-   - 显示AI响应
257
-   - 不显示文档卡片
258
-
259
-### 调试日志
260
-
261
-在浏览器控制台可以看到工作流执行日志:
262
-
263
-- `🔄 Using workflow for document generation` - 开始使用工作流
264
-- `🔄 Triggering document generation workflow...` - 触发工作流
265
-- `📝 User input: 生成一个地质报告` - 用户输入
266
-- `✅ Workflow response received:` - 收到工作流响应
267
-- `📄 Export record extracted:` - 提取导出记录
268
-- `⚠️ No export record found in workflow response` - 未找到导出记录
269
-- `❌ Workflow API error:` - 工作流API错误
270
-
271
-## 测试
272
-
273
-### 手动测试步骤
274
-
275
-1. **启动开发服务器**:
276
-   ```bash
277
-   cd d:\work space\ax-shell\ax-frontend-app
278
-   npm run dev
279
-   ```
280
-
281
-2. **打开应用**: 浏览器访问 `http://localhost:5173`
282
-
283
-3. **创建新会话**: 点击聊天面板
284
-
285
-4. **输入文档生成请求**:
286
-   ```
287
-   生成一个地质报告
288
-   ```
289
-
290
-5. **验证工作流**:
291
-   - 检查浏览器控制台日志
292
-   - 等待AI响应
293
-   - 查看消息下方的文档卡片
294
-   - 验证文件名显示正确
295
-
296
-6. **测试文档下载**:
297
-   - 点击文档卡片
298
-   - 文档应在编辑器中打开
299
-   - 或在新标签页下载
300
-
301
-### 预期行为
302
-
303
-1. 用户输入:"生成一个地质报告"
304
-2. 聊天显示加载指示器
305
-3. 工作流在后台触发
306
-4. AI响应显示
307
-5. 文档卡片出现,显示:
308
-   - 文件图标
309
-   - 文件名(例如:"地质报告_1782369497876.doc")
310
-   - "点击预览文档"文本
311
-6. 点击卡片在编辑器中打开文档
312
-7. 显示成功提示:"✅ 文档已生成,点击下方卡片查看"
313
-
314
-## 后端集成
315
-
316
-### 导出记录API
317
-
318
-工作流内部调用本地后端导出记录API:
319
-
320
-```
321
-GET http://192.168.0.200:8000/api/v1/export/records?userId=default-user&page=1&pageSize=20
322
-```
323
-
324
-此API返回生成的导出记录列表,工作流将其转发给前端。
325
-
326
-### 文档下载
327
-
328
-文档通过以下端点下载:
329
-
330
-```
331
-GET http://192.168.0.200:8000/api/v1/export/records/{recordId}/download?userId={userId}
332
-```
333
-
334
-## 工作流平台配置
335
-
336
-### 工作流设置
337
-
338
-在外部AI平台 (http://114.242.25.27:3000) 中,工作流应该配置为:
339
-
340
-1. **接收用户输入**: 从聊天消息中提取文档生成请求
341
-2. **处理请求**: 根据请求类型(地质报告、技术文档等)生成文档
342
-3. **调用后端API**: 调用 `http://192.168.0.200:8000/api/v1/export/records`
343
-4. **返回结果**: 将导出记录数据返回给前端
344
-
345
-### API响应格式
346
-
347
-工作流API应该返回以下格式之一:
348
-
349
-**格式1:单个导出记录**
350
-```json
351
-{
352
-  "content": "AI响应文本",
353
-  "exportRecord": {
354
-    "recordId": "rec-xxx",
355
-    "fileName": "地质报告_xxx.doc",
356
-    "downloadUrl": "http://192.168.0.200:8000/api/v1/export/records/rec-xxx/download?userId=xxx",
357
-    "documentId": "doc-xxx",
358
-    "userId": "default-user",
359
-    "fileSize": 39076,
360
-    "styleId": "default",
361
-    "createdAt": 1782369497764
362
-  }
363
-}
364
-```
365
-
366
-**格式2:导出记录数组**
367
-```json
368
-{
369
-  "content": "AI响应文本",
370
-  "records": [
371
-    {
372
-      "recordId": "rec-xxx",
373
-      "fileName": "地质报告_xxx.doc",
374
-      "downloadUrl": "http://...",
375
-      ...
376
-    }
377
-  ]
378
-}
379
-```
380
-
381
-## 安全考虑
382
-
383
-1. **API密钥存储**: 
384
-   - 存储在环境变量中
385
-   - 不提交到版本控制
386
-   - `.env.example` 中使用占位符
387
-
388
-2. **身份验证**:
389
-   - 使用 `Bearer` token 进行工作流认证
390
-   - 每个请求包含用户ID用于授权
391
-
392
-3. **CORS**:
393
-   - 工作流API必须允许来自前端源的CORS
394
-   - 后端API必须允许来自前端源的CORS
395
-
396
-## 故障排查
397
-
398
-### 问题:工作流未触发
399
-
400
-**检查**:
401
-1. `.env` 中是否设置了 `VITE_WORKFLOW_API_KEY`?
402
-2. 输入是否匹配触发模式?
403
-3. 检查浏览器控制台错误
404
-4. 验证工作流API可访问性
405
-
406
-### 问题:未显示文档链接
407
-
408
-**检查**:
409
-1. 工作流是否返回了记录?检查控制台日志
410
-2. `exportRecord` 是否正确附加到消息?
411
-3. 检查网络标签中的工作流API响应
412
-4. 验证响应格式是否匹配预期结构
413
-
414
-### 问题:文档下载失败
415
-
416
-**检查**:
417
-1. 后端API是否运行?
418
-2. 下载URL是否包含正确的记录ID?
419
-3. 检查后端日志错误
420
-4. 验证文件在服务器上存在
421
-
422
-## 未来增强
423
-
424
-1. **多文档生成**: 
425
-   - 支持一次请求生成多个文档
426
-   - 显示多个文档卡片
427
-
428
-2. **文档模板**:
429
-   - 允许用户在生成前选择模板
430
-   - 将模板ID传递给工作流
431
-
432
-3. **进度指示**:
433
-   - 显示文档生成进度条
434
-   - 显示预计剩余时间
435
-
436
-4. **重试逻辑**:
437
-   - 工作流失败时自动重试
438
-   - 指数退避策略
439
-
440
-5. **文档预览**:
441
-   - 聊天中内联文档预览
442
-   - 下载前预览
443
-
444
-## 相关文档
445
-
446
-- [导出记录API](../../ax-backend-shell/docs/text-editor-backend-api.md)
447
-- [聊天面板架构](./text-editor-architecture.md)
448
-- [导出服务设计](./export-doc-content-mapping.md)
449
-
450
-## 版本历史
451
-
452
-- **v1.0** (2026-06-25): 工作流集成初始实现
453
-  - 添加工作流服务
454
-  - 集成到聊天store
455
-  - 更新消息渲染
456
-  - 添加环境配置

+ 0 - 562
docs/WORKFLOW_INTEGRATION_SUMMARY.md

@@ -1,562 +0,0 @@
1
-# Workflow Integration Summary
2
-
3
-## 概述
4
-
5
-本文档说明了如何将外部 AI 工作流平台与前端聊天功能集成,实现"输入文字 → 生成文档 → 返回链接"的完整流程。
6
-
7
----
8
-
9
-## 功能流程
10
-
11
-```
12
-用户输入: "生成一个地质报告"
13
-    ↓
14
-前端检测关键词 (chatStore.ts)
15
-    ↓
16
-调用工作流 API (workflowService.ts)
17
-    ↓
18
-工作流平台处理
19
-    ↓
20
-工作流调用本地后端 API
21
-    ↓
22
-返回导出记录 JSON
23
-    ↓
24
-前端解析并显示文档卡片 (MessageItem.tsx)
25
-    ↓
26
-用户点击卡片
27
-    ↓
28
-打开文档预览 (DocumentViewerLayout.tsx)
29
-```
30
-
31
----
32
-
33
-## 核心组件
34
-
35
-### 1. 环境变量配置 (.env)
36
-
37
-```env
38
-# 本地后端 API
39
-VITE_API_BASE_URL=http://192.168.0.195:8000
40
-
41
-# 工作流 API
42
-VITE_WORKFLOW_API_URL=http://114.242.25.27:3000/api/v2/chat/completions
43
-VITE_WORKFLOW_API_KEY=XAgent-mWHBqQw06psUYRqx6PrHWiKdfY05ebt7I9drDBHzaG9QQesIkVEICj
44
-```
45
-
46
-**重要**: 修改 `.env` 后必须重启开发服务器!
47
-
48
-```bash
49
-# 停止服务器
50
-Ctrl+C
51
-
52
-# 重新启动
53
-npm run dev
54
-
55
-# 清除浏览器缓存
56
-Ctrl+Shift+Delete
57
-```
58
-
59
----
60
-
61
-### 2. 工作流服务 (workflowService.ts)
62
-
63
-**位置**: `src/services/workflowService.ts`
64
-
65
-**功能**:
66
-- 检测用户输入是否需要生成文档 (`shouldTriggerWorkflow`)
67
-- 调用外部工作流 API (`triggerDocumentWorkflow`)
68
-- 解析返回的导出记录信息
69
-- 提取文档类型 (`extractReportType`)
70
-
71
-**关键函数**:
72
-
73
-```typescript
74
-// 检测是否应该触发工作流
75
-export const shouldTriggerWorkflow = (input: string): boolean => {
76
-  const lowerInput = input.toLowerCase().trim();
77
-  
78
-  const generatePatterns = ['生成', '创建', '制作', '编写'];
79
-  const documentPatterns = ['报告', '文档', '方案', '总结'];
80
-  
81
-  const hasGenerateAction = generatePatterns.some(p => lowerInput.includes(p));
82
-  const hasDocumentType = documentPatterns.some(p => lowerInput.includes(p));
83
-  
84
-  return hasGenerateAction && hasDocumentType;
85
-};
86
-
87
-// 调用工作流生成文档
88
-export const triggerDocumentWorkflow = async (
89
-  userInput: string,
90
-  chatId: string
91
-): Promise<{ content: string; exportRecord?: ExportRecordInfo }> => {
92
-  // 调用外部 API
93
-  const response = await fetch(config.apiUrl, {
94
-    method: 'POST',
95
-    headers: {
96
-      'Content-Type': 'application/json',
97
-      Authorization: `Bearer ${config.apiKey}`,
98
-    },
99
-    body: JSON.stringify({
100
-      chatId,
101
-      stream: false,
102
-      detail: false,
103
-      messages: [{ role: 'user', content: userInput }],
104
-    }),
105
-  });
106
-  
107
-  const data = await response.json();
108
-  
109
-  // 解析导出记录
110
-  // 可能在 data.content (JSON字符串)
111
-  // 或 data.exportRecord (对象)
112
-  // 或 data.records (数组)
113
-  
114
-  return { content, exportRecord };
115
-};
116
-```
117
-
118
----
119
-
120
-### 3. 聊天状态管理 (chatStore.ts)
121
-
122
-**位置**: `src/stores/chatStore.ts`
123
-
124
-**集成点**: `sendMessage` 函数
125
-
126
-```typescript
127
-sendMessage: async (content: string) => {
128
-  // 创建用户消息
129
-  const userMessage = { ... };
130
-  
131
-  // 检查是否需要触发工作流
132
-  const shouldUseWorkflow = shouldTriggerWorkflow(content);
133
-  
134
-  let aiResponse: string;
135
-  let exportRecord: ExportRecordInfo | undefined;
136
-  
137
-  if (shouldUseWorkflow) {
138
-    // 使用工作流生成文档
139
-    const result = await triggerDocumentWorkflow(content, sessionId);
140
-    aiResponse = result.content;
141
-    exportRecord = result.exportRecord;
142
-  } else {
143
-    // 使用普通 AI 聊天
144
-    const result = await getAIResponse(content, sessionId, messages);
145
-    aiResponse = result.response;
146
-  }
147
-  
148
-  // 创建 AI 消息(带有 exportRecord)
149
-  const aiMessage = {
150
-    id: uuidv4(),
151
-    role: 'assistant',
152
-    content: aiResponse,
153
-    timestamp: Date.now(),
154
-    exportRecord: exportRecord,  // 关键!
155
-  };
156
-  
157
-  // 添加到消息列表
158
-  set(state => ({
159
-    messages: [...state.messages, aiMessage],
160
-  }));
161
-  
162
-  // 如果有文档生成,显示成功提示
163
-  if (exportRecord) {
164
-    message.success('✅ 文档已生成,点击下方卡片查看');
165
-  }
166
-}
167
-```
168
-
169
----
170
-
171
-### 4. 消息显示组件 (MessageItem.tsx)
172
-
173
-**位置**: `src/components/ChatPanel/MessageItem.tsx`
174
-
175
-**功能**:
176
-- 显示聊天消息气泡
177
-- 如果消息包含 `exportRecord`,显示可点击的文档卡片
178
-- 点击卡片触发 `onDocumentClick` 回调
179
-
180
-```typescript
181
-const MessageItem: React.FC<MessageItemProps> = ({ 
182
-  message, 
183
-  onDocumentClick 
184
-}) => {
185
-  const { content, exportRecord } = message;
186
-  
187
-  return (
188
-    <div>
189
-      {/* 消息气泡 */}
190
-      <div>{content}</div>
191
-      
192
-      {/* 文档卡片(如果有 exportRecord) */}
193
-      {exportRecord && (
194
-        <Card
195
-          onClick={() => onDocumentClick?.(exportRecord)}
196
-          hoverable
197
-        >
198
-          <FileWordOutlined />
199
-          <div>
200
-            <Text>📄 {exportRecord.fileName}</Text>
201
-            <Text>点击预览文档</Text>
202
-          </div>
203
-          <EyeOutlined />
204
-        </Card>
205
-      )}
206
-    </div>
207
-  );
208
-};
209
-```
210
-
211
----
212
-
213
-### 5. 文档内容解析 (documentContentService.ts)
214
-
215
-**位置**: `src/services/documentContentService.ts`
216
-
217
-**更新内容**: 现在真实下载并解析 Word 文档
218
-
219
-```typescript
220
-export const fetchDocumentStructure = async (
221
-  recordId: string,
222
-  userId: string
223
-): Promise<DocumentStructure> => {
224
-  // 1. 构建下载 URL
225
-  const baseUrl = import.meta.env.VITE_API_BASE_URL;
226
-  const downloadUrl = `${baseUrl}/api/v1/export/records/${recordId}/download?userId=${userId}`;
227
-  
228
-  // 2. 下载 Word 文件
229
-  const response = await fetch(downloadUrl);
230
-  const blob = await response.blob();
231
-  const arrayBuffer = await blob.arrayBuffer();
232
-  
233
-  // 3. 使用 mammoth.js 解析
234
-  const mammoth = await import('mammoth');
235
-  const result = await mammoth.convertToHtml({ arrayBuffer });
236
-  
237
-  // 4. 解析 HTML 提取结构
238
-  const parser = new DOMParser();
239
-  const doc = parser.parseFromString(result.value, 'text/html');
240
-  
241
-  // 5. 提取标题、段落、表格
242
-  const outline: OutlineItem[] = [];
243
-  const content: ContentBlock[] = [];
244
-  
245
-  doc.body.querySelectorAll('*').forEach(element => {
246
-    if (element.tagName.match(/^H[1-6]$/)) {
247
-      // 处理标题
248
-    } else if (element.tagName === 'P') {
249
-      // 处理段落
250
-    } else if (element.tagName === 'TABLE') {
251
-      // 处理表格
252
-    }
253
-  });
254
-  
255
-  return { recordId, fileName, outline, content, metadata };
256
-};
257
-```
258
-
259
-**依赖**: mammoth.js (已在 package.json 中安装)
260
-
261
----
262
-
263
-## 工作流 API 响应格式
264
-
265
-工作流平台可能返回以下格式之一:
266
-
267
-### 格式 1: JSON 字符串在 content 中
268
-
269
-```json
270
-{
271
-  "content": "{\"records\":[{\"recordId\":\"rec-xxx\",\"fileName\":\"地质报告.doc\",\"downloadUrl\":\"http://...\",\"documentId\":\"doc-xxx\"}]}",
272
-  "choices": [...]
273
-}
274
-```
275
-
276
-### 格式 2: 顶层 exportRecord 对象
277
-
278
-```json
279
-{
280
-  "content": "文档已生成",
281
-  "exportRecord": {
282
-    "recordId": "rec-xxx",
283
-    "fileName": "地质报告.doc",
284
-    "downloadUrl": "http://...",
285
-    "documentId": "doc-xxx"
286
-  }
287
-}
288
-```
289
-
290
-### 格式 3: 顶层 records 数组
291
-
292
-```json
293
-{
294
-  "content": "文档已生成",
295
-  "records": [
296
-    {
297
-      "recordId": "rec-xxx",
298
-      "fileName": "地质报告.doc",
299
-      "downloadUrl": "http://...",
300
-      "documentId": "doc-xxx"
301
-    }
302
-  ]
303
-}
304
-```
305
-
306
-**workflowService.ts 会自动处理所有这些格式!**
307
-
308
----
309
-
310
-## 后端 API 端点
311
-
312
-### 1. 获取导出记录列表
313
-
314
-```
315
-GET http://192.168.0.195:8000/api/v1/export/records?userId=default-user&page=1&pageSize=20
316
-```
317
-
318
-**响应**:
319
-```json
320
-{
321
-  "records": [
322
-    {
323
-      "recordId": "rec-xxx",
324
-      "userId": "default-user",
325
-      "fileName": "地质报告.doc",
326
-      "fileSize": 39076,
327
-      "downloadUrl": "http://192.168.0.195:8000/api/v1/export/records/rec-xxx/download?userId=default-user",
328
-      "documentId": "doc-xxx",
329
-      "styleId": "default",
330
-      "createdAt": 1782369497764
331
-    }
332
-  ],
333
-  "pagination": { ... }
334
-}
335
-```
336
-
337
-### 2. 下载文档
338
-
339
-```
340
-GET http://192.168.0.195:8000/api/v1/export/records/{recordId}/download?userId={userId}
341
-```
342
-
343
-**响应**: Word 文档二进制数据 (application/vnd.openxmlformats-officedocument.wordprocessingml.document)
344
-
345
----
346
-
347
-## 类型定义
348
-
349
-### ExportRecordInfo
350
-
351
-```typescript
352
-export interface ExportRecordInfo {
353
-  recordId: string;      // 导出记录 ID
354
-  fileName: string;      // 文件名
355
-  downloadUrl: string;   // 下载 URL
356
-  documentId: string;    // 文档 ID
357
-}
358
-```
359
-
360
-### ChatMessage
361
-
362
-```typescript
363
-export interface ChatMessage {
364
-  id: string;
365
-  role: 'user' | 'assistant';
366
-  content: string;
367
-  timestamp: number;
368
-  documentId?: string;
369
-  exportRecord?: ExportRecordInfo;  // 关键字段!
370
-}
371
-```
372
-
373
----
374
-
375
-## 测试流程
376
-
377
-### 1. 启动后端服务
378
-
379
-```bash
380
-cd ax-backend-shell
381
-python -m uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
382
-```
383
-
384
-确保后端运行在 `http://192.168.0.195:8000`
385
-
386
-### 2. 启动前端服务
387
-
388
-```bash
389
-cd ax-frontend-app
390
-npm run dev
391
-```
392
-
393
-前端运行在 `http://localhost:5173`
394
-
395
-### 3. 测试工作流集成
396
-
397
-在聊天面板输入:
398
-
399
-- ✅ "生成一个地质报告" → 应触发工作流
400
-- ✅ "创建技术文档" → 应触发工作流
401
-- ✅ "制作项目方案" → 应触发工作流
402
-- ❌ "你好" → 不触发工作流,普通聊天
403
-- ❌ "什么是地质报告" → 不触发工作流,普通聊天
404
-
405
-### 4. 验证文档显示
406
-
407
-1. 触发工作流后,应该看到:
408
-   - AI 回复消息
409
-   - 下方显示文档卡片
410
-   - 卡片包含文件名和"点击预览文档"
411
-
412
-2. 点击文档卡片后:
413
-   - 打开文档查看器
414
-   - 左侧显示文档大纲
415
-   - 中间显示文档内容(标题、段落、表格)
416
-   - 右侧显示写入/预览切换
417
-
418
----
419
-
420
-## 常见问题
421
-
422
-### Q1: 修改 .env 后没有生效?
423
-
424
-**A**: Vite 不会热重载环境变量,必须重启开发服务器:
425
-
426
-```bash
427
-# 停止
428
-Ctrl+C
429
-
430
-# 启动
431
-npm run dev
432
-
433
-# 清除浏览器缓存
434
-Ctrl+Shift+Delete → 清除缓存和 Cookie → 清除数据
435
-```
436
-
437
-### Q2: 工作流没有被触发?
438
-
439
-**A**: 检查关键词匹配逻辑:
440
-
441
-```typescript
442
-// 必须同时包含"生成类"词汇和"文档类"词汇
443
-生成 + 报告 ✅
444
-创建 + 文档 ✅
445
-制作 + 方案 ✅
446
-生成 + 什么 ❌  (没有文档类词汇)
447
-报告 + 是什么 ❌  (没有生成类词汇)
448
-```
449
-
450
-### Q3: 文档内容显示为空?
451
-
452
-**A**: 可能的原因:
453
-
454
-1. **网络问题**: 检查后端 API 是否可访问
455
-   ```bash
456
-   curl http://192.168.0.195:8000/api/v1/export/records/{recordId}/download?userId=default-user
457
-   ```
458
-
459
-2. **CORS 问题**: 检查浏览器控制台是否有 CORS 错误
460
-
461
-3. **文档格式问题**: mammoth.js 只支持 .docx 格式,不支持旧的 .doc 格式
462
-
463
-### Q4: 工作流返回的数据格式不对?
464
-
465
-**A**: workflowService.ts 支持多种格式:
466
-
467
-1. JSON 字符串在 `content` 字段
468
-2. 对象在 `exportRecord` 字段
469
-3. 数组在 `records` 字段
470
-
471
-如果都不匹配,检查实际返回数据:
472
-
473
-```typescript
474
-console.log('[workflowService] Response:', data);
475
-```
476
-
477
----
478
-
479
-## 架构图
480
-
481
-```
482
-┌─────────────────────────────────────────────────────────────────┐
483
-│                         Frontend (React)                         │
484
-├─────────────────────────────────────────────────────────────────┤
485
-│                                                                  │
486
-│  ┌──────────────┐      ┌──────────────┐     ┌─────────────┐   │
487
-│  │  ChatPanel   │─────▶│  chatStore   │────▶│ MessageItem │   │
488
-│  │  (用户输入)   │      │  (状态管理)   │     │ (显示卡片)   │   │
489
-│  └──────────────┘      └──────┬───────┘     └─────────────┘   │
490
-│                               │                                 │
491
-│                               ▼                                 │
492
-│                    ┌──────────────────┐                        │
493
-│                    │ workflowService  │                        │
494
-│                    │ (检测&调用工作流) │                        │
495
-│                    └─────────┬────────┘                        │
496
-└──────────────────────────────┼─────────────────────────────────┘
497
-                               │
498
-                               ▼
499
-┌─────────────────────────────────────────────────────────────────┐
500
-│              External AI Platform (工作流平台)                    │
501
-│  http://114.242.25.27:3000/api/v2/chat/completions             │
502
-├─────────────────────────────────────────────────────────────────┤
503
-│                                                                  │
504
-│  1. 接收用户请求                                                  │
505
-│  2. 生成文档内容                                                  │
506
-│  3. 调用本地后端 API                                              │
507
-│  4. 返回导出记录                                                  │
508
-│                                                                  │
509
-└──────────────────────────────┬──────────────────────────────────┘
510
-                               │
511
-                               ▼
512
-┌─────────────────────────────────────────────────────────────────┐
513
-│                Local Backend API (FastAPI)                      │
514
-│             http://192.168.0.195:8000                           │
515
-├─────────────────────────────────────────────────────────────────┤
516
-│                                                                  │
517
-│  GET  /api/v1/export/records                                    │
518
-│  GET  /api/v1/export/records/{recordId}/download                │
519
-│  POST /api/v1/export                                            │
520
-│                                                                  │
521
-└─────────────────────────────────────────────────────────────────┘
522
-```
523
-
524
----
525
-
526
-## 文件清单
527
-
528
-### 核心文件
529
-
530
-- ✅ `.env` - 环境变量配置
531
-- ✅ `src/services/workflowService.ts` - 工作流服务
532
-- ✅ `src/stores/chatStore.ts` - 聊天状态管理
533
-- ✅ `src/components/ChatPanel/MessageItem.tsx` - 消息显示
534
-- ✅ `src/services/documentContentService.ts` - 文档内容解析
535
-- ✅ `src/types/chat.ts` - 类型定义
536
-
537
-### 文档
538
-
539
-- ✅ `docs/WORKFLOW_INTEGRATION_SUMMARY.md` - 本文档
540
-
541
----
542
-
543
-## 下一步改进
544
-
545
-1. **错误处理增强**
546
-   - 工作流 API 失败时的友好提示
547
-   - 文档解析失败时的降级方案
548
-
549
-2. **性能优化**
550
-   - 文档缓存机制
551
-   - 大文档分页加载
552
-
553
-3. **功能扩展**
554
-   - 支持更多文档格式 (PDF, Excel)
555
-   - 文档编辑功能
556
-   - 版本历史
557
-
558
----
559
-
560
-**文档版本**: 1.0  
561
-**最后更新**: 2026-06-25  
562
-**维护者**: AX Team

+ 0 - 334
public/check-duplication.html

@@ -1,334 +0,0 @@
1
-<!DOCTYPE html>
2
-<html lang="zh-CN">
3
-<head>
4
-    <meta charset="UTF-8">
5
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
-    <title>文档重复检查工具</title>
7
-    <style>
8
-        body {
9
-            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10
-            max-width: 1200px;
11
-            margin: 0 auto;
12
-            padding: 20px;
13
-            background: #f5f5f5;
14
-        }
15
-        .container {
16
-            background: white;
17
-            padding: 30px;
18
-            border-radius: 8px;
19
-            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
20
-        }
21
-        h1 {
22
-            color: #333;
23
-            border-bottom: 3px solid #1890ff;
24
-            padding-bottom: 10px;
25
-        }
26
-        .section {
27
-            margin: 20px 0;
28
-            padding: 15px;
29
-            background: #fafafa;
30
-            border-left: 4px solid #1890ff;
31
-            border-radius: 4px;
32
-        }
33
-        .input-group {
34
-            margin: 15px 0;
35
-        }
36
-        label {
37
-            display: block;
38
-            margin-bottom: 5px;
39
-            font-weight: 600;
40
-            color: #555;
41
-        }
42
-        input, select {
43
-            width: 100%;
44
-            padding: 10px;
45
-            border: 1px solid #d9d9d9;
46
-            border-radius: 4px;
47
-            font-size: 14px;
48
-            box-sizing: border-box;
49
-        }
50
-        button {
51
-            background: #1890ff;
52
-            color: white;
53
-            border: none;
54
-            padding: 12px 24px;
55
-            border-radius: 4px;
56
-            cursor: pointer;
57
-            font-size: 14px;
58
-            font-weight: 600;
59
-            margin-right: 10px;
60
-            margin-top: 10px;
61
-        }
62
-        button:hover {
63
-            background: #40a9ff;
64
-        }
65
-        button:disabled {
66
-            background: #d9d9d9;
67
-            cursor: not-allowed;
68
-        }
69
-        .results {
70
-            margin-top: 20px;
71
-        }
72
-        .result-box {
73
-            background: white;
74
-            border: 1px solid #e8e8e8;
75
-            padding: 15px;
76
-            margin: 10px 0;
77
-            border-radius: 4px;
78
-            white-space: pre-wrap;
79
-            font-family: 'Courier New', monospace;
80
-            font-size: 12px;
81
-            max-height: 400px;
82
-            overflow-y: auto;
83
-        }
84
-        .success {
85
-            color: #52c41a;
86
-            font-weight: 600;
87
-        }
88
-        .error {
89
-            color: #ff4d4f;
90
-            font-weight: 600;
91
-        }
92
-        .warning {
93
-            color: #faad14;
94
-            font-weight: 600;
95
-        }
96
-        .info {
97
-            color: #1890ff;
98
-            font-weight: 600;
99
-        }
100
-        .stat {
101
-            display: inline-block;
102
-            background: #e6f7ff;
103
-            padding: 5px 15px;
104
-            margin: 5px 5px 5px 0;
105
-            border-radius: 20px;
106
-            font-size: 13px;
107
-            font-weight: 600;
108
-            color: #0050b3;
109
-        }
110
-    </style>
111
-</head>
112
-<body>
113
-    <div class="container">
114
-        <h1>🔍 文档重复检查工具</h1>
115
-        
116
-        <div class="section">
117
-            <h3>步骤 1: 从数据库获取文档</h3>
118
-            <div class="input-group">
119
-                <label>后端 API 地址:</label>
120
-                <input type="text" id="apiBase" value="http://192.168.0.195:8000" />
121
-            </div>
122
-            <div class="input-group">
123
-                <label>文档 ID:</label>
124
-                <input type="text" id="documentId" placeholder="例如: doc-7791bc371ad7" />
125
-            </div>
126
-            <button onclick="fetchDocument()">获取文档</button>
127
-            <button onclick="fetchLatestDocument()">获取最新文档</button>
128
-        </div>
129
-
130
-        <div class="section">
131
-            <h3>步骤 2: 检查导出的Word文件</h3>
132
-            <div class="input-group">
133
-                <label>导出记录 ID:</label>
134
-                <input type="text" id="recordId" placeholder="例如: rec-xxx" />
135
-            </div>
136
-            <div class="input-group">
137
-                <label>用户 ID:</label>
138
-                <input type="text" id="userId" value="default-user" />
139
-            </div>
140
-            <button onclick="checkExportedWord()">下载并检查Word文件</button>
141
-        </div>
142
-
143
-        <div id="results" class="results"></div>
144
-    </div>
145
-
146
-    <script>
147
-        const API_BASE = () => document.getElementById('apiBase').value;
148
-
149
-        function showResult(message, type = 'info') {
150
-            const results = document.getElementById('results');
151
-            const box = document.createElement('div');
152
-            box.className = 'result-box';
153
-            box.innerHTML = `<span class="${type}">[${type.toUpperCase()}]</span> ${message}`;
154
-            results.appendChild(box);
155
-            results.scrollTop = results.scrollHeight;
156
-        }
157
-
158
-        async function fetchLatestDocument() {
159
-            showResult('正在获取最新文档...', 'info');
160
-            
161
-            try {
162
-                const response = await fetch(`${API_BASE()}/api/v1/documents?userId=default-user&page=1&pageSize=1&sortBy=createdAt&sortOrder=desc`);
163
-                
164
-                if (!response.ok) {
165
-                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
166
-                }
167
-
168
-                const data = await response.json();
169
-                
170
-                if (data.code === 200 && data.data.documents.length > 0) {
171
-                    const doc = data.data.documents[0];
172
-                    document.getElementById('documentId').value = doc.id;
173
-                    showResult(`最新文档 ID: ${doc.id}`, 'success');
174
-                    
175
-                    // 自动获取文档详情
176
-                    await fetchDocument();
177
-                } else {
178
-                    showResult('未找到文档', 'warning');
179
-                }
180
-            } catch (error) {
181
-                showResult(`错误: ${error.message}`, 'error');
182
-            }
183
-        }
184
-
185
-        async function fetchDocument() {
186
-            const docId = document.getElementById('documentId').value;
187
-            
188
-            if (!docId) {
189
-                showResult('请输入文档ID', 'warning');
190
-                return;
191
-            }
192
-
193
-            showResult(`正在获取文档 ${docId}...`, 'info');
194
-            
195
-            try {
196
-                const response = await fetch(`${API_BASE()}/api/v1/documents/${docId}`);
197
-                
198
-                if (!response.ok) {
199
-                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
200
-                }
201
-
202
-                const data = await response.json();
203
-                
204
-                if (data.code === 200) {
205
-                    const doc = data.data;
206
-                    const content = doc.content;
207
-                    
208
-                    showResult(`文档ID: ${doc.id}`, 'info');
209
-                    showResult(`格式: ${doc.format}`, 'info');
210
-                    showResult(`创建时间: ${new Date(doc.createdAt).toLocaleString()}`, 'info');
211
-                    
212
-                    // 统计信息
213
-                    const totalLength = content.length;
214
-                    const lines = content.split('\n');
215
-                    const totalLines = lines.length;
216
-                    
217
-                    showResult(`
218
-<span class="stat">总长度: ${totalLength} 字符</span>
219
-<span class="stat">总行数: ${totalLines} 行</span>
220
-                    `, 'info');
221
-                    
222
-                    // 检查是否有重复
223
-                    const halfLength = Math.floor(totalLength / 2);
224
-                    const firstHalf = content.substring(0, halfLength);
225
-                    const secondHalf = content.substring(halfLength);
226
-                    
227
-                    // 检查前500字符是否在后半部分重复出现
228
-                    const sample = content.substring(0, Math.min(500, halfLength));
229
-                    const isDuplicated = secondHalf.includes(sample);
230
-                    
231
-                    if (isDuplicated) {
232
-                        showResult('⚠️ 警告: 检测到内容可能重复!前500字符在后半部分也出现了', 'warning');
233
-                    } else {
234
-                        showResult('✓ 数据库内容没有重复', 'success');
235
-                    }
236
-                    
237
-                    // 显示前1000字符
238
-                    showResult('数据库中的前1000字符:\n' + content.substring(0, 1000), 'info');
239
-                    
240
-                    // 显示后500字符
241
-                    showResult('数据库中的后500字符:\n' + content.substring(totalLength - 500), 'info');
242
-                    
243
-                } else {
244
-                    showResult(`API错误: ${data.message}`, 'error');
245
-                }
246
-            } catch (error) {
247
-                showResult(`错误: ${error.message}`, 'error');
248
-            }
249
-        }
250
-
251
-        async function checkExportedWord() {
252
-            const recordId = document.getElementById('recordId').value;
253
-            const userId = document.getElementById('userId').value;
254
-            
255
-            if (!recordId || !userId) {
256
-                showResult('请输入导出记录ID和用户ID', 'warning');
257
-                return;
258
-            }
259
-
260
-            showResult(`正在下载并检查Word文件...`, 'info');
261
-            
262
-            try {
263
-                const downloadUrl = `${API_BASE()}/api/v1/export/records/${recordId}/download?userId=${encodeURIComponent(userId)}`;
264
-                showResult(`下载URL: ${downloadUrl}`, 'info');
265
-                
266
-                const response = await fetch(downloadUrl);
267
-                
268
-                if (!response.ok) {
269
-                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
270
-                }
271
-
272
-                const blob = await response.blob();
273
-                const arrayBuffer = await blob.arrayBuffer();
274
-                
275
-                showResult(`Word文件大小: ${arrayBuffer.byteLength} bytes`, 'info');
276
-                
277
-                // 使用mammoth解析Word文件
278
-                showResult('正在解析Word文档...', 'info');
279
-                
280
-                // 动态加载mammoth库
281
-                if (typeof mammoth === 'undefined') {
282
-                    const script = document.createElement('script');
283
-                    script.src = 'https://cdn.jsdelivr.net/npm/mammoth@1.6.0/mammoth.browser.min.js';
284
-                    document.head.appendChild(script);
285
-                    
286
-                    await new Promise((resolve, reject) => {
287
-                        script.onload = resolve;
288
-                        script.onerror = reject;
289
-                    });
290
-                }
291
-                
292
-                const result = await mammoth.convertToHtml({ arrayBuffer });
293
-                const html = result.value;
294
-                
295
-                showResult(`HTML长度: ${html.length} 字符`, 'info');
296
-                
297
-                // 提取纯文本
298
-                const parser = new DOMParser();
299
-                const doc = parser.parseFromString(html, 'text/html');
300
-                const text = doc.body.textContent || '';
301
-                
302
-                showResult(`纯文本长度: ${text.length} 字符`, 'info');
303
-                
304
-                // 检查是否有重复
305
-                const lines = text.split('\n').filter(line => line.trim());
306
-                const halfPoint = Math.floor(lines.length / 2);
307
-                
308
-                let matchCount = 0;
309
-                for (let i = 0; i < Math.min(10, halfPoint); i++) {
310
-                    if (lines[i] === lines[halfPoint + i]) {
311
-                        matchCount++;
312
-                    }
313
-                }
314
-                
315
-                if (matchCount >= 8) {
316
-                    showResult(`⚠️ 警告: Word文件中检测到重复内容!前10行有${matchCount}行与后半部分匹配`, 'warning');
317
-                } else {
318
-                    showResult('✓ Word文件内容没有明显重复', 'success');
319
-                }
320
-                
321
-                // 显示前1000字符
322
-                showResult('Word文件的前1000字符:\n' + text.substring(0, 1000), 'info');
323
-                
324
-            } catch (error) {
325
-                showResult(`错误: ${error.message}`, 'error');
326
-                console.error(error);
327
-            }
328
-        }
329
-
330
-        // 初始化
331
-        showResult('工具已就绪。请使用上面的按钮检查文档。', 'success');
332
-    </script>
333
-</body>
334
-</html>

+ 0 - 199
public/test-api.html

@@ -1,199 +0,0 @@
1
-<!DOCTYPE html>
2
-<html lang="zh-CN">
3
-<head>
4
-    <meta charset="UTF-8">
5
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
-    <title>API 测试</title>
7
-    <style>
8
-        body {
9
-            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10
-            max-width: 900px;
11
-            margin: 50px auto;
12
-            padding: 20px;
13
-            background: #f5f5f5;
14
-        }
15
-        .container {
16
-            background: white;
17
-            padding: 30px;
18
-            border-radius: 8px;
19
-            box-shadow: 0 2px 8px rgba(0,0,0,0.1);
20
-        }
21
-        button {
22
-            background: #1890ff;
23
-            color: white;
24
-            border: none;
25
-            padding: 10px 20px;
26
-            border-radius: 4px;
27
-            cursor: pointer;
28
-            font-size: 14px;
29
-            margin-right: 10px;
30
-        }
31
-        button:hover {
32
-            background: #40a9ff;
33
-        }
34
-        .result {
35
-            margin-top: 20px;
36
-            padding: 15px;
37
-            background: #f0f0f0;
38
-            border-radius: 4px;
39
-            white-space: pre-wrap;
40
-            font-family: 'Courier New', monospace;
41
-            font-size: 12px;
42
-            max-height: 500px;
43
-            overflow: auto;
44
-        }
45
-        .error {
46
-            color: #ff4d4f;
47
-            background: #fff2f0;
48
-            border: 1px solid #ffccc7;
49
-        }
50
-        .success {
51
-            color: #52c41a;
52
-            background: #f6ffed;
53
-            border: 1px solid #b7eb8f;
54
-        }
55
-        input {
56
-            padding: 8px;
57
-            border: 1px solid #d9d9d9;
58
-            border-radius: 4px;
59
-            width: 300px;
60
-            font-size: 14px;
61
-        }
62
-        .input-group {
63
-            margin: 15px 0;
64
-        }
65
-        label {
66
-            display: inline-block;
67
-            width: 120px;
68
-            font-weight: 500;
69
-        }
70
-    </style>
71
-</head>
72
-<body>
73
-    <div class="container">
74
-        <h1>📡 Document Content API 测试</h1>
75
-        
76
-        <div class="input-group">
77
-            <label>API Base URL:</label>
78
-            <input type="text" id="baseUrl" value="http://192.168.0.195:8000" />
79
-        </div>
80
-        
81
-        <div class="input-group">
82
-            <label>Record ID:</label>
83
-            <input type="text" id="recordId" value="rec-48d00cf5664a" />
84
-        </div>
85
-        
86
-        <div class="input-group">
87
-            <label>User ID:</label>
88
-            <input type="text" id="userId" value="default-user" />
89
-        </div>
90
-        
91
-        <div>
92
-            <button onclick="testListRecords()">测试列表 API</button>
93
-            <button onclick="testContentAPI()">测试内容 API</button>
94
-            <button onclick="clearResult()">清空结果</button>
95
-        </div>
96
-        
97
-        <div id="result" class="result"></div>
98
-    </div>
99
-
100
-    <script>
101
-        function getBaseUrl() {
102
-            return document.getElementById('baseUrl').value;
103
-        }
104
-        
105
-        function getRecordId() {
106
-            return document.getElementById('recordId').value;
107
-        }
108
-        
109
-        function getUserId() {
110
-            return document.getElementById('userId').value;
111
-        }
112
-        
113
-        function showResult(data, isError = false) {
114
-            const resultDiv = document.getElementById('result');
115
-            resultDiv.className = 'result ' + (isError ? 'error' : 'success');
116
-            resultDiv.textContent = JSON.stringify(data, null, 2);
117
-        }
118
-        
119
-        function clearResult() {
120
-            document.getElementById('result').textContent = '';
121
-            document.getElementById('result').className = 'result';
122
-        }
123
-        
124
-        async function testListRecords() {
125
-            try {
126
-                const baseUrl = getBaseUrl();
127
-                const userId = getUserId();
128
-                const url = `${baseUrl}/api/v1/export/records?userId=${userId}&page=1&pageSize=5`;
129
-                
130
-                console.log('Fetching:', url);
131
-                const response = await fetch(url);
132
-                
133
-                if (!response.ok) {
134
-                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
135
-                }
136
-                
137
-                const data = await response.json();
138
-                console.log('Response:', data);
139
-                
140
-                showResult({
141
-                    status: '✅ SUCCESS',
142
-                    code: data.code,
143
-                    records: data.data.records.length,
144
-                    data: data
145
-                });
146
-            } catch (error) {
147
-                console.error('Error:', error);
148
-                showResult({
149
-                    status: '❌ ERROR',
150
-                    message: error.message,
151
-                    error: error
152
-                }, true);
153
-            }
154
-        }
155
-        
156
-        async function testContentAPI() {
157
-            try {
158
-                const baseUrl = getBaseUrl();
159
-                const recordId = getRecordId();
160
-                const userId = getUserId();
161
-                const url = `${baseUrl}/api/v1/export/records/${recordId}/content?userId=${userId}`;
162
-                
163
-                console.log('Fetching:', url);
164
-                const response = await fetch(url);
165
-                
166
-                if (!response.ok) {
167
-                    const text = await response.text();
168
-                    throw new Error(`HTTP ${response.status}: ${response.statusText}\n${text}`);
169
-                }
170
-                
171
-                const data = await response.json();
172
-                console.log('Response:', data);
173
-                
174
-                showResult({
175
-                    status: '✅ SUCCESS',
176
-                    code: data.code,
177
-                    fileName: data.data.fileName,
178
-                    outlineItems: data.data.outline.length,
179
-                    contentBlocks: data.data.content.length,
180
-                    outline: data.data.outline,
181
-                    sample_content: data.data.content.slice(0, 3)
182
-                });
183
-            } catch (error) {
184
-                console.error('Error:', error);
185
-                showResult({
186
-                    status: '❌ ERROR',
187
-                    message: error.message,
188
-                    error: error.toString()
189
-                }, true);
190
-            }
191
-        }
192
-        
193
-        // Auto-test on load
194
-        window.addEventListener('load', () => {
195
-            console.log('Page loaded, ready to test');
196
-        });
197
-    </script>
198
-</body>
199
-</html>

+ 0 - 128
public/test-dedup.html

@@ -1,128 +0,0 @@
1
-<!DOCTYPE html>
2
-<html lang="zh-CN">
3
-<head>
4
-    <meta charset="UTF-8">
5
-    <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
-    <title>测试去重逻辑</title>
7
-    <style>
8
-        body {
9
-            font-family: Arial, sans-serif;
10
-            max-width: 800px;
11
-            margin: 50px auto;
12
-            padding: 20px;
13
-        }
14
-        button {
15
-            padding: 10px 20px;
16
-            font-size: 16px;
17
-            cursor: pointer;
18
-            margin: 10px 0;
19
-        }
20
-        #result {
21
-            margin-top: 20px;
22
-            padding: 20px;
23
-            background: #f5f5f5;
24
-            border-radius: 4px;
25
-        }
26
-        .log {
27
-            margin: 5px 0;
28
-            padding: 5px;
29
-            background: white;
30
-            border-left: 3px solid #4CAF50;
31
-        }
32
-        .log.duplicate {
33
-            border-left-color: #f44336;
34
-        }
35
-    </style>
36
-</head>
37
-<body>
38
-    <h1>文档去重测试</h1>
39
-    <p>这个页面用于测试文档内容去重逻辑是否正常工作。</p>
40
-    
41
-    <div>
42
-        <label>
43
-            Record ID: 
44
-            <input type="text" id="recordId" value="请从实际记录中获取" style="width: 300px;">
45
-        </label>
46
-    </div>
47
-    
48
-    <div>
49
-        <label>
50
-            User ID: 
51
-            <input type="text" id="userId" value="default-user" style="width: 200px;">
52
-        </label>
53
-    </div>
54
-    
55
-    <button onclick="testDedup()">测试去重逻辑</button>
56
-    <button onclick="clearResult()">清除结果</button>
57
-    
58
-    <div id="result"></div>
59
-
60
-    <script>
61
-        async function testDedup() {
62
-            const recordId = document.getElementById('recordId').value;
63
-            const userId = document.getElementById('userId').value;
64
-            const resultDiv = document.getElementById('result');
65
-            
66
-            resultDiv.innerHTML = '<div class="log">开始测试...</div>';
67
-            
68
-            try {
69
-                // 模拟内容数组(完全重复的情况)
70
-                const mockContent = [
71
-                    { type: 'heading', level: 1, text: '东河塘油田东河1****区块****DH1-10H****井', id: 'h1' },
72
-                    { type: 'paragraph', text: '调剖****地质设计', id: 'p1' },
73
-                    { type: 'paragraph', text: '作业类型:调剖', id: 'p2' },
74
-                    // 重复内容
75
-                    { type: 'heading', level: 1, text: '东河塘油田东河1****区块****DH1-10H****井', id: 'h1-dup' },
76
-                    { type: 'paragraph', text: '调剖****地质设计', id: 'p1-dup' },
77
-                    { type: 'paragraph', text: '作业类型:调剖', id: 'p2-dup' },
78
-                ];
79
-                
80
-                resultDiv.innerHTML += `<div class="log">原始内容块数量: ${mockContent.length}</div>`;
81
-                
82
-                // 执行去重逻辑
83
-                const uniqueContent = [];
84
-                const seenTexts = new Map();
85
-                let duplicateCount = 0;
86
-                
87
-                mockContent.forEach((block, index) => {
88
-                    let key = '';
89
-                    if (block.type === 'heading') {
90
-                        key = `H${block.level}:${block.text}`;
91
-                    } else if (block.type === 'paragraph') {
92
-                        key = `P:${block.text}`;
93
-                    }
94
-                    
95
-                    const firstIndex = seenTexts.get(key);
96
-                    if (firstIndex === undefined) {
97
-                        seenTexts.set(key, uniqueContent.length);
98
-                        uniqueContent.push(block);
99
-                        resultDiv.innerHTML += `<div class="log">✓ 保留 [${index}]: ${block.text?.substring(0, 50)}...</div>`;
100
-                    } else {
101
-                        duplicateCount++;
102
-                        resultDiv.innerHTML += `<div class="log duplicate">✗ 跳过重复 [${index}]: ${block.text?.substring(0, 50)}... (首次出现在 ${firstIndex})</div>`;
103
-                    }
104
-                });
105
-                
106
-                resultDiv.innerHTML += `<div class="log"><strong>去重结果:</strong></div>`;
107
-                resultDiv.innerHTML += `<div class="log">- 原始: ${mockContent.length} 个块</div>`;
108
-                resultDiv.innerHTML += `<div class="log">- 唯一: ${uniqueContent.length} 个块</div>`;
109
-                resultDiv.innerHTML += `<div class="log">- 移除: ${duplicateCount} 个重复块</div>`;
110
-                resultDiv.innerHTML += `<div class="log">- 移除比例: ${((duplicateCount / mockContent.length) * 100).toFixed(1)}%</div>`;
111
-                
112
-                if (duplicateCount === 3) {
113
-                    resultDiv.innerHTML += `<div class="log" style="border-left-color: #4CAF50; font-weight: bold;">✓ 去重逻辑工作正常!</div>`;
114
-                } else {
115
-                    resultDiv.innerHTML += `<div class="log duplicate" style="font-weight: bold;">✗ 去重逻辑可能有问题</div>`;
116
-                }
117
-                
118
-            } catch (error) {
119
-                resultDiv.innerHTML += `<div class="log duplicate">错误: ${error.message}</div>`;
120
-            }
121
-        }
122
-        
123
-        function clearResult() {
124
-            document.getElementById('result').innerHTML = '';
125
-        }
126
-    </script>
127
-</body>
128
-</html>

+ 0 - 3
src/App.tsx

@@ -163,10 +163,7 @@ const App: React.FC = () => {
163 163
    */
164 164
   const handleDocumentSelect = useCallback(
165 165
     async (documentId: string) => {
166
-      console.log('Document selected:', documentId);
167
-      // Close drawer after selection
168 166
       setDocListOpen(false);
169
-      // Future: could open document in viewer or perform other actions
170 167
     },
171 168
     [],
172 169
   );

+ 0 - 4
src/components/ChatPanel/MessageItem.tsx

@@ -155,7 +155,6 @@ const MessageItem: React.FC<MessageItemProps> = memo(
155 155
           // URL format: http://xxx/api/v1/export/records/{recordId}/download?userId=xxx
156 156
           const urlMatch = exportRecord.downloadUrl.match(/\/export\/records\/([^/]+)\/download/);
157 157
           if (!urlMatch) {
158
-            console.error('[MessageItem] Cannot extract recordId from URL:', exportRecord.downloadUrl);
159 158
             return;
160 159
           }
161 160
           
@@ -200,10 +199,7 @@ const MessageItem: React.FC<MessageItemProps> = memo(
200 199
             document.body.removeChild(link);
201 200
             URL.revokeObjectURL(blobUrl);
202 201
           }, 100);
203
-          
204
-          console.log('[MessageItem] Download triggered:', fileName);
205 202
         } catch (error) {
206
-          console.error('[MessageItem] Download failed:', error);
207 203
         }
208 204
       },
209 205
       [exportRecord]

+ 0 - 4
src/components/DocumentManagement/DocumentManagement.tsx

@@ -169,14 +169,10 @@ export const DocumentManagement: React.FC<DocumentManagementProps> = ({
169 169
           document.body.removeChild(link);
170 170
           URL.revokeObjectURL(blobUrl);
171 171
         }, 100);
172
-        
173
-        console.log('[DocumentManagement] Word 文档下载已触发:', result.fileName);
174 172
       } catch (downloadError) {
175
-        console.error('[DocumentManagement] 下载失败:', downloadError);
176 173
         message.error('文件下载失败');
177 174
       }
178 175
     } catch (error) {
179
-      console.error('[DocumentManagement] 导出失败:', error);
180 176
       message.error('导出失败: ' + (error instanceof Error ? error.message : '未知错误'));
181 177
     } finally {
182 178
       setExporting((prev) => ({ ...prev, [documentId]: false }));

+ 0 - 5
src/components/DocumentManagementLayout/DocumentManagementLayout.tsx

@@ -276,10 +276,7 @@ const DocumentManagementLayout: React.FC<DocumentManagementLayoutProps> = ({
276 276
         document.body.removeChild(link);
277 277
         URL.revokeObjectURL(blobUrl);
278 278
       }, 100);
279
-      
280
-      console.log('[DocumentManagementLayout] Word 文档下载已触发:', result.fileName);
281 279
     } catch (error) {
282
-      console.error('[DocumentManagementLayout] 导出失败:', error);
283 280
       message.error('导出失败: ' + (error instanceof Error ? error.message : '未知错误'));
284 281
     } finally {
285 282
       setLoading(false);
@@ -341,9 +338,7 @@ const DocumentManagementLayout: React.FC<DocumentManagementLayoutProps> = ({
341 338
       }, 100);
342 339
       
343 340
       message.success('下载已开始');
344
-      console.log('[DocumentManagementLayout] 下载已触发:', fileName);
345 341
     } catch (error) {
346
-      console.error('[DocumentManagementLayout] 下载失败:', error);
347 342
       message.error('下载失败: ' + (error instanceof Error ? error.message : '未知错误'));
348 343
     }
349 344
   }, [selectedRecordId, userId]);

+ 0 - 4
src/components/EditorPanel/EditorPanel.tsx

@@ -303,14 +303,10 @@ export const EditorPanel: React.FC<EditorPanelProps> = ({
303 303
           document.body.removeChild(link);
304 304
           URL.revokeObjectURL(blobUrl);
305 305
         }, 100);
306
-        
307
-        console.log('[EditorPanel] Word 文档下载已触发:', result.fileName);
308 306
       } catch (downloadError) {
309
-        console.error('[EditorPanel] 下载失败:', downloadError);
310 307
         message.error('文件下载失败');
311 308
       }
312 309
     } catch (error) {
313
-      console.error('[EditorPanel] 导出失败:', error);
314 310
       message.error('导出失败: ' + (error instanceof Error ? error.message : '未知错误'));
315 311
     } finally {
316 312
       setExporting(false);

+ 0 - 7
src/components/EditorPanel/WYSIWYGEditor.tsx

@@ -68,7 +68,6 @@ const sanitizeMarkdown = (content: string): string => {
68 68
     
69 69
     return cleaned;
70 70
   } catch (error) {
71
-    console.error('Error sanitizing markdown:', error);
72 71
     return content;
73 72
   }
74 73
 };
@@ -104,16 +103,12 @@ export const WYSIWYGEditor: React.FC<WYSIWYGEditorProps> = ({
104 103
 
105 104
   // Sanitize markdown when it changes
106 105
   useEffect(() => {
107
-    console.log('[WYSIWYGEditor] Markdown input:', markdown?.substring(0, 100)); // 打印前100个字符
108 106
     try {
109 107
       const sanitized = sanitizeMarkdown(markdown);
110
-      console.log('[WYSIWYGEditor] Sanitized markdown:', sanitized?.substring(0, 100));
111 108
       setCleanedMarkdown(sanitized);
112 109
       setError(null);
113
-      // 强制重新创建编辑器实例
114 110
       setEditorKey(prev => prev + 1);
115 111
     } catch (err) {
116
-      console.error('Failed to sanitize markdown:', err);
117 112
       setError('文档内容包含无效字符,请在源码模式下修复');
118 113
       setCleanedMarkdown('');
119 114
     }
@@ -178,8 +173,6 @@ export const WYSIWYGEditor: React.FC<WYSIWYGEditorProps> = ({
178 173
             readOnly={readOnly}
179 174
             placeholder={placeholder}
180 175
             onError={(payload) => {
181
-              console.error('MDXEditor error:', payload);
182
-              // MDXEditor的错误对象可能有不同的结构
183 176
               const errorMessage = typeof payload === 'object' && payload !== null
184 177
                 ? ('error' in payload ? String(payload.error) : '编辑器发生错误')
185 178
                 : String(payload);

+ 0 - 4
src/components/MarkdownPreview/MarkdownPreview.tsx

@@ -385,14 +385,10 @@ export const MarkdownPreview: React.FC<MarkdownPreviewProps> = ({
385 385
           document.body.removeChild(link);
386 386
           URL.revokeObjectURL(blobUrl);
387 387
         }, 100);
388
-        
389
-        console.log('[MarkdownPreview] Word 文档下载已触发:', result.fileName);
390 388
       } catch (downloadError) {
391
-        console.error('[MarkdownPreview] 下载失败:', downloadError);
392 389
         message.error('文件下载失败');
393 390
       }
394 391
     } catch (error) {
395
-      console.error('[MarkdownPreview] 导出失败:', error);
396 392
       message.error('导出失败: ' + (error instanceof Error ? error.message : '未知错误'));
397 393
     } finally {
398 394
       setExporting(false);

+ 0 - 253
src/services/README.md

@@ -1,253 +0,0 @@
1
-# Services Layer
2
-
3
-This directory contains the business logic services that handle API communication and data operations.
4
-
5
-## Files
6
-
7
-### `api.ts` - API Client
8
-
9
-The core HTTP client built on Axios with the following features:
10
-
11
-#### Features
12
-
13
-1. **Base URL Configuration**
14
-   - Reads from `VITE_API_BASE_URL` environment variable
15
-   - Defaults to `http://localhost:8000` if not set
16
-
17
-2. **Request Timeout**
18
-   - Default timeout: 30 seconds (30000ms)
19
-   - Configurable per request
20
-
21
-3. **Request Interceptor**
22
-   - Adds `Content-Type: application/json` header
23
-   - Debug logging when `VITE_DEBUG=true`
24
-   - Extensible for authentication tokens
25
-
26
-4. **Response Interceptor**
27
-   - Handles business errors (code !== 0)
28
-   - Normalizes error responses
29
-   - Debug logging support
30
-
31
-5. **Error Handling**
32
-   - **Network Errors**: Connection failures, timeouts
33
-   - **Client Errors (4xx)**: Bad requests, unauthorized, not found
34
-   - **Server Errors (5xx)**: Internal server errors
35
-   - **Business Errors**: Backend returns code !== 0
36
-
37
-6. **Request Cancellation**
38
-   - Full support for `AbortController`
39
-   - Cancel in-flight requests
40
-
41
-#### Usage
42
-
43
-```typescript
44
-import apiClient, { getErrorMessage, isApiError } from './services/api';
45
-
46
-// GET request
47
-const response = await apiClient.get('/api/v1/documents/123');
48
-const document = response.data.data;
49
-
50
-// POST request
51
-const response = await apiClient.post('/api/v1/documents', {
52
-  title: 'New Document',
53
-  content: 'Content here',
54
-  format: 'markdown',
55
-  source: 'chat',
56
-});
57
-
58
-// PATCH request
59
-await apiClient.patch('/api/v1/documents/123', {
60
-  content: 'Updated content',
61
-});
62
-
63
-// DELETE request
64
-await apiClient.delete('/api/v1/documents/123');
65
-
66
-// With cancellation
67
-const controller = new AbortController();
68
-const response = await apiClient.get('/api/v1/documents', {
69
-  signal: controller.signal,
70
-});
71
-// Cancel: controller.abort();
72
-
73
-// Error handling
74
-try {
75
-  await apiClient.get('/api/v1/documents/123');
76
-} catch (error) {
77
-  if (isApiError(error)) {
78
-    console.error(error.type, error.message);
79
-  } else {
80
-    console.error(getErrorMessage(error));
81
-  }
82
-}
83
-```
84
-
85
-#### Error Types
86
-
87
-| Type       | Description          | Example                            |
88
-| ---------- | -------------------- | ---------------------------------- |
89
-| `network`  | No response received | Connection timeout, offline        |
90
-| `client`   | 4xx status codes     | 400 Bad Request, 404 Not Found     |
91
-| `server`   | 5xx status codes     | 500 Internal Server Error          |
92
-| `business` | Backend code !== 0   | Business logic validation failures |
93
-| `unknown`  | Unexpected errors    | Other edge cases                   |
94
-
95
-#### Exported Utilities
96
-
97
-- `apiClient` (default): Configured Axios instance
98
-- `isApiError(error)`: Type guard for ApiError
99
-- `getErrorMessage(error)`: Extract user-friendly message
100
-- `axios`: Raw axios for advanced use cases
101
-
102
----
103
-
104
-### `documentService.ts` - Document CRUD Operations
105
-
106
-Provides methods for managing documents through the backend API.
107
-
108
-#### Available Methods
109
-
110
-1. **`createDocument(request)`** - Create a new document
111
-   - POST `/api/v1/documents`
112
-   - Returns: `{ documentId, title, format, createdAt }`
113
-
114
-2. **`getDocument(documentId)`** - Retrieve a document by ID
115
-   - GET `/api/v1/documents/:id`
116
-   - Returns: Full `Document` object
117
-
118
-3. **`listDocuments(filters?)`** - List documents with pagination
119
-   - GET `/api/v1/documents`
120
-   - Supports filtering by source, sorting, pagination
121
-   - Returns: `{ documents, pagination }`
122
-
123
-4. **`updateDocument(documentId, request)`** - Update a document
124
-   - PUT `/api/v1/documents/:id`
125
-   - Supports full content update or partial block updates
126
-   - Returns: `{ documentId, updatedAt }`
127
-
128
-5. **`deleteDocument(documentId)`** - Delete a document
129
-   - DELETE `/api/v1/documents/:id`
130
-   - Returns: `void` on success
131
-
132
-#### Usage Examples
133
-
134
-```typescript
135
-import { documentService } from './services';
136
-// or
137
-import {
138
-  createDocument,
139
-  getDocument,
140
-  listDocuments,
141
-  updateDocument,
142
-  deleteDocument,
143
-} from './services/documentService';
144
-
145
-// Create a document
146
-const result = await createDocument({
147
-  title: 'My Document',
148
-  content: '# Hello World',
149
-  format: 'markdown',
150
-  source: 'chat',
151
-  sessionId: 'session-123', // optional
152
-});
153
-console.log('Created:', result.documentId);
154
-
155
-// Get a document
156
-const document = await getDocument('doc-123');
157
-console.log('Title:', document.title);
158
-
159
-// List documents with filters
160
-const { documents, pagination } = await listDocuments({
161
-  source: 'chat',
162
-  sortBy: 'createdAt',
163
-  sortOrder: 'desc',
164
-  page: 1,
165
-  pageSize: 20,
166
-});
167
-
168
-// Update document content
169
-await updateDocument('doc-123', {
170
-  content: '# Updated content',
171
-});
172
-
173
-// Update document title
174
-await updateDocument('doc-123', {
175
-  title: 'New Title',
176
-});
177
-
178
-// Update with blocks (partial update)
179
-await updateDocument('doc-123', {
180
-  blocks: [
181
-    { level: 1, index: 0, content: '# Updated heading' },
182
-  ],
183
-});
184
-
185
-// Delete a document
186
-await deleteDocument('doc-123');
187
-
188
-// Using namespace pattern
189
-await documentService.create({ ... });
190
-await documentService.get('doc-123');
191
-await documentService.list({ source: 'chat' });
192
-await documentService.update('doc-123', { title: 'New' });
193
-await documentService.delete('doc-123');
194
-```
195
-
196
-#### Error Handling
197
-
198
-All methods throw errors with Chinese error messages on failure:
199
-
200
-```typescript
201
-try {
202
-  await getDocument('invalid-id');
203
-} catch (error) {
204
-  console.error(error.message); // "获取文档失败: 请求的资源不存在"
205
-}
206
-```
207
-
208
-Error messages include:
209
-
210
-- `创建文档失败: ...` - Document creation failed
211
-- `获取文档失败: ...` - Document retrieval failed
212
-- `获取文档列表失败: ...` - Document list retrieval failed
213
-- `更新文档失败: ...` - Document update failed
214
-- `删除文档失败: ...` - Document deletion failed
215
-
216
----
217
-
218
-## Future Services
219
-
220
-- **`exportService.ts`** - Document export and download handling
221
-- **`autoSaveService.ts`** - Debounced auto-save logic
222
-
223
----
224
-
225
-## Best Practices
226
-
227
-1. **Always use the configured `apiClient`** - Don't create new axios instances
228
-2. **Handle errors consistently** - Use `isApiError()` and `getErrorMessage()`
229
-3. **Type all responses** - Use `ApiResponse<T>` wrapper
230
-4. **Cancel requests when unmounting** - Use AbortController with useEffect
231
-5. **Don't expose raw errors to users** - Always provide friendly messages
232
-
233
----
234
-
235
-## Environment Variables
236
-
237
-```bash
238
-# .env
239
-VITE_API_BASE_URL=http://localhost:8000  # Backend API URL
240
-VITE_DEBUG=false                          # Enable debug logging
241
-```
242
-
243
----
244
-
245
-## Testing
246
-
247
-See `api.test.example.ts` for usage examples and patterns.
248
-
249
-For actual tests, use tools like:
250
-
251
-- **Vitest** - Unit testing
252
-- **MSW** (Mock Service Worker) - API mocking
253
-- **Playwright** - E2E testing

+ 0 - 68
src/services/documentService.ts

@@ -34,18 +34,6 @@ import type {
34 34
  * @param request - Create document request payload
35 35
  * @returns Created document metadata
36 36
  * @throws {Error} When the API request fails or file cannot be parsed
37
- *
38
- * @example
39
- * ```typescript
40
- * const result = await createDocument({
41
- *   userId: 'user-123',
42
- *   fileUrl: 'https://example.com/report.docx',
43
- *   sessionId: 'session-456',
44
- * });
45
- *
46
- * console.log('Document ID:', result.documentId);
47
- * console.log('Format:', result.format); // 'markdown'
48
- * ```
49 37
  */
50 38
 export const createDocument = async (
51 39
   request: CreateDocumentRequest
@@ -78,13 +66,6 @@ export const createDocument = async (
78 66
  * @param documentId - Document ID to retrieve
79 67
  * @returns Document entity with full content
80 68
  * @throws {Error} When the document is not found or request fails
81
- *
82
- * @example
83
- * ```typescript
84
- * const document = await getDocument('doc-abc123');
85
- * console.log('Content:', document.content);
86
- * console.log('Last updated:', new Date(document.updatedAt));
87
- * ```
88 69
  */
89 70
 export const getDocument = async (documentId: string): Promise<Document> => {
90 71
   try {
@@ -113,13 +94,6 @@ export const getDocument = async (documentId: string): Promise<Document> => {
113 94
  * @param documentId - Document ID to retrieve
114 95
  * @returns Document with title and content
115 96
  * @throws {Error} When the document is not found or request fails
116
- *
117
- * @example
118
- * ```typescript
119
- * const doc = await getDocumentContent('doc-abc123');
120
- * console.log('Title:', doc.title);
121
- * console.log('Content:', doc.content);
122
- * ```
123 97
  */
124 98
 export const getDocumentContent = async (
125 99
   documentId: string
@@ -143,25 +117,6 @@ export const getDocumentContent = async (
143 117
  * @param request - Update request (content OR blocks, not both)
144 118
  * @returns Update response with new timestamp
145 119
  * @throws {Error} When document not found or validation fails
146
- *
147
- * @example
148
- * ```typescript
149
- * // Full update
150
- * await updateDocument('doc-abc123', {
151
- *   content: '# Updated Title\n\nNew content...',
152
- * });
153
- *
154
- * // Partial block update
155
- * await updateDocument('doc-abc123', {
156
- *   blocks: [
157
- *     {
158
- *       level: 2,
159
- *       index: 1,
160
- *       content: '## Updated Section\n\nUpdated paragraph...',
161
- *     },
162
- *   ],
163
- * });
164
- * ```
165 120
  */
166 121
 export const updateDocument = async (
167 122
   documentId: string,
@@ -196,12 +151,6 @@ export const updateDocument = async (
196 151
  * @param sessionId - Session ID whose documents should be deleted
197 152
  * @returns Success message with deletion count
198 153
  * @throws {Error} When the delete operation fails
199
- *
200
- * @example
201
- * ```typescript
202
- * const result = await deleteDocuments('session-456');
203
- * console.log(result); // "Deleted 3 document(s) successfully"
204
- * ```
205 154
  */
206 155
 export const deleteDocuments = async (sessionId: string): Promise<string> => {
207 156
   try {
@@ -227,23 +176,6 @@ export const deleteDocuments = async (sessionId: string): Promise<string> => {
227 176
  * @param filters - Query filters including userId, pagination, and sorting
228 177
  * @returns Paginated list of documents
229 178
  * @throws {Error} When the request fails
230
- *
231
- * @example
232
- * ```typescript
233
- * const result = await listDocuments({
234
- *   userId: 'user-123',
235
- *   page: 1,
236
- *   pageSize: 20,
237
- *   sessionId: 'session-456',
238
- *   sortBy: 'updatedAt',
239
- *   sortOrder: 'desc',
240
- * });
241
- *
242
- * console.log('Total documents:', result.pagination.total);
243
- * result.documents.forEach(doc => {
244
- *   console.log(doc.id, new Date(doc.updatedAt));
245
- * });
246
- * ```
247 179
  */
248 180
 export const listDocuments = async (
249 181
   filters: DocumentListFilters

+ 0 - 46
src/services/exportRecordService.ts

@@ -27,19 +27,6 @@ import type {
27 27
  * @param filters - Filters for listing export records (userId is required)
28 28
  * @returns List of export records and pagination information
29 29
  * @throws {Error} When the API request fails
30
- *
31
- * @example
32
- * ```typescript
33
- * const result = await listExportRecords({
34
- *   userId: 'user-123',
35
- *   page: 1,
36
- *   pageSize: 20,
37
- *   sortOrder: 'desc',
38
- * });
39
- *
40
- * console.log('Total records:', result.pagination.total);
41
- * console.log('Records:', result.records);
42
- * ```
43 30
  */
44 31
 export const listExportRecords = async (
45 32
   filters: ExportRecordListFilters
@@ -69,7 +56,6 @@ export const listExportRecords = async (
69 56
             const urlObj = new URL(record.downloadUrl);
70 57
             const correctUrlObj = new URL(correctBaseURL);
71 58
             if (urlObj.origin !== correctUrlObj.origin) {
72
-              console.warn(`[ExportRecord] Replacing origin in record ${record.recordId}: ${urlObj.origin} -> ${correctUrlObj.origin}`);
73 59
               return {
74 60
                 ...record,
75 61
                 downloadUrl: record.downloadUrl.replace(urlObj.origin, correctUrlObj.origin)
@@ -99,15 +85,6 @@ export const listExportRecords = async (
99 85
  * @param recordId - The export record ID
100 86
  * @param userId - The user ID who owns the record
101 87
  * @throws {Error} When the download fails
102
- *
103
- * @example
104
- * ```typescript
105
- * try {
106
- *   await downloadExportRecord('record-123', 'user-456');
107
- * } catch (error) {
108
- *   console.error('Download failed:', error);
109
- * }
110
- * ```
111 88
  */
112 89
 export const downloadExportRecord = async (
113 90
   recordId: string,
@@ -151,8 +128,6 @@ export const downloadExportRecord = async (
151 128
       document.body.removeChild(link);
152 129
       URL.revokeObjectURL(blobUrl);
153 130
     }, 100);
154
-    
155
-    console.log('[ExportRecord] Download completed:', fileName);
156 131
   } catch (error) {
157 132
     const message = getErrorMessage(error);
158 133
     throw new Error(`下载文件失败: ${message}`, { cause: error });
@@ -168,16 +143,6 @@ export const downloadExportRecord = async (
168 143
  * @param userId - The user ID who owns the record
169 144
  * @returns Promise that resolves when deletion is successful
170 145
  * @throws {Error} When the API request fails or record not found
171
- *
172
- * @example
173
- * ```typescript
174
- * try {
175
- *   await deleteExportRecord('record-123', 'user-456');
176
- *   console.log('Export record deleted successfully');
177
- * } catch (error) {
178
- *   console.error('Failed to delete export record:', error);
179
- * }
180
- * ```
181 146
  */
182 147
 export const deleteExportRecord = async (
183 148
   recordId: string,
@@ -206,17 +171,6 @@ export const deleteExportRecord = async (
206 171
  *
207 172
  * @returns Storage usage information
208 173
  * @throws {Error} When the API request fails or user is not authorized
209
- *
210
- * @example
211
- * ```typescript
212
- * try {
213
- *   const info = await getAdminStorage();
214
- *   console.log('Total users:', info.activeUsers);
215
- *   console.log('Quota exceeded:', info.quotaExceeded);
216
- * } catch (error) {
217
- *   console.error('Failed to get storage info:', error);
218
- * }
219
- * ```
220 174
  */
221 175
 export const getAdminStorage = async (): Promise<StorageInfo> => {
222 176
   try {

+ 0 - 57
src/services/exportService.ts

@@ -31,56 +31,18 @@ import type { ExportDocRequest, ExportDocResponse } from '../types/export';
31 31
  * @param request - Export request payload (documentId required, styleId optional)
32 32
  * @returns Export response with download URL, filename, and record ID
33 33
  * @throws {Error} When the API request fails or document not found
34
- *
35
- * @example
36
- * ```typescript
37
- * // Export with default style
38
- * const result = await exportToWord({
39
- *   documentId: 'doc-abc123',
40
- * });
41
- *
42
- * console.log('Download URL:', result.downloadUrl);
43
- * console.log('File name:', result.fileName);
44
- * console.log('Record ID:', result.recordId);
45
- *
46
- * // Export with custom style
47
- * const result = await exportToWord({
48
- *   documentId: 'doc-abc123',
49
- *   styleId: 'custom-style-1',
50
- * });
51
- *
52
- * if (result.warning) {
53
- *   console.warn('Storage warning:', result.warning);
54
- * }
55
- * ```
56 34
  */
57 35
 export const exportToWord = async (
58 36
   request: ExportDocRequest
59 37
 ): Promise<ExportDocResponse> => {
60 38
   try {
61
-    // Debug logging
62
-    console.log('[Export Service] Sending request:', {
63
-      url: '/api/v1/export/doc',
64
-      documentId: request.documentId,
65
-      styleId: request.styleId,
66
-    });
67
-    
68 39
     const response = await apiClient.post<ApiResponse<ExportDocResponse>>(
69 40
       '/api/v1/export/doc',
70 41
       request
71 42
     );
72 43
     
73
-    console.log('[Export Service] Response received:', {
74
-      status: response.status,
75
-      data: response.data,
76
-    });
77
-    
78 44
     const data = response.data.data;
79 45
     
80
-    if (data.warning) {
81
-      console.warn('[Export Service] Warning:', data.warning);
82
-    }
83
-    
84 46
     // Ensure downloadUrl is properly formatted and uses correct base URL
85 47
     if (data.downloadUrl) {
86 48
       // If URL doesn't start with http, prepend base URL
@@ -95,32 +57,14 @@ export const exportToWord = async (
95 57
           const urlObj = new URL(data.downloadUrl);
96 58
           const correctUrlObj = new URL(correctBaseURL);
97 59
           if (urlObj.origin !== correctUrlObj.origin) {
98
-            console.warn(`[Export Service] Replacing origin ${urlObj.origin} with ${correctUrlObj.origin}`);
99 60
             data.downloadUrl = data.downloadUrl.replace(urlObj.origin, correctUrlObj.origin);
100 61
           }
101 62
         }
102 63
       }
103 64
     }
104 65
     
105
-    console.log('[Export Service] Export successful:', {
106
-      fileName: data.fileName,
107
-      downloadUrl: data.downloadUrl,
108
-    });
109
-    
110 66
     return data;
111 67
   } catch (error: any) {
112
-    // Enhanced error logging
113
-    console.error('[Export Service] Export failed:', {
114
-      status: error?.response?.status,
115
-      statusText: error?.response?.statusText,
116
-      data: error?.response?.data,
117
-      message: error?.message,
118
-      request: {
119
-        documentId: request.documentId,
120
-        styleId: request.styleId,
121
-      },
122
-    });
123
-    
124 68
     let friendlyMessage = '导出Word文档失败';
125 69
     let detailMessage = '';
126 70
 
@@ -132,7 +76,6 @@ export const exportToWord = async (
132 76
       detailMessage = error?.response?.data?.detail || error?.response?.data?.message || '';
133 77
     } else if (error?.response?.status === 500) {
134 78
       friendlyMessage = '服务器内部错误';
135
-      // Try to extract detailed error from response
136 79
       const backendError = error?.response?.data?.detail || error?.response?.data?.message || '';
137 80
       detailMessage = backendError ? `后端错误: ${backendError}` : 'Markdown解析或文件写入失败';
138 81
     } else if (error?.code === 'ECONNREFUSED' || error?.code === 'NETWORK_ERROR') {

+ 0 - 11
vitest.config.ts

@@ -1,11 +0,0 @@
1
-import { defineConfig } from 'vitest/config';
2
-import react from '@vitejs/plugin-react';
3
-
4
-export default defineConfig({
5
-  plugins: [react()],
6
-  test: {
7
-    environment: 'jsdom',
8
-    globals: true,
9
-    setupFiles: ['./src/test-setup.ts'],
10
-  },
11
-});

+ 0 - 271
阶段性分析报告.md

@@ -1,271 +0,0 @@
1
-# AX Frontend App 开发报告
2
-
3
-**项目定位**: 本地辅助工具 - Word 文档生成助手  
4
-**报告日期**: 2026年6月18日  
5
-**项目类型**: 微前端应用(部署在本地)
6
-
7
----
8
-
9
-## 📋 项目概述
10
-
11
-### 核心功能
12
-这是一个部署在本地的轻量级辅助工具,帮助工程师快速生成 Word 文档:
13
-
14
-1. **双栏布局**
15
-   - 左侧:聊天面板(与 AI 交互,描述文档需求)
16
-   - 右侧:编辑器面板(编辑生成的文档内容)
17
-
18
-2. **Word 模板驱动**
19
-   - 上传 .doc 模板文件
20
-   - AI 根据模板和对话内容生成文档
21
-   - 导出为格式化的 Word 文档
22
-
23
-3. **微前端架构**
24
-   - 独立部署运行
25
-   - 可集成到其他平台
26
-
27
-### 技术栈
28
-- **前端框架**: React 18 + TypeScript
29
-- **UI 组件**: Ant Design 5
30
-- **富文本编辑**: Slate.js
31
-- **状态管理**: Zustand
32
-- **构建工具**: Vite
33
-
34
----
35
-
36
-## ✅ 已完成的部分
37
-
38
-### 1. 基础架构(100%)
39
-- ✅ React 18 + TypeScript 项目搭建
40
-- ✅ Vite 构建配置
41
-- ✅ 微前端架构基础
42
-- ✅ 代码规范(ESLint + Prettier)
43
-
44
-### 2. 核心 UI(100%)
45
-- ✅ **双栏布局**
46
-  - 左侧聊天面板
47
-  - 右侧编辑器面板
48
-  - 可拖拽调整宽度
49
-  - 布局状态持久化
50
-
51
-- ✅ **聊天面板**
52
-  - 消息列表展示
53
-  - 消息输入框
54
-  - AI 响应显示(Mock)
55
-  - 滚动到底部
56
-
57
-- ✅ **编辑器面板**
58
-  - Markdown 编辑器(基于 Slate.js)
59
-  - 文档标题编辑
60
-  - 基础格式化工具栏
61
-  - 自动保存提示
62
-
63
-### 3. 基础功能(80%)
64
-- ✅ 文档列表查看
65
-- ✅ 文档创建/编辑/删除
66
-- ✅ 自动保存(防抖 3 秒)
67
-- ⚠️ 文档导出(当前为 .doc,需支持模板)
68
-- ⚠️ AI 对话(Mock 数据,待接入真实服务)
69
-
70
-### 4. 状态管理(100%)
71
-- ✅ documentStore(文档 CRUD)
72
-- ✅ chatStore(聊天消息)
73
-- ✅ uiStore(UI 状态)
74
-
75
----
76
-
77
-## ❌ 未完成的部分
78
-
79
-### 1. Word 模板核心功能(优先级最高)
80
-- ❌ **模板上传**
81
-  - 上传 .doc 模板文件
82
-  - 模板预览
83
-  - 模板管理(列表、删除)
84
-
85
-- ❌ **模板样式提取**
86
-  - 提取 Word 文档样式(字体、标题、段落格式)
87
-  - 样式存储和缓存
88
-  - 样式应用到编辑器
89
-
90
-- ❌ **基于模板导出**
91
-  - 使用模板样式生成 Word 文档
92
-  - 占位符填充
93
-  - 格式保真
94
-
95
-### 2. AI 集成(优先级高)
96
-- ❌ 接入真实 AI 服务
97
-- ❌ AI 理解模板结构
98
-- ❌ 根据对话生成符合模板的内容
99
-
100
-### 3. 用户体验优化(优先级中)
101
-- ❌ 离线提示
102
-- ❌ 错误处理优化
103
-- ❌ 加载状态优化
104
-- ❌ 快捷键支持
105
-
106
----
107
-
108
-## 📅 下周工作计划
109
-
110
-### 核心任务:完成 Word 模板功能
111
-
112
-**第 1-2 天:模板上传与管理**
113
-- [ ] 实现模板上传组件(拖拽上传或点击上传)
114
-- [ ] 模板列表展示
115
-- [ ] 选择/切换当前使用的模板
116
-- [ ] 模板删除功能
117
-
118
-**第 3-4 天:模板样式提取**
119
-- [ ] 后端实现样式提取(python-docx)
120
-  - 提取段落样式
121
-  - 提取字符样式
122
-  - 提取标题层级
123
-- [ ] 前端接收样式数据
124
-- [ ] 样式缓存管理
125
-
126
-**第 5-6 天:基于模板导出**
127
-- [ ] 后端实现模板填充逻辑
128
-  - 识别占位符
129
-  - 填充文档内容
130
-  - 保持格式一致
131
-- [ ] 前端触发导出
132
-- [ ] 下载生成的 Word 文档
133
-
134
-**第 7 天:测试与优化**
135
-- [ ] 完整流程测试
136
-  - 上传模板 → 对话生成内容 → 编辑 → 导出
137
-- [ ] 修复 Bug
138
-- [ ] 用户体验优化
139
-
140
-### 次要任务:AI 集成(如果时间允许)
141
-- [ ] 替换 Mock AI 为真实服务
142
-- [ ] 测试 AI 生成质量
143
-
144
----
145
-
146
-## ⚠️ 遇到的问题
147
-
148
-### 1. React 版本兼容性 ✅ 已解决
149
-**问题**: Ant Design v5 不支持 React 19
150
-
151
-**解决**: 使用 React 18.3.1,记录在 `REACT_VERSION_NOTES.md`
152
-
153
----
154
-
155
-### 2. 响应式布局需求变更 ✅ 已解决
156
-**问题**: 最初计划支持移动端,但实际只需桌面端
157
-
158
-**解决**: 移除响应式布局代码,专注桌面体验
159
-
160
----
161
-
162
-### 3. Word 模板样式提取复杂度 ⚠️ 待解决
163
-**问题**: 
164
-- Word 样式系统复杂(字体、颜色、段落、标题、列表等)
165
-- 样式提取可能不完整
166
-- 导出文档与模板格式不一致
167
-
168
-**计划**: 
169
-- 第一版先支持基础样式(标题、正文、字体、颜色)
170
-- 复杂样式(表格、页眉页脚、图片)后续迭代
171
-- 明确告知用户当前支持的样式范围
172
-
173
-**技术方案**: 
174
-- 后端使用 `python-docx` 提取样式
175
-- 样式序列化为 JSON 传给前端
176
-- 导出时加载样式并注入
177
-
178
----
179
-
180
-### 4. 后端 API 未就绪 ⚠️ 进行中
181
-**问题**: 
182
-- 前端开发快于后端
183
-- 无法联调测试
184
-
185
-**应对**: 
186
-- 使用 Mock 数据继续开发
187
-- 定义清晰的 API 接口规范
188
-- 等待后端完成后快速联调
189
-
190
----
191
-
192
-### 5. 微前端架构待实现 ⚠️ 待规划
193
-**问题**: 
194
-- 当前是独立应用
195
-- 需要作为微前端模块集成到其他平台
196
-
197
-**计划**: 
198
-- 使用 Vite Module Federation 或 qiankun
199
-- 封装为独立模块
200
-- 提供清晰的集成接口
201
-
202
----
203
-
204
-## 📊 项目进度
205
-
206
-### 当前完成度:**60%**
207
-
208
-| 模块 | 完成度 | 说明 |
209
-|------|-------|------|
210
-| 前端架构 | 100% | React + TS + Vite |
211
-| 双栏布局 | 100% | 聊天 + 编辑器 |
212
-| 基础编辑器 | 100% | Markdown 编辑 |
213
-| 文档管理 | 80% | CRUD 功能完成 |
214
-| **模板上传** | 0% | **核心功能待开发** |
215
-| **模板样式提取** | 0% | **核心功能待开发** |
216
-| **基于模板导出** | 0% | **核心功能待开发** |
217
-| AI 集成 | 20% | Mock 实现 |
218
-| 微前端化 | 0% | 待规划 |
219
-
220
-### 预计时间
221
-- **下周**: 完成 Word 模板核心功能(上传、样式提取、导出)
222
-- **下下周**: AI 集成 + 微前端架构 + 测试优化
223
-
224
----
225
-
226
-### 前端关键组件
227
-
228
-```
229
-src/
230
-├── components/
231
-│   ├── ChatPanel/          # 聊天面板
232
-│   ├── EditorPanel/        # 编辑器面板
233
-│   ├── TemplateManager/    # 模板管理(待开发)
234
-│   └── Layout/             # 双栏布局
235
-├── services/
236
-│   ├── documentService.ts  # 文档 CRUD
237
-│   ├── templateService.ts  # 模板管理(待开发)
238
-│   └── exportService.ts    # 导出服务
239
-└── stores/
240
-    ├── documentStore.ts
241
-    ├── chatStore.ts
242
-    └── templateStore.ts    # 模板状态(待开发)
243
-```
244
-
245
----
246
-
247
-## 💡 总结
248
-
249
-### 项目定位
250
-这是一个**本地部署的轻量级辅助工具**,不是企业级应用,功能聚焦在:
251
-1. 通过对话生成 Word 文档内容
252
-2. 基于模板保持格式一致
253
-3. 快速导出可用的 Word 文件
254
-
255
-### 优势
256
-- ✅ 前端基础架构完成
257
-- ✅ 双栏布局和编辑器可用
258
-- ✅ 代码质量高(TypeScript + 规范)
259
-
260
-### 挑战
261
-- ⚠️ Word 模板样式提取是核心难点
262
-- ⚠️ 格式保真需要大量测试
263
-- ⚠️ 后端开发进度待跟进
264
-
265
-### 下周重点
266
-**完成 Word 模板功能(上传 → 样式提取 → 导出)**
267
-
268
----
269
-
270
-**报告编制**: Kiro AI Assistant  
271
-**下次更新**: 2026-06-27