@neus117/dsh-anchor 0.0.0-stage → 0.6.0

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/CHANGELOG.md ADDED
@@ -0,0 +1,32 @@
1
+ # 变更记录
2
+
3
+ ## 0.6.0 — 2026-11-06
4
+
5
+ 锚点系统合并成一个包(前身:`dsh-anchor-inject` 0.4.0 + `dsh-anchor-form` 0.1.0)。
6
+
7
+ **新增**
8
+
9
+ - 工具 `anchor_form`:四栏(做什么 / 为什么 / 还没验证 / 问我)。三行写回锚点,第四栏原样回会话,由智能体按问题范围翻历史回答。
10
+ - 可点候选:取本会话最近两条原话、最近两条失败调用;label 只指路,选中后写完整原文。
11
+ - 打回重填:`做什么` 只写通用动词、或 `为什么` 不带判据 → 打回一次,卡片上写明理由。
12
+ - 压缩注入时,若「下一步」三行还是空位 → 注入里指路「调 `anchor_form` 让用户填」,并点出本文件路径让它核对配置。
13
+ - 客户端半边:问答卡片里题的说明文字改成次级色(`tertiary`)+ 上间距 8px。
14
+ - `test/compat.mjs`:钉死四个宿主内部标识(卡片 bundle、`.wnLwla_detail`、主题变量、`AskUserQuestionItem` 字段),升级后先跑它。
15
+
16
+ **变更**
17
+
18
+ - **锚点按会话分文件**:`anchorFile` 留空时解析成 `<anchorDir>\<cwd目录名>\<会话短id>-anchor.md`。
19
+ 表单与注入走同一个 `resolveAnchorPath()`,结构上不可能写到不同文件。
20
+ - 表单首次使用会按 `createIfMissing` 落模板骨架,不再因"文件还没被创建"报错。
21
+ - `traceFile` 超过 256 KB 在 apply 时清空;大会话只扫尾部 400 条事件。
22
+
23
+ **修复**
24
+
25
+ - `writeJudgment` 的 `$&` 注入 bug(`replace` 的字符串参数是模板)。
26
+ - 客户端注入改到 factory 执行时,不再依赖 `ctx.effect`;客户端 API 有差异时不会静默失效。
27
+ - `apply` 永不抛:两个半边各自 try/catch 隔离,任一失败不拖垮宿主或预设挂载。
28
+
29
+ ## 更早
30
+
31
+ - `dsh-anchor-inject` 0.4.0:压缩后重注入锚点;`judge.mode=model` 让模型补三行。
32
+ - `dsh-anchor-form` 0.1.0:`anchor_form` 工具初版。
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Neus117
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,113 @@
1
- # Temporary Holding Version
1
+ # dsh-anchor
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ 锚点系统只有一个插件,两个半边共用同一个锚点文件:
4
+
5
+ | 半边 | 做什么 | 开关 |
6
+ |---|---|---|
7
+ | **注入** | 会话出现 `compaction/end` 后,把锚点原文追加回上下文(`user/message`,`source.kind=runtime-context`) | `inject.enabled` |
8
+ | **表单** | 工具 `anchor_form`:让**人在会话里**填三行(做什么 / 为什么 / 还没验证 / 问我),写回同一份锚点 | `form.enabled` |
9
+
10
+ 2026-11-06 合并自 `dsh-anchor-inject` 0.4.0 + `dsh-anchor-form` 0.1.0。
11
+
12
+ ## 为什么合并
13
+
14
+ 两个包各有一份 `writeJudgment`,`$&` 那个写盘 bug 只长在其中一份里(另一份是安全的);两份 `anchorFile`
15
+ 配置还可以指向不同文件,于是"人填的"喂不到"压缩后读取的"。合并后:写盘规则、锚点路径解析、三行文案、
16
+ 配置都只有一份,**表单和注入在结构上就不可能写到不同文件**。
17
+
18
+ ## 配置
19
+
20
+ ```yaml
21
+ - id: dsh-anchor
22
+ name: dsh-anchor
23
+ config:
24
+ anchorFile: '' # 留空 = 按会话分文件
25
+ anchorDir: 'D:\Coding\DeepSeek 开发经验\projects\anchors' # <cwd目录名>\<会话短id>-anchor.md
26
+
27
+ createIfMissing: true
28
+ maxChars: 6000
29
+ protectedFiles: []
30
+ traceFile: ''
31
+ inject:
32
+ enabled: true
33
+ onceSession: ''
34
+ onceOnLoad: false
35
+ judge: { enabled: true, mode: model, minIntervalMs: 1800000 }
36
+ form:
37
+ enabled: true
38
+ intro: ''
39
+ ```
40
+
41
+ | 字段 | 作用 |
42
+ |---|---|
43
+ | `anchorFile` | 显式锚点(存在就用它)。留空则**按会话分**:`<anchorDir>\<cwd目录名>\<会话短id>-anchor.md`——用户的会话 cwd 全是 `D:\Coding\workspace`,按 cwd 分不开项目,按会话分是确定性上界。**表单与注入走同一个解析函数**。 |
44
+ | `anchorDir` / `createIfMissing` / `maxChars` | 锚点根目录、缺文件自动落骨架(表单首次使用也会落)、注入上限。 |
45
+ | `protectedFiles` | 这些路径拒写(例如"测试不许碰正式锚点")。 |
46
+ | `traceFile` | 填路径就记 apply / 注入 / 提问 / 写入状态;留空完全不写盘。**排查先看它**。 |
47
+ | `inject.*` | 注入半边:`judge.mode=model` 让模型补三行;`ask` 在宿主侧不可用(见下);`onceOnLoad` 是测试通道。 |
48
+ | `form.*` | 表单半边:`intro` 是第一栏开头的说明句,写 `''` 关掉。 |
49
+
50
+ ## 表单怎么引导人填对
51
+
52
+ 1. **每个空都写清怎么填 + 合格/不合格例子**;
53
+ 2. **可点候选**:取本会话最近两条真实原话、最近两条失败调用做成选项。label 只负责指路
54
+ (`刚才说的第 1 条` / `上一轮报错 1`),原文在说明里,**选中后写进锚点的是完整原文**;
55
+ 3. **打回重填**:`做什么` 只写「继续推进」这类通用动词、或 `为什么` 不带判据 → 打回一次,
56
+ 卡片上写明打回理由;仍不合格才放行(`why` 追加 `[判据缺失]`);
57
+ 4. **初见说明**:第一栏开头说明「压缩后我只看得见这个文件」,`form.intro` 可覆盖。
58
+
59
+ ## 写入规则(只有一个实现)
60
+
61
+ 只改 `## 下一步` 段:`[待填]` 空位 → 原位替换;已有 `- 做什么:` 行 → 原位更新;手写锚点 → 插在标题之后。
62
+ 替换值一律走函数形式(否则 `$&` 会被当成模板吃掉)。这个形状 `anchor-gen.ps1 -Refresh` 认得、会保留。
63
+
64
+ ## 压缩注入长什么样
65
+
66
+ `<system-reminder>` + 说明「这不是用户指令」+ 来源与写入时间 + 锚点原文 + 「与当前窗口冲突时以窗口为准」。
67
+ 判断段还是空位时,额外指路:**调 `anchor_form` 让用户填,不要自己编**,并点出本文件路径让它核对配置。
68
+
69
+ ## 为什么 `judge.mode = ask` 不可用
70
+
71
+ 宿主根插件调 `ctx.userQuestions.ask` 拿不到合法的 agent 载体(活 agent 对象 / 不传 / `{id}` 占位,三种实测全失败)。
72
+ 工具执行上下文里的 `exec.agent` 才是唯一入口——所以问人这件事只存在于 `anchor_form` 里。
73
+
74
+ ## 客户端半边
75
+
76
+ 只做一件事:**题的说明文字**改成次级色。内置卡片里选项说明有 `tertiary`,题的说明(`.wnLwla_detail`)没有 `color`。
77
+
78
+ ```css
79
+ .wnLwla_card .wnLwla_detail, .wnLwla_detail,
80
+ .wnLwla_card .wnLwla_detail *, .wnLwla_detail * { /* 文字在更深的自带颜色的元素里 */
81
+ color: var(--dsw-alias-label-tertiary) !important; /* 强制:内置样式会压过普通声明 */
82
+ }
83
+ .wnLwla_card .wnLwla_detail, .wnLwla_detail {
84
+ margin-top: 8px !important; /* 与标题之间再拉开一点;左右沿用内置 24px */
85
+ }
86
+ ```
87
+
88
+ 三条踩过的坑:
89
+
90
+ - **注入必须在 factory 执行时做,不能放在 `apply(ctx)` 的 `ctx.effect` 里**:客户端 API 一有差异就静默失败,
91
+ 页面上看不出任何异常(2026-11-06 连试三次都"改了没反应")。
92
+ - **验证只能看页面,用二元探针**:`outline: 2px solid red !important` → 问"有没有红框"。
93
+ 有红框=模块加载+选择器命中;没红框=模块没进页面。别用"CSS 文件里写了什么"当证据。
94
+ - 类名带构建期 hash(`wnLwla_`),升级后选择器失效会退回原样;`test/compat.mjs` 每次升级后跑一次。
95
+ - **factory id 必须等于包名 `dsh-anchor`**(2026-11-06 曾因写成别的名字触发 web boot 致命失败)。
96
+
97
+ ## 边界
98
+
99
+ - `ask` 没有超时:没人回答就一直等,直到会话结束。
100
+ - 一次调用最多 5 张卡(4 栏 + 2 次打回)。
101
+ - 候选来自会话末尾 2 条原话 / 2 条失败调用,不是"总结"。
102
+ - 注入只在 `compaction/end` 后发生一次;压缩失败也会发这个事件(验收时别把"失败也注入"当成"压缩成功")。
103
+
104
+ ## 开发
105
+
106
+ ```powershell
107
+ $env:ELECTRON_RUN_AS_NODE=1
108
+ $exe = "D:\Coding\DeepSeek Harness\DeepSeek Harness.exe"
109
+ & $exe "D:\Coding\dsh-plugins\dsh-anchor\test\run.mjs" # 表单/写盘 30
110
+ & $exe "D:\Coding\dsh-plugins\dsh-anchor\test\inject.mjs" # 注入/判断层/指路 35
111
+ & $exe "D:\Coding\dsh-plugins\dsh-anchor\test\client.mjs" # 客户端半边 5
112
+ & $exe "D:\Coding\dsh-plugins\dsh-anchor\test\compat.mjs" # 升级自检 5(钉死宿主内部标识)
113
+ ```
@@ -0,0 +1,34 @@
1
+ # dsh-anchor — 锚点系统(注入 + 表单,一个包)
2
+ #
3
+ # 2026-11-06 合并自 dsh-anchor-inject(0.4.0)+ dsh-anchor-form(0.1.0)。
4
+ # 合并的理由:两份 writeJudgment 里只有一份有 $& 的修复;两份 anchorFile 可以指向不同文件,
5
+ # 于是"人填的"喂不到"压缩后读取的"。现在写盘规则、路径解析、三行文案、配置文件都只有一份。
6
+ #
7
+ # 两个半边可以分别关:inject.enabled / form.enabled。任一边抛错只影响自己(index.js 里各自 try/catch)。
8
+ - insert:
9
+ - id: dsh-anchor
10
+ name: '@neus117/dsh-anchor'
11
+ config:
12
+ # 注入与表单共用这一份解析:anchorFile 存在就用它,否则退回 anchorDir + 会话 cwd。
13
+ anchorFile: ''
14
+ anchorDir: 'D:\Coding\DeepSeek 开发经验\projects\anchors'
15
+ createIfMissing: true
16
+ maxChars: 6000
17
+ # 需要"测试不许覆盖正式锚点"时,把路径加到这里。
18
+ protectedFiles: []
19
+ traceFile: 'D:\Coding\DeepSeek 开发经验\projects\anchor-trace.log'
20
+ inject:
21
+ enabled: true
22
+ onceSession: ''
23
+ onceOnLoad: false
24
+ judge:
25
+ # model = 让模型补三行(宿主作用域可用);
26
+ # ask = 宿主侧拿不到 live agent,实测三种传参全失败,只保留失败路径;
27
+ # 真要问人请让智能体调 anchor_form。
28
+ enabled: true
29
+ mode: model
30
+ minIntervalMs: 1800000
31
+ form:
32
+ enabled: true
33
+ # 第一栏开头的说明句;写 '' 关掉。
34
+ intro: ''
package/lib/client.js ADDED
@@ -0,0 +1,91 @@
1
+ // ============================================================================
2
+ // dsh-anchor — 浏览器半边(只做一件事:问答卡片的说明文字用次级色)
3
+ // ============================================================================
4
+ // 内置卡片(@deepseek-ai/dsh-client-ui-user-questions,0.2.2-rc.1 实测):
5
+ // .wnLwla_detail { flex-shrink: 0; margin: 0 24px 8px } ← **没有 color**
6
+ // .wnLwla_description { color: var(--dsw-alias-label-tertiary) } ← 选项说明有次级色
7
+ // 也就是:选项说明是次级色,题的说明文字却继承卡片主色(和标题同色)。只补这个缺口。
8
+ //
9
+ // **不要覆盖左间距**:0.2.1 是 `margin: 0 2px 8px`(贴选项框);0.2.2 官方改成 `0 24px 8px`
10
+ // (与标题同边界),用户抱怨的"没距离"已由上游修好。
11
+ //
12
+ // 可靠性:样式在**模块加载时**就注入,不依赖客户端 ctx 的 `effect` 是否存在。
13
+ // 2026-11-06 第一版把注入放在 `apply(ctx)` 的 `ctx.effect(...)` 里,客户端 API 一有差异就
14
+ // 静默不生效(被 try/catch 吃掉),页面上完全看不出来。现在有两条退路:
15
+ // 1. 模块一执行就注入(主路径);
16
+ // 2. apply(ctx) 时再确保一次,并在 ctx 提供 effect 时登记卸载。
17
+ // 注入成功会打一条 `[dsh-anchor] style injected` 到控制台——排查时先看它。
18
+ //
19
+ // 已知脆弱点:类名带构建期 hash(wnLwla_),升级后选择器可能失效 → 退回原样,不会改坏。
20
+ // **factory id 必须等于包名**(现为 `@neus117/dsh-anchor`;写成别的名字 = web boot 致命失败,见 0.2.1 事故)。
21
+ // ============================================================================
22
+
23
+ window.__ModuleLoader__.load({
24
+ id: "@neus117/dsh-anchor",
25
+ factory: (require) => {
26
+ var module = { exports: {} };
27
+ var exports = module.exports;
28
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
29
+
30
+ const STYLE_ID = "dsh-anchor";
31
+ const CSS = [
32
+ // detail 的内容是 MarkdownText,文字挂在更深的子元素上且自带颜色;只给外层上色不生效
33
+ // (2026-11-06 实测:红框探针证明选择器命中,但颜色不变)。所以覆盖到所有后代。
34
+ // tertiary 比 secondary 再浅一档,与选项说明同一级。
35
+ ".wnLwla_card .wnLwla_detail, .wnLwla_detail,",
36
+ ".wnLwla_card .wnLwla_detail *, .wnLwla_detail * {",
37
+ " color: var(--dsw-alias-label-tertiary) !important;",
38
+ "}",
39
+ // 与标题之间再拉开一点(内置是 margin: 0 24px 8px,上边距为 0)。
40
+ // 只动上边距;左右沿用内置的 24px。
41
+ ".wnLwla_card .wnLwla_detail, .wnLwla_detail {",
42
+ " margin-top: 8px !important;",
43
+ "}"
44
+ ].join("\n");
45
+
46
+ /** 幂等注入:已有就返回那一个;没有 head 就落到 documentElement。 */
47
+ function injectStyle() {
48
+ if (typeof document === "undefined") return null;
49
+ try {
50
+ if (typeof document.querySelector === "function") {
51
+ const existing = document.querySelector(`style[data-dsh-plugin="${STYLE_ID}"]`);
52
+ if (existing) return existing;
53
+ }
54
+ const style = document.createElement("style");
55
+ style.dataset.dshPlugin = STYLE_ID;
56
+ style.textContent = CSS;
57
+ const host = document.head || document.documentElement;
58
+ if (!host) return null;
59
+ host.appendChild(style);
60
+ return style;
61
+ } catch (error) {
62
+ try { console.warn("[dsh-anchor] style inject failed", error); } catch {}
63
+ return null;
64
+ }
65
+ }
66
+
67
+ // 模块加载即注入:不等人调用 apply,也不依赖 ctx.effect。
68
+ const injected = injectStyle();
69
+ if (injected) {
70
+ try { console.info("[dsh-anchor] style injected"); } catch {}
71
+ }
72
+
73
+ /**
74
+ * 插件入口:再确保一次注入;ctx 提供 effect 时登记卸载。
75
+ * 任何异常都不往外抛——客户端样式不该有能力弄坏页面。
76
+ * @param ctx - 插件自己的客户端上下文。
77
+ */
78
+ function apply(ctx) {
79
+ const style = injectStyle();
80
+ try {
81
+ ctx?.effect?.(() => () => style?.remove?.(), "dsh-anchor: question card detail color");
82
+ } catch (error) {
83
+ try { console.warn("[dsh-anchor] effect unavailable (style 仍已注入)", error); } catch {}
84
+ }
85
+ }
86
+
87
+ exports.apply = apply;
88
+
89
+ return module.exports;
90
+ }
91
+ });
package/lib/core.js ADDED
@@ -0,0 +1,127 @@
1
+ /**
2
+ * dsh-anchor — 共享核心:读锚点、写三行、判据判定。
3
+ *
4
+ * 这里只放**两个入口(注入 / 表单)都要用的东西**,避免同一件事写两遍:
5
+ * 2026-11-06 之前 `writeJudgment` 在两个包里各有一份,`$&` 那个 bug 只长在其中一份里。
6
+ *
7
+ * @module dsh-anchor/core
8
+ */
9
+
10
+ import { readFileSync, writeFileSync } from "node:fs";
11
+
12
+ export function readText(file) {
13
+ const text = readFileSync(file, "utf8");
14
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
15
+ }
16
+
17
+ /** 判据:命令、路径、读数、报错原文、用户原话——压缩后的我能拿去重跑的东西。 */
18
+ export function looksLikeEvidence(text) {
19
+ return /(\d|[\\/]|`|FAIL|exit|→|原话|第\s*\d+\s*轮)/.test(String(text || ""));
20
+ }
21
+
22
+ // 「做什么」只写一个通用动词时,换个人(或压缩后的自己)不知道要动哪里。
23
+ const VAGUE_WHAT = /^(继续|继续推进|继续做|继续工作|推进|往下做|往下走|优化|改进|完善|修复|修复问题|完成|收尾|测试一下|看看|跟进|开始|处理)[。.!!]?$/;
24
+
25
+ export function isVagueWhat(text) {
26
+ return VAGUE_WHAT.test(String(text || "").trim());
27
+ }
28
+
29
+ /** 压成一行、截断,给选项 label 用(label 会被原样写成答案,所以要短)。 */
30
+ export function clip(text, max = 48) {
31
+ const flat = String(text || "").replace(/\s+/g, " ").trim();
32
+ return flat.length > max ? `${flat.slice(0, max)}…` : flat;
33
+ }
34
+
35
+ export function normalizePath(file) {
36
+ return String(file || "").replace(/\//g, "\\").replace(/\\+$/, "").toLowerCase();
37
+ }
38
+
39
+ export function isProtected(file, protectedFiles) {
40
+ const target = normalizePath(file);
41
+ return (Array.isArray(protectedFiles) ? protectedFiles : []).some((entry) => normalizePath(entry) === target);
42
+ }
43
+
44
+ const LINE = {
45
+ what: { todo: /- \[待填\] 做什么:[^\r\n]*/, filled: /- 做什么:[^\r\n]*/, label: "做什么" },
46
+ why: { todo: /- \[待填\] 为什么([^)]*):[^\r\n]*/, filled: /- 为什么:[^\r\n]*/, label: "为什么" },
47
+ pending: { todo: /- \[待填\] 还没验证:[^\r\n]*/, filled: /- 还没验证:[^\r\n]*/, label: "还没验证" },
48
+ };
49
+
50
+ function applySection(section, judgment) {
51
+ let next = section;
52
+ let touched = false;
53
+ for (const key of Object.keys(LINE)) {
54
+ const value = judgment[key];
55
+ if (!value) continue;
56
+ const spec = LINE[key];
57
+ const line = `- ${spec.label}:${value}`;
58
+ if (spec.todo.test(next)) {
59
+ next = next.replace(spec.todo, () => line);
60
+ touched = true;
61
+ } else if (spec.filled.test(next)) {
62
+ next = next.replace(spec.filled, () => line);
63
+ touched = true;
64
+ }
65
+ }
66
+ if (touched) return { text: next, touched: true };
67
+ const insert = Object.keys(LINE)
68
+ .filter((key) => judgment[key])
69
+ .map((key) => `- ${LINE[key].label}:${judgment[key]}`)
70
+ .join("\n");
71
+ if (!insert) return { text: next, touched: false };
72
+ const lead = /^(?:\r?\n)+/.exec(next)?.[0] ?? "";
73
+ return { text: `${lead}${insert}\n${next.slice(lead.length)}`, touched: true };
74
+ }
75
+
76
+ /**
77
+ * 把三行写进「下一步」段;只动这一段,其它段一个字节不碰。
78
+ * 1. 有 `- [待填] 做什么:` 空位 → 原位替换;
79
+ * 2. 已有 `- 做什么:` 行 → 原位更新(重复填写不堆行);
80
+ * 3. 两样都没有(手写锚点)→ 插在 `## 下一步` 标题之后。
81
+ * 替换值一律走函数形式:`replace` 的字符串参数里 `$&`、`` $` ``、`$'` 是模板,会被吃掉。
82
+ * @returns {{status: "written"|"no-change"|"failed", detail: string}}
83
+ */
84
+ export function writeJudgment(file, judgment) {
85
+ let text;
86
+ try {
87
+ text = readText(file);
88
+ } catch (error) {
89
+ return { status: "failed", detail: `读不到锚点文件:${error?.message || error}` };
90
+ }
91
+ const heading = /^## 下一步[^\r\n]*$/m.exec(text);
92
+ if (!heading) {
93
+ return { status: "failed", detail: "锚点文件里没有 `## 下一步` 段,拒绝写入(不猜位置)" };
94
+ }
95
+ const start = heading.index + heading[0].length;
96
+ const rest = text.slice(start);
97
+ const endRel = rest.search(/^\r?\n## /m);
98
+ const end = endRel >= 0 ? start + endRel : text.length;
99
+ const result = applySection(text.slice(start, end), judgment);
100
+ if (!result.touched) return { status: "no-change", detail: "没有可写的行" };
101
+ const next = text.slice(0, start) + result.text + text.slice(end);
102
+ if (next === text) return { status: "no-change", detail: "内容未变" };
103
+ try {
104
+ writeFileSync(file, next, "utf8");
105
+ } catch (error) {
106
+ return { status: "failed", detail: `写入失败:${error?.message || error}` };
107
+ }
108
+ return { status: "written", detail: "" };
109
+ }
110
+
111
+ /** 判断段是否还是空位(给压缩注入决定要不要指路)。 */
112
+ export function judgmentEmpty(file) {
113
+ try {
114
+ const text = readText(file);
115
+ if (text.includes("- [待填] 做什么:") || text.includes("- [待填] 为什么")) return true;
116
+ const after = text.split(/^## 下一步[^\r\n]*$/m)[1];
117
+ if (after === undefined) return false;
118
+ const section = after.split(/^## /m)[0] || "";
119
+ const meaningful = section
120
+ .split(/\r?\n/)
121
+ .map((line) => line.trim())
122
+ .filter((line) => line && !line.startsWith("- [指针]") && !line.startsWith("- [待填]"));
123
+ return meaningful.length === 0;
124
+ } catch {
125
+ return false;
126
+ }
127
+ }