Files
what-new-in-chrome/public/demo/webmcp/app.js
T
2026-06-14 15:01:42 +08:00

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("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}