Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83bcfd54d5 |
No files matched your search
@@ -29,6 +29,8 @@ ARTEMIS_RECORDING_ENABLED=false
|
|||||||
# JSON 配置 agent.recording 和任务 recording 可覆盖。截图分辨率不受此配置影响。
|
# JSON 配置 agent.recording 和任务 recording 可覆盖。截图分辨率不受此配置影响。
|
||||||
ARTEMIS_RECORDING_CONFIG={"codec":"auto","max_long_edge":1280,"fps":15,"h265_kbps":1000,"h264_kbps":1500,"computer_scope":"screen","minimum_disk_mb":256,"hevc_playback":false}
|
ARTEMIS_RECORDING_CONFIG={"codec":"auto","max_long_edge":1280,"fps":15,"h265_kbps":1000,"h264_kbps":1500,"computer_scope":"screen","minimum_disk_mb":256,"hevc_playback":false}
|
||||||
ARTEMIS_OUTPUTTER_ENABLED=true
|
ARTEMIS_OUTPUTTER_ENABLED=true
|
||||||
|
# Flash 执行结束后的独立 AI 分析与判定,默认关闭;不影响 Pro 或执行过程。
|
||||||
|
ARTEMIS_FLASH_FINAL_CHECK_ENABLED=false
|
||||||
ARTEMIS_STEP_SUMMARIZER_ENABLED=true
|
ARTEMIS_STEP_SUMMARIZER_ENABLED=true
|
||||||
# 默认将任务截图长边压至 2048;false 保留设备原始截图。长边值可在 config/artemis.jsonc 的 agent.screenshot 中覆盖。
|
# 默认将任务截图长边压至 2048;false 保留设备原始截图。长边值可在 config/artemis.jsonc 的 agent.screenshot 中覆盖。
|
||||||
ARTEMIS_SCREENSHOT_LIMIT_ENABLED=true
|
ARTEMIS_SCREENSHOT_LIMIT_ENABLED=true
|
||||||
|
|||||||
@@ -21,7 +21,7 @@
|
|||||||
| 探索、示范、用例提炼与复跑 | [探索说明](docs/exploration-mode.md) |
|
| 探索、示范、用例提炼与复跑 | [探索说明](docs/exploration-mode.md) |
|
||||||
| 插件协议、适配或市场协同 | [插件开发规范](docs/plugin-development.md)及插件仓库自身的 AGENTS.md、DESIGN.md |
|
| 插件协议、适配或市场协同 | [插件开发规范](docs/plugin-development.md)及插件仓库自身的 AGENTS.md、DESIGN.md |
|
||||||
| 远程 Python SDK | [SDK 设计](packages/artemis-client/DESIGN.md),保持标准库薄客户端边界 |
|
| 远程 Python SDK | [SDK 设计](packages/artemis-client/DESIGN.md),保持标准库薄客户端边界 |
|
||||||
| 继续历史开发、确认版本或发布 | [迁入时的开发快照](docs/development-handoff.md)、[2.0.1 发行说明](docs/releases/2.0.1.md),再核对实时 Git 状态;`docs/plans/` 保留各自阶段的状态 |
|
| 继续历史开发、确认版本或发布 | [迁入时的开发快照](docs/development-handoff.md)、[2.1.0 发行说明](docs/releases/2.1.0.md),再核对实时 Git 状态;`docs/plans/` 保留各自阶段的状态 |
|
||||||
|
|
||||||
## Quick Commands
|
## Quick Commands
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ components:
|
|||||||
|
|
||||||
ARTEMIS 面向使用自然语言执行、探索和复跑测试的人。当前产品包含 Flash、Pro、探索三种入口,按需调用安卓设备和电脑插件,提供批次、精选任务、历史、报告及证据。用户应能看懂当前在做什么、何时需要回答、哪些结果已有证据。
|
ARTEMIS 面向使用自然语言执行、探索和复跑测试的人。当前产品包含 Flash、Pro、探索三种入口,按需调用安卓设备和电脑插件,提供批次、精选任务、历史、报告及证据。用户应能看懂当前在做什么、何时需要回答、哪些结果已有证据。
|
||||||
|
|
||||||
本文记录主程序的职责边界及 `apps/showcase_ui` 的现有视觉与交互规则,不提出新的主题改版。远程 SDK 的协议边界见[局部设计](packages/artemis-client/DESIGN.md);独立插件仓库维护适配器设计和构建规则。阶段性进度见[开发快照](docs/development-handoff.md),发行状态见[2.0.1 说明](docs/releases/2.0.1.md)。
|
本文记录主程序的职责边界及 `apps/showcase_ui` 的现有视觉与交互规则,不提出新的主题改版。远程 SDK 的协议边界见[局部设计](packages/artemis-client/DESIGN.md);独立插件仓库维护适配器设计和构建规则。阶段性进度见[开发快照](docs/development-handoff.md),发行状态见[2.1.0 说明](docs/releases/2.1.0.md)。
|
||||||
|
|
||||||
## System architecture
|
## System architecture
|
||||||
|
|
||||||
@@ -68,6 +68,8 @@ ARTEMIS 面向使用自然语言执行、探索和复跑测试的人。当前产
|
|||||||
|
|
||||||
最终验收有独立总预算,按请求超时、重试和剩余任务时间协调,保留取证结果;完整有效的结构化结论才更新正式裁定。`completed`、工具成功、探索通过、用例提炼和复跑通过各自表达自己的状态。
|
最终验收有独立总预算,按请求超时、重试和剩余任务时间协调,保留取证结果;完整有效的结构化结论才更新正式裁定。`completed`、工具成功、探索通过、用例提炼和复跑通过各自表达自己的状态。
|
||||||
|
|
||||||
|
Flash 的最终 AI 判定与分析独立配置,默认关闭;首页功能配置和 MCP 共用保存的全局设置。任务开始时固定设置,尚未开始的任务使用最新值;批量、精选及提炼用例的 Flash 复跑同样遵循。关闭时跳过最终 Checker,保留正常执行、步骤、截图、报告和人工复验;结果为“执行完成 · 未做 AI 判定”,不标记“AI确认”或测试通过。Pro 与主动探索的验收规则保持不变。
|
||||||
|
|
||||||
示范来源保存在元数据中,贯穿分析、提问、规划、执行、验收和用例生成。补充网址不会隐式把电脑示范改到安卓;用户明确改端或提出跨端步骤时按其要求执行。证据必须对应真实来源、窗口、采集时间与操作。
|
示范来源保存在元数据中,贯穿分析、提问、规划、执行、验收和用例生成。补充网址不会隐式把电脑示范改到安卓;用户明确改端或提出跨端步骤时按其要求执行。证据必须对应真实来源、窗口、采集时间与操作。
|
||||||
|
|
||||||
录像使用用户选择的范围。人工示范的窗口模式跟随前台窗口,仅保留一路,无有效窗口时保留缺口;普通录屏在只读验收前停止。恢复操作再追加片段,时间映射以实际录像为准,不借用另一来源或时间的画面。
|
录像使用用户选择的范围。人工示范的窗口模式跟随前台窗口,仅保留一路,无有效窗口时保留缺口;普通录屏在只读验收前停止。恢复操作再追加片段,时间映射以实际录像为准,不借用另一来源或时间的画面。
|
||||||
@@ -94,6 +96,8 @@ ARTEMIS 面向使用自然语言执行、探索和复跑测试的人。当前产
|
|||||||
|
|
||||||
沿用全局字体栈:`Google Sans`、`Google Sans Text`、`Inter`、系统无衬线字体;中文使用系统可用字形。代码、路径及请求诊断使用现有等宽样式,保留换行与复制能力。
|
沿用全局字体栈:`Google Sans`、`Google Sans Text`、`Inter`、系统无衬线字体;中文使用系统可用字形。代码、路径及请求诊断使用现有等宽样式,保留换行与复制能力。
|
||||||
|
|
||||||
|
`Inter`、`Outfit`、`Roboto` 及 `Material Symbols Outlined` 使用随源码分发的[本地字体](apps/showcase_ui/public/fonts/README.md),构建和显示不依赖 Google Fonts。图标保留完整字形及 `FILL`、`GRAD`、`opsz`、`wght` 可变轴,不用文字代替缺失图标。
|
||||||
|
|
||||||
模型控件正文为 13px,探索输入正文为 14px,用例标题为 18px;辅助信息依现有组件使用 11–12px。保留字号、字重、留白形成的层次,不通过大段加粗堆叠状态。Markdown、表格、长链接和中文长标题应换行,不撑破容器。
|
模型控件正文为 13px,探索输入正文为 14px,用例标题为 18px;辅助信息依现有组件使用 11–12px。保留字号、字重、留白形成的层次,不通过大段加粗堆叠状态。Markdown、表格、长链接和中文长标题应换行,不撑破容器。
|
||||||
|
|
||||||
图标沿用 Material Symbols Outlined,常规全局尺寸 20px;组件局部按原有尺寸调整。图标与相邻文字垂直居中,独立图标按钮提供可访问名称。
|
图标沿用 Material Symbols Outlined,常规全局尺寸 20px;组件局部按原有尺寸调整。图标与相邻文字垂直居中,独立图标按钮提供可访问名称。
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
基于 [ARTEMIS](https://github.com/google/artemis) 扩展的 Android AI 自动化测试平台。用自然语言描述任务,即可通过简体中文网页或 MCP 驱动真机、模拟器执行操作并收集结果。模型统一使用 **OpenAI Responses API**。
|
基于 [ARTEMIS](https://github.com/google/artemis) 扩展的 Android AI 自动化测试平台。用自然语言描述任务,即可通过简体中文网页或 MCP 驱动真机、模拟器执行操作并收集结果。模型统一使用 **OpenAI Responses API**。
|
||||||
|
|
||||||
**2.0.1 已发布**:修复 Python 3.12 全新安装时网页服务无法启动的问题,并统一不同 Python 版本的 MCP 工具描述。修复与升级步骤见 [2.0.1 发行说明](docs/releases/2.0.1.md);探索、插件和精选任务等功能见 [2.0.0 发行说明](docs/releases/2.0.0.md)。
|
**2.1.0 已发布**:新增 Flash 最终 AI 判定与分析开关,默认关闭;网页字体和图标随源码分发,修复 Google 字体访问超时导致的构建与启动失败。功能与升级步骤见 [2.1.0 发行说明](docs/releases/2.1.0.md);Python 3.12 启动修复见 [2.0.1](docs/releases/2.0.1.md),探索、插件和精选任务等功能见 [2.0.0](docs/releases/2.0.0.md)。
|
||||||
|
|
||||||
## 主要功能
|
## 主要功能
|
||||||
|
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
|
|
||||||
准备一台开启 USB 调试并完成授权的 Android 手机,或已启动的 Android 模拟器。首次安装需要联网。可使用 Git 克隆或下载 1.1.1 及更新版本的发布源码包;新版 ZIP 解压后也保留 Git 更新能力。
|
准备一台开启 USB 调试并完成授权的 Android 手机,或已启动的 Android 模拟器。首次安装需要联网。可使用 Git 克隆或下载 1.1.1 及更新版本的发布源码包;新版 ZIP 解压后也保留 Git 更新能力。
|
||||||
|
|
||||||
|
网页字体和 Material Symbols 图标已随源码提供,网页构建及显示无需访问 Google Fonts。
|
||||||
|
|
||||||
### Windows
|
### Windows
|
||||||
|
|
||||||
在 PowerShell 中执行:
|
在 PowerShell 中执行:
|
||||||
@@ -59,6 +61,8 @@ cd Artemis-Eapil
|
|||||||
|
|
||||||
“功能开关”默认将任务截图长边限制为 2048 像素;关闭后保留设备原始分辨率。可在 [config/artemis.jsonc](config/artemis.jsonc) 的 `agent.screenshot` 中覆盖 `enabled` 和 `max_long_edge`,重启后对新任务生效。
|
“功能开关”默认将任务截图长边限制为 2048 像素;关闭后保留设备原始分辨率。可在 [config/artemis.jsonc](config/artemis.jsonc) 的 `agent.screenshot` 中覆盖 `enabled` 和 `max_long_edge`,重启后对新任务生效。
|
||||||
|
|
||||||
|
首页“功能配置”提供“Flash 最终 AI 判定与分析”,默认关闭。开启后,Flash 在执行结束时根据用例需求和执行步骤进行独立复核;关闭后显示“执行完成 · 未做 AI 判定”,保留执行步骤、截图和报告,不自动记为测试通过。此设置适用于所有 Flash 任务,包括批量、精选、MCP/SDK 和探索提炼用例的 Flash 复跑;保存后对尚未开始的任务生效,运行中的任务保持原设置。Pro 和主动探索不受影响。
|
||||||
|
|
||||||
报告会区分“执行完成”和“验证通过”;没有验证证据的结果标为“未验证”。任务步骤与截图可查看,设备动作回放不提供。
|
报告会区分“执行完成”和“验证通过”;没有验证证据的结果标为“未验证”。任务步骤与截图可查看,设备动作回放不提供。
|
||||||
|
|
||||||
## 让 AI 使用
|
## 让 AI 使用
|
||||||
|
|||||||
@@ -9,7 +9,11 @@ from artemis.runtime.case_result import read_case_result
|
|||||||
def reviewed_result(
|
def reviewed_result(
|
||||||
session_id: str, ai_result: dict | None, storage: AutomationStore
|
session_id: str, ai_result: dict | None, storage: AutomationStore
|
||||||
) -> dict | None:
|
) -> dict | None:
|
||||||
original = {**ai_result, "source": "ai"} if ai_result else None
|
original = (
|
||||||
|
{**ai_result, "source": "system" if ai_result.get("verification_skipped") else "ai"}
|
||||||
|
if ai_result
|
||||||
|
else None
|
||||||
|
)
|
||||||
record = storage.get("case_review", session_id)
|
record = storage.get("case_review", session_id)
|
||||||
reviews = (record or {}).get("reviews") or []
|
reviews = (record or {}).get("reviews") or []
|
||||||
if not reviews:
|
if not reviews:
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ def execution_config(item: dict) -> dict:
|
|||||||
options["recording"] = {**options["recording"], **(overrides.get("recording") or {})}
|
options["recording"] = {**options["recording"], **(overrides.get("recording") or {})}
|
||||||
options.update({k: v for k, v in overrides.items() if k != "recording" and v is not None})
|
options.update({k: v for k, v in overrides.items() if k != "recording" and v is not None})
|
||||||
return {
|
return {
|
||||||
|
"flash_final_check_enabled": agent.flash.final_check_enabled
|
||||||
|
if profile == "flash"
|
||||||
|
else None,
|
||||||
"runtime_overrides": TaskOptions.model_validate(options).model_dump(exclude_none=True),
|
"runtime_overrides": TaskOptions.model_validate(options).model_dump(exclude_none=True),
|
||||||
"expected_output": item.get("expected_output"),
|
"expected_output": item.get("expected_output"),
|
||||||
"enable_outputter": item.get("enable_outputter")
|
"enable_outputter": item.get("enable_outputter")
|
||||||
|
|||||||
@@ -177,6 +177,9 @@ class CapabilitySettings(BaseModel):
|
|||||||
recording_enabled: bool = False
|
recording_enabled: bool = False
|
||||||
recording: dict = Field(default_factory=dict)
|
recording: dict = Field(default_factory=dict)
|
||||||
outputter_enabled: bool = True
|
outputter_enabled: bool = True
|
||||||
|
flash_final_check_enabled: bool = Field(
|
||||||
|
default_factory=lambda: load_agent_config().flash.final_check_enabled
|
||||||
|
)
|
||||||
step_summarizer_enabled: bool = True
|
step_summarizer_enabled: bool = True
|
||||||
screenshot_limit_enabled: bool = True
|
screenshot_limit_enabled: bool = True
|
||||||
screenshot_max_long_edge: int = 2048
|
screenshot_max_long_edge: int = 2048
|
||||||
@@ -207,6 +210,7 @@ def get_capability_settings() -> CapabilitySettings:
|
|||||||
recording_enabled=settings.ARTEMIS_RECORDING_ENABLED,
|
recording_enabled=settings.ARTEMIS_RECORDING_ENABLED,
|
||||||
recording=load_agent_config().recording.model_dump(),
|
recording=load_agent_config().recording.model_dump(),
|
||||||
outputter_enabled=settings.ARTEMIS_OUTPUTTER_ENABLED,
|
outputter_enabled=settings.ARTEMIS_OUTPUTTER_ENABLED,
|
||||||
|
flash_final_check_enabled=load_agent_config().flash.final_check_enabled,
|
||||||
step_summarizer_enabled=settings.ARTEMIS_STEP_SUMMARIZER_ENABLED,
|
step_summarizer_enabled=settings.ARTEMIS_STEP_SUMMARIZER_ENABLED,
|
||||||
screenshot_limit_enabled=screenshot.enabled,
|
screenshot_limit_enabled=screenshot.enabled,
|
||||||
screenshot_max_long_edge=screenshot.max_long_edge,
|
screenshot_max_long_edge=screenshot.max_long_edge,
|
||||||
@@ -227,6 +231,7 @@ def save_capability_settings(update: CapabilitySettings) -> CapabilitySettings:
|
|||||||
"ARTEMIS_VIDEO_ENABLED": update.video_enabled,
|
"ARTEMIS_VIDEO_ENABLED": update.video_enabled,
|
||||||
"ARTEMIS_RECORDING_ENABLED": update.recording_enabled,
|
"ARTEMIS_RECORDING_ENABLED": update.recording_enabled,
|
||||||
"ARTEMIS_OUTPUTTER_ENABLED": update.outputter_enabled,
|
"ARTEMIS_OUTPUTTER_ENABLED": update.outputter_enabled,
|
||||||
|
"ARTEMIS_FLASH_FINAL_CHECK_ENABLED": update.flash_final_check_enabled,
|
||||||
"ARTEMIS_STEP_SUMMARIZER_ENABLED": update.step_summarizer_enabled,
|
"ARTEMIS_STEP_SUMMARIZER_ENABLED": update.step_summarizer_enabled,
|
||||||
"ARTEMIS_SCREENSHOT_LIMIT_ENABLED": update.screenshot_limit_enabled,
|
"ARTEMIS_SCREENSHOT_LIMIT_ENABLED": update.screenshot_limit_enabled,
|
||||||
"ARTEMIS_TASK_TIMEOUT_SECONDS": update.task_timeout_seconds,
|
"ARTEMIS_TASK_TIMEOUT_SECONDS": update.task_timeout_seconds,
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ def text(value) -> str:
|
|||||||
def verdict(outcome: dict | None) -> str:
|
def verdict(outcome: dict | None) -> str:
|
||||||
result = (outcome or {}).get("test_result")
|
result = (outcome or {}).get("test_result")
|
||||||
if isinstance(result, dict) and result.get("status") in RESULT_LABELS:
|
if isinstance(result, dict) and result.get("status") in RESULT_LABELS:
|
||||||
|
if result["status"] == "inconclusive" and result.get("verification_skipped"):
|
||||||
|
return "未做 AI 判定"
|
||||||
label = RESULT_LABELS[result["status"]]
|
label = RESULT_LABELS[result["status"]]
|
||||||
if result.get("source"):
|
if result.get("source"):
|
||||||
label += "(人工确认)" if result["source"] == "human" else "(AI确认)"
|
label += "(人工确认)" if result["source"] == "human" else "(AI确认)"
|
||||||
@@ -273,7 +275,11 @@ def metadata(report: dict) -> list[tuple]:
|
|||||||
("验证结论", report["verdict"]),
|
("验证结论", report["verdict"]),
|
||||||
("人工复验时间", time_text((report.get("test_result") or {}).get("reviewed_at"))),
|
("人工复验时间", time_text((report.get("test_result") or {}).get("reviewed_at"))),
|
||||||
(
|
(
|
||||||
"原 AI 结论",
|
"原始结果"
|
||||||
|
if ((report.get("test_result") or {}).get("ai_result") or {}).get(
|
||||||
|
"verification_skipped"
|
||||||
|
)
|
||||||
|
else "原 AI 结论",
|
||||||
verdict({"test_result": (report.get("test_result") or {}).get("ai_result")})
|
verdict({"test_result": (report.get("test_result") or {}).get("ai_result")})
|
||||||
if (report.get("test_result") or {}).get("source") == "human"
|
if (report.get("test_result") or {}).get("source") == "human"
|
||||||
else "—",
|
else "—",
|
||||||
|
|||||||
@@ -579,6 +579,13 @@ class TaskQueueService:
|
|||||||
env["ARTEMIS_SESSION_ID"] = str(sess_id)
|
env["ARTEMIS_SESSION_ID"] = str(sess_id)
|
||||||
env["ARTEMIS_TASK_INGRESS"] = str(task_item.get("ingress", "frontend"))
|
env["ARTEMIS_TASK_INGRESS"] = str(task_item.get("ingress", "frontend"))
|
||||||
env["ARTEMIS_TASK_WORKER"] = "1"
|
env["ARTEMIS_TASK_WORKER"] = "1"
|
||||||
|
# Capture the global Flash choice at launch, including saved favorites.
|
||||||
|
# It is metadata in the snapshot, never a favorite's runtime override.
|
||||||
|
flash_final_check = (task_item.get("execution_config") or {}).get(
|
||||||
|
"flash_final_check_enabled"
|
||||||
|
)
|
||||||
|
if profile == "flash" and flash_final_check is not None:
|
||||||
|
env["ARTEMIS_FLASH_FINAL_CHECK_ENABLED"] = str(flash_final_check).lower()
|
||||||
from artemis.exploration.store import explorations
|
from artemis.exploration.store import explorations
|
||||||
|
|
||||||
exploration_run = explorations.run(str(sess_id)) if sess_id else None
|
exploration_run = explorations.run(str(sess_id)) if sess_id else None
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "artemis-showcase-ui",
|
"name": "artemis-showcase-ui",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "artemis-showcase-ui",
|
"name": "artemis-showcase-ui",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/cdk": "^22.1.5",
|
"@angular/cdk": "^22.1.5",
|
||||||
"@angular/common": "^22.1.4",
|
"@angular/common": "^22.1.4",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "artemis-showcase-ui",
|
"name": "artemis-showcase-ui",
|
||||||
"version": "2.0.1",
|
"version": "2.1.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve --proxy-config proxy.conf.json",
|
"start": "ng serve --proxy-config proxy.conf.json",
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# 本地网页字体
|
||||||
|
|
||||||
|
这些字体作为静态资源随源码和网页构建产物分发,由 `src/styles.scss` 的 `@font-face` 引用。构建和页面显示不需要连接 Google Fonts;不要恢复外部字体样式表或预连接。
|
||||||
|
|
||||||
|
正文提供 Latin 字形,中文继续使用系统字体。Material Symbols 保留上游完整图标集和 `FILL`、`GRAD`、`opsz`、`wght` 四个可变轴,兼容现有连字名称及组件样式。
|
||||||
|
|
||||||
|
## 来源与许可证
|
||||||
|
|
||||||
|
| 文件 | 固定来源 | 许可证 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `inter-latin-wght-normal.woff2` | [@fontsource-variable/inter 5.3.0](https://www.npmjs.com/package/@fontsource-variable/inter/v/5.3.0),`files/inter-latin-wght-normal.woff2` | [SIL OFL 1.1](licenses/inter-OFL.txt) |
|
||||||
|
| `outfit-latin-wght-normal.woff2` | [@fontsource-variable/outfit 5.3.0](https://www.npmjs.com/package/@fontsource-variable/outfit/v/5.3.0),`files/outfit-latin-wght-normal.woff2` | [SIL OFL 1.1](licenses/outfit-OFL.txt) |
|
||||||
|
| `roboto-latin-wght-normal.woff2` | [@fontsource-variable/roboto 5.3.0](https://www.npmjs.com/package/@fontsource-variable/roboto/v/5.3.0),`files/roboto-latin-wght-normal.woff2` | [SIL OFL 1.1](licenses/roboto-OFL.txt) |
|
||||||
|
| `material-symbols-outlined.woff2` | Google [material-design-icons 的固定提交](https://github.com/google/material-design-icons/tree/10ecc14790612ca8fd164e3f4ffe9c16fc50d5b7/variablefont),`MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].woff2` | [Apache 2.0](licenses/material-symbols-outlined-Apache-2.0.txt) |
|
||||||
|
|
||||||
|
字体文件未经修改。更新时从固定版本获取,保留上游版权与许可证,并同步以下 SHA256;不要在安装或构建时下载字体。
|
||||||
|
|
||||||
|
| 文件 | SHA256 |
|
||||||
|
| --- | --- |
|
||||||
|
| `inter-latin-wght-normal.woff2` | `3100e775e8616cd2611beecfa23a4263d7037586789b43f035236a2e6fbd4c62` |
|
||||||
|
| `outfit-latin-wght-normal.woff2` | `6c18d579fd87c3776be068b762cbc83fde3acb543d49eabd3ade842eb987e887` |
|
||||||
|
| `roboto-latin-wght-normal.woff2` | `1404ca348bd75ef836f4dd8b6f2cc719458642d1237c368296b2fc652dca47dc` |
|
||||||
|
| `material-symbols-outlined.woff2` | `3ae08513a06396b0258cf1d1a1091c2d336977518fb755ea5b4a13aacdbdc6ef` |
|
||||||
Binary file not shown.
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
|
||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright [yyyy] [name of copyright owner]
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2021 The Outfit Project Authors (https://github.com/Outfitio/Outfit-Fonts)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic) Roboto-Italic[wdth,wght].ttf: Copyright 2011 The Roboto Project Authors (https://github.com/googlefonts/roboto-classic)
|
||||||
|
|
||||||
|
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||||
|
This license is copied below, and is also available with a FAQ at:
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
|
||||||
|
|
||||||
|
-----------------------------------------------------------
|
||||||
|
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||||
|
-----------------------------------------------------------
|
||||||
|
|
||||||
|
PREAMBLE
|
||||||
|
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||||
|
development of collaborative font projects, to support the font creation
|
||||||
|
efforts of academic and linguistic communities, and to provide a free and
|
||||||
|
open framework in which fonts may be shared and improved in partnership
|
||||||
|
with others.
|
||||||
|
|
||||||
|
The OFL allows the licensed fonts to be used, studied, modified and
|
||||||
|
redistributed freely as long as they are not sold by themselves. The
|
||||||
|
fonts, including any derivative works, can be bundled, embedded,
|
||||||
|
redistributed and/or sold with any software provided that any reserved
|
||||||
|
names are not used by derivative works. The fonts and derivatives,
|
||||||
|
however, cannot be released under any other type of license. The
|
||||||
|
requirement for fonts to remain under this license does not apply
|
||||||
|
to any document created using the fonts or their derivatives.
|
||||||
|
|
||||||
|
DEFINITIONS
|
||||||
|
"Font Software" refers to the set of files released by the Copyright
|
||||||
|
Holder(s) under this license and clearly marked as such. This may
|
||||||
|
include source files, build scripts and documentation.
|
||||||
|
|
||||||
|
"Reserved Font Name" refers to any names specified as such after the
|
||||||
|
copyright statement(s).
|
||||||
|
|
||||||
|
"Original Version" refers to the collection of Font Software components as
|
||||||
|
distributed by the Copyright Holder(s).
|
||||||
|
|
||||||
|
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||||
|
or substituting -- in part or in whole -- any of the components of the
|
||||||
|
Original Version, by changing formats or by porting the Font Software to a
|
||||||
|
new environment.
|
||||||
|
|
||||||
|
"Author" refers to any designer, engineer, programmer, technical
|
||||||
|
writer or other person who contributed to the Font Software.
|
||||||
|
|
||||||
|
PERMISSION & CONDITIONS
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||||
|
redistribute, and sell modified and unmodified copies of the Font
|
||||||
|
Software, subject to the following conditions:
|
||||||
|
|
||||||
|
1) Neither the Font Software nor any of its individual components,
|
||||||
|
in Original or Modified Versions, may be sold by itself.
|
||||||
|
|
||||||
|
2) Original or Modified Versions of the Font Software may be bundled,
|
||||||
|
redistributed and/or sold with any software, provided that each copy
|
||||||
|
contains the above copyright notice and this license. These can be
|
||||||
|
included either as stand-alone text files, human-readable headers or
|
||||||
|
in the appropriate machine-readable metadata fields within text or
|
||||||
|
binary files as long as those fields can be easily viewed by the user.
|
||||||
|
|
||||||
|
3) No Modified Version of the Font Software may use the Reserved Font
|
||||||
|
Name(s) unless explicit written permission is granted by the corresponding
|
||||||
|
Copyright Holder. This restriction only applies to the primary font name as
|
||||||
|
presented to the users.
|
||||||
|
|
||||||
|
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||||
|
Software shall not be used to promote, endorse or advertise any
|
||||||
|
Modified Version, except to acknowledge the contribution(s) of the
|
||||||
|
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||||
|
permission.
|
||||||
|
|
||||||
|
5) The Font Software, modified or unmodified, in part or in whole,
|
||||||
|
must be distributed entirely under this license, and must not be
|
||||||
|
distributed under any other license. The requirement for fonts to
|
||||||
|
remain under this license does not apply to any document created
|
||||||
|
using the Font Software.
|
||||||
|
|
||||||
|
TERMINATION
|
||||||
|
This license becomes null and void if any of the above conditions are
|
||||||
|
not met.
|
||||||
|
|
||||||
|
DISCLAIMER
|
||||||
|
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||||
|
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||||
|
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||||
|
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||||
|
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||||
|
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||||
|
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -12,6 +12,7 @@ interface Capabilities {
|
|||||||
video_enabled: boolean;
|
video_enabled: boolean;
|
||||||
recording_enabled: boolean;
|
recording_enabled: boolean;
|
||||||
outputter_enabled: boolean;
|
outputter_enabled: boolean;
|
||||||
|
flash_final_check_enabled: boolean;
|
||||||
step_summarizer_enabled: boolean;
|
step_summarizer_enabled: boolean;
|
||||||
screenshot_limit_enabled: boolean;
|
screenshot_limit_enabled: boolean;
|
||||||
screenshot_max_long_edge: number;
|
screenshot_max_long_edge: number;
|
||||||
@@ -28,7 +29,7 @@ interface Capabilities {
|
|||||||
template: `
|
template: `
|
||||||
<section aria-label="功能开关">
|
<section aria-label="功能开关">
|
||||||
<h3>功能开关</h3>
|
<h3>功能开关</h3>
|
||||||
<p>按需启用,保存后对新任务生效。</p>
|
<p>按需启用,保存后对尚未开始的任务生效,不影响运行中的任务。</p>
|
||||||
<label><input type="checkbox" role="switch" [(ngModel)]="config.adb_auto_connect" [disabled]="busy() || loading() || !loaded()" />
|
<label><input type="checkbox" role="switch" [(ngModel)]="config.adb_auto_connect" [disabled]="busy() || loading() || !loaded()" />
|
||||||
<span>自动连接设备<small>刷新设备时发现本机常见模拟器,并重连手动连接过的 TCP 设备;重启 ADB 后也会重连。不扫描局域网,不自动选择任务设备。保存后立即生效。</small></span></label>
|
<span>自动连接设备<small>刷新设备时发现本机常见模拟器,并重连手动连接过的 TCP 设备;重启 ADB 后也会重连。不扫描局域网,不自动选择任务设备。保存后立即生效。</small></span></label>
|
||||||
<label><input type="checkbox" role="switch" [(ngModel)]="config.video_enabled" [disabled]="busy() || loading() || !loaded()" />
|
<label><input type="checkbox" role="switch" [(ngModel)]="config.video_enabled" [disabled]="busy() || loading() || !loaded()" />
|
||||||
@@ -51,6 +52,8 @@ interface Capabilities {
|
|||||||
}
|
}
|
||||||
<label><input type="checkbox" role="switch" [(ngModel)]="config.outputter_enabled" [disabled]="busy() || loading() || !loaded()" />
|
<label><input type="checkbox" role="switch" [(ngModel)]="config.outputter_enabled" [disabled]="busy() || loading() || !loaded()" />
|
||||||
<span>最终报告生成<small>根据执行记录生成任务结果报告。</small></span></label>
|
<span>最终报告生成<small>根据执行记录生成任务结果报告。</small></span></label>
|
||||||
|
<label><input type="checkbox" role="switch" aria-label="Flash 最终 AI 判定与分析" [(ngModel)]="config.flash_final_check_enabled" [disabled]="busy() || loading() || !loaded()" />
|
||||||
|
<span>Flash 最终 AI 判定与分析<small>默认关闭。开启后,在 Flash 执行结束时根据用例需求和执行步骤复核结果,会增加耗时。关闭时显示“执行完成 · 未做 AI 判定”,仍保留步骤和报告。不影响 Pro。</small></span></label>
|
||||||
<label><input type="checkbox" role="switch" [(ngModel)]="config.step_summarizer_enabled" [disabled]="busy() || loading() || !loaded()" />
|
<label><input type="checkbox" role="switch" [(ngModel)]="config.step_summarizer_enabled" [disabled]="busy() || loading() || !loaded()" />
|
||||||
<span>步骤摘要压缩<small>Flash 模式下在后台生成摘要,减少长任务的上下文占用。</small></span></label>
|
<span>步骤摘要压缩<small>Flash 模式下在后台生成摘要,减少长任务的上下文占用。</small></span></label>
|
||||||
<label><input type="checkbox" role="switch" [(ngModel)]="config.screenshot_limit_enabled" [disabled]="busy() || loading() || !loaded()" />
|
<label><input type="checkbox" role="switch" [(ngModel)]="config.screenshot_limit_enabled" [disabled]="busy() || loading() || !loaded()" />
|
||||||
@@ -96,7 +99,7 @@ interface Capabilities {
|
|||||||
export class CapabilitySettingsComponent implements OnInit {
|
export class CapabilitySettingsComponent implements OnInit {
|
||||||
changed = output<Capabilities>();
|
changed = output<Capabilities>();
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
config: Capabilities = { adb_auto_connect: true, adb_discovery_interval_seconds: 10, adb_discovery_timeout_seconds: 5, video_enabled: false, recording_enabled: false, outputter_enabled: true, step_summarizer_enabled: true, screenshot_limit_enabled: true, screenshot_max_long_edge: 2048, task_timeout_seconds: 1800, observation_timeout_seconds: 60, observation_max_attempts: 3, device_command_timeout_seconds: 10, action_failure_attempts: 3, worker_cleanup_timeout_seconds: 30 };
|
config: Capabilities = { adb_auto_connect: true, adb_discovery_interval_seconds: 10, adb_discovery_timeout_seconds: 5, video_enabled: false, recording_enabled: false, outputter_enabled: true, flash_final_check_enabled: false, step_summarizer_enabled: true, screenshot_limit_enabled: true, screenshot_max_long_edge: 2048, task_timeout_seconds: 1800, observation_timeout_seconds: 60, observation_max_attempts: 3, device_command_timeout_seconds: 10, action_failure_attempts: 3, worker_cleanup_timeout_seconds: 30 };
|
||||||
busy = signal(false); loading = signal(true); loaded = signal(false); message = signal(''); error = signal('');
|
busy = signal(false); loading = signal(true); loaded = signal(false); message = signal(''); error = signal('');
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.http.get<Capabilities>('/api/system/capability-settings').pipe(timeout(UI_READ_TIMEOUT_MS), finalize(() => this.loading.set(false))).subscribe({
|
this.http.get<Capabilities>('/api/system/capability-settings').pipe(timeout(UI_READ_TIMEOUT_MS), finalize(() => this.loading.set(false))).subscribe({
|
||||||
|
|||||||
@@ -220,4 +220,37 @@ describe('持久化模型与功能设置', () => {
|
|||||||
fixture.componentInstance.save();
|
fixture.componentInstance.save();
|
||||||
http.expectNone('/api/system/capability-settings');
|
http.expectNone('/api/system/capability-settings');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('Flash 最终 AI 判定默认关闭,保存失败保留选择,成功后重新打开仍保留', async () => {
|
||||||
|
const fixture = TestBed.createComponent(CapabilitySettingsComponent);
|
||||||
|
const capabilities = { ...fixture.componentInstance.config };
|
||||||
|
fixture.detectChanges();
|
||||||
|
const root: HTMLElement = fixture.nativeElement;
|
||||||
|
const toggle = root.querySelector<HTMLInputElement>('[aria-label="Flash 最终 AI 判定与分析"]')!;
|
||||||
|
await Promise.resolve();
|
||||||
|
expect(toggle.disabled).toBeTrue();
|
||||||
|
http.expectOne('/api/system/capability-settings').flush(capabilities);
|
||||||
|
fixture.detectChanges(); await fixture.whenStable();
|
||||||
|
expect(toggle.checked).toBeFalse();
|
||||||
|
expect(root.textContent).toContain('不影响 Pro');
|
||||||
|
toggle.click(); fixture.detectChanges(); await fixture.whenStable();
|
||||||
|
root.querySelector<HTMLButtonElement>('button')!.click();
|
||||||
|
let request = http.expectOne('/api/system/capability-settings');
|
||||||
|
expect(request.request.body.flash_final_check_enabled).toBeTrue();
|
||||||
|
request.flush({}, {status:500,statusText:'Error'});
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(fixture.componentInstance.config.flash_final_check_enabled).toBeTrue();
|
||||||
|
expect(fixture.componentInstance.error()).toContain('保存失败');
|
||||||
|
root.querySelector<HTMLButtonElement>('button')!.click();
|
||||||
|
request = http.expectOne('/api/system/capability-settings');
|
||||||
|
const saved = request.request.body;
|
||||||
|
request.flush(saved);
|
||||||
|
fixture.destroy();
|
||||||
|
const reopened = TestBed.createComponent(CapabilitySettingsComponent);
|
||||||
|
reopened.detectChanges();
|
||||||
|
http.expectOne('/api/system/capability-settings').flush(saved);
|
||||||
|
reopened.detectChanges(); await reopened.whenStable();
|
||||||
|
expect(reopened.nativeElement.querySelector('[aria-label="Flash 最终 AI 判定与分析"]').checked).toBeTrue();
|
||||||
|
reopened.destroy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
+7
-1
@@ -5,10 +5,14 @@ export const TEST_RESULT_LABELS: Record<CaseTestResult['status'], string> = {
|
|||||||
passed: '测试通过', failed: '测试不通过', inconclusive: '未验证', blocked: '测试阻塞'
|
passed: '测试通过', failed: '测试不通过', inconclusive: '未验证', blocked: '测试阻塞'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export function testResultSourceLabel(result: CaseTestResult | null | undefined): string {
|
||||||
|
return result?.source === 'human' ? '人工确认' : result?.verification_skipped ? 'AI 判定已关闭' : 'AI确认';
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-test-result-badge',
|
selector: 'app-test-result-badge',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
template: `<span [class]="'test-result-badge ' + status()" [class.compact]="compact()" [class.prominent]="prominent()" [title]="(result()?.source === 'human' ? '人工确认:' : 'AI确认:') + (result()?.reason || '执行状态与测试结论分别记录')"><span class="material-symbols-outlined" aria-hidden="true">{{icon()}}</span>{{label()}}</span>`,
|
template: `<span [class]="'test-result-badge ' + status()" [class.compact]="compact()" [class.prominent]="prominent()" [title]="sourceLabel() + ':' + (result()?.reason || '执行状态与测试结论分别记录')"><span class="material-symbols-outlined" aria-hidden="true">{{icon()}}</span>{{label()}}</span>`,
|
||||||
styles: [`
|
styles: [`
|
||||||
:host{display:inline-flex;vertical-align:middle;max-width:100%}
|
:host{display:inline-flex;vertical-align:middle;max-width:100%}
|
||||||
.test-result-badge{display:inline-flex;align-items:center;gap:4px;white-space:nowrap;border-radius:6px;border:1px solid #cbd5e1;padding:3px 8px;font-size:12px;font-weight:700;line-height:1.5;background:#f1f5f9;color:#475569}
|
.test-result-badge{display:inline-flex;align-items:center;gap:4px;white-space:nowrap;border-radius:6px;border:1px solid #cbd5e1;padding:3px 8px;font-size:12px;font-weight:700;line-height:1.5;background:#f1f5f9;color:#475569}
|
||||||
@@ -29,8 +33,10 @@ export class TestResultBadgeComponent {
|
|||||||
compact = input(false);
|
compact = input(false);
|
||||||
prominent = input(false);
|
prominent = input(false);
|
||||||
status = computed(() => this.result()?.status || (['completed', 'success', 'failed', 'cancelled', 'interrupted', 'skipped'].includes(this.executionStatus()) ? 'inconclusive' : 'pending'));
|
status = computed(() => this.result()?.status || (['completed', 'success', 'failed', 'cancelled', 'interrupted', 'skipped'].includes(this.executionStatus()) ? 'inconclusive' : 'pending'));
|
||||||
|
sourceLabel = computed(() => testResultSourceLabel(this.result()));
|
||||||
label = computed(() => this.result()?.source === 'human'
|
label = computed(() => this.result()?.source === 'human'
|
||||||
? (this.status() === 'passed' ? '人工通过' : '人工不通过')
|
? (this.status() === 'passed' ? '人工通过' : '人工不通过')
|
||||||
|
: this.status() === 'inconclusive' && this.result()?.verification_skipped ? '未做 AI 判定'
|
||||||
: this.status() === 'pending' ? '待判定' : TEST_RESULT_LABELS[this.status() as CaseTestResult['status']]);
|
: this.status() === 'pending' ? '待判定' : TEST_RESULT_LABELS[this.status() as CaseTestResult['status']]);
|
||||||
icon = computed(() => ({passed: 'check_circle', failed: 'cancel', blocked: 'block', inconclusive: 'help', pending: 'hourglass_empty'})[this.status()]);
|
icon = computed(() => ({passed: 'check_circle', failed: 'cancel', blocked: 'block', inconclusive: 'help', pending: 'hourglass_empty'})[this.status()]);
|
||||||
}
|
}
|
||||||
+20
@@ -62,4 +62,24 @@ describe('用例测试结果卡片', () => {
|
|||||||
expect(fixture.nativeElement.querySelector('.confirm-fail').disabled).toBeFalse();
|
expect(fixture.nativeElement.querySelector('.confirm-fail').disabled).toBeFalse();
|
||||||
fixture.destroy();
|
fixture.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('跳过 Flash AI 判定时不显示通过或 AI 确认,仍允许人工复验', () => {
|
||||||
|
const fixture = TestBed.createComponent(TestResultCardComponent);
|
||||||
|
const skipped = {status:'inconclusive',reason:'执行完成 · 未做 AI 判定',evidence:[],verification_skipped:true,source:'system'};
|
||||||
|
fixture.componentRef.setInput('result', skipped);
|
||||||
|
fixture.componentRef.setInput('sessionId','flash-skipped');
|
||||||
|
fixture.componentRef.setInput('executionStatus','completed');
|
||||||
|
fixture.detectChanges();
|
||||||
|
const root: HTMLElement = fixture.nativeElement;
|
||||||
|
expect(root.querySelector('.prominent')?.textContent).toContain('未做 AI 判定');
|
||||||
|
expect(root.querySelector('.caption')?.textContent).toContain('AI 判定已关闭');
|
||||||
|
expect(root.querySelector('.prominent')?.getAttribute('title')).not.toContain('AI确认');
|
||||||
|
expect(root.querySelector('.review-toolbar')).not.toBeNull();
|
||||||
|
fixture.componentRef.setInput('result', {status:'passed',reason:'人工核对完成',evidence:[],source:'human',ai_result:skipped});
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(root.textContent).toContain('人工通过');
|
||||||
|
expect(root.textContent).toContain('原始结果 · 未做 AI 判定');
|
||||||
|
expect(root.textContent).not.toContain('原 AI 结论');
|
||||||
|
fixture.destroy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
@@ -4,17 +4,17 @@ import { FormsModule } from '@angular/forms';
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { CaseReviewResponse, CaseTestResult } from '../../core/models/session.model';
|
import { CaseReviewResponse, CaseTestResult } from '../../core/models/session.model';
|
||||||
import { ReportMarkdownComponent } from '../report-markdown/report-markdown.component';
|
import { ReportMarkdownComponent } from '../report-markdown/report-markdown.component';
|
||||||
import { TestResultBadgeComponent } from './test-result-badge.component';
|
import { TestResultBadgeComponent, testResultSourceLabel } from './test-result-badge.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-test-result-card',
|
selector: 'app-test-result-card',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ReportMarkdownComponent, TestResultBadgeComponent, DatePipe, FormsModule],
|
imports: [ReportMarkdownComponent, TestResultBadgeComponent, DatePipe, FormsModule],
|
||||||
template: `<section [class]="'test-result-card ' + result().status" aria-label="用例测试结果" role="status">
|
template: `<section [class]="'test-result-card ' + result().status" aria-label="用例测试结果" role="status">
|
||||||
<header><span class="caption">用例测试结果 <small>{{result().source === 'human' ? '人工确认' : 'AI确认'}}</small></span><app-test-result-badge [result]="result()" [prominent]="true" /></header>
|
<header><span class="caption">用例测试结果 <small>{{sourceLabel()}}</small></span><app-test-result-badge [result]="result()" [prominent]="true" /></header>
|
||||||
<app-report-markdown class="reason" [content]="result().reason" />
|
<app-report-markdown class="reason" [content]="result().reason" />
|
||||||
@if(result().reviewed_at){<p class="review-time">复验于 {{result().reviewed_at! * 1000 | date:'yyyy-MM-dd HH:mm:ss'}}</p>}
|
@if(result().reviewed_at){<p class="review-time">复验于 {{result().reviewed_at! * 1000 | date:'yyyy-MM-dd HH:mm:ss'}}</p>}
|
||||||
@if(result().ai_result; as original){<details><summary>原 AI 结论 · {{original.status === 'passed' ? '测试通过' : original.status === 'failed' ? '测试不通过' : original.status === 'blocked' ? '测试阻塞' : '未验证'}}</summary><app-report-markdown class="reason" [content]="original.reason" />@if(original.evidence.length){<ol>@for(item of original.evidence; track $index){<li><app-report-markdown [content]="item" /></li>}</ol>}</details>}
|
@if(result().ai_result; as original){<details><summary>{{original.verification_skipped ? '原始结果 · 未做 AI 判定' : '原 AI 结论 · ' + (original.status === 'passed' ? '测试通过' : original.status === 'failed' ? '测试不通过' : original.status === 'blocked' ? '测试阻塞' : '未验证')}}</summary><app-report-markdown class="reason" [content]="original.reason" />@if(original.evidence.length){<ol>@for(item of original.evidence; track $index){<li><app-report-markdown [content]="item" /></li>}</ol>}</details>}
|
||||||
@if(result().evidence.length){<details><summary>查看验收证据 · {{result().evidence.length}} 条</summary><ol>@for(item of result().evidence; track $index){<li><app-report-markdown [content]="item" /></li>}</ol></details>}
|
@if(result().evidence.length){<details><summary>查看验收证据 · {{result().evidence.length}} 条</summary><ol>@for(item of result().evidence; track $index){<li><app-report-markdown [content]="item" /></li>}</ol></details>}
|
||||||
@if(canReview()){
|
@if(canReview()){
|
||||||
<div class="review-toolbar"><button type="button" [disabled]="saving()" (click)="toggleReview()" [attr.aria-expanded]="reviewing()">{{result().source === 'human' ? '修改人工复验' : '人工复验'}}</button>@if(notice()){<span role="status">{{notice()}}</span>}</div>
|
<div class="review-toolbar"><button type="button" [disabled]="saving()" (click)="toggleReview()" [attr.aria-expanded]="reviewing()">{{result().source === 'human' ? '修改人工复验' : '人工复验'}}</button>@if(notice()){<span role="status">{{notice()}}</span>}</div>
|
||||||
@@ -53,6 +53,7 @@ import { TestResultBadgeComponent } from './test-result-badge.component';
|
|||||||
export class TestResultCardComponent {
|
export class TestResultCardComponent {
|
||||||
private http = inject(HttpClient);
|
private http = inject(HttpClient);
|
||||||
result = input.required<CaseTestResult>();
|
result = input.required<CaseTestResult>();
|
||||||
|
sourceLabel = computed(() => testResultSourceLabel(this.result()));
|
||||||
sessionId = input<string | null>(null);
|
sessionId = input<string | null>(null);
|
||||||
executionStatus = input('');
|
executionStatus = input('');
|
||||||
reviewSaved = output<CaseReviewResponse>();
|
reviewSaved = output<CaseReviewResponse>();
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export interface CaseTestResult {
|
|||||||
status: 'passed' | 'failed' | 'inconclusive' | 'blocked';
|
status: 'passed' | 'failed' | 'inconclusive' | 'blocked';
|
||||||
reason: string;
|
reason: string;
|
||||||
evidence: string[];
|
evidence: string[];
|
||||||
source?: 'ai' | 'human';
|
source?: 'ai' | 'human' | 'system';
|
||||||
|
verification_skipped?: boolean;
|
||||||
reviewed_at?: number;
|
reviewed_at?: number;
|
||||||
ai_result?: CaseTestResult | null;
|
ai_result?: CaseTestResult | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1423,7 +1423,7 @@
|
|||||||
<span class="mode-name">Flash</span>
|
<span class="mode-name">Flash</span>
|
||||||
<span class="bullet">·</span>
|
<span class="bullet">·</span>
|
||||||
<span>节省 Token,快速响应 <strong>每步 3–5 秒</strong> 直接根据实时屏幕执行操作,适合路径明确的日常任务。</span>
|
<span>节省 Token,快速响应 <strong>每步 3–5 秒</strong> 直接根据实时屏幕执行操作,适合路径明确的日常任务。</span>
|
||||||
<span class="limitation-tag">(轻量执行,结束时判断测试结果)</span>
|
<span class="limitation-tag">(最终 AI 判定默认关闭,可在功能配置中开启)</span>
|
||||||
</p>
|
</p>
|
||||||
} @else if(selectedProfile() === 'exploration') {
|
} @else if(selectedProfile() === 'exploration') {
|
||||||
<p class="desc-line"><span class="mode-name">探索</span><span class="bullet">·</span><span>边执行边交流,支持人工示范,成功后生成可复用用例。</span></p>
|
<p class="desc-line"><span class="mode-name">探索</span><span class="bullet">·</span><span>边执行边交流,支持人工示范,成功后生成可复用用例。</span></p>
|
||||||
|
|||||||
@@ -21,10 +21,6 @@
|
|||||||
<title>artemis</title>
|
<title>artemis</title>
|
||||||
<base href="/">
|
<base href="/">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200" />
|
|
||||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Outfit:wght@300;400;500;600;700&family=Roboto:wght@400;500;700&display=swap" />
|
|
||||||
<link rel="icon" type="image/png" sizes="192x192" href="/logo.png?v=2">
|
<link rel="icon" type="image/png" sizes="192x192" href="/logo.png?v=2">
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="/logo.png?v=2">
|
<link rel="icon" type="image/png" sizes="32x32" href="/logo.png?v=2">
|
||||||
<link rel="icon" type="image/svg+xml" href="/logo.svg?v=2">
|
<link rel="icon" type="image/svg+xml" href="/logo.svg?v=2">
|
||||||
|
|||||||
@@ -14,10 +14,42 @@
|
|||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&family=Outfit:wght@300;400;500;600;700&family=Roboto:wght@400;500;700&display=swap');
|
|
||||||
@import url('https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200');
|
|
||||||
@import '@angular/cdk/overlay-prebuilt.css';
|
@import '@angular/cdk/overlay-prebuilt.css';
|
||||||
|
|
||||||
|
// Keep fonts local so production builds and icons do not depend on Google Fonts.
|
||||||
|
// Sources and licenses: public/fonts/README.md.
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/inter-latin-wght-normal.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Outfit';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/outfit-latin-wght-normal.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 900;
|
||||||
|
font-display: swap;
|
||||||
|
src: url('/fonts/roboto-latin-wght-normal.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Material Symbols Outlined';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 100 700;
|
||||||
|
font-display: block;
|
||||||
|
src: url('/fonts/material-symbols-outlined.woff2') format('woff2');
|
||||||
|
}
|
||||||
|
|
||||||
.cdk-overlay-container {
|
.cdk-overlay-container {
|
||||||
z-index: 1200 !important;
|
z-index: 1200 !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -119,7 +119,9 @@ async def settle_flash_outcome(
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
result = CaseTestResult(
|
result = CaseTestResult(
|
||||||
status="inconclusive", reason="已关闭最终检查,未进行测试结果验收。"
|
status="inconclusive",
|
||||||
|
reason="执行完成 · 未做 AI 判定。已关闭 Flash 最终 AI 分析与判定,执行记录和报告仍保留。",
|
||||||
|
verification_skipped=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
outcome = RunOutcome(
|
outcome = RunOutcome(
|
||||||
|
|||||||
@@ -735,6 +735,10 @@ class MemoryConfig(BaseModel):
|
|||||||
class FlashProfileConfig(BaseModel):
|
class FlashProfileConfig(BaseModel):
|
||||||
"""Configuration options specific to the ⚡ Flash execution profile (FlashRunner/ReactiveRunner)."""
|
"""Configuration options specific to the ⚡ Flash execution profile (FlashRunner/ReactiveRunner)."""
|
||||||
|
|
||||||
|
final_check_enabled: bool = Field(
|
||||||
|
default=False,
|
||||||
|
description="Run the independent AI analysis and verdict after Flash execution ends.",
|
||||||
|
)
|
||||||
max_turns: int = Field(
|
max_turns: int = Field(
|
||||||
default=0,
|
default=0,
|
||||||
ge=0,
|
ge=0,
|
||||||
@@ -1017,6 +1021,10 @@ class AgentGlobalConfig(BaseModel):
|
|||||||
if out_env is not None:
|
if out_env is not None:
|
||||||
data.setdefault("outputter", {})["enabled"] = out_env
|
data.setdefault("outputter", {})["enabled"] = out_env
|
||||||
|
|
||||||
|
flash_check_env = _parse_bool("ARTEMIS_FLASH_FINAL_CHECK_ENABLED")
|
||||||
|
if flash_check_env is not None:
|
||||||
|
data.setdefault("flash", {})["final_check_enabled"] = flash_check_env
|
||||||
|
|
||||||
vid_rec_env = _parse_bool("ARTEMIS_VIDEO_RECORDING_ENABLED") or _parse_bool(
|
vid_rec_env = _parse_bool("ARTEMIS_VIDEO_RECORDING_ENABLED") or _parse_bool(
|
||||||
"ARTEMIS_WITH_VIDEO_RECORDING_TOOLS"
|
"ARTEMIS_WITH_VIDEO_RECORDING_TOOLS"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -131,6 +131,7 @@ class Settings(BaseSettings):
|
|||||||
# Worker-only override, applied after the user's JSON recording defaults.
|
# Worker-only override, applied after the user's JSON recording defaults.
|
||||||
ARTEMIS_TASK_RECORDING_CONFIG: dict = Field(default_factory=dict)
|
ARTEMIS_TASK_RECORDING_CONFIG: dict = Field(default_factory=dict)
|
||||||
ARTEMIS_OUTPUTTER_ENABLED: bool = True
|
ARTEMIS_OUTPUTTER_ENABLED: bool = True
|
||||||
|
ARTEMIS_FLASH_FINAL_CHECK_ENABLED: bool = False
|
||||||
ARTEMIS_STEP_SUMMARIZER_ENABLED: bool = True
|
ARTEMIS_STEP_SUMMARIZER_ENABLED: bool = True
|
||||||
ARTEMIS_SCREENSHOT_LIMIT_ENABLED: bool = True
|
ARTEMIS_SCREENSHOT_LIMIT_ENABLED: bool = True
|
||||||
# Consecutive Pro responses without a valid tool call before failing the task.
|
# Consecutive Pro responses without a valid tool call before failing the task.
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"max_long_edge": 2048
|
"max_long_edge": 2048
|
||||||
},
|
},
|
||||||
"flash": {
|
"flash": {
|
||||||
|
"final_check_enabled": "${env:ARTEMIS_FLASH_FINAL_CHECK_ENABLED:-false}",
|
||||||
"max_turns": 0,
|
"max_turns": 0,
|
||||||
"explorer_mode": "flash",
|
"explorer_mode": "flash",
|
||||||
"step_summarizer": {
|
"step_summarizer": {
|
||||||
|
|||||||
@@ -5,11 +5,16 @@ from pathlib import Path
|
|||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
from pydantic import BaseModel, Field, ValidationError
|
from pydantic import BaseModel, Field, ValidationError
|
||||||
|
from pydantic.json_schema import SkipJsonSchema
|
||||||
|
|
||||||
from artemis.config import TRACES_PATH
|
from artemis.config import TRACES_PATH
|
||||||
|
|
||||||
|
|
||||||
class CaseTestResult(BaseModel):
|
class CaseTestResult(BaseModel):
|
||||||
|
# Host metadata, not part of the model's verdict schema. Old results omit it.
|
||||||
|
verification_skipped: SkipJsonSchema[bool] = Field(
|
||||||
|
default=False, exclude_if=lambda value: not value
|
||||||
|
)
|
||||||
status: Literal["passed", "failed", "inconclusive", "blocked"] = Field(
|
status: Literal["passed", "failed", "inconclusive", "blocked"] = Field(
|
||||||
description="Judge the system under test against the user's expectations. A correctly executed inspection that reveals an expectation mismatch is failed, even if the executor completed and reported the mismatch correctly."
|
description="Judge the system under test against the user's expectations. A correctly executed inspection that reveals an expectation mismatch is failed, even if the executor completed and reported the mismatch correctly."
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1261,7 +1261,11 @@ class Agent:
|
|||||||
record_video_enabled=self._config.record_video_enabled,
|
record_video_enabled=self._config.record_video_enabled,
|
||||||
disable_checker=self._config.disable_checker,
|
disable_checker=self._config.disable_checker,
|
||||||
disable_midway_checks=self._config.disable_midway_checks,
|
disable_midway_checks=self._config.disable_midway_checks,
|
||||||
disable_final_check=self._config.disable_final_check,
|
disable_final_check=self._config.disable_final_check
|
||||||
|
or (
|
||||||
|
(task.request.profile or "").lower() == "flash"
|
||||||
|
and not self._config.flash.final_check_enabled
|
||||||
|
),
|
||||||
checker_max_iterations=self._config.checker_max_iterations,
|
checker_max_iterations=self._config.checker_max_iterations,
|
||||||
final_check_max_attempts=self._config.final_check_max_attempts,
|
final_check_max_attempts=self._config.final_check_max_attempts,
|
||||||
checkpoint_max_repairs=self._config.checkpoint_max_repairs,
|
checkpoint_max_repairs=self._config.checkpoint_max_repairs,
|
||||||
|
|||||||
@@ -48,6 +48,7 @@
|
|||||||
"max_long_edge": 2048
|
"max_long_edge": 2048
|
||||||
},
|
},
|
||||||
"flash": {
|
"flash": {
|
||||||
|
"final_check_enabled": "${env:ARTEMIS_FLASH_FINAL_CHECK_ENABLED:-false}",
|
||||||
"max_turns": 0,
|
"max_turns": 0,
|
||||||
"explorer_mode": "flash",
|
"explorer_mode": "flash",
|
||||||
"step_summarizer": {
|
"step_summarizer": {
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ Flash 和 Pro 默认复用只读 Checker,以任务提示词和用户指导中
|
|||||||
|
|
||||||
`mobile_run_task(runtime_overrides={"timeout_seconds":1800,"observation_timeout_seconds":60,"observation_max_attempts":3,"device_command_timeout_seconds":10,"action_failure_attempts":3})` 可为单任务覆盖执行保护;其中 `model` 是 API 模型名,外层 `model` 仍是 Flash/Pro 模式。相同字段也支持批次的 `task_config`。全局默认通过网页功能配置或明确授权的 `mobile_config(section="capabilities", changes={"task_timeout_seconds":1800}, persist_defaults=true)` 保存,对后续任务生效,不接受密钥。
|
`mobile_run_task(runtime_overrides={"timeout_seconds":1800,"observation_timeout_seconds":60,"observation_max_attempts":3,"device_command_timeout_seconds":10,"action_failure_attempts":3})` 可为单任务覆盖执行保护;其中 `model` 是 API 模型名,外层 `model` 仍是 Flash/Pro 模式。相同字段也支持批次的 `task_config`。全局默认通过网页功能配置或明确授权的 `mobile_config(section="capabilities", changes={"task_timeout_seconds":1800}, persist_defaults=true)` 保存,对后续任务生效,不接受密钥。
|
||||||
|
|
||||||
|
Flash 最终 AI 判定与分析默认关闭。读取 `mobile_config(section="capabilities")` 的 `flash_final_check_enabled`;用户要求保存全局设置时,可用 `changes={"flash_final_check_enabled":true}` 和 `persist_defaults=true` 开启。它影响尚未开始的所有 Flash 任务,运行中的任务、Pro 和主动探索不变;不作为单任务或精选模板的覆盖项。关闭时 `test_result.status="inconclusive"`、`verification_skipped=true`、`source="system"`,表示未做 AI 判定,不是测试通过;执行异常仍按原失败状态记录。人工复验可随后给出独立结论。
|
||||||
|
|
||||||
任务已经产生最终结果,但 SDK 线程或资源清理仍卡住时,默认最多再等待 30 秒后回收工作进程,避免继续占用队列。全局可用 `ARTEMIS_WORKER_CLEANUP_TIMEOUT_SECONDS` 或功能配置的 `worker_cleanup_timeout_seconds` 调整,任务 JSON 同名字段可覆盖。原有结果与截图保留,未完成保存的录屏尽力恢复。
|
任务已经产生最终结果,但 SDK 线程或资源清理仍卡住时,默认最多再等待 30 秒后回收工作进程,避免继续占用队列。全局可用 `ARTEMIS_WORKER_CLEANUP_TIMEOUT_SECONDS` 或功能配置的 `worker_cleanup_timeout_seconds` 调整,任务 JSON 同名字段可覆盖。原有结果与截图保留,未完成保存的录屏尽力恢复。
|
||||||
|
|
||||||
## 插件证据与留档参数
|
## 插件证据与留档参数
|
||||||
|
|||||||
@@ -14,13 +14,15 @@
|
|||||||
|
|
||||||
报告包含目标、预期结果、执行状态、验证结论、实际输出和步骤证据。Word 内嵌关键截图,Excel 包含结果与证据工作表;任务输出中的 Markdown 表格会转换为真正的文档表格和 Excel 工作表。
|
报告包含目标、预期结果、执行状态、验证结论、实际输出和步骤证据。Word 内嵌关键截图,Excel 包含结果与证据工作表;任务输出中的 Markdown 表格会转换为真正的文档表格和 Excel 工作表。
|
||||||
|
|
||||||
“执行完成”与测试结论分开记录。预期直接写在任务提示词中即可;Flash 和 Pro 默认用只读终检核对提示词、用户指导、执行结论及实际证据,不增加必填表单。任务列表显式标注“测试通过 / 测试不通过 / 未验证 / 测试阻塞”,结果区域和 Word/Excel 同时列出判定依据及证据。终检关闭、出错或缺少证据时显示“未验证”,设备或执行异常不当作产品缺陷;已确认的断言失败会保留。正在执行的任务也可以下载,但文件只代表下载时的快照。
|
“执行完成”与测试结论分开记录。预期直接写在任务提示词中即可;Pro 默认用只读终检核对提示词、用户指导、执行结论及实际证据,不增加必填表单。Flash 的“最终 AI 判定与分析”默认关闭,可在首页功能配置开启;关闭时正常完成的任务显示“执行完成 · 未做 AI 判定”,历史、批次与 Word/Excel 保持一致,不自动记为测试通过。终检出错或缺少证据时仍显示“未验证”,设备或执行异常不当作产品缺陷;已确认的断言失败会保留。正在执行的任务也可以下载,但文件只代表下载时的快照。
|
||||||
|
|
||||||
|
Flash 开关保存在全局配置中,对网页、批量、精选、MCP/SDK 和探索提炼用例的 Flash 复跑统一生效;在任务开始时固定,排队中的任务使用最新配置,运行中的任务不变。开关只省去末尾独立 AI 复核,执行模型的最终答复、步骤摘要收尾和资源清理仍保留,不影响 Pro 或主动探索。环境变量为 `ARTEMIS_FLASH_FINAL_CHECK_ENABLED=false`,JSON 默认在 `agent.flash.final_check_enabled`;旧配置缺少该字段时也默认关闭。
|
||||||
|
|
||||||
终检已输出完整且符合结构的结论时,直接校验并保存,不额外请求模型改写格式;若兼容接口把 `test_result` 编码为 JSON 字符串,会先解码再按同一规则校验。普通说明文字、被截断的输出、缺少检查项或证据的结果不会直接算通过。判定范围以提示词为准:明确要求“发现并如实报告问题、不要求无缺陷”的检查任务通过,不表示被测产品没有缺陷。
|
终检已输出完整且符合结构的结论时,直接校验并保存,不额外请求模型改写格式;若兼容接口把 `test_result` 编码为 JSON 字符串,会先解码再按同一规则校验。普通说明文字、被截断的输出、缺少检查项或证据的结果不会直接算通过。判定范围以提示词为准:明确要求“发现并如实报告问题、不要求无缺陷”的检查任务通过,不表示被测产品没有缺陷。
|
||||||
|
|
||||||
报告由项目自己的 `python-docx`、`openpyxl` 生成,不依赖 Codex 客户端的私有工具。输出保存在 `artifacts/reports/`。
|
报告由项目自己的 `python-docx`、`openpyxl` 生成,不依赖 Codex 客户端的私有工具。输出保存在 `artifacts/reports/`。
|
||||||
|
|
||||||
任务结束后,可在“用例测试结果”中点击“人工复验”,确认通过或不通过,备注选填。复验会保留原 AI 结论和历次复验记录,不改变任务的执行状态。任务队列、单任务报告和批次汇总采用最近一次人工结论,标明“人工确认”;未复验的结论标明“AI确认”。复验后下载的报告包会重新生成。
|
任务结束后,可在“用例测试结果”中点击“人工复验”,确认通过或不通过,备注选填。复验会保留原始结果和历次复验记录,不改变任务的执行状态。任务队列、单任务报告和批次汇总采用最近一次人工结论,标明“人工确认”;已做 AI 判定的结论标明“AI确认”,关闭判定的结果标明“未做 AI 判定”。复验后下载的报告包会重新生成。
|
||||||
|
|
||||||
任务队列的“任务范围”可筛选全部批量任务、单独任务或指定批次,日期、搜索和状态筛选可同时使用。点击批次内的用例会在左侧打开执行记录,右侧保留批次列表;可用“在任务队列查看此批次”集中查看,进入单任务报告后也可返回原批次。
|
任务队列的“任务范围”可筛选全部批量任务、单独任务或指定批次,日期、搜索和状态筛选可同时使用。点击批次内的用例会在左侧打开执行记录,右侧保留批次列表;可用“在任务队列查看此批次”集中查看,进入单任务报告后也可返回原批次。
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# ARTEMIS-Eapil 2.1.0
|
||||||
|
|
||||||
|
本版增加 Flash 最终 AI 判定与分析开关,并修复全新安装时因 Google 字体访问超时而无法完成网页构建的问题。
|
||||||
|
|
||||||
|
## Flash 最终 AI 判定可选
|
||||||
|
|
||||||
|
- 在首页打开“系统设置与运行准备”,找到“功能开关”中的 **Flash 最终 AI 判定与分析**,保存选择;默认关闭。
|
||||||
|
- 关闭时只跳过 Flash 执行结束后,按用例需求和执行步骤进行的最终 AI 复核。执行过程中的 AI 操作及必要检查继续保留,Pro 模式沿用现有行为。
|
||||||
|
- 正常执行结束但未做最终判定时,页面显示 **执行完成 · 未做 AI 判定**,不自动记为测试通过;执行报错仍显示失败。历史、报告、人工复验及 MCP 结果沿用相同口径。
|
||||||
|
- 该设置统一用于网页单任务、批量、精选任务、MCP/SDK 及探索用例的 Flash 复跑。任务开始时读取并固定:修改后尚未开始的任务使用新值,运行中的任务保持原值。
|
||||||
|
|
||||||
|
## 修复字体网络依赖
|
||||||
|
|
||||||
|
- 将 Inter、Outfit、Roboto 和完整 Material Symbols 图标字体随源码分发,移除全局样式及页面入口中的 Google Fonts 外部引用和预连接。
|
||||||
|
- 生产构建及字体显示无需访问 Google Fonts,避免网络超时导致启动脚本报“网页构建失败”。图标字形及可变轴保留,字体来源、校验值和许可证见[本地字体说明](../../apps/showcase_ui/public/fonts/README.md)。
|
||||||
|
- 首次安装仍需下载 Python/Node 依赖,可按网络环境选择官方源或国内镜像启动脚本。
|
||||||
|
|
||||||
|
## 升级与安装
|
||||||
|
|
||||||
|
1. 先结束正在运行的任务,再运行 `update.bat`;国内网络使用 `update-cn.bat`。升级程序会停止当前项目的服务与任务进程,并同步依赖环境。
|
||||||
|
2. 完成后运行 `start.bat` / `start-cn.bat`,等待网页构建并打开页面。启动脚本只检查更新,不代替升级脚本。
|
||||||
|
3. 全新安装下载 `artemis-eapil-2.1.0-source.zip`,解压后运行上述启动脚本。用附件 `SHA256SUMS.txt` 校验下载完整性。
|
||||||
|
|
||||||
|
源码包保留浅层 Git 更新能力,包含本地字体及许可证;不包含依赖环境、用户密钥、任务历史或预编译网页。升级主程序不会自动升级已安装插件。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
- 本轮开发的默认后端测试 **2967 项通过、2 项跳过、8 项按标记排除**;Ruff 格式与静态检查、质量基线和核心类型检查通过。
|
||||||
|
- 前端 **292 项测试通过**;禁用 Angular 构建缓存并阻断构建进程网络后,生产构建成功,未发生网络请求。
|
||||||
|
- 浏览器在仅允许同源字体的环境中确认四套字体均可加载,描边及填充图标显示正常;字体及许可证已完整复制到构建产物,源码打包过滤规则保留这些资源。
|
||||||
|
- 升版后启动导入、安装资源和源码归档相关的 **12 项测试通过**,前后端及锁文件版本均为 `2.1.0`,再次完成无网络生产构建。
|
||||||
|
|
||||||
|
以上为确定性验证;真实设备及付费模型流程未纳入本轮测试。
|
||||||
@@ -431,7 +431,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "mobile_config",
|
"name": "mobile_config",
|
||||||
"description": "Read configuration or explicitly save non-secret defaults for FUTURE tasks.\n\nOnly set persist_defaults=true when the user requests saved/global configuration.\nmodel changes: model (preset or custom), base_url, reasoning_effort\n(default/none/minimal/low/medium/high/xhigh/max), timeout_seconds, max_retries,\nretry_interval_seconds, rate_limit_retry_seconds, tool_fallback_model,\ntool_fallback_reasoning_effort, tool_fallback_max_retries,\ntool_fallback_no_progress_attempts. 'default' omits reasoning;\nunsupported values fail visibly.\ncapabilities: video_enabled and recording_enabled are independent; outputter_enabled,\nstep_summarizer_enabled, screenshot_limit_enabled, task_timeout_seconds (default 1800),\nobservation_timeout_seconds (60), observation_max_attempts (3),\ndevice_command_timeout_seconds (10), action_failure_attempts (3),\nworker_cleanup_timeout_seconds (30), adb_auto_connect (true),\nadb_discovery_interval_seconds (10), adb_discovery_timeout_seconds (5).\nreports: formats=['docx','xlsx'] (at least one).\nexploration: model (default gpt-6.1-sol), reasoning_effort (high), timeout_seconds\n(1800 active seconds), demonstration_timeout_seconds (900), demonstration_warning_seconds (60).\nAPI keys are never accepted or returned: use the Web UI or .env for credentials.\nSaving settings does not change an already running task. Flash/Pro are execution\nprofiles, not model names. Model retries do not repeat device test cases.\n",
|
"description": "Read configuration or explicitly save non-secret defaults for FUTURE tasks.\n\nOnly set persist_defaults=true when the user requests saved/global configuration.\nmodel changes: model (preset or custom), base_url, reasoning_effort\n(default/none/minimal/low/medium/high/xhigh/max), timeout_seconds, max_retries,\nretry_interval_seconds, rate_limit_retry_seconds, tool_fallback_model,\ntool_fallback_reasoning_effort, tool_fallback_max_retries,\ntool_fallback_no_progress_attempts. 'default' omits reasoning;\nunsupported values fail visibly.\ncapabilities: video_enabled and recording_enabled are independent; outputter_enabled,\nflash_final_check_enabled (false; Flash exit-only AI review),\nstep_summarizer_enabled, screenshot_limit_enabled, task_timeout_seconds (default 1800),\nobservation_timeout_seconds (60), observation_max_attempts (3),\ndevice_command_timeout_seconds (10), action_failure_attempts (3),\nworker_cleanup_timeout_seconds (30), adb_auto_connect (true),\nadb_discovery_interval_seconds (10), adb_discovery_timeout_seconds (5).\nreports: formats=['docx','xlsx'] (at least one).\nexploration: model (default gpt-6.1-sol), reasoning_effort (high), timeout_seconds\n(1800 active seconds), demonstration_timeout_seconds (900), demonstration_warning_seconds (60).\nAPI keys are never accepted or returned: use the Web UI or .env for credentials.\nSaving settings does not change an already running task. Flash/Pro are execution\nprofiles, not model names. Model retries do not repeat device test cases.\n",
|
||||||
"inputSchema": {
|
"inputSchema": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"section": {
|
"section": {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ CAPABILITY_FIELDS = {
|
|||||||
"recording_enabled",
|
"recording_enabled",
|
||||||
"recording",
|
"recording",
|
||||||
"outputter_enabled",
|
"outputter_enabled",
|
||||||
|
"flash_final_check_enabled",
|
||||||
"step_summarizer_enabled",
|
"step_summarizer_enabled",
|
||||||
"screenshot_limit_enabled",
|
"screenshot_limit_enabled",
|
||||||
"task_timeout_seconds",
|
"task_timeout_seconds",
|
||||||
@@ -192,6 +193,7 @@ async def mobile_config(
|
|||||||
tool_fallback_no_progress_attempts. 'default' omits reasoning;
|
tool_fallback_no_progress_attempts. 'default' omits reasoning;
|
||||||
unsupported values fail visibly.
|
unsupported values fail visibly.
|
||||||
capabilities: video_enabled and recording_enabled are independent; outputter_enabled,
|
capabilities: video_enabled and recording_enabled are independent; outputter_enabled,
|
||||||
|
flash_final_check_enabled (false; Flash exit-only AI review),
|
||||||
step_summarizer_enabled, screenshot_limit_enabled, task_timeout_seconds (default 1800),
|
step_summarizer_enabled, screenshot_limit_enabled, task_timeout_seconds (default 1800),
|
||||||
observation_timeout_seconds (60), observation_max_attempts (3),
|
observation_timeout_seconds (60), observation_max_attempts (3),
|
||||||
device_command_timeout_seconds (10), action_failure_attempts (3),
|
device_command_timeout_seconds (10), action_failure_attempts (3),
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "artemis"
|
name = "artemis"
|
||||||
version = "2.0.1"
|
version = "2.1.0"
|
||||||
description = "Autonomous AI Assistant for Mobile Automation"
|
description = "Autonomous AI Assistant for Mobile Automation"
|
||||||
|
|
||||||
authors = [
|
authors = [
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ ARTEMIS 自己编排手机和已授权插件,共用网页的任务队列、历
|
|||||||
|
|
||||||
## 三种模式
|
## 三种模式
|
||||||
|
|
||||||
- **Flash**:路径明确的快速执行,单个执行模型观察并操作;适合冒烟、稳定流程与探索后复跑。可以接受中途指导,也有结束验收。任务长不自动等于需要 Pro。
|
- **Flash**:路径明确的快速执行,单个执行模型观察并操作;适合冒烟、稳定流程与探索后复跑。可以接受中途指导;最终 AI 判定与分析默认关闭,可在全局功能配置开启。关闭时显示“未做 AI 判定”,不能当作验收通过。任务长不自动等于需要 Pro。
|
||||||
- **Pro**:需要持久计划、多个分支、检查点、ADB/日志诊断或详细报告;按计划执行并验证。`explorer_mode` 是 Pro 的视觉定位深度,不是探索任务模式。
|
- **Pro**:需要持久计划、多个分支、检查点、ADB/日志诊断或详细报告;按计划执行并验证。`explorer_mode` 是 Pro 的视觉定位深度,不是探索任务模式。
|
||||||
- **探索**:用户只有大概流程,需要与模型交流、纠正方向或人工示范;以 Pro 的执行能力推进,最终提炼一个可复用用例。超时保存并暂停,可继续。探索成功、生成用例和 Flash/Pro 复跑成功是三个不同结果。
|
- **探索**:用户只有大概流程,需要与模型交流、纠正方向或人工示范;以 Pro 的执行能力推进,最终提炼一个可复用用例。超时保存并暂停,可继续。探索成功、生成用例和 Flash/Pro 复跑成功是三个不同结果。
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| 适合 | 目标清楚、路径明确、低分支流程;冒烟和已提炼用例复跑 | 复杂分支、持久计划、检查点、长程监控、ADB/日志、详细记录 | 流程尚未定型,用户参与纠正或示范的一个复杂用例 |
|
| 适合 | 目标清楚、路径明确、低分支流程;冒烟和已提炼用例复跑 | 复杂分支、持久计划、检查点、长程监控、ADB/日志、详细记录 | 流程尚未定型,用户参与纠正或示范的一个复杂用例 |
|
||||||
| 执行方式 | 单执行模型观察→动作;需要时使用定位等辅助工具 | Planner 制订计划;Operator 执行;Checker 验收;按需生成报告 | 复用 Pro 工具和计划,加持续对话、提问等待、示范分析和用例提炼 |
|
| 执行方式 | 单执行模型观察→动作;需要时使用定位等辅助工具 | Planner 制订计划;Operator 执行;Checker 验收;按需生成报告 | 复用 Pro 工具和计划,加持续对话、提问等待、示范分析和用例提炼 |
|
||||||
| 验收 | 默认结束时只读验收 | 默认结束验收,可配置检查点/strict | 先处理用户指导,再验收探索目标;不把示范当独立复跑通过 |
|
| 验收 | 最终 AI 判定与分析默认关闭,可在全局功能配置开启 | 默认结束验收,可配置检查点/strict | 先处理用户指导,再验收探索目标;不把示范当独立复跑通过 |
|
||||||
| 中途输入 | `inject_instruction` 指导当前任务 | 同 Flash,还能记录计划及诊断信息 | `message` 持续交流;结束后消息仅讨论,再操作需 `continue` |
|
| 中途输入 | `inject_instruction` 指导当前任务 | 同 Flash,还能记录计划及诊断信息 | `message` 持续交流;结束后消息仅讨论,再操作需 `continue` |
|
||||||
| 入口 | `mobile_run_task(model="Flash")` | `mobile_run_task(model="Pro")` | `mobile_exploration(create/start/...)` |
|
| 入口 | `mobile_run_task(model="Flash")` | `mobile_run_task(model="Pro")` | `mobile_exploration(create/start/...)` |
|
||||||
| 产物 | 步骤、证据、测试结果,按需导出报告 | 以上加计划、笔记和可选详细报告 | 对话、各轮执行和示范证据、一个用例的不可变版本 |
|
| 产物 | 步骤、证据、测试结果,按需导出报告 | 以上加计划、笔记和可选详细报告 | 对话、各轮执行和示范证据、一个用例的不可变版本 |
|
||||||
|
|||||||
@@ -951,6 +951,51 @@ def test_human_review_overrides_every_report_preserves_ai_and_invalidates_batch_
|
|||||||
assert outcome_file.read_text(encoding="utf-8") == original
|
assert outcome_file.read_text(encoding="utf-8") == original
|
||||||
|
|
||||||
|
|
||||||
|
def test_skipped_flash_review_is_consistent_in_history_reports_and_human_review(
|
||||||
|
client, isolated, monkeypatch
|
||||||
|
):
|
||||||
|
from artemis.runtime import case_result
|
||||||
|
|
||||||
|
monkeypatch.setattr(case_result, "TRACES_PATH", reports.TRACES_PATH)
|
||||||
|
batch = service.create_batch(payload())
|
||||||
|
job = isolated.jobs(batch["id"])[0]
|
||||||
|
isolated.save_job({**job, "status": "completed"})
|
||||||
|
folder = reports.TRACES_PATH / job["session_id"]
|
||||||
|
folder.mkdir(parents=True)
|
||||||
|
result = {
|
||||||
|
"status": "inconclusive",
|
||||||
|
"reason": "执行完成 · 未做 AI 判定",
|
||||||
|
"evidence": [],
|
||||||
|
"verification_skipped": True,
|
||||||
|
}
|
||||||
|
(folder / "run_outcome.json").write_text(
|
||||||
|
json.dumps({"task_status": "completed", "tests": {}, "test_result": result}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
for item in (
|
||||||
|
client.get("/api/automation/history").json()["items"][0],
|
||||||
|
client.get(f"/api/automation/batches/{batch['id']}").json()["jobs"][0],
|
||||||
|
client.get(f"/api/automation/reports/{job['session_id']}").json(),
|
||||||
|
):
|
||||||
|
assert item["status"] == "completed"
|
||||||
|
assert item["test_result"] == {**result, "source": "system"}
|
||||||
|
assert reports.session_report(job["session_id"])["verdict"] == "未做 AI 判定"
|
||||||
|
paths = reports.export_session(job["session_id"])
|
||||||
|
workbook = load_workbook(next(path for path in paths if path.suffix == ".xlsx"))
|
||||||
|
assert "未做 AI 判定" in [
|
||||||
|
cell.value for row in workbook["任务结果"].iter_rows() for cell in row
|
||||||
|
]
|
||||||
|
doc = Document(next(path for path in paths if path.suffix == ".docx"))
|
||||||
|
assert "未做 AI 判定" in "\n".join(paragraph.text for paragraph in doc.paragraphs)
|
||||||
|
reviewed = client.put(
|
||||||
|
f"/api/automation/reports/{job['session_id']}/review",
|
||||||
|
json={"status": "passed", "reason": "已人工核对"},
|
||||||
|
).json()
|
||||||
|
assert reviewed["test_result"]["source"] == "human"
|
||||||
|
assert reviewed["test_result"]["ai_result"] == {**result, "source": "system"}
|
||||||
|
assert reviewed["verdict"] == "测试通过(人工确认)"
|
||||||
|
|
||||||
|
|
||||||
def test_review_rejects_active_missing_invalid_and_packing_tasks(client, isolated, monkeypatch):
|
def test_review_rejects_active_missing_invalid_and_packing_tasks(client, isolated, monkeypatch):
|
||||||
job = isolated.jobs(service.create_batch(payload())["id"])[0]
|
job = isolated.jobs(service.create_batch(payload())["id"])[0]
|
||||||
url = f"/api/automation/reports/{job['session_id']}/review"
|
url = f"/api/automation/reports/{job['session_id']}/review"
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ def isolated_settings(monkeypatch, tmp_path):
|
|||||||
"OPENAI_REASONING_EFFORT",
|
"OPENAI_REASONING_EFFORT",
|
||||||
"ARTEMIS_VIDEO_ENABLED",
|
"ARTEMIS_VIDEO_ENABLED",
|
||||||
"ARTEMIS_OUTPUTTER_ENABLED",
|
"ARTEMIS_OUTPUTTER_ENABLED",
|
||||||
|
"ARTEMIS_FLASH_FINAL_CHECK_ENABLED",
|
||||||
"ARTEMIS_STEP_SUMMARIZER_ENABLED",
|
"ARTEMIS_STEP_SUMMARIZER_ENABLED",
|
||||||
"ARTEMIS_SCREENSHOT_LIMIT_ENABLED",
|
"ARTEMIS_SCREENSHOT_LIMIT_ENABLED",
|
||||||
"ARTEMIS_TASK_TIMEOUT_SECONDS",
|
"ARTEMIS_TASK_TIMEOUT_SECONDS",
|
||||||
@@ -149,6 +150,36 @@ def test_capabilities_persist_and_apply_to_new_tasks(isolated_settings):
|
|||||||
assert load_agent_config().video_analyzer.enabled is True
|
assert load_agent_config().video_analyzer.enabled is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_flash_review_defaults_off_and_persists_without_changing_pro(isolated_settings):
|
||||||
|
assert Settings(_env_file=None).ARTEMIS_FLASH_FINAL_CHECK_ENABLED is False
|
||||||
|
assert service.get_capability_settings().flash_final_check_enabled is False
|
||||||
|
assert load_agent_config().checker.final_check is True
|
||||||
|
for enabled in (True, False):
|
||||||
|
saved = service.save_capability_settings(
|
||||||
|
service.CapabilitySettings(flash_final_check_enabled=enabled)
|
||||||
|
)
|
||||||
|
assert saved.flash_final_check_enabled is enabled
|
||||||
|
assert Settings(_env_file=isolated_settings).ARTEMIS_FLASH_FINAL_CHECK_ENABLED is enabled
|
||||||
|
config = load_agent_config()
|
||||||
|
assert config.flash.final_check_enabled is enabled
|
||||||
|
assert config.checker.final_check is True
|
||||||
|
assert config.pro.checker.final_check is True
|
||||||
|
# Older clients omit the new switch; saving another setting keeps it.
|
||||||
|
assert (
|
||||||
|
service.save_capability_settings(
|
||||||
|
service.CapabilitySettings(recording_enabled=False)
|
||||||
|
).flash_final_check_enabled
|
||||||
|
is enabled
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_old_checker_config_does_not_enable_flash_review(monkeypatch, tmp_path):
|
||||||
|
monkeypatch.delenv("ARTEMIS_FLASH_FINAL_CHECK_ENABLED", raising=False)
|
||||||
|
path = tmp_path / "artemis.jsonc"
|
||||||
|
path.write_text('{"agent":{"checker":{"enabled":true,"final_check":true}}}', encoding="utf-8")
|
||||||
|
assert load_agent_config(path).flash.final_check_enabled is False
|
||||||
|
|
||||||
|
|
||||||
def test_screenshot_json_can_override_environment(monkeypatch, tmp_path):
|
def test_screenshot_json_can_override_environment(monkeypatch, tmp_path):
|
||||||
config_path = tmp_path / "artemis.jsonc"
|
config_path = tmp_path / "artemis.jsonc"
|
||||||
config_path.write_text(
|
config_path.write_text(
|
||||||
|
|||||||
@@ -767,6 +767,34 @@ async def test_enqueue_tasks_unified_ingress():
|
|||||||
assert task["goal"] == "Test unified goal"
|
assert task["goal"] == "Test unified goal"
|
||||||
|
|
||||||
|
|
||||||
|
def test_flash_review_is_refreshed_at_launch_and_frozen_in_worker_environment(monkeypatch):
|
||||||
|
from apps.admin_console.services.execution_config import execution_config
|
||||||
|
|
||||||
|
monkeypatch.setattr("artemis.exploration.store.explorations.run", lambda _sid: None)
|
||||||
|
monkeypatch.setattr("apps.admin_console.services.automation_store.store.job", lambda _sid: None)
|
||||||
|
item = {"profile": "flash", "goal": "queued", "session_id": "flash-config-snapshot"}
|
||||||
|
monkeypatch.setenv("ARTEMIS_FLASH_FINAL_CHECK_ENABLED", "false")
|
||||||
|
old_snapshot = execution_config(item)
|
||||||
|
item["execution_config"] = old_snapshot
|
||||||
|
assert old_snapshot["flash_final_check_enabled"] is False
|
||||||
|
assert "flash_final_check_enabled" not in old_snapshot["runtime_overrides"]
|
||||||
|
monkeypatch.setenv("ARTEMIS_FLASH_FINAL_CHECK_ENABLED", "true")
|
||||||
|
item["execution_config"] = execution_config(item)
|
||||||
|
assert item["execution_config"]["flash_final_check_enabled"] is True
|
||||||
|
# Simulate a later save between this task's snapshot and worker launch.
|
||||||
|
monkeypatch.setenv("ARTEMIS_FLASH_FINAL_CHECK_ENABLED", "false")
|
||||||
|
_cmd, env = TaskQueueService._build_worker_invocation(
|
||||||
|
item,
|
||||||
|
"flash-config-snapshot",
|
||||||
|
item["session_id"],
|
||||||
|
item["goal"],
|
||||||
|
"flash",
|
||||||
|
TaskQueueService._task_target(item),
|
||||||
|
)
|
||||||
|
assert env["ARTEMIS_FLASH_FINAL_CHECK_ENABLED"] == "true"
|
||||||
|
assert os.environ["ARTEMIS_FLASH_FINAL_CHECK_ENABLED"] == "false"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_queue_worker_notifies_conversation():
|
async def test_queue_worker_notifies_conversation():
|
||||||
"""Verify queue_worker calls notify() when conversation_id is attached to task."""
|
"""Verify queue_worker calls notify() when conversation_id is attached to task."""
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ async def test_acceptance_timeout_finishes_with_unknown_verdict(tmp_path, monkey
|
|||||||
)
|
)
|
||||||
assert data["task_status"] == "completed"
|
assert data["task_status"] == "completed"
|
||||||
assert data["test_result"]["status"] == "inconclusive"
|
assert data["test_result"]["status"] == "inconclusive"
|
||||||
|
|
||||||
assert "总预算已用尽(0.01 秒)" in data["test_result"]["reason"]
|
assert "总预算已用尽(0.01 秒)" in data["test_result"]["reason"]
|
||||||
|
|
||||||
|
|
||||||
@@ -155,6 +156,23 @@ async def test_disabled_acceptance_does_not_call_a_model_or_claim_pass(
|
|||||||
)
|
)
|
||||||
check.assert_not_called()
|
check.assert_not_called()
|
||||||
assert data["test_result"]["status"] == "inconclusive"
|
assert data["test_result"]["status"] == "inconclusive"
|
||||||
|
assert data["test_result"]["verification_skipped"] is True
|
||||||
|
assert "未做 AI 判定" in data["test_result"]["reason"]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_skipping_review_does_not_hide_execution_failure(tmp_path, monkeypatch):
|
||||||
|
check = AsyncMock()
|
||||||
|
monkeypatch.setattr(flash, "run_final_check", check)
|
||||||
|
data = await flash.settle_flash_outcome(
|
||||||
|
context(tmp_path, disable_final_check=True),
|
||||||
|
"任务",
|
||||||
|
{"status": "failed", "explanation": "设备断开"},
|
||||||
|
)
|
||||||
|
check.assert_not_called()
|
||||||
|
assert data["task_status"] == "blocked"
|
||||||
|
assert data["test_result"]["status"] == "blocked"
|
||||||
|
assert not data["test_result"].get("verification_skipped")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
|
|||||||
@@ -74,6 +74,24 @@ async def test_config_requires_explicit_persistence_and_excludes_secrets(monkeyp
|
|||||||
assert payload["tool_fallback_model"] == "gpt-6-luna"
|
assert payload["tool_fallback_model"] == "gpt-6-luna"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_config_can_save_flash_review_without_exposing_task_overrides(monkeypatch):
|
||||||
|
request = AsyncMock(
|
||||||
|
return_value={"flash_final_check_enabled": False, "recording_enabled": False}
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(tools, "_request", request)
|
||||||
|
await tools.mobile_config(
|
||||||
|
section="capabilities",
|
||||||
|
changes={"flash_final_check_enabled": True},
|
||||||
|
persist_defaults=True,
|
||||||
|
)
|
||||||
|
assert request.call_args.args == ("POST", "/api/system/capability-settings")
|
||||||
|
assert request.call_args.kwargs["json"] == {
|
||||||
|
"flash_final_check_enabled": True,
|
||||||
|
"recording_enabled": False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_preview_is_read_only_and_create_is_draft(monkeypatch, tmp_path):
|
async def test_preview_is_read_only_and_create_is_draft(monkeypatch, tmp_path):
|
||||||
source = tmp_path / "cases.xlsx"
|
source = tmp_path / "cases.xlsx"
|
||||||
|
|||||||
@@ -114,11 +114,24 @@ async def test_locked_work_profile_does_not_block_unlocked_device_owner():
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_agent_inherits_session_id_from_env_and_propagates_to_tracing(monkeypatch):
|
@pytest.mark.parametrize(
|
||||||
|
"profile,enabled,expected",
|
||||||
|
[
|
||||||
|
("flash", False, False),
|
||||||
|
("flash", True, True),
|
||||||
|
("Flash", False, False),
|
||||||
|
("pro", False, True),
|
||||||
|
("pro", True, True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_agent_inherits_session_id_from_env_and_propagates_to_tracing(
|
||||||
|
monkeypatch, profile, enabled, expected
|
||||||
|
):
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
canonical_sid = "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"
|
canonical_sid = "a1b2c3d4-e5f6-4a5b-8c9d-0e1f2a3b4c5d"
|
||||||
monkeypatch.setenv("ARTEMIS_SESSION_ID", canonical_sid)
|
monkeypatch.setenv("ARTEMIS_SESSION_ID", canonical_sid)
|
||||||
|
monkeypatch.setenv("ARTEMIS_FLASH_FINAL_CHECK_ENABLED", str(enabled).lower())
|
||||||
|
|
||||||
agent = Agent()
|
agent = Agent()
|
||||||
assert agent._session_id == canonical_sid
|
assert agent._session_id == canonical_sid
|
||||||
@@ -127,7 +140,7 @@ async def test_agent_inherits_session_id_from_env_and_propagates_to_tracing(monk
|
|||||||
task.id = "fallback-id"
|
task.id = "fallback-id"
|
||||||
task.get_name.return_value = "task-name"
|
task.get_name.return_value = "task-name"
|
||||||
task.request.goal = "test goal"
|
task.request.goal = "test goal"
|
||||||
task.request.profile = "flash"
|
task.request.profile = profile
|
||||||
task.request.task_name = "task-name"
|
task.request.task_name = "task-name"
|
||||||
task.request.trace_path = "traces"
|
task.request.trace_path = "traces"
|
||||||
|
|
||||||
@@ -138,6 +151,7 @@ async def test_agent_inherits_session_id_from_env_and_propagates_to_tracing(monk
|
|||||||
mock_data_engine_cls.return_value = mock_data_engine
|
mock_data_engine_cls.return_value = mock_data_engine
|
||||||
|
|
||||||
agent._prepare_tracing(task=task, context=context)
|
agent._prepare_tracing(task=task, context=context)
|
||||||
|
assert context.execution_setup.final_check_enabled is expected
|
||||||
|
|
||||||
mock_data_engine.start_session.assert_called_once()
|
mock_data_engine.start_session.assert_called_once()
|
||||||
call_kwargs = mock_data_engine.start_session.call_args.kwargs
|
call_kwargs = mock_data_engine.start_session.call_args.kwargs
|
||||||
|
|||||||
@@ -44,6 +44,25 @@ def test_completed_execution_and_failed_test_are_independent():
|
|||||||
assert resolve_trace_suffix("completed", outcome("failed")) == "_TESTFAIL"
|
assert resolve_trace_suffix("completed", outcome("failed")) == "_TESTFAIL"
|
||||||
|
|
||||||
|
|
||||||
|
def test_skipped_review_metadata_survives_reload_without_changing_model_schema(tmp_path):
|
||||||
|
result = CaseTestResult(
|
||||||
|
status="inconclusive", reason="执行完成 · 未做 AI 判定", verification_skipped=True
|
||||||
|
)
|
||||||
|
directory = tmp_path / "task-skipped"
|
||||||
|
directory.mkdir()
|
||||||
|
(directory / "run_outcome.json").write_text(
|
||||||
|
json.dumps({"task_status": "completed", "test_result": result.model_dump()}),
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
loaded = read_case_result("task-skipped", "completed", root=tmp_path)
|
||||||
|
assert loaded == result.model_dump()
|
||||||
|
assert loaded["verification_skipped"] is True
|
||||||
|
assert "verification_skipped" not in CaseTestResult.model_json_schema()["properties"]
|
||||||
|
assert (
|
||||||
|
"verification_skipped" not in CaseTestResult(status="blocked", reason="失败").model_dump()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("status", ["failed", "cancelled", "interrupted", "skipped"])
|
@pytest.mark.parametrize("status", ["failed", "cancelled", "interrupted", "skipped"])
|
||||||
def test_incomplete_execution_does_not_inherit_a_pass(status):
|
def test_incomplete_execution_does_not_inherit_a_pass(status):
|
||||||
result = resolve_case_result(outcome(), status, "环境初始化失败")
|
result = resolve_case_result(outcome(), status, "环境初始化失败")
|
||||||
|
|||||||
Reference in new issue
Block a user