android-midscene-automation 0.1.0

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.
Files changed (78) hide show
  1. package/README.md +160 -0
  2. package/bin/android-midscene-automation.js +27 -0
  3. package/index.html +12 -0
  4. package/package.json +49 -0
  5. package/remote-agent/index.ts +206 -0
  6. package/server/appium-recorder/appium-runner.ts +427 -0
  7. package/server/appium-recorder/repository.ts +228 -0
  8. package/server/appium-recorder/routes.ts +219 -0
  9. package/server/config-store.ts +167 -0
  10. package/server/config.ts +130 -0
  11. package/server/device-locks/repository.ts +147 -0
  12. package/server/device-locks/service.ts +72 -0
  13. package/server/device-locks/types.ts +22 -0
  14. package/server/device-sessions/repository.ts +169 -0
  15. package/server/device-sessions/service.ts +59 -0
  16. package/server/device-sessions/types.ts +24 -0
  17. package/server/http-api.ts +1389 -0
  18. package/server/model-call-usage-importer.ts +108 -0
  19. package/server/model-tester.ts +104 -0
  20. package/server/model-usage-repository.ts +131 -0
  21. package/server/operations/repository.ts +218 -0
  22. package/server/operations/service.ts +84 -0
  23. package/server/operations/types.ts +27 -0
  24. package/server/paths.ts +27 -0
  25. package/server/remote-agents/protocol.ts +38 -0
  26. package/server/remote-agents/registry.ts +136 -0
  27. package/server/remote-agents/routes.ts +89 -0
  28. package/server/script-agent.ts +284 -0
  29. package/server/script-db.ts +281 -0
  30. package/server/script-runner.ts +551 -0
  31. package/server/storage/sqlite.ts +49 -0
  32. package/server/test-case-import/formatter.ts +28 -0
  33. package/server/test-case-import/parsers/excel.ts +69 -0
  34. package/server/test-case-import/parsers/txt.ts +11 -0
  35. package/server/test-case-import/parsers/word.ts +9 -0
  36. package/server/test-case-import/service.ts +58 -0
  37. package/server/test-case-import/text-normalizer.ts +98 -0
  38. package/server/test-case-import/types.ts +24 -0
  39. package/server/test-case-import/validator.ts +34 -0
  40. package/src/App.vue +1450 -0
  41. package/src/api.ts +290 -0
  42. package/src/appium-recorder/AppiumPage.vue +894 -0
  43. package/src/appium-recorder/api.ts +64 -0
  44. package/src/appium-recorder/components/ComponentTree.vue +44 -0
  45. package/src/appium-recorder/components/NodeDetail.vue +152 -0
  46. package/src/appium-recorder/components/RecordedSteps.vue +79 -0
  47. package/src/appium-recorder/tree.ts +129 -0
  48. package/src/appium-recorder/types.ts +88 -0
  49. package/src/assets/device-actions/back.svg +5 -0
  50. package/src/assets/device-actions/home.svg +3 -0
  51. package/src/assets/device-actions/power.svg +5 -0
  52. package/src/assets/device-actions/tasks.svg +3 -0
  53. package/src/assets/device-actions/volume-down.svg +3 -0
  54. package/src/assets/device-actions/volume-up.svg +3 -0
  55. package/src/components/config/ModelUsageChart.vue +188 -0
  56. package/src/components/device/DevicePreviewPanel.vue +266 -0
  57. package/src/components/generator/GeneratedCodePanel.vue +70 -0
  58. package/src/components/generator/TestCaseFileUpload.vue +97 -0
  59. package/src/config/midscene-model-presets.ts +75 -0
  60. package/src/config/prompt-example.ts +6 -0
  61. package/src/main.ts +7 -0
  62. package/src/pages/AiGeneratorPage.vue +90 -0
  63. package/src/pages/AutomationPage.vue +161 -0
  64. package/src/pages/ConfigPage.vue +273 -0
  65. package/src/pages/GeneratorPage.vue +97 -0
  66. package/src/pages/ManualStepsPage.vue +179 -0
  67. package/src/script-generator/codegen.ts +126 -0
  68. package/src/script-generator/index.ts +4 -0
  69. package/src/script-generator/presets.ts +37 -0
  70. package/src/script-generator/step-options.ts +52 -0
  71. package/src/script-generator/types.ts +27 -0
  72. package/src/style.css +1983 -0
  73. package/src/types.ts +157 -0
  74. package/src/vite-env.d.ts +1 -0
  75. package/tsconfig.app.json +8 -0
  76. package/tsconfig.json +11 -0
  77. package/tsconfig.node.json +16 -0
  78. package/vite.config.ts +28 -0
@@ -0,0 +1,11 @@
1
+ import { normalizeExtractedText } from '../text-normalizer';
2
+
3
+ // 优先按 UTF-8 解码;出现大量替换字符时兼容常见的 GB18030 中文文本。
4
+ export function parseTxt(buffer: Buffer) {
5
+ let text = buffer.toString('utf8');
6
+ const replacementCount = (text.match(/\uFFFD/g) || []).length;
7
+ if (replacementCount > 2) {
8
+ text = new TextDecoder('gb18030').decode(buffer);
9
+ }
10
+ return normalizeExtractedText(text);
11
+ }
@@ -0,0 +1,9 @@
1
+ import WordExtractor from 'word-extractor';
2
+ import { normalizeExtractedText } from '../text-normalizer';
3
+
4
+ // word-extractor 同时支持 OLE .doc 与 OOXML .docx,并直接从 Buffer 提取正文。
5
+ export async function parseWord(buffer: Buffer) {
6
+ const extractor = new WordExtractor();
7
+ const document = await extractor.extract(buffer);
8
+ return normalizeExtractedText(document.getBody());
9
+ }
@@ -0,0 +1,58 @@
1
+ import path from 'node:path';
2
+ import { formatTestCases } from './formatter';
3
+ import { parseExcel } from './parsers/excel';
4
+ import { parseTxt } from './parsers/txt';
5
+ import { parseWord } from './parsers/word';
6
+ import { parseTextCases } from './text-normalizer';
7
+ import type { ParsedTestCaseDocument, TestCaseImportResult, TestCaseSourceFormat } from './types';
8
+ import { validateTestCaseDocument } from './validator';
9
+
10
+ export const MAX_TEST_CASE_FILE_SIZE = 10 * 1024 * 1024;
11
+
12
+ const formatByExtension: Record<string, TestCaseSourceFormat> = {
13
+ '.txt': 'txt',
14
+ '.xls': 'excel',
15
+ '.xlsx': 'excel',
16
+ '.doc': 'word',
17
+ '.docx': 'word',
18
+ };
19
+
20
+ // 文件扩展名决定解析器;所有解析结果随后统一校验并格式化为 Prompt。
21
+ export async function importTestCaseFile(input: { fileName: string; buffer: Buffer }): Promise<TestCaseImportResult> {
22
+ const fileName = path.basename(input.fileName || '').trim();
23
+ const extension = path.extname(fileName).toLowerCase();
24
+ const format = formatByExtension[extension];
25
+
26
+ if (!fileName || !format) {
27
+ throw new Error('仅支持 txt、xls、xlsx、doc、docx 格式的测试用例文件');
28
+ }
29
+ if (!input.buffer.length) {
30
+ throw new Error('上传文件为空');
31
+ }
32
+ if (input.buffer.length > MAX_TEST_CASE_FILE_SIZE) {
33
+ throw new Error('文件大小不能超过 10MB');
34
+ }
35
+
36
+ let document: ParsedTestCaseDocument;
37
+ if (format === 'excel') {
38
+ const parsed = parseExcel(input.buffer);
39
+ document = { format, ...parsed };
40
+ } else {
41
+ const rawText = format === 'txt' ? parseTxt(input.buffer) : await parseWord(input.buffer);
42
+ document = {
43
+ format,
44
+ rawText,
45
+ cases: parseTextCases(rawText, fileName),
46
+ };
47
+ }
48
+
49
+ validateTestCaseDocument(document);
50
+ const cases = document.cases.length ? document.cases : parseTextCases(document.rawText, fileName);
51
+
52
+ return {
53
+ fileName,
54
+ format,
55
+ caseCount: cases.length,
56
+ prompt: formatTestCases(fileName, cases),
57
+ };
58
+ }
@@ -0,0 +1,98 @@
1
+ import path from 'node:path';
2
+ import type { ImportedTestCase } from './types';
3
+
4
+ type SectionName = 'preconditions' | 'steps' | 'expectedResults' | 'testData' | 'description';
5
+
6
+ const sectionPatterns: Array<{ section: SectionName; pattern: RegExp }> = [
7
+ { section: 'preconditions', pattern: /^(?:前置条件|前提条件|准备条件)\s*[::]?\s*(.*)$/i },
8
+ { section: 'steps', pattern: /^(?:测试步骤|操作步骤|执行步骤|步骤)\s*[::]?\s*(.*)$/i },
9
+ { section: 'expectedResults', pattern: /^(?:预期结果|期望结果|预期|expected\s*results?)\s*[::]?\s*(.*)$/i },
10
+ { section: 'testData', pattern: /^(?:测试数据|输入数据|数据)\s*[::]?\s*(.*)$/i },
11
+ ];
12
+
13
+ const caseTitlePattern = /^(?:#{1,3}\s*)?(?:测试)?(?:用例名称|用例标题|用例|场景标题|测试场景)\s*[::]\s*(.+)$/i;
14
+ const priorityPattern = /^(?:优先级|priority)\s*[::]\s*(.+)$/i;
15
+
16
+ export function normalizeExtractedText(value: string) {
17
+ return value
18
+ .replace(/^\uFEFF/, '')
19
+ .replace(/\r\n?/g, '\n')
20
+ .replace(/[\t\u00a0]+/g, ' ')
21
+ .replace(/[ ]{2,}/g, ' ')
22
+ .replace(/\n{3,}/g, '\n\n')
23
+ .trim();
24
+ }
25
+
26
+ function cleanListItem(value: string) {
27
+ return value.replace(/^\s*(?:[-*•]|\d+[.)、])\s*/, '').trim();
28
+ }
29
+
30
+ function createCase(title: string): ImportedTestCase {
31
+ return {
32
+ title,
33
+ preconditions: [],
34
+ steps: [],
35
+ expectedResults: [],
36
+ testData: [],
37
+ description: [],
38
+ priority: '',
39
+ };
40
+ }
41
+
42
+ // 将 TXT/Word 中常见的“字段标题 + 内容”结构转换为统一测试用例对象。
43
+ export function parseTextCases(rawText: string, fileName: string) {
44
+ const text = normalizeExtractedText(rawText);
45
+ const fallbackTitle = path.basename(fileName, path.extname(fileName)) || '导入测试用例';
46
+ const lines = text.split('\n').map((line) => line.trim()).filter(Boolean);
47
+ const cases: ImportedTestCase[] = [];
48
+ let currentCase = createCase(fallbackTitle);
49
+ let activeSection: SectionName = 'description';
50
+
51
+ const pushCurrentCase = () => {
52
+ const hasContent = [
53
+ ...currentCase.preconditions,
54
+ ...currentCase.steps,
55
+ ...currentCase.expectedResults,
56
+ ...currentCase.testData,
57
+ ...currentCase.description,
58
+ ].some(Boolean);
59
+ if (hasContent) cases.push(currentCase);
60
+ };
61
+
62
+ for (const line of lines) {
63
+ const titleMatch = line.match(caseTitlePattern);
64
+ if (titleMatch) {
65
+ pushCurrentCase();
66
+ currentCase = createCase(titleMatch[1]?.trim() || fallbackTitle);
67
+ activeSection = 'description';
68
+ continue;
69
+ }
70
+
71
+ const priorityMatch = line.match(priorityPattern);
72
+ if (priorityMatch) {
73
+ currentCase.priority = priorityMatch[1]?.trim() || '';
74
+ continue;
75
+ }
76
+
77
+ const sectionMatch = sectionPatterns.find(({ pattern }) => pattern.test(line));
78
+ if (sectionMatch) {
79
+ const match = line.match(sectionMatch.pattern);
80
+ activeSection = sectionMatch.section;
81
+ const inlineValue = cleanListItem(match?.[1] || '');
82
+ if (inlineValue) currentCase[activeSection].push(inlineValue);
83
+ continue;
84
+ }
85
+
86
+ const item = cleanListItem(line);
87
+ if (item) currentCase[activeSection].push(item);
88
+ }
89
+
90
+ pushCurrentCase();
91
+ return cases.length ? cases : [createCase(fallbackTitle)];
92
+ }
93
+
94
+ export function splitCellItems(value: unknown) {
95
+ const text = normalizeExtractedText(String(value ?? ''));
96
+ if (!text) return [];
97
+ return text.split('\n').map(cleanListItem).filter(Boolean);
98
+ }
@@ -0,0 +1,24 @@
1
+ export type TestCaseSourceFormat = 'txt' | 'excel' | 'word';
2
+
3
+ export type ImportedTestCase = {
4
+ title: string;
5
+ preconditions: string[];
6
+ steps: string[];
7
+ expectedResults: string[];
8
+ testData: string[];
9
+ description: string[];
10
+ priority: string;
11
+ };
12
+
13
+ export type ParsedTestCaseDocument = {
14
+ format: TestCaseSourceFormat;
15
+ rawText: string;
16
+ cases: ImportedTestCase[];
17
+ };
18
+
19
+ export type TestCaseImportResult = {
20
+ fileName: string;
21
+ format: TestCaseSourceFormat;
22
+ caseCount: number;
23
+ prompt: string;
24
+ };
@@ -0,0 +1,34 @@
1
+ import type { ParsedTestCaseDocument } from './types';
2
+
3
+ const MAX_EXTRACTED_TEXT_LENGTH = 100_000;
4
+ const structurePattern = /测试用例|用例名称|用例标题|测试场景|前置条件|测试步骤|操作步骤|预期结果|期望结果|test\s*case|precondition|expected/i;
5
+ const actionPattern = /打开|启动|进入|点击|选择|输入|填写|提交|登录|注册|搜索|切换|滑动|等待|关闭|允许|同意|检查|验证|操作|执行/i;
6
+ const assertionPattern = /预期|期望|结果|成功|失败|显示|出现|跳转|进入首页|提示|返回|应当|应该|不存在|可见/i;
7
+
8
+ function countMatches(text: string, pattern: RegExp) {
9
+ const globalPattern = new RegExp(pattern.source, `${pattern.flags.replace('g', '')}g`);
10
+ return (text.match(globalPattern) || []).length;
11
+ }
12
+
13
+ // 结构化用例优先通过;非结构化文本必须同时具备操作和结果语义,避免导入普通文章。
14
+ export function validateTestCaseDocument(document: ParsedTestCaseDocument) {
15
+ if (!document.rawText.trim()) {
16
+ throw new Error('文件内容为空,未读取到测试用例');
17
+ }
18
+ if (document.rawText.length > MAX_EXTRACTED_TEXT_LENGTH) {
19
+ throw new Error('测试用例内容过长,请控制在 10 万个字符以内');
20
+ }
21
+
22
+ const hasCanonicalCase = document.cases.some(
23
+ (item) => item.steps.length > 0 && item.expectedResults.length > 0,
24
+ );
25
+ const structureCount = countMatches(document.rawText, structurePattern);
26
+ const actionCount = countMatches(document.rawText, actionPattern);
27
+ const assertionCount = countMatches(document.rawText, assertionPattern);
28
+ const hasStructuredTestText = structureCount >= 2 && actionCount >= 1;
29
+ const hasBehaviorTestText = actionCount >= 2 && assertionCount >= 1 && document.rawText.length >= 20;
30
+
31
+ if (!hasCanonicalCase && !hasStructuredTestText && !hasBehaviorTestText) {
32
+ throw new Error('文件内容与测试用例无关,请上传包含测试步骤、操作动作和预期结果的用例文件');
33
+ }
34
+ }