pi-think-anchor 1.0.2 → 1.0.3
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/README.md +1 -1
- package/index.ts +90 -22
- package/package.json +1 -1
package/README.md
CHANGED
package/index.ts
CHANGED
|
@@ -181,6 +181,14 @@ interface TAState {
|
|
|
181
181
|
lastL2: number;
|
|
182
182
|
lastRatio: number;
|
|
183
183
|
lastBand: TABand;
|
|
184
|
+
/** v3:效果闭环与自适应(借鉴 ptc-meter 的数据自校正 + patch-keeper 的条件触发) */
|
|
185
|
+
recent: number[];
|
|
186
|
+
pending: { turn: number; pre: number } | null;
|
|
187
|
+
effSamples: number;
|
|
188
|
+
effImproved: number;
|
|
189
|
+
autoMuted: boolean;
|
|
190
|
+
lastLoggedBand: TABand;
|
|
191
|
+
skipped: number;
|
|
184
192
|
}
|
|
185
193
|
|
|
186
194
|
function newState(cfg: TAConfig): TAState {
|
|
@@ -197,9 +205,22 @@ function newState(cfg: TAConfig): TAState {
|
|
|
197
205
|
lastL2: 0,
|
|
198
206
|
lastRatio: 0,
|
|
199
207
|
lastBand: "mixed",
|
|
208
|
+
recent: [],
|
|
209
|
+
pending: null,
|
|
210
|
+
effSamples: 0,
|
|
211
|
+
effImproved: 0,
|
|
212
|
+
autoMuted: false,
|
|
213
|
+
lastLoggedBand: "mixed",
|
|
214
|
+
skipped: 0,
|
|
200
215
|
};
|
|
201
216
|
}
|
|
202
217
|
|
|
218
|
+
/** 采样写盘(v3):非事件轮每 5 轮留一行——实测 6151 行 / 1.0 MB 逼近 1 MiB 上限,降为 1/5 仍够统计 */
|
|
219
|
+
const SAMPLE_EVERY = 5;
|
|
220
|
+
/** 自适应(v3):有效样本 ≥ AUTO_MIN 且下降率 < AUTO_RATE(不及抛硬币)即转静默,只记录不注入 */
|
|
221
|
+
const AUTO_MIN = 8;
|
|
222
|
+
const AUTO_RATE = 0.5;
|
|
223
|
+
|
|
203
224
|
/** 日志上限 1 MiB(与 ptc-meter 同口径):超限保留尾部,避免无界增长。 */
|
|
204
225
|
const MAX_LOG = 1 << 20;
|
|
205
226
|
|
|
@@ -234,7 +255,7 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
234
255
|
const fp = fingerprint(thinking, st.cfg.lexicon);
|
|
235
256
|
if (fp.pos + fp.neg + fp.neu === 0) {
|
|
236
257
|
st.turns++;
|
|
237
|
-
appendLog({ ts: new Date().toISOString(), turn: st.turns, v:
|
|
258
|
+
if (st.turns % SAMPLE_EVERY === 0) appendLog({ ts: new Date().toISOString(), turn: st.turns, v: 3, len: thinking.length, pos: 0, neg: 0, neu: 0, ratio: null, hits: 0, negRate: null, band: "none", phase: st.phase, want: "none", acted: "none", eff: null });
|
|
238
259
|
return;
|
|
239
260
|
}
|
|
240
261
|
st.window.push(fp);
|
|
@@ -256,7 +277,7 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
256
277
|
const now = Date.now();
|
|
257
278
|
if (d.level === "none") {
|
|
258
279
|
st.phase = d.phase;
|
|
259
|
-
} else if (d.level === "L1" && now - st.lastL1 > st.cfg.intervention.l1CooldownMin * 60_000) {
|
|
280
|
+
} else if (d.level === "L1" && !st.autoMuted && now - st.lastL1 > st.cfg.intervention.l1CooldownMin * 60_000) {
|
|
260
281
|
st.lastL1 = now;
|
|
261
282
|
st.l1Count++;
|
|
262
283
|
(pi as unknown as { sendMessage?: (m: unknown, o?: unknown) => void }).sendMessage?.(
|
|
@@ -264,8 +285,9 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
264
285
|
{ deliverAs: "nextTurn" },
|
|
265
286
|
);
|
|
266
287
|
acted = "L1";
|
|
288
|
+
st.pending = { turn: st.turns, pre: st.recent.slice(-3).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(3, st.recent.length)) };
|
|
267
289
|
st.phase = d.phase;
|
|
268
|
-
} else if (d.level === "L2" && now - st.lastL2 > st.cfg.intervention.l2CooldownMin * 60_000) {
|
|
290
|
+
} else if (d.level === "L2" && !st.autoMuted && now - st.lastL2 > st.cfg.intervention.l2CooldownMin * 60_000) {
|
|
269
291
|
st.lastL2 = now;
|
|
270
292
|
st.l2Tried++;
|
|
271
293
|
st.l2Count++;
|
|
@@ -274,36 +296,58 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
274
296
|
{ deliverAs: "nextTurn" },
|
|
275
297
|
);
|
|
276
298
|
acted = "L2";
|
|
299
|
+
st.pending = { turn: st.turns, pre: st.recent.slice(-3).reduce((a, b) => a + b, 0) / Math.max(1, Math.min(3, st.recent.length)) };
|
|
277
300
|
st.phase = d.phase;
|
|
278
301
|
} else {
|
|
279
302
|
// 被冷却/上限挡下:记 blocked,避免与「无需动作」混淆(实测 583 次 want=L2 只有 55 次真发)
|
|
280
303
|
st.phase = prevPhase;
|
|
281
|
-
if (d.level !== "none") acted = "blocked-" + d.level;
|
|
304
|
+
if (d.level !== "none") acted = st.autoMuted ? "muted-" + d.level : "blocked-" + d.level;
|
|
282
305
|
}
|
|
283
306
|
if (band === "spec" && st.phase === "healthy") st.l2Tried = 0; // 恢复后重置 L2 计数
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
307
|
+
// v3 效果闭环:干预后第 3 轮结算(pre = 干预前最近 3 轮 ratio 均值)
|
|
308
|
+
st.recent.push(ratio);
|
|
309
|
+
if (st.recent.length > 6) st.recent.shift();
|
|
310
|
+
let eff: number | null = null;
|
|
311
|
+
if (st.pending && st.turns - st.pending.turn >= 3) {
|
|
312
|
+
const tail = st.recent.slice(-3);
|
|
313
|
+
const post = tail.reduce((a, b) => a + b, 0) / Math.max(1, tail.length);
|
|
314
|
+
eff = Math.round((post - st.pending.pre) * 100) / 100;
|
|
315
|
+
st.effSamples++;
|
|
316
|
+
if (eff < 0) st.effImproved++;
|
|
317
|
+
st.pending = null;
|
|
318
|
+
// 自适应:样本够且实测下降率不及抛硬币 → 转静默(只记录,不注入)
|
|
319
|
+
st.autoMuted = st.effSamples >= AUTO_MIN && st.effImproved / st.effSamples < AUTO_RATE;
|
|
320
|
+
}
|
|
321
|
+
// 采样写盘:事件轮(有干预/有结算/波段变化)必写,其余每 SAMPLE_EVERY 轮一行
|
|
322
|
+
const eventful = acted !== "none" || eff !== null || band !== st.lastLoggedBand;
|
|
323
|
+
if (!eventful && st.turns % SAMPLE_EVERY !== 0) { st.skipped++; }
|
|
324
|
+
else {
|
|
325
|
+
st.lastLoggedBand = band;
|
|
326
|
+
appendLog({
|
|
327
|
+
ts: new Date().toISOString(),
|
|
328
|
+
turn: st.turns,
|
|
329
|
+
v: 3,
|
|
330
|
+
len: thinking.length,
|
|
331
|
+
pos: fp.pos,
|
|
332
|
+
neg: fp.neg,
|
|
333
|
+
neu: fp.neu,
|
|
334
|
+
hits,
|
|
335
|
+
negRate: Math.round(negRate * 100) / 100,
|
|
336
|
+
ratio: Math.round(ratio * 100) / 100,
|
|
337
|
+
band,
|
|
338
|
+
phase: st.phase,
|
|
339
|
+
want: d.level,
|
|
340
|
+
acted,
|
|
341
|
+
eff,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
300
344
|
} catch {
|
|
301
345
|
/* 绝不抛错 */
|
|
302
346
|
}
|
|
303
347
|
});
|
|
304
348
|
|
|
305
349
|
pi.registerCommand("think-anchor", {
|
|
306
|
-
description: "思维锚定监测:状态 / on / off / reset",
|
|
350
|
+
description: "思维锚定监测:状态 / stats(日志统计)/ on / off / reset",
|
|
307
351
|
handler: async (args, ctx) => {
|
|
308
352
|
const sub = (args || "").trim().toLowerCase();
|
|
309
353
|
if (sub === "on") {
|
|
@@ -321,6 +365,28 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
321
365
|
if (ctx.hasUI) ctx.ui.notify("think-anchor: 会话状态已重置。", "info");
|
|
322
366
|
return;
|
|
323
367
|
}
|
|
368
|
+
if (sub === "stats") {
|
|
369
|
+
try {
|
|
370
|
+
const txt = readFileSync(LOG, "utf8");
|
|
371
|
+
const rows = txt.split("\n").filter((l) => l.trim()).map((l) => { try { return JSON.parse(l) as Record<string, unknown>; } catch { return null; } }).filter(Boolean) as Array<Record<string, unknown>>;
|
|
372
|
+
const group = (k: string) => {
|
|
373
|
+
const m = new Map<string, number>();
|
|
374
|
+
for (const r of rows) { const key = String(r[k] ?? "?"); m.set(key, (m.get(key) ?? 0) + 1); }
|
|
375
|
+
return [...m.entries()].map(([a, b]) => a + "=" + b).join(" ") || "-";
|
|
376
|
+
};
|
|
377
|
+
const effRows = rows.filter((r) => typeof r.eff === "number");
|
|
378
|
+
const improved = effRows.filter((r) => (r.eff as number) < 0).length;
|
|
379
|
+
const size = (statSync(LOG).size / 1048576).toFixed(2);
|
|
380
|
+
const lines = [
|
|
381
|
+
"think-anchor 统计:" + size + " MB / 上限 1.00 · " + rows.length + " 行",
|
|
382
|
+
" 版本 " + group("v") + " · 波段 " + group("band"),
|
|
383
|
+
" 动作 " + group("acted") + " · want " + group("want"),
|
|
384
|
+
" 效果结算 " + improved + "/" + effRows.length + (effRows.length ? "(下降 " + Math.round((100 * improved) / effRows.length) + "%)" : ""),
|
|
385
|
+
];
|
|
386
|
+
if (ctx.hasUI) ctx.ui.notify(lines.join("\n"), "info");
|
|
387
|
+
} catch { if (ctx.hasUI) ctx.ui.notify("think-anchor: 统计失败(日志读取异常)", "warning"); }
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
324
390
|
const line =
|
|
325
391
|
"think-anchor:" +
|
|
326
392
|
(st.cfg.enabled ? "ON" : "OFF") +
|
|
@@ -328,7 +394,9 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
|
|
|
328
394
|
" · 消息 " + st.turns +
|
|
329
395
|
" · 波段 spec/mixed/react=" + st.bandHist.spec + "/" + st.bandHist.mixed + "/" + st.bandHist.react +
|
|
330
396
|
" · 干预 L1×" + st.l1Count + " L2×" + st.l2Count +
|
|
331
|
-
" ·
|
|
397
|
+
" · 有效率 " + st.effImproved + "/" + st.effSamples + (st.autoMuted ? "(不及抛硬币→已静默)" : "") +
|
|
398
|
+
" · 上轮 命中" + lastHits + " 发散占比" + (Math.round(lastNegRate * 100) / 100) + "(" + st.lastBand + ")" +
|
|
399
|
+
" · 采样跳过 " + st.skipped;
|
|
332
400
|
if (ctx.hasUI) ctx.ui.notify(line, "info");
|
|
333
401
|
},
|
|
334
402
|
});
|