@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,100 @@
1
+ // probe-card-render.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 = 9588
9
+ const profile = mkdtempSync(join(tmpdir(), 'dsh-card5-'))
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, 700))
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
+ async function clickExactText(text) {
46
+ const r = await rpc('Runtime.evaluate', {
47
+ expression: `(() => {
48
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT)
49
+ let node
50
+ const targets = []
51
+ while ((node = walker.nextNode())) {
52
+ if ((node.textContent || '').trim() !== ${JSON.stringify(text)}) continue
53
+ let el = node.parentElement
54
+ while (el && el !== document.body) {
55
+ if (el.tagName === 'BUTTON' || el.getAttribute('role') === 'button' || el.tagName === 'LI' || el.getAttribute('role') === 'tab') { targets.push(el); break }
56
+ el = el.parentElement
57
+ }
58
+ if (!targets.length && node.parentElement) targets.push(node.parentElement)
59
+ }
60
+ const el = targets[0]
61
+ if (!el) return 'none:' + ${JSON.stringify(text)}
62
+ el.click()
63
+ return 'clicked:' + el.tagName + '::' + (el.textContent || '').trim().slice(0, 16)
64
+ })()`,
65
+ returnByValue: true,
66
+ })
67
+ console.log('CLICK', JSON.stringify(text), '=>', r.result?.result?.value)
68
+ }
69
+
70
+ await clickExactText('设置')
71
+ await new Promise((r) => setTimeout(r, 3000))
72
+ // 插件页签(左侧导航):点文本为「插件」的最小元素
73
+ await clickExactText('插件')
74
+ await new Promise((r) => setTimeout(r, 3500))
75
+
76
+ const check = await rpc('Runtime.evaluate', {
77
+ expression: `(() => {
78
+ const b = document.body.innerText || ''
79
+ const lines = b.split('\\n').map((s) => s.trim()).filter(Boolean)
80
+ return JSON.stringify({
81
+ // 精确行匹配(避免侧边栏会话名误判)
82
+ cardTitleLine: lines.includes('会话完成提醒'),
83
+ hasSubtitle: b.includes('(保存后刷新生效)') || b.includes('保存后刷新'),
84
+ hasPresetRow: lines.includes('预设') && b.includes('未命名预设'),
85
+ hasLangRow: lines.includes('语言') && b.includes('简体中文'),
86
+ hasFooter: b.includes('保存') && b.includes('放弃'),
87
+ modalTextSample: b.slice(b.indexOf('插件列表') - 100, b.indexOf('插件列表') + 200),
88
+ })
89
+ })()`,
90
+ returnByValue: true,
91
+ })
92
+ console.log('CHECK:', check.result?.result?.value)
93
+ console.log('ERRORS:', JSON.stringify(errs.slice(0, 5)))
94
+ console.log('LOGS:', JSON.stringify(logs.slice(0, 8)))
95
+
96
+ try { ws.close() } catch {}
97
+ chrome.kill()
98
+ await new Promise((r) => setTimeout(r, 300))
99
+ try { rmSync(profile, { recursive: true, force: true }) } catch {}
100
+ process.exit(0)
@@ -0,0 +1,134 @@
1
+ // probe-client-e2e.mjs — 端到端:无头浏览器保持打开,等待真实会话完成事件,
2
+ // 验证客户端插件在 running→idle 边沿触发推送(toast DOM / Notification)。
3
+ // 可选参数 2:会话 id —— 预置 localStorage 'dsh.sessions.current' 并重载页面,
4
+ // 模拟「正在查看该会话」,使事件窗口开启(推送正文可取到宿主通知全文)。
5
+ import { spawn } from 'node:child_process'
6
+ import { mkdtempSync, rmSync } from 'node:fs'
7
+ import { tmpdir } from 'node:os'
8
+ import { join } from 'node:path'
9
+
10
+ const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe'
11
+ const PORT = 9344
12
+ const APP_URL = 'http://127.0.0.1:3080'
13
+ const TIMEOUT_MS = Number(process.argv[2] ?? 900000) // 默认 15 分钟
14
+ const PINNED_SESSION = process.argv[3]
15
+ const profile = mkdtempSync(join(tmpdir(), 'dsh-e2e-'))
16
+
17
+ const chrome = spawn(CHROME, [
18
+ '--headless=new',
19
+ `--remote-debugging-port=${PORT}`,
20
+ `--user-data-dir=${profile}`,
21
+ '--no-first-run',
22
+ '--disable-gpu',
23
+ '--window-size=1400,900',
24
+ 'about:blank',
25
+ ], { stdio: 'ignore' })
26
+
27
+ async function waitFor(fn, timeoutMs, label) {
28
+ const t0 = Date.now()
29
+ while (Date.now() - t0 < timeoutMs) {
30
+ try { const v = await fn(); if (v) return v } catch { /* retry */ }
31
+ await new Promise((r) => setTimeout(r, 400))
32
+ }
33
+ throw new Error('timeout waiting for ' + label)
34
+ }
35
+
36
+ const wsUrl = await waitFor(async () => {
37
+ const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json()
38
+ return list.find((t) => t.type === 'page')?.webSocketDebuggerUrl
39
+ }, 20000, 'cdp endpoint')
40
+
41
+ const target = await waitFor(async () => {
42
+ const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json()
43
+ const page = list.find((t) => t.type === 'page')
44
+ return page
45
+ }, 20000, 'page target')
46
+
47
+ const ws = new WebSocket(target.webSocketDebuggerUrl)
48
+ let seq = 0
49
+ const pending = new Map()
50
+ const events = []
51
+ ws.onmessage = (m) => {
52
+ const msg = JSON.parse(m.data)
53
+ if (msg.id !== undefined && pending.has(msg.id)) {
54
+ pending.get(msg.id)(msg)
55
+ pending.delete(msg.id)
56
+ return
57
+ }
58
+ if (msg.method === 'Runtime.consoleAPICalled') {
59
+ const args = (msg.params.args || []).map((a) => a.value ?? a.description ?? '').join(' ')
60
+ events.push(`[console.${msg.params.type}] ${args.slice(0, 300)}`)
61
+ console.log(`EVENT ${new Date().toISOString()} ${args.slice(0, 300)}`)
62
+ } else if (msg.method === 'Runtime.exceptionThrown') {
63
+ const d = msg.params.exceptionDetails
64
+ events.push(`[pageerror] ${d?.exception?.description ?? d?.text ?? ''}`.slice(0, 500))
65
+ console.log(`EVENT ${new Date().toISOString()} [pageerror] ${(d?.exception?.description ?? d?.text ?? '').slice(0, 400)}`)
66
+ }
67
+ }
68
+ function rpc(method, params = {}) {
69
+ return new Promise((resolve) => {
70
+ const id = ++seq
71
+ pending.set(id, resolve)
72
+ ws.send(JSON.stringify({ id, method, params }))
73
+ })
74
+ }
75
+ await new Promise((r) => { ws.onopen = r })
76
+ await rpc('Runtime.enable')
77
+ await rpc('Page.enable')
78
+ await rpc('Page.navigate', { url: APP_URL })
79
+
80
+ await waitFor(async () => {
81
+ const r = await rpc('Runtime.evaluate', { expression: '!!window.__DSH_BOOT__', returnByValue: true })
82
+ return r.result?.result?.value
83
+ }, 30000, 'boot')
84
+
85
+ if (PINNED_SESSION) {
86
+ await rpc('Runtime.evaluate', {
87
+ expression: `localStorage.setItem('dsh.sessions.current', JSON.stringify({ id: ${JSON.stringify(PINNED_SESSION)} }))`,
88
+ returnByValue: true,
89
+ })
90
+ console.log(`${new Date().toISOString()} pinned session ${PINNED_SESSION}, reloading…`)
91
+ await rpc('Page.reload')
92
+ await waitFor(async () => {
93
+ const r = await rpc('Runtime.evaluate', { expression: '!!window.__DSH_BOOT__', returnByValue: true })
94
+ return r.result?.result?.value
95
+ }, 30000, 'boot after reload')
96
+ // 等窗口打开(历史含上一次通知),再 dump 快照:验证「通知节点在不在已开窗口」
97
+ await new Promise((r) => setTimeout(r, 9000))
98
+ const dbg = await rpc('Runtime.evaluate', {
99
+ expression: `window.__dsch_notify_debug ? window.__dsch_notify_debug.snapshotDebug(${JSON.stringify(PINNED_SESSION)}) : 'no debug hook'`,
100
+ returnByValue: true,
101
+ })
102
+ console.log(`${new Date().toISOString()} snapshotDebug: ${dbg.result?.result?.value}`)
103
+ }
104
+
105
+ console.log(`${new Date().toISOString()} booted, waiting for completion push (timeout ${TIMEOUT_MS}ms)…`)
106
+
107
+ const t0 = Date.now()
108
+ let fired = null
109
+ while (Date.now() - t0 < TIMEOUT_MS) {
110
+ await new Promise((r) => setTimeout(r, 2000))
111
+ const r = await rpc('Runtime.evaluate', {
112
+ expression: `(() => {
113
+ const root = document.querySelector('[data-dsh-notify-root]')
114
+ const toasts = root ? root.children.length : 0
115
+ const text = toasts > 0 ? Array.from(root.children).map((el) => el.textContent || '').join(' | ') : ''
116
+ return JSON.stringify({ toasts, text })
117
+ })()`,
118
+ returnByValue: true,
119
+ })
120
+ const res = JSON.parse(r.result?.result?.value ?? '{}')
121
+ const toasts = res.toasts ?? 0
122
+ if (toasts > 0 || events.some((e) => e.includes('notification'))) {
123
+ fired = { toasts, text: res.text || '', events: events.slice() }
124
+ console.log(`${new Date().toISOString()} PUSH FIRED: ${JSON.stringify(fired)}`)
125
+ break
126
+ }
127
+ }
128
+
129
+ console.log(`${new Date().toISOString()} RESULT: ${fired ? 'PUSH-E2E-OK' : 'NO-PUSH-IN-WINDOW'}`)
130
+ try { ws.close() } catch {}
131
+ chrome.kill()
132
+ await new Promise((r) => setTimeout(r, 300))
133
+ try { rmSync(profile, { recursive: true, force: true }) } catch {}
134
+ process.exit(0)
@@ -0,0 +1,145 @@
1
+ // probe-client.mjs — headless Chrome 验证 dsh-session-complete-notify 客户端插件:
2
+ // 1) 打开 http://127.0.0.1:3080
3
+ // 2) 采集 console / pageerror
4
+ // 3) 检查 boot graph 与模块装载
5
+ // 用法: node scripts/probe-client.mjs
6
+ import { spawn } from 'node:child_process'
7
+ import { mkdtempSync, rmSync } from 'node:fs'
8
+ import { tmpdir } from 'node:os'
9
+ import { join } from 'node:path'
10
+
11
+ const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe'
12
+ const PORT = 9333
13
+ const APP_URL = 'http://127.0.0.1:3080'
14
+ const profile = mkdtempSync(join(tmpdir(), 'dsh-probe-'))
15
+
16
+ const chrome = spawn(CHROME, [
17
+ '--headless=new',
18
+ `--remote-debugging-port=${PORT}`,
19
+ `--user-data-dir=${profile}`,
20
+ '--no-first-run',
21
+ '--disable-gpu',
22
+ '--window-size=1400,900',
23
+ 'about:blank',
24
+ ], { stdio: 'ignore' })
25
+
26
+ async function waitFor(fn, timeoutMs, label) {
27
+ const t0 = Date.now()
28
+ while (Date.now() - t0 < timeoutMs) {
29
+ try { const v = await fn(); if (v) return v } catch { /* retry */ }
30
+ await new Promise((r) => setTimeout(r, 300))
31
+ }
32
+ throw new Error('timeout waiting for ' + label)
33
+ }
34
+
35
+ const wsUrl = await waitFor(async () => {
36
+ const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json()
37
+ const page = list.find((t) => t.type === 'page')
38
+ return page?.webSocketDebuggerUrl
39
+ }, 20000, 'cdp endpoint')
40
+
41
+ const ws = new WebSocket(wsUrl)
42
+ let seq = 0
43
+ const pending = new Map()
44
+ const events = []
45
+ ws.onmessage = (m) => {
46
+ const msg = JSON.parse(m.data)
47
+ if (msg.id !== undefined && pending.has(msg.id)) {
48
+ pending.get(msg.id)(msg)
49
+ pending.delete(msg.id)
50
+ return
51
+ }
52
+ if (msg.method === 'Runtime.consoleAPICalled') {
53
+ const args = (msg.params.args || []).map((a) => a.value ?? a.description ?? '').join(' ')
54
+ events.push(`[console.${msg.params.type}] ${args}`)
55
+ } else if (msg.method === 'Runtime.exceptionThrown') {
56
+ events.push(`[pageerror] ${msg.params.exceptionDetails?.text ?? ''} ${msg.params.exceptionDetails?.exception?.description ?? ''}`)
57
+ } else if (msg.method === 'Network.loadingFailed') {
58
+ events.push(`[net-fail] ${msg.params.errorText} ${msg.params.requestId}`)
59
+ }
60
+ }
61
+ function rpc(method, params = {}) {
62
+ return new Promise((resolve) => {
63
+ const id = ++seq
64
+ pending.set(id, resolve)
65
+ ws.send(JSON.stringify({ id, method, params }))
66
+ })
67
+ }
68
+ await new Promise((r) => { ws.onopen = r })
69
+ await rpc('Runtime.enable')
70
+ await rpc('Network.enable')
71
+ await rpc('Page.enable')
72
+
73
+ const target = await rpc('Target.createTarget', { url: 'about:blank' })
74
+ const targetId = target.result?.targetId
75
+ // 直接在当前页面导航更简单:拿新的 page target
76
+ const list2 = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json()
77
+ const pageTarget = list2.find((t) => t.id === targetId) ?? list2.find((t) => t.type === 'page')
78
+ const ws2 = new WebSocket(pageTarget.webSocketDebuggerUrl)
79
+ let seq2 = 0
80
+ const pending2 = new Map()
81
+ const pageEvents = []
82
+ ws2.onmessage = (m) => {
83
+ const msg = JSON.parse(m.data)
84
+ if (msg.id !== undefined && pending2.has(msg.id)) {
85
+ pending2.get(msg.id)(msg)
86
+ pending2.delete(msg.id)
87
+ return
88
+ }
89
+ if (msg.method === 'Runtime.consoleAPICalled') {
90
+ const args = (msg.params.args || []).map((a) => a.value ?? a.description ?? '').join(' ')
91
+ pageEvents.push(`[console.${msg.params.type}] ${args.slice(0, 300)}`)
92
+ } else if (msg.method === 'Runtime.exceptionThrown') {
93
+ const d = msg.params.exceptionDetails
94
+ pageEvents.push(`[pageerror] ${d?.exception?.description ?? d?.text ?? ''}`.slice(0, 500))
95
+ }
96
+ }
97
+ function rpc2(method, params = {}) {
98
+ return new Promise((resolve) => {
99
+ const id = ++seq2
100
+ pending2.set(id, resolve)
101
+ ws2.send(JSON.stringify({ id, method, params }))
102
+ })
103
+ }
104
+ await new Promise((r) => { ws2.onopen = r })
105
+ await rpc2('Runtime.enable')
106
+ await rpc2('Page.enable')
107
+ await rpc2('Page.navigate', { url: APP_URL })
108
+
109
+ await waitFor(async () => {
110
+ const r = await rpc2('Runtime.evaluate', { expression: '!!window.__ModuleLoader__ && !!window.__DSH_BOOT__', returnByValue: true })
111
+ return r.result?.result?.value
112
+ }, 30000, 'boot')
113
+
114
+ // 等待客户端插件激活(console 日志由 apply 打印)
115
+ await new Promise((r) => setTimeout(r, 6000))
116
+
117
+ const checks = {}
118
+ {
119
+ const r = await rpc2('Runtime.evaluate', {
120
+ expression: `(() => {
121
+ const boot = window.__DSH_BOOT__ || {}
122
+ const entries = (boot.entries || []).map(e => e.id)
123
+ return JSON.stringify({
124
+ graphLength: entries.length,
125
+ hasEntry: entries.includes('@telosmaylx/dsh-session-notify'),
126
+ entry: entries.find(e => e.includes('session-notify')),
127
+ notificationSupported: typeof window.Notification !== 'undefined',
128
+ notificationPermission: typeof window.Notification !== 'undefined' ? window.Notification.permission : 'n/a',
129
+ })
130
+ })()`,
131
+ returnByValue: true,
132
+ })
133
+ checks.graph = JSON.parse(r.result?.result?.value ?? '{}')
134
+ }
135
+
136
+ console.log('=== console/pageerror events ===')
137
+ for (const e of pageEvents) console.log(e)
138
+ console.log('=== checks ===')
139
+ console.log(JSON.stringify(checks, null, 2))
140
+
141
+ try { ws.close() } catch {}
142
+ try { ws2.close() } catch {}
143
+ chrome.kill()
144
+ await new Promise((r) => setTimeout(r, 300))
145
+ try { rmSync(profile, { recursive: true, force: true }) } catch {}
@@ -0,0 +1,79 @@
1
+ // probe-diag-settings.mjs — 诊断设置面板入口:列出含 data-* 属性的元素与设置相关文本
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 = 9377
9
+ const profile = mkdtempSync(join(tmpdir(), 'dsh-diag2-'))
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, 400)) }
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
+ ws.onmessage = (m) => { const msg = JSON.parse(m.data); if (msg.id !== undefined && pending.has(msg.id)) { pending.get(msg.id)(msg); pending.delete(msg.id) } }
25
+ function rpc(method, params = {}) { return new Promise((res) => { const id = ++seq; pending.set(id, res); ws.send(JSON.stringify({ id, method, params })) }) }
26
+ await new Promise((r) => { ws.onopen = r })
27
+ await rpc('Runtime.enable')
28
+ await rpc('Page.enable')
29
+ await rpc('Page.navigate', { url: 'http://127.0.0.1:3080' })
30
+ await waitFor(async () => { const r = await rpc('Runtime.evaluate', { expression: '!!window.__DSH_BOOT__', returnByValue: true }); return r.result?.result?.value }, 30000, 'boot')
31
+ await new Promise((r) => setTimeout(r, 5000))
32
+
33
+ const diag = await rpc('Runtime.evaluate', {
34
+ expression: `(() => {
35
+ const out = { dataAttrs: [], settingTexts: [] }
36
+ const all = document.querySelectorAll('[data-*]')
37
+ const seen = new Set()
38
+ document.querySelectorAll('[data-dsh-settings-root], [data-settings], [data-settings-page], [data-view]').forEach((e) => out.dataAttrs.push((e.getAttribute('data-dsh-settings-root') || e.getAttribute('data-settings') || e.getAttribute('data-settings-page') || e.getAttribute('data-view') || '') + ' :: ' + (e.textContent || '').slice(0, 40)))
39
+ // 找文本节点包含 设置 的元素
40
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT)
41
+ let node
42
+ while ((node = walker.nextNode())) {
43
+ const t = (node.textContent || '').trim()
44
+ if (t === '设置') { const p = node.parentElement; const tag = p?.tagName; const attr = p?.outerHTML?.slice(0, 160); out.settingTexts.push(tag + ' :: ' + attr) }
45
+ }
46
+ return JSON.stringify(out)
47
+ })()`,
48
+ returnByValue: true,
49
+ })
50
+ console.log('DIAG:', diag.result?.result?.value)
51
+
52
+ // 点击 settings 入口后再 dump body 结构
53
+ await rpc('Runtime.evaluate', {
54
+ expression: `(() => {
55
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT)
56
+ let node
57
+ while ((node = walker.nextNode())) {
58
+ if ((node.textContent || '').trim() === '设置') { node.parentElement?.click(); return 'clicked' }
59
+ }
60
+ return 'none'
61
+ })()`,
62
+ returnByValue: true,
63
+ })
64
+ await new Promise((r) => setTimeout(r, 3000))
65
+ const post = await rpc('Runtime.evaluate', {
66
+ expression: `(() => {
67
+ const body = document.body.innerText
68
+ const idx = body.indexOf('设置')
69
+ return JSON.stringify({ len: body.length, ctx: body.slice(idx, idx + 500) })
70
+ })()`,
71
+ returnByValue: true,
72
+ })
73
+ console.log('POST:', post.result?.result?.value)
74
+
75
+ try { ws.close() } catch {}
76
+ chrome.kill()
77
+ await new Promise((r) => setTimeout(r, 300))
78
+ try { rmSync(profile, { recursive: true, force: true }) } catch {}
79
+ process.exit(0)
@@ -0,0 +1,137 @@
1
+ // probe-settings-card.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 = 9355
9
+ const APP_URL = 'http://127.0.0.1:3080'
10
+ const profile = mkdtempSync(join(tmpdir(), 'dsh-card-'))
11
+
12
+ const chrome = spawn(CHROME, ['--headless=new', `--remote-debugging-port=${PORT}`, `--user-data-dir=${profile}`, '--no-first-run', '--disable-gpu', '--window-size=1400,900', 'about:blank'], { stdio: 'ignore' })
13
+
14
+ async function waitFor(fn, timeoutMs, label) {
15
+ const t0 = Date.now()
16
+ while (Date.now() - t0 < timeoutMs) {
17
+ try { const v = await fn(); if (v) return v } catch { /* retry */ }
18
+ await new Promise((r) => setTimeout(r, 400))
19
+ }
20
+ throw new Error('timeout waiting for ' + label)
21
+ }
22
+
23
+ const page = await waitFor(async () => {
24
+ const list = await (await fetch(`http://127.0.0.1:${PORT}/json/list`)).json()
25
+ return list.find((t) => t.type === 'page')
26
+ }, 20000, 'page')
27
+
28
+ const ws = new WebSocket(page.webSocketDebuggerUrl)
29
+ let seq = 0
30
+ const pending = new Map()
31
+ const events = []
32
+ ws.onmessage = (m) => {
33
+ const msg = JSON.parse(m.data)
34
+ if (msg.id !== undefined && pending.has(msg.id)) { pending.get(msg.id)(msg); pending.delete(msg.id); return }
35
+ if (msg.method === 'Runtime.consoleAPICalled') {
36
+ const args = (msg.params.args || []).map((a) => a.value ?? a.description ?? '').join(' ')
37
+ console.log('CONSOLE:', args.slice(0, 200))
38
+ events.push(args.slice(0, 200))
39
+ } else if (msg.method === 'Runtime.exceptionThrown') {
40
+ const d = msg.params.exceptionDetails
41
+ console.log('PAGEERROR:', (d?.exception?.description ?? d?.text ?? '').slice(0, 400))
42
+ }
43
+ }
44
+ function rpc(method, params = {}) {
45
+ return new Promise((resolve) => {
46
+ const id = ++seq
47
+ pending.set(id, resolve)
48
+ ws.send(JSON.stringify({ id, method, params }))
49
+ })
50
+ }
51
+ await new Promise((r) => { ws.onopen = r })
52
+ await rpc('Runtime.enable')
53
+ await rpc('Page.enable')
54
+ await rpc('Page.navigate', { url: APP_URL })
55
+ await waitFor(async () => {
56
+ const r = await rpc('Runtime.evaluate', { expression: '!!window.__DSH_BOOT__', returnByValue: true })
57
+ return r.result?.result?.value
58
+ }, 30000, 'boot')
59
+ await new Promise((r) => setTimeout(r, 5000)) // 等 UI 就绪
60
+
61
+ const clickByText = async (text, selector) => {
62
+ const r = await rpc('Runtime.evaluate', {
63
+ expression: `(() => {
64
+ const els = Array.from(document.querySelectorAll(${JSON.stringify(selector ?? 'button, [role="button"], [data-menu-item], a, div')}))
65
+ const el = els.find((e) => (e.textContent || '').trim().includes(${JSON.stringify(text)}) && (e.textContent || '').trim().length < 30)
66
+ if (!el) return 'NOT_FOUND:' + text
67
+ el.click()
68
+ return 'CLICKED:' + (el.textContent || '').trim().slice(0, 20)
69
+ })()`,
70
+ returnByValue: true,
71
+ })
72
+ console.log(r.result?.result?.value)
73
+ }
74
+
75
+ // 1) 打开设置:找"设置"入口
76
+ const clickResult1 = await rpc('Runtime.evaluate', {
77
+ expression: `(() => {
78
+ const els = Array.from(document.querySelectorAll('*')).filter((e) => {
79
+ const t = (e.textContent || '').trim(); const aria = e.getAttribute('aria-label') || ''; const title = e.getAttribute('title') || ''
80
+ return (t === '设置' || aria.includes('设置') || title.includes('设置')) && t.length < 12
81
+ })
82
+ const el = els[0]
83
+ if (!el) return 'none-found'
84
+ el.click()
85
+ return 'clicked: ' + (el.getAttribute('aria-label') || el.textContent || '').trim().slice(0, 20)
86
+ })()`,
87
+ returnByValue: true,
88
+ })
89
+ console.log('OPEN-SETTINGS:', clickResult1.result?.result?.value)
90
+ await new Promise((r) => setTimeout(r, 2500))
91
+ // 2) 打印面板内标签文本,找"插件"tab
92
+ const tabsDump = await rpc('Runtime.evaluate', {
93
+ expression: `(() => {
94
+ const found = []
95
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT)
96
+ let node
97
+ while ((node = walker.nextNode())) {
98
+ const t = (node.textContent || '').trim()
99
+ if (t && t.length < 24 && (t.includes('插件') || t.includes('Plugins') || t.includes('通用') || t.includes('模型') || t.includes('General') || t.includes('Models'))) found.push(t)
100
+ }
101
+ return JSON.stringify([...new Set(found)].slice(0, 40))
102
+ })()`,
103
+ returnByValue: true,
104
+ })
105
+ console.log('PLUGIN-TEXTS:', tabsDump.result?.result?.value)
106
+ // 3) 点含"插件"的最短文本节点(任意可点元素)
107
+ const clickTab = await rpc('Runtime.evaluate', {
108
+ expression: `(() => {
109
+ const els = Array.from(document.querySelectorAll('button, [role=tab], [role=button], [data-state], li, div, a'))
110
+ .filter((e) => (e.textContent || '').trim().length > 0 && (e.textContent || '').trim().length < 24 && (e.textContent || '').includes('插件'))
111
+ .sort((a, b) => (a.textContent || '').trim().length - (b.textContent || '').trim().length)
112
+ const el = els[0]
113
+ if (!el) return 'none-found'
114
+ el.click()
115
+ return 'clicked: ' + (el.textContent || '').trim()
116
+ })()`,
117
+ returnByValue: true,
118
+ })
119
+ console.log('CLICK-TAB:', clickTab.result?.result?.value)
120
+ await new Promise((r) => setTimeout(r, 3000))
121
+ // 3) 检查卡片文本
122
+ const found = await rpc('Runtime.evaluate', {
123
+ expression: `(() => {
124
+ const text = document.body.innerText || ''
125
+ const has = text.includes('会话完成提醒')
126
+ const hasNg = text.includes('会话完成提醒') && (text.includes('占位符') || text.includes('跳过子代理'))
127
+ return JSON.stringify({ hasCard: has, hasCardForm: hasNg, cardCtx: text.slice(text.indexOf('会话完成提醒') - 60, text.indexOf('会话完成提醒') + 260) })
128
+ })()`,
129
+ returnByValue: true,
130
+ })
131
+ console.log('CARD-CHECK:', found.result?.result?.value)
132
+
133
+ try { ws.close() } catch {}
134
+ chrome.kill()
135
+ await new Promise((r) => setTimeout(r, 300))
136
+ try { rmSync(profile, { recursive: true, force: true }) } catch {}
137
+ process.exit(0)