@ww_nero/mini-cli 1.0.95 → 1.0.97
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/package.json +1 -1
- package/src/chat.js +70 -21
- package/src/tools/bash.js +5 -5
- package/src/tools/edit.js +9 -9
- package/src/tools/mcp.js +3 -3
- package/src/tools/read.js +3 -3
- package/src/tools/skills.js +1 -1
- package/src/tools/write.js +4 -4
package/package.json
CHANGED
package/src/chat.js
CHANGED
|
@@ -111,16 +111,35 @@ const splitDisplayLines = (text = '') => {
|
|
|
111
111
|
return lines;
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
+
const isToolError = (result) => {
|
|
115
|
+
if (typeof result === 'string' && result.startsWith('[Error]')) {
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (result && typeof result === 'object' && result.success === false) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
};
|
|
123
|
+
|
|
114
124
|
const formatWriteOutput = (result) => {
|
|
125
|
+
if (isToolError(result)) {
|
|
126
|
+
const errorMsg = typeof result === 'string' ? result : (result.message || '写入失败');
|
|
127
|
+
return { isError: true, message: errorMsg.replace('[Error] ', '') };
|
|
128
|
+
}
|
|
115
129
|
if (!result || typeof result !== 'object' || !result.success) {
|
|
116
|
-
return
|
|
130
|
+
return { isError: true, message: '写入失败' };
|
|
117
131
|
}
|
|
118
|
-
|
|
132
|
+
const lineCount = result.lineCount || countLines(result.content);
|
|
133
|
+
return { isError: false, lineCount };
|
|
119
134
|
};
|
|
120
135
|
|
|
121
136
|
const formatEditOutput = (result) => {
|
|
137
|
+
if (isToolError(result)) {
|
|
138
|
+
const errorMsg = typeof result === 'string' ? result : (result.message || '编辑失败');
|
|
139
|
+
return { isError: true, message: errorMsg.replace('[Error] ', '') };
|
|
140
|
+
}
|
|
122
141
|
if (!result || typeof result !== 'object' || !result.success) {
|
|
123
|
-
return
|
|
142
|
+
return { isError: true, message: '编辑失败' };
|
|
124
143
|
}
|
|
125
144
|
const lines = [];
|
|
126
145
|
const parts = diffLines(result.search || '', result.replace || '');
|
|
@@ -133,7 +152,7 @@ const formatEditOutput = (result) => {
|
|
|
133
152
|
}
|
|
134
153
|
}
|
|
135
154
|
|
|
136
|
-
return lines.join('\n');
|
|
155
|
+
return { isError: false, diff: lines.join('\n') };
|
|
137
156
|
};
|
|
138
157
|
|
|
139
158
|
const stringifyToolResult = (value) => {
|
|
@@ -172,6 +191,22 @@ const formatToolResultForDisplay = (text, maxLength = 100) => {
|
|
|
172
191
|
return normalized.length > maxLength ? 'Done' : normalized;
|
|
173
192
|
};
|
|
174
193
|
|
|
194
|
+
const getReadableErrorMessage = (error) => {
|
|
195
|
+
if (error && typeof error.message === 'string' && error.message.trim()) {
|
|
196
|
+
return error.message.trim();
|
|
197
|
+
}
|
|
198
|
+
if (typeof error === 'string' && error.trim()) {
|
|
199
|
+
return error.trim();
|
|
200
|
+
}
|
|
201
|
+
return '未知错误';
|
|
202
|
+
};
|
|
203
|
+
|
|
204
|
+
const buildEmptyAssistantResponseNotice = (usage) => {
|
|
205
|
+
const usageText = formatUsageTokens(usage);
|
|
206
|
+
const usageSuffix = usageText ? `(${usageText})` : '';
|
|
207
|
+
return `本次请求已结束,但模型未返回可显示内容${usageSuffix}。可能是接口返回空响应、只返回了空白内容,或流式输出在正文前结束。请重试,必要时检查模型服务日志。`;
|
|
208
|
+
};
|
|
209
|
+
|
|
175
210
|
const indentToolResult = (text = '') => splitDisplayLines(text)
|
|
176
211
|
.map((line) => ` ${line}`)
|
|
177
212
|
.join('\n');
|
|
@@ -928,7 +963,8 @@ const startChatSession = async ({
|
|
|
928
963
|
onReasoningToken: handleReasoningChunk,
|
|
929
964
|
onUsage: updateLoadingPromptTokens,
|
|
930
965
|
onRetry: (retryCount, error) => {
|
|
931
|
-
const
|
|
966
|
+
const detailMessage = error ? getReadableErrorMessage(error) : '';
|
|
967
|
+
const detail = detailMessage ? `(${detailMessage})` : '';
|
|
932
968
|
process.stdout.write(`\n${chalk.yellow(`请求失败,${retryCount} 次重试中${detail}...`)}\n`);
|
|
933
969
|
}
|
|
934
970
|
});
|
|
@@ -983,7 +1019,7 @@ const startChatSession = async ({
|
|
|
983
1019
|
try {
|
|
984
1020
|
toolResult = await handler(parsedArgs);
|
|
985
1021
|
} catch (toolError) {
|
|
986
|
-
toolResult =
|
|
1022
|
+
toolResult = `[Error] 工具执行异常: ${toolError.message}`;
|
|
987
1023
|
} finally {
|
|
988
1024
|
clearLoadingPrompt();
|
|
989
1025
|
}
|
|
@@ -994,30 +1030,33 @@ const startChatSession = async ({
|
|
|
994
1030
|
|
|
995
1031
|
// Display tool output
|
|
996
1032
|
if (functionName === 'write') {
|
|
997
|
-
// For write, display full content in white
|
|
998
1033
|
const writeOutput = formatWriteOutput(toolResult);
|
|
999
|
-
if (writeOutput) {
|
|
1000
|
-
console.log(writeOutput);
|
|
1034
|
+
if (writeOutput.isError) {
|
|
1035
|
+
console.log(chalk.red(indentToolResult(writeOutput.message)));
|
|
1036
|
+
} else {
|
|
1037
|
+
console.log(chalk.gray(`已写入,共${writeOutput.lineCount}行`));
|
|
1001
1038
|
}
|
|
1002
1039
|
} else if (functionName === 'edit') {
|
|
1003
|
-
// For edit, display diff-style output
|
|
1004
1040
|
const editOutput = formatEditOutput(toolResult);
|
|
1005
|
-
if (editOutput) {
|
|
1006
|
-
console.log(editOutput);
|
|
1041
|
+
if (editOutput.isError) {
|
|
1042
|
+
console.log(chalk.red(indentToolResult(editOutput.message)));
|
|
1043
|
+
} else if (editOutput.diff) {
|
|
1044
|
+
console.log(editOutput.diff);
|
|
1007
1045
|
}
|
|
1008
1046
|
} else if (functionName === 'read') {
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
if (normalized && !normalized.startsWith('读取失败') && !normalized.startsWith('无效路径') && !normalized.startsWith('filePath 参数不能为空')) {
|
|
1013
|
-
console.log(chalk.gray(`已读取,共${lineCount}行`));
|
|
1047
|
+
const isError = rawToolContent.startsWith('[Error]');
|
|
1048
|
+
if (isError) {
|
|
1049
|
+
console.log(chalk.red(indentToolResult(rawToolContent.replace('[Error] ', ''))));
|
|
1014
1050
|
} else {
|
|
1015
|
-
|
|
1051
|
+
const normalized = rawToolContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n').trim();
|
|
1052
|
+
const lineCount = countLines(normalized);
|
|
1053
|
+
console.log(chalk.gray(`已读取,共${lineCount}行`));
|
|
1016
1054
|
}
|
|
1017
1055
|
} else {
|
|
1056
|
+
const isError = rawToolContent.startsWith('[Error]');
|
|
1018
1057
|
const displayResult = formatToolResultForDisplay(rawToolContent, 100);
|
|
1019
1058
|
if (displayResult) {
|
|
1020
|
-
console.log(chalk.gray(indentToolResult(displayResult)));
|
|
1059
|
+
console.log((isError ? chalk.red : chalk.gray)(indentToolResult(displayResult.replace('[Error] ', ''))));
|
|
1021
1060
|
}
|
|
1022
1061
|
}
|
|
1023
1062
|
|
|
@@ -1070,8 +1109,18 @@ const startChatSession = async ({
|
|
|
1070
1109
|
continue;
|
|
1071
1110
|
}
|
|
1072
1111
|
|
|
1073
|
-
conversationAdvanced = true;
|
|
1074
1112
|
const finalText = sanitizeContentWithThink(streamState.content, result.content);
|
|
1113
|
+
if (!mergedReasoning && !finalText) {
|
|
1114
|
+
if (messages[messages.length - 1] === userMessage) {
|
|
1115
|
+
messages.pop();
|
|
1116
|
+
persistHistorySafely();
|
|
1117
|
+
}
|
|
1118
|
+
ensureNewline();
|
|
1119
|
+
console.log(chalk.yellow(buildEmptyAssistantResponseNotice(result.usage)));
|
|
1120
|
+
break;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
conversationAdvanced = true;
|
|
1075
1124
|
renderAssistantOutput(mergedReasoning, finalText, ensureNewline);
|
|
1076
1125
|
const assistantMessage = buildHistoryAssistantMessage(
|
|
1077
1126
|
result.message,
|
|
@@ -1102,7 +1151,7 @@ const startChatSession = async ({
|
|
|
1102
1151
|
console.log(chalk.yellow('对话已取消。'));
|
|
1103
1152
|
}
|
|
1104
1153
|
} else {
|
|
1105
|
-
console.error(chalk.red(`请求失败: ${error
|
|
1154
|
+
console.error(chalk.red(`请求失败: ${getReadableErrorMessage(error)}`));
|
|
1106
1155
|
}
|
|
1107
1156
|
} finally {
|
|
1108
1157
|
clearLoadingPrompt();
|
package/src/tools/bash.js
CHANGED
|
@@ -3,7 +3,7 @@ const { resolveWorkspacePath } = require('../utils/helpers');
|
|
|
3
3
|
|
|
4
4
|
const executeCommand = async ({ command, workingDirectory = '.', isService = false } = {}, context = {}) => {
|
|
5
5
|
if (!command || typeof command !== 'string' || !command.trim()) {
|
|
6
|
-
return 'command 参数不能为空';
|
|
6
|
+
return '[Error] command 参数不能为空';
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
const normalizedCommand = command.replace(/\bpython3\b/g, 'python');
|
|
@@ -16,7 +16,7 @@ const executeCommand = async ({ command, workingDirectory = '.', isService = fal
|
|
|
16
16
|
try {
|
|
17
17
|
cwd = resolveWorkspacePath(context.workspaceRoot, workingDirectory || '.');
|
|
18
18
|
} catch (error) {
|
|
19
|
-
return
|
|
19
|
+
return `[Error] 工作目录无效: ${error.message}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
return new Promise((resolve) => {
|
|
@@ -53,7 +53,7 @@ const executeCommand = async ({ command, workingDirectory = '.', isService = fal
|
|
|
53
53
|
if (settled) return;
|
|
54
54
|
settled = true;
|
|
55
55
|
cleanup();
|
|
56
|
-
resolve(
|
|
56
|
+
resolve(`[Error] 命令执行失败: ${error.message}`);
|
|
57
57
|
};
|
|
58
58
|
|
|
59
59
|
const handleProcessClose = (code) => {
|
|
@@ -66,7 +66,7 @@ const executeCommand = async ({ command, workingDirectory = '.', isService = fal
|
|
|
66
66
|
} else {
|
|
67
67
|
const errOutput = stderr.trim();
|
|
68
68
|
const stdOutput = stdout.trim();
|
|
69
|
-
resolve(
|
|
69
|
+
resolve(`[Error] 命令执行失败,退出码 ${code}${errOutput ? `\n错误: ${errOutput}` : ''}${stdOutput ? `\n输出: ${stdOutput}` : ''}`);
|
|
70
70
|
}
|
|
71
71
|
};
|
|
72
72
|
|
|
@@ -98,7 +98,7 @@ const executeCommand = async ({ command, workingDirectory = '.', isService = fal
|
|
|
98
98
|
if (settled) return;
|
|
99
99
|
child.kill('SIGTERM');
|
|
100
100
|
cleanup();
|
|
101
|
-
resolve(
|
|
101
|
+
resolve(`[Error] 命令执行超时 (超过 ${executionTimeout / 1000}s)`);
|
|
102
102
|
}, executionTimeout);
|
|
103
103
|
}
|
|
104
104
|
});
|
package/src/tools/edit.js
CHANGED
|
@@ -10,27 +10,27 @@ const {
|
|
|
10
10
|
|
|
11
11
|
const editFile = async ({ filePath, search, replace } = {}, context = {}) => {
|
|
12
12
|
if (!filePath || typeof filePath !== 'string') {
|
|
13
|
-
return 'filePath 参数不能为空';
|
|
13
|
+
return '[Error] filePath 参数不能为空';
|
|
14
14
|
}
|
|
15
15
|
if (typeof search !== 'string' || search.trim() === '') {
|
|
16
|
-
return 'search 参数不能为空';
|
|
16
|
+
return '[Error] search 参数不能为空';
|
|
17
17
|
}
|
|
18
18
|
if (typeof replace !== 'string') {
|
|
19
|
-
return 'replace 参数必须是字符串,可为空字符串表示删除';
|
|
19
|
+
return '[Error] replace 参数必须是字符串,可为空字符串表示删除';
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
let absolutePath;
|
|
23
23
|
try {
|
|
24
24
|
absolutePath = resolveWorkspacePath(context.workspaceRoot, filePath);
|
|
25
25
|
} catch (error) {
|
|
26
|
-
return
|
|
26
|
+
return `[Error] 无效路径: ${error.message}`;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
let originCode;
|
|
30
30
|
try {
|
|
31
31
|
originCode = await readTextFile(absolutePath);
|
|
32
32
|
} catch (error) {
|
|
33
|
-
return
|
|
33
|
+
return `[Error] 读取文件失败 ${filePath}: ${error.message}`;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
const normalizedOrigin = normalizeLineBreaks(originCode);
|
|
@@ -38,7 +38,7 @@ const editFile = async ({ filePath, search, replace } = {}, context = {}) => {
|
|
|
38
38
|
let normalizedReplace = normalizeLineBreaks(replace);
|
|
39
39
|
|
|
40
40
|
if (!normalizedSearch) {
|
|
41
|
-
return 'search 参数规范化后为空';
|
|
41
|
+
return '[Error] search 参数规范化后为空';
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
if (normalizedReplace) {
|
|
@@ -58,12 +58,12 @@ const editFile = async ({ filePath, search, replace } = {}, context = {}) => {
|
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
if (!normalizedOrigin.includes(searchPattern)) {
|
|
61
|
-
return
|
|
61
|
+
return `[Error] 在文件 ${filePath} 中未找到要搜索的内容`;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
const updated = normalizedOrigin.replaceAll(searchPattern, normalizedReplace);
|
|
65
65
|
if (isCodeIdentical(originCode, updated)) {
|
|
66
|
-
return
|
|
66
|
+
return `[Error] 修改前后内容相同: ${filePath}`;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const processed = processContent(updated);
|
|
@@ -78,7 +78,7 @@ const editFile = async ({ filePath, search, replace } = {}, context = {}) => {
|
|
|
78
78
|
replace: normalizedReplace
|
|
79
79
|
};
|
|
80
80
|
} catch (error) {
|
|
81
|
-
return
|
|
81
|
+
return `[Error] 搜索替换失败 ${filePath}: ${error.message}`;
|
|
82
82
|
}
|
|
83
83
|
};
|
|
84
84
|
|
package/src/tools/mcp.js
CHANGED
|
@@ -319,7 +319,7 @@ const resolveImageSource = (workspaceRoot, args = {}) => {
|
|
|
319
319
|
const resolved = path.resolve(root, imageSource);
|
|
320
320
|
const relative = path.relative(root, resolved);
|
|
321
321
|
if (relative.startsWith('..') || path.isAbsolute(relative) || !fs.existsSync(resolved)) {
|
|
322
|
-
return { error: '找不到图片' };
|
|
322
|
+
return { error: '[Error] 找不到图片' };
|
|
323
323
|
}
|
|
324
324
|
return { args: { ...args, image_source: resolved } };
|
|
325
325
|
};
|
|
@@ -438,11 +438,11 @@ class McpManager {
|
|
|
438
438
|
const isError = Boolean(result && result.isError);
|
|
439
439
|
const output = formatMcpContent(result && result.content);
|
|
440
440
|
if (isError) {
|
|
441
|
-
return output ? `MCP 工具返回错误:${output}` : 'MCP 工具返回错误';
|
|
441
|
+
return output ? `[Error] MCP 工具返回错误:${output}` : '[Error] MCP 工具返回错误';
|
|
442
442
|
}
|
|
443
443
|
return output || 'MCP 工具无输出';
|
|
444
444
|
} catch (error) {
|
|
445
|
-
return
|
|
445
|
+
return `[Error] 调用 MCP 工具失败:${error.message}`;
|
|
446
446
|
}
|
|
447
447
|
}
|
|
448
448
|
};
|
package/src/tools/read.js
CHANGED
|
@@ -2,20 +2,20 @@ const { resolveWorkspacePath, readTextFile } = require('../utils/helpers');
|
|
|
2
2
|
|
|
3
3
|
const readFile = async ({ filePath } = {}, context = {}) => {
|
|
4
4
|
if (!filePath || typeof filePath !== 'string') {
|
|
5
|
-
return 'filePath 参数不能为空';
|
|
5
|
+
return '[Error] filePath 参数不能为空';
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
let absolutePath;
|
|
9
9
|
try {
|
|
10
10
|
absolutePath = resolveWorkspacePath(context.workspaceRoot, filePath);
|
|
11
11
|
} catch (error) {
|
|
12
|
-
return
|
|
12
|
+
return `[Error] 无效路径: ${error.message}`;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
try {
|
|
16
16
|
return await readTextFile(absolutePath);
|
|
17
17
|
} catch (error) {
|
|
18
|
-
return
|
|
18
|
+
return `[Error] 读取失败 ${filePath}: ${error.message}`;
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
21
|
|
package/src/tools/skills.js
CHANGED
package/src/tools/write.js
CHANGED
|
@@ -11,17 +11,17 @@ const countLines = (text = '') => {
|
|
|
11
11
|
|
|
12
12
|
const writeFile = async ({ filePath, content } = {}, context = {}) => {
|
|
13
13
|
if (!filePath || typeof filePath !== 'string') {
|
|
14
|
-
return 'filePath 参数不能为空';
|
|
14
|
+
return '[Error] filePath 参数不能为空';
|
|
15
15
|
}
|
|
16
16
|
if (typeof content !== 'string') {
|
|
17
|
-
return 'content 必须是字符串';
|
|
17
|
+
return '[Error] content 必须是字符串';
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
let absolutePath;
|
|
21
21
|
try {
|
|
22
22
|
absolutePath = resolveWorkspacePath(context.workspaceRoot, filePath);
|
|
23
23
|
} catch (error) {
|
|
24
|
-
return
|
|
24
|
+
return `[Error] 无效路径: ${error.message}`;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
try {
|
|
@@ -33,7 +33,7 @@ const writeFile = async ({ filePath, content } = {}, context = {}) => {
|
|
|
33
33
|
lineCount: countLines(content)
|
|
34
34
|
};
|
|
35
35
|
} catch (error) {
|
|
36
|
-
return
|
|
36
|
+
return `[Error] 写入失败 ${filePath}: ${error.message}`;
|
|
37
37
|
}
|
|
38
38
|
};
|
|
39
39
|
|