@huicai/cli 2026.721.0 → 2026.804.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/README.md CHANGED
@@ -158,10 +158,17 @@ huicai -c -p "继续修" --yolo # 非交互续接(CI)
158
158
 
159
159
  ### 上下文自动压缩(非破坏式)
160
160
 
161
- 双触发:累计字符超阈值(约 16k tokens),或上一轮**真实 `prompt_tokens`** 超限(默认 80k,
162
- `autoCompactTokens` 可调)。**摘要独立存储在会话的 compaction 状态里,原对话消息与页面显示完全不变**——
163
- 只是发给模型的上下文换成「system + 摘要 + 最近几轮」的视图。以用户消息为切割边界,绝不拆散工具调用与其结果。
164
- `/compact` 可随时手动压缩(同样不影响已显示内容)。
161
+ 双触发:上一轮**真实 `prompt_tokens`** 超阈值,或(尚无实测值时)本地估算超阈值——估算与 `/context`
162
+ 同口径、CJK 感知。**默认阈值 100 万 tokens**(`autoCompactTokens` 或 `/autocompact <N>` 可调,`off` 关闭)。
163
+
164
+ 阈值刻意高于多数模型的实际窗口:真正的把关在于**模型报「上下文装不下」时会自动强制压缩一次再重试同一模型**
165
+ (用户无感,每回合至多 3 次),所以小窗口模型不必手动调小阈值,长会话也不会每隔几轮就被摘要打断。
166
+
167
+ **摘要独立存储在会话的 compaction 状态里,原对话消息与页面显示完全不变**——只是发给模型的上下文换成
168
+ 「system + 摘要 + 最近几轮」的视图。优先以用户消息为切割边界;单个超长回合(一条提问 + 上百轮工具往返)
169
+ 则在回合内切,切点吸附到非工具结果处,两种情况都绝不拆散工具调用与其结果。超大上下文的摘要会按预算
170
+ 分块、链式递进(每块并入上一块摘要),不会因摘要请求自身超限而失效。`/compact` 可随时手动压缩
171
+ (同样不影响已显示内容),`/context` 查看当前占用与距阈值的比例。
165
172
 
166
173
  ### 平台编排对话(分享页同款,CLI 直连)
167
174
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@huicai/cli",
3
- "version": "2026.721.0",
4
- "versionDisplay": "2026.07.21",
3
+ "version": "2026.804.0",
4
+ "versionDisplay": "2026.08.04",
5
5
  "description": "在终端运行的智能体 CLI:以 HuiCai_AI 平台的 OpenAI 兼容 /v1 为模型后端,在用户本机执行文件/命令工具。",
6
6
  "type": "module",
7
7
  "bin": {
package/src/advisor.js CHANGED
@@ -1 +1 @@
1
- import{streamChat as e}from"./api.js";export function makeAdvisorTool({config:r}){if(!r?.advisorModel)return null;return{readOnly:!0,spec:{type:"function",function:{name:"ask_advisor",description:"遇到难题、复杂决策或反复卡住时,向一个更强的「技术顾问」模型请教独立的第二意见。把问题背景、已知信息、已尝试过什么、待决策点写清楚(自足),它会给出判断/方案与关键风险。用于真正的难点或高风险决策,不要滥用。",parameters:{type:"object",properties:{question:{type:"string",description:"自足的难题描述(含背景、已尝试、待决策点)"}},required:["question"]}}},run:async t=>{const n=String(t?.question||"").trim();if(!n)return"缺少 question";try{const t=await e({baseUrl:r.baseUrl,apiKey:r.apiKey,model:r.advisorModel,messages:[{role:"system",content:"你是资深技术顾问,正被一个编码智能体咨询一个难题。基于它提供的信息,给出清晰、具体、可执行的判断或方案:指出关键风险、推荐做法、以及为何。简洁直接,不要寒暄。"},{role:"user",content:n}]});return`【顾问(${r.advisorModel})意见】\n`+(t.content||"(顾问无回复)").trim()}catch(e){return"咨询顾问失败:"+e.message}}}}
1
+ import{streamChat as e}from"./api.js";export function makeAdvisorTool({config:t}){if(!t?.advisorModel)return null;return{readOnly:!0,spec:{type:"function",function:{name:"ask_advisor",description:"遇到难题、复杂决策或反复卡住时,向一个更强的「技术顾问」模型请教独立的第二意见。把问题背景、已知信息、已尝试过什么、待决策点写清楚(自足),它会给出判断/方案与关键风险。用于真正的难点或高风险决策,不要滥用。",parameters:{type:"object",properties:{question:{type:"string",description:"自足的难题描述(含背景、已尝试、待决策点)"}},required:["question"]}}},run:async r=>{const n=String(r?.question||"").trim();if(!n)return"缺少 question";try{const r=await e({baseUrl:t.baseUrl,apiKey:t.apiKey,model:t.advisorModel,client:t.client,aux:!0,messages:[{role:"system",content:"你是资深技术顾问,正被一个编码智能体咨询一个难题。基于它提供的信息,给出清晰、具体、可执行的判断或方案:指出关键风险、推荐做法、以及为何。简洁直接,不要寒暄。"},{role:"user",content:n}]});return`【顾问(${t.advisorModel})意见】\n`+(r.content||"(顾问无回复)").trim()}catch(e){return"咨询顾问失败:"+e.message}}}}
package/src/agent.js CHANGED
@@ -1 +1 @@
1
- import t from"node:fs";import e from"node:path";import{streamChat as o}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 u,renderFileDiff as c}from"./render.js";import{compactIfNeeded as m,buildModelMessages as g}from"./compaction.js";import{makeTodoTool as p,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";const b=/(现在|接下来|下面|这就|马上|我来|让我|我要|我先|我这就|咱们|让我们|稍等|待我)/,T=/(做|进行|生成|编写|写(一|个|完|出)?|制作|计算|统计|核对|汇总|整理|分析|梳理|输出|列出|准备|运行|执行|开始)/,k=/\b(let me|now let me|i.?ll|i will|let.?s|next,? i)\b[^.!?\n]*\b(write|create|generat|calculat|comput|do|produc|compil|analyz|summar|prepar|run|make|list)\b/i;function x(t){const e=String(t||"").trim().split(/\n+/).map(t=>t.trim()).filter(Boolean),o=e[e.length-1]||"";return!(!o||o.length>120)&&(!!k.test(o)||b.test(o)&&T.test(o))}function $(t){const e=String(t);return e.startsWith("工具执行失败")||e.startsWith("命令启动失败")||e.includes("不是合法 JSON")||e.includes("未找到要替换的内容")||e.includes("路径超出项目目录")||e.includes("是目录而非文件")}function P(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}function j(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function M(t,e,o){const n=new Array(t.length);let r=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;r<t.length;){const e=r++;n[e]=await o(t[e],e)}})),n}export function isPlatformModel(t){return/^(agent|workflow|datahuman|squad)-/.test(String(t||""))}function E(t,e=200){const o=String(t).split("\n")[0];return o.length>e?o.slice(0,e)+"…":o}export function capToolResult(t){const e=String(t);if(e.length<=2e4)return e;const o=e.length-16e3-3e3;return e.slice(0,16e3)+`\n…【工具输出过长,中间已省略 ${o} 字符】…\n`+e.slice(-3e3)}export async function runTurn({config:b,messages:T,permissions:k,cwd:I,ask:U,signal:v,extraTools:A=[],depth:R=0,io:O,events:C,todoState:N,compaction:D,pullQueued:J,maxIters:q,fileJournal:F}){const K=O&&O.write||(t=>process.stdout.write(t)),W=C||null,z=t=>{W?W.onInfo?.(t):K(_.gray(" · "+t)+"\n")};let B=0,L=0,Q=0;const G=Number(q||b.maxTurns)||120,H=isPlatformModel(b.model);let V=b.model;if(0===R&&b.autoModelStrong&&!H){const t=[...T].reverse().find(t=>"user"===t.role),e=j(t?.content);(b.planMode||e.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(e))&&b.autoModelStrong!==b.model&&(V=b.autoModelStrong,b.debug&&z(`[automodel] 复杂回合升配 → ${V}`))}const X=D||{},Y=[...A];if(0===R&&!H&&!Y.some(t=>t.spec?.function?.name===d)){const t=N||{list:[]};Y.push(p({state:t,emit:t=>{W?W.onTodo?W.onTodo(t.map(t=>({...t}))):W.onInfo?.("任务清单:\n"+f(t)):K(_.gray(u(f(t)," "))+"\n")}}))}0!==R||H||!1===b.memoryEnabled||Y.some(t=>"memory_write"===t.spec?.function?.name)||Y.push(h({cwd:I,emit:t=>{W?W.onMemory?W.onMemory(t):W.onInfo?.("已记入长期记忆:"+E(t,80)):K(_.gray(" ✎ 已记入长期记忆:"+E(t,80))+"\n")}}));const Z=new Map;for(const t of Y)Z.set(t.spec.function.name,t);const tt=H?void 0:[...s,...Y.map(t=>t.spec)];let et=0,ot=!1,nt=0,rt=null,st=0,at=0,lt=!1,it="";const ut=0!==R||H?null:y(I),ct=!(!ut||!ut.all.length);if(ct){const t=[];if(T.filter(t=>"user"===t.role).length<=1){const e=w("SessionStart",{cwd:I},ut);e.injectContext&&t.push(e.injectContext);for(const o of e.outputs)t.push(o.stdout)}const e=[...T].reverse().find(t=>"user"===t.role),o=w("UserPromptSubmit",{cwd:I,prompt:j(e?.content)},ut);if(o.block)return z("提交被 UserPromptSubmit 钩子阻断:"+o.reason),{error:"提交被钩子阻断:"+o.reason,usage:{prompt:B,completion:L},lastPromptTokens:Q};o.injectContext&&t.push(o.injectContext);for(const e of o.outputs)t.push(e.stdout);t.length&&(rt="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let s=0;s<G;s++){const u=s===G-1;!u||H||ot||rt||(rt="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const p=await m(T,b,{state:X,lastPromptTokens:Q});p>0&&b.debug&&z(`[debug] 已压缩上下文(${p} 条较早消息已摘要)`);let f=!1,d=!1;const h=new n,y=t=>{t&&(f||!d||W||K("\n"),f=!0,W?W.onDelta?.(t):K(t))},j=g(T,X);let U=j;if(!H&&j.length&&"system"===j[0]?.role){let t="";b.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),b.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),b.goal&&String(b.goal).trim()&&(t+=P(b.goal)),t&&(U=[{...j[0],content:String(j[0].content||"")+t},...j.slice(1)])}const A=[];it?A.push({role:"assistant",content:it},{role:"user",content:"你上一条回复因达到单轮输出长度上限而被截断,用户尚未看到剩余部分。请从截断处直接继续输出剩余内容:无缝衔接上文(若截断处在句子、代码块或表格中间,从中断字符处接着写),不要重复任何已输出的内容,不要道歉或解释,不要重新开头或总结前文。"}):rt&&A.push({role:"user",content:rt});const R=Date.now();let O;try{O=await o({baseUrl:b.baseUrl,apiKey:b.apiKey,model:V,messages:A.length?[...U,...A]:U,tools:ot||u?void 0:tt,maxTokens:b.maxTokens||8192,reasoningEffort:b.reasoningEffort,client:b.client,sessionId:b.sessionId},{onContent:t=>y(h.feed(t)),onReasoning:t=>{!1!==b.showReasoning&&(it||(W?W.onReasoning?.(t):(d||K(_.gray("\n[思考] ")),K(_.gray(t))),d=!0))},onRetry:(t,e,o)=>z(`模型响应异常,${Math.round(e/1e3)}s 后自动重试(第 ${t} 次):${o}`)},v)}catch(t){return"AbortError"===t.name?(W?W.onInfo?.("已中断"):K(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:B,completion:L},lastPromptTokens:Q}):(W?W.onInfo?.("✗ 模型调用失败:"+t.message):K("\n"+_.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,usage:{prompt:B,completion:L},lastPromptTokens:Q})}y(h.flush()),O.usage&&(B+=O.usage.prompt_tokens||0,L+=O.usage.completion_tokens||0,Q=O.usage.prompt_tokens||Q),b.debug&&z(`[debug] 轮${s+1} · ${b.model} · in ${O.usage?.prompt_tokens||0} / out ${O.usage?.completion_tokens||0} tok · ${Date.now()-R}ms · ${(O.toolCalls||[]).length} 工具调用`);const{text:C,sawMarkup:N}=r(O.content),D=N||h.sawMarkup;rt=null;const q=H?[]:O.toolCalls;if(!q.length){if(D&&!C.trim()&&!H){if(et<2){et++,2===et&&(ot=!0),rt=1===et?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",z(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${et} 次)`);continue}z("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}if(!H&&"length"===O.finishReason&&C&&st<3){st++,it+=C,b.debug&&z(`[debug] 输出达单轮长度上限,自动续写(第 ${st} 段)`);continue}f&&!W&&K("\n");const t=it?it+C:C;it="",t&&T.push({role:"assistant",content:t});const e=!t.trim()||x(t);if(lt&&e&&"length"!==O.finishReason&&!ot&&at<2){at++,b.debug&&z(`[debug] 检测到「宣告未落地」收尾(空正文或预告式),注入续跑纠偏(第 ${at} 次)`),T.push({role:"user",content:'你上一条只是说明"接下来要做什么",但既没有发起工具调用、也没有给出结果——用户还没拿到任何最终产物。请立即执行你刚才所说的步骤(需要就用 run_python/工具,不需要就直接算),并直接输出面向用户的完整最终结果(例如完整的按月汇总表与逐月核对表、差异结论),不要再只描述你打算做什么。'});continue}W&&W.onTurnEnd?.(t);const o=J?.();if(o){T.push({role:"user",content:o});continue}if(ct){const e=w("Stop",{cwd:I,result:t},ut);for(const t of e.outputs)z("钩子·Stop:"+E(t.stdout,160))}return{usage:{prompt:B,completion:L},lastPromptTokens:Q}}lt=!0,f&&!W&&K("\n"),W&&C&&W.onAssistantText?.(C),T.push({role:"assistant",content:it?it+C:C,tool_calls:q}),it="";const Y=q.map((t,e)=>{const o=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=Z.get(o),i=S.isActive()&&("write_file"===o||"edit_file"===o);return{tc:t,idx:e,name:o,args:n,argsError:r,extra:l,readOnly:l?!!l.readOnly:a.has(o),deferToIde:i}}),mt=t=>{for(let e=t;e<Y.length;e++)T.push({role:"tool",tool_call_id:Y[e].tc.id||`call_${e}`,content:"(用户中断,该工具未执行)"});return W?W.onInfo?.("已中断"):K(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:B,completion:L},lastPromptTokens:Q}},gt=async t=>{if(t.argsError)return t.argsError;let e;try{e=t.extra?await t.extra.run(t.args):await l(t.name,t.args,I,b)}catch(t){e=`工具执行失败:${t.message}`}return capToolResult(String(e))},pt=t=>{W?W.onToolStart?.(t.name,t.args):K(_.cyan(" ⏺ ")+i(t.name,t.args)+"\n")},ft=(t,e)=>{$(e)&&nt++,W?W.onToolResult?.(t.name,String(e)):K(_.gray(" "+E(e))+"\n"),T.push({role:"tool",tool_call_id:t.tc.id,content:String(e)})},dt=(t,e)=>{W?W.onSkip?.(t.name,t.args):K(_.gray(` ⊘ ${i(t.name,t.args)} — 已跳过\n`)),T.push({role:"tool",tool_call_id:t.tc.id,content:e||"未授权,未执行。"})},ht=t=>"edit_file"===t||"write_file"===t,yt=e=>{try{return t.readFileSync(e,"utf8")}catch{return""}},wt=(t,o)=>{const n=e.resolve(I,String(t.args.path)),r=c(o,yt(n),{path:String(t.args.path)});r&&(W?W.onFileDiff?.(r):K(r+"\n"))};let St=0;for(;St<Y.length;){if(v?.aborted)return mt(St);const o=Y[St];if(o.argsError){pt(o),ft(o,o.argsError),St++;continue}if(o.readOnly){let t=St;for(;t<Y.length&&Y[t].readOnly&&!Y[t].argsError;)t++;const e=Y.slice(St,t),o=await M(e,8,async t=>{const e=await k.request(t.name,t.args,I,!0);return e.allow?(pt(t),{item:t,result:await gt(t)}):{item:t,denied:e.reason||"未授权,未执行。"}});for(const t of o)null!=t.denied?dt(t.item,t.denied):ft(t.item,t.result);if(St=t,v?.aborted)return mt(St);continue}const n=await k.request(o.name,o.args,I,o.readOnly||o.deferToIde);if(v?.aborted)return mt(St);if(!n.allow){dt(o,n.reason),St++;continue}if(ct){const t=w("PreToolUse",{cwd:I,toolName:o.name,args:o.args},ut);for(const e of t.outputs)z("钩子·PreToolUse:"+E(e.stdout,120));if(t.block){dt(o,"PreToolUse 钩子阻断:"+t.reason),St++;continue}t.modifiedArgs&&(o.args={...o.args,...t.modifiedArgs},z("钩子·PreToolUse 已修改工具参数"))}pt(o);const r=ht(o.name)&&o.args?.path?yt(e.resolve(I,String(o.args.path))):null;if(F&&null!=r&&o.args?.path){const n=t.existsSync(e.resolve(I,String(o.args.path)));F.push(r.length>2097152?{path:String(o.args.path),tooLarge:!0}:{path:String(o.args.path),before:r,existed:n})}let s=await gt(o);if(ct){const t=w("PostToolUse",{cwd:I,toolName:o.name,args:o.args,result:String(s)},ut);if(t.outputs.length){for(const e of t.outputs)z("钩子·PostToolUse:"+E(e.stdout,120));s=String(s)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}ft(o,s),null!=r&&wt(o,r),St++}nt>=3&&!ot&&!rt&&(rt="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",nt=0,z("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const _t=J?.();_t&&T.push({role:"user",content:_t})}return it&&(T.push({role:"assistant",content:it}),W&&W.onTurnEnd?.(it)),W?W.onInfo?.("已达最大工具轮数,停止"):K(_.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:B,completion:L},lastPromptTokens:Q}}
1
+ import t from"node:fs";import e from"node:os";import n from"node:path";import{streamChat as o,getPlatformConfig as r}from"./api.js";import{MarkupFilter as s,stripMarkup as i}from"./markup-filter.js";import{TOOL_SPECS as a,READONLY_TOOLS as l,runTool as c}from"./tools.js";import{toolLabel as u,indent as m,renderFileDiff as f}from"./render.js";import{compactIfNeeded as g,buildModelMessages as p}from"./compaction.js";import{makeTodoTool as d,formatTodos as h,TODO_TOOL_NAME as y}from"./todo.js";import{makeDecisionTool as w,DECISION_TOOL_NAME as b,recordDecision as S,extractDecisionFromTurn as x}from"./decisions.js";import{makeMemoryTool as _}from"./memory.js";import{loadHooks as T,runHooks as k}from"./hooks.js";import*as M from"./ide.js";import{c as $}from"./colors.js";export const MAX_TOOL_RESULT_CHARS=6e4;export function toolConcurrency(t){const n=Number(t?.toolConcurrency);if(Number.isFinite(n)&&n>0)return Math.min(24,Math.floor(n));let o=4;try{o=e.cpus()?.length||4}catch{}return Math.max(8,Math.min(24,o-2))}const E=/context[_\s-]?length|maximum context|context window|too many tokens|reduce the length|prompt is too long|上下文.*(超|过长|超出|溢出)|输入.*过长|token.*(超出|超过).*(上限|限制)/i;export function isContextOverflowError(t){return E.test(String(t||""))}const P=/(现在|接下来|下面|这就|马上|我来|让我|我要|我先|我这就|咱们|让我们|稍等|待我)/,j=/(做|进行|生成|编写|写(一|个|完|出)?|制作|计算|统计|核对|汇总|整理|分析|梳理|输出|列出|准备|运行|执行|开始)/,I=/\b(let me|now let me|i.?ll|i will|let.?s|next,? i)\b[^.!?\n]*\b(write|create|generat|calculat|comput|do|produc|compil|analyz|summar|prepar|run|make|list)\b/i;function v(t){const e=String(t||"").trim().split(/\n+/).map(t=>t.trim()).filter(Boolean),n=e[e.length-1]||"";return!(!n||n.length>120)&&(!!I.test(n)||P.test(n)&&j.test(n))}function C(t){const e=String(t);return e.startsWith("工具执行失败")||e.startsWith("命令启动失败")||e.includes("不是合法 JSON")||e.includes("未找到要替换的内容")||e.includes("路径超出项目目录")||e.includes("是目录而非文件")}function A(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}const U="【用户中断】",O="\n\n# 上一轮被用户中断(重要)\n上一回合被用户主动中断:会话里标注「"+U+"」的工具调用并未真正执行、没有任何结果。请勿臆测这些未完成工具的输出,也不要假设被中断的工作已完成或推断其结论;一切以用户最新一条消息为准——如与被中断的工作无关,就按新指令重新开始,需要时先与用户确认再继续。";function R(t){if(!Array.isArray(t))return!1;let e=-1;for(let n=t.length-1;n>=0;n--)if("user"===t[n].role){e=n;break}if(e<=0)return!1;for(let n=e-1;n>=0;n--){const e=t[n];if("user"===e.role)break;if("tool"===e.role&&"string"==typeof e.content&&e.content.includes(U))return!0}return!1}function N(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function D(t,e,n){const o=new Array(t.length);let r=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;r<t.length;){const e=r++;o[e]=await n(t[e],e)}})),o}export function isPlatformModel(t){return/^(agent|workflow|datahuman|squad)-/.test(String(t||""))}function J(t,e=200){const n=String(t).split("\n")[0];return n.length>e?n.slice(0,e)+"…":n}export function capToolResult(t){const e=String(t);if(e.length<=6e4)return e;const n=e.length-48e3-9e3;return e.slice(0,48e3)+`\n…【工具输出过长,中间已省略 ${n} 字符】…\n`+e.slice(-9e3)}export async function runTurn({config:e,messages:E,permissions:P,cwd:j,ask:I,signal:q,extraTools:F=[],depth:K=0,io:L,events:W,todoState:z,compaction:B,pullQueued:H,maxIters:Q,fileJournal:X}){const G=L&&L.write||(t=>process.stdout.write(t)),V=W||null,Y=t=>{V?V.onInfo?.(t):G($.gray(" · "+t)+"\n")};let Z=0,tt=0,et=0;const nt=Number(Q||e.maxTurns)||120,ot=isPlatformModel(e.model),rt={low:24e4,medium:36e4,high:6e5}[e.reasoningEffort];let st=!1,it=String(e.fallbackModel||"").trim(),at=0,lt=e.model;if(0===K&&e.autoModelStrong&&!ot){const t=[...E].reverse().find(t=>"user"===t.role),n=N(t?.content);(e.planMode||n.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(n))&&e.autoModelStrong!==e.model&&(lt=e.autoModelStrong,e.debug&&Y(`[automodel] 复杂回合升配 → ${lt}`))}const ct=B||{},ut=[...F];if(0===K&&!ot&&!ut.some(t=>t.spec?.function?.name===y)){const t=z||{list:[]};ut.push(d({state:t,emit:t=>{V?V.onTodo?V.onTodo(t.map(t=>({...t}))):V.onInfo?.("任务清单:\n"+h(t)):G($.gray(m(h(t)," "))+"\n")}}))}if(0!==K||ot||!1===e.memoryEnabled||ut.some(t=>"memory_write"===t.spec?.function?.name)||ut.push(_({cwd:j,emit:t=>{V?V.onMemory?V.onMemory(t):V.onInfo?.("已记入长期记忆:"+J(t,80)):G($.gray(" ✎ 已记入长期记忆:"+J(t,80))+"\n")}})),0===K&&!ot&&!1!==e.decisionsEnabled&&!ut.some(t=>t.spec?.function?.name===b)){const t=E.filter(t=>"user"===t.role).length;ut.push(w({cwd:j,sessionId:e.sessionId||"",turn:t,emit:t=>{St=!0,V?V.onDecision?V.onDecision(t):V.onInfo?.("已记录决策:"+J(t.title,80)):G($.gray(" ◆ 已记录决策:"+J(t.title,80))+"\n")}}))}const mt=new Map;for(const t of ut)mt.set(t.spec.function.name,t);const ft=ot?void 0:[...a,...ut.map(t=>t.spec)];let gt=0,pt=!1,dt=0,ht=null,yt=0,wt=0,bt=!1,St=!1;const xt=new Set;let _t="";const Tt=0!==K||ot?null:T(j),kt=!(!Tt||!Tt.all.length);if(kt){const t=[];if(E.filter(t=>"user"===t.role).length<=1){const e=k("SessionStart",{cwd:j},Tt);e.injectContext&&t.push(e.injectContext);for(const n of e.outputs)t.push(n.stdout)}const e=[...E].reverse().find(t=>"user"===t.role),n=k("UserPromptSubmit",{cwd:j,prompt:N(e?.content)},Tt);if(n.block)return Y("提交被 UserPromptSubmit 钩子阻断:"+n.reason),{error:"提交被钩子阻断:"+n.reason,usage:{prompt:Z,completion:tt},lastPromptTokens:et};n.injectContext&&t.push(n.injectContext);for(const e of n.outputs)t.push(e.stdout);t.length&&(ht="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let a=0;a<nt;a++){const m=a===nt-1;!m||ot||pt||ht||(ht="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const d=await g(E,e,{state:ct,lastPromptTokens:et});d>0&&e.debug&&Y(`[debug] 已压缩上下文(${d} 条较早消息已摘要)`);let h=!1,y=!1;const w=new s,b=t=>{t&&(h||!y||V||G("\n"),h=!0,V?V.onDelta?.(t):G(t))},_=p(E,ct);let T=_;if(!ot&&_.length&&"system"===_[0]?.role){let t="";e.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),e.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),e.goal&&String(e.goal).trim()&&(t+=A(e.goal)),R(E)&&(t+=O),t&&(T=[{..._[0],content:String(_[0].content||"")+t},..._.slice(1)])}const I=[];_t?I.push({role:"assistant",content:_t},{role:"user",content:"你上一条回复因达到单轮输出长度上限而被截断,用户尚未看到剩余部分。请从截断处直接继续输出剩余内容:无缝衔接上文(若截断处在句子、代码块或表格中间,从中断字符处接着写),不要重复任何已输出的内容,不要道歉或解释,不要重新开头或总结前文。"}):ht&&I.push({role:"user",content:ht});const N=Date.now();let F,L=!1,W="";try{F=await o({baseUrl:e.baseUrl,apiKey:e.apiKey,model:lt,messages:I.length?[...T,...I]:T,tools:pt||m?void 0:ft,maxTokens:e.maxTokens||8192,reasoningEffort:e.reasoningEffort,stallTimeoutMs:rt,client:e.client,sessionId:e.sessionId,aux:e.aux||void 0,continuation:!!_t,tailCount:I.length},{onContent:t=>{L=!0,W+=t,b(w.feed(t))},onReasoning:t=>{!1!==e.showReasoning&&(_t||(V?V.onReasoning?.(t):(y||G($.gray("\n[思考] ")),G($.gray(t))),y=!0))},onRetry:(t,n,o)=>{e.debug&&Y(`[retry] 模型响应异常,${Math.round(n/1e3)}s 后自动重试(第 ${t} 次):${o}`)}},q)}catch(t){if("AbortError"===t.name)return V?V.onInfo?.("已中断"):G($.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:Z,completion:tt},lastPromptTokens:et};if(!ot&&!q?.aborted&&isContextOverflowError(t.message)&&at<3){at++;const t=await g(E,e,{state:ct,force:!0});if(t>0){L&&W&&(_t+=W),e.debug&&Y(`[overflow] 上下文超出模型窗口,已强制压缩 ${t} 条较早消息后重试`);continue}e.debug&&Y("[overflow] 上下文超限但无可压缩内容,转兜底模型")}if(!ot&&!st&&!q?.aborted){if(!it)try{const t=await r(e.baseUrl,e.apiKey);it=String(t&&t.fallback_model||"").trim()}catch{}if(it&&it!==lt){st=!0,lt=it,L&&W&&(_t+=W),e.debug&&Y(`[fallback] 主模型失败(${t.message}),静默切兜底模型 ${it}${L?"(续写)":""}`);continue}}return V?V.onInfo?.("✗ 模型调用失败:"+t.message):G("\n"+$.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,usage:{prompt:Z,completion:tt},lastPromptTokens:et}}b(w.flush()),F.usage&&(Z+=F.usage.prompt_tokens||0,tt+=F.usage.completion_tokens||0,et=F.usage.prompt_tokens||et,0===K&&V&&V.onUsage?.({lastPromptTokens:et,prompt:Z,completion:tt})),e.debug&&Y(`[debug] 轮${a+1} · ${e.model} · in ${F.usage?.prompt_tokens||0} / out ${F.usage?.completion_tokens||0} tok · ${Date.now()-N}ms · ${(F.toolCalls||[]).length} 工具调用`);const{text:z,sawMarkup:B}=i(F.content),Q=B||w.sawMarkup;ht=null;const ut=ot?[]:F.toolCalls;if(!ut.length){if(Q&&!z.trim()&&!ot){if(gt<2){gt++,2===gt&&(pt=!0),ht=1===gt?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",Y(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${gt} 次)`);continue}Y("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}if(!ot&&"length"===F.finishReason&&z&&yt<3){yt++,_t+=z,e.debug&&Y(`[debug] 输出达单轮长度上限,自动续写(第 ${yt} 段)`);continue}h&&!V&&G("\n");const t=_t?_t+z:z;_t="",t&&E.push({role:"assistant",content:t});const n=!t.trim()||v(t);if(bt&&n&&"length"!==F.finishReason&&!pt&&wt<2){wt++,e.debug&&Y(`[debug] 检测到「宣告未落地」收尾(空正文或预告式),注入续跑纠偏(第 ${wt} 次)`),E.push({role:"user",content:'你上一条只是说明"接下来要做什么",但既没有发起工具调用、也没有给出结果——用户还没拿到任何最终产物。请立即执行你刚才所说的步骤(需要就用 run_python/工具,不需要就直接算),并直接输出面向用户的完整最终结果(例如完整的按月汇总表与逐月核对表、差异结论),不要再只描述你打算做什么。'});continue}V&&V.onTurnEnd?.(t);const o=H?.();if(o){E.push({role:"user",content:o});continue}if(kt){const e=k("Stop",{cwd:j,result:t},Tt);for(const t of e.outputs)Y("钩子·Stop:"+J(t.stdout,160))}if(!ot&&e.decisionAutoExtract&&!1!==e.decisionsEnabled&&bt&&!St&&t&&t.trim())try{const n=[...E].reverse().find(t=>"user"===t.role),o=n&&"string"==typeof n.content?n.content:"",r=await x(e,{userText:o,assistantText:t,toolNames:[...xt]}),s="number"==typeof e.decisionExtractMinConfidence?e.decisionExtractMinConfidence:.55;if(r&&(r.confidence??0)>=s){const t=E.filter(t=>"user"===t.role).length,n=S(j,{...r,title:r.what,status:"draft",origin:"agent",confidence:Math.min(r.confidence??.5,.7),sessionId:e.sessionId||"",turn:t},{origin:"agent"});V?V.onDecision?V.onDecision(n):V.onInfo?.("已抽取决策草稿(待确认):"+J(n.title,80)):G($.gray(" ◇ 已抽取决策草稿(待确认):"+J(n.title,80))+"\n")}}catch{}return{usage:{prompt:Z,completion:tt},lastPromptTokens:et}}bt=!0,h&&!V&&G("\n"),V&&z&&V.onAssistantText?.(z),E.push({role:"assistant",content:_t?_t+z:z,tool_calls:ut}),_t="";const Mt=ut.map((t,e)=>{const n=t.function?.name||"";let o={},r=null;const s=t.function?.arguments||"";if(s)try{o=JSON.parse(s)}catch(t){r=`工具参数不是合法 JSON(${t.message}):${String(s).slice(0,200)}…请修正参数后重新调用。`}const i=mt.get(n),a=M.isActive()&&("write_file"===n||"edit_file"===n);return{tc:t,idx:e,name:n,args:o,argsError:r,extra:i,readOnly:i?!!i.readOnly:l.has(n),deferToIde:a}}),$t=t=>{for(let e=t;e<Mt.length;e++)E.push({role:"tool",tool_call_id:Mt[e].tc.id||`call_${e}`,content:U+" 该工具未执行,无任何结果,请勿臆测其输出。"});return V?V.onInfo?.("已中断"):G($.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:Z,completion:tt},lastPromptTokens:et}},Et=async t=>{if(t.argsError)return t.argsError;let n;try{n=t.extra?await t.extra.run(t.args):await c(t.name,t.args,j,e,q)}catch(t){n=`工具执行失败:${t.message}`}return capToolResult(String(n))},Pt=t=>{xt.add(t.name),V?V.onToolStart?.(t.name,t.args):G($.cyan(" ⏺ ")+u(t.name,t.args)+"\n")},jt=(t,e)=>{C(e)&&dt++,V?V.onToolResult?.(t.name,String(e)):G($.gray(" "+J(e))+"\n"),E.push({role:"tool",tool_call_id:t.tc.id,content:String(e)})},It=(t,e)=>{V?V.onSkip?.(t.name,t.args):G($.gray(` ⊘ ${u(t.name,t.args)} — 已跳过\n`)),E.push({role:"tool",tool_call_id:t.tc.id,content:e||"未授权,未执行。"})},vt=t=>"edit_file"===t||"write_file"===t,Ct=e=>{try{return t.readFileSync(e,"utf8")}catch{return""}},At=(t,e)=>{const o=n.resolve(j,String(t.args.path)),r=f(e,Ct(o),{path:String(t.args.path)});r&&(V?V.onFileDiff?.(r):G(r+"\n"))};let Ut=0;for(;Ut<Mt.length;){if(q?.aborted)return $t(Ut);const o=Mt[Ut];if(o.argsError){Pt(o),jt(o,o.argsError),Ut++;continue}if(o.readOnly){let t=Ut;for(;t<Mt.length&&Mt[t].readOnly&&!Mt[t].argsError;)t++;const n=Mt.slice(Ut,t),o=await D(n,toolConcurrency(e),async t=>{const e=await P.request(t.name,t.args,j,!0);return e.allow?(Pt(t),{item:t,result:await Et(t)}):{item:t,denied:e.reason||"未授权,未执行。"}});for(const t of o)null!=t.denied?It(t.item,t.denied):jt(t.item,t.result);if(Ut=t,q?.aborted)return $t(Ut);continue}const r=await P.request(o.name,o.args,j,o.readOnly||o.deferToIde);if(q?.aborted)return $t(Ut);if(!r.allow){It(o,r.reason),Ut++;continue}if(kt){const t=k("PreToolUse",{cwd:j,toolName:o.name,args:o.args},Tt);for(const e of t.outputs)Y("钩子·PreToolUse:"+J(e.stdout,120));if(t.block){It(o,"PreToolUse 钩子阻断:"+t.reason),Ut++;continue}t.modifiedArgs&&(o.args={...o.args,...t.modifiedArgs},Y("钩子·PreToolUse 已修改工具参数"))}Pt(o);const s=vt(o.name)&&o.args?.path?Ct(n.resolve(j,String(o.args.path))):null;if(X&&null!=s&&o.args?.path){const e=t.existsSync(n.resolve(j,String(o.args.path)));X.push(s.length>2097152?{path:String(o.args.path),tooLarge:!0}:{path:String(o.args.path),before:s,existed:e})}let i=await Et(o);if(kt){const t=k("PostToolUse",{cwd:j,toolName:o.name,args:o.args,result:String(i)},Tt);if(t.outputs.length){for(const e of t.outputs)Y("钩子·PostToolUse:"+J(e.stdout,120));i=String(i)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}jt(o,i),null!=s&&At(o,s),Ut++}dt>=3&&!pt&&!ht&&(ht="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",dt=0,Y("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const Ot=H?.();Ot&&E.push({role:"user",content:Ot})}return _t&&(E.push({role:"assistant",content:_t}),V&&V.onTurnEnd?.(_t)),V?V.onInfo?.("已达最大工具轮数,停止"):G($.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:Z,completion:tt},lastPromptTokens:et}}
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}`}}function o(t,e){try{const e=JSON.parse(t),n=e?.error?.type||e?.type||"";if(e?.error?.message)return{message:e.error.message,type:n};if(e?.message)return{message:e.message,type:n};if(e?.msg)return{message:e.msg,type:n}}catch{}return{message:t||e,type:""}}async function r(t){let e="";try{e=await t.text()}catch{}return o(e,`HTTP ${t.status}`)}function a(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 s(t,e){return new Promise((n,o)=>{if(e?.aborted)return o(i());const r=setTimeout(()=>{e?.removeEventListener?.("abort",a),n()},t),a=()=>{clearTimeout(r),o(i())};e?.addEventListener?.("abort",a,{once:!0})})}function i(){const t=new Error("已中断");return t.name="AbortError",t}function c(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,o={},a){const u=`${e.baseUrl}/v1/chat/completions`,l={model:e.model,messages:e.messages,stream:!0,stream_options:{include_usage:!0}};e.tools&&e.tools.length&&(l.tools=e.tools,l.tool_choice="auto"),"number"==typeof e.temperature&&(l.temperature=e.temperature),Number.isFinite(e.maxTokens)&&e.maxTokens>0&&(l.max_tokens=Math.floor(e.maxTokens)),e.reasoningEffort&&"off"!==e.reasoningEffort&&(l.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 h=JSON.stringify(l);for(let n=0;;n++){let l;try{l=await fetch(u,{method:"POST",headers:m,body:h,signal:a})}catch(t){if("AbortError"===t?.name||a?.aborted)throw i();if(n<2){const e=c(n,null);o.onRetry?.(n+1,e,"网络错误:"+(t?.message||t)),await s(e,a);continue}throw new Error("网络错误:"+(t?.message||t))}if(!l.ok||!l.body){const e=await r(l);if(!("usage_limit_exceeded"===e.type)&&t.has(l.status)&&n<2){const t=c(n,l);o.onRetry?.(n+1,t,`HTTP ${l.status}:${e.message}`),await s(t,a);continue}throw new Error(e.message)}return f(l,o,a,e.stallTimeoutMs??9e4)}}async function f(t,e,n,o){const r=t.body.getReader(),s=new TextDecoder;let c="",f="";const u=[];let l=null,m=null,h=!1,g=null;const d=()=>{g&&clearTimeout(g),g=setTimeout(()=>{h=!0,r.cancel().catch(()=>{})},o)},y=()=>r.cancel().catch(()=>{});n?.addEventListener?.("abort",y,{once:!0});try{for(d();;){let t;try{t=await r.read()}catch(t){if(n?.aborted)throw i();if(h)throw new Error(`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`);throw t}if(n?.aborted)throw i();if(h)throw new Error(`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`);const{done:g,value:y}=t;if(g)break;d(),c+=s.decode(y,{stream:!0});const p=c.split("\n");c=p.pop()??"";for(const t of p){const n=t.trim();if(!n.startsWith("data:"))continue;const o=n.slice(5).trim();if("[DONE]"===o||""===o)continue;let r;try{r=JSON.parse(o)}catch{continue}r.usage&&(m=r.usage);const s=r.choices?.[0];if(!s)continue;const i=s.delta||{},c=i.reasoning_content??i.reasoning;c&&e.onReasoning?.(c),i.content&&(f+=i.content,e.onContent?.(i.content)),Array.isArray(i.tool_calls)&&a(u,i.tool_calls),s.finish_reason&&(l=s.finish_reason)}}}finally{g&&clearTimeout(g),n?.removeEventListener?.("abort",y)}return{content:f,toolCalls:u.filter(Boolean),finishReason:l,usage:m}}export async function embed(t,e,o,a){const s=await fetch(`${t}/v1/embeddings`,{method:"POST",headers:n(e),body:JSON.stringify({model:o,input:Array.isArray(a)?a:[a]}),signal:AbortSignal.timeout(3e4)});if(!s.ok)throw new Error((await r(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,o,a){const s=await fetch(`${t}/v1/knowledge/${encodeURIComponent(o)}/sync`,{method:"POST",headers:n(e),body:JSON.stringify({files:a}),signal:AbortSignal.timeout(12e4)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}export async function listModels(t,o){const a=await fetch(`${t}/v1/models`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await r(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,o,a){const s=await fetch(`${t}/v1/agents/${encodeURIComponent(a)}/persona`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}export async function fetchUsage(t,o){const a=await fetch(`${t}/v1/usage`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await r(a)).message);return a.json()}export async function extractDocument(t,e,o,a){const s=o.bytes||(o.data?Buffer.from(String(o.data),"base64"):null);if(s&&!1!==l.get(t))try{const{body:r,contentType:i}=function(t,e,n,o){const r="----huicai"+Date.now().toString(36)+Math.random().toString(36).slice(2,10),a=String(t||"file").replace(/[\r\n"]/g,"_"),s=[];s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="file"; filename="${a}"\r\nContent-Type: application/octet-stream\r\n\r\n`)),s.push(e,Buffer.from("\r\n"));for(const[t,e]of[["maxChars",n],["offset",o]])null!=e&&s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="${t}"\r\n\r\n${e}\r\n`));return s.push(Buffer.from(`--${r}--\r\n`)),{body:Buffer.concat(s),contentType:`multipart/form-data; boundary=${r}`}}(o.filename,s,o.maxChars,o.offset),c=u(await d(`${t}/v1/files/extract`,{...n(e),"content-type":i},r,a),!0);return l.set(t,!0),c}catch(e){if(!e||!e._noMultipart)throw e;l.set(t,!1)}const i=o.bytes?{...o,bytes:void 0,data:o.bytes.toString("base64")}:o,c=JSON.stringify(i);if("function"==typeof a){return u(await d(`${t}/v1/files/extract`,{...n(e)},Buffer.from(c),a),!1)}const f=await fetch(`${t}/v1/files/extract`,{method:"POST",headers:n(e),body:c,signal:AbortSignal.timeout(12e4)});if(!f.ok)throw new Error((await r(f)).message);return u(await f.text(),!1)}function u(t,e){let n=null;try{n=JSON.parse(t)}catch{}if(n&&"string"==typeof n.text)return n;const o=n&&(n.message||n.msg||n.error?.message)||"解析服务返回异常响应",r=new Error(o);throw e&&(r._noMultipart=!0),r}const l=new Map;const m=12e4,h=25,g=9e5;async function d(t,e,n,r){const{request:a}=await import("http:"===new URL(t).protocol?"node:http":"node:https"),s={};for(const[t,n]of Object.entries(e||{}))s[String(t).toLowerCase()]=n;return s["content-type"]||(s["content-type"]="application/json"),s["content-length"]=n.length,new Promise((e,i)=>{const c=a(t,{method:"POST",headers:s,agent:!1},t=>{const n=[];t.on("data",t=>n.push(t)),t.on("error",i),t.on("end",()=>{const r=Buffer.concat(n).toString("utf8");t.statusCode>=200&&t.statusCode<300?e(r):415===t.statusCode||405===t.statusCode?i(Object.assign(new Error(o(r,`HTTP ${t.statusCode}`).message),{_noMultipart:!0})):i(new Error(o(r,`HTTP ${t.statusCode}`).message))})}),f=Math.min(g,m+Math.floor(n.length/h));c.setTimeout(f,()=>c.destroy(new Error(`网络传输/服务端处理超时(已等待 ${Math.round(f/1e3)} 秒无响应),请检查网络后重试`))),c.on("error",i);let u=0;const l=()=>{for(;u<n.length;){const t=Math.min(u+262144,n.length),e=c.write(n.subarray(u,t));u=t;try{r&&r(u,n.length)}catch{}if(!e)return void c.once("drain",l)}c.end()};l()})}export function startParseTicker(t,e,n=200){const o=Math.max(600,400+t/15360),r=Date.now(),a=setInterval(()=>{const t=Date.now()-r,n=Math.min(99,Math.round(100*(1-Math.exp(-t/o))));try{e(n,t)}catch{}},n);return()=>clearInterval(a)}export async function listPlatformSkills(t,o){const a=await fetch(`${t}/v1/skills`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await r(a)).message);const s=await a.json();return Array.isArray(s?.data)?s.data:[]}export async function getPlatformSkill(t,o,a){const s=await fetch(`${t}/v1/skills/${encodeURIComponent(a)}`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}export async function pushPlatformSkill(t,o,a){const s=await fetch(`${t}/v1/skills/import`,{method:"POST",headers:{...n(o),"Content-Type":"application/json"},body:JSON.stringify(a),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await r(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}`}}function o(t,e){try{const e=JSON.parse(t),n=e?.error?.type||e?.type||"";if(e?.error?.message)return{message:e.error.message,type:n};if(e?.message)return{message:e.message,type:n};if(e?.msg)return{message:e.msg,type:n}}catch{}return{message:t||e,type:""}}async function r(t){let e="";try{e=await t.text()}catch{}return o(e,`HTTP ${t.status}`)}function a(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 s(t,e){return new Promise((n,o)=>{if(e?.aborted)return o(i());const r=setTimeout(()=>{e?.removeEventListener?.("abort",a),n()},t),a=()=>{clearTimeout(r),o(i())};e?.addEventListener?.("abort",a,{once:!0})})}function i(){const t=new Error("已中断");return t.name="AbortError",t}function c(t,e){const n=new AbortController;let o=!1;const r=()=>n.abort();t&&(t.aborted?n.abort():t.addEventListener("abort",r,{once:!0}));const a=setTimeout(()=>{o=!0,n.abort()},e);return{signal:n.signal,ms:e,get timedOut(){return o},done(){clearTimeout(a),t?.removeEventListener?.("abort",r)}}}function u(t){const e=String(t?.message||t||""),n=t?.cause?.code||t?.code||"";return"ECONNREFUSED"===n||/ECONNREFUSED|connection refused/i.test(e)?"无法连接模型服务(连接被拒绝):请检查网络或稍后重试。":"ENOTFOUND"===n||"EAI_AGAIN"===n||/getaddrinfo|ENOTFOUND|EAI_AGAIN/i.test(e)?"无法解析模型服务地址(DNS 异常):请检查网络连接后重试。":"ETIMEDOUT"===n||/ETIMEDOUT|timeout|timed out/i.test(e)?"连接模型服务超时:网络较慢或服务繁忙,请稍后重试。":"ECONNRESET"===n||/ECONNRESET|socket hang up|network|fetch failed/i.test(e)?"与模型服务的连接中断:网络波动,请稍后重试。":"网络错误:"+e}function f(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,o={},a){const m=`${e.baseUrl}/v1/chat/completions`,d={model:e.model,messages:e.messages,stream:!0,stream_options:{include_usage:!0}};e.tools&&e.tools.length&&(d.tools=e.tools,d.tool_choice="auto"),"number"==typeof e.temperature&&(d.temperature=e.temperature),Number.isFinite(e.maxTokens)&&e.maxTokens>0&&(d.max_tokens=Math.floor(e.maxTokens)),e.reasoningEffort&&"off"!==e.reasoningEffort&&(d.reasoning_effort=e.reasoningEffort),e.extraBody&&"object"==typeof e.extraBody&&Object.assign(d,e.extraBody);const g=n(e.apiKey);e.client&&(g["X-Huicai-Client"]=e.client),e.sessionId&&(g["X-Huicai-Session"]=e.sessionId),e.aux&&(g["X-Huicai-Aux"]="1"),e.continuation&&(g["X-Huicai-Continue"]="1"),e.tailCount>0&&(g["X-Huicai-Tail"]=String(e.tailCount));const h=JSON.stringify(d);for(let n=0;;n++){let d;const y=c(a,e.headersTimeoutMs??12e4);try{d=await fetch(m,{method:"POST",headers:g,body:h,signal:y.signal})}catch(t){if(a?.aborted)throw i();if(y.timedOut){const t=`等待模型服务响应超时(${Math.round(y.ms/1e3)}s 内未返回响应头)`;if(n<2){const e=f(n,null);o.onRetry?.(n+1,e,t),await s(e,a);continue}throw new Error(t+",请稍后重试。")}if("AbortError"===t?.name)throw i();if(n<2){const e=f(n,null);o.onRetry?.(n+1,e,u(t)),await s(e,a);continue}throw new Error(u(t))}finally{y.done()}if(!d.ok||!d.body){const e=await r(d);if(!("usage_limit_exceeded"===e.type)&&t.has(d.status)&&n<2){const t=f(n,d);o.onRetry?.(n+1,t,`HTTP ${d.status}:${e.message}`),await s(t,a);continue}throw new Error(e.message)}return l(d,o,a,e.stallTimeoutMs??18e4)}}async function l(t,e,n,o){const r=t.body.getReader(),s=new TextDecoder;let c="",u="";const f=[];let l=null,m=null,d=!1,g=null,h=!1;const y=()=>{g&&clearTimeout(g),g=setTimeout(()=>{d=!0,r.cancel().catch(()=>{})},o)},w=()=>{h=!0,y()},p=()=>new Error(h?`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`:`模型 ${Math.round(o/1e3)}s 内没有任何输出,已中止(可重试)`),b=()=>r.cancel().catch(()=>{});n?.addEventListener?.("abort",b,{once:!0});try{for(y();;){let t;try{t=await r.read()}catch(t){if(n?.aborted)throw i();if(d)throw p();throw t}if(n?.aborted)throw i();if(d)throw p();const{done:o,value:g}=t;if(o)break;c+=s.decode(g,{stream:!0});const h=c.split("\n");c=h.pop()??"";for(const t of h){const n=t.trim();if(!n.startsWith("data:"))continue;const o=n.slice(5).trim();if("[DONE]"===o||""===o)continue;let r;try{r=JSON.parse(o)}catch{continue}if(r.error){const t=r.error&&(r.error.message||r.error.msg||r.error);throw new Error("模型返回错误:"+("string"==typeof t?t:JSON.stringify(t)))}r.usage&&(m=r.usage,w());const s=r.choices?.[0];if(!s)continue;const i=s.delta||{},c=i.reasoning_content??i.reasoning;c&&(w(),e.onReasoning?.(c)),i.content&&(w(),u+=i.content,e.onContent?.(i.content)),Array.isArray(i.tool_calls)&&(w(),a(f,i.tool_calls)),s.finish_reason&&(w(),l=s.finish_reason)}}}finally{g&&clearTimeout(g),n?.removeEventListener?.("abort",b)}return{content:u,toolCalls:f.filter(Boolean),finishReason:l,usage:m}}export async function embed(t,e,o,a){const s=await fetch(`${t}/v1/embeddings`,{method:"POST",headers:n(e),body:JSON.stringify({model:o,input:Array.isArray(a)?a:[a]}),signal:AbortSignal.timeout(3e4)});if(!s.ok)throw new Error((await r(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,o,a){const s=await fetch(`${t}/v1/knowledge/${encodeURIComponent(o)}/sync`,{method:"POST",headers:n(e),body:JSON.stringify({files:a}),signal:AbortSignal.timeout(12e4)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}export async function pushDecisions(t,e,o,a){const s=await fetch(`${t}/v1/decisions/sync`,{method:"POST",headers:n(e),body:JSON.stringify({projectKey:o,nodes:a}),signal:AbortSignal.timeout(6e4)});if(!s.ok)throw new Error((await r(s)).message);const i=await s.json();return Number(i?.accepted||0)}export async function pullDecisions(t,e,o,a){const s=new URLSearchParams({projectKey:o});a&&s.set("since",String(a));const i=await fetch(`${t}/v1/decisions?${s.toString()}`,{headers:n(e),signal:AbortSignal.timeout(6e4)});if(!i.ok)throw new Error((await r(i)).message);const c=await i.json();return Array.isArray(c?.data)?c.data:[]}export async function listModels(t,o){const a=await fetch(`${t}/v1/models`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await r(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,o,a){const s=await fetch(`${t}/v1/agents/${encodeURIComponent(a)}/persona`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}export async function fetchUsage(t,o){const a=await fetch(`${t}/v1/usage`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await r(a)).message);return a.json()}export async function extractDocument(t,e,o,a){const s=o.bytes||(o.data?Buffer.from(String(o.data),"base64"):null);if(s&&!1!==d.get(t))try{const{body:r,contentType:i}=function(t,e,n,o){const r="----huicai"+Date.now().toString(36)+Math.random().toString(36).slice(2,10),a=String(t||"file").replace(/[\r\n"]/g,"_"),s=[];s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="file"; filename="${a}"\r\nContent-Type: application/octet-stream\r\n\r\n`)),s.push(e,Buffer.from("\r\n"));for(const[t,e]of[["maxChars",n],["offset",o]])null!=e&&s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="${t}"\r\n\r\n${e}\r\n`));return s.push(Buffer.from(`--${r}--\r\n`)),{body:Buffer.concat(s),contentType:`multipart/form-data; boundary=${r}`}}(o.filename,s,o.maxChars,o.offset),c=m(await w(`${t}/v1/files/extract`,{...n(e),"content-type":i},r,a),!0);return d.set(t,!0),c}catch(e){if(!e||!e._noMultipart)throw e;d.set(t,!1)}const i=o.bytes?{...o,bytes:void 0,data:o.bytes.toString("base64")}:o,c=JSON.stringify(i);if("function"==typeof a){return m(await w(`${t}/v1/files/extract`,{...n(e)},Buffer.from(c),a),!1)}const u=await fetch(`${t}/v1/files/extract`,{method:"POST",headers:n(e),body:c,signal:AbortSignal.timeout(12e4)});if(!u.ok)throw new Error((await r(u)).message);return m(await u.text(),!1)}function m(t,e){let n=null;try{n=JSON.parse(t)}catch{}if(n&&"string"==typeof n.text)return n;const o=n&&(n.message||n.msg||n.error?.message)||"解析服务返回异常响应",r=new Error(o);throw e&&(r._noMultipart=!0),r}const d=new Map;const g=12e4,h=25,y=9e5;async function w(t,e,n,r){const{request:a}=await import("http:"===new URL(t).protocol?"node:http":"node:https"),s={};for(const[t,n]of Object.entries(e||{}))s[String(t).toLowerCase()]=n;return s["content-type"]||(s["content-type"]="application/json"),s["content-length"]=n.length,new Promise((e,i)=>{const c=a(t,{method:"POST",headers:s,agent:!1},t=>{const n=[];t.on("data",t=>n.push(t)),t.on("error",i),t.on("end",()=>{const r=Buffer.concat(n).toString("utf8");t.statusCode>=200&&t.statusCode<300?e(r):415===t.statusCode||405===t.statusCode?i(Object.assign(new Error(o(r,`HTTP ${t.statusCode}`).message),{_noMultipart:!0})):i(new Error(o(r,`HTTP ${t.statusCode}`).message))})}),u=Math.min(y,g+Math.floor(n.length/h));c.setTimeout(u,()=>c.destroy(new Error(`网络传输/服务端处理超时(已等待 ${Math.round(u/1e3)} 秒无响应),请检查网络后重试`))),c.on("error",i);let f=0;const l=()=>{for(;f<n.length;){const t=Math.min(f+262144,n.length),e=c.write(n.subarray(f,t));f=t;try{r&&r(f,n.length)}catch{}if(!e)return void c.once("drain",l)}c.end()};l()})}export function startParseTicker(t,e,n=200){const o=Math.max(600,400+t/15360),r=Date.now(),a=setInterval(()=>{const t=Date.now()-r,n=Math.min(99,Math.round(100*(1-Math.exp(-t/o))));try{e(n,t)}catch{}},n);return()=>clearInterval(a)}export async function listPlatformSkills(t,o){const a=await fetch(`${t}/v1/skills`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await r(a)).message);const s=await a.json();return Array.isArray(s?.data)?s.data:[]}export async function getPlatformSkill(t,o,a){const s=await fetch(`${t}/v1/skills/${encodeURIComponent(a)}`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}export async function pushPlatformSkill(t,o,a){const s=await fetch(`${t}/v1/skills/import`,{method:"POST",headers:{...n(o),"Content-Type":"application/json"},body:JSON.stringify(a),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}
@@ -0,0 +1 @@
1
+ import e from"node:fs";import s from"node:path";import t from"node:os";import{parseFrontmatter as i}from"./skills.js";export const EXTERNAL_SOURCES=[{cli:"Claude Code",agents:".claude/agents",skills:".claude/skills"},{cli:"Codex",agents:".codex/agents",skills:".codex/skills"},{cli:"Gemini CLI",agents:".gemini/agents",skills:".gemini/skills"},{cli:"Cursor",agents:".cursor/agents",skills:".cursor/skills"}];const n=new Set([".md",".txt",".py",".js",".ts",".sh",".json",".yaml",".yml",".csv",".sql",".toml",".ini"]),r=/^[a-zA-Z0-9_-]{1,64}$/;function c(){return s.join(t.homedir(),".huicai","agents")}function o(){return s.join(t.homedir(),".huicai","skills")}export function discoverExternalAssets({base:n=t.homedir()}={}){const l=function(){try{return new Set(e.readdirSync(c()).filter(e=>e.endsWith(".md")).map(e=>e.replace(/\.md$/,"")))}catch{return new Set}}(),a=function(){try{return new Set(e.readdirSync(o(),{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>e.name))}catch{return new Set}}(),m=[],d=[],p=new Set,u=new Set;for(const t of EXTERNAL_SOURCES){const c=s.join(n,t.agents);let o=[];try{o=e.readdirSync(c).filter(e=>e.endsWith(".md"))}catch{}for(const n of o){const o=s.join(c,n);let a;try{a=i(e.readFileSync(o,"utf8"))}catch{continue}const d=String(a.name||n.replace(/\.md$/,"")).trim();r.test(d)&&!p.has(d)&&(p.add(d),m.push({name:d,description:(a.description||"").slice(0,200),cli:t.cli,src:o,exists:l.has(d)}))}const f=s.join(n,t.skills);let h=[];try{h=e.readdirSync(f,{withFileTypes:!0}).filter(e=>e.isDirectory())}catch{}for(const n of h){const c=n.name.trim(),o=s.join(f,c),l=s.join(o,"SKILL.md");if(!r.test(c)||u.has(c)||!e.existsSync(l))continue;u.add(c);let m="";try{m=(i(e.readFileSync(l,"utf8")).description||"").slice(0,200)}catch{}d.push({name:c,description:m,cli:t.cli,src:o,exists:a.has(c)})}}return{agents:m,skills:d}}function l(t,i,r=0){if(r>3)return 0;let c=0,o=[];try{o=e.readdirSync(t,{withFileTypes:!0})}catch{return 0}e.mkdirSync(i,{recursive:!0});for(const a of o){const o=s.join(t,a.name),m=s.join(i,a.name);if(a.isDirectory())c+=l(o,m,r+1);else if(a.isFile()&&("SKILL.md"===a.name||n.has(s.extname(a.name).toLowerCase())))try{e.copyFileSync(o,m),c++}catch{}}return c}export function importExternalAssets(t={},{overwrite:i=!1}={}){const n=[],a={agents:{imported:0,skipped:0},skills:{imported:0,skipped:0},details:n},m=c(),d=o();for(const c of t.agents||[]){if(!c||!r.test(String(c.name))||!c.src)continue;const t=s.join(m,c.name+".md");if(i||!e.existsSync(t))try{e.mkdirSync(m,{recursive:!0}),e.copyFileSync(c.src,t),a.agents.imported++,n.push(`导入 agent:${c.name}`)}catch(e){n.push(`失败 agent ${c.name}:${e.message}`)}else a.agents.skipped++,n.push(`跳过 agent(已存在):${c.name}`)}for(const c of t.skills||[]){if(!c||!r.test(String(c.name))||!c.src)continue;const t=s.join(d,c.name);if(!i&&e.existsSync(t)){a.skills.skipped++,n.push(`跳过 skill(已存在):${c.name}`);continue}const o=l(c.src,t);o>0?(a.skills.imported++,n.push(`导入 skill:${c.name}(${o} 文件)`)):(a.skills.skipped++,n.push(`跳过 skill(无内容):${c.name}`))}return a}export function importAllExternalAssets(e={}){const s=discoverExternalAssets(e);return importExternalAssets({agents:s.agents.filter(e=>!e.exists),skills:s.skills.filter(e=>!e.exists)},e)}
package/src/autofix.js CHANGED
@@ -1 +1 @@
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}}
1
+ import{spawn as t}from"node:child_process";import{StringDecoder as e}from"node:string_decoder";import r from"node:fs";import n from"node:path";const o=16e3;export function captureCommand(r,n,s=18e4){const i="win32"===process.platform;return new Promise(c=>{let d="",a=!1;const p=i?t(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",n],{cwd:r,windowsVerbatimArguments:!0}):t(process.env.SHELL||"/bin/sh",["-c",n],{cwd:r,detached:!0}),u=setTimeout(()=>{a=!0;try{!i&&p.pid?process.kill(-p.pid,"SIGKILL"):p.kill("SIGKILL")}catch{}},s),m=new e("utf8"),l=new e("utf8"),f=t=>{d.length<32e3&&(d+=t)};p.stdout.on("data",t=>f(m.write(t))),p.stderr.on("data",t=>f(l.write(t))),p.on("error",t=>{clearTimeout(u),c({code:-1,output:"命令启动失败:"+t.message})}),p.on("close",t=>{var e;clearTimeout(u),f(m.end()),f(l.end()),c({code:a?124:null==t?1:t,output:(e=d,((e=String(e)).length>o?e.slice(0,o)+"\n…(输出过长已截断)":e)||"(无输出)")})})})}export function detectVerifyCommand(t){const e=e=>{try{return r.existsSync(n.join(t,e))}catch{return!1}};try{if(e("package.json")){const e=JSON.parse(r.readFileSync(n.join(t,"package.json"),"utf8"));if(e.scripts?.test&&!/no test specified/i.test(e.scripts.test))return"npm test";if(e.scripts?.build)return"npm run build"}}catch{}return e("pyproject.toml")||e("pytest.ini")||e("tox.ini")?"pytest -q":e("pom.xml")?"mvn -q test":e("build.gradle")||e("build.gradle.kts")?"./gradlew test":e("Cargo.toml")?"cargo test":e("go.mod")?"go test ./...":e("Makefile")?"make test":null}export async function runVerifyLoop({command:t,maxIters:e=5,cwd:r,runTurnOnce:n,emit:o}){for(let s=0;s<e;s++){o(`▶ 验证 ${s+1}/${e}:${t}`);const{code:i,output:c}=await captureCommand(r,t);if(0===i)return o(`✅ 验证通过(第 ${s+1} 轮)。`),{passed:!0,iters:s+1};if(o(`✗ 未通过(退出码 ${i})`+(s<e-1?",让智能体定位修复后自动重跑…":"")),s===e-1)return o(`⚠ 已达上限 ${e} 轮仍未通过。`),{passed:!1,iters:s+1,output:c};const d=await n(`自动验证:运行 \`${t}\` 失败(退出码 ${i})。输出如下:\n\n${c}\n\n请定位根因并**修复代码/配置**。不要自己重跑该命令——我会自动重跑验证。本轮只做必要的修复。`);if(d&&d.aborted)return o("已中断修复循环。"),{passed:!1,aborted:!0,iters:s+1}}return{passed:!1}}
package/src/automation.js CHANGED
@@ -1 +1 @@
1
- import t from"node:fs";import e from"node:path";import{spawn as n}from"node:child_process";import{runBash as o}from"./executors.js";const r=3e4,a=3e5;export const AUTOMATION_TOOLS=new Set(["install_packages","setup_environment","install_plugin","open_browser","open_path","open_app","auto_execute"]);export function automationActionLabel(t,e={}){switch(t){case"install_packages":return`安装 Node.js 包:${Array.isArray(e.packages)&&e.packages.length?e.packages.join(" "):"package.json 全部依赖"}${e.global?"(全局)":""}${e.dev?"(devDependencies)":""}`;case"setup_environment":return Array.isArray(e.commands)&&e.commands.length?`自动配置运行环境:执行 ${e.commands.length} 条配置命令`:"自动配置运行环境:检测项目技术栈并安装依赖";case"install_plugin":return`安装${"jetbrains"===e.target?"JetBrains 插件":"npm"===e.target?"npm 全局工具":"VS Code 扩展"}:${e.id??""}`;case"open_browser":return`打开浏览器访问:${e.url??""}`;case"open_path":return`在文件管理器中打开:${e.path??""}`;case"open_app":return`启动应用:${e.name??""}${e.target?"("+e.target+")":""}`;case"auto_execute":{const t=String(e.goal||"").replace(/\s+/g," ");return"自动执行已规划的任务"+(t?":"+(t.length>60?t.slice(0,60)+"…":t):"")}default:return t}}export function automationDetail(t,e={}){return"setup_environment"===t&&Array.isArray(e.commands)&&e.commands.length?e.commands.slice(0,12).map(t=>"$ "+String(t)).join("\n"):"open_browser"===t?String(e.url??""):"open_path"===t?String(e.path??""):"open_app"===t?String(e.target??""):""}export function which(n,{pathStr:o=process.env.PATH||"",platform:r=process.platform}={}){const a="win32"===r?";":":",s="win32"===r?[".cmd",".exe",".bat",""]:[""];for(const r of o.split(a))if(r)for(const o of s){const a=e.join(r,n+o);try{if(t.accessSync(a,t.constants.X_OK),t.statSync(a).isFile())return a}catch{}}return null}export function runArgv(t,e,{cwd:o,timeoutMs:a=3e5,winShell:s=!1}={}){const i="win32"===process.platform;return new Promise(c=>{let l,p="",u=!1;try{l=n(t,e,i?{cwd:o,shell:!!s}:{cwd:o,detached:!0})}catch(t){return void c({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}const d=setTimeout(()=>{u=!0,(()=>{try{!i&&l.pid?process.kill(-l.pid,"SIGKILL"):l.kill("SIGKILL")}catch{try{l.kill("SIGKILL")}catch{}}})()},a),m=t=>{p.length<r&&(p+=t.toString())};l.stdout?.on("data",m),l.stderr?.on("data",m),l.on("error",t=>{clearTimeout(d),c({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}),l.on("close",t=>{clearTimeout(d),c({code:t,killed:u,output:p.length>r?p.slice(0,r)+"\n…(输出已截断)":p})})})}function s(t,e){return new Promise(o=>{let r,a="";try{r=n(t,e,{stdio:["ignore","pipe","pipe"]})}catch(t){return void o({code:-1,output:`命令启动失败:${t.message}`})}const s=setTimeout(()=>{try{r.unref()}catch{}o({code:0,output:a,lingering:!0})},8e3);r.stdout.on("data",t=>a+=t),r.stderr.on("data",t=>a+=t),r.on("error",t=>{clearTimeout(s),o({code:-1,output:`命令启动失败:${t.message}`})}),r.on("close",t=>{clearTimeout(s),o({code:t,output:a})})})}function i(t){if(!t||"string"!=typeof t)throw new Error("工作目录未设置:请在项目目录中运行(VS Code / IDEA 中需先「打开文件夹」)")}function c(t,e=1200){const n=String(t||"").trim();return n.length>e?"…"+n.slice(-e):n}export function detectManager(n){try{if(t.existsSync(e.join(n,"pnpm-lock.yaml")))return"pnpm";if(t.existsSync(e.join(n,"yarn.lock")))return"yarn"}catch{}return"npm"}export function validPackageToken(t){return!("string"!=typeof t||!t.trim()||/\s/.test(t)||t.startsWith("-"))&&/^(@[a-z0-9~][\w.-]*\/)?[a-z0-9~][\w.-]*(@[\w.^~=+*:-]+)?$/i.test(t)}export function buildInstallPlan(t,e={},n=process.platform){const o=["npm","pnpm","yarn"].includes(String(e.manager||"").toLowerCase())?String(e.manager).toLowerCase():detectManager(t),r=(Array.isArray(e.packages)?e.packages:[]).map(t=>String(t).trim()).filter(Boolean);for(const t of r)if(!validPackageToken(t))throw new Error(`包名不合法(仅支持 名字[@版本],不接受选项/空白/特殊字符):${t}`);const a=!!e.dev,s=!!e.global;if(s&&!r.length)throw new Error("全局安装(global=true)需要明确的 packages 列表");let i;i=r.length?"yarn"===o?s?["global","add",...r]:["add",...a?["-D"]:[],...r]:"pnpm"===o?["add",...s?["-g"]:[],...a?["-D"]:[],...r]:["install",...s?["-g"]:[],...a?["--save-dev"]:[],...r]:["install"];return{manager:o,bin:o,argv:i,winShell:"win32"===n,display:`${o} ${i.join(" ")}`}}export async function installPackages(t,e){i(t);const n=buildInstallPlan(t,e||{});if(!which(n.bin))return`未找到 ${n.bin}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`+("npm"!==n.manager?',或改用 manager:"npm"':"");const o=Number.isFinite(e?.timeout)?Math.min(e.timeout,6e5):a,r=await runArgv(n.bin,n.argv,{cwd:t,timeoutMs:o,winShell:n.winShell}),s=r.killed?"【超时被终止】":`退出码 ${r.code}`,l=`$ ${n.display}\n${s}`;return 0===r.code?`${l}\n已安装完成。${c(r.output,800)?"\n"+c(r.output,800):""}`:`${l}\n${c(r.output)||"(无输出)"}`}export function buildSetupSteps(n,o={},r={}){const a=r.platform||process.platform,s=r.pathStr,i=Array.isArray(o.commands)?o.commands.map(t=>String(t).trim()).filter(Boolean).slice(0,12):null;if(i&&i.length)return{mode:"commands",commands:i,detected:[]};const c=o=>{try{return t.existsSync(e.join(n,o))}catch{return!1}},l=[];c("package.json")&&l.push("node"),(c("requirements.txt")||c("pyproject.toml"))&&l.push("python"),c("go.mod")&&l.push("go"),c("Cargo.toml")&&l.push("rust"),c("pom.xml")&&l.push("maven"),(c("build.gradle")||c("build.gradle.kts"))&&l.push("gradle");const p=Array.isArray(o.stacks)&&o.stacks.length?l.filter(t=>o.stacks.includes(t)):l,u=[],d=[],m=(t,e)=>which(t,{pathStr:s,platform:a})?null:e;for(const t of p)if("node"===t){const e=detectManager(n),o=m(e,`未找到 ${e}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`);u.push(o?{stack:t,label:"安装 Node.js 依赖",missing:o}:{stack:t,label:`安装 Node.js 依赖(${e} install)`,bin:e,argv:["install"],winShell:!0})}else if("python"===t){const o="win32"===a?"python":"python3",r=m(o,`未找到 ${o}:macOS 可 brew install python,或从 python.org 安装`);if(r){u.push({stack:t,label:"配置 Python 环境",missing:r});continue}c(".venv")?d.push(".venv 已存在,跳过创建"):u.push({stack:t,label:"创建 Python 虚拟环境 .venv",bin:o,argv:["-m","venv",".venv"]});const s="win32"===a?".venv\\Scripts\\pip.exe":e.join(n,".venv/bin/pip");u.push(c("requirements.txt")?{stack:t,label:"安装 Python 依赖(requirements.txt)",bin:s,argv:["install","-r","requirements.txt"]}:{stack:t,label:"安装 Python 依赖(pyproject.toml)",bin:s,argv:["install","-e","."]})}else if("go"===t){const e=m("go","未找到 go:可从 https://go.dev/dl 安装(macOS 可 brew install go)");u.push(e?{stack:t,label:"下载 Go 依赖",missing:e}:{stack:t,label:"下载 Go 依赖(go mod download)",bin:"go",argv:["mod","download"]})}else if("rust"===t){const e=m("cargo","未找到 cargo:可用 rustup 安装(https://rustup.rs)");u.push(e?{stack:t,label:"拉取 Rust 依赖",missing:e}:{stack:t,label:"拉取 Rust 依赖(cargo fetch)",bin:"cargo",argv:["fetch"]})}else if("maven"===t){const e=m("mvn","未找到 mvn:macOS 可 brew install maven");u.push(e?{stack:t,label:"解析 Maven 依赖",missing:e}:{stack:t,label:"解析 Maven 依赖(mvn dependency:resolve)",bin:"mvn",argv:["-q","-DskipTests","dependency:resolve"],winShell:!0})}else"gradle"===t&&d.push("检测到 Gradle 项目:构建脚本可执行任意代码,出于安全未自动执行;可手动运行 ./gradlew build,或让我用 run_bash 执行");return{mode:"auto",detected:l,want:p,steps:u,notes:d}}export async function setupEnvironment(t,e){i(t);const n=e||{},r=buildSetupSteps(t,n),a=Number.isFinite(n.timeout)?Math.min(n.timeout,6e5):6e5;if("commands"===r.mode){const e=["环境配置(按给定命令逐条执行):"];for(const n of r.commands){const r=await o(t,{command:n,timeout:a});if(e.push(`$ ${n}\n${r}`),!/^退出码 0\b/.test(String(r))){e.push("(上一步未成功,中止后续配置命令)");break}}return e.join("\n\n")}if(!r.detected.length)return"未检测到已知技术栈(package.json / requirements.txt / pyproject.toml / go.mod / Cargo.toml / pom.xml / build.gradle)。可改用 commands 参数显式指定要执行的配置命令。";const s=[`检测到技术栈:${r.detected.join("、")}`+(r.want.length!==r.detected.length?`(本次仅处理:${r.want.join("、")||"无"})`:"")];for(const e of r.steps){if(e.missing){s.push(`✘ ${e.label}:${e.missing}`);continue}const n=await runArgv(e.bin,e.argv,{cwd:t,timeoutMs:a,winShell:e.winShell});if(0!==n.code){s.push(`✘ ${e.label} → ${n.killed?"超时被终止":"退出码 "+n.code}${c(n.output)?"\n"+c(n.output):""}`),s.push("(该步失败,停止后续步骤;可修正后重试,或用 commands 显式指定命令)");break}s.push(`✔ ${e.label} → 退出码 0`)}for(const t of r.notes||[])s.push("· "+t);return"环境配置报告:\n"+s.join("\n")}const l=["code","code-insiders","cursor","windsurf"],p=["idea","idea64","webstorm","pycharm","goland","rider","clion"];export function buildPluginPlan(t={},e={}){const n=e.platform||process.platform,o=e.pathStr,r=String(t.id||"").trim();if(!r||r.startsWith("-")||!/^[@\w./:-]+$/.test(r))throw new Error(`插件 id 不合法:${r||"(空)"}`);const a=String(t.target||"").trim().toLowerCase();if("vscode"===a){for(const t of l)if(which(t,{pathStr:o,platform:n}))return{kind:"vscode",bin:t,argv:["--install-extension",r],winShell:!0,note:`经 ${t} CLI 安装`};throw new Error("未找到 VS Code 命令行工具(code)。请在 VS Code 命令面板执行「Shell Command: Install 'code' command in PATH」后重试,或到扩展市场手动安装 "+r)}if("jetbrains"===a){for(const t of p)if(which(t,{pathStr:o,platform:n}))return{kind:"jetbrains",bin:t,argv:["installPlugins",r],note:"安装后需重启 IDE 生效"};throw new Error("未找到 JetBrains IDE 命令行启动器(idea 等)。可在 IDE 的 Tools → Create Command-line Launcher 生成,或到 Settings → Plugins 里搜索安装 "+r)}if("npm"===a){if(!which("npm",{pathStr:o,platform:n}))throw new Error("未找到 npm:请先安装 Node.js(https://nodejs.org)");return{kind:"npm",bin:"npm",argv:["install","-g",r],winShell:!0,note:"全局 CLI 工具"}}throw new Error("target 需为 vscode | jetbrains | npm")}export async function installPlugin(t,e){const n=buildPluginPlan(e||{}),o="jetbrains"===n.kind?18e4:a,r=await runArgv(n.bin,n.argv,{cwd:t||void 0,timeoutMs:o,winShell:n.winShell}),s=`${n.bin} ${n.argv.join(" ")}`;return 0===r.code?`$ ${s}\n退出码 0\n已安装 ${e?.id}${n.note?"("+n.note+")":""}${c(r.output,600)?"\n"+c(r.output,600):""}`:`$ ${s}\n${r.killed?"【超时被终止】":"退出码 "+r.code}\n${c(r.output)||"(无输出)"}`}export async function openBrowser(t,e){const n=String(e?.url||"").trim();if(!/^https?:\/\/\S+$/i.test(n))throw new Error("仅支持 http/https 网址");const o=process.platform,r="darwin"===o?await s("open",[n]):"win32"===o?await s("rundll32",["url.dll,FileProtocolHandler",n]):await s("xdg-open",[n]);return 0!==r.code?`打开浏览器失败(退出码 ${r.code}):${c(r.output)||"未知原因"}`:`已在系统默认浏览器打开:${n}`}export async function openPath(n,o){const r=String(o?.path||"").trim();if(!r)throw new Error("缺少路径 path");const a=e.resolve(n||".",r);if(!t.existsSync(a))throw new Error("路径不存在:"+a);const i=t.statSync(a).isDirectory(),l=null!=o?.reveal?!!o.reveal:!i,p=process.platform;let u;return"darwin"===p?u=await s("open",l&&!i?["-R",a]:[a]):"win32"===p?(u=await s("explorer.exe",l&&!i?["/select,"+a]:[a]),u={code:0,output:u.output}):u=await s("xdg-open",[l&&!i?e.dirname(a):a]),0!==u.code?`打开文件管理器失败(退出码 ${u.code}):${c(u.output)||"未知原因"}`:`已在文件管理器中${l&&!i?"定位":"打开"}:${a}`}export async function openApp(t,e){const o=String(e?.name||"").trim(),r=null!=e?.target?String(e.target).trim():"";if(!o)throw new Error("缺少应用名 name");if(o.length>120||/[\0-\x1f"'`$\\;|&<>{}[\]*?%]/.test(o))throw new Error("应用名含不允许的字符:"+o);if(r&&/[\0-\x1f"'`$\\;|&<>]/.test(r))throw new Error("target 含不允许的字符:"+r);const a=process.platform;if("darwin"===a){const t=await s("open",r?["-a",o,r]:["-a",o]);return 0!==t.code?`启动应用失败(退出码 ${t.code}):${c(t.output)||"未找到应用 "+o}`:`已启动应用:${o}${r?"(打开 "+r+")":""}`}if("win32"===a)return await new Promise(t=>{const e=`start "" "${o}"${r?` "${r}"`:""}`,a=n(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",e],{windowsVerbatimArguments:!0});a.on("error",e=>t(`启动应用失败:${e.message}`)),a.on("close",e=>t(0===e?`已启动应用:${o}`:`启动应用失败(退出码 ${e})`))});if(!/^[\w./+-]+$/.test(o))throw new Error("Linux 下应用名需为可执行文件名(字母数字._/+-):"+o);return await new Promise(t=>{const e=n(o,r?[r]:[],{detached:!0,stdio:"ignore"});e.once("error",e=>t(`启动应用失败:${e.message}`)),e.once("spawn",()=>{e.unref(),t(`已启动应用:${o}${r?"(打开 "+r+")":""}`)})})}const u=(t,e)=>({type:"object",properties:t,required:e||[]}),d=t=>({type:"string",description:t}),m=t=>({type:"boolean",description:t}),g=t=>({type:"integer",description:t});export const AUTOMATION_TOOL_SPECS=[{type:"function",function:{name:"install_packages",description:"安装 Node.js 依赖包(npm/pnpm/yarn 按锁文件自动识别;执行前会征询用户同意)。省略 packages 时安装 package.json 的全部依赖。仅支持包名[@版本],不接受额外命令行选项。",parameters:u({packages:{type:"array",items:{type:"string"},description:'要安装的包(如 ["lodash","@types/node@^20"]);省略=安装全部依赖'},dev:m("可选:装入 devDependencies(默认否)"),global:m("可选:全局安装(需明确 packages;默认否)"),manager:d("可选:npm | pnpm | yarn(默认按锁文件自动识别)"),timeout:g("可选:超时毫秒(默认 300000,上限 600000)")},[])}},{type:"function",function:{name:"setup_environment",description:"自动配置项目运行环境(执行前会征询用户同意):检测技术栈(Node/Python/Go/Rust/Maven)→ 检查工具链是否安装 → 安装依赖(npm install、创建 .venv 并 pip install、go mod download 等)。也可用 commands 显式指定要执行的配置命令(逐条执行,失败即停)。",parameters:u({stacks:{type:"array",items:{type:"string"},description:"可选:只处理这些技术栈(node|python|go|rust|maven)"},commands:{type:"array",items:{type:"string"},description:"可选:显式配置命令列表(优先于自动检测,最多 12 条)"},timeout:g("可选:单步超时毫秒(默认 600000)")},[])}},{type:"function",function:{name:"install_plugin",description:"安装编辑器/IDE 插件(执行前会征询用户同意):VS Code 扩展(code --install-extension)、JetBrains 插件(idea installPlugins,装后需重启 IDE)、或 npm 全局 CLI 工具(npm install -g)。",parameters:u({target:d("vscode | jetbrains | npm"),id:d("插件标识:VS Code 扩展 id(如 dbaeumer.vscode-eslint)/ JetBrains 插件 id / npm 包名")},["target","id"])}},{type:"function",function:{name:"open_browser",description:"用系统默认浏览器打开一个 http/https 网址(执行前会征询用户同意)。适合把文档、本地预览页、报告直接呈现给用户。",parameters:u({url:d("完整网址(http/https)")},["url"])}},{type:"function",function:{name:"open_path",description:"在系统文件管理器(访达/资源管理器)中打开目录或定位文件(执行前会征询用户同意)。",parameters:u({path:d("目录或文件路径(相对项目根或绝对路径)"),reveal:m("可选:定位(选中)该文件而非打开;文件默认定位、目录默认打开")},["path"])}},{type:"function",function:{name:"open_app",description:'启动本机任意应用程序(执行前会征询用户同意),可附带要打开的文件或网址。macOS 用应用名(如 "Safari"、"Visual Studio Code"),Windows 用可启动名,Linux 用可执行文件名。',parameters:u({name:d("应用名/可执行名"),target:d("可选:让该应用打开的文件路径或 URL")},["name"])}}];export const AUTOMATION_EXECUTORS={install_packages:installPackages,setup_environment:setupEnvironment,install_plugin:installPlugin,open_browser:openBrowser,open_path:openPath,open_app:openApp};
1
+ import t from"node:fs";import e from"node:path";import{spawn as n}from"node:child_process";import{StringDecoder as o}from"node:string_decoder";import{runBash as r}from"./executors.js";const a=3e4,s=3e5;export const AUTOMATION_TOOLS=new Set(["install_packages","setup_environment","install_plugin","open_browser","open_path","open_app","auto_execute"]);export function automationActionLabel(t,e={}){switch(t){case"install_packages":return`安装 Node.js 包:${Array.isArray(e.packages)&&e.packages.length?e.packages.join(" "):"package.json 全部依赖"}${e.global?"(全局)":""}${e.dev?"(devDependencies)":""}`;case"setup_environment":return Array.isArray(e.commands)&&e.commands.length?`自动配置运行环境:执行 ${e.commands.length} 条配置命令`:"自动配置运行环境:检测项目技术栈并安装依赖";case"install_plugin":return`安装${"jetbrains"===e.target?"JetBrains 插件":"npm"===e.target?"npm 全局工具":"VS Code 扩展"}:${e.id??""}`;case"open_browser":return`打开浏览器访问:${e.url??""}`;case"open_path":return`在文件管理器中打开:${e.path??""}`;case"open_app":return`启动应用:${e.name??""}${e.target?"("+e.target+")":""}`;case"auto_execute":{const t=String(e.goal||"").replace(/\s+/g," ");return"自动执行已规划的任务"+(t?":"+(t.length>60?t.slice(0,60)+"…":t):"")}default:return t}}export function automationDetail(t,e={}){return"setup_environment"===t&&Array.isArray(e.commands)&&e.commands.length?e.commands.slice(0,12).map(t=>"$ "+String(t)).join("\n"):"open_browser"===t?String(e.url??""):"open_path"===t?String(e.path??""):"open_app"===t?String(e.target??""):""}export function which(n,{pathStr:o=process.env.PATH||"",platform:r=process.platform}={}){const a="win32"===r?";":":",s="win32"===r?[".cmd",".exe",".bat",""]:[""];for(const r of o.split(a))if(r)for(const o of s){const a=e.join(r,n+o);try{if(t.accessSync(a,t.constants.X_OK),t.statSync(a).isFile())return a}catch{}}return null}export function runArgv(t,e,{cwd:r,timeoutMs:s=3e5,winShell:i=!1}={}){const c="win32"===process.platform;return new Promise(p=>{let l,u="",d=!1;try{l=n(t,e,c?{cwd:r,shell:!!i}:{cwd:r,detached:!0})}catch(t){return void p({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}const m=setTimeout(()=>{d=!0,(()=>{try{!c&&l.pid?process.kill(-l.pid,"SIGKILL"):l.kill("SIGKILL")}catch{try{l.kill("SIGKILL")}catch{}}})()},s),g=new o("utf8"),h=new o("utf8"),w=t=>{u.length<a&&(u+=t)};l.stdout?.on("data",t=>w(g.write(t))),l.stderr?.on("data",t=>w(h.write(t))),l.on("error",t=>{clearTimeout(m),p({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}),l.on("close",t=>{clearTimeout(m),w(g.end()),w(h.end()),p({code:t,killed:d,output:u.length>a?u.slice(0,a)+"\n…(输出已截断)":u})})})}function i(t,e){return new Promise(o=>{let r,a="";try{r=n(t,e,{stdio:["ignore","pipe","pipe"]})}catch(t){return void o({code:-1,output:`命令启动失败:${t.message}`})}const s=setTimeout(()=>{try{r.unref()}catch{}o({code:0,output:a,lingering:!0})},8e3);r.stdout.on("data",t=>a+=t),r.stderr.on("data",t=>a+=t),r.on("error",t=>{clearTimeout(s),o({code:-1,output:`命令启动失败:${t.message}`})}),r.on("close",t=>{clearTimeout(s),o({code:t,output:a})})})}function c(t){if(!t||"string"!=typeof t)throw new Error("工作目录未设置:请在项目目录中运行(VS Code / IDEA 中需先「打开文件夹」)")}function p(t,e=1200){const n=String(t||"").trim();return n.length>e?"…"+n.slice(-e):n}export function detectManager(n){try{if(t.existsSync(e.join(n,"pnpm-lock.yaml")))return"pnpm";if(t.existsSync(e.join(n,"yarn.lock")))return"yarn"}catch{}return"npm"}export function validPackageToken(t){return!("string"!=typeof t||!t.trim()||/\s/.test(t)||t.startsWith("-"))&&/^(@[a-z0-9~][\w.-]*\/)?[a-z0-9~][\w.-]*(@[\w.^~=+*:-]+)?$/i.test(t)}export function buildInstallPlan(t,e={},n=process.platform){const o=["npm","pnpm","yarn"].includes(String(e.manager||"").toLowerCase())?String(e.manager).toLowerCase():detectManager(t),r=(Array.isArray(e.packages)?e.packages:[]).map(t=>String(t).trim()).filter(Boolean);for(const t of r)if(!validPackageToken(t))throw new Error(`包名不合法(仅支持 名字[@版本],不接受选项/空白/特殊字符):${t}`);const a=!!e.dev,s=!!e.global;if(s&&!r.length)throw new Error("全局安装(global=true)需要明确的 packages 列表");let i;i=r.length?"yarn"===o?s?["global","add",...r]:["add",...a?["-D"]:[],...r]:"pnpm"===o?["add",...s?["-g"]:[],...a?["-D"]:[],...r]:["install",...s?["-g"]:[],...a?["--save-dev"]:[],...r]:["install"];return{manager:o,bin:o,argv:i,winShell:"win32"===n,display:`${o} ${i.join(" ")}`}}export async function installPackages(t,e){c(t);const n=buildInstallPlan(t,e||{});if(!which(n.bin))return`未找到 ${n.bin}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`+("npm"!==n.manager?',或改用 manager:"npm"':"");const o=Number.isFinite(e?.timeout)?Math.min(e.timeout,6e5):s,r=await runArgv(n.bin,n.argv,{cwd:t,timeoutMs:o,winShell:n.winShell}),a=r.killed?"【超时被终止】":`退出码 ${r.code}`,i=`$ ${n.display}\n${a}`;return 0===r.code?`${i}\n已安装完成。${p(r.output,800)?"\n"+p(r.output,800):""}`:`${i}\n${p(r.output)||"(无输出)"}`}export function buildSetupSteps(n,o={},r={}){const a=r.platform||process.platform,s=r.pathStr,i=Array.isArray(o.commands)?o.commands.map(t=>String(t).trim()).filter(Boolean).slice(0,12):null;if(i&&i.length)return{mode:"commands",commands:i,detected:[]};const c=o=>{try{return t.existsSync(e.join(n,o))}catch{return!1}},p=[];c("package.json")&&p.push("node"),(c("requirements.txt")||c("pyproject.toml"))&&p.push("python"),c("go.mod")&&p.push("go"),c("Cargo.toml")&&p.push("rust"),c("pom.xml")&&p.push("maven"),(c("build.gradle")||c("build.gradle.kts"))&&p.push("gradle");const l=Array.isArray(o.stacks)&&o.stacks.length?p.filter(t=>o.stacks.includes(t)):p,u=[],d=[],m=(t,e)=>which(t,{pathStr:s,platform:a})?null:e;for(const t of l)if("node"===t){const e=detectManager(n),o=m(e,`未找到 ${e}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`);u.push(o?{stack:t,label:"安装 Node.js 依赖",missing:o}:{stack:t,label:`安装 Node.js 依赖(${e} install)`,bin:e,argv:["install"],winShell:!0})}else if("python"===t){const o="win32"===a?"python":"python3",r=m(o,`未找到 ${o}:macOS 可 brew install python,或从 python.org 安装`);if(r){u.push({stack:t,label:"配置 Python 环境",missing:r});continue}c(".venv")?d.push(".venv 已存在,跳过创建"):u.push({stack:t,label:"创建 Python 虚拟环境 .venv",bin:o,argv:["-m","venv",".venv"]});const s="win32"===a?".venv\\Scripts\\pip.exe":e.join(n,".venv/bin/pip");u.push(c("requirements.txt")?{stack:t,label:"安装 Python 依赖(requirements.txt)",bin:s,argv:["install","-r","requirements.txt"]}:{stack:t,label:"安装 Python 依赖(pyproject.toml)",bin:s,argv:["install","-e","."]})}else if("go"===t){const e=m("go","未找到 go:可从 https://go.dev/dl 安装(macOS 可 brew install go)");u.push(e?{stack:t,label:"下载 Go 依赖",missing:e}:{stack:t,label:"下载 Go 依赖(go mod download)",bin:"go",argv:["mod","download"]})}else if("rust"===t){const e=m("cargo","未找到 cargo:可用 rustup 安装(https://rustup.rs)");u.push(e?{stack:t,label:"拉取 Rust 依赖",missing:e}:{stack:t,label:"拉取 Rust 依赖(cargo fetch)",bin:"cargo",argv:["fetch"]})}else if("maven"===t){const e=m("mvn","未找到 mvn:macOS 可 brew install maven");u.push(e?{stack:t,label:"解析 Maven 依赖",missing:e}:{stack:t,label:"解析 Maven 依赖(mvn dependency:resolve)",bin:"mvn",argv:["-q","-DskipTests","dependency:resolve"],winShell:!0})}else"gradle"===t&&d.push("检测到 Gradle 项目:构建脚本可执行任意代码,出于安全未自动执行;可手动运行 ./gradlew build,或让我用 run_bash 执行");return{mode:"auto",detected:p,want:l,steps:u,notes:d}}export async function setupEnvironment(t,e){c(t);const n=e||{},o=buildSetupSteps(t,n),a=Number.isFinite(n.timeout)?Math.min(n.timeout,6e5):6e5;if("commands"===o.mode){const e=["环境配置(按给定命令逐条执行):"];for(const n of o.commands){const o=await r(t,{command:n,timeout:a});if(e.push(`$ ${n}\n${o}`),!/^退出码 0\b/.test(String(o))){e.push("(上一步未成功,中止后续配置命令)");break}}return e.join("\n\n")}if(!o.detected.length)return"未检测到已知技术栈(package.json / requirements.txt / pyproject.toml / go.mod / Cargo.toml / pom.xml / build.gradle)。可改用 commands 参数显式指定要执行的配置命令。";const s=[`检测到技术栈:${o.detected.join("、")}`+(o.want.length!==o.detected.length?`(本次仅处理:${o.want.join("、")||"无"})`:"")];for(const e of o.steps){if(e.missing){s.push(`✘ ${e.label}:${e.missing}`);continue}const n=await runArgv(e.bin,e.argv,{cwd:t,timeoutMs:a,winShell:e.winShell});if(0!==n.code){s.push(`✘ ${e.label} → ${n.killed?"超时被终止":"退出码 "+n.code}${p(n.output)?"\n"+p(n.output):""}`),s.push("(该步失败,停止后续步骤;可修正后重试,或用 commands 显式指定命令)");break}s.push(`✔ ${e.label} → 退出码 0`)}for(const t of o.notes||[])s.push("· "+t);return"环境配置报告:\n"+s.join("\n")}const l=["code","code-insiders","cursor","windsurf"],u=["idea","idea64","webstorm","pycharm","goland","rider","clion"];export function buildPluginPlan(t={},e={}){const n=e.platform||process.platform,o=e.pathStr,r=String(t.id||"").trim();if(!r||r.startsWith("-")||!/^[@\w./:-]+$/.test(r))throw new Error(`插件 id 不合法:${r||"(空)"}`);const a=String(t.target||"").trim().toLowerCase();if("vscode"===a){for(const t of l)if(which(t,{pathStr:o,platform:n}))return{kind:"vscode",bin:t,argv:["--install-extension",r],winShell:!0,note:`经 ${t} CLI 安装`};throw new Error("未找到 VS Code 命令行工具(code)。请在 VS Code 命令面板执行「Shell Command: Install 'code' command in PATH」后重试,或到扩展市场手动安装 "+r)}if("jetbrains"===a){for(const t of u)if(which(t,{pathStr:o,platform:n}))return{kind:"jetbrains",bin:t,argv:["installPlugins",r],note:"安装后需重启 IDE 生效"};throw new Error("未找到 JetBrains IDE 命令行启动器(idea 等)。可在 IDE 的 Tools → Create Command-line Launcher 生成,或到 Settings → Plugins 里搜索安装 "+r)}if("npm"===a){if(!which("npm",{pathStr:o,platform:n}))throw new Error("未找到 npm:请先安装 Node.js(https://nodejs.org)");return{kind:"npm",bin:"npm",argv:["install","-g",r],winShell:!0,note:"全局 CLI 工具"}}throw new Error("target 需为 vscode | jetbrains | npm")}export async function installPlugin(t,e){const n=buildPluginPlan(e||{}),o="jetbrains"===n.kind?18e4:s,r=await runArgv(n.bin,n.argv,{cwd:t||void 0,timeoutMs:o,winShell:n.winShell}),a=`${n.bin} ${n.argv.join(" ")}`;return 0===r.code?`$ ${a}\n退出码 0\n已安装 ${e?.id}${n.note?"("+n.note+")":""}${p(r.output,600)?"\n"+p(r.output,600):""}`:`$ ${a}\n${r.killed?"【超时被终止】":"退出码 "+r.code}\n${p(r.output)||"(无输出)"}`}export async function openBrowser(t,e){const n=String(e?.url||"").trim();if(!/^https?:\/\/\S+$/i.test(n))throw new Error("仅支持 http/https 网址");const o=process.platform,r="darwin"===o?await i("open",[n]):"win32"===o?await i("rundll32",["url.dll,FileProtocolHandler",n]):await i("xdg-open",[n]);return 0!==r.code?`打开浏览器失败(退出码 ${r.code}):${p(r.output)||"未知原因"}`:`已在系统默认浏览器打开:${n}`}export async function openPath(n,o){const r=String(o?.path||"").trim();if(!r)throw new Error("缺少路径 path");const a=e.resolve(n||".",r);if(!t.existsSync(a))throw new Error("路径不存在:"+a);const s=t.statSync(a).isDirectory(),c=null!=o?.reveal?!!o.reveal:!s,l=process.platform;let u;return"darwin"===l?u=await i("open",c&&!s?["-R",a]:[a]):"win32"===l?(u=await i("explorer.exe",c&&!s?["/select,"+a]:[a]),u={code:0,output:u.output}):u=await i("xdg-open",[c&&!s?e.dirname(a):a]),0!==u.code?`打开文件管理器失败(退出码 ${u.code}):${p(u.output)||"未知原因"}`:`已在文件管理器中${c&&!s?"定位":"打开"}:${a}`}export async function openApp(t,e){const o=String(e?.name||"").trim(),r=null!=e?.target?String(e.target).trim():"";if(!o)throw new Error("缺少应用名 name");if(o.length>120||/[\0-\x1f"'`$\\;|&<>{}[\]*?%]/.test(o))throw new Error("应用名含不允许的字符:"+o);if(r&&/[\0-\x1f"'`$\\;|&<>]/.test(r))throw new Error("target 含不允许的字符:"+r);const a=process.platform;if("darwin"===a){const t=await i("open",r?["-a",o,r]:["-a",o]);return 0!==t.code?`启动应用失败(退出码 ${t.code}):${p(t.output)||"未找到应用 "+o}`:`已启动应用:${o}${r?"(打开 "+r+")":""}`}if("win32"===a)return await new Promise(t=>{const e=`start "" "${o}"${r?` "${r}"`:""}`,a=n(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",e],{windowsVerbatimArguments:!0});a.on("error",e=>t(`启动应用失败:${e.message}`)),a.on("close",e=>t(0===e?`已启动应用:${o}`:`启动应用失败(退出码 ${e})`))});if(!/^[\w./+-]+$/.test(o))throw new Error("Linux 下应用名需为可执行文件名(字母数字._/+-):"+o);return await new Promise(t=>{const e=n(o,r?[r]:[],{detached:!0,stdio:"ignore"});e.once("error",e=>t(`启动应用失败:${e.message}`)),e.once("spawn",()=>{e.unref(),t(`已启动应用:${o}${r?"(打开 "+r+")":""}`)})})}const d=(t,e)=>({type:"object",properties:t,required:e||[]}),m=t=>({type:"string",description:t}),g=t=>({type:"boolean",description:t}),h=t=>({type:"integer",description:t});export const AUTOMATION_TOOL_SPECS=[{type:"function",function:{name:"install_packages",description:"安装 Node.js 依赖包(npm/pnpm/yarn 按锁文件自动识别;执行前会征询用户同意)。省略 packages 时安装 package.json 的全部依赖。仅支持包名[@版本],不接受额外命令行选项。",parameters:d({packages:{type:"array",items:{type:"string"},description:'要安装的包(如 ["lodash","@types/node@^20"]);省略=安装全部依赖'},dev:g("可选:装入 devDependencies(默认否)"),global:g("可选:全局安装(需明确 packages;默认否)"),manager:m("可选:npm | pnpm | yarn(默认按锁文件自动识别)"),timeout:h("可选:超时毫秒(默认 300000,上限 600000)")},[])}},{type:"function",function:{name:"setup_environment",description:"自动配置项目运行环境(执行前会征询用户同意):检测技术栈(Node/Python/Go/Rust/Maven)→ 检查工具链是否安装 → 安装依赖(npm install、创建 .venv 并 pip install、go mod download 等)。也可用 commands 显式指定要执行的配置命令(逐条执行,失败即停)。",parameters:d({stacks:{type:"array",items:{type:"string"},description:"可选:只处理这些技术栈(node|python|go|rust|maven)"},commands:{type:"array",items:{type:"string"},description:"可选:显式配置命令列表(优先于自动检测,最多 12 条)"},timeout:h("可选:单步超时毫秒(默认 600000)")},[])}},{type:"function",function:{name:"install_plugin",description:"安装编辑器/IDE 插件(执行前会征询用户同意):VS Code 扩展(code --install-extension)、JetBrains 插件(idea installPlugins,装后需重启 IDE)、或 npm 全局 CLI 工具(npm install -g)。",parameters:d({target:m("vscode | jetbrains | npm"),id:m("插件标识:VS Code 扩展 id(如 dbaeumer.vscode-eslint)/ JetBrains 插件 id / npm 包名")},["target","id"])}},{type:"function",function:{name:"open_browser",description:"用系统默认浏览器打开一个 http/https 网址(执行前会征询用户同意)。适合把文档、本地预览页、报告直接呈现给用户。",parameters:d({url:m("完整网址(http/https)")},["url"])}},{type:"function",function:{name:"open_path",description:"在系统文件管理器(访达/资源管理器)中打开目录或定位文件(执行前会征询用户同意)。",parameters:d({path:m("目录或文件路径(相对项目根或绝对路径)"),reveal:g("可选:定位(选中)该文件而非打开;文件默认定位、目录默认打开")},["path"])}},{type:"function",function:{name:"open_app",description:'启动本机任意应用程序(执行前会征询用户同意),可附带要打开的文件或网址。macOS 用应用名(如 "Safari"、"Visual Studio Code"),Windows 用可启动名,Linux 用可执行文件名。',parameters:d({name:m("应用名/可执行名"),target:m("可选:让该应用打开的文件路径或 URL")},["name"])}}];export const AUTOMATION_EXECUTORS={install_packages:installPackages,setup_environment:setupEnvironment,install_plugin:installPlugin,open_browser:openBrowser,open_path:openPath,open_app:openApp};