@huicai/cli 2026.804.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 CHANGED
@@ -122,6 +122,13 @@ huicai -c -p "继续修" --yolo # 非交互续接(CI)
122
122
  | `install_plugin` | 安装 VS Code 扩展 / JetBrains 插件 / npm 全局 CLI | **是否继续?三选项确认** |
123
123
  | `open_browser` / `open_path` / `open_app` | 打开默认浏览器 / 文件管理器(访达·资源管理器)/ 启动任意本机应用 | **是否继续?三选项确认** |
124
124
  | `mcp__<服务>__<工具>` | 本地 MCP 服务提供的工具 | 按 readOnlyHint(写型需授权) |
125
+ | `list_mcp_servers` / `connect_mcp` | 查看可用的 MCP 连接器 / **会话中途**按需连上(新工具下一轮即可调用,无需重开会话) | 查看自动放行;连接**三选项确认** |
126
+ | `list_automations` / `create_automation` / `update_automation` / `pause_automation` / `resume_automation` / `delete_automation` | 对话式增删改查定时任务(「以后每天九点帮我汇总昨天的提交」直接落成自动化) | 查看自动放行;写改**三选项确认** |
127
+ | `schedule_followup` | 智能体给自己排后续任务(等构建完成后检查、明早再跑…),到点起全新无头会话续做 | **是否继续?三选项确认** |
128
+ | `send_notification` | 弹系统通知把用户叫回来(长任务完成 / 需要人拍板 / 无人值守出结果) | 自动放行(本机提示,不外发) |
129
+ | `machine_presence` / `set_wake_lock` / `wait_for_unlock` | 查锁屏与空闲状态 / 持防休眠锁让长任务跑完 / 等用户解锁再做界面操作 | 查看自动放行;持锁**三选项确认** |
130
+ | `computer_screenshot` / `computer_click` / `computer_move` / `computer_drag` / `computer_scroll` / `computer_type` / `computer_key` / `computer_apps` / `computer_focus_app` | 计算机使用:看屏幕、点鼠标、敲键盘、切换窗口——操作没有命令行的桌面软件 | **是否继续?三选项确认**(`computer_apps` 只读放行) |
131
+ | `run_native_script` | 用系统原生脚本直接指挥应用(macOS AppleScript/JXA、Windows PowerShell/COM)——**操作桌面软件的首选**,比对着截图点坐标可靠 | **是否继续?三选项确认** |
125
132
 
126
133
  **自动化操作的三选项确认**(三端一致):执行前询问「是否继续?」——`1、是`(仅此一次)·
127
134
  `2、是,并允许在当前对话中后续相同操作不再询问`(按工具名记忆,仅本会话)· `3、否`(可直接输入原因,会回灌给模型调整方案)。
@@ -130,18 +137,48 @@ huicai -c -p "继续修" --yolo # 非交互续接(CI)
130
137
  **执行策略**:同一轮里连续的只读工具调用**并行执行**(结果仍按序回灌);单条工具输出超长自动**截断保头尾**,
131
138
  防止撑爆上下文;中断(Esc/Ctrl+C)时为未执行的调用合成占位结果,保证消息协议不被破坏、会话可无缝续聊。
132
139
 
140
+ ### 自动化能力层(四端同源)
141
+
142
+ 下面这些能力在 **CLI / 桌面客户端 / VS Code 扩展 / IDEA 插件**里是同一份实现(`buildExtraTools` 是唯一收口),
143
+ 无需用户配置,对话里按需自动生效:
144
+
145
+ - **自动加载技能 / 专家团 / 专家**:本地技能(`.huicai/skills` + `~/.huicai/skills` + 随包内置)以「名称+描述」
146
+ 常驻系统提示词、正文经 `load_skill` 按需载入;本地专家团(`.huicai/squads` + 内置 11 个)与子智能体人格
147
+ 进「能力目录」,模型据此自行 `dispatch_member` 派活;平台上你有权限的专家团/工作流/智能体/数字人自动编队成
148
+ `consult_expert`。平台技能库还会在后台自动同步到本地(**本地优先、只增不删**,6 小时节流,`autoSkillSync:false` 关)。
149
+ - **会话中途接入 MCP**:发现缺能力时 `list_mcp_servers` → `connect_mcp`,新工具**下一轮就能调用**
150
+ (agent 循环每轮重算工具表),不必重开会话。
151
+ - **对话式自动化任务**:说「以后每天…」就直接落成定时任务;改/停/删也是一句话。
152
+ - **消息通知**:长任务跑完(默认 ≥60s)自动弹系统通知;模型也可主动 `send_notification` 叫人。
153
+ - **锁屏与无人值守**:Auto 模式与 `-p --yolo`(含 `huicai daemon` 派生的任务)全程持**防休眠锁**,
154
+ 合盖离开也不断。锁屏期间命令 / Python / 文件 / 联网照常跑;**只有屏幕点击键入会被操作系统挡住**——
155
+ 这是系统的安全边界,客户端绕不过去,因此这类步骤会被明确拦下并提示改走非界面路径或 `wait_for_unlock`。
156
+ - **操作任意桌面软件**:优先 `run_native_script`(AppleScript / PowerShell·COM,语义级、可复现),
157
+ 没有脚本接口时才退回截图+点击。截图会经**视觉桥接**送进对话(模型不支持看图时自动转述成文字)。
158
+
159
+ 安全边界:全部屏幕操作与原生脚本都走「是否继续?」三选项确认;应用分级不可放大——
160
+ **浏览器仅可看**(防替用户点掉危险确认)、**终端/IDE 仅可点**(防绕过命令审批);沙箱模式下整体禁用;
161
+ `computerUse:false` / `HUICAI_COMPUTER_USE=0` 可彻底关掉。macOS 首次使用需在
162
+ 「系统设置 → 隐私与安全性」授予**屏幕录制**(截图)与**辅助功能**(点击/键入)。
163
+
133
164
  ### 本地 MCP 服务
134
165
 
135
- 在 `~/.huicai/mcp.json` 配置本地 MCP 服务(通用 mcpServers 格式),启动时自动连接(stdio)并把其工具接入智能体:
166
+ 在 `~/.huicai/mcp.json`(个人全局)或 `<项目>/.huicai/mcp.json`(项目级,可提交进仓库共享)配置本地 MCP
167
+ 服务(通用 mcpServers 格式),启动时自动连接(stdio)并把其工具接入智能体:
136
168
 
137
169
  ```json
138
170
  {
139
171
  "mcpServers": {
140
- "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/你的/目录"] }
172
+ "filesystem": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "/你的/目录"] },
173
+ "冷门服务": { "command": "npx", "args": ["-y", "some-mcp-server"], "autoStart": false }
141
174
  }
142
175
  }
143
176
  ```
144
177
 
178
+ 项目级同名项覆盖全局。`"disabled": true` 登记但不用;`"autoStart": false` 登记但不自动连——
179
+ 启动只连常用的几个,冷门服务留给会话中途 `connect_mcp` 按需拉起。此外还内置了一份常用服务目录
180
+ (filesystem / fetch / git / memory / sequentialthinking / sqlite),没写进配置也能一句话装上并连接。
181
+
145
182
  工具名暴露为 `mcp__filesystem__<工具>`;只读工具(`readOnlyHint`)自动放行,写型需授权。未配置该文件时完全无副作用。
146
183
 
147
184
  ### 子智能体(dispatch_agent)
package/bin/huicai.js CHANGED
@@ -749,7 +749,7 @@ async function main() {
749
749
  if (!session) {
750
750
  const sys = flags.systemPrompt
751
751
  ? flags.systemPrompt
752
- : buildSystemPrompt(cwd, { persona, append: flags.appendSystemPrompt });
752
+ : buildSystemPrompt(cwd, { persona, append: flags.appendSystemPrompt, config });
753
753
  session = sessions.createSession({ cwd, model: config.model, messages: [{ role: 'system', content: sys }] });
754
754
  }
755
755
 
@@ -778,15 +778,35 @@ async function main() {
778
778
  const wantTui = flags.tui || config.tui === 'rich'
779
779
  || (!flags.plain && config.tui !== 'plain' && process.stdout.isTTY && process.stdin.isTTY);
780
780
  if (wantTui) {
781
+ let ran = null;
781
782
  try {
782
783
  const { runInk } = await import('../src/tui/app.js');
783
- await runInk({ config, cwd, session, yolo: flags.yolo, initialPrompt: prompt || null, version: VERSION });
784
- return;
784
+ ran = (await runInk({ config, cwd, session, yolo: flags.yolo, initialPrompt: prompt || null, version: VERSION })) || {};
785
785
  } catch (e) {
786
786
  process.stderr.write(c.gray('(未启用 TUI:' + e.message + ',回退普通模式)') + '\n');
787
787
  }
788
+ // 重新登录放在 try 外:登录流程自身失败不该被误读成「TUI 不可用」而回退去开普通 REPL
789
+ if (ran) {
790
+ await reloginIfNeeded(ran, flags);
791
+ return;
792
+ }
793
+ }
794
+ const r = await startRepl({ config, cwd, yolo: flags.yolo, initialPrompt: prompt || null, session, version: VERSION });
795
+ await reloginIfNeeded(r, flags);
796
+ }
797
+
798
+ /**
799
+ * 令牌失效退出交互后:自动回到登录(浏览器授权页),而不是把用户丢回 shell 自己想办法。
800
+ * 令牌已由 account.js 清除,这里只负责「跳转到登录页」这一步;非 TTY 环境不弹浏览器,只给命令提示。
801
+ */
802
+ async function reloginIfNeeded(result, flags = {}) {
803
+ if (!result || !result.relogin) return;
804
+ if (!process.stdin.isTTY) {
805
+ process.stderr.write(c.yellow('登录已失效,请重新运行 `huicai login` 登录。') + '\n');
806
+ process.exit(1);
788
807
  }
789
- await startRepl({ config, cwd, yolo: flags.yolo, initialPrompt: prompt || null, session, version: VERSION });
808
+ process.stdout.write(c.gray('正在重新打开登录授权…') + '\n');
809
+ await login(flags);
790
810
  }
791
811
 
792
812
  /** 交互式选择要恢复的会话(当前目录);返回选中的会话 id,或 null(无会话/取消)。 */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@huicai/cli",
3
- "version": "2026.804.0",
4
- "versionDisplay": "2026.08.04",
3
+ "version": "2026.814.0",
4
+ "versionDisplay": "2026.08.14",
5
5
  "description": "在终端运行的智能体 CLI:以 HuiCai_AI 平台的 OpenAI 兼容 /v1 为模型后端,在用户本机执行文件/命令工具。",
6
6
  "type": "module",
7
7
  "bin": {
package/src/account.js ADDED
@@ -0,0 +1 @@
1
+ import{saveConfig as r,CONFIG_PATH as o}from"./config.js";import{classifyError as n,billingActionForKind as t,KIND_AUTH as e}from"./errkind.js";import{c}from"./colors.js";export function accountErrorKind(r){return r&&"object"==typeof r&&r.errorKind?r.errorKind:r&&"object"==typeof r&&r.error?n(r.error):n(r)}export function handleAccountError(n,i={},u){const s=accountErrorKind(n);if(!s)return"";const f=u||(r=>process.stdout.write(r));if(s===e)return r({apiKey:""}),f("\n"+c.red("登录已失效")+c.gray(`:已清除本机令牌(${o}),即将回到登录。`)+"\n"),s;const a=t(s,i.baseUrl);return a&&(f("\n"+c.yellow(a.hint)+"\n"),a.url&&f(c.gray(`${a.label} → `)+c.cyan(a.url)+"\n")),s}
package/src/agent.js CHANGED
@@ -1 +1 @@
1
- import t from"node:fs";import e from"node:os";import n from"node:path";import{streamChat as o,getPlatformConfig as r}from"./api.js";import{MarkupFilter as s,stripMarkup as i}from"./markup-filter.js";import{TOOL_SPECS as a,READONLY_TOOLS as l,runTool as c}from"./tools.js";import{toolLabel as u,indent as m,renderFileDiff as f}from"./render.js";import{compactIfNeeded as g,buildModelMessages as p}from"./compaction.js";import{makeTodoTool as d,formatTodos as h,TODO_TOOL_NAME as y}from"./todo.js";import{makeDecisionTool as w,DECISION_TOOL_NAME as b,recordDecision as S,extractDecisionFromTurn as x}from"./decisions.js";import{makeMemoryTool as _}from"./memory.js";import{loadHooks as T,runHooks as k}from"./hooks.js";import*as M from"./ide.js";import{c as $}from"./colors.js";export const MAX_TOOL_RESULT_CHARS=6e4;export function toolConcurrency(t){const n=Number(t?.toolConcurrency);if(Number.isFinite(n)&&n>0)return Math.min(24,Math.floor(n));let o=4;try{o=e.cpus()?.length||4}catch{}return Math.max(8,Math.min(24,o-2))}const E=/context[_\s-]?length|maximum context|context window|too many tokens|reduce the length|prompt is too long|上下文.*(超|过长|超出|溢出)|输入.*过长|token.*(超出|超过).*(上限|限制)/i;export function isContextOverflowError(t){return E.test(String(t||""))}const P=/(现在|接下来|下面|这就|马上|我来|让我|我要|我先|我这就|咱们|让我们|稍等|待我)/,j=/(做|进行|生成|编写|写(一|个|完|出)?|制作|计算|统计|核对|汇总|整理|分析|梳理|输出|列出|准备|运行|执行|开始)/,I=/\b(let me|now let me|i.?ll|i will|let.?s|next,? i)\b[^.!?\n]*\b(write|create|generat|calculat|comput|do|produc|compil|analyz|summar|prepar|run|make|list)\b/i;function v(t){const e=String(t||"").trim().split(/\n+/).map(t=>t.trim()).filter(Boolean),n=e[e.length-1]||"";return!(!n||n.length>120)&&(!!I.test(n)||P.test(n)&&j.test(n))}function C(t){const e=String(t);return e.startsWith("工具执行失败")||e.startsWith("命令启动失败")||e.includes("不是合法 JSON")||e.includes("未找到要替换的内容")||e.includes("路径超出项目目录")||e.includes("是目录而非文件")}function A(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}const U="【用户中断】",O="\n\n# 上一轮被用户中断(重要)\n上一回合被用户主动中断:会话里标注「"+U+"」的工具调用并未真正执行、没有任何结果。请勿臆测这些未完成工具的输出,也不要假设被中断的工作已完成或推断其结论;一切以用户最新一条消息为准——如与被中断的工作无关,就按新指令重新开始,需要时先与用户确认再继续。";function R(t){if(!Array.isArray(t))return!1;let e=-1;for(let n=t.length-1;n>=0;n--)if("user"===t[n].role){e=n;break}if(e<=0)return!1;for(let n=e-1;n>=0;n--){const e=t[n];if("user"===e.role)break;if("tool"===e.role&&"string"==typeof e.content&&e.content.includes(U))return!0}return!1}function N(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function D(t,e,n){const o=new Array(t.length);let r=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;r<t.length;){const e=r++;o[e]=await n(t[e],e)}})),o}export function isPlatformModel(t){return/^(agent|workflow|datahuman|squad)-/.test(String(t||""))}function J(t,e=200){const n=String(t).split("\n")[0];return n.length>e?n.slice(0,e)+"…":n}export function capToolResult(t){const e=String(t);if(e.length<=6e4)return e;const n=e.length-48e3-9e3;return e.slice(0,48e3)+`\n…【工具输出过长,中间已省略 ${n} 字符】…\n`+e.slice(-9e3)}export async function runTurn({config:e,messages:E,permissions:P,cwd:j,ask:I,signal:q,extraTools:F=[],depth:K=0,io:L,events:W,todoState:z,compaction:B,pullQueued:H,maxIters:Q,fileJournal:X}){const G=L&&L.write||(t=>process.stdout.write(t)),V=W||null,Y=t=>{V?V.onInfo?.(t):G($.gray(" · "+t)+"\n")};let Z=0,tt=0,et=0;const nt=Number(Q||e.maxTurns)||120,ot=isPlatformModel(e.model),rt={low:24e4,medium:36e4,high:6e5}[e.reasoningEffort];let st=!1,it=String(e.fallbackModel||"").trim(),at=0,lt=e.model;if(0===K&&e.autoModelStrong&&!ot){const t=[...E].reverse().find(t=>"user"===t.role),n=N(t?.content);(e.planMode||n.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(n))&&e.autoModelStrong!==e.model&&(lt=e.autoModelStrong,e.debug&&Y(`[automodel] 复杂回合升配 → ${lt}`))}const ct=B||{},ut=[...F];if(0===K&&!ot&&!ut.some(t=>t.spec?.function?.name===y)){const t=z||{list:[]};ut.push(d({state:t,emit:t=>{V?V.onTodo?V.onTodo(t.map(t=>({...t}))):V.onInfo?.("任务清单:\n"+h(t)):G($.gray(m(h(t)," "))+"\n")}}))}if(0!==K||ot||!1===e.memoryEnabled||ut.some(t=>"memory_write"===t.spec?.function?.name)||ut.push(_({cwd:j,emit:t=>{V?V.onMemory?V.onMemory(t):V.onInfo?.("已记入长期记忆:"+J(t,80)):G($.gray(" ✎ 已记入长期记忆:"+J(t,80))+"\n")}})),0===K&&!ot&&!1!==e.decisionsEnabled&&!ut.some(t=>t.spec?.function?.name===b)){const t=E.filter(t=>"user"===t.role).length;ut.push(w({cwd:j,sessionId:e.sessionId||"",turn:t,emit:t=>{St=!0,V?V.onDecision?V.onDecision(t):V.onInfo?.("已记录决策:"+J(t.title,80)):G($.gray(" ◆ 已记录决策:"+J(t.title,80))+"\n")}}))}const mt=new Map;for(const t of ut)mt.set(t.spec.function.name,t);const ft=ot?void 0:[...a,...ut.map(t=>t.spec)];let gt=0,pt=!1,dt=0,ht=null,yt=0,wt=0,bt=!1,St=!1;const xt=new Set;let _t="";const Tt=0!==K||ot?null:T(j),kt=!(!Tt||!Tt.all.length);if(kt){const t=[];if(E.filter(t=>"user"===t.role).length<=1){const e=k("SessionStart",{cwd:j},Tt);e.injectContext&&t.push(e.injectContext);for(const n of e.outputs)t.push(n.stdout)}const e=[...E].reverse().find(t=>"user"===t.role),n=k("UserPromptSubmit",{cwd:j,prompt:N(e?.content)},Tt);if(n.block)return Y("提交被 UserPromptSubmit 钩子阻断:"+n.reason),{error:"提交被钩子阻断:"+n.reason,usage:{prompt:Z,completion:tt},lastPromptTokens:et};n.injectContext&&t.push(n.injectContext);for(const e of n.outputs)t.push(e.stdout);t.length&&(ht="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let a=0;a<nt;a++){const m=a===nt-1;!m||ot||pt||ht||(ht="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const d=await g(E,e,{state:ct,lastPromptTokens:et});d>0&&e.debug&&Y(`[debug] 已压缩上下文(${d} 条较早消息已摘要)`);let h=!1,y=!1;const w=new s,b=t=>{t&&(h||!y||V||G("\n"),h=!0,V?V.onDelta?.(t):G(t))},_=p(E,ct);let T=_;if(!ot&&_.length&&"system"===_[0]?.role){let t="";e.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),e.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),e.goal&&String(e.goal).trim()&&(t+=A(e.goal)),R(E)&&(t+=O),t&&(T=[{..._[0],content:String(_[0].content||"")+t},..._.slice(1)])}const I=[];_t?I.push({role:"assistant",content:_t},{role:"user",content:"你上一条回复因达到单轮输出长度上限而被截断,用户尚未看到剩余部分。请从截断处直接继续输出剩余内容:无缝衔接上文(若截断处在句子、代码块或表格中间,从中断字符处接着写),不要重复任何已输出的内容,不要道歉或解释,不要重新开头或总结前文。"}):ht&&I.push({role:"user",content:ht});const N=Date.now();let F,L=!1,W="";try{F=await o({baseUrl:e.baseUrl,apiKey:e.apiKey,model:lt,messages:I.length?[...T,...I]:T,tools:pt||m?void 0:ft,maxTokens:e.maxTokens||8192,reasoningEffort:e.reasoningEffort,stallTimeoutMs:rt,client:e.client,sessionId:e.sessionId,aux:e.aux||void 0,continuation:!!_t,tailCount:I.length},{onContent:t=>{L=!0,W+=t,b(w.feed(t))},onReasoning:t=>{!1!==e.showReasoning&&(_t||(V?V.onReasoning?.(t):(y||G($.gray("\n[思考] ")),G($.gray(t))),y=!0))},onRetry:(t,n,o)=>{e.debug&&Y(`[retry] 模型响应异常,${Math.round(n/1e3)}s 后自动重试(第 ${t} 次):${o}`)}},q)}catch(t){if("AbortError"===t.name)return V?V.onInfo?.("已中断"):G($.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:Z,completion:tt},lastPromptTokens:et};if(!ot&&!q?.aborted&&isContextOverflowError(t.message)&&at<3){at++;const t=await g(E,e,{state:ct,force:!0});if(t>0){L&&W&&(_t+=W),e.debug&&Y(`[overflow] 上下文超出模型窗口,已强制压缩 ${t} 条较早消息后重试`);continue}e.debug&&Y("[overflow] 上下文超限但无可压缩内容,转兜底模型")}if(!ot&&!st&&!q?.aborted){if(!it)try{const t=await r(e.baseUrl,e.apiKey);it=String(t&&t.fallback_model||"").trim()}catch{}if(it&&it!==lt){st=!0,lt=it,L&&W&&(_t+=W),e.debug&&Y(`[fallback] 主模型失败(${t.message}),静默切兜底模型 ${it}${L?"(续写)":""}`);continue}}return V?V.onInfo?.("✗ 模型调用失败:"+t.message):G("\n"+$.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,usage:{prompt:Z,completion:tt},lastPromptTokens:et}}b(w.flush()),F.usage&&(Z+=F.usage.prompt_tokens||0,tt+=F.usage.completion_tokens||0,et=F.usage.prompt_tokens||et,0===K&&V&&V.onUsage?.({lastPromptTokens:et,prompt:Z,completion:tt})),e.debug&&Y(`[debug] 轮${a+1} · ${e.model} · in ${F.usage?.prompt_tokens||0} / out ${F.usage?.completion_tokens||0} tok · ${Date.now()-N}ms · ${(F.toolCalls||[]).length} 工具调用`);const{text:z,sawMarkup:B}=i(F.content),Q=B||w.sawMarkup;ht=null;const ut=ot?[]:F.toolCalls;if(!ut.length){if(Q&&!z.trim()&&!ot){if(gt<2){gt++,2===gt&&(pt=!0),ht=1===gt?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",Y(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${gt} 次)`);continue}Y("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}if(!ot&&"length"===F.finishReason&&z&&yt<3){yt++,_t+=z,e.debug&&Y(`[debug] 输出达单轮长度上限,自动续写(第 ${yt} 段)`);continue}h&&!V&&G("\n");const t=_t?_t+z:z;_t="",t&&E.push({role:"assistant",content:t});const n=!t.trim()||v(t);if(bt&&n&&"length"!==F.finishReason&&!pt&&wt<2){wt++,e.debug&&Y(`[debug] 检测到「宣告未落地」收尾(空正文或预告式),注入续跑纠偏(第 ${wt} 次)`),E.push({role:"user",content:'你上一条只是说明"接下来要做什么",但既没有发起工具调用、也没有给出结果——用户还没拿到任何最终产物。请立即执行你刚才所说的步骤(需要就用 run_python/工具,不需要就直接算),并直接输出面向用户的完整最终结果(例如完整的按月汇总表与逐月核对表、差异结论),不要再只描述你打算做什么。'});continue}V&&V.onTurnEnd?.(t);const o=H?.();if(o){E.push({role:"user",content:o});continue}if(kt){const e=k("Stop",{cwd:j,result:t},Tt);for(const t of e.outputs)Y("钩子·Stop:"+J(t.stdout,160))}if(!ot&&e.decisionAutoExtract&&!1!==e.decisionsEnabled&&bt&&!St&&t&&t.trim())try{const n=[...E].reverse().find(t=>"user"===t.role),o=n&&"string"==typeof n.content?n.content:"",r=await x(e,{userText:o,assistantText:t,toolNames:[...xt]}),s="number"==typeof e.decisionExtractMinConfidence?e.decisionExtractMinConfidence:.55;if(r&&(r.confidence??0)>=s){const t=E.filter(t=>"user"===t.role).length,n=S(j,{...r,title:r.what,status:"draft",origin:"agent",confidence:Math.min(r.confidence??.5,.7),sessionId:e.sessionId||"",turn:t},{origin:"agent"});V?V.onDecision?V.onDecision(n):V.onInfo?.("已抽取决策草稿(待确认):"+J(n.title,80)):G($.gray(" ◇ 已抽取决策草稿(待确认):"+J(n.title,80))+"\n")}}catch{}return{usage:{prompt:Z,completion:tt},lastPromptTokens:et}}bt=!0,h&&!V&&G("\n"),V&&z&&V.onAssistantText?.(z),E.push({role:"assistant",content:_t?_t+z:z,tool_calls:ut}),_t="";const Mt=ut.map((t,e)=>{const n=t.function?.name||"";let o={},r=null;const s=t.function?.arguments||"";if(s)try{o=JSON.parse(s)}catch(t){r=`工具参数不是合法 JSON(${t.message}):${String(s).slice(0,200)}…请修正参数后重新调用。`}const i=mt.get(n),a=M.isActive()&&("write_file"===n||"edit_file"===n);return{tc:t,idx:e,name:n,args:o,argsError:r,extra:i,readOnly:i?!!i.readOnly:l.has(n),deferToIde:a}}),$t=t=>{for(let e=t;e<Mt.length;e++)E.push({role:"tool",tool_call_id:Mt[e].tc.id||`call_${e}`,content:U+" 该工具未执行,无任何结果,请勿臆测其输出。"});return V?V.onInfo?.("已中断"):G($.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:Z,completion:tt},lastPromptTokens:et}},Et=async t=>{if(t.argsError)return t.argsError;let n;try{n=t.extra?await t.extra.run(t.args):await c(t.name,t.args,j,e,q)}catch(t){n=`工具执行失败:${t.message}`}return capToolResult(String(n))},Pt=t=>{xt.add(t.name),V?V.onToolStart?.(t.name,t.args):G($.cyan(" ⏺ ")+u(t.name,t.args)+"\n")},jt=(t,e)=>{C(e)&&dt++,V?V.onToolResult?.(t.name,String(e)):G($.gray(" "+J(e))+"\n"),E.push({role:"tool",tool_call_id:t.tc.id,content:String(e)})},It=(t,e)=>{V?V.onSkip?.(t.name,t.args):G($.gray(` ⊘ ${u(t.name,t.args)} — 已跳过\n`)),E.push({role:"tool",tool_call_id:t.tc.id,content:e||"未授权,未执行。"})},vt=t=>"edit_file"===t||"write_file"===t,Ct=e=>{try{return t.readFileSync(e,"utf8")}catch{return""}},At=(t,e)=>{const o=n.resolve(j,String(t.args.path)),r=f(e,Ct(o),{path:String(t.args.path)});r&&(V?V.onFileDiff?.(r):G(r+"\n"))};let Ut=0;for(;Ut<Mt.length;){if(q?.aborted)return $t(Ut);const o=Mt[Ut];if(o.argsError){Pt(o),jt(o,o.argsError),Ut++;continue}if(o.readOnly){let t=Ut;for(;t<Mt.length&&Mt[t].readOnly&&!Mt[t].argsError;)t++;const n=Mt.slice(Ut,t),o=await D(n,toolConcurrency(e),async t=>{const e=await P.request(t.name,t.args,j,!0);return e.allow?(Pt(t),{item:t,result:await Et(t)}):{item:t,denied:e.reason||"未授权,未执行。"}});for(const t of o)null!=t.denied?It(t.item,t.denied):jt(t.item,t.result);if(Ut=t,q?.aborted)return $t(Ut);continue}const r=await P.request(o.name,o.args,j,o.readOnly||o.deferToIde);if(q?.aborted)return $t(Ut);if(!r.allow){It(o,r.reason),Ut++;continue}if(kt){const t=k("PreToolUse",{cwd:j,toolName:o.name,args:o.args},Tt);for(const e of t.outputs)Y("钩子·PreToolUse:"+J(e.stdout,120));if(t.block){It(o,"PreToolUse 钩子阻断:"+t.reason),Ut++;continue}t.modifiedArgs&&(o.args={...o.args,...t.modifiedArgs},Y("钩子·PreToolUse 已修改工具参数"))}Pt(o);const s=vt(o.name)&&o.args?.path?Ct(n.resolve(j,String(o.args.path))):null;if(X&&null!=s&&o.args?.path){const e=t.existsSync(n.resolve(j,String(o.args.path)));X.push(s.length>2097152?{path:String(o.args.path),tooLarge:!0}:{path:String(o.args.path),before:s,existed:e})}let i=await Et(o);if(kt){const t=k("PostToolUse",{cwd:j,toolName:o.name,args:o.args,result:String(i)},Tt);if(t.outputs.length){for(const e of t.outputs)Y("钩子·PostToolUse:"+J(e.stdout,120));i=String(i)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}jt(o,i),null!=s&&At(o,s),Ut++}dt>=3&&!pt&&!ht&&(ht="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",dt=0,Y("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const Ot=H?.();Ot&&E.push({role:"user",content:Ot})}return _t&&(E.push({role:"assistant",content:_t}),V&&V.onTurnEnd?.(_t)),V?V.onInfo?.("已达最大工具轮数,停止"):G($.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:Z,completion:tt},lastPromptTokens:et}}
1
+ import t from"node:fs";import e from"node:os";import n from"node:path";import{streamChat as o,getPlatformConfig as r}from"./api.js";import{classifyError as s}from"./errkind.js";import{MarkupFilter as i,stripMarkup as a}from"./markup-filter.js";import{TOOL_SPECS as l,READONLY_TOOLS as c,runTool as u}from"./tools.js";import{toolLabel as m,indent as f,renderFileDiff as g}from"./render.js";import{compactIfNeeded as p,buildModelMessages as d}from"./compaction.js";import{makeTodoTool as h,formatTodos as y,TODO_TOOL_NAME as w}from"./todo.js";import{makeDecisionTool as b,DECISION_TOOL_NAME as S,recordDecision as x,extractDecisionFromTurn as _}from"./decisions.js";import{makeMemoryTool as T}from"./memory.js";import{loadHooks as k,runHooks as M}from"./hooks.js";import*as $ from"./ide.js";import{c as E}from"./colors.js";export const MAX_TOOL_RESULT_CHARS=6e4;export function toolConcurrency(t){const n=Number(t?.toolConcurrency);if(Number.isFinite(n)&&n>0)return Math.min(24,Math.floor(n));let o=4;try{o=e.cpus()?.length||4}catch{}return Math.max(8,Math.min(24,o-2))}const P=/context[_\s-]?length|maximum context|context window|too many tokens|reduce the length|prompt is too long|上下文.*(超|过长|超出|溢出)|输入.*过长|token.*(超出|超过).*(上限|限制)/i;export function isContextOverflowError(t){return P.test(String(t||""))}const j=/(现在|接下来|下面|这就|马上|我来|让我|我要|我先|我这就|咱们|让我们|稍等|待我)/,A=/(做|进行|生成|编写|写(一|个|完|出)?|制作|计算|统计|核对|汇总|整理|分析|梳理|输出|列出|准备|运行|执行|开始)/,I=/\b(let me|now let me|i.?ll|i will|let.?s|next,? i)\b[^.!?\n]*\b(write|create|generat|calculat|comput|do|produc|compil|analyz|summar|prepar|run|make|list)\b/i;function v(t){const e=String(t||"").trim().split(/\n+/).map(t=>t.trim()).filter(Boolean),n=e[e.length-1]||"";return!(!n||n.length>120)&&(!!I.test(n)||j.test(n)&&A.test(n))}function C(t){const e=String(t);return e.startsWith("工具执行失败")||e.startsWith("命令启动失败")||e.includes("不是合法 JSON")||e.includes("未找到要替换的内容")||e.includes("路径超出项目目录")||e.includes("是目录而非文件")}function U(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}const O="【用户中断】",R="\n\n# 上一轮被用户中断(重要)\n上一回合被用户主动中断:会话里标注「"+O+"」的工具调用并未真正执行、没有任何结果。请勿臆测这些未完成工具的输出,也不要假设被中断的工作已完成或推断其结论;一切以用户最新一条消息为准——如与被中断的工作无关,就按新指令重新开始,需要时先与用户确认再继续。";function N(t){if(!Array.isArray(t))return!1;let e=-1;for(let n=t.length-1;n>=0;n--)if("user"===t[n].role){e=n;break}if(e<=0)return!1;for(let n=e-1;n>=0;n--){const e=t[n];if("user"===e.role)break;if("tool"===e.role&&"string"==typeof e.content&&e.content.includes(O))return!0}return!1}function D(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function J(t,e,n){const o=new Array(t.length);let r=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;r<t.length;){const e=r++;o[e]=await n(t[e],e)}})),o}export function isPlatformModel(t){return/^(agent|workflow|datahuman|squad)-/.test(String(t||""))}function K(t,e=200){const n=String(t).split("\n")[0];return n.length>e?n.slice(0,e)+"…":n}export function capToolResult(t){const e=String(t);if(e.length<=6e4)return e;const n=e.length-48e3-9e3;return e.slice(0,48e3)+`\n…【工具输出过长,中间已省略 ${n} 字符】…\n`+e.slice(-9e3)}export async function runTurn({config:e,messages:P,permissions:j,cwd:A,ask:I,signal:q,extraTools:F=[],depth:L=0,io:W,events:z,todoState:B,compaction:H,pullQueued:Q,maxIters:X,fileJournal:G}){const V=W&&W.write||(t=>process.stdout.write(t)),Y=z||null,Z=t=>{Y?Y.onInfo?.(t):V(E.gray(" · "+t)+"\n")};let tt=0,et=0,nt=0;const ot=Number(X||e.maxTurns)||120,rt=isPlatformModel(e.model),st={low:24e4,medium:36e4,high:6e5}[e.reasoningEffort];let it=!1,at=String(e.fallbackModel||"").trim(),lt=0,ct=e.model;if(0===L&&e.autoModelStrong&&!rt){const t=[...P].reverse().find(t=>"user"===t.role),n=D(t?.content);(e.planMode||n.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(n))&&e.autoModelStrong!==e.model&&(ct=e.autoModelStrong,e.debug&&Z(`[automodel] 复杂回合升配 → ${ct}`))}const ut=H||{},mt=[...F];if(0===L&&!rt&&!mt.some(t=>t.spec?.function?.name===w)){const t=B||{list:[]};mt.push(h({state:t,emit:t=>{Y?Y.onTodo?Y.onTodo(t.map(t=>({...t}))):Y.onInfo?.("任务清单:\n"+y(t)):V(E.gray(f(y(t)," "))+"\n")}}))}if(0!==L||rt||!1===e.memoryEnabled||mt.some(t=>"memory_write"===t.spec?.function?.name)||mt.push(T({cwd:A,emit:t=>{Y?Y.onMemory?Y.onMemory(t):Y.onInfo?.("已记入长期记忆:"+K(t,80)):V(E.gray(" ✎ 已记入长期记忆:"+K(t,80))+"\n")}})),0===L&&!rt&&!1!==e.decisionsEnabled&&!mt.some(t=>t.spec?.function?.name===S)){const t=P.filter(t=>"user"===t.role).length;mt.push(b({cwd:A,sessionId:e.sessionId||"",turn:t,emit:t=>{Tt=!0,Y?Y.onDecision?Y.onDecision(t):Y.onInfo?.("已记录决策:"+K(t.title,80)):V(E.gray(" ◆ 已记录决策:"+K(t.title,80))+"\n")}}))}const ft=new Map;let gt=null,pt=0;const dt=()=>{if(Array.isArray(F)&&F.length>pt){for(const t of F.slice(pt)){const e=t?.spec?.function?.name;e&&!ft.has(e)&&mt.push(t)}pt=F.length,gt=null}if(null===gt){ft.clear();for(const t of mt)ft.set(t.spec.function.name,t);gt=rt?void 0:[...l,...mt.map(t=>t.spec)]}};pt=Array.isArray(F)?F.length:0,dt();let ht=0,yt=!1,wt=0,bt=null,St=0,xt=0,_t=!1,Tt=!1;const kt=new Set;let Mt="";const $t=0!==L||rt?null:k(A),Et=!(!$t||!$t.all.length);if(Et){const t=[];if(P.filter(t=>"user"===t.role).length<=1){const e=M("SessionStart",{cwd:A},$t);e.injectContext&&t.push(e.injectContext);for(const n of e.outputs)t.push(n.stdout)}const e=[...P].reverse().find(t=>"user"===t.role),n=M("UserPromptSubmit",{cwd:A,prompt:D(e?.content)},$t);if(n.block)return Z("提交被 UserPromptSubmit 钩子阻断:"+n.reason),{error:"提交被钩子阻断:"+n.reason,usage:{prompt:tt,completion:et},lastPromptTokens:nt};n.injectContext&&t.push(n.injectContext);for(const e of n.outputs)t.push(e.stdout);t.length&&(bt="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let l=0;l<ot;l++){dt();const f=l===ot-1;!f||rt||yt||bt||(bt="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const h=await p(P,e,{state:ut,lastPromptTokens:nt});h>0&&e.debug&&Z(`[debug] 已压缩上下文(${h} 条较早消息已摘要)`);let y=!1,w=!1;const b=new i,S=t=>{t&&(y||!w||Y||V("\n"),y=!0,Y?Y.onDelta?.(t):V(t))},T=d(P,ut);let k=T;if(!rt&&T.length&&"system"===T[0]?.role){let t="";e.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),e.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),e.goal&&String(e.goal).trim()&&(t+=U(e.goal)),N(P)&&(t+=R),t&&(k=[{...T[0],content:String(T[0].content||"")+t},...T.slice(1)])}const I=[];Mt?I.push({role:"assistant",content:Mt},{role:"user",content:"你上一条回复因达到单轮输出长度上限而被截断,用户尚未看到剩余部分。请从截断处直接继续输出剩余内容:无缝衔接上文(若截断处在句子、代码块或表格中间,从中断字符处接着写),不要重复任何已输出的内容,不要道歉或解释,不要重新开头或总结前文。"}):bt&&I.push({role:"user",content:bt});const D=Date.now();let F,W=!1,z="";try{F=await o({baseUrl:e.baseUrl,apiKey:e.apiKey,model:ct,messages:I.length?[...k,...I]:k,tools:yt||f?void 0:gt,maxTokens:e.maxTokens||8192,reasoningEffort:e.reasoningEffort,stallTimeoutMs:st,client:e.client,sessionId:e.sessionId,aux:e.aux||void 0,continuation:!!Mt,tailCount:I.length},{onContent:t=>{W=!0,z+=t,S(b.feed(t))},onReasoning:t=>{!1!==e.showReasoning&&(Mt||(Y?Y.onReasoning?.(t):(w||V(E.gray("\n[思考] ")),V(E.gray(t))),w=!0))},onRetry:(t,n,o)=>{e.debug&&Z(`[retry] 模型响应异常,${Math.round(n/1e3)}s 后自动重试(第 ${t} 次):${o}`)}},q)}catch(t){if("AbortError"===t.name)return Y?Y.onInfo?.("已中断"):V(E.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:tt,completion:et},lastPromptTokens:nt};if(!rt&&!q?.aborted&&isContextOverflowError(t.message)&&lt<3){lt++;const t=await p(P,e,{state:ut,force:!0});if(t>0){W&&z&&(Mt+=z),e.debug&&Z(`[overflow] 上下文超出模型窗口,已强制压缩 ${t} 条较早消息后重试`);continue}e.debug&&Z("[overflow] 上下文超限但无可压缩内容,转兜底模型")}const n=s(t);if(!(n||rt||it||q?.aborted)){if(!at)try{const t=await r(e.baseUrl,e.apiKey);at=String(t&&t.fallback_model||"").trim()}catch{}if(at&&at!==ct){it=!0,ct=at,W&&z&&(Mt+=z),e.debug&&Z(`[fallback] 主模型失败(${t.message}),静默切兜底模型 ${at}${W?"(续写)":""}`);continue}}return Y?Y.onInfo?.("✗ 模型调用失败:"+t.message):V("\n"+E.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,errorKind:n,usage:{prompt:tt,completion:et},lastPromptTokens:nt}}S(b.flush()),F.usage&&(tt+=F.usage.prompt_tokens||0,et+=F.usage.completion_tokens||0,nt=F.usage.prompt_tokens||nt,0===L&&Y&&Y.onUsage?.({lastPromptTokens:nt,prompt:tt,completion:et})),e.debug&&Z(`[debug] 轮${l+1} · ${e.model} · in ${F.usage?.prompt_tokens||0} / out ${F.usage?.completion_tokens||0} tok · ${Date.now()-D}ms · ${(F.toolCalls||[]).length} 工具调用`);const{text:B,sawMarkup:H}=a(F.content),X=H||b.sawMarkup;bt=null;const mt=rt?[]:F.toolCalls;if(!mt.length){if(X&&!B.trim()&&!rt){if(ht<2){ht++,2===ht&&(yt=!0),bt=1===ht?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",Z(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${ht} 次)`);continue}Z("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}if(!rt&&"length"===F.finishReason&&B&&St<3){St++,Mt+=B,e.debug&&Z(`[debug] 输出达单轮长度上限,自动续写(第 ${St} 段)`);continue}y&&!Y&&V("\n");const t=Mt?Mt+B:B;Mt="",t&&P.push({role:"assistant",content:t});const n=!t.trim()||v(t);if(_t&&n&&"length"!==F.finishReason&&!yt&&xt<2){xt++,e.debug&&Z(`[debug] 检测到「宣告未落地」收尾(空正文或预告式),注入续跑纠偏(第 ${xt} 次)`),P.push({role:"user",content:'你上一条只是说明"接下来要做什么",但既没有发起工具调用、也没有给出结果——用户还没拿到任何最终产物。请立即执行你刚才所说的步骤(需要就用 run_python/工具,不需要就直接算),并直接输出面向用户的完整最终结果(例如完整的按月汇总表与逐月核对表、差异结论),不要再只描述你打算做什么。'});continue}Y&&Y.onTurnEnd?.(t);const o=Q?.();if(o){P.push({role:"user",content:o});continue}if(Et){const e=M("Stop",{cwd:A,result:t},$t);for(const t of e.outputs)Z("钩子·Stop:"+K(t.stdout,160))}if(!rt&&e.decisionAutoExtract&&!1!==e.decisionsEnabled&&_t&&!Tt&&t&&t.trim())try{const n=[...P].reverse().find(t=>"user"===t.role),o=n&&"string"==typeof n.content?n.content:"",r=await _(e,{userText:o,assistantText:t,toolNames:[...kt]}),s="number"==typeof e.decisionExtractMinConfidence?e.decisionExtractMinConfidence:.55;if(r&&(r.confidence??0)>=s){const t=P.filter(t=>"user"===t.role).length,n=x(A,{...r,title:r.what,status:"draft",origin:"agent",confidence:Math.min(r.confidence??.5,.7),sessionId:e.sessionId||"",turn:t},{origin:"agent"});Y?Y.onDecision?Y.onDecision(n):Y.onInfo?.("已抽取决策草稿(待确认):"+K(n.title,80)):V(E.gray(" ◇ 已抽取决策草稿(待确认):"+K(n.title,80))+"\n")}}catch{}return{usage:{prompt:tt,completion:et},lastPromptTokens:nt}}_t=!0,y&&!Y&&V("\n"),Y&&B&&Y.onAssistantText?.(B),P.push({role:"assistant",content:Mt?Mt+B:B,tool_calls:mt}),Mt="";const pt=mt.map((t,e)=>{const n=t.function?.name||"";let o={},r=null;const s=t.function?.arguments||"";if(s)try{o=JSON.parse(s)}catch(t){r=`工具参数不是合法 JSON(${t.message}):${String(s).slice(0,200)}…请修正参数后重新调用。`}const i=ft.get(n),a=$.isActive()&&("write_file"===n||"edit_file"===n);return{tc:t,idx:e,name:n,args:o,argsError:r,extra:i,readOnly:i?!!i.readOnly:c.has(n),deferToIde:a}}),Pt=t=>{for(let e=t;e<pt.length;e++)P.push({role:"tool",tool_call_id:pt[e].tc.id||`call_${e}`,content:O+" 该工具未执行,无任何结果,请勿臆测其输出。"});return Y?Y.onInfo?.("已中断"):V(E.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:tt,completion:et},lastPromptTokens:nt}},jt=async t=>{if(t.argsError)return t.argsError;let n;try{n=t.extra?await t.extra.run(t.args):await u(t.name,t.args,A,e,q)}catch(t){n=`工具执行失败:${t.message}`}return capToolResult(String(n))},At=t=>{kt.add(t.name),Y?Y.onToolStart?.(t.name,t.args):V(E.cyan(" ⏺ ")+m(t.name,t.args)+"\n")},It=(t,e)=>{C(e)&&wt++,Y?Y.onToolResult?.(t.name,String(e)):V(E.gray(" "+K(e))+"\n"),P.push({role:"tool",tool_call_id:t.tc.id,content:String(e)})},vt=(t,e)=>{Y?Y.onSkip?.(t.name,t.args):V(E.gray(` ⊘ ${m(t.name,t.args)} — 已跳过\n`)),P.push({role:"tool",tool_call_id:t.tc.id,content:e||"未授权,未执行。"})},Ct=t=>"edit_file"===t||"write_file"===t,Ut=e=>{try{return t.readFileSync(e,"utf8")}catch{return""}},Ot=(t,e)=>{const o=n.resolve(A,String(t.args.path)),r=g(e,Ut(o),{path:String(t.args.path)});r&&(Y?Y.onFileDiff?.(r):V(r+"\n"))};let Rt=0;for(;Rt<pt.length;){if(q?.aborted)return Pt(Rt);const o=pt[Rt];if(o.argsError){At(o),It(o,o.argsError),Rt++;continue}if(o.readOnly){let t=Rt;for(;t<pt.length&&pt[t].readOnly&&!pt[t].argsError;)t++;const n=pt.slice(Rt,t),o=await J(n,toolConcurrency(e),async t=>{const e=await j.request(t.name,t.args,A,!0);return e.allow?(At(t),{item:t,result:await jt(t)}):{item:t,denied:e.reason||"未授权,未执行。"}});for(const t of o)null!=t.denied?vt(t.item,t.denied):It(t.item,t.result);if(Rt=t,q?.aborted)return Pt(Rt);continue}const r=await j.request(o.name,o.args,A,o.readOnly||o.deferToIde);if(q?.aborted)return Pt(Rt);if(!r.allow){vt(o,r.reason),Rt++;continue}if(Et){const t=M("PreToolUse",{cwd:A,toolName:o.name,args:o.args},$t);for(const e of t.outputs)Z("钩子·PreToolUse:"+K(e.stdout,120));if(t.block){vt(o,"PreToolUse 钩子阻断:"+t.reason),Rt++;continue}t.modifiedArgs&&(o.args={...o.args,...t.modifiedArgs},Z("钩子·PreToolUse 已修改工具参数"))}At(o);const s=Ct(o.name)&&o.args?.path?Ut(n.resolve(A,String(o.args.path))):null;if(G&&null!=s&&o.args?.path){const e=t.existsSync(n.resolve(A,String(o.args.path)));G.push(s.length>2097152?{path:String(o.args.path),tooLarge:!0}:{path:String(o.args.path),before:s,existed:e})}let i=await jt(o);if(Et){const t=M("PostToolUse",{cwd:A,toolName:o.name,args:o.args,result:String(i)},$t);if(t.outputs.length){for(const e of t.outputs)Z("钩子·PostToolUse:"+K(e.stdout,120));i=String(i)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}It(o,i),null!=s&&Ot(o,s),Rt++}wt>=3&&!yt&&!bt&&(bt="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",wt=0,Z("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const Nt=Q?.();Nt&&P.push({role:"user",content:Nt})}return Mt&&(P.push({role:"assistant",content:Mt}),Y&&Y.onTurnEnd?.(Mt)),Y?Y.onInfo?.("已达最大工具轮数,停止"):V(E.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:tt,completion:et},lastPromptTokens:nt}}
package/src/api.js CHANGED
@@ -1 +1 @@
1
- const t=new Set([429,500,502,503,504]),e=15e3;function n(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t}`}}function o(t,e){try{const e=JSON.parse(t),n=e?.error?.type||e?.type||"";if(e?.error?.message)return{message:e.error.message,type:n};if(e?.message)return{message:e.message,type:n};if(e?.msg)return{message:e.msg,type:n}}catch{}return{message:t||e,type:""}}async function r(t){let e="";try{e=await t.text()}catch{}return o(e,`HTTP ${t.status}`)}function a(t,e){for(const n of e){const e="number"==typeof n.index?n.index:t.length;t[e]||(t[e]={id:"",type:"function",function:{name:"",arguments:""}}),n.id&&(t[e].id=n.id),n.type&&(t[e].type=n.type),n.function?.name&&(t[e].function.name+=n.function.name),n.function?.arguments&&(t[e].function.arguments+=n.function.arguments)}}function s(t,e){return new Promise((n,o)=>{if(e?.aborted)return o(i());const r=setTimeout(()=>{e?.removeEventListener?.("abort",a),n()},t),a=()=>{clearTimeout(r),o(i())};e?.addEventListener?.("abort",a,{once:!0})})}function i(){const t=new Error("已中断");return t.name="AbortError",t}function c(t,e){const n=new AbortController;let o=!1;const r=()=>n.abort();t&&(t.aborted?n.abort():t.addEventListener("abort",r,{once:!0}));const a=setTimeout(()=>{o=!0,n.abort()},e);return{signal:n.signal,ms:e,get timedOut(){return o},done(){clearTimeout(a),t?.removeEventListener?.("abort",r)}}}function u(t){const e=String(t?.message||t||""),n=t?.cause?.code||t?.code||"";return"ECONNREFUSED"===n||/ECONNREFUSED|connection refused/i.test(e)?"无法连接模型服务(连接被拒绝):请检查网络或稍后重试。":"ENOTFOUND"===n||"EAI_AGAIN"===n||/getaddrinfo|ENOTFOUND|EAI_AGAIN/i.test(e)?"无法解析模型服务地址(DNS 异常):请检查网络连接后重试。":"ETIMEDOUT"===n||/ETIMEDOUT|timeout|timed out/i.test(e)?"连接模型服务超时:网络较慢或服务繁忙,请稍后重试。":"ECONNRESET"===n||/ECONNRESET|socket hang up|network|fetch failed/i.test(e)?"与模型服务的连接中断:网络波动,请稍后重试。":"网络错误:"+e}function f(t,e){const n=e?.headers?.get?.("retry-after"),o=null!=n?Number(n):NaN;return Number.isFinite(o)&&o>=0?Math.min(1e3*o,3e4):Math.min(500*2**t+Math.floor(250*Math.random()),8e3)}export async function streamChat(e,o={},a){const m=`${e.baseUrl}/v1/chat/completions`,d={model:e.model,messages:e.messages,stream:!0,stream_options:{include_usage:!0}};e.tools&&e.tools.length&&(d.tools=e.tools,d.tool_choice="auto"),"number"==typeof e.temperature&&(d.temperature=e.temperature),Number.isFinite(e.maxTokens)&&e.maxTokens>0&&(d.max_tokens=Math.floor(e.maxTokens)),e.reasoningEffort&&"off"!==e.reasoningEffort&&(d.reasoning_effort=e.reasoningEffort),e.extraBody&&"object"==typeof e.extraBody&&Object.assign(d,e.extraBody);const g=n(e.apiKey);e.client&&(g["X-Huicai-Client"]=e.client),e.sessionId&&(g["X-Huicai-Session"]=e.sessionId),e.aux&&(g["X-Huicai-Aux"]="1"),e.continuation&&(g["X-Huicai-Continue"]="1"),e.tailCount>0&&(g["X-Huicai-Tail"]=String(e.tailCount));const h=JSON.stringify(d);for(let n=0;;n++){let d;const y=c(a,e.headersTimeoutMs??12e4);try{d=await fetch(m,{method:"POST",headers:g,body:h,signal:y.signal})}catch(t){if(a?.aborted)throw i();if(y.timedOut){const t=`等待模型服务响应超时(${Math.round(y.ms/1e3)}s 内未返回响应头)`;if(n<2){const e=f(n,null);o.onRetry?.(n+1,e,t),await s(e,a);continue}throw new Error(t+",请稍后重试。")}if("AbortError"===t?.name)throw i();if(n<2){const e=f(n,null);o.onRetry?.(n+1,e,u(t)),await s(e,a);continue}throw new Error(u(t))}finally{y.done()}if(!d.ok||!d.body){const e=await r(d);if(!("usage_limit_exceeded"===e.type)&&t.has(d.status)&&n<2){const t=f(n,d);o.onRetry?.(n+1,t,`HTTP ${d.status}:${e.message}`),await s(t,a);continue}throw new Error(e.message)}return l(d,o,a,e.stallTimeoutMs??18e4)}}async function l(t,e,n,o){const r=t.body.getReader(),s=new TextDecoder;let c="",u="";const f=[];let l=null,m=null,d=!1,g=null,h=!1;const y=()=>{g&&clearTimeout(g),g=setTimeout(()=>{d=!0,r.cancel().catch(()=>{})},o)},w=()=>{h=!0,y()},p=()=>new Error(h?`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`:`模型 ${Math.round(o/1e3)}s 内没有任何输出,已中止(可重试)`),b=()=>r.cancel().catch(()=>{});n?.addEventListener?.("abort",b,{once:!0});try{for(y();;){let t;try{t=await r.read()}catch(t){if(n?.aborted)throw i();if(d)throw p();throw t}if(n?.aborted)throw i();if(d)throw p();const{done:o,value:g}=t;if(o)break;c+=s.decode(g,{stream:!0});const h=c.split("\n");c=h.pop()??"";for(const t of h){const n=t.trim();if(!n.startsWith("data:"))continue;const o=n.slice(5).trim();if("[DONE]"===o||""===o)continue;let r;try{r=JSON.parse(o)}catch{continue}if(r.error){const t=r.error&&(r.error.message||r.error.msg||r.error);throw new Error("模型返回错误:"+("string"==typeof t?t:JSON.stringify(t)))}r.usage&&(m=r.usage,w());const s=r.choices?.[0];if(!s)continue;const i=s.delta||{},c=i.reasoning_content??i.reasoning;c&&(w(),e.onReasoning?.(c)),i.content&&(w(),u+=i.content,e.onContent?.(i.content)),Array.isArray(i.tool_calls)&&(w(),a(f,i.tool_calls)),s.finish_reason&&(w(),l=s.finish_reason)}}}finally{g&&clearTimeout(g),n?.removeEventListener?.("abort",b)}return{content:u,toolCalls:f.filter(Boolean),finishReason:l,usage:m}}export async function embed(t,e,o,a){const s=await fetch(`${t}/v1/embeddings`,{method:"POST",headers:n(e),body:JSON.stringify({model:o,input:Array.isArray(a)?a:[a]}),signal:AbortSignal.timeout(3e4)});if(!s.ok)throw new Error((await r(s)).message);const i=await s.json();return Array.isArray(i?.data)?i.data.map(t=>t.embedding):null}export async function syncKnowledge(t,e,o,a){const s=await fetch(`${t}/v1/knowledge/${encodeURIComponent(o)}/sync`,{method:"POST",headers:n(e),body:JSON.stringify({files:a}),signal:AbortSignal.timeout(12e4)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}export async function pushDecisions(t,e,o,a){const s=await fetch(`${t}/v1/decisions/sync`,{method:"POST",headers:n(e),body:JSON.stringify({projectKey:o,nodes:a}),signal:AbortSignal.timeout(6e4)});if(!s.ok)throw new Error((await r(s)).message);const i=await s.json();return Number(i?.accepted||0)}export async function pullDecisions(t,e,o,a){const s=new URLSearchParams({projectKey:o});a&&s.set("since",String(a));const i=await fetch(`${t}/v1/decisions?${s.toString()}`,{headers:n(e),signal:AbortSignal.timeout(6e4)});if(!i.ok)throw new Error((await r(i)).message);const c=await i.json();return Array.isArray(c?.data)?c.data:[]}export async function listModels(t,o){const a=await fetch(`${t}/v1/models`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await r(a)).message);const s=await a.json();return Array.isArray(s?.data)?s.data:[]}export async function getPlatformConfig(t,o){try{const r=await fetch(`${t}/v1/config`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!r.ok)return{};const a=await r.json();return a&&"object"==typeof a?a:{}}catch{return{}}}export async function getAgentPersona(t,o,a){const s=await fetch(`${t}/v1/agents/${encodeURIComponent(a)}/persona`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}export async function fetchUsage(t,o){const a=await fetch(`${t}/v1/usage`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await r(a)).message);return a.json()}export async function extractDocument(t,e,o,a){const s=o.bytes||(o.data?Buffer.from(String(o.data),"base64"):null);if(s&&!1!==d.get(t))try{const{body:r,contentType:i}=function(t,e,n,o){const r="----huicai"+Date.now().toString(36)+Math.random().toString(36).slice(2,10),a=String(t||"file").replace(/[\r\n"]/g,"_"),s=[];s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="file"; filename="${a}"\r\nContent-Type: application/octet-stream\r\n\r\n`)),s.push(e,Buffer.from("\r\n"));for(const[t,e]of[["maxChars",n],["offset",o]])null!=e&&s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="${t}"\r\n\r\n${e}\r\n`));return s.push(Buffer.from(`--${r}--\r\n`)),{body:Buffer.concat(s),contentType:`multipart/form-data; boundary=${r}`}}(o.filename,s,o.maxChars,o.offset),c=m(await w(`${t}/v1/files/extract`,{...n(e),"content-type":i},r,a),!0);return d.set(t,!0),c}catch(e){if(!e||!e._noMultipart)throw e;d.set(t,!1)}const i=o.bytes?{...o,bytes:void 0,data:o.bytes.toString("base64")}:o,c=JSON.stringify(i);if("function"==typeof a){return m(await w(`${t}/v1/files/extract`,{...n(e)},Buffer.from(c),a),!1)}const u=await fetch(`${t}/v1/files/extract`,{method:"POST",headers:n(e),body:c,signal:AbortSignal.timeout(12e4)});if(!u.ok)throw new Error((await r(u)).message);return m(await u.text(),!1)}function m(t,e){let n=null;try{n=JSON.parse(t)}catch{}if(n&&"string"==typeof n.text)return n;const o=n&&(n.message||n.msg||n.error?.message)||"解析服务返回异常响应",r=new Error(o);throw e&&(r._noMultipart=!0),r}const d=new Map;const g=12e4,h=25,y=9e5;async function w(t,e,n,r){const{request:a}=await import("http:"===new URL(t).protocol?"node:http":"node:https"),s={};for(const[t,n]of Object.entries(e||{}))s[String(t).toLowerCase()]=n;return s["content-type"]||(s["content-type"]="application/json"),s["content-length"]=n.length,new Promise((e,i)=>{const c=a(t,{method:"POST",headers:s,agent:!1},t=>{const n=[];t.on("data",t=>n.push(t)),t.on("error",i),t.on("end",()=>{const r=Buffer.concat(n).toString("utf8");t.statusCode>=200&&t.statusCode<300?e(r):415===t.statusCode||405===t.statusCode?i(Object.assign(new Error(o(r,`HTTP ${t.statusCode}`).message),{_noMultipart:!0})):i(new Error(o(r,`HTTP ${t.statusCode}`).message))})}),u=Math.min(y,g+Math.floor(n.length/h));c.setTimeout(u,()=>c.destroy(new Error(`网络传输/服务端处理超时(已等待 ${Math.round(u/1e3)} 秒无响应),请检查网络后重试`))),c.on("error",i);let f=0;const l=()=>{for(;f<n.length;){const t=Math.min(f+262144,n.length),e=c.write(n.subarray(f,t));f=t;try{r&&r(f,n.length)}catch{}if(!e)return void c.once("drain",l)}c.end()};l()})}export function startParseTicker(t,e,n=200){const o=Math.max(600,400+t/15360),r=Date.now(),a=setInterval(()=>{const t=Date.now()-r,n=Math.min(99,Math.round(100*(1-Math.exp(-t/o))));try{e(n,t)}catch{}},n);return()=>clearInterval(a)}export async function listPlatformSkills(t,o){const a=await fetch(`${t}/v1/skills`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!a.ok)throw new Error((await r(a)).message);const s=await a.json();return Array.isArray(s?.data)?s.data:[]}export async function getPlatformSkill(t,o,a){const s=await fetch(`${t}/v1/skills/${encodeURIComponent(a)}`,{headers:n(o),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}export async function pushPlatformSkill(t,o,a){const s=await fetch(`${t}/v1/skills/import`,{method:"POST",headers:{...n(o),"Content-Type":"application/json"},body:JSON.stringify(a),signal:AbortSignal.timeout(e)});if(!s.ok)throw new Error((await r(s)).message);return s.json()}
1
+ import{sanitizeMessagesForModel as t}from"./vision.js";const e=new Set([429,500,502,503,504]),n=15e3;function o(t){return{"Content-Type":"application/json",Authorization:`Bearer ${t}`}}function r(t,e){try{const e=JSON.parse(t),n=e?.error?.type||e?.type||"";if(e?.error?.message)return{message:e.error.message,type:n};if(e?.message)return{message:e.message,type:n};if(e?.msg)return{message:e.msg,type:n}}catch{}return{message:t||e,type:""}}async function a(t){let e="";try{e=await t.text()}catch{}return{...r(e,`HTTP ${t.status}`),status:t.status}}function s(t){return Object.assign(new Error(t.message),{status:t.status,type:t.type})}function i(t,e){for(const n of e){const e="number"==typeof n.index?n.index:t.length;t[e]||(t[e]={id:"",type:"function",function:{name:"",arguments:""}}),n.id&&(t[e].id=n.id),n.type&&(t[e].type=n.type),n.function?.name&&(t[e].function.name+=n.function.name),n.function?.arguments&&(t[e].function.arguments+=n.function.arguments)}}function c(t,e){return new Promise((n,o)=>{if(e?.aborted)return o(u());const r=setTimeout(()=>{e?.removeEventListener?.("abort",a),n()},t),a=()=>{clearTimeout(r),o(u())};e?.addEventListener?.("abort",a,{once:!0})})}function u(){const t=new Error("已中断");return t.name="AbortError",t}function f(t,e){const n=new AbortController;let o=!1;const r=()=>n.abort();t&&(t.aborted?n.abort():t.addEventListener("abort",r,{once:!0}));const a=setTimeout(()=>{o=!0,n.abort()},e);return{signal:n.signal,ms:e,get timedOut(){return o},done(){clearTimeout(a),t?.removeEventListener?.("abort",r)}}}function l(t){const e=String(t?.message||t||""),n=t?.cause?.code||t?.code||"";return"ECONNREFUSED"===n||/ECONNREFUSED|connection refused/i.test(e)?"无法连接模型服务(连接被拒绝):请检查网络或稍后重试。":"ENOTFOUND"===n||"EAI_AGAIN"===n||/getaddrinfo|ENOTFOUND|EAI_AGAIN/i.test(e)?"无法解析模型服务地址(DNS 异常):请检查网络连接后重试。":"ETIMEDOUT"===n||/ETIMEDOUT|timeout|timed out/i.test(e)?"连接模型服务超时:网络较慢或服务繁忙,请稍后重试。":"ECONNRESET"===n||/ECONNRESET|socket hang up|network|fetch failed/i.test(e)?"与模型服务的连接中断:网络波动,请稍后重试。":"网络错误:"+e}function m(t,e){const n=e?.headers?.get?.("retry-after"),o=null!=n?Number(n):NaN;return Number.isFinite(o)&&o>=0?Math.min(1e3*o,3e4):Math.min(500*2**t+Math.floor(250*Math.random()),8e3)}export async function streamChat(n,r={},i){const h=`${n.baseUrl}/v1/chat/completions`,{messages:g}=t(n.messages,n.model),y={model:n.model,messages:g,stream:!0,stream_options:{include_usage:!0}};n.tools&&n.tools.length&&(y.tools=n.tools,y.tool_choice="auto"),"number"==typeof n.temperature&&(y.temperature=n.temperature),Number.isFinite(n.maxTokens)&&n.maxTokens>0&&(y.max_tokens=Math.floor(n.maxTokens)),n.reasoningEffort&&"off"!==n.reasoningEffort&&(y.reasoning_effort=n.reasoningEffort),n.extraBody&&"object"==typeof n.extraBody&&Object.assign(y,n.extraBody);const p=o(n.apiKey);n.client&&(p["X-Huicai-Client"]=n.client),n.sessionId&&(p["X-Huicai-Session"]=n.sessionId),n.aux&&(p["X-Huicai-Aux"]="1"),n.continuation&&(p["X-Huicai-Continue"]="1"),n.tailCount>0&&(p["X-Huicai-Tail"]=String(n.tailCount));const w=JSON.stringify(y);for(let t=0;;t++){let o;const g=f(i,n.headersTimeoutMs??12e4);try{o=await fetch(h,{method:"POST",headers:p,body:w,signal:g.signal})}catch(e){if(i?.aborted)throw u();if(g.timedOut){const e=`等待模型服务响应超时(${Math.round(g.ms/1e3)}s 内未返回响应头)`;if(t<2){const n=m(t,null);r.onRetry?.(t+1,n,e),await c(n,i);continue}throw new Error(e+",请稍后重试。")}if("AbortError"===e?.name)throw u();if(t<2){const n=m(t,null);r.onRetry?.(t+1,n,l(e)),await c(n,i);continue}throw new Error(l(e))}finally{g.done()}if(!o.ok||!o.body){const n=await a(o);if(!("usage_limit_exceeded"===n.type)&&e.has(o.status)&&t<2){const e=m(t,o);r.onRetry?.(t+1,e,`HTTP ${o.status}:${n.message}`),await c(e,i);continue}throw s(n)}return d(o,r,i,n.stallTimeoutMs??18e4)}}async function d(t,e,n,o){const r=t.body.getReader(),a=new TextDecoder;let s="",c="";const f=[];let l=null,m=null,d=!1,h=null,g=!1;const y=()=>{h&&clearTimeout(h),h=setTimeout(()=>{d=!0,r.cancel().catch(()=>{})},o)},p=()=>{g=!0,y()},w=()=>new Error(g?`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`:`模型 ${Math.round(o/1e3)}s 内没有任何输出,已中止(可重试)`),b=()=>r.cancel().catch(()=>{});n?.addEventListener?.("abort",b,{once:!0});try{for(y();;){let t;try{t=await r.read()}catch(t){if(n?.aborted)throw u();if(d)throw w();throw t}if(n?.aborted)throw u();if(d)throw w();const{done:o,value:h}=t;if(o)break;s+=a.decode(h,{stream:!0});const g=s.split("\n");s=g.pop()??"";for(const t of g){const n=t.trim();if(!n.startsWith("data:"))continue;const o=n.slice(5).trim();if("[DONE]"===o||""===o)continue;let r;try{r=JSON.parse(o)}catch{continue}if(r.error){const t=r.error&&(r.error.message||r.error.msg||r.error);throw new Error("模型返回错误:"+("string"==typeof t?t:JSON.stringify(t)))}r.usage&&(m=r.usage,p());const a=r.choices?.[0];if(!a)continue;const s=a.delta||{},u=s.reasoning_content??s.reasoning;u&&(p(),e.onReasoning?.(u)),s.content&&(p(),c+=s.content,e.onContent?.(s.content)),Array.isArray(s.tool_calls)&&(p(),i(f,s.tool_calls)),a.finish_reason&&(p(),l=a.finish_reason)}}}finally{h&&clearTimeout(h),n?.removeEventListener?.("abort",b)}return{content:c,toolCalls:f.filter(Boolean),finishReason:l,usage:m}}export async function embed(t,e,n,r){const i=await fetch(`${t}/v1/embeddings`,{method:"POST",headers:o(e),body:JSON.stringify({model:n,input:Array.isArray(r)?r:[r]}),signal:AbortSignal.timeout(3e4)});if(!i.ok)throw s(await a(i));const c=await i.json();return Array.isArray(c?.data)?c.data.map(t=>t.embedding):null}export async function syncKnowledge(t,e,n,r){const i=await fetch(`${t}/v1/knowledge/${encodeURIComponent(n)}/sync`,{method:"POST",headers:o(e),body:JSON.stringify({files:r}),signal:AbortSignal.timeout(12e4)});if(!i.ok)throw s(await a(i));return i.json()}export async function pushDecisions(t,e,n,r){const i=await fetch(`${t}/v1/decisions/sync`,{method:"POST",headers:o(e),body:JSON.stringify({projectKey:n,nodes:r}),signal:AbortSignal.timeout(6e4)});if(!i.ok)throw s(await a(i));const c=await i.json();return Number(c?.accepted||0)}export async function pullDecisions(t,e,n,r){const i=new URLSearchParams({projectKey:n});r&&i.set("since",String(r));const c=await fetch(`${t}/v1/decisions?${i.toString()}`,{headers:o(e),signal:AbortSignal.timeout(6e4)});if(!c.ok)throw s(await a(c));const u=await c.json();return Array.isArray(u?.data)?u.data:[]}export async function listModels(t,e){const r=await fetch(`${t}/v1/models`,{headers:o(e),signal:AbortSignal.timeout(n)});if(!r.ok)throw s(await a(r));const i=await r.json();return Array.isArray(i?.data)?i.data:[]}export async function getPlatformConfig(t,e){try{const r=await fetch(`${t}/v1/config`,{headers:o(e),signal:AbortSignal.timeout(n)});if(!r.ok)return{};const a=await r.json();return a&&"object"==typeof a?a:{}}catch{return{}}}export async function getAgentPersona(t,e,r){const i=await fetch(`${t}/v1/agents/${encodeURIComponent(r)}/persona`,{headers:o(e),signal:AbortSignal.timeout(n)});if(!i.ok)throw s(await a(i));return i.json()}export async function fetchUsage(t,e){const r=await fetch(`${t}/v1/usage`,{headers:o(e),signal:AbortSignal.timeout(n)});if(!r.ok)throw s(await a(r));return r.json()}export async function extractDocument(t,e,n,r){const i=n.bytes||(n.data?Buffer.from(String(n.data),"base64"):null);if(i&&!1!==g.get(t))try{const{body:a,contentType:s}=function(t,e,n,o){const r="----huicai"+Date.now().toString(36)+Math.random().toString(36).slice(2,10),a=String(t||"file").replace(/[\r\n"]/g,"_"),s=[];s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="file"; filename="${a}"\r\nContent-Type: application/octet-stream\r\n\r\n`)),s.push(e,Buffer.from("\r\n"));for(const[t,e]of[["maxChars",n],["offset",o]])null!=e&&s.push(Buffer.from(`--${r}\r\nContent-Disposition: form-data; name="${t}"\r\n\r\n${e}\r\n`));return s.push(Buffer.from(`--${r}--\r\n`)),{body:Buffer.concat(s),contentType:`multipart/form-data; boundary=${r}`}}(n.filename,i,n.maxChars,n.offset),c=h(await b(`${t}/v1/files/extract`,{...o(e),"content-type":s},a,r),!0);return g.set(t,!0),c}catch(e){if(!e||!e._noMultipart)throw e;g.set(t,!1)}const c=n.bytes?{...n,bytes:void 0,data:n.bytes.toString("base64")}:n,u=JSON.stringify(c);if("function"==typeof r){return h(await b(`${t}/v1/files/extract`,{...o(e)},Buffer.from(u),r),!1)}const f=await fetch(`${t}/v1/files/extract`,{method:"POST",headers:o(e),body:u,signal:AbortSignal.timeout(12e4)});if(!f.ok)throw s(await a(f));return h(await f.text(),!1)}function h(t,e){let n=null;try{n=JSON.parse(t)}catch{}if(n&&"string"==typeof n.text)return n;const o=n&&(n.message||n.msg||n.error?.message)||"解析服务返回异常响应",r=new Error(o);throw e&&(r._noMultipart=!0),r}const g=new Map;const y=12e4,p=25,w=9e5;async function b(t,e,n,o){const{request:a}=await import("http:"===new URL(t).protocol?"node:http":"node:https"),s={};for(const[t,n]of Object.entries(e||{}))s[String(t).toLowerCase()]=n;return s["content-type"]||(s["content-type"]="application/json"),s["content-length"]=n.length,new Promise((e,i)=>{const c=a(t,{method:"POST",headers:s,agent:!1},t=>{const n=[];t.on("data",t=>n.push(t)),t.on("error",i),t.on("end",()=>{const o=Buffer.concat(n).toString("utf8");t.statusCode>=200&&t.statusCode<300?e(o):415===t.statusCode||405===t.statusCode?i(Object.assign(new Error(r(o,`HTTP ${t.statusCode}`).message),{_noMultipart:!0})):i(new Error(r(o,`HTTP ${t.statusCode}`).message))})}),u=Math.min(w,y+Math.floor(n.length/p));c.setTimeout(u,()=>c.destroy(new Error(`网络传输/服务端处理超时(已等待 ${Math.round(u/1e3)} 秒无响应),请检查网络后重试`))),c.on("error",i);let f=0;const l=()=>{for(;f<n.length;){const t=Math.min(f+262144,n.length),e=c.write(n.subarray(f,t));f=t;try{o&&o(f,n.length)}catch{}if(!e)return void c.once("drain",l)}c.end()};l()})}export function startParseTicker(t,e,n=200){const o=Math.max(600,400+t/15360),r=Date.now(),a=setInterval(()=>{const t=Date.now()-r,n=Math.min(99,Math.round(100*(1-Math.exp(-t/o))));try{e(n,t)}catch{}},n);return()=>clearInterval(a)}export async function listPlatformSkills(t,e){const r=await fetch(`${t}/v1/skills`,{headers:o(e),signal:AbortSignal.timeout(n)});if(!r.ok)throw s(await a(r));const i=await r.json();return Array.isArray(i?.data)?i.data:[]}export async function getPlatformSkill(t,e,r){const i=await fetch(`${t}/v1/skills/${encodeURIComponent(r)}`,{headers:o(e),signal:AbortSignal.timeout(n)});if(!i.ok)throw s(await a(i));return i.json()}export async function pushPlatformSkill(t,e,r){const i=await fetch(`${t}/v1/skills/import`,{method:"POST",headers:{...o(e),"Content-Type":"application/json"},body:JSON.stringify(r),signal:AbortSignal.timeout(n)});if(!i.ok)throw s(await a(i));return i.json()}
@@ -0,0 +1 @@
1
+ import*as e from"./schedule.js";import{isValidCron as r}from"./cron.js";import{describeSchedule as n}from"./schedule-freq.js";const t=(e,r,n={},t=[])=>({type:"function",function:{name:e,description:r,parameters:{type:"object",properties:n,required:t}}}),i=e=>({type:"string",description:e}),o=e=>String(e).padStart(2,"0");export function parseWhen(e,r=new Date){const n=String(e||"").trim();if(!n)return{error:"缺少时间"};if(/^(\S+\s+){4}\S+$/.test(n))return{cron:n};let t=/^每\s*隔?\s*(\d+)\s*(分钟?|min|m)$/i.exec(n);if(t){const e=Math.min(59,Math.max(1,Number(t[1])));return{cron:`*/${e} * * * *`,freq:{kind:"interval",every:e,unit:"minute"}}}if(t=/^每\s*隔?\s*(\d+)\s*(小时|时|h)$/i.exec(n),t){const e=Math.min(23,Math.max(1,Number(t[1])));return{cron:`0 */${e} * * *`,freq:{kind:"interval",every:e,unit:"hour"}}}const i=/^\+\s*(\d+)\s*(m|min|分钟?|h|小时|时)?$/i.exec(n);if(i){const e=Number(i[1]),n=(i[2]||"m").toLowerCase(),t=new Date(r.getTime()+6e4*(/h|小时|时/.test(n)?60*e:e));return{cron:`${t.getMinutes()} ${t.getHours()} ${t.getDate()} ${t.getMonth()+1} *`,once:!0,freq:{kind:"once",date:`${t.getFullYear()}-${o(t.getMonth()+1)}-${o(t.getDate())}`,time:`${o(t.getHours())}:${o(t.getMinutes())}`}}}const s=/^每\s*(周|星期)\s*([一二三四五六日天1-7])\s*(?:的?\s*(\d{1,2}):(\d{2}))?$/.exec(n);if(s){const e={"日":0,"天":0,"一":1,"二":2,"三":3,"四":4,"五":5,"六":6,7:0},r=null!=e[s[2]]?e[s[2]]:Number(s[2])%7,n=null!=s[3]?Number(s[3]):9,t=null!=s[4]?Number(s[4]):0;return n>23||t>59?{error:"无效时间"}:{cron:`${t} ${n} * * ${r}`,freq:{kind:"periodic",period:"weekly",weekday:r,time:`${o(n)}:${o(t)}`}}}const a=/^(?:每天\s*)?(\d{1,2}):(\d{2})$/.exec(n);if(a){const e=Number(a[1]),r=Number(a[2]);return e>23||r>59?{error:"无效时间"}:{cron:`${r} ${e} * * *`,freq:{kind:"periodic",period:"daily",time:`${o(e)}:${o(r)}`}}}return/每天|daily/i.test(n)?{cron:"0 9 * * *",freq:{kind:"periodic",period:"daily",time:"09:00"}}:/工作日|weekday/i.test(n)?{cron:"0 9 * * 1-5"}:{error:"无法解析时间(用 +30m / 14:30 / 每隔5分钟 / 每天 / 工作日 / 每周一 / cron 五段)"}}export function toCron(e,r=new Date){const n=parseWhen(e,r);return n.error?{error:n.error}:n.once?{cron:n.cron,once:!0}:{cron:n.cron}}export function resolveTask(e,{id:r,name:n,match:t}={}){const i=e&&e()||[];if(r){const e=i.find(e=>e.id===r);if(e)return e}const o=String(n||t||r||"").trim().toLowerCase();return o&&(i.find(e=>String(e.name||"").toLowerCase()===o)||i.find(e=>String(e.name||"").toLowerCase().includes(o))||i.find(e=>String(e.prompt||"").toLowerCase().includes(o)))||null}export function makeAutomationTools({cwd:o,deps:s={},selfSchedule:a=!0,runnerHint:c=""}={}){const u=s.addTask||e.addTask,m=s.listTasks||e.listTasks,d=s.updateTask||e.updateTask,l=s.removeTask||e.removeTask,p=s.setEnabled||e.setEnabled,f=s.isValidCron||r,$=c||"到点由调度器在该目录自动执行(桌面端需保持应用运行;终端/IDE 需有 `huicai daemon` 在跑)。",h=[];a&&h.push({readOnly:!1,spec:t("schedule_followup","给自己安排一个后续任务,到点由系统在同一项目目录自动继续执行(无需用户在场,也无需你还开着)。适合「稍后重试」「等 CI/构建完成后检查」「每天定时汇总」这类需要延迟或周期执行的工作。时间写法:+30m、+2h、14:30、每隔5分钟、每天、工作日、每周一、或标准 cron 五段。",{when:i("触发时间:+30m / +2h / 14:30 / 每隔5分钟 / 每天 / 工作日 / 每周一 / cron 五段"),task:i("到点要执行的任务说明(必须独立自足——届时是全新会话,看不到当前对话)"),name:i("(可选)给这个自动化起个简短名字")},["when","task"]),run:async e=>{const{cron:r,freq:n,once:t,error:i}=parseWhen(e?.when);if(i)return"排期失败:"+i;if(!f(r))return"排期失败:生成的 cron 无效("+r+")。";const s=String(e?.task||"").trim();if(!s)return"排期失败:任务说明不能为空。";const{id:a}=u({cron:r,freq:n,prompt:s,cwd:o,name:String(e?.name||"").trim()||s.slice(0,20)});return`✅ 已安排自动化 #${a}(${e.when}${t?",一次性":""}):${s.slice(0,60)}\n${$}`}}),h.push({readOnly:!0,spec:t("list_automations","列出当前已配置的自动化任务(含 id、名称、频率、启用状态、上次运行)。用户问「我都有哪些定时任务」或你要改动某条之前,先用它定位。"),run:async()=>{const e=m()||[];return e.length?"当前自动化任务:\n"+e.map(e=>{const r=e.lastRunAt?new Date(e.lastRunAt).toLocaleString("zh-CN"):"从未运行";return`- #${e.id}|${e.name||String(e.prompt||"").slice(0,16)}|${n(e)}|${!1===e.enabled?"已暂停":"启用中"}|${"full"===e.permission?"完全访问":"默认权限"}|上次:${r}`}).join("\n"):"当前没有任何自动化任务。可用 create_automation 新建。"}}),h.push({readOnly:!1,spec:t("create_automation","按用户口述创建一条新的自动化任务。用于「以后每天…」「每隔…就…」「下周一提醒我…」这类请求。任务说明要写成「到时候一个全新会话读了就能独立执行」的样子。",{name:i("自动化名称(简短)"),task:i("到点要执行的任务说明(独立自足、可单独执行)"),when:i("触发时间:每隔5分钟 / 每天 / 14:30 / 工作日 / 每周一 / +30m / cron 五段"),workspace:i("(可选)执行的工作目录,默认当前项目"),permission:i("(可选)default=默认权限(更安全,需审批的操作会被跳过)| full=完全访问权限(可写文件/跑命令,真正无人值守)")},["task","when"]),run:async e=>{const{cron:r,freq:t,once:i,error:s}=parseWhen(e?.when);if(s)return"创建失败:"+s;if(!f(r))return"创建失败:生成的 cron 无效("+r+")。";const a=String(e?.task||"").trim();if(!a)return"创建失败:任务说明不能为空。";const c="full"===e?.permission?"full":"default",{id:d}=u({cron:r,freq:t,prompt:a,cwd:String(e?.workspace||"").trim()||o,name:String(e?.name||"").trim()||a.slice(0,20),permission:c}),l=resolveTask(m,{id:d});return`✅ 已创建自动化 #${d}「${l?.name||""}」:${n(l||{cron:r})}${i?"(一次性)":""},${"full"===c?"完全访问权限":"默认权限"}运行。\n${$}`+("default"===c?"\n提示:默认权限下需审批的操作(写文件/跑命令)会被自动跳过;若这条任务要真正动手干活,改成 full。":"")}}),h.push({readOnly:!1,spec:t("update_automation","修改一条既有自动化(改频率/提示词/名称/权限/工作目录)。先用 id 或名称定位,只填要变更的字段,其余保持不变。改完请用「新旧配置对照」的方式向用户复述变化。",{id:i("目标自动化 id(优先)"),match:i("若无 id,用名称或关键词定位目标"),name:i("(可选)新名称"),task:i("(可选)新的任务提示词"),when:i("(可选)新的触发时间:每隔5分钟 / 每天 / 14:30 / cron 五段"),workspace:i("(可选)新的工作目录"),permission:i("(可选)default | full")}),run:async e=>{const r=resolveTask(m,{id:e?.id,match:e?.match||e?.name});if(!r)return"未找到匹配的自动化,请先用 list_automations 查看现有任务。";const t=r.name,i=n(r),o=String(r.prompt||"").slice(0,40),s={};if(e?.name&&(s.name=String(e.name).trim()),e?.task&&(s.prompt=String(e.task).trim()),e?.workspace&&(s.cwd=String(e.workspace).trim()),e?.permission&&(s.permission="full"===e.permission?"full":"default"),e?.when){const{cron:r,freq:n,error:t}=parseWhen(e.when);if(t)return"修改失败:"+t;if(!f(r))return"修改失败:生成的 cron 无效("+r+")。";s.cron=r,s.freq=n||null}if(!Object.keys(s).length)return"没有需要修改的字段。请指明要改的名称/提示词/频率/权限/目录。";d(r.id,s);const a=resolveTask(m,{id:r.id});return`✅ 已更新自动化 #${r.id}\n|项目|旧配置|新配置\n|名称|${t||"—"}|${a?.name||"—"}\n|频率|${i||"—"}|${n(a||{})}\n|提示|${o||"—"}|${String(a?.prompt||"").slice(0,40)}`}});for(const[e,r,n]of[["pause_automation",!1,"暂停"],["resume_automation",!0,"启用"]])h.push({readOnly:!1,spec:t(e,`${n}一条自动化(用 id 或名称定位)。`,{id:i("目标 id(优先)"),match:i("或用名称/关键词定位")}),run:async e=>{const t=resolveTask(m,{id:e?.id,match:e?.match});return t?(p(t.id,r),`✅ 已${n}自动化 #${t.id}「${t.name||""}」。`):"未找到匹配的自动化。"}});return h.push({readOnly:!1,spec:t("delete_automation","删除一条自动化(用 id 或名称定位)。**不可恢复**:调用前必须先向用户复述这条任务的名称与频率并得到明确确认,不要凭「像是要删」就动手。",{id:i("目标 id(优先)"),match:i("或用名称/关键词定位")}),run:async e=>{const r=resolveTask(m,{id:e?.id,match:e?.match});return r?(l(r.id),`🗑️ 已删除自动化 #${r.id}「${r.name||""}」(${n(r)})。`):"未找到匹配的自动化。"}}),h}
package/src/automation.js CHANGED
@@ -1 +1 @@
1
- import t from"node:fs";import e from"node:path";import{spawn as n}from"node:child_process";import{StringDecoder as o}from"node:string_decoder";import{runBash as r}from"./executors.js";const a=3e4,s=3e5;export const AUTOMATION_TOOLS=new Set(["install_packages","setup_environment","install_plugin","open_browser","open_path","open_app","auto_execute"]);export function automationActionLabel(t,e={}){switch(t){case"install_packages":return`安装 Node.js 包:${Array.isArray(e.packages)&&e.packages.length?e.packages.join(" "):"package.json 全部依赖"}${e.global?"(全局)":""}${e.dev?"(devDependencies)":""}`;case"setup_environment":return Array.isArray(e.commands)&&e.commands.length?`自动配置运行环境:执行 ${e.commands.length} 条配置命令`:"自动配置运行环境:检测项目技术栈并安装依赖";case"install_plugin":return`安装${"jetbrains"===e.target?"JetBrains 插件":"npm"===e.target?"npm 全局工具":"VS Code 扩展"}:${e.id??""}`;case"open_browser":return`打开浏览器访问:${e.url??""}`;case"open_path":return`在文件管理器中打开:${e.path??""}`;case"open_app":return`启动应用:${e.name??""}${e.target?"("+e.target+")":""}`;case"auto_execute":{const t=String(e.goal||"").replace(/\s+/g," ");return"自动执行已规划的任务"+(t?":"+(t.length>60?t.slice(0,60)+"…":t):"")}default:return t}}export function automationDetail(t,e={}){return"setup_environment"===t&&Array.isArray(e.commands)&&e.commands.length?e.commands.slice(0,12).map(t=>"$ "+String(t)).join("\n"):"open_browser"===t?String(e.url??""):"open_path"===t?String(e.path??""):"open_app"===t?String(e.target??""):""}export function which(n,{pathStr:o=process.env.PATH||"",platform:r=process.platform}={}){const a="win32"===r?";":":",s="win32"===r?[".cmd",".exe",".bat",""]:[""];for(const r of o.split(a))if(r)for(const o of s){const a=e.join(r,n+o);try{if(t.accessSync(a,t.constants.X_OK),t.statSync(a).isFile())return a}catch{}}return null}export function runArgv(t,e,{cwd:r,timeoutMs:s=3e5,winShell:i=!1}={}){const c="win32"===process.platform;return new Promise(p=>{let l,u="",d=!1;try{l=n(t,e,c?{cwd:r,shell:!!i}:{cwd:r,detached:!0})}catch(t){return void p({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}const m=setTimeout(()=>{d=!0,(()=>{try{!c&&l.pid?process.kill(-l.pid,"SIGKILL"):l.kill("SIGKILL")}catch{try{l.kill("SIGKILL")}catch{}}})()},s),g=new o("utf8"),h=new o("utf8"),w=t=>{u.length<a&&(u+=t)};l.stdout?.on("data",t=>w(g.write(t))),l.stderr?.on("data",t=>w(h.write(t))),l.on("error",t=>{clearTimeout(m),p({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}),l.on("close",t=>{clearTimeout(m),w(g.end()),w(h.end()),p({code:t,killed:d,output:u.length>a?u.slice(0,a)+"\n…(输出已截断)":u})})})}function i(t,e){return new Promise(o=>{let r,a="";try{r=n(t,e,{stdio:["ignore","pipe","pipe"]})}catch(t){return void o({code:-1,output:`命令启动失败:${t.message}`})}const s=setTimeout(()=>{try{r.unref()}catch{}o({code:0,output:a,lingering:!0})},8e3);r.stdout.on("data",t=>a+=t),r.stderr.on("data",t=>a+=t),r.on("error",t=>{clearTimeout(s),o({code:-1,output:`命令启动失败:${t.message}`})}),r.on("close",t=>{clearTimeout(s),o({code:t,output:a})})})}function c(t){if(!t||"string"!=typeof t)throw new Error("工作目录未设置:请在项目目录中运行(VS Code / IDEA 中需先「打开文件夹」)")}function p(t,e=1200){const n=String(t||"").trim();return n.length>e?"…"+n.slice(-e):n}export function detectManager(n){try{if(t.existsSync(e.join(n,"pnpm-lock.yaml")))return"pnpm";if(t.existsSync(e.join(n,"yarn.lock")))return"yarn"}catch{}return"npm"}export function validPackageToken(t){return!("string"!=typeof t||!t.trim()||/\s/.test(t)||t.startsWith("-"))&&/^(@[a-z0-9~][\w.-]*\/)?[a-z0-9~][\w.-]*(@[\w.^~=+*:-]+)?$/i.test(t)}export function buildInstallPlan(t,e={},n=process.platform){const o=["npm","pnpm","yarn"].includes(String(e.manager||"").toLowerCase())?String(e.manager).toLowerCase():detectManager(t),r=(Array.isArray(e.packages)?e.packages:[]).map(t=>String(t).trim()).filter(Boolean);for(const t of r)if(!validPackageToken(t))throw new Error(`包名不合法(仅支持 名字[@版本],不接受选项/空白/特殊字符):${t}`);const a=!!e.dev,s=!!e.global;if(s&&!r.length)throw new Error("全局安装(global=true)需要明确的 packages 列表");let i;i=r.length?"yarn"===o?s?["global","add",...r]:["add",...a?["-D"]:[],...r]:"pnpm"===o?["add",...s?["-g"]:[],...a?["-D"]:[],...r]:["install",...s?["-g"]:[],...a?["--save-dev"]:[],...r]:["install"];return{manager:o,bin:o,argv:i,winShell:"win32"===n,display:`${o} ${i.join(" ")}`}}export async function installPackages(t,e){c(t);const n=buildInstallPlan(t,e||{});if(!which(n.bin))return`未找到 ${n.bin}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`+("npm"!==n.manager?',或改用 manager:"npm"':"");const o=Number.isFinite(e?.timeout)?Math.min(e.timeout,6e5):s,r=await runArgv(n.bin,n.argv,{cwd:t,timeoutMs:o,winShell:n.winShell}),a=r.killed?"【超时被终止】":`退出码 ${r.code}`,i=`$ ${n.display}\n${a}`;return 0===r.code?`${i}\n已安装完成。${p(r.output,800)?"\n"+p(r.output,800):""}`:`${i}\n${p(r.output)||"(无输出)"}`}export function buildSetupSteps(n,o={},r={}){const a=r.platform||process.platform,s=r.pathStr,i=Array.isArray(o.commands)?o.commands.map(t=>String(t).trim()).filter(Boolean).slice(0,12):null;if(i&&i.length)return{mode:"commands",commands:i,detected:[]};const c=o=>{try{return t.existsSync(e.join(n,o))}catch{return!1}},p=[];c("package.json")&&p.push("node"),(c("requirements.txt")||c("pyproject.toml"))&&p.push("python"),c("go.mod")&&p.push("go"),c("Cargo.toml")&&p.push("rust"),c("pom.xml")&&p.push("maven"),(c("build.gradle")||c("build.gradle.kts"))&&p.push("gradle");const l=Array.isArray(o.stacks)&&o.stacks.length?p.filter(t=>o.stacks.includes(t)):p,u=[],d=[],m=(t,e)=>which(t,{pathStr:s,platform:a})?null:e;for(const t of l)if("node"===t){const e=detectManager(n),o=m(e,`未找到 ${e}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`);u.push(o?{stack:t,label:"安装 Node.js 依赖",missing:o}:{stack:t,label:`安装 Node.js 依赖(${e} install)`,bin:e,argv:["install"],winShell:!0})}else if("python"===t){const o="win32"===a?"python":"python3",r=m(o,`未找到 ${o}:macOS 可 brew install python,或从 python.org 安装`);if(r){u.push({stack:t,label:"配置 Python 环境",missing:r});continue}c(".venv")?d.push(".venv 已存在,跳过创建"):u.push({stack:t,label:"创建 Python 虚拟环境 .venv",bin:o,argv:["-m","venv",".venv"]});const s="win32"===a?".venv\\Scripts\\pip.exe":e.join(n,".venv/bin/pip");u.push(c("requirements.txt")?{stack:t,label:"安装 Python 依赖(requirements.txt)",bin:s,argv:["install","-r","requirements.txt"]}:{stack:t,label:"安装 Python 依赖(pyproject.toml)",bin:s,argv:["install","-e","."]})}else if("go"===t){const e=m("go","未找到 go:可从 https://go.dev/dl 安装(macOS 可 brew install go)");u.push(e?{stack:t,label:"下载 Go 依赖",missing:e}:{stack:t,label:"下载 Go 依赖(go mod download)",bin:"go",argv:["mod","download"]})}else if("rust"===t){const e=m("cargo","未找到 cargo:可用 rustup 安装(https://rustup.rs)");u.push(e?{stack:t,label:"拉取 Rust 依赖",missing:e}:{stack:t,label:"拉取 Rust 依赖(cargo fetch)",bin:"cargo",argv:["fetch"]})}else if("maven"===t){const e=m("mvn","未找到 mvn:macOS 可 brew install maven");u.push(e?{stack:t,label:"解析 Maven 依赖",missing:e}:{stack:t,label:"解析 Maven 依赖(mvn dependency:resolve)",bin:"mvn",argv:["-q","-DskipTests","dependency:resolve"],winShell:!0})}else"gradle"===t&&d.push("检测到 Gradle 项目:构建脚本可执行任意代码,出于安全未自动执行;可手动运行 ./gradlew build,或让我用 run_bash 执行");return{mode:"auto",detected:p,want:l,steps:u,notes:d}}export async function setupEnvironment(t,e){c(t);const n=e||{},o=buildSetupSteps(t,n),a=Number.isFinite(n.timeout)?Math.min(n.timeout,6e5):6e5;if("commands"===o.mode){const e=["环境配置(按给定命令逐条执行):"];for(const n of o.commands){const o=await r(t,{command:n,timeout:a});if(e.push(`$ ${n}\n${o}`),!/^退出码 0\b/.test(String(o))){e.push("(上一步未成功,中止后续配置命令)");break}}return e.join("\n\n")}if(!o.detected.length)return"未检测到已知技术栈(package.json / requirements.txt / pyproject.toml / go.mod / Cargo.toml / pom.xml / build.gradle)。可改用 commands 参数显式指定要执行的配置命令。";const s=[`检测到技术栈:${o.detected.join("、")}`+(o.want.length!==o.detected.length?`(本次仅处理:${o.want.join("、")||"无"})`:"")];for(const e of o.steps){if(e.missing){s.push(`✘ ${e.label}:${e.missing}`);continue}const n=await runArgv(e.bin,e.argv,{cwd:t,timeoutMs:a,winShell:e.winShell});if(0!==n.code){s.push(`✘ ${e.label} → ${n.killed?"超时被终止":"退出码 "+n.code}${p(n.output)?"\n"+p(n.output):""}`),s.push("(该步失败,停止后续步骤;可修正后重试,或用 commands 显式指定命令)");break}s.push(`✔ ${e.label} → 退出码 0`)}for(const t of o.notes||[])s.push("· "+t);return"环境配置报告:\n"+s.join("\n")}const l=["code","code-insiders","cursor","windsurf"],u=["idea","idea64","webstorm","pycharm","goland","rider","clion"];export function buildPluginPlan(t={},e={}){const n=e.platform||process.platform,o=e.pathStr,r=String(t.id||"").trim();if(!r||r.startsWith("-")||!/^[@\w./:-]+$/.test(r))throw new Error(`插件 id 不合法:${r||"(空)"}`);const a=String(t.target||"").trim().toLowerCase();if("vscode"===a){for(const t of l)if(which(t,{pathStr:o,platform:n}))return{kind:"vscode",bin:t,argv:["--install-extension",r],winShell:!0,note:`经 ${t} CLI 安装`};throw new Error("未找到 VS Code 命令行工具(code)。请在 VS Code 命令面板执行「Shell Command: Install 'code' command in PATH」后重试,或到扩展市场手动安装 "+r)}if("jetbrains"===a){for(const t of u)if(which(t,{pathStr:o,platform:n}))return{kind:"jetbrains",bin:t,argv:["installPlugins",r],note:"安装后需重启 IDE 生效"};throw new Error("未找到 JetBrains IDE 命令行启动器(idea 等)。可在 IDE 的 Tools → Create Command-line Launcher 生成,或到 Settings → Plugins 里搜索安装 "+r)}if("npm"===a){if(!which("npm",{pathStr:o,platform:n}))throw new Error("未找到 npm:请先安装 Node.js(https://nodejs.org)");return{kind:"npm",bin:"npm",argv:["install","-g",r],winShell:!0,note:"全局 CLI 工具"}}throw new Error("target 需为 vscode | jetbrains | npm")}export async function installPlugin(t,e){const n=buildPluginPlan(e||{}),o="jetbrains"===n.kind?18e4:s,r=await runArgv(n.bin,n.argv,{cwd:t||void 0,timeoutMs:o,winShell:n.winShell}),a=`${n.bin} ${n.argv.join(" ")}`;return 0===r.code?`$ ${a}\n退出码 0\n已安装 ${e?.id}${n.note?"("+n.note+")":""}${p(r.output,600)?"\n"+p(r.output,600):""}`:`$ ${a}\n${r.killed?"【超时被终止】":"退出码 "+r.code}\n${p(r.output)||"(无输出)"}`}export async function openBrowser(t,e){const n=String(e?.url||"").trim();if(!/^https?:\/\/\S+$/i.test(n))throw new Error("仅支持 http/https 网址");const o=process.platform,r="darwin"===o?await i("open",[n]):"win32"===o?await i("rundll32",["url.dll,FileProtocolHandler",n]):await i("xdg-open",[n]);return 0!==r.code?`打开浏览器失败(退出码 ${r.code}):${p(r.output)||"未知原因"}`:`已在系统默认浏览器打开:${n}`}export async function openPath(n,o){const r=String(o?.path||"").trim();if(!r)throw new Error("缺少路径 path");const a=e.resolve(n||".",r);if(!t.existsSync(a))throw new Error("路径不存在:"+a);const s=t.statSync(a).isDirectory(),c=null!=o?.reveal?!!o.reveal:!s,l=process.platform;let u;return"darwin"===l?u=await i("open",c&&!s?["-R",a]:[a]):"win32"===l?(u=await i("explorer.exe",c&&!s?["/select,"+a]:[a]),u={code:0,output:u.output}):u=await i("xdg-open",[c&&!s?e.dirname(a):a]),0!==u.code?`打开文件管理器失败(退出码 ${u.code}):${p(u.output)||"未知原因"}`:`已在文件管理器中${c&&!s?"定位":"打开"}:${a}`}export async function openApp(t,e){const o=String(e?.name||"").trim(),r=null!=e?.target?String(e.target).trim():"";if(!o)throw new Error("缺少应用名 name");if(o.length>120||/[\0-\x1f"'`$\\;|&<>{}[\]*?%]/.test(o))throw new Error("应用名含不允许的字符:"+o);if(r&&/[\0-\x1f"'`$\\;|&<>]/.test(r))throw new Error("target 含不允许的字符:"+r);const a=process.platform;if("darwin"===a){const t=await i("open",r?["-a",o,r]:["-a",o]);return 0!==t.code?`启动应用失败(退出码 ${t.code}):${p(t.output)||"未找到应用 "+o}`:`已启动应用:${o}${r?"(打开 "+r+")":""}`}if("win32"===a)return await new Promise(t=>{const e=`start "" "${o}"${r?` "${r}"`:""}`,a=n(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",e],{windowsVerbatimArguments:!0});a.on("error",e=>t(`启动应用失败:${e.message}`)),a.on("close",e=>t(0===e?`已启动应用:${o}`:`启动应用失败(退出码 ${e})`))});if(!/^[\w./+-]+$/.test(o))throw new Error("Linux 下应用名需为可执行文件名(字母数字._/+-):"+o);return await new Promise(t=>{const e=n(o,r?[r]:[],{detached:!0,stdio:"ignore"});e.once("error",e=>t(`启动应用失败:${e.message}`)),e.once("spawn",()=>{e.unref(),t(`已启动应用:${o}${r?"(打开 "+r+")":""}`)})})}const d=(t,e)=>({type:"object",properties:t,required:e||[]}),m=t=>({type:"string",description:t}),g=t=>({type:"boolean",description:t}),h=t=>({type:"integer",description:t});export const AUTOMATION_TOOL_SPECS=[{type:"function",function:{name:"install_packages",description:"安装 Node.js 依赖包(npm/pnpm/yarn 按锁文件自动识别;执行前会征询用户同意)。省略 packages 时安装 package.json 的全部依赖。仅支持包名[@版本],不接受额外命令行选项。",parameters:d({packages:{type:"array",items:{type:"string"},description:'要安装的包(如 ["lodash","@types/node@^20"]);省略=安装全部依赖'},dev:g("可选:装入 devDependencies(默认否)"),global:g("可选:全局安装(需明确 packages;默认否)"),manager:m("可选:npm | pnpm | yarn(默认按锁文件自动识别)"),timeout:h("可选:超时毫秒(默认 300000,上限 600000)")},[])}},{type:"function",function:{name:"setup_environment",description:"自动配置项目运行环境(执行前会征询用户同意):检测技术栈(Node/Python/Go/Rust/Maven)→ 检查工具链是否安装 → 安装依赖(npm install、创建 .venv 并 pip install、go mod download 等)。也可用 commands 显式指定要执行的配置命令(逐条执行,失败即停)。",parameters:d({stacks:{type:"array",items:{type:"string"},description:"可选:只处理这些技术栈(node|python|go|rust|maven)"},commands:{type:"array",items:{type:"string"},description:"可选:显式配置命令列表(优先于自动检测,最多 12 条)"},timeout:h("可选:单步超时毫秒(默认 600000)")},[])}},{type:"function",function:{name:"install_plugin",description:"安装编辑器/IDE 插件(执行前会征询用户同意):VS Code 扩展(code --install-extension)、JetBrains 插件(idea installPlugins,装后需重启 IDE)、或 npm 全局 CLI 工具(npm install -g)。",parameters:d({target:m("vscode | jetbrains | npm"),id:m("插件标识:VS Code 扩展 id(如 dbaeumer.vscode-eslint)/ JetBrains 插件 id / npm 包名")},["target","id"])}},{type:"function",function:{name:"open_browser",description:"用系统默认浏览器打开一个 http/https 网址(执行前会征询用户同意)。适合把文档、本地预览页、报告直接呈现给用户。",parameters:d({url:m("完整网址(http/https)")},["url"])}},{type:"function",function:{name:"open_path",description:"在系统文件管理器(访达/资源管理器)中打开目录或定位文件(执行前会征询用户同意)。",parameters:d({path:m("目录或文件路径(相对项目根或绝对路径)"),reveal:g("可选:定位(选中)该文件而非打开;文件默认定位、目录默认打开")},["path"])}},{type:"function",function:{name:"open_app",description:'启动本机任意应用程序(执行前会征询用户同意),可附带要打开的文件或网址。macOS 用应用名(如 "Safari"、"Visual Studio Code"),Windows 用可启动名,Linux 用可执行文件名。',parameters:d({name:m("应用名/可执行名"),target:m("可选:让该应用打开的文件路径或 URL")},["name"])}}];export const AUTOMATION_EXECUTORS={install_packages:installPackages,setup_environment:setupEnvironment,install_plugin:installPlugin,open_browser:openBrowser,open_path:openPath,open_app:openApp};
1
+ import t from"node:fs";import e from"node:path";import{spawn as n}from"node:child_process";import{StringDecoder as r}from"node:string_decoder";import{runBash as o}from"./executors.js";const a=3e4,s=3e5;export const AUTOMATION_TOOLS=new Set(["install_packages","setup_environment","install_plugin","open_browser","open_path","open_app","auto_execute","computer_screenshot","computer_click","computer_move","computer_drag","computer_scroll","computer_type","computer_key","computer_focus_app","run_native_script","connect_mcp","set_wake_lock","schedule_followup","create_automation","update_automation","pause_automation","resume_automation","delete_automation"]);export function automationActionLabel(t,e={}){switch(t){case"install_packages":return`安装 Node.js 包:${Array.isArray(e.packages)&&e.packages.length?e.packages.join(" "):"package.json 全部依赖"}${e.global?"(全局)":""}${e.dev?"(devDependencies)":""}`;case"setup_environment":return Array.isArray(e.commands)&&e.commands.length?`自动配置运行环境:执行 ${e.commands.length} 条配置命令`:"自动配置运行环境:检测项目技术栈并安装依赖";case"install_plugin":return`安装${"jetbrains"===e.target?"JetBrains 插件":"npm"===e.target?"npm 全局工具":"VS Code 扩展"}:${e.id??""}`;case"open_browser":return`打开浏览器访问:${e.url??""}`;case"open_path":return`在文件管理器中打开:${e.path??""}`;case"open_app":return`启动应用:${e.name??""}${e.target?"("+e.target+")":""}`;case"auto_execute":{const t=String(e.goal||"").replace(/\s+/g," ");return"自动执行已规划的任务"+(t?":"+(t.length>60?t.slice(0,60)+"…":t):"")}case"computer_screenshot":return"对屏幕截图"+(e.app?`(先切到 ${e.app})`:"(整屏)");case"computer_click":return`在屏幕 (${e.x??"?"}, ${e.y??"?"}) 处${Number(e.count)>=2?"双击":"点击"}`+(e.app?`(${e.app})`:"");case"computer_move":return`移动鼠标到 (${e.x??"?"}, ${e.y??"?"})`;case"computer_drag":return`拖拽 (${e.from_x??"?"}, ${e.from_y??"?"}) → (${e.to_x??"?"}, ${e.to_y??"?"})`;case"computer_scroll":return`滚动屏幕内容(${e.direction||"down"} × ${e.amount??3})`;case"computer_type":{const t=String(e.text||"").replace(/\s+/g," ");return"向当前窗口键入文本:"+(t.length>50?t.slice(0,50)+"…":t)}case"computer_key":return"按下按键:"+[...Array.isArray(e.modifiers)?e.modifiers:[],e.key].filter(Boolean).join("+");case"computer_focus_app":return`把应用切到前台:${e.name??""}`;case"run_native_script":return"执行系统原生自动化脚本"+(e.app?`(操作 ${e.app})`:"");case"connect_mcp":return`连接 MCP 服务:${e.name??""}${e.argument?`(${e.argument})`:""}`;case"set_wake_lock":return!1===e.on?"释放防休眠锁":"获取防休眠锁(任务期间电脑不休眠)";case"schedule_followup":return`安排后续自动化任务(${e.when??""}):${String(e.task||"").slice(0,40)}`;case"create_automation":return`新建自动化任务「${e.name||String(e.task||"").slice(0,20)}」(${e.when??""})`;case"update_automation":return`修改自动化任务:${e.id||e.match||""}`;case"pause_automation":return`暂停自动化任务:${e.id||e.match||""}`;case"resume_automation":return`启用自动化任务:${e.id||e.match||""}`;case"delete_automation":return`删除自动化任务:${e.id||e.match||""}(不可恢复)`;default:return t}}export function automationDetail(t,e={}){return"setup_environment"===t&&Array.isArray(e.commands)&&e.commands.length?e.commands.slice(0,12).map(t=>"$ "+String(t)).join("\n"):"open_browser"===t?String(e.url??""):"open_path"===t?String(e.path??""):"open_app"===t?String(e.target??""):"run_native_script"===t?String(e.script??"").split("\n").slice(0,12).join("\n"):"computer_type"===t?String(e.text??"").split("\n").slice(0,8).join("\n"):"schedule_followup"===t||"create_automation"===t?String(e.task??""):"update_automation"===t?[e.when?"新频率:"+e.when:"",e.task?"新提示词:"+String(e.task).slice(0,200):"",e.permission?"新权限:"+e.permission:""].filter(Boolean).join("\n"):""}export function which(n,{pathStr:r=process.env.PATH||"",platform:o=process.platform}={}){const a="win32"===o?";":":",s="win32"===o?[".cmd",".exe",".bat",""]:[""];for(const o of r.split(a))if(o)for(const r of s){const a=e.join(o,n+r);try{if(t.accessSync(a,t.constants.X_OK),t.statSync(a).isFile())return a}catch{}}return null}export function runArgv(t,e,{cwd:o,timeoutMs:s=3e5,winShell:i=!1}={}){const c="win32"===process.platform;return new Promise(p=>{let l,u="",m=!1;try{l=n(t,e,c?{cwd:o,shell:!!i}:{cwd:o,detached:!0})}catch(t){return void p({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}const d=setTimeout(()=>{m=!0,(()=>{try{!c&&l.pid?process.kill(-l.pid,"SIGKILL"):l.kill("SIGKILL")}catch{try{l.kill("SIGKILL")}catch{}}})()},s),g=new r("utf8"),h=new r("utf8"),f=t=>{u.length<a&&(u+=t)};l.stdout?.on("data",t=>f(g.write(t))),l.stderr?.on("data",t=>f(h.write(t))),l.on("error",t=>{clearTimeout(d),p({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}),l.on("close",t=>{clearTimeout(d),f(g.end()),f(h.end()),p({code:t,killed:m,output:u.length>a?u.slice(0,a)+"\n…(输出已截断)":u})})})}function i(t,e){return new Promise(r=>{let o,a="";try{o=n(t,e,{stdio:["ignore","pipe","pipe"]})}catch(t){return void r({code:-1,output:`命令启动失败:${t.message}`})}const s=setTimeout(()=>{try{o.unref()}catch{}r({code:0,output:a,lingering:!0})},8e3);o.stdout.on("data",t=>a+=t),o.stderr.on("data",t=>a+=t),o.on("error",t=>{clearTimeout(s),r({code:-1,output:`命令启动失败:${t.message}`})}),o.on("close",t=>{clearTimeout(s),r({code:t,output:a})})})}function c(t){if(!t||"string"!=typeof t)throw new Error("工作目录未设置:请在项目目录中运行(VS Code / IDEA 中需先「打开文件夹」)")}function p(t,e=1200){const n=String(t||"").trim();return n.length>e?"…"+n.slice(-e):n}export function detectManager(n){try{if(t.existsSync(e.join(n,"pnpm-lock.yaml")))return"pnpm";if(t.existsSync(e.join(n,"yarn.lock")))return"yarn"}catch{}return"npm"}export function validPackageToken(t){return!("string"!=typeof t||!t.trim()||/\s/.test(t)||t.startsWith("-"))&&/^(@[a-z0-9~][\w.-]*\/)?[a-z0-9~][\w.-]*(@[\w.^~=+*:-]+)?$/i.test(t)}export function buildInstallPlan(t,e={},n=process.platform){const r=["npm","pnpm","yarn"].includes(String(e.manager||"").toLowerCase())?String(e.manager).toLowerCase():detectManager(t),o=(Array.isArray(e.packages)?e.packages:[]).map(t=>String(t).trim()).filter(Boolean);for(const t of o)if(!validPackageToken(t))throw new Error(`包名不合法(仅支持 名字[@版本],不接受选项/空白/特殊字符):${t}`);const a=!!e.dev,s=!!e.global;if(s&&!o.length)throw new Error("全局安装(global=true)需要明确的 packages 列表");let i;i=o.length?"yarn"===r?s?["global","add",...o]:["add",...a?["-D"]:[],...o]:"pnpm"===r?["add",...s?["-g"]:[],...a?["-D"]:[],...o]:["install",...s?["-g"]:[],...a?["--save-dev"]:[],...o]:["install"];return{manager:r,bin:r,argv:i,winShell:"win32"===n,display:`${r} ${i.join(" ")}`}}export async function installPackages(t,e){c(t);const n=buildInstallPlan(t,e||{});if(!which(n.bin))return`未找到 ${n.bin}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`+("npm"!==n.manager?',或改用 manager:"npm"':"");const r=Number.isFinite(e?.timeout)?Math.min(e.timeout,6e5):s,o=await runArgv(n.bin,n.argv,{cwd:t,timeoutMs:r,winShell:n.winShell}),a=o.killed?"【超时被终止】":`退出码 ${o.code}`,i=`$ ${n.display}\n${a}`;return 0===o.code?`${i}\n已安装完成。${p(o.output,800)?"\n"+p(o.output,800):""}`:`${i}\n${p(o.output)||"(无输出)"}`}export function buildSetupSteps(n,r={},o={}){const a=o.platform||process.platform,s=o.pathStr,i=Array.isArray(r.commands)?r.commands.map(t=>String(t).trim()).filter(Boolean).slice(0,12):null;if(i&&i.length)return{mode:"commands",commands:i,detected:[]};const c=r=>{try{return t.existsSync(e.join(n,r))}catch{return!1}},p=[];c("package.json")&&p.push("node"),(c("requirements.txt")||c("pyproject.toml"))&&p.push("python"),c("go.mod")&&p.push("go"),c("Cargo.toml")&&p.push("rust"),c("pom.xml")&&p.push("maven"),(c("build.gradle")||c("build.gradle.kts"))&&p.push("gradle");const l=Array.isArray(r.stacks)&&r.stacks.length?p.filter(t=>r.stacks.includes(t)):p,u=[],m=[],d=(t,e)=>which(t,{pathStr:s,platform:a})?null:e;for(const t of l)if("node"===t){const e=detectManager(n),r=d(e,`未找到 ${e}:请先安装 Node.js(https://nodejs.org,macOS 可 brew install node)`);u.push(r?{stack:t,label:"安装 Node.js 依赖",missing:r}:{stack:t,label:`安装 Node.js 依赖(${e} install)`,bin:e,argv:["install"],winShell:!0})}else if("python"===t){const r="win32"===a?"python":"python3",o=d(r,`未找到 ${r}:macOS 可 brew install python,或从 python.org 安装`);if(o){u.push({stack:t,label:"配置 Python 环境",missing:o});continue}c(".venv")?m.push(".venv 已存在,跳过创建"):u.push({stack:t,label:"创建 Python 虚拟环境 .venv",bin:r,argv:["-m","venv",".venv"]});const s="win32"===a?".venv\\Scripts\\pip.exe":e.join(n,".venv/bin/pip");u.push(c("requirements.txt")?{stack:t,label:"安装 Python 依赖(requirements.txt)",bin:s,argv:["install","-r","requirements.txt"]}:{stack:t,label:"安装 Python 依赖(pyproject.toml)",bin:s,argv:["install","-e","."]})}else if("go"===t){const e=d("go","未找到 go:可从 https://go.dev/dl 安装(macOS 可 brew install go)");u.push(e?{stack:t,label:"下载 Go 依赖",missing:e}:{stack:t,label:"下载 Go 依赖(go mod download)",bin:"go",argv:["mod","download"]})}else if("rust"===t){const e=d("cargo","未找到 cargo:可用 rustup 安装(https://rustup.rs)");u.push(e?{stack:t,label:"拉取 Rust 依赖",missing:e}:{stack:t,label:"拉取 Rust 依赖(cargo fetch)",bin:"cargo",argv:["fetch"]})}else if("maven"===t){const e=d("mvn","未找到 mvn:macOS 可 brew install maven");u.push(e?{stack:t,label:"解析 Maven 依赖",missing:e}:{stack:t,label:"解析 Maven 依赖(mvn dependency:resolve)",bin:"mvn",argv:["-q","-DskipTests","dependency:resolve"],winShell:!0})}else"gradle"===t&&m.push("检测到 Gradle 项目:构建脚本可执行任意代码,出于安全未自动执行;可手动运行 ./gradlew build,或让我用 run_bash 执行");return{mode:"auto",detected:p,want:l,steps:u,notes:m}}export async function setupEnvironment(t,e){c(t);const n=e||{},r=buildSetupSteps(t,n),a=Number.isFinite(n.timeout)?Math.min(n.timeout,6e5):6e5;if("commands"===r.mode){const e=["环境配置(按给定命令逐条执行):"];for(const n of r.commands){const r=await o(t,{command:n,timeout:a});if(e.push(`$ ${n}\n${r}`),!/^退出码 0\b/.test(String(r))){e.push("(上一步未成功,中止后续配置命令)");break}}return e.join("\n\n")}if(!r.detected.length)return"未检测到已知技术栈(package.json / requirements.txt / pyproject.toml / go.mod / Cargo.toml / pom.xml / build.gradle)。可改用 commands 参数显式指定要执行的配置命令。";const s=[`检测到技术栈:${r.detected.join("、")}`+(r.want.length!==r.detected.length?`(本次仅处理:${r.want.join("、")||"无"})`:"")];for(const e of r.steps){if(e.missing){s.push(`✘ ${e.label}:${e.missing}`);continue}const n=await runArgv(e.bin,e.argv,{cwd:t,timeoutMs:a,winShell:e.winShell});if(0!==n.code){s.push(`✘ ${e.label} → ${n.killed?"超时被终止":"退出码 "+n.code}${p(n.output)?"\n"+p(n.output):""}`),s.push("(该步失败,停止后续步骤;可修正后重试,或用 commands 显式指定命令)");break}s.push(`✔ ${e.label} → 退出码 0`)}for(const t of r.notes||[])s.push("· "+t);return"环境配置报告:\n"+s.join("\n")}const l=["code","code-insiders","cursor","windsurf"],u=["idea","idea64","webstorm","pycharm","goland","rider","clion"];export function buildPluginPlan(t={},e={}){const n=e.platform||process.platform,r=e.pathStr,o=String(t.id||"").trim();if(!o||o.startsWith("-")||!/^[@\w./:-]+$/.test(o))throw new Error(`插件 id 不合法:${o||"(空)"}`);const a=String(t.target||"").trim().toLowerCase();if("vscode"===a){for(const t of l)if(which(t,{pathStr:r,platform:n}))return{kind:"vscode",bin:t,argv:["--install-extension",o],winShell:!0,note:`经 ${t} CLI 安装`};throw new Error("未找到 VS Code 命令行工具(code)。请在 VS Code 命令面板执行「Shell Command: Install 'code' command in PATH」后重试,或到扩展市场手动安装 "+o)}if("jetbrains"===a){for(const t of u)if(which(t,{pathStr:r,platform:n}))return{kind:"jetbrains",bin:t,argv:["installPlugins",o],note:"安装后需重启 IDE 生效"};throw new Error("未找到 JetBrains IDE 命令行启动器(idea 等)。可在 IDE 的 Tools → Create Command-line Launcher 生成,或到 Settings → Plugins 里搜索安装 "+o)}if("npm"===a){if(!which("npm",{pathStr:r,platform:n}))throw new Error("未找到 npm:请先安装 Node.js(https://nodejs.org)");return{kind:"npm",bin:"npm",argv:["install","-g",o],winShell:!0,note:"全局 CLI 工具"}}throw new Error("target 需为 vscode | jetbrains | npm")}export async function installPlugin(t,e){const n=buildPluginPlan(e||{}),r="jetbrains"===n.kind?18e4:s,o=await runArgv(n.bin,n.argv,{cwd:t||void 0,timeoutMs:r,winShell:n.winShell}),a=`${n.bin} ${n.argv.join(" ")}`;return 0===o.code?`$ ${a}\n退出码 0\n已安装 ${e?.id}${n.note?"("+n.note+")":""}${p(o.output,600)?"\n"+p(o.output,600):""}`:`$ ${a}\n${o.killed?"【超时被终止】":"退出码 "+o.code}\n${p(o.output)||"(无输出)"}`}export async function openBrowser(t,e){const n=String(e?.url||"").trim();if(!/^https?:\/\/\S+$/i.test(n))throw new Error("仅支持 http/https 网址");const r=process.platform,o="darwin"===r?await i("open",[n]):"win32"===r?await i("rundll32",["url.dll,FileProtocolHandler",n]):await i("xdg-open",[n]);return 0!==o.code?`打开浏览器失败(退出码 ${o.code}):${p(o.output)||"未知原因"}`:`已在系统默认浏览器打开:${n}`}export async function openPath(n,r){const o=String(r?.path||"").trim();if(!o)throw new Error("缺少路径 path");const a=e.resolve(n||".",o);if(!t.existsSync(a))throw new Error("路径不存在:"+a);const s=t.statSync(a).isDirectory(),c=null!=r?.reveal?!!r.reveal:!s,l=process.platform;let u;return"darwin"===l?u=await i("open",c&&!s?["-R",a]:[a]):"win32"===l?(u=await i("explorer.exe",c&&!s?["/select,"+a]:[a]),u={code:0,output:u.output}):u=await i("xdg-open",[c&&!s?e.dirname(a):a]),0!==u.code?`打开文件管理器失败(退出码 ${u.code}):${p(u.output)||"未知原因"}`:`已在文件管理器中${c&&!s?"定位":"打开"}:${a}`}export async function openApp(t,e){const r=String(e?.name||"").trim(),o=null!=e?.target?String(e.target).trim():"";if(!r)throw new Error("缺少应用名 name");if(r.length>120||/[\0-\x1f"'`$\\;|&<>{}[\]*?%]/.test(r))throw new Error("应用名含不允许的字符:"+r);if(o&&/[\0-\x1f"'`$\\;|&<>]/.test(o))throw new Error("target 含不允许的字符:"+o);const a=process.platform;if("darwin"===a){const t=await i("open",o?["-a",r,o]:["-a",r]);return 0!==t.code?`启动应用失败(退出码 ${t.code}):${p(t.output)||"未找到应用 "+r}`:`已启动应用:${r}${o?"(打开 "+o+")":""}`}if("win32"===a)return await new Promise(t=>{const e=`start "" "${r}"${o?` "${o}"`:""}`,a=n(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",e],{windowsVerbatimArguments:!0});a.on("error",e=>t(`启动应用失败:${e.message}`)),a.on("close",e=>t(0===e?`已启动应用:${r}`:`启动应用失败(退出码 ${e})`))});if(!/^[\w./+-]+$/.test(r))throw new Error("Linux 下应用名需为可执行文件名(字母数字._/+-):"+r);return await new Promise(t=>{const e=n(r,o?[o]:[],{detached:!0,stdio:"ignore"});e.once("error",e=>t(`启动应用失败:${e.message}`)),e.once("spawn",()=>{e.unref(),t(`已启动应用:${r}${o?"(打开 "+o+")":""}`)})})}const m=(t,e)=>({type:"object",properties:t,required:e||[]}),d=t=>({type:"string",description:t}),g=t=>({type:"boolean",description:t}),h=t=>({type:"integer",description:t});export const AUTOMATION_TOOL_SPECS=[{type:"function",function:{name:"install_packages",description:"安装 Node.js 依赖包(npm/pnpm/yarn 按锁文件自动识别;执行前会征询用户同意)。省略 packages 时安装 package.json 的全部依赖。仅支持包名[@版本],不接受额外命令行选项。",parameters:m({packages:{type:"array",items:{type:"string"},description:'要安装的包(如 ["lodash","@types/node@^20"]);省略=安装全部依赖'},dev:g("可选:装入 devDependencies(默认否)"),global:g("可选:全局安装(需明确 packages;默认否)"),manager:d("可选:npm | pnpm | yarn(默认按锁文件自动识别)"),timeout:h("可选:超时毫秒(默认 300000,上限 600000)")},[])}},{type:"function",function:{name:"setup_environment",description:"自动配置项目运行环境(执行前会征询用户同意):检测技术栈(Node/Python/Go/Rust/Maven)→ 检查工具链是否安装 → 安装依赖(npm install、创建 .venv 并 pip install、go mod download 等)。也可用 commands 显式指定要执行的配置命令(逐条执行,失败即停)。",parameters:m({stacks:{type:"array",items:{type:"string"},description:"可选:只处理这些技术栈(node|python|go|rust|maven)"},commands:{type:"array",items:{type:"string"},description:"可选:显式配置命令列表(优先于自动检测,最多 12 条)"},timeout:h("可选:单步超时毫秒(默认 600000)")},[])}},{type:"function",function:{name:"install_plugin",description:"安装编辑器/IDE 插件(执行前会征询用户同意):VS Code 扩展(code --install-extension)、JetBrains 插件(idea installPlugins,装后需重启 IDE)、或 npm 全局 CLI 工具(npm install -g)。",parameters:m({target:d("vscode | jetbrains | npm"),id:d("插件标识:VS Code 扩展 id(如 dbaeumer.vscode-eslint)/ JetBrains 插件 id / npm 包名")},["target","id"])}},{type:"function",function:{name:"open_browser",description:"用系统默认浏览器打开一个 http/https 网址(执行前会征询用户同意)。适合把文档、本地预览页、报告直接呈现给用户。",parameters:m({url:d("完整网址(http/https)")},["url"])}},{type:"function",function:{name:"open_path",description:"在系统文件管理器(访达/资源管理器)中打开目录或定位文件(执行前会征询用户同意)。",parameters:m({path:d("目录或文件路径(相对项目根或绝对路径)"),reveal:g("可选:定位(选中)该文件而非打开;文件默认定位、目录默认打开")},["path"])}},{type:"function",function:{name:"open_app",description:'启动本机任意应用程序(执行前会征询用户同意),可附带要打开的文件或网址。macOS 用应用名(如 "Safari"、"Visual Studio Code"),Windows 用可启动名,Linux 用可执行文件名。',parameters:m({name:d("应用名/可执行名"),target:d("可选:让该应用打开的文件路径或 URL")},["name"])}}];export const AUTOMATION_EXECUTORS={install_packages:installPackages,setup_environment:setupEnvironment,install_plugin:installPlugin,open_browser:openBrowser,open_path:openPath,open_app:openApp};
@@ -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 r from"node:path";import o from"node:os";export const CONFIG_DIR=r.join(o.homedir(),".huicai");export const CONFIG_PATH=r.join(CONFIG_DIR,"config.json");export const DEFAULT_BASE_URL="https://huicai-ai.sjht.com";export const DEFAULT_MODEL="deepseek-v4-flash";function n(){try{return JSON.parse(e.readFileSync(CONFIG_PATH,"utf8"))}catch{return{}}}export function loadConfig(){const e=n(),r={baseUrl:(process.env.HUICAI_BASE_URL||e.baseUrl||DEFAULT_BASE_URL).trim(),apiKey:(process.env.HUICAI_API_KEY||e.apiKey||"").trim(),model:(process.env.HUICAI_MODEL||e.model||DEFAULT_MODEL).trim(),maxTurns:Number(process.env.HUICAI_MAX_TURNS||e.maxTurns)||0,maxTokens:Number(process.env.HUICAI_MAX_TOKENS||e.maxTokens)||0,autoCompactTokens:Number(process.env.HUICAI_AUTOCOMPACT_TOKENS||e.autoCompactTokens)||0,toolConcurrency:Number(process.env.HUICAI_TOOL_CONCURRENCY||e.toolConcurrency)||0,subagentConcurrency:Number(process.env.HUICAI_SUBAGENT_CONCURRENCY||e.subagentConcurrency)||0,reasoningEffort:(process.env.HUICAI_EFFORT||e.reasoningEffort||"").trim(),compactModel:(process.env.HUICAI_COMPACT_MODEL||e.compactModel||"").trim(),memoryEnabled:"0"!==process.env.HUICAI_MEMORY&&!1!==e.memory,showReasoning:"0"!==process.env.HUICAI_REASONING&&!1!==e.showReasoning,markdown:"0"!==process.env.HUICAI_MARKDOWN&&!1!==e.markdown,client:(process.env.HUICAI_CLIENT||"cli").trim().toLowerCase(),theme:(process.env.HUICAI_THEME||e.theme||"default").trim(),accentColor:(e.accentColor||"cyan").trim(),tui:(process.env.HUICAI_TUI||e.tui||"auto").trim(),debug:"1"===process.env.HUICAI_DEBUG||!0===e.debug,statusLine:(e.statusLine||"").trim(),advisorModel:(process.env.HUICAI_ADVISOR||e.advisorModel||"").trim(),autoModelStrong:(process.env.HUICAI_AUTOMODEL||e.autoModelStrong||"").trim(),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};return r.baseUrl=r.baseUrl.replace(/\/+$/,""),r}export function saveConfig(r){const o={...n(),...r};e.mkdirSync(CONFIG_DIR,{recursive:!0}),e.writeFileSync(CONFIG_PATH,JSON.stringify(o,null,2)+"\n",{mode:384});try{e.chmodSync(CONFIG_PATH,384)}catch{}return o}
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.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 a=["- 先了解现状再动手:并行使用 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(c,l={}){const p=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}}(c),m=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}(c),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 f="";if(l.persona&&l.persona.trim()?(f+="# 你的角色设定\n"+l.persona.trim()+"\n\n",f+="# 运行方式\n",f+="你现在运行在用户终端里,可通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令来真正完成任务。请在保持上述角色设定的前提下遵循以下准则:\n"):(f+="你是 huicai,一个运行在用户终端里的专业自治编码与自动化智能体。\n",f+="你能通过工具在用户本机的项目里读写文件、搜索、执行 shell 命令,从而真正完成任务,而不仅是给建议。\n\n",f+="# 工作准则\n"),f+=a.join("\n")+"\n\n",f+="# 运行环境\n",f+=`- 工作目录: ${c}\n`,f+=`- 操作系统: ${t.platform()} ${t.release()} (${t.arch()})\n`,f+=`- 日期: ${(new Date).toISOString().slice(0,10)}\n`,p&&(f+=`- Git 分支: ${p.branch}\n`,p.status)){f+=`- 未提交改动:\n${p.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""}}(c);_&&(f+=`- 顶层条目: ${_}\n`);const u=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(u&&(f+="\n# 编辑器上下文(来自 IDE 集成)\n"+u),d&&(f+=`\n# 全局用户说明(~/.huicai/HUICAI.md)\n${d}\n`),m&&(f+=`\n# 项目说明(${m.name})\n${m.content}\n`),!1!==l.repoMap)try{const n=s(c);n&&(f+=`\n# 项目地图(关键文件与主要符号;用 search_code 按概念检索相关实现)\n${n}\n`)}catch{}const h=o(c);if(h&&(f+=`\n# 可用技能(本地 Agent Skills)\n${h}\n`),!1!==l.memory){const n=i(c);n&&(f+=`\n# 长期记忆(此前会话经 memory_write 记录,跨会话生效)\n${n}\n`,f+="(发现新的、值得跨会话记住的稳定事实/偏好/结论时,用 memory_write 记录。)\n")}return l.append&&String(l.append).trim()&&(f+="\n# 附加指示\n"+String(l.append).trim()+"\n"),f}
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:s=o=>process.stdout.write(o+"\n"),once:p=!1,spawnImpl:d}={}){const a=c();if(!a&&!d)throw new Error("无法定位 huicai 可执行文件(daemon 需在 CLI 环境运行)");e.mkdirSync(DAEMON_LOG_DIR,{recursive:!0});const m=d||(n=>{const t=e.openSync(r.join(DAEMON_LOG_DIR,n.id+".log"),"a"),i=o(process.execPath,[a,"-p",n.prompt,"--yolo","--plain"],{cwd:n.cwd,detached:!0,stdio:["ignore",t,t],env:process.env});return i.unref(),i.pid}),f=new Map,l=()=>{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(f.get(n.id)===e)continue;let t=!1;try{t=i(n.cron,o)}catch{t=!1}if(!t)continue;f.set(n.id,e);const c=m(n);r++,s(`[${e}] 触发 #${n.id}(pid ${c}):${String(n.prompt).replace(/\s+/g," ").slice(0,60)}`)}return r},u=l();if(p)return{fired:u};s(`huicai 调度守护已启动:每 ${Math.round(n/1e3)}s 检查 ~/.huicai/schedule.json(${t().length} 个任务)· Ctrl-C 退出`);const h=setInterval(l,n);return await new Promise(o=>{process.on("SIGINT",()=>{clearInterval(h),s("\n调度守护已停止。"),o()})}),{fired:u}}
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/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)}
package/src/mcp.js CHANGED
@@ -1 +1 @@
1
- import t from"node:fs";import e from"node:path";import{spawn as i}from"node:child_process";import{CONFIG_DIR as s}from"./config.js";import{c as n}from"./colors.js";export const MCP_CONFIG_PATH=e.join(s,"mcp.json");export class McpStdioClient{constructor(t,e){this.name=t,this.spec=e,this.child=null,this.buffer="",this.nextId=1,this.pending=new Map,this.closed=!1}async start(){const t={...process.env,...this.spec.env||{}};this.child=i(this.spec.command,this.spec.args||[],{env:t,stdio:["pipe","pipe","pipe"]}),this.child.on("error",t=>this._failAll(new Error(`MCP[${this.name}] 进程错误:${t.message}`))),this.child.on("exit",()=>{this.closed=!0,this._failAll(new Error(`MCP[${this.name}] 进程已退出`))}),this.child.stdout.setEncoding("utf8"),this.child.stdout.on("data",t=>this._onData(t)),this.child.stderr.on("data",()=>{}),await this._request("initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"huicai-cli",version:"0.2.0"}},2e4),this._notify("notifications/initialized",{})}async listTools(){const t=await this._request("tools/list",{},2e4);return Array.isArray(t?.tools)?t.tools:[]}async callTool(t,e){const i=await this._request("tools/call",{name:t,arguments:e||{}},12e4),s=(Array.isArray(i?.content)?i.content:[]).map(t=>"text"===t?.type?t.text:t?.text||JSON.stringify(t)).filter(Boolean).join("\n");return i?.isError?`工具报错:${s||"(无详情)"}`:s||"(无输出)"}close(){this.closed=!0;try{this.child?.kill("SIGTERM")}catch{}}_onData(t){let e;for(this.buffer+=t;(e=this.buffer.indexOf("\n"))>=0;){const t=this.buffer.slice(0,e).trim();if(this.buffer=this.buffer.slice(e+1),!t)continue;let i;try{i=JSON.parse(t)}catch{continue}if(null!=i.id&&this.pending.has(i.id)){const{resolve:t,reject:e,timer:s}=this.pending.get(i.id);clearTimeout(s),this.pending.delete(i.id),i.error?e(new Error(i.error.message||"MCP 错误")):t(i.result)}}}_request(t,e,i){if(this.closed)return Promise.reject(new Error(`MCP[${this.name}] 已关闭`));const s=this.nextId++,n=JSON.stringify({jsonrpc:"2.0",id:s,method:t,params:e})+"\n";return new Promise((e,o)=>{const r=setTimeout(()=>{this.pending.delete(s),o(new Error(`MCP[${this.name}] ${t} 超时`))},i);this.pending.set(s,{resolve:e,reject:o,timer:r});try{this.child.stdin.write(n)}catch(t){clearTimeout(r),this.pending.delete(s),o(t)}})}_notify(t,e){try{this.child.stdin.write(JSON.stringify({jsonrpc:"2.0",method:t,params:e})+"\n")}catch{}}_failAll(t){for(const{reject:e,timer:i}of this.pending.values())clearTimeout(i),e(t);this.pending.clear()}}function o(t){const e=String(t).replace(/[^a-zA-Z0-9_-]/g,"_");return e.length>60?e.slice(0,60):e}export async function loadMcpTools(){const e=function(){try{const e=JSON.parse(t.readFileSync(MCP_CONFIG_PATH,"utf8"));return e?.mcpServers&&"object"==typeof e.mcpServers?e.mcpServers:{}}catch{return{}}}(),i=Object.keys(e),s=[],r=[];if(0===i.length)return{tools:s,clients:r};for(const t of i){const i=e[t]||{};if(!i.command)continue;const c=new McpStdioClient(t,i);try{await c.start();const e=await c.listTools();r.push(c);for(const i of e){const e=i.name,n=o(`mcp__${t}__${e}`),r=!0===i.annotations?.readOnlyHint;s.push({readOnly:r,spec:{type:"function",function:{name:n,description:`[MCP:${t}] ${i.description||e}`,parameters:i.inputSchema&&"object"==typeof i.inputSchema?i.inputSchema:{type:"object",properties:{}}}},run:t=>c.callTool(e,t)})}process.stdout.write(n.gray(` · MCP[${t}] 已连接,工具 ${e.length} 个`)+"\n")}catch(e){c.close(),process.stdout.write(n.yellow(` · MCP[${t}] 加载失败,已跳过:${e.message}`)+"\n")}}return{tools:s,clients:r}}export function closeMcp(t){for(const e of t||[])e.close()}
1
+ import t from"node:fs";import e from"node:path";import{spawn as n}from"node:child_process";import{CONFIG_DIR as r}from"./config.js";import{c as o}from"./colors.js";export const MCP_CONFIG_PATH=e.join(r,"mcp.json");export function projectMcpConfigPath(t){return t?e.join(t,".huicai","mcp.json"):null}export const BUILTIN_MCP_CATALOG={filesystem:{description:"读写指定目录下的文件(沙箱化的文件系统访问)",command:"npx",args:["-y","@modelcontextprotocol/server-filesystem"],needsArg:"要开放的目录绝对路径",defaultsToCwd:!0},fetch:{description:"抓取网页并转成适合模型阅读的文本",command:"npx",args:["-y","@modelcontextprotocol/server-fetch"]},git:{description:"读取 Git 仓库历史、diff、分支信息",command:"npx",args:["-y","@modelcontextprotocol/server-git","--repository"],needsArg:"仓库目录",defaultsToCwd:!0},memory:{description:"基于知识图谱的长期记忆存取",command:"npx",args:["-y","@modelcontextprotocol/server-memory"]},sequentialthinking:{description:"结构化的分步推理辅助",command:"npx",args:["-y","@modelcontextprotocol/server-sequential-thinking"]},sqlite:{description:"查询/操作本地 SQLite 数据库",command:"npx",args:["-y","@modelcontextprotocol/server-sqlite","--db-path"],needsArg:"数据库文件路径"}};export class McpStdioClient{constructor(t,e){this.name=t,this.spec=e,this.child=null,this.buffer="",this.nextId=1,this.pending=new Map,this.closed=!1}async start(){const t={...process.env,...this.spec.env||{}};this.child=n(this.spec.command,this.spec.args||[],{env:t,stdio:["pipe","pipe","pipe"]}),this.child.on("error",t=>this._failAll(new Error(`MCP[${this.name}] 进程错误:${t.message}`))),this.child.on("exit",()=>{this.closed=!0,this._failAll(new Error(`MCP[${this.name}] 进程已退出`))}),this.child.stdout.setEncoding("utf8"),this.child.stdout.on("data",t=>this._onData(t)),this.child.stderr.on("data",()=>{}),await this._request("initialize",{protocolVersion:"2024-11-05",capabilities:{},clientInfo:{name:"huicai-cli",version:"0.2.0"}},2e4),this._notify("notifications/initialized",{})}async listTools(){const t=await this._request("tools/list",{},2e4);return Array.isArray(t?.tools)?t.tools:[]}async callTool(t,e){const n=await this._request("tools/call",{name:t,arguments:e||{}},12e4),r=(Array.isArray(n?.content)?n.content:[]).map(t=>"text"===t?.type?t.text:t?.text||JSON.stringify(t)).filter(Boolean).join("\n");return n?.isError?`工具报错:${r||"(无详情)"}`:r||"(无输出)"}close(){this.closed=!0;try{this.child?.kill("SIGTERM")}catch{}}_onData(t){let e;for(this.buffer+=t;(e=this.buffer.indexOf("\n"))>=0;){const t=this.buffer.slice(0,e).trim();if(this.buffer=this.buffer.slice(e+1),!t)continue;let n;try{n=JSON.parse(t)}catch{continue}if(null!=n.id&&this.pending.has(n.id)){const{resolve:t,reject:e,timer:r}=this.pending.get(n.id);clearTimeout(r),this.pending.delete(n.id),n.error?e(new Error(n.error.message||"MCP 错误")):t(n.result)}}}_request(t,e,n){if(this.closed)return Promise.reject(new Error(`MCP[${this.name}] 已关闭`));const r=this.nextId++,o=JSON.stringify({jsonrpc:"2.0",id:r,method:t,params:e})+"\n";return new Promise((e,s)=>{const i=setTimeout(()=>{this.pending.delete(r),s(new Error(`MCP[${this.name}] ${t} 超时`))},n);this.pending.set(r,{resolve:e,reject:s,timer:i});try{this.child.stdin.write(o)}catch(t){clearTimeout(i),this.pending.delete(r),s(t)}})}_notify(t,e){try{this.child.stdin.write(JSON.stringify({jsonrpc:"2.0",method:t,params:e})+"\n")}catch{}}_failAll(t){for(const{reject:e,timer:n}of this.pending.values())clearTimeout(n),e(t);this.pending.clear()}}function s(t){const e=String(t).replace(/[^a-zA-Z0-9_-]/g,"_");return e.length>60?e.slice(0,60):e}function i(e){try{const n=JSON.parse(t.readFileSync(e,"utf8"));return n?.mcpServers&&"object"==typeof n.mcpServers?n.mcpServers:{}}catch{return{}}}export function readConfig(t){const e={};for(const[n,r]of[[MCP_CONFIG_PATH,"global"],[projectMcpConfigPath(t),"project"]])if(n)for(const[t,o]of Object.entries(i(n)))o&&"object"==typeof o&&(e[t]={...o,scope:r});return e}function c(t,e,n){return n.map(n=>{const r=n.name;return{readOnly:!0===n.annotations?.readOnlyHint,mcpServer:t,spec:{type:"function",function:{name:s(`mcp__${t}__${r}`),description:`[MCP:${t}] ${n.description||r}`,parameters:n.inputSchema&&"object"==typeof n.inputSchema?n.inputSchema:{type:"object",properties:{}}}},run:t=>e.callTool(r,t)}})}export async function connectServer(t,e){if(!e||!e.command)return{error:`服务「${t}」缺少 command 字段`};const n=new McpStdioClient(t,e);try{await n.start();const e=await n.listTools();return{client:n,tools:c(t,n,e)}}catch(t){return n.close(),{error:t.message}}}export async function loadMcpTools(t,e={}){const n=readConfig(t),r=e.onInfo||(t=>process.stdout.write(o.gray(" · "+t)+"\n")),s=e.onInfo||(t=>process.stdout.write(o.yellow(" · "+t)+"\n")),i=[],c=[],a={cwd:t||"",servers:{}};for(const[t,e]of Object.entries(n))a.servers[t]={name:t,spec:e,scope:e.scope||"global",status:e.disabled?"disabled":!1===e.autoStart?"idle":"pending",toolCount:0,error:""};for(const[t,e]of Object.entries(a.servers)){if("pending"!==e.status)continue;const n=await connectServer(t,e.spec);n.error?(e.status="failed",e.error=n.error,s(`MCP[${t}] 加载失败,已跳过:${n.error}`)):(e.status="connected",e.toolCount=n.tools.length,e.client=n.client,c.push(n.client),i.push(...n.tools),r(`MCP[${t}] 已连接,工具 ${n.tools.length} 个`))}return{tools:i,clients:c,registry:a}}const a=(t,e,n={},r=[])=>({type:"function",function:{name:t,description:e,parameters:{type:"object",properties:n,required:r}}});export function mcpCatalogText(t){const e=Object.values(t?.servers||{}),n=[];for(const t of e){const e="connected"===t.status?`已连接(${t.toolCount} 个工具)`:"idle"===t.status?"已登记,未连接(需要时 connect_mcp 连上)":"failed"===t.status?`连接失败:${String(t.error||"").slice(0,60)}`:"disabled"===t.status?"已禁用":"未连接";n.push(`- ${t.name}(${"project"===t.scope?"项目级":"全局"}):${e}`)}const r=Object.entries(BUILTIN_MCP_CATALOG).filter(([e])=>!t?.servers?.[e]).map(([t,e])=>`- ${t}:${e.description}(未安装,可 connect_mcp 一键装上)`);return r.length&&n.push(...r),n.join("\n")}export function makeMcpConnectTools({registry:t,register:e,clients:n=[],cwd:r=""}){return t&&"function"==typeof e?[{readOnly:!0,spec:a("list_mcp_servers","列出本机可用的 MCP 服务(外部能力连接器):哪些已连上、哪些登记了没连、哪些是内置目录里可一键装的。当任务需要当前工具覆盖不到的能力(访问某个系统、某类数据源)时先看这里。"),run:async()=>{const e=mcpCatalogText(t);return e?"MCP 服务:\n"+e+"\n\n用 connect_mcp(name) 连接后,该服务的工具会以 mcp__<服务名>__<工具名> 出现,下一轮即可调用。":"本机没有配置任何 MCP 服务,也没有内置可装的服务。"}},{readOnly:!1,spec:a("connect_mcp","连接一个 MCP 服务并把它的工具挂进当前会话(下一轮就能调用)。用于会话进行中发现「这活儿需要某个外部能力」时按需接入,不必重开会话。已登记但未自动连接的服务直接给名字即可;内置目录里的服务会用 npx 拉起(首次会下载,需用户同意)。",{name:{type:"string",description:"服务名(见 list_mcp_servers)"},argument:{type:"string",description:"(可选)该服务需要的参数,如 filesystem 的目录、sqlite 的库文件路径"}},["name"]),run:async o=>{const s=String(o?.name||"").trim();if(!s)return"缺少服务名。";const i=t.servers[s];if("connected"===i?.status)return`MCP[${s}] 已经连接(${i.toolCount} 个工具),可直接调用 mcp__${s}__* 工具。`;if("disabled"===i?.status)return`MCP[${s}] 已被禁用(在 mcp.json 里去掉 "disabled": true 后可用)。`;let c=i?.spec;if(!c){const e=BUILTIN_MCP_CATALOG[s];if(!e)return`未找到 MCP 服务「${s}」。可用的见 list_mcp_servers;要接入新服务请在 ~/.huicai/mcp.json 或 <项目>/.huicai/mcp.json 的 mcpServers 里登记。`;const n=String(o?.argument||"").trim()||(e.defaultsToCwd?r:"");if(e.needsArg&&!n)return`服务「${s}」需要参数:${e.needsArg}。请带 argument 再调一次。`;c={command:e.command,args:n?[...e.args,n]:[...e.args]},t.servers[s]={name:s,spec:c,scope:"builtin",status:"pending",toolCount:0,error:""}}const a=await connectServer(s,c),l=t.servers[s];if(a.error)return l.status="failed",l.error=a.error,`连接 MCP[${s}] 失败:${a.error}。(若是内置服务,可能是网络受限导致 npx 拉包失败;可改用本机已有工具完成任务。)`;l.status="connected",l.toolCount=a.tools.length,l.client=a.client,n.push(a.client),e(a.tools);const p=a.tools.map(t=>t.spec.function.name).slice(0,12).join("、");return`✅ 已连接 MCP[${s}],新增 ${a.tools.length} 个工具:${p}`+(a.tools.length>12?" …":"")+"\n下一轮起即可直接调用它们。"}}]:[]}export function closeMcp(t){for(const e of t||[])e.close()}
package/src/notify.js ADDED
@@ -0,0 +1 @@
1
+ import{execFile as e,execFileSync as t}from"node:child_process";import n from"node:os";let i;function o(t,n,{timeout:i=8e3}={}){return new Promise(o=>{let r;try{r=e(t,n,{timeout:i,windowsHide:!0},(e,t,n)=>o({ok:!e,out:String(t||""),err:e?String(n||"")||e.message:""}))}catch(e){return void o({ok:!1,out:"",err:e.message})}r.on("error",e=>o({ok:!1,out:"",err:e.message}))})}function r(e){try{return t("win32"===process.platform?"where":"which",[e],{stdio:["ignore","ignore","ignore"],timeout:3e3,windowsHide:!0}),!0}catch{return!1}}export function detectBackend(e=process.platform){return void 0!==i||(i="darwin"===e?r("terminal-notifier")?"terminal-notifier":"osascript":"win32"===e?"powershell":r("notify-send")?"notify-send":null),i}export function resetBackendCache(){i=void 0}export function escapeAppleScript(e){return String(null==e?"":e).replace(/\\/g,"\\\\").replace(/"/g,'\\"')}export function escapePowerShell(e){return String(null==e?"":e).replace(/'/g,"''")}export function normalizeNotification({title:e,message:t,subtitle:n}={}){const i=(e,t)=>String(null==e?"":e).replace(/\s+/g," ").trim().slice(0,t);return{title:i(e,80)||"huicai",message:i(t,240),subtitle:i(n,80)}}export function buildPowerShellScript({title:e,message:t}){return["Add-Type -AssemblyName System.Windows.Forms;","$n = New-Object System.Windows.Forms.NotifyIcon;","$n.Icon = [System.Drawing.SystemIcons]::Information;","$n.BalloonTipTitle = '"+escapePowerShell(e)+"';","$n.BalloonTipText = '"+escapePowerShell(t)+"';","$n.Visible = $true;","$n.ShowBalloonTip(8000);","Start-Sleep -Milliseconds 3000;","$n.Dispose();"].join(" ")}export async function notify(e={}){const{title:t,message:n,subtitle:i}=normalizeNotification(e),r=!1!==e.sound,s=detectBackend();try{if("terminal-notifier"===s){const e=["-title",t,"-message",n||t];i&&e.push("-subtitle",i),r&&e.push("-sound","default");const a=await o("terminal-notifier",e);return a.ok?{ok:!0,backend:s}:{ok:!1,backend:s,reason:a.err}}if("osascript"===s){let e=`display notification "${escapeAppleScript(n||t)}" with title "${escapeAppleScript(t)}"`;i&&(e+=` subtitle "${escapeAppleScript(i)}"`),r&&(e+=' sound name "Glass"');const a=await o("osascript",["-e",e]);return a.ok?{ok:!0,backend:s}:{ok:!1,backend:s,reason:a.err}}if("powershell"===s){const e=buildPowerShellScript({title:t,message:n||t}),i=await o("powershell",["-NoProfile","-NonInteractive","-Command",e],{timeout:12e3});return i.ok?{ok:!0,backend:s}:{ok:!1,backend:s,reason:i.err}}if("notify-send"===s){const i=["-a","huicai",t,n||t];"critical"===e.urgency&&i.unshift("-u","critical");const r=await o("notify-send",i);return r.ok?{ok:!0,backend:s}:{ok:!1,backend:s,reason:r.err}}return{ok:!1,backend:null,reason:"本机没有可用的通知后端(Linux 需安装 libnotify 的 notify-send)"}}catch(e){return{ok:!1,backend:s,reason:e.message}}}export function bell(e){try{(e&&e.write?e.write:e=>process.stdout.write(e))("")}catch{}}export async function notifyTurnDone({durationMs:e,summary:t="",cwd:n="",minMs:i=6e4,force:o=!1,enabled:r=!0}={}){if(!1===r)return{ok:!1,backend:null,reason:"通知已关闭"};if(!(o||e>=i))return{ok:!1,backend:null,reason:"任务时长未达提醒阈值"};const s=n?n.split(/[\\/]/).filter(Boolean).pop():"",a=Math.round(e/6e4);return notify({title:"huicai · 任务完成"+(s?"("+s+")":""),subtitle:a>=1?`耗时约 ${a} 分钟`:"",message:t||"会话已产出结果,回到终端查看。"})}export function makeNotifyTool({enabled:e=!0,appName:t="huicai"}={}){return!1===e?null:{readOnly:!0,spec:{type:"function",function:{name:"send_notification",description:"在用户电脑上弹一条系统通知(通知中心/托盘),用于把用户「叫回来」:长任务完成、遇到必须由人决定的岔路、无人值守自动化产出了结果、监控发现异常等。不要为每个小步骤刷屏——只在真正需要用户注意时用。",parameters:{type:"object",properties:{title:{type:"string",description:"通知标题(简短,如「数据核对完成」)"},message:{type:"string",description:"通知正文(一句话说清发生了什么、需要用户做什么)"},urgency:{type:"string",description:"可选:normal(默认)| critical(仅 Linux 生效,会常驻)"}},required:["title","message"]}}},run:async e=>{const i=await notify({title:`${t} · ${e?.title||"提醒"}`,message:String(e?.message||""),urgency:e?.urgency});return i.ok?`已发送系统通知(${i.backend}):${e?.title}`:(bell(),`系统通知未能发送(${i.reason||"未知原因"}),已改用终端响铃提醒。`+("linux"===n.platform()?"在 Linux 上可安装 libnotify(notify-send)后恢复。":""))}}}
@@ -0,0 +1 @@
1
+ import{execFile as e,spawn as n}from"node:child_process";function o(n,o,t=4e3){return new Promise(r=>{let s;try{s=e(n,o,{timeout:t,windowsHide:!0,maxBuffer:4194304},(e,n)=>r({ok:!e,out:String(n||"")}))}catch(e){return void r({ok:!1,out:"",err:e.message})}s.on("error",()=>r({ok:!1,out:""}))})}export function parseMacLocked(e){const n=/"CGSSessionScreenIsLocked"\s*=\s*(Yes|No|true|false|1|0)/i.exec(String(e||""));return n?/^(yes|true|1)$/i.test(n[1]):null}export function parseMacIdleSeconds(e){const n=/"HIDIdleTime"\s*=\s*(\d+)/.exec(String(e||""));return n?Math.floor(Number(n[1])/1e9):null}export function parseLoginctlLocked(e){const n=/LockedHint\s*=\s*(yes|no|true|false)/i.exec(String(e||""));return n?/^(yes|true)$/i.test(n[1]):null}export function parseWindowsLocked(e){const n=String(e||"");return!/No tasks are running/i.test(n)&&(!!/LogonUI\.exe/i.test(n)||null)}export async function presence(e=process.platform){let n=null,t=null;if("darwin"===e){const[e,r]=await Promise.all([o("ioreg",["-n","Root","-d1"]),o("ioreg",["-c","IOHIDSystem"])]);n=parseMacLocked(e.out),null===n&&e.ok&&(n=!1),t=parseMacIdleSeconds(r.out)}else if("win32"===e){const e=await o("tasklist",["/FI","IMAGENAME eq LogonUI.exe","/NH"]);n=e.ok?parseWindowsLocked(e.out):null}else{const e=await o("loginctl",["show-session","self","-p","LockedHint"]);n=e.ok?parseLoginctlLocked(e.out):null,null===n&&(process.env.DISPLAY||process.env.WAYLAND_DISPLAY||(n=!0))}return{platform:e,locked:n,idleSeconds:t,guiAvailable:null===n?null:!n}}export function describePresence(e){const n=[];return!0===e.locked?n.push("屏幕已锁定"):!1===e.locked?n.push("屏幕未锁定"):n.push("锁屏状态未知"),null!=e.idleSeconds&&n.push(e.idleSeconds>=60?`已空闲约 ${Math.round(e.idleSeconds/60)} 分钟`:`刚刚有操作(空闲 ${e.idleSeconds}s)`),n.join(",")}let t=null;export function wakeLockCommand(e=process.platform,{keepDisplayOn:n=!1}={}){if("darwin"===e)return{bin:"caffeinate",args:n?["-dimsu"]:["-ims"]};if("linux"===e)return{bin:"systemd-inhibit",args:["--what=idle:sleep"+(n?":handle-lid-switch":""),"--who=huicai","--why=自动化任务运行中","--mode=block","sleep","infinity"]};if("win32"===e){return{bin:"powershell",args:["-NoProfile","-NonInteractive","-Command","Add-Type -Name P -Namespace W -MemberDefinition '[DllImport(\"kernel32.dll\")] public static extern uint SetThreadExecutionState(uint e);'; [W.P]::SetThreadExecutionState("+(n?"0x80000003":"0x80000001")+") | Out-Null; while ($true) { Start-Sleep -Seconds 30 }"]}}return null}export function acquireWakeLock({reason:e="自动化任务运行中",keepDisplayOn:o=!1,spawnImpl:r}={}){if(t)return t.refs++,{ok:!0,held:!0,reason:t.reason,release:()=>releaseWakeLock()};const s=wakeLockCommand(process.platform,{keepDisplayOn:o});if(!s)return{ok:!1,held:!1,reason:"当前平台没有内置的防休眠手段",release:()=>{}};const i=r||((e,o)=>n(e,o,{stdio:"ignore",detached:!1,windowsHide:!0}));let a;try{a=i(s.bin,s.args)}catch(e){return{ok:!1,held:!1,reason:`${s.bin} 启动失败:${e.message}`,release:()=>{}}}let l=null;if(a.on?.("error",e=>{l=e.message,t=null}),a.on?.("exit",()=>{t&&t.child===a&&(t=null)}),t={child:a,refs:1,reason:e,keepDisplayOn:o,since:Date.now()},!acquireWakeLock._hooked){acquireWakeLock._hooked=!0;for(const e of["exit","SIGINT","SIGTERM"])try{process.once(e,()=>{try{t?.child?.kill?.()}catch{}})}catch{}}return{ok:!l,held:!l,reason:l||e,release:()=>releaseWakeLock()}}export function releaseWakeLock({force:e=!1}={}){if(!t)return{held:!1};if(t.refs-=1,t.refs>0&&!e)return{held:!0,refs:t.refs};try{t.child?.kill?.()}catch{}return t=null,{held:!1}}export function wakeLockStatus(){return t?{held:!0,reason:t.reason,keepDisplayOn:!!t.keepDisplayOn,sinceMs:Date.now()-t.since}:{held:!1}}export function _resetWakeLock(){t=null}export async function waitForUnlock({timeoutMs:e=18e5,pollMs:n=5e3,signal:o}={}){const t=Date.now();for(;;){if(o?.aborted)return{unlocked:!1,waitedMs:Date.now()-t,reason:"已中断"};const r=await presence();if(!1===r.locked)return{unlocked:!0,waitedMs:Date.now()-t};if(null===r.locked)return{unlocked:!1,waitedMs:Date.now()-t,reason:"无法探测锁屏状态(该环境可能没有图形会话)"};if(Date.now()-t>=e)return{unlocked:!1,waitedMs:Date.now()-t,reason:`等待超时(${Math.round(e/6e4)} 分钟内未解锁)`};await new Promise(e=>setTimeout(e,n))}}const r=(e,n,o={},t=[])=>({type:"function",function:{name:e,description:n,parameters:{type:"object",properties:o,required:t}}});export function makePresenceTools({signalRef:e}={}){return[{readOnly:!0,spec:r("machine_presence","查看用户电脑当前的在场状态:是否锁屏、空闲多久、是否已持有防休眠锁。在准备做界面自动化(computer_* / 打开应用操作)之前先查一次:锁屏时界面操作必然失败,应改为先做不需要界面的部分(命令/Python/文件/联网),或用 wait_for_unlock 等用户回来。"),run:async()=>{const e=await presence(),n=wakeLockStatus();return`在场状态:${describePresence(e)}。\n界面自动化:${!0===e.guiAvailable?"可用":!1===e.guiAvailable?"不可用(锁屏中)":"未知"}。\n防休眠锁:${n.held?`已持有(${n.reason})`:"未持有"}。`+(e.locked?"\n提示:锁屏期间命令/Python/文件/联网类工作照常可跑,只有屏幕点击键入不行。":"")}},{readOnly:!1,spec:r("set_wake_lock","开启/关闭「防休眠锁」:开启后系统在任务期间不会睡眠,用户合盖或离开时长任务也能跑完。适合在开始一个预计要跑很久的任务(大批量数据处理、长构建、无人值守自动化)前调用,任务结束后关掉。",{on:{type:"boolean",description:"true=获取防休眠锁;false=释放"},reason:{type:"string",description:"(可选)为什么需要,写进状态里便于用户理解"},keep_display_on:{type:"boolean",description:"(可选)连屏幕一起保持常亮(默认否——只防系统睡眠,屏幕仍可自动关闭/锁屏)"}},["on"]),run:async e=>{if(!1===e?.on){return releaseWakeLock({force:!0}).held?"仍有其他持有者,未释放。":"✅ 已释放防休眠锁,系统恢复正常休眠策略。"}const n=acquireWakeLock({reason:String(e?.reason||"自动化任务运行中"),keepDisplayOn:!!e?.keep_display_on});return n.ok?`✅ 已获取防休眠锁(${n.reason})。系统在任务期间不会休眠;`+(e?.keep_display_on?"屏幕也会保持常亮。":"屏幕仍可能自动关闭或锁屏——界面自动化需先 machine_presence 确认。")+"任务结束后请调用 set_wake_lock(on=false) 释放。":"未能获取防休眠锁:"+(n.reason||"未知原因")}},{readOnly:!0,spec:r("wait_for_unlock","挂起等待用户解锁电脑(默认最多 30 分钟)。只在「剩下的步骤必须操作图形界面、且当前锁屏」时用;能改成命令/脚本完成的事不要等人。",{timeout_minutes:{type:"number",description:"(可选)最长等待分钟数,默认 30,上限 240"}}),run:async n=>{const o=Math.min(240,Math.max(1,Number(n?.timeout_minutes)||30)),t=await waitForUnlock({timeoutMs:6e4*o,signal:e?.current});return t.unlocked?`用户已解锁(等待 ${Math.round(t.waitedMs/1e3)}s),现在可以继续界面自动化。`:`未等到解锁:${t.reason||"超时"}。请把剩余工作改为不依赖图形界面的方式完成,或用 send_notification 提醒用户回来,并把当前进度说清楚。`}}]}
package/src/repl.js CHANGED
@@ -1 +1 @@
1
- import s from"node:readline";import{c as o}from"./colors.js";import{createPermissions as t}from"./permissions.js";import{runTurn as e}from"./agent.js";import{createSession as n,saveSession as r,turnCount as i}from"./session.js";import{ensureSessionTitle as a}from"./title.js";import{createCheckpoints as c}from"./checkpoints.js";import{buildSystemPrompt as l}from"./context.js";import{runVerifyLoop as m}from"./autofix.js";import{runAutoFlow as p}from"./autorun.js";import{loadMcpTools as u,closeMcp as d}from"./mcp.js";import{makeSubagentTool as g,makeDispatchAgentsTool as f}from"./subagent.js";import{makeDispatchMemberTool as w}from"./squad.js";import{makeConsultTool as y}from"./consult.js";import{makeExpertTools as h}from"./experts.js";import{listWorkspaceFiles as x}from"./executors.js";import{makeAdvisorTool as j}from"./advisor.js";import{makeRecallTool as T,readMemoryRaw as b}from"./memory.js";import{detectScanners as k,makeScanTool as S}from"./scan.js";import{runSlash as I}from"./commands.js";import{expandAtMentions as $,buildUserContent as v,runBangCommand as P,loadHistory as E,appendHistory as _}from"./input.js";import{makeSkillTools as q}from"./skills.js";import{renderWelcome as C}from"./banner.js";export async function buildExtraTools(s,t,e={}){const n="function"==typeof e.onSub?e.onSub:null;let r={tools:[],clients:[]};try{r=await u()}catch(s){process.stdout.write(o.yellow("MCP 初始化失败(忽略):"+s.message)+"\n")}const i=[...q(t),...r.tools],a=g({config:s,cwd:t,extraTools:r.tools,depth:0,onSub:n});a&&i.push(a);const c=f({config:s,cwd:t,extraTools:r.tools,depth:0,onSub:n});c&&i.push(c);const l=w({config:s,cwd:t,extraTools:r.tools,onSub:n});l&&i.push(l);const m=y({config:s});m&&i.push(m);try{i.push(...await h({config:s}))}catch{}const p=j({config:s});return p&&i.push(p),!1!==s.memoryEnabled&&b(t)&&i.push(T({cwd:t})),k(t).length&&i.push(S({cwd:t})),{extraTools:i,clients:r.clients}}export async function startRepl({config:i,cwd:u,yolo:g,initialPrompt:f,session:w,version:y}){const h=s.createInterface({input:process.stdin,output:process.stdout,history:E(u).reverse(),historySize:200,completer:s=>{const o=/(?:^|\s)@([^\s@]*)$/.exec(s);if(!o)return[[],s];const t="@"+o[1],e=t.toLowerCase();return[x(u).map(s=>"@"+s).filter(s=>s.toLowerCase().startsWith(e)).slice(0,40),t]}}),j=s=>new Promise(o=>h.question(s,o));let T=t({yolo:g,ask:j,cwd:u});const b={prompt:0,completion:0},k={list:[]},S=c();let q=null,O=null;const A={config:i,cwd:u,permissions:T,usage:b,session:w,version:y};h.on("SIGINT",()=>{q?q.abort():process.stdout.write("\n"+o.gray("输入 /exit 退出。")+"\n")}),function(s,o,t,e){process.stdout.write(C({model:s.model,cwd:o,version:e,platform:/^(agent|workflow|datahuman|squad)-/.test(String(s.model||"")),yolo:t.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(i,u,T,y),W(w);let{extraTools:L,clients:J}=await buildExtraTools(i,u);const N=()=>{a(A.session,i).then(s=>{s&&r(A.session)}).catch(()=>{})},Y=async s=>{const{text:t,mentions:n,images:a}=await $(s,u,i,{onDocStart:(s,t,e)=>process.stdout.write(o.gray(` · 正在解析文档 @${s}${e>1?`(${t}/${e})`:""}…`)+"\n")});n.length&&process.stdout.write(o.gray(" · 已展开引用:"+n.join("、")+(a.length?`(含 ${a.length} 张图片)`:""))+"\n");const c=S.snapshot({session:A.session,label:s});A.session.messages.push({role:"user",content:v(t,a)}),N(),q=new AbortController,A.session.compaction||(A.session.compaction={}),i.sessionId=A.session.id;const l=await e({config:i,messages:A.session.messages,permissions:T,cwd:u,signal:q.signal,extraTools:L,todoState:k,compaction:A.session.compaction,fileJournal:c});return l?.usage&&(b.prompt+=l.usage.prompt,b.completion+=l.usage.completion),l?.lastPromptTokens&&(A.session.lastPromptTokens=l.lastPromptTokens),q=null,r(A.session),N(),l},z=()=>({...A,session:A.session,extraTools:L,checkpoints:S,print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(f&&(process.stdout.write(o.cyan("\nhuicai> ")+f+"\n"),await Y(f));;){let s;try{s=(await j(o.cyan("\nhuicai> "))).trim()}catch{break}if(""!==s){if(_(u,s),"/exit"===s||"/quit"===s)break;if(s.startsWith("!")&&s.length>1){const t=s.slice(1).trim(),{output:e,record:n}=await P(t,u);process.stdout.write(o.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),A.session.messages.push({role:"user",content:n}),r(A.session);continue}if(s.startsWith("/")){const e=await I(s,z());if("exit"===e.action)break;"clear"===e.action&&(A.session=n({cwd:u,model:i.model,messages:[A.session.messages[0]]}),k.list=[],S.list.length=0,process.stdout.write(o.gray("已开启新会话(旧会话已保存,可用 huicai --resume 恢复)。\n"))),"refresh"===e.action&&W(A.session),"cd"===e.action&&(u=e.cwd,A.cwd=u,T=t({yolo:T.isYolo(),ask:j,cwd:u}),A.permissions=T,d(J),({extraTools:L,clients:J}=await buildExtraTools(i,u)),A.session=n({cwd:u,model:i.model,messages:[{role:"system",content:l(u,{memory:!1!==i.memoryEnabled})}]}),k.list=[],S.list.length=0,i.sessionId=A.session.id,process.stdout.write(o.gray("已切换到 "+u+"(新会话,旧会话已保存)。\n"))),"autofix"===e.action&&await m({command:e.command,maxIters:e.maxIters,cwd:u,runTurnOnce:s=>Y(s),emit:s=>process.stdout.write(o.gray(s)+"\n")}),"autorun"===e.action&&await p({goal:e.goal,config:i,permissions:T,cwd:u,runTurnOnce:s=>Y(s),emit:s=>process.stdout.write(o.gray(" · "+s)+"\n")}),"loop"===e.action&&(O&&(clearInterval(O),O=null),e.stop?process.stdout.write(o.gray("已停止循环。\n")):(process.stdout.write(o.gray(`已开始循环:每 ${e.seconds}s 运行「${e.prompt}」(/loop off 停止)。\n`)),O=setInterval(()=>{q||Y(e.prompt)},1e3*e.seconds))),e.prompt&&await Y(e.prompt);continue}await Y(s)}}O&&clearInterval(O),h.close(),d(J),process.stdout.write(o.gray("\n再见。\n"))}function O(s){return"string"==typeof s?s:Array.isArray(s)?s.map(s=>"string"==typeof s?s:s&&"text"===s.type&&s.text||"").join(""):""}function W(s){const t=(s.messages||[]).filter(s=>"user"===s.role||"assistant"===s.role),e=i(s.messages);if(0===e)return;process.stdout.write(o.gray(` 已恢复会话「${s.title||""}」(${e} 轮)——历史对话如下:`)+"\n");const n=t.length>40?t.length-40:0;n&&process.stdout.write(o.gray(` (较早 ${n} 条已省略,完整记录在 ~/.huicai/sessions)`)+"\n");for(const s of t.slice(-40)){const t=O(s.content).trim();if(!t)continue;const e=t.length>800?t.slice(0,800)+"…":t,n="user"===s.role?o.bold(" 你 › "):o.gray(" 助手 › ");process.stdout.write(n+e.replace(/\n/g,"\n ")+"\n")}process.stdout.write(o.gray(" ——以上为历史,可继续对话——")+"\n")}export async function runOnce({config:s,cwd:o,yolo:n,prompt:a,session:c,persist:l,outputFormat:m,maxTurns:p}){a||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const u="json"===m,g=t({yolo:n,ask:null,cwd:o}),{extraTools:f,clients:w}=await buildExtraTools(s,o),{text:y,images:h}=await $(a,o,s);c.messages.push({role:"user",content:v(y,h)}),c.compaction||(c.compaction={}),s.sessionId=c.id;const x=await e({config:s,messages:c.messages,permissions:g,cwd:o,extraTools:f,maxIters:p,compaction:c.compaction,io:u?{write:()=>{}}:void 0});if(x?.lastPromptTokens&&(c.lastPromptTokens=x.lastPromptTokens),l&&r(c),d(w),u){const o=[...c.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!x?.error,...x?.error?{error:x.error}:{},result:o?String(o.content):"",usage:{input_tokens:x?.usage?.prompt??0,output_tokens:x?.usage?.completion??0},session_id:c.id,model:s.model,num_turns:i(c.messages)})+"\n")}x?.error&&process.exit(1)}
1
+ import s from"node:readline";import{c as o}from"./colors.js";import{createPermissions as t}from"./permissions.js";import{runTurn as e}from"./agent.js";import{createSession as r,saveSession as n,turnCount as i}from"./session.js";import{ensureSessionTitle as c}from"./title.js";import{createCheckpoints as a}from"./checkpoints.js";import{buildSystemPrompt as l}from"./context.js";import{runVerifyLoop as u}from"./autofix.js";import{runAutoFlow as p}from"./autorun.js";import{loadMcpTools as m,closeMcp as d,makeMcpConnectTools as g}from"./mcp.js";import{makeAutomationTools as f}from"./automation-tools.js";import{makeNotifyTool as w,notifyTurnDone as h}from"./notify.js";import{makePresenceTools as y,acquireWakeLock as j,releaseWakeLock as x}from"./presence.js";import{makeComputerTools as b}from"./computer.js";import{createVisualBridge as k}from"./visual.js";import{autoSyncPlatformSkills as T}from"./skills.js";import{makeSubagentTool as I,makeDispatchAgentsTool as S}from"./subagent.js";import{makeDispatchMemberTool as v}from"./squad.js";import{makeConsultTool as $}from"./consult.js";import{makeExpertTools as q}from"./experts.js";import{listWorkspaceFiles as P}from"./executors.js";import{makeAdvisorTool as E}from"./advisor.js";import{makeRecallTool as R,readMemoryRaw as _}from"./memory.js";import{detectScanners as A,makeScanTool as C}from"./scan.js";import{runSlash as L}from"./commands.js";import{handleAccountError as M,accountErrorKind as O}from"./account.js";import{expandAtMentions as W,buildUserContent as D,runBangCommand as H,loadHistory as V,appendHistory as J}from"./input.js";import{makeSkillTools as N}from"./skills.js";import{renderWelcome as Q}from"./banner.js";export async function buildExtraTools(s,t,e={}){const r="function"==typeof e.onSub?e.onSub:null;let n={tools:[],clients:[],registry:{servers:{}}};try{n=await m(t)}catch(s){process.stdout.write(o.yellow("MCP 初始化失败(忽略):"+s.message)+"\n")}const i=[...N(t),...n.tools],c=I({config:s,cwd:t,extraTools:n.tools,depth:0,onSub:r});c&&i.push(c);const a=S({config:s,cwd:t,extraTools:n.tools,depth:0,onSub:r});a&&i.push(a);const l=v({config:s,cwd:t,extraTools:n.tools,onSub:r});l&&i.push(l);const u=$({config:s});u&&i.push(u);try{i.push(...await q({config:s}))}catch{}const p=E({config:s});p&&i.push(p),!1!==s.memoryEnabled&&_(t)&&i.push(R({cwd:t})),A(t).length&&i.push(C({cwd:t})),i.push(...g({registry:n.registry,clients:n.clients,cwd:t,register:s=>i.push(...s)})),i.push(...f({cwd:t,selfSchedule:!1!==e.selfSchedule,runnerHint:e.runnerHint}));const d=w({enabled:!1!==s.notifications});d&&i.push(d),i.push(...y({signalRef:e.signalRef}));const h="function"==typeof e.injectVisual?e.injectVisual:"function"==typeof e.queueInject?k({config:s,queue:e.queueInject,currentModel:e.currentModel}):null;return i.push(...b({config:s,injectVisual:h,omit:e.omitComputer||[],enabled:e.computerUse})),T(s,{onInfo:t=>{s.debug&&process.stdout.write(o.gray(" · "+t)+"\n")}}).catch(()=>{}),{extraTools:i,clients:n.clients,mcpRegistry:n.registry}}export async function startRepl({config:i,cwd:m,yolo:g,initialPrompt:f,session:w,version:y}){const b=s.createInterface({input:process.stdin,output:process.stdout,history:V(m).reverse(),historySize:200,completer:s=>{const o=/(?:^|\s)@([^\s@]*)$/.exec(s);if(!o)return[[],s];const t="@"+o[1],e=t.toLowerCase();return[P(m).map(s=>"@"+s).filter(s=>s.toLowerCase().startsWith(e)).slice(0,40),t]}}),k=s=>new Promise(o=>b.question(s,o));let T=!1,I=t({yolo:g,ask:k,cwd:m});const S={prompt:0,completion:0},v={list:[]},$=a();let q=null,E=null;const R={config:i,cwd:m,permissions:I,usage:S,session:w,version:y};b.on("SIGINT",()=>{q?q.abort():process.stdout.write("\n"+o.gray("输入 /exit 退出。")+"\n")}),function(s,o,t,e){process.stdout.write(Q({model:s.model,cwd:o,version:e,platform:/^(agent|workflow|datahuman|squad)-/.test(String(s.model||"")),yolo:t.isYolo(),consult:s.consult?s.consultLabel||s.consult:null})+"\n")}(i,m,I,y),z(w);const _=[],A={current:null};let{extraTools:C,clients:O}=await buildExtraTools(i,m,{queueInject:s=>_.push(s),signalRef:A,runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"});const N=()=>{c(R.session,i).then(s=>{s&&n(R.session)}).catch(()=>{})},Y=async s=>{const{text:t,mentions:r,images:c}=await W(s,m,i,{onDocStart:(s,t,e)=>process.stdout.write(o.gray(` · 正在解析文档 @${s}${e>1?`(${t}/${e})`:""}…`)+"\n")});r.length&&process.stdout.write(o.gray(" · 已展开引用:"+r.join("、")+(c.length?`(含 ${c.length} 张图片)`:""))+"\n");const a=$.snapshot({session:R.session,label:s});R.session.messages.push({role:"user",content:D(t,c)}),N(),q=new AbortController,A.current=q.signal,R.session.compaction||(R.session.compaction={}),i.sessionId=R.session.id;const l="auto"===I.getMode?.()&&!1!==i.wakeLockInAuto?j({reason:"自动模式任务执行中"}):null,u=Date.now();let p;try{p=await e({config:i,messages:R.session.messages,permissions:I,cwd:m,signal:q.signal,extraTools:C,todoState:v,compaction:R.session.compaction,fileJournal:a,pullQueued:()=>_.shift()||null})}finally{l?.ok&&x()}return p?.usage&&(S.prompt+=p.usage.prompt,S.completion+=p.usage.completion),p?.lastPromptTokens&&(R.session.lastPromptTokens=p.lastPromptTokens),q=null,A.current=null,p?.aborted||p?.error||h({durationMs:Date.now()-u,cwd:m,summary:String(s).replace(/\s+/g," ").slice(0,80),enabled:!1!==i.notifications}).catch(()=>{}),n(R.session),N(),p?.error&&"auth"===M(p,i)&&(T=!0),p},F=()=>({...R,session:R.session,extraTools:C,checkpoints:$,print:s=>process.stdout.write(String(s).endsWith("\n")?String(s):String(s)+"\n")});for(f&&(process.stdout.write(o.cyan("\nhuicai> ")+f+"\n"),await Y(f));!T;){let s;try{s=(await k(o.cyan("\nhuicai> "))).trim()}catch{break}if(""!==s){if(J(m,s),"/exit"===s||"/quit"===s)break;if(s.startsWith("!")&&s.length>1){const t=s.slice(1).trim(),{output:e,record:r}=await H(t,m);process.stdout.write(o.gray(e.split("\n").map(s=>" "+s).join("\n"))+"\n"),R.session.messages.push({role:"user",content:r}),n(R.session);continue}if(s.startsWith("/")){const e=await L(s,F());if("exit"===e.action)break;"clear"===e.action&&(R.session=r({cwd:m,model:i.model,messages:[R.session.messages[0]]}),v.list=[],$.list.length=0,process.stdout.write(o.gray("已开启新会话(旧会话已保存,可用 huicai --resume 恢复)。\n"))),"refresh"===e.action&&z(R.session),"cd"===e.action&&(m=e.cwd,R.cwd=m,I=t({yolo:I.isYolo(),ask:k,cwd:m}),R.permissions=I,d(O),({extraTools:C,clients:O}=await buildExtraTools(i,m,{queueInject:s=>_.push(s),signalRef:A})),R.session=r({cwd:m,model:i.model,messages:[{role:"system",content:l(m,{memory:!1!==i.memoryEnabled,config:i})}]}),v.list=[],$.list.length=0,i.sessionId=R.session.id,process.stdout.write(o.gray("已切换到 "+m+"(新会话,旧会话已保存)。\n"))),"autofix"===e.action&&await u({command:e.command,maxIters:e.maxIters,cwd:m,runTurnOnce:s=>Y(s),emit:s=>process.stdout.write(o.gray(s)+"\n")}),"autorun"===e.action&&await p({goal:e.goal,config:i,permissions:I,cwd:m,runTurnOnce:s=>Y(s),emit:s=>process.stdout.write(o.gray(" · "+s)+"\n")}),"loop"===e.action&&(E&&(clearInterval(E),E=null),e.stop?process.stdout.write(o.gray("已停止循环。\n")):(process.stdout.write(o.gray(`已开始循环:每 ${e.seconds}s 运行「${e.prompt}」(/loop off 停止)。\n`)),E=setInterval(()=>{q||Y(e.prompt)},1e3*e.seconds))),e.prompt&&await Y(e.prompt);continue}await Y(s)}}return E&&clearInterval(E),b.close(),d(O),T?{relogin:!0}:(process.stdout.write(o.gray("\n再见。\n")),{})}function Y(s){return"string"==typeof s?s:Array.isArray(s)?s.map(s=>"string"==typeof s?s:s&&"text"===s.type&&s.text||"").join(""):""}function z(s){const t=(s.messages||[]).filter(s=>"user"===s.role||"assistant"===s.role),e=i(s.messages);if(0===e)return;process.stdout.write(o.gray(` 已恢复会话「${s.title||""}」(${e} 轮)——历史对话如下:`)+"\n");const r=t.length>40?t.length-40:0;r&&process.stdout.write(o.gray(` (较早 ${r} 条已省略,完整记录在 ~/.huicai/sessions)`)+"\n");for(const s of t.slice(-40)){const t=Y(s.content).trim();if(!t)continue;const e=t.length>800?t.slice(0,800)+"…":t,r="user"===s.role?o.bold(" 你 › "):o.gray(" 助手 › ");process.stdout.write(r+e.replace(/\n/g,"\n ")+"\n")}process.stdout.write(o.gray(" ——以上为历史,可继续对话——")+"\n")}export async function runOnce({config:s,cwd:r,yolo:c,prompt:a,session:l,persist:u,outputFormat:p,maxTurns:m}){a||(process.stderr.write('用法: huicai -p "你的指令"\n'),process.exit(1));const g="json"===p,f=t({yolo:c,ask:null,cwd:r}),w=[],{extraTools:h,clients:y}=await buildExtraTools(s,r,{queueInject:s=>w.push(s)}),{text:b,images:k}=await W(a,r,s);l.messages.push({role:"user",content:D(b,k)}),l.compaction||(l.compaction={}),s.sessionId=l.id;const T=c&&!1!==s.wakeLockInAuto?j({reason:"无人值守任务执行中"}):null;let I;try{I=await e({config:s,messages:l.messages,permissions:f,cwd:r,extraTools:h,maxIters:m,compaction:l.compaction,io:g?{write:()=>{}}:void 0,pullQueued:()=>w.shift()||null})}finally{T?.ok&&x()}I?.lastPromptTokens&&(l.lastPromptTokens=I.lastPromptTokens),u&&n(l),d(y);const S=I?.error?O(I):"";if(S&&(M(I,s,s=>process.stderr.write(s)),"auth"===S&&process.stderr.write(o.gray("请重新运行 `huicai login` 登录后重试。")+"\n")),g){const o=[...l.messages].reverse().find(s=>"assistant"===s.role&&s.content);process.stdout.write(JSON.stringify({type:"result",is_error:!!I?.error,...I?.error?{error:I.error}:{},...S?{error_kind:S}:{},result:o?String(o.content):"",usage:{input_tokens:I?.usage?.prompt??0,output_tokens:I?.usage?.completion??0},session_id:l.id,model:s.model,num_turns:i(l.messages)})+"\n")}I?.error&&process.exit(1)}
package/src/skills.js CHANGED
@@ -1 +1 @@
1
- import e from"node:fs";import n from"node:path";import r from"node:os";import{fileURLToPath as t}from"node:url";export const BUNDLED_SKILLS_DIR=n.join(n.dirname(t(import.meta.url)),"..","skills");const i=new Set([".md",".markdown",".txt",".json",".yaml",".yml",".py",".js",".mjs",".cjs",".ts",".sh",".sql",".csv",".html",".css",".xml",".toml"]),o=262144;export const SKILLS_STATE_PATH=n.join(r.homedir(),".huicai","skills-state.json");export function loadDisabledSet(){try{const n=JSON.parse(e.readFileSync(SKILLS_STATE_PATH,"utf8"));return new Set(Array.isArray(n?.disabled)?n.disabled.map(String):[])}catch{return new Set}}function l(r){e.mkdirSync(n.dirname(SKILLS_STATE_PATH),{recursive:!0}),e.writeFileSync(SKILLS_STATE_PATH,JSON.stringify({disabled:[...r].sort()},null,2)+"\n",{mode:384})}export function setSkillEnabled(e,n){const r=String(e||"").trim();if(!r)throw new Error("技能名为空");const t=loadDisabledSet();return n?t.delete(r):t.add(r),l(t),[...t]}export function parseFrontmatter(e){const n={name:"",description:"",version:"",body:(e||"").trim()},r=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(n.body);if(!r)return n;n.body=n.body.slice(r[0].length).trim();for(const e of r[1].split(/\r?\n/)){const r=/^(name|description|version)\s*:\s*(.*)$/.exec(e.trim());r&&(n[r[1]]=r[2].trim().replace(/^["']|["']$/g,""))}return n}export function skillDirs(e){const t=[];return e&&t.push(n.join(e,".huicai","skills")),t.push(n.join(r.homedir(),".huicai","skills")),t}function s(r,t="",o=0,l=[]){if(o>3||l.length>=50)return l;let a;try{a=e.readdirSync(r,{withFileTypes:!0})}catch{return l}for(const e of a){if(l.length>=50)break;if(e.name.startsWith("."))continue;const a=t?`${t}/${e.name}`:e.name;e.isDirectory()?s(n.join(r,e.name),a,o+1,l):e.isFile()&&"SKILL.md"!==a&&i.has(n.extname(e.name).toLowerCase())&&l.push(a)}return l}export function loadLocalSkills(t){const i=new Map,o=loadDisabledSet(),l=[...t?[{dir:n.join(t,".huicai","skills"),scope:"project"}]:[],{dir:n.join(r.homedir(),".huicai","skills"),scope:"global"},{dir:BUNDLED_SKILLS_DIR,scope:"builtin"}];for(const{dir:r,scope:t}of l){let l;try{l=e.readdirSync(r,{withFileTypes:!0})}catch{continue}for(const a of l){if(!a.isDirectory())continue;const l=n.join(r,a.name);let c;try{c=e.readFileSync(n.join(l,"SKILL.md"),"utf8")}catch{continue}const d=parseFrontmatter(c),m=(d.name||a.name).trim();m&&!i.has(m)&&i.set(m,{name:m,description:d.description||"",version:d.version||"",dir:l,scope:t,files:s(l),enabled:!o.has(m)})}}return[...i.values()]}export function readSkillBody(r,t){const i=loadLocalSkills(r).find(e=>e.name===t);if(!i)return null;try{const r=e.readFileSync(n.join(i.dir,"SKILL.md"),"utf8");return{skill:i,raw:r.length>65536?r.slice(0,65536)+"\n…(正文已截断)":r}}catch{return null}}export function readSkillFileContent(r,t,i){const l=loadLocalSkills(r).find(e=>e.name===t);if(!l)throw new Error(`本地技能不存在:${t}`);const s=String(i||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,""),a=n.resolve(l.dir,s);if(!a.startsWith(n.resolve(l.dir)+n.sep))throw new Error(`路径越界:${i}`);if(e.statSync(a).size>o)throw new Error(`附件过大(>256KB):${s}`);return e.readFileSync(a,"utf8")}export function skillCatalog(e){const n=loadLocalSkills(e).filter(e=>!1!==e.enabled);if(!n.length)return"";return"以下本地技能可用 load_skill(name) 载入其操作指引后遵循执行(描述含「何时使用」,据此判断是否载入;无关任务不要载入):\n"+n.map(e=>{const n=(e.description||"(无描述)").replace(/\s+/g," ").slice(0,300);return`- ${e.name}:${n}`}).join("\n")}export function makeSkillTools(e){const n=loadLocalSkills(e).filter(e=>!1!==e.enabled);if(!n.length)return[];const r=[{readOnly:!0,spec:{type:"function",function:{name:"load_skill",description:"载入一个本地技能的操作指引(系统提示词「可用技能」目录中的名称)。载入后严格遵循其流程执行。",parameters:{type:"object",properties:{name:{type:"string",description:"技能名(见「可用技能」目录)"}},required:["name"]}}},run:async n=>{const r=String(n?.name||"").trim(),t=readSkillBody(e,r);if(!t){return`本地技能不存在:${r}(可用:${loadLocalSkills(e).filter(e=>!1!==e.enabled).map(e=>e.name).join(", ")||"无"})`}if(!1===t.skill.enabled)return`技能「${r}」已被禁用,无法载入(可在「技能·MCP」中重新启用)。`;let i=t.raw;return t.skill.files.length&&(i+=`\n\n---\n本技能附带 ${t.skill.files.length} 个资源文件,需要时用 read_skill_file(skill="${r}", path=...) 读取:\n`+t.skill.files.map(e=>`- ${e}`).join("\n")),i}}];return n.some(e=>e.files.length)&&r.push({readOnly:!0,spec:{type:"function",function:{name:"read_skill_file",description:"读取本地技能附带的资源文件(先 load_skill 查看其文件清单,再按 path 读取)。",parameters:{type:"object",properties:{skill:{type:"string",description:"技能名"},path:{type:"string",description:"资源文件相对路径(技能载入结果中列出)"}},required:["skill","path"]}}},run:async n=>{try{const r=readSkillFileContent(e,String(n?.skill||"").trim(),n?.path);return`文件 ${n?.path}:\n${r}`}catch(e){return`读取失败:${e.message}`}}}),r}export function writeSkillToDir(r,t){const i=String(t?.name||"").trim();if(!i||!t?.content)throw new Error("技能数据不完整(缺 name 或 content)");const o=n.join(r,i.replace(/[^a-zA-Z0-9_-]/g,"-"));e.mkdirSync(o,{recursive:!0}),e.writeFileSync(n.join(o,"SKILL.md"),t.content,"utf8");for(const r of t.files||[]){const t=String(r?.path||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,"");if(!t||t.includes(".."))continue;const i=n.join(o,t);e.mkdirSync(n.dirname(i),{recursive:!0}),e.writeFileSync(i,r.content??"","utf8")}return o}export function isValidSkillName(e){return/^[a-zA-Z0-9_-]{1,48}$/.test(String(e||""))}function a(e,t){if("project"===e){if(!t)throw new Error("项目级技能需要在会话/项目内创建");return n.join(t,".huicai","skills")}return n.join(r.homedir(),".huicai","skills")}export function scaffoldSkillMarkdown(e,n,r){return`---\nname: ${e}\ndescription: ${String(n||"").replace(/\r?\n/g," ").trim()||"(补充:这个技能做什么、何时使用)"}\nversion: 1.0.0\n---\n\n`+(null!=r&&String(r).trim()?String(r).trim()+"\n":`# ${e}\n\n## 何时使用\n\n(描述触发场景,让模型据此判断是否 load_skill)\n\n## 流程\n\n1. \n2. \n3. \n\n## 注意事项\n\n- \n`)}export function createSkillPackage({name:r,description:t="",scope:i="global",cwd:o=null,body:l=""}={}){const s=String(r||"").trim();if(!isValidSkillName(s))throw new Error("技能名非法:仅限字母/数字/下划线/连字符,长度 1–48");if("project"!==i&&"global"!==i)throw new Error("只能创建到项目级或全局级");if(loadLocalSkills(o).some(e=>e.name===s))throw new Error(`技能已存在:${s}`);const c=a(i,o),d=n.join(c,s);return e.mkdirSync(d,{recursive:!0}),e.writeFileSync(n.join(d,"SKILL.md"),scaffoldSkillMarkdown(s,t,l),"utf8"),{dir:d,name:s,scope:i}}export function installSkillPackage({name:r,content:t,files:i=[],scope:o="global",cwd:l=null,conflict:s="skip"}={}){const c=String(r||"").trim();if(!isValidSkillName(c))throw new Error("技能名非法:仅限字母/数字/下划线/连字符,长度 1–48(来源 SKILL.md 的 frontmatter name)");if(!String(t||"").trim())throw new Error("SKILL.md 正文为空");if("project"!==o&&"global"!==o)throw new Error("只能导入到项目级或全局级");const d=(Array.isArray(i)?i:[]).filter(e=>e&&"string"==typeof e.path),m=loadLocalSkills(l).find(e=>e.name===c&&"builtin"!==e.scope);if(m){if("overwrite"!==s)return{name:c,status:"skipped",dir:m.dir};e.writeFileSync(n.join(m.dir,"SKILL.md"),String(t),"utf8");for(const r of d){const t=String(r.path).replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,"");if(!t||t.includes(".."))continue;const i=n.join(m.dir,t);e.mkdirSync(n.dirname(i),{recursive:!0}),e.writeFileSync(i,r.content??"","utf8")}return{name:c,status:"overwritten",dir:m.dir}}const f=writeSkillToDir(a(o,l),{name:c,content:t,files:d});return{name:c,status:"imported",dir:f}}export function overwriteSkillBody(r,t,i){const o=loadLocalSkills(r).find(e=>e.name===String(t||"").trim());if(!o)throw new Error("本地技能不存在:"+t);if("builtin"===o.scope)throw new Error("内置技能只读,请先「复制到全局」再编辑");if(!String(i||"").trim())throw new Error("SKILL.md 正文不能为空");return e.writeFileSync(n.join(o.dir,"SKILL.md"),String(i),"utf8"),{dir:o.dir,name:o.name}}export function removeSkillPackage(r,t){const i=loadLocalSkills(r).find(e=>e.name===String(t||"").trim());if(!i)throw new Error("本地技能不存在:"+t);if("builtin"===i.scope)throw new Error("内置技能不可卸载(可在「技能·MCP」中禁用)");const o=n.resolve(a(i.scope,r)),s=n.resolve(i.dir);if(s!==o&&!s.startsWith(o+n.sep))throw new Error("拒绝删除技能库之外的目录");e.rmSync(s,{recursive:!0,force:!0});const c=loadDisabledSet();return c.delete(i.name)&&l(c),{name:i.name,scope:i.scope}}export function forkSkillToWritable(e,n,r="global"){const t=loadLocalSkills(e).find(e=>e.name===String(n||"").trim());if(!t)throw new Error("本地技能不存在:"+n);if("project"!==r&&"global"!==r)throw new Error("只能复制到项目级或全局级");const i=new Set(loadLocalSkills(e).map(e=>e.name));let o=t.name;for(;i.has(o);)o=(o+"-copy").slice(0,48);const l=readSkillDirForPush(t.dir),s=l.content.replace(/^(---[\s\S]*?\nname:\s*).*$/m,`$1${o}`);return{dir:writeSkillToDir(a(r,e),{name:o,content:s,files:l.files}),name:o,scope:r}}export function readSkillDirForPush(r){const t=n.join(r,"SKILL.md"),i=e.readFileSync(t,"utf8"),l=[];for(const t of s(r))try{const i=n.join(r,t);if(e.statSync(i).size>o)continue;l.push({path:t,content:e.readFileSync(i,"utf8")})}catch{}return{content:i,filename:n.basename(r),files:l}}
1
+ import e from"node:fs";import n from"node:path";import r from"node:os";import{fileURLToPath as t}from"node:url";export const BUNDLED_SKILLS_DIR=n.join(n.dirname(t(import.meta.url)),"..","skills");const i=new Set([".md",".markdown",".txt",".json",".yaml",".yml",".py",".js",".mjs",".cjs",".ts",".sh",".sql",".csv",".html",".css",".xml",".toml"]),o=262144;export const SKILLS_STATE_PATH=n.join(r.homedir(),".huicai","skills-state.json");export function loadDisabledSet(){try{const n=JSON.parse(e.readFileSync(SKILLS_STATE_PATH,"utf8"));return new Set(Array.isArray(n?.disabled)?n.disabled.map(String):[])}catch{return new Set}}function l(r){e.mkdirSync(n.dirname(SKILLS_STATE_PATH),{recursive:!0}),e.writeFileSync(SKILLS_STATE_PATH,JSON.stringify({disabled:[...r].sort()},null,2)+"\n",{mode:384})}export function setSkillEnabled(e,n){const r=String(e||"").trim();if(!r)throw new Error("技能名为空");const t=loadDisabledSet();return n?t.delete(r):t.add(r),l(t),[...t]}export function parseFrontmatter(e){const n={name:"",description:"",version:"",body:(e||"").trim()},r=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(n.body);if(!r)return n;n.body=n.body.slice(r[0].length).trim();for(const e of r[1].split(/\r?\n/)){const r=/^(name|description|version)\s*:\s*(.*)$/.exec(e.trim());r&&(n[r[1]]=r[2].trim().replace(/^["']|["']$/g,""))}return n}export function skillDirs(e){const t=[];return e&&t.push(n.join(e,".huicai","skills")),t.push(n.join(r.homedir(),".huicai","skills")),t}function s(r,t="",o=0,l=[]){if(o>3||l.length>=50)return l;let a;try{a=e.readdirSync(r,{withFileTypes:!0})}catch{return l}for(const e of a){if(l.length>=50)break;if(e.name.startsWith("."))continue;const a=t?`${t}/${e.name}`:e.name;e.isDirectory()?s(n.join(r,e.name),a,o+1,l):e.isFile()&&"SKILL.md"!==a&&i.has(n.extname(e.name).toLowerCase())&&l.push(a)}return l}export function loadLocalSkills(t){const i=new Map,o=loadDisabledSet(),l=[...t?[{dir:n.join(t,".huicai","skills"),scope:"project"}]:[],{dir:n.join(r.homedir(),".huicai","skills"),scope:"global"},{dir:BUNDLED_SKILLS_DIR,scope:"builtin"}];for(const{dir:r,scope:t}of l){let l;try{l=e.readdirSync(r,{withFileTypes:!0})}catch{continue}for(const a of l){if(!a.isDirectory())continue;const l=n.join(r,a.name);let c;try{c=e.readFileSync(n.join(l,"SKILL.md"),"utf8")}catch{continue}const d=parseFrontmatter(c),p=(d.name||a.name).trim();p&&!i.has(p)&&i.set(p,{name:p,description:d.description||"",version:d.version||"",dir:l,scope:t,files:s(l),enabled:!o.has(p)})}}return[...i.values()]}export function readSkillBody(r,t){const i=loadLocalSkills(r).find(e=>e.name===t);if(!i)return null;try{const r=e.readFileSync(n.join(i.dir,"SKILL.md"),"utf8");return{skill:i,raw:r.length>65536?r.slice(0,65536)+"\n…(正文已截断)":r}}catch{return null}}export function readSkillFileContent(r,t,i){const l=loadLocalSkills(r).find(e=>e.name===t);if(!l)throw new Error(`本地技能不存在:${t}`);const s=String(i||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,""),a=n.resolve(l.dir,s);if(!a.startsWith(n.resolve(l.dir)+n.sep))throw new Error(`路径越界:${i}`);if(e.statSync(a).size>o)throw new Error(`附件过大(>256KB):${s}`);return e.readFileSync(a,"utf8")}export function skillCatalog(e){const n=loadLocalSkills(e).filter(e=>!1!==e.enabled);if(!n.length)return"";return"以下本地技能可用 load_skill(name) 载入其操作指引后遵循执行(描述含「何时使用」,据此判断是否载入;无关任务不要载入):\n"+n.map(e=>{const n=(e.description||"(无描述)").replace(/\s+/g," ").slice(0,300);return`- ${e.name}:${n}`}).join("\n")}export function makeSkillTools(e){const n=loadLocalSkills(e).filter(e=>!1!==e.enabled);if(!n.length)return[];const r=[{readOnly:!0,spec:{type:"function",function:{name:"load_skill",description:"载入一个本地技能的操作指引(系统提示词「可用技能」目录中的名称)。载入后严格遵循其流程执行。",parameters:{type:"object",properties:{name:{type:"string",description:"技能名(见「可用技能」目录)"}},required:["name"]}}},run:async n=>{const r=String(n?.name||"").trim(),t=readSkillBody(e,r);if(!t){return`本地技能不存在:${r}(可用:${loadLocalSkills(e).filter(e=>!1!==e.enabled).map(e=>e.name).join(", ")||"无"})`}if(!1===t.skill.enabled)return`技能「${r}」已被禁用,无法载入(可在「技能·MCP」中重新启用)。`;let i=t.raw;return t.skill.files.length&&(i+=`\n\n---\n本技能附带 ${t.skill.files.length} 个资源文件,需要时用 read_skill_file(skill="${r}", path=...) 读取:\n`+t.skill.files.map(e=>`- ${e}`).join("\n")),i}}];return n.some(e=>e.files.length)&&r.push({readOnly:!0,spec:{type:"function",function:{name:"read_skill_file",description:"读取本地技能附带的资源文件(先 load_skill 查看其文件清单,再按 path 读取)。",parameters:{type:"object",properties:{skill:{type:"string",description:"技能名"},path:{type:"string",description:"资源文件相对路径(技能载入结果中列出)"}},required:["skill","path"]}}},run:async n=>{try{const r=readSkillFileContent(e,String(n?.skill||"").trim(),n?.path);return`文件 ${n?.path}:\n${r}`}catch(e){return`读取失败:${e.message}`}}}),r}export function writeSkillToDir(r,t){const i=String(t?.name||"").trim();if(!i||!t?.content)throw new Error("技能数据不完整(缺 name 或 content)");const o=n.join(r,i.replace(/[^a-zA-Z0-9_-]/g,"-"));e.mkdirSync(o,{recursive:!0}),e.writeFileSync(n.join(o,"SKILL.md"),t.content,"utf8");for(const r of t.files||[]){const t=String(r?.path||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,"");if(!t||t.includes(".."))continue;const i=n.join(o,t);e.mkdirSync(n.dirname(i),{recursive:!0}),e.writeFileSync(i,r.content??"","utf8")}return o}export function isValidSkillName(e){return/^[a-zA-Z0-9_-]{1,48}$/.test(String(e||""))}function a(e,t){if("project"===e){if(!t)throw new Error("项目级技能需要在会话/项目内创建");return n.join(t,".huicai","skills")}return n.join(r.homedir(),".huicai","skills")}export function scaffoldSkillMarkdown(e,n,r){return`---\nname: ${e}\ndescription: ${String(n||"").replace(/\r?\n/g," ").trim()||"(补充:这个技能做什么、何时使用)"}\nversion: 1.0.0\n---\n\n`+(null!=r&&String(r).trim()?String(r).trim()+"\n":`# ${e}\n\n## 何时使用\n\n(描述触发场景,让模型据此判断是否 load_skill)\n\n## 流程\n\n1. \n2. \n3. \n\n## 注意事项\n\n- \n`)}export function createSkillPackage({name:r,description:t="",scope:i="global",cwd:o=null,body:l=""}={}){const s=String(r||"").trim();if(!isValidSkillName(s))throw new Error("技能名非法:仅限字母/数字/下划线/连字符,长度 1–48");if("project"!==i&&"global"!==i)throw new Error("只能创建到项目级或全局级");if(loadLocalSkills(o).some(e=>e.name===s))throw new Error(`技能已存在:${s}`);const c=a(i,o),d=n.join(c,s);return e.mkdirSync(d,{recursive:!0}),e.writeFileSync(n.join(d,"SKILL.md"),scaffoldSkillMarkdown(s,t,l),"utf8"),{dir:d,name:s,scope:i}}export function installSkillPackage({name:r,content:t,files:i=[],scope:o="global",cwd:l=null,conflict:s="skip"}={}){const c=String(r||"").trim();if(!isValidSkillName(c))throw new Error("技能名非法:仅限字母/数字/下划线/连字符,长度 1–48(来源 SKILL.md 的 frontmatter name)");if(!String(t||"").trim())throw new Error("SKILL.md 正文为空");if("project"!==o&&"global"!==o)throw new Error("只能导入到项目级或全局级");const d=(Array.isArray(i)?i:[]).filter(e=>e&&"string"==typeof e.path),p=loadLocalSkills(l).find(e=>e.name===c&&"builtin"!==e.scope);if(p){if("overwrite"!==s)return{name:c,status:"skipped",dir:p.dir};e.writeFileSync(n.join(p.dir,"SKILL.md"),String(t),"utf8");for(const r of d){const t=String(r.path).replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,"");if(!t||t.includes(".."))continue;const i=n.join(p.dir,t);e.mkdirSync(n.dirname(i),{recursive:!0}),e.writeFileSync(i,r.content??"","utf8")}return{name:c,status:"overwritten",dir:p.dir}}const f=writeSkillToDir(a(o,l),{name:c,content:t,files:d});return{name:c,status:"imported",dir:f}}export function overwriteSkillBody(r,t,i){const o=loadLocalSkills(r).find(e=>e.name===String(t||"").trim());if(!o)throw new Error("本地技能不存在:"+t);if("builtin"===o.scope)throw new Error("内置技能只读,请先「复制到全局」再编辑");if(!String(i||"").trim())throw new Error("SKILL.md 正文不能为空");return e.writeFileSync(n.join(o.dir,"SKILL.md"),String(i),"utf8"),{dir:o.dir,name:o.name}}export function removeSkillPackage(r,t){const i=loadLocalSkills(r).find(e=>e.name===String(t||"").trim());if(!i)throw new Error("本地技能不存在:"+t);if("builtin"===i.scope)throw new Error("内置技能不可卸载(可在「技能·MCP」中禁用)");const o=n.resolve(a(i.scope,r)),s=n.resolve(i.dir);if(s!==o&&!s.startsWith(o+n.sep))throw new Error("拒绝删除技能库之外的目录");e.rmSync(s,{recursive:!0,force:!0});const c=loadDisabledSet();return c.delete(i.name)&&l(c),{name:i.name,scope:i.scope}}export function forkSkillToWritable(e,n,r="global"){const t=loadLocalSkills(e).find(e=>e.name===String(n||"").trim());if(!t)throw new Error("本地技能不存在:"+n);if("project"!==r&&"global"!==r)throw new Error("只能复制到项目级或全局级");const i=new Set(loadLocalSkills(e).map(e=>e.name));let o=t.name;for(;i.has(o);)o=(o+"-copy").slice(0,48);const l=readSkillDirForPush(t.dir),s=l.content.replace(/^(---[\s\S]*?\nname:\s*).*$/m,`$1${o}`);return{dir:writeSkillToDir(a(r,e),{name:o,content:s,files:l.files}),name:o,scope:r}}export const SKILLS_SYNC_STAMP=n.join(r.homedir(),".huicai","cache","skills-sync.json");function c(){try{return JSON.parse(e.readFileSync(SKILLS_SYNC_STAMP,"utf8"))||{}}catch{return{}}}export async function autoSyncPlatformSkills(r,t={}){if(!r?.baseUrl||!r?.apiKey)return{synced:0,skipped:0,skipped_reason:"未登录"};if(!1===r.autoSkillSync)return{synced:0,skipped:0,skipped_reason:"已关闭自动同步"};const i=c();if(!t.force&&i.at&&Date.now()-i.at<216e5)return{synced:0,skipped:0,skipped_reason:"距上次同步未满 6 小时"};const o=t.api||await import("./api.js");let l;try{l=await o.listPlatformSkills(r.baseUrl,r.apiKey)}catch(e){return{synced:0,skipped:0,skipped_reason:e.message}}!function(r){try{e.mkdirSync(n.dirname(SKILLS_SYNC_STAMP),{recursive:!0}),e.writeFileSync(SKILLS_SYNC_STAMP,JSON.stringify({...c(),...r})+"\n",{mode:384})}catch{}}({at:Date.now()});const s=new Set(loadLocalSkills(null).map(e=>e.name));let a=0,d=0;for(const e of l){const n=String(e?.name||"").trim();if(n&&isValidSkillName(n))if(s.has(n))d++;else try{const t=await o.getPlatformSkill(r.baseUrl,r.apiKey,e.id);"skipped"===installSkillPackage({name:n,content:t?.content,files:t?.files||[],scope:"global",conflict:"skip"}).status?d++:a++}catch{d++}else d++}return a&&t.onInfo&&t.onInfo(`已从平台自动同步 ${a} 个技能到本地技能库`),{synced:a,skipped:d}}export function readSkillDirForPush(r){const t=n.join(r,"SKILL.md"),i=e.readFileSync(t,"utf8"),l=[];for(const t of s(r))try{const i=n.join(r,t);if(e.statSync(i).size>o)continue;l.push({path:t,content:e.readFileSync(i,"utf8")})}catch{}return{content:i,filename:n.basename(r),files:l}}
package/src/tui/app.js CHANGED
@@ -1 +1 @@
1
- import r,{useState as t,useRef as e,useReducer as n,useEffect as o,useMemo as l}from"react";import{execSync as c}from"node:child_process";import{render as s,Box as u,Text as i,Static as a,useInput as m,useApp as p}from"ink";import{runTurn as g}from"../agent.js";import{fetchUsage as f}from"../api.js";import{toolLabel as d}from"../render.js";import{renderMarkdown as h,looksLikeMarkdown as x}from"../markdown.js";import{renderWelcome as w}from"../banner.js";import{createSession as y,saveSession as b}from"../session.js";import{ensureSessionTitle as k}from"../title.js";import{createCheckpoints as v}from"../checkpoints.js";import{buildExtraTools as $}from"../repl.js";import{buildSystemPrompt as M}from"../context.js";import{closeMcp as T}from"../mcp.js";import{runSlash as E,listCommands as j}from"../commands.js";import{usageStatus as P,fmtReset as A}from"../usage.js";import{expandAtMentions as S,buildUserContent as D,runBangCommand as I,loadHistory as L,appendHistory as C}from"../input.js";import{listWorkspaceFiles as U}from"../executors.js";import{loadAllowRules as _,loadDenyRules as W,matchAllowRules as O,suggestRule as R,addProjectAllowRule as F,riskWarning as K}from"../settings.js";import{PLAN_DENY_REASON as Y,SANDBOX_DENY_REASON as q,sandboxBlocked as B,AUTOMATION_TOOLS as G,automationActionLabel as J,automationDetail as N,PERMISSION_MODES as Q,MODE_ICON as X,MODE_LABEL as z}from"../permissions.js";import{runVerifyLoop as H}from"../autofix.js";import{runAutoFlow as V}from"../autorun.js";const Z=r.createElement,rr=10;function tr(r,t=12){const e=String(r).split("\n");return e.length<=t?r:e.slice(0,t).join("\n")+`\n…(共 ${e.length} 行)`}function er(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}const nr=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];export function App({config:r,cwd:s,session:w,extraTools:lr,mcp:cr,yolo:sr,initialPrompt:ur,version:ir,onExit:ar}){const mr=p(),[pr,gr]=t(s),fr=e(lr),dr=e([]),hr=e(0),xr=e(""),wr=e(w),yr=e(null),br=e(null),kr=e(new Set),vr=e(_(pr)),$r=e(W(pr)),Mr=e(!1),Tr=e(!1),Er=e(!1),jr=e(v()),Pr=e(!!sr),Ar=e(!1),Sr=e("idle"),Dr=e([]),Ir=e({list:[]}),Lr=e(L(pr)),Cr=e(null),Ur=e(""),[,_r]=n(r=>r+1,0),[Wr,Or]=n(r=>r+1,0),[Rr,Fr]=t(0),[Kr,Yr]=t("idle"),[qr,Br]=t({text:"",cursor:0}),[Gr,Jr]=t(null),[Nr,Qr]=t({prompt:0,completion:0}),[Xr,zr]=t(0),[Hr,Vr]=t([]),[Zr,rt]=t(0),[tt,et]=t(sr?"auto":"manual"),nt=()=>Mr.current?"plan":Tr.current?"sandbox":Pr.current?"auto":Er.current?"acceptEdits":"manual",[ot,lt]=t(""),ct=e(null),[st,ut]=t(0),[it,at]=t(null),mt=e(null),[pt,gt]=t(0),[ft,dt]=t(!1),ht=l(()=>j(pr),[pr]),[xt,wt]=t(0),[yt,bt]=t(!1),kt=l(()=>{try{return U(pr)}catch{return[]}},[pr]),[vt,$t]=t(!1),[Mt,Tt]=t(0),Et=e(0),jt=(r,t)=>{dr.current.push({id:hr.current++,kind:r,text:t}),Or()},Pt=()=>{if(xr.current){const t=xr.current,e=!1!==r.markdown&&x(t);jt("assistant",e?h(t):t),xr.current=""}},At=r=>{Sr.current=r?"busy":"idle",Yr(Sr.current)};o(()=>{if("busy"!==Kr)return;const r=setInterval(()=>ut(r=>(r+1)%nr.length),80);return()=>clearInterval(r)},[Kr]);const St=async()=>{if(r.apiKey)try{const t=await f(r.baseUrl,r.apiKey);mt.current=t,at(t)}catch{}};o(()=>{St();const r=setInterval(()=>{St()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void lt("");const t=()=>{try{const t=c(r.statusLine,{cwd:pr,timeout:3e3,stdio:["ignore","pipe","ignore"]}).toString().split("\n")[0].slice(0,80);lt(t)}catch{lt("")}};t();const e=setInterval(t,5e3);return()=>clearInterval(e)},[r.statusLine]),o(()=>()=>{ct.current&&clearInterval(ct.current)},[]);const Dt=e(""),It={onDelta:r=>{Dt.current&&(Dt.current=""),xr.current+=r,_r()},onReasoning:r=>{Dt.current=(Dt.current+r).slice(-400),_r()},onToolStart:(r,t)=>{Dt.current="",Pt(),jt("tool",d(r,t))},onToolResult:(r,t)=>jt("toolresult",function(r,t=200){const e=String(r).split("\n")[0];return e.length>t?e.slice(0,t)+"…":e}(t)),onFileDiff:r=>jt("diff",r),onSkip:(r,t)=>jt("skip",d(r,t)+" — 已跳过"),onInfo:r=>{Pt(),jt("info",r)},onTodo:r=>Vr(r),onDecision:r=>{Pt();const t=(r.alternatives||[]).length?` · ${r.alternatives.length} 备选`:"",e="draft"===r.status?"(待确认草稿)":"";jt("info",`◆ 已记录决策:${r.title}${t}${e}`)},onTurnEnd:()=>{Dt.current="",Pt()}},Lt={request:(r,t,e,n)=>{if($r.current.length&&O($r.current,r,t))return Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions 查看或移除)。"});if(Mr.current&&!n)return Promise.resolve({allow:!1,reason:Y});if(Tr.current&&B(r))return Promise.resolve({allow:!1,reason:q});if(n||Pr.current||kr.current.has(r)||O(vr.current,r,t))return Promise.resolve({allow:!0});if(Er.current&&("write_file"===r||"edit_file"===r))return Promise.resolve({allow:!0});const o=G.has(r);return new Promise(e=>{br.current={name:r,args:t,resolve:e,consent:o},Jr({label:o?J(r,t):d(r,t),detail:o?N(r,t):"",name:r,args:t,consent:o,risk:K(r,t)})})},isYolo:()=>Pr.current,setYolo:r=>{Pr.current=!!r,et(nt())},setPlanMode:r=>{Mr.current=!!r,et(nt())},isPlanMode:()=>Mr.current,setSandbox:r=>{Tr.current=!!r,et(nt())},isSandbox:()=>Tr.current,setAcceptEdits:r=>{Er.current=!!r,et(nt())},isAcceptEdits:()=>Er.current,setMode:r=>{Pr.current="auto"===r,Er.current="acceptEdits"===r,Mr.current="plan"===r,Tr.current="sandbox"===r,et(r)},getMode:()=>nt(),reloadRules:()=>{vr.current=_(pr),$r.current=W(pr)}},Ct=()=>{const r=Dr.current.shift();return rt(Dr.current.length),null==r?null:r},Ut=()=>{k(wr.current,r).then(r=>{r&&b(wr.current)}).catch(()=>{})},_t=async t=>{const e=P(mt.current);if(e&&"exhausted"===e.level)return void jt("info",`✗ Token 用量窗口(${e.label})已用尽,暂无法继续。${A(e.resetAtEpochMs)}`);const{text:n,images:o}=await S(t,pr,r),l=jr.current.snapshot({session:wr.current,label:t});wr.current.messages.push({role:"user",content:D(n,o)}),Ut(),At(!0),yr.current=new AbortController,wr.current.compaction||(wr.current.compaction={});const c=await g({config:r,messages:wr.current.messages,permissions:Lt,cwd:pr,extraTools:fr.current,events:It,signal:yr.current.signal,todoState:Ir.current,compaction:wr.current.compaction,pullQueued:Ct,fileJournal:l});return yr.current=null,Pt(),c?.usage&&Qr(r=>({prompt:r.prompt+c.usage.prompt,completion:r.completion+c.usage.completion})),c?.lastPromptTokens&&(zr(c.lastPromptTokens),wr.current.lastPromptTokens=c.lastPromptTokens),b(wr.current),Ut(),(c?.aborted||c?.error)&&Dr.current.length&&(Dr.current=[],rt(0),jt("info","已清空排队消息")),At(!1),St(),c},Wt=()=>{ar?.(),mr.exit()},Ot=async t=>{const e=t.trim();if(Br({text:"",cursor:0}),Cr.current=null,e){if(C(pr,e),Lr.current[Lr.current.length-1]!==e&&Lr.current.push(e),e.startsWith("/"))return(async t=>{const e={version:ir,config:r,cwd:pr,usage:Nr,permissions:Lt,extraTools:fr.current,session:wr.current,checkpoints:jr.current,print:r=>jt("info",String(r))},n=await E(t,e);if("exit"===n.action)return Wt();if("clear"===n.action&&(wr.current=y({cwd:pr,model:r.model,messages:[wr.current.messages[0]]}),Ir.current.list=[],jr.current.list.length=0,Vr([]),zr(0),dr.current=[],xr.current="",Fr(r=>r+1),jt("info","已开启新会话(旧会话仍可 --resume 恢复)")),"refresh"===n.action&&(Ir.current.list=[],Vr([]),zr(0)),"autofix"===n.action&&await H({command:n.command,maxIters:n.maxIters,cwd:pr,runTurnOnce:r=>_t(r),emit:r=>jt("info",r)}),"autorun"===n.action&&await V({goal:n.goal,config:r,permissions:Lt,cwd:pr,runTurnOnce:r=>_t(r),emit:r=>jt("info",r)}),"cd"===n.action){vr.current=_(n.cwd),$r.current=W(n.cwd),T(cr?.clients);const t=await $(r,n.cwd);fr.current=t.extraTools,cr&&(cr.clients=t.clients),wr.current=y({cwd:n.cwd,model:r.model,messages:[{role:"system",content:M(n.cwd,{memory:!1!==r.memoryEnabled})}]}),r.sessionId=wr.current.id,Ir.current.list=[],jr.current.list.length=0,Vr([]),zr(0),dr.current=[],xr.current="",Fr(r=>r+1),gr(n.cwd),jt("info","已切换到 "+n.cwd+"(新会话,旧会话已保存)")}"loop"===n.action&&(ct.current&&(clearInterval(ct.current),ct.current=null),n.stop?jt("info","已停止循环。"):(jt("info",`已开始循环:每 ${n.seconds}s 运行「${n.prompt}」(/loop off 停止)`),ct.current=setInterval(()=>{"busy"!==Sr.current&&(jt("user",n.prompt),_t(n.prompt))},1e3*n.seconds))),n.prompt&&("busy"===Sr.current?(Dr.current.push(n.prompt),rt(Dr.current.length)):(jt("user",n.prompt),await _t(n.prompt)))})(e);if(e.startsWith("!")&&e.length>1)return(async r=>{if("busy"===Sr.current)return void jt("info","正在处理上一条,! 命令请稍候再试");jt("user","! "+r);const{output:t,record:e}=await I(r,pr);jt("toolresult",tr(t)),wr.current.messages.push({role:"user",content:e}),b(wr.current)})(e.slice(1).trim());if("busy"===Sr.current)return jt("user",e),Dr.current.push(e),void rt(Dr.current.length);jt("user",e),await _t(e)}};o(()=>{!Ar.current&&ur&&(Ar.current=!0,Ot(ur))},[]);const Rt=/^\/(\S*)$/.exec(qr.text),Ft=Rt?Rt[1]:null,Kt=null==Ft?[]:ht.filter(r=>r.name.toLowerCase().startsWith(Ft.toLowerCase())),Yt=Kt.length>0&&!ft,qt=Kt.length<=rr?0:Math.max(0,Math.min(pt-Math.floor(5),Kt.length-rr)),Bt=Kt.slice(qt,qt+rr);o(()=>{gt(0),dt(!1)},[Ft]);const Gt=null==Ft?/(?:^|\s)@([^\s@]*)$/.exec(qr.text):null,Jt=Gt?Gt[1]:null,Nt=null==Jt?[]:(()=>{const r=Jt.toLowerCase(),t=[];for(const e of kt){const n=e.toLowerCase(),o=n.split("/").pop();let l=-1;if(r?o.startsWith(r)?l=3:o.includes(r)?l=2:n.includes(r)&&(l=1):l=0,l>=0&&t.push({f:e,rank:l}),!r&&t.length>=50)break}return t.sort((r,t)=>t.rank-r.rank||r.f.length-t.f.length||r.f.localeCompare(t.f)),t.slice(0,50).map(r=>r.f)})(),Qt=Nt.length>0&&!yt,Xt=Nt.length<=rr?0:Math.max(0,Math.min(xt-Math.floor(5),Nt.length-rr)),zt=Nt.slice(Xt,Xt+rr);o(()=>{wt(0),bt(!1)},[Jt]);const Ht=l(()=>{const r=new Set,t=[];for(let e=Lr.current.length-1;e>=0&&t.length<50;e--){const n=Lr.current[e];n&&!r.has(n)&&(r.add(n),t.push(n))}return t},[vt]),Vt=Ht.length<=rr?0:Math.max(0,Math.min(Mt-Math.floor(5),Ht.length-rr)),Zt=Ht.slice(Vt,Vt+rr);m((t,e)=>{if(br.current){const r=br.current,n=t=>{br.current=null,Jr(null),r.resolve(t)};if("y"===t||"1"===t)return n({allow:!0});if("a"===t||"2"===t)return kr.current.add(r.name),n({allow:!0});if("3"===t)return n({allow:!1,reason:"用户选择了「否」,该操作未执行。"});if("A"===t){kr.current.add(r.name);const t=R(r.name,r.args);try{F(pr,t),vr.current=_(pr),jt("info",`已写入允许规则:${t} → .huicai/settings.json`)}catch(r){jt("info",`规则写入失败(本会话仍生效):${r.message}`)}return n({allow:!0})}return"n"===t||e.escape||e.return?n({allow:!1,reason:"用户拒绝了该操作。"}):void 0}if(vt){if(!Ht.length)return void $t(!1);if(e.upArrow)return Tt(r=>(r-1+Ht.length)%Ht.length);if(e.downArrow)return Tt(r=>(r+1)%Ht.length);if(e.pageUp)return Tt(r=>Math.max(0,r-rr));if(e.pageDown)return Tt(r=>Math.min(Ht.length-1,r+rr));if(e.return){const r=Ht[Math.min(Mt,Ht.length-1)]||"";return $t(!1),Cr.current=null,Br({text:r,cursor:r.length})}return e.escape?$t(!1):void 0}if(e.tab&&e.shift){const t=Q[(Q.indexOf(Lt.getMode())+1)%Q.length];return Lt.setMode(n=t),r.planMode="plan"===n,r.sandbox="sandbox"===n,void jt("info",`对话模式 → ${X[n]} ${z[n]}`+("auto"===n?"(已开放所有权限,任何命令免审批)":""))}var n;if(Yt){const r=()=>Kt[Math.min(pt,Kt.length-1)];if(e.upArrow)return gt(r=>(r-1+Kt.length)%Kt.length);if(e.downArrow)return gt(r=>(r+1)%Kt.length);if(e.pageUp)return gt(r=>Math.max(0,r-rr));if(e.pageDown)return gt(r=>Math.min(Kt.length-1,r+rr));if(e.tab){const t=r().name;return Br({text:"/"+t+" ",cursor:t.length+2})}if(e.return)return void Ot("/"+r().name);if(e.escape)return dt(!0)}if(Qt){const r=()=>Nt[Math.min(xt,Nt.length-1)];if(e.upArrow)return wt(r=>(r-1+Nt.length)%Nt.length);if(e.downArrow)return wt(r=>(r+1)%Nt.length);if(e.pageUp)return wt(r=>Math.max(0,r-rr));if(e.pageDown)return wt(r=>Math.min(Nt.length-1,r+rr));if(e.tab||e.return){const t=r(),e=qr.text.length-(Jt?Jt.length:0)-1,n=qr.text.slice(0,e+1)+t+" ";return Br({text:n,cursor:n.length})}if(e.escape)return bt(!0)}if(e.escape){if("busy"===Sr.current&&yr.current)return yr.current.abort(),void jt("info","已请求中断(Esc)");const r=Date.now();return r-Et.current<600&&Lr.current.length?(Et.current=0,Tt(0),void $t(!0)):void(Et.current=r)}if(e.ctrl&&"c"===t)"busy"===Sr.current&&yr.current?(yr.current.abort(),jt("info","已请求中断")):Wt();else if(e.return)Ot(qr.text);else{if(e.upArrow)return(()=>{const r=Lr.current;if(!r.length)return;null==Cr.current?(Ur.current=qr.text,Cr.current=r.length-1):Cr.current>0&&Cr.current--;const t=r[Cr.current]??"";Br({text:t,cursor:t.length})})();if(e.downArrow)return(()=>{if(null==Cr.current)return;const r=Lr.current;if(Cr.current<r.length-1){Cr.current++;const t=r[Cr.current];Br({text:t,cursor:t.length})}else Cr.current=null,Br({text:Ur.current,cursor:Ur.current.length})})();if(e.leftArrow)return Br(r=>({...r,cursor:Math.max(0,r.cursor-1)}));if(e.rightArrow)return Br(r=>({...r,cursor:Math.min(r.text.length,r.cursor+1)}));if(e.ctrl&&"a"===t)return Br(r=>({...r,cursor:0}));if(e.ctrl&&"e"===t)return Br(r=>({...r,cursor:r.text.length}));if(e.ctrl&&"u"===t)return Br(r=>({text:r.text.slice(r.cursor),cursor:0}));if(e.ctrl&&"k"===t)return Br(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(e.ctrl&&"w"===t)return Br(r=>{const t=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:t+r.text.slice(r.cursor),cursor:t.length}});if(e.backspace||e.delete)return Cr.current=null,Br(r=>r.cursor>0?{text:r.text.slice(0,r.cursor-1)+r.text.slice(r.cursor),cursor:r.cursor-1}:r);if(t&&!e.ctrl&&!e.meta){Cr.current=null;const r=t.replace(/\r\n?/g,"\n");Br(t=>({text:t.text.slice(0,t.cursor)+r+t.text.slice(t.cursor),cursor:t.cursor+r.length}))}}});const re=l(()=>dr.current.slice(),[Wr]),te=pr.split("/").filter(Boolean).pop()||pr,ee=qr.text.slice(0,qr.cursor),ne=qr.text[qr.cursor]??" ",oe=qr.text.slice(qr.cursor+1),le=Math.max(8,process.stdout.columns||80),ce=P(it),se=ce?"exhausted"===ce.level?`⛔ ${ce.label}用量已用尽${A(ce.resetAtEpochMs)?" · "+A(ce.resetAtEpochMs):""}`:`⚠ ${ce.label}用量 ${Math.round(100*ce.pct)}%(${er(ce.used)}/${er(ce.limit)})`:"";return Z(u,{flexDirection:"column"},Z(a,{key:Rr,items:re},r=>Z(or,{key:r.id,item:r})),Dt.current?Z(u,null,Z(i,{color:"gray",italic:!0},"✦ 思考 "+Dt.current.replace(/\n/g," ").slice(-160))):null,xr.current?Z(u,{marginTop:0},Z(i,null,xr.current)):null,Hr.length?Z(u,{flexDirection:"column",paddingLeft:2,marginTop:1},...Hr.map((r,t)=>Z(i,{key:t,color:"completed"===r.status?"green":"in_progress"===r.status?"cyan":"gray",dimColor:"completed"===r.status,bold:"in_progress"===r.status},("completed"===r.status?"☑ ":"in_progress"===r.status?"◐ ":"☐ ")+r.content))):null,"busy"!==Kr||xr.current?null:Z(u,null,Z(i,{color:"cyan"},nr[st]+" 思考中…"),Z(i,{color:"gray"},"(Esc 中断 · 输入回车可排队补充)")),Gr?Z(u,{flexDirection:"column",borderStyle:"round",borderColor:"yellow",paddingX:1,marginTop:1},Z(i,{color:"yellow",bold:!0},Gr.consent?"● 是否继续?"+Gr.label:"● 需要授权:"+Gr.label),Gr.risk?Z(i,{color:"red",bold:!0},"⚠ "+Gr.risk):null,Gr.consent&&Gr.detail?Z(i,{color:"gray"},tr(Gr.detail,6)):null,"run_bash"===Gr.name?Z(i,{color:"gray"},"$ "+(Gr.args.command||"")):null,"fetch_url"===Gr.name?Z(i,{color:"gray"},"GET "+(Gr.args.url||"")):null,Z(u,{marginTop:1}),...Gr.consent?[Z(u,{key:"c1"},Z(i,{color:"green",bold:!0}," [1] "),Z(i,null,"是,继续")),Z(u,{key:"c2"},Z(i,{color:"cyan",bold:!0}," [2] "),Z(i,null,"是,并在本次对话中后续相同操作不再询问")),Z(u,{key:"c3"},Z(i,{color:"gray",bold:!0}," [3] "),Z(i,{color:"gray"},"否(或按 Esc / 回车)"))]:[Z(u,{key:"a1"},Z(i,{color:"green",bold:!0}," [y] "),Z(i,null,"允许一次")),Z(u,{key:"a2"},Z(i,{color:"cyan",bold:!0}," [a] "),Z(i,null,"本次对话都允许")),Z(u,{key:"a3"},Z(i,{color:"blue",bold:!0}," [A] "),Z(i,null,"永久允许(写入项目设置)")),Z(u,{key:"a4"},Z(i,{color:"gray",bold:!0}," [n] "),Z(i,{color:"gray"},"拒绝(或按 Esc / 回车)"))]):null,Yt?Z(u,{flexDirection:"column",marginTop:1},Z(i,{color:"gray"},Kt.length>rr?` 斜杠命令 ${qt+1}-${qt+Bt.length}/${Kt.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · Tab 补全 · ⏎ 执行 · Esc 关闭`:" 斜杠命令 · ↑↓ 选择 · Tab 补全 · ⏎ 执行 · Esc 关闭"),...Bt.map((r,t)=>{const e=qt+t;return Z(u,{key:r.name},Z(i,{color:"cyan",bold:e===pt},(e===pt?" ❯ ":" ")+"/"+r.name+(r.args?" "+r.args:"")),Z(i,{color:"gray"}," "+String(r.desc||"").replace(/\s+/g," ").slice(0,46)))})):null,Qt?Z(u,{flexDirection:"column",marginTop:1},Z(i,{color:"gray"},Nt.length>rr?` @ 引用文件 ${Xt+1}-${Xt+zt.length}/${Nt.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · Tab/⏎ 插入 · Esc 关闭`:" @ 引用文件 · ↑↓ 选择 · Tab/⏎ 插入 · Esc 关闭"),...zt.map((r,t)=>{const e=Xt+t;return Z(u,{key:r},Z(i,{color:"cyan",bold:e===xt},(e===xt?" ❯ ":" ")+"@"+r))})):null,vt&&Ht.length?Z(u,{flexDirection:"column",marginTop:1},Z(i,{color:"gray"},Ht.length>rr?` 历史记录 ${Vt+1}-${Vt+Zt.length}/${Ht.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · ⏎ 填入 · Esc 关闭`:" 历史记录 · ↑↓ 选择 · ⏎ 填入 · Esc 关闭"),...Zt.map((r,t)=>{const e=Vt+t;return Z(u,{key:e},Z(i,{color:"cyan",bold:e===Mt},(e===Mt?" ❯ ":" ")+r.replace(/\s+/g," ").slice(0,le-6)))})):null,Z(u,{marginTop:Yt||Qt||vt?0:1},Z(i,{color:"blue"},"─".repeat(le))),Z(u,null,Z(i,{color:"busy"===Kr?"yellow":"cyan"},"busy"===Kr?"⧗ ":"› "),Z(i,null,ee),Z(i,{inverse:!0},ne),Z(i,null,oe)),Z(i,{color:"blue"},"─".repeat(le)),Z(u,{justifyContent:"space-between"},Z(i,{color:"gray"},(ot?ot+" · ":"")+`${te} · ${r.model} · ${X[tt]} ${z[tt]}`+(Zr?` · 排队 ${Zr}`:"")+" · ⇧Tab 模式 · /help"),se?Z(i,{color:"exhausted"===ce.level?"red":"yellow",bold:!0},se):Z(i,null,"")))}function or({item:r}){const{kind:t,text:e}=r;if("user"===t)return Z(u,{marginTop:1},Z(i,{color:"cyan",bold:!0},"› "),Z(i,{bold:!0},e));if("assistant"===t)return Z(u,null,Z(i,null,e));if("diff"===t)return Z(u,{marginLeft:2},Z(i,null,e));if("tool"===t)return Z(i,{color:"cyan"}," ⏺ "+e);if("toolresult"===t)return Z(i,{color:"gray"}," "+e);if("skip"===t)return Z(i,{color:"gray"}," ⊘ "+e);if("info"===t){const r=/✗|失败|错误|error/i.test(e);return Z(i,{color:r?"red":"yellow"},(r?" ✗ ":" · ")+e.replace(/^✗\s*/,""))}return Z(i,null,e)}export async function runInk({config:r,cwd:t,session:e,yolo:n,initialPrompt:o,version:l}){process.stdout.write(w({model:r.model,cwd:t,version:l,platform:/^(agent|workflow|datahuman)-/.test(String(r.model||"")),yolo:n,consult:r.consult?r.consultLabel||r.consult:null,ink:!0})+"\n");const{extraTools:c,clients:u}=await $(r,t),i={clients:u},{waitUntilExit:a}=s(Z(App,{config:r,cwd:t,session:e,extraTools:c,mcp:i,yolo:n,initialPrompt:o,version:l,onExit:null}));await a(),T(i.clients)}
1
+ import r,{useState as t,useRef as e,useReducer as n,useEffect as o,useMemo as l}from"react";import{execSync as c}from"node:child_process";import{render as s,Box as u,Text as i,Static as a,useInput as m,useApp as p}from"ink";import{runTurn as f}from"../agent.js";import{fetchUsage as g}from"../api.js";import{toolLabel as d}from"../render.js";import{renderMarkdown as h,looksLikeMarkdown as w}from"../markdown.js";import{renderWelcome as x}from"../banner.js";import{createSession as y,saveSession as b}from"../session.js";import{ensureSessionTitle as k}from"../title.js";import{createCheckpoints as v}from"../checkpoints.js";import{buildExtraTools as $}from"../repl.js";import{buildSystemPrompt as M}from"../context.js";import{closeMcp as T}from"../mcp.js";import{runSlash as j,listCommands as E}from"../commands.js";import{usageStatus as A,fmtReset as P}from"../usage.js";import{handleAccountError as S}from"../account.js";import{pricingUrl as D}from"../errkind.js";import{expandAtMentions as I,buildUserContent as L,runBangCommand as C,loadHistory as U,appendHistory as _}from"../input.js";import{listWorkspaceFiles as W}from"../executors.js";import{loadAllowRules as q,loadDenyRules as O,matchAllowRules as Q,suggestRule as R,addProjectAllowRule as F,riskWarning as K}from"../settings.js";import{PLAN_DENY_REASON as Y,SANDBOX_DENY_REASON as B,sandboxBlocked as G,AUTOMATION_TOOLS as H,automationActionLabel as J,automationDetail as N,PERMISSION_MODES as X,MODE_ICON as z,MODE_LABEL as V}from"../permissions.js";import{runVerifyLoop as Z}from"../autofix.js";import{runAutoFlow as rr}from"../autorun.js";import{acquireWakeLock as tr,releaseWakeLock as er}from"../presence.js";import{notifyTurnDone as nr}from"../notify.js";const or=r.createElement,lr=10;function cr(r,t=12){const e=String(r).split("\n");return e.length<=t?r:e.slice(0,t).join("\n")+`\n…(共 ${e.length} 行)`}function sr(r){return(r=Number(r)||0)>=1e3?(r/1e3).toFixed(1)+"k":String(r)}const ur=["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];export function App({config:r,cwd:s,session:x,extraTools:ar,mcp:mr,yolo:pr,initialPrompt:fr,version:gr,onExit:dr,exitState:hr={},injectQueue:wr=[]}){const xr=p(),[yr,br]=t(s),kr=e(ar),vr=e([]),$r=e(0),Mr=e(""),Tr=e(x),jr=e(null),Er=e(null),Ar=e(new Set),Pr=e(q(yr)),Sr=e(O(yr)),Dr=e(!1),Ir=e(!1),Lr=e(!1),Cr=e(v()),Ur=e(!!pr),_r=e(!1),Wr=e("idle"),qr=e([]),Or=e({list:[]}),Qr=e(U(yr)),Rr=e(null),Fr=e(""),[,Kr]=n(r=>r+1,0),[Yr,Br]=n(r=>r+1,0),[Gr,Hr]=t(0),[Jr,Nr]=t("idle"),[Xr,zr]=t({text:"",cursor:0}),[Vr,Zr]=t(null),[rt,tt]=t({prompt:0,completion:0}),[et,nt]=t(0),[ot,lt]=t([]),[ct,st]=t(0),[ut,it]=t(pr?"auto":"manual"),at=()=>Dr.current?"plan":Ir.current?"sandbox":Ur.current?"auto":Lr.current?"acceptEdits":"manual",[mt,pt]=t(""),ft=e(null),[gt,dt]=t(0),[ht,wt]=t(null),xt=e(null),[yt,bt]=t(0),[kt,vt]=t(!1),$t=l(()=>E(yr),[yr]),[Mt,Tt]=t(0),[jt,Et]=t(!1),At=l(()=>{try{return W(yr)}catch{return[]}},[yr]),[Pt,St]=t(!1),[Dt,It]=t(0),Lt=e(0),Ct=(r,t)=>{vr.current.push({id:$r.current++,kind:r,text:t}),Br()},Ut=()=>{if(Mr.current){const t=Mr.current,e=!1!==r.markdown&&w(t);Ct("assistant",e?h(t):t),Mr.current=""}},_t=r=>{Wr.current=r?"busy":"idle",Nr(Wr.current)};o(()=>{if("busy"!==Jr)return;const r=setInterval(()=>dt(r=>(r+1)%ur.length),80);return()=>clearInterval(r)},[Jr]);const Wt=async()=>{if(r.apiKey)try{const t=await g(r.baseUrl,r.apiKey);xt.current=t,wt(t)}catch{}};o(()=>{Wt();const r=setInterval(()=>{Wt()},18e4);return()=>clearInterval(r)},[]),o(()=>{if(!r.statusLine)return void pt("");const t=()=>{try{const t=c(r.statusLine,{cwd:yr,timeout:3e3,stdio:["ignore","pipe","ignore"]}).toString().split("\n")[0].slice(0,80);pt(t)}catch{pt("")}};t();const e=setInterval(t,5e3);return()=>clearInterval(e)},[r.statusLine]),o(()=>()=>{ft.current&&clearInterval(ft.current)},[]);const qt=e(""),Ot={onDelta:r=>{qt.current&&(qt.current=""),Mr.current+=r,Kr()},onReasoning:r=>{qt.current=(qt.current+r).slice(-400),Kr()},onToolStart:(r,t)=>{qt.current="",Ut(),Ct("tool",d(r,t))},onToolResult:(r,t)=>Ct("toolresult",function(r,t=200){const e=String(r).split("\n")[0];return e.length>t?e.slice(0,t)+"…":e}(t)),onFileDiff:r=>Ct("diff",r),onSkip:(r,t)=>Ct("skip",d(r,t)+" — 已跳过"),onInfo:r=>{Ut(),Ct("info",r)},onTodo:r=>lt(r),onDecision:r=>{Ut();const t=(r.alternatives||[]).length?` · ${r.alternatives.length} 备选`:"",e="draft"===r.status?"(待确认草稿)":"";Ct("info",`◆ 已记录决策:${r.title}${t}${e}`)},onTurnEnd:()=>{qt.current="",Ut()}},Qt={request:(r,t,e,n)=>{if(Sr.current.length&&Q(Sr.current,r,t))return Promise.resolve({allow:!1,reason:"该操作被拒绝规则阻止(/permissions 查看或移除)。"});if(Dr.current&&!n)return Promise.resolve({allow:!1,reason:Y});if(Ir.current&&G(r))return Promise.resolve({allow:!1,reason:B});if(n||Ur.current||Ar.current.has(r)||Q(Pr.current,r,t))return Promise.resolve({allow:!0});if(Lr.current&&("write_file"===r||"edit_file"===r))return Promise.resolve({allow:!0});const o=H.has(r);return new Promise(e=>{Er.current={name:r,args:t,resolve:e,consent:o},Zr({label:o?J(r,t):d(r,t),detail:o?N(r,t):"",name:r,args:t,consent:o,risk:K(r,t)})})},isYolo:()=>Ur.current,setYolo:r=>{Ur.current=!!r,it(at())},setPlanMode:r=>{Dr.current=!!r,it(at())},isPlanMode:()=>Dr.current,setSandbox:r=>{Ir.current=!!r,it(at())},isSandbox:()=>Ir.current,setAcceptEdits:r=>{Lr.current=!!r,it(at())},isAcceptEdits:()=>Lr.current,setMode:r=>{Ur.current="auto"===r,Lr.current="acceptEdits"===r,Dr.current="plan"===r,Ir.current="sandbox"===r,it(r)},getMode:()=>at(),reloadRules:()=>{Pr.current=q(yr),Sr.current=O(yr)}},Rt=()=>{if(wr.length)return wr.shift();const r=qr.current.shift();return st(qr.current.length),null==r?null:r},Ft=()=>{k(Tr.current,r).then(r=>{r&&b(Tr.current)}).catch(()=>{})},Kt=async t=>{const e=A(xt.current);if(e&&"exhausted"===e.level){const t=D(r.baseUrl);return void Ct("info",`✗ Token 用量窗口(${e.label})已用尽,暂无法继续。${P(e.resetAtEpochMs)}`+(t?`\n 升级会员提高上限 → ${t}`:""))}const{text:n,images:o}=await I(t,yr,r),l=Cr.current.snapshot({session:Tr.current,label:t});Tr.current.messages.push({role:"user",content:L(n,o)}),Ft(),_t(!0),jr.current=new AbortController,Tr.current.compaction||(Tr.current.compaction={});const c="auto"===at()&&!1!==r.wakeLockInAuto?tr({reason:"自动模式任务执行中"}):null,s=Date.now();let u;try{u=await f({config:r,messages:Tr.current.messages,permissions:Qt,cwd:yr,extraTools:kr.current,events:Ot,signal:jr.current.signal,todoState:Or.current,compaction:Tr.current.compaction,pullQueued:Rt,fileJournal:l})}finally{c?.ok&&er()}if(jr.current=null,u?.aborted||u?.error||nr({durationMs:Date.now()-s,cwd:yr,summary:String(t).replace(/\s+/g," ").slice(0,80),enabled:!1!==r.notifications}).catch(()=>{}),Ut(),u?.usage&&tt(r=>({prompt:r.prompt+u.usage.prompt,completion:r.completion+u.usage.completion})),u?.lastPromptTokens&&(nt(u.lastPromptTokens),Tr.current.lastPromptTokens=u.lastPromptTokens),b(Tr.current),Ft(),(u?.aborted||u?.error)&&qr.current.length&&(qr.current=[],st(0),Ct("info","已清空排队消息")),_t(!1),u?.error){let t="";const e=S(u,r,r=>{t+=r});if(t.trim()&&Ct("info",t.trim()),"auth"===e)return hr.relogin=!0,Yt(),u}return Wt(),u},Yt=()=>{dr?.(),xr.exit()},Bt=async t=>{const e=t.trim();if(zr({text:"",cursor:0}),Rr.current=null,e){if(_(yr,e),Qr.current[Qr.current.length-1]!==e&&Qr.current.push(e),e.startsWith("/"))return(async t=>{const e={version:gr,config:r,cwd:yr,usage:rt,permissions:Qt,extraTools:kr.current,session:Tr.current,checkpoints:Cr.current,print:r=>Ct("info",String(r))},n=await j(t,e);if("exit"===n.action)return Yt();if("clear"===n.action&&(Tr.current=y({cwd:yr,model:r.model,messages:[Tr.current.messages[0]]}),Or.current.list=[],Cr.current.list.length=0,lt([]),nt(0),vr.current=[],Mr.current="",Hr(r=>r+1),Ct("info","已开启新会话(旧会话仍可 --resume 恢复)")),"refresh"===n.action&&(Or.current.list=[],lt([]),nt(0)),"autofix"===n.action&&await Z({command:n.command,maxIters:n.maxIters,cwd:yr,runTurnOnce:r=>Kt(r),emit:r=>Ct("info",r)}),"autorun"===n.action&&await rr({goal:n.goal,config:r,permissions:Qt,cwd:yr,runTurnOnce:r=>Kt(r),emit:r=>Ct("info",r)}),"cd"===n.action){Pr.current=q(n.cwd),Sr.current=O(n.cwd),T(mr?.clients);const t=await $(r,n.cwd,{queueInject:r=>wr.push(r)});kr.current=t.extraTools,mr&&(mr.clients=t.clients),Tr.current=y({cwd:n.cwd,model:r.model,messages:[{role:"system",content:M(n.cwd,{memory:!1!==r.memoryEnabled,config:r})}]}),r.sessionId=Tr.current.id,Or.current.list=[],Cr.current.list.length=0,lt([]),nt(0),vr.current=[],Mr.current="",Hr(r=>r+1),br(n.cwd),Ct("info","已切换到 "+n.cwd+"(新会话,旧会话已保存)")}"loop"===n.action&&(ft.current&&(clearInterval(ft.current),ft.current=null),n.stop?Ct("info","已停止循环。"):(Ct("info",`已开始循环:每 ${n.seconds}s 运行「${n.prompt}」(/loop off 停止)`),ft.current=setInterval(()=>{"busy"!==Wr.current&&(Ct("user",n.prompt),Kt(n.prompt))},1e3*n.seconds))),n.prompt&&("busy"===Wr.current?(qr.current.push(n.prompt),st(qr.current.length)):(Ct("user",n.prompt),await Kt(n.prompt)))})(e);if(e.startsWith("!")&&e.length>1)return(async r=>{if("busy"===Wr.current)return void Ct("info","正在处理上一条,! 命令请稍候再试");Ct("user","! "+r);const{output:t,record:e}=await C(r,yr);Ct("toolresult",cr(t)),Tr.current.messages.push({role:"user",content:e}),b(Tr.current)})(e.slice(1).trim());if("busy"===Wr.current)return Ct("user",e),qr.current.push(e),void st(qr.current.length);Ct("user",e),await Kt(e)}};o(()=>{!_r.current&&fr&&(_r.current=!0,Bt(fr))},[]);const Gt=/^\/(\S*)$/.exec(Xr.text),Ht=Gt?Gt[1]:null,Jt=null==Ht?[]:$t.filter(r=>r.name.toLowerCase().startsWith(Ht.toLowerCase())),Nt=Jt.length>0&&!kt,Xt=Jt.length<=lr?0:Math.max(0,Math.min(yt-Math.floor(5),Jt.length-lr)),zt=Jt.slice(Xt,Xt+lr);o(()=>{bt(0),vt(!1)},[Ht]);const Vt=null==Ht?/(?:^|\s)@([^\s@]*)$/.exec(Xr.text):null,Zt=Vt?Vt[1]:null,re=null==Zt?[]:(()=>{const r=Zt.toLowerCase(),t=[];for(const e of At){const n=e.toLowerCase(),o=n.split("/").pop();let l=-1;if(r?o.startsWith(r)?l=3:o.includes(r)?l=2:n.includes(r)&&(l=1):l=0,l>=0&&t.push({f:e,rank:l}),!r&&t.length>=50)break}return t.sort((r,t)=>t.rank-r.rank||r.f.length-t.f.length||r.f.localeCompare(t.f)),t.slice(0,50).map(r=>r.f)})(),te=re.length>0&&!jt,ee=re.length<=lr?0:Math.max(0,Math.min(Mt-Math.floor(5),re.length-lr)),ne=re.slice(ee,ee+lr);o(()=>{Tt(0),Et(!1)},[Zt]);const oe=l(()=>{const r=new Set,t=[];for(let e=Qr.current.length-1;e>=0&&t.length<50;e--){const n=Qr.current[e];n&&!r.has(n)&&(r.add(n),t.push(n))}return t},[Pt]),le=oe.length<=lr?0:Math.max(0,Math.min(Dt-Math.floor(5),oe.length-lr)),ce=oe.slice(le,le+lr);m((t,e)=>{if(Er.current){const r=Er.current,n=t=>{Er.current=null,Zr(null),r.resolve(t)};if("y"===t||"1"===t)return n({allow:!0});if("a"===t||"2"===t)return Ar.current.add(r.name),n({allow:!0});if("3"===t)return n({allow:!1,reason:"用户选择了「否」,该操作未执行。"});if("A"===t){Ar.current.add(r.name);const t=R(r.name,r.args);try{F(yr,t),Pr.current=q(yr),Ct("info",`已写入允许规则:${t} → .huicai/settings.json`)}catch(r){Ct("info",`规则写入失败(本会话仍生效):${r.message}`)}return n({allow:!0})}return"n"===t||e.escape||e.return?n({allow:!1,reason:"用户拒绝了该操作。"}):void 0}if(Pt){if(!oe.length)return void St(!1);if(e.upArrow)return It(r=>(r-1+oe.length)%oe.length);if(e.downArrow)return It(r=>(r+1)%oe.length);if(e.pageUp)return It(r=>Math.max(0,r-lr));if(e.pageDown)return It(r=>Math.min(oe.length-1,r+lr));if(e.return){const r=oe[Math.min(Dt,oe.length-1)]||"";return St(!1),Rr.current=null,zr({text:r,cursor:r.length})}return e.escape?St(!1):void 0}if(e.tab&&e.shift){const t=X[(X.indexOf(Qt.getMode())+1)%X.length];return Qt.setMode(n=t),r.planMode="plan"===n,r.sandbox="sandbox"===n,void Ct("info",`对话模式 → ${z[n]} ${V[n]}`+("auto"===n?"(已开放所有权限,任何命令免审批)":""))}var n;if(Nt){const r=()=>Jt[Math.min(yt,Jt.length-1)];if(e.upArrow)return bt(r=>(r-1+Jt.length)%Jt.length);if(e.downArrow)return bt(r=>(r+1)%Jt.length);if(e.pageUp)return bt(r=>Math.max(0,r-lr));if(e.pageDown)return bt(r=>Math.min(Jt.length-1,r+lr));if(e.tab){const t=r().name;return zr({text:"/"+t+" ",cursor:t.length+2})}if(e.return)return void Bt("/"+r().name);if(e.escape)return vt(!0)}if(te){const r=()=>re[Math.min(Mt,re.length-1)];if(e.upArrow)return Tt(r=>(r-1+re.length)%re.length);if(e.downArrow)return Tt(r=>(r+1)%re.length);if(e.pageUp)return Tt(r=>Math.max(0,r-lr));if(e.pageDown)return Tt(r=>Math.min(re.length-1,r+lr));if(e.tab||e.return){const t=r(),e=Xr.text.length-(Zt?Zt.length:0)-1,n=Xr.text.slice(0,e+1)+t+" ";return zr({text:n,cursor:n.length})}if(e.escape)return Et(!0)}if(e.escape){if("busy"===Wr.current&&jr.current)return jr.current.abort(),void Ct("info","已请求中断(Esc)");const r=Date.now();return r-Lt.current<600&&Qr.current.length?(Lt.current=0,It(0),void St(!0)):void(Lt.current=r)}if(e.ctrl&&"c"===t)"busy"===Wr.current&&jr.current?(jr.current.abort(),Ct("info","已请求中断")):Yt();else if(e.return)Bt(Xr.text);else{if(e.upArrow)return(()=>{const r=Qr.current;if(!r.length)return;null==Rr.current?(Fr.current=Xr.text,Rr.current=r.length-1):Rr.current>0&&Rr.current--;const t=r[Rr.current]??"";zr({text:t,cursor:t.length})})();if(e.downArrow)return(()=>{if(null==Rr.current)return;const r=Qr.current;if(Rr.current<r.length-1){Rr.current++;const t=r[Rr.current];zr({text:t,cursor:t.length})}else Rr.current=null,zr({text:Fr.current,cursor:Fr.current.length})})();if(e.leftArrow)return zr(r=>({...r,cursor:Math.max(0,r.cursor-1)}));if(e.rightArrow)return zr(r=>({...r,cursor:Math.min(r.text.length,r.cursor+1)}));if(e.ctrl&&"a"===t)return zr(r=>({...r,cursor:0}));if(e.ctrl&&"e"===t)return zr(r=>({...r,cursor:r.text.length}));if(e.ctrl&&"u"===t)return zr(r=>({text:r.text.slice(r.cursor),cursor:0}));if(e.ctrl&&"k"===t)return zr(r=>({text:r.text.slice(0,r.cursor),cursor:r.cursor}));if(e.ctrl&&"w"===t)return zr(r=>{const t=r.text.slice(0,r.cursor).replace(/\S+\s*$/,"");return{text:t+r.text.slice(r.cursor),cursor:t.length}});if(e.backspace||e.delete)return Rr.current=null,zr(r=>r.cursor>0?{text:r.text.slice(0,r.cursor-1)+r.text.slice(r.cursor),cursor:r.cursor-1}:r);if(t&&!e.ctrl&&!e.meta){Rr.current=null;const r=t.replace(/\r\n?/g,"\n");zr(t=>({text:t.text.slice(0,t.cursor)+r+t.text.slice(t.cursor),cursor:t.cursor+r.length}))}}});const se=l(()=>vr.current.slice(),[Yr]),ue=yr.split("/").filter(Boolean).pop()||yr,ie=Xr.text.slice(0,Xr.cursor),ae=Xr.text[Xr.cursor]??" ",me=Xr.text.slice(Xr.cursor+1),pe=Math.max(8,process.stdout.columns||80),fe=A(ht),ge=fe?"exhausted"===fe.level?`⛔ ${fe.label}用量已用尽${P(fe.resetAtEpochMs)?" · "+P(fe.resetAtEpochMs):""}`:`⚠ ${fe.label}用量 ${Math.round(100*fe.pct)}%(${sr(fe.used)}/${sr(fe.limit)})`:"";return or(u,{flexDirection:"column"},or(a,{key:Gr,items:se},r=>or(ir,{key:r.id,item:r})),qt.current?or(u,null,or(i,{color:"gray",italic:!0},"✦ 思考 "+qt.current.replace(/\n/g," ").slice(-160))):null,Mr.current?or(u,{marginTop:0},or(i,null,Mr.current)):null,ot.length?or(u,{flexDirection:"column",paddingLeft:2,marginTop:1},...ot.map((r,t)=>or(i,{key:t,color:"completed"===r.status?"green":"in_progress"===r.status?"cyan":"gray",dimColor:"completed"===r.status,bold:"in_progress"===r.status},("completed"===r.status?"☑ ":"in_progress"===r.status?"◐ ":"☐ ")+r.content))):null,"busy"!==Jr||Mr.current?null:or(u,null,or(i,{color:"cyan"},ur[gt]+" 思考中…"),or(i,{color:"gray"},"(Esc 中断 · 输入回车可排队补充)")),Vr?or(u,{flexDirection:"column",borderStyle:"round",borderColor:"yellow",paddingX:1,marginTop:1},or(i,{color:"yellow",bold:!0},Vr.consent?"● 是否继续?"+Vr.label:"● 需要授权:"+Vr.label),Vr.risk?or(i,{color:"red",bold:!0},"⚠ "+Vr.risk):null,Vr.consent&&Vr.detail?or(i,{color:"gray"},cr(Vr.detail,6)):null,"run_bash"===Vr.name?or(i,{color:"gray"},"$ "+(Vr.args.command||"")):null,"fetch_url"===Vr.name?or(i,{color:"gray"},"GET "+(Vr.args.url||"")):null,or(u,{marginTop:1}),...Vr.consent?[or(u,{key:"c1"},or(i,{color:"green",bold:!0}," [1] "),or(i,null,"是,继续")),or(u,{key:"c2"},or(i,{color:"cyan",bold:!0}," [2] "),or(i,null,"是,并在本次对话中后续相同操作不再询问")),or(u,{key:"c3"},or(i,{color:"gray",bold:!0}," [3] "),or(i,{color:"gray"},"否(或按 Esc / 回车)"))]:[or(u,{key:"a1"},or(i,{color:"green",bold:!0}," [y] "),or(i,null,"允许一次")),or(u,{key:"a2"},or(i,{color:"cyan",bold:!0}," [a] "),or(i,null,"本次对话都允许")),or(u,{key:"a3"},or(i,{color:"blue",bold:!0}," [A] "),or(i,null,"永久允许(写入项目设置)")),or(u,{key:"a4"},or(i,{color:"gray",bold:!0}," [n] "),or(i,{color:"gray"},"拒绝(或按 Esc / 回车)"))]):null,Nt?or(u,{flexDirection:"column",marginTop:1},or(i,{color:"gray"},Jt.length>lr?` 斜杠命令 ${Xt+1}-${Xt+zt.length}/${Jt.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · Tab 补全 · ⏎ 执行 · Esc 关闭`:" 斜杠命令 · ↑↓ 选择 · Tab 补全 · ⏎ 执行 · Esc 关闭"),...zt.map((r,t)=>{const e=Xt+t;return or(u,{key:r.name},or(i,{color:"cyan",bold:e===yt},(e===yt?" ❯ ":" ")+"/"+r.name+(r.args?" "+r.args:"")),or(i,{color:"gray"}," "+String(r.desc||"").replace(/\s+/g," ").slice(0,46)))})):null,te?or(u,{flexDirection:"column",marginTop:1},or(i,{color:"gray"},re.length>lr?` @ 引用文件 ${ee+1}-${ee+ne.length}/${re.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · Tab/⏎ 插入 · Esc 关闭`:" @ 引用文件 · ↑↓ 选择 · Tab/⏎ 插入 · Esc 关闭"),...ne.map((r,t)=>{const e=ee+t;return or(u,{key:r},or(i,{color:"cyan",bold:e===Mt},(e===Mt?" ❯ ":" ")+"@"+r))})):null,Pt&&oe.length?or(u,{flexDirection:"column",marginTop:1},or(i,{color:"gray"},oe.length>lr?` 历史记录 ${le+1}-${le+ce.length}/${oe.length} · ↑↓ 选择 · PgUp/PgDn 翻页 · ⏎ 填入 · Esc 关闭`:" 历史记录 · ↑↓ 选择 · ⏎ 填入 · Esc 关闭"),...ce.map((r,t)=>{const e=le+t;return or(u,{key:e},or(i,{color:"cyan",bold:e===Dt},(e===Dt?" ❯ ":" ")+r.replace(/\s+/g," ").slice(0,pe-6)))})):null,or(u,{marginTop:Nt||te||Pt?0:1},or(i,{color:"blue"},"─".repeat(pe))),or(u,null,or(i,{color:"busy"===Jr?"yellow":"cyan"},"busy"===Jr?"⧗ ":"› "),or(i,null,ie),or(i,{inverse:!0},ae),or(i,null,me)),or(i,{color:"blue"},"─".repeat(pe)),or(u,{justifyContent:"space-between"},or(i,{color:"gray"},(mt?mt+" · ":"")+`${ue} · ${r.model} · ${z[ut]} ${V[ut]}`+(ct?` · 排队 ${ct}`:"")+" · ⇧Tab 模式 · /help"),ge?or(i,{color:"exhausted"===fe.level?"red":"yellow",bold:!0},ge):or(i,null,"")))}function ir({item:r}){const{kind:t,text:e}=r;if("user"===t)return or(u,{marginTop:1},or(i,{color:"cyan",bold:!0},"› "),or(i,{bold:!0},e));if("assistant"===t)return or(u,null,or(i,null,e));if("diff"===t)return or(u,{marginLeft:2},or(i,null,e));if("tool"===t)return or(i,{color:"cyan"}," ⏺ "+e);if("toolresult"===t)return or(i,{color:"gray"}," "+e);if("skip"===t)return or(i,{color:"gray"}," ⊘ "+e);if("info"===t){const r=/✗|失败|错误|error/i.test(e);return or(i,{color:r?"red":"yellow"},(r?" ✗ ":" · ")+e.replace(/^✗\s*/,""))}return or(i,null,e)}export async function runInk({config:r,cwd:t,session:e,yolo:n,initialPrompt:o,version:l}){process.stdout.write(x({model:r.model,cwd:t,version:l,platform:/^(agent|workflow|datahuman)-/.test(String(r.model||"")),yolo:n,consult:r.consult?r.consultLabel||r.consult:null,ink:!0})+"\n");const c=[],{extraTools:u,clients:i}=await $(r,t,{queueInject:r=>c.push(r),runnerHint:"到点由调度器在该目录自动执行(终端里需有 `huicai daemon` 常驻;桌面端保持应用运行亦可)。"}),a={clients:i},m={},{waitUntilExit:p}=s(or(App,{config:r,cwd:t,session:e,extraTools:u,mcp:a,yolo:n,initialPrompt:o,version:l,onExit:null,exitState:m,injectQueue:c}));return await p(),T(a.clients),m}
package/src/vision.js ADDED
@@ -0,0 +1 @@
1
+ export const PSEUDO_MODEL_RE=/^(agent|workflow|datahuman|squad)-/;export const VISION_MODEL_RE=/(vl|vision|gpt-4o|omni|multimodal|\dv\b)/i;export function modelSeesImages(t){const e=String(t||"").trim();return!(!e||"auto"===e)&&(!!PSEUDO_MODEL_RE.test(e)||VISION_MODEL_RE.test(e))}export function hasImageParts(t){return Array.isArray(t)&&t.some(t=>t&&"image_url"===t.type)}export function messagesHaveImages(t){return Array.isArray(t)&&t.some(t=>hasImageParts(t&&t.content))}export function stripImageParts(t,e="(图片已省略:当前模型不支持读取图片)"){if(!hasImageParts(t))return t;const s=[];let r=0;for(const e of t)e&&"image_url"===e.type?r++:s.push(e);r&&s.push({type:"text",text:e});const a=s.filter(t=>t&&"text"===t.type);return a.length===s.length?a.map(t=>t.text||"").join("\n\n"):s}export function sanitizeMessagesForModel(t,e){if(!Array.isArray(t)||modelSeesImages(e)||!messagesHaveImages(t))return{messages:t,stripped:0};let s=0;return{messages:t.map(t=>hasImageParts(t&&t.content)?(s++,{...t,content:stripImageParts(t.content)}):t),stripped:s}}
package/src/visual.js ADDED
@@ -0,0 +1 @@
1
+ import{streamChat as e,listModels as t}from"./api.js";import{modelSeesImages as n,PSEUDO_MODEL_RE as i}from"./vision.js";let r,o=0;export async function resolveVisionModel(e){const s=String(e?.visionModel||"").trim();if(s)return s;if(void 0!==r&&(r||Date.now()-o<6e4))return r;o=Date.now();try{const o=await t(e.baseUrl,e.apiKey);r=(o||[]).map(e=>e.id).filter(e=>!i.test(e)).find(e=>n(e))||null}catch{r=null}return r}export function resetVisionModelCache(){r=void 0,o=0}export async function describeImage(t,n,i){const r=await resolveVisionModel(t);if(!r||!n)return"";try{const o=await e({baseUrl:t.baseUrl,apiKey:t.apiKey,model:r,messages:[{role:"user",content:[{type:"text",text:i},{type:"image_url",image_url:{url:n}}]}],client:t.client,sessionId:t.sessionId,aux:!0},{},AbortSignal.timeout(12e4));return String(o?.content||"").trim()}catch{return""}}export function createVisualBridge({config:e,queue:t,currentModel:i}){return async function(r,o){const s=(i?i():"")||e.model;if(n(s))return void t([{type:"text",text:String(r||"")},{type:"image_url",image_url:{url:o}}]);const a=await describeImage(e,o,"请客观、详细地描述这张界面截图:整体布局、各区域的可见文字(逐字转录,含按钮/菜单/输入框/报错)、控件的位置与状态。另一个不能看图的模型会据此决定下一步点哪里,所以**位置描述要具体**(如「左上角」「窗口中部偏右的蓝色按钮」)。不要臆测未出现的内容。");t(a?`${r}\n\n【视觉模型转述的截图内容】\n${a}`:`${r}\n\n【提示】当前模型不支持读图,且暂无可用的视觉转述模型(可在配置里设 visionModel),本次截图已跳过。请改用不依赖看图的方式推进:优先 run_native_script 直接指挥应用,或用 run_python 读取/分析截图文件。`)}}