@bolloon/bolloon-agent 0.3.46 → 0.3.48
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/dist/agents/knowledge-organizer.js +470 -0
- package/dist/agents/pi-sdk-tools.js +5 -3
- package/dist/agents/skill-organizer.js +322 -0
- package/dist/bootstrap/context-os.js +19 -10
- package/dist/cli/ink-app.js +15 -1
- package/dist/cli/loading-tui.js +18 -10
- package/dist/index.js +163 -30
- package/dist/social/agent-heartbeat.js +62 -1
- package/dist/web/server.js +43 -0
- package/package.json +1 -1
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* skill-organizer.ts — 自动整理心跳 (2026-08-10)
|
|
3
|
+
*
|
|
4
|
+
* 目的: 把"自动整理"变成 Bolloon 的心跳之一 (与社交心跳并列), 周期性做两件事:
|
|
5
|
+
*
|
|
6
|
+
* A. 扫描 skills view (~/.bolloon/skills + <cwd>/.bolloon/skills), 找出"遗留下来的 skills 指导":
|
|
7
|
+
* - 外部智能体迁移残留 (openclaw/hermes 迁移的 skills 展平为 `<分类>-<技能>`, 如 apple-* / creative-* / autonomous-ai-agents-*)
|
|
8
|
+
* - 空 body / 无 description 的占位 skill
|
|
9
|
+
* - status: archived 的归档残留
|
|
10
|
+
* - 用户级与项目级同名重复
|
|
11
|
+
* B. 经验进化 (完整总结, 不再只是记录使用什么工具): 读 run-end 候选, 用 LLM 把
|
|
12
|
+
* "工具调用记录" 扩写成完整的经验文档 (背景/触发条件/流程/注意事项/验证), 转正为正式 skill.
|
|
13
|
+
*
|
|
14
|
+
* 设计原则 (compile-first / 可测):
|
|
15
|
+
* - 所有目录/LLM 可注入 (home/cwd/llm), 测试用临时 HOME + mock LLM.
|
|
16
|
+
* - runSkillOrganize 纯函数式: 输入候选 + 现有 skills, 输出进化结果 + 遗留报告, 不依赖网络.
|
|
17
|
+
* - startOrganizeHeartbeat 提供 CLI/server 统一的心跳壳: interval + 重入锁 + onStart/onEnd/onError,
|
|
18
|
+
* CLI 通过 onStart/onEnd 把显示接到颜文字行 (inkSetTransient), 结束后清空.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'fs/promises';
|
|
21
|
+
import * as os from 'os';
|
|
22
|
+
import * as path from 'path';
|
|
23
|
+
import { getUserSkillsDir, getProjectSkillsDir, sanitizeSkillName } from './skill-writer.js';
|
|
24
|
+
/** 迁移来源分类前缀 — 这些是外部智能体 (openclaw/hermes 等) 迁移进来的 skills, 属于"遗留"候选 */
|
|
25
|
+
const MIGRATED_PREFIXES = [
|
|
26
|
+
'apple-',
|
|
27
|
+
'autonomous-ai-agents-',
|
|
28
|
+
'creative-',
|
|
29
|
+
'data-science-',
|
|
30
|
+
'email-',
|
|
31
|
+
'github-',
|
|
32
|
+
'media-',
|
|
33
|
+
'mlops-',
|
|
34
|
+
'note-taking-',
|
|
35
|
+
'openclaw-imports-',
|
|
36
|
+
'productivity-',
|
|
37
|
+
'research-',
|
|
38
|
+
'smart-home-',
|
|
39
|
+
'social-media-',
|
|
40
|
+
'software-development-',
|
|
41
|
+
];
|
|
42
|
+
// ───────────────────────── A. 遗留 skills 扫描 ─────────────────────────
|
|
43
|
+
/** 判定一个 skill 是否"遗留": 返回原因列表 (空数组 = 正常) */
|
|
44
|
+
export function leftoverReasons(meta) {
|
|
45
|
+
const reasons = [];
|
|
46
|
+
const name = meta.name || '';
|
|
47
|
+
// 1. 迁移残留: 带外部智能体分类前缀
|
|
48
|
+
if (MIGRATED_PREFIXES.some((p) => name.startsWith(p))) {
|
|
49
|
+
reasons.push('迁移遗留 (外部智能体分类前缀)');
|
|
50
|
+
}
|
|
51
|
+
// 2. 占位: 无描述或正文过短
|
|
52
|
+
const desc = (meta.description || '').trim();
|
|
53
|
+
const body = (meta.body || '').trim();
|
|
54
|
+
if (!desc)
|
|
55
|
+
reasons.push('无 description');
|
|
56
|
+
if (body.length < 50)
|
|
57
|
+
reasons.push('正文过短 (疑似占位)');
|
|
58
|
+
// 3. 归档残留
|
|
59
|
+
if (meta.status === 'archived')
|
|
60
|
+
reasons.push('status=archived (归档残留)');
|
|
61
|
+
return reasons;
|
|
62
|
+
}
|
|
63
|
+
/** 扫描一个 skill 目录, 返回遗留报告 */
|
|
64
|
+
export async function scanSkillDir(dir) {
|
|
65
|
+
const out = [];
|
|
66
|
+
let entries;
|
|
67
|
+
try {
|
|
68
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (!entry.isDirectory() || entry.name.startsWith('.'))
|
|
75
|
+
continue;
|
|
76
|
+
const skillFile = path.join(dir, entry.name, 'SKILL.md');
|
|
77
|
+
let raw = '';
|
|
78
|
+
try {
|
|
79
|
+
raw = await fs.readFile(skillFile, 'utf-8');
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
continue; // 没有 SKILL.md 不算 skill
|
|
83
|
+
}
|
|
84
|
+
const meta = parseSkillRaw(raw, entry.name);
|
|
85
|
+
const reasons = leftoverReasons(meta);
|
|
86
|
+
if (reasons.length > 0) {
|
|
87
|
+
out.push({ dir, name: meta.name, description: meta.description || '', reasons });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
/** 轻量 frontmatter/正文解析 (不依赖 skill-loader, 避免循环依赖) */
|
|
93
|
+
function parseSkillRaw(raw, fallbackName) {
|
|
94
|
+
let name = fallbackName;
|
|
95
|
+
let description = '';
|
|
96
|
+
let status = '';
|
|
97
|
+
let body = raw;
|
|
98
|
+
const fmMatch = raw.match(/^---\n([\s\S]*?)\n---\n?/);
|
|
99
|
+
if (fmMatch) {
|
|
100
|
+
body = raw.slice(fmMatch[0].length);
|
|
101
|
+
const fm = fmMatch[1];
|
|
102
|
+
const nameM = fm.match(/^name:\s*(.+)$/m);
|
|
103
|
+
const descM = fm.match(/^description:\s*(.+)$/m);
|
|
104
|
+
const statusM = fm.match(/^status:\s*(.+)$/m);
|
|
105
|
+
if (nameM)
|
|
106
|
+
name = nameM[1].trim();
|
|
107
|
+
if (descM)
|
|
108
|
+
description = descM[1].trim();
|
|
109
|
+
if (statusM)
|
|
110
|
+
status = statusM[1].trim();
|
|
111
|
+
}
|
|
112
|
+
return { name, description, status, body: body.trim() };
|
|
113
|
+
}
|
|
114
|
+
/** 扫描全部 skills view (用户级 + 项目级), 找同名重复 */
|
|
115
|
+
export async function scanLeftoverSkills(opts = {}) {
|
|
116
|
+
const home = opts.home || os.homedir();
|
|
117
|
+
const cwd = opts.cwd || process.cwd();
|
|
118
|
+
const dirs = [getUserSkillsDir(home), getProjectSkillsDir(cwd)];
|
|
119
|
+
const found = new Map();
|
|
120
|
+
const seenNames = new Set();
|
|
121
|
+
for (const dir of dirs) {
|
|
122
|
+
const items = await scanSkillDir(dir);
|
|
123
|
+
for (const it of items) {
|
|
124
|
+
found.set(`${dir}::${it.name}`, it);
|
|
125
|
+
}
|
|
126
|
+
// 同名重复检测 (跨目录)
|
|
127
|
+
const names = (await fs.readdir(dir).catch(() => [])).filter((n) => !n.startsWith('.'));
|
|
128
|
+
for (const n of names) {
|
|
129
|
+
if (seenNames.has(n)) {
|
|
130
|
+
const dup = {
|
|
131
|
+
dir,
|
|
132
|
+
name: n,
|
|
133
|
+
description: '同名 skill 已在其他目录存在',
|
|
134
|
+
reasons: ['跨目录同名重复'],
|
|
135
|
+
};
|
|
136
|
+
found.set(`${dir}::dup::${n}`, dup);
|
|
137
|
+
}
|
|
138
|
+
seenNames.add(n);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return Array.from(found.values());
|
|
142
|
+
}
|
|
143
|
+
// ───────────────────────── B. 经验进化 (LLM 完整总结) ─────────────────────────
|
|
144
|
+
/** 把候选的工具调用记录扩写成完整经验 — LLM prompt (结构化 JSON 输出) */
|
|
145
|
+
export function buildEvolvePrompt(c) {
|
|
146
|
+
return `你是经验总结专家。下面是 Bolloon 智能体在一轮运行中连续成功调用的工具记录 (已运行 ${c.runs || 1} 次)。
|
|
147
|
+
|
|
148
|
+
工具记录:
|
|
149
|
+
名称: ${c.name}
|
|
150
|
+
描述: ${c.description}
|
|
151
|
+
原始记录:
|
|
152
|
+
${String(c.body || '').slice(0, 4000)}
|
|
153
|
+
|
|
154
|
+
请把这份"工具调用记录"扩写成一份完整的、可复用的经验文档, 要求:
|
|
155
|
+
1. name: 一个简短的小写英文名 (字母数字连字符, 概括这套操作)
|
|
156
|
+
2. description: 一句话描述 (什么场景用)
|
|
157
|
+
3. body: 完整经验正文, 按以下结构:
|
|
158
|
+
## 背景 — 这套操作在什么场景下用, 解决什么问题
|
|
159
|
+
## 触发条件 — 什么信号/需求出现时应该使用
|
|
160
|
+
## 流程 — 具体步骤 (保留原始工具名), 每步说明目的
|
|
161
|
+
## 注意事项 — 易错点 / 边界情况 / 已知坑
|
|
162
|
+
## 验证 — 怎么确认结果正确
|
|
163
|
+
|
|
164
|
+
只输出一个 JSON 对象, 不要任何其他文字:
|
|
165
|
+
{"name": "...", "description": "...", "body": "..."}`;
|
|
166
|
+
}
|
|
167
|
+
/** 解析 LLM 返回的 JSON (容错: 剥 markdown 代码块, 取第一个 {...}) */
|
|
168
|
+
export function parseEvolveJson(raw) {
|
|
169
|
+
if (!raw)
|
|
170
|
+
return null;
|
|
171
|
+
const cleaned = raw.replace(/```(?:json)?/g, '').trim();
|
|
172
|
+
const m = cleaned.match(/\{[\s\S]*\}/);
|
|
173
|
+
if (!m)
|
|
174
|
+
return null;
|
|
175
|
+
try {
|
|
176
|
+
const obj = JSON.parse(m[0]);
|
|
177
|
+
return {
|
|
178
|
+
name: typeof obj.name === 'string' ? obj.name : undefined,
|
|
179
|
+
description: typeof obj.description === 'string' ? obj.description : undefined,
|
|
180
|
+
body: typeof obj.body === 'string' ? obj.body : undefined,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** 完整经验进化: 候选 → LLM 总结 → 正式 skill (转正 + 清理候选). 返回转正 skill 名列表 */
|
|
188
|
+
export async function evolveCandidates(opts) {
|
|
189
|
+
const { listSkillCandidates, writeSkillCandidate } = await import('./skill-writer.js');
|
|
190
|
+
const home = opts.home || os.homedir();
|
|
191
|
+
const cands = await listSkillCandidates(home);
|
|
192
|
+
const evolved = [];
|
|
193
|
+
const max = opts.maxEvolve ?? 3;
|
|
194
|
+
// 优先进化 runs 多的 (更可能可复用)
|
|
195
|
+
const sorted = [...cands].sort((a, b) => (b.runs ?? 1) - (a.runs ?? 1)).slice(0, max);
|
|
196
|
+
for (const c of sorted) {
|
|
197
|
+
try {
|
|
198
|
+
const raw = await opts.llm(buildEvolvePrompt(c));
|
|
199
|
+
const parsed = parseEvolveJson(raw);
|
|
200
|
+
if (!parsed?.body || parsed.body.trim().length < 50) {
|
|
201
|
+
// LLM 输出不可用 → 保留候选, 记录一次失败 (不转正)
|
|
202
|
+
await writeSkillCandidate({
|
|
203
|
+
...c,
|
|
204
|
+
body: `${c.body}\n- ${new Date().toISOString().slice(0, 16)} ${opts.source || 'organize'}: LLM 总结失败, 保留待人工`,
|
|
205
|
+
}).catch(() => { });
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
const { createSkill } = await import('./skill-writer.js');
|
|
209
|
+
const safeName = sanitizeSkillName(parsed.name || c.name) || sanitizeSkillName(c.name);
|
|
210
|
+
const r = await createSkill(safeName, parsed.description || c.description, parsed.body, {
|
|
211
|
+
status: 'active',
|
|
212
|
+
triggers: c.description ? [c.description.slice(0, 60)] : undefined,
|
|
213
|
+
});
|
|
214
|
+
if (!r.ok)
|
|
215
|
+
continue;
|
|
216
|
+
evolved.push(safeName);
|
|
217
|
+
// 转正成功 → 清理候选文件
|
|
218
|
+
const { listSkillCandidates: listAgain } = await import('./skill-writer.js');
|
|
219
|
+
const after = await listAgain(home);
|
|
220
|
+
for (const cc of after) {
|
|
221
|
+
if (cc.file && (cc.name === c.name || sanitizeSkillName(cc.name) === sanitizeSkillName(c.name))) {
|
|
222
|
+
await fs.rm(cc.file, { force: true }).catch(() => { });
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
/* 单个候选失败不阻塞整轮 */
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return { evolved, scanned: cands.length };
|
|
231
|
+
}
|
|
232
|
+
/** 自动整理主流程: 遗留扫描 (必做) + 经验进化 (有 LLM 时) */
|
|
233
|
+
export async function runSkillOrganize(opts = {}) {
|
|
234
|
+
const evolve = opts.evolve ?? true;
|
|
235
|
+
const result = { scannedCandidates: 0, evolved: [], leftovers: [] };
|
|
236
|
+
try {
|
|
237
|
+
result.leftovers = await scanLeftoverSkills({ home: opts.home, cwd: opts.cwd });
|
|
238
|
+
}
|
|
239
|
+
catch (e) {
|
|
240
|
+
result.error = `遗留扫描失败: ${e?.message || String(e)}`;
|
|
241
|
+
}
|
|
242
|
+
if (evolve && opts.llm) {
|
|
243
|
+
try {
|
|
244
|
+
const { evolved, scanned } = await evolveCandidates({
|
|
245
|
+
llm: opts.llm,
|
|
246
|
+
source: opts.source,
|
|
247
|
+
home: opts.home,
|
|
248
|
+
maxEvolve: opts.maxEvolve,
|
|
249
|
+
});
|
|
250
|
+
result.evolved = evolved;
|
|
251
|
+
result.scannedCandidates = scanned;
|
|
252
|
+
}
|
|
253
|
+
catch (e) {
|
|
254
|
+
result.error = `${result.error ? result.error + '; ' : ''}经验进化失败: ${e?.message || String(e)}`;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
try {
|
|
259
|
+
const { listSkillCandidates } = await import('./skill-writer.js');
|
|
260
|
+
result.scannedCandidates = (await listSkillCandidates(opts.home || os.homedir())).length;
|
|
261
|
+
}
|
|
262
|
+
catch {
|
|
263
|
+
result.scannedCandidates = 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
return result;
|
|
267
|
+
}
|
|
268
|
+
/** 自动整理总入口 (2026-08-10): skills 整理 (遗留扫描 + 经验进化) + 知识层整理 (9 类) */
|
|
269
|
+
export async function runAutoOrganize(opts = {}) {
|
|
270
|
+
const result = await runSkillOrganize(opts);
|
|
271
|
+
try {
|
|
272
|
+
const { runKnowledgeOrganize } = await import('./knowledge-organizer.js');
|
|
273
|
+
result.knowledge = await runKnowledgeOrganize({ home: opts.home, cwd: opts.cwd, llm: opts.llm });
|
|
274
|
+
}
|
|
275
|
+
catch (e) {
|
|
276
|
+
result.error = `${result.error ? result.error + '; ' : ''}知识层整理失败: ${e?.message || String(e)}`;
|
|
277
|
+
}
|
|
278
|
+
return result;
|
|
279
|
+
}
|
|
280
|
+
/** 自动整理心跳: 定时触发整理, 带重入锁 (上一轮没跑完不重复触发) */
|
|
281
|
+
export function startOrganizeHeartbeat(opts) {
|
|
282
|
+
const intervalMs = opts.intervalMs ?? 30 * 60_000;
|
|
283
|
+
let timer = null;
|
|
284
|
+
let stopped = false;
|
|
285
|
+
let running = false;
|
|
286
|
+
const runOnce = async () => {
|
|
287
|
+
if (running || stopped)
|
|
288
|
+
return null;
|
|
289
|
+
running = true;
|
|
290
|
+
try {
|
|
291
|
+
opts.onStart?.();
|
|
292
|
+
const r = await opts.run();
|
|
293
|
+
opts.onEnd?.(r);
|
|
294
|
+
return r;
|
|
295
|
+
}
|
|
296
|
+
catch (e) {
|
|
297
|
+
opts.onError?.(e instanceof Error ? e : new Error(String(e)));
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
finally {
|
|
301
|
+
running = false;
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const schedule = () => {
|
|
305
|
+
if (stopped)
|
|
306
|
+
return;
|
|
307
|
+
timer = setTimeout(async () => {
|
|
308
|
+
await runOnce();
|
|
309
|
+
schedule();
|
|
310
|
+
}, intervalMs);
|
|
311
|
+
};
|
|
312
|
+
schedule();
|
|
313
|
+
return {
|
|
314
|
+
stop() {
|
|
315
|
+
stopped = true;
|
|
316
|
+
if (timer)
|
|
317
|
+
clearTimeout(timer);
|
|
318
|
+
timer = null;
|
|
319
|
+
},
|
|
320
|
+
runOnce,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
@@ -48,7 +48,16 @@ const LAYER_KEYS = new Set(CONTEXT_OS_LAYERS.map((l) => l.key));
|
|
|
48
48
|
export function getContextOsRoot(home = os.homedir()) {
|
|
49
49
|
return path.join(home, '.bolloon', 'context-os');
|
|
50
50
|
}
|
|
51
|
-
|
|
51
|
+
/**
|
|
52
|
+
* 2026-08-09: 层目录 — 支持按 agentId 分区 (每个智能体独立 Context OS).
|
|
53
|
+
* agentId 有值 → ~/.bolloon/context-os/<sanitizeAgentId>/<layer>
|
|
54
|
+
* agentId 空 → ~/.bolloon/context-os/<layer> (旧全局路径, 兼容)
|
|
55
|
+
*/
|
|
56
|
+
export function getLayerDir(layer, home = os.homedir(), agentId) {
|
|
57
|
+
const safeId = String(agentId || '').replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
|
|
58
|
+
if (safeId) {
|
|
59
|
+
return path.join(getContextOsRoot(home), safeId, layer);
|
|
60
|
+
}
|
|
52
61
|
return path.join(getContextOsRoot(home), layer);
|
|
53
62
|
}
|
|
54
63
|
/** 校验 layer 合法; 非法返回 null */
|
|
@@ -86,11 +95,11 @@ ${l.usage}
|
|
|
86
95
|
→ 阶段2 固化 (本层唯一位置) → 阶段3 索引化 (高频引用) → 阶段4 归档/删除.
|
|
87
96
|
`;
|
|
88
97
|
}
|
|
89
|
-
export async function ensureContextOsDirs(home) {
|
|
98
|
+
export async function ensureContextOsDirs(home, agentId) {
|
|
90
99
|
const root = getContextOsRoot(home);
|
|
91
100
|
await fs.mkdir(root, { recursive: true });
|
|
92
101
|
for (const l of CONTEXT_OS_LAYERS) {
|
|
93
|
-
const dir = getLayerDir(l.key, home);
|
|
102
|
+
const dir = getLayerDir(l.key, home, agentId);
|
|
94
103
|
await fs.mkdir(dir, { recursive: true });
|
|
95
104
|
const readmePath = path.join(dir, 'README.md');
|
|
96
105
|
try {
|
|
@@ -106,7 +115,7 @@ export async function ensureContextOsDirs(home) {
|
|
|
106
115
|
* 文件名: <ts>-<slug>.md; frontmatter v2 (stage0 = 临时价值点, 待验证).
|
|
107
116
|
* 幂等: 同层同 slug 已存在 → 跳过 (不重复造文件, Context OS §6 Step3).
|
|
108
117
|
*/
|
|
109
|
-
export async function writeContextAsset(input, home) {
|
|
118
|
+
export async function writeContextAsset(input, home, agentId) {
|
|
110
119
|
const layer = resolveLayer(input.layer);
|
|
111
120
|
if (!layer) {
|
|
112
121
|
return { ok: false, error: `layer 非法: '${input.layer}'. 合法: ${CONTEXT_OS_LAYERS.map((l) => l.key).join(' / ')}` };
|
|
@@ -117,15 +126,15 @@ export async function writeContextAsset(input, home) {
|
|
|
117
126
|
const content = String(input.content || '').trim();
|
|
118
127
|
if (!content)
|
|
119
128
|
return { ok: false, error: 'content 必填' };
|
|
120
|
-
await ensureContextOsDirs(home);
|
|
129
|
+
await ensureContextOsDirs(home, agentId);
|
|
121
130
|
const now = new Date().toISOString();
|
|
122
131
|
const ts = Date.now();
|
|
123
132
|
const slug = slugify(title);
|
|
124
133
|
const fileName = `${ts}-${slug}.md`;
|
|
125
|
-
const filePath = path.join(getLayerDir(layer.key, home), fileName);
|
|
134
|
+
const filePath = path.join(getLayerDir(layer.key, home, agentId), fileName);
|
|
126
135
|
// 幂等: 同 slug 已存在 → 跳过
|
|
127
136
|
try {
|
|
128
|
-
const files = await fs.readdir(getLayerDir(layer.key, home));
|
|
137
|
+
const files = await fs.readdir(getLayerDir(layer.key, home, agentId));
|
|
129
138
|
if (files.some((f) => f.endsWith(`-${slug}.md`))) {
|
|
130
139
|
return { ok: true, skipped: true, error: `同标题资产已存在 (${slug}.md), 未重复写入` };
|
|
131
140
|
}
|
|
@@ -157,7 +166,7 @@ export async function writeContextAsset(input, home) {
|
|
|
157
166
|
}
|
|
158
167
|
}
|
|
159
168
|
/** 列出层资产; layer 为空 → 全层汇总 */
|
|
160
|
-
export async function readContextAssets(layer, keyword, home) {
|
|
169
|
+
export async function readContextAssets(layer, keyword, home, agentId) {
|
|
161
170
|
const root = getContextOsRoot(home);
|
|
162
171
|
const kw = String(keyword || '').trim().toLowerCase();
|
|
163
172
|
const wanted = layer ? [resolveLayer(layer)].filter(Boolean).map((l) => l.key) : CONTEXT_OS_LAYERS.map((l) => l.key);
|
|
@@ -165,11 +174,11 @@ export async function readContextAssets(layer, keyword, home) {
|
|
|
165
174
|
for (const key of wanted) {
|
|
166
175
|
const l = resolveLayer(key);
|
|
167
176
|
try {
|
|
168
|
-
const files = (await fs.readdir(getLayerDir(key, home))).filter((f) => f.endsWith('.md') && f !== 'README.md');
|
|
177
|
+
const files = (await fs.readdir(getLayerDir(key, home, agentId))).filter((f) => f.endsWith('.md') && f !== 'README.md');
|
|
169
178
|
const entries = [];
|
|
170
179
|
for (const f of files) {
|
|
171
180
|
try {
|
|
172
|
-
const raw = await fs.readFile(path.join(getLayerDir(key, home), f), 'utf-8');
|
|
181
|
+
const raw = await fs.readFile(path.join(getLayerDir(key, home, agentId), f), 'utf-8');
|
|
173
182
|
const titleM = raw.match(/^title:\s*(.+)$/m);
|
|
174
183
|
const createdM = raw.match(/^created:\s*(.+)$/m);
|
|
175
184
|
const title = titleM ? titleM[1].trim() : f.replace(/\.md$/, '');
|
package/dist/cli/ink-app.js
CHANGED
|
@@ -54,6 +54,9 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
54
54
|
const [status, setStatus] = useState(initialStatus);
|
|
55
55
|
const { exit } = useApp();
|
|
56
56
|
const [thinking, setThinking] = useState(false);
|
|
57
|
+
// 2026-08-10: 临时状态行 (自动整理心跳/run-end 经验整理用) — 显示在颜文字行位置,
|
|
58
|
+
// 结束后设 null 即清空 (显示为空). 不进入消息历史, 不会残留显示效果.
|
|
59
|
+
const [transient, setTransient] = useState(null);
|
|
57
60
|
const thinkingIdx = useRef(0);
|
|
58
61
|
// 双击 Esc 退出当前进程 (500ms 窗口内第二次按下)
|
|
59
62
|
const lastEscRef = useRef(0);
|
|
@@ -286,10 +289,15 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
286
289
|
globalThis.__inkSetStatus = (s) => {
|
|
287
290
|
setStatus(s);
|
|
288
291
|
};
|
|
292
|
+
// 2026-08-10: 临时状态行 (自动整理/经验整理): 传字符串显示, 传 null 清空 (显示为空)
|
|
293
|
+
globalThis.__inkSetTransient = (v) => {
|
|
294
|
+
setTransient(v === undefined ? null : v);
|
|
295
|
+
};
|
|
289
296
|
return () => {
|
|
290
297
|
delete globalThis.__inkAppend;
|
|
291
298
|
delete globalThis.__inkSetStatus;
|
|
292
299
|
delete globalThis.__inkSetThinking;
|
|
300
|
+
delete globalThis.__inkSetTransient;
|
|
293
301
|
};
|
|
294
302
|
}, []);
|
|
295
303
|
const onSubmit = useCallback((value) => {
|
|
@@ -539,7 +547,7 @@ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH
|
|
|
539
547
|
}, 600);
|
|
540
548
|
return () => clearInterval(timer);
|
|
541
549
|
}, [thinking]);
|
|
542
|
-
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsxs(Box, { flexGrow: 1, flexDirection: "column", justifyContent: "flex-start", children: [_jsx(LogoBox, { width: terminalW }), _jsx(Messages, { msgs: msgs }), thinking && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [KAOMOJI[thinkingIdx.current], " \u601D\u8003\u4E2D..."] }) }))] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "#c4d640", children: '─'.repeat(terminalW) }) }), _jsx(Box, { children: _jsx(Text, { children: status }) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "#c4d640", children: '─'.repeat(terminalW) }) }), popupOpen && (tabState || mention) && (_jsx(MentionPopup, { title: popupTitle, items: filtered, sel: safeSel, width: terminalW, loading: loadingFiles })), picker && (_jsx(MentionPopup, { title: picker.title, items: picker.items, sel: Math.min(picker.sel, picker.items.length - 1), width: terminalW })), _jsxs(Box, { children: [_jsx(Text, { bold: true, color: "#c4d640", children: "\u276F " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, focus: !popupOpen && !picker, placeholder: "\u8F93\u5165\u6D88\u606F... @\u667A\u80FD\u4F53 /\u547D\u4EE4 #\u6587\u4EF6 \u00B7 Esc \u53CC\u51FB\u9000\u51FA \u00B7 /queue \u6392\u961F \u00B7 !\u7EC8\u7AEF\u547D\u4EE4" }, tiKey)] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "#c4d640", children: '─'.repeat(terminalW) }) })] }));
|
|
550
|
+
return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsxs(Box, { flexGrow: 1, flexDirection: "column", justifyContent: "flex-start", children: [_jsx(LogoBox, { width: terminalW }), _jsx(Messages, { msgs: msgs }), thinking && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [KAOMOJI[thinkingIdx.current], " \u601D\u8003\u4E2D..."] }) })), transient && (_jsx(Box, { children: _jsx(Text, { children: transient }) }))] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "#c4d640", children: '─'.repeat(terminalW) }) }), _jsx(Box, { children: _jsx(Text, { children: status }) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "#c4d640", children: '─'.repeat(terminalW) }) }), popupOpen && (tabState || mention) && (_jsx(MentionPopup, { title: popupTitle, items: filtered, sel: safeSel, width: terminalW, loading: loadingFiles })), picker && (_jsx(MentionPopup, { title: picker.title, items: picker.items, sel: Math.min(picker.sel, picker.items.length - 1), width: terminalW })), _jsxs(Box, { children: [_jsx(Text, { bold: true, color: "#c4d640", children: "\u276F " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, focus: !popupOpen && !picker, placeholder: "\u8F93\u5165\u6D88\u606F... @\u667A\u80FD\u4F53 /\u547D\u4EE4 #\u6587\u4EF6 \u00B7 Esc \u53CC\u51FB\u9000\u51FA \u00B7 /queue \u6392\u961F \u00B7 !\u7EC8\u7AEF\u547D\u4EE4" }, tiKey)] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "#c4d640", children: '─'.repeat(terminalW) }) })] }));
|
|
543
551
|
};
|
|
544
552
|
export { InkApp };
|
|
545
553
|
// ─── 启动 ────────────────────────────────────────────────────────────────────
|
|
@@ -576,3 +584,9 @@ export function inkSetThinking(v) {
|
|
|
576
584
|
if (fn)
|
|
577
585
|
fn(v);
|
|
578
586
|
}
|
|
587
|
+
/** 2026-08-10: 设置/清除临时状态行 (自动整理/经验整理). 传 null 清空 → 显示为空 */
|
|
588
|
+
export function inkSetTransient(v) {
|
|
589
|
+
const fn = globalThis.__inkSetTransient;
|
|
590
|
+
if (fn)
|
|
591
|
+
fn(v);
|
|
592
|
+
}
|
package/dist/cli/loading-tui.js
CHANGED
|
@@ -45,14 +45,20 @@ function getPackageVersion() {
|
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
const BOLLOON_VERSION = getPackageVersion();
|
|
48
|
-
// ── 品牌图标:
|
|
48
|
+
// ── 品牌图标: 笑脸机器人 (2026-08-09, bolloon 色系填充) ──────
|
|
49
|
+
// 头: 主色边框 + 亮绿填充 (C_ACCENT_BG); 眼睛 ◉ / 嘴 ◡ 用亮色填充;
|
|
50
|
+
// 末行 BOLLOON 主色艺术字 (仅 printBanner 用, brandArtLines 会裁掉避免双 logo).
|
|
51
|
+
const ROBOT_HEAD = [
|
|
52
|
+
`${C_ACCENT} ╭───────╮${RESET}`,
|
|
53
|
+
`${C_ACCENT} ╭─╯${C_ACCENT_BG} ${C_WHITE}◉${C_ACCENT_BG} ${C_WHITE}◉${C_ACCENT_BG} ${RESET}${C_ACCENT}╰─╮${RESET}`,
|
|
54
|
+
`${C_ACCENT} │${C_ACCENT_BG} ${C_WHITE}◡${C_ACCENT_BG} ${RESET}${C_ACCENT}│${RESET}`,
|
|
55
|
+
`${C_ACCENT} ╰─╮${C_ACCENT_BG} ${RESET}${C_ACCENT}╭─╯${RESET}`,
|
|
56
|
+
`${C_ACCENT} ╰───┬───╯${RESET}`,
|
|
57
|
+
`${C_ACCENT} │${RESET}`,
|
|
58
|
+
];
|
|
49
59
|
export const BOLLOON_ICON = [
|
|
50
|
-
|
|
51
|
-
`${C_ACCENT}
|
|
52
|
-
`${C_ACCENT} ════◆════${RESET}`,
|
|
53
|
-
`${C_ACCENT} ╲ ╱${RESET}`,
|
|
54
|
-
`${C_ACCENT} ╲${RESET}`,
|
|
55
|
-
`${C_ACCENT} ✦ ╲${RESET}`,
|
|
60
|
+
...ROBOT_HEAD,
|
|
61
|
+
`${C_ACCENT}${BOLD} BOLLOON${RESET}`,
|
|
56
62
|
].join('\n');
|
|
57
63
|
// ── 艺术字: BOLLOON (box 字体) + Bolloon Agent 副标题 ──
|
|
58
64
|
export const BOLLOON_BANNER = [
|
|
@@ -64,9 +70,10 @@ export const BOLLOON_BANNER = [
|
|
|
64
70
|
`${C_TEXT}${BOLD}╚═════╝ ╚═════╝ ╚══════╝╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═══╝${RESET}`,
|
|
65
71
|
`${C_DIM}Bolloon Agent v${BOLLOON_VERSION}${RESET}`,
|
|
66
72
|
].join('\n');
|
|
67
|
-
/** 艺术字全部行 (
|
|
73
|
+
/** 艺术字全部行 (机器人头在左, BOLLOON 艺术字在右), 供框内渲染 */
|
|
68
74
|
export function brandArtLines() {
|
|
69
|
-
|
|
75
|
+
// 2026-08-09: icon 只取机器人头 (裁掉末行 BOLLOON 文字), 避免和右侧 banner 双 logo
|
|
76
|
+
const icon = ROBOT_HEAD;
|
|
70
77
|
const banner = BOLLOON_BANNER.split('\n');
|
|
71
78
|
const gap = 2;
|
|
72
79
|
const iconW = Math.max(1, ...icon.map(l => dispWidth(l)));
|
|
@@ -81,8 +88,9 @@ export function brandArtLines() {
|
|
|
81
88
|
return rows;
|
|
82
89
|
}
|
|
83
90
|
export function printBanner(version) {
|
|
91
|
+
// 2026-08-09: 新品牌 logo = 笑脸机器人 (BOLLOON_ICON 自带 BOLLOON 文字),
|
|
92
|
+
// 不再叠加旧 box 字体 banner (避免双 logo)
|
|
84
93
|
console.log(BOLLOON_ICON);
|
|
85
|
-
console.log(BOLLOON_BANNER);
|
|
86
94
|
if (version)
|
|
87
95
|
console.log(`${C_DIM} Bolloon Agent v${version}${RESET}`);
|
|
88
96
|
console.log(`${C_DIM} P2P AI Agent · 文档智能体${RESET}`);
|