Files
inkreach-agent/apps/desktop/scripts/capture-app.cjs
T
Rick Ye a5951849c8 v0.6.0: beautifului 整合 + 会话/工作区/队列/权限/命令/附件全链路
- beautifului.dev 组件直连:Sidebar Nav、Tool Chips、Approval Card(抽屉化)、Streaming/Thinking、Code Block、Selection Actions、Prompt Bar、Search
2026-09-06 09:57:14 +08:00

264 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 真实 App 渲染层截图(离屏,注入假 IPC 数据):
// npx electron scripts/capture-app.cjs
// 产出 preview-shots/app-*.png:展开态 / 收起态 / markdown 渲染
const { app, BrowserWindow, ipcMain } = require('electron')
const { mkdirSync, writeFileSync, existsSync } = require('node:fs')
const { join } = require('node:path')
app.commandLine.appendSwitch('disable-gpu')
const ROOT = join(__dirname, '..')
const OUT = join(ROOT, 'preview-shots')
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
const MARKDOWN_SAMPLE = [
'## 1. "这是演示数据,仅用于验证链路"',
'',
'我配置的 `hotword_lookup` 工具目前连的是**演示实现(demo)**,[项目主页](https://example.com)。',
'',
'| 说明 | 结论 |',
'| :--- | ---: |',
'| **工具可用** | 工具这个"壳"是好的 —— 链路没有问题 |',
'| **数据源是空的** | 它背后的"仓库"是空的,查询返回"无匹配项" |',
'',
'- 列表项一,包含 `inline code`',
'- 列表项二,**加粗** 与 ~~删除线~~',
'',
'```python',
"print('Hello llm-ui!')",
'for i in range(3):',
' print(i)',
'```',
'',
'```rust',
'fn main() {',
' println!("Hello, World!");',
'}',
'```'
].join('\n')
const FAKE_STATE = {
sessionId: 's1',
sessionName: 'hi',
streaming: false,
activeTools: [],
messageCount: 2,
workspace: 'C:\\fake\\workspace'
}
const FAKE_SESSIONS = [
{ id: 's1', path: 'C:\\fake\\s1.jsonl', name: 'hi', firstMessage: '帮我写一个hello world', created: '2026-01-01', modified: '2026-01-02', messageCount: 2 },
{ id: 's2', path: 'C:\\fake\\s2.jsonl', name: 'hello world', firstMessage: '帮我写一个hello world', created: '2026-01-01', modified: '2026-01-02', messageCount: 1 }
]
function registerFakeIpc() {
ipcMain.handle('agent:state', () => FAKE_STATE)
ipcMain.handle('sessions:list', () => FAKE_SESSIONS)
ipcMain.handle('workspaces:list', () => [{ name: 'Default', path: 'C:\\fake\\workspace' }])
ipcMain.handle('models:catalog', () => ({ providers: [] }))
ipcMain.handle('settings:get', () => ({ systemPrompt: '' }))
ipcMain.handle('app:versions', () => ({ app: 'dev', electron: 'x', node: 'x', chrome: 'x', user: 'x' }))
ipcMain.handle('agent:send', () => undefined)
ipcMain.handle('chat:resources', () => ({
commands: [{ name: 'daily-report', description: '生成每日经营日报', kind: 'prompt' }],
skills: [{ name: 'hotword-analysis', description: '分析近 7 天类目热点词' }]
}))
ipcMain.handle('files:list', () => [
{ path: 'data/products.csv', name: 'products.csv', isDir: false },
{ path: 'docs/sop.md', name: 'sop.md', isDir: false },
{ path: 'hello_world.py', name: 'hello_world.py', isDir: false },
{ path: 'assets', name: 'assets', isDir: true }
])
}
async function capture(win, name) {
// 离屏窗口 capturePage 常给旧帧:连拍多张,取最后一张(大概率已提交新帧)
let image = null
for (let k = 0; k < 4; k += 1) {
win.webContents.invalidate()
await sleep(320)
image = await win.webContents.capturePage()
}
writeFileSync(join(OUT, name), image.toPNG())
console.log('saved', name, `${image.getSize().width}x${image.getSize().height}`)
}
async function run() {
mkdirSync(OUT, { recursive: true })
registerFakeIpc()
const preload = join(ROOT, 'out', 'preload', 'index.cjs')
const win = new BrowserWindow({
width: 1280,
height: 900,
show: false,
webPreferences: {
paintWhenInitiallyHidden: true,
backgroundThrottling: false,
preload: existsSync(preload) ? preload : undefined,
contextIsolation: true
}
})
await win.loadFile(join(ROOT, 'out', 'renderer', 'index.html'))
// 清掉历史 localStorage(收起态持久化),再重载让侧栏以展开态启动
await win.webContents.executeJavaScript('try { localStorage.clear() } catch {}')
await win.reload()
await sleep(1200)
// 注入一条含 markdown 的会话(user + assistant)
win.webContents.send('agent:event', {
type: 'session_switched',
state: FAKE_STATE,
messages: [
{ role: 'user', content: '解释一下那两句话的意思' },
{ role: 'assistant', content: [{ type: 'text', text: MARKDOWN_SAMPLE }] }
],
activeTools: {}
})
await sleep(1200)
console.log(
'hl-diag:',
await win.webContents.executeJavaScript(
`(() => { const codes = [...document.querySelectorAll('code')]; return JSON.stringify({ ready: window.__shikiReady, codes: codes.map((c) => c.querySelectorAll('span[style]').length) }) })()`
)
)
await capture(win, 'app-markdown.png')
// 注入排队消息场景(streaming + 队列两条)
win.webContents.send('agent:event', { type: 'agent_start' })
win.webContents.send('agent:event', {
type: 'queue_update',
steering: ['设置里面做成这样,分成基础设置、Agent 能力'],
followUp: ['随后帮我补上skills、命令和附件的支持,chatbox里面/调起命令或skill,@则是引用文件,加号是附件']
})
await sleep(900)
win.webContents.invalidate()
await sleep(300)
FAKE_STATE.permissionMode = 'confirm'
await capture(win, 'app-queue.png')
// 权限模式菜单
await win.webContents.executeJavaScript(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`)
win.webContents.send('agent:event', { type: 'agent_end' })
win.webContents.send('agent:event', { type: 'queue_update', steering: [], followUp: [] })
await sleep(500)
await win.webContents.executeJavaScript(
`[...document.querySelectorAll('button')].find((b) => b.title.startsWith('权限模式'))?.click()`
)
await sleep(500)
win.webContents.invalidate()
await sleep(300)
console.log(
'perm-diag:',
await win.webContents.executeJavaScript(
`(() => { const panels = [...document.querySelectorAll('div.fixed.z-40')]; const m = panels.find((d) => d.textContent.includes('减少确认次数')); if (!m) return 'closed'; const r = m.getBoundingClientRect(); const cs = getComputedStyle(m); return JSON.stringify({ x: r.x, y: r.y, w: r.width, h: r.height, opacity: cs.opacity, display: cs.display, zIndex: cs.zIndex, buttons: m.querySelectorAll('button').length }) })()`
)
)
await capture(win, 'app-permissions.png')
// 结束流式,恢复常规状态
win.webContents.send('agent:event', { type: 'agent_end' })
win.webContents.send('agent:event', { type: 'queue_update', steering: [], followUp: [] })
await sleep(400)
// "/" 命令/技能面板
const typeInPrompt = (text) =>
win.webContents.executeJavaScript(
`(() => { const ta = document.querySelector('textarea[aria-label="Prompt"]'); if (!ta) return 'no-ta'; const set = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, 'value').set; set.call(ta, ${JSON.stringify(text)}); ta.dispatchEvent(new Event('input', { bubbles: true })); return 'ok' })()`
)
await typeInPrompt('/')
await sleep(600)
win.webContents.invalidate()
await sleep(300)
await capture(win, 'app-slash-menu.png')
// "@ " 文件引用
await typeInPrompt('帮我看看 @')
await sleep(600)
win.webContents.invalidate()
await sleep(300)
await capture(win, 'app-at-menu.png')
await typeInPrompt('')
// 注入审批卡
win.webContents.send('agent:event', {
type: 'approval_request',
request: {
id: 'approval-1',
toolName: 'write',
args: { path: 'hello_world.py', content: 'def main():\n print("Hello world")\n\nmain()' }
}
})
await sleep(800)
win.webContents.invalidate()
await sleep(400)
await capture(win, 'app-approval.png')
// 行内操作浮层:悬停操作按钮点击后弹出(重命名 / 删除)
await win.webContents.executeJavaScript(
`[...document.querySelectorAll('button')].find((b) => b.title === '重命名会话')?.click()`
)
await sleep(500)
win.webContents.invalidate()
await sleep(300)
await capture(win, 'app-row-rename.png')
await win.webContents.executeJavaScript(
`[...document.querySelectorAll('button')].find((b) => b.title === '删除会话')?.click()`
)
await sleep(500)
win.webContents.invalidate()
await sleep(300)
await capture(win, 'app-row-delete.png')
await win.webContents.executeJavaScript(`document.body.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))`)
// 设置页:Ctrl+, 打开,截「模型设置」与「常规」
await win.webContents.executeJavaScript(
`window.dispatchEvent(new KeyboardEvent('keydown', { key: ',', ctrlKey: true, bubbles: true }))`
)
await sleep(600)
win.webContents.invalidate()
await sleep(300)
await capture(win, 'app-settings-model.png')
await win.webContents.executeJavaScript(
`[...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '常规')?.click()`
)
await sleep(400)
win.webContents.invalidate()
await sleep(300)
await capture(win, 'app-settings-general.png')
await win.webContents.executeJavaScript(`window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`)
console.log(
'diag:',
await win.webContents.executeJavaScript(
`(() => { const btn = document.querySelector('button[aria-label="拒绝并关闭"]'); if (!btn) return 'missing'; const card = btn.closest('div[class*="rounded-card"]') ?? btn.parentElement; const r = card.getBoundingClientRect(); return JSON.stringify({ x: r.x, y: r.y, w: r.width, h: r.height, winH: innerHeight, opacity: getComputedStyle(card).opacity }) })()`
)
)
// 侧栏收起回归检查
const collapsed = await win.webContents.executeJavaScript(
`(() => { const b = document.querySelector('[aria-label="折叠侧栏"]'); if (!b) return 'no-button'; b.click(); return 'clicked' })()`
)
console.log('collapse:', collapsed)
await sleep(800)
const geo = await win.webContents.executeJavaScript(
`(() => {
const aside = document.querySelector('aside[aria-label="工作区导航"]')
const row = document.querySelector('.sidebar-row')
const icon = row ? row.querySelector('span') : null
if (!aside || !row || !icon) return 'missing'
const a = aside.getBoundingClientRect()
const i = icon.getBoundingClientRect()
return JSON.stringify({ rail: Math.round(a.width), iconCenter: Math.round(i.left - a.left + i.width / 2) })
})()`
)
console.log('collapsed-geo:', geo)
await capture(win, 'app-collapsed.png')
app.exit(0)
}
app.whenReady().then(run).catch((err) => {
console.error(err)
app.exit(1)
})