dsh-project-deck 0.0.0-stage → 0.1.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 +21 -0
- package/README.md +111 -2
- package/backend/__pycache__/panel.cpython-314.pyc +0 -0
- package/backend/deck +1765 -0
- package/backend/deck-node +188 -0
- package/backend/deck-node.js +152 -0
- package/backend/deck-node.ps1 +106 -0
- package/backend/panel.html +855 -0
- package/backend/panel.py +953 -0
- package/cordis.patch.yml +4 -0
- package/lib/client.js +366 -0
- package/lib/index.js +98 -0
- package/package.json +56 -4
package/cordis.patch.yml
ADDED
package/lib/client.js
ADDED
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
// dsh-project-deck client half: 在 DSH 界面里嵌入项目甲板 Project Deck(deck 面板)。
|
|
2
|
+
// 面板本体是独立进程(~/.project-deck/bin/panel.py,http://127.0.0.1:9199),
|
|
3
|
+
// 这里用 iframe 内嵌,避免在插件里重复实现一遍 UI。
|
|
4
|
+
window.__ModuleLoader__.load({
|
|
5
|
+
id: 'dsh-project-deck',
|
|
6
|
+
factory: (require) => {
|
|
7
|
+
const module = { exports: {} }
|
|
8
|
+
const exports = module.exports
|
|
9
|
+
const React = require('react')
|
|
10
|
+
const h = React.createElement
|
|
11
|
+
const PANEL = 'http://127.0.0.1:9199'
|
|
12
|
+
|
|
13
|
+
// 兼容读取可选服务(uiWorkspace 在 0.1.6+,旧版回退 sessions)
|
|
14
|
+
function optionalService(ctx, name) {
|
|
15
|
+
try {
|
|
16
|
+
const value = ctx && ctx.get ? ctx.get(name) : undefined
|
|
17
|
+
if (value != null) return value
|
|
18
|
+
} catch { /* get 异常时继续属性访问 */ }
|
|
19
|
+
try { return (ctx && ctx[name]) || null } catch { return null }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 自定义深链:#deck-session=<id>(DSH 本身没有 URL scheme / 会话路由,
|
|
23
|
+
// 所以由插件读 hash 后自己跳;链接对普通浏览器也可用)
|
|
24
|
+
function readDeepLink() {
|
|
25
|
+
try {
|
|
26
|
+
const hash = String(window.location.hash || '')
|
|
27
|
+
const m = hash.match(/deck-session=([^&]+)/)
|
|
28
|
+
if (m) return { kind: 'session', value: decodeURIComponent(m[1]) }
|
|
29
|
+
const p = hash.match(/deck-project=([^&]+)/)
|
|
30
|
+
if (p) return { kind: 'project', value: decodeURIComponent(p[1]) }
|
|
31
|
+
return null
|
|
32
|
+
} catch { return null }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function clearDeepLink() {
|
|
36
|
+
try {
|
|
37
|
+
if (/deck-session=/.test(window.location.hash)) {
|
|
38
|
+
history.replaceState(null, '', window.location.pathname + window.location.search)
|
|
39
|
+
}
|
|
40
|
+
} catch { /* 忽略 */ }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function sessionKnown(ctx, sessionId) {
|
|
44
|
+
const sessions = optionalService(ctx, 'sessions')
|
|
45
|
+
if (!sessions) return false
|
|
46
|
+
try {
|
|
47
|
+
const snap = sessions.list && sessions.list.getSnapshot ? sessions.list.getSnapshot() : null
|
|
48
|
+
if (snap && snap.byId && snap.byId[sessionId]) return true
|
|
49
|
+
if (sessions.binding && sessions.binding(sessionId)) return true
|
|
50
|
+
} catch { /* 忽略 */ }
|
|
51
|
+
return false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function findWorkspaceByPath(ctx, root) {
|
|
55
|
+
const ws = optionalService(ctx, 'workspaces')
|
|
56
|
+
try {
|
|
57
|
+
const snap = ws && ws.list ? ws.list.getSnapshot() : null
|
|
58
|
+
const items = (snap && snap.items) || []
|
|
59
|
+
return items.find((w) => w.path === root) || null
|
|
60
|
+
} catch { return null }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function openProjectWith(ctx, payload) {
|
|
64
|
+
const uw = optionalService(ctx, 'uiWorkspace')
|
|
65
|
+
if (!uw) return Promise.resolve(false)
|
|
66
|
+
if (payload.sessionId && sessionKnown(ctx, payload.sessionId)) {
|
|
67
|
+
return Promise.resolve(openSessionWith(ctx, payload.sessionId))
|
|
68
|
+
}
|
|
69
|
+
// 没有会话:找到 workspace(没有就创建),再开会话
|
|
70
|
+
const existing = findWorkspaceByPath(ctx, payload.root)
|
|
71
|
+
const ensureWorkspace = existing
|
|
72
|
+
? Promise.resolve(existing)
|
|
73
|
+
: (optionalService(ctx, 'workspaces') && optionalService(ctx, 'workspaces').create
|
|
74
|
+
? optionalService(ctx, 'workspaces').create({ path: payload.root }).catch(() => null)
|
|
75
|
+
: Promise.resolve(null))
|
|
76
|
+
return ensureWorkspace.then((ws) => {
|
|
77
|
+
const wid = ws && ws.workspaceId
|
|
78
|
+
if (!wid || typeof uw.openWorkspace !== 'function') return false
|
|
79
|
+
return uw.openWorkspace(wid).then(() => true).catch(() => false)
|
|
80
|
+
})
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function openSessionWith(ctx, sessionId) {
|
|
84
|
+
const uw = optionalService(ctx, 'uiWorkspace')
|
|
85
|
+
if (uw && typeof uw.openSession === 'function') {
|
|
86
|
+
try { uw.openSession(sessionId); return true } catch (e) { console.warn('[dsh-project-deck] uiWorkspace.openSession 失败', e) }
|
|
87
|
+
}
|
|
88
|
+
const sessions = optionalService(ctx, 'sessions')
|
|
89
|
+
if (sessions && typeof sessions.open === 'function') {
|
|
90
|
+
try { sessions.open(sessionId); return true } catch (e) { console.warn('[dsh-project-deck] sessions.open 失败', e) }
|
|
91
|
+
}
|
|
92
|
+
console.warn('[dsh-project-deck] 没有可用的会话导航服务(uiWorkspace / sessions 都不可用)')
|
|
93
|
+
return false
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const css = `
|
|
97
|
+
.deck-fab{position:fixed;right:18px;bottom:112px;left:auto;z-index:120;position:fixed;pointer-events:auto;touch-action:none;cursor:grab;user-select:none;display:flex;align-items:center;justify-content:center;
|
|
98
|
+
width:46px;height:46px;border-radius:50%;border:1px solid rgba(53,208,186,.45);cursor:pointer;
|
|
99
|
+
background:linear-gradient(180deg,#12303a,#0f1e1d);color:#7ce7da;font-size:20px;line-height:1;
|
|
100
|
+
box-shadow:0 10px 24px rgba(0,0,0,.35),0 2px 6px rgba(0,0,0,.2);transition:filter .12s,transform .12s;
|
|
101
|
+
font-family:system-ui,-apple-system,"PingFang SC",sans-serif;padding:0}
|
|
102
|
+
.deck-fab:hover{filter:brightness(1.12);transform:translateY(-1px)}
|
|
103
|
+
.deck-fab:active{cursor:grabbing}
|
|
104
|
+
.deck-fab svg{display:block}
|
|
105
|
+
.deck-fab .deck-badge{position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;border-radius:9px;
|
|
106
|
+
background:#ef5b5b;color:#fff;font-size:10px;line-height:16px;text-align:center;font-weight:700;box-shadow:0 0 0 2px #0b0f14}
|
|
107
|
+
.deck-fab.ai-on{box-shadow:0 0 0 2px rgba(53,208,186,.55),0 10px 24px rgba(0,0,0,.35)}
|
|
108
|
+
.deck-fab:active{transform:translateY(0)}
|
|
109
|
+
.deck-mask{position:fixed;inset:0;z-index:130;background:rgba(4,8,12,.62);display:flex;align-items:center;justify-content:center;padding:24px;pointer-events:auto}
|
|
110
|
+
.deck-box{width:min(1180px,94vw);height:min(860px,88vh);background:#0b0f14;border:1px solid #1f2a37;border-radius:14px;
|
|
111
|
+
display:flex;flex-direction:column;overflow:hidden;box-shadow:0 24px 60px rgba(0,0,0,.45)}
|
|
112
|
+
.deck-head{display:flex;align-items:center;gap:10px;padding:10px 14px;border-bottom:1px solid #1f2a37;
|
|
113
|
+
background:#0f151d;color:#e6edf3;font:600 13px/1.4 system-ui,-apple-system,"PingFang SC",sans-serif}
|
|
114
|
+
.deck-head .dot{width:8px;height:8px;border-radius:50%;background:#3ddc97;box-shadow:0 0 8px rgba(61,220,151,.7)}
|
|
115
|
+
.deck-head .sp{flex:1}
|
|
116
|
+
.deck-head a,.deck-head button{color:#8b98a5;background:none;border:1px solid #1f2a37;border-radius:7px;
|
|
117
|
+
padding:4px 10px;font-size:12px;cursor:pointer;text-decoration:none;font-family:inherit}
|
|
118
|
+
.deck-head a:hover,.deck-head button:hover{color:#e6edf3;border-color:#2bb8a5}
|
|
119
|
+
.deck-frame{flex:1;width:100%;border:0;background:#0b0f14}
|
|
120
|
+
.deck-tip{padding:8px 14px;color:#6f7d8a;font:11px/1.5 system-ui,-apple-system,"PingFang SC",sans-serif;border-top:1px solid #1a232d}
|
|
121
|
+
`
|
|
122
|
+
|
|
123
|
+
if (typeof document !== 'undefined' && !document.getElementById('dsh-project-deck-style')) {
|
|
124
|
+
const style = document.createElement('style')
|
|
125
|
+
style.id = 'dsh-project-deck-style'
|
|
126
|
+
style.textContent = css
|
|
127
|
+
document.head.appendChild(style)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function Panel(props) {
|
|
131
|
+
const appCtx = props && props.appCtx
|
|
132
|
+
const [open, setOpen] = React.useState(false)
|
|
133
|
+
const [loaded, setLoaded] = React.useState(false)
|
|
134
|
+
const frameRef = React.useRef(null)
|
|
135
|
+
|
|
136
|
+
// 悬浮按钮可拖到页面任意位置(位置持久化)
|
|
137
|
+
const [pos, setPos] = React.useState(() => {
|
|
138
|
+
try { const v = JSON.parse(localStorage.getItem('deck-fab-pos') || 'null'); return v && typeof v.x === 'number' ? v : null } catch { return null }
|
|
139
|
+
})
|
|
140
|
+
const [summary, setSummary] = React.useState({ auto: 0, assist: 0, pending: 0 })
|
|
141
|
+
React.useEffect(() => {
|
|
142
|
+
let alive = true
|
|
143
|
+
const load = () => {
|
|
144
|
+
fetch('http://127.0.0.1:9199/api/state', { cache: 'no-store' })
|
|
145
|
+
.then((r) => r.json())
|
|
146
|
+
.then((d) => {
|
|
147
|
+
if (!alive) return
|
|
148
|
+
const ps = d.projects || []
|
|
149
|
+
setSummary({
|
|
150
|
+
auto: ps.filter((p) => p.autonomy && p.autonomy.mode === 'auto').length,
|
|
151
|
+
assist: ps.filter((p) => p.autonomy && p.autonomy.mode === 'assist').length,
|
|
152
|
+
pending: (d.approvals || []).length,
|
|
153
|
+
})
|
|
154
|
+
})
|
|
155
|
+
.catch(() => {})
|
|
156
|
+
}
|
|
157
|
+
load()
|
|
158
|
+
const t = setInterval(load, 60000)
|
|
159
|
+
return () => { alive = false; clearInterval(t) }
|
|
160
|
+
}, [])
|
|
161
|
+
|
|
162
|
+
const dragState = React.useRef(null)
|
|
163
|
+
const movedRef = React.useRef(false)
|
|
164
|
+
|
|
165
|
+
const clamp = (x, y) => ({
|
|
166
|
+
x: Math.max(6, Math.min(x, window.innerWidth - 54)),
|
|
167
|
+
y: Math.max(6, Math.min(y, window.innerHeight - 54)),
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
// 用 window 级监听,避免指针移出按钮就丢事件(之前只能按住按钮原地挪)
|
|
171
|
+
React.useEffect(() => {
|
|
172
|
+
const onMove = (e) => {
|
|
173
|
+
const d = dragState.current
|
|
174
|
+
if (!d) return
|
|
175
|
+
if (!d.moved && (Math.abs(e.clientX - d.sx) > 3 || Math.abs(e.clientY - d.sy) > 3)) d.moved = true
|
|
176
|
+
setPos(clamp(e.clientX - d.dx, e.clientY - d.dy))
|
|
177
|
+
}
|
|
178
|
+
const onUp = () => {
|
|
179
|
+
const d = dragState.current
|
|
180
|
+
if (!d) return
|
|
181
|
+
dragState.current = null
|
|
182
|
+
movedRef.current = d.moved
|
|
183
|
+
setPos((cur) => { if (cur) { try { localStorage.setItem('deck-fab-pos', JSON.stringify(cur)) } catch {} } return cur })
|
|
184
|
+
}
|
|
185
|
+
window.addEventListener('pointermove', onMove, true)
|
|
186
|
+
window.addEventListener('pointerup', onUp, true)
|
|
187
|
+
window.addEventListener('mousemove', onMove, true)
|
|
188
|
+
window.addEventListener('mouseup', onUp, true)
|
|
189
|
+
return () => {
|
|
190
|
+
window.removeEventListener('pointermove', onMove, true)
|
|
191
|
+
window.removeEventListener('pointerup', onUp, true)
|
|
192
|
+
window.removeEventListener('mousemove', onMove, true)
|
|
193
|
+
window.removeEventListener('mouseup', onUp, true)
|
|
194
|
+
}
|
|
195
|
+
}, [])
|
|
196
|
+
|
|
197
|
+
const fabStyle = pos ? { left: pos.x + 'px', top: pos.y + 'px', right: 'auto', bottom: 'auto' } : undefined
|
|
198
|
+
|
|
199
|
+
const onDragStart = (e) => {
|
|
200
|
+
const r = e.currentTarget.getBoundingClientRect()
|
|
201
|
+
dragState.current = { dx: e.clientX - r.left, dy: e.clientY - r.top, sx: e.clientX, sy: e.clientY, moved: false }
|
|
202
|
+
movedRef.current = false
|
|
203
|
+
if (!pos) setPos({ x: r.left, y: r.top })
|
|
204
|
+
if (e.preventDefault) e.preventDefault()
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// 面板里也能改图标位置(拖拽失效时的兜底)
|
|
208
|
+
React.useEffect(() => {
|
|
209
|
+
const onPos = (e) => {
|
|
210
|
+
if (e.origin !== PANEL) return
|
|
211
|
+
const d = e.data || {}
|
|
212
|
+
if (d.type !== 'deck:set-fab-pos') return
|
|
213
|
+
const W = window.innerWidth, H = window.innerHeight
|
|
214
|
+
const spots = {
|
|
215
|
+
'right-bottom': { x: W - 60, y: H - 160 },
|
|
216
|
+
'left-bottom': { x: 16, y: H - 160 },
|
|
217
|
+
'right-top': { x: W - 60, y: 84 },
|
|
218
|
+
'center-right': { x: W - 60, y: Math.round(H / 2) },
|
|
219
|
+
default: { x: W - 60, y: H - 160 },
|
|
220
|
+
}
|
|
221
|
+
const next = spots[d.pos] || spots.default
|
|
222
|
+
setPos(clamp(next.x, next.y))
|
|
223
|
+
try { localStorage.setItem('deck-fab-pos', JSON.stringify(clamp(next.x, next.y))) } catch {}
|
|
224
|
+
}
|
|
225
|
+
window.addEventListener('message', onPos)
|
|
226
|
+
return () => window.removeEventListener('message', onPos)
|
|
227
|
+
}, [])
|
|
228
|
+
|
|
229
|
+
React.useEffect(() => {
|
|
230
|
+
if (!open) return undefined
|
|
231
|
+
const onKey = (e) => { if (e.key === 'Escape') setOpen(false) }
|
|
232
|
+
window.addEventListener('keydown', onKey)
|
|
233
|
+
return () => window.removeEventListener('keydown', onKey)
|
|
234
|
+
}, [open])
|
|
235
|
+
|
|
236
|
+
const openPanel = () => { setLoaded(false); setOpen(true) }
|
|
237
|
+
|
|
238
|
+
return h(React.Fragment, null,
|
|
239
|
+
h('button', {
|
|
240
|
+
className: 'deck-fab' + (summary.auto > 0 ? ' ai-on' : ''),
|
|
241
|
+
style: fabStyle,
|
|
242
|
+
title: '项目甲板 · AI 项目管理与自动推进(可拖动 v4)'
|
|
243
|
+
+ (summary.auto ? `|auto ${summary.auto}` : '')
|
|
244
|
+
+ (summary.pending ? `|待你拍板 ${summary.pending}` : ''),
|
|
245
|
+
onPointerDown: onDragStart,
|
|
246
|
+
onMouseDown: onDragStart,
|
|
247
|
+
onClick: () => { if (!movedRef.current) openPanel(); movedRef.current = false },
|
|
248
|
+
onDragStart: (e) => e.preventDefault(),
|
|
249
|
+
},
|
|
250
|
+
h('svg', { width: 26, height: 26, viewBox: '0 0 24 24', fill: 'none', stroke: '#7ce7da', strokeWidth: 1.5, strokeLinecap: 'round', strokeLinejoin: 'round' },
|
|
251
|
+
// 左:AI 机器人
|
|
252
|
+
h('rect', { x: 1.6, y: 8.2, width: 8.2, height: 7.2, rx: 2.2 }),
|
|
253
|
+
h('path', { d: 'M5.7 8.2V6.2' }),
|
|
254
|
+
h('circle', { cx: 5.7, cy: 5, r: 1.05, fill: '#7ce7da', stroke: 'none' }),
|
|
255
|
+
h('circle', { cx: 4.1, cy: 11.5, r: 0.9, fill: '#7ce7da', stroke: 'none' }),
|
|
256
|
+
h('circle', { cx: 7.3, cy: 11.5, r: 0.9, fill: '#7ce7da', stroke: 'none' }),
|
|
257
|
+
h('path', { d: 'M4.5 13.7h2.4' }),
|
|
258
|
+
// 右:待办清单(两条已完成)
|
|
259
|
+
h('rect', { x: 11.8, y: 4.6, width: 10.4, height: 14.8, rx: 2.6 }),
|
|
260
|
+
h('path', { d: 'M13.9 9.4l1.1 1.1 1.9-2.1' }),
|
|
261
|
+
h('path', { d: 'M18.1 9.9h2.3' }),
|
|
262
|
+
h('path', { d: 'M13.9 14l1.1 1.1 1.9-2.1' }),
|
|
263
|
+
h('path', { d: 'M18.1 14.5h2.3' }),
|
|
264
|
+
),
|
|
265
|
+
summary.pending > 0 ? h('span', { className: 'deck-badge' }, String(summary.pending)) : null,
|
|
266
|
+
),
|
|
267
|
+
open
|
|
268
|
+
? h('div', { className: 'deck-mask', onClick: (e) => { if (e.target === e.currentTarget) setOpen(false) } },
|
|
269
|
+
h('div', { className: 'deck-box' },
|
|
270
|
+
h('div', { className: 'deck-head' },
|
|
271
|
+
h('span', { className: 'dot' }),
|
|
272
|
+
h('span', null, '项目甲板 Project Deck'),
|
|
273
|
+
h('span', { className: 'sp' }),
|
|
274
|
+
h('a', { href: PANEL, target: '_blank', rel: 'noreferrer' }, '新窗口'),
|
|
275
|
+
h('button', { onClick: () => setOpen(false) }, '关闭'),
|
|
276
|
+
),
|
|
277
|
+
h('iframe', {
|
|
278
|
+
ref: frameRef,
|
|
279
|
+
className: 'deck-frame',
|
|
280
|
+
src: PANEL,
|
|
281
|
+
title: 'deck panel',
|
|
282
|
+
onLoad: () => setLoaded(true),
|
|
283
|
+
}),
|
|
284
|
+
h('div', { className: 'deck-tip' },
|
|
285
|
+
loaded
|
|
286
|
+
? '面板由 ~/.project-deck/bin/panel.py 提供(127.0.0.1:9199)'
|
|
287
|
+
: '正在连接面板… 若长时间空白,请在终端运行 deck status 检查服务是否在跑',
|
|
288
|
+
),
|
|
289
|
+
),
|
|
290
|
+
)
|
|
291
|
+
: null,
|
|
292
|
+
)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// 崩溃兜底:任何渲染错误都不让图标凭空消失
|
|
296
|
+
class Boundary extends React.Component {
|
|
297
|
+
constructor(props) { super(props); this.state = { err: null } }
|
|
298
|
+
static getDerivedStateFromError(err) { return { err } }
|
|
299
|
+
render() {
|
|
300
|
+
if (this.state.err) {
|
|
301
|
+
return h('div', {
|
|
302
|
+
title: String(this.state.err && this.state.err.message || this.state.err),
|
|
303
|
+
style: { position: 'fixed', right: 12, bottom: 100, zIndex: 121, background: '#2a1518',
|
|
304
|
+
color: '#ffb3b3', padding: '5px 9px', borderRadius: 8, fontSize: 11,
|
|
305
|
+
border: '1px solid #4a2226', cursor: 'pointer', pointerEvents: 'auto' },
|
|
306
|
+
onClick: () => this.setState({ err: null }),
|
|
307
|
+
}, '项目甲板加载失败(点我重试)')
|
|
308
|
+
}
|
|
309
|
+
return this.props.children
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
exports.name = 'dsh-project-deck'
|
|
314
|
+
exports.inject = ['slots']
|
|
315
|
+
exports.apply = function apply(ctx) {
|
|
316
|
+
const slots = ctx.get('slots')
|
|
317
|
+
if (!slots) {
|
|
318
|
+
console.error('[dsh-project-deck] slots 服务不可用')
|
|
319
|
+
return
|
|
320
|
+
}
|
|
321
|
+
// 深链:支持 #deck-session=<id>(会话)与 #deck-project=<路径>(项目→开会话)。
|
|
322
|
+
// 刚加载时会话列表还没拉回来,直接 open 会被丢掉,所以要等就绪后重试。
|
|
323
|
+
let deepLinkTimer = null
|
|
324
|
+
const finishDeepLink = (label) => {
|
|
325
|
+
clearDeepLink()
|
|
326
|
+
console.log('[dsh-project-deck] deep link opened', label)
|
|
327
|
+
}
|
|
328
|
+
const tryDeepLink = (target, attempt) => {
|
|
329
|
+
const cur = readDeepLink()
|
|
330
|
+
if (!cur || cur.kind !== target.kind || cur.value !== target.value) return
|
|
331
|
+
if (target.kind === 'session') {
|
|
332
|
+
if (sessionKnown(ctx, target.value)) {
|
|
333
|
+
if (openSessionWith(ctx, target.value)) { finishDeepLink(target.value); return }
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
openProjectWith(ctx, { root: target.value, sessionId: null }).then((ok) => {
|
|
337
|
+
if (ok) finishDeepLink(target.value)
|
|
338
|
+
else if (attempt < 50) deepLinkTimer = setTimeout(() => tryDeepLink(target, attempt + 1), 400)
|
|
339
|
+
else console.warn('[dsh-project-deck] deep link 放弃:workspace 创建/打开失败', target.value)
|
|
340
|
+
})
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
if (attempt >= 50) {
|
|
344
|
+
console.warn('[dsh-project-deck] deep link 放弃:会话未出现在列表里', target.value)
|
|
345
|
+
return
|
|
346
|
+
}
|
|
347
|
+
deepLinkTimer = setTimeout(() => tryDeepLink(target, attempt + 1), 400)
|
|
348
|
+
}
|
|
349
|
+
const onDeepLink = () => {
|
|
350
|
+
const target = readDeepLink()
|
|
351
|
+
if (!target) return
|
|
352
|
+
if (deepLinkTimer) clearTimeout(deepLinkTimer)
|
|
353
|
+
tryDeepLink(target, 0)
|
|
354
|
+
}
|
|
355
|
+
onDeepLink()
|
|
356
|
+
window.addEventListener('hashchange', onDeepLink)
|
|
357
|
+
|
|
358
|
+
slots.inject('shell.overlay', () => slots.register(
|
|
359
|
+
{ name: 'shell.overlay', id: 'deck-panel', order: 6, label: '项目甲板' },
|
|
360
|
+
(props) => h(Boundary, null, h(Panel, Object.assign({}, props, { appCtx: ctx }))),
|
|
361
|
+
))
|
|
362
|
+
console.log('[dsh-project-deck] client ready')
|
|
363
|
+
}
|
|
364
|
+
return module.exports
|
|
365
|
+
},
|
|
366
|
+
})
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// dsh-project-deck host half: 把本机的 deck CLI 暴露成模型工具。
|
|
2
|
+
// 面板本体是独立进程(~/.project-deck/bin/panel.py, http://127.0.0.1:9199);
|
|
3
|
+
// 客户端半边(client.js)以 iframe 嵌入,不依赖本文件的 HTTP 路由。
|
|
4
|
+
import { execFile } from 'node:child_process'
|
|
5
|
+
import { existsSync } from 'node:fs'
|
|
6
|
+
import { homedir } from 'node:os'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
|
|
9
|
+
export const name = 'dsh-project-deck'
|
|
10
|
+
export const inject = ['tools']
|
|
11
|
+
|
|
12
|
+
const PAGENT = join(homedir(), '.person-agent', 'bin', 'deck')
|
|
13
|
+
|
|
14
|
+
function run(args, timeout = 60000) {
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
if (!existsSync(PAGENT)) {
|
|
17
|
+
resolve('❌ 找不到 deck CLI:' + PAGENT + '(项目甲板 尚未初始化)')
|
|
18
|
+
return
|
|
19
|
+
}
|
|
20
|
+
execFile(PAGENT, args, {
|
|
21
|
+
timeout,
|
|
22
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
23
|
+
env: { ...process.env, PATH: '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin' },
|
|
24
|
+
}, (err, stdout, stderr) => {
|
|
25
|
+
const out = String(stdout || '').trim()
|
|
26
|
+
const errText = String(stderr || '').trim()
|
|
27
|
+
if (err && !out) {
|
|
28
|
+
resolve('❌ deck ' + args.join(' ') + ' 失败:' + (errText || err.message))
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
resolve(out + (errText ? '\n' + errText : ''))
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function toolText(name, description, argsBuilder) {
|
|
37
|
+
return {
|
|
38
|
+
name,
|
|
39
|
+
description,
|
|
40
|
+
parameters: { type: 'object', properties: {} },
|
|
41
|
+
output: {
|
|
42
|
+
schema: { type: 'string' },
|
|
43
|
+
render(_args, value) {
|
|
44
|
+
return [{ type: 'text', text: String(value || '') }]
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
async execute() {
|
|
48
|
+
return run(argsBuilder())
|
|
49
|
+
},
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function apply(ctx) {
|
|
54
|
+
const tools = ctx.get('tools')
|
|
55
|
+
if (!tools) {
|
|
56
|
+
console.error('[dsh-project-deck] tools 服务不可用,跳过工具注册')
|
|
57
|
+
return
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const registrations = [
|
|
61
|
+
toolText(
|
|
62
|
+
'deck_status',
|
|
63
|
+
'查看项目甲板 运行状态:项目数、周期播报开关与间隔、守护进程、微信通道、自动发现上次扫描时间。',
|
|
64
|
+
() => ['status'],
|
|
65
|
+
),
|
|
66
|
+
toolText(
|
|
67
|
+
'deck_list',
|
|
68
|
+
'列出项目注册表里的全部项目(id、名称、类型、状态、优先级、根目录)。',
|
|
69
|
+
() => ['list'],
|
|
70
|
+
),
|
|
71
|
+
toolText(
|
|
72
|
+
'deck_scan',
|
|
73
|
+
'扫描本机(以及已上报的其它节点)发现新项目候选,返回评分与理由;达到阈值的项目会在守护进程扫描时自动登记。',
|
|
74
|
+
() => ['scan'],
|
|
75
|
+
),
|
|
76
|
+
toolText(
|
|
77
|
+
'deck_nodes',
|
|
78
|
+
'查看多节点扫描的节点列表:平台、最近上报时间、项目数、在线状态。',
|
|
79
|
+
() => ['nodes'],
|
|
80
|
+
),
|
|
81
|
+
toolText(
|
|
82
|
+
'deck_report',
|
|
83
|
+
'立即生成项目播报并推送到微信(文字 + 可选的看板图)。会真实发送消息,仅在用户明确要求时调用。',
|
|
84
|
+
() => ['report'],
|
|
85
|
+
),
|
|
86
|
+
]
|
|
87
|
+
|
|
88
|
+
const registered = []
|
|
89
|
+
for (const reg of registrations) {
|
|
90
|
+
try {
|
|
91
|
+
tools.register(reg)
|
|
92
|
+
registered.push(reg.name)
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.error('[dsh-project-deck] 注册工具失败', reg.name, (e && e.message) || e)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
console.log('[dsh-project-deck] host ready, tools:', registered.join(', '))
|
|
98
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,58 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-project-deck",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "DeepSeek Harness plugin: a project deck for your repos — registry, dashboard, scheduled WeChat reports, auto-discovery, multi-node scanning.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"default": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./client": {
|
|
12
|
+
"default": "./lib/client.js"
|
|
13
|
+
},
|
|
14
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"dsh": {
|
|
18
|
+
"bundle": {
|
|
19
|
+
"patch": "./cordis.patch.yml"
|
|
20
|
+
},
|
|
21
|
+
"client": {
|
|
22
|
+
"platform": "web",
|
|
23
|
+
"immediately": true
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"lib",
|
|
28
|
+
"cordis.patch.yml",
|
|
29
|
+
"backend",
|
|
30
|
+
"README.md",
|
|
31
|
+
"LICENSE"
|
|
32
|
+
],
|
|
33
|
+
"keywords": [
|
|
34
|
+
"dsh",
|
|
35
|
+
"deepseek-harness",
|
|
36
|
+
"plugin",
|
|
37
|
+
"project-management",
|
|
38
|
+
"wechat",
|
|
39
|
+
"dashboard"
|
|
40
|
+
],
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"engines": {
|
|
43
|
+
"node": ">=22.15"
|
|
44
|
+
},
|
|
45
|
+
"repository": {
|
|
46
|
+
"type": "git",
|
|
47
|
+
"url": "git+https://github.com/peng456/dsh-project-deck.git"
|
|
48
|
+
},
|
|
49
|
+
"author": "peng456",
|
|
50
|
+
"homepage": "https://github.com/peng456/dsh-project-deck#readme",
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/peng456/dsh-project-deck/issues"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public",
|
|
56
|
+
"registry": "https://registry.npmjs.org/"
|
|
57
|
+
}
|
|
58
|
+
}
|