Initial rrweb SGrobot skill package
This commit is contained in:
BIN
output/playwright/customer-callback-after-execute.png
Normal file
BIN
output/playwright/customer-callback-after-execute.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
@@ -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."
|
||||
@@ -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(),
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
[plan]
|
||||
id = "rrweb-customer-callback-draft"
|
||||
name = "客户回访工单登记"
|
||||
description = "客服打开待回访工单,选择客户满意,填写回访备注,并保存草稿。"
|
||||
version = "1.0.0"
|
||||
|
||||
[plan.trigger]
|
||||
kind = "manual"
|
||||
|
||||
[[plan.steps]]
|
||||
id = "open-target-page"
|
||||
title = "Open target page"
|
||||
description = "Call browser.open_url to open http://127.0.0.1:4187/customer-callback.html."
|
||||
skills = ["browser.open_url"]
|
||||
depends_on = []
|
||||
|
||||
[[plan.steps]]
|
||||
id = "run-recorded-flow"
|
||||
title = "Run recorded browser flow"
|
||||
description = "Execute the generated rrweb semantic trace through sgBrowser on 127.0.0.1. Default mode: execute."
|
||||
skills = ["rrweb-customer-callback-draft-execute"]
|
||||
depends_on = ["open-target-page"]
|
||||
requires_approval = true
|
||||
|
||||
[[plan.steps]]
|
||||
id = "save-result"
|
||||
title = "Save structured result"
|
||||
description = "Save the generated flow result as a structured JSON artifact."
|
||||
skills = ["json.save_result"]
|
||||
depends_on = ["run-recorded-flow"]
|
||||
Reference in New Issue
Block a user