chore(docs,scenarios-*): better structure
|
Before Width: | Height: | Size: 773 KiB |
|
Before Width: | Height: | Size: 806 KiB |
|
Before Width: | Height: | Size: 755 KiB |
|
Before Width: | Height: | Size: 777 KiB |
|
Before Width: | Height: | Size: 763 KiB |
|
Before Width: | Height: | Size: 806 KiB |
|
Before Width: | Height: | Size: 779 KiB |
|
Before Width: | Height: | Size: 776 KiB |
|
Before Width: | Height: | Size: 757 KiB |
|
Before Width: | Height: | Size: 769 KiB |
|
Before Width: | Height: | Size: 773 KiB |
|
Before Width: | Height: | Size: 783 KiB |
|
Before Width: | Height: | Size: 773 KiB |
|
Before Width: | Height: | Size: 783 KiB |
|
Before Width: | Height: | Size: 808 KiB |
|
Before Width: | Height: | Size: 788 KiB |
|
Before Width: | Height: | Size: 826 KiB |
|
Before Width: | Height: | Size: 775 KiB |
|
Before Width: | Height: | Size: 790 KiB |
|
Before Width: | Height: | Size: 786 KiB |
|
Before Width: | Height: | Size: 807 KiB |
|
Before Width: | Height: | Size: 811 KiB |
|
Before Width: | Height: | Size: 772 KiB |
|
Before Width: | Height: | Size: 816 KiB |
|
Before Width: | Height: | Size: 811 KiB |
|
Before Width: | Height: | Size: 807 KiB |
|
Before Width: | Height: | Size: 799 KiB |
|
Before Width: | Height: | Size: 798 KiB |
|
Before Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 3.4 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 38 KiB |
|
Before Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 37 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 27 KiB |
|
Before Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 34 KiB |
@@ -1,27 +1,28 @@
|
||||
# Scenarios - Stage Tamagotchi Electron
|
||||
|
||||
Own the raw Electron capture scenarios for stage-tamagotchi.
|
||||
Own the Electron capture scenarios used to generate tamagotchi docs screenshots.
|
||||
|
||||
## Purpose
|
||||
|
||||
This package owns the product-specific scenario definitions only. It depends on `@proj-airi/vishot-runner-electron` for:
|
||||
This package owns product-specific Electron scenario definitions only. It depends on `@proj-airi/vishot-runner-electron` for:
|
||||
|
||||
- the `defineScenario()` helper
|
||||
- the capture context surface
|
||||
- Electron window and screenshot helpers exposed by the runner package
|
||||
|
||||
It does not launch Electron itself and it does not compose final browser-scene exports.
|
||||
It does not launch Electron itself and it does not own browser-scene composition or shared screenshot staging.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Build the Electron app.
|
||||
2. Run the runner package against one of the scenario modules in this package.
|
||||
3. Write raw screenshots into `packages/scenarios-stage-tamagotchi-browser/artifacts/raw`.
|
||||
4. Let the browser scene package consume those raw screenshots for final composition.
|
||||
2. Run the Electron runner against a section-based scenario entrypoint under `src/scenarios/demo-controls-settings-chat-websocket/index.ts`.
|
||||
3. Write the scenario's raw working outputs into the scenario-local working directory.
|
||||
4. Publish the final docs screenshots directly into `docs/content/en/docs/manual/tamagotchi/setup-and-use/assets`.
|
||||
5. Skip any separate final staging directory for this docs workflow.
|
||||
|
||||
```bash
|
||||
pnpm -F @proj-airi/stage-tamagotchi build
|
||||
pnpm -F @proj-airi/vishot-runner-electron capture -- packages/scenarios-stage-tamagotchi-electron/src/scenarios/demo-controls-settings-chat-websocket.ts --output-dir packages/scenarios-stage-tamagotchi-browser/artifacts/raw
|
||||
pnpm -F @proj-airi/vishot-runner-electron capture --format avif -- packages/scenarios-stage-tamagotchi-electron/src/scenarios/demo-controls-settings-chat-websocket/index.ts --output-dir /tmp/tamagotchi-docs-capture
|
||||
```
|
||||
|
||||
## Scenario Authoring
|
||||
@@ -45,8 +46,12 @@ export default defineScenario({
|
||||
})
|
||||
```
|
||||
|
||||
## Scenario Layout
|
||||
|
||||
The docs workflow is organized as one section-based scenario module under `src/scenarios/demo-controls-settings-chat-websocket/`. The top-level `index.ts` orchestrates section manifests and writes working outputs into the scenario-local raw directory, so the capture flow stays close to the scenario being authored.
|
||||
|
||||
## Notes
|
||||
|
||||
- Raw scenario modules live under `src/scenarios`.
|
||||
- Scenario modules are consumed by the runner package, not by the browser composition package.
|
||||
- The package stays focused on business capture flows and avoids browser-scene composition concerns.
|
||||
- Scenario entrypoints should point at `index.ts` when the workflow is organized as a section folder.
|
||||
- Keep this package focused on Electron capture flows for docs screenshots.
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
/* eslint-disable e18e/prefer-static-regex */
|
||||
import { sleep } from '@moeru/std'
|
||||
import { defineScenario } from '@proj-airi/vishot-runner-electron'
|
||||
|
||||
const airiCardPattern = /sort|排序|upload|上传/i
|
||||
const providersPattern = /Chat|Speech|Transcription/i
|
||||
// NOTICE: Must stay unique to /settings/data. The prior alternates `chat` / `聊天`
|
||||
// also matched the providers page (step 06), which renders a `Chat` / `聊天` provider
|
||||
// category immediately before this step — so the readiness check could pass on stale
|
||||
// providers DOM and silently produce a mislabeled screenshot. `打开` was also too
|
||||
// generic (appears on multiple pages as button labels). `Open app data folder` is
|
||||
// the desktop-folder section title and is literal English across every locale file.
|
||||
const dataPattern = /Open app data folder/i
|
||||
const systemGeneralPattern = /theme|主题|language|语言/i
|
||||
const systemColorSchemePattern = /RGB|Primary Color|主题颜色|500\/50/i
|
||||
const modelsPattern = /select model|confirm|缩放与位置|Zoom & Position/i
|
||||
const modulesPattern = /Consciousness|意识|Speech|发声|Hearing|听觉/i
|
||||
const hearingPattern = /Audio Input Device|音频输入设备|start monitoring|Start Monitoring|Transcription Result/i
|
||||
const developerPattern = /Open DevTools|打开|Markdown|Lag|Vision Capture|Screen Capture/i
|
||||
// NOTICE: Anchor on the consciousness page's always-rendered section description
|
||||
// (consciousness.vue renders this unconditionally regardless of provider/model state
|
||||
// or user locale). Previous alternates like `提供商` / `No Providers Configured` did
|
||||
// not match zh-Hans (which renders `服务来源` / `没有配置服务来源`), and `当前模型` /
|
||||
// `Current model` only appear when a model is already selected — so fresh environments
|
||||
// with no providers configured would hang on readiness.
|
||||
//
|
||||
// Caveat: vision.vue reuses the same `provider-model-selection.description` i18n key,
|
||||
// so this pattern also matches on /settings/modules/vision. That is safe here because
|
||||
// consciousness (step 14) is reached from developer (step 13), which does not render
|
||||
// either phrase — no stale-DOM collision is possible at this step. If the step order
|
||||
// ever changes, pick a token unique to consciousness.vue instead.
|
||||
const consciousnessPattern = /Select the suitable LLM|为意识选择合适/i
|
||||
const speechPattern = /Hello, my name is AI Assistant|Test voice|Voice|声音|Speech|选择语音合成服务来源/i
|
||||
const visionPattern = /Capture interval|context|ollama|提供商|Current model|Chat|Vision capture cadence/i
|
||||
const useWindowMousePattern = /useWindowMouse|\d+,\s*\d+/i
|
||||
const displaysPattern = /useElectronAllDisplays|@\s*\d+°|Visualize connected displays and cursor position/i
|
||||
const widgetsCallingPattern = /Widget id is required|Small \(s\)|Spawned widget|Component name/i
|
||||
const contextFlowPattern = /Active contexts|Prompt projection|Runtime|Context Flow|Filters/i
|
||||
const relativeMousePattern = /windowX = screenX - windowBounds\.x|Green dot shows current window-relative cursor position|Relative Mouse/i
|
||||
const beatSyncPattern = /Beat sync driver|Hit beat|Punchy V|Beat Sync Visualizer/i
|
||||
const websocketInspectorPattern = /Incoming|Outgoing|Filter payload|No messages found|WebSocket Inspector/i
|
||||
const pluginHostPattern = /Discovered|Enabled|Loaded|Capabilities|Plugin Host Debug/i
|
||||
const screenCapturePattern = /Applications|Displays|Refetch|Share Window|Share Screen|屏幕捕获|Open system preferences|打开系统偏好设置/i
|
||||
// NOTICE: Must stay unique to /devtools/vision. Step 25 captures /devtools/screen-capture
|
||||
// immediately before this, and both pages render `Applications` / `Displays` tab labels, so
|
||||
// matching against those generics lets the readiness check pass against the stale
|
||||
// screen-capture DOM and silently produce a mislabeled screenshot. Only use text that does
|
||||
// not appear on screen-capture.vue.
|
||||
const visionCapturePattern = /Capture interval|No vision output yet|vision capture/i
|
||||
const websocketServerAddressPattern = /WebSocket Server Address|WebSocket 服务器地址/i
|
||||
|
||||
function normalizeHashPath(hash: string): string {
|
||||
const withoutHash = hash.startsWith('#')
|
||||
? hash.slice(1)
|
||||
: hash
|
||||
|
||||
return withoutHash || '/'
|
||||
}
|
||||
|
||||
function isTimeoutLikeError(error: unknown): boolean {
|
||||
return error instanceof Error && error.name === 'TimeoutError'
|
||||
}
|
||||
|
||||
export default defineScenario({
|
||||
id: 'demo-controls-settings-chat-websocket',
|
||||
async run({ capture, controlsIsland, settingsWindow, stageWindows }) {
|
||||
const mainWindow = await stageWindows.waitFor('main')
|
||||
await controlsIsland.waitForReady(mainWindow.page)
|
||||
|
||||
async function ensureControlsIslandExpanded() {
|
||||
const chatButton = mainWindow.page
|
||||
.locator('button')
|
||||
.filter({
|
||||
has: mainWindow.page.locator('[i-solar\\:chat-line-line-duotone]'),
|
||||
})
|
||||
.first()
|
||||
|
||||
const chatButtonVisible = await chatButton.isVisible().catch(() => false)
|
||||
if (!chatButtonVisible) {
|
||||
await controlsIsland.expand(mainWindow.page)
|
||||
await sleep(250)
|
||||
}
|
||||
}
|
||||
|
||||
await capture('00-stage-tamagotchi', mainWindow.page)
|
||||
await sleep(500)
|
||||
|
||||
await controlsIsland.expand(mainWindow.page)
|
||||
await sleep(250)
|
||||
await capture('01-controls-island-expanded', mainWindow.page)
|
||||
await sleep(250)
|
||||
|
||||
await ensureControlsIslandExpanded()
|
||||
const chatWindowSnapshot = await controlsIsland.openChat(mainWindow.page)
|
||||
await chatWindowSnapshot.page.getByText(/Chat/i).first().waitFor({ state: 'visible' })
|
||||
await sleep(1000)
|
||||
await capture('04-chat-window', chatWindowSnapshot.page)
|
||||
await sleep(250)
|
||||
|
||||
await mainWindow.page.bringToFront()
|
||||
await controlsIsland.waitForReady(mainWindow.page)
|
||||
await ensureControlsIslandExpanded()
|
||||
|
||||
const settingsWindowSnapshot = await controlsIsland.openSettings(mainWindow.page)
|
||||
await settingsWindowSnapshot.page.getByText(/connection|websocket|router/i).first().waitFor({ state: 'visible' })
|
||||
await sleep(1000)
|
||||
await capture('02-settings-window', settingsWindowSnapshot.page)
|
||||
|
||||
async function captureSettingsRoute(name: string, routePath: string, readyPattern: RegExp, waitMs = 250) {
|
||||
await settingsWindow.goToRoute(settingsWindowSnapshot.page, routePath)
|
||||
try {
|
||||
await settingsWindowSnapshot.page.getByText(readyPattern).first().waitFor({ state: 'visible', timeout: 15_000 })
|
||||
}
|
||||
catch (error) {
|
||||
if (!isTimeoutLikeError(error)) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const currentHashPath = normalizeHashPath(new URL(settingsWindowSnapshot.page.url()).hash)
|
||||
if (currentHashPath !== routePath) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// NOTICE: Some settings/devtools pages animate in or hydrate content asynchronously.
|
||||
// Give known-slow pages one final bounded grace period, but still fail if the target route never becomes ready.
|
||||
await sleep(1250)
|
||||
await settingsWindowSnapshot.page.getByText(readyPattern).first().waitFor({ state: 'visible', timeout: 5_000 })
|
||||
}
|
||||
await sleep(waitMs)
|
||||
await capture(name, settingsWindowSnapshot.page)
|
||||
}
|
||||
|
||||
await settingsWindow.goToRoute(settingsWindowSnapshot.page, '/settings/airi-card')
|
||||
await settingsWindowSnapshot.page.getByText(airiCardPattern).first().waitFor({ state: 'visible' })
|
||||
await sleep(1000)
|
||||
await capture('05-airi-card', settingsWindowSnapshot.page)
|
||||
|
||||
await settingsWindow.goToRoute(settingsWindowSnapshot.page, '/settings/providers')
|
||||
await settingsWindowSnapshot.page.getByText(providersPattern).first().waitFor({ state: 'visible' })
|
||||
await sleep(1000)
|
||||
await capture('06-providers', settingsWindowSnapshot.page)
|
||||
|
||||
await settingsWindow.goToRoute(settingsWindowSnapshot.page, '/settings/data')
|
||||
await settingsWindowSnapshot.page.getByText(dataPattern).first().waitFor({ state: 'visible' })
|
||||
await sleep(1000)
|
||||
await capture('07-data', settingsWindowSnapshot.page)
|
||||
|
||||
await settingsWindow.goToRoute(settingsWindowSnapshot.page, '/settings/system/general')
|
||||
await settingsWindowSnapshot.page.getByText(systemGeneralPattern).first().waitFor({ state: 'visible' })
|
||||
await sleep(1000)
|
||||
await capture('08-system-general', settingsWindowSnapshot.page)
|
||||
|
||||
await settingsWindow.goToRoute(settingsWindowSnapshot.page, '/settings/system/color-scheme')
|
||||
await settingsWindowSnapshot.page.getByText(systemColorSchemePattern).first().waitFor({ state: 'visible' })
|
||||
await sleep(1000)
|
||||
await capture('09-system-color-scheme', settingsWindowSnapshot.page)
|
||||
|
||||
await settingsWindow.goToRoute(settingsWindowSnapshot.page, '/settings/models')
|
||||
await settingsWindowSnapshot.page.getByText(modelsPattern).first().waitFor({ state: 'visible' })
|
||||
await sleep(1000)
|
||||
await capture('10-models', settingsWindowSnapshot.page)
|
||||
|
||||
await settingsWindow.goToRoute(settingsWindowSnapshot.page, '/settings/modules')
|
||||
await settingsWindowSnapshot.page.getByText(modulesPattern).first().waitFor({ state: 'visible' })
|
||||
await sleep(1000)
|
||||
await capture('11-modules', settingsWindowSnapshot.page)
|
||||
|
||||
await settingsWindow.goToRoute(settingsWindowSnapshot.page, '/settings/modules/hearing')
|
||||
await settingsWindowSnapshot.page.getByText(hearingPattern).first().waitFor({ state: 'visible' })
|
||||
await sleep(1000)
|
||||
await capture('12-hearing', settingsWindowSnapshot.page)
|
||||
|
||||
await settingsWindow.goToRoute(settingsWindowSnapshot.page, '/settings/system/developer')
|
||||
await settingsWindowSnapshot.page.getByText(developerPattern).first().waitFor({ state: 'visible' })
|
||||
await sleep(1000)
|
||||
await capture('13-system-developer', settingsWindowSnapshot.page)
|
||||
|
||||
await captureSettingsRoute('14-consciousness', '/settings/modules/consciousness', consciousnessPattern)
|
||||
await captureSettingsRoute('15-speech', '/settings/modules/speech', speechPattern, 500)
|
||||
await captureSettingsRoute('16-vision', '/settings/modules/vision', visionPattern)
|
||||
await captureSettingsRoute('17-devtools-use-window-mouse', '/devtools/use-window-mouse', useWindowMousePattern)
|
||||
await captureSettingsRoute('18-devtools-displays', '/devtools/use-electron-all-displays', displaysPattern)
|
||||
await captureSettingsRoute('19-devtools-widgets-calling', '/devtools/widgets-calling', widgetsCallingPattern)
|
||||
await captureSettingsRoute('20-devtools-context-flow', '/devtools/context-flow', contextFlowPattern)
|
||||
await captureSettingsRoute('21-devtools-relative-mouse', '/devtools/use-electron-relative-mouse', relativeMousePattern)
|
||||
await captureSettingsRoute('22-devtools-beat-sync', '/devtools/beat-sync', beatSyncPattern)
|
||||
await captureSettingsRoute('23-devtools-websocket-inspector', '/devtools/websocket-inspector', websocketInspectorPattern)
|
||||
await captureSettingsRoute('24-devtools-plugin-host', '/devtools/plugin-host', pluginHostPattern)
|
||||
await captureSettingsRoute('25-devtools-screen-capture', '/devtools/screen-capture', screenCapturePattern, 500)
|
||||
await captureSettingsRoute('26-devtools-vision-capture', '/devtools/vision', visionCapturePattern, 500)
|
||||
|
||||
await settingsWindowSnapshot.page.bringToFront()
|
||||
await sleep(500)
|
||||
|
||||
const websocketSettingsPage = await settingsWindow.goToConnection(settingsWindowSnapshot.page)
|
||||
await websocketSettingsPage.getByText(websocketServerAddressPattern).waitFor({ state: 'visible' })
|
||||
await sleep(1000)
|
||||
await capture('03-websocket-settings', websocketSettingsPage)
|
||||
},
|
||||
})
|
||||
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 63 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 4.0 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 26 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 7.1 KiB |
|
After Width: | Height: | Size: 27 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 20 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 2.5 KiB |
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 22 KiB |
@@ -0,0 +1,34 @@
|
||||
import type { ManualRuntime } from './shared/types'
|
||||
|
||||
import { defineScenario } from '@proj-airi/vishot-runner-electron'
|
||||
|
||||
import { manualCaptureSections } from './manifest'
|
||||
import { formatStepFailure, removePublishedManualAssets, resetScenarioOutputDirectories } from './shared/output'
|
||||
import { runCaptureStep } from './shared/steps'
|
||||
|
||||
export default defineScenario({
|
||||
id: 'demo-controls-settings-chat-websocket',
|
||||
async run(context) {
|
||||
const mainWindow = await context.stageWindows.waitFor('main')
|
||||
await context.controlsIsland.waitForReady(mainWindow.page)
|
||||
|
||||
const runtime: ManualRuntime = {
|
||||
context,
|
||||
mainWindow,
|
||||
}
|
||||
|
||||
await removePublishedManualAssets()
|
||||
await resetScenarioOutputDirectories()
|
||||
|
||||
for (const section of manualCaptureSections) {
|
||||
for (const step of section.steps) {
|
||||
try {
|
||||
await runCaptureStep(step, runtime)
|
||||
}
|
||||
catch (error) {
|
||||
throw formatStepFailure(section.id, step.id, error)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
import { devtoolsSection } from './sections/devtools'
|
||||
import { overviewSection } from './sections/overview'
|
||||
import { settingsSection } from './sections/settings'
|
||||
|
||||
export const manualCaptureSections = [
|
||||
overviewSection,
|
||||
settingsSection,
|
||||
devtoolsSection,
|
||||
]
|
||||
|
||||
export const manualAssetFileNames = manualCaptureSections.flatMap(section =>
|
||||
section.steps.map(step => step.docAssetFileName),
|
||||
)
|
||||
@@ -0,0 +1,105 @@
|
||||
import type { ManualCaptureSection } from '../shared/types'
|
||||
|
||||
const useWindowMousePattern = /useWindowMouse|\d+,\s*\d+/i
|
||||
const displaysPattern = /useElectronAllDisplays|@\s*\d+°|Visualize connected displays and cursor position/i
|
||||
const widgetsCallingPattern = /Widget id is required|Small \(s\)|Spawned widget|Component name/i
|
||||
const contextFlowPattern = /Active contexts|Prompt projection|Runtime|Context Flow|Filters/i
|
||||
const relativeMousePattern = /windowX = screenX - windowBounds\.x|Green dot shows current window-relative cursor position|Relative Mouse/i
|
||||
const beatSyncPattern = /Beat sync driver|Hit beat|Punchy V|Beat Sync Visualizer/i
|
||||
const websocketInspectorPattern = /Incoming|Outgoing|Filter payload|No messages found|WebSocket Inspector/i
|
||||
const pluginHostPattern = /Discovered|Enabled|Loaded|Capabilities|Plugin Host Debug/i
|
||||
const screenCapturePattern = /Applications|Displays|Refetch|Share Window|Share Screen|屏幕捕获|Open system preferences|打开系统偏好设置/i
|
||||
// NOTICE: Must stay unique to /devtools/vision. The previous step captures
|
||||
// /devtools/screen-capture and both pages render `Applications` / `Displays`,
|
||||
// so matching against those generics can pass against stale screen-capture DOM
|
||||
// and silently produce a mislabeled screenshot.
|
||||
const visionCapturePattern = /Capture interval|No vision output yet|vision capture/i
|
||||
|
||||
export const devtoolsSection: ManualCaptureSection = {
|
||||
id: 'devtools',
|
||||
label: 'Developer tools',
|
||||
steps: [
|
||||
{
|
||||
id: 'use-window-mouse',
|
||||
kind: 'settings-route',
|
||||
routePath: '/devtools/use-window-mouse',
|
||||
readyPattern: useWindowMousePattern,
|
||||
rawCaptureName: '17-devtools-use-window-mouse',
|
||||
docAssetFileName: 'manual-devtools-use-window-mouse.avif',
|
||||
},
|
||||
{
|
||||
id: 'displays',
|
||||
kind: 'settings-route',
|
||||
routePath: '/devtools/use-electron-all-displays',
|
||||
readyPattern: displaysPattern,
|
||||
rawCaptureName: '18-devtools-displays',
|
||||
docAssetFileName: 'manual-devtools-displays.avif',
|
||||
},
|
||||
{
|
||||
id: 'widgets-calling',
|
||||
kind: 'settings-route',
|
||||
routePath: '/devtools/widgets-calling',
|
||||
readyPattern: widgetsCallingPattern,
|
||||
rawCaptureName: '19-devtools-widgets-calling',
|
||||
docAssetFileName: 'manual-devtools-widgets-calling.avif',
|
||||
},
|
||||
{
|
||||
id: 'context-flow',
|
||||
kind: 'settings-route',
|
||||
routePath: '/devtools/context-flow',
|
||||
readyPattern: contextFlowPattern,
|
||||
rawCaptureName: '20-devtools-context-flow',
|
||||
docAssetFileName: 'manual-devtools-context-flow.avif',
|
||||
},
|
||||
{
|
||||
id: 'relative-mouse',
|
||||
kind: 'settings-route',
|
||||
routePath: '/devtools/use-electron-relative-mouse',
|
||||
readyPattern: relativeMousePattern,
|
||||
rawCaptureName: '21-devtools-relative-mouse',
|
||||
docAssetFileName: 'manual-devtools-relative-mouse.avif',
|
||||
},
|
||||
{
|
||||
id: 'beat-sync',
|
||||
kind: 'settings-route',
|
||||
routePath: '/devtools/beat-sync',
|
||||
readyPattern: beatSyncPattern,
|
||||
rawCaptureName: '22-devtools-beat-sync',
|
||||
docAssetFileName: 'manual-devtools-beat-sync.avif',
|
||||
},
|
||||
{
|
||||
id: 'websocket-inspector',
|
||||
kind: 'settings-route',
|
||||
routePath: '/devtools/websocket-inspector',
|
||||
readyPattern: websocketInspectorPattern,
|
||||
rawCaptureName: '23-devtools-websocket-inspector',
|
||||
docAssetFileName: 'manual-devtools-websocket-inspector.avif',
|
||||
},
|
||||
{
|
||||
id: 'plugin-host',
|
||||
kind: 'settings-route',
|
||||
routePath: '/devtools/plugin-host',
|
||||
readyPattern: pluginHostPattern,
|
||||
rawCaptureName: '24-devtools-plugin-host',
|
||||
docAssetFileName: 'manual-devtools-plugin-host.avif',
|
||||
},
|
||||
{
|
||||
id: 'screen-capture',
|
||||
kind: 'settings-route',
|
||||
routePath: '/devtools/screen-capture',
|
||||
readyPattern: screenCapturePattern,
|
||||
rawCaptureName: '25-devtools-screen-capture',
|
||||
docAssetFileName: 'manual-devtools-screen-capture.avif',
|
||||
waitMs: 500,
|
||||
},
|
||||
{
|
||||
id: 'vision-capture',
|
||||
kind: 'settings-route',
|
||||
routePath: '/devtools/vision',
|
||||
readyPattern: visionCapturePattern,
|
||||
rawCaptureName: '26-devtools-vision-capture',
|
||||
docAssetFileName: 'manual-devtools-vision-capture.avif',
|
||||
waitMs: 500,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import type { ManualCaptureSection } from '../shared/types'
|
||||
|
||||
const websocketServerAddressPattern = /WebSocket Server Address|WebSocket 服务器地址/i
|
||||
|
||||
export const overviewSection: ManualCaptureSection = {
|
||||
id: 'overview',
|
||||
label: 'Interface overview',
|
||||
steps: [
|
||||
{
|
||||
id: 'main-window',
|
||||
kind: 'main-window',
|
||||
rawCaptureName: '00-stage-tamagotchi',
|
||||
docAssetFileName: 'manual-main-window.avif',
|
||||
},
|
||||
{
|
||||
id: 'controls-island-expanded',
|
||||
kind: 'controls-island',
|
||||
rawCaptureName: '01-controls-island-expanded',
|
||||
docAssetFileName: 'manual-controls-island-expanded.avif',
|
||||
waitMs: 250,
|
||||
},
|
||||
{
|
||||
id: 'chat-window',
|
||||
kind: 'chat-window',
|
||||
rawCaptureName: '04-chat-window',
|
||||
docAssetFileName: 'manual-chat-window.avif',
|
||||
readyPattern: /Chat/i,
|
||||
waitMs: 1000,
|
||||
},
|
||||
{
|
||||
id: 'settings-window',
|
||||
kind: 'settings-overview',
|
||||
rawCaptureName: '02-settings-window',
|
||||
docAssetFileName: 'manual-settings-window.avif',
|
||||
readyPattern: /connection|websocket|router/i,
|
||||
waitMs: 1000,
|
||||
},
|
||||
{
|
||||
id: 'websocket-settings',
|
||||
kind: 'connection',
|
||||
rawCaptureName: '03-websocket-settings',
|
||||
docAssetFileName: 'manual-websocket-settings.avif',
|
||||
readyPattern: websocketServerAddressPattern,
|
||||
waitMs: 1000,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
import type { ManualCaptureSection } from '../shared/types'
|
||||
|
||||
const airiCardPattern = /sort|排序|upload|上传/i
|
||||
const providersPattern = /Chat|Speech|Transcription/i
|
||||
// NOTICE: Must stay unique to /settings/data. The prior alternates `chat` / `聊天`
|
||||
// also matched the providers page, which renders a `Chat` / `聊天` provider category
|
||||
// immediately before this step. That allowed the readiness check to pass against stale
|
||||
// providers DOM and silently produce a mislabeled screenshot. `打开` was also too
|
||||
// generic because it appears on multiple pages as button labels. `Open app data folder`
|
||||
// is the desktop-folder section title and is literal English across every locale file.
|
||||
const dataPattern = /Open app data folder/i
|
||||
const systemGeneralPattern = /theme|主题|language|语言/i
|
||||
const systemColorSchemePattern = /RGB|Primary Color|主题颜色|500\/50/i
|
||||
const modelsPattern = /select model|confirm|缩放与位置|Zoom & Position/i
|
||||
const modulesPattern = /Consciousness|意识|Speech|发声|Hearing|听觉/i
|
||||
const hearingPattern = /Audio Input Device|音频输入设备|start monitoring|Start Monitoring|Transcription Result/i
|
||||
const developerPattern = /Open DevTools|打开|Markdown|Lag|Vision Capture|Screen Capture/i
|
||||
// NOTICE: Anchor on the consciousness page's always-rendered section description.
|
||||
// Previous alternates like `提供商` / `No Providers Configured` did not match zh-Hans,
|
||||
// and `当前模型` / `Current model` only appear when a model is already selected.
|
||||
// Fresh environments with no providers configured would hang without this pattern.
|
||||
//
|
||||
// Caveat: vision.vue reuses the same i18n key, so this also matches on
|
||||
// /settings/modules/vision. That remains safe because this step is reached from
|
||||
// developer, which does not render either phrase. If the step order changes,
|
||||
// pick a token unique to consciousness.vue instead.
|
||||
const consciousnessPattern = /Select the suitable LLM|为意识选择合适/i
|
||||
const speechPattern = /Hello, my name is AI Assistant|Test voice|Voice|声音|Speech|选择语音合成服务来源/i
|
||||
const visionPattern = /Capture interval|context|ollama|提供商|Current model|Chat|Vision capture cadence/i
|
||||
|
||||
export const settingsSection: ManualCaptureSection = {
|
||||
id: 'settings',
|
||||
label: 'Settings surfaces',
|
||||
steps: [
|
||||
{
|
||||
id: 'airi-card',
|
||||
kind: 'settings-route',
|
||||
routePath: '/settings/airi-card',
|
||||
readyPattern: airiCardPattern,
|
||||
rawCaptureName: '05-airi-card',
|
||||
docAssetFileName: 'manual-airi-card.avif',
|
||||
waitMs: 1000,
|
||||
},
|
||||
{
|
||||
id: 'providers',
|
||||
kind: 'settings-route',
|
||||
routePath: '/settings/providers',
|
||||
readyPattern: providersPattern,
|
||||
rawCaptureName: '06-providers',
|
||||
docAssetFileName: 'manual-providers.avif',
|
||||
waitMs: 1000,
|
||||
},
|
||||
{
|
||||
id: 'data',
|
||||
kind: 'settings-route',
|
||||
routePath: '/settings/data',
|
||||
readyPattern: dataPattern,
|
||||
rawCaptureName: '07-data',
|
||||
docAssetFileName: 'manual-data-settings.avif',
|
||||
waitMs: 1000,
|
||||
},
|
||||
{
|
||||
id: 'system-general',
|
||||
kind: 'settings-route',
|
||||
routePath: '/settings/system/general',
|
||||
readyPattern: systemGeneralPattern,
|
||||
rawCaptureName: '08-system-general',
|
||||
docAssetFileName: 'manual-system-general.avif',
|
||||
waitMs: 1000,
|
||||
},
|
||||
{
|
||||
id: 'system-color-scheme',
|
||||
kind: 'settings-route',
|
||||
routePath: '/settings/system/color-scheme',
|
||||
readyPattern: systemColorSchemePattern,
|
||||
rawCaptureName: '09-system-color-scheme',
|
||||
docAssetFileName: 'manual-system-color-scheme.avif',
|
||||
waitMs: 1000,
|
||||
},
|
||||
{
|
||||
id: 'models',
|
||||
kind: 'settings-route',
|
||||
routePath: '/settings/models',
|
||||
readyPattern: modelsPattern,
|
||||
rawCaptureName: '10-models',
|
||||
docAssetFileName: 'manual-models.avif',
|
||||
waitMs: 1000,
|
||||
},
|
||||
{
|
||||
id: 'modules',
|
||||
kind: 'settings-route',
|
||||
routePath: '/settings/modules',
|
||||
readyPattern: modulesPattern,
|
||||
rawCaptureName: '11-modules',
|
||||
docAssetFileName: 'manual-modules.avif',
|
||||
waitMs: 1000,
|
||||
},
|
||||
{
|
||||
id: 'hearing',
|
||||
kind: 'settings-route',
|
||||
routePath: '/settings/modules/hearing',
|
||||
readyPattern: hearingPattern,
|
||||
rawCaptureName: '12-hearing',
|
||||
docAssetFileName: 'manual-hearing.avif',
|
||||
waitMs: 1000,
|
||||
},
|
||||
{
|
||||
id: 'system-developer',
|
||||
kind: 'settings-route',
|
||||
routePath: '/settings/system/developer',
|
||||
readyPattern: developerPattern,
|
||||
rawCaptureName: '13-system-developer',
|
||||
docAssetFileName: 'manual-system-developer.avif',
|
||||
waitMs: 1000,
|
||||
},
|
||||
{
|
||||
id: 'consciousness',
|
||||
kind: 'settings-route',
|
||||
routePath: '/settings/modules/consciousness',
|
||||
readyPattern: consciousnessPattern,
|
||||
rawCaptureName: '14-consciousness',
|
||||
docAssetFileName: 'manual-consciousness.avif',
|
||||
},
|
||||
{
|
||||
id: 'speech',
|
||||
kind: 'settings-route',
|
||||
routePath: '/settings/modules/speech',
|
||||
readyPattern: speechPattern,
|
||||
rawCaptureName: '15-speech',
|
||||
docAssetFileName: 'manual-speech.avif',
|
||||
waitMs: 500,
|
||||
},
|
||||
{
|
||||
id: 'vision',
|
||||
kind: 'settings-route',
|
||||
routePath: '/settings/modules/vision',
|
||||
readyPattern: visionPattern,
|
||||
rawCaptureName: '16-vision',
|
||||
docAssetFileName: 'manual-vision.avif',
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import path from 'node:path'
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const scenarioDirectoryPath = fileURLToPath(new URL('..', import.meta.url))
|
||||
const repoRootPath = fileURLToPath(new URL('../../../../../../', import.meta.url))
|
||||
|
||||
export const scenarioRawOutputDir = path.join(scenarioDirectoryPath, 'assets', 'raw')
|
||||
export const manualDocsAssetsDir = path.join(
|
||||
repoRootPath,
|
||||
'docs',
|
||||
'content',
|
||||
'en',
|
||||
'docs',
|
||||
'manual',
|
||||
'tamagotchi',
|
||||
'setup-and-use',
|
||||
'assets',
|
||||
)
|
||||
@@ -0,0 +1,80 @@
|
||||
import type { VishotArtifact } from '@proj-airi/vishot-runner-electron'
|
||||
|
||||
import type { PublishedArtifacts } from './types'
|
||||
|
||||
import path from 'node:path'
|
||||
|
||||
import { copyFile, mkdir, rm } from 'node:fs/promises'
|
||||
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
|
||||
import { manualAssetFileNames } from '../manifest'
|
||||
import { manualDocsAssetsDir, scenarioRawOutputDir } from './constants'
|
||||
|
||||
function extensionOf(filePath: string): string {
|
||||
return path.extname(filePath).toLowerCase()
|
||||
}
|
||||
|
||||
export async function resetScenarioOutputDirectories() {
|
||||
await rm(scenarioRawOutputDir, { recursive: true, force: true })
|
||||
await mkdir(scenarioRawOutputDir, { recursive: true })
|
||||
}
|
||||
|
||||
export async function removePublishedManualAssets() {
|
||||
await mkdir(manualDocsAssetsDir, { recursive: true })
|
||||
|
||||
await Promise.all(
|
||||
manualAssetFileNames.map(fileName =>
|
||||
rm(path.join(manualDocsAssetsDir, fileName), { force: true }),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
export async function publishArtifactsToDocs(
|
||||
artifacts: VishotArtifact[],
|
||||
targetFileName: string,
|
||||
): Promise<PublishedArtifacts> {
|
||||
if (artifacts.length === 0) {
|
||||
throw new Error(`Expected at least one artifact when publishing "${targetFileName}".`)
|
||||
}
|
||||
|
||||
await mkdir(scenarioRawOutputDir, { recursive: true })
|
||||
await mkdir(manualDocsAssetsDir, { recursive: true })
|
||||
|
||||
const rawArtifactPaths: string[] = []
|
||||
|
||||
for (const artifact of artifacts) {
|
||||
const rawArtifactPath = path.join(scenarioRawOutputDir, path.basename(artifact.filePath))
|
||||
await copyFile(artifact.filePath, rawArtifactPath)
|
||||
rawArtifactPaths.push(rawArtifactPath)
|
||||
}
|
||||
|
||||
const primaryArtifact = artifacts[0]
|
||||
if (extensionOf(primaryArtifact.filePath) !== extensionOf(targetFileName)) {
|
||||
throw new Error(
|
||||
[
|
||||
`Cannot publish "${targetFileName}" from "${path.basename(primaryArtifact.filePath)}".`,
|
||||
'Run the capture CLI with a matching format, for example `--format avif` for the manual docs flow.',
|
||||
].join(' '),
|
||||
)
|
||||
}
|
||||
|
||||
const docsAssetPath = path.join(manualDocsAssetsDir, targetFileName)
|
||||
await copyFile(primaryArtifact.filePath, docsAssetPath)
|
||||
await Promise.all(
|
||||
artifacts.map(artifact => rm(artifact.filePath, { force: true })),
|
||||
)
|
||||
|
||||
return {
|
||||
docsAssetPath,
|
||||
rawArtifactPaths,
|
||||
}
|
||||
}
|
||||
|
||||
export function formatStepFailure(sectionId: string, stepId: string, error: unknown): Error {
|
||||
const message = errorMessageFrom(error) ?? 'Unknown screenshot automation error'
|
||||
|
||||
return new Error(`[${sectionId}/${stepId}] ${message}`, {
|
||||
cause: error instanceof Error ? error : undefined,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { ManualRuntime } from './types'
|
||||
|
||||
import { sleep } from '@moeru/std'
|
||||
|
||||
export function normalizeHashPath(hash: string): string {
|
||||
const withoutHash = hash.startsWith('#')
|
||||
? hash.slice(1)
|
||||
: hash
|
||||
|
||||
return withoutHash || '/'
|
||||
}
|
||||
|
||||
export function isTimeoutLikeError(error: unknown): boolean {
|
||||
return error instanceof Error && error.name === 'TimeoutError'
|
||||
}
|
||||
|
||||
export async function ensureControlsIslandExpanded(runtime: ManualRuntime) {
|
||||
const { mainWindow, context } = runtime
|
||||
const chatButton = mainWindow.page
|
||||
.locator('button')
|
||||
.filter({
|
||||
has: mainWindow.page.locator('[i-solar\\:chat-line-line-duotone]'),
|
||||
})
|
||||
.first()
|
||||
|
||||
const chatButtonVisible = await chatButton.isVisible().catch(() => false)
|
||||
|
||||
if (!chatButtonVisible) {
|
||||
await context.controlsIsland.expand(mainWindow.page)
|
||||
await sleep(250)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getChatWindowSnapshot(runtime: ManualRuntime) {
|
||||
if (runtime.chatWindowSnapshot) {
|
||||
return runtime.chatWindowSnapshot
|
||||
}
|
||||
|
||||
await ensureControlsIslandExpanded(runtime)
|
||||
|
||||
const chatWindowSnapshot = await runtime.context.controlsIsland.openChat(runtime.mainWindow.page)
|
||||
runtime.chatWindowSnapshot = chatWindowSnapshot
|
||||
|
||||
return chatWindowSnapshot
|
||||
}
|
||||
|
||||
export async function getSettingsWindowSnapshot(runtime: ManualRuntime) {
|
||||
if (runtime.settingsWindowSnapshot) {
|
||||
return runtime.settingsWindowSnapshot
|
||||
}
|
||||
|
||||
await runtime.mainWindow.page.bringToFront()
|
||||
await runtime.context.controlsIsland.waitForReady(runtime.mainWindow.page)
|
||||
await ensureControlsIslandExpanded(runtime)
|
||||
|
||||
const settingsWindowSnapshot = await runtime.context.controlsIsland.openSettings(runtime.mainWindow.page)
|
||||
runtime.settingsWindowSnapshot = settingsWindowSnapshot
|
||||
|
||||
return settingsWindowSnapshot
|
||||
}
|
||||
|
||||
export async function waitForRouteReadiness(
|
||||
runtime: ManualRuntime,
|
||||
routePath: string,
|
||||
readyPattern: RegExp,
|
||||
) {
|
||||
const settingsWindowSnapshot = await getSettingsWindowSnapshot(runtime)
|
||||
|
||||
await runtime.context.settingsWindow.goToRoute(settingsWindowSnapshot.page, routePath)
|
||||
|
||||
try {
|
||||
await settingsWindowSnapshot.page.getByText(readyPattern).first().waitFor({ state: 'visible', timeout: 15_000 })
|
||||
}
|
||||
catch (error) {
|
||||
if (!isTimeoutLikeError(error)) {
|
||||
throw error
|
||||
}
|
||||
|
||||
const currentHashPath = normalizeHashPath(new URL(settingsWindowSnapshot.page.url()).hash)
|
||||
if (currentHashPath !== routePath) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// NOTICE: Some settings and devtools pages animate in or hydrate content
|
||||
// asynchronously. Give known-slow pages one final bounded grace period.
|
||||
await sleep(1250)
|
||||
await settingsWindowSnapshot.page.getByText(readyPattern).first().waitFor({ state: 'visible', timeout: 5_000 })
|
||||
}
|
||||
|
||||
return settingsWindowSnapshot
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { CaptureExecutionResult, ManualCaptureStep, ManualRuntime } from './types'
|
||||
|
||||
import { sleep } from '@moeru/std'
|
||||
|
||||
import { publishArtifactsToDocs } from './output'
|
||||
import { ensureControlsIslandExpanded, getChatWindowSnapshot, getSettingsWindowSnapshot, waitForRouteReadiness } from './runtime'
|
||||
|
||||
async function captureStepPage(step: ManualCaptureStep, runtime: ManualRuntime): Promise<CaptureExecutionResult> {
|
||||
let page
|
||||
|
||||
switch (step.kind) {
|
||||
case 'main-window': {
|
||||
page = runtime.mainWindow.page
|
||||
break
|
||||
}
|
||||
case 'controls-island': {
|
||||
await ensureControlsIslandExpanded(runtime)
|
||||
page = runtime.mainWindow.page
|
||||
break
|
||||
}
|
||||
case 'chat-window': {
|
||||
const chatWindowSnapshot = await getChatWindowSnapshot(runtime)
|
||||
|
||||
if (step.readyPattern) {
|
||||
await chatWindowSnapshot.page.getByText(step.readyPattern).first().waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
page = chatWindowSnapshot.page
|
||||
break
|
||||
}
|
||||
case 'settings-overview': {
|
||||
const settingsWindowSnapshot = await getSettingsWindowSnapshot(runtime)
|
||||
|
||||
if (step.readyPattern) {
|
||||
await settingsWindowSnapshot.page.getByText(step.readyPattern).first().waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
page = settingsWindowSnapshot.page
|
||||
break
|
||||
}
|
||||
case 'settings-route': {
|
||||
if (!step.routePath || !step.readyPattern) {
|
||||
throw new Error(`Step "${step.id}" requires both routePath and readyPattern.`)
|
||||
}
|
||||
|
||||
const settingsWindowSnapshot = await waitForRouteReadiness(runtime, step.routePath, step.readyPattern)
|
||||
page = settingsWindowSnapshot.page
|
||||
break
|
||||
}
|
||||
case 'connection': {
|
||||
const settingsWindowSnapshot = await getSettingsWindowSnapshot(runtime)
|
||||
const websocketSettingsPage = await runtime.context.settingsWindow.goToConnection(settingsWindowSnapshot.page)
|
||||
|
||||
if (step.readyPattern) {
|
||||
await websocketSettingsPage.getByText(step.readyPattern).waitFor({ state: 'visible' })
|
||||
}
|
||||
|
||||
page = websocketSettingsPage
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (step.waitMs) {
|
||||
await sleep(step.waitMs)
|
||||
}
|
||||
|
||||
const artifacts = await runtime.context.capture(step.rawCaptureName, page)
|
||||
const publishedArtifacts = await publishArtifactsToDocs(artifacts, step.docAssetFileName)
|
||||
|
||||
return {
|
||||
artifacts,
|
||||
publishedArtifacts,
|
||||
}
|
||||
}
|
||||
|
||||
export async function runCaptureStep(step: ManualCaptureStep, runtime: ManualRuntime): Promise<CaptureExecutionResult> {
|
||||
return captureStepPage(step, runtime)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import type { ScenarioContext, VishotArtifact } from '@proj-airi/vishot-runner-electron'
|
||||
|
||||
export type ManualSectionId = 'overview' | 'settings' | 'devtools'
|
||||
export type ManualCaptureStepKind = 'main-window' | 'controls-island' | 'chat-window' | 'settings-overview' | 'settings-route' | 'connection'
|
||||
export type StageWindowSnapshotLike = Awaited<ReturnType<ScenarioContext['stageWindows']['waitFor']>>
|
||||
|
||||
export interface ManualCaptureStep {
|
||||
docAssetFileName: string
|
||||
id: string
|
||||
kind: ManualCaptureStepKind
|
||||
rawCaptureName: string
|
||||
readyPattern?: RegExp
|
||||
routePath?: string
|
||||
waitMs?: number
|
||||
}
|
||||
|
||||
export interface ManualCaptureSection {
|
||||
id: ManualSectionId
|
||||
label: string
|
||||
steps: ManualCaptureStep[]
|
||||
}
|
||||
|
||||
export interface ManualRuntime {
|
||||
chatWindowSnapshot?: StageWindowSnapshotLike
|
||||
context: ScenarioContext
|
||||
mainWindow: StageWindowSnapshotLike
|
||||
settingsWindowSnapshot?: StageWindowSnapshotLike
|
||||
}
|
||||
|
||||
export interface PublishedArtifacts {
|
||||
docsAssetPath: string
|
||||
rawArtifactPaths: string[]
|
||||
}
|
||||
|
||||
export interface CaptureExecutionResult {
|
||||
artifacts: VishotArtifact[]
|
||||
publishedArtifacts: PublishedArtifacts
|
||||
}
|
||||