ai-worktool 1.0.69 → 1.0.71
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -1
- package/dist/agents/prompt.js +30 -2
- package/dist/agents/toolCall.js +13 -8
- package/dist/report.js +644 -0
- package/dist/testAgent.js +11 -5
- package/dist/tools/file.js +1 -1
- package/dist/tools/jest.js +45 -14
- package/dist/tools/project.js +18 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
## 1.0.
|
|
1
|
+
## 1.0.71 (2025-08-19)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Bug Fixes
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* **app:** 修正 settings 路由的挂载路径 ([2a2890b](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/2a2890be96b824f885dbd19e58f82ec3f1933a4b))
|
|
11
11
|
* **coverage:** 修复代码覆盖率实时更新的问题 ([14bd1f4](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/14bd1f4eb4cc774547b55672422ace574fdec192))
|
|
12
12
|
* ESM 文件导入 ([1e3b68b](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/1e3b68bf951c13fdfb0ef4fbe4e82d97f9a6c688))
|
|
13
|
+
* **extension:** 优化代码结构和功能 ([51e1450](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/51e145090123c64a0384275005ede314610b3222))
|
|
14
|
+
* **extension:** 添加取消登录功能并优化扩展程序的退出流程 ([8b4ddcc](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/8b4ddcca29bae106ea146128dbf38f5b9821b396))
|
|
13
15
|
* **file:** 修复插入操作行号验证逻辑 ([4202d7c](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/4202d7c6524d21f200a8dced1a5e4bcba63787dd))
|
|
14
16
|
* JEST无法输出colors ([3a1bd24](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/3a1bd24ba0b9b6d78dc9cdb5958f3262d08d9ded))
|
|
15
17
|
* **login:** 更新客户端 ID ([bfeac59](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/bfeac590f8bae37c1a123c8356dd600ed9add4ad))
|
|
@@ -103,6 +105,7 @@
|
|
|
103
105
|
* **commands:** 在打开订单和使用页面时添加 token 参数 ([f52c7a3](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/f52c7a349988eeec08370b4fb673bc479256ab72))
|
|
104
106
|
* **commands:** 在打开订单和使用页面时添加 token 参数 ([c20e809](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/c20e809cec5a0c2c1ca886c5414730ce4893fabd))
|
|
105
107
|
* **configuration:** 增加包管理器、测试框架和编程语言选择配置项 ([ec3cceb](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/ec3cceba695485010c5c710894ea2e49f57737f8))
|
|
108
|
+
* **coverage:** 提升代码覆盖率测量的准确性和可读性 ([9111a6c](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/9111a6c42f204d944463113985ee53dcc7502326))
|
|
106
109
|
* **dify-plugin:** 新增重命名文件或目录的功能 ([9bb6961](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/9bb69613a28aba6859f72b14b2aa1a586ac9ca70))
|
|
107
110
|
* **extension:** 重构 Webview 并添加覆盖率报告功能 ([fd8a62b](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/fd8a62b79aff4c03af8b56779d607f01d65c175a))
|
|
108
111
|
* **jianguoke:** 优化工具调用信息格式化及消息 ID 生成 ([1b240e7](https://codeup.aliyun.com/666cf9ed29ecbe23053513a3/JianGuoKe/ai-worktools/commits/1b240e7fbcabb2112789c116b7f483308c184fce))
|
package/dist/agents/prompt.js
CHANGED
|
@@ -32,12 +32,40 @@ ${await (0, tools_1.readFile)(failedTest.testFilePath, 'utf8', true)}
|
|
|
32
32
|
\`\`\``;
|
|
33
33
|
}
|
|
34
34
|
async function addUncoveredLinePrompt(srcPath, report) {
|
|
35
|
-
|
|
35
|
+
const pctType = report.coverage.functions.pct < 100
|
|
36
|
+
? '函数'
|
|
37
|
+
: report.coverage.lines.pct < 100
|
|
38
|
+
? '行'
|
|
39
|
+
: report.coverage.branches.pct < 100
|
|
40
|
+
? '分支'
|
|
41
|
+
: '语句';
|
|
42
|
+
const pct = report.coverage.functions.pct < 100
|
|
43
|
+
? report.coverage.functions.pct
|
|
44
|
+
: report.coverage.lines.pct < 100
|
|
45
|
+
? report.coverage.lines.pct
|
|
46
|
+
: report.coverage.branches.pct < 100
|
|
47
|
+
? report.coverage.branches.pct
|
|
48
|
+
: report.coverage.statements.pct;
|
|
49
|
+
const unitName = report.coverage.functions.pct < 100 ? '函数' : '行';
|
|
50
|
+
const lines = (report.uncoveredLines.functions ||
|
|
51
|
+
report.uncoveredLines.lines ||
|
|
52
|
+
report.uncoveredLines.branches ||
|
|
53
|
+
report.uncoveredLines.statements).join(',');
|
|
54
|
+
return (`源文件"${path_1.default.resolve(srcPath)}"的单元测试${pctType}覆盖率是${pct}%, 还有行号是"${lines}"的${unitName}未完全覆盖单测。
|
|
36
55
|
|
|
37
56
|
源文件内容如下:
|
|
38
57
|
\`\`\`
|
|
39
58
|
${await (0, tools_1.readFile)(srcPath, 'utf8', true)}
|
|
40
|
-
|
|
59
|
+
\`\`\`` +
|
|
60
|
+
((report.coverage.functions.total > 0 && report.coverage.functions.pct > 0) ||
|
|
61
|
+
(report.coverage.lines.total > 0 && report.coverage.lines.pct > 0) ||
|
|
62
|
+
(report.coverage.branches.total > 0 && report.coverage.branches.pct > 0) ||
|
|
63
|
+
(report.coverage.statements.total > 0 && report.coverage.statements.pct > 0)
|
|
64
|
+
? `
|
|
65
|
+
|
|
66
|
+
查找已有测试文件增加单测代码。
|
|
67
|
+
`
|
|
68
|
+
: ''));
|
|
41
69
|
}
|
|
42
70
|
async function addUncoveredFilePrompt(total, files) {
|
|
43
71
|
const testFileNames = files.map((it) => it.filePath);
|
package/dist/agents/toolCall.js
CHANGED
|
@@ -103,7 +103,7 @@ function inferCodeTypeFromExtension(filePath) {
|
|
|
103
103
|
* @param argsObj 包含参数的对象
|
|
104
104
|
* @returns 函数执行结果
|
|
105
105
|
*/
|
|
106
|
-
async function callWithObject(name, argsObj,
|
|
106
|
+
async function callWithObject(name, argsObj, projectDir) {
|
|
107
107
|
const fn = tools[name];
|
|
108
108
|
if (!fn) {
|
|
109
109
|
throw new Error(`Tool ${name} not found`);
|
|
@@ -113,8 +113,10 @@ async function callWithObject(name, argsObj, cwd) {
|
|
|
113
113
|
// 从对象中提取参数值
|
|
114
114
|
const args = paramNames.map((name) => {
|
|
115
115
|
// 转成决定路径
|
|
116
|
-
if (name === 'filePath'
|
|
117
|
-
|
|
116
|
+
if ((name === 'filePath' || name === 'oldPath' || name === 'newPath' || name === 'directory') &&
|
|
117
|
+
projectDir &&
|
|
118
|
+
!path_1.default.isAbsolute(argsObj[name])) {
|
|
119
|
+
return path_1.default.resolve(projectDir, argsObj[name]);
|
|
118
120
|
}
|
|
119
121
|
return argsObj[name];
|
|
120
122
|
});
|
|
@@ -172,7 +174,7 @@ toolCall, root, result) {
|
|
|
172
174
|
root = root || '/';
|
|
173
175
|
const params = toolCall.params || {};
|
|
174
176
|
if (toolCall.function_name === 'readFile') {
|
|
175
|
-
const outputResult = result ? (result?.success ? ` ✅` : result?.message
|
|
177
|
+
const outputResult = result ? (result?.success ? ` ✅` : ` ❌${result?.message}`) : '';
|
|
176
178
|
const filePath = params[0] || params.filePath;
|
|
177
179
|
return `读取文件:${formatDisplayText(filePath ? (path_1.default.isAbsolute(filePath) ? path_1.default.relative(root, filePath) : filePath) : '')}${outputResult}`;
|
|
178
180
|
}
|
|
@@ -181,21 +183,24 @@ toolCall, root, result) {
|
|
|
181
183
|
toolCall.function_name === 'modifyLine' ||
|
|
182
184
|
toolCall.function_name === 'deleteLine' ||
|
|
183
185
|
toolCall.function_name === 'batchModifyLines') {
|
|
184
|
-
const outputResult = result ? (result?.success ? ` ✅` : result?.message
|
|
186
|
+
const outputResult = result ? (result?.success ? ` ✅` : ` ❌${result?.message}`) : '';
|
|
185
187
|
const filePath = params[0] || params.filePath;
|
|
186
188
|
return `修改文件:${formatDisplayText(filePath ? (path_1.default.isAbsolute(filePath) ? path_1.default.relative(root, filePath) : filePath) : '')}${outputResult}`;
|
|
187
189
|
}
|
|
188
190
|
if (toolCall.function_name === 'searchFilesByExtension') {
|
|
189
191
|
const outputResult = result
|
|
190
192
|
? result?.success
|
|
191
|
-
? ` => ${formatDisplayText(result.data
|
|
192
|
-
|
|
193
|
+
? ` => ${formatDisplayText(result.data
|
|
194
|
+
.replaceAll(root + path_1.default.sep, '')
|
|
195
|
+
.replaceAll('- ', '')
|
|
196
|
+
.replaceAll('\n', ' '))} ✅`
|
|
197
|
+
: ` ❌${result?.message}`
|
|
193
198
|
: '';
|
|
194
199
|
const searchPath = params[0] || params.directory;
|
|
195
200
|
const exts = params[1] || params.extensions || '';
|
|
196
201
|
return `检索文件:${searchPath ? (path_1.default.isAbsolute(searchPath) ? path_1.default.relative(root, searchPath) : searchPath) : ''}${exts ? '/*' : ''}${exts}${outputResult}`;
|
|
197
202
|
}
|
|
198
|
-
const outputResult = result ? (result?.success ? ` ✅` : result?.message
|
|
203
|
+
const outputResult = result ? (result?.success ? ` ✅` : ` ❌${result?.message}`) : '';
|
|
199
204
|
return toolCall.function_name + outputResult;
|
|
200
205
|
}
|
|
201
206
|
//# sourceMappingURL=toolCall.js.map
|
package/dist/report.js
ADDED
|
@@ -0,0 +1,644 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.DEFAULT_PORT = void 0;
|
|
40
|
+
exports.startServer = startServer;
|
|
41
|
+
exports.stopServer = stopServer;
|
|
42
|
+
exports.addOrUpdateProject = addOrUpdateProject;
|
|
43
|
+
exports.removeProject = removeProject;
|
|
44
|
+
exports.getProjects = getProjects;
|
|
45
|
+
exports.openLcovReport = openLcovReport;
|
|
46
|
+
const http = __importStar(require("http"));
|
|
47
|
+
const fs = __importStar(require("fs/promises"));
|
|
48
|
+
const fsSync = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
const url = __importStar(require("url"));
|
|
51
|
+
const child_process_1 = require("child_process");
|
|
52
|
+
const util_1 = __importDefault(require("util"));
|
|
53
|
+
// 转换exec为Promise形式,便于async/await使用
|
|
54
|
+
const execAsync = util_1.default.promisify(child_process_1.exec);
|
|
55
|
+
// 配置
|
|
56
|
+
exports.DEFAULT_PORT = 3000;
|
|
57
|
+
const LCOV_REPORT_DIR = 'lcov-report';
|
|
58
|
+
// 服务器状态变量
|
|
59
|
+
let server = null;
|
|
60
|
+
let serverPort;
|
|
61
|
+
const watchers = new Map(); // 每个项目一个监控器,key为项目名
|
|
62
|
+
let lastChangeTime = new Map(); // 每个项目的最后修改时间,key为项目名
|
|
63
|
+
const clients = [];
|
|
64
|
+
let projectsConfig = {}; // 项目配置映射
|
|
65
|
+
// MIME类型映射表
|
|
66
|
+
const mimeTypes = {
|
|
67
|
+
'.html': 'text/html',
|
|
68
|
+
'.css': 'text/css',
|
|
69
|
+
'.js': 'text/javascript',
|
|
70
|
+
'.json': 'application/json',
|
|
71
|
+
'.png': 'image/png',
|
|
72
|
+
'.jpg': 'image/jpeg',
|
|
73
|
+
'.gif': 'image/gif',
|
|
74
|
+
'.svg': 'image/svg+xml',
|
|
75
|
+
'.ico': 'image/x-icon'
|
|
76
|
+
};
|
|
77
|
+
// 获取MIME类型
|
|
78
|
+
function getMimeType(filePath) {
|
|
79
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
80
|
+
return mimeTypes[ext] || 'application/octet-stream';
|
|
81
|
+
}
|
|
82
|
+
// 解析URL,获取项目名和相对路径
|
|
83
|
+
function parseUrlPath(pathname) {
|
|
84
|
+
// 移除开头的斜杠并分割路径
|
|
85
|
+
const parts = pathname.replace(/^\/+/, '').split('/');
|
|
86
|
+
// 第一个部分是项目名
|
|
87
|
+
const project = parts[0] || '';
|
|
88
|
+
// 剩余部分组成相对路径
|
|
89
|
+
const relativePath = parts.slice(1).join('/') || '';
|
|
90
|
+
return { project, relativePath };
|
|
91
|
+
}
|
|
92
|
+
// 查找项目的lcov-report目录
|
|
93
|
+
async function findProjectReportDir(projectName) {
|
|
94
|
+
// 检查项目是否在配置中
|
|
95
|
+
if (!projectsConfig[projectName])
|
|
96
|
+
return null;
|
|
97
|
+
try {
|
|
98
|
+
const projectDir = projectsConfig[projectName];
|
|
99
|
+
const reportPath = path.join(projectDir, LCOV_REPORT_DIR);
|
|
100
|
+
const stats = await fs.stat(reportPath);
|
|
101
|
+
return stats.isDirectory() ? reportPath : null;
|
|
102
|
+
}
|
|
103
|
+
catch (err) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// 检查项目路径是否有效
|
|
108
|
+
async function validateProjectPath(projectPath) {
|
|
109
|
+
try {
|
|
110
|
+
const stats = await fs.stat(projectPath);
|
|
111
|
+
if (!stats.isDirectory()) {
|
|
112
|
+
return { valid: false, error: '路径不是有效的目录' };
|
|
113
|
+
}
|
|
114
|
+
return { valid: true };
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
return { valid: false, error: '路径不存在或无法访问' };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// 验证多个项目配置是否有效
|
|
121
|
+
async function validateProjectsConfig(config) {
|
|
122
|
+
const errors = {};
|
|
123
|
+
for (const [projectName, projectPath] of Object.entries(config)) {
|
|
124
|
+
const { valid, error } = await validateProjectPath(projectPath);
|
|
125
|
+
if (!valid && error) {
|
|
126
|
+
errors[projectName] = error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return { valid: Object.keys(errors).length === 0, errors };
|
|
130
|
+
}
|
|
131
|
+
// 设置项目的文件监控
|
|
132
|
+
function setupProjectWatcher(projectName) {
|
|
133
|
+
const projectPath = projectsConfig[projectName];
|
|
134
|
+
if (!projectPath)
|
|
135
|
+
return;
|
|
136
|
+
// 如果已有监控器,关闭它
|
|
137
|
+
if (watchers.has(projectName)) {
|
|
138
|
+
watchers.get(projectName).close();
|
|
139
|
+
}
|
|
140
|
+
// 监控整个项目目录
|
|
141
|
+
const watcher = fsSync.watch(projectPath, { recursive: true }, (eventType) => {
|
|
142
|
+
if (eventType === 'change' || eventType === 'rename') {
|
|
143
|
+
const time = Date.now();
|
|
144
|
+
lastChangeTime.set(projectName, time);
|
|
145
|
+
console.log(`项目 ${projectName} 的文件变化 detected at ${new Date(time).toLocaleTimeString()}`);
|
|
146
|
+
// 通知该项目的所有客户端
|
|
147
|
+
const projectClients = clients.filter((c) => c.project === projectName);
|
|
148
|
+
projectClients.forEach(({ res }) => {
|
|
149
|
+
const index = clients.findIndex((c) => c.res === res);
|
|
150
|
+
if (index !== -1) {
|
|
151
|
+
clients.splice(index, 1);
|
|
152
|
+
sendChangeNotification(res, projectName);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
watchers.set(projectName, watcher);
|
|
158
|
+
console.log(`正在监控项目变化: ${projectName} -> ${projectPath}`);
|
|
159
|
+
return watcher;
|
|
160
|
+
}
|
|
161
|
+
// 发送变化通知
|
|
162
|
+
function sendChangeNotification(response, projectName) {
|
|
163
|
+
response.writeHead(200, {
|
|
164
|
+
'Content-Type': 'text/event-stream',
|
|
165
|
+
'Cache-Control': 'no-cache',
|
|
166
|
+
Connection: 'keep-alive'
|
|
167
|
+
});
|
|
168
|
+
response.write(`data: ${JSON.stringify({
|
|
169
|
+
changed: true,
|
|
170
|
+
time: Date.now(),
|
|
171
|
+
project: projectName
|
|
172
|
+
})}\n\n`);
|
|
173
|
+
response.end();
|
|
174
|
+
}
|
|
175
|
+
// 注入静默刷新脚本到HTML内容
|
|
176
|
+
function injectSilentRefreshScript(htmlContent, projectName) {
|
|
177
|
+
// 项目名为空表示是根页面,不需要处理
|
|
178
|
+
if (!projectName) {
|
|
179
|
+
return htmlContent;
|
|
180
|
+
}
|
|
181
|
+
// 注入静默刷新脚本
|
|
182
|
+
const script = `
|
|
183
|
+
<script>
|
|
184
|
+
// 当前项目名称
|
|
185
|
+
const currentProject = '${projectName}';
|
|
186
|
+
|
|
187
|
+
// 保存当前滚动位置
|
|
188
|
+
function saveScrollPosition() {
|
|
189
|
+
return {
|
|
190
|
+
x: window.scrollX,
|
|
191
|
+
y: window.scrollY
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 恢复滚动位置
|
|
196
|
+
function restoreScrollPosition(pos) {
|
|
197
|
+
window.scrollTo(pos.x, pos.y);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// 重新加载CSS
|
|
201
|
+
function reloadStylesheets() {
|
|
202
|
+
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
203
|
+
links.forEach(link => {
|
|
204
|
+
const href = link.getAttribute('href');
|
|
205
|
+
if (href) {
|
|
206
|
+
// 添加时间戳防止缓存
|
|
207
|
+
const newHref = href + (href.includes('?') ? '&' : '?') + 't=' + Date.now();
|
|
208
|
+
link.setAttribute('href', newHref);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 静默更新页面内容
|
|
214
|
+
async function updatePageContent() {
|
|
215
|
+
try {
|
|
216
|
+
// 保存当前滚动位置
|
|
217
|
+
const scrollPos = saveScrollPosition();
|
|
218
|
+
|
|
219
|
+
// 获取当前页面的HTML
|
|
220
|
+
const response = await fetch(window.location.pathname);
|
|
221
|
+
if (!response.ok) throw new Error('获取页面失败');
|
|
222
|
+
|
|
223
|
+
const newHtml = await response.text();
|
|
224
|
+
const parser = new DOMParser();
|
|
225
|
+
const newDocument = parser.parseFromString(newHtml, 'text/html');
|
|
226
|
+
|
|
227
|
+
// 更新body内容
|
|
228
|
+
document.body.innerHTML = newDocument.body.innerHTML;
|
|
229
|
+
|
|
230
|
+
// 重新加载样式表
|
|
231
|
+
reloadStylesheets();
|
|
232
|
+
|
|
233
|
+
// 恢复滚动位置
|
|
234
|
+
restoreScrollPosition(scrollPos);
|
|
235
|
+
|
|
236
|
+
console.log(\`项目 \${currentProject} 页面已静默更新\`);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('更新页面时出错:', error);
|
|
239
|
+
// 出错时回退到整页刷新
|
|
240
|
+
window.location.reload();
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// 长轮询检查变化
|
|
245
|
+
function checkForChanges() {
|
|
246
|
+
fetch(\`/\${currentProject}/__watch\`)
|
|
247
|
+
.then(response => {
|
|
248
|
+
if (response.ok) {
|
|
249
|
+
return response.text();
|
|
250
|
+
}
|
|
251
|
+
throw new Error('轮询响应错误');
|
|
252
|
+
})
|
|
253
|
+
.then(data => {
|
|
254
|
+
// 解析服务器发送的事件
|
|
255
|
+
if (data.startsWith('data:')) {
|
|
256
|
+
const jsonData = JSON.parse(data.substring(5).trim());
|
|
257
|
+
|
|
258
|
+
// 项目页面的内容变化
|
|
259
|
+
if (jsonData.changed && jsonData.project === currentProject) {
|
|
260
|
+
console.log(\`检测到项目 \${currentProject} 的文件变化,正在更新页面...\`);
|
|
261
|
+
updatePageContent();
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
// 继续轮询
|
|
265
|
+
checkForChanges();
|
|
266
|
+
})
|
|
267
|
+
.catch(error => {
|
|
268
|
+
console.error('轮询错误:', error);
|
|
269
|
+
// 出错后延迟重试
|
|
270
|
+
setTimeout(checkForChanges, 1000);
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 页面加载后开始轮询
|
|
275
|
+
window.addEventListener('load', checkForChanges);
|
|
276
|
+
</script>
|
|
277
|
+
`;
|
|
278
|
+
// 将脚本注入到</body>标签前
|
|
279
|
+
return htmlContent.replace('</body>', `${script}</body>`);
|
|
280
|
+
}
|
|
281
|
+
// 处理请求
|
|
282
|
+
async function handleRequest(request, response) {
|
|
283
|
+
try {
|
|
284
|
+
const parsedUrl = url.parse(request.url || '', true);
|
|
285
|
+
const pathname = parsedUrl.pathname || '/';
|
|
286
|
+
// 解析URL获取项目名和相对路径
|
|
287
|
+
const { project: projectName, relativePath } = parseUrlPath(pathname);
|
|
288
|
+
// 处理根路径请求,显示项目列表
|
|
289
|
+
if (!projectName) {
|
|
290
|
+
return handleRootRequest(response);
|
|
291
|
+
}
|
|
292
|
+
// 检查项目是否存在于配置中
|
|
293
|
+
if (!projectsConfig[projectName]) {
|
|
294
|
+
response.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
295
|
+
response.end(`项目 "${projectName}" 不存在于配置中`);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// 处理项目的监控请求
|
|
299
|
+
if (relativePath === '__watch') {
|
|
300
|
+
const clientLastChange = Number(parsedUrl.query.lastChange) || 0;
|
|
301
|
+
const projectLastChange = lastChangeTime.get(projectName) || 0;
|
|
302
|
+
if (projectLastChange > clientLastChange) {
|
|
303
|
+
sendChangeNotification(response, projectName);
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
// 保持连接等待
|
|
307
|
+
clients.push({ res: response, project: projectName });
|
|
308
|
+
// 10秒后超时
|
|
309
|
+
setTimeout(() => {
|
|
310
|
+
const index = clients.findIndex((c) => c.res === response);
|
|
311
|
+
if (index !== -1) {
|
|
312
|
+
clients.splice(index, 1);
|
|
313
|
+
response.writeHead(204); // 无内容
|
|
314
|
+
response.end();
|
|
315
|
+
}
|
|
316
|
+
}, 10000);
|
|
317
|
+
}
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
// 查找项目的报告目录
|
|
321
|
+
const reportDir = await findProjectReportDir(projectName);
|
|
322
|
+
if (!reportDir) {
|
|
323
|
+
response.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
324
|
+
response.end(`项目 "${projectName}" 未找到或不包含 ${LCOV_REPORT_DIR} 目录`);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
// 设置项目的文件监控(如果尚未设置)
|
|
328
|
+
if (!watchers.has(projectName)) {
|
|
329
|
+
setupProjectWatcher(projectName);
|
|
330
|
+
}
|
|
331
|
+
// 处理静态文件请求
|
|
332
|
+
let filePath = path.join(reportDir, relativePath);
|
|
333
|
+
// 防止路径遍历攻击
|
|
334
|
+
if (!filePath.startsWith(reportDir)) {
|
|
335
|
+
response.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
336
|
+
response.end('禁止访问');
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
// 检查文件状态
|
|
340
|
+
let stats;
|
|
341
|
+
try {
|
|
342
|
+
stats = await fs.stat(filePath);
|
|
343
|
+
}
|
|
344
|
+
catch (err) {
|
|
345
|
+
response.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
346
|
+
response.end(`文件未找到: ${relativePath}`);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
// 如果是目录,尝试加载index.html
|
|
350
|
+
if (stats.isDirectory()) {
|
|
351
|
+
filePath = path.join(filePath, 'index.html');
|
|
352
|
+
try {
|
|
353
|
+
await fs.stat(filePath);
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
response.writeHead(404, { 'Content-Type': 'text/plain' });
|
|
357
|
+
response.end(`目录下未找到index.html: ${relativePath}`);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
// 读取文件
|
|
362
|
+
const data = await fs.readFile(filePath);
|
|
363
|
+
const mimeType = getMimeType(filePath);
|
|
364
|
+
response.writeHead(200, { 'Content-Type': mimeType });
|
|
365
|
+
// 如果是HTML文件,注入静默刷新脚本
|
|
366
|
+
if (mimeType === 'text/html') {
|
|
367
|
+
const htmlContent = data.toString();
|
|
368
|
+
const modifiedContent = injectSilentRefreshScript(htmlContent, projectName);
|
|
369
|
+
response.end(modifiedContent);
|
|
370
|
+
}
|
|
371
|
+
else {
|
|
372
|
+
response.end(data);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
catch (error) {
|
|
376
|
+
console.error('处理请求时出错:', error);
|
|
377
|
+
response.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
378
|
+
response.end('服务器内部错误');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// 处理根路径请求,显示项目列表
|
|
382
|
+
async function handleRootRequest(response) {
|
|
383
|
+
// 生成项目列表HTML
|
|
384
|
+
const html = `
|
|
385
|
+
<!DOCTYPE html>
|
|
386
|
+
<html>
|
|
387
|
+
<head>
|
|
388
|
+
<title>覆盖率报告项目列表</title>
|
|
389
|
+
<style>
|
|
390
|
+
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
|
391
|
+
h1 { color: #333; }
|
|
392
|
+
.project-list { list-style: none; padding: 0; }
|
|
393
|
+
.project-item { margin: 10px 0; padding: 10px; border: 1px solid #eee; border-radius: 4px; }
|
|
394
|
+
.project-name { font-weight: bold; }
|
|
395
|
+
.project-path { color: #666; font-size: 0.9em; margin-top: 5px; }
|
|
396
|
+
a { color: #007bff; text-decoration: none; }
|
|
397
|
+
a:hover { text-decoration: underline; }
|
|
398
|
+
.status { margin-left: 10px; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; }
|
|
399
|
+
.status-ok { background-color: #d4edda; color: #155724; }
|
|
400
|
+
.status-error { background-color: #f8d7da; color: #721c24; }
|
|
401
|
+
</style>
|
|
402
|
+
</head>
|
|
403
|
+
<body>
|
|
404
|
+
<h1>覆盖率报告项目列表</h1>
|
|
405
|
+
<ul class="project-list">
|
|
406
|
+
${Object.entries(projectsConfig)
|
|
407
|
+
.map(([projectName, projectPath]) => `
|
|
408
|
+
<li class="project-item">
|
|
409
|
+
<div>
|
|
410
|
+
<a href="/${projectName}" class="project-name">${projectName}</a>
|
|
411
|
+
${(async () => {
|
|
412
|
+
const reportDir = await findProjectReportDir(projectName);
|
|
413
|
+
return reportDir
|
|
414
|
+
? '<span class="status status-ok">正常</span>'
|
|
415
|
+
: '<span class="status status-error">缺少报告目录</span>';
|
|
416
|
+
})()
|
|
417
|
+
.then((status) => status)
|
|
418
|
+
.catch(() => '')}
|
|
419
|
+
</div>
|
|
420
|
+
<div class="project-path">${projectPath}</div>
|
|
421
|
+
</li>
|
|
422
|
+
`)
|
|
423
|
+
.join('')}
|
|
424
|
+
</ul>
|
|
425
|
+
${Object.keys(projectsConfig).length === 0 ? '<p>当前没有配置任何项目。</p>' : ''}
|
|
426
|
+
</body>
|
|
427
|
+
</html>
|
|
428
|
+
`;
|
|
429
|
+
response.writeHead(200, { 'Content-Type': 'text/html' });
|
|
430
|
+
response.end(html);
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* 获取可用端口
|
|
434
|
+
* @returns 可用端口号
|
|
435
|
+
*/
|
|
436
|
+
function getAvailablePort() {
|
|
437
|
+
return new Promise((resolve, reject) => {
|
|
438
|
+
const tempServer = http.createServer();
|
|
439
|
+
tempServer.listen(0, 'localhost', () => {
|
|
440
|
+
const address = tempServer.address();
|
|
441
|
+
if (typeof address === 'object' && address && address.port) {
|
|
442
|
+
const port = address.port;
|
|
443
|
+
tempServer.close(() => resolve(port));
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
tempServer.close();
|
|
447
|
+
reject(new Error('无法获取可用端口'));
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* 启动LCOV报告服务器
|
|
454
|
+
* @param projects 项目配置映射:{ 项目名: 项目实际路径 }
|
|
455
|
+
* @param port 服务器端口,默认为3000
|
|
456
|
+
* @returns 服务器实例和启动信息
|
|
457
|
+
*/
|
|
458
|
+
async function startServer(projects = {}, port) {
|
|
459
|
+
// 停止已运行的服务器
|
|
460
|
+
if (server) {
|
|
461
|
+
await stopServer();
|
|
462
|
+
}
|
|
463
|
+
// 验证项目配置
|
|
464
|
+
const { valid, errors } = await validateProjectsConfig(projects);
|
|
465
|
+
if (!valid) {
|
|
466
|
+
const errorMsg = `项目配置无效:\n${Object.entries(errors)
|
|
467
|
+
.map(([name, err]) => `- ${name}: ${err}`)
|
|
468
|
+
.join('\n')}`;
|
|
469
|
+
console.error(errorMsg);
|
|
470
|
+
throw new Error(errorMsg);
|
|
471
|
+
}
|
|
472
|
+
// 保存项目配置
|
|
473
|
+
projectsConfig = { ...projects };
|
|
474
|
+
// 为所有项目设置监控
|
|
475
|
+
// Object.keys(projectsConfig).forEach((projectName) => {
|
|
476
|
+
// setupProjectWatcher(projectName);
|
|
477
|
+
// });
|
|
478
|
+
// 如果没有配置项目,给出警告
|
|
479
|
+
if (Object.keys(projectsConfig).length === 0) {
|
|
480
|
+
console.warn('警告: 未配置任何项目,服务器将无法提供有效内容');
|
|
481
|
+
}
|
|
482
|
+
serverPort = port || (await getAvailablePort());
|
|
483
|
+
// 创建服务器
|
|
484
|
+
server = http.createServer((req, res) => {
|
|
485
|
+
handleRequest(req, res).catch((err) => {
|
|
486
|
+
console.error('未捕获的请求错误:', err);
|
|
487
|
+
if (!res.finished) {
|
|
488
|
+
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
|
489
|
+
res.end('服务器错误');
|
|
490
|
+
}
|
|
491
|
+
});
|
|
492
|
+
});
|
|
493
|
+
// 启动服务器
|
|
494
|
+
return new Promise((resolve, reject) => {
|
|
495
|
+
server
|
|
496
|
+
.listen(serverPort, () => {
|
|
497
|
+
console.log(`服务器运行在 http://localhost:${serverPort}`);
|
|
498
|
+
console.log('配置的项目:');
|
|
499
|
+
Object.entries(projectsConfig).forEach(([name, path]) => {
|
|
500
|
+
console.log(`- ${name}: ${path}`);
|
|
501
|
+
});
|
|
502
|
+
console.log(`访问 http://localhost:${serverPort} 查看项目列表`);
|
|
503
|
+
resolve({ server: server, port: serverPort, projects: { ...projectsConfig } });
|
|
504
|
+
})
|
|
505
|
+
.on('error', (err) => {
|
|
506
|
+
reject(err);
|
|
507
|
+
});
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* 停止LCOV报告服务器
|
|
512
|
+
* @returns 停止成功的Promise
|
|
513
|
+
*/
|
|
514
|
+
async function stopServer() {
|
|
515
|
+
return new Promise((resolve) => {
|
|
516
|
+
// 关闭所有文件监控
|
|
517
|
+
watchers.forEach((watcher) => watcher.close());
|
|
518
|
+
watchers.clear();
|
|
519
|
+
lastChangeTime.clear();
|
|
520
|
+
// 关闭客户端连接
|
|
521
|
+
clients.forEach(({ res }) => {
|
|
522
|
+
if (!res.finished) {
|
|
523
|
+
res.end();
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
clients.length = 0;
|
|
527
|
+
// 清空项目配置
|
|
528
|
+
projectsConfig = {};
|
|
529
|
+
// 关闭服务器
|
|
530
|
+
if (server) {
|
|
531
|
+
server.close(() => {
|
|
532
|
+
console.log('服务器已关闭');
|
|
533
|
+
server = null;
|
|
534
|
+
resolve();
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
else {
|
|
538
|
+
resolve();
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* 添加或更新项目
|
|
544
|
+
* @param projectName 项目名称
|
|
545
|
+
* @param projectPath 项目实际路径
|
|
546
|
+
* @returns 操作结果
|
|
547
|
+
*/
|
|
548
|
+
async function addOrUpdateProject(projectName, projectPath) {
|
|
549
|
+
if (!server) {
|
|
550
|
+
return {
|
|
551
|
+
success: false,
|
|
552
|
+
error: '服务器未启动',
|
|
553
|
+
projects: { ...projectsConfig }
|
|
554
|
+
};
|
|
555
|
+
}
|
|
556
|
+
// 验证项目路径
|
|
557
|
+
const { valid, error } = await validateProjectPath(projectPath);
|
|
558
|
+
if (!valid && error) {
|
|
559
|
+
return {
|
|
560
|
+
success: false,
|
|
561
|
+
error,
|
|
562
|
+
projects: { ...projectsConfig }
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
// 添加或更新项目配置
|
|
566
|
+
projectsConfig[projectName] = projectPath;
|
|
567
|
+
// 设置或更新监控
|
|
568
|
+
// setupProjectWatcher(projectName);
|
|
569
|
+
console.log(`项目已${projectsConfig.hasOwnProperty(projectName) ? '更新' : '添加'}: ${projectName} -> ${projectPath}`);
|
|
570
|
+
return {
|
|
571
|
+
success: true,
|
|
572
|
+
projects: { ...projectsConfig }
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* 删除项目
|
|
577
|
+
* @param projectName 要删除的项目名称
|
|
578
|
+
* @returns 操作结果
|
|
579
|
+
*/
|
|
580
|
+
async function removeProject(projectName) {
|
|
581
|
+
if (!server) {
|
|
582
|
+
return {
|
|
583
|
+
success: false,
|
|
584
|
+
error: '服务器未启动',
|
|
585
|
+
projects: { ...projectsConfig }
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
// 检查项目是否存在
|
|
589
|
+
if (!projectsConfig[projectName]) {
|
|
590
|
+
return {
|
|
591
|
+
success: false,
|
|
592
|
+
error: `项目 "${projectName}" 不存在`,
|
|
593
|
+
projects: { ...projectsConfig }
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
// 关闭并移除监控
|
|
597
|
+
if (watchers.has(projectName)) {
|
|
598
|
+
watchers.get(projectName).close();
|
|
599
|
+
watchers.delete(projectName);
|
|
600
|
+
lastChangeTime.delete(projectName);
|
|
601
|
+
}
|
|
602
|
+
// 从配置中删除项目
|
|
603
|
+
delete projectsConfig[projectName];
|
|
604
|
+
console.log(`项目已删除: ${projectName}`);
|
|
605
|
+
return {
|
|
606
|
+
success: true,
|
|
607
|
+
projects: { ...projectsConfig }
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
/**
|
|
611
|
+
* 获取当前所有项目配置
|
|
612
|
+
* @returns 项目配置
|
|
613
|
+
*/
|
|
614
|
+
function getProjects() {
|
|
615
|
+
return { ...projectsConfig };
|
|
616
|
+
}
|
|
617
|
+
async function openLcovReport(projectName, projectRoot) {
|
|
618
|
+
try {
|
|
619
|
+
if (!server) {
|
|
620
|
+
await startServer();
|
|
621
|
+
}
|
|
622
|
+
await addOrUpdateProject(projectName, projectRoot);
|
|
623
|
+
const url = `http://localhost:${serverPort}/${projectName}/index.html`;
|
|
624
|
+
console.log(`正在打开覆盖率报告: ${url}`);
|
|
625
|
+
// 根据不同操作系统,使用相应的命令打开浏览器
|
|
626
|
+
switch (process.platform) {
|
|
627
|
+
case 'darwin': // macOS
|
|
628
|
+
await execAsync(`open "${url}"`);
|
|
629
|
+
break;
|
|
630
|
+
case 'win32': // Windows
|
|
631
|
+
await execAsync(`start "" "${url}"`);
|
|
632
|
+
break;
|
|
633
|
+
default: // Linux及其他类Unix系统
|
|
634
|
+
await execAsync(`xdg-open "${url}"`);
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
console.log('覆盖率报告已在默认浏览器中打开');
|
|
638
|
+
}
|
|
639
|
+
catch (error) {
|
|
640
|
+
console.error('打开覆盖率报告时出错:', error instanceof Error ? error.message : String(error));
|
|
641
|
+
throw error; // 重新抛出错误,允许调用者处理
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
//# sourceMappingURL=report.js.map
|
package/dist/testAgent.js
CHANGED
|
@@ -92,16 +92,22 @@ async function startTestAgent(options) {
|
|
|
92
92
|
if (options.autoCommit) {
|
|
93
93
|
}
|
|
94
94
|
// 5、要是有未覆盖的代码生成新的单测用例代码,重复第2步
|
|
95
|
-
const { total, files } = await (0, tools_1.generateUncoveredLinesReport)(projectConfig.projectRoot, projectConfig.coverageDir
|
|
95
|
+
const { total, files } = await (0, tools_1.generateUncoveredLinesReport)(projectConfig.projectRoot, projectConfig.coverageDir);
|
|
96
96
|
if (ctrl?.signal.aborted) {
|
|
97
97
|
break;
|
|
98
98
|
}
|
|
99
|
-
output(
|
|
100
|
-
const finished = total
|
|
99
|
+
output(`当前代码总覆盖率为 Statements:${parseFloat(total['statements'].pct.toString()) || 0}% Branches:${parseFloat(total['branches'].pct.toString()) || 0}% Functions:${parseFloat(total['functions'].pct.toString()) || 0}% Lines:${parseFloat(total['lines'].pct.toString()) || 0}% `);
|
|
100
|
+
const finished = total.functions.pct >= 100 &&
|
|
101
|
+
total.lines.pct >= 100 &&
|
|
102
|
+
total.branches.pct >= 100 &&
|
|
103
|
+
total.statements.pct >= 100;
|
|
101
104
|
if (!finished) {
|
|
102
105
|
if (files.length) {
|
|
103
|
-
for (const uncoveredLineFile of files.sort((a, b) => a.lines.coverage.
|
|
104
|
-
|
|
106
|
+
for (const uncoveredLineFile of files.sort((a, b) => a.lines.coverage.functions.pct - b.lines.coverage.functions.pct ||
|
|
107
|
+
a.lines.coverage.lines.pct - b.lines.coverage.lines.pct ||
|
|
108
|
+
a.lines.coverage.branches.pct - b.lines.coverage.branches.pct ||
|
|
109
|
+
a.lines.coverage.statements.pct - b.lines.coverage.statements.pct)) {
|
|
110
|
+
output(`发现有未覆盖代码,开始补充...`);
|
|
105
111
|
await (0, agents_1.startAgent)(options.platform, 'addtest', await (0, agents_1.addUncoveredLinePrompt)(uncoveredLineFile.filePath, uncoveredLineFile.lines), projectConfig, options.withConversation, ctrl, options.mode, options.onMessage, async (tool) => {
|
|
106
112
|
toolCalls.push(tool);
|
|
107
113
|
});
|
package/dist/tools/file.js
CHANGED
|
@@ -297,7 +297,7 @@ async function writeFile(filePath, content, encoding = 'utf8', overwrite = true)
|
|
|
297
297
|
await fs.mkdir(dir, { recursive: true });
|
|
298
298
|
}
|
|
299
299
|
const data = content.replace(/\r\n/g, '\n');
|
|
300
|
-
const old = process.env.NODE_ENV === 'test' ? await fs.readFile(filePath, encoding) : '';
|
|
300
|
+
const old = process.env.NODE_ENV === 'test' && exists ? await fs.readFile(filePath, encoding) : '';
|
|
301
301
|
await fs.writeFile(filePath, data, encoding);
|
|
302
302
|
if (process.env.NODE_ENV === 'test') {
|
|
303
303
|
// 比较文件差异
|
package/dist/tools/jest.js
CHANGED
|
@@ -232,21 +232,41 @@ function parseLcovForUncoveredLines(lcovContent, measureType = 'branches') {
|
|
|
232
232
|
const uncoveredLines = {};
|
|
233
233
|
let currentFile = null;
|
|
234
234
|
const DAKey = measureType === 'branches' ? 'BRDA' : measureType === 'functions' ? 'FNDA' : 'DA';
|
|
235
|
-
lcovContent.split('\n')
|
|
235
|
+
const lines = lcovContent.split('\n');
|
|
236
|
+
const fns = new Map();
|
|
237
|
+
lines.forEach((line) => {
|
|
236
238
|
if (line.startsWith('SF:')) {
|
|
237
239
|
// 记录当前文件路径
|
|
238
240
|
currentFile = line.substring(3).trim();
|
|
239
241
|
uncoveredLines[currentFile] = [];
|
|
242
|
+
fns.clear();
|
|
243
|
+
}
|
|
244
|
+
else if (line.startsWith('FN:')) {
|
|
245
|
+
// FN:35,(anonymous_23)
|
|
246
|
+
const lineNumber = parseInt(line.substring(3, line?.lastIndexOf(',')), 10);
|
|
247
|
+
const functionName = line.substring(line?.lastIndexOf(',') + 1);
|
|
248
|
+
fns.set(functionName, lineNumber);
|
|
240
249
|
}
|
|
241
250
|
else if (line.startsWith(DAKey) && currentFile) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
if (measureType === 'functions') {
|
|
252
|
+
// FNDA:3,(anonymous_23)
|
|
253
|
+
const [executionCountStr, functionName] = line.substring(DAKey.length + 1).split(',');
|
|
254
|
+
const lineNumber = fns.get(functionName);
|
|
255
|
+
const executionCount = parseInt(executionCountStr, 10);
|
|
256
|
+
if (executionCount === 0 && !uncoveredLines[currentFile].includes(lineNumber)) {
|
|
257
|
+
uncoveredLines[currentFile].push(lineNumber);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
// 解析行覆盖率数据 (格式: DA:行号,执行次数)
|
|
262
|
+
// BRDA:53,0,0,8 第三位是执行次数
|
|
263
|
+
const [lineNumberStr, ...executionCountStr] = line.substring(DAKey.length + 1).split(',');
|
|
264
|
+
const lineNumber = parseInt(lineNumberStr, 10);
|
|
265
|
+
const executionCount = parseInt(executionCountStr[DAKey === 'BRDA' ? 2 : 0], 10);
|
|
266
|
+
// 如果执行次数为0,表示该行未被覆盖
|
|
267
|
+
if (executionCount === 0 && !uncoveredLines[currentFile].includes(lineNumber)) {
|
|
268
|
+
uncoveredLines[currentFile].push(lineNumber);
|
|
269
|
+
}
|
|
250
270
|
}
|
|
251
271
|
}
|
|
252
272
|
});
|
|
@@ -273,11 +293,10 @@ async function readCoverageSummary(summaryPath) {
|
|
|
273
293
|
* @param projectRoot 项目根目录
|
|
274
294
|
* @param coverage 报告文件夹,默认 'coverage'
|
|
275
295
|
*/
|
|
276
|
-
async function generateUncoveredLinesReport(projectRoot, coverage = 'coverage'
|
|
296
|
+
async function generateUncoveredLinesReport(projectRoot, coverage = 'coverage') {
|
|
277
297
|
const coverageDir = path.join(projectRoot, coverage);
|
|
278
298
|
const lcovInfoPath = path.join(coverageDir, 'lcov.info');
|
|
279
299
|
const summaryPath = path.join(coverageDir, 'coverage-summary.json');
|
|
280
|
-
measureType = measureType || 'branches';
|
|
281
300
|
// 检查覆盖率文件是否存在
|
|
282
301
|
if (!fileExists(lcovInfoPath) || !fileExists(summaryPath)) {
|
|
283
302
|
throw new Error('未找到覆盖率报告文件,请先执行测试并生成覆盖率报告');
|
|
@@ -286,7 +305,10 @@ async function generateUncoveredLinesReport(projectRoot, coverage = 'coverage',
|
|
|
286
305
|
const lcovContent = await fs.readFile(lcovInfoPath, 'utf-8');
|
|
287
306
|
const summary = await readCoverageSummary(summaryPath);
|
|
288
307
|
// 解析未覆盖的行号
|
|
289
|
-
const
|
|
308
|
+
const uncoveredFNLines = parseLcovForUncoveredLines(lcovContent, 'functions');
|
|
309
|
+
const uncoveredLNLines = parseLcovForUncoveredLines(lcovContent, 'lines');
|
|
310
|
+
const uncoveredBRLines = parseLcovForUncoveredLines(lcovContent, 'branches');
|
|
311
|
+
const uncoveredSTLines = parseLcovForUncoveredLines(lcovContent, 'statements');
|
|
290
312
|
// 构建报告对象
|
|
291
313
|
const report = {
|
|
292
314
|
total: summary.total,
|
|
@@ -296,14 +318,23 @@ async function generateUncoveredLinesReport(projectRoot, coverage = 'coverage',
|
|
|
296
318
|
Object.keys(summary).forEach((filePath) => {
|
|
297
319
|
// 相对路径处理
|
|
298
320
|
const relativePath = path.relative(projectRoot, filePath);
|
|
299
|
-
if (filePath === 'total' ||
|
|
321
|
+
if (filePath === 'total' ||
|
|
322
|
+
((uncoveredBRLines[relativePath] || []).length === 0 &&
|
|
323
|
+
(uncoveredFNLines[relativePath] || []).length === 0 &&
|
|
324
|
+
(uncoveredLNLines[relativePath] || []).length === 0 &&
|
|
325
|
+
(uncoveredSTLines[relativePath] || []).length === 0)) {
|
|
300
326
|
return;
|
|
301
327
|
}
|
|
302
328
|
report.files.push({
|
|
303
329
|
filePath,
|
|
304
330
|
lines: {
|
|
305
331
|
coverage: summary[filePath],
|
|
306
|
-
uncoveredLines:
|
|
332
|
+
uncoveredLines: {
|
|
333
|
+
branches: uncoveredBRLines[relativePath],
|
|
334
|
+
functions: uncoveredFNLines[relativePath],
|
|
335
|
+
lines: uncoveredLNLines[relativePath],
|
|
336
|
+
statements: uncoveredSTLines[relativePath]
|
|
337
|
+
}
|
|
307
338
|
}
|
|
308
339
|
});
|
|
309
340
|
});
|
package/dist/tools/project.js
CHANGED
|
@@ -56,10 +56,10 @@ async function detectProjectConfig(projectRoot, defaultConfig) {
|
|
|
56
56
|
await detectLanguages(projectRoot, config);
|
|
57
57
|
// 检测源代码目录
|
|
58
58
|
await detectSrcDir(projectRoot, config);
|
|
59
|
-
// 检测测试相关配置
|
|
60
|
-
await detectTestConfig(projectRoot, config);
|
|
61
59
|
// 检测项目类型(前端/后端/全栈)
|
|
62
60
|
await detectProjectType(projectRoot, config);
|
|
61
|
+
// 检测测试相关配置
|
|
62
|
+
await detectTestConfig(projectRoot, config);
|
|
63
63
|
// 检测框架信息
|
|
64
64
|
await detectFramework(projectRoot, config);
|
|
65
65
|
// 检测Git配置
|
|
@@ -170,6 +170,10 @@ async function detectTestConfig(projectRoot, config) {
|
|
|
170
170
|
}
|
|
171
171
|
// 检测测试目录
|
|
172
172
|
const possibleTestDirs = ['test', 'tests', '__tests__', 'spec', 'specs'];
|
|
173
|
+
if (config.srcDir) {
|
|
174
|
+
// __tests__:这是前端项目(尤其是使用 Jest 作为测试框架的 React 项目)中比较有特色的放置方式。
|
|
175
|
+
possibleTestDirs.push(path_1.default.join(config.srcDir, '__tests__'));
|
|
176
|
+
}
|
|
173
177
|
for (const dir of possibleTestDirs) {
|
|
174
178
|
const dirPath = path_1.default.join(projectRoot, dir);
|
|
175
179
|
if (await directoryExists(dirPath)) {
|
|
@@ -238,16 +242,28 @@ async function detectTestConfig(projectRoot, config) {
|
|
|
238
242
|
const extensions = new Set();
|
|
239
243
|
if (config.languages?.includes('javascript')) {
|
|
240
244
|
extensions.add('js');
|
|
245
|
+
if (config.projectType === 'frontend') {
|
|
246
|
+
extensions.add('jsx');
|
|
247
|
+
}
|
|
241
248
|
}
|
|
242
249
|
if (config.languages?.includes('typescript')) {
|
|
243
250
|
extensions.add('ts');
|
|
251
|
+
if (config.projectType === 'frontend') {
|
|
252
|
+
extensions.add('tsx');
|
|
253
|
+
}
|
|
244
254
|
}
|
|
245
255
|
if (config.languages?.includes('coffeescript')) {
|
|
246
256
|
extensions.add('coffee');
|
|
257
|
+
if (config.projectType === 'frontend') {
|
|
258
|
+
extensions.add('cjsx');
|
|
259
|
+
}
|
|
247
260
|
}
|
|
248
261
|
// 默认为js(如果没有检测到语言)
|
|
249
262
|
if (extensions.size === 0) {
|
|
250
263
|
extensions.add('js');
|
|
264
|
+
if (config.projectType === 'frontend') {
|
|
265
|
+
extensions.add('jsx');
|
|
266
|
+
}
|
|
251
267
|
}
|
|
252
268
|
// 生成基础模式集合
|
|
253
269
|
const basePatterns = new Set();
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai-worktool",
|
|
3
3
|
"displayName": "吃豆豆:单测智能体",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.71",
|
|
5
5
|
"description": "单元测试智能体,帮你开发单元测试用例代码直到100%单测覆盖通过。",
|
|
6
6
|
"author": "jianguoke.cn",
|
|
7
7
|
"license": "MIT",
|
|
@@ -28,11 +28,11 @@
|
|
|
28
28
|
"createup": "node ./uposs.js create-local-folder",
|
|
29
29
|
"changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0",
|
|
30
30
|
"puball": "yarn ver && yarn exepack && yarn vspack && yarn pubvs && yarn pubovsx && yarn puboss && yarn pubnpm",
|
|
31
|
-
"exepack": "yarn createup && cross-env PKG_CACHE_PATH=./binaries pkg -o packages/aicoder-1.0.
|
|
31
|
+
"exepack": "yarn createup && cross-env PKG_CACHE_PATH=./binaries pkg -o packages/aicoder-1.0.71 . && yarn upicon",
|
|
32
32
|
"pubvs": "yarn remove-deps && yarn changelog && vsce publish --yarn --baseContentUrl https://aicoder.jianguoke.cn/assets && yarn restore-deps",
|
|
33
33
|
"pubovsx": "yarn remove-deps && ovsx publish -p 47621ff6-be56-4814-865e-d2a8e8a76f86 --yarn --baseContentUrl https://aicoder.jianguoke.cn/assets && yarn restore-deps",
|
|
34
34
|
"patch": "yarn remove-deps && vsce publish patch --yarn && yarn restore-deps",
|
|
35
|
-
"vspack": "yarn createup && yarn remove-deps && vsce package -o packages/aicoder-1.0.
|
|
35
|
+
"vspack": "yarn createup && yarn remove-deps && vsce package -o packages/aicoder-1.0.71.vsix --yarn --baseContentUrl https://aicoder.jianguoke.cn/assets && yarn restore-deps",
|
|
36
36
|
"puboss": "node ./uposs.js upload",
|
|
37
37
|
"pubnpm": "npm publish --registry=https://registry.npmjs.org",
|
|
38
38
|
"prepare": "husky"
|