@telosmaylx/dsh-session-notify 0.1.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/README.md +188 -0
- package/lib/client.js +1180 -0
- package/lib/core.js +303 -0
- package/lib/index.js +291 -0
- package/package.json +66 -0
- package/scripts/build.sh +16 -0
- package/scripts/probe-card-render.mjs +100 -0
- package/scripts/probe-client-e2e.mjs +134 -0
- package/scripts/probe-client.mjs +145 -0
- package/scripts/probe-diag-settings.mjs +79 -0
- package/scripts/probe-settings-card.mjs +137 -0
- package/scripts/probe-settings-check.mjs +87 -0
- package/scripts/verify-notice.mjs +110 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// probe-settings-check.mjs — 无头验证设置面板中卡片渲染与报错
|
|
2
|
+
import { spawn } from 'node:child_process'
|
|
3
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
4
|
+
import { tmpdir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
|
|
7
|
+
const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe'
|
|
8
|
+
const PORT = 9489
|
|
9
|
+
const profile = mkdtempSync(join(tmpdir(), 'dsh-card4-'))
|
|
10
|
+
const chrome = spawn(CHROME, ['--headless=new', `--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`, '--no-first-run', '--disable-gpu', '--window-size=1500,950', 'about:blank'], { stdio: 'ignore' })
|
|
11
|
+
|
|
12
|
+
async function waitFor(fn, t, l) {
|
|
13
|
+
const t0 = Date.now()
|
|
14
|
+
while (Date.now() - t0 < t) { try { const v = await fn(); if (v) return v } catch {} await new Promise((r) => setTimeout(r, 500)) }
|
|
15
|
+
throw new Error('timeout ' + l)
|
|
16
|
+
}
|
|
17
|
+
const page = await waitFor(async () => {
|
|
18
|
+
const l = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json()
|
|
19
|
+
return l.find((x) => x.type === 'page')
|
|
20
|
+
}, 20000, 'page')
|
|
21
|
+
const ws = new WebSocket(page.webSocketDebuggerUrl)
|
|
22
|
+
let seq = 0
|
|
23
|
+
const pending = new Map()
|
|
24
|
+
const errs = []
|
|
25
|
+
const logs = []
|
|
26
|
+
ws.onmessage = (m) => {
|
|
27
|
+
const msg = JSON.parse(m.data)
|
|
28
|
+
if (msg.id !== undefined && pending.has(msg.id)) { pending.get(msg.id)(msg); pending.delete(msg.id); return }
|
|
29
|
+
if (msg.method === 'Runtime.exceptionThrown') {
|
|
30
|
+
const d = msg.params.exceptionDetails
|
|
31
|
+
errs.push((d?.exception?.description ?? d?.text ?? '').slice(0, 600))
|
|
32
|
+
} else if (msg.method === 'Runtime.consoleAPICalled') {
|
|
33
|
+
const a = (msg.params.args || []).map((x) => x.value ?? x.description ?? '').join(' ')
|
|
34
|
+
logs.push(a.slice(0, 300))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function rpc(method, params = {}) { return new Promise((res) => { const id = ++seq; pending.set(id, res); ws.send(JSON.stringify({ id, method, params })) }) }
|
|
38
|
+
await new Promise((r) => { ws.onopen = r })
|
|
39
|
+
await rpc('Runtime.enable')
|
|
40
|
+
await rpc('Page.enable')
|
|
41
|
+
await rpc('Page.navigate', { url: 'http://127.0.0.1:3080' })
|
|
42
|
+
await waitFor(async () => { const r = await rpc('Runtime.evaluate', { expression: '!!window.__DSH_BOOT__', returnByValue: true }); return r.result?.result?.value }, 30000, 'boot')
|
|
43
|
+
await new Promise((r) => setTimeout(r, 6000))
|
|
44
|
+
|
|
45
|
+
// 点击侧边栏「设置」(找 text=设置 的最小元素的按钮祖先)
|
|
46
|
+
const c1 = await rpc('Runtime.evaluate', {
|
|
47
|
+
expression: `(() => {
|
|
48
|
+
const textNodes = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT)
|
|
49
|
+
let t
|
|
50
|
+
const targets = []
|
|
51
|
+
while ((t = textNodes.nextNode())) {
|
|
52
|
+
if ((t.textContent || '').trim() !== '设置') continue
|
|
53
|
+
let el = t.parentElement
|
|
54
|
+
while (el && el !== document.body) {
|
|
55
|
+
if (el.tagName === 'BUTTON' || el.getAttribute('role') === 'button') { targets.push(el); break }
|
|
56
|
+
el = el.parentElement
|
|
57
|
+
}
|
|
58
|
+
if (!targets.length && t.parentElement) targets.push(t.parentElement)
|
|
59
|
+
}
|
|
60
|
+
const el = targets[0]
|
|
61
|
+
if (!el) return 'none'
|
|
62
|
+
el.click()
|
|
63
|
+
return 'clicked ' + el.tagName + ' :: ' + (el.textContent || '').slice(0, 24)
|
|
64
|
+
})()`,
|
|
65
|
+
returnByValue: true,
|
|
66
|
+
})
|
|
67
|
+
console.log('SETTINGS-CLICK:', c1.result?.result?.value)
|
|
68
|
+
await new Promise((r) => setTimeout(r, 3500))
|
|
69
|
+
|
|
70
|
+
const dump1 = await rpc('Runtime.evaluate', {
|
|
71
|
+
expression: `(() => {
|
|
72
|
+
const b = document.body.innerText || ''
|
|
73
|
+
const idx = b.indexOf('会话完成提醒')
|
|
74
|
+
const pluginIdx = b.indexOf('插件配置')
|
|
75
|
+
return JSON.stringify({ hasCardText: idx >= 0, hasPluginsTab: pluginIdx >= 0, cardCtx: idx >= 0 ? b.slice(idx - 80, idx + 120) : '', sample: b.slice(pluginIdx, pluginIdx + 160) })
|
|
76
|
+
})()`,
|
|
77
|
+
returnByValue: true,
|
|
78
|
+
})
|
|
79
|
+
console.log('PANEL:', dump1.result?.result?.value)
|
|
80
|
+
console.log('ERRORS:', JSON.stringify(errs.slice(0, 4)))
|
|
81
|
+
console.log('LOGS:', JSON.stringify(logs.slice(0, 8)))
|
|
82
|
+
|
|
83
|
+
try { ws.close() } catch {}
|
|
84
|
+
chrome.kill()
|
|
85
|
+
await new Promise((r) => setTimeout(r, 300))
|
|
86
|
+
try { rmSync(profile, { recursive: true, force: true }) } catch {}
|
|
87
|
+
process.exit(0)
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* verify-notice.mjs —— 验证 dsh-session-complete-notify 的落盘证据。
|
|
4
|
+
*
|
|
5
|
+
* 用法:
|
|
6
|
+
* node scripts/verify-notice.mjs <session.js.jsonl.zstd 路径>
|
|
7
|
+
* node scripts/verify-notice.mjs # 自动选 ~/.dsh/sessions 下最新会话
|
|
8
|
+
*
|
|
9
|
+
* 输出:该日志中所有 plugin-source(kind=plugin)的 user/message 事件,
|
|
10
|
+
* 以及最近一次 turn/end 之后的尾部事件序列。
|
|
11
|
+
*
|
|
12
|
+
* 说明:存储文件是多帧 zstd(每次写批一帧),Node 的 zstdDecompressSync
|
|
13
|
+
* 只解首帧,因此逐帧解压后拼接。
|
|
14
|
+
*/
|
|
15
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
|
16
|
+
import { join } from 'node:path'
|
|
17
|
+
import { homedir } from 'node:os'
|
|
18
|
+
import { zstdDecompressSync } from 'node:zlib'
|
|
19
|
+
|
|
20
|
+
function* zstdFrames(buffer) {
|
|
21
|
+
let i = 0
|
|
22
|
+
while (i + 4 <= buffer.length) {
|
|
23
|
+
if (buffer[i] !== 0x28 || buffer[i + 1] !== 0xb5 || buffer[i + 2] !== 0x2f || buffer[i + 3] !== 0xfd) throw new Error(`非 zstd 帧头 @${i}`)
|
|
24
|
+
const start = i
|
|
25
|
+
i += 4
|
|
26
|
+
// 粗扫下一个帧头(真实帧边界);帧头里的合法 0x28b52ffd 概率可忽略
|
|
27
|
+
while (i + 4 <= buffer.length) {
|
|
28
|
+
if (buffer[i] === 0x28 && buffer[i + 1] === 0xb5 && buffer[i + 2] === 0x2f && buffer[i + 3] === 0xfd) break
|
|
29
|
+
i += 1
|
|
30
|
+
}
|
|
31
|
+
yield buffer.subarray(start, i)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function newestSessionLog() {
|
|
36
|
+
const root = join(homedir(), '.dsh', 'sessions')
|
|
37
|
+
const found = []
|
|
38
|
+
const walk = (dir) => {
|
|
39
|
+
for (const name of readdirSync(dir)) {
|
|
40
|
+
const full = join(dir, name)
|
|
41
|
+
const stat = statSync(full)
|
|
42
|
+
if (stat.isDirectory()) walk(full)
|
|
43
|
+
else if (name.startsWith('session.jsonl.zstd')) found.push({ full, mtime: stat.mtimeMs })
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
walk(root)
|
|
47
|
+
found.sort((a, b) => b.mtime - a.mtime)
|
|
48
|
+
return found[0]?.full
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const path = process.argv[2] ?? newestSessionLog()
|
|
52
|
+
if (!path) {
|
|
53
|
+
console.error('未找到会话日志(请传入 session.jsonl.zstd 路径)')
|
|
54
|
+
process.exit(1)
|
|
55
|
+
}
|
|
56
|
+
console.log(`日志: ${path}`)
|
|
57
|
+
|
|
58
|
+
let text = ''
|
|
59
|
+
let frames = 0
|
|
60
|
+
for (const frame of zstdFrames(readFileSync(path))) {
|
|
61
|
+
text += zstdDecompressSync(frame).toString('utf8')
|
|
62
|
+
frames += 1
|
|
63
|
+
}
|
|
64
|
+
const lines = text.split('\n').filter(Boolean)
|
|
65
|
+
console.log(`帧数: ${frames},行数: ${lines.length}`)
|
|
66
|
+
|
|
67
|
+
const notices = lines
|
|
68
|
+
.map((l, i) => ({ l, i }))
|
|
69
|
+
.filter(({ l }) => l.includes('"type":"user/message"') && l.includes('"kind":"plugin"'))
|
|
70
|
+
console.log(`\n===== plugin-source 系统消息(${notices.length} 条)=====`)
|
|
71
|
+
for (const n of notices) {
|
|
72
|
+
try {
|
|
73
|
+
const row = JSON.parse(n.l)
|
|
74
|
+
const msg = row.data?.message ?? row.data
|
|
75
|
+
console.log(`row ${n.i} seq ${row.seq}: summary=${JSON.stringify(msg?.source?.summary)} text=${JSON.stringify((msg?.content ?? [])[0]?.text ?? '')}`)
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.log(`row ${n.i}(无法解析): ${n.l.slice(0, 200)}`)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
console.log('\n===== turn/end 事件(触发点核验)=====')
|
|
82
|
+
for (const l of lines) {
|
|
83
|
+
if (!l.includes('"type":"turn/end"')) continue
|
|
84
|
+
try {
|
|
85
|
+
const row = JSON.parse(l)
|
|
86
|
+
console.log(`seq ${row.seq} time ${row.time} turn=${row.data?.turn} reason=${JSON.stringify(row.data?.reason ?? {})}`)
|
|
87
|
+
} catch { /* 忽略无法解析行 */ }
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
console.log('\n===== turn/start 事件(计时核验)=====')
|
|
91
|
+
for (const l of lines) {
|
|
92
|
+
if (!l.includes('"type":"turn/start"')) continue
|
|
93
|
+
try {
|
|
94
|
+
const row = JSON.parse(l)
|
|
95
|
+
console.log(`seq ${row.seq} time ${row.time} turn=${row.data?.turn}`)
|
|
96
|
+
} catch { /* 忽略无法解析行 */ }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
console.log('\n===== 事件尾序列(最近 24 行的 type/seq/来源)=====')
|
|
100
|
+
for (const l of lines.slice(-24)) {
|
|
101
|
+
try {
|
|
102
|
+
const row = JSON.parse(l)
|
|
103
|
+
const data = row.data ?? {}
|
|
104
|
+
const src = data.message?.source
|
|
105
|
+
const extra = src ? ` [source=${src.kind}${src.kind === 'plugin' ? `:${src.plugin}` : ''}]` : ''
|
|
106
|
+
console.log(`seq ${row.seq} ${row.type}${extra}`)
|
|
107
|
+
} catch {
|
|
108
|
+
console.log(`?? ${l.slice(0, 120)}`)
|
|
109
|
+
}
|
|
110
|
+
}
|