pi-think-anchor 1.0.0 → 1.0.2

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 (3) hide show
  1. package/README.md +4 -0
  2. package/index.ts +38 -11
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,6 +2,10 @@
2
2
 
3
3
  监测 thinking 的「专注指纹」,波段漂移时分级拉回,避免长时间发散。
4
4
 
5
+ ## 来源与借鉴
6
+
7
+ 蒸馏自 [dsh-anchored-monitor](https://dsh-plugin.org/zh/plugins/aik358/dsh-anchored-monitor) 0.3.1(DSH 生态):机制思想来源,非代码移植(回合级而非流内实时、无独立进程)。判据 v2(证据门槛 + 发散占比)为本机 6151 条日志校准后的本地改进。
8
+
5
9
  ## 功能
6
10
 
7
11
  - 对每段 thinking 计算专注/发散词频比率,滑动窗口聚合判定波段。
package/index.ts CHANGED
@@ -30,6 +30,8 @@ interface TAConfig {
30
30
  lexicon: { positive: string[]; negative: string[]; neutral: string[] };
31
31
  windowSize: number;
32
32
  bands: { spec: number; react: number };
33
+ /** v2 主判据:证据门槛 + 发散词占比(与长度无关) */
34
+ score: { minHits: number; specRate: number; reactRate: number };
33
35
  weights: { beta: number; eps: number };
34
36
  intervention: { l1CooldownMin: number; l2CooldownMin: number; maxL2: number };
35
37
  }
@@ -39,10 +41,15 @@ const DEF_CFG: TAConfig = {
39
41
  lexicon: { positive: DEF_POS, negative: DEF_NEG, neutral: DEF_NEU },
40
42
  windowSize: 5,
41
43
  bands: { spec: 1.2, react: 0.5 },
44
+ score: { minHits: 3, specRate: 0.25, reactRate: 0.5 },
42
45
  weights: { beta: 0.4, eps: 0.5 },
43
46
  intervention: { l1CooldownMin: 10, l2CooldownMin: 15, maxL2: 2 },
44
47
  };
45
48
 
49
+ // 当前轮诊断量(status 显示用;比值/命中量随轮更新)
50
+ let lastHits = 0;
51
+ let lastNegRate = 0;
52
+
46
53
  function loadCfg(): TAConfig {
47
54
  try {
48
55
  if (!existsSync(CONF)) return DEF_CFG;
@@ -95,6 +102,19 @@ export function bandOf(ratio: number, bands: { spec: number; react: number } = D
95
102
  return "react";
96
103
  }
97
104
 
105
+ /**
106
+ * v2 判据:先过证据门槛,再看发散词占比 negRate = neg/(pos+neg+neu)。
107
+ * 数据依据(本机 6151 条实测):react 行 thinking 长度中位 1998、词命中中位 2,43% 的 neg<=1
108
+ * —— 旧 ratio(pos/(neg+eps))把「长而散」误判成「反复试探」:571 次 react 里 326 次证据不足。
109
+ * negRate 分离度:spec 中位 0.14 / mixed 0.50 / react 1.00。
110
+ */
111
+ export function classify(hits: number, negRate: number, c: { minHits: number; specRate: number; reactRate: number }): TABand {
112
+ if (hits < c.minHits) return "none";
113
+ if (negRate >= c.reactRate) return "react";
114
+ if (negRate >= c.specRate) return "mixed";
115
+ return "spec";
116
+ }
117
+
98
118
  /** 从 assistant 消息 content 提取 thinking 全文(多形态兼容) */
99
119
  export function extractThinking(content: unknown): string {
100
120
  try {
@@ -115,12 +135,14 @@ export function extractThinking(content: unknown): string {
115
135
  }
116
136
  }
117
137
 
118
- export type TABand = "spec" | "mixed" | "react";
138
+ export type TABand = "spec" | "mixed" | "react" | "none";
119
139
  export type TAPhase = "healthy" | "warning" | "critical";
120
140
  export interface TADecision { level: "none" | "L1" | "L2"; phase: TAPhase; reason: string }
121
141
 
122
142
  /** 分级干预决策状态机(纯函数) */
123
143
  export function decideNext(phase: TAPhase, band: TABand, l2Tried: number, maxL2: number): TADecision {
144
+ // v2:证据不足的轮次不参与决策(否则 critical 下的 none 会被当成 react 去升 L2)
145
+ if (band === "none") return { level: "none", phase, reason: "证据不足(不评分)" };
124
146
  if (phase === "healthy") {
125
147
  if (band === "react") return { level: "L1", phase: "warning", reason: "首次跌入 react 波段" };
126
148
  if (band === "mixed") return { level: "none", phase: "warning", reason: "进入过渡带观察" };
@@ -212,20 +234,24 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
212
234
  const fp = fingerprint(thinking, st.cfg.lexicon);
213
235
  if (fp.pos + fp.neg + fp.neu === 0) {
214
236
  st.turns++;
215
- appendLog({ ts: new Date().toISOString(), turn: st.turns, len: thinking.length, pos: 0, neg: 0, neu: 0, wpos: 0, wneg: 0, wneu: 0, ratio: null, band: "none", phase: st.phase, want: "none", acted: "none" });
237
+ appendLog({ ts: new Date().toISOString(), turn: st.turns, v: 2, len: thinking.length, pos: 0, neg: 0, neu: 0, ratio: null, hits: 0, negRate: null, band: "none", phase: st.phase, want: "none", acted: "none" });
216
238
  return;
217
239
  }
218
240
  st.window.push(fp);
219
241
  if (st.window.length > st.cfg.windowSize) st.window.shift();
220
242
  const agg = st.window.reduce((a, b) => ({ pos: a.pos + b.pos, neg: a.neg + b.neg, neu: a.neu + b.neu }), { pos: 0, neg: 0, neu: 0 });
243
+ const hits = agg.pos + agg.neg + agg.neu;
244
+ const negRate = hits > 0 ? agg.neg / hits : 0;
245
+ lastHits = hits;
246
+ lastNegRate = negRate;
221
247
  const ratio = ratioOf(agg.pos, agg.neg, agg.neu, st.cfg.weights);
222
- const band = bandOf(ratio, st.cfg.bands);
248
+ const band = classify(hits, negRate, st.cfg.score);
223
249
  const prevPhase = st.phase;
224
250
  const d = decideNext(st.phase, band, st.l2Tried, st.cfg.intervention.maxL2);
225
251
  st.turns++;
226
252
  st.lastRatio = ratio;
227
253
  st.lastBand = band;
228
- st.bandHist[band]++;
254
+ if (band !== "none") st.bandHist[band]++; // none = 证据不足,不计波段(否则 NaN)
229
255
  let acted = "none";
230
256
  const now = Date.now();
231
257
  if (d.level === "none") {
@@ -234,7 +260,7 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
234
260
  st.lastL1 = now;
235
261
  st.l1Count++;
236
262
  (pi as unknown as { sendMessage?: (m: unknown, o?: unknown) => void }).sendMessage?.(
237
- { customType: "think-anchor", content: L1_TEXT + `(本轮波段 ${band} · 比率 ${ratio.toFixed(2)})`, display: false },
263
+ { customType: "think-anchor", content: L1_TEXT + `(发散占比 ${negRate.toFixed(2)} · 命中 ${hits})`, display: false },
238
264
  { deliverAs: "nextTurn" },
239
265
  );
240
266
  acted = "L1";
@@ -244,26 +270,27 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
244
270
  st.l2Tried++;
245
271
  st.l2Count++;
246
272
  (pi as unknown as { sendMessage?: (m: unknown, o?: unknown) => void }).sendMessage?.(
247
- { customType: "think-anchor", content: L2_TEXT + `(本轮波段 ${band} · 比率 ${ratio.toFixed(2)},L2 ${st.l2Tried}/${st.cfg.intervention.maxL2})`, display: false },
273
+ { customType: "think-anchor", content: L2_TEXT + `(发散占比 ${negRate.toFixed(2)} · 命中 ${hits},L2 ${st.l2Tried}/${st.cfg.intervention.maxL2})`, display: false },
248
274
  { deliverAs: "nextTurn" },
249
275
  );
250
276
  acted = "L2";
251
277
  st.phase = d.phase;
252
278
  } else {
253
- // 冷却挡下或等级不可执行:保持原相位,下一轮仍可重试 L1(不再直接跳到 L2)
279
+ // 被冷却/上限挡下:记 blocked,避免与「无需动作」混淆(实测 583 次 want=L2 只有 55 次真发)
254
280
  st.phase = prevPhase;
281
+ if (d.level !== "none") acted = "blocked-" + d.level;
255
282
  }
256
283
  if (band === "spec" && st.phase === "healthy") st.l2Tried = 0; // 恢复后重置 L2 计数
257
284
  appendLog({
258
285
  ts: new Date().toISOString(),
259
286
  turn: st.turns,
287
+ v: 2,
260
288
  len: thinking.length,
261
289
  pos: fp.pos,
262
- wpos: agg.pos,
263
- wneg: agg.neg,
264
- wneu: agg.neu,
265
290
  neg: fp.neg,
266
291
  neu: fp.neu,
292
+ hits,
293
+ negRate: Math.round(negRate * 100) / 100,
267
294
  ratio: Math.round(ratio * 100) / 100,
268
295
  band,
269
296
  phase: st.phase,
@@ -301,7 +328,7 @@ export default function thinkAnchor(pi: ExtensionAPI): void {
301
328
  " · 消息 " + st.turns +
302
329
  " · 波段 spec/mixed/react=" + st.bandHist.spec + "/" + st.bandHist.mixed + "/" + st.bandHist.react +
303
330
  " · 干预 L1×" + st.l1Count + " L2×" + st.l2Count +
304
- " · 窗口比率 " + (Math.round(st.lastRatio * 100) / 100) + "(" + st.lastBand + ")";
331
+ " · 上轮 命中" + lastHits + " 发散占比" + (Math.round(lastNegRate * 100) / 100) + "(" + st.lastBand + ")";
305
332
  if (ctx.hasUI) ctx.ui.notify(line, "info");
306
333
  },
307
334
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-think-anchor",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Monitors thinking focus drift and nudges the model back on track.",
5
5
  "type": "module",
6
6
  "license": "MIT",