android-midscene-automation 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/CHANGELOG.md +15 -1
  2. package/README.md +15 -1
  3. package/package.json +1 -1
  4. package/remote-agent/index.ts +50 -22
  5. package/server/analytics.test.ts +46 -0
  6. package/server/analytics.ts +24 -0
  7. package/server/api-safety-analytics.test.ts +57 -0
  8. package/server/appium-auto-save.test.ts +6 -0
  9. package/server/appium-recorder/appium-runner.ts +30 -9
  10. package/server/appium-recorder/appium-version.test.ts +55 -0
  11. package/server/appium-recorder/managed-appium.ts +37 -1
  12. package/server/appium-recorder/managed-cleanup.test.ts +38 -8
  13. package/server/appium-recorder/replay-lifecycle.test.ts +123 -0
  14. package/server/appium-recorder/routes.ts +8 -18
  15. package/server/appium-recorder/run-history.ts +11 -0
  16. package/server/config-access.test.ts +27 -0
  17. package/server/config-access.ts +27 -0
  18. package/server/http-api.ts +63 -54
  19. package/server/operations/repository.ts +8 -0
  20. package/server/remote-agents/protocol.ts +1 -0
  21. package/server/remote-agents/registry.test.ts +65 -0
  22. package/server/remote-agents/registry.ts +60 -5
  23. package/server/remote-agents/replay-integration.test.ts +79 -0
  24. package/server/remote-agents/routes.ts +12 -15
  25. package/server/request-body.test.ts +40 -0
  26. package/server/request-body.ts +48 -0
  27. package/src/App.vue +96 -758
  28. package/src/analytics/summary.ts +75 -0
  29. package/src/api.ts +11 -0
  30. package/src/appium-recorder/AppiumPage.vue +4 -1
  31. package/src/components/AppSidebar.vue +31 -0
  32. package/src/components/analytics/AnalysisCharts.vue +189 -0
  33. package/src/components/analytics/ReportAnalysis.vue +78 -0
  34. package/src/components/config/AppiumConfigPanel.vue +4 -1
  35. package/src/components/config/AppiumModelSettings.vue +1 -1
  36. package/src/components/config/BaseConfigPanel.vue +6 -2
  37. package/src/components/config/MidsceneConfigPanel.vue +4 -5
  38. package/src/components/help/HelpCenter.vue +185 -0
  39. package/src/composables/use-device-preview.ts +279 -0
  40. package/src/composables/use-initial-resource.ts +17 -0
  41. package/src/composables/use-script-execution.ts +277 -0
  42. package/src/config/credentials.ts +2 -0
  43. package/src/config/use-appium-auto-save.ts +22 -11
  44. package/src/config/use-model-config.ts +157 -0
  45. package/src/main.ts +2 -2
  46. package/src/pages/AnalysisPage.vue +60 -0
  47. package/src/pages/ConfigPage.vue +36 -5
  48. package/src/style.css +711 -123
  49. package/src/types.ts +7 -1
  50. package/src/components/help/HelpCenterDialog.vue +0 -160
  51. package/src/ui-refresh.css +0 -677
@@ -1,5 +1,8 @@
1
+ import { saveRunHistory } from '../appium-recorder/run-history';
2
+ import type { RunDetail } from '../../src/appium-recorder/run-history';
3
+ import { readBody, requestErrorStatus } from '../request-body';
1
4
  import type { IncomingMessage, ServerResponse } from 'node:http';
2
- import { completeRemoteCommand, listRemoteAndroidDevices, pollRemoteCommand, registerRemoteAgent } from './registry';
5
+ import { completeRemoteCommand, makeRemoteDeviceId, listRemoteAndroidDevices, pollRemoteCommand, registerRemoteAgent } from './registry';
3
6
  import type { RemoteAgentDevice } from './protocol';
4
7
 
5
8
  function sendJson(res: ServerResponse, payload: unknown, statusCode = 200) {
@@ -8,17 +11,6 @@ function sendJson(res: ServerResponse, payload: unknown, statusCode = 200) {
8
11
  res.end(JSON.stringify(payload));
9
12
  }
10
13
 
11
- async function readBody<T>(req: IncomingMessage) {
12
- let body = '';
13
- req.on('data', (chunk) => {
14
- body += chunk;
15
- });
16
- return await new Promise<T>((resolve) => {
17
- req.on('end', () => {
18
- resolve(JSON.parse(body || '{}') as T);
19
- });
20
- });
21
- }
22
14
 
23
15
  export async function handleRemoteAgentRequest(req: IncomingMessage, res: ServerResponse) {
24
16
  const requestUrl = new URL(req.url || '/', 'http://localhost');
@@ -39,14 +31,14 @@ export async function handleRemoteAgentRequest(req: IncomingMessage, res: Server
39
31
  token?: string;
40
32
  devices?: RemoteAgentDevice[];
41
33
  }>(req);
42
- registerRemoteAgent({
34
+ const control = registerRemoteAgent({
43
35
  agentId: parsed.agentId || '',
44
36
  agentName: parsed.agentName,
45
37
  version: parsed.version,
46
38
  token: parsed.token,
47
39
  devices: parsed.devices || [],
48
40
  });
49
- sendJson(res, { success: true });
41
+ sendJson(res, { success: true, ...control });
50
42
  return true;
51
43
  }
52
44
 
@@ -75,6 +67,11 @@ export async function handleRemoteAgentRequest(req: IncomingMessage, res: Server
75
67
  ok: parsed.ok === true,
76
68
  data: parsed.data,
77
69
  error: parsed.error,
70
+ }, command => {
71
+ const result = parsed.data as { history?: RunDetail } | undefined;
72
+ if (command.type !== 'replay' || !result?.history) return;
73
+ const script = command.payload?.script as { id: string };
74
+ saveRunHistory({ ...result.history, scriptId: script.id, deviceId: makeRemoteDeviceId(parsed.agentId!, command.deviceId) });
78
75
  });
79
76
  sendJson(res, { success: true });
80
77
  return true;
@@ -83,7 +80,7 @@ export async function handleRemoteAgentRequest(req: IncomingMessage, res: Server
83
80
  sendJson(res, { message: 'Remote Agent 接口不存在' }, 404);
84
81
  return true;
85
82
  } catch (error) {
86
- sendJson(res, { message: error instanceof Error ? error.message : 'Remote Agent 请求失败' }, 500);
83
+ sendJson(res, { message: error instanceof Error ? error.message : 'Remote Agent 请求失败' }, requestErrorStatus(error));
87
84
  return true;
88
85
  }
89
86
  }
@@ -0,0 +1,40 @@
1
+ import { test } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { createServer, type IncomingMessage } from 'node:http';
4
+ import { PassThrough } from 'node:stream';
5
+ import { readBody, readRawBody, requestErrorStatus } from './request-body';
6
+
7
+ test('malformed JSON returns 400 and subsequent requests remain healthy', async () => {
8
+ const server = createServer(async (req, res) => {
9
+ try { res.end(JSON.stringify(await readBody(req))); }
10
+ catch (error) { res.statusCode = requestErrorStatus(error); res.end(); }
11
+ });
12
+ await new Promise<void>(resolve => server.listen(0, '127.0.0.1', resolve));
13
+ const address = server.address();
14
+ assert.ok(address && typeof address === 'object');
15
+ try {
16
+ const url = `http://127.0.0.1:${address.port}`;
17
+ for (const body of ['{', 'null', '[]', '123']) assert.equal((await fetch(url, { method: 'POST', body })).status, 400);
18
+ const response = await fetch(url, { method: 'POST', body: '{"ok":true}' });
19
+ assert.equal(response.status, 200);
20
+ assert.deepEqual(await response.json(), { ok: true });
21
+ } finally { server.closeAllConnections(); await new Promise<void>(resolve => server.close(() => resolve())); }
22
+ });
23
+
24
+ test('body limits cover content-length and streamed chunks; aborts reject without crashing', async () => {
25
+ for (const useLength of [true, false]) {
26
+ const stream = new PassThrough();
27
+ const req = Object.assign(stream, { headers: useLength ? { 'content-length': '9' } : {} }) as unknown as IncomingMessage;
28
+ const promise = readRawBody(req, 8);
29
+ const rejected = assert.rejects(promise, error => requestErrorStatus(error) === 413);
30
+ if (!useLength) { stream.write('1234'); stream.write('56789'); }
31
+ await rejected;
32
+ stream.end();
33
+ }
34
+ const stream = Object.assign(new PassThrough(), { headers: {} });
35
+ const rejected = assert.rejects(readRawBody(stream as unknown as IncomingMessage), error => requestErrorStatus(error) === 400);
36
+ stream.emit('aborted');
37
+ stream.emit('error', new Error('ECONNRESET'));
38
+ await rejected;
39
+ stream.destroy();
40
+ });
@@ -0,0 +1,48 @@
1
+ import type { IncomingMessage } from 'node:http';
2
+
3
+ export class RequestBodyError extends Error {
4
+ constructor(message: string, readonly statusCode: number) { super(message); }
5
+ }
6
+
7
+ export function requestErrorStatus(error: unknown, fallback = 500) {
8
+ return error instanceof RequestBodyError ? error.statusCode : fallback;
9
+ }
10
+
11
+ export function readRawBody(req: IncomingMessage, limit = 16 * 1024 * 1024): Promise<Buffer> {
12
+ return new Promise((resolve, reject) => {
13
+ const chunks: Buffer[] = [];
14
+ let size = 0;
15
+ const timer = setTimeout(() => fail(new RequestBodyError('请求读取超时', 408)), 30_000);
16
+ const cleanup = () => {
17
+ clearTimeout(timer);
18
+ req.off('data', data); req.off('end', end); req.off('aborted', aborted); req.off('error', fail);
19
+ };
20
+ const fail = (error: Error) => {
21
+ cleanup(); chunks.length = 0;
22
+ // An aborted socket can emit an error after the aborted event.
23
+ req.once('error', () => {});
24
+ req.resume(); reject(error);
25
+ };
26
+ const data = (chunk: Buffer | string) => {
27
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
28
+ size += buffer.length;
29
+ if (size > limit) { fail(new RequestBodyError('请求内容过大', 413)); return; }
30
+ chunks.push(buffer);
31
+ };
32
+ const end = () => { cleanup(); resolve(Buffer.concat(chunks)); };
33
+ const aborted = () => fail(new RequestBodyError('请求已中断', 400));
34
+ req.on('data', data); req.once('end', end); req.once('aborted', aborted); req.once('error', fail);
35
+ if (Number(req.headers['content-length']) > limit) fail(new RequestBodyError('请求内容过大', 413));
36
+ });
37
+ }
38
+
39
+ export async function readBody<T>(req: IncomingMessage): Promise<T> {
40
+ const body = await readRawBody(req);
41
+ try {
42
+ const value: unknown = JSON.parse(body.toString('utf8') || '{}');
43
+ if (!value || typeof value !== 'object' || Array.isArray(value)) throw new Error();
44
+ return value as T;
45
+ } catch {
46
+ throw new RequestBodyError('请求必须是有效的 JSON 对象', 400);
47
+ }
48
+ }