dsh-fishpai 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.
@@ -0,0 +1,93 @@
1
+ /**
2
+ * 局部改稿:按块 id / 序号打补丁,而不是整篇重写。
3
+ *
4
+ * 这是"AI 不去动人手改过的其它块"的落地方式:模型改哪块就只改哪块,
5
+ * 剩下的字节原样保留(连空行、行尾空格都不动)。
6
+ *
7
+ * 补丁从文档底部往上应用,避免前面的改动把后面的行号顶偏。
8
+ */
9
+
10
+ /**
11
+ * @param {string} markdown 当前文档
12
+ * @param {Array<object>} blocks `splitBlocks(markdown)` 的结果
13
+ * @param {Array<object>} patches `{block_id?|block_index?, op, markdown?}`
14
+ * @returns {{markdown: string, applied: Array<object>, errors: Array<string>}}
15
+ */
16
+ export function applyPatches(markdown, blocks, patches) {
17
+ // CRLF 文档(Windows 上用记事本/别的编辑器存的 .md 很常见):split('\n') 会把行尾的
18
+ // `\r` 留在每行末尾,于是「空行」是 `'\r'` 而不是 `''`——下面所有「是不是空行」的判据
19
+ // 都会失灵(replace 会吞掉块尾的分隔行,下一块被并进列表)。统一在 LF 世界里做行操作,
20
+ // 输出时再按原文档的行尾风格还原(最后一行不加 `\r`:它后面没有换行)。
21
+ const source = String(markdown ?? '')
22
+ const isCrlf = /\r\n/.test(source)
23
+ const lines = isCrlf ? source.replace(/\r\n/g, '\n').split('\n') : source.split('\n')
24
+ const denorm = (out) => {
25
+ if (!isCrlf) return out.join('\n')
26
+ return out.map((l, i) => (i === out.length - 1 ? l : `${l}\r`)).join('\n')
27
+ }
28
+ const byId = new Map(blocks.map((b) => [b.id, b]))
29
+ const byIndex = new Map(blocks.map((b) => [b.index, b]))
30
+
31
+ const resolved = []
32
+ const errors = []
33
+ for (const patch of patches || []) {
34
+ const op = String(patch.op || 'replace')
35
+ if (!['replace', 'insert_after', 'delete'].includes(op)) {
36
+ errors.push(`未知 op「${op}」`)
37
+ continue
38
+ }
39
+ let block = null
40
+ if (patch.block_id) block = byId.get(String(patch.block_id)) || null
41
+ else if (patch.block_index !== undefined && patch.block_index !== null) block = byIndex.get(Number(patch.block_index)) || null
42
+ else {
43
+ errors.push('补丁缺少 block_id 或 block_index')
44
+ continue
45
+ }
46
+ if (!block) {
47
+ errors.push(`找不到块 ${patch.block_id ?? patch.block_index}(文档可能已变,请重新 fishpai_read)`)
48
+ continue
49
+ }
50
+ resolved.push({ op, block, text: patch.markdown === undefined ? null : String(patch.markdown) })
51
+ }
52
+
53
+ // 同一个块被多次 patch:按原始顺序自下而上执行,后写的在下(同位置时后执行者更靠前)
54
+ resolved.sort((a, b) => (a.block.startLine === b.block.startLine ? b.block.endLine - a.block.endLine : b.block.startLine - a.block.startLine))
55
+
56
+ const applied = []
57
+ for (const { op, block, text } of resolved) {
58
+ const from = block.startLine - 1
59
+ const to = block.endLine // 独占末端(1-based 闭区间 → 0-based 半开)
60
+ const newLines = text === null || text === '' ? [] : text.replace(/\r\n?/g, '\n').replace(/\n+$/, '').split('\n')
61
+ if (op === 'replace') {
62
+ if (!newLines.length) {
63
+ errors.push(`replace 需要 markdown:块 ${block.id}`)
64
+ continue
65
+ }
66
+ // 块的行范围**包含末尾那个空行**(它就是块与块之间的分隔符)。替换时必须把分隔符补回去:
67
+ // 少了它,下一块会与这里的结尾粘在一起——markdown 的"懒延续"会把下一段并进列表/段落,
68
+ // 而下一轮再 replace 这个被并大了的块时,**被吞掉的块就被一起删掉了**。
69
+ // 踩过:改一次列表项,吃掉了文末的「项目地址」两行。
70
+ const keepsBlank = to > from && lines[to - 1] === ''
71
+ lines.splice(from, to - from, ...newLines, ...(keepsBlank ? [''] : []))
72
+ } else if (op === 'delete') {
73
+ lines.splice(from, to - from)
74
+ } else {
75
+ if (!newLines.length) {
76
+ errors.push(`insert_after 需要 markdown:块 ${block.id}`)
77
+ continue
78
+ }
79
+ const at = to
80
+ // 前后各保证**恰好一个**空行:上一行是内容、且插入文本自己没带前导空行,就补一个;
81
+ // 下一行是内容、且插入文本没带尾随空行,也补一个。分隔行(列表的 map 里就含着它;
82
+ // 段落的 map 不含、但紧跟在 at 后面)本来就在,不重复补。
83
+ // 少了任一边,markdown 的"懒延续"会把新块与相邻块并成同一段——
84
+ // 模型以为加了新段落,实际只给已有段落加了一行。
85
+ const needHead = at > 0 && lines[at - 1] !== '' && newLines[0] !== ''
86
+ const needTail = lines[at] !== undefined && lines[at] !== '' && newLines[newLines.length - 1] !== ''
87
+ lines.splice(at, 0, ...(needHead ? [''] : []), ...newLines, ...(needTail ? [''] : []))
88
+ }
89
+ applied.push({ blockId: block.id, op })
90
+ }
91
+
92
+ return { markdown: denorm(lines), applied, errors }
93
+ }