@bolloon/bolloon-agent 0.1.13 → 0.1.14

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.
Files changed (52) hide show
  1. package/dist/agents/pi-sdk.js +185 -0
  2. package/dist/agents/shell-guard.js +354 -0
  3. package/dist/agents/shell-tool.js +83 -0
  4. package/dist/agents/skill-loader.js +174 -0
  5. package/dist/bollharness-integration/context-chain-router.js +3 -3
  6. package/dist/bollharness-integration/context-router.js +1 -1
  7. package/dist/heartbeat/Watchdog.js +7 -5
  8. package/dist/heartbeat/index.js +1 -0
  9. package/dist/heartbeat/self-improve-bus.js +85 -0
  10. package/dist/pi-ecosystem-judgment/index.js +1 -2
  11. package/dist/utils/auto-update.js +44 -12
  12. package/dist/web/client.js +839 -103
  13. package/dist/web/components/p2p/P2PModal.js +188 -0
  14. package/dist/web/components/p2p/index.js +264 -226
  15. package/dist/web/components/p2p/p2p-modal.js +657 -0
  16. package/dist/web/components/p2p/p2p-tools.js +248 -0
  17. package/dist/web/index.html +88 -8
  18. package/dist/web/server.js +2360 -0
  19. package/dist/web/style.css +506 -9
  20. package/package.json +2 -2
  21. package/scripts/build-cli.js +11 -1
  22. package/src/agents/pi-sdk.ts +196 -0
  23. package/src/agents/shell-guard.ts +417 -0
  24. package/src/agents/shell-tool.ts +103 -0
  25. package/src/agents/skill-loader.ts +202 -0
  26. package/src/bollharness-integration/context-chain-router.ts +3 -3
  27. package/src/bollharness-integration/context-router.ts +1 -1
  28. package/src/heartbeat/Watchdog.ts +7 -5
  29. package/src/heartbeat/index.ts +1 -0
  30. package/src/heartbeat/self-improve-bus.ts +110 -0
  31. package/src/types.d.ts +12 -0
  32. package/src/utils/auto-update.ts +45 -14
  33. package/src/web/client.js +839 -103
  34. package/src/web/index.html +88 -8
  35. package/src/web/server.ts +427 -101
  36. package/src/web/style.css +506 -9
  37. package/dist/bollharness-integration/bollharness-integration/context-router-judgment.d.ts +0 -48
  38. package/dist/bollharness-integration/bollharness-integration/context-router-judgment.js +0 -261
  39. package/dist/bollharness-integration/bollharness-integration/context-router.d.ts +0 -110
  40. package/dist/bollharness-integration/bollharness-integration/context-router.js +0 -542
  41. package/dist/bollharness-integration/bollharness-integration/gate-state-machine.d.ts +0 -87
  42. package/dist/bollharness-integration/bollharness-integration/gate-state-machine.js +0 -231
  43. package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.d.ts +0 -30
  44. package/dist/bollharness-integration/bollharness-integration/gate-transition-hooks.js +0 -91
  45. package/dist/bollharness-integration/bollharness-integration/guard-checker.d.ts +0 -105
  46. package/dist/bollharness-integration/bollharness-integration/guard-checker.js +0 -353
  47. package/dist/bollharness-integration/bollharness-integration/index.d.ts +0 -66
  48. package/dist/bollharness-integration/bollharness-integration/index.js +0 -32
  49. package/dist/bollharness-integration/bollharness-integration/integration.d.ts +0 -219
  50. package/dist/bollharness-integration/bollharness-integration/integration.js +0 -420
  51. package/dist/bollharness-integration/bollharness-integration/skill-adapter.d.ts +0 -151
  52. package/dist/bollharness-integration/bollharness-integration/skill-adapter.js +0 -518
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Shell 工具: 给 Bolloon agent 跑受限的 shell 命令
3
+ *
4
+ * 这个工具**只做两件事**:
5
+ * 1. 把命令交给硬护栏检查
6
+ * 2. 在沙箱 cwd 下用 child_process 执行
7
+ *
8
+ * AI 完全自主触发自改, 但 shell 工具本身**只接受白名单内命令**.
9
+ * 禁区列表在 shell-guard.ts, AI 改不了那个文件.
10
+ */
11
+
12
+ import { spawn } from 'child_process';
13
+ import * as fs from 'fs';
14
+ import { checkCommand, checkWritePath, getSandboxCwd } from './shell-guard.js';
15
+
16
+ export interface ShellExecResult {
17
+ success: boolean;
18
+ output?: string;
19
+ error?: string;
20
+ exitCode?: number;
21
+ /** true 表示被护栏拒绝, AI 不应该重试 */
22
+ deniedByGuard?: boolean;
23
+ }
24
+
25
+ /**
26
+ * 在沙箱里跑一条命令
27
+ * @param cmd 可执行文件名, 必须命中白名单
28
+ * @param args 参数列表
29
+ * @param opts.timeoutMs 超时毫秒, 默认 30s
30
+ * @param opts.allowedWriteTargets 允许的写入路径, 命中禁区列表的路径会拒
31
+ */
32
+ export async function shellExec(
33
+ cmd: string,
34
+ args: string[] = [],
35
+ opts: { timeoutMs?: number; allowedWriteTargets?: string[] } = {}
36
+ ): Promise<ShellExecResult> {
37
+ // 1. 护栏检查
38
+ const cmdCheck = checkCommand(cmd, args);
39
+ if (!cmdCheck.allowed) {
40
+ return {
41
+ success: false,
42
+ error: `[shell-guard] ${cmdCheck.reason}`,
43
+ deniedByGuard: true
44
+ };
45
+ }
46
+
47
+ // 2. 写入目标检查
48
+ if (opts.allowedWriteTargets) {
49
+ for (const target of opts.allowedWriteTargets) {
50
+ const pathCheck = checkWritePath(target);
51
+ if (!pathCheck.allowed) {
52
+ return {
53
+ success: false,
54
+ error: `[shell-guard] ${pathCheck.reason}`,
55
+ deniedByGuard: true
56
+ };
57
+ }
58
+ }
59
+ }
60
+
61
+ // 3. 确保沙箱存在
62
+ const sandboxCwd = getSandboxCwd();
63
+ try {
64
+ fs.mkdirSync(sandboxCwd, { recursive: true });
65
+ } catch {
66
+ // 已经存在则忽略
67
+ }
68
+
69
+ // 4. 跑命令
70
+ return new Promise((resolve) => {
71
+ const proc = spawn(cmd, args, {
72
+ cwd: sandboxCwd,
73
+ env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, // 禁止 git 弹交互
74
+ shell: false, // **关键**: 禁用 shell, 防止元字符注入
75
+ windowsHide: true
76
+ });
77
+
78
+ let stdout = '';
79
+ let stderr = '';
80
+ const timeout = setTimeout(() => {
81
+ proc.kill('SIGKILL');
82
+ resolve({ success: false, error: `命令超时 (>${opts.timeoutMs || 30000}ms)`, exitCode: -1 });
83
+ }, opts.timeoutMs || 30000);
84
+
85
+ proc.stdout.on('data', (data) => { stdout += data.toString(); });
86
+ proc.stderr.on('data', (data) => { stderr += data.toString(); });
87
+
88
+ proc.on('error', (err) => {
89
+ clearTimeout(timeout);
90
+ resolve({ success: false, error: `启动失败: ${err.message}` });
91
+ });
92
+
93
+ proc.on('close', (code) => {
94
+ clearTimeout(timeout);
95
+ const output = (stdout + (stderr ? `\n[stderr]\n${stderr}` : '')).trim();
96
+ if (code === 0) {
97
+ resolve({ success: true, output, exitCode: 0 });
98
+ } else {
99
+ resolve({ success: false, output, error: `exit code ${code}`, exitCode: code ?? -1 });
100
+ }
101
+ });
102
+ });
103
+ }
@@ -0,0 +1,202 @@
1
+ /**
2
+ * skill-loader.ts — 双 frontmatter 兼容的 SKILL.md 加载器
3
+ *
4
+ * 兼容两套 SKILL.md frontmatter:
5
+ * A. Anthropic Agent Skills 标准 (2025-12): name / description / license / compatibility / keywords
6
+ * B. bollharness 现有 frontmatter: name / description / status / tier / triggers / outputs / truth_policy
7
+ *
8
+ * 字段映射规则(统一到内部 SkillMeta):
9
+ * description ← 直接取
10
+ * license ← 取 A,没有则空
11
+ * status ← 取 B,没有则默认 'active'
12
+ * tier ← 取 B("tier" 是 bollharness 概念)
13
+ * triggers / keywords ← 合并 A.keywords 和 B.triggers 数组
14
+ * body ← 去掉 frontmatter 后的 Markdown 正文
15
+ *
16
+ * Skill 的 execute() 把 body 作为 Markdown 文档注入到 LLM context。
17
+ * 这是 Skills 协议的核心 — "告诉 agent 怎么做",与 MCP "能调什么"互补。
18
+ */
19
+
20
+ import * as fs from 'fs/promises';
21
+ import * as os from 'os';
22
+ import * as path from 'path';
23
+ import type { Skill } from '@bolloon/constraint-runtime';
24
+
25
+ /** 解析后的 SKILL.md 内部表示 */
26
+ export interface SkillMeta {
27
+ /** 唯一名, 通常 = 目录名 = frontmatter.name */
28
+ name: string;
29
+ /** SKILL.md 绝对路径 */
30
+ sourcePath: string;
31
+ /** 去掉 frontmatter 后的 Markdown body */
32
+ body: string;
33
+ /** 原始 frontmatter 解析结果, 保留以备调用方取 license/compatibility 等 */
34
+ frontmatter: Record<string, unknown>;
35
+ /** 统一后的 description */
36
+ description: string;
37
+ /** 状态: active / archived / draft, 缺省 active */
38
+ status: 'active' | 'archived' | 'draft';
39
+ /** tier (bollharness 概念, 缺省 'utility') */
40
+ tier: string;
41
+ /** 触发条件 (合并 keywords + triggers) */
42
+ triggers: string[];
43
+ }
44
+
45
+ /** YAML frontmatter 最小解析器 — 避免引入额外依赖, 支持双格式 */
46
+ function parseFrontmatter(raw: string): { frontmatter: Record<string, unknown>; body: string } {
47
+ // frontmatter 必须以 --- 开头, 紧跟换行, 再以 --- 闭合
48
+ const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
49
+ if (!match) {
50
+ return { frontmatter: {}, body: raw };
51
+ }
52
+ const [, yamlBlock, body] = match;
53
+ const frontmatter: Record<string, unknown> = {};
54
+ const lines = yamlBlock.split(/\r?\n/);
55
+ let currentKey: string | null = null;
56
+ let currentArray: string[] | null = null;
57
+
58
+ for (const rawLine of lines) {
59
+ const line = rawLine.replace(/\s+$/, '');
60
+ if (!line.trim()) continue;
61
+ // 数组项: " - value"
62
+ const arrItem = line.match(/^\s+-\s+(.*)$/);
63
+ if (arrItem && currentKey && currentArray) {
64
+ currentArray.push(stripQuotes(arrItem[1]));
65
+ continue;
66
+ }
67
+ // 键值对: "key: value" 或 "key:"
68
+ const kv = line.match(/^([A-Za-z_][A-Za-z0-9_-]*):\s*(.*)$/);
69
+ if (kv) {
70
+ const [, key, value] = kv;
71
+ if (value === '') {
72
+ // 可能是数组开始 (下一行 " - xxx")
73
+ currentKey = key;
74
+ currentArray = [];
75
+ frontmatter[key] = currentArray;
76
+ } else {
77
+ currentKey = key;
78
+ currentArray = null;
79
+ frontmatter[key] = stripQuotes(value);
80
+ }
81
+ }
82
+ }
83
+
84
+ return { frontmatter, body: body.replace(/^\r?\n/, '') };
85
+ }
86
+
87
+ function stripQuotes(s: string): string {
88
+ const t = s.trim();
89
+ if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
90
+ return t.slice(1, -1);
91
+ }
92
+ return t;
93
+ }
94
+
95
+ /** 把 frontmatter 统一到 SkillMeta */
96
+ function normalize(name: string, sourcePath: string, raw: string): SkillMeta | null {
97
+ const { frontmatter, body } = parseFrontmatter(raw);
98
+
99
+ // name 至少要有一个来源: frontmatter.name 或目录名
100
+ const fmName = typeof frontmatter.name === 'string' ? frontmatter.name : name;
101
+ const finalName = fmName || name;
102
+ if (!finalName) return null;
103
+
104
+ const description =
105
+ typeof frontmatter.description === 'string'
106
+ ? frontmatter.description
107
+ : '';
108
+
109
+ const statusRaw = typeof frontmatter.status === 'string' ? frontmatter.status.toLowerCase() : 'active';
110
+ const status: SkillMeta['status'] =
111
+ statusRaw === 'archived' || statusRaw === 'draft' ? statusRaw : 'active';
112
+
113
+ const tier = typeof frontmatter.tier === 'string' ? frontmatter.tier : 'utility';
114
+
115
+ // 合并两套触发字段
116
+ const triggers: string[] = [];
117
+ if (Array.isArray(frontmatter.triggers)) {
118
+ for (const t of frontmatter.triggers) if (typeof t === 'string') triggers.push(t);
119
+ }
120
+ if (Array.isArray(frontmatter.keywords)) {
121
+ for (const k of frontmatter.keywords) if (typeof k === 'string') triggers.push(k);
122
+ }
123
+
124
+ return { name: finalName, sourcePath, body, frontmatter, description, status, tier, triggers };
125
+ }
126
+
127
+ /** 解析单个 SKILL.md 文件 */
128
+ export async function parseSkillFile(filePath: string): Promise<SkillMeta | null> {
129
+ let raw: string;
130
+ try {
131
+ raw = await fs.readFile(filePath, 'utf-8');
132
+ } catch {
133
+ return null;
134
+ }
135
+ // 从路径推目录名作为 name 兜底
136
+ const dirName = path.basename(path.dirname(filePath));
137
+ return normalize(dirName, filePath, raw);
138
+ }
139
+
140
+ /** 扫描一个目录, 找所有 {name}/SKILL.md (一层嵌套结构) */
141
+ export async function loadSkillsDir(dir: string): Promise<SkillMeta[]> {
142
+ const out: SkillMeta[] = [];
143
+ let entries: import('fs').Dirent[];
144
+ try {
145
+ entries = await fs.readdir(dir, { withFileTypes: true });
146
+ } catch {
147
+ return out;
148
+ }
149
+ for (const entry of entries) {
150
+ if (!entry.isDirectory()) continue;
151
+ if (entry.name.startsWith('.')) continue;
152
+ const skillFile = path.join(dir, entry.name, 'SKILL.md');
153
+ const meta = await parseSkillFile(skillFile);
154
+ if (meta) out.push(meta);
155
+ }
156
+ return out;
157
+ }
158
+
159
+ /** 默认 skill 路径优先级 (后者覆盖前者同名 skill) */
160
+ export function defaultSkillPaths(home: string = os.homedir(), cwd: string = process.cwd()): string[] {
161
+ return [
162
+ path.join(home, '.bolloon', 'skills'), // 全局用户级
163
+ path.join(cwd, '.bolloon', 'skills'), // 项目级
164
+ path.join(home, '.boll', 'skills'), // 全局 (兼容 bollharness 旧用户)
165
+ ];
166
+ }
167
+
168
+ /** 把 SkillMeta 包成 @bolloon/constraint-runtime 期望的 Skill 对象 */
169
+ export function skillFromMeta(meta: SkillMeta): Skill {
170
+ return {
171
+ name: meta.name,
172
+ description: meta.description || meta.tier,
173
+ execute: async (_params: Record<string, unknown>): Promise<string> => {
174
+ // Skills 协议: 把 body 当 Markdown 文档返回, 由调用方注入 LLM context
175
+ // 调用方 (use_skill tool) 拿到后会把 body 放到 tool result,
176
+ // LLM 下一轮对话看到这份指南, 按它执行
177
+ const header = `## Skill: ${meta.name}\n\n${meta.description ? `> ${meta.description}\n\n` : ''}`;
178
+ const triggersBlock = meta.triggers.length
179
+ ? `**触发条件**: ${meta.triggers.join('; ')}\n\n`
180
+ : '';
181
+ return `${header}${triggersBlock}${meta.body}`;
182
+ },
183
+ };
184
+ }
185
+
186
+ /** 加载多个目录, 同名 skill 后者覆盖前者 */
187
+ export async function loadSkillsFromPaths(paths: string[]): Promise<Skill[]> {
188
+ const seen = new Map<string, Skill>();
189
+ for (const p of paths) {
190
+ const metas = await loadSkillsDir(p);
191
+ for (const m of metas) {
192
+ if (m.status === 'archived') continue; // 归档的跳过
193
+ seen.set(m.name, skillFromMeta(m));
194
+ }
195
+ }
196
+ return Array.from(seen.values());
197
+ }
198
+
199
+ /** 列出已加载的 skills (调试/UI 用) */
200
+ export function describeSkill(s: Skill): string {
201
+ return `${s.name}: ${s.description}`;
202
+ }
@@ -4,20 +4,20 @@
4
4
  * Integrates with existing ContextRouter and Judgment systems.
5
5
  *
6
6
  * Architecture:
7
- * - Session end → extract summary by work_type → store in .boll/state/context-chains/
7
+ * - Session end → extract summary by work_type → store in .bolloon/state/context-chains/
8
8
  * - Session start (Gate 0/3) → lookup related chains → inject summaries
9
9
  * - Work type: code_change | review | design | question | planning | debugging
10
10
  *
11
11
  * Integration points:
12
12
  * - Uses existing context-router-judgment.ts pattern (extends, not replaces)
13
13
  * - Gate injection via gate-judgment-inject.ts
14
- * - Storage in .boll/state/context-chains/
14
+ * - Storage in .bolloon/state/context-chains/
15
15
  */
16
16
 
17
17
  import * as fs from 'fs';
18
18
  import * as path from 'path';
19
19
 
20
- export const CONTEXT_CHAINS_DIR = path.join('.boll', 'state', 'context-chains');
20
+ export const CONTEXT_CHAINS_DIR = path.join('.bolloon', 'state', 'context-chains');
21
21
 
22
22
  export type WorkType = 'code_change' | 'review' | 'design' | 'question' | 'planning' | 'debugging';
23
23
 
@@ -119,7 +119,7 @@ export class ContextRouter {
119
119
 
120
120
  constructor(fragmentsDir?: string) {
121
121
  this.fragmentsDir = fragmentsDir || FRAGMENTS_DIR;
122
- this.injectedFile = path.join('.boll', 'guard', 'injected.json');
122
+ this.injectedFile = path.join('.bolloon', 'guard', 'injected.json');
123
123
  }
124
124
 
125
125
  /**
@@ -126,12 +126,14 @@ export class Watchdog {
126
126
  return;
127
127
  }
128
128
 
129
- // 检查内存使用
129
+ // 检查内存使用: 用绝对阈值, 不看 heapUsed/heapTotal 比例 (V8 内部比例不可靠, 经常 80-95% 误报)
130
130
  const usage = process.memoryUsage();
131
- const heapUsedPercent = (usage.heapUsed / usage.heapTotal) * 100;
132
- if (heapUsedPercent > 90) {
133
- console.warn(`[Watchdog] Memory usage critical: ${heapUsedPercent.toFixed(1)}%`);
134
- this.triggerRestart(1, `Memory usage ${heapUsedPercent.toFixed(1)}%`);
131
+ const heapUsedMB = usage.heapUsed / 1024 / 1024;
132
+ const rssMB = usage.rss / 1024 / 1024;
133
+ // 1.2GB heap 1.5GB RSS 才是真危险
134
+ if (heapUsedMB > 1224 || rssMB > 1536) {
135
+ console.warn(`[Watchdog] Memory usage critical: heapUsed=${heapUsedMB.toFixed(0)}MB, rss=${rssMB.toFixed(0)}MB`);
136
+ this.triggerRestart(1, `Memory usage heap=${heapUsedMB.toFixed(0)}MB rss=${rssMB.toFixed(0)}MB`);
135
137
  }
136
138
  }
137
139
 
@@ -8,6 +8,7 @@ export * from './HealthMonitor.js';
8
8
  export * from './Watchdog.js';
9
9
  export * from './DaemonManager.js';
10
10
  export * from './StartupVerifier.js';
11
+ export * from './self-improve-bus.js';
11
12
 
12
13
  import { createHealthMonitor, getHealthMonitor } from './HealthMonitor.js';
13
14
  import { createWatchdog, getWatchdog } from './Watchdog.js';
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Self-Improve Event Bus
3
+ *
4
+ * 心跳事件 → 自改触发器 (解耦 watchdog)
5
+ *
6
+ * 设计原则:
7
+ * - Watchdog 负责保活 (重启进程), 不知道"自改"是什么
8
+ * - Self-Improve Bus 监听"信号事件" (CI 失败, 任务连续失败, 静默超时)
9
+ * - 信号达到阈值 + 通过冷却期 → 触发 runSelfImproveLoop
10
+ * - 触发时通过 SSE 广播给前端, 用户能在 UI 里看到
11
+ *
12
+ * 关键不变量:
13
+ * 1. 心跳**不**直接调自改 - 通过 emit() 异步触发
14
+ * 2. 触发频率受 SELF_IMPROVE_COOLDOWN_MS 限制
15
+ * 3. 同类事件 24 小时内只触发 1 次
16
+ * 4. 触发后不阻塞健康检查
17
+ */
18
+
19
+ import { SELF_IMPROVE_COOLDOWN_MS } from '../agents/shell-guard.js';
20
+
21
+ export type SelfImproveEvent =
22
+ | { kind: 'ci-failed'; details: string }
23
+ | { kind: 'task-failures'; details: string }
24
+ | { kind: 'silent-timeout'; details: string }
25
+ | { kind: 'user-requested'; details: string };
26
+
27
+ interface EventRecord {
28
+ at: number;
29
+ count: number;
30
+ }
31
+
32
+ const EVENT_COOLDOWN_MS = 24 * 60 * 60 * 1000; // 24 小时内同类型只触发 1 次
33
+
34
+ const eventHistory: Map<SelfImproveEvent['kind'], EventRecord> = new Map();
35
+ let lastTriggerAt: number | null = null;
36
+
37
+ type Listener = (event: SelfImproveEvent, goal: string) => void | Promise<void>;
38
+ const listeners: Set<Listener> = new Set();
39
+
40
+ /**
41
+ * 订阅自改触发事件
42
+ */
43
+ export function onSelfImproveTrigger(fn: Listener): () => void {
44
+ listeners.add(fn);
45
+ return () => { listeners.delete(fn); };
46
+ }
47
+
48
+ /**
49
+ * 心跳事件 → 自改总线
50
+ *
51
+ * @returns { triggered: boolean, reason?: string }
52
+ */
53
+ export function reportSelfImproveEvent(event: SelfImproveEvent): { triggered: boolean; reason?: string } {
54
+ // 1. 24 小时同类事件冷却
55
+ const prev = eventHistory.get(event.kind);
56
+ if (prev && Date.now() - prev.at < EVENT_COOLDOWN_MS) {
57
+ return { triggered: false, reason: `同类事件 ${event.kind} 在 24h 内已记录过, 跳过` };
58
+ }
59
+
60
+ // 2. 累加计数
61
+ eventHistory.set(event.kind, {
62
+ at: Date.now(),
63
+ count: (prev?.count || 0) + 1
64
+ });
65
+
66
+ // 3. 自改循环冷却
67
+ if (lastTriggerAt && Date.now() - lastTriggerAt < SELF_IMPROVE_COOLDOWN_MS) {
68
+ const waitHrs = Math.ceil((SELF_IMPROVE_COOLDOWN_MS - (Date.now() - lastTriggerAt)) / 3600000);
69
+ return { triggered: false, reason: `自改冷却中, 还需要约 ${waitHrs} 小时` };
70
+ }
71
+
72
+ // 4. 触发
73
+ lastTriggerAt = Date.now();
74
+ const goal = `信号事件: ${event.kind} - ${event.details}`;
75
+
76
+ console.log(`[self-improve-bus] 🚀 触发自改循环: ${goal}`);
77
+
78
+ // 异步触发所有 listener, 不阻塞调用方
79
+ Promise.resolve().then(async () => {
80
+ for (const listener of listeners) {
81
+ try {
82
+ await listener(event, goal);
83
+ } catch (err) {
84
+ console.error(`[self-improve-bus] listener 失败:`, err);
85
+ }
86
+ }
87
+ });
88
+
89
+ return { triggered: true };
90
+ }
91
+
92
+ /**
93
+ * 获取当前事件历史 (供调试 / UI 显示)
94
+ */
95
+ export function getEventHistory(): Array<{ kind: string; at: string; count: number }> {
96
+ return Array.from(eventHistory.entries()).map(([kind, rec]) => ({
97
+ kind,
98
+ at: new Date(rec.at).toISOString(),
99
+ count: rec.count
100
+ }));
101
+ }
102
+
103
+ /**
104
+ * 强制重置 (仅供调试)
105
+ */
106
+ export function resetSelfImproveBus(): void {
107
+ eventHistory.clear();
108
+ lastTriggerAt = null;
109
+ console.log('[self-improve-bus] 已重置');
110
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 缺失类型模块的兜底声明 — 让 strict tsc 通过, 避免阻塞发布。
3
+ * 这些是项目里实际使用但 @types 包未安装或缺失的小依赖。
4
+ */
5
+ declare module 'js-yaml' {
6
+ const yaml: {
7
+ load: (str: string) => any;
8
+ dump: (obj: any, opts?: any) => string;
9
+ };
10
+ export default yaml;
11
+ export = yaml;
12
+ }
@@ -283,27 +283,27 @@ function checkNpmOutdated(): OutdatedPackage[] {
283
283
  * 自动更新 npm 包
284
284
  */
285
285
  async function updatePackages(packages?: string[]): Promise<UpdateResult> {
286
- try {
287
- const args = packages && packages.length > 0
288
- ? ['npm', 'install', ...packages, '--save']
289
- : ['npm', 'install', '-g', '@bolloon/bolloon-agent'];
286
+ // 记录更新前的版本,用于事后判断"是否真的升级了"
287
+ // getInstalledVersion 的"优先读全局"保持一致 —— install 也用 -g,
288
+ // 否则判断和执行落在不同的目录,永远改不到那个被读取的版本号。
289
+ const targets = packages && packages.length > 0 ? packages : ['@bolloon/bolloon-agent'];
290
+ const before = new Map<string, string | null>();
291
+ for (const p of targets) before.set(p, getInstalledVersion(p));
292
+
293
+ const isGlobal = !packages || packages.length === 0;
294
+ const args = isGlobal
295
+ ? ['npm', 'install', '-g', ...targets]
296
+ : ['npm', 'install', ...targets, '--save'];
290
297
 
291
- log(`\n${CYAN}📦 正在更新包...${RESET}\n`, RESET);
298
+ log(`\n${CYAN}📦 正在更新包...${RESET}\n`, RESET);
292
299
 
293
- // 执行 npm install
294
- const result = execSync(args.join(' '), {
300
+ try {
301
+ execSync(args.join(' '), {
295
302
  encoding: 'utf-8',
296
303
  timeout: 300000, // 5分钟超时
297
304
  stdio: 'inherit',
298
305
  cwd: process.cwd()
299
306
  });
300
-
301
- return {
302
- success: true,
303
- updated: true,
304
- message: '更新成功',
305
- updatedPackages: packages
306
- };
307
307
  } catch (e: any) {
308
308
  return {
309
309
  success: false,
@@ -312,6 +312,37 @@ async function updatePackages(packages?: string[]): Promise<UpdateResult> {
312
312
  error: e.message
313
313
  };
314
314
  }
315
+
316
+ // install 退出码 0 并不等于"真的升上去了"("up to date" 也是 0)。
317
+ // 重新读取磁盘版本,只有真的达到目标 latest 之一才算 updated。
318
+ const upgraded: string[] = [];
319
+ const failed: string[] = [];
320
+ for (const p of targets) {
321
+ const after = getInstalledVersion(p);
322
+ const was = before.get(p);
323
+ if (after && was && compareVersions(was, after) < 0) {
324
+ upgraded.push(p);
325
+ } else if (after && was && compareVersions(was, after) === 0) {
326
+ // 版本没变 —— install 跑过但没改动;不当作"刚升级"
327
+ } else {
328
+ failed.push(p);
329
+ }
330
+ }
331
+
332
+ if (upgraded.length > 0) {
333
+ return {
334
+ success: true,
335
+ updated: true,
336
+ message: `已更新: ${upgraded.join(', ')}`,
337
+ updatedPackages: upgraded
338
+ };
339
+ }
340
+ return {
341
+ success: true,
342
+ updated: false,
343
+ message: '已是最新版本,无需重启',
344
+ updatedPackages: []
345
+ };
315
346
  }
316
347
 
317
348
  /**