@huicai/cli 2026.721.0 → 2026.814.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -6
- package/bin/huicai.js +24 -4
- package/package.json +2 -2
- package/src/account.js +1 -0
- package/src/advisor.js +1 -1
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/assets-import.js +1 -0
- package/src/autofix.js +1 -1
- package/src/automation-tools.js +1 -0
- package/src/automation.js +1 -1
- package/src/commands.js +1 -1
- package/src/compaction.js +1 -1
- package/src/computer.js +1 -0
- package/src/config.js +1 -1
- package/src/context-report.js +1 -1
- package/src/context.js +1 -1
- package/src/daemon.js +1 -1
- package/src/decisions.js +1 -0
- package/src/errkind.js +1 -0
- package/src/executors.js +1 -1
- package/src/expert-catalog.js +1 -0
- package/src/experts-store.js +1 -0
- package/src/gitflow.js +1 -1
- package/src/mcp.js +1 -1
- package/src/notify.js +1 -0
- package/src/presence.js +1 -0
- package/src/pyruntime.js +1 -1
- package/src/repl.js +1 -1
- package/src/schedule-freq.js +1 -0
- package/src/schedule.js +1 -1
- package/src/session.js +1 -1
- package/src/skills.js +1 -1
- package/src/squad.js +1 -1
- package/src/subagent.js +1 -1
- package/src/title.js +1 -0
- package/src/tools.js +1 -1
- package/src/transcript.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/vision.js +1 -0
- package/src/visual.js +1 -0
package/src/commands.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import n from"node:fs";import e 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}from"./config.js";import{listModels as a,fetchUsage as c,streamChat as d}from"./api.js";import{listSessions as m,relTime as l,turnCount as p,saveSession as u,createSession as f}from"./session.js";import{compactNow as g,buildModelMessages as h}from"./compaction.js";import{computeInsights as w,formatInsights as y}from"./insights.js";import{setColorMode as $,setAccent as k,getAccent as v,ACCENTS as b}from"./colors.js";import{detectVerifyCommand as S}from"./autofix.js";import{PERMISSION_MODES as j,MODE_ICON as x,MODE_LABEL as T,MODE_DESC as _}from"./permissions.js";import{loadAllowRules as M,describeRules as C,parseRule as I,addProjectRule as L,removeProjectRule as E,GLOBAL_SETTINGS_PATH as N,projectSettingsPath as P}from"./settings.js";import{contextReport as A}from"./context-report.js";import{renderDiff as D}from"./diffview.js";import{loadHooks as U,formatHooks as q}from"./hooks.js";import{copyToClipboard as F}from"./clipboard.js";import{toMarkdown as R,recapConversation as O}from"./transcript.js";import{runDoctor as K}from"./doctor.js";import{TOOL_SPECS as B}from"./tools.js";import{makeTodoTool as H}from"./todo.js";import{makeMemoryTool as J}from"./memory.js";const G=/^(agent|workflow|datahuman|squad)-/,W="\n\n# 附加可访问目录(可用 read_file/list_files/grep 以绝对路径访问;写入仍限主目录)\n";function Y(n){const e=n.messages&&n.messages[0];if(!e||"system"!==e.role||"string"!=typeof e.content)return;const t=e.content.indexOf(W);let o=t>=0?e.content.slice(0,t):e.content;const i=n.extraDirs||[];i.length&&(o+=W+i.map(n=>"- "+n).join("\n")+"\n"),e.content=o}export const INIT_PROMPT="请为当前项目生成或完善 HUICAI.md(给终端智能体看的项目说明):先用 list_files/glob/read_file 快速调研项目(技术栈、目录结构、构建/测试/运行命令、代码约定),再用 write_file 创建或更新 HUICAI.md(若已存在则保留人工撰写的内容,增量完善)。内容包含:项目简介、关键目录、常用命令(构建/测试/运行)、代码风格与注意事项。控制在 100 行以内,用中文。";const Q={manual:"manual","请求批准":"manual","审批":"manual",acceptedits:"acceptEdits","accept-edits":"acceptEdits","接受编辑":"acceptEdits","编辑":"acceptEdits",auto:"auto","自动":"auto",yolo:"auto","无限制":"auto",plan:"plan","计划":"plan",sandbox:"sandbox","沙箱":"sandbox"};function V(n){return n.permissions?.getMode?.()??(n.permissions?.isYolo?.()?"auto":"manual")}function X(n,e){n.permissions?.setMode?n.permissions.setMode(e):(n.permissions?.setYolo?.("auto"===e),n.permissions?.setAcceptEdits?.("acceptEdits"===e),n.permissions?.setPlanMode?.("plan"===e),n.permissions?.setSandbox?.("sandbox"===e)),n.config&&(n.config.planMode="plan"===e,n.config.sandbox="sandbox"===e)}const z=[{name:"help",desc:"显示可用命令(含自定义命令)",run(n){const e=z.map(n=>[`/${n.name}${n.args?" "+n.args:""}`,n.desc]);for(const t of loadCustomCommands(n.cwd))e.push([`/${t.name}`,`${t.desc}(自定义)`]);e.push(["! <命令>","直接执行 shell 命令并把输出加入上下文"]),e.push(["@<文件路径>","在消息中引用文件内容(自动展开给模型)"]);const t=Math.min(24,Math.max(...e.map(([n])=>n.length))+2);n.print(e.map(([n,e])=>" "+n.padEnd(t)+e).join("\n"))}},{name:"model",args:"[名称|auto]",desc:"查看或切换模型(auto=按账号自动选可用模型,与 VSCode 一致)",async run(n,e){if(e){if("auto"===e){try{const e=(await a(n.config.baseUrl,n.config.apiKey)).map(n=>n.id).filter(n=>!/^(agent|workflow|datahuman|squad)-/.test(n));n.config.model=(e.includes(s)?s:e[0])||n.config.model}catch{}return i({model:"auto"}),void n.print("Auto 已选:"+n.config.model)}try{const t=(await a(n.config.baseUrl,n.config.apiKey)).map(n=>n.id);if(!t.includes(e))return void n.print(`当前账号无权使用模型「${e}」。\n 可用模型:${t.slice(0,12).join(", ")}`+(t.length>12?` …(共 ${t.length} 个,/models 查看全部)`:"")+"\n 或用 /model auto 自动选择可用模型。")}catch(e){n.print(`(未能校验模型可用性:${e.message})仍按输入切换;若无权限,平台会在调用时拒绝。`)}n.config.model=e,i({model:e}),n.print("已切换模型:"+e)}else n.print("当前模型:"+(n.config.model||"(未设置)"))}},{name:"models",desc:"列出平台可用模型",async run(n){try{const e=await a(n.config.baseUrl,n.config.apiKey);n.print(e.map(n=>" "+n.id).join("\n")||"(无)")}catch(e){n.print("拉取失败:"+e.message)}}},{name:"usage",desc:"查看 Token 用量窗口(5小时/7天,按会员等级)",async run(n){const e=n=>"number"==typeof n&&Number.isFinite(n)?n:0,t=(n,t)=>(n=e(n),(t=e(t))<0?`${n.toLocaleString()} / 无限`:0===t?"该档未开放":`${n.toLocaleString()} / ${t.toLocaleString()} (${Math.min(100,Math.round(n/t*100))}%)`);try{const e=await c(n.config.baseUrl,n.config.apiKey);if(!e||"number"!=typeof e.used5h&&"number"!=typeof e.used7d)return void n.print("平台未返回用量数据(请确认平台已支持 /v1/usage)。");n.print(`Token 用量窗口\n 近 5 小时:${t(e.used5h,e.limit5h)}\n 近 7 天 :${t(e.used7d,e.limit7d)}`+(!e.allowed&&e.message?"\n "+e.message:""))}catch(e){n.print("拉取用量失败:"+e.message)}}},{name:"login",desc:"重新登录(浏览器授权;无浏览器请退出后 huicai login --paste)",async run(n){try{const{deviceLogin:e}=await import("./device.js");n.print("正在打开浏览器授权…");const t=await e(n.config.baseUrl);n.config.apiKey=t,i({apiKey:t}),n.print("登录成功。")}catch(e){n.print("登录未完成:"+e.message+"(可退出后用 huicai login --paste 粘贴令牌)")}}},{name:"logout",desc:"退出登录(清除本机保存的令牌)",run(n){n.config.apiKey="",i({apiKey:""}),n.print("已退出登录。后续对话需重新 /login 或 huicai login。")}},{name:"clear",desc:"开启新会话(旧会话仍可 --resume 恢复)",run:()=>({action:"clear"})},{name:"compact",desc:"立即压缩上下文(摘要独立存储,不影响页面与会话原文)",async run(n){n.session.compaction||(n.session.compaction={});const e=await g(n.session.messages,n.config,{state:n.session.compaction});n.print(e>0?`已压缩:${e} 条较早消息已摘要(原对话完整保留,仅发给模型的上下文变小)`:"暂无可压缩的中段历史(会话较短或摘要失败)")}},{name:"memory",args:"[clear]",desc:"查看本项目长期记忆(clear 清空)",async run(n,e){const{loadMemory:t,clearMemory:o,memoryPath:i}=await import("./memory.js");if("clear"===e)return void n.print(o(n.cwd)?"已清空本项目长期记忆。":"本项目暂无长期记忆。");const s=t(n.cwd);n.print(s?s+`\n(文件:${i(n.cwd)} · /memory clear 清空)`:"本项目暂无长期记忆(模型会经 memory_write 自动记录值得跨会话记住的信息;配置 memory:false 可关闭)。")}},{name:"cost",desc:"查看本次运行累计 token 用量(及预算进度)",run(n){const e=n.usage.prompt+n.usage.completion,t=Number(n.config.budgetTokens)||0;n.print(`token 用量:输入 ${n.usage.prompt} / 输出 ${n.usage.completion}`+(n.session.lastPromptTokens?` · 当前上下文 ~${n.session.lastPromptTokens} tok`:"")+(t>0?`\n预算:${e.toLocaleString()} / ${t.toLocaleString()} tokens(${Math.round(e/t*100)}%)`+(e>=t?" ⚠ 已超预算":e>=.8*t?" ⚠ 接近预算":""):""))}},{name:"status",desc:"显示运行状态(版本/模型/目录/会话/MCP/权限规则)",run(n){const e=n.session,t=[...new Set((n.extraTools||[]).map(n=>n.spec?.function?.name||"").filter(n=>n.startsWith("mcp__")).map(n=>n.split("__")[1]))];let i="";try{i=o("git rev-parse --abbrev-ref HEAD",{cwd:n.cwd,stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{}const s=M(n.cwd),r=V(n),a=[`版本 huicai ${n.version||""}`,`模型 ${n.config.model}`,`平台 ${n.config.baseUrl}`,`目录 ${n.cwd}${i?`(git:${i})`:""}`,`会话 ${e.id} · ${p(e.messages)} 轮${e.lastPromptTokens?` · 上下文 ~${e.lastPromptTokens} tok`:""}`,`token 输入 ${n.usage.prompt} / 输出 ${n.usage.completion}`,`模式 ${x[r]} ${T[r]}(${_[r]})`,`审批 允许规则 ${s.length} 条${n.config.goal?" · 🎯 目标已设":""}`,`MCP ${t.length?t.join(", "):"(未配置)"}`];n.print(a.join("\n"))}},{name:"sessions",desc:"列出当前目录的历史会话",run(n){const e=m(n.cwd);if(!e.length)return void n.print("该目录暂无历史会话。");const t=e.slice(0,15).map(e=>{const t=e.id===n.session.id?" ← 当前":"";return` ${l(e.updatedAt).padStart(8)} ${e.title} ${e.turns}轮 · ${e.id}${t}`});t.push("(退出后 huicai --resume 可选择恢复)"),n.print(t.join("\n"))}},{name:"mode",args:"["+j.join("|")+"]",desc:"对话模式:请求批准/接受编辑/自动/计划/沙箱(自动=无限制,任何命令免审批)",run(n,e){const t=V(n),o=String(e||"").trim();if(!o){const e=[`当前对话模式:${x[t]} ${T[t]}(${_[t]})`,"","可切换:"];for(const n of j)e.push(` ${n===t?"●":" "} ${x[n]} ${n.padEnd(11)} ${T[n]} — ${_[n]}`);return e.push(""," 用法:/mode <名称>(也可用 /yolo、/plan、/sandbox 快捷切换)"),void n.print(e.join("\n"))}const i=Q[o]||Q[o.toLowerCase()];i?(X(n,i),n.print(`对话模式 → ${x[i]} ${T[i]}(${_[i]})`+("plan"===i?"\n 只做只读调研出方案;/plan approve 批准并分步执行,或 /mode auto 直接放手执行。":"")+("auto"===i?"\n ⚠ 已开放所有权限:shell/Python/联网/自动化等任何命令都不再询问。":""))):n.print(`未知模式:${o}。可选:${j.join(" / ")}`)}},{name:"yolo",desc:"切换「自动」模式(无任何限制,任何命令免审批)",run(n){const e="auto"!==V(n);X(n,e?"auto":"manual"),n.print(e?"⚡ 自动模式:开。已开放所有权限,任何命令(含 shell/联网/自动化)都不再询问。":"🔒 自动模式:关。回到「请求批准」,写/执行按权限逐项审批。")}},{name:"cwd",desc:"显示当前工作目录",run(n){n.print(n.cwd)}},{name:"markdown",desc:"切换助手回复的 Markdown 渲染(TUI 富文本;关闭则显示原始文本,便于复制)",run(n){const e=!1===n.config.markdown;n.config.markdown=e,i({markdown:e}),n.print("Markdown 渲染:"+(e?"开(下一条回复起生效)":"关(显示原始 Markdown 文本)"))}},{name:"init",desc:"让智能体调研项目并生成/完善 HUICAI.md",run:()=>({prompt:INIT_PROMPT})},{name:"skills",desc:"列出本地技能(Agent Skills;pull/push 用终端命令 huicai skills)",async run(n){const{loadLocalSkills:e}=await import("./skills.js"),t=e(n.cwd);t.length?n.print(t.map(n=>` ${n.name}${n.version?" v"+n.version:""}(${"project"===n.scope?"项目":"全局"}${n.files.length?` · ${n.files.length} 附件`:""})${n.description?" "+n.description.slice(0,60):""}`).join("\n")+"\n(模型会按描述经 load_skill 按需载入;本会话启动时已注入目录)"):n.print("暂无本地技能(.huicai/skills 或 ~/.huicai/skills)。可用 huicai skills pull 从平台拉取。")}},{name:"mcp",desc:"列出已连接的 MCP 工具",run(n){const e=(n.extraTools||[]).filter(n=>(n.spec?.function?.name||"").startsWith("mcp__"));e.length?n.print(e.map(n=>` ${n.spec.function.name}${n.readOnly?"(只读)":""}`).join("\n")):n.print("未连接 MCP 服务。配置 ~/.huicai/mcp.json(通用 mcpServers 格式)后重启生效。")}},{name:"context",desc:"可视化当前上下文占用(各类别 token 估算)",run(n){const e=G.test(String(n.config.model||""));let t=[];if(!e)try{const e=H({state:{list:[]},emit(){}}).spec,o=!1!==n.config.memoryEnabled?J({cwd:n.cwd,emit(){}}).spec:null;t=[...B,e,...o?[o]:[],...(n.extraTools||[]).map(n=>n.spec).filter(Boolean)]}catch{t=B}n.print(A({messages:n.session.messages,compaction:n.session.compaction||{},specs:t,lastPromptTokens:n.session.lastPromptTokens||0,autoCompactTokens:Number(n.config.autoCompactTokens)||0,platform:e}))}},{name:"permissions",args:"[allow|deny <规则> | remove <规则>]",desc:"查看/增删工具权限规则(allow/deny)",run(n,e){const t=String(e||"").trim().split(/\s+/).filter(Boolean),o=(t.shift()||"").toLowerCase(),i=t.join(" ").trim();if(!o){const e=C(n.cwd),t=n=>n.length?n.map(n=>" "+n).join("\n"):" (无)";return void n.print("权限规则(deny 优先于 allow/yolo,对只读工具也生效)\n 全局 ~/.huicai/settings.json\n 允许:\n"+t(e.global.allow)+"\n 拒绝:\n"+t(e.global.deny)+"\n 项目 .huicai/settings.json\n 允许:\n"+t(e.project.allow)+"\n 拒绝:\n"+t(e.project.deny)+"\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 n.print("用法:/permissions remove <规则原文>(仅移除项目设置里的规则)");const e=E(n.cwd,i);return n.permissions?.reloadRules?.(),void n.print(e.removed?`已移除规则:${i}`:`未在项目设置中找到该规则:${i}`)}n.print("未知子命令。用法:/permissions [allow|deny|remove] <规则>")}else{if(!i||!I(i))return void n.print("规则无效。示例:run_bash(git *)、edit_file、fetch_url(https://a.com*)");try{const e=L(n.cwd,o,i);n.permissions?.reloadRules?.(),n.print(`已添加${"deny"===o?"拒绝":"允许"}规则:${i} → ${e}`)}catch(e){n.print("写入失败:"+e.message)}}}},{name:"diff",desc:"查看未提交改动(git)与本会话智能体改过的文件",run(n){n.print(D(n.cwd,n.session.messages))}},{name:"pr",args:"[commit|address]",desc:"自动提交并开 PR(生成规范 commit→建分支→push→gh 开 PR);address=按 review 评论自动改",async run(n,e){const t=String(e||"").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 e=await o({cwd:n.cwd,config:n.config,emit:e=>n.print(e),push:"commit"!==t,openPr:"commit"!==t});if(!e.ok)return void n.print("✗ "+e.error);n.print(e.pr?"✅ 已开 PR:"+e.pr:"✅ 完成(分支 "+e.branch+")"+(e.note?":"+e.note:""))}catch(e){n.print("PR 流程出错:"+e.message)}}},{name:"plan",args:"[on|off|approve]",desc:"计划模式:只读调研出方案→/plan approve 批准并分步执行",run(n,e){const t=String(e||"").trim().toLowerCase();if("approve"===t||"go"===t)return X(n,"manual"),n.print("计划已批准,开始分步执行。"),{prompt:"计划已获批准。现在请按你刚才制定的计划(todo 列表)逐步执行:每完成一步用 todo_write 更新状态并简要自检;遇到与计划不符及时调整并说明;全部完成后总结改了什么、以及如何验证(可用 /verify 让我自动跑验证)。"};const o="on"===t||"off"!==t&&"plan"!==V(n);X(n,o?"plan":"manual"),n.print(o?"计划模式:开。只做只读调研并用 todo_write 产出分步方案;写/执行会被拦截。\n 方案满意后用 /plan approve 批准并自动分步执行;或 /plan off 手动执行。":"计划模式:关。恢复正常执行(写/执行仍按权限审批)。")}},{name:"auto",args:"<目标>",desc:"自动规划并自动执行:只读调研出分步计划,确认「是否继续?」后自动逐步执行",run(n,e){const t=String(e||"").trim();if(t)return{action:"autorun",goal:t};n.print("用法:/auto <目标>\n 先自动规划(只读调研 + todo 分步计划),随后询问「是否继续?1、是 2、是,并允许在当前对话中后续相同操作不再询问 3、否」,确认后自动逐步执行并自检。")}},{name:"rewind",args:"[N [chat|code|both]]",desc:"检查点回滚:无参列出;/rewind N 回退对话,加 code/both 一并回滚文件",run(n,e){const t=n.checkpoints;if(!t||!t.list.length)return void n.print("暂无检查点(每个用户回合前自动记录,发生对话后可用)。");const o=String(e||"").trim().split(/\s+/).filter(Boolean);if(!o.length){const e=["检查点(回滚到某个回合之前)"];for(const n of t.list)e.push(` #${n.id} ${l(n.time)} ${n.label}`+(n.journal.length?` · 改动 ${n.journal.length} 处`:""));return e.push(" 用法:/rewind N(仅回退对话,安全)· /rewind N code(回滚文件)· /rewind N both"),e.push(" 提示:code/both 会覆盖当前文件,建议先 /diff 查看。"),void n.print(e.join("\n"))}const i=Number(o[0]);if(!Number.isFinite(i))return void n.print("用法:/rewind N [chat|code|both]");const s=["chat","code","both"].includes(o[1])?o[1]:"chat",r=t.restore(i,s,{session:n.session,cwd:n.cwd});if(!r.ok)return void n.print(r.error);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{u(n.session)}catch{}return n.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(n){n.print(q(U(n.cwd)))}},{name:"copy",args:"[N]",desc:"复制助手回复到剪贴板(默认最后一条;N=倒数第 N 条)",async run(n,e){const t=n.session.messages.filter(n=>"assistant"===n.role&&"string"==typeof n.content&&n.content.trim());if(!t.length)return void n.print("还没有可复制的助手回复。");const o=Math.max(1,parseInt(e,10)||1),i=t[t.length-o];if(!i)return void n.print(`只有 ${t.length} 条助手回复。`);const s=await F(i.content);n.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=R(t.session),s=String(o||"").trim()||`huicai-对话-${(new Date).toISOString().slice(0,19).replace(/[:T]/g,"-")}.md`,r=e.isAbsolute(s)?s:e.join(t.cwd,s);try{n.writeFileSync(r,i,"utf8"),t.print("已导出对话到 "+r)}catch(n){t.print("导出失败:"+n.message)}}},{name:"rename",args:"<新标题>",desc:"重命名当前会话",run(n,e){const t=String(e||"").trim();if(t){n.session.title=t.slice(0,80);try{u(n.session)}catch{}n.print("会话已重命名为:"+n.session.title)}else n.print("当前标题:"+(n.session.title||"(未命名)")+"\n 用法:/rename <新标题>")}},{name:"recap",desc:"用一句话总结当前对话进展(直连原始模型,不写入对话)",async run(n){if(n.session.messages.some(n=>"system"!==n.role))try{n.print("小结:"+await O(n.session.messages,n.config))}catch(e){n.print("生成小结失败:"+e.message)}else n.print("对话还没有内容。")}},{name:"distill",desc:"从当前对话提炼项目知识,写入长期记忆(供后续会话自动加载)",async run(n){if(!1===n.config.memoryEnabled)return void n.print("长期记忆已关闭(config.memory=false)。");if(!n.session.messages.some(n=>"system"!==n.role))return void n.print("对话还没有内容。");const{distillMemory:e}=await import("./transcript.js"),{loadMemory:t,appendMemory:o}=await import("./memory.js");try{const i=await e(n.session.messages,n.config,t(n.cwd));if(!i.length)return void n.print("未提炼出值得长期记住的新知识。");let s=0;for(const e of i)o(n.cwd,e)&&s++;n.print(`已写入 ${s} 条项目知识到长期记忆:\n`+i.map(n=>" · "+n).join("\n"))}catch(e){n.print("提炼失败:"+e.message)}}},{name:"doctor",desc:"本机环境体检(登录/网络/模型/git/工作目录/钩子)",async run(n){try{n.print(await K(n))}catch(e){n.print("体检失败:"+e.message)}}},{name:"config",desc:"查看配置文件路径与当前生效配置(密钥脱敏)",run(n){const e=n.config,t=Number(e.autoCompactTokens);var o;n.print(["配置"," 文件 "+r," 平台 "+e.baseUrl," 密钥 "+(o=e.apiKey,o?o.slice(0,4)+"…"+o.slice(-4)+`(${o.length}位)`:"(未设置)")," 模型 "+e.model," 推理强度 "+(e.reasoningEffort||"off")," 自动压缩 "+(t<0?"关闭":t>0?t.toLocaleString()+" tokens":"默认(80k)")," 记忆 "+(!1===e.memoryEnabled?"关":"开")," Markdown "+(!1===e.markdown?"关":"开")," 全局设置 "+N," 项目设置 "+P(n.cwd)," (多数项有对应斜杠命令直接改:/model /effort /autocompact /permissions …)"].join("\n"))}},{name:"effort",args:"[minimal|low|medium|high|off]",desc:"设置推理强度(reasoning_effort 透传;模型不支持则忽略)",run(n,e){const t=String(e||"").trim().toLowerCase();t?["minimal","low","medium","high","off"].includes(t)?(n.config.reasoningEffort="off"===t?"":t,i({reasoningEffort:n.config.reasoningEffort}),n.print("推理强度:"+(n.config.reasoningEffort||"off")+"(下一条消息起生效;模型不支持则被忽略)")):n.print("取值:minimal | low | medium | high | off"):n.print("当前推理强度:"+(n.config.reasoningEffort||"off")+"\n 用法:/effort minimal|low|medium|high|off")}},{name:"autocompact",args:"[N|off|status]",desc:"自动压缩上下文的触发阈值(token 数;off 关闭)",run(n,e){const t=String(e||"").trim().toLowerCase(),o=Number(n.config.autoCompactTokens)||0;if(!t||"status"===t)return void n.print("自动压缩:"+(o<0?"已关闭":o>0?`阈值 ${o.toLocaleString()} tokens`:"默认阈值 80,000 tokens")+"\n /autocompact <N> 设定 · /autocompact off 关闭 · /autocompact status 查看");if("off"===t)return n.config.autoCompactTokens=-1,i({autoCompactTokens:-1}),void n.print("已关闭自动压缩(仍可 /compact 手动压缩)。");const s=parseInt(t,10);!Number.isFinite(s)||s<1e3?n.print("请给 ≥1000 的 token 阈值,或 off / status。"):(n.config.autoCompactTokens=s,i({autoCompactTokens:s}),n.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(n,e){const{detectScanners:t,runScanners:o}=await import("./scan.js"),i=t(n.cwd);if(!i.length)return void n.print("未检测到可用扫描器。可安装 semgrep / trivy / gitleaks,或项目含 package.json(npm audit)等。");if("fix"===String(e||"").trim().toLowerCase())return{prompt:"请对本项目做安全/质量扫描并修复:依次运行并理解这些扫描器的输出——"+i.map(n=>"`"+n.command+"`("+n.why+")").join("、")+";对发现的问题逐个修复(升级/更换依赖、修代码、移除硬编码密钥等),修完重跑对应扫描确认清零;最后汇总修了什么。"};n.print("运行扫描器:"+i.map(n=>n.name).join(", ")+"(可能较慢)…");const s=await o(n.cwd,i);for(const e of s)n.print(`\n── ${e.name}(${0===e.code?"✅ 无高危":"⚠ 有发现"})──`),n.print(e.output.split("\n").slice(0,20).join("\n"));n.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(n,e){let t=String(e||"").trim();if(!t){if(t=S(n.cwd)||"",!t)return void n.print("未能自动探测验证命令。用法:/verify <命令>(如 /verify npm test)");n.print("自动探测到验证命令:"+t)}return{action:"autofix",command:t,maxIters:Math.min(10,Math.max(1,Number(n.config.verifyMaxIters)||5))}}},{name:"simplify",desc:"审查改动的可复用/简化/抽象层级并直接清理(只清理不找 bug)",run:()=>({prompt:"请只做质量清理(不找 bug):审查最近改动的复用(是否重复造轮子、可否复用现有工具/函数)、简化(是否有更直接的写法)、效率、以及抽象层级是否合适;把确定的改进直接用 edit_file 应用,保持与项目既有风格一致,改后跑测试/构建自检。若本地有 refactor-safely 技能可 load_skill 参照。最后说明改了什么、为什么。"})},{name:"insights",desc:"本机会话使用洞察(数量/轮数/活跃度/按项目·模型分布)",run(n){n.print(y(w(m(null),{cwd:n.cwd})))}},{name:"goal",args:"[目标 | clear]",desc:"设定目标(每轮注入,模型停止前自检是否达成;clear 清除)",run(n,e){const t=String(e||"").trim();if(t){if("clear"===t)return n.config.goal="",void n.print("已清除目标。");n.config.goal=t,n.print("已设定目标:"+t+"(每轮注入,模型将在停止前自检是否达成)")}else n.print(n.config.goal?"当前目标:"+n.config.goal+"\n /goal <新目标> 修改 · /goal clear 清除":"未设定目标。用法:/goal <目标>(模型会在停止前自检是否达成)")}},{name:"btw",args:"<问题>",desc:"问一个旁支问题(带当前上下文,但不写入对话、不调用工具)",async run(n,e){const t=String(e||"").trim();if(!t)return void n.print("用法:/btw <问题>(快速旁支问答,不影响主对话)");const o=G.test(String(n.config.model||""))?n.config.compactModel||s:n.config.model;try{const e=h(n.session.messages,n.session.compaction||{}),i=await d({baseUrl:n.config.baseUrl,apiKey:n.config.apiKey,model:o,messages:[...e,{role:"user",content:"(这是一个旁支问题,请结合上文直接简答,不要调用工具、不要展开无关内容)\n"+t}]});n.print("↪ "+(i.content||"(无回复)").trim())}catch(e){n.print("旁支问答失败:"+e.message)}}},{name:"ide",desc:"查看 IDE 集成状态(编辑器上下文与内联审阅)",run(n){const e=!!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(" 内联审阅 "+(e?"已连接(write/edit 交编辑器做 diff 审阅)":"未连接(CLI 直接写盘)")),s.push(" 客户端 "+(n.config.client||"cli")),s.push(" 当前文件 "+(t||"(无)")),s.push(" 选区 "+(o?o.length+" 字":"(无)")),s.push(" 诊断 "+(i?"有(来自 Problems 面板)":"(无)")),e||s.push(" (在 VS Code/IDEA 的 huicai 面板或其启动的终端里运行可启用内联审阅与编辑器上下文)"),n.print(s.join("\n"))}},{name:"branch",args:"[标题]",desc:"把当前对话另存为一个可 --resume 的分支副本(当前会话继续)",run(n,e){const t=n.session;if(!t.messages.some(n=>"system"!==n.role))return void n.print("当前对话为空,无需分支。");const o=f({cwd:n.cwd,model:t.model,messages:t.messages.map(n=>({...n}))});try{o.compaction=JSON.parse(JSON.stringify(t.compaction||{}))}catch{o.compaction={}}o.title=(String(e||"").trim()||t.title||"对话").slice(0,70)+"(分支)",o.lastPromptTokens=t.lastPromptTokens||0;try{u(o),u(t)}catch{}n.print(`已保存分支副本「${o.title}」(${o.id})。\n 当前会话继续;分支可用 huicai --resume 恢复到此刻状态。`)}},{name:"theme",args:"[default|mono]",desc:"输出主题:default 彩色 / mono 无色",run(n,e){const t=String(e||"").trim().toLowerCase();t?["default","mono"].includes(t)?(n.config.theme=t,$(t),i({theme:t}),n.print("主题:"+t+("mono"===t?"(已关闭颜色)":"(彩色)"))):n.print("取值:default | mono"):n.print("当前主题:"+(n.config.theme||"default")+"\n /theme default | mono(终端配色由终端掌控;mono=关闭颜色,便于复制/管道)")}},{name:"color",args:"["+b.join("|")+"]",desc:"设置强调色(提示符/高亮)",run(n,e){const t=String(e||"").trim().toLowerCase();t?k(t)?(n.config.accentColor=t,i({accentColor:t}),n.print("强调色:"+t)):n.print("可选:"+b.join(" / ")):n.print("当前强调色:"+v()+"\n 可选:"+b.join(" / "))}},{name:"tui",args:"[auto|rich|plain]",desc:"交互渲染器偏好(下次启动生效)",run(n,e){const t=String(e||"").trim().toLowerCase();t?["auto","rich","plain"].includes(t)?(n.config.tui=t,i({tui:t}),n.print("渲染器偏好:"+t+"(下次启动生效)")):n.print("取值:auto | rich | plain"):n.print("当前渲染器偏好:"+(n.config.tui||"auto")+"\n auto(TTY 下富 TUI)| rich | plain(下次启动生效;本次可加 --tui/--plain 覆盖)")}},{name:"advisor",args:"[模型id|off]",desc:"设置顾问模型(更强模型):智能体遇难题可 ask_advisor 咨询第二意见",async run(n,e){const t=String(e||"").trim();if(!t)return void n.print(n.config.advisorModel?"当前顾问模型:"+n.config.advisorModel+"(智能体可用 ask_advisor 咨询)":"未设置顾问模型。用法:/advisor <模型id>(设一个更强的模型;/models 看可用)· /advisor off 关闭");const o="off"===t?"":t;if(n.config.advisorModel=o,i({advisorModel:o}),Array.isArray(n.extraTools)){const e=n.extraTools.findIndex(n=>"ask_advisor"===n.spec?.function?.name);if(e>=0&&n.extraTools.splice(e,1),o){const{makeAdvisorTool:e}=await import("./advisor.js"),t=e({config:n.config});t&&n.extraTools.push(t)}}n.print(o?"顾问模型已设为 "+o+"(ask_advisor 工具已可用)":"已关闭顾问模型。")}},{name:"automodel",args:"[强模型id|off]",desc:"按任务自动选模型:复杂回合自动升配到该强模型,其余用当前模型",run(n,e){const t=String(e||"").trim();if(!t)return void n.print(n.config.autoModelStrong?"自动升配:开(复杂回合→"+n.config.autoModelStrong+",其余→"+n.config.model+")":"未开启。用法:/automodel <强模型id>(复杂回合自动用它;简单回合仍用当前便宜模型)· /models 看可用");const o="off"===t.toLowerCase()?"":t;n.config.autoModelStrong=o,i({autoModelStrong:o}),n.print(o?"按任务自动选模型:开(复杂回合→"+o+",其余→"+n.config.model+")":"已关闭自动升配。")}},{name:"embed",args:"[嵌入模型id|off]",desc:"search_code 向量语义重排:设一个嵌入模型走 /v1/embeddings 重排(空/off=纯 BM25)",run(n,e){const t=String(e||"").trim();if(!t)return void n.print(n.config.embeddingModel?"search_code 向量重排:开(嵌入模型 "+n.config.embeddingModel+")":"未启用向量重排(纯 BM25,离线可用)。用法:/embed <嵌入模型id>(需平台有该嵌入模型)· /embed off");const o="off"===t.toLowerCase()?"":t;n.config.embeddingModel=o,i({embeddingModel:o}),n.print(o?"search_code 向量语义重排:开(嵌入模型 "+o+";下次检索生效,BM25 召回后按向量相似度重排)":"已关闭向量重排(回到纯 BM25)。")}},{name:"budget",args:"[token数|off|status]",desc:"设置本项目 token 预算(软提醒;/cost 看进度)",run(n,e){const t=String(e||"").trim().toLowerCase(),o=(n.usage?.prompt||0)+(n.usage?.completion||0);if(!t||"status"===t){const e=Number(n.config.budgetTokens)||0;return void n.print(e>0?`token 预算:${o.toLocaleString()} / ${e.toLocaleString()}(${Math.round(o/e*100)}%)`:`未设预算(本次已用 ${o.toLocaleString()} tokens)。/budget <token数> 设定`)}if("off"===t)return n.config.budgetTokens=0,i({budgetTokens:0}),void n.print("已取消预算。");const s=parseInt(t,10);!Number.isFinite(s)||s<1e3?n.print("请给 ≥1000 的 token 数,或 off / status。"):(n.config.budgetTokens=s,i({budgetTokens:s}),n.print("token 预算设为 "+s.toLocaleString()+"(软提醒;/cost 显示进度)。"))}},{name:"debug",args:"[on|off]",desc:"调试输出(每轮打印模型/耗时/token)",run(n,e){const t=String(e||"").trim().toLowerCase(),o="on"===t||"off"!==t&&!n.config.debug;n.config.debug=o,i({debug:o}),n.print("调试输出:"+(o?"开(下一条消息起每轮打印 [debug] 行)":"关"))}},{name:"sandbox",args:"[on|off]",desc:"沙箱模式:禁 shell 与联网(仍可读写本地文件)",run(n,e){const t=String(e||"").trim().toLowerCase(),o="on"===t||"off"!==t&&"sandbox"!==V(n);X(n,o?"sandbox":"manual"),n.print(o?"沙箱模式:开。run_bash 与 fetch_url 将被拒绝;本地文件读写照常。":"沙箱模式:关(写/执行仍按权限审批)。")}},{name:"reload-skills",desc:"从磁盘重新加载技能(新增/改名技能生效;改正文已实时生效)",async run(n){const{makeSkillTools:e,loadLocalSkills:t}=await import("./skills.js");if(Array.isArray(n.extraTools)){for(let e=n.extraTools.length-1;e>=0;e--){const t=n.extraTools[e]?.spec?.function?.name;"load_skill"!==t&&"read_skill_file"!==t||n.extraTools.splice(e,1)}n.extraTools.push(...e(n.cwd))}const o=t(n.cwd);n.print(o.length?`已重载 ${o.length} 个技能:${o.map(n=>n.name).join(", ")}\n (load_skill 现可载入其中任意一个;系统提示词内的技能目录将在下次新会话刷新)`:"未发现本地技能(.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(n=>" "+n).join("\n")+"\n /add-dir <目录> 添加 · /add-dir remove <目录> 移除":"暂无附加目录。用法:/add-dir <目录>(可读/搜索,写仍限主目录)");if("remove"===r||"rm"===r){const n=e.resolve(t.cwd,s.slice(1).join(" ")),o=i.extraDirs.length;i.extraDirs=i.extraDirs.filter(e=>e!==n),Y(i);try{u(i)}catch{}return void t.print(i.extraDirs.length<o?"已移除:"+n:"未找到该附加目录:"+n)}const a=e.resolve(t.cwd,String(o).trim());let c=!1;try{c=n.statSync(a).isDirectory()}catch{}if(c)if(a!==t.cwd){i.extraDirs.includes(a)||i.extraDirs.push(a),Y(i);try{u(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(n){const{loadAgents:e}=await import("./subagent.js"),t=e(n.cwd);n.print(t.length?"子智能体人格(dispatch_agent 的 agent 参数可指定):\n"+t.map(n=>` ${n.name}${n.description?" — "+n.description.replace(/\s+/g," ").slice(0,60):""}`).join("\n"):"未定义子智能体人格。\n 在 .huicai/agents/<名字>.md 写 frontmatter(name/description)+正文(人格提示词)即可;\n dispatch_agent 默认用通用只读子智能体(无需定义也能用)。")}},{name:"orchestrate",args:"<目标>",desc:"把一个目标自动分解为并行子任务、多智能体协作完成",run(n,e){const t=String(e||"").trim();if(t)return{prompt:`目标:${t}\n\n请作为编排者(只调度不代工)按以下方式完成:\n1) 任务分配预检:先解析目标要什么,把它分解为若干**独立、可并行**的子任务(研究/检索/分析/定位),并确认每个子任务与所派子智能体的能力匹配——超出能力范围的部分如实说明,不硬派;\n2) 用 dispatch_agents 一次性并行委派这些子任务(每个说明必须独立自足——子智能体看不到本对话;可指定合适的子智能体人格);\n3) 信息流经你中转:需要衔接的结论由你提炼后放进下一批子任务的 context,再行派发;\n4) 汇总各子智能体结论形成整体方案或结果,并自查:每部分都有子任务产出支撑、无遗漏、未完成的如实说明;\n5) 若需落地改动,再据此逐步执行并自检(可用 /verify 让我自动跑验证)。\n优先用 dispatch_agents 并行,减少串行等待。`};n.print("用法:/orchestrate <较大的目标>(我会分解为并行子任务,用 dispatch_agents 并行推进后综合)")}},{name:"squads",desc:"列出可用专家团(内置10个+全局/项目自定义;复杂任务也会被自动选团)",async run(n){const{loadSquads:e}=await import("./squad.js"),t=e(n.cwd),o={project:"项目",global:"全局",builtin:"内置"};n.print(t.length?"专家团(/squad <名称> <任务> 交给团队完成;复杂任务对话中也会按「何时使用」自动选团):\n"+t.map(n=>` [${o[n.scope]||"自定义"}] ${n.name} — 主理人${n.leader} · ${n.members.length} 位成员`+(n.when?" · "+n.when.replace(/\s+/g," ").slice(0,46):n.description?" · "+n.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(n,e){const{loadSquads:t,squadCommandPrompt:o,ensureTeamDir:i}=await import("./squad.js"),s=t(n.cwd),r=String(e||"").trim(),a=r.indexOf(" "),c=a<0?r:r.slice(0,a),d=a<0?"":r.slice(a+1).trim(),m=s.find(n=>n.name===c);if(!r||!m||!d)return void n.print("用法:/squad <名称> <任务>"+(s.length?`\n可用专家团:${s.map(n=>n.name).join("、")}`:"\n还没有专家团定义,先 /squad-init <名称> 生成模板。")+(r&&!m&&c?`\n(未找到「${c}」)`:""));if(!m.members.length)return void n.print(`专家团「${c}」还没有成员,请编辑其定义文件补充「## 成员」。`);const l=i(n.cwd,m.name);return n.print(`已召集专家团「${m.name}」(主理人${m.leader},${m.members.length} 位成员)`+(l?`,团队工作区:${l}`:"")),{prompt:o(m,d,l)}}},{name:"squad-init",args:"[名称] [--from <内置团>]",desc:"生成专家团定义(空白模板,或以内置团为底稿定制)",async run(n,e){const t=await import("node:fs"),o=await import("node:path"),{SQUAD_TEMPLATE:i,loadSquads:s}=await import("./squad.js"),r=String(e||"").trim().split(/\s+/).filter(Boolean),a=r.indexOf("--from"),c=a>=0?r[a+1]:null,d=(a>=0?r.filter((n,e)=>e!==a&&e!==a+1):r)[0];let m,l;if(c){const e=s(n.cwd).find(n=>n.name===c);if(!e||!e.file)return void n.print(`未找到底稿团「${c}」(/squads 查看可用团名)`);l=(d||c).replace(/[^\w.\-一-龥]/g,"-"),m=t.readFileSync(e.file,"utf8").replace(/^(---[\s\S]*?\bname:\s*).*$/m,`$1${l}`)}else l=(d||"my-team").replace(/[^\w.\-一-龥]/g,"-"),m=i.replace("{NAME}",l);const p=o.join(n.cwd,".huicai","squads"),u=o.join(p,`${l}.md`);t.existsSync(u)?n.print(`已存在:${u}(直接编辑即可)`):(t.mkdirSync(p,{recursive:!0}),t.writeFileSync(u,m),n.print(`已生成专家团定义:${u}${c?`(底稿:${c}${l===c?",项目层将覆盖内置同名团":""})`:""}\n 编辑成员表(执行者可指 agent:本地人格 / platform:平台伪模型 / platform:auto:类型 自动绑定 / model:模型名)、成员人设、预检速查表与预设工作流后,/squad `+l+" <任务> 即可开工。"))}},{name:"experts",args:"[refresh|on|off]",desc:"平台能力自动编队:查看/刷新可用的专家团·工作流·智能体·数字人",async run(n,e){const{discoverExperts:t,makeExpertTools:o}=await import("./experts.js"),i=String(e||"").trim().toLowerCase(),s=n=>["consult_expert","consult_experts"].includes(n?.spec?.function?.name);if("off"===i){if(n.config.autoExperts=!1,Array.isArray(n.extraTools))for(let e=n.extraTools.length-1;e>=0;e--)s(n.extraTools[e])&&n.extraTools.splice(e,1);return void n.print('已关闭平台能力自动编队(本会话即时生效)。永久关闭:在 ~/.huicai/config.json 中设 "autoExperts": false')}if("on"===i){if(n.config.autoExperts=!0,Array.isArray(n.extraTools)&&!n.extraTools.some(s)){const e=await o({config:n.config});n.extraTools.push(...e),n.print(e.length?"已开启平台能力自动编队(本会话即时生效)。":"已开启,但当前无可编队的平台专家(需登录且账号下有可用资源,或当前驱动模型为伪模型)。")}else n.print("平台能力自动编队已是开启状态。");return}const r=await t(n.config,{force:"refresh"===i});if(!r.length)return void n.print("当前无可编队的平台专家。"+(n.config.apiKey?"(账号下暂无可用的专家团/工作流/智能体/数字人,或平台不可达)":"请先 huicai login 登录平台。"));const a={squad:"专家团",workflow:"工作流",agent:"智能体",datahuman:"数字人"},c=["平台能力编队(原始模型对话时可被自动咨询"+(!1===n.config.autoExperts?",当前已关闭,/experts on 开启":"")+"):"];for(const n of["squad","workflow","agent","datahuman"]){const e=r.filter(e=>e.type===n);if(e.length){c.push(` ${a[n]}(${e.length})`);for(const n of e)c.push(` ${n.id} — ${n.name}${n.description?":"+n.description.slice(0,50):""}`)}}c.push("refresh"===i?"(已强制刷新)":"(/experts refresh 强制刷新;/experts off 关闭)"),n.print(c.join("\n"))}},{name:"batch",args:"<大改动目标>",desc:"把一个大改动拆成独立子任务,在隔离 git worktree 中并行推进",run(n,e){const t=String(e||"").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\` 清理。)`};n.print("用法:/batch <一个可拆分的大改动目标>(拆成互不冲突的子任务,各在隔离 worktree 中落地)")}},{name:"fork",args:"<后台任务提示词>",desc:"派生后台智能体(继承当前对话副本,--yolo 自动放行,输出写日志)",async run(n,e){const t=String(e||"").trim();if(!t)return void n.print("用法:/fork <让后台智能体做什么>(以当前对话副本为上下文,后台运行,--yolo 自动放行)");const o=f({cwd:n.cwd,model:n.session.model,messages:n.session.messages.map(n=>({...n}))});try{o.compaction=JSON.parse(JSON.stringify(n.session.compaction||{}))}catch{o.compaction={}}o.title="(fork) "+(n.session.title||"");try{u(o)}catch{}const{forkAgent:i}=await import("./forks.js");let s;try{s=i({prompt:t,cwd:n.cwd,sessionId:o.id})}catch(e){return void n.print("无法派生后台任务:"+e.message)}n.print(`已派生后台智能体 pid ${s.pid}(--yolo 自动放行)\n 上下文:当前对话副本 ${o.id}\n 日志:${s.log}\n /tasks 查看 · /tasks kill ${s.pid} 终止`)}},{name:"tasks",args:"[kill <pid>]",desc:"查看/终止本会话派生的后台任务(/fork)",async run(n,e){const{listForks:t,killFork:o}=await import("./forks.js"),i=String(e||"").trim().split(/\s+/).filter(Boolean);if("kill"===i[0]){const e=Number(i[1]);if(!Number.isFinite(e))return void n.print("用法:/tasks kill <pid>");const t=o(e);return void n.print(t.ok?"已终止 pid "+e:"终止失败:"+(t.reason||""))}const s=t();s.length?n.print("后台任务:\n"+s.map(n=>` pid ${n.pid} · ${n.alive?"运行中":"已结束"} · ${l(n.started)} · ${n.prompt}\n 日志 ${n.log}`).join("\n")+"\n /tasks kill <pid> 终止"):n.print("本会话暂无后台任务(/fork <提示词> 派生)。")}},{name:"schedule",args:'[list | add "<cron>" "<提示词>" | remove <id>]',desc:"管理定时任务(生成 crontab 行交系统 cron 运行)",async run(n,e){const t=await import("./schedule.js"),o=String(e||"").trim(),i="huicai";if(!o||"list"===o){const e=t.listTasks();return e.length?void n.print("定时任务:\n"+e.map(n=>` #${n.id} ${n.cron} ${n.prompt.replace(/\s+/g," ").slice(0,40)}\n crontab: ${t.crontabLine(n,i)}`).join("\n")+"\n /schedule remove <id> 删除"):void n.print('暂无定时任务。\n 添加:/schedule add "*/30 * * * *" "跑测试并把结果写入 report.md"\n (随后把打印的 crontab 行加入 crontab -e)')}if(/^add\b/.test(o)){const e=o.match(/"([^"]*)"/g);if(!e||e.length<2)return void n.print('用法:/schedule add "<cron>" "<提示词>"(cron 与提示词都要加引号)');const s=e[0].slice(1,-1).trim(),r=e[1].slice(1,-1).trim();if(5!==s.split(/\s+/).length)return void n.print('cron 需 5 个字段,如 "*/30 * * * *"(分 时 日 月 周)');const{id:a}=t.addTask({cron:s,prompt:r,cwd:n.cwd});return void n.print("已添加定时任务 #"+a+"\n 加入 crontab(crontab -e):\n "+t.crontabLine({id:a,cron:s,prompt:r,cwd:n.cwd},i))}if(/^(remove|rm)\b/.test(o)){const e=o.split(/\s+/)[1];return void n.print(t.removeTask(e)?"已删除定时任务 "+e:"未找到定时任务 "+e)}n.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=e.resolve(t.cwd,i);let r=!1;try{r=n.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(n,e){const t=String(e||"").trim();if(!t)return void n.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()};n.print("用法:/loop <秒> <提示词> · /loop off")}},{name:"statusline",args:"<shell命令> | off",desc:"自定义 TUI 底栏(运行该命令,展示其 stdout)",run(n,e){const t=String(e||"").trim();if(!t)return void n.print("当前状态栏命令:"+(n.config.statusLine||"(默认)")+"\n /statusline <shell命令>(stdout 显示在 TUI 底栏)· /statusline off 恢复默认");const o="off"===t?"":t;n.config.statusLine=o,i({statusLine:o}),n.print(o?"状态栏命令已设置(TUI 底栏生效):"+o:"已恢复默认状态栏。")}},{name:"background",desc:"保存当前会话并给出在别处继续/后台运行的方式",run(n){try{u(n.session)}catch{}n.print(`会话已保存(${n.session.id})。\n · 在别处继续:huicai resume ${n.session.id}\n · 后台跑一个任务:/fork <提示词>(派生后台智能体,日志可查)\n · 定时运行:/schedule add "<cron>" "<提示词>"`)}},{name:"exit",desc:"退出",run:()=>({action:"exit"})}];export function listCommands(n){return[...z.map(n=>({name:n.name,args:n.args||"",desc:n.desc})),...loadCustomCommands(n).map(n=>({name:n.name,args:"",desc:n.desc}))]}export function loadCustomCommands(o){const i=[o?e.join(o,".huicai","commands"):null,e.join(t.homedir(),".huicai","commands")].filter(Boolean),s=new Map;for(const t of i){let o;try{o=n.readdirSync(t).filter(n=>n.endsWith(".md"))}catch{continue}for(const i of o){const o=i.replace(/\.md$/,"").trim();if(o&&!s.has(o)&&!z.some(n=>n.name===o))try{const r=n.readFileSync(e.join(t,i),"utf8"),{desc:a,body:c}=Z(r);s.set(o,{name:o,desc:a||"自定义命令",template:c})}catch{}}}return[...s.values()]}function Z(n){const e=/^---\n([\s\S]*?)\n---\n?/.exec(n);if(!e)return{desc:"",body:n.trim()};const t=e[1],o=/^description:\s*(.+)$/m.exec(t);return{desc:o?o[1].trim():"",body:n.slice(e[0].length).trim()}}export async function runSlash(n,e){const t=n.slice(1).split(/\s+/),o=(t[0]||"").trim(),i=t.slice(1).join(" ").trim();if("quit"===o)return{handled:!0,action:"exit"};const s=z.find(n=>n.name===o);if(s){return{handled:!0,...await s.run(e,i)||{}}}const r=loadCustomCommands(e.cwd).find(n=>n.name===o);if(r){let n=r.template;return n.includes("$ARGUMENTS")?n=n.replaceAll("$ARGUMENTS",i):i&&(n=n+"\n\n"+i),{handled:!0,prompt:n}}return e.print("未知命令。/help 查看可用命令。"),{handled:!0}}
|
|
1
|
+
import n from"node:fs";import t from"node:path";import e from"node:os";import{execSync as o}from"node:child_process";import{saveConfig as i,DEFAULT_MODEL as s,CONFIG_PATH as r}from"./config.js";import{listModels as a,fetchUsage as c,streamChat as d}from"./api.js";import{listSessions as m,relTime as l,turnCount as p,saveSession as u,createSession as f}from"./session.js";import{compactNow as g,buildModelMessages as h,DEFAULT_AUTOCOMPACT_TOKENS as w}from"./compaction.js";import{computeInsights as y,formatInsights as $}from"./insights.js";import{setColorMode as v,setAccent as k,getAccent as b,ACCENTS as S}from"./colors.js";import{detectVerifyCommand as j}from"./autofix.js";import{PERMISSION_MODES as x,MODE_ICON as T,MODE_LABEL as _,MODE_DESC as C}from"./permissions.js";import{loadAllowRules as M,describeRules as I,parseRule as L,addProjectRule as E,removeProjectRule as N,GLOBAL_SETTINGS_PATH as A,projectSettingsPath as D}from"./settings.js";import{contextReport as P}from"./context-report.js";import{renderDiff as U}from"./diffview.js";import{loadHooks as q,formatHooks as R}from"./hooks.js";import{copyToClipboard as F}from"./clipboard.js";import{toMarkdown as O,recapConversation as K}from"./transcript.js";import{runDoctor as B}from"./doctor.js";import{TOOL_SPECS as H}from"./tools.js";import{makeTodoTool as J}from"./todo.js";import{makeMemoryTool as G}from"./memory.js";const W=/^(agent|workflow|datahuman|squad)-/,Y="\n\n# 附加可访问目录(可用 read_file/list_files/grep 以绝对路径访问;写入仍限主目录)\n";function Q(n){const t=n.messages&&n.messages[0];if(!t||"system"!==t.role||"string"!=typeof t.content)return;const e=t.content.indexOf(Y);let o=e>=0?t.content.slice(0,e):t.content;const i=n.extraDirs||[];i.length&&(o+=Y+i.map(n=>"- "+n).join("\n")+"\n"),t.content=o}export const INIT_PROMPT="请为当前项目生成或完善 HUICAI.md(给终端智能体看的项目说明):先用 list_files/glob/read_file 快速调研项目(技术栈、目录结构、构建/测试/运行命令、代码约定),再用 write_file 创建或更新 HUICAI.md(若已存在则保留人工撰写的内容,增量完善)。内容包含:项目简介、关键目录、常用命令(构建/测试/运行)、代码风格与注意事项。控制在 100 行以内,用中文。";const V={manual:"manual","请求批准":"manual","审批":"manual",acceptedits:"acceptEdits","accept-edits":"acceptEdits","接受编辑":"acceptEdits","编辑":"acceptEdits",auto:"auto","自动":"auto",yolo:"auto","无限制":"auto",plan:"plan","计划":"plan",sandbox:"sandbox","沙箱":"sandbox"};function z(n){return n.permissions?.getMode?.()??(n.permissions?.isYolo?.()?"auto":"manual")}function X(n,t){n.permissions?.setMode?n.permissions.setMode(t):(n.permissions?.setYolo?.("auto"===t),n.permissions?.setAcceptEdits?.("acceptEdits"===t),n.permissions?.setPlanMode?.("plan"===t),n.permissions?.setSandbox?.("sandbox"===t)),n.config&&(n.config.planMode="plan"===t,n.config.sandbox="sandbox"===t)}const Z=[{name:"help",desc:"显示可用命令(含自定义命令)",run(n){const t=Z.map(n=>[`/${n.name}${n.args?" "+n.args:""}`,n.desc]);for(const e of loadCustomCommands(n.cwd))t.push([`/${e.name}`,`${e.desc}(自定义)`]);t.push(["! <命令>","直接执行 shell 命令并把输出加入上下文"]),t.push(["@<文件路径>","在消息中引用文件内容(自动展开给模型)"]);const e=Math.min(24,Math.max(...t.map(([n])=>n.length))+2);n.print(t.map(([n,t])=>" "+n.padEnd(e)+t).join("\n"))}},{name:"model",args:"[名称|auto]",desc:"查看或切换模型(auto=按账号自动选可用模型,与 VSCode 一致)",async run(n,t){if(t){if("auto"===t){try{const t=(await a(n.config.baseUrl,n.config.apiKey)).map(n=>n.id).filter(n=>!/^(agent|workflow|datahuman|squad)-/.test(n));n.config.model=(t.includes(s)?s:t[0])||n.config.model}catch{}return i({model:"auto"}),void n.print("Auto 已选:"+n.config.model)}try{const e=(await a(n.config.baseUrl,n.config.apiKey)).map(n=>n.id);if(!e.includes(t))return void n.print(`当前账号无权使用模型「${t}」。\n 可用模型:${e.slice(0,12).join(", ")}`+(e.length>12?` …(共 ${e.length} 个,/models 查看全部)`:"")+"\n 或用 /model auto 自动选择可用模型。")}catch(t){n.print(`(未能校验模型可用性:${t.message})仍按输入切换;若无权限,平台会在调用时拒绝。`)}n.config.model=t,i({model:t}),n.print("已切换模型:"+t)}else n.print("当前模型:"+(n.config.model||"(未设置)"))}},{name:"models",desc:"列出平台可用模型",async run(n){try{const t=await a(n.config.baseUrl,n.config.apiKey);n.print(t.map(n=>" "+n.id).join("\n")||"(无)")}catch(t){n.print("拉取失败:"+t.message)}}},{name:"usage",desc:"查看 Token 用量窗口(5小时/7天,按会员等级)",async run(n){const t=n=>"number"==typeof n&&Number.isFinite(n)?n:0,e=(n,e)=>(n=t(n),(e=t(e))<0?`${n.toLocaleString()} / 无限`:0===e?"该档未开放":`${n.toLocaleString()} / ${e.toLocaleString()} (${Math.min(100,Math.round(n/e*100))}%)`);try{const t=await c(n.config.baseUrl,n.config.apiKey);if(!t||"number"!=typeof t.used5h&&"number"!=typeof t.used7d)return void n.print("平台未返回用量数据(请确认平台已支持 /v1/usage)。");n.print(`Token 用量窗口\n 近 5 小时:${e(t.used5h,t.limit5h)}\n 近 7 天 :${e(t.used7d,t.limit7d)}`+(!t.allowed&&t.message?"\n "+t.message:""))}catch(t){n.print("拉取用量失败:"+t.message)}}},{name:"login",desc:"重新登录(浏览器授权;无浏览器请退出后 huicai login --paste)",async run(n){try{const{deviceLogin:t}=await import("./device.js");n.print("正在打开浏览器授权…");const e=await t(n.config.baseUrl);n.config.apiKey=e,i({apiKey:e}),n.print("登录成功。")}catch(t){n.print("登录未完成:"+t.message+"(可退出后用 huicai login --paste 粘贴令牌)")}}},{name:"logout",desc:"退出登录(清除本机保存的令牌)",run(n){n.config.apiKey="",i({apiKey:""}),n.print("已退出登录。后续对话需重新 /login 或 huicai login。")}},{name:"clear",desc:"开启新会话(旧会话仍可 --resume 恢复)",run:()=>({action:"clear"})},{name:"compact",desc:"立即压缩上下文(摘要独立存储,不影响页面与会话原文)",async run(n){n.session.compaction||(n.session.compaction={});const t=await g(n.session.messages,n.config,{state:n.session.compaction});n.print(t>0?`已压缩:${t} 条较早消息已摘要(原对话完整保留,仅发给模型的上下文变小)`:"暂无可压缩的中段历史(会话较短或摘要失败)")}},{name:"memory",args:"[clear]",desc:"查看本项目长期记忆(clear 清空)",async run(n,t){const{loadMemory:e,clearMemory:o,memoryPath:i}=await import("./memory.js");if("clear"===t)return void n.print(o(n.cwd)?"已清空本项目长期记忆。":"本项目暂无长期记忆。");const s=e(n.cwd);n.print(s?s+`\n(文件:${i(n.cwd)} · /memory clear 清空)`:"本项目暂无长期记忆(模型会经 memory_write 自动记录值得跨会话记住的信息;配置 memory:false 可关闭)。")}},{name:"decisions",args:"[关键字|sync]",desc:"列出本项目决策图谱(决策追溯);sync 与云端双向同步",async run(n,t){const e=String(t||"").trim().toLowerCase();if("sync"===e){const{baseUrl:t,apiKey:e}=n.config||{};if(!e)return void n.print("未登录,无法同步决策图(请先登录平台)。");const o=await import("./decisions.js"),i=await import("./api.js"),s=o.projectKey(n.cwd);let r=0;const a=o.unsyncedDecisions(n.cwd);if(a.length){const c=a.map(n=>({decisionId:n.id,title:n.title,status:n.status,payload:JSON.stringify(n),clientCreatedAt:n.createdAt}));try{r=await i.pushDecisions(t,e,s,c),o.markSynced(n.cwd,a.map(n=>n.id))}catch(t){return void n.print("决策同步失败(推送):"+t.message)}}let c=0;try{const r=await i.pullDecisions(t,e,s,o.getLastSync(n.cwd));c=o.mergeRemote(n.cwd,r),o.setLastSync(n.cwd,Date.now())}catch(t){return void n.print("决策同步失败(拉取):"+t.message)}return void n.print(`决策图同步完成:推送 ${r} 条,拉取新增 ${c} 条。`)}const{listDecisions:o}=await import("./decisions.js");let i=o(n.cwd);if(e&&(i=i.filter(n=>`${n.title} ${n.why} ${(n.tags||[]).join(" ")}`.toLowerCase().includes(e))),!i.length)return void n.print(e?"没有匹配的决策。":"本项目暂无决策记录(模型做取舍时会经 record_decision 自动记;也可用 /decision 手动记)。");const s=i.slice(0,40).map(n=>{const t="draft"===n.status?"◇":"superseded"===n.status?"▽":"◆",e=(n.alternatives||[]).length?` · ${n.alternatives.length} 备选`:"",o=new Date(n.createdAt).toLocaleString("zh-CN",{month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"}),i=n.anchor&&n.anchor.git?" · @"+String(n.anchor.git).slice(0,7):"";return` ${t} ${n.title}${e}\n ${o} · ${n.id}${i}`});n.print(`本项目决策(${i.length},◆正式 ◇草稿 ▽已取代):\n`+s.join("\n"))}},{name:"decision",args:"<what> | <why> | <备选1;备选2>",desc:"手动记录一个决策(| 分隔,备选用 ; 分隔)",async run(n,t){const e=String(t||"").trim();if(!e)return void n.print("用法:/decision <选择了什么> | <为什么> | <备选1;备选2>\n例:/decision 向量检索用 pgvector | 大库应用层余弦会超时 | 应用层余弦;外部向量库");const[o,i,s]=e.split("|").map(n=>(n||"").trim()),r=(s?s.split(";"):[]).map(n=>({option:(n||"").trim()})).filter(n=>n.option),{recordDecision:a}=await import("./decisions.js"),c=a(n.cwd,{what:o,title:o,why:i,alternatives:r,sessionId:n.config?.sessionId||""},{origin:"user"});n.print(`◆ 已记录决策:${c.title}${"draft"===c.status?"(无备选,存为待确认草稿)":""} · ${c.id}`)}},{name:"cost",desc:"查看本次运行累计 token 用量(及预算进度)",run(n){const t=n.usage.prompt+n.usage.completion,e=Number(n.config.budgetTokens)||0;n.print(`token 用量:输入 ${n.usage.prompt} / 输出 ${n.usage.completion}`+(n.session.lastPromptTokens?` · 当前上下文 ~${n.session.lastPromptTokens} tok`:"")+(e>0?`\n预算:${t.toLocaleString()} / ${e.toLocaleString()} tokens(${Math.round(t/e*100)}%)`+(t>=e?" ⚠ 已超预算":t>=.8*e?" ⚠ 接近预算":""):""))}},{name:"status",desc:"显示运行状态(版本/模型/目录/会话/MCP/权限规则)",run(n){const t=n.session,e=[...new Set((n.extraTools||[]).map(n=>n.spec?.function?.name||"").filter(n=>n.startsWith("mcp__")).map(n=>n.split("__")[1]))];let i="";try{i=o("git rev-parse --abbrev-ref HEAD",{cwd:n.cwd,stdio:["ignore","pipe","ignore"]}).toString().trim()}catch{}const s=M(n.cwd),r=z(n),a=[`版本 huicai ${n.version||""}`,`模型 ${n.config.model}`,`平台 ${n.config.baseUrl}`,`目录 ${n.cwd}${i?`(git:${i})`:""}`,`会话 ${t.id} · ${p(t.messages)} 轮${t.lastPromptTokens?` · 上下文 ~${t.lastPromptTokens} tok`:""}`,`token 输入 ${n.usage.prompt} / 输出 ${n.usage.completion}`,`模式 ${T[r]} ${_[r]}(${C[r]})`,`审批 允许规则 ${s.length} 条${n.config.goal?" · 🎯 目标已设":""}`,`MCP ${e.length?e.join(", "):"(未配置)"}`];n.print(a.join("\n"))}},{name:"sessions",desc:"列出当前目录的历史会话",run(n){const t=m(n.cwd);if(!t.length)return void n.print("该目录暂无历史会话。");const e=t.slice(0,15).map(t=>{const e=t.id===n.session.id?" ← 当前":"";return` ${l(t.updatedAt).padStart(8)} ${t.title} ${t.turns}轮 · ${t.id}${e}`});e.push("(退出后 huicai --resume 可选择恢复)"),n.print(e.join("\n"))}},{name:"mode",args:"["+x.join("|")+"]",desc:"对话模式:请求批准/接受编辑/自动/计划/沙箱(自动=无限制,任何命令免审批)",run(n,t){const e=z(n),o=String(t||"").trim();if(!o){const t=[`当前对话模式:${T[e]} ${_[e]}(${C[e]})`,"","可切换:"];for(const n of x)t.push(` ${n===e?"●":" "} ${T[n]} ${n.padEnd(11)} ${_[n]} — ${C[n]}`);return t.push(""," 用法:/mode <名称>(也可用 /yolo、/plan、/sandbox 快捷切换)"),void n.print(t.join("\n"))}const i=V[o]||V[o.toLowerCase()];i?(X(n,i),n.print(`对话模式 → ${T[i]} ${_[i]}(${C[i]})`+("plan"===i?"\n 只做只读调研出方案;/plan approve 批准并分步执行,或 /mode auto 直接放手执行。":"")+("auto"===i?"\n ⚠ 已开放所有权限:shell/Python/联网/自动化等任何命令都不再询问。":""))):n.print(`未知模式:${o}。可选:${x.join(" / ")}`)}},{name:"yolo",desc:"切换「自动」模式(无任何限制,任何命令免审批)",run(n){const t="auto"!==z(n);X(n,t?"auto":"manual"),n.print(t?"⚡ 自动模式:开。已开放所有权限,任何命令(含 shell/联网/自动化)都不再询问。":"🔒 自动模式:关。回到「请求批准」,写/执行按权限逐项审批。")}},{name:"cwd",desc:"显示当前工作目录",run(n){n.print(n.cwd)}},{name:"markdown",desc:"切换助手回复的 Markdown 渲染(TUI 富文本;关闭则显示原始文本,便于复制)",run(n){const t=!1===n.config.markdown;n.config.markdown=t,i({markdown:t}),n.print("Markdown 渲染:"+(t?"开(下一条回复起生效)":"关(显示原始 Markdown 文本)"))}},{name:"init",desc:"让智能体调研项目并生成/完善 HUICAI.md",run:()=>({prompt:INIT_PROMPT})},{name:"skills",desc:"列出本地技能(Agent Skills;pull/push 用终端命令 huicai skills)",async run(n){const{loadLocalSkills:t}=await import("./skills.js"),e=t(n.cwd);e.length?n.print(e.map(n=>` ${n.name}${n.version?" v"+n.version:""}(${"project"===n.scope?"项目":"全局"}${n.files.length?` · ${n.files.length} 附件`:""})${n.description?" "+n.description.slice(0,60):""}`).join("\n")+"\n(模型会按描述经 load_skill 按需载入;本会话启动时已注入目录)"):n.print("暂无本地技能(.huicai/skills 或 ~/.huicai/skills)。可用 huicai skills pull 从平台拉取。")}},{name:"mcp",desc:"列出已连接的 MCP 工具",run(n){const t=(n.extraTools||[]).filter(n=>(n.spec?.function?.name||"").startsWith("mcp__"));t.length?n.print(t.map(n=>` ${n.spec.function.name}${n.readOnly?"(只读)":""}`).join("\n")):n.print("未连接 MCP 服务。配置 ~/.huicai/mcp.json(通用 mcpServers 格式)后重启生效。")}},{name:"context",desc:"可视化当前上下文占用(各类别 token 估算)",run(n){const t=W.test(String(n.config.model||""));let e=[];if(!t)try{const t=J({state:{list:[]},emit(){}}).spec,o=!1!==n.config.memoryEnabled?G({cwd:n.cwd,emit(){}}).spec:null;e=[...H,t,...o?[o]:[],...(n.extraTools||[]).map(n=>n.spec).filter(Boolean)]}catch{e=H}n.print(P({messages:n.session.messages,compaction:n.session.compaction||{},specs:e,lastPromptTokens:n.session.lastPromptTokens||0,autoCompactTokens:Number(n.config.autoCompactTokens)||0,platform:t}))}},{name:"permissions",args:"[allow|deny <规则> | remove <规则>]",desc:"查看/增删工具权限规则(allow/deny)",run(n,t){const e=String(t||"").trim().split(/\s+/).filter(Boolean),o=(e.shift()||"").toLowerCase(),i=e.join(" ").trim();if(!o){const t=I(n.cwd),e=n=>n.length?n.map(n=>" "+n).join("\n"):" (无)";return void n.print("权限规则(deny 优先于 allow/yolo,对只读工具也生效)\n 全局 ~/.huicai/settings.json\n 允许:\n"+e(t.global.allow)+"\n 拒绝:\n"+e(t.global.deny)+"\n 项目 .huicai/settings.json\n 允许:\n"+e(t.project.allow)+"\n 拒绝:\n"+e(t.project.deny)+"\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 n.print("用法:/permissions remove <规则原文>(仅移除项目设置里的规则)");const t=N(n.cwd,i);return n.permissions?.reloadRules?.(),void n.print(t.removed?`已移除规则:${i}`:`未在项目设置中找到该规则:${i}`)}n.print("未知子命令。用法:/permissions [allow|deny|remove] <规则>")}else{if(!i||!L(i))return void n.print("规则无效。示例:run_bash(git *)、edit_file、fetch_url(https://a.com*)");try{const t=E(n.cwd,o,i);n.permissions?.reloadRules?.(),n.print(`已添加${"deny"===o?"拒绝":"允许"}规则:${i} → ${t}`)}catch(t){n.print("写入失败:"+t.message)}}}},{name:"diff",desc:"查看未提交改动(git)与本会话智能体改过的文件",run(n){n.print(U(n.cwd,n.session.messages))}},{name:"pr",args:"[commit|address]",desc:"自动提交并开 PR(生成规范 commit→建分支→push→gh 开 PR);address=按 review 评论自动改",async run(n,t){const e=String(t||"").trim().toLowerCase();if("address"===e||"comments"===e)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 t=await o({cwd:n.cwd,config:n.config,emit:t=>n.print(t),push:"commit"!==e,openPr:"commit"!==e});if(!t.ok)return void n.print("✗ "+t.error);n.print(t.pr?"✅ 已开 PR:"+t.pr:"✅ 完成(分支 "+t.branch+")"+(t.note?":"+t.note:""))}catch(t){n.print("PR 流程出错:"+t.message)}}},{name:"plan",args:"[on|off|approve]",desc:"计划模式:只读调研出方案→/plan approve 批准并分步执行",async run(n,t){const e=String(t||"").trim().toLowerCase();if("approve"===e||"go"===e){X(n,"manual"),n.print("计划已批准,开始分步执行。");try{const{recordDecision:t}=await import("./decisions.js"),e=n.session?.messages||[];let o="";for(let n=e.length-1;n>=0;n--)if("assistant"===e[n].role&&"string"==typeof e[n].content){o=e[n].content;break}const i=(o.split("\n").find(n=>n.trim())||"批准的执行计划").replace(/^[#>\-*\s]+/,"").slice(0,120);t(n.cwd,{what:"批准并执行方案:"+i,why:o?o.slice(0,2e3):"用户在计划模式下评审后批准执行。",tags:["计划"],status:"active",sessionId:n.config?.sessionId||""},{origin:"user"})}catch{}return{prompt:"计划已获批准。现在请按你刚才制定的计划(todo 列表)逐步执行:每完成一步用 todo_write 更新状态并简要自检;遇到与计划不符及时调整并说明;全部完成后总结改了什么、以及如何验证(可用 /verify 让我自动跑验证)。"}}const o="on"===e||"off"!==e&&"plan"!==z(n);X(n,o?"plan":"manual"),n.print(o?"计划模式:开。只做只读调研并用 todo_write 产出分步方案;写/执行会被拦截。\n 方案满意后用 /plan approve 批准并自动分步执行;或 /plan off 手动执行。":"计划模式:关。恢复正常执行(写/执行仍按权限审批)。")}},{name:"auto",args:"<目标>",desc:"自动规划并自动执行:只读调研出分步计划,确认「是否继续?」后自动逐步执行",run(n,t){const e=String(t||"").trim();if(e)return{action:"autorun",goal:e};n.print("用法:/auto <目标>\n 先自动规划(只读调研 + todo 分步计划),随后询问「是否继续?1、是 2、是,并允许在当前对话中后续相同操作不再询问 3、否」,确认后自动逐步执行并自检。")}},{name:"rewind",args:"[N [chat|code|both]]",desc:"检查点回滚:无参列出;/rewind N 回退对话,加 code/both 一并回滚文件",run(n,t){const e=n.checkpoints;if(!e||!e.list.length)return void n.print("暂无检查点(每个用户回合前自动记录,发生对话后可用)。");const o=String(t||"").trim().split(/\s+/).filter(Boolean);if(!o.length){const t=["检查点(回滚到某个回合之前)"];for(const n of e.list)t.push(` #${n.id} ${l(n.time)} ${n.label}`+(n.journal.length?` · 改动 ${n.journal.length} 处`:""));return t.push(" 用法:/rewind N(仅回退对话,安全)· /rewind N code(回滚文件)· /rewind N both"),t.push(" 提示:code/both 会覆盖当前文件,建议先 /diff 查看。"),void n.print(t.join("\n"))}const i=Number(o[0]);if(!Number.isFinite(i))return void n.print("用法:/rewind N [chat|code|both]");const s=["chat","code","both"].includes(o[1])?o[1]:"chat",r=e.restore(i,s,{session:n.session,cwd:n.cwd});if(!r.ok)return void n.print(r.error);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{u(n.session)}catch{}return n.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(n){n.print(R(q(n.cwd)))}},{name:"copy",args:"[N]",desc:"复制助手回复到剪贴板(默认最后一条;N=倒数第 N 条)",async run(n,t){const e=n.session.messages.filter(n=>"assistant"===n.role&&"string"==typeof n.content&&n.content.trim());if(!e.length)return void n.print("还没有可复制的助手回复。");const o=Math.max(1,parseInt(t,10)||1),i=e[e.length-o];if(!i)return void n.print(`只有 ${e.length} 条助手回复。`);const s=await F(i.content);n.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=O(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(n,t){const e=String(t||"").trim();if(e){n.session.title=e.slice(0,80);try{u(n.session)}catch{}n.print("会话已重命名为:"+n.session.title)}else n.print("当前标题:"+(n.session.title||"(未命名)")+"\n 用法:/rename <新标题>")}},{name:"recap",desc:"用一句话总结当前对话进展(直连原始模型,不写入对话)",async run(n){if(n.session.messages.some(n=>"system"!==n.role))try{n.print("小结:"+await K(n.session.messages,n.config))}catch(t){n.print("生成小结失败:"+t.message)}else n.print("对话还没有内容。")}},{name:"distill",desc:"从当前对话提炼项目知识,写入长期记忆(供后续会话自动加载)",async run(n){if(!1===n.config.memoryEnabled)return void n.print("长期记忆已关闭(config.memory=false)。");if(!n.session.messages.some(n=>"system"!==n.role))return void n.print("对话还没有内容。");const{distillMemory:t}=await import("./transcript.js"),{loadMemory:e,appendMemory:o}=await import("./memory.js");try{const i=await t(n.session.messages,n.config,e(n.cwd));if(!i.length)return void n.print("未提炼出值得长期记住的新知识。");let s=0;for(const t of i)o(n.cwd,t)&&s++;n.print(`已写入 ${s} 条项目知识到长期记忆:\n`+i.map(n=>" · "+n).join("\n"))}catch(t){n.print("提炼失败:"+t.message)}}},{name:"doctor",desc:"本机环境体检(登录/网络/模型/git/工作目录/钩子)",async run(n){try{n.print(await B(n))}catch(t){n.print("体检失败:"+t.message)}}},{name:"config",desc:"查看配置文件路径与当前生效配置(密钥脱敏)",run(n){const t=n.config,e=Number(t.autoCompactTokens);var o;n.print(["配置"," 文件 "+r," 平台 "+t.baseUrl," 密钥 "+(o=t.apiKey,o?o.slice(0,4)+"…"+o.slice(-4)+`(${o.length}位)`:"(未设置)")," 模型 "+t.model," 推理强度 "+(t.reasoningEffort||"off")," 自动压缩 "+(e<0?"关闭":(e>0?e:w).toLocaleString()+" tokens"+(e>0?"":"(默认)"))," 记忆 "+(!1===t.memoryEnabled?"关":"开")," Markdown "+(!1===t.markdown?"关":"开")," 全局设置 "+A," 项目设置 "+D(n.cwd)," (多数项有对应斜杠命令直接改:/model /effort /autocompact /permissions …)"].join("\n"))}},{name:"effort",args:"[minimal|low|medium|high|off]",desc:"设置推理强度(reasoning_effort 透传;模型不支持则忽略)",run(n,t){const e=String(t||"").trim().toLowerCase();e?["minimal","low","medium","high","off"].includes(e)?(n.config.reasoningEffort="off"===e?"":e,i({reasoningEffort:n.config.reasoningEffort}),n.print("推理强度:"+(n.config.reasoningEffort||"off")+"(下一条消息起生效;模型不支持则被忽略)")):n.print("取值:minimal | low | medium | high | off"):n.print("当前推理强度:"+(n.config.reasoningEffort||"off")+"\n 用法:/effort minimal|low|medium|high|off")}},{name:"autocompact",args:"[N|off|status]",desc:"自动压缩上下文的触发阈值(token 数;off 关闭)",run(n,t){const e=String(t||"").trim().toLowerCase(),o=Number(n.config.autoCompactTokens)||0;if(!e||"status"===e)return void n.print("自动压缩:"+(o<0?"已关闭":o>0?`阈值 ${o.toLocaleString()} tokens`:`默认阈值 ${w.toLocaleString()} tokens`)+"\n /autocompact <N> 设定 · /autocompact off 关闭 · /autocompact status 查看");if("off"===e)return n.config.autoCompactTokens=-1,i({autoCompactTokens:-1}),void n.print("已关闭自动压缩(仍可 /compact 手动压缩)。");const s=parseInt(e,10);!Number.isFinite(s)||s<1e3?n.print("请给 ≥1000 的 token 阈值,或 off / status。"):(n.config.autoCompactTokens=s,i({autoCompactTokens:s}),n.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(n,t){const{detectScanners:e,runScanners:o}=await import("./scan.js"),i=e(n.cwd);if(!i.length)return void n.print("未检测到可用扫描器。可安装 semgrep / trivy / gitleaks,或项目含 package.json(npm audit)等。");if("fix"===String(t||"").trim().toLowerCase())return{prompt:"请对本项目做安全/质量扫描并修复:依次运行并理解这些扫描器的输出——"+i.map(n=>"`"+n.command+"`("+n.why+")").join("、")+";对发现的问题逐个修复(升级/更换依赖、修代码、移除硬编码密钥等),修完重跑对应扫描确认清零;最后汇总修了什么。"};n.print("运行扫描器:"+i.map(n=>n.name).join(", ")+"(可能较慢)…");const s=await o(n.cwd,i);for(const t of s)n.print(`\n── ${t.name}(${0===t.code?"✅ 无高危":"⚠ 有发现"})──`),n.print(t.output.split("\n").slice(0,20).join("\n"));n.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(n,t){let e=String(t||"").trim();if(!e){if(e=j(n.cwd)||"",!e)return void n.print("未能自动探测验证命令。用法:/verify <命令>(如 /verify npm test)");n.print("自动探测到验证命令:"+e)}return{action:"autofix",command:e,maxIters:Math.min(10,Math.max(1,Number(n.config.verifyMaxIters)||5))}}},{name:"simplify",desc:"审查改动的可复用/简化/抽象层级并直接清理(只清理不找 bug)",run:()=>({prompt:"请只做质量清理(不找 bug):审查最近改动的复用(是否重复造轮子、可否复用现有工具/函数)、简化(是否有更直接的写法)、效率、以及抽象层级是否合适;把确定的改进直接用 edit_file 应用,保持与项目既有风格一致,改后跑测试/构建自检。若本地有 refactor-safely 技能可 load_skill 参照。最后说明改了什么、为什么。"})},{name:"insights",desc:"本机会话使用洞察(数量/轮数/活跃度/按项目·模型分布)",run(n){n.print($(y(m(null),{cwd:n.cwd})))}},{name:"goal",args:"[目标 | clear]",desc:"设定目标(每轮注入,模型停止前自检是否达成;clear 清除)",run(n,t){const e=String(t||"").trim();if(e){if("clear"===e)return n.config.goal="",void n.print("已清除目标。");n.config.goal=e,n.print("已设定目标:"+e+"(每轮注入,模型将在停止前自检是否达成)")}else n.print(n.config.goal?"当前目标:"+n.config.goal+"\n /goal <新目标> 修改 · /goal clear 清除":"未设定目标。用法:/goal <目标>(模型会在停止前自检是否达成)")}},{name:"btw",args:"<问题>",desc:"问一个旁支问题(带当前上下文,但不写入对话、不调用工具)",async run(n,t){const e=String(t||"").trim();if(!e)return void n.print("用法:/btw <问题>(快速旁支问答,不影响主对话)");const o=W.test(String(n.config.model||""))?n.config.compactModel||s:n.config.model;try{const t=h(n.session.messages,n.session.compaction||{}),i=await d({baseUrl:n.config.baseUrl,apiKey:n.config.apiKey,model:o,client:n.config.client,sessionId:n.config.sessionId,messages:[...t,{role:"user",content:"(这是一个旁支问题,请结合上文直接简答,不要调用工具、不要展开无关内容)\n"+e}]});n.print("↪ "+(i.content||"(无回复)").trim())}catch(t){n.print("旁支问答失败:"+t.message)}}},{name:"ide",desc:"查看 IDE 集成状态(编辑器上下文与内联审阅)",run(n){const t=!!process.env.HUICAI_IDE_PORT,e=process.env.HUICAI_IDE_FILE,o=process.env.HUICAI_IDE_SELECTION,i=process.env.HUICAI_IDE_DIAGNOSTICS,s=["IDE 集成"];s.push(" 内联审阅 "+(t?"已连接(write/edit 交编辑器做 diff 审阅)":"未连接(CLI 直接写盘)")),s.push(" 客户端 "+(n.config.client||"cli")),s.push(" 当前文件 "+(e||"(无)")),s.push(" 选区 "+(o?o.length+" 字":"(无)")),s.push(" 诊断 "+(i?"有(来自 Problems 面板)":"(无)")),t||s.push(" (在 VS Code/IDEA 的 huicai 面板或其启动的终端里运行可启用内联审阅与编辑器上下文)"),n.print(s.join("\n"))}},{name:"branch",args:"[标题]",desc:"把当前对话另存为一个可 --resume 的分支副本(当前会话继续)",run(n,t){const e=n.session;if(!e.messages.some(n=>"system"!==n.role))return void n.print("当前对话为空,无需分支。");const o=f({cwd:n.cwd,model:e.model,messages:e.messages.map(n=>({...n}))});try{o.compaction=JSON.parse(JSON.stringify(e.compaction||{}))}catch{o.compaction={}}o.title=(String(t||"").trim()||e.title||"对话").slice(0,70)+"(分支)",o.lastPromptTokens=e.lastPromptTokens||0;try{u(o),u(e)}catch{}n.print(`已保存分支副本「${o.title}」(${o.id})。\n 当前会话继续;分支可用 huicai --resume 恢复到此刻状态。`)}},{name:"theme",args:"[default|mono]",desc:"输出主题:default 彩色 / mono 无色",run(n,t){const e=String(t||"").trim().toLowerCase();e?["default","mono"].includes(e)?(n.config.theme=e,v(e),i({theme:e}),n.print("主题:"+e+("mono"===e?"(已关闭颜色)":"(彩色)"))):n.print("取值:default | mono"):n.print("当前主题:"+(n.config.theme||"default")+"\n /theme default | mono(终端配色由终端掌控;mono=关闭颜色,便于复制/管道)")}},{name:"color",args:"["+S.join("|")+"]",desc:"设置强调色(提示符/高亮)",run(n,t){const e=String(t||"").trim().toLowerCase();e?k(e)?(n.config.accentColor=e,i({accentColor:e}),n.print("强调色:"+e)):n.print("可选:"+S.join(" / ")):n.print("当前强调色:"+b()+"\n 可选:"+S.join(" / "))}},{name:"tui",args:"[auto|rich|plain]",desc:"交互渲染器偏好(下次启动生效)",run(n,t){const e=String(t||"").trim().toLowerCase();e?["auto","rich","plain"].includes(e)?(n.config.tui=e,i({tui:e}),n.print("渲染器偏好:"+e+"(下次启动生效)")):n.print("取值:auto | rich | plain"):n.print("当前渲染器偏好:"+(n.config.tui||"auto")+"\n auto(TTY 下富 TUI)| rich | plain(下次启动生效;本次可加 --tui/--plain 覆盖)")}},{name:"advisor",args:"[模型id|off]",desc:"设置顾问模型(更强模型):智能体遇难题可 ask_advisor 咨询第二意见",async run(n,t){const e=String(t||"").trim();if(!e)return void n.print(n.config.advisorModel?"当前顾问模型:"+n.config.advisorModel+"(智能体可用 ask_advisor 咨询)":"未设置顾问模型。用法:/advisor <模型id>(设一个更强的模型;/models 看可用)· /advisor off 关闭");const o="off"===e?"":e;if(n.config.advisorModel=o,i({advisorModel:o}),Array.isArray(n.extraTools)){const t=n.extraTools.findIndex(n=>"ask_advisor"===n.spec?.function?.name);if(t>=0&&n.extraTools.splice(t,1),o){const{makeAdvisorTool:t}=await import("./advisor.js"),e=t({config:n.config});e&&n.extraTools.push(e)}}n.print(o?"顾问模型已设为 "+o+"(ask_advisor 工具已可用)":"已关闭顾问模型。")}},{name:"automodel",args:"[强模型id|off]",desc:"按任务自动选模型:复杂回合自动升配到该强模型,其余用当前模型",run(n,t){const e=String(t||"").trim();if(!e)return void n.print(n.config.autoModelStrong?"自动升配:开(复杂回合→"+n.config.autoModelStrong+",其余→"+n.config.model+")":"未开启。用法:/automodel <强模型id>(复杂回合自动用它;简单回合仍用当前便宜模型)· /models 看可用");const o="off"===e.toLowerCase()?"":e;n.config.autoModelStrong=o,i({autoModelStrong:o}),n.print(o?"按任务自动选模型:开(复杂回合→"+o+",其余→"+n.config.model+")":"已关闭自动升配。")}},{name:"embed",args:"[嵌入模型id|off]",desc:"search_code 向量语义重排:设一个嵌入模型走 /v1/embeddings 重排(空/off=纯 BM25)",run(n,t){const e=String(t||"").trim();if(!e)return void n.print(n.config.embeddingModel?"search_code 向量重排:开(嵌入模型 "+n.config.embeddingModel+")":"未启用向量重排(纯 BM25,离线可用)。用法:/embed <嵌入模型id>(需平台有该嵌入模型)· /embed off");const o="off"===e.toLowerCase()?"":e;n.config.embeddingModel=o,i({embeddingModel:o}),n.print(o?"search_code 向量语义重排:开(嵌入模型 "+o+";下次检索生效,BM25 召回后按向量相似度重排)":"已关闭向量重排(回到纯 BM25)。")}},{name:"budget",args:"[token数|off|status]",desc:"设置本项目 token 预算(软提醒;/cost 看进度)",run(n,t){const e=String(t||"").trim().toLowerCase(),o=(n.usage?.prompt||0)+(n.usage?.completion||0);if(!e||"status"===e){const t=Number(n.config.budgetTokens)||0;return void n.print(t>0?`token 预算:${o.toLocaleString()} / ${t.toLocaleString()}(${Math.round(o/t*100)}%)`:`未设预算(本次已用 ${o.toLocaleString()} tokens)。/budget <token数> 设定`)}if("off"===e)return n.config.budgetTokens=0,i({budgetTokens:0}),void n.print("已取消预算。");const s=parseInt(e,10);!Number.isFinite(s)||s<1e3?n.print("请给 ≥1000 的 token 数,或 off / status。"):(n.config.budgetTokens=s,i({budgetTokens:s}),n.print("token 预算设为 "+s.toLocaleString()+"(软提醒;/cost 显示进度)。"))}},{name:"debug",args:"[on|off]",desc:"调试输出(每轮打印模型/耗时/token)",run(n,t){const e=String(t||"").trim().toLowerCase(),o="on"===e||"off"!==e&&!n.config.debug;n.config.debug=o,i({debug:o}),n.print("调试输出:"+(o?"开(下一条消息起每轮打印 [debug] 行)":"关"))}},{name:"sandbox",args:"[on|off]",desc:"沙箱模式:禁 shell 与联网(仍可读写本地文件)",run(n,t){const e=String(t||"").trim().toLowerCase(),o="on"===e||"off"!==e&&"sandbox"!==z(n);X(n,o?"sandbox":"manual"),n.print(o?"沙箱模式:开。run_bash 与 fetch_url 将被拒绝;本地文件读写照常。":"沙箱模式:关(写/执行仍按权限审批)。")}},{name:"reload-skills",desc:"从磁盘重新加载技能(新增/改名技能生效;改正文已实时生效)",async run(n){const{makeSkillTools:t,loadLocalSkills:e}=await import("./skills.js");if(Array.isArray(n.extraTools)){for(let t=n.extraTools.length-1;t>=0;t--){const e=n.extraTools[t]?.spec?.function?.name;"load_skill"!==e&&"read_skill_file"!==e||n.extraTools.splice(t,1)}n.extraTools.push(...t(n.cwd))}const o=e(n.cwd);n.print(o.length?`已重载 ${o.length} 个技能:${o.map(n=>n.name).join(", ")}\n (load_skill 现可载入其中任意一个;系统提示词内的技能目录将在下次新会话刷新)`:"未发现本地技能(.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(n=>" "+n).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(t=>t!==n),Q(i);try{u(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),Q(i);try{u(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(n){const{loadAgents:t}=await import("./subagent.js"),e=t(n.cwd);n.print(e.length?"子智能体人格(dispatch_agent 的 agent 参数可指定):\n"+e.map(n=>` ${n.name}${n.description?" — "+n.description.replace(/\s+/g," ").slice(0,60):""}`).join("\n"):"未定义子智能体人格。\n 在 .huicai/agents/<名字>.md 写 frontmatter(name/description)+正文(人格提示词)即可;\n dispatch_agent 默认用通用只读子智能体(无需定义也能用)。")}},{name:"orchestrate",args:"<目标>",desc:"把一个目标自动分解为并行子任务、多智能体协作完成",run(n,t){const e=String(t||"").trim();if(e)return{prompt:`目标:${e}\n\n请作为编排者(只调度不代工)按以下方式完成:\n1) 任务分配预检:先解析目标要什么,把它分解为若干**独立、可并行**的子任务(研究/检索/分析/定位),并确认每个子任务与所派子智能体的能力匹配——超出能力范围的部分如实说明,不硬派;\n2) 用 dispatch_agents 一次性并行委派这些子任务(每个说明必须独立自足——子智能体看不到本对话;可指定合适的子智能体人格);\n3) 信息流经你中转:需要衔接的结论由你提炼后放进下一批子任务的 context,再行派发;\n4) 汇总各子智能体结论形成整体方案或结果,并自查:每部分都有子任务产出支撑、无遗漏、未完成的如实说明;\n5) 若需落地改动,再据此逐步执行并自检(可用 /verify 让我自动跑验证)。\n优先用 dispatch_agents 并行,减少串行等待。`};n.print("用法:/orchestrate <较大的目标>(我会分解为并行子任务,用 dispatch_agents 并行推进后综合)")}},{name:"squads",desc:"列出可用专家团(内置10个+全局/项目自定义;复杂任务也会被自动选团)",async run(n){const{loadSquads:t}=await import("./squad.js"),e=t(n.cwd),o={project:"项目",global:"全局",builtin:"内置"};n.print(e.length?"专家团(/squad <名称> <任务> 交给团队完成;复杂任务对话中也会按「何时使用」自动选团):\n"+e.map(n=>` [${o[n.scope]||"自定义"}] ${n.name} — 主理人${n.leader} · ${n.members.length} 位成员`+(n.when?" · "+n.when.replace(/\s+/g," ").slice(0,46):n.description?" · "+n.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(n,t){const{loadSquads:e,squadCommandPrompt:o,ensureTeamDir:i}=await import("./squad.js"),s=e(n.cwd),r=String(t||"").trim(),a=r.indexOf(" "),c=a<0?r:r.slice(0,a),d=a<0?"":r.slice(a+1).trim(),m=s.find(n=>n.name===c);if(!r||!m||!d)return void n.print("用法:/squad <名称> <任务>"+(s.length?`\n可用专家团:${s.map(n=>n.name).join("、")}`:"\n还没有专家团定义,先 /squad-init <名称> 生成模板。")+(r&&!m&&c?`\n(未找到「${c}」)`:""));if(!m.members.length)return void n.print(`专家团「${c}」还没有成员,请编辑其定义文件补充「## 成员」。`);const l=i(n.cwd,m.name);return n.print(`已召集专家团「${m.name}」(主理人${m.leader},${m.members.length} 位成员)`+(l?`,团队工作区:${l}`:"")),{prompt:o(m,d,l)}}},{name:"squad-init",args:"[名称] [--from <内置团>]",desc:"生成专家团定义(空白模板,或以内置团为底稿定制)",async run(n,t){const e=await import("node:fs"),o=await import("node:path"),{SQUAD_TEMPLATE:i,loadSquads:s}=await import("./squad.js"),r=String(t||"").trim().split(/\s+/).filter(Boolean),a=r.indexOf("--from"),c=a>=0?r[a+1]:null,d=(a>=0?r.filter((n,t)=>t!==a&&t!==a+1):r)[0];let m,l;if(c){const t=s(n.cwd).find(n=>n.name===c);if(!t||!t.file)return void n.print(`未找到底稿团「${c}」(/squads 查看可用团名)`);l=(d||c).replace(/[^\w.\-一-龥]/g,"-"),m=e.readFileSync(t.file,"utf8").replace(/^(---[\s\S]*?\bname:\s*).*$/m,`$1${l}`)}else l=(d||"my-team").replace(/[^\w.\-一-龥]/g,"-"),m=i.replace("{NAME}",l);const p=o.join(n.cwd,".huicai","squads"),u=o.join(p,`${l}.md`);e.existsSync(u)?n.print(`已存在:${u}(直接编辑即可)`):(e.mkdirSync(p,{recursive:!0}),e.writeFileSync(u,m),n.print(`已生成专家团定义:${u}${c?`(底稿:${c}${l===c?",项目层将覆盖内置同名团":""})`:""}\n 编辑成员表(执行者可指 agent:本地人格 / platform:平台伪模型 / platform:auto:类型 自动绑定 / model:模型名)、成员人设、预检速查表与预设工作流后,/squad `+l+" <任务> 即可开工。"))}},{name:"experts",args:"[refresh|on|off]",desc:"平台能力自动编队:查看/刷新可用的专家团·工作流·智能体·数字人",async run(n,t){const{discoverExperts:e,makeExpertTools:o}=await import("./experts.js"),i=String(t||"").trim().toLowerCase(),s=n=>["consult_expert","consult_experts"].includes(n?.spec?.function?.name);if("off"===i){if(n.config.autoExperts=!1,Array.isArray(n.extraTools))for(let t=n.extraTools.length-1;t>=0;t--)s(n.extraTools[t])&&n.extraTools.splice(t,1);return void n.print('已关闭平台能力自动编队(本会话即时生效)。永久关闭:在 ~/.huicai/config.json 中设 "autoExperts": false')}if("on"===i){if(n.config.autoExperts=!0,Array.isArray(n.extraTools)&&!n.extraTools.some(s)){const t=await o({config:n.config});n.extraTools.push(...t),n.print(t.length?"已开启平台能力自动编队(本会话即时生效)。":"已开启,但当前无可编队的平台专家(需登录且账号下有可用资源,或当前驱动模型为伪模型)。")}else n.print("平台能力自动编队已是开启状态。");return}const r=await e(n.config,{force:"refresh"===i});if(!r.length)return void n.print("当前无可编队的平台专家。"+(n.config.apiKey?"(账号下暂无可用的专家团/工作流/智能体/数字人,或平台不可达)":"请先 huicai login 登录平台。"));const a={squad:"专家团",workflow:"工作流",agent:"智能体",datahuman:"数字人"},c=["平台能力编队(原始模型对话时可被自动咨询"+(!1===n.config.autoExperts?",当前已关闭,/experts on 开启":"")+"):"];for(const n of["squad","workflow","agent","datahuman"]){const t=r.filter(t=>t.type===n);if(t.length){c.push(` ${a[n]}(${t.length})`);for(const n of t)c.push(` ${n.id} — ${n.name}${n.description?":"+n.description.slice(0,50):""}`)}}c.push("refresh"===i?"(已强制刷新)":"(/experts refresh 强制刷新;/experts off 关闭)"),n.print(c.join("\n"))}},{name:"batch",args:"<大改动目标>",desc:"把一个大改动拆成独立子任务,在隔离 git worktree 中并行推进",run(n,t){const e=String(t||"").trim();if(e)return{prompt:`目标(需拆分并行落地):${e}\n\n请这样做:\n1) 先把目标拆成若干**相互独立、可并行**的改动子任务(尽量互不冲突的文件/模块);\n2) 对每个子任务,用 \`git worktree add -b huicai-batch/<名> <路径>\` 建隔离工作区,在其中完成该子任务的改动并 commit(不影响主工作区);\n3) 全部完成后,汇总每个 worktree 的分支与改动;需要时可对每个分支执行 \`huicai pr\` 开 PR。\n(并行调研可用 dispatch_agents;改动务必隔离在各自 worktree,避免互相踩踏。完成后可 \`git worktree remove\` 清理。)`};n.print("用法:/batch <一个可拆分的大改动目标>(拆成互不冲突的子任务,各在隔离 worktree 中落地)")}},{name:"fork",args:"<后台任务提示词>",desc:"派生后台智能体(继承当前对话副本,--yolo 自动放行,输出写日志)",async run(n,t){const e=String(t||"").trim();if(!e)return void n.print("用法:/fork <让后台智能体做什么>(以当前对话副本为上下文,后台运行,--yolo 自动放行)");const o=f({cwd:n.cwd,model:n.session.model,messages:n.session.messages.map(n=>({...n}))});try{o.compaction=JSON.parse(JSON.stringify(n.session.compaction||{}))}catch{o.compaction={}}o.title="(fork) "+(n.session.title||"");try{u(o)}catch{}const{forkAgent:i}=await import("./forks.js");let s;try{s=i({prompt:e,cwd:n.cwd,sessionId:o.id})}catch(t){return void n.print("无法派生后台任务:"+t.message)}n.print(`已派生后台智能体 pid ${s.pid}(--yolo 自动放行)\n 上下文:当前对话副本 ${o.id}\n 日志:${s.log}\n /tasks 查看 · /tasks kill ${s.pid} 终止`)}},{name:"tasks",args:"[kill <pid>]",desc:"查看/终止本会话派生的后台任务(/fork)",async run(n,t){const{listForks:e,killFork:o}=await import("./forks.js"),i=String(t||"").trim().split(/\s+/).filter(Boolean);if("kill"===i[0]){const t=Number(i[1]);if(!Number.isFinite(t))return void n.print("用法:/tasks kill <pid>");const e=o(t);return void n.print(e.ok?"已终止 pid "+t:"终止失败:"+(e.reason||""))}const s=e();s.length?n.print("后台任务:\n"+s.map(n=>` pid ${n.pid} · ${n.alive?"运行中":"已结束"} · ${l(n.started)} · ${n.prompt}\n 日志 ${n.log}`).join("\n")+"\n /tasks kill <pid> 终止"):n.print("本会话暂无后台任务(/fork <提示词> 派生)。")}},{name:"schedule",args:'[list | add "<cron>" "<提示词>" | remove <id>]',desc:"管理定时任务(生成 crontab 行交系统 cron 运行)",async run(n,t){const e=await import("./schedule.js"),o=String(t||"").trim(),i="huicai";if(!o||"list"===o){const t=e.listTasks();return t.length?void n.print("定时任务:\n"+t.map(n=>` #${n.id} ${n.cron} ${n.prompt.replace(/\s+/g," ").slice(0,40)}\n crontab: ${e.crontabLine(n,i)}`).join("\n")+"\n /schedule remove <id> 删除"):void n.print('暂无定时任务。\n 添加:/schedule add "*/30 * * * *" "跑测试并把结果写入 report.md"\n (随后把打印的 crontab 行加入 crontab -e)')}if(/^add\b/.test(o)){const t=o.match(/"([^"]*)"/g);if(!t||t.length<2)return void n.print('用法:/schedule add "<cron>" "<提示词>"(cron 与提示词都要加引号)');const s=t[0].slice(1,-1).trim(),r=t[1].slice(1,-1).trim();if(5!==s.split(/\s+/).length)return void n.print('cron 需 5 个字段,如 "*/30 * * * *"(分 时 日 月 周)');const{id:a}=e.addTask({cron:s,prompt:r,cwd:n.cwd});return void n.print("已添加定时任务 #"+a+"\n 加入 crontab(crontab -e):\n "+e.crontabLine({id:a,cron:s,prompt:r,cwd:n.cwd},i))}if(/^(remove|rm)\b/.test(o)){const t=o.split(/\s+/)[1];return void n.print(e.removeTask(t)?"已删除定时任务 "+t:"未找到定时任务 "+t)}n.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(n,t){const e=String(t||"").trim();if(!e)return void n.print("用法:/loop <秒> <提示词>(如 /loop 300 检查构建是否通过)· /loop off 停止");if("off"===e)return{action:"loop",stop:!0};const o=/^(\d+)\s+([\s\S]+)$/.exec(e);if(o)return{action:"loop",seconds:Math.max(10,parseInt(o[1],10)),prompt:o[2].trim()};n.print("用法:/loop <秒> <提示词> · /loop off")}},{name:"statusline",args:"<shell命令> | off",desc:"自定义 TUI 底栏(运行该命令,展示其 stdout)",run(n,t){const e=String(t||"").trim();if(!e)return void n.print("当前状态栏命令:"+(n.config.statusLine||"(默认)")+"\n /statusline <shell命令>(stdout 显示在 TUI 底栏)· /statusline off 恢复默认");const o="off"===e?"":e;n.config.statusLine=o,i({statusLine:o}),n.print(o?"状态栏命令已设置(TUI 底栏生效):"+o:"已恢复默认状态栏。")}},{name:"background",desc:"保存当前会话并给出在别处继续/后台运行的方式",run(n){try{u(n.session)}catch{}n.print(`会话已保存(${n.session.id})。\n · 在别处继续:huicai resume ${n.session.id}\n · 后台跑一个任务:/fork <提示词>(派生后台智能体,日志可查)\n · 定时运行:/schedule add "<cron>" "<提示词>"`)}},{name:"exit",desc:"退出",run:()=>({action:"exit"})}];export function listCommands(n){return[...Z.map(n=>({name:n.name,args:n.args||"",desc:n.desc})),...loadCustomCommands(n).map(n=>({name:n.name,args:"",desc:n.desc}))]}export function loadCustomCommands(o){const i=[o?t.join(o,".huicai","commands"):null,t.join(e.homedir(),".huicai","commands")].filter(Boolean),s=new Map;for(const e of i){let o;try{o=n.readdirSync(e).filter(n=>n.endsWith(".md"))}catch{continue}for(const i of o){const o=i.replace(/\.md$/,"").trim();if(o&&!s.has(o)&&!Z.some(n=>n.name===o))try{const r=n.readFileSync(t.join(e,i),"utf8"),{desc:a,body:c}=nn(r);s.set(o,{name:o,desc:a||"自定义命令",template:c})}catch{}}}return[...s.values()]}function nn(n){const t=/^---\n([\s\S]*?)\n---\n?/.exec(n);if(!t)return{desc:"",body:n.trim()};const e=t[1],o=/^description:\s*(.+)$/m.exec(e);return{desc:o?o[1].trim():"",body:n.slice(t[0].length).trim()}}export async function runSlash(n,t){const e=n.slice(1).split(/\s+/),o=(e[0]||"").trim(),i=e.slice(1).join(" ").trim();if("quit"===o)return{handled:!0,action:"exit"};const s=Z.find(n=>n.name===o);if(s){return{handled:!0,...await s.run(t,i)||{}}}const r=loadCustomCommands(t.cwd).find(n=>n.name===o);if(r){let n=r.template;return n.includes("$ARGUMENTS")?n=n.replaceAll("$ARGUMENTS",i):i&&(n=n+"\n\n"+i),{handled:!0,prompt:n}}return t.print("未知命令。/help 查看可用命令。"),{handled:!0}}
|
package/src/compaction.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";import{stripMarkup as n}from"./markup-filter.js";export function buildModelMessages(t,e){return e&&e.summary&&e.upTo>1&&!(e.upTo>t.length)?[...t.slice(0,1),{role:"user",content:"【上下文压缩】以下是此前多轮对话与工具执行的摘要(原始对话仍完整保留在会话中,仅为节省上下文而摘要):\n"+e.summary},...t.slice(e.upTo)]:t}export async function compactIfNeeded(e,
|
|
1
|
+
import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";import{stripMarkup as n}from"./markup-filter.js";export const DEFAULT_AUTOCOMPACT_TOKENS=1e6;const o=12e4,r=/[-ヿ㐀-鿿가-豈--]/g;export function estimateTokens(t){if(!t)return 0;const e=String(t),n=(e.match(r)||[]).length,o=e.length-n;return Math.ceil(1.6*n+o/4)}const s=new WeakMap;function c(t){if(!t||"object"!=typeof t)return 0;const e=function(t){const e=t.content;return 31*("string"==typeof e||Array.isArray(e)?e.length:0)+(t.tool_calls?t.tool_calls.length:0)}(t),n=s.get(t);if(void 0!==n&&n.key===e)return n.tok;let o=0;if("string"==typeof t.content)o+=estimateTokens(t.content);else if(Array.isArray(t.content))for(const e of t.content)"text"===e?.type&&e.text?o+=estimateTokens(e.text):"image_url"===e?.type&&(o+=300);return t.tool_calls&&(o+=estimateTokens(JSON.stringify(t.tool_calls))),s.set(t,{key:e,tok:o}),o}export function estimateMessagesTokens(t){let e=0;for(const n of t||[])e+=c(n);return e}export function buildModelMessages(t,e){return e&&e.summary&&e.upTo>1&&!(e.upTo>t.length)?[...t.slice(0,1),{role:"user",content:"【上下文压缩】以下是此前多轮对话与工具执行的摘要(原始对话仍完整保留在会话中,仅为节省上下文而摘要):\n"+e.summary},...t.slice(e.upTo)]:t}export async function compactIfNeeded(t,e,n={}){const r=n.state||{},s=n.keepRecent||(n.force?3:6);if(!t||t.length<6)return 0;const c=Number(e?.autoCompactTokens);if(!n.force&&Number.isFinite(c)&&c<0)return 0;const l=c>0?c:1e6,i=buildModelMessages(t,r);if(!n.force){const t=Number(n.lastPromptTokens)>l,e=n.thresholdChars?function(t){let e=0;for(const n of t)n.content&&(e+=String(n.content).length),n.tool_calls&&(e+=JSON.stringify(n.tool_calls).length);return e}(i)>n.thresholdChars:estimateMessagesTokens(i)>l;if(!t&&!e)return 0}const a=Math.max(Number(r.upTo)||1,1),m=[];for(let e=a;e<t.length;e++)"user"===t[e].role&&m.push(e);const p=m.length>s?m[m.length-s]:function(t,e){const n=t.length-e;if(n<6)return-1;let o=e+Math.floor(.7*n);for(;o<t.length&&"tool"===t[o].role;)o++;return o<t.length?o:-1}(t,a);if(!(p>a)||p-a<2)return 0;const h=t.slice(a,p);let g;try{g=await async function(t,e,n){let r=n;for(const n of function(t,e){const n=[];let o=[],r=0;for(const s of t){const t=f([s]).length+1;o.length&&r+t>e&&(n.push(o),o=[],r=0),o.push(s),r+=t}o.length&&n.push(o);return n.length?n:[t]}(t,o))if(r=await u(n,e,r),!r||!r.trim())return r;return r}(h,e,r.summary)}catch{return 0}return g&&g.trim()?(r.summary=g.trim(),r.upTo=p,p-a):0}export function compactNow(t,e,n={}){return compactIfNeeded(t,e,{...n,force:!0})}const l=/^(agent|workflow|datahuman|squad)-/;function i(t){if(t?.compactModel)return t.compactModel;const n=t?.model||"";return l.test(n)?e:n}async function u(e,o,r){const s=[];r&&r.trim()&&s.push("【先前的摘要(需并入新摘要)】\n"+r.trim()),s.push(f(e));const c=await t({baseUrl:o.baseUrl,apiKey:o.apiKey,model:i(o),client:o.client,aux:!0,messages:[{role:"system",content:"你是对话/执行记录压缩器。把下面的记录(含可能存在的先前摘要)压缩成一份简洁但信息完整的中文摘要:保留已完成的关键步骤、工具得到的重要事实/数据/结论、涉及的文件路径、尚未解决的问题与下一步线索。只依据记录、不要臆造、不要寒暄。"},{role:"user",content:s.join("\n\n")}]});return n(c.content).text}function a(t,e){const n=String(t||"");if(n.length<=e)return n;const o=Math.floor(.75*e);return n.slice(0,o)+`\n…【已省略 ${n.length-e} 字符】…\n`+n.slice(-(e-o))}function f(t){const e=[];for(const n of t)if("assistant"===n.role&&n.tool_calls?.length){const t=n.tool_calls.map(t=>`${t.function?.name}(${String(t.function?.arguments||"").slice(0,200)})`).join(" ");e.push(`[助手调用工具] ${t}${n.content?" | "+a(n.content,1200):""}`)}else"tool"===n.role?e.push(`[工具结果] ${a(n.content,1200)}`):n.content&&e.push(`[${n.role}] ${a(m(n.content),6e3)}`);return e.join("\n")}function m(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}
|
package/src/computer.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:fs";import t from"node:os";import r from"node:path";import{execFile as o}from"node:child_process";import{presence as n}from"./presence.js";import{escapeAppleScript as i,escapePowerShell as a}from"./notify.js";const s=3e4;export const TIER={VIEW:"view",CLICK:"click",FULL:"full"};export function tierForApp(e){const t=String(e||"").toLowerCase();return/(chrome|safari|firefox|edge|arc|brave|opera|chromium)/.test(t)?TIER.VIEW:/(terminal|iterm|warp|kitty|alacritty|hyper|powershell|cmd\.exe|conhost|vscode|visual studio code|cursor|windsurf|zed|xcode|intellij|pycharm|webstorm|goland|clion|rider|android studio|eclipse)/.test(t)?TIER.CLICK:TIER.FULL}export function tierAllows(e,t){return e===TIER.VIEW?"screenshot"===t:e!==TIER.CLICK||("screenshot"===t||"click"===t||"move"===t)}function c(e,t,{timeout:r=2e4,encoding:n}={}){return new Promise(i=>{let a;try{a=o(e,t,{timeout:r,windowsHide:!0,maxBuffer:33554432,encoding:n||"utf8"},(e,t,r)=>i({ok:!e,out:String(t||""),err:e?String(r||"").trim()||e.message:""}))}catch(e){return void i({ok:!1,out:"",err:e.message})}a.on("error",e=>i({ok:!1,out:"",err:e.message}))})}const p=new Map;async function l(e){if(p.has(e))return p.get(e);const t=await c("win32"===process.platform?"where":"which",[e],{timeout:3e3});return p.set(e,t.ok),t.ok}async function u(e){return await l("gnome-screenshot")?{bin:"gnome-screenshot",args:["-f",e]}:await l("scrot")?{bin:"scrot",args:["-o",e]}:await l("spectacle")?{bin:"spectacle",args:["-b","-n","-o",e]}:await l("import")?{bin:"import",args:["-window","root",e]}:await l("grim")?{bin:"grim",args:[e]}:null}export async function capabilities(e=process.platform){const t=[];if("darwin"===e)return{platform:e,screenshot:!0,pointer:!0,keyboard:!0,nativeScript:!0,missing:t,hint:"首次使用需在「系统设置 → 隐私与安全性」里为运行本程序的终端/应用勾选「屏幕录制」(截图)与「辅助功能」(点击/键入)。"};if("win32"===e){const r=await l("powershell");return r||t.push("powershell"),{platform:e,screenshot:r,pointer:r,keyboard:r,nativeScript:r,missing:t,hint:r?"":"未找到 powershell:Windows 上的屏幕操作依赖它(系统自带,请检查 PATH)。"}}const r=await l("xdotool"),o=await u("/dev/null");return r||t.push("xdotool"),o||t.push("gnome-screenshot/scrot/spectacle/import/grim"),{platform:e,screenshot:!!o,pointer:r,keyboard:r,nativeScript:!1,missing:t,hint:t.length?`Linux 需要:${t.join("、")}(如 sudo apt install xdotool gnome-screenshot)。Wayland 下 xdotool 可能无效,届时请改用应用自带的 CLI。`:""}}export function shotDir(){return r.join(t.tmpdir(),"huicai-screenshots")}export async function screenshot({app:t="",region:o=null,platform:n=process.platform}={}){const p=(e.mkdirSync(shotDir(),{recursive:!0}),r.join(shotDir(),`shot-${Date.now()}-${Math.random().toString(36).slice(2,6)}.png`));let l,m;if("darwin"===n){t&&await c("osascript",["-e",`tell application "${i(t)}" to activate`],{timeout:8e3});const r=["-x","-t","png"];if(o&&Number.isFinite(o.x)&&r.push("-R",`${Math.round(o.x)},${Math.round(o.y)},${Math.round(o.width)},${Math.round(o.height)}`),r.push(p),l=await c("screencapture",r,{timeout:s}),!l.ok||!e.existsSync(p))return{error:`截屏失败:${l.err||"未生成图片"}。macOS 需要「屏幕录制」权限(系统设置 → 隐私与安全性 → 屏幕录制,勾选运行本程序的终端/应用后重启它)。`}}else if("win32"===n){t&&await activateApp(t,n);const r=["Add-Type -AssemblyName System.Windows.Forms,System.Drawing;",o&&Number.isFinite(o.x)?`$r = New-Object System.Drawing.Rectangle(${Math.round(o.x)},${Math.round(o.y)},${Math.round(o.width)},${Math.round(o.height)});`:"$r = [System.Windows.Forms.SystemInformation]::VirtualScreen;","$bmp = New-Object System.Drawing.Bitmap($r.Width, $r.Height);","$g = [System.Drawing.Graphics]::FromImage($bmp);","$g.CopyFromScreen($r.Location, [System.Drawing.Point]::Empty, $r.Size);",`$bmp.Save('${a(p)}', [System.Drawing.Imaging.ImageFormat]::Png);`,"$g.Dispose(); $bmp.Dispose();"].join(" ");if(l=await c("powershell",["-NoProfile","-NonInteractive","-Command",r],{timeout:s}),!l.ok||!e.existsSync(p))return{error:`截屏失败:${l.err||"未生成图片"}`}}else{t&&await activateApp(t,n);const r=await u(p);if(!r)return{error:"截屏失败:未找到截图命令(可安装 gnome-screenshot / scrot / import / grim)"};if(l=await c(r.bin,r.args,{timeout:s}),!l.ok||!e.existsSync(p))return{error:`截屏失败:${l.err||"未生成图片"}`}}try{m="data:image/png;base64,"+e.readFileSync(p).toString("base64")}catch(e){return{error:"截屏已生成但读取失败:"+e.message,file:p}}return{file:p,dataUri:m,label:t||"整屏"}}export async function screenSize(e=process.platform){if("darwin"===e){const e=await c("osascript",["-e",'tell application "Finder" to get bounds of window of desktop']),t=/(-?\d+),\s*(-?\d+),\s*(-?\d+),\s*(-?\d+)/.exec(e.out||"");return t?{width:Number(t[3])-Number(t[1]),height:Number(t[4])-Number(t[2])}:null}if("win32"===e){const e=await c("powershell",["-NoProfile","-NonInteractive","-Command",'Add-Type -AssemblyName System.Windows.Forms; $v=[System.Windows.Forms.SystemInformation]::VirtualScreen; "$($v.Width)x$($v.Height)"']),t=/(\d+)x(\d+)/.exec(e.out||"");return t?{width:Number(t[1]),height:Number(t[2])}:null}const t=await c("xdotool",["getdisplaygeometry"]),r=/(\d+)\s+(\d+)/.exec(t.out||"");return r?{width:Number(r[1]),height:Number(r[2])}:null}export async function listWindows(e=process.platform){if("darwin"===e){const e='tell application "System Events" to get {name, title of front window} of (every application process whose visible is true and background only is false)',t=await c("osascript",["-e",e]);if(!t.ok){const e=await c("osascript",["-e",'tell application "System Events" to get name of (every application process whose visible is true and background only is false)']);return e.ok?e.out.trim():""}return t.out.trim()}if("win32"===e){return(await c("powershell",["-NoProfile","-NonInteractive","-Command",'Get-Process | Where-Object { $_.MainWindowTitle -ne "" } | Select-Object -First 40 ProcessName,MainWindowTitle | Format-Table -AutoSize | Out-String -Width 200'])).out.trim()}if(await l("wmctrl")){return(await c("wmctrl",["-l"])).out.trim()}return(await c("xdotool",["search","--onlyvisible","--name","."])).out.trim()}export async function activateApp(e,t=process.platform){const r=String(e||"").trim();if(!r)return{error:"缺少应用名"};if("darwin"===t){const e=await c("osascript",["-e",`tell application "${i(r)}" to activate`]);return e.ok?{ok:`已切到前台:${r}`}:{error:`无法激活「${r}」:${e.err}`}}if("win32"===t){const e=await c("powershell",["-NoProfile","-NonInteractive","-Command",`$w = New-Object -ComObject WScript.Shell; if (-not $w.AppActivate('${a(r)}')) { throw '未找到窗口' }`]);return e.ok?{ok:`已切到前台:${r}`}:{error:`无法激活「${r}」:${e.err}`}}const o=await c("xdotool",["search","--onlyvisible","--name",r,"windowactivate","--sync","%1"]);return o.ok?{ok:`已切到前台:${r}`}:{error:`无法激活「${r}」:${o.err}(Linux 需要 xdotool,Wayland 下可能无效)`}}export const MAC_KEY_CODES={return:36,enter:36,tab:48,space:49,delete:51,backspace:51,escape:53,esc:53,forwarddelete:117,left:123,right:124,down:125,up:126,home:115,end:119,pageup:116,pagedown:121,f1:122,f2:120,f3:99,f4:118,f5:96,f6:97,f7:98,f8:100,f9:101,f10:109,f11:103,f12:111};export const WIN_KEYS={return:"{ENTER}",enter:"{ENTER}",tab:"{TAB}",space:" ",delete:"{DEL}",backspace:"{BACKSPACE}",escape:"{ESC}",esc:"{ESC}",left:"{LEFT}",right:"{RIGHT}",up:"{UP}",down:"{DOWN}",home:"{HOME}",end:"{END}",pageup:"{PGUP}",pagedown:"{PGDN}",f1:"{F1}",f2:"{F2}",f3:"{F3}",f4:"{F4}",f5:"{F5}",f6:"{F6}",f7:"{F7}",f8:"{F8}",f9:"{F9}",f10:"{F10}",f11:"{F11}",f12:"{F12}"};export function normalizeModifier(e){const t=String(e||"").toLowerCase().trim();return/^(cmd|command|meta|super|win|windows)$/.test(t)?"command":/^(ctrl|control)$/.test(t)?"control":/^(opt|option|alt)$/.test(t)?"option":/^shift$/.test(t)?"shift":""}export function buildKeyCommand(e,t=[],r=process.platform){const o=String(e||"").toLowerCase().trim();if(!o)return{error:"缺少按键名"};const n=(Array.isArray(t)?t:[t]).map(normalizeModifier).filter(Boolean);if("darwin"===r){const t=n.length?` using {${n.map(e=>e+" down").join(", ")}}`:"";return null!=MAC_KEY_CODES[o]?{kind:"osa",payload:`key code ${MAC_KEY_CODES[o]}${t}`}:1===o.length?{kind:"osa",payload:`keystroke "${i(o)}"${t}`}:{error:`不认识的按键:${e}(可用:单个字符,或 ${Object.keys(MAC_KEY_CODES).join("/")})`}}if("win32"===r){const t=n.map(e=>"control"===e?"^":"option"===e?"%":"shift"===e?"+":"^").join(""),r=WIN_KEYS[o]||(1===o.length?o:null);return r?{kind:"sendkeys",payload:t?`${t}(${r})`:r}:{error:`不认识的按键:${e}`}}const a={command:"super",control:"ctrl",option:"alt",shift:"shift"},s={escape:"Escape",esc:"Escape",return:"Return",enter:"Return",tab:"Tab",space:"space",delete:"BackSpace",backspace:"BackSpace",forwarddelete:"Delete",left:"Left",right:"Right",up:"Up",down:"Down",home:"Home",end:"End",pageup:"Page_Up",pagedown:"Page_Down"}[o]||(/^f\d{1,2}$/.test(o)?o.toUpperCase():1===o.length?o:null);return s?{kind:"xdotool",payload:[...n.map(e=>a[e]),s].join("+")}:{error:`不认识的按键:${e}`}}export async function click({x:e,y:t,button:r="left",count:o=1,platform:n=process.platform}={}){const i=Math.round(Number(e)),a=Math.round(Number(t));if(!Number.isFinite(i)||!Number.isFinite(a))return{error:"坐标不合法"};const s=Math.min(3,Math.max(1,Math.round(Number(o)||1)));if("darwin"===n){if(await l("cliclick")){const e="right"===r?"rc":"middle"===r?"mc":s>=2?"dc":"c",t=await c("cliclick",[`${e}:${i},${a}`]);return t.ok?{ok:`已${s>=2?"双击":"点击"}(${i}, ${a})`}:{error:"cliclick 失败:"+t.err}}if("left"!==r||s>1)return{error:`macOS 上的${"left"!==r?"右键/中键":"双击"}需要 cliclick(brew install cliclick);当前只能左键单击。也可以改用 run_native_script 写 AppleScript 直接指挥该应用(更可靠)。`};const e=await c("osascript",["-e",`tell application "System Events" to click at {${i}, ${a}}`]);return e.ok?{ok:`已点击 (${i}, ${a})`}:{error:`点击失败:${e.err}。macOS 需要「辅助功能」权限(系统设置 → 隐私与安全性 → 辅助功能,勾选运行本程序的终端/应用后重启它)。`}}if("win32"===n){const e=`Add-Type -Name U -Namespace W -MemberDefinition '[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);[DllImport("user32.dll")] public static extern void mouse_event(uint f, uint dx, uint dy, uint d, int e);'; [W.U]::SetCursorPos(${i}, ${a}); 1..${s} | ForEach-Object { [W.U]::mouse_event(${{left:"0x0002",right:"0x0008",middle:"0x0020"}[r]||"0x0002"},0,0,0,0); [W.U]::mouse_event(${{left:"0x0004",right:"0x0010",middle:"0x0040"}[r]||"0x0004"},0,0,0,0); Start-Sleep -Milliseconds 60 }`,t=await c("powershell",["-NoProfile","-NonInteractive","-Command",e]);return t.ok?{ok:`已点击 (${i}, ${a})`}:{error:"点击失败:"+t.err}}const p="right"===r?"3":"middle"===r?"2":"1",u=await c("xdotool",["mousemove",String(i),String(a),"click","--repeat",String(s),p]);return u.ok?{ok:`已点击 (${i}, ${a})`}:{error:"点击失败:"+u.err+"(Linux 需 xdotool)"}}export async function moveMouse({x:e,y:t,platform:r=process.platform}={}){const o=Math.round(Number(e)),n=Math.round(Number(t));if(!Number.isFinite(o)||!Number.isFinite(n))return{error:"坐标不合法"};if("darwin"===r){if(!await l("cliclick"))return{error:"macOS 上移动鼠标需要 cliclick(brew install cliclick);多数场景直接 computer_click 即可。"};const e=await c("cliclick",[`m:${o},${n}`]);return e.ok?{ok:`鼠标已移到 (${o}, ${n})`}:{error:e.err}}if("win32"===r){const e=`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.Cursor]::Position = New-Object System.Drawing.Point(${o}, ${n})`,t=await c("powershell",["-NoProfile","-NonInteractive","-Command","Add-Type -AssemblyName System.Drawing; "+e]);return t.ok?{ok:`鼠标已移到 (${o}, ${n})`}:{error:t.err}}const i=await c("xdotool",["mousemove",String(o),String(n)]);return i.ok?{ok:`鼠标已移到 (${o}, ${n})`}:{error:i.err}}export async function drag({fromX:e,fromY:t,toX:r,toY:o,platform:n=process.platform}={}){const i=[e,t,r,o].map(e=>Math.round(Number(e)));if(i.some(e=>!Number.isFinite(e)))return{error:"坐标不合法"};const[a,s,p,u]=i;if("darwin"===n){if(!await l("cliclick"))return{error:"macOS 上拖拽需要 cliclick(brew install cliclick);也可改用 run_native_script 让应用自己完成该操作。"};const e=await c("cliclick",[`dd:${a},${s}`,`dm:${p},${u}`,`du:${p},${u}`]);return e.ok?{ok:`已从 (${a}, ${s}) 拖到 (${p}, ${u})`}:{error:e.err}}if("win32"===n){const e=`Add-Type -Name U -Namespace W -MemberDefinition '[DllImport("user32.dll")] public static extern bool SetCursorPos(int x, int y);[DllImport("user32.dll")] public static extern void mouse_event(uint f, uint dx, uint dy, uint d, int e);'; [W.U]::SetCursorPos(${a}, ${s}); [W.U]::mouse_event(0x0002,0,0,0,0); Start-Sleep -Milliseconds 120; [W.U]::SetCursorPos(${p}, ${u}); Start-Sleep -Milliseconds 120; [W.U]::mouse_event(0x0004,0,0,0,0)`,t=await c("powershell",["-NoProfile","-NonInteractive","-Command",e]);return t.ok?{ok:`已从 (${a}, ${s}) 拖到 (${p}, ${u})`}:{error:t.err}}const m=await c("xdotool",["mousemove",String(a),String(s),"mousedown","1","mousemove",String(p),String(u),"mouseup","1"]);return m.ok?{ok:`已从 (${a}, ${s}) 拖到 (${p}, ${u})`}:{error:m.err}}export async function scroll({direction:e="down",amount:t=3,x:r,y:o,platform:n=process.platform}={}){const i=Math.min(30,Math.max(1,Math.round(Number(t)||3))),a=["up","down","left","right"].includes(String(e))?String(e):"down";if(Number.isFinite(Number(r))&&Number.isFinite(Number(o))){const e=await moveMouse({x:r,y:o,platform:n});if(e.error&&"darwin"!==n)return e}if("darwin"===n){const e=`tell application "System Events" to repeat ${i} times\nkey code ${"down"===a?121:"up"===a?116:"left"===a?123:124}\nend repeat`,t=await c("osascript",["-e",e]);return t.ok?{ok:`已向${"down"===a?"下":"up"===a?"上":"left"===a?"左":"右"}滚动 ${i} 次(macOS 用翻页/方向键实现)`}:{error:`滚动失败:${t.err}(需要「辅助功能」权限)`}}if("win32"===n){const e="up"===a?120:"down"===a?-120:0;if(!e)return{error:"Windows 暂不支持横向滚动,可改用方向键(computer_key)"};const t=`Add-Type -Name U -Namespace W -MemberDefinition '[DllImport("user32.dll")] public static extern void mouse_event(uint f, uint dx, uint dy, int d, int e);'; 1..${i} | ForEach-Object { [W.U]::mouse_event(0x0800,0,0,${e},0); Start-Sleep -Milliseconds 40 }`,r=await c("powershell",["-NoProfile","-NonInteractive","-Command",t]);return r.ok?{ok:`已滚动 ${i} 格`}:{error:"滚动失败:"+r.err}}const s="up"===a?"4":"down"===a?"5":"left"===a?"6":"7",p=await c("xdotool",["click","--repeat",String(i),s]);return p.ok?{ok:`已滚动 ${i} 格`}:{error:"滚动失败:"+p.err}}export async function typeText({text:e,platform:t=process.platform}={}){const r=String(null==e?"":e);if(!r)return{error:"没有要键入的文本"};if(r.length>4e3)return{error:`文本过长(${r.length} > 4000 字符)。大段内容请写进文件再让应用打开,不要"打"进界面。`};const o=r.split(/\r?\n/);if("darwin"===t){const e=o.map((e,t)=>(t?"key code 36\n":"")+(e?`keystroke "${i(e)}"\n`:"")).join(""),t=await c("osascript",["-e",`tell application "System Events"\n${e}end tell`],{timeout:6e4});return t.ok?{ok:`已键入 ${r.length} 个字符`}:{error:`键入失败:${t.err}(需要「辅助功能」权限)`}}if("win32"===t){const e=e=>e.replace(/([+^%~(){}[\]])/g,"{$1}"),t=o.map(e).join("{ENTER}"),n=await c("powershell",["-NoProfile","-NonInteractive","-Command",`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${a(t)}')`],{timeout:6e4});return n.ok?{ok:`已键入 ${r.length} 个字符`}:{error:"键入失败:"+n.err}}const n=await c("xdotool",["type","--clearmodifiers","--delay","12","--",r],{timeout:6e4});return n.ok?{ok:`已键入 ${r.length} 个字符`}:{error:"键入失败:"+n.err}}export async function pressKey({key:e,modifiers:t=[],platform:r=process.platform}={}){const o=buildKeyCommand(e,t,r);if(o.error)return{error:o.error};const n=[...(Array.isArray(t)?t:[t]).map(normalizeModifier).filter(Boolean),e].join("+");if("osa"===o.kind){const e=await c("osascript",["-e",`tell application "System Events" to ${o.payload}`]);return e.ok?{ok:`已按下 ${n}`}:{error:`按键失败:${e.err}(需要「辅助功能」权限)`}}if("sendkeys"===o.kind){const e=await c("powershell",["-NoProfile","-NonInteractive","-Command",`Add-Type -AssemblyName System.Windows.Forms; [System.Windows.Forms.SendKeys]::SendWait('${a(o.payload)}')`]);return e.ok?{ok:`已按下 ${n}`}:{error:"按键失败:"+e.err}}const i=await c("xdotool",["key","--clearmodifiers",o.payload]);return i.ok?{ok:`已按下 ${n}`}:{error:"按键失败:"+i.err}}export async function runNativeScript({script:e,language:t="",timeout:r=12e4,platform:o=process.platform}={}){const n=String(e||"").trim();if(!n)return{error:"脚本为空"};if("darwin"===o){const e=/^(jxa|javascript)$/i.test(String(t))?["-l","JavaScript"]:[],o=await c("osascript",[...e,"-e",n],{timeout:Math.min(6e5,Number(r)||12e4)});return o.ok?{ok:o.out.trim()||"(脚本执行完成,无输出)"}:{error:`AppleScript 执行失败:${o.err}`}}if("win32"===o){const e=await c("powershell",["-NoProfile","-NonInteractive","-Command",n],{timeout:Math.min(6e5,Number(r)||12e4)});return e.ok?{ok:e.out.trim()||"(脚本执行完成,无输出)"}:{error:`PowerShell 执行失败:${e.err}`}}return{error:"Linux 没有统一的应用脚本接口。请改用该应用自带的 CLI/D-Bus 接口(run_bash),或用 computer_* 屏幕操作。"}}const m=(e,t,r={},o=[])=>({type:"function",function:{name:e,description:t,parameters:{type:"object",properties:r,required:o}}}),d=e=>({type:"number",description:e}),f=e=>({type:"string",description:e});export function computerUseEnabled(e={}){return"0"!==process.env.HUICAI_COMPUTER_USE&&("1"===process.env.HUICAI_COMPUTER_USE||!1!==e.computerUse)}let w={at:0,value:null};async function y(e){if("screenshot"===e)return null;return!0===(await async function(){if(Date.now()-w.at<3e3&&w.value)return w.value;const e=await n();return w={at:Date.now(),value:e},e}()).locked?"当前屏幕已锁定,操作系统会阻断一切界面自动化(点击/键入都打不进去)。请改为:①先做不需要界面的部分(run_bash / run_python / 文件 / 联网);②必须用界面时先 wait_for_unlock 等用户解锁;③或用 send_notification 提醒用户回来。":null}export function makeComputerTools({config:e={},injectVisual:t=null,omit:r=[],enabled:o}={}){if(!1===o)return[];if(!0!==o&&!computerUseEnabled(e))return[];const i=new Set(r instanceof Set?[...r]:r||[]),a=[],s=(e,t,r,o=!1)=>{i.has(e)||a.push({spec:t,run:r,readOnly:o})},c=async(e,t)=>{const r=tierForApp(t);return tierAllows(r,e)?y(e):`「${t||"该应用"}」被限制为「${r}」层级,不允许 ${e}:浏览器仅可看(避免替用户点掉危险确认)、终端/IDE 仅可点(避免绕过命令审批)。如果目标是操作浏览器网页,请改用浏览器工具或 fetch_url;如果是跑命令,直接用 run_bash。`};return s("computer_screenshot",m("computer_screenshot","对屏幕截图并「看」到画面,用于操作没有命令行/接口的桌面软件。可选 app:先把该应用切到前台再截。每做一步操作后重新截图确认结果,不要连点一串没验证过的坐标。",{app:f("(可选)要观察的应用名,会先切到前台"),region:{type:"object",description:"(可选)只截某个矩形区域 {x,y,width,height}",properties:{x:d("左上角 X"),y:d("左上角 Y"),width:d("宽"),height:d("高")}}}),async e=>{const r=await c("screenshot",e?.app);if(r)return r;const o=await screenshot({app:e?.app||"",region:e?.region||null});if(o.error)return o.error;const n=await screenSize(),i=n?`屏幕尺寸 ${n.width}×${n.height}。`:"";return t?(await t(`【屏幕截图${o.label?" · "+o.label:""}】${i}请据此判断下一步操作的坐标。`,o.dataUri),`已截屏(${o.label})。${i}图片会随下一条消息注入,请基于画面内容决定后续点击/键入。`):`已截屏并保存到:${o.file}\n${i}当前宿主无法把图片注入对话——可用 read_document 或 run_python(PIL/OCR)分析该图片,或改用 run_native_script 直接指挥应用(不依赖看图,更可靠)。`}),s("computer_click",m("computer_click","在屏幕坐标处点击(先 computer_screenshot 看清位置再定坐标)。支持左/右/中键与双击。",{x:d("屏幕 X 坐标"),y:d("屏幕 Y 坐标"),button:f("(可选)left(默认)| right | middle"),count:d("(可选)点击次数,2=双击"),app:f("(可选)目标应用名,用于安全分级判定")},["x","y"]),async e=>{const t=await c("click",e?.app);if(t)return t;const r=await click({x:e?.x,y:e?.y,button:e?.button,count:e?.count});return r.error||r.ok}),s("computer_move",m("computer_move","把鼠标移到指定坐标(不点击)——用于触发悬停菜单/工具提示。",{x:d("屏幕 X 坐标"),y:d("屏幕 Y 坐标"),app:f("(可选)目标应用名")},["x","y"]),async e=>{const t=await c("move",e?.app);if(t)return t;const r=await moveMouse({x:e?.x,y:e?.y});return r.error||r.ok}),s("computer_drag",m("computer_drag","从一点拖拽到另一点(拖动文件、调整滑块、框选区域)。",{from_x:d("起点 X"),from_y:d("起点 Y"),to_x:d("终点 X"),to_y:d("终点 Y"),app:f("(可选)目标应用名")},["from_x","from_y","to_x","to_y"]),async e=>{const t=await c("drag",e?.app);if(t)return t;const r=await drag({fromX:e?.from_x,fromY:e?.from_y,toX:e?.to_x,toY:e?.to_y});return r.error||r.ok}),s("computer_scroll",m("computer_scroll","滚动当前窗口内容(找不到目标时先滚动再截图)。",{direction:f("up | down(默认)| left | right"),amount:d("滚动格数,默认 3"),x:d("(可选)先把鼠标移到这个 X 再滚"),y:d("(可选)先把鼠标移到这个 Y 再滚"),app:f("(可选)目标应用名")}),async e=>{const t=await c("scroll",e?.app);if(t)return t;const r=await scroll({direction:e?.direction,amount:e?.amount,x:e?.x,y:e?.y});return r.error||r.ok}),s("computer_type",m("computer_type","把文本键入当前聚焦的输入框(先点中输入框再键入)。大段内容请写文件后让应用打开,不要硬打。",{text:f("要键入的文本(支持多行,行间自动回车)"),app:f("(可选)目标应用名")},["text"]),async e=>{const t=await c("type",e?.app);if(t)return t;const r=await typeText({text:e?.text});return r.error||r.ok}),s("computer_key",m("computer_key","按下一个键或组合键:保存(command/control + s)、确认(return)、取消(escape)、切换(tab)、打开命令面板(control+shift+p)等。比点击菜单更稳。",{key:f("键名:单个字符,或 return/tab/escape/space/delete/up/down/left/right/home/end/pageup/pagedown/f1…f12"),modifiers:{type:"array",items:{type:"string"},description:"(可选)修饰键:command|control|option|shift(macOS 的 command 在 Windows/Linux 自动映射)"},app:f("(可选)目标应用名")},["key"]),async e=>{const t=await c("key",e?.app);if(t)return t;const r=await pressKey({key:e?.key,modifiers:e?.modifiers||[]});return r.error||r.ok}),s("computer_apps",m("computer_apps","查看电脑上当前开着哪些应用与窗口,以及屏幕尺寸。开始界面自动化前先看一眼,别对着不存在的窗口操作。"),async()=>{const[e,t,r,o]=await Promise.all([listWindows(),screenSize(),capabilities(),n()]);return`屏幕:${t?`${t.width}×${t.height}`:"未知"}|锁屏:${!0===o.locked?"是":!1===o.locked?"否":"未知"}\n本机能力:截图 ${r.screenshot?"✓":"✗"}、指针 ${r.pointer?"✓":"✗"}、键盘 ${r.keyboard?"✓":"✗"}、原生脚本 ${r.nativeScript?"✓":"✗"}`+(r.hint?`\n提示:${r.hint}`:"")+`\n\n当前窗口/应用:\n${e||"(未获取到)"}`},!0),s("computer_focus_app",m("computer_focus_app","把某个应用切到前台(后续键入/点击都作用在它上面)。",{name:f('应用名(macOS 如 "Numbers"、Windows 如窗口标题片段、Linux 如窗口名)')},["name"]),async e=>{const t=await c("click",e?.name);if(t)return t;const r=await activateApp(e?.name);return r.error||r.ok}),s("run_native_script",m("run_native_script",'【操作桌面软件的首选】用系统原生自动化脚本直接指挥应用:macOS 用 AppleScript(或 JXA),Windows 用 PowerShell/COM。这类脚本是语义级的——"让 Excel 打开这个文件并把 A1 设为 X"、"让邮件客户端新建一封发给某人的邮件"、"让 Finder 把这批文件按类型分好"——比对着截图点坐标准确得多、可复现、也不怕窗口位置变化。只有当目标应用完全没有脚本接口时,才退回用 computer_click/computer_type 之类的屏幕操作。\n示例(macOS):tell application "Numbers" to open POSIX file "/tmp/a.numbers"\n示例(Windows):$x = New-Object -ComObject Excel.Application; $x.Visible = $true; $x.Workbooks.Open("C:\\a.xlsx")',{script:f("脚本源码(macOS=AppleScript 或 JXA;Windows=PowerShell)"),language:f("(可选,仅 macOS)applescript(默认)| jxa"),app:f("(可选)目标应用名,用于安全分级判定"),timeout:d("(可选)超时毫秒,默认 120000,上限 600000")},["script"]),async e=>{const t=await c("script",e?.app);if(t)return t;const r=await runNativeScript({script:e?.script,language:e?.language,timeout:e?.timeout});return r.error||r.ok}),a}export const COMPUTER_PROMPT_HINT="\n\n# 操作本机软件(计算机使用)\n当任务需要操作某个桌面软件时,按这个优先级选路径:\n1) 该软件有命令行/接口 → 直接 run_bash / run_python(最可靠);\n2) 有原生脚本接口(macOS AppleScript、Windows COM/PowerShell)→ run_native_script 下语义级指令;\n3) 都没有 → computer_apps 看清现状 → computer_screenshot 看屏幕 → computer_click/type/key/scroll 操作,**每步操作后重新截图确认**,不要连点一串没验证过的坐标。\n开始前先 machine_presence 确认没锁屏(锁屏时界面操作必然失败,应先做不需要界面的部分或 wait_for_unlock);预计跑很久就先 set_wake_lock(on=true) 别让电脑睡着,跑完 send_notification 叫用户回来。\n安全分级不可放大:浏览器仅可看、终端/IDE 仅可点。";
|
package/src/config.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import e from"node:fs";import
|
|
1
|
+
import e from"node:fs";import o from"node:path";import r from"node:os";export const CONFIG_DIR=o.join(r.homedir(),".huicai");export const CONFIG_PATH=o.join(CONFIG_DIR,"config.json");export const DEFAULT_BASE_URL="https://huicai-ai.sjht.com";export const DEFAULT_MODEL="deepseek-v4-flash";function n(){try{return JSON.parse(e.readFileSync(CONFIG_PATH,"utf8"))}catch{return{}}}export function loadConfig(){const e=n(),o={baseUrl:(process.env.HUICAI_BASE_URL||e.baseUrl||DEFAULT_BASE_URL).trim(),apiKey:(process.env.HUICAI_API_KEY||e.apiKey||"").trim(),model:(process.env.HUICAI_MODEL||e.model||DEFAULT_MODEL).trim(),maxTurns:Number(process.env.HUICAI_MAX_TURNS||e.maxTurns)||0,maxTokens:Number(process.env.HUICAI_MAX_TOKENS||e.maxTokens)||0,autoCompactTokens:Number(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(),subagentModel:(process.env.HUICAI_SUBAGENT_MODEL||e.subagentModel||"").trim(),subagentMaxIters:Number(process.env.HUICAI_SUBAGENT_MAXITERS||e.subagentMaxIters)||0,budgetTokens:Number(e.budgetTokens)||0,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,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()};return o.baseUrl=o.baseUrl.replace(/\/+$/,""),o}export function saveConfig(o){const r={...n(),...o};e.mkdirSync(CONFIG_DIR,{recursive:!0}),e.writeFileSync(CONFIG_PATH,JSON.stringify(r,null,2)+"\n",{mode:384});try{e.chmodSync(CONFIG_PATH,384)}catch{}return r}
|
package/src/context-report.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{buildModelMessages as t}from"./compaction.js";import{c as
|
|
1
|
+
import{buildModelMessages as t,estimateTokens as o,DEFAULT_AUTOCOMPACT_TOKENS as e}from"./compaction.js";import{c as r}from"./colors.js";export{o as estimateTokens};function s(t){if("string"==typeof t.content)return o(t.content);if(Array.isArray(t.content)){let e=0;for(const r of t.content)"text"===r?.type&&r.text?e+=o(r.text):"image_url"===r?.type&&(e+=300);return e}return 0}function a(t){return(t=Math.round(Number(t)||0))>=1e6?(t/1e6).toFixed(t>=1e7?0:1)+"M":t>=1e3?(t/1e3).toFixed(1)+"k":String(t)}function n(t,o=14){const e=Math.max(0,Math.min(o,Math.round(t*o)));return"█".repeat(e)+"░".repeat(o-e)}export function contextReport({messages:l,compaction:u,specs:c,lastPromptTokens:p,autoCompactTokens:i,platform:m}){const y=m?l:t(l,u||{}),k=!!(u&&u.summary&&u.upTo>1),f={system:{label:"系统提示词",tok:0},tools:{label:"工具定义",tok:0},summary:{label:"压缩摘要",tok:0},user:{label:"用户消息",tok:0},assistant:{label:"助手回复",tok:0},toolcall:{label:"工具调用",tok:0},toolresult:{label:"工具结果",tok:0}};y.forEach((t,e)=>{0!==e||"system"!==t.role?1===e&&k&&"user"===t.role?f.summary.tok+=s(t):"user"===t.role?f.user.tok+=s(t):"assistant"===t.role?(f.assistant.tok+=s(t),t.tool_calls&&(f.toolcall.tok+=o(JSON.stringify(t.tool_calls)))):"tool"===t.role&&(f.toolresult.tok+=s(t)):f.system.tok+=s(t)}),c&&c.length&&(f.tools.tok=o(JSON.stringify(c)));const h=["system","tools","summary","user","assistant","toolcall","toolresult"];let g=h.reduce((t,o)=>t+f[o].tok,0),d=1;p>0&&g>0&&(d=p/g);const b=t=>f[t].tok*d,x=p>0?p:g,M=[];M.push(r.bold("上下文占用")+r.gray("(当前发给模型的视图 · 估算)"));for(const t of h){const o=b(t);if(o<1&&0===f[t].tok)continue;const e=x>0?o/x:0;M.push(" "+f[t].label.padEnd(6," ")+" "+("~"+a(o)).padStart(7)+" "+r.cyan(n(e))+" "+String(Math.round(100*e)).padStart(3)+"%")}M.push(r.gray(" "+"─".repeat(34)));const S=Number(i)||e,N=Math.min(100,Math.round(x/S*100));return M.push(" 合计 "+("~"+a(x)).padStart(7)+r.gray(` 自动压缩阈值 ${a(S)}(${N}%)`)),p>0?M.push(r.gray(` 实测上一轮 prompt_tokens:${a(p)}(分类按此校准)`)):M.push(r.gray(" (尚无实测值,纯估算;发生一轮对话后更准)")),k&&M.push(r.gray(" 已启用上下文压缩:更早的历史以摘要形式计入,原文完整保留。")),M.push(r.gray(" /compact 立即压缩 · /context 再次查看")),M.join("\n")}
|
package/src/context.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import n from"node:fs";import e from"node:path";import t from"node:os";import{execSync as r}from"node:child_process";import{loadMemory as i}from"./memory.js";import{skillCatalog as o}from"./skills.js";import{buildRepoMap as s}from"./codeindex.js";const
|
|
1
|
+
import n from"node:fs";import e from"node:path";import t from"node:os";import{execSync as r}from"node:child_process";import{loadMemory as i}from"./memory.js";import{skillCatalog as o}from"./skills.js";import{buildRepoMap as s}from"./codeindex.js";import{loadSquads as a}from"./squad.js";import{loadAgents as c}from"./subagent.js";import{COMPUTER_PROMPT_HINT as l,computerUseEnabled as p}from"./computer.js";const m=["- 先了解现状再动手:并行使用 read_file/glob/grep/list_files 收集信息(一轮可同时调多个只读工具),不要臆测文件内容。","- read_file 输出的「行号→」前缀仅供定位,不是文件内容;写 edit_file 的 old_string/new_string 时绝不能包含行号前缀。","- 多步骤任务先用 todo_write 列出计划,并随进度更新状态(同一时刻至多一项 in_progress);单步小事不必用。","- 大而可拆的任务优先并行铺开,别一个人串行啃:互相独立的调研/检索/分析子任务用 dispatch_agents 一次派出多路(每个子任务须自足,多路共用的背景放 shared_context,不要逐条重复抄);单个独立子任务用 dispatch_agent;跨角色的复杂任务用 dispatch_member 按专家团分工派发,独立任务同一轮多次调用即并行。子智能体只读、在隔离上下文中执行、只回传结论,因此主线上下文保持精简、总耗时按最慢的一路算。(这些工具未挂载时就自己做;简单任务直接做,不要为拆而拆——派发本身也有开销。)","- 收到多路子任务结论后,先交叉核对再采信:结论之间矛盾、或与你已知事实冲突时,用本机工具亲自复核关键点,不要把子智能体的说法当既成事实直接写进产物。","- 改代码优先用 edit_file 做最小精确改动;新建文件才用 write_file。遵循项目既有风格与命名。","- 用 run_bash 执行构建/测试/git 等;改动后尽量运行测试或构建自检,失败要定位修复而不是忽略。","- 处理数据/表格/文档/各类文件时,用 run_python(内置 pandas/openpyxl/xlrd/pypdf/python-docx/python-pptx/matplotlib 等)在本机精确处理,绝不要只凭肉眼或纯文本摘要臆断数字;先摸清结构(表头在第几行/多 Sheet/合并单元格/金额是否文本/日期格式/编码)再算,关键中间量 print 出来核对。","- 复杂数据任务优先载入并复用现成技能脚本,别从零手搓易错的解析:读任意文件(Excel/PDF/Word/PPT/OFD/RTF/图片…)用 file-ingest 的 read_any;Excel 分析/透视/对账用 xlsx-analysis(smart_read/sheet_map/reconcile);多表关联匹配用 data-wrangling(safe_join)、维度透视交叉用 pivot-crosstab、统计检验用 statistical-testing、回归预测用 regression-forecast。跨文档/跨表交叉分析务必口径一致、逐步可复核。","- 需要在线资料(文档/API 说明)可用 fetch_url;带明确目的使用,不要漫游。","- 环境与应用自动化:install_packages 装 Node.js 包、setup_environment 一键配置运行环境、install_plugin 装 VS Code/JetBrains 插件、open_browser/open_path/open_app 打开浏览器/文件管理器/任意应用。这些操作会先征询用户「是否继续」;被拒绝时不要重试,改为说明手动做法。","- 破坏性或不可逆操作(删除、强推、重置等)前先说明意图;写/执行类工具会请用户授权,被拒绝时据反馈调整方案。","- 不要把密钥、令牌等敏感值回显到输出或写进代码。","- 简洁直接,用中文回答;完成后用一两句话总结改了什么、如何验证的。","- 深度思考/推理过程(reasoning、思维链)默认也用简体中文书写,除非用户明确要求其他语言。"];export function buildSystemPrompt(a,c={}){const l=function(n){const e=e=>r(e,{cwd:n,stdio:["ignore","pipe","ignore"]}).toString().trim();try{const n=e("git rev-parse --abbrev-ref HEAD");let t="";try{t=e("git status --short")}catch{}return{branch:n,status:t}}catch{return null}}(a),p=function(t){for(const r of["HUICAI.md","AGENTS.md","CLAUDE.md"])try{const i=n.readFileSync(e.join(t,r),"utf8");if(i.trim())return{name:r,content:i.slice(0,8e3)}}catch{}return null}(a),d=function(){try{const r=n.readFileSync(e.join(t.homedir(),".huicai","HUICAI.md"),"utf8");return r.trim()?r.slice(0,4e3):null}catch{return null}}();let u="";if(c.persona&&c.persona.trim()?(u+="# 你的角色设定\n"+c.persona.trim()+"\n\n",u+="# 运行方式\n",u+="你现在运行在用户终端里,可通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令来真正完成任务。请在保持上述角色设定的前提下遵循以下准则:\n"):(u+="你是 huicai,一个运行在用户终端里的专业自治编码与自动化智能体。\n",u+="你能通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令,从而真正完成任务,而不仅是给建议。\n\n",u+="# 工作准则\n"),u+=m.join("\n")+"\n\n",u+="# 运行环境\n",u+=`- 工作目录: ${a}\n`,u+=`- 操作系统: ${t.platform()} ${t.release()} (${t.arch()})\n`,u+=`- 日期: ${(new Date).toISOString().slice(0,10)}\n`,l&&(u+=`- Git 分支: ${l.branch}\n`,l.status)){u+=`- 未提交改动:\n${l.status.split("\n").slice(0,30).map(n=>" "+n).join("\n")}\n`}const _=function(e){try{return n.readdirSync(e,{withFileTypes:!0}).filter(n=>".git"!==n.name&&"node_modules"!==n.name).slice(0,60).map(n=>n.isDirectory()?n.name+"/":n.name).join(" ")}catch{return""}}(a);_&&(u+=`- 顶层条目: ${_}\n`);const f=function(){const n=process.env.HUICAI_IDE_FILE,e=process.env.HUICAI_IDE_SELECTION,t=process.env.HUICAI_IDE_DIAGNOSTICS;if(!n&&!e&&!t)return"";let r="";n&&(r+=`- 用户当前正在编辑的文件: ${n}\n`);if(e){r+="- 用户当前选中的代码片段:\n```\n"+(e.length>4e3?e.slice(0,4e3)+"\n…(选区已截断)":e)+"\n```\n"}if(t){r+="- 编辑器诊断(错误/警告,来自 Problems 面板):\n```\n"+(t.length>4e3?t.slice(0,4e3)+"\n…(诊断已截断)":t)+"\n```\n"}return r}();if(f&&(u+="\n# 编辑器上下文(来自 IDE 集成)\n"+f),d&&(u+=`\n# 全局用户说明(~/.huicai/HUICAI.md)\n${d}\n`),p&&(u+=`\n# 项目说明(${p.name})\n${p.content}\n`),!1!==c.repoMap)try{const n=s(a);n&&(u+=`\n# 项目地图(关键文件与主要符号;用 search_code 按概念检索相关实现)\n${n}\n`)}catch{}const g=o(a);if(g&&(u+=`\n# 可用技能(本地 Agent Skills)\n${g}\n`),u+=capabilityCatalog(a,c),!1!==c.memory){const n=i(a);n&&(u+=`\n# 长期记忆(此前会话经 memory_write 记录,跨会话生效)\n${n}\n`,u+="(发现新的、值得跨会话记住的稳定事实/偏好/结论时,用 memory_write 记录。)\n")}return c.append&&String(c.append).trim()&&(u+="\n# 附加指示\n"+String(c.append).trim()+"\n"),u}export function capabilityCatalog(n,e={}){let t="",r=[];try{r=a(n)}catch{}r.length&&(t+="\n# 可召唤的专家团(本地,dispatch_member 派发成员)\n",t+=r.slice(0,20).map(n=>{const e=String(n.when||n.description||"").replace(/\s+/g," ").slice(0,100),t=(n.members||[]).map(n=>n.callName||n.id).filter(Boolean).slice(0,6).join("、");return`- ${n.name}${e?":"+e:""}${t?`(成员:${t})`:""}`}).join("\n")+"\n",t+="跨角色、要分工协作的复杂任务,直接用 dispatch_member 把子任务派给对应成员,不必等用户点名。\n");let i=[];try{i=c(n)}catch{}return i.length&&(t+="\n# 可调用的子智能体人格(本地,dispatch_agent 指定 agent 参数)\n",t+=i.slice(0,20).map(n=>`- ${n.name}${n.description?":"+String(n.description).replace(/\s+/g," ").slice(0,100):""}`).join("\n")+"\n"),Array.isArray(e.experts)&&e.experts.length&&(t+="\n# 可咨询的平台专家(consult_expert / consult_experts)\n",t+=e.experts.slice(0,20).map(n=>`- [${n.type||""}]${n.name}${n.description?":"+String(n.description).slice(0,80):""}`).join("\n")+"\n"),e.mcpCatalog&&String(e.mcpCatalog).trim()&&(t+="\n# 外部能力连接器(MCP)\n"+String(e.mcpCatalog).trim()+"\n需要当前工具覆盖不到的能力时,先 list_mcp_servers 看有什么,再 connect_mcp 连上——连接后新工具下一轮即可调用,无需重开会话。\n"),!1!==e.computerUse&&p(e.config||{})&&(t+=l+"\n"),!1!==e.automation&&(t+="\n# 自动化与提醒(主动使用,不必等用户明说)\n- 用户表达出「以后每天/每周/每隔多久…」这类周期性意图时,直接用 create_automation 把它落成定时任务,再向用户复述频率与内容确认;要改/停/删已有任务先 list_automations 定位。\n- 当前任务需要「等一会儿再做」(等构建完成、等对方回复、明早再跑)时,用 schedule_followup 给自己排后续,不要让用户干等,也不要假装已经完成。\n- 长任务跑完、或遇到必须由人拍板的岔路时,用 send_notification 把用户叫回来(用户可能已经离开电脑)。\n- 预计要跑很久的任务,开始前 set_wake_lock(on=true) 防止电脑休眠导致半途中断,结束后释放。\n"),t}
|
package/src/daemon.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as o}from"node:child_process";import e from"node:fs";import r from"node:path";import n from"node:os";import{listTasks as t}from"./schedule.js";import{cronMatches as i}from"./cron.js";import{huicaiBin as c}from"./forks.js";export const DAEMON_LOG_DIR=r.join(n.homedir(),".huicai","daemon");export async function runDaemon({intervalMs:n=6e4,log:
|
|
1
|
+
import{spawn as o}from"node:child_process";import e from"node:fs";import r from"node:path";import n from"node:os";import{listTasks as t}from"./schedule.js";import{cronMatches as i}from"./cron.js";import{huicaiBin as c}from"./forks.js";import{acquireWakeLock as s,releaseWakeLock as p,presence as a,describePresence as d}from"./presence.js";export const DAEMON_LOG_DIR=r.join(n.homedir(),".huicai","daemon");export async function runDaemon({intervalMs:n=6e4,log:m=o=>process.stdout.write(o+"\n"),once:f=!1,spawnImpl:u}={}){const l=c();if(!l&&!u)throw new Error("无法定位 huicai 可执行文件(daemon 需在 CLI 环境运行)");e.mkdirSync(DAEMON_LOG_DIR,{recursive:!0});const h=u||(n=>{const t=e.openSync(r.join(DAEMON_LOG_DIR,n.id+".log"),"a"),i=o(process.execPath,[l,"-p",n.prompt,"--yolo","--plain"],{cwd:n.cwd,detached:!0,stdio:["ignore",t,t],env:process.env});return i.unref(),i.pid}),w=new Map,I=()=>{const o=new Date,e=o.toISOString().slice(0,16);let r=0;for(const n of t()){if(!n.cron||!n.prompt)continue;if(w.get(n.id)===e)continue;let t=!1;try{t=i(n.cron,o)}catch{t=!1}if(!t)continue;w.set(n.id,e);const c=h(n);r++,m(`[${e}] 触发 #${n.id}(pid ${c}):${String(n.prompt).replace(/\s+/g," ").slice(0,60)}`)}return r},g=I();if(f)return{fired:g};const D=s({reason:"huicai 调度守护运行中"});m(`huicai 调度守护已启动:每 ${Math.round(n/1e3)}s 检查 ~/.huicai/schedule.json(${t().length} 个任务)· Ctrl-C 退出`),m(D.ok?" · 已持防休眠锁:合盖/离开时系统不会睡眠,锁屏期间任务照常触发(屏幕点击类操作除外)。":` · 未能持防休眠锁(${D.reason}):系统若进入休眠,期间的触发点会被跳过。`);try{const o=await a();m(" · 当前在场状态:"+d(o))}catch{}const j=setInterval(I,n);return await new Promise(o=>{process.on("SIGINT",()=>{clearInterval(j),D.ok&&p({force:!0}),m("\n调度守护已停止。"),o()})}),{fired:g}}
|
package/src/decisions.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import e from"node:fs";import t from"node:path";import r from"node:crypto";import{execFileSync as n}from"node:child_process";import{CONFIG_DIR as s}from"./config.js";export const DECISIONS_DIR=t.join(s,"decisions");const o=2e3;function i(e,t){return n("git",t,{cwd:e,encoding:"utf8",stdio:["ignore","pipe","ignore"]}).trim()}export function projectKey(e){let n="";try{n=i(e,["config","--get","remote.origin.url"])}catch{}if(!n)try{n=i(e,["rev-parse","--show-toplevel"])}catch{}return n||(n=t.resolve(e||".")),r.createHash("sha1").update(n).digest("hex").slice(0,16)}export function resolveAnchor(e){try{const t=i(e,["rev-parse","HEAD"]);if(!t)return{};let r=!1;try{r=i(e,["status","--porcelain"]).length>0}catch{}return{git:t,dirty:r}}catch{return{}}}function a(e){return t.join(DECISIONS_DIR,e,"graph.json")}function c(e){return{version:1,projectKey:e,nodes:[]}}export function loadGraph(t){try{const r=JSON.parse(e.readFileSync(a(t),"utf8"));return r&&Array.isArray(r.nodes)?(r.version=r.version||1,r.projectKey=t,r):c(t)}catch{return c(t)}}export function saveGraph(r){if(r&&r.projectKey&&Array.isArray(r.nodes))try{e.mkdirSync(t.join(DECISIONS_DIR,r.projectKey),{recursive:!0}),e.writeFileSync(a(r.projectKey),JSON.stringify(r),{mode:384})}catch(e){process.stderr.write(`(决策图保存失败,不影响主流程:${e.message})\n`)}}function d(e,t){return String(null==e?"":e).slice(0,t)}function l(e){return Array.isArray(e)?e.filter(e=>e&&e.option).slice(0,20).map(e=>({option:d(e.option,500),rejectedBecause:d(e.rejectedBecause,1e3)})):[]}export function recordDecision(e,t={},n={}){const s=projectKey(e),i=loadGraph(s),a=l(t.alternatives),c=a.length>0,p={id:"dec_"+Date.now().toString(36)+r.randomBytes(4).toString("hex"),projectKey:s,sessionId:d(t.sessionId,64),turn:Number.isFinite(t.turn)?t.turn:null,title:d(t.title||t.what,200),what:d(t.what,2e3),why:d(t.why,4e3),alternatives:a,affects:Array.isArray(t.affects)?t.affects.slice(0,50).map(e=>d(e,400)):[],anchor:t.anchor&&"object"==typeof t.anchor?t.anchor:resolveAnchor(e),edges:{causedBy:Array.isArray(t.causedBy)?t.causedBy.map(e=>d(e,64)):[],supersedes:t.supersedes?d(t.supersedes,64):null,branchOf:t.branchOf?d(t.branchOf,64):null},status:t.status||(c?"active":"draft"),origin:n.origin||t.origin||"user",confidence:"number"==typeof t.confidence?t.confidence:c?1:.4,tags:Array.isArray(t.tags)?t.tags.slice(0,12).map(e=>d(e,40)):[],memoryRefs:Array.isArray(t.memoryRefs)?t.memoryRefs.map(e=>d(e,120)):[],createdAt:Date.now(),author:d(t.author,200),synced:!1};return i.nodes.push(p),i.nodes.length>o&&i.nodes.splice(0,i.nodes.length-o),saveGraph(i),p}export function listDecisions(e){return loadGraph(projectKey(e)).nodes.slice().sort((e,t)=>(t.createdAt||0)-(e.createdAt||0))}export function getDecision(e,t){return loadGraph(projectKey(e)).nodes.find(e=>e.id===t)||null}export function unsyncedDecisions(e){return loadGraph(projectKey(e)).nodes.filter(e=>!e.synced)}export function markSynced(e,t){const r=new Set(t||[]);if(!r.size)return;const n=loadGraph(projectKey(e));let s=!1;for(const e of n.nodes)r.has(e.id)&&!e.synced&&(e.synced=!0,s=!0);s&&saveGraph(n)}export function getLastSync(e){return loadGraph(projectKey(e)).lastSyncAt||0}export function setLastSync(e,t){const r=loadGraph(projectKey(e));r.lastSyncAt=t,saveGraph(r)}export function mergeRemote(e,t){if(!Array.isArray(t)||!t.length)return 0;const r=projectKey(e),n=loadGraph(r),s=new Set(n.nodes.map(e=>e.id));let i=0;for(const e of t){if(!e||!e.decisionId||s.has(e.decisionId))continue;let t=null;try{t=e.payload?JSON.parse(e.payload):null}catch{t=null}t&&t.id===e.decisionId||(t={id:e.decisionId,projectKey:r,sessionId:"",turn:null,title:d(e.title,200),what:d(e.title,200),why:"",alternatives:[],affects:[],anchor:{},edges:{causedBy:[],supersedes:null,branchOf:null},status:e.status||"active",origin:"agent",confidence:1,tags:[],memoryRefs:[],createdAt:e.clientCreatedAt||Date.now(),author:""}),t.projectKey=r,t.synced=!0,n.nodes.push(t),s.add(e.decisionId),i++}return i&&(n.nodes.sort((e,t)=>(e.createdAt||0)-(t.createdAt||0)),n.nodes.length>o&&n.nodes.splice(0,n.nodes.length-o),saveGraph(n)),i}export const DECISION_TOOL_NAME="record_decision";export function makeDecisionTool({cwd:e,sessionId:t,turn:r,emit:n}){return{readOnly:!0,spec:{type:"function",function:{name:"record_decision",description:"当你在多个可行方案之间做出取舍(技术选型、架构/接口方案、安全策略等)时,记录这次「决策」以便日后追溯「为什么这样做」。仅在存在真实岔路(考虑过其它方案)时调用;读文件、修小问题、单纯信息汇总不要记。务必给出至少一个 alternatives(考虑过但未选的方案及否决理由)——这是决策区别于普通动作的关键;缺少 alternatives 会被存为「待确认草稿」而非正式决策。",parameters:{type:"object",properties:{what:{type:"string",description:"选择了什么(结论,一句话可复述)"},why:{type:"string",description:"为什么这样选(前提 / 约束 / 权衡)"},alternatives:{type:"array",description:"考虑过但未选的方案及否决理由",items:{type:"object",properties:{option:{type:"string",description:"被否决的方案"},rejectedBecause:{type:"string",description:"为什么没选它"}},required:["option"]}},affects:{type:"array",items:{type:"string"},description:"受影响的文件 / 模块(相对路径)"},tags:{type:"array",items:{type:"string"},description:"标签,如 架构 / 性能 / 安全"}},required:["what","why"]}}},run:async s=>{const o=String(s?.what||"").trim();if(!o)return"record_decision 需要非空 what";const i=recordDecision(e,{what:o,title:o,why:s?.why,alternatives:s?.alternatives,affects:s?.affects,tags:s?.tags,sessionId:t,turn:r},{origin:"agent"});try{n?.(i)}catch{}const a="draft"===i.status?"(未给 alternatives,已存为待确认草稿)":"";return`已记录决策:${i.title}${a}`}}}export function parseExtraction(e){if(!e)return null;let t=String(e).trim();const r=t.match(/```(?:json)?\s*([\s\S]*?)```/i);r&&(t=r[1].trim());const n=t.indexOf("{"),s=t.lastIndexOf("}");if(n<0||s<=n)return null;let o;try{o=JSON.parse(t.slice(n,s+1))}catch{return null}if(!o||!0!==o.isDecision)return null;const i=d(o.what,2e3).trim(),a=l(o.alternatives);if(!i||!a.length)return null;const c="number"==typeof o.confidence?Math.max(0,Math.min(1,o.confidence)):.5;return{what:i,why:d(o.why,4e3).trim(),alternatives:a,tags:Array.isArray(o.tags)?o.tags.slice(0,12).map(e=>d(e,40)):[],confidence:c}}export async function extractDecisionFromTurn(e,{userText:t,assistantText:r,toolNames:n}={}){if(!e||!e.baseUrl||!e.apiKey)return null;try{const{streamChat:s}=await import("./api.js"),o=[t?"用户指令:\n"+String(t).slice(0,2e3):"",n&&n.length?"本回合用到的工具:"+n.join(", "):"",r?"助手结论:\n"+String(r).slice(0,4e3):""].filter(Boolean).join("\n\n");if(!o)return null;const i=await s({baseUrl:e.baseUrl,apiKey:e.apiKey,model:e.decisionExtractModel||e.model,client:e.client,aux:!0,messages:[{role:"system",content:'你是「决策抽取器」。判断这一回合里是否发生了一次真实的技术「决策」——在多个可行方案之间做了取舍(存在被认真考虑却否决的其它方案)。\n\n算决策(举例):技术选型(用 A 库而非 B 库)、架构/接口方案取舍、数据/安全策略选择、算法或数据结构的权衡。\n不算决策(举例):读文件/检索、修拼写或明显 bug、跑测试/构建、单纯总结或解释、按用户明确指令直接执行且并无其它可选方案。\n\n判据(须全部满足才算):①确实在多个方案间做了选择;②能说清"为什么选它、为什么不选别的";③对后续有影响、值得日后追溯。\n\n严格只输出一个 JSON 对象,不要任何多余文字/解释/markdown 围栏:\n{"isDecision":true|false,"what":"选择了什么","why":"为什么","alternatives":[{"option":"被否决方案","rejectedBecause":"否决理由"}],"tags":["架构"],"confidence":0.0到1.0}\n不是决策时只输出 {"isDecision":false}。confidence 表示你对"这确实是一次值得记录的决策"的把握(越不确定越低)。\n\n示例(正):选用 pgvector 下推而非应用层余弦 → {"isDecision":true,"what":"向量检索用 pgvector 下推","why":"大库应用层每次全量拉取会超时,HNSW O(logN)","alternatives":[{"option":"应用层余弦","rejectedBecause":"首查装载慢、内存随库线性增长"}],"tags":["架构","性能"],"confidence":0.9}\n示例(负):只是读了几个文件并总结 → {"isDecision":false}'},{role:"user",content:o}],maxTokens:600},{});return parseExtraction(i&&i.content)}catch{return null}}
|
package/src/errkind.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const KIND_AUTH="auth";export const KIND_QUOTA="quota";export const KIND_USAGE="usage";const t=new Set(["authentication_error","invalid_api_key"]),r=/(登录已失效|令牌校验失败|缺少访问令牌|令牌(?:已)?(?:失效|无效|过期|不存在)|尚未登录|未登录|invalid[_ ]api[_ ]key|authentication_error|unauthorized|HTTP\s*401)/i,n=/(余额不足|额度不足|请充值|insufficient_quota)/i,i=/(usage_limit_exceeded|用量上限|用量已达|已达当前会员等级)/i;function e(t){if(null==t)return{status:0,type:"",text:""};if("string"==typeof t)return{status:0,type:"",text:t};const r=String(t.message||t.error||t.text||"");return{status:Number(t.status)||0,type:String(t.type||""),text:r}}export function isAuthError(n){const{status:i,type:o,text:u}=e(n);return 401===i||(!!t.has(o)||r.test(u))}export function isQuotaError(t){const{status:r,type:i,text:o}=e(t);return 402===r||("insufficient_quota"===i||n.test(o))}export function isUsageLimitError(t){const{type:r,text:n}=e(t);return"usage_limit_exceeded"===r||i.test(n)}export function classifyError(t){return isUsageLimitError(t)?"usage":isQuotaError(t)?"quota":isAuthError(t)?"auth":""}export function isBillingError(t){return isUsageLimitError(t)||isQuotaError(t)}function o(t){return String(t||"").replace(/\/+$/,"")}export function consoleUrl(t){const r=o(t);return r?`${r}/console/dashboard`:""}export function billingUrl(t){const r=o(t);return r?`${r}/console/billing`:""}export function pricingUrl(t){const r=o(t);return r?`${r}/pricing`:""}export function billingActionForKind(t,r){if("quota"!==t&&"usage"!==t)return null;return{kind:t,label:"quota"===t?"去充值":"升级会员",url:"quota"===t?billingUrl(r):pricingUrl(r),hint:"quota"===t?"账户积分已用尽:可在控制台「额度明细」充值或兑换积分。":"已达当前会员等级的 Token 用量窗口上限:可等待窗口逐步释放,或升级会员提高上限。"}}export function billingAction(t,r){return billingActionForKind(classifyError(t),r)}
|