dsh-notes-board 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/NOTICE.md +14 -0
- package/README.md +61 -2
- package/client.js +1253 -0
- package/cordis.patch.yml +6 -0
- package/icon.svg +16 -0
- package/index.js +185 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +55 -3
- package/scripts/build-client.mjs +175 -0
- package/scripts/test-board.mjs +188 -0
- package/src/panel.js +294 -0
- package/vendor/board.html +1779 -0
package/src/panel.js
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/* dsh-notes-board · 面板外壳(源文件,请在 src/panel.js 里改)
|
|
2
|
+
*
|
|
3
|
+
* client.js 是生成物:scripts/build-client.mjs 读取 vendor/board.html(原版看板模板),
|
|
4
|
+
* 把它的 <style> / <body> / <script> 注入本文件的三处占位符后输出。
|
|
5
|
+
* 改造点全部集中在 build-client.mjs 的 PATCHES 列表里,原版逻辑一行没重写。
|
|
6
|
+
*/
|
|
7
|
+
window.__ModuleLoader__.load({
|
|
8
|
+
id: 'dsh-notes-board',
|
|
9
|
+
factory(require) {
|
|
10
|
+
const React = require('react')
|
|
11
|
+
const h = React.createElement
|
|
12
|
+
const { useState, useEffect, useRef, useCallback } = React
|
|
13
|
+
|
|
14
|
+
const PANEL_ID = 'dsh-notes-board'
|
|
15
|
+
const API = '/api/dsh-notes-board'
|
|
16
|
+
const STORE_KEY = 'dsh-notes-board:workspace'
|
|
17
|
+
|
|
18
|
+
/* 由构建脚本注入:原版看板的 CSS / 正文骨架 / 外链字体 */
|
|
19
|
+
const BOARD_CSS = /*__BOARD_CSS__*/ ''
|
|
20
|
+
const BOARD_HTML = /*__BOARD_HTML__*/ ''
|
|
21
|
+
const BOARD_FONT = /*__BOARD_FONT__*/ ''
|
|
22
|
+
|
|
23
|
+
const SHELL_CSS = `
|
|
24
|
+
.nbx-shell{display:flex;flex-direction:column;height:100%;min-height:0;background:var(--dsw-alias-bg-base);color:var(--dsw-alias-label-primary);font-size:12px}
|
|
25
|
+
.nbx-bar{display:flex;align-items:center;gap:10px;padding:8px 14px;border-bottom:1px solid var(--dsw-alias-border-l1);flex:0 0 auto}
|
|
26
|
+
.nbx-title{font-size:13px;font-weight:650;letter-spacing:.2px}
|
|
27
|
+
.nbx-select{appearance:none;background:var(--dsw-alias-bg-layer-1);color:inherit;border:1px solid var(--dsw-alias-border-l1);border-radius:7px;padding:4px 24px 4px 9px;font-size:12px;font-family:inherit;cursor:pointer;max-width:260px}
|
|
28
|
+
.nbx-select:hover{border-color:var(--dsw-alias-border-l2)}
|
|
29
|
+
.nbx-btn{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);border:1px solid var(--dsw-alias-border-l1);border-radius:7px;padding:4px 9px;font-size:12px;font-family:inherit;cursor:pointer}
|
|
30
|
+
.nbx-btn:hover{color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l2)}
|
|
31
|
+
.nbx-btn[disabled]{opacity:.5;cursor:default}
|
|
32
|
+
.nbx-state{color:var(--dsw-alias-label-secondary);font-size:11.5px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
33
|
+
.nbx-err{color:var(--dsw-alias-state-error-primary)}
|
|
34
|
+
.nbx-grow{flex:1 1 auto}
|
|
35
|
+
.nbx-host{flex:1 1 auto;min-height:0;position:relative;display:block}
|
|
36
|
+
`
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* 把原版看板挂进一个 shadow root。
|
|
40
|
+
* root: ShadowRoot(原版脚本里的 document.getElementById / querySelector 全部被改写到它上面)
|
|
41
|
+
* host: { onRefresh?: () => void }
|
|
42
|
+
*/
|
|
43
|
+
function mountBoard(root, host) {
|
|
44
|
+
const cleanups = []
|
|
45
|
+
const byId = (id) => root.getElementById(id)
|
|
46
|
+
const onRoot = (type, fn, opts) => {
|
|
47
|
+
root.addEventListener(type, fn, opts)
|
|
48
|
+
cleanups.push(() => root.removeEventListener(type, fn, opts))
|
|
49
|
+
}
|
|
50
|
+
const onWin = (type, fn, opts) => {
|
|
51
|
+
window.addEventListener(type, fn, opts)
|
|
52
|
+
cleanups.push(() => window.removeEventListener(type, fn, opts))
|
|
53
|
+
}
|
|
54
|
+
const setInterval = (fn, ms) => {
|
|
55
|
+
const id = window.setInterval(fn, ms)
|
|
56
|
+
cleanups.push(() => window.clearInterval(id))
|
|
57
|
+
return id
|
|
58
|
+
}
|
|
59
|
+
const panelRoute = {
|
|
60
|
+
_h: '',
|
|
61
|
+
get hash() {
|
|
62
|
+
return this._h ? '#' + this._h : ''
|
|
63
|
+
},
|
|
64
|
+
replaceState(_state, _title, url) {
|
|
65
|
+
this._h = String(url || '').replace(/^#/, '')
|
|
66
|
+
},
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/* ===== 以下为原版看板脚本(构建时注入,未重写) ===== */
|
|
70
|
+
/*__BOARD_SCRIPT__*/
|
|
71
|
+
/* ===== 原版看板脚本结束 ===== */
|
|
72
|
+
|
|
73
|
+
/** 宿主把工作区的 .agents/notes 原文喂进来,走原版 parseNote / ingestDataset */
|
|
74
|
+
function ingestFiles(files) {
|
|
75
|
+
const notesMap = new Map()
|
|
76
|
+
for (const file of files || []) {
|
|
77
|
+
const cleanRel = String(file.path || '').replace(/\.zh\.md$/, '.md')
|
|
78
|
+
const parts = cleanRel.split('/')
|
|
79
|
+
if (parts.length >= 3 && LIFECYCLES.includes(parts[0])) notesMap.set(cleanRel, String(file.raw || ''))
|
|
80
|
+
}
|
|
81
|
+
const slugToId = new Map()
|
|
82
|
+
for (const rel of notesMap.keys()) {
|
|
83
|
+
const slug = rel.split('/').pop().replace(/\.md$/, '')
|
|
84
|
+
slugToId.set(slug, rel)
|
|
85
|
+
slugToId.set(rel, rel)
|
|
86
|
+
}
|
|
87
|
+
const notes = []
|
|
88
|
+
for (const [rel, text] of notesMap.entries()) notes.push(parseNote(text, rel, slugToId))
|
|
89
|
+
ingestDataset(notes)
|
|
90
|
+
return notes.length
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const boot = startMission()
|
|
95
|
+
if (boot && typeof boot.catch === 'function') {
|
|
96
|
+
boot.catch((error) => console.error('[dsh-notes-board] startMission failed', error))
|
|
97
|
+
}
|
|
98
|
+
} catch (error) {
|
|
99
|
+
console.error('[dsh-notes-board] startMission failed', error)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
ingest: ingestFiles,
|
|
104
|
+
/* 测试用:scripts/test-board.mjs 用它验证懒渲染 */
|
|
105
|
+
switchTab: switchDeckTab,
|
|
106
|
+
dispose() {
|
|
107
|
+
cleanups.splice(0).forEach((fn) => {
|
|
108
|
+
try {
|
|
109
|
+
fn()
|
|
110
|
+
} catch {
|
|
111
|
+
/* 卸载期单个清理失败不影响其余 */
|
|
112
|
+
}
|
|
113
|
+
})
|
|
114
|
+
},
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ── 面板 ─────────────────────────────────────────────── */
|
|
119
|
+
|
|
120
|
+
function NotesBoard() {
|
|
121
|
+
const [workspaces, setWorkspaces] = useState([])
|
|
122
|
+
const [selected, setSelected] = useState('')
|
|
123
|
+
const [state, setState] = useState('正在读取工作区清单…')
|
|
124
|
+
const [busy, setBusy] = useState(false)
|
|
125
|
+
const [failed, setFailed] = useState(false)
|
|
126
|
+
const hostRef = useRef(null)
|
|
127
|
+
const boardRef = useRef(null)
|
|
128
|
+
const abortRef = useRef(null)
|
|
129
|
+
|
|
130
|
+
const fetchJson = async (url, signal) => {
|
|
131
|
+
const response = await fetch(url, { cache: 'no-store', signal })
|
|
132
|
+
const payload = await response.json().catch(() => null)
|
|
133
|
+
if (!response.ok || !payload || payload.ok === false) {
|
|
134
|
+
throw new Error((payload && payload.message) || `请求失败 (${response.status})`)
|
|
135
|
+
}
|
|
136
|
+
return payload
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
const hostEl = hostRef.current
|
|
141
|
+
if (!hostEl) return undefined
|
|
142
|
+
const shadow = hostEl.shadowRoot || hostEl.attachShadow({ mode: 'open' })
|
|
143
|
+
shadow.innerHTML =
|
|
144
|
+
'<style>' + BOARD_CSS + '</style>' + BOARD_FONT + '<div class="nbx-stage">' + BOARD_HTML + '</div>'
|
|
145
|
+
let mounted = null
|
|
146
|
+
try {
|
|
147
|
+
mounted = mountBoard(shadow, {})
|
|
148
|
+
boardRef.current = mounted
|
|
149
|
+
} catch (error) {
|
|
150
|
+
console.error('[dsh-notes-board] mount failed', error)
|
|
151
|
+
setState('看板挂载失败:' + (error && error.message ? error.message : error))
|
|
152
|
+
setFailed(true)
|
|
153
|
+
}
|
|
154
|
+
return () => {
|
|
155
|
+
if (mounted) mounted.dispose()
|
|
156
|
+
boardRef.current = null
|
|
157
|
+
shadow.innerHTML = ''
|
|
158
|
+
}
|
|
159
|
+
}, [])
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
let alive = true
|
|
163
|
+
fetchJson(`${API}/workspaces`)
|
|
164
|
+
.then((data) => {
|
|
165
|
+
if (!alive) return
|
|
166
|
+
const list = data.workspaces || []
|
|
167
|
+
setWorkspaces(list)
|
|
168
|
+
const remembered = window.localStorage.getItem(STORE_KEY) || ''
|
|
169
|
+
const preferred = list.find((w) => w.id === remembered) || list.find((w) => w.hasNotes) || list[0]
|
|
170
|
+
setSelected(preferred ? preferred.id : '')
|
|
171
|
+
if (!preferred) setState('没有可用工作区')
|
|
172
|
+
})
|
|
173
|
+
.catch((error) => {
|
|
174
|
+
if (!alive) return
|
|
175
|
+
setFailed(true)
|
|
176
|
+
setState('读取工作区失败:' + (error.message || error))
|
|
177
|
+
})
|
|
178
|
+
return () => {
|
|
179
|
+
alive = false
|
|
180
|
+
}
|
|
181
|
+
}, [])
|
|
182
|
+
|
|
183
|
+
const load = useCallback((workspaceId) => {
|
|
184
|
+
if (!workspaceId) return
|
|
185
|
+
if (abortRef.current) abortRef.current.abort()
|
|
186
|
+
const controller = new AbortController()
|
|
187
|
+
abortRef.current = controller
|
|
188
|
+
setBusy(true)
|
|
189
|
+
setFailed(false)
|
|
190
|
+
setState('正在扫描 .agents/notes…')
|
|
191
|
+
fetchJson(`${API}/scan?workspace=${encodeURIComponent(workspaceId)}`, controller.signal)
|
|
192
|
+
.then((data) => {
|
|
193
|
+
const count = boardRef.current ? boardRef.current.ingest(data.files || []) : 0
|
|
194
|
+
const label = (data.workspace && data.workspace.title) || '工作区'
|
|
195
|
+
setBusy(false)
|
|
196
|
+
const cut = data.truncated ? `(已达安全阀 ${data.safetyLimit} 篇,结果被截断)` : ''
|
|
197
|
+
setState(data.exists ? `${label} · ${count} 篇笔记${cut}` : `${label} 下还没有 .agents/notes`)
|
|
198
|
+
setFailed(!!data.truncated)
|
|
199
|
+
const shadow = hostRef.current && hostRef.current.shadowRoot
|
|
200
|
+
const beacon = shadow && shadow.getElementById('beacon-status')
|
|
201
|
+
const radar = shadow && shadow.getElementById('radar-indicator')
|
|
202
|
+
if (beacon) beacon.textContent = data.exists ? `宿主直连 · ${count} 篇` : '该工作区没有笔记'
|
|
203
|
+
if (radar) radar.className = data.exists ? 'radar-dot active' : 'radar-dot'
|
|
204
|
+
})
|
|
205
|
+
.catch((error) => {
|
|
206
|
+
if (error && error.name === 'AbortError') return
|
|
207
|
+
setBusy(false)
|
|
208
|
+
setFailed(true)
|
|
209
|
+
setState('扫描失败:' + (error.message || error))
|
|
210
|
+
})
|
|
211
|
+
}, [])
|
|
212
|
+
|
|
213
|
+
useEffect(() => {
|
|
214
|
+
if (!selected) return
|
|
215
|
+
window.localStorage.setItem(STORE_KEY, selected)
|
|
216
|
+
load(selected)
|
|
217
|
+
}, [selected, load])
|
|
218
|
+
|
|
219
|
+
useEffect(() => () => abortRef.current && abortRef.current.abort(), [])
|
|
220
|
+
|
|
221
|
+
return h(
|
|
222
|
+
'div',
|
|
223
|
+
{ className: 'nbx-shell' },
|
|
224
|
+
h('style', null, SHELL_CSS),
|
|
225
|
+
h(
|
|
226
|
+
'div',
|
|
227
|
+
{ className: 'nbx-bar' },
|
|
228
|
+
h('span', { className: 'nbx-title' }, '笔记看板'),
|
|
229
|
+
h(
|
|
230
|
+
'select',
|
|
231
|
+
{
|
|
232
|
+
className: 'nbx-select',
|
|
233
|
+
value: selected,
|
|
234
|
+
onChange: (event) => setSelected(event.target.value),
|
|
235
|
+
},
|
|
236
|
+
workspaces.length
|
|
237
|
+
? workspaces.map((w) =>
|
|
238
|
+
h('option', { key: w.id, value: w.id }, `${w.title}${w.hasNotes ? '' : '(无笔记)'}`),
|
|
239
|
+
)
|
|
240
|
+
: h('option', { value: '' }, '没有可用工作区'),
|
|
241
|
+
),
|
|
242
|
+
h('button', {
|
|
243
|
+
className: 'nbx-btn',
|
|
244
|
+
disabled: !selected || busy,
|
|
245
|
+
onClick: () => load(selected),
|
|
246
|
+
}, busy ? '刷新中…' : '刷新'),
|
|
247
|
+
h('span', { className: 'nbx-grow' }),
|
|
248
|
+
h('span', { className: 'nbx-state' + (failed ? ' nbx-err' : '') }, state),
|
|
249
|
+
),
|
|
250
|
+
h('div', { className: 'nbx-host', ref: hostRef }),
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* ── 侧栏图标 ─────────────────────────────────────────── */
|
|
255
|
+
|
|
256
|
+
function PanelIcon(props) {
|
|
257
|
+
const size = (props && props.size) || 20
|
|
258
|
+
const active = !!(props && props.active)
|
|
259
|
+
return h(
|
|
260
|
+
'svg',
|
|
261
|
+
{
|
|
262
|
+
width: size,
|
|
263
|
+
height: size,
|
|
264
|
+
viewBox: '0 0 24 24',
|
|
265
|
+
fill: 'none',
|
|
266
|
+
stroke: 'currentColor',
|
|
267
|
+
strokeWidth: active ? 2 : 1.7,
|
|
268
|
+
strokeLinecap: 'round',
|
|
269
|
+
strokeLinejoin: 'round',
|
|
270
|
+
'aria-hidden': true,
|
|
271
|
+
style: { display: 'block' },
|
|
272
|
+
},
|
|
273
|
+
h('path', { d: 'M6 3.5h9.5L19 7v13.5H6z' }),
|
|
274
|
+
h('path', { d: 'M15 3.5V7h4' }),
|
|
275
|
+
h('path', { d: 'M9 11h7M9 14.5h7M9 18h4' }),
|
|
276
|
+
)
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return {
|
|
280
|
+
inject: ['slots'],
|
|
281
|
+
/* 测试用出口:scripts/test-board.mjs 直接拿它跑桩 DOM 冒烟 */
|
|
282
|
+
mountBoard,
|
|
283
|
+
apply(ctx) {
|
|
284
|
+
ctx.slots.inject('main', () => ctx.slots.register({ name: 'main', key: PANEL_ID }, NotesBoard))
|
|
285
|
+
ctx.slots.inject('sidebar.panellist', () =>
|
|
286
|
+
ctx.slots.register(
|
|
287
|
+
{ name: 'sidebar.panellist', id: PANEL_ID, order: 20, label: '笔记看板' },
|
|
288
|
+
PanelIcon,
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
},
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
})
|