@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
package/package.json
CHANGED
|
@@ -1,66 +1,70 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@telosmaylx/dsh-session-notify",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "DSH session completion notifier: appends a plugin system message to the session log on turn/end and pushes browser notifications (Web Notification + toast), with a fully customizable official settings panel (presets, 5-language templates, cache-hit rate & tok/s from official projections).",
|
|
5
|
-
"private": false,
|
|
6
|
-
"type": "module",
|
|
7
|
-
"main": "./lib/index.js",
|
|
8
|
-
"exports": {
|
|
9
|
-
".": {
|
|
10
|
-
"default": "./lib/index.js"
|
|
11
|
-
},
|
|
12
|
-
"./client": {
|
|
13
|
-
"default": "./lib/client.js"
|
|
14
|
-
},
|
|
15
|
-
"./core": {
|
|
16
|
-
"default": "./lib/core.js"
|
|
17
|
-
},
|
|
18
|
-
"./package.json": "./package.json"
|
|
19
|
-
},
|
|
20
|
-
"files": [
|
|
21
|
-
"lib",
|
|
22
|
-
"scripts",
|
|
23
|
-
"
|
|
24
|
-
"
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"
|
|
32
|
-
"dsh
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@telosmaylx/dsh-session-notify",
|
|
3
|
+
"version": "0.1.3",
|
|
4
|
+
"description": "DSH session completion notifier: appends a plugin system message to the session log on turn/end and pushes browser notifications (Web Notification + toast), with a fully customizable official settings panel (presets, 5-language templates, cache-hit rate & tok/s from official projections).",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"default": "./lib/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./client": {
|
|
13
|
+
"default": "./lib/client.js"
|
|
14
|
+
},
|
|
15
|
+
"./core": {
|
|
16
|
+
"default": "./lib/core.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"lib",
|
|
22
|
+
"scripts",
|
|
23
|
+
"cordis.patch.yml",
|
|
24
|
+
"README.md",
|
|
25
|
+
"LICENSE"
|
|
26
|
+
],
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"deepseek-harness",
|
|
32
|
+
"dsh",
|
|
33
|
+
"dsh-plugin",
|
|
34
|
+
"session-notify",
|
|
35
|
+
"notification",
|
|
36
|
+
"completion-reminder",
|
|
37
|
+
"agent"
|
|
38
|
+
],
|
|
39
|
+
"author": "dsh-session-notify contributors",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"repository": {
|
|
42
|
+
"type": "git",
|
|
43
|
+
"url": "git+https://github.com/TelosmaYLX/dsh-session-notify.git"
|
|
44
|
+
},
|
|
45
|
+
"homepage": "https://github.com/TelosmaYLX/dsh-session-notify#readme",
|
|
46
|
+
"bugs": {
|
|
47
|
+
"url": "https://github.com/TelosmaYLX/dsh-session-notify/issues"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"cordis": ">=4.0.0-rc <5"
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=22"
|
|
54
|
+
},
|
|
55
|
+
"dsh": {
|
|
56
|
+
"bundle": {
|
|
57
|
+
"patch": "./cordis.patch.yml"
|
|
58
|
+
},
|
|
59
|
+
"client": {
|
|
60
|
+
"inject": [
|
|
61
|
+
"@deepseek-ai/dsh-client-runtime"
|
|
62
|
+
],
|
|
63
|
+
"platform": "web"
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"scripts": {
|
|
67
|
+
"build": "bash scripts/build.sh || node --check lib/index.js && node --check lib/core.js && node --check lib/client.js",
|
|
68
|
+
"prepublishOnly": "node --check lib/index.js && node --check lib/core.js && node --check lib/client.js"
|
|
69
|
+
}
|
|
70
|
+
}
|
package/scripts/build.sh
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# Build dsh-session-complete-notify: the plugin is hand-written zero-dependency
|
|
3
|
-
# ESM — no tsc step, just syntax-verify the shipped libs.
|
|
4
|
-
set -euo pipefail
|
|
5
|
-
|
|
6
|
-
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
7
|
-
cd "$ROOT"
|
|
8
|
-
|
|
9
|
-
if [ ! -f lib/index.js ] || [ ! -f lib/core.js ]; then
|
|
10
|
-
echo "build: lib/index.js or lib/core.js missing" >&2
|
|
11
|
-
exit 1
|
|
12
|
-
fi
|
|
13
|
-
|
|
14
|
-
node --check lib/index.js
|
|
15
|
-
node --check lib/core.js
|
|
16
|
-
echo "=== Build complete (lib verified, ${PWD}) ==="
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Build dsh-session-complete-notify: the plugin is hand-written zero-dependency
|
|
3
|
+
# ESM — no tsc step, just syntax-verify the shipped libs.
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
7
|
+
cd "$ROOT"
|
|
8
|
+
|
|
9
|
+
if [ ! -f lib/index.js ] || [ ! -f lib/core.js ]; then
|
|
10
|
+
echo "build: lib/index.js or lib/core.js missing" >&2
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
node --check lib/index.js
|
|
15
|
+
node --check lib/core.js
|
|
16
|
+
echo "=== Build complete (lib verified, ${PWD}) ==="
|
|
@@ -1,100 +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)
|
|
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)
|
|
@@ -1,134 +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)
|
|
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)
|