feat(AI 建任务): AI 自己在真机探索 → 写出可调度任务 → 人工确认入库
AI 控制台下新增子分栏「🧭 AI 建任务」:描述要做什么(例:建一个跑 2 小时的任务、自动刷
某 App、随机点赞),AI 用 de_* 工具自己在设备上探索(看屏/读元素树/点按验证),把走通的
路径写成一条任务草稿,经服务端校验后交人在步骤编辑器里核对/手改/试跑,保存才入库。
链路:POST /api/agent/run{mode:"designer", settings}
→ Agent 自探 → 本地工具 submit_task(draft)
→ core/task_draft 校验(失败把 errors 回灌模型让它改)
→ 只暂存(运行态 + app_meta.agent_task_draft,**不落库**)
→ SSE done{mode,draft,warnings} → 页面草稿预览 → openTaskModal(null, prefill) 预填
关键实现
- core/task_draft.py(新):把执行器的"静默跳过点"(未知 type/空 selector/空 children/
嵌套>5/节点>60/cron 非法/必填缺失)前移成显式 error——POST /api/jobs 对 params 是盲存的,
执行器又静默跳过错误步骤,没有这道闸门就是"任务建好了、跑起来什么都没做"。
归一化兜底任务名/target/schedule/retry/时长;页面填的设置以 overrides 优先于模型。
故意**不比执行器更严**:loop_mode 近义值归一(count→rounds)、缺 max_iterations 补默认 10
(执行器本来就默认)——实测卡太死会把一轮探索耗在改字段上。
有副作用的步骤(评论/发送/购买/删除…)只警告并把触发概率压到 30%(编辑器可改回)。
- mcp_agent/agent.py:双系统提示词(CHAT/DESIGNER)+ 平台级本地工具
(LOCAL_TOOL_SPECS,不进 MCP)+ 每工具调用上限 40 + designer 输出上限 8192 +
**json.loads 容错**(草稿被截断时给模型可读错误,而不是整轮失败)。
- web/agent_api.py:mode/settings 透传、submit_task 处理器(app_context 内校验+暂存)、
done 带 draft、GET /api/agent/task_draft{,+POST,/clear}(草稿走 app_meta,不新建表)。
- 前端:static/admin/taskgen.js + #agent-sub-taskgen 子面板(showSubTab 机制);
tasks.js 的 openTaskModal(jobId, prefill) + 信封归一化 + 唯一 draftKey;
agent.js 按 mode 门控(一个 run 只有一个事件队列,两个 EventSource 会互相瓜分事件)。
顺带修掉一个 chat 也踩的协议 bug:一轮里同时调 de_screenshot 与别的工具时,截图图像会被
插在两条 tool 消息之间 → 模型侧判"工具回应不足"直接 400。改为本轮 tool 消息发完再附图像,
_repair_tool_messages 也改成只数**连续**的 tool 消息。
真机实测(Redmi 22120RN86C,设置页):8 步探索(含 tap_text 验证)→ submit_task 一次通过 →
草稿 8 个顶层步骤(screen_on/open_app/wait_el/click/wait/key_event…)、max_duration 1800、
无 click_xy、3 条 evidence;页面恢复草稿 + 预填编辑器 + 提示块渲染均正常,无 JS 报错。
自测数据已清理(草稿已丢弃、未创建任何任务)。
文档:AI_TASK_GEN.md 状态改「P0 已实现」+ §10 实现记录(差异/护栏/未做项)、AI_CONSOLE.md
(子分栏、designer 分支、SSE done 负载、app_meta 键)、API.md、DATA_MODEL.md、ARCHITECTURE.md、
DEVELOPMENT.md(自测入口)、README.md 索引、backlog 勾掉 P0。
This commit is contained in:
@@ -352,6 +352,40 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
|
||||
.agent-live-body .agent-empty{color:#4b5563;padding:20px;font-size:12px;line-height:1.8}
|
||||
.agent-shell{display:flex;flex-direction:column;height:calc(100vh - 150px);min-height:520px;border:1px solid #e5e7eb;border-radius:14px;overflow:hidden;background:#fff}
|
||||
.agent-shell{display:flex;flex-direction:column;height:calc(100vh - 150px);min-height:480px;border:1px solid #e5e7eb;border-radius:14px;overflow:hidden;background:#fff}
|
||||
/* AI 控制台里的子分栏占了一行,聊天区高度相应扣掉,避免整页出现滚动条 */
|
||||
#agent-sub-chat .agent-shell{height:calc(100vh - 205px)}
|
||||
|
||||
/* ===== AI 建任务(子面板 2) ===== */
|
||||
.tg-wrap{display:flex;gap:14px;height:calc(100vh - 205px);min-height:500px}
|
||||
.tg-left{width:360px;flex:none;display:flex;flex-direction:column;gap:10px;overflow-y:auto;padding-right:2px}
|
||||
.tg-right{flex:1;min-width:0;display:flex;flex-direction:column;gap:10px}
|
||||
.tg-card{border:1px solid #e5e7eb;border-radius:12px;padding:12px;background:#fff}
|
||||
.tg-title{font-size:12.5px;font-weight:700;color:#374151;margin-bottom:8px}
|
||||
.tg-fold:hover{color:var(--primary)}
|
||||
.tg-row{display:flex;align-items:center;gap:8px;margin-top:8px}
|
||||
.tg-hint{font-size:11px;color:var(--text-light);line-height:1.5}
|
||||
.tg-actions{display:flex;gap:8px}
|
||||
.tg-actions .btn{flex:1}
|
||||
.tg-status{font-size:11.5px;color:var(--text-light);min-height:16px;line-height:1.5}
|
||||
.tg-right-head{display:flex;justify-content:space-between;align-items:center;font-size:12.5px;font-weight:700;color:#374151}
|
||||
.tg-stream{flex:1;min-height:120px;overflow-y:auto;border:1px solid #e5e7eb;border-radius:12px;background:#fff;padding:10px;display:flex;flex-direction:column;gap:6px}
|
||||
.tg-stream .agent-toolcard{border-left:2px solid var(--teal);padding-left:8px;font-size:12px}
|
||||
.tg-stream .agent-toolcard img{max-width:180px;border-radius:6px;display:block;margin-top:6px}
|
||||
.tg-stream .agent-toolcard .dot{width:6px;height:6px;border-radius:99px;background:var(--teal);display:inline-block;margin-right:6px}
|
||||
.tg-stream .tg-err{color:#b91c1c;font-size:11.5px;margin-top:4px;font-family:var(--mono);word-break:break-all}
|
||||
.tg-draft-wrap{max-height:46%;overflow-y:auto}
|
||||
.tg-draft{border:1px solid #e5e7eb;border-radius:12px;background:#fff;padding:12px}
|
||||
.tg-draft h4{margin:0 0 8px;font-size:13px}
|
||||
.tg-step{font-family:var(--mono);font-size:11.5px;padding:3px 0;border-bottom:1px dashed #eef0f3;white-space:pre-wrap;word-break:break-all}
|
||||
.tg-step:last-child{border-bottom:none}
|
||||
.tg-step .t{color:#0f766e;font-weight:600}
|
||||
.tg-step .p{color:#b45309}
|
||||
.tg-note{background:#fffbeb;border:1px solid #fde68a;border-radius:8px;padding:8px 10px;font-size:11.5px;color:#92400e;margin-top:8px}
|
||||
.tg-warn{background:#fef2f2;border:1px solid #fecaca;border-radius:8px;padding:8px 10px;font-size:11.5px;color:#b91c1c;margin-top:8px}
|
||||
.tg-evidence{margin-top:8px;font-size:11px;color:var(--text-light)}
|
||||
.tg-evidence summary{cursor:pointer;font-weight:600}
|
||||
.tg-evidence div{padding:3px 0;border-bottom:1px dashed #eef0f3}
|
||||
.tg-btns{display:flex;gap:8px;margin-top:12px;flex-wrap:wrap}
|
||||
.agent-topbar{display:flex;align-items:center;justify-content:space-between;padding:10px 16px;border-bottom:1px solid #e5e7eb;background:#f9fafb}
|
||||
.agent-title{font-size:14px;font-weight:700;color:#111827}
|
||||
.agent-model-tag{margin-left:8px;font-size:11px;color:#0e7490;font-family:var(--mono);background:#e0f2fe;padding:2px 8px;border-radius:10px}
|
||||
@@ -532,6 +566,13 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
|
||||
<!-- ========== Tab 3: 分组 ========== -->
|
||||
<!-- ========== Tab: AI 控制台(DeepSeek 风格聊天 + 实时 MCP 步骤) ========== -->
|
||||
<div id="tab-agent" class="tab-panel">
|
||||
<div class="sub-tabs">
|
||||
<button class="sub-tab active" data-sub="chat" onclick="showSubTab('agent','chat')">💬 聊天</button>
|
||||
<button class="sub-tab" data-sub="taskgen" onclick="showSubTab('agent','taskgen')">🧭 AI 建任务</button>
|
||||
</div>
|
||||
|
||||
<!-- ===== 子面板 1:聊天(原有 AI 控制台,原样不动) ===== -->
|
||||
<div class="sub-panel active" id="agent-sub-chat">
|
||||
<div class="agent-shell">
|
||||
<div class="agent-topbar">
|
||||
<div class="agent-title">🤖 AI 控制台<span id="agent-model-tag" class="agent-model-tag"></span><span id="agent-token-total" class="agent-token-total" title=""></span></div>
|
||||
@@ -580,6 +621,93 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /agent-sub-chat -->
|
||||
|
||||
<!-- ===== 子面板 2:AI 建任务 =====
|
||||
AI 自己在真机上探索(看屏/读元素树/点按验证),把路径写成**任务步骤**,
|
||||
经服务端校验后交人工确认(打开步骤编辑器 → 保存才入库)。
|
||||
见 doc/AI_TASK_GEN.md -->
|
||||
<div class="sub-panel" id="agent-sub-taskgen">
|
||||
<div class="tg-wrap">
|
||||
<div class="tg-left">
|
||||
<div class="tg-card">
|
||||
<div class="tg-title">1️⃣ 说清楚要做什么</div>
|
||||
<textarea id="tg-prompt" class="form-control" rows="4"
|
||||
placeholder="例如:建一个跑 2 小时的任务,自动刷抖音、随机点赞;拿不准的先别真发"></textarea>
|
||||
<div class="tg-row">
|
||||
<button class="btn btn-xs" onclick="tgExample()">用个例子</button>
|
||||
<span class="tg-hint">AI 会自己在设备上探索,把走通的路径写成任务步骤</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tg-card">
|
||||
<div class="tg-title">2️⃣ 选一台空闲设备</div>
|
||||
<select id="tg-serial" class="form-control">
|
||||
<option value="">加载设备…</option>
|
||||
</select>
|
||||
<div class="tg-hint" id="tg-serial-hint">有任务在跑的设备不可选(AI 不与任务抢设备)</div>
|
||||
</div>
|
||||
|
||||
<div class="tg-card">
|
||||
<div class="tg-title tg-fold" onclick="tgToggleAdv()">3️⃣ 任务设置(可选,留空由 AI 判断)
|
||||
<span style="float:right" id="tg-adv-arrow">▸</span></div>
|
||||
<div id="tg-adv" style="display:none">
|
||||
<div class="form-group"><label>任务名</label>
|
||||
<input id="tg-name" class="form-control" placeholder="留空=AI 自动起名"></div>
|
||||
<div class="form-group"><label>目标设备</label>
|
||||
<select id="tg-target" class="form-control" onchange="tgToggleTarget()">
|
||||
<option value="">跟随探索设备</option>
|
||||
<option value="all">全部空闲设备</option>
|
||||
<option value="group">按分组</option>
|
||||
<option value="serial">指定设备</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" id="tg-group-wrap" style="display:none">
|
||||
<label>分组</label><select id="tg-group" class="form-control"></select>
|
||||
</div>
|
||||
<div class="form-group"><label>调度</label>
|
||||
<select id="tg-sched-mode" class="form-control" onchange="tgToggleSched()">
|
||||
<option value="">跟随 AI 判断</option>
|
||||
<option value="once">手动(不自动启动)</option>
|
||||
<option value="daily">每天定时</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-row" id="tg-sched-wrap" style="display:none">
|
||||
<div class="form-group"><label>启动</label>
|
||||
<input type="time" id="tg-sched-start" class="form-control" value="20:00"></div>
|
||||
<div class="form-group"><label>停止(可留空)</label>
|
||||
<input type="time" id="tg-sched-stop" class="form-control"></div>
|
||||
</div>
|
||||
<div class="form-group"><label>运行时长上限(秒)</label>
|
||||
<input id="tg-maxdur" type="number" class="form-control"
|
||||
placeholder="如 7200 = 2 小时;0/留空 = 不限"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tg-actions">
|
||||
<button class="btn btn-primary" id="tg-start" onclick="startExplore()">🚀 开始探索</button>
|
||||
<button class="btn btn-danger" id="tg-stop" style="display:none" onclick="stopExplore()">■ 停止</button>
|
||||
</div>
|
||||
<div class="tg-status" id="tg-status"></div>
|
||||
|
||||
<div class="tg-card" id="tg-live-card" style="display:none">
|
||||
<div class="tg-title">📺 跟随画面 <span class="tg-hint" id="tg-live-serial"></span></div>
|
||||
<img id="tg-live-img" alt="" style="width:100%;border-radius:8px;display:none">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tg-right">
|
||||
<div class="tg-right-head">
|
||||
<span>探索回放</span>
|
||||
<span class="tg-hint" id="tg-token"></span>
|
||||
</div>
|
||||
<div class="tg-stream" id="tg-stream">
|
||||
<div class="agent-empty">AI 在设备上的每一步(看屏 / 读元素树 / 点按验证)会实时显示在这里。</div>
|
||||
</div>
|
||||
<div class="tg-draft-wrap" id="tg-draft"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div><!-- /agent-sub-taskgen -->
|
||||
</div>
|
||||
|
||||
<!-- ========== 配置模态框 ========== -->
|
||||
@@ -1082,6 +1210,7 @@ body{background:var(--bg);font-family:var(--body);color:var(--text);font-size:14
|
||||
<script src="/static/admin/apps.js"></script>
|
||||
<script src="/static/admin/admin.js"></script>
|
||||
<script src="/static/admin/agent.js"></script>
|
||||
<script src="/static/admin/taskgen.js"></script>
|
||||
<script src="/static/admin/system.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user