@huicai/cli 2026.1019.0 → 2026.1022.59980

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.
@@ -0,0 +1 @@
1
+ import e from"node:fs";import t from"node:path";import r from"node:os";import o from"node:crypto";import{CONFIG_DIR as n}from"./config.js";import{accountSnapshot as i,withAccountOperation as a}from"./account-scope.js";import{readScheduleState as s,editScheduleState as c,scheduleFile as f}from"./schedule.js";export function automationDeviceId(){const r=t.join(n,"automation-device-id");e.mkdirSync(n,{recursive:!0});try{e.writeFileSync(r,o.randomUUID(),{flag:"wx",mode:384})}catch(e){if("EEXIST"!==e.code)throw e}const i=e.readFileSync(r,"utf8").trim();if(!/^[\w-]{8,80}$/.test(i))throw new Error("自动化终端标识损坏");return i}const d=e=>Array.isArray(e)?e.map(d):e&&"object"==typeof e?Object.fromEntries(Object.keys(e).sort().map(t=>[t,d(e[t])])):e,w=new Map;export async function syncAutomations({fetchFn:t=fetch}={}){const o=i();if(!o.scope)throw new Error("请先登录并完成用户身份校验");const n=o.scope+":"+o.config.apiKey;if(w.has(n))return w.get(n);const a=async function(t,o){const n=f(),i=automationDeviceId(),a=()=>{if(!t.current())throw new Error("登录状态已改变,请重试")},w=async e=>{a();const r=await o(t.config.baseUrl+"/v1/automations?deviceId="+encodeURIComponent(i),{method:e?"POST":"GET",headers:{Authorization:"Bearer "+t.config.apiKey,"Content-Type":"application/json"},...e?{body:JSON.stringify(e)}:{},signal:AbortSignal.timeout(15e3)}),n=await r.json();if(a(),!r.ok||200!==n.code)throw new Error(n.message||"自动化同步失败,请重试");if(!Array.isArray(n.data?.tasks)||!Number.isSafeInteger(n.data.revision))throw new Error("自动化同步响应格式错误");return n.data};let u=await w();a();const m=s(n);if(m.dirty){const t=(e=>e.map(({owner:e,user:t,...r})=>r))(m.tasks);if(l=t,h=u.tasks,JSON.stringify(d(l))!==JSON.stringify(d(h))){if(u.revision!==m.revision){const t=n+".conflict-"+Date.now()+".json";throw c(r=>{e.writeFileSync(t,JSON.stringify(r,null,2),{mode:384}),Object.assign(r,{tasks:y(u.tasks),revision:u.revision,dirty:!1,generation:r.generation+1})},n),new Error("控制台已更新任务,已采用云端版本。未同步的本地修改保存在:"+t)}u=await w({deviceId:i,deviceName:r.hostname(),revision:u.revision,tasks:t})}}var l,h;return a(),c(e=>{const t=e.generation!==m.generation;e.revision=Math.max(e.revision,u.revision),t||(e.tasks=y(u.tasks),e.dirty=!1)},n),s(n).tasks;function y(e){const r=t.config.user;return e.map(e=>({...e,owner:t.scope,user:String(r.id??r.userId??r.username)}))}}(o,t).finally(()=>w.delete(n));return w.set(n,a),a}export async function runClaimedAutomation(e,t,{manual:r=!1,slot:o=Math.floor(Date.now()/6e4),signal:n,fetchFn:d=fetch,heartbeatMs:w=3e4}={}){const u=i();if(!u.current()||e?.owner!==u.scope)throw new Error("任务不属于当前登录用户");const m=()=>{if(!u.current())throw new Error("登录状态已改变")},l=automationDeviceId(),h=async(t,r)=>{m();const o=await d(u.config.baseUrl+"/v1/automations/"+t,{method:"POST",headers:{Authorization:"Bearer "+u.config.apiKey,"Content-Type":"application/json"},body:JSON.stringify({deviceId:l,taskId:e.id,...r}),signal:AbortSignal.timeout(15e3)}),n=await o.json();if(m(),!o.ok||200!==n.code)throw new Error(n.message||"自动化领取失败");return n.data};if(await syncAutomations({fetchFn:d}),m(),n?.aborted)throw new Error("任务已取消");const y=f(),g=s(y);if(g.dirty)throw new Error("任务有尚未同步的修改,请重试");const p=g.tasks.find(t=>t.id===e.id);if(!p||!1===p.enabled)throw new Error("任务已删除或暂停");if(!r){const{isDue:e}=await import("./schedule-freq.js");if(!e(p,new Date(6e4*o)))throw new Error("任务尚未到执行时间")}const v=await h("claim",{revision:g.revision,manual:r,slot:o}),k=v?.token;if(!k||!v.document||v.task?.id!==e.id)throw new Error("领取响应格式错误");const b=new AbortController,E=()=>b.abort(new Error("任务已取消或执行租约失效"));n?.addEventListener("abort",E,{once:!0});let I,S,j,A=!1,D=Date.now()+12e4;try{if(m(),c(e=>{if(e.generation!==g.generation)throw new Error("领取期间任务已修改,请重试");const t=String(u.config.user.id??u.config.user.userId??u.config.user.username);e.tasks=v.document.tasks.map(e=>({...e,owner:u.scope,user:t})),e.revision=v.document.revision,e.dirty=!1,e.generation++},y),n?.aborted&&E(),S=setInterval(()=>{(!u.current()||Date.now()>=D)&&E()},100),I=setInterval(async()=>{if(!A&&!b.signal.aborted){A=!0;try{await h("heartbeat",{token:k})?D=Date.now()+12e4:E()}catch{E()}finally{A=!1}}},w),b.signal.aborted)throw b.signal.reason;const e={...v.task,owner:u.scope,user:p.user};return j=await a(u,()=>(m(),t(e,b.signal)))||{ok:!0},b.signal.aborted&&(j={...j,ok:!1,error:"任务已取消或执行租约失效"}),j}catch(e){throw j={ok:!1,error:e.message},e}finally{if(clearInterval(I),clearInterval(S),n?.removeEventListener("abort",E),u.current()){if(!await h("complete",{token:k,ok:!0===j?.ok,output:j?.output||j?.text||j?.error||""})&&j?.ok)throw new Error("运行已结束,但租约失效,结果未写入控制台")}}}
package/src/codeindex.js CHANGED
@@ -1 +1 @@
1
- import e from"node:fs";import t from"node:path";import n from"node:zlib";import s from"node:crypto";import{execFileSync as r}from"node:child_process";import{CONFIG_DIR as o}from"./config.js";import{embed as c}from"./api.js";export const IGNORE=new Set([".git","node_modules","dist","build","target","out",".next",".nuxt",".venv","venv","__pycache__",".idea",".gradle",".mvn","coverage",".cache","vendor","bower_components","Pods","DerivedData",".turbo",".parcel-cache",".terraform",".pytest_cache",".mypy_cache",".tox",".angular",".svelte-kit",".output",".serverless",".svn",".hg"]);const i=new Set([".js",".mjs",".cjs",".ts",".tsx",".jsx",".java",".kt",".py",".go",".rs",".rb",".php",".c",".h",".cpp",".hpp",".cs",".swift",".scala",".sql",".vue",".svelte",".sh",".css",".scss",".html",".md",".yaml",".yml",".json",".toml",".xml",".properties",".gradle",".groovy",".proto",".graphql",".gql",".tf",".ini",".cfg",".conf",".dart",".lua",".clj",".ex",".exs",".erl",".hs",".jl",".pl",".pm",".r",".dockerfile"]),a=/(?:\.min\.(?:js|css)$)|(?:\.bundle\.js$)|(?:\.map$)|(?:(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|npm-shrinkwrap\.json)$)/i;export const MAX_FILE_BYTES=524288;const l=t.join(o,"cache","index"),u=new Map;export function tokenize(e){const t=String(e),n=t.replace(/([a-z0-9])([A-Z])/g,"$1 $2").toLowerCase().split(/[^a-z0-9_]+/).filter(e=>e.length>=2&&e.length<=40),s=t.match(/[一-鿿]/g);if(s)for(const e of s)n.push(e);const r=t.match(/[一-鿿]{2,}/g);if(r)for(const e of r)for(let t=0;t<e.length-1;t++)n.push(e.slice(t,t+2));return n}function*f(n,s=0,r=14){let o;try{o=e.readdirSync(n,{withFileTypes:!0})}catch{return}for(const e of o){if(IGNORE.has(e.name)||e.name.startsWith("."))continue;const o=t.join(n,e.name);e.isDirectory()?s<r&&(yield*f(o,s+1,r)):e.isFile()&&i.has(t.extname(e.name).toLowerCase())&&(yield o)}}let p=null;export function ignoreGlobs(){return[...IGNORE].flatMap(e=>["-g",`!**/${e}/**`,"-g",`!${e}/**`])}export function listSourceFiles(e){let n=null;if(function(){if(null!==p)return p;try{r("rg",["--version"],{stdio:"ignore"}),p=!0}catch{p=!1}return p}())try{n=r("rg",["--files",...ignoreGlobs()],{cwd:e,encoding:"utf8",maxBuffer:64<<20,stdio:["ignore","pipe","ignore"]}).split("\n").map(e=>e.replace(/\\/g,"/")).filter(Boolean)}catch{n=null}if(!n){n=[];for(const s of f(e))n.push(t.relative(e,s).split(t.sep).join("/"))}const s=[];for(const r of n)i.has(t.extname(r).toLowerCase())&&(a.test(r)||s.push({rel:r,abs:t.join(e,r)}));return s}const h=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".java",".kt",".py",".go",".rs",".rb",".php",".c",".h",".cpp",".hpp",".cs",".swift",".scala",".vue",".svelte",".dart",".lua",".clj",".ex",".exs",".erl",".hs",".jl",".groovy"]),d=/(?:^|\/)(?:tests?|__tests__|spec|specs|fixtures?|mocks?|__mocks__|generated|gen|snapshots?|migrations?|node_modules|vendor)(?:\/|$)/i;export function indexPriority(e){const n=e.toLowerCase();let s=0;return h.has(t.extname(n))?s-=100:s+=25,d.test(n)&&(s+=60),s+=2*(n.split("/").length-1),s}const m=new Set([".js",".mjs",".cjs",".ts",".tsx",".jsx",".java",".kt",".go",".rs",".c",".h",".cpp",".hpp",".cc",".cs",".swift",".scala",".php",".dart",".groovy"]);const g=/^(?:(?:export|default|public|private|protected|internal|static|final|abstract|sealed|open|override|async|suspend|inline|const|unsafe|extern|virtual|partial|pub|data|companion)\s+)*(?:function|func|fn|def|fun|class|interface|enum|record|struct|trait|impl|module|mod|namespace|protocol|extension|object|actor|type)\b(?:\s+([A-Za-z_]\w*))?/,y=/^(?:@[\w.]+(?:\([^)]*\))?\s+)*(?:(?:public|private|protected|internal|static|final|abstract|synchronized|native|override|open|suspend|inline|virtual|sealed|async|unsafe|extern)\s+)+[\w<>\[\],.?&:*\s]*?\b([A-Za-z_]\w*)\s*\([^;{]*\)[^;]*?(?:\{|$)/,x=/^(?!(?:if|for|while|switch|catch|return|do|else|try|with|function|class|new|await|yield|throw|case|default|synchronized)\b)(?:static\s+|async\s+|get\s+|set\s+|readonly\s+|public\s+|private\s+|protected\s+|\*\s*)*([A-Za-z_$][\w$]*)\s*\([^;]*\)\s*(?::\s*[\w<>\[\].|&, ]+)?\s*\{/,w=/^(?:export\s+)?(?:default\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\b|\([^)]*\)\s*(?::\s*[\w<>\[\].|&, ]+)?\s*=>|[A-Za-z_$][\w$]*\s*=>)/,b=/^(?:async\s+)?(?:def|class)\s+([A-Za-z_]\w*)/,z=/^(?:def|class|module)\s+([A-Za-z_][\w:]*)/;function j(e,t){if("py"===e){const e=b.exec(t);return e?{name:e[1]}:null}if("ruby"===e){const e=z.exec(t);return e?{name:e[1]}:null}let n=g.exec(t);return n?{name:n[1]||""}:(n=y.exec(t),n?{name:n[1]}:(n=x.exec(t),n?{name:n[1]}:(n=w.exec(t),n?{name:n[1]}:null)))}function v(e,t=0){const n=[];for(let s=0;s<e.length;s+=40){const r=Math.min(e.length,s+50),o=e.slice(s,r).join("\n"),c=tokenize(o);if(c.length){const e=new Map;for(const t of c)e.set(t,(e.get(t)||0)+1);n.push({start:t+s+1,end:t+r,len:c.length,text:o,tf:[...e]})}if(r>=e.length)break}return n}function k(e,t){const n=function(e){return m.has(e)?"brace":".py"===e?"py":".rb"===e?"ruby":null}(e);if(!n)return null;const s=[];for(let e=0;e<t.length;e++){const r=t[e].trim();if(!r)continue;const o=j(n,r);o&&s.push({line:e,sym:o.name||""})}if(!s.length)return null;for(let e=0;e<s.length;e++){let n=s[e].line;const r=e>0?s[e-1].line+1:0;for(;n>r&&/^@[\w.]/.test(t[n-1].trim());)n--;s[e].start=n}const r=[];s[0].start>0&&r.push({start:0,sym:""});for(const e of s)r.push({start:e.start,sym:e.sym});const o=[];for(let e=0;e<r.length;e++){const n=r[e].start,s=e+1<r.length?r[e+1].start:t.length;if(s<=n)continue;const c=t.slice(n,s);if(c.length>160){const t=v(c,n);t.length&&r[e].sym&&(t[0].sym=r[e].sym),o.push(...t);continue}const i=c.join("\n"),a=tokenize(i);if(!a.length)continue;const l=new Map;for(const e of a)l.set(e,(l.get(e)||0)+1);const u={start:n+1,end:s,len:a.length,text:i,tf:[...l]};r[e].sym&&(u.sym=r[e].sym),o.push(u)}return o.length?o:null}export function chunkText(e,n=""){const s=e.split("\n"),r=n?t.extname(n).toLowerCase():"";return r&&k(r,s)||v(s,0)}export function buildIndexCore(t,n){const s=Date.now()+2500,r=listSourceFiles(t).map(t=>{let n=1/0,s=0;try{const r=e.statSync(t.abs);n=r.size,s=r.mtimeMs}catch{}return{...t,size:n,mtimeMs:s,prio:indexPriority(t.rel)}}).filter(e=>e.size<=524288).sort((e,t)=>e.prio-t.prio||e.size-t.size),o=new Set(r.map(e=>e.rel)),c=[],i=new Map,a={};let l=0,u=0,f=0,p=0,h=!1;for(const t of r){if(c.length>=12e3||Date.now()>s){h=!0;break}const r=n&&n[t.rel];let o;if(r&&r.mtimeMs===t.mtimeMs&&r.size===t.size&&Array.isArray(r.chunks))o=r.chunks,f++;else{let n;try{const s=e.readFileSync(t.abs);if(s.includes(0))continue;n=s.toString("utf8")}catch{continue}o=chunkText(n,t.rel),p++}u++,a[t.rel]={mtimeMs:t.mtimeMs,size:t.size,chunks:o};const d=new Set(tokenize(t.rel));for(const e of o){const n=new Map(e.tf);for(const[t]of e.tf)i.set(t,(i.get(t)||0)+1);c.push({file:t.rel,start:e.start,end:e.end,len:e.len,text:e.text,tf:n,pathTokens:d,sym:e.sym}),l+=e.len}}if(h&&n)for(const e in n)!a[e]&&o.has(e)&&(a[e]=n[e]);const d=n?Object.keys(n).length:-1,m=p>0||Object.keys(a).length!==d;return{index:{chunks:c,df:i,N:c.length,avgLen:c.length?l/c.length:1,truncated:h,indexedFiles:u,totalFiles:r.length,reused:f,rechunked:p},filesMap:a,changed:m}}export function buildIndex(e){return buildIndexCore(e,null).index}function _(e){const n=s.createHash("sha1").update(t.resolve(e)).digest("hex").slice(0,16);return t.join(l,n+".json.gz")}function S(s,r){try{e.mkdirSync(l,{recursive:!0});const o=_(s),c=n.gzipSync(JSON.stringify({v:2,cwd:t.resolve(s),at:Date.now(),files:r}));e.writeFileSync(o+".tmp",c),e.renameSync(o+".tmp",o),function(){try{const n=e.readdirSync(l).filter(e=>e.endsWith(".json.gz"));if(n.length<=24)return;const s=n.map(n=>{try{return{f:n,t:e.statSync(t.join(l,n)).mtimeMs}}catch{return null}}).filter(Boolean).sort((e,t)=>e.t-t.t);for(const{f:n}of s.slice(0,s.length-24))try{e.unlinkSync(t.join(l,n))}catch{}}catch{}}()}catch{}}export function getIndex(s){const r=u.get(s);if(r&&Date.now()-r.builtAt<3e5)return r.index;const{index:o,filesMap:c,changed:i}=buildIndexCore(s,function(s){try{const r=_(s),o=e.statSync(r);if(Date.now()-o.mtimeMs>12096e5)return null;const c=JSON.parse(n.gunzipSync(e.readFileSync(r)).toString("utf8"));if(!c||2!==c.v||c.cwd!==t.resolve(s)||!c.files)return null;const i=new Date;try{e.utimesSync(r,i,i)}catch{}return c.files}catch{return null}}(s));return i&&S(s,c),u.set(s,{index:o,builtAt:Date.now()}),o}export function invalidate(e){u.delete(e)}export function clearIndexCache(){u.clear();try{e.rmSync(l,{recursive:!0,force:!0})}catch{}}export function search(e,t,n=8){const s=[...new Set(tokenize(t))];if(!s.length||!e.N)return[];const{chunks:r,df:o,N:c,avgLen:i}=e,a=new Map;for(const e of s){const t=o.get(e)||1;a.set(e,Math.log(1+(c-t+.5)/(t+.5)))}const l=[];for(const e of r){let t=0;for(const n of s){const s=e.tf.get(n);s&&(t+=a.get(n)*(2.5*s/(s+1.5*(.25+e.len/i*.75))))}if(t>0){if(e.pathTokens)for(const n of s)e.pathTokens.has(n)&&(t+=.6*a.get(n));l.push({file:e.file,start:e.start,end:e.end,text:e.text,score:t,sym:e.sym})}}return l.sort((e,t)=>t.score-e.score),l.slice(0,n)}export function cosine(e,t){if(!e||!t)return 0;let n=0,s=0,r=0;const o=Math.min(e.length,t.length);for(let c=0;c<o;c++)n+=e[c]*t[c],s+=e[c]*e[c],r+=t[c]*t[c];return s&&r?n/(Math.sqrt(s)*Math.sqrt(r)):0}export async function searchCode(e,t,n){const s=String(t?.query||"").trim();if(!s)return"缺少query(要检索的概念/功能描述)";const r=Number.isFinite(t?.k)&&t.k>0?Math.min(t.k,20):8,o=getIndex(e);if(!o.N)return"(当前目录未索引到代码文件)";const i=!!(n&&n.embeddingModel&&n.baseUrl&&n.apiKey);let a=search(o,s,i?Math.min(30,Math.max(3*r,15)):r);if(!a.length)return`(未找到与「${s}」明显相关的代码;可换关键词,或用grep精确匹配)`;let l=!1;if(i&&a.length>1)try{const e=a.map(e=>e.text.slice(0,2e3)),t=await c(n.baseUrl,n.apiKey,n.embeddingModel,[s,...e]);if(t&&t.length===e.length+1&&t[0]){const e=t[0];a=a.map((n,s)=>({...n,score:cosine(e,t[s+1])})).sort((e,t)=>t.score-e.score),l=!0}}catch{}let u=l?"(向量语义重排)\n":"";return o.truncated&&(u+=`(大型仓库:已索引最相关的约 ${o.indexedFiles}/${o.totalFiles} 个文件;如需穷尽匹配请配合grep)\n`),u+a.slice(0,r).map(e=>{const t=e.text.length>1200?e.text.slice(0,1200)+"\n…(本块过长已截断)":e.text;return`${`${e.file}:${e.start}-${e.end}${e.sym?` ▸ ${e.sym}`:""}`}\n${t}`}).join("\n\n─────\n\n")}function M(e){const t=new Set,n=[/(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_]\w*)/g,/\bclass\s+([A-Za-z_]\w*)/g,/\bdef\s+([A-Za-z_]\w*)/g,/\b(?:export\s+)?const\s+([A-Za-z_]\w*)\s*=\s*(?:async\s*)?(?:function|\([^)]*\)\s*=>)/g,/@(?:Get|Post|Put|Delete|Request)Mapping\(\s*"([^"]+)"/g,/\bpublic\s+[\w<>[\]]+\s+([A-Za-z_]\w*)\s*\(/g,/\b(?:export\s+)?(?:interface|type|enum)\s+([A-Za-z_]\w*)/g,/\b(?:func|fn|fun)\s+([A-Za-z_]\w*)/g,/\b(?:struct|trait|impl)\s+([A-Za-z_]\w*)/g];for(const s of n){let n;for(;(n=s.exec(e))&&t.size<12;)t.add(n[1])}return[...t]}export function buildRepoMap(t,{maxFiles:n=40,maxChars:s=2500}={}){const r=[];for(const n of listSourceFiles(t))try{const t=e.statSync(n.abs);if(t.size>524288)continue;r.push({abs:n.abs,size:t.size,rel:n.rel,prio:indexPriority(n.rel)})}catch{}if(!r.length)return"";r.sort((e,t)=>e.prio-t.prio||t.size-e.size);const o=[];let c=0;for(const t of r.slice(0,n)){let n=[];try{n=M(e.readFileSync(t.abs,"utf8").slice(0,4e4))}catch{}const r=`- ${t.rel}${n.length?":"+n.slice(0,8).join(", "):""}`;if(c+r.length>s)break;o.push(r),c+=r.length+1}return o.join("\n")}
1
+ import e from"node:fs";import t from"node:path";import n from"node:zlib";import s from"node:crypto";import{execFileSync as r}from"node:child_process";import{accountDataDir as o}from"./account-scope.js";import{embed as c}from"./api.js";export const IGNORE=new Set([".git","node_modules","dist","build","target","out",".next",".nuxt",".venv","venv","__pycache__",".idea",".gradle",".mvn","coverage",".cache","vendor","bower_components","Pods","DerivedData",".turbo",".parcel-cache",".terraform",".pytest_cache",".mypy_cache",".tox",".angular",".svelte-kit",".output",".serverless",".svn",".hg"]);const i=new Set([".js",".mjs",".cjs",".ts",".tsx",".jsx",".java",".kt",".py",".go",".rs",".rb",".php",".c",".h",".cpp",".hpp",".cs",".swift",".scala",".sql",".vue",".svelte",".sh",".css",".scss",".html",".md",".yaml",".yml",".json",".toml",".xml",".properties",".gradle",".groovy",".proto",".graphql",".gql",".tf",".ini",".cfg",".conf",".dart",".lua",".clj",".ex",".exs",".erl",".hs",".jl",".pl",".pm",".r",".dockerfile"]),a=/(?:\.min\.(?:js|css)$)|(?:\.bundle\.js$)|(?:\.map$)|(?:(?:^|\/)(?:package-lock\.json|pnpm-lock\.yaml|npm-shrinkwrap\.json)$)/i;export const MAX_FILE_BYTES=524288;const l=()=>t.join(o(),"cache","index"),u=new Map;export function tokenize(e){const t=String(e),n=t.replace(/([a-z0-9])([A-Z])/g,"$1 $2").toLowerCase().split(/[^a-z0-9_]+/).filter(e=>e.length>=2&&e.length<=40),s=t.match(/[一-鿿]/g);if(s)for(const e of s)n.push(e);const r=t.match(/[一-鿿]{2,}/g);if(r)for(const e of r)for(let t=0;t<e.length-1;t++)n.push(e.slice(t,t+2));return n}function*f(n,s=0,r=14){let o;try{o=e.readdirSync(n,{withFileTypes:!0})}catch{return}for(const e of o){if(IGNORE.has(e.name)||e.name.startsWith("."))continue;const o=t.join(n,e.name);e.isDirectory()?s<r&&(yield*f(o,s+1,r)):e.isFile()&&i.has(t.extname(e.name).toLowerCase())&&(yield o)}}let p=null;export function ignoreGlobs(){return[...IGNORE].flatMap(e=>["-g",`!**/${e}/**`,"-g",`!${e}/**`])}export function listSourceFiles(e){let n=null;if(function(){if(null!==p)return p;try{r("rg",["--version"],{stdio:"ignore"}),p=!0}catch{p=!1}return p}())try{n=r("rg",["--files",...ignoreGlobs()],{cwd:e,encoding:"utf8",maxBuffer:64<<20,stdio:["ignore","pipe","ignore"]}).split("\n").map(e=>e.replace(/\\/g,"/")).filter(Boolean)}catch{n=null}if(!n){n=[];for(const s of f(e))n.push(t.relative(e,s).split(t.sep).join("/"))}const s=[];for(const r of n)i.has(t.extname(r).toLowerCase())&&(a.test(r)||s.push({rel:r,abs:t.join(e,r)}));return s}const h=new Set([".ts",".tsx",".js",".jsx",".mjs",".cjs",".java",".kt",".py",".go",".rs",".rb",".php",".c",".h",".cpp",".hpp",".cs",".swift",".scala",".vue",".svelte",".dart",".lua",".clj",".ex",".exs",".erl",".hs",".jl",".groovy"]),d=/(?:^|\/)(?:tests?|__tests__|spec|specs|fixtures?|mocks?|__mocks__|generated|gen|snapshots?|migrations?|node_modules|vendor)(?:\/|$)/i;export function indexPriority(e){const n=e.toLowerCase();let s=0;return h.has(t.extname(n))?s-=100:s+=25,d.test(n)&&(s+=60),s+=2*(n.split("/").length-1),s}const m=new Set([".js",".mjs",".cjs",".ts",".tsx",".jsx",".java",".kt",".go",".rs",".c",".h",".cpp",".hpp",".cc",".cs",".swift",".scala",".php",".dart",".groovy"]);const g=/^(?:(?:export|default|public|private|protected|internal|static|final|abstract|sealed|open|override|async|suspend|inline|const|unsafe|extern|virtual|partial|pub|data|companion)\s+)*(?:function|func|fn|def|fun|class|interface|enum|record|struct|trait|impl|module|mod|namespace|protocol|extension|object|actor|type)\b(?:\s+([A-Za-z_]\w*))?/,y=/^(?:@[\w.]+(?:\([^)]*\))?\s+)*(?:(?:public|private|protected|internal|static|final|abstract|synchronized|native|override|open|suspend|inline|virtual|sealed|async|unsafe|extern)\s+)+[\w<>\[\],.?&:*\s]*?\b([A-Za-z_]\w*)\s*\([^;{]*\)[^;]*?(?:\{|$)/,x=/^(?!(?:if|for|while|switch|catch|return|do|else|try|with|function|class|new|await|yield|throw|case|default|synchronized)\b)(?:static\s+|async\s+|get\s+|set\s+|readonly\s+|public\s+|private\s+|protected\s+|\*\s*)*([A-Za-z_$][\w$]*)\s*\([^;]*\)\s*(?::\s*[\w<>\[\].|&, ]+)?\s*\{/,w=/^(?:export\s+)?(?:default\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*(?:async\s+)?(?:function\b|\([^)]*\)\s*(?::\s*[\w<>\[\].|&, ]+)?\s*=>|[A-Za-z_$][\w$]*\s*=>)/,b=/^(?:async\s+)?(?:def|class)\s+([A-Za-z_]\w*)/,z=/^(?:def|class|module)\s+([A-Za-z_][\w:]*)/;function j(e,t){if("py"===e){const e=b.exec(t);return e?{name:e[1]}:null}if("ruby"===e){const e=z.exec(t);return e?{name:e[1]}:null}let n=g.exec(t);return n?{name:n[1]||""}:(n=y.exec(t),n?{name:n[1]}:(n=x.exec(t),n?{name:n[1]}:(n=w.exec(t),n?{name:n[1]}:null)))}function v(e,t=0){const n=[];for(let s=0;s<e.length;s+=40){const r=Math.min(e.length,s+50),o=e.slice(s,r).join("\n"),c=tokenize(o);if(c.length){const e=new Map;for(const t of c)e.set(t,(e.get(t)||0)+1);n.push({start:t+s+1,end:t+r,len:c.length,text:o,tf:[...e]})}if(r>=e.length)break}return n}function k(e,t){const n=function(e){return m.has(e)?"brace":".py"===e?"py":".rb"===e?"ruby":null}(e);if(!n)return null;const s=[];for(let e=0;e<t.length;e++){const r=t[e].trim();if(!r)continue;const o=j(n,r);o&&s.push({line:e,sym:o.name||""})}if(!s.length)return null;for(let e=0;e<s.length;e++){let n=s[e].line;const r=e>0?s[e-1].line+1:0;for(;n>r&&/^@[\w.]/.test(t[n-1].trim());)n--;s[e].start=n}const r=[];s[0].start>0&&r.push({start:0,sym:""});for(const e of s)r.push({start:e.start,sym:e.sym});const o=[];for(let e=0;e<r.length;e++){const n=r[e].start,s=e+1<r.length?r[e+1].start:t.length;if(s<=n)continue;const c=t.slice(n,s);if(c.length>160){const t=v(c,n);t.length&&r[e].sym&&(t[0].sym=r[e].sym),o.push(...t);continue}const i=c.join("\n"),a=tokenize(i);if(!a.length)continue;const l=new Map;for(const e of a)l.set(e,(l.get(e)||0)+1);const u={start:n+1,end:s,len:a.length,text:i,tf:[...l]};r[e].sym&&(u.sym=r[e].sym),o.push(u)}return o.length?o:null}export function chunkText(e,n=""){const s=e.split("\n"),r=n?t.extname(n).toLowerCase():"";return r&&k(r,s)||v(s,0)}export function buildIndexCore(t,n){const s=Date.now()+2500,r=listSourceFiles(t).map(t=>{let n=1/0,s=0;try{const r=e.statSync(t.abs);n=r.size,s=r.mtimeMs}catch{}return{...t,size:n,mtimeMs:s,prio:indexPriority(t.rel)}}).filter(e=>e.size<=524288).sort((e,t)=>e.prio-t.prio||e.size-t.size),o=new Set(r.map(e=>e.rel)),c=[],i=new Map,a={};let l=0,u=0,f=0,p=0,h=!1;for(const t of r){if(c.length>=12e3||Date.now()>s){h=!0;break}const r=n&&n[t.rel];let o;if(r&&r.mtimeMs===t.mtimeMs&&r.size===t.size&&Array.isArray(r.chunks))o=r.chunks,f++;else{let n;try{const s=e.readFileSync(t.abs);if(s.includes(0))continue;n=s.toString("utf8")}catch{continue}o=chunkText(n,t.rel),p++}u++,a[t.rel]={mtimeMs:t.mtimeMs,size:t.size,chunks:o};const d=new Set(tokenize(t.rel));for(const e of o){const n=new Map(e.tf);for(const[t]of e.tf)i.set(t,(i.get(t)||0)+1);c.push({file:t.rel,start:e.start,end:e.end,len:e.len,text:e.text,tf:n,pathTokens:d,sym:e.sym}),l+=e.len}}if(h&&n)for(const e in n)!a[e]&&o.has(e)&&(a[e]=n[e]);const d=n?Object.keys(n).length:-1,m=p>0||Object.keys(a).length!==d;return{index:{chunks:c,df:i,N:c.length,avgLen:c.length?l/c.length:1,truncated:h,indexedFiles:u,totalFiles:r.length,reused:f,rechunked:p},filesMap:a,changed:m}}export function buildIndex(e){return buildIndexCore(e,null).index}function _(e){const n=s.createHash("sha1").update(t.resolve(e)).digest("hex").slice(0,16);return t.join(l(),n+".json.gz")}function S(s,r){try{e.mkdirSync(l(),{recursive:!0});const o=_(s),c=n.gzipSync(JSON.stringify({v:2,cwd:t.resolve(s),at:Date.now(),files:r}));e.writeFileSync(o+".tmp",c),e.renameSync(o+".tmp",o),function(){try{const n=e.readdirSync(l()).filter(e=>e.endsWith(".json.gz"));if(n.length<=24)return;const s=n.map(n=>{try{return{f:n,t:e.statSync(t.join(l(),n)).mtimeMs}}catch{return null}}).filter(Boolean).sort((e,t)=>e.t-t.t);for(const{f:n}of s.slice(0,s.length-24))try{e.unlinkSync(t.join(l(),n))}catch{}}catch{}}()}catch{}}export function getIndex(s){const r=u.get(o()+":"+s);if(r&&Date.now()-r.builtAt<3e5)return r.index;const{index:c,filesMap:i,changed:a}=buildIndexCore(s,function(s){try{const r=_(s),o=e.statSync(r);if(Date.now()-o.mtimeMs>12096e5)return null;const c=JSON.parse(n.gunzipSync(e.readFileSync(r)).toString("utf8"));if(!c||2!==c.v||c.cwd!==t.resolve(s)||!c.files)return null;const i=new Date;try{e.utimesSync(r,i,i)}catch{}return c.files}catch{return null}}(s));return a&&S(s,i),u.set(o()+":"+s,{index:c,builtAt:Date.now()}),c}export function invalidate(e){u.delete(o()+":"+e)}export function clearIndexCache(){u.clear();try{e.rmSync(l(),{recursive:!0,force:!0})}catch{}}export function search(e,t,n=8){const s=[...new Set(tokenize(t))];if(!s.length||!e.N)return[];const{chunks:r,df:o,N:c,avgLen:i}=e,a=new Map;for(const e of s){const t=o.get(e)||1;a.set(e,Math.log(1+(c-t+.5)/(t+.5)))}const l=[];for(const e of r){let t=0;for(const n of s){const s=e.tf.get(n);s&&(t+=a.get(n)*(2.5*s/(s+1.5*(.25+e.len/i*.75))))}if(t>0){if(e.pathTokens)for(const n of s)e.pathTokens.has(n)&&(t+=.6*a.get(n));l.push({file:e.file,start:e.start,end:e.end,text:e.text,score:t,sym:e.sym})}}return l.sort((e,t)=>t.score-e.score),l.slice(0,n)}export function cosine(e,t){if(!e||!t)return 0;let n=0,s=0,r=0;const o=Math.min(e.length,t.length);for(let c=0;c<o;c++)n+=e[c]*t[c],s+=e[c]*e[c],r+=t[c]*t[c];return s&&r?n/(Math.sqrt(s)*Math.sqrt(r)):0}export async function searchCode(e,t,n){const s=String(t?.query||"").trim();if(!s)return"缺少query(要检索的概念/功能描述)";const r=Number.isFinite(t?.k)&&t.k>0?Math.min(t.k,20):8,o=getIndex(e);if(!o.N)return"(当前目录未索引到代码文件)";const i=!!(n&&n.embeddingModel&&n.baseUrl&&n.apiKey);let a=search(o,s,i?Math.min(30,Math.max(3*r,15)):r);if(!a.length)return`(未找到与「${s}」明显相关的代码;可换关键词,或用grep精确匹配)`;let l=!1;if(i&&a.length>1)try{const e=a.map(e=>e.text.slice(0,2e3)),t=await c(n.baseUrl,n.apiKey,n.embeddingModel,[s,...e]);if(t&&t.length===e.length+1&&t[0]){const e=t[0];a=a.map((n,s)=>({...n,score:cosine(e,t[s+1])})).sort((e,t)=>t.score-e.score),l=!0}}catch{}let u=l?"(向量语义重排)\n":"";return o.truncated&&(u+=`(大型仓库:已索引最相关的约 ${o.indexedFiles}/${o.totalFiles} 个文件;如需穷尽匹配请配合grep)\n`),u+a.slice(0,r).map(e=>{const t=e.text.length>1200?e.text.slice(0,1200)+"\n…(本块过长已截断)":e.text;return`${`${e.file}:${e.start}-${e.end}${e.sym?` ▸ ${e.sym}`:""}`}\n${t}`}).join("\n\n─────\n\n")}function M(e){const t=new Set,n=[/(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_]\w*)/g,/\bclass\s+([A-Za-z_]\w*)/g,/\bdef\s+([A-Za-z_]\w*)/g,/\b(?:export\s+)?const\s+([A-Za-z_]\w*)\s*=\s*(?:async\s*)?(?:function|\([^)]*\)\s*=>)/g,/@(?:Get|Post|Put|Delete|Request)Mapping\(\s*"([^"]+)"/g,/\bpublic\s+[\w<>[\]]+\s+([A-Za-z_]\w*)\s*\(/g,/\b(?:export\s+)?(?:interface|type|enum)\s+([A-Za-z_]\w*)/g,/\b(?:func|fn|fun)\s+([A-Za-z_]\w*)/g,/\b(?:struct|trait|impl)\s+([A-Za-z_]\w*)/g];for(const s of n){let n;for(;(n=s.exec(e))&&t.size<12;)t.add(n[1])}return[...t]}export function buildRepoMap(t,{maxFiles:n=40,maxChars:s=2500}={}){const r=[];for(const n of listSourceFiles(t))try{const t=e.statSync(n.abs);if(t.size>524288)continue;r.push({abs:n.abs,size:t.size,rel:n.rel,prio:indexPriority(n.rel)})}catch{}if(!r.length)return"";r.sort((e,t)=>e.prio-t.prio||t.size-e.size);const o=[];let c=0;for(const t of r.slice(0,n)){let n=[];try{n=M(e.readFileSync(t.abs,"utf8").slice(0,4e4))}catch{}const r=`- ${t.rel}${n.length?":"+n.slice(0,8).join(", "):""}`;if(c+r.length>s)break;o.push(r),c+=r.length+1}return o.join("\n")}
package/src/commands.js CHANGED
@@ -1 +1 @@
1
- import e from"node:fs";import n from"node:path";import t from"node:os";import{execSync as o}from"node:child_process";import{saveConfig as i,DEFAULT_MODEL as s,CONFIG_PATH as r,isWebSearchOn as a,setWebSearchOn as c}from"./config.js";import{refreshPlatformWebSearch as d,platformWebSearchState as l,platformStatusText as m}from"./websearch.js";import{listModels as p,fetchUsage as u,streamChat as f}from"./api.js";import{listSessions as g,loadSession as h,relTime as w,turnCount as y,saveSession as b,createSession as v,userKeyOf as $}from"./session.js";import{searchSessions as k,formatResults as S}from"./sessionindex.js";import*as j from"./goalround.js";import{detectBackend as x}from"./sandbox.js";import{compactNow as _,buildModelMessages as T,DEFAULT_AUTOCOMPACT_TOKENS as C}from"./compaction.js";import{memberContextLimit as I,MEMBER_TRIGGER_RATIO as M}from"./context-window.js";function L(e){const n=I(e);return n>0?`${n.toLocaleString()} tokens(会员等级上限;到 ${Math.round(100*M)}% 自动压缩,即使关闭了自动压缩,超出时也会压缩后重试)`:"按模型窗口(会员等级不限;超出时自动压缩后重试)"}import{computeInsights as E,formatInsights as A}from"./insights.js";import{setColorMode as N,setAccent as P,getAccent as D,ACCENTS as U}from"./colors.js";import{detectVerifyCommand as R}from"./autofix.js";import{PERMISSION_MODES as q,MODE_ICON as O,MODE_LABEL as K,MODE_DESC as B}from"./permissions.js";import{loadAllowRules as H,describeRules as F,parseRule as G,addProjectRule as W,removeProjectRule as J,GLOBAL_SETTINGS_PATH as Y,projectSettingsPath as Q}from"./settings.js";import{contextReport as z}from"./context-report.js";import{renderDiff as V}from"./diffview.js";import{loadHooks as X,formatHooks as Z}from"./hooks.js";import{copyToClipboard as ee}from"./clipboard.js";import{toMarkdown as ne,recapConversation as te}from"./transcript.js";import{runDoctor as oe}from"./doctor.js";import{TOOL_SPECS as ie}from"./tools.js";import{makeTodoTool as se}from"./todo.js";import{makeMemoryTool as re}from"./memory.js";import{loadLocalSkills as ae,isUserInvocable as ce}from"./skills.js";import{COMMAND_ALIASES as de,commandCategory as le,commandAliases as me,matchCommands as pe,option as ue,settingMenu as fe,choose as ge,commandRequest as he}from"./command-ui.js";const we=/^(agent|workflow|datahuman|squad)-/,ye="\n\n# 附加可访问目录(可用read_file/list_files/grep以绝对路径访问;写入仍限主目录)\n";function be(e){const n=e.messages&&e.messages[0];if(!n||"system"!==n.role||"string"!=typeof n.content)return;const t=n.content.indexOf(ye);let o=t>=0?n.content.slice(0,t):n.content;const i=e.extraDirs||[];i.length&&(o+=ye+i.map(e=>"- "+e).join("\n")+"\n"),n.content=o}export const INIT_PROMPT="请为当前项目生成或完善HUICAI.md(给终端智能体看的项目说明):先用list_files/glob/read_file快速调研项目(技术栈、目录结构、构建/测试/运行命令、代码约定),再用write_file创建或更新HUICAI.md(若已存在则保留人工撰写的内容,增量完善)。内容包含:项目简介、关键目录、常用命令(构建/测试/运行)、代码风格与注意事项。控制在 100 行以内,用中文。";const ve={manual:"manual","请求批准":"manual","审批":"manual",acceptedits:"acceptEdits","accept-edits":"acceptEdits","接受编辑":"acceptEdits","编辑":"acceptEdits",auto:"auto","自动":"auto",yolo:"auto","无限制":"auto",plan:"plan","计划":"plan",sandbox:"sandbox","沙箱":"sandbox"};function $e(e){return e.permissions?.getMode?.()??(e.permissions?.isYolo?.()?"auto":"manual")}function ke(e,n){e.permissions?.setMode?e.permissions.setMode(n):(e.permissions?.setYolo?.("auto"===n),e.permissions?.setAcceptEdits?.("acceptEdits"===n),e.permissions?.setPlanMode?.("plan"===n),e.permissions?.setSandbox?.("sandbox"===n)),e.config&&(e.config.planMode="plan"===n,e.config.sandbox="sandbox"===n)}const Se=[{name:"help",args:"[命令名]",desc:"分类浏览命令、别名与技能;/help 命令名查看用法",run(e,n){const t=listCommands(e.cwd);if(n){const o=n.replace(/^\//,""),i=t.find(e=>e.name===o||e.aliases?.includes(o));return void e.print(i?`/${i.name} ${i.args}\n${i.desc}`+(i.aliases.length?`\n兼容别名:${i.aliases.map(e=>"/"+e).join("、")}`:"")+(i.menu?"\n直接回车打开紧凑选择面板;方向键选择,Enter 确认,Esc 取消。":""):"未知命令。/help 查看完整列表。")}const o=new Map;for(const e of t)o.has(e.category)||o.set(e.category,[]),o.get(e.category).push(` /${e.name}${e.args?" "+e.args:""} ${e.desc}`);e.print([...o].map(([e,n])=>e+"\n"+n.join("\n")).join("\n\n")+"\n\n输入 / 搜索命令与技能 · /help <命令名> 查看详情和别名\n! <命令> 直接执行 shell 命令并把输出加入上下文\n@<文件路径> 在消息中引用文件内容")}},{name:"model",args:"[名称|auto]",desc:"选择账号可用模型;auto 自动选择,支持直接传入模型 ID",async run(e,n){if(n){if("auto"===n){try{const n=(e.commandModels||await he(p(e.config.baseUrl,e.config.apiKey),e.signal)).filter(e=>!/embed/i.test(e.id)&&"embedding"!==e.type&&!e.capabilities?.includes?.("embeddings")).map(e=>e.id).filter(e=>!we.test(e));if(!n.length)return void e.print("账号下暂无可自动选择的对话模型,当前设置保持不变。");if(e.signal?.aborted)return;e.config.model=n.includes(s)?s:n[0]}catch(n){return void(e.signal?.aborted||e.print("自动选择失败:"+n.message+";当前设置保持不变。"))}if(e.signal?.aborted)return;return e.session&&(e.session.model=e.config.model),e.session?.id&&b(e.session),i({model:"auto"}),void e.print("Auto已选:"+e.config.model)}try{const t=(e.commandModels||await he(p(e.config.baseUrl,e.config.apiKey),e.signal)).map(e=>e.id);if(!t.includes(n))return void e.print(`当前账号无权使用模型「${n}」。\n 可用模型:${t.slice(0,12).join(", ")}`+(t.length>12?` …(共 ${t.length} 个,/models查看全部)`:"")+"\n 或用 /model auto自动选择可用模型。")}catch(n){if(e.signal?.aborted)return;e.print(`(未能校验模型可用性:${n.message})仍按输入切换;若无权限,平台会在调用时拒绝。`)}e.signal?.aborted||(e.config.model=n,e.session&&(e.session.model=n),i({model:n}),e.session?.id&&b(e.session),e.print("已切换模型:"+n))}else e.print("当前模型:"+(e.config.model||"(未设置)"))}},{name:"models",desc:"选择可用模型(普通输出宿主列出模型清单)",async run(e){try{const n=await he(p(e.config.baseUrl,e.config.apiKey),e.signal);e.print(n.map(e=>" "+e.id).join("\n")||"(无)")}catch(n){e.print("拉取失败:"+n.message)}}},{name:"usage",desc:"查看Token用量窗口(5小时/7天,按会员等级)",async run(e){const n=e=>"number"==typeof e&&Number.isFinite(e)?e:0,t=(e,t)=>(e=n(e),(t=n(t))<0?`${e.toLocaleString()} / 无限`:0===t?"该档未开放":`${e.toLocaleString()} / ${t.toLocaleString()} (${Math.min(100,Math.round(e/t*100))}%)`);try{const n=await u(e.config.baseUrl,e.config.apiKey);if(!n||"number"!=typeof n.used5h&&"number"!=typeof n.used7d)return void e.print("平台未返回用量数据(请确认平台已支持 /v1/usage)。");e.print(`Token用量窗口\n 近 5 小时:${t(n.used5h,n.limit5h)}\n 近 7 天 :${t(n.used7d,n.limit7d)}`+(!n.allowed&&n.message?"\n "+n.message:""))}catch(n){e.print("拉取用量失败:"+n.message)}}},{name:"login",desc:"重新登录(浏览器授权;无浏览器请退出后huicai login --paste)",async run(e){try{const{deviceLogin:n}=await import("./device.js");e.print("正在打开浏览器授权…");const t=await n(e.config.baseUrl);e.config.apiKey=t,i({apiKey:t}),e.print("登录成功。")}catch(n){e.print("登录未完成:"+n.message+"(可退出后用huicai login --paste粘贴令牌)")}}},{name:"logout",desc:"退出登录(清除本机保存的令牌)",run(e){e.config.apiKey="",i({apiKey:""}),e.print("已退出登录。后续对话需重新 /login或huicai login。")}},{name:"clear",desc:"开启新会话(旧会话仍可 --resume恢复)",run:()=>({action:"clear"})},{name:"compact",desc:"立即压缩上下文(摘要独立存储,不影响页面与会话原文)",async run(e){e.session.compaction||(e.session.compaction={});const n=await _(e.session.messages,e.config,{state:e.session.compaction});e.print(n>0?`已压缩:${n} 条较早消息已摘要(原对话完整保留,仅发给模型的上下文变小)`:"暂无可压缩的中段历史(会话较短或摘要失败)")}},{name:"memory",args:"[clear]",desc:"查看本项目长期记忆(clear清空)",async run(e,n){const{loadMemory:t,clearMemory:o,memoryPath:i}=await import("./memory.js");if("clear"===n)return void e.print(o(e.cwd)?"已清空本项目长期记忆。":"本项目暂无长期记忆。");const s=t(e.cwd);e.print(s?s+`\n(文件:${i(e.cwd)} · /memory clear清空)`:"本项目暂无长期记忆(模型会经memory_write自动记录值得跨会话记住的信息;配置memory:false可关闭)。")}},{name:"decisions",args:"[关键字|sync]",desc:"列出本项目决策图谱(决策追溯);sync与云端双向同步",async run(e,n){const t=String(n||"").trim().toLowerCase();if("sync"===t){const{baseUrl:n,apiKey:t}=e.config||{};if(!t)return void e.print("未登录,无法同步决策图(请先登录平台)。");const o=await import("./decisions.js"),i=await import("./api.js"),s=o.projectKey(e.cwd);let r=0;const a=o.unsyncedDecisions(e.cwd);if(a.length){const c=a.map(e=>({decisionId:e.id,title:e.title,status:e.status,payload:JSON.stringify(e),clientCreatedAt:e.createdAt}));try{r=await i.pushDecisions(n,t,s,c),o.markSynced(e.cwd,a.map(e=>e.id))}catch(n){return void e.print("决策同步失败(推送):"+n.message)}}let c=0;try{const r=await i.pullDecisions(n,t,s,o.getLastSync(e.cwd));c=o.mergeRemote(e.cwd,r),o.setLastSync(e.cwd,Date.now())}catch(n){return void e.print("决策同步失败(拉取):"+n.message)}return void e.print(`决策图同步完成:推送 ${r} 条,拉取新增 ${c} 条。`)}const{listDecisions:o}=await import("./decisions.js");let i=o(e.cwd);if(t&&(i=i.filter(e=>`${e.title} ${e.why} ${(e.tags||[]).join(" ")}`.toLowerCase().includes(t))),!i.length)return void e.print(t?"没有匹配的决策。":"本项目暂无决策记录(模型做取舍时会经record_decision自动记;也可用 /decision手动记)。");const s=i.slice(0,40).map(e=>{const n="draft"===e.status?"◇":"superseded"===e.status?"▽":"◆",t=(e.alternatives||[]).length?` · ${e.alternatives.length} 备选`:"",o=new Date(e.createdAt).toLocaleString("zh-CN",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}),i=e.anchor&&e.anchor.git?" · @"+String(e.anchor.git).slice(0,7):"";return` ${n} ${e.title}${t}\n ${o} · ${e.id}${i}`});e.print(`本项目决策(${i.length},◆正式 ◇草稿 ▽已取代):\n`+s.join("\n"))}},{name:"decision",args:"<what> | <why> | <备选1;备选2>",desc:"手动记录一个决策(| 分隔,备选用 ; 分隔)",async run(e,n){const t=String(n||"").trim();if(!t)return void e.print("用法:/decision <选择了什么> | <为什么> | <备选1;备选2>\n例:/decision向量检索用pgvector | 大库应用层余弦会超时 | 应用层余弦;外部向量库");const[o,i,s]=t.split("|").map(e=>(e||"").trim()),r=(s?s.split(";"):[]).map(e=>({option:(e||"").trim()})).filter(e=>e.option),{recordDecision:a}=await import("./decisions.js"),c=a(e.cwd,{what:o,title:o,why:i,alternatives:r,sessionId:e.config?.sessionId||""},{origin:"user"});e.print(`◆ 已记录决策:${c.title}${"draft"===c.status?"(无备选,存为待确认草稿)":""} · ${c.id}`)}},{name:"cost",desc:"查看本次运行累计token用量(及token预算 / 班子派发额度进度)",run(e){const n=e.usage.prompt+e.usage.completion,t=Number(e.config.budgetTokens)||0,o="function"==typeof e.crewOf?e.crewOf():null;e.print(`token用量:输入 ${e.usage.prompt} / 输出 ${e.usage.completion}`+(e.session.lastPromptTokens?` · 当前上下文 ~${e.session.lastPromptTokens} tok`:"")+(t>0?`\n预算:${n.toLocaleString()} / ${t.toLocaleString()} tokens(${Math.round(n/t*100)}%)`+(n>=t?" ⚠ 已超预算":n>=.8*t?" ⚠ 接近预算":""):"")+(o?"\n\n班子预算(P2-14):\n"+o.status():""))}},{name:"status",desc:"显示运行状态(版本/模型/目录/会话/MCP/权限规则)",run(e){const n=e.session,t=[...new Set((e.extraTools||[]).map(e=>e.spec?.function?.name||"").filter(e=>e.startsWith("mcp__")).map(e=>e.split("__")[1]))];let i="";try{i=o("git rev-parse --abbrev-ref HEAD",{cwd:e.cwd,stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{}const s=H(e.cwd),r=$e(e),a=[`版本huicai ${e.version||""}`,`模型 ${e.config.model}`,`平台 ${e.config.baseUrl}`,`目录 ${e.cwd}${i?`(git:${i})`:""}`,`会话 ${n.id} · ${y(n.messages)} 轮${n.lastPromptTokens?` · 上下文 ~${n.lastPromptTokens} tok`:""}`,`token输入 ${e.usage.prompt} / 输出 ${e.usage.completion}`,`模式 ${O[r]} ${K[r]}(${B[r]})`,`审批 允许规则 ${s.length} 条${e.config.goal?" · 🎯 目标已设":""}`,`MCP ${t.length?t.join(", "):"(未配置)"}`];e.print(a.join("\n"))}},{name:"resume",args:"[会话ID]",desc:"选择当前项目的历史会话并继续对话",async run(e,n){const t=$(e.config),o=g(e.cwd).filter(e=>!e.user||e.user===t);let i=n;if(!i&&e.select&&(i=await ge(e,{title:"继续历史会话",current:e.session?.id,options:o.map(e=>ue(e.id,e.title,`${w(e.updatedAt)} · ${e.turns} 轮 · ${e.id}`))})),!i)return void(e.select||e.print("用法:/resume <会话ID>;/sessions 查看当前项目的会话。"));if(!o.some(e=>e.id===i))return void e.print("未找到当前项目与账号下的会话。");const s=h(i);s?e.resumeSession?(b(e.session),await e.resumeSession(s),e.print(`已恢复会话:${s.title}(${s.id})`)):e.print(`请在终端运行:huicai resume ${i}`):e.print("会话无法读取,当前对话保持不变。")}},{name:"sessions",desc:"列出当前目录的历史会话",run(e){const n=g(e.cwd);if(!n.length)return void e.print("该目录暂无历史会话。");const t=n.slice(0,15).map(n=>{const t=n.id===e.session.id?" ← 当前":"";return` ${w(n.updatedAt).padStart(8)} ${n.title} ${n.turns}轮 · ${n.id}${t}`});t.push("(/resume 选择恢复;也可退出后 huicai --resume)"),e.print(t.join("\n"))}},{name:"mode",args:"["+q.join("|")+"]",desc:"对话模式:请求批准/接受编辑/自动/计划/沙箱(自动=无限制,任何命令免审批)",run(e,n){const t=$e(e),o=String(n||"").trim();if(!o){const n=[`当前对话模式:${O[t]} ${K[t]}(${B[t]})`,"","可切换:"];for(const e of q)n.push(` ${e===t?"●":" "} ${O[e]} ${e.padEnd(11)} ${K[e]} — ${B[e]}`);return n.push(""," 用法:/mode <名称>(也可用 /yolo、/plan、/sandbox快捷切换)"),void e.print(n.join("\n"))}const i=ve[o]||ve[o.toLowerCase()];i?(ke(e,i),e.print(`对话模式 → ${O[i]} ${K[i]}(${B[i]})`+("plan"===i?"\n 只做只读调研出方案;/plan approve批准并分步执行,或 /mode auto直接放手执行。":"")+("auto"===i?"\n ⚠ 已开放所有权限:shell/Python/联网/自动化等任何命令都不再询问。":""))):e.print(`未知模式:${o}。可选:${q.join(" / ")}`)}},{name:"yolo",desc:"选择自动执行或请求批准;自动模式下命令免审批",run(e){const n="auto"!==$e(e);ke(e,n?"auto":"manual"),e.print(n?"⚡ 自动模式:开。已开放所有权限,任何命令(含shell/联网/自动化)都不再询问。":"🔒 自动模式:关。回到「请求批准」,写/执行按权限逐项审批。")}},{name:"cwd",desc:"显示当前工作目录",run(e){e.print(e.cwd)}},{name:"markdown",args:"[on|off]",desc:"切换助手回复的Markdown渲染(TUI富文本;关闭则显示原始文本,便于复制)",run(e,n){if(n&&!["on","off"].includes(n))return void e.print("用法:/markdown [on|off]");const t=n?"on"===n:!1===e.config.markdown;e.config.markdown=t,i({markdown:t}),e.print("Markdown渲染:"+(t?"开(下一条回复起生效)":"关(显示原始Markdown文本)"))}},{name:"init",desc:"让智能体调研项目并生成/完善HUICAI.md",run:()=>({prompt:INIT_PROMPT})},{name:"skills",desc:"选择并调用本地技能(Agent Skills);pull/push 使用 huicai skills",async run(e){const{loadLocalSkills:n}=await import("./skills.js"),t=n(e.cwd);t.length?e.print(t.map(e=>` ${e.name}${e.version?" v"+e.version:""}(${"project"===e.scope?"项目":"全局"}${e.files.length?` · ${e.files.length} 附件`:""})${e.description?" "+e.description.slice(0,60):""}`).join("\n")+"\n(模型会按描述经load_skill按需载入;本会话启动时已注入目录)"):e.print("暂无本地技能(.huicai/skills或 ~/.huicai/skills)。可用huicai skills pull从平台拉取。")}},{name:"mcp",desc:"列出已连接的MCP工具",run(e){const n=(e.extraTools||[]).filter(e=>(e.spec?.function?.name||"").startsWith("mcp__"));n.length?e.print(n.map(e=>` ${e.spec.function.name}${e.readOnly?"(只读)":""}`).join("\n")):e.print("未连接MCP服务。配置 ~/.huicai/mcp.json(通用mcpServers格式)后重启生效。")}},{name:"context",desc:"可视化当前上下文占用(各类别token估算)",run(e){const n=we.test(String(e.config.model||""));let t=[];if(!n)try{const n=se({state:{list:[]},emit(){}}).spec,o=!1!==e.config.memoryEnabled?re({cwd:e.cwd,emit(){}}).spec:null;t=[...ie,n,...o?[o]:[],...(e.extraTools||[]).map(e=>e.spec).filter(Boolean)]}catch{t=ie}e.print(z({messages:e.session.messages,compaction:e.session.compaction||{},specs:t,lastPromptTokens:e.session.lastPromptTokens||0,autoCompactTokens:Number(e.config.autoCompactTokens)||0,memberLimit:I(e.config),platform:n,lastUsage:e.session.lastUsage||null}))}},{name:"permissions",args:"[模式|rules|allow|deny|remove [规则]]",desc:"选择权限模式、查看或管理工具权限规则",run(e,n){const t=String(n||"").trim().split(/\s+/).filter(Boolean),o=(t.shift()||"").toLowerCase(),i=t.join(" ").trim();if(ve[o])return Se.find(e=>"mode"===e.name).run(e,ve[o]);if(!o||"rules"===o){const n=F(e.cwd),t=e=>e.length?e.map(e=>" "+e).join("\n"):" (无)";return void e.print("权限规则(deny优先于allow/yolo,对只读工具也生效)\n 全局 ~/.huicai/settings.json\n 允许:\n"+t(n.global.allow)+"\n 拒绝:\n"+t(n.global.deny)+"\n 项目 .huicai/settings.json\n 允许:\n"+t(n.project.allow)+"\n 拒绝:\n"+t(n.project.deny)+"\n"+(e.permissions?.policySummary?" ── 声明式策略(policy.json,只做减法,拒绝先于一切)──\n "+e.permissions.policySummary().split("\n").join("\n ")+"\n":"")+" 用法:/permissions allow <规则> · /permissions deny <规则> · /permissions remove <规则>\n 规则语法:`工具名` 或 `工具名(模式*)`,如run_bash(git *)、edit_file、read_file(**/.env*)")}if("allow"!==o&&"deny"!==o){if("remove"===o||"rm"===o){if(!i)return void e.print("用法:/permissions remove <规则原文>(仅移除项目设置里的规则)");const n=J(e.cwd,i);return e.permissions?.reloadRules?.(),void e.print(n.removed?`已移除规则:${i}`:`未在项目设置中找到该规则:${i}`)}e.print("未知子命令。用法:/permissions [allow|deny|remove] <规则>")}else{if(!i||!G(i))return void e.print("规则无效。示例:run_bash(git *)、edit_file、fetch_url(https://a.com*)");try{const n=W(e.cwd,o,i);e.permissions?.reloadRules?.(),e.print(`已添加${"deny"===o?"拒绝":"允许"}规则:${i} → ${n}`)}catch(n){e.print("写入失败:"+n.message)}}}},{name:"diff",desc:"查看未提交改动(git)与本会话智能体改过的文件",run(e){e.print(V(e.cwd,e.session.messages))}},{name:"pr",args:"[commit|address]",desc:"自动提交并开PR(生成规范commit→建分支→push→gh开PR);address=按review评论自动改",async run(e,n){const t=String(n||"").trim().toLowerCase();if("address"===t||"comments"===t)return{prompt:"请处理当前分支/PR的review评论:用 `gh pr view --comments`(无PR则先 `gh pr status`)读取评论,逐条理解并修改代码解决,然后git add/commit(规范信息)+ git push;完成后逐条说明如何处理的。若未装gh或无PR,先告知。"};const{runPrFlow:o}=await import("./gitflow.js");try{const n=await o({cwd:e.cwd,config:e.config,emit:n=>e.print(n),push:"commit"!==t,openPr:"commit"!==t});if(!n.ok)return void e.print("✗ "+n.error);e.print(n.pr?"✅ 已开PR:"+n.pr:"✅ 完成(分支 "+n.branch+")"+(n.note?":"+n.note:""))}catch(n){e.print("PR流程出错:"+n.message)}}},{name:"plan",args:"[on|off|approve]",desc:"计划模式:只读调研出方案→/plan approve批准并分步执行",async run(e,n){const t=String(n||"").trim().toLowerCase();if("approve"===t||"go"===t){ke(e,"manual"),e.print("计划已批准,开始分步执行。");try{const{recordDecision:n}=await import("./decisions.js"),t=e.session?.messages||[];let o="";for(let e=t.length-1;e>=0;e--)if("assistant"===t[e].role&&"string"==typeof t[e].content){o=t[e].content;break}const i=(o.split("\n").find(e=>e.trim())||"批准的执行计划").replace(/^[#>\-*\s]+/,"").slice(0,120);n(e.cwd,{what:"批准并执行方案:"+i,why:o?o.slice(0,2e3):"用户在计划模式下评审后批准执行。",tags:["计划"],status:"active",sessionId:e.config?.sessionId||""},{origin:"user"})}catch{}return{prompt:"计划已获批准。现在请按你刚才制定的计划(todo列表)逐步执行:每完成一步用todo_write更新状态并简要自检;遇到与计划不符及时调整并说明;全部完成后总结改了什么、以及如何验证(可用 /verify让我自动跑验证)。"}}if(t&&!["on","off"].includes(t))return void e.print("用法:/plan [on|off|approve]");const o="on"===t||"off"!==t&&"plan"!==$e(e);ke(e,o?"plan":"manual"),e.print(o?"计划模式:开。只做只读调研并用todo_write产出分步方案;写/执行会被拦截。\n 方案满意后用 /plan approve批准并自动分步执行;或 /plan off手动执行。":"计划模式:关。恢复正常执行(写/执行仍按权限审批)。")}},{name:"auto",args:"<目标>",desc:"自动规划并自动执行:只读调研出分步计划,确认「是否继续?」后自动逐步执行",run(e,n){const t=String(n||"").trim();if(t)return{action:"autorun",goal:t};e.print("用法:/auto <目标>\n 先自动规划(只读调研 + todo分步计划),随后询问「是否继续?1、是 2、是,并允许在当前对话中后续相同操作不再询问 3、否」,确认后自动逐步执行并自检。")}},{name:"rewind",args:"[N [chat|code|both]]",desc:"检查点回滚:无参列出;/rewind N回退对话,加code/both一并回滚文件",run(e,n){const t=e.checkpoints;if(!t||!t.list.length)return void e.print("暂无检查点(每个用户回合前自动记录,发生对话后可用)。");const o=String(n||"").trim().split(/\s+/).filter(Boolean);if(!o.length){const n=["检查点(回滚到某个回合之前)"];for(const e of t.list)n.push(` #${e.id} ${w(e.time)} ${e.label}`+(e.journal.length?` · 改动 ${e.journal.length} 处`:""));return n.push(" 用法:/rewind N(仅回退对话,安全)· /rewind N code(回滚文件)· /rewind N both"),n.push(" 提示:code/both会覆盖当前文件,建议先 /diff查看。"),void e.print(n.join("\n"))}const i=Number(o[0]);if(!Number.isFinite(i))return void e.print("用法:/rewind N [chat|code|both]");const s=["chat","code","both"].includes(o[1])?o[1]:"chat",r=t.restore(i,s,{session:e.session,cwd:e.cwd});if(!r.ok)return void e.print(r.error);if(r.conversation&&"function"==typeof e.onRewind)try{e.onRewind(e.session.messages.length)}catch{}const a=[];r.conversation&&a.push("对话已回退到该回合之前"),r.filesRestored.length&&a.push(`恢复文件 ${r.filesRestored.length} 个`),r.filesDeleted.length&&a.push(`删除新建文件 ${r.filesDeleted.length} 个`),r.filesSkipped.length&&a.push(`跳过(过大) ${r.filesSkipped.length} 个`),r.filesFailed.length&&a.push(`失败 ${r.filesFailed.length} 个`);try{b(e.session)}catch{}return e.print("已回滚到 #"+i+":"+(a.join(";")||"无变化")+(r.filesFailed.length?"\n 失败:"+r.filesFailed.join(";"):"")),r.conversation?{action:"refresh"}:void 0}},{name:"hooks",desc:"查看已配置的工具事件钩子(Pre/PostToolUse/UserPromptSubmit/Stop)",run(e){e.print(Z(X(e.cwd)))}},{name:"copy",args:"[N]",desc:"复制助手回复到剪贴板(默认最后一条;N=倒数第N条)",async run(e,n){const t=e.session.messages.filter(e=>"assistant"===e.role&&"string"==typeof e.content&&e.content.trim());if(!t.length)return void e.print("还没有可复制的助手回复。");const o=Math.max(1,parseInt(n,10)||1),i=t[t.length-o];if(!i)return void e.print(`只有 ${t.length} 条助手回复。`);const s=await ee(i.content);e.print(s.ok?`已复制${o>1?`倒数第 ${o} 条`:"最后一条"}回复到剪贴板(${i.content.length} 字)`:`复制失败(未找到 ${s.tool})。以下为该回复内容:\n\n${i.content}`)}},{name:"export",args:"[文件路径]",desc:"导出当前对话为Markdown文件",run(t,o){const i=ne(t.session),s=String(o||"").trim()||`huicai-对话-${(new Date).toISOString().slice(0,19).replace(/[:T]/g,"-")}.md`,r=n.isAbsolute(s)?s:n.join(t.cwd,s);try{e.writeFileSync(r,i,"utf8"),t.print("已导出对话到 "+r)}catch(e){t.print("导出失败:"+e.message)}}},{name:"rename",args:"<新标题>",desc:"重命名当前会话",run(e,n){const t=String(n||"").trim();if(t){e.session.title=t.slice(0,80);try{b(e.session)}catch{}e.print("会话已重命名为:"+e.session.title)}else e.print("当前标题:"+(e.session.title||"(未命名)")+"\n 用法:/rename <新标题>")}},{name:"recap",desc:"用一句话总结当前对话进展(直连原始模型,不写入对话)",async run(e){if(e.session.messages.some(e=>"system"!==e.role))try{e.print("小结:"+await te(e.session.messages,e.config))}catch(n){e.print("生成小结失败:"+n.message)}else e.print("对话还没有内容。")}},{name:"distill",desc:"从当前对话提炼项目知识,写入长期记忆(供后续会话自动加载)",async run(e){if(!1===e.config.memoryEnabled)return void e.print("长期记忆已关闭(config.memory=false)。");if(!e.session.messages.some(e=>"system"!==e.role))return void e.print("对话还没有内容。");const{distillMemory:n}=await import("./transcript.js"),{loadMemory:t,appendMemory:o}=await import("./memory.js");try{const i=await n(e.session.messages,e.config,t(e.cwd));if(!i.length)return void e.print("未提炼出值得长期记住的新知识。");let s=0;for(const n of i)o(e.cwd,n)&&s++;e.print(`已写入 ${s} 条项目知识到长期记忆:\n`+i.map(e=>" · "+e).join("\n"))}catch(n){e.print("提炼失败:"+n.message)}}},{name:"doctor",desc:"本机环境体检(登录/网络/模型/git/工作目录/钩子);加capabilities实测渠道能力",async run(e,n){try{if(/^(capabilit(y|ies)|能力)\b/i.test(String(n||"").trim())){const{probeAll:t,formatProbes:o}=await import("./capability-probe.js"),i=String(n).trim().replace(/^\S+\s*/,""),s=(/--embed-model[= ]([^\s]+)/.exec(i)||[])[1]||"",r=(/--vision-model[= ]([^\s]+)/.exec(i)||[])[1]||e.config.visionModel;return e.config.baseUrl&&e.config.apiKey?(e.print("正在实测(会真的向平台发几个最小请求)…"),void e.print(o(await t({baseUrl:e.config.baseUrl,apiKey:e.config.apiKey,model:e.config.model,visionModel:r,embedModel:s})))):void e.print("未登录平台,无法实测渠道能力(/login或huicai login)。")}e.print(await oe(e))}catch(n){e.print("体检失败:"+n.message)}}},{name:"config",desc:"打开设置菜单;查看当前配置与文件路径(密钥脱敏)",run(e){const n=e.config,t=Number(n.autoCompactTokens);var o;e.print(["配置"," 文件 "+r," 平台 "+n.baseUrl," 密钥 "+(o=n.apiKey,o?o.slice(0,4)+"…"+o.slice(-4)+`(${o.length}位)`:"(未设置)")," 模型 "+n.model," 推理强度 "+(n.reasoningEffort||"off")," 自动压缩 "+(t<0?"关闭":(t>0?t:C).toLocaleString()+" tokens"+(t>0?"":"(默认)"))," 上下文窗口 "+L(n)," 记忆 "+(!1===n.memoryEnabled?"关":"开")," 联网搜索 "+(a()?"开":"关")+"(平台:"+m(l(n))+";/websearch查看与切换)"," Markdown "+(!1===n.markdown?"关":"开")," 全局设置 "+Y," 项目设置 "+Q(e.cwd)," (多数项有对应斜杠命令直接改:/model /effort /autocompact /permissions …)"].join("\n"))}},{name:"effort",args:"[minimal|low|medium|high|off]",desc:"设置推理强度(reasoning_effort透传;模型不支持则忽略)",run(e,n){const t=String(n||"").trim().toLowerCase();t?["minimal","low","medium","high","off"].includes(t)?(e.config.reasoningEffort="off"===t?"":t,i({reasoningEffort:e.config.reasoningEffort}),e.print("推理强度:"+(e.config.reasoningEffort||"off")+"(下一条消息起生效;模型不支持则被忽略)")):e.print("取值:minimal | low | medium | high | off"):e.print("当前推理强度:"+(e.config.reasoningEffort||"off")+"\n 用法:/effort minimal|low|medium|high|off")}},{name:"autocompact",args:"[N|default|off|status]",desc:"自动压缩上下文的触发阈值(token数;off关闭)",run(e,n){const t=String(n||"").trim().toLowerCase(),o=Number(e.config.autoCompactTokens)||0;if(!t||"status"===t)return void e.print("自动压缩:"+(o<0?"已关闭":o>0?`阈值 ${o.toLocaleString()} tokens`:`默认阈值 ${C.toLocaleString()} tokens`)+"\n上下文窗口:"+L(e.config)+"\n /autocompact <N> 设定 · /autocompact off关闭 · /autocompact status查看");if("default"===t)return e.config.autoCompactTokens=0,i({autoCompactTokens:0}),void e.print("已恢复默认自动压缩阈值。");if("off"===t)return e.config.autoCompactTokens=-1,i({autoCompactTokens:-1}),void e.print("已关闭自动压缩(仍可 /compact手动压缩)。");const s=/^\d+$/.test(t)?Number(t):NaN;!Number.isSafeInteger(s)||s<1e3?e.print("请给 ≥1000 的token阈值,或off / status。"):(e.config.autoCompactTokens=s,i({autoCompactTokens:s}),e.print("自动压缩阈值设为 "+s.toLocaleString()+" tokens。"))}},{name:"review",desc:"审查当前未提交改动(找bug与可简化处)",run:()=>({prompt:"请审查当前工作区的未提交改动:先用run_bash跑 `git diff`(及 `git status`)查看改了什么,聚焦正确性bug、边界与错误处理、可复用/可简化处、安全隐患。若本地有code-review技能可load_skill参照其方法。逐条给出:问题、定位(文件:行)、修改建议,按严重度排序;不确定的地方说明。"})},{name:"security-review",desc:"对当前改动做安全审查",run:()=>({prompt:"请对当前工作区改动做安全审查:注入(SQL/命令/路径穿越)、越权、密钥/凭据泄漏、SSRF、不安全依赖、反序列化、XSS等。先run_bash跑 `git diff` 查看改动。若本地有security-audit技能可load_skill参照。给出风险点、可利用场景、影响与修复建议,按严重度排序。"})},{name:"scan",args:"[fix]",desc:"安全/质量扫描(自动选可用的npm audit/semgrep/trivy/gitleaks等);fix=让智能体逐条修",async run(e,n){const{detectScanners:t,runScanners:o}=await import("./scan.js"),i=t(e.cwd);if(!i.length)return void e.print("未检测到可用扫描器。可安装semgrep / trivy / gitleaks,或项目含package.json(npm audit)等。");if("fix"===String(n||"").trim().toLowerCase())return{prompt:"请对本项目做安全/质量扫描并修复:依次运行并理解这些扫描器的输出——"+i.map(e=>"`"+e.command+"`("+e.why+")").join("、")+";对发现的问题逐个修复(升级/更换依赖、修代码、移除硬编码密钥等),修完重跑对应扫描确认清零;最后汇总修了什么。"};e.print("运行扫描器:"+i.map(e=>e.name).join(", ")+"(可能较慢)…");const s=await o(e.cwd,i);for(const n of s)e.print(`\n── ${n.name}(${0===n.code?"✅ 无高危":"⚠ 有发现"})──`),e.print(n.output.split("\n").slice(0,20).join("\n"));e.print("\n(/scan fix让智能体逐条修复)")}},{name:"cover",desc:"覆盖率驱动测试生成:跑覆盖率→定位未覆盖的改动→补测试→复测",run:()=>({prompt:"请做覆盖率驱动的测试补全:\n1) 跑测试覆盖率(按项目选:npm test -- --coverage / jest --coverage / pytest --cov / go test -cover / mvn test等);\n2) 重点看**最近改动**且覆盖率低或未覆盖的函数与分支;\n3) 为这些薄弱处补写有意义的测试(覆盖正常/边界/错误路径),遵循项目既有测试风格与框架;\n4) 重跑测试与覆盖率,确认通过且覆盖率提升;最后汇报补了哪些测试、覆盖率变化。"})},{name:"verify",args:"[验证命令]",desc:"修到通过:跑测试/构建,失败自动定位修复并重跑,直到通过或达上限",run(e,n){let t=String(n||"").trim();if(!t){if(t=R(e.cwd)||"",!t)return void e.print("未能自动探测验证命令。用法:/verify <命令>(如 /verify npm test)");e.print("自动探测到验证命令:"+t)}return{action:"autofix",command:t,maxIters:Math.min(10,Math.max(1,Number(e.config.verifyMaxIters)||5))}}},{name:"inspect",args:"[路径]",desc:"代码体检:逻辑/注释/可读性/结构/测试/安全七类机器排查,必修项交给智能体逐条修",async run(e,n){const{makeCodeTools:t}=await import("./code-tools.js"),o=t({cwd:e.cwd,config:e.config}).find(e=>"code_inspect"===e.spec.function.name),i=String(n||"").trim();e.print("正在体检"+(i?`:${i}`:"整个工作目录")+"…");const s=await o.run(i?{path:i}:{});if(e.print(s),!/未发现问题|必修 0/.test(s))return{prompt:"下面是刚跑出来的代码体检结论,请**逐条修掉必修项**(建议项挑真正该改的改):\n\n"+s+"\n\n改完用code_inspect复检确认必修清零再回话。确认某条是误报就加 `code-inspect-ignore: 规则名 理由` 并说明为什么,不要默默跳过。"}}},{name:"simplify",desc:"审查改动的可复用/简化/抽象层级并直接清理(只清理不找bug)",run:()=>({prompt:"请只做质量清理(不找bug):审查最近改动的复用(是否重复造轮子、可否复用现有工具/函数)、简化(是否有更直接的写法)、效率、以及抽象层级是否合适;把确定的改进直接用edit_file应用,保持与项目既有风格一致,改后跑测试/构建自检。若本地有refactor-safely技能可load_skill参照。最后说明改了什么、为什么。"})},{name:"insights",desc:"本机会话使用洞察(数量/轮数/活跃度/按项目·模型分布)",run(e){e.print(A(E(g(null),{cwd:e.cwd})))}},{name:"execsandbox",args:"[off | workspace-write | read-only]",desc:"执行沙箱:把run_bash/run_python关进内核约束,只能写工作区(与 /sandbox权限模式正交,可叠加)",run(e,n){const t=String(n||"").trim(),o=String(e.config.execSandbox||"off"),s=x();if(!t)return void e.print(`执行沙箱:${o}\n本机后端:${s.kind||"不可用("+s.reason+")"}\n /execsandbox workspace-write只能写工作区与临时目录(推荐)\n /execsandbox read-only任何文件都不许写\n /execsandbox off关闭(默认)\n说明:它约束的是「命令能动哪些文件」,与 /sandbox(决定「工具能不能被调用」)是两件事,可同时开。`);const r="on"===t?"workspace-write":t;["off","workspace-write","read-only"].includes(r)?"off"===r||s.kind?(e.config.execSandbox=r,i({execSandbox:r}),e.print("off"===r?"已关闭执行沙箱。":`已开启执行沙箱:${r}(后端 ${s.kind})。此后run_bash/run_python只能写工作区与临时目录。`)):e.print(`无法开启:${s.reason}\n(本机没有可用的沙箱后端,开了也不会生效,故不予开启。)`):e.print("用法:/execsandbox [off | workspace-write | read-only]")}},{name:"toolmode",args:"[native | both | code]",desc:"工具呈现方式:让模型写程序来编排工具调用(批量改文件/全仓统计这类多步活儿更快、更省上下文)",run(e,n){const t=String(n||"").trim(),o=String(e.config.toolMode||"native");t?["native","both","code"].includes(t)?(e.config.toolMode=t,i({toolMode:t}),e.print("native"===t?"已回到逐个调用工具(run_program已从工具表移除)。":`已切换为 ${t}:模型可以用run_program写程序编排工具${"code"===t?";读写/检索类工具已从它的工具表隐去":""}。`)):e.print("用法:/toolmode [native | both | code]"):e.print(`工具呈现方式:${o}\n /toolmode native逐个调用工具(默认,一切照旧)\n /toolmode both额外给一件run_program,模型自己判断何时写程序编排\n /toolmode code再把读写/检索类工具从模型工具表里隐去,一律走程序\n说明:程序里的每次工具调用照常走权限审批与钩子,不是绕过审批的通道;\n但一段程序是「一次批准、连做多步」,中途不便干预——这是它省时间的代价,请按任务性质选。`)}},{name:"history",args:"<关键词> [--here] [--legacy]",desc:"按内容检索历史对话(中英皆可;--here只看当前项目;--legacy连登录前的旧会话一起找)",run(e,n){const t=String(n||"").trim();if(!t)return void e.print("用法:/history <关键词> (按对话内容检索历史;--here只看当前项目;--legacy连登录前无用户标记的旧会话一起找)\n例:/history nginx配置 · /history上次那个报错");const o=/(^|\s)--here(\s|$)/.test(t),i=/(^|\s)--legacy(\s|$)/.test(t),s=t.replace(/(^|\s)--(here|legacy)(\s|$)/g," ").replace(/\s+/g," ").trim(),r=k(s,{k:10,userKey:$(e.config),cwd:o?e.cwd:void 0,includeUnowned:i});e.print(S(r,s))}},{name:"goal",args:"[目标 | status | pause | resume | limit <N> | clear]",desc:"设定长目标并自动续跑(到达/受阻/触顶才停);pause暂停 · resume继续 · limit改轮次上限 · clear清除",run(e,n){const t=String(n||"").trim(),o=j.viewGoal(e.session);if(!t||"status"===t)return e.print(o?`目标:${o.objective}\n状态:${{active:"进行中",paused:"已暂停",blocked:"受阻",complete:"已完成"}[o.phase]||o.phase}${o.armed?"(自动续跑已武装)":"(未武装,不会自动续跑)"}\n已跑 ${o.rounds}/${o.maxRounds} 轮`+(o.blockedReason?`\n卡点:${o.blockedReason}`:"")+"\n /goal pause暂停 · /goal resume继续 · /goal limit <N> 改上限 · /goal clear清除":"未设定目标。用法:/goal <目标>(设定后会自动逐轮推进,直到达成/受阻/触顶)");j.setRoundKind(e.session?.id,"human");const i=o?.rev;let s;return s="clear"===t?j.clearGoal(e.session):"pause"===t?j.pauseGoal(e.session,{rev:i}):"resume"===t?j.resumeGoal(e.session,{rev:i}):/^limit\s+\d+$/.test(t)?j.editGoal(e.session,{rev:i,maxRounds:Number(t.split(/\s+/)[1])}):j.createGoal(e.session,{objective:t}),s.ok?"clear"===t?e.print("已清除目标(不再自动续跑)。"):(b(e.session),void e.print(("pause"===t?"已暂停。":"resume"===t?"已恢复,下一轮起继续自动推进。":/^limit/.test(t)?"已更新轮次上限。":"已设定目标,本轮结束后开始自动推进。")+"\n"+`已跑 ${s.goal.rounds}/${s.goal.maxRounds} 轮`)):e.print(s.error)}},{name:"btw",args:"<问题>",desc:"问一个旁支问题(带当前上下文,但不写入对话、不调用工具)",async run(e,n){const t=String(n||"").trim();if(!t)return void e.print("用法:/btw <问题>(快速旁支问答,不影响主对话)");const o=we.test(String(e.config.model||""))?e.config.compactModel||s:e.config.model;try{const n=T(e.session.messages,e.session.compaction||{}),i=await f({baseUrl:e.config.baseUrl,apiKey:e.config.apiKey,model:o,client:e.config.client,sessionId:e.config.sessionId,messages:[...n,{role:"user",content:"(这是一个旁支问题,请结合上文直接简答,不要调用工具、不要展开无关内容)\n"+t}]});e.print("↪ "+(i.content||"(无回复)").trim())}catch(n){e.print("旁支问答失败:"+n.message)}}},{name:"ide",desc:"查看IDE集成状态(编辑器上下文与内联审阅)",run(e){const n=!!process.env.HUICAI_IDE_PORT,t=process.env.HUICAI_IDE_FILE,o=process.env.HUICAI_IDE_SELECTION,i=process.env.HUICAI_IDE_DIAGNOSTICS,s=["IDE集成"];s.push(" 内联审阅 "+(n?"已连接(write/edit交编辑器做diff审阅)":"未连接(CLI直接写盘)")),s.push(" 客户端 "+(e.config.client||"cli")),s.push(" 当前文件 "+(t||"(无)")),s.push(" 选区 "+(o?o.length+" 字":"(无)")),s.push(" 诊断 "+(i?"有(来自Problems面板)":"(无)")),n||s.push(" (在VS Code/IDEA的huicai面板或其启动的终端里运行可启用内联审阅与编辑器上下文)"),e.print(s.join("\n"))}},{name:"branch",args:"[标题]",desc:"把当前对话另存为一个可 --resume的分支副本(当前会话继续)",run(e,n){const t=e.session;if(!t.messages.some(e=>"system"!==e.role))return void e.print("当前对话为空,无需分支。");const o=v({cwd:e.cwd,model:t.model,messages:t.messages.map(e=>({...e}))});try{o.compaction=JSON.parse(JSON.stringify(t.compaction||{}))}catch{o.compaction={}}o.title=(String(n||"").trim()||t.title||"对话").slice(0,70)+"(分支)",o.lastPromptTokens=t.lastPromptTokens||0;try{b(o),b(t)}catch{}e.print(`已保存分支副本「${o.title}」(${o.id})。\n 当前会话继续;分支可用huicai --resume恢复到此刻状态。`)}},{name:"theme",args:"[default|mono]",desc:"输出主题:default彩色 / mono无色",run(e,n){const t=String(n||"").trim().toLowerCase();t?["default","mono"].includes(t)?(e.config.theme=t,N(t),i({theme:t}),e.print("主题:"+t+("mono"===t?"(已关闭颜色)":"(彩色)"))):e.print("取值:default | mono"):e.print("当前主题:"+(e.config.theme||"default")+"\n /theme default | mono(终端配色由终端掌控;mono=关闭颜色,便于复制/管道)")}},{name:"color",args:"["+U.join("|")+"]",desc:"设置强调色(提示符/高亮)",run(e,n){const t=String(n||"").trim().toLowerCase();t?P(t)?(e.config.accentColor=t,i({accentColor:t}),e.print("强调色:"+t)):e.print("可选:"+U.join(" / ")):e.print("当前强调色:"+D()+"\n 可选:"+U.join(" / "))}},{name:"tui",args:"[auto|rich|plain]",desc:"交互渲染器偏好(下次启动生效)",run(e,n){const t=String(n||"").trim().toLowerCase();t?["auto","rich","plain"].includes(t)?(e.config.tui=t,i({tui:t}),e.print("渲染器偏好:"+t+"(下次启动生效)")):e.print("取值:auto | rich | plain"):e.print("当前渲染器偏好:"+(e.config.tui||"auto")+"\n auto(TTY下富TUI)| rich | plain(下次启动生效;本次可加 --tui/--plain覆盖)")}},{name:"advisor",args:"[模型id|off]",desc:"设置顾问模型(更强模型):智能体遇难题可ask_advisor咨询第二意见",async run(e,n){const t=String(n||"").trim();if(!t)return void e.print(e.config.advisorModel?"当前顾问模型:"+e.config.advisorModel+"(智能体可用ask_advisor咨询)":"未设置顾问模型。用法:/advisor <模型id>(设一个更强的模型;/models看可用)· /advisor off关闭");const o="off"===t?"":t;if(e.config.advisorModel=o,i({advisorModel:o}),Array.isArray(e.extraTools)){const n=e.extraTools.findIndex(e=>"ask_advisor"===e.spec?.function?.name);if(n>=0&&e.extraTools.splice(n,1),o){const{makeAdvisorTool:n}=await import("./advisor.js"),t=n({config:e.config});t&&e.extraTools.push(t)}}e.print(o?"顾问模型已设为 "+o+"(ask_advisor工具已可用)":"已关闭顾问模型。")}},{name:"automodel",args:"[强模型id|off]",desc:"按任务自动选模型:复杂回合自动升配到该强模型,其余用当前模型",run(e,n){const t=String(n||"").trim();if(!t)return void e.print(e.config.autoModelStrong?"自动升配:开(复杂回合→"+e.config.autoModelStrong+",其余→"+e.config.model+")":"未开启。用法:/automodel <强模型id>(复杂回合自动用它;简单回合仍用当前便宜模型)· /models看可用");const o="off"===t.toLowerCase()?"":t;e.config.autoModelStrong=o,i({autoModelStrong:o}),e.print(o?"按任务自动选模型:开(复杂回合→"+o+",其余→"+e.config.model+")":"已关闭自动升配。")}},{name:"embed",args:"[嵌入模型id|off]",desc:"search_code向量语义重排:设一个嵌入模型走 /v1/embeddings重排(空/off=纯BM25)",run(e,n){const t=String(n||"").trim();if(!t)return void e.print(e.config.embeddingModel?"search_code向量重排:开(嵌入模型 "+e.config.embeddingModel+")":"未启用向量重排(纯BM25,离线可用)。用法:/embed <嵌入模型id>(需平台有该嵌入模型)· /embed off");const o="off"===t.toLowerCase()?"":t;e.config.embeddingModel=o,i({embeddingModel:o}),e.print(o?"search_code向量语义重排:开(嵌入模型 "+o+";下次检索生效,BM25 召回后按向量相似度重排)":"已关闭向量重排(回到纯BM25)。")}},{name:"crew",args:"[子任务数] [advisor次数]",desc:"查看/放宽班子编排额度(P2-14;撞顶后只有你能放行)",run(e,n){const t="function"==typeof e.crewOf?e.crewOf():null;if(!t)return void e.print('班子预算未开启。在 ~/.huicai/config.json设 "crewBudget": true,或HUICAI_CREW_BUDGET=1。');const o=String(n||"").trim().split(/\s+/).filter(Boolean);if(!o.length)return void e.print(t.status());let i=0,s=0;for(let e=0;e<o.length;e++)/^adv(isor)?$/i.test(o[e])?(s=Number(o[e+1])||0,e++):i||(i=Number(o[e])||0);i||s?e.print("已放宽额度:\n"+t.grant({subtasks:i,advisories:s})):e.print("用法:/crew 6(再放 6 个子任务的派发额度) · /crew advisor 3(再放 3 次顾问) · /crew(看进度)")}},{name:"budget",args:"[token数|off|status]",desc:"设置本项目token预算(软提醒;/cost看进度)",run(e,n){const t=String(n||"").trim().toLowerCase(),o=(e.usage?.prompt||0)+(e.usage?.completion||0);if(!t||"status"===t){const n=Number(e.config.budgetTokens)||0;return void e.print(n>0?`token预算:${o.toLocaleString()} / ${n.toLocaleString()}(${Math.round(o/n*100)}%)`:`未设预算(本次已用 ${o.toLocaleString()} tokens)。/budget <token数> 设定`)}if("off"===t)return e.config.budgetTokens=0,i({budgetTokens:0}),void e.print("已取消预算。");const s=/^\d+$/.test(t)?Number(t):NaN;!Number.isSafeInteger(s)||s<1e3?e.print("请给 ≥1000 的token数,或off / status。"):(e.config.budgetTokens=s,i({budgetTokens:s}),e.print("token预算设为 "+s.toLocaleString()+"(软提醒;/cost显示进度)。"))}},{name:"debug",args:"[on|off]",desc:"调试输出(每轮打印模型/耗时/token)",run(e,n){const t=String(n||"").trim().toLowerCase();if(t&&!["on","off"].includes(t))return void e.print("用法:/debug [on|off]");const o="on"===t||"off"!==t&&!e.config.debug;e.config.debug=o,i({debug:o}),e.print("调试输出:"+(o?"开(下一条消息起每轮打印 [debug] 行)":"关"))}},{name:"websearch",args:"[on|off|status]",desc:"联网搜索开关(统一走平台联网搜索;关闭后本端不再联网检索,含平台智能体的服务端联网)",async run(e,n){const t=String(n||"").trim().toLowerCase();if("on"===t||"off"===t){const n=await c("on"===t);if(e.config&&(e.config.webSearch=n),n&&!a())return void e.print("已记录为「开」,但环境变量HUICAI_WEB_SEARCH=0 强制关闭了联网搜索,当前仍不可用(去掉该环境变量后生效)。");if(!n)return void e.print(["联网搜索:关。"," · 本机web_search工具即刻从工具表消失,fetch_url也不再抓搜索引擎的检索页;"," · 平台智能体 / 工作流 / 专家团 / 数字人在本端的对话里也不会再做服务端联网搜索。"," (与桌面端 / IDEA共用同一开关 ~/.huicai/config.json,正在运行的会话下一次调用即生效;/websearch on恢复)"].join("\n"));const o=await d(e.config,{force:!0});return void e.print("联网搜索:开。平台状态:"+m(o)+("unavailable"===o.state?"\n 平台侧当前不可用,需管理员在管理后台「联网搜索」页启用并配置提供商。":""))}if(t&&"status"!==t)return void e.print("用法:/websearch [on|off|status]");const o=a(),i=e.config?.apiKey?await d(e.config,{force:!0}):l(e.config);e.print(["联网搜索"," 本端开关 "+(o?"开":"关")+("0"===process.env.HUICAI_WEB_SEARCH?"(环境变量HUICAI_WEB_SEARCH=0 强制关闭)":"")," 平台状态 "+(e.config?.apiKey?m(i):"未登录(联网搜索统一由平台提供)")," 说明 本端不直连任何搜索引擎,提供商与密钥由管理员在管理后台统一配置。"," 关闭后本机web_search与平台智能体的服务端联网都不会再被调用。/websearch on|off切换"].join("\n"))}},{name:"sandbox",args:"[on|off]",desc:"沙箱模式:禁shell与联网(仍可读写本地文件)",run(e,n){const t=String(n||"").trim().toLowerCase();if(t&&!["on","off"].includes(t))return void e.print("用法:/sandbox [on|off]");const o="on"===t||"off"!==t&&"sandbox"!==$e(e);ke(e,o?"sandbox":"manual"),e.print(o?"沙箱模式:开。run_bash与fetch_url将被拒绝;本地文件读写照常。":"沙箱模式:关(写/执行仍按权限审批)。")}},{name:"reload-skills",desc:"从磁盘重新加载技能(新增/改名技能生效;改正文已实时生效)",async run(e){const{makeSkillTools:n,loadLocalSkills:t}=await import("./skills.js");if(Array.isArray(e.extraTools)){for(let n=e.extraTools.length-1;n>=0;n--){const t=e.extraTools[n]?.spec?.function?.name;"load_skill"!==t&&"read_skill_file"!==t||e.extraTools.splice(n,1)}e.extraTools.push(...n(e.cwd))}const o=t(e.cwd);e.print(o.length?`已重载 ${o.length} 个技能:${o.map(e=>e.name).join(", ")}\n (目录会在下一轮自动刷新给模型,不必重开会话)`:"未发现本地技能(.huicai/skills或 ~/.huicai/skills)。")}},{name:"add-dir",args:"[目录 | list | remove <目录>]",desc:"添加可读/搜索的附加目录(写入仍限主目录)",run(t,o){const i=t.session;Array.isArray(i.extraDirs)||(i.extraDirs=[]);const s=String(o||"").trim().split(/\s+/).filter(Boolean),r=(s[0]||"").toLowerCase();if(!o||"list"===r)return void t.print(i.extraDirs.length?"附加目录:\n"+i.extraDirs.map(e=>" "+e).join("\n")+"\n /add-dir <目录> 添加 · /add-dir remove <目录> 移除":"暂无附加目录。用法:/add-dir <目录>(可读/搜索,写仍限主目录)");if("remove"===r||"rm"===r){const e=n.resolve(t.cwd,s.slice(1).join(" ")),o=i.extraDirs.length;i.extraDirs=i.extraDirs.filter(n=>n!==e),be(i);try{b(i)}catch{}return void t.print(i.extraDirs.length<o?"已移除:"+e:"未找到该附加目录:"+e)}const a=n.resolve(t.cwd,String(o).trim());let c=!1;try{c=e.statSync(a).isDirectory()}catch{}if(c)if(a!==t.cwd){i.extraDirs.includes(a)||i.extraDirs.push(a),be(i);try{b(i)}catch{}t.print("已添加可访问目录:"+a+"\n (模型可用read_file/list_files/grep以绝对路径访问;写入仍限主目录)")}else t.print("这已是主工作目录。");else t.print("目录不存在:"+a)}},{name:"agents",desc:"列出可用子智能体人格(.huicai/agents/*.md;dispatch_agent可指定)",async run(e){const{loadAgents:n}=await import("./subagent.js"),t=n(e.cwd);e.print(t.length?"子智能体人格(dispatch_agent的agent参数可指定):\n"+t.map(e=>` ${e.name}${e.description?" — "+e.description.replace(/\s+/g," ").slice(0,60):""}`).join("\n"):"未定义子智能体人格。\n 在 .huicai/agents/<名字>.md写frontmatter(name/description)+正文(人格提示词)即可;\n dispatch_agent默认用通用只读子智能体(无需定义也能用)。")}},{name:"orchestrate",args:"<目标>",desc:"把一个目标自动分解为并行子任务、多智能体协作完成",run(e,n){const t=String(n||"").trim();if(t)return{prompt:`目标:${t}\n\n请作为编排者(只调度不代工)按以下方式完成:\n1) 任务分配预检:先解析目标要什么,把它分解为若干**独立、可并行**的子任务(研究/检索/分析/定位),并确认每个子任务与所派子智能体的能力匹配——超出能力范围的部分如实说明,不硬派;\n2) 用dispatch_agents一次性并行委派这些子任务(每个说明必须独立自足——子智能体看不到本对话;可指定合适的子智能体人格);\n3) 信息流经你中转:需要衔接的结论由你提炼后放进下一批子任务的context,再行派发;\n4) 汇总各子智能体结论形成整体方案或结果,并自查:每部分都有子任务产出支撑、无遗漏、未完成的如实说明;\n5) 若需落地改动,再据此逐步执行并自检(可用 /verify让我自动跑验证)。\n优先用dispatch_agents并行,减少串行等待。`};e.print("用法:/orchestrate <较大的目标>(我会分解为并行子任务,用dispatch_agents并行推进后综合)")}},{name:"squads",desc:"列出可用专家团(内置10个+全局/项目自定义;复杂任务也会被自动选团)",async run(e){const{loadSquads:n}=await import("./squad.js"),t=n(e.cwd),o={project:"项目",global:"全局",builtin:"内置"};e.print(t.length?"专家团(/squad <名称> <任务> 交给团队完成;复杂任务对话中也会按「何时使用」自动选团):\n"+t.map(e=>` [${o[e.scope]||"自定义"}] ${e.name} — 主理人${e.leader} · ${e.members.length} 位成员`+(e.when?" · "+e.when.replace(/\s+/g," ").slice(0,46):e.description?" · "+e.description.replace(/\s+/g," ").slice(0,46):"")).join("\n")+"\n(/squad-init <新名> --from <内置团> 以内置团为底稿定制;项目/全局同名可覆盖内置)":"未定义专家团。\n /squad-init <名称> 生成模板(.huicai/squads/<名称>.md):成员表+任务预检+预设工作流;\n 平台上的专家团可直接 --model squad-<分享码> 使用(服务端编排)。")}},{name:"squad",args:"<名称> <任务>",desc:"把任务交给专家团:主理人预检派发、成员协作、汇总交付",async run(e,n){const{loadSquads:t,squadCommandPrompt:o,ensureTeamDir:i}=await import("./squad.js"),s=t(e.cwd),r=String(n||"").trim(),a=r.indexOf(" "),c=a<0?r:r.slice(0,a),d=a<0?"":r.slice(a+1).trim(),l=s.find(e=>e.name===c);if(!r||!l||!d)return void e.print("用法:/squad <名称> <任务>"+(s.length?`\n可用专家团:${s.map(e=>e.name).join("、")}`:"\n还没有专家团定义,先 /squad-init <名称> 生成模板。")+(r&&!l&&c?`\n(未找到「${c}」)`:""));if(!l.members.length)return void e.print(`专家团「${c}」还没有成员,请编辑其定义文件补充「## 成员」。`);const m=i(e.cwd,l.name);return e.print(`已召集专家团「${l.name}」(主理人${l.leader},${l.members.length} 位成员)`+(m?`,团队工作区:${m}`:"")),{prompt:o(l,d,m)}}},{name:"squad-init",args:"[名称] [--from <内置团>]",desc:"生成专家团定义(空白模板,或以内置团为底稿定制)",async run(e,n){const t=await import("node:fs"),o=await import("node:path"),{SQUAD_TEMPLATE:i,loadSquads:s}=await import("./squad.js"),r=String(n||"").trim().split(/\s+/).filter(Boolean),a=r.indexOf("--from"),c=a>=0?r[a+1]:null,d=(a>=0?r.filter((e,n)=>n!==a&&n!==a+1):r)[0];let l,m;if(c){const n=s(e.cwd).find(e=>e.name===c);if(!n||!n.file)return void e.print(`未找到底稿团「${c}」(/squads查看可用团名)`);m=(d||c).replace(/[^\w.\-一-龥]/g,"-"),l=t.readFileSync(n.file,"utf8").replace(/^(---[\s\S]*?\bname:\s*).*$/m,`$1${m}`)}else m=(d||"my-team").replace(/[^\w.\-一-龥]/g,"-"),l=i.replace("{NAME}",m);const p=o.join(e.cwd,".huicai","squads"),u=o.join(p,`${m}.md`);if(t.existsSync(u))return void e.print(`已存在:${u}(直接编辑即可)`);const{countUserSquads:f}=await import("./squad.js"),{refreshPlan:g,checkLimit:h}=await import("./plan-limits.js");await g();const w=h("squads",f(e.cwd));w?e.print(w.message):(t.mkdirSync(p,{recursive:!0}),t.writeFileSync(u,l),e.print(`已生成专家团定义:${u}${c?`(底稿:${c}${m===c?",项目层将覆盖内置同名团":""})`:""}\n 编辑成员表(执行者可指agent:本地人格 / platform:平台伪模型 / platform:auto:类型 自动绑定 / model:模型名)、成员人设、预检速查表与预设工作流后,/squad `+m+" <任务> 即可开工。"))}},{name:"experts",args:"[refresh|on|off]",desc:"平台能力自动编队:查看/刷新可用的专家团·工作流·智能体·数字人",async run(e,n){const{discoverExperts:t,makeExpertTools:o}=await import("./experts.js"),i=String(n||"").trim().toLowerCase(),s=e=>["consult_expert","consult_experts"].includes(e?.spec?.function?.name);if("off"===i){if(e.config.autoExperts=!1,Array.isArray(e.extraTools))for(let n=e.extraTools.length-1;n>=0;n--)s(e.extraTools[n])&&e.extraTools.splice(n,1);return void e.print('已关闭平台能力自动编队(本会话即时生效)。永久关闭:在 ~/.huicai/config.json中设 "autoExperts": false')}if("on"===i){if(e.config.autoExperts=!0,Array.isArray(e.extraTools)&&!e.extraTools.some(s)){const n=await o({config:e.config});e.extraTools.push(...n),e.print(n.length?"已开启平台能力自动编队(本会话即时生效)。":"已开启,但当前无可编队的平台专家(需登录且账号下有可用资源,或当前驱动模型为伪模型)。")}else e.print("平台能力自动编队已是开启状态。");return}const r=await t(e.config,{force:"refresh"===i});if(!r.length)return void e.print("当前无可编队的平台专家。"+(e.config.apiKey?"(账号下暂无可用的专家团/工作流/智能体/数字人,或平台不可达)":"请先huicai login登录平台。"));const a={squad:"专家团",workflow:"工作流",agent:"智能体",datahuman:"数字人"},c=["平台能力编队(原始模型对话时可被自动咨询"+(!1===e.config.autoExperts?",当前已关闭,/experts on开启":"")+"):"];for(const e of["squad","workflow","agent","datahuman"]){const n=r.filter(n=>n.type===e);if(n.length){c.push(` ${a[e]}(${n.length})`);for(const e of n)c.push(` ${e.id} — ${e.name}${e.description?":"+e.description.slice(0,50):""}`)}}c.push("refresh"===i?"(已强制刷新)":"(/experts refresh强制刷新;/experts off关闭)"),e.print(c.join("\n"))}},{name:"batch",args:"<大改动目标>",desc:"把一个大改动拆成独立子任务,在隔离git worktree中并行推进",run(e,n){const t=String(n||"").trim();if(t)return{prompt:`目标(需拆分并行落地):${t}\n\n请这样做:\n1) 先把目标拆成若干**相互独立、可并行**的改动子任务(尽量互不冲突的文件/模块);\n2) 对每个子任务,用 \`git worktree add -b huicai-batch/<名> <路径>\` 建隔离工作区,在其中完成该子任务的改动并commit(不影响主工作区);\n3) 全部完成后,汇总每个worktree的分支与改动;需要时可对每个分支执行 \`huicai pr\` 开PR。\n(并行调研可用dispatch_agents;改动务必隔离在各自worktree,避免互相踩踏。完成后可 \`git worktree remove\` 清理。)`};e.print("用法:/batch <一个可拆分的大改动目标>(拆成互不冲突的子任务,各在隔离worktree中落地)")}},{name:"fork",args:"<后台任务提示词>",desc:"派生后台智能体(继承当前对话副本,--yolo自动放行,输出写日志)",async run(e,n){const t=String(n||"").trim();if(!t)return void e.print("用法:/fork <让后台智能体做什么>(以当前对话副本为上下文,后台运行,--yolo自动放行)");const o=v({cwd:e.cwd,model:e.session.model,messages:e.session.messages.map(e=>({...e}))});try{o.compaction=JSON.parse(JSON.stringify(e.session.compaction||{}))}catch{o.compaction={}}o.title="(fork) "+(e.session.title||"");try{b(o)}catch{}const{forkAgent:i}=await import("./forks.js");let s;try{s=i({prompt:t,cwd:e.cwd,sessionId:o.id,ownerSessionId:e.session?.id||""})}catch(n){return void e.print("无法派生后台任务:"+n.message)}e.print(`已派生后台智能体,作业 ${s.id}(pid ${s.pid},--yolo自动放行)\n 上下文:当前对话副本 ${o.id}\n 日志:${s.log}\n /tasks查看 · /tasks kill ${s.id} 终止`)}},{name:"tasks",args:"[kill <作业id>]",desc:"查看/终止本会话的后台作业(/fork派生的智能体、run_bash后台命令)",async run(e,n){const t=await import("./jobs.js"),o=e.session?.id||"",i=String(n||"").trim().split(/\s+/).filter(Boolean);if("kill"===i[0]){const n=i[1];if(!n)return void e.print("用法:/tasks kill <作业id>(作业id见 /tasks列表)");const s=t.kill(n,{sessionId:o,reason:"用户经 /tasks终止"});return void e.print(s.ok?"已请求终止作业 "+n:"终止失败:"+(s.reason||""))}const s=t.list(o);s.length?e.print("后台作业:\n"+s.map(e=>" "+t.describe(e)).join("\n")+"\n /tasks kill <作业id> 终止"):e.print("本会话暂无后台作业。(/fork <提示词> 派生后台智能体;run_bash设run_in_background放后台跑命令)")}},{name:"schedule",args:'[list | add "<cron>" "<提示词>" | remove <id>]',desc:"管理定时任务(生成crontab行交系统cron运行)",async run(e,n){const t=await import("./schedule.js"),o=String(n||"").trim(),i="huicai";if(!o||"list"===o){const n=t.listTasks();return n.length?void e.print("定时任务:\n"+n.map(e=>` #${e.id} ${e.cron} ${e.prompt.replace(/\s+/g," ").slice(0,40)}\n crontab: ${t.crontabLine(e,i)}`).join("\n")+"\n /schedule remove <id> 删除"):void e.print('暂无定时任务。\n 添加:/schedule add "*/30 * * * *" "跑测试并把结果写入report.md"\n (随后把打印的crontab行加入crontab -e)')}if(/^add\b/.test(o)){const n=o.match(/"([^"]*)"/g);if(!n||n.length<2)return void e.print('用法:/schedule add "<cron>" "<提示词>"(cron与提示词都要加引号)');const s=n[0].slice(1,-1).trim(),r=n[1].slice(1,-1).trim();if(5!==s.split(/\s+/).length)return void e.print('cron需 5 个字段,如 "*/30 * * * *"(分 时 日 月 周)');const{refreshPlan:a,isPlanLimitError:c}=await import("./plan-limits.js");let d;await a();try{({id:d}=t.addTask({cron:s,prompt:r,cwd:e.cwd}))}catch(n){if(!c(n))throw n;return void e.print(n.message)}return void e.print("已添加定时任务 #"+d+"\n 加入crontab(crontab -e):\n "+t.crontabLine({id:d,cron:s,prompt:r,cwd:e.cwd},i))}if(/^(remove|rm)\b/.test(o)){const n=o.split(/\s+/)[1];return void e.print(t.removeTask(n)?"已删除定时任务 "+n:"未找到定时任务 "+n)}e.print('用法:/schedule list · /schedule add "<cron>" "<提示词>" · /schedule remove <id>')}},{name:"cd",args:"<目录>",desc:"切换工作目录(在新目录开启新会话;IDE中请用「打开文件夹」)",run(t,o){const i=String(o||"").trim();if(!i)return void t.print("当前工作目录:"+t.cwd+"\n 用法:/cd <目录>(将在新目录开启新会话,旧会话已保存)");const s=n.resolve(t.cwd,i);let r=!1;try{r=e.statSync(s).isDirectory()}catch{}if(r){if(s!==t.cwd)return{action:"cd",cwd:s};t.print("已在该目录。")}else t.print("目录不存在:"+s)}},{name:"loop",args:"<秒> <提示词> | off",desc:"每隔N秒自动重跑一个提示词(off停止)",run(e,n){const t=String(n||"").trim();if(!t)return void e.print("用法:/loop <秒> <提示词>(如 /loop 300 检查构建是否通过)· /loop off停止");if("off"===t)return{action:"loop",stop:!0};const o=/^(\d+)\s+([\s\S]+)$/.exec(t);if(o)return{action:"loop",seconds:Math.max(10,parseInt(o[1],10)),prompt:o[2].trim()};e.print("用法:/loop <秒> <提示词> · /loop off")}},{name:"statusline",args:"<shell命令> | off",desc:"自定义TUI底栏(运行该命令,展示其stdout)",run(e,n){const t=String(n||"").trim();if(!t)return void e.print("当前状态栏命令:"+(e.config.statusLine||"(默认)")+"\n /statusline <shell命令>(stdout显示在TUI底栏)· /statusline off恢复默认");const o="off"===t?"":t;e.config.statusLine=o,i({statusLine:o}),e.print(o?"状态栏命令已设置(TUI底栏生效):"+o:"已恢复默认状态栏。")}},{name:"background",desc:"保存当前会话并给出在别处继续/后台运行的方式",run(e){try{b(e.session)}catch{}e.print(`会话已保存(${e.session.id})。\n · 在别处继续:huicai resume ${e.session.id}\n · 后台跑一个任务:/fork <提示词>(派生后台智能体,日志可查)\n · 定时运行:/schedule add "<cron>" "<提示词>"`)}},{name:"exit",desc:"退出",run:()=>({action:"exit"})}];export function listCommands(e,{includeSkills:n=!0}={}){const t=new Set("model models mode permissions config effort theme color tui execsandbox toolmode advisor automodel embed markdown debug websearch sandbox yolo plan autocompact budget skills resume sessions rewind pr".split(" ")),o=Se.map(e=>({name:e.name,args:e.args||"",desc:e.desc,aliases:me(e.name),category:le(e.name),menu:t.has(e.name),kind:"builtin"})),i=loadCustomCommands(e).map(e=>({name:e.name,args:"",desc:e.desc,aliases:[],category:"自定义命令",kind:"custom"})),s=new Set([...o,...i].map(e=>e.name).concat(Object.keys(de))),r=n?ae(e).filter(e=>ce(e)&&!s.has(e.name)).map(e=>({name:e.name,args:"[补充说明]",desc:e.description||"调用技能",aliases:[],category:"技能",kind:"skill"})):[];return[...o,...i,...r]}export function loadCustomCommands(o){const i=[o?n.join(o,".huicai","commands"):null,n.join(t.homedir(),".huicai","commands")].filter(Boolean),s=new Map;for(const t of i){let o;try{o=e.readdirSync(t).filter(e=>e.endsWith(".md"))}catch{continue}for(const i of o){const o=i.replace(/\.md$/,"").trim();if(o&&!Object.hasOwn(de,o)&&!s.has(o)&&!Se.some(e=>e.name===o))try{const r=e.readFileSync(n.join(t,i),"utf8"),{desc:a,body:c}=je(r);s.set(o,{name:o,desc:a||"自定义命令",template:c})}catch{}}}return[...s.values()]}function je(e){const n=/^---\n([\s\S]*?)\n---\n?/.exec(e);if(!n)return{desc:"",body:e.trim()};const t=n[1],o=/^description:\s*(.+)$/m.exec(t);return{desc:o?o[1].trim():"",body:e.slice(n[0].length).trim()}}export async function runSlash(e,n){const t=e.slice(1).split(/\s+/),o=(t[0]||"").trim(),i=Object.hasOwn(de,o)?de[o]:o||"help";let s=t.slice(1).join(" ").trim();if(n.signal?.aborted)return{handled:!0};if(n.select){const e=await async function(e,n,t){const o={cancelled:!0},i=async e=>({result:await runSlash(e,t)});if("permissions"===e&&["allow","deny","remove"].includes(n)){let e;if("remove"===n){const n=F(t.cwd).project;e=[...new Set([...n.allow,...n.deny])].map(e=>ue(e))}else{const n=[...ie,...(t.extraTools||[]).map(e=>e.spec)];e=[...new Map(n.filter(e=>e?.function?.name).map(e=>[e.function.name,ue(e.function.name,e.function.name,e.function.description?.split("\n")[0]||"")])).values()]}const i=await ge(t,{title:"remove"===n?"移除项目权限规则":`添加${"allow"===n?"允许":"拒绝"}规则 · 选定工具的所有参数`,options:e});return null==i?o:{arg:n+" "+i}}if(n)return null;if("sessions"===e&&t.resumeSession)return i("/resume");if("rewind"===e){const e=await ge(t,{title:"选择回退检查点",options:[...t.checkpoints?.list||[]].reverse().map(e=>ue(String(e.id),`#${e.id} ${e.label}`,`${w(e.time)} · ${e.journal.length} 处文件改动`))});if(null==e)return o;const n=await ge(t,{title:"选择回退范围",current:"chat",options:[ue("chat","仅对话","保留工作区文件"),ue("code","仅文件","覆盖当前文件,恢复检查点内容"),ue("both","对话与文件","同时回退对话并覆盖当前文件")]});return null==n?o:{arg:e+" "+n}}if("pr"===e){const e=await ge(t,{title:"Pull Request 操作",options:[ue("commit","提交并创建 PR","生成提交、建分支、推送并使用 gh 创建 PR"),ue("address","处理 PR 审查意见","读取 review 评论并修改代码")]});return null==e?o:{arg:e}}if("config"===e){const e="model permissions effort advisor automodel embed toolmode execsandbox websearch autocompact budget theme color tui markdown debug".split(" "),n=await ge(t,{title:"HuiCai AI 设置",options:[...e.map(e=>ue(e,"/"+e,Se.find(n=>n.name===e).desc)),ue("show","查看全部配置(隐藏凭据)")]});return null==n?o:"show"===n?{result:{handled:!0,...await Se.find(e=>"config"===e.name).run(t)}}:i("/"+n)}if("skills"===e){const e=listCommands(t.cwd).filter(e=>"skill"===e.kind),n=await ge(t,{title:"选择要调用的技能",options:e.map(e=>ue(e.name,e.name,e.desc))});return null==n?o:i("/"+n)}if(["model","models","advisor","automodel","embed"].includes(e)){let n;try{n=await he(p(t.config.baseUrl,t.config.apiKey),t.signal)}catch(e){return t.signal?.aborted||t.print("无法加载模型列表:"+e.message+"\n请检查登录与网络后重试;当前设置保持不变。"),o}const i="model"===e||"models"===e,s=i?"model":"advisor"===e?"advisorModel":"automodel"===e?"autoModelStrong":"embeddingModel",r=t.config[s]||"off",a=e=>/embed/i.test(e.id)||"embedding"===e.type||e.capabilities?.includes?.("embeddings"),c=n.filter(n=>"string"==typeof n.id&&("embed"===e?a(n):!a(n))&&(i||!we.test(n.id))),d=[...new Map(c.map(e=>[e.id,ue(e.id,e.id,e.name&&e.name!==e.id?e.name:"")])).values()];i?c.some(e=>!we.test(e.id))&&d.unshift(ue("auto","Auto · 自动选择可用模型")):d.unshift(ue("off","关闭")),"embed"!==e||c.length||t.print("平台未提供可识别的嵌入模型;可用 /embed <模型ID> 显式配置。");const l=await ge(t,{title:i?"选择模型":"advisor"===e?"顾问模型":"automodel"===e?"复杂任务自动升配模型":"嵌入模型",current:r,options:d});if(null==l)return o;const m=i?"model":e;return{result:{handled:!0,...await Se.find(e=>e.name===m).run({...t,commandModels:n},l)}}}const s=fe(e,t);if(!s)return null;s.current&&!s.options.some(e=>e.value===s.current)&&s.options.unshift(ue(s.current,s.current+"(自定义当前值)"));const r=await ge(t,s);if(null==r)return o;if("yolo"===e)return i("/mode "+r);if("permissions"===e){if("execsandbox"===r)return i("/execsandbox");if(["allow","deny","remove"].includes(r))return i("/permissions "+r)}return{arg:r}}(i,s,n);if(e?.result)return e.result;if(e?.cancelled)return{handled:!0};e&&(s=e.arg)}const r=Se.find(e=>e.name===i);if(r){return{handled:!0,...await r.run(n,s)||{}}}const a=loadCustomCommands(n.cwd).find(e=>e.name===i);if(a){let e=a.template;return e.includes("$ARGUMENTS")?e=e.replaceAll("$ARGUMENTS",s):s&&(e=e+"\n\n"+s),{handled:!0,prompt:e}}{const{loadLocalSkills:e,readSkillBody:t,renderSkillContent:o,isUserInvocable:r}=await import("./skills.js"),a=e(n.cwd).find(e=>e.name===i);if(a&&r(a)){const e=t(n.cwd,i);if(e){n.print(`已投入技能「${i}」的操作指引${s?",并附上你的补充说明":""}。`);const t=o(e.skill,e.raw);return{handled:!0,prompt:s?`${t}\n\n${s}`:t}}}if(a&&!r(a))return n.print(`技能「${i}」标记为不可由用户直接调用(user-invocable: false),它只供模型按需载入。`),{handled:!0}}const c=pe(listCommands(n.cwd),i).slice(0,4);return n.print("未知命令。/help查看可用命令。"+(c.length?"\n可能要找:"+c.map(e=>"/"+e.name).join("、"):"")),{handled:!0}}
1
+ import{accountDataDir as e}from"./account-scope.js";import n from"node:fs";import t from"node:path";import o from"node:os";import{execSync as i}from"node:child_process";import{saveConfig as s,DEFAULT_MODEL as r,CONFIG_PATH as a,isWebSearchOn as c,setWebSearchOn as d}from"./config.js";import{refreshPlatformWebSearch as l,platformWebSearchState as m,platformStatusText as p}from"./websearch.js";import{listModels as u,fetchUsage as f,streamChat as g}from"./api.js";import{listSessions as h,loadSession as w,relTime as y,turnCount as b,saveSession as v,createSession as $,userKeyOf as k}from"./session.js";import{searchSessions as S,formatResults as j}from"./sessionindex.js";import*as x from"./goalround.js";import{detectBackend as _}from"./sandbox.js";import{compactNow as T,buildModelMessages as C,DEFAULT_AUTOCOMPACT_TOKENS as I}from"./compaction.js";import{memberContextLimit as M,MEMBER_TRIGGER_RATIO as L}from"./context-window.js";function E(e){const n=M(e);return n>0?`${n.toLocaleString()} tokens(会员等级上限;到 ${Math.round(100*L)}% 自动压缩,即使关闭了自动压缩,超出时也会压缩后重试)`:"按模型窗口(会员等级不限;超出时自动压缩后重试)"}import{computeInsights as A,formatInsights as N}from"./insights.js";import{setColorMode as P,setAccent as D,getAccent as U,ACCENTS as R}from"./colors.js";import{detectVerifyCommand as q}from"./autofix.js";import{PERMISSION_MODES as O,MODE_ICON as K,MODE_LABEL as B,MODE_DESC as H}from"./permissions.js";import{loadAllowRules as F,describeRules as G,parseRule as W,addProjectRule as J,removeProjectRule as Y,GLOBAL_SETTINGS_PATH as Q,projectSettingsPath as z}from"./settings.js";import{contextReport as V}from"./context-report.js";import{renderDiff as X}from"./diffview.js";import{loadHooks as Z,formatHooks as ee}from"./hooks.js";import{copyToClipboard as ne}from"./clipboard.js";import{toMarkdown as te,recapConversation as oe}from"./transcript.js";import{runDoctor as ie}from"./doctor.js";import{TOOL_SPECS as se}from"./tools.js";import{makeTodoTool as re}from"./todo.js";import{makeMemoryTool as ae}from"./memory.js";import{loadLocalSkills as ce,isUserInvocable as de}from"./skills.js";import{COMMAND_ALIASES as le,commandCategory as me,commandAliases as pe,matchCommands as ue,option as fe,settingMenu as ge,choose as he,commandRequest as we}from"./command-ui.js";const ye=/^(agent|workflow|datahuman|squad)-/,be="\n\n# 附加可访问目录(可用read_file/list_files/grep以绝对路径访问;写入仍限主目录)\n";function ve(e){const n=e.messages&&e.messages[0];if(!n||"system"!==n.role||"string"!=typeof n.content)return;const t=n.content.indexOf(be);let o=t>=0?n.content.slice(0,t):n.content;const i=e.extraDirs||[];i.length&&(o+=be+i.map(e=>"- "+e).join("\n")+"\n"),n.content=o}export const INIT_PROMPT="请为当前项目生成或完善HUICAI.md(给终端智能体看的项目说明):先用list_files/glob/read_file快速调研项目(技术栈、目录结构、构建/测试/运行命令、代码约定),再用write_file创建或更新HUICAI.md(若已存在则保留人工撰写的内容,增量完善)。内容包含:项目简介、关键目录、常用命令(构建/测试/运行)、代码风格与注意事项。控制在 100 行以内,用中文。";const $e={manual:"manual","请求批准":"manual","审批":"manual",acceptedits:"acceptEdits","accept-edits":"acceptEdits","接受编辑":"acceptEdits","编辑":"acceptEdits",auto:"auto","自动":"auto",yolo:"auto","无限制":"auto",plan:"plan","计划":"plan",sandbox:"sandbox","沙箱":"sandbox"};function ke(e){return e.permissions?.getMode?.()??(e.permissions?.isYolo?.()?"auto":"manual")}function Se(e,n){e.permissions?.setMode?e.permissions.setMode(n):(e.permissions?.setYolo?.("auto"===n),e.permissions?.setAcceptEdits?.("acceptEdits"===n),e.permissions?.setPlanMode?.("plan"===n),e.permissions?.setSandbox?.("sandbox"===n)),e.config&&(e.config.planMode="plan"===n,e.config.sandbox="sandbox"===n)}const je=[{name:"help",args:"[命令名]",desc:"分类浏览命令、别名与技能;/help 命令名查看用法",run(e,n){const t=listCommands(e.cwd);if(n){const o=n.replace(/^\//,""),i=t.find(e=>e.name===o||e.aliases?.includes(o));return void e.print(i?`/${i.name} ${i.args}\n${i.desc}`+(i.aliases.length?`\n兼容别名:${i.aliases.map(e=>"/"+e).join("、")}`:"")+(i.menu?"\n直接回车打开紧凑选择面板;方向键选择,Enter 确认,Esc 取消。":""):"未知命令。/help 查看完整列表。")}const o=new Map;for(const e of t)o.has(e.category)||o.set(e.category,[]),o.get(e.category).push(` /${e.name}${e.args?" "+e.args:""} ${e.desc}`);e.print([...o].map(([e,n])=>e+"\n"+n.join("\n")).join("\n\n")+"\n\n输入 / 搜索命令与技能 · /help <命令名> 查看详情和别名\n! <命令> 直接执行 shell 命令并把输出加入上下文\n@<文件路径> 在消息中引用文件内容")}},{name:"model",args:"[名称|auto]",desc:"选择账号可用模型;auto 自动选择,支持直接传入模型 ID",async run(e,n){if(n){if("auto"===n){try{const n=(e.commandModels||await we(u(e.config.baseUrl,e.config.apiKey),e.signal)).filter(e=>!/embed/i.test(e.id)&&"embedding"!==e.type&&!e.capabilities?.includes?.("embeddings")).map(e=>e.id).filter(e=>!ye.test(e));if(!n.length)return void e.print("账号下暂无可自动选择的对话模型,当前设置保持不变。");if(e.signal?.aborted)return;e.config.model=n.includes(r)?r:n[0]}catch(n){return void(e.signal?.aborted||e.print("自动选择失败:"+n.message+";当前设置保持不变。"))}if(e.signal?.aborted)return;return e.session&&(e.session.model=e.config.model),e.session?.id&&v(e.session),s({model:"auto"}),void e.print("Auto已选:"+e.config.model)}try{const t=(e.commandModels||await we(u(e.config.baseUrl,e.config.apiKey),e.signal)).map(e=>e.id);if(!t.includes(n))return void e.print(`当前账号无权使用模型「${n}」。\n 可用模型:${t.slice(0,12).join(", ")}`+(t.length>12?` …(共 ${t.length} 个,/models查看全部)`:"")+"\n 或用 /model auto自动选择可用模型。")}catch(n){if(e.signal?.aborted)return;e.print(`(未能校验模型可用性:${n.message})仍按输入切换;若无权限,平台会在调用时拒绝。`)}e.signal?.aborted||(e.config.model=n,e.session&&(e.session.model=n),s({model:n}),e.session?.id&&v(e.session),e.print("已切换模型:"+n))}else e.print("当前模型:"+(e.config.model||"(未设置)"))}},{name:"models",desc:"选择可用模型(普通输出宿主列出模型清单)",async run(e){try{const n=await we(u(e.config.baseUrl,e.config.apiKey),e.signal);e.print(n.map(e=>" "+e.id).join("\n")||"(无)")}catch(n){e.print("拉取失败:"+n.message)}}},{name:"usage",desc:"查看Token用量窗口(5小时/7天,按会员等级)",async run(e){const n=e=>"number"==typeof e&&Number.isFinite(e)?e:0,t=(e,t)=>(e=n(e),(t=n(t))<0?`${e.toLocaleString()} / 无限`:0===t?"该档未开放":`${e.toLocaleString()} / ${t.toLocaleString()} (${Math.min(100,Math.round(e/t*100))}%)`);try{const n=await f(e.config.baseUrl,e.config.apiKey);if(!n||"number"!=typeof n.used5h&&"number"!=typeof n.used7d)return void e.print("平台未返回用量数据(请确认平台已支持 /v1/usage)。");e.print(`Token用量窗口\n 近 5 小时:${t(n.used5h,n.limit5h)}\n 近 7 天 :${t(n.used7d,n.limit7d)}`+(!n.allowed&&n.message?"\n "+n.message:""))}catch(n){e.print("拉取用量失败:"+n.message)}}},{name:"login",desc:"重新登录(浏览器授权;无浏览器请退出后huicai login --paste)",async run(e){try{const{deviceLogin:n}=await import("./device.js");e.print("正在打开浏览器授权…");const t=await n(e.config.baseUrl);e.config.apiKey=t,s({apiKey:t}),e.print("登录成功。")}catch(n){e.print("登录未完成:"+n.message+"(可退出后用huicai login --paste粘贴令牌)")}}},{name:"logout",desc:"退出登录(清除本机保存的令牌)",run(e){e.config.apiKey="",s({apiKey:""}),e.print("已退出登录。后续对话需重新 /login或huicai login。")}},{name:"clear",desc:"开启新会话(旧会话仍可 --resume恢复)",run:()=>({action:"clear"})},{name:"compact",desc:"立即压缩上下文(摘要独立存储,不影响页面与会话原文)",async run(e){e.session.compaction||(e.session.compaction={});const n=await T(e.session.messages,e.config,{state:e.session.compaction});e.print(n>0?`已压缩:${n} 条较早消息已摘要(原对话完整保留,仅发给模型的上下文变小)`:"暂无可压缩的中段历史(会话较短或摘要失败)")}},{name:"memory",args:"[clear]",desc:"查看本项目长期记忆(clear清空)",async run(e,n){const{loadMemory:t,clearMemory:o,memoryPath:i}=await import("./memory.js");if("clear"===n)return void e.print(o(e.cwd)?"已清空本项目长期记忆。":"本项目暂无长期记忆。");const s=t(e.cwd);e.print(s?s+`\n(文件:${i(e.cwd)} · /memory clear清空)`:"本项目暂无长期记忆(模型会经memory_write自动记录值得跨会话记住的信息;配置memory:false可关闭)。")}},{name:"decisions",args:"[关键字|sync]",desc:"列出本项目决策图谱(决策追溯);sync与云端双向同步",async run(e,n){const t=String(n||"").trim().toLowerCase();if("sync"===t){const{baseUrl:n,apiKey:t}=e.config||{};if(!t)return void e.print("未登录,无法同步决策图(请先登录平台)。");const o=await import("./decisions.js"),i=await import("./api.js"),s=o.projectKey(e.cwd);let r=0;const a=o.unsyncedDecisions(e.cwd);if(a.length){const c=a.map(e=>({decisionId:e.id,title:e.title,status:e.status,payload:JSON.stringify(e),clientCreatedAt:e.createdAt}));try{r=await i.pushDecisions(n,t,s,c),o.markSynced(e.cwd,a.map(e=>e.id))}catch(n){return void e.print("决策同步失败(推送):"+n.message)}}let c=0;try{const r=await i.pullDecisions(n,t,s,o.getLastSync(e.cwd));c=o.mergeRemote(e.cwd,r),o.setLastSync(e.cwd,Date.now())}catch(n){return void e.print("决策同步失败(拉取):"+n.message)}return void e.print(`决策图同步完成:推送 ${r} 条,拉取新增 ${c} 条。`)}const{listDecisions:o}=await import("./decisions.js");let i=o(e.cwd);if(t&&(i=i.filter(e=>`${e.title} ${e.why} ${(e.tags||[]).join(" ")}`.toLowerCase().includes(t))),!i.length)return void e.print(t?"没有匹配的决策。":"本项目暂无决策记录(模型做取舍时会经record_decision自动记;也可用 /decision手动记)。");const s=i.slice(0,40).map(e=>{const n="draft"===e.status?"◇":"superseded"===e.status?"▽":"◆",t=(e.alternatives||[]).length?` · ${e.alternatives.length} 备选`:"",o=new Date(e.createdAt).toLocaleString("zh-CN",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}),i=e.anchor&&e.anchor.git?" · @"+String(e.anchor.git).slice(0,7):"";return` ${n} ${e.title}${t}\n ${o} · ${e.id}${i}`});e.print(`本项目决策(${i.length},◆正式 ◇草稿 ▽已取代):\n`+s.join("\n"))}},{name:"decision",args:"<what> | <why> | <备选1;备选2>",desc:"手动记录一个决策(| 分隔,备选用 ; 分隔)",async run(e,n){const t=String(n||"").trim();if(!t)return void e.print("用法:/decision <选择了什么> | <为什么> | <备选1;备选2>\n例:/decision向量检索用pgvector | 大库应用层余弦会超时 | 应用层余弦;外部向量库");const[o,i,s]=t.split("|").map(e=>(e||"").trim()),r=(s?s.split(";"):[]).map(e=>({option:(e||"").trim()})).filter(e=>e.option),{recordDecision:a}=await import("./decisions.js"),c=a(e.cwd,{what:o,title:o,why:i,alternatives:r,sessionId:e.config?.sessionId||""},{origin:"user"});e.print(`◆ 已记录决策:${c.title}${"draft"===c.status?"(无备选,存为待确认草稿)":""} · ${c.id}`)}},{name:"cost",desc:"查看本次运行累计token用量(及token预算 / 班子派发额度进度)",run(e){const n=e.usage.prompt+e.usage.completion,t=Number(e.config.budgetTokens)||0,o="function"==typeof e.crewOf?e.crewOf():null;e.print(`token用量:输入 ${e.usage.prompt} / 输出 ${e.usage.completion}`+(e.session.lastPromptTokens?` · 当前上下文 ~${e.session.lastPromptTokens} tok`:"")+(t>0?`\n预算:${n.toLocaleString()} / ${t.toLocaleString()} tokens(${Math.round(n/t*100)}%)`+(n>=t?" ⚠ 已超预算":n>=.8*t?" ⚠ 接近预算":""):"")+(o?"\n\n班子预算(P2-14):\n"+o.status():""))}},{name:"status",desc:"显示运行状态(版本/模型/目录/会话/MCP/权限规则)",run(e){const n=e.session,t=[...new Set((e.extraTools||[]).map(e=>e.spec?.function?.name||"").filter(e=>e.startsWith("mcp__")).map(e=>e.split("__")[1]))];let o="";try{o=i("git rev-parse --abbrev-ref HEAD",{cwd:e.cwd,stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{}const s=F(e.cwd),r=ke(e),a=[`版本huicai ${e.version||""}`,`模型 ${e.config.model}`,`平台 ${e.config.baseUrl}`,`目录 ${e.cwd}${o?`(git:${o})`:""}`,`会话 ${n.id} · ${b(n.messages)} 轮${n.lastPromptTokens?` · 上下文 ~${n.lastPromptTokens} tok`:""}`,`token输入 ${e.usage.prompt} / 输出 ${e.usage.completion}`,`模式 ${K[r]} ${B[r]}(${H[r]})`,`审批 允许规则 ${s.length} 条${e.config.goal?" · 🎯 目标已设":""}`,`MCP ${t.length?t.join(", "):"(未配置)"}`];e.print(a.join("\n"))}},{name:"resume",args:"[会话ID]",desc:"选择当前项目的历史会话并继续对话",async run(e,n){const t=k(e.config),o=h(e.cwd).filter(e=>!e.user||e.user===t);let i=n;if(!i&&e.select&&(i=await he(e,{title:"继续历史会话",current:e.session?.id,options:o.map(e=>fe(e.id,e.title,`${y(e.updatedAt)} · ${e.turns} 轮 · ${e.id}`))})),!i)return void(e.select||e.print("用法:/resume <会话ID>;/sessions 查看当前项目的会话。"));if(!o.some(e=>e.id===i))return void e.print("未找到当前项目与账号下的会话。");const s=w(i);s?e.resumeSession?(v(e.session),await e.resumeSession(s),e.print(`已恢复会话:${s.title}(${s.id})`)):e.print(`请在终端运行:huicai resume ${i}`):e.print("会话无法读取,当前对话保持不变。")}},{name:"sessions",desc:"列出当前目录的历史会话",run(e){const n=h(e.cwd);if(!n.length)return void e.print("该目录暂无历史会话。");const t=n.slice(0,15).map(n=>{const t=n.id===e.session.id?" ← 当前":"";return` ${y(n.updatedAt).padStart(8)} ${n.title} ${n.turns}轮 · ${n.id}${t}`});t.push("(/resume 选择恢复;也可退出后 huicai --resume)"),e.print(t.join("\n"))}},{name:"mode",args:"["+O.join("|")+"]",desc:"对话模式:请求批准/接受编辑/自动/计划/沙箱(自动=无限制,任何命令免审批)",run(e,n){const t=ke(e),o=String(n||"").trim();if(!o){const n=[`当前对话模式:${K[t]} ${B[t]}(${H[t]})`,"","可切换:"];for(const e of O)n.push(` ${e===t?"●":" "} ${K[e]} ${e.padEnd(11)} ${B[e]} — ${H[e]}`);return n.push(""," 用法:/mode <名称>(也可用 /yolo、/plan、/sandbox快捷切换)"),void e.print(n.join("\n"))}const i=$e[o]||$e[o.toLowerCase()];i?(Se(e,i),e.print(`对话模式 → ${K[i]} ${B[i]}(${H[i]})`+("plan"===i?"\n 只做只读调研出方案;/plan approve批准并分步执行,或 /mode auto直接放手执行。":"")+("auto"===i?"\n ⚠ 已开放所有权限:shell/Python/联网/自动化等任何命令都不再询问。":""))):e.print(`未知模式:${o}。可选:${O.join(" / ")}`)}},{name:"yolo",desc:"选择自动执行或请求批准;自动模式下命令免审批",run(e){const n="auto"!==ke(e);Se(e,n?"auto":"manual"),e.print(n?"⚡ 自动模式:开。已开放所有权限,任何命令(含shell/联网/自动化)都不再询问。":"🔒 自动模式:关。回到「请求批准」,写/执行按权限逐项审批。")}},{name:"cwd",desc:"显示当前工作目录",run(e){e.print(e.cwd)}},{name:"markdown",args:"[on|off]",desc:"切换助手回复的Markdown渲染(TUI富文本;关闭则显示原始文本,便于复制)",run(e,n){if(n&&!["on","off"].includes(n))return void e.print("用法:/markdown [on|off]");const t=n?"on"===n:!1===e.config.markdown;e.config.markdown=t,s({markdown:t}),e.print("Markdown渲染:"+(t?"开(下一条回复起生效)":"关(显示原始Markdown文本)"))}},{name:"init",desc:"让智能体调研项目并生成/完善HUICAI.md",run:()=>({prompt:INIT_PROMPT})},{name:"skills",desc:"选择并调用本地技能(Agent Skills);pull/push 使用 huicai skills",async run(e){const{loadLocalSkills:n}=await import("./skills.js"),t=n(e.cwd);t.length?e.print(t.map(e=>` ${e.name}${e.version?" v"+e.version:""}(${"project"===e.scope?"项目":"全局"}${e.files.length?` · ${e.files.length} 附件`:""})${e.description?" "+e.description.slice(0,60):""}`).join("\n")+"\n(模型会按描述经load_skill按需载入;本会话启动时已注入目录)"):e.print("暂无本地技能(.huicai/skills或 ~/.huicai/skills)。可用huicai skills pull从平台拉取。")}},{name:"mcp",desc:"列出已连接的MCP工具",run(e){const n=(e.extraTools||[]).filter(e=>(e.spec?.function?.name||"").startsWith("mcp__"));n.length?e.print(n.map(e=>` ${e.spec.function.name}${e.readOnly?"(只读)":""}`).join("\n")):e.print("未连接MCP服务。配置 ~/.huicai/mcp.json(通用mcpServers格式)后重启生效。")}},{name:"context",desc:"可视化当前上下文占用(各类别token估算)",run(e){const n=ye.test(String(e.config.model||""));let t=[];if(!n)try{const n=re({state:{list:[]},emit(){}}).spec,o=!1!==e.config.memoryEnabled?ae({cwd:e.cwd,emit(){}}).spec:null;t=[...se,n,...o?[o]:[],...(e.extraTools||[]).map(e=>e.spec).filter(Boolean)]}catch{t=se}e.print(V({messages:e.session.messages,compaction:e.session.compaction||{},specs:t,lastPromptTokens:e.session.lastPromptTokens||0,autoCompactTokens:Number(e.config.autoCompactTokens)||0,memberLimit:M(e.config),platform:n,lastUsage:e.session.lastUsage||null}))}},{name:"permissions",args:"[模式|rules|allow|deny|remove [规则]]",desc:"选择权限模式、查看或管理工具权限规则",run(e,n){const t=String(n||"").trim().split(/\s+/).filter(Boolean),o=(t.shift()||"").toLowerCase(),i=t.join(" ").trim();if($e[o])return je.find(e=>"mode"===e.name).run(e,$e[o]);if(!o||"rules"===o){const n=G(e.cwd),t=e=>e.length?e.map(e=>" "+e).join("\n"):" (无)";return void e.print("权限规则(deny优先于allow/yolo,对只读工具也生效)\n 全局 ~/.huicai/settings.json\n 允许:\n"+t(n.global.allow)+"\n 拒绝:\n"+t(n.global.deny)+"\n 项目 .huicai/settings.json\n 允许:\n"+t(n.project.allow)+"\n 拒绝:\n"+t(n.project.deny)+"\n"+(e.permissions?.policySummary?" ── 声明式策略(policy.json,只做减法,拒绝先于一切)──\n "+e.permissions.policySummary().split("\n").join("\n ")+"\n":"")+" 用法:/permissions allow <规则> · /permissions deny <规则> · /permissions remove <规则>\n 规则语法:`工具名` 或 `工具名(模式*)`,如run_bash(git *)、edit_file、read_file(**/.env*)")}if("allow"!==o&&"deny"!==o){if("remove"===o||"rm"===o){if(!i)return void e.print("用法:/permissions remove <规则原文>(仅移除项目设置里的规则)");const n=Y(e.cwd,i);return e.permissions?.reloadRules?.(),void e.print(n.removed?`已移除规则:${i}`:`未在项目设置中找到该规则:${i}`)}e.print("未知子命令。用法:/permissions [allow|deny|remove] <规则>")}else{if(!i||!W(i))return void e.print("规则无效。示例:run_bash(git *)、edit_file、fetch_url(https://a.com*)");try{const n=J(e.cwd,o,i);e.permissions?.reloadRules?.(),e.print(`已添加${"deny"===o?"拒绝":"允许"}规则:${i} → ${n}`)}catch(n){e.print("写入失败:"+n.message)}}}},{name:"diff",desc:"查看未提交改动(git)与本会话智能体改过的文件",run(e){e.print(X(e.cwd,e.session.messages))}},{name:"pr",args:"[commit|address]",desc:"自动提交并开PR(生成规范commit→建分支→push→gh开PR);address=按review评论自动改",async run(e,n){const t=String(n||"").trim().toLowerCase();if("address"===t||"comments"===t)return{prompt:"请处理当前分支/PR的review评论:用 `gh pr view --comments`(无PR则先 `gh pr status`)读取评论,逐条理解并修改代码解决,然后git add/commit(规范信息)+ git push;完成后逐条说明如何处理的。若未装gh或无PR,先告知。"};const{runPrFlow:o}=await import("./gitflow.js");try{const n=await o({cwd:e.cwd,config:e.config,emit:n=>e.print(n),push:"commit"!==t,openPr:"commit"!==t});if(!n.ok)return void e.print("✗ "+n.error);e.print(n.pr?"✅ 已开PR:"+n.pr:"✅ 完成(分支 "+n.branch+")"+(n.note?":"+n.note:""))}catch(n){e.print("PR流程出错:"+n.message)}}},{name:"plan",args:"[on|off|approve]",desc:"计划模式:只读调研出方案→/plan approve批准并分步执行",async run(e,n){const t=String(n||"").trim().toLowerCase();if("approve"===t||"go"===t){Se(e,"manual"),e.print("计划已批准,开始分步执行。");try{const{recordDecision:n}=await import("./decisions.js"),t=e.session?.messages||[];let o="";for(let e=t.length-1;e>=0;e--)if("assistant"===t[e].role&&"string"==typeof t[e].content){o=t[e].content;break}const i=(o.split("\n").find(e=>e.trim())||"批准的执行计划").replace(/^[#>\-*\s]+/,"").slice(0,120);n(e.cwd,{what:"批准并执行方案:"+i,why:o?o.slice(0,2e3):"用户在计划模式下评审后批准执行。",tags:["计划"],status:"active",sessionId:e.config?.sessionId||""},{origin:"user"})}catch{}return{prompt:"计划已获批准。现在请按你刚才制定的计划(todo列表)逐步执行:每完成一步用todo_write更新状态并简要自检;遇到与计划不符及时调整并说明;全部完成后总结改了什么、以及如何验证(可用 /verify让我自动跑验证)。"}}if(t&&!["on","off"].includes(t))return void e.print("用法:/plan [on|off|approve]");const o="on"===t||"off"!==t&&"plan"!==ke(e);Se(e,o?"plan":"manual"),e.print(o?"计划模式:开。只做只读调研并用todo_write产出分步方案;写/执行会被拦截。\n 方案满意后用 /plan approve批准并自动分步执行;或 /plan off手动执行。":"计划模式:关。恢复正常执行(写/执行仍按权限审批)。")}},{name:"auto",args:"<目标>",desc:"自动规划并自动执行:只读调研出分步计划,确认「是否继续?」后自动逐步执行",run(e,n){const t=String(n||"").trim();if(t)return{action:"autorun",goal:t};e.print("用法:/auto <目标>\n 先自动规划(只读调研 + todo分步计划),随后询问「是否继续?1、是 2、是,并允许在当前对话中后续相同操作不再询问 3、否」,确认后自动逐步执行并自检。")}},{name:"rewind",args:"[N [chat|code|both]]",desc:"检查点回滚:无参列出;/rewind N回退对话,加code/both一并回滚文件",run(e,n){const t=e.checkpoints;if(!t||!t.list.length)return void e.print("暂无检查点(每个用户回合前自动记录,发生对话后可用)。");const o=String(n||"").trim().split(/\s+/).filter(Boolean);if(!o.length){const n=["检查点(回滚到某个回合之前)"];for(const e of t.list)n.push(` #${e.id} ${y(e.time)} ${e.label}`+(e.journal.length?` · 改动 ${e.journal.length} 处`:""));return n.push(" 用法:/rewind N(仅回退对话,安全)· /rewind N code(回滚文件)· /rewind N both"),n.push(" 提示:code/both会覆盖当前文件,建议先 /diff查看。"),void e.print(n.join("\n"))}const i=Number(o[0]);if(!Number.isFinite(i))return void e.print("用法:/rewind N [chat|code|both]");const s=["chat","code","both"].includes(o[1])?o[1]:"chat",r=t.restore(i,s,{session:e.session,cwd:e.cwd});if(!r.ok)return void e.print(r.error);if(r.conversation&&"function"==typeof e.onRewind)try{e.onRewind(e.session.messages.length)}catch{}const a=[];r.conversation&&a.push("对话已回退到该回合之前"),r.filesRestored.length&&a.push(`恢复文件 ${r.filesRestored.length} 个`),r.filesDeleted.length&&a.push(`删除新建文件 ${r.filesDeleted.length} 个`),r.filesSkipped.length&&a.push(`跳过(过大) ${r.filesSkipped.length} 个`),r.filesFailed.length&&a.push(`失败 ${r.filesFailed.length} 个`);try{v(e.session)}catch{}return e.print("已回滚到 #"+i+":"+(a.join(";")||"无变化")+(r.filesFailed.length?"\n 失败:"+r.filesFailed.join(";"):"")),r.conversation?{action:"refresh"}:void 0}},{name:"hooks",desc:"查看已配置的工具事件钩子(Pre/PostToolUse/UserPromptSubmit/Stop)",run(e){e.print(ee(Z(e.cwd)))}},{name:"copy",args:"[N]",desc:"复制助手回复到剪贴板(默认最后一条;N=倒数第N条)",async run(e,n){const t=e.session.messages.filter(e=>"assistant"===e.role&&"string"==typeof e.content&&e.content.trim());if(!t.length)return void e.print("还没有可复制的助手回复。");const o=Math.max(1,parseInt(n,10)||1),i=t[t.length-o];if(!i)return void e.print(`只有 ${t.length} 条助手回复。`);const s=await ne(i.content);e.print(s.ok?`已复制${o>1?`倒数第 ${o} 条`:"最后一条"}回复到剪贴板(${i.content.length} 字)`:`复制失败(未找到 ${s.tool})。以下为该回复内容:\n\n${i.content}`)}},{name:"export",args:"[文件路径]",desc:"导出当前对话为Markdown文件",run(e,o){const i=te(e.session),s=String(o||"").trim()||`huicai-对话-${(new Date).toISOString().slice(0,19).replace(/[:T]/g,"-")}.md`,r=t.isAbsolute(s)?s:t.join(e.cwd,s);try{n.writeFileSync(r,i,"utf8"),e.print("已导出对话到 "+r)}catch(n){e.print("导出失败:"+n.message)}}},{name:"rename",args:"<新标题>",desc:"重命名当前会话",run(e,n){const t=String(n||"").trim();if(t){e.session.title=t.slice(0,80);try{v(e.session)}catch{}e.print("会话已重命名为:"+e.session.title)}else e.print("当前标题:"+(e.session.title||"(未命名)")+"\n 用法:/rename <新标题>")}},{name:"recap",desc:"用一句话总结当前对话进展(直连原始模型,不写入对话)",async run(e){if(e.session.messages.some(e=>"system"!==e.role))try{e.print("小结:"+await oe(e.session.messages,e.config))}catch(n){e.print("生成小结失败:"+n.message)}else e.print("对话还没有内容。")}},{name:"distill",desc:"从当前对话提炼项目知识,写入长期记忆(供后续会话自动加载)",async run(e){if(!1===e.config.memoryEnabled)return void e.print("长期记忆已关闭(config.memory=false)。");if(!e.session.messages.some(e=>"system"!==e.role))return void e.print("对话还没有内容。");const{distillMemory:n}=await import("./transcript.js"),{loadMemory:t,appendMemory:o}=await import("./memory.js");try{const i=await n(e.session.messages,e.config,t(e.cwd));if(!i.length)return void e.print("未提炼出值得长期记住的新知识。");let s=0;for(const n of i)o(e.cwd,n)&&s++;e.print(`已写入 ${s} 条项目知识到长期记忆:\n`+i.map(e=>" · "+e).join("\n"))}catch(n){e.print("提炼失败:"+n.message)}}},{name:"doctor",desc:"本机环境体检(登录/网络/模型/git/工作目录/钩子);加capabilities实测渠道能力",async run(e,n){try{if(/^(capabilit(y|ies)|能力)\b/i.test(String(n||"").trim())){const{probeAll:t,formatProbes:o}=await import("./capability-probe.js"),i=String(n).trim().replace(/^\S+\s*/,""),s=(/--embed-model[= ]([^\s]+)/.exec(i)||[])[1]||"",r=(/--vision-model[= ]([^\s]+)/.exec(i)||[])[1]||e.config.visionModel;return e.config.baseUrl&&e.config.apiKey?(e.print("正在实测(会真的向平台发几个最小请求)…"),void e.print(o(await t({baseUrl:e.config.baseUrl,apiKey:e.config.apiKey,model:e.config.model,visionModel:r,embedModel:s})))):void e.print("未登录平台,无法实测渠道能力(/login或huicai login)。")}e.print(await ie(e))}catch(n){e.print("体检失败:"+n.message)}}},{name:"config",desc:"打开设置菜单;查看当前配置与文件路径(密钥脱敏)",run(e){const n=e.config,t=Number(n.autoCompactTokens);var o;e.print(["配置"," 文件 "+a," 平台 "+n.baseUrl," 密钥 "+(o=n.apiKey,o?o.slice(0,4)+"…"+o.slice(-4)+`(${o.length}位)`:"(未设置)")," 模型 "+n.model," 推理强度 "+(n.reasoningEffort||"off")," 自动压缩 "+(t<0?"关闭":(t>0?t:I).toLocaleString()+" tokens"+(t>0?"":"(默认)"))," 上下文窗口 "+E(n)," 记忆 "+(!1===n.memoryEnabled?"关":"开")," 联网搜索 "+(c()?"开":"关")+"(平台:"+p(m(n))+";/websearch查看与切换)"," Markdown "+(!1===n.markdown?"关":"开")," 全局设置 "+Q," 项目设置 "+z(e.cwd)," (多数项有对应斜杠命令直接改:/model /effort /autocompact /permissions …)"].join("\n"))}},{name:"effort",args:"[minimal|low|medium|high|off]",desc:"设置推理强度(reasoning_effort透传;模型不支持则忽略)",run(e,n){const t=String(n||"").trim().toLowerCase();t?["minimal","low","medium","high","off"].includes(t)?(e.config.reasoningEffort="off"===t?"":t,s({reasoningEffort:e.config.reasoningEffort}),e.print("推理强度:"+(e.config.reasoningEffort||"off")+"(下一条消息起生效;模型不支持则被忽略)")):e.print("取值:minimal | low | medium | high | off"):e.print("当前推理强度:"+(e.config.reasoningEffort||"off")+"\n 用法:/effort minimal|low|medium|high|off")}},{name:"autocompact",args:"[N|default|off|status]",desc:"自动压缩上下文的触发阈值(token数;off关闭)",run(e,n){const t=String(n||"").trim().toLowerCase(),o=Number(e.config.autoCompactTokens)||0;if(!t||"status"===t)return void e.print("自动压缩:"+(o<0?"已关闭":o>0?`阈值 ${o.toLocaleString()} tokens`:`默认阈值 ${I.toLocaleString()} tokens`)+"\n上下文窗口:"+E(e.config)+"\n /autocompact <N> 设定 · /autocompact off关闭 · /autocompact status查看");if("default"===t)return e.config.autoCompactTokens=0,s({autoCompactTokens:0}),void e.print("已恢复默认自动压缩阈值。");if("off"===t)return e.config.autoCompactTokens=-1,s({autoCompactTokens:-1}),void e.print("已关闭自动压缩(仍可 /compact手动压缩)。");const i=/^\d+$/.test(t)?Number(t):NaN;!Number.isSafeInteger(i)||i<1e3?e.print("请给 ≥1000 的token阈值,或off / status。"):(e.config.autoCompactTokens=i,s({autoCompactTokens:i}),e.print("自动压缩阈值设为 "+i.toLocaleString()+" tokens。"))}},{name:"review",desc:"审查当前未提交改动(找bug与可简化处)",run:()=>({prompt:"请审查当前工作区的未提交改动:先用run_bash跑 `git diff`(及 `git status`)查看改了什么,聚焦正确性bug、边界与错误处理、可复用/可简化处、安全隐患。若本地有code-review技能可load_skill参照其方法。逐条给出:问题、定位(文件:行)、修改建议,按严重度排序;不确定的地方说明。"})},{name:"security-review",desc:"对当前改动做安全审查",run:()=>({prompt:"请对当前工作区改动做安全审查:注入(SQL/命令/路径穿越)、越权、密钥/凭据泄漏、SSRF、不安全依赖、反序列化、XSS等。先run_bash跑 `git diff` 查看改动。若本地有security-audit技能可load_skill参照。给出风险点、可利用场景、影响与修复建议,按严重度排序。"})},{name:"scan",args:"[fix]",desc:"安全/质量扫描(自动选可用的npm audit/semgrep/trivy/gitleaks等);fix=让智能体逐条修",async run(e,n){const{detectScanners:t,runScanners:o}=await import("./scan.js"),i=t(e.cwd);if(!i.length)return void e.print("未检测到可用扫描器。可安装semgrep / trivy / gitleaks,或项目含package.json(npm audit)等。");if("fix"===String(n||"").trim().toLowerCase())return{prompt:"请对本项目做安全/质量扫描并修复:依次运行并理解这些扫描器的输出——"+i.map(e=>"`"+e.command+"`("+e.why+")").join("、")+";对发现的问题逐个修复(升级/更换依赖、修代码、移除硬编码密钥等),修完重跑对应扫描确认清零;最后汇总修了什么。"};e.print("运行扫描器:"+i.map(e=>e.name).join(", ")+"(可能较慢)…");const s=await o(e.cwd,i);for(const n of s)e.print(`\n── ${n.name}(${0===n.code?"✅ 无高危":"⚠ 有发现"})──`),e.print(n.output.split("\n").slice(0,20).join("\n"));e.print("\n(/scan fix让智能体逐条修复)")}},{name:"cover",desc:"覆盖率驱动测试生成:跑覆盖率→定位未覆盖的改动→补测试→复测",run:()=>({prompt:"请做覆盖率驱动的测试补全:\n1) 跑测试覆盖率(按项目选:npm test -- --coverage / jest --coverage / pytest --cov / go test -cover / mvn test等);\n2) 重点看**最近改动**且覆盖率低或未覆盖的函数与分支;\n3) 为这些薄弱处补写有意义的测试(覆盖正常/边界/错误路径),遵循项目既有测试风格与框架;\n4) 重跑测试与覆盖率,确认通过且覆盖率提升;最后汇报补了哪些测试、覆盖率变化。"})},{name:"verify",args:"[验证命令]",desc:"修到通过:跑测试/构建,失败自动定位修复并重跑,直到通过或达上限",run(e,n){let t=String(n||"").trim();if(!t){if(t=q(e.cwd)||"",!t)return void e.print("未能自动探测验证命令。用法:/verify <命令>(如 /verify npm test)");e.print("自动探测到验证命令:"+t)}return{action:"autofix",command:t,maxIters:Math.min(10,Math.max(1,Number(e.config.verifyMaxIters)||5))}}},{name:"inspect",args:"[路径]",desc:"代码体检:逻辑/注释/可读性/结构/测试/安全七类机器排查,必修项交给智能体逐条修",async run(e,n){const{makeCodeTools:t}=await import("./code-tools.js"),o=t({cwd:e.cwd,config:e.config}).find(e=>"code_inspect"===e.spec.function.name),i=String(n||"").trim();e.print("正在体检"+(i?`:${i}`:"整个工作目录")+"…");const s=await o.run(i?{path:i}:{});if(e.print(s),!/未发现问题|必修 0/.test(s))return{prompt:"下面是刚跑出来的代码体检结论,请**逐条修掉必修项**(建议项挑真正该改的改):\n\n"+s+"\n\n改完用code_inspect复检确认必修清零再回话。确认某条是误报就加 `code-inspect-ignore: 规则名 理由` 并说明为什么,不要默默跳过。"}}},{name:"simplify",desc:"审查改动的可复用/简化/抽象层级并直接清理(只清理不找bug)",run:()=>({prompt:"请只做质量清理(不找bug):审查最近改动的复用(是否重复造轮子、可否复用现有工具/函数)、简化(是否有更直接的写法)、效率、以及抽象层级是否合适;把确定的改进直接用edit_file应用,保持与项目既有风格一致,改后跑测试/构建自检。若本地有refactor-safely技能可load_skill参照。最后说明改了什么、为什么。"})},{name:"insights",desc:"本机会话使用洞察(数量/轮数/活跃度/按项目·模型分布)",run(e){e.print(N(A(h(null),{cwd:e.cwd})))}},{name:"execsandbox",args:"[off | workspace-write | read-only]",desc:"执行沙箱:把run_bash/run_python关进内核约束,只能写工作区(与 /sandbox权限模式正交,可叠加)",run(e,n){const t=String(n||"").trim(),o=String(e.config.execSandbox||"off"),i=_();if(!t)return void e.print(`执行沙箱:${o}\n本机后端:${i.kind||"不可用("+i.reason+")"}\n /execsandbox workspace-write只能写工作区与临时目录(推荐)\n /execsandbox read-only任何文件都不许写\n /execsandbox off关闭(默认)\n说明:它约束的是「命令能动哪些文件」,与 /sandbox(决定「工具能不能被调用」)是两件事,可同时开。`);const r="on"===t?"workspace-write":t;["off","workspace-write","read-only"].includes(r)?"off"===r||i.kind?(e.config.execSandbox=r,s({execSandbox:r}),e.print("off"===r?"已关闭执行沙箱。":`已开启执行沙箱:${r}(后端 ${i.kind})。此后run_bash/run_python只能写工作区与临时目录。`)):e.print(`无法开启:${i.reason}\n(本机没有可用的沙箱后端,开了也不会生效,故不予开启。)`):e.print("用法:/execsandbox [off | workspace-write | read-only]")}},{name:"toolmode",args:"[native | both | code]",desc:"工具呈现方式:让模型写程序来编排工具调用(批量改文件/全仓统计这类多步活儿更快、更省上下文)",run(e,n){const t=String(n||"").trim(),o=String(e.config.toolMode||"native");t?["native","both","code"].includes(t)?(e.config.toolMode=t,s({toolMode:t}),e.print("native"===t?"已回到逐个调用工具(run_program已从工具表移除)。":`已切换为 ${t}:模型可以用run_program写程序编排工具${"code"===t?";读写/检索类工具已从它的工具表隐去":""}。`)):e.print("用法:/toolmode [native | both | code]"):e.print(`工具呈现方式:${o}\n /toolmode native逐个调用工具(默认,一切照旧)\n /toolmode both额外给一件run_program,模型自己判断何时写程序编排\n /toolmode code再把读写/检索类工具从模型工具表里隐去,一律走程序\n说明:程序里的每次工具调用照常走权限审批与钩子,不是绕过审批的通道;\n但一段程序是「一次批准、连做多步」,中途不便干预——这是它省时间的代价,请按任务性质选。`)}},{name:"history",args:"<关键词> [--here] [--legacy]",desc:"按内容检索历史对话(中英皆可;--here只看当前项目;--legacy连登录前的旧会话一起找)",run(e,n){const t=String(n||"").trim();if(!t)return void e.print("用法:/history <关键词> (按对话内容检索历史;--here只看当前项目;--legacy连登录前无用户标记的旧会话一起找)\n例:/history nginx配置 · /history上次那个报错");const o=/(^|\s)--here(\s|$)/.test(t),i=/(^|\s)--legacy(\s|$)/.test(t),s=t.replace(/(^|\s)--(here|legacy)(\s|$)/g," ").replace(/\s+/g," ").trim(),r=S(s,{k:10,userKey:k(e.config),cwd:o?e.cwd:void 0,includeUnowned:i});e.print(j(r,s))}},{name:"goal",args:"[目标 | status | pause | resume | limit <N> | clear]",desc:"设定长目标并自动续跑(到达/受阻/触顶才停);pause暂停 · resume继续 · limit改轮次上限 · clear清除",run(e,n){const t=String(n||"").trim(),o=x.viewGoal(e.session);if(!t||"status"===t)return e.print(o?`目标:${o.objective}\n状态:${{active:"进行中",paused:"已暂停",blocked:"受阻",complete:"已完成"}[o.phase]||o.phase}${o.armed?"(自动续跑已武装)":"(未武装,不会自动续跑)"}\n已跑 ${o.rounds}/${o.maxRounds} 轮`+(o.blockedReason?`\n卡点:${o.blockedReason}`:"")+"\n /goal pause暂停 · /goal resume继续 · /goal limit <N> 改上限 · /goal clear清除":"未设定目标。用法:/goal <目标>(设定后会自动逐轮推进,直到达成/受阻/触顶)");x.setRoundKind(e.session?.id,"human");const i=o?.rev;let s;return s="clear"===t?x.clearGoal(e.session):"pause"===t?x.pauseGoal(e.session,{rev:i}):"resume"===t?x.resumeGoal(e.session,{rev:i}):/^limit\s+\d+$/.test(t)?x.editGoal(e.session,{rev:i,maxRounds:Number(t.split(/\s+/)[1])}):x.createGoal(e.session,{objective:t}),s.ok?"clear"===t?e.print("已清除目标(不再自动续跑)。"):(v(e.session),void e.print(("pause"===t?"已暂停。":"resume"===t?"已恢复,下一轮起继续自动推进。":/^limit/.test(t)?"已更新轮次上限。":"已设定目标,本轮结束后开始自动推进。")+"\n"+`已跑 ${s.goal.rounds}/${s.goal.maxRounds} 轮`)):e.print(s.error)}},{name:"btw",args:"<问题>",desc:"问一个旁支问题(带当前上下文,但不写入对话、不调用工具)",async run(e,n){const t=String(n||"").trim();if(!t)return void e.print("用法:/btw <问题>(快速旁支问答,不影响主对话)");const o=ye.test(String(e.config.model||""))?e.config.compactModel||r:e.config.model;try{const n=C(e.session.messages,e.session.compaction||{}),i=await g({baseUrl:e.config.baseUrl,apiKey:e.config.apiKey,model:o,client:e.config.client,sessionId:e.config.sessionId,messages:[...n,{role:"user",content:"(这是一个旁支问题,请结合上文直接简答,不要调用工具、不要展开无关内容)\n"+t}]});e.print("↪ "+(i.content||"(无回复)").trim())}catch(n){e.print("旁支问答失败:"+n.message)}}},{name:"ide",desc:"查看IDE集成状态(编辑器上下文与内联审阅)",run(e){const n=!!process.env.HUICAI_IDE_PORT,t=process.env.HUICAI_IDE_FILE,o=process.env.HUICAI_IDE_SELECTION,i=process.env.HUICAI_IDE_DIAGNOSTICS,s=["IDE集成"];s.push(" 内联审阅 "+(n?"已连接(write/edit交编辑器做diff审阅)":"未连接(CLI直接写盘)")),s.push(" 客户端 "+(e.config.client||"cli")),s.push(" 当前文件 "+(t||"(无)")),s.push(" 选区 "+(o?o.length+" 字":"(无)")),s.push(" 诊断 "+(i?"有(来自Problems面板)":"(无)")),n||s.push(" (在VS Code/IDEA的huicai面板或其启动的终端里运行可启用内联审阅与编辑器上下文)"),e.print(s.join("\n"))}},{name:"branch",args:"[标题]",desc:"把当前对话另存为一个可 --resume的分支副本(当前会话继续)",run(e,n){const t=e.session;if(!t.messages.some(e=>"system"!==e.role))return void e.print("当前对话为空,无需分支。");const o=$({cwd:e.cwd,model:t.model,messages:t.messages.map(e=>({...e}))});try{o.compaction=JSON.parse(JSON.stringify(t.compaction||{}))}catch{o.compaction={}}o.title=(String(n||"").trim()||t.title||"对话").slice(0,70)+"(分支)",o.lastPromptTokens=t.lastPromptTokens||0;try{v(o),v(t)}catch{}e.print(`已保存分支副本「${o.title}」(${o.id})。\n 当前会话继续;分支可用huicai --resume恢复到此刻状态。`)}},{name:"theme",args:"[default|mono]",desc:"输出主题:default彩色 / mono无色",run(e,n){const t=String(n||"").trim().toLowerCase();t?["default","mono"].includes(t)?(e.config.theme=t,P(t),s({theme:t}),e.print("主题:"+t+("mono"===t?"(已关闭颜色)":"(彩色)"))):e.print("取值:default | mono"):e.print("当前主题:"+(e.config.theme||"default")+"\n /theme default | mono(终端配色由终端掌控;mono=关闭颜色,便于复制/管道)")}},{name:"color",args:"["+R.join("|")+"]",desc:"设置强调色(提示符/高亮)",run(e,n){const t=String(n||"").trim().toLowerCase();t?D(t)?(e.config.accentColor=t,s({accentColor:t}),e.print("强调色:"+t)):e.print("可选:"+R.join(" / ")):e.print("当前强调色:"+U()+"\n 可选:"+R.join(" / "))}},{name:"tui",args:"[auto|rich|plain]",desc:"交互渲染器偏好(下次启动生效)",run(e,n){const t=String(n||"").trim().toLowerCase();t?["auto","rich","plain"].includes(t)?(e.config.tui=t,s({tui:t}),e.print("渲染器偏好:"+t+"(下次启动生效)")):e.print("取值:auto | rich | plain"):e.print("当前渲染器偏好:"+(e.config.tui||"auto")+"\n auto(TTY下富TUI)| rich | plain(下次启动生效;本次可加 --tui/--plain覆盖)")}},{name:"advisor",args:"[模型id|off]",desc:"设置顾问模型(更强模型):智能体遇难题可ask_advisor咨询第二意见",async run(e,n){const t=String(n||"").trim();if(!t)return void e.print(e.config.advisorModel?"当前顾问模型:"+e.config.advisorModel+"(智能体可用ask_advisor咨询)":"未设置顾问模型。用法:/advisor <模型id>(设一个更强的模型;/models看可用)· /advisor off关闭");const o="off"===t?"":t;if(e.config.advisorModel=o,s({advisorModel:o}),Array.isArray(e.extraTools)){const n=e.extraTools.findIndex(e=>"ask_advisor"===e.spec?.function?.name);if(n>=0&&e.extraTools.splice(n,1),o){const{makeAdvisorTool:n}=await import("./advisor.js"),t=n({config:e.config});t&&e.extraTools.push(t)}}e.print(o?"顾问模型已设为 "+o+"(ask_advisor工具已可用)":"已关闭顾问模型。")}},{name:"automodel",args:"[强模型id|off]",desc:"按任务自动选模型:复杂回合自动升配到该强模型,其余用当前模型",run(e,n){const t=String(n||"").trim();if(!t)return void e.print(e.config.autoModelStrong?"自动升配:开(复杂回合→"+e.config.autoModelStrong+",其余→"+e.config.model+")":"未开启。用法:/automodel <强模型id>(复杂回合自动用它;简单回合仍用当前便宜模型)· /models看可用");const o="off"===t.toLowerCase()?"":t;e.config.autoModelStrong=o,s({autoModelStrong:o}),e.print(o?"按任务自动选模型:开(复杂回合→"+o+",其余→"+e.config.model+")":"已关闭自动升配。")}},{name:"embed",args:"[嵌入模型id|off]",desc:"search_code向量语义重排:设一个嵌入模型走 /v1/embeddings重排(空/off=纯BM25)",run(e,n){const t=String(n||"").trim();if(!t)return void e.print(e.config.embeddingModel?"search_code向量重排:开(嵌入模型 "+e.config.embeddingModel+")":"未启用向量重排(纯BM25,离线可用)。用法:/embed <嵌入模型id>(需平台有该嵌入模型)· /embed off");const o="off"===t.toLowerCase()?"":t;e.config.embeddingModel=o,s({embeddingModel:o}),e.print(o?"search_code向量语义重排:开(嵌入模型 "+o+";下次检索生效,BM25 召回后按向量相似度重排)":"已关闭向量重排(回到纯BM25)。")}},{name:"crew",args:"[子任务数] [advisor次数]",desc:"查看/放宽班子编排额度(P2-14;撞顶后只有你能放行)",run(e,n){const t="function"==typeof e.crewOf?e.crewOf():null;if(!t)return void e.print('班子预算未开启。在 ~/.huicai/config.json设 "crewBudget": true,或HUICAI_CREW_BUDGET=1。');const o=String(n||"").trim().split(/\s+/).filter(Boolean);if(!o.length)return void e.print(t.status());let i=0,s=0;for(let e=0;e<o.length;e++)/^adv(isor)?$/i.test(o[e])?(s=Number(o[e+1])||0,e++):i||(i=Number(o[e])||0);i||s?e.print("已放宽额度:\n"+t.grant({subtasks:i,advisories:s})):e.print("用法:/crew 6(再放 6 个子任务的派发额度) · /crew advisor 3(再放 3 次顾问) · /crew(看进度)")}},{name:"budget",args:"[token数|off|status]",desc:"设置本项目token预算(软提醒;/cost看进度)",run(e,n){const t=String(n||"").trim().toLowerCase(),o=(e.usage?.prompt||0)+(e.usage?.completion||0);if(!t||"status"===t){const n=Number(e.config.budgetTokens)||0;return void e.print(n>0?`token预算:${o.toLocaleString()} / ${n.toLocaleString()}(${Math.round(o/n*100)}%)`:`未设预算(本次已用 ${o.toLocaleString()} tokens)。/budget <token数> 设定`)}if("off"===t)return e.config.budgetTokens=0,s({budgetTokens:0}),void e.print("已取消预算。");const i=/^\d+$/.test(t)?Number(t):NaN;!Number.isSafeInteger(i)||i<1e3?e.print("请给 ≥1000 的token数,或off / status。"):(e.config.budgetTokens=i,s({budgetTokens:i}),e.print("token预算设为 "+i.toLocaleString()+"(软提醒;/cost显示进度)。"))}},{name:"debug",args:"[on|off]",desc:"调试输出(每轮打印模型/耗时/token)",run(e,n){const t=String(n||"").trim().toLowerCase();if(t&&!["on","off"].includes(t))return void e.print("用法:/debug [on|off]");const o="on"===t||"off"!==t&&!e.config.debug;e.config.debug=o,s({debug:o}),e.print("调试输出:"+(o?"开(下一条消息起每轮打印 [debug] 行)":"关"))}},{name:"websearch",args:"[on|off|status]",desc:"联网搜索开关(统一走平台联网搜索;关闭后本端不再联网检索,含平台智能体的服务端联网)",async run(e,n){const t=String(n||"").trim().toLowerCase();if("on"===t||"off"===t){const n=await d("on"===t);if(e.config&&(e.config.webSearch=n),n&&!c())return void e.print("已记录为「开」,但环境变量HUICAI_WEB_SEARCH=0 强制关闭了联网搜索,当前仍不可用(去掉该环境变量后生效)。");if(!n)return void e.print(["联网搜索:关。"," · 本机web_search工具即刻从工具表消失,fetch_url也不再抓搜索引擎的检索页;"," · 平台智能体 / 工作流 / 专家团 / 数字人在本端的对话里也不会再做服务端联网搜索。"," (与桌面端 / IDEA共用同一开关 ~/.huicai/config.json,正在运行的会话下一次调用即生效;/websearch on恢复)"].join("\n"));const o=await l(e.config,{force:!0});return void e.print("联网搜索:开。平台状态:"+p(o)+("unavailable"===o.state?"\n 平台侧当前不可用,需管理员在管理后台「联网搜索」页启用并配置提供商。":""))}if(t&&"status"!==t)return void e.print("用法:/websearch [on|off|status]");const o=c(),i=e.config?.apiKey?await l(e.config,{force:!0}):m(e.config);e.print(["联网搜索"," 本端开关 "+(o?"开":"关")+("0"===process.env.HUICAI_WEB_SEARCH?"(环境变量HUICAI_WEB_SEARCH=0 强制关闭)":"")," 平台状态 "+(e.config?.apiKey?p(i):"未登录(联网搜索统一由平台提供)")," 说明 本端不直连任何搜索引擎,提供商与密钥由管理员在管理后台统一配置。"," 关闭后本机web_search与平台智能体的服务端联网都不会再被调用。/websearch on|off切换"].join("\n"))}},{name:"sandbox",args:"[on|off]",desc:"沙箱模式:禁shell与联网(仍可读写本地文件)",run(e,n){const t=String(n||"").trim().toLowerCase();if(t&&!["on","off"].includes(t))return void e.print("用法:/sandbox [on|off]");const o="on"===t||"off"!==t&&"sandbox"!==ke(e);Se(e,o?"sandbox":"manual"),e.print(o?"沙箱模式:开。run_bash与fetch_url将被拒绝;本地文件读写照常。":"沙箱模式:关(写/执行仍按权限审批)。")}},{name:"reload-skills",desc:"从磁盘重新加载技能(新增/改名技能生效;改正文已实时生效)",async run(e){const{makeSkillTools:n,loadLocalSkills:t}=await import("./skills.js");if(Array.isArray(e.extraTools)){for(let n=e.extraTools.length-1;n>=0;n--){const t=e.extraTools[n]?.spec?.function?.name;"load_skill"!==t&&"read_skill_file"!==t||e.extraTools.splice(n,1)}e.extraTools.push(...n(e.cwd))}const o=t(e.cwd);e.print(o.length?`已重载 ${o.length} 个技能:${o.map(e=>e.name).join(", ")}\n (目录会在下一轮自动刷新给模型,不必重开会话)`:"未发现本地技能(.huicai/skills或 ~/.huicai/skills)。")}},{name:"add-dir",args:"[目录 | list | remove <目录>]",desc:"添加可读/搜索的附加目录(写入仍限主目录)",run(e,o){const i=e.session;Array.isArray(i.extraDirs)||(i.extraDirs=[]);const s=String(o||"").trim().split(/\s+/).filter(Boolean),r=(s[0]||"").toLowerCase();if(!o||"list"===r)return void e.print(i.extraDirs.length?"附加目录:\n"+i.extraDirs.map(e=>" "+e).join("\n")+"\n /add-dir <目录> 添加 · /add-dir remove <目录> 移除":"暂无附加目录。用法:/add-dir <目录>(可读/搜索,写仍限主目录)");if("remove"===r||"rm"===r){const n=t.resolve(e.cwd,s.slice(1).join(" ")),o=i.extraDirs.length;i.extraDirs=i.extraDirs.filter(e=>e!==n),ve(i);try{v(i)}catch{}return void e.print(i.extraDirs.length<o?"已移除:"+n:"未找到该附加目录:"+n)}const a=t.resolve(e.cwd,String(o).trim());let c=!1;try{c=n.statSync(a).isDirectory()}catch{}if(c)if(a!==e.cwd){i.extraDirs.includes(a)||i.extraDirs.push(a),ve(i);try{v(i)}catch{}e.print("已添加可访问目录:"+a+"\n (模型可用read_file/list_files/grep以绝对路径访问;写入仍限主目录)")}else e.print("这已是主工作目录。");else e.print("目录不存在:"+a)}},{name:"agents",desc:"列出可用子智能体人格(.huicai/agents/*.md;dispatch_agent可指定)",async run(e){const{loadAgents:n}=await import("./subagent.js"),t=n(e.cwd);e.print(t.length?"子智能体人格(dispatch_agent的agent参数可指定):\n"+t.map(e=>` ${e.name}${e.description?" — "+e.description.replace(/\s+/g," ").slice(0,60):""}`).join("\n"):"未定义子智能体人格。\n 在 .huicai/agents/<名字>.md写frontmatter(name/description)+正文(人格提示词)即可;\n dispatch_agent默认用通用只读子智能体(无需定义也能用)。")}},{name:"orchestrate",args:"<目标>",desc:"把一个目标自动分解为并行子任务、多智能体协作完成",run(e,n){const t=String(n||"").trim();if(t)return{prompt:`目标:${t}\n\n请作为编排者(只调度不代工)按以下方式完成:\n1) 任务分配预检:先解析目标要什么,把它分解为若干**独立、可并行**的子任务(研究/检索/分析/定位),并确认每个子任务与所派子智能体的能力匹配——超出能力范围的部分如实说明,不硬派;\n2) 用dispatch_agents一次性并行委派这些子任务(每个说明必须独立自足——子智能体看不到本对话;可指定合适的子智能体人格);\n3) 信息流经你中转:需要衔接的结论由你提炼后放进下一批子任务的context,再行派发;\n4) 汇总各子智能体结论形成整体方案或结果,并自查:每部分都有子任务产出支撑、无遗漏、未完成的如实说明;\n5) 若需落地改动,再据此逐步执行并自检(可用 /verify让我自动跑验证)。\n优先用dispatch_agents并行,减少串行等待。`};e.print("用法:/orchestrate <较大的目标>(我会分解为并行子任务,用dispatch_agents并行推进后综合)")}},{name:"squads",desc:"列出可用专家团(内置10个+全局/项目自定义;复杂任务也会被自动选团)",async run(e){const{loadSquads:n}=await import("./squad.js"),t=n(e.cwd),o={project:"项目",global:"全局",builtin:"内置"};e.print(t.length?"专家团(/squad <名称> <任务> 交给团队完成;复杂任务对话中也会按「何时使用」自动选团):\n"+t.map(e=>` [${o[e.scope]||"自定义"}] ${e.name} — 主理人${e.leader} · ${e.members.length} 位成员`+(e.when?" · "+e.when.replace(/\s+/g," ").slice(0,46):e.description?" · "+e.description.replace(/\s+/g," ").slice(0,46):"")).join("\n")+"\n(/squad-init <新名> --from <内置团> 以内置团为底稿定制;项目/全局同名可覆盖内置)":"未定义专家团。\n /squad-init <名称> 生成模板(.huicai/squads/<名称>.md):成员表+任务预检+预设工作流;\n 平台上的专家团可直接 --model squad-<分享码> 使用(服务端编排)。")}},{name:"squad",args:"<名称> <任务>",desc:"把任务交给专家团:主理人预检派发、成员协作、汇总交付",async run(e,n){const{loadSquads:t,squadCommandPrompt:o,ensureTeamDir:i}=await import("./squad.js"),s=t(e.cwd),r=String(n||"").trim(),a=r.indexOf(" "),c=a<0?r:r.slice(0,a),d=a<0?"":r.slice(a+1).trim(),l=s.find(e=>e.name===c);if(!r||!l||!d)return void e.print("用法:/squad <名称> <任务>"+(s.length?`\n可用专家团:${s.map(e=>e.name).join("、")}`:"\n还没有专家团定义,先 /squad-init <名称> 生成模板。")+(r&&!l&&c?`\n(未找到「${c}」)`:""));if(!l.members.length)return void e.print(`专家团「${c}」还没有成员,请编辑其定义文件补充「## 成员」。`);const m=i(e.cwd,l.name);return e.print(`已召集专家团「${l.name}」(主理人${l.leader},${l.members.length} 位成员)`+(m?`,团队工作区:${m}`:"")),{prompt:o(l,d,m)}}},{name:"squad-init",args:"[名称] [--from <内置团>]",desc:"生成专家团定义(空白模板,或以内置团为底稿定制)",async run(e,n){const t=await import("node:fs"),o=await import("node:path"),{SQUAD_TEMPLATE:i,loadSquads:s}=await import("./squad.js"),r=String(n||"").trim().split(/\s+/).filter(Boolean),a=r.indexOf("--from"),c=a>=0?r[a+1]:null,d=(a>=0?r.filter((e,n)=>n!==a&&n!==a+1):r)[0];let l,m;if(c){const n=s(e.cwd).find(e=>e.name===c);if(!n||!n.file)return void e.print(`未找到底稿团「${c}」(/squads查看可用团名)`);m=(d||c).replace(/[^\w.\-一-龥]/g,"-"),l=t.readFileSync(n.file,"utf8").replace(/^(---[\s\S]*?\bname:\s*).*$/m,`$1${m}`)}else m=(d||"my-team").replace(/[^\w.\-一-龥]/g,"-"),l=i.replace("{NAME}",m);const p=o.join(e.cwd,".huicai","squads"),u=o.join(p,`${m}.md`);if(t.existsSync(u))return void e.print(`已存在:${u}(直接编辑即可)`);const{countUserSquads:f}=await import("./squad.js"),{refreshPlan:g,checkLimit:h}=await import("./plan-limits.js");await g();const w=h("squads",f(e.cwd));w?e.print(w.message):(t.mkdirSync(p,{recursive:!0}),t.writeFileSync(u,l),e.print(`已生成专家团定义:${u}${c?`(底稿:${c}${m===c?",项目层将覆盖内置同名团":""})`:""}\n 编辑成员表(执行者可指agent:本地人格 / platform:平台伪模型 / platform:auto:类型 自动绑定 / model:模型名)、成员人设、预检速查表与预设工作流后,/squad `+m+" <任务> 即可开工。"))}},{name:"experts",args:"[refresh|on|off]",desc:"平台能力自动编队:查看/刷新可用的专家团·工作流·智能体·数字人",async run(e,n){const{discoverExperts:t,makeExpertTools:o}=await import("./experts.js"),i=String(n||"").trim().toLowerCase(),s=e=>["consult_expert","consult_experts"].includes(e?.spec?.function?.name);if("off"===i){if(e.config.autoExperts=!1,Array.isArray(e.extraTools))for(let n=e.extraTools.length-1;n>=0;n--)s(e.extraTools[n])&&e.extraTools.splice(n,1);return void e.print('已关闭平台能力自动编队(本会话即时生效)。永久关闭:在 ~/.huicai/config.json中设 "autoExperts": false')}if("on"===i){if(e.config.autoExperts=!0,Array.isArray(e.extraTools)&&!e.extraTools.some(s)){const n=await o({config:e.config});e.extraTools.push(...n),e.print(n.length?"已开启平台能力自动编队(本会话即时生效)。":"已开启,但当前无可编队的平台专家(需登录且账号下有可用资源,或当前驱动模型为伪模型)。")}else e.print("平台能力自动编队已是开启状态。");return}const r=await t(e.config,{force:"refresh"===i});if(!r.length)return void e.print("当前无可编队的平台专家。"+(e.config.apiKey?"(账号下暂无可用的专家团/工作流/智能体/数字人,或平台不可达)":"请先huicai login登录平台。"));const a={squad:"专家团",workflow:"工作流",agent:"智能体",datahuman:"数字人"},c=["平台能力编队(原始模型对话时可被自动咨询"+(!1===e.config.autoExperts?",当前已关闭,/experts on开启":"")+"):"];for(const e of["squad","workflow","agent","datahuman"]){const n=r.filter(n=>n.type===e);if(n.length){c.push(` ${a[e]}(${n.length})`);for(const e of n)c.push(` ${e.id} — ${e.name}${e.description?":"+e.description.slice(0,50):""}`)}}c.push("refresh"===i?"(已强制刷新)":"(/experts refresh强制刷新;/experts off关闭)"),e.print(c.join("\n"))}},{name:"batch",args:"<大改动目标>",desc:"把一个大改动拆成独立子任务,在隔离git worktree中并行推进",run(e,n){const t=String(n||"").trim();if(t)return{prompt:`目标(需拆分并行落地):${t}\n\n请这样做:\n1) 先把目标拆成若干**相互独立、可并行**的改动子任务(尽量互不冲突的文件/模块);\n2) 对每个子任务,用 \`git worktree add -b huicai-batch/<名> <路径>\` 建隔离工作区,在其中完成该子任务的改动并commit(不影响主工作区);\n3) 全部完成后,汇总每个worktree的分支与改动;需要时可对每个分支执行 \`huicai pr\` 开PR。\n(并行调研可用dispatch_agents;改动务必隔离在各自worktree,避免互相踩踏。完成后可 \`git worktree remove\` 清理。)`};e.print("用法:/batch <一个可拆分的大改动目标>(拆成互不冲突的子任务,各在隔离worktree中落地)")}},{name:"fork",args:"<后台任务提示词>",desc:"派生后台智能体(继承当前对话副本,--yolo自动放行,输出写日志)",async run(e,n){const t=String(n||"").trim();if(!t)return void e.print("用法:/fork <让后台智能体做什么>(以当前对话副本为上下文,后台运行,--yolo自动放行)");const o=$({cwd:e.cwd,model:e.session.model,messages:e.session.messages.map(e=>({...e}))});try{o.compaction=JSON.parse(JSON.stringify(e.session.compaction||{}))}catch{o.compaction={}}o.title="(fork) "+(e.session.title||"");try{v(o)}catch{}const{forkAgent:i}=await import("./forks.js");let s;try{s=i({prompt:t,cwd:e.cwd,sessionId:o.id,ownerSessionId:e.session?.id||""})}catch(n){return void e.print("无法派生后台任务:"+n.message)}e.print(`已派生后台智能体,作业 ${s.id}(pid ${s.pid},--yolo自动放行)\n 上下文:当前对话副本 ${o.id}\n 日志:${s.log}\n /tasks查看 · /tasks kill ${s.id} 终止`)}},{name:"tasks",args:"[kill <作业id>]",desc:"查看/终止本会话的后台作业(/fork派生的智能体、run_bash后台命令)",async run(e,n){const t=await import("./jobs.js"),o=e.session?.id||"",i=String(n||"").trim().split(/\s+/).filter(Boolean);if("kill"===i[0]){const n=i[1];if(!n)return void e.print("用法:/tasks kill <作业id>(作业id见 /tasks列表)");const s=t.kill(n,{sessionId:o,reason:"用户经 /tasks终止"});return void e.print(s.ok?"已请求终止作业 "+n:"终止失败:"+(s.reason||""))}const s=t.list(o);s.length?e.print("后台作业:\n"+s.map(e=>" "+t.describe(e)).join("\n")+"\n /tasks kill <作业id> 终止"):e.print("本会话暂无后台作业。(/fork <提示词> 派生后台智能体;run_bash设run_in_background放后台跑命令)")}},{name:"schedule",args:'[list | add "<cron>" "<提示词>" | remove <id>]',desc:"管理定时任务(生成crontab行交系统cron运行)",async run(e,n){const t=await import("./schedule.js"),o=String(n||"").trim(),i="huicai";if(!o||"list"===o){const n=t.listTasks();return n.length?void e.print("定时任务:\n"+n.map(e=>` #${e.id} ${e.cron} ${e.prompt.replace(/\s+/g," ").slice(0,40)}\n crontab: ${t.crontabLine(e,i)}`).join("\n")+"\n /schedule remove <id> 删除"):void e.print('暂无定时任务。\n 添加:/schedule add "*/30 * * * *" "跑测试并把结果写入report.md"\n (随后把打印的crontab行加入crontab -e)')}if(/^add\b/.test(o)){const n=o.match(/"([^"]*)"/g);if(!n||n.length<2)return void e.print('用法:/schedule add "<cron>" "<提示词>"(cron与提示词都要加引号)');const s=n[0].slice(1,-1).trim(),r=n[1].slice(1,-1).trim();if(5!==s.split(/\s+/).length)return void e.print('cron需 5 个字段,如 "*/30 * * * *"(分 时 日 月 周)');const{refreshPlan:a,isPlanLimitError:c}=await import("./plan-limits.js");let d;await a();try{({id:d}=t.addTask({cron:s,prompt:r,cwd:e.cwd}))}catch(n){if(!c(n))throw n;return void e.print(n.message)}return void e.print("已添加定时任务 #"+d+"\n 加入crontab(crontab -e):\n "+t.crontabLine({id:d,cron:s,prompt:r,cwd:e.cwd},i))}if(/^(remove|rm)\b/.test(o)){const n=o.split(/\s+/)[1];return void e.print(t.removeTask(n)?"已删除定时任务 "+n:"未找到定时任务 "+n)}e.print('用法:/schedule list · /schedule add "<cron>" "<提示词>" · /schedule remove <id>')}},{name:"cd",args:"<目录>",desc:"切换工作目录(在新目录开启新会话;IDE中请用「打开文件夹」)",run(e,o){const i=String(o||"").trim();if(!i)return void e.print("当前工作目录:"+e.cwd+"\n 用法:/cd <目录>(将在新目录开启新会话,旧会话已保存)");const s=t.resolve(e.cwd,i);let r=!1;try{r=n.statSync(s).isDirectory()}catch{}if(r){if(s!==e.cwd)return{action:"cd",cwd:s};e.print("已在该目录。")}else e.print("目录不存在:"+s)}},{name:"loop",args:"<秒> <提示词> | off",desc:"每隔N秒自动重跑一个提示词(off停止)",run(e,n){const t=String(n||"").trim();if(!t)return void e.print("用法:/loop <秒> <提示词>(如 /loop 300 检查构建是否通过)· /loop off停止");if("off"===t)return{action:"loop",stop:!0};const o=/^(\d+)\s+([\s\S]+)$/.exec(t);if(o)return{action:"loop",seconds:Math.max(10,parseInt(o[1],10)),prompt:o[2].trim()};e.print("用法:/loop <秒> <提示词> · /loop off")}},{name:"statusline",args:"<shell命令> | off",desc:"自定义TUI底栏(运行该命令,展示其stdout)",run(e,n){const t=String(n||"").trim();if(!t)return void e.print("当前状态栏命令:"+(e.config.statusLine||"(默认)")+"\n /statusline <shell命令>(stdout显示在TUI底栏)· /statusline off恢复默认");const o="off"===t?"":t;e.config.statusLine=o,s({statusLine:o}),e.print(o?"状态栏命令已设置(TUI底栏生效):"+o:"已恢复默认状态栏。")}},{name:"background",desc:"保存当前会话并给出在别处继续/后台运行的方式",run(e){try{v(e.session)}catch{}e.print(`会话已保存(${e.session.id})。\n · 在别处继续:huicai resume ${e.session.id}\n · 后台跑一个任务:/fork <提示词>(派生后台智能体,日志可查)\n · 定时运行:/schedule add "<cron>" "<提示词>"`)}},{name:"exit",desc:"退出",run:()=>({action:"exit"})}];export function listCommands(e,{includeSkills:n=!0}={}){const t=new Set("model models mode permissions config effort theme color tui execsandbox toolmode advisor automodel embed markdown debug websearch sandbox yolo plan autocompact budget skills resume sessions rewind pr".split(" ")),o=je.map(e=>({name:e.name,args:e.args||"",desc:e.desc,aliases:pe(e.name),category:me(e.name),menu:t.has(e.name),kind:"builtin"})),i=loadCustomCommands(e).map(e=>({name:e.name,args:"",desc:e.desc,aliases:[],category:"自定义命令",kind:"custom"})),s=new Set([...o,...i].map(e=>e.name).concat(Object.keys(le))),r=n?ce(e).filter(e=>de(e)&&!s.has(e.name)).map(e=>({name:e.name,args:"[补充说明]",desc:e.description||"调用技能",aliases:[],category:"技能",kind:"skill"})):[];return[...o,...i,...r]}export function loadCustomCommands(o){const i=[o?t.join(o,".huicai","commands"):null,t.join(e(),"commands")].filter(Boolean),s=new Map;for(const e of i){let o;try{o=n.readdirSync(e).filter(e=>e.endsWith(".md"))}catch{continue}for(const i of o){const o=i.replace(/\.md$/,"").trim();if(o&&!Object.hasOwn(le,o)&&!s.has(o)&&!je.some(e=>e.name===o))try{const r=n.readFileSync(t.join(e,i),"utf8"),{desc:a,body:c}=xe(r);s.set(o,{name:o,desc:a||"自定义命令",template:c})}catch{}}}return[...s.values()]}function xe(e){const n=/^---\n([\s\S]*?)\n---\n?/.exec(e);if(!n)return{desc:"",body:e.trim()};const t=n[1],o=/^description:\s*(.+)$/m.exec(t);return{desc:o?o[1].trim():"",body:e.slice(n[0].length).trim()}}export async function runSlash(e,n){const t=e.slice(1).split(/\s+/),o=(t[0]||"").trim(),i=Object.hasOwn(le,o)?le[o]:o||"help";let s=t.slice(1).join(" ").trim();if(n.signal?.aborted)return{handled:!0};if(n.select){const e=await async function(e,n,t){const o={cancelled:!0},i=async e=>({result:await runSlash(e,t)});if("permissions"===e&&["allow","deny","remove"].includes(n)){let e;if("remove"===n){const n=G(t.cwd).project;e=[...new Set([...n.allow,...n.deny])].map(e=>fe(e))}else{const n=[...se,...(t.extraTools||[]).map(e=>e.spec)];e=[...new Map(n.filter(e=>e?.function?.name).map(e=>[e.function.name,fe(e.function.name,e.function.name,e.function.description?.split("\n")[0]||"")])).values()]}const i=await he(t,{title:"remove"===n?"移除项目权限规则":`添加${"allow"===n?"允许":"拒绝"}规则 · 选定工具的所有参数`,options:e});return null==i?o:{arg:n+" "+i}}if(n)return null;if("sessions"===e&&t.resumeSession)return i("/resume");if("rewind"===e){const e=await he(t,{title:"选择回退检查点",options:[...t.checkpoints?.list||[]].reverse().map(e=>fe(String(e.id),`#${e.id} ${e.label}`,`${y(e.time)} · ${e.journal.length} 处文件改动`))});if(null==e)return o;const n=await he(t,{title:"选择回退范围",current:"chat",options:[fe("chat","仅对话","保留工作区文件"),fe("code","仅文件","覆盖当前文件,恢复检查点内容"),fe("both","对话与文件","同时回退对话并覆盖当前文件")]});return null==n?o:{arg:e+" "+n}}if("pr"===e){const e=await he(t,{title:"Pull Request 操作",options:[fe("commit","提交并创建 PR","生成提交、建分支、推送并使用 gh 创建 PR"),fe("address","处理 PR 审查意见","读取 review 评论并修改代码")]});return null==e?o:{arg:e}}if("config"===e){const e="model permissions effort advisor automodel embed toolmode execsandbox websearch autocompact budget theme color tui markdown debug".split(" "),n=await he(t,{title:"HuiCai AI 设置",options:[...e.map(e=>fe(e,"/"+e,je.find(n=>n.name===e).desc)),fe("show","查看全部配置(隐藏凭据)")]});return null==n?o:"show"===n?{result:{handled:!0,...await je.find(e=>"config"===e.name).run(t)}}:i("/"+n)}if("skills"===e){const e=listCommands(t.cwd).filter(e=>"skill"===e.kind),n=await he(t,{title:"选择要调用的技能",options:e.map(e=>fe(e.name,e.name,e.desc))});return null==n?o:i("/"+n)}if(["model","models","advisor","automodel","embed"].includes(e)){let n;try{n=await we(u(t.config.baseUrl,t.config.apiKey),t.signal)}catch(e){return t.signal?.aborted||t.print("无法加载模型列表:"+e.message+"\n请检查登录与网络后重试;当前设置保持不变。"),o}const i="model"===e||"models"===e,s=i?"model":"advisor"===e?"advisorModel":"automodel"===e?"autoModelStrong":"embeddingModel",r=t.config[s]||"off",a=e=>/embed/i.test(e.id)||"embedding"===e.type||e.capabilities?.includes?.("embeddings"),c=n.filter(n=>"string"==typeof n.id&&("embed"===e?a(n):!a(n))&&(i||!ye.test(n.id))),d=[...new Map(c.map(e=>[e.id,fe(e.id,e.id,e.name&&e.name!==e.id?e.name:"")])).values()];i?c.some(e=>!ye.test(e.id))&&d.unshift(fe("auto","Auto · 自动选择可用模型")):d.unshift(fe("off","关闭")),"embed"!==e||c.length||t.print("平台未提供可识别的嵌入模型;可用 /embed <模型ID> 显式配置。");const l=await he(t,{title:i?"选择模型":"advisor"===e?"顾问模型":"automodel"===e?"复杂任务自动升配模型":"嵌入模型",current:r,options:d});if(null==l)return o;const m=i?"model":e;return{result:{handled:!0,...await je.find(e=>e.name===m).run({...t,commandModels:n},l)}}}const s=ge(e,t);if(!s)return null;s.current&&!s.options.some(e=>e.value===s.current)&&s.options.unshift(fe(s.current,s.current+"(自定义当前值)"));const r=await he(t,s);if(null==r)return o;if("yolo"===e)return i("/mode "+r);if("permissions"===e){if("execsandbox"===r)return i("/execsandbox");if(["allow","deny","remove"].includes(r))return i("/permissions "+r)}return{arg:r}}(i,s,n);if(e?.result)return e.result;if(e?.cancelled)return{handled:!0};e&&(s=e.arg)}const r=je.find(e=>e.name===i);if(r){return{handled:!0,...await r.run(n,s)||{}}}const a=loadCustomCommands(n.cwd).find(e=>e.name===i);if(a){let e=a.template;return e.includes("$ARGUMENTS")?e=e.replaceAll("$ARGUMENTS",s):s&&(e=e+"\n\n"+s),{handled:!0,prompt:e}}{const{loadLocalSkills:e,readSkillBody:t,renderSkillContent:o,isUserInvocable:r}=await import("./skills.js"),a=e(n.cwd).find(e=>e.name===i);if(a&&r(a)){const e=t(n.cwd,i);if(e){n.print(`已投入技能「${i}」的操作指引${s?",并附上你的补充说明":""}。`);const t=o(e.skill,e.raw);return{handled:!0,prompt:s?`${t}\n\n${s}`:t}}}if(a&&!r(a))return n.print(`技能「${i}」标记为不可由用户直接调用(user-invocable: false),它只供模型按需载入。`),{handled:!0}}const c=ue(listCommands(n.cwd),i).slice(0,4);return n.print("未知命令。/help查看可用命令。"+(c.length?"\n可能要找:"+c.map(e=>"/"+e.name).join("、"):"")),{handled:!0}}
package/src/config.js CHANGED
@@ -1 +1 @@
1
- import e from"node:fs";import r from"node:path";import o from"node:os";export const CONFIG_DIR=r.join(o.homedir(),".huicai");export const CONFIG_PATH=r.join(CONFIG_DIR,"config.json");export const DEFAULT_BASE_URL="https://huicai-ai.sjht.com";export const DEFAULT_MODEL="deepseek-v4-flash";function n(){try{return JSON.parse(e.readFileSync(CONFIG_PATH,"utf8"))}catch{return{}}}export function loadConfig(){const e=n(),r={baseUrl:(process.env.HUICAI_BASE_URL||e.baseUrl||DEFAULT_BASE_URL).trim(),apiKey:(process.env.HUICAI_API_KEY||e.apiKey||"").trim(),model:(process.env.HUICAI_MODEL||e.model||DEFAULT_MODEL).trim(),maxTurns:Number(process.env.HUICAI_MAX_TURNS||e.maxTurns)||0,maxTokens:Number(process.env.HUICAI_MAX_TOKENS||e.maxTokens)||0,autoCompactTokens:Number(process.env.HUICAI_AUTOCOMPACT_TOKENS||e.autoCompactTokens)||0,toolConcurrency:Number(process.env.HUICAI_TOOL_CONCURRENCY||e.toolConcurrency)||0,subagentConcurrency:Number(process.env.HUICAI_SUBAGENT_CONCURRENCY||e.subagentConcurrency)||0,reasoningEffort:(process.env.HUICAI_EFFORT||e.reasoningEffort||"").trim(),compactModel:(process.env.HUICAI_COMPACT_MODEL||e.compactModel||"").trim(),memoryEnabled:"0"!==process.env.HUICAI_MEMORY&&!1!==e.memory,showReasoning:"0"!==process.env.HUICAI_REASONING&&!1!==e.showReasoning,markdown:"0"!==process.env.HUICAI_MARKDOWN&&!1!==e.markdown,client:(process.env.HUICAI_CLIENT||"cli").trim().toLowerCase(),theme:(process.env.HUICAI_THEME||e.theme||"default").trim(),accentColor:(e.accentColor||"cyan").trim(),tui:(process.env.HUICAI_TUI||e.tui||"auto").trim(),debug:"1"===process.env.HUICAI_DEBUG||!0===e.debug,statusLine:(e.statusLine||"").trim(),advisorModel:(process.env.HUICAI_ADVISOR||e.advisorModel||"").trim(),autoModelStrong:(process.env.HUICAI_AUTOMODEL||e.autoModelStrong||"").trim(),autoModelThreshold:Number(process.env.HUICAI_AUTOMODEL_THRESHOLD||e.autoModelThreshold)||0,subagentModel:(process.env.HUICAI_SUBAGENT_MODEL||e.subagentModel||"").trim(),subagentMaxIters:Number(process.env.HUICAI_SUBAGENT_MAXITERS||e.subagentMaxIters)||0,budgetTokens:Number(e.budgetTokens)||0,crewBudget:"1"===process.env.HUICAI_CREW_BUDGET||!0===e.crewBudget,crewBudgetPlan:e.crewBudgetPlan&&"object"==typeof e.crewBudgetPlan?e.crewBudgetPlan:{},embeddingModel:(process.env.HUICAI_EMBED_MODEL||e.embeddingModel||"").trim(),pyUvMirror:(process.env.HUICAI_UV_MIRROR||e.pyUvMirror||"").trim(),pyUvVersion:(process.env.HUICAI_UV_VERSION||e.pyUvVersion||"").trim(),pyPythonMirror:(process.env.HUICAI_PYTHON_MIRROR||e.pyPythonMirror||"").trim(),pyPypiMirror:(process.env.HUICAI_PYPI_MIRROR||e.pyPypiMirror||"").trim(),pyBundleUrl:(process.env.HUICAI_PYRT_BUNDLE||e.pyBundleUrl||"").trim(),pyPrewarm:!1!==e.pyPrewarm,webSearch:"0"!==process.env.HUICAI_WEB_SEARCH&&!1!==e.webSearch,computerUse:"0"!==process.env.HUICAI_COMPUTER_USE&&!1!==e.computerUse,notifications:"0"!==process.env.HUICAI_NOTIFY&&!1!==e.notifications,autoSkillSync:"0"!==process.env.HUICAI_SKILL_SYNC&&!1!==e.autoSkillSync,wakeLockInAuto:"0"!==process.env.HUICAI_WAKELOCK&&!1!==e.wakeLockInAuto,visionModel:(process.env.HUICAI_VISION_MODEL||e.visionModel||"").trim(),spill:"0"!==process.env.HUICAI_SPILL&&!1!==e.spill,crush:"0"!==process.env.HUICAI_CRUSH&&!1!==e.crush,execSandbox:(process.env.HUICAI_EXEC_SANDBOX||e.execSandbox||"off").trim(),toolMode:(process.env.HUICAI_TOOL_MODE||e.toolMode||"native").trim(),user:e.user&&"object"==typeof e.user?e.user:null};return r.baseUrl=r.baseUrl.replace(/\/+$/,""),r}export function saveConfig(r){const o={...n(),...r};e.mkdirSync(CONFIG_DIR,{recursive:!0}),e.writeFileSync(CONFIG_PATH,JSON.stringify(o,null,2)+"\n",{mode:384});try{e.chmodSync(CONFIG_PATH,384)}catch{}return t=null,o}let s=null,t=null;export function setWebSearchHost(e){s=e&&"function"==typeof e.get?e:null}export function isWebSearchOn(){if("0"===process.env.HUICAI_WEB_SEARCH)return!1;if(s)try{return!1!==s.get()}catch{}let r;try{r=e.statSync(CONFIG_PATH)}catch{return!0}if(t&&t.mtimeMs===r.mtimeMs&&t.size===r.size)return t.on;const o=!1!==n().webSearch;return t={mtimeMs:r.mtimeMs,size:r.size,on:o},o}export async function setWebSearchOn(e){const r=!!e;return s&&"function"==typeof s.set?(await s.set(r),r):(saveConfig({webSearch:r}),r)}
1
+ import e from"node:fs";import r from"node:crypto";import o from"node:path";import s from"node:os";export const CONFIG_DIR=process.env.HUICAI_CONFIG_DIR?o.resolve(process.env.HUICAI_CONFIG_DIR):o.join(s.homedir(),".huicai");export const CONFIG_PATH=o.join(CONFIG_DIR,"config.json");export const DEFAULT_BASE_URL="https://huicai-ai.sjht.com";export const DEFAULT_MODEL="deepseek-v4-flash";let n={};export function setRuntimeCredentials(e={}){n={...e}}const t=(e,o)=>r.createHash("sha256").update(String(e||"").replace(/\/+$/,"")+"\n"+String(o||"")).digest("hex");function c(){try{return JSON.parse(e.readFileSync(CONFIG_PATH,"utf8"))}catch{return{}}}export function loadConfig(){const e=c(),r={baseUrl:(n.baseUrl||process.env.HUICAI_BASE_URL||e.baseUrl||DEFAULT_BASE_URL).trim(),apiKey:(n.apiKey||process.env.HUICAI_API_KEY||e.apiKey||"").trim(),model:(process.env.HUICAI_MODEL||e.model||DEFAULT_MODEL).trim(),maxTurns:Number(process.env.HUICAI_MAX_TURNS||e.maxTurns)||0,maxTokens:Number(process.env.HUICAI_MAX_TOKENS||e.maxTokens)||0,autoCompactTokens:Number(process.env.HUICAI_AUTOCOMPACT_TOKENS||e.autoCompactTokens)||0,toolConcurrency:Number(process.env.HUICAI_TOOL_CONCURRENCY||e.toolConcurrency)||0,subagentConcurrency:Number(process.env.HUICAI_SUBAGENT_CONCURRENCY||e.subagentConcurrency)||0,reasoningEffort:(process.env.HUICAI_EFFORT||e.reasoningEffort||"").trim(),compactModel:(process.env.HUICAI_COMPACT_MODEL||e.compactModel||"").trim(),memoryEnabled:"0"!==process.env.HUICAI_MEMORY&&!1!==e.memory,showReasoning:"0"!==process.env.HUICAI_REASONING&&!1!==e.showReasoning,markdown:"0"!==process.env.HUICAI_MARKDOWN&&!1!==e.markdown,client:(process.env.HUICAI_CLIENT||"cli").trim().toLowerCase(),theme:(process.env.HUICAI_THEME||e.theme||"default").trim(),accentColor:(e.accentColor||"cyan").trim(),tui:(process.env.HUICAI_TUI||e.tui||"auto").trim(),debug:"1"===process.env.HUICAI_DEBUG||!0===e.debug,statusLine:(e.statusLine||"").trim(),advisorModel:(process.env.HUICAI_ADVISOR||e.advisorModel||"").trim(),autoModelStrong:(process.env.HUICAI_AUTOMODEL||e.autoModelStrong||"").trim(),autoModelThreshold:Number(process.env.HUICAI_AUTOMODEL_THRESHOLD||e.autoModelThreshold)||0,subagentModel:(process.env.HUICAI_SUBAGENT_MODEL||e.subagentModel||"").trim(),subagentMaxIters:Number(process.env.HUICAI_SUBAGENT_MAXITERS||e.subagentMaxIters)||0,budgetTokens:Number(e.budgetTokens)||0,crewBudget:"1"===process.env.HUICAI_CREW_BUDGET||!0===e.crewBudget,crewBudgetPlan:e.crewBudgetPlan&&"object"==typeof e.crewBudgetPlan?e.crewBudgetPlan:{},embeddingModel:(process.env.HUICAI_EMBED_MODEL||e.embeddingModel||"").trim(),pyUvMirror:(process.env.HUICAI_UV_MIRROR||e.pyUvMirror||"").trim(),pyUvVersion:(process.env.HUICAI_UV_VERSION||e.pyUvVersion||"").trim(),pyPythonMirror:(process.env.HUICAI_PYTHON_MIRROR||e.pyPythonMirror||"").trim(),pyPypiMirror:(process.env.HUICAI_PYPI_MIRROR||e.pyPypiMirror||"").trim(),pyBundleUrl:(process.env.HUICAI_PYRT_BUNDLE||e.pyBundleUrl||"").trim(),pyPrewarm:!1!==e.pyPrewarm,webSearch:"0"!==process.env.HUICAI_WEB_SEARCH&&!1!==e.webSearch,computerUse:"0"!==process.env.HUICAI_COMPUTER_USE&&!1!==e.computerUse,notifications:"0"!==process.env.HUICAI_NOTIFY&&!1!==e.notifications,autoSkillSync:"0"!==process.env.HUICAI_SKILL_SYNC&&!1!==e.autoSkillSync,wakeLockInAuto:"0"!==process.env.HUICAI_WAKELOCK&&!1!==e.wakeLockInAuto,visionModel:(process.env.HUICAI_VISION_MODEL||e.visionModel||"").trim(),spill:"0"!==process.env.HUICAI_SPILL&&!1!==e.spill,crush:"0"!==process.env.HUICAI_CRUSH&&!1!==e.crush,execSandbox:(process.env.HUICAI_EXEC_SANDBOX||e.execSandbox||"off").trim(),toolMode:(process.env.HUICAI_TOOL_MODE||e.toolMode||"native").trim(),user:e.user&&"object"==typeof e.user?e.user:null};return(e.userCredentialHash?e.userCredentialHash!==t(r.baseUrl,r.apiKey):r.apiKey!==(e.apiKey||"")||r.baseUrl.replace(/\/+$/,"")!==String(e.baseUrl||DEFAULT_BASE_URL).replace(/\/+$/,""))&&(r.user=null),r.baseUrl=r.baseUrl.replace(/\/+$/,""),r}export function saveConfig(r){const o=c(),s={...o,...r};if(!Object.hasOwn(r,"user")&&["apiKey","baseUrl"].some(e=>Object.hasOwn(r,e)&&r[e]!==o[e])&&(s.user=null),Object.hasOwn(r,"user")){const e=r.baseUrl??n.baseUrl??process.env.HUICAI_BASE_URL??s.baseUrl??DEFAULT_BASE_URL,o=r.apiKey??n.apiKey??process.env.HUICAI_API_KEY??s.apiKey??"";s.userCredentialHash=s.user?t(e,o):null}e.mkdirSync(CONFIG_DIR,{recursive:!0});const i=CONFIG_PATH+"."+process.pid+".tmp";e.writeFileSync(i,JSON.stringify(s,null,2)+"\n",{mode:384}),e.renameSync(i,CONFIG_PATH);try{e.chmodSync(CONFIG_PATH,384)}catch{}return I=null,s}let i=null,I=null;export function setWebSearchHost(e){i=e&&"function"==typeof e.get?e:null}export function isWebSearchOn(){if("0"===process.env.HUICAI_WEB_SEARCH)return!1;if(i)try{return!1!==i.get()}catch{}let r;try{r=e.statSync(CONFIG_PATH)}catch{return!0}if(I&&I.mtimeMs===r.mtimeMs&&I.size===r.size)return I.on;const o=!1!==c().webSearch;return I={mtimeMs:r.mtimeMs,size:r.size,on:o},o}export async function setWebSearchOn(e){const r=!!e;return i&&"function"==typeof i.set?(await i.set(r),r):(saveConfig({webSearch:r}),r)}