@huicai/cli 2026.710.97950 → 2026.711.24616
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/LICENSE +43 -21
- package/bin/huicai.js +93 -2
- package/package.json +4 -3
- package/src/advisor.js +1 -0
- package/src/agent.js +1 -385
- package/src/api.js +1 -326
- package/src/autofix.js +1 -0
- package/src/banner.js +1 -83
- package/src/checkpoints.js +1 -0
- package/src/clipboard.js +1 -0
- package/src/codeindex.js +1 -0
- package/src/colors.js +1 -22
- package/src/commands.js +1 -339
- package/src/compaction.js +1 -140
- package/src/config.js +1 -66
- package/src/consult.js +1 -65
- package/src/context-report.js +1 -0
- package/src/context.js +1 -144
- package/src/cron.js +1 -0
- package/src/daemon.js +1 -0
- package/src/device.js +1 -88
- package/src/diffview.js +1 -0
- package/src/doctor.js +1 -0
- package/src/executors.js +1 -391
- package/src/forks.js +1 -0
- package/src/gitflow.js +1 -0
- package/src/hooks.js +1 -0
- package/src/ide.js +1 -53
- package/src/input.js +1 -182
- package/src/insights.js +1 -0
- package/src/kbsync.js +1 -0
- package/src/markdown.js +0 -0
- package/src/markup-filter.js +1 -85
- package/src/mcp.js +1 -207
- package/src/memory.js +1 -110
- package/src/permissions.js +1 -82
- package/src/render.js +1 -134
- package/src/repl.js +1 -205
- package/src/scan.js +1 -0
- package/src/schedule.js +1 -0
- package/src/session.js +1 -140
- package/src/settings.js +0 -0
- package/src/skills.js +1 -248
- package/src/subagent.js +1 -82
- package/src/todo.js +1 -75
- package/src/tools.js +1 -132
- package/src/transcript.js +1 -0
- package/src/tui/app.js +1 -566
- package/src/usage.js +1 -43
- package/src/watch.js +1 -0
package/src/skills.js
CHANGED
|
@@ -1,248 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// (<技能名>/SKILL.md + references/scripts 等文本附件),并提供两个动态工具:
|
|
3
|
-
// load_skill —— 按需载入技能正文(渐进披露:系统提示词只常驻「名称:描述」目录)
|
|
4
|
-
// read_skill_file —— 按需读取技能附件
|
|
5
|
-
// 与平台 skills 表经 /v1/skills 双向同步(huicai skills pull/push),平台即云端技能库。
|
|
6
|
-
|
|
7
|
-
import fs from 'node:fs';
|
|
8
|
-
import path from 'node:path';
|
|
9
|
-
import os from 'node:os';
|
|
10
|
-
import { fileURLToPath } from 'node:url';
|
|
11
|
-
|
|
12
|
-
// 随 CLI 一起分发的内置技能目录(<包根>/skills)。作为「只读、最低优先级」的技能来源:
|
|
13
|
-
// 用户在项目 .huicai/skills 或全局 ~/.huicai/skills 建同名技能即可覆盖。仅进入「发现/载入」读路径,
|
|
14
|
-
// 不进入 skillDirs()(pull 写入目标仍是用户目录,勿写包内)。
|
|
15
|
-
export const BUNDLED_SKILLS_DIR = path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'skills');
|
|
16
|
-
|
|
17
|
-
/** 附件允许的文本扩展名(与平台 SkillFile.TEXT_EXTS 对齐;二进制不收) */
|
|
18
|
-
const TEXT_EXTS = new Set([
|
|
19
|
-
'.md', '.markdown', '.txt', '.json', '.yaml', '.yml', '.py', '.js', '.mjs', '.cjs', '.ts',
|
|
20
|
-
'.sh', '.sql', '.csv', '.html', '.css', '.xml', '.toml',
|
|
21
|
-
]);
|
|
22
|
-
const MAX_FILE_BYTES = 256 * 1024; // 单附件读取上限
|
|
23
|
-
const MAX_FILES = 50; // 单技能附件数上限
|
|
24
|
-
const MAX_BODY_CHARS = 64 * 1024; // 载入正文回填上限
|
|
25
|
-
|
|
26
|
-
/** 解析 SKILL.md 的 YAML frontmatter(零依赖轻量解析:仅顶层 key: value 的 name/description/version)。 */
|
|
27
|
-
export function parseFrontmatter(raw) {
|
|
28
|
-
const out = { name: '', description: '', version: '', body: (raw || '').trim() };
|
|
29
|
-
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(out.body);
|
|
30
|
-
if (!m) return out;
|
|
31
|
-
out.body = out.body.slice(m[0].length).trim();
|
|
32
|
-
for (const line of m[1].split(/\r?\n/)) {
|
|
33
|
-
const kv = /^(name|description|version)\s*:\s*(.*)$/.exec(line.trim());
|
|
34
|
-
if (!kv) continue;
|
|
35
|
-
out[kv[1]] = kv[2].trim().replace(/^["']|["']$/g, '');
|
|
36
|
-
}
|
|
37
|
-
return out;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
/** 技能根目录(项目级优先于全局;不存在的目录由调用方容错)。 */
|
|
41
|
-
export function skillDirs(cwd) {
|
|
42
|
-
const dirs = [];
|
|
43
|
-
if (cwd) dirs.push(path.join(cwd, '.huicai', 'skills'));
|
|
44
|
-
dirs.push(path.join(os.homedir(), '.huicai', 'skills'));
|
|
45
|
-
return dirs;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/** 递归收集技能目录下 SKILL.md 之外的文本附件(相对路径,深度≤3,数量受限)。 */
|
|
49
|
-
function collectFiles(dir, base = '', depth = 0, out = []) {
|
|
50
|
-
if (depth > 3 || out.length >= MAX_FILES) return out;
|
|
51
|
-
let entries;
|
|
52
|
-
try {
|
|
53
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
54
|
-
} catch {
|
|
55
|
-
return out;
|
|
56
|
-
}
|
|
57
|
-
for (const e of entries) {
|
|
58
|
-
if (out.length >= MAX_FILES) break;
|
|
59
|
-
if (e.name.startsWith('.')) continue;
|
|
60
|
-
const rel = base ? `${base}/${e.name}` : e.name;
|
|
61
|
-
if (e.isDirectory()) {
|
|
62
|
-
collectFiles(path.join(dir, e.name), rel, depth + 1, out);
|
|
63
|
-
} else if (e.isFile() && rel !== 'SKILL.md' && TEXT_EXTS.has(path.extname(e.name).toLowerCase())) {
|
|
64
|
-
out.push(rel);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* 发现本地技能(项目级同名覆盖全局)。
|
|
72
|
-
* @returns {{name:string,description:string,version:string,dir:string,scope:'project'|'global',files:string[]}[]}
|
|
73
|
-
*/
|
|
74
|
-
export function loadLocalSkills(cwd) {
|
|
75
|
-
const byName = new Map();
|
|
76
|
-
// 优先级:项目 > 全局 > 内置(同名先扫者生效,故用户目录可覆盖内置技能)
|
|
77
|
-
const dirs = [
|
|
78
|
-
...(cwd ? [{ dir: path.join(cwd, '.huicai', 'skills'), scope: 'project' }] : []),
|
|
79
|
-
{ dir: path.join(os.homedir(), '.huicai', 'skills'), scope: 'global' },
|
|
80
|
-
{ dir: BUNDLED_SKILLS_DIR, scope: 'builtin' },
|
|
81
|
-
];
|
|
82
|
-
for (const { dir: base, scope } of dirs) {
|
|
83
|
-
let entries;
|
|
84
|
-
try {
|
|
85
|
-
entries = fs.readdirSync(base, { withFileTypes: true });
|
|
86
|
-
} catch {
|
|
87
|
-
continue;
|
|
88
|
-
}
|
|
89
|
-
for (const e of entries) {
|
|
90
|
-
if (!e.isDirectory()) continue;
|
|
91
|
-
const dir = path.join(base, e.name);
|
|
92
|
-
let raw;
|
|
93
|
-
try {
|
|
94
|
-
raw = fs.readFileSync(path.join(dir, 'SKILL.md'), 'utf8');
|
|
95
|
-
} catch {
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
const fm = parseFrontmatter(raw);
|
|
99
|
-
const name = (fm.name || e.name).trim();
|
|
100
|
-
if (!name || byName.has(name)) continue; // 同名:先扫者(更高优先级)生效
|
|
101
|
-
byName.set(name, {
|
|
102
|
-
name,
|
|
103
|
-
description: fm.description || '',
|
|
104
|
-
version: fm.version || '',
|
|
105
|
-
dir,
|
|
106
|
-
scope,
|
|
107
|
-
files: collectFiles(dir),
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
return [...byName.values()];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** 读取技能正文(SKILL.md 全文);找不到返回 null。 */
|
|
115
|
-
export function readSkillBody(cwd, name) {
|
|
116
|
-
const skill = loadLocalSkills(cwd).find((s) => s.name === name);
|
|
117
|
-
if (!skill) return null;
|
|
118
|
-
try {
|
|
119
|
-
const raw = fs.readFileSync(path.join(skill.dir, 'SKILL.md'), 'utf8');
|
|
120
|
-
return { skill, raw: raw.length > MAX_BODY_CHARS ? raw.slice(0, MAX_BODY_CHARS) + '\n…(正文已截断)' : raw };
|
|
121
|
-
} catch {
|
|
122
|
-
return null;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** 读取技能附件(限制在技能目录内,防路径穿越);返回文本或抛出可读错误。 */
|
|
127
|
-
export function readSkillFileContent(cwd, skillName, relPath) {
|
|
128
|
-
const skill = loadLocalSkills(cwd).find((s) => s.name === skillName);
|
|
129
|
-
if (!skill) throw new Error(`本地技能不存在:${skillName}`);
|
|
130
|
-
const cleaned = String(relPath || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '');
|
|
131
|
-
const abs = path.resolve(skill.dir, cleaned);
|
|
132
|
-
if (!abs.startsWith(path.resolve(skill.dir) + path.sep)) throw new Error(`路径越界:${relPath}`);
|
|
133
|
-
const stat = fs.statSync(abs);
|
|
134
|
-
if (stat.size > MAX_FILE_BYTES) throw new Error(`附件过大(>${MAX_FILE_BYTES / 1024}KB):${cleaned}`);
|
|
135
|
-
return fs.readFileSync(abs, 'utf8');
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** 技能目录文本(系统提示词注入);无技能返回空串。 */
|
|
139
|
-
export function skillCatalog(cwd) {
|
|
140
|
-
const skills = loadLocalSkills(cwd);
|
|
141
|
-
if (!skills.length) return '';
|
|
142
|
-
const lines = skills.map((s) => {
|
|
143
|
-
const desc = (s.description || '(无描述)').replace(/\s+/g, ' ').slice(0, 300);
|
|
144
|
-
return `- ${s.name}:${desc}`;
|
|
145
|
-
});
|
|
146
|
-
return '以下本地技能可用 load_skill(name) 载入其操作指引后遵循执行(描述含「何时使用」,据此判断是否载入;无关任务不要载入):\n'
|
|
147
|
-
+ lines.join('\n');
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/** 构造 load_skill / read_skill_file 动态工具(extraTools 形态:{spec,run,readOnly});无本地技能返回 []。 */
|
|
151
|
-
export function makeSkillTools(cwd) {
|
|
152
|
-
const skills = loadLocalSkills(cwd);
|
|
153
|
-
if (!skills.length) return [];
|
|
154
|
-
const tools = [
|
|
155
|
-
{
|
|
156
|
-
readOnly: true,
|
|
157
|
-
spec: {
|
|
158
|
-
type: 'function',
|
|
159
|
-
function: {
|
|
160
|
-
name: 'load_skill',
|
|
161
|
-
description: '载入一个本地技能的操作指引(系统提示词「可用技能」目录中的名称)。载入后严格遵循其流程执行。',
|
|
162
|
-
parameters: {
|
|
163
|
-
type: 'object',
|
|
164
|
-
properties: { name: { type: 'string', description: '技能名(见「可用技能」目录)' } },
|
|
165
|
-
required: ['name'],
|
|
166
|
-
},
|
|
167
|
-
},
|
|
168
|
-
},
|
|
169
|
-
run: async (args) => {
|
|
170
|
-
const name = String(args?.name || '').trim();
|
|
171
|
-
const r = readSkillBody(cwd, name);
|
|
172
|
-
if (!r) {
|
|
173
|
-
const names = loadLocalSkills(cwd).map((s) => s.name).join(', ');
|
|
174
|
-
return `本地技能不存在:${name}(可用:${names || '无'})`;
|
|
175
|
-
}
|
|
176
|
-
let text = r.raw;
|
|
177
|
-
if (r.skill.files.length) {
|
|
178
|
-
text += `\n\n---\n本技能附带 ${r.skill.files.length} 个资源文件,需要时用 read_skill_file(skill="${name}", path=...) 读取:\n`
|
|
179
|
-
+ r.skill.files.map((f) => `- ${f}`).join('\n');
|
|
180
|
-
}
|
|
181
|
-
return text;
|
|
182
|
-
},
|
|
183
|
-
},
|
|
184
|
-
];
|
|
185
|
-
if (skills.some((s) => s.files.length)) {
|
|
186
|
-
tools.push({
|
|
187
|
-
readOnly: true,
|
|
188
|
-
spec: {
|
|
189
|
-
type: 'function',
|
|
190
|
-
function: {
|
|
191
|
-
name: 'read_skill_file',
|
|
192
|
-
description: '读取本地技能附带的资源文件(先 load_skill 查看其文件清单,再按 path 读取)。',
|
|
193
|
-
parameters: {
|
|
194
|
-
type: 'object',
|
|
195
|
-
properties: {
|
|
196
|
-
skill: { type: 'string', description: '技能名' },
|
|
197
|
-
path: { type: 'string', description: '资源文件相对路径(技能载入结果中列出)' },
|
|
198
|
-
},
|
|
199
|
-
required: ['skill', 'path'],
|
|
200
|
-
},
|
|
201
|
-
},
|
|
202
|
-
},
|
|
203
|
-
run: async (args) => {
|
|
204
|
-
try {
|
|
205
|
-
const content = readSkillFileContent(cwd, String(args?.skill || '').trim(), args?.path);
|
|
206
|
-
return `文件 ${args?.path}:\n${content}`;
|
|
207
|
-
} catch (e) {
|
|
208
|
-
return `读取失败:${e.message}`;
|
|
209
|
-
}
|
|
210
|
-
},
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
return tools;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/** 把平台技能详情写入本地目录(pull 落盘):baseDir/<name>/SKILL.md + 附件。返回技能目录路径。 */
|
|
217
|
-
export function writeSkillToDir(baseDir, item) {
|
|
218
|
-
const name = String(item?.name || '').trim();
|
|
219
|
-
if (!name || !item?.content) throw new Error('技能数据不完整(缺 name 或 content)');
|
|
220
|
-
const dir = path.join(baseDir, name.replace(/[^a-zA-Z0-9_-]/g, '-'));
|
|
221
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
222
|
-
fs.writeFileSync(path.join(dir, 'SKILL.md'), item.content, 'utf8');
|
|
223
|
-
for (const f of item.files || []) {
|
|
224
|
-
const rel = String(f?.path || '').replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+/, '');
|
|
225
|
-
if (!rel || rel.includes('..')) continue; // 非法附件路径直接跳过
|
|
226
|
-
const abs = path.join(dir, rel);
|
|
227
|
-
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
228
|
-
fs.writeFileSync(abs, f.content ?? '', 'utf8');
|
|
229
|
-
}
|
|
230
|
-
return dir;
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
/** 读取一个本地技能目录为 push 载荷:{content, filename, files:[{path,content}]}。 */
|
|
234
|
-
export function readSkillDirForPush(dir) {
|
|
235
|
-
const skillMd = path.join(dir, 'SKILL.md');
|
|
236
|
-
const content = fs.readFileSync(skillMd, 'utf8');
|
|
237
|
-
const files = [];
|
|
238
|
-
for (const rel of collectFiles(dir)) {
|
|
239
|
-
try {
|
|
240
|
-
const abs = path.join(dir, rel);
|
|
241
|
-
if (fs.statSync(abs).size > MAX_FILE_BYTES) continue;
|
|
242
|
-
files.push({ path: rel, content: fs.readFileSync(abs, 'utf8') });
|
|
243
|
-
} catch {
|
|
244
|
-
/* 跳过读不了的附件 */
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return { content, filename: path.basename(dir), files };
|
|
248
|
-
}
|
|
1
|
+
import e from"node:fs";import n from"node:path";import t from"node:os";import{fileURLToPath as r}from"node:url";export const BUNDLED_SKILLS_DIR=n.join(n.dirname(r(import.meta.url)),"..","skills");const i=new Set([".md",".markdown",".txt",".json",".yaml",".yml",".py",".js",".mjs",".cjs",".ts",".sh",".sql",".csv",".html",".css",".xml",".toml"]),o=262144;export function parseFrontmatter(e){const n={name:"",description:"",version:"",body:(e||"").trim()},t=/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(n.body);if(!t)return n;n.body=n.body.slice(t[0].length).trim();for(const e of t[1].split(/\r?\n/)){const t=/^(name|description|version)\s*:\s*(.*)$/.exec(e.trim());t&&(n[t[1]]=t[2].trim().replace(/^["']|["']$/g,""))}return n}export function skillDirs(e){const r=[];return e&&r.push(n.join(e,".huicai","skills")),r.push(n.join(t.homedir(),".huicai","skills")),r}function s(t,r="",o=0,l=[]){if(o>3||l.length>=50)return l;let c;try{c=e.readdirSync(t,{withFileTypes:!0})}catch{return l}for(const e of c){if(l.length>=50)break;if(e.name.startsWith("."))continue;const c=r?`${r}/${e.name}`:e.name;e.isDirectory()?s(n.join(t,e.name),c,o+1,l):e.isFile()&&"SKILL.md"!==c&&i.has(n.extname(e.name).toLowerCase())&&l.push(c)}return l}export function loadLocalSkills(r){const i=new Map,o=[...r?[{dir:n.join(r,".huicai","skills"),scope:"project"}]:[],{dir:n.join(t.homedir(),".huicai","skills"),scope:"global"},{dir:BUNDLED_SKILLS_DIR,scope:"builtin"}];for(const{dir:t,scope:r}of o){let o;try{o=e.readdirSync(t,{withFileTypes:!0})}catch{continue}for(const l of o){if(!l.isDirectory())continue;const o=n.join(t,l.name);let c;try{c=e.readFileSync(n.join(o,"SKILL.md"),"utf8")}catch{continue}const a=parseFrontmatter(c),p=(a.name||l.name).trim();p&&!i.has(p)&&i.set(p,{name:p,description:a.description||"",version:a.version||"",dir:o,scope:r,files:s(o)})}}return[...i.values()]}export function readSkillBody(t,r){const i=loadLocalSkills(t).find(e=>e.name===r);if(!i)return null;try{const t=e.readFileSync(n.join(i.dir,"SKILL.md"),"utf8");return{skill:i,raw:t.length>65536?t.slice(0,65536)+"\n…(正文已截断)":t}}catch{return null}}export function readSkillFileContent(t,r,i){const s=loadLocalSkills(t).find(e=>e.name===r);if(!s)throw new Error(`本地技能不存在:${r}`);const l=String(i||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,""),c=n.resolve(s.dir,l);if(!c.startsWith(n.resolve(s.dir)+n.sep))throw new Error(`路径越界:${i}`);if(e.statSync(c).size>o)throw new Error(`附件过大(>256KB):${l}`);return e.readFileSync(c,"utf8")}export function skillCatalog(e){const n=loadLocalSkills(e);if(!n.length)return"";return"以下本地技能可用 load_skill(name) 载入其操作指引后遵循执行(描述含「何时使用」,据此判断是否载入;无关任务不要载入):\n"+n.map(e=>{const n=(e.description||"(无描述)").replace(/\s+/g," ").slice(0,300);return`- ${e.name}:${n}`}).join("\n")}export function makeSkillTools(e){const n=loadLocalSkills(e);if(!n.length)return[];const t=[{readOnly:!0,spec:{type:"function",function:{name:"load_skill",description:"载入一个本地技能的操作指引(系统提示词「可用技能」目录中的名称)。载入后严格遵循其流程执行。",parameters:{type:"object",properties:{name:{type:"string",description:"技能名(见「可用技能」目录)"}},required:["name"]}}},run:async n=>{const t=String(n?.name||"").trim(),r=readSkillBody(e,t);if(!r){return`本地技能不存在:${t}(可用:${loadLocalSkills(e).map(e=>e.name).join(", ")||"无"})`}let i=r.raw;return r.skill.files.length&&(i+=`\n\n---\n本技能附带 ${r.skill.files.length} 个资源文件,需要时用 read_skill_file(skill="${t}", path=...) 读取:\n`+r.skill.files.map(e=>`- ${e}`).join("\n")),i}}];return n.some(e=>e.files.length)&&t.push({readOnly:!0,spec:{type:"function",function:{name:"read_skill_file",description:"读取本地技能附带的资源文件(先 load_skill 查看其文件清单,再按 path 读取)。",parameters:{type:"object",properties:{skill:{type:"string",description:"技能名"},path:{type:"string",description:"资源文件相对路径(技能载入结果中列出)"}},required:["skill","path"]}}},run:async n=>{try{const t=readSkillFileContent(e,String(n?.skill||"").trim(),n?.path);return`文件 ${n?.path}:\n${t}`}catch(e){return`读取失败:${e.message}`}}}),t}export function writeSkillToDir(t,r){const i=String(r?.name||"").trim();if(!i||!r?.content)throw new Error("技能数据不完整(缺 name 或 content)");const o=n.join(t,i.replace(/[^a-zA-Z0-9_-]/g,"-"));e.mkdirSync(o,{recursive:!0}),e.writeFileSync(n.join(o,"SKILL.md"),r.content,"utf8");for(const t of r.files||[]){const r=String(t?.path||"").replace(/\\/g,"/").replace(/^\.\//,"").replace(/^\/+/,"");if(!r||r.includes(".."))continue;const i=n.join(o,r);e.mkdirSync(n.dirname(i),{recursive:!0}),e.writeFileSync(i,t.content??"","utf8")}return o}export function readSkillDirForPush(t){const r=n.join(t,"SKILL.md"),i=e.readFileSync(r,"utf8"),l=[];for(const r of s(t))try{const i=n.join(t,r);if(e.statSync(i).size>o)continue;l.push({path:r,content:e.readFileSync(i,"utf8")})}catch{}return{content:i,filename:n.basename(t),files:l}}
|
package/src/subagent.js
CHANGED
|
@@ -1,82 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 一个内置工具 dispatch_agent,把明确子任务交给「隔离上下文 + 只读工具」的子智能体自主完成,只回传结论。
|
|
3
|
-
// 让主线程上下文保持精简,适合并行/发散的调研、检索、分析类子任务。
|
|
4
|
-
|
|
5
|
-
import { runTurn } from './agent.js';
|
|
6
|
-
import { createPermissions } from './permissions.js';
|
|
7
|
-
|
|
8
|
-
const MAX_DEPTH = 2; // 顶层 depth=0;到达上限不再提供 dispatch_agent,防无限递归
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* 构造 dispatch_agent 工具。达到深度上限返回 null(不暴露委派)。
|
|
12
|
-
* @param {object} p
|
|
13
|
-
* @param {object} p.config 运行配置(闭包引用,读时取最新 model)
|
|
14
|
-
* @param {string} p.cwd 工作目录
|
|
15
|
-
* @param {{spec:object,run:Function,readOnly:boolean}[]} [p.extraTools] 可传给子智能体的外部工具(仅其只读子集会被采用,通常为 MCP 工具,不含 dispatch 自身)
|
|
16
|
-
* @param {number} p.depth 当前深度
|
|
17
|
-
*/
|
|
18
|
-
export function makeSubagentTool({ config, cwd, extraTools = [], depth = 0 }) {
|
|
19
|
-
if (depth >= MAX_DEPTH) return null;
|
|
20
|
-
|
|
21
|
-
const spec = {
|
|
22
|
-
type: 'function',
|
|
23
|
-
function: {
|
|
24
|
-
name: 'dispatch_agent',
|
|
25
|
-
description:
|
|
26
|
-
'把一个明确、独立、自足的子任务委派给子智能体:它在隔离上下文中自主使用(只读)工具完成后只回传结论。'
|
|
27
|
-
+ '适合可并行的调研/检索/分析类子任务,让主线程上下文保持精简。可多次调用以分解复杂任务。',
|
|
28
|
-
parameters: {
|
|
29
|
-
type: 'object',
|
|
30
|
-
properties: {
|
|
31
|
-
objective: { type: 'string', description: '要子智能体完成的单一、明确、自足的子任务' },
|
|
32
|
-
context: { type: 'string', description: '可选:完成该子任务所需的背景信息' },
|
|
33
|
-
},
|
|
34
|
-
required: ['objective'],
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
// 子智能体只用只读外部工具(如只读 MCP);内置读工具由 runTurn 默认提供,写/执行被其非交互权限拒绝
|
|
40
|
-
const readOnlyExtras = extraTools.filter((t) => t.readOnly);
|
|
41
|
-
|
|
42
|
-
const run = async (args) => {
|
|
43
|
-
const objective = String(args?.objective || '').trim();
|
|
44
|
-
if (!objective) return '未提供子任务目标 objective';
|
|
45
|
-
|
|
46
|
-
const messages = [
|
|
47
|
-
{ role: 'system', content: subagentPrompt(cwd, args?.context) },
|
|
48
|
-
{ role: 'user', content: objective },
|
|
49
|
-
];
|
|
50
|
-
// 只读、非交互:读工具自动放行,写/执行被拒并把原因回灌(子智能体不产生副作用)
|
|
51
|
-
const permissions = createPermissions({ yolo: false, ask: null });
|
|
52
|
-
|
|
53
|
-
// 允许有限层级的再委派
|
|
54
|
-
const nested = makeSubagentTool({ config, cwd, extraTools: readOnlyExtras, depth: depth + 1 });
|
|
55
|
-
const subExtra = nested ? [...readOnlyExtras, nested] : readOnlyExtras;
|
|
56
|
-
|
|
57
|
-
const r = await runTurn({
|
|
58
|
-
config,
|
|
59
|
-
messages,
|
|
60
|
-
permissions,
|
|
61
|
-
cwd,
|
|
62
|
-
extraTools: subExtra,
|
|
63
|
-
depth: depth + 1,
|
|
64
|
-
maxIters: 50, // 子智能体是聚焦的只读子任务:显式收敛轮数,不随顶层上限(120)放大,控成本与时延
|
|
65
|
-
io: { write: () => {} }, // 静默:子智能体过程不刷主终端
|
|
66
|
-
});
|
|
67
|
-
if (r?.error) return `子智能体执行失败:${r.error}`;
|
|
68
|
-
const last = [...messages].reverse().find((m) => m.role === 'assistant' && m.content);
|
|
69
|
-
return last ? last.content : '(子智能体未产出结论)';
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
return { readOnly: true, spec, run };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function subagentPrompt(cwd, context) {
|
|
76
|
-
let p =
|
|
77
|
-
'你是一个专注的子智能体,只负责完成主智能体委派的单一子任务。要求:聚焦目标;'
|
|
78
|
-
+ '用可用的只读工具(读文件 / 搜索 / 列目录等)收集信息;直接输出结构清晰、可被主智能体直接复用的最终结论,'
|
|
79
|
-
+ '不要寒暄,也不要复述任务本身。\n工作目录:' + cwd + '\n';
|
|
80
|
-
if (context && String(context).trim()) p += '\n## 主智能体提供的背景\n' + context + '\n';
|
|
81
|
-
return p;
|
|
82
|
-
}
|
|
1
|
+
import t from"node:fs";import e from"node:path";import n from"node:os";import{runTurn as r}from"./agent.js";import{createPermissions as o}from"./permissions.js";import{parseFrontmatter as i}from"./skills.js";export function loadAgents(r){const o=[r?e.join(r,".huicai","agents"):null,e.join(n.homedir(),".huicai","agents")].filter(Boolean),a=new Map;for(const n of o){let r;try{r=t.readdirSync(n).filter(t=>t.endsWith(".md"))}catch{continue}for(const o of r){const r=o.replace(/\.md$/,"").trim();if(r&&!a.has(r))try{const s=t.readFileSync(e.join(n,o),"utf8"),c=i(s);a.set(r,{name:(c.name||r).trim(),description:c.description||"",prompt:c.body||s})}catch{}}}return[...a.values()]}async function a({config:t,cwd:e,readOnlyExtras:n,depth:i,objective:a,context:s,agentDef:c}){if(!a)return"未提供子任务目标 objective";const p=c?c.prompt.trim()+"\n\n工作目录:"+e+(s&&String(s).trim()?"\n\n## 背景\n"+s:""):function(t,e){let n="你是一个专注的子智能体,只负责完成主智能体委派的单一子任务。要求:聚焦目标;用可用的只读工具(读文件 / 搜索 / 列目录等)收集信息;直接输出结构清晰、可被主智能体直接复用的最终结论,不要寒暄,也不要复述任务本身。\n工作目录:"+t+"\n";e&&String(e).trim()&&(n+="\n## 主智能体提供的背景\n"+e+"\n");return n}(e,s),l=[{role:"system",content:p},{role:"user",content:a}],d=o({yolo:!1,ask:null}),m=makeSubagentTool({config:t,cwd:e,extraTools:n,depth:i+1}),g=m?[...n,m]:n,f=await r({config:t,messages:l,permissions:d,cwd:e,extraTools:g,depth:i+1,maxIters:50,io:{write:()=>{}}});if(f?.error)return`子智能体执行失败:${f.error}`;const u=[...l].reverse().find(t=>"assistant"===t.role&&t.content);return u?u.content:"(子智能体未产出结论)"}export function makeSubagentTool({config:t,cwd:e,extraTools:n=[],depth:r=0}){if(r>=2)return null;const o=loadAgents(e),i={type:"function",function:{name:"dispatch_agent",description:"把一个明确、独立、自足的子任务委派给子智能体:它在隔离上下文中自主使用(只读)工具完成后只回传结论。适合可并行的调研/检索/分析类子任务,让主线程上下文保持精简。可多次调用以分解复杂任务。",parameters:{type:"object",properties:{objective:{type:"string",description:"要子智能体完成的单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需的背景信息"},...o.length?{agent:{type:"string",enum:o.map(t=>t.name),description:"可选:使用某个已定义的子智能体人格 — "+o.map(t=>t.name+(t.description?`(${t.description.slice(0,40)})`:"")).join(";")}}:{}},required:["objective"]}}},s=n.filter(t=>t.readOnly);return{readOnly:!0,spec:i,run:async n=>{const i=n?.agent?o.find(t=>t.name===n.agent):null;return a({config:t,cwd:e,readOnlyExtras:s,depth:r,objective:String(n?.objective||"").trim(),context:n?.context,agentDef:i})}}}export function makeDispatchAgentsTool({config:t,cwd:e,extraTools:n=[],depth:r=0}){if(r>=2)return null;const o=loadAgents(e),i=n.filter(t=>t.readOnly);return{readOnly:!0,spec:{type:"function",function:{name:"dispatch_agents",description:"一次性把多个**独立、可并行**的子任务同时委派给多个子智能体(隔离上下文、只读工具),并行执行后聚合各自结论。适合把一个较大目标拆成 2-6 个可并行的调研/检索/分析子任务同时推进,显著减少串行等待。每个子任务需自足。",parameters:{type:"object",properties:{tasks:{type:"array",description:"并行子任务数组(建议 2-6 个,上限 8)",items:{type:"object",properties:{objective:{type:"string",description:"单一、明确、自足的子任务"},context:{type:"string",description:"可选:完成该子任务所需背景"},...o.length?{agent:{type:"string",enum:o.map(t=>t.name),description:"可选:指定子智能体人格"}}:{}},required:["objective"]}}},required:["tasks"]}}},run:async n=>{const s=Array.isArray(n?.tasks)?n.tasks.slice(0,8):[];if(!s.length)return"未提供 tasks(需要一个子任务数组)";return(await async function(t,e,n){const r=new Array(t.length);let o=0;return await Promise.all(Array.from({length:Math.max(1,Math.min(e,t.length))},async()=>{for(;o<t.length;){const e=o++;r[e]=await n(t[e],e)}})),r}(s,4,async n=>{const s=n?.agent?o.find(t=>t.name===n.agent):null,c=await a({config:t,cwd:e,readOnlyExtras:i,depth:r,objective:String(n?.objective||"").trim(),context:n?.context,agentDef:s});return{objective:String(n?.objective||"").replace(/\s+/g," ").slice(0,80),agent:n?.agent,out:c}})).map((t,e)=>`【子任务 ${e+1}${t.agent?" · "+t.agent:""}】${t.objective}\n${t.out}`).join("\n\n---\n\n")}}}
|
package/src/todo.js
CHANGED
|
@@ -1,75 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 模型用 todo_write 维护当前任务的步骤清单(每次提交完整列表),CLI 实时渲染进度。
|
|
3
|
-
// 纯客户端状态(不落盘、不进模型上下文之外的系统),状态由调用方(会话)持有以跨轮保留。
|
|
4
|
-
|
|
5
|
-
const STATUSES = new Set(['pending', 'in_progress', 'completed']);
|
|
6
|
-
|
|
7
|
-
export const TODO_TOOL_NAME = 'todo_write';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* 构造 todo_write 工具。
|
|
11
|
-
* @param {{state:{list:any[]}, emit:(list:any[])=>void}} p state 由调用方持有(跨轮保留);emit 用于 UI 渲染
|
|
12
|
-
* @returns {{spec:object, run:Function, readOnly:boolean}}
|
|
13
|
-
*/
|
|
14
|
-
export function makeTodoTool({ state, emit }) {
|
|
15
|
-
const spec = {
|
|
16
|
-
type: 'function',
|
|
17
|
-
function: {
|
|
18
|
-
name: TODO_TOOL_NAME,
|
|
19
|
-
description:
|
|
20
|
-
'维护当前任务的步骤清单,帮助用户看到进度。多步骤/非琐碎任务开始前先列出计划,'
|
|
21
|
-
+ '每完成一步就更新状态(同一时刻至多一项 in_progress)。每次调用需提交完整清单(全量覆盖)。'
|
|
22
|
-
+ '单步小事无需使用。',
|
|
23
|
-
parameters: {
|
|
24
|
-
type: 'object',
|
|
25
|
-
properties: {
|
|
26
|
-
todos: {
|
|
27
|
-
type: 'array',
|
|
28
|
-
description: '完整的任务清单(全量覆盖旧清单)',
|
|
29
|
-
items: {
|
|
30
|
-
type: 'object',
|
|
31
|
-
properties: {
|
|
32
|
-
content: { type: 'string', description: '任务内容(简短祈使句)' },
|
|
33
|
-
status: { type: 'string', enum: ['pending', 'in_progress', 'completed'], description: '任务状态' },
|
|
34
|
-
},
|
|
35
|
-
required: ['content', 'status'],
|
|
36
|
-
},
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
required: ['todos'],
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const run = async (args) => {
|
|
45
|
-
const raw = Array.isArray(args?.todos) ? args.todos : null;
|
|
46
|
-
if (!raw) return 'todo_write 需要 todos 数组';
|
|
47
|
-
const list = raw
|
|
48
|
-
.map((t) => ({
|
|
49
|
-
content: String(t?.content ?? '').trim(),
|
|
50
|
-
status: STATUSES.has(t?.status) ? t.status : 'pending',
|
|
51
|
-
}))
|
|
52
|
-
.filter((t) => t.content)
|
|
53
|
-
.slice(0, 50);
|
|
54
|
-
state.list = list;
|
|
55
|
-
try {
|
|
56
|
-
emit?.(list);
|
|
57
|
-
} catch {
|
|
58
|
-
/* 渲染失败不影响流程 */
|
|
59
|
-
}
|
|
60
|
-
const done = list.filter((t) => t.status === 'completed').length;
|
|
61
|
-
return `已更新任务清单:共 ${list.length} 项,完成 ${done} 项。`;
|
|
62
|
-
};
|
|
63
|
-
|
|
64
|
-
return { readOnly: true, spec, run };
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
/** 把清单渲染为多行文本(REPL/降级输出用)。 */
|
|
68
|
-
export function formatTodos(list) {
|
|
69
|
-
return (list || [])
|
|
70
|
-
.map((t) => {
|
|
71
|
-
const mark = t.status === 'completed' ? '☑' : t.status === 'in_progress' ? '◐' : '☐';
|
|
72
|
-
return `${mark} ${t.content}`;
|
|
73
|
-
})
|
|
74
|
-
.join('\n');
|
|
75
|
-
}
|
|
1
|
+
const t=new Set(["pending","in_progress","completed"]);export const TODO_TOOL_NAME="todo_write";export function makeTodoTool({state:e,emit:o}){return{readOnly:!0,spec:{type:"function",function:{name:"todo_write",description:"维护当前任务的步骤清单,帮助用户看到进度。多步骤/非琐碎任务开始前先列出计划,每完成一步就更新状态(同一时刻至多一项 in_progress)。每次调用需提交完整清单(全量覆盖)。单步小事无需使用。",parameters:{type:"object",properties:{todos:{type:"array",description:"完整的任务清单(全量覆盖旧清单)",items:{type:"object",properties:{content:{type:"string",description:"任务内容(简短祈使句)"},status:{type:"string",enum:["pending","in_progress","completed"],description:"任务状态"}},required:["content","status"]}}},required:["todos"]}}},run:async n=>{const r=Array.isArray(n?.todos)?n.todos:null;if(!r)return"todo_write 需要 todos 数组";const s=r.map(e=>({content:String(e?.content??"").trim(),status:t.has(e?.status)?e.status:"pending"})).filter(t=>t.content).slice(0,50);e.list=s;try{o?.(s)}catch{}const i=s.filter(t=>"completed"===t.status).length;return`已更新任务清单:共 ${s.length} 项,完成 ${i} 项。`}}}export function formatTodos(t){return(t||[]).map(t=>`${"completed"===t.status?"☑":"in_progress"===t.status?"◐":"☐"} ${t.content}`).join("\n")}
|
package/src/tools.js
CHANGED
|
@@ -1,132 +1 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import * as ex from './executors.js';
|
|
4
|
-
|
|
5
|
-
const obj = (properties, required) => ({ type: 'object', properties, required: required || [] });
|
|
6
|
-
const str = (description) => ({ type: 'string', description });
|
|
7
|
-
const int = (description) => ({ type: 'integer', description });
|
|
8
|
-
const bool = (description) => ({ type: 'boolean', description });
|
|
9
|
-
|
|
10
|
-
/** @type {any[]} 暴露给模型的工具(函数调用规格)。 */
|
|
11
|
-
export const TOOL_SPECS = [
|
|
12
|
-
{
|
|
13
|
-
type: 'function',
|
|
14
|
-
function: {
|
|
15
|
-
name: 'read_file',
|
|
16
|
-
description:
|
|
17
|
-
'读取文本文件内容,每行带「行号→」前缀(行号仅供定位,不属于文件内容,编辑时不要包含它)。'
|
|
18
|
-
+ '默认最多 2000 行,可用 offset(起始行,1基)/limit(行数) 分段读取大文件。',
|
|
19
|
-
parameters: obj(
|
|
20
|
-
{ path: str('相对项目根的文件路径'), offset: int('可选:起始行(1基)'), limit: int('可选:读取行数') },
|
|
21
|
-
['path'],
|
|
22
|
-
),
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
type: 'function',
|
|
27
|
-
function: {
|
|
28
|
-
name: 'list_files',
|
|
29
|
-
description: '列出目录下的文件树(跳过 .git/node_modules 等)。可选 path 指定子目录(默认项目根)。',
|
|
30
|
-
parameters: obj({ path: str('可选:子目录路径,默认当前项目根') }, []),
|
|
31
|
-
},
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
type: 'function',
|
|
35
|
-
function: {
|
|
36
|
-
name: 'glob',
|
|
37
|
-
description: '按 glob 模式查找文件(支持 ** 与 *),如 "**/*.ts"、"src/**/*.java"。',
|
|
38
|
-
parameters: obj({ pattern: str('glob 模式') }, ['pattern']),
|
|
39
|
-
},
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
type: 'function',
|
|
43
|
-
function: {
|
|
44
|
-
name: 'grep',
|
|
45
|
-
description:
|
|
46
|
-
'在文件内容中按正则搜索,返回 匹配文件:行号: 该行。可选 path 限定目录、glob 限定文件名模式、'
|
|
47
|
-
+ 'ignore_case 忽略大小写、files_only 只列出命中文件。',
|
|
48
|
-
parameters: obj(
|
|
49
|
-
{
|
|
50
|
-
pattern: str('正则表达式'),
|
|
51
|
-
path: str('可选:搜索目录,默认项目根'),
|
|
52
|
-
glob: str('可选:文件名 glob 过滤'),
|
|
53
|
-
ignore_case: bool('可选:忽略大小写(默认否)'),
|
|
54
|
-
files_only: bool('可选:只返回命中文件列表,不含匹配行(默认否)'),
|
|
55
|
-
max_results: int('可选:最多返回条数(默认 200,上限 500)'),
|
|
56
|
-
},
|
|
57
|
-
['pattern'],
|
|
58
|
-
),
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
{
|
|
62
|
-
type: 'function',
|
|
63
|
-
function: {
|
|
64
|
-
name: 'write_file',
|
|
65
|
-
description: '把文本写入文件(覆盖式,自动建父目录)。仅限当前项目目录内。仅用于新建文件或全量重写;小改动优先 edit_file。',
|
|
66
|
-
parameters: obj({ path: str('相对项目根的文件路径'), content: str('完整文件内容') }, ['path', 'content']),
|
|
67
|
-
},
|
|
68
|
-
},
|
|
69
|
-
{
|
|
70
|
-
type: 'function',
|
|
71
|
-
function: {
|
|
72
|
-
name: 'edit_file',
|
|
73
|
-
description:
|
|
74
|
-
'精确替换文件中的片段:old_string 必须是文件原文(不含 read_file 的行号前缀)且在文件中唯一'
|
|
75
|
-
+ '(除非 replace_all=true)。仅限项目目录内。',
|
|
76
|
-
parameters: obj(
|
|
77
|
-
{
|
|
78
|
-
path: str('相对项目根的文件路径'),
|
|
79
|
-
old_string: str('被替换的原文(需在文件中唯一,除非 replace_all;不要包含行号前缀)'),
|
|
80
|
-
new_string: str('替换为的新内容'),
|
|
81
|
-
replace_all: bool('是否替换全部匹配(默认否)'),
|
|
82
|
-
},
|
|
83
|
-
['path', 'old_string', 'new_string'],
|
|
84
|
-
),
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
type: 'function',
|
|
89
|
-
function: {
|
|
90
|
-
name: 'run_bash',
|
|
91
|
-
description:
|
|
92
|
-
'在项目目录下执行一条 shell 命令并返回输出(stdout+stderr+退出码)。用于构建、测试、git、脚手架等。'
|
|
93
|
-
+ '默认超时 120s(可用 timeout 调整)。避免用它读文件/搜索(用 read_file/grep 更高效)。',
|
|
94
|
-
parameters: obj({ command: str('要执行的 shell 命令'), timeout: int('可选:超时毫秒') }, ['command']),
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
type: 'function',
|
|
99
|
-
function: {
|
|
100
|
-
name: 'fetch_url',
|
|
101
|
-
description:
|
|
102
|
-
'抓取一个 http/https 网页或接口(GET),HTML 自动转为纯文本。用于查在线文档、API 说明、Issue 等。'
|
|
103
|
-
+ '结果可能很长,尽量带着明确目的使用。',
|
|
104
|
-
parameters: obj({ url: str('完整 URL(http/https)'), timeout: int('可选:超时毫秒,默认 30000') }, ['url']),
|
|
105
|
-
},
|
|
106
|
-
},
|
|
107
|
-
];
|
|
108
|
-
|
|
109
|
-
/** 只读工具(默认自动放行,无需审批)。fetch_url 涉及外发请求,需用户授权。 */
|
|
110
|
-
export const READONLY_TOOLS = new Set(['read_file', 'list_files', 'glob', 'grep']);
|
|
111
|
-
|
|
112
|
-
const EXECUTORS = {
|
|
113
|
-
read_file: ex.readFile,
|
|
114
|
-
list_files: ex.listFiles,
|
|
115
|
-
glob: ex.glob,
|
|
116
|
-
grep: ex.grep,
|
|
117
|
-
write_file: ex.writeFile,
|
|
118
|
-
edit_file: ex.editFile,
|
|
119
|
-
run_bash: ex.runBash,
|
|
120
|
-
fetch_url: ex.fetchUrl,
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
/** 执行一个本机工具,返回文本结果(回填给模型)。抛错转为可读文本,保证循环不中断。 */
|
|
124
|
-
export async function runTool(name, args, cwd) {
|
|
125
|
-
const fn = EXECUTORS[name];
|
|
126
|
-
if (!fn) return `未知工具:${name}`;
|
|
127
|
-
try {
|
|
128
|
-
return await fn(cwd, args || {});
|
|
129
|
-
} catch (e) {
|
|
130
|
-
return `工具执行失败:${e.message}`;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
1
|
+
import*as e from"./executors.js";import{searchCode as t}from"./codeindex.js";const r=(e,t)=>({type:"object",properties:e,required:t||[]}),n=e=>({type:"string",description:e}),i=e=>({type:"integer",description:e}),o=e=>({type:"boolean",description:e});export const TOOL_SPECS=[{type:"function",function:{name:"read_file",description:"读取文本文件内容,每行带「行号→」前缀(行号仅供定位,不属于文件内容,编辑时不要包含它)。默认最多 2000 行,可用 offset(起始行,1基)/limit(行数) 分段读取大文件。",parameters:r({path:n("相对项目根的文件路径"),offset:i("可选:起始行(1基)"),limit:i("可选:读取行数")},["path"])}},{type:"function",function:{name:"list_files",description:"列出目录下的文件树(跳过 .git/node_modules 等)。可选 path 指定子目录(默认项目根)。",parameters:r({path:n("可选:子目录路径,默认当前项目根")},[])}},{type:"function",function:{name:"glob",description:'按 glob 模式查找文件(支持 ** 与 *),如 "**/*.ts"、"src/**/*.java"。',parameters:r({pattern:n("glob 模式")},["pattern"])}},{type:"function",function:{name:"search_code",description:"按「概念/功能」检索最相关的代码块(BM25 相关性排序,比 grep 更懂「哪段代码在做这件事」)。适合「登录逻辑在哪」「哪里处理支付回调」这类找相关实现;要找精确字符串仍用 grep。返回 文件:行范围 + 代码块。",parameters:r({query:n("要找的功能/概念(自然语言或关键词组合,多写几个相关词更准)"),k:i("可选:返回块数,默认 8,上限 20")},["query"])}},{type:"function",function:{name:"grep",description:"在文件内容中按正则搜索,返回 匹配文件:行号: 该行。可选 path 限定目录、glob 限定文件名模式、ignore_case 忽略大小写、files_only 只列出命中文件。",parameters:r({pattern:n("正则表达式"),path:n("可选:搜索目录,默认项目根"),glob:n("可选:文件名 glob 过滤"),ignore_case:o("可选:忽略大小写(默认否)"),files_only:o("可选:只返回命中文件列表,不含匹配行(默认否)"),max_results:i("可选:最多返回条数(默认 200,上限 500)")},["pattern"])}},{type:"function",function:{name:"write_file",description:"把文本写入文件(覆盖式,自动建父目录)。仅限当前项目目录内。仅用于新建文件或全量重写;小改动优先 edit_file。",parameters:r({path:n("相对项目根的文件路径"),content:n("完整文件内容")},["path","content"])}},{type:"function",function:{name:"edit_file",description:"精确替换文件中的片段:old_string 必须是文件原文(不含 read_file 的行号前缀)且在文件中唯一(除非 replace_all=true)。仅限项目目录内。",parameters:r({path:n("相对项目根的文件路径"),old_string:n("被替换的原文(需在文件中唯一,除非 replace_all;不要包含行号前缀)"),new_string:n("替换为的新内容"),replace_all:o("是否替换全部匹配(默认否)")},["path","old_string","new_string"])}},{type:"function",function:{name:"run_bash",description:"在项目目录下执行一条 shell 命令并返回输出(stdout+stderr+退出码)。用于构建、测试、git、脚手架等。默认超时 120s(可用 timeout 调整)。避免用它读文件/搜索(用 read_file/grep 更高效)。",parameters:r({command:n("要执行的 shell 命令"),timeout:i("可选:超时毫秒")},["command"])}},{type:"function",function:{name:"fetch_url",description:"抓取一个 http/https 网页或接口(GET),HTML 自动转为纯文本。用于查在线文档、API 说明、Issue 等。结果可能很长,尽量带着明确目的使用。",parameters:r({url:n("完整 URL(http/https)"),timeout:i("可选:超时毫秒,默认 30000")},["url"])}}];export const READONLY_TOOLS=new Set(["read_file","list_files","glob","grep","search_code"]);const s={read_file:e.readFile,list_files:e.listFiles,glob:e.glob,grep:e.grep,search_code:t,write_file:e.writeFile,edit_file:e.editFile,run_bash:e.runBash,fetch_url:e.fetchUrl};export async function runTool(e,t,r,n){const i=s[e];if(!i)return`未知工具:${e}`;try{return await i(r,t||{},n)}catch(e){return`工具执行失败:${e.message}`}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{streamChat as t}from"./api.js";import{DEFAULT_MODEL as e}from"./config.js";const n=/^(agent|workflow|datahuman)-/;export function textOf(t){return"string"==typeof t?t:Array.isArray(t)?t.map(t=>"text"===t?.type?t.text:"image_url"===t?.type?"[图片]":"").join("\n"):""}export function toMarkdown(t){const e=t||{},n=["# HuiCai 对话记录","",`- 会话:${e.id||""}`,`- 模型:${e.model||""}`,`- 导出时间:${(new Date).toLocaleString()}`,"","---",""];for(const t of e.messages||[])if("system"!==t.role)if("user"===t.role)n.push("### 🧑 用户","",textOf(t.content).trim()||"(空)","");else if("assistant"===t.role){const e=textOf(t.content).trim();e&&n.push("### 🤖 助手","",e,"");for(const e of t.tool_calls||[]){const t=String(e.function?.arguments||"").replace(/\s+/g," ").slice(0,200);n.push(`> 🔧 \`${e.function?.name}(${t})\``,"")}}else if("tool"===t.role){const e=String(t.content||"").slice(0,1500);n.push("> 工具结果:","","```",e,"```","")}return n.join("\n")}export async function distillMemory(o,r,s=""){const i=n.test(String(r.model||""))?r.compactModel||e:r.model,l=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,600)}`).join("\n").slice(0,16e3);if(!l.trim())return[];const c=(s?"【已有记忆(勿重复)】\n"+s+"\n\n":"")+"【本次记录】\n"+l,a=((await t({baseUrl:r.baseUrl,apiKey:r.apiKey,model:i,messages:[{role:"system",content:"你是项目知识提炼器。从下面这段对话/执行记录中,提炼出**值得跨会话长期记住**的稳定事实/约定/结论/踩坑(如构建与测试命令、架构决定、代码约定、易错点、外部依赖要点)。要求:每条一行,以「- 」开头,简洁具体;只输出真正长期有用的,不要一次性细节、不要大段代码;若与「已有记忆」重复则跳过;最多 5 条;没有则输出「(无)」。"},{role:"user",content:c}]})).content||"").trim();return!a||/^[((]无[))]$/.test(a)?[]:a.split("\n").map(t=>t.replace(/^[-*]\s*/,"").trim()).filter(t=>t&&!/^[((]无[))]$/.test(t)).slice(0,5)}export async function recapConversation(o,r){const s=n.test(String(r.model||""))?r.compactModel||e:r.model,i=(o||[]).filter(t=>"system"!==t.role).map(t=>`[${t.role}] ${textOf(t.content).replace(/\s+/g," ").slice(0,500)}`).join("\n").slice(0,12e3);if(!i.trim())return"(对话尚无内容)";return((await t({baseUrl:r.baseUrl,apiKey:r.apiKey,model:s,messages:[{role:"system",content:"用一句话(不超过40个汉字)概括这段对话到目前为止在做什么、进展如何。只输出这一句话,不要引号、不要多余说明。"},{role:"user",content:i}]})).content||"").trim().split("\n")[0]||"(无法生成小结)"}
|