@huicai/cli 2026.721.0 → 2026.814.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -6
- package/bin/huicai.js +24 -4
- package/package.json +2 -2
- package/src/account.js +1 -0
- package/src/advisor.js +1 -1
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/assets-import.js +1 -0
- package/src/autofix.js +1 -1
- package/src/automation-tools.js +1 -0
- package/src/automation.js +1 -1
- package/src/commands.js +1 -1
- package/src/compaction.js +1 -1
- package/src/computer.js +1 -0
- package/src/config.js +1 -1
- package/src/context-report.js +1 -1
- package/src/context.js +1 -1
- package/src/daemon.js +1 -1
- package/src/decisions.js +1 -0
- package/src/errkind.js +1 -0
- package/src/executors.js +1 -1
- package/src/expert-catalog.js +1 -0
- package/src/experts-store.js +1 -0
- package/src/gitflow.js +1 -1
- package/src/mcp.js +1 -1
- package/src/notify.js +1 -0
- package/src/presence.js +1 -0
- package/src/pyruntime.js +1 -1
- package/src/repl.js +1 -1
- package/src/schedule-freq.js +1 -0
- package/src/schedule.js +1 -1
- package/src/session.js +1 -1
- package/src/skills.js +1 -1
- package/src/squad.js +1 -1
- package/src/subagent.js +1 -1
- package/src/title.js +1 -0
- package/src/tools.js +1 -1
- package/src/transcript.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/vision.js +1 -0
- package/src/visual.js +1 -0
package/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
|
|
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)
|
|
@@ -158,10 +195,17 @@ huicai -c -p "继续修" --yolo # 非交互续接(CI)
|
|
|
158
195
|
|
|
159
196
|
### 上下文自动压缩(非破坏式)
|
|
160
197
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
198
|
+
双触发:上一轮**真实 `prompt_tokens`** 超阈值,或(尚无实测值时)本地估算超阈值——估算与 `/context`
|
|
199
|
+
同口径、CJK 感知。**默认阈值 100 万 tokens**(`autoCompactTokens` 或 `/autocompact <N>` 可调,`off` 关闭)。
|
|
200
|
+
|
|
201
|
+
阈值刻意高于多数模型的实际窗口:真正的把关在于**模型报「上下文装不下」时会自动强制压缩一次再重试同一模型**
|
|
202
|
+
(用户无感,每回合至多 3 次),所以小窗口模型不必手动调小阈值,长会话也不会每隔几轮就被摘要打断。
|
|
203
|
+
|
|
204
|
+
**摘要独立存储在会话的 compaction 状态里,原对话消息与页面显示完全不变**——只是发给模型的上下文换成
|
|
205
|
+
「system + 摘要 + 最近几轮」的视图。优先以用户消息为切割边界;单个超长回合(一条提问 + 上百轮工具往返)
|
|
206
|
+
则在回合内切,切点吸附到非工具结果处,两种情况都绝不拆散工具调用与其结果。超大上下文的摘要会按预算
|
|
207
|
+
分块、链式递进(每块并入上一块摘要),不会因摘要请求自身超限而失效。`/compact` 可随时手动压缩
|
|
208
|
+
(同样不影响已显示内容),`/context` 查看当前占用与距阈值的比例。
|
|
165
209
|
|
|
166
210
|
### 平台编排对话(分享页同款,CLI 直连)
|
|
167
211
|
|
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
|
-
|
|
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.
|
|
4
|
-
"versionDisplay": "2026.
|
|
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/advisor.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{streamChat as e}from"./api.js";export function makeAdvisorTool({config:
|
|
1
|
+
import{streamChat as e}from"./api.js";export function makeAdvisorTool({config:t}){if(!t?.advisorModel)return null;return{readOnly:!0,spec:{type:"function",function:{name:"ask_advisor",description:"遇到难题、复杂决策或反复卡住时,向一个更强的「技术顾问」模型请教独立的第二意见。把问题背景、已知信息、已尝试过什么、待决策点写清楚(自足),它会给出判断/方案与关键风险。用于真正的难点或高风险决策,不要滥用。",parameters:{type:"object",properties:{question:{type:"string",description:"自足的难题描述(含背景、已尝试、待决策点)"}},required:["question"]}}},run:async r=>{const n=String(r?.question||"").trim();if(!n)return"缺少 question";try{const r=await e({baseUrl:t.baseUrl,apiKey:t.apiKey,model:t.advisorModel,client:t.client,aux:!0,messages:[{role:"system",content:"你是资深技术顾问,正被一个编码智能体咨询一个难题。基于它提供的信息,给出清晰、具体、可执行的判断或方案:指出关键风险、推荐做法、以及为何。简洁直接,不要寒暄。"},{role:"user",content:n}]});return`【顾问(${t.advisorModel})意见】\n`+(r.content||"(顾问无回复)").trim()}catch(e){return"咨询顾问失败:"+e.message}}}}
|
package/src/agent.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import t from"node:fs";import e from"node:path";import{streamChat as o}from"./api.js";import{MarkupFilter as n,stripMarkup as r}from"./markup-filter.js";import{TOOL_SPECS as s,READONLY_TOOLS as a,runTool as l}from"./tools.js";import{toolLabel as i,indent as u,renderFileDiff as c}from"./render.js";import{compactIfNeeded as m,buildModelMessages as g}from"./compaction.js";import{makeTodoTool as p,formatTodos as f,TODO_TOOL_NAME as d}from"./todo.js";import{makeMemoryTool as h}from"./memory.js";import{loadHooks as y,runHooks as w}from"./hooks.js";import*as S from"./ide.js";import{c as _}from"./colors.js";const b=/(现在|接下来|下面|这就|马上|我来|让我|我要|我先|我这就|咱们|让我们|稍等|待我)/,T=/(做|进行|生成|编写|写(一|个|完|出)?|制作|计算|统计|核对|汇总|整理|分析|梳理|输出|列出|准备|运行|执行|开始)/,k=/\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 x(t){const e=String(t||"").trim().split(/\n+/).map(t=>t.trim()).filter(Boolean),o=e[e.length-1]||"";return!(!o||o.length>120)&&(!!k.test(o)||b.test(o)&&T.test(o))}function $(t){const e=String(t);return e.startsWith("工具执行失败")||e.startsWith("命令启动失败")||e.includes("不是合法 JSON")||e.includes("未找到要替换的内容")||e.includes("路径超出项目目录")||e.includes("是目录而非文件")}function P(t){return"\n\n# 当前目标(用户设定)\n"+String(t).trim()+"\n在本回合结束、停止作答前,检查该目标是否已达成:若未达成,应继续推进(补充调研/改动/验证),不要过早停止;达成后用一句话说明已达成。"}function j(t){return"string"==typeof t?t:Array.isArray(t)?t.filter(t=>"text"===t?.type).map(t=>t.text||"").join("\n"):""}async function M(t,e,o){const n=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++;n[e]=await o(t[e],e)}})),n}export function isPlatformModel(t){return/^(agent|workflow|datahuman|squad)-/.test(String(t||""))}function E(t,e=200){const o=String(t).split("\n")[0];return o.length>e?o.slice(0,e)+"…":o}export function capToolResult(t){const e=String(t);if(e.length<=2e4)return e;const o=e.length-16e3-3e3;return e.slice(0,16e3)+`\n…【工具输出过长,中间已省略 ${o} 字符】…\n`+e.slice(-3e3)}export async function runTurn({config:b,messages:T,permissions:k,cwd:I,ask:U,signal:v,extraTools:A=[],depth:R=0,io:O,events:C,todoState:N,compaction:D,pullQueued:J,maxIters:q,fileJournal:F}){const K=O&&O.write||(t=>process.stdout.write(t)),W=C||null,z=t=>{W?W.onInfo?.(t):K(_.gray(" · "+t)+"\n")};let B=0,L=0,Q=0;const G=Number(q||b.maxTurns)||120,H=isPlatformModel(b.model);let V=b.model;if(0===R&&b.autoModelStrong&&!H){const t=[...T].reverse().find(t=>"user"===t.role),e=j(t?.content);(b.planMode||e.length>400||/重构|架构|设计|复杂|排查|调试|debug|优化|性能|并发|死锁|重写|迁移|安全审查|根因/i.test(e))&&b.autoModelStrong!==b.model&&(V=b.autoModelStrong,b.debug&&z(`[automodel] 复杂回合升配 → ${V}`))}const X=D||{},Y=[...A];if(0===R&&!H&&!Y.some(t=>t.spec?.function?.name===d)){const t=N||{list:[]};Y.push(p({state:t,emit:t=>{W?W.onTodo?W.onTodo(t.map(t=>({...t}))):W.onInfo?.("任务清单:\n"+f(t)):K(_.gray(u(f(t)," "))+"\n")}}))}0!==R||H||!1===b.memoryEnabled||Y.some(t=>"memory_write"===t.spec?.function?.name)||Y.push(h({cwd:I,emit:t=>{W?W.onMemory?W.onMemory(t):W.onInfo?.("已记入长期记忆:"+E(t,80)):K(_.gray(" ✎ 已记入长期记忆:"+E(t,80))+"\n")}}));const Z=new Map;for(const t of Y)Z.set(t.spec.function.name,t);const tt=H?void 0:[...s,...Y.map(t=>t.spec)];let et=0,ot=!1,nt=0,rt=null,st=0,at=0,lt=!1,it="";const ut=0!==R||H?null:y(I),ct=!(!ut||!ut.all.length);if(ct){const t=[];if(T.filter(t=>"user"===t.role).length<=1){const e=w("SessionStart",{cwd:I},ut);e.injectContext&&t.push(e.injectContext);for(const o of e.outputs)t.push(o.stdout)}const e=[...T].reverse().find(t=>"user"===t.role),o=w("UserPromptSubmit",{cwd:I,prompt:j(e?.content)},ut);if(o.block)return z("提交被 UserPromptSubmit 钩子阻断:"+o.reason),{error:"提交被钩子阻断:"+o.reason,usage:{prompt:B,completion:L},lastPromptTokens:Q};o.injectContext&&t.push(o.injectContext);for(const e of o.outputs)t.push(e.stdout);t.length&&(rt="【钩子提供的附加上下文】\n"+t.join("\n"))}for(let s=0;s<G;s++){const u=s===G-1;!u||H||ot||rt||(rt="已接近本回合的工具调用轮数上限。请不要再调用任何工具,基于目前已获得的信息,直接用中文输出面向用户的完整最终答复。");const p=await m(T,b,{state:X,lastPromptTokens:Q});p>0&&b.debug&&z(`[debug] 已压缩上下文(${p} 条较早消息已摘要)`);let f=!1,d=!1;const h=new n,y=t=>{t&&(f||!d||W||K("\n"),f=!0,W?W.onDelta?.(t):K(t))},j=g(T,X);let U=j;if(!H&&j.length&&"system"===j[0]?.role){let t="";b.planMode&&(t+="\n\n# 计划模式(当前开启)\n你现在处于计划模式:只能用只读工具(read_file/glob/grep/list_files/fetch_url)调研,不得做任何改动——write_file/edit_file/run_bash 等写或执行操作都会被拒绝,不要反复尝试。请先充分理解需求与现状,然后产出一份清晰、分步的实施方案:涉及哪些文件、每步改动要点、如何验证、风险点,用 todo_write 列出步骤,随后停下等待用户确认。用户关闭计划模式后你再执行。"),b.sandbox&&(t+="\n\n# 沙箱模式(当前开启)\n禁止执行 shell 命令(run_bash)、联网(fetch_url)与环境/应用自动化工具(install_*/setup_environment/open_*),调用会被拒绝,不要反复尝试。只做本地文件读写与分析。"),b.goal&&String(b.goal).trim()&&(t+=P(b.goal)),t&&(U=[{...j[0],content:String(j[0].content||"")+t},...j.slice(1)])}const A=[];it?A.push({role:"assistant",content:it},{role:"user",content:"你上一条回复因达到单轮输出长度上限而被截断,用户尚未看到剩余部分。请从截断处直接继续输出剩余内容:无缝衔接上文(若截断处在句子、代码块或表格中间,从中断字符处接着写),不要重复任何已输出的内容,不要道歉或解释,不要重新开头或总结前文。"}):rt&&A.push({role:"user",content:rt});const R=Date.now();let O;try{O=await o({baseUrl:b.baseUrl,apiKey:b.apiKey,model:V,messages:A.length?[...U,...A]:U,tools:ot||u?void 0:tt,maxTokens:b.maxTokens||8192,reasoningEffort:b.reasoningEffort,client:b.client,sessionId:b.sessionId},{onContent:t=>y(h.feed(t)),onReasoning:t=>{!1!==b.showReasoning&&(it||(W?W.onReasoning?.(t):(d||K(_.gray("\n[思考] ")),K(_.gray(t))),d=!0))},onRetry:(t,e,o)=>z(`模型响应异常,${Math.round(e/1e3)}s 后自动重试(第 ${t} 次):${o}`)},v)}catch(t){return"AbortError"===t.name?(W?W.onInfo?.("已中断"):K(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:B,completion:L},lastPromptTokens:Q}):(W?W.onInfo?.("✗ 模型调用失败:"+t.message):K("\n"+_.red("✗ 模型调用失败:")+t.message+"\n"),{error:t.message,usage:{prompt:B,completion:L},lastPromptTokens:Q})}y(h.flush()),O.usage&&(B+=O.usage.prompt_tokens||0,L+=O.usage.completion_tokens||0,Q=O.usage.prompt_tokens||Q),b.debug&&z(`[debug] 轮${s+1} · ${b.model} · in ${O.usage?.prompt_tokens||0} / out ${O.usage?.completion_tokens||0} tok · ${Date.now()-R}ms · ${(O.toolCalls||[]).length} 工具调用`);const{text:C,sawMarkup:N}=r(O.content),D=N||h.sawMarkup;rt=null;const q=H?[]:O.toolCalls;if(!q.length){if(D&&!C.trim()&&!H){if(et<2){et++,2===et&&(ot=!0),rt=1===et?"你刚才的输出把工具调用写成了文本标记,工具并未被执行,该内容也不会展示给用户。请重新处理:若需调用工具,必须使用规范的函数调用(tool_calls)通道发起,绝不要以任何文本形式书写工具调用标记;若无需工具,请直接用中文输出面向用户的最终答复。":"请停止调用任何工具。基于目前已获得的全部信息,直接用中文输出面向用户的完整最终答复,不要输出任何工具调用或协议标记。",z(`已拦截模型的文本化工具调用标记(工具未被执行),纠正重试中(第 ${et} 次)`);continue}z("模型持续输出无效的工具调用标记,已全部拦截;本轮无有效答复(可换个说法重试或更换模型)")}if(!H&&"length"===O.finishReason&&C&&st<3){st++,it+=C,b.debug&&z(`[debug] 输出达单轮长度上限,自动续写(第 ${st} 段)`);continue}f&&!W&&K("\n");const t=it?it+C:C;it="",t&&T.push({role:"assistant",content:t});const e=!t.trim()||x(t);if(lt&&e&&"length"!==O.finishReason&&!ot&&at<2){at++,b.debug&&z(`[debug] 检测到「宣告未落地」收尾(空正文或预告式),注入续跑纠偏(第 ${at} 次)`),T.push({role:"user",content:'你上一条只是说明"接下来要做什么",但既没有发起工具调用、也没有给出结果——用户还没拿到任何最终产物。请立即执行你刚才所说的步骤(需要就用 run_python/工具,不需要就直接算),并直接输出面向用户的完整最终结果(例如完整的按月汇总表与逐月核对表、差异结论),不要再只描述你打算做什么。'});continue}W&&W.onTurnEnd?.(t);const o=J?.();if(o){T.push({role:"user",content:o});continue}if(ct){const e=w("Stop",{cwd:I,result:t},ut);for(const t of e.outputs)z("钩子·Stop:"+E(t.stdout,160))}return{usage:{prompt:B,completion:L},lastPromptTokens:Q}}lt=!0,f&&!W&&K("\n"),W&&C&&W.onAssistantText?.(C),T.push({role:"assistant",content:it?it+C:C,tool_calls:q}),it="";const Y=q.map((t,e)=>{const o=t.function?.name||"";let n={},r=null;const s=t.function?.arguments||"";if(s)try{n=JSON.parse(s)}catch(t){r=`工具参数不是合法 JSON(${t.message}):${String(s).slice(0,200)}…请修正参数后重新调用。`}const l=Z.get(o),i=S.isActive()&&("write_file"===o||"edit_file"===o);return{tc:t,idx:e,name:o,args:n,argsError:r,extra:l,readOnly:l?!!l.readOnly:a.has(o),deferToIde:i}}),mt=t=>{for(let e=t;e<Y.length;e++)T.push({role:"tool",tool_call_id:Y[e].tc.id||`call_${e}`,content:"(用户中断,该工具未执行)"});return W?W.onInfo?.("已中断"):K(_.gray("\n[已中断]\n")),{aborted:!0,usage:{prompt:B,completion:L},lastPromptTokens:Q}},gt=async t=>{if(t.argsError)return t.argsError;let e;try{e=t.extra?await t.extra.run(t.args):await l(t.name,t.args,I,b)}catch(t){e=`工具执行失败:${t.message}`}return capToolResult(String(e))},pt=t=>{W?W.onToolStart?.(t.name,t.args):K(_.cyan(" ⏺ ")+i(t.name,t.args)+"\n")},ft=(t,e)=>{$(e)&&nt++,W?W.onToolResult?.(t.name,String(e)):K(_.gray(" "+E(e))+"\n"),T.push({role:"tool",tool_call_id:t.tc.id,content:String(e)})},dt=(t,e)=>{W?W.onSkip?.(t.name,t.args):K(_.gray(` ⊘ ${i(t.name,t.args)} — 已跳过\n`)),T.push({role:"tool",tool_call_id:t.tc.id,content:e||"未授权,未执行。"})},ht=t=>"edit_file"===t||"write_file"===t,yt=e=>{try{return t.readFileSync(e,"utf8")}catch{return""}},wt=(t,o)=>{const n=e.resolve(I,String(t.args.path)),r=c(o,yt(n),{path:String(t.args.path)});r&&(W?W.onFileDiff?.(r):K(r+"\n"))};let St=0;for(;St<Y.length;){if(v?.aborted)return mt(St);const o=Y[St];if(o.argsError){pt(o),ft(o,o.argsError),St++;continue}if(o.readOnly){let t=St;for(;t<Y.length&&Y[t].readOnly&&!Y[t].argsError;)t++;const e=Y.slice(St,t),o=await M(e,8,async t=>{const e=await k.request(t.name,t.args,I,!0);return e.allow?(pt(t),{item:t,result:await gt(t)}):{item:t,denied:e.reason||"未授权,未执行。"}});for(const t of o)null!=t.denied?dt(t.item,t.denied):ft(t.item,t.result);if(St=t,v?.aborted)return mt(St);continue}const n=await k.request(o.name,o.args,I,o.readOnly||o.deferToIde);if(v?.aborted)return mt(St);if(!n.allow){dt(o,n.reason),St++;continue}if(ct){const t=w("PreToolUse",{cwd:I,toolName:o.name,args:o.args},ut);for(const e of t.outputs)z("钩子·PreToolUse:"+E(e.stdout,120));if(t.block){dt(o,"PreToolUse 钩子阻断:"+t.reason),St++;continue}t.modifiedArgs&&(o.args={...o.args,...t.modifiedArgs},z("钩子·PreToolUse 已修改工具参数"))}pt(o);const r=ht(o.name)&&o.args?.path?yt(e.resolve(I,String(o.args.path))):null;if(F&&null!=r&&o.args?.path){const n=t.existsSync(e.resolve(I,String(o.args.path)));F.push(r.length>2097152?{path:String(o.args.path),tooLarge:!0}:{path:String(o.args.path),before:r,existed:n})}let s=await gt(o);if(ct){const t=w("PostToolUse",{cwd:I,toolName:o.name,args:o.args,result:String(s)},ut);if(t.outputs.length){for(const e of t.outputs)z("钩子·PostToolUse:"+E(e.stdout,120));s=String(s)+"\n\n[PostToolUse 钩子输出]\n"+t.outputs.map(t=>t.stdout).join("\n")}}ft(o,s),null!=r&&wt(o,r),St++}nt>=3&&!ot&&!rt&&(rt="你刚才连续多次工具调用失败。请停下来重新审视:是不是文件路径/内容/前提假设错了?先用 read_file / grep / list_files 核实现状,再换一种更可靠的方法,绝不要重复同样会失败的调用。",nt=0,z("检测到连续工具失败,已提示智能体换一种更可靠的方法"));const _t=J?.();_t&&T.push({role:"user",content:_t})}return it&&(T.push({role:"assistant",content:it}),W&&W.onTurnEnd?.(it)),W?W.onInfo?.("已达最大工具轮数,停止"):K(_.yellow("\n[已达最大工具轮数,停止]\n")),{usage:{prompt:B,completion:L},lastPromptTokens:Q}}
|
|
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)&<<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=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 u=`${e.baseUrl}/v1/chat/completions`,l={model:e.model,messages:e.messages,stream:!0,stream_options:{include_usage:!0}};e.tools&&e.tools.length&&(l.tools=e.tools,l.tool_choice="auto"),"number"==typeof e.temperature&&(l.temperature=e.temperature),Number.isFinite(e.maxTokens)&&e.maxTokens>0&&(l.max_tokens=Math.floor(e.maxTokens)),e.reasoningEffort&&"off"!==e.reasoningEffort&&(l.reasoning_effort=e.reasoningEffort);const m=n(e.apiKey);e.client&&(m["X-Huicai-Client"]=e.client),e.sessionId&&(m["X-Huicai-Session"]=e.sessionId);const h=JSON.stringify(l);for(let n=0;;n++){let l;try{l=await fetch(u,{method:"POST",headers:m,body:h,signal:a})}catch(t){if("AbortError"===t?.name||a?.aborted)throw i();if(n<2){const e=c(n,null);o.onRetry?.(n+1,e,"网络错误:"+(t?.message||t)),await s(e,a);continue}throw new Error("网络错误:"+(t?.message||t))}if(!l.ok||!l.body){const e=await r(l);if(!("usage_limit_exceeded"===e.type)&&t.has(l.status)&&n<2){const t=c(n,l);o.onRetry?.(n+1,t,`HTTP ${l.status}:${e.message}`),await s(t,a);continue}throw new Error(e.message)}return f(l,o,a,e.stallTimeoutMs??9e4)}}async function f(t,e,n,o){const r=t.body.getReader(),s=new TextDecoder;let c="",f="";const u=[];let l=null,m=null,h=!1,g=null;const d=()=>{g&&clearTimeout(g),g=setTimeout(()=>{h=!0,r.cancel().catch(()=>{})},o)},y=()=>r.cancel().catch(()=>{});n?.addEventListener?.("abort",y,{once:!0});try{for(d();;){let t;try{t=await r.read()}catch(t){if(n?.aborted)throw i();if(h)throw new Error(`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`);throw t}if(n?.aborted)throw i();if(h)throw new Error(`响应停滞超过 ${Math.round(o/1e3)}s,已中止(可重试)`);const{done:g,value:y}=t;if(g)break;d(),c+=s.decode(y,{stream:!0});const p=c.split("\n");c=p.pop()??"";for(const t of p){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}r.usage&&(m=r.usage);const s=r.choices?.[0];if(!s)continue;const i=s.delta||{},c=i.reasoning_content??i.reasoning;c&&e.onReasoning?.(c),i.content&&(f+=i.content,e.onContent?.(i.content)),Array.isArray(i.tool_calls)&&a(u,i.tool_calls),s.finish_reason&&(l=s.finish_reason)}}}finally{g&&clearTimeout(g),n?.removeEventListener?.("abort",y)}return{content:f,toolCalls:u.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 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!==l.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=u(await d(`${t}/v1/files/extract`,{...n(e),"content-type":i},r,a),!0);return l.set(t,!0),c}catch(e){if(!e||!e._noMultipart)throw e;l.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 u(await d(`${t}/v1/files/extract`,{...n(e)},Buffer.from(c),a),!1)}const f=await fetch(`${t}/v1/files/extract`,{method:"POST",headers:n(e),body:c,signal:AbortSignal.timeout(12e4)});if(!f.ok)throw new Error((await r(f)).message);return u(await f.text(),!1)}function u(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 l=new Map;const m=12e4,h=25,g=9e5;async function d(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))})}),f=Math.min(g,m+Math.floor(n.length/h));c.setTimeout(f,()=>c.destroy(new Error(`网络传输/服务端处理超时(已等待 ${Math.round(f/1e3)} 秒无响应),请检查网络后重试`))),c.on("error",i);let u=0;const l=()=>{for(;u<n.length;){const t=Math.min(u+262144,n.length),e=c.write(n.subarray(u,t));u=t;try{r&&r(u,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 e from"node:fs";import s from"node:path";import t from"node:os";import{parseFrontmatter as i}from"./skills.js";export const EXTERNAL_SOURCES=[{cli:"Claude Code",agents:".claude/agents",skills:".claude/skills"},{cli:"Codex",agents:".codex/agents",skills:".codex/skills"},{cli:"Gemini CLI",agents:".gemini/agents",skills:".gemini/skills"},{cli:"Cursor",agents:".cursor/agents",skills:".cursor/skills"}];const n=new Set([".md",".txt",".py",".js",".ts",".sh",".json",".yaml",".yml",".csv",".sql",".toml",".ini"]),r=/^[a-zA-Z0-9_-]{1,64}$/;function c(){return s.join(t.homedir(),".huicai","agents")}function o(){return s.join(t.homedir(),".huicai","skills")}export function discoverExternalAssets({base:n=t.homedir()}={}){const l=function(){try{return new Set(e.readdirSync(c()).filter(e=>e.endsWith(".md")).map(e=>e.replace(/\.md$/,"")))}catch{return new Set}}(),a=function(){try{return new Set(e.readdirSync(o(),{withFileTypes:!0}).filter(e=>e.isDirectory()).map(e=>e.name))}catch{return new Set}}(),m=[],d=[],p=new Set,u=new Set;for(const t of EXTERNAL_SOURCES){const c=s.join(n,t.agents);let o=[];try{o=e.readdirSync(c).filter(e=>e.endsWith(".md"))}catch{}for(const n of o){const o=s.join(c,n);let a;try{a=i(e.readFileSync(o,"utf8"))}catch{continue}const d=String(a.name||n.replace(/\.md$/,"")).trim();r.test(d)&&!p.has(d)&&(p.add(d),m.push({name:d,description:(a.description||"").slice(0,200),cli:t.cli,src:o,exists:l.has(d)}))}const f=s.join(n,t.skills);let h=[];try{h=e.readdirSync(f,{withFileTypes:!0}).filter(e=>e.isDirectory())}catch{}for(const n of h){const c=n.name.trim(),o=s.join(f,c),l=s.join(o,"SKILL.md");if(!r.test(c)||u.has(c)||!e.existsSync(l))continue;u.add(c);let m="";try{m=(i(e.readFileSync(l,"utf8")).description||"").slice(0,200)}catch{}d.push({name:c,description:m,cli:t.cli,src:o,exists:a.has(c)})}}return{agents:m,skills:d}}function l(t,i,r=0){if(r>3)return 0;let c=0,o=[];try{o=e.readdirSync(t,{withFileTypes:!0})}catch{return 0}e.mkdirSync(i,{recursive:!0});for(const a of o){const o=s.join(t,a.name),m=s.join(i,a.name);if(a.isDirectory())c+=l(o,m,r+1);else if(a.isFile()&&("SKILL.md"===a.name||n.has(s.extname(a.name).toLowerCase())))try{e.copyFileSync(o,m),c++}catch{}}return c}export function importExternalAssets(t={},{overwrite:i=!1}={}){const n=[],a={agents:{imported:0,skipped:0},skills:{imported:0,skipped:0},details:n},m=c(),d=o();for(const c of t.agents||[]){if(!c||!r.test(String(c.name))||!c.src)continue;const t=s.join(m,c.name+".md");if(i||!e.existsSync(t))try{e.mkdirSync(m,{recursive:!0}),e.copyFileSync(c.src,t),a.agents.imported++,n.push(`导入 agent:${c.name}`)}catch(e){n.push(`失败 agent ${c.name}:${e.message}`)}else a.agents.skipped++,n.push(`跳过 agent(已存在):${c.name}`)}for(const c of t.skills||[]){if(!c||!r.test(String(c.name))||!c.src)continue;const t=s.join(d,c.name);if(!i&&e.existsSync(t)){a.skills.skipped++,n.push(`跳过 skill(已存在):${c.name}`);continue}const o=l(c.src,t);o>0?(a.skills.imported++,n.push(`导入 skill:${c.name}(${o} 文件)`)):(a.skills.skipped++,n.push(`跳过 skill(无内容):${c.name}`))}return a}export function importAllExternalAssets(e={}){const s=discoverExternalAssets(e);return importExternalAssets({agents:s.agents.filter(e=>!e.exists),skills:s.skills.filter(e=>!e.exists)},e)}
|
package/src/autofix.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as t}from"node:child_process";import e from"node:fs";import
|
|
1
|
+
import{spawn as t}from"node:child_process";import{StringDecoder as e}from"node:string_decoder";import r from"node:fs";import n from"node:path";const o=16e3;export function captureCommand(r,n,s=18e4){const i="win32"===process.platform;return new Promise(c=>{let d="",a=!1;const p=i?t(process.env.COMSPEC||"cmd.exe",["/d","/s","/c",n],{cwd:r,windowsVerbatimArguments:!0}):t(process.env.SHELL||"/bin/sh",["-c",n],{cwd:r,detached:!0}),u=setTimeout(()=>{a=!0;try{!i&&p.pid?process.kill(-p.pid,"SIGKILL"):p.kill("SIGKILL")}catch{}},s),m=new e("utf8"),l=new e("utf8"),f=t=>{d.length<32e3&&(d+=t)};p.stdout.on("data",t=>f(m.write(t))),p.stderr.on("data",t=>f(l.write(t))),p.on("error",t=>{clearTimeout(u),c({code:-1,output:"命令启动失败:"+t.message})}),p.on("close",t=>{var e;clearTimeout(u),f(m.end()),f(l.end()),c({code:a?124:null==t?1:t,output:(e=d,((e=String(e)).length>o?e.slice(0,o)+"\n…(输出过长已截断)":e)||"(无输出)")})})})}export function detectVerifyCommand(t){const e=e=>{try{return r.existsSync(n.join(t,e))}catch{return!1}};try{if(e("package.json")){const e=JSON.parse(r.readFileSync(n.join(t,"package.json"),"utf8"));if(e.scripts?.test&&!/no test specified/i.test(e.scripts.test))return"npm test";if(e.scripts?.build)return"npm run build"}}catch{}return e("pyproject.toml")||e("pytest.ini")||e("tox.ini")?"pytest -q":e("pom.xml")?"mvn -q test":e("build.gradle")||e("build.gradle.kts")?"./gradlew test":e("Cargo.toml")?"cargo test":e("go.mod")?"go test ./...":e("Makefile")?"make test":null}export async function runVerifyLoop({command:t,maxIters:e=5,cwd:r,runTurnOnce:n,emit:o}){for(let s=0;s<e;s++){o(`▶ 验证 ${s+1}/${e}:${t}`);const{code:i,output:c}=await captureCommand(r,t);if(0===i)return o(`✅ 验证通过(第 ${s+1} 轮)。`),{passed:!0,iters:s+1};if(o(`✗ 未通过(退出码 ${i})`+(s<e-1?",让智能体定位修复后自动重跑…":"")),s===e-1)return o(`⚠ 已达上限 ${e} 轮仍未通过。`),{passed:!1,iters:s+1,output:c};const d=await n(`自动验证:运行 \`${t}\` 失败(退出码 ${i})。输出如下:\n\n${c}\n\n请定位根因并**修复代码/配置**。不要自己重跑该命令——我会自动重跑验证。本轮只做必要的修复。`);if(d&&d.aborted)return o("已中断修复循环。"),{passed:!1,aborted:!0,iters:s+1}}return{passed:!1}}
|
|
@@ -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{runBash as o}from"./executors.js";const r=3e4,a=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:o,timeoutMs:a=3e5,winShell:s=!1}={}){const i="win32"===process.platform;return new Promise(c=>{let l,p="",u=!1;try{l=n(t,e,i?{cwd:o,shell:!!s}:{cwd:o,detached:!0})}catch(t){return void c({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}const d=setTimeout(()=>{u=!0,(()=>{try{!i&&l.pid?process.kill(-l.pid,"SIGKILL"):l.kill("SIGKILL")}catch{try{l.kill("SIGKILL")}catch{}}})()},a),m=t=>{p.length<r&&(p+=t.toString())};l.stdout?.on("data",m),l.stderr?.on("data",m),l.on("error",t=>{clearTimeout(d),c({code:-1,killed:!1,output:`命令启动失败:${t.message}`})}),l.on("close",t=>{clearTimeout(d),c({code:t,killed:u,output:p.length>r?p.slice(0,r)+"\n…(输出已截断)":p})})})}function s(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 i(t){if(!t||"string"!=typeof t)throw new Error("工作目录未设置:请在项目目录中运行(VS Code / IDEA 中需先「打开文件夹」)")}function c(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){i(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):a,r=await runArgv(n.bin,n.argv,{cwd:t,timeoutMs:o,winShell:n.winShell}),s=r.killed?"【超时被终止】":`退出码 ${r.code}`,l=`$ ${n.display}\n${s}`;return 0===r.code?`${l}\n已安装完成。${c(r.output,800)?"\n"+c(r.output,800):""}`:`${l}\n${c(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}},l=[];c("package.json")&&l.push("node"),(c("requirements.txt")||c("pyproject.toml"))&&l.push("python"),c("go.mod")&&l.push("go"),c("Cargo.toml")&&l.push("rust"),c("pom.xml")&&l.push("maven"),(c("build.gradle")||c("build.gradle.kts"))&&l.push("gradle");const p=Array.isArray(o.stacks)&&o.stacks.length?l.filter(t=>o.stacks.includes(t)):l,u=[],d=[],m=(t,e)=>which(t,{pathStr:s,platform:a})?null:e;for(const t of p)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:l,want:p,steps:u,notes:d}}export async function setupEnvironment(t,e){i(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}${c(n.output)?"\n"+c(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"],p=["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 p)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:a,r=await runArgv(n.bin,n.argv,{cwd:t||void 0,timeoutMs:o,winShell:n.winShell}),s=`${n.bin} ${n.argv.join(" ")}`;return 0===r.code?`$ ${s}\n退出码 0\n已安装 ${e?.id}${n.note?"("+n.note+")":""}${c(r.output,600)?"\n"+c(r.output,600):""}`:`$ ${s}\n${r.killed?"【超时被终止】":"退出码 "+r.code}\n${c(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 s("open",[n]):"win32"===o?await s("rundll32",["url.dll,FileProtocolHandler",n]):await s("xdg-open",[n]);return 0!==r.code?`打开浏览器失败(退出码 ${r.code}):${c(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 i=t.statSync(a).isDirectory(),l=null!=o?.reveal?!!o.reveal:!i,p=process.platform;let u;return"darwin"===p?u=await s("open",l&&!i?["-R",a]:[a]):"win32"===p?(u=await s("explorer.exe",l&&!i?["/select,"+a]:[a]),u={code:0,output:u.output}):u=await s("xdg-open",[l&&!i?e.dirname(a):a]),0!==u.code?`打开文件管理器失败(退出码 ${u.code}):${c(u.output)||"未知原因"}`:`已在文件管理器中${l&&!i?"定位":"打开"}:${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 s("open",r?["-a",o,r]:["-a",o]);return 0!==t.code?`启动应用失败(退出码 ${t.code}):${c(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 u=(t,e)=>({type:"object",properties:t,required:e||[]}),d=t=>({type:"string",description:t}),m=t=>({type:"boolean",description:t}),g=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:u({packages:{type:"array",items:{type:"string"},description:'要安装的包(如 ["lodash","@types/node@^20"]);省略=安装全部依赖'},dev:m("可选:装入 devDependencies(默认否)"),global:m("可选:全局安装(需明确 packages;默认否)"),manager:d("可选:npm | pnpm | yarn(默认按锁文件自动识别)"),timeout:g("可选:超时毫秒(默认 300000,上限 600000)")},[])}},{type:"function",function:{name:"setup_environment",description:"自动配置项目运行环境(执行前会征询用户同意):检测技术栈(Node/Python/Go/Rust/Maven)→ 检查工具链是否安装 → 安装依赖(npm install、创建 .venv 并 pip install、go mod download 等)。也可用 commands 显式指定要执行的配置命令(逐条执行,失败即停)。",parameters:u({stacks:{type:"array",items:{type:"string"},description:"可选:只处理这些技术栈(node|python|go|rust|maven)"},commands:{type:"array",items:{type:"string"},description:"可选:显式配置命令列表(优先于自动检测,最多 12 条)"},timeout:g("可选:单步超时毫秒(默认 600000)")},[])}},{type:"function",function:{name:"install_plugin",description:"安装编辑器/IDE 插件(执行前会征询用户同意):VS Code 扩展(code --install-extension)、JetBrains 插件(idea installPlugins,装后需重启 IDE)、或 npm 全局 CLI 工具(npm install -g)。",parameters:u({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:u({url:d("完整网址(http/https)")},["url"])}},{type:"function",function:{name:"open_path",description:"在系统文件管理器(访达/资源管理器)中打开目录或定位文件(执行前会征询用户同意)。",parameters:u({path:d("目录或文件路径(相对项目根或绝对路径)"),reveal:m("可选:定位(选中)该文件而非打开;文件默认定位、目录默认打开")},["path"])}},{type:"function",function:{name:"open_app",description:'启动本机任意应用程序(执行前会征询用户同意),可附带要打开的文件或网址。macOS 用应用名(如 "Safari"、"Visual Studio Code"),Windows 用可启动名,Linux 用可执行文件名。',parameters:u({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};
|
|
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};
|