chore: scripts 与 preview 调试工具移出版本跟踪(.gitignore 忽略)

This commit is contained in:
Rick Ye
2026-09-06 10:01:13 +08:00
parent a5951849c8
commit 5799be543b
50 changed files with 2 additions and 4576 deletions
-12
View File
@@ -1,12 +0,0 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>beautifului primitives preview</title>
</head>
<body class="bg-zinc-50 text-zinc-900">
<div id="root"></div>
<script type="module" src="./main.tsx"></script>
</body>
</html>
-210
View File
@@ -1,210 +0,0 @@
// beautifului.dev 组件视觉验收页(静态构建,模拟聊天流内的真实用法)
import { useRef, useState } from 'react'
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './preview.css'
import { Thinking } from '../src/renderer/src/components/Thinking'
import { StreamingText } from '../src/renderer/src/components/StreamingText'
import { ApprovalStack } from '../src/renderer/src/components/ApprovalCard'
import { ToolChips } from '../src/renderer/src/components/ToolChips'
import { SidebarNav } from '../src/renderer/src/components/SidebarNav'
import { Search } from '../src/renderer/src/components/Search'
import { CodeBlock } from '../src/renderer/src/components/CodeBlock'
import { SelectionActions } from '../src/renderer/src/components/SelectionActions'
import { Loading } from '../src/renderer/src/components/Loading'
import { Markdown } from '../src/renderer/src/components/Markdown'
import type { ApprovalRequest, SessionMeta } from '../src/shared/types'
const thinkingRows = [
{ primary: '用户想了解今天的品类热点,我先查一下最近的趋势数据。' },
{ primary: '需要调用 query_trend 工具获取近 7 天的热点词,再按类目分组。' },
{ primary: '结果里先给 TOP3 品类,附上建议的标题关键词。' }
]
const toolCalls = [
{
id: 'call-1',
name: 'query_trend',
tool: {
toolName: 'query_trend',
args: { category: 'home', days: 7 },
status: 'done' as const,
result: { content: [{ type: 'text', text: '{ "top": ["收纳", "香薰", "桌布"], "lift": [0.32, 0.21, 0.18] }' }] }
}
},
{
id: 'call-2',
name: 'draft_title',
tool: {
toolName: 'draft_title',
args: { keywords: ['收纳', '灵感'] },
status: 'error' as const,
result: { content: [{ type: 'text', text: 'Error: rate limited, retry after 30s' }] }
}
},
{
id: 'call-3',
name: 'update_price',
pendingApproval: { args: { sku: 'SKU-1024', from: 39.9, to: 35.9 } }
}
]
const approvalRequest: ApprovalRequest = {
id: 'approval-1',
toolName: 'update_price',
args: { sku: 'SKU-1024', from: 39.9, to: 35.9 }
}
const sessions: SessionMeta[] = [
{ id: 's1', path: 'a.json', name: '供应商记录整理', firstMessage: '帮我整理供应商', created: '', modified: new Date().toISOString(), messageCount: 12 },
{ id: 's2', path: 'b.json', name: '', firstMessage: '根据热点词起草一个商品标题方案', created: '', modified: new Date(Date.now() - 86400000).toISOString(), messageCount: 5 },
{ id: 's3', path: 'c.json', name: '', firstMessage: '查询最近一次上架任务的执行结果', created: '', modified: new Date(Date.now() - 3 * 86400000).toISOString(), messageCount: 8 }
]
const CODE_SNIPPET = `export async function churnBatch() {
const flavor = await getFlavor("pistachio");
const base = await dairy.fetch({ flavor });
await freezer.store(base, { temp: "-16C" });
if (!base.approved) return null;
return base.gallons;
}`
const DIFF_SNIPPET = `@@ -1,4 +1,5 @@
export const hero = "mint-chip";
-const windows = slots;
+const windows = slots.filter(
+ (s) => s.temp <= -12,
+);
schedule(windows);`
const PARA =
'百香果风味在整个周末保持热销榜首。周六一早就要开始搅拌,让批次在下午高峰前有足够时间固化。同时需要预留两个晚间冷冻窗口,避免与常规生产冲突。'
function PreviewCard({ title, children }: { title: string; children: React.ReactNode }) {
return (
<section className="mb-8 rounded-2xl border border-zinc-200 bg-white p-5">
<h2 className="mb-3 text-[12px] font-medium tracking-wide text-zinc-400 uppercase">{title}</h2>
{children}
</section>
)
}
function App() {
const [searchOpen, setSearchOpen] = useState(false)
const [applied, setApplied] = useState('')
const selectionBoxRef = useRef<HTMLDivElement>(null)
// 调试分区:URL hash 指定只渲染某一节(#thinking / #streaming / #tools /
// #approval / #code / #selection / #loading / #sidebar / #none),空 hash 全渲染
const only = window.location.hash.slice(1)
const show = (id: string): boolean => only === '' || only === id
return (
<div className="mx-auto max-w-5xl px-6 py-8">
<h1 className="mb-6 text-[14px] font-semibold text-zinc-900">beautifului.dev · 8 组件直连验收(light)</h1>
<div className="grid grid-cols-[minmax(0,1fr)_240px] items-start gap-6">
<div className="min-w-0">
{show('thinking') && (
<PreviewCard title="Thinking · 流式中 / 已完成">
<Thinking rows={thinkingRows} streaming />
<div className="mt-4">
<Thinking rows={thinkingRows} streaming={false} />
</div>
</PreviewCard>
)}
{show('streaming') && (
<PreviewCard title="Streaming Text · 流式正文 + Follow-ups">
<StreamingText streaming={false} text="已完成的回答文本。" followUps={['冬季哪些口味卖得最好', '对比凝胶和软冰的毛利']} onFollowUp={(q) => setApplied(q)}>
<Markdown text={'夏季核果风味需求激增——**桃子和杏子**领先。建议先盘点蛋筒库存,再推广华夫碗套餐。'} />
</StreamingText>
</PreviewCard>
)}
{show('tools') && (
<PreviewCard title="Tool Chips · 真实工具调用(含待审批行)">
<div className="w-fit max-w-full rounded-card bg-inset p-3">
<ToolChips calls={toolCalls} thinking={['先查近 7 天热点词,再按类目分组给出 TOP3。']} />
</div>
</PreviewCard>
)}
{show('approval') && (
<PreviewCard title="Approval Card · 单题向导(允许/拒绝 + 倒计时)">
<ApprovalStack requests={[approvalRequest]} onResolve={(_id, approved) => setApplied(approved ? '已允许' : '已拒绝')} />
<div className="h-2" />
</PreviewCard>
)}
{show('code') && (
<PreviewCard title="Code Block · 行号代码 / unified diff">
<CodeBlock code={CODE_SNIPPET} lang="ts" filename="churn.ts" />
<div className="mt-4">
<CodeBlock code={DIFF_SNIPPET} lang="diff" />
</div>
</PreviewCard>
)}
{show('selection') && (
<PreviewCard title="Selection Actions · 划选文本试试">
<div ref={selectionBoxRef} className="relative pb-10">
<p className="text-[13px] leading-relaxed text-ink">{PARA}</p>
<SelectionActions containerRef={selectionBoxRef} onApply={(instruction, selection) => setApplied(`${instruction}:${selection.slice(0, 18)}…`)} />
</div>
{applied && <p className="mt-2 font-mono text-[11px] text-zinc-500">onApply → {applied}</p>}
</PreviewCard>
)}
{show('loading') && (
<PreviewCard title="Loading(附带)">
<Loading label="思考中" variant="Dots" />
</PreviewCard>
)}
</div>
{show('sidebar') && (
<div className="sticky top-8">
<PreviewCard title="Sidebar Nav · 可折叠">
<div className="h-[560px] overflow-hidden rounded-xl border border-zinc-200">
<SidebarNav
sessionState={{ sessionId: 's1', streaming: false, activeTools: [], messageCount: 12 }}
sessions={sessions}
workspaces={[{ name: 'Default', path: 'C:/workspaces/Default' }]}
activeWorkspace={{ name: 'Default', path: 'C:/workspaces/Default' }}
onCreateWorkspace={() => undefined}
onOpenWorkspace={() => undefined}
onNewSession={() => setApplied('新会话')}
onOpenSession={() => setApplied('打开会话')}
onRenameSession={() => setApplied('重命名')}
onDeleteSession={() => setApplied('删除')}
onRefreshSessions={() => setApplied('刷新')}
onOpenSearch={() => setSearchOpen(true)}
onOpenSettings={() => setApplied('设置')}
/>
</div>
</PreviewCard>
</div>
)}
</div>
{show('sidebar') && (
<Search
open={searchOpen}
onClose={() => setSearchOpen(false)}
sessions={sessions}
onNewSession={() => setApplied('新会话')}
onRefreshSessions={() => setApplied('刷新')}
onOpenSettings={() => setApplied('设置')}
onOpenSession={() => setApplied('打开会话')}
/>
)}
</div>
)
}
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>
)
-6
View File
@@ -1,6 +0,0 @@
/* 预览页专用入口:复用应用全套样式(tailwind + beautifului foundation + 字体),
* 并显式声明组件源码目录参与工具类扫描(vite root 在 preview/ 下,
* 自动检测覆盖不到 ../src)。 */
@import "../src/renderer/src/styles/globals.css";
@source "../src/renderer/src";
-263
View File
@@ -1,263 +0,0 @@
// 真实 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)
})
-95
View File
@@ -1,95 +0,0 @@
// 预览页视觉验收截图(Electron 离屏捕获):
// npx electron scripts/capture-preview.cjs
// 产出 preview-shots/*.png:整页 / 侧栏折叠 / 搜索面板 / 选区工具条
const { app, BrowserWindow } = require('electron')
const { mkdirSync, writeFileSync } = require('node:fs')
const { join } = require('node:path')
// 离屏捕获在 GPU 合成异常时会得到空白帧,改用软件合成
app.commandLine.appendSwitch('disable-gpu')
const URL = 'http://localhost:4173/'
const OUT = join(__dirname, '..', 'preview-shots')
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
async function capture(win, name) {
try {
const image = await win.webContents.capturePage()
writeFileSync(join(OUT, name), image.toPNG())
console.log('saved', name, `${image.getSize().width}x${image.getSize().height}`)
} catch (e) {
console.log('capture failed', name, e.message)
}
}
async function js(win, code) {
try {
return await win.webContents.executeJavaScript(code)
} catch (e) {
console.log('js failed:', e.message)
return undefined
}
}
async function run() {
mkdirSync(OUT, { recursive: true })
const win = new BrowserWindow({
width: 1280,
height: 960,
show: false,
webPreferences: { paintWhenInitiallyHidden: true }
})
await win.loadURL(URL)
await sleep(1500)
// 1) 整页(上半)
await capture(win, '01-full.png')
// 2) 折叠侧栏(等宽度过渡)
await js(win, `document.querySelector('[aria-label="折叠侧栏"]')?.click()`)
await sleep(700)
await capture(win, '02-sidebar-collapsed.png')
await js(win, `document.querySelector('[aria-label="展开侧栏"]')?.click()`)
await sleep(700)
// 3) 搜索面板(侧栏「搜索」行)+ 输入过滤
await js(win, `[...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '搜索')?.click()`)
await sleep(600)
await js(win, `(() => { const i = document.querySelector('input[aria-label="搜索会话或命令"]'); if (!i) return 'no-input'; const set = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; set.call(i, '供应'); i.dispatchEvent(new Event('input', { bubbles: true })); return 'ok' })()`)
await sleep(400)
await capture(win, '03-search-filtered.png')
await js(win, `document.querySelector('input[aria-label="搜索会话或命令"]')?.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }))`)
await sleep(400)
// 4) 划选文本 → Selection Actions 工具条(展开「更多」)
await js(win, `(() => {
const p = [...document.querySelectorAll('p')].find((el) => el.textContent.includes('百香果'));
if (!p) return 'no-p';
const textNode = p.firstChild;
const range = document.createRange();
range.setStart(textNode, 0);
range.setEnd(textNode, 30);
const sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
document.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }));
return 'ok';
})()`)
await sleep(600)
await js(win, `document.querySelector('[aria-label="更多操作"]')?.click()`)
await sleep(500)
await capture(win, '04-selection-actions.png')
// 5) 审批卡交互:选择「允许执行」(radio 自动前进 480ms 后提交)
await js(win, `[...document.querySelectorAll('button')].find((b) => b.textContent.trim() === '允许执行')?.click()`)
await sleep(900)
await capture(win, '05-approval-approved.png')
app.exit(0)
}
app.whenReady().then(run).catch((err) => {
console.error(err)
app.exit(1)
})
-21
View File
@@ -1,21 +0,0 @@
// 验证侧栏折叠交互
const { app, BrowserWindow } = require('electron')
app.commandLine.appendSwitch('disable-gpu')
app.commandLine.appendSwitch('disable-software-rasterizer')
app.whenReady().then(async () => {
const win = new BrowserWindow({ width: 1280, height: 900, show: false, webPreferences: { offscreen: true } })
await win.loadURL('http://127.0.0.1:4173/')
await new Promise((r) => setTimeout(r, 1200))
const found = await win.webContents.executeJavaScript(`document.querySelector('[aria-label="折叠侧栏"]') ? 'found' : 'NOT-FOUND'`)
console.log('collapse btn:', found)
await win.webContents.executeJavaScript(`document.querySelector('[aria-label="折叠侧栏"]').click()`)
await new Promise((r) => setTimeout(r, 600))
console.log(
'after click: collapsed=',
await win.webContents.executeJavaScript(`document.querySelector('aside')?.getAttribute('data-sidebar-collapsed')`),
'width=',
await win.webContents.executeJavaScript(`document.querySelector('aside')?.style.width`)
)
app.exit(0)
})
-41
View File
@@ -1,41 +0,0 @@
// 预览页控制台诊断:node 调试某一分区
// npx electron scripts/debug-preview.cjs "#thinking"
const { app, BrowserWindow } = require('electron')
const hash = process.argv[2] ?? ''
const URL = `http://127.0.0.1:4173/${hash}`
// 兜底退出(渲染进程卡死时 executeJavaScript 永不返回)
const guard = setTimeout(() => {
console.log('[guard] hard exit (page busy or crashed)')
app.exit(2)
}, 12000)
app.whenReady().then(async () => {
const win = new BrowserWindow({
width: 1280,
height: 900,
show: false,
webPreferences: { offscreen: true }
})
win.webContents.on('console-message', (_e, level, message, line, source) => {
console.log(`[console:${level}]`, message, `(${source}:${line})`)
})
win.webContents.on('did-fail-load', (_e, code, desc, url) => {
console.log(`[did-fail-load] ${code} ${desc} ${url}`)
})
win.webContents.on('render-process-gone', (_e, details) => {
console.log('[crashed]', details.reason)
})
await win.loadURL(URL)
setTimeout(async () => {
try {
const text = await win.webContents.executeJavaScript('document.body.innerText.slice(0, 120)')
console.log('[body]', JSON.stringify(text))
} catch (e) {
console.log('[executeJavaScript failed]', e.message)
}
clearTimeout(guard)
app.exit(0)
}, 2500)
})
-17
View File
@@ -1,17 +0,0 @@
// 对比 out/renderer 旧 bundle 与当前源码的 reducer 逻辑
const fs = require('fs')
const t = fs.readFileSync('apps/desktop/out/renderer/assets/index-DJXyfP9T.js', 'utf8')
console.log('has message_end:', t.includes('message_end'))
console.log('has optimistic-user:', t.includes('optimistic-user'))
console.log('has session_switched:', t.includes('session_switched'))
console.log('has agent_settled:', t.includes('agent_settled'))
console.log('has approval_request:', t.includes('approval_request'))
// reducer 里 message_end 附近的代码
let i = t.indexOf('"message_end"')
if (i < 0) i = t.indexOf('message_end')
if (i >= 0) {
console.log('\n=== around first message_end ===')
console.log(t.slice(i - 500, i + 900))
}
-87
View File
@@ -1,87 +0,0 @@
// 生成应用图标(build/icon.ico + build/icon.png):
// npx electron scripts/make-icons.cjs
// 用 Electron 离屏渲染 build/icon.svg(居中于方形画布、留 9% 边距),
// 输出多尺寸 PNG 并打包为 PNG 内嵌式 ICO(Vista+ 支持)。
const { app, BrowserWindow } = require('electron')
const { readFileSync, writeFileSync } = require('node:fs')
const { join } = require('node:path')
app.commandLine.appendSwitch('disable-gpu')
const SIZES = [16, 24, 32, 48, 64, 128, 256]
const ROOT = join(__dirname, '..')
const sleep = (ms) => new Promise((r) => setTimeout(r, ms))
/** PNG 内嵌式 ICO 打包 */
function buildIco(pngs) {
const count = pngs.length
const header = Buffer.alloc(6)
header.writeUInt16LE(0, 0) // reserved
header.writeUInt16LE(1, 2) // type: icon
header.writeUInt16LE(count, 4)
const entries = Buffer.alloc(16 * count)
const blobs = []
let offset = 6 + 16 * count
pngs.forEach((png, i) => {
const size = png.size
entries.writeUInt8(size >= 256 ? 0 : size, i * 16 + 0) // width
entries.writeUInt8(size >= 256 ? 0 : size, i * 16 + 1) // height
entries.writeUInt8(0, i * 16 + 2) // palette
entries.writeUInt8(0, i * 16 + 3) // reserved
entries.writeUInt16LE(1, i * 16 + 4) // planes
entries.writeUInt16LE(32, i * 16 + 6) // bpp
entries.writeUInt32LE(png.data.length, i * 16 + 8)
entries.writeUInt32LE(offset, i * 16 + 12)
offset += png.data.length
blobs.push(png.data)
})
return Buffer.concat([header, entries, ...blobs])
}
async function renderSize(win, svgDataUrl, size) {
await win.loadURL(
'data:text/html,' +
encodeURIComponent(
`<!doctype html><html><body style="margin:0;background:transparent">` +
`<div style="display:flex;align-items:center;justify-content:center;width:${size}px;height:${size}px">` +
`<img src="${svgDataUrl}" style="width:${Math.round(size * 0.86)}px;height:auto;display:block"/>` +
`</div></body></html>`
)
)
await sleep(350)
const image = await win.webContents.capturePage({ x: 0, y: 0, width: size, height: size })
return image.toPNG()
}
async function run() {
const svg = readFileSync(join(ROOT, 'build', 'icon.svg'), 'utf8')
const svgDataUrl = 'data:image/svg+xml;base64,' + Buffer.from(svg, 'utf8').toString('base64')
const win = new BrowserWindow({
width: 256,
height: 256,
show: false,
frame: false,
transparent: true,
webPreferences: { paintWhenInitiallyHidden: true, offscreen: true }
})
const pngs = []
for (const size of SIZES) {
await win.setSize ? win.setSize(size, size) : null
const png = await renderSize(win, svgDataUrl, size)
pngs.push({ size, data: png })
console.log('rendered', size, `${png.length}B`)
}
writeFileSync(join(ROOT, 'build', 'icon.png'), pngs.find((p) => p.size === 256).data)
writeFileSync(join(ROOT, 'build', 'icon.ico'), buildIco(pngs))
console.log('written build/icon.ico & build/icon.png')
app.exit(0)
}
app.whenReady().then(run).catch((err) => {
console.error(err)
app.exit(1)
})
-15
View File
@@ -1,15 +0,0 @@
// 打印 pi SDK 关键类型定义(skills / slash-commands / prompt 参数)
const fs = require('node:fs')
const files = [
'dist/core/skills.d.ts',
'dist/core/slash-commands.d.ts',
'dist/modes/interactive/components/skill-invocation-message.d.ts'
]
for (const f of files) {
console.log('=====', f)
try {
console.log(fs.readFileSync(f, 'utf8'))
} catch (e) {
console.log('READ FAIL', e.message)
}
}
-22
View File
@@ -1,22 +0,0 @@
// 打印 ImageContent / PromptTemplate / AgentSessionServices 类型
const fs = require('node:fs')
const root = 'C:/Users/21976/Desktop/Develop/agent-store-operations/apps/desktop/node_modules'
const files = [
[root + '/@earendil-works/pi-ai/dist/compat.d.ts', /export (interface|type) (ImageContent|TextContent)[\s\S]*?\n\}/],
[root + '/@earendil-works/pi-coding-agent/dist/core/prompt-templates.d.ts', null],
[root + '/@earendil-works/pi-coding-agent/dist/core/sdk.d.ts', /export interface AgentSessionServices[\s\S]*?\n\}/]
]
for (const [f, re] of files) {
console.log('=====', f.split('/').slice(-2).join('/'))
try {
const s = fs.readFileSync(f, 'utf8')
if (re) {
const m = s.match(re)
console.log(m ? m[0] : '(no match)')
} else {
console.log(s.slice(0, 2500))
}
} catch (e) {
console.log('READ FAIL', e.message)
}
}
@@ -1,21 +0,0 @@
// 验证 pi SDK 对空目录(无会话的工作区)的行为:
// node scripts/repro-empty-workspace.mjs "C:\path\to\empty\dir"
import { SessionManager, getAgentDir } from '@earendil-works/pi-coding-agent'
import { mkdirSync, mkdtempSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
const target = process.argv[2] ?? mkdtempSync(join(tmpdir(), 'ws-empty-'))
mkdirSync(target, { recursive: true })
console.log('target cwd:', target)
try {
const sm = SessionManager.continueRecent(target)
console.log('continueRecent OK, sessionFile:', sm.sessionFile ?? '(none)')
const list = await SessionManager.list(target)
console.log('list count:', list.length)
} catch (err) {
console.error('continueRecent FAILED:', err instanceof Error ? err.message : err)
process.exit(1)
}
console.log('agentDir:', getAgentDir())
-103
View File
@@ -1,103 +0,0 @@
// 复现渲染层"输出一闪而过":用与 agent-service.ts 相同的 SDK 调用链,
// 订阅全部事件并打印事件序列与消息对象结构。
import os from 'node:os'
import {
createAgentSessionRuntime,
createAgentSessionServices,
createAgentSessionFromServices,
getAgentDir,
SessionManager
} from '@earendil-works/pi-coding-agent'
const cwd = os.homedir()
const agentDir = getAgentDir()
const services = await createAgentSessionServices({ cwd, agentDir })
const factory = async (opts) => {
const result = await createAgentSessionFromServices({
services,
sessionManager: opts.sessionManager,
sessionStartEvent: opts.sessionStartEvent
})
return { ...result, services, diagnostics: services.diagnostics }
}
const sessionManager = SessionManager.continueRecent(cwd)
const runtime = await createAgentSessionRuntime(factory, { cwd, agentDir, sessionManager })
const session = runtime.session
let rebindCount = 0
runtime.setRebindSession(async (s) => {
rebindCount++
console.log(`\n>>> REBIND fired (#${rebindCount}) — 渲染层会收到 session_switched,messages.length=${s.messages.length}`)
})
session.subscribe((event) => {
switch (event.type) {
case 'message_start':
console.log('EVT message_start role=', JSON.stringify(event.message?.role))
break
case 'message_update': {
const inner = event.assistantMessageEvent
if (inner) {
if (inner.type.endsWith('_delta')) {
process.stdout.write(`.`)
} else {
console.log(`EVT message_update ${inner.type}`)
}
}
break
}
case 'message_end': {
const m = event.message
console.log('\nEVT message_end role=', m?.role)
console.log(' message keys:', Object.keys(m ?? {}))
try {
JSON.stringify(m)
console.log(' JSON.stringify: OK, length=', JSON.stringify(m).length)
} catch (err) {
console.log(' JSON.stringify FAILED ->', err.message, '(toCloneable 会变成 null → 渲染层消息变空!)')
}
if (m?.role === 'assistant') {
console.log(' stopReason=', m.stopReason, ' errorMessage=', m.errorMessage)
console.log(' content types=', (m.content ?? []).map((b) => b.type).join(','))
for (const b of m.content ?? []) {
if (b.type === 'text') console.log(' text block:', JSON.stringify(b.text).slice(0, 200))
}
}
break
}
case 'agent_end':
console.log(`\nEVT agent_end willRetry=${event.willRetry} messages=${event.messages?.length}`)
break
default:
console.log('EVT', event.type)
}
})
console.log('--- session:', session.sessionId, 'messages:', session.messages.length)
console.log('--- model:', session.model?.provider + '/' + session.model?.id)
console.log('--- sending "hi" ...')
try {
await session.prompt('hi')
console.log('\n--- prompt resolved')
} catch (err) {
console.log('\n--- prompt THREW:', err?.message)
}
// 等待 agent_settled 之后的异步收尾
await new Promise((r) => setTimeout(r, 1500))
console.log('\n=== final session.messages ===')
for (const [i, m] of session.messages.entries()) {
const blocks = Array.isArray(m.content)
? m.content.map((b) => b.type).join(',')
: typeof m.content === 'string'
? 'str:' + m.content.slice(0, 30)
: '??'
console.log(`#${i} ${m.role} [${blocks}]`)
}
console.log('\nrebindCount=', rebindCount)
process.exit(0)
-41
View File
@@ -1,41 +0,0 @@
// 验证 Markdown.tsx 的 parseBlocks 是否产出空内容
const INLINE = /^(\s*)([-*+]\s+|\d+[.)]\s+|>\s?|(#{1,3}\s+))?(.*)$/
function parseBlocks(text) {
const blocks = []
const lines = text.split('\n')
let current = null
const flush = () => {
if (current) blocks.push(current)
current = null
}
for (const line of lines) {
const m = line.match(INLINE)
if (!m) continue
const [, indent, marker, content] = m
const isListItem = Boolean(marker && /^([-*+]\s+|\d+[.)]\s+)/.test(marker))
const kind = marker
? marker.startsWith('#')
? 'h1'
: isListItem
? /^\d/.test(marker) ? 'ol' : 'ul'
: marker.startsWith('>') ? 'quote' : null
: 'p'
if (kind === 'p') {
if (!current || current.kind !== 'p') {
flush()
current = { kind: 'p', lines: [] }
}
current.lines.push(content)
continue
}
flush()
}
return blocks
}
for (const t of ['pong', 'Hello world', '# 标题', '- 列表项', '> 引用', '1. 有序']) {
const blocks = parseBlocks(t)
const rendered = blocks.map((b) => b.lines.join('\n'))
console.log(JSON.stringify(t), '->', JSON.stringify(rendered))
}
@@ -1,81 +0,0 @@
// 验证渲染层对 session_switched 历史消息的渲染逻辑:
// 用与主进程一致的 session.messages 源,跑 MessageItem 的 readBlocks + AssistantBody 判断。
import os from 'node:os'
import { createAgentSessionServices, getAgentDir } from '@earendil-works/pi-coding-agent'
const cwd = os.homedir()
const services = await createAgentSessionServices({ cwd, agentDir: getAgentDir() })
// SessionManager.list + 加载最近会话的等价做法:直接用 services 里加载当前会话
const session = services.sessionManager
? null
: null
// 走 SessionManager 拿最近会话并加载
const { SessionManager } = await import('@earendil-works/pi-coding-agent')
const sm = SessionManager.continueRecent(cwd)
const smAny = sm
// SessionManager.continueRecent 返回的 manager 需要挂到 session 上才有 messages;
// 更直接:读它指向的会话文件并解析 message 条目
const entries = []
if (typeof smAny.loadEntries === 'function') {
entries.push(...(await smAny.loadEntries()))
}
console.log('loadEntries available:', typeof smAny.loadEntries === 'function', 'entries:', entries.length)
// 退路:直接解析磁盘上的最近 jsonl(与主进程 toCloneable 后的消息同构)
import { readdirSync, readFileSync, statSync } from 'node:fs'
import { join } from 'node:path'
const dir = join(cwd, '.pi', 'agent', 'sessions', '--C--Users-21976--')
const files = readdirSync(dir).filter((f) => f.endsWith('.jsonl'))
files.sort((a, b) => statSync(join(dir, b)).mtimeMs - statSync(join(dir, a)).mtimeMs)
const latest = files[0]
console.log('latest session file:', latest)
const lines = readFileSync(join(dir, latest), 'utf8').trim().split('\n')
const messages = []
for (const line of lines) {
let entry
try {
entry = JSON.parse(line)
} catch {
continue
}
if (entry.type === 'message' && entry.message) messages.push(entry.message)
}
// ==== 渲染层逻辑复刻(与 MessageItem.tsx 一致)====
function readBlocks(raw) {
if (!raw || typeof raw !== 'object') return []
const content = raw.content
if (typeof content === 'string') return [{ type: 'text', text: content }]
if (Array.isArray(content)) {
return content.filter((c) => typeof c === 'object' && c !== null)
}
return []
}
for (const [i, m] of messages.entries()) {
console.log(`\n=== history msg #${i} role=${m.role} ===`)
const blocks = readBlocks(m)
let rendered = 0
for (const block of blocks) {
if (block.type === 'thinking' && typeof block.text === 'string') {
console.log(' -> ThinkingBlock (text field)')
rendered++
continue
}
if (block.type === 'text' && block.text) {
console.log(` -> Markdown("${String(block.text).slice(0, 60)}…")`)
rendered++
continue
}
if (block.type === 'toolCall' && block.id) {
console.log(' -> ToolCard', block.name)
rendered++
continue
}
console.log(' -> **被丢弃** block type =', block.type, 'keys =', Object.keys(block).join(','))
}
console.log(` rendered ${rendered}/${blocks.length} blocks`)
}
-65
View File
@@ -1,65 +0,0 @@
/**
* v0.3.0 冒烟:模型目录 / API key login-logout / setModel / 系统提示词
* 不打真实 LLM 请求;key 用假值(仅验证凭据写入与模型切换逻辑)。
*/
import { createAgentSessionServices, createAgentSessionRuntime, createAgentSessionFromServices, getAgentDir, SessionManager } from '@earendil-works/pi-coding-agent'
import { homedir } from 'node:os'
import { readFileSync, writeFileSync, mkdtempSync } from 'node:fs'
import { join } from 'node:path'
import { tmpdir } from 'node:os'
const cwd = homedir()
const agentDir = getAgentDir()
const services = await createAgentSessionServices({ cwd, agentDir })
const factory = async ({ cwd: nc, agentDir: na, sessionManager }) => {
const svc = nc === services.cwd && na === services.agentDir ? services : await createAgentSessionServices({ cwd: nc, agentDir: na })
const result = await createAgentSessionFromServices({ services: svc, sessionManager })
return { ...result, services: svc, diagnostics: svc.diagnostics }
}
const runtime = await createAgentSessionRuntime(factory, { cwd, agentDir, sessionManager: SessionManager.continueRecent(cwd) })
const mr = runtime.session.modelRuntime
// 1) 模型目录
const providers = mr.getProviders()
console.log('[smoke3] providers:', providers.map((p) => p.id).join(', '))
const anthropic = providers.find((p) => p.id === 'anthropic')
if (anthropic) {
const models = mr.getModels('anthropic')
console.log('[smoke3] anthropic models: %d (first: %s)', models.length, models[0]?.id)
// 2) login(假 key)→ listCredentials
const before = await mr.listCredentials()
await mr.login('anthropic', 'api_key', { prompt: async () => 'sk-smoke-fake-key', notify: async () => undefined })
const after = await mr.listCredentials()
console.log('[smoke3] credentials before=%d after=%d (anthropic=%s)', before.length, after.length, after.some((c) => c.providerId === 'anthropic' && c.type === 'api_key'))
// auth.json 应包含 anthropic 条目
const auth = JSON.parse(readFileSync(join(agentDir, 'auth.json'), 'utf8'))
console.log('[smoke3] auth.json has anthropic:', Boolean(auth.anthropic))
// 3) setModel(有凭据时允许)
const target = models[0]
await runtime.session.setModel(target, { persist: false })
console.log('[smoke3] setModel ok -> %s/%s', runtime.session.model?.provider, runtime.session.model?.id)
// 4) logout
await mr.logout('anthropic')
const finalCreds = await mr.listCredentials()
console.log('[smoke3] after logout, anthropic credential present:', finalCreds.some((c) => c.providerId === 'anthropic'))
} else {
console.log('[smoke3] anthropic not found in providers, skip key tests')
}
// 5) 系统提示词 override(独立 services,用 resourceLoaderOptions.systemPrompt)
const tmp = mkdtempSync(join(tmpdir(), 'aso-smoke-'))
const promptText = '你是冒烟测试助手。'
const svc2 = await createAgentSessionServices({ cwd, agentDir, resourceLoaderOptions: { systemPrompt: promptText } })
console.log('[smoke3] systemPrompt applied:', svc2.resourceLoader ? 'services-created' : 'fail')
const rt2 = await createAgentSessionFromServices({ services: svc2, sessionManager: SessionManager.inMemory(cwd) })
console.log('[smoke3] session systemPrompt startsWith expected:', rt2.session.systemPrompt.startsWith(promptText.slice(0, 8)))
await runtime.dispose()
writeFileSync(join(tmp, 'done'), '')
console.log('[smoke3] done')
process.exit(0)
-76
View File
@@ -1,76 +0,0 @@
/**
* pi SDK 会话管理冒烟测试(v0.2.0)
* 验证:runtime 创建、list、newSession、switchSession(历史恢复)、rename、删除(回收站)。
*/
import { createAgentSessionRuntime, createAgentSessionServices, createAgentSessionFromServices, getAgentDir, SessionManager } from '@earendil-works/pi-coding-agent'
import { homedir } from 'node:os'
import { join } from 'node:path'
const cwd = homedir()
const agentDir = getAgentDir()
console.log('[smoke2] listing sessions (cwd=%s)...', cwd)
const before = await SessionManager.list(cwd)
console.log('[smoke2] found %d sessions', before.length)
const initialServices = await createAgentSessionServices({ cwd, agentDir })
const factory = async ({ cwd: nextCwd, agentDir: nextAgentDir, sessionManager, sessionStartEvent }) => {
const services =
nextCwd === initialServices.cwd && nextAgentDir === initialServices.agentDir
? initialServices
: await createAgentSessionServices({ cwd: nextCwd, agentDir: nextAgentDir })
const result = await createAgentSessionFromServices({ services, sessionManager, sessionStartEvent })
return { ...result, services, diagnostics: services.diagnostics }
}
const sessionManager = SessionManager.create(cwd)
const runtime = await createAgentSessionRuntime(factory, { cwd, agentDir, sessionManager })
let rebindCount = 0
runtime.setRebindSession(async () => {
rebindCount += 1
})
console.log('[smoke2] runtime ready, session=%s', runtime.session.sessionId)
// 重命名
runtime.session.setSessionName('smoke-测试会话')
console.log('[smoke2] renamed to: %s', runtime.session.sessionName)
// 会话里追加用户+助手消息(直接写会话文件,不触发 LLM)
// pi 约定:只有出现 assistant 消息后才会真正落盘(避免空会话残留文件)
runtime.session.sessionManager.appendMessage({ role: 'user', content: '这是一条冒烟测试消息', timestamp: Date.now() })
runtime.session.sessionManager.appendMessage({
role: 'assistant',
content: [{ type: 'text', text: '冒烟回复(不经过 LLM)' }],
api: 'openai-completions',
provider: 'smoke',
model: 'smoke-model',
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0, cost: 0 },
stopReason: 'stop',
timestamp: Date.now()
})
console.log('[smoke2] messages in session:', runtime.session.messages.length)
const listAfter = await SessionManager.list(cwd)
const mine = listAfter.find((s) => s.name === 'smoke-测试会话' || s.firstMessage.includes('冒烟测试'))
console.log('[smoke2] session visible in list: %s (total %d)', Boolean(mine), listAfter.length)
if (mine) {
console.log('[smoke2] meta: name=%s firstMessage=%s count=%d', mine.name, JSON.stringify(mine.firstMessage.slice(0, 30)), mine.messageCount)
// 新会话(切换)
await runtime.newSession()
console.log('[smoke2] newSession ok, rebind=%d, new id=%s, messages=%d', rebindCount, runtime.session.sessionId, runtime.session.messages.length)
// 切回
await runtime.switchSession(mine.path)
console.log(
'[smoke2] switch back ok, rebind=%d, id=%s, messages restored=%d, name=%s',
rebindCount,
runtime.session.sessionId,
runtime.session.messages.length,
runtime.session.sessionName ?? '(none)'
)
}
await runtime.dispose()
console.log('[smoke2] done')
process.exit(0)
-66
View File
@@ -1,66 +0,0 @@
/**
* v0.4.0 冒烟:业务工具注册 / 手动直调(D2 双模式路径)/ tool_call 拦截契约
* 说明:审批拦截逻辑与 src/main/pi/approval.ts 保持一致(此处内联同构实现,
* 因冒烟脚本无法直接加载 TS 源文件;拦截契约由 pi 的 ExtensionHandler 类型保证)。
*/
import { createAgentSessionFromServices, createAgentSessionServices, defineTool, getAgentDir, SessionManager } from '@earendil-works/pi-coding-agent'
import { Type } from '@earendil-works/pi-ai'
import { homedir } from 'node:os'
const cwd = homedir()
const agentDir = getAgentDir()
// 与 src/main/tools/demo-tools.ts 同构的工具定义
const hotwordLookup = defineTool({
name: 'hotword_lookup',
label: '热点词查询(演示)',
description: 'demo',
parameters: Type.Object({ query: Type.String(), limit: Type.Optional(Type.Number()) }),
async execute(_id, params) {
return {
content: [{ type: 'text', text: `热点词查询结果(演示数据源,query="${params.query}"):ok` }],
details: { source: 'demo' }
}
}
})
// ---- 1) 工具注册检查(customTools 路径) ----
const services = await createAgentSessionServices({ cwd, agentDir })
const { session } = await createAgentSessionFromServices({
services,
sessionManager: SessionManager.inMemory(cwd),
customTools: [hotwordLookup]
})
const all = session.getAllTools()
console.log('[smoke4] registered tools:', all.map((t) => t.name).join(', '))
console.log('[smoke4] hotword_lookup registered:', all.some((t) => t.name === 'hotword_lookup'))
// ---- 2) 手动直调(双模式共用能力的证明,不经 LLM) ----
const result = await hotwordLookup.execute('smoke-call', { query: '宠物' }, undefined, undefined, {})
console.log('[smoke4] manual execute text:', JSON.stringify(result.content[0]?.text?.slice(0, 60)))
// ---- 3) tool_call 拦截契约(模拟 ExtensionAPI 捕获 handler) ----
const handlers = {}
const fakePi = { on: (event, handler) => { handlers[event] = handler } }
const needsApproval = new Set(['price_check', 'bash'])
let decision = false
fakePi.on('tool_call', async (event) => {
if (!needsApproval.has(event.toolName)) return
const approved = decision
if (!approved) return { block: true, reason: '用户拒绝了本次工具调用审批' }
return
})
const passthrough = await handlers['tool_call']({ type: 'tool_call', toolName: 'read', input: { path: 'x' } })
console.log('[smoke4] read(不在名单)放行:', passthrough === undefined)
decision = false
const denied = await handlers['tool_call']({ type: 'tool_call', toolName: 'price_check', input: { product_id: 'P1', target_price: 9.9 } })
console.log('[smoke4] price_check 拒绝 -> block:', JSON.stringify(denied))
decision = true
const approvedRes = await handlers['tool_call']({ type: 'tool_call', toolName: 'bash', input: { command: 'ls' } })
console.log('[smoke4] bash 批准 -> 放行:', approvedRes === undefined)
console.log('[smoke4] done')
process.exit(0)
-39
View File
@@ -1,39 +0,0 @@
/**
* pi SDK 集成冒烟测试(与 src/main/pi/agent-service.ts 相同的调用路径)
* 用法:node scripts/smoke.mjs
* 验证点:会话创建、事件订阅、prompt 在无凭据时的报错传播。
*/
import { createAgentSession } from '@earendil-works/pi-coding-agent'
import { homedir } from 'node:os'
const events = []
let settled = false
console.log('[smoke] creating session (cwd=%s)...', homedir())
const { session } = await createAgentSession({ cwd: homedir() })
console.log('[smoke] session created: id=%s model=%s/%s', session.sessionId, session.model?.provider ?? '-', session.model?.id ?? '-')
session.subscribe((event) => {
events.push(event.type)
if (event.type === 'message_update' && event.assistantMessageEvent?.type?.endsWith('_delta')) {
const d = event.assistantMessageEvent.delta ?? ''
process.stdout.write(d)
}
})
try {
await session.prompt('回复"链路正常"两个字即可,不要调用任何工具。')
console.log('\n[smoke] prompt resolved; event sequence:', [...new Set(events)].join(' -> '))
const last = session.getLastAssistantText()
console.log('[smoke] last assistant text:', last ? JSON.stringify(last.slice(0, 120)) : '(none)')
settled = true
} catch (err) {
console.log('\n[smoke] prompt rejected as expected (no credentials):', err instanceof Error ? err.message : err)
}
const stats = session.getSessionStats()
console.log('[smoke] stats:', JSON.stringify({ messages: session.messages.length, cost: stats.cost }))
console.log('[smoke] active tools:', session.getActiveToolNames().join(', '))
session.dispose()
console.log('[smoke] done, settled=%s', String(settled))
process.exit(0)