const TRACE = { "id": "customer-callback-draft", "name": "客户回访工单登记", "start_url": "http://127.0.0.1:4187/customer-callback.html", "expected_domain": "127.0.0.1", "default_mode": "execute", "actions": [ { "type": "assertText", "text": "客户回访工单登记", "label": "确认进入客户回访工单页面" }, { "type": "select", "selector": "[data-trace=\"callback-result\"]", "value": "satisfied", "label": "选择回访结果" }, { "type": "fill", "selector": "[data-trace=\"callback-remark\"]", "value": "已电话回访客户,现场供电恢复正常,客户确认满意。", "label": "填写回访备注" }, { "type": "click", "selector": "[data-action=\"save-draft\"]", "label": "点击保存草稿", "risk": "write" } ] }; const input = (typeof args === "object" && args) || (typeof tool_args === "object" && tool_args) || (typeof sgclaw_args === "object" && sgclaw_args) || {}; function normalizeMode(value) { const raw = String(value || TRACE.default_mode || "dry-run").trim().toLowerCase(); return raw === "execute" ? "execute" : "dry-run"; } function normalizeType(value) { return String(value || "").replace(/[\s_-]+/g, "").toLowerCase(); } function cleanText(value) { return String(value || "").replace(/\s+/g, " ").trim(); } function selectorList(action) { const selectors = []; if (action.selector) { selectors.push(action.selector); } if (Array.isArray(action.selectors)) { for (const selector of action.selectors) { if (selector) { selectors.push(selector); } } } return selectors; } function findByXPath(xpath) { try { return document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; } catch (_) { return null; } } function findByText(text) { const needle = cleanText(text); if (!needle) { return null; } const candidates = Array.from(document.querySelectorAll("button,a,input,textarea,select,[role='button'],[role='link'],label")); return candidates.find((node) => cleanText(node.innerText || node.value || node.getAttribute("aria-label") || node.textContent).includes(needle)) || null; } function findElement(action) { for (const rawSelector of selectorList(action)) { const selector = String(rawSelector).trim(); if (!selector) { continue; } if (selector.startsWith("xpath=")) { const node = findByXPath(selector.slice("xpath=".length)); if (node) { return node; } continue; } if (selector.startsWith("//") || selector.startsWith("(//")) { const node = findByXPath(selector); if (node) { return node; } continue; } try { const node = document.querySelector(selector); if (node) { return node; } } catch (_) { continue; } } return findByText(action.text || action.label); } function describeElement(node) { if (!node) { return null; } return { tag: String(node.tagName || "").toLowerCase(), id: node.id || "", name: node.getAttribute ? node.getAttribute("name") || "" : "", text: cleanText(node.innerText || node.value || node.textContent).slice(0, 120), }; } function dispatchFieldEvents(node) { node.dispatchEvent(new Event("input", { bubbles: true })); node.dispatchEvent(new Event("change", { bubbles: true })); } function runAction(action, index, shouldExecute) { const type = normalizeType(action.type); const result = { index, type: action.type, label: action.label || "", mode: shouldExecute ? "execute" : "dry-run", ok: true, }; if (type === "asserttext" || type === "waitfortext") { const needle = cleanText(action.text || action.value); const haystack = cleanText(document.body ? document.body.innerText : document.documentElement.textContent); result.ok = needle ? haystack.includes(needle) : false; result.expected_text = needle; return result; } if (type === "extracttext") { const target = findElement(action) || document.body || document.documentElement; result.text = cleanText(target.innerText || target.textContent).slice(0, Number(action.max_chars || 2000)); result.element = describeElement(target); return result; } const element = findElement(action); result.element = describeElement(element); if (!element) { result.ok = false; result.error = "target element not found"; return result; } if (type === "click" || type === "submit") { if (shouldExecute) { element.scrollIntoView({ block: "center", inline: "center" }); if (typeof element.focus === "function") { element.focus(); } if (type === "submit" && element.form) { element.form.requestSubmit ? element.form.requestSubmit() : element.form.submit(); } else { element.click(); } } return result; } if (type === "fill" || type === "input" || type === "type") { result.value = action.value; if (shouldExecute) { element.focus && element.focus(); element.value = String(action.value); dispatchFieldEvents(element); } return result; } if (type === "select") { result.value = action.value; if (shouldExecute) { element.value = String(action.value); dispatchFieldEvents(element); } return result; } if (type === "check" || type === "uncheck") { result.checked = type === "check"; if (shouldExecute) { element.checked = type === "check"; dispatchFieldEvents(element); } return result; } result.ok = false; result.error = "unsupported action type"; return result; } const mode = normalizeMode(input.mode || input.execution_mode || input.executionMode); const shouldExecute = mode === "execute"; const actionResults = []; let ok = true; for (let index = 0; index < TRACE.actions.length; index += 1) { const actionResult = runAction(TRACE.actions[index], index, shouldExecute); actionResults.push(actionResult); if (!actionResult.ok) { ok = false; break; } } return JSON.stringify({ ok, trace_id: TRACE.id, trace_name: TRACE.name, mode, executed: shouldExecute, expected_domain: TRACE.expected_domain, url: window.location.href, title: document.title || "", action_count: TRACE.actions.length, action_results: actionResults, finished_at: new Date().toISOString(), });