@huicai/cli 2026.711.24617 → 2026.712.80880
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 +35 -3
- package/bin/huicai.js +40 -5
- package/bin/preuninstall.cjs +20 -0
- package/package.json +4 -2
- package/squads/hc-content.md +50 -0
- package/squads/hc-data.md +50 -0
- package/squads/hc-debug.md +50 -0
- package/squads/hc-fullstack.md +59 -0
- package/squads/hc-product.md +50 -0
- package/squads/hc-refactor.md +50 -0
- package/squads/hc-release.md +55 -0
- package/squads/hc-research.md +50 -0
- package/squads/hc-review.md +49 -0
- package/squads/hc-test.md +49 -0
- package/src/agent.js +1 -1
- package/src/automation.js +1 -0
- package/src/autorun.js +1 -0
- package/src/commands.js +1 -1
- package/src/compaction.js +1 -1
- package/src/consult.js +1 -1
- package/src/context.js +1 -1
- package/src/experts.js +1 -0
- package/src/gitflow.js +1 -1
- package/src/permissions.js +1 -1
- package/src/render.js +1 -1
- package/src/repl.js +1 -1
- package/src/settings.js +1 -1
- package/src/squad.js +1 -0
- package/src/tools.js +1 -1
- package/src/transcript.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/uninstall.js +1 -0
package/src/permissions.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as o}from"./colors.js";import{toolLabel as t,renderEditDiff as r,indent as e}from"./render.js";import{loadAllowRules as s,loadDenyRules as n,matchAllowRules as l,suggestRule as a,addProjectAllowRule as i,riskWarning as
|
|
1
|
+
import{c as o}from"./colors.js";import{toolLabel as t,renderEditDiff as r,indent as e}from"./render.js";import{loadAllowRules as s,loadDenyRules as n,matchAllowRules as l,suggestRule as a,addProjectAllowRule as i,riskWarning as c}from"./settings.js";import{AUTOMATION_TOOLS as u,automationActionLabel as d,automationDetail as w}from"./automation.js";export{u as AUTOMATION_TOOLS,d as automationActionLabel,w as automationDetail};export function sandboxBlocked(o){return"run_bash"===o||"fetch_url"===o||u.has(o)}export const PLAN_DENY_REASON="当前处于计划模式:只做只读调研,不执行任何改动。请先产出完整方案交用户确认;用户 /plan 关闭计划模式后再执行改动。";export const SANDBOX_DENY_REASON="当前处于沙箱模式:禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化操作。可继续读写本地文件;如需执行/联网,请用户 /sandbox off 关闭沙箱。";export function createPermissions({yolo:f=!1,ask:p=null,cwd:y=null}={}){const _=new Set;let m=!!f,g=!1,h=!1,b=y?s(y):[],N=y?n(y):[];return{request:async function(n,f,S,$=!1){if(N.length&&l(N,n,f))return{allow:!1,reason:"该操作被拒绝规则阻止(/permissions 查看或移除)。"};if(g&&!$)return{allow:!1,reason:PLAN_DENY_REASON};if(h&&sandboxBlocked(n))return{allow:!1,reason:SANDBOX_DENY_REASON};if($)return{allow:!0};if(m||_.has(n))return{allow:!0};if(b.length&&l(b,n,f))return{allow:!0};if(!p)return{allow:!1,reason:"当前为非交互模式,未授权写/执行操作(加 --yolo 可自动放行)。"};if(u.has(n)){!function(t,r){process.stdout.write("\n"+o.bold(o.yellow("● 是否继续?"))+d(t,r)+"\n");const s=w(t,r);s&&process.stdout.write(e(o.gray(s)," ")+"\n");process.stdout.write(" "+o.bold("1、是")+" · "+o.bold("2、是,并允许在当前对话中后续相同操作不再询问")+" · "+o.bold("3、否")+"\n")}(n,f);const t=(await p(o.yellow("请选择")+" [1/2/3]: ")||"").trim();return"1"===t||/^(y|yes|是)$/i.test(t)?{allow:!0}:"2"===t||"a"===t?(_.add(n),{allow:!0}):""===t||"3"===t||/^(n|no|否)$/i.test(t)?{allow:!1,reason:"用户选择了「否」,该操作未执行。"}:{allow:!1,reason:`用户拒绝并说明:${t}`}}!function(s,n){process.stdout.write("\n"+o.bold(o.yellow("● 需要授权:"))+t(s,n)+"\n");const l=c(s,n);l&&process.stdout.write(" "+o.bold(o.red("⚠ "+l))+"\n");if("run_bash"===s)process.stdout.write(o.gray(" $ ")+String(n.command??"")+"\n");else if("write_file"===s){const t=String(n.content??"").split("\n").slice(0,12).join("\n");process.stdout.write(o.gray(` 将写入 ${n.path}:`)+"\n"+e(t)+"\n")}else"edit_file"===s?process.stdout.write(o.gray(` 编辑 ${n.path}:`)+"\n"+e(r(n.old_string,n.new_string))+"\n"):"fetch_url"===s&&process.stdout.write(o.gray(" GET ")+String(n.url??"")+"\n")}(n,f);const x=(await p(`${o.yellow("允许执行?")} [y]是 · [a]本会话该工具 · [A]永久允许(写入项目设置) · [N]否(可附原因): `)||"").trim();if("a"===x)return _.add(n),{allow:!0};if("A"===x){if(_.add(n),y){const t=a(n,f);try{const r=i(y,t);b=s(y),process.stdout.write(o.gray(` 已写入规则 ${t} → ${r}`)+"\n")}catch(t){process.stdout.write(o.yellow(` 规则写入失败(本会话仍生效):${t.message}`)+"\n")}}return{allow:!0}}const A=x.toLowerCase();return"y"===A||"yes"===A?{allow:!0}:""===x||"n"===A||"no"===A?{allow:!1,reason:"用户拒绝了该操作。"}:{allow:!1,reason:`用户拒绝并说明:${x}`}},setYolo(o){m=!!o},isYolo:()=>m,setPlanMode(o){g=!!o},isPlanMode:()=>g,setSandbox(o){h=!!o},isSandbox:()=>h,reloadRules(){b=y?s(y):[],N=y?n(y):[]}}}
|
package/src/render.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{c as t}from"./colors.js";export function toolLabel(t,e={}){switch(t){case"read_file":return`read_file ${e.path??""}`;case"list_files":return`list_files ${e.path??"."}`;case"glob":return`glob ${e.pattern??""}`;case"grep":return`grep ${JSON.stringify(e.pattern??"")}${e.path?" in "+e.path:""}`;case"search_code":return`search_code ${JSON.stringify(e.query??"")}`;case"write_file":return`write_file ${e.path??""}`;case"edit_file":return`edit_file ${e.path??""}`;case"run_bash":return`run_bash: ${String(e.command??"").split("\n")[0]}`;case"fetch_url":return`fetch_url ${e.url??""}`;case"todo_write":return"todo_write(更新任务清单)";case"dispatch_agent":{const t=String(e.objective??"").split("\n")[0];return`dispatch_agent: ${t.length>60?t.slice(0,60)+"…":t}`}case"dispatch_agents":return`dispatch_agents(并行 ${Array.isArray(e.tasks)?e.tasks.length:"?"} 个子任务)`;default:return`${t}(${Object.keys(e).join(", ")})`}}export function renderEditDiff(e,
|
|
1
|
+
import{c as t}from"./colors.js";export function toolLabel(t,e={}){switch(t){case"read_file":return`read_file ${e.path??""}`;case"list_files":return`list_files ${e.path??"."}`;case"glob":return`glob ${e.pattern??""}`;case"grep":return`grep ${JSON.stringify(e.pattern??"")}${e.path?" in "+e.path:""}`;case"search_code":return`search_code ${JSON.stringify(e.query??"")}`;case"write_file":return`write_file ${e.path??""}`;case"edit_file":return`edit_file ${e.path??""}`;case"run_bash":return`run_bash: ${String(e.command??"").split("\n")[0]}`;case"fetch_url":return`fetch_url ${e.url??""}`;case"todo_write":return"todo_write(更新任务清单)";case"dispatch_agent":{const t=String(e.objective??"").split("\n")[0];return`dispatch_agent: ${t.length>60?t.slice(0,60)+"…":t}`}case"dispatch_agents":return`dispatch_agents(并行 ${Array.isArray(e.tasks)?e.tasks.length:"?"} 个子任务)`;case"dispatch_member":{const t=String(e.task??"").split("\n")[0];return`dispatch_member ${e.squad??""}·${e.member??""}: ${t.length>48?t.slice(0,48)+"…":t}`}case"consult_expert":{const t=String(e.query??"").split("\n")[0];return`consult_expert ${e.expert??""}: ${t.length>48?t.slice(0,48)+"…":t}`}case"consult_experts":return`consult_experts(并行咨询 ${Array.isArray(e.tasks)?e.tasks.length:"?"} 个平台专家)`;case"install_packages":return`install_packages ${Array.isArray(e.packages)&&e.packages.length?e.packages.join(" "):"(全部依赖)"}${e.global?" -g":""}${e.dev?" -D":""}`;case"setup_environment":return"setup_environment"+(Array.isArray(e.commands)&&e.commands.length?`(${e.commands.length} 条配置命令)`:"(自动配置运行环境)");case"install_plugin":return`install_plugin ${e.target??""} ${e.id??""}`;case"open_browser":return`open_browser ${e.url??""}`;case"open_path":return`open_path ${e.path??""}`;case"open_app":return`open_app ${e.name??""}${e.target?" "+e.target:""}`;default:return`${t}(${Object.keys(e).join(", ")})`}}export function renderEditDiff(e,r){const n=[];for(const r of String(e??"").split("\n"))n.push(t.red("- "+r));for(const e of String(r??"").split("\n"))n.push(t.green("+ "+e));return n.join("\n")}export function diffLines(t,e){const r=t=>null==t||""===t?[]:String(t).split("\n"),n=r(t),s=r(e),a=n.length,i=s.length;if(a*i>4e6)return[...n.map(t=>({type:"-",text:t})),...s.map(t=>({type:"+",text:t}))];const p=Array.from({length:a+1},()=>new Uint32Array(i+1));for(let t=a-1;t>=0;t--)for(let e=i-1;e>=0;e--)p[t][e]=n[t]===s[e]?p[t+1][e+1]+1:Math.max(p[t+1][e],p[t][e+1]);const o=[];let l=0,c=0;for(;l<a&&c<i;)n[l]===s[c]?(o.push({type:" ",text:n[l]}),l++,c++):p[l+1][c]>=p[l][c+1]?(o.push({type:"-",text:n[l]}),l++):(o.push({type:"+",text:s[c]}),c++);for(;l<a;)o.push({type:"-",text:n[l++]});for(;c<i;)o.push({type:"+",text:s[c++]});return o}export function renderFileDiff(e,r,n={}){const{path:s="",context:a=3,maxLines:i=80}=n,p=diffLines(e,r);let o=0,l=0;for(const t of p)"+"===t.type?o++:"-"===t.type&&l++;if(0===o&&0===l)return"";const c=[];let u=0,h=0;for(const t of p)" "===t.type?(u++,h++,c.push({type:" ",text:t.text,no:u})):"-"===t.type?(u++,c.push({type:"-",text:t.text,no:u})):(h++,c.push({type:"+",text:t.text,no:h}));const g=new Array(c.length).fill(!1);c.forEach((t,e)=>{if(" "!==t.type)for(let t=Math.max(0,e-a);t<=Math.min(c.length-1,e+a);t++)g[t]=!0});const f=String(Math.max(u,h)).length,_=t=>String(null==t?"":t).padStart(f),y=[(s?t.bold(s)+" ":"")+t.green("+"+o)+" "+t.red("−"+l)];let d=!1,$=0,x=!1;for(let e=0;e<c.length;e++){if(!g[e]){d||(y.push(t.gray(" "+" ".repeat(f)+" ⋯")),d=!0);continue}if(d=!1,$>=i){x=!0;break}const r=c[e];"+"===r.type?y.push(t.green(" "+_(r.no)+" + ")+t.green(r.text)):"-"===r.type?y.push(t.red(" "+_(r.no)+" - ")+t.red(r.text)):y.push(t.gray(" "+_(r.no)+" "+r.text)),$++}return x&&y.push(t.gray(` …(差异较大,仅显示前 ${i} 行;共 +${o} −${l})`)),y.join("\n")}export function indent(t,e=" "){return String(t).split("\n").map(t=>e+t).join("\n")}export function summarize(e,r=6){const n=String(e).split("\n");return n.length<=r?e:n.slice(0,r).join("\n")+t.gray(`\n …(共 ${n.length} 行)`)}
|
package/src/repl.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import s from"node:readline";import{c as o}from"./colors.js";import{createPermissions as t}from"./permissions.js";import{runTurn as e}from"./agent.js";import{createSession as n,saveSession as r,turnCount as i}from"./session.js";import{createCheckpoints as a}from"./checkpoints.js";import{buildSystemPrompt as c}from"./context.js";import{runVerifyLoop as l}from"./autofix.js";import{loadMcpTools as
|
|
1
|
+
import s from"node:readline";import{c as o}from"./colors.js";import{createPermissions as t}from"./permissions.js";import{runTurn as e}from"./agent.js";import{createSession as n,saveSession as r,turnCount as i}from"./session.js";import{createCheckpoints as a}from"./checkpoints.js";import{buildSystemPrompt as c}from"./context.js";import{runVerifyLoop as l}from"./autofix.js";import{runAutoFlow as m}from"./autorun.js";import{loadMcpTools as p,closeMcp as u}from"./mcp.js";import{makeSubagentTool as d,makeDispatchAgentsTool as g}from"./subagent.js";import{makeDispatchMemberTool as w}from"./squad.js";import{makeConsultTool as f}from"./consult.js";import{makeExpertTools as h}from"./experts.js";import{makeAdvisorTool as y}from"./advisor.js";import{makeRecallTool as x,readMemoryRaw as T}from"./memory.js";import{detectScanners as j,makeScanTool as k}from"./scan.js";import{runSlash as b}from"./commands.js";import{expandAtMentions as I,buildUserContent as v,runBangCommand as S,loadHistory as P,appendHistory as E}from"./input.js";import{makeSkillTools as $}from"./skills.js";import{renderWelcome as _}from"./banner.js";export async function buildExtraTools(s,t){let e={tools:[],clients:[]};try{e=await p()}catch(s){process.stdout.write(o.yellow("MCP 初始化失败(忽略):"+s.message)+"\n")}const n=[...$(t),...e.tools],r=d({config:s,cwd:t,extraTools:e.tools,depth:0});r&&n.push(r);const i=g({config:s,cwd:t,extraTools:e.tools,depth:0});i&&n.push(i);const a=w({config:s,cwd:t,extraTools:e.tools});a&&n.push(a);const c=f({config:s});c&&n.push(c);try{n.push(...await h({config:s}))}catch{}const l=y({config:s});return l&&n.push(l),!1!==s.memoryEnabled&&T(t)&&n.push(x({cwd:t})),j(t).length&&n.push(k({cwd:t})),{extraTools:n,clients:e.clients}}export async function startRepl({config:i,cwd:p,yolo:d,initialPrompt:g,session:w,version:f}){const h=s.createInterface({input:process.stdin,output:process.stdout,history:P(p).reverse(),historySize:200}),y=s=>new Promise(o=>h.question(s,o));let x=t({yolo:d,ask:y,cwd:p});const T={prompt:0,completion:0},j={list:[]},k=a();let $=null,O=null;const W={config:i,cwd:p,permissions:x,usage:T,session:w,version:f};h.on("SIGINT",()=>{$?$.abort():process.stdout.write("\n"+o.gray("输入 /exit 退出。")+"\n")}),function(s,o,t,e){process.stdout.write(_({model:s.model,cwd:o,version:e,platform:/^(agent|workflow|datahuman|squad)-/.test(String(s.model||"")),yolo:t.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(i,p,x,f),q(w);let{extraTools:C,clients:J}=await buildExtraTools(i,p);const N=async s=>{const{text:t,mentions:n,images:a}=await I(s,p,i);n.length&&process.stdout.write(o.gray(" · 已展开引用:"+n.join("、")+(a.length?`(含 ${a.length} 张图片)`:""))+"\n");const c=k.snapshot({session:W.session,label:s});W.session.messages.push({role:"user",content:v(t,a)}),$=new AbortController,W.session.compaction||(W.session.compaction={}),i.sessionId=W.session.id;const l=await e({config:i,messages:W.session.messages,permissions:x,cwd:p,signal:$.signal,extraTools:C,todoState:j,compaction:W.session.compaction,fileJournal:c});return l?.usage&&(T.prompt+=l.usage.prompt,T.completion+=l.usage.completion),l?.lastPromptTokens&&(W.session.lastPromptTokens=l.lastPromptTokens),$=null,r(W.session),l},Y=()=>({...W,session:W.session,extraTools:C,checkpoints:k,print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(g&&(process.stdout.write(o.cyan("\nhuicai> ")+g+"\n"),await N(g));;){let s;try{s=(await y(o.cyan("\nhuicai> "))).trim()}catch{break}if(""!==s){if(E(p,s),"/exit"===s||"/quit"===s)break;if(s.startsWith("!")&&s.length>1){const t=s.slice(1).trim(),{output:e,record:n}=await S(t,p);process.stdout.write(o.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),W.session.messages.push({role:"user",content:n}),r(W.session);continue}if(s.startsWith("/")){const e=await b(s,Y());if("exit"===e.action)break;"clear"===e.action&&(W.session=n({cwd:p,model:i.model,messages:[W.session.messages[0]]}),j.list=[],k.list.length=0,process.stdout.write(o.gray("已开启新会话(旧会话已保存,可用 huicai --resume 恢复)。\n"))),"refresh"===e.action&&q(W.session),"cd"===e.action&&(p=e.cwd,W.cwd=p,x=t({yolo:x.isYolo(),ask:y,cwd:p}),W.permissions=x,u(J),({extraTools:C,clients:J}=await buildExtraTools(i,p)),W.session=n({cwd:p,model:i.model,messages:[{role:"system",content:c(p,{memory:!1!==i.memoryEnabled})}]}),j.list=[],k.list.length=0,i.sessionId=W.session.id,process.stdout.write(o.gray("已切换到 "+p+"(新会话,旧会话已保存)。\n"))),"autofix"===e.action&&await l({command:e.command,maxIters:e.maxIters,cwd:p,runTurnOnce:s=>N(s),emit:s=>process.stdout.write(o.gray(s)+"\n")}),"autorun"===e.action&&await m({goal:e.goal,config:i,permissions:x,cwd:p,runTurnOnce:s=>N(s),emit:s=>process.stdout.write(o.gray(" · "+s)+"\n")}),"loop"===e.action&&(O&&(clearInterval(O),O=null),e.stop?process.stdout.write(o.gray("已停止循环。\n")):(process.stdout.write(o.gray(`已开始循环:每 ${e.seconds}s 运行「${e.prompt}」(/loop off 停止)。\n`)),O=setInterval(()=>{$||N(e.prompt)},1e3*e.seconds))),e.prompt&&await N(e.prompt);continue}await N(s)}}O&&clearInterval(O),h.close(),u(J),process.stdout.write(o.gray("\n再见。\n"))}function q(s){const t=i(s.messages);if(0===t)return;process.stdout.write(o.gray(` 已恢复会话「${s.title||""}」(${t} 轮)`)+"\n");const e=[...s.messages].reverse().find(s=>"assistant"===s.role&&s.content);if(e){const s=String(e.content).trim(),t=s.length>400?s.slice(0,400)+"…":s;process.stdout.write(o.gray(" 上次回复:")+t.replace(/\n/g,"\n ")+"\n")}}export async function runOnce({config:s,cwd:o,yolo:n,prompt:a,session:c,persist:l,outputFormat:m,maxTurns:p}){a||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const d="json"===m,g=t({yolo:n,ask:null,cwd:o}),{extraTools:w,clients:f}=await buildExtraTools(s,o),{text:h,images:y}=await I(a,o,s);c.messages.push({role:"user",content:v(h,y)}),c.compaction||(c.compaction={}),s.sessionId=c.id;const x=await e({config:s,messages:c.messages,permissions:g,cwd:o,extraTools:w,maxIters:p,compaction:c.compaction,io:d?{write:()=>{}}:void 0});if(x?.lastPromptTokens&&(c.lastPromptTokens=x.lastPromptTokens),l&&r(c),u(f),d){const o=[...c.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!x?.error,...x?.error?{error:x.error}:{},result:o?String(o.content):"",usage:{input_tokens:x?.usage?.prompt??0,output_tokens:x?.usage?.completion??0},session_id:c.id,model:s.model,num_turns:i(c.messages)})+"\n")}x?.error&&process.exit(1)}
|
package/src/settings.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import r from"node:fs";import t from"node:path";import{CONFIG_DIR as n}from"./config.js";export const GLOBAL_SETTINGS_PATH=t.join(n,"settings.json");export function projectSettingsPath(r){return t.join(r,".huicai","settings.json")}function e(t){try{return JSON.parse(r.readFileSync(t,"utf8"))}catch{return null}}function s(r,t){const n=r?.permissions?.[t];return Array.isArray(n)?n.filter(r=>"string"==typeof r&&r.trim()):[]}function o(r){return s(r,"allow")}function i(r){return s(r,"deny")}export function loadAllowRules(r){const t=[...o(e(GLOBAL_SETTINGS_PATH))];return r&&t.push(...o(e(projectSettingsPath(r)))),t}export function loadDenyRules(r){const t=[...i(e(GLOBAL_SETTINGS_PATH))];return r&&t.push(...i(e(projectSettingsPath(r)))),t}export function loadRules(r){return{allow:loadAllowRules(r),deny:loadDenyRules(r)}}export function describeRules(r){const t=e(GLOBAL_SETTINGS_PATH),n=r?e(projectSettingsPath(r)):null;return{global:{allow:o(t),deny:i(t)},project:{allow:o(n),deny:i(n)}}}export function loadRawSettings(r){return{global:e(GLOBAL_SETTINGS_PATH)||{},project:r&&e(projectSettingsPath(r))||{}}}export function parseRule(r){const t=/^\s*([A-Za-z0-9_-]+)\s*(?:\((.*)\))?\s*$/.exec(String(r));return t?{name:t[1],pattern:null!=t[2]&&""!==t[2]?t[2]:null}:null}function u(r,t={}){return"run_bash"===r?String(t.command??""):"fetch_url"===r?String(t.url??""):null!=t.path?String(t.path):""}function l(r){const t=String(r).replace(/[.*+?^${}()|[\]\\]/g,r=>"*"===r?"\0":"\\"+r);return new RegExp("^"+t.replaceAll("\0","[\\s\\S]*")+"$")}export function matchAllowRules(r,t,n){for(const e of r||[]){const r=parseRule(e);if(r&&r.name===t){if(!r.pattern)return!0;try{if(l(r.pattern).test(u(t,n)))return!0}catch{}}}return!1}export function suggestRule(r,t={}){if("run_bash"===r){const r=String(t.command??"").trim().split(/\s+/)[0];return r?`run_bash(${r} *)`:"run_bash"}if("fetch_url"===r)try{return`fetch_url(${new URL(String(t.url)).origin}*)`}catch{return"fetch_url"}return r}export function addProjectRule(n,s,o){const i="deny"===s?"deny":"allow",u=projectSettingsPath(n),l=e(u)||{};return l.permissions&&"object"==typeof l.permissions||(l.permissions={}),Array.isArray(l.permissions[i])||(l.permissions[i]=[]),l.permissions[i].includes(o)||l.permissions[i].push(o),r.mkdirSync(t.dirname(u),{recursive:!0}),r.writeFileSync(u,JSON.stringify(l,null,2)+"\n"),u}export function removeProjectRule(t,n){const s=projectSettingsPath(t),o=e(s);if(!o?.permissions)return{removed:!1,path:s};let i=!1;for(const r of["allow","deny"])Array.isArray(o.permissions[r])&&o.permissions[r].includes(n)&&(o.permissions[r]=o.permissions[r].filter(r=>r!==n),i=!0);return i&&r.writeFileSync(s,JSON.stringify(o,null,2)+"\n"),{removed:i,path:s}}export function addProjectAllowRule(r,t){return addProjectRule(r,"allow",t)}export function riskWarning(r,t={}){if("run_bash"!==r)return null;const n=String(t.command??""),e=[[/\brm\s+(-\w*[rf]\w*\s+)+/i,"递归/强制删除"],[/\bsudo\b/,"提权执行"],[/\bgit\s+push\b.*(--force|-f\b)/,"强制推送"],[/\bgit\s+reset\s+--hard\b/,"硬重置(丢弃改动)"],[/\bgit\s+clean\b.*-\w*f/,"清除未跟踪文件"],[/\bchmod\s+(-R\s+)?777\b/,"开放全部权限"],[/\bdd\s+if=/,"磁盘级写入"],[/\bmkfs\b|\bdiskutil\s+erase/i,"格式化磁盘"],[/>\s*\/dev\/(sd|disk|nvme)/,"覆写设备"],[/\bDROP\s+(TABLE|DATABASE)\b/i,"删除数据表/库"],[/\bkubectl\s+delete\b|\bterraform\s+destroy\b/,"销毁云资源"]];for(const[r,t]of e)if(r.test(n))return`高风险命令(${t}),请仔细确认`;return null}
|
|
1
|
+
import r from"node:fs";import t from"node:path";import{CONFIG_DIR as n}from"./config.js";export const GLOBAL_SETTINGS_PATH=t.join(n,"settings.json");export function projectSettingsPath(r){return t.join(r,".huicai","settings.json")}function e(t){try{return JSON.parse(r.readFileSync(t,"utf8"))}catch{return null}}function s(r,t){const n=r?.permissions?.[t];return Array.isArray(n)?n.filter(r=>"string"==typeof r&&r.trim()):[]}function o(r){return s(r,"allow")}function i(r){return s(r,"deny")}export function loadAllowRules(r){const t=[...o(e(GLOBAL_SETTINGS_PATH))];return r&&t.push(...o(e(projectSettingsPath(r)))),t}export function loadDenyRules(r){const t=[...i(e(GLOBAL_SETTINGS_PATH))];return r&&t.push(...i(e(projectSettingsPath(r)))),t}export function loadRules(r){return{allow:loadAllowRules(r),deny:loadDenyRules(r)}}export function describeRules(r){const t=e(GLOBAL_SETTINGS_PATH),n=r?e(projectSettingsPath(r)):null;return{global:{allow:o(t),deny:i(t)},project:{allow:o(n),deny:i(n)}}}export function loadRawSettings(r){return{global:e(GLOBAL_SETTINGS_PATH)||{},project:r&&e(projectSettingsPath(r))||{}}}export function parseRule(r){const t=/^\s*([A-Za-z0-9_-]+)\s*(?:\((.*)\))?\s*$/.exec(String(r));return t?{name:t[1],pattern:null!=t[2]&&""!==t[2]?t[2]:null}:null}function u(r,t={}){return"run_bash"===r?String(t.command??""):"fetch_url"===r||"open_browser"===r?String(t.url??""):"open_app"===r?String(t.name??""):"install_plugin"===r?String(t.id??""):"install_packages"===r?Array.isArray(t.packages)?t.packages.join(" "):"":null!=t.path?String(t.path):""}function l(r){const t=String(r).replace(/[.*+?^${}()|[\]\\]/g,r=>"*"===r?"\0":"\\"+r);return new RegExp("^"+t.replaceAll("\0","[\\s\\S]*")+"$")}export function matchAllowRules(r,t,n){for(const e of r||[]){const r=parseRule(e);if(r&&r.name===t){if(!r.pattern)return!0;try{if(l(r.pattern).test(u(t,n)))return!0}catch{}}}return!1}export function suggestRule(r,t={}){if("run_bash"===r){const r=String(t.command??"").trim().split(/\s+/)[0];return r?`run_bash(${r} *)`:"run_bash"}if("fetch_url"===r)try{return`fetch_url(${new URL(String(t.url)).origin}*)`}catch{return"fetch_url"}return r}export function addProjectRule(n,s,o){const i="deny"===s?"deny":"allow",u=projectSettingsPath(n),l=e(u)||{};return l.permissions&&"object"==typeof l.permissions||(l.permissions={}),Array.isArray(l.permissions[i])||(l.permissions[i]=[]),l.permissions[i].includes(o)||l.permissions[i].push(o),r.mkdirSync(t.dirname(u),{recursive:!0}),r.writeFileSync(u,JSON.stringify(l,null,2)+"\n"),u}export function removeProjectRule(t,n){const s=projectSettingsPath(t),o=e(s);if(!o?.permissions)return{removed:!1,path:s};let i=!1;for(const r of["allow","deny"])Array.isArray(o.permissions[r])&&o.permissions[r].includes(n)&&(o.permissions[r]=o.permissions[r].filter(r=>r!==n),i=!0);return i&&r.writeFileSync(s,JSON.stringify(o,null,2)+"\n"),{removed:i,path:s}}export function addProjectAllowRule(r,t){return addProjectRule(r,"allow",t)}export function riskWarning(r,t={}){if("run_bash"!==r)return null;const n=String(t.command??""),e=[[/\brm\s+(-\w*[rf]\w*\s+)+/i,"递归/强制删除"],[/\bsudo\b/,"提权执行"],[/\bgit\s+push\b.*(--force|-f\b)/,"强制推送"],[/\bgit\s+reset\s+--hard\b/,"硬重置(丢弃改动)"],[/\bgit\s+clean\b.*-\w*f/,"清除未跟踪文件"],[/\bchmod\s+(-R\s+)?777\b/,"开放全部权限"],[/\bdd\s+if=/,"磁盘级写入"],[/\bmkfs\b|\bdiskutil\s+erase/i,"格式化磁盘"],[/>\s*\/dev\/(sd|disk|nvme)/,"覆写设备"],[/\bDROP\s+(TABLE|DATABASE)\b/i,"删除数据表/库"],[/\bkubectl\s+delete\b|\bterraform\s+destroy\b/,"销毁云资源"]];for(const[r,t]of e)if(r.test(n))return`高风险命令(${t}),请仔细确认`;return null}
|
package/src/squad.js
ADDED
|
@@ -0,0 +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";
|
package/src/tools.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import*as e from"./executors.js";import{searchCode as t}from"./codeindex.js";const
|
|
1
|
+
import*as e from"./executors.js";import{searchCode as t}from"./codeindex.js";import{AUTOMATION_TOOL_SPECS as r,AUTOMATION_EXECUTORS as i}from"./automation.js";const n=(e,t)=>({type:"object",properties:e,required:t||[]}),o=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:n({path:o("相对项目根的文件路径"),offset:a("可选:起始行(1基)"),limit:a("可选:读取行数")},["path"])}},{type:"function",function:{name:"list_files",description:"列出目录下的文件树(跳过 .git/node_modules 等)。可选 path 指定子目录(默认项目根)。",parameters:n({path:o("可选:子目录路径,默认当前项目根")},[])}},{type:"function",function:{name:"glob",description:'按 glob 模式查找文件(支持 ** 与 *),如 "**/*.ts"、"src/**/*.java"。',parameters:n({pattern:o("glob 模式")},["pattern"])}},{type:"function",function:{name:"search_code",description:"按「概念/功能」检索最相关的代码块(BM25 相关性排序,比 grep 更懂「哪段代码在做这件事」)。适合「登录逻辑在哪」「哪里处理支付回调」这类找相关实现;要找精确字符串仍用 grep。返回 文件:行范围 + 代码块。",parameters:n({query:o("要找的功能/概念(自然语言或关键词组合,多写几个相关词更准)"),k:a("可选:返回块数,默认 8,上限 20")},["query"])}},{type:"function",function:{name:"grep",description:"在文件内容中按正则搜索,返回 匹配文件:行号: 该行。可选 path 限定目录、glob 限定文件名模式、ignore_case 忽略大小写、files_only 只列出命中文件。",parameters:n({pattern:o("正则表达式"),path:o("可选:搜索目录,默认项目根"),glob:o("可选:文件名 glob 过滤"),ignore_case:s("可选:忽略大小写(默认否)"),files_only:s("可选:只返回命中文件列表,不含匹配行(默认否)"),max_results:a("可选:最多返回条数(默认 200,上限 500)")},["pattern"])}},{type:"function",function:{name:"write_file",description:"把文本写入文件(覆盖式,自动建父目录)。仅限当前项目目录内。仅用于新建文件或全量重写;小改动优先 edit_file。",parameters:n({path:o("相对项目根的文件路径"),content:o("完整文件内容")},["path","content"])}},{type:"function",function:{name:"edit_file",description:"精确替换文件中的片段:old_string 必须是文件原文(不含 read_file 的行号前缀)且在文件中唯一(除非 replace_all=true)。仅限项目目录内。",parameters:n({path:o("相对项目根的文件路径"),old_string:o("被替换的原文(需在文件中唯一,除非 replace_all;不要包含行号前缀)"),new_string:o("替换为的新内容"),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:n({command:o("要执行的 shell 命令"),timeout:a("可选:超时毫秒")},["command"])}},{type:"function",function:{name:"fetch_url",description:"抓取一个 http/https 网页或接口(GET),HTML 自动转为纯文本。用于查在线文档、API 说明、Issue 等。结果可能很长,尽量带着明确目的使用。",parameters:n({url:o("完整 URL(http/https)"),timeout:a("可选:超时毫秒,默认 30000")},["url"])}},...r];export const READONLY_TOOLS=new Set(["read_file","list_files","glob","grep","search_code"]);const p={read_file:e.readFile,list_files:e.listFiles,glob:e.glob,grep:e.grep,search_code:t,write_file:e.writeFile,edit_file:e.editFile,run_bash:e.runBash,fetch_url:e.fetchUrl,...i};export async function runTool(e,t,r,i){const n=p[e];if(!n)return`未知工具:${e}`;try{return await n(r,t||{},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)-/;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,r
|
|
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]||"(无法生成小结)"}
|
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 s}from"react";import{execSync as c}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 g}from"../api.js";import{toolLabel as d}from"../render.js";import{renderMarkdown as h,looksLikeMarkdown as x}from"../markdown.js";import{renderWelcome as y}from"../banner.js";import{createSession as w,saveSession as b}from"../session.js";import{createCheckpoints as v}from"../checkpoints.js";import{buildExtraTools as k}from"../repl.js";import{closeMcp as T}from"../mcp.js";import{runSlash as $,listCommands as j}from"../commands.js";import{usageStatus as P,fmtReset as S}from"../usage.js";import{expandAtMentions as M,buildUserContent as A,runBangCommand as E,loadHistory as I,appendHistory as D}from"../input.js";import{loadAllowRules as L,loadDenyRules as _,matchAllowRules as C,suggestRule as U,addProjectAllowRule as R,riskWarning as W}from"../settings.js";import{PLAN_DENY_REASON as F,SANDBOX_DENY_REASON as K}from"../permissions.js";import{runVerifyLoop as Y}from"../autofix.js";const q=r.createElement,B=10;function G(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}const J=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];export function App({config:r,cwd:l,session:y,extraTools:k,yolo:T,initialPrompt:O,version:Q,onExit:X}){const z=p(),H=e([]),V=e(0),Z=e(""),rr=e(y),tr=e(null),er=e(null),nr=e(new Set),or=e(L(l)),sr=e(_(l)),cr=e(!1),lr=e(!1),ur=e(v()),ir=e(!!T),ar=e(!1),mr=e("idle"),pr=e([]),fr=e({list:[]}),gr=e(I(l)),dr=e(null),hr=e(""),[,xr]=n(r=>r+1,0),[yr,wr]=n(r=>r+1,0),[br,vr]=t(0),[kr,Tr]=t("idle"),[$r,jr]=t({text:"",cursor:0}),[Pr,Sr]=t(null),[Mr,Ar]=t({prompt:0,completion:0}),[Er,Ir]=t(0),[Dr,Lr]=t([]),[_r,Cr]=t(0),[Ur,Rr]=t(!1),[Wr,Fr]=t(""),Kr=e(null),[Yr,qr]=t(0),[Br,Gr]=t(null),Jr=e(null),[Nr,Or]=t(0),[Qr,Xr]=t(!1),zr=s(()=>j(l),[l]),Hr=(r,t)=>{H.current.push({id:V.current++,kind:r,text:t}),wr()},Vr=()=>{if(Z.current){const t=Z.current,e=!1!==r.markdown&&x(t);Hr("assistant",e?h(t):t),Z.current=""}},Zr=r=>{mr.current=r?"busy":"idle",Tr(mr.current)};o(()=>{if("busy"!==kr)return;const r=setInterval(()=>qr(r=>(r+1)%J.length),80);return()=>clearInterval(r)},[kr]);const rt=async()=>{if(r.apiKey)try{const t=await g(r.baseUrl,r.apiKey);Jr.current=t,Gr(t)}catch{}};o(()=>{rt();const r=setInterval(()=>{rt()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void Fr("");const t=()=>{try{const t=c(r.statusLine,{cwd:l,timeout:3e3,stdio:["ignore","pipe","ignore"]}).toString().split("\n")[0].slice(0,80);Fr(t)}catch{Fr("")}};t();const e=setInterval(t,5e3);return()=>clearInterval(e)},[r.statusLine]),o(()=>()=>{Kr.current&&clearInterval(Kr.current)},[]);const tt=e(""),et={onDelta:r=>{tt.current&&(tt.current=""),Z.current+=r,xr()},onReasoning:r=>{tt.current=(tt.current+r).slice(-400),xr()},onToolStart:(r,t)=>{tt.current="",Vr(),Hr("tool",d(r,t))},onToolResult:(r,t)=>Hr("toolresult",function(r,t=200){const e=String(r).split("\n")[0];return e.length>t?e.slice(0,t)+"…":e}(t)),onFileDiff:r=>Hr("diff",r),onSkip:(r,t)=>Hr("skip",d(r,t)+" — 已跳过"),onInfo:r=>{Vr(),Hr("info",r)},onTodo:r=>Lr(r),onTurnEnd:()=>{tt.current="",Vr()}},nt={request:(r,t,e,n)=>sr.current.length&&C(sr.current,r,t)?Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions 查看或移除)。"}):cr.current&&!n?Promise.resolve({allow:!1,reason:F}):!lr.current||"run_bash"!==r&&"fetch_url"!==r?n||ir.current||nr.current.has(r)||C(or.current,r,t)?Promise.resolve({allow:!0}):new Promise(e=>{er.current={name:r,args:t,resolve:e},Sr({label:d(r,t),name:r,args:t,risk:W(r,t)})}):Promise.resolve({allow:!1,reason:K}),isYolo:()=>ir.current,setYolo:r=>{ir.current=!!r},setPlanMode:r=>{cr.current=!!r,Rr(!!r)},isPlanMode:()=>cr.current,setSandbox:r=>{lr.current=!!r},isSandbox:()=>lr.current,reloadRules:()=>{or.current=L(l),sr.current=_(l)}},ot=()=>{const r=pr.current.shift();return Cr(pr.current.length),null==r?null:r},st=async t=>{const e=P(Jr.current);if(e&&"exhausted"===e.level)return void Hr("info",`✗ Token 用量窗口(${e.label})已用尽,暂无法继续。${S(e.resetAtEpochMs)}`);const{text:n,images:o}=await M(t,l,r),s=ur.current.snapshot({session:rr.current,label:t});rr.current.messages.push({role:"user",content:A(n,o)}),Zr(!0),tr.current=new AbortController,rr.current.compaction||(rr.current.compaction={});const c=await f({config:r,messages:rr.current.messages,permissions:nt,cwd:l,extraTools:k,events:et,signal:tr.current.signal,todoState:fr.current,compaction:rr.current.compaction,pullQueued:ot,fileJournal:s});return tr.current=null,Vr(),c?.usage&&Ar(r=>({prompt:r.prompt+c.usage.prompt,completion:r.completion+c.usage.completion})),c?.lastPromptTokens&&(Ir(c.lastPromptTokens),rr.current.lastPromptTokens=c.lastPromptTokens),b(rr.current),(c?.aborted||c?.error)&&pr.current.length&&(pr.current=[],Cr(0),Hr("info","已清空排队消息")),Zr(!1),rt(),c},ct=()=>{X?.(),z.exit()},lt=async r=>{if("busy"===mr.current)return void Hr("info","正在处理上一条,! 命令请稍候再试");Hr("user","! "+r);const{output:t,record:e}=await E(r,l);Hr("toolresult",function(r,t=12){const e=String(r).split("\n");return e.length<=t?r:e.slice(0,t).join("\n")+`\n…(共 ${e.length} 行)`}(t)),rr.current.messages.push({role:"user",content:e}),b(rr.current)},ut=async t=>{const e=t.trim();if(jr({text:"",cursor:0}),dr.current=null,e){if(D(l,e),gr.current[gr.current.length-1]!==e&&gr.current.push(e),e.startsWith("/"))return(async t=>{const e={version:Q,config:r,cwd:l,usage:Mr,permissions:nt,extraTools:k,session:rr.current,checkpoints:ur.current,print:r=>Hr("info",String(r))},n=await $(t,e);if("exit"===n.action)return ct();"clear"===n.action&&(rr.current=w({cwd:l,model:r.model,messages:[rr.current.messages[0]]}),fr.current.list=[],ur.current.list.length=0,Lr([]),Ir(0),H.current=[],Z.current="",vr(r=>r+1),Hr("info","已开启新会话(旧会话仍可 --resume 恢复)")),"refresh"===n.action&&(fr.current.list=[],Lr([]),Ir(0)),"autofix"===n.action&&await Y({command:n.command,maxIters:n.maxIters,cwd:l,runTurnOnce:r=>st(r),emit:r=>Hr("info",r)}),"cd"===n.action&&Hr("info","富 TUI 暂不支持 /cd 切换目录;请用 huicai --plain(普通模式)使用,或退出后到目标目录重开。"),"loop"===n.action&&(Kr.current&&(clearInterval(Kr.current),Kr.current=null),n.stop?Hr("info","已停止循环。"):(Hr("info",`已开始循环:每 ${n.seconds}s 运行「${n.prompt}」(/loop off 停止)`),Kr.current=setInterval(()=>{"busy"!==mr.current&&(Hr("user",n.prompt),st(n.prompt))},1e3*n.seconds))),n.prompt&&("busy"===mr.current?(pr.current.push(n.prompt),Cr(pr.current.length)):(Hr("user",n.prompt),await st(n.prompt)))})(e);if(e.startsWith("!")&&e.length>1)return lt(e.slice(1).trim());if("busy"===mr.current)return Hr("user",e),pr.current.push(e),void Cr(pr.current.length);Hr("user",e),await st(e)}};o(()=>{!ar.current&&O&&(ar.current=!0,ut(O))},[]);const it=/^\/(\S*)$/.exec($r.text),at=it?it[1]:null,mt=null==at?[]:zr.filter(r=>r.name.toLowerCase().startsWith(at.toLowerCase())),pt=mt.length>0&&!Qr,ft=mt.length<=B?0:Math.max(0,Math.min(Nr-Math.floor(5),mt.length-B)),gt=mt.slice(ft,ft+B);o(()=>{Or(0),Xr(!1)},[at]),m((r,t)=>{if(er.current){const e=er.current,n=r=>{er.current=null,Sr(null),e.resolve(r)};if("y"===r)return n({allow:!0});if("a"===r)return nr.current.add(e.name),n({allow:!0});if("A"===r){nr.current.add(e.name);const r=U(e.name,e.args);try{R(l,r),or.current=L(l),Hr("info",`已写入允许规则:${r} → .huicai/settings.json`)}catch(r){Hr("info",`规则写入失败(本会话仍生效):${r.message}`)}return n({allow:!0})}return"n"===r||t.escape||t.return?n({allow:!1,reason:"用户拒绝了该操作。"}):void 0}if(pt){const r=()=>mt[Math.min(Nr,mt.length-1)];if(t.upArrow)return Or(r=>(r-1+mt.length)%mt.length);if(t.downArrow)return Or(r=>(r+1)%mt.length);if(t.pageUp)return Or(r=>Math.max(0,r-B));if(t.pageDown)return Or(r=>Math.min(mt.length-1,r+B));if(t.tab){const t=r().name;return jr({text:"/"+t+" ",cursor:t.length+2})}if(t.return)return void ut("/"+r().name);if(t.escape)return Xr(!0)}if(t.escape)"busy"===mr.current&&tr.current&&(tr.current.abort(),Hr("info","已请求中断(Esc)"));else if(t.ctrl&&"c"===r)"busy"===mr.current&&tr.current?(tr.current.abort(),Hr("info","已请求中断")):ct();else if(t.return)ut($r.text);else{if(t.upArrow)return(()=>{const r=gr.current;if(!r.length)return;null==dr.current?(hr.current=$r.text,dr.current=r.length-1):dr.current>0&&dr.current--;const t=r[dr.current]??"";jr({text:t,cursor:t.length})})();if(t.downArrow)return(()=>{if(null==dr.current)return;const r=gr.current;if(dr.current<r.length-1){dr.current++;const t=r[dr.current];jr({text:t,cursor:t.length})}else dr.current=null,jr({text:hr.current,cursor:hr.current.length})})();if(t.leftArrow)return jr(r=>({...r,cursor:Math.max(0,r.cursor-1)}));if(t.rightArrow)return jr(r=>({...r,cursor:Math.min(r.text.length,r.cursor+1)}));if(t.ctrl&&"a"===r)return jr(r=>({...r,cursor:0}));if(t.ctrl&&"e"===r)return jr(r=>({...r,cursor:r.text.length}));if(t.ctrl&&"u"===r)return jr(r=>({text:r.text.slice(r.cursor),cursor:0}));if(t.ctrl&&"k"===r)return jr(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(t.ctrl&&"w"===r)return jr(r=>{const t=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:t+r.text.slice(r.cursor),cursor:t.length}});if(t.backspace||t.delete)return dr.current=null,jr(r=>r.cursor>0?{text:r.text.slice(0,r.cursor-1)+r.text.slice(r.cursor),cursor:r.cursor-1}:r);if(r&&!t.ctrl&&!t.meta){dr.current=null;const t=r.replace(/\r\n?/g,"\n");jr(r=>({text:r.text.slice(0,r.cursor)+t+r.text.slice(r.cursor),cursor:r.cursor+t.length}))}}});const dt=s(()=>H.current.slice(),[yr]),ht=l.split("/").filter(Boolean).pop()||l,xt=$r.text.slice(0,$r.cursor),yt=$r.text[$r.cursor]??" ",wt=$r.text.slice($r.cursor+1),bt=Math.max(8,process.stdout.columns||80),vt=P(Br),kt=vt?"exhausted"===vt.level?`⛔ ${vt.label}用量已用尽${S(vt.resetAtEpochMs)?" · "+S(vt.resetAtEpochMs):""}`:`⚠ ${vt.label}用量 ${Math.round(100*vt.pct)}%(${G(vt.used)}/${G(vt.limit)})`:"";return q(u,{flexDirection:"column"},q(a,{key:br,items:dt},r=>q(N,{key:r.id,item:r})),tt.current?q(u,null,q(i,{color:"gray",italic:!0},"✦ 思考 "+tt.current.replace(/\n/g," ").slice(-160))):null,Z.current?q(u,{marginTop:0},q(i,null,Z.current)):null,Dr.length?q(u,{flexDirection:"column",paddingLeft:2,marginTop:1},...Dr.map((r,t)=>q(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"!==kr||Z.current?null:q(u,null,q(i,{color:"cyan"},J[Yr]+" 思考中…"),q(i,{color:"gray"},"(Esc 中断 · 输入回车可排队补充)")),Pr?q(u,{flexDirection:"column",borderStyle:"round",borderColor:"yellow",paddingX:1,marginTop:1},q(i,{color:"yellow",bold:!0},"● 需要授权:"+Pr.label),Pr.risk?q(i,{color:"red",bold:!0},"⚠ "+Pr.risk):null,"run_bash"===Pr.name?q(i,{color:"gray"},"$ "+(Pr.args.command||"")):null,"fetch_url"===Pr.name?q(i,{color:"gray"},"GET "+(Pr.args.url||"")):null,q(i,null,"[y] 允许一次 · [a] 本会话 · [A] 永久(写入项目设置) · [n/Esc] 拒绝")):null,pt?q(u,{flexDirection:"column",marginTop:1},q(i,{color:"gray"},mt.length>B?` 斜杠命令 ${ft+1}-${ft+gt.length}/${mt.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · Tab 补全 · ⏎ 执行 · Esc 关闭`:" 斜杠命令 · ↑↓ 选择 · Tab 补全 · ⏎ 执行 · Esc 关闭"),...gt.map((r,t)=>{const e=ft+t;return q(u,{key:r.name},q(i,{color:"cyan",bold:e===Nr},(e===Nr?" ❯ ":" ")+"/"+r.name+(r.args?" "+r.args:"")),q(i,{color:"gray"}," "+String(r.desc||"").replace(/\s+/g," ").slice(0,46)))})):null,q(u,{marginTop:pt?0:1},q(i,{color:"blue"},"─".repeat(bt))),q(u,null,q(i,{color:"busy"===kr?"yellow":"cyan"},"busy"===kr?"⧗ ":"› "),q(i,null,xt),q(i,{inverse:!0},yt),q(i,null,wt)),q(i,{color:"blue"},"─".repeat(bt)),q(u,{justifyContent:"space-between"},q(i,{color:"gray"},(Wr?Wr+" · ":"")+`${ht} · ${r.model}${ir.current?" · ⚠ yolo":""}${Ur?" · 📋 计划":""}`+(_r?` · 排队 ${_r}`:"")+" · /help"),kt?q(i,{color:"exhausted"===vt.level?"red":"yellow",bold:!0},kt):q(i,null,"")))}function N({item:r}){const{kind:t,text:e}=r;if("user"===t)return q(u,{marginTop:1},q(i,{color:"cyan",bold:!0},"› "),q(i,{bold:!0},e));if("assistant"===t)return q(u,null,q(i,null,e));if("diff"===t)return q(u,{marginLeft:2},q(i,null,e));if("tool"===t)return q(i,{color:"cyan"}," ⏺ "+e);if("toolresult"===t)return q(i,{color:"gray"}," "+e);if("skip"===t)return q(i,{color:"gray"}," ⊘ "+e);if("info"===t){const r=/✗|失败|错误|error/i.test(e);return q(i,{color:r?"red":"yellow"},(r?" ✗ ":" · ")+e.replace(/^✗\s*/,""))}return q(i,null,e)}export async function runInk({config:r,cwd:t,session:e,yolo:n,initialPrompt:o,version:s}){process.stdout.write(y({model:r.model,cwd:t,version:s,platform:/^(agent|workflow|datahuman)-/.test(String(r.model||"")),yolo:n,consult:r.consult?r.consultLabel||r.consult:null,ink:!0})+"\n");const{extraTools:c,clients:u}=await k(r,t),{waitUntilExit:i}=l(q(App,{config:r,cwd:t,session:e,extraTools:c,yolo:n,initialPrompt:o,version:s,onExit:()=>T(u)}));await i(),T(u)}
|
|
1
|
+
import r,{useState as t,useRef as e,useReducer as n,useEffect as o,useMemo as s}from"react";import{execSync as c}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 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{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 P,fmtReset as S}from"../usage.js";import{expandAtMentions as M,buildUserContent as A,runBangCommand as I,loadHistory as D,appendHistory as L}from"../input.js";import{loadAllowRules as C,loadDenyRules as _,matchAllowRules as U,suggestRule as R,addProjectAllowRule as W,riskWarning as F}from"../settings.js";import{PLAN_DENY_REASON as K,SANDBOX_DENY_REASON as O,sandboxBlocked as Y,AUTOMATION_TOOLS as q,automationActionLabel as B,automationDetail as G}from"../permissions.js";import{runVerifyLoop as J}from"../autofix.js";import{runAutoFlow as N}from"../autorun.js";const Q=r.createElement,X=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 H(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}const V=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];export function App({config:r,cwd:l,session:x,extraTools:rr,mcp:tr,yolo:er,initialPrompt:nr,version:or,onExit:sr}){const cr=p(),[lr,ur]=t(l),ir=e(rr),ar=e([]),mr=e(0),pr=e(""),fr=e(x),gr=e(null),dr=e(null),hr=e(new Set),wr=e(C(lr)),xr=e(_(lr)),yr=e(!1),br=e(!1),vr=e(v()),kr=e(!!er),Tr=e(!1),$r=e("idle"),jr=e([]),Er=e({list:[]}),Pr=e(D(lr)),Sr=e(null),Mr=e(""),[,Ar]=n(r=>r+1,0),[Ir,Dr]=n(r=>r+1,0),[Lr,Cr]=t(0),[_r,Ur]=t("idle"),[Rr,Wr]=t({text:"",cursor:0}),[Fr,Kr]=t(null),[Or,Yr]=t({prompt:0,completion:0}),[qr,Br]=t(0),[Gr,Jr]=t([]),[Nr,Qr]=t(0),[Xr,zr]=t(!1),[Hr,Vr]=t(""),Zr=e(null),[rt,tt]=t(0),[et,nt]=t(null),ot=e(null),[st,ct]=t(0),[lt,ut]=t(!1),it=s(()=>E(lr),[lr]),at=(r,t)=>{ar.current.push({id:mr.current++,kind:r,text:t}),Dr()},mt=()=>{if(pr.current){const t=pr.current,e=!1!==r.markdown&&w(t);at("assistant",e?h(t):t),pr.current=""}},pt=r=>{$r.current=r?"busy":"idle",Ur($r.current)};o(()=>{if("busy"!==_r)return;const r=setInterval(()=>tt(r=>(r+1)%V.length),80);return()=>clearInterval(r)},[_r]);const ft=async()=>{if(r.apiKey)try{const t=await g(r.baseUrl,r.apiKey);ot.current=t,nt(t)}catch{}};o(()=>{ft();const r=setInterval(()=>{ft()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void Vr("");const t=()=>{try{const t=c(r.statusLine,{cwd:lr,timeout:3e3,stdio:["ignore","pipe","ignore"]}).toString().split("\n")[0].slice(0,80);Vr(t)}catch{Vr("")}};t();const e=setInterval(t,5e3);return()=>clearInterval(e)},[r.statusLine]),o(()=>()=>{Zr.current&&clearInterval(Zr.current)},[]);const gt=e(""),dt={onDelta:r=>{gt.current&&(gt.current=""),pr.current+=r,Ar()},onReasoning:r=>{gt.current=(gt.current+r).slice(-400),Ar()},onToolStart:(r,t)=>{gt.current="",mt(),at("tool",d(r,t))},onToolResult:(r,t)=>at("toolresult",function(r,t=200){const e=String(r).split("\n")[0];return e.length>t?e.slice(0,t)+"…":e}(t)),onFileDiff:r=>at("diff",r),onSkip:(r,t)=>at("skip",d(r,t)+" — 已跳过"),onInfo:r=>{mt(),at("info",r)},onTodo:r=>Jr(r),onTurnEnd:()=>{gt.current="",mt()}},ht={request:(r,t,e,n)=>{if(xr.current.length&&U(xr.current,r,t))return Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions 查看或移除)。"});if(yr.current&&!n)return Promise.resolve({allow:!1,reason:K});if(br.current&&Y(r))return Promise.resolve({allow:!1,reason:O});if(n||kr.current||hr.current.has(r)||U(wr.current,r,t))return Promise.resolve({allow:!0});const o=q.has(r);return new Promise(e=>{dr.current={name:r,args:t,resolve:e,consent:o},Kr({label:o?B(r,t):d(r,t),detail:o?G(r,t):"",name:r,args:t,consent:o,risk:F(r,t)})})},isYolo:()=>kr.current,setYolo:r=>{kr.current=!!r},setPlanMode:r=>{yr.current=!!r,zr(!!r)},isPlanMode:()=>yr.current,setSandbox:r=>{br.current=!!r},isSandbox:()=>br.current,reloadRules:()=>{wr.current=C(lr),xr.current=_(lr)}},wt=()=>{const r=jr.current.shift();return Qr(jr.current.length),null==r?null:r},xt=async t=>{const e=P(ot.current);if(e&&"exhausted"===e.level)return void at("info",`✗ Token 用量窗口(${e.label})已用尽,暂无法继续。${S(e.resetAtEpochMs)}`);const{text:n,images:o}=await M(t,lr,r),s=vr.current.snapshot({session:fr.current,label:t});fr.current.messages.push({role:"user",content:A(n,o)}),pt(!0),gr.current=new AbortController,fr.current.compaction||(fr.current.compaction={});const c=await f({config:r,messages:fr.current.messages,permissions:ht,cwd:lr,extraTools:ir.current,events:dt,signal:gr.current.signal,todoState:Er.current,compaction:fr.current.compaction,pullQueued:wt,fileJournal:s});return gr.current=null,mt(),c?.usage&&Yr(r=>({prompt:r.prompt+c.usage.prompt,completion:r.completion+c.usage.completion})),c?.lastPromptTokens&&(Br(c.lastPromptTokens),fr.current.lastPromptTokens=c.lastPromptTokens),b(fr.current),(c?.aborted||c?.error)&&jr.current.length&&(jr.current=[],Qr(0),at("info","已清空排队消息")),pt(!1),ft(),c},yt=()=>{sr?.(),cr.exit()},bt=async t=>{const e=t.trim();if(Wr({text:"",cursor:0}),Sr.current=null,e){if(L(lr,e),Pr.current[Pr.current.length-1]!==e&&Pr.current.push(e),e.startsWith("/"))return(async t=>{const e={version:or,config:r,cwd:lr,usage:Or,permissions:ht,extraTools:ir.current,session:fr.current,checkpoints:vr.current,print:r=>at("info",String(r))},n=await j(t,e);if("exit"===n.action)return yt();if("clear"===n.action&&(fr.current=y({cwd:lr,model:r.model,messages:[fr.current.messages[0]]}),Er.current.list=[],vr.current.list.length=0,Jr([]),Br(0),ar.current=[],pr.current="",Cr(r=>r+1),at("info","已开启新会话(旧会话仍可 --resume 恢复)")),"refresh"===n.action&&(Er.current.list=[],Jr([]),Br(0)),"autofix"===n.action&&await J({command:n.command,maxIters:n.maxIters,cwd:lr,runTurnOnce:r=>xt(r),emit:r=>at("info",r)}),"autorun"===n.action&&await N({goal:n.goal,config:r,permissions:ht,cwd:lr,runTurnOnce:r=>xt(r),emit:r=>at("info",r)}),"cd"===n.action){wr.current=C(n.cwd),xr.current=_(n.cwd),$(tr?.clients);const t=await k(r,n.cwd);ir.current=t.extraTools,tr&&(tr.clients=t.clients),fr.current=y({cwd:n.cwd,model:r.model,messages:[{role:"system",content:T(n.cwd,{memory:!1!==r.memoryEnabled})}]}),r.sessionId=fr.current.id,Er.current.list=[],vr.current.list.length=0,Jr([]),Br(0),ar.current=[],pr.current="",Cr(r=>r+1),ur(n.cwd),at("info","已切换到 "+n.cwd+"(新会话,旧会话已保存)")}"loop"===n.action&&(Zr.current&&(clearInterval(Zr.current),Zr.current=null),n.stop?at("info","已停止循环。"):(at("info",`已开始循环:每 ${n.seconds}s 运行「${n.prompt}」(/loop off 停止)`),Zr.current=setInterval(()=>{"busy"!==$r.current&&(at("user",n.prompt),xt(n.prompt))},1e3*n.seconds))),n.prompt&&("busy"===$r.current?(jr.current.push(n.prompt),Qr(jr.current.length)):(at("user",n.prompt),await xt(n.prompt)))})(e);if(e.startsWith("!")&&e.length>1)return(async r=>{if("busy"===$r.current)return void at("info","正在处理上一条,! 命令请稍候再试");at("user","! "+r);const{output:t,record:e}=await I(r,lr);at("toolresult",z(t)),fr.current.messages.push({role:"user",content:e}),b(fr.current)})(e.slice(1).trim());if("busy"===$r.current)return at("user",e),jr.current.push(e),void Qr(jr.current.length);at("user",e),await xt(e)}};o(()=>{!Tr.current&&nr&&(Tr.current=!0,bt(nr))},[]);const vt=/^\/(\S*)$/.exec(Rr.text),kt=vt?vt[1]:null,Tt=null==kt?[]:it.filter(r=>r.name.toLowerCase().startsWith(kt.toLowerCase())),$t=Tt.length>0&&!lt,jt=Tt.length<=X?0:Math.max(0,Math.min(st-Math.floor(5),Tt.length-X)),Et=Tt.slice(jt,jt+X);o(()=>{ct(0),ut(!1)},[kt]),m((r,t)=>{if(dr.current){const e=dr.current,n=r=>{dr.current=null,Kr(null),e.resolve(r)};if("y"===r||"1"===r)return n({allow:!0});if("a"===r||"2"===r)return hr.current.add(e.name),n({allow:!0});if("3"===r)return n({allow:!1,reason:"用户选择了「否」,该操作未执行。"});if("A"===r){hr.current.add(e.name);const r=R(e.name,e.args);try{W(lr,r),wr.current=C(lr),at("info",`已写入允许规则:${r} → .huicai/settings.json`)}catch(r){at("info",`规则写入失败(本会话仍生效):${r.message}`)}return n({allow:!0})}return"n"===r||t.escape||t.return?n({allow:!1,reason:"用户拒绝了该操作。"}):void 0}if($t){const r=()=>Tt[Math.min(st,Tt.length-1)];if(t.upArrow)return ct(r=>(r-1+Tt.length)%Tt.length);if(t.downArrow)return ct(r=>(r+1)%Tt.length);if(t.pageUp)return ct(r=>Math.max(0,r-X));if(t.pageDown)return ct(r=>Math.min(Tt.length-1,r+X));if(t.tab){const t=r().name;return Wr({text:"/"+t+" ",cursor:t.length+2})}if(t.return)return void bt("/"+r().name);if(t.escape)return ut(!0)}if(t.escape)"busy"===$r.current&&gr.current&&(gr.current.abort(),at("info","已请求中断(Esc)"));else if(t.ctrl&&"c"===r)"busy"===$r.current&&gr.current?(gr.current.abort(),at("info","已请求中断")):yt();else if(t.return)bt(Rr.text);else{if(t.upArrow)return(()=>{const r=Pr.current;if(!r.length)return;null==Sr.current?(Mr.current=Rr.text,Sr.current=r.length-1):Sr.current>0&&Sr.current--;const t=r[Sr.current]??"";Wr({text:t,cursor:t.length})})();if(t.downArrow)return(()=>{if(null==Sr.current)return;const r=Pr.current;if(Sr.current<r.length-1){Sr.current++;const t=r[Sr.current];Wr({text:t,cursor:t.length})}else Sr.current=null,Wr({text:Mr.current,cursor:Mr.current.length})})();if(t.leftArrow)return Wr(r=>({...r,cursor:Math.max(0,r.cursor-1)}));if(t.rightArrow)return Wr(r=>({...r,cursor:Math.min(r.text.length,r.cursor+1)}));if(t.ctrl&&"a"===r)return Wr(r=>({...r,cursor:0}));if(t.ctrl&&"e"===r)return Wr(r=>({...r,cursor:r.text.length}));if(t.ctrl&&"u"===r)return Wr(r=>({text:r.text.slice(r.cursor),cursor:0}));if(t.ctrl&&"k"===r)return Wr(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(t.ctrl&&"w"===r)return Wr(r=>{const t=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:t+r.text.slice(r.cursor),cursor:t.length}});if(t.backspace||t.delete)return Sr.current=null,Wr(r=>r.cursor>0?{text:r.text.slice(0,r.cursor-1)+r.text.slice(r.cursor),cursor:r.cursor-1}:r);if(r&&!t.ctrl&&!t.meta){Sr.current=null;const t=r.replace(/\r\n?/g,"\n");Wr(r=>({text:r.text.slice(0,r.cursor)+t+r.text.slice(r.cursor),cursor:r.cursor+t.length}))}}});const Pt=s(()=>ar.current.slice(),[Ir]),St=lr.split("/").filter(Boolean).pop()||lr,Mt=Rr.text.slice(0,Rr.cursor),At=Rr.text[Rr.cursor]??" ",It=Rr.text.slice(Rr.cursor+1),Dt=Math.max(8,process.stdout.columns||80),Lt=P(et),Ct=Lt?"exhausted"===Lt.level?`⛔ ${Lt.label}用量已用尽${S(Lt.resetAtEpochMs)?" · "+S(Lt.resetAtEpochMs):""}`:`⚠ ${Lt.label}用量 ${Math.round(100*Lt.pct)}%(${H(Lt.used)}/${H(Lt.limit)})`:"";return Q(u,{flexDirection:"column"},Q(a,{key:Lr,items:Pt},r=>Q(Z,{key:r.id,item:r})),gt.current?Q(u,null,Q(i,{color:"gray",italic:!0},"✦ 思考 "+gt.current.replace(/\n/g," ").slice(-160))):null,pr.current?Q(u,{marginTop:0},Q(i,null,pr.current)):null,Gr.length?Q(u,{flexDirection:"column",paddingLeft:2,marginTop:1},...Gr.map((r,t)=>Q(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"!==_r||pr.current?null:Q(u,null,Q(i,{color:"cyan"},V[rt]+" 思考中…"),Q(i,{color:"gray"},"(Esc 中断 · 输入回车可排队补充)")),Fr?Q(u,{flexDirection:"column",borderStyle:"round",borderColor:"yellow",paddingX:1,marginTop:1},Q(i,{color:"yellow",bold:!0},Fr.consent?"● 是否继续?"+Fr.label:"● 需要授权:"+Fr.label),Fr.risk?Q(i,{color:"red",bold:!0},"⚠ "+Fr.risk):null,Fr.consent&&Fr.detail?Q(i,{color:"gray"},z(Fr.detail,6)):null,"run_bash"===Fr.name?Q(i,{color:"gray"},"$ "+(Fr.args.command||"")):null,"fetch_url"===Fr.name?Q(i,{color:"gray"},"GET "+(Fr.args.url||"")):null,Q(i,null,Fr.consent?"[1] 是 · [2] 是,并允许在当前对话中后续相同操作不再询问 · [3/Esc] 否":"[y] 允许一次 · [a] 本会话 · [A] 永久(写入项目设置) · [n/Esc] 拒绝")):null,$t?Q(u,{flexDirection:"column",marginTop:1},Q(i,{color:"gray"},Tt.length>X?` 斜杠命令 ${jt+1}-${jt+Et.length}/${Tt.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · Tab 补全 · ⏎ 执行 · Esc 关闭`:" 斜杠命令 · ↑↓ 选择 · Tab 补全 · ⏎ 执行 · Esc 关闭"),...Et.map((r,t)=>{const e=jt+t;return Q(u,{key:r.name},Q(i,{color:"cyan",bold:e===st},(e===st?" ❯ ":" ")+"/"+r.name+(r.args?" "+r.args:"")),Q(i,{color:"gray"}," "+String(r.desc||"").replace(/\s+/g," ").slice(0,46)))})):null,Q(u,{marginTop:$t?0:1},Q(i,{color:"blue"},"─".repeat(Dt))),Q(u,null,Q(i,{color:"busy"===_r?"yellow":"cyan"},"busy"===_r?"⧗ ":"› "),Q(i,null,Mt),Q(i,{inverse:!0},At),Q(i,null,It)),Q(i,{color:"blue"},"─".repeat(Dt)),Q(u,{justifyContent:"space-between"},Q(i,{color:"gray"},(Hr?Hr+" · ":"")+`${St} · ${r.model}${kr.current?" · ⚠ yolo":""}${Xr?" · 📋 计划":""}`+(Nr?` · 排队 ${Nr}`:"")+" · /help"),Ct?Q(i,{color:"exhausted"===Lt.level?"red":"yellow",bold:!0},Ct):Q(i,null,"")))}function Z({item:r}){const{kind:t,text:e}=r;if("user"===t)return Q(u,{marginTop:1},Q(i,{color:"cyan",bold:!0},"› "),Q(i,{bold:!0},e));if("assistant"===t)return Q(u,null,Q(i,null,e));if("diff"===t)return Q(u,{marginLeft:2},Q(i,null,e));if("tool"===t)return Q(i,{color:"cyan"}," ⏺ "+e);if("toolresult"===t)return Q(i,{color:"gray"}," "+e);if("skip"===t)return Q(i,{color:"gray"}," ⊘ "+e);if("info"===t){const r=/✗|失败|错误|error/i.test(e);return Q(i,{color:r?"red":"yellow"},(r?" ✗ ":" · ")+e.replace(/^✗\s*/,""))}return Q(i,null,e)}export async function runInk({config:r,cwd:t,session:e,yolo:n,initialPrompt:o,version:s}){process.stdout.write(x({model:r.model,cwd:t,version:s,platform:/^(agent|workflow|datahuman)-/.test(String(r.model||"")),yolo:n,consult:r.consult?r.consultLabel||r.consult:null,ink:!0})+"\n");const{extraTools:c,clients:u}=await k(r,t),i={clients:u},{waitUntilExit:a}=l(Q(App,{config:r,cwd:t,session:e,extraTools:c,mcp:i,yolo:n,initialPrompt:o,version:s,onExit:null}));await a(),$(i.clients)}
|
package/src/uninstall.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import t from"node:fs";import e from"node:path";import{CONFIG_DIR as r}from"./config.js";function i(r){let n=0,o=[];try{o=t.readdirSync(r,{withFileTypes:!0})}catch{return 0}for(const s of o){const o=e.join(r,s.name);try{n+=s.isDirectory()?i(o):t.statSync(o).size}catch{}}return n}export function describeData(){const n=r;if(!t.existsSync(n))return{exists:!1,dir:n,entries:[],total:0};const o=[];let s=0;for(const r of t.readdirSync(n,{withFileTypes:!0})){const c=e.join(n,r.name);let a=0;try{a=r.isDirectory()?i(c):t.statSync(c).size}catch{}s+=a,o.push({name:r.name,kind:r.isDirectory()?"dir":"file",size:a})}return o.sort((t,e)=>t.name.localeCompare(e.name)),{exists:!0,dir:n,entries:o,total:s}}export function purgeData(){const e=r;return t.existsSync(e)?(t.rmSync(e,{recursive:!0,force:!0}),{removed:!0,dir:e}):{removed:!1,dir:e}}export function fmtSize(t){return t<1024?t+"B":t<1048576?(t/1024).toFixed(1)+"KB":(t/1024/1024).toFixed(1)+"MB"}
|