From 9d7de039334f7854da45e8406d538c079d899ed7 Mon Sep 17 00:00:00 2001 From: oliver Date: Sat, 10 Oct 2026 09:10:23 +0800 Subject: [PATCH] Keep a small Desktop diagnostic script set and ignore local scratch. Drop bak/tmp/probe leftovers from the workspace; track status/form/settings checks alongside the existing config and takeover verifiers. Co-authored-by: Cursor --- .gitignore | 16 +++ scripts/_check-form.mjs | 189 +++++++++++++++++++++++++++++++++++ scripts/_status.mjs | 20 ++++ scripts/_verify-settings.mjs | 186 ++++++++++++++++++++++++++++++++++ 4 files changed, 411 insertions(+) create mode 100644 scripts/_check-form.mjs create mode 100644 scripts/_status.mjs create mode 100644 scripts/_verify-settings.mjs diff --git a/.gitignore b/.gitignore index 3fb7a06..6364c1a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,22 @@ node_modules/ *.tgz .DS_Store +# Local scratch / one-off Desktop probes (do not commit). +*.bak +*.bak-* +lib/*.bak* +scripts/_tmp_* +scripts/_asar-* +scripts/_build-* +scripts/_patch-* +scripts/_restore-* +scripts/_fix-* +scripts/_swap-* +scripts/_bump-* +scripts/_sync-* +scripts/_open-* +scripts/_probe-* + # Local security scan state and private project handoff. .mimosa/ PROJECT_CONTEXT.md diff --git a/scripts/_check-form.mjs b/scripts/_check-form.mjs new file mode 100644 index 0000000..15f1fcd --- /dev/null +++ b/scripts/_check-form.mjs @@ -0,0 +1,189 @@ +import http from 'node:http' +import net from 'node:net' +import crypto from 'node:crypto' + +const timeoutMs = 15000 + +function httpGetJson(url) { + return new Promise((resolve, reject) => { + const req = http.get(url, { timeout: 3000 }, (res) => { + let data = '' + res.on('data', (c) => { data += c }) + res.on('end', () => { + try { resolve(JSON.parse(data)) } catch (e) { reject(e) } + }) + }) + req.on('error', reject) + req.on('timeout', () => { req.destroy(); reject(new Error('http timeout')) }) + }) +} + +class Cdp { + constructor(wsUrl) { + this.wsUrl = wsUrl + this.id = 0 + this.pending = new Map() + this.buf = Buffer.alloc(0) + } + async connect() { + const u = new URL(this.wsUrl) + const key = crypto.randomBytes(16).toString('base64') + await new Promise((resolve, reject) => { + const socket = net.connect({ host: u.hostname, port: Number(u.port) }, () => { + socket.write( + `GET ${u.pathname}${u.search} HTTP/1.1\r\nHost: ${u.host}\r\n` + + `Upgrade: websocket\r\nConnection: Upgrade\r\n` + + `Sec-WebSocket-Key: ${key}\r\nSec-WebSocket-Version: 13\r\n\r\n`, + ) + }) + socket.setTimeout(timeoutMs) + socket.on('timeout', () => reject(new Error('socket timeout'))) + socket.on('error', reject) + socket.once('data', (chunk) => { + if (!chunk.toString().includes('101')) return reject(new Error('upgrade fail')) + this.socket = socket + const rest = chunk.indexOf('\r\n\r\n') + if (rest >= 0 && rest + 4 < chunk.length) this.buf = chunk.subarray(rest + 4) + socket.on('data', (c) => this.onData(c)) + resolve() + }) + }) + } + onData(chunk) { + this.buf = Buffer.concat([this.buf, chunk]) + while (true) { + if (this.buf.length < 2) return + let len = this.buf[1] & 0x7f + let off = 2 + if (len === 126) { + if (this.buf.length < 4) return + len = this.buf.readUInt16BE(2) + off = 4 + } else if (len === 127) { + if (this.buf.length < 10) return + len = Number(this.buf.readBigUInt64BE(2)) + off = 10 + } + if (this.buf.length < off + len) return + const payload = this.buf.subarray(off, off + len) + this.buf = this.buf.subarray(off + len) + try { + const msg = JSON.parse(payload.toString()) + if (msg.id && this.pending.has(msg.id)) { + const { resolve, reject } = this.pending.get(msg.id) + this.pending.delete(msg.id) + msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result) + } + } catch { /* ignore */ } + } + } + send(obj) { + const data = Buffer.from(JSON.stringify(obj)) + const len = data.length + let header + if (len < 126) header = Buffer.from([0x81, 0x80 | len]) + else if (len < 65536) { + header = Buffer.alloc(4) + header[0] = 0x81 + header[1] = 0x80 | 126 + header.writeUInt16BE(len, 2) + } else { + header = Buffer.alloc(10) + header[0] = 0x81 + header[1] = 0x80 | 127 + header.writeBigUInt64BE(BigInt(len), 2) + } + const mask = crypto.randomBytes(4) + const masked = Buffer.alloc(len) + for (let i = 0; i < len; i++) masked[i] = data[i] ^ mask[i % 4] + this.socket.write(Buffer.concat([header, mask, masked])) + } + call(method, params = {}) { + const id = ++this.id + return new Promise((resolve, reject) => { + const t = setTimeout(() => { + this.pending.delete(id) + reject(new Error(`timeout ${method}`)) + }, timeoutMs) + this.pending.set(id, { + resolve: (v) => { clearTimeout(t); resolve(v) }, + reject: (e) => { clearTimeout(t); reject(e) }, + }) + this.send({ id, method, params }) + }) + } + close() { + try { this.socket?.destroy() } catch { /* ignore */ } + } +} + +async function evalExpr(cdp, expression) { + const result = await cdp.call('Runtime.evaluate', { + expression, + returnByValue: true, + awaitPromise: true, + }) + if (result.exceptionDetails) throw new Error(JSON.stringify(result.exceptionDetails)) + return result.result?.value +} + +const hard = setTimeout(() => { console.error('HARD_TIMEOUT'); process.exit(2) }, 90000) + +try { + // Ensure CDP + let tabs + try { + tabs = await httpGetJson('http://127.0.0.1:9222/json') + } catch { + console.error('CDP not available') + process.exit(5) + } + const page = tabs.find((t) => t.type === 'page' && t.webSocketDebuggerUrl) + if (!page) throw new Error('no page') + const cdp = new Cdp(page.webSocketDebuggerUrl) + await cdp.connect() + await cdp.call('Runtime.enable') + + const result = await evalExpr(cdp, `(async () => { + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) + const visible = (el) => !!(el && el.offsetParent !== null) + const click = (el) => { + if (!el) return false + el.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })) + el.click?.() + return true + } + const all = () => [...document.querySelectorAll('button,a,[role="button"],[role="tab"],[role="menuitem"],div,span,li,h1,h2,h3,p,label')] + const byExact = (re) => all().find((e) => visible(e) && re.test((e.textContent || '').trim())) + + // Open settings + click(byExact(/^设置$/) || all().find((e) => visible(e) && /settings|设置/i.test(e.getAttribute('aria-label') || ''))) + await sleep(900) + + // Click 搜索 MCP + const nav = byExact(/^搜索\\s*MCP$/) || byExact(/^Search\\s*MCP$/i) || + all().find((e) => visible(e) && /搜索\\s*MCP|Search\\s*MCP/i.test((e.textContent || '').trim()) && (e.textContent || '').trim().length < 20) + click(nav) + await sleep(1200) + + const t = (document.body && document.body.innerText) || '' + const smcp = !!document.querySelector('.smcp_section, .smcp_card, .smcp_lede, .smcp_footer, .smcp_btn') + return { + hasNav: !!nav, + hasSmcpClass: smcp, + hasSave: /保存|Save|丢弃|Discard/i.test(t), + hasServerUi: /默认服务器|default server|快速添加|Quick add|添加服务器|maxResults|搜索超时|bailian|Tavily|Brave|Exa|自定义/i.test(t), + hasPlaceholder: /表单暂时不可用|Visual editor|下一版挂回|Host defaults/i.test(t), + panelSlice: t.replace(/\\s+/g, ' ').slice(Math.max(0, t.replace(/\\s+/g, ' ').indexOf('搜索 MCP')), Math.max(0, t.replace(/\\s+/g, ' ').indexOf('搜索 MCP')) + 500), + } + })()`) + + console.log(JSON.stringify(result, null, 2)) + cdp.close() + clearTimeout(hard) + process.exit(result.hasServerUi || result.hasSmcpClass ? 0 : 3) +} catch (e) { + console.error(String(e && e.stack || e)) + clearTimeout(hard) + process.exit(1) +} diff --git a/scripts/_status.mjs b/scripts/_status.mjs new file mode 100644 index 0000000..ec838a2 --- /dev/null +++ b/scripts/_status.mjs @@ -0,0 +1,20 @@ +import { readFileSync } from 'node:fs' +import { homedir } from 'node:os' +import { join } from 'node:path' + +const profile = JSON.parse(readFileSync(join(homedir(), '.dsh/profiles/desktop/package.json'), 'utf8')) +const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url))) +const c = readFileSync(new URL('../lib/client.browser.js', import.meta.url), 'utf8') +console.log(JSON.stringify({ + inBundles: profile.dsh.profile.bundles.includes('dsh-search-mcp'), + ver: pkg.version, + imm: pkg.dsh.client.immediately, + lines: c.split(/\n/).length, + settingsScopeInject: /inject\(\["settingsScope"\]/.test(c), + configFormsInject: /inject\(\["configForms"\]/.test(c), + configFormsPoll: c.includes('configForms poll'), + settingsSection: /name:\s*"settings\.section"/.test(c), + forcedFallback: /const createSnapshotStore = createSnapshotStoreFallback/.test(c), + SearchMcpSection: c.includes('SearchMcpSection'), + Placeholder: c.includes('PlaceholderSection') || c.includes('配置表单加载中'), +}, null, 2)) diff --git a/scripts/_verify-settings.mjs b/scripts/_verify-settings.mjs new file mode 100644 index 0000000..5a52f14 --- /dev/null +++ b/scripts/_verify-settings.mjs @@ -0,0 +1,186 @@ +import http from 'node:http' +import net from 'node:net' +import crypto from 'node:crypto' + +const timeoutMs = 12000 + +function httpGetJson(url) { + return new Promise((resolve, reject) => { + const req = http.get(url, { timeout: 3000 }, (res) => { + let data = '' + res.on('data', (c) => { data += c }) + res.on('end', () => { + try { resolve(JSON.parse(data)) } catch (e) { reject(e) } + }) + }) + req.on('error', reject) + req.on('timeout', () => { req.destroy(); reject(new Error('http timeout')) }) + }) +} + +class Cdp { + constructor(wsUrl) { + this.wsUrl = wsUrl + this.id = 0 + this.pending = new Map() + this.buf = Buffer.alloc(0) + } + async connect() { + const u = new URL(this.wsUrl) + const key = crypto.randomBytes(16).toString('base64') + await new Promise((resolve, reject) => { + const socket = net.connect({ host: u.hostname, port: Number(u.port) }, () => { + socket.write( + `GET ${u.pathname}${u.search} HTTP/1.1\r\nHost: ${u.host}\r\n` + + `Upgrade: websocket\r\nConnection: Upgrade\r\n` + + `Sec-WebSocket-Key: ${key}\r\nSec-WebSocket-Version: 13\r\n\r\n`, + ) + }) + socket.setTimeout(timeoutMs) + socket.on('timeout', () => reject(new Error('socket timeout'))) + socket.on('error', reject) + socket.once('data', (chunk) => { + if (!chunk.toString().includes('101')) return reject(new Error('upgrade fail')) + this.socket = socket + const rest = chunk.indexOf('\r\n\r\n') + if (rest >= 0 && rest + 4 < chunk.length) this.buf = chunk.subarray(rest + 4) + socket.on('data', (c) => this.onData(c)) + resolve() + }) + }) + } + onData(chunk) { + this.buf = Buffer.concat([this.buf, chunk]) + while (true) { + if (this.buf.length < 2) return + let len = this.buf[1] & 0x7f + let off = 2 + if (len === 126) { + if (this.buf.length < 4) return + len = this.buf.readUInt16BE(2) + off = 4 + } else if (len === 127) { + if (this.buf.length < 10) return + len = Number(this.buf.readBigUInt64BE(2)) + off = 10 + } + if (this.buf.length < off + len) return + const payload = this.buf.subarray(off, off + len) + this.buf = this.buf.subarray(off + len) + try { + const msg = JSON.parse(payload.toString()) + if (msg.id && this.pending.has(msg.id)) { + const { resolve, reject } = this.pending.get(msg.id) + this.pending.delete(msg.id) + msg.error ? reject(new Error(JSON.stringify(msg.error))) : resolve(msg.result) + } + } catch { /* ignore */ } + } + } + send(obj) { + const data = Buffer.from(JSON.stringify(obj)) + const len = data.length + let header + if (len < 126) header = Buffer.from([0x81, 0x80 | len]) + else if (len < 65536) { + header = Buffer.alloc(4) + header[0] = 0x81 + header[1] = 0x80 | 126 + header.writeUInt16BE(len, 2) + } else { + header = Buffer.alloc(10) + header[0] = 0x81 + header[1] = 0x80 | 127 + header.writeBigUInt64BE(BigInt(len), 2) + } + const mask = crypto.randomBytes(4) + const masked = Buffer.alloc(len) + for (let i = 0; i < len; i++) masked[i] = data[i] ^ mask[i % 4] + this.socket.write(Buffer.concat([header, mask, masked])) + } + call(method, params = {}) { + const id = ++this.id + return new Promise((resolve, reject) => { + const t = setTimeout(() => { + this.pending.delete(id) + reject(new Error(`timeout ${method}`)) + }, timeoutMs) + this.pending.set(id, { + resolve: (v) => { clearTimeout(t); resolve(v) }, + reject: (e) => { clearTimeout(t); reject(e) }, + }) + this.send({ id, method, params }) + }) + } + close() { + try { this.socket?.destroy() } catch { /* ignore */ } + } +} + +async function evalExpr(cdp, expression) { + const result = await cdp.call('Runtime.evaluate', { + expression, + returnByValue: true, + awaitPromise: true, + }) + return result.result?.value +} + +const timer = setTimeout(() => { + console.error('HARD_TIMEOUT') + process.exit(2) +}, 60000) + +try { + const tabs = await httpGetJson('http://127.0.0.1:9222/json') + const page = tabs.find((t) => t.type === 'page' && t.webSocketDebuggerUrl) + if (!page) throw new Error('no page') + const cdp = new Cdp(page.webSocketDebuggerUrl) + await cdp.connect() + await cdp.call('Runtime.enable') + + const boot = await evalExpr(cdp, `(() => { + const t = (document.body && document.body.innerText) || '' + return { + loading: /Loading plugins/i.test(t), + bad: /unavailable|无法使用|web boot|did not activate/i.test(t), + head: t.replace(/\\s+/g, ' ').slice(0, 160), + } + })()`) + console.log('boot', JSON.stringify(boot)) + + // Open settings if needed + await evalExpr(cdp, `(async () => { + const sleep = (ms) => new Promise((r) => setTimeout(r, ms)) + const click = (el) => { el?.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true, view: window })); el?.click?.() } + const byText = (re) => [...document.querySelectorAll('button,a,[role="button"],div,span')].find((e) => re.test((e.textContent||'').trim()) && e.offsetParent !== null) + // Prefer sidebar gear / 设置 + let btn = byText(/^设置$|^Settings$/i) || byText(/设置|Settings/) + if (btn) { click(btn); await sleep(800) } + return { opened: !!btn } + })()`) + + await new Promise((r) => setTimeout(r, 1000)) + + const nav = await evalExpr(cdp, `(() => { + const t = (document.body && document.body.innerText) || '' + const items = [...document.querySelectorAll('button,a,[role="tab"],[role="menuitem"],nav *')] + .map((e) => (e.textContent||'').replace(/\\s+/g,' ').trim()) + .filter((s) => s && s.length < 40) + const uniq = [...new Set(items)] + return { + hasSearchMcp: /搜索\\s*MCP|Search\\s*MCP/i.test(t), + hasNetx: /Netx\\s*Ops|NetX/i.test(t), + sidebarHits: uniq.filter((s) => /搜索|Search|MCP|Netx|定时|插件|设置/i.test(s)).slice(0, 40), + head: t.replace(/\\s+/g, ' ').slice(0, 300), + } + })()`) + console.log('nav', JSON.stringify(nav, null, 2)) + cdp.close() + clearTimeout(timer) + process.exit(nav?.hasSearchMcp ? 0 : 3) +} catch (e) { + console.error(String(e && e.stack || e)) + clearTimeout(timer) + process.exit(1) +}