@bolloon/bolloon-agent 0.3.42 → 0.3.43

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.
@@ -146,8 +146,26 @@ export async function writeSkillCandidate(c) {
146
146
  const dir = getCandidateDir();
147
147
  await fs.mkdir(dir, { recursive: true });
148
148
  const safeName = sanitizeSkillName(c.name);
149
- const file = path.join(dir, `${safeName}-${Date.now()}.json`);
150
- await fs.writeFile(file, JSON.stringify(c, null, 2), 'utf-8');
149
+ // 2026-08-08: signature 的候选用固定文件名 (合并更新同一个), 无 signature 才带时间戳
150
+ const file = c.signature
151
+ ? path.join(dir, `${safeName}.json`)
152
+ : path.join(dir, `${safeName}-${Date.now()}.json`);
153
+ // 追加式合并: 若同 signature 已存在, 累积 runs + 追加 body
154
+ let runs = c.runs ?? 1;
155
+ let body = c.body;
156
+ try {
157
+ const prev = JSON.parse(await fs.readFile(file, 'utf-8'));
158
+ if (prev && prev.runs)
159
+ runs = prev.runs + 1;
160
+ if (prev && prev.body && body !== prev.body && c.signature) {
161
+ // 同一 signature 重复运行 → 追加一条经验 (去重, 避免 body 膨胀)
162
+ const line = `- ${new Date().toISOString().slice(0, 16)} ${c.source}: ${c.description}`;
163
+ body = `${prev.body}\n${line}`;
164
+ }
165
+ }
166
+ catch { /* 新文件 */ }
167
+ const merged = { ...c, runs, body, timestamp: c.timestamp || new Date().toISOString() };
168
+ await fs.writeFile(file, JSON.stringify(merged, null, 2), 'utf-8');
151
169
  return file;
152
170
  }
153
171
  export async function listSkillCandidates(home = os.homedir()) {
@@ -165,12 +183,37 @@ export async function listSkillCandidates(home = os.homedir()) {
165
183
  const raw = await fs.readFile(path.join(dir, f), 'utf-8');
166
184
  const c = JSON.parse(raw);
167
185
  if (c.name && c.body)
168
- out.push(c);
186
+ out.push({ ...c, file: path.join(dir, f) });
169
187
  }
170
188
  catch { /* 坏文件跳过 */ }
171
189
  }
172
190
  return out;
173
191
  }
192
+ /** 按名字删除所有同名候选文件 (名可能与文件名前缀不完全一致) */
193
+ async function removeCandidateFiles(name, home) {
194
+ const safe = sanitizeSkillName(name);
195
+ const dir = getCandidateDir(home);
196
+ let files;
197
+ try {
198
+ files = await fs.readdir(dir);
199
+ }
200
+ catch {
201
+ return;
202
+ }
203
+ for (const f of files) {
204
+ if (!f.endsWith('.json'))
205
+ continue;
206
+ try {
207
+ const c = JSON.parse(await fs.readFile(path.join(dir, f), 'utf-8'));
208
+ if (sanitizeSkillName(c.name) === safe)
209
+ await fs.rm(path.join(dir, f), { force: true });
210
+ }
211
+ catch {
212
+ if (f.startsWith(safe))
213
+ await fs.rm(path.join(dir, f), { force: true });
214
+ }
215
+ }
216
+ }
174
217
  /** 把候选转正为正式 skill (可选: 转正后删除候选文件) */
175
218
  export async function promoteCandidate(name, opts = {}, home = os.homedir()) {
176
219
  const candidates = await listSkillCandidates(home);
@@ -179,18 +222,25 @@ export async function promoteCandidate(name, opts = {}, home = os.homedir()) {
179
222
  return { ok: false, path: '', error: `候选 '${name}' 不存在` };
180
223
  const r = await createSkill(c.name, c.description, c.body, opts);
181
224
  if (r.ok) {
182
- // 清理已转正的候选文件
183
- try {
184
- const dir = getCandidateDir(home);
185
- for (const f of (await fs.readdir(dir))) {
186
- if (f.startsWith(sanitizeSkillName(c.name) + '-'))
187
- await fs.rm(path.join(dir, f), { force: true });
188
- }
189
- }
190
- catch { /* 清理失败不阻塞 */ }
225
+ // 清理已转正的候选文件: 同 name 的所有候选
226
+ await removeCandidateFiles(c.name, home);
191
227
  }
192
228
  return r;
193
229
  }
230
+ /**
231
+ * 从一轮成功的工具调用生成稳定签名 — 同一套工具序列 (有序去重, 最多 4 个) 视为同一经验.
232
+ * 用于跨运行合并: 第二次跑同样的工具 → 更新同一个候选, 而不是新建一个.
233
+ */
234
+ export function toolSignature(okSteps) {
235
+ const seq = [];
236
+ for (const s of (okSteps || [])) {
237
+ if (s.name && !seq.includes(s.name))
238
+ seq.push(s.name);
239
+ if (seq.length >= 4)
240
+ break;
241
+ }
242
+ return seq.join('_');
243
+ }
194
244
  export async function writeRunEndSkillCandidates(steps, source, minOk = 2) {
195
245
  const okSteps = (steps || []).filter((s) => s.status === 'ok' && s.name && s.name !== 'system' && s.name !== '?');
196
246
  if (okSteps.length < minOk) {
@@ -200,13 +250,19 @@ export async function writeRunEndSkillCandidates(steps, source, minOk = 2) {
200
250
  const body = `## 背景\n本轮对话连续成功调用了 ${okSteps.length} 个工具: ${toolNames}.\n\n` +
201
251
  `## 流程\n${okSteps.map((s) => `1. 调用 ${s.name}${s.output ? ': ' + String(s.output).slice(0, 120) : ''}`).join('\n')}\n\n` +
202
252
  `## 注意事项\n- 工具名以 list_skills / get_operation_logs 的实际注册名为准\n- 沉淀为正式 skill 前请人工确认流程可复用\n`;
203
- const candName = `auto-${okSteps[0].name}-${Date.now().toString(36)}`;
253
+ // 2026-08-08: 稳定签名 + 固定文件名 → 同一套工具反复跑时合并更新到同一个候选 (runs++)
254
+ const signature = toolSignature(okSteps);
255
+ const candName = `auto-${signature}`;
256
+ const existing = (await listSkillCandidates()).find((x) => x.signature === signature || sanitizeSkillName(x.name) === sanitizeSkillName(candName));
204
257
  const file = await writeSkillCandidate({
205
258
  name: candName,
206
259
  description: `自动候选: ${okSteps.length} 个工具连续成功 (${toolNames})`,
207
260
  body,
208
261
  source,
209
262
  timestamp: new Date().toISOString(),
263
+ signature,
210
264
  });
211
- return { wrote: true, file, count: okSteps.length, names: toolNames };
265
+ const merged = !!existing;
266
+ const runs = (existing?.runs ?? 0) + 1;
267
+ return { wrote: true, file, count: okSteps.length, names: toolNames, merged, runs };
212
268
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bolloon/bolloon-agent",
3
- "version": "0.3.42",
3
+ "version": "0.3.43",
4
4
  "type": "module",
5
5
  "description": "P2P AI Document Agent - 全局安装后执行 `bolloon` 启动产品",
6
6
  "main": "dist/cli-entry.js",