@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.
@@ -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
+ }