android-midscene-automation 0.1.21 → 0.1.22
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/CHANGELOG.md +55 -128
- package/README.md +71 -145
- package/USAGE.md +16 -17
- package/bin/android-midscene-automation.js +4 -0
- package/package.json +35 -20
- package/server/appium-recorder/appium-runner.ts +43 -6
- package/server/appium-recorder/repository.ts +1 -0
- package/server/appium-recorder/routes.ts +14 -1
- package/server/http-api.ts +8 -4
- package/server/model-tester.ts +22 -6
- package/src/App.vue +4 -1
- package/src/api.ts +3 -1
- package/src/appium-recorder/AppiumPage.vue +175 -87
- package/src/appium-recorder/api.ts +4 -0
- package/src/appium-recorder/components/FlowCanvas.vue +201 -0
- package/src/appium-recorder/components/FlowNodeCard.vue +264 -0
- package/src/appium-recorder/components/FlowRoundedEdge.vue +67 -0
- package/src/appium-recorder/components/NestedConditionBranches.vue +24 -29
- package/src/appium-recorder/components/RecordedSteps.vue +119 -1457
- package/src/appium-recorder/flow-graph.ts +709 -0
- package/src/appium-recorder/flow-labels.ts +81 -0
- package/src/appium-recorder/types.ts +1 -0
- package/src/components/device/DevicePreviewPanel.vue +4 -1
- package/src/main.ts +3 -0
- package/src/style.css +387 -38
- package/src/ui-refresh.css +619 -0
package/USAGE.md
CHANGED
|
@@ -459,14 +459,14 @@ Home 键
|
|
|
459
459
|
4. 点击判断节点,按需填写“指定文本”。
|
|
460
460
|
5. 选择“模糊匹配(包含)”或“精准匹配(完全一致)”。
|
|
461
461
|
6. 在“是”或“否”分支下点击“插入操作”,分别添加分支步骤。
|
|
462
|
-
7.
|
|
462
|
+
7. 如果两个分支后续要执行相同操作,可以复制公共节点到对应分支,或把公共流程拆成“连接脚本”复用。
|
|
463
463
|
|
|
464
464
|
示例:
|
|
465
465
|
|
|
466
466
|
```text
|
|
467
467
|
判断存在 确认按钮
|
|
468
468
|
是 -> 点击 确认按钮
|
|
469
|
-
否 ->
|
|
469
|
+
否 -> 不添加操作
|
|
470
470
|
```
|
|
471
471
|
|
|
472
472
|
分支说明:
|
|
@@ -476,7 +476,6 @@ Home 键
|
|
|
476
476
|
- 模糊匹配会忽略换行和连续空白差异,只要规范化后的文本包含目标内容即可。
|
|
477
477
|
- 精准匹配要求 Appium 返回的完整文本与指定文本完全一致。
|
|
478
478
|
- 分支可以包含多个连续步骤,也可以在分支末尾连接其他脚本。
|
|
479
|
-
- “连接到下一节点”只在判断后仍有主流程节点时显示。
|
|
480
479
|
|
|
481
480
|
适用场景:
|
|
482
481
|
|
|
@@ -496,7 +495,7 @@ Home 键
|
|
|
496
495
|
3. 选择“判断存在”。
|
|
497
496
|
4. 展开节点,在“指定文本”中输入该弹窗特有的文案。
|
|
498
497
|
5. 根据需要选择模糊匹配或精准匹配。
|
|
499
|
-
6.
|
|
498
|
+
6. 在“是”分支添加该弹窗对应的确认、取消或关闭步骤;“否”分支继续下一个判断或不添加操作。
|
|
500
499
|
|
|
501
500
|
示例:
|
|
502
501
|
|
|
@@ -505,7 +504,7 @@ Home 键
|
|
|
505
504
|
是 -> 点击 确定
|
|
506
505
|
否 -> 判断存在:指定文本“是否退出登录”
|
|
507
506
|
是 -> 点击 确定
|
|
508
|
-
否 ->
|
|
507
|
+
否 -> 不添加操作
|
|
509
508
|
```
|
|
510
509
|
|
|
511
510
|
适用场景:
|
|
@@ -867,12 +866,8 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
867
866
|
输入 账号
|
|
868
867
|
输入 密码
|
|
869
868
|
判断存在 用户协议弹窗
|
|
870
|
-
是 -> 点击 同意
|
|
871
|
-
否 ->
|
|
872
|
-
点击 登录
|
|
873
|
-
判断登录是否成功
|
|
874
|
-
是 -> 连接脚本 首页脚本
|
|
875
|
-
否 -> 断言文本 登录错误提示
|
|
869
|
+
是 -> 点击 同意 -> 点击 登录 -> 判断登录是否成功 -> 连接脚本 首页脚本
|
|
870
|
+
否 -> 点击 登录 -> 判断登录是否成功 -> 连接脚本 首页脚本
|
|
876
871
|
```
|
|
877
872
|
|
|
878
873
|
首页脚本:
|
|
@@ -906,16 +901,15 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
906
901
|
2. 选择用于区分登录结果的稳定元素,添加“判断存在”。
|
|
907
902
|
3. 在“是”分支插入首页校验或“连接脚本”。
|
|
908
903
|
4. 在“否”分支插入错误提示断言。
|
|
909
|
-
5.
|
|
904
|
+
5. 如果两个分支后续有相同公共步骤,使用批量复制把公共节点复制到对应分支。
|
|
910
905
|
|
|
911
906
|
### 示例 3:一次性弹窗
|
|
912
907
|
|
|
913
908
|
```text
|
|
914
909
|
启动 App
|
|
915
910
|
判断存在 确认按钮
|
|
916
|
-
是 -> 点击 确认按钮
|
|
917
|
-
否 ->
|
|
918
|
-
断言存在 首页
|
|
911
|
+
是 -> 点击 确认按钮 -> 断言存在 首页
|
|
912
|
+
否 -> 断言存在 首页
|
|
919
913
|
```
|
|
920
914
|
|
|
921
915
|
适合场景:
|
|
@@ -984,6 +978,12 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
984
978
|
- 分支连接允许选择同一 App 的其他 Activity,但不会主动跳转页面;前面的点击或输入必须真正触发跳转。
|
|
985
979
|
- 如果页面未跳转,回放会在等待目标 Activity 超时后失败,这是为了防止在错误页面执行目标脚本。
|
|
986
980
|
|
|
981
|
+
### 为什么在桌面点击 App 后回放无效
|
|
982
|
+
|
|
983
|
+
部分厂商系统会限制桌面或启动器页面的控件信息,Appium 可能无法获取桌面图标的稳定 id。此时在桌面录制“点击 App 图标”可能无法回放。
|
|
984
|
+
|
|
985
|
+
建议在开始节点后添加“启动 App”操作,由系统按包名打开目标 App,替代桌面点击图标。需要重置登录态或缓存时,可以先添加“清理 App 缓存”,再添加“启动 App”。
|
|
986
|
+
|
|
987
987
|
### 弹窗只出现一次怎么办
|
|
988
988
|
|
|
989
989
|
使用“判断存在”。
|
|
@@ -991,7 +991,7 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
991
991
|
```text
|
|
992
992
|
判断存在 确认按钮
|
|
993
993
|
是 -> 点击 确认按钮
|
|
994
|
-
否 ->
|
|
994
|
+
否 -> 不添加操作
|
|
995
995
|
```
|
|
996
996
|
|
|
997
997
|
不要直接把弹窗确认按钮作为普通必选点击,否则弹窗不出现时脚本会失败。
|
|
@@ -1043,5 +1043,4 @@ output/2026-08-21_17-13-42-831-登录流程.html
|
|
|
1043
1043
|
- 弹窗或其他可选组件使用“判断存在”,不要直接写死必点。
|
|
1044
1044
|
- 坐标点击只作为兜底。
|
|
1045
1045
|
- 给关键节点填写“登录按钮”“账号输入框”等备注,方便查看流程和报告。
|
|
1046
|
-
- 判断分支需要回到主流程时,明确点击“连接到下一节点”。
|
|
1047
1046
|
- 回放失败后优先查看“回放输出”中的失败节点、Activity 和 selector,再打开 `output` 中的 Markdown 报告查看完整配置。
|
|
@@ -10,12 +10,16 @@ const viteRoot = path.dirname(require.resolve('vite/package.json'));
|
|
|
10
10
|
const viteBin = path.join(viteRoot, 'bin', 'vite.js');
|
|
11
11
|
const args = process.argv.slice(2);
|
|
12
12
|
const userRoot = process.cwd();
|
|
13
|
+
const githubUrl = 'https://github.com/oooooooko/android-midscene-automation';
|
|
13
14
|
|
|
14
15
|
if (!args.includes('--host')) {
|
|
15
16
|
args.unshift('127.0.0.1');
|
|
16
17
|
args.unshift('--host');
|
|
17
18
|
}
|
|
18
19
|
|
|
20
|
+
console.log(`\nGitHub 源码 / 二次修改:${githubUrl}`);
|
|
21
|
+
console.log(`问题反馈 / 功能建议:${githubUrl}/issues\n`);
|
|
22
|
+
|
|
19
23
|
const child = spawn(process.execPath, [viteBin, ...args], {
|
|
20
24
|
cwd: packageRoot,
|
|
21
25
|
env: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "android-midscene-automation",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.22",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/oooooooko/android-midscene-automation.git"
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"homepage": "https://github.com/oooooooko/android-midscene-automation#readme",
|
|
12
12
|
"type": "module",
|
|
13
13
|
"bin": {
|
|
14
|
-
"android-midscene-automation": "
|
|
14
|
+
"android-midscene-automation": "bin/android-midscene-automation.js"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bin",
|
|
@@ -35,25 +35,40 @@
|
|
|
35
35
|
"test:e2e": "playwright test"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@
|
|
39
|
-
"@
|
|
40
|
-
"@midscene/
|
|
41
|
-
"@
|
|
42
|
-
"
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
"
|
|
46
|
-
"
|
|
47
|
-
"vue": "
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
38
|
+
"@dagrejs/dagre": "1.1.5",
|
|
39
|
+
"@element-plus/icons-vue": "2.3.2",
|
|
40
|
+
"@midscene/android": "https://registry.npmjs.org/@midscene/android/-/android-1.12.0.tgz",
|
|
41
|
+
"@midscene/android-playground": "1.12.0",
|
|
42
|
+
"@midscene/core": "https://registry.npmjs.org/@midscene/core/-/core-1.12.0.tgz",
|
|
43
|
+
"@midscene/playground": "https://registry.npmjs.org/@midscene/playground/-/playground-1.12.0.tgz",
|
|
44
|
+
"@midscene/shared": "https://registry.npmjs.org/@midscene/shared/-/shared-1.12.0.tgz",
|
|
45
|
+
"@midscene/web": "1.12.0",
|
|
46
|
+
"@vitejs/plugin-vue": "6.0.7",
|
|
47
|
+
"@vue-flow/core": "1.48.2",
|
|
48
|
+
"element-plus": "2.11.1",
|
|
49
|
+
"openai": "5.23.2",
|
|
50
|
+
"tsx": "4.23.0",
|
|
51
|
+
"typescript": "5.9.3",
|
|
52
|
+
"vite": "7.3.6",
|
|
53
|
+
"vue": "3.5.39",
|
|
54
|
+
"word-extractor": "1.0.4",
|
|
55
|
+
"xlsx": "0.18.5",
|
|
56
|
+
"yaml": "2.9.0"
|
|
57
|
+
},
|
|
58
|
+
"overrides": {
|
|
59
|
+
"@floating-ui/core": "1.7.5",
|
|
60
|
+
"@floating-ui/dom": "1.7.6",
|
|
61
|
+
"nanoid": "3.3.15",
|
|
62
|
+
"playwright": "1.61.1",
|
|
63
|
+
"playwright-core": "1.61.1",
|
|
64
|
+
"postcss": "8.5.16",
|
|
65
|
+
"rollup": "4.62.2"
|
|
51
66
|
},
|
|
52
67
|
"devDependencies": {
|
|
53
|
-
"@playwright/test": "
|
|
54
|
-
"@types/node": "
|
|
55
|
-
"@types/word-extractor": "
|
|
56
|
-
"@vue/tsconfig": "
|
|
57
|
-
"vue-tsc": "
|
|
68
|
+
"@playwright/test": "1.61.1",
|
|
69
|
+
"@types/node": "24.13.2",
|
|
70
|
+
"@types/word-extractor": "1.0.6",
|
|
71
|
+
"@vue/tsconfig": "0.7.0",
|
|
72
|
+
"vue-tsc": "3.3.6"
|
|
58
73
|
}
|
|
59
74
|
}
|
|
@@ -122,6 +122,27 @@ export function launchAppOnDevice(deviceId: string, packageName: string) {
|
|
|
122
122
|
});
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
export function clearAppDataOnDevice(deviceId: string, packageName: string) {
|
|
126
|
+
if (!packageName) return Promise.reject(new Error('清理 App 缓存缺少包名'));
|
|
127
|
+
return new Promise<void>((resolve, reject) => {
|
|
128
|
+
execFile(getAdbCommand(), [
|
|
129
|
+
'-s',
|
|
130
|
+
deviceId,
|
|
131
|
+
'shell',
|
|
132
|
+
'pm',
|
|
133
|
+
'clear',
|
|
134
|
+
packageName,
|
|
135
|
+
], { maxBuffer: 4 * 1024 * 1024 }, (error, stdout, stderr) => {
|
|
136
|
+
const output = `${stdout || ''}\n${stderr || ''}`.trim();
|
|
137
|
+
if (error || !/^success$/im.test(output)) {
|
|
138
|
+
reject(new Error(output || error?.message || `ADB 清理 ${packageName} 失败`));
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
resolve();
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
125
146
|
function adbSwipe(deviceId: string, step: Required<AppiumRecordedStepRecord>['swipe']) {
|
|
126
147
|
return new Promise<void>((resolve, reject) => {
|
|
127
148
|
execFile(getAdbCommand(), [
|
|
@@ -500,6 +521,12 @@ async function runStep(sessionId: string, deviceId: string, step: AppiumRecorded
|
|
|
500
521
|
return `ADB 已启动 ${packageName}`;
|
|
501
522
|
}
|
|
502
523
|
|
|
524
|
+
if (step.type === 'clearAppData') {
|
|
525
|
+
const packageName = step.value || '';
|
|
526
|
+
await clearAppDataOnDevice(deviceId, packageName);
|
|
527
|
+
return `ADB 已清除 ${packageName} 的应用数据与缓存`;
|
|
528
|
+
}
|
|
529
|
+
|
|
503
530
|
if (step.type === 'screenshot') {
|
|
504
531
|
await saveScreenshot(sessionId);
|
|
505
532
|
return;
|
|
@@ -710,10 +737,20 @@ async function appendStepDiagnostics(lines: string[], deviceId: string, step: Ap
|
|
|
710
737
|
}
|
|
711
738
|
|
|
712
739
|
type ReplayStepOptions = {
|
|
713
|
-
|
|
740
|
+
skipAppInitialization?: boolean;
|
|
714
741
|
scriptName?: string;
|
|
715
742
|
};
|
|
716
743
|
|
|
744
|
+
function shouldSkipLinkedScriptInitStep(options: ReplayStepOptions, step: AppiumRecordedStepRecord) {
|
|
745
|
+
return Boolean(options.skipAppInitialization && (step.type === 'launchApp' || step.type === 'clearAppData'));
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
function linkedScriptInitSkipReason(step: AppiumRecordedStepRecord) {
|
|
749
|
+
return step.type === 'clearAppData'
|
|
750
|
+
? '连接脚本不重复清理 App'
|
|
751
|
+
: '连接脚本不重复启动 App';
|
|
752
|
+
}
|
|
753
|
+
|
|
717
754
|
async function replayLinkedScript(
|
|
718
755
|
sessionId: string,
|
|
719
756
|
deviceId: string,
|
|
@@ -754,7 +791,7 @@ async function replayLinkedScript(
|
|
|
754
791
|
lines.push(`连接脚本开始:${linkedScript.name}`);
|
|
755
792
|
const nextStack = [...stack, linkedScript.id];
|
|
756
793
|
await replayScriptSteps(sessionId, deviceId, linkedScript.steps, lines, nextStack, {
|
|
757
|
-
|
|
794
|
+
skipAppInitialization: true,
|
|
758
795
|
scriptName: linkedScript.name,
|
|
759
796
|
});
|
|
760
797
|
lines.push(`连接脚本完成:${linkedScript.name}`);
|
|
@@ -770,8 +807,8 @@ async function replayLinearSteps(
|
|
|
770
807
|
) {
|
|
771
808
|
for (const [index, step] of steps.entries()) {
|
|
772
809
|
throwIfReplayStopped();
|
|
773
|
-
if (options
|
|
774
|
-
lines.push(`[节点 ${index + 1}] 跳过:${step.label}
|
|
810
|
+
if (shouldSkipLinkedScriptInitStep(options, step)) {
|
|
811
|
+
lines.push(`[节点 ${index + 1}] 跳过:${step.label}(${linkedScriptInitSkipReason(step)})`);
|
|
775
812
|
continue;
|
|
776
813
|
}
|
|
777
814
|
lines.push(`[节点 ${index + 1}] 开始:${step.label}`);
|
|
@@ -841,8 +878,8 @@ async function replayFlowSteps(
|
|
|
841
878
|
if (guard > maxVisits) throw new Error('流程图可能存在循环,已终止回放');
|
|
842
879
|
|
|
843
880
|
const step = steps[index];
|
|
844
|
-
if (options
|
|
845
|
-
lines.push(`[节点 ${index + 1}] 跳过:${step.label}
|
|
881
|
+
if (shouldSkipLinkedScriptInitStep(options, step)) {
|
|
882
|
+
lines.push(`[节点 ${index + 1}] 跳过:${step.label}(${linkedScriptInitSkipReason(step)})`);
|
|
846
883
|
index = nextIndexAfterStep(index, step);
|
|
847
884
|
continue;
|
|
848
885
|
}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
saveAppiumRecordedScript,
|
|
12
12
|
type AppiumRecordedStepRecord,
|
|
13
13
|
} from './repository';
|
|
14
|
-
import { launchAppOnDevice, replayAppiumScript } from './appium-runner';
|
|
14
|
+
import { clearAppDataOnDevice, launchAppOnDevice, replayAppiumScript } from './appium-runner';
|
|
15
15
|
import { isRemoteDeviceId, sendRemoteCommand } from '../remote-agents/registry';
|
|
16
16
|
import { getAdbCommand } from '../android-sdk';
|
|
17
17
|
|
|
@@ -205,6 +205,19 @@ export async function handleAppiumRecorderRequest(
|
|
|
205
205
|
return true;
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
+
if (pathname === '/api/appium-recorder/clear-app-data' && req.method === 'POST') {
|
|
209
|
+
const parsed = await readBody<{ deviceId?: string; packageName?: string }>(req);
|
|
210
|
+
const deviceId = parsed.deviceId?.trim() || selectedDeviceId;
|
|
211
|
+
const packageName = parsed.packageName?.trim() || '';
|
|
212
|
+
if (!deviceId) throw new Error('未检测到可用设备');
|
|
213
|
+
if (!packageName) throw new Error('请选择预设 App');
|
|
214
|
+
assertDeviceAllowed(deviceId);
|
|
215
|
+
if (isRemoteDeviceId(deviceId)) throw new Error('远程设备暂不支持清理 App 缓存');
|
|
216
|
+
await clearAppDataOnDevice(deviceId, packageName);
|
|
217
|
+
sendJson(res, { success: true });
|
|
218
|
+
return true;
|
|
219
|
+
}
|
|
220
|
+
|
|
208
221
|
if (pathname === '/api/appium-recorder/scripts' && req.method === 'GET') {
|
|
209
222
|
sendJson(res, { scripts: listAppiumRecordedScripts() });
|
|
210
223
|
return true;
|
package/server/http-api.ts
CHANGED
|
@@ -95,9 +95,13 @@ function writeNdjson(res: ServerResponse, event: RunGeneratedScriptEvent | { typ
|
|
|
95
95
|
res.write(`${JSON.stringify(event)}\n`);
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
98
|
+
const START_ANDROID_PLAYGROUND_WITHOUT_BROWSER = [
|
|
99
|
+
"import { androidPlaygroundPlatform, ScrcpyServer } from '@midscene/android-playground';",
|
|
100
|
+
"import { launchPreparedPlaygroundPlatform } from '@midscene/playground';",
|
|
101
|
+
'const scrcpyServer = new ScrcpyServer();',
|
|
102
|
+
'const prepared = await androidPlaygroundPlatform.prepare({ scrcpyServer });',
|
|
103
|
+
'await launchPreparedPlaygroundPlatform(prepared);',
|
|
104
|
+
].join('\n');
|
|
101
105
|
|
|
102
106
|
type AndroidDevice = {
|
|
103
107
|
id: string;
|
|
@@ -513,7 +517,7 @@ export function createApiMiddleware() {
|
|
|
513
517
|
};
|
|
514
518
|
}
|
|
515
519
|
|
|
516
|
-
playgroundProcess = spawn(
|
|
520
|
+
playgroundProcess = spawn(process.execPath, ['--input-type=module', '--eval', START_ANDROID_PLAYGROUND_WITHOUT_BROWSER], {
|
|
517
521
|
cwd: appPath(),
|
|
518
522
|
env: {
|
|
519
523
|
...process.env,
|
package/server/model-tester.ts
CHANGED
|
@@ -30,6 +30,18 @@ type ModelTestResult = {
|
|
|
30
30
|
usage?: ModelUsage;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
function normalizeBaseUrl(value: string) {
|
|
34
|
+
return value.trim().replace(/\/+$/, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizeModelName(value: string) {
|
|
38
|
+
return value.trim().replace(/[‐‑‒–—―]/g, '-');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function visibleContent(value: string) {
|
|
42
|
+
return value.replace(/<think>[\s\S]*?<\/think>/g, '').trim() || value.trim();
|
|
43
|
+
}
|
|
44
|
+
|
|
33
45
|
function execCodex(args: string[]) {
|
|
34
46
|
return new Promise<string>((resolve, reject) => {
|
|
35
47
|
execFile('codex', args, { timeout: 10_000 }, (error, stdout, stderr) => {
|
|
@@ -63,23 +75,27 @@ async function testCodexConnection(input: ModelInput): Promise<ModelTestResult>
|
|
|
63
75
|
}
|
|
64
76
|
|
|
65
77
|
export async function testModelConnection(input: ModelInput): Promise<ModelTestResult> {
|
|
66
|
-
|
|
78
|
+
const baseUrl = normalizeBaseUrl(input.baseUrl);
|
|
79
|
+
const modelName = normalizeModelName(input.name);
|
|
80
|
+
|
|
81
|
+
if (input.provider === 'codex' || baseUrl === 'codex://app-server') {
|
|
67
82
|
return testCodexConnection(input);
|
|
68
83
|
}
|
|
69
84
|
|
|
70
|
-
if (!
|
|
85
|
+
if (!baseUrl || !input.apiKey.trim() || !modelName) {
|
|
71
86
|
throw new Error('模型配置不完整');
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
const startedAt = Date.now();
|
|
75
90
|
const client = new OpenAI({
|
|
76
|
-
apiKey: input.apiKey,
|
|
77
|
-
baseURL:
|
|
91
|
+
apiKey: input.apiKey.trim(),
|
|
92
|
+
baseURL: baseUrl,
|
|
78
93
|
});
|
|
79
94
|
|
|
80
95
|
const result = await client.chat.completions.create({
|
|
81
|
-
model:
|
|
96
|
+
model: modelName,
|
|
82
97
|
temperature: 0,
|
|
98
|
+
max_completion_tokens: 128,
|
|
83
99
|
messages: [
|
|
84
100
|
{
|
|
85
101
|
role: 'user',
|
|
@@ -88,7 +104,7 @@ export async function testModelConnection(input: ModelInput): Promise<ModelTestR
|
|
|
88
104
|
],
|
|
89
105
|
});
|
|
90
106
|
|
|
91
|
-
const content = result.choices[0]?.message?.content
|
|
107
|
+
const content = visibleContent(result.choices[0]?.message?.content || '');
|
|
92
108
|
const usage: ModelUsage = {
|
|
93
109
|
promptTokens: result.usage?.prompt_tokens,
|
|
94
110
|
completionTokens: result.usage?.completion_tokens,
|
package/src/App.vue
CHANGED
|
@@ -1391,7 +1391,10 @@ onUnmounted(() => {
|
|
|
1391
1391
|
|
|
1392
1392
|
<main
|
|
1393
1393
|
class="page-body"
|
|
1394
|
-
:class="{
|
|
1394
|
+
:class="{
|
|
1395
|
+
'page-body--fixed': activeMenu === 'generator' && activeGeneratorMode === 'ai',
|
|
1396
|
+
'page-body--appium': activeMenu === 'appium',
|
|
1397
|
+
}"
|
|
1395
1398
|
>
|
|
1396
1399
|
<el-alert v-if="activeMenu !== 'appium' && errorMessage" type="error" :closable="false" show-icon class="page-alert">
|
|
1397
1400
|
<template #title>{{ errorMessage }}</template>
|
package/src/api.ts
CHANGED
|
@@ -32,6 +32,8 @@ async function postJson<T>(url: string, body?: unknown) {
|
|
|
32
32
|
method: 'POST',
|
|
33
33
|
headers: { 'Content-Type': 'application/json' },
|
|
34
34
|
body: JSON.stringify(body || {}),
|
|
35
|
+
}).catch((error) => {
|
|
36
|
+
throw new Error(`无法连接本地后端 ${url}:${error instanceof Error ? error.message : '网络请求失败'}`);
|
|
35
37
|
});
|
|
36
38
|
return readJson<T>(response);
|
|
37
39
|
}
|
|
@@ -287,4 +289,4 @@ export async function runScript(
|
|
|
287
289
|
if (buffer.trim()) {
|
|
288
290
|
onEvent(JSON.parse(buffer) as RunScriptStreamEvent);
|
|
289
291
|
}
|
|
290
|
-
}
|
|
292
|
+
}
|