pi-patch-keeper 1.2.0 → 1.2.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.
- package/README.en.md +50 -21
- package/README.md +50 -21
- package/index.ts +30 -403
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
> Requires pi >= 0.74 · Node >= 20 · MIT · no runtime dependencies · no PiDeck or third-party patching
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## What it solves
|
|
6
|
+
|
|
7
|
+
Three configuration failures happen silently: the codemode switch disagreeing with the installed guard, a force-type extension sitting mid-list (dropping every later section), and an injected manual that is older than the one on disk. None of them error — behaviour just drifts. This extension checks at startup and fixes what it can.
|
|
6
8
|
|
|
7
9
|
## Features
|
|
8
10
|
|
|
9
11
|
- **codemode single-source check**: disabling `builtin:codemode` and installing `pi-codemode-guard` must agree.
|
|
10
|
-
- **force-order guard**: force-type extensions
|
|
11
|
-
- **Environment fingerprint drift**: notifies when `settings.json`
|
|
12
|
-
- **Injection alignment**: compares
|
|
13
|
-
- Commands `/patch-keeper`
|
|
12
|
+
- **force-order guard**: force-type extensions must sit last in `packages`; wrong order is fixed automatically (with a backup).
|
|
13
|
+
- **Environment fingerprint drift**: notifies when key `settings.json` fields change, since manuals or prior verification may be stale.
|
|
14
|
+
- **Injection alignment**: compares injected `APPEND_SYSTEM.md` version with disk and tells the model to trust disk.
|
|
15
|
+
- Commands `/patch-keeper` and `/patch-keeper fix`.
|
|
14
16
|
|
|
15
17
|
## Install
|
|
16
18
|
|
|
@@ -21,36 +23,63 @@ pi install ./pi-patch-keeper
|
|
|
21
23
|
|
|
22
24
|
## Configuration
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
### 1 · Config file
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
| Manual sentinel | `~/.pi/patch-keeper-manual.json` | Manual version / content signature, catches "content changed without a version bump" |
|
|
30
|
-
| Fix backup | `~/.pi/agent/settings.json.bak-force-order-*` | Backup created before repairing force order |
|
|
28
|
+
**None** — it only reads `settings.json` and the manual.
|
|
29
|
+
|
|
30
|
+
### 2 · Environment
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
None.
|
|
33
|
+
|
|
34
|
+
### 3 · Commands
|
|
33
35
|
|
|
34
36
|
| Command | Purpose |
|
|
35
37
|
| --- | --- |
|
|
36
|
-
| `/patch-keeper` |
|
|
38
|
+
| `/patch-keeper` | codemode / force / fingerprint / injection-alignment status |
|
|
37
39
|
| `/patch-keeper fix` | Repair force order in `packages` (with backup) |
|
|
38
40
|
|
|
39
|
-
###
|
|
41
|
+
### 4 · State files (self-maintained)
|
|
42
|
+
|
|
43
|
+
| Item | Path | Description |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| Fingerprint | `~/.pi/patch-keeper.json` | Settings snapshot for drift detection |
|
|
46
|
+
| Manual sentinel | `~/.pi/patch-keeper-manual.json` | Manual version / content signature |
|
|
47
|
+
| Fix backup | `~/.pi/agent/settings.json.bak-force-order-*` | Backup before repair |
|
|
48
|
+
|
|
49
|
+
### 5 · Not included (local module)
|
|
50
|
+
|
|
51
|
+
PiDeck builtin patch restore, read-only asar checks, pi-lens / pi-memory / pi-tactician source patches, local snapshot restore — all depend on the PiDeck install directory, third-party internals or local paths, so they are **not shipped**; a local module `patch-keeper-local` provides them (`/patch-keeper-local`).
|
|
52
|
+
|
|
53
|
+
## Where it acts (prompt and config paths)
|
|
54
|
+
|
|
55
|
+
| Stage | Does |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| factory (startup) | Fingerprint compare, force self-check and auto-repair; failures only notify |
|
|
58
|
+
| `session_start` | Summarises drift / order / manual drift in one notification |
|
|
59
|
+
| `before_agent_start` | Compares injected manual version with disk (**read-only**, never rewrites the prompt) |
|
|
60
|
+
|
|
61
|
+
It never touches thinking or tool calls: it acts on the **system prompt and configuration state**.
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
## Feedback path
|
|
64
|
+
|
|
65
|
+
| Item | Path | Why |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| Injection-alignment hint | User notification + trailing message `nextTurn`, `display:false` | The user already knows; the model must too, otherwise "use the disk version" stops at the user side |
|
|
68
|
+
| Alignment text | "injected vX != disk vY -> injected is not evidence; action: read disk and continue" | Gives both criterion and action |
|
|
69
|
+
| Never returns systemPrompt | Only its own section / message | Returning it replaces the whole prompt (`forceSystemPrompt`) and silently drops every later section |
|
|
70
|
+
| Drift / force problems | Notification only (once per `session_start`) | Low-frequency information |
|
|
42
71
|
|
|
43
72
|
## How it works
|
|
44
73
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
74
|
+
- All writes are atomic (temp file + rename); a backup precedes any repair.
|
|
75
|
+
- Force repair moves known force-type packages to the end, preserving the rest.
|
|
76
|
+
- Manual drift uses version + content signature, so unversioned content edits are caught.
|
|
48
77
|
|
|
49
78
|
## Limitations
|
|
50
79
|
|
|
51
|
-
- Covers pi's own configuration only
|
|
52
|
-
- Force
|
|
53
|
-
-
|
|
80
|
+
- Covers pi's own configuration only.
|
|
81
|
+
- Force repair relies on a built-in known-force list.
|
|
82
|
+
- Drift reports what changed, not whether the new value is right.
|
|
54
83
|
|
|
55
84
|
## License
|
|
56
85
|
|
package/README.md
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
> 适用 pi ≥ 0.74 · Node ≥ 20 · MIT · 无运行时依赖 · 不含 PiDeck / 第三方补丁
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 它解决什么问题
|
|
6
|
+
|
|
7
|
+
pi 的配置有三类静默失效:codemode 开关与插件不一致、force 型扩展排在中途导致其后 section 被丢弃、注入的手册与磁盘版本不同步。它们都不会报错,只会让行为悄悄偏离。本扩展在启动时检查并在能修时自动修。
|
|
6
8
|
|
|
7
9
|
## 功能
|
|
8
10
|
|
|
9
11
|
- **codemode 单点一致性**:禁 `builtin:codemode` 与安装 `pi-codemode-guard` 必须同时成立。
|
|
10
|
-
- **force 顺序守卫**:force
|
|
11
|
-
- **环境指纹漂移**:`settings.json`
|
|
12
|
-
- **注入对齐**:比对注入的 `APPEND_SYSTEM.md`
|
|
13
|
-
- 命令 `/patch-keeper`
|
|
12
|
+
- **force 顺序守卫**:force 型扩展必须排在 `packages` 末尾,乱序自动修复(先备份)。
|
|
13
|
+
- **环境指纹漂移**:`settings.json` 的关键字段变化时提示「手册或验证结论可能过期」。
|
|
14
|
+
- **注入对齐**:比对注入的 `APPEND_SYSTEM.md` 版本与磁盘版本,不一致时按磁盘版执行。
|
|
15
|
+
- 命令 `/patch-keeper` / `/patch-keeper fix`。
|
|
14
16
|
|
|
15
17
|
## 安装
|
|
16
18
|
|
|
@@ -21,36 +23,63 @@ pi install ./pi-patch-keeper
|
|
|
21
23
|
|
|
22
24
|
## 配置
|
|
23
25
|
|
|
24
|
-
|
|
26
|
+
### 1 · 配置文件
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
| 手册哨兵 | `~/.pi/patch-keeper-manual.json` | 手册版本 / 内容签名,用于检测「改了内容没升版本」 |
|
|
30
|
-
| 自动修复备份 | `~/.pi/agent/settings.json.bak-force-order-*` | 修复 force 顺序前的备份 |
|
|
28
|
+
**无配置文件**:本包只读 `settings.json` 与手册,不引入自己的键。
|
|
29
|
+
|
|
30
|
+
### 2 · 环境变量
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
无。
|
|
33
|
+
|
|
34
|
+
### 3 · 命令
|
|
33
35
|
|
|
34
36
|
| 命令 | 作用 |
|
|
35
37
|
| --- | --- |
|
|
36
|
-
| `/patch-keeper` |
|
|
38
|
+
| `/patch-keeper` | codemode / force / 指纹 / 注入对齐状态 |
|
|
37
39
|
| `/patch-keeper fix` | 修复 `packages` 的 force 顺序(带备份) |
|
|
38
40
|
|
|
39
|
-
###
|
|
41
|
+
### 4 · 状态文件(自动维护)
|
|
42
|
+
|
|
43
|
+
| 项 | 路径 | 说明 |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
| 环境指纹 | `~/.pi/patch-keeper.json` | 上次启动的配置快照,用于漂移对比 |
|
|
46
|
+
| 手册哨兵 | `~/.pi/patch-keeper-manual.json` | 手册版本 / 内容签名,抓「改了内容没升版本」 |
|
|
47
|
+
| 修复备份 | `~/.pi/agent/settings.json.bak-force-order-*` | 修复前备份 |
|
|
48
|
+
|
|
49
|
+
### 5 · 不包含(本机模块)
|
|
50
|
+
|
|
51
|
+
PiDeck builtin 补丁双份恢复、asar 只读检测、pi-lens / pi-memory / pi-tactician 源码补丁、本机快照恢复 —— 依赖 PiDeck 安装目录、第三方包内部实现与本机路径,**不随本包发布**;由本机模块 `patch-keeper-local` 提供(`/patch-keeper-local`)。
|
|
52
|
+
|
|
53
|
+
## 作用点(配置与提示通路)
|
|
54
|
+
|
|
55
|
+
| 阶段 | 做什么 |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| factory(启动) | 指纹对比、force 自检与自动修复;失败只提示 |
|
|
58
|
+
| `session_start` | 汇总漂移 / 顺序 / 手册漂移,一次通知 |
|
|
59
|
+
| `before_agent_start` | 比对注入手册版本与磁盘版本(**只读**,不改 prompt) |
|
|
60
|
+
|
|
61
|
+
它不碰 thinking,也不碰工具调用:作用的是**系统提示与配置状态**。
|
|
40
62
|
|
|
41
|
-
|
|
63
|
+
## 回灌(反馈回到模型的通路)
|
|
64
|
+
|
|
65
|
+
| 项 | 通路 | 设计理由 |
|
|
66
|
+
| --- | --- | --- |
|
|
67
|
+
| 注入对齐提示 | 通知用户 + 尾部消息 `nextTurn`、`display:false` | 用户已收到通知,模型也必须知道,否则「以磁盘版执行」只停在用户侧 |
|
|
68
|
+
| 注入对齐文本 | 「注入 vX ≠ 磁盘 vY → 注入版不可作判断依据;动作:读回全文后执行」 | 给出判据与动作,而不是只报事实 |
|
|
69
|
+
| 绝不返回 systemPrompt | 只写自己的 section / 消息 | 返回即 forceSystemPrompt **覆盖式替换**,会静默丢弃其后所有 section 的修改 |
|
|
70
|
+
| 指纹漂移 / force 问题 | 仅通知(`session_start` 一次) | 低频信息,不需要每轮打扰 |
|
|
42
71
|
|
|
43
72
|
## 机制
|
|
44
73
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
74
|
+
- 所有写盘走「临时文件 + rename」原子写;修复前留备份。
|
|
75
|
+
- force 顺序修复只移动已知 force 型包到末尾,其余相对顺序不变。
|
|
76
|
+
- 手册漂移用「版本号 + 内容签名」双判据,避免只看版本号漏掉未登记的正文改动。
|
|
48
77
|
|
|
49
78
|
## 限制
|
|
50
79
|
|
|
51
|
-
- 只覆盖 pi
|
|
52
|
-
- force
|
|
53
|
-
-
|
|
80
|
+
- 只覆盖 pi 自身配置,不检查第三方插件行为。
|
|
81
|
+
- force 修复依赖内置的 known-force 列表(随版本更新)。
|
|
82
|
+
- 指纹只报「变了什么」,不判断新值是否正确。
|
|
54
83
|
|
|
55
84
|
## License
|
|
56
85
|
|
package/index.ts
CHANGED
|
@@ -1,65 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* patch-keeper
|
|
2
|
+
* patch-keeper(发布版 · 纯 pi)—— 只看 pi 自身配置:不碰 PiDeck、不改第三方源码、不读本机快照。
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* PiDeck app.asar ×3(只检测)、npm 包 ×2(pi-lens / pi-memory,自愈)、PiDeck builtin ×5(双份自愈)、
|
|
7
|
-
* npm 包注入通道 ×2(pi-memory / tactician 改 section)。
|
|
8
|
-
* 上游一升级就覆盖,得重跑桌面上的补丁脚本 —— 容易忘。这个扩展把它变成自愈:
|
|
9
|
-
* 每次 pi 启动时检查一遍,能修的直接修,不能修的(PiDeck 正开着,asar 被锁)明确报出来。
|
|
10
|
-
*
|
|
11
|
-
* ## 为什么能修 pi-lens
|
|
12
|
-
* pi 的资源加载有优先级(dist/core/package-manager.js resourcePrecedenceRank):
|
|
13
|
-
* 0/1 project · 2/3 user(本扩展)· 4 package(pi-lens)· 5 builtin
|
|
14
|
-
* 本扩展是「user 扩展」= rank 2,pi-lens 是「package 扩展」= rank 4,**先加载**。
|
|
15
|
-
* 所以在 factory 里同步改 dist/index.js,pi-lens 随后 import 到的就是打过补丁的内容 —— 当轮生效。
|
|
16
|
-
*
|
|
17
|
-
* ## 边界(刻意的)
|
|
18
|
-
* - PiDeck asar:**只检测不修**。PiDeck 正开着,文件被锁;且插件无权碰 Electron 主进程。
|
|
19
|
-
* 检测到未打补丁 → 提示跑 `run-all.cmd` / `launch-pideck.cmd`(桌面 PI脚本补丁/ 不在时用快照副本 05-脚本补丁/)。
|
|
20
|
-
* - 任何失败都只 notify,不抛错、不影响会话(纪律:不合法就跳过 + 记日志,绝不崩溃)。
|
|
4
|
+
* 职责:codemode 单点一致性 · force 顺序守卫(可自动修复)· 环境指纹漂移 · 注入对齐(手册版本)。
|
|
5
|
+
* 依赖 PiDeck / 第三方包源码 / 本机快照的部分在 patch-keeper-local(不发布)。
|
|
21
6
|
*/
|
|
7
|
+
|
|
22
8
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
23
|
-
|
|
24
|
-
import {
|
|
9
|
+
|
|
10
|
+
import { existsSync, readFileSync, writeFileSync, copyFileSync, renameSync } from "node:fs";
|
|
11
|
+
|
|
25
12
|
import * as os from "node:os";
|
|
13
|
+
|
|
26
14
|
import * as path from "node:path";
|
|
27
15
|
|
|
28
|
-
// 路径从 home / %LOCALAPPDATA% 推导(不写死用户名:换机 / 换用户名 / 复刻包到别的机器仍可用)
|
|
29
16
|
const HOME = os.homedir();
|
|
30
|
-
|
|
31
|
-
const PI_LENS = path.join(HOME, ".pi", "agent", "npm", "node_modules", "pi-lens", "dist", "index.js");
|
|
32
|
-
const PI_MEMORY = path.join(HOME, ".pi", "agent", "npm", "node_modules", "pi-memory", "index.ts");
|
|
33
|
-
const ASAR = path.join(LOCALAPPDATA, "Programs", "PiDeck", "resources", "app.asar");
|
|
34
|
-
// 唯一处置入口:快照 05-脚本补丁/(桌面旧目录 PI脚本补丁/ 已不存在,2026-10-15 核实)
|
|
35
|
-
const SNAPSHOT_PATCH_DIR = path.join(HOME, "Desktop", "pi-env-snapshot", "05-脚本补丁");
|
|
36
|
-
// 自建扩展保护:发布精简版覆盖自愈版时(2026-10-18 实证)从快照恢复
|
|
37
|
-
const SELF_EXT = path.join(HOME, ".pi", "agent", "extensions", "patch-keeper", "index.ts");
|
|
38
|
-
const SELF_SNAP = path.join(HOME, "Desktop", "pi-env-snapshot", "01-pi核心配置", "extensions", "patch-keeper", "index.ts");
|
|
39
|
-
const SELF_MARK = "SNAPSHOT_PATCH_DIR"; // 自愈版特征:发布精简版没有这行
|
|
40
|
-
// 环境指纹文件(settings 关键字段,用于漂移检测)
|
|
17
|
+
|
|
41
18
|
const FINGERPRINT = path.join(HOME, ".pi", "patch-keeper.json");
|
|
19
|
+
|
|
42
20
|
const SETTINGS = path.join(HOME, ".pi", "agent", "settings.json");
|
|
43
21
|
// 手册(注入对齐检测:本轮注入文本的版本行 vs 磁盘版)
|
|
22
|
+
|
|
44
23
|
const MANUAL = path.join(HOME, ".pi", "agent", "APPEND_SYSTEM.md");
|
|
45
24
|
|
|
46
|
-
|
|
47
|
-
const PIDECK_BUILTIN = path.join(HOME, "AppData", "Roaming", "PiDeck", "builtin-extensions");
|
|
48
|
-
const PIDECK_INSTALL = path.join(process.env.LOCALAPPDATA ?? path.join(HOME, "AppData", "Local"), "Programs", "PiDeck", "resources", "extensions");
|
|
49
|
-
// needle = 本机修复特征串(不是 section 名):命中即视为已打;升级覆盖后从快照恢复。
|
|
50
|
-
const BUILTIN_PATCHES: Array<{ file: string; install: string; needle: string; name: string }> = [
|
|
51
|
-
{ file: path.join(PIDECK_BUILTIN, "pi-deck-security-gate.ts"), install: path.join(PIDECK_INSTALL, "pi-deck-security-gate.ts"), needle: "越界操作先说明理由", name: "security-gate" },
|
|
52
|
-
{ file: path.join(PIDECK_BUILTIN, "pi-deck-plan-mode.ts"), install: path.join(PIDECK_INSTALL, "pi-deck-plan-mode.ts"), needle: "keepThirdParty", name: "plan-mode" },
|
|
53
|
-
{ file: path.join(PIDECK_BUILTIN, "pi-deck-goal-mode.ts"), install: path.join(PIDECK_INSTALL, "pi-deck-goal-mode.ts"), needle: "roundLine", name: "goal-mode" },
|
|
54
|
-
{ file: path.join(PIDECK_BUILTIN, "pi-deck-trash-guard.ts"), install: path.join(PIDECK_INSTALL, "pi-deck-trash-guard.ts"), needle: "deleteFlagStyle", name: "trash-guard" },
|
|
55
|
-
{ file: path.join(PIDECK_BUILTIN, "pi-deck-session-title.ts"), install: path.join(PIDECK_INSTALL, "pi-deck-session-title.ts"), needle: "TITLE_SOURCE_MARKER_HEAD", name: "session-title" },
|
|
56
|
-
];
|
|
57
|
-
const SNAPSHOT_BUILTIN = path.join(HOME, "Desktop", "pi-env-snapshot", "03-PiDeck", "builtin-extensions");
|
|
25
|
+
|
|
58
26
|
const MANUAL_STATE = path.join(HOME, ".pi", "patch-keeper-manual.json");
|
|
27
|
+
|
|
59
28
|
/**
|
|
60
29
|
* 从手册文本里取版本号(标题「自主工作手册」后 200 字符内的第一个 vNN.NN)。
|
|
61
30
|
* 取不到返回 null(注入文本若不含标题即静默跳过,绝不猜)。
|
|
62
31
|
*/
|
|
32
|
+
|
|
63
33
|
function manualVer(text: string): string | null {
|
|
64
34
|
const i = text.indexOf("自主工作手册");
|
|
65
35
|
if (i < 0) return null;
|
|
@@ -67,126 +37,10 @@ function manualVer(text: string): string | null {
|
|
|
67
37
|
return m ? m[1] : null;
|
|
68
38
|
}
|
|
69
39
|
|
|
70
|
-
// ── pi-lens word index 补丁:两处替换(与 patches/pi-lens-wordindex-patch.js 同源)
|
|
71
|
-
const A1_FROM = " warmWordIndexes.set(key4, entry);\n }\n entry.leases++;";
|
|
72
|
-
const A1_MARK = "if (entry && !entry.index) {";
|
|
73
|
-
const A1_TO = " warmWordIndexes.set(key4, entry);\n }\n if (entry && !entry.index) {\n const late = deserializeWordIndex(loadProjectSnapshot(key4)?.wordIndex) ?? void 0;\n if (late?.forward) entry.index = late;\n }\n entry.leases++;";
|
|
74
|
-
const A2_FROM = "if (!snapshot.wordIndex && existing.wordIndex && existing.seq === snapshot.seq && (snapshot.incomplete || !existing.incomplete)) {";
|
|
75
|
-
const A2_MARK = "existing.wordIndex && (snapshot.incomplete";
|
|
76
|
-
const A2_TO = "if (!snapshot.wordIndex && existing.wordIndex && (snapshot.incomplete || !existing.incomplete)) {";
|
|
77
|
-
// 上游修复标记(#4583 / PR #4586):build 完成即推送 warm cache;出现即停打 A 补丁(与 patches/pi-lens-wordindex-patch.js 同源)
|
|
78
|
-
const UPSTREAM_MARK = "warm_cache_adopted";
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// ── pi-memory exit summary 空行规范化补丁(幂等;与 pi-memory/index.ts 内已打状态一致)
|
|
82
|
-
const ME_MARK = "normalizeExitSummaryMarkdown";
|
|
83
|
-
const ME_FROM_FN = "function formatExitSummaryEntry(";
|
|
84
|
-
const ME_FROM_RET = 'header, "", summary.trim()].join("\\n");';
|
|
85
|
-
const ME_TO_RET = 'header, "", normalizeExitSummaryMarkdown(summary.trim())].join("\\n");';
|
|
86
|
-
const ME_FN = ['/**', ' * 规范 exit summary 的块级空行(MD022 / MD032:标题 / 列表前后须空行);代码围栏内不动。', ' */', 'function normalizeExitSummaryMarkdown(text: string): string {', '\tconst lines = text.split(/\\r?\\n/);', '\tconst out: string[] = [];', '\tconst isFence = (l: string) => /^\\s*(\\x60{3,}|~{3,})/.test(l);', '\tconst isHeading = (l: string) => /^#{1,6}\\s/.test(l);', '\tconst isList = (l: string) => /^\\s*(?:[-*+]|\\d+\\.)\\s/.test(l);', '\tlet inFence = false;', '\tfor (const line of lines) {', '\t\tconst fenceLine = isFence(line);', '\t\tif (fenceLine) inFence = !inFence;', '\t\tconst prev = out.length ? out[out.length - 1] : "";', '\t\tif (!fenceLine && !inFence && prev !== "") {', '\t\t\tconst startsBlock = (isHeading(line) || isList(line)) && !(isList(line) && isList(prev));', '\t\t\tconst afterBlock = isHeading(prev) || (isList(prev) && !isList(line));', '\t\t\tif (startsBlock || afterBlock) out.push("");', '\t\t}', '\t\tout.push(line);', '\t}', '\treturn out.join("\\n");', '}'].join("\n");
|
|
87
|
-
const MEM_SECTION_MARK = 'sections["pi_memory"]';
|
|
88
|
-
const MEM_SECTION_ANCHOR = [
|
|
89
|
-
"\t\treturn {",
|
|
90
|
-
"\t\t\tsystemPrompt: event.systemPrompt + headerLines.join(\"\\n\"),",
|
|
91
|
-
"\t\t};",
|
|
92
|
-
].join("\n");
|
|
93
|
-
const MEM_SECTION_BLOCK = [
|
|
94
|
-
"\t\t// R7:有 sections 的宿主优先写 section —— 返回 systemPrompt 是 force 覆盖,",
|
|
95
|
-
"\t\t// 会丢弃其后(builtin 组)goal / plan / security 的 section 写入",
|
|
96
|
-
"\t\t// (2026-10-17 实证:goal 的 Objective 与 Rules 从 system 消失,只剩 Round 消息)。",
|
|
97
|
-
"\t\tconst sections = (event as { systemPromptOptions?: { sections?: Record<string, string> } }).systemPromptOptions?.sections;",
|
|
98
|
-
"\t\tif (sections) {",
|
|
99
|
-
"\t\t\tsections[\"pi_memory\"] = headerLines.join(\"\\n\").replace(/^\\n+/, \"\");",
|
|
100
|
-
"\t\t\treturn;",
|
|
101
|
-
"\t\t}",
|
|
102
|
-
"\t\treturn {",
|
|
103
|
-
"\t\t\tsystemPrompt: event.systemPrompt + headerLines.join(\"\\n\"),",
|
|
104
|
-
"\t\t};",
|
|
105
|
-
].join("\n");
|
|
106
|
-
const TACT = path.join(HOME, ".pi", "agent", "npm", "node_modules", "@khanhicetea", "pi-tactician", "src", "index.ts");
|
|
107
|
-
const TACT_MARK = 'sections["tactician"]';
|
|
108
|
-
const TACT_ANCHOR = [
|
|
109
|
-
"\t\tif (event.systemPrompt.includes(GUIDANCE_MARKER)) return;",
|
|
110
|
-
"\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${TACTICIAN_GUIDANCE}` };",
|
|
111
|
-
].join("\n");
|
|
112
|
-
const TACT_BLOCK = [
|
|
113
|
-
"\t\t// R7b:有 sections 的宿主写 section(不 force)——force 会让其后 builtin 组",
|
|
114
|
-
"\t\t// (goal / plan / security)的 section 写入被整组丢弃(2026-10-17 实证)。",
|
|
115
|
-
"\t\tconst sections = (event as { systemPromptOptions?: { sections?: Record<string, string> } }).systemPromptOptions?.sections;",
|
|
116
|
-
"\t\tif (sections) {",
|
|
117
|
-
"\t\t\tsections[\"tactician\"] = TACTICIAN_GUIDANCE;",
|
|
118
|
-
"\t\t\treturn;",
|
|
119
|
-
"\t\t}",
|
|
120
|
-
"\t\tif (event.systemPrompt.includes(GUIDANCE_MARKER)) return;",
|
|
121
|
-
"\t\treturn { systemPrompt: `${event.systemPrompt}\\n\\n${TACTICIAN_GUIDANCE}` };",
|
|
122
|
-
].join("\n");
|
|
123
|
-
|
|
124
|
-
/** pi-memory 状态:done / none。 */
|
|
125
|
-
function piMemoryState(src: string): "done" | "none" {
|
|
126
|
-
return src.includes(ME_MARK) ? "done" : "none";
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/** 打/补 pi-memory exit summary 规范化(幂等)。 */
|
|
130
|
-
function healPiMemory(): Result {
|
|
131
|
-
if (!existsSync(PI_MEMORY)) return { ok: false, detail: "pi-memory/index.ts 不存在(未安装?)" };
|
|
132
|
-
const src = safeRead(PI_MEMORY);
|
|
133
|
-
if (src === null) return { ok: false, detail: "读取失败" };
|
|
134
|
-
if (piMemoryState(src) === "done") return { ok: true, detail: "已是最新(无需动作)" };
|
|
135
|
-
if (!src.includes(ME_FROM_FN) || !src.includes(ME_FROM_RET)) return { ok: false, detail: "锚点未找到(版本变化?)—— 需人工确认" };
|
|
136
|
-
let next = src.replace(ME_FROM_RET, ME_TO_RET);
|
|
137
|
-
next = next.replace(ME_FROM_FN, ME_FN + "\n\n" + ME_FROM_FN);
|
|
138
|
-
if (!next.includes(ME_MARK) || next === src) return { ok: false, detail: "打补丁失败(结果校验不通过)" };
|
|
139
|
-
try {
|
|
140
|
-
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
141
|
-
const bak = `${PI_MEMORY}.bak-exitsummary-${stamp}`;
|
|
142
|
-
if (!existsSync(bak)) copyFileSync(PI_MEMORY, bak);
|
|
143
|
-
writeAtomic(PI_MEMORY, next);
|
|
144
|
-
} catch (e) { return { ok: false, detail: `写入失败:${(e as Error).message}` }; }
|
|
145
|
-
return { ok: true, detail: "已自动修补(exit summary 空行规范化)—— 本轮即生效" };
|
|
146
|
-
}
|
|
147
|
-
/** R7:memory 段改 section(防 force 吞掉其后 builtin 组的 section)。 */
|
|
148
|
-
function healMemorySection(): Result {
|
|
149
|
-
if (!existsSync(PI_MEMORY)) return { ok: false, detail: "pi-memory/index.ts 不存在(未安装?)" };
|
|
150
|
-
const src = safeRead(PI_MEMORY);
|
|
151
|
-
if (src === null) return { ok: false, detail: "读取失败" };
|
|
152
|
-
if (src.includes(MEM_SECTION_MARK)) return { ok: true, detail: "已是最新(无需动作)" };
|
|
153
|
-
if (!src.includes(MEM_SECTION_ANCHOR)) return { ok: false, detail: "锚点未找到(版本变化?)—— 需人工确认" };
|
|
154
|
-
const next = src.replace(MEM_SECTION_ANCHOR, MEM_SECTION_BLOCK);
|
|
155
|
-
if (!next.includes(MEM_SECTION_MARK) || next === src) return { ok: false, detail: "打补丁失败(结果校验不通过)" };
|
|
156
|
-
try {
|
|
157
|
-
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
158
|
-
const bak = `${PI_MEMORY}.bak-memsec-${stamp}`;
|
|
159
|
-
if (!existsSync(bak)) copyFileSync(PI_MEMORY, bak);
|
|
160
|
-
writeAtomic(PI_MEMORY, next);
|
|
161
|
-
} catch (e) { return { ok: false, detail: `写入失败:${(e as Error).message}` }; }
|
|
162
|
-
return { ok: true, detail: "已自动修补(memory 段改 section)—— 重启后生效" };
|
|
163
|
-
}
|
|
164
|
-
/** R7b:tactician 改 section(防 force 吞掉其后 builtin 组的 section)。 */
|
|
165
|
-
function healTactician(): Result {
|
|
166
|
-
if (!existsSync(TACT)) return { ok: false, detail: "pi-tactician/src/index.ts 不存在(未安装?)" };
|
|
167
|
-
const src = safeRead(TACT);
|
|
168
|
-
if (src === null) return { ok: false, detail: "读取失败" };
|
|
169
|
-
if (src.includes(TACT_MARK)) return { ok: true, detail: "已是最新(无需动作)" };
|
|
170
|
-
if (!src.includes(TACT_ANCHOR)) return { ok: false, detail: "锚点未找到(版本变化?)—— 需人工确认" };
|
|
171
|
-
const next = src.replace(TACT_ANCHOR, TACT_BLOCK);
|
|
172
|
-
if (!next.includes(TACT_MARK) || next === src) return { ok: false, detail: "打补丁失败(结果校验不通过)" };
|
|
173
|
-
try {
|
|
174
|
-
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
175
|
-
const bak = `${TACT}.bak-tactician-${stamp}`;
|
|
176
|
-
if (!existsSync(bak)) copyFileSync(TACT, bak);
|
|
177
|
-
writeAtomic(TACT, next);
|
|
178
|
-
} catch (e) { return { ok: false, detail: `写入失败:${(e as Error).message}` }; }
|
|
179
|
-
return { ok: true, detail: "已自动修补(tactician 改 section)—— 重启后生效" };
|
|
180
|
-
}
|
|
181
|
-
// ── PiDeck asar 补丁:只读检测(needle 出现次数)
|
|
182
|
-
const ASAR_CHECKS: Array<{ name: string; needle: string; expected: number }> = [
|
|
183
|
-
{ name: "history-race", needle: "runtime.tab.runtimeGeneration !== runtimeGeneration", expected: 8 },
|
|
184
|
-
{ name: "katex", needle: "singleDollarTextMath: !0", expected: 0 },
|
|
185
|
-
{ name: "noise", needle: "appLogger.warn(\"app\", \"Main window renderer console error\"", expected: 0 },
|
|
186
|
-
];
|
|
187
40
|
|
|
188
41
|
type Result = { ok: boolean; detail: string };
|
|
189
42
|
|
|
43
|
+
|
|
190
44
|
/** 读文件;失败返回 null(永不抛)。 */
|
|
191
45
|
function safeRead(p: string): string | null {
|
|
192
46
|
try { return readFileSync(p, "utf8"); } catch { return null; }
|
|
@@ -206,105 +60,7 @@ function count(hay: string, needle: string): number {
|
|
|
206
60
|
return n;
|
|
207
61
|
}
|
|
208
62
|
|
|
209
|
-
/** pi-lens 状态:both / partial / none / missing。 */
|
|
210
|
-
export function piLensState(src: string): { state: "upstream" | "both" | "partial" | "none"; a1: boolean; a2: boolean } {
|
|
211
|
-
const upstream = src.includes(UPSTREAM_MARK); // 上游已修 #4583:A 无需本机补丁
|
|
212
|
-
const a1 = src.includes(A1_MARK);
|
|
213
|
-
const a2 = !src.includes(A2_FROM) && src.includes(A2_MARK);
|
|
214
|
-
let state: "upstream" | "both" | "partial" | "none" = "none";
|
|
215
|
-
if (upstream) state = "upstream";
|
|
216
|
-
else if (a1 && a2) state = "both";
|
|
217
|
-
else if (a1 || a2) state = "partial";
|
|
218
|
-
return { state, a1, a2 };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
/** 只在未打或半打时修补 pi-lens(幂等)。 */
|
|
222
|
-
function healPiLens(): Result {
|
|
223
|
-
if (!existsSync(PI_LENS)) return { ok: false, detail: "pi-lens dist 不存在(未安装?)" };
|
|
224
|
-
const src = safeRead(PI_LENS);
|
|
225
|
-
if (src === null) return { ok: false, detail: "读取失败" };
|
|
226
|
-
const { state, a1, a2 } = piLensState(src);
|
|
227
|
-
if (state === "both") return { ok: true, detail: "已是最新(无需动作)" };
|
|
228
|
-
if (state === "upstream" && a2) return { ok: true, detail: "上游已修复 #4583(B 已打,无需动作)" };
|
|
229
|
-
let next = src;
|
|
230
|
-
// 半打状态先还原,再整体重打(避免只打一半造成新的不一致)
|
|
231
|
-
if (a2 && !a1) next = next.replace(A2_TO, A2_FROM); // 半打:先把 B 还原,再由下面两步整体重打
|
|
232
|
-
if (state !== "upstream" && !next.includes(A1_MARK)) next = next.replace(A1_FROM, A1_TO);
|
|
233
|
-
if (next.includes(A2_FROM)) next = next.replace(A2_FROM, A2_TO);
|
|
234
|
-
const after = piLensState(next);
|
|
235
|
-
if (after.state !== "both" && after.state !== "upstream") return { ok: false, detail: `打补丁失败(结果 ${after.state},锚点可能已随版本变化)` };
|
|
236
|
-
try {
|
|
237
|
-
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
238
|
-
const bak = `${PI_LENS}.bak-wordindex-${stamp}`;
|
|
239
|
-
if (!existsSync(bak)) copyFileSync(PI_LENS, bak);
|
|
240
|
-
writeAtomic(PI_LENS, next);
|
|
241
|
-
} catch (e) { return { ok: false, detail: `写入失败:${(e as Error).message}` }; }
|
|
242
|
-
return { ok: true, detail: state === "upstream" ? `上游已修复 #4583 —— 跳过 A,仅补 B${a1 ? "(A 残留,建议 npm 重装清除)" : ""}` : `已自动修补(原状态 ${state})—— 本轮即生效` };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
/** 记忆文件格式规范化(压连续空行 + 单尾换行;围栏内不动)。 */
|
|
247
|
-
function normMd(txt: string): string {
|
|
248
|
-
const lines = txt.split(/\r?\n/);
|
|
249
|
-
const out: string[] = [];
|
|
250
|
-
let inFence = false;
|
|
251
|
-
for (const line of lines) {
|
|
252
|
-
const isF = /^\s*(?:`{3,}|~{3,})/.test(line);
|
|
253
|
-
if (isF) inFence = !inFence;
|
|
254
|
-
if (!inFence && line.trim() === "" && out.length > 0 && out[out.length - 1].trim() === "") continue;
|
|
255
|
-
out.push(line);
|
|
256
|
-
}
|
|
257
|
-
while (out.length > 0 && out[out.length - 1].trim() === "") out.pop();
|
|
258
|
-
return out.join("\n") + "\n";
|
|
259
|
-
}
|
|
260
63
|
|
|
261
|
-
/** 第 3 处自愈:memory_write 每次写入都会引入 MD012/MD047(同日复发 3 次后机制化)——启动即规范。 */
|
|
262
|
-
function healMemoryFormat(): Result {
|
|
263
|
-
try {
|
|
264
|
-
const dir = path.join(HOME, ".pi", "agent", "memory");
|
|
265
|
-
if (!existsSync(dir)) return { ok: true, detail: "memory 目录不存在(跳过)" };
|
|
266
|
-
let fixed = 0;
|
|
267
|
-
const walk = (d: string): void => {
|
|
268
|
-
for (const e of readdirSync(d, { withFileTypes: true })) {
|
|
269
|
-
const p = path.join(d, e.name);
|
|
270
|
-
if (e.isDirectory()) { if (e.name !== ".git") walk(p); continue; }
|
|
271
|
-
if (!e.name.endsWith(".md")) continue;
|
|
272
|
-
const txt = safeRead(p);
|
|
273
|
-
if (txt === null) continue;
|
|
274
|
-
const norm = normMd(txt);
|
|
275
|
-
if (norm !== txt) { try { writeAtomic(p, norm); fixed += 1; } catch { /* 静默 */ } }
|
|
276
|
-
}
|
|
277
|
-
};
|
|
278
|
-
walk(dir);
|
|
279
|
-
return { ok: true, detail: fixed > 0 ? `已规范化 ${fixed} 个记忆文件(空行 / 尾换行)` : "记忆格式无需动作" };
|
|
280
|
-
} catch (e) { return { ok: false, detail: (e as Error).message }; }
|
|
281
|
-
}
|
|
282
|
-
/** PiDeck asar:只读检测,返回未打补丁的项名。 */
|
|
283
|
-
let asarOk = false;
|
|
284
|
-
/** 未打补丁时的处置提示(纯函数,便于单测)。 */
|
|
285
|
-
function patchTip(lines: string[]): string {
|
|
286
|
-
if (!lines.some((l) => l.includes("未打") || l.includes("半打"))) return "";
|
|
287
|
-
const dir = existsSync(SNAPSHOT_PATCH_DIR) ? SNAPSHOT_PATCH_DIR : "pi-env-snapshot/05-脚本补丁/(也未找到)";
|
|
288
|
-
return ` → PiDeck 补丁需退出 PiDeck 后重跑补丁脚本(快照副本:${dir};或用其中的 launch-pideck.cmd 启动 PiDeck 时自动重打)`;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
function checkAsar(): { ran: boolean; missing: string[]; detail: string } {
|
|
293
|
-
if (asarOk) return { ran: true, missing: [], detail: "三个补丁都在" };
|
|
294
|
-
if (!existsSync(ASAR)) return { ran: false, missing: [], detail: "app.asar 不存在" };
|
|
295
|
-
let buf: Buffer;
|
|
296
|
-
try { buf = readFileSync(ASAR); } catch (e) { return { ran: false, missing: [], detail: `读取失败:${(e as NodeJS.ErrnoException).code ?? (e as Error).message}` }; }
|
|
297
|
-
const txt = buf.toString("utf8");
|
|
298
|
-
const missing: string[] = [];
|
|
299
|
-
for (const c of ASAR_CHECKS) {
|
|
300
|
-
const n = count(txt, c.needle);
|
|
301
|
-
if (n !== c.expected) missing.push(`${c.name}(=${n}, 期望 ${c.expected})`);
|
|
302
|
-
}
|
|
303
|
-
if (!missing.length) asarOk = true; // 只在通过时缓存:缺失时每次都实测,便于打完补丁后立刻复检
|
|
304
|
-
return { ran: true, missing, detail: missing.length ? `未打补丁:${missing.join(" ")}` : "三个补丁都在" };
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/** 汇总报告(给命令与启动提示共用)。 */
|
|
308
64
|
// ── 环境指纹:settings 关键字段漂移 → 提示「手册 / 验证结论可能过期」
|
|
309
65
|
function fingerprint(): Record<string, string[]> | null {
|
|
310
66
|
const txt = safeRead(SETTINGS);
|
|
@@ -350,24 +106,6 @@ export function consistencyCheck(exts: string[], pkgs: string[]): string[] {
|
|
|
350
106
|
}
|
|
351
107
|
|
|
352
108
|
|
|
353
|
-
function restoreBuiltinPatches(): string | null {
|
|
354
|
-
const out: string[] = [];
|
|
355
|
-
for (const bp of BUILTIN_PATCHES) {
|
|
356
|
-
for (const target of [bp.install, bp.file]) {
|
|
357
|
-
if (!existsSync(target)) continue; // 未装 PiDeck / 路径不同:跳过,不算缺失
|
|
358
|
-
const cur = safeRead(target);
|
|
359
|
-
if (cur !== null && cur.includes(bp.needle)) continue;
|
|
360
|
-
const snap = path.join(SNAPSHOT_BUILTIN, path.basename(target));
|
|
361
|
-
if (!existsSync(snap)) { out.push(`${bp.name}: 缺补丁且快照无副本`); continue; }
|
|
362
|
-
try {
|
|
363
|
-
if (cur !== null) copyFileSync(target, `${target}.bak-restore-${Date.now()}`);
|
|
364
|
-
copyFileSync(snap, target);
|
|
365
|
-
out.push(`${bp.name}(${target === bp.install ? "安装目录" : "用户层"}): 已从快照恢复`);
|
|
366
|
-
} catch (e) { out.push(`${bp.name}: 恢复失败(${(e as Error).message})`); }
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
return out.length ? out.join(";") : null;
|
|
370
|
-
}
|
|
371
109
|
|
|
372
110
|
/** 手册内容漂移:版本号未变但正文变了(只比版本号会漏)。 */
|
|
373
111
|
function manualDrift(): string | null {
|
|
@@ -466,121 +204,37 @@ export function scanForceFallbacks(packages: string[]): string[] {
|
|
|
466
204
|
return classifyPackages(packages).fallback;
|
|
467
205
|
}
|
|
468
206
|
|
|
469
|
-
|
|
470
|
-
function healSelfExtension(): Result {
|
|
471
|
-
if (!existsSync(SELF_EXT)) return { ok: false, detail: "本地 patch-keeper 不存在" };
|
|
472
|
-
const src = safeRead(SELF_EXT);
|
|
473
|
-
if (src === null) return { ok: false, detail: "读取失败" };
|
|
474
|
-
if (src.includes(SELF_MARK)) return { ok: true, detail: "自愈版完好(无需动作)" };
|
|
475
|
-
if (!existsSync(SELF_SNAP)) return { ok: false, detail: "疑似被发布精简版覆盖,快照缺失 —— 需人工恢复" };
|
|
476
|
-
const snap = safeRead(SELF_SNAP);
|
|
477
|
-
if (snap === null || !snap.includes(SELF_MARK)) return { ok: false, detail: "快照不含自愈版 —— 需人工确认" };
|
|
478
|
-
try {
|
|
479
|
-
const stamp = new Date().toISOString().slice(0, 10).replace(/-/g, "");
|
|
480
|
-
const bak = `${SELF_EXT}.bak-overwrite-${stamp}`;
|
|
481
|
-
if (!existsSync(bak)) copyFileSync(SELF_EXT, bak);
|
|
482
|
-
writeAtomic(SELF_EXT, snap);
|
|
483
|
-
} catch (e) { return { ok: false, detail: `恢复失败:${(e as Error).message}` }; }
|
|
484
|
-
return { ok: true, detail: "疑似被发布精简版覆盖 —— 已从快照恢复(重启后生效)" };
|
|
485
|
-
}
|
|
207
|
+
|
|
486
208
|
function report(): string[] {
|
|
487
209
|
const lines: string[] = [];
|
|
488
|
-
const lensSrc = safeRead(PI_LENS);
|
|
489
|
-
if (lensSrc === null) lines.push("pi-lens : 读不到 dist/index.js");
|
|
490
|
-
else {
|
|
491
|
-
const st = piLensState(lensSrc);
|
|
492
|
-
let label = `半打(A=${st.a1} B=${st.a2})`;
|
|
493
|
-
if (st.state === "upstream") label = `上游已修复 #4583(B=${st.a2 ? "已打" : "未打"};A 无需补丁)`;
|
|
494
|
-
else if (st.state === "both") label = "已打(A+B)";
|
|
495
|
-
else if (st.state === "none") label = "未打";
|
|
496
|
-
lines.push(`pi-lens : ${label}`);
|
|
497
|
-
}
|
|
498
|
-
const memSrc = safeRead(PI_MEMORY);
|
|
499
|
-
if (memSrc === null) lines.push("pi-memory: 读不到 index.ts");
|
|
500
|
-
else lines.push(`pi-memory: ${memSrc.includes(ME_MARK) ? "已打(exit summary 空行规范化)" : "未打"} · ${memSrc.includes(MEM_SECTION_MARK) ? "已打(section 优先)" : "未打(section 优先)"}`);
|
|
501
|
-
const tactSrc = safeRead(TACT);
|
|
502
|
-
lines.push(`tactician: ${tactSrc === null ? "读不到" : tactSrc.includes(TACT_MARK) ? "已打(section 优先)" : "未打(section 优先)"}`);
|
|
503
|
-
lines.push(`自建扩展: ${safeRead(SELF_EXT)?.includes(SELF_MARK) ? "自愈版" : "疑似被覆盖(跑 /patch-keeper fix)"}`);
|
|
504
|
-
const asar = checkAsar();
|
|
505
|
-
lines.push(`PiDeck : ${asar.detail}`);
|
|
506
|
-
for (const bp of BUILTIN_PATCHES) {
|
|
507
|
-
const st = (p: string): string => { const src = safeRead(p); return src === null ? "无" : src.includes(bp.needle) ? "已打" : "未打"; };
|
|
508
|
-
lines.push(`builtin : ${bp.name} 生效版=${st(bp.install)} 用户层=${st(bp.file)}`);
|
|
509
|
-
}
|
|
510
210
|
const fp = fingerprint();
|
|
511
211
|
const cm = fp ? consistencyCheck(fp.extensions, fp.packages) : [];
|
|
512
|
-
|
|
513
|
-
if (fp) cmLabel = cm.length ? cm[0] : "单点一致";
|
|
514
|
-
lines.push(`codemode : ${cmLabel}`);
|
|
212
|
+
lines.push("codemode : " + (fp ? (cm.length ? cm[0] : "单点一致") : "读不到 settings"));
|
|
515
213
|
const fo = fp ? forceOrderIssues(fp.packages) : [];
|
|
516
|
-
lines.push(
|
|
214
|
+
lines.push("force序 : " + (fp ? (fo.length ? fo[0] : "OK") : "读不到 settings"));
|
|
517
215
|
const fe = fp ? scanForcePackages(fp.packages) : [];
|
|
518
|
-
lines.push(
|
|
216
|
+
lines.push("forceExt: " + (fp ? (fe.length ? "疑似 " + fe.join(" / ") : "无(静态扫描)") : "读不到 settings"));
|
|
519
217
|
const fb = fp ? scanForceFallbacks(fp.packages) : [];
|
|
520
|
-
lines.push(
|
|
218
|
+
lines.push("force兜底: " + (fp ? (fb.length ? fb.join(" / ") : "无") : "读不到 settings"));
|
|
521
219
|
return lines;
|
|
522
220
|
}
|
|
523
221
|
|
|
524
222
|
export default function (pi: ExtensionAPI) {
|
|
525
|
-
// 启动自愈:factory 是同步上下文,此时 pi-lens(rank 4)尚未加载。
|
|
526
|
-
let healNote: string | null = null;
|
|
527
|
-
try {
|
|
528
|
-
const r = healPiLens();
|
|
529
|
-
if (!r.ok) healNote = `patch-keeper: pi-lens 自愈失败 —— ${r.detail}`;
|
|
530
|
-
} catch (e) { healNote = `patch-keeper: pi-lens 自愈异常 —— ${(e as Error).message}`; }
|
|
531
|
-
try {
|
|
532
|
-
const r = healPiMemory();
|
|
533
|
-
if (!r.ok) healNote = (healNote ? healNote + "\n" : "") + `patch-keeper: pi-memory 自愈失败 —— ${r.detail}`;
|
|
534
|
-
} catch (e) { healNote = (healNote ? healNote + "\n" : "") + `patch-keeper: pi-memory 自愈异常 —— ${(e as Error).message}`; }
|
|
535
|
-
try {
|
|
536
|
-
const r = healMemorySection();
|
|
537
|
-
if (!r.ok) healNote = (healNote ? healNote + "\n" : "") + `patch-keeper: pi-memory section 自愈失败 —— ${r.detail}`;
|
|
538
|
-
} catch (e) { healNote = (healNote ? healNote + "\n" : "") + `patch-keeper: pi-memory section 自愈异常 —— ${(e as Error).message}`; }
|
|
539
|
-
try {
|
|
540
|
-
const rt = healTactician();
|
|
541
|
-
if (!rt.ok) healNote = (healNote ? healNote + "\n" : "") + `patch-keeper: tactician section 自愈失败 —— ${rt.detail}`;
|
|
542
|
-
} catch (e) { healNote = (healNote ? healNote + "\n" : "") + `patch-keeper: tactician section 自愈异常 —— ${(e as Error).message}`; }
|
|
543
|
-
try {
|
|
544
|
-
const r = healSelfExtension();
|
|
545
|
-
if (!r.ok) healNote = (healNote ? healNote + "\n" : "") + `patch-keeper: 自建扩展保护失败 —— ${r.detail}`;
|
|
546
|
-
} catch (e) { healNote = (healNote ? healNote + "\n" : "") + `patch-keeper: 自建扩展保护异常 —— ${(e as Error).message}`; }
|
|
547
|
-
try {
|
|
548
|
-
const r = healMemoryFormat();
|
|
549
|
-
if (!r.ok) healNote = (healNote ? healNote + "\n" : "") + `patch-keeper: 记忆格式自愈失败 —— ${r.detail}`;
|
|
550
|
-
} catch (e) { healNote = (healNote ? healNote + "\n" : "") + `patch-keeper: 记忆格式自愈异常 —— ${(e as Error).message}`; }
|
|
551
|
-
|
|
552
223
|
pi.registerCommand("patch-keeper", {
|
|
553
|
-
description: "
|
|
224
|
+
description: "pi 配置守卫:codemode 一致性 / force 顺序 / 环境指纹 / 注入对齐",
|
|
554
225
|
handler: async (args: string, ctx: ExtensionContext) => {
|
|
555
226
|
const arg = (args || "").trim().toLowerCase();
|
|
556
227
|
if (arg === "fix") {
|
|
557
|
-
const r = healPiLens();
|
|
558
|
-
ctx.ui.notify(`pi-lens: ${r.detail}`, r.ok ? "info" : "error");
|
|
559
|
-
|
|
560
|
-
const r2 = healPiMemory();
|
|
561
|
-
ctx.ui.notify(`pi-memory: ${r2.detail}`, r2.ok ? "info" : "error");
|
|
562
|
-
const r3 = healMemorySection();
|
|
563
|
-
ctx.ui.notify(`pi-memory section: ${r3.detail}`, r3.ok ? "info" : "error");
|
|
564
|
-
const r4 = healTactician();
|
|
565
|
-
ctx.ui.notify(`tactician section: ${r4.detail}`, r4.ok ? "info" : "error");
|
|
566
|
-
const r5 = healSelfExtension();
|
|
567
|
-
ctx.ui.notify(`自建扩展: ${r5.detail}`, r5.ok ? "info" : "error");
|
|
568
228
|
const rForce = fixForceOrder();
|
|
569
|
-
|
|
570
|
-
const rb = restoreBuiltinPatches();
|
|
571
|
-
if (rb) ctx.ui.notify(`patch-keeper: ${rb}`, "warning");
|
|
229
|
+
ctx.ui.notify("patch-keeper: " + (rForce || "force 顺序无需修复"), "info");
|
|
572
230
|
return;
|
|
573
231
|
}
|
|
574
|
-
|
|
575
|
-
const tip = patchTip(lines);
|
|
576
|
-
ctx.ui.notify(`补丁状态\n${lines.join("\n")}${tip}`, "info");
|
|
232
|
+
ctx.ui.notify("配置守卫状态" + String.fromCharCode(10) + report().join(String.fromCharCode(10)), "info");
|
|
577
233
|
},
|
|
578
234
|
});
|
|
579
235
|
|
|
580
|
-
// 启动提示:只在「PiDeck 补丁缺失」时出声(pi-lens 已自动修好,不打扰)
|
|
581
236
|
pi.on("session_start", async (_event, ctx: ExtensionContext) => {
|
|
582
237
|
try {
|
|
583
|
-
const asar = checkAsar();
|
|
584
238
|
const msgs: string[] = [];
|
|
585
239
|
const drift = fingerprintDrift();
|
|
586
240
|
const forceFix = fixForceOrder();
|
|
@@ -594,35 +248,13 @@ export default function (pi: ExtensionAPI) {
|
|
|
594
248
|
const mid = fe.filter((n) => fp.packages.indexOf(n) < fp.packages.length - 1);
|
|
595
249
|
if (mid.length) msgs.push("patch-keeper: 疑似 force 型扩展不在末尾(其后的 section 可能被静默丢弃):" + mid.join(" / ") + " —— 核源码后移到 packages 末尾");
|
|
596
250
|
}
|
|
597
|
-
if (drift.length) msgs.push(
|
|
598
|
-
if (asar.ran && asar.missing.length) {
|
|
599
|
-
msgs.push(`patch-keeper: PiDeck 补丁缺失 —— ${asar.detail}`);
|
|
600
|
-
msgs.push(`关闭 PiDeck 后重跑补丁脚本(快照 ${SNAPSHOT_PATCH_DIR};或用其中的 launch-pideck.cmd 启动 PiDeck 时自动重打)`);
|
|
601
|
-
}
|
|
602
|
-
for (const bp of BUILTIN_PATCHES) {
|
|
603
|
-
const missing = [bp.install, bp.file].filter((p: string) => { const src = safeRead(p); return src !== null && !src.includes(bp.needle); });
|
|
604
|
-
if (missing.length) {
|
|
605
|
-
const where = missing.length === 2 ? "两处" : missing[0] === bp.install ? "安装目录(生效版)" : "用户层";
|
|
606
|
-
msgs.push(`patch-keeper: PiDeck 内置扩展补丁缺失(${bp.name},${where})—— 升级覆盖?从快照 03-PiDeck/builtin-extensions 恢复`);
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
{
|
|
610
|
-
const memSrc2 = safeRead(PI_MEMORY);
|
|
611
|
-
if (memSrc2 !== null && !memSrc2.includes(MEM_SECTION_MARK)) msgs.push("patch-keeper: pi-memory 未打 section 优先补丁 —— goal / plan / security 的 section 会被 force 吞掉,建议 /patch-keeper fix");
|
|
612
|
-
}
|
|
613
|
-
{
|
|
614
|
-
const tactSrc2 = safeRead(TACT);
|
|
615
|
-
if (tactSrc2 !== null && !tactSrc2.includes(TACT_MARK)) msgs.push("patch-keeper: tactician 未打 section 优先补丁 —— 会 force 吞掉其后的 section(goal / plan / security),建议 /patch-keeper fix");
|
|
616
|
-
}
|
|
251
|
+
if (drift.length) msgs.push("patch-keeper: 环境指纹已变(" + drift.join(";") + ")—— 手册 / 上轮验证结论可能过期,按 08-台账核对纪律逐项重验");
|
|
617
252
|
const md = manualDrift();
|
|
618
|
-
if (md) msgs.push(
|
|
619
|
-
if (healNote) msgs.push(healNote);
|
|
253
|
+
if (md) msgs.push("patch-keeper: " + md);
|
|
620
254
|
for (const m of msgs) ctx.ui.notify(m, "warning");
|
|
621
255
|
} catch { /* 静默:检测不该影响会话 */ }
|
|
622
256
|
});
|
|
623
257
|
|
|
624
|
-
// 注入对齐(手册 §2 纪律区):注入的是宿主上次读取的文本,可能与磁盘版不同步。每轮比对一次,不一致时提示一次。
|
|
625
|
-
// 纪律:**绝不返回 systemPrompt** —— 返回即 forceSystemPrompt 覆盖式替换,会静默丢弃其后所有 section 的修改。
|
|
626
258
|
let lastPair = "";
|
|
627
259
|
pi.on("before_agent_start", async (event, ctx: ExtensionContext) => {
|
|
628
260
|
try {
|
|
@@ -634,19 +266,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
634
266
|
const pair = vi + "|" + vd;
|
|
635
267
|
if (vi === vd || pair === lastPair) return undefined;
|
|
636
268
|
lastPair = pair;
|
|
637
|
-
ctx.ui.notify(
|
|
638
|
-
// 回灌四律②:用户已收到通知;模型也需要知道,否则「以磁盘版执行」停在用户侧。
|
|
269
|
+
ctx.ui.notify("注入对齐:注入 v" + vi + " ≠ 磁盘 v" + vd + "(观察者 = 磁盘版)→ 判据:注入版不可作判断依据;动作:读回全文执行,重启后自愈", "warning");
|
|
639
270
|
try {
|
|
640
271
|
(pi as unknown as { sendMessage?: (m: unknown, o?: unknown) => void }).sendMessage?.(
|
|
641
|
-
{
|
|
642
|
-
customType: "patch-keeper",
|
|
643
|
-
content: `注入对齐:注入 v${vi} ≠ 磁盘 v${vd}(观察者 = 磁盘版)→ 注入版不可作判断依据;动作:读回 APPEND_SYSTEM.md 全文后执行,重启后自愈。`,
|
|
644
|
-
display: false,
|
|
645
|
-
},
|
|
272
|
+
{ customType: "patch-keeper", content: "注入对齐:注入 v" + vi + " ≠ 磁盘 v" + vd + "(观察者 = 磁盘版)→ 注入版不可作判断依据;动作:读回 APPEND_SYSTEM.md 全文后执行,重启后自愈。", display: false },
|
|
646
273
|
{ deliverAs: "nextTurn" },
|
|
647
274
|
);
|
|
648
|
-
} catch { /*
|
|
649
|
-
} catch { /*
|
|
275
|
+
} catch { /* 静默 */ }
|
|
276
|
+
} catch { /* 静默 */ }
|
|
650
277
|
return undefined;
|
|
651
278
|
});
|
|
652
|
-
}
|
|
279
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-patch-keeper",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
4
4
|
"description": "pi config guards: codemode single-source check, force-order guard (auto-fix), settings fingerprint drift, injected-manual alignment. (PiDeck / third-party source patches are a local-only module, not bundled.)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|