241 lines
6.4 KiB
JavaScript
241 lines
6.4 KiB
JavaScript
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(),
|
|
});
|