Skip to content

Commit 13b4f1d

Browse files
quanruclaude
andauthored
fix(core): support number type for aiInput value field (#1339)
* fix(core): support number type for aiInput value field This change allows aiInput.value to accept both string and number types, addressing scenarios where: 1. AI models return numeric values instead of strings 2. YAML files contain unquoted numbers that parse as number type Changes: - Updated type definitions to accept string | number - Added Zod schema transformation to convert numbers to strings - Updated runtime validation to accept both types - Added explicit conversion in YAML player as fallback All conversions happen internally and are transparent to users. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix(core): update aiInput type signatures to accept number values Update the TypeScript method signatures for aiInput to accept string | number for the value parameter, matching the runtime implementation. Changes: - New signature: opt parameter now accepts { value: string | number } - Legacy signature: first parameter now accepts string | number - Implementation signature: locatePromptOrValue now accepts TUserPrompt | string | number - Type assertion updated from `as string` to `as string | number` This ensures type safety and allows users to pass number values directly without TypeScript errors, while maintaining backward compatibility with existing string-based usage. Fixes type errors in test cases that use number values. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 4defe5c commit 13b4f1d

File tree

5 files changed

+558
-16
lines changed

5 files changed

+558
-16
lines changed

packages/core/src/agent/agent.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -496,7 +496,7 @@ export class Agent<
496496
// New signature, always use locatePrompt as the first param
497497
async aiInput(
498498
locatePrompt: TUserPrompt,
499-
opt: LocateOption & { value: string } & {
499+
opt: LocateOption & { value: string | number } & {
500500
autoDismissKeyboard?: boolean;
501501
} & { mode?: 'replace' | 'clear' | 'append' },
502502
): Promise<any>;
@@ -506,7 +506,7 @@ export class Agent<
506506
* @deprecated Use aiInput(locatePrompt, opt) instead where opt contains the value
507507
*/
508508
async aiInput(
509-
value: string,
509+
value: string | number,
510510
locatePrompt: TUserPrompt,
511511
opt?: LocateOption & { autoDismissKeyboard?: boolean } & {
512512
mode?: 'replace' | 'clear' | 'append';
@@ -515,19 +515,19 @@ export class Agent<
515515

516516
// Implementation
517517
async aiInput(
518-
locatePromptOrValue: TUserPrompt | string,
518+
locatePromptOrValue: TUserPrompt | string | number,
519519
locatePromptOrOpt:
520520
| TUserPrompt
521-
| (LocateOption & { value: string } & {
521+
| (LocateOption & { value: string | number } & {
522522
autoDismissKeyboard?: boolean;
523523
} & { mode?: 'replace' | 'clear' | 'append' }) // AndroidDeviceInputOpt &
524524
| undefined,
525525
optOrUndefined?: LocateOption, // AndroidDeviceInputOpt &
526526
) {
527-
let value: string;
527+
let value: string | number;
528528
let locatePrompt: TUserPrompt;
529529
let opt:
530-
| (LocateOption & { value: string } & {
530+
| (LocateOption & { value: string | number } & {
531531
autoDismissKeyboard?: boolean;
532532
} & { mode?: 'replace' | 'clear' | 'append' }) // AndroidDeviceInputOpt &
533533
| undefined;
@@ -542,14 +542,14 @@ export class Agent<
542542
locatePrompt = locatePromptOrValue as TUserPrompt;
543543
const optWithValue = locatePromptOrOpt as LocateOption & {
544544
// AndroidDeviceInputOpt &
545-
value: string;
545+
value: string | number;
546546
autoDismissKeyboard?: boolean;
547547
};
548548
value = optWithValue.value;
549549
opt = optWithValue;
550550
} else {
551551
// Legacy signature: aiInput(value, locatePrompt, opt)
552-
value = locatePromptOrValue as string;
552+
value = locatePromptOrValue as string | number;
553553
locatePrompt = locatePromptOrOpt as TUserPrompt;
554554
opt = {
555555
...optOrUndefined,
@@ -558,8 +558,8 @@ export class Agent<
558558
}
559559

560560
assert(
561-
typeof value === 'string',
562-
'input value must be a string, use empty string if you want to clear the input',
561+
typeof value === 'string' || typeof value === 'number',
562+
'input value must be a string or number, use empty string if you want to clear the input',
563563
);
564564
assert(locatePrompt, 'missing locate prompt for input');
565565

packages/core/src/device/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ export const defineActionHover = (
155155
// Input
156156
export const actionInputParamSchema = z.object({
157157
value: z
158-
.string()
158+
.union([z.string(), z.number()])
159+
.transform((val) => String(val))
159160
.describe(
160161
'The text to input. Provide the final content for replace/append modes, or an empty string when using clear mode to remove existing text.',
161162
),

packages/core/src/yaml/player.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ interface MidsceneYamlFlowItemAIInput extends LocateOption {
88
// aiInput: string; // value to input
99
// locate: TUserPrompt; // where to input
1010
aiInput: TUserPrompt | undefined; // where to input
11-
value: string; // value to input
11+
value: string | number; // value to input
1212
}
1313

1414
interface MidsceneYamlFlowItemAIKeyboardPress extends LocateOption {
@@ -333,10 +333,10 @@ export class ScriptPlayer<T extends MidsceneYamlScriptEnv> {
333333

334334
// Compatibility with previous version:
335335
// Old format: { aiInput: string (value), locate: TUserPrompt }
336-
// New format - 1: { aiInput: TUserPrompt, value: string }
337-
// New format - 2: { aiInput: undefined, locate: TUserPrompt, value: string }
336+
// New format - 1: { aiInput: TUserPrompt, value: string | number }
337+
// New format - 2: { aiInput: undefined, locate: TUserPrompt, value: string | number }
338338
let locatePrompt: TUserPrompt | undefined;
339-
let value: string | undefined;
339+
let value: string | number | undefined;
340340
if ((inputTask as any).locate) {
341341
// Old format - aiInput is the value, locate is the prompt
342342
value = (aiInput as string) || inputTask.value;
@@ -349,7 +349,7 @@ export class ScriptPlayer<T extends MidsceneYamlScriptEnv> {
349349

350350
await agent.callActionInActionSpace('Input', {
351351
...inputTask,
352-
...(value !== undefined ? { value } : {}),
352+
...(value !== undefined ? { value: String(value) } : {}),
353353
...(locatePrompt
354354
? { locate: buildDetailedLocateParam(locatePrompt, inputTask) }
355355
: {}),
Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
import type { AbstractWebPage } from '@/web-page';
2+
import { Agent as PageAgent } from '@midscene/core/agent';
3+
import { beforeEach, describe, expect, it, vi } from 'vitest';
4+
5+
declare const __VERSION__: string;
6+
7+
// Mock only the necessary parts to avoid side effects
8+
vi.mock('@midscene/core/utils', async () => {
9+
const actual = await vi.importActual('@midscene/core/utils');
10+
return {
11+
...actual,
12+
writeLogFile: vi.fn(() => null),
13+
reportHTMLContent: vi.fn(() => ''),
14+
stringifyDumpData: vi.fn(() => '{}'),
15+
groupedActionDumpFileExt: '.json',
16+
getVersion: () => __VERSION__,
17+
sleep: vi.fn(() => Promise.resolve()),
18+
};
19+
});
20+
21+
vi.mock('@midscene/shared/logger', () => ({
22+
getDebug: vi.fn(() => vi.fn()),
23+
logMsg: vi.fn(),
24+
}));
25+
26+
vi.mock('@midscene/core', async () => {
27+
const actual = await vi.importActual('@midscene/core');
28+
return {
29+
...actual,
30+
Insight: vi.fn().mockImplementation(() => ({})),
31+
};
32+
});
33+
34+
// Partial mock for utils - only mock the async functions that need mocking
35+
vi.mock('@/common/utils', async () => {
36+
const actual = await vi.importActual('@/common/utils');
37+
return {
38+
...actual,
39+
WebPageContextParser: vi.fn().mockResolvedValue({}),
40+
trimContextByViewport: vi.fn((execution) => execution),
41+
printReportMsg: vi.fn(),
42+
};
43+
});
44+
45+
// Mock page implementation
46+
const mockPage = {
47+
interfaceType: 'puppeteer',
48+
keyboard: {
49+
type: vi.fn(),
50+
},
51+
screenshotBase64: vi.fn().mockResolvedValue('mock-screenshot'),
52+
evaluateJavaScript: vi.fn(),
53+
size: vi.fn().mockResolvedValue({ dpr: 1 }),
54+
destroy: vi.fn(),
55+
} as unknown as AbstractWebPage;
56+
57+
const mockedModelConfigFnResult = {
58+
MIDSCENE_MODEL_NAME: 'mock-model',
59+
MIDSCENE_OPENAI_API_KEY: 'mock-api-key',
60+
MIDSCENE_OPENAI_BASE_URL: 'mock-base-url',
61+
};
62+
63+
// Mock task executor
64+
const mockTaskExecutor = {
65+
runPlans: vi.fn(),
66+
} as any;
67+
68+
describe('PageAgent aiInput with number value', () => {
69+
let agent: PageAgent;
70+
71+
beforeEach(() => {
72+
vi.clearAllMocks();
73+
74+
// Create agent instance
75+
agent = new PageAgent(mockPage, {
76+
generateReport: false,
77+
autoPrintReportMsg: false,
78+
modelConfig: () => mockedModelConfigFnResult,
79+
});
80+
81+
// Replace the taskExecutor with our mock
82+
agent.taskExecutor = mockTaskExecutor;
83+
});
84+
85+
it('should accept string value for aiInput', async () => {
86+
const mockPlans = [
87+
{
88+
type: 'Locate' as const,
89+
locate: {
90+
prompt: 'input field',
91+
},
92+
param: {
93+
prompt: 'input field',
94+
},
95+
thought: '',
96+
},
97+
{
98+
type: 'Input' as const,
99+
locate: {
100+
prompt: 'input field',
101+
},
102+
param: {
103+
value: 'test string',
104+
},
105+
thought: '',
106+
},
107+
];
108+
109+
const mockExecutorResult = {
110+
executor: {
111+
dump: () => ({ name: 'test', tasks: [] }),
112+
isInErrorState: () => false,
113+
},
114+
output: {},
115+
};
116+
117+
mockTaskExecutor.runPlans.mockResolvedValue(mockExecutorResult);
118+
119+
// Call aiInput with string value (should work as before)
120+
await expect(
121+
agent.aiInput('input field', { value: 'test string' }),
122+
).resolves.not.toThrow();
123+
});
124+
125+
it('should accept number value for aiInput', async () => {
126+
const mockPlans = [
127+
{
128+
type: 'Locate' as const,
129+
locate: {
130+
prompt: 'input field',
131+
},
132+
param: {
133+
prompt: 'input field',
134+
},
135+
thought: '',
136+
},
137+
{
138+
type: 'Input' as const,
139+
locate: {
140+
prompt: 'input field',
141+
},
142+
param: {
143+
value: '123456',
144+
},
145+
thought: '',
146+
},
147+
];
148+
149+
const mockExecutorResult = {
150+
executor: {
151+
dump: () => ({ name: 'test', tasks: [] }),
152+
isInErrorState: () => false,
153+
},
154+
output: {},
155+
};
156+
157+
mockTaskExecutor.runPlans.mockResolvedValue(mockExecutorResult);
158+
159+
// Call aiInput with number value (should not throw error)
160+
await expect(
161+
agent.aiInput('input field', { value: 123456 }),
162+
).resolves.not.toThrow();
163+
});
164+
165+
it('should accept integer zero for aiInput', async () => {
166+
const mockExecutorResult = {
167+
executor: {
168+
dump: () => ({ name: 'test', tasks: [] }),
169+
isInErrorState: () => false,
170+
},
171+
output: {},
172+
};
173+
174+
mockTaskExecutor.runPlans.mockResolvedValue(mockExecutorResult);
175+
176+
// Call aiInput with number value 0 (should not throw error)
177+
await expect(
178+
agent.aiInput('input field', { value: 0 }),
179+
).resolves.not.toThrow();
180+
});
181+
182+
it('should accept negative number for aiInput', async () => {
183+
const mockExecutorResult = {
184+
executor: {
185+
dump: () => ({ name: 'test', tasks: [] }),
186+
isInErrorState: () => false,
187+
},
188+
output: {},
189+
};
190+
191+
mockTaskExecutor.runPlans.mockResolvedValue(mockExecutorResult);
192+
193+
// Call aiInput with negative number value (should not throw error)
194+
await expect(
195+
agent.aiInput('input field', { value: -999 }),
196+
).resolves.not.toThrow();
197+
});
198+
199+
it('should accept decimal number for aiInput', async () => {
200+
const mockExecutorResult = {
201+
executor: {
202+
dump: () => ({ name: 'test', tasks: [] }),
203+
isInErrorState: () => false,
204+
},
205+
output: {},
206+
};
207+
208+
mockTaskExecutor.runPlans.mockResolvedValue(mockExecutorResult);
209+
210+
// Call aiInput with decimal number value (should not throw error)
211+
await expect(
212+
agent.aiInput('input field', { value: 3.14 }),
213+
).resolves.not.toThrow();
214+
});
215+
216+
it('should use legacy aiInput(value, locatePrompt) signature with number', async () => {
217+
const mockExecutorResult = {
218+
executor: {
219+
dump: () => ({ name: 'test', tasks: [] }),
220+
isInErrorState: () => false,
221+
},
222+
output: {},
223+
};
224+
225+
mockTaskExecutor.runPlans.mockResolvedValue(mockExecutorResult);
226+
227+
// Test legacy signature: aiInput(value, locatePrompt) with number value
228+
await expect(agent.aiInput(88888, 'input field')).resolves.not.toThrow();
229+
});
230+
});

0 commit comments

Comments
 (0)