@midscene/core 1.10.2-beta-20260706032158.0 → 1.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/es/agent/agent.mjs +40 -1
- package/dist/es/agent/agent.mjs.map +1 -1
- package/dist/es/agent/metrics.mjs +81 -0
- package/dist/es/agent/metrics.mjs.map +1 -0
- package/dist/es/agent/task-builder.mjs +1 -1
- package/dist/es/agent/task-builder.mjs.map +1 -1
- package/dist/es/agent/utils.mjs +1 -1
- package/dist/es/ai-model/connectivity/run-connectivity-test.mjs +9 -3
- package/dist/es/ai-model/connectivity/run-connectivity-test.mjs.map +1 -1
- package/dist/es/ai-model/inspect.mjs +3 -3
- package/dist/es/ai-model/inspect.mjs.map +1 -1
- package/dist/es/ai-model/llm-planning.mjs +1 -1
- package/dist/es/ai-model/llm-planning.mjs.map +1 -1
- package/dist/es/ai-model/model-adapter/resolve.mjs +2 -2
- package/dist/es/ai-model/model-adapter/resolve.mjs.map +1 -1
- package/dist/es/ai-model/models/default.mjs +2 -1
- package/dist/es/ai-model/models/default.mjs.map +1 -1
- package/dist/es/ai-model/models/doubao.mjs +38 -60
- package/dist/es/ai-model/models/doubao.mjs.map +1 -1
- package/dist/es/ai-model/models/gemini.mjs +4 -15
- package/dist/es/ai-model/models/gemini.mjs.map +1 -1
- package/dist/es/ai-model/models/glm.mjs +4 -0
- package/dist/es/ai-model/models/glm.mjs.map +1 -1
- package/dist/es/ai-model/models/gpt.mjs +5 -1
- package/dist/es/ai-model/models/gpt.mjs.map +1 -1
- package/dist/es/ai-model/models/kimi.mjs +4 -0
- package/dist/es/ai-model/models/kimi.mjs.map +1 -1
- package/dist/es/ai-model/models/mimo.mjs +3 -2
- package/dist/es/ai-model/models/mimo.mjs.map +1 -1
- package/dist/es/ai-model/models/qwen.mjs.map +1 -1
- package/dist/es/ai-model/models/ui-tars/adapter.mjs +2 -41
- package/dist/es/ai-model/models/ui-tars/adapter.mjs.map +1 -1
- package/dist/es/ai-model/models/utils/intent.mjs +6 -0
- package/dist/es/ai-model/models/utils/intent.mjs.map +1 -0
- package/dist/es/ai-model/prompt/describe.mjs +31 -34
- package/dist/es/ai-model/prompt/describe.mjs.map +1 -1
- package/dist/es/ai-model/prompt/extraction.mjs +15 -9
- package/dist/es/ai-model/prompt/extraction.mjs.map +1 -1
- package/dist/es/ai-model/prompt/llm-planning.mjs +19 -19
- package/dist/es/ai-model/prompt/llm-planning.mjs.map +1 -1
- package/dist/es/ai-model/prompt/locate-grounding-rules.mjs +7 -3
- package/dist/es/ai-model/prompt/locate-grounding-rules.mjs.map +1 -1
- package/dist/es/ai-model/prompt/recorder-metadata-generator.mjs +2 -1
- package/dist/es/ai-model/prompt/recorder-metadata-generator.mjs.map +1 -1
- package/dist/es/ai-model/service-caller/index.mjs +40 -19
- package/dist/es/ai-model/service-caller/index.mjs.map +1 -1
- package/dist/es/ai-model/service-caller/json.mjs +31 -28
- package/dist/es/ai-model/service-caller/json.mjs.map +1 -1
- package/dist/es/device/index.mjs +2 -4
- package/dist/es/device/index.mjs.map +1 -1
- package/dist/es/dump/html-utils.mjs +63 -1
- package/dist/es/dump/html-utils.mjs.map +1 -1
- package/dist/es/dump/screenshot-restoration.mjs +6 -0
- package/dist/es/dump/screenshot-restoration.mjs.map +1 -1
- package/dist/es/element-describer.mjs +36 -18
- package/dist/es/element-describer.mjs.map +1 -1
- package/dist/es/index.mjs +2 -2
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/report-generator.mjs +28 -1
- package/dist/es/report-generator.mjs.map +1 -1
- package/dist/es/report-markdown.mjs +300 -35
- package/dist/es/report-markdown.mjs.map +1 -1
- package/dist/es/report.mjs +29 -1
- package/dist/es/report.mjs.map +1 -1
- package/dist/es/service/index.mjs +101 -25
- package/dist/es/service/index.mjs.map +1 -1
- package/dist/es/service/utils.mjs +65 -1
- package/dist/es/service/utils.mjs.map +1 -1
- package/dist/es/types.mjs.map +1 -1
- package/dist/es/utils.mjs +2 -2
- package/dist/lib/agent/agent.js +40 -1
- package/dist/lib/agent/agent.js.map +1 -1
- package/dist/lib/agent/metrics.js +115 -0
- package/dist/lib/agent/metrics.js.map +1 -0
- package/dist/lib/agent/task-builder.js +1 -1
- package/dist/lib/agent/task-builder.js.map +1 -1
- package/dist/lib/agent/utils.js +1 -1
- package/dist/lib/ai-model/connectivity/run-connectivity-test.js +9 -3
- package/dist/lib/ai-model/connectivity/run-connectivity-test.js.map +1 -1
- package/dist/lib/ai-model/inspect.js +3 -3
- package/dist/lib/ai-model/inspect.js.map +1 -1
- package/dist/lib/ai-model/llm-planning.js +1 -1
- package/dist/lib/ai-model/llm-planning.js.map +1 -1
- package/dist/lib/ai-model/model-adapter/resolve.js +1 -1
- package/dist/lib/ai-model/model-adapter/resolve.js.map +1 -1
- package/dist/lib/ai-model/models/default.js +2 -1
- package/dist/lib/ai-model/models/default.js.map +1 -1
- package/dist/lib/ai-model/models/doubao.js +38 -69
- package/dist/lib/ai-model/models/doubao.js.map +1 -1
- package/dist/lib/ai-model/models/gemini.js +4 -15
- package/dist/lib/ai-model/models/gemini.js.map +1 -1
- package/dist/lib/ai-model/models/glm.js +4 -0
- package/dist/lib/ai-model/models/glm.js.map +1 -1
- package/dist/lib/ai-model/models/gpt.js +5 -1
- package/dist/lib/ai-model/models/gpt.js.map +1 -1
- package/dist/lib/ai-model/models/kimi.js +4 -0
- package/dist/lib/ai-model/models/kimi.js.map +1 -1
- package/dist/lib/ai-model/models/mimo.js +3 -2
- package/dist/lib/ai-model/models/mimo.js.map +1 -1
- package/dist/lib/ai-model/models/qwen.js.map +1 -1
- package/dist/lib/ai-model/models/ui-tars/adapter.js +1 -40
- package/dist/lib/ai-model/models/ui-tars/adapter.js.map +1 -1
- package/dist/lib/ai-model/models/utils/intent.js +40 -0
- package/dist/lib/ai-model/models/utils/intent.js.map +1 -0
- package/dist/lib/ai-model/prompt/describe.js +31 -34
- package/dist/lib/ai-model/prompt/describe.js.map +1 -1
- package/dist/lib/ai-model/prompt/extraction.js +14 -8
- package/dist/lib/ai-model/prompt/extraction.js.map +1 -1
- package/dist/lib/ai-model/prompt/llm-planning.js +19 -19
- package/dist/lib/ai-model/prompt/llm-planning.js.map +1 -1
- package/dist/lib/ai-model/prompt/locate-grounding-rules.js +7 -3
- package/dist/lib/ai-model/prompt/locate-grounding-rules.js.map +1 -1
- package/dist/lib/ai-model/prompt/recorder-metadata-generator.js +4 -3
- package/dist/lib/ai-model/prompt/recorder-metadata-generator.js.map +1 -1
- package/dist/lib/ai-model/service-caller/index.js +44 -23
- package/dist/lib/ai-model/service-caller/index.js.map +1 -1
- package/dist/lib/ai-model/service-caller/json.js +33 -33
- package/dist/lib/ai-model/service-caller/json.js.map +1 -1
- package/dist/lib/device/index.js +2 -4
- package/dist/lib/device/index.js.map +1 -1
- package/dist/lib/dump/html-utils.js +65 -0
- package/dist/lib/dump/html-utils.js.map +1 -1
- package/dist/lib/dump/screenshot-restoration.js +6 -0
- package/dist/lib/dump/screenshot-restoration.js.map +1 -1
- package/dist/lib/element-describer.js +37 -22
- package/dist/lib/element-describer.js.map +1 -1
- package/dist/lib/index.js +1 -4
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/report-generator.js +27 -0
- package/dist/lib/report-generator.js.map +1 -1
- package/dist/lib/report-markdown.js +300 -35
- package/dist/lib/report-markdown.js.map +1 -1
- package/dist/lib/report.js +28 -0
- package/dist/lib/report.js.map +1 -1
- package/dist/lib/service/index.js +99 -23
- package/dist/lib/service/index.js.map +1 -1
- package/dist/lib/service/utils.js +98 -1
- package/dist/lib/service/utils.js.map +1 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/lib/utils.js +2 -2
- package/dist/types/agent/agent.d.ts +15 -2
- package/dist/types/agent/index.d.ts +1 -0
- package/dist/types/agent/metrics.d.ts +48 -0
- package/dist/types/ai-model/model-adapter/types.d.ts +8 -0
- package/dist/types/ai-model/models/doubao.d.ts +1 -4
- package/dist/types/ai-model/models/gemini.d.ts +1 -1
- package/dist/types/ai-model/models/registry.d.ts +1 -1
- package/dist/types/ai-model/models/utils/intent.d.ts +2 -0
- package/dist/types/ai-model/service-caller/index.d.ts +8 -3
- package/dist/types/ai-model/service-caller/json.d.ts +19 -2
- package/dist/types/device/device-options.d.ts +0 -28
- package/dist/types/device/index.d.ts +0 -5
- package/dist/types/dump/html-utils.d.ts +2 -0
- package/dist/types/dump/screenshot-restoration.d.ts +1 -1
- package/dist/types/element-describer.d.ts +3 -6
- package/dist/types/index.d.ts +2 -2
- package/dist/types/report-generator.d.ts +6 -0
- package/dist/types/service/index.d.ts +1 -1
- package/dist/types/service/utils.d.ts +21 -2
- package/dist/types/types.d.ts +12 -2
- package/package.json +3 -3
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report-generator.mjs","sources":["../../src/report-generator.ts"],"sourcesContent":["/*\n * PERF INVARIANT — DO NOT reintroduce sync fs APIs (writeFileSync /\n * appendFileSync) in this file's write paths. `ReportGenerator` runs on\n * the Electron main event loop during agent execution, and a single\n * progress tick appends a multi-MB ExecutionDump payload. Sync I/O here\n * blocked the loop for 20+ seconds per run, freezing IPC, scrcpy and\n * every renderer round-trip. Always use `fs/promises`. See commit\n * 6a25e05c and `report-generator-async-contract.test.ts`.\n */\nimport { existsSync, mkdirSync, readdirSync } from 'node:fs';\nimport {\n appendFile as appendFileAsync,\n writeFile as writeFileAsync,\n} from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { getMidsceneRunSubDir } from '@midscene/shared/common';\nimport {\n MIDSCENE_REPORT_QUIET,\n globalConfigManager,\n} from '@midscene/shared/env';\nimport { ifInBrowser, logMsg, uuid } from '@midscene/shared/utils';\nimport {\n DATA_SCREENSHOT_MODE_ATTR,\n generateDumpScriptTag,\n generateImageScriptTag,\n getBaseUrlFixScript,\n} from './dump/html-utils';\nimport { ScreenshotStore } from './dump/screenshot-store';\nimport {\n type ExecutionDump,\n ReportActionDump,\n type ReportAttributes,\n type ReportMeta,\n type ScreenshotMode,\n} from './types';\nimport { getReportTpl } from './utils';\n\nexport interface IReportGenerator {\n /**\n * Write or update a single execution.\n * Each call appends a new dump script tag. The frontend deduplicates\n * executions with the same id/name, keeping only the last one.\n *\n * @param execution Current execution's full data\n * @param reportMeta Report-level metadata (groupName, sdkVersion, etc.)\n */\n onExecutionUpdate(\n execution: ExecutionDump,\n reportMeta: ReportMeta,\n attributes?: ReportAttributes,\n ): void;\n\n /**\n * @deprecated Use onExecutionUpdate instead. Kept for backward compatibility.\n */\n onDumpUpdate?(dump: ReportActionDump): void;\n\n /**\n * Wait for all queued write operations to complete.\n */\n flush(): Promise<void>;\n\n /**\n * Finalize the report. Calls flush() internally.\n */\n finalize(): Promise<string | undefined>;\n\n getReportPath(): string | undefined;\n}\n\nexport const nullReportGenerator: IReportGenerator = {\n onExecutionUpdate: () => {},\n flush: async () => {},\n finalize: async () => undefined,\n getReportPath: () => undefined,\n};\n\nexport function assertReportGenerationOptions(opts: {\n generateReport?: boolean;\n persistExecutionDump?: boolean;\n}): void {\n if (opts.generateReport === false && opts.persistExecutionDump === true) {\n throw new Error(\n 'persistExecutionDump cannot be true when generateReport is false',\n );\n }\n}\n\nexport class ReportGenerator implements IReportGenerator {\n private reportPath: string;\n private screenshotMode: ScreenshotMode;\n private shouldPersistExecutionDump: boolean;\n private autoPrint: boolean;\n private firstWriteDone = false;\n private executionLogIndex = 0;\n private executionLogFileIndexByExecutionKey = new Map<string, number>();\n\n // Unique identifier for this report stream — used as data-group-id\n private readonly reportStreamId: string;\n\n // Tracks screenshots already written to disk (by id) to avoid duplicates\n private screenshotStore: ScreenshotStore;\n private initialized = false;\n\n // Tracks the last execution + groupMeta for re-writing on finalize\n private lastExecution?: ExecutionDump;\n private lastReportMeta?: ReportMeta;\n private reportAttributes: Record<string, string> = {};\n\n // write queue for serial execution\n private writeQueue: Promise<void> = Promise.resolve();\n private destroyed = false;\n\n constructor(options: {\n reportPath: string;\n screenshotMode: ScreenshotMode;\n persistExecutionDump?: boolean;\n autoPrint?: boolean;\n reuseExistingReport?: boolean;\n }) {\n this.reportPath = options.reportPath;\n this.screenshotMode = options.screenshotMode;\n this.shouldPersistExecutionDump = options.persistExecutionDump ?? false;\n this.autoPrint = options.autoPrint ?? true;\n this.reportStreamId = uuid();\n this.screenshotStore = new ScreenshotStore({\n mode: this.screenshotMode === 'inline' ? 'inline' : 'directory',\n reportPath: this.reportPath,\n screenshotsDir: join(dirname(this.reportPath), 'screenshots'),\n writeInlineImage: async (id, base64) => {\n await appendFileAsync(\n this.reportPath,\n `\\n${generateImageScriptTag(id, base64)}`,\n );\n },\n alsoWriteFileCopy: this.shouldPersistExecutionDump,\n });\n if (options.reuseExistingReport) {\n this.hydrateStateFromExistingReport();\n }\n }\n\n static create(\n reportFileName: string,\n opts: {\n generateReport?: boolean;\n persistExecutionDump?: boolean;\n outputFormat?: 'single-html' | 'html-and-external-assets';\n autoPrintReportMsg?: boolean;\n reuseExistingReport?: boolean;\n },\n ): IReportGenerator {\n assertReportGenerationOptions(opts);\n validateReportFileName(reportFileName);\n if (opts.generateReport === false) return nullReportGenerator;\n\n // In browser environment, file system is not available\n if (ifInBrowser) return nullReportGenerator;\n\n const reportRootDir = getMidsceneRunSubDir('report');\n const outputDir = join(reportRootDir, reportFileName);\n const reportPath =\n opts.outputFormat === 'html-and-external-assets'\n ? join(outputDir, 'index.html')\n : join(reportRootDir, ensureHtmlFileName(reportFileName));\n return new ReportGenerator({\n reportPath,\n screenshotMode:\n opts.outputFormat === 'html-and-external-assets'\n ? 'directory'\n : 'inline',\n persistExecutionDump: opts.persistExecutionDump,\n autoPrint: opts.autoPrintReportMsg,\n reuseExistingReport: opts.reuseExistingReport,\n });\n }\n\n onExecutionUpdate(\n execution: ExecutionDump,\n reportMeta: ReportMeta,\n attributes?: ReportAttributes,\n ): void {\n this.lastExecution = execution;\n this.lastReportMeta = reportMeta;\n this.mergeReportAttributes(attributes);\n this.writeQueue = this.writeQueue.then(async () => {\n if (this.destroyed) return;\n await this.doWriteExecution(execution, reportMeta);\n });\n }\n\n async flush(): Promise<void> {\n await this.writeQueue;\n }\n\n async finalize(): Promise<string | undefined> {\n // Re-write the last execution to capture any final state changes\n if (this.lastExecution && this.lastReportMeta) {\n this.onExecutionUpdate(this.lastExecution, this.lastReportMeta);\n }\n await this.flush();\n this.destroyed = true;\n\n if (!this.initialized) {\n // No executions were ever written — no file exists\n return undefined;\n }\n\n return this.reportPath;\n }\n\n getReportPath(): string | undefined {\n return this.reportPath;\n }\n\n private printReportPath(): void {\n if (!this.autoPrint || !this.reportPath) return;\n if (globalConfigManager.getEnvConfigInBoolean(MIDSCENE_REPORT_QUIET))\n return;\n\n if (this.screenshotMode === 'directory') {\n logMsg(\n `Midscene - report file updated: npx serve ${dirname(this.reportPath)}`,\n );\n } else {\n logMsg(`Midscene - report file updated: ${this.reportPath}`);\n }\n }\n\n private async doWriteExecution(\n execution: ExecutionDump,\n reportMeta: ReportMeta,\n ): Promise<void> {\n const singleDump = this.wrapAsReportDump(execution, reportMeta);\n\n if (this.screenshotMode === 'inline') {\n await this.writeInlineExecution(execution, singleDump);\n } else {\n await this.writeDirectoryExecution(execution, singleDump);\n }\n\n if (this.shouldPersistExecutionDump) {\n await this.persistExecutionDumpToFile(execution, singleDump);\n }\n\n if (!this.firstWriteDone) {\n this.firstWriteDone = true;\n this.printReportPath();\n }\n }\n\n private mergeReportAttributes(attributes?: ReportAttributes): void {\n if (!attributes) {\n return;\n }\n\n for (const [key, value] of Object.entries(attributes)) {\n if (value === undefined || value === null) {\n continue;\n }\n this.reportAttributes[key] = String(value);\n }\n }\n\n private hydrateStateFromExistingReport(): void {\n if (!existsSync(this.reportPath)) {\n return;\n }\n\n // Reuse existing report file and append new updates instead of rewriting.\n this.initialized = true;\n\n if (!this.shouldPersistExecutionDump) {\n return;\n }\n\n const reportDir = dirname(this.reportPath);\n const existingExecutionIndices = readdirSync(reportDir)\n .map((name) => /^(\\d+)\\.execution\\.json$/.exec(name)?.[1])\n .filter((index): index is string => Boolean(index))\n .map((index) => Number.parseInt(index, 10))\n .filter((index) => Number.isFinite(index));\n\n if (existingExecutionIndices.length > 0) {\n this.executionLogIndex = Math.max(...existingExecutionIndices);\n }\n }\n\n private getDumpScriptAttributes(): Record<string, string> {\n return {\n 'data-group-id': this.reportStreamId,\n // Self-describe how this report file stores screenshots so consumers\n // (merge/delete) never have to guess the mode from the filesystem.\n [DATA_SCREENSHOT_MODE_ATTR]: this.screenshotMode,\n ...this.reportAttributes,\n };\n }\n\n /**\n * Wrap an ExecutionDump + ReportMeta into a single-execution ReportActionDump.\n */\n private wrapAsReportDump(\n execution: ExecutionDump,\n reportMeta: ReportMeta,\n ): ReportActionDump {\n return new ReportActionDump({\n sdkVersion: reportMeta.sdkVersion,\n groupName: reportMeta.groupName,\n groupDescription: reportMeta.groupDescription,\n modelBriefs: reportMeta.modelBriefs,\n deviceType: reportMeta.deviceType,\n executions: [execution],\n });\n }\n\n /**\n * Append-only inline mode: write new screenshots and a dump tag on every call.\n * The frontend deduplicates executions with the same id/name (keeps last).\n * Duplicate dump JSON is acceptable; only screenshots are deduplicated.\n *\n * All writes go through `fs/promises` so they run on libuv's thread pool\n * rather than blocking the Node event loop. A long agent run previously\n * appended multi-MB dumps (screenshots + serialized tasks) per progress\n * tick on the main thread, starving IPC and UI for >10s per stall.\n */\n private async writeInlineExecution(\n execution: ExecutionDump,\n singleDump: ReportActionDump,\n ): Promise<void> {\n const dir = dirname(this.reportPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // Initialize: write HTML template once\n if (!this.initialized) {\n await writeFileAsync(this.reportPath, getReportTpl());\n this.initialized = true;\n }\n\n // Append new screenshots (skip already-written ones)\n for (const screenshot of execution.collectScreenshots()) {\n await this.screenshotStore.persist(screenshot);\n }\n\n // Append dump tag (always — frontend keeps only last per execution id)\n const serialized = singleDump.serialize();\n await appendFileAsync(\n this.reportPath,\n `\\n${generateDumpScriptTag(serialized, this.getDumpScriptAttributes())}`,\n );\n }\n\n private async writeDirectoryExecution(\n execution: ExecutionDump,\n singleDump: ReportActionDump,\n ): Promise<void> {\n const dir = dirname(this.reportPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n for (const screenshot of execution.collectScreenshots()) {\n await this.screenshotStore.persist(screenshot);\n }\n\n // 2. Append dump tag (always — frontend keeps only last per execution id)\n const serialized = singleDump.serialize();\n\n if (!this.initialized) {\n await writeFileAsync(\n this.reportPath,\n `${getReportTpl()}${getBaseUrlFixScript()}`,\n );\n this.initialized = true;\n }\n\n await appendFileAsync(\n this.reportPath,\n `\\n${generateDumpScriptTag(serialized, this.getDumpScriptAttributes())}`,\n );\n }\n\n private getExecutionLogKey(execution: ExecutionDump): string {\n if (!execution.id) {\n throw new Error(\n 'ReportGenerator: execution.id is required for persisting execution dumps',\n );\n }\n return `id:${execution.id}`;\n }\n\n private async persistExecutionDumpToFile(\n execution: ExecutionDump,\n singleDump: ReportActionDump,\n ): Promise<void> {\n const dir = dirname(this.reportPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const executionLogKey = this.getExecutionLogKey(execution);\n let fileIndex =\n this.executionLogFileIndexByExecutionKey.get(executionLogKey);\n if (!fileIndex) {\n this.executionLogIndex += 1;\n fileIndex = this.executionLogIndex;\n this.executionLogFileIndexByExecutionKey.set(executionLogKey, fileIndex);\n }\n\n const fileName = `${fileIndex}.execution.json`;\n const filePath = join(dirname(this.reportPath), fileName);\n await writeFileAsync(filePath, singleDump.serialize(2), 'utf-8');\n }\n}\n\nfunction ensureHtmlFileName(reportFileName: string): string {\n return reportFileName.endsWith('.html')\n ? reportFileName\n : `${reportFileName}.html`;\n}\n\nfunction validateReportFileName(reportFileName: string): void {\n if (!reportFileName?.trim()) {\n throw new Error('reportFileName must be a non-empty string');\n }\n\n if (/[\\\\/]/.test(reportFileName)) {\n throw new Error(\n 'reportFileName must not contain path separators (`/` or `\\\\\\\\`)',\n );\n }\n\n if (/[:*?\"<>|]/.test(reportFileName)) {\n throw new Error(\n 'reportFileName contains illegal filename characters: : * ? \" < > |',\n );\n }\n}\n"],"names":["nullReportGenerator","undefined","assertReportGenerationOptions","opts","Error","ReportGenerator","reportFileName","validateReportFileName","ifInBrowser","reportRootDir","getMidsceneRunSubDir","outputDir","join","reportPath","ensureHtmlFileName","execution","reportMeta","attributes","globalConfigManager","MIDSCENE_REPORT_QUIET","logMsg","dirname","singleDump","key","value","Object","String","existsSync","reportDir","existingExecutionIndices","readdirSync","name","index","Boolean","Number","Math","DATA_SCREENSHOT_MODE_ATTR","ReportActionDump","dir","mkdirSync","writeFileAsync","getReportTpl","screenshot","serialized","appendFileAsync","generateDumpScriptTag","getBaseUrlFixScript","executionLogKey","fileIndex","fileName","filePath","options","Map","Promise","uuid","ScreenshotStore","id","base64","generateImageScriptTag"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"report-generator.mjs","sources":["../../src/report-generator.ts"],"sourcesContent":["/*\n * PERF INVARIANT — DO NOT reintroduce sync fs APIs (writeFileSync /\n * appendFileSync) in this file's write paths. `ReportGenerator` runs on\n * the Electron main event loop during agent execution, and a single\n * progress tick appends a multi-MB ExecutionDump payload. Sync I/O here\n * blocked the loop for 20+ seconds per run, freezing IPC, scrcpy and\n * every renderer round-trip. Always use `fs/promises`. See commit\n * 6a25e05c and `report-generator-async-contract.test.ts`.\n */\nimport { existsSync, mkdirSync, readdirSync } from 'node:fs';\nimport {\n appendFile as appendFileAsync,\n writeFile as writeFileAsync,\n} from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport { getMidsceneRunSubDir } from '@midscene/shared/common';\nimport {\n MIDSCENE_REPORT_QUIET,\n globalConfigManager,\n} from '@midscene/shared/env';\nimport { ifInBrowser, logMsg, uuid } from '@midscene/shared/utils';\nimport {\n DATA_SCREENSHOT_MODE_ATTR,\n generateAgentReportComment,\n generateDumpScriptTag,\n generateImageScriptTag,\n getBaseUrlFixScript,\n} from './dump/html-utils';\nimport { ScreenshotStore } from './dump/screenshot-store';\nimport {\n type ExecutionDump,\n ReportActionDump,\n type ReportAttributes,\n type ReportMeta,\n type ScreenshotMode,\n} from './types';\nimport { getReportTpl } from './utils';\n\nexport interface IReportGenerator {\n /**\n * Write or update a single execution.\n * Each call appends a new dump script tag. The frontend deduplicates\n * executions with the same id/name, keeping only the last one.\n *\n * @param execution Current execution's full data\n * @param reportMeta Report-level metadata (groupName, sdkVersion, etc.)\n */\n onExecutionUpdate(\n execution: ExecutionDump,\n reportMeta: ReportMeta,\n attributes?: ReportAttributes,\n ): void;\n\n /**\n * @deprecated Use onExecutionUpdate instead. Kept for backward compatibility.\n */\n onDumpUpdate?(dump: ReportActionDump): void;\n\n /**\n * Wait for all queued write operations to complete.\n */\n flush(): Promise<void>;\n\n /**\n * Finalize the report. Calls flush() internally.\n */\n finalize(): Promise<string | undefined>;\n\n getReportPath(): string | undefined;\n}\n\nexport const nullReportGenerator: IReportGenerator = {\n onExecutionUpdate: () => {},\n flush: async () => {},\n finalize: async () => undefined,\n getReportPath: () => undefined,\n};\n\nexport function assertReportGenerationOptions(opts: {\n generateReport?: boolean;\n persistExecutionDump?: boolean;\n}): void {\n if (opts.generateReport === false && opts.persistExecutionDump === true) {\n throw new Error(\n 'persistExecutionDump cannot be true when generateReport is false',\n );\n }\n}\n\nexport class ReportGenerator implements IReportGenerator {\n private reportPath: string;\n private screenshotMode: ScreenshotMode;\n private shouldPersistExecutionDump: boolean;\n private autoPrint: boolean;\n private firstWriteDone = false;\n private executionLogIndex = 0;\n private executionLogFileIndexByExecutionKey = new Map<string, number>();\n\n // Unique identifier for this report stream — used as data-group-id\n private readonly reportStreamId: string;\n\n // Tracks screenshots already written to disk (by id) to avoid duplicates\n private screenshotStore: ScreenshotStore;\n private initialized = false;\n\n // Tracks the last execution + groupMeta for re-writing on finalize\n private lastExecution?: ExecutionDump;\n private lastReportMeta?: ReportMeta;\n private executionsByKey = new Map<string, ExecutionDump>();\n private executionCommentKeyByObject = new WeakMap<ExecutionDump, string>();\n private executionCommentKeyIndex = 0;\n private reportAttributes: Record<string, string> = {};\n private agentCommentWritten = false;\n\n // write queue for serial execution\n private writeQueue: Promise<void> = Promise.resolve();\n private destroyed = false;\n\n constructor(options: {\n reportPath: string;\n screenshotMode: ScreenshotMode;\n persistExecutionDump?: boolean;\n autoPrint?: boolean;\n reuseExistingReport?: boolean;\n }) {\n this.reportPath = options.reportPath;\n this.screenshotMode = options.screenshotMode;\n this.shouldPersistExecutionDump = options.persistExecutionDump ?? false;\n this.autoPrint = options.autoPrint ?? true;\n this.reportStreamId = uuid();\n this.screenshotStore = new ScreenshotStore({\n mode: this.screenshotMode === 'inline' ? 'inline' : 'directory',\n reportPath: this.reportPath,\n screenshotsDir: join(dirname(this.reportPath), 'screenshots'),\n writeInlineImage: async (id, base64) => {\n await appendFileAsync(\n this.reportPath,\n `\\n${generateImageScriptTag(id, base64)}`,\n );\n },\n alsoWriteFileCopy: this.shouldPersistExecutionDump,\n });\n if (options.reuseExistingReport) {\n this.hydrateStateFromExistingReport();\n }\n }\n\n static create(\n reportFileName: string,\n opts: {\n generateReport?: boolean;\n persistExecutionDump?: boolean;\n outputFormat?: 'single-html' | 'html-and-external-assets';\n autoPrintReportMsg?: boolean;\n reuseExistingReport?: boolean;\n },\n ): IReportGenerator {\n assertReportGenerationOptions(opts);\n validateReportFileName(reportFileName);\n if (opts.generateReport === false) return nullReportGenerator;\n\n // In browser environment, file system is not available\n if (ifInBrowser) return nullReportGenerator;\n\n const reportRootDir = getMidsceneRunSubDir('report');\n const outputDir = join(reportRootDir, reportFileName);\n const reportPath =\n opts.outputFormat === 'html-and-external-assets'\n ? join(outputDir, 'index.html')\n : join(reportRootDir, ensureHtmlFileName(reportFileName));\n return new ReportGenerator({\n reportPath,\n screenshotMode:\n opts.outputFormat === 'html-and-external-assets'\n ? 'directory'\n : 'inline',\n persistExecutionDump: opts.persistExecutionDump,\n autoPrint: opts.autoPrintReportMsg,\n reuseExistingReport: opts.reuseExistingReport,\n });\n }\n\n onExecutionUpdate(\n execution: ExecutionDump,\n reportMeta: ReportMeta,\n attributes?: ReportAttributes,\n ): void {\n this.lastExecution = execution;\n this.lastReportMeta = reportMeta;\n this.executionsByKey.set(this.getExecutionCommentKey(execution), execution);\n this.mergeReportAttributes(attributes);\n this.writeQueue = this.writeQueue.then(async () => {\n if (this.destroyed) return;\n await this.doWriteExecution(execution, reportMeta);\n });\n }\n\n async flush(): Promise<void> {\n await this.writeQueue;\n }\n\n async finalize(): Promise<string | undefined> {\n // Re-write the last execution to capture any final state changes\n if (this.lastExecution && this.lastReportMeta) {\n this.onExecutionUpdate(this.lastExecution, this.lastReportMeta);\n }\n await this.flush();\n this.destroyed = true;\n\n if (!this.initialized) {\n // No executions were ever written — no file exists\n return undefined;\n }\n\n await this.appendAgentReportComment();\n return this.reportPath;\n }\n\n getReportPath(): string | undefined {\n return this.reportPath;\n }\n\n private printReportPath(): void {\n if (!this.autoPrint || !this.reportPath) return;\n if (globalConfigManager.getEnvConfigInBoolean(MIDSCENE_REPORT_QUIET))\n return;\n\n if (this.screenshotMode === 'directory') {\n logMsg(\n `Midscene - report file updated: npx serve ${dirname(this.reportPath)}`,\n );\n } else {\n logMsg(`Midscene - report file updated: ${this.reportPath}`);\n }\n }\n\n private async doWriteExecution(\n execution: ExecutionDump,\n reportMeta: ReportMeta,\n ): Promise<void> {\n const singleDump = this.wrapAsReportDump(execution, reportMeta);\n\n if (this.screenshotMode === 'inline') {\n await this.writeInlineExecution(execution, singleDump);\n } else {\n await this.writeDirectoryExecution(execution, singleDump);\n }\n\n if (this.shouldPersistExecutionDump) {\n await this.persistExecutionDumpToFile(execution, singleDump);\n }\n\n if (!this.firstWriteDone) {\n this.firstWriteDone = true;\n this.printReportPath();\n }\n }\n\n private mergeReportAttributes(attributes?: ReportAttributes): void {\n if (!attributes) {\n return;\n }\n\n for (const [key, value] of Object.entries(attributes)) {\n if (value === undefined || value === null) {\n continue;\n }\n this.reportAttributes[key] = String(value);\n }\n }\n\n private hydrateStateFromExistingReport(): void {\n if (!existsSync(this.reportPath)) {\n return;\n }\n\n // Reuse existing report file and append new updates instead of rewriting.\n this.initialized = true;\n\n if (!this.shouldPersistExecutionDump) {\n return;\n }\n\n const reportDir = dirname(this.reportPath);\n const existingExecutionIndices = readdirSync(reportDir)\n .map((name) => /^(\\d+)\\.execution\\.json$/.exec(name)?.[1])\n .filter((index): index is string => Boolean(index))\n .map((index) => Number.parseInt(index, 10))\n .filter((index) => Number.isFinite(index));\n\n if (existingExecutionIndices.length > 0) {\n this.executionLogIndex = Math.max(...existingExecutionIndices);\n }\n }\n\n private getDumpScriptAttributes(): Record<string, string> {\n return {\n 'data-group-id': this.reportStreamId,\n // Self-describe how this report file stores screenshots so consumers\n // (merge/delete) never have to guess the mode from the filesystem.\n [DATA_SCREENSHOT_MODE_ATTR]: this.screenshotMode,\n ...this.reportAttributes,\n };\n }\n\n /**\n * Wrap an ExecutionDump + ReportMeta into a single-execution ReportActionDump.\n */\n private wrapAsReportDump(\n execution: ExecutionDump,\n reportMeta: ReportMeta,\n ): ReportActionDump {\n return new ReportActionDump({\n sdkVersion: reportMeta.sdkVersion,\n groupName: reportMeta.groupName,\n groupDescription: reportMeta.groupDescription,\n modelBriefs: reportMeta.modelBriefs,\n deviceType: reportMeta.deviceType,\n executions: [execution],\n });\n }\n\n /**\n * Append-only inline mode: write new screenshots and a dump tag on every call.\n * The frontend deduplicates executions with the same id/name (keeps last).\n * Duplicate dump JSON is acceptable; only screenshots are deduplicated.\n *\n * All writes go through `fs/promises` so they run on libuv's thread pool\n * rather than blocking the Node event loop. A long agent run previously\n * appended multi-MB dumps (screenshots + serialized tasks) per progress\n * tick on the main thread, starving IPC and UI for >10s per stall.\n */\n private async writeInlineExecution(\n execution: ExecutionDump,\n singleDump: ReportActionDump,\n ): Promise<void> {\n const dir = dirname(this.reportPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // Initialize: write HTML template once\n if (!this.initialized) {\n await writeFileAsync(this.reportPath, getReportTpl());\n this.initialized = true;\n }\n\n // Append new screenshots (skip already-written ones)\n for (const screenshot of execution.collectScreenshots()) {\n await this.screenshotStore.persist(screenshot);\n }\n\n // Append dump tag (always — frontend keeps only last per execution id)\n const serialized = singleDump.serialize();\n await appendFileAsync(\n this.reportPath,\n `\\n${generateDumpScriptTag(serialized, this.getDumpScriptAttributes())}`,\n );\n }\n\n private async writeDirectoryExecution(\n execution: ExecutionDump,\n singleDump: ReportActionDump,\n ): Promise<void> {\n const dir = dirname(this.reportPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n for (const screenshot of execution.collectScreenshots()) {\n await this.screenshotStore.persist(screenshot);\n }\n\n // 2. Append dump tag (always — frontend keeps only last per execution id)\n const serialized = singleDump.serialize();\n\n if (!this.initialized) {\n await writeFileAsync(\n this.reportPath,\n `${getReportTpl()}${getBaseUrlFixScript()}`,\n );\n this.initialized = true;\n }\n\n await appendFileAsync(\n this.reportPath,\n `\\n${generateDumpScriptTag(serialized, this.getDumpScriptAttributes())}`,\n );\n }\n\n private async appendAgentReportComment(): Promise<void> {\n if (\n this.agentCommentWritten ||\n !this.lastReportMeta ||\n this.executionsByKey.size === 0\n ) {\n return;\n }\n\n const reportDump = new ReportActionDump({\n sdkVersion: this.lastReportMeta.sdkVersion,\n groupName: this.lastReportMeta.groupName,\n groupDescription: this.lastReportMeta.groupDescription,\n modelBriefs: this.lastReportMeta.modelBriefs,\n deviceType: this.lastReportMeta.deviceType,\n executions: Array.from(this.executionsByKey.values()),\n });\n await appendFileAsync(\n this.reportPath,\n `\\n${generateAgentReportComment(reportDump)}`,\n );\n this.agentCommentWritten = true;\n }\n\n private getExecutionLogKey(execution: ExecutionDump): string {\n if (!execution.id) {\n throw new Error(\n 'ReportGenerator: execution.id is required for persisting execution dumps',\n );\n }\n return `id:${execution.id}`;\n }\n\n private getExecutionCommentKey(execution: ExecutionDump): string {\n if (execution.id) {\n return `id:${execution.id}`;\n }\n\n const existingKey = this.executionCommentKeyByObject.get(execution);\n if (existingKey) {\n return existingKey;\n }\n\n const key = `no-id:${this.executionCommentKeyIndex++}`;\n this.executionCommentKeyByObject.set(execution, key);\n return key;\n }\n\n private async persistExecutionDumpToFile(\n execution: ExecutionDump,\n singleDump: ReportActionDump,\n ): Promise<void> {\n const dir = dirname(this.reportPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n const executionLogKey = this.getExecutionLogKey(execution);\n let fileIndex =\n this.executionLogFileIndexByExecutionKey.get(executionLogKey);\n if (!fileIndex) {\n this.executionLogIndex += 1;\n fileIndex = this.executionLogIndex;\n this.executionLogFileIndexByExecutionKey.set(executionLogKey, fileIndex);\n }\n\n const fileName = `${fileIndex}.execution.json`;\n const filePath = join(dirname(this.reportPath), fileName);\n await writeFileAsync(filePath, singleDump.serialize(2), 'utf-8');\n }\n}\n\nfunction ensureHtmlFileName(reportFileName: string): string {\n return reportFileName.endsWith('.html')\n ? reportFileName\n : `${reportFileName}.html`;\n}\n\nfunction validateReportFileName(reportFileName: string): void {\n if (!reportFileName?.trim()) {\n throw new Error('reportFileName must be a non-empty string');\n }\n\n if (/[\\\\/]/.test(reportFileName)) {\n throw new Error(\n 'reportFileName must not contain path separators (`/` or `\\\\\\\\`)',\n );\n }\n\n if (/[:*?\"<>|]/.test(reportFileName)) {\n throw new Error(\n 'reportFileName contains illegal filename characters: : * ? \" < > |',\n );\n }\n}\n"],"names":["nullReportGenerator","undefined","assertReportGenerationOptions","opts","Error","ReportGenerator","reportFileName","validateReportFileName","ifInBrowser","reportRootDir","getMidsceneRunSubDir","outputDir","join","reportPath","ensureHtmlFileName","execution","reportMeta","attributes","globalConfigManager","MIDSCENE_REPORT_QUIET","logMsg","dirname","singleDump","key","value","Object","String","existsSync","reportDir","existingExecutionIndices","readdirSync","name","index","Boolean","Number","Math","DATA_SCREENSHOT_MODE_ATTR","ReportActionDump","dir","mkdirSync","writeFileAsync","getReportTpl","screenshot","serialized","appendFileAsync","generateDumpScriptTag","getBaseUrlFixScript","reportDump","Array","generateAgentReportComment","existingKey","executionLogKey","fileIndex","fileName","filePath","options","Map","WeakMap","Promise","uuid","ScreenshotStore","id","base64","generateImageScriptTag"],"mappings":"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"}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { basename } from "node:path";
|
|
2
1
|
import { extractInsightParam, paramStr, typeStr } from "./agent/ui-utils.mjs";
|
|
3
2
|
import { ScreenshotItem } from "./screenshot-item.mjs";
|
|
4
3
|
import { normalizeScreenshotRef } from "./dump/screenshot-store.mjs";
|
|
4
|
+
const screenshotDataUrlPattern = /^data:image\/(png|jpeg|jpg);base64,([\s\S]*)$/i;
|
|
5
|
+
const rawBase64BodyPattern = /^[a-zA-Z0-9+/=\s]+$/;
|
|
6
|
+
const jsonContextMaxStringLength = 12000;
|
|
5
7
|
function toExecutionDump(execution) {
|
|
6
8
|
if (!execution || 'object' != typeof execution) throw new Error('executionToMarkdown: execution is required');
|
|
7
9
|
if (!Array.isArray(execution.tasks)) throw new Error('executionToMarkdown: execution.tasks must be an array');
|
|
@@ -17,6 +19,165 @@ function formatTime(ts) {
|
|
|
17
19
|
if ('number' != typeof ts || Number.isNaN(ts)) return 'N/A';
|
|
18
20
|
return new Date(ts).toISOString();
|
|
19
21
|
}
|
|
22
|
+
function formatValue(value) {
|
|
23
|
+
if (null == value || '' === value) return 'N/A';
|
|
24
|
+
if ('number' == typeof value) return Number.isFinite(value) ? String(value) : 'N/A';
|
|
25
|
+
return String(value);
|
|
26
|
+
}
|
|
27
|
+
function escapeTableCell(value) {
|
|
28
|
+
return formatValue(value).replace(/\|/g, '\\|').replace(/\n/g, '<br>');
|
|
29
|
+
}
|
|
30
|
+
function markdownTable(headers, rows) {
|
|
31
|
+
return [
|
|
32
|
+
`| ${headers.map(escapeTableCell).join(' | ')} |`,
|
|
33
|
+
`| ${headers.map(()=>'---').join(' | ')} |`,
|
|
34
|
+
...rows.map((row)=>`| ${row.map(escapeTableCell).join(' | ')} |`)
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
function modelBriefRows(modelBriefs) {
|
|
38
|
+
return modelBriefs.map((brief)=>[
|
|
39
|
+
brief.intent || 'default',
|
|
40
|
+
brief.name || 'N/A',
|
|
41
|
+
brief.modelDescription || 'N/A'
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
function modelRowsFromUsage(report) {
|
|
45
|
+
const rows = new Map();
|
|
46
|
+
const addUsage = (source, usage)=>{
|
|
47
|
+
if (!hasUsage(usage)) return;
|
|
48
|
+
const intent = usage.intent || source;
|
|
49
|
+
const model = usage.model_name || usage.response_model_name;
|
|
50
|
+
const description = usage.model_description;
|
|
51
|
+
const key = `${intent}\0${model || ''}\0${description || ''}`;
|
|
52
|
+
if (rows.has(key)) return;
|
|
53
|
+
rows.set(key, [
|
|
54
|
+
intent,
|
|
55
|
+
model || 'N/A',
|
|
56
|
+
description || 'N/A'
|
|
57
|
+
]);
|
|
58
|
+
};
|
|
59
|
+
for (const execution of report.executions)for (const task of execution.tasks){
|
|
60
|
+
const taskWithUsage = task;
|
|
61
|
+
addUsage('main', taskWithUsage.usage);
|
|
62
|
+
addUsage('searchArea', taskWithUsage.searchAreaUsage);
|
|
63
|
+
}
|
|
64
|
+
return Array.from(rows.values());
|
|
65
|
+
}
|
|
66
|
+
function usageValue(usage, key) {
|
|
67
|
+
const value = usage?.[key];
|
|
68
|
+
return 'number' == typeof value && Number.isFinite(value) ? value : 0;
|
|
69
|
+
}
|
|
70
|
+
function hasUsage(usage) {
|
|
71
|
+
if (!usage) return false;
|
|
72
|
+
return [
|
|
73
|
+
'intent',
|
|
74
|
+
'model_name',
|
|
75
|
+
"model_description",
|
|
76
|
+
'prompt_tokens',
|
|
77
|
+
'cached_input',
|
|
78
|
+
'completion_tokens',
|
|
79
|
+
'total_tokens',
|
|
80
|
+
'time_cost',
|
|
81
|
+
'request_id'
|
|
82
|
+
].some((key)=>void 0 !== usage[key] && null !== usage[key]);
|
|
83
|
+
}
|
|
84
|
+
function usageTotalTokens(usage) {
|
|
85
|
+
const explicitTotal = usageValue(usage, 'total_tokens');
|
|
86
|
+
if (explicitTotal > 0) return explicitTotal;
|
|
87
|
+
return usageValue(usage, 'prompt_tokens') + usageValue(usage, 'completion_tokens');
|
|
88
|
+
}
|
|
89
|
+
function usageModelName(usage) {
|
|
90
|
+
return usage.model_name || usage.response_model_name || usage.model_description || usage.intent || 'Unknown';
|
|
91
|
+
}
|
|
92
|
+
function usageRowsForTask(task) {
|
|
93
|
+
const taskWithUsage = task;
|
|
94
|
+
const rows = [];
|
|
95
|
+
const appendUsage = (source, usage)=>{
|
|
96
|
+
if (!hasUsage(usage)) return;
|
|
97
|
+
rows.push([
|
|
98
|
+
source,
|
|
99
|
+
usage.intent,
|
|
100
|
+
usage.model_name || usage.response_model_name,
|
|
101
|
+
usage.model_description,
|
|
102
|
+
usageValue(usage, 'prompt_tokens'),
|
|
103
|
+
usageValue(usage, 'cached_input'),
|
|
104
|
+
usageValue(usage, 'completion_tokens'),
|
|
105
|
+
usageTotalTokens(usage),
|
|
106
|
+
usageValue(usage, 'time_cost'),
|
|
107
|
+
usage.request_id
|
|
108
|
+
]);
|
|
109
|
+
};
|
|
110
|
+
appendUsage('main', taskWithUsage.usage);
|
|
111
|
+
appendUsage('searchArea', taskWithUsage.searchAreaUsage);
|
|
112
|
+
return rows;
|
|
113
|
+
}
|
|
114
|
+
function collectUsageTotals(report) {
|
|
115
|
+
const totalsByModel = new Map();
|
|
116
|
+
const addUsage = (usage)=>{
|
|
117
|
+
if (!hasUsage(usage)) return;
|
|
118
|
+
const modelName = usageModelName(usage);
|
|
119
|
+
const current = totalsByModel.get(modelName) ?? {
|
|
120
|
+
calls: 0,
|
|
121
|
+
prompt: 0,
|
|
122
|
+
cachedInput: 0,
|
|
123
|
+
completion: 0,
|
|
124
|
+
total: 0,
|
|
125
|
+
timeCost: 0
|
|
126
|
+
};
|
|
127
|
+
totalsByModel.set(modelName, {
|
|
128
|
+
calls: current.calls + 1,
|
|
129
|
+
prompt: current.prompt + usageValue(usage, 'prompt_tokens'),
|
|
130
|
+
cachedInput: current.cachedInput + usageValue(usage, 'cached_input'),
|
|
131
|
+
completion: current.completion + usageValue(usage, 'completion_tokens'),
|
|
132
|
+
total: current.total + usageTotalTokens(usage),
|
|
133
|
+
timeCost: current.timeCost + usageValue(usage, 'time_cost')
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
for (const execution of report.executions)for (const task of execution.tasks){
|
|
137
|
+
const taskWithUsage = task;
|
|
138
|
+
addUsage(taskWithUsage.usage);
|
|
139
|
+
addUsage(taskWithUsage.searchAreaUsage);
|
|
140
|
+
}
|
|
141
|
+
return totalsByModel;
|
|
142
|
+
}
|
|
143
|
+
function sanitizeJsonForMarkdown(value, seen = new WeakSet()) {
|
|
144
|
+
if ('string' == typeof value) {
|
|
145
|
+
if (screenshotDataUrlPattern.test(value) || value.length > 1000000 || /^[a-zA-Z0-9+/=\s]{100000,}$/.test(value)) return `[omitted image/base64 string, length=${value.length}]`;
|
|
146
|
+
if (value.length > jsonContextMaxStringLength) return `${value.slice(0, jsonContextMaxStringLength)}\n[truncated, original length=${value.length}]`;
|
|
147
|
+
return value;
|
|
148
|
+
}
|
|
149
|
+
if (null === value || 'number' == typeof value || 'boolean' == typeof value) return value;
|
|
150
|
+
if ('function' == typeof value) return '[omitted function]';
|
|
151
|
+
if ('object' != typeof value || void 0 === value) return;
|
|
152
|
+
if (seen.has(value)) return '[omitted circular reference]';
|
|
153
|
+
seen.add(value);
|
|
154
|
+
if (Array.isArray(value)) return value.map((item)=>sanitizeJsonForMarkdown(item, seen));
|
|
155
|
+
const input = value;
|
|
156
|
+
const output = {};
|
|
157
|
+
for (const [key, entry] of Object.entries(input))if (void 0 !== entry && 'function' != typeof entry) {
|
|
158
|
+
if ('string' == typeof entry && /(^|_|-)(base64|screenshotBase64|imageBase64)$/i.test(key)) {
|
|
159
|
+
output[key] = `[omitted base64 string, length=${entry.length}]`;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
output[key] = sanitizeJsonForMarkdown(entry, seen);
|
|
163
|
+
}
|
|
164
|
+
seen.delete(value);
|
|
165
|
+
return output;
|
|
166
|
+
}
|
|
167
|
+
function appendJsonSection(lines, title, value) {
|
|
168
|
+
if (null == value || '' === value) return;
|
|
169
|
+
const sanitized = sanitizeJsonForMarkdown(value);
|
|
170
|
+
if (void 0 === sanitized) return;
|
|
171
|
+
lines.push('', `### ${title}`, '', '```json');
|
|
172
|
+
lines.push(JSON.stringify(sanitized, null, 2));
|
|
173
|
+
lines.push('```');
|
|
174
|
+
}
|
|
175
|
+
function appendTextSection(lines, title, value) {
|
|
176
|
+
if ('string' != typeof value || !value.trim()) return;
|
|
177
|
+
lines.push('', `### ${title}`, '', '```text');
|
|
178
|
+
lines.push(value.replace(/```/g, '` ` `'));
|
|
179
|
+
lines.push('```');
|
|
180
|
+
}
|
|
20
181
|
function resolveTaskTiming(task) {
|
|
21
182
|
const timing = task.timing;
|
|
22
183
|
if (!timing) return {};
|
|
@@ -57,16 +218,35 @@ function extractLocateCenter(task) {
|
|
|
57
218
|
];
|
|
58
219
|
}
|
|
59
220
|
function tryExtractBase64(screenshot) {
|
|
221
|
+
if ('string' == typeof screenshot) {
|
|
222
|
+
const trimmedScreenshot = screenshot.trim();
|
|
223
|
+
const dataUrlMatch = trimmedScreenshot.match(screenshotDataUrlPattern);
|
|
224
|
+
if (dataUrlMatch) {
|
|
225
|
+
const format = 'jpg' === dataUrlMatch[1].toLowerCase() ? 'jpeg' : 'png';
|
|
226
|
+
const base64Body = dataUrlMatch[2].replace(/\s/g, '');
|
|
227
|
+
if (!base64Body) return;
|
|
228
|
+
return `data:image/${format};base64,${base64Body}`;
|
|
229
|
+
}
|
|
230
|
+
if (trimmedScreenshot.startsWith('data:') || !rawBase64BodyPattern.test(trimmedScreenshot)) return;
|
|
231
|
+
const base64Body = trimmedScreenshot.replace(/\s/g, '');
|
|
232
|
+
return base64Body ? `data:image/png;base64,${base64Body}` : void 0;
|
|
233
|
+
}
|
|
60
234
|
if (!screenshot || 'object' != typeof screenshot) return;
|
|
61
235
|
const s = screenshot;
|
|
62
236
|
if ('string' == typeof s.base64 && s.base64.length > 0) return s.base64;
|
|
63
237
|
}
|
|
64
|
-
function
|
|
238
|
+
function restoredSourceRef(screenshot) {
|
|
239
|
+
if (!screenshot || 'object' != typeof screenshot) return;
|
|
240
|
+
const sourceRef = screenshot.sourceRef;
|
|
241
|
+
return normalizeScreenshotRef(sourceRef) ?? void 0;
|
|
242
|
+
}
|
|
243
|
+
function screenshotAttachment(screenshot, screenshotBaseDir, executionIndex, taskIndex, options) {
|
|
244
|
+
const markdownLabel = options?.label || `task-${taskIndex + 1}`;
|
|
65
245
|
if (screenshot instanceof ScreenshotItem) {
|
|
66
246
|
const ext = screenshot.extension;
|
|
67
247
|
const suggestedFileName = `execution-${executionIndex + 1}-task-${taskIndex + 1}-${screenshot.id}.${ext}`;
|
|
68
248
|
return {
|
|
69
|
-
markdown: `\n`,
|
|
70
250
|
attachment: {
|
|
71
251
|
id: screenshot.id,
|
|
72
252
|
suggestedFileName,
|
|
@@ -82,7 +262,7 @@ function screenshotAttachment(screenshot, screenshotBaseDir, executionIndex, tas
|
|
|
82
262
|
const ext = 'image/jpeg' === ref.mimeType ? 'jpeg' : 'png';
|
|
83
263
|
const suggestedFileName = `execution-${executionIndex + 1}-task-${taskIndex + 1}-${ref.id}.${ext}`;
|
|
84
264
|
return {
|
|
85
|
-
markdown: `\n`,
|
|
86
266
|
attachment: {
|
|
87
267
|
id: ref.id,
|
|
88
268
|
suggestedFileName,
|
|
@@ -94,13 +274,31 @@ function screenshotAttachment(screenshot, screenshotBaseDir, executionIndex, tas
|
|
|
94
274
|
}
|
|
95
275
|
};
|
|
96
276
|
}
|
|
277
|
+
const sourceRef = restoredSourceRef(screenshot);
|
|
278
|
+
if (sourceRef) {
|
|
279
|
+
const ext = 'image/jpeg' === sourceRef.mimeType ? 'jpeg' : 'png';
|
|
280
|
+
const suggestedFileName = `execution-${executionIndex + 1}-task-${taskIndex + 1}-${sourceRef.id}.${ext}`;
|
|
281
|
+
return {
|
|
282
|
+
markdown: `\n`,
|
|
283
|
+
attachment: {
|
|
284
|
+
id: sourceRef.id,
|
|
285
|
+
suggestedFileName,
|
|
286
|
+
sourceRef,
|
|
287
|
+
mimeType: sourceRef.mimeType,
|
|
288
|
+
executionIndex,
|
|
289
|
+
taskIndex,
|
|
290
|
+
base64Data: tryExtractBase64(screenshot)
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
}
|
|
97
294
|
const base64 = tryExtractBase64(screenshot);
|
|
98
295
|
if (base64) {
|
|
99
296
|
const ext = base64.startsWith('data:image/jpeg') ? 'jpeg' : 'png';
|
|
100
|
-
const
|
|
297
|
+
const idSuffix = options?.fallbackIdSuffix ? `-${options.fallbackIdSuffix}` : '';
|
|
298
|
+
const id = `restored-${executionIndex + 1}-${taskIndex + 1}${idSuffix}`;
|
|
101
299
|
const suggestedFileName = `execution-${executionIndex + 1}-task-${taskIndex + 1}-${id}.${ext}`;
|
|
102
300
|
return {
|
|
103
|
-
markdown: `\n`,
|
|
104
302
|
attachment: {
|
|
105
303
|
id,
|
|
106
304
|
suggestedFileName,
|
|
@@ -113,27 +311,46 @@ function screenshotAttachment(screenshot, screenshotBaseDir, executionIndex, tas
|
|
|
113
311
|
}
|
|
114
312
|
throw new Error(`executionToMarkdown: missing screenshot for execution #${executionIndex + 1} task #${taskIndex + 1}`);
|
|
115
313
|
}
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
};
|
|
314
|
+
function imageLabelSuffix(value) {
|
|
315
|
+
return value.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'screenshot';
|
|
316
|
+
}
|
|
317
|
+
function screenshotsMarkdownSection(task, screenshotBaseDir, executionIndex, taskIndex) {
|
|
121
318
|
const lines = [
|
|
122
319
|
'',
|
|
123
|
-
'###
|
|
320
|
+
'### Screenshots'
|
|
124
321
|
];
|
|
125
322
|
const attachments = [];
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
323
|
+
let screenshotIndex = 0;
|
|
324
|
+
if (task.uiContext?.screenshot) {
|
|
325
|
+
const imageResult = screenshotAttachment(task.uiContext.screenshot, screenshotBaseDir, executionIndex, taskIndex, {
|
|
326
|
+
fallbackIdSuffix: 'ui-context',
|
|
327
|
+
label: `task-${taskIndex + 1}-ui-context`
|
|
328
|
+
});
|
|
329
|
+
const time = resolveTaskTiming(task);
|
|
330
|
+
screenshotIndex += 1;
|
|
331
|
+
lines.push(`- #${screenshotIndex} type=ui-context, ts=${formatTime(time.start)}, timing=ui-context`);
|
|
332
|
+
lines.push(imageResult.markdown);
|
|
333
|
+
attachments.push(imageResult.attachment);
|
|
334
|
+
}
|
|
335
|
+
task.recorder?.forEach((item, recorderIndex)=>{
|
|
129
336
|
if (!item.screenshot) return;
|
|
130
|
-
const
|
|
337
|
+
const descriptionText = item.description ? `, description=${item.description}` : '';
|
|
338
|
+
const timing = item.timing || 'N/A';
|
|
339
|
+
screenshotIndex += 1;
|
|
340
|
+
lines.push(`- #${screenshotIndex} type=${item.type}, ts=${formatTime(item.ts)}, timing=${timing}${descriptionText}`);
|
|
341
|
+
const imageResult = screenshotAttachment(item.screenshot, screenshotBaseDir, executionIndex, taskIndex, {
|
|
342
|
+
fallbackIdSuffix: `recorder-${recorderIndex + 1}`,
|
|
343
|
+
label: `task-${taskIndex + 1}-${imageLabelSuffix(timing)}`
|
|
344
|
+
});
|
|
131
345
|
lines.push(imageResult.markdown);
|
|
132
346
|
attachments.push(imageResult.attachment);
|
|
133
347
|
});
|
|
134
|
-
return {
|
|
348
|
+
return screenshotIndex > 0 ? {
|
|
135
349
|
lines,
|
|
136
350
|
attachments
|
|
351
|
+
} : {
|
|
352
|
+
lines: [],
|
|
353
|
+
attachments
|
|
137
354
|
};
|
|
138
355
|
}
|
|
139
356
|
function renderExecution(executionRaw, executionIndex, options) {
|
|
@@ -145,11 +362,15 @@ function renderExecution(executionRaw, executionIndex, options) {
|
|
|
145
362
|
if (execution.description) lines.push('', execution.description);
|
|
146
363
|
lines.push('', `- Execution start: ${formatTime(execution.logTime)}`);
|
|
147
364
|
lines.push(`- Task count: ${execution.tasks.length}`);
|
|
365
|
+
if (execution.aiActContext) appendTextSection(lines, 'AI Action Context', execution.aiActContext);
|
|
148
366
|
execution.tasks.forEach((task, taskIndex)=>{
|
|
149
367
|
const title = typeStr(task);
|
|
150
368
|
const detail = safeTaskParam(task);
|
|
151
369
|
const time = resolveTaskTiming(task);
|
|
152
370
|
lines.push('', `## ${taskIndex + 1}. ${title}${detail ? ` - ${detail}` : ''}`);
|
|
371
|
+
lines.push(`- Task ID: ${task.taskId || 'N/A'}`);
|
|
372
|
+
lines.push(`- Type: ${task.type || 'N/A'}`);
|
|
373
|
+
lines.push(`- SubType: ${task.subType || 'N/A'}`);
|
|
153
374
|
lines.push(`- Status: ${task.status || 'unknown'}`);
|
|
154
375
|
lines.push(`- Start: ${formatTime(time.start)}`);
|
|
155
376
|
lines.push(`- End: ${formatTime(time.end)}`);
|
|
@@ -160,15 +381,31 @@ function renderExecution(executionRaw, executionIndex, options) {
|
|
|
160
381
|
if (locateCenter) lines.push(`- Locate center: (${locateCenter[0]}, ${locateCenter[1]})`);
|
|
161
382
|
}
|
|
162
383
|
if (task.errorMessage) lines.push(`- Error: ${task.errorMessage}`);
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
lines.push(
|
|
166
|
-
|
|
384
|
+
const usageRows = usageRowsForTask(task);
|
|
385
|
+
if (usageRows.length) {
|
|
386
|
+
lines.push('', '### Model Usage');
|
|
387
|
+
lines.push(...markdownTable([
|
|
388
|
+
'Source',
|
|
389
|
+
'Intent',
|
|
390
|
+
'Model',
|
|
391
|
+
"Description",
|
|
392
|
+
'Prompt Tokens',
|
|
393
|
+
'Cached Input',
|
|
394
|
+
'Completion Tokens',
|
|
395
|
+
'Total Tokens',
|
|
396
|
+
'Time Cost(ms)',
|
|
397
|
+
'Request ID'
|
|
398
|
+
], usageRows));
|
|
167
399
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
400
|
+
appendJsonSection(lines, 'Param', task.param);
|
|
401
|
+
appendJsonSection(lines, 'Output', task.output);
|
|
402
|
+
appendJsonSection(lines, 'Log', task.log);
|
|
403
|
+
appendTextSection(lines, 'Thought', task.thought);
|
|
404
|
+
appendTextSection(lines, 'Reasoning Content', task.reasoning_content);
|
|
405
|
+
const screenshotsSection = screenshotsMarkdownSection(task, screenshotBaseDir, executionIndex, taskIndex);
|
|
406
|
+
if (screenshotsSection.lines.length) {
|
|
407
|
+
lines.push(...screenshotsSection.lines);
|
|
408
|
+
attachments.push(...screenshotsSection.attachments);
|
|
172
409
|
}
|
|
173
410
|
});
|
|
174
411
|
return {
|
|
@@ -176,10 +413,6 @@ function renderExecution(executionRaw, executionIndex, options) {
|
|
|
176
413
|
attachments
|
|
177
414
|
};
|
|
178
415
|
}
|
|
179
|
-
function reportFileName(execution, executionIndex) {
|
|
180
|
-
const safeName = execution.name.trim().replace(/\s+/g, '-').replace(/[^a-zA-Z0-9-_]/g, '') || `execution-${executionIndex + 1}`;
|
|
181
|
-
return `${executionIndex + 1}-${basename(safeName)}.md`;
|
|
182
|
-
}
|
|
183
416
|
function executionToMarkdown(execution, options) {
|
|
184
417
|
return renderExecution(execution, 0, options);
|
|
185
418
|
}
|
|
@@ -188,21 +421,53 @@ function reportToMarkdown(report) {
|
|
|
188
421
|
const executionResults = reportDump.executions.map((execution, index)=>{
|
|
189
422
|
const rendered = renderExecution(execution, index);
|
|
190
423
|
return {
|
|
191
|
-
executionIndex: index,
|
|
192
|
-
executionName: execution.name,
|
|
193
424
|
markdown: rendered.markdown,
|
|
194
|
-
attachments: rendered.attachments
|
|
195
|
-
suggestedFileName: reportFileName(execution, index)
|
|
425
|
+
attachments: rendered.attachments
|
|
196
426
|
};
|
|
197
427
|
});
|
|
198
428
|
const attachments = executionResults.flatMap((item)=>item.attachments);
|
|
429
|
+
const usageTotals = collectUsageTotals(reportDump);
|
|
430
|
+
const modelRows = reportDump.modelBriefs?.length ? modelBriefRows(reportDump.modelBriefs) : modelRowsFromUsage(reportDump);
|
|
431
|
+
const modelInfoLines = [
|
|
432
|
+
'\n## Model Info',
|
|
433
|
+
...modelRows.length ? markdownTable([
|
|
434
|
+
'Intent',
|
|
435
|
+
'Model',
|
|
436
|
+
"Description"
|
|
437
|
+
], modelRows) : [
|
|
438
|
+
'- No model metadata recorded.'
|
|
439
|
+
]
|
|
440
|
+
];
|
|
441
|
+
const tokenSummaryLines = [
|
|
442
|
+
'\n## Token Usage Summary',
|
|
443
|
+
...usageTotals.size ? markdownTable([
|
|
444
|
+
'Model',
|
|
445
|
+
'Calls',
|
|
446
|
+
'Prompt Tokens',
|
|
447
|
+
'Cached Input',
|
|
448
|
+
'Completion Tokens',
|
|
449
|
+
'Total Tokens',
|
|
450
|
+
'Time Cost(ms)'
|
|
451
|
+
], Array.from(usageTotals.entries()).map(([modelName, totals])=>[
|
|
452
|
+
modelName,
|
|
453
|
+
totals.calls,
|
|
454
|
+
totals.prompt,
|
|
455
|
+
totals.cachedInput,
|
|
456
|
+
totals.completion,
|
|
457
|
+
totals.total,
|
|
458
|
+
totals.timeCost
|
|
459
|
+
])) : [
|
|
460
|
+
'- No token usage recorded.'
|
|
461
|
+
]
|
|
462
|
+
];
|
|
199
463
|
const header = [
|
|
200
464
|
`# ${reportDump.groupName}`,
|
|
201
465
|
reportDump.groupDescription ? `\n${reportDump.groupDescription}` : '',
|
|
202
466
|
`\n- SDK Version: ${reportDump.sdkVersion}`,
|
|
467
|
+
reportDump.deviceType ? `- Device Type: ${reportDump.deviceType}` : '',
|
|
203
468
|
`- Execution count: ${reportDump.executions.length}`,
|
|
204
|
-
|
|
205
|
-
...
|
|
469
|
+
...modelInfoLines,
|
|
470
|
+
...tokenSummaryLines
|
|
206
471
|
].filter(Boolean).join('\n');
|
|
207
472
|
return {
|
|
208
473
|
markdown: `${header}\n\n${executionResults.map((item)=>item.markdown).join('\n\n---\n\n')}`,
|