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.
- package/LICENSE +22 -0
- package/NOTICE.md +46 -0
- package/README.md +169 -0
- package/cordis.patch.yml +10 -0
- package/lib/client.js +2770 -0
- package/package.json +65 -0
- package/plugin/core/diff.mjs +208 -0
- package/plugin/core/markdown.mjs +450 -0
- package/plugin/core/notes.mjs +151 -0
- package/plugin/core/patch.mjs +93 -0
- package/plugin/core/render.mjs +906 -0
- package/plugin/core/runtime.mjs +83 -0
- package/plugin/core/theme-info.mjs +80 -0
- package/plugin/core/theme-spec.mjs +276 -0
- package/plugin/host/assets.mjs +120 -0
- package/plugin/host/custom-theme.mjs +121 -0
- package/plugin/host/routes.mjs +494 -0
- package/plugin/host/store.mjs +719 -0
- package/plugin/host/tools.mjs +573 -0
- package/plugin/index.mjs +127 -0
- package/plugin/vendor/highlight.min.js +1213 -0
- package/plugin/vendor/hljs-map.json +202 -0
- package/plugin/vendor/markdown-it.min.js +2 -0
- package/plugin/vendor/themes.js +414 -0
- package/skills/fishpai/SKILL.md +146 -0
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-fishpai",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"description": "DeepSeek Harness plugin: 鱼排 FishPai — a WeChat Official Account typesetting workbench in the DSH right sidebar, with block-level diffs and annotations a model can read. Rendering core derived from the MIT mopai-markdown.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "OMSociety (https://github.com/OMSociety)",
|
|
8
|
+
"homepage": "https://github.com/OMSociety/dsh-fishpai#readme",
|
|
9
|
+
"bugs": "https://github.com/OMSociety/dsh-fishpai/issues",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/OMSociety/dsh-fishpai.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"dsh",
|
|
16
|
+
"dsh-plugin",
|
|
17
|
+
"deepseek-harness",
|
|
18
|
+
"wechat",
|
|
19
|
+
"markdown",
|
|
20
|
+
"typesetting",
|
|
21
|
+
"sidebar",
|
|
22
|
+
"skill"
|
|
23
|
+
],
|
|
24
|
+
"main": "./plugin/index.mjs",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./plugin/index.mjs",
|
|
27
|
+
"./client": "./lib/client.js",
|
|
28
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
29
|
+
"./package.json": "./package.json"
|
|
30
|
+
},
|
|
31
|
+
"files": [
|
|
32
|
+
"plugin",
|
|
33
|
+
"lib",
|
|
34
|
+
"skills",
|
|
35
|
+
"cordis.patch.yml",
|
|
36
|
+
"README.md",
|
|
37
|
+
"NOTICE.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"dsh": {
|
|
41
|
+
"bundle": {
|
|
42
|
+
"patch": "./cordis.patch.yml"
|
|
43
|
+
},
|
|
44
|
+
"client": {
|
|
45
|
+
"platform": "web"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"node": ">=20"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "node scripts/build-client.mjs",
|
|
53
|
+
"typecheck": "tsc --noEmit",
|
|
54
|
+
"test": "node --test \"test/**/*.test.mjs\"",
|
|
55
|
+
"test:golden:regen": "node scripts/regen-golden.mjs",
|
|
56
|
+
"check:build": "node scripts/build-client.mjs && git diff --exit-code -- lib/"
|
|
57
|
+
},
|
|
58
|
+
"devDependencies": {
|
|
59
|
+
"@deepseek-ai/cordis": "^4.0.2",
|
|
60
|
+
"@types/react": "~18.3.1",
|
|
61
|
+
"@types/react-dom": "~18.3.1",
|
|
62
|
+
"esbuild": "^0.28.2",
|
|
63
|
+
"typescript": "^5.6.0"
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 块级 diff:把「AI 上次写入的版本」与「当前文档」对照成人能读、模型也能读的结构化差异。
|
|
3
|
+
*
|
|
4
|
+
* 为什么不是纯文本 diff:人改一句、AI 只需重写那一块。行级 diff 会把"改了一个词"摊成
|
|
5
|
+
* 十几行噪声,块级 diff 直接给出"第 3 块(正文)从 X 变成 Y" —— 模型据此可以只 patch 那一块,
|
|
6
|
+
* 从而不去碰人改过的其它块。
|
|
7
|
+
*
|
|
8
|
+
* 对齐算法:块 hash 序列的 LCS,再把相邻的 删除/新增 按位置配对成 `changed`(相似度 ≥ 阈值),
|
|
9
|
+
* 配对失败就退回独立的 removed / added。
|
|
10
|
+
*/
|
|
11
|
+
import { splitBlocks, similarity } from './markdown.mjs'
|
|
12
|
+
|
|
13
|
+
/** 判定"这是同一块被改写"而不是"删一块加一块"的相似度下限。 */
|
|
14
|
+
export const CHANGE_THRESHOLD = 0.34
|
|
15
|
+
|
|
16
|
+
/** 差异条目数超过这个比例、且总块数够多时,折叠成"整篇重写"摘要。 */
|
|
17
|
+
export const REWRITE_RATIO = 0.6
|
|
18
|
+
export const REWRITE_MIN_BLOCKS = 12
|
|
19
|
+
/** 折叠后最多保留的改写样本数。 */
|
|
20
|
+
export const REWRITE_SAMPLES = 8
|
|
21
|
+
/** LCS 的规模上限(单元格数):超了就降级成按位置比对,避免阻塞宿主。 */
|
|
22
|
+
export const MAX_DP_CELLS = 4_000_000
|
|
23
|
+
|
|
24
|
+
export { similarity }
|
|
25
|
+
|
|
26
|
+
/** 块 hash 序列的 LCS(返回对齐操作序列)。 */
|
|
27
|
+
function alignByLcs(a, b) {
|
|
28
|
+
const n = a.length
|
|
29
|
+
const m = b.length
|
|
30
|
+
// dp[i][j] = LCS 长度;O(n·m) 时间与内存(调用方用 MAX_DP_CELLS 兜住规模)
|
|
31
|
+
const dp = Array.from({ length: n + 1 }, () => new Uint32Array(m + 1))
|
|
32
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
33
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
34
|
+
dp[i][j] = a[i].hash === b[j].hash ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1])
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const ops = []
|
|
38
|
+
let i = 0
|
|
39
|
+
let j = 0
|
|
40
|
+
while (i < n && j < m) {
|
|
41
|
+
if (a[i].hash === b[j].hash) {
|
|
42
|
+
ops.push({ op: 'equal', a: a[i], b: b[j] })
|
|
43
|
+
i++
|
|
44
|
+
j++
|
|
45
|
+
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
|
46
|
+
ops.push({ op: 'delete', a: a[i] })
|
|
47
|
+
i++
|
|
48
|
+
} else {
|
|
49
|
+
ops.push({ op: 'insert', b: b[j] })
|
|
50
|
+
j++
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
while (i < n) ops.push({ op: 'delete', a: a[i++] })
|
|
54
|
+
while (j < m) ops.push({ op: 'insert', b: b[j++] })
|
|
55
|
+
return ops
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* 超大文档的降级对齐:不做 LCS,直接按位置比。
|
|
60
|
+
*
|
|
61
|
+
* 会漏判"中间插了一段"导致的整体位移,但换来的是 O(n) 而不是 O(n·m) 的宿主阻塞。
|
|
62
|
+
* 文章级文档(几百块以内)永远走 LCS 那条路。
|
|
63
|
+
*/
|
|
64
|
+
function alignByPosition(a, b) {
|
|
65
|
+
const ops = []
|
|
66
|
+
const max = Math.max(a.length, b.length)
|
|
67
|
+
for (let i = 0; i < max; i++) {
|
|
68
|
+
const x = a[i]
|
|
69
|
+
const y = b[i]
|
|
70
|
+
if (x && y && x.hash === y.hash) ops.push({ op: 'equal', a: x, b: y })
|
|
71
|
+
else {
|
|
72
|
+
if (x) ops.push({ op: 'delete', a: x })
|
|
73
|
+
if (y) ops.push({ op: 'insert', b: y })
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return ops
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function entry(status, block, extra = {}) {
|
|
80
|
+
return {
|
|
81
|
+
status,
|
|
82
|
+
blockId: block ? block.id : null,
|
|
83
|
+
index: block ? block.index : null,
|
|
84
|
+
kind: block ? block.kind : null,
|
|
85
|
+
headingPath: block ? block.headingPath : [],
|
|
86
|
+
startLine: block ? block.startLine : null,
|
|
87
|
+
endLine: block ? block.endLine : null,
|
|
88
|
+
...extra,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* 计算块级差异。
|
|
94
|
+
*
|
|
95
|
+
* @param {string} baselineMarkdown AI 上次写入的版本(人改之前的基线)
|
|
96
|
+
* @param {string} currentMarkdown 当前文档
|
|
97
|
+
* @param {{limit?: number}} [opts] limit 是折叠后仍返回的条目上限(默认 40)
|
|
98
|
+
* @returns {{entries: Array<object>, stats: object, rewritten: boolean}}
|
|
99
|
+
*/
|
|
100
|
+
export function diffBlocks(baselineMarkdown, currentMarkdown, opts = {}) {
|
|
101
|
+
const limit = opts.limit ?? 40
|
|
102
|
+
const before = splitBlocks(baselineMarkdown || '')
|
|
103
|
+
const after = splitBlocks(currentMarkdown || '')
|
|
104
|
+
// O(n·m) 的 DP 兜底:文章级文档几十到几百块,永不触发;真遇到怪文档就降级,别卡住宿主事件循环
|
|
105
|
+
const degraded = (before.length + 1) * (after.length + 1) > MAX_DP_CELLS
|
|
106
|
+
const ops = degraded ? alignByPosition(before, after) : alignByLcs(before, after)
|
|
107
|
+
|
|
108
|
+
const entries = []
|
|
109
|
+
const stats = {
|
|
110
|
+
blocksBefore: before.length,
|
|
111
|
+
blocksAfter: after.length,
|
|
112
|
+
unchanged: 0,
|
|
113
|
+
added: 0,
|
|
114
|
+
removed: 0,
|
|
115
|
+
changed: 0,
|
|
116
|
+
charsBefore: (baselineMarkdown || '').length,
|
|
117
|
+
charsAfter: (currentMarkdown || '').length,
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 先切成 run,再把删除/新增 run 配对成 changed
|
|
121
|
+
let k = 0
|
|
122
|
+
while (k < ops.length) {
|
|
123
|
+
const op = ops[k]
|
|
124
|
+
if (op.op === 'equal') {
|
|
125
|
+
stats.unchanged++
|
|
126
|
+
k++
|
|
127
|
+
continue
|
|
128
|
+
}
|
|
129
|
+
const dels = []
|
|
130
|
+
const ins = []
|
|
131
|
+
while (k < ops.length && ops[k].op !== 'equal') {
|
|
132
|
+
if (ops[k].op === 'delete') dels.push(ops[k].a)
|
|
133
|
+
else ins.push(ops[k].b)
|
|
134
|
+
k++
|
|
135
|
+
}
|
|
136
|
+
const pairs = Math.min(dels.length, ins.length)
|
|
137
|
+
for (let p = 0; p < pairs; p++) {
|
|
138
|
+
const sim = similarity(dels[p].text, ins[p].text)
|
|
139
|
+
if (sim >= CHANGE_THRESHOLD) {
|
|
140
|
+
stats.changed++
|
|
141
|
+
entries.push(
|
|
142
|
+
entry('changed', ins[p], {
|
|
143
|
+
before: dels[p].text,
|
|
144
|
+
after: ins[p].text,
|
|
145
|
+
similarity: Number(sim.toFixed(3)),
|
|
146
|
+
beforeHeadingPath: dels[p].headingPath,
|
|
147
|
+
}),
|
|
148
|
+
)
|
|
149
|
+
} else {
|
|
150
|
+
stats.removed++
|
|
151
|
+
stats.added++
|
|
152
|
+
entries.push(entry('removed', dels[p], { before: dels[p].text }))
|
|
153
|
+
entries.push(entry('added', ins[p], { after: ins[p].text }))
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
for (let p = pairs; p < dels.length; p++) {
|
|
157
|
+
stats.removed++
|
|
158
|
+
entries.push(entry('removed', dels[p], { before: dels[p].text }))
|
|
159
|
+
}
|
|
160
|
+
for (let p = pairs; p < ins.length; p++) {
|
|
161
|
+
stats.added++
|
|
162
|
+
entries.push(entry('added', ins[p], { after: ins[p].text }))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const changedCount = entries.length
|
|
167
|
+
const denominator = Math.max(before.length, 1)
|
|
168
|
+
// 折叠门槛:改动面够大 + 文档够长 + 条目多到会灌满上下文
|
|
169
|
+
const rewritten =
|
|
170
|
+
before.length >= REWRITE_MIN_BLOCKS && changedCount > REWRITE_SAMPLES && changedCount / denominator > REWRITE_RATIO
|
|
171
|
+
|
|
172
|
+
const payload = rewritten ? collapse(entries, stats) : entries.slice(0, limit)
|
|
173
|
+
return {
|
|
174
|
+
entries: payload,
|
|
175
|
+
truncated: entries.length > payload.length,
|
|
176
|
+
stats,
|
|
177
|
+
rewritten,
|
|
178
|
+
degraded,
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** 整篇重写:只留统计与少量样本,避免把上下文灌满。 */
|
|
183
|
+
function collapse(entries, stats) {
|
|
184
|
+
const sample = entries
|
|
185
|
+
.filter((e) => e.status === 'changed')
|
|
186
|
+
.slice(0, REWRITE_SAMPLES)
|
|
187
|
+
.map((e) => ({ ...e, before: clip(e.before), after: clip(e.after) }))
|
|
188
|
+
const added = entries.filter((e) => e.status === 'added').length
|
|
189
|
+
const removed = entries.filter((e) => e.status === 'removed').length
|
|
190
|
+
return [
|
|
191
|
+
{
|
|
192
|
+
status: 'rewritten',
|
|
193
|
+
blockId: null,
|
|
194
|
+
index: null,
|
|
195
|
+
kind: 'document',
|
|
196
|
+
headingPath: [],
|
|
197
|
+
startLine: null,
|
|
198
|
+
endLine: null,
|
|
199
|
+
summary: `整篇重写:${stats.blocksBefore} 块 → ${stats.blocksAfter} 块(新增 ${added}、删除 ${removed}、改写 ${stats.changed})`,
|
|
200
|
+
},
|
|
201
|
+
...sample,
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function clip(text, max = 400) {
|
|
206
|
+
const s = String(text ?? '')
|
|
207
|
+
return s.length <= max ? s : `${s.slice(0, max)}…`
|
|
208
|
+
}
|