diff --git a/doc/AI_TASK_GEN.md b/doc/AI_TASK_GEN.md new file mode 100644 index 0000000..80d9c62 --- /dev/null +++ b/doc/AI_TASK_GEN.md @@ -0,0 +1,180 @@ +# AI 建任务(AI Task Generator)设计文档 + +> 分支:dev | 状态:设计稿(未实现) | 日期:2026-09-09 +> 关联:AI 控制台(web/agent_api.py)、MCP 设备工具(mcp_server/)、任务/步骤编辑器(tasks/generic、static/admin/editor.js) + +## 1. 背景与目标 + +平台已有两套能力,但互不相通: +- **AI 控制台**:一句话 + 选设备 → 多模态 Agent(DeepSeek)通过 19 个 `de_*` 工具在手机上「边看边做」(截图看屏、`de_ui_tree` 拿元素树、`de_tap_element/de_tap_text` 语义点按),流式回放步骤。 +- **任务系统 + 步骤编辑器**:`generic_steps` 任务 = 一棵可嵌套步骤树(open_app/click/swipe/loop/group/if_el…18 种节点),在编辑器里拖拽编排、单步试跑、定时调度。 + +目标:让**非工程用户用一句自然语言需求**(例:「创建一个每日养号刷视频的任务,每天 8:00-9:00 在 100.100.10.13 跑」)得到**一条可直接调度、可继续在现有步骤编辑器里手改的任务**。AI 先自己在设备上打开 App、看 UI 树、确认可点元素,再直接撰写编辑器的步骤 JSON。 + +### 产品体验(一页) +1. 用户进入「AI 控制台 → 模式=AI 建任务」,选一台**空闲**设备,填需求 +(可选)任务名/调度/目标。 +2. AI 自探并**流式回放**(工具卡 + 截图,与现在一致):开 App → dump UI 树 → 确认要点的元素可命中 → 写步骤。 +3. 完成后平台返回任务草稿 `draft`,服务端 schema 校验后,**前端直接打开现有「新建任务 → 步骤编辑器」**预填。 +4. 用户核对/手改/单步试跑 → 保存 → 进任务列表,走原有调度器执行。 + +**核心信任原则:AI 只“提案”,不直接建库、不直接执行最终任务;最终落库/执行都在用户确认后由现有机制完成。** + +## 2. 范围(P0 定稿,2026-09-09 与用户确认) + +- **入口**:AI 控制台加「聊天 / AI 建任务」模式开关,复用会话/SSE/设备选择/停止/刷新恢复骨架。不新建顶级 Tab。 +- **产出方式**:**AI 直接撰写编辑器的步骤 JSON**(非操作轨迹翻译)。自探信息只作上下文与审计;探索中的误点/多余截图不会混入任务。 +- **任务类型**:P0 **只产 `generic_steps`**(通用步骤)。`douyin_nurture` 预设参数生成放 P1。 +- **定位原则:全部基于 UI 树**: + - 模型定位/点击一律走 `de_ui_tree` 拿到的元素(`text`/`id`/`description`/`text_contains`),多实例用 index,或后端 `uiauto_helper` 生成的 `//*[@resource-id=..][k]` XPath; + - `de_tap_element` 确认能命中后才写进 `click` 步骤; + - **默认不产 `click_xy` 坐标点击**;UI 树给不了的元素记入 notes,交给人工/OCR 兜底; + - 滑动用方向语义 `swipe{direction}`,不用像素坐标。 +- 产出完整任务信封:`{name, task_type:"generic_steps", target, schedule, retry, enabled, params:{max_duration, steps}}`。 + +## 3. 现状与可复用点(实现依据) + +- 多模态 Agent 链路:`web/agent_api.py`(`POST /api/agent/run` 起后台线程、SSE `delta/step/done/error`、`/stop`、刷新恢复 `GET /api/agent/run`)→ `mcp_agent/agent.py`(OpenAI 兼容流式,工具经 fastmcp Client 拉 8033 的 19 个 `de_*`)→ `mcp_server/mcp_server.py`。 +- 前端 AI 控制台骨架:`templates/admin/monitor.html` `#tab-agent` + `static/admin/agent.js`(设备选择、`sendAgentMsg`、`listenStream` 渲染 `agent-toolcard`、实时画面跟随)。 +- 步骤权威 schema(前后端同构):`tasks/generic/task.py` `STEP_TYPES`(L44-83)/ `DEFAULT_PARAMS`(L86-103) 与 `static/admin/editor.js` `STEP_LIB`(L3-23)。 +- 元素抓取 → XPath:`web/tasks_api.py /api/uiauto/elements`(L224)→ `core/uiauto_helper.get_elements`(`suggested.value` 即编辑器可用的 XPath)。 +- 单步试跑:`/api/steps/test`(tasks_api.py L249)+ `editor.js _testStep`;`tasks/generic/task.py test_step`(L633)。 +- 任务/调度/目标:`core/task_manager.py` `TaskJob`(L121-199);`POST /api/jobs`(tasks_api.py L68)。 +- 设备占用语义:MCP 写工具 `_ensure_device_free`(mcp_server.py L60-78,running/connecting → `device_busy`);`agent_api` run 入口同样对 worker running/connecting 拒绝(409)。 +- 经验库自进化:`agent_experience` + bigram 检索注入 + 每日巡检(`web/agent_api.py`)——P1 沉淀模板的现成载体。 + +## 4. 架构与数据流 + +``` +需求+设备(空闲) ──▶ Designer Agent(自探, 模式=designer) + │ de_open_app / de_ui_tree / de_screenshot / de_tap_element… + ▼ + 平台工具 submit_task(draft) ← 结束性调用 + │ 服务端 draft schema 校验(白名单+必填+深度≤5) + ▼ + 前端: 打开 openTaskModal 步骤编辑器, 预填 draft.task + │ 用户核对/手改/单步试跑(/api/steps/test) + ▼ + POST /api/jobs → 任务列表(原调度器执行) + (P1) draft+需求 沉淀 agent_experience, 相似需求注入参考 +``` + +### 4.1 Designer Agent(新增模式,复用现有 Agent) +- `mcp_agent` 增加 designer 系统提示词:角色=自动化任务设计师;行为约束见 §5.1。 +- 增加**平台级工具**(不属设备 `de_*`): + - `submit_task(draft)`:结束性工具,模型完成自探后提交草稿即停止,服务端立即校验。 +- 会话内同时记录**结构化 trace**(每轮 `on_tool` 的 `{tool, args 精简, 屏号/证据}`),用于:校验证据(每个 selector 来自哪次树)、审计、P2 回放。 +- `POST /api/agent/run` 增加 `mode:"designer"`;`done` 事件负载携带 `draft`(校验通过)或 `draft_error`(校验失败+原因,让模型补一轮)。 + +### 4.2 draft 契约 +```json +{ + "summary": "每日8-9点刷抖音养号:开抖音→循环(看5~35s+上滑)+随机间隔", + "task": { + "name": "抖音每日养号", + "task_type": "generic_steps", + "target": {"mode":"serial","serial":"100.100.10.13:5555"} + | {"mode":"group","group_name":"测试"} | {"mode":"all"}, + "schedule": {"mode":"once"} | {"mode":"cron","cron":"0 8 * * *"} + | {"mode":"cron_stop","cron":"0 8 * * *","stop_cron":"0 9 * * *"}, + "retry": {"max_attempts":5,"delay":30}, + "enabled": true, + "params": { + "max_duration": 0, + "steps": [ + {"type":"open_app","label":"打开抖音","params":{"package":"com.ss.android.ugc.aweme","wait_home":true}}, + {"type":"loop","params":{"loop_mode":"rounds","max_iterations":30,"children":[ + {"type":"wait","params":{"min":5,"max":35}}, + {"type":"swipe","params":{"direction":"up","duration_min":0.25,"duration_max":0.5}} + ]}} + ] + } + }, + "notes": ["评论按钮需先进入视频页才可见"], + "evidence": [{"screen":"抖音首页","element":{"text":"关注","id":"..."},"xpath":"//*[@resource-id=\".../gvo\"]"}] +} +``` +- 步骤节点结构:`{id?, type, label?, params}`;`id` 执行端忽略(编辑器重新生成),`params` 必填。 +- 必填字段语义(**空值会被执行端静默跳过**,校验器必须拦): + - `click / long_click / swipe_until / wait_el / if_el` → `params.selector_value` + - `open_app / stop_app` → `params.package` + - 容器:`loop / group` → `params.children`(非空);`if_el` → `params.then`(`else` 可选) + - `swipe_until` → direction + max_swipes;`click_xy` P0 不产(如允许则 x/y 0-100) + - 节点公共可选 `params.probability`(0-100,缺省 100) +- `selector_type` 允许值:`xpath / description / text / resourceId / descriptionContains / className`(`if_el` 可 `ocr`)。 + +### 4.3 服务端新增 +- `core/task_draft.py`: + - `STEP_TYPES` 白名单 + 每类必填/深度校验 `validate_steps(steps, depth)`(嵌套≤5); + - `validate_draft(draft)`:任务信封(name 非空、task_type==generic_steps、target mode ∈ {all,group,serial}(group 名存在)、schedule cron 合法、steps 校验); + - 归一化:把 `schedule` 里「每天 8-9 点」这类由前端/向导填的值转成 cron/cron_stop。 +- `web/taskgen_api.py`(或并入 `agent_api`,推荐并入以最大化复用): + - 入口检查:serial 必须、设备在池/在线、worker 非 running/connecting(409,与现有语义一致); + - 起 designer 后台线程;SSE 事件在现有 `delta/step/done/error` 基础上,`done` 可带 `draft`。 +- **注意**:现有 `POST /api/jobs` 对 params **盲存**(只校验 name+task_type)。AI 通道在**保存前**必须过 `validate_steps`,避免「任务 done 但什么都没做」(执行器对未知 type/空 selector 静默跳过)。 + +### 4.4 前端 +- `monitor.html` AI 控制台加模式切换;建任务模式下输入栏旁有折叠「任务设置」(名称/调度时间/目标 serial·分组·全部/备注)。 +- `agent.js`:done 携带 draft 后: + - generic_steps → 调 `openTaskModal()`(tasks.js)并把 `draft.task` 灌入步骤编辑器(step 卡片可视化、可拖改、单步试跑、保存); + - 弹窗内对 `notes`(含“需人工复核/OCR 兜底”项)给出醒目提示。 +- 过程回放沿用现有 `agent-toolcard` 渲染;可标记当前为 designer 轮以便后续区分。 + +## 5. 约束与安全(红线) + +### 5.1 Designer 自探规则(写入提示词) +1. 先 `de_open_app(package)`,再 `de_ui_tree` + `de_screenshot` 看每屏;点到关键状态后再 dump 下一屏。 +2. 每个将写入步骤的目标元素,先用 `de_tap_element`(by=text/id/desc…)+ 截图**确认可命中**,并记下证据。 +3. **不做破坏性动作**:需“评论/发送”时只确认输入框/发送键存在,不真发;产物里这类步骤 `probability` 调低并在 notes 标注“请人工复核”。 +4. 探索步数上限(P0 建议 30 步),可被 `/stop` 打断;结束后尽力还原前台 App。 +5. 未命中的元素一律不进任务;拿不准的进 notes 而非硬编。 + +### 5.2 平台级约束 +- 自探/试跑只在**用户选的空闲设备**(busy → 409),杜绝与运行中任务在设备上物理打架。 +- AI 不直接建库;生成任务仍需用户点保存(POST /api/jobs 现有权限)。 +- 尽量不写死坐标;P0 默认禁 `click_xy`,产物以树元素定位为主。 + +## 6. 里程碑 +- **P0(本设计主体)**:designer 模式 → 自探(UI 树定位)→ 直接撰写 generic_steps draft → 服务端 schema 校验 → 前端步骤编辑器预填确认保存。验收:一句话在真实设备上生成一条可调度的 generic_steps,步骤全部来自 UI 树且编辑器可打开。 +- **P1**:`douyin_nurture` 参数预设生成(照 default_params 结构);模板沉淀:把 draft+需求写入 `agent_experience`(新列存结构化 steps 或 JSON),相似需求注入参考;整链「演示试跑」(把 steps 在设备上以受控方式跑一遍并截图回报,需新增端点,复刻 `device_busy` 拒绝语义)。 +- **P2**:自定义动作支持(内联展开成 group,或新增 `action_ref` 节点 + 执行器/编辑器同步);多设备并行;成本与 token 控制。 + +## 7. 实现时需新增/改动文件(规划) +- 改:`mcp_agent/`(designer 提示词与 `submit_task` 工具、结构化 trace)、`web/agent_api.py`(mode=designer、done 带 draft)、`static/admin/agent.js` + `templates/admin/monitor.html`(模式切换/任务设置/draft 预填)、`doc/`(本文档关联)。 +- 新:`core/task_draft.py`(schema+校验+归一化)、(可选)`web/taskgen_api.py`。 +- 不动:任务执行器、调度器、`POST /api/jobs` 主体(保持现有盲存,只在 AI 通道校验)。 + +## 8. 验收(P0 实现后自测) +1. 目标设备空闲时:需求「每日 8-9 点刷抖音养号」→ 生成 generic_steps 任务,步骤为 `open_app → loop(wait+swipe)` 结构,调度 cron_stop 8-9 点。 +2. 打开编辑器中该任务:步骤卡片完整、可拖改、单步试跑命中;保存后任务列表出现且下次运行时间正确。 +3. 反例:模型产出含 `click_xy` 或未知 type / 空 selector → 服务端校验拦截并让模型补正;busy 设备入口 409。 +4. 探索全程可在前端回放(工具卡+截图),未发送真实评论/未污染设备状态。 + +## 9. 需要转成 MCP / 平台工具的能力(分层,2026-09-09 与用户确认) + +> 背景问答结论:目前 MCP 只有**设备层 19 个 `de_*`**(控制 + 只读 `de_list_tasks`),平台 CRUD(任务增改/启停/立即运行、分组、设备池、自定义动作、APK、备份、用户)**都还没 MCP 化**。方向认同「先把工具链补完善」,但不做"把所有平台 CRUD 一次性搬成 MCP"的大而全——**按消费方(AI 建任务 / 外部自动化)分层、按需补**。新增 MCP 工具一律:进 `doc/MCP.md` 手册 + `doc/MCP_DESIGN.md` 规格 + 与 web 同源的权限/busy/审计 + 校验逻辑下沉到 `core/` 共用(防双份漂移)。 + +### 9.1 现状盘点 +- MCP(`mcp_server/mcp_server.py`,19 个 `de_*`)= 设备控制 + 设备只读 + `de_list_tasks`(平台任务只读)。 +- 任务创建/修改/删除、toggle、立即运行、分组、设备池管理、自定义动作、APK、系统备份等 **REST 路由只给前端用,未暴露 MCP**。 + +### 9.2 P0 —— AI 建任务链路真正需要的「平台工具」(最小集) +只补两小类,其余设备操作全部复用现有 `de_*`: +1. **只读清单**(供自探确定 target/能不能做): + - `list_task_types` / `list_groups`(target 选 group 用)/ `list_pool`(可调度设备,含 busy 状态) + - (可与现有 `de_list_devices` 合并语义,避免重复) +2. **校验 + 提交(结束性)**: + - `submit_task(draft)` → 服务端用 **共用** `core/task_draft.validate_steps()/validate_draft()` 校验,**不直接落库**,返回 draft 供前端打开步骤编辑器预填、人工确认后走 `POST /api/jobs`。 + +> 关键:P0 的 Agent **不暴露任务 CRUD 写权限**(create/update/toggle/run),否则模型可绕过"AI 提案 → 人工确认"直接入库,破坏信任模型。 + +### 9.3 P1 —— 外部自动化 / 后续 Agent 的「写 MCP」(受权限约束,逐块加) +若目标延伸为"外部程序能像调 REST 一样操控平台",则按此清单**逐个**补(每加一个都做权限+busy+审计+校验下沉): +- 任务:`create_job` / `update_job` / `delete_job` / `toggle_job` / `run_job_now` / `query_jobs` +- 分组:`list_groups` / `create_group` / `update_group` / `delete_group` +- 设备池:`pool_list` / `pool_add` / `pool_remove` / `pool_toggle` +- 自定义动作 / APK 清单 等视使用再加 +- **只读清单优先搬**;写类确认有真实消费方再做,避免空转。 + +### 9.4 分层与登记(红线) +- 每新增/修改/删除一个 MCP 工具或平台配置:同步更新 `doc/MCP.md`(全清单)、`doc/MCP_DESIGN.md`(规格/层级),并在提交里体现——见全局「doc 同步红线」。 +- `core/task_draft.py` 是 web 校验与 MCP 校验的**唯一来源**,杜绝两套规则漂移。 diff --git a/doc/API.md b/doc/API.md index 6cc3614..c12478b 100644 --- a/doc/API.md +++ b/doc/API.md @@ -1,6 +1,6 @@ # API 接口文档 -`platform-tools` Web 后台提供 JSON API,所有接口需登录后访问(Flask-Login session 认证)。 +`platform-tools` Web 后台提供 JSON API,绝大多数接口需登录后访问(Flask-Login session 认证);免登录例外见下方权限模型。 **Base URL**:`http://localhost:18050` @@ -15,16 +15,17 @@ --- **权限模型**(v2 起): -- 所有接口需登录;**查看类 GET 接口**(状态/列表/截图)所有登录用户可用 +- 所有接口需登录(Flask-Login session);**免登录例外**:`GET /api/health`(探活)、`GET /locate`(设备端定位页,只显示 serial 文本)、`/login` 与静态资源 +- **多数查看类 GET**(状态/任务/分组/自定义动作/APK 列表等)仅需登录即可用;设备维护/看屏/元素抓取类 GET 需对应 `devices` 权限 - **写操作按权限位授权**(管理员拥有全部权限): | 权限位 | 中文 | 覆盖接口 | |--------|------|---------| | `tasks` | 任务管理 | 任务/自定义动作/分组的增删改、启停、立即执行 | - | `devices` | 设备控制 | 停止设备、释放占用、清除异常、前台扫描、元素抓取 | + | `devices` | 设备控制 | 停止设备、清除异常、定位、前台扫描、远程看屏/触控、元素抓取、设备池管理、自动发现 | | `apks` | 应用管理 | APK 上传、安装、删除 | | `logs` | 日志查看 | `GET /api/logs` | -- **用户管理接口仅管理员可用**(普通用户即使被授予业务权限也无法访问) -- 无权限访问返回 `403 {"ok": false, "error": "无权限执行此操作..."}` +- **仅管理员可用**(普通用户即使被授予业务权限也无法访问):AI 控制台 `/api/agent/*`、系统备份 `/api/system/backup/*`、Tailscale `/api/tailscale/*`、用户管理 `/api/users`、adb 终端 `/api/adb/*`、工具 `/api/tools/*` +- 403 文案两种:业务权限缺失 → `{"ok": false, "error": "无权限执行此操作(需要权限: X)"}`;仅管理员接口被非管理员访问 → `{"ok": false, "error": "仅管理员可执行此操作"}` - 当前用户权限查询:`GET /api/me` --- @@ -47,25 +48,55 @@ 登出,重定向到登录页。 +### GET /api/me + +当前登录用户信息(含权限位),前端据此隐藏无权限的功能入口。需登录。 + +**响应**: +```json +{"ok": true, "user": { + "id": 1, "username": "admin", "is_admin": true, + "perms": ["tasks", "devices", "apks", "logs"] +}} +``` +管理员返回全部权限位;普通用户返回其被授予的业务权限数组。 + +### GET /api/csrf + +获取当前会话的 CSRF token(登录后先获取一次;变更类请求需在 `X-CSRF-Token` 请求头携带)。 + +**响应**: +```json +{"ok": true, "token": "…"} +``` + --- ## 2. 页面路由 ### GET / -单页应用首页(需登录)。响应头设置 `Cache-Control: no-store` 防止缓存。 +单页应用首页(需登录)。响应头设置 `Cache-Control: no-store, no-cache, must-revalidate, max-age=0`(并带 `Pragma: no-cache`)防止缓存。 ### GET /login 登录页面(GET)。 +### GET /wall + +监控大屏页面(需登录,全屏深色控制室风格,供挂墙/电视展示):设备卡片网格 +(缩略图/型号/状态/当前动作/进度)、顶部统计与时钟;状态每 5s 刷新、缩略图每 2.5s 轮询。 +20 台设备整体开销约 0.2 核 CPU + 100KB/s 带宽,普通电脑无压力。 + --- ## 3. 设备状态 ### GET /api/status -获取设备池 + Worker 综合状态(带 5 秒缓存)。 +获取设备池 + Worker 综合状态(带 5 秒缓存;worker 状态实时读内存)。需登录。 + +字段说明:`server_time` 服务端时间戳;`fg_scanning`/`fg_last_scan` 前台 App 扫描状态;`devices` 设备数组。 **响应**: ```json @@ -81,27 +112,58 @@ "device_name": "测试机1", "present": true, "ready": true, - "stf_occupied": false, "owner": "", - "worker_status": "idle", - "foreground_app": "空闲", + "worker_status": "running", + "foreground_app": "抖音", "progress": {"done": 5, "total": 80, "unit": "视频", "action_counts": {"like": 3}}, "current_action": "观看视频 6", "last_error": "", + "last_warning": "", "running_job": "", "task_job": "", - "attempt": 0 + "attempt": 0, + "end_time": 0 } ] } ``` -**worker_status 取值**:`idle` / `connecting` / `running` / `done` / `error` / `failed` / `released` +`worker_status` 取值:`idle` / `connecting` / `running` / `done` / `error` / `failed` -(权限:设备控制) +### GET /api/summary + +失败/异常任务汇总(供监控页"异常汇总"面板),观察设备长期健康度。需登录。 + +**响应**: +```json +{ + "ok": true, + "counts": {"total": 8, "running": 1, "done": 5, "error": 1, "failed": 1, "idle": 0}, + "errors": [ + {"serial": "192.168.1.100:5555", "model": "Pixel 6", "status": "failed", + "last_error": "重试3次失败", "task": "抖音养号", "attempt": 3, "updated": 1700000000.0} + ] +} +``` +`counts` 各状态计数;`errors` 为 `error`/`failed` 且有 `last_error` 的异常设备 +(按最近心跳倒序,最多 50 条;已不在设备池的陈旧失败记录不展示)。 + +### GET /api/health + +轻量健康检查(免登录,供运维探活):进程存活 + 设备/任务摘要,不暴露敏感信息。 + +**响应**: +```json +{"ok": true, "status": "up", "time": 1700000000.0, "device_total": 8, + "device_online": 6, "device_running": 2, "device_error": 1, "jobs": 3} +``` + +### POST /api/scan_foreground 手动触发前台 App 扫描(后台异步执行,不打扰设备)。 +(权限:设备控制) + **响应**: ```json {"ok": true, "msg": "扫描已启动"} @@ -117,6 +179,18 @@ {"ok": true, "devices": ["192.168.1.100:5555", "192.168.1.101:5555"]} ``` +### GET /api/devices//apps + +获取指定设备上已安装的应用列表(包名 + versionCode/versionName + APK 路径 + 应用名)。需登录。 + +**响应**: +```json +{"ok": true, "apps": [ + {"package": "com.ss.android.ugc.aweme", "path": "/data/app/.../base.apk", + "version_code": 2500, "version_name": "25.0.0", "label": "抖音"} +]} +``` + ### GET /api/device/screenshot 获取设备当前画面截图(PNG)。 @@ -198,10 +272,12 @@ ``` `next_run`:下次真正执行时间(已按运行窗口跳过窗口外触发点,格式 `YYYY-MM-DD HH:MM`);手动任务/已停用为 `null`。 -(权限:任务管理) +### POST /api/jobs 创建任务计划。 +(权限:任务管理) + **请求**(JSON): ```json { @@ -228,10 +304,12 @@ | `stop_cron` | (cron_stop 必填)到点停止本任务 worker | | `window` | 可选,运行窗口 `{"start": "21:00", "end": "09:00"}`(每天重复,支持跨午夜)。窗口外定时触发和手动执行(`POST /api/jobs/:id/run`)均不启动,手动执行返回错误提示 | -(权限:任务管理) +### PUT /api/jobs/ 更新任务计划。只需传要更新的字段。 +(权限:任务管理) + **请求**(JSON): ```json {"params": {"watch_count": 100}} @@ -242,25 +320,29 @@ {"ok": true, "msg": "任务已更新", "job": {"...": "..."}} ``` -(权限:任务管理) +### DELETE /api/jobs/ -删除任务计划。 +删除任务计划。不存在返回 404。 + +(权限:任务管理) **响应**: ```json {"ok": true, "msg": "任务已删除"} ``` -(权限:任务管理) +### POST /api/jobs//run 立即执行任务(异步,不阻塞)。 +(权限:任务管理) + **响应**: ```json {"ok": true, "msg": "任务 抖音养号 已触发"} ``` -(权限:任务管理) +### POST /api/jobs//toggle 启用/停用任务。 @@ -292,36 +374,50 @@ } ``` -(权限:任务管理) +### POST /api/groups -创建分组。 +创建分组。重名返回 400。 + +(权限:任务管理) **请求**(JSON): ```json {"name": "A组", "serials": ["192.168.1.100:5555"], "description": "测试组"} ``` -(权限:任务管理) +**响应**:`{"ok": true, "msg": "分组已创建"}` -更新分组。 +### PUT /api/groups/ + +更新分组(serials/description 按需传字段)。`` 不存在返回 404。 + +(权限:任务管理) **请求**(JSON): ```json {"serials": ["192.168.1.100:5555", "192.168.1.101:5555"], "description": "更新描述"} ``` +**响应**:`{"ok": true, "msg": "分组已更新"}` + +### DELETE /api/groups/ + +删除分组。不存在返回 404。 + (权限:任务管理) -删除分组。 +**响应**:`{"ok": true, "msg": "分组已删除"}` --- ## 7. 运行控制 -(权限:设备控制) +### POST /api/stop_device 停止单台设备的 worker(并阻止后续重试)。 +(权限:设备控制) + **请求**(JSON): ```json {"serial": "192.168.1.100:5555"} @@ -332,67 +428,107 @@ {"ok": true, "msg": "已发送停止信号给 192.168.1.100:5555"} ``` -(权限:设备控制) +### POST /api/stop_all 停止所有运行中的 worker。 +(权限:设备控制) + **响应**: ```json {"ok": true, "stopped": ["192.168.1.100:5555", "192.168.1.101:5555"]} ``` +> 旧「释放设备占用」端点已随 STF 摘除移除,此接口不再存在;设备互斥由调度器内存锁保证。 + +### POST /api/device/clear_error + +清除单台设备的异常状态(`error`/`failed` → `idle`),供设备列表"清除异常"按钮使用。 +设备正在运行或等待重试时返回 400。 + (权限:设备控制) -(已随 STF 摘除移除,此接口不再存在;设备互斥由调度器内存锁保证) +**请求**(JSON): +```json +{"serial": "192.168.1.100:5555"} +``` **响应**: ```json -{"ok": true, "released": ["192.168.1.100:5555"]} +{"ok": true, "msg": "已清除 192.168.1.100:5555 的异常状态"} +``` + +### POST /api/device/clear_all_errors + +一键清除所有异常/失败设备(自动跳过正在运行/等待重试的)。 + +(权限:设备控制) + +**响应**: +```json +{"ok": true, "cleared": 2, "msg": "已清除 2 台设备的异常状态"} ``` --- ## 8. 用户管理 -(仅管理员) +用户管理接口**仅管理员可用**(非管理员返回 403 `仅管理员可执行此操作`)。`uid` 为用户 id(整数)。 + +### GET /api/users 列出所有用户。 **响应**: ```json -{"ok": true, "users": [{"id": 1, "username": "admin", "is_admin": true}]} +{"ok": true, "users": [ + {"id": 1, "username": "admin", "is_admin": true, "perms": []}, + {"id": 2, "username": "user1", "is_admin": false, "perms": ["tasks", "devices"]} +]} ``` +`perms`:用户被授予的业务权限位数组(存储值;管理员以 `is_admin` 为准,perms 照常保存,取消管理员后按 perms 生效)。 -(仅管理员) +### POST /api/users 创建用户。 **请求**(JSON): ```json -{"username": "user1", "password": "pass123", "is_admin": false} +{"username": "user1", "password": "pass123", "is_admin": false, "perms": ["tasks"]} ``` +`perms` 可选,默认无业务权限;`is_admin` 默认 false。 -(仅管理员) +**响应**:`{"ok": true, "msg": "用户已创建"}` -更新用户(修改密码/管理员权限)。 +### PUT /api/users/ + +更新用户(改密码 / 管理员权限 / 权限位),只需传要改的字段。`uid` 不存在返回 404。 **请求**(JSON): ```json {"password": "newpass", "is_admin": true} ``` -(仅管理员) +**响应**:`{"ok": true, "msg": "用户已更新"}` +> 不能取消最后一个管理员(返回 400)。 -删除用户(不能删除 admin 和当前登录用户)。 +### DELETE /api/users/ + +删除用户。`uid` 不存在返回 404。 + +**响应**:`{"ok": true, "msg": "用户已删除"}` +> 不能删除默认管理员 `admin`、不能删除当前登录用户、也不能删除最后一个管理员(均返回 400)。 --- ## 9. 日志 -(权限:日志查看) +### GET /api/logs 查看日志文件内容。 +(权限:日志查看) + **参数**: | 参数 | 类型 | 默认 | 说明 | |------|------|------|------| @@ -407,9 +543,10 @@ "ok": true, "content": "2026-08-08 10:00:00 [INFO] [core.worker] ...", "file": "core.log", - "files": {"core": "core.log", "task": "task.log", "web": "web.log", "action": "action.log"} + "files": ["core.log", "task.log", "web.log", "action.log"] } ``` +`files`:可选日志文件名数组。 --- @@ -417,24 +554,49 @@ ### GET /api/custom_actions -列出所有自定义动作(步骤打包)。 +列出所有自定义动作(步骤打包)。需登录。 -(权限:任务管理) +**响应**: +```json +{"ok": true, "actions": [ + {"id": "a1b2c3d4", "name": "登录流程", "icon": "📦", + "steps": [{"type": "click", "...": "..."}], "created_at": "2026-08-08 10:00:00"} +]} +``` + +### POST /api/custom_actions 创建自定义动作。 +(权限:任务管理) + **请求**(JSON): ```json {"name": "登录流程", "icon": "📦", "steps": [{"type": "click", "...": "..."}]} ``` -(权限:任务管理) +**响应**:`{"ok": true, "msg": "动作已保存", "action": {...}}` -更新自定义动作。 +### PUT /api/custom_actions/ + +更新自定义动作(name/icon/steps,按需传字段)。不存在返回 404。 (权限:任务管理) -删除自定义动作。 +**请求**(JSON): +```json +{"name": "登录流程 v2", "steps": [{"type": "click", "...": "..."}]} +``` + +**响应**:`{"ok": true, "msg": "已更新", "action": {...}}` + +### DELETE /api/custom_actions/ + +删除自定义动作。不存在返回 404。 + +(权限:任务管理) + +**响应**:`{"ok": true, "msg": "已删除"}` --- @@ -449,18 +611,22 @@ {"ok": true, "running": true} ``` -(权限:设备控制) +### GET /api/uiauto/devices -获取 uiauto2 已连接的设备列表。 +获取 uiauto2 已连接的设备列表。uiautodev 本地服务未运行(或列表获取失败)返回 503。 + +(权限:设备控制) **响应**: ```json {"ok": true, "devices": [{"serial": "192.168.1.100:5555", "model": "Pixel 6"}]} ``` -(权限:设备控制) +### GET /api/uiauto/screenshot -通过 uiauto2 获取设备截图(JPEG)。 +通过 uiauto2 获取设备截图(JPEG)。uiautodev 本地服务未运行返回 503。 + +(权限:设备控制) **参数**: | 参数 | 类型 | 说明 | @@ -469,9 +635,11 @@ **响应**:成功返回 `image/jpeg`,失败返回 JSON 错误。 -(权限:设备控制) +### GET /api/uiauto/elements -获取设备 UI 元素树。 +获取设备 UI 元素树。uiautodev 本地服务未运行返回 503。 + +(权限:设备控制) **参数**: | 参数 | 类型 | 说明 | @@ -523,10 +691,12 @@ } ``` -(权限:应用管理) +### POST /api/apks/upload 上传 APK 文件(自动解析包名/版本/应用名)。 +(权限:应用管理) + **请求**(multipart/form-data): | 字段 | 类型 | 说明 | |------|------|------| @@ -537,14 +707,20 @@ {"ok": true, "apk": {"...": "..."}, "msg": "上传成功: 抖音"} ``` -(权限:应用管理) +### DELETE /api/apks/ 删除 APK 文件和记录。 (权限:应用管理) +**响应**:`{"ok": true, "msg": "..."}`;删除失败返回 400。 + +### POST /api/apks/install + 批量安装 APK 到指定设备。 +(权限:应用管理) + **请求**(JSON): ```json {"apk_id": "abc123", "serials": ["192.168.1.100:5555", "192.168.1.101:5555"]} @@ -557,13 +733,17 @@ ### GET /api/apks/install/devices -可安装设备列表:设备池在线设备(标记 `pool`)+ 本机 adb 设备(**含 USB 有线连接**,serial 无冒号标记 `usb`)。 +可安装设备列表:设备池在线设备 + 本机 adb 设备(含 USB 有线连接)。 安装弹窗用此列表,USB 设备安装时跳过 adb connect 直接安装。 +`source` 取值:`pool`=设备池在线;`usb`=本机 USB 有线(serial 无冒号);`adb`=本机网络 adb(serial 含冒号)。 + +(权限:应用管理) **响应**: ```json {"ok": true, "devices": [ - {"serial": "100.100.10.11:5555", "model": "22120RN86C", "source": "stf"}, + {"serial": "100.100.10.11:5555", "model": "22120RN86C", "source": "pool"}, + {"serial": "192.168.1.5:5555", "model": "", "source": "adb"}, {"serial": "ZY322ABCDEF", "model": "", "source": "usb"} ]} ``` @@ -595,10 +775,11 @@ --- -## 13. 维护 / 工具(仅管理员) +## 13. 维护 / 工具 -以下接口都**仅管理员可用**(普通用户即使有业务权限也访问不了,返回 403)。 -设备池管理、远程看屏、adb 终端等集中在"工具"页(页内子分栏)。 +设备池管理、自动发现、远程看屏/触控、定位、维护终端等集中在"工具"页(页内子分栏)。 +**权限说明**:设备池管理、自动发现、远程看屏/触控、定位等接口需 `devices` 权限; +**adb 终端仅管理员可用**(见各节标注)。 ### GET /api/devices/pool @@ -636,6 +817,69 @@ IP:5555 设备添加后立即尝试 adb connect,并后台采集型号(getpro 批量采集池内在线设备的型号(后台执行)。**权限**:`devices`。 +### GET /api/devices/discovery + +自动发现状态 + 待连接列表(pending)+ 正式池断联设备。前端约 10s 轮询一次。**权限**:`devices`。 + +**响应**: +```json +{ + "ok": true, + "enabled": true, "subnets": ["192.168.1.0/24"], "interval": 60, "port": 5555, + "scanning": false, + "last_scan": "2026-08-08 10:00", "last_result": {"found": 12, "verified": 3, "new": 1}, + "last_error": "", "pending_count": 2, + "pending": [{"serial": "192.168.20.5:5555", "source": "lan", + "first_seen": "2026-08-08 10:00", "last_seen": "2026-08-08 10:05", "online": true}], + "pool_offline": [{"serial": "192.168.1.100:5555", "model": "Pixel 6", "name": "测试机1"}] +} +``` +`pending` 只列当前在线设备(离线候选不可确认,下轮扫描自动更新);`pool_offline` +为正式池中已断联设备(发现线程每轮自动重连,也可手动触发重连)。 + +### POST /api/devices/discovery/scan + +手动触发一轮扫描(后台执行,约 5-30 秒)。**权限**:`devices`。 +扫描只验证并把新设备放进待连接池(pending),**确认后才入正式设备池**。 + +**响应**: +```json +{"ok": true, "msg": "扫描已启动(后台执行,约 5-30 秒)", + "result": {"found": 12, "verified": 3, "new": 1}} +``` +扫描进行中返回 409;未配置网段返回 400 并附原因。 + +### POST /api/devices/discovery/confirm + +确认连接:把待连接设备加入正式设备池并后台 adb connect/采型号。**权限**:`devices`。 + +**请求**(JSON):`{"serial": "192.168.20.5:5555", "name": "客厅机"}` +**响应**:`{"ok": true, "msg": "已加入设备池", "is_new": true}` + +### POST /api/devices/discovery/ignore + +忽略:从待连接列表删除(下轮扫描可能再次发现)。**权限**:`devices`。 + +**请求**(JSON):`{"serial": "..."}` +**响应**:`{"ok": true, "msg": "已忽略"}` + +### POST /api/devices/discovery/reconnect + +手动立即重连正式池中的断联设备(后台 adb connect + 采型号)。**权限**:`devices`。 +日常无需手动——发现线程每轮(默认 60s)自动重连断联设备。 + +**请求**(JSON):`{"serial": "..."}` +**响应**:`{"ok": true, "msg": "重连已启动(约 5-15 秒生效)"}` + +### POST /api/devices/discovery/settings + +保存自动发现配置(部分字段更新)。**权限**:`devices`。 + +**请求**(JSON):`{"enabled": true, "subnets": ["192.168.1.0/24"], "interval": 60, "port": 5555}` +`interval` 需在 10-3600 秒之间;`subnets` 逐项校验 CIDR,非法返回 400。 + +**响应**:`{"ok": true, "msg": "已保存"}` + ### GET /api/screen/stream 远程看屏:MJPEG 实时画面流(`multipart/x-mixed-replace`)。**权限**:`devices`。 @@ -649,12 +893,6 @@ IP:5555 设备添加后立即尝试 adb connect,并后台采集型号(getpro `?serial=xxx` 指定设备。监控大屏按设备每 ~2.5s 轮询一帧(页面不可见时暂停); 一次性请求(非流),与任务并发安全(与任务截图同走 u2 minicap)。 -### GET /wall - -监控大屏页面(全屏深色控制室风格,登录后可访问):设备卡片网格(缩略图/型号/ -状态/当前动作/进度)、顶部统计与时钟;状态每 5s 刷新、缩略图每 2.5s 轮询。 -20 台设备整体开销约 0.2 核 CPU + 100KB/s 带宽,普通电脑无压力。 - ### POST /api/screen/tap 点击设备屏幕。**权限**:`devices`。 @@ -673,6 +911,26 @@ IP:5555 设备添加后立即尝试 adb connect,并后台采集型号(getpro 输入文字(需焦点在输入框)。**请求**(JSON):`{"serial": "...", "text": "你好"}` +### POST /api/screen/tap_text + +按屏幕文字点击:先在 UI 树里做 text/description 子串匹配点元素中心(原生控件); +未命中则截图 OCR 找文字中心(WebView/图片/画布渲染文字)。**权限**:`devices`。 + +**请求**(JSON):`{"serial": "...", "text": "立即下载"}` +**响应**: +```json +{"ok": true, "found": true, "method": "ui", "matched": "立即下载", "x": 540, "y": 1200} +``` +`method`:`ui` 或 `ocr`;`found=false` 表示屏幕确实没有该文字(业务结果,非设备错误)。 + +### GET /api/screen/size + +获取设备屏幕原生分辨率(只读,供坐标换算——截图常是缩放图、操作需原生坐标)。**权限**:`devices`。 +离线/不可达返回 503。 + +`?serial=xxx` +**响应**:`{"ok": true, "width": 1080, "height": 2400}` + ### POST /api/device/screen_all 批量亮屏/息屏(并发)。**权限**:`devices`。 @@ -683,9 +941,25 @@ IP:5555 设备添加后立即尝试 adb connect,并后台采集型号(getpro ``` `serials` 可选:指定设备(离线自动过滤);不带则作用于全部在线设备。息屏会中断运行中的任务,前端有确认提示。 +### POST /api/device/locate + +定位设备:点亮屏幕并解锁(WAKEUP → dismiss-keyguard → MENU 兜底)。 +`show=true` 时额外用设备浏览器打开平台 `/locate` 大字定位页(更醒目,但会切换前台,任务运行中慎用)。**权限**:`devices`。 + +**请求**(JSON):`{"serial": "192.168.1.100:5555", "show": true}` +**响应**:`{"ok": true, "msg": "192.168.1.100:5555 屏幕已点亮;已打开大字定位页(按返回键退出)"}` + +### POST /api/device/locate/stop + +结束定位:优先 force-stop 定位时启动的浏览器(无论前后台都能关掉);无记录时前台是 +浏览器则 force-stop,否则按返回键轻量退出(不误杀任务应用)。**权限**:`devices`。 + +**请求**(JSON):`{"serial": "..."}` +**响应**:`{"ok": true, "msg": "已关闭浏览器 com.android.chrome"}` + ### GET /api/adb/devices -维护终端设备列表:本地 adb 已连接(含 offline)+ 设备池已配置设备(标记 `pool`)。 +维护终端设备列表(**仅管理员**):本地 adb 已连接(含 offline)+ 设备池已配置设备(标记 `pool`)。 供终端设备选择器使用——选中后前端自动附加 `-s `。 **响应**: @@ -693,13 +967,13 @@ IP:5555 设备添加后立即尝试 adb connect,并后台采集型号(getpro {"ok": true, "devices": [ {"serial": "100.100.10.11:5555", "state": "device"}, {"serial": "100.100.10.13:5555", "state": "offline"}, - {"serial": "100.100.10.12:5555", "state": "stf"} + {"serial": "100.100.10.12:5555", "state": "pool"} ]} ``` ### POST /api/adb/cmd -adb 远程终端:用平台 adb 二进制执行任意 adb 命令(20s 超时)。 +adb 远程终端(**仅管理员**):用平台 adb 二进制执行任意 adb 命令(20s 超时)。 **请求**(JSON): ```json @@ -716,7 +990,7 @@ adb 远程终端:用平台 adb 二进制执行任意 adb 命令(20s 超时 --- -## 14. 步骤测试(仅管理员可触发,需"设备控制"权限) +## 14. 步骤测试(需"设备控制"权限) ### POST /api/steps/test @@ -744,7 +1018,9 @@ adb 远程终端:用平台 adb 二进制执行任意 adb 命令(20s 超时 工具页"Tailscale 管理"子分栏,调用 Tailscale 官方 API v2。所有接口仅管理员可用。 前置:`.env` 配置 `TAILSCALE_API_KEY`(Settings → API Access Tokens)与 -`TAILSCALE_TAILNET`(tailnet 名,个人账号一般为邮箱前缀);未配置返回 502 并附提示。 +`TAILSCALE_TAILNET`(tailnet 名,个人账号一般为邮箱前缀)。 +`GET /api/tailscale/status` 未配置时返回 `200 {"ok": true, "configured": false}` 并附 `hint` 提示; +其余接口在未配置/调用失败时返回 `502` 并附错误信息。 设备 IP 由 tailnet 分配,API 不可修改,列表只读展示。 ### GET /api/tailscale/status @@ -814,7 +1090,7 @@ adb 远程终端:用平台 adb 二进制执行任意 adb 命令(20s 超时 ## 16. 工具(仅管理员) -工具页(剪贴板注入 / 应用版本管理 / 设备池管理)接口,均仅管理员可用。 +工具页(剪贴板注入 / 应用版本管理)接口,均仅管理员可用。 ### POST /api/tools/clipboard/set @@ -825,8 +1101,12 @@ adb 远程终端:用平台 adb 二进制执行任意 adb 命令(20s 超时 {"serials": ["100.100.10.11:5555", "0123456789ABCDEF"], "text": "要注入的文字"} ``` 设备来源与维护终端一致(本地 adb 含 USB + 设备池)。 -实现:u2 `jsonrpc.setClipboard`(实测 `cmd clipboard` 在 MIUI 上不存在),支持中文/引号/换行; -IP 设备先 adb connect(已连接跳过,绝不 disconnect),USB 设备首次自动推送 atx-agent。 +实现:通过 ClipInject(`com.example.clipinject`)透明 Activity 前台聚焦后写入剪贴板 +(shell 启动前台 Activity 不受后台启动限制),再用 u2 读回比对校验,支持中文/引号/换行。 +不再使用 u2 setClipboard / 自动推送 atx-agent(Android 10+ 禁止后台写剪贴板,旧 u2 +调用"成功"但内容被系统静默丢弃)。 +IP 设备先 adb connect(已连接跳过,绝不 disconnect);**设备未安装 ClipInject 时** am start +返回 unable to resolve Intent,接口明确报错提示先安装。 **响应**: ```json @@ -849,3 +1129,193 @@ IP 设备先 adb connect(已连接跳过,绝不 disconnect),USB 设备 "results": {"100.100.10.11:5555": {"installed": true, "version_name": "28.5.0", "version_code": "280500"}}} ``` 未安装返回 `installed: false`;查询失败的设备带 `error` 字段。 + +--- + +## 17. AI 控制台(仅管理员) + +浏览器内 AI 助手(DeepSeek 式多会话):用 MCP 工具操作指定设备、多轮上下文延续、 +成功后自动提炼经验记忆。**单实例:同时只允许一个 Agent 运行。** 以下接口均仅管理员可用。 + +### GET /api/agent/config + +读 Agent 配置。`api_key` 打码回显在 `api_key_masked` 字段。 + +**响应**: +```json +{"ok": true, "api_base": "https://api.deepseek.com", "model": "deepseek-v4-flash-vision-exp", + "api_key_masked": "sk-***abcd", "default_serial": "", "max_steps": "40"} +``` + +### POST /api/agent/config + +保存 Agent 配置(部分更新)。**请求**(JSON): +`{"api_base": "...", "model": "...", "api_key": "...", "default_serial": "...", "max_steps": 40}` +(`max_steps` 1-200,默认 40) + +**响应**:`{"ok": true, "msg": "已保存"}` + +### GET /api/agent/devices + +AI 可用设备列表(在线状态 + 是否有任务运行,前端据此把 busy 设备禁选)。 + +**响应**: +```json +{"ok": true, "devices": [ + {"serial": "192.168.1.100:5555", "model": "Pixel 6", "online": true, + "busy": false, "worker_status": "idle", "task_job": ""} +]} +``` + +### POST /api/agent/run + +启动 Agent(后台线程执行,立即返回 `run_id`)。**请求**(JSON): +`{"prompt": "打开抖音并点赞前 3 条视频", "serial": "192.168.1.100:5555", "conversation_id": "abc..."}` +`serial` 也可省略、用配置的 `default_serial`;`conversation_id` 绑定会话(历史从会话加载)。 + +**校验**:未配 API Key/模型名 → 400;设备不在池/离线 → 400;设备正有任务运行 → 409; +已有 Agent 运行中 → 409。 + +**响应**:`{"ok": true, "run_id": "8f3a2c9d"}` + +### GET /api/agent/run + +当前 Agent 运行状态(多窗口/页面刷新恢复用)。 + +**响应**: +```json +{"ok": true, "state": "running"|"idle"|"done", "run_id": "...", "prompt": "...", + "serial": "...", "started": "10:00:01", "answer": "...", "error": "", + "history": [{"role": "user", "content": "..."}]} +``` + +### GET /api/agent/stream?run_id= + +订阅事件流(SSE,EventSource)。事件: +- `event: delta` `{text, kind: content|reasoning}` — 流式文本增量 +- `event: step` `{tool, args, image?}` — 工具调用完成(MCP 步骤,image 为缩略截图) +- `event: done` `{answer}` — 完成 +- `event: error` `{message}` — 失败 +- 空闲时每 15s 发一行 `: keepalive` 注释防超时;`done`/`error` 后关流 + +### POST /api/agent/stop + +中断当前运行的 Agent(下一个检查点生效,数秒内)。无运行中任务返回 400。 + +**响应**:`{"ok": true, "msg": "已请求停止"}` + +### POST /api/agent/clear + +清空当前对话历史。 + +**响应**:`{"ok": true, "msg": "已清空"}` + +### GET /api/agent/conversations + +会话列表(按最近更新倒序)。 + +**响应**: +```json +{"ok": true, "conversations": [ + {"id": "abc...", "title": "打开抖音点赞", "updated_at": "2026-08-08 10:00", "count": 3} +]} +``` +`count` 为轮数(用户+助手消息对数)。 + +### POST /api/agent/conversations + +新建会话(空消息)。 + +**响应**:`{"ok": true, "id": "新会话id", "title": "新会话"}` + +### GET /api/agent/conversations/ + +会话详情(全部消息文本)。不存在返回 404。 + +**响应**: +```json +{"ok": true, "id": "...", "title": "...", "messages": [{"role": "user", "content": "..."}], + "created_at": "...", "updated_at": "..."} +``` + +### DELETE /api/agent/conversations/ + +删除会话(消息一并删除,不可恢复)。 + +**响应**:`{"ok": true, "msg": "会话已删除"}` + +### POST /api/agent/conversations//rename + +重命名会话。**请求**(JSON):`{"title": "新标题"}` + +**响应**:`{"ok": true, "msg": "已重命名"}` + +### GET /api/agent/experience + +经验记忆库列表(自进化,含最近一次巡检结论)。 + +**响应**: +```json +{"ok": true, "running": false, "last": "2026-08-08 03:47", "last_summary": "评审 5 条,建议删除 1 条(待人工确认)", + "experiences": [{"id": 1, "task_prompt": "打开抖音并点赞", "recipe": "...", "tool_seq": "...", + "hits": 3, "created_at": "...", "audit": {"verdict": "delete", "score": 3, + "reason": "...", "action": "pending", "at": "..."}}]} +``` +`audit` 为最近一次 AI 巡检结论(无则 null)。 + +### POST /api/agent/experience/delete + +人工确认删除经验(真删,巡检绝不自动删)。**请求**(JSON):`{"id": 1}` + +**响应**:`{"ok": true, "msg": "经验 #1 已删除"}` + +### POST /api/agent/experience/audit + +手动触发一轮经验巡检(后台线程,AI 评审只建议不删)。巡检进行中返回 409。 + +**响应**:`{"ok": true, "msg": "巡检已启动,完成后刷新列表查看建议"}` + +### POST /api/agent/experience/keep + +人工保留经验(撤销"建议删除",后续巡检不再重复建议)。**请求**(JSON):`{"id": 1}` + +**响应**:`{"ok": true, "msg": "经验 #1 已保留"}` + +--- + +## 18. 系统备份(仅管理员) + +整库备份导出/导入(users.db + 可选 APK 文件)。导入涉及整库替换,仅管理员可用。 + +### POST /api/system/backup/export + +生成导出 zip 并作为附件返回(含 users.db 一致快照 + manifest.json + 可选 `apks/`)。 +**请求**(JSON):`{"include_apk": true}` +**响应**:`application/zip` 附件下载(`download_name` 形如 `export_20260808_101000.zip`)。 + +### POST /api/system/backup/preview + +上传备份文件(multipart 字段 `file`,支持 .zip 或 .db)→ 暂存并校验 → 返回预览。 +校验失败返回 400。 + +**响应**: +```json +{"ok": true, "token": "12位hex", "preview": { + "file_name": "export_xxx.zip", "file_size": 123456, + "integrity": "ok", "schema_version": 4, "current_schema_version": 4, + "tables": [{"table": "user", "label": "用户", "rows": 3}], + "missing_optional": [], "warnings": ["备份为全量快照:含敏感信息,请妥善保管"] +}} +``` + +### POST /api/system/backup/apply + +确认应用导入:自动备份当前库到 `BACKUP_DIR/pre_restore_*.db`(安全网),再把暂存库 +落为「待生效恢复任务」。**重启 web_server 后生效**。token 无效/过期返回 400。 + +**请求**(JSON):`{"token": "12位hex"}` +**响应**: +```json +{"ok": true, "backup_name": "pre_restore_20260808_101000.db", + "message": "恢复任务已生成:当前库已自动备份,重启 web_server 后即应用导入的数据"} +``` diff --git a/doc/ARCHITECTURE.md b/doc/ARCHITECTURE.md index f6513ff..d10b9ba 100644 --- a/doc/ARCHITECTURE.md +++ b/doc/ARCHITECTURE.md @@ -1,6 +1,6 @@ # 架构详解 -本文面向想深入理解 `platform-tools` 内部设计的开发者。如果你只想使用,看 [README.md](file:///d:/platform-tools/README.md) 即可。 +本文面向想深入理解 `auto_control` 内部设计的开发者。如果你只想使用,看 [README.md](../README.md) 即可。 --- @@ -45,7 +45,7 @@ │ ▼ ┌──────────────┐ -│ data/users.db│ SQLite 持久化(用户/分组/任务/设备池/自定义动作/APK记录) +│ data/users.db│ SQLite 持久化(用户/分组/任务/设备池/待连接设备/自定义动作/APK记录/AI会话/经验库 + app_meta KV) └──────────────┘ ``` @@ -175,17 +175,23 @@ SQLAlchemy 模型,存于 `data/users.db`: | 模型 | 表名 | 说明 | |------|------|------| -| `User` | user | 后台用户(Flask-Login 认证,SHA256 密码;`is_admin` 管理员 + `perms` 业务权限位) | +| `User` | user | 后台用户(Flask-Login 认证,Werkzeug 哈希密码;`is_admin` 管理员 + `perms` 业务权限位) | | `DeviceGroup` | device_group | 设备分组(serials 存 JSON) | | `TaskJob` | task_job | 任务计划(target/params/schedule/retry 存 JSON) | | `CustomAction` | custom_action | 自定义动作(步骤打包,steps 存 JSON) | | `ApkFile` | apk_file | APK 文件元信息 | +| `Device` | device | 设备池清单(替代 STF 池;enabled=False 不参与调度,含 model 型号列) | +| `PendingDevice` | pending_device | 自动发现「待连接池」(扫描发现、用户确认后才入正式池) | + +> 表名默认取类名小写(models.py 未写 `__tablename__`)。另有三张非模型表,由原生 SQL 幂等创建、**不走 SCHEMA_MIGRATIONS**: +> - `app_meta`(KV):`_migrate_schema()` 内建表,存 `schema_version`、`discovery_*`、agent 配置 `agent_*` 等; +> - `agent_conversation` / `agent_experience` / `experience_audit`:AI 控制台会话 / 经验库 / 经验巡检(`web/agent_api.py` 顶部 `CREATE TABLE IF NOT EXISTS`)。 **数据库初始化**(`init_db`): - 创建所有表 - 首次启动创建默认管理员 `admin/admin123` - 自动迁移旧 `groups.json` / `jobs.json` 到 SQLite(迁移后归档为 `.migrated`) -- 版本化 schema 迁移(`SCHEMA_MIGRATIONS`):结构变更必须追加迁移条目,`create_all` 只建新表不加列 +- 版本化 schema 迁移(`SCHEMA_MIGRATIONS`,**当前到 v4**,见 `core/models.py`):结构变更必须追加迁移条目,`create_all` 只建新表不加列 ### 权限模型 @@ -242,13 +248,15 @@ APK 上传/解析/批量安装。 | `tasks_api.py` | 任务计划/分组/自定义动作/元素抓取/步骤测试 | | `admin_api.py` | 用户管理/日志 | | `tools_api.py` | adb 终端/剪贴板注入/应用版本 | -| `devices_api.py` | 设备池管理(增删停用/重连/型号采集) | +| `devices_api.py` | 设备池管理 + 自动发现(扫描/确认/忽略/手动重连/型号采集) | | `apks_api.py` | 应用管理 | | `tailscale_api.py` | Tailscale 管理 | +| `agent_api.py` | AI 控制台:会话 / 运行 / SSE / 停止 / 配置 / 经验库与巡检(读写 `agent_conversation`、`agent_experience`、app_meta `agent_*`) | +| `system_api.py` | 系统数据备份导出 / 导入恢复(`/api/system/backup/*`,仅 admin) | | `common.py` | 跨模块共享(合并设备列表/屏幕状态) | | `context.py` | 共享对象注入(mgr/apk_mgr/device_pool) | -`web_server.py` 只保留:app 创建、数据库初始化、蓝图注册、uiautodev 生命周期、启动(193 行)。 +`web_server.py` 只做装配与启动(266 行):app 创建;`init_db` 前消费待生效备份恢复(`consume_pending_restore`);初始化 device_pool / device_discovery;装配 TaskManager / ApkManager;注册 10 个蓝图(auth/monitor/tasks/admin/tools/devices/apks/tailscale/agent/system,`web/__init__.py`);注册经验巡检 APScheduler(03:47 Asia/Shanghai);uiautodev 子进程启停与设备池预连接线程;启动。 --- @@ -300,7 +308,7 @@ tasks/douyin/actions/like.py — @register_action(ACTIONS) LikeAction `templates/admin/monitor.html` 是纯 HTML+CSS+JS 单页应用,无框架依赖。 -- **Tab 切换**:5 个 Tab(监控/任务/分组/日志/用户),纯 DOM 操作 +- **Tab 切换**:7 个顶级 Tab(监控/任务/日志/用户/工具/AI 控制台/系统),均在 monitor.html 内 `.tab-panel` 切换(纯 DOM 操作);独立页面仅 `/login`、`/wall`。原"分组"已无顶层入口(移到工具页子分栏) - **数据获取**:`fetch()` 调 JSON API,5 秒轮询 `/api/status` - **状态渲染**:设备表格、任务卡片、进度条、徽章,纯 DOM 操作 @@ -317,9 +325,13 @@ tasks/douyin/actions/like.py — @register_action(ACTIONS) LikeAction ### 5.3 页内子分栏 -任务/工具 Tab 用通用 `showSubTab(tabId, name)` 实现页内子分栏:每个子分栏一个 `.sub-panel`, -`_activeSubs` 记住各 Tab 上次选中的子分栏。工具 Tab 集中了全部管理工具(剪贴板注入/adb 终端/ -Tailscale/应用管理/应用版本/已装应用/STF 设备管理),维护 Tab 仅保留 STF 服务。 +任务/工具/系统 Tab 用通用 `showSubTab(tabId, name)` 实现页内子分栏:每个子分栏一个 `.sub-panel`, +`_activeSubs` 记住各 Tab 上次选中的子分栏。现状子分栏: +- **任务**:任务计划 / 自定义动作 +- **工具**:剪贴板注入 / adb 远程终端 / Tailscale 管理 / 应用管理 / 应用版本管理 / 设备已装应用 / 设备池管理 / 设备分组 +- **系统**:数据备份 / 导入恢复 + +(原"分组"顶级 Tab 与"维护/STF 服务"子分栏已不存在——分组已移入工具页子分栏,STF 已摘除。) ### 5.4 元素抓取模态框 @@ -344,15 +356,15 @@ Tailscale/应用管理/应用版本/已装应用/STF 设备管理),维护 Ta ### 6.3 为什么绝不 kill-server -`adb kill-server` 会断开所有设备的 adb transport,导致 STF provider 误判全部设备离线并触发重连。连接失败就返回 False,由调用方处理。 +`adb kill-server` / `adb disconnect` 会断开共享的 adb transport(历史与 STF provider 共享;STF 摘除后红线仍保留——多 worker、前台扫描、设备自动发现共用同一 adb server),影响所有运行中的任务。连接失败就返回 False,由调用方处理。 ### 6.4 为什么状态查询带缓存 -STF API 响应慢(设备多时 2-5 秒),每次 `/api/status` 都打 STF 会阻塞 Flask。带 5 秒缓存,Worker 状态实时读内存(无 IO)。 +状态源 = 本地 SQLite 设备池 + adb + 内存 worker 状态。5 秒缓存避免每次 `/api/status` 都查库/adb 阻塞 Flask;Worker 实时状态读内存,不受缓存影响。 ### 6.5 为什么 DeviceOfflineError 不重试 -设备掉线后短时间内不会自愈,重试只会浪费配额并阻塞调度器。让设备进入冷却,由运维/STF 恢复后再启用。 +设备掉线后短时间内不会自愈,重试只会浪费配额并阻塞调度器。让设备进入冷却,可依赖设备自动发现(device_discovery 对正式池断联设备每轮 adb 重连)恢复后再启用。 --- @@ -361,14 +373,21 @@ STF API 响应慢(设备多时 2-5 秒),每次 `/api/status` 都打 STF ``` 主线程(Flask) ├── HTTP 请求处理(threaded=True,每请求一线程) - ├── APScheduler 线程(cron 触发) + ├── TaskManager.scheduler(APScheduler,cron 触发任务计划) + ├── 经验巡检 BackgroundScheduler(03:47 Asia/Shanghai,web_server 装配) + ├── 设备自动发现线程(device_discovery._discovery_loop,默认 60s 一轮) + ├── 设备池型号采集后台线程(device_pool._refresh_models_bg,启动/手动触发) + ├── 设备池预连接线程(web_server._preconnect_pool_devices,重启后加速恢复) + ├── AI Agent 运行线程(agent_api._agent_thread,单实例 + SSE 推送) ├── 看门狗线程(_Watchdog,30s 间隔) - ├── uiautodev 子进程 + ├── uiautodev 子进程(PID + cmdline 校验,防容器 PID 复用误杀) └── Worker 线程(每台设备一个) ├── _run_with_retry 线程(重试循环) └── BaseWorker 线程(设备生命周期 + run_task) ``` +> MCP server(127.0.0.1:8033)是**独立进程**(scripts/start.sh 拉起),不是 web_server 的线程。 + **线程安全**: - `_WORKERS_LOCK`:保护全局 worker 状态字典 - `_ADB_LOCK`:串行化所有 adb 调用 diff --git a/doc/DEPLOY.md b/doc/DEPLOY.md index 00d3abf..f3f0af3 100644 --- a/doc/DEPLOY.md +++ b/doc/DEPLOY.md @@ -75,6 +75,8 @@ pip install -r requirements.txt | uiautodev | >=0.14 | UI 元素抓取 | | pyaxmlparser | >=0.3.27 | APK 元信息解析 | | rapidocr_onnxruntime | >=1.4 | 屏幕 OCR(条件判断的 OCR识别 选择器,中英文模型随包内置,跨平台) | +| fastmcp | >=2.0 | MCP Server 与 AI 控制台 Agent(Streamable HTTP 服务端/客户端) | +| paramiko | >=3.0 | SSH 运维预留(历史 STF SSH 通道退役;配置 `STF_SSH_PASSWORD` 时走密码认证,未配置则退回系统 ssh 免密) | > 服务器(无显示器/Linux)环境建议把 opencv-python 换成 `opencv-python-headless`(rapidocr 依赖 cv2,两者取一)。 @@ -102,11 +104,17 @@ TAILSCALE_API_KEY=你的Tailscale_API_key | `USB_ADB_HOST` | `100.100.10.1` | USB 设备所在部署机(220)的 Tailscale IP | | `USB_ADB_PORT` | `5037` | 220 adb 容器监听端口(host 网络模式) | | `TAILSCALE_TAILNET` | 按邮箱前缀 | tailnet 名称/ID(个人账号一般为登录邮箱前缀) | +| `DISCOVERY_PORT` | `5555` | 设备自动发现扫描的 adb 端口(网段默认局域网 + Tailscale,可在设备池面板改) | +| `DISCOVERY_INTERVAL` | `60` | 自动发现扫描间隔(秒) | + +> 配置来源区分:`WEB_HOST`/`WEB_PORT`/`ADB_PATH` 是 **config.py 常量**(直接改文件,不经 .env); +> `TAILSCALE_API_KEY`/`TAILSCALE_TAILNET`/`USB_ADB_HOST`/`USB_ADB_PORT`/`DISCOVERY_PORT`/`DISCOVERY_INTERVAL` 由 config.py 以环境变量读取,**可在 .env 覆盖**; +> `BACKUP_DIR`/`RESTORE_STAGING_DIR`/`RESTORE_PENDING_DIR` 是常量(硬编码到 `data/`,见 §3.5)。 > **未配置 `WEB_SECRET_KEY`**:启动时随机生成(每次重启登录态失效,生产务必配置固定值)。 > **工具页 Tailscale 管理前置**:`.env` 写入 `TAILSCALE_API_KEY` 后重启服务; > 未配置时管理分区显示明确提示,不影响其他功能。 -> (历史遗留的 `STF_URL`/`STF_TOKEN`/`STF_SSH_*` 等配置已废弃,代码不再读取。) +> (历史遗留的 `STF_URL`/`STF_TOKEN`/`STF_SSH_*` 等配置:`config.py` 仍读取但无任何功能使用,仅历史保留;生产 `.env` 可留空。) ### 2.4 启动服务 @@ -118,12 +126,26 @@ python web_server.py (推荐配合 `scripts/supervise.sh` 进程守护,见 3.1 节) +**生产容器(220)入口 `scripts/start.sh`**(docker-compose 的 python-app 服务 command): + +1. **依赖就绪守卫**:flask/u2/uiautodev/rapidocr/cv2/fastmcp 全可用则跳过安装;否则 `pip install -r requirements.txt` 并做 cv2 环境修复(卸 GUI opencv → 装 headless) +2. **后台拉起 MCP server**:`MCP_ENABLED` 默认 `1` 时执行 `python3 -m mcp_server.mcp_server`(`MCP_ALLOW_WRITE=1`、`MCP_PLATFORM_USER` 默认 admin、`MCP_PLATFORM_PASS` 兜底 `admin123`),日志 `/tmp/mcp_server.log` +3. **前台启动主服务**:`exec python -u web_server.py` + +注意: + +- `scripts/supervise.sh` **只守护 web_server,不拉起 MCP**——容器场景 MCP 由 start.sh 拉起,不要用 supervise.sh 替代容器入口 +- **AI 控制台依赖 MCP**:`AGENT_MCP_URL` 默认 `http://127.0.0.1:8033/mcp`(`mcp_agent/config.py`) +- 手动起 web_server 需另启 MCP:`MCP_ALLOW_WRITE=1 python -m mcp_server.mcp_server` +- **改过 admin 密码务必同步 `MCP_PLATFORM_PASS`**(start.sh 兜底 `admin123` 会登录失败) +- 发布链路:dev 开发测试完成 → 负责人确认合并 main → 220 `git pull` → 重启 python-app 容器生效(详见 DEVELOPMENT §6) + ### 2.5 验证部署 1. 控制台看到 `启动服务: http://localhost:18050/` 即成功 2. 浏览器访问 `http://localhost:18050/` 3. 用 `admin/admin123` 登录 -4. 监控页应显示 STF 设备池中的设备 +4. 监控页应显示设备池中的设备(本机 adb 在线 + SQLite 配置清单,serial 形如 `100.100.10.x:5555`) --- @@ -169,7 +191,7 @@ PYTHON=./.venv/bin/python bash scripts/supervise.sh > 进程守护只保证服务重启,不恢复已运行的任务(worker 状态在内存)。 > -> **悬空设备自愈**:崩溃后残留的设备占用(STF 仍显示占用)可配置启动自动清理。单实例部署时在 `.env` 设置 `AUTO_RELEASE_STALE_OCCUPY=true`,web_server 启动会自动释放本账户残留占用;**多实例共用 STF 账户时不要开**(会误放另一实例的任务)。默认关,仅启动时提示。 +> **崩溃后占用自愈(需人工核实)**:历史 `AUTO_RELEASE_STALE_OCCUPY` 机制已随 STF 摘除失效(全库代码不再读取该配置)。设备池的占用互斥仍在(任务 running/connecting 的设备会被锁),但崩溃后残留占用如何释放、是否自动清理,需按当前实现核实后补写本节。 **Docker(生产 python-app 容器)**: @@ -207,7 +229,7 @@ server { - **修改默认密码**:登录后立即在"用户"Tab 修改 admin 密码 - **最小权限分配**:需要多人使用后台时,在"用户"Tab 创建普通用户并只勾选必要权限 (任务管理/设备控制/应用管理/日志查看),不要把 admin 密码共享出去 -- **修改 SECRET_KEY**:编辑 `web_server.py`,把 `app.config["SECRET_KEY"]` 改成随机字符串 +- **会话密钥 SECRET_KEY**:在 `.env` 写固定 `WEB_SECRET_KEY`(`python -c "import secrets;print(secrets.token_hex(32))"` 生成);未配置时启动随机生成(每次重启登录态失效,生产务必配置固定值) - **限制访问**:生产环境把 `WEB_HOST` 改为 `127.0.0.1`,配合反向代理 - **防火墙**:只开放必要端口 @@ -219,9 +241,22 @@ server { ### 3.5 数据备份 -- 数据库 `data/users.db` 包含用户/分组/任务数据 -- APK 文件在 `data/apks/` -- 建议定期备份 `data/` 目录 +**优先推荐平台功能「系统 → 数据备份导出/导入」(仅 admin)**: + +- **导出**:`POST /api/system/backup/export` → 用 sqlite 在线备份 API 对 `data/users.db` 做一致快照,打包 zip(`users.db` + `manifest.json` + 可选 `apks/*.apk`) +- **导入**:上传 zip/`.db` → 校验预览(完整性/必需表/schema 版本告警)→ 确认后自动把当前库快照到 `data/backups/pre_restore_*.db`(安全网可回滚)→ 落 `data/restore_pending/` → **重启 web_server 生效**(web_server 在 `init_db` 前自动消费恢复任务) + +目录与常量: + +| 目录 | 用途 | +|---|---| +| `data/backups/` | 导出临时 zip + 恢复前快照 `pre_restore_*.db` + 校验失败的 `restore_failed_*` | +| `data/restore_staging/` | 导入暂存,TTL 30 分钟未应用自动清理 | +| `data/restore_pending/` | 待生效恢复任务(重启时消费) | + +常量 `BACKUP_DIR` / `RESTORE_STAGING_DIR` / `RESTORE_PENDING_DIR`(config.py L63-68,非 .env,已 .gitignore)。 + +手工备份 `data/` 目录仍可作兜底,但**整库恢复建议走上述功能**(在线一致快照 + 预恢复备份 + 重启原子生效,避免手工替换被 WAL/占用文件破坏)。 --- @@ -233,7 +268,7 @@ server { |------|------|------| | 18050 | Web 后台 | 主服务端口(config.py 可改) | | 20242 | uiautodev | 元素抓取服务(自动启动,固定端口) | -| 7100 | STF | STF 服务端口(STF 自己的配置) | +| 8033 | MCP | MCP 手机控制 Server(scripts/start.sh 自动拉起,`MCP_ENABLED=0` 可关) | | 5555 | adb | 设备 adb 网络端口(设备端) | ### 4.2 Windows 端口问题(仅 Windows) @@ -291,16 +326,16 @@ python web_server.py | `ModuleNotFoundError: No module named 'flask'` | 依赖未安装 | `pip install -r requirements.txt` | | `WinError 10013` | 端口被排除/权限不足 | 改端口或用管理员运行 | | `WinError 10048` | 端口被占用 | 改端口或杀占用进程 | -| STF 获取设备列表失败 | STF 地址/token 错误 | 检查 `config.py` 的 `STF_URL` 和 `STF_TOKEN` | +| 监控页设备列表为空 | 设备不在池中 / adb 连不上 | 「工具 → 设备池管理」确认已添加且在线,检查设备网络与 5555 端口 | ### 6.2 设备连接失败 | 现象 | 原因 | 解决 | |------|------|------| -| `DeviceOfflineError` | 设备掉线/STF provider 卡死 | 检查设备网络/STF 状态 | +| `DeviceOfflineError` | u2 连接超时 / atx-agent 无响应 | 检查设备网络,重启设备或重新推送 atx-agent | | `u2.connect 超时` | atx-agent 无响应 | 重启设备/重新推送 atx-agent | | `adb connect failed` | 设备网络不通/端口未开放 | 检查设备 IP 和 5555 端口 | -| STF 设备显示离线 | STF 状态缓存 | 用"扫描前台App"复测 | +| 设备显示离线 | 状态缓存/误报 | 管理后台设备池「重连」,或「扫描前台 App」复测 | ### 6.3 任务不执行 @@ -317,7 +352,7 @@ python web_server.py # 查看核心日志 # 方式一:Web 后台"日志"Tab # 方式二:直接看文件 -# logs/core.log — STF/adb/worker/task_manager +# logs/core.log — adb/worker/task_manager # logs/task.log — 任务执行 # logs/web.log — Web 请求 # logs/action.log — 操作执行 diff --git a/doc/DEVELOPMENT.md b/doc/DEVELOPMENT.md index c977548..65e73c8 100644 --- a/doc/DEVELOPMENT.md +++ b/doc/DEVELOPMENT.md @@ -43,7 +43,7 @@ |------|------|------| | 开发机 | 本机(192.168.20.57) | `.venv` + 本地运行 `web_server.py` | | 生产机 | 部署机 220 的 `auto_control` | python-app 容器,`network_mode: host` | -| STF 服务 | ~~`192.168.20.220:7100`~~ | 已停用(2026-08-18 `docker stop stf`,代码已摘除依赖) | +| STF 服务 | ~~`192.168.20.220:7100`~~ | 已停用(代码已摘除依赖)。注意:此处"已停用"与 [STF_REMOVAL.md](STF_REMOVAL.md) 的"待人工确认"项矛盾(220 侧是否已 `docker stop stf` 未核实),需以 220 实际为准 | | adb 容器 | 220 上 `adb`(host 网络 5037) | USB 设备远程 adb server;网络设备补连用 | | 设备 | Tailscale `100.100.10.x:5555` | Xiaomi 舰队,本机 `100.100.10.2` 在 tailnet 内 | | uiautodev | 本机 `20242` | 元素抓取服务(web_server 自动拉起) | @@ -62,6 +62,22 @@ - 本机已在 tailnet 内,直连可靠且快(<1s) - 设备加入/退出平台:工具 → 设备池管理(SQLite 清单,自动连接 + 型号采集) +### 2.3 配置键速查(`config.py` / `.env`) + +`.env` 加载方式:项目根目录逐行解析、`os.environ.setdefault`(环境变量已设则不覆盖)。以下默认值以 `config.py` 为准: + +| 键 | 默认值 | 说明 | +|------|------|------| +| `WEB_HOST` / `WEB_PORT` | `0.0.0.0` / `18050` | Web 后台监听(18050 避开 Windows 动态端口范围) | +| `DATA_DIR` / `APK_DIR` | `data/` / `data/apks/` | 持久化数据目录 / APK 存储目录 | +| `BACKUP_DIR` / `RESTORE_STAGING_DIR` / `RESTORE_PENDING_DIR` | `data/backups` / `data/restore_staging` / `data/restore_pending` | 系统备份:导出 zip、导入暂存、待重启生效的恢复目录 | +| `ADB_PATH` | `bin/adb/adb`(Windows 为 `adb.exe`) | 按平台自动识别,代码只拼路径 | +| `USB_ADB_HOST` / `USB_ADB_PORT` | `100.100.10.1` / `5037` | 220 的 adb 容器(host 网络),驱动远程 USB 设备 | +| `DISCOVERY_PORT` / `DISCOVERY_SUBNETS` / `DISCOVERY_INTERVAL` | `5555` / 局域网+Tailscale 网段 / `60` | 设备自动发现;可在工具页设备池面板改,存 `app_meta` `discovery_*` 覆盖默认 | +| `TAILSCALE_API_KEY` / `TAILSCALE_TAILNET` | 空 / 空 | 工具页 Tailscale 管理(官方 API v2) | +| `WEB_SECRET_KEY` | 未配置则随机生成 | 会话密钥(web_server 读 `.env`;不配则重启登录态失效) | +| `STF_URL` / `STF_TOKEN` / `STF_SSH_*` | 废弃 | STF 摘除后仅历史保留,代码不再使用 | + --- ## 3. 技术红线(开发限制)—— 违反会打断共享 adb transport,需人工恢复 @@ -92,7 +108,7 @@ - **任务参数放各自 `tasks//task.py` 顶部,不放 `config.py`** - **生产环境(220)默认只读**:任何写操作(改文件/重启容器/部署)都必须先经负责人确认 - **数据库是 SQLite**(`data/users.db`,WAL 模式):运行时数据不提交 git -- **前端 JS 在 `static/admin/monitor.js`**(已从 HTML 拆分),HTML 里用 `