@telosmaylx/dsh-session-notify 0.1.2 → 0.1.3
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 -21
- package/README.md +202 -196
- package/cordis.patch.yml +10 -0
- package/lib/client.js +1180 -1180
- package/lib/core.js +303 -303
- package/lib/index.js +312 -291
- package/package.json +70 -66
- package/scripts/build.sh +16 -16
- package/scripts/probe-card-render.mjs +100 -100
- package/scripts/probe-client-e2e.mjs +134 -134
- package/scripts/probe-client.mjs +145 -145
- package/scripts/probe-diag-settings.mjs +79 -79
- package/scripts/probe-settings-card.mjs +137 -137
- package/scripts/probe-settings-check.mjs +87 -87
- package/scripts/verify-notice.mjs +110 -110
|
@@ -1,137 +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)
|
|
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)
|
|
@@ -1,87 +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)
|
|
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)
|