@midscene/core 1.4.5 → 1.4.7
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 +4 -56
- package/dist/es/agent/agent.mjs.map +1 -1
- package/dist/es/agent/task-builder.mjs +4 -1
- package/dist/es/agent/task-builder.mjs.map +1 -1
- package/dist/es/agent/utils.mjs +91 -18
- package/dist/es/agent/utils.mjs.map +1 -1
- package/dist/es/ai-model/auto-glm/planning.mjs +1 -1
- package/dist/es/ai-model/auto-glm/planning.mjs.map +1 -1
- package/dist/es/ai-model/inspect.mjs +5 -5
- package/dist/es/ai-model/inspect.mjs.map +1 -1
- package/dist/es/ai-model/llm-planning.mjs +3 -3
- package/dist/es/ai-model/llm-planning.mjs.map +1 -1
- package/dist/es/ai-model/service-caller/index.mjs +73 -47
- package/dist/es/ai-model/service-caller/index.mjs.map +1 -1
- package/dist/es/ai-model/ui-tars-planning.mjs +14 -14
- package/dist/es/ai-model/ui-tars-planning.mjs.map +1 -1
- package/dist/es/common.mjs +2 -15
- package/dist/es/common.mjs.map +1 -1
- package/dist/es/device/index.mjs.map +1 -1
- package/dist/es/dump/html-utils.mjs +2 -1
- package/dist/es/dump/html-utils.mjs.map +1 -1
- package/dist/es/index.mjs.map +1 -1
- package/dist/es/report-generator.mjs +2 -2
- package/dist/es/report-generator.mjs.map +1 -1
- package/dist/es/report.mjs +39 -7
- package/dist/es/report.mjs.map +1 -1
- package/dist/es/service/index.mjs +6 -6
- package/dist/es/service/index.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 +4 -56
- package/dist/lib/agent/agent.js.map +1 -1
- package/dist/lib/agent/task-builder.js +4 -1
- package/dist/lib/agent/task-builder.js.map +1 -1
- package/dist/lib/agent/utils.js +96 -14
- package/dist/lib/agent/utils.js.map +1 -1
- package/dist/lib/ai-model/auto-glm/planning.js +1 -1
- package/dist/lib/ai-model/auto-glm/planning.js.map +1 -1
- package/dist/lib/ai-model/inspect.js +5 -5
- package/dist/lib/ai-model/inspect.js.map +1 -1
- package/dist/lib/ai-model/llm-planning.js +3 -3
- package/dist/lib/ai-model/llm-planning.js.map +1 -1
- package/dist/lib/ai-model/service-caller/index.js +75 -49
- package/dist/lib/ai-model/service-caller/index.js.map +1 -1
- package/dist/lib/ai-model/ui-tars-planning.js +14 -14
- package/dist/lib/ai-model/ui-tars-planning.js.map +1 -1
- package/dist/lib/common.js +2 -15
- package/dist/lib/common.js.map +1 -1
- package/dist/lib/device/index.js.map +1 -1
- package/dist/lib/dump/html-utils.js +4 -0
- package/dist/lib/dump/html-utils.js.map +1 -1
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/report-generator.js +1 -1
- package/dist/lib/report-generator.js.map +1 -1
- package/dist/lib/report.js +36 -4
- package/dist/lib/report.js.map +1 -1
- package/dist/lib/service/index.js +6 -6
- package/dist/lib/service/index.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 +1 -15
- package/dist/types/agent/utils.d.ts +13 -1
- package/dist/types/ai-model/index.d.ts +1 -1
- package/dist/types/ai-model/service-caller/index.d.ts +4 -2
- package/dist/types/common.d.ts +0 -310
- package/dist/types/device/index.d.ts +1 -2
- package/dist/types/dump/html-utils.d.ts +11 -0
- package/dist/types/index.d.ts +1 -1
- package/dist/types/report.d.ts +5 -0
- package/dist/types/types.d.ts +2 -2
- package/package.json +2 -2
package/dist/es/common.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"common.mjs","sources":["../../src/common.ts"],"sourcesContent":["import type {\n BaseElement,\n DeviceAction,\n ElementTreeNode,\n MidsceneYamlFlowItem,\n PlanningAction,\n Rect,\n Size,\n} from '@/types';\nimport { assert, isPlainObject } from '@midscene/shared/utils';\n\nimport type { ChatCompletionMessageParam } from 'openai/resources/index';\n\nimport { isUITars } from '@/ai-model/auto-glm/util';\nimport type { PlanningLocateParam } from '@/types';\nimport { NodeType } from '@midscene/shared/constants';\nimport type { TModelFamily } from '@midscene/shared/env';\nimport { treeToList } from '@midscene/shared/extractor';\nimport { compositeElementInfoImg } from '@midscene/shared/img';\nimport { getDebug } from '@midscene/shared/logger';\nimport { z } from 'zod';\n\nexport type AIArgs = ChatCompletionMessageParam[];\n\nconst defaultBboxSize = 20; // must be even number\nconst debugInspectUtils = getDebug('ai:common');\ntype AdaptBboxInput = number[] | string[] | string | (number[] | string[])[];\n\n/**\n * Convert a point coordinate [0, 1000] to a small bbox [0, 1000]\n * Creates a small bbox around the center point in the same coordinate space\n *\n * @param x - X coordinate in [0, 1000] range\n * @param y - Y coordinate in [0, 1000] range\n * @param bboxSize - Size of the bbox to create (default: 20)\n * @returns [x1, y1, x2, y2] bbox in [0, 1000] coordinate space\n */\nexport function pointToBbox(\n x: number,\n y: number,\n bboxSize = defaultBboxSize,\n): [number, number, number, number] {\n const halfSize = bboxSize / 2;\n const x1 = Math.max(x - halfSize, 0);\n const y1 = Math.max(y - halfSize, 0);\n const x2 = Math.min(x + halfSize, 1000);\n const y2 = Math.min(y + halfSize, 1000);\n\n return [x1, y1, x2, y2];\n}\n\n// transform the param of locate from qwen mode\nexport function fillBboxParam(\n locate: PlanningLocateParam,\n width: number,\n height: number,\n modelFamily: TModelFamily | undefined,\n) {\n // The Qwen model might have hallucinations of naming bbox as bbox_2d.\n if ((locate as any).bbox_2d && !locate?.bbox) {\n locate.bbox = (locate as any).bbox_2d;\n // biome-ignore lint/performance/noDelete: <explanation>\n delete (locate as any).bbox_2d;\n }\n\n if (locate?.bbox) {\n locate.bbox = adaptBbox(locate.bbox, width, height, modelFamily);\n }\n\n return locate;\n}\n\nexport function adaptQwen2_5Bbox(\n bbox: number[],\n): [number, number, number, number] {\n if (bbox.length < 2) {\n const msg = `invalid bbox data for qwen-vl mode: ${JSON.stringify(bbox)} `;\n throw new Error(msg);\n }\n\n const result: [number, number, number, number] = [\n Math.round(bbox[0]),\n Math.round(bbox[1]),\n typeof bbox[2] === 'number'\n ? Math.round(bbox[2])\n : Math.round(bbox[0] + defaultBboxSize),\n typeof bbox[3] === 'number'\n ? Math.round(bbox[3])\n : Math.round(bbox[1] + defaultBboxSize),\n ];\n return result;\n}\n\nexport function adaptDoubaoBbox(\n bbox: string[] | number[] | string,\n width: number,\n height: number,\n): [number, number, number, number] {\n assert(\n width > 0 && height > 0,\n 'width and height must be greater than 0 in doubao mode',\n );\n\n if (typeof bbox === 'string') {\n assert(\n /^(\\d+)\\s(\\d+)\\s(\\d+)\\s(\\d+)$/.test(bbox.trim()),\n `invalid bbox data string for doubao-vision mode: ${bbox}`,\n );\n const splitted = bbox.split(' ');\n if (splitted.length === 4) {\n return [\n Math.round((Number(splitted[0]) * width) / 1000),\n Math.round((Number(splitted[1]) * height) / 1000),\n Math.round((Number(splitted[2]) * width) / 1000),\n Math.round((Number(splitted[3]) * height) / 1000),\n ];\n }\n throw new Error(`invalid bbox data string for doubao-vision mode: ${bbox}`);\n }\n\n let bboxList: number[] = [];\n if (Array.isArray(bbox) && typeof bbox[0] === 'string') {\n bbox.forEach((item) => {\n if (typeof item === 'string' && item.includes(',')) {\n const [x, y] = item.split(',');\n bboxList.push(Number(x.trim()), Number(y.trim()));\n } else if (typeof item === 'string' && item.includes(' ')) {\n const [x, y] = item.split(' ');\n bboxList.push(Number(x.trim()), Number(y.trim()));\n } else {\n bboxList.push(Number(item));\n }\n });\n } else {\n bboxList = bbox as any;\n }\n\n if (bboxList.length === 4 || bboxList.length === 5) {\n return [\n Math.round((bboxList[0] * width) / 1000),\n Math.round((bboxList[1] * height) / 1000),\n Math.round((bboxList[2] * width) / 1000),\n Math.round((bboxList[3] * height) / 1000),\n ];\n }\n\n // treat the bbox as a center point\n if (\n bboxList.length === 6 ||\n bboxList.length === 2 ||\n bboxList.length === 3 ||\n bboxList.length === 7\n ) {\n return [\n Math.max(\n 0,\n Math.round((bboxList[0] * width) / 1000) - defaultBboxSize / 2,\n ),\n Math.max(\n 0,\n Math.round((bboxList[1] * height) / 1000) - defaultBboxSize / 2,\n ),\n Math.min(\n width,\n Math.round((bboxList[0] * width) / 1000) + defaultBboxSize / 2,\n ),\n Math.min(\n height,\n Math.round((bboxList[1] * height) / 1000) + defaultBboxSize / 2,\n ),\n ];\n }\n\n if (bbox.length === 8) {\n return [\n Math.round((bboxList[0] * width) / 1000),\n Math.round((bboxList[1] * height) / 1000),\n Math.round((bboxList[4] * width) / 1000),\n Math.round((bboxList[5] * height) / 1000),\n ];\n }\n\n const msg = `invalid bbox data for doubao-vision mode: ${JSON.stringify(bbox)} `;\n throw new Error(msg);\n}\n\nfunction normalizeBboxInput(\n bbox: AdaptBboxInput,\n): number[] | string[] | string {\n if (Array.isArray(bbox)) {\n if (Array.isArray(bbox[0])) {\n return bbox[0] as number[] | string[];\n }\n return bbox as number[] | string[];\n }\n return bbox as string;\n}\n\nexport function adaptBbox(\n bbox: AdaptBboxInput,\n width: number,\n height: number,\n modelFamily: TModelFamily | undefined,\n): [number, number, number, number] {\n const normalizedBbox = normalizeBboxInput(bbox);\n\n let result: [number, number, number, number] = [0, 0, 0, 0];\n if (modelFamily === 'doubao-vision' || isUITars(modelFamily)) {\n result = adaptDoubaoBbox(normalizedBbox, width, height);\n } else if (modelFamily === 'gemini') {\n result = adaptGeminiBbox(normalizedBbox as number[], width, height);\n } else if (modelFamily === 'qwen2.5-vl') {\n result = adaptQwen2_5Bbox(normalizedBbox as number[]);\n } else {\n // Default: normalized 0-1000 coordinate system\n // Includes: qwen3-vl, glm-v, auto-glm, auto-glm-multilingual, and future models\n result = normalized01000(normalizedBbox as number[], width, height);\n }\n\n return result;\n}\n\n// x1, y1, x2, y2 -> 0-1000\nexport function normalized01000(\n bbox: number[],\n width: number,\n height: number,\n): [number, number, number, number] {\n return [\n Math.round((bbox[0] * width) / 1000),\n Math.round((bbox[1] * height) / 1000),\n Math.round((bbox[2] * width) / 1000),\n Math.round((bbox[3] * height) / 1000),\n ];\n}\n\n// y1, x1, y2, x2 -> 0-1000\nexport function adaptGeminiBbox(\n bbox: number[],\n width: number,\n height: number,\n): [number, number, number, number] {\n const left = Math.round((bbox[1] * width) / 1000);\n const top = Math.round((bbox[0] * height) / 1000);\n const right = Math.round((bbox[3] * width) / 1000);\n const bottom = Math.round((bbox[2] * height) / 1000);\n return [left, top, right, bottom];\n}\n\nexport function adaptBboxToRect(\n bbox: number[],\n width: number,\n height: number,\n offsetX = 0,\n offsetY = 0,\n rightLimit = width,\n bottomLimit = height,\n modelFamily?: TModelFamily | undefined,\n scale = 1,\n): Rect {\n debugInspectUtils(\n 'adaptBboxToRect',\n bbox,\n width,\n height,\n 'offset',\n offsetX,\n offsetY,\n 'limit',\n rightLimit,\n bottomLimit,\n 'modelFamily',\n modelFamily,\n 'scale',\n scale,\n );\n\n const [left, top, right, bottom] = adaptBbox(\n bbox,\n width,\n height,\n modelFamily,\n );\n\n // Calculate initial rect dimensions and apply boundary constraints\n // For left and top: take max with 0 to ensure they're not negative\n const rectLeft = Math.max(0, left);\n const rectTop = Math.max(0, top);\n\n // For width and height: calculate from bounded coordinates and constrain to limits\n const boundedRight = Math.min(right, rightLimit);\n const boundedBottom = Math.min(bottom, bottomLimit);\n\n const rectWidth = boundedRight - rectLeft + 1;\n const rectHeight = boundedBottom - rectTop + 1;\n\n // Apply scale after calculating dimensions - scale back to original size\n const finalLeft = scale !== 1 ? Math.round(rectLeft / scale) : rectLeft;\n const finalTop = scale !== 1 ? Math.round(rectTop / scale) : rectTop;\n const finalWidth = scale !== 1 ? Math.round(rectWidth / scale) : rectWidth;\n const finalHeight = scale !== 1 ? Math.round(rectHeight / scale) : rectHeight;\n\n const rect = {\n left: finalLeft + offsetX,\n top: finalTop + offsetY,\n width: finalWidth,\n height: finalHeight,\n };\n debugInspectUtils('adaptBboxToRect, result=', rect);\n\n return rect;\n}\n\nexport function mergeRects(rects: Rect[]) {\n const minLeft = Math.min(...rects.map((r) => r.left));\n const minTop = Math.min(...rects.map((r) => r.top));\n const maxRight = Math.max(...rects.map((r) => r.left + r.width));\n const maxBottom = Math.max(...rects.map((r) => r.top + r.height));\n return {\n left: minLeft,\n top: minTop,\n width: maxRight - minLeft,\n height: maxBottom - minTop,\n };\n}\n\n/**\n * Expand the search area to at least 400 x 400 pixels\n *\n * Step 1: Extend 100px on each side (top, right, bottom, left)\n * - If the element is near a boundary, expansion on that side will be limited\n * - No compensation is made for boundary limitations (this is intentional)\n *\n * Step 2: Ensure the area is at least 400x400 pixels\n * - Scale up proportionally from the center if needed\n * - Final result is clamped to screen boundaries\n */\nexport function expandSearchArea(rect: Rect, screenSize: Size): Rect {\n const minArea = 400 * 400;\n const expandSize = 100;\n\n // Step 1: Extend each side by expandSize (100px), clamped to screen boundaries\n // Note: If element is near boundary, actual expansion may be less than 100px on that side\n const expandedLeft = Math.max(rect.left - expandSize, 0);\n const expandedTop = Math.max(rect.top - expandSize, 0);\n\n const expandRect = {\n left: expandedLeft,\n top: expandedTop,\n width: Math.min(\n rect.left - expandedLeft + rect.width + expandSize,\n screenSize.width - expandedLeft,\n ),\n height: Math.min(\n rect.top - expandedTop + rect.height + expandSize,\n screenSize.height - expandedTop,\n ),\n };\n\n // Step 2: Check if area is already >= 400x400\n const currentArea = expandRect.width * expandRect.height;\n\n if (currentArea >= minArea) {\n return expandRect;\n }\n\n // Step 2: Scale up from center to reach minimum 400x400 area\n const centerX = expandRect.left + expandRect.width / 2;\n const centerY = expandRect.top + expandRect.height / 2;\n\n // Calculate scale factor needed to reach minimum area\n const scaleFactor = Math.sqrt(minArea / currentArea);\n const newWidth = Math.round(expandRect.width * scaleFactor);\n const newHeight = Math.round(expandRect.height * scaleFactor);\n\n // Calculate new position based on center point\n const newLeft = Math.round(centerX - newWidth / 2);\n const newTop = Math.round(centerY - newHeight / 2);\n\n // Clamp to screen boundaries\n const left = Math.max(newLeft, 0);\n const top = Math.max(newTop, 0);\n\n return {\n left,\n top,\n width: Math.min(newWidth, screenSize.width - left),\n height: Math.min(newHeight, screenSize.height - top),\n };\n}\n\nexport async function markupImageForLLM(\n screenshotBase64: string,\n tree: ElementTreeNode<BaseElement>,\n size: Size,\n) {\n const elementsInfo = treeToList(tree);\n const elementsPositionInfoWithoutText = elementsInfo!.filter(\n (elementInfo) => {\n if (elementInfo.attributes.nodeType === NodeType.TEXT) {\n return false;\n }\n return true;\n },\n );\n\n const imagePayload = await compositeElementInfoImg({\n inputImgBase64: screenshotBase64,\n elementsPositionInfo: elementsPositionInfoWithoutText,\n size,\n });\n return imagePayload;\n}\n\nexport function buildYamlFlowFromPlans(\n plans: PlanningAction[],\n actionSpace: DeviceAction<any>[],\n): MidsceneYamlFlowItem[] {\n const flow: MidsceneYamlFlowItem[] = [];\n\n for (const plan of plans) {\n const verb = plan.type;\n\n const action = actionSpace.find((action) => action.name === verb);\n if (!action) {\n console.warn(\n `Cannot convert action ${verb} to yaml flow. Will ignore it.`,\n );\n continue;\n }\n\n const flowKey = action.interfaceAlias || verb;\n const flowParam = action.paramSchema\n ? dumpActionParam(plan.param || {}, action.paramSchema)\n : {};\n\n const flowItem: MidsceneYamlFlowItem = {\n [flowKey]: '',\n ...flowParam,\n };\n\n flow.push(flowItem);\n }\n\n return flow;\n}\n\n// Zod schemas for shared types\nexport const PointSchema = z.object({\n left: z.number(),\n top: z.number(),\n});\n\nexport const SizeSchema = z.object({\n width: z.number(),\n height: z.number(),\n dpr: z.number().optional(),\n});\n\nexport const RectSchema = PointSchema.and(SizeSchema).and(\n z.object({\n zoom: z.number().optional(),\n }),\n);\n\n// Zod schema for TMultimodalPrompt\nexport const TMultimodalPromptSchema = z.object({\n images: z\n .array(\n z.object({\n name: z.string(),\n url: z.string(),\n }),\n )\n .optional(),\n convertHttpImage2Base64: z.boolean().optional(),\n});\n\n// Zod schema for TUserPrompt\nexport const TUserPromptSchema = z.union([\n z.string(),\n z\n .object({\n prompt: z.string(),\n })\n .and(TMultimodalPromptSchema.partial()),\n]);\n\n// Generate TypeScript types from Zod schemas\nexport type TMultimodalPrompt = z.infer<typeof TMultimodalPromptSchema>;\nexport type TUserPrompt = z.infer<typeof TUserPromptSchema>;\n\nconst locateFieldFlagName = 'midscene_location_field_flag';\n\n// Schema for locator field input (when users provide locate parameters)\nconst MidsceneLocationInput = z\n .object({\n prompt: TUserPromptSchema,\n deepThink: z.boolean().optional(),\n cacheable: z.boolean().optional(),\n xpath: z.union([z.string(), z.boolean()]).optional(),\n })\n .passthrough();\n\n// Schema for locator field result (when AI returns locate results)\nconst MidsceneLocationResult = z\n .object({\n [locateFieldFlagName]: z.literal(true),\n prompt: TUserPromptSchema,\n\n // optional fields\n deepThink: z.boolean().optional(), // only available in vl model\n cacheable: z.boolean().optional(),\n xpath: z.boolean().optional(), // preset result for xpath\n\n // these two fields will only appear in the result\n center: z.tuple([z.number(), z.number()]),\n rect: RectSchema,\n })\n .passthrough();\n\n// Export the result type - this is used for runtime results that include center and rect\nexport type MidsceneLocationResultType = z.infer<typeof MidsceneLocationResult>;\n\n// Export the input type - this is the inferred type from getMidsceneLocationSchema()\nexport type MidsceneLocationInputType = z.infer<typeof MidsceneLocationInput>;\n\n/**\n * Returns the schema for locator fields.\n * This now returns the input schema which is more permissive and suitable for validation.\n */\nexport const getMidsceneLocationSchema = () => {\n return MidsceneLocationInput;\n};\n\nexport const ifMidsceneLocatorField = (field: any): boolean => {\n // Handle optional fields by getting the inner type\n let actualField = field;\n if (actualField._def?.typeName === 'ZodOptional') {\n actualField = actualField._def.innerType;\n }\n\n // Check if this is a ZodObject\n if (actualField._def?.typeName === 'ZodObject') {\n const shape = actualField._def.shape();\n\n // Method 1: Check for the location field flag (for result schema)\n if (locateFieldFlagName in shape) {\n return true;\n }\n\n // Method 2: Check if it's the input schema by checking for 'prompt' field\n // Input schema has 'prompt' as a required field\n if ('prompt' in shape && shape.prompt) {\n return true;\n }\n }\n\n return false;\n};\n\nexport const dumpMidsceneLocatorField = (field: any): string => {\n assert(\n ifMidsceneLocatorField(field),\n 'field is not a midscene locator field',\n );\n\n // If field is a string, return it directly\n if (typeof field === 'string') {\n return field;\n }\n\n // If field is an object with prompt property\n if (field && typeof field === 'object' && field.prompt) {\n // If prompt is a string, return it directly\n if (typeof field.prompt === 'string') {\n return field.prompt;\n }\n // If prompt is a TUserPrompt object, extract the prompt string\n if (typeof field.prompt === 'object' && field.prompt.prompt) {\n return field.prompt.prompt; // TODO: dump images if necessary\n }\n }\n\n // Fallback: try to convert to string\n return String(field);\n};\n\nexport const findAllMidsceneLocatorField = (\n zodType?: z.ZodType<any>,\n requiredOnly?: boolean,\n): string[] => {\n if (!zodType) {\n return [];\n }\n\n // Check if this is a ZodObject by checking if it has a shape property\n const zodObject = zodType as any;\n if (zodObject._def?.typeName === 'ZodObject' && zodObject.shape) {\n const keys = Object.keys(zodObject.shape);\n return keys.filter((key) => {\n const field = zodObject.shape[key];\n if (!ifMidsceneLocatorField(field)) {\n return false;\n }\n\n // If requiredOnly is true, filter out optional fields\n if (requiredOnly) {\n return field._def?.typeName !== 'ZodOptional';\n }\n\n return true;\n });\n }\n\n // For other ZodType instances, we can't extract field names\n return [];\n};\n\nexport const dumpActionParam = (\n jsonObject: Record<string, any>,\n zodSchema: z.ZodType<any>,\n): Record<string, any> => {\n // Prevent spreading strings into {0: 'c', 1: 'o', ...}\n if (!isPlainObject(jsonObject)) {\n return {};\n }\n\n const locatorFields = findAllMidsceneLocatorField(zodSchema);\n const result = { ...jsonObject };\n\n for (const fieldName of locatorFields) {\n const fieldValue = result[fieldName];\n if (fieldValue) {\n // If it's already a string, keep it as is\n if (typeof fieldValue === 'string') {\n result[fieldName] = fieldValue;\n } else if (typeof fieldValue === 'object') {\n // Check if this field is actually a MidsceneLocationType object\n if (fieldValue.prompt) {\n // If prompt is a string, use it directly\n if (typeof fieldValue.prompt === 'string') {\n result[fieldName] = fieldValue.prompt;\n } else if (\n typeof fieldValue.prompt === 'object' &&\n fieldValue.prompt.prompt\n ) {\n // If prompt is a TUserPrompt object, extract the prompt string\n result[fieldName] = fieldValue.prompt.prompt;\n }\n }\n }\n }\n }\n\n return result;\n};\n\n/**\n * Parse and validate action parameters using Zod schema.\n * All fields are validated through Zod, EXCEPT locator fields which are skipped.\n * Default values defined in the schema are automatically applied.\n *\n * Locator fields are special business logic fields with complex validation requirements,\n * so they are intentionally excluded from Zod parsing and use existing validation logic.\n */\nexport const parseActionParam = (\n rawParam: Record<string, any> | undefined,\n zodSchema?: z.ZodType<any>,\n): Record<string, any> | undefined => {\n // If no schema is provided, return undefined (action takes no parameters)\n if (!zodSchema) {\n return undefined;\n }\n\n // Handle undefined or null rawParam by providing an empty object\n const param = rawParam ?? {};\n\n // Find all locate fields in the schema\n const locateFields = findAllMidsceneLocatorField(zodSchema);\n\n // If there are no locate fields, just do normal validation\n if (locateFields.length === 0) {\n return zodSchema.parse(param);\n }\n\n // Extract locate field values to restore later\n const locateFieldValues: Record<string, any> = {};\n for (const fieldName of locateFields) {\n if (fieldName in param) {\n locateFieldValues[fieldName] = param[fieldName];\n }\n }\n\n // Build params for validation - skip locate fields and use dummy values\n const paramsForValidation: Record<string, any> = {};\n for (const key in param) {\n if (locateFields.includes(key)) {\n // Use dummy value to satisfy schema validation\n paramsForValidation[key] = { prompt: '_dummy_' };\n } else {\n paramsForValidation[key] = param[key];\n }\n }\n\n // Validate with dummy locate values\n const validated = zodSchema.parse(paramsForValidation);\n\n // Restore the actual locate field values (unvalidated, as per business requirement)\n for (const fieldName in locateFieldValues) {\n validated[fieldName] = locateFieldValues[fieldName];\n }\n\n return validated;\n};\n\nexport const finalizeActionName = 'Finalize';\n\n/**\n * Get a readable time string for a given timestamp or the current time\n * @param format - Optional format string. Supports: YYYY, MM, DD, HH, mm, ss. Default: 'YYYY-MM-DD HH:mm:ss'\n * @param timestamp - Optional timestamp in milliseconds. If not provided, uses current system time.\n * @returns A formatted time string with format label\n */\nexport const getReadableTimeString = (\n format = 'YYYY-MM-DD HH:mm:ss',\n timestamp?: number,\n): string => {\n const now = timestamp !== undefined ? new Date(timestamp) : new Date();\n const year = now.getFullYear();\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const day = String(now.getDate()).padStart(2, '0');\n const hours = String(now.getHours()).padStart(2, '0');\n const minutes = String(now.getMinutes()).padStart(2, '0');\n const seconds = String(now.getSeconds()).padStart(2, '0');\n\n const timeString = format\n .replace('YYYY', String(year))\n .replace('MM', month)\n .replace('DD', day)\n .replace('HH', hours)\n .replace('mm', minutes)\n .replace('ss', seconds);\n\n return `${timeString} (${format})`;\n};\n"],"names":["defaultBboxSize","debugInspectUtils","getDebug","pointToBbox","x","y","bboxSize","halfSize","x1","Math","y1","x2","y2","fillBboxParam","locate","width","height","modelFamily","adaptBbox","adaptQwen2_5Bbox","bbox","msg","JSON","Error","result","adaptDoubaoBbox","assert","splitted","Number","bboxList","Array","item","normalizeBboxInput","normalizedBbox","isUITars","adaptGeminiBbox","normalized01000","left","top","right","bottom","adaptBboxToRect","offsetX","offsetY","rightLimit","bottomLimit","scale","rectLeft","rectTop","boundedRight","boundedBottom","rectWidth","rectHeight","finalLeft","finalTop","finalWidth","finalHeight","rect","mergeRects","rects","minLeft","r","minTop","maxRight","maxBottom","expandSearchArea","screenSize","minArea","expandSize","expandedLeft","expandedTop","expandRect","currentArea","centerX","centerY","scaleFactor","newWidth","newHeight","newLeft","newTop","markupImageForLLM","screenshotBase64","tree","size","elementsInfo","treeToList","elementsPositionInfoWithoutText","elementInfo","NodeType","imagePayload","compositeElementInfoImg","buildYamlFlowFromPlans","plans","actionSpace","flow","plan","verb","action","console","flowKey","flowParam","dumpActionParam","flowItem","PointSchema","z","SizeSchema","RectSchema","TMultimodalPromptSchema","TUserPromptSchema","locateFieldFlagName","MidsceneLocationInput","getMidsceneLocationSchema","ifMidsceneLocatorField","field","actualField","shape","dumpMidsceneLocatorField","String","findAllMidsceneLocatorField","zodType","requiredOnly","zodObject","keys","Object","key","jsonObject","zodSchema","isPlainObject","locatorFields","fieldName","fieldValue","parseActionParam","rawParam","param","locateFields","locateFieldValues","paramsForValidation","validated","finalizeActionName","getReadableTimeString","format","timestamp","now","undefined","Date","year","month","day","hours","minutes","seconds","timeString"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"common.mjs","sources":["../../src/common.ts"],"sourcesContent":["import type {\n BaseElement,\n DeviceAction,\n ElementTreeNode,\n MidsceneYamlFlowItem,\n PlanningAction,\n Rect,\n Size,\n} from '@/types';\nimport { assert, isPlainObject } from '@midscene/shared/utils';\n\nimport type { ChatCompletionMessageParam } from 'openai/resources/index';\n\nimport { isUITars } from '@/ai-model/auto-glm/util';\nimport type { PlanningLocateParam } from '@/types';\nimport { NodeType } from '@midscene/shared/constants';\nimport type { TModelFamily } from '@midscene/shared/env';\nimport { treeToList } from '@midscene/shared/extractor';\nimport { compositeElementInfoImg } from '@midscene/shared/img';\nimport { getDebug } from '@midscene/shared/logger';\nimport { z } from 'zod';\n\nexport type AIArgs = ChatCompletionMessageParam[];\n\nconst defaultBboxSize = 20; // must be even number\nconst debugInspectUtils = getDebug('ai:common');\ntype AdaptBboxInput = number[] | string[] | string | (number[] | string[])[];\n\n/**\n * Convert a point coordinate [0, 1000] to a small bbox [0, 1000]\n * Creates a small bbox around the center point in the same coordinate space\n *\n * @param x - X coordinate in [0, 1000] range\n * @param y - Y coordinate in [0, 1000] range\n * @param bboxSize - Size of the bbox to create (default: 20)\n * @returns [x1, y1, x2, y2] bbox in [0, 1000] coordinate space\n */\nexport function pointToBbox(\n x: number,\n y: number,\n bboxSize = defaultBboxSize,\n): [number, number, number, number] {\n const halfSize = bboxSize / 2;\n const x1 = Math.max(x - halfSize, 0);\n const y1 = Math.max(y - halfSize, 0);\n const x2 = Math.min(x + halfSize, 1000);\n const y2 = Math.min(y + halfSize, 1000);\n\n return [x1, y1, x2, y2];\n}\n\n// transform the param of locate from qwen mode\nexport function fillBboxParam(\n locate: PlanningLocateParam,\n width: number,\n height: number,\n modelFamily: TModelFamily | undefined,\n) {\n // The Qwen model might have hallucinations of naming bbox as bbox_2d.\n if ((locate as any).bbox_2d && !locate?.bbox) {\n locate.bbox = (locate as any).bbox_2d;\n // biome-ignore lint/performance/noDelete: <explanation>\n delete (locate as any).bbox_2d;\n }\n\n if (locate?.bbox) {\n locate.bbox = adaptBbox(locate.bbox, width, height, modelFamily);\n }\n\n return locate;\n}\n\nexport function adaptQwen2_5Bbox(\n bbox: number[],\n): [number, number, number, number] {\n if (bbox.length < 2) {\n const msg = `invalid bbox data for qwen-vl mode: ${JSON.stringify(bbox)} `;\n throw new Error(msg);\n }\n\n const result: [number, number, number, number] = [\n Math.round(bbox[0]),\n Math.round(bbox[1]),\n typeof bbox[2] === 'number'\n ? Math.round(bbox[2])\n : Math.round(bbox[0] + defaultBboxSize),\n typeof bbox[3] === 'number'\n ? Math.round(bbox[3])\n : Math.round(bbox[1] + defaultBboxSize),\n ];\n return result;\n}\n\nexport function adaptDoubaoBbox(\n bbox: string[] | number[] | string,\n width: number,\n height: number,\n): [number, number, number, number] {\n assert(\n width > 0 && height > 0,\n 'width and height must be greater than 0 in doubao mode',\n );\n\n if (typeof bbox === 'string') {\n assert(\n /^(\\d+)\\s(\\d+)\\s(\\d+)\\s(\\d+)$/.test(bbox.trim()),\n `invalid bbox data string for doubao-vision mode: ${bbox}`,\n );\n const splitted = bbox.split(' ');\n if (splitted.length === 4) {\n return [\n Math.round((Number(splitted[0]) * width) / 1000),\n Math.round((Number(splitted[1]) * height) / 1000),\n Math.round((Number(splitted[2]) * width) / 1000),\n Math.round((Number(splitted[3]) * height) / 1000),\n ];\n }\n throw new Error(`invalid bbox data string for doubao-vision mode: ${bbox}`);\n }\n\n let bboxList: number[] = [];\n if (Array.isArray(bbox) && typeof bbox[0] === 'string') {\n bbox.forEach((item) => {\n if (typeof item === 'string' && item.includes(',')) {\n const [x, y] = item.split(',');\n bboxList.push(Number(x.trim()), Number(y.trim()));\n } else if (typeof item === 'string' && item.includes(' ')) {\n const [x, y] = item.split(' ');\n bboxList.push(Number(x.trim()), Number(y.trim()));\n } else {\n bboxList.push(Number(item));\n }\n });\n } else {\n bboxList = bbox as any;\n }\n\n if (bboxList.length === 4 || bboxList.length === 5) {\n return [\n Math.round((bboxList[0] * width) / 1000),\n Math.round((bboxList[1] * height) / 1000),\n Math.round((bboxList[2] * width) / 1000),\n Math.round((bboxList[3] * height) / 1000),\n ];\n }\n\n // treat the bbox as a center point\n if (\n bboxList.length === 6 ||\n bboxList.length === 2 ||\n bboxList.length === 3 ||\n bboxList.length === 7\n ) {\n return [\n Math.max(\n 0,\n Math.round((bboxList[0] * width) / 1000) - defaultBboxSize / 2,\n ),\n Math.max(\n 0,\n Math.round((bboxList[1] * height) / 1000) - defaultBboxSize / 2,\n ),\n Math.min(\n width,\n Math.round((bboxList[0] * width) / 1000) + defaultBboxSize / 2,\n ),\n Math.min(\n height,\n Math.round((bboxList[1] * height) / 1000) + defaultBboxSize / 2,\n ),\n ];\n }\n\n if (bbox.length === 8) {\n return [\n Math.round((bboxList[0] * width) / 1000),\n Math.round((bboxList[1] * height) / 1000),\n Math.round((bboxList[4] * width) / 1000),\n Math.round((bboxList[5] * height) / 1000),\n ];\n }\n\n const msg = `invalid bbox data for doubao-vision mode: ${JSON.stringify(bbox)} `;\n throw new Error(msg);\n}\n\nfunction normalizeBboxInput(\n bbox: AdaptBboxInput,\n): number[] | string[] | string {\n if (Array.isArray(bbox)) {\n if (Array.isArray(bbox[0])) {\n return bbox[0] as number[] | string[];\n }\n return bbox as number[] | string[];\n }\n return bbox as string;\n}\n\nexport function adaptBbox(\n bbox: AdaptBboxInput,\n width: number,\n height: number,\n modelFamily: TModelFamily | undefined,\n): [number, number, number, number] {\n const normalizedBbox = normalizeBboxInput(bbox);\n\n let result: [number, number, number, number] = [0, 0, 0, 0];\n if (\n modelFamily === 'doubao-vision' ||\n modelFamily === 'doubao-seed' ||\n isUITars(modelFamily)\n ) {\n result = adaptDoubaoBbox(normalizedBbox, width, height);\n } else if (modelFamily === 'gemini') {\n result = adaptGeminiBbox(normalizedBbox as number[], width, height);\n } else if (modelFamily === 'qwen2.5-vl') {\n result = adaptQwen2_5Bbox(normalizedBbox as number[]);\n } else {\n // Default: normalized 0-1000 coordinate system\n // Includes: qwen3-vl, qwen3.5, glm-v, auto-glm, auto-glm-multilingual, and future models\n result = normalized01000(normalizedBbox as number[], width, height);\n }\n\n return result;\n}\n\n// x1, y1, x2, y2 -> 0-1000\nexport function normalized01000(\n bbox: number[],\n width: number,\n height: number,\n): [number, number, number, number] {\n return [\n Math.round((bbox[0] * width) / 1000),\n Math.round((bbox[1] * height) / 1000),\n Math.round((bbox[2] * width) / 1000),\n Math.round((bbox[3] * height) / 1000),\n ];\n}\n\n// y1, x1, y2, x2 -> 0-1000\nexport function adaptGeminiBbox(\n bbox: number[],\n width: number,\n height: number,\n): [number, number, number, number] {\n const left = Math.round((bbox[1] * width) / 1000);\n const top = Math.round((bbox[0] * height) / 1000);\n const right = Math.round((bbox[3] * width) / 1000);\n const bottom = Math.round((bbox[2] * height) / 1000);\n return [left, top, right, bottom];\n}\n\nexport function adaptBboxToRect(\n bbox: number[],\n width: number,\n height: number,\n offsetX = 0,\n offsetY = 0,\n rightLimit = width,\n bottomLimit = height,\n modelFamily?: TModelFamily | undefined,\n scale = 1,\n): Rect {\n debugInspectUtils(\n 'adaptBboxToRect',\n bbox,\n width,\n height,\n 'offset',\n offsetX,\n offsetY,\n 'limit',\n rightLimit,\n bottomLimit,\n 'modelFamily',\n modelFamily,\n 'scale',\n scale,\n );\n\n const [left, top, right, bottom] = adaptBbox(\n bbox,\n width,\n height,\n modelFamily,\n );\n\n // Calculate initial rect dimensions and apply boundary constraints\n // For left and top: take max with 0 to ensure they're not negative\n const rectLeft = Math.max(0, left);\n const rectTop = Math.max(0, top);\n\n // For width and height: calculate from bounded coordinates and constrain to limits\n const boundedRight = Math.min(right, rightLimit);\n const boundedBottom = Math.min(bottom, bottomLimit);\n\n const rectWidth = boundedRight - rectLeft + 1;\n const rectHeight = boundedBottom - rectTop + 1;\n\n // Apply scale after calculating dimensions - scale back to original size\n const finalLeft = scale !== 1 ? Math.round(rectLeft / scale) : rectLeft;\n const finalTop = scale !== 1 ? Math.round(rectTop / scale) : rectTop;\n const finalWidth = scale !== 1 ? Math.round(rectWidth / scale) : rectWidth;\n const finalHeight = scale !== 1 ? Math.round(rectHeight / scale) : rectHeight;\n\n const rect = {\n left: finalLeft + offsetX,\n top: finalTop + offsetY,\n width: finalWidth,\n height: finalHeight,\n };\n debugInspectUtils('adaptBboxToRect, result=', rect);\n\n return rect;\n}\n\nexport function mergeRects(rects: Rect[]) {\n const minLeft = Math.min(...rects.map((r) => r.left));\n const minTop = Math.min(...rects.map((r) => r.top));\n const maxRight = Math.max(...rects.map((r) => r.left + r.width));\n const maxBottom = Math.max(...rects.map((r) => r.top + r.height));\n return {\n left: minLeft,\n top: minTop,\n width: maxRight - minLeft,\n height: maxBottom - minTop,\n };\n}\n\n/**\n * Expand the search area to at least 400 x 400 pixels\n *\n * Step 1: Extend 100px on each side (top, right, bottom, left)\n * - If the element is near a boundary, expansion on that side will be limited\n * - No compensation is made for boundary limitations (this is intentional)\n *\n * Step 2: Ensure the area is at least 400x400 pixels\n * - Scale up proportionally from the center if needed\n * - Final result is clamped to screen boundaries\n */\nexport function expandSearchArea(rect: Rect, screenSize: Size): Rect {\n const minArea = 400 * 400;\n const expandSize = 100;\n\n // Step 1: Extend each side by expandSize (100px), clamped to screen boundaries\n // Note: If element is near boundary, actual expansion may be less than 100px on that side\n const expandedLeft = Math.max(rect.left - expandSize, 0);\n const expandedTop = Math.max(rect.top - expandSize, 0);\n\n const expandRect = {\n left: expandedLeft,\n top: expandedTop,\n width: Math.min(\n rect.left - expandedLeft + rect.width + expandSize,\n screenSize.width - expandedLeft,\n ),\n height: Math.min(\n rect.top - expandedTop + rect.height + expandSize,\n screenSize.height - expandedTop,\n ),\n };\n\n // Step 2: Check if area is already >= 400x400\n const currentArea = expandRect.width * expandRect.height;\n\n if (currentArea >= minArea) {\n return expandRect;\n }\n\n // Step 2: Scale up from center to reach minimum 400x400 area\n const centerX = expandRect.left + expandRect.width / 2;\n const centerY = expandRect.top + expandRect.height / 2;\n\n // Calculate scale factor needed to reach minimum area\n const scaleFactor = Math.sqrt(minArea / currentArea);\n const newWidth = Math.round(expandRect.width * scaleFactor);\n const newHeight = Math.round(expandRect.height * scaleFactor);\n\n // Calculate new position based on center point\n const newLeft = Math.round(centerX - newWidth / 2);\n const newTop = Math.round(centerY - newHeight / 2);\n\n // Clamp to screen boundaries\n const left = Math.max(newLeft, 0);\n const top = Math.max(newTop, 0);\n\n return {\n left,\n top,\n width: Math.min(newWidth, screenSize.width - left),\n height: Math.min(newHeight, screenSize.height - top),\n };\n}\n\nexport async function markupImageForLLM(\n screenshotBase64: string,\n tree: ElementTreeNode<BaseElement>,\n size: Size,\n) {\n const elementsInfo = treeToList(tree);\n const elementsPositionInfoWithoutText = elementsInfo!.filter(\n (elementInfo) => {\n if (elementInfo.attributes.nodeType === NodeType.TEXT) {\n return false;\n }\n return true;\n },\n );\n\n const imagePayload = await compositeElementInfoImg({\n inputImgBase64: screenshotBase64,\n elementsPositionInfo: elementsPositionInfoWithoutText,\n size,\n });\n return imagePayload;\n}\n\nexport function buildYamlFlowFromPlans(\n plans: PlanningAction[],\n actionSpace: DeviceAction<any>[],\n): MidsceneYamlFlowItem[] {\n const flow: MidsceneYamlFlowItem[] = [];\n\n for (const plan of plans) {\n const verb = plan.type;\n\n const action = actionSpace.find((action) => action.name === verb);\n if (!action) {\n console.warn(\n `Cannot convert action ${verb} to yaml flow. Will ignore it.`,\n );\n continue;\n }\n\n const flowKey = action.interfaceAlias || verb;\n const flowParam = action.paramSchema\n ? dumpActionParam(plan.param || {}, action.paramSchema)\n : {};\n\n const flowItem: MidsceneYamlFlowItem = {\n [flowKey]: '',\n ...flowParam,\n };\n\n flow.push(flowItem);\n }\n\n return flow;\n}\n\n// Zod schemas for shared types\nexport const PointSchema = z.object({\n left: z.number(),\n top: z.number(),\n});\n\nexport const SizeSchema = z.object({\n width: z.number(),\n height: z.number(),\n});\n\nexport const RectSchema = PointSchema.and(SizeSchema).and(\n z.object({\n zoom: z.number().optional(),\n }),\n);\n\n// Zod schema for TMultimodalPrompt\nexport const TMultimodalPromptSchema = z.object({\n images: z\n .array(\n z.object({\n name: z.string(),\n url: z.string(),\n }),\n )\n .optional(),\n convertHttpImage2Base64: z.boolean().optional(),\n});\n\n// Zod schema for TUserPrompt\nexport const TUserPromptSchema = z.union([\n z.string(),\n z\n .object({\n prompt: z.string(),\n })\n .and(TMultimodalPromptSchema.partial()),\n]);\n\n// Generate TypeScript types from Zod schemas\nexport type TMultimodalPrompt = z.infer<typeof TMultimodalPromptSchema>;\nexport type TUserPrompt = z.infer<typeof TUserPromptSchema>;\n\nconst locateFieldFlagName = 'midscene_location_field_flag';\n\n// Schema for locator field input (when users provide locate parameters)\nconst MidsceneLocationInput = z\n .object({\n prompt: TUserPromptSchema,\n deepThink: z.boolean().optional(),\n cacheable: z.boolean().optional(),\n xpath: z.union([z.string(), z.boolean()]).optional(),\n })\n .passthrough();\n\n/**\n * Returns the schema for locator fields.\n * This now returns the input schema which is more permissive and suitable for validation.\n */\nexport const getMidsceneLocationSchema = () => {\n return MidsceneLocationInput;\n};\n\nexport const ifMidsceneLocatorField = (field: any): boolean => {\n // Handle optional fields by getting the inner type\n let actualField = field;\n if (actualField._def?.typeName === 'ZodOptional') {\n actualField = actualField._def.innerType;\n }\n\n // Check if this is a ZodObject\n if (actualField._def?.typeName === 'ZodObject') {\n const shape = actualField._def.shape();\n\n // Method 1: Check for the location field flag (for result schema)\n if (locateFieldFlagName in shape) {\n return true;\n }\n\n // Method 2: Check if it's the input schema by checking for 'prompt' field\n // Input schema has 'prompt' as a required field\n if ('prompt' in shape && shape.prompt) {\n return true;\n }\n }\n\n return false;\n};\n\nexport const dumpMidsceneLocatorField = (field: any): string => {\n assert(\n ifMidsceneLocatorField(field),\n 'field is not a midscene locator field',\n );\n\n // If field is a string, return it directly\n if (typeof field === 'string') {\n return field;\n }\n\n // If field is an object with prompt property\n if (field && typeof field === 'object' && field.prompt) {\n // If prompt is a string, return it directly\n if (typeof field.prompt === 'string') {\n return field.prompt;\n }\n // If prompt is a TUserPrompt object, extract the prompt string\n if (typeof field.prompt === 'object' && field.prompt.prompt) {\n return field.prompt.prompt; // TODO: dump images if necessary\n }\n }\n\n // Fallback: try to convert to string\n return String(field);\n};\n\nexport const findAllMidsceneLocatorField = (\n zodType?: z.ZodType<any>,\n requiredOnly?: boolean,\n): string[] => {\n if (!zodType) {\n return [];\n }\n\n // Check if this is a ZodObject by checking if it has a shape property\n const zodObject = zodType as any;\n if (zodObject._def?.typeName === 'ZodObject' && zodObject.shape) {\n const keys = Object.keys(zodObject.shape);\n return keys.filter((key) => {\n const field = zodObject.shape[key];\n if (!ifMidsceneLocatorField(field)) {\n return false;\n }\n\n // If requiredOnly is true, filter out optional fields\n if (requiredOnly) {\n return field._def?.typeName !== 'ZodOptional';\n }\n\n return true;\n });\n }\n\n // For other ZodType instances, we can't extract field names\n return [];\n};\n\nexport const dumpActionParam = (\n jsonObject: Record<string, any>,\n zodSchema: z.ZodType<any>,\n): Record<string, any> => {\n // Prevent spreading strings into {0: 'c', 1: 'o', ...}\n if (!isPlainObject(jsonObject)) {\n return {};\n }\n\n const locatorFields = findAllMidsceneLocatorField(zodSchema);\n const result = { ...jsonObject };\n\n for (const fieldName of locatorFields) {\n const fieldValue = result[fieldName];\n if (fieldValue) {\n // If it's already a string, keep it as is\n if (typeof fieldValue === 'string') {\n result[fieldName] = fieldValue;\n } else if (typeof fieldValue === 'object') {\n // Check if this field is actually a MidsceneLocationType object\n if (fieldValue.prompt) {\n // If prompt is a string, use it directly\n if (typeof fieldValue.prompt === 'string') {\n result[fieldName] = fieldValue.prompt;\n } else if (\n typeof fieldValue.prompt === 'object' &&\n fieldValue.prompt.prompt\n ) {\n // If prompt is a TUserPrompt object, extract the prompt string\n result[fieldName] = fieldValue.prompt.prompt;\n }\n }\n }\n }\n }\n\n return result;\n};\n\n/**\n * Parse and validate action parameters using Zod schema.\n * All fields are validated through Zod, EXCEPT locator fields which are skipped.\n * Default values defined in the schema are automatically applied.\n *\n * Locator fields are special business logic fields with complex validation requirements,\n * so they are intentionally excluded from Zod parsing and use existing validation logic.\n */\nexport const parseActionParam = (\n rawParam: Record<string, any> | undefined,\n zodSchema?: z.ZodType<any>,\n): Record<string, any> | undefined => {\n // If no schema is provided, return undefined (action takes no parameters)\n if (!zodSchema) {\n return undefined;\n }\n\n // Handle undefined or null rawParam by providing an empty object\n const param = rawParam ?? {};\n\n // Find all locate fields in the schema\n const locateFields = findAllMidsceneLocatorField(zodSchema);\n\n // If there are no locate fields, just do normal validation\n if (locateFields.length === 0) {\n return zodSchema.parse(param);\n }\n\n // Extract locate field values to restore later\n const locateFieldValues: Record<string, any> = {};\n for (const fieldName of locateFields) {\n if (fieldName in param) {\n locateFieldValues[fieldName] = param[fieldName];\n }\n }\n\n // Build params for validation - skip locate fields and use dummy values\n const paramsForValidation: Record<string, any> = {};\n for (const key in param) {\n if (locateFields.includes(key)) {\n // Use dummy value to satisfy schema validation\n paramsForValidation[key] = { prompt: '_dummy_' };\n } else {\n paramsForValidation[key] = param[key];\n }\n }\n\n // Validate with dummy locate values\n const validated = zodSchema.parse(paramsForValidation);\n\n // Restore the actual locate field values (unvalidated, as per business requirement)\n for (const fieldName in locateFieldValues) {\n validated[fieldName] = locateFieldValues[fieldName];\n }\n\n return validated;\n};\n\nexport const finalizeActionName = 'Finalize';\n\n/**\n * Get a readable time string for a given timestamp or the current time\n * @param format - Optional format string. Supports: YYYY, MM, DD, HH, mm, ss. Default: 'YYYY-MM-DD HH:mm:ss'\n * @param timestamp - Optional timestamp in milliseconds. If not provided, uses current system time.\n * @returns A formatted time string with format label\n */\nexport const getReadableTimeString = (\n format = 'YYYY-MM-DD HH:mm:ss',\n timestamp?: number,\n): string => {\n const now = timestamp !== undefined ? new Date(timestamp) : new Date();\n const year = now.getFullYear();\n const month = String(now.getMonth() + 1).padStart(2, '0');\n const day = String(now.getDate()).padStart(2, '0');\n const hours = String(now.getHours()).padStart(2, '0');\n const minutes = String(now.getMinutes()).padStart(2, '0');\n const seconds = String(now.getSeconds()).padStart(2, '0');\n\n const timeString = format\n .replace('YYYY', String(year))\n .replace('MM', month)\n .replace('DD', day)\n .replace('HH', hours)\n .replace('mm', minutes)\n .replace('ss', seconds);\n\n return `${timeString} (${format})`;\n};\n"],"names":["defaultBboxSize","debugInspectUtils","getDebug","pointToBbox","x","y","bboxSize","halfSize","x1","Math","y1","x2","y2","fillBboxParam","locate","width","height","modelFamily","adaptBbox","adaptQwen2_5Bbox","bbox","msg","JSON","Error","result","adaptDoubaoBbox","assert","splitted","Number","bboxList","Array","item","normalizeBboxInput","normalizedBbox","isUITars","adaptGeminiBbox","normalized01000","left","top","right","bottom","adaptBboxToRect","offsetX","offsetY","rightLimit","bottomLimit","scale","rectLeft","rectTop","boundedRight","boundedBottom","rectWidth","rectHeight","finalLeft","finalTop","finalWidth","finalHeight","rect","mergeRects","rects","minLeft","r","minTop","maxRight","maxBottom","expandSearchArea","screenSize","minArea","expandSize","expandedLeft","expandedTop","expandRect","currentArea","centerX","centerY","scaleFactor","newWidth","newHeight","newLeft","newTop","markupImageForLLM","screenshotBase64","tree","size","elementsInfo","treeToList","elementsPositionInfoWithoutText","elementInfo","NodeType","imagePayload","compositeElementInfoImg","buildYamlFlowFromPlans","plans","actionSpace","flow","plan","verb","action","console","flowKey","flowParam","dumpActionParam","flowItem","PointSchema","z","SizeSchema","RectSchema","TMultimodalPromptSchema","TUserPromptSchema","locateFieldFlagName","MidsceneLocationInput","getMidsceneLocationSchema","ifMidsceneLocatorField","field","actualField","shape","dumpMidsceneLocatorField","String","findAllMidsceneLocatorField","zodType","requiredOnly","zodObject","keys","Object","key","jsonObject","zodSchema","isPlainObject","locatorFields","fieldName","fieldValue","parseActionParam","rawParam","param","locateFields","locateFieldValues","paramsForValidation","validated","finalizeActionName","getReadableTimeString","format","timestamp","now","undefined","Date","year","month","day","hours","minutes","seconds","timeString"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"device/index.mjs","sources":["../../../src/device/index.ts"],"sourcesContent":["import { getMidsceneLocationSchema } from '@/common';\nimport type {\n ActionScrollParam,\n DeviceAction,\n LocateResultElement,\n} from '@/types';\nimport type { IModelConfig } from '@midscene/shared/env';\nimport type { ElementNode } from '@midscene/shared/extractor';\nimport { getDebug } from '@midscene/shared/logger';\nimport { _keyDefinitions } from '@midscene/shared/us-keyboard-layout';\nimport { z } from 'zod';\nimport type { ElementCacheFeature, Rect, Size, UIContext } from '../types';\n\nexport interface FileChooserHandler {\n accept(files: string[]): Promise<void>;\n}\n\nexport abstract class AbstractInterface {\n abstract interfaceType: string;\n\n abstract screenshotBase64(): Promise<string>;\n abstract size(): Promise<Size>;\n abstract actionSpace(): DeviceAction[];\n\n abstract cacheFeatureForPoint?(\n center: [number, number],\n options?: {\n targetDescription?: string;\n modelConfig?: IModelConfig;\n },\n ): Promise<ElementCacheFeature>;\n abstract rectMatchesCacheFeature?(\n feature: ElementCacheFeature,\n ): Promise<Rect>;\n\n abstract destroy?(): Promise<void>;\n\n abstract describe?(): string;\n abstract beforeInvokeAction?(actionName: string, param: any): Promise<void>;\n abstract afterInvokeAction?(actionName: string, param: any): Promise<void>;\n\n // for web only\n registerFileChooserListener?(\n handler: (chooser: FileChooserHandler) => Promise<void>,\n ): Promise<{ dispose: () => void; getError: () => Error | undefined }>;\n\n // @deprecated do NOT extend this method\n abstract getElementsNodeTree?: () => Promise<ElementNode>;\n\n // @deprecated do NOT extend this method\n abstract url?: () => string | Promise<string>;\n\n // @deprecated do NOT extend this method\n abstract evaluateJavaScript?<T = any>(script: string): Promise<T>;\n\n // @deprecated do NOT extend this method\n abstract getContext?(): Promise<UIContext>;\n\n /**\n * Get the current time from the device.\n * Returns the device's current timestamp in milliseconds.\n * This is useful when the system time and device time are not synchronized.\n */\n getTimestamp?(): Promise<number>;\n\n /** URL of native MJPEG stream for real-time screen preview (e.g. WDA MJPEG server) */\n mjpegStreamUrl?: string;\n}\n\n// Generic function to define actions with proper type inference\n// TRuntime allows specifying a different type for the runtime parameter (after location resolution)\n// TReturn allows specifying the return type of the action\nexport const defineAction = <\n TSchema extends z.ZodType | undefined = undefined,\n TRuntime = TSchema extends z.ZodType ? z.infer<TSchema> : undefined,\n TReturn = any,\n>(\n config: {\n name: string;\n description: string;\n interfaceAlias?: string;\n paramSchema?: TSchema;\n call: (param: TRuntime) => Promise<TReturn> | TReturn;\n } & Partial<\n Omit<\n DeviceAction<TRuntime, TReturn>,\n 'name' | 'description' | 'interfaceAlias' | 'paramSchema' | 'call'\n >\n >,\n): DeviceAction<TRuntime, TReturn> => {\n return config as any; // Type assertion needed because schema validation type differs from runtime type\n};\n\n// Tap\nexport const actionTapParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe('The element to be tapped'),\n});\nexport type ActionTapParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionTap = (\n call: (param: ActionTapParam) => Promise<void>,\n): DeviceAction<ActionTapParam> => {\n return defineAction<typeof actionTapParamSchema, ActionTapParam>({\n name: 'Tap',\n description: 'Tap the element',\n interfaceAlias: 'aiTap',\n paramSchema: actionTapParamSchema,\n call,\n });\n};\n\n// RightClick\nexport const actionRightClickParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe(\n 'The element to be right clicked',\n ),\n});\nexport type ActionRightClickParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionRightClick = (\n call: (param: ActionRightClickParam) => Promise<void>,\n): DeviceAction<ActionRightClickParam> => {\n return defineAction<\n typeof actionRightClickParamSchema,\n ActionRightClickParam\n >({\n name: 'RightClick',\n description: 'Right click the element',\n interfaceAlias: 'aiRightClick',\n paramSchema: actionRightClickParamSchema,\n call,\n });\n};\n\n// DoubleClick\nexport const actionDoubleClickParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe(\n 'The element to be double clicked',\n ),\n});\nexport type ActionDoubleClickParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionDoubleClick = (\n call: (param: ActionDoubleClickParam) => Promise<void>,\n): DeviceAction<ActionDoubleClickParam> => {\n return defineAction<\n typeof actionDoubleClickParamSchema,\n ActionDoubleClickParam\n >({\n name: 'DoubleClick',\n description: 'Double click the element',\n interfaceAlias: 'aiDoubleClick',\n paramSchema: actionDoubleClickParamSchema,\n call,\n });\n};\n\n// Hover\nexport const actionHoverParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe('The element to be hovered'),\n});\nexport type ActionHoverParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionHover = (\n call: (param: ActionHoverParam) => Promise<void>,\n): DeviceAction<ActionHoverParam> => {\n return defineAction<typeof actionHoverParamSchema, ActionHoverParam>({\n name: 'Hover',\n description: 'Move the mouse to the element',\n interfaceAlias: 'aiHover',\n paramSchema: actionHoverParamSchema,\n call,\n });\n};\n\n// Input\nconst inputLocateDescription =\n 'the position of the placeholder or text content in the target input field. If there is no content, locate the center of the input field.';\nexport const actionInputParamSchema = z.object({\n value: z\n .union([z.string(), z.number()])\n .transform((val) => String(val))\n .describe(\n 'The text to input. Provide the final content for replace/append modes, or an empty string when using clear mode to remove existing text.',\n ),\n locate: getMidsceneLocationSchema()\n .describe(inputLocateDescription)\n .optional(),\n mode: z\n .enum(['replace', 'clear', 'typeOnly'])\n .default('replace')\n .describe(\n 'Input mode: \"replace\" (default) - clear the field and input the value; \"typeOnly\" - type the value directly without clearing the field first; \"clear\" - clear the field without inputting new text.',\n ),\n});\nexport type ActionInputParam = {\n value: string;\n locate?: LocateResultElement;\n mode?: 'replace' | 'clear' | 'typeOnly' | 'append';\n};\n\nexport const defineActionInput = (\n call: (param: ActionInputParam) => Promise<void>,\n): DeviceAction<ActionInputParam> => {\n return defineAction<typeof actionInputParamSchema, ActionInputParam>({\n name: 'Input',\n description: 'Input the value into the element',\n interfaceAlias: 'aiInput',\n paramSchema: actionInputParamSchema,\n call: (param) => {\n // backward compat: convert deprecated 'append' to 'typeOnly'\n if ((param.mode as string) === 'append') {\n param.mode = 'typeOnly';\n }\n return call(param);\n },\n });\n};\n\n// KeyboardPress\nexport const actionKeyboardPressParamSchema = z.object({\n locate: getMidsceneLocationSchema()\n .describe('The element to be clicked before pressing the key')\n .optional(),\n keyName: z\n .string()\n .describe(\n \"The key to be pressed. Use '+' for key combinations, e.g., 'Control+A', 'Shift+Enter'\",\n ),\n});\nexport type ActionKeyboardPressParam = {\n locate?: LocateResultElement;\n keyName: string;\n};\n\nexport const defineActionKeyboardPress = (\n call: (param: ActionKeyboardPressParam) => Promise<void>,\n): DeviceAction<ActionKeyboardPressParam> => {\n return defineAction<\n typeof actionKeyboardPressParamSchema,\n ActionKeyboardPressParam\n >({\n name: 'KeyboardPress',\n description:\n 'Press a key or key combination, like \"Enter\", \"Tab\", \"Escape\", or \"Control+A\", \"Shift+Enter\". Do not use this to type text.',\n interfaceAlias: 'aiKeyboardPress',\n paramSchema: actionKeyboardPressParamSchema,\n call,\n });\n};\n\n// Scroll\nexport const actionScrollParamSchema = z.object({\n scrollType: z\n .enum([\n 'singleAction',\n 'scrollToBottom',\n 'scrollToTop',\n 'scrollToRight',\n 'scrollToLeft',\n ])\n .default('singleAction')\n .describe(\n 'The scroll behavior: \"singleAction\" for a single scroll action, \"scrollToBottom\" for scrolling all the way to the bottom by rapidly scrolling 5-10 times (skipping intermediate content until reaching the bottom), \"scrollToTop\" for scrolling all the way to the top by rapidly scrolling 5-10 times (skipping intermediate content until reaching the top), \"scrollToRight\" for scrolling all the way to the right by rapidly scrolling multiple times, \"scrollToLeft\" for scrolling all the way to the left by rapidly scrolling multiple times',\n ),\n direction: z\n .enum(['down', 'up', 'right', 'left'])\n .default('down')\n .describe(\n 'The direction to scroll. Only effective when scrollType is \"singleAction\".',\n ),\n distance: z\n .number()\n .nullable()\n .optional()\n .describe('The distance in pixels to scroll'),\n locate: getMidsceneLocationSchema()\n .optional()\n .describe(\n 'Describe the target element to be scrolled on, like \"the table\" or \"the list\" or \"the content area\" or \"the scrollable area\". Do NOT provide a general intent like \"scroll to find some element\"',\n ),\n});\n\nexport const defineActionScroll = (\n call: (param: ActionScrollParam) => Promise<void>,\n): DeviceAction<ActionScrollParam> => {\n return defineAction<typeof actionScrollParamSchema, ActionScrollParam>({\n name: 'Scroll',\n description:\n 'Scroll the page or a scrollable element to browse content. This is the preferred way to scroll on all platforms, including mobile. Supports scrollToBottom/scrollToTop for boundary navigation. Default: direction `down`, scrollType `singleAction`, distance `null`.',\n interfaceAlias: 'aiScroll',\n paramSchema: actionScrollParamSchema,\n call,\n });\n};\n\n// DragAndDrop\nexport const actionDragAndDropParamSchema = z.object({\n from: getMidsceneLocationSchema().describe('The position to be dragged'),\n to: getMidsceneLocationSchema().describe('The position to be dropped'),\n});\nexport type ActionDragAndDropParam = {\n from: LocateResultElement;\n to: LocateResultElement;\n};\n\nexport const defineActionDragAndDrop = (\n call: (param: ActionDragAndDropParam) => Promise<void>,\n): DeviceAction<ActionDragAndDropParam> => {\n return defineAction<\n typeof actionDragAndDropParamSchema,\n ActionDragAndDropParam\n >({\n name: 'DragAndDrop',\n description:\n 'Pick up a specific UI element and move it to a new position (e.g., reorder a card, move a file into a folder, sort list items). The element itself moves with your finger/mouse.',\n interfaceAlias: 'aiDragAndDrop',\n paramSchema: actionDragAndDropParamSchema,\n call,\n });\n};\n\nexport const ActionLongPressParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe(\n 'The element to be long pressed',\n ),\n duration: z\n .number()\n .default(500)\n .optional()\n .describe('Long press duration in milliseconds'),\n});\n\nexport type ActionLongPressParam = {\n locate: LocateResultElement;\n duration?: number;\n};\nexport const defineActionLongPress = (\n call: (param: ActionLongPressParam) => Promise<void>,\n): DeviceAction<ActionLongPressParam> => {\n return defineAction<typeof ActionLongPressParamSchema, ActionLongPressParam>({\n name: 'LongPress',\n description: 'Long press the element',\n paramSchema: ActionLongPressParamSchema,\n call,\n });\n};\n\nexport const ActionSwipeParamSchema = z.object({\n start: getMidsceneLocationSchema()\n .optional()\n .describe(\n 'Starting point of the swipe gesture, if not specified, the center of the page will be used',\n ),\n direction: z\n .enum(['up', 'down', 'left', 'right'])\n .optional()\n .describe(\n 'The direction to swipe (required when using distance). The direction means the direction of the finger swipe.',\n ),\n distance: z\n .number()\n .optional()\n .describe('The distance in pixels to swipe (mutually exclusive with end)'),\n end: getMidsceneLocationSchema()\n .optional()\n .describe(\n 'Ending point of the swipe gesture (mutually exclusive with distance)',\n ),\n duration: z\n .number()\n .default(300)\n .describe('Duration of the swipe gesture in milliseconds'),\n repeat: z\n .number()\n .optional()\n .describe(\n 'The number of times to repeat the swipe gesture. 1 for default, 0 for infinite (e.g. endless swipe until the end of the page)',\n ),\n});\n\nexport type ActionSwipeParam = {\n start?: LocateResultElement;\n direction?: 'up' | 'down' | 'left' | 'right';\n distance?: number;\n end?: LocateResultElement;\n duration?: number;\n repeat?: number;\n};\n\nexport function normalizeMobileSwipeParam(\n param: ActionSwipeParam,\n screenSize: { width: number; height: number },\n): {\n startPoint: { x: number; y: number };\n endPoint: { x: number; y: number };\n duration: number;\n repeatCount: number;\n} {\n const { width, height } = screenSize;\n const { start, end } = param;\n\n const startPoint = start\n ? { x: start.center[0], y: start.center[1] }\n : { x: width / 2, y: height / 2 };\n\n let endPoint: { x: number; y: number };\n\n if (end) {\n endPoint = { x: end.center[0], y: end.center[1] };\n } else if (param.distance) {\n const direction = param.direction;\n if (!direction) {\n throw new Error('direction is required for swipe gesture');\n }\n endPoint = {\n x:\n startPoint.x +\n (direction === 'right'\n ? param.distance\n : direction === 'left'\n ? -param.distance\n : 0),\n y:\n startPoint.y +\n (direction === 'down'\n ? param.distance\n : direction === 'up'\n ? -param.distance\n : 0),\n };\n } else {\n throw new Error(\n 'Either end or distance must be specified for swipe gesture',\n );\n }\n\n endPoint.x = Math.max(0, Math.min(endPoint.x, width));\n endPoint.y = Math.max(0, Math.min(endPoint.y, height));\n\n const duration = param.duration ?? 300;\n\n let repeatCount = typeof param.repeat === 'number' ? param.repeat : 1;\n if (repeatCount === 0) {\n repeatCount = 10;\n }\n\n return { startPoint, endPoint, duration, repeatCount };\n}\n\nexport const defineActionSwipe = (\n call: (param: ActionSwipeParam) => Promise<void>,\n): DeviceAction<ActionSwipeParam> => {\n return defineAction<typeof ActionSwipeParamSchema, ActionSwipeParam>({\n name: 'Swipe',\n description:\n 'Perform a touch gesture for interactions beyond regular scrolling (e.g., flip pages in a carousel, dismiss a notification, swipe-to-delete a list item). For regular content scrolling, use Scroll instead. Use \"distance\" + \"direction\" for relative movement, or \"end\" for precise endpoint.',\n paramSchema: ActionSwipeParamSchema,\n call,\n });\n};\n\n// ClearInput\nexport const actionClearInputParamSchema = z.object({\n locate: getMidsceneLocationSchema()\n .describe('The input field to be cleared')\n .optional(),\n});\nexport type ActionClearInputParam = {\n locate?: LocateResultElement;\n};\n\nexport const defineActionClearInput = (\n call: (param: ActionClearInputParam) => Promise<void>,\n): DeviceAction<ActionClearInputParam> => {\n return defineAction<\n typeof actionClearInputParamSchema,\n ActionClearInputParam\n >({\n name: 'ClearInput',\n description: inputLocateDescription,\n interfaceAlias: 'aiClearInput',\n paramSchema: actionClearInputParamSchema,\n call,\n });\n};\n\n// CursorMove\nexport const actionCursorMoveParamSchema = z.object({\n direction: z\n .enum(['left', 'right'])\n .describe('The direction to move the cursor'),\n times: z\n .number()\n .int()\n .min(1)\n .default(1)\n .describe(\n 'The number of times to move the cursor in the specified direction',\n ),\n});\nexport type ActionCursorMoveParam = {\n direction: 'left' | 'right';\n times?: number;\n};\n\nexport const defineActionCursorMove = (\n call: (param: ActionCursorMoveParam) => Promise<void>,\n): DeviceAction<ActionCursorMoveParam> => {\n return defineAction<\n typeof actionCursorMoveParamSchema,\n ActionCursorMoveParam\n >({\n name: 'CursorMove',\n description:\n 'Move the text cursor (caret) left or right within an input field or text area. Use this to reposition the cursor without selecting text.',\n paramSchema: actionCursorMoveParamSchema,\n call,\n });\n};\n// Sleep\nexport const ActionSleepParamSchema = z.object({\n timeMs: z\n .number()\n .default(1000)\n .optional()\n .describe('Sleep duration in milliseconds, defaults to 1000ms (1 second)'),\n});\n\nexport type ActionSleepParam = {\n timeMs?: number;\n};\n\nexport const defineActionSleep = (): DeviceAction<ActionSleepParam> => {\n return defineAction<typeof ActionSleepParamSchema, ActionSleepParam>({\n name: 'Sleep',\n description:\n 'Wait for a specified duration before continuing. Defaults to 1 second (1000ms) if not specified.',\n paramSchema: ActionSleepParamSchema,\n call: async (param) => {\n const duration = param?.timeMs ?? 1000;\n getDebug('device:common-action')(`Sleeping for ${duration}ms`);\n await new Promise((resolve) => setTimeout(resolve, duration));\n },\n });\n};\n\nexport type { DeviceAction } from '../types';\nexport type {\n AndroidDeviceOpt,\n AndroidDeviceInputOpt,\n IOSDeviceOpt,\n IOSDeviceInputOpt,\n} from './device-options';\n"],"names":["AbstractInterface","defineAction","config","actionTapParamSchema","z","getMidsceneLocationSchema","defineActionTap","call","actionRightClickParamSchema","defineActionRightClick","actionDoubleClickParamSchema","defineActionDoubleClick","actionHoverParamSchema","defineActionHover","inputLocateDescription","actionInputParamSchema","val","String","defineActionInput","param","actionKeyboardPressParamSchema","defineActionKeyboardPress","actionScrollParamSchema","defineActionScroll","actionDragAndDropParamSchema","defineActionDragAndDrop","ActionLongPressParamSchema","defineActionLongPress","ActionSwipeParamSchema","normalizeMobileSwipeParam","screenSize","width","height","start","end","startPoint","endPoint","direction","Error","Math","duration","repeatCount","defineActionSwipe","actionClearInputParamSchema","defineActionClearInput","actionCursorMoveParamSchema","defineActionCursorMove","ActionSleepParamSchema","defineActionSleep","getDebug","Promise","resolve","setTimeout"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"device/index.mjs","sources":["../../../src/device/index.ts"],"sourcesContent":["import { getMidsceneLocationSchema } from '@/common';\nimport type {\n ActionScrollParam,\n DeviceAction,\n LocateResultElement,\n} from '@/types';\nimport type { IModelConfig } from '@midscene/shared/env';\nimport type { ElementNode } from '@midscene/shared/extractor';\nimport { getDebug } from '@midscene/shared/logger';\nimport { _keyDefinitions } from '@midscene/shared/us-keyboard-layout';\nimport { z } from 'zod';\nimport type { ElementCacheFeature, Rect, Size, UIContext } from '../types';\n\nexport interface FileChooserHandler {\n accept(files: string[]): Promise<void>;\n}\n\nexport abstract class AbstractInterface {\n abstract interfaceType: string;\n\n abstract screenshotBase64(): Promise<string>;\n abstract size(): Promise<Size>;\n abstract actionSpace(): DeviceAction[];\n\n abstract cacheFeatureForPoint?(\n center: [number, number],\n options?: {\n targetDescription?: string;\n modelConfig?: IModelConfig;\n },\n ): Promise<ElementCacheFeature>;\n abstract rectMatchesCacheFeature?(\n feature: ElementCacheFeature,\n ): Promise<Rect>;\n\n abstract destroy?(): Promise<void>;\n\n abstract describe?(): string;\n abstract beforeInvokeAction?(actionName: string, param: any): Promise<void>;\n abstract afterInvokeAction?(actionName: string, param: any): Promise<void>;\n\n // for web only\n registerFileChooserListener?(\n handler: (chooser: FileChooserHandler) => Promise<void>,\n ): Promise<{ dispose: () => void; getError: () => Error | undefined }>;\n\n // @deprecated do NOT extend this method\n abstract getElementsNodeTree?: () => Promise<ElementNode>;\n\n // @deprecated do NOT extend this method\n abstract url?: () => string | Promise<string>;\n\n // @deprecated do NOT extend this method\n abstract evaluateJavaScript?<T = any>(script: string): Promise<T>;\n\n /**\n * Get the current time from the device.\n * Returns the device's current timestamp in milliseconds.\n * This is useful when the system time and device time are not synchronized.\n */\n getTimestamp?(): Promise<number>;\n\n /** URL of native MJPEG stream for real-time screen preview (e.g. WDA MJPEG server) */\n mjpegStreamUrl?: string;\n}\n\n// Generic function to define actions with proper type inference\n// TRuntime allows specifying a different type for the runtime parameter (after location resolution)\n// TReturn allows specifying the return type of the action\nexport const defineAction = <\n TSchema extends z.ZodType | undefined = undefined,\n TRuntime = TSchema extends z.ZodType ? z.infer<TSchema> : undefined,\n TReturn = any,\n>(\n config: {\n name: string;\n description: string;\n interfaceAlias?: string;\n paramSchema?: TSchema;\n call: (param: TRuntime) => Promise<TReturn> | TReturn;\n } & Partial<\n Omit<\n DeviceAction<TRuntime, TReturn>,\n 'name' | 'description' | 'interfaceAlias' | 'paramSchema' | 'call'\n >\n >,\n): DeviceAction<TRuntime, TReturn> => {\n return config as any; // Type assertion needed because schema validation type differs from runtime type\n};\n\n// Tap\nexport const actionTapParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe('The element to be tapped'),\n});\nexport type ActionTapParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionTap = (\n call: (param: ActionTapParam) => Promise<void>,\n): DeviceAction<ActionTapParam> => {\n return defineAction<typeof actionTapParamSchema, ActionTapParam>({\n name: 'Tap',\n description: 'Tap the element',\n interfaceAlias: 'aiTap',\n paramSchema: actionTapParamSchema,\n call,\n });\n};\n\n// RightClick\nexport const actionRightClickParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe(\n 'The element to be right clicked',\n ),\n});\nexport type ActionRightClickParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionRightClick = (\n call: (param: ActionRightClickParam) => Promise<void>,\n): DeviceAction<ActionRightClickParam> => {\n return defineAction<\n typeof actionRightClickParamSchema,\n ActionRightClickParam\n >({\n name: 'RightClick',\n description: 'Right click the element',\n interfaceAlias: 'aiRightClick',\n paramSchema: actionRightClickParamSchema,\n call,\n });\n};\n\n// DoubleClick\nexport const actionDoubleClickParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe(\n 'The element to be double clicked',\n ),\n});\nexport type ActionDoubleClickParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionDoubleClick = (\n call: (param: ActionDoubleClickParam) => Promise<void>,\n): DeviceAction<ActionDoubleClickParam> => {\n return defineAction<\n typeof actionDoubleClickParamSchema,\n ActionDoubleClickParam\n >({\n name: 'DoubleClick',\n description: 'Double click the element',\n interfaceAlias: 'aiDoubleClick',\n paramSchema: actionDoubleClickParamSchema,\n call,\n });\n};\n\n// Hover\nexport const actionHoverParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe('The element to be hovered'),\n});\nexport type ActionHoverParam = {\n locate: LocateResultElement;\n};\n\nexport const defineActionHover = (\n call: (param: ActionHoverParam) => Promise<void>,\n): DeviceAction<ActionHoverParam> => {\n return defineAction<typeof actionHoverParamSchema, ActionHoverParam>({\n name: 'Hover',\n description: 'Move the mouse to the element',\n interfaceAlias: 'aiHover',\n paramSchema: actionHoverParamSchema,\n call,\n });\n};\n\n// Input\nconst inputLocateDescription =\n 'the position of the placeholder or text content in the target input field. If there is no content, locate the center of the input field.';\nexport const actionInputParamSchema = z.object({\n value: z\n .union([z.string(), z.number()])\n .transform((val) => String(val))\n .describe(\n 'The text to input. Provide the final content for replace/append modes, or an empty string when using clear mode to remove existing text.',\n ),\n locate: getMidsceneLocationSchema()\n .describe(inputLocateDescription)\n .optional(),\n mode: z\n .enum(['replace', 'clear', 'typeOnly'])\n .default('replace')\n .describe(\n 'Input mode: \"replace\" (default) - clear the field and input the value; \"typeOnly\" - type the value directly without clearing the field first; \"clear\" - clear the field without inputting new text.',\n ),\n});\nexport type ActionInputParam = {\n value: string;\n locate?: LocateResultElement;\n mode?: 'replace' | 'clear' | 'typeOnly' | 'append';\n};\n\nexport const defineActionInput = (\n call: (param: ActionInputParam) => Promise<void>,\n): DeviceAction<ActionInputParam> => {\n return defineAction<typeof actionInputParamSchema, ActionInputParam>({\n name: 'Input',\n description: 'Input the value into the element',\n interfaceAlias: 'aiInput',\n paramSchema: actionInputParamSchema,\n call: (param) => {\n // backward compat: convert deprecated 'append' to 'typeOnly'\n if ((param.mode as string) === 'append') {\n param.mode = 'typeOnly';\n }\n return call(param);\n },\n });\n};\n\n// KeyboardPress\nexport const actionKeyboardPressParamSchema = z.object({\n locate: getMidsceneLocationSchema()\n .describe('The element to be clicked before pressing the key')\n .optional(),\n keyName: z\n .string()\n .describe(\n \"The key to be pressed. Use '+' for key combinations, e.g., 'Control+A', 'Shift+Enter'\",\n ),\n});\nexport type ActionKeyboardPressParam = {\n locate?: LocateResultElement;\n keyName: string;\n};\n\nexport const defineActionKeyboardPress = (\n call: (param: ActionKeyboardPressParam) => Promise<void>,\n): DeviceAction<ActionKeyboardPressParam> => {\n return defineAction<\n typeof actionKeyboardPressParamSchema,\n ActionKeyboardPressParam\n >({\n name: 'KeyboardPress',\n description:\n 'Press a key or key combination, like \"Enter\", \"Tab\", \"Escape\", or \"Control+A\", \"Shift+Enter\". Do not use this to type text.',\n interfaceAlias: 'aiKeyboardPress',\n paramSchema: actionKeyboardPressParamSchema,\n call,\n });\n};\n\n// Scroll\nexport const actionScrollParamSchema = z.object({\n scrollType: z\n .enum([\n 'singleAction',\n 'scrollToBottom',\n 'scrollToTop',\n 'scrollToRight',\n 'scrollToLeft',\n ])\n .default('singleAction')\n .describe(\n 'The scroll behavior: \"singleAction\" for a single scroll action, \"scrollToBottom\" for scrolling all the way to the bottom by rapidly scrolling 5-10 times (skipping intermediate content until reaching the bottom), \"scrollToTop\" for scrolling all the way to the top by rapidly scrolling 5-10 times (skipping intermediate content until reaching the top), \"scrollToRight\" for scrolling all the way to the right by rapidly scrolling multiple times, \"scrollToLeft\" for scrolling all the way to the left by rapidly scrolling multiple times',\n ),\n direction: z\n .enum(['down', 'up', 'right', 'left'])\n .default('down')\n .describe(\n 'The direction to scroll. Only effective when scrollType is \"singleAction\".',\n ),\n distance: z\n .number()\n .nullable()\n .optional()\n .describe('The distance in pixels to scroll'),\n locate: getMidsceneLocationSchema()\n .optional()\n .describe(\n 'Describe the target element to be scrolled on, like \"the table\" or \"the list\" or \"the content area\" or \"the scrollable area\". Do NOT provide a general intent like \"scroll to find some element\"',\n ),\n});\n\nexport const defineActionScroll = (\n call: (param: ActionScrollParam) => Promise<void>,\n): DeviceAction<ActionScrollParam> => {\n return defineAction<typeof actionScrollParamSchema, ActionScrollParam>({\n name: 'Scroll',\n description:\n 'Scroll the page or a scrollable element to browse content. This is the preferred way to scroll on all platforms, including mobile. Supports scrollToBottom/scrollToTop for boundary navigation. Default: direction `down`, scrollType `singleAction`, distance `null`.',\n interfaceAlias: 'aiScroll',\n paramSchema: actionScrollParamSchema,\n call,\n });\n};\n\n// DragAndDrop\nexport const actionDragAndDropParamSchema = z.object({\n from: getMidsceneLocationSchema().describe('The position to be dragged'),\n to: getMidsceneLocationSchema().describe('The position to be dropped'),\n});\nexport type ActionDragAndDropParam = {\n from: LocateResultElement;\n to: LocateResultElement;\n};\n\nexport const defineActionDragAndDrop = (\n call: (param: ActionDragAndDropParam) => Promise<void>,\n): DeviceAction<ActionDragAndDropParam> => {\n return defineAction<\n typeof actionDragAndDropParamSchema,\n ActionDragAndDropParam\n >({\n name: 'DragAndDrop',\n description:\n 'Pick up a specific UI element and move it to a new position (e.g., reorder a card, move a file into a folder, sort list items). The element itself moves with your finger/mouse.',\n interfaceAlias: 'aiDragAndDrop',\n paramSchema: actionDragAndDropParamSchema,\n call,\n });\n};\n\nexport const ActionLongPressParamSchema = z.object({\n locate: getMidsceneLocationSchema().describe(\n 'The element to be long pressed',\n ),\n duration: z\n .number()\n .default(500)\n .optional()\n .describe('Long press duration in milliseconds'),\n});\n\nexport type ActionLongPressParam = {\n locate: LocateResultElement;\n duration?: number;\n};\nexport const defineActionLongPress = (\n call: (param: ActionLongPressParam) => Promise<void>,\n): DeviceAction<ActionLongPressParam> => {\n return defineAction<typeof ActionLongPressParamSchema, ActionLongPressParam>({\n name: 'LongPress',\n description: 'Long press the element',\n paramSchema: ActionLongPressParamSchema,\n call,\n });\n};\n\nexport const ActionSwipeParamSchema = z.object({\n start: getMidsceneLocationSchema()\n .optional()\n .describe(\n 'Starting point of the swipe gesture, if not specified, the center of the page will be used',\n ),\n direction: z\n .enum(['up', 'down', 'left', 'right'])\n .optional()\n .describe(\n 'The direction to swipe (required when using distance). The direction means the direction of the finger swipe.',\n ),\n distance: z\n .number()\n .optional()\n .describe('The distance in pixels to swipe (mutually exclusive with end)'),\n end: getMidsceneLocationSchema()\n .optional()\n .describe(\n 'Ending point of the swipe gesture (mutually exclusive with distance)',\n ),\n duration: z\n .number()\n .default(300)\n .describe('Duration of the swipe gesture in milliseconds'),\n repeat: z\n .number()\n .optional()\n .describe(\n 'The number of times to repeat the swipe gesture. 1 for default, 0 for infinite (e.g. endless swipe until the end of the page)',\n ),\n});\n\nexport type ActionSwipeParam = {\n start?: LocateResultElement;\n direction?: 'up' | 'down' | 'left' | 'right';\n distance?: number;\n end?: LocateResultElement;\n duration?: number;\n repeat?: number;\n};\n\nexport function normalizeMobileSwipeParam(\n param: ActionSwipeParam,\n screenSize: { width: number; height: number },\n): {\n startPoint: { x: number; y: number };\n endPoint: { x: number; y: number };\n duration: number;\n repeatCount: number;\n} {\n const { width, height } = screenSize;\n const { start, end } = param;\n\n const startPoint = start\n ? { x: start.center[0], y: start.center[1] }\n : { x: width / 2, y: height / 2 };\n\n let endPoint: { x: number; y: number };\n\n if (end) {\n endPoint = { x: end.center[0], y: end.center[1] };\n } else if (param.distance) {\n const direction = param.direction;\n if (!direction) {\n throw new Error('direction is required for swipe gesture');\n }\n endPoint = {\n x:\n startPoint.x +\n (direction === 'right'\n ? param.distance\n : direction === 'left'\n ? -param.distance\n : 0),\n y:\n startPoint.y +\n (direction === 'down'\n ? param.distance\n : direction === 'up'\n ? -param.distance\n : 0),\n };\n } else {\n throw new Error(\n 'Either end or distance must be specified for swipe gesture',\n );\n }\n\n endPoint.x = Math.max(0, Math.min(endPoint.x, width));\n endPoint.y = Math.max(0, Math.min(endPoint.y, height));\n\n const duration = param.duration ?? 300;\n\n let repeatCount = typeof param.repeat === 'number' ? param.repeat : 1;\n if (repeatCount === 0) {\n repeatCount = 10;\n }\n\n return { startPoint, endPoint, duration, repeatCount };\n}\n\nexport const defineActionSwipe = (\n call: (param: ActionSwipeParam) => Promise<void>,\n): DeviceAction<ActionSwipeParam> => {\n return defineAction<typeof ActionSwipeParamSchema, ActionSwipeParam>({\n name: 'Swipe',\n description:\n 'Perform a touch gesture for interactions beyond regular scrolling (e.g., flip pages in a carousel, dismiss a notification, swipe-to-delete a list item). For regular content scrolling, use Scroll instead. Use \"distance\" + \"direction\" for relative movement, or \"end\" for precise endpoint.',\n paramSchema: ActionSwipeParamSchema,\n call,\n });\n};\n\n// ClearInput\nexport const actionClearInputParamSchema = z.object({\n locate: getMidsceneLocationSchema()\n .describe('The input field to be cleared')\n .optional(),\n});\nexport type ActionClearInputParam = {\n locate?: LocateResultElement;\n};\n\nexport const defineActionClearInput = (\n call: (param: ActionClearInputParam) => Promise<void>,\n): DeviceAction<ActionClearInputParam> => {\n return defineAction<\n typeof actionClearInputParamSchema,\n ActionClearInputParam\n >({\n name: 'ClearInput',\n description: inputLocateDescription,\n interfaceAlias: 'aiClearInput',\n paramSchema: actionClearInputParamSchema,\n call,\n });\n};\n\n// CursorMove\nexport const actionCursorMoveParamSchema = z.object({\n direction: z\n .enum(['left', 'right'])\n .describe('The direction to move the cursor'),\n times: z\n .number()\n .int()\n .min(1)\n .default(1)\n .describe(\n 'The number of times to move the cursor in the specified direction',\n ),\n});\nexport type ActionCursorMoveParam = {\n direction: 'left' | 'right';\n times?: number;\n};\n\nexport const defineActionCursorMove = (\n call: (param: ActionCursorMoveParam) => Promise<void>,\n): DeviceAction<ActionCursorMoveParam> => {\n return defineAction<\n typeof actionCursorMoveParamSchema,\n ActionCursorMoveParam\n >({\n name: 'CursorMove',\n description:\n 'Move the text cursor (caret) left or right within an input field or text area. Use this to reposition the cursor without selecting text.',\n paramSchema: actionCursorMoveParamSchema,\n call,\n });\n};\n// Sleep\nexport const ActionSleepParamSchema = z.object({\n timeMs: z\n .number()\n .default(1000)\n .optional()\n .describe('Sleep duration in milliseconds, defaults to 1000ms (1 second)'),\n});\n\nexport type ActionSleepParam = {\n timeMs?: number;\n};\n\nexport const defineActionSleep = (): DeviceAction<ActionSleepParam> => {\n return defineAction<typeof ActionSleepParamSchema, ActionSleepParam>({\n name: 'Sleep',\n description:\n 'Wait for a specified duration before continuing. Defaults to 1 second (1000ms) if not specified.',\n paramSchema: ActionSleepParamSchema,\n call: async (param) => {\n const duration = param?.timeMs ?? 1000;\n getDebug('device:common-action')(`Sleeping for ${duration}ms`);\n await new Promise((resolve) => setTimeout(resolve, duration));\n },\n });\n};\n\nexport type { DeviceAction } from '../types';\nexport type {\n AndroidDeviceOpt,\n AndroidDeviceInputOpt,\n IOSDeviceOpt,\n IOSDeviceInputOpt,\n} from './device-options';\n"],"names":["AbstractInterface","defineAction","config","actionTapParamSchema","z","getMidsceneLocationSchema","defineActionTap","call","actionRightClickParamSchema","defineActionRightClick","actionDoubleClickParamSchema","defineActionDoubleClick","actionHoverParamSchema","defineActionHover","inputLocateDescription","actionInputParamSchema","val","String","defineActionInput","param","actionKeyboardPressParamSchema","defineActionKeyboardPress","actionScrollParamSchema","defineActionScroll","actionDragAndDropParamSchema","defineActionDragAndDrop","ActionLongPressParamSchema","defineActionLongPress","ActionSwipeParamSchema","normalizeMobileSwipeParam","screenSize","width","height","start","end","startPoint","endPoint","direction","Error","Math","duration","repeatCount","defineActionSwipe","actionClearInputParamSchema","defineActionClearInput","actionCursorMoveParamSchema","defineActionCursorMove","ActionSleepParamSchema","defineActionSleep","getDebug","Promise","resolve","setTimeout"],"mappings":"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"}
|
|
@@ -193,11 +193,12 @@ function parseDumpScriptAttributes(html) {
|
|
|
193
193
|
function generateImageScriptTag(id, data) {
|
|
194
194
|
return '<script type="midscene-image" data-id="' + id + '">' + escapeContent(data) + "<\/script>";
|
|
195
195
|
}
|
|
196
|
+
const BASE_URL_FIX_SCRIPT = '\n<script>(function(){var p=window.location.pathname;if(p.endsWith("/")||/\\.\\w+$/.test(p))return;var b=document.createElement("base");b.href=p+"/";document.head.insertBefore(b,document.head.firstChild)})()<\\/script>\n';
|
|
196
197
|
function generateDumpScriptTag(json, attributes) {
|
|
197
198
|
let attrString = '';
|
|
198
199
|
if (attributes && Object.keys(attributes).length > 0) attrString = ' ' + Object.entries(attributes).map(([k, v])=>k + '="' + encodeURIComponent(v) + '"').join(' ');
|
|
199
200
|
return '<script type="midscene_web_dump"' + attrString + '>' + escapeContent(json) + "<\/script>";
|
|
200
201
|
}
|
|
201
|
-
export { STREAMING_CHUNK_SIZE, escapeContent, extractImageByIdSync, extractLastDumpScriptSync, generateDumpScriptTag, generateImageScriptTag, parseDumpScript, parseDumpScriptAttributes, parseImageScripts, streamImageScriptsToFile, streamScanTags, unescapeContent };
|
|
202
|
+
export { BASE_URL_FIX_SCRIPT, STREAMING_CHUNK_SIZE, escapeContent, extractImageByIdSync, extractLastDumpScriptSync, generateDumpScriptTag, generateImageScriptTag, parseDumpScript, parseDumpScriptAttributes, parseImageScripts, streamImageScriptsToFile, streamScanTags, unescapeContent };
|
|
202
203
|
|
|
203
204
|
//# sourceMappingURL=html-utils.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dump/html-utils.mjs","sources":["../../../src/dump/html-utils.ts"],"sourcesContent":["import { closeSync, openSync, readSync, statSync } from 'node:fs';\nimport { antiEscapeScriptTag, escapeScriptTag } from '@midscene/shared/utils';\n\nexport const escapeContent = escapeScriptTag;\nexport const unescapeContent = antiEscapeScriptTag;\n\n/** Chunk size for streaming file operations (64KB) */\nexport const STREAMING_CHUNK_SIZE = 64 * 1024;\n\n/**\n * Callback for processing matched tags during streaming.\n * @param content - The content between open and close tags\n * @returns true to stop scanning, false to continue\n */\ntype TagMatchCallback = (content: string) => boolean;\n\n/**\n * Stream through a file and find tags matching the pattern.\n * Memory usage: O(chunk_size + tag_size), not O(file_size).\n *\n * @param filePath - Absolute path to the file\n * @param openTag - Opening tag to search for\n * @param closeTag - Closing tag\n * @param onMatch - Callback for each matched tag content\n */\nexport function streamScanTags(\n filePath: string,\n openTag: string,\n closeTag: string,\n onMatch: TagMatchCallback,\n): void {\n const fd = openSync(filePath, 'r');\n const fileSize = statSync(filePath).size;\n const buffer = Buffer.alloc(STREAMING_CHUNK_SIZE);\n\n let position = 0;\n let leftover = '';\n let capturing = false;\n let currentContent = '';\n\n try {\n while (position < fileSize) {\n const bytesRead = readSync(fd, buffer, 0, STREAMING_CHUNK_SIZE, position);\n const chunk = leftover + buffer.toString('utf-8', 0, bytesRead);\n position += bytesRead;\n\n let searchStart = 0;\n\n while (searchStart < chunk.length) {\n if (!capturing) {\n const startIdx = chunk.indexOf(openTag, searchStart);\n if (startIdx !== -1) {\n capturing = true;\n currentContent = chunk.slice(startIdx + openTag.length);\n const endIdx = currentContent.indexOf(closeTag);\n if (endIdx !== -1) {\n const shouldStop = onMatch(currentContent.slice(0, endIdx));\n if (shouldStop) return;\n capturing = false;\n currentContent = '';\n searchStart =\n startIdx + openTag.length + endIdx + closeTag.length;\n } else {\n leftover = currentContent.slice(-closeTag.length);\n currentContent = currentContent.slice(0, -closeTag.length);\n break;\n }\n } else {\n leftover = chunk.slice(-openTag.length);\n break;\n }\n } else {\n const endIdx = chunk.indexOf(closeTag, searchStart);\n if (endIdx !== -1) {\n currentContent += chunk.slice(searchStart, endIdx);\n const shouldStop = onMatch(currentContent);\n if (shouldStop) return;\n capturing = false;\n currentContent = '';\n searchStart = endIdx + closeTag.length;\n } else {\n currentContent += chunk.slice(searchStart, -closeTag.length);\n leftover = chunk.slice(-closeTag.length);\n break;\n }\n }\n }\n }\n } finally {\n closeSync(fd);\n }\n}\n\n/**\n * Synchronously extract a specific image's base64 data from an HTML file by its id.\n * Uses streaming to avoid loading the entire file into memory.\n *\n * @param htmlPath - Absolute path to the HTML file\n * @param imageId - The id of the image to extract\n * @returns The base64 data string, or null if not found\n */\nexport function extractImageByIdSync(\n htmlPath: string,\n imageId: string,\n): string | null {\n const targetTag = `<script type=\"midscene-image\" data-id=\"${imageId}\">`;\n const closeTag = '</script>';\n\n let result: string | null = null;\n\n streamScanTags(htmlPath, targetTag, closeTag, (content) => {\n result = unescapeContent(content);\n return true; // Stop after first match\n });\n\n return result;\n}\n\n/**\n * Stream image script tags from source file directly to output file.\n * Memory usage: O(single_image_size), not O(all_images_size).\n *\n * @param srcFilePath - Source HTML file path\n * @param destFilePath - Destination file path to append to\n */\nexport function streamImageScriptsToFile(\n srcFilePath: string,\n destFilePath: string,\n): void {\n const { appendFileSync } = require('node:fs');\n const openTag = '<script type=\"midscene-image\"';\n const closeTag = '</script>';\n\n streamScanTags(srcFilePath, openTag, closeTag, (content) => {\n // Write complete tag immediately to destination, don't accumulate\n appendFileSync(destFilePath, `${openTag}${content}${closeTag}\\n`);\n return false; // Continue scanning for more tags\n });\n}\n\n/**\n * Extract the LAST dump script content from HTML file using streaming.\n * Memory usage: O(dump_size), not O(file_size).\n *\n * @param filePath - Absolute path to the HTML file\n * @returns The dump script content (trimmed), or empty string if not found\n */\nexport function extractLastDumpScriptSync(filePath: string): string {\n const openTagPrefix = '<script type=\"midscene_web_dump\"';\n const closeTag = '</script>';\n\n let lastContent = '';\n\n // Custom streaming to handle the special case where open tag has variable attributes\n const fd = openSync(filePath, 'r');\n const fileSize = statSync(filePath).size;\n const buffer = Buffer.alloc(STREAMING_CHUNK_SIZE);\n\n let position = 0;\n let leftover = '';\n let capturing = false;\n let currentContent = '';\n\n try {\n while (position < fileSize) {\n const bytesRead = readSync(fd, buffer, 0, STREAMING_CHUNK_SIZE, position);\n const chunk = leftover + buffer.toString('utf-8', 0, bytesRead);\n position += bytesRead;\n\n let searchStart = 0;\n\n while (searchStart < chunk.length) {\n if (!capturing) {\n const startIdx = chunk.indexOf(openTagPrefix, searchStart);\n if (startIdx !== -1) {\n // Find the end of the opening tag (the '>' character)\n const tagEndIdx = chunk.indexOf('>', startIdx);\n if (tagEndIdx !== -1) {\n capturing = true;\n currentContent = chunk.slice(tagEndIdx + 1);\n const endIdx = currentContent.indexOf(closeTag);\n if (endIdx !== -1) {\n lastContent = currentContent.slice(0, endIdx).trim();\n capturing = false;\n currentContent = '';\n searchStart = tagEndIdx + 1 + endIdx + closeTag.length;\n } else {\n leftover = currentContent.slice(-closeTag.length);\n currentContent = currentContent.slice(0, -closeTag.length);\n break;\n }\n } else {\n leftover = chunk.slice(startIdx);\n break;\n }\n } else {\n leftover = chunk.slice(-openTagPrefix.length);\n break;\n }\n } else {\n const endIdx = chunk.indexOf(closeTag, searchStart);\n if (endIdx !== -1) {\n currentContent += chunk.slice(searchStart, endIdx);\n lastContent = currentContent.trim();\n capturing = false;\n currentContent = '';\n searchStart = endIdx + closeTag.length;\n } else {\n currentContent += chunk.slice(searchStart, -closeTag.length);\n leftover = chunk.slice(-closeTag.length);\n break;\n }\n }\n }\n }\n } finally {\n closeSync(fd);\n }\n\n return lastContent;\n}\n\nexport function parseImageScripts(html: string): Record<string, string> {\n const imageMap: Record<string, string> = {};\n const regex =\n /<script type=\"midscene-image\" data-id=\"([^\"]+)\">([\\s\\S]*?)<\\/script>/g;\n\n for (const match of html.matchAll(regex)) {\n const [, id, content] = match;\n imageMap[id] = unescapeContent(content);\n }\n\n return imageMap;\n}\n\nexport function parseDumpScript(html: string): string {\n // Use string search instead of regex to avoid ReDoS vulnerability\n // Find the LAST dump script tag (template may contain similar patterns in bundled JS)\n const scriptOpenTag = '<script type=\"midscene_web_dump\"';\n const scriptCloseTag = '</script>';\n\n // Find the last occurrence of the opening tag\n const lastOpenIndex = html.lastIndexOf(scriptOpenTag);\n if (lastOpenIndex === -1) {\n throw new Error('No dump script found in HTML');\n }\n\n // Find the end of the opening tag (the '>' character)\n const tagEndIndex = html.indexOf('>', lastOpenIndex);\n if (tagEndIndex === -1) {\n throw new Error('No dump script found in HTML');\n }\n\n // Find the closing tag after the opening tag\n const closeIndex = html.indexOf(scriptCloseTag, tagEndIndex);\n if (closeIndex === -1) {\n throw new Error('No dump script found in HTML');\n }\n\n // Extract content between opening and closing tags\n const content = html.substring(tagEndIndex + 1, closeIndex);\n return unescapeContent(content);\n}\n\nexport function parseDumpScriptAttributes(\n html: string,\n): Record<string, string> {\n const regex = /<script type=\"midscene_web_dump\"([^>]*)>/;\n const match = regex.exec(html);\n\n if (!match) {\n return {};\n }\n\n const attrString = match[1];\n const attributes: Record<string, string> = {};\n const attrRegex = /(\\w+)=\"([^\"]*)\"/g;\n\n for (const attrMatch of attrString.matchAll(attrRegex)) {\n const [, key, value] = attrMatch;\n if (key !== 'type') {\n attributes[key] = decodeURIComponent(value);\n }\n }\n\n return attributes;\n}\n\nexport function generateImageScriptTag(id: string, data: string): string {\n // Do not use template string here, will cause bundle error with <script\n return (\n // biome-ignore lint/style/useTemplate: <explanation>\n '<script type=\"midscene-image\" data-id=\"' +\n id +\n '\">' +\n escapeContent(data) +\n '</script>'\n );\n}\n\nexport function generateDumpScriptTag(\n json: string,\n attributes?: Record<string, string>,\n): string {\n let attrString = '';\n if (attributes && Object.keys(attributes).length > 0) {\n // Do not use template string here, will cause bundle error with <script\n attrString =\n // biome-ignore lint/style/useTemplate: <explanation>\n ' ' +\n Object.entries(attributes)\n // biome-ignore lint/style/useTemplate: <explanation>\n .map(([k, v]) => k + '=\"' + encodeURIComponent(v) + '\"')\n .join(' ');\n }\n\n // Do not use template string here, will cause bundle error with <script\n return (\n // biome-ignore lint/style/useTemplate: <explanation>\n '<script type=\"midscene_web_dump\"' +\n attrString +\n '>' +\n escapeContent(json) +\n '</script>'\n );\n}\n"],"names":["escapeContent","escapeScriptTag","unescapeContent","antiEscapeScriptTag","STREAMING_CHUNK_SIZE","streamScanTags","filePath","openTag","closeTag","onMatch","fd","openSync","fileSize","statSync","buffer","Buffer","position","leftover","capturing","currentContent","bytesRead","readSync","chunk","searchStart","endIdx","shouldStop","startIdx","closeSync","extractImageByIdSync","htmlPath","imageId","targetTag","result","content","streamImageScriptsToFile","srcFilePath","destFilePath","appendFileSync","require","extractLastDumpScriptSync","openTagPrefix","lastContent","tagEndIdx","parseImageScripts","html","imageMap","regex","match","id","parseDumpScript","scriptOpenTag","scriptCloseTag","lastOpenIndex","Error","tagEndIndex","closeIndex","parseDumpScriptAttributes","attrString","attributes","attrRegex","attrMatch","key","value","decodeURIComponent","generateImageScriptTag","data","generateDumpScriptTag","json","Object","k","v","encodeURIComponent"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"dump/html-utils.mjs","sources":["../../../src/dump/html-utils.ts"],"sourcesContent":["import { closeSync, openSync, readSync, statSync } from 'node:fs';\nimport { antiEscapeScriptTag, escapeScriptTag } from '@midscene/shared/utils';\n\nexport const escapeContent = escapeScriptTag;\nexport const unescapeContent = antiEscapeScriptTag;\n\n/** Chunk size for streaming file operations (64KB) */\nexport const STREAMING_CHUNK_SIZE = 64 * 1024;\n\n/**\n * Callback for processing matched tags during streaming.\n * @param content - The content between open and close tags\n * @returns true to stop scanning, false to continue\n */\ntype TagMatchCallback = (content: string) => boolean;\n\n/**\n * Stream through a file and find tags matching the pattern.\n * Memory usage: O(chunk_size + tag_size), not O(file_size).\n *\n * @param filePath - Absolute path to the file\n * @param openTag - Opening tag to search for\n * @param closeTag - Closing tag\n * @param onMatch - Callback for each matched tag content\n */\nexport function streamScanTags(\n filePath: string,\n openTag: string,\n closeTag: string,\n onMatch: TagMatchCallback,\n): void {\n const fd = openSync(filePath, 'r');\n const fileSize = statSync(filePath).size;\n const buffer = Buffer.alloc(STREAMING_CHUNK_SIZE);\n\n let position = 0;\n let leftover = '';\n let capturing = false;\n let currentContent = '';\n\n try {\n while (position < fileSize) {\n const bytesRead = readSync(fd, buffer, 0, STREAMING_CHUNK_SIZE, position);\n const chunk = leftover + buffer.toString('utf-8', 0, bytesRead);\n position += bytesRead;\n\n let searchStart = 0;\n\n while (searchStart < chunk.length) {\n if (!capturing) {\n const startIdx = chunk.indexOf(openTag, searchStart);\n if (startIdx !== -1) {\n capturing = true;\n currentContent = chunk.slice(startIdx + openTag.length);\n const endIdx = currentContent.indexOf(closeTag);\n if (endIdx !== -1) {\n const shouldStop = onMatch(currentContent.slice(0, endIdx));\n if (shouldStop) return;\n capturing = false;\n currentContent = '';\n searchStart =\n startIdx + openTag.length + endIdx + closeTag.length;\n } else {\n leftover = currentContent.slice(-closeTag.length);\n currentContent = currentContent.slice(0, -closeTag.length);\n break;\n }\n } else {\n leftover = chunk.slice(-openTag.length);\n break;\n }\n } else {\n const endIdx = chunk.indexOf(closeTag, searchStart);\n if (endIdx !== -1) {\n currentContent += chunk.slice(searchStart, endIdx);\n const shouldStop = onMatch(currentContent);\n if (shouldStop) return;\n capturing = false;\n currentContent = '';\n searchStart = endIdx + closeTag.length;\n } else {\n currentContent += chunk.slice(searchStart, -closeTag.length);\n leftover = chunk.slice(-closeTag.length);\n break;\n }\n }\n }\n }\n } finally {\n closeSync(fd);\n }\n}\n\n/**\n * Synchronously extract a specific image's base64 data from an HTML file by its id.\n * Uses streaming to avoid loading the entire file into memory.\n *\n * @param htmlPath - Absolute path to the HTML file\n * @param imageId - The id of the image to extract\n * @returns The base64 data string, or null if not found\n */\nexport function extractImageByIdSync(\n htmlPath: string,\n imageId: string,\n): string | null {\n const targetTag = `<script type=\"midscene-image\" data-id=\"${imageId}\">`;\n const closeTag = '</script>';\n\n let result: string | null = null;\n\n streamScanTags(htmlPath, targetTag, closeTag, (content) => {\n result = unescapeContent(content);\n return true; // Stop after first match\n });\n\n return result;\n}\n\n/**\n * Stream image script tags from source file directly to output file.\n * Memory usage: O(single_image_size), not O(all_images_size).\n *\n * @param srcFilePath - Source HTML file path\n * @param destFilePath - Destination file path to append to\n */\nexport function streamImageScriptsToFile(\n srcFilePath: string,\n destFilePath: string,\n): void {\n const { appendFileSync } = require('node:fs');\n const openTag = '<script type=\"midscene-image\"';\n const closeTag = '</script>';\n\n streamScanTags(srcFilePath, openTag, closeTag, (content) => {\n // Write complete tag immediately to destination, don't accumulate\n appendFileSync(destFilePath, `${openTag}${content}${closeTag}\\n`);\n return false; // Continue scanning for more tags\n });\n}\n\n/**\n * Extract the LAST dump script content from HTML file using streaming.\n * Memory usage: O(dump_size), not O(file_size).\n *\n * @param filePath - Absolute path to the HTML file\n * @returns The dump script content (trimmed), or empty string if not found\n */\nexport function extractLastDumpScriptSync(filePath: string): string {\n const openTagPrefix = '<script type=\"midscene_web_dump\"';\n const closeTag = '</script>';\n\n let lastContent = '';\n\n // Custom streaming to handle the special case where open tag has variable attributes\n const fd = openSync(filePath, 'r');\n const fileSize = statSync(filePath).size;\n const buffer = Buffer.alloc(STREAMING_CHUNK_SIZE);\n\n let position = 0;\n let leftover = '';\n let capturing = false;\n let currentContent = '';\n\n try {\n while (position < fileSize) {\n const bytesRead = readSync(fd, buffer, 0, STREAMING_CHUNK_SIZE, position);\n const chunk = leftover + buffer.toString('utf-8', 0, bytesRead);\n position += bytesRead;\n\n let searchStart = 0;\n\n while (searchStart < chunk.length) {\n if (!capturing) {\n const startIdx = chunk.indexOf(openTagPrefix, searchStart);\n if (startIdx !== -1) {\n // Find the end of the opening tag (the '>' character)\n const tagEndIdx = chunk.indexOf('>', startIdx);\n if (tagEndIdx !== -1) {\n capturing = true;\n currentContent = chunk.slice(tagEndIdx + 1);\n const endIdx = currentContent.indexOf(closeTag);\n if (endIdx !== -1) {\n lastContent = currentContent.slice(0, endIdx).trim();\n capturing = false;\n currentContent = '';\n searchStart = tagEndIdx + 1 + endIdx + closeTag.length;\n } else {\n leftover = currentContent.slice(-closeTag.length);\n currentContent = currentContent.slice(0, -closeTag.length);\n break;\n }\n } else {\n leftover = chunk.slice(startIdx);\n break;\n }\n } else {\n leftover = chunk.slice(-openTagPrefix.length);\n break;\n }\n } else {\n const endIdx = chunk.indexOf(closeTag, searchStart);\n if (endIdx !== -1) {\n currentContent += chunk.slice(searchStart, endIdx);\n lastContent = currentContent.trim();\n capturing = false;\n currentContent = '';\n searchStart = endIdx + closeTag.length;\n } else {\n currentContent += chunk.slice(searchStart, -closeTag.length);\n leftover = chunk.slice(-closeTag.length);\n break;\n }\n }\n }\n }\n } finally {\n closeSync(fd);\n }\n\n return lastContent;\n}\n\nexport function parseImageScripts(html: string): Record<string, string> {\n const imageMap: Record<string, string> = {};\n const regex =\n /<script type=\"midscene-image\" data-id=\"([^\"]+)\">([\\s\\S]*?)<\\/script>/g;\n\n for (const match of html.matchAll(regex)) {\n const [, id, content] = match;\n imageMap[id] = unescapeContent(content);\n }\n\n return imageMap;\n}\n\nexport function parseDumpScript(html: string): string {\n // Use string search instead of regex to avoid ReDoS vulnerability\n // Find the LAST dump script tag (template may contain similar patterns in bundled JS)\n const scriptOpenTag = '<script type=\"midscene_web_dump\"';\n const scriptCloseTag = '</script>';\n\n // Find the last occurrence of the opening tag\n const lastOpenIndex = html.lastIndexOf(scriptOpenTag);\n if (lastOpenIndex === -1) {\n throw new Error('No dump script found in HTML');\n }\n\n // Find the end of the opening tag (the '>' character)\n const tagEndIndex = html.indexOf('>', lastOpenIndex);\n if (tagEndIndex === -1) {\n throw new Error('No dump script found in HTML');\n }\n\n // Find the closing tag after the opening tag\n const closeIndex = html.indexOf(scriptCloseTag, tagEndIndex);\n if (closeIndex === -1) {\n throw new Error('No dump script found in HTML');\n }\n\n // Extract content between opening and closing tags\n const content = html.substring(tagEndIndex + 1, closeIndex);\n return unescapeContent(content);\n}\n\nexport function parseDumpScriptAttributes(\n html: string,\n): Record<string, string> {\n const regex = /<script type=\"midscene_web_dump\"([^>]*)>/;\n const match = regex.exec(html);\n\n if (!match) {\n return {};\n }\n\n const attrString = match[1];\n const attributes: Record<string, string> = {};\n const attrRegex = /(\\w+)=\"([^\"]*)\"/g;\n\n for (const attrMatch of attrString.matchAll(attrRegex)) {\n const [, key, value] = attrMatch;\n if (key !== 'type') {\n attributes[key] = decodeURIComponent(value);\n }\n }\n\n return attributes;\n}\n\nexport function generateImageScriptTag(id: string, data: string): string {\n // Do not use template string here, will cause bundle error with <script\n return (\n // biome-ignore lint/style/useTemplate: <explanation>\n '<script type=\"midscene-image\" data-id=\"' +\n id +\n '\">' +\n escapeContent(data) +\n '</script>'\n );\n}\n\n/**\n * Inline script that fixes relative URL resolution for directory-mode reports.\n *\n * Problem: when a static server (e.g. `npx serve`) serves `name/index.html`\n * at URL `/name` (without trailing slash), relative paths like\n * `./screenshots/xxx.png` resolve to `/screenshots/xxx.png` instead of\n * `/name/screenshots/xxx.png`.\n *\n * Fix: dynamically insert a <base> tag so relative URLs resolve correctly.\n */\n// Do not use template string here, will cause bundle error with <script\n// Use <\\/script> to avoid breaking HTML when this string is embedded inside a <script> tag\nexport const BASE_URL_FIX_SCRIPT =\n '\\n<script>(function(){' +\n 'var p=window.location.pathname;' +\n 'if(p.endsWith(\"/\")||/\\\\.\\\\w+$/.test(p))return;' +\n 'var b=document.createElement(\"base\");' +\n 'b.href=p+\"/\";' +\n 'document.head.insertBefore(b,document.head.firstChild)' +\n '})()<\\\\/script>\\n';\n\nexport function generateDumpScriptTag(\n json: string,\n attributes?: Record<string, string>,\n): string {\n let attrString = '';\n if (attributes && Object.keys(attributes).length > 0) {\n // Do not use template string here, will cause bundle error with <script\n attrString =\n // biome-ignore lint/style/useTemplate: <explanation>\n ' ' +\n Object.entries(attributes)\n // biome-ignore lint/style/useTemplate: <explanation>\n .map(([k, v]) => k + '=\"' + encodeURIComponent(v) + '\"')\n .join(' ');\n }\n\n // Do not use template string here, will cause bundle error with <script\n return (\n // biome-ignore lint/style/useTemplate: <explanation>\n '<script type=\"midscene_web_dump\"' +\n attrString +\n '>' +\n escapeContent(json) +\n '</script>'\n );\n}\n"],"names":["escapeContent","escapeScriptTag","unescapeContent","antiEscapeScriptTag","STREAMING_CHUNK_SIZE","streamScanTags","filePath","openTag","closeTag","onMatch","fd","openSync","fileSize","statSync","buffer","Buffer","position","leftover","capturing","currentContent","bytesRead","readSync","chunk","searchStart","endIdx","shouldStop","startIdx","closeSync","extractImageByIdSync","htmlPath","imageId","targetTag","result","content","streamImageScriptsToFile","srcFilePath","destFilePath","appendFileSync","require","extractLastDumpScriptSync","openTagPrefix","lastContent","tagEndIdx","parseImageScripts","html","imageMap","regex","match","id","parseDumpScript","scriptOpenTag","scriptCloseTag","lastOpenIndex","Error","tagEndIndex","closeIndex","parseDumpScriptAttributes","attrString","attributes","attrRegex","attrMatch","key","value","decodeURIComponent","generateImageScriptTag","data","BASE_URL_FIX_SCRIPT","generateDumpScriptTag","json","Object","k","v","encodeURIComponent"],"mappings":"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"}
|
package/dist/es/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","sources":["../../src/index.ts"],"sourcesContent":["import { z } from 'zod';\nimport Service from './service/index';\nimport { TaskRunner } from './task-runner';\nimport { getVersion } from './utils';\n\nexport {\n plan,\n AiLocateElement,\n getMidsceneLocationSchema,\n
|
|
1
|
+
{"version":3,"file":"index.mjs","sources":["../../src/index.ts"],"sourcesContent":["import { z } from 'zod';\nimport Service from './service/index';\nimport { TaskRunner } from './task-runner';\nimport { getVersion } from './utils';\n\nexport {\n plan,\n AiLocateElement,\n getMidsceneLocationSchema,\n PointSchema,\n SizeSchema,\n RectSchema,\n TMultimodalPromptSchema,\n TUserPromptSchema,\n type TMultimodalPrompt,\n type TUserPrompt,\n} from './ai-model/index';\n\nexport {\n MIDSCENE_MODEL_NAME,\n type CreateOpenAIClientFn,\n} from '@midscene/shared/env';\n\nexport type * from './types';\nexport {\n ServiceError,\n ExecutionDump,\n GroupedActionDump,\n type IExecutionDump,\n type IGroupedActionDump,\n} from './types';\n\nexport { z };\n\nexport default Service;\nexport { TaskRunner, Service, getVersion };\n\nexport type {\n MidsceneYamlScript,\n MidsceneYamlTask,\n MidsceneYamlFlowItem,\n MidsceneYamlConfigResult,\n MidsceneYamlConfig,\n MidsceneYamlScriptWebEnv,\n MidsceneYamlScriptAndroidEnv,\n MidsceneYamlScriptIOSEnv,\n MidsceneYamlScriptEnv,\n LocateOption,\n DetailedLocateParam,\n} from './yaml';\n\nexport { Agent, type AgentOpt, type AiActOptions, createAgent } from './agent';\n\n// Dump utilities\nexport {\n restoreImageReferences,\n escapeContent,\n unescapeContent,\n parseImageScripts,\n parseDumpScript,\n parseDumpScriptAttributes,\n generateImageScriptTag,\n generateDumpScriptTag,\n} from './dump';\n\n// Report generator\nexport type { IReportGenerator } from './report-generator';\nexport { ReportGenerator, nullReportGenerator } from './report-generator';\n\n// ScreenshotItem\nexport { ScreenshotItem } from './screenshot-item';\n"],"names":["Service"],"mappings":";;;;;;;;;;;AAkCA,YAAeA"}
|
|
@@ -3,7 +3,7 @@ import { dirname, join } from "node:path";
|
|
|
3
3
|
import { getMidsceneRunSubDir } from "@midscene/shared/common";
|
|
4
4
|
import { MIDSCENE_REPORT_QUIET, globalConfigManager } from "@midscene/shared/env";
|
|
5
5
|
import { ifInBrowser, logMsg } from "@midscene/shared/utils";
|
|
6
|
-
import { generateDumpScriptTag, generateImageScriptTag } from "./dump/html-utils.mjs";
|
|
6
|
+
import { BASE_URL_FIX_SCRIPT, generateDumpScriptTag, generateImageScriptTag } from "./dump/html-utils.mjs";
|
|
7
7
|
import { appendFileSync, getReportTpl } from "./utils.mjs";
|
|
8
8
|
function _define_property(obj, key, value) {
|
|
9
9
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
@@ -111,7 +111,7 @@ class ReportGenerator {
|
|
|
111
111
|
screenshot.markPersistedToPath(`./screenshots/${screenshot.id}.${ext}`, absolutePath);
|
|
112
112
|
}
|
|
113
113
|
const serialized = dump.serialize();
|
|
114
|
-
writeFileSync(this.reportPath, `${getReportTpl()}
|
|
114
|
+
writeFileSync(this.reportPath, `${getReportTpl()}${BASE_URL_FIX_SCRIPT}${generateDumpScriptTag(serialized)}`);
|
|
115
115
|
}
|
|
116
116
|
constructor(options){
|
|
117
117
|
_define_property(this, "reportPath", void 0);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"report-generator.mjs","sources":["../../src/report-generator.ts"],"sourcesContent":["import {\n existsSync,\n mkdirSync,\n statSync,\n truncateSync,\n writeFileSync,\n} from 'node:fs';\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 } from '@midscene/shared/utils';\nimport {\n generateDumpScriptTag,\n generateImageScriptTag,\n} from './dump/html-utils';\nimport type { GroupedActionDump } from './types';\nimport { appendFileSync, getReportTpl } from './utils';\n\nexport interface IReportGenerator {\n /**\n * Schedule a dump update. Writes are queued internally to guarantee serial execution.\n * This method returns immediately (fire-and-forget).\n * Screenshots are written and memory is released during this call.\n */\n onDumpUpdate(dump: GroupedActionDump): void;\n /**\n * Wait for all queued write operations to complete.\n */\n flush(): Promise<void>;\n /**\n * Finalize the report. Calls flush() internally.\n */\n finalize(dump: GroupedActionDump): Promise<string | undefined>;\n getReportPath(): string | undefined;\n}\n\nexport const nullReportGenerator: IReportGenerator = {\n onDumpUpdate: () => {},\n flush: async () => {},\n finalize: async () => undefined,\n getReportPath: () => undefined,\n};\n\nexport class ReportGenerator implements IReportGenerator {\n private reportPath: string;\n private screenshotMode: 'inline' | 'directory';\n private autoPrint: boolean;\n private writtenScreenshots = new Set<string>();\n private firstWriteDone = false;\n\n // inline mode state\n private imageEndOffset = 0;\n private initialized = 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: 'inline' | 'directory';\n autoPrint?: boolean;\n }) {\n this.reportPath = options.reportPath;\n this.screenshotMode = options.screenshotMode;\n this.autoPrint = options.autoPrint ?? true;\n this.printReportPath('will be generated at');\n }\n\n static create(\n reportFileName: string,\n opts: {\n generateReport?: boolean;\n outputFormat?: 'single-html' | 'html-and-external-assets';\n autoPrintReportMsg?: boolean;\n },\n ): IReportGenerator {\n if (opts.generateReport === false) return nullReportGenerator;\n\n // In browser environment, file system is not available\n if (ifInBrowser) return nullReportGenerator;\n\n if (opts.outputFormat === 'html-and-external-assets') {\n const outputDir = join(getMidsceneRunSubDir('report'), reportFileName);\n return new ReportGenerator({\n reportPath: join(outputDir, 'index.html'),\n screenshotMode: 'directory',\n autoPrint: opts.autoPrintReportMsg,\n });\n }\n\n return new ReportGenerator({\n reportPath: join(\n getMidsceneRunSubDir('report'),\n `${reportFileName}.html`,\n ),\n screenshotMode: 'inline',\n autoPrint: opts.autoPrintReportMsg,\n });\n }\n\n onDumpUpdate(dump: GroupedActionDump): void {\n this.writeQueue = this.writeQueue.then(() => {\n if (this.destroyed) return;\n this.doWrite(dump);\n });\n }\n\n async flush(): Promise<void> {\n await this.writeQueue;\n }\n\n async finalize(dump: GroupedActionDump): Promise<string | undefined> {\n this.onDumpUpdate(dump);\n await this.flush();\n this.destroyed = true;\n this.printReportPath('finalized');\n\n return this.reportPath;\n }\n\n getReportPath(): string | undefined {\n return this.reportPath;\n }\n\n private printReportPath(verb: string): 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 ${verb}: npx serve ${dirname(this.reportPath)}`,\n );\n } else {\n logMsg(`Midscene - report ${verb}: ${this.reportPath}`);\n }\n }\n\n private doWrite(dump: GroupedActionDump): void {\n if (this.screenshotMode === 'inline') {\n this.writeInlineReport(dump);\n } else {\n this.writeDirectoryReport(dump);\n }\n if (!this.firstWriteDone) {\n this.firstWriteDone = true;\n this.printReportPath('generated');\n }\n }\n\n private writeInlineReport(dump: GroupedActionDump): void {\n const dir = dirname(this.reportPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n if (!this.initialized) {\n writeFileSync(this.reportPath, getReportTpl());\n this.imageEndOffset = statSync(this.reportPath).size;\n this.initialized = true;\n }\n\n // 1. truncate: remove old dump JSON, keep template + existing image tags\n truncateSync(this.reportPath, this.imageEndOffset);\n\n // 2. append new image tags and release memory immediately after writing\n // Screenshots can be recovered from HTML file via lazy loading\n const screenshots = dump.collectAllScreenshots();\n for (const screenshot of screenshots) {\n if (!this.writtenScreenshots.has(screenshot.id)) {\n appendFileSync(\n this.reportPath,\n `\\n${generateImageScriptTag(screenshot.id, screenshot.base64)}`,\n );\n this.writtenScreenshots.add(screenshot.id);\n // Release memory - screenshot can be recovered via extractImageByIdSync\n screenshot.markPersistedInline(this.reportPath);\n }\n }\n\n // 3. update image end offset\n this.imageEndOffset = statSync(this.reportPath).size;\n\n // 4. append new dump JSON (compact { $screenshot: id } format)\n const serialized = dump.serialize();\n appendFileSync(this.reportPath, `\\n${generateDumpScriptTag(serialized)}`);\n }\n\n private writeDirectoryReport(dump: GroupedActionDump): void {\n const dir = dirname(this.reportPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // create screenshots subdirectory\n const screenshotsDir = join(dir, 'screenshots');\n if (!existsSync(screenshotsDir)) {\n mkdirSync(screenshotsDir, { recursive: true });\n }\n\n // 1. write new screenshots and release memory immediately\n // Screenshots can be recovered from disk via lazy loading\n const screenshots = dump.collectAllScreenshots();\n for (const screenshot of screenshots) {\n if (!this.writtenScreenshots.has(screenshot.id)) {\n const ext = screenshot.extension;\n const absolutePath = join(screenshotsDir, `${screenshot.id}.${ext}`);\n const buffer = Buffer.from(screenshot.rawBase64, 'base64');\n writeFileSync(absolutePath, buffer);\n this.writtenScreenshots.add(screenshot.id);\n screenshot.markPersistedToPath(\n `./screenshots/${screenshot.id}.${ext}`,\n absolutePath,\n );\n }\n }\n\n // 2. write HTML with dump JSON (toSerializable() returns { $screenshot: id } format)\n const serialized = dump.serialize();\n writeFileSync(\n this.reportPath,\n `${getReportTpl()}\\n${generateDumpScriptTag(serialized)}`,\n );\n }\n}\n"],"names":["nullReportGenerator","undefined","ReportGenerator","reportFileName","opts","ifInBrowser","outputDir","join","getMidsceneRunSubDir","dump","verb","globalConfigManager","MIDSCENE_REPORT_QUIET","logMsg","dirname","dir","existsSync","mkdirSync","writeFileSync","getReportTpl","statSync","truncateSync","screenshots","screenshot","appendFileSync","generateImageScriptTag","serialized","generateDumpScriptTag","screenshotsDir","ext","absolutePath","buffer","Buffer","options","Set","Promise"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"report-generator.mjs","sources":["../../src/report-generator.ts"],"sourcesContent":["import {\n existsSync,\n mkdirSync,\n statSync,\n truncateSync,\n writeFileSync,\n} from 'node:fs';\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 } from '@midscene/shared/utils';\nimport {\n BASE_URL_FIX_SCRIPT,\n generateDumpScriptTag,\n generateImageScriptTag,\n} from './dump/html-utils';\nimport type { GroupedActionDump } from './types';\nimport { appendFileSync, getReportTpl } from './utils';\n\nexport interface IReportGenerator {\n /**\n * Schedule a dump update. Writes are queued internally to guarantee serial execution.\n * This method returns immediately (fire-and-forget).\n * Screenshots are written and memory is released during this call.\n */\n onDumpUpdate(dump: GroupedActionDump): void;\n /**\n * Wait for all queued write operations to complete.\n */\n flush(): Promise<void>;\n /**\n * Finalize the report. Calls flush() internally.\n */\n finalize(dump: GroupedActionDump): Promise<string | undefined>;\n getReportPath(): string | undefined;\n}\n\nexport const nullReportGenerator: IReportGenerator = {\n onDumpUpdate: () => {},\n flush: async () => {},\n finalize: async () => undefined,\n getReportPath: () => undefined,\n};\n\nexport class ReportGenerator implements IReportGenerator {\n private reportPath: string;\n private screenshotMode: 'inline' | 'directory';\n private autoPrint: boolean;\n private writtenScreenshots = new Set<string>();\n private firstWriteDone = false;\n\n // inline mode state\n private imageEndOffset = 0;\n private initialized = 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: 'inline' | 'directory';\n autoPrint?: boolean;\n }) {\n this.reportPath = options.reportPath;\n this.screenshotMode = options.screenshotMode;\n this.autoPrint = options.autoPrint ?? true;\n this.printReportPath('will be generated at');\n }\n\n static create(\n reportFileName: string,\n opts: {\n generateReport?: boolean;\n outputFormat?: 'single-html' | 'html-and-external-assets';\n autoPrintReportMsg?: boolean;\n },\n ): IReportGenerator {\n if (opts.generateReport === false) return nullReportGenerator;\n\n // In browser environment, file system is not available\n if (ifInBrowser) return nullReportGenerator;\n\n if (opts.outputFormat === 'html-and-external-assets') {\n const outputDir = join(getMidsceneRunSubDir('report'), reportFileName);\n return new ReportGenerator({\n reportPath: join(outputDir, 'index.html'),\n screenshotMode: 'directory',\n autoPrint: opts.autoPrintReportMsg,\n });\n }\n\n return new ReportGenerator({\n reportPath: join(\n getMidsceneRunSubDir('report'),\n `${reportFileName}.html`,\n ),\n screenshotMode: 'inline',\n autoPrint: opts.autoPrintReportMsg,\n });\n }\n\n onDumpUpdate(dump: GroupedActionDump): void {\n this.writeQueue = this.writeQueue.then(() => {\n if (this.destroyed) return;\n this.doWrite(dump);\n });\n }\n\n async flush(): Promise<void> {\n await this.writeQueue;\n }\n\n async finalize(dump: GroupedActionDump): Promise<string | undefined> {\n this.onDumpUpdate(dump);\n await this.flush();\n this.destroyed = true;\n this.printReportPath('finalized');\n\n return this.reportPath;\n }\n\n getReportPath(): string | undefined {\n return this.reportPath;\n }\n\n private printReportPath(verb: string): 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 ${verb}: npx serve ${dirname(this.reportPath)}`,\n );\n } else {\n logMsg(`Midscene - report ${verb}: ${this.reportPath}`);\n }\n }\n\n private doWrite(dump: GroupedActionDump): void {\n if (this.screenshotMode === 'inline') {\n this.writeInlineReport(dump);\n } else {\n this.writeDirectoryReport(dump);\n }\n if (!this.firstWriteDone) {\n this.firstWriteDone = true;\n this.printReportPath('generated');\n }\n }\n\n private writeInlineReport(dump: GroupedActionDump): void {\n const dir = dirname(this.reportPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n if (!this.initialized) {\n writeFileSync(this.reportPath, getReportTpl());\n this.imageEndOffset = statSync(this.reportPath).size;\n this.initialized = true;\n }\n\n // 1. truncate: remove old dump JSON, keep template + existing image tags\n truncateSync(this.reportPath, this.imageEndOffset);\n\n // 2. append new image tags and release memory immediately after writing\n // Screenshots can be recovered from HTML file via lazy loading\n const screenshots = dump.collectAllScreenshots();\n for (const screenshot of screenshots) {\n if (!this.writtenScreenshots.has(screenshot.id)) {\n appendFileSync(\n this.reportPath,\n `\\n${generateImageScriptTag(screenshot.id, screenshot.base64)}`,\n );\n this.writtenScreenshots.add(screenshot.id);\n // Release memory - screenshot can be recovered via extractImageByIdSync\n screenshot.markPersistedInline(this.reportPath);\n }\n }\n\n // 3. update image end offset\n this.imageEndOffset = statSync(this.reportPath).size;\n\n // 4. append new dump JSON (compact { $screenshot: id } format)\n const serialized = dump.serialize();\n appendFileSync(this.reportPath, `\\n${generateDumpScriptTag(serialized)}`);\n }\n\n private writeDirectoryReport(dump: GroupedActionDump): void {\n const dir = dirname(this.reportPath);\n if (!existsSync(dir)) {\n mkdirSync(dir, { recursive: true });\n }\n\n // create screenshots subdirectory\n const screenshotsDir = join(dir, 'screenshots');\n if (!existsSync(screenshotsDir)) {\n mkdirSync(screenshotsDir, { recursive: true });\n }\n\n // 1. write new screenshots and release memory immediately\n // Screenshots can be recovered from disk via lazy loading\n const screenshots = dump.collectAllScreenshots();\n for (const screenshot of screenshots) {\n if (!this.writtenScreenshots.has(screenshot.id)) {\n const ext = screenshot.extension;\n const absolutePath = join(screenshotsDir, `${screenshot.id}.${ext}`);\n const buffer = Buffer.from(screenshot.rawBase64, 'base64');\n writeFileSync(absolutePath, buffer);\n this.writtenScreenshots.add(screenshot.id);\n screenshot.markPersistedToPath(\n `./screenshots/${screenshot.id}.${ext}`,\n absolutePath,\n );\n }\n }\n\n // 2. write HTML with dump JSON (toSerializable() returns { $screenshot: id } format)\n const serialized = dump.serialize();\n writeFileSync(\n this.reportPath,\n `${getReportTpl()}${BASE_URL_FIX_SCRIPT}${generateDumpScriptTag(serialized)}`,\n );\n }\n}\n"],"names":["nullReportGenerator","undefined","ReportGenerator","reportFileName","opts","ifInBrowser","outputDir","join","getMidsceneRunSubDir","dump","verb","globalConfigManager","MIDSCENE_REPORT_QUIET","logMsg","dirname","dir","existsSync","mkdirSync","writeFileSync","getReportTpl","statSync","truncateSync","screenshots","screenshot","appendFileSync","generateImageScriptTag","serialized","generateDumpScriptTag","screenshotsDir","ext","absolutePath","buffer","Buffer","BASE_URL_FIX_SCRIPT","options","Set","Promise"],"mappings":"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"}
|
package/dist/es/report.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, unlinkSync } from "node:fs";
|
|
2
|
-
import { resolve } from "node:path";
|
|
1
|
+
import { appendFileSync, copyFileSync, existsSync, mkdirSync, readdirSync, rmSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
3
3
|
import { getMidsceneRunSubDir } from "@midscene/shared/common";
|
|
4
4
|
import { logMsg } from "@midscene/shared/utils";
|
|
5
5
|
import { getReportFileName } from "./agent/index.mjs";
|
|
6
|
-
import { extractLastDumpScriptSync, streamImageScriptsToFile } from "./dump/html-utils.mjs";
|
|
6
|
+
import { BASE_URL_FIX_SCRIPT, extractLastDumpScriptSync, streamImageScriptsToFile } from "./dump/html-utils.mjs";
|
|
7
7
|
import { getReportTpl, reportHTMLContent } from "./utils.mjs";
|
|
8
8
|
function _define_property(obj, key, value) {
|
|
9
9
|
if (key in obj) Object.defineProperty(obj, key, {
|
|
@@ -22,6 +22,10 @@ class ReportMergingTool {
|
|
|
22
22
|
clear() {
|
|
23
23
|
this.reportInfos = [];
|
|
24
24
|
}
|
|
25
|
+
isDirectoryModeReport(reportFilePath) {
|
|
26
|
+
const reportDir = dirname(reportFilePath);
|
|
27
|
+
return 'index.html' === basename(reportFilePath) && existsSync(join(reportDir, 'screenshots'));
|
|
28
|
+
}
|
|
25
29
|
mergeReports(reportFileName = 'AUTO', opts) {
|
|
26
30
|
if (this.reportInfos.length <= 1) {
|
|
27
31
|
logMsg('Not enough reports to merge');
|
|
@@ -29,18 +33,40 @@ class ReportMergingTool {
|
|
|
29
33
|
}
|
|
30
34
|
const { rmOriginalReports = false, overwrite = false } = opts ?? {};
|
|
31
35
|
const targetDir = getMidsceneRunSubDir('report');
|
|
32
|
-
const
|
|
36
|
+
const hasDirectoryModeReport = this.reportInfos.some((info)=>this.isDirectoryModeReport(info.reportFilePath));
|
|
37
|
+
const resolvedName = 'AUTO' === reportFileName ? getReportFileName('merged-report') : reportFileName;
|
|
38
|
+
const outputFilePath = hasDirectoryModeReport ? resolve(targetDir, resolvedName, 'index.html') : resolve(targetDir, `${resolvedName}.html`);
|
|
33
39
|
if ('AUTO' !== reportFileName && existsSync(outputFilePath)) {
|
|
34
40
|
if (!overwrite) throw new Error(`Report file already exists: ${outputFilePath}\nSet overwrite to true to overwrite this file.`);
|
|
35
|
-
|
|
41
|
+
if (hasDirectoryModeReport) rmSync(dirname(outputFilePath), {
|
|
42
|
+
recursive: true,
|
|
43
|
+
force: true
|
|
44
|
+
});
|
|
45
|
+
else unlinkSync(outputFilePath);
|
|
36
46
|
}
|
|
47
|
+
if (hasDirectoryModeReport) mkdirSync(dirname(outputFilePath), {
|
|
48
|
+
recursive: true
|
|
49
|
+
});
|
|
37
50
|
logMsg(`Start merging ${this.reportInfos.length} reports...\nCreating template file...`);
|
|
38
51
|
try {
|
|
39
52
|
appendFileSync(outputFilePath, getReportTpl());
|
|
53
|
+
if (hasDirectoryModeReport) appendFileSync(outputFilePath, BASE_URL_FIX_SCRIPT);
|
|
40
54
|
for(let i = 0; i < this.reportInfos.length; i++){
|
|
41
55
|
const reportInfo = this.reportInfos[i];
|
|
42
56
|
logMsg(`Processing report ${i + 1}/${this.reportInfos.length}`);
|
|
43
|
-
|
|
57
|
+
if (this.isDirectoryModeReport(reportInfo.reportFilePath)) {
|
|
58
|
+
const reportDir = dirname(reportInfo.reportFilePath);
|
|
59
|
+
const screenshotsDir = join(reportDir, 'screenshots');
|
|
60
|
+
const mergedScreenshotsDir = join(dirname(outputFilePath), 'screenshots');
|
|
61
|
+
mkdirSync(mergedScreenshotsDir, {
|
|
62
|
+
recursive: true
|
|
63
|
+
});
|
|
64
|
+
for (const file of readdirSync(screenshotsDir)){
|
|
65
|
+
const src = join(screenshotsDir, file);
|
|
66
|
+
const dest = join(mergedScreenshotsDir, file);
|
|
67
|
+
copyFileSync(src, dest);
|
|
68
|
+
}
|
|
69
|
+
} else streamImageScriptsToFile(reportInfo.reportFilePath, outputFilePath);
|
|
44
70
|
const dumpString = extractLastDumpScriptSync(reportInfo.reportFilePath);
|
|
45
71
|
const { reportAttributes } = reportInfo;
|
|
46
72
|
const reportHtmlStr = `${reportHTMLContent({
|
|
@@ -58,7 +84,13 @@ class ReportMergingTool {
|
|
|
58
84
|
logMsg(`Successfully merged new report: ${outputFilePath}`);
|
|
59
85
|
if (rmOriginalReports) {
|
|
60
86
|
for (const info of this.reportInfos)try {
|
|
61
|
-
|
|
87
|
+
if (this.isDirectoryModeReport(info.reportFilePath)) {
|
|
88
|
+
const reportDir = dirname(info.reportFilePath);
|
|
89
|
+
rmSync(reportDir, {
|
|
90
|
+
recursive: true,
|
|
91
|
+
force: true
|
|
92
|
+
});
|
|
93
|
+
} else unlinkSync(info.reportFilePath);
|
|
62
94
|
} catch (error) {
|
|
63
95
|
logMsg(`Error deleting report ${info.reportFilePath}: ${error}`);
|
|
64
96
|
}
|