@huicai/cli 2026.721.0 → 2026.814.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/src/squad.js CHANGED
@@ -1 +1 @@
1
- import e from"node:fs";import t from"node:path";import n from"node:os";import{fileURLToPath as r}from"node:url";import{runTurn as s}from"./agent.js";import{createPermissions as o}from"./permissions.js";import{loadAgents as i}from"./subagent.js";import{streamChat as a}from"./api.js";import{discoverExperts as c}from"./experts.js";export const BUNDLED_SQUADS_DIR=t.join(t.dirname(r(import.meta.url)),"..","squads");export function squadDirs(e){const r=[];return e&&r.push(t.join(e,".huicai","squads")),r.push(t.join(n.homedir(),".huicai","squads")),r}export function parseSquadDoc(e,t=""){const{attrs:n,body:r}=function(e){const t={attrs:{},body:String(e||"").trim()},n=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(t.body);if(!n)return t;t.body=t.body.slice(n[0].length).trim();for(const e of n[1].split(/\r?\n/)){const n=/^([A-Za-z][\w-]*)\s*:\s*(.*)$/.exec(e.trim());n&&(t.attrs[n[1]]=n[2].trim().replace(/^["']|["']$/g,""))}return t}(e),s={name:n.name||t,description:n.description||"",leader:n.leader||"主理人",mode:["dispatch","review","ensemble"].includes(n.mode)?n.mode:"dispatch",when:n.when||"",mission:"",members:[],precheck:[],workflows:[]},o=new Map,i=r.split(/^##\s+/m);s.mission=(i[0]||"").trim();for(const e of i.slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1);if(/^成员人设/.test(n))for(const e of r.split(/^###\s+/m).slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1).trim();n&&r&&o.set(n,r)}else if(/^成员/.test(n))for(const e of r.split(/\r?\n/)){const t=/^\s*[-*]\s+(.+)$/.exec(e);if(!t)continue;const n=t[1].split("|").map(e=>e.trim());n[0]&&s.members.push({id:n[0],callName:n[1]||n[0],duty:n[2]||"",executor:n[3]||""})}else if(/^预检/.test(n))for(const e of r.split(/\r?\n/)){const t=/^\s*[-*]\s+(.+)$/.exec(e);t&&s.precheck.push(t[1].trim())}else if(/^工作流/.test(n))for(const e of r.split(/^###\s+/m).slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1),o=/^(.*?)(?:(触发[::](.*?)))?$/.exec(n)||[],i={name:(o[1]||n).trim(),trigger:(o[2]||"").trim(),stages:[],deliverable:""};for(const e of r.split(/\r?\n/)){const t=/^\s*\d+[.、)]\s*(.+)$/.exec(e);if(t){i.stages.push(t[1].trim());continue}const n=/^\s*交付[::]\s*(.+)$/.exec(e);n&&(i.deliverable=n[1].trim())}i.name&&s.workflows.push(i)}}for(const[e,t]of o){const n=s.members.find(t=>t.id===e);n&&(n.persona=t)}return s}export function loadSquads(n){const r=new Map,s=[...squadDirs(n).map((e,t)=>({dir:e,scope:n&&0===t?"project":"global"})),{dir:BUNDLED_SQUADS_DIR,scope:"builtin"}];for(const{dir:n,scope:o}of s){let s;try{s=e.readdirSync(n).filter(e=>e.endsWith(".md"))}catch{continue}for(const i of s){const s=i.replace(/\.md$/,"").trim();if(s)try{const a=t.join(n,i),c=parseSquadDoc(e.readFileSync(a,"utf8"),s);c.scope=o,c.file=a,r.has(c.name)||r.set(c.name,c)}catch{}}}return[...r.values()]}export function ensureTeamDir(n,r){const s=(new Date).toISOString().slice(0,16).replace(/[-:]/g,"").replace("T","-"),o=t.join(n,".huicai","teams",`${r}-${s}`);try{e.mkdirSync(o,{recursive:!0})}catch{return null}return o}export function buildLeaderPrompt(e,t){const n=[];n.push("**Role Override**:以下专家团主理人定义优先于此前任何身份设定,是你在本任务中唯一、权威的角色。"),n.push(""),n.push(`# 专家团「${e.name}」· 主理人${e.leader}`),n.push(`你是专家团「${e.name}」的主理人${e.leader},负责把用户任务拆解、派发给团队成员并汇总交付。`),e.mission&&n.push("",e.mission),n.push("","## 关键规则:只调度,不代工","- 成员职责范围内的专业产出必须经 dispatch_member 由对应成员完成后采信,你不代写;","- 简单澄清/寒暄/对既有产出的说明可直接回答,无须派发。"),n.push("","## 团队架构(调度依据)","| 成员ID | 称呼 | 职责 | 执行者 |","|---|---|---|---|");for(const t of e.members)n.push(`| ${t.id} | ${t.callName} | ${t.duty} | ${t.executor||"(当前模型)"} |`);if(n.push("","## 任务分配预检(每次派发前必做)","成员不会说「这个我不会」——超能力范围的任务结果无保证。所以:","1. 解析用户意图(研究/分析/生成/编辑/写作/审校…);","2. 对照职责与速查表做能力匹配;","3. 匹配 → 派发;不匹配 → 告知用户团队能力限制与替代建议,禁止硬派;","4. 意图模糊 → 先向用户确认。"),e.precheck.length){n.push("### 速查表(常见误派提醒)");for(const t of e.precheck)n.push(`- ${t}`)}if(e.workflows.length){n.push("","## 预设工作流(识别到触发条件时按阶段执行,不自由发挥)");for(const t of e.workflows)n.push(`### ${t.name}${t.trigger?`(触发:${t.trigger})`:""}`),t.stages.forEach((e,t)=>n.push(`${t+1}. ${e}`)),t.deliverable&&n.push(`交付:${t.deliverable}`)}return n.push("","## 团队协作规范","1. 成员在隔离上下文执行,看不到本对话与其他成员产出——任务说明必须独立自足;","2. 跨成员信息流经你中转:把上一阶段产出的必要部分放进下一位成员的 context;","3. 互相独立的任务同一轮并行派发(一次输出多个 dispatch_member 调用),减少串行等待;","4. 成员结论为准:如实汇编;产出不符时修正任务说明重派,而非替他改写。"),"review"===e.mode?n.push("### 生成-审查模式(本团约束)","每个关键产出完成后,必须把「产出全文+原始要求」派给职责匹配的审查成员评审,按意见让原成员修订至通过;未经审查的产出不得作为最终交付。"):"ensemble"===e.mode&&n.push("### 多候选聚合模式(本团约束)","对开放性核心任务,把同一任务并行派给多位匹配成员(或不同侧重多份),对比候选择优或综合,并向用户说明取舍。"),n.push("","## 异常处理(失败三级升级)","- 一级:修正任务说明(更自足、更具体、补 context)后重派原成员;同一任务累计失败满 2 次系统会终止该任务线;","- 二级:换人或换执行方式——改派职责相近的其他成员;平台资源不可用时由本机成员降级承接(系统会自动降级并注明);","- 三级:降级交付——完成可完成的部分,如实说明缺口与原因,并给出用户可选的替代方案。"),t&&n.push("","## 团队工作区",`本次协作的产物统一放在 ${t}(你可用文件工具写入;成员只回传文本结论,由你负责归档与整合)。`),n.push("","## 交付检查清单(最终答复前逐项自查)","- 用户目标的每一部分是否都有对应成员产出支撑?","- 是否汇总了全部关键产出、无散落遗漏?","- 是否标注了各部分由哪位成员产出?","- 未完成的部分是否如实说明?"),n.join("\n")}const m=/^(agent|workflow|datahuman|squad)-/;async function p({squad:e,member:t,ref:n,task:r,context:s,expectation:o,config:i}){const c=[`【专家团「${e.name}」成员任务】${r}`,s?`\n【上游产出/背景】\n${s}`:"",o?`\n【交付要求】\n${o}`:""].join("");return(await a({baseUrl:i.baseUrl,apiKey:i.apiKey,model:n,messages:[{role:"user",content:c}],client:i.client,sessionId:i.sessionId},{})).content||"(平台无返回)"}async function l({squad:e,member:t,task:n,context:r,expectation:a,config:l,cwd:u,readOnlyExtras:d}){const f=String(t.executor||"").trim();let h="";if(f.startsWith("platform:auto:")){const s=await async function(e,t){const n=/^platform:auto:(agent|workflow|datahuman|squad)(?::(.+))?$/.exec(e);if(!n)return null;const[,r,s]=n;try{const e=await c(t),n=(s||"").trim().toLowerCase();return e.find(e=>e.type===r&&(!n||(String(e.name)+" "+String(e.description)).toLowerCase().includes(n)))||null}catch{return null}}(f,l);if(s){const o=await p({squad:e,member:t,ref:s.id,task:n,context:r,expectation:a,config:l});return`(由平台资源「${s.name}」执行)\n${o}`}h=`(未发现可用的平台${f.split(":")[2]||""}资源,已由本机成员降级执行)\n`}else if(f.startsWith("platform:")){const s=f.slice(9).trim();return m.test(s)?p({squad:e,member:t,ref:s,task:n,context:r,expectation:a,config:l}):`成员「${t.callName}」的执行者 ${s} 不是有效的平台伪模型引用`}const $=["**Role Override**:以下角色定义在本次任务中拥有最高优先级。",""];t.persona&&$.push(t.persona,""),$.push(`你是专家团「${e.name}」的成员「${t.callName}」${t.duty?`(职责:${t.duty})`:""},正在执行主理人下发的一项独立任务。`,"- 只完成本任务,直接输出可被主理人直接采用的成果本身,不要寒暄、不要复述任务;","- 无法反问,信息不全时按合理假设执行并注明假设。"),r&&$.push("","## 主理人提供的上游产出/背景",String(r)),a&&$.push("","## 交付要求",String(a));let g=$.join("\n"),w=l;if(f.startsWith("agent:")){const e=f.slice(6).trim(),t=i(u).find(t=>t.name===e);t&&(g=t.prompt.trim()+"\n\n"+g)}else if(f.startsWith("model:")){const e=f.slice(6).trim();e&&(w={...l,model:e})}g+="\n工作目录:"+u;const x=[{role:"system",content:g},{role:"user",content:n}],b=await s({config:w,messages:x,permissions:o({yolo:!1,ask:null}),cwd:u,extraTools:d,depth:1,maxIters:50,io:{write:()=>{}}});if(b?.error)return`成员执行失败:${b.error}`;const y=[...x].reverse().find(e=>"assistant"===e.role&&e.content);return y?h+y.content:"(成员未产出结论)"}export function makeDispatchMemberTool({config:e,cwd:t,extraTools:n=[]}){const r=loadSquads(t);if(!r.length)return null;const s=n.filter(e=>e.readOnly),o=new Map;return{readOnly:!0,spec:{type:"function",function:{name:"dispatch_member",description:"把一项独立、自足的任务派发给某个专家团的一位成员:成员在隔离上下文中执行并只回传成果。复杂/跨角色任务优先按团开工:对照下方各团「何时使用」自主选团,按其预设工作流分阶段派发(互相独立的任务同一轮多次调用并行);成员产出由你汇总并用本机工具落地,落地后验证。派发前先做任务分配预检(对照成员职责做能力匹配)。\n可用专家团:\n"+r.map(e=>`- ${e.name}:${e.when||e.description||""}(成员 ${e.members.map(e=>e.id).join("/")})`).join("\n"),parameters:{type:"object",properties:{squad:{type:"string",enum:r.map(e=>e.name),description:"专家团名称"},member:{type:"string",description:"成员ID(推荐)或称呼,须来自该团的团队架构表"},task:{type:"string",description:"下发的任务说明——必须独立自足(成员看不到本对话与其他成员产出)"},context:{type:"string",description:"可选:上游产出/背景(跨成员信息流经主理人中转,把必要部分放这里)"},expectation:{type:"string",description:"可选:交付要求/验收标准"}},required:["squad","member","task"]}}},run:async n=>{const i=r.find(e=>e.name===String(n?.squad||"").trim());if(!i)return`未找到专家团「${n?.squad}」(可用:${r.map(e=>e.name).join("、")})`;const a=function(e,t){const n=String(t||"").trim();return e.members.find(e=>e.id===n)||e.members.find(e=>e.callName===n)||null}(i,n?.member);if(!a)return`专家团「${i.name}」中没有成员「${n?.member}」(可用:${i.members.map(e=>`${e.id}(${e.callName})`).join("、")})`;const c=String(n?.task||"").trim();if(!c)return"缺少 task(任务说明)";const m=`${i.name}|${a.id}|${c.slice(0,40)}`,p=o.get(m)||0;if(p>=2)return`该任务线已按异常协议终止(成员「${a.callName}」同一任务累计失败满 2 次)。请向用户说明原因并给出替代方案。`;try{const r=await l({squad:i,member:a,task:c,context:n?.context,expectation:n?.expectation,config:e,cwd:t,readOnlyExtras:s});return/^(成员执行失败|平台调用失败)/.test(r)?(o.set(m,p+1),`${r}(累计失败 ${p+1}/2,可修正任务说明后重试或改派他人)`):`成员「${a.callName}」对任务【${c.replace(/\s+/g," ").slice(0,60)}】的产出:\n${r}`}catch(e){if(e&&"AbortError"===e.name)throw e;return o.set(m,p+1),`成员「${a.callName}」执行失败:${e.message}(累计失败 ${p+1}/2)`}}}}export function squadCommandPrompt(e,t,n){return buildLeaderPrompt(e,n)+"\n\n---\n\n用户任务:"+t+`\n\n请以主理人${e.leader}的身份完成:先做任务分配预检(含预设工作流匹配),然后经 dispatch_member`+`(squad="${e.name}")派发执行(独立任务同轮并行),跨成员信息流经你中转,最后按交付检查清单汇总交付。`}export const SQUAD_TEMPLATE="---\nname: {NAME}\ndescription: 内容创作专家团(示例:改成你的团队)\nleader: 司远\nmode: dispatch\nwhen: 需要成套内容创作/深度文章且涉及调研-写作-审校多角色协作时\n---\n\n擅长把用户的创作需求拆解为具体执行方案,并调度合适的成员高效交付。\n\n## 成员\n- researcher | 调研员 | 资料收集、事实核查、竞品调研 | agent:researcher\n- writer | 笔澜 | 公众号长文、营销文案、脚本 |\n- reviewer | 审校 | 审查产出、给出修订意见 |\n- fetcher | 源泉 | (示例)平台数字人自动绑定查数 | platform:auto:datahuman\n\n## 成员人设\n### writer\n角色:资深中文内容主笔。\n工作方法:先定读者与核心论点,再列结构,后成文;每段只讲一件事;用事实与例子代替空话。\n交付标准:结构清晰可扫读、观点有依据、结尾给行动建议;附 3 条备选标题。\n\n## 预检\n- 写长文/文案 → writer,不要派给 researcher\n- 资料/数据收集 → researcher,产出交给 writer 前先经你提炼要点\n\n## 工作流\n### 深度长文(触发:用户要深度文章/长文)\n1. researcher:收集资料与事实\n2. writer:按资料成稿\n3. reviewer:审查并给修订意见(mode=review 时必经)\n交付:终稿 Markdown + 三条标题备选\n";
1
+ import e from"node:fs";import t from"node:path";import n from"node:os";import{fileURLToPath as r}from"node:url";import{runTurn as s}from"./agent.js";import{createPermissions as o}from"./permissions.js";import{loadAgents as i,makeSubEvents as a,withSubagentSlot as c}from"./subagent.js";import{streamChat as m}from"./api.js";import{discoverExperts as l}from"./experts.js";export const BUNDLED_SQUADS_DIR=t.join(t.dirname(r(import.meta.url)),"..","squads");export function squadDirs(e){const r=[];return e&&r.push(t.join(e,".huicai","squads")),r.push(t.join(n.homedir(),".huicai","squads")),r}export function parseSquadDoc(e,t=""){const{attrs:n,body:r}=function(e){const t={attrs:{},body:String(e||"").trim()},n=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(t.body);if(!n)return t;t.body=t.body.slice(n[0].length).trim();for(const e of n[1].split(/\r?\n/)){const n=/^([A-Za-z][\w-]*)\s*:\s*(.*)$/.exec(e.trim());n&&(t.attrs[n[1]]=n[2].trim().replace(/^["']|["']$/g,""))}return t}(e),s={name:n.name||t,description:n.description||"",leader:n.leader||"主理人",mode:["dispatch","review","ensemble"].includes(n.mode)?n.mode:"dispatch",when:n.when||"",mission:"",members:[],precheck:[],workflows:[]},o=new Map,i=r.split(/^##\s+/m);s.mission=(i[0]||"").trim();for(const e of i.slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1);if(/^成员人设/.test(n))for(const e of r.split(/^###\s+/m).slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1).trim();n&&r&&o.set(n,r)}else if(/^成员/.test(n))for(const e of r.split(/\r?\n/)){const t=/^\s*[-*]\s+(.+)$/.exec(e);if(!t)continue;const n=t[1].split("|").map(e=>e.trim());n[0]&&s.members.push({id:n[0],callName:n[1]||n[0],duty:n[2]||"",executor:n[3]||""})}else if(/^预检/.test(n))for(const e of r.split(/\r?\n/)){const t=/^\s*[-*]\s+(.+)$/.exec(e);t&&s.precheck.push(t[1].trim())}else if(/^工作流/.test(n))for(const e of r.split(/^###\s+/m).slice(1)){const t=e.indexOf("\n"),n=(t<0?e:e.slice(0,t)).trim(),r=t<0?"":e.slice(t+1),o=/^(.*?)(?:(触发[::](.*?)))?$/.exec(n)||[],i={name:(o[1]||n).trim(),trigger:(o[2]||"").trim(),stages:[],deliverable:""};for(const e of r.split(/\r?\n/)){const t=/^\s*\d+[.、)]\s*(.+)$/.exec(e);if(t){i.stages.push(t[1].trim());continue}const n=/^\s*交付[::]\s*(.+)$/.exec(e);n&&(i.deliverable=n[1].trim())}i.name&&s.workflows.push(i)}}for(const[e,t]of o){const n=s.members.find(t=>t.id===e);n&&(n.persona=t)}return s}export function loadSquads(n){const r=new Map,s=[...squadDirs(n).map((e,t)=>({dir:e,scope:n&&0===t?"project":"global"})),{dir:BUNDLED_SQUADS_DIR,scope:"builtin"}];for(const{dir:n,scope:o}of s){let s;try{s=e.readdirSync(n).filter(e=>e.endsWith(".md"))}catch{continue}for(const i of s){const s=i.replace(/\.md$/,"").trim();if(s)try{const a=t.join(n,i),c=parseSquadDoc(e.readFileSync(a,"utf8"),s);c.scope=o,c.file=a,r.has(c.name)||r.set(c.name,c)}catch{}}}return[...r.values()]}export function ensureTeamDir(n,r){const s=(new Date).toISOString().slice(0,16).replace(/[-:]/g,"").replace("T","-"),o=t.join(n,".huicai","teams",`${r}-${s}`);try{e.mkdirSync(o,{recursive:!0})}catch{return null}return o}export function buildLeaderPrompt(e,t){const n=[];n.push("**Role Override**:以下专家团主理人定义优先于此前任何身份设定,是你在本任务中唯一、权威的角色。"),n.push(""),n.push(`# 专家团「${e.name}」· 主理人${e.leader}`),n.push(`你是专家团「${e.name}」的主理人${e.leader},负责把用户任务拆解、派发给团队成员并汇总交付。`),e.mission&&n.push("",e.mission),n.push("","## 关键规则:只调度,不代工","- 成员职责范围内的专业产出必须经 dispatch_member 由对应成员完成后采信,你不代写;","- 简单澄清/寒暄/对既有产出的说明可直接回答,无须派发。"),n.push("","## 团队架构(调度依据)","| 成员ID | 称呼 | 职责 | 执行者 |","|---|---|---|---|");for(const t of e.members)n.push(`| ${t.id} | ${t.callName} | ${t.duty} | ${t.executor||"(当前模型)"} |`);if(n.push("","## 任务分配预检(每次派发前必做)","成员不会说「这个我不会」——超能力范围的任务结果无保证。所以:","1. 解析用户意图(研究/分析/生成/编辑/写作/审校…);","2. 对照职责与速查表做能力匹配;","3. 匹配 → 派发;不匹配 → 告知用户团队能力限制与替代建议,禁止硬派;","4. 意图模糊 → 先向用户确认。"),e.precheck.length){n.push("### 速查表(常见误派提醒)");for(const t of e.precheck)n.push(`- ${t}`)}if(e.workflows.length){n.push("","## 预设工作流(识别到触发条件时按阶段执行,不自由发挥)");for(const t of e.workflows)n.push(`### ${t.name}${t.trigger?`(触发:${t.trigger})`:""}`),t.stages.forEach((e,t)=>n.push(`${t+1}. ${e}`)),t.deliverable&&n.push(`交付:${t.deliverable}`)}return n.push("","## 团队协作规范","1. 成员在隔离上下文执行,看不到本对话与其他成员产出——任务说明必须独立自足;","2. 跨成员信息流经你中转:把上一阶段产出的必要部分放进下一位成员的 context;","3. 互相独立的任务同一轮并行派发(一次输出多个 dispatch_member 调用),减少串行等待;","4. 成员结论为准:如实汇编;产出不符时修正任务说明重派,而非替他改写。"),"review"===e.mode?n.push("### 生成-审查模式(本团约束)","每个关键产出完成后,必须把「产出全文+原始要求」派给职责匹配的审查成员评审,按意见让原成员修订至通过;未经审查的产出不得作为最终交付。"):"ensemble"===e.mode&&n.push("### 多候选聚合模式(本团约束)","对开放性核心任务,把同一任务并行派给多位匹配成员(或不同侧重多份),对比候选择优或综合,并向用户说明取舍。"),n.push("","## 异常处理(失败三级升级)","- 一级:修正任务说明(更自足、更具体、补 context)后重派原成员;同一任务累计失败满 2 次系统会终止该任务线;","- 二级:换人或换执行方式——改派职责相近的其他成员;平台资源不可用时由本机成员降级承接(系统会自动降级并注明);","- 三级:降级交付——完成可完成的部分,如实说明缺口与原因,并给出用户可选的替代方案。"),t&&n.push("","## 团队工作区",`本次协作的产物统一放在 ${t}(你可用文件工具写入;成员只回传文本结论,由你负责归档与整合)。`),n.push("","## 交付检查清单(最终答复前逐项自查)","- 用户目标的每一部分是否都有对应成员产出支撑?","- 是否汇总了全部关键产出、无散落遗漏?","- 是否标注了各部分由哪位成员产出?","- 未完成的部分是否如实说明?"),n.join("\n")}const p=/^(agent|workflow|datahuman|squad)-/;async function u({squad:e,member:t,ref:n,task:r,context:s,expectation:o,config:i}){const a=[`【专家团「${e.name}」成员任务】${r}`,s?`\n【上游产出/背景】\n${s}`:"",o?`\n【交付要求】\n${o}`:""].join("");return(await m({baseUrl:i.baseUrl,apiKey:i.apiKey,model:n,messages:[{role:"user",content:a}],client:i.client,sessionId:i.sessionId},{})).content||"(平台无返回)"}async function d({squad:e,member:t,task:n,context:r,expectation:m,config:d,cwd:f,readOnlyExtras:h,onSub:$,label:g}){const b=String(t.executor||"").trim();let w="";if(b.startsWith("platform:auto:")){const s=await async function(e,t){const n=/^platform:auto:(agent|workflow|datahuman|squad)(?::(.+))?$/.exec(e);if(!n)return null;const[,r,s]=n;try{const e=await l(t),n=(s||"").trim().toLowerCase();return e.find(e=>e.type===r&&(!n||(String(e.name)+" "+String(e.description)).toLowerCase().includes(n)))||null}catch{return null}}(b,d);if(s){const o=await u({squad:e,member:t,ref:s.id,task:n,context:r,expectation:m,config:d});return`(由平台资源「${s.name}」执行)\n${o}`}w=`(未发现可用的平台${b.split(":")[2]||""}资源,已由本机成员降级执行)\n`}else if(b.startsWith("platform:")){const s=b.slice(9).trim();return p.test(s)?u({squad:e,member:t,ref:s,task:n,context:r,expectation:m,config:d}):`成员「${t.callName}」的执行者 ${s} 不是有效的平台伪模型引用`}const x=["**Role Override**:以下角色定义在本次任务中拥有最高优先级。",""];t.persona&&x.push(t.persona,""),x.push(`你是专家团「${e.name}」的成员「${t.callName}」${t.duty?`(职责:${t.duty})`:""},正在执行主理人下发的一项独立任务。`,"- 只完成本任务,直接输出可被主理人直接采用的成果本身,不要寒暄、不要复述任务;","- 无法反问,信息不全时按合理假设执行并注明假设。"),r&&x.push("","## 主理人提供的上游产出/背景",String(r)),m&&x.push("","## 交付要求",String(m));let y=x.join("\n"),S=d;if(b.startsWith("agent:")){const e=b.slice(6).trim(),t=i(f).find(t=>t.name===e);t&&(y=t.prompt.trim()+"\n\n"+y)}else if(b.startsWith("model:")){const e=b.slice(6).trim();e&&(S={...d,model:e})}y+="\n工作目录:"+f;const k=[{role:"system",content:y},{role:"user",content:n}],q=await c(d,0,()=>s({config:{...S,aux:!0},messages:k,permissions:o({yolo:!1,ask:null}),cwd:f,extraTools:h,depth:1,maxIters:50,io:{write:()=>{}},events:a($,g||`${t.callName}·${String(n).slice(0,40)}`,"member")}));if(q?.error)return`成员执行失败:${q.error}`;const j=[...k].reverse().find(e=>"assistant"===e.role&&e.content);return j?w+j.content:"(成员未产出结论)"}export function makeDispatchMemberTool({config:e,cwd:t,extraTools:n=[],onSub:r=null}){const s=loadSquads(t);if(!s.length)return null;const o=n.filter(e=>e.readOnly),i=new Map;return{readOnly:!0,spec:{type:"function",function:{name:"dispatch_member",description:"把一项独立、自足的任务派发给某个专家团的一位成员:成员在隔离上下文中执行并只回传成果。复杂/跨角色任务优先按团开工:对照下方各团「何时使用」自主选团,按其预设工作流分阶段派发(互相独立的任务同一轮多次调用并行);成员产出由你汇总并用本机工具落地,落地后验证。派发前先做任务分配预检(对照成员职责做能力匹配)。\n可用专家团:\n"+s.map(e=>`- ${e.name}:${e.when||e.description||""}(成员 ${e.members.map(e=>e.id).join("/")})`).join("\n"),parameters:{type:"object",properties:{squad:{type:"string",enum:s.map(e=>e.name),description:"专家团名称"},member:{type:"string",description:"成员ID(推荐)或称呼,须来自该团的团队架构表"},task:{type:"string",description:"下发的任务说明——必须独立自足(成员看不到本对话与其他成员产出)"},context:{type:"string",description:"可选:上游产出/背景(跨成员信息流经主理人中转,把必要部分放这里)"},expectation:{type:"string",description:"可选:交付要求/验收标准"}},required:["squad","member","task"]}}},run:async n=>{const a=s.find(e=>e.name===String(n?.squad||"").trim());if(!a)return`未找到专家团「${n?.squad}」(可用:${s.map(e=>e.name).join("、")})`;const c=function(e,t){const n=String(t||"").trim();return e.members.find(e=>e.id===n)||e.members.find(e=>e.callName===n)||null}(a,n?.member);if(!c)return`专家团「${a.name}」中没有成员「${n?.member}」(可用:${a.members.map(e=>`${e.id}(${e.callName})`).join("、")})`;const m=String(n?.task||"").trim();if(!m)return"缺少 task(任务说明)";const l=`${a.name}|${c.id}|${m.slice(0,40)}`,p=i.get(l)||0;if(p>=2)return`该任务线已按异常协议终止(成员「${c.callName}」同一任务累计失败满 2 次)。请向用户说明原因并给出替代方案。`;try{const s=await d({squad:a,member:c,task:m,context:n?.context,expectation:n?.expectation,config:e,cwd:t,readOnlyExtras:o,onSub:r,label:`${c.callName}·${m.replace(/\s+/g," ").slice(0,40)}`});return/^(成员执行失败|平台调用失败)/.test(s)?(i.set(l,p+1),`${s}(累计失败 ${p+1}/2,可修正任务说明后重试或改派他人)`):`成员「${c.callName}」对任务【${m.replace(/\s+/g," ").slice(0,60)}】的产出:\n${s}`}catch(e){if(e&&"AbortError"===e.name)throw e;return i.set(l,p+1),`成员「${c.callName}」执行失败:${e.message}(累计失败 ${p+1}/2)`}}}}export function squadCommandPrompt(e,t,n){return buildLeaderPrompt(e,n)+"\n\n---\n\n用户任务:"+t+`\n\n请以主理人${e.leader}的身份完成:先做任务分配预检(含预设工作流匹配),然后经 dispatch_member`+`(squad="${e.name}")派发执行(独立任务同轮并行),跨成员信息流经你中转,最后按交付检查清单汇总交付。`}export const SQUAD_TEMPLATE="---\nname: {NAME}\ndescription: 内容创作专家团(示例:改成你的团队)\nleader: 司远\nmode: dispatch\nwhen: 需要成套内容创作/深度文章且涉及调研-写作-审校多角色协作时\n---\n\n擅长把用户的创作需求拆解为具体执行方案,并调度合适的成员高效交付。\n\n## 成员\n- researcher | 调研员 | 资料收集、事实核查、竞品调研 | agent:researcher\n- writer | 笔澜 | 公众号长文、营销文案、脚本 |\n- reviewer | 审校 | 审查产出、给出修订意见 |\n- fetcher | 源泉 | (示例)平台数字人自动绑定查数 | platform:auto:datahuman\n\n## 成员人设\n### writer\n角色:资深中文内容主笔。\n工作方法:先定读者与核心论点,再列结构,后成文;每段只讲一件事;用事实与例子代替空话。\n交付标准:结构清晰可扫读、观点有依据、结尾给行动建议;附 3 条备选标题。\n\n## 预检\n- 写长文/文案 → writer,不要派给 researcher\n- 资料/数据收集 → researcher,产出交给 writer 前先经你提炼要点\n\n## 工作流\n### 深度长文(触发:用户要深度文章/长文)\n1. researcher:收集资料与事实\n2. writer:按资料成稿\n3. reviewer:审查并给修订意见(mode=review 时必经)\n交付:终稿 Markdown + 三条标题备选\n";
package/src/subagent.js CHANGED
@@ -1 +1 @@
1
- import t from"node:fs";import e from"node:path";import n from"node:os";import{runTurn as r}from"./agent.js";import{createPermissions as o}from"./permissions.js";import{parseFrontmatter as i}from"./skills.js";export function loadAgents(r){const o=[r?e.join(r,".huicai","agents"):null,e.join(n.homedir(),".huicai","agents")].filter(Boolean),a=new Map;for(const n of o){let r;try{r=t.readdirSync(n).filter(t=>t.endsWith(".md"))}catch{continue}for(const o of r){const r=o.replace(/\.md$/,"").trim();if(r&&!a.has(r))try{const s=t.readFileSync(e.join(n,o),"utf8"),c=i(s);a.set(r,{name:(c.name||r).trim(),description:c.description||"",prompt:c.body||s})}catch{}}}return[...a.values()]}async function a({config:t,cwd:e,readOnlyExtras:n,depth:i,objective:a,context:s,agentDef:c}){if(!a)return"未提供子任务目标 objective";const p=c?c.prompt.trim()+"\n\n工作目录:"+e+(s&&String(s).trim()?"\n\n## 背景\n"+s:""):function(t,e){let n="你是一个专注的子智能体,只负责完成主智能体委派的单一子任务。要求:聚焦目标;用可用的只读工具(读文件 / 搜索 / 列目录等)收集信息;直接输出结构清晰、可被主智能体直接复用的最终结论,不要寒暄,也不要复述任务本身。\n工作目录:"+t+"\n";e&&String(e).trim()&&(n+="\n## 主智能体提供的背景\n"+e+"\n");return n}(e,s),l=[{role:"system",content:p},{role:"user",content:a}],d=o({yolo:!1,ask:null}),m=makeSubagentTool({config:t,cwd:e,extraTools:n,depth:i+1}),g=m?[...n,m]:n,f=await r({config:t,messages:l,permissions:d,cwd:e,extraTools:g,depth:i+1,maxIters:50,io:{write:()=>{}}});if(f?.error)return`子智能体执行失败:${f.error}`;const u=[...l].reverse().find(t=>"assistant"===t.role&&t.content);return u?u.content:"(子智能体未产出结论)"}export function makeSubagentTool({config:t,cwd:e,extraTools:n=[],depth:r=0}){if(r>=2)return null;const o=loadAgents(e),i={type:"function",function:{name:"dispatch_agent",description:"把一个明确、独立、自足的子任务委派给子智能体:它在隔离上下文中自主使用(只读)工具完成后只回传结论。适合可并行的调研/检索/分析类子任务,让主线程上下文保持精简。可多次调用以分解复杂任务。",parameters:{type:"object",properties:{objective:{type:"string",description:"要子智能体完成的单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需的背景信息"},...o.length?{agent:{type:"string",enum:o.map(t=>t.name),description:"可选:使用某个已定义的子智能体人格 — "+o.map(t=>t.name+(t.description?`(${t.description.slice(0,40)})`:"")).join(";")}}:{}},required:["objective"]}}},s=n.filter(t=>t.readOnly);return{readOnly:!0,spec:i,run:async n=>{const i=n?.agent?o.find(t=>t.name===n.agent):null;return a({config:t,cwd:e,readOnlyExtras:s,depth:r,objective:String(n?.objective||"").trim(),context:n?.context,agentDef:i})}}}export function makeDispatchAgentsTool({config:t,cwd:e,extraTools:n=[],depth:r=0}){if(r>=2)return null;const o=loadAgents(e),i=n.filter(t=>t.readOnly);return{readOnly:!0,spec:{type:"function",function:{name:"dispatch_agents",description:"一次性把多个**独立、可并行**的子任务同时委派给多个子智能体(隔离上下文、只读工具),并行执行后聚合各自结论。适合把一个较大目标拆成 2-6 个可并行的调研/检索/分析子任务同时推进,显著减少串行等待。每个子任务需自足。",parameters:{type:"object",properties:{tasks:{type:"array",description:"并行子任务数组(建议 2-6 个,上限 8)",items:{type:"object",properties:{objective:{type:"string",description:"单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需背景"},...o.length?{agent:{type:"string",enum:o.map(t=>t.name),description:"可选:指定子智能体人格"}}:{}},required:["objective"]}}},required:["tasks"]}}},run:async n=>{const s=Array.isArray(n?.tasks)?n.tasks.slice(0,8):[];if(!s.length)return"未提供 tasks(需要一个子任务数组)";return(await async function(t,e,n){const r=new Array(t.length);let o=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;o<t.length;){const e=o++;r[e]=await n(t[e],e)}})),r}(s,4,async n=>{const s=n?.agent?o.find(t=>t.name===n.agent):null,c=await a({config:t,cwd:e,readOnlyExtras:i,depth:r,objective:String(n?.objective||"").trim(),context:n?.context,agentDef:s});return{objective:String(n?.objective||"").replace(/\s+/g," ").slice(0,80),agent:n?.agent,out:c}})).map((t,e)=>`【子任务 ${e+1}${t.agent?" · "+t.agent:""}】${t.objective}\n${t.out}`).join("\n\n---\n\n")}}}
1
+ import t from"node:fs";import e from"node:path";import n from"node:os";import{runTurn as r}from"./agent.js";import{createPermissions as o}from"./permissions.js";import{parseFrontmatter as i}from"./skills.js";import{getIndex as s}from"./codeindex.js";let a=0;const c=[];export async function withSubagentSlot(t,e,n){if(e>0)return n();a>=function(t){const e=Number(t?.subagentConcurrency);return Number.isFinite(e)&&e>0?Math.max(1,Math.floor(e)):8}(t)?await new Promise(t=>c.push(t)):a++;try{return await n()}finally{const t=c.shift();t?t():a--}}function l(t){try{s(t)}catch{}}export function loadAgents(r){const o=[r?e.join(r,".huicai","agents"):null,e.join(n.homedir(),".huicai","agents")].filter(Boolean),s=new Map;for(const n of o){let r;try{r=t.readdirSync(n).filter(t=>t.endsWith(".md"))}catch{continue}for(const o of r){const r=o.replace(/\.md$/,"").trim();if(r&&!s.has(r))try{const a=t.readFileSync(e.join(n,o),"utf8"),c=i(a);s.set(r,{name:(c.name||r).trim(),description:c.description||"",prompt:c.body||a})}catch{}}}return[...s.values()]}function u(t,e){return t+(e&&e.path?" "+e.path:e&&e.command?": "+String(e.command).split("\n")[0].slice(0,80):e&&e.objective?": "+String(e.objective).slice(0,60):e&&e.query?" "+String(e.query).slice(0,60):e&&e.pattern?" "+e.pattern:"")}export function makeSubEvents(t,e,n){if("function"!=typeof t)return;const r=(r,o)=>{try{t({scope:e,kind:n,type:r,...o})}catch{}};return{onToolStart:(t,e)=>r("tool",{name:t,text:u(t,e)}),onToolResult:(t,e)=>r("toolresult",{name:t,text:String(e).split("\n")[0].slice(0,160)}),onInfo:t=>r("info",{text:String(t).slice(0,200)}),onAssistantText:t=>r("text",{text:String(t).slice(0,400)})}}async function p({config:t,cwd:e,readOnlyExtras:n,depth:i,objective:s,context:a,agentDef:c,onSub:l,label:u}){if(!s)return"未提供子任务目标 objective";const p={...t,aux:!0};t.subagentModel&&t.subagentModel!==t.model&&(p.model=t.subagentModel);const g=c?c.prompt.trim()+"\n\n工作目录:"+e+(a&&String(a).trim()?"\n\n## 背景\n"+a:""):function(t,e){let n="你是一个专注的子智能体,只负责完成主智能体委派的单一子任务。要求:聚焦目标;用可用的只读工具(读文件 / 搜索 / 列目录等)收集信息;直接输出结构清晰、可被主智能体直接复用的最终结论,不要寒暄,也不要复述任务本身。\n工作目录:"+t+"\n";e&&String(e).trim()&&(n+="\n## 主智能体提供的背景\n"+e+"\n");return n}(e,a),m=[{role:"system",content:g},{role:"user",content:s}],d=o({yolo:!1,ask:null}),f=makeSubagentTool({config:p,cwd:e,extraTools:n,depth:i+1,onSub:l}),y=f?[...n,f]:n,h=makeSubEvents(l,u||String(s).slice(0,60),"agent"),b=await withSubagentSlot(t,i,()=>r({config:p,messages:m,permissions:d,cwd:e,extraTools:y,depth:i+1,maxIters:t.subagentMaxIters||40,io:{write:()=>{}},events:h}));if(b?.error)return`子智能体执行失败:${b.error}`;const x=[...m].reverse().find(t=>"assistant"===t.role&&t.content);return x?x.content:"(子智能体未产出结论)"}export function makeSubagentTool({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){if(r>=2)return null;const i=loadAgents(e),s={type:"function",function:{name:"dispatch_agent",description:"把一个明确、独立、自足的子任务委派给子智能体:它在隔离上下文中自主使用(只读)工具完成后只回传结论。适合可并行的调研/检索/分析类子任务,让主线程上下文保持精简。可多次调用以分解复杂任务。",parameters:{type:"object",properties:{objective:{type:"string",description:"要子智能体完成的单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需的背景信息"},...i.length?{agent:{type:"string",enum:i.map(t=>t.name),description:"可选:使用某个已定义的子智能体人格 — "+i.map(t=>t.name+(t.description?`(${t.description.slice(0,40)})`:"")).join(";")}}:{}},required:["objective"]}}},a=n.filter(t=>t.readOnly);return{readOnly:!0,spec:s,run:async n=>{l(e);const s=n?.agent?i.find(t=>t.name===n.agent):null,c=String(n?.objective||"").trim();return p({config:t,cwd:e,readOnlyExtras:a,depth:r,objective:c,context:n?.context,agentDef:s,onSub:o,label:c.slice(0,60)})}}}export function makeDispatchAgentsTool({config:t,cwd:e,extraTools:n=[],depth:r=0,onSub:o=null}){if(r>=2)return null;const i=loadAgents(e),s=n.filter(t=>t.readOnly);return{readOnly:!0,spec:{type:"function",function:{name:"dispatch_agents",description:"一次性把多个**独立、可并行**的子任务同时委派给多个子智能体(隔离上下文、只读工具),并行执行后聚合各自结论。适合把一个较大目标拆成 3-8 个可并行的调研/检索/分析子任务同时推进,显著减少串行等待(上限 12 个)。每个子任务需自足;多个子任务共用的背景放 shared_context,不要在每个 context 里重复抄一遍。复杂任务优先用它一次性铺开,而不是反复单发 dispatch_agent 串行等待。",parameters:{type:"object",properties:{tasks:{type:"array",description:"并行子任务数组(建议 3-8 个,上限 12)",items:{type:"object",properties:{objective:{type:"string",description:"单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需背景"},...i.length?{agent:{type:"string",enum:i.map(t=>t.name),description:"可选:指定子智能体人格"}}:{}},required:["objective"]}},shared_context:{type:"string",description:"可选:所有子任务共用的背景(会附加到每个子任务,省去逐条重复)"}},required:["tasks"]}}},run:async n=>{const a=Array.isArray(n?.tasks)?n.tasks:[],c=a.slice(0,12);if(!c.length)return"未提供 tasks(需要一个子任务数组)";const u=a.length-c.length;l(e);const g=String(n?.shared_context||"").trim(),m=(await async function(t,e,n){const r=new Array(t.length);let o=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;o<t.length;){const e=o++;r[e]=await n(t[e],e)}})),r}(c,c.length,async(n,a)=>{const c=n?.agent?i.find(t=>t.name===n.agent):null,l=String(n?.objective||"").trim(),u=String(n?.context||"").trim(),m=g&&u?`${g}\n\n${u}`:g||u;try{const i=await p({config:t,cwd:e,readOnlyExtras:s,depth:r,objective:l,context:m,agentDef:c,onSub:o,label:`#${a+1} ${l.slice(0,50)}`});return{objective:l.replace(/\s+/g," ").slice(0,80),agent:n?.agent,out:i}}catch(t){if(t&&"AbortError"===t.name)throw t;return{objective:l.replace(/\s+/g," ").slice(0,80),agent:n?.agent,out:`子任务执行异常:${t.message}`}}})).map((t,e)=>`【子任务 ${e+1}${t.agent?" · "+t.agent:""}】${t.objective}\n${t.out}`).join("\n\n---\n\n");return u>0?`${m}\n\n---\n\n(注意:本次提交了 ${a.length} 个子任务,超出上限 12,末尾 ${u} 个未执行,需要的话再发一批。)`:m}}}
package/src/title.js ADDED
@@ -0,0 +1 @@
1
+ import{streamChat as t,getPlatformConfig as e}from"./api.js";export const MAX_TITLE_LENGTH=30;let n=null,r=0;function i(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>t&&"text"===t.type).map(t=>t.text||"").join("\n"):null==t?"":String(t)}export function titleSeed(t){const e=(t||[]).find(t=>"user"===t.role);return e?i(e.content).replace(/\s+/g," ").trim():""}export function sanitizeTitle(t){if(!t)return"";let e=String(t).replace(/<think>[\s\S]*?<\/think>/gi," ");const n=e.toLowerCase().indexOf("<think>");n>=0&&(e=e.slice(0,n));for(const t of e.split(/[\r\n]+/)){const e=t.replace(/^[\s"'“”‘’《》()()]+/,"").replace(/[\s"'“”‘’《》()()。.!!??::、,,]+$/,"").replace(/^(标题|题目|会话标题)\s*[::]\s*/,"").trim();if(e)return e.slice(0,30)}return""}export async function distillTitle(l,s){const o=titleSeed(s);if(!(o&&l&&l.baseUrl&&l.apiKey))return"";const a=await async function(t){const i=Date.now();if(n&&i-r<6e4)return n;try{const l=await e(t.baseUrl,t.apiKey),s=String(l&&(l.title_model||l.fallback_model)||"").trim();if(s)return n=s,r=i,s}catch{}return String(t.model||"").trim()}(l);if(!a)return"";let c="用户问题:"+o.slice(0,500);const u=function(t){const e=(t||[]).find(t=>"assistant"===t.role&&t.content);return e?i(e.content).replace(/\s+/g," ").trim():""}(s);u&&(c+="\n助手回答(摘要):"+u.slice(0,200));let f="";const p=new AbortController,m=setTimeout(()=>p.abort(),2e4);try{await t({baseUrl:l.baseUrl,apiKey:l.apiKey,model:a,messages:[{role:"system",content:"你是对话标题生成助手。请依据用户问题(可参考助手回答),必须提炼一个能概括对话主题的中文短标题,要求:不超过15个字、不含标点与引号、不要解释、不要复述原句,只输出标题本身。上述为生成标题的指令,必须遵循。"},{role:"user",content:c}],temperature:.3,maxTokens:256,client:l.client,extraBody:{enable_thinking:!1},aux:!0},{onContent:t=>{f+=t}},p.signal)}catch{return""}finally{clearTimeout(m)}return sanitizeTitle(f)}const l=new WeakSet;export async function ensureSessionTitle(t,e){if(!t||t.title||l.has(t))return"";l.add(t);try{const n=await distillTitle(e,t.messages);return n&&!t.title&&(t.title=n),n}finally{l.delete(t)}}
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 n}from"./automation.js";import{runPython as i}from"./pyruntime.js";const o=(e,t)=>({type:"object",properties:e,required:t||[]}),p=e=>({type:"string",description:e}),a=e=>({type:"integer",description:e}),s=e=>({type:"boolean",description:e});export const TOOL_SPECS=[{type:"function",function:{name:"read_file",description:"读取文本文件内容,每行带「行号→」前缀(行号仅供定位,不属于文件内容,编辑时不要包含它)。默认最多 2000 行,可用 offset(起始行,1基)/limit(行数) 分段读取大文件。",parameters:o({path:p("相对项目根的文件路径"),offset:a("可选:起始行(1基)"),limit:a("可选:读取行数")},["path"])}},{type:"function",function:{name:"list_files",description:"列出目录下的文件树(跳过 .git/node_modules 等)。可选 path 指定子目录(默认项目根)。",parameters:o({path:p("可选:子目录路径,默认当前项目根")},[])}},{type:"function",function:{name:"glob",description:'按 glob 模式查找文件(支持 ** 与 *),如 "**/*.ts"、"src/**/*.java"。',parameters:o({pattern:p("glob 模式")},["pattern"])}},{type:"function",function:{name:"search_code",description:"按「概念/功能」检索最相关的代码块(BM25 相关性排序,比 grep 更懂「哪段代码在做这件事」)。适合「登录逻辑在哪」「哪里处理支付回调」这类找相关实现;要找精确字符串仍用 grep。返回 文件:行范围 + 代码块。",parameters:o({query:p("要找的功能/概念(自然语言或关键词组合,多写几个相关词更准)"),k:a("可选:返回块数,默认 8,上限 20")},["query"])}},{type:"function",function:{name:"grep",description:"在文件内容中按正则搜索(底层用 ripgrep,大型仓库快、自动尊重 .gitignore、不受单文件大小限制→大文件也能命中)。返回 匹配文件:行号: 该行。可选 path 限定目录、glob 限定文件名模式、ignore_case 忽略大小写、files_only 只列出命中文件、max_results 上限。",parameters:o({pattern:p("正则表达式"),path:p("可选:搜索目录,默认项目根"),glob:p("可选:文件名 glob 过滤"),ignore_case:s("可选:忽略大小写(默认否)"),files_only:s("可选:只返回命中文件列表,不含匹配行(默认否)"),max_results:a("可选:最多返回条数(默认 200,上限 500)")},["pattern"])}},{type:"function",function:{name:"read_document",description:"读取 PDF/Word/Excel/PPT/RTF 等文档的纯文本内容(经平台解析,read_file 读不了这类二进制文档)。大型文档分段通读:段末标注「全文共N字符/本段区间」,继续调用并传 offset 读取后续,直到「已到结尾」。对话附件若标注「已截断」,用本工具读取被省略的部分。",parameters:o({path:p("相对项目根的文档路径"),offset:a("可选:窗口起始字符位置(0 基,默认 0,从头顺序读取)"),max_chars:a("可选:本段最大字符数(默认 16000)")},["path"])}},{type:"function",function:{name:"write_file",description:"把文本写入文件(覆盖式,自动建父目录)。仅限当前项目目录内。仅用于新建文件或全量重写;小改动优先 edit_file。生成超长文件(长文档/PPT/大代码文件)时分段产出:先写首段,后续每段设 append=true 追加到文件末尾,避免单次内容过长被截断。",parameters:o({path:p("相对项目根的文件路径"),content:p("本次写入的内容(append=true 时为追加的续段)"),append:s("可选:true=追加到文件末尾(分段产出大文件);默认覆盖写")},["path","content"])}},{type:"function",function:{name:"edit_file",description:"精确替换文件中的片段:old_string 必须是文件原文(不含 read_file 的行号前缀)且在文件中唯一(除非 replace_all=true)。仅限项目目录内。",parameters:o({path:p("相对项目根的文件路径"),old_string:p("被替换的原文(需在文件中唯一,除非 replace_all;不要包含行号前缀)"),new_string:p("替换为的新内容"),replace_all:s("是否替换全部匹配(默认否)")},["path","old_string","new_string"])}},{type:"function",function:{name:"run_bash",description:"在项目目录下执行一条 shell 命令并返回输出(stdout+stderr+退出码)。用于构建、测试、git、脚手架等。默认超时 120s(可用 timeout 调整)。避免用它读文件/搜索(用 read_file/grep 更高效)。",parameters:o({command:p("要执行的 shell 命令"),timeout:a("可选:超时毫秒")},["command"])}},{type:"function",function:{name:"run_python",description:"在内置托管 Python(含 pandas/openpyxl/python-docx/python-pptx/pypdf/matplotlib/tabulate)中执行代码,用于数据分析与 Excel/Word/PPT/PDF 文档处理(读写原始文档优先用它,勿依赖 read_document 的纯文本摘要做精确分析)。首次调用会自动准备运行时(可能需数分钟,进度随结果返回)。结果必须 print 输出;图表用 matplotlib savefig 落盘(Agg 无窗口后端)。缺库时用 packages 按需安装。",parameters:o({code:p("内联 Python 代码(与 script 二选一)"),script:p("脚本路径(相对项目根,与 code 二选一)"),args:{type:"array",items:{type:"string"},description:"可选:传给脚本的命令行参数"},packages:{type:"array",items:{type:"string"},description:"可选:额外 PyPI 包(如 reportlab、xlsxwriter),自动按需安装"},timeout:a("可选:超时毫秒,默认 120000,上限 600000")},[])}},{type:"function",function:{name:"fetch_url",description:"抓取一个 http/https 网页或接口(GET),HTML 自动转为纯文本(按页面字符集解码,附标题)。用于查在线文档、API 说明、Issue、公开数据页等。需要顺着页面继续跳转/翻页采集时传 links=true 获取「锚文本 → 绝对URL」清单。结果可能很长,尽量带着明确目的使用。",parameters:o({url:p("完整 URL(http/https)"),timeout:a("可选:超时毫秒,默认 30000"),links:s("可选:附带页面链接清单(继续跳转/翻页采集用),默认不附")},["url"])}},...r].filter(e=>!("1"===process.env.HUICAI_PYTHON_DISABLED&&"run_python"===e.function.name));export const READONLY_TOOLS=new Set(["read_file","list_files","glob","grep","search_code","read_document"]);const c={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,run_python:i,fetch_url:e.fetchUrl,...n};export async function runTool(e,t,r,n){const i=c[e];if(!i)return`未知工具:${e}`;try{return await i(r,t||{},n)}catch(e){return`工具执行失败:${e.message}`}}
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";import{runPython as i}from"./pyruntime.js";const o=(e,t)=>({type:"object",properties:e,required:t||[]}),p=e=>({type:"string",description:e}),a=e=>({type:"integer",description:e}),s=e=>({type:"boolean",description:e});export const TOOL_SPECS=[{type:"function",function:{name:"read_file",description:"读取文本文件内容,每行带「行号→」前缀(行号仅供定位,不属于文件内容,编辑时不要包含它)。默认最多 2000 行,可用 offset(起始行,1基)/limit(行数) 分段读取大文件。",parameters:o({path:p("相对项目根的文件路径"),offset:a("可选:起始行(1基)"),limit:a("可选:读取行数")},["path"])}},{type:"function",function:{name:"list_files",description:"列出目录下的文件树(跳过 .git/node_modules 等)。可选 path 指定子目录(默认项目根)。",parameters:o({path:p("可选:子目录路径,默认当前项目根")},[])}},{type:"function",function:{name:"glob",description:'按 glob 模式查找文件(支持 ** 与 *),如 "**/*.ts"、"src/**/*.java"。',parameters:o({pattern:p("glob 模式")},["pattern"])}},{type:"function",function:{name:"search_code",description:"按「概念/功能」检索最相关的代码块(BM25 相关性排序,比 grep 更懂「哪段代码在做这件事」)。适合「登录逻辑在哪」「哪里处理支付回调」这类找相关实现;要找精确字符串仍用 grep。返回 文件:行范围 + 代码块。",parameters:o({query:p("要找的功能/概念(自然语言或关键词组合,多写几个相关词更准)"),k:a("可选:返回块数,默认 8,上限 20")},["query"])}},{type:"function",function:{name:"grep",description:"在文件内容中按正则搜索(底层用 ripgrep,大型仓库快、自动尊重 .gitignore、不受单文件大小限制→大文件也能命中)。返回 匹配文件:行号: 该行。可选 path 限定目录、glob 限定文件名模式、ignore_case 忽略大小写、files_only 只列出命中文件、max_results 上限。",parameters:o({pattern:p("正则表达式"),path:p("可选:搜索目录,默认项目根"),glob:p("可选:文件名 glob 过滤"),ignore_case:s("可选:忽略大小写(默认否)"),files_only:s("可选:只返回命中文件列表,不含匹配行(默认否)"),max_results:a("可选:最多返回条数(默认 200,上限 500)")},["pattern"])}},{type:"function",function:{name:"read_document",description:"读取 PDF/Word/Excel/PPT/RTF 等文档的纯文本内容(经平台解析,read_file 读不了这类二进制文档)。大型文档分段通读:段末标注「全文共N字符/本段区间」,继续调用并传 offset 读取后续,直到「已到结尾」。对话附件若标注「已截断」,用本工具读取被省略的部分。",parameters:o({path:p("相对项目根的文档路径"),offset:a("可选:窗口起始字符位置(0 基,默认 0,从头顺序读取)"),max_chars:a("可选:本段最大字符数(默认 16000)")},["path"])}},{type:"function",function:{name:"write_file",description:"把文本写入文件(覆盖式,自动建父目录)。仅限当前项目目录内。仅用于新建文件或全量重写;小改动优先 edit_file。生成超长文件(长文档/PPT/大代码文件)时分段产出:先写首段,后续每段设 append=true 追加到文件末尾,避免单次内容过长被截断。",parameters:o({path:p("相对项目根的文件路径"),content:p("本次写入的内容(append=true 时为追加的续段)"),append:s("可选:true=追加到文件末尾(分段产出大文件);默认覆盖写")},["path","content"])}},{type:"function",function:{name:"edit_file",description:"精确替换文件中的片段:old_string 必须是文件原文(不含 read_file 的行号前缀)且在文件中唯一(除非 replace_all=true)。仅限项目目录内。",parameters:o({path:p("相对项目根的文件路径"),old_string:p("被替换的原文(需在文件中唯一,除非 replace_all;不要包含行号前缀)"),new_string:p("替换为的新内容"),replace_all:s("是否替换全部匹配(默认否)")},["path","old_string","new_string"])}},{type:"function",function:{name:"run_bash",description:"在项目目录下执行一条 shell 命令并返回输出(stdout+stderr+退出码)。用于构建、测试、git、脚手架等。默认超时 120s(可用 timeout 调整)。避免用它读文件/搜索(用 read_file/grep 更高效)。注意:以当前用户的完整权限运行、无沙箱隔离(可访问项目目录之外),删除/覆盖/联网等破坏性或外发操作须先说明意图再执行。",parameters:o({command:p("要执行的 shell 命令"),timeout:a("可选:超时毫秒")},["command"])}},{type:"function",function:{name:"run_python",description:"在内置托管 Python(含 pandas/openpyxl/python-docx/python-pptx/pypdf/matplotlib/tabulate/requests)中执行代码,用于数据分析与 Excel/Word/PPT/PDF 文档处理(读写原始文档优先用它,勿依赖 read_document 的纯文本摘要做精确分析)。首次调用会自动准备运行时(可能需数分钟,进度随结果返回)。结果必须 print 输出;图表用 matplotlib savefig 落盘(Agg 无窗口后端)。缺库时用 packages 按需安装。",parameters:o({code:p("内联 Python 代码(与 script 二选一)"),script:p("脚本路径(相对项目根,与 code 二选一)"),args:{type:"array",items:{type:"string"},description:"可选:传给脚本的命令行参数"},packages:{type:"array",items:{type:"string"},description:"可选:额外 PyPI 包(如 reportlab、xlsxwriter),自动按需安装"},timeout:a("可选:超时毫秒,默认 120000,上限 600000")},[])}},{type:"function",function:{name:"fetch_url",description:"抓取一个 http/https 网页或接口(GET),HTML 自动转为纯文本(按页面字符集解码,附标题)。用于查在线文档、API 说明、Issue、公开数据页等。需要顺着页面继续跳转/翻页采集时传 links=true 获取「锚文本 → 绝对URL」清单。结果可能很长,尽量带着明确目的使用。",parameters:o({url:p("完整 URL(http/https)"),timeout:a("可选:超时毫秒,默认 30000"),links:s("可选:附带页面链接清单(继续跳转/翻页采集用),默认不附")},["url"])}},...r].filter(e=>!("1"===process.env.HUICAI_PYTHON_DISABLED&&"run_python"===e.function.name));export const READONLY_TOOLS=new Set(["read_file","list_files","glob","grep","search_code","read_document"]);const c={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,run_python:i,fetch_url:e.fetchUrl,...n};export async function runTool(e,t,r,n,i){const o=c[e];if(!o)return`未知工具:${e}`;try{return await o(r,t||{},n,i)}catch(e){return`工具执行失败:${e.message}`}}
package/src/transcript.js CHANGED
@@ -1 +1 @@
1
- import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";const n=/^(agent|workflow|datahuman|squad)-/;export function textOf(t){return"string"==typeof t?t:Array.isArray(t)?t.map(t=>"text"===t?.type?t.text:"image_url"===t?.type?"[图片]":"").join("\n"):""}export function toMarkdown(t){const e=t||{},n=["# HuiCai 对话记录","",`- 会话:${e.id||""}`,`- 模型:${e.model||""}`,`- 导出时间:${(new Date).toLocaleString()}`,"","---",""];for(const t of e.messages||[])if("system"!==t.role)if("user"===t.role)n.push("### 🧑 用户","",textOf(t.content).trim()||"(空)","");else if("assistant"===t.role){const e=textOf(t.content).trim();e&&n.push("### 🤖 助手","",e,"");for(const e of t.tool_calls||[]){const t=String(e.function?.arguments||"").replace(/\s+/g," ").slice(0,200);n.push(`> 🔧 \`${e.function?.name}(${t})\``,"")}}else if("tool"===t.role){const e=String(t.content||"").slice(0,1500);n.push("> 工具结果:","","```",e,"```","")}return n.join("\n")}export async function distillMemory(o,s,r=""){const i=n.test(String(s.model||""))?s.compactModel||e:s.model,l=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,600)}`).join("\n").slice(0,16e3);if(!l.trim())return[];const c=(r?"【已有记忆(勿重复)】\n"+r+"\n\n":"")+"【本次记录】\n"+l,a=((await t({baseUrl:s.baseUrl,apiKey:s.apiKey,model:i,messages:[{role:"system",content:"你是项目知识提炼器。从下面这段对话/执行记录中,提炼出**值得跨会话长期记住**的稳定事实/约定/结论/踩坑(如构建与测试命令、架构决定、代码约定、易错点、外部依赖要点)。要求:每条一行,以「- 」开头,简洁具体;只输出真正长期有用的,不要一次性细节、不要大段代码;若与「已有记忆」重复则跳过;最多 5 条;没有则输出「(无)」。"},{role:"user",content:c}]})).content||"").trim();return!a||/^[((]无[))]$/.test(a)?[]:a.split("\n").map(t=>t.replace(/^[-*]\s*/,"").trim()).filter(t=>t&&!/^[((]无[))]$/.test(t)).slice(0,5)}export async function recapConversation(o,s){const r=n.test(String(s.model||""))?s.compactModel||e:s.model,i=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,500)}`).join("\n").slice(0,12e3);if(!i.trim())return"(对话尚无内容)";return((await t({baseUrl:s.baseUrl,apiKey:s.apiKey,model:r,messages:[{role:"system",content:"用一句话(不超过40个汉字)概括这段对话到目前为止在做什么、进展如何。只输出这一句话,不要引号、不要多余说明。"},{role:"user",content:i}]})).content||"").trim().split("\n")[0]||"(无法生成小结)"}
1
+ import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";const n=/^(agent|workflow|datahuman|squad)-/;export function textOf(t){return"string"==typeof t?t:Array.isArray(t)?t.map(t=>"text"===t?.type?t.text:"image_url"===t?.type?"[图片]":"").join("\n"):""}export function toMarkdown(t){const e=t||{},n=["# HuiCai 对话记录","",`- 会话:${e.id||""}`,`- 模型:${e.model||""}`,`- 导出时间:${(new Date).toLocaleString()}`,"","---",""];for(const t of e.messages||[])if("system"!==t.role)if("user"===t.role)n.push("### 🧑 用户","",textOf(t.content).trim()||"(空)","");else if("assistant"===t.role){const e=textOf(t.content).trim();e&&n.push("### 🤖 助手","",e,"");for(const e of t.tool_calls||[]){const t=String(e.function?.arguments||"").replace(/\s+/g," ").slice(0,200);n.push(`> 🔧 \`${e.function?.name}(${t})\``,"")}}else if("tool"===t.role){const e=String(t.content||"").slice(0,1500);n.push("> 工具结果:","","```",e,"```","")}return n.join("\n")}export async function distillMemory(o,s,r=""){const i=n.test(String(s.model||""))?s.compactModel||e:s.model,l=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,600)}`).join("\n").slice(0,16e3);if(!l.trim())return[];const c=(r?"【已有记忆(勿重复)】\n"+r+"\n\n":"")+"【本次记录】\n"+l,a=((await t({baseUrl:s.baseUrl,apiKey:s.apiKey,model:i,client:s.client,aux:!0,messages:[{role:"system",content:"你是项目知识提炼器。从下面这段对话/执行记录中,提炼出**值得跨会话长期记住**的稳定事实/约定/结论/踩坑(如构建与测试命令、架构决定、代码约定、易错点、外部依赖要点)。要求:每条一行,以「- 」开头,简洁具体;只输出真正长期有用的,不要一次性细节、不要大段代码;若与「已有记忆」重复则跳过;最多 5 条;没有则输出「(无)」。"},{role:"user",content:c}]})).content||"").trim();return!a||/^[((]无[))]$/.test(a)?[]:a.split("\n").map(t=>t.replace(/^[-*]\s*/,"").trim()).filter(t=>t&&!/^[((]无[))]$/.test(t)).slice(0,5)}export async function recapConversation(o,s){const r=n.test(String(s.model||""))?s.compactModel||e:s.model,i=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,500)}`).join("\n").slice(0,12e3);if(!i.trim())return"(对话尚无内容)";return((await t({baseUrl:s.baseUrl,apiKey:s.apiKey,model:r,client:s.client,aux:!0,messages:[{role:"system",content:"用一句话(不超过40个汉字)概括这段对话到目前为止在做什么、进展如何。只输出这一句话,不要引号、不要多余说明。"},{role:"user",content:i}]})).content||"").trim().split("\n")[0]||"(无法生成小结)"}
package/src/tui/app.js CHANGED
@@ -1 +1 @@
1
- import r,{useState as t,useRef as e,useReducer as n,useEffect as o,useMemo as c}from"react";import{execSync as s}from"node:child_process";import{render as l,Box as u,Text as i,Static as a,useInput as m,useApp as p}from"ink";import{runTurn as f}from"../agent.js";import{fetchUsage as d}from"../api.js";import{toolLabel as g}from"../render.js";import{renderMarkdown as h,looksLikeMarkdown as w}from"../markdown.js";import{renderWelcome as x}from"../banner.js";import{createSession as y,saveSession as b}from"../session.js";import{createCheckpoints as v}from"../checkpoints.js";import{buildExtraTools as k}from"../repl.js";import{buildSystemPrompt as T}from"../context.js";import{closeMcp as $}from"../mcp.js";import{runSlash as j,listCommands as E}from"../commands.js";import{usageStatus as M,fmtReset as P}from"../usage.js";import{expandAtMentions as A,buildUserContent as S,runBangCommand as I,loadHistory as D,appendHistory as L}from"../input.js";import{loadAllowRules as _,loadDenyRules as C,matchAllowRules as U,suggestRule as O,addProjectAllowRule as R,riskWarning as W}from"../settings.js";import{PLAN_DENY_REASON as F,SANDBOX_DENY_REASON as K,sandboxBlocked as Y,AUTOMATION_TOOLS as q,automationActionLabel as B,automationDetail as G,PERMISSION_MODES as J,MODE_ICON as N,MODE_LABEL as Q}from"../permissions.js";import{runVerifyLoop as X}from"../autofix.js";import{runAutoFlow as z}from"../autorun.js";const H=r.createElement,V=10;function Z(r,t=12){const e=String(r).split("\n");return e.length<=t?r:e.slice(0,t).join("\n")+`\n…(共 ${e.length} 行)`}function rr(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}const tr=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];export function App({config:r,cwd:l,session:x,extraTools:nr,mcp:or,yolo:cr,initialPrompt:sr,version:lr,onExit:ur}){const ir=p(),[ar,mr]=t(l),pr=e(nr),fr=e([]),dr=e(0),gr=e(""),hr=e(x),wr=e(null),xr=e(null),yr=e(new Set),br=e(_(ar)),vr=e(C(ar)),kr=e(!1),Tr=e(!1),$r=e(!1),jr=e(v()),Er=e(!!cr),Mr=e(!1),Pr=e("idle"),Ar=e([]),Sr=e({list:[]}),Ir=e(D(ar)),Dr=e(null),Lr=e(""),[,_r]=n(r=>r+1,0),[Cr,Ur]=n(r=>r+1,0),[Or,Rr]=t(0),[Wr,Fr]=t("idle"),[Kr,Yr]=t({text:"",cursor:0}),[qr,Br]=t(null),[Gr,Jr]=t({prompt:0,completion:0}),[Nr,Qr]=t(0),[Xr,zr]=t([]),[Hr,Vr]=t(0),[Zr,rt]=t(cr?"auto":"manual"),tt=()=>kr.current?"plan":Tr.current?"sandbox":Er.current?"auto":$r.current?"acceptEdits":"manual",[et,nt]=t(""),ot=e(null),[ct,st]=t(0),[lt,ut]=t(null),it=e(null),[at,mt]=t(0),[pt,ft]=t(!1),dt=c(()=>E(ar),[ar]),gt=(r,t)=>{fr.current.push({id:dr.current++,kind:r,text:t}),Ur()},ht=()=>{if(gr.current){const t=gr.current,e=!1!==r.markdown&&w(t);gt("assistant",e?h(t):t),gr.current=""}},wt=r=>{Pr.current=r?"busy":"idle",Fr(Pr.current)};o(()=>{if("busy"!==Wr)return;const r=setInterval(()=>st(r=>(r+1)%tr.length),80);return()=>clearInterval(r)},[Wr]);const xt=async()=>{if(r.apiKey)try{const t=await d(r.baseUrl,r.apiKey);it.current=t,ut(t)}catch{}};o(()=>{xt();const r=setInterval(()=>{xt()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void nt("");const t=()=>{try{const t=s(r.statusLine,{cwd:ar,timeout:3e3,stdio:["ignore","pipe","ignore"]}).toString().split("\n")[0].slice(0,80);nt(t)}catch{nt("")}};t();const e=setInterval(t,5e3);return()=>clearInterval(e)},[r.statusLine]),o(()=>()=>{ot.current&&clearInterval(ot.current)},[]);const yt=e(""),bt={onDelta:r=>{yt.current&&(yt.current=""),gr.current+=r,_r()},onReasoning:r=>{yt.current=(yt.current+r).slice(-400),_r()},onToolStart:(r,t)=>{yt.current="",ht(),gt("tool",g(r,t))},onToolResult:(r,t)=>gt("toolresult",function(r,t=200){const e=String(r).split("\n")[0];return e.length>t?e.slice(0,t)+"…":e}(t)),onFileDiff:r=>gt("diff",r),onSkip:(r,t)=>gt("skip",g(r,t)+" — 已跳过"),onInfo:r=>{ht(),gt("info",r)},onTodo:r=>zr(r),onTurnEnd:()=>{yt.current="",ht()}},vt={request:(r,t,e,n)=>{if(vr.current.length&&U(vr.current,r,t))return Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions 查看或移除)。"});if(kr.current&&!n)return Promise.resolve({allow:!1,reason:F});if(Tr.current&&Y(r))return Promise.resolve({allow:!1,reason:K});if(n||Er.current||yr.current.has(r)||U(br.current,r,t))return Promise.resolve({allow:!0});if($r.current&&("write_file"===r||"edit_file"===r))return Promise.resolve({allow:!0});const o=q.has(r);return new Promise(e=>{xr.current={name:r,args:t,resolve:e,consent:o},Br({label:o?B(r,t):g(r,t),detail:o?G(r,t):"",name:r,args:t,consent:o,risk:W(r,t)})})},isYolo:()=>Er.current,setYolo:r=>{Er.current=!!r,rt(tt())},setPlanMode:r=>{kr.current=!!r,rt(tt())},isPlanMode:()=>kr.current,setSandbox:r=>{Tr.current=!!r,rt(tt())},isSandbox:()=>Tr.current,setAcceptEdits:r=>{$r.current=!!r,rt(tt())},isAcceptEdits:()=>$r.current,setMode:r=>{Er.current="auto"===r,$r.current="acceptEdits"===r,kr.current="plan"===r,Tr.current="sandbox"===r,rt(r)},getMode:()=>tt(),reloadRules:()=>{br.current=_(ar),vr.current=C(ar)}},kt=()=>{const r=Ar.current.shift();return Vr(Ar.current.length),null==r?null:r},Tt=async t=>{const e=M(it.current);if(e&&"exhausted"===e.level)return void gt("info",`✗ Token 用量窗口(${e.label})已用尽,暂无法继续。${P(e.resetAtEpochMs)}`);const{text:n,images:o}=await A(t,ar,r),c=jr.current.snapshot({session:hr.current,label:t});hr.current.messages.push({role:"user",content:S(n,o)}),wt(!0),wr.current=new AbortController,hr.current.compaction||(hr.current.compaction={});const s=await f({config:r,messages:hr.current.messages,permissions:vt,cwd:ar,extraTools:pr.current,events:bt,signal:wr.current.signal,todoState:Sr.current,compaction:hr.current.compaction,pullQueued:kt,fileJournal:c});return wr.current=null,ht(),s?.usage&&Jr(r=>({prompt:r.prompt+s.usage.prompt,completion:r.completion+s.usage.completion})),s?.lastPromptTokens&&(Qr(s.lastPromptTokens),hr.current.lastPromptTokens=s.lastPromptTokens),b(hr.current),(s?.aborted||s?.error)&&Ar.current.length&&(Ar.current=[],Vr(0),gt("info","已清空排队消息")),wt(!1),xt(),s},$t=()=>{ur?.(),ir.exit()},jt=async t=>{const e=t.trim();if(Yr({text:"",cursor:0}),Dr.current=null,e){if(L(ar,e),Ir.current[Ir.current.length-1]!==e&&Ir.current.push(e),e.startsWith("/"))return(async t=>{const e={version:lr,config:r,cwd:ar,usage:Gr,permissions:vt,extraTools:pr.current,session:hr.current,checkpoints:jr.current,print:r=>gt("info",String(r))},n=await j(t,e);if("exit"===n.action)return $t();if("clear"===n.action&&(hr.current=y({cwd:ar,model:r.model,messages:[hr.current.messages[0]]}),Sr.current.list=[],jr.current.list.length=0,zr([]),Qr(0),fr.current=[],gr.current="",Rr(r=>r+1),gt("info","已开启新会话(旧会话仍可 --resume 恢复)")),"refresh"===n.action&&(Sr.current.list=[],zr([]),Qr(0)),"autofix"===n.action&&await X({command:n.command,maxIters:n.maxIters,cwd:ar,runTurnOnce:r=>Tt(r),emit:r=>gt("info",r)}),"autorun"===n.action&&await z({goal:n.goal,config:r,permissions:vt,cwd:ar,runTurnOnce:r=>Tt(r),emit:r=>gt("info",r)}),"cd"===n.action){br.current=_(n.cwd),vr.current=C(n.cwd),$(or?.clients);const t=await k(r,n.cwd);pr.current=t.extraTools,or&&(or.clients=t.clients),hr.current=y({cwd:n.cwd,model:r.model,messages:[{role:"system",content:T(n.cwd,{memory:!1!==r.memoryEnabled})}]}),r.sessionId=hr.current.id,Sr.current.list=[],jr.current.list.length=0,zr([]),Qr(0),fr.current=[],gr.current="",Rr(r=>r+1),mr(n.cwd),gt("info","已切换到 "+n.cwd+"(新会话,旧会话已保存)")}"loop"===n.action&&(ot.current&&(clearInterval(ot.current),ot.current=null),n.stop?gt("info","已停止循环。"):(gt("info",`已开始循环:每 ${n.seconds}s 运行「${n.prompt}」(/loop off 停止)`),ot.current=setInterval(()=>{"busy"!==Pr.current&&(gt("user",n.prompt),Tt(n.prompt))},1e3*n.seconds))),n.prompt&&("busy"===Pr.current?(Ar.current.push(n.prompt),Vr(Ar.current.length)):(gt("user",n.prompt),await Tt(n.prompt)))})(e);if(e.startsWith("!")&&e.length>1)return(async r=>{if("busy"===Pr.current)return void gt("info","正在处理上一条,! 命令请稍候再试");gt("user","! "+r);const{output:t,record:e}=await I(r,ar);gt("toolresult",Z(t)),hr.current.messages.push({role:"user",content:e}),b(hr.current)})(e.slice(1).trim());if("busy"===Pr.current)return gt("user",e),Ar.current.push(e),void Vr(Ar.current.length);gt("user",e),await Tt(e)}};o(()=>{!Mr.current&&sr&&(Mr.current=!0,jt(sr))},[]);const Et=/^\/(\S*)$/.exec(Kr.text),Mt=Et?Et[1]:null,Pt=null==Mt?[]:dt.filter(r=>r.name.toLowerCase().startsWith(Mt.toLowerCase())),At=Pt.length>0&&!pt,St=Pt.length<=V?0:Math.max(0,Math.min(at-Math.floor(5),Pt.length-V)),It=Pt.slice(St,St+V);o(()=>{mt(0),ft(!1)},[Mt]),m((t,e)=>{if(xr.current){const r=xr.current,n=t=>{xr.current=null,Br(null),r.resolve(t)};if("y"===t||"1"===t)return n({allow:!0});if("a"===t||"2"===t)return yr.current.add(r.name),n({allow:!0});if("3"===t)return n({allow:!1,reason:"用户选择了「否」,该操作未执行。"});if("A"===t){yr.current.add(r.name);const t=O(r.name,r.args);try{R(ar,t),br.current=_(ar),gt("info",`已写入允许规则:${t} → .huicai/settings.json`)}catch(r){gt("info",`规则写入失败(本会话仍生效):${r.message}`)}return n({allow:!0})}return"n"===t||e.escape||e.return?n({allow:!1,reason:"用户拒绝了该操作。"}):void 0}if(e.tab&&e.shift){const t=J[(J.indexOf(vt.getMode())+1)%J.length];return vt.setMode(n=t),r.planMode="plan"===n,r.sandbox="sandbox"===n,void gt("info",`对话模式 → ${N[n]} ${Q[n]}`+("auto"===n?"(已开放所有权限,任何命令免审批)":""))}var n;if(At){const r=()=>Pt[Math.min(at,Pt.length-1)];if(e.upArrow)return mt(r=>(r-1+Pt.length)%Pt.length);if(e.downArrow)return mt(r=>(r+1)%Pt.length);if(e.pageUp)return mt(r=>Math.max(0,r-V));if(e.pageDown)return mt(r=>Math.min(Pt.length-1,r+V));if(e.tab){const t=r().name;return Yr({text:"/"+t+" ",cursor:t.length+2})}if(e.return)return void jt("/"+r().name);if(e.escape)return ft(!0)}if(e.escape)"busy"===Pr.current&&wr.current&&(wr.current.abort(),gt("info","已请求中断(Esc)"));else if(e.ctrl&&"c"===t)"busy"===Pr.current&&wr.current?(wr.current.abort(),gt("info","已请求中断")):$t();else if(e.return)jt(Kr.text);else{if(e.upArrow)return(()=>{const r=Ir.current;if(!r.length)return;null==Dr.current?(Lr.current=Kr.text,Dr.current=r.length-1):Dr.current>0&&Dr.current--;const t=r[Dr.current]??"";Yr({text:t,cursor:t.length})})();if(e.downArrow)return(()=>{if(null==Dr.current)return;const r=Ir.current;if(Dr.current<r.length-1){Dr.current++;const t=r[Dr.current];Yr({text:t,cursor:t.length})}else Dr.current=null,Yr({text:Lr.current,cursor:Lr.current.length})})();if(e.leftArrow)return Yr(r=>({...r,cursor:Math.max(0,r.cursor-1)}));if(e.rightArrow)return Yr(r=>({...r,cursor:Math.min(r.text.length,r.cursor+1)}));if(e.ctrl&&"a"===t)return Yr(r=>({...r,cursor:0}));if(e.ctrl&&"e"===t)return Yr(r=>({...r,cursor:r.text.length}));if(e.ctrl&&"u"===t)return Yr(r=>({text:r.text.slice(r.cursor),cursor:0}));if(e.ctrl&&"k"===t)return Yr(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(e.ctrl&&"w"===t)return Yr(r=>{const t=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:t+r.text.slice(r.cursor),cursor:t.length}});if(e.backspace||e.delete)return Dr.current=null,Yr(r=>r.cursor>0?{text:r.text.slice(0,r.cursor-1)+r.text.slice(r.cursor),cursor:r.cursor-1}:r);if(t&&!e.ctrl&&!e.meta){Dr.current=null;const r=t.replace(/\r\n?/g,"\n");Yr(t=>({text:t.text.slice(0,t.cursor)+r+t.text.slice(t.cursor),cursor:t.cursor+r.length}))}}});const Dt=c(()=>fr.current.slice(),[Cr]),Lt=ar.split("/").filter(Boolean).pop()||ar,_t=Kr.text.slice(0,Kr.cursor),Ct=Kr.text[Kr.cursor]??" ",Ut=Kr.text.slice(Kr.cursor+1),Ot=Math.max(8,process.stdout.columns||80),Rt=M(lt),Wt=Rt?"exhausted"===Rt.level?`⛔ ${Rt.label}用量已用尽${P(Rt.resetAtEpochMs)?" · "+P(Rt.resetAtEpochMs):""}`:`⚠ ${Rt.label}用量 ${Math.round(100*Rt.pct)}%(${rr(Rt.used)}/${rr(Rt.limit)})`:"";return H(u,{flexDirection:"column"},H(a,{key:Or,items:Dt},r=>H(er,{key:r.id,item:r})),yt.current?H(u,null,H(i,{color:"gray",italic:!0},"✦ 思考 "+yt.current.replace(/\n/g," ").slice(-160))):null,gr.current?H(u,{marginTop:0},H(i,null,gr.current)):null,Xr.length?H(u,{flexDirection:"column",paddingLeft:2,marginTop:1},...Xr.map((r,t)=>H(i,{key:t,color:"completed"===r.status?"green":"in_progress"===r.status?"cyan":"gray",dimColor:"completed"===r.status,bold:"in_progress"===r.status},("completed"===r.status?"☑ ":"in_progress"===r.status?"◐ ":"☐ ")+r.content))):null,"busy"!==Wr||gr.current?null:H(u,null,H(i,{color:"cyan"},tr[ct]+" 思考中…"),H(i,{color:"gray"},"(Esc 中断 · 输入回车可排队补充)")),qr?H(u,{flexDirection:"column",borderStyle:"round",borderColor:"yellow",paddingX:1,marginTop:1},H(i,{color:"yellow",bold:!0},qr.consent?"● 是否继续?"+qr.label:"● 需要授权:"+qr.label),qr.risk?H(i,{color:"red",bold:!0},"⚠ "+qr.risk):null,qr.consent&&qr.detail?H(i,{color:"gray"},Z(qr.detail,6)):null,"run_bash"===qr.name?H(i,{color:"gray"},"$ "+(qr.args.command||"")):null,"fetch_url"===qr.name?H(i,{color:"gray"},"GET "+(qr.args.url||"")):null,H(i,null,qr.consent?"[1] 是 · [2] 是,并允许在当前对话中后续相同操作不再询问 · [3/Esc] 否":"[y] 允许一次 · [a] 本会话 · [A] 永久(写入项目设置) · [n/Esc] 拒绝")):null,At?H(u,{flexDirection:"column",marginTop:1},H(i,{color:"gray"},Pt.length>V?` 斜杠命令 ${St+1}-${St+It.length}/${Pt.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · Tab 补全 · ⏎ 执行 · Esc 关闭`:" 斜杠命令 · ↑↓ 选择 · Tab 补全 · ⏎ 执行 · Esc 关闭"),...It.map((r,t)=>{const e=St+t;return H(u,{key:r.name},H(i,{color:"cyan",bold:e===at},(e===at?" ❯ ":" ")+"/"+r.name+(r.args?" "+r.args:"")),H(i,{color:"gray"}," "+String(r.desc||"").replace(/\s+/g," ").slice(0,46)))})):null,H(u,{marginTop:At?0:1},H(i,{color:"blue"},"─".repeat(Ot))),H(u,null,H(i,{color:"busy"===Wr?"yellow":"cyan"},"busy"===Wr?"⧗ ":"› "),H(i,null,_t),H(i,{inverse:!0},Ct),H(i,null,Ut)),H(i,{color:"blue"},"─".repeat(Ot)),H(u,{justifyContent:"space-between"},H(i,{color:"gray"},(et?et+" · ":"")+`${Lt} · ${r.model} · ${N[Zr]} ${Q[Zr]}`+(Hr?` · 排队 ${Hr}`:"")+" · ⇧Tab 模式 · /help"),Wt?H(i,{color:"exhausted"===Rt.level?"red":"yellow",bold:!0},Wt):H(i,null,"")))}function er({item:r}){const{kind:t,text:e}=r;if("user"===t)return H(u,{marginTop:1},H(i,{color:"cyan",bold:!0},"› "),H(i,{bold:!0},e));if("assistant"===t)return H(u,null,H(i,null,e));if("diff"===t)return H(u,{marginLeft:2},H(i,null,e));if("tool"===t)return H(i,{color:"cyan"}," ⏺ "+e);if("toolresult"===t)return H(i,{color:"gray"}," "+e);if("skip"===t)return H(i,{color:"gray"}," ⊘ "+e);if("info"===t){const r=/✗|失败|错误|error/i.test(e);return H(i,{color:r?"red":"yellow"},(r?" ✗ ":" · ")+e.replace(/^✗\s*/,""))}return H(i,null,e)}export async function runInk({config:r,cwd:t,session:e,yolo:n,initialPrompt:o,version:c}){process.stdout.write(x({model:r.model,cwd:t,version:c,platform:/^(agent|workflow|datahuman)-/.test(String(r.model||"")),yolo:n,consult:r.consult?r.consultLabel||r.consult:null,ink:!0})+"\n");const{extraTools:s,clients:u}=await k(r,t),i={clients:u},{waitUntilExit:a}=l(H(App,{config:r,cwd:t,session:e,extraTools:s,mcp:i,yolo:n,initialPrompt:o,version:c,onExit:null}));await a(),$(i.clients)}
1
+ import r,{useState as t,useRef as e,useReducer as n,useEffect as o,useMemo as l}from"react";import{execSync as c}from"node:child_process";import{render as s,Box as u,Text as i,Static as a,useInput as m,useApp as p}from"ink";import{runTurn as f}from"../agent.js";import{fetchUsage as g}from"../api.js";import{toolLabel as d}from"../render.js";import{renderMarkdown as h,looksLikeMarkdown as w}from"../markdown.js";import{renderWelcome as x}from"../banner.js";import{createSession as y,saveSession as b}from"../session.js";import{ensureSessionTitle as k}from"../title.js";import{createCheckpoints as v}from"../checkpoints.js";import{buildExtraTools as $}from"../repl.js";import{buildSystemPrompt as M}from"../context.js";import{closeMcp as T}from"../mcp.js";import{runSlash as j,listCommands as E}from"../commands.js";import{usageStatus as A,fmtReset as P}from"../usage.js";import{handleAccountError as S}from"../account.js";import{pricingUrl as D}from"../errkind.js";import{expandAtMentions as I,buildUserContent as L,runBangCommand as C,loadHistory as U,appendHistory as _}from"../input.js";import{listWorkspaceFiles as W}from"../executors.js";import{loadAllowRules as q,loadDenyRules as O,matchAllowRules as Q,suggestRule as R,addProjectAllowRule as F,riskWarning as K}from"../settings.js";import{PLAN_DENY_REASON as Y,SANDBOX_DENY_REASON as B,sandboxBlocked as G,AUTOMATION_TOOLS as H,automationActionLabel as J,automationDetail as N,PERMISSION_MODES as X,MODE_ICON as z,MODE_LABEL as V}from"../permissions.js";import{runVerifyLoop as Z}from"../autofix.js";import{runAutoFlow as rr}from"../autorun.js";import{acquireWakeLock as tr,releaseWakeLock as er}from"../presence.js";import{notifyTurnDone as nr}from"../notify.js";const or=r.createElement,lr=10;function cr(r,t=12){const e=String(r).split("\n");return e.length<=t?r:e.slice(0,t).join("\n")+`\n…(共 ${e.length} 行)`}function sr(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}const ur=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];export function App({config:r,cwd:s,session:x,extraTools:ar,mcp:mr,yolo:pr,initialPrompt:fr,version:gr,onExit:dr,exitState:hr={},injectQueue:wr=[]}){const xr=p(),[yr,br]=t(s),kr=e(ar),vr=e([]),$r=e(0),Mr=e(""),Tr=e(x),jr=e(null),Er=e(null),Ar=e(new Set),Pr=e(q(yr)),Sr=e(O(yr)),Dr=e(!1),Ir=e(!1),Lr=e(!1),Cr=e(v()),Ur=e(!!pr),_r=e(!1),Wr=e("idle"),qr=e([]),Or=e({list:[]}),Qr=e(U(yr)),Rr=e(null),Fr=e(""),[,Kr]=n(r=>r+1,0),[Yr,Br]=n(r=>r+1,0),[Gr,Hr]=t(0),[Jr,Nr]=t("idle"),[Xr,zr]=t({text:"",cursor:0}),[Vr,Zr]=t(null),[rt,tt]=t({prompt:0,completion:0}),[et,nt]=t(0),[ot,lt]=t([]),[ct,st]=t(0),[ut,it]=t(pr?"auto":"manual"),at=()=>Dr.current?"plan":Ir.current?"sandbox":Ur.current?"auto":Lr.current?"acceptEdits":"manual",[mt,pt]=t(""),ft=e(null),[gt,dt]=t(0),[ht,wt]=t(null),xt=e(null),[yt,bt]=t(0),[kt,vt]=t(!1),$t=l(()=>E(yr),[yr]),[Mt,Tt]=t(0),[jt,Et]=t(!1),At=l(()=>{try{return W(yr)}catch{return[]}},[yr]),[Pt,St]=t(!1),[Dt,It]=t(0),Lt=e(0),Ct=(r,t)=>{vr.current.push({id:$r.current++,kind:r,text:t}),Br()},Ut=()=>{if(Mr.current){const t=Mr.current,e=!1!==r.markdown&&w(t);Ct("assistant",e?h(t):t),Mr.current=""}},_t=r=>{Wr.current=r?"busy":"idle",Nr(Wr.current)};o(()=>{if("busy"!==Jr)return;const r=setInterval(()=>dt(r=>(r+1)%ur.length),80);return()=>clearInterval(r)},[Jr]);const Wt=async()=>{if(r.apiKey)try{const t=await g(r.baseUrl,r.apiKey);xt.current=t,wt(t)}catch{}};o(()=>{Wt();const r=setInterval(()=>{Wt()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void pt("");const t=()=>{try{const t=c(r.statusLine,{cwd:yr,timeout:3e3,stdio:["ignore","pipe","ignore"]}).toString().split("\n")[0].slice(0,80);pt(t)}catch{pt("")}};t();const e=setInterval(t,5e3);return()=>clearInterval(e)},[r.statusLine]),o(()=>()=>{ft.current&&clearInterval(ft.current)},[]);const qt=e(""),Ot={onDelta:r=>{qt.current&&(qt.current=""),Mr.current+=r,Kr()},onReasoning:r=>{qt.current=(qt.current+r).slice(-400),Kr()},onToolStart:(r,t)=>{qt.current="",Ut(),Ct("tool",d(r,t))},onToolResult:(r,t)=>Ct("toolresult",function(r,t=200){const e=String(r).split("\n")[0];return e.length>t?e.slice(0,t)+"…":e}(t)),onFileDiff:r=>Ct("diff",r),onSkip:(r,t)=>Ct("skip",d(r,t)+" — 已跳过"),onInfo:r=>{Ut(),Ct("info",r)},onTodo:r=>lt(r),onDecision:r=>{Ut();const t=(r.alternatives||[]).length?` · ${r.alternatives.length} 备选`:"",e="draft"===r.status?"(待确认草稿)":"";Ct("info",`◆ 已记录决策:${r.title}${t}${e}`)},onTurnEnd:()=>{qt.current="",Ut()}},Qt={request:(r,t,e,n)=>{if(Sr.current.length&&Q(Sr.current,r,t))return Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions 查看或移除)。"});if(Dr.current&&!n)return Promise.resolve({allow:!1,reason:Y});if(Ir.current&&G(r))return Promise.resolve({allow:!1,reason:B});if(n||Ur.current||Ar.current.has(r)||Q(Pr.current,r,t))return Promise.resolve({allow:!0});if(Lr.current&&("write_file"===r||"edit_file"===r))return Promise.resolve({allow:!0});const o=H.has(r);return new Promise(e=>{Er.current={name:r,args:t,resolve:e,consent:o},Zr({label:o?J(r,t):d(r,t),detail:o?N(r,t):"",name:r,args:t,consent:o,risk:K(r,t)})})},isYolo:()=>Ur.current,setYolo:r=>{Ur.current=!!r,it(at())},setPlanMode:r=>{Dr.current=!!r,it(at())},isPlanMode:()=>Dr.current,setSandbox:r=>{Ir.current=!!r,it(at())},isSandbox:()=>Ir.current,setAcceptEdits:r=>{Lr.current=!!r,it(at())},isAcceptEdits:()=>Lr.current,setMode:r=>{Ur.current="auto"===r,Lr.current="acceptEdits"===r,Dr.current="plan"===r,Ir.current="sandbox"===r,it(r)},getMode:()=>at(),reloadRules:()=>{Pr.current=q(yr),Sr.current=O(yr)}},Rt=()=>{if(wr.length)return wr.shift();const r=qr.current.shift();return st(qr.current.length),null==r?null:r},Ft=()=>{k(Tr.current,r).then(r=>{r&&b(Tr.current)}).catch(()=>{})},Kt=async t=>{const e=A(xt.current);if(e&&"exhausted"===e.level){const t=D(r.baseUrl);return void Ct("info",`✗ Token 用量窗口(${e.label})已用尽,暂无法继续。${P(e.resetAtEpochMs)}`+(t?`\n 升级会员提高上限 → ${t}`:""))}const{text:n,images:o}=await I(t,yr,r),l=Cr.current.snapshot({session:Tr.current,label:t});Tr.current.messages.push({role:"user",content:L(n,o)}),Ft(),_t(!0),jr.current=new AbortController,Tr.current.compaction||(Tr.current.compaction={});const c="auto"===at()&&!1!==r.wakeLockInAuto?tr({reason:"自动模式任务执行中"}):null,s=Date.now();let u;try{u=await f({config:r,messages:Tr.current.messages,permissions:Qt,cwd:yr,extraTools:kr.current,events:Ot,signal:jr.current.signal,todoState:Or.current,compaction:Tr.current.compaction,pullQueued:Rt,fileJournal:l})}finally{c?.ok&&er()}if(jr.current=null,u?.aborted||u?.error||nr({durationMs:Date.now()-s,cwd:yr,summary:String(t).replace(/\s+/g," ").slice(0,80),enabled:!1!==r.notifications}).catch(()=>{}),Ut(),u?.usage&&tt(r=>({prompt:r.prompt+u.usage.prompt,completion:r.completion+u.usage.completion})),u?.lastPromptTokens&&(nt(u.lastPromptTokens),Tr.current.lastPromptTokens=u.lastPromptTokens),b(Tr.current),Ft(),(u?.aborted||u?.error)&&qr.current.length&&(qr.current=[],st(0),Ct("info","已清空排队消息")),_t(!1),u?.error){let t="";const e=S(u,r,r=>{t+=r});if(t.trim()&&Ct("info",t.trim()),"auth"===e)return hr.relogin=!0,Yt(),u}return Wt(),u},Yt=()=>{dr?.(),xr.exit()},Bt=async t=>{const e=t.trim();if(zr({text:"",cursor:0}),Rr.current=null,e){if(_(yr,e),Qr.current[Qr.current.length-1]!==e&&Qr.current.push(e),e.startsWith("/"))return(async t=>{const e={version:gr,config:r,cwd:yr,usage:rt,permissions:Qt,extraTools:kr.current,session:Tr.current,checkpoints:Cr.current,print:r=>Ct("info",String(r))},n=await j(t,e);if("exit"===n.action)return Yt();if("clear"===n.action&&(Tr.current=y({cwd:yr,model:r.model,messages:[Tr.current.messages[0]]}),Or.current.list=[],Cr.current.list.length=0,lt([]),nt(0),vr.current=[],Mr.current="",Hr(r=>r+1),Ct("info","已开启新会话(旧会话仍可 --resume 恢复)")),"refresh"===n.action&&(Or.current.list=[],lt([]),nt(0)),"autofix"===n.action&&await Z({command:n.command,maxIters:n.maxIters,cwd:yr,runTurnOnce:r=>Kt(r),emit:r=>Ct("info",r)}),"autorun"===n.action&&await rr({goal:n.goal,config:r,permissions:Qt,cwd:yr,runTurnOnce:r=>Kt(r),emit:r=>Ct("info",r)}),"cd"===n.action){Pr.current=q(n.cwd),Sr.current=O(n.cwd),T(mr?.clients);const t=await $(r,n.cwd,{queueInject:r=>wr.push(r)});kr.current=t.extraTools,mr&&(mr.clients=t.clients),Tr.current=y({cwd:n.cwd,model:r.model,messages:[{role:"system",content:M(n.cwd,{memory:!1!==r.memoryEnabled,config:r})}]}),r.sessionId=Tr.current.id,Or.current.list=[],Cr.current.list.length=0,lt([]),nt(0),vr.current=[],Mr.current="",Hr(r=>r+1),br(n.cwd),Ct("info","已切换到 "+n.cwd+"(新会话,旧会话已保存)")}"loop"===n.action&&(ft.current&&(clearInterval(ft.current),ft.current=null),n.stop?Ct("info","已停止循环。"):(Ct("info",`已开始循环:每 ${n.seconds}s 运行「${n.prompt}」(/loop off 停止)`),ft.current=setInterval(()=>{"busy"!==Wr.current&&(Ct("user",n.prompt),Kt(n.prompt))},1e3*n.seconds))),n.prompt&&("busy"===Wr.current?(qr.current.push(n.prompt),st(qr.current.length)):(Ct("user",n.prompt),await Kt(n.prompt)))})(e);if(e.startsWith("!")&&e.length>1)return(async r=>{if("busy"===Wr.current)return void Ct("info","正在处理上一条,! 命令请稍候再试");Ct("user","! "+r);const{output:t,record:e}=await C(r,yr);Ct("toolresult",cr(t)),Tr.current.messages.push({role:"user",content:e}),b(Tr.current)})(e.slice(1).trim());if("busy"===Wr.current)return Ct("user",e),qr.current.push(e),void st(qr.current.length);Ct("user",e),await Kt(e)}};o(()=>{!_r.current&&fr&&(_r.current=!0,Bt(fr))},[]);const Gt=/^\/(\S*)$/.exec(Xr.text),Ht=Gt?Gt[1]:null,Jt=null==Ht?[]:$t.filter(r=>r.name.toLowerCase().startsWith(Ht.toLowerCase())),Nt=Jt.length>0&&!kt,Xt=Jt.length<=lr?0:Math.max(0,Math.min(yt-Math.floor(5),Jt.length-lr)),zt=Jt.slice(Xt,Xt+lr);o(()=>{bt(0),vt(!1)},[Ht]);const Vt=null==Ht?/(?:^|\s)@([^\s@]*)$/.exec(Xr.text):null,Zt=Vt?Vt[1]:null,re=null==Zt?[]:(()=>{const r=Zt.toLowerCase(),t=[];for(const e of At){const n=e.toLowerCase(),o=n.split("/").pop();let l=-1;if(r?o.startsWith(r)?l=3:o.includes(r)?l=2:n.includes(r)&&(l=1):l=0,l>=0&&t.push({f:e,rank:l}),!r&&t.length>=50)break}return t.sort((r,t)=>t.rank-r.rank||r.f.length-t.f.length||r.f.localeCompare(t.f)),t.slice(0,50).map(r=>r.f)})(),te=re.length>0&&!jt,ee=re.length<=lr?0:Math.max(0,Math.min(Mt-Math.floor(5),re.length-lr)),ne=re.slice(ee,ee+lr);o(()=>{Tt(0),Et(!1)},[Zt]);const oe=l(()=>{const r=new Set,t=[];for(let e=Qr.current.length-1;e>=0&&t.length<50;e--){const n=Qr.current[e];n&&!r.has(n)&&(r.add(n),t.push(n))}return t},[Pt]),le=oe.length<=lr?0:Math.max(0,Math.min(Dt-Math.floor(5),oe.length-lr)),ce=oe.slice(le,le+lr);m((t,e)=>{if(Er.current){const r=Er.current,n=t=>{Er.current=null,Zr(null),r.resolve(t)};if("y"===t||"1"===t)return n({allow:!0});if("a"===t||"2"===t)return Ar.current.add(r.name),n({allow:!0});if("3"===t)return n({allow:!1,reason:"用户选择了「否」,该操作未执行。"});if("A"===t){Ar.current.add(r.name);const t=R(r.name,r.args);try{F(yr,t),Pr.current=q(yr),Ct("info",`已写入允许规则:${t} → .huicai/settings.json`)}catch(r){Ct("info",`规则写入失败(本会话仍生效):${r.message}`)}return n({allow:!0})}return"n"===t||e.escape||e.return?n({allow:!1,reason:"用户拒绝了该操作。"}):void 0}if(Pt){if(!oe.length)return void St(!1);if(e.upArrow)return It(r=>(r-1+oe.length)%oe.length);if(e.downArrow)return It(r=>(r+1)%oe.length);if(e.pageUp)return It(r=>Math.max(0,r-lr));if(e.pageDown)return It(r=>Math.min(oe.length-1,r+lr));if(e.return){const r=oe[Math.min(Dt,oe.length-1)]||"";return St(!1),Rr.current=null,zr({text:r,cursor:r.length})}return e.escape?St(!1):void 0}if(e.tab&&e.shift){const t=X[(X.indexOf(Qt.getMode())+1)%X.length];return Qt.setMode(n=t),r.planMode="plan"===n,r.sandbox="sandbox"===n,void Ct("info",`对话模式 → ${z[n]} ${V[n]}`+("auto"===n?"(已开放所有权限,任何命令免审批)":""))}var n;if(Nt){const r=()=>Jt[Math.min(yt,Jt.length-1)];if(e.upArrow)return bt(r=>(r-1+Jt.length)%Jt.length);if(e.downArrow)return bt(r=>(r+1)%Jt.length);if(e.pageUp)return bt(r=>Math.max(0,r-lr));if(e.pageDown)return bt(r=>Math.min(Jt.length-1,r+lr));if(e.tab){const t=r().name;return zr({text:"/"+t+" ",cursor:t.length+2})}if(e.return)return void Bt("/"+r().name);if(e.escape)return vt(!0)}if(te){const r=()=>re[Math.min(Mt,re.length-1)];if(e.upArrow)return Tt(r=>(r-1+re.length)%re.length);if(e.downArrow)return Tt(r=>(r+1)%re.length);if(e.pageUp)return Tt(r=>Math.max(0,r-lr));if(e.pageDown)return Tt(r=>Math.min(re.length-1,r+lr));if(e.tab||e.return){const t=r(),e=Xr.text.length-(Zt?Zt.length:0)-1,n=Xr.text.slice(0,e+1)+t+" ";return zr({text:n,cursor:n.length})}if(e.escape)return Et(!0)}if(e.escape){if("busy"===Wr.current&&jr.current)return jr.current.abort(),void Ct("info","已请求中断(Esc)");const r=Date.now();return r-Lt.current<600&&Qr.current.length?(Lt.current=0,It(0),void St(!0)):void(Lt.current=r)}if(e.ctrl&&"c"===t)"busy"===Wr.current&&jr.current?(jr.current.abort(),Ct("info","已请求中断")):Yt();else if(e.return)Bt(Xr.text);else{if(e.upArrow)return(()=>{const r=Qr.current;if(!r.length)return;null==Rr.current?(Fr.current=Xr.text,Rr.current=r.length-1):Rr.current>0&&Rr.current--;const t=r[Rr.current]??"";zr({text:t,cursor:t.length})})();if(e.downArrow)return(()=>{if(null==Rr.current)return;const r=Qr.current;if(Rr.current<r.length-1){Rr.current++;const t=r[Rr.current];zr({text:t,cursor:t.length})}else Rr.current=null,zr({text:Fr.current,cursor:Fr.current.length})})();if(e.leftArrow)return zr(r=>({...r,cursor:Math.max(0,r.cursor-1)}));if(e.rightArrow)return zr(r=>({...r,cursor:Math.min(r.text.length,r.cursor+1)}));if(e.ctrl&&"a"===t)return zr(r=>({...r,cursor:0}));if(e.ctrl&&"e"===t)return zr(r=>({...r,cursor:r.text.length}));if(e.ctrl&&"u"===t)return zr(r=>({text:r.text.slice(r.cursor),cursor:0}));if(e.ctrl&&"k"===t)return zr(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(e.ctrl&&"w"===t)return zr(r=>{const t=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:t+r.text.slice(r.cursor),cursor:t.length}});if(e.backspace||e.delete)return Rr.current=null,zr(r=>r.cursor>0?{text:r.text.slice(0,r.cursor-1)+r.text.slice(r.cursor),cursor:r.cursor-1}:r);if(t&&!e.ctrl&&!e.meta){Rr.current=null;const r=t.replace(/\r\n?/g,"\n");zr(t=>({text:t.text.slice(0,t.cursor)+r+t.text.slice(t.cursor),cursor:t.cursor+r.length}))}}});const se=l(()=>vr.current.slice(),[Yr]),ue=yr.split("/").filter(Boolean).pop()||yr,ie=Xr.text.slice(0,Xr.cursor),ae=Xr.text[Xr.cursor]??" ",me=Xr.text.slice(Xr.cursor+1),pe=Math.max(8,process.stdout.columns||80),fe=A(ht),ge=fe?"exhausted"===fe.level?`⛔ ${fe.label}用量已用尽${P(fe.resetAtEpochMs)?" · "+P(fe.resetAtEpochMs):""}`:`⚠ ${fe.label}用量 ${Math.round(100*fe.pct)}%(${sr(fe.used)}/${sr(fe.limit)})`:"";return or(u,{flexDirection:"column"},or(a,{key:Gr,items:se},r=>or(ir,{key:r.id,item:r})),qt.current?or(u,null,or(i,{color:"gray",italic:!0},"✦ 思考 "+qt.current.replace(/\n/g," ").slice(-160))):null,Mr.current?or(u,{marginTop:0},or(i,null,Mr.current)):null,ot.length?or(u,{flexDirection:"column",paddingLeft:2,marginTop:1},...ot.map((r,t)=>or(i,{key:t,color:"completed"===r.status?"green":"in_progress"===r.status?"cyan":"gray",dimColor:"completed"===r.status,bold:"in_progress"===r.status},("completed"===r.status?"☑ ":"in_progress"===r.status?"◐ ":"☐ ")+r.content))):null,"busy"!==Jr||Mr.current?null:or(u,null,or(i,{color:"cyan"},ur[gt]+" 思考中…"),or(i,{color:"gray"},"(Esc 中断 · 输入回车可排队补充)")),Vr?or(u,{flexDirection:"column",borderStyle:"round",borderColor:"yellow",paddingX:1,marginTop:1},or(i,{color:"yellow",bold:!0},Vr.consent?"● 是否继续?"+Vr.label:"● 需要授权:"+Vr.label),Vr.risk?or(i,{color:"red",bold:!0},"⚠ "+Vr.risk):null,Vr.consent&&Vr.detail?or(i,{color:"gray"},cr(Vr.detail,6)):null,"run_bash"===Vr.name?or(i,{color:"gray"},"$ "+(Vr.args.command||"")):null,"fetch_url"===Vr.name?or(i,{color:"gray"},"GET "+(Vr.args.url||"")):null,or(u,{marginTop:1}),...Vr.consent?[or(u,{key:"c1"},or(i,{color:"green",bold:!0}," [1] "),or(i,null,"是,继续")),or(u,{key:"c2"},or(i,{color:"cyan",bold:!0}," [2] "),or(i,null,"是,并在本次对话中后续相同操作不再询问")),or(u,{key:"c3"},or(i,{color:"gray",bold:!0}," [3] "),or(i,{color:"gray"},"否(或按 Esc / 回车)"))]:[or(u,{key:"a1"},or(i,{color:"green",bold:!0}," [y] "),or(i,null,"允许一次")),or(u,{key:"a2"},or(i,{color:"cyan",bold:!0}," [a] "),or(i,null,"本次对话都允许")),or(u,{key:"a3"},or(i,{color:"blue",bold:!0}," [A] "),or(i,null,"永久允许(写入项目设置)")),or(u,{key:"a4"},or(i,{color:"gray",bold:!0}," [n] "),or(i,{color:"gray"},"拒绝(或按 Esc / 回车)"))]):null,Nt?or(u,{flexDirection:"column",marginTop:1},or(i,{color:"gray"},Jt.length>lr?` 斜杠命令 ${Xt+1}-${Xt+zt.length}/${Jt.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · Tab 补全 · ⏎ 执行 · Esc 关闭`:" 斜杠命令 · ↑↓ 选择 · Tab 补全 · ⏎ 执行 · Esc 关闭"),...zt.map((r,t)=>{const e=Xt+t;return or(u,{key:r.name},or(i,{color:"cyan",bold:e===yt},(e===yt?" ❯ ":" ")+"/"+r.name+(r.args?" "+r.args:"")),or(i,{color:"gray"}," "+String(r.desc||"").replace(/\s+/g," ").slice(0,46)))})):null,te?or(u,{flexDirection:"column",marginTop:1},or(i,{color:"gray"},re.length>lr?` @ 引用文件 ${ee+1}-${ee+ne.length}/${re.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · Tab/⏎ 插入 · Esc 关闭`:" @ 引用文件 · ↑↓ 选择 · Tab/⏎ 插入 · Esc 关闭"),...ne.map((r,t)=>{const e=ee+t;return or(u,{key:r},or(i,{color:"cyan",bold:e===Mt},(e===Mt?" ❯ ":" ")+"@"+r))})):null,Pt&&oe.length?or(u,{flexDirection:"column",marginTop:1},or(i,{color:"gray"},oe.length>lr?` 历史记录 ${le+1}-${le+ce.length}/${oe.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · ⏎ 填入 · Esc 关闭`:" 历史记录 · ↑↓ 选择 · ⏎ 填入 · Esc 关闭"),...ce.map((r,t)=>{const e=le+t;return or(u,{key:e},or(i,{color:"cyan",bold:e===Dt},(e===Dt?" ❯ ":" ")+r.replace(/\s+/g," ").slice(0,pe-6)))})):null,or(u,{marginTop:Nt||te||Pt?0:1},or(i,{color:"blue"},"─".repeat(pe))),or(u,null,or(i,{color:"busy"===Jr?"yellow":"cyan"},"busy"===Jr?"⧗ ":"› "),or(i,null,ie),or(i,{inverse:!0},ae),or(i,null,me)),or(i,{color:"blue"},"─".repeat(pe)),or(u,{justifyContent:"space-between"},or(i,{color:"gray"},(mt?mt+" · ":"")+`${ue} · ${r.model} · ${z[ut]} ${V[ut]}`+(ct?` · 排队 ${ct}`:"")+" · ⇧Tab 模式 · /help"),ge?or(i,{color:"exhausted"===fe.level?"red":"yellow",bold:!0},ge):or(i,null,"")))}function ir({item:r}){const{kind:t,text:e}=r;if("user"===t)return or(u,{marginTop:1},or(i,{color:"cyan",bold:!0},"› "),or(i,{bold:!0},e));if("assistant"===t)return or(u,null,or(i,null,e));if("diff"===t)return or(u,{marginLeft:2},or(i,null,e));if("tool"===t)return or(i,{color:"cyan"}," ⏺ "+e);if("toolresult"===t)return or(i,{color:"gray"}," "+e);if("skip"===t)return or(i,{color:"gray"}," ⊘ "+e);if("info"===t){const r=/✗|失败|错误|error/i.test(e);return or(i,{color:r?"red":"yellow"},(r?" ✗ ":" · ")+e.replace(/^✗\s*/,""))}return or(i,null,e)}export async function runInk({config:r,cwd:t,session:e,yolo:n,initialPrompt:o,version:l}){process.stdout.write(x({model:r.model,cwd:t,version:l,platform:/^(agent|workflow|datahuman)-/.test(String(r.model||"")),yolo:n,consult:r.consult?r.consultLabel||r.consult:null,ink:!0})+"\n");const c=[],{extraTools:u,clients:i}=await $(r,t,{queueInject:r=>c.push(r),runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"}),a={clients:i},m={},{waitUntilExit:p}=s(or(App,{config:r,cwd:t,session:e,extraTools:u,mcp:a,yolo:n,initialPrompt:o,version:l,onExit:null,exitState:m,injectQueue:c}));return await p(),T(a.clients),m}
package/src/vision.js ADDED
@@ -0,0 +1 @@
1
+ export const PSEUDO_MODEL_RE=/^(agent|workflow|datahuman|squad)-/;export const VISION_MODEL_RE=/(vl|vision|gpt-4o|omni|multimodal|\dv\b)/i;export function modelSeesImages(t){const e=String(t||"").trim();return!(!e||"auto"===e)&&(!!PSEUDO_MODEL_RE.test(e)||VISION_MODEL_RE.test(e))}export function hasImageParts(t){return Array.isArray(t)&&t.some(t=>t&&"image_url"===t.type)}export function messagesHaveImages(t){return Array.isArray(t)&&t.some(t=>hasImageParts(t&&t.content))}export function stripImageParts(t,e="(图片已省略:当前模型不支持读取图片)"){if(!hasImageParts(t))return t;const s=[];let r=0;for(const e of t)e&&"image_url"===e.type?r++:s.push(e);r&&s.push({type:"text",text:e});const a=s.filter(t=>t&&"text"===t.type);return a.length===s.length?a.map(t=>t.text||"").join("\n\n"):s}export function sanitizeMessagesForModel(t,e){if(!Array.isArray(t)||modelSeesImages(e)||!messagesHaveImages(t))return{messages:t,stripped:0};let s=0;return{messages:t.map(t=>hasImageParts(t&&t.content)?(s++,{...t,content:stripImageParts(t.content)}):t),stripped:s}}
package/src/visual.js ADDED
@@ -0,0 +1 @@
1
+ import{streamChat as e,listModels as t}from"./api.js";import{modelSeesImages as n,PSEUDO_MODEL_RE as i}from"./vision.js";let r,o=0;export async function resolveVisionModel(e){const s=String(e?.visionModel||"").trim();if(s)return s;if(void 0!==r&&(r||Date.now()-o<6e4))return r;o=Date.now();try{const o=await t(e.baseUrl,e.apiKey);r=(o||[]).map(e=>e.id).filter(e=>!i.test(e)).find(e=>n(e))||null}catch{r=null}return r}export function resetVisionModelCache(){r=void 0,o=0}export async function describeImage(t,n,i){const r=await resolveVisionModel(t);if(!r||!n)return"";try{const o=await e({baseUrl:t.baseUrl,apiKey:t.apiKey,model:r,messages:[{role:"user",content:[{type:"text",text:i},{type:"image_url",image_url:{url:n}}]}],client:t.client,sessionId:t.sessionId,aux:!0},{},AbortSignal.timeout(12e4));return String(o?.content||"").trim()}catch{return""}}export function createVisualBridge({config:e,queue:t,currentModel:i}){return async function(r,o){const s=(i?i():"")||e.model;if(n(s))return void t([{type:"text",text:String(r||"")},{type:"image_url",image_url:{url:o}}]);const a=await describeImage(e,o,"请客观、详细地描述这张界面截图:整体布局、各区域的可见文字(逐字转录,含按钮/菜单/输入框/报错)、控件的位置与状态。另一个不能看图的模型会据此决定下一步点哪里,所以**位置描述要具体**(如「左上角」「窗口中部偏右的蓝色按钮」)。不要臆测未出现的内容。");t(a?`${r}\n\n【视觉模型转述的截图内容】\n${a}`:`${r}\n\n【提示】当前模型不支持读图,且暂无可用的视觉转述模型(可在配置里设 visionModel),本次截图已跳过。请改用不依赖看图的方式推进:优先 run_native_script 直接指挥应用,或用 run_python 读取/分析截图文件。`)}}