@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.
@@ -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
- export function getLayerDir(layer, home = os.homedir()) {
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$/, '');
@@ -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
+ }
@@ -45,14 +45,20 @@ function getPackageVersion() {
45
45
  }
46
46
  }
47
47
  const BOLLOON_VERSION = getPackageVersion();
48
- // ── 品牌图标: 顶部带圆标注的 0 (气球) ──────────────
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
- `${C_ACCENT} ✦${RESET}`,
51
- `${C_ACCENT} ╱ ╲${RESET}`,
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
- /** 艺术字全部行 (图标在左, BOLLOON 艺术字在右), 供框内渲染 */
73
+ /** 艺术字全部行 (机器人头在左, BOLLOON 艺术字在右), 供框内渲染 */
68
74
  export function brandArtLines() {
69
- const icon = BOLLOON_ICON.split('\n');
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}`);