Initial rrweb SGrobot skill package

This commit is contained in:
2026-06-24 09:45:48 +08:00
commit 27a0ebe1d7
17 changed files with 1554 additions and 0 deletions

View File

@@ -0,0 +1,23 @@
[skill]
name = "rrweb-customer-callback-draft-execute"
description = "Run the generated rrweb semantic trace for 客户回访工单登记 through sgBrowser."
version = "0.1.0"
author = "rrweb-skill"
tags = ["rrweb", "semantic-trace", "sgbrowser-action", "generated"]
prompts = [
"Use rrweb-customer-callback-draft-execute only for the generated 客户回访工单登记 browser flow.",
"Run it after browser.open_url has opened the target page.",
"Return the structured JSON evidence emitted by the tool; do not use Playwright, CDP, or generic browser_action for this step."
]
[[tools]]
name = "execute_trace"
description = "Replay or dry-run the generated 客户回访工单登记 semantic trace in sgBrowser."
kind = "sgbrowser_action"
command = "sgBrowserExcuteJsCodeByDomain"
expected_domain = "127.0.0.1"
[tools.args]
script_path = "scripts/execute_trace.js"
mode = "Baked default mode is execute. Runtime may pass mode=execute or mode=dry-run if supported."

View File

@@ -0,0 +1,240 @@
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(),
});

View File

@@ -0,0 +1,31 @@
{
"id": "customer-callback-draft",
"name": "客户回访工单登记",
"description": "客服打开待回访工单,选择客户满意,填写回访备注,并保存草稿。",
"startUrl": "http://127.0.0.1:4187/customer-callback.html",
"steps": [
{
"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"
}
]
}