插件
为安卓测试扩展电脑等操作能力,AI 按任务需要自主调用。
插件
扩展任务操作能力,也可为回放界面和报告添加人工评审等功能。
diff --git a/.env.example b/.env.example index ef935a5..809a3a0 100644 --- a/.env.example +++ b/.env.example @@ -89,7 +89,7 @@ ARTEMIS_BATCH_MAX_EXECUTIONS=10000 ARTEMIS_IMPORT_MAX_BYTES=10485760 # 插件连接与安装预算(修改后重启服务) -ARTEMIS_PLUGIN_MARKETPLACE_URL=https://git.playones.com/chenmingxuan/Artemis-Plugins/raw/branch/main/catalog.json +ARTEMIS_PLUGIN_MARKETPLACE_URL=https://git.playones.com/chenmingxuan/Artemis-Plugins/raw/branch/main/catalog-v3.json ARTEMIS_PLUGIN_START_TIMEOUT_SECONDS=30 ARTEMIS_PLUGIN_TOOL_TIMEOUT_SECONDS=60 ARTEMIS_PLUGIN_SHUTDOWN_TIMEOUT_SECONDS=15 diff --git a/DESIGN.md b/DESIGN.md index 437837a..b2142d8 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -183,3 +183,11 @@ Flash 的最终 AI 判定与分析独立配置,默认关闭;首页功能配 - 保留可见焦点、禁用反馈和减少动画偏好;录制红点表示实际录制,等待或执行中使用各自文案。 - 源码已更新而页面仍旧时,先核对服务目录、构建与缓存;不通过反复增加样式覆盖掩盖旧构建问题。 - 视觉变化验证代表性宽度、长标题、加载、等待回答、错误与展开状态;文档格式检查不等于渲染验证。开发检查按[前端指南](apps/showcase_ui/AGENTS.md)执行。 + +## Workspace plugin extensions + +工作区扩展使用声明式清单版本 3,宿主提供回放面板、人工表单、任务存储和报告接口;不运行插件页面脚本,不另建队列或结果库。纯工作区插件不加入模型执行选择。接口定义见 [工作区插件](docs/workspace-plugins.md)。 + +回放两侧面板默认收起、独立展开,普通浮窗和影院模式共用;插件声明期望/最小/最大宽度,宿主按视口收缩,窄屏一次展开一侧,始终保持面板位于回放的左侧或右侧。任务需求只展示本次运行最初的提示词,复用安全 Markdown 渲染。人工表单字段均可留空,不调用模型、不覆盖已有执行结论或人工复验。 + +评审按单次任务和插件 ID 保存版本历史;运行中只读,终态可编辑或清空。停用/卸载隐藏面板与新导出字段但保留记录,任务删除时同步清理。报告只采用已安装且启用的贡献,空值保持空白;修改记录或插件状态使后续报告缓存失效。 diff --git a/README.md b/README.md index d5b5079..ebeaadd 100644 --- a/README.md +++ b/README.md @@ -2,13 +2,13 @@ 基于 [ARTEMIS](https://github.com/google/artemis) 扩展的 Android AI 自动化测试平台。用自然语言描述任务,即可通过简体中文网页或 MCP 驱动真机、模拟器执行操作并收集结果。模型统一使用 **OpenAI Responses API**。 -**2.2.0**:新增无需手机在线的仅 PC 测试、全局串行队列、暗色模式、Markdown 表格及可折叠复制的工具结果;修复安卓输入清空、观测恢复、排队状态与最终验收匹配。升级内容、验证范围与配套 Cua 插件见 [2.2.0 发行说明](docs/releases/2.2.0.md)。上一版本见 [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)。 +**2.3.0**:新增工作区插件接口,支持步骤回放左右侧栏、Markdown 初始需求、人工评审及报告字段扩展;配套「任务人工评审 1.0.0」从插件市场独立安装。新增 gpt-6.1-terra,探索默认 high。见 [2.3.0 发行说明](docs/releases/2.3.0.md);上一版本见 [2.2.0](docs/releases/2.2.0.md)。 ## 主要功能 - **自然语言任务**:Flash 适合明确的操作流程,Pro 适合需要计划、分支探索和验证的任务。 - **交互探索**:边执行边交流,也可录制人工示范;探索成功后生成一个可编辑、复跑的用例。默认使用独立的 `gpt-6.1-terra / high` 配置,超时暂停并保留进度。见 [探索模式](docs/exploration-mode.md)。 -- **插件扩展**:在插件市场安装并启用电脑控制等能力,AI 在安卓任务中按需使用;手机、电脑的步骤与证据统一留档。见 [插件与跨端任务](docs/plugins.md)。 +- **插件扩展**:安装并启用电脑控制等执行能力,或使用工作区插件为回放和报告增加人工评审。手机、电脑的步骤与证据统一留档。见 [插件与跨端任务](docs/plugins.md)和[工作区插件接口](docs/workspace-plugins.md)。 - **可保存的模型配置**:支持自定义接口、模型、思考强度和超时重试,以及视频分析与录屏开关。 - **批量测试**:按新版 Excel 模板导入提示词,每项可独立指定模型、模式和参数;任务可暂停、补充指导并继续。 - **精选任务**:收藏单任务、批次或探索用例,在首页一键复跑,默认沿用已保存的配置。 diff --git a/apps/admin_console/routers/plugins.py b/apps/admin_console/routers/plugins.py index b9e105d..69ae102 100644 --- a/apps/admin_console/routers/plugins.py +++ b/apps/admin_console/routers/plugins.py @@ -214,6 +214,9 @@ async def diagnose_plugin(plugin_id: str, request: DiagnoseRequest): try: store = get_store() manifest = PluginManifest.model_validate(store.get(plugin_id)["manifest"]) + if manifest.workspace and manifest.server is None and manifest.skill is None: + store.resolve(PluginSelection(id=plugin_id, profile=request.profile)) + return {"ok": True, "tools": {}, "message": "工作区扩展已就绪"} runtime = PluginRuntime( [PluginSelection(id=plugin_id, profile=request.profile)], store=store, diff --git a/apps/admin_console/routers/task_extensions.py b/apps/admin_console/routers/task_extensions.py new file mode 100644 index 0000000..12ad3e8 --- /dev/null +++ b/apps/admin_console/routers/task_extensions.py @@ -0,0 +1,67 @@ +"""Workspace UI extension API. No model or device actions are dispatched.""" + +from uuid import UUID + +from fastapi import APIRouter, HTTPException +from pydantic import BaseModel, ConfigDict, Field + +from apps.admin_console.services import batch_archive, report_export, task_extensions +from apps.admin_console.services.automation_store import store + +router = APIRouter(prefix="/api/task-extensions", tags=["task-extensions"]) + + +class ExtensionUpdate(BaseModel): + model_config = ConfigDict(extra="forbid") + values: dict[str, str] = Field(max_length=20) + expected_revision: int = Field(ge=0) + request_id: UUID + + +@router.get("/{session_id}") +def read_extensions(session_id: str): + try: + return { + **task_extensions.task_context(session_id, store), + "plugins": task_extensions.contributions(session_id, store), + } + except ValueError as exc: + raise HTTPException(404, str(exc)) from exc + + +@router.put("/{session_id}/{plugin_id}") +def update_extension(session_id: str, plugin_id: str, value: ExtensionUpdate): + job = store.job(session_id) or {} + batch_id = job.get("batch_id") + if ( + batch_id + and batch_archive.status(batch_id, store.get("batch", batch_id) or {})["status"] + == "preparing" + ): + raise HTTPException(409, "报告正在打包,请稍后保存评审") + with ( + report_export.export_lock( + *(["batch:" + batch_id] if batch_id else []), "session:" + session_id + ), + task_extensions.plugin_store().mutation_lock, + ): + context = read_extensions(session_id) + if not context["editable"]: + raise HTTPException(409, "请在任务结束后填写评审") + manifest = next((m for m in task_extensions.active_manifests() if m.id == plugin_id), None) + if manifest is None: + raise HTTPException(409, "插件未安装或未启用,请刷新页面") + try: + task_extensions.save_values( + store, + session_id, + manifest, + value.values, + value.expected_revision, + str(value.request_id), + ) + except ValueError as exc: + raise HTTPException(409, str(exc)) from exc + if batch_id: + batch_archive.forget(batch_id) + return read_extensions(session_id) diff --git a/apps/admin_console/server.py b/apps/admin_console/server.py index 862ba6c..f0c91cb 100644 --- a/apps/admin_console/server.py +++ b/apps/admin_console/server.py @@ -284,6 +284,9 @@ app.include_router(favorites_router) from apps.admin_console.routers.plugins import router as plugins_router app.include_router(plugins_router) +from apps.admin_console.routers.task_extensions import router as task_extensions_router + +app.include_router(task_extensions_router) from apps.admin_console.routers.exploration import router as exploration_router diff --git a/apps/admin_console/services/automation_store.py b/apps/admin_console/services/automation_store.py index 5a6f0a1..4ff7933 100644 --- a/apps/admin_console/services/automation_store.py +++ b/apps/admin_console/services/automation_store.py @@ -294,7 +294,7 @@ class AutomationStore: if batch.get("status") != "deleting": raise ValueError("批次尚未准备删除") conn.execute( - "DELETE FROM automation_records WHERE kind='case_review' AND id IN " + "DELETE FROM automation_records WHERE kind IN ('case_review','task_extensions') AND id IN " "(SELECT session_id FROM automation_jobs WHERE batch_id=?)", (batch_id,), ) @@ -312,13 +312,15 @@ class AutomationStore: if session_id: conn.execute("DELETE FROM automation_jobs WHERE session_id=?", (session_id,)) conn.execute( - "DELETE FROM automation_records WHERE kind='case_review' AND id=?", + "DELETE FROM automation_records WHERE kind IN ('case_review','task_extensions') AND id=?", (session_id,), ) else: conn.execute("DELETE FROM automation_jobs") conn.execute("DELETE FROM automation_records WHERE kind='batch'") - conn.execute("DELETE FROM automation_records WHERE kind='case_review'") + conn.execute( + "DELETE FROM automation_records WHERE kind IN ('case_review','task_extensions')" + ) conn.commit() diff --git a/apps/admin_console/services/batch_archive.py b/apps/admin_console/services/batch_archive.py index 1aaa9f2..a0c291f 100644 --- a/apps/admin_console/services/batch_archive.py +++ b/apps/admin_console/services/batch_archive.py @@ -15,7 +15,7 @@ def archive_path(batch_id: str): def cached(batch_id: str, batch: dict) -> bool: return bool(batch.get("report_ready")) and report_export.reusable( - archive_path(batch_id), report_export.BATCH_ARCHIVE_SIGNATURE, "completed" + archive_path(batch_id), report_export.batch_archive_signature(batch_id), "completed" ) @@ -41,8 +41,14 @@ def _snapshot(state: dict) -> dict: def status(batch_id: str, batch: dict) -> dict: with _lock: state = _states.get(batch_id) - if state: + if state and state["status"] != "ready": return _snapshot(state) + if ( + state + and state.get("signature") == report_export.batch_archive_signature(batch_id) + and archive_path(batch_id).is_file() + ): + return _snapshot(state) if cached(batch_id, batch): return { "status": "ready", @@ -65,11 +71,12 @@ def status(batch_id: str, batch: dict) -> dict: def prepare(batch_id: str, batch: dict) -> dict: + cache_ready = cached(batch_id, batch) with _lock: current = _states.get(batch_id) if current and current["status"] == "preparing": return _snapshot(current) - if cached(batch_id, batch): + if cache_ready: _states.pop(batch_id, None) return status_from_cache() state = { @@ -88,11 +95,17 @@ def prepare(batch_id: str, batch: dict) -> dict: def run() -> None: try: with report_export.export_lock("batch:" + batch_id): + signature = report_export.batch_archive_signature(batch_id) latest = report_export.store.get("batch", batch_id) if not (latest and cached(batch_id, latest)): report_export.export_batch(batch_id, progress=update) with _lock: - state.update(status="ready", stage="报告包已就绪", finished_at=time.monotonic()) + state.update( + status="ready", + stage="报告包已就绪", + finished_at=time.monotonic(), + signature=signature, + ) except Exception as exc: with _lock: state.update( diff --git a/apps/admin_console/services/report_export.py b/apps/admin_console/services/report_export.py index bd0573f..241614f 100644 --- a/apps/admin_console/services/report_export.py +++ b/apps/admin_console/services/report_export.py @@ -15,6 +15,7 @@ from apps.admin_console.services.export_cache import ( ) from apps.admin_console.services.request_diagnostics import diagnostics_json from apps.admin_console.services.task_lifecycle import guidance_details +from apps.admin_console.services import task_extensions from docx import Document from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT @@ -36,6 +37,31 @@ from apps.admin_console.services.case_review import reviewed_result EXPORT_ROOT = WORKSPACE_ROOT / "artifacts" / "reports" BATCH_ARCHIVE_SIGNATURE = "artemis-batch-layout-2" + + +def batch_archive_signature(batch_id: str | None = None) -> str: + signature = task_extensions.installation_signature() + if batch_id: + with store.connection() as conn: + changes = conn.execute( + "SELECT r.id,r.updated_at FROM automation_records r " + "JOIN automation_jobs j ON j.session_id=r.id " + "WHERE r.kind='task_extensions' AND j.batch_id=? ORDER BY r.id", + (batch_id,), + ).fetchall() + if changes: + signature += ":" + fingerprint({"reviews": [tuple(row) for row in changes]}) + return BATCH_ARCHIVE_SIGNATURE + (":" + signature if signature else "") + + +def extension_rows(report: dict) -> list[tuple[str, str, str]]: + return [ + (section["name"], field["label"], field["value"]) + for section in report.get("task_extensions", []) + for field in section["fields"] + ] + + STATUS = { "completed": "执行完成", "success": "执行完成", @@ -239,6 +265,7 @@ def session_report(session_id: str, db_path=None) -> dict: "subcategory": case.get("subcategory", ""), "preconditions": case.get("preconditions", ""), "task_config": case.get("task_config") or {}, + "task_extensions": task_extensions.report_sections(session_id, store), } @@ -659,6 +686,11 @@ def word_report(report: dict, path: Path): doc.add_paragraph("任务配置:" + config_text(report["task_config"])) doc.add_heading("结论与发现", 1) add_markdown_content(doc, report["content"]) + if review_rows := extension_rows(report): + doc.add_heading("人工评审(插件)", 1) + word_table( + doc, [(f"{name} · {label}", value) for name, label, value in review_rows], (5.5, 11) + ) doc.add_heading("执行步骤与证据", 1) for step in report["steps"]: paragraph = doc.add_paragraph() @@ -792,6 +824,10 @@ def excel_report(report: dict, path: Path): wb.active.title = "任务结果" sheet_rows(wb.active, [("项目", "内容"), *metadata(report)], (22, 82)) sheet_rows(wb.create_sheet("结论"), conclusion_rows(report["content"]), (15, 96)) + if review_rows := extension_rows(report): + sheet_rows( + wb.create_sheet("人工评审"), [("插件", "项目", "内容"), *review_rows], (24, 30, 82) + ) table_number = 0 for kind, rows in markdown_blocks(report["content"]): if kind == "table": @@ -1064,7 +1100,9 @@ def export_batch(batch_id: str, progress: Callable[[int, int, str], None] | None directory = EXPORT_ROOT / "batches" / batch_id directory.mkdir(parents=True, exist_ok=True) formats = batch["formats"] - records = [session_report(job["session_id"]) for job in store.jobs(batch_id)] + with task_extensions.plugin_store().mutation_lock: + signature = batch_archive_signature(batch_id) + records = [session_report(job["session_id"]) for job in store.jobs(batch_id)] if progress: progress(0, len(records), "正在生成批次汇总") rows = [("任务编号", "任务名称", "执行状态", "验证结论", "异常原因")] @@ -1078,6 +1116,17 @@ def export_batch(batch_id: str, progress: Callable[[int, int, str], None] | None assert wb.active is not None wb.active.title = "批次结果" sheet_rows(wb.active, rows, (40, 48, 16, 28, 70)) + review_rows = [ + (record["id"], record["name"], *row) + for record in records + for row in extension_rows(record) + ] + if review_rows: + sheet_rows( + wb.create_sheet("人工评审"), + [("任务编号", "任务名称", "插件", "项目", "内容"), *review_rows], + (40, 40, 24, 30, 70), + ) path = directory / "批次结果.xlsx" wb.save(path) outputs.append(path) @@ -1088,6 +1137,15 @@ def export_batch(batch_id: str, progress: Callable[[int, int, str], None] | None f"批次编号:{batch_id};共 {len(records)} 个任务。验证通过与执行完成分别统计。" ) word_table(doc, rows, (3.5, 5.4, 2.5, 3.5, 7.3), header=True) + for record in records: + if review_rows := extension_rows(record): + doc.add_heading("人工评审 · " + record["name"], 1) + doc.add_paragraph("任务编号:" + record["id"]) + word_table( + doc, + [(f"{name} · {label}", value) for name, label, value in review_rows], + (7, 15.2), + ) path = directory / "批次汇总.docx" doc.save(str(path)) outputs.append(path) @@ -1146,5 +1204,5 @@ def export_batch(batch_id: str, progress: Callable[[int, int, str], None] | None if progress: progress(len(records), len(records), "正在完成压缩包") temporary.replace(target) - remember(target, BATCH_ARCHIVE_SIGNATURE) + remember(target, signature) return target diff --git a/apps/admin_console/services/task_extensions.py b/apps/admin_console/services/task_extensions.py new file mode 100644 index 0000000..407cc79 --- /dev/null +++ b/apps/admin_console/services/task_extensions.py @@ -0,0 +1,162 @@ +"""Human-authored task metadata owned by the host, scoped to installed plugins.""" + +import hashlib +import json +import time +from functools import lru_cache + +from apps.admin_console.services.automation_store import AutomationStore, identifier +from apps.admin_console.database.repositories.session_repository import SessionRepository +from artemis.plugins.models import PluginManifest +from artemis.plugins.store import PluginStore + +TERMINAL = {"completed", "success", "failed", "cancelled", "interrupted", "skipped"} + + +@lru_cache(maxsize=1) +def plugin_store() -> PluginStore: + return PluginStore() + + +def active_manifests() -> list[PluginManifest]: + return [ + PluginManifest.model_validate(plugin["manifest"]) + for plugin in plugin_store().list() + if plugin["enabled"] and plugin["manifest"].get("workspace") + ] + + +def installation_signature() -> str: + manifests = [manifest.model_dump(mode="json") for manifest in active_manifests()] + return ( + hashlib.sha256(json.dumps(manifests, sort_keys=True).encode()).hexdigest() + if manifests + else "" + ) + + +def task_context(session_id: str, store: AutomationStore) -> dict: + identifier(session_id) + session = SessionRepository(store.path).get_session_by_id(session_id) or {} + job = store.job(session_id) or {} + if not session and not job: + raise ValueError("任务记录不存在") + return { + "session_id": session_id, + "initial_prompt": session.get("initial_goal") or (job.get("case") or {}).get("goal", ""), + "editable": (job.get("status") or session.get("status")) in TERMINAL, + } + + +def contributions(session_id: str, store: AutomationStore) -> list[dict]: + record = store.get("task_extensions", session_id) or {} + result = [] + for manifest in active_manifests(): + assert manifest.workspace is not None + saved = record.get(manifest.id) or {} + values = saved.get("values", {}) + result.append( + { + "plugin_id": manifest.id, + "name": manifest.name, + "version": manifest.version, + "workspace": manifest.workspace.model_dump(), + "revision": saved.get("revision", 0), + "values": { + field.id: values.get(field.id, "") for field in manifest.workspace.fields + }, + "history": saved.get("history", []), + } + ) + return result + + +def report_sections(session_id: str, store: AutomationStore) -> list[dict]: + return [ + { + "plugin_id": item["plugin_id"], + "name": item["name"], + "version": item["version"], + "revision": item["revision"], + "fields": [ + {"id": field["id"], "label": field["label"], "value": item["values"][field["id"]]} + for field in item["workspace"]["fields"] + if field["report"] + ], + } + for item in contributions(session_id, store) + if any(field["report"] for field in item["workspace"]["fields"]) + ] + + +def save_values( + store: AutomationStore, + session_id: str, + manifest: PluginManifest, + values: dict[str, str], + expected_revision: int, + request_id: str, +) -> None: + """CAS + request receipts prevent lost edits and repeated history entries.""" + identifier(session_id) + assert manifest.workspace is not None + fields = {field.id: field for field in manifest.workspace.fields} + if values.keys() - fields.keys(): + raise ValueError("包含未声明的扩展字段") + for key, value in values.items(): + field = fields[key] + if len(value) > field.max_length or ( + field.kind == "choice" and value and value not in field.options + ): + raise ValueError(f"字段 {field.label} 的内容不符合插件声明") + now = time.time() + with store.connection() as conn: + conn.execute("BEGIN IMMEDIATE") + row = conn.execute( + "SELECT payload FROM automation_records WHERE kind='task_extensions' AND id=?", + (session_id,), + ).fetchone() + record = json.loads(row[0]) if row else {} + saved = record.get(manifest.id, {"revision": 0, "values": {}, "history": []}) + for entry in saved["history"]: + if entry["request_id"] == request_id: + if entry["patch"] != values: + raise ValueError("同一保存请求不能提交不同内容") + return + if saved["revision"] != expected_revision: + raise ValueError("评审已在其他页面修改,请重新加载后保存") + updated = {**saved["values"], **values} + revision = saved["revision"] + 1 + saved["history"].append( + { + "revision": revision, + "saved_at": now, + "request_id": request_id, + "patch": values, + "values": updated, + "plugin_version": manifest.version, + "fields": [field.model_dump() for field in manifest.workspace.fields], + } + ) + record[manifest.id] = {**saved, "revision": revision, "values": updated} + conn.execute( + "INSERT INTO automation_records VALUES ('task_extensions',?,?,?) " + "ON CONFLICT(kind,id) DO UPDATE SET payload=excluded.payload,updated_at=excluded.updated_at", + (session_id, json.dumps(record, ensure_ascii=False), now), + ) + job = conn.execute( + "SELECT batch_id FROM automation_jobs WHERE session_id=?", (session_id,) + ).fetchone() + if job and job[0]: + batch = conn.execute( + "SELECT payload FROM automation_records WHERE kind='batch' AND id=?", (job[0],) + ).fetchone() + if not batch or json.loads(batch[0]).get("status") == "deleting": + raise ValueError("批次正在删除,不能保存评审") + payload = json.loads(batch[0]) + payload["report_ready"] = False + conn.execute( + "UPDATE automation_records SET payload=?,updated_at=? WHERE kind='batch' AND id=?", + (json.dumps(payload, ensure_ascii=False), now, job[0]), + ) + conn.commit() diff --git a/apps/showcase_ui/package-lock.json b/apps/showcase_ui/package-lock.json index cf9f0a4..4726712 100644 --- a/apps/showcase_ui/package-lock.json +++ b/apps/showcase_ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "artemis-showcase-ui", - "version": "2.2.0", + "version": "2.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "artemis-showcase-ui", - "version": "2.2.0", + "version": "2.3.0", "dependencies": { "@angular/cdk": "^22.1.5", "@angular/common": "^22.1.4", diff --git a/apps/showcase_ui/package.json b/apps/showcase_ui/package.json index dbb9e1f..5cea666 100644 --- a/apps/showcase_ui/package.json +++ b/apps/showcase_ui/package.json @@ -1,6 +1,6 @@ { "name": "artemis-showcase-ui", - "version": "2.2.0", + "version": "2.3.0", "scripts": { "ng": "ng", "start": "ng serve --proxy-config proxy.conf.json", diff --git a/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html b/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html index ca037fa..617cff3 100644 --- a/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html +++ b/apps/showcase_ui/src/app/components/floating-video-player/floating-video-player.component.html @@ -27,7 +27,9 @@ limitations under the License. class="floating-video-wrapper" [class.minimized]="agentService.isVideoMinimized()" [class.theater]="isTheaterMode()" - [style.left.px]="!isTheaterMode() ? posX() : null" + [style.--extension-width]="extensionWidth() + 'px'" + [style.left.px]="!isTheaterMode() ? windowLeft() : null" + [style.width.px]="isTheaterMode() || agentService.isVideoMinimized() ? null : 400 + extensionWidth()" [style.top.px]="!isTheaterMode() ? posY() : null" [style.--player-top]="posY() + 'px'" > @@ -42,6 +44,9 @@ limitations under the License.
该任务未记录初始需求提示词。
} + } @else { + @if (!data()?.editable) {任务结束后可填写评审。
} + + @if (notice()) {{{ notice() }}
} + @if (plugin.history.length) { +{{ field.label }}:{{ entry.values[field.id] || '未填写' }}
} +为安卓测试扩展电脑等操作能力,AI 按任务需要自主调用。
扩展任务操作能力,也可为回放界面和报告添加人工评审等功能。