@huicai/cli 2026.711.0 → 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.
Files changed (49) hide show
  1. package/LICENSE +43 -21
  2. package/package.json +4 -3
  3. package/src/advisor.js +1 -46
  4. package/src/agent.js +1 -518
  5. package/src/api.js +1 -359
  6. package/src/autofix.js +1 -90
  7. package/src/banner.js +1 -83
  8. package/src/checkpoints.js +1 -99
  9. package/src/clipboard.js +1 -34
  10. package/src/codeindex.js +1 -229
  11. package/src/colors.js +1 -47
  12. package/src/commands.js +1 -1294
  13. package/src/compaction.js +1 -144
  14. package/src/config.js +1 -86
  15. package/src/consult.js +1 -65
  16. package/src/context-report.js +1 -119
  17. package/src/context.js +1 -152
  18. package/src/cron.js +1 -64
  19. package/src/daemon.js +1 -62
  20. package/src/device.js +1 -88
  21. package/src/diffview.js +1 -115
  22. package/src/doctor.js +1 -72
  23. package/src/executors.js +1 -391
  24. package/src/forks.js +1 -59
  25. package/src/gitflow.js +1 -140
  26. package/src/hooks.js +1 -207
  27. package/src/ide.js +1 -53
  28. package/src/input.js +1 -182
  29. package/src/insights.js +1 -102
  30. package/src/kbsync.js +1 -141
  31. package/src/markdown.js +0 -0
  32. package/src/markup-filter.js +1 -85
  33. package/src/mcp.js +1 -207
  34. package/src/memory.js +1 -161
  35. package/src/permissions.js +1 -120
  36. package/src/render.js +1 -138
  37. package/src/repl.js +1 -271
  38. package/src/scan.js +1 -84
  39. package/src/schedule.js +1 -48
  40. package/src/session.js +1 -140
  41. package/src/settings.js +0 -0
  42. package/src/skills.js +1 -248
  43. package/src/subagent.js +1 -183
  44. package/src/todo.js +1 -75
  45. package/src/tools.js +1 -147
  46. package/src/transcript.js +1 -94
  47. package/src/tui/app.js +1 -666
  48. package/src/usage.js +1 -43
  49. package/src/watch.js +1 -74
package/src/api.js CHANGED
@@ -1,359 +1 @@
1
- // HuiCai_AI 平台 OpenAI 兼容 /v1 客户端(零依赖,用 Node 内置 fetch + Web Streams)。
2
- //
3
- // 对「原始模型 id」,平台 /v1/chat/completions 为透明代理:透传 tools、原样回传 tool_calls 与流式增量。
4
- // 因此本客户端把平台当作「会做函数调用决策」的模型后端,工具在本机执行的智能体模式。
5
- //
6
- // 可靠性:首字节前的 429/5xx/网络错自动指数退避重试(尊重 Retry-After);流中途长时间无数据由
7
- // 停滞看门狗中止,避免挂死。一旦开始收到增量则不再重试(防止重复输出/重复计费)。
8
-
9
- const RETRYABLE_STATUS = new Set([429, 500, 502, 503, 504]);
10
- const MAX_RETRIES = 2; // 共尝试 1+2 次
11
- const STALL_TIMEOUT_MS = 90_000; // 流中无任何字节超过该时长视为停滞
12
- const REQUEST_TIMEOUT_MS = 15_000; // 非流式小请求超时
13
-
14
- /** 从 Authorization 组装请求头。 */
15
- function authHeaders(apiKey) {
16
- return {
17
- 'Content-Type': 'application/json',
18
- Authorization: `Bearer ${apiKey}`,
19
- };
20
- }
21
-
22
- /** 读取错误响应体并抽出可读信息与 OpenAI error.type(供识别用量窗口超限等终态)。 */
23
- async function readError(res) {
24
- let text = '';
25
- try {
26
- text = await res.text();
27
- } catch {
28
- /* ignore */
29
- }
30
- try {
31
- const j = JSON.parse(text);
32
- const type = j?.error?.type || j?.type || '';
33
- if (j?.error?.message) return { message: j.error.message, type };
34
- if (j?.message) return { message: j.message, type };
35
- if (j?.msg) return { message: j.msg, type };
36
- } catch {
37
- /* not json */
38
- }
39
- return { message: text || `HTTP ${res.status}`, type: '' };
40
- }
41
-
42
- /** 把一批 tool_calls 增量按 index 累加到 acc。 */
43
- function mergeToolCalls(acc, deltas) {
44
- for (const tc of deltas) {
45
- const i = typeof tc.index === 'number' ? tc.index : acc.length;
46
- if (!acc[i]) acc[i] = { id: '', type: 'function', function: { name: '', arguments: '' } };
47
- if (tc.id) acc[i].id = tc.id;
48
- if (tc.type) acc[i].type = tc.type;
49
- if (tc.function?.name) acc[i].function.name += tc.function.name;
50
- if (tc.function?.arguments) acc[i].function.arguments += tc.function.arguments;
51
- }
52
- }
53
-
54
- function sleep(ms, signal) {
55
- return new Promise((resolve, reject) => {
56
- if (signal?.aborted) return reject(abortError());
57
- const t = setTimeout(() => {
58
- signal?.removeEventListener?.('abort', onAbort);
59
- resolve();
60
- }, ms);
61
- const onAbort = () => {
62
- clearTimeout(t);
63
- reject(abortError());
64
- };
65
- signal?.addEventListener?.('abort', onAbort, { once: true });
66
- });
67
- }
68
-
69
- function abortError() {
70
- const e = new Error('已中断');
71
- e.name = 'AbortError';
72
- return e;
73
- }
74
-
75
- /** 由重试次数与 Retry-After 头计算退避时长(毫秒)。 */
76
- function backoffMs(attempt, res) {
77
- const ra = res?.headers?.get?.('retry-after');
78
- const raSec = ra != null ? Number(ra) : NaN;
79
- if (Number.isFinite(raSec) && raSec >= 0) return Math.min(raSec * 1000, 30_000);
80
- return Math.min(500 * 2 ** attempt + Math.floor(Math.random() * 250), 8_000);
81
- }
82
-
83
- /**
84
- * 流式对话补全。逐块解析 SSE:content/reasoning 经 handlers 实时下发,tool_calls 累积后返回。
85
- *
86
- * @param {{baseUrl:string, apiKey:string, model:string, messages:any[], tools?:any[], temperature?:number, stallTimeoutMs?:number}} opts
87
- * @param {{onContent?:(t:string)=>void, onReasoning?:(t:string)=>void, onRetry?:(attempt:number, waitMs:number, reason:string)=>void}} [handlers]
88
- * @param {AbortSignal} [signal]
89
- * @returns {Promise<{content:string, toolCalls:any[], finishReason:string|null, usage:any}>}
90
- */
91
- export async function streamChat(opts, handlers = {}, signal) {
92
- const url = `${opts.baseUrl}/v1/chat/completions`;
93
- const payload = {
94
- model: opts.model,
95
- messages: opts.messages,
96
- stream: true,
97
- stream_options: { include_usage: true },
98
- };
99
- if (opts.tools && opts.tools.length) {
100
- payload.tools = opts.tools;
101
- payload.tool_choice = 'auto';
102
- }
103
- if (typeof opts.temperature === 'number') payload.temperature = opts.temperature;
104
- // 推理强度:OpenAI 兼容字段 reasoning_effort(透传;模型不支持则平台/模型侧忽略)
105
- if (opts.reasoningEffort && opts.reasoningEffort !== 'off') payload.reasoning_effort = opts.reasoningEffort;
106
- // 对话落库审计标识:来源(cli/vscode) + 会话号(平台按此归并同会话多轮,供后台审计)
107
- const chatHeaders = authHeaders(opts.apiKey);
108
- if (opts.client) chatHeaders['X-Huicai-Client'] = opts.client;
109
- if (opts.sessionId) chatHeaders['X-Huicai-Session'] = opts.sessionId;
110
- const body = JSON.stringify(payload);
111
-
112
- // 重试仅发生在「拿到可读响应体之前」:状态可重试或网络错。开始读流后不再重试。
113
- for (let attempt = 0; ; attempt++) {
114
- let res;
115
- try {
116
- res = await fetch(url, { method: 'POST', headers: chatHeaders, body, signal });
117
- } catch (e) {
118
- if (e?.name === 'AbortError' || signal?.aborted) throw abortError();
119
- if (attempt < MAX_RETRIES) {
120
- const wait = backoffMs(attempt, null);
121
- handlers.onRetry?.(attempt + 1, wait, '网络错误:' + (e?.message || e));
122
- await sleep(wait, signal);
123
- continue;
124
- }
125
- throw new Error('网络错误:' + (e?.message || e));
126
- }
127
- if (!res.ok || !res.body) {
128
- const err = await readError(res);
129
- // 用量窗口超限(5小时/7天滚动窗)为终态:不重试,直接抛出清晰提示(含重置时间),避免无谓退避重试
130
- const usageLimited = err.type === 'usage_limit_exceeded';
131
- if (!usageLimited && RETRYABLE_STATUS.has(res.status) && attempt < MAX_RETRIES) {
132
- const wait = backoffMs(attempt, res);
133
- handlers.onRetry?.(attempt + 1, wait, `HTTP ${res.status}:${err.message}`);
134
- await sleep(wait, signal);
135
- continue;
136
- }
137
- throw new Error(err.message);
138
- }
139
- return readStream(res, handlers, signal, opts.stallTimeoutMs ?? STALL_TIMEOUT_MS);
140
- }
141
- }
142
-
143
- /** 读取 SSE 响应体:外部 signal 可中断;停滞看门狗防挂死。 */
144
- async function readStream(res, handlers, signal, stallMs) {
145
- const reader = res.body.getReader();
146
- const decoder = new TextDecoder();
147
- let buffer = '';
148
- let content = '';
149
- const toolCalls = [];
150
- let finishReason = null;
151
- let usage = null;
152
-
153
- let stalled = false;
154
- let stallTimer = null;
155
- const armStall = () => {
156
- if (stallTimer) clearTimeout(stallTimer);
157
- stallTimer = setTimeout(() => {
158
- stalled = true;
159
- reader.cancel().catch(() => {});
160
- }, stallMs);
161
- };
162
- const onAbort = () => reader.cancel().catch(() => {});
163
- signal?.addEventListener?.('abort', onAbort, { once: true });
164
-
165
- try {
166
- armStall();
167
- while (true) {
168
- let chunk;
169
- try {
170
- chunk = await reader.read();
171
- } catch (e) {
172
- if (signal?.aborted) throw abortError();
173
- if (stalled) throw new Error(`响应停滞超过 ${Math.round(stallMs / 1000)}s,已中止(可重试)`);
174
- throw e;
175
- }
176
- if (signal?.aborted) throw abortError();
177
- if (stalled) throw new Error(`响应停滞超过 ${Math.round(stallMs / 1000)}s,已中止(可重试)`);
178
- const { done, value } = chunk;
179
- if (done) break;
180
- armStall();
181
- buffer += decoder.decode(value, { stream: true });
182
- const lines = buffer.split('\n');
183
- buffer = lines.pop() ?? '';
184
- for (const raw of lines) {
185
- const line = raw.trim();
186
- if (!line.startsWith('data:')) continue;
187
- const data = line.slice(5).trim();
188
- if (data === '[DONE]' || data === '') continue;
189
- let json;
190
- try {
191
- json = JSON.parse(data);
192
- } catch {
193
- continue; // 半个 JSON(跨块),忽略;完整帧下轮再来
194
- }
195
- if (json.usage) usage = json.usage;
196
- const choice = json.choices?.[0];
197
- if (!choice) continue;
198
- const delta = choice.delta || {};
199
- const rc = delta.reasoning_content ?? delta.reasoning;
200
- if (rc) handlers.onReasoning?.(rc);
201
- if (delta.content) {
202
- content += delta.content;
203
- handlers.onContent?.(delta.content);
204
- }
205
- if (Array.isArray(delta.tool_calls)) mergeToolCalls(toolCalls, delta.tool_calls);
206
- if (choice.finish_reason) finishReason = choice.finish_reason;
207
- }
208
- }
209
- } finally {
210
- if (stallTimer) clearTimeout(stallTimer);
211
- signal?.removeEventListener?.('abort', onAbort);
212
- }
213
- return { content, toolCalls: toolCalls.filter(Boolean), finishReason, usage };
214
- }
215
-
216
- /** 文本向量化(POST /v1/embeddings,OpenAI 兼容)。返回向量数组(与 input 一一对应);失败抛出。 */
217
- export async function embed(baseUrl, apiKey, model, input) {
218
- const res = await fetch(`${baseUrl}/v1/embeddings`, {
219
- method: 'POST',
220
- headers: authHeaders(apiKey),
221
- body: JSON.stringify({ model, input: Array.isArray(input) ? input : [input] }),
222
- signal: AbortSignal.timeout(30_000),
223
- });
224
- if (!res.ok) throw new Error((await readError(res)).message);
225
- const j = await res.json();
226
- return Array.isArray(j?.data) ? j.data.map((d) => d.embedding) : null;
227
- }
228
-
229
- /**
230
- * 知识库代码增量同步(POST /v1/knowledge/{kbId}/sync)。
231
- * 把一批变更文件(新增/修改/删除)提交给平台按路径 upsert,保鲜 RAG。
232
- * @param {string|number} kbId 知识库ID
233
- * @param {Array<{path:string, content?:string, deleted?:boolean}>} files 变更文件
234
- * @returns {Promise<{accepted:number, skipped:number, truncated:boolean, message:string}>}
235
- */
236
- export async function syncKnowledge(baseUrl, apiKey, kbId, files) {
237
- const res = await fetch(`${baseUrl}/v1/knowledge/${encodeURIComponent(kbId)}/sync`, {
238
- method: 'POST',
239
- headers: authHeaders(apiKey),
240
- body: JSON.stringify({ files }),
241
- signal: AbortSignal.timeout(120_000),
242
- });
243
- if (!res.ok) throw new Error((await readError(res)).message);
244
- return res.json();
245
- }
246
-
247
- /** 列出可用模型(含平台已发布智能体伪模型 agent-/workflow-/datahuman-)。 */
248
- export async function listModels(baseUrl, apiKey) {
249
- const res = await fetch(`${baseUrl}/v1/models`, {
250
- headers: authHeaders(apiKey),
251
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
252
- });
253
- if (!res.ok) throw new Error((await readError(res)).message);
254
- const j = await res.json();
255
- return Array.isArray(j?.data) ? j.data : [];
256
- }
257
-
258
- /**
259
- * 读取客户端相关的平台默认配置(GET /v1/config)。
260
- * @returns {Promise<{vision_bridge_model?:string}>} 失败时返回 {}(客户端自行回退)
261
- */
262
- export async function getPlatformConfig(baseUrl, apiKey) {
263
- try {
264
- const res = await fetch(`${baseUrl}/v1/config`, {
265
- headers: authHeaders(apiKey),
266
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
267
- });
268
- if (!res.ok) return {};
269
- const j = await res.json();
270
- return j && typeof j === 'object' ? j : {};
271
- } catch {
272
- return {};
273
- }
274
- }
275
-
276
- /**
277
- * 拉取某已发布智能体的人格(供 --agent 本机执行模式采用)。
278
- * @param {string} ref 分享令牌,或伪模型ID agent-{token}
279
- * @returns {Promise<{name:string,description:string,systemPrompt:string,model:string,temperature:number,maxTokens:number}>}
280
- */
281
- export async function getAgentPersona(baseUrl, apiKey, ref) {
282
- const res = await fetch(`${baseUrl}/v1/agents/${encodeURIComponent(ref)}/persona`, {
283
- headers: authHeaders(apiKey),
284
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
285
- });
286
- if (!res.ok) throw new Error((await readError(res)).message);
287
- return res.json();
288
- }
289
-
290
- /**
291
- * 查询当前令牌用户的 Token 用量窗口(5小时/7天滚动窗,GET /v1/usage)。
292
- * @returns {Promise<{allowed:boolean,used5h:number,limit5h:number,used7d:number,limit7d:number,resetAtEpochMs?:number|null,message?:string}>}
293
- */
294
- export async function fetchUsage(baseUrl, apiKey) {
295
- const res = await fetch(`${baseUrl}/v1/usage`, {
296
- headers: authHeaders(apiKey),
297
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
298
- });
299
- if (!res.ok) throw new Error((await readError(res)).message);
300
- return res.json();
301
- }
302
-
303
- /**
304
- * 解析文档附件为纯文本(POST /v1/files/extract:PDF/Word/Excel/PPT/RTF 及各类文本,Base64 传输)。
305
- * 服务端与 Web 分享页同源解析(knowledge DocumentParser),扫描件/二进制会得到明确报错。
306
- * 大文件解析较慢,超时放宽到 2 分钟。
307
- * @param {{filename:string, data:string, maxChars?:number}} payload data=文件内容 Base64
308
- * @returns {Promise<{name:string,ext:string,category:string,chars:number,truncated:boolean,pages?:number|null,text:string}>}
309
- */
310
- export async function extractDocument(baseUrl, apiKey, payload) {
311
- const res = await fetch(`${baseUrl}/v1/files/extract`, {
312
- method: 'POST',
313
- headers: authHeaders(apiKey),
314
- body: JSON.stringify(payload),
315
- signal: AbortSignal.timeout(120_000),
316
- });
317
- if (!res.ok) throw new Error((await readError(res)).message);
318
- return res.json();
319
- }
320
-
321
- /**
322
- * 列出平台上我的文档技能(GET /v1/skills,云端技能库;供 huicai skills pull)。
323
- * @returns {Promise<{id:number,name:string,description:string,version:string,updateTime:string}[]>}
324
- */
325
- export async function listPlatformSkills(baseUrl, apiKey) {
326
- const res = await fetch(`${baseUrl}/v1/skills`, {
327
- headers: authHeaders(apiKey),
328
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
329
- });
330
- if (!res.ok) throw new Error((await readError(res)).message);
331
- const j = await res.json();
332
- return Array.isArray(j?.data) ? j.data : [];
333
- }
334
-
335
- /** 平台技能详情(含 SKILL.md 全文与附件,GET /v1/skills/{id})。 */
336
- export async function getPlatformSkill(baseUrl, apiKey, id) {
337
- const res = await fetch(`${baseUrl}/v1/skills/${encodeURIComponent(id)}`, {
338
- headers: authHeaders(apiKey),
339
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
340
- });
341
- if (!res.ok) throw new Error((await readError(res)).message);
342
- return res.json();
343
- }
344
-
345
- /**
346
- * 推送本地技能到平台(POST /v1/skills/import;同名冲突 skip/overwrite)。
347
- * @param {{content:string,filename?:string,conflict?:string,files?:{path:string,content:string}[]}} payload
348
- * @returns {Promise<{id:number,status:'imported'|'overwritten'|'skipped',warnings?:string[]}>}
349
- */
350
- export async function pushPlatformSkill(baseUrl, apiKey, payload) {
351
- const res = await fetch(`${baseUrl}/v1/skills/import`, {
352
- method: 'POST',
353
- headers: { ...authHeaders(apiKey), 'Content-Type': 'application/json' },
354
- body: JSON.stringify(payload),
355
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
356
- });
357
- if (!res.ok) throw new Error((await readError(res)).message);
358
- return res.json();
359
- }
1
+ const t=new Set([429,500,502,503,504]),e=15e3;function n(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t}`}}async function o(t){let e="";try{e=await t.text()}catch{}try{const t=JSON.parse(e),n=t?.error?.type||t?.type||"";if(t?.error?.message)return{message:t.error.message,type:n};if(t?.message)return{message:t.message,type:n};if(t?.msg)return{message:t.msg,type:n}}catch{}return{message:e||`HTTP ${t.status}`,type:""}}function r(t,e){for(const n of e){const e="number"==typeof n.index?n.index:t.length;t[e]||(t[e]={id:"",type:"function",function:{name:"",arguments:""}}),n.id&&(t[e].id=n.id),n.type&&(t[e].type=n.type),n.function?.name&&(t[e].function.name+=n.function.name),n.function?.arguments&&(t[e].function.arguments+=n.function.arguments)}}function a(t,e){return new Promise((n,o)=>{if(e?.aborted)return o(s());const r=setTimeout(()=>{e?.removeEventListener?.("abort",a),n()},t),a=()=>{clearTimeout(r),o(s())};e?.addEventListener?.("abort",a,{once:!0})})}function s(){const t=new Error("已中断");return t.name="AbortError",t}function i(t,e){const n=e?.headers?.get?.("retry-after"),o=null!=n?Number(n):NaN;return Number.isFinite(o)&&o>=0?Math.min(1e3*o,3e4):Math.min(500*2**t+Math.floor(250*Math.random()),8e3)}export async function streamChat(e,r={},l){const u=`${e.baseUrl}/v1/chat/completions`,f={model:e.model,messages:e.messages,stream:!0,stream_options:{include_usage:!0}};e.tools&&e.tools.length&&(f.tools=e.tools,f.tool_choice="auto"),"number"==typeof e.temperature&&(f.temperature=e.temperature),e.reasoningEffort&&"off"!==e.reasoningEffort&&(f.reasoning_effort=e.reasoningEffort);const m=n(e.apiKey);e.client&&(m["X-Huicai-Client"]=e.client),e.sessionId&&(m["X-Huicai-Session"]=e.sessionId);const g=JSON.stringify(f);for(let n=0;;n++){let f;try{f=await fetch(u,{method:"POST",headers:m,body:g,signal:l})}catch(t){if("AbortError"===t?.name||l?.aborted)throw s();if(n<2){const e=i(n,null);r.onRetry?.(n+1,e,"网络错误:"+(t?.message||t)),await a(e,l);continue}throw new Error("网络错误:"+(t?.message||t))}if(!f.ok||!f.body){const e=await o(f);if(!("usage_limit_exceeded"===e.type)&&t.has(f.status)&&n<2){const t=i(n,f);r.onRetry?.(n+1,t,`HTTP ${f.status}:${e.message}`),await a(t,l);continue}throw new Error(e.message)}return c(f,r,l,e.stallTimeoutMs??9e4)}}async function c(t,e,n,o){const a=t.body.getReader(),i=new TextDecoder;let c="",l="";const u=[];let f=null,m=null,g=!1,d=null;const h=()=>{d&&clearTimeout(d),d=setTimeout(()=>{g=!0,a.cancel().catch(()=>{})},o)},w=()=>a.cancel().catch(()=>{});n?.addEventListener?.("abort",w,{once:!0});try{for(h();;){let t;try{t=await a.read()}catch(t){if(n?.aborted)throw s();if(g)throw new Error(`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`);throw t}if(n?.aborted)throw s();if(g)throw new Error(`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`);const{done:d,value:w}=t;if(d)break;h(),c+=i.decode(w,{stream:!0});const y=c.split("\n");c=y.pop()??"";for(const t of y){const n=t.trim();if(!n.startsWith("data:"))continue;const o=n.slice(5).trim();if("[DONE]"===o||""===o)continue;let a;try{a=JSON.parse(o)}catch{continue}a.usage&&(m=a.usage);const s=a.choices?.[0];if(!s)continue;const i=s.delta||{},c=i.reasoning_content??i.reasoning;c&&e.onReasoning?.(c),i.content&&(l+=i.content,e.onContent?.(i.content)),Array.isArray(i.tool_calls)&&r(u,i.tool_calls),s.finish_reason&&(f=s.finish_reason)}}}finally{d&&clearTimeout(d),n?.removeEventListener?.("abort",w)}return{content:l,toolCalls:u.filter(Boolean),finishReason:f,usage:m}}export async function embed(t,e,r,a){const s=await fetch(`${t}/v1/embeddings`,{method:"POST",headers:n(e),body:JSON.stringify({model:r,input:Array.isArray(a)?a:[a]}),signal:AbortSignal.timeout(3e4)});if(!s.ok)throw new Error((await o(s)).message);const i=await s.json();return Array.isArray(i?.data)?i.data.map(t=>t.embedding):null}export async function syncKnowledge(t,e,r,a){const s=await fetch(`${t}/v1/knowledge/${encodeURIComponent(r)}/sync`,{method:"POST",headers:n(e),body:JSON.stringify({files:a}),signal:AbortSignal.timeout(12e4)});if(!s.ok)throw new Error((await o(s)).message);return s.json()}export async function listModels(t,r){const a=await fetch(`${t}/v1/models`,{headers:n(r),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await o(a)).message);const s=await a.json();return Array.isArray(s?.data)?s.data:[]}export async function getPlatformConfig(t,o){try{const r=await fetch(`${t}/v1/config`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!r.ok)return{};const a=await r.json();return a&&"object"==typeof a?a:{}}catch{return{}}}export async function getAgentPersona(t,r,a){const s=await fetch(`${t}/v1/agents/${encodeURIComponent(a)}/persona`,{headers:n(r),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await o(s)).message);return s.json()}export async function fetchUsage(t,r){const a=await fetch(`${t}/v1/usage`,{headers:n(r),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await o(a)).message);return a.json()}export async function extractDocument(t,e,r){const a=await fetch(`${t}/v1/files/extract`,{method:"POST",headers:n(e),body:JSON.stringify(r),signal:AbortSignal.timeout(12e4)});if(!a.ok)throw new Error((await o(a)).message);return a.json()}export async function listPlatformSkills(t,r){const a=await fetch(`${t}/v1/skills`,{headers:n(r),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await o(a)).message);const s=await a.json();return Array.isArray(s?.data)?s.data:[]}export async function getPlatformSkill(t,r,a){const s=await fetch(`${t}/v1/skills/${encodeURIComponent(a)}`,{headers:n(r),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await o(s)).message);return s.json()}export async function pushPlatformSkill(t,r,a){const s=await fetch(`${t}/v1/skills/import`,{method:"POST",headers:{...n(r),"Content-Type":"application/json"},body:JSON.stringify(a),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await o(s)).message);return s.json()}
package/src/autofix.js CHANGED
@@ -1,90 +1 @@
1
- // 自主「修到通过」闭环(/verify 深化):跑验证命令 → 失败则让智能体定位修复 → 自动重跑,直到通过或达上限。
2
- // 这是自动化+智能化的核心:把「跑测试、看报错、改代码、再跑」这个人类循环交给智能体自主完成。
3
-
4
- import { spawn } from 'node:child_process';
5
- import fs from 'node:fs';
6
- import path from 'node:path';
7
-
8
- const CAP = 16000;
9
-
10
- function clip(s) {
11
- s = String(s);
12
- return s.length > CAP ? s.slice(0, CAP) + '\n…(输出过长已截断)' : s;
13
- }
14
-
15
- /** 跑一条命令,返回 {code, output}(合并 stdout/stderr,截断)。超时整组击杀。 */
16
- export function captureCommand(cwd, command, timeout = 180000) {
17
- const isWin = process.platform === 'win32';
18
- return new Promise((resolve) => {
19
- let out = '';
20
- let killed = false;
21
- const child = isWin
22
- ? spawn(process.env.COMSPEC || 'cmd.exe', ['/d', '/s', '/c', command], { cwd, windowsVerbatimArguments: true })
23
- : spawn(process.env.SHELL || '/bin/sh', ['-c', command], { cwd, detached: true });
24
- const timer = setTimeout(() => {
25
- killed = true;
26
- try {
27
- if (!isWin && child.pid) process.kill(-child.pid, 'SIGKILL');
28
- else child.kill('SIGKILL');
29
- } catch { /* ignore */ }
30
- }, timeout);
31
- const append = (d) => { if (out.length < CAP * 2) out += d.toString(); };
32
- child.stdout.on('data', append);
33
- child.stderr.on('data', append);
34
- child.on('error', (e) => { clearTimeout(timer); resolve({ code: -1, output: '命令启动失败:' + e.message }); });
35
- child.on('close', (code) => { clearTimeout(timer); resolve({ code: killed ? 124 : (code == null ? 1 : code), output: clip(out) || '(无输出)' }); });
36
- });
37
- }
38
-
39
- /** 自动探测验证命令(测试/构建)。返回命令字符串或 null。 */
40
- export function detectVerifyCommand(cwd) {
41
- const has = (f) => { try { return fs.existsSync(path.join(cwd, f)); } catch { return false; } };
42
- try {
43
- if (has('package.json')) {
44
- const pkg = JSON.parse(fs.readFileSync(path.join(cwd, 'package.json'), 'utf8'));
45
- if (pkg.scripts?.test && !/no test specified/i.test(pkg.scripts.test)) return 'npm test';
46
- if (pkg.scripts?.build) return 'npm run build';
47
- }
48
- } catch { /* 坏 package.json 忽略 */ }
49
- if (has('pyproject.toml') || has('pytest.ini') || has('tox.ini')) return 'pytest -q';
50
- if (has('pom.xml')) return 'mvn -q test';
51
- if (has('build.gradle') || has('build.gradle.kts')) return './gradlew test';
52
- if (has('Cargo.toml')) return 'cargo test';
53
- if (has('go.mod')) return 'go test ./...';
54
- if (has('Makefile')) return 'make test';
55
- return null;
56
- }
57
-
58
- /**
59
- * 修到通过循环。
60
- * @param {object} p
61
- * @param {string} p.command 验证命令
62
- * @param {number} [p.maxIters] 最多修复轮数(默认 5)
63
- * @param {string} p.cwd
64
- * @param {(prompt:string)=>Promise<{aborted?:boolean}|void>} p.runTurnOnce 跑一个智能体回合(前端提供)
65
- * @param {(text:string)=>void} p.emit 进度输出
66
- * @returns {Promise<{passed:boolean, iters?:number, output?:string, aborted?:boolean}>}
67
- */
68
- export async function runVerifyLoop({ command, maxIters = 5, cwd, runTurnOnce, emit }) {
69
- for (let i = 0; i < maxIters; i++) {
70
- emit(`▶ 验证 ${i + 1}/${maxIters}:${command}`);
71
- const { code, output } = await captureCommand(cwd, command);
72
- if (code === 0) {
73
- emit(`✅ 验证通过(第 ${i + 1} 轮)。`);
74
- return { passed: true, iters: i + 1 };
75
- }
76
- emit(`✗ 未通过(退出码 ${code})` + (i < maxIters - 1 ? ',让智能体定位修复后自动重跑…' : ''));
77
- if (i === maxIters - 1) {
78
- emit(`⚠ 已达上限 ${maxIters} 轮仍未通过。`);
79
- return { passed: false, iters: i + 1, output };
80
- }
81
- const r = await runTurnOnce(
82
- `自动验证:运行 \`${command}\` 失败(退出码 ${code})。输出如下:\n\n${output}\n\n`
83
- + '请定位根因并**修复代码/配置**。不要自己重跑该命令——我会自动重跑验证。本轮只做必要的修复。');
84
- if (r && r.aborted) {
85
- emit('已中断修复循环。');
86
- return { passed: false, aborted: true, iters: i + 1 };
87
- }
88
- }
89
- return { passed: false };
90
- }
1
+ import{spawn as t}from"node:child_process";import e from"node:fs";import r from"node:path";const o=16e3;export function captureCommand(e,r,n=18e4){const s="win32"===process.platform;return new Promise(i=>{let c="",a=!1;const d=s?t(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",r],{cwd:e,windowsVerbatimArguments:!0}):t(process.env.SHELL||"/bin/sh",["-c",r],{cwd:e,detached:!0}),p=setTimeout(()=>{a=!0;try{!s&&d.pid?process.kill(-d.pid,"SIGKILL"):d.kill("SIGKILL")}catch{}},n),u=t=>{c.length<32e3&&(c+=t.toString())};d.stdout.on("data",u),d.stderr.on("data",u),d.on("error",t=>{clearTimeout(p),i({code:-1,output:"命令启动失败:"+t.message})}),d.on("close",t=>{var e;clearTimeout(p),i({code:a?124:null==t?1:t,output:(e=c,((e=String(e)).length>o?e.slice(0,o)+"\n…(输出过长已截断)":e)||"(无输出)")})})})}export function detectVerifyCommand(t){const o=o=>{try{return e.existsSync(r.join(t,o))}catch{return!1}};try{if(o("package.json")){const o=JSON.parse(e.readFileSync(r.join(t,"package.json"),"utf8"));if(o.scripts?.test&&!/no test specified/i.test(o.scripts.test))return"npm test";if(o.scripts?.build)return"npm run build"}}catch{}return o("pyproject.toml")||o("pytest.ini")||o("tox.ini")?"pytest -q":o("pom.xml")?"mvn -q test":o("build.gradle")||o("build.gradle.kts")?"./gradlew test":o("Cargo.toml")?"cargo test":o("go.mod")?"go test ./...":o("Makefile")?"make test":null}export async function runVerifyLoop({command:t,maxIters:e=5,cwd:r,runTurnOnce:o,emit:n}){for(let s=0;s<e;s++){n(`▶ 验证 ${s+1}/${e}:${t}`);const{code:i,output:c}=await captureCommand(r,t);if(0===i)return n(`✅ 验证通过(第 ${s+1} 轮)。`),{passed:!0,iters:s+1};if(n(`✗ 未通过(退出码 ${i})`+(s<e-1?",让智能体定位修复后自动重跑…":"")),s===e-1)return n(`⚠ 已达上限 ${e} 轮仍未通过。`),{passed:!1,iters:s+1,output:c};const a=await o(`自动验证:运行 \`${t}\` 失败(退出码 ${i})。输出如下:\n\n${c}\n\n请定位根因并**修复代码/配置**。不要自己重跑该命令——我会自动重跑验证。本轮只做必要的修复。`);if(a&&a.aborted)return n("已中断修复循环。"),{passed:!1,aborted:!0,iters:s+1}}return{passed:!1}}
package/src/banner.js CHANGED
@@ -1,83 +1 @@
1
- // 启动欢迎横幅:渐变 ASCII 字标 + 上下文信息 + 快捷提示(对标 Claude CLI 进入时的欢迎屏)。
2
- // 零依赖;真彩(COLORTERM=truecolor)时走 24-bit 青→紫渐变,仅 16 色时退化为青色,NO_COLOR/非 TTY 输出朴素字标。
3
-
4
- import { c, colorEnabled } from './colors.js';
5
-
6
- // 5 行块字(每字形 5 列宽,字间 1 空格)。
7
- const FONT = {
8
- H: ['█ █', '█ █', '█████', '█ █', '█ █'],
9
- U: ['█ █', '█ █', '█ █', '█ █', '█████'],
10
- I: ['█████', ' █ ', ' █ ', ' █ ', '█████'],
11
- C: ['█████', '█ ', '█ ', '█ ', '█████'],
12
- A: ['█████', '█ █', '█████', '█ █', '█ █'],
13
- ' ': [' ', ' ', ' ', ' ', ' '],
14
- };
15
-
16
- function wordmark(word) {
17
- const rows = ['', '', '', '', ''];
18
- [...word].forEach((ch, idx) => {
19
- const g = FONT[ch] || FONT[' '];
20
- for (let r = 0; r < 5; r++) rows[r] += (idx ? ' ' : '') + g[r];
21
- });
22
- return rows;
23
- }
24
-
25
- const truecolor = colorEnabled && /truecolor|24bit/i.test(process.env.COLORTERM || '');
26
- const lerp = (a, b, t) => Math.round(a + (b - a) * t);
27
-
28
- // 按列做水平渐变;无真彩退化为青色;无色原样返回。
29
- function gradientRow(row, from, to) {
30
- if (!colorEnabled) return row;
31
- const chars = [...row];
32
- const n = chars.length;
33
- let out = '';
34
- for (let i = 0; i < n; i++) {
35
- const ch = chars[i];
36
- if (ch === ' ') { out += ' '; continue; }
37
- if (!truecolor) { out += `\x1b[36m${ch}`; continue; }
38
- const t = n <= 1 ? 0 : i / (n - 1);
39
- out += `\x1b[38;2;${lerp(from[0], to[0], t)};${lerp(from[1], to[1], t)};${lerp(from[2], to[2], t)}m${ch}`;
40
- }
41
- return out + '\x1b[0m';
42
- }
43
-
44
- const PAD = ' ';
45
- const FROM = [0, 229, 255]; // 青
46
- const TO = [168, 85, 247]; // 紫
47
-
48
- /**
49
- * 生成欢迎横幅字符串(不含结尾换行)。
50
- * @param {object} p
51
- * @param {string} p.model 当前模型 / 平台目标
52
- * @param {string} p.cwd 工作目录
53
- * @param {string} [p.version] 版本展示串
54
- * @param {boolean} [p.platform] 是否平台伪模型(agent-/workflow-/datahuman-)
55
- * @param {boolean} [p.yolo] 是否已开自动放行
56
- * @param {string} [p.consult] 混合模式咨询目标展示名
57
- * @param {boolean} [p.ink] 是否 ink 富终端(附注一行)
58
- */
59
- export function renderWelcome({ model, cwd, version, platform, yolo, consult, ink } = {}) {
60
- const cols = process.stdout.columns || 60;
61
- const sep = c.gray('─'.repeat(Math.min(Math.max(cols - 2, 24), 50)));
62
- const L = [''];
63
-
64
- if (colorEnabled) {
65
- for (const row of wordmark('HUICAI')) L.push(PAD + gradientRow(row, FROM, TO));
66
- } else {
67
- L.push(PAD + 'H U I C A I');
68
- }
69
-
70
- L.push('');
71
- L.push(PAD + c.bold('HuiCai AI') + c.gray(' · 终端智能体') + (version ? c.gray(' v' + version) : '')
72
- + (ink ? c.gray(' · ink') : ''));
73
- L.push(PAD + sep);
74
- L.push(PAD + c.gray(platform ? '目标 ' : '模型 ') + c.cyan(model || c.gray('(未设置,/model 设置)'))
75
- + (platform ? c.gray(' · 平台编排(无本机工具)') : ''));
76
- L.push(PAD + c.gray('目录 ') + cwd);
77
- if (consult) L.push(PAD + c.gray('咨询 ') + c.magenta(consult) + c.gray(' · 混合模式(本机工具 + 平台咨询)'));
78
- if (yolo) L.push(PAD + c.yellow('⚠ 自动放行(yolo) 已开:写文件/执行命令不再逐次确认'));
79
- L.push('');
80
- L.push(PAD + c.magenta('✦ ') + c.gray('/help 命令 · @文件 引用 · !命令 直通 shell · Ctrl+C 中断'));
81
-
82
- return L.join('\n');
83
- }
1
+ import{c as o,colorEnabled as t}from"./colors.js";const n={H:["█ █","█ █","█████","█ █","█ █"],U:["█ █","█ █","█ █","█ █","█████"],I:["█████"," █ "," █ "," █ ","█████"],C:["█████","█ ","█ ","█ ","█████"],A:["█████","█ █","█████","█ █","█ █"]," ":[" "," "," "," "," "]};const r=t&&/truecolor|24bit/i.test(process.env.COLORTERM||""),s=(o,t,n)=>Math.round(o+(t-o)*n);function e(o,n,e){if(!t)return o;const u=[...o],c=u.length;let a="";for(let o=0;o<c;o++){const t=u[o];if(" "===t){a+=" ";continue}if(!r){a+=`${t}`;continue}const l=c<=1?0:o/(c-1);a+=`[38;2;${s(n[0],e[0],l)};${s(n[1],e[1],l)};${s(n[2],e[2],l)}m${t}`}return a+""}const u=" ",c=[0,229,255],a=[168,85,247];export function renderWelcome({model:r,cwd:s,version:l,platform:i,yolo:h,consult:p,ink:y}={}){const f=process.stdout.columns||60,g=o.gray("─".repeat(Math.min(Math.max(f-2,24),50))),m=[""];if(t)for(const o of function(o){const t=["","","","",""];return[...o].forEach((o,r)=>{const s=n[o]||n[" "];for(let o=0;o<5;o++)t[o]+=(r?" ":"")+s[o]}),t}("HUICAI"))m.push(u+e(o,c,a));else m.push(" H U I C A I");return m.push(""),m.push(u+o.bold("HuiCai AI")+o.gray(" · 终端智能体")+(l?o.gray(" v"+l):"")+(y?o.gray(" · ink"):"")),m.push(u+g),m.push(u+o.gray(i?"目标 ":"模型 ")+o.cyan(r||o.gray("(未设置,/model 设置)"))+(i?o.gray(" · 平台编排(无本机工具)"):"")),m.push(u+o.gray("目录 ")+s),p&&m.push(u+o.gray("咨询 ")+o.magenta(p)+o.gray(" · 混合模式(本机工具 + 平台咨询)")),h&&m.push(u+o.yellow("⚠ 自动放行(yolo) 已开:写文件/执行命令不再逐次确认")),m.push(""),m.push(u+o.magenta("✦ ")+o.gray("/help 命令 · @文件 引用 · !命令 直通 shell · Ctrl+C 中断")),m.join("\n")}
@@ -1,99 +1 @@
1
- // 会话检查点(对标 Claude Code /rewind):每个用户回合前快照「对话位置 + 该回合将改动文件的原始内容」,
2
- // 可把对话/文件/两者回滚到某个检查点之前。文件原文由 runTurn 经 fileJournal 在写入前记录(见 agent.js)。
3
- //
4
- // 检查点存活于当前运行的会话(内存),不落盘(避免把大段文件内容写进会话 JSON)。
5
- // 语义:检查点 N 记录「第 N 个用户回合开始前」的状态;回滚到 N 即回到该回合之前。
6
-
7
- import fs from 'node:fs';
8
- import path from 'node:path';
9
-
10
- const MAX_CHECKPOINTS = 100;
11
-
12
- function safeClone(o) {
13
- try {
14
- return o ? JSON.parse(JSON.stringify(o)) : {};
15
- } catch {
16
- return {};
17
- }
18
- }
19
-
20
- function deriveLabel(text) {
21
- const first = String(text || '').replace(/\s+/g, ' ').trim();
22
- return first ? (first.length > 60 ? first.slice(0, 60) + '…' : first) : '(空)';
23
- }
24
-
25
- export function createCheckpoints() {
26
- const list = []; // {id, time, label, messagesLen, compaction, journal:[{path,before,existed,tooLarge?}]}
27
- let seq = 0;
28
-
29
- /** 回合开始前调用:记录对话位置,返回一个 journal 数组交给 runTurn 累积本回合的文件原文。 */
30
- function snapshot({ session, label }) {
31
- const cp = {
32
- id: ++seq,
33
- time: Date.now(),
34
- label: deriveLabel(label),
35
- messagesLen: session.messages.length,
36
- compaction: safeClone(session.compaction),
37
- journal: [],
38
- };
39
- list.push(cp);
40
- // 上限保护:超出则丢弃最早的(其对应文件原文一并释放)
41
- if (list.length > MAX_CHECKPOINTS) list.splice(0, list.length - MAX_CHECKPOINTS);
42
- return cp.journal;
43
- }
44
-
45
- /** 从某检查点(含)起、各文件最早一次记录的原始内容(用于回滚到该点之前的文件状态)。 */
46
- function changedFilesSince(pos) {
47
- const map = new Map(); // relPath -> journal entry(最早)
48
- for (let i = pos; i < list.length; i++) {
49
- for (const e of list[i].journal) {
50
- if (e && e.path && !map.has(e.path)) map.set(e.path, e);
51
- }
52
- }
53
- return map;
54
- }
55
-
56
- /**
57
- * 回滚到检查点 id。
58
- * @param {number} id 检查点编号
59
- * @param {'chat'|'code'|'both'} mode
60
- * @param {{session:object, cwd:string}} p
61
- */
62
- function restore(id, mode, { session, cwd }) {
63
- const pos = list.findIndex((c) => c.id === id);
64
- if (pos < 0) return { ok: false, error: `检查点 ${id} 不存在(/rewind 查看可用检查点)` };
65
- const cp = list[pos];
66
- const summary = { ok: true, id, mode, conversation: false, filesRestored: [], filesDeleted: [], filesFailed: [], filesSkipped: [] };
67
-
68
- if (mode === 'code' || mode === 'both') {
69
- for (const [relPath, e] of changedFilesSince(pos)) {
70
- if (e.tooLarge) { summary.filesSkipped.push(relPath); continue; }
71
- const abs = path.resolve(cwd, relPath);
72
- try {
73
- if (!e.existed) {
74
- fs.rmSync(abs, { force: true });
75
- summary.filesDeleted.push(relPath);
76
- } else {
77
- fs.mkdirSync(path.dirname(abs), { recursive: true });
78
- fs.writeFileSync(abs, e.before, 'utf8');
79
- summary.filesRestored.push(relPath);
80
- }
81
- } catch (err) {
82
- summary.filesFailed.push(`${relPath}:${err.message}`);
83
- }
84
- }
85
- }
86
-
87
- if (mode === 'chat' || mode === 'both') {
88
- if (cp.messagesLen <= session.messages.length) session.messages.length = cp.messagesLen;
89
- session.compaction = safeClone(cp.compaction);
90
- session.lastPromptTokens = 0;
91
- summary.conversation = true;
92
- // 对话被截断 → 该点及之后的检查点失效
93
- list.length = pos;
94
- }
95
- return summary;
96
- }
97
-
98
- return { list, snapshot, restore, changedFilesSince };
99
- }
1
+ import e from"node:fs";import t from"node:path";function n(e){try{return e?JSON.parse(JSON.stringify(e)):{}}catch{return{}}}function s(e){const t=String(e||"").replace(/\s+/g," ").trim();return t?t.length>60?t.slice(0,60)+"…":t:"(空)"}export function createCheckpoints(){const o=[];let r=0;function i(e){const t=new Map;for(let n=e;n<o.length;n++)for(const e of o[n].journal)e&&e.path&&!t.has(e.path)&&t.set(e.path,e);return t}return{list:o,snapshot:function({session:e,label:t}){const i={id:++r,time:Date.now(),label:s(t),messagesLen:e.messages.length,compaction:n(e.compaction),journal:[]};return o.push(i),o.length>100&&o.splice(0,o.length-100),i.journal},restore:function(s,r,{session:c,cwd:a}){const l=o.findIndex(e=>e.id===s);if(l<0)return{ok:!1,error:`检查点 ${s} 不存在(/rewind 查看可用检查点)`};const f=o[l],p={ok:!0,id:s,mode:r,conversation:!1,filesRestored:[],filesDeleted:[],filesFailed:[],filesSkipped:[]};if("code"===r||"both"===r)for(const[n,s]of i(l)){if(s.tooLarge){p.filesSkipped.push(n);continue}const o=t.resolve(a,n);try{s.existed?(e.mkdirSync(t.dirname(o),{recursive:!0}),e.writeFileSync(o,s.before,"utf8"),p.filesRestored.push(n)):(e.rmSync(o,{force:!0}),p.filesDeleted.push(n))}catch(e){p.filesFailed.push(`${n}:${e.message}`)}}return"chat"!==r&&"both"!==r||(f.messagesLen<=c.messages.length&&(c.messages.length=f.messagesLen),c.compaction=n(f.compaction),c.lastPromptTokens=0,p.conversation=!0,o.length=l),p},changedFilesSince:i}}