@huicai/cli 2026.712.80880 → 2026.714.0
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 +2 -2
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/config.js +1 -1
- package/src/device.js +1 -1
- package/src/executors.js +1 -1
- package/src/input.js +1 -1
- package/src/session.js +1 -1
- package/src/tools.js +1 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huicai/cli",
|
|
3
|
-
"version": "2026.
|
|
4
|
-
"versionDisplay": "2026.07.
|
|
3
|
+
"version": "2026.714.0",
|
|
4
|
+
"versionDisplay": "2026.07.14",
|
|
5
5
|
"description": "在终端运行的智能体 CLI:以 HuiCai_AI 平台的 OpenAI 兼容 /v1 为模型后端,在用户本机执行文件/命令工具。",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": {
|
package/src/agent.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import o from"node:path";import{streamChat as e}from"./api.js";import{MarkupFilter as n,stripMarkup as r}from"./markup-filter.js";import{TOOL_SPECS as s,READONLY_TOOLS as a,runTool as l}from"./tools.js";import{toolLabel as i,indent as c,renderFileDiff as u}from"./render.js";import{compactIfNeeded as m,buildModelMessages as
|
|
1
|
+
import t from"node:fs";import o from"node:path";import{streamChat as e}from"./api.js";import{MarkupFilter as n,stripMarkup as r}from"./markup-filter.js";import{TOOL_SPECS as s,READONLY_TOOLS as a,runTool as l}from"./tools.js";import{toolLabel as i,indent as c,renderFileDiff as u}from"./render.js";import{compactIfNeeded as m,buildModelMessages as p}from"./compaction.js";import{makeTodoTool as g,formatTodos as f,TODO_TOOL_NAME as d}from"./todo.js";import{makeMemoryTool as h}from"./memory.js";import{loadHooks as y,runHooks as w}from"./hooks.js";import*as S from"./ide.js";import{c as _}from"./colors.js";function T(t){const o=String(t);return o.startsWith("工具执行失败")||o.startsWith("命令启动失败")||o.includes("不是合法 JSON")||o.includes("未找到要替换的内容")||o.includes("路径超出项目目录")||o.includes("是目录而非文件")}function k(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}function x(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function b(t,o,e){const n=new Array(t.length);let r=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(o,t.length))},async()=>{for(;r<t.length;){const o=r++;n[o]=await e(t[o],o)}})),n}export function isPlatformModel(t){return/^(agent|workflow|datahuman|squad)-/.test(String(t||""))}function P(t,o=200){const e=String(t).split("\n")[0];return e.length>o?e.slice(0,o)+"…":e}export function capToolResult(t){const o=String(t);if(o.length<=2e4)return o;const e=o.length-16e3-3e3;return o.slice(0,16e3)+`\n…【工具输出过长,中间已省略 ${e} 字符】…\n`+o.slice(-3e3)}export async function runTurn({config:$,messages:j,permissions:M,cwd:E,ask:I,signal:U,extraTools:v=[],depth:A=0,io:O,events:R,todoState:C,compaction:N,pullQueued:D,maxIters:J,fileJournal:q}){const F=O&&O.write||(t=>process.stdout.write(t)),K=R||null,W=t=>{K?K.onInfo?.(t):F(_.gray(" · "+t)+"\n")};let L=0,Q=0,z=0;const B=Number(J||$.maxTurns)||120,G=isPlatformModel($.model);let H=$.model;if(0===A&&$.autoModelStrong&&!G){const t=[...j].reverse().find(t=>"user"===t.role),o=x(t?.content);($.planMode||o.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(o))&&$.autoModelStrong!==$.model&&(H=$.autoModelStrong,$.debug&&W(`[automodel] 复杂回合升配 → ${H}`))}const V=N||{},X=[...v];if(0===A&&!G&&!X.some(t=>t.spec?.function?.name===d)){const t=C||{list:[]};X.push(g({state:t,emit:t=>{K?K.onTodo?K.onTodo(t.map(t=>({...t}))):K.onInfo?.("任务清单:\n"+f(t)):F(_.gray(c(f(t)," "))+"\n")}}))}0!==A||G||!1===$.memoryEnabled||X.some(t=>"memory_write"===t.spec?.function?.name)||X.push(h({cwd:E,emit:t=>{K?K.onMemory?K.onMemory(t):K.onInfo?.("已记入长期记忆:"+P(t,80)):F(_.gray(" ✎ 已记入长期记忆:"+P(t,80))+"\n")}}));const Y=new Map;for(const t of X)Y.set(t.spec.function.name,t);const Z=G?void 0:[...s,...X.map(t=>t.spec)];let tt=0,ot=!1,et=0,nt=null,rt=0,st="";const at=0!==A||G?null:y(E),lt=!(!at||!at.all.length);if(lt){const t=[];if(j.filter(t=>"user"===t.role).length<=1){const o=w("SessionStart",{cwd:E},at);o.injectContext&&t.push(o.injectContext);for(const e of o.outputs)t.push(e.stdout)}const o=[...j].reverse().find(t=>"user"===t.role),e=w("UserPromptSubmit",{cwd:E,prompt:x(o?.content)},at);if(e.block)return W("提交被 UserPromptSubmit 钩子阻断:"+e.reason),{error:"提交被钩子阻断:"+e.reason,usage:{prompt:L,completion:Q},lastPromptTokens:z};e.injectContext&&t.push(e.injectContext);for(const o of e.outputs)t.push(o.stdout);t.length&&(nt="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let s=0;s<B;s++){const c=s===B-1;!c||G||ot||nt||(nt="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const g=await m(j,$,{state:V,lastPromptTokens:z});g>0&&W(`已压缩上下文(${g} 条较早消息已摘要,页面与会话记录不受影响)`);let f=!1,d=!1;const h=new n,y=t=>{t&&(f||!d||K||F("\n"),f=!0,K?K.onDelta?.(t):F(t))},x=p(j,V);let I=x;if(!G&&x.length&&"system"===x[0]?.role){let t="";$.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),$.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),$.goal&&String($.goal).trim()&&(t+=k($.goal)),t&&(I=[{...x[0],content:String(x[0].content||"")+t},...x.slice(1)])}const v=[];st?v.push({role:"assistant",content:st},{role:"user",content:"你上一条回复因达到单轮输出长度上限而被截断,用户尚未看到剩余部分。请从截断处直接继续输出剩余内容:无缝衔接上文(若截断处在句子、代码块或表格中间,从中断字符处接着写),不要重复任何已输出的内容,不要道歉或解释,不要重新开头或总结前文。"}):nt&&v.push({role:"user",content:nt});const A=Date.now();let O;try{O=await e({baseUrl:$.baseUrl,apiKey:$.apiKey,model:H,messages:v.length?[...I,...v]:I,tools:ot||c?void 0:Z,maxTokens:$.maxTokens,reasoningEffort:$.reasoningEffort,client:$.client,sessionId:$.sessionId},{onContent:t=>y(h.feed(t)),onReasoning:t=>{!1!==$.showReasoning&&(K?K.onReasoning?.(t):(d||F(_.gray("\n[思考] ")),F(_.gray(t))),d=!0)},onRetry:(t,o,e)=>W(`模型响应异常,${Math.round(o/1e3)}s 后自动重试(第 ${t} 次):${e}`)},U)}catch(t){return"AbortError"===t.name?(K?K.onInfo?.("已中断"):F(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:L,completion:Q},lastPromptTokens:z}):(K?K.onInfo?.("✗ 模型调用失败:"+t.message):F("\n"+_.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,usage:{prompt:L,completion:Q},lastPromptTokens:z})}y(h.flush()),O.usage&&(L+=O.usage.prompt_tokens||0,Q+=O.usage.completion_tokens||0,z=O.usage.prompt_tokens||z),$.debug&&W(`[debug] 轮${s+1} · ${$.model} · in ${O.usage?.prompt_tokens||0} / out ${O.usage?.completion_tokens||0} tok · ${Date.now()-A}ms · ${(O.toolCalls||[]).length} 工具调用`);const{text:R,sawMarkup:C}=r(O.content),N=C||h.sawMarkup;nt=null;const J=G?[]:O.toolCalls;if(!J.length){if(N&&!R.trim()&&!G){if(tt<2){tt++,2===tt&&(ot=!0),nt=1===tt?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",W(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${tt} 次)`);continue}W("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}if(!G&&"length"===O.finishReason&&R&&rt<3){rt++,st+=R,W(`输出达单轮长度上限,自动续写中(第 ${rt} 段)`);continue}f&&!K&&F("\n");const t=st?st+R:R;st="",t&&j.push({role:"assistant",content:t}),K&&K.onTurnEnd?.(t);const o=D?.();if(o){j.push({role:"user",content:o});continue}if(lt){const o=w("Stop",{cwd:E,result:t},at);for(const t of o.outputs)W("钩子·Stop:"+P(t.stdout,160))}return{usage:{prompt:L,completion:Q},lastPromptTokens:z}}f&&!K&&F("\n"),K&&R&&K.onAssistantText?.(R),j.push({role:"assistant",content:st?st+R:R,tool_calls:J}),st="";const X=J.map((t,o)=>{const e=t.function?.name||"";let n={},r=null;const s=t.function?.arguments||"";if(s)try{n=JSON.parse(s)}catch(t){r=`工具参数不是合法 JSON(${t.message}):${String(s).slice(0,200)}…请修正参数后重新调用。`}const l=Y.get(e),i=S.isActive()&&("write_file"===e||"edit_file"===e);return{tc:t,idx:o,name:e,args:n,argsError:r,extra:l,readOnly:l?!!l.readOnly:a.has(e),deferToIde:i}}),it=t=>{for(let o=t;o<X.length;o++)j.push({role:"tool",tool_call_id:X[o].tc.id||`call_${o}`,content:"(用户中断,该工具未执行)"});return K?K.onInfo?.("已中断"):F(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:L,completion:Q},lastPromptTokens:z}},ct=async t=>{if(t.argsError)return t.argsError;let o;try{o=t.extra?await t.extra.run(t.args):await l(t.name,t.args,E,$)}catch(t){o=`工具执行失败:${t.message}`}return capToolResult(String(o))},ut=t=>{K?K.onToolStart?.(t.name,t.args):F(_.cyan(" ⏺ ")+i(t.name,t.args)+"\n")},mt=(t,o)=>{T(o)&&et++,K?K.onToolResult?.(t.name,String(o)):F(_.gray(" "+P(o))+"\n"),j.push({role:"tool",tool_call_id:t.tc.id,content:String(o)})},pt=(t,o)=>{K?K.onSkip?.(t.name,t.args):F(_.gray(` ⊘ ${i(t.name,t.args)} — 已跳过\n`)),j.push({role:"tool",tool_call_id:t.tc.id,content:o||"未授权,未执行。"})},gt=t=>"edit_file"===t||"write_file"===t,ft=o=>{try{return t.readFileSync(o,"utf8")}catch{return""}},dt=(t,e)=>{const n=o.resolve(E,String(t.args.path)),r=u(e,ft(n),{path:String(t.args.path)});r&&(K?K.onFileDiff?.(r):F(r+"\n"))};let ht=0;for(;ht<X.length;){if(U?.aborted)return it(ht);const e=X[ht];if(e.argsError){ut(e),mt(e,e.argsError),ht++;continue}if(e.readOnly){let t=ht;for(;t<X.length&&X[t].readOnly&&!X[t].argsError;)t++;const o=X.slice(ht,t),e=await b(o,8,async t=>{const o=await M.request(t.name,t.args,E,!0);return o.allow?(ut(t),{item:t,result:await ct(t)}):{item:t,denied:o.reason||"未授权,未执行。"}});for(const t of e)null!=t.denied?pt(t.item,t.denied):mt(t.item,t.result);if(ht=t,U?.aborted)return it(ht);continue}const n=await M.request(e.name,e.args,E,e.readOnly||e.deferToIde);if(U?.aborted)return it(ht);if(!n.allow){pt(e,n.reason),ht++;continue}if(lt){const t=w("PreToolUse",{cwd:E,toolName:e.name,args:e.args},at);for(const o of t.outputs)W("钩子·PreToolUse:"+P(o.stdout,120));if(t.block){pt(e,"PreToolUse 钩子阻断:"+t.reason),ht++;continue}t.modifiedArgs&&(e.args={...e.args,...t.modifiedArgs},W("钩子·PreToolUse 已修改工具参数"))}ut(e);const r=gt(e.name)&&e.args?.path?ft(o.resolve(E,String(e.args.path))):null;if(q&&null!=r&&e.args?.path){const n=t.existsSync(o.resolve(E,String(e.args.path)));q.push(r.length>2097152?{path:String(e.args.path),tooLarge:!0}:{path:String(e.args.path),before:r,existed:n})}let s=await ct(e);if(lt){const t=w("PostToolUse",{cwd:E,toolName:e.name,args:e.args,result:String(s)},at);if(t.outputs.length){for(const o of t.outputs)W("钩子·PostToolUse:"+P(o.stdout,120));s=String(s)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}mt(e,s),null!=r&&dt(e,r),ht++}et>=3&&!ot&&!nt&&(nt="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",et=0,W("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const yt=D?.();yt&&j.push({role:"user",content:yt})}return st&&(j.push({role:"assistant",content:st}),K&&K.onTurnEnd?.(st)),K?K.onInfo?.("已达最大工具轮数,停止"):F(_.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:L,completion:Q},lastPromptTokens:z}}
|
package/src/api.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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()}
|
|
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),Number.isFinite(e.maxTokens)&&e.maxTokens>0&&(f.max_tokens=Math.floor(e.maxTokens)),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/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import o from"node:path";import r from"node:os";export const CONFIG_DIR=o.join(r.homedir(),".huicai");export const CONFIG_PATH=o.join(CONFIG_DIR,"config.json");export const DEFAULT_BASE_URL="https://huicai-ai.sjht.com";export const DEFAULT_MODEL="deepseek-v4-flash";function n(){try{return JSON.parse(e.readFileSync(CONFIG_PATH,"utf8"))}catch{return{}}}export function loadConfig(){const e=n(),o={baseUrl:(process.env.HUICAI_BASE_URL||e.baseUrl||DEFAULT_BASE_URL).trim(),apiKey:(process.env.HUICAI_API_KEY||e.apiKey||"").trim(),model:(process.env.HUICAI_MODEL||e.model||DEFAULT_MODEL).trim(),maxTurns:Number(process.env.HUICAI_MAX_TURNS||e.maxTurns)||0,autoCompactTokens:Number(e.autoCompactTokens)||0,reasoningEffort:(process.env.HUICAI_EFFORT||e.reasoningEffort||"").trim(),compactModel:(process.env.HUICAI_COMPACT_MODEL||e.compactModel||"").trim(),memoryEnabled:"0"!==process.env.HUICAI_MEMORY&&!1!==e.memory,showReasoning:"0"!==process.env.HUICAI_REASONING&&!1!==e.showReasoning,markdown:"0"!==process.env.HUICAI_MARKDOWN&&!1!==e.markdown,client:(process.env.HUICAI_CLIENT||"cli").trim().toLowerCase(),theme:(process.env.HUICAI_THEME||e.theme||"default").trim(),accentColor:(e.accentColor||"cyan").trim(),tui:(process.env.HUICAI_TUI||e.tui||"auto").trim(),debug:"1"===process.env.HUICAI_DEBUG||!0===e.debug,statusLine:(e.statusLine||"").trim(),advisorModel:(process.env.HUICAI_ADVISOR||e.advisorModel||"").trim(),autoModelStrong:(process.env.HUICAI_AUTOMODEL||e.autoModelStrong||"").trim(),budgetTokens:Number(e.budgetTokens)||0,embeddingModel:(process.env.HUICAI_EMBED_MODEL||e.embeddingModel||"").trim()};return o.baseUrl=o.baseUrl.replace(/\/+$/,""),o}export function saveConfig(o){const r={...n(),...o};e.mkdirSync(CONFIG_DIR,{recursive:!0}),e.writeFileSync(CONFIG_PATH,JSON.stringify(r,null,2)+"\n",{mode:384});try{e.chmodSync(CONFIG_PATH,384)}catch{}return r}
|
|
1
|
+
import e from"node:fs";import o from"node:path";import r from"node:os";export const CONFIG_DIR=o.join(r.homedir(),".huicai");export const CONFIG_PATH=o.join(CONFIG_DIR,"config.json");export const DEFAULT_BASE_URL="https://huicai-ai.sjht.com";export const DEFAULT_MODEL="deepseek-v4-flash";function n(){try{return JSON.parse(e.readFileSync(CONFIG_PATH,"utf8"))}catch{return{}}}export function loadConfig(){const e=n(),o={baseUrl:(process.env.HUICAI_BASE_URL||e.baseUrl||DEFAULT_BASE_URL).trim(),apiKey:(process.env.HUICAI_API_KEY||e.apiKey||"").trim(),model:(process.env.HUICAI_MODEL||e.model||DEFAULT_MODEL).trim(),maxTurns:Number(process.env.HUICAI_MAX_TURNS||e.maxTurns)||0,maxTokens:Number(process.env.HUICAI_MAX_TOKENS||e.maxTokens)||0,autoCompactTokens:Number(e.autoCompactTokens)||0,reasoningEffort:(process.env.HUICAI_EFFORT||e.reasoningEffort||"").trim(),compactModel:(process.env.HUICAI_COMPACT_MODEL||e.compactModel||"").trim(),memoryEnabled:"0"!==process.env.HUICAI_MEMORY&&!1!==e.memory,showReasoning:"0"!==process.env.HUICAI_REASONING&&!1!==e.showReasoning,markdown:"0"!==process.env.HUICAI_MARKDOWN&&!1!==e.markdown,client:(process.env.HUICAI_CLIENT||"cli").trim().toLowerCase(),theme:(process.env.HUICAI_THEME||e.theme||"default").trim(),accentColor:(e.accentColor||"cyan").trim(),tui:(process.env.HUICAI_TUI||e.tui||"auto").trim(),debug:"1"===process.env.HUICAI_DEBUG||!0===e.debug,statusLine:(e.statusLine||"").trim(),advisorModel:(process.env.HUICAI_ADVISOR||e.advisorModel||"").trim(),autoModelStrong:(process.env.HUICAI_AUTOMODEL||e.autoModelStrong||"").trim(),budgetTokens:Number(e.budgetTokens)||0,embeddingModel:(process.env.HUICAI_EMBED_MODEL||e.embeddingModel||"").trim()};return o.baseUrl=o.baseUrl.replace(/\/+$/,""),o}export function saveConfig(o){const r={...n(),...o};e.mkdirSync(CONFIG_DIR,{recursive:!0}),e.writeFileSync(CONFIG_PATH,JSON.stringify(r,null,2)+"\n",{mode:384});try{e.chmodSync(CONFIG_PATH,384)}catch{}return r}
|
package/src/device.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as e}from"node:child_process";import{c as t}from"./colors.js";
|
|
1
|
+
import{spawn as e}from"node:child_process";import{c as t}from"./colors.js";export function openBrowser(t){if(!t||process.env.HUICAI_NO_BROWSER)return;const r=process.platform,o="darwin"===r?"open":"win32"===r?"cmd":"xdg-open",i="win32"===r?["/c","start","",t]:[t];try{const t=e(o,i,{stdio:"ignore",detached:!0});t.on("error",()=>{}),t.unref()}catch{}}async function r(e,t,r){const o=await fetch(`${e}${t}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(r||{})}),i=await o.text();let n;try{n=JSON.parse(i)}catch{throw new Error(`服务返回异常(${o.status}):${i.slice(0,200)}`)}if(!o.ok)throw new Error(n?.message||`HTTP ${o.status}`);if(n&&"object"==typeof n&&"data"in n&&"code"in n){if(0!==n.code&&200!==n.code)throw new Error(n.message||"请求失败");return n.data}return n}const o=e=>new Promise(t=>setTimeout(t,e));export async function startDeviceAuth(e,t){const o=await r(e,"/v1/device/code",{client_name:t||`huicai-cli (${process.platform})`}),{deviceCode:i,userCode:n,verificationUri:s,verificationUriComplete:c}=o,a=Math.max(2,o.interval||5),d=o.expiresIn||600,p=s||e+"/device",u=c||p+(p.includes("?")?"&":"?")+"code="+encodeURIComponent(n);return{deviceCode:i,userCode:n,verificationUri:p,verificationUriComplete:u,interval:a,expiresIn:d}}export function pollDeviceAuth(e,t){return r(e,"/v1/device/token",{device_code:t})}export async function deviceLogin(e){const{deviceCode:r,userCode:i,verificationUri:n,verificationUriComplete:s,interval:c,expiresIn:a}=await startDeviceAuth(e);process.stdout.write("\n"+t.bold("请在浏览器中完成授权:")+"\n"),process.stdout.write(" 1) 打开 "+t.cyan(n)+"\n"),process.stdout.write(" 2) 确认授权码 "+t.bold(t.yellow(i))+"\n"),process.stdout.write(t.gray(" (已尝试自动打开浏览器;未弹出请手动访问上面的地址)")+"\n"),openBrowser(s),process.stdout.write(t.gray(`\n等待授权中…(有效期约 ${Math.round(a/60)} 分钟,Ctrl+C 取消)`)+"\n");const d=Date.now()+1e3*a;for(;Date.now()<d;){let i;await o(1e3*c);try{i=await pollDeviceAuth(e,r)}catch(e){process.stdout.write(t.gray(` 轮询出错(将重试):${e.message}`)+"\n");continue}if("approved"===i.status&&i.apiKey)return process.stdout.write(t.green("\n✓ 授权成功")+"\n"),i.apiKey;if("denied"===i.status)throw new Error("用户在浏览器中拒绝了授权");if("expired"===i.status)throw new Error("授权码已过期,请重新登录");process.stdout.write(t.gray(" · 仍在等待浏览器确认…")+"\n")}throw new Error("授权超时,请重新运行 huicai login")}
|
package/src/executors.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import
|
|
1
|
+
import e from"node:fs";import t from"node:path";import{spawn as r}from"node:child_process";import*as n from"./ide.js";import{extractDocument as i}from"./api.js";const o=16777216,s=3e4,a=new Set([".git","node_modules","dist","build","target","out",".next",".nuxt",".venv","venv","__pycache__",".idea",".gradle",".mvn","coverage",".cache"]);function l(e,r,n){if(!e||"string"!=typeof e)throw new Error("工作目录未设置:请在项目目录中运行(VS Code 中需先「打开文件夹」再对话)");if(!r||"string"!=typeof r)throw new Error("缺少路径 path(工具参数里必须提供 path 字段)");const i=t.resolve(e,r);if(n){const n=t.relative(e,i);if(""===n||n.startsWith("..")||t.isAbsolute(n))throw new Error(`路径超出项目目录(写操作仅限当前项目内):${r}`)}return i}function c(e,r){const n=t.relative(e,r);return""===n?".":n}function u(e){const t=Math.min(e.length,4096);for(let r=0;r<t;r++)if(0===e[r])return!0;return!1}function*f(r,n=0,i=14){let o;try{o=e.readdirSync(r,{withFileTypes:!0})}catch{return}for(const e of o){if(a.has(e.name))continue;const o=t.join(r,e.name);e.isDirectory()?n<i&&(yield*f(o,n+1,i)):e.isFile()&&(yield o)}}function p(e){let t="";for(let r=0;r<e.length;r++){const n=e[r];"*"===n?"*"===e[r+1]?(t+=".*",r++,"/"===e[r+1]&&r++):t+="[^/]*":"?"===n?t+="[^/]":"\\^$+.()|{}[]".includes(n)?t+="\\"+n:t+=n}return new RegExp("^"+t+"$")}export function readFile(t,r){const n=l(t,r.path,!1),i=e.statSync(n);if(i.isDirectory())throw new Error(`是目录而非文件:${r.path}`);if(i.size>o)throw new Error(`文件过大(${Math.round(i.size/1024/1024)}MB):请用 grep 检索,或用 run_bash 的 head/tail/sed 分段查看`);const s=e.readFileSync(n);if(u(s))return`(二进制文件,${i.size} 字节,未展示内容)`;const a=s.toString("utf8");if(""===a)return"(空文件)";const c=a.split("\n");c.length>1&&""===c[c.length-1]&&c.pop();const f=c.length,p=Math.max(0,(Number.isFinite(r.offset)?r.offset:1)-1),h=Number.isFinite(r.limit)&&r.limit>0?r.limit:2e3,g=Math.min(f,p+h);if(p>=f)return`(起始行 ${p+1} 超出文件总行数 ${f})`;const m=String(g).length,d=[];for(let e=p;e<g;e++){let t=c[e];t.length>2e3&&(t=t.slice(0,2e3)+"…(行过长已截断)"),d.push(`${String(e+1).padStart(m)}→${t}`)}let $=d.join("\n");return(p>0||g<f)&&($+=`\n…(文件共 ${f} 行,本次显示第 ${p+1}-${g} 行,可用 offset/limit 继续读取)`),$}const h=20971520;export async function readDocument(r,n,o){const s=l(r,n.path,!1),a=e.statSync(s);if(a.isDirectory())throw new Error(`是目录而非文件:${n.path}`);if(a.size>h)throw new Error(`文档超过 ${Math.round(20)}MB 解析上限`);if(!o||!o.baseUrl||!o.apiKey)throw new Error("解析文档需登录平台(先执行 huicai login)");const u=Number.isFinite(Number(n.offset))&&Number(n.offset)>0?Math.floor(Number(n.offset)):0,f=Number.isFinite(Number(n.max_chars))&&Number(n.max_chars)>0?Math.min(18e3,Math.max(1e3,Math.floor(Number(n.max_chars)))):16e3,p=await i(o.baseUrl,o.apiKey,{filename:t.basename(s),data:e.readFileSync(s).toString("base64"),maxChars:f,offset:u}),g=p.chars??0,m=p.text||"";if(!m)return`(第 ${u} 字符起已无内容;全文共 ${g} 字符)`;const d=u+m.length,$=p.truncated?`\n…(全文共 ${g} 字符,本段为第 ${u}–${d} 字符;继续调用 read_document 并传 offset=${d} 读取后续)`:`\n(全文共 ${g} 字符,本段为第 ${u}–${d} 字符,已到结尾)`,w=p.pages?`${p.pages}页·`:"";return`【文档 ${c(r,s)}(${w}共 ${g} 字符)】\n${m}${$}`}export async function writeFile(r,i){const s=l(r,i.path,!0),a=null==i.content?"":String(i.content);if(!0===i.append){let i="";try{i=e.readFileSync(s,"utf8")}catch{}const l=i+a;if(Buffer.byteLength(l)>o)throw new Error(`追加后文件将超过 ${Math.round(16)}MB 上限,请拆分为多个文件`);if(n.isActive()){const e=await n.proposeWrite(s,l,r);if(e.decided)return e.applied?`已追加到 ${c(r,s)}(经 IDE 审阅应用,本段 ${Buffer.byteLength(a)} 字节)`:`用户在 IDE 中拒绝了对 ${c(r,s)} 的追加${e.reason?":"+e.reason:""}`}return e.mkdirSync(t.dirname(s),{recursive:!0}),e.writeFileSync(s,l,"utf8"),`已追加到 ${c(r,s)}(本段 ${Buffer.byteLength(a)} 字节,文件现 ${Buffer.byteLength(l)} 字节)`+(n.isActive()?"(IDE 未响应,已直接写入)":"")}if(n.isActive()){const e=await n.proposeWrite(s,a,r);if(e.decided)return e.applied?`已写入 ${c(r,s)}(经 IDE 审阅应用)`:`用户在 IDE 中拒绝了对 ${c(r,s)} 的修改${e.reason?":"+e.reason:""}`}return e.mkdirSync(t.dirname(s),{recursive:!0}),e.writeFileSync(s,a,"utf8"),`已写入 ${c(r,s)}(${Buffer.byteLength(a)} 字节)`+(n.isActive()?"(IDE 未响应,已直接写入)":"")}export async function editFile(t,r){const i=l(t,r.path,!0),o=null==r.old_string?"":String(r.old_string),s=null==r.new_string?"":String(r.new_string);if(""===o)throw new Error("edit_file 需要非空 old_string");const a=e.readFileSync(i,"utf8"),u=a.split(o).length-1;if(0===u)throw new Error("未找到要替换的内容(old_string 未出现在文件中)。注意:old_string 必须是文件原文,不能包含 read_file 显示的「行号→」前缀");if(u>1&&!r.replace_all)throw new Error(`old_string 在文件中出现 ${u} 处,不唯一;请补充上下文使其唯一,或设 replace_all=true`);const f=r.replace_all?a.split(o).join(s):a.replace(o,s),p=r.replace_all?u:1;if(n.isActive()){const e=await n.proposeWrite(i,f,t);if(e.decided)return e.applied?`已编辑 ${c(t,i)}(经 IDE 审阅应用,替换 ${p} 处)`:`用户在 IDE 中拒绝了对 ${c(t,i)} 的修改${e.reason?":"+e.reason:""}`}return e.writeFileSync(i,f,"utf8"),`已编辑 ${c(t,i)},替换 ${p} 处`+(n.isActive()?"(IDE 未响应,已直接写入)":"")}export function listFiles(r,n){const i=l(r,n.path||".",!1),o=[];let s=0;const c=(r,n)=>{if(s>=400)return;let l;try{l=e.readdirSync(r,{withFileTypes:!0})}catch{return}l.sort((e,t)=>e.name.localeCompare(t.name));for(const e of l){if(s>=400)break;if(a.has(e.name))continue;const l=t.join(r,e.name),u=t.relative(i,l);e.isDirectory()?(o.push(u+"/"),s++,n<6&&c(l,n+1)):(o.push(u),s++)}};return c(i,0),0===o.length?"(空目录)":o.join("\n")+(s>=400?"\n…(条目已截断)":"")}export function glob(e,r){if(!r.pattern)throw new Error("缺少 pattern");const n=p(r.pattern),i=[];for(const r of f(e)){const o=t.relative(e,r).split(t.sep).join("/");if(n.test(o)&&(i.push(o),i.length>=300))break}return 0===i.length?"(无匹配文件)":i.join("\n")}export function grep(r,n){if(!n.pattern)throw new Error("缺少 pattern");let i;try{i=new RegExp(n.pattern,n.ignore_case?"i":void 0)}catch(e){throw new Error(`无效的正则:${e.message}`)}const o=n.glob?p(n.glob):null,s=l(r,n.path||".",!1),a=Number.isFinite(n.max_results)&&n.max_results>0?Math.min(n.max_results,500):200,c=[],h=[];for(const l of f(s)){if(c.length>=a)break;const s=t.relative(r,l).split(t.sep).join("/");if(o&&!o.test(s))continue;let f;try{f=e.readFileSync(l)}catch{continue}if(f.length>262144||u(f))continue;const p=f.toString("utf8").split("\n");let g=!1;for(let e=0;e<p.length;e++)if(i.test(p[e])){if(g=!0,n.files_only)break;if(c.push(`${s}:${e+1}: ${p[e].slice(0,240)}`),c.length>=a)break}if(g&&(h.push(s),n.files_only&&h.length>=a))break}return n.files_only?h.length?h.join("\n"):"(无匹配)":0===c.length?"(无匹配)":c.join("\n")+(c.length>=a?"\n…(结果已截断)":"")}export function runBash(e,t){const n=null==t.command?"":String(t.command);if(""===n.trim())return Promise.resolve("(未提供命令)");const i=Number.isFinite(t.timeout)?Math.min(t.timeout,6e5):12e4,o="win32"===process.platform;return new Promise(t=>{let a="",l=!1;const c=o?r(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",n],{cwd:e,windowsVerbatimArguments:!0}):r(process.env.SHELL||"/bin/sh",["-c",n],{cwd:e,detached:!0}),u=setTimeout(()=>{l=!0,(()=>{try{!o&&c.pid?process.kill(-c.pid,"SIGKILL"):c.kill("SIGKILL")}catch{try{c.kill("SIGKILL")}catch{}}})()},i),f=e=>{a.length<s&&(a+=e.toString())};c.stdout.on("data",f),c.stderr.on("data",f),c.on("error",e=>{clearTimeout(u),t(`命令启动失败:${e.message}`)}),c.on("close",e=>{clearTimeout(u);let r=a.length>s?a.slice(0,s)+"\n…(输出已截断)":a;""===r.trim()&&(r="(无输出)");t(`${l?"【超时被终止】":`退出码 ${e}`}\n${r}`)})})}export async function fetchUrl(e,t){const r=String(t.url||"").trim();if(!/^https?:\/\//i.test(r))throw new Error("仅支持 http/https URL");let n;try{n=await fetch(r,{redirect:"follow",signal:AbortSignal.timeout(Number.isFinite(t.timeout)?Math.min(t.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(e){throw new Error(`请求失败:${e?.cause?.message||e.message}`)}const i=await async function(e,t){if(!e.body)return Buffer.alloc(0);const r=e.body.getReader(),n=[];let i=0;for(;;){const{done:e,value:o}=await r.read();if(e)break;if(n.push(o),i+=o.length,i>=t){r.cancel().catch(()=>{});break}}return Buffer.concat(n.map(e=>Buffer.from(e)),Math.min(i,t))}(n,2097152),o=(n.headers.get("content-type")||"").toLowerCase();if(u(i)&&!/text|json|xml|html/.test(o))return`HTTP ${n.status} · ${o||"未知类型"} · ${i.length} 字节(二进制内容未展示)`;let s=i.toString("utf8");(o.includes("html")||/^\s*<(!doctype|html)/i.test(s))&&(s=function(e){let t=String(e);return t=t.replace(/<(script|style|noscript|svg|head)[\s\S]*?<\/\1>/gi,""),t=t.replace(/<!--[\s\S]*?-->/g,""),t=t.replace(/<br\s*\/?>/gi,"\n"),t=t.replace(/<\/(p|div|li|tr|h[1-6]|section|article|blockquote|pre|ul|ol|table)>/gi,"\n"),t=t.replace(/<li[^>]*>/gi,"- "),t=t.replace(/<[^>]+>/g,""),t=t.replace(/ /g," ").replace(/</g,"<").replace(/>/g,">").replace(/"/g,'"').replace(/'|'/g,"'").replace(/&/g,"&"),t=t.replace(/[ \t]+\n/g,"\n").replace(/\n{3,}/g,"\n\n"),t}(s)),s=s.trim(),s.length>8e4&&(s=s.slice(0,8e4)+"\n…(内容过长已截断)");const a=`HTTP ${n.status}${n.url&&n.url!==r?` · 重定向至 ${n.url}`:""}`;return n.ok,`${a}\n${s||"(无内容)"}`}
|
package/src/input.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import t from"node:path";import n from"node:os";import{CONFIG_DIR as r}from"./config.js";import{runBash as i}from"./executors.js";import{extractDocument as s}from"./api.js";const o=t.join(r,"history.json"),a={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",bmp:"image/bmp",svg:"image/svg+xml"},c=new Set(["pdf","doc","docx","xls","xlsx","ppt","pptx","rtf","odt","ods","odp","csv"]),p=8388608,l=20971520;export async function expandAtMentions(r,i,o=null){const
|
|
1
|
+
import e from"node:fs";import t from"node:path";import n from"node:os";import{CONFIG_DIR as r}from"./config.js";import{runBash as i}from"./executors.js";import{extractDocument as s}from"./api.js";const o=t.join(r,"history.json"),a={png:"image/png",jpg:"image/jpeg",jpeg:"image/jpeg",gif:"image/gif",webp:"image/webp",bmp:"image/bmp",svg:"image/svg+xml"},c=new Set(["pdf","doc","docx","xls","xlsx","ppt","pptx","rtf","odt","ods","odp","csv"]),p=8388608,l=20971520;export async function expandAtMentions(r,i,o=null){const d=/(^|[\s("'`(【,,::])@([~A-Za-z0-9_./\\-]+)/g,f=new Set,m=[];let g;for(;(g=d.exec(r))&&m.length<5;){const e=g[2].replace(/[.,;:。,;]+$/,"");e&&!f.has(e)&&(f.add(e),m.push(e))}const h=[],y=[],$=[];for(const r of m){const d=r.startsWith("~")?t.join(n.homedir(),r.slice(1)):t.resolve(i,r);let f;try{f=e.statSync(d)}catch{continue}if($.push(r),f.isDirectory()){try{const t=e.readdirSync(d).slice(0,100).join("\n");h.push(`【引用目录 @${r}】\n${t||"(空目录)"}`)}catch{}continue}const m=t.extname(d).slice(1).toLowerCase();if(a[m])if(f.size>p)h.push(`【图片 @${r}】(超过 ${Math.round(8)}MB,未附加)`);else try{const t=e.readFileSync(d).toString("base64");y.push({label:r,dataUri:`data:${a[m]};base64,${t}`}),h.push(`【已附加图片 @${r}】`)}catch{}else if(c.has(m))if(o&&o.baseUrl&&o.apiKey)if(f.size>l)h.push(`【文档 @${r}】(超过 ${Math.round(20)}MB,未解析)`);else try{const n=e.readFileSync(d).toString("base64"),i=await s(o.baseUrl,o.apiKey,{filename:t.basename(d),data:n,maxChars:6e4}),a=/^(agent|workflow|datahuman|squad)-/.test(String(o?.model||"")),c=i.truncated?a?"·已截断(头尾摘要)":"·已截断(头尾摘要,被省略部分可用 read_document 工具按 offset 分段读取)":"",p=`${i.pages?i.pages+"页·":""}${i.chars??0}字${c}`;h.push(`【引用文档 @${r}(${p})】\n${i.text||"(空)"}`)}catch(e){h.push(`【文档 @${r}】(解析失败:${e.message})`)}else h.push(`【文档 @${r}】(需登录后才能解析为文本)`);else if(f.size>524288)h.push(`【引用文件 @${r}】\n(文件过大:${Math.round(f.size/1024)}KB,未展开;可让我用 read_file 分段读取)`);else try{const t=e.readFileSync(d);if(u(t)){h.push(`【引用文件 @${r}】\n(二进制文件,未展开)`);continue}let n=t.toString("utf8");n.length>12e3&&(n=n.slice(0,12e3)+"\n…(已截断)"),h.push(`【引用文件 @${r}】\n\`\`\`\n${n}\n\`\`\``)}catch{}}return{text:h.length?r+"\n\n"+h.join("\n\n"):r,mentions:$,images:y}}function u(e){const t=Math.min(e.length,4096);for(let n=0;n<t;n++)if(0===e[n])return!0;return!1}export function buildUserContent(e,t){return t&&t.length?[{type:"text",text:e},...t.map(e=>({type:"image_url",image_url:{url:e.dataUri}}))]:e}export async function runBangCommand(e,t){const n=await i(t,{command:e}),r=n.length>8e3?n.slice(0,8e3)+"\n…(已截断)":n;return{output:n,record:`我在终端直接执行了命令:$ ${e}\n${r}`}}function d(){try{const t=JSON.parse(e.readFileSync(o,"utf8"));return Array.isArray(t?.entries)?t.entries:[]}catch{return[]}}export function loadHistory(e,t=200){return d().filter(t=>t&&t.cwd===e&&"string"==typeof t.line).slice(-t).map(e=>e.line)}export function appendHistory(t,n){const i=String(n||"").trim();if(i)try{let n=d();const s=[...n].reverse().find(e=>e.cwd===t);if(s&&s.line===i)return;n.push({cwd:t,line:i,ts:Date.now()}),n.length>1e3&&(n=n.slice(-1e3)),e.mkdirSync(r,{recursive:!0}),e.writeFileSync(o,JSON.stringify({entries:n}),{mode:384})}catch{}}
|
package/src/session.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import e from"node:path";import{CONFIG_DIR as n}from"./config.js";export const SESSIONS_DIR=e.join(n,"sessions");function r(){t.mkdirSync(SESSIONS_DIR,{recursive:!0})}function o(t){return e.join(SESSIONS_DIR,t+".json")}function
|
|
1
|
+
import t from"node:fs";import e from"node:path";import{CONFIG_DIR as n}from"./config.js";export const SESSIONS_DIR=e.join(n,"sessions");function r(){t.mkdirSync(SESSIONS_DIR,{recursive:!0})}function o(t){return e.join(SESSIONS_DIR,t+".json")}function s(t){const e=(t||[]).find(t=>"user"===t.role);return e&&e.content&&String(e.content).replace(/\s+/g," ").trim().slice(0,60)||"(空会话)"}export function turnCount(t){return(t||[]).filter(t=>"user"===t.role).length}export function createSession({cwd:t,model:e,messages:n}){const r=Date.now();return{id:Date.now().toString(36)+"-"+Math.random().toString(36).slice(2,6),cwd:t,model:e,createdAt:r,updatedAt:r,title:"",messages:n||[],compaction:{}}}export function saveSession(e){if(e&&e.id)try{r(),e.updatedAt=Date.now(),e.title||(e.title=s(e.messages)),t.writeFileSync(o(e.id),JSON.stringify(e),{mode:384})}catch(t){process.stderr.write(`(会话保存失败,不影响对话:${t.message})\n`)}}export function loadSession(e){if(!e)return null;try{const n=JSON.parse(t.readFileSync(o(e),"utf8"));return n&&Array.isArray(n.messages)?(n.id||(n.id=e),n.compaction&&"object"==typeof n.compaction||(n.compaction={}),n):null}catch{return null}}export function listSessions(n){let o;try{r(),o=t.readdirSync(SESSIONS_DIR).filter(t=>t.endsWith(".json"))}catch{return[]}const i=[];for(const r of o)try{const o=JSON.parse(t.readFileSync(e.join(SESSIONS_DIR,r),"utf8"));if(n&&o.cwd!==n)continue;i.push({id:o.id||r.replace(/\.json$/,""),title:o.title||s(o.messages),cwd:o.cwd,model:o.model,updatedAt:o.updatedAt||0,turns:turnCount(o.messages),user:o.user?String(o.user):""})}catch{}return i.sort((t,e)=>e.updatedAt-t.updatedAt),i}export function mostRecent(t){const e=listSessions(t);return e.length?e[0].id:null}export function deleteSession(e){try{return t.unlinkSync(o(e)),!0}catch{return!1}}export function relTime(t){if(!t)return"未知时间";const e=Math.max(0,Math.floor((Date.now()-t)/1e3));if(e<60)return e+" 秒前";const n=Math.floor(e/60);if(n<60)return n+" 分钟前";const r=Math.floor(n/60);if(r<24)return r+" 小时前";return Math.floor(r/24)+" 天前"}
|
package/src/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as e from"./executors.js";import{searchCode as t}from"./codeindex.js";import{AUTOMATION_TOOL_SPECS as r,AUTOMATION_EXECUTORS as
|
|
1
|
+
import*as e from"./executors.js";import{searchCode as t}from"./codeindex.js";import{AUTOMATION_TOOL_SPECS as r,AUTOMATION_EXECUTORS as n}from"./automation.js";const i=(e,t)=>({type:"object",properties:e,required:t||[]}),o=e=>({type:"string",description:e}),a=e=>({type:"integer",description:e}),p=e=>({type:"boolean",description:e});export const TOOL_SPECS=[{type:"function",function:{name:"read_file",description:"读取文本文件内容,每行带「行号→」前缀(行号仅供定位,不属于文件内容,编辑时不要包含它)。默认最多 2000 行,可用 offset(起始行,1基)/limit(行数) 分段读取大文件。",parameters:i({path:o("相对项目根的文件路径"),offset:a("可选:起始行(1基)"),limit:a("可选:读取行数")},["path"])}},{type:"function",function:{name:"list_files",description:"列出目录下的文件树(跳过 .git/node_modules 等)。可选 path 指定子目录(默认项目根)。",parameters:i({path:o("可选:子目录路径,默认当前项目根")},[])}},{type:"function",function:{name:"glob",description:'按 glob 模式查找文件(支持 ** 与 *),如 "**/*.ts"、"src/**/*.java"。',parameters:i({pattern:o("glob 模式")},["pattern"])}},{type:"function",function:{name:"search_code",description:"按「概念/功能」检索最相关的代码块(BM25 相关性排序,比 grep 更懂「哪段代码在做这件事」)。适合「登录逻辑在哪」「哪里处理支付回调」这类找相关实现;要找精确字符串仍用 grep。返回 文件:行范围 + 代码块。",parameters:i({query:o("要找的功能/概念(自然语言或关键词组合,多写几个相关词更准)"),k:a("可选:返回块数,默认 8,上限 20")},["query"])}},{type:"function",function:{name:"grep",description:"在文件内容中按正则搜索,返回 匹配文件:行号: 该行。可选 path 限定目录、glob 限定文件名模式、ignore_case 忽略大小写、files_only 只列出命中文件。",parameters:i({pattern:o("正则表达式"),path:o("可选:搜索目录,默认项目根"),glob:o("可选:文件名 glob 过滤"),ignore_case:p("可选:忽略大小写(默认否)"),files_only:p("可选:只返回命中文件列表,不含匹配行(默认否)"),max_results:a("可选:最多返回条数(默认 200,上限 500)")},["pattern"])}},{type:"function",function:{name:"read_document",description:"读取 PDF/Word/Excel/PPT/RTF 等文档的纯文本内容(经平台解析,read_file 读不了这类二进制文档)。大型文档分段通读:段末标注「全文共N字符/本段区间」,继续调用并传 offset 读取后续,直到「已到结尾」。对话附件若标注「已截断」,用本工具读取被省略的部分。",parameters:i({path:o("相对项目根的文档路径"),offset:a("可选:窗口起始字符位置(0 基,默认 0,从头顺序读取)"),max_chars:a("可选:本段最大字符数(默认 16000)")},["path"])}},{type:"function",function:{name:"write_file",description:"把文本写入文件(覆盖式,自动建父目录)。仅限当前项目目录内。仅用于新建文件或全量重写;小改动优先 edit_file。生成超长文件(长文档/PPT/大代码文件)时分段产出:先写首段,后续每段设 append=true 追加到文件末尾,避免单次内容过长被截断。",parameters:i({path:o("相对项目根的文件路径"),content:o("本次写入的内容(append=true 时为追加的续段)"),append:p("可选:true=追加到文件末尾(分段产出大文件);默认覆盖写")},["path","content"])}},{type:"function",function:{name:"edit_file",description:"精确替换文件中的片段:old_string 必须是文件原文(不含 read_file 的行号前缀)且在文件中唯一(除非 replace_all=true)。仅限项目目录内。",parameters:i({path:o("相对项目根的文件路径"),old_string:o("被替换的原文(需在文件中唯一,除非 replace_all;不要包含行号前缀)"),new_string:o("替换为的新内容"),replace_all:p("是否替换全部匹配(默认否)")},["path","old_string","new_string"])}},{type:"function",function:{name:"run_bash",description:"在项目目录下执行一条 shell 命令并返回输出(stdout+stderr+退出码)。用于构建、测试、git、脚手架等。默认超时 120s(可用 timeout 调整)。避免用它读文件/搜索(用 read_file/grep 更高效)。",parameters:i({command:o("要执行的 shell 命令"),timeout:a("可选:超时毫秒")},["command"])}},{type:"function",function:{name:"fetch_url",description:"抓取一个 http/https 网页或接口(GET),HTML 自动转为纯文本。用于查在线文档、API 说明、Issue 等。结果可能很长,尽量带着明确目的使用。",parameters:i({url:o("完整 URL(http/https)"),timeout:a("可选:超时毫秒,默认 30000")},["url"])}},...r];export const READONLY_TOOLS=new Set(["read_file","list_files","glob","grep","search_code","read_document"]);const s={read_file:e.readFile,read_document:e.readDocument,list_files:e.listFiles,glob:e.glob,grep:e.grep,search_code:t,write_file:e.writeFile,edit_file:e.editFile,run_bash:e.runBash,fetch_url:e.fetchUrl,...n};export async function runTool(e,t,r,n){const i=s[e];if(!i)return`未知工具:${e}`;try{return await i(r,t||{},n)}catch(e){return`工具执行失败:${e.message}`}}
|