@neus117/dsh-anchor 0.0.0-stage → 0.6.1
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 +38 -0
- package/LICENSE +21 -0
- package/README.md +76 -2
- package/cordis.patch.yml +34 -0
- package/lib/client.js +91 -0
- package/lib/core.js +127 -0
- package/lib/form.js +320 -0
- package/lib/index.js +86 -0
- package/lib/inject.js +480 -0
- package/package.json +42 -3
- package/templates/anchor.md +26 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# 变更记录
|
|
2
|
+
|
|
3
|
+
## 0.6.1 — 2026-11-06
|
|
4
|
+
|
|
5
|
+
**文档**
|
|
6
|
+
|
|
7
|
+
- README 重写:按「能做什么 / 锚点长什么样 / 安装 / 写到哪 / 填的时候 / 边界」组织,去掉开发过程与内部实现细节。
|
|
8
|
+
|
|
9
|
+
## 0.6.0 — 2026-11-06
|
|
10
|
+
|
|
11
|
+
锚点系统合并成一个包(前身:`dsh-anchor-inject` 0.4.0 + `dsh-anchor-form` 0.1.0)。
|
|
12
|
+
|
|
13
|
+
**新增**
|
|
14
|
+
|
|
15
|
+
- 工具 `anchor_form`:四栏(做什么 / 为什么 / 还没验证 / 问我)。三行写回锚点,第四栏原样回会话,由智能体按问题范围翻历史回答。
|
|
16
|
+
- 可点候选:取本会话最近两条原话、最近两条失败调用;label 只指路,选中后写完整原文。
|
|
17
|
+
- 打回重填:`做什么` 只写通用动词、或 `为什么` 不带判据 → 打回一次,卡片上写明理由。
|
|
18
|
+
- 压缩注入时,若「下一步」三行还是空位 → 注入里指路「调 `anchor_form` 让用户填」,并点出本文件路径让它核对配置。
|
|
19
|
+
- 客户端半边:问答卡片里题的说明文字改成次级色(`tertiary`)+ 上间距 8px。
|
|
20
|
+
- `test/compat.mjs`:钉死四个宿主内部标识(卡片 bundle、`.wnLwla_detail`、主题变量、`AskUserQuestionItem` 字段),升级后先跑它。
|
|
21
|
+
|
|
22
|
+
**变更**
|
|
23
|
+
|
|
24
|
+
- **锚点按会话分文件**:`anchorFile` 留空时解析成 `<anchorDir>\<cwd目录名>\<会话短id>-anchor.md`。
|
|
25
|
+
表单与注入走同一个 `resolveAnchorPath()`,结构上不可能写到不同文件。
|
|
26
|
+
- 表单首次使用会按 `createIfMissing` 落模板骨架,不再因"文件还没被创建"报错。
|
|
27
|
+
- `traceFile` 超过 256 KB 在 apply 时清空;大会话只扫尾部 400 条事件。
|
|
28
|
+
|
|
29
|
+
**修复**
|
|
30
|
+
|
|
31
|
+
- `writeJudgment` 的 `$&` 注入 bug(`replace` 的字符串参数是模板)。
|
|
32
|
+
- 客户端注入改到 factory 执行时,不再依赖 `ctx.effect`;客户端 API 有差异时不会静默失效。
|
|
33
|
+
- `apply` 永不抛:两个半边各自 try/catch 隔离,任一失败不拖垮宿主或预设挂载。
|
|
34
|
+
|
|
35
|
+
## 更早
|
|
36
|
+
|
|
37
|
+
- `dsh-anchor-inject` 0.4.0:压缩后重注入锚点;`judge.mode=model` 让模型补三行。
|
|
38
|
+
- `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,77 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dsh-anchor
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
上下文被压缩后,把你自己写的「锚点」重新塞回模型的上下文;三行「下一步」还可以在会话里用表单让人填。
|
|
4
|
+
|
|
5
|
+
## 它能做什么
|
|
6
|
+
|
|
7
|
+
| 注入 | 表单 |
|
|
8
|
+
|---|---|
|
|
9
|
+
| 每次上下文压缩后,自动把锚点文件原文追加回上下文,并声明「这不是用户指令」 | 智能体调 `anchor_form` 时,弹出四个空——**做什么 / 为什么 / 还没验证 / 问我** |
|
|
10
|
+
| 触发:`compaction/end` | 触发:智能体主动调用 |
|
|
11
|
+
| 开关:`inject.enabled` | 开关:`form.enabled` |
|
|
12
|
+
|
|
13
|
+
压缩能保住事实,保不住判断。判断得由人写——表单就是把「让人写」搬进会话里;写回的文件和压缩后注入的是**同一个**。
|
|
14
|
+
|
|
15
|
+
## 锚点长什么样
|
|
16
|
+
|
|
17
|
+
一份普通 Markdown,关键是这三行:
|
|
18
|
+
|
|
19
|
+
```markdown
|
|
20
|
+
## 下一步(做什么 + 为什么)
|
|
21
|
+
- 做什么:把 anchor_form 挂进预设并调一次验收
|
|
22
|
+
- 为什么:跑 test\run.mjs 得到 34 passed
|
|
23
|
+
- 还没验证:真实压缩后的注入还没触发过
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
只改 `## 下一步` 段:有 `[待填]` 空位就原位替换,已有这三行就原位更新,手写锚点则插在标题之后——其它段一个字节不动。
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
```powershell
|
|
31
|
+
npm i @neus117/dsh-anchor
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
挂载一行即可(两个半边可以分别关):
|
|
35
|
+
|
|
36
|
+
```yaml
|
|
37
|
+
- id: dsh-anchor
|
|
38
|
+
name: '@neus117/dsh-anchor'
|
|
39
|
+
config:
|
|
40
|
+
anchorFile: '' # 留空 = 按会话分文件
|
|
41
|
+
anchorDir: 'D:\Coding\DeepSeek 开发经验\projects\anchors'
|
|
42
|
+
inject: { enabled: true }
|
|
43
|
+
form: { enabled: true }
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## 锚点写到哪
|
|
47
|
+
|
|
48
|
+
- `anchorFile` 留空时按会话分:`<anchorDir>\<工作目录名>\<会话短id>-anchor.md`,**任何会话都不会读到别的项目的锚点**。
|
|
49
|
+
- 文件不存在时自动落一份模板骨架(表单第一次用也会落)。
|
|
50
|
+
- 想固定一份共享锚点:把 `anchorFile` 写成那个路径。
|
|
51
|
+
|
|
52
|
+
## 填的时候
|
|
53
|
+
|
|
54
|
+
- **可点候选**:本会话最近两条原话、最近两条失败调用做成选项,选中就写入完整原文。
|
|
55
|
+
- **打回重填**:「做什么」只写「继续推进」这类通用动词、或「为什么」不带判据 → 打回一次,卡片上写明理由。
|
|
56
|
+
- **第四栏「问我」**:写的话不写进锚点,原样回到会话,智能体按问题范围翻历史回答。
|
|
57
|
+
- 判断段还空着时,压缩注入里会指路「调 `anchor_form` 让用户填,别自己编」。
|
|
58
|
+
|
|
59
|
+
## 边界
|
|
60
|
+
|
|
61
|
+
- 问人只在**工具执行上下文**里成立;宿主侧拿不到可用的 agent 载体,所以没有"自动弹表单"。
|
|
62
|
+
- `ask` 没有超时:没人回答就一直等,直到会话结束。
|
|
63
|
+
- 一次调用最多 5 张卡(4 栏 + 最多 2 次打回)。
|
|
64
|
+
- 压缩失败也会发 `compaction/end`——注入发生了不等于压缩成功了。
|
|
65
|
+
|
|
66
|
+
## 开发
|
|
67
|
+
|
|
68
|
+
```powershell
|
|
69
|
+
$env:ELECTRON_RUN_AS_NODE=1
|
|
70
|
+
$exe = "D:\Coding\DeepSeek Harness\DeepSeek Harness.exe"
|
|
71
|
+
& $exe "…\dsh-anchor\test\run.mjs" # 表单 / 写盘 / 健壮性
|
|
72
|
+
& $exe "…\dsh-anchor\test\inject.mjs" # 注入 / 判断层 / 指路
|
|
73
|
+
& $exe "…\dsh-anchor\test\client.mjs" # 客户端半边
|
|
74
|
+
& $exe "…\dsh-anchor\test\compat.mjs" # 升级自检:钉死宿主内部标识
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
变更记录见 [CHANGELOG.md](./CHANGELOG.md)。
|
package/cordis.patch.yml
ADDED
|
@@ -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
|
+
}
|