@huicai/cli 2026.710.97950 → 2026.711.24616
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/LICENSE +43 -21
- package/bin/huicai.js +93 -2
- package/package.json +4 -3
- package/src/advisor.js +1 -0
- package/src/agent.js +1 -385
- package/src/api.js +1 -326
- package/src/autofix.js +1 -0
- package/src/banner.js +1 -83
- package/src/checkpoints.js +1 -0
- package/src/clipboard.js +1 -0
- package/src/codeindex.js +1 -0
- package/src/colors.js +1 -22
- package/src/commands.js +1 -339
- package/src/compaction.js +1 -140
- package/src/config.js +1 -66
- package/src/consult.js +1 -65
- package/src/context-report.js +1 -0
- package/src/context.js +1 -144
- package/src/cron.js +1 -0
- package/src/daemon.js +1 -0
- package/src/device.js +1 -88
- package/src/diffview.js +1 -0
- package/src/doctor.js +1 -0
- package/src/executors.js +1 -391
- package/src/forks.js +1 -0
- package/src/gitflow.js +1 -0
- package/src/hooks.js +1 -0
- package/src/ide.js +1 -53
- package/src/input.js +1 -182
- package/src/insights.js +1 -0
- package/src/kbsync.js +1 -0
- package/src/markdown.js +0 -0
- package/src/markup-filter.js +1 -85
- package/src/mcp.js +1 -207
- package/src/memory.js +1 -110
- package/src/permissions.js +1 -82
- package/src/render.js +1 -134
- package/src/repl.js +1 -205
- package/src/scan.js +1 -0
- package/src/schedule.js +1 -0
- package/src/session.js +1 -140
- package/src/settings.js +0 -0
- package/src/skills.js +1 -248
- package/src/subagent.js +1 -82
- package/src/todo.js +1 -75
- package/src/tools.js +1 -132
- package/src/transcript.js +1 -0
- package/src/tui/app.js +1 -566
- package/src/usage.js +1 -43
- package/src/watch.js +1 -0
package/src/executors.js
CHANGED
|
@@ -1,391 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 所有写操作强制限定在当前项目目录内(防误伤系统路径);读/搜索可跨目录。
|
|
3
|
-
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import { spawn } from 'node:child_process';
|
|
7
|
-
import * as ide from './ide.js';
|
|
8
|
-
|
|
9
|
-
const MAX_READ_BYTES = 256 * 1024;
|
|
10
|
-
const MAX_FILE_BYTES = 16 * 1024 * 1024; // 超过则要求用 offset/limit 或 grep
|
|
11
|
-
const DEFAULT_READ_LINES = 2000;
|
|
12
|
-
const MAX_LINE_CHARS = 2000;
|
|
13
|
-
const MAX_GREP_MATCHES = 200;
|
|
14
|
-
const MAX_LIST_ENTRIES = 400;
|
|
15
|
-
const MAX_GLOB_RESULTS = 300;
|
|
16
|
-
const BASH_DEFAULT_TIMEOUT = 120_000;
|
|
17
|
-
const BASH_MAX_OUTPUT = 30_000;
|
|
18
|
-
const FETCH_TIMEOUT = 30_000;
|
|
19
|
-
const FETCH_MAX_BYTES = 2 * 1024 * 1024;
|
|
20
|
-
const FETCH_MAX_CHARS = 80_000;
|
|
21
|
-
|
|
22
|
-
const DEFAULT_IGNORES = new Set([
|
|
23
|
-
'.git', 'node_modules', 'dist', 'build', 'target', 'out', '.next', '.nuxt',
|
|
24
|
-
'.venv', 'venv', '__pycache__', '.idea', '.gradle', '.mvn', 'coverage', '.cache',
|
|
25
|
-
]);
|
|
26
|
-
|
|
27
|
-
/** 解析路径为绝对路径;mustBeInside 时强制落在项目目录内。 */
|
|
28
|
-
function resolvePath(cwd, p, mustBeInside) {
|
|
29
|
-
if (!cwd || typeof cwd !== 'string') {
|
|
30
|
-
throw new Error('工作目录未设置:请在项目目录中运行(VS Code 中需先「打开文件夹」再对话)');
|
|
31
|
-
}
|
|
32
|
-
if (!p || typeof p !== 'string') throw new Error('缺少路径 path(工具参数里必须提供 path 字段)');
|
|
33
|
-
const abs = path.resolve(cwd, p);
|
|
34
|
-
if (mustBeInside) {
|
|
35
|
-
const rel = path.relative(cwd, abs);
|
|
36
|
-
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {
|
|
37
|
-
throw new Error(`路径超出项目目录(写操作仅限当前项目内):${p}`);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return abs;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function rel(cwd, abs) {
|
|
44
|
-
const r = path.relative(cwd, abs);
|
|
45
|
-
return r === '' ? '.' : r;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function isProbablyBinary(buf) {
|
|
49
|
-
const n = Math.min(buf.length, 4096);
|
|
50
|
-
for (let i = 0; i < n; i++) if (buf[i] === 0) return true;
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
/** 深度优先遍历文件(跳过忽略目录),产出绝对文件路径。 */
|
|
55
|
-
function* walkFiles(dir, depth = 0, maxDepth = 14) {
|
|
56
|
-
let entries;
|
|
57
|
-
try {
|
|
58
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
59
|
-
} catch {
|
|
60
|
-
return;
|
|
61
|
-
}
|
|
62
|
-
for (const e of entries) {
|
|
63
|
-
if (DEFAULT_IGNORES.has(e.name)) continue;
|
|
64
|
-
const full = path.join(dir, e.name);
|
|
65
|
-
if (e.isDirectory()) {
|
|
66
|
-
if (depth < maxDepth) yield* walkFiles(full, depth + 1, maxDepth);
|
|
67
|
-
} else if (e.isFile()) {
|
|
68
|
-
yield full;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/** 简易 glob → 正则:支持 **(跨目录)、*(同级任意)、?(单字符)。 */
|
|
74
|
-
function globToRegex(glob) {
|
|
75
|
-
let re = '';
|
|
76
|
-
for (let i = 0; i < glob.length; i++) {
|
|
77
|
-
const ch = glob[i];
|
|
78
|
-
if (ch === '*') {
|
|
79
|
-
if (glob[i + 1] === '*') {
|
|
80
|
-
re += '.*';
|
|
81
|
-
i++;
|
|
82
|
-
if (glob[i + 1] === '/') i++;
|
|
83
|
-
} else {
|
|
84
|
-
re += '[^/]*';
|
|
85
|
-
}
|
|
86
|
-
} else if (ch === '?') {
|
|
87
|
-
re += '[^/]';
|
|
88
|
-
} else if ('\\^$+.()|{}[]'.includes(ch)) {
|
|
89
|
-
re += '\\' + ch;
|
|
90
|
-
} else {
|
|
91
|
-
re += ch;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
return new RegExp('^' + re + '$');
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* 读取文件:输出「行号→内容」格式(行号仅供定位,不属于文件内容),默认最多 2000 行。
|
|
99
|
-
* 大文件(>16MB)要求用 offset/limit 分段或 grep 检索,避免占满内存与上下文。
|
|
100
|
-
*/
|
|
101
|
-
export function readFile(cwd, args) {
|
|
102
|
-
const abs = resolvePath(cwd, args.path, false);
|
|
103
|
-
const st = fs.statSync(abs);
|
|
104
|
-
if (st.isDirectory()) throw new Error(`是目录而非文件:${args.path}`);
|
|
105
|
-
if (st.size > MAX_FILE_BYTES) {
|
|
106
|
-
throw new Error(`文件过大(${Math.round(st.size / 1024 / 1024)}MB):请用 grep 检索,或用 run_bash 的 head/tail/sed 分段查看`);
|
|
107
|
-
}
|
|
108
|
-
const buf = fs.readFileSync(abs);
|
|
109
|
-
if (isProbablyBinary(buf)) return `(二进制文件,${st.size} 字节,未展示内容)`;
|
|
110
|
-
const text = buf.toString('utf8');
|
|
111
|
-
if (text === '') return '(空文件)';
|
|
112
|
-
|
|
113
|
-
const lines = text.split('\n');
|
|
114
|
-
// 尾部空行(文件以 \n 结尾产生)不计入展示行数
|
|
115
|
-
if (lines.length > 1 && lines[lines.length - 1] === '') lines.pop();
|
|
116
|
-
const total = lines.length;
|
|
117
|
-
const from = Math.max(0, (Number.isFinite(args.offset) ? args.offset : 1) - 1);
|
|
118
|
-
const limit = Number.isFinite(args.limit) && args.limit > 0 ? args.limit : DEFAULT_READ_LINES;
|
|
119
|
-
const to = Math.min(total, from + limit);
|
|
120
|
-
if (from >= total) return `(起始行 ${from + 1} 超出文件总行数 ${total})`;
|
|
121
|
-
|
|
122
|
-
const width = String(to).length;
|
|
123
|
-
const outLines = [];
|
|
124
|
-
for (let i = from; i < to; i++) {
|
|
125
|
-
let line = lines[i];
|
|
126
|
-
if (line.length > MAX_LINE_CHARS) line = line.slice(0, MAX_LINE_CHARS) + '…(行过长已截断)';
|
|
127
|
-
outLines.push(`${String(i + 1).padStart(width)}→${line}`);
|
|
128
|
-
}
|
|
129
|
-
let outText = outLines.join('\n');
|
|
130
|
-
if (from > 0 || to < total) {
|
|
131
|
-
outText += `\n…(文件共 ${total} 行,本次显示第 ${from + 1}-${to} 行,可用 offset/limit 继续读取)`;
|
|
132
|
-
}
|
|
133
|
-
return outText;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export async function writeFile(cwd, args) {
|
|
137
|
-
const abs = resolvePath(cwd, args.path, true);
|
|
138
|
-
const content = args.content == null ? '' : String(args.content);
|
|
139
|
-
// IDE 内联审阅:把拟写内容交扩展做 diff + 应用;仅传输失败时回退直接写
|
|
140
|
-
if (ide.isActive()) {
|
|
141
|
-
const d = await ide.proposeWrite(abs, content, cwd);
|
|
142
|
-
if (d.decided) {
|
|
143
|
-
return d.applied
|
|
144
|
-
? `已写入 ${rel(cwd, abs)}(经 IDE 审阅应用)`
|
|
145
|
-
: `用户在 IDE 中拒绝了对 ${rel(cwd, abs)} 的修改${d.reason ? ':' + d.reason : ''}`;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
149
|
-
fs.writeFileSync(abs, content, 'utf8');
|
|
150
|
-
return `已写入 ${rel(cwd, abs)}(${Buffer.byteLength(content)} 字节)`
|
|
151
|
-
+ (ide.isActive() ? '(IDE 未响应,已直接写入)' : '');
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export async function editFile(cwd, args) {
|
|
155
|
-
const abs = resolvePath(cwd, args.path, true);
|
|
156
|
-
const oldStr = args.old_string == null ? '' : String(args.old_string);
|
|
157
|
-
const newStr = args.new_string == null ? '' : String(args.new_string);
|
|
158
|
-
if (oldStr === '') throw new Error('edit_file 需要非空 old_string');
|
|
159
|
-
const content = fs.readFileSync(abs, 'utf8');
|
|
160
|
-
const count = content.split(oldStr).length - 1;
|
|
161
|
-
if (count === 0) {
|
|
162
|
-
throw new Error('未找到要替换的内容(old_string 未出现在文件中)。注意:old_string 必须是文件原文,不能包含 read_file 显示的「行号→」前缀');
|
|
163
|
-
}
|
|
164
|
-
if (count > 1 && !args.replace_all) {
|
|
165
|
-
throw new Error(`old_string 在文件中出现 ${count} 处,不唯一;请补充上下文使其唯一,或设 replace_all=true`);
|
|
166
|
-
}
|
|
167
|
-
const updated = args.replace_all ? content.split(oldStr).join(newStr) : content.replace(oldStr, newStr);
|
|
168
|
-
const times = args.replace_all ? count : 1;
|
|
169
|
-
if (ide.isActive()) {
|
|
170
|
-
const d = await ide.proposeWrite(abs, updated, cwd);
|
|
171
|
-
if (d.decided) {
|
|
172
|
-
return d.applied
|
|
173
|
-
? `已编辑 ${rel(cwd, abs)}(经 IDE 审阅应用,替换 ${times} 处)`
|
|
174
|
-
: `用户在 IDE 中拒绝了对 ${rel(cwd, abs)} 的修改${d.reason ? ':' + d.reason : ''}`;
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
fs.writeFileSync(abs, updated, 'utf8');
|
|
178
|
-
return `已编辑 ${rel(cwd, abs)},替换 ${times} 处` + (ide.isActive() ? '(IDE 未响应,已直接写入)' : '');
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export function listFiles(cwd, args) {
|
|
182
|
-
const base = resolvePath(cwd, args.path || '.', false);
|
|
183
|
-
const out = [];
|
|
184
|
-
let count = 0;
|
|
185
|
-
const walk = (dir, depth) => {
|
|
186
|
-
if (count >= MAX_LIST_ENTRIES) return;
|
|
187
|
-
let entries;
|
|
188
|
-
try {
|
|
189
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
190
|
-
} catch {
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
193
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
194
|
-
for (const e of entries) {
|
|
195
|
-
if (count >= MAX_LIST_ENTRIES) break;
|
|
196
|
-
if (DEFAULT_IGNORES.has(e.name)) continue;
|
|
197
|
-
const full = path.join(dir, e.name);
|
|
198
|
-
const r = path.relative(base, full);
|
|
199
|
-
if (e.isDirectory()) {
|
|
200
|
-
out.push(r + '/');
|
|
201
|
-
count++;
|
|
202
|
-
if (depth < 6) walk(full, depth + 1);
|
|
203
|
-
} else {
|
|
204
|
-
out.push(r);
|
|
205
|
-
count++;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
walk(base, 0);
|
|
210
|
-
if (out.length === 0) return '(空目录)';
|
|
211
|
-
return out.join('\n') + (count >= MAX_LIST_ENTRIES ? '\n…(条目已截断)' : '');
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
export function glob(cwd, args) {
|
|
215
|
-
if (!args.pattern) throw new Error('缺少 pattern');
|
|
216
|
-
const re = globToRegex(args.pattern);
|
|
217
|
-
const results = [];
|
|
218
|
-
for (const abs of walkFiles(cwd)) {
|
|
219
|
-
const r = path.relative(cwd, abs).split(path.sep).join('/');
|
|
220
|
-
if (re.test(r)) {
|
|
221
|
-
results.push(r);
|
|
222
|
-
if (results.length >= MAX_GLOB_RESULTS) break;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
if (results.length === 0) return '(无匹配文件)';
|
|
226
|
-
return results.join('\n');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
export function grep(cwd, args) {
|
|
230
|
-
if (!args.pattern) throw new Error('缺少 pattern');
|
|
231
|
-
let re;
|
|
232
|
-
try {
|
|
233
|
-
re = new RegExp(args.pattern, args.ignore_case ? 'i' : undefined);
|
|
234
|
-
} catch (e) {
|
|
235
|
-
throw new Error(`无效的正则:${e.message}`);
|
|
236
|
-
}
|
|
237
|
-
const globRe = args.glob ? globToRegex(args.glob) : null;
|
|
238
|
-
const base = resolvePath(cwd, args.path || '.', false);
|
|
239
|
-
const maxMatches = Number.isFinite(args.max_results) && args.max_results > 0
|
|
240
|
-
? Math.min(args.max_results, 500)
|
|
241
|
-
: MAX_GREP_MATCHES;
|
|
242
|
-
const matches = [];
|
|
243
|
-
const filesHit = [];
|
|
244
|
-
for (const abs of walkFiles(base)) {
|
|
245
|
-
if (matches.length >= maxMatches) break;
|
|
246
|
-
const r = path.relative(cwd, abs).split(path.sep).join('/');
|
|
247
|
-
if (globRe && !globRe.test(r)) continue;
|
|
248
|
-
let buf;
|
|
249
|
-
try {
|
|
250
|
-
buf = fs.readFileSync(abs);
|
|
251
|
-
} catch {
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
254
|
-
if (buf.length > MAX_READ_BYTES || isProbablyBinary(buf)) continue;
|
|
255
|
-
const lines = buf.toString('utf8').split('\n');
|
|
256
|
-
let hit = false;
|
|
257
|
-
for (let i = 0; i < lines.length; i++) {
|
|
258
|
-
if (re.test(lines[i])) {
|
|
259
|
-
hit = true;
|
|
260
|
-
if (args.files_only) break;
|
|
261
|
-
matches.push(`${r}:${i + 1}: ${lines[i].slice(0, 240)}`);
|
|
262
|
-
if (matches.length >= maxMatches) break;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
if (hit) {
|
|
266
|
-
filesHit.push(r);
|
|
267
|
-
if (args.files_only && filesHit.length >= maxMatches) break;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
if (args.files_only) {
|
|
271
|
-
return filesHit.length ? filesHit.join('\n') : '(无匹配)';
|
|
272
|
-
}
|
|
273
|
-
if (matches.length === 0) return '(无匹配)';
|
|
274
|
-
return matches.join('\n') + (matches.length >= maxMatches ? '\n…(结果已截断)' : '');
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
export function runBash(cwd, args) {
|
|
278
|
-
const command = args.command == null ? '' : String(args.command);
|
|
279
|
-
if (command.trim() === '') return Promise.resolve('(未提供命令)');
|
|
280
|
-
const timeout = Number.isFinite(args.timeout) ? Math.min(args.timeout, 600_000) : BASH_DEFAULT_TIMEOUT;
|
|
281
|
-
const isWin = process.platform === 'win32';
|
|
282
|
-
return new Promise((resolve) => {
|
|
283
|
-
let out = '';
|
|
284
|
-
let killed = false;
|
|
285
|
-
// POSIX 下 detached 让子命令自成进程组,超时可整组击杀(防止 shell 被杀而孙进程残留)
|
|
286
|
-
const child = isWin
|
|
287
|
-
? spawn(process.env.COMSPEC || 'cmd.exe', ['/d', '/s', '/c', command], { cwd, windowsVerbatimArguments: true })
|
|
288
|
-
: spawn(process.env.SHELL || '/bin/sh', ['-c', command], { cwd, detached: true });
|
|
289
|
-
const killTree = () => {
|
|
290
|
-
try {
|
|
291
|
-
if (!isWin && child.pid) process.kill(-child.pid, 'SIGKILL');
|
|
292
|
-
else child.kill('SIGKILL');
|
|
293
|
-
} catch {
|
|
294
|
-
try {
|
|
295
|
-
child.kill('SIGKILL');
|
|
296
|
-
} catch {
|
|
297
|
-
/* ignore */
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
};
|
|
301
|
-
const timer = setTimeout(() => {
|
|
302
|
-
killed = true;
|
|
303
|
-
killTree();
|
|
304
|
-
}, timeout);
|
|
305
|
-
const append = (d) => {
|
|
306
|
-
if (out.length < BASH_MAX_OUTPUT) out += d.toString();
|
|
307
|
-
};
|
|
308
|
-
child.stdout.on('data', append);
|
|
309
|
-
child.stderr.on('data', append);
|
|
310
|
-
child.on('error', (e) => {
|
|
311
|
-
clearTimeout(timer);
|
|
312
|
-
resolve(`命令启动失败:${e.message}`);
|
|
313
|
-
});
|
|
314
|
-
child.on('close', (code) => {
|
|
315
|
-
clearTimeout(timer);
|
|
316
|
-
let text = out.length > BASH_MAX_OUTPUT ? out.slice(0, BASH_MAX_OUTPUT) + '\n…(输出已截断)' : out;
|
|
317
|
-
if (text.trim() === '') text = '(无输出)';
|
|
318
|
-
const status = killed ? `【超时被终止】` : `退出码 ${code}`;
|
|
319
|
-
resolve(`${status}\n${text}`);
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
/** 抓取网页/接口并转为可读文本(HTML 去标签;JSON/纯文本原样)。仅 http/https GET。 */
|
|
325
|
-
export async function fetchUrl(cwd, args) {
|
|
326
|
-
const url = String(args.url || '').trim();
|
|
327
|
-
if (!/^https?:\/\//i.test(url)) throw new Error('仅支持 http/https URL');
|
|
328
|
-
let res;
|
|
329
|
-
try {
|
|
330
|
-
res = await fetch(url, {
|
|
331
|
-
redirect: 'follow',
|
|
332
|
-
signal: AbortSignal.timeout(Number.isFinite(args.timeout) ? Math.min(args.timeout, 120_000) : FETCH_TIMEOUT),
|
|
333
|
-
headers: {
|
|
334
|
-
'User-Agent': 'huicai-cli',
|
|
335
|
-
Accept: 'text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.5',
|
|
336
|
-
},
|
|
337
|
-
});
|
|
338
|
-
} catch (e) {
|
|
339
|
-
throw new Error(`请求失败:${e?.cause?.message || e.message}`);
|
|
340
|
-
}
|
|
341
|
-
const buf = await readBodyCapped(res, FETCH_MAX_BYTES);
|
|
342
|
-
const ctype = (res.headers.get('content-type') || '').toLowerCase();
|
|
343
|
-
if (isProbablyBinary(buf) && !/text|json|xml|html/.test(ctype)) {
|
|
344
|
-
return `HTTP ${res.status} · ${ctype || '未知类型'} · ${buf.length} 字节(二进制内容未展示)`;
|
|
345
|
-
}
|
|
346
|
-
let text = buf.toString('utf8');
|
|
347
|
-
if (ctype.includes('html') || /^\s*<(!doctype|html)/i.test(text)) text = htmlToText(text);
|
|
348
|
-
text = text.trim();
|
|
349
|
-
if (text.length > FETCH_MAX_CHARS) text = text.slice(0, FETCH_MAX_CHARS) + '\n…(内容过长已截断)';
|
|
350
|
-
const head = `HTTP ${res.status}${res.url && res.url !== url ? ` · 重定向至 ${res.url}` : ''}`;
|
|
351
|
-
if (!res.ok) return `${head}\n${text || '(无内容)'}`;
|
|
352
|
-
return `${head}\n${text || '(无内容)'}`;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
async function readBodyCapped(res, maxBytes) {
|
|
356
|
-
if (!res.body) return Buffer.alloc(0);
|
|
357
|
-
const reader = res.body.getReader();
|
|
358
|
-
const chunks = [];
|
|
359
|
-
let size = 0;
|
|
360
|
-
while (true) {
|
|
361
|
-
const { done, value } = await reader.read();
|
|
362
|
-
if (done) break;
|
|
363
|
-
chunks.push(value);
|
|
364
|
-
size += value.length;
|
|
365
|
-
if (size >= maxBytes) {
|
|
366
|
-
reader.cancel().catch(() => {});
|
|
367
|
-
break;
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
return Buffer.concat(chunks.map((u) => Buffer.from(u)), Math.min(size, maxBytes));
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
/** 极简 HTML→文本:去 script/style,块级标签换行,去其余标签,解码常见实体,压缩空行。 */
|
|
374
|
-
function htmlToText(html) {
|
|
375
|
-
let s = String(html);
|
|
376
|
-
s = s.replace(/<(script|style|noscript|svg|head)[\s\S]*?<\/\1>/gi, '');
|
|
377
|
-
s = s.replace(/<!--[\s\S]*?-->/g, '');
|
|
378
|
-
s = s.replace(/<br\s*\/?>/gi, '\n');
|
|
379
|
-
s = s.replace(/<\/(p|div|li|tr|h[1-6]|section|article|blockquote|pre|ul|ol|table)>/gi, '\n');
|
|
380
|
-
s = s.replace(/<li[^>]*>/gi, '- ');
|
|
381
|
-
s = s.replace(/<[^>]+>/g, '');
|
|
382
|
-
s = s
|
|
383
|
-
.replace(/ /g, ' ')
|
|
384
|
-
.replace(/</g, '<')
|
|
385
|
-
.replace(/>/g, '>')
|
|
386
|
-
.replace(/"/g, '"')
|
|
387
|
-
.replace(/'|'/g, "'")
|
|
388
|
-
.replace(/&/g, '&');
|
|
389
|
-
s = s.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n');
|
|
390
|
-
return s;
|
|
391
|
-
}
|
|
1
|
+
import t from"node:fs";import e from"node:path";import{spawn as n}from"node:child_process";import*as r from"./ide.js";const i=3e4,o=new Set([".git","node_modules","dist","build","target","out",".next",".nuxt",".venv","venv","__pycache__",".idea",".gradle",".mvn","coverage",".cache"]);function s(t,n,r){if(!t||"string"!=typeof t)throw new Error("工作目录未设置:请在项目目录中运行(VS Code 中需先「打开文件夹」再对话)");if(!n||"string"!=typeof n)throw new Error("缺少路径 path(工具参数里必须提供 path 字段)");const i=e.resolve(t,n);if(r){const r=e.relative(t,i);if(""===r||r.startsWith("..")||e.isAbsolute(r))throw new Error(`路径超出项目目录(写操作仅限当前项目内):${n}`)}return i}function l(t,n){const r=e.relative(t,n);return""===r?".":r}function c(t){const e=Math.min(t.length,4096);for(let n=0;n<e;n++)if(0===t[n])return!0;return!1}function*a(n,r=0,i=14){let s;try{s=t.readdirSync(n,{withFileTypes:!0})}catch{return}for(const t of s){if(o.has(t.name))continue;const s=e.join(n,t.name);t.isDirectory()?r<i&&(yield*a(s,r+1,i)):t.isFile()&&(yield s)}}function u(t){let e="";for(let n=0;n<t.length;n++){const r=t[n];"*"===r?"*"===t[n+1]?(e+=".*",n++,"/"===t[n+1]&&n++):e+="[^/]*":"?"===r?e+="[^/]":"\\^$+.()|{}[]".includes(r)?e+="\\"+r:e+=r}return new RegExp("^"+e+"$")}export function readFile(e,n){const r=s(e,n.path,!1),i=t.statSync(r);if(i.isDirectory())throw new Error(`是目录而非文件:${n.path}`);if(i.size>16777216)throw new Error(`文件过大(${Math.round(i.size/1024/1024)}MB):请用 grep 检索,或用 run_bash 的 head/tail/sed 分段查看`);const o=t.readFileSync(r);if(c(o))return`(二进制文件,${i.size} 字节,未展示内容)`;const l=o.toString("utf8");if(""===l)return"(空文件)";const a=l.split("\n");a.length>1&&""===a[a.length-1]&&a.pop();const u=a.length,p=Math.max(0,(Number.isFinite(n.offset)?n.offset:1)-1),f=Number.isFinite(n.limit)&&n.limit>0?n.limit:2e3,h=Math.min(u,p+f);if(p>=u)return`(起始行 ${p+1} 超出文件总行数 ${u})`;const g=String(h).length,d=[];for(let t=p;t<h;t++){let e=a[t];e.length>2e3&&(e=e.slice(0,2e3)+"…(行过长已截断)"),d.push(`${String(t+1).padStart(g)}→${e}`)}let m=d.join("\n");return(p>0||h<u)&&(m+=`\n…(文件共 ${u} 行,本次显示第 ${p+1}-${h} 行,可用 offset/limit 继续读取)`),m}export async function writeFile(n,i){const o=s(n,i.path,!0),c=null==i.content?"":String(i.content);if(r.isActive()){const t=await r.proposeWrite(o,c,n);if(t.decided)return t.applied?`已写入 ${l(n,o)}(经 IDE 审阅应用)`:`用户在 IDE 中拒绝了对 ${l(n,o)} 的修改${t.reason?":"+t.reason:""}`}return t.mkdirSync(e.dirname(o),{recursive:!0}),t.writeFileSync(o,c,"utf8"),`已写入 ${l(n,o)}(${Buffer.byteLength(c)} 字节)`+(r.isActive()?"(IDE 未响应,已直接写入)":"")}export async function editFile(e,n){const i=s(e,n.path,!0),o=null==n.old_string?"":String(n.old_string),c=null==n.new_string?"":String(n.new_string);if(""===o)throw new Error("edit_file 需要非空 old_string");const a=t.readFileSync(i,"utf8"),u=a.split(o).length-1;if(0===u)throw new Error("未找到要替换的内容(old_string 未出现在文件中)。注意:old_string 必须是文件原文,不能包含 read_file 显示的「行号→」前缀");if(u>1&&!n.replace_all)throw new Error(`old_string 在文件中出现 ${u} 处,不唯一;请补充上下文使其唯一,或设 replace_all=true`);const p=n.replace_all?a.split(o).join(c):a.replace(o,c),f=n.replace_all?u:1;if(r.isActive()){const t=await r.proposeWrite(i,p,e);if(t.decided)return t.applied?`已编辑 ${l(e,i)}(经 IDE 审阅应用,替换 ${f} 处)`:`用户在 IDE 中拒绝了对 ${l(e,i)} 的修改${t.reason?":"+t.reason:""}`}return t.writeFileSync(i,p,"utf8"),`已编辑 ${l(e,i)},替换 ${f} 处`+(r.isActive()?"(IDE 未响应,已直接写入)":"")}export function listFiles(n,r){const i=s(n,r.path||".",!1),l=[];let c=0;const a=(n,r)=>{if(c>=400)return;let s;try{s=t.readdirSync(n,{withFileTypes:!0})}catch{return}s.sort((t,e)=>t.name.localeCompare(e.name));for(const t of s){if(c>=400)break;if(o.has(t.name))continue;const s=e.join(n,t.name),u=e.relative(i,s);t.isDirectory()?(l.push(u+"/"),c++,r<6&&a(s,r+1)):(l.push(u),c++)}};return a(i,0),0===l.length?"(空目录)":l.join("\n")+(c>=400?"\n…(条目已截断)":"")}export function glob(t,n){if(!n.pattern)throw new Error("缺少 pattern");const r=u(n.pattern),i=[];for(const n of a(t)){const o=e.relative(t,n).split(e.sep).join("/");if(r.test(o)&&(i.push(o),i.length>=300))break}return 0===i.length?"(无匹配文件)":i.join("\n")}export function grep(n,r){if(!r.pattern)throw new Error("缺少 pattern");let i;try{i=new RegExp(r.pattern,r.ignore_case?"i":void 0)}catch(t){throw new Error(`无效的正则:${t.message}`)}const o=r.glob?u(r.glob):null,l=s(n,r.path||".",!1),p=Number.isFinite(r.max_results)&&r.max_results>0?Math.min(r.max_results,500):200,f=[],h=[];for(const s of a(l)){if(f.length>=p)break;const l=e.relative(n,s).split(e.sep).join("/");if(o&&!o.test(l))continue;let a;try{a=t.readFileSync(s)}catch{continue}if(a.length>262144||c(a))continue;const u=a.toString("utf8").split("\n");let g=!1;for(let t=0;t<u.length;t++)if(i.test(u[t])){if(g=!0,r.files_only)break;if(f.push(`${l}:${t+1}: ${u[t].slice(0,240)}`),f.length>=p)break}if(g&&(h.push(l),r.files_only&&h.length>=p))break}return r.files_only?h.length?h.join("\n"):"(无匹配)":0===f.length?"(无匹配)":f.join("\n")+(f.length>=p?"\n…(结果已截断)":"")}export function runBash(t,e){const r=null==e.command?"":String(e.command);if(""===r.trim())return Promise.resolve("(未提供命令)");const o=Number.isFinite(e.timeout)?Math.min(e.timeout,6e5):12e4,s="win32"===process.platform;return new Promise(e=>{let l="",c=!1;const a=s?n(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",r],{cwd:t,windowsVerbatimArguments:!0}):n(process.env.SHELL||"/bin/sh",["-c",r],{cwd:t,detached:!0}),u=setTimeout(()=>{c=!0,(()=>{try{!s&&a.pid?process.kill(-a.pid,"SIGKILL"):a.kill("SIGKILL")}catch{try{a.kill("SIGKILL")}catch{}}})()},o),p=t=>{l.length<i&&(l+=t.toString())};a.stdout.on("data",p),a.stderr.on("data",p),a.on("error",t=>{clearTimeout(u),e(`命令启动失败:${t.message}`)}),a.on("close",t=>{clearTimeout(u);let n=l.length>i?l.slice(0,i)+"\n…(输出已截断)":l;""===n.trim()&&(n="(无输出)");e(`${c?"【超时被终止】":`退出码 ${t}`}\n${n}`)})})}export async function fetchUrl(t,e){const n=String(e.url||"").trim();if(!/^https?:\/\//i.test(n))throw new Error("仅支持 http/https URL");let r;try{r=await fetch(n,{redirect:"follow",signal:AbortSignal.timeout(Number.isFinite(e.timeout)?Math.min(e.timeout,12e4):3e4),headers:{"User-Agent":"huicai-cli",Accept:"text/html,application/json;q=0.9,text/plain;q=0.8,*/*;q=0.5"}})}catch(t){throw new Error(`请求失败:${t?.cause?.message||t.message}`)}const i=await async function(t,e){if(!t.body)return Buffer.alloc(0);const n=t.body.getReader(),r=[];let i=0;for(;;){const{done:t,value:o}=await n.read();if(t)break;if(r.push(o),i+=o.length,i>=e){n.cancel().catch(()=>{});break}}return Buffer.concat(r.map(t=>Buffer.from(t)),Math.min(i,e))}(r,2097152),o=(r.headers.get("content-type")||"").toLowerCase();if(c(i)&&!/text|json|xml|html/.test(o))return`HTTP ${r.status} · ${o||"未知类型"} · ${i.length} 字节(二进制内容未展示)`;let s=i.toString("utf8");(o.includes("html")||/^\s*<(!doctype|html)/i.test(s))&&(s=function(t){let e=String(t);return e=e.replace(/<(script|style|noscript|svg|head)[\s\S]*?<\/\1>/gi,""),e=e.replace(/<!--[\s\S]*?-->/g,""),e=e.replace(/<br\s*\/?>/gi,"\n"),e=e.replace(/<\/(p|div|li|tr|h[1-6]|section|article|blockquote|pre|ul|ol|table)>/gi,"\n"),e=e.replace(/<li[^>]*>/gi,"- "),e=e.replace(/<[^>]+>/g,""),e=e.replace(/ /g," ").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'|'/g,"'").replace(/&/g,"&"),e=e.replace(/[ \t]+\n/g,"\n").replace(/\n{3,}/g,"\n\n"),e}(s)),s=s.trim(),s.length>8e4&&(s=s.slice(0,8e4)+"\n…(内容过长已截断)");const l=`HTTP ${r.status}${r.url&&r.url!==n?` · 重定向至 ${r.url}`:""}`;return r.ok,`${l}\n${s||"(无内容)"}`}
|
package/src/forks.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawn as o}from"node:child_process";import r from"node:fs";import e from"node:path";import n from"node:os";const t=[];export function huicaiBin(){const o=process.argv[1]||"";return/huicai/.test(o)?o:null}export function forkAgent({prompt:i,cwd:s,sessionId:c}){const p=huicaiBin();if(!p)throw new Error("无法定位 huicai 可执行文件(请在 CLI 中使用 /fork)");const u=e.join(n.homedir(),".huicai","forks");r.mkdirSync(u,{recursive:!0});const a=e.join(u,Date.now().toString(36)+".log"),d=r.openSync(a,"a"),f=[p];c&&f.push("resume",c),f.push("-p",i,"--yolo","--plain");const l=o(process.execPath,f,{cwd:s,detached:!0,stdio:["ignore",d,d],env:process.env});l.unref();const h={pid:l.pid,prompt:i.replace(/\s+/g," ").slice(0,80),cwd:s,log:a,started:Date.now()};return t.push(h),h}function i(o){try{return process.kill(o,0),!0}catch{return!1}}export function listForks(){return t.map(o=>({...o,alive:i(o.pid)}))}export function killFork(o){if(!t.find(r=>r.pid===o))return{ok:!1,reason:"不是本会话派生的任务"};try{return process.kill(o,"SIGTERM"),{ok:!0}}catch(o){return{ok:!1,reason:o.message}}}
|
package/src/gitflow.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import t from"node:fs";import e from"node:os";import r from"node:path";import{execSync as o}from"node:child_process";import{streamChat as n}from"./api.js";import{DEFAULT_MODEL as i}from"./config.js";const c=/^(agent|workflow|datahuman)-/,a=new Set(["main","master","develop","dev"]);function s(t,e){return o("git "+e,{cwd:t,encoding:"utf8",stdio:["ignore","pipe","pipe"]})}function m(t,e){try{return s(t,e)}catch{return""}}export function gitInfo(t){const e={isRepo:!1,branch:"",dirty:!1,hasRemote:!1,hasGh:!1};try{e.isRepo="true"===s(t,"rev-parse --is-inside-work-tree").trim()}catch{return e}e.branch=m(t,"rev-parse --abbrev-ref HEAD").trim(),e.dirty=m(t,"status --porcelain").trim().length>0,e.hasRemote=m(t,"remote").trim().length>0;try{o("gh --version",{stdio:"ignore"}),e.hasGh=!0}catch{}return e}export async function generateCommitMessage(t,e){let r=m(t,"diff --staged");if(r.trim()||(r=m(t,"diff")),r=r.slice(0,12e3),!r.trim())return"chore: update";const o=c.test(String(e.model||""))?e.compactModel||i:e.model;try{const t=await n({baseUrl:e.baseUrl,apiKey:e.apiKey,model:o,messages:[{role:"system",content:"你是提交信息生成器。根据 git diff 生成一条规范的 Conventional Commits 提交信息:第一行 `<type>(<scope>): <简述>`(简述≤50字,type 用 feat/fix/refactor/docs/test/chore/perf/style 之一);改动较大可空行后加 2-4 条正文要点。只输出提交信息本身,不要解释、不要代码块围栏。"},{role:"user",content:r}]});return(t.content||"").trim().replace(/^```[a-z]*\n?|\n?```$/g,"").trim()||"chore: update"}catch{return"chore: update"}}export async function runPrFlow({cwd:n,config:i,emit:c=()=>{},push:p=!0,openPr:h=!0}){const d=gitInfo(n);if(!d.isRepo)return{ok:!1,error:"当前目录不是 git 仓库"};const u=a.has(d.branch);d.dirty&&(s(n,"add -A"),c("已暂存全部改动"));const f=m(n,"diff --staged --stat").trim();let g="";f&&(c("正在生成提交信息…"),g=await generateCommitMessage(n,i),c("提交信息:"+g.split("\n")[0]));let l=d.branch;u&&(l="huicai/"+function(t){return String(t).split("\n")[0].replace(/^[a-z]+(\([^)]*\))?!?:\s*/i,"").replace(/[^a-zA-Z0-9一-鿿]+/g,"-").replace(/^-+|-+$/g,"").toLowerCase().slice(0,32)||"change"}(g||"change")+"-"+Date.now().toString(36).slice(-4),s(n,`checkout -b ${l}`),c("已从 "+d.branch+" 创建分支 "+l));let y=!1;if(f){const o=r.join(e.tmpdir(),"huicai-commit-"+Date.now()+".txt");t.writeFileSync(o,g);try{s(n,`commit -F "${o}"`),y=!0,c("已提交")}finally{try{t.unlinkSync(o)}catch{}}}else c("无新暂存改动,跳过提交");if(!p)return{ok:!0,branch:l,committed:y};if(!d.hasRemote)return{ok:!0,branch:l,committed:y,note:"无 remote,未推送"};try{s(n,`push -u origin ${l}`),c("已推送 origin/"+l)}catch(t){return{ok:!1,error:"push 失败:"+String(t.message).split("\n")[0],branch:l}}if(!h)return{ok:!0,branch:l,committed:y,pushed:!0};if(!d.hasGh)return{ok:!0,branch:l,committed:y,pushed:!0,note:"未装 gh CLI,PR 请手动开"};try{const t=o("gh pr create --fill",{cwd:n,encoding:"utf8",stdio:["ignore","pipe","pipe"]}).trim();return c("已开 PR:"+t),{ok:!0,branch:l,committed:y,pr:t}}catch(t){return{ok:!0,branch:l,committed:y,pushed:!0,note:"gh pr create 失败(可手动开):"+String(t.message).split("\n")[0]}}}
|
package/src/hooks.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{spawnSync as t}from"node:child_process";import{loadRawSettings as o}from"./settings.js";export const HOOK_EVENTS=["SessionStart","UserPromptSubmit","PreToolUse","PostToolUse","Stop"];const r=new Set(["UserPromptSubmit","PreToolUse"]);function e(t){const o=String(t||"").trim();if(!o.startsWith("{"))return null;try{return JSON.parse(o)}catch{return null}}function s(t,o){const r=[];if(!t||"object"!=typeof t)return r;for(const e of HOOK_EVENTS){const s=t[e];if(Array.isArray(s))for(const t of s){const s="string"==typeof t?t:t?.command;"string"==typeof s&&s.trim()&&r.push({event:e,matcher:t&&"string"==typeof t.matcher&&t.matcher.trim()?t.matcher.trim():null,command:s.trim(),timeout:Number(t?.timeout)>0?Number(t.timeout):3e4,source:o})}}return r}export function loadHooks(t){const{global:r,project:e}=o(t),n=(c=r,"1"===process.env.HUICAI_PROJECT_HOOKS||!0===c?.hooks?.trustProject);var c;const i=[...s(r.hooks,"global")],u=s(e.hooks,"project");let m=0;n?i.push(...u):m=u.length;const a=Object.fromEntries(HOOK_EVENTS.map(t=>[t,[]]));for(const t of i)a[t.event].push(t);return{byEvent:a,all:i,projectIgnored:m,projectTrusted:n}}export function hookMatches(t,o){if(!t.matcher)return!0;if(!o)return!0;try{return new RegExp(t.matcher).test(o)}catch{return!1}}function n(t){return(t=String(t||"")).length>8e3?t.slice(0,8e3)+"\n…(钩子输出已截断)":t}function c(o,r,e){const s="win32"===process.platform?t(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",o.command],{cwd:r.HUICAI_CWD,env:r,input:e,encoding:"utf8",timeout:o.timeout,maxBuffer:4194304,windowsVerbatimArguments:!0}):t(process.env.SHELL||"/bin/sh",["-c",o.command],{cwd:r.HUICAI_CWD,env:r,input:e,encoding:"utf8",timeout:o.timeout,maxBuffer:4194304}),c=n(s.stdout),i=n(s.stderr),u=s.error&&"ETIMEDOUT"===s.error.code;return{stdout:c,stderr:i,status:u?124:null==s.status?1:s.status,timedOut:u}}export function runHooks(t,o,s){const n=o.cwd,i=(s?.byEvent?.[t]||loadHooks(n).byEvent[t]||[]).filter(t=>hookMatches(t,o.toolName)),u={block:!1,reason:"",outputs:[],injectContext:"",modifiedArgs:null};if(!i.length)return u;const m={...process.env,HUICAI_EVENT:t,HUICAI_CWD:n||process.cwd(),HUICAI_TOOL_NAME:o.toolName||"",HUICAI_TOOL_PATH:o.args&&null!=o.args.path?String(o.args.path):"",HUICAI_TOOL_ARGS:(()=>{try{return JSON.stringify(o.args||{})}catch{return"{}"}})()},a=(()=>{try{return JSON.stringify(o)}catch{return"{}"}})();for(const o of i){let s;try{s=c(o,m,a)}catch(t){s={stdout:"",stderr:String(t.message||t),status:1,timedOut:!1}}const n=e(s.stdout);if(n){if("string"==typeof n.context&&n.context.trim()&&(u.injectContext+=(u.injectContext?"\n":"")+n.context.trim()),n.modifiedArgs&&"object"==typeof n.modifiedArgs&&(u.modifiedArgs={...u.modifiedArgs||{},...n.modifiedArgs}),"block"===n.decision&&r.has(t)){u.block=!0,u.reason=String(n.reason||"钩子决定阻断该操作");break}continue}if((s.stdout||s.stderr||"").trim()&&u.outputs.push({command:o.command,stdout:s.stdout.trim()||s.stderr.trim()}),0!==s.status&&r.has(t)){u.block=!0,u.reason=s.timedOut?`钩子超时(${o.command})`:s.stderr.trim()||s.stdout.trim()||`钩子以退出码 ${s.status} 阻断(${o.command})`;break}}return u}export function formatHooks(t){const o=[];if(t.all.length)for(const r of HOOK_EVENTS){const e=t.byEvent[r];if(e.length){o.push(`${r}`);for(const t of e){const r=t.matcher?`[${t.matcher}] `:"";o.push(` · ${r}${t.command}${"global"===t.source?"":"(项目)"}`)}}}else o.push('未配置钩子。可在 ~/.huicai/settings.json 或 项目 .huicai/settings.json 的 "hooks" 中添加。');return t.projectIgnored>0&&(o.push(""),o.push(`⚠ 已忽略 ${t.projectIgnored} 条项目级钩子(未信任本项目)。`),o.push(" 如信任本项目,设 HUICAI_PROJECT_HOOKS=1,或全局设置 hooks.trustProject=true。")),o.push(""),o.push("事件:SessionStart(会话开始,注入上下文)· UserPromptSubmit(提交前,注入/阻断)· PreToolUse(工具前,阻断/改参数)· PostToolUse(工具后,采输出)· Stop(回合结束)。"),o.push('结构化控制:钩子可把 JSON 打到 stdout —— {"decision":"block","reason":..} 阻断 / {"context":".."} 注入上下文 / {"modifiedArgs":{..}} 改工具参数。'),o.join("\n")}
|
package/src/ide.js
CHANGED
|
@@ -1,53 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 把 write_file/edit_file 的「拟写入内容」发给扩展做「内联 diff 审阅 + 应用」,而非 CLI 直接落盘。
|
|
3
|
-
// 未在 IDE 环境(无 HUICAI_IDE_PORT)则本模块不介入,CLI 照常直接写文件。
|
|
4
|
-
|
|
5
|
-
import http from 'node:http';
|
|
6
|
-
|
|
7
|
-
const APPLY_TIMEOUT = 120_000; // 用户审阅可能较久
|
|
8
|
-
|
|
9
|
-
/** 是否处于 IDE 集成环境(由扩展注入端口)。 */
|
|
10
|
-
export function isActive() {
|
|
11
|
-
return !!process.env.HUICAI_IDE_PORT;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 把拟写入内容提交给 IDE 扩展做内联 diff 审阅。
|
|
16
|
-
* @returns {Promise<{decided:boolean, applied?:boolean, reason?:string}>}
|
|
17
|
-
* decided=false 表示传输失败(扩展未响应)——调用方应回退为直接写盘。
|
|
18
|
-
*/
|
|
19
|
-
export function proposeWrite(absPath, content, cwd) {
|
|
20
|
-
return new Promise((resolve) => {
|
|
21
|
-
const port = process.env.HUICAI_IDE_PORT;
|
|
22
|
-
const token = process.env.HUICAI_IDE_TOKEN || '';
|
|
23
|
-
const body = JSON.stringify({ path: absPath, content, cwd, token });
|
|
24
|
-
const req = http.request(
|
|
25
|
-
{
|
|
26
|
-
host: '127.0.0.1',
|
|
27
|
-
port,
|
|
28
|
-
path: '/apply',
|
|
29
|
-
method: 'POST',
|
|
30
|
-
headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(body) },
|
|
31
|
-
},
|
|
32
|
-
(res) => {
|
|
33
|
-
let d = '';
|
|
34
|
-
res.on('data', (c) => (d += c));
|
|
35
|
-
res.on('end', () => {
|
|
36
|
-
try {
|
|
37
|
-
const j = JSON.parse(d);
|
|
38
|
-
resolve({ decided: true, applied: !!j.applied, reason: j.reason });
|
|
39
|
-
} catch {
|
|
40
|
-
resolve({ decided: false });
|
|
41
|
-
}
|
|
42
|
-
});
|
|
43
|
-
},
|
|
44
|
-
);
|
|
45
|
-
req.on('error', () => resolve({ decided: false }));
|
|
46
|
-
req.setTimeout(APPLY_TIMEOUT, () => {
|
|
47
|
-
req.destroy();
|
|
48
|
-
resolve({ decided: false });
|
|
49
|
-
});
|
|
50
|
-
req.write(body);
|
|
51
|
-
req.end();
|
|
52
|
-
});
|
|
53
|
-
}
|
|
1
|
+
import e from"node:http";export function isActive(){return!!process.env.HUICAI_IDE_PORT}export function proposeWrite(t,n,o){return new Promise(r=>{const d=process.env.HUICAI_IDE_PORT,p=process.env.HUICAI_IDE_TOKEN||"",s=JSON.stringify({path:t,content:n,cwd:o,token:p}),i=e.request({host:"127.0.0.1",port:d,path:"/apply",method:"POST",headers:{"Content-Type":"application/json","Content-Length":Buffer.byteLength(s)}},e=>{let t="";e.on("data",e=>t+=e),e.on("end",()=>{try{const e=JSON.parse(t);r({decided:!0,applied:!!e.applied,reason:e.reason})}catch{r({decided:!1})}})});i.on("error",()=>r({decided:!1})),i.setTimeout(12e4,()=>{i.destroy(),r({decided:!1})}),i.write(s),i.end()})}
|