android-midscene-automation 1.1.4 → 1.1.5
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 +3 -0
- package/README.md +3 -0
- package/artemis-main/apps/admin_console/server.py +33 -0
- package/package.json +1 -1
- package/src/appium-recorder/components/ComponentTree.vue +72 -10
- package/src/appium-recorder/components/NodeDetail.vue +129 -52
- package/src/components/config/AiDeduplicationSettings.vue +0 -1
- package/src/components/config/MidsceneConfigPanel.vue +12 -2
- package/src/style.css +147 -2
package/CHANGELOG.md
CHANGED
package/README.md
CHANGED
|
@@ -19,7 +19,9 @@ Modular entrypoint for full trace inspection, step replay, and task execution ma
|
|
|
19
19
|
|
|
20
20
|
import argparse
|
|
21
21
|
import asyncio
|
|
22
|
+
import copy
|
|
22
23
|
from contextlib import asynccontextmanager
|
|
24
|
+
import logging
|
|
23
25
|
import os
|
|
24
26
|
from pathlib import Path
|
|
25
27
|
import secrets
|
|
@@ -452,6 +454,35 @@ class ArtemisUvicornServer(uvicorn.Server):
|
|
|
452
454
|
super().handle_exit(sig, frame)
|
|
453
455
|
|
|
454
456
|
|
|
457
|
+
class QuietProbeAccessFilter(logging.Filter):
|
|
458
|
+
"""Hide successful high-frequency probe requests from Uvicorn access logs."""
|
|
459
|
+
|
|
460
|
+
_quiet_paths = frozenset({"/api/status", "/api/system/readiness"})
|
|
461
|
+
|
|
462
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
463
|
+
args = record.args
|
|
464
|
+
if not isinstance(args, tuple) or len(args) < 5:
|
|
465
|
+
return True
|
|
466
|
+
method = str(args[1]).upper()
|
|
467
|
+
path = str(args[2]).partition("?")[0]
|
|
468
|
+
try:
|
|
469
|
+
status_code = int(args[4])
|
|
470
|
+
except (TypeError, ValueError):
|
|
471
|
+
return True
|
|
472
|
+
return not (method == "GET" and path in self._quiet_paths and status_code == 200)
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
def _uvicorn_log_config() -> dict:
|
|
476
|
+
"""Keep normal access logs while silencing successful background probes."""
|
|
477
|
+
config = copy.deepcopy(uvicorn.config.LOGGING_CONFIG)
|
|
478
|
+
filter_name = "quiet_artemis_probes"
|
|
479
|
+
config.setdefault("filters", {})[filter_name] = {"()": QuietProbeAccessFilter}
|
|
480
|
+
access_handler = config.get("handlers", {}).get("access")
|
|
481
|
+
if access_handler is not None:
|
|
482
|
+
access_handler["filters"] = [*access_handler.get("filters", []), filter_name]
|
|
483
|
+
return config
|
|
484
|
+
|
|
485
|
+
|
|
455
486
|
def run_ui_server(host: str, port: int, reload: bool = False) -> None:
|
|
456
487
|
"""Run the UI server with bounded, signal-aware graceful shutdown."""
|
|
457
488
|
state.host = host
|
|
@@ -466,6 +497,7 @@ def run_ui_server(host: str, port: int, reload: bool = False) -> None:
|
|
|
466
497
|
port=port,
|
|
467
498
|
reload=True,
|
|
468
499
|
timeout_graceful_shutdown=5,
|
|
500
|
+
log_config=_uvicorn_log_config(),
|
|
469
501
|
)
|
|
470
502
|
return
|
|
471
503
|
|
|
@@ -474,6 +506,7 @@ def run_ui_server(host: str, port: int, reload: bool = False) -> None:
|
|
|
474
506
|
host=host,
|
|
475
507
|
port=port,
|
|
476
508
|
timeout_graceful_shutdown=5,
|
|
509
|
+
log_config=_uvicorn_log_config(),
|
|
477
510
|
)
|
|
478
511
|
server = ArtemisUvicornServer(config)
|
|
479
512
|
app.state.uvicorn_server = server
|
package/package.json
CHANGED
|
@@ -1,42 +1,99 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import { computed, nextTick, ref, watch } from 'vue';
|
|
3
3
|
import type { AppiumNode } from '../types';
|
|
4
|
-
import { Search } from '@element-plus/icons-vue';
|
|
4
|
+
import { ArrowDown, ArrowUp, Search } from '@element-plus/icons-vue';
|
|
5
|
+
import { flattenNodes } from '../tree';
|
|
5
6
|
|
|
6
7
|
const props = defineProps<{
|
|
7
8
|
tree: AppiumNode | null;
|
|
8
9
|
selectedId: string;
|
|
9
10
|
}>();
|
|
10
11
|
|
|
11
|
-
defineEmits<{
|
|
12
|
+
const emit = defineEmits<{
|
|
12
13
|
select: [node: AppiumNode];
|
|
13
14
|
}>();
|
|
14
15
|
|
|
15
16
|
const containerRef = ref<HTMLElement | null>(null);
|
|
16
17
|
const query = ref('');
|
|
18
|
+
const currentMatchIndex = ref(-1);
|
|
17
19
|
// Keep the tree data identity stable while typing so filtering is not reset.
|
|
18
20
|
const treeData = computed(() => props.tree ? [props.tree] : []);
|
|
19
21
|
const treeRef = ref<{ setCurrentKey: (key: string) => void; filter: (value: string) => void } | null>(null);
|
|
20
22
|
const filterNode = (value: string, node: AppiumNode) => !value || [node.label, node.resourceId, node.text, node.contentDesc, node.className].some((text) => text?.toLowerCase().includes(value.toLowerCase()));
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
const matches = computed(() => {
|
|
24
|
+
const value = query.value.trim();
|
|
25
|
+
return value ? flattenNodes(props.tree).filter(node => filterNode(value, node)) : [];
|
|
26
|
+
});
|
|
27
|
+
const matchPosition = computed(() => matches.value.length && currentMatchIndex.value >= 0
|
|
28
|
+
? `${currentMatchIndex.value + 1}/${matches.value.length}`
|
|
29
|
+
: `0/${matches.value.length}`);
|
|
30
|
+
|
|
31
|
+
const scrollToCurrentNode = async () => {
|
|
32
|
+
await nextTick();
|
|
33
|
+
containerRef.value
|
|
34
|
+
?.querySelector('.el-tree-node.is-current')
|
|
35
|
+
?.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const focusMatch = (index: number) => {
|
|
39
|
+
if (!matches.value.length) return;
|
|
40
|
+
currentMatchIndex.value = (index + matches.value.length) % matches.value.length;
|
|
41
|
+
const node = matches.value[currentMatchIndex.value];
|
|
42
|
+
treeRef.value?.setCurrentKey(node.id);
|
|
43
|
+
emit('select', node);
|
|
44
|
+
void scrollToCurrentNode();
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const refreshSearch = async () => {
|
|
48
|
+
treeRef.value?.filter(query.value);
|
|
49
|
+
await nextTick();
|
|
50
|
+
if (!query.value.trim() || !matches.value.length) {
|
|
51
|
+
currentMatchIndex.value = -1;
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const selectedMatchIndex = matches.value.findIndex(node => node.id === props.selectedId);
|
|
55
|
+
focusMatch(selectedMatchIndex >= 0 ? selectedMatchIndex : 0);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
watch(query, refreshSearch);
|
|
59
|
+
watch(() => props.tree, refreshSearch);
|
|
23
60
|
|
|
24
61
|
watch(
|
|
25
62
|
() => props.selectedId,
|
|
26
63
|
async () => {
|
|
27
|
-
query.value
|
|
64
|
+
if (query.value.trim()) {
|
|
65
|
+
const selectedMatchIndex = matches.value.findIndex(node => node.id === props.selectedId);
|
|
66
|
+
if (selectedMatchIndex >= 0) currentMatchIndex.value = selectedMatchIndex;
|
|
67
|
+
else query.value = '';
|
|
68
|
+
}
|
|
28
69
|
treeRef.value?.setCurrentKey(props.selectedId);
|
|
29
|
-
await
|
|
30
|
-
containerRef.value
|
|
31
|
-
?.querySelector('.el-tree-node.is-current')
|
|
32
|
-
?.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
70
|
+
await scrollToCurrentNode();
|
|
33
71
|
},
|
|
34
72
|
);
|
|
35
73
|
</script>
|
|
36
74
|
|
|
37
75
|
<template>
|
|
38
76
|
<div class="appium-tree-container">
|
|
39
|
-
<
|
|
77
|
+
<div class="appium-tree-search">
|
|
78
|
+
<el-input
|
|
79
|
+
v-model="query"
|
|
80
|
+
size="small"
|
|
81
|
+
:prefix-icon="Search"
|
|
82
|
+
clearable
|
|
83
|
+
placeholder="搜索文字、ID、组件类型"
|
|
84
|
+
aria-label="搜索组件"
|
|
85
|
+
@keydown.enter.prevent="focusMatch(currentMatchIndex + ($event.shiftKey ? -1 : 1))"
|
|
86
|
+
@keydown.up.prevent="focusMatch(currentMatchIndex - 1)"
|
|
87
|
+
@keydown.down.prevent="focusMatch(currentMatchIndex + 1)"
|
|
88
|
+
/>
|
|
89
|
+
<span class="appium-tree-search__count" aria-live="polite">{{ matchPosition }}</span>
|
|
90
|
+
<el-tooltip content="上一个匹配项(Shift + Enter)" placement="top" :show-after="200">
|
|
91
|
+
<el-button :icon="ArrowUp" size="small" aria-label="上一个匹配项" :disabled="!matches.length" @click="focusMatch(currentMatchIndex - 1)" />
|
|
92
|
+
</el-tooltip>
|
|
93
|
+
<el-tooltip content="下一个匹配项(Enter)" placement="top" :show-after="200">
|
|
94
|
+
<el-button :icon="ArrowDown" size="small" aria-label="下一个匹配项" :disabled="!matches.length" @click="focusMatch(currentMatchIndex + 1)" />
|
|
95
|
+
</el-tooltip>
|
|
96
|
+
</div>
|
|
40
97
|
<div ref="containerRef" class="appium-tree">
|
|
41
98
|
<el-empty v-if="!tree" description="暂无组件树" />
|
|
42
99
|
<el-tree
|
|
@@ -70,6 +127,11 @@ watch(
|
|
|
70
127
|
|
|
71
128
|
<style scoped>
|
|
72
129
|
.appium-tree-container { display: flex; flex-direction: column; gap: 8px; min-width: 0; min-height: 0; }
|
|
130
|
+
.appium-tree-search { display: flex; align-items: center; gap: 2px; }
|
|
131
|
+
.appium-tree-search .el-input { flex: 1; min-width: 0; }
|
|
132
|
+
.appium-tree-search__count { min-width: 28px; padding: 0 2px; text-align: center; font-size: 11px; color: var(--ui-text-secondary); font-variant-numeric: tabular-nums; }
|
|
133
|
+
.appium-tree-search .el-button { flex: none; width: 24px; height: 24px; min-height: 24px; padding: 0; border-radius: 5px; }
|
|
134
|
+
.appium-tree-search .el-button + .el-button { margin-left: 0; }
|
|
73
135
|
.appium-tree { flex: 1; height: auto; overflow: auto; }
|
|
74
136
|
.appium-tree :deep(.el-tree) { width: max-content; min-width: 100%; }
|
|
75
137
|
.appium-tree-node { flex: 1 0 auto; }
|
|
@@ -11,7 +11,32 @@ const props = defineProps<{
|
|
|
11
11
|
loading?: boolean;
|
|
12
12
|
}>();
|
|
13
13
|
const detailsVisible = ref(false);
|
|
14
|
-
const
|
|
14
|
+
const locatorItems = computed(() => {
|
|
15
|
+
const node = props.node;
|
|
16
|
+
if (!node) return [];
|
|
17
|
+
const available = nodeLocators(node).filter(locator => Boolean(locator.value));
|
|
18
|
+
const preferredNames: Partial<Record<AppiumNode['selector']['strategy'], string>> = {
|
|
19
|
+
id: 'Resource ID',
|
|
20
|
+
accessibilityId: 'Accessibility ID',
|
|
21
|
+
xpath: 'XPath',
|
|
22
|
+
androidUiAutomator: 'UiAutomator',
|
|
23
|
+
};
|
|
24
|
+
const preferredName = preferredNames[node.selector.strategy];
|
|
25
|
+
const preferredIndex = Math.max(0, available.findIndex(locator => locator.name === preferredName));
|
|
26
|
+
const descriptions: Record<string, string> = {
|
|
27
|
+
'Resource ID': '通常最稳定,页面文案变化时也可继续定位',
|
|
28
|
+
'Accessibility ID': '使用 content-desc 定位,适合无 Resource ID 的组件',
|
|
29
|
+
XPath: '依赖组件层级,页面结构变化后可能需要重新获取',
|
|
30
|
+
UiAutomator: 'Android 原生定位表达式,可组合组件属性',
|
|
31
|
+
};
|
|
32
|
+
return available
|
|
33
|
+
.map((locator, index) => ({
|
|
34
|
+
...locator,
|
|
35
|
+
description: descriptions[locator.name] || '',
|
|
36
|
+
recommended: index === preferredIndex,
|
|
37
|
+
}))
|
|
38
|
+
.sort((left, right) => Number(right.recommended) - Number(left.recommended));
|
|
39
|
+
});
|
|
15
40
|
async function copyLocator(value: string) {
|
|
16
41
|
if (!value) return;
|
|
17
42
|
try { await navigator.clipboard.writeText(value); ElMessage.success('定位内容已复制'); }
|
|
@@ -33,53 +58,77 @@ async function copyLocator(value: string) {
|
|
|
33
58
|
<span v-else class="appium-node-summary__empty">请选择组件</span>
|
|
34
59
|
<el-button size="small" type="primary" :icon="View" :disabled="!node" @click="detailsVisible = true">详情</el-button>
|
|
35
60
|
</div>
|
|
36
|
-
<el-drawer v-model="detailsVisible" title="组件详情" size="
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
61
|
+
<el-drawer v-model="detailsVisible" title="组件详情" size="560px" class="appium-node-detail-drawer" append-to-body destroy-on-close>
|
|
62
|
+
<template v-if="node">
|
|
63
|
+
<div class="appium-node-detail__content">
|
|
64
|
+
<section class="appium-node-detail__summary">
|
|
65
|
+
<div>
|
|
66
|
+
<span>当前组件</span>
|
|
67
|
+
<strong>{{ node.label }}</strong>
|
|
68
|
+
<code>{{ node.className || '未知组件类型' }}</code>
|
|
69
|
+
</div>
|
|
70
|
+
<div class="appium-node-detail__tags">
|
|
71
|
+
<el-tag :type="node.enabled ? 'success' : 'info'" size="small">{{ node.enabled ? '可用' : '不可用' }}</el-tag>
|
|
72
|
+
<el-tag :type="node.clickable ? 'primary' : 'info'" size="small">{{ node.clickable ? '可点击' : '不可点击' }}</el-tag>
|
|
73
|
+
<el-tag v-if="node.selector.matchCount" :type="node.selector.matchCount > 1 ? 'warning' : 'success'" size="small">{{ node.selector.matchCount > 1 ? `定位重复 ${node.selector.matchCount}` : '定位唯一' }}</el-tag>
|
|
74
|
+
</div>
|
|
75
|
+
</section>
|
|
76
|
+
|
|
77
|
+
<section class="appium-node-detail__panel appium-node-detail__panel--locators">
|
|
78
|
+
<div class="appium-node-detail__heading">
|
|
79
|
+
<strong>定位方式</strong>
|
|
80
|
+
<span>已按当前脚本使用优先级排列,未提供的定位属性自动隐藏</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div class="appium-node-locators">
|
|
83
|
+
<article v-for="locator in locatorItems" :key="locator.name" class="appium-node-locator-card" :class="{ 'is-recommended': locator.recommended }">
|
|
84
|
+
<div class="appium-node-locator-card__header">
|
|
85
|
+
<el-tooltip :content="locator.description" placement="top" :show-after="300">
|
|
86
|
+
<span class="appium-node-locator-label"><strong>{{ locator.name }}</strong><small v-if="locator.attribute">{{ locator.attribute }}</small></span>
|
|
87
|
+
</el-tooltip>
|
|
88
|
+
<el-tag v-if="locator.recommended" type="primary" size="small">当前使用</el-tag>
|
|
89
|
+
</div>
|
|
90
|
+
<div class="appium-node-locator-value">
|
|
91
|
+
<code>{{ locator.value }}</code>
|
|
92
|
+
<el-tooltip :content="`复制 ${locator.name}`" placement="top" :show-after="200">
|
|
93
|
+
<el-button class="appium-node-locator-copy" text size="small" :icon="CopyDocument" :aria-label="`复制 ${locator.name}`" @click="copyLocator(locator.value)" />
|
|
94
|
+
</el-tooltip>
|
|
95
|
+
</div>
|
|
96
|
+
<small v-if="locator.warning" class="appium-node-locator-warning">{{ locator.warning }}</small>
|
|
97
|
+
</article>
|
|
98
|
+
<el-empty v-if="!locatorItems.length" :image-size="48" description="当前组件没有可用定位信息" />
|
|
99
|
+
</div>
|
|
100
|
+
</section>
|
|
101
|
+
|
|
102
|
+
<section class="appium-node-detail__panel">
|
|
103
|
+
<div class="appium-node-detail__heading"><strong>组件属性</strong><span>当前组件树快照中的原始信息</span></div>
|
|
104
|
+
<el-descriptions class="appium-node-properties" :column="1" border size="small">
|
|
105
|
+
<el-descriptions-item label="text">{{ node.text || '-' }}</el-descriptions-item>
|
|
106
|
+
<el-descriptions-item label="content-desc">{{ node.contentDesc || '-' }}</el-descriptions-item>
|
|
107
|
+
<el-descriptions-item label="resource-id">{{ node.resourceId || '-' }}</el-descriptions-item>
|
|
108
|
+
<el-descriptions-item label="class">{{ node.className || '-' }}</el-descriptions-item>
|
|
109
|
+
<el-descriptions-item label="package">{{ node.packageName || '-' }}</el-descriptions-item>
|
|
110
|
+
<el-descriptions-item label="enabled">{{ String(node.enabled) }}</el-descriptions-item>
|
|
111
|
+
<el-descriptions-item label="clickable">{{ String(node.clickable) }}</el-descriptions-item>
|
|
112
|
+
<el-descriptions-item v-if="node.checkable !== undefined" label="checkable">{{ String(node.checkable) }}</el-descriptions-item>
|
|
113
|
+
<el-descriptions-item v-if="node.checkable" label="checked">{{ node.checked === undefined ? '-' : String(node.checked) }}</el-descriptions-item>
|
|
114
|
+
<el-descriptions-item label="当前 Activity">{{ currentActivity || '-' }}</el-descriptions-item>
|
|
115
|
+
<el-descriptions-item label="selector">
|
|
116
|
+
<code>{{ node.selector.strategy }} {{ node.selector.value || '' }}</code>
|
|
117
|
+
</el-descriptions-item>
|
|
118
|
+
<el-descriptions-item label="推荐定位">
|
|
119
|
+
<span v-if="node.contextSelector">父级 {{ node.contextSelector.strategy }} {{ node.contextSelector.value || '' }} + 子级 {{ node.selector.strategy }} {{ node.selector.value || '' }}</span>
|
|
120
|
+
<span v-else>{{ node.selector.unique ? '当前 selector 唯一' : '使用当前 selector,定位失败时使用坐标兜底' }}</span>
|
|
121
|
+
</el-descriptions-item>
|
|
122
|
+
<el-descriptions-item label="bounds">
|
|
123
|
+
<code v-if="node.bounds">[{{ node.bounds.left }},{{ node.bounds.top }}][{{ node.bounds.right }},{{ node.bounds.bottom }}]</code>
|
|
124
|
+
<span v-else>-</span>
|
|
125
|
+
</el-descriptions-item>
|
|
126
|
+
</el-descriptions>
|
|
127
|
+
</section>
|
|
128
|
+
</div>
|
|
129
|
+
</template>
|
|
130
|
+
<el-empty v-else description="请选择组件" />
|
|
131
|
+
<template #footer><el-button type="primary" @click="detailsVisible = false">关闭</el-button></template>
|
|
83
132
|
</el-drawer>
|
|
84
133
|
|
|
85
134
|
</div>
|
|
@@ -90,13 +139,41 @@ async function copyLocator(value: string) {
|
|
|
90
139
|
.appium-node-summary__text { display: grid; gap: 2px; min-width: 0; font-size: 12px; }
|
|
91
140
|
.appium-node-summary__text strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; }
|
|
92
141
|
.appium-node-summary__text span, .appium-node-summary__empty { color: var(--el-text-color-secondary); font-size: 12px; }
|
|
142
|
+
:global(.appium-node-detail-drawer.el-drawer) { max-width: calc(100vw - 24px); border-left: 1px solid var(--ui-border); background: var(--ui-bg); box-shadow: -18px 0 48px rgb(15 23 42 / 18%); }
|
|
143
|
+
:global(.appium-node-detail-drawer .el-drawer__header) { min-height: 56px; margin: 0; padding: 16px 20px; border-bottom: 1px solid var(--ui-border); background: linear-gradient(180deg, #fff, #f8fafc); color: var(--ui-text); }
|
|
144
|
+
:global(.appium-node-detail-drawer .el-drawer__title) { font-size: 16px; font-weight: 650; }
|
|
145
|
+
:global(.appium-node-detail-drawer .el-drawer__body) { padding: 20px; background: var(--ui-bg); }
|
|
146
|
+
:global(.appium-node-detail-drawer .el-drawer__footer) { padding: 14px 20px; border-top: 1px solid var(--ui-border); background: #f8fafc; }
|
|
147
|
+
.appium-node-detail__content { display: grid; gap: 16px; }
|
|
148
|
+
.appium-node-detail__summary { display: flex; align-items: flex-start; justify-content: space-between; gap: 14px; padding: 16px; border: 1px solid #dbeafe; border-radius: var(--ui-radius); background: linear-gradient(135deg, #f8fbff, #eff6ff); box-shadow: var(--ui-shadow-card); }
|
|
149
|
+
.appium-node-detail__summary > div:first-child { display: grid; min-width: 0; gap: 5px; }
|
|
150
|
+
.appium-node-detail__summary span { color: var(--ui-text-muted); font-size: 12px; }
|
|
151
|
+
.appium-node-detail__summary strong { color: var(--ui-text); font-size: 16px; overflow-wrap: anywhere; }
|
|
152
|
+
.appium-node-detail__summary code { color: var(--ui-text-secondary); font-size: 12px; overflow-wrap: anywhere; }
|
|
153
|
+
.appium-node-detail__tags { display: flex; flex: none; justify-content: flex-end; gap: 6px; flex-wrap: wrap; max-width: 220px; }
|
|
154
|
+
.appium-node-detail__panel { padding: 16px; border: 1px solid var(--ui-border); border-radius: var(--ui-radius); background: var(--ui-surface); box-shadow: var(--ui-shadow-card); }
|
|
155
|
+
.appium-node-detail__panel--locators { padding: 12px; }
|
|
156
|
+
.appium-node-detail__panel--locators .appium-node-detail__heading { margin-bottom: 8px; }
|
|
157
|
+
.appium-node-detail__heading { display: grid; gap: 4px; margin-bottom: 12px; }
|
|
158
|
+
.appium-node-detail__heading strong { color: var(--ui-text); font-size: 14px; }
|
|
159
|
+
.appium-node-detail__heading span { color: var(--ui-text-muted); font-size: 12px; }
|
|
160
|
+
.appium-node-locators { display: grid; gap: 7px; }
|
|
161
|
+
.appium-node-locator-card { display: grid; gap: 6px; min-width: 0; padding: 9px 10px; border: 1px solid var(--ui-border); border-radius: 9px; background: #fbfcfe; box-shadow: 0 2px 6px rgb(15 23 42 / 5%); }
|
|
162
|
+
.appium-node-locator-card.is-recommended { border-color: #bfdbfe; background: linear-gradient(135deg, #f8fbff, #eff6ff); box-shadow: 0 0 0 2px rgb(26 115 232 / 8%), 0 4px 10px rgb(26 115 232 / 8%); }
|
|
163
|
+
.appium-node-locator-card__header { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
|
|
164
|
+
.appium-node-locator-value { display: grid; grid-template-columns: minmax(0, 1fr) 28px; align-items: center; gap: 6px; }
|
|
165
|
+
.appium-node-locator-value code { min-height: 26px; padding: 5px 8px; border: 1px solid rgb(203 213 225 / 65%); border-radius: 7px; background: #f1f5f9; color: var(--ui-text-secondary); font-size: 11px; line-height: 1.45; white-space: pre-wrap; overflow-wrap: anywhere; user-select: text; }
|
|
93
166
|
:deep(.el-descriptions__content) { overflow-wrap: anywhere; word-break: break-word; }
|
|
94
167
|
.appium-node-properties :deep(.el-descriptions__table) { table-layout: fixed; width: 100%; }
|
|
95
168
|
.appium-node-properties :deep(.el-descriptions__label) { width: 118px; overflow-wrap: anywhere; }
|
|
96
|
-
.appium-node-locator-label { display:
|
|
169
|
+
.appium-node-locator-label { display: inline-flex; align-items: baseline; gap: 6px; color: var(--ui-text); cursor: help; }
|
|
97
170
|
.appium-node-locator-label small { font-weight: 400; color: var(--el-text-color-secondary); }
|
|
98
|
-
.appium-node-locator {
|
|
99
|
-
.appium-node-locator code { flex: 1; min-width: 0; white-space: pre-wrap; overflow-wrap: anywhere; user-select: text; }
|
|
100
|
-
.appium-node-locator .appium-node-locator-copy { flex: none; width: 26px; height: 26px; min-height: 26px; padding: 0; }
|
|
171
|
+
.appium-node-locator-copy { width: 28px; min-width: 28px; height: 28px; min-height: 28px; padding: 0; margin: 0; }
|
|
101
172
|
.appium-node-locator-warning { display: block; margin-top: 4px; color: var(--el-color-warning); }
|
|
173
|
+
@media (max-width: 520px) {
|
|
174
|
+
:global(.appium-node-detail-drawer .el-drawer__body) { padding: 14px; }
|
|
175
|
+
.appium-node-detail__summary { display: grid; padding: 14px; }
|
|
176
|
+
.appium-node-detail__tags { justify-content: flex-start; max-width: none; }
|
|
177
|
+
.appium-node-detail__panel { padding: 12px; }
|
|
178
|
+
}
|
|
102
179
|
</style>
|
|
@@ -56,5 +56,4 @@ function patch(value: Partial<AiDeduplicationConfig>) {
|
|
|
56
56
|
.ai-dedup-explanation { margin-top: 18px; padding-top: 14px; border-top: 1px solid var(--ui-border); color: var(--el-text-color-regular); font-size: 13px; line-height: 1.7; }
|
|
57
57
|
.ai-dedup-explanation h4 { margin: 0 0 8px; color: var(--el-text-color-primary); font-size: 13px; }
|
|
58
58
|
.ai-dedup-explanation p { margin: 6px 0; }
|
|
59
|
-
.ai-dedup-settings :deep(.el-radio-group) { gap: 4px 0; }
|
|
60
59
|
</style>
|
|
@@ -118,7 +118,12 @@ const openModelConfigGuide = () => {
|
|
|
118
118
|
/>
|
|
119
119
|
</el-form-item>
|
|
120
120
|
<el-form-item label="API Key">
|
|
121
|
-
<el-input
|
|
121
|
+
<el-input
|
|
122
|
+
v-model="configForm.midscene.model.apiKey"
|
|
123
|
+
type="password"
|
|
124
|
+
placeholder="已保存的 Key 显示为掩码,可输入新 Key 替换"
|
|
125
|
+
show-password
|
|
126
|
+
/>
|
|
122
127
|
</el-form-item>
|
|
123
128
|
</template>
|
|
124
129
|
<template v-else>
|
|
@@ -181,7 +186,12 @@ const openModelConfigGuide = () => {
|
|
|
181
186
|
<el-input v-model="configForm.scriptOptimizer.model.baseUrl" />
|
|
182
187
|
</el-form-item>
|
|
183
188
|
<el-form-item label="API Key">
|
|
184
|
-
<el-input
|
|
189
|
+
<el-input
|
|
190
|
+
v-model="configForm.scriptOptimizer.model.apiKey"
|
|
191
|
+
type="password"
|
|
192
|
+
placeholder="已保存的 Key 显示为掩码,可输入新 Key 替换"
|
|
193
|
+
show-password
|
|
194
|
+
/>
|
|
185
195
|
</el-form-item>
|
|
186
196
|
<el-form-item label="Model Name">
|
|
187
197
|
<el-input v-model="configForm.scriptOptimizer.model.name" />
|
package/src/style.css
CHANGED
|
@@ -3296,11 +3296,81 @@ body {
|
|
|
3296
3296
|
}
|
|
3297
3297
|
|
|
3298
3298
|
.el-message-box {
|
|
3299
|
+
--el-messagebox-padding-primary: 0;
|
|
3300
|
+
padding: 0;
|
|
3301
|
+
overflow: hidden;
|
|
3299
3302
|
border: 1px solid var(--ui-border);
|
|
3300
3303
|
border-radius: var(--ui-radius);
|
|
3301
3304
|
box-shadow: 0 18px 48px rgba(15, 23, 42, 0.18);
|
|
3302
3305
|
}
|
|
3303
3306
|
|
|
3307
|
+
.el-message-box__header {
|
|
3308
|
+
box-sizing: border-box;
|
|
3309
|
+
min-height: 56px;
|
|
3310
|
+
padding: 16px 20px;
|
|
3311
|
+
border-bottom: 1px solid var(--ui-border);
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
.el-message-box__header.show-close {
|
|
3315
|
+
padding-right: 64px;
|
|
3316
|
+
}
|
|
3317
|
+
|
|
3318
|
+
.el-message-box__headerbtn {
|
|
3319
|
+
top: 8px;
|
|
3320
|
+
right: 12px;
|
|
3321
|
+
border-radius: var(--ui-radius-control);
|
|
3322
|
+
}
|
|
3323
|
+
|
|
3324
|
+
.el-message-box__headerbtn:hover {
|
|
3325
|
+
background: #eff6ff;
|
|
3326
|
+
}
|
|
3327
|
+
|
|
3328
|
+
.el-message-box__title {
|
|
3329
|
+
color: var(--ui-text);
|
|
3330
|
+
font-size: 16px;
|
|
3331
|
+
font-weight: 650;
|
|
3332
|
+
line-height: 24px;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
.el-message-box__content {
|
|
3336
|
+
display: grid;
|
|
3337
|
+
align-content: center;
|
|
3338
|
+
box-sizing: border-box;
|
|
3339
|
+
min-height: 88px;
|
|
3340
|
+
padding: 18px 24px;
|
|
3341
|
+
}
|
|
3342
|
+
|
|
3343
|
+
.el-message-box__container {
|
|
3344
|
+
justify-content: center;
|
|
3345
|
+
width: 100%;
|
|
3346
|
+
}
|
|
3347
|
+
|
|
3348
|
+
.el-message-box__message {
|
|
3349
|
+
text-align: center;
|
|
3350
|
+
}
|
|
3351
|
+
|
|
3352
|
+
.el-message-box__input {
|
|
3353
|
+
width: 100%;
|
|
3354
|
+
padding-top: 14px;
|
|
3355
|
+
}
|
|
3356
|
+
|
|
3357
|
+
.el-message-box__btns {
|
|
3358
|
+
gap: 10px;
|
|
3359
|
+
min-height: 68px;
|
|
3360
|
+
padding: 14px 20px;
|
|
3361
|
+
border-top: 1px solid var(--ui-border);
|
|
3362
|
+
background: #f8fafc;
|
|
3363
|
+
}
|
|
3364
|
+
|
|
3365
|
+
.el-message-box__btns .el-button + .el-button {
|
|
3366
|
+
margin-left: 0;
|
|
3367
|
+
}
|
|
3368
|
+
|
|
3369
|
+
@media (max-width: 520px) {
|
|
3370
|
+
.el-message-box__content { padding: 16px 18px; }
|
|
3371
|
+
.el-message-box__btns { padding: 12px 16px; }
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3304
3374
|
.el-tabs__item {
|
|
3305
3375
|
color: var(--ui-text-secondary);
|
|
3306
3376
|
font-weight: 500;
|
|
@@ -3955,10 +4025,85 @@ button:focus-visible { outline: 2px solid var(--ui-primary); outline-offset: -2p
|
|
|
3955
4025
|
box-shadow: var(--ui-shadow-inset);
|
|
3956
4026
|
}
|
|
3957
4027
|
.layout .subnav__item--active, .layout .trend-tabs button.active,
|
|
3958
|
-
.el-tabs:not(.el-tabs--card):not(.el-tabs--border-card) > .el-tabs__header .el-tabs__item.is-active
|
|
3959
|
-
.el-radio-group .el-radio-button.is-active .el-radio-button__inner {
|
|
4028
|
+
.el-tabs:not(.el-tabs--card):not(.el-tabs--border-card) > .el-tabs__header .el-tabs__item.is-active {
|
|
3960
4029
|
box-shadow: var(--ui-shadow-control);
|
|
3961
4030
|
}
|
|
4031
|
+
|
|
4032
|
+
.el-radio-group:has(.el-radio-button) {
|
|
4033
|
+
display: inline-flex;
|
|
4034
|
+
align-items: stretch;
|
|
4035
|
+
gap: 2px;
|
|
4036
|
+
box-sizing: border-box;
|
|
4037
|
+
width: fit-content;
|
|
4038
|
+
max-width: 100%;
|
|
4039
|
+
padding: 2px;
|
|
4040
|
+
border: 1px solid #dbe3ee;
|
|
4041
|
+
border-radius: 14px;
|
|
4042
|
+
background: linear-gradient(180deg, #fff, #f8fafc);
|
|
4043
|
+
box-shadow: inset 0 1px 0 rgb(255 255 255 / 95%), 0 4px 12px -4px rgb(15 23 42 / 20%);
|
|
4044
|
+
}
|
|
4045
|
+
|
|
4046
|
+
.el-radio-group:has(.el-radio-button) .el-radio-button {
|
|
4047
|
+
flex: 1 1 auto;
|
|
4048
|
+
min-width: 0;
|
|
4049
|
+
}
|
|
4050
|
+
|
|
4051
|
+
.el-radio-group:has(.el-radio-button) .el-radio-button__inner {
|
|
4052
|
+
display: flex;
|
|
4053
|
+
align-items: center;
|
|
4054
|
+
justify-content: center;
|
|
4055
|
+
box-sizing: border-box;
|
|
4056
|
+
min-height: 32px;
|
|
4057
|
+
padding: 6px 12px;
|
|
4058
|
+
border: 0 !important;
|
|
4059
|
+
border-radius: 11px !important;
|
|
4060
|
+
background: transparent;
|
|
4061
|
+
box-shadow: none !important;
|
|
4062
|
+
color: var(--ui-text-secondary);
|
|
4063
|
+
font-weight: 500;
|
|
4064
|
+
line-height: 1.25;
|
|
4065
|
+
white-space: normal;
|
|
4066
|
+
text-align: center;
|
|
4067
|
+
transition: color 160ms ease, background-color 160ms ease, box-shadow 160ms ease, transform 160ms ease;
|
|
4068
|
+
}
|
|
4069
|
+
|
|
4070
|
+
.el-radio-group:has(.el-radio-button) .el-radio-button:not(.is-active):not(.is-disabled) .el-radio-button__inner:hover {
|
|
4071
|
+
background: #f1f5f9;
|
|
4072
|
+
color: var(--ui-primary);
|
|
4073
|
+
}
|
|
4074
|
+
|
|
4075
|
+
.el-radio-group:has(.el-radio-button) .el-radio-button.is-active .el-radio-button__inner {
|
|
4076
|
+
background: linear-gradient(180deg, #438cf5, #2574e8);
|
|
4077
|
+
box-shadow: inset 0 1px 0 rgb(255 255 255 / 30%), 0 4px 12px rgb(37 116 232 / 34%) !important;
|
|
4078
|
+
color: #fff;
|
|
4079
|
+
font-weight: 600;
|
|
4080
|
+
}
|
|
4081
|
+
|
|
4082
|
+
.el-radio-group:has(.el-radio-button) .el-radio-button.is-active:not(.is-disabled) .el-radio-button__inner:active {
|
|
4083
|
+
box-shadow: inset 0 2px 5px rgb(15 23 42 / 20%) !important;
|
|
4084
|
+
transform: translateY(1px);
|
|
4085
|
+
}
|
|
4086
|
+
|
|
4087
|
+
.el-radio-group:has(.el-radio-button) .el-radio-button.is-disabled .el-radio-button__inner {
|
|
4088
|
+
background: transparent;
|
|
4089
|
+
color: var(--el-disabled-text-color);
|
|
4090
|
+
}
|
|
4091
|
+
|
|
4092
|
+
.el-radio-group:has(.el-radio-button) .el-radio-button.is-active.is-disabled .el-radio-button__inner {
|
|
4093
|
+
background: #dbeafe;
|
|
4094
|
+
color: #64748b;
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4097
|
+
.el-radio-group:has(.el-radio-button) .el-radio-button__original-radio:focus-visible + .el-radio-button__inner {
|
|
4098
|
+
outline: 2px solid var(--ui-primary);
|
|
4099
|
+
outline-offset: 1px;
|
|
4100
|
+
}
|
|
4101
|
+
|
|
4102
|
+
.el-radio-group:has(.el-radio-button) .el-radio-button--small .el-radio-button__inner {
|
|
4103
|
+
min-height: 27px;
|
|
4104
|
+
padding: 4px 9px;
|
|
4105
|
+
font-size: 12px;
|
|
4106
|
+
}
|
|
3962
4107
|
.el-dialog, .el-message-box, .el-popper.is-light { box-shadow: var(--ui-shadow-popover); }
|
|
3963
4108
|
.el-dialog__header, .el-message-box__header { background: linear-gradient(180deg, #fff, #f8fafc); }
|
|
3964
4109
|
.el-table {
|