278 lines
8.0 KiB
JavaScript
278 lines
8.0 KiB
JavaScript
const logEl = document.querySelector("#log");
|
|
const toolListEl = document.querySelector("#tool-list");
|
|
const nativeStatusEl = document.querySelector("#native-status");
|
|
const orderStateEl = document.querySelector("#order-state");
|
|
|
|
const nativeModelContext = window.navigator.modelContext;
|
|
const registry = new Map();
|
|
|
|
const demoOrders = {
|
|
"CHR-2026-0614": {
|
|
orderId: "CHR-2026-0614",
|
|
status: "delivered",
|
|
risk: "missing_accessory",
|
|
customerTier: "enterprise",
|
|
nextAction: "open_refund_review",
|
|
},
|
|
};
|
|
|
|
const modelContext = nativeModelContext ?? createMockModelContext();
|
|
|
|
nativeStatusEl.textContent = nativeModelContext
|
|
? "Native WebMCP detected"
|
|
: "Mock WebMCP runtime";
|
|
nativeStatusEl.className = `pill ${nativeModelContext ? "native" : "mock"}`;
|
|
|
|
registerDeclarativeForms();
|
|
registerImperativeTools();
|
|
renderTools();
|
|
log("runtime.ready", {
|
|
nativeWebMcp: Boolean(nativeModelContext),
|
|
registeredTools: Array.from(registry.keys()),
|
|
});
|
|
|
|
document.querySelector("#ticket-form").addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
const form = event.currentTarget;
|
|
const toolName = form.getAttribute("toolname");
|
|
const payload = Object.fromEntries(new FormData(form).entries());
|
|
const result = await runTool(toolName, payload);
|
|
log("form.submit", result);
|
|
});
|
|
|
|
document.querySelectorAll("[data-run-tool]").forEach((button) => {
|
|
button.addEventListener("click", async () => {
|
|
const name = button.getAttribute("data-run-tool");
|
|
const input = sampleInputFor(name);
|
|
const result = await runTool(name, input);
|
|
log(`tool.${name}`, result);
|
|
});
|
|
});
|
|
|
|
document.querySelector("#run-workflow").addEventListener("click", async () => {
|
|
log("agent.plan", {
|
|
task: "客户反馈缺少配件,请查询订单、判断是否进入退款审核,并生成回复草稿。",
|
|
});
|
|
const order = await runTool("lookup_order_status", { orderId: "CHR-2026-0614" });
|
|
const refund = await runTool("approve_refund_review", {
|
|
orderId: order.orderId,
|
|
reason: "missing_accessory",
|
|
});
|
|
const reply = await runTool("draft_customer_reply", {
|
|
email: "dev@example.com",
|
|
category: "refund",
|
|
orderId: order.orderId,
|
|
});
|
|
log("agent.workflow.complete", { order, refund, reply });
|
|
});
|
|
|
|
function createMockModelContext() {
|
|
return {
|
|
registerTool(tool) {
|
|
registry.set(tool.name, tool);
|
|
return Promise.resolve({ name: tool.name, mock: true });
|
|
},
|
|
};
|
|
}
|
|
|
|
function registerDeclarativeForms() {
|
|
document.querySelectorAll("form[toolname]").forEach((form) => {
|
|
const name = form.getAttribute("toolname");
|
|
const description = form.getAttribute("tooldescription") || `Submit ${name}`;
|
|
const inputSchema = schemaFromForm(form);
|
|
|
|
registerTool({
|
|
source: "declarative",
|
|
name,
|
|
description,
|
|
inputSchema,
|
|
execute: async (input) => {
|
|
const ticketId = `SUP-${Date.now().toString().slice(-6)}`;
|
|
return {
|
|
ticketId,
|
|
status: "created",
|
|
summary: `${input.category} issue for ${input.email}`,
|
|
visibleUpdate: "工单已创建,页面保留原有人类操作流程。",
|
|
};
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
function registerImperativeTools() {
|
|
registerTool({
|
|
source: "imperative",
|
|
name: "lookup_order_status",
|
|
description: "Look up order status, risk and recommended next action by order id.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
orderId: { type: "string", description: "Order id shown in the support workspace." },
|
|
},
|
|
required: ["orderId"],
|
|
},
|
|
execute: async ({ orderId }) => {
|
|
const order = demoOrders[orderId] ?? {
|
|
orderId,
|
|
status: "unknown",
|
|
risk: "not_found",
|
|
customerTier: "unknown",
|
|
nextAction: "ask_user_for_more_information",
|
|
};
|
|
orderStateEl.textContent = order.nextAction;
|
|
return order;
|
|
},
|
|
});
|
|
|
|
registerTool({
|
|
source: "imperative",
|
|
name: "approve_refund_review",
|
|
description: "Move an eligible order into refund review after checking business policy.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
orderId: { type: "string" },
|
|
reason: {
|
|
type: "string",
|
|
enum: ["missing_accessory", "damaged_item", "delivery_delay"],
|
|
},
|
|
},
|
|
required: ["orderId", "reason"],
|
|
},
|
|
execute: async ({ orderId, reason }) => {
|
|
orderStateEl.textContent = "已进入退款审核";
|
|
return {
|
|
orderId,
|
|
reason,
|
|
approvedForReview: true,
|
|
needsHumanConfirmation: true,
|
|
message: "敏感动作只进入审核,不直接退款。",
|
|
};
|
|
},
|
|
});
|
|
|
|
registerTool({
|
|
source: "imperative",
|
|
name: "draft_customer_reply",
|
|
description: "Draft a support reply based on customer issue category and order id.",
|
|
inputSchema: {
|
|
type: "object",
|
|
properties: {
|
|
email: { type: "string", format: "email" },
|
|
category: { type: "string", enum: ["refund", "delivery", "account"] },
|
|
orderId: { type: "string" },
|
|
},
|
|
required: ["email", "category", "orderId"],
|
|
},
|
|
execute: async ({ email, category, orderId }) => ({
|
|
to: email,
|
|
category,
|
|
orderId,
|
|
draft:
|
|
"我们已经定位到该订单的缺件问题,并将它提交到退款审核。审核完成前不会自动扣改账户,请等待人工确认。",
|
|
}),
|
|
});
|
|
}
|
|
|
|
function registerTool(tool) {
|
|
registry.set(tool.name, tool);
|
|
const nativePayload = {
|
|
name: tool.name,
|
|
description: tool.description,
|
|
inputSchema: tool.inputSchema,
|
|
execute: tool.execute,
|
|
};
|
|
|
|
try {
|
|
modelContext.registerTool(nativePayload);
|
|
} catch (error) {
|
|
log("register.error", { tool: tool.name, message: error.message });
|
|
}
|
|
}
|
|
|
|
async function runTool(name, input) {
|
|
const tool = registry.get(name);
|
|
if (!tool) {
|
|
throw new Error(`Unknown tool: ${name}`);
|
|
}
|
|
const result = await tool.execute(input);
|
|
return { tool: name, input, output: result };
|
|
}
|
|
|
|
function schemaFromForm(form) {
|
|
const properties = {};
|
|
const required = [];
|
|
form.querySelectorAll("input[name], select[name], textarea[name]").forEach((field) => {
|
|
const name = field.getAttribute("name");
|
|
properties[name] = {
|
|
type: inputTypeToJsonSchema(field),
|
|
description: field.closest("label")?.firstChild?.textContent?.trim() || name,
|
|
};
|
|
if (field.hasAttribute("required")) {
|
|
required.push(name);
|
|
}
|
|
});
|
|
return { type: "object", properties, required };
|
|
}
|
|
|
|
function inputTypeToJsonSchema(field) {
|
|
if (field.tagName === "SELECT" || field.tagName === "TEXTAREA") {
|
|
return "string";
|
|
}
|
|
if (field.type === "number" || field.type === "range") {
|
|
return "number";
|
|
}
|
|
if (field.type === "checkbox") {
|
|
return "boolean";
|
|
}
|
|
return "string";
|
|
}
|
|
|
|
function renderTools() {
|
|
toolListEl.innerHTML = "";
|
|
registry.forEach((tool) => {
|
|
const card = document.createElement("article");
|
|
card.className = "tool-card";
|
|
card.innerHTML = `
|
|
<h3>${tool.name}</h3>
|
|
<p>${tool.description}</p>
|
|
<span class="pill">${tool.source}</span>
|
|
<pre class="schema">${escapeHtml(JSON.stringify(tool.inputSchema, null, 2))}</pre>
|
|
`;
|
|
toolListEl.append(card);
|
|
});
|
|
}
|
|
|
|
function sampleInputFor(name) {
|
|
const samples = {
|
|
create_support_ticket: {
|
|
email: "dev@example.com",
|
|
category: "refund",
|
|
description: "订单 CHR-2026-0614 到货后发现配件缺失。",
|
|
},
|
|
lookup_order_status: { orderId: "CHR-2026-0614" },
|
|
approve_refund_review: { orderId: "CHR-2026-0614", reason: "missing_accessory" },
|
|
draft_customer_reply: {
|
|
email: "dev@example.com",
|
|
category: "refund",
|
|
orderId: "CHR-2026-0614",
|
|
},
|
|
};
|
|
return samples[name] ?? {};
|
|
}
|
|
|
|
function log(title, payload) {
|
|
const entry = document.createElement("div");
|
|
entry.className = "entry";
|
|
entry.innerHTML = `<strong>${title}</strong><pre>${escapeHtml(JSON.stringify(payload, null, 2))}</pre>`;
|
|
logEl.prepend(entry);
|
|
}
|
|
|
|
function escapeHtml(value) {
|
|
return String(value)
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """);
|
|
}
|