@huicai/cli 2026.711.0 → 2026.711.24617
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/package.json +4 -3
- package/src/advisor.js +1 -46
- package/src/agent.js +1 -518
- package/src/api.js +1 -359
- package/src/autofix.js +1 -90
- package/src/banner.js +1 -83
- package/src/checkpoints.js +1 -99
- package/src/clipboard.js +1 -34
- package/src/codeindex.js +1 -229
- package/src/colors.js +1 -47
- package/src/commands.js +1 -1294
- package/src/compaction.js +1 -144
- package/src/config.js +1 -86
- package/src/consult.js +1 -65
- package/src/context-report.js +1 -119
- package/src/context.js +1 -152
- package/src/cron.js +1 -64
- package/src/daemon.js +1 -62
- package/src/device.js +1 -88
- package/src/diffview.js +1 -115
- package/src/doctor.js +1 -72
- package/src/executors.js +1 -391
- package/src/forks.js +1 -59
- package/src/gitflow.js +1 -140
- package/src/hooks.js +1 -207
- package/src/ide.js +1 -53
- package/src/input.js +1 -182
- package/src/insights.js +1 -102
- package/src/kbsync.js +1 -141
- package/src/markdown.js +0 -0
- package/src/markup-filter.js +1 -85
- package/src/mcp.js +1 -207
- package/src/memory.js +1 -161
- package/src/permissions.js +1 -120
- package/src/render.js +1 -138
- package/src/repl.js +1 -271
- package/src/scan.js +1 -84
- package/src/schedule.js +1 -48
- package/src/session.js +1 -140
- package/src/settings.js +0 -0
- package/src/skills.js +1 -248
- package/src/subagent.js +1 -183
- package/src/todo.js +1 -75
- package/src/tools.js +1 -147
- package/src/transcript.js +1 -94
- package/src/tui/app.js +1 -666
- package/src/usage.js +1 -43
- package/src/watch.js +1 -74
package/src/schedule.js
CHANGED
|
@@ -1,48 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 不自带调度器——把定时职责交给成熟的系统 cron;本模块负责登记 + 生成可粘贴的 crontab 条目。
|
|
3
|
-
|
|
4
|
-
import fs from 'node:fs';
|
|
5
|
-
import path from 'node:path';
|
|
6
|
-
import os from 'node:os';
|
|
7
|
-
|
|
8
|
-
export const SCHEDULE_FILE = path.join(os.homedir(), '.huicai', 'schedule.json');
|
|
9
|
-
|
|
10
|
-
function read() {
|
|
11
|
-
try {
|
|
12
|
-
const j = JSON.parse(fs.readFileSync(SCHEDULE_FILE, 'utf8'));
|
|
13
|
-
return Array.isArray(j) ? j : [];
|
|
14
|
-
} catch {
|
|
15
|
-
return [];
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function write(list) {
|
|
20
|
-
fs.mkdirSync(path.dirname(SCHEDULE_FILE), { recursive: true });
|
|
21
|
-
fs.writeFileSync(SCHEDULE_FILE, JSON.stringify(list, null, 2) + '\n', { mode: 0o600 });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function listTasks() {
|
|
25
|
-
return read();
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/** 添加定时任务。@param {{cron:string, prompt:string, cwd:string}} t */
|
|
29
|
-
export function addTask({ cron, prompt, cwd }) {
|
|
30
|
-
const list = read();
|
|
31
|
-
const id = Date.now().toString(36) + Math.random().toString(36).slice(2, 4);
|
|
32
|
-
list.push({ id, cron, prompt, cwd, created: Date.now() });
|
|
33
|
-
write(list);
|
|
34
|
-
return { id };
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function removeTask(id) {
|
|
38
|
-
const list = read();
|
|
39
|
-
const next = list.filter((t) => t.id !== id);
|
|
40
|
-
write(next);
|
|
41
|
-
return next.length < list.length;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** 生成一条 crontab 行(供 crontab -e 粘贴)。 */
|
|
45
|
-
export function crontabLine(t, bin = 'huicai') {
|
|
46
|
-
return `${t.cron} cd ${JSON.stringify(t.cwd)} && ${bin} -p ${JSON.stringify(t.prompt)} --yolo --plain `
|
|
47
|
-
+ `>> ${JSON.stringify(path.join(os.homedir(), '.huicai', 'schedule-' + t.id + '.log'))} 2>&1`;
|
|
48
|
-
}
|
|
1
|
+
import r from"node:fs";import n from"node:path";import t from"node:os";export const SCHEDULE_FILE=n.join(t.homedir(),".huicai","schedule.json");function o(){try{const n=JSON.parse(r.readFileSync(SCHEDULE_FILE,"utf8"));return Array.isArray(n)?n:[]}catch{return[]}}function i(t){r.mkdirSync(n.dirname(SCHEDULE_FILE),{recursive:!0}),r.writeFileSync(SCHEDULE_FILE,JSON.stringify(t,null,2)+"\n",{mode:384})}export function listTasks(){return o()}export function addTask({cron:r,prompt:n,cwd:t}){const e=o(),c=Date.now().toString(36)+Math.random().toString(36).slice(2,4);return e.push({id:c,cron:r,prompt:n,cwd:t,created:Date.now()}),i(e),{id:c}}export function removeTask(r){const n=o(),t=n.filter(n=>n.id!==r);return i(t),t.length<n.length}export function crontabLine(r,o="huicai"){return`${r.cron} cd ${JSON.stringify(r.cwd)} && ${o} -p ${JSON.stringify(r.prompt)} --yolo --plain >> ${JSON.stringify(n.join(t.homedir(),".huicai","schedule-"+r.id+".log"))} 2>&1`}
|
package/src/session.js
CHANGED
|
@@ -1,140 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 支持列出、恢复、删除。按项目目录(cwd)归属,使 --continue/--resume 只看当前项目的会话。
|
|
3
|
-
// 所有磁盘操作 best-effort:失败仅告警,绝不让保存/读取异常中断对话本身。
|
|
4
|
-
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import { CONFIG_DIR } from './config.js';
|
|
8
|
-
|
|
9
|
-
export const SESSIONS_DIR = path.join(CONFIG_DIR, 'sessions');
|
|
10
|
-
|
|
11
|
-
function ensureDir() {
|
|
12
|
-
fs.mkdirSync(SESSIONS_DIR, { recursive: true });
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
function fileOf(id) {
|
|
16
|
-
return path.join(SESSIONS_DIR, id + '.json');
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function genId() {
|
|
20
|
-
return Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 6);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** 由首条用户消息派生标题(截断)。 */
|
|
24
|
-
function deriveTitle(messages) {
|
|
25
|
-
const firstUser = (messages || []).find((m) => m.role === 'user');
|
|
26
|
-
if (!firstUser || !firstUser.content) return '(空会话)';
|
|
27
|
-
return String(firstUser.content).replace(/\s+/g, ' ').trim().slice(0, 60) || '(空会话)';
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** 会话中的用户轮数(对话往返次数的近似)。 */
|
|
31
|
-
export function turnCount(messages) {
|
|
32
|
-
return (messages || []).filter((m) => m.role === 'user').length;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* 新建会话对象(内存态);messages 需已含 system 消息。落盘发生在首次 saveSession。
|
|
37
|
-
*/
|
|
38
|
-
export function createSession({ cwd, model, messages }) {
|
|
39
|
-
const now = Date.now();
|
|
40
|
-
return {
|
|
41
|
-
id: genId(),
|
|
42
|
-
cwd,
|
|
43
|
-
model,
|
|
44
|
-
createdAt: now,
|
|
45
|
-
updatedAt: now,
|
|
46
|
-
title: '',
|
|
47
|
-
messages: messages || [],
|
|
48
|
-
// 非破坏式上下文压缩状态(摘要独立存储,messages 永远保留全量原文)
|
|
49
|
-
compaction: {},
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/** 保存会话(覆盖写,0600)。best-effort。 */
|
|
54
|
-
export function saveSession(session) {
|
|
55
|
-
if (!session || !session.id) return;
|
|
56
|
-
try {
|
|
57
|
-
ensureDir();
|
|
58
|
-
session.updatedAt = Date.now();
|
|
59
|
-
if (!session.title) session.title = deriveTitle(session.messages);
|
|
60
|
-
fs.writeFileSync(fileOf(session.id), JSON.stringify(session), { mode: 0o600 });
|
|
61
|
-
} catch (e) {
|
|
62
|
-
process.stderr.write(`(会话保存失败,不影响对话:${e.message})\n`);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** 读取会话;不存在或损坏返回 null。 */
|
|
67
|
-
export function loadSession(id) {
|
|
68
|
-
if (!id) return null;
|
|
69
|
-
try {
|
|
70
|
-
const j = JSON.parse(fs.readFileSync(fileOf(id), 'utf8'));
|
|
71
|
-
if (!j || !Array.isArray(j.messages)) return null;
|
|
72
|
-
if (!j.id) j.id = id;
|
|
73
|
-
if (!j.compaction || typeof j.compaction !== 'object') j.compaction = {}; // 旧会话兼容
|
|
74
|
-
return j;
|
|
75
|
-
} catch {
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* 列出会话(按更新时间倒序)。cwd 非空时仅列该项目目录的会话。
|
|
82
|
-
* @returns {{id,title,cwd,model,updatedAt,turns}[]}
|
|
83
|
-
*/
|
|
84
|
-
export function listSessions(cwd) {
|
|
85
|
-
let files;
|
|
86
|
-
try {
|
|
87
|
-
ensureDir();
|
|
88
|
-
files = fs.readdirSync(SESSIONS_DIR).filter((f) => f.endsWith('.json'));
|
|
89
|
-
} catch {
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
92
|
-
const out = [];
|
|
93
|
-
for (const f of files) {
|
|
94
|
-
try {
|
|
95
|
-
const j = JSON.parse(fs.readFileSync(path.join(SESSIONS_DIR, f), 'utf8'));
|
|
96
|
-
if (cwd && j.cwd !== cwd) continue;
|
|
97
|
-
out.push({
|
|
98
|
-
id: j.id || f.replace(/\.json$/, ''),
|
|
99
|
-
title: j.title || deriveTitle(j.messages),
|
|
100
|
-
cwd: j.cwd,
|
|
101
|
-
model: j.model,
|
|
102
|
-
updatedAt: j.updatedAt || 0,
|
|
103
|
-
turns: turnCount(j.messages),
|
|
104
|
-
});
|
|
105
|
-
} catch {
|
|
106
|
-
// 跳过损坏文件
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
out.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
110
|
-
return out;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** 当前项目最近一次会话的 id;无则 null。 */
|
|
114
|
-
export function mostRecent(cwd) {
|
|
115
|
-
const list = listSessions(cwd);
|
|
116
|
-
return list.length ? list[0].id : null;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** 删除会话文件。 */
|
|
120
|
-
export function deleteSession(id) {
|
|
121
|
-
try {
|
|
122
|
-
fs.unlinkSync(fileOf(id));
|
|
123
|
-
return true;
|
|
124
|
-
} catch {
|
|
125
|
-
return false;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** 相对时间(如「3 分钟前」)。 */
|
|
130
|
-
export function relTime(ts) {
|
|
131
|
-
if (!ts) return '未知时间';
|
|
132
|
-
const s = Math.max(0, Math.floor((Date.now() - ts) / 1000));
|
|
133
|
-
if (s < 60) return s + ' 秒前';
|
|
134
|
-
const m = Math.floor(s / 60);
|
|
135
|
-
if (m < 60) return m + ' 分钟前';
|
|
136
|
-
const h = Math.floor(m / 60);
|
|
137
|
-
if (h < 24) return h + ' 小时前';
|
|
138
|
-
const d = Math.floor(h / 24);
|
|
139
|
-
return d + ' 天前';
|
|
140
|
-
}
|
|
1
|
+
import t from"node:fs";import e from"node:path";import{CONFIG_DIR as n}from"./config.js";export const SESSIONS_DIR=e.join(n,"sessions");function r(){t.mkdirSync(SESSIONS_DIR,{recursive:!0})}function o(t){return e.join(SESSIONS_DIR,t+".json")}function i(t){const e=(t||[]).find(t=>"user"===t.role);return e&&e.content&&String(e.content).replace(/\s+/g," ").trim().slice(0,60)||"(空会话)"}export function turnCount(t){return(t||[]).filter(t=>"user"===t.role).length}export function createSession({cwd:t,model:e,messages:n}){const r=Date.now();return{id:Date.now().toString(36)+"-"+Math.random().toString(36).slice(2,6),cwd:t,model:e,createdAt:r,updatedAt:r,title:"",messages:n||[],compaction:{}}}export function saveSession(e){if(e&&e.id)try{r(),e.updatedAt=Date.now(),e.title||(e.title=i(e.messages)),t.writeFileSync(o(e.id),JSON.stringify(e),{mode:384})}catch(t){process.stderr.write(`(会话保存失败,不影响对话:${t.message})\n`)}}export function loadSession(e){if(!e)return null;try{const n=JSON.parse(t.readFileSync(o(e),"utf8"));return n&&Array.isArray(n.messages)?(n.id||(n.id=e),n.compaction&&"object"==typeof n.compaction||(n.compaction={}),n):null}catch{return null}}export function listSessions(n){let o;try{r(),o=t.readdirSync(SESSIONS_DIR).filter(t=>t.endsWith(".json"))}catch{return[]}const s=[];for(const r of o)try{const o=JSON.parse(t.readFileSync(e.join(SESSIONS_DIR,r),"utf8"));if(n&&o.cwd!==n)continue;s.push({id:o.id||r.replace(/\.json$/,""),title:o.title||i(o.messages),cwd:o.cwd,model:o.model,updatedAt:o.updatedAt||0,turns:turnCount(o.messages)})}catch{}return s.sort((t,e)=>e.updatedAt-t.updatedAt),s}export function mostRecent(t){const e=listSessions(t);return e.length?e[0].id:null}export function deleteSession(e){try{return t.unlinkSync(o(e)),!0}catch{return!1}}export function relTime(t){if(!t)return"未知时间";const e=Math.max(0,Math.floor((Date.now()-t)/1e3));if(e<60)return e+" 秒前";const n=Math.floor(e/60);if(n<60)return n+" 分钟前";const r=Math.floor(n/60);if(r<24)return r+" 小时前";return Math.floor(r/24)+" 天前"}
|
package/src/settings.js
CHANGED
|
Binary file
|
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,183 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// 一个内置工具 dispatch_agent,把明确子任务交给「隔离上下文 + 只读工具」的子智能体自主完成,只回传结论。
|
|
3
|
-
// 让主线程上下文保持精简,适合并行/发散的调研、检索、分析类子任务。
|
|
4
|
-
|
|
5
|
-
import fs from 'node:fs';
|
|
6
|
-
import path from 'node:path';
|
|
7
|
-
import os from 'node:os';
|
|
8
|
-
import { runTurn } from './agent.js';
|
|
9
|
-
import { createPermissions } from './permissions.js';
|
|
10
|
-
import { parseFrontmatter } from './skills.js';
|
|
11
|
-
|
|
12
|
-
const MAX_DEPTH = 2; // 顶层 depth=0;到达上限不再提供 dispatch_agent,防无限递归
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* 加载已定义的子智能体人格:<cwd>/.huicai/agents/*.md 与 ~/.huicai/agents/*.md
|
|
16
|
-
* (frontmatter name/description + 正文=该子智能体的系统提示词)。项目级优先于全局同名。
|
|
17
|
-
* @returns {{name:string, description:string, prompt:string}[]}
|
|
18
|
-
*/
|
|
19
|
-
export function loadAgents(cwd) {
|
|
20
|
-
const dirs = [
|
|
21
|
-
cwd ? path.join(cwd, '.huicai', 'agents') : null,
|
|
22
|
-
path.join(os.homedir(), '.huicai', 'agents'),
|
|
23
|
-
].filter(Boolean);
|
|
24
|
-
const byName = new Map();
|
|
25
|
-
for (const dir of dirs) {
|
|
26
|
-
let files;
|
|
27
|
-
try {
|
|
28
|
-
files = fs.readdirSync(dir).filter((f) => f.endsWith('.md'));
|
|
29
|
-
} catch {
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
for (const f of files) {
|
|
33
|
-
const base = f.replace(/\.md$/, '').trim();
|
|
34
|
-
if (!base || byName.has(base)) continue;
|
|
35
|
-
try {
|
|
36
|
-
const raw = fs.readFileSync(path.join(dir, f), 'utf8');
|
|
37
|
-
const fm = parseFrontmatter(raw);
|
|
38
|
-
byName.set(base, { name: (fm.name || base).trim(), description: fm.description || '', prompt: fm.body || raw });
|
|
39
|
-
} catch {
|
|
40
|
-
/* 跳过坏文件 */
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
return [...byName.values()];
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** 有序有限并发 map(并发≤limit,结果按输入下标回填)。 */
|
|
48
|
-
async function mapLimit(items, limit, fn) {
|
|
49
|
-
const results = new Array(items.length);
|
|
50
|
-
let next = 0;
|
|
51
|
-
const worker = async () => {
|
|
52
|
-
while (next < items.length) {
|
|
53
|
-
const i = next++;
|
|
54
|
-
results[i] = await fn(items[i], i);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
57
|
-
await Promise.all(Array.from({ length: Math.max(1, Math.min(limit, items.length)) }, worker));
|
|
58
|
-
return results;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/** 跑一个子智能体子任务(隔离上下文 + 只读工具),返回结论文本。dispatch_agent 与 dispatch_agents 共用。 */
|
|
62
|
-
async function dispatchSubtask({ config, cwd, readOnlyExtras, depth, objective, context, agentDef }) {
|
|
63
|
-
if (!objective) return '未提供子任务目标 objective';
|
|
64
|
-
const sys = agentDef
|
|
65
|
-
? agentDef.prompt.trim() + '\n\n工作目录:' + cwd + (context && String(context).trim() ? '\n\n## 背景\n' + context : '')
|
|
66
|
-
: subagentPrompt(cwd, context);
|
|
67
|
-
const messages = [{ role: 'system', content: sys }, { role: 'user', content: objective }];
|
|
68
|
-
const permissions = createPermissions({ yolo: false, ask: null });
|
|
69
|
-
const nested = makeSubagentTool({ config, cwd, extraTools: readOnlyExtras, depth: depth + 1 });
|
|
70
|
-
const subExtra = nested ? [...readOnlyExtras, nested] : readOnlyExtras;
|
|
71
|
-
const r = await runTurn({
|
|
72
|
-
config, messages, permissions, cwd, extraTools: subExtra,
|
|
73
|
-
depth: depth + 1, maxIters: 50, io: { write: () => {} },
|
|
74
|
-
});
|
|
75
|
-
if (r?.error) return `子智能体执行失败:${r.error}`;
|
|
76
|
-
const last = [...messages].reverse().find((m) => m.role === 'assistant' && m.content);
|
|
77
|
-
return last ? last.content : '(子智能体未产出结论)';
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* 构造 dispatch_agent 工具。达到深度上限返回 null(不暴露委派)。
|
|
82
|
-
* @param {object} p
|
|
83
|
-
* @param {object} p.config 运行配置(闭包引用,读时取最新 model)
|
|
84
|
-
* @param {string} p.cwd 工作目录
|
|
85
|
-
* @param {{spec:object,run:Function,readOnly:boolean}[]} [p.extraTools] 可传给子智能体的外部工具(仅其只读子集会被采用,通常为 MCP 工具,不含 dispatch 自身)
|
|
86
|
-
* @param {number} p.depth 当前深度
|
|
87
|
-
*/
|
|
88
|
-
export function makeSubagentTool({ config, cwd, extraTools = [], depth = 0 }) {
|
|
89
|
-
if (depth >= MAX_DEPTH) return null;
|
|
90
|
-
|
|
91
|
-
const agents = loadAgents(cwd); // 已定义的子智能体人格(可选指定)
|
|
92
|
-
const spec = {
|
|
93
|
-
type: 'function',
|
|
94
|
-
function: {
|
|
95
|
-
name: 'dispatch_agent',
|
|
96
|
-
description:
|
|
97
|
-
'把一个明确、独立、自足的子任务委派给子智能体:它在隔离上下文中自主使用(只读)工具完成后只回传结论。'
|
|
98
|
-
+ '适合可并行的调研/检索/分析类子任务,让主线程上下文保持精简。可多次调用以分解复杂任务。',
|
|
99
|
-
parameters: {
|
|
100
|
-
type: 'object',
|
|
101
|
-
properties: {
|
|
102
|
-
objective: { type: 'string', description: '要子智能体完成的单一、明确、自足的子任务' },
|
|
103
|
-
context: { type: 'string', description: '可选:完成该子任务所需的背景信息' },
|
|
104
|
-
...(agents.length ? {
|
|
105
|
-
agent: {
|
|
106
|
-
type: 'string',
|
|
107
|
-
enum: agents.map((a) => a.name),
|
|
108
|
-
description: '可选:使用某个已定义的子智能体人格 — '
|
|
109
|
-
+ agents.map((a) => a.name + (a.description ? `(${a.description.slice(0, 40)})` : '')).join(';'),
|
|
110
|
-
},
|
|
111
|
-
} : {}),
|
|
112
|
-
},
|
|
113
|
-
required: ['objective'],
|
|
114
|
-
},
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
// 子智能体只用只读外部工具(如只读 MCP);内置读工具由 runTurn 默认提供,写/执行被其非交互权限拒绝
|
|
119
|
-
const readOnlyExtras = extraTools.filter((t) => t.readOnly);
|
|
120
|
-
|
|
121
|
-
const run = async (args) => {
|
|
122
|
-
const agentDef = args?.agent ? agents.find((a) => a.name === args.agent) : null;
|
|
123
|
-
return dispatchSubtask({ config, cwd, readOnlyExtras, depth, objective: String(args?.objective || '').trim(), context: args?.context, agentDef });
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
return { readOnly: true, spec, run };
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* 构造 dispatch_agents(并行批量)工具:一次调用把多个独立子任务并行委派给多个子智能体,聚合返回。
|
|
131
|
-
* 是多智能体编排的核心——把一个大目标拆成可并行子任务同时推进,显著提速。达深度上限返回 null。
|
|
132
|
-
*/
|
|
133
|
-
export function makeDispatchAgentsTool({ config, cwd, extraTools = [], depth = 0 }) {
|
|
134
|
-
if (depth >= MAX_DEPTH) return null;
|
|
135
|
-
const agents = loadAgents(cwd);
|
|
136
|
-
const readOnlyExtras = extraTools.filter((t) => t.readOnly);
|
|
137
|
-
const taskProps = {
|
|
138
|
-
objective: { type: 'string', description: '单一、明确、自足的子任务' },
|
|
139
|
-
context: { type: 'string', description: '可选:完成该子任务所需背景' },
|
|
140
|
-
...(agents.length ? { agent: { type: 'string', enum: agents.map((a) => a.name), description: '可选:指定子智能体人格' } } : {}),
|
|
141
|
-
};
|
|
142
|
-
const spec = {
|
|
143
|
-
type: 'function',
|
|
144
|
-
function: {
|
|
145
|
-
name: 'dispatch_agents',
|
|
146
|
-
description:
|
|
147
|
-
'一次性把多个**独立、可并行**的子任务同时委派给多个子智能体(隔离上下文、只读工具),并行执行后聚合各自结论。'
|
|
148
|
-
+ '适合把一个较大目标拆成 2-6 个可并行的调研/检索/分析子任务同时推进,显著减少串行等待。每个子任务需自足。',
|
|
149
|
-
parameters: {
|
|
150
|
-
type: 'object',
|
|
151
|
-
properties: {
|
|
152
|
-
tasks: {
|
|
153
|
-
type: 'array',
|
|
154
|
-
description: '并行子任务数组(建议 2-6 个,上限 8)',
|
|
155
|
-
items: { type: 'object', properties: taskProps, required: ['objective'] },
|
|
156
|
-
},
|
|
157
|
-
},
|
|
158
|
-
required: ['tasks'],
|
|
159
|
-
},
|
|
160
|
-
},
|
|
161
|
-
};
|
|
162
|
-
const run = async (args) => {
|
|
163
|
-
const tasks = Array.isArray(args?.tasks) ? args.tasks.slice(0, 8) : [];
|
|
164
|
-
if (!tasks.length) return '未提供 tasks(需要一个子任务数组)';
|
|
165
|
-
const results = await mapLimit(tasks, 4, async (t) => {
|
|
166
|
-
const agentDef = t?.agent ? agents.find((a) => a.name === t.agent) : null;
|
|
167
|
-
const out = await dispatchSubtask({ config, cwd, readOnlyExtras, depth, objective: String(t?.objective || '').trim(), context: t?.context, agentDef });
|
|
168
|
-
return { objective: String(t?.objective || '').replace(/\s+/g, ' ').slice(0, 80), agent: t?.agent, out };
|
|
169
|
-
});
|
|
170
|
-
return results.map((r, i) =>
|
|
171
|
-
`【子任务 ${i + 1}${r.agent ? ' · ' + r.agent : ''}】${r.objective}\n${r.out}`).join('\n\n---\n\n');
|
|
172
|
-
};
|
|
173
|
-
return { readOnly: true, spec, run };
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function subagentPrompt(cwd, context) {
|
|
177
|
-
let p =
|
|
178
|
-
'你是一个专注的子智能体,只负责完成主智能体委派的单一子任务。要求:聚焦目标;'
|
|
179
|
-
+ '用可用的只读工具(读文件 / 搜索 / 列目录等)收集信息;直接输出结构清晰、可被主智能体直接复用的最终结论,'
|
|
180
|
-
+ '不要寒暄,也不要复述任务本身。\n工作目录:' + cwd + '\n';
|
|
181
|
-
if (context && String(context).trim()) p += '\n## 主智能体提供的背景\n' + context + '\n';
|
|
182
|
-
return p;
|
|
183
|
-
}
|
|
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")}}}
|