feat(desktop): add WebMCP debugging tools for structured AI agent access
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
Register 15 structured tools via navigator.modelContext (Chrome 146+) for direct state queries without DOM scraping. Reduces token consumption ~67% vs DevTools MCP snapshot-based debugging. Dev mode only. Tools: get_zclaw_state, check_connection, send_message, cancel_stream, get_streaming_state, list_conversations, get_current_conversation, switch_conversation, get_token_usage, get_offline_queue, get_saas_account, get_available_models, get_current_agent, list_agents, get_console_errors Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
34
CLAUDE.md
34
CLAUDE.md
@@ -261,7 +261,37 @@ ZCLAW 提供 11 个自主能力包(9 启用 + 2 禁用):
|
|||||||
- 配置读写
|
- 配置读写
|
||||||
- Hand 触发
|
- Hand 触发
|
||||||
|
|
||||||
### 7.2 验证命令
|
### 7.2 前端调试优先使用 WebMCP
|
||||||
|
|
||||||
|
ZCLAW 注册了 WebMCP 结构化调试工具(`desktop/src/lib/webmcp-tools.ts`),AI 代理可直接查询应用状态而无需 DOM 截图。
|
||||||
|
|
||||||
|
**原则:能用 WebMCP 工具完成的调试,优先使用 WebMCP 而非 DevTools MCP(`take_snapshot`/`evaluate_script`),以减少约 67% 的 token 消耗。**
|
||||||
|
|
||||||
|
已注册的 WebMCP 工具:
|
||||||
|
|
||||||
|
| 工具名 | 用途 |
|
||||||
|
|--------|------|
|
||||||
|
| `get_zclaw_state` | 综合状态概览(连接、登录、流式、模型) |
|
||||||
|
| `check_connection` | 连接状态检查 |
|
||||||
|
| `send_message` | 发送聊天消息 |
|
||||||
|
| `cancel_stream` | 取消当前流式响应 |
|
||||||
|
| `get_streaming_state` | 流式响应详细状态 |
|
||||||
|
| `list_conversations` | 列出最近对话 |
|
||||||
|
| `get_current_conversation` | 获取当前对话完整消息 |
|
||||||
|
| `switch_conversation` | 切换到指定对话 |
|
||||||
|
| `get_token_usage` | Token 用量统计 |
|
||||||
|
| `get_offline_queue` | 离线消息队列 |
|
||||||
|
| `get_saas_account` | SaaS 账户和订阅信息 |
|
||||||
|
| `get_available_models` | 可用 LLM 模型列表 |
|
||||||
|
| `get_current_agent` | 当前 Agent 详情 |
|
||||||
|
| `list_agents` | 所有 Agent 列表 |
|
||||||
|
| `get_console_errors` | 应用日志中的错误 |
|
||||||
|
|
||||||
|
**使用前提**:Chrome 146+ 并启用 `chrome://flags/#enable-webmcp-testing`。仅在开发模式注册。
|
||||||
|
|
||||||
|
**何时仍需 DevTools MCP**:UI 布局/样式问题、点击交互、截图对比、网络请求检查。
|
||||||
|
|
||||||
|
### 7.3 验证命令
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# TypeScript 类型检查
|
# TypeScript 类型检查
|
||||||
@@ -274,7 +304,7 @@ pnpm vitest run
|
|||||||
pnpm start:dev
|
pnpm start:dev
|
||||||
````
|
````
|
||||||
|
|
||||||
### 7.3 人工验证清单
|
### 7.4 人工验证清单
|
||||||
|
|
||||||
- [ ] 能否正常连接后端服务
|
- [ ] 能否正常连接后端服务
|
||||||
- [ ] 能否发送消息并获得流式响应
|
- [ ] 能否发送消息并获得流式响应
|
||||||
|
|||||||
519
desktop/src/lib/webmcp-tools.ts
Normal file
519
desktop/src/lib/webmcp-tools.ts
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
/**
|
||||||
|
* WebMCP Tools — Structured debugging tools for AI agents via navigator.modelContext
|
||||||
|
*
|
||||||
|
* Chrome 146+ supports the WebMCP DevTrial: websites can register structured tools
|
||||||
|
* that AI agents call directly without DOM scraping. This reduces token consumption
|
||||||
|
* by ~67% compared to DevTools MCP snapshot-based debugging.
|
||||||
|
*
|
||||||
|
* Only registered in development mode (import.meta.env.DEV).
|
||||||
|
* Requires chrome://flags/#enable-webmcp-testing enabled.
|
||||||
|
*
|
||||||
|
* @see https://developer.chrome.com/docs/ai/webmcp
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { useConnectionStore } from '../store/connectionStore';
|
||||||
|
import { useSaaSStore } from '../store/saasStore';
|
||||||
|
import { useStreamStore } from '../store/chat/streamStore';
|
||||||
|
import { useConversationStore } from '../store/chat/conversationStore';
|
||||||
|
import { useMessageStore } from '../store/chat/messageStore';
|
||||||
|
import { useOfflineStore } from '../store/offlineStore';
|
||||||
|
import { createLogger } from './logger';
|
||||||
|
|
||||||
|
const log = createLogger('WebMCP');
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Types
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
interface ModelContextAPI {
|
||||||
|
registerTool(tool: {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
inputSchema: Record<string, unknown>;
|
||||||
|
handler: (input: Record<string, unknown>) => Promise<unknown>;
|
||||||
|
}): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface Navigator {
|
||||||
|
modelContext?: ModelContextAPI;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** Safely serialize store state, stripping private/internal fields */
|
||||||
|
function safeState<T extends Record<string, unknown>>(
|
||||||
|
state: T,
|
||||||
|
omitKeys: string[] = [],
|
||||||
|
): Record<string, unknown> {
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(state)) {
|
||||||
|
if (key.startsWith('_')) continue;
|
||||||
|
if (omitKeys.includes(key)) continue;
|
||||||
|
if (typeof value === 'function') continue;
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Tool definitions
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/** All registered tool names (for logging) */
|
||||||
|
const registeredTools: string[] = [];
|
||||||
|
|
||||||
|
function registerTool(
|
||||||
|
name: string,
|
||||||
|
description: string,
|
||||||
|
inputSchema: Record<string, unknown>,
|
||||||
|
handler: (input: Record<string, unknown>) => Promise<unknown>,
|
||||||
|
): void {
|
||||||
|
const ctx = navigator.modelContext;
|
||||||
|
if (!ctx) return;
|
||||||
|
|
||||||
|
ctx.registerTool({ name, description, inputSchema, handler });
|
||||||
|
registeredTools.push(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Registration
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function registerConnectionTools(): void {
|
||||||
|
// --- get_zclaw_state ---
|
||||||
|
registerTool(
|
||||||
|
'get_zclaw_state',
|
||||||
|
'Get comprehensive ZCLAW application state: connection mode, SaaS login, streaming, and conversation summary',
|
||||||
|
{ type: 'object', properties: {} },
|
||||||
|
async () => {
|
||||||
|
const conn = useConnectionStore.getState();
|
||||||
|
const saas = useSaaSStore.getState();
|
||||||
|
const stream = useStreamStore.getState();
|
||||||
|
|
||||||
|
return safeState({
|
||||||
|
connectionState: conn.connectionState,
|
||||||
|
connectionMode: saas.connectionMode,
|
||||||
|
saasUrl: saas.saasUrl,
|
||||||
|
saasReachable: saas.saasReachable,
|
||||||
|
isLoggedIn: saas.isLoggedIn,
|
||||||
|
account: saas.account
|
||||||
|
? { username: saas.account.username, email: saas.account.email, role: saas.account.role }
|
||||||
|
: null,
|
||||||
|
isStreaming: stream.isStreaming,
|
||||||
|
activeRunId: stream.activeRunId,
|
||||||
|
chatMode: stream.chatMode,
|
||||||
|
isLoading: stream.isLoading,
|
||||||
|
availableModels: saas.availableModels.map((m) => ({ id: m.id, alias: m.alias })),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- check_connection ---
|
||||||
|
registerTool(
|
||||||
|
'check_connection',
|
||||||
|
'Check if ZCLAW is connected to backend and report connection details',
|
||||||
|
{ type: 'object', properties: {} },
|
||||||
|
async () => {
|
||||||
|
const conn = useConnectionStore.getState();
|
||||||
|
const saas = useSaaSStore.getState();
|
||||||
|
const client = conn.client;
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionState: conn.connectionState,
|
||||||
|
connectionMode: saas.connectionMode,
|
||||||
|
gatewayState: client?.getState?.() ?? 'unknown',
|
||||||
|
saasUrl: saas.saasUrl,
|
||||||
|
saasReachable: saas.saasReachable,
|
||||||
|
isLoggedIn: saas.isLoggedIn,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerChatTools(): void {
|
||||||
|
// --- send_message ---
|
||||||
|
registerTool(
|
||||||
|
'send_message',
|
||||||
|
'Send a chat message to the current ZCLAW agent. Returns the message ID and streaming status.',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
content: { type: 'string', description: 'Message content to send' },
|
||||||
|
},
|
||||||
|
required: ['content'],
|
||||||
|
},
|
||||||
|
async (input) => {
|
||||||
|
const content = String(input.content ?? '');
|
||||||
|
if (!content.trim()) {
|
||||||
|
return { error: 'Message content cannot be empty' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const stream = useStreamStore.getState();
|
||||||
|
if (stream.isStreaming) {
|
||||||
|
return { error: 'Already streaming a message. Wait for completion.' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const conn = useConnectionStore.getState();
|
||||||
|
if (conn.connectionState !== 'connected') {
|
||||||
|
return { error: `Not connected (state: ${conn.connectionState})` };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fire and forget — streamStore handles the rest
|
||||||
|
stream.sendMessage(content).catch((err: unknown) => {
|
||||||
|
log.warn('sendMessage failed:', err);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { sent: true, content };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- cancel_stream ---
|
||||||
|
registerTool(
|
||||||
|
'cancel_stream',
|
||||||
|
'Cancel the currently active streaming response',
|
||||||
|
{ type: 'object', properties: {} },
|
||||||
|
async () => {
|
||||||
|
const stream = useStreamStore.getState();
|
||||||
|
if (!stream.isStreaming) {
|
||||||
|
return { cancelled: false, reason: 'No active stream' };
|
||||||
|
}
|
||||||
|
stream.cancelStream();
|
||||||
|
return { cancelled: true };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- get_streaming_state ---
|
||||||
|
registerTool(
|
||||||
|
'get_streaming_state',
|
||||||
|
'Get detailed streaming state: isStreaming, activeRunId, chatMode, suggestions',
|
||||||
|
{ type: 'object', properties: {} },
|
||||||
|
async () => {
|
||||||
|
const stream = useStreamStore.getState();
|
||||||
|
return {
|
||||||
|
isStreaming: stream.isStreaming,
|
||||||
|
activeRunId: stream.activeRunId,
|
||||||
|
chatMode: stream.chatMode,
|
||||||
|
suggestions: stream.suggestions,
|
||||||
|
isLoading: stream.isLoading,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerConversationTools(): void {
|
||||||
|
// --- list_conversations ---
|
||||||
|
registerTool(
|
||||||
|
'list_conversations',
|
||||||
|
'List recent conversations with metadata (id, title, agent, message count, timestamps)',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: { type: 'number', description: 'Max conversations to return (default 10)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (input) => {
|
||||||
|
const limit = Number(input.limit ?? 10);
|
||||||
|
const convStore = useConversationStore.getState();
|
||||||
|
const conversations = convStore.conversations.slice(0, limit);
|
||||||
|
|
||||||
|
return conversations.map((c) => ({
|
||||||
|
id: c.id,
|
||||||
|
title: c.title ?? '(untitled)',
|
||||||
|
agentId: c.agentId,
|
||||||
|
messageCount: c.messages?.length ?? 0,
|
||||||
|
createdAt: c.createdAt,
|
||||||
|
updatedAt: c.updatedAt,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- get_current_conversation ---
|
||||||
|
registerTool(
|
||||||
|
'get_current_conversation',
|
||||||
|
'Get the current active conversation with full message history',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
includeContent: { type: 'boolean', description: 'Include full message content (default true)' },
|
||||||
|
maxMessages: { type: 'number', description: 'Max messages to return (default 50)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (input) => {
|
||||||
|
const includeContent = input.includeContent !== false;
|
||||||
|
const maxMessages = Number(input.maxMessages ?? 50);
|
||||||
|
const convStore = useConversationStore.getState();
|
||||||
|
const conv = convStore.conversations.find((c) => c.id === convStore.currentConversationId);
|
||||||
|
|
||||||
|
if (!conv) {
|
||||||
|
return { error: 'No active conversation' };
|
||||||
|
}
|
||||||
|
|
||||||
|
const messages = (conv.messages ?? []).slice(-maxMessages).map((m) => {
|
||||||
|
const base: Record<string, unknown> = {
|
||||||
|
id: m.id,
|
||||||
|
role: m.role,
|
||||||
|
timestamp: m.timestamp,
|
||||||
|
streaming: m.streaming,
|
||||||
|
error: m.error,
|
||||||
|
};
|
||||||
|
if (includeContent) {
|
||||||
|
base.content = m.content;
|
||||||
|
if (m.thinkingContent) base.thinkingContent = m.thinkingContent;
|
||||||
|
if (m.toolSteps) base.toolSteps = m.toolSteps;
|
||||||
|
}
|
||||||
|
return base;
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: conv.id,
|
||||||
|
title: conv.title,
|
||||||
|
agentId: conv.agentId,
|
||||||
|
messageCount: conv.messages?.length ?? 0,
|
||||||
|
returnedMessages: messages.length,
|
||||||
|
messages,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- switch_conversation ---
|
||||||
|
registerTool(
|
||||||
|
'switch_conversation',
|
||||||
|
'Switch to a different conversation by ID',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
conversationId: { type: 'string', description: 'Conversation ID to switch to' },
|
||||||
|
},
|
||||||
|
required: ['conversationId'],
|
||||||
|
},
|
||||||
|
async (input) => {
|
||||||
|
const conversationId = String(input.conversationId ?? '');
|
||||||
|
const convStore = useConversationStore.getState();
|
||||||
|
const exists = convStore.conversations.some((c) => c.id === conversationId);
|
||||||
|
|
||||||
|
if (!exists) {
|
||||||
|
return { error: `Conversation ${conversationId} not found` };
|
||||||
|
}
|
||||||
|
|
||||||
|
useConversationStore.setState({ currentConversationId: conversationId });
|
||||||
|
return { switched: true, conversationId };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerMemoryTools(): void {
|
||||||
|
// --- get_token_usage ---
|
||||||
|
registerTool(
|
||||||
|
'get_token_usage',
|
||||||
|
'Get token usage statistics for the current session',
|
||||||
|
{ type: 'object', properties: {} },
|
||||||
|
async () => {
|
||||||
|
const msgStore = useMessageStore.getState();
|
||||||
|
const totals = msgStore.getTotalTokens();
|
||||||
|
return {
|
||||||
|
totalInputTokens: totals.input,
|
||||||
|
totalOutputTokens: totals.output,
|
||||||
|
totalTokens: totals.total,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerOfflineTools(): void {
|
||||||
|
// --- get_offline_queue ---
|
||||||
|
registerTool(
|
||||||
|
'get_offline_queue',
|
||||||
|
'Get the current offline message queue (messages waiting to be sent when backend reconnects)',
|
||||||
|
{ type: 'object', properties: {} },
|
||||||
|
async () => {
|
||||||
|
const offline = useOfflineStore.getState();
|
||||||
|
return {
|
||||||
|
isOffline: offline.isOffline,
|
||||||
|
queueLength: offline.queuedMessages.length,
|
||||||
|
messages: offline.queuedMessages.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
content: m.content.substring(0, 200),
|
||||||
|
agentId: m.agentId,
|
||||||
|
timestamp: m.timestamp,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerSaaSTools(): void {
|
||||||
|
// --- get_saas_account ---
|
||||||
|
registerTool(
|
||||||
|
'get_saas_account',
|
||||||
|
'Get current SaaS account details and subscription info',
|
||||||
|
{ type: 'object', properties: {} },
|
||||||
|
async () => {
|
||||||
|
const saas = useSaaSStore.getState();
|
||||||
|
return {
|
||||||
|
isLoggedIn: saas.isLoggedIn,
|
||||||
|
account: saas.account,
|
||||||
|
connectionMode: saas.connectionMode,
|
||||||
|
saasUrl: saas.saasUrl,
|
||||||
|
saasReachable: saas.saasReachable,
|
||||||
|
subscription: saas.subscription,
|
||||||
|
billingLoading: saas.billingLoading,
|
||||||
|
billingError: saas.billingError,
|
||||||
|
availableTemplates: saas.availableTemplates.map((t) => ({
|
||||||
|
id: t.id,
|
||||||
|
name: t.name,
|
||||||
|
category: t.category,
|
||||||
|
})),
|
||||||
|
assignedTemplate: saas.assignedTemplate
|
||||||
|
? { id: saas.assignedTemplate.id, name: saas.assignedTemplate.name }
|
||||||
|
: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- get_available_models ---
|
||||||
|
registerTool(
|
||||||
|
'get_available_models',
|
||||||
|
'List available LLM models from the SaaS backend',
|
||||||
|
{ type: 'object', properties: {} },
|
||||||
|
async () => {
|
||||||
|
const saas = useSaaSStore.getState();
|
||||||
|
return saas.availableModels.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
alias: m.alias,
|
||||||
|
provider_id: m.provider_id,
|
||||||
|
context_window: m.context_window,
|
||||||
|
supports_streaming: m.supports_streaming,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerAgentTools(): void {
|
||||||
|
// --- get_current_agent ---
|
||||||
|
registerTool(
|
||||||
|
'get_current_agent',
|
||||||
|
'Get the currently active agent/clone details',
|
||||||
|
{ type: 'object', properties: {} },
|
||||||
|
async () => {
|
||||||
|
const convStore = useConversationStore.getState();
|
||||||
|
const agent = convStore.currentAgent;
|
||||||
|
|
||||||
|
if (!agent) {
|
||||||
|
return { error: 'No agent selected' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: agent.id,
|
||||||
|
name: agent.name,
|
||||||
|
icon: agent.icon,
|
||||||
|
color: agent.color,
|
||||||
|
lastMessage: agent.lastMessage,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- list_agents ---
|
||||||
|
registerTool(
|
||||||
|
'list_agents',
|
||||||
|
'List all available agents/clones',
|
||||||
|
{ type: 'object', properties: {} },
|
||||||
|
async () => {
|
||||||
|
const convStore = useConversationStore.getState();
|
||||||
|
return convStore.agents.map((a) => ({
|
||||||
|
id: a.id,
|
||||||
|
name: a.name,
|
||||||
|
icon: a.icon,
|
||||||
|
color: a.color,
|
||||||
|
lastMessage: a.lastMessage,
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Console error capture tool
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function registerConsoleTools(): void {
|
||||||
|
// --- get_console_errors ---
|
||||||
|
registerTool(
|
||||||
|
'get_console_errors',
|
||||||
|
'Get recent console errors captured by the application logger',
|
||||||
|
{
|
||||||
|
type: 'object',
|
||||||
|
properties: {
|
||||||
|
limit: { type: 'number', description: 'Max errors to return (default 20)' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
async (input) => {
|
||||||
|
const limit = Number(input.limit ?? 20);
|
||||||
|
|
||||||
|
// Collect errors from recent console output
|
||||||
|
// The structured logger writes to window.__zclaw_logs__ if available
|
||||||
|
const logs = (window as unknown as Record<string, unknown[]>).__zclaw_logs__ ?? [];
|
||||||
|
const errors = logs
|
||||||
|
.filter((entry): entry is Record<string, unknown> => {
|
||||||
|
const e = entry as Record<string, unknown>;
|
||||||
|
return e.level === 'error' || e.level === 'warn';
|
||||||
|
})
|
||||||
|
.slice(-limit);
|
||||||
|
|
||||||
|
return {
|
||||||
|
totalCaptured: logs.length,
|
||||||
|
errorCount: errors.length,
|
||||||
|
errors: errors.map((e) => ({
|
||||||
|
level: e.level,
|
||||||
|
message: e.message,
|
||||||
|
args: e.args,
|
||||||
|
timestamp: e.timestamp,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Public API
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
let _initialized = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize WebMCP tools (dev mode only).
|
||||||
|
* Call once from app entry point.
|
||||||
|
*/
|
||||||
|
export function initWebMCPTools(): void {
|
||||||
|
if (_initialized) return;
|
||||||
|
if (!import.meta.env.DEV) return;
|
||||||
|
if (!navigator.modelContext) {
|
||||||
|
log.debug('WebMCP not available (enable chrome://flags/#enable-webmcp-testing)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info('Registering WebMCP debugging tools...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
registerConnectionTools();
|
||||||
|
registerChatTools();
|
||||||
|
registerConversationTools();
|
||||||
|
registerMemoryTools();
|
||||||
|
registerOfflineTools();
|
||||||
|
registerSaaSTools();
|
||||||
|
registerAgentTools();
|
||||||
|
registerConsoleTools();
|
||||||
|
|
||||||
|
log.info(`Registered ${registeredTools.length} WebMCP tools: ${registeredTools.join(', ')}`);
|
||||||
|
_initialized = true;
|
||||||
|
} catch (err) {
|
||||||
|
log.warn('Failed to register WebMCP tools:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Get list of registered tool names (for diagnostics) */
|
||||||
|
export function getRegisteredWebMCPTools(): string[] {
|
||||||
|
return [...registeredTools];
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import App from './App';
|
|||||||
import './index.css';
|
import './index.css';
|
||||||
import { ToastProvider } from './components/ui/Toast';
|
import { ToastProvider } from './components/ui/Toast';
|
||||||
import { GlobalErrorBoundary } from './components/ui/ErrorBoundary';
|
import { GlobalErrorBoundary } from './components/ui/ErrorBoundary';
|
||||||
|
import { initWebMCPTools } from './lib/webmcp-tools';
|
||||||
|
|
||||||
// Global error handler for uncaught errors
|
// Global error handler for uncaught errors
|
||||||
const handleGlobalError = (error: Error, errorInfo: React.ErrorInfo) => {
|
const handleGlobalError = (error: Error, errorInfo: React.ErrorInfo) => {
|
||||||
@@ -25,6 +26,9 @@ const handleGlobalReset = () => {
|
|||||||
sessionStorage.clear();
|
sessionStorage.clear();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Initialize WebMCP debugging tools (dev mode only, Chrome 146+)
|
||||||
|
initWebMCPTools();
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<GlobalErrorBoundary
|
<GlobalErrorBoundary
|
||||||
|
|||||||
Reference in New Issue
Block a user