ソースを参照

Release v0.2.29

qdy 1ヶ月前
コミット
7c3a55903a

+ 185
- 0
web/TESTING.md ファイルの表示

@@ -0,0 +1,185 @@
1
+# API 测试指南
2
+
3
+本文档介绍如何测试 svc-code 项目的后端API接口。
4
+
5
+## 测试架构
6
+
7
+项目采用 **双路径调用架构**:
8
+
9
+1. **业务API路径** (`src/api/`): 供Vue页面使用,通过Vite代理
10
+2. **测试API路径** (`src/test/`): 独立测试使用,直接调用后端
11
+
12
+## 快速开始
13
+
14
+### 1. 启动服务
15
+
16
+```bash
17
+# 启动后端服务 (端口8020)
18
+cd svc-code && go run main.go
19
+
20
+# 启动前端开发服务器 (端口3000) - 可选
21
+cd svc-code/web && npm run dev
22
+```
23
+
24
+### 2. 运行测试
25
+
26
+#### 方法A: 使用测试页面 (推荐)
27
+在浏览器中打开: [http://localhost:3000/test-api.html](http://localhost:3000/test-api.html)
28
+
29
+或直接打开文件: `svc-code/web/public/test-api.html`
30
+
31
+#### 方法B: 在浏览器控制台中测试
32
+1. 打开浏览器开发者工具 (F12)
33
+2. 在Console标签页中输入:
34
+
35
+```javascript
36
+// 加载测试模块
37
+import('./src/test/api-test-standalone.js').then(module => {
38
+  module.runAllTests();
39
+});
40
+
41
+// 或者使用全局对象(如果页面已加载)
42
+testAPI.runAllTests();
43
+```
44
+
45
+#### 方法C: 使用curl命令行测试
46
+```bash
47
+# 测试健康检查
48
+curl http://localhost:8020/api/health
49
+
50
+# 测试项目列表
51
+curl http://localhost:8020/api/projects
52
+```
53
+
54
+## 测试用例
55
+
56
+### 1. 健康检查 API
57
+- **端点**: `GET /api/health`
58
+- **预期响应**: `{ "success": true, "data": { "healthy": true, "version": "1.0" } }`
59
+- **测试命令**: `testAPI.testHealth()`
60
+
61
+### 2. 项目列表 API
62
+- **端点**: `GET /api/projects`
63
+- **预期响应**: `{ "success": true, "data": [ ... ] }` (包含3个模拟项目)
64
+- **测试命令**: `testAPI.testProjects()`
65
+
66
+## 测试模块说明
67
+
68
+### 业务API模块 (`src/api/`)
69
+供Vue组件使用,通过Vite代理配置转发请求:
70
+
71
+```typescript
72
+// 页面中使用
73
+import { getProjects } from '@/api/project';
74
+
75
+// 实际调用路径: /projects → 代理到 http://localhost:8020/api/projects
76
+const projects = await getProjects();
77
+```
78
+
79
+### 独立测试模块 (`src/test/api-test-standalone.ts`)
80
+纯TypeScript模块,不依赖Vite配置,直接调用后端:
81
+
82
+```typescript
83
+// 在浏览器控制台或Node.js中使用
84
+import { testApi } from './src/test/api-test-standalone.js';
85
+
86
+// 直接调用后端API
87
+const projects = await testApi.getProjects();
88
+```
89
+
90
+主要功能:
91
+- `runAllTests()` - 运行所有测试
92
+- `testGetProjects()` - 测试项目列表API
93
+- `testHealthCheck()` - 测试健康检查API
94
+- `testBackendConnection()` - 测试后端连接
95
+- `testApi` - 直接API调用对象
96
+
97
+## 故障排除
98
+
99
+### 常见问题
100
+
101
+#### 1. 连接失败
102
+```
103
+错误: NetworkError 或 Failed to fetch
104
+```
105
+**解决方案**:
106
+- 检查后端服务是否运行: `lsof -i :8020`
107
+- 检查网络连接
108
+- 检查跨域设置
109
+
110
+#### 2. 404错误
111
+```
112
+错误: HTTP 404
113
+```
114
+**解决方案**:
115
+- 确认API端点正确: `/api/health`, `/api/projects`
116
+- 检查Vite代理配置 (`vite.config.ts`)
117
+
118
+#### 3. 跨域问题 (CORS)
119
+```
120
+错误: Access-Control-Allow-Origin
121
+```
122
+**解决方案**:
123
+- 后端需要配置CORS头
124
+- 使用Vite代理避免跨域
125
+
126
+#### 4. 响应格式错误
127
+```
128
+错误: 无法解析JSON 或 success字段不正确
129
+```
130
+**解决方案**:
131
+- 检查后端返回格式是否符合预期
132
+- 查看原始响应: `curl -v http://localhost:8020/api/health`
133
+
134
+### 调试技巧
135
+
136
+1. **查看原始请求**:
137
+```javascript
138
+// 在浏览器控制台
139
+fetch('http://localhost:8020/api/health')
140
+  .then(r => r.text())
141
+  .then(console.log);
142
+```
143
+
144
+2. **检查网络请求**:
145
+- 打开浏览器开发者工具的Network标签页
146
+- 查看请求详情和响应
147
+
148
+3. **后端日志**:
149
+- 查看后端终端输出
150
+- 检查日志文件: `svc-code/logs/`
151
+
152
+## 开发指南
153
+
154
+### 添加新的API测试
155
+
156
+1. 在 `src/api/` 添加业务API模块
157
+2. 在 `src/test/api-test-standalone.ts` 添加对应的测试函数
158
+3. 更新测试页面 (`public/test-api.html`) 添加测试按钮
159
+
160
+### 示例: 添加用户API测试
161
+
162
+```typescript
163
+// src/api/user.ts - 业务API
164
+export const getUsers = () => request.get('/users');
165
+
166
+// src/test/api-test-standalone.ts - 测试API
167
+export const testGetUsers = async () => {
168
+  return testApi.get('/users');
169
+};
170
+```
171
+
172
+## 相关文件
173
+
174
+- `src/api/project.ts` - 项目API业务模块
175
+- `src/api/health.ts` - 健康检查API业务模块
176
+- `src/test/api-test-standalone.ts` - 独立测试模块
177
+- `public/test-api.html` - 测试页面
178
+- `vite.config.ts` - Vite代理配置
179
+
180
+## 下一步
181
+
182
+- [ ] 添加更多的API测试用例
183
+- [ ] 集成单元测试框架 (Vitest/Jest)
184
+- [ ] 添加API文档生成
185
+- [ ] 实现自动化测试流水线

+ 538
- 0
web/public/test-api.html ファイルの表示

@@ -0,0 +1,538 @@
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测试工具 - svc-code</title>
7
+    <style>
8
+        * {
9
+            margin: 0;
10
+            padding: 0;
11
+            box-sizing: border-box;
12
+        }
13
+        
14
+        body {
15
+            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
16
+            line-height: 1.6;
17
+            color: #333;
18
+            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
19
+            min-height: 100vh;
20
+            padding: 20px;
21
+        }
22
+        
23
+        .container {
24
+            max-width: 1000px;
25
+            margin: 0 auto;
26
+            background: white;
27
+            border-radius: 12px;
28
+            box-shadow: 0 20px 60px rgba(0,0,0,0.3);
29
+            overflow: hidden;
30
+        }
31
+        
32
+        .header {
33
+            background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
34
+            color: white;
35
+            padding: 30px;
36
+            text-align: center;
37
+        }
38
+        
39
+        .header h1 {
40
+            font-size: 2.5rem;
41
+            margin-bottom: 10px;
42
+        }
43
+        
44
+        .header p {
45
+            opacity: 0.9;
46
+            font-size: 1.1rem;
47
+        }
48
+        
49
+        .content {
50
+            padding: 30px;
51
+        }
52
+        
53
+        .section {
54
+            margin-bottom: 30px;
55
+            border: 1px solid #e5e7eb;
56
+            border-radius: 8px;
57
+            overflow: hidden;
58
+        }
59
+        
60
+        .section-header {
61
+            background: #f9fafb;
62
+            padding: 15px 20px;
63
+            border-bottom: 1px solid #e5e7eb;
64
+            font-weight: 600;
65
+            color: #374151;
66
+        }
67
+        
68
+        .section-body {
69
+            padding: 20px;
70
+        }
71
+        
72
+        .config-info {
73
+            background: #f0f9ff;
74
+            border: 1px solid #bae6fd;
75
+            border-radius: 6px;
76
+            padding: 15px;
77
+            margin-bottom: 20px;
78
+        }
79
+        
80
+        .config-info code {
81
+            background: #e0f2fe;
82
+            padding: 2px 6px;
83
+            border-radius: 4px;
84
+            font-family: 'Monaco', 'Menlo', monospace;
85
+            font-size: 0.9em;
86
+        }
87
+        
88
+        .buttons {
89
+            display: flex;
90
+            gap: 10px;
91
+            flex-wrap: wrap;
92
+            margin-bottom: 20px;
93
+        }
94
+        
95
+        button {
96
+            padding: 12px 24px;
97
+            border: none;
98
+            border-radius: 6px;
99
+            font-size: 1rem;
100
+            font-weight: 500;
101
+            cursor: pointer;
102
+            transition: all 0.2s;
103
+            display: flex;
104
+            align-items: center;
105
+            gap: 8px;
106
+        }
107
+        
108
+        button:hover {
109
+            transform: translateY(-2px);
110
+            box-shadow: 0 4px 12px rgba(0,0,0,0.15);
111
+        }
112
+        
113
+        button:active {
114
+            transform: translateY(0);
115
+        }
116
+        
117
+        .btn-primary {
118
+            background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
119
+            color: white;
120
+        }
121
+        
122
+        .btn-secondary {
123
+            background: #f3f4f6;
124
+            color: #374151;
125
+            border: 1px solid #d1d5db;
126
+        }
127
+        
128
+        .btn-success {
129
+            background: linear-gradient(135deg, #10b981 0%, #059669 100%);
130
+            color: white;
131
+        }
132
+        
133
+        .btn-danger {
134
+            background: linear-gradient(135deg, #ef4444 0%, #dc2626 100%);
135
+            color: white;
136
+        }
137
+        
138
+        .btn:disabled {
139
+            opacity: 0.5;
140
+            cursor: not-allowed;
141
+            transform: none !important;
142
+            box-shadow: none !important;
143
+        }
144
+        
145
+        .output {
146
+            background: #1e293b;
147
+            color: #e2e8f0;
148
+            border-radius: 6px;
149
+            padding: 15px;
150
+            font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
151
+            font-size: 0.9em;
152
+            line-height: 1.5;
153
+            white-space: pre-wrap;
154
+            max-height: 400px;
155
+            overflow-y: auto;
156
+            margin-top: 20px;
157
+        }
158
+        
159
+        .status {
160
+            padding: 10px 15px;
161
+            border-radius: 6px;
162
+            margin-bottom: 15px;
163
+            display: none;
164
+        }
165
+        
166
+        .status.success {
167
+            background: #d1fae5;
168
+            color: #065f46;
169
+            border: 1px solid #a7f3d0;
170
+            display: block;
171
+        }
172
+        
173
+        .status.error {
174
+            background: #fee2e2;
175
+            color: #991b1b;
176
+            border: 1px solid #fecaca;
177
+            display: block;
178
+        }
179
+        
180
+        .status.info {
181
+            background: #dbeafe;
182
+            color: #1e40af;
183
+            border: 1px solid #bfdbfe;
184
+            display: block;
185
+        }
186
+        
187
+        .instructions {
188
+            background: #fef3c7;
189
+            border: 1px solid #fde68a;
190
+            border-radius: 6px;
191
+            padding: 15px;
192
+            margin-top: 20px;
193
+        }
194
+        
195
+        .instructions h3 {
196
+            color: #92400e;
197
+            margin-bottom: 10px;
198
+        }
199
+        
200
+        .instructions ul {
201
+            padding-left: 20px;
202
+        }
203
+        
204
+        .instructions li {
205
+            margin-bottom: 5px;
206
+        }
207
+        
208
+        .log-item {
209
+            padding: 5px 0;
210
+            border-bottom: 1px solid #2d3748;
211
+        }
212
+        
213
+        .log-item:last-child {
214
+            border-bottom: none;
215
+        }
216
+        
217
+        .log-time {
218
+            color: #94a3b8;
219
+            margin-right: 10px;
220
+        }
221
+        
222
+        .log-level {
223
+            font-weight: bold;
224
+            margin-right: 10px;
225
+        }
226
+        
227
+        .log-level.info { color: #60a5fa; }
228
+        .log-level.success { color: #34d399; }
229
+        .log-level.error { color: #f87171; }
230
+        .log-level.warn { color: #fbbf24; }
231
+        
232
+        @media (max-width: 768px) {
233
+            .container {
234
+                margin: 10px;
235
+            }
236
+            
237
+            .header h1 {
238
+                font-size: 2rem;
239
+            }
240
+            
241
+            .buttons {
242
+                flex-direction: column;
243
+            }
244
+            
245
+            button {
246
+                width: 100%;
247
+                justify-content: center;
248
+            }
249
+        }
250
+    </style>
251
+</head>
252
+<body>
253
+    <div class="container">
254
+        <div class="header">
255
+            <h1>🔧 API测试工具</h1>
256
+            <p>svc-code 后端API独立测试</p>
257
+        </div>
258
+        
259
+        <div class="content">
260
+            <!-- 配置信息 -->
261
+            <div class="section">
262
+                <div class="section-header">📋 配置信息</div>
263
+                <div class="section-body">
264
+                    <div class="config-info">
265
+                        <p><strong>后端服务:</strong> <code id="backend-url">http://localhost:8020</code></p>
266
+                        <p><strong>API前缀:</strong> <code>/api</code></p>
267
+                        <p><strong>测试端点:</strong> <code>/api/health</code>, <code>/api/projects</code></p>
268
+                        <p><strong>状态:</strong> <span id="connection-status">等待测试...</span></p>
269
+                    </div>
270
+                </div>
271
+            </div>
272
+            
273
+            <!-- 状态显示 -->
274
+            <div id="status-area"></div>
275
+            
276
+            <!-- 测试控制 -->
277
+            <div class="section">
278
+                <div class="section-header">🚀 测试控制</div>
279
+                <div class="section-body">
280
+                    <div class="buttons">
281
+                        <button id="btn-test-all" class="btn-primary">
282
+                            <span>▶️</span> 运行所有测试
283
+                        </button>
284
+                        <button id="btn-test-health" class="btn-secondary">
285
+                            <span>❤️</span> 健康检查
286
+                        </button>
287
+                        <button id="btn-test-projects" class="btn-success">
288
+                            <span>📁</span> 项目列表
289
+                        </button>
290
+                        <button id="btn-clear" class="btn-danger">
291
+                            <span>🗑️</span> 清空日志
292
+                        </button>
293
+                    </div>
294
+                    
295
+                    <div id="output" class="output">
296
+                        <div id="logs">
297
+                            <div class="log-item">
298
+                                <span class="log-time" id="current-time">--:--:--</span>
299
+                                <span class="log-level info">[INFO]</span>
300
+                                <span>API测试工具已就绪!</span>
301
+                            </div>
302
+                            <div class="log-item">
303
+                                <span class="log-time">--:--:--</span>
304
+                                <span class="log-level info">[INFO]</span>
305
+                                <span>点击上方按钮开始测试</span>
306
+                            </div>
307
+                        </div>
308
+                    </div>
309
+                </div>
310
+            </div>
311
+            
312
+            <!-- 使用说明 -->
313
+            <div class="section">
314
+                <div class="section-header">📖 使用说明</div>
315
+                <div class="section-body">
316
+                    <div class="instructions">
317
+                        <h3>如何运行测试:</h3>
318
+                        <ul>
319
+                            <li><strong>运行所有测试</strong>:点击"运行所有测试"按钮,依次测试健康检查和项目列表API</li>
320
+                            <li><strong>单独测试</strong>:点击"健康检查"或"项目列表"按钮测试特定API</li>
321
+                            <li><strong>控制台测试</strong>:在浏览器控制台中输入 <code>window.testAPI.runAllTests()</code></li>
322
+                        </ul>
323
+                        
324
+                        <h3>前提条件:</h3>
325
+                        <ul>
326
+                            <li>后端服务必须正在运行(端口8020)</li>
327
+                            <li>确保没有跨域问题(CORS已配置)</li>
328
+                            <li>如果测试失败,请检查后端日志</li>
329
+                        </ul>
330
+                        
331
+                        <h3>快速命令:</h3>
332
+                        <pre style="background:#1e293b;color:#e2e8f0;padding:10px;border-radius:4px;margin-top:10px;">
333
+# 启动后端服务
334
+cd svc-code && go run main.go
335
+
336
+# 测试API(命令行)
337
+curl http://localhost:8020/api/health
338
+curl http://localhost:8020/api/projects</pre>
339
+                    </div>
340
+                </div>
341
+            </div>
342
+        </div>
343
+    </div>
344
+
345
+    <script type="module">
346
+        // 更新时间显示
347
+        function updateTime() {
348
+            const now = new Date();
349
+            const timeStr = now.toTimeString().split(' ')[0];
350
+            document.querySelectorAll('#current-time').forEach(el => {
351
+                el.textContent = timeStr;
352
+            });
353
+        }
354
+        setInterval(updateTime, 1000);
355
+        updateTime();
356
+        
357
+        // 添加日志
358
+        function addLog(level, message) {
359
+            const now = new Date();
360
+            const timeStr = now.toTimeString().split(' ')[0];
361
+            const logItem = document.createElement('div');
362
+            logItem.className = 'log-item';
363
+            logItem.innerHTML = `
364
+                <span class="log-time">${timeStr}</span>
365
+                <span class="log-level ${level}">[${level.toUpperCase()}]</span>
366
+                <span>${message}</span>
367
+            `;
368
+            document.getElementById('logs').prepend(logItem);
369
+            
370
+            // 限制日志数量
371
+            const logs = document.getElementById('logs');
372
+            if (logs.children.length > 50) {
373
+                logs.removeChild(logs.lastChild);
374
+            }
375
+        }
376
+        
377
+        // 显示状态
378
+        function showStatus(type, message) {
379
+            const statusArea = document.getElementById('status-area');
380
+            const statusDiv = document.createElement('div');
381
+            statusDiv.className = `status ${type}`;
382
+            statusDiv.textContent = message;
383
+            statusArea.innerHTML = '';
384
+            statusArea.appendChild(statusDiv);
385
+        }
386
+        
387
+        // API请求函数
388
+        async function apiRequest(endpoint) {
389
+            const backendUrl = document.getElementById('backend-url').textContent;
390
+            const url = `${backendUrl}/api${endpoint}`;
391
+            const startTime = Date.now();
392
+            
393
+            try {
394
+                const response = await fetch(url);
395
+                const duration = Date.now() - startTime;
396
+                
397
+                if (!response.ok) {
398
+                    throw new Error(`HTTP ${response.status}`);
399
+                }
400
+                
401
+                const data = await response.json();
402
+                return { success: true, data, duration };
403
+            } catch (error) {
404
+                const duration = Date.now() - startTime;
405
+                return { success: false, error: error.message, duration };
406
+            }
407
+        }
408
+        
409
+        // 测试函数
410
+        async function testHealth() {
411
+            showStatus('info', '正在测试健康检查API...');
412
+            addLog('info', '开始健康检查测试');
413
+            
414
+            const result = await apiRequest('/health');
415
+            
416
+            if (result.success) {
417
+                const isHealthy = result.data?.data?.healthy || result.data?.healthy;
418
+                const message = `健康检查成功!状态: ${isHealthy ? '健康' : '不健康'}, 耗时: ${result.duration}ms`;
419
+                showStatus('success', message);
420
+                addLog('success', message);
421
+                return true;
422
+            } else {
423
+                const message = `健康检查失败: ${result.error}`;
424
+                showStatus('error', message);
425
+                addLog('error', message);
426
+                return false;
427
+            }
428
+        }
429
+        
430
+        async function testProjects() {
431
+            showStatus('info', '正在测试项目列表API...');
432
+            addLog('info', '开始项目列表测试');
433
+            
434
+            const result = await apiRequest('/projects');
435
+            
436
+            if (result.success) {
437
+                const projects = result.data?.data || result.data || [];
438
+                const message = `获取到 ${projects.length} 个项目, 耗时: ${result.duration}ms`;
439
+                showStatus('success', message);
440
+                addLog('success', message);
441
+                
442
+                // 显示项目详情
443
+                if (projects.length > 0) {
444
+                    projects.forEach((project, index) => {
445
+                        addLog('info', `项目 ${index + 1}: ${project.name} (${project.id})`);
446
+                    });
447
+                }
448
+                return true;
449
+            } else {
450
+                const message = `项目列表测试失败: ${result.error}`;
451
+                showStatus('error', message);
452
+                addLog('error', message);
453
+                return false;
454
+            }
455
+        }
456
+        
457
+        async function runAllTests() {
458
+            showStatus('info', '开始运行所有测试...');
459
+            addLog('info', '='.repeat(50));
460
+            addLog('info', '开始运行所有API测试');
461
+            addLog('info', '='.repeat(50));
462
+            
463
+            // 测试健康检查
464
+            const healthOk = await testHealth();
465
+            
466
+            // 只有健康检查通过才测试项目列表
467
+            let projectsOk = false;
468
+            if (healthOk) {
469
+                projectsOk = await testProjects();
470
+            } else {
471
+                addLog('warn', '跳过项目列表测试(健康检查失败)');
472
+            }
473
+            
474
+            // 显示结果
475
+            addLog('info', '='.repeat(50));
476
+            addLog('info', '测试结果汇总:');
477
+            addLog('info', `健康检查: ${healthOk ? '✅ 通过' : '❌ 失败'}`);
478
+            addLog('info', `项目列表: ${projectsOk ? '✅ 通过' : healthOk ? '❌ 失败' : '⏭️ 跳过'}`);
479
+            
480
+            const allPassed = healthOk && projectsOk;
481
+            addLog(allPassed ? 'success' : 'warn', 
482
+                   allPassed ? '🎉 所有测试通过!' : '⚠️ 部分测试失败');
483
+            
484
+            showStatus(allPassed ? 'success' : 'error', 
485
+                      allPassed ? '所有测试通过!' : '部分测试失败');
486
+        }
487
+        
488
+        // 按钮事件
489
+        document.getElementById('btn-test-all').addEventListener('click', runAllTests);
490
+        document.getElementById('btn-test-health').addEventListener('click', testHealth);
491
+        document.getElementById('btn-test-projects').addEventListener('click', testProjects);
492
+        document.getElementById('btn-clear').addEventListener('click', () => {
493
+            document.getElementById('logs').innerHTML = `
494
+                <div class="log-item">
495
+                    <span class="log-time" id="current-time">--:--:--</span>
496
+                    <span class="log-level info">[INFO]</span>
497
+                    <span>日志已清空</span>
498
+                </div>
499
+            `;
500
+            addLog('info', '日志已清空');
501
+            document.getElementById('status-area').innerHTML = '';
502
+        });
503
+        
504
+        // 更新连接状态
505
+        async function checkConnection() {
506
+            const backendUrl = document.getElementById('backend-url').textContent;
507
+            const statusEl = document.getElementById('connection-status');
508
+            
509
+            try {
510
+                const response = await fetch(`${backendUrl}/api/health`, { signal: AbortSignal.timeout(3000) });
511
+                if (response.ok) {
512
+                    statusEl.textContent = '✅ 连接正常';
513
+                    statusEl.style.color = '#10b981';
514
+                } else {
515
+                    statusEl.textContent = '⚠️ 连接异常';
516
+                    statusEl.style.color = '#f59e0b';
517
+                }
518
+            } catch {
519
+                statusEl.textContent = '❌ 无法连接';
520
+                statusEl.style.color = '#ef4444';
521
+            }
522
+        }
523
+        
524
+        // 初始连接检查
525
+        setTimeout(checkConnection, 1000);
526
+        
527
+        // 在控制台中注册测试函数
528
+        window.testAPI = {
529
+            runAllTests,
530
+            testHealth,
531
+            testProjects,
532
+            checkConnection
533
+        };
534
+        
535
+        addLog('success', '测试工具已初始化,在控制台输入 window.testAPI.runAllTests() 开始测试');
536
+    </script>
537
+</body>
538
+</html>

+ 113
- 0
web/src/api/health.ts ファイルの表示

@@ -0,0 +1,113 @@
1
+/**
2
+ * 健康检查API模块
3
+ * 对应后端: GET /api/health
4
+ */
5
+
6
+import request from './index'
7
+
8
+/**
9
+ * 健康检查响应接口
10
+ */
11
+export interface HealthCheckResponse {
12
+  healthy: boolean
13
+  version: string
14
+  [key: string]: any
15
+}
16
+
17
+/**
18
+ * 检查后端服务健康状态
19
+ */
20
+export const checkHealth = async (): Promise<HealthCheckResponse> => {
21
+  const response = await request.get('/health')
22
+  return response
23
+}
24
+
25
+/**
26
+ * 测试后端连接
27
+ * @param timeoutMs 超时时间(毫秒)
28
+ */
29
+export const testConnection = async (timeoutMs: number = 5000): Promise<{
30
+  success: boolean
31
+  duration: number
32
+  data?: HealthCheckResponse
33
+  error?: string
34
+}> => {
35
+  const startTime = Date.now()
36
+  
37
+  try {
38
+    // 创建超时Promise
39
+    const timeoutPromise = new Promise<never>((_, reject) => {
40
+      setTimeout(() => reject(new Error(`连接超时 (${timeoutMs}ms)`)), timeoutMs)
41
+    })
42
+    
43
+    // 执行健康检查
44
+    const healthPromise = checkHealth()
45
+    
46
+    const data = await Promise.race([healthPromise, timeoutPromise])
47
+    const duration = Date.now() - startTime
48
+    
49
+    return {
50
+      success: true,
51
+      duration,
52
+      data
53
+    }
54
+  } catch (error: any) {
55
+    const duration = Date.now() - startTime
56
+    
57
+    return {
58
+      success: false,
59
+      duration,
60
+      error: error.message || '未知错误'
61
+    }
62
+  }
63
+}
64
+
65
+/**
66
+ * 监控后端服务状态
67
+ * @param intervalMs 检查间隔(毫秒)
68
+ * @param maxChecks 最大检查次数
69
+ */
70
+export const monitorHealth = (
71
+  onStatusChange: (status: HealthCheckResponse) => void,
72
+  intervalMs: number = 30000,
73
+  maxChecks: number = -1 // -1 表示无限
74
+) => {
75
+  let checkCount = 0
76
+  let lastStatus: HealthCheckResponse | null = null
77
+  
78
+  const check = async () => {
79
+    try {
80
+      const status = await checkHealth()
81
+      
82
+      // 只在状态变化时通知
83
+      if (!lastStatus || JSON.stringify(lastStatus) !== JSON.stringify(status)) {
84
+        lastStatus = status
85
+        onStatusChange(status)
86
+      }
87
+    } catch (error) {
88
+      console.error('健康检查失败:', error)
89
+    }
90
+    
91
+    checkCount++
92
+    
93
+    // 继续检查(如果未达到最大次数)
94
+    if (maxChecks === -1 || checkCount < maxChecks) {
95
+      setTimeout(check, intervalMs)
96
+    }
97
+  }
98
+  
99
+  // 立即开始第一次检查
100
+  check()
101
+  
102
+  // 返回停止监控的函数
103
+  return () => {
104
+    // 这里可以添加清理逻辑
105
+    console.log('停止健康监控')
106
+  }
107
+}
108
+
109
+export default {
110
+  checkHealth,
111
+  testConnection,
112
+  monitorHealth
113
+}

+ 8
- 0
web/src/api/index.ts ファイルの表示

@@ -36,6 +36,14 @@ request.interceptors.response.use(
36 36
         return Promise.reject(new Error(response.data.message || '请求失败'))
37 37
       }
38 38
     }
39
+    // 检查success字段(当前OpenCode API使用)
40
+    if (response.data && response.data.success !== undefined) {
41
+      if (response.data.success === true) {
42
+        return response.data.data || response.data
43
+      } else {
44
+        return Promise.reject(new Error(response.data.message || response.data.error || '请求失败'))
45
+      }
46
+    }
39 47
     return response.data
40 48
   },
41 49
   (error) => {

+ 1
- 0
web/src/api/init.ts ファイルの表示

@@ -1,4 +1,5 @@
1 1
 import request from './index'
2
+import { getProjects } from './project'
2 3
 
3 4
 // 树节点类型
4 5
 export interface TreeNode {

+ 33
- 16
web/src/api/project.ts ファイルの表示

@@ -66,64 +66,81 @@ export interface SSEMessage {
66 66
  * 获取项目列表
67 67
  */
68 68
 export const getProjects = async (): Promise<ProjectInfo[]> => {
69
-  const response = await request.get('/api/projects')
70
-  return response.data
69
+  const response = await request.get('/projects')
70
+  // 根据拦截器逻辑,response可能已经是data数组,也可能是完整的响应对象
71
+  // 确保我们始终返回项目数组
72
+  if (Array.isArray(response)) {
73
+    return response
74
+  } else if (response && response.data && Array.isArray(response.data)) {
75
+    return response.data
76
+  } else if (response && Array.isArray(response)) {
77
+    return response
78
+  }
79
+  console.warn('获取项目列表返回格式异常:', response)
80
+  return []
71 81
 }
72 82
 
73 83
 /**
74 84
  * 启动项目OpenCode实例
75 85
  */
76 86
 export const startInstance = async (projectId: string, params: StartInstanceRequest): Promise<OpenCodeInstance> => {
77
-  const response = await request.post(`/api/opencode/projects/${projectId}/start`, params)
78
-  return response.data
87
+  const response = await request.post(`/opencode/projects/${projectId}/start`, params)
88
+  // 后端返回格式: {success: true, data: {...}}
89
+  return response.data?.data || response.data
79 90
 }
80 91
 
81 92
 /**
82 93
  * 停止项目OpenCode实例
83 94
  */
84 95
 export const stopInstance = async (projectId: string): Promise<boolean> => {
85
-  const response = await request.post(`/api/opencode/projects/${projectId}/stop`, {})
86
-  return response.success
96
+  const response = await request.post(`/opencode/projects/${projectId}/stop`, {})
97
+  // 后端返回格式: {success: true, data: {...}} 或 {success: true}
98
+  return response.data?.success ?? response.success ?? false
87 99
 }
88 100
 
89 101
 /**
90 102
  * 获取项目实例状态
91 103
  */
92 104
 export const getInstanceStatus = async (projectId: string): Promise<OpenCodeInstance> => {
93
-  const response = await request.get(`/api/opencode/projects/${projectId}/status`)
94
-  return response.data
105
+  const response = await request.get(`/opencode/projects/${projectId}/status`)
106
+  // 后端返回格式: {success: true, data: {...}}
107
+  return response.data?.data || response.data
95 108
 }
96 109
 
97 110
 /**
98 111
  * 创建OpenCode会话
99 112
  */
100 113
 export const createSession = async (projectId: string, params: CreateSessionRequest): Promise<any> => {
101
-  const response = await request.post(`/api/opencode/projects/${projectId}/sessions`, params)
102
-  return response.data
114
+  const response = await request.post(`/opencode/projects/${projectId}/sessions`, params)
115
+  // 后端返回格式: {success: true, data: {...}}
116
+  return response.data?.data || response.data
103 117
 }
104 118
 
105 119
 /**
106 120
  * 发送消息到OpenCode会话(同步)
107 121
  */
108 122
 export const sendMessage = async (projectId: string, params: SendMessageRequest): Promise<any> => {
109
-  const response = await request.post(`/api/opencode/projects/${projectId}/messages`, params)
110
-  return response.data
123
+  const response = await request.post(`/opencode/projects/${projectId}/messages`, params)
124
+  // 后端返回格式: {success: true, data: {...}}
125
+  return response.data?.data || response.data
111 126
 }
112 127
 
113 128
 /**
114 129
  * 异步发送消息到OpenCode会话
115 130
  */
116 131
 export const sendMessageAsync = async (projectId: string, params: SendMessageRequest): Promise<any> => {
117
-  const response = await request.post(`/api/opencode/projects/${projectId}/messages/async`, params)
118
-  return response.data
132
+  const response = await request.post(`/opencode/projects/${projectId}/messages/async`, params)
133
+  // 后端返回格式: {success: true, data: {...}}
134
+  return response.data?.data || response.data
119 135
 }
120 136
 
121 137
 /**
122 138
  * 准备项目目录
123 139
  */
124 140
 export const prepareDirectory = async (projectId: string): Promise<ProjectDirectory> => {
125
-  const response = await request.post(`/api/projects/${projectId}/prepare-directory`, {})
126
-  return response.data
141
+  const response = await request.post(`/projects/${projectId}/prepare-directory`, {})
142
+  // 后端返回格式: {success: true, data: {...}}
143
+  return response.data?.data || response.data
127 144
 }
128 145
 
129 146
 /**

+ 2
- 1
web/src/router/index.ts ファイルの表示

@@ -36,7 +36,8 @@ const router = createRouter({
36 36
       path: '/report',
37 37
       name: 'report',
38 38
       component: () => import('@/views/report/Index.vue')
39
-    }
39
+    },
40
+
40 41
   ]
41 42
 })
42 43
 

+ 336
- 0
web/src/test/api-test-standalone.ts ファイルの表示

@@ -0,0 +1,336 @@
1
+/**
2
+ * 独立API测试模块 - 纯TypeScript/JavaScript
3
+ * 可以直接在浏览器控制台或Node.js中运行
4
+ * 不依赖Vite别名或前端代理配置
5
+ * 直接调用后端API服务
6
+ */
7
+
8
+interface ProjectInfo {
9
+  id: string
10
+  name: string
11
+  description: string
12
+  path: string
13
+  tenant_id: string
14
+  creator: string
15
+  created_at: string
16
+}
17
+
18
+interface HealthCheckResponse {
19
+  healthy: boolean
20
+  version: string
21
+  [key: string]: any
22
+}
23
+
24
+interface TestResult {
25
+  success: boolean
26
+  duration: number
27
+  data?: any
28
+  error?: string
29
+}
30
+
31
+/**
32
+ * 配置
33
+ */
34
+const CONFIG = {
35
+  // 后端API基础URL
36
+  BACKEND_BASE_URL: 'http://localhost:8020',
37
+  // API前缀
38
+  API_PREFIX: '/api',
39
+  // 超时时间(毫秒)
40
+  TIMEOUT: 10000,
41
+}
42
+
43
+/**
44
+ * 获取完整的API URL
45
+ */
46
+function getApiUrl(endpoint: string): string {
47
+  // 确保endpoint以/开头
48
+  const normalizedEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`
49
+  return `${CONFIG.BACKEND_BASE_URL}${CONFIG.API_PREFIX}${normalizedEndpoint}`
50
+}
51
+
52
+/**
53
+ * 发起API请求
54
+ */
55
+async function apiRequest<T = any>(
56
+  method: string,
57
+  endpoint: string,
58
+  data?: any
59
+): Promise<T> {
60
+  const url = getApiUrl(endpoint)
61
+  const controller = new AbortController()
62
+  const timeoutId = setTimeout(() => controller.abort(), CONFIG.TIMEOUT)
63
+
64
+  try {
65
+    const response = await fetch(url, {
66
+      method,
67
+      headers: {
68
+        'Content-Type': 'application/json',
69
+        'Accept': 'application/json',
70
+      },
71
+      body: data ? JSON.stringify(data) : undefined,
72
+      signal: controller.signal,
73
+    })
74
+
75
+    clearTimeout(timeoutId)
76
+
77
+    if (!response.ok) {
78
+      let errorMessage = `HTTP ${response.status}`
79
+      try {
80
+        const errorData = await response.json()
81
+        errorMessage += `: ${JSON.stringify(errorData)}`
82
+      } catch {
83
+        // 忽略JSON解析错误
84
+      }
85
+      throw new Error(errorMessage)
86
+    }
87
+
88
+    const responseData = await response.json()
89
+    
90
+    // 处理后端返回格式(假设是 {success: true, data: ...} 或直接是数据)
91
+    if (responseData.success !== undefined) {
92
+      if (!responseData.success) {
93
+        throw new Error(responseData.message || responseData.error || '请求失败')
94
+      }
95
+      return responseData.data || responseData
96
+    }
97
+    
98
+    return responseData
99
+  } catch (error: any) {
100
+    clearTimeout(timeoutId)
101
+    if (error.name === 'AbortError') {
102
+      throw new Error(`请求超时 (${CONFIG.TIMEOUT}ms)`)
103
+    }
104
+    throw error
105
+  }
106
+}
107
+
108
+/**
109
+ * API函数 - 测试专用(直接调用后端)
110
+ */
111
+export const testApi = {
112
+  /**
113
+   * 获取项目列表
114
+   */
115
+  async getProjects(): Promise<ProjectInfo[]> {
116
+    return apiRequest<ProjectInfo[]>('GET', '/projects')
117
+  },
118
+
119
+  /**
120
+   * 健康检查
121
+   */
122
+  async checkHealth(): Promise<HealthCheckResponse> {
123
+    return apiRequest<HealthCheckResponse>('GET', '/health')
124
+  },
125
+
126
+  /**
127
+   * 测试连接
128
+   */
129
+  async testConnection(timeoutMs: number = 5000): Promise<TestResult> {
130
+    const startTime = Date.now()
131
+    
132
+    try {
133
+      const controller = new AbortController()
134
+      const timeoutId = setTimeout(() => controller.abort(), timeoutMs)
135
+      
136
+      const response = await fetch(getApiUrl('/health'), {
137
+        signal: controller.signal,
138
+      })
139
+      
140
+      clearTimeout(timeoutId)
141
+      
142
+      if (!response.ok) {
143
+        throw new Error(`HTTP ${response.status}`)
144
+      }
145
+      
146
+      const data = await response.json()
147
+      const duration = Date.now() - startTime
148
+      
149
+      return {
150
+        success: true,
151
+        duration,
152
+        data
153
+      }
154
+    } catch (error: any) {
155
+      const duration = Date.now() - startTime
156
+      return {
157
+        success: false,
158
+        duration,
159
+        error: error.message || '未知错误'
160
+      }
161
+    }
162
+  },
163
+}
164
+
165
+/**
166
+ * 测试函数
167
+ */
168
+export async function testGetProjects(): Promise<boolean> {
169
+  console.log('🧪 开始测试 getProjects API (直接调用后端)...')
170
+  console.log(`   后端URL: ${CONFIG.BACKEND_BASE_URL}`)
171
+  console.log(`   API端点: ${getApiUrl('/projects')}`)
172
+  
173
+  try {
174
+    const projects = await testApi.getProjects()
175
+    
176
+    console.log(`✅ 测试成功!获取到 ${projects.length} 个项目`)
177
+    
178
+    if (projects.length > 0) {
179
+      console.log('   项目列表:')
180
+      projects.forEach((project: ProjectInfo, index: number) => {
181
+        console.log(`     ${index + 1}. ${project.name} (ID: ${project.id})`)
182
+        console.log(`         描述: ${project.description}`)
183
+        console.log(`         路径: ${project.path}`)
184
+      })
185
+    } else {
186
+      console.log('⚠️  警告: 项目列表为空')
187
+    }
188
+    
189
+    return true
190
+  } catch (error: any) {
191
+    console.error('❌ 测试失败:', error.message || error)
192
+    console.error('   请检查:')
193
+    console.error('   1. 后端服务是否运行在端口8020')
194
+    console.error('   2. 网络连接是否正常')
195
+    console.error('   3. 跨域设置是否正确')
196
+    return false
197
+  }
198
+}
199
+
200
+export async function testHealthCheck(): Promise<boolean> {
201
+  console.log('🧪 开始测试健康检查 API...')
202
+  console.log(`   后端URL: ${CONFIG.BACKEND_BASE_URL}`)
203
+  console.log(`   API端点: ${getApiUrl('/health')}`)
204
+  
205
+  try {
206
+    const health = await testApi.checkHealth()
207
+    
208
+    console.log(`✅ 健康检查成功!`)
209
+    console.log(`   健康状态: ${health.healthy ? '健康' : '不健康'}`)
210
+    console.log(`   版本: ${health.version}`)
211
+    
212
+    if (!health.healthy) {
213
+      console.warn('⚠️  警告: 后端报告不健康状态')
214
+    }
215
+    
216
+    return true
217
+  } catch (error: any) {
218
+    console.error('❌ 健康检查失败:', error.message || error)
219
+    return false
220
+  }
221
+}
222
+
223
+export async function testBackendConnection(): Promise<boolean> {
224
+  console.log('🧪 开始测试后端连接...')
225
+  console.log(`   后端URL: ${CONFIG.BACKEND_BASE_URL}`)
226
+  
227
+  try {
228
+    const result = await testApi.testConnection(3000)
229
+    
230
+    if (result.success) {
231
+      console.log(`✅ 连接测试成功!`)
232
+      console.log(`   响应时间: ${result.duration}ms`)
233
+      console.log(`   健康状态: ${result.data?.healthy ? '健康' : '不健康'}`)
234
+      return true
235
+    } else {
236
+      console.error(`❌ 连接测试失败: ${result.error}`)
237
+      return false
238
+    }
239
+  } catch (error: any) {
240
+    console.error('❌ 连接测试异常:', error.message || error)
241
+    return false
242
+  }
243
+}
244
+
245
+/**
246
+ * 运行所有测试
247
+ */
248
+export async function runAllTests() {
249
+  console.log('='.repeat(60))
250
+  console.log('🚀 开始运行独立API测试 (直接调用后端)')
251
+  console.log('='.repeat(60))
252
+  console.log(`后端服务: ${CONFIG.BACKEND_BASE_URL}`)
253
+  console.log(`API前缀: ${CONFIG.API_PREFIX}`)
254
+  console.log('='.repeat(60))
255
+  
256
+  const results = {
257
+    healthCheck: false,
258
+    connection: false,
259
+    getProjects: false,
260
+  }
261
+  
262
+  // 测试健康检查
263
+  results.healthCheck = await testHealthCheck()
264
+  
265
+  // 测试连接
266
+  results.connection = await testBackendConnection()
267
+  
268
+  // 测试获取项目列表
269
+  if (results.connection) {
270
+    results.getProjects = await testGetProjects()
271
+  } else {
272
+    console.log('⏭️  跳过项目列表测试(连接测试失败)')
273
+  }
274
+  
275
+  console.log('='.repeat(60))
276
+  console.log('📊 测试结果汇总:')
277
+  console.log('='.repeat(60))
278
+  console.log(`健康检查: ${results.healthCheck ? '✅ 通过' : '❌ 失败'}`)
279
+  console.log(`连接测试: ${results.connection ? '✅ 通过' : '❌ 失败'}`)
280
+  console.log(`获取项目: ${results.getProjects ? '✅ 通过' : results.connection ? '❌ 失败' : '⏭️  跳过'}`)
281
+  
282
+  const allPassed = Object.values(results).every(result => result === true)
283
+  console.log(`\n${allPassed ? '🎉 所有测试通过!' : '⚠️  部分测试失败'}`)
284
+  
285
+  return allPassed
286
+}
287
+
288
+/**
289
+ * 浏览器控制台快捷方式
290
+ */
291
+if (typeof window !== 'undefined') {
292
+  // 在浏览器控制台中可以使用 window.testAPI 来运行测试
293
+  ;(window as any).testAPI = {
294
+    runAllTests,
295
+    testGetProjects,
296
+    testHealthCheck,
297
+    testBackendConnection,
298
+    testApi,
299
+  }
300
+  
301
+  console.log(`
302
+🛠️  API测试工具已加载!
303
+在浏览器控制台中输入以下命令进行测试:
304
+
305
+1. testAPI.runAllTests()          - 运行所有测试
306
+2. testAPI.testGetProjects()      - 测试项目列表API
307
+3. testAPI.testHealthCheck()      - 测试健康检查API
308
+4. testAPI.testBackendConnection() - 测试后端连接
309
+5. testAPI.testApi.getProjects()  - 直接调用API函数
310
+
311
+后端配置:
312
+- URL: ${CONFIG.BACKEND_BASE_URL}
313
+- API前缀: ${CONFIG.API_PREFIX}
314
+`)
315
+}
316
+
317
+/**
318
+ * Node.js环境支持
319
+ */
320
+if (typeof module !== 'undefined' && module.exports) {
321
+  module.exports = {
322
+    runAllTests,
323
+    testGetProjects,
324
+    testHealthCheck,
325
+    testBackendConnection,
326
+    testApi,
327
+  }
328
+}
329
+
330
+export default {
331
+  runAllTests,
332
+  testGetProjects,
333
+  testHealthCheck,
334
+  testBackendConnection,
335
+  testApi,
336
+}

+ 171
- 0
web/src/test/verify-setup.js ファイルの表示

@@ -0,0 +1,171 @@
1
+/**
2
+ * 验证API架构设置
3
+ * 在浏览器控制台中运行此脚本
4
+ */
5
+
6
+console.log('🔍 验证API架构设置...');
7
+console.log('='.repeat(60));
8
+
9
+// 测试配置
10
+const config = {
11
+  backend: 'http://localhost:8020',
12
+  frontend: 'http://localhost:3000',
13
+  apiPrefix: '/api'
14
+};
15
+
16
+// 1. 测试直接后端连接
17
+async function testDirectBackend() {
18
+  console.log('🧪 测试直接后端连接...');
19
+  try {
20
+    const response = await fetch(`${config.backend}${config.apiPrefix}/health`);
21
+    if (response.ok) {
22
+      const data = await response.json();
23
+      console.log(`✅ 直接连接成功: ${JSON.stringify(data)}`);
24
+      return true;
25
+    } else {
26
+      console.log(`❌ 直接连接失败: HTTP ${response.status}`);
27
+      return false;
28
+    }
29
+  } catch (error) {
30
+    console.log(`❌ 直接连接异常: ${error.message}`);
31
+    return false;
32
+  }
33
+}
34
+
35
+// 2. 测试前端代理连接
36
+async function testFrontendProxy() {
37
+  console.log('🧪 测试前端代理连接...');
38
+  try {
39
+    const response = await fetch(`${config.frontend}/api/health`);
40
+    if (response.ok) {
41
+      const data = await response.json();
42
+      console.log(`✅ 代理连接成功: ${JSON.stringify(data)}`);
43
+      return true;
44
+    } else {
45
+      console.log(`❌ 代理连接失败: HTTP ${response.status}`);
46
+      return false;
47
+    }
48
+  } catch (error) {
49
+    console.log(`❌ 代理连接异常: ${error.message}`);
50
+    return false;
51
+  }
52
+}
53
+
54
+// 3. 测试项目列表API
55
+async function testProjectsAPI() {
56
+  console.log('🧪 测试项目列表API...');
57
+  
58
+  // 测试直接连接
59
+  try {
60
+    const directResponse = await fetch(`${config.backend}${config.apiPrefix}/projects`);
61
+    const directData = directResponse.ok ? await directResponse.json() : null;
62
+    
63
+    // 测试代理连接
64
+    const proxyResponse = await fetch(`${config.frontend}/api/projects`);
65
+    const proxyData = proxyResponse.ok ? await proxyResponse.json() : null;
66
+    
67
+    console.log(`📊 直接连接结果: ${directResponse.status}, 项目数: ${directData?.data?.length || 0}`);
68
+    console.log(`📊 代理连接结果: ${proxyResponse.status}, 项目数: ${proxyData?.data?.length || 0}`);
69
+    
70
+    if (directResponse.ok && proxyResponse.ok) {
71
+      const directProjects = directData?.data || directData || [];
72
+      const proxyProjects = proxyData?.data || proxyData || [];
73
+      
74
+      console.log(`✅ 两种方式都成功`);
75
+      console.log(`📁 项目列表:`);
76
+      directProjects.forEach((project, index) => {
77
+        console.log(`   ${index + 1}. ${project.name} (${project.id})`);
78
+      });
79
+      
80
+      return true;
81
+    } else {
82
+      console.log(`⚠️  至少有一种方式失败`);
83
+      return false;
84
+    }
85
+  } catch (error) {
86
+    console.log(`❌ 测试异常: ${error.message}`);
87
+    return false;
88
+  }
89
+}
90
+
91
+// 4. 验证架构分离
92
+function verifyArchitecture() {
93
+  console.log('🧠 验证架构分离...');
94
+  
95
+  const architecture = {
96
+    businessAPI: {
97
+      path: 'src/api/',
98
+      purpose: '页面业务使用',
99
+      callPath: '相对路径 (/projects) → Vite代理 → 后端',
100
+      files: ['project.ts', 'health.ts']
101
+    },
102
+    testAPI: {
103
+      path: 'src/test/',
104
+      purpose: '独立测试使用',
105
+      callPath: '直接调用后端 (http://localhost:8020/api/...)',
106
+      files: ['api-test-standalone.ts']
107
+    }
108
+  };
109
+  
110
+  console.log('📁 业务API模块:');
111
+  console.log(`   - 用途: ${architecture.businessAPI.purpose}`);
112
+  console.log(`   - 调用路径: ${architecture.businessAPI.callPath}`);
113
+  console.log(`   - 文件: ${architecture.businessAPI.files.join(', ')}`);
114
+  
115
+  console.log('📁 测试API模块:');
116
+  console.log(`   - 用途: ${architecture.testAPI.purpose}`);
117
+  console.log(`   - 调用路径: ${architecture.testAPI.callPath}`);
118
+  console.log(`   - 文件: ${architecture.testAPI.files.join(', ')}`);
119
+  
120
+  console.log('✅ 架构验证完成');
121
+  return true;
122
+}
123
+
124
+// 运行所有验证
125
+async function runAllVerifications() {
126
+  console.log('🚀 开始验证API架构设置');
127
+  console.log('='.repeat(60));
128
+  
129
+  const results = {
130
+    directBackend: await testDirectBackend(),
131
+    frontendProxy: await testFrontendProxy(),
132
+    projectsAPI: await testProjectsAPI(),
133
+    architecture: verifyArchitecture()
134
+  };
135
+  
136
+  console.log('='.repeat(60));
137
+  console.log('📊 验证结果汇总:');
138
+  console.log(`   直接后端连接: ${results.directBackend ? '✅' : '❌'}`);
139
+  console.log(`   前端代理连接: ${results.frontendProxy ? '✅' : '❌'}`);
140
+  console.log(`   项目列表API: ${results.projectsAPI ? '✅' : '❌'}`);
141
+  console.log(`   架构验证: ${results.architecture ? '✅' : '❌'}`);
142
+  
143
+  const allPassed = Object.values(results).every(r => r);
144
+  console.log(`\n${allPassed ? '🎉 所有验证通过!' : '⚠️  部分验证失败'}`);
145
+  
146
+  if (!allPassed) {
147
+    console.log('\n🔧 故障排除:');
148
+    console.log('   1. 确保后端运行: cd svc-code && go run main.go');
149
+    console.log('   2. 确保前端运行: cd svc-code/web && npm run dev');
150
+    console.log('   3. 检查端口占用: lsof -i :8020, lsof -i :3000');
151
+    console.log('   4. 检查Vite代理配置: vite.config.ts');
152
+  }
153
+  
154
+  return allPassed;
155
+}
156
+
157
+// 自动运行(如果在浏览器中)
158
+if (typeof window !== 'undefined') {
159
+  console.log('🛠️  验证脚本已加载');
160
+  console.log('   输入 runAllVerifications() 运行验证');
161
+  window.runAllVerifications = runAllVerifications;
162
+}
163
+
164
+// 导出函数
165
+export {
166
+  testDirectBackend,
167
+  testFrontendProxy,
168
+  testProjectsAPI,
169
+  verifyArchitecture,
170
+  runAllVerifications
171
+};

+ 116
- 31
web/src/views/init/Index.vue ファイルの表示

@@ -14,6 +14,7 @@
14 14
               </div>
15 15
               <div class="tree-container">
16 16
                 <el-tree
17
+                  v-if="!treeLoading && treeData.length > 0"
17 18
                   ref="treeRef"
18 19
                   :data="treeData"
19 20
                   :props="treeProps"
@@ -25,8 +26,9 @@
25 26
                 >
26 27
                   <template #default="{ node, data }">
27 28
                     <span class="tree-node">
28
-                      <el-icon v-if="data.icon" :size="16">
29
-                        <component :is="data.icon" />
29
+                      <el-icon :size="16">
30
+                        <component :is="iconMap[data.icon]" v-if="data.icon && iconMap[data.icon]" />
31
+                        <Document v-else />
30 32
                       </el-icon>
31 33
                       <span>{{ node.label }}</span>
32 34
                       <span v-if="data.count" class="node-count">
@@ -35,6 +37,14 @@
35 37
                     </span>
36 38
                   </template>
37 39
                 </el-tree>
40
+                <div v-else-if="treeLoading" class="tree-loading">
41
+                  <el-icon class="loading-icon"><Loading /></el-icon>
42
+                  <span>加载项目列表中...</span>
43
+                </div>
44
+                <div v-else class="tree-empty">
45
+                  <el-icon><Document /></el-icon>
46
+                  <span>暂无项目数据</span>
47
+                </div>
38 48
               </div>
39 49
             </div>
40 50
           </Pane>
@@ -207,7 +217,7 @@
207 217
                                   :disabled="getInstanceStatus(selectedProjectId) === 'running' || getInstanceStatus(selectedProjectId) === 'starting'"
208 218
                                   :loading="getInstanceStatus(selectedProjectId) === 'starting'"
209 219
                                 >
210
-                                  <el-icon><PlayCircle /></el-icon>
220
+                                  <el-icon><CaretRight /></el-icon>
211 221
                                   启动实例
212 222
                                 </el-button>
213 223
                                 <el-button 
@@ -216,7 +226,7 @@
216 226
                                   @click="stopOpenCodeInstance(selectedProjectId)"
217 227
                                   :disabled="getInstanceStatus(selectedProjectId) !== 'running'"
218 228
                                 >
219
-                                  <el-icon><Stop /></el-icon>
229
+                                  <el-icon><Close /></el-icon>
220 230
                                   停止实例
221 231
                                 </el-button>
222 232
                                 <el-button 
@@ -289,7 +299,7 @@
289 299
                         清空
290 300
                       </el-button>
291 301
                       <el-button type="text" size="small" @click="sendInput">
292
-                        <el-icon><Promotion /></el-icon>
302
+                         <el-icon><Right /></el-icon>
293 303
                         发送
294 304
                       </el-button>
295 305
                     </div>
@@ -327,17 +337,19 @@ import 'splitpanes/dist/splitpanes.css'
327 337
 import {
328 338
   Refresh,
329 339
   Delete,
330
-  Promotion,
340
+  Right,
331 341
   Folder,
332 342
   Document,
333
-  DataBoard,
334
-  Setting,
335
-  PlayCircle,
336
-  Stop,
337
-  Connection,
338
-  ChatDotRound,
339
-  Loading
340
-} from '@element-plus/icons-vue'
343
+   Setting,
344
+   CaretRight,
345
+   Close,
346
+   Link,
347
+   ChatDotSquare,
348
+   Loading,
349
+   User,
350
+   Box,
351
+   InfoFilled
352
+ } from '@element-plus/icons-vue'
341 353
 import { ElMessage, ElMessageBox } from 'element-plus'
342 354
 import VMarkdownEditor from '@/components/VMarkdownEditor.vue'
343 355
 import VMarkdownPreview from '@/components/VMarkdownPreview.vue'
@@ -357,20 +369,32 @@ import {
357 369
 
358 370
 // 树数据
359 371
 const treeRef = ref()
360
-const treeData = ref([
361
-  {
362
-    id: 'projects',
363
-    label: 'OpenCode项目',
364
-    icon: 'Folder',
365
-    children: [] // 动态加载项目
366
-  }
367
-])
372
+const treeData = ref([])
373
+const treeLoading = ref(true)
368 374
 
369 375
 const treeProps = {
370 376
   label: 'label',
371 377
   children: 'children'
372 378
 }
373 379
 
380
+// 图标映射
381
+const iconMap: Record<string, any> = {
382
+  Folder,
383
+  Document,
384
+  Setting,
385
+  User,
386
+  Box,
387
+  InfoFilled,
388
+   CaretRight,
389
+   Close,
390
+   Link,
391
+   ChatDotSquare,
392
+   Loading,
393
+   Refresh,
394
+   Delete,
395
+   Right
396
+ }
397
+
374 398
 // OpenCode项目管理
375 399
 const projects = ref<ProjectInfo[]>([])
376 400
 const opencodeInstances = ref<Record<string, OpenCodeInstance>>({})
@@ -483,8 +507,9 @@ const handleTreeNodeClick = (data: any) => {
483 507
   console.log('点击节点:', data)
484 508
   
485 509
   if (data.type === 'project') {
486
-    selectedProjectId.value = data.id
487
-    addLog('info', `选择项目: ${data.label} (ID: ${data.id})`)
510
+    const projectId = data.data?.id || data.id.replace('project-', '')
511
+    selectedProjectId.value = projectId
512
+    addLog('info', `选择项目: ${data.label} (ID: ${projectId})`)
488 513
     // 切换到项目管理tab
489 514
     activeTab.value = 'opencode'
490 515
   } else {
@@ -543,29 +568,89 @@ const formatData = () => {
543 568
 // OpenCode项目管理方法
544 569
 const loadProjects = async () => {
545 570
   projectLoading.value = true
571
+  treeLoading.value = true
546 572
   try {
547 573
     projects.value = await getProjects()
574
+    console.log('获取到的项目数据:', projects.value)
548 575
     addLog('info', `加载项目列表成功,共 ${projects.value.length} 个项目`)
549 576
     
550
-    // 更新treeData
551
-    treeData.value[0].children = projects.value.map(project => ({
552
-      id: project.id,
577
+    // 创建项目节点
578
+    const projectNodes = projects.value.map(project => ({
579
+      id: `project-${project.id}`,
553 580
       label: project.name,
554
-      icon: 'Document',
581
+      icon: 'Folder',
555 582
       description: project.description,
556 583
       type: 'project',
557 584
       data: project
558 585
     }))
586
+    console.log('创建的项目节点:', projectNodes)
587
+    
588
+    // 构建完整的树数据(包含OpenCode项目和其他静态分类)
589
+    const fullTreeData = [
590
+      {
591
+        id: 'projects-category',
592
+        label: 'OpenCode项目',
593
+        icon: 'Box',
594
+        count: projects.value.length,
595
+        children: projectNodes
596
+      },
597
+      {
598
+        id: 'database-category',
599
+        label: '数据库',
600
+        icon: 'Folder',
601
+        children: [
602
+          { id: 'db-1', label: '用户表', icon: 'Document', count: 100 },
603
+          { id: 'db-2', label: '订单表', icon: 'Document', count: 500 },
604
+          { id: 'db-3', label: '商品表', icon: 'Document', count: 200 },
605
+          { id: 'db-4', label: '库存表', icon: 'Document', count: 150 }
606
+        ]
607
+      },
608
+      {
609
+        id: 'config-category',
610
+        label: '配置',
611
+        icon: 'Setting',
612
+        children: [
613
+          { id: 'cfg-1', label: '系统配置', icon: 'Document' },
614
+          { id: 'cfg-2', label: '用户配置', icon: 'Document' },
615
+          { id: 'cfg-3', label: '权限配置', icon: 'Document' }
616
+        ]
617
+      },
618
+      {
619
+        id: 'logs-category',
620
+        label: '日志',
621
+        icon: 'Document',
622
+        children: [
623
+          { id: 'log-1', label: '运行日志', icon: 'Document', count: 1000 },
624
+          { id: 'log-2', label: '错误日志', icon: 'Document', count: 50 },
625
+          { id: 'log-3', label: '操作日志', icon: 'Document', count: 300 }
626
+        ]
627
+      },
628
+      {
629
+        id: 'agent-category',
630
+        label: 'Agent',
631
+        icon: 'User',
632
+        children: [
633
+          { id: 'agent-1', label: '配置管理', icon: 'Document' },
634
+          { id: 'agent-2', label: '运行状态', icon: 'Document' },
635
+          { id: 'agent-3', label: '历史记录', icon: 'Document', count: 120 }
636
+        ]
637
+      }
638
+    ]
639
+    
640
+    treeData.value = fullTreeData
641
+    console.log('更新后的完整treeData:', treeData.value)
559 642
     
560 643
     // 加载每个项目的实例状态
561 644
     for (const project of projects.value) {
562 645
       await loadInstanceStatus(project.id)
563 646
     }
564
-  } catch (error) {
565
-    addLog('error', `加载项目列表失败: ${error}`)
566
-    ElMessage.error('加载项目列表失败')
647
+  } catch (error: any) {
648
+    console.error('加载项目列表详细错误:', error)
649
+    addLog('error', `加载项目列表失败: ${error.message || error}`)
650
+    ElMessage.error(`加载项目列表失败: ${error.message || '请检查后端服务'}`)
567 651
   } finally {
568 652
     projectLoading.value = false
653
+    treeLoading.value = false
569 654
   }
570 655
 }
571 656
 

+ 1
- 1
web/vite.config.ts ファイルの表示

@@ -14,7 +14,7 @@ export default defineConfig({
14 14
     port: 3000,
15 15
     proxy: {
16 16
       '/api': {
17
-        target: 'http://localhost:8787',
17
+        target: 'http://localhost:8020',
18 18
         changeOrigin: true
19 19
       }
20 20
     }

読み込み中…
キャンセル
保存