dsh-notes-board 0.0.0-stage → 0.3.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/LICENSE +21 -0
- package/NOTICE.md +21 -0
- package/README.md +163 -2
- package/client.js +1253 -0
- package/cordis.patch.yml +6 -0
- package/icon.svg +16 -0
- package/index.js +28 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +79 -4
- package/scripts/build-client.mjs +175 -0
- package/scripts/test-board.mjs +202 -0
- package/src/core/notes.js +486 -0
- package/src/core/tools.js +221 -0
- package/src/host/board.js +184 -0
- package/src/host/notes.js +250 -0
- package/src/host/skills.js +48 -0
- package/src/panel.js +294 -0
- package/vendor/board.html +1779 -0
- package/vendor/write-notes-like-deepseek/README.md +323 -0
- package/vendor/write-notes-like-deepseek/package.json +21 -0
- package/vendor/write-notes-like-deepseek/scripts/export-assets.sh +40 -0
- package/vendor/write-notes-like-deepseek/scripts/import-dsh-notes.ts +125 -0
- package/vendor/write-notes-like-deepseek/scripts/test-skill-install.mjs +63 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/SKILL.md +195 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/assets/agent-notes-board.html +1779 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/references/archiving.md +109 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/references/classification.md +33 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/references/note-format.md +69 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/references/prose-checklist.md +51 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/references/quality-gate.md +58 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/references/simplification-checklist.md +26 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/references/verification.md +34 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/references/when-to-write.md +83 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/scripts/agent-note-tree.ts +75 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/scripts/archive-agent-note.ts +167 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/scripts/build-board.ts +235 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/scripts/check-note-anchors.ts +103 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/scripts/verify-agent-note-format.ts +158 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/scripts/verify-agent-note-tree.ts +56 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/scripts/verify-archived-agent-notes.ts +127 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/templates/implemented.md +32 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/templates/proposed.md +38 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/templates/rejected.md +27 -0
- package/vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/templates/verify-notes.yml +38 -0
package/cordis.patch.yml
ADDED
package/icon.svg
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Notes Board">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="nb-cover" x1="0" y1="0" x2="1" y2="1">
|
|
4
|
+
<stop offset="0" stop-color="#3b82f6"/>
|
|
5
|
+
<stop offset="1" stop-color="#1e3a8a"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
<rect x="6" y="6" width="52" height="52" rx="13" fill="url(#nb-cover)"/>
|
|
9
|
+
<path d="M22 16h14.5L45 24.5V48H22z" fill="#f8fafc" opacity="0.96"/>
|
|
10
|
+
<path d="M36 16v9h9" fill="none" stroke="#93c5fd" stroke-width="2.4" stroke-linejoin="round"/>
|
|
11
|
+
<g stroke="#1e40af" stroke-width="2.6" stroke-linecap="round">
|
|
12
|
+
<path d="M27.5 28h12"/>
|
|
13
|
+
<path d="M27.5 33.5h12"/>
|
|
14
|
+
<path d="M27.5 39h7.5"/>
|
|
15
|
+
</g>
|
|
16
|
+
</svg>
|
package/index.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-notes-board · 插件入口
|
|
3
|
+
*
|
|
4
|
+
* 一个包、一条插件行,拼起三件事:
|
|
5
|
+
* - 看板半边 src/host/board.js:面板 + /api/dsh-notes-board/* 只读路由,依赖 webServer;
|
|
6
|
+
* - 笔记半边 src/host/notes.js:5 个原生笔记工具 + turn/end 钩子,依赖 tools;
|
|
7
|
+
* - 技能半边 src/host/skills.js:把随包的 write-notes-like-deepseek 挂成 agent 技能,
|
|
8
|
+
* 依赖宿主自带的 @deepseek-ai/dsh-skill-filesystem(缺了就静默跳过)。
|
|
9
|
+
*
|
|
10
|
+
* 两半依赖的服务不同,所以入口不整体 inject 任一服务:没有 webServer 的 profile 里
|
|
11
|
+
* 看板静默缺席,没有 tools 的 profile 里钩子静默缺席,都不会让整条插件行抛错。
|
|
12
|
+
*
|
|
13
|
+
* 注意 applyNotes 拿到的是**插件自己的 ctx**(不是 ctx.inject 的子 scope):它的
|
|
14
|
+
* session/event 监听必须挂在插件这一层,事件才冒泡得到它。tools 的就绪判断由
|
|
15
|
+
* applyNotes 内部负责。
|
|
16
|
+
*/
|
|
17
|
+
import { applyBoard } from './src/host/board.js'
|
|
18
|
+
import { applyNotes, NotesConfig } from './src/host/notes.js'
|
|
19
|
+
import { mountNotesSkills } from './src/host/skills.js'
|
|
20
|
+
|
|
21
|
+
export const name = 'dsh-notes-board'
|
|
22
|
+
export const Config = NotesConfig
|
|
23
|
+
|
|
24
|
+
export function apply(ctx, config = {}) {
|
|
25
|
+
ctx.inject(['webServer'], (sub) => applyBoard(sub))
|
|
26
|
+
applyNotes(ctx, config)
|
|
27
|
+
mountNotesSkills(ctx)
|
|
28
|
+
}
|
package/locale/en.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "Notes Board",
|
|
4
|
+
"description": "Three things in one plugin: a Notes Board sidebar panel that renders each workspace's .agents/notes decision records (architecture baseline, evolution timeline, pitfalls, decision list, plus the five-section detail drawer); five native tools (note_search / note_read / note_write / note_verify / note_skip) with an end-of-turn hook that forces every turn touching code to either write a note or explicitly skip; and the bundled write-notes-like-deepseek skill, mounted for the agent."
|
|
5
|
+
}
|
|
6
|
+
}
|
package/locale/zh.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "笔记看板",
|
|
4
|
+
"description": "一个插件三件事:左侧栏「笔记看板」面板渲染各工作区 .agents/notes 的工程决策笔记(架构基线、演进时间线、避坑智库、决策清单四板块 + 抽屉五节);note_search / note_read / note_write / note_verify / note_skip 五个原生工具与 turn/end 钩子,保证改了代码的轮次结束时必须落一篇笔记或显式 skip;并把随包的 write-notes-like-deepseek 技能挂给 agent。"
|
|
5
|
+
}
|
|
6
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,81 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-notes-board",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"description": "
|
|
6
|
-
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "笔记看板:面板渲染各工作区 .agents/notes 的工程决策笔记(四板块 + 抽屉五节),同一插件还提供 note_search/note_read/note_write/note_verify/note_skip 五个原生工具、turn/end 钩子,以及随包的 write-notes-like-deepseek 技能。",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "scccy",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"dsh",
|
|
10
|
+
"dsh-plugin",
|
|
11
|
+
"deepseek-harness",
|
|
12
|
+
"agent-notes",
|
|
13
|
+
"notes",
|
|
14
|
+
"decision-log"
|
|
15
|
+
],
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/scccy/dsh-notes-board.git"
|
|
19
|
+
},
|
|
20
|
+
"homepage": "https://github.com/scccy/dsh-notes-board#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/scccy/dsh-notes-board/issues"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=22.18"
|
|
26
|
+
},
|
|
27
|
+
"exports": {
|
|
28
|
+
".": "./index.js",
|
|
29
|
+
"./client": "./client.js",
|
|
30
|
+
"./core": "./src/core/notes.js",
|
|
31
|
+
"./locale/*.json": "./locale/*.json",
|
|
32
|
+
"./icon": "./icon.svg"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"index.js",
|
|
36
|
+
"client.js",
|
|
37
|
+
"cordis.patch.yml",
|
|
38
|
+
"locale/*.json",
|
|
39
|
+
"icon.svg",
|
|
40
|
+
"src/**",
|
|
41
|
+
"scripts/**",
|
|
42
|
+
"vendor/**",
|
|
43
|
+
"README.md",
|
|
44
|
+
"LICENSE",
|
|
45
|
+
"NOTICE.md"
|
|
46
|
+
],
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "node scripts/build-client.mjs",
|
|
49
|
+
"test": "node scripts/test-board.mjs && node --import ./test/stubs/register.mjs test/dsh-plugin.mjs",
|
|
50
|
+
"test:board": "node scripts/test-board.mjs",
|
|
51
|
+
"test:hooks": "node --import ./test/stubs/register.mjs test/dsh-plugin.mjs"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@deepseek-ai/dsh-llm": "*",
|
|
55
|
+
"@deepseek-ai/dsh-skill-filesystem": "*",
|
|
56
|
+
"@deepseek-ai/dsh-tools": "*",
|
|
57
|
+
"@deepseek-ai/schemastery": "*"
|
|
58
|
+
},
|
|
59
|
+
"peerDependenciesMeta": {
|
|
60
|
+
"@deepseek-ai/dsh-llm": {
|
|
61
|
+
"optional": true
|
|
62
|
+
},
|
|
63
|
+
"@deepseek-ai/dsh-skill-filesystem": {
|
|
64
|
+
"optional": true
|
|
65
|
+
},
|
|
66
|
+
"@deepseek-ai/dsh-tools": {
|
|
67
|
+
"optional": true
|
|
68
|
+
},
|
|
69
|
+
"@deepseek-ai/schemastery": {
|
|
70
|
+
"optional": true
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"dsh": {
|
|
74
|
+
"bundle": {
|
|
75
|
+
"patch": "./cordis.patch.yml"
|
|
76
|
+
},
|
|
77
|
+
"client": {
|
|
78
|
+
"platform": "web"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* 把原版看板模板(vendor/board.html)改造成能挂在 Harness 面板里的形态,
|
|
4
|
+
* 与 src/panel.js 拼成 client.js。
|
|
5
|
+
*
|
|
6
|
+
* 设计原则:原版逻辑一行都不重写,只做「作用域改写 + 宿主接管数据源」两类机械补丁。
|
|
7
|
+
* 上游更新时:替换 vendor/board.html → 重跑本脚本;补丁数量断言会立刻告诉你哪里变了。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
11
|
+
import { dirname, resolve } from 'node:path'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
|
|
14
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
15
|
+
const src = readFileSync(resolve(ROOT, 'vendor/board.html'), 'utf8')
|
|
16
|
+
const shell = readFileSync(resolve(ROOT, 'src/panel.js'), 'utf8')
|
|
17
|
+
|
|
18
|
+
function must(condition, message) {
|
|
19
|
+
if (!condition) throw new Error('build failed: ' + message)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function replaceCounted(text, from, to, expected, label) {
|
|
23
|
+
const count = text.split(from).length - 1
|
|
24
|
+
must(count === expected, `${label}: 期望 ${expected} 处 "${from}",实际 ${count} 处(上游可能已变)`)
|
|
25
|
+
return text.split(from).join(to)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/* ── 1. 切出 CSS / 正文 / 脚本 / 字体 ─────────────────────── */
|
|
29
|
+
|
|
30
|
+
const cssMatch = /<style[^>]*>([\s\S]*?)<\/style>/.exec(src)
|
|
31
|
+
must(cssMatch, '找不到 <style> 块')
|
|
32
|
+
let css = cssMatch[1]
|
|
33
|
+
|
|
34
|
+
const bodyStart = src.indexOf('<body>')
|
|
35
|
+
const bodyEnd = src.indexOf('<script')
|
|
36
|
+
must(bodyStart >= 0 && bodyEnd > bodyStart, '找不到 <body> … <script> 之间的正文')
|
|
37
|
+
const bodyHtml = src.slice(bodyStart + '<body>'.length, bodyEnd).trim()
|
|
38
|
+
|
|
39
|
+
const scripts = [...src.matchAll(/<script[^>]*>([\s\S]*?)<\/script>/g)].map((m) => m[1])
|
|
40
|
+
const script = scripts.find((s) => s.length > 5000)
|
|
41
|
+
must(script, '找不到看板主脚本')
|
|
42
|
+
|
|
43
|
+
const fontMatch = /<link href="https:\/\/fonts\.googleapis\.com[^>]*>/.exec(src)
|
|
44
|
+
const font = fontMatch ? fontMatch[0] : ''
|
|
45
|
+
|
|
46
|
+
/* ── 2. CSS::root → :host,抽屉定位从 fixed 收到面板内 ───── */
|
|
47
|
+
|
|
48
|
+
must((css.match(/:root\s*\{/g) || []).length === 1, 'CSS 里 :root 应该只有一处')
|
|
49
|
+
css = css.replace(
|
|
50
|
+
/:root\s*\{/,
|
|
51
|
+
':host{position:relative;display:block;height:100%;overflow:hidden;',
|
|
52
|
+
)
|
|
53
|
+
css = replaceCounted(css, 'position: fixed', 'position: absolute', 2, 'CSS 抽屉定位')
|
|
54
|
+
css +=
|
|
55
|
+
'\n/* ── 宿主注入 ──────────────────────────────────────────── */\n' +
|
|
56
|
+
'/* 原版靠浏览器默认白底,面板里没有 body,这里补上 */\n' +
|
|
57
|
+
'.nbx-stage{height:100%;overflow:auto;background:var(--bg-base);color:var(--text-primary);}\n' +
|
|
58
|
+
'/* 原版顶栏按整页宽度设计,窄面板里会被压成竖排:改成可换行 + 子项不压缩 */\n' +
|
|
59
|
+
'header.hud-topbar{flex-wrap:wrap;height:auto;min-height:56px;padding:8px 16px;gap:12px;row-gap:8px;}\n' +
|
|
60
|
+
'header.hud-topbar>*{flex-shrink:0;}\n' +
|
|
61
|
+
'.brand-text,nav.deck-tabs,.deck-tab,.sync-beacon,.telemetry-chip{white-space:nowrap;}\n' +
|
|
62
|
+
'.hud-grow{flex:1 1 auto;min-width:0;}\n' +
|
|
63
|
+
'.hud-search{width:220px;flex:0 0 auto;}\n' +
|
|
64
|
+
'/* 原版聚焦时把输入框撑到 320px,溢出 260px 容器、盖住右侧同步状态 */\n' +
|
|
65
|
+
'.hud-search input:focus{width:100%;}\n' +
|
|
66
|
+
'.onboarding-deck{display:none !important;}\n'
|
|
67
|
+
|
|
68
|
+
/* ── 3. JS:作用域改写 ───────────────────────────────────── */
|
|
69
|
+
|
|
70
|
+
// 先摘掉「自动读取浏览器授权目录」那段启动逻辑,数据改由宿主注入。
|
|
71
|
+
const bootStart = script.indexOf('if (window.__INLINE_DATA__')
|
|
72
|
+
const bootEnd = script.indexOf("window.addEventListener('focus'")
|
|
73
|
+
must(bootStart > 0 && bootEnd > bootStart, '找不到原版启动数据块')
|
|
74
|
+
let code =
|
|
75
|
+
script.slice(0, bootStart) +
|
|
76
|
+
'// 数据由宿主注入:React 侧拿到 .agents/notes 原文后调用 ingestFiles()\n ' +
|
|
77
|
+
script.slice(bootEnd)
|
|
78
|
+
|
|
79
|
+
code = replaceCounted(code, 'document.getElementById(', 'byId(', 51, 'getElementById 作用域改写')
|
|
80
|
+
code = replaceCounted(code, 'document.querySelectorAll(', 'root.querySelectorAll(', 3, 'querySelectorAll 作用域改写')
|
|
81
|
+
code = replaceCounted(code, 'document.querySelector(', 'root.querySelector(', 1, 'querySelector 作用域改写')
|
|
82
|
+
code = replaceCounted(code, 'window.addEventListener(', 'onWin(', 3, 'window 事件接管')
|
|
83
|
+
code = replaceCounted(code, 'document.addEventListener(', 'onRoot(', 2, 'document 事件接管')
|
|
84
|
+
code = replaceCounted(
|
|
85
|
+
code,
|
|
86
|
+
"onRoot('DOMContentLoaded', startMission);",
|
|
87
|
+
'/* 启动由宿主 mountBoard() 调用 startMission() */',
|
|
88
|
+
1,
|
|
89
|
+
'移除 DOMContentLoaded 自启动',
|
|
90
|
+
)
|
|
91
|
+
code = replaceCounted(code, 'location.hash', 'panelRoute.hash', 5, 'hash 路由收进面板')
|
|
92
|
+
code = replaceCounted(code, 'history.replaceState', 'panelRoute.replaceState', 3, 'history 收进面板')
|
|
93
|
+
|
|
94
|
+
// 懒渲染:原版一次把四个视图全渲染出来,1300 篇时时间线单次产出近 1MB HTML。
|
|
95
|
+
// 改成只渲染当前 tab,切 tab 时再按需渲染 —— 首屏从四份降到一份。
|
|
96
|
+
code = replaceCounted(
|
|
97
|
+
code,
|
|
98
|
+
`function renderInterface() {
|
|
99
|
+
renderBaseline();
|
|
100
|
+
renderTimeline();
|
|
101
|
+
renderAlternatives();
|
|
102
|
+
renderDirectory();
|
|
103
|
+
}`,
|
|
104
|
+
`function renderView(view) {
|
|
105
|
+
if (view === 'timeline') renderTimeline();
|
|
106
|
+
else if (view === 'alternatives') renderAlternatives();
|
|
107
|
+
else if (view === 'directory') renderDirectory();
|
|
108
|
+
else renderBaseline();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function renderInterface() {
|
|
112
|
+
renderView(Core.activeTab);
|
|
113
|
+
}`,
|
|
114
|
+
1,
|
|
115
|
+
'视图懒渲染',
|
|
116
|
+
)
|
|
117
|
+
code = replaceCounted(
|
|
118
|
+
code,
|
|
119
|
+
' Core.activeTab = viewId;',
|
|
120
|
+
' Core.activeTab = viewId;\n renderView(viewId);',
|
|
121
|
+
1,
|
|
122
|
+
'切换 tab 时补渲染',
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
// KPI 卡片:原版是内联 onclick="routeToListFilter(...)"。内联处理器在「全局作用域」求值,
|
|
126
|
+
// 而改造后所有原版函数都收进了 factory 闭包 → 每点一次必抛 ReferenceError: routeToListFilter is not defined。
|
|
127
|
+
// 机械补丁:标记换成 data-kpi-filter,点击交给下面那段已有的 root 委托监听。
|
|
128
|
+
code = replaceCounted(
|
|
129
|
+
code,
|
|
130
|
+
'<div class="kpi-instrument" onclick="routeToListFilter(\'${escapeHtml(k.filter)}\')">',
|
|
131
|
+
'<div class="kpi-instrument" data-kpi-filter="${escapeHtml(k.filter)}">',
|
|
132
|
+
1,
|
|
133
|
+
'KPI 卡片内联 onclick → data-kpi-filter',
|
|
134
|
+
)
|
|
135
|
+
code = replaceCounted(
|
|
136
|
+
code,
|
|
137
|
+
`onRoot('click', e => {
|
|
138
|
+
const nodeTrigger = e.target.closest('[data-node-id]');`,
|
|
139
|
+
`onRoot('click', e => {
|
|
140
|
+
const kpiTrigger = e.target.closest('[data-kpi-filter]');
|
|
141
|
+
if (kpiTrigger) {
|
|
142
|
+
const kpiFilter = kpiTrigger.getAttribute('data-kpi-filter');
|
|
143
|
+
if (kpiFilter) routeToListFilter(kpiFilter);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const nodeTrigger = e.target.closest('[data-node-id]');`,
|
|
147
|
+
1,
|
|
148
|
+
'KPI 卡片点击委托',
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// 兜底断言:内联事件处理器在全局作用域求值,而本文件所有函数都在 factory 闭包里,
|
|
152
|
+
// 只要再出现一个就跟 routeToListFilter 一样必抛。上游新增一定要在这里被拦住。
|
|
153
|
+
must(
|
|
154
|
+
!/\son(?:click|change|input|keydown|keyup|keypress|submit|load|focus|blur|mouseover|mouseout)\s*=/.test(code),
|
|
155
|
+
'注入脚本里仍有内联事件处理器,改造成模块闭包后会抛 ReferenceError,请用 data-属性 + 事件委托替代',
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
/* ── 4. 拼装 client.js ───────────────────────────────────── */
|
|
159
|
+
|
|
160
|
+
let out = shell
|
|
161
|
+
for (const [token, value] of [
|
|
162
|
+
['/*__BOARD_CSS__*/ \'\'', `/*__BOARD_CSS__*/ ${JSON.stringify(css)}`],
|
|
163
|
+
['/*__BOARD_HTML__*/ \'\'', `/*__BOARD_HTML__*/ ${JSON.stringify(bodyHtml)}`],
|
|
164
|
+
['/*__BOARD_FONT__*/ \'\'', `/*__BOARD_FONT__*/ ${JSON.stringify(font)}`],
|
|
165
|
+
['/*__BOARD_SCRIPT__*/', code],
|
|
166
|
+
]) {
|
|
167
|
+
must(out.includes(token), `src/panel.js 缺少占位符 ${token.slice(0, 24)}…`)
|
|
168
|
+
out = out.replace(token, () => value)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
writeFileSync(resolve(ROOT, 'client.js'), out, 'utf8')
|
|
172
|
+
console.log(
|
|
173
|
+
`ok: client.js ${(out.length / 1024).toFixed(1)}KB(css ${(css.length / 1024).toFixed(1)}KB / ` +
|
|
174
|
+
`html ${(bodyHtml.length / 1024).toFixed(1)}KB / js ${(code.length / 1024).toFixed(1)}KB)`,
|
|
175
|
+
)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import vm from 'node:vm'
|
|
4
|
+
|
|
5
|
+
import { dirname, resolve } from 'node:path'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
8
|
+
const SRC = resolve(ROOT, 'client.js')
|
|
9
|
+
|
|
10
|
+
// 合并后仓库里有两份同源看板模板:构建源 vendor/board.html,和技能资产副本
|
|
11
|
+
// vendor/write-notes-like-deepseek/skills/write-notes-like-deepseek/assets/agent-notes-board.html
|
|
12
|
+
// (后者被上游 test-skill-install.mjs 的存在性断言依赖)。它们必须逐字一致,
|
|
13
|
+
// 否则上游更新只换了一份,面板和技能产物就悄悄分叉了。
|
|
14
|
+
const BUILD_SOURCE = resolve(ROOT, 'vendor', 'board.html')
|
|
15
|
+
const SKILL_ASSET = resolve(ROOT, 'vendor', 'write-notes-like-deepseek', 'skills', 'write-notes-like-deepseek', 'assets', 'agent-notes-board.html')
|
|
16
|
+
const sha = (p) => createHash('sha256').update(readFileSync(p)).digest('hex')
|
|
17
|
+
if (sha(BUILD_SOURCE) !== sha(SKILL_ASSET)) {
|
|
18
|
+
throw new Error(`vendor 里两份 agent-notes-board.html 不一致:${BUILD_SOURCE} vs ${SKILL_ASSET}`)
|
|
19
|
+
}
|
|
20
|
+
console.log(' ok 两份看板模板逐字一致(sha256 ' + sha(BUILD_SOURCE).slice(0, 12) + '…)')
|
|
21
|
+
|
|
22
|
+
const cache = new Map()
|
|
23
|
+
function makeEl(id) {
|
|
24
|
+
return {
|
|
25
|
+
id, style: {}, dataset: {}, children: [], innerHTML: '', textContent: '', value: '', className: '',
|
|
26
|
+
classList: { add() {}, remove() {}, toggle() {}, contains() { return false } },
|
|
27
|
+
appendChild() {}, removeChild() {}, append() {},
|
|
28
|
+
addEventListener() {}, removeEventListener() {},
|
|
29
|
+
querySelector(sel) { return bySel(sel) }, querySelectorAll() { return [] },
|
|
30
|
+
getAttribute() { return null }, setAttribute() {}, closest() { return null },
|
|
31
|
+
focus() {}, scrollIntoView() {}, insertAdjacentHTML() {}, remove() {},
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const byId = (id) => { if (!cache.has(id)) cache.set(id, makeEl(id)); return cache.get(id) }
|
|
35
|
+
const bySel = (sel) => { const k = 'sel:' + sel; if (!cache.has(k)) cache.set(k, makeEl(k)); return cache.get(k) }
|
|
36
|
+
|
|
37
|
+
const listeners = []
|
|
38
|
+
const shadow = {
|
|
39
|
+
getElementById: byId,
|
|
40
|
+
querySelector: bySel,
|
|
41
|
+
querySelectorAll: () => [],
|
|
42
|
+
addEventListener: (t, fn) => listeners.push([t, fn]),
|
|
43
|
+
removeEventListener: () => {},
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const sandbox = {
|
|
47
|
+
window: { __ModuleLoader__: { load(d) { sandbox.__def = d } }, addEventListener() {}, removeEventListener() {}, setInterval: () => 0, clearInterval() {} },
|
|
48
|
+
console, setTimeout, clearTimeout,
|
|
49
|
+
setInterval: () => 0, clearInterval() {},
|
|
50
|
+
location: { hash: '' }, history: { replaceState() {} },
|
|
51
|
+
document: { activeElement: null, hasFocus: () => true, addEventListener() {}, createElement: () => makeEl('tmp') },
|
|
52
|
+
alert: (m) => console.log('[alert]', m),
|
|
53
|
+
}
|
|
54
|
+
sandbox.globalThis = sandbox
|
|
55
|
+
vm.createContext(sandbox)
|
|
56
|
+
vm.runInContext(readFileSync(SRC, 'utf8'), sandbox, { filename: SRC })
|
|
57
|
+
|
|
58
|
+
const ReactStub = { createElement: () => null, useState: () => [null, () => {}], useEffect: () => {}, useRef: () => ({ current: null }), useCallback: (f) => f }
|
|
59
|
+
const plugin = sandbox.__def.factory((name) => { if (name === 'react') return ReactStub; throw new Error('unexpected require ' + name) })
|
|
60
|
+
plugin.apply({ slots: { inject(_n, fn) { fn() }, register() { return () => {} } } })
|
|
61
|
+
|
|
62
|
+
const A = `# Agent Note: 能力 seam 三权分立
|
|
63
|
+
|
|
64
|
+
Status: implemented
|
|
65
|
+
|
|
66
|
+
## Problem
|
|
67
|
+
|
|
68
|
+
各项能力关注点以不同速率变化,换沙箱实现时工具 Schema 频发破坏性变更。
|
|
69
|
+
|
|
70
|
+
## Decision
|
|
71
|
+
|
|
72
|
+
拆成 Service Definition / Provider / Consumer 三者独立演进。
|
|
73
|
+
|
|
74
|
+
## Alternatives considered
|
|
75
|
+
|
|
76
|
+
### 始终合并各角色
|
|
77
|
+
否决,重新耦合了独立变化的关注点。
|
|
78
|
+
|
|
79
|
+
## Consequences
|
|
80
|
+
|
|
81
|
+
包数量增加;换来后端替换零破坏。
|
|
82
|
+
`
|
|
83
|
+
const B = `# Agent Note: 工具输出规范化
|
|
84
|
+
|
|
85
|
+
Status: implemented
|
|
86
|
+
|
|
87
|
+
## Problem
|
|
88
|
+
|
|
89
|
+
工具直接产出模型可读文本,编程式调用拿不到强类型对象。
|
|
90
|
+
|
|
91
|
+
## Decision
|
|
92
|
+
|
|
93
|
+
每个工具声明 Canonical Output,执行期给强类型 DTO。
|
|
94
|
+
|
|
95
|
+
## Alternatives considered
|
|
96
|
+
|
|
97
|
+
### 双轨返回值
|
|
98
|
+
否决,双重契约增加心智负担。
|
|
99
|
+
|
|
100
|
+
## Consequences
|
|
101
|
+
|
|
102
|
+
PTC 模式无需反解析字符串。参见 [能力 seam](../architecture/2026-10-01-a.md)。
|
|
103
|
+
`
|
|
104
|
+
const C = `# Agent Note: 笔记看板接入 GUI
|
|
105
|
+
|
|
106
|
+
Status: proposed
|
|
107
|
+
|
|
108
|
+
## Problem
|
|
109
|
+
|
|
110
|
+
笔记只能翻目录看。
|
|
111
|
+
|
|
112
|
+
## Proposal
|
|
113
|
+
|
|
114
|
+
左侧栏加一个原生面板。
|
|
115
|
+
|
|
116
|
+
## Alternatives considered
|
|
117
|
+
|
|
118
|
+
无
|
|
119
|
+
|
|
120
|
+
## Acceptance criteria
|
|
121
|
+
|
|
122
|
+
能在面板里切换工作区。
|
|
123
|
+
|
|
124
|
+
## Risks
|
|
125
|
+
|
|
126
|
+
只读路由的权限边界。
|
|
127
|
+
`
|
|
128
|
+
const D = `# Agent Note: 用 iframe 复用原版看板
|
|
129
|
+
|
|
130
|
+
Status: rejected
|
|
131
|
+
|
|
132
|
+
## Problem
|
|
133
|
+
|
|
134
|
+
想直接嵌原版 HTML。
|
|
135
|
+
|
|
136
|
+
## Proposal
|
|
137
|
+
|
|
138
|
+
Host 提供 HTML,面板 iframe 引用。
|
|
139
|
+
|
|
140
|
+
## Alternatives considered
|
|
141
|
+
|
|
142
|
+
### 内联移植
|
|
143
|
+
否决,工作量大。
|
|
144
|
+
|
|
145
|
+
## Risks
|
|
146
|
+
|
|
147
|
+
拿不到宿主主题与 locale。
|
|
148
|
+
`
|
|
149
|
+
|
|
150
|
+
const files = [
|
|
151
|
+
{ path: 'implemented/architecture/2026-10-01-a.md', raw: A },
|
|
152
|
+
{ path: 'implemented/feature/2026-10-15-b.md', raw: B },
|
|
153
|
+
{ path: 'proposed/process/2026-11-01-c.md', raw: C },
|
|
154
|
+
{ path: 'rejected/bug-fix/2026-11-02-d.md', raw: D },
|
|
155
|
+
]
|
|
156
|
+
|
|
157
|
+
const board = plugin.mountBoard(shadow, {})
|
|
158
|
+
const count = board.ingest(files)
|
|
159
|
+
console.log('ingest 数量:', count)
|
|
160
|
+
console.log('总计数 chip:', byId('chip-total-count').textContent)
|
|
161
|
+
console.log('统计条:', byId('beacon-status').textContent)
|
|
162
|
+
|
|
163
|
+
const probe = (label, id, needle) => {
|
|
164
|
+
const html = byId(id).innerHTML || ''
|
|
165
|
+
console.log(` ${html.includes(needle) ? 'ok ' : 'MISS'} ${label} (${id}, ${html.length}B)`)
|
|
166
|
+
}
|
|
167
|
+
probe('承重墙含被引用笔记', 'pillars-vault', '能力 seam')
|
|
168
|
+
probe('分类阵列非空', 'matrix-chambers', '架构')
|
|
169
|
+
console.log(' 首屏未渲染时间线(懒渲染生效):', (byId('chrono-track').innerHTML || '').length === 0)
|
|
170
|
+
|
|
171
|
+
board.switchTab('timeline')
|
|
172
|
+
probe('时间线非空', 'chrono-track', '2026')
|
|
173
|
+
board.switchTab('alternatives')
|
|
174
|
+
probe('避坑智库含否决项', 'pitfalls-grid', 'iframe')
|
|
175
|
+
board.switchTab('directory')
|
|
176
|
+
probe('决策清单含标题', 'matrix-rows-stream', '能力 seam 三权分立')
|
|
177
|
+
|
|
178
|
+
console.log('注册的根监听:', listeners.map(([t]) => t))
|
|
179
|
+
// 抽屉:模拟点击某行 -> openConsole
|
|
180
|
+
const clickHandler = (listeners.find(([t]) => t === 'click') || [])[1]
|
|
181
|
+
if (!clickHandler) throw new Error('没有拿到 click 代理')
|
|
182
|
+
clickHandler({
|
|
183
|
+
target: { closest: (sel) => (sel === '[data-node-id]' ? { getAttribute: () => 'implemented/architecture/2026-10-01-a.md' } : null) },
|
|
184
|
+
})
|
|
185
|
+
probe('抽屉-背景', 'dr-body-problem', '换沙箱实现')
|
|
186
|
+
probe('抽屉-裁定', 'dr-body-decision', 'Service Definition')
|
|
187
|
+
probe('抽屉-备选', 'dr-body-alt', '始终合并各角色')
|
|
188
|
+
probe('抽屉-代价', 'dr-body-consequences', '零破坏')
|
|
189
|
+
console.log('抽屉标题:', byId('dr-headline').textContent)
|
|
190
|
+
console.log('抽屉血缘:', (byId('dr-lineage-stream').innerHTML || '').length, 'B')
|
|
191
|
+
|
|
192
|
+
// KPI 卡片:原版用内联 onclick 调 routeToListFilter,收进 factory 闭包后点击必抛
|
|
193
|
+
// ReferenceError。现在改成 data-kpi-filter + 事件委托,这里模拟一次点击看筛选是否生效。
|
|
194
|
+
clickHandler({
|
|
195
|
+
target: { closest: (sel) => (sel === '[data-kpi-filter]' ? { getAttribute: () => 'rejected' } : null) },
|
|
196
|
+
})
|
|
197
|
+
const kpiHtml = byId('matrix-rows-stream').innerHTML || ''
|
|
198
|
+
const kpiOk = kpiHtml.includes('用 iframe 复用原版看板') && !kpiHtml.includes('能力 seam')
|
|
199
|
+
console.log(` ${kpiOk ? 'ok ' : 'MISS'} KPI 点击 → 决策清单只剩「否决」项(${kpiHtml.length}B)`)
|
|
200
|
+
|
|
201
|
+
board.dispose()
|
|
202
|
+
console.log('dispose ok')
|