MSG Chain AI Agent 前端与 dApp 集成指南
本指南面向前端开发者和 dApp 构建者,详细讲解如何将 MSG Chain 上的 AI Agent 能力集成到 Web 和移动端应用中。
链 ID:
msg-chain-1地址前缀:
msg⚠️ No-Go Disclaimer: MSGChain 主网裁决为 No-Go。本文件所有内容反映的是开发阶段的技术设计,不代表主网未独立核验上线状态。生产部署状态请以白皮书为准:https://msgchain.org/whitepaper/
目录
1. 概述
1.1 为什么前端集成对 AI Agent 至关重要
AI Agent 是 MSG Chain 上的自主执行程序——它们可以分析数据、执行交易、生成内容,甚至与其他 Agent 协作。然而,没有良好的前端集成,Agent 的能力将被封锁在区块链的底层调用中。前端是用户与 Agent 之间的桥梁,承担以下职责:
- 身份层: 通过钱包连接将用户链上身份(
msg1...)映射到 Agent 会话 - 交互层: 提供直观的聊天、表单、仪表板等 UI,让用户自然地与 Agent 沟通
- 交易层: 自动构造、签名并广播交易,使 Agent 能够代表用户执行链上操作
- 可视化层: 展示 Agent 的执行结果、任务进度、链上数据和实时事件流
1.2 系统架构
+-----------------------------------------------------------+
| 前端应用 (Web/Mobile) |
| +-----------+ +-----------+ +-------------------+ |
| | Wallet UI | | Agent UI | | Dashboard / Chat | |
| +-----+-----+ +-----+-----+ +---------+---------+ |
| | | | |
| +-----+--------------+-------------------+-----------+ |
| | CosmJS / @msgchain/* SDK | |
| +-----+----------------------------------------+-----+ |
+-------+----------------------------------------+----------+
| |
v v
+--------------+ +------------------+
| Keplr/Leap | | RPC / REST API |
| Wallet | | (Tendermint) |
+-------+-------+ +----------+--------+
| |
v v
+-------------------------------------------------------+
| MSG Chain 区块链网络 |
| +------------------+ +---------------------------+ |
| | Agent 注册合约 | | Agent 运行时模块 | |
| | (Registry) | | (Runtime / VM) | |
| +------------------+ +---------------------------+ |
| | A2A 消息协议 | | Agent 状态存储 | |
| | (Agent-to-Agent) | | (State / KV Store) | |
| +------------------+ +---------------------------+ |
+-------------------------------------------------------+
1.3 技术栈推荐
| 层级 | 技术选型 |
|---|---|
| 框架 | React 18+ / Next.js 14+ |
| 链交互 | CosmJS v0.32+ / @msgchain/client |
| 钱包 | Keplr / Leap / @msgchain/wallet-adapter |
| 状态管理 | Zustand / Jotai |
| 实时通信 | WebSocket (Tendermint RPC) / Server-Sent Events |
| UI 组件 | Radix UI / shadcn/ui / Ant Design |
| 移动端 | React Native / Flutter |
| 构建工具 | Vite / Turbopack |
1.4 前提条件
npm install cosmjs @msgchain/client @msgchain/wallet-adapter
1.5 网络端点
const MSG_CHAIN_CONFIG = {
chainId: 'msg-chain-1',
rpcEndpoint: 'https://rpc.msgchain.org',
restEndpoint: 'https://api.msgchain.org',
wsEndpoint: 'wss://rpc.msgchain.org/websocket',
prefix: 'msg',
denom: 'uaimp',
gasPrice: '1000000000attoMSG',
};
2. 钱包连接与身份认证
2.1 初始化 CosmJS 客户端
import { SigningStargateClient } from '@cosmjs/stargate';
import { GasPrice } from '@cosmjs/stargate';
import { OfflineSigner } from '@cosmjs/proto-signing';
const config = {
rpcEndpoint: 'https://rpc.msgchain.org',
prefix: 'msg',
gasPrice: GasPrice.fromString('1000000000attoMSG'),
};
let cosmClient: SigningStargateClient | null = null;
export const getCosmClient = async (
signer: OfflineSigner
): Promise<SigningStargateClient> => {
if (cosmClient) return cosmClient;
cosmClient = await SigningStargateClient.connectWithSigner(
config.rpcEndpoint,
signer,
{ gasPrice: config.gasPrice }
);
return cosmClient;
};
export const disconnectClient = () => {
if (cosmClient) {
cosmClient.disconnect();
cosmClient = null;
}
};
2.2 Keplr 钱包连接
import { useState, useCallback, useEffect } from 'react';
interface KeplrState {
address: string;
isConnected: boolean;
isInstalled: boolean;
chainId: string;
}
type KeplrWindow = Window & {
keplr?: {
enable: (chainId: string) => Promise<void>;
getOfflineSigner: (chainId: string) => OfflineSigner;
suggestChain?: (config: unknown) => Promise<void>;
getKey: (chainId: string) => Promise<{
name: string;
algo: string;
pubKey: Uint8Array;
address: Uint8Array;
bech32Address: string;
}>;
};
leap?: KeplrWindow['keplr'];
};
export const useKeplr = () => {
const [state, setState] = useState<KeplrState>({
address: '',
isConnected: false,
isInstalled: false,
chainId: 'msg-chain-1',
});
useEffect(() => {
const hasKeplr = !!(window as KeplrWindow).keplr;
const hasLeap = !!(window as KeplrWindow).leap;
setState((prev) => ({ ...prev, isInstalled: hasKeplr || hasLeap }));
}, []);
const getWallet = useCallback((): KeplrWindow['keplr'] => {
return (window as KeplrWindow).keplr || (window as KeplrWindow).leap;
}, []);
const suggestChain = useCallback(async () => {
const wallet = getWallet();
if (!wallet?.suggestChain) return;
await wallet.suggestChain({
chainId: 'msg-chain-1',
chainName: 'MSG Chain',
rpc: 'https://rpc.msgchain.org',
rest: 'https://api.msgchain.org',
bip44: { coinType: 118 },
bech32Config: {
bech32PrefixAccAddr: 'msg',
bech32PrefixAccPub: 'msgpub',
bech32PrefixValAddr: 'msgvaloper',
bech32PrefixValPub: 'msgvaloperpub',
bech32PrefixConsAddr: 'msgvalcons',
bech32PrefixConsPub: 'msgvalconspub',
},
currencies: [{
coinDenom: 'AIMP',
coinMinimalDenom: 'uaimp',
coinDecimals: 6,
}],
feeCurrencies: [{
coinDenom: 'AIMP',
coinMinimalDenom: 'uaimp',
coinDecimals: 6,
gasPriceStep: { low: 500000000, average: 1000000000, high: 2000000000 },
}],
stakeCurrency: 'uaimp',
features: ['stargate', 'no-legacy-stdTx', 'ibc-go'],
});
}, [getWallet]);
const connect = useCallback(async () => {
try {
const wallet = getWallet();
if (!wallet) throw new Error('请安装 Keplr 或 Leap 钱包');
await suggestChain();
await wallet.enable('msg-chain-1');
const offlineSigner = wallet.getOfflineSigner('msg-chain-1');
const accounts = await offlineSigner.getAccounts();
setState({
address: accounts[0].address,
isConnected: true,
isInstalled: true,
chainId: 'msg-chain-1',
});
return accounts[0].address;
} catch (err) {
console.error('钱包连接失败:', err);
throw err;
}
}, [getWallet, suggestChain]);
const disconnect = useCallback(() => {
setState({
address: '',
isConnected: false,
isInstalled: state.isInstalled,
chainId: 'msg-chain-1',
});
disconnectClient();
}, [state.isInstalled]);
return { ...state, connect, disconnect, getWallet };
};
2.3 钱包连接组件
import React, { useState } from 'react';
import { useKeplr } from '../hooks/useKeplr';
const truncateAddress = (address: string): string => {
return `${address.slice(0, 8)}...${address.slice(-6)}`;
};
export const WalletConnect: React.FC = () => {
const { address, isConnected, isInstalled, connect, disconnect } = useKeplr();
const [connecting, setConnecting] = useState(false);
const handleConnect = async () => {
if (!isInstalled) {
window.open('https://www.keplr.app/download', '_blank');
return;
}
setConnecting(true);
try {
await connect();
} catch (err) {
console.error(err);
} finally {
setConnecting(false);
}
};
if (isConnected && address) {
return (
<div className="wallet-connected">
<div className="wallet-info">
<span className="wallet-indicator" />
<span className="wallet-address">{truncateAddress(address)}</span>
<span className="wallet-balance" id="wallet-balance" />
</div>
<button className="wallet-disconnect-btn" onClick={disconnect} type="button">
断开连接
</button>
</div>
);
}
return (
<button className="wallet-connect-btn" onClick={handleConnect} disabled={connecting} type="button">
{connecting ? '连接中...' : isInstalled ? '连接钱包' : '安装 Keplr'}
</button>
);
};
2.4 Agent 身份绑定
import { SigningStargateClient } from '@cosmjs/stargate';
import { sha256 } from '@cosmjs/crypto';
import { toHex } from '@cosmjs/encoding';
export interface AgentDidBinding {
walletAddress: string;
agentDid: string;
signature: string;
timestamp: number;
}
export const createAgentIdentity = async (
client: SigningStargateClient,
walletAddress: string,
agentDid: string
): Promise<AgentDidBinding> => {
const payload = JSON.stringify({
wallet: walletAddress,
agent: agentDid,
action: 'bind',
timestamp: Date.now(),
});
const signDoc = {
chain_id: 'msg-chain-1',
account_number: '0',
sequence: '0',
fee: { amount: [], gas: '0' },
msgs: [],
memo: payload,
};
const { signature } = await client.sign(walletAddress, [signDoc] as any);
return {
walletAddress,
agentDid,
signature: toHex(sha256(new TextEncoder().encode(JSON.stringify(signature.data ?? signature)))),
timestamp: Date.now(),
};
};
export const verifyAgentBinding = async (
client: SigningStargateClient,
binding: AgentDidBinding
): Promise<boolean> => {
try {
const account = await client.getAccount(binding.walletAddress);
return account !== null && account.address === binding.walletAddress;
} catch {
return false;
}
};
2.5 会话管理
export interface AgentSession {
sessionId: string;
walletAddress: string;
agentId: string;
createdAt: number;
expiresAt: number;
permissions: SessionPermission[];
}
export type SessionPermission =
| 'agent:chat'
| 'agent:execute'
| 'agent:payment'
| 'agent:history'
| 'agent:admin';
const SESSION_DURATION_MS = 24 * 60 * 60 * 1000;
const generateSessionId = (): string => {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return Array.from(bytes).map((b) => b.toString(16).padStart(2, '0')).join('');
};
export const createSession = (
walletAddress: string,
agentId: string,
permissions: SessionPermission[] = ['agent:chat', 'agent:execute']
): AgentSession => {
const now = Date.now();
return {
sessionId: generateSessionId(),
walletAddress,
agentId,
createdAt: now,
expiresAt: now + SESSION_DURATION_MS,
permissions,
};
};
export const isSessionValid = (session: AgentSession): boolean => {
return Date.now() < session.expiresAt;
};
export const hasPermission = (session: AgentSession, permission: SessionPermission): boolean => {
return session.permissions.includes(permission);
};
const SESSION_KEY = 'msg_agent_session';
export const persistSession = (session: AgentSession): void => {
localStorage.setItem(SESSION_KEY, JSON.stringify(session));
};
export const loadSession = (): AgentSession | null => {
try {
const data = localStorage.getItem(SESSION_KEY);
if (!data) return null;
const session = JSON.parse(data) as AgentSession;
return isSessionValid(session) ? session : null;
} catch {
return null;
}
};
export const clearSession = (): void => {
localStorage.removeItem(SESSION_KEY);
};
2.6 全局 Auth Provider
import React, { createContext, useContext, useState, useCallback, useEffect, ReactNode } from 'react';
import { useKeplr } from '../hooks/useKeplr';
import { SigningStargateClient } from '@cosmjs/stargate';
import { getCosmClient, disconnectClient } from '../lib/cosmClient';
import { AgentSession, createSession, persistSession, loadSession, clearSession } from '../lib/session';
interface AuthContextValue {
address: string | null;
isConnected: boolean;
isAuthenticated: boolean;
session: AgentSession | null;
cosmClient: SigningStargateClient | null;
connect: () => Promise<string>;
disconnect: () => void;
bindSession: (agentId: string) => Promise<AgentSession>;
}
const AuthContext = createContext<AuthContextValue | null>(null);
export const AuthProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
const keplr = useKeplr();
const [session, setSession] = useState<AgentSession | null>(null);
const [cosmClient, setCosmClient] = useState<SigningStargateClient | null>(null);
useEffect(() => {
const saved = loadSession();
if (saved) setSession(saved);
}, []);
useEffect(() => {
if (keplr.isConnected && keplr.address) {
const wallet = keplr.getWallet();
if (wallet) {
const signer = wallet.getOfflineSigner('msg-chain-1');
getCosmClient(signer).then(setCosmClient);
}
}
}, [keplr.isConnected, keplr.address, keplr.getWallet]);
const connect = useCallback(async () => {
const address = await keplr.connect();
return address;
}, [keplr]);
const disconnect = useCallback(() => {
keplr.disconnect();
setSession(null);
clearSession();
disconnectClient();
setCosmClient(null);
}, [keplr]);
const bindSession = useCallback(async (agentId: string): Promise<AgentSession> => {
if (!keplr.address) throw new Error('钱包未连接');
const newSession = createSession(keplr.address, agentId);
persistSession(newSession);
setSession(newSession);
return newSession;
}, [keplr.address]);
return (
<AuthContext.Provider value={{
address: keplr.address ?? null,
isConnected: keplr.isConnected,
isAuthenticated: keplr.isConnected && session !== null,
session,
cosmClient,
connect,
disconnect,
bindSession,
}}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = (): AuthContextValue => {
const ctx = useContext(AuthContext);
if (!ctx) throw new Error('useAuth must be used within AuthProvider');
return ctx;
};
3. Agent 查询与展示
3.1 Agent 注册表客户端
import { SigningStargateClient } from '@cosmjs/stargate';
import { toUtf8, fromUtf8 } from '@cosmjs/encoding';
export interface AgentInfo {
id: string;
name: string;
description: string;
avatar: string;
owner: string;
capabilities: string[];
reputation: number;
price: string;
priceDenom: string;
status: 'active' | 'paused' | 'offline';
totalTasks: number;
successRate: number;
createdAt: number;
tags: string[];
}
export interface AgentSearchParams {
capability?: string;
minReputation?: number;
maxPrice?: string;
status?: AgentInfo['status'];
query?: string;
limit?: number;
offset?: number;
}
export interface AgentRegistryQuery {
agents: AgentInfo[];
total: number;
hasMore: boolean;
}
const REGISTRY_CONTRACT_ADDRESS = 'msg1registrycontractaddressxxxxxxxxxxxxxxxxxx';
export class AgentRegistryClient {
private client: SigningStargateClient;
constructor(client: SigningStargateClient) {
this.client = client;
}
async getAgent(agentId: string): Promise<AgentInfo | null> {
try {
const query = { get_agent: { agent_id: agentId } };
return await this.client.queryContractSmart(REGISTRY_CONTRACT_ADDRESS, query) as AgentInfo;
} catch {
return null;
}
}
async searchByCapability(params: AgentSearchParams): Promise<AgentRegistryQuery> {
const query = {
search_agents: {
capability: params.capability,
min_reputation: params.minReputation,
max_price: params.maxPrice,
status: params.status,
query: params.query,
limit: params.limit ?? 20,
offset: params.offset ?? 0,
},
};
const response = await this.client.queryContractSmart(REGISTRY_CONTRACT_ADDRESS, query) as { agents: AgentInfo[]; total: number };
return {
agents: response.agents,
total: response.total,
hasMore: (params.offset ?? 0) + (params.limit ?? 20) < response.total,
};
}
async getTopAgents(limit = 10): Promise<AgentInfo[]> {
const response = await this.client.queryContractSmart(REGISTRY_CONTRACT_ADDRESS, { get_top_agents: { limit } }) as { agents: AgentInfo[] };
return response.agents;
}
async getAgentStats(agentId: string): Promise<{
totalTasks: number;
successRate: number;
avgResponseTime: number;
totalEarned: string;
}> {
return this.client.queryContractSmart(REGISTRY_CONTRACT_ADDRESS, { get_agent_stats: { agent_id: agentId } }) as Promise<any>;
}
}
3.2 Agent 列表组件
import React, { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../providers/AuthProvider';
import { AgentRegistryClient, AgentInfo, AgentSearchParams } from '../lib/agentRegistry';
interface AgentListProps {
onSelectAgent: (agent: AgentInfo) => void;
filters?: Partial<AgentSearchParams>;
}
export const AgentList: React.FC<AgentListProps> = ({ onSelectAgent, filters }) => {
const { cosmClient } = useAuth();
const [agents, setAgents] = useState<AgentInfo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [offset, setOffset] = useState(0);
const [hasMore, setHasMore] = useState(false);
const [searchQuery, setSearchQuery] = useState('');
const fetchAgents = useCallback(async (resetOffset = false) => {
if (!cosmClient) return;
setLoading(true);
setError(null);
try {
const registry = new AgentRegistryClient(cosmClient);
const currentOffset = resetOffset ? 0 : offset;
const result = await registry.searchByCapability({
...filters,
query: searchQuery || undefined,
limit: 20,
offset: currentOffset,
});
if (resetOffset) {
setAgents(result.agents);
} else {
setAgents((prev) => [...prev, ...result.agents]);
}
setHasMore(result.hasMore);
setOffset(currentOffset + 20);
} catch (err) {
setError('获取 Agent 列表失败,请检查网络连接');
console.error(err);
} finally {
setLoading(false);
}
}, [cosmClient, filters, offset, searchQuery]);
useEffect(() => { fetchAgents(true); }, [cosmClient, filters]);
const handleSearch = (e: React.FormEvent) => {
e.preventDefault();
setOffset(0);
fetchAgents(true);
};
if (error) {
return (
<div className="agent-list-error">
<p>{error}</p>
<button onClick={() => fetchAgents(true)} type="button">重试</button>
</div>
);
}
return (
<div className="agent-list-container">
<form className="agent-search-bar" onSubmit={handleSearch}>
<input type="text" placeholder="搜索 Agent 名称、能力或描述..." value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
<button type="submit">搜索</button>
</form>
{loading && agents.length === 0 ? (
<div className="agent-list-loading">
{Array.from({ length: 6 }).map((_, i) => <div key={i} className="agent-card-skeleton" />)}
</div>
) : agents.length === 0 ? (
<div className="agent-list-empty">
<p>未找到匹配的 Agent</p>
<p className="hint">尝试修改搜索条件或筛选器</p>
</div>
) : (
<>
<div className="agent-grid">
{agents.map((agent) => <AgentCard key={agent.id} agent={agent} onSelect={() => onSelectAgent(agent)} />)}
</div>
{hasMore && (
<div className="agent-list-footer">
<button onClick={() => fetchAgents(false)} disabled={loading} type="button">
{loading ? '加载中...' : '加载更多'}
</button>
<span className="agent-total">共 {agents.length}+ 个 Agent</span>
</div>
)}
</>
)}
</div>
);
};
3.3 Agent 卡片组件
import React from 'react';
import { AgentInfo } from '../lib/agentRegistry';
interface AgentCardProps {
agent: AgentInfo;
onSelect: () => void;
compact?: boolean;
}
const STATUS_LABELS: Record<AgentInfo['status'], string> = {
active: '在线',
paused: '暂停',
offline: '离线',
};
const STATUS_COLORS: Record<AgentInfo['status'], string> = {
active: '#22c55e',
paused: '#f59e0b',
offline: '#6b7280',
};
const formatPrice = (price: string): string => {
const num = parseInt(price, 10);
if (num >= 1_000_000) return (num / 1_000_000).toFixed(1) + 'M AIMP';
if (num >= 1_000) return (num / 1_000).toFixed(1) + 'K AIMP';
return num + ' AIMP';
};
const formatAddress = (address: string): string => {
return address.slice(0, 6) + '...' + address.slice(-4);
};
export const AgentCard: React.FC<AgentCardProps> = ({ agent, onSelect, compact = false }) => {
return (
<div className={'agent-card' + (compact ? ' agent-card--compact' : '')} onClick={onSelect} role="button" tabIndex={0} onKeyDown={(e) => e.key === 'Enter' && onSelect()}>
<div className="agent-card-header">
<img className="agent-avatar" src={agent.avatar || '/default-agent.png'} alt={agent.name}
onError={(e) => { (e.target as HTMLImageElement).src = '/default-agent.png'; }} />
<div className="agent-card-info">
<h3 className="agent-name">{agent.name}</h3>
<span className="agent-status" style={{ color: STATUS_COLORS[agent.status] }}>{STATUS_LABELS[agent.status]}</span>
</div>
</div>
{!compact && <p className="agent-description">{agent.description}</p>}
<div className="agent-capabilities">
{agent.capabilities.slice(0, compact ? 2 : 4).map((cap) => <span key={cap} className="capability-tag">{cap}</span>)}
{agent.capabilities.length > (compact ? 2 : 4) && <span className="capability-more">+{agent.capabilities.length - (compact ? 2 : 4)}</span>}
</div>
<div className="agent-card-meta">
<div className="agent-reputation">
<span className="meta-label">信誉分</span>
<span className="meta-value">{agent.reputation}/100</span>
</div>
<div className="agent-price">
<span className="meta-label">价格</span>
<span className="meta-value">{formatPrice(agent.price)}</span>
</div>
</div>
{!compact && (
<div className="agent-card-stats">
<div className="stat">
<span className="stat-value">{agent.totalTasks}</span>
<span className="stat-label">任务数</span>
</div>
<div className="stat">
<span className="stat-value">{(agent.successRate * 100).toFixed(0)}%</span>
<span className="stat-label">成功率</span>
</div>
</div>
)}
<div className="agent-card-footer">
<span className="agent-owner">所有者: {formatAddress(agent.owner)}</span>
<span className="agent-tags">{agent.tags.slice(0, 2).map((tag) => <span key={tag}>#{tag}</span>)}</span>
</div>
<button className="agent-interact-btn" onClick={(e) => { e.stopPropagation(); onSelect(); }} type="button">
与 Agent 对话
</button>
</div>
);
};
3.4 能力筛选器
import React, { useState } from 'react';
export interface FilterState {
capability: string;
minReputation: number;
maxPrice: string;
status: string;
}
interface CapabilityFilterProps {
value: FilterState;
onChange: (filters: FilterState) => void;
}
const CAPABILITY_OPTIONS = [
'data-analysis', 'content-generation', 'smart-contract-audit',
'defi-strategy', 'nft-analysis', 'market-prediction',
'governance-advisor', 'social-monitoring', 'portfolio-management', 'cross-chain-bridge',
];
const PRICE_RANGES = [
{ label: '不限', value: '' },
{ label: '< 100 AIMP', value: '100000000' },
{ label: '< 500 AIMP', value: '500000000' },
{ label: '< 1000 AIMP', value: '1000000000' },
];
const REPUTATION_LEVELS = [
{ label: '不限', value: 0 },
{ label: '>= 60', value: 60 },
{ label: '>= 80', value: 80 },
{ label: '>= 90', value: 90 },
];
export const CapabilityFilter: React.FC<CapabilityFilterProps> = ({ value, onChange }) => {
const [expanded, setExpanded] = useState(false);
const update = (partial: Partial<FilterState>) => onChange({ ...value, ...partial });
return (
<div className="capability-filter">
<div className="filter-header">
<h4>筛选条件</h4>
<button className="filter-toggle" onClick={() => setExpanded(!expanded)} type="button">
{expanded ? '收起' : '展开'}
</button>
</div>
{expanded && (
<div className="filter-body">
<div className="filter-group">
<label>能力类型</label>
<select value={value.capability} onChange={(e) => update({ capability: e.target.value })}>
<option value="">全部能力</option>
{CAPABILITY_OPTIONS.map((opt) => <option key={opt} value={opt}>{opt.replace(/-/g, ' ')}</option>)}
</select>
</div>
<div className="filter-group">
<label>最低信誉分</label>
<select value={value.minReputation} onChange={(e) => update({ minReputation: parseInt(e.target.value, 10) })}>
{REPUTATION_LEVELS.map((level) => <option key={level.value} value={level.value}>{level.label}</option>)}
</select>
</div>
<div className="filter-group">
<label>最高价格</label>
<select value={value.maxPrice} onChange={(e) => update({ maxPrice: e.target.value })}>
{PRICE_RANGES.map((range) => <option key={range.value} value={range.value}>{range.label}</option>)}
</select>
</div>
<div className="filter-group">
<label>状态</label>
<select value={value.status} onChange={(e) => update({ status: e.target.value })}>
<option value="">全部状态</option>
<option value="active">在线</option>
<option value="paused">暂停</option>
<option value="offline">离线</option>
</select>
</div>
</div>
)}
<div className="filter-active-tags">
{value.capability && <span className="active-tag">能力: {value.capability}<button onClick={() => update({ capability: '' })} type="button">x</button></span>}
{value.minReputation > 0 && <span className="active-tag">信誉 >= {value.minReputation}<button onClick={() => update({ minReputation: 0 })} type="button">x</button></span>}
{value.maxPrice && <span className="active-tag">价格 < {parseInt(value.maxPrice) / 1_000_000} AIMP<button onClick={() => update({ maxPrice: '' })} type="button">x</button></span>}
</div>
</div>
);
};
3.5 Agent 详情页
import React, { useState, useEffect } from 'react';
import { useAuth } from '../providers/AuthProvider';
import { AgentRegistryClient, AgentInfo } from '../lib/agentRegistry';
interface AgentDetailProps {
agentId: string;
onBack: () => void;
onStartChat: (agentId: string) => void;
}
export const AgentDetail: React.FC<AgentDetailProps> = ({ agentId, onBack, onStartChat }) => {
const { cosmClient } = useAuth();
const [agent, setAgent] = useState<AgentInfo | null>(null);
const [stats, setStats] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
if (!cosmClient) return;
const fetchDetail = async () => {
setLoading(true);
try {
const registry = new AgentRegistryClient(cosmClient);
const [agentData, statsData] = await Promise.all([
registry.getAgent(agentId),
registry.getAgentStats(agentId),
]);
setAgent(agentData);
setStats(statsData);
} catch (err) {
console.error('获取 Agent 详情失败:', err);
} finally {
setLoading(false);
}
};
fetchDetail();
}, [cosmClient, agentId]);
if (loading) return <div className="detail-loading">加载 Agent 详情...</div>;
if (!agent) return <div className="detail-not-found"><p>Agent 未找到或已下线</p><button onClick={onBack} type="button">返回</button></div>;
return (
<div className="agent-detail">
<button className="back-btn" onClick={onBack} type="button"><- 返回列表</button>
<div className="detail-hero">
<img className="detail-avatar" src={agent.avatar} alt={agent.name} />
<div className="detail-title">
<h1>{agent.name}</h1>
<p className="detail-description">{agent.description}</p>
</div>
</div>
<div className="detail-grid">
<div className="detail-section">
<h3>能力列表</h3>
<div className="capability-list">
{agent.capabilities.map((cap) => <span key={cap} className="capability-tag capability-tag--lg">{cap}</span>)}
</div>
</div>
<div className="detail-section">
<h3>信誉与价格</h3>
<div className="detail-metrics">
<div className="metric">
<div className="metric-ring" style={{ background: 'conic-gradient(#22c55e ' + agent.reputation * 3.6 + 'deg, #e5e7eb 0deg)' }}>
<span>{agent.reputation}</span>
</div>
<label>信誉分</label>
</div>
<div className="metric">
<div className="metric-value">{parseInt(agent.price) / 1_000_000}</div>
<label>每次交互 (AIMP)</label>
</div>
<div className="metric">
<div className="metric-value">{(agent.successRate * 100).toFixed(0)}%</div>
<label>成功率</label>
</div>
<div className="metric">
<div className="metric-value">{agent.totalTasks}</div>
<label>总任务数</label>
</div>
</div>
</div>
{stats && (
<div className="detail-section">
<h3>性能统计</h3>
<div className="stats-grid">
<div className="stat-card">
<span className="stat-card-label">平均响应时间</span>
<span className="stat-card-value">{stats.avgResponseTime}ms</span>
</div>
<div className="stat-card">
<span className="stat-card-label">累计收益</span>
<span className="stat-card-value">{parseInt(stats.totalEarned) / 1_000_000} AIMP</span>
</div>
</div>
</div>
)}
<div className="detail-section">
<h3>标签</h3>
<div className="tags-list">{agent.tags.map((tag) => <span key={tag} className="tag">#{tag}</span>)}</div>
</div>
</div>
<div className="detail-actions">
<button className="btn-primary" onClick={() => onStartChat(agent.id)} type="button">与 Agent 对话</button>
<button className="btn-secondary" type="button">委托任务</button>
</div>
</div>
);
};
3.6 Agent 市场页面
import React, { useState } from 'react';
import { AgentList } from '../components/AgentList';
import { AgentDetail } from '../components/AgentDetail';
import { CapabilityFilter, FilterState } from '../components/CapabilityFilter';
import { AgentInfo } from '../lib/agentRegistry';
type ViewState = { type: 'list' } | { type: 'detail'; agentId: string } | { type: 'chat'; agentId: string };
export const AgentMarket: React.FC = () => {
const [view, setView] = useState<ViewState>({ type: 'list' });
const [filters, setFilters] = useState<FilterState>({ capability: '', minReputation: 0, maxPrice: '', status: '' });
const handleSelectAgent = (agent: AgentInfo) => setView({ type: 'detail', agentId: agent.id });
const handleStartChat = (agentId: string) => setView({ type: 'chat', agentId });
const searchParams = {
...(filters.capability && { capability: filters.capability }),
...(filters.minReputation > 0 && { minReputation: filters.minReputation }),
...(filters.maxPrice && { maxPrice: filters.maxPrice }),
...(filters.status && { status: filters.status as AgentInfo['status'] }),
};
switch (view.type) {
case 'detail':
return <AgentDetail agentId={view.agentId} onBack={() => setView({ type: 'list' })} onStartChat={handleStartChat} />;
case 'chat':
return <div>Chat view for {view.agentId}</div>;
default:
return (
<div className="agent-market">
<div className="market-header">
<h1>Agent 市场</h1>
<p>发现并连接 MSG Chain 上的 AI Agent</p>
</div>
<CapabilityFilter value={filters} onChange={setFilters} />
<AgentList onSelectAgent={handleSelectAgent} filters={searchParams} />
</div>
);
}
};
4. Agent 交互界面
4.1 Agent 消息协议客户端
import { SigningStargateClient } from '@cosmjs/stargate';
import { toUtf8, fromUtf8 } from '@cosmjs/encoding';
export interface AgentMessage {
id: string;
role: 'user' | 'agent' | 'system';
content: string;
timestamp: number;
txHash?: string;
metadata?: Record<string, unknown>;
}
export interface AgentCommand {
type: string;
payload: unknown;
requireApproval: boolean;
}
export interface AgentTaskResponse {
taskId: string;
status: 'pending' | 'running' | 'completed' | 'failed';
result?: unknown;
error?: string;
txHash?: string;
}
const AGENT_EXECUTOR_ADDRESS = 'msg1agentexecutorcontractaddressxxxxxxxxxxxxx';
export class AgentClient {
private cosmClient: SigningStargateClient;
private walletAddress: string;
constructor(cosmClient: SigningStargateClient, walletAddress: string) {
this.cosmClient = cosmClient;
this.walletAddress = walletAddress;
}
async sendMessage(agentId: string, content: string, sessionId: string): Promise<AgentMessage> {
const msg = {
send_message: {
agent_id: agentId,
session_id: sessionId,
content,
sender: this.walletAddress,
},
};
const fee = { amount: [{ denom: 'uaimp', amount: '5000' }], gas: '300000' };
const result = await this.cosmClient.execute(this.walletAddress, AGENT_EXECUTOR_ADDRESS, msg, fee);
const rawLog = JSON.parse(result.rawLog ?? '{}');
const responseMsg = rawLog?.events
?.find((e: any) => e.type === 'agent_response')
?.attributes?.find((a: any) => a.key === 'response')?.value;
return {
id: 'msg_' + Date.now(),
role: 'agent',
content: responseMsg || 'Agent 已收到消息,处理中...',
timestamp: Date.now(),
txHash: result.transactionHash,
};
}
async executeTask(agentId: string, command: AgentCommand, sessionId: string): Promise<AgentTaskResponse> {
const msg = {
execute_task: {
agent_id: agentId,
session_id: sessionId,
command_type: command.type,
payload: toUtf8(JSON.stringify(command.payload)),
require_approval: command.requireApproval,
},
};
const fee = { amount: [{ denom: 'uaimp', amount: '10000' }], gas: '500000' };
const result = await this.cosmClient.execute(this.walletAddress, AGENT_EXECUTOR_ADDRESS, msg, fee);
const rawLog = JSON.parse(result.rawLog ?? '{}');
const taskAttr = rawLog?.events?.find((e: any) => e.type === 'agent_task')?.attributes;
return {
taskId: taskAttr?.find((a: any) => a.key === 'task_id')?.value ?? '',
status: 'pending',
txHash: result.transactionHash,
};
}
async getMessageHistory(agentId: string, sessionId: string, limit = 50): Promise<AgentMessage[]> {
const response = await this.cosmClient.queryContractSmart(AGENT_EXECUTOR_ADDRESS, {
get_messages: { agent_id: agentId, session_id: sessionId, limit },
}) as { messages: AgentMessage[] };
return response.messages.map((msg) => ({ ...msg, timestamp: Number(msg.timestamp) }));
}
async getTaskStatus(taskId: string): Promise<AgentTaskResponse> {
return this.cosmClient.queryContractSmart(AGENT_EXECUTOR_ADDRESS, { get_task_status: { task_id: taskId } }) as Promise<AgentTaskResponse>;
}
}
4.2 聊天界面
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useAuth } from '../providers/AuthProvider';
import { AgentClient, AgentMessage } from '../lib/agentClient';
interface AgentChatProps {
agentId: string;
agentName: string;
onClose: () => void;
}
export const AgentChat: React.FC<AgentChatProps> = ({ agentId, agentName, onClose }) => {
const { cosmClient, address, session } = useAuth();
const [messages, setMessages] = useState<AgentMessage[]>([]);
const [input, setInput] = useState('');
const [sending, setSending] = useState(false);
const [agentClient, setAgentClient] = useState<AgentClient | null>(null);
const messagesEndRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLTextAreaElement>(null);
const sessionId = session?.sessionId ?? 'temp_session';
useEffect(() => {
if (!cosmClient || !address) return;
setAgentClient(new AgentClient(cosmClient, address));
}, [cosmClient, address]);
useEffect(() => {
if (!agentClient) return;
const loadHistory = async () => {
try {
const history = await agentClient.getMessageHistory(agentId, sessionId);
if (history.length > 0) {
setMessages(history);
} else {
setMessages([{ id: 'welcome', role: 'system', content: '你好!我是 ' + agentName + '。请问有什么可以帮助你的?', timestamp: Date.now() }]);
}
} catch {
setMessages([{ id: 'welcome', role: 'system', content: '你好!我是 ' + agentName + '。请问有什么可以帮助你的?', timestamp: Date.now() }]);
}
};
loadHistory();
}, [agentClient, agentId, agentName, sessionId]);
useEffect(() => { messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [messages]);
const sendMessage = useCallback(async (content: string) => {
if (!agentClient || !content.trim() || sending) return;
const userMessage: AgentMessage = { id: 'user_' + Date.now(), role: 'user', content: content.trim(), timestamp: Date.now() };
setMessages((prev) => [...prev, userMessage]);
setInput('');
setSending(true);
try {
const response = await agentClient.sendMessage(agentId, content.trim(), sessionId);
setMessages((prev) => [...prev, response]);
} catch (err) {
setMessages((prev) => [...prev, { id: 'err_' + Date.now(), role: 'system', content: '消息发送失败,请重试。', timestamp: Date.now() }]);
console.error('发送消息失败:', err);
} finally {
setSending(false);
inputRef.current?.focus();
}
}, [agentClient, agentId, sending, sessionId]);
const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); sendMessage(input); };
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage(input); }
};
return (
<div className="agent-chat">
<div className="chat-header">
<div className="chat-header-info">
<div className="chat-agent-avatar">{agentName[0]}</div>
<div><h3>{agentName}</h3><span className="chat-status">在线</span></div>
</div>
<div className="chat-header-actions">
<button className="chat-clear-btn" onClick={() => setMessages([])} type="button" title="清空对话">X</button>
<button className="chat-close-btn" onClick={onClose} type="button" title="关闭">-</button>
</div>
</div>
<div className="chat-messages">
{messages.map((msg) => (
<div key={msg.id} className={'chat-message chat-message--' + msg.role}>
<div className="message-avatar">{msg.role === 'user' ? address?.slice(2, 4).toUpperCase() : 'AI'}</div>
<div className="message-content">
<div className="message-bubble">{msg.content}</div>
<div className="message-meta">
<span className="message-time">{new Date(msg.timestamp).toLocaleTimeString('zh-CN')}</span>
{msg.txHash && <a className="message-tx" href={'https://explorer.msgchain.org/tx/' + msg.txHash} target="_blank" rel="noopener noreferrer">交易详情</a>}
</div>
</div>
</div>
))}
{sending && (
<div className="chat-message chat-message--agent">
<div className="message-avatar">AI</div>
<div className="message-content">
<div className="message-bubble message-bubble--typing"><span className="typing-dot" /><span className="typing-dot" /><span className="typing-dot" /></div>
</div>
</div>
)}
<div ref={messagesEndRef} />
</div>
<form className="chat-input-bar" onSubmit={handleSubmit}>
<textarea ref={inputRef} className="chat-input" placeholder="输入消息... (Enter 发送, Shift+Enter 换行)" value={input} onChange={(e) => setInput(e.target.value)} onKeyDown={handleKeyDown} rows={1} disabled={sending} />
<button className="chat-send-btn" type="submit" disabled={!input.trim() || sending}>{sending ? '...' : '发送'}</button>
</form>
</div>
);
};
4.3 交易确认流程
import React, { useState } from 'react';
import { useAuth } from '../providers/AuthProvider';
export interface PendingTransaction {
id: string;
title: string;
description: string;
agentId: string;
agentName: string;
amount: string;
denom: string;
contractAddress: string;
rawMessage: unknown;
}
interface TransactionApprovalProps {
tx: PendingTransaction;
onConfirm: (txId: string) => Promise<void>;
onReject: (txId: string) => void;
}
const formatAmount = (amount: string, denom: string): string => {
const num = parseInt(amount, 10);
if (denom === 'uaimp') return (num / 1_000_000).toFixed(2) + ' AIMP';
return num + ' ' + denom;
};
export const TransactionApproval: React.FC<TransactionApprovalProps> = ({ tx, onConfirm, onReject }) => {
const [confirming, setConfirming] = useState(false);
const [expanded, setExpanded] = useState(false);
const handleConfirm = async () => {
setConfirming(true);
try { await onConfirm(tx.id); } finally { setConfirming(false); }
};
return (
<div className="tx-approval-overlay">
<div className="tx-approval-modal">
<div className="tx-approval-header">
<div className="tx-approval-icon">!</div>
<h3>确认交易</h3>
<p>Agent "{tx.agentName}" 请求执行以下操作</p>
</div>
<div className="tx-approval-body">
<div className="tx-field"><label>操作</label><span>{tx.title}</span></div>
<div className="tx-field"><label>描述</label><span>{tx.description}</span></div>
<div className="tx-field tx-field--amount"><label>金额</label><span className="tx-amount">{formatAmount(tx.amount, tx.denom)}</span></div>
<div className="tx-field"><label>Agent</label><span>{tx.agentName}</span></div>
<div className="tx-field"><label>合约地址</label><span className="tx-address">{tx.contractAddress}</span></div>
<button className="tx-expand-btn" onClick={() => setExpanded(!expanded)} type="button">
{expanded ? '隐藏原始消息' : '查看原始消息'}
</button>
{expanded && <pre className="tx-raw-message">{JSON.stringify(tx.rawMessage, null, 2)}</pre>}
</div>
<div className="tx-approval-actions">
<button className="tx-reject-btn" onClick={() => onReject(tx.id)} disabled={confirming} type="button">拒绝</button>
<button className="tx-confirm-btn" onClick={handleConfirm} disabled={confirming} type="button">
{confirming ? '确认中...' : '确认并签名'}
</button>
</div>
</div>
</div>
);
};
4.4 支付批准组件
import React, { useState } from 'react';
import { useAuth } from '../providers/AuthProvider';
export interface PaymentRequest {
requestId: string;
agentId: string;
agentName: string;
amount: string;
denom: string;
purpose: string;
recurring: boolean;
maxExecutions?: number;
}
interface PaymentApprovalProps {
request: PaymentRequest;
onApprove: (requestId: string, maxAmount?: string) => Promise<void>;
onDeny: (requestId: string) => void;
}
export const PaymentApproval: React.FC<PaymentApprovalProps> = ({ request, onApprove, onDeny }) => {
const [processing, setProcessing] = useState(false);
const [budgetOverride, setBudgetOverride] = useState('');
const [showBudgetInput, setShowBudgetInput] = useState(false);
const handleApprove = async () => {
setProcessing(true);
try {
const maxAmount = showBudgetInput ? (budgetOverride || undefined) : undefined;
await onApprove(request.requestId, maxAmount);
} finally { setProcessing(false); }
};
const formatAimp = (amount: string): string => (parseInt(amount) / 1_000_000).toFixed(2) + ' AIMP';
return (
<div className="payment-approval">
<div className="payment-approval-icon">$</div>
<h4>支付授权请求</h4>
<div className="payment-details">
<div className="payment-row"><span>Agent</span><strong>{request.agentName}</strong></div>
<div className="payment-row"><span>金额</span><strong className="payment-amount">{formatAimp(request.amount)}</strong></div>
<div className="payment-row"><span>用途</span><span>{request.purpose}</span></div>
<div className="payment-row"><span>类型</span><span>{request.recurring ? '循环授权' : '单次支付'}</span></div>
{request.maxExecutions && <div className="payment-row"><span>最大执行次数</span><span>{request.maxExecutions}</span></div>}
</div>
<div className="payment-budget-override">
<label>
<input type="checkbox" checked={showBudgetInput} onChange={(e) => setShowBudgetInput(e.target.checked)} />
设置预算上限
</label>
{showBudgetInput && (
<div className="budget-input-group">
<input type="number" placeholder="输入 AIMP 数量" value={budgetOverride} onChange={(e) => setBudgetOverride(e.target.value)} min="0" step="0.01" />
<span>AIMP</span>
</div>
)}
</div>
<div className="payment-approval-actions">
<button className="payment-deny-btn" onClick={() => onDeny(request.requestId)} disabled={processing} type="button">拒绝</button>
<button className="payment-approve-btn" onClick={handleApprove} disabled={processing} type="button">{processing ? '处理中...' : '授权支付'}</button>
</div>
<p className="payment-disclaimer">确认后将通过 Keplr 钱包签名并广播交易。链上 Gas 费用将从您的余额中扣除。</p>
</div>
);
};
4.5 交互界面管理器
import React, { useState, useCallback } from 'react';
import { AgentChat } from './AgentChat';
import { TransactionApproval, PendingTransaction } from './TransactionApproval';
import { PaymentApproval, PaymentRequest } from './PaymentApproval';
type InteractionView =
| { type: 'chat'; agentId: string; agentName: string }
| { type: 'tx_approval'; tx: PendingTransaction }
| { type: 'payment_approval'; request: PaymentRequest }
| { type: 'none' };
interface AgentInteractionManagerProps {
children: React.ReactNode;
}
export const AgentInteractionContext = React.createContext<{
openChat: (agentId: string, agentName: string) => void;
requestTxApproval: (tx: PendingTransaction) => Promise<boolean>;
requestPaymentApproval: (request: PaymentRequest) => Promise<boolean>;
closeAll: () => void;
}>({
openChat: () => {},
requestTxApproval: () => Promise.resolve(false),
requestPaymentApproval: () => Promise.resolve(false),
closeAll: () => {},
});
export const AgentInteractionManager: React.FC<AgentInteractionManagerProps> = ({ children }) => {
const [view, setView] = useState<InteractionView>({ type: 'none' });
const [txResolver, setTxResolver] = useState<((value: boolean) => void) | null>(null);
const [paymentResolver, setPaymentResolver] = useState<((value: boolean) => void) | null>(null);
const openChat = useCallback((agentId: string, agentName: string) => setView({ type: 'chat', agentId, agentName }), []);
const requestTxApproval = useCallback((tx: PendingTransaction): Promise<boolean> => new Promise((resolve) => { setTxResolver(() => resolve); setView({ type: 'tx_approval', tx }); }), []);
const requestPaymentApproval = useCallback((request: PaymentRequest): Promise<boolean> => new Promise((resolve) => { setPaymentResolver(() => resolve); setView({ type: 'payment_approval', request }); }), []);
const handleTxConfirm = async (txId: string) => { txResolver?.(true); setTxResolver(null); setView({ type: 'none' }); };
const handleTxReject = (txId: string) => { txResolver?.(false); setTxResolver(null); setView({ type: 'none' }); };
const handlePaymentApprove = async (requestId: string, _maxAmount?: string) => { paymentResolver?.(true); setPaymentResolver(null); setView({ type: 'none' }); };
const handlePaymentDeny = (requestId: string) => { paymentResolver?.(false); setPaymentResolver(null); setView({ type: 'none' }); };
const closeAll = useCallback(() => { setView({ type: 'none' }); setTxResolver(null); setPaymentResolver(null); }, []);
return (
<AgentInteractionContext.Provider value={{ openChat, requestTxApproval, requestPaymentApproval, closeAll }}>
{children}
{view.type === 'chat' && <div className="interaction-overlay"><AgentChat agentId={view.agentId} agentName={view.agentName} onClose={closeAll} /></div>}
{view.type === 'tx_approval' && <TransactionApproval tx={view.tx} onConfirm={handleTxConfirm} onReject={handleTxReject} />}
{view.type === 'payment_approval' && <PaymentApproval request={view.request} onApprove={handlePaymentApprove} onDeny={handlePaymentDeny} />}
</AgentInteractionContext.Provider>
);
};
export const useAgentInteraction = () => {
const ctx = React.useContext(AgentInteractionContext);
if (!ctx) throw new Error('useAgentInteraction must be used within AgentInteractionManager');
return ctx;
};
5. Agent 任务面板
5.1 任务管理客户端
import { SigningStargateClient } from '@cosmjs/stargate';
import { toUtf8, fromUtf8 } from '@cosmjs/encoding';
export type TaskStatus = 'pending' | 'approved' | 'running' | 'completed' | 'failed' | 'cancelled';
export type TaskPriority = 'low' | 'medium' | 'high' | 'critical';
export interface TaskEvent {
timestamp: number;
type: string;
message: string;
data?: unknown;
txHash?: string;
}
export interface AgentTask {
id: string;
agentId: string;
agentName: string;
title: string;
description: string;
status: TaskStatus;
priority: TaskPriority;
params: Record<string, unknown>;
result?: unknown;
events: TaskEvent[];
createdAt: number;
updatedAt: number;
completedAt?: number;
walletAddress: string;
txHash?: string;
estimatedCost: string;
actualCost?: string;
}
export interface CreateTaskParams {
agentId: string;
title: string;
description: string;
priority: TaskPriority;
params: Record<string, unknown>;
maxBudget: string;
}
const TASK_MANAGER_ADDRESS = 'msg1taskmanagercontractaddressxxxxxxxxxxxxxxx';
export class TaskClient {
private cosmClient: SigningStargateClient;
private walletAddress: string;
constructor(cosmClient: SigningStargateClient, walletAddress: string) {
this.cosmClient = cosmClient;
this.walletAddress = walletAddress;
}
async createTask(params: CreateTaskParams): Promise<AgentTask> {
const msg = {
create_task: {
agent_id: params.agentId,
title: params.title,
description: params.description,
priority: params.priority,
params: toUtf8(JSON.stringify(params.params)),
max_budget: params.maxBudget,
creator: this.walletAddress,
},
};
const fee = { amount: [{ denom: 'uaimp', amount: '10000' }], gas: '400000' };
const result = await this.cosmClient.execute(this.walletAddress, TASK_MANAGER_ADDRESS, msg, fee);
const rawLog = JSON.parse(result.rawLog ?? '{}');
const taskAttr = rawLog?.events?.find((e: any) => e.type === 'task_created')?.attributes;
return {
id: taskAttr?.find((a: any) => a.key === 'task_id')?.value ?? '',
agentId: params.agentId,
agentName: '',
title: params.title,
description: params.description,
status: 'pending',
priority: params.priority,
params: params.params,
events: [{ timestamp: Date.now(), type: 'created', message: '任务已创建', txHash: result.transactionHash }],
createdAt: Date.now(),
updatedAt: Date.now(),
walletAddress: this.walletAddress,
txHash: result.transactionHash,
estimatedCost: params.maxBudget,
};
}
async getTask(taskId: string): Promise<AgentTask | null> {
try {
return await this.cosmClient.queryContractSmart(TASK_MANAGER_ADDRESS, { get_task: { task_id: taskId } }) as AgentTask;
} catch { return null; }
}
async listUserTasks(status?: TaskStatus, limit = 20, offset = 0): Promise<{ tasks: AgentTask[]; total: number }> {
return this.cosmClient.queryContractSmart(TASK_MANAGER_ADDRESS, {
list_user_tasks: { address: this.walletAddress, status, limit, offset },
}) as Promise<{ tasks: AgentTask[]; total: number }>;
}
async cancelTask(taskId: string): Promise<void> {
const fee = { amount: [{ denom: 'uaimp', amount: '5000' }], gas: '200000' };
await this.cosmClient.execute(this.walletAddress, TASK_MANAGER_ADDRESS, { cancel_task: { task_id: taskId, canceller: this.walletAddress } }, fee);
}
async getTaskEvents(taskId: string, fromIndex = 0): Promise<TaskEvent[]> {
const response = await this.cosmClient.queryContractSmart(TASK_MANAGER_ADDRESS, { get_task_events: { task_id: taskId, from_index: fromIndex } }) as { events: TaskEvent[] };
return response.events.map((e) => ({ ...e, timestamp: Number(e.timestamp) }));
}
}
5.2 任务创建表单
import React, { useState } from 'react';
import { useAuth } from '../providers/AuthProvider';
import { TaskClient, CreateTaskParams, TaskPriority } from '../lib/taskClient';
interface TaskCreateFormProps {
agentId: string;
agentName: string;
onTaskCreated: (taskId: string) => void;
onCancel: () => void;
}
const PRIORITY_LABELS: Record<TaskPriority, string> = {
low: '低', medium: '中', high: '高', critical: '紧急',
};
export const TaskCreateForm: React.FC<TaskCreateFormProps> = ({ agentId, agentName, onTaskCreated, onCancel }) => {
const { cosmClient, address } = useAuth();
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [priority, setPriority] = useState<TaskPriority>('medium');
const [maxBudget, setMaxBudget] = useState('100');
const [paramsJson, setParamsJson] = useState('{}');
const [submitting, setSubmitting] = useState(false);
const [error, setError] = useState<string | null>(null);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!cosmClient || !address) return;
setSubmitting(true);
setError(null);
try {
let parsedParams: Record<string, unknown>;
try { parsedParams = JSON.parse(paramsJson); } catch { throw new Error('参数格式错误,请输入有效的 JSON'); }
const taskClient = new TaskClient(cosmClient, address);
const budgetAmount = (parseFloat(maxBudget) * 1_000_000).toString();
const params: CreateTaskParams = { agentId, title, description, priority, params: parsedParams, maxBudget: budgetAmount };
const task = await taskClient.createTask(params);
onTaskCreated(task.id);
} catch (err: any) {
setError(err.message || '创建任务失败');
} finally { setSubmitting(false); }
};
return (
<div className="task-create-form">
<h3>创建新任务 - {agentName}</h3>
{error && <div className="form-error"><span>X</span><p>{error}</p></div>}
<form onSubmit={handleSubmit}>
<div className="form-group">
<label htmlFor="task-title">任务标题 *</label>
<input id="task-title" type="text" value={title} onChange={(e) => setTitle(e.target.value)} placeholder="例如: 分析钱包交易历史" required maxLength={100} />
</div>
<div className="form-group">
<label htmlFor="task-desc">任务描述</label>
<textarea id="task-desc" value={description} onChange={(e) => setDescription(e.target.value)} placeholder="详细描述你希望 Agent 完成的工作..." rows={4} maxLength={2000} />
</div>
<div className="form-row">
<div className="form-group">
<label htmlFor="task-priority">优先级</label>
<select id="task-priority" value={priority} onChange={(e) => setPriority(e.target.value as TaskPriority)}>
{(Object.entries(PRIORITY_LABELS) as [TaskPriority, string][]).map(([value, label]) => <option key={value} value={value}>{label}</option>)}
</select>
</div>
<div className="form-group">
<label htmlFor="task-budget">预算上限 (AIMP)</label>
<input id="task-budget" type="number" value={maxBudget} onChange={(e) => setMaxBudget(e.target.value)} min="1" step="1" required />
</div>
</div>
<div className="form-group">
<label htmlFor="task-params">任务参数 (JSON) <span className="label-hint">可选</span></label>
<textarea id="task-params" value={paramsJson} onChange={(e) => setParamsJson(e.target.value)} rows={4} className="form-textarea-mono" placeholder='{"target": "0x...", "depth": "full"}' />
</div>
<div className="form-actions">
<button type="button" className="btn-cancel" onClick={onCancel} disabled={submitting}>取消</button>
<button type="submit" className="btn-submit" disabled={!title.trim() || submitting}>
{submitting ? '提交中 (需要钱包签名)...' : '创建任务'}
</button>
</div>
</form>
</div>
);
};
5.3 任务列表组件
import React, { useState, useEffect, useCallback } from 'react';
import { useAuth } from '../providers/AuthProvider';
import { TaskClient, AgentTask, TaskStatus } from '../lib/taskClient';
interface TaskListProps {
onSelectTask: (task: AgentTask) => void;
filterStatus?: TaskStatus;
}
const STATUS_LABELS: Record<TaskStatus, string> = {
pending: '待处理', approved: '已批准', running: '执行中',
completed: '已完成', failed: '失败', cancelled: '已取消',
};
const STATUS_COLORS: Record<TaskStatus, string> = {
pending: '#6b7280', approved: '#3b82f6', running: '#f59e0b',
completed: '#22c55e', failed: '#ef4444', cancelled: '#9ca3af',
};
const STATUS_ICONS: Record<TaskStatus, string> = {
pending: '[]', approved: '[x]', running: '>>',
completed: '(v)', failed: '(x)', cancelled: '(-)',
};
export const TaskList: React.FC<TaskListProps> = ({ onSelectTask, filterStatus }) => {
const { cosmClient, address } = useAuth();
const [tasks, setTasks] = useState<AgentTask[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(0);
const [total, setTotal] = useState(0);
const fetchTasks = useCallback(async (resetPage = false) => {
if (!cosmClient || !address) return;
setLoading(true);
try {
const taskClient = new TaskClient(cosmClient, address);
const currentPage = resetPage ? 0 : page;
const result = await taskClient.listUserTasks(filterStatus, 20, currentPage * 20);
if (resetPage) setTasks(result.tasks);
else setTasks((prev) => [...prev, ...result.tasks]);
setTotal(result.total);
} catch (err) { console.error('获取任务列表失败:', err); }
finally { setLoading(false); }
}, [cosmClient, address, filterStatus, page]);
useEffect(() => { setPage(0); fetchTasks(true); }, [cosmClient, address, filterStatus]);
const hasMore = tasks.length < total;
const formatDate = (ts: number): string => {
const d = new Date(ts);
return (d.getMonth() + 1) + '/' + d.getDate() + ' ' + d.getHours() + ':' + String(d.getMinutes()).padStart(2, '0');
};
const formatCost = (cost: string): string => (parseInt(cost) / 1_000_000).toFixed(2) + ' AIMP';
if (loading && tasks.length === 0) {
return <div className="task-list-loading">{Array.from({ length: 5 }).map((_, i) => <div key={i} className="task-row-skeleton" />)}</div>;
}
if (tasks.length === 0) {
return <div className="task-list-empty"><p>{STATUS_ICONS[filterStatus ?? 'pending']}</p><p>暂无任务</p><p className="hint">选择一个 Agent 创建新任务</p></div>;
}
return (
<div className="task-list">
<div className="task-list-header"><span className="task-count">共 {total} 个任务</span></div>
<div className="task-rows">
{tasks.map((task) => (
<div key={task.id} className="task-row" onClick={() => onSelectTask(task)} role="button" tabIndex={0} onKeyDown={(e) => e.key === 'Enter' && onSelectTask(task)}>
<div className="task-status-icon" style={{ color: STATUS_COLORS[task.status] }}>{STATUS_ICONS[task.status]}</div>
<div className="task-row-info">
<div className="task-row-title">{task.title}</div>
<div className="task-row-meta">
<span className="task-agent-name">{task.agentName}</span>
<span className="task-date">{formatDate(task.createdAt)}</span>
</div>
</div>
<div className="task-row-right">
<span className="task-status-badge" style={{ backgroundColor: STATUS_COLORS[task.status] + '20', color: STATUS_COLORS[task.status] }}>{STATUS_LABELS[task.status]}</span>
<span className="task-cost">{task.actualCost ? formatCost(task.actualCost) : formatCost(task.estimatedCost)}</span>
</div>
</div>
))}
</div>
{hasMore && <div className="task-list-footer"><button onClick={() => setPage((p) => p + 1)} disabled={loading} type="button">{loading ? '加载中...' : '加载更多'}</button></div>}
</div>
);
};
5.4 任务详情视图
import React, { useState, useEffect } from 'react';
import { useAuth } from '../providers/AuthProvider';
import { TaskClient, AgentTask, TaskStatus, TaskEvent } from '../lib/taskClient';
interface TaskDetailProps {
task: AgentTask;
onBack: () => void;
onCancel: (taskId: string) => void;
}
const STATUS_LABELS: Record<TaskStatus, string> = {
pending: '待处理', approved: '已批准', running: '执行中',
completed: '已完成', failed: '失败', cancelled: '已取消',
};
const PROGRESS_MAP: Record<TaskStatus, number> = {
pending: 0, approved: 25, running: 60,
completed: 100, failed: 100, cancelled: 100,
};
export const TaskDetail: React.FC<TaskDetailProps> = ({ task, onBack, onCancel }) => {
const { cosmClient, address } = useAuth();
const [events, setEvents] = useState<TaskEvent[]>(task.events ?? []);
const [currentStatus, setCurrentStatus] = useState(task.status);
const [refreshing, setRefreshing] = useState(false);
const refreshTask = async () => {
if (!cosmClient || !address) return;
setRefreshing(true);
try {
const taskClient = new TaskClient(cosmClient, address);
const [updatedTask, taskEvents] = await Promise.all([taskClient.getTask(task.id), taskClient.getTaskEvents(task.id)]);
if (updatedTask) setCurrentStatus(updatedTask.status);
setEvents(taskEvents);
} catch (err) { console.error('刷新任务失败:', err); }
finally { setRefreshing(false); }
};
useEffect(() => {
const interval = setInterval(() => {
if (currentStatus === 'running' || currentStatus === 'pending') refreshTask();
}, 5000);
return () => clearInterval(interval);
}, [currentStatus, task.id]);
const progress = PROGRESS_MAP[currentStatus];
const formatTime = (ts: number): string => new Date(ts).toLocaleString('zh-CN');
const formatCost = (cost: string): string => (parseInt(cost) / 1_000_000).toFixed(2) + ' AIMP';
return (
<div className="task-detail">
<div className="task-detail-header">
<button className="back-btn" onClick={onBack} type="button"><- 返回列表</button>
<button className="refresh-btn" onClick={refreshTask} disabled={refreshing} type="button">{refreshing ? '刷新中...' : '刷新'}</button>
</div>
<div className="task-detail-body">
<div className="task-section"><h2>{task.title}</h2><p className="task-description">{task.description || '无描述'}</p></div>
<div className="task-section">
<div className="task-progress-bar">
<div className="task-progress-fill" style={{ width: progress + '%', backgroundColor: currentStatus === 'failed' ? '#ef4444' : '#22c55e' }} />
<span className="task-progress-label">{STATUS_LABELS[currentStatus]} ({progress}%)</span>
</div>
</div>
<div className="task-section">
<h3>任务信息</h3>
<div className="task-info-grid">
<div className="task-info-item"><label>Agent</label><span>{task.agentName || task.agentId}</span></div>
<div className="task-info-item"><label>优先级</label><span className={'priority-badge priority-' + task.priority}>{task.priority}</span></div>
<div className="task-info-item"><label>创建时间</label><span>{formatTime(task.createdAt)}</span></div>
<div className="task-info-item"><label>预计费用</label><span>{formatCost(task.estimatedCost)}</span></div>
{task.actualCost && <div className="task-info-item"><label>实际费用</label><span>{formatCost(task.actualCost)}</span></div>}
{task.completedAt && <div className="task-info-item"><label>完成时间</label><span>{formatTime(task.completedAt)}</span></div>}
{task.txHash && <div className="task-info-item"><label>交易哈希</label><a href={'https://explorer.msgchain.org/tx/' + task.txHash} target="_blank" rel="noopener noreferrer" className="tx-link">{task.txHash.slice(0, 16)}...</a></div>}
</div>
</div>
<div className="task-section">
<h3>参数 <span className="section-badge">{Object.keys(task.params).length} 项</span></h3>
<pre className="task-params-json">{JSON.stringify(task.params, null, 2)}</pre>
</div>
{task.result && (
<div className="task-section">
<h3>执行结果</h3>
<div className="task-result">{typeof task.result === 'string' ? <p>{task.result}</p> : <pre>{JSON.stringify(task.result, null, 2)}</pre>}</div>
</div>
)}
<div className="task-section">
<h3>事件时间线 <span className="section-badge">{events.length} 条</span></h3>
<div className="event-timeline">
{events.map((event, idx) => (
<div key={idx} className="timeline-item">
<div className="timeline-dot" />
<div className="timeline-content">
<div className="timeline-header"><span className="timeline-type">{event.type}</span><span className="timeline-time">{formatTime(event.timestamp)}</span></div>
<p className="timeline-message">{event.message}</p>
{event.txHash && <a className="timeline-tx" href={'https://explorer.msgchain.org/tx/' + event.txHash} target="_blank" rel="noopener noreferrer">交易: {event.txHash.slice(0, 16)}...</a>}
{event.data && <pre className="timeline-data">{JSON.stringify(event.data, null, 2)}</pre>}
</div>
</div>
))}
</div>
</div>
</div>
<div className="task-detail-actions">
{(currentStatus === 'pending' || currentStatus === 'approved') && <button className="btn-cancel-task" onClick={() => onCancel(task.id)} type="button">取消任务</button>}
{(currentStatus === 'running') && <div className="task-refresh-hint">每 5 秒自动刷新</div>}
</div>
</div>
);
};
5.5 任务仪表板
import React, { useState, useCallback } from 'react';
import { useAuth } from '../providers/AuthProvider';
import { TaskList } from './TaskList';
import { TaskDetail } from './TaskDetail';
import { AgentTask, TaskStatus } from '../lib/taskClient';
type DashboardView = { type: 'list'; filterStatus?: TaskStatus } | { type: 'detail'; task: AgentTask };
export const AgentDashboard: React.FC = () => {
const { address, isConnected } = useAuth();
const [view, setView] = useState<DashboardView>({ type: 'list' });
const [activeTab, setActiveTab] = useState<TaskStatus | 'all'>('all');
const tabs: { key: TaskStatus | 'all'; label: string }[] = [
{ key: 'all', label: '全部' }, { key: 'running', label: '执行中' },
{ key: 'pending', label: '待处理' }, { key: 'completed', label: '已完成' }, { key: 'failed', label: '失败' },
];
const handleSelectTask = useCallback((task: AgentTask) => setView({ type: 'detail', task }), []);
const handleCancelTask = useCallback(async (taskId: string) => setView({ type: 'list', filterStatus: activeTab === 'all' ? undefined : activeTab }), [activeTab]);
if (!isConnected || !address) {
return <div className="dashboard-not-connected"><h2>任务仪表板</h2><p>请先连接钱包以查看您的任务</p></div>;
}
return (
<div className="agent-dashboard">
<div className="dashboard-header"><h1>Agent 任务面板</h1><p>管理您委托给 AI Agent 的所有任务</p></div>
{view.type === 'list' ? (
<>
<div className="dashboard-tabs">
{tabs.map((tab) => (
<button key={tab.key} className={'tab-btn ' + (activeTab === tab.key ? 'tab-btn--active' : '')}
onClick={() => { setActiveTab(tab.key); setView({ type: 'list', filterStatus: tab.key === 'all' ? undefined : tab.key }); }} type="button">
{tab.label}
</button>
))}
</div>
<div className="dashboard-summary">
<div className="summary-card"><span className="summary-value">5</span><span className="summary-label">总任务数</span></div>
<div className="summary-card"><span className="summary-value">80%</span><span className="summary-label">成功率</span></div>
<div className="summary-card"><span className="summary-value">1,250</span><span className="summary-label">总花费 (AIMP)</span></div>
</div>
<TaskList onSelectTask={handleSelectTask} filterStatus={activeTab === 'all' ? undefined : activeTab} />
</>
) : (
<TaskDetail task={view.task} onBack={() => setView({ type: 'list', filterStatus: activeTab === 'all' ? undefined : activeTab })} onCancel={handleCancelTask} />
)}
</div>
);
};
6. 实时事件推送
6.1 WebSocket 连接管理器
export type WsEventHandler = (event: WsEvent) => void;
export type WsConnectionState = 'disconnected' | 'connecting' | 'connected';
export interface WsEvent {
type: string;
data: unknown;
query: string;
timestamp: number;
}
export interface WsSubscription {
id: string;
query: string;
handler: WsEventHandler;
}
const RECONNECT_DELAYS = [1000, 2000, 5000, 10000, 30000];
const MAX_RECONNECT_ATTEMPTS = 10;
export class WebSocketManager {
private ws: WebSocket | null = null;
private url: string;
private subscriptions: Map<string, WsSubscription> = new Map();
private state: WsConnectionState = 'disconnected';
private reconnectAttempts = 0;
private reconnectTimer: ReturnType<typeof setTimeout> | null = null;
private pendingSubscriptions: string[] = [];
private stateListeners: Set<(state: WsConnectionState) => void> = new Set();
constructor(url: string = 'wss://rpc.msgchain.org/websocket') {
this.url = url;
}
connect(): void {
if (this.state === 'connecting' || this.state === 'connected') return;
this.setState('connecting');
try {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.setState('connected');
this.reconnectAttempts = 0;
this.pendingSubscriptions.forEach((query) => this.sendSubscriptionRequest(query));
this.pendingSubscriptions = [];
};
this.ws.onmessage = (event: MessageEvent) => {
try {
const parsed = JSON.parse(event.data as string);
const result = parsed?.result;
if (!result) return;
const events = result.events ?? result.data?.events;
if (!events) return;
const query = result.query ?? '';
events.forEach((evt: any) => {
const wsEvent: WsEvent = {
type: evt.type ?? 'unknown',
data: evt.attributes ? this.parseAttributes(evt.attributes) : evt,
query,
timestamp: Date.now(),
};
this.subscriptions.forEach((sub) => {
if (sub.query === query || query.includes(sub.query)) sub.handler(wsEvent);
});
});
} catch (err) { console.error('WebSocket 消息解析失败:', err); }
};
this.ws.onclose = () => { this.setState('disconnected'); this.scheduleReconnect(); };
this.ws.onerror = () => { console.error('WebSocket 连接错误'); this.ws?.close(); };
} catch (err) { console.error('WebSocket 创建失败:', err); this.scheduleReconnect(); }
}
disconnect(): void {
if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; }
this.reconnectAttempts = MAX_RECONNECT_ATTEMPTS;
this.subscriptions.clear();
this.pendingSubscriptions = [];
this.ws?.close();
this.ws = null;
this.setState('disconnected');
}
subscribe(query: string, handler: WsEventHandler): string {
const id = 'sub_' + Date.now() + '_' + Math.random().toString(36).slice(2, 8);
this.subscriptions.set(id, { id, query, handler });
if (this.state === 'connected') this.sendSubscriptionRequest(query);
else this.pendingSubscriptions.push(query);
if (this.state === 'disconnected') this.connect();
return id;
}
unsubscribe(id: string): void {
const sub = this.subscriptions.get(id);
if (sub) {
this.subscriptions.delete(id);
if (this.state === 'connected') this.sendUnsubscriptionRequest(sub.query);
}
}
getState(): WsConnectionState { return this.state; }
onStateChange(listener: (state: WsConnectionState) => void): () => void {
this.stateListeners.add(listener);
return () => this.stateListeners.delete(listener);
}
private setState(state: WsConnectionState): void {
this.state = state;
this.stateListeners.forEach((listener) => listener(state));
}
private sendSubscriptionRequest(query: string): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
this.ws.send(JSON.stringify({ jsonrpc: '2.0', method: 'subscribe', id: 'sub_' + Date.now(), params: { query } }));
}
private sendUnsubscriptionRequest(query: string): void {
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) return;
this.ws.send(JSON.stringify({ jsonrpc: '2.0', method: 'unsubscribe', id: 'unsub_' + Date.now(), params: { query } }));
}
private scheduleReconnect(): void {
if (this.reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) return;
const delay = RECONNECT_DELAYS[Math.min(this.reconnectAttempts, RECONNECT_DELAYS.length - 1)];
this.reconnectTimer = setTimeout(() => { this.reconnectAttempts++; this.connect(); }, delay);
}
private parseAttributes(attributes: { key: string; value: string }[]): Record<string, string> {
const result: Record<string, string> = {};
attributes.forEach((attr) => { result[attr.key] = attr.value; });
return result;
}
}
export const wsManager = new WebSocketManager();
6.2 事件类型与订阅管理
import { wsManager, WsEvent, WsEventHandler } from './websocketManager';
export enum AgentEventType {
AGENT_ONLINE = 'agent:online',
AGENT_OFFLINE = 'agent:offline',
AGENT_REPUTATION_CHANGE = 'agent:reputation_change',
MESSAGE_SENT = 'message:sent',
MESSAGE_RECEIVED = 'message:received',
MESSAGE_ERROR = 'message:error',
TASK_CREATED = 'task:created',
TASK_STARTED = 'task:started',
TASK_PROGRESS = 'task:progress',
TASK_COMPLETED = 'task:completed',
TASK_FAILED = 'task:failed',
TASK_CANCELLED = 'task:cancelled',
PAYMENT_REQUESTED = 'payment:requested',
PAYMENT_CONFIRMED = 'payment:confirmed',
PAYMENT_FAILED = 'payment:failed',
AGENT_LOG = 'agent:log',
AGENT_ERROR = 'agent:error',
AGENT_WARNING = 'agent:warning',
}
export interface AgentEventPayload {
agentId?: string;
taskId?: string;
sessionId?: string;
walletAddress?: string;
message?: string;
data?: unknown;
txHash?: string;
timestamp: number;
}
export interface AgentEvent {
type: AgentEventType;
payload: AgentEventPayload;
}
export class AgentEventManager {
private subscriptions: Map<string, string> = new Map();
private handlers: Map<AgentEventType, Set<WsEventHandler>> = new Map();
constructor() {
Object.values(AgentEventType).forEach((type) => this.handlers.set(type, new Set()));
}
connect(): void { wsManager.connect(); }
disconnect(): void {
this.subscriptions.forEach((_subId, query) => {
const id = this.subscriptions.get(query);
if (id) wsManager.unsubscribe(id);
});
this.subscriptions.clear();
wsManager.disconnect();
}
subscribeToAgent(agentId: string): void {
const query = "tm.event = 'Tx' AND agent_id = '" + agentId + "'";
if (this.subscriptions.has(query)) return;
const handler: WsEventHandler = (event: WsEvent) => {
const agentEvent = this.parseWsEvent(event, agentId);
if (agentEvent) this.handlers.get(agentEvent.type)?.forEach((h) => h(event));
};
this.subscriptions.set(query, wsManager.subscribe(query, handler));
}
subscribeToUser(walletAddress: string): void {
const query = "tm.event = 'Tx' AND sender = '" + walletAddress + "'";
if (this.subscriptions.has(query)) return;
const handler: WsEventHandler = (event: WsEvent) => {
const agentEvent = this.parseWsEvent(event, undefined);
if (agentEvent) this.handlers.get(agentEvent.type)?.forEach((h) => h(event));
};
this.subscriptions.set(query, wsManager.subscribe(query, handler));
}
subscribeToTasks(taskId: string): void {
const query = "tm.event = 'Tx' AND task_id = '" + taskId + "'";
if (this.subscriptions.has(query)) return;
const handler: WsEventHandler = (event: WsEvent) => {
const agentEvent = this.parseWsEvent(event, undefined);
if (agentEvent) this.handlers.get(agentEvent.type)?.forEach((h) => h(event));
};
this.subscriptions.set(query, wsManager.subscribe(query, handler));
}
on(type: AgentEventType, handler: WsEventHandler): () => void {
const handlers = this.handlers.get(type);
if (!handlers) throw new Error('未知事件类型: ' + type);
handlers.add(handler);
return () => { handlers.delete(handler); };
}
private parseWsEvent(event: WsEvent, agentId?: string): AgentEvent | null {
const data = event.data as Record<string, any>;
if (!data || !data.action) return null;
let type: AgentEventType;
switch (data.action) {
case 'message_sent': type = AgentEventType.MESSAGE_SENT; break;
case 'task_created': type = AgentEventType.TASK_CREATED; break;
case 'task_started': type = AgentEventType.TASK_STARTED; break;
case 'task_progress': type = AgentEventType.TASK_PROGRESS; break;
case 'task_completed': type = AgentEventType.TASK_COMPLETED; break;
case 'task_failed': type = AgentEventType.TASK_FAILED; break;
case 'payment_requested': type = AgentEventType.PAYMENT_REQUESTED; break;
default: return null;
}
return { type, payload: { agentId: data.agent_id ?? agentId, taskId: data.task_id, sessionId: data.session_id, walletAddress: data.sender ?? data.wallet, message: data.message ?? data.content, data: data.result ?? data.params, txHash: data.tx_hash, timestamp: Date.now() } };
}
}
export const agentEventManager = new AgentEventManager();
6.3 实时事件展示组件
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { agentEventManager, AgentEvent, AgentEventType } from '../lib/agentEvents';
import { wsManager, WsConnectionState } from '../lib/websocketManager';
interface EventFeedProps {
agentId?: string;
walletAddress?: string;
taskId?: string;
maxEvents?: number;
}
const EVENT_LABELS: Record<AgentEventType, string> = {
[AgentEventType.AGENT_ONLINE]: 'Agent 上线',
[AgentEventType.AGENT_OFFLINE]: 'Agent 下线',
[AgentEventType.AGENT_REPUTATION_CHANGE]: '信誉分变更',
[AgentEventType.MESSAGE_SENT]: '消息已发送',
[AgentEventType.MESSAGE_RECEIVED]: '消息已接收',
[AgentEventType.MESSAGE_ERROR]: '消息错误',
[AgentEventType.TASK_CREATED]: '任务已创建',
[AgentEventType.TASK_STARTED]: '任务已开始',
[AgentEventType.TASK_PROGRESS]: '任务进度',
[AgentEventType.TASK_COMPLETED]: '任务已完成',
[AgentEventType.TASK_FAILED]: '任务失败',
[AgentEventType.TASK_CANCELLED]: '任务已取消',
[AgentEventType.PAYMENT_REQUESTED]: '支付请求',
[AgentEventType.PAYMENT_CONFIRMED]: '支付确认',
[AgentEventType.PAYMENT_FAILED]: '支付失败',
[AgentEventType.AGENT_LOG]: 'Agent 日志',
[AgentEventType.AGENT_ERROR]: 'Agent 错误',
[AgentEventType.AGENT_WARNING]: 'Agent 警告',
};
const EVENT_COLORS: Record<AgentEventType, string> = {
[AgentEventType.AGENT_ONLINE]: '#22c55e',
[AgentEventType.AGENT_OFFLINE]: '#ef4444',
[AgentEventType.AGENT_REPUTATION_CHANGE]: '#f59e0b',
[AgentEventType.MESSAGE_SENT]: '#3b82f6',
[AgentEventType.MESSAGE_RECEIVED]: '#8b5cf6',
[AgentEventType.MESSAGE_ERROR]: '#f97316',
[AgentEventType.TASK_CREATED]: '#06b6d4',
[AgentEventType.TASK_STARTED]: '#3b82f6',
[AgentEventType.TASK_PROGRESS]: '#f59e0b',
[AgentEventType.TASK_COMPLETED]: '#22c55e',
[AgentEventType.TASK_FAILED]: '#ef4444',
[AgentEventType.TASK_CANCELLED]: '#6b7280',
[AgentEventType.PAYMENT_REQUESTED]: '#ec4899',
[AgentEventType.PAYMENT_CONFIRMED]: '#22c55e',
[AgentEventType.PAYMENT_FAILED]: '#ef4444',
[AgentEventType.AGENT_LOG]: '#6b7280',
[AgentEventType.AGENT_ERROR]: '#ef4444',
[AgentEventType.AGENT_WARNING]: '#f59e0b',
};
export const EventFeed: React.FC<EventFeedProps> = ({ agentId, walletAddress, taskId, maxEvents = 100 }) => {
const [events, setEvents] = useState<AgentEvent[]>([]);
const [connectionState, setConnectionState] = useState<WsConnectionState>('disconnected');
const feedRef = useRef<HTMLDivElement>(null);
const autoScroll = useRef(true);
useEffect(() => {
const unsubState = wsManager.onStateChange(setConnectionState);
agentEventManager.connect();
if (agentId) agentEventManager.subscribeToAgent(agentId);
if (walletAddress) agentEventManager.subscribeToUser(walletAddress);
if (taskId) agentEventManager.subscribeToTasks(taskId);
const unsubList: (() => void)[] = [];
Object.values(AgentEventType).forEach((type) => {
const unsub = agentEventManager.on(type, (event) => {
const data = event.data as Record<string, any>;
const agentEvent: AgentEvent = {
type,
payload: {
agentId: data?.agent_id ?? agentId,
taskId: data?.task_id ?? taskId,
message: data?.message ?? data?.content,
data: data?.result ?? data?.params,
txHash: data?.tx_hash,
timestamp: Date.now(),
},
};
setEvents((prev) => [agentEvent, ...prev].slice(0, maxEvents));
});
unsubList.push(unsub);
});
return () => { unsubList.forEach((fn) => fn()); agentEventManager.disconnect(); unsubState(); };
}, [agentId, walletAddress, taskId, maxEvents]);
const handleScroll = () => {
if (!feedRef.current) return;
autoScroll.current = feedRef.current.scrollTop < 50;
};
useEffect(() => { if (autoScroll.current && feedRef.current) feedRef.current.scrollTop = 0; }, [events]);
const clearEvents = useCallback(() => setEvents([]), []);
const formatTime = (ts: number): string => new Date(ts).toLocaleTimeString('zh-CN');
return (
<div className="event-feed">
<div className="event-feed-header">
<h3>实时事件流</h3>
<div className="event-feed-controls">
<span className={'connection-indicator ' + connectionState}>
{connectionState === 'connected' ? '已连接' : connectionState === 'connecting' ? '连接中' : '未连接'}
</span>
<button onClick={clearEvents} type="button" className="clear-btn">清空</button>
</div>
</div>
<div className="event-feed-body" ref={feedRef} onScroll={handleScroll}>
{events.length === 0 ? (
<div className="event-feed-empty"><p>等待事件...</p><p className="hint">执行任务或与 Agent 交互后将显示事件</p></div>
) : (
events.map((event, idx) => (
<div key={event.type + '_' + event.payload.timestamp + '_' + idx} className="event-item" style={{ borderLeftColor: EVENT_COLORS[event.type] ?? '#6b7280' }}>
<div className="event-icon">{event.type.startsWith('agent:') ? 'A' : event.type.startsWith('task:') ? 'T' : 'M'}</div>
<div className="event-content">
<div className="event-header">
<span className="event-type" style={{ color: EVENT_COLORS[event.type] ?? '#6b7280' }}>{EVENT_LABELS[event.type] ?? event.type}</span>
<span className="event-time">{formatTime(event.payload.timestamp)}</span>
</div>
<p className="event-message">{event.payload.message ?? JSON.stringify(event.payload.data)}</p>
{event.payload.txHash && <a className="event-tx-link" href={'https://explorer.msgchain.org/tx/' + event.payload.txHash} target="_blank" rel="noopener noreferrer">查看交易</a>}
</div>
</div>
))
)}
</div>
<div className="event-feed-footer">
<span>共 {events.length} 条事件</span>
{connectionState === 'connected' && <span className="live-indicator">* 实时</span>}
</div>
</div>
);
};
6.4 自定义 Hooks 封装
import { useState, useEffect, useCallback } from 'react';
import { agentEventManager, AgentEvent, AgentEventType } from '../lib/agentEvents';
import { wsManager, WsConnectionState } from '../lib/websocketManager';
export const useAgentEvents = (agentId?: string) => {
const [events, setEvents] = useState<AgentEvent[]>([]);
const [connectionState, setConnectionState] = useState<WsConnectionState>('disconnected');
useEffect(() => { const unsub = wsManager.onStateChange(setConnectionState); return unsub; }, []);
useEffect(() => {
agentEventManager.connect();
if (agentId) agentEventManager.subscribeToAgent(agentId);
return () => { agentEventManager.disconnect(); };
}, [agentId]);
useEffect(() => {
const handler = (event: WsEvent) => {
const data = event.data as Record<string, any>;
if (data?.agent_id === agentId || !agentId) {
setEvents((prev) => [{ type: data?.action ?? 'unknown', payload: { agentId: data?.agent_id, message: data?.message, data: data?.result, timestamp: Date.now() } } as AgentEvent, ...prev].slice(0, 50));
}
};
const unsub = agentEventManager.on(AgentEventType.MESSAGE_RECEIVED, handler as any);
return () => unsub();
}, [agentId]);
const clear = useCallback(() => setEvents([]), []);
return { events, connectionState, clear };
};
export const useTaskEvents = (taskId: string) => {
const [events, setEvents] = useState<AgentEvent[]>([]);
const [lastStatus, setLastStatus] = useState<string>('pending');
useEffect(() => {
agentEventManager.connect();
agentEventManager.subscribeToTasks(taskId);
const unsub = agentEventManager.on(AgentEventType.TASK_PROGRESS, (event) => {
const data = event.data as Record<string, any>;
setEvents((prev) => [...prev, { type: AgentEventType.TASK_PROGRESS, payload: { taskId, message: data?.message, data: data?.progress, timestamp: Date.now() } }]);
});
const unsub2 = agentEventManager.on(AgentEventType.TASK_COMPLETED, () => setLastStatus('completed'));
const unsub3 = agentEventManager.on(AgentEventType.TASK_FAILED, () => setLastStatus('failed'));
return () => { unsub(); unsub2(); unsub3(); agentEventManager.disconnect(); };
}, [taskId]);
return { events, lastStatus };
};
7. 移动端集成
7.1 React Native 钱包连接
import { useState, useCallback, useEffect } from 'react';
import { Linking, Platform } from 'react-native';
interface WalletConnectSession {
uri: string;
address: string;
chainId: string;
}
export const useKeplrWalletRN = () => {
const [address, setAddress] = useState<string | null>(null);
const [isConnected, setIsConnected] = useState(false);
const [connecting, setConnecting] = useState(false);
const connectKeplrMobile = useCallback(async () => {
setConnecting(true);
try {
const wcUri = 'keplrwallet://wc?uri=...';
const supported = await Linking.canOpenURL(wcUri);
if (supported) {
await Linking.openURL(wcUri);
} else {
await Linking.openURL(
Platform.OS === 'ios'
? 'https://apps.apple.com/app/keplr-wallet/id1567851089'
: 'https://play.google.com/store/apps/details?id=com.keplr.app'
);
}
} catch (err) {
console.error('Keplr 移动端连接失败:', err);
throw err;
} finally { setConnecting(false); }
}, []);
const disconnect = useCallback(() => { setAddress(null); setIsConnected(false); }, []);
return { address, isConnected, connecting, connectKeplrMobile, disconnect };
};
7.2 React Native Agent 聊天组件
import React, { useState, useCallback, useRef } from 'react';
import {
View, Text, TextInput, TouchableOpacity, FlatList, StyleSheet,
SafeAreaView, KeyboardAvoidingView, Platform,
} from 'react-native';
import { useAuth } from '../providers/AuthProvider';
import { AgentClient, AgentMessage } from '../lib/agentClient';
interface AgentChatScreenProps {
agentId: string;
agentName: string;
onClose: () => void;
}
export const AgentChatScreen: React.FC<AgentChatScreenProps> = ({ agentId, agentName, onClose }) => {
const { cosmClient, address, session } = useAuth();
const [messages, setMessages] = useState<AgentMessage[]>([]);
const [inputText, setInputText] = useState('');
const [sending, setSending] = useState(false);
const flatListRef = useRef<FlatList>(null);
const agentClient = cosmClient && address ? new AgentClient(cosmClient, address) : null;
const sessionId = session?.sessionId ?? 'mobile_session';
const sendMessage = useCallback(async () => {
if (!agentClient || !inputText.trim() || sending) return;
const text = inputText.trim();
setInputText('');
setSending(true);
const userMsg: AgentMessage = { id: 'user_' + Date.now(), role: 'user', content: text, timestamp: Date.now() };
setMessages((prev) => [...prev, userMsg]);
try {
const response = await agentClient.sendMessage(agentId, text, sessionId);
setMessages((prev) => [...prev, response]);
} catch {
setMessages((prev) => [...prev, { id: 'err_' + Date.now(), role: 'system', content: '发送失败,请重试', timestamp: Date.now() }]);
} finally { setSending(false); }
}, [agentClient, agentId, inputText, sending, sessionId]);
const renderMessage = ({ item }: { item: AgentMessage }) => (
<View style={[styles.messageRow, item.role === 'user' ? styles.userRow : styles.agentRow]}>
{item.role !== 'user' && <View style={styles.avatar}><Text style={styles.avatarText}>AI</Text></View>}
<View style={[styles.bubble, item.role === 'user' ? styles.userBubble : styles.agentBubble, item.role === 'system' && styles.systemBubble]}>
<Text style={styles.bubbleText}>{item.content}</Text>
<Text style={styles.timestamp}>{new Date(item.timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}</Text>
</View>
{item.role === 'user' && <View style={styles.avatar}><Text style={styles.avatarText}>{address?.slice(2, 4).toUpperCase() ?? 'U'}</Text></View>}
</View>
);
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={onClose}><Text style={styles.backBtn}>{'<-'}</Text></TouchableOpacity>
<View><Text style={styles.headerTitle}>{agentName}</Text><Text style={styles.headerStatus}>在线</Text></View>
</View>
<KeyboardAvoidingView style={styles.flex} behavior={Platform.OS === 'ios' ? 'padding' : undefined}>
<FlatList ref={flatListRef} data={messages} renderItem={renderMessage} keyExtractor={(item) => item.id}
style={styles.messageList} onContentSizeChange={() => flatListRef.current?.scrollToEnd({ animated: true })} />
<View style={styles.inputBar}>
<TextInput style={styles.input} value={inputText} onChangeText={setInputText} placeholder="输入消息..." multiline maxLength={2000} editable={!sending} />
<TouchableOpacity style={[styles.sendBtn, (!inputText.trim() || sending) && styles.sendBtnDisabled]} onPress={sendMessage} disabled={!inputText.trim() || sending}>
<Text style={styles.sendBtnText}>{sending ? '...' : '发送'}</Text>
</TouchableOpacity>
</View>
</KeyboardAvoidingView>
</SafeAreaView>
);
};
const styles = StyleSheet.create({
container: { flex: 1, backgroundColor: '#f8fafc' },
flex: { flex: 1 },
header: { flexDirection: 'row', alignItems: 'center', padding: 16, borderBottomWidth: 1, borderBottomColor: '#e2e8f0', backgroundColor: '#fff' },
backBtn: { fontSize: 24, marginRight: 12, color: '#3b82f6' },
headerTitle: { fontSize: 18, fontWeight: '600', color: '#0f172a' },
headerStatus: { fontSize: 12, color: '#22c55e' },
messageList: { flex: 1, padding: 12 },
messageRow: { flexDirection: 'row', marginBottom: 12, alignItems: 'flex-end' },
userRow: { justifyContent: 'flex-end' },
agentRow: { justifyContent: 'flex-start' },
avatar: { width: 32, height: 32, borderRadius: 16, backgroundColor: '#3b82f6', justifyContent: 'center', alignItems: 'center', marginHorizontal: 8 },
avatarText: { color: '#fff', fontSize: 12, fontWeight: '600' },
bubble: { maxWidth: '70%', padding: 12, borderRadius: 16 },
userBubble: { backgroundColor: '#3b82f6', borderBottomRightRadius: 4 },
agentBubble: { backgroundColor: '#fff', borderBottomLeftRadius: 4, borderWidth: 1, borderColor: '#e2e8f0' },
systemBubble: { backgroundColor: '#f1f5f9', alignSelf: 'center', maxWidth: '90%' },
bubbleText: { fontSize: 15, color: '#0f172a', lineHeight: 22 },
timestamp: { fontSize: 11, color: '#94a3b8', marginTop: 4, textAlign: 'right' },
inputBar: { flexDirection: 'row', padding: 12, borderTopWidth: 1, borderTopColor: '#e2e8f0', backgroundColor: '#fff', alignItems: 'flex-end' },
input: { flex: 1, minHeight: 40, maxHeight: 100, borderWidth: 1, borderColor: '#e2e8f0', borderRadius: 20, paddingHorizontal: 16, paddingVertical: 8, fontSize: 15, backgroundColor: '#f8fafc' },
sendBtn: { marginLeft: 8, backgroundColor: '#3b82f6', borderRadius: 20, paddingHorizontal: 20, paddingVertical: 10 },
sendBtnDisabled: { backgroundColor: '#93c5fd' },
sendBtnText: { color: '#fff', fontWeight: '600', fontSize: 14 },
});
7.3 Flutter SDK 集成
import 'package:flutter/material.dart';
import 'package:cosmjs_flutter/cosmjs_flutter.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'dart:convert';
class MsgChainAgent {
final String rpcEndpoint = 'https://rpc.msgchain.org';
final String chainId = 'msg-chain-1';
final String prefix = 'msg';
late SigningStargateClient client;
String? walletAddress;
Future<void> connectWallet(String mnemonic) async {
final signer = await DirectSecp256k1HdWallet.fromMnemonic(mnemonic, prefix);
client = await SigningStargateClient.connectWithSigner(rpcEndpoint, signer);
final accounts = await signer.getAccounts();
walletAddress = accounts.first.address;
}
Future<Map<String, dynamic>> queryAgent(String agentId) async {
return await client.queryContractSmart(
'msg1registrycontractaddressxxxxxxxxxxxxxxxxxx',
{'get_agent': {'agent_id': agentId}},
);
}
Future<String> sendMessage(String agentId, String content) async {
final msg = {'send_message': {'agent_id': agentId, 'content': content, 'sender': walletAddress}};
final result = await client.execute(
walletAddress!,
'msg1agentexecutorcontractaddressxxxxxxxxxxxxx',
msg,
fee: const Fee(amount: [], gas: '300000'),
);
return result.transactionHash;
}
}
class AgentCard extends StatelessWidget {
final Map<String, dynamic> agent;
const AgentCard({super.key, required this.agent});
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
child: InkWell(
onTap: () => Navigator.push(context, MaterialPageRoute(builder: (_) => AgentDetailScreen(agent: agent))),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(children: [
CircleAvatar(radius: 24, backgroundImage: NetworkImage(agent['avatar'] ?? ''), child: Text(agent['name']?[0] ?? '?')),
const SizedBox(width: 12),
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Text(agent['name'] ?? 'Unknown', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Text(agent['status'] == 'active' ? '在线' : '离线', style: TextStyle(color: agent['status'] == 'active' ? Colors.green : Colors.grey)),
]),
]),
const SizedBox(height: 8),
Text(agent['description'] ?? ''),
const SizedBox(height: 8),
Wrap(spacing: 6, runSpacing: 4, children: (agent['capabilities'] as List? ?? []).take(3).map<Widget>((cap) => Chip(label: Text(cap.toString()), materialTapTargetSize: MaterialTapTargetSize.shrinkWrap)).toList()),
const SizedBox(height: 8),
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
Text('信誉: ${agent['reputation'] ?? 0}/100'),
Text('价格: ${agent['price'] ?? 0} AIMP', style: const TextStyle(fontWeight: FontWeight.w600)),
]),
],
),
),
),
);
}
}
class AgentDetailScreen extends StatelessWidget {
final Map<String, dynamic> agent;
const AgentDetailScreen({super.key, required this.agent});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(agent['name'] ?? 'Agent 详情')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(child: CircleAvatar(radius: 48, backgroundImage: NetworkImage(agent['avatar'] ?? ''), child: Text(agent['name']?[0] ?? '?'))),
const SizedBox(height: 16),
Text(agent['name'] ?? '', style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
Text(agent['description'] ?? ''),
const SizedBox(height: 24),
const Text('能力', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)),
const SizedBox(height: 8),
Wrap(spacing: 8, runSpacing: 4, children: (agent['capabilities'] as List? ?? []).map<Widget>((cap) => Chip(label: Text(cap.toString()))).toList()),
const SizedBox(height: 24),
SizedBox(width: double.infinity, child: ElevatedButton.icon(
onPressed: () => Navigator.push(context, MaterialPageRoute(builder: (_) => AgentChatScreen(agentId: agent['id']))),
icon: const Icon(Icons.chat), label: const Text('与 Agent 对话'),
style: ElevatedButton.styleFrom(padding: const EdgeInsets.all(16)),
)),
],
),
),
);
}
}
class AgentChatScreen extends StatefulWidget {
final String agentId;
const AgentChatScreen({super.key, required this.agentId});
@override
State<AgentChatScreen> createState() => _AgentChatScreenState();
}
class _AgentChatScreenState extends State<AgentChatScreen> {
final TextEditingController _controller = TextEditingController();
final List<Map<String, String>> _messages = [];
final MsgChainAgent _agent = MsgChainAgent();
bool _sending = false;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Agent 对话')),
body: Column(children: [
Expanded(child: ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: _messages.length,
itemBuilder: (context, index) {
final msg = _messages[index];
final isUser = msg['role'] == 'user';
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(color: isUser ? Colors.blue[100] : Colors.grey[200], borderRadius: BorderRadius.circular(12)),
child: Text(msg['content'] ?? ''),
),
);
},
)),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(color: Colors.white, boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.3), blurRadius: 4, offset: const Offset(0, -2))]),
child: Row(children: [
Expanded(child: TextField(controller: _controller, decoration: const InputDecoration(hintText: '输入消息...', border: OutlineInputBorder()), maxLines: 3, minLines: 1)),
const SizedBox(width: 8),
IconButton(onPressed: _sending ? null : _sendMessage, icon: const Icon(Icons.send), color: Colors.blue),
]),
),
]),
);
}
void _sendMessage() async {
final text = _controller.text.trim();
if (text.isEmpty) return;
setState(() { _messages.add({'role': 'user', 'content': text}); _sending = true; });
_controller.clear();
try {
final txHash = await _agent.sendMessage(widget.agentId, text);
setState(() { _messages.add({'role': 'agent', 'content': '消息已上链 (tx: ${txHash.substring(0, 16)}...)'}); });
} catch (e) {
setState(() { _messages.add({'role': 'system', 'content': '发送失败: $e'}); });
} finally { setState(() => _sending = false); }
}
}
7.4 推送通知
import { Platform } from 'react-native';
export interface PushNotificationPayload {
title: string;
body: string;
data: { agentId?: string; taskId?: string; eventType: string; timestamp: number };
}
export class AgentPushNotifications {
private static instance: AgentPushNotifications;
private token: string | null = null;
static getInstance(): AgentPushNotifications {
if (!AgentPushNotifications.instance) AgentPushNotifications.instance = new AgentPushNotifications();
return AgentPushNotifications.instance;
}
async register(): Promise<string | null> {
try {
return this.token;
} catch (err) { console.error('推送注册失败:', err); return null; }
}
async handleNotificationPress(notification: PushNotificationPayload): Promise<void> {
if (notification.data.taskId) { }
else if (notification.data.agentId) { }
}
async requestPermission(): Promise<boolean> { return true; }
}
8. 完整 dApp 示例
8.1 Agent Market Explorer
一个完整的 React dApp —— Agent 市场浏览器,包含钱包连接、Agent 发现、聊天交互和任务管理。
// src/App.tsx
import React from 'react';
import { AuthProvider } from './providers/AuthProvider';
import { AgentInteractionManager } from './components/AgentInteractionManager';
import { AgentMarket } from './pages/AgentMarket';
import { AgentDashboard } from './components/AgentDashboard';
import { EventFeed } from './components/EventFeed';
import { useAuth } from './providers/AuthProvider';
const AppContent: React.FC = () => {
const { address, isConnected } = useAuth();
const [activeTab, setActiveTab] = useState<'market' | 'dashboard' | 'events'>('market');
return (
<div className="app-container">
<nav className="app-nav">
<div className="app-logo">MSG Agent Explorer</div>
<div className="app-tabs">
<button className={'tab ' + (activeTab === 'market' ? 'active' : '')} onClick={() => setActiveTab('market')} type="button">Agent 市场</button>
<button className={'tab ' + (activeTab === 'dashboard' ? 'active' : '')} onClick={() => setActiveTab('dashboard')} type="button" disabled={!isConnected}>任务面板</button>
<button className={'tab ' + (activeTab === 'events' ? 'active' : '')} onClick={() => setActiveTab('events')} type="button">实时事件</button>
</div>
<WalletConnect />
</nav>
<main className="app-main">
{activeTab === 'market' && <AgentMarket />}
{activeTab === 'dashboard' && <AgentDashboard />}
{activeTab === 'events' && (
<div className="events-page">
<h1>实时事件流</h1>
<EventFeed walletAddress={address ?? undefined} maxEvents={50} />
</div>
)}
</main>
</div>
);
};
export const App: React.FC = () => (
<AuthProvider>
<AgentInteractionManager>
<AppContent />
</AgentInteractionManager>
</AuthProvider>
);
// src/styles.css (核心样式)
/* 布局 */
.app-container { min-height: 100vh; background: #f8fafc; }
.app-nav { display: flex; align-items: center; justify-content: space-between; padding: 12px 24px; background: #fff; border-bottom: 1px solid #e2e8f0; }
.app-logo { font-size: 20px; font-weight: 700; color: #3b82f6; }
.app-tabs { display: flex; gap: 4px; }
.app-tabs .tab { padding: 8px 16px; border: none; background: transparent; cursor: pointer; border-radius: 6px; font-size: 14px; color: #64748b; }
.app-tabs .tab.active { background: #3b82f6; color: #fff; }
.app-tabs .tab:disabled { opacity: 0.5; cursor: not-allowed; }
.app-main { max-width: 1200px; margin: 0 auto; padding: 24px; }
/* 钱包连接 */
.wallet-connected { display: flex; align-items: center; gap: 12px; }
.wallet-info { display: flex; align-items: center; gap: 8px; padding: 6px 12px; background: #f0fdf4; border: 1px solid #bbf7d0; border-radius: 8px; }
.wallet-indicator { width: 8px; height: 8px; background: #22c55e; border-radius: 50%; }
.wallet-address { font-family: monospace; font-size: 14px; color: #166534; }
.wallet-balance { font-size: 13px; color: #16a34a; }
.wallet-disconnect-btn { padding: 6px 12px; border: 1px solid #fca5a5; background: #fef2f2; color: #dc2626; border-radius: 6px; cursor: pointer; font-size: 13px; }
.wallet-connect-btn { padding: 8px 20px; background: #3b82f6; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }
/* Agent 网格 */
.agent-market { }
.market-header { text-align: center; margin-bottom: 24px; }
.market-header h1 { font-size: 28px; color: #0f172a; margin: 0; }
.market-header p { color: #64748b; margin-top: 4px; }
.agent-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); gap: 16px; }
/* Agent 卡片 */
.agent-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 20px; cursor: pointer; transition: all 0.2s; }
.agent-card:hover { box-shadow: 0 4px 12px rgba(0,0,0,0.08); border-color: #3b82f6; }
.agent-card-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; }
.agent-avatar { width: 48px; height: 48px; border-radius: 50%; object-fit: cover; background: #e2e8f0; }
.agent-name { margin: 0; font-size: 16px; font-weight: 600; color: #0f172a; }
.agent-status { font-size: 13px; font-weight: 500; }
.agent-description { font-size: 14px; color: #64748b; margin-bottom: 12px; line-height: 1.5; }
.agent-capabilities { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
.capability-tag { padding: 3px 8px; background: #eff6ff; color: #3b82f6; border-radius: 4px; font-size: 12px; font-weight: 500; }
.capability-more { padding: 3px 8px; color: #94a3b8; font-size: 12px; }
.agent-card-meta { display: flex; justify-content: space-between; margin-bottom: 8px; }
.meta-label { display: block; font-size: 11px; color: #94a3b8; text-transform: uppercase; }
.meta-value { font-size: 14px; font-weight: 600; color: #0f172a; }
.agent-card-stats { display: flex; gap: 16px; padding: 8px 0; border-top: 1px solid #f1f5f9; margin-bottom: 8px; }
.stat { text-align: center; flex: 1; }
.stat-value { display: block; font-size: 18px; font-weight: 700; color: #0f172a; }
.stat-label { font-size: 12px; color: #94a3b8; }
.agent-card-footer { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: #94a3b8; margin-bottom: 12px; }
.agent-interact-btn { width: 100%; padding: 10px; background: #3b82f6; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; }
/* 聊天界面 */
.agent-chat { display: flex; flex-direction: column; height: 600px; background: #fff; border-radius: 12px; overflow: hidden; }
.chat-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid #e2e8f0; }
.chat-header-info { display: flex; align-items: center; gap: 12px; }
.chat-agent-avatar { width: 36px; height: 36px; border-radius: 50%; background: #3b82f6; color: #fff; display: flex; align-items: center; justify-content: center; font-weight: 700; }
.chat-messages { flex: 1; overflow-y: auto; padding: 16px; display: flex; flex-direction: column; gap: 12px; }
.chat-message { display: flex; gap: 8px; }
.chat-message--user { flex-direction: row-reverse; }
.chat-message--system { justify-content: center; }
.message-avatar { width: 28px; height: 28px; border-radius: 50%; background: #3b82f6; color: #fff; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; flex-shrink: 0; }
.message-bubble { padding: 10px 14px; border-radius: 12px; font-size: 14px; line-height: 1.5; background: #f1f5f9; color: #0f172a; }
.chat-message--user .message-bubble { background: #3b82f6; color: #fff; }
.chat-message--system .message-bubble { background: #f8fafc; border: 1px solid #e2e8f0; font-size: 13px; color: #64748b; }
.message-meta { display: flex; gap: 8px; margin-top: 4px; font-size: 11px; }
.message-time { color: #94a3b8; }
.message-tx { color: #3b82f6; text-decoration: none; }
.chat-input-bar { display: flex; gap: 8px; padding: 12px 16px; border-top: 1px solid #e2e8f0; }
.chat-input { flex: 1; padding: 10px 14px; border: 1px solid #e2e8f0; border-radius: 8px; resize: none; font-size: 14px; outline: none; }
.chat-send-btn { padding: 10px 20px; background: #3b82f6; color: #fff; border: none; border-radius: 8px; cursor: pointer; font-weight: 500; }
.chat-send-btn:disabled { background: #93c5fd; cursor: not-allowed; }
/* 任务面板 */
.agent-dashboard { }
.dashboard-header { margin-bottom: 24px; }
.dashboard-tabs { display: flex; gap: 4px; margin-bottom: 16px; }
.tab-btn { padding: 8px 16px; border: 1px solid #e2e8f0; background: #fff; cursor: pointer; border-radius: 6px; font-size: 14px; color: #64748b; }
.tab-btn--active { background: #3b82f6; color: #fff; border-color: #3b82f6; }
.dashboard-summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 24px; }
.summary-card { background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; padding: 16px; text-align: center; }
.summary-value { display: block; font-size: 24px; font-weight: 700; color: #0f172a; }
.summary-label { font-size: 13px; color: #64748b; }
/* 任务行 */
.task-row { display: flex; align-items: center; gap: 12px; padding: 12px 16px; background: #fff; border: 1px solid #e2e8f0; border-radius: 8px; margin-bottom: 8px; cursor: pointer; }
.task-row:hover { border-color: #3b82f6; }
.task-status-icon { font-size: 20px; width: 24px; text-align: center; }
.task-row-info { flex: 1; }
.task-row-title { font-size: 15px; font-weight: 500; color: #0f172a; margin-bottom: 4px; }
.task-row-meta { display: flex; gap: 12px; font-size: 13px; color: #94a3b8; }
.task-row-right { text-align: right; }
.task-status-badge { display: inline-block; padding: 3px 8px; border-radius: 4px; font-size: 12px; font-weight: 500; margin-bottom: 4px; }
.task-cost { display: block; font-size: 13px; font-weight: 600; color: #0f172a; }
/* 任务详情 */
.task-detail { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; padding: 24px; }
.task-detail-header { display: flex; justify-content: space-between; margin-bottom: 20px; }
.back-btn { padding: 6px 12px; background: transparent; border: 1px solid #e2e8f0; border-radius: 6px; cursor: pointer; font-size: 14px; color: #64748b; }
.task-section { margin-bottom: 24px; }
.task-progress-bar { height: 24px; background: #e2e8f0; border-radius: 12px; overflow: hidden; position: relative; }
.task-progress-fill { height: 100%; transition: width 0.5s; border-radius: 12px; }
.task-progress-label { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); font-size: 12px; font-weight: 600; color: #0f172a; }
.task-info-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
.task-info-item { padding: 8px; background: #f8fafc; border-radius: 6px; }
.task-info-item label { display: block; font-size: 12px; color: #94a3b8; margin-bottom: 2px; }
.task-info-item span { font-size: 14px; color: #0f172a; }
/* 时间线 */
.event-timeline { position: relative; padding-left: 20px; }
.timeline-item { position: relative; padding-bottom: 16px; }
.timeline-dot { position: absolute; left: -16px; top: 4px; width: 10px; height: 10px; border-radius: 50%; background: #3b82f6; border: 2px solid #fff; }
.timeline-content { background: #f8fafc; padding: 12px; border-radius: 8px; }
.timeline-header { display: flex; justify-content: space-between; margin-bottom: 4px; }
.timeline-type { font-size: 13px; font-weight: 600; color: #3b82f6; text-transform: capitalize; }
.timeline-time { font-size: 12px; color: #94a3b8; }
/* 实时事件流 */
.event-feed { background: #fff; border: 1px solid #e2e8f0; border-radius: 12px; overflow: hidden; }
.event-feed-header { display: flex; justify-content: space-between; align-items: center; padding: 16px; border-bottom: 1px solid #e2e8f0; }
.event-feed-header h3 { margin: 0; font-size: 16px; color: #0f172a; }
.event-feed-controls { display: flex; align-items: center; gap: 12px; }
.connection-indicator { font-size: 12px; padding: 3px 8px; border-radius: 4px; background: #f1f5f9; color: #64748b; }
.event-feed-body { max-height: 500px; overflow-y: auto; padding: 12px; }
.event-item { display: flex; gap: 10px; padding: 10px; border-left: 3px solid; margin-bottom: 8px; background: #f8fafc; border-radius: 0 6px 6px 0; }
.event-icon { width: 24px; text-align: center; font-size: 14px; }
.event-header { display: flex; justify-content: space-between; margin-bottom: 2px; }
.event-type { font-size: 12px; font-weight: 600; text-transform: uppercase; }
.event-time { font-size: 11px; color: #94a3b8; }
/* 交易确认弹窗 */
.tx-approval-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 100; }
.tx-approval-modal { background: #fff; border-radius: 16px; padding: 24px; width: 480px; max-width: 90vw; }
.tx-approval-header { text-align: center; margin-bottom: 20px; }
.tx-approval-icon { width: 48px; height: 48px; border-radius: 50%; background: #fef2f2; color: #dc2626; display: flex; align-items: center; justify-content: center; margin: 0 auto 12px; font-size: 24px; }
.tx-field { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f1f5f9; }
.tx-field label { font-size: 13px; color: #64748b; }
.tx-field span { font-size: 14px; color: #0f172a; font-weight: 500; }
.tx-amount { font-size: 18px; font-weight: 700; color: #dc2626; }
.tx-address { font-family: monospace; font-size: 13px; }
.tx-approval-actions { display: flex; gap: 12px; margin-top: 20px; }
.tx-reject-btn, .tx-confirm-btn { flex: 1; padding: 12px; border-radius: 8px; border: none; cursor: pointer; font-size: 15px; font-weight: 500; }
.tx-reject-btn { background: #f1f5f9; color: #64748b; }
.tx-confirm-btn { background: #3b82f6; color: #fff; }
/* 加载骨架屏 */
.agent-card-skeleton, .task-row-skeleton { height: 180px; background: linear-gradient(90deg, #f1f5f9 25%, #e2e8f0 50%, #f1f5f9 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: 12px; margin-bottom: 8px; }
.task-row-skeleton { height: 60px; }
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
/* 状态提示 */
.hint { font-size: 12px; color: #94a3b8; }
.detail-loading, .detail-not-found { text-align: center; padding: 40px; color: #64748b; }
.task-list-empty { text-align: center; padding: 60px 20px; color: #64748b; }
.task-list-empty .hint { margin-top: 8px; }
8.2 项目结构
src/
components/
AgentCard.tsx # Agent 卡片
AgentChat.tsx # 聊天界面
AgentDashboard.tsx # 任务仪表板
AgentDetail.tsx # Agent 详情
AgentInteractionManager.tsx # 交互管理器
AgentList.tsx # Agent 列表
CapabilityFilter.tsx # 能力筛选器
EventFeed.tsx # 实时事件流
PaymentApproval.tsx # 支付批准
TaskCreateForm.tsx # 任务创建表单
TaskDetail.tsx # 任务详情
TaskList.tsx # 任务列表
TransactionApproval.tsx # 交易确认
WalletConnect.tsx # 钱包连接
hooks/
useKeplr.ts # Keplr hook
useAgentEvents.ts # 事件 hook
lib/
agentClient.ts # Agent 通信客户端
agentEvents.ts # 事件管理
agentIdentity.ts # 身份绑定
agentRegistry.ts # Agent 注册表查询
cosmClient.ts # CosmJS 客户端
session.ts # 会话管理
taskClient.ts # 任务管理客户端
websocketManager.ts # WebSocket 管理器
pages/
AgentMarket.tsx # Agent 市场页面
providers/
AuthProvider.tsx # 全局认证上下文
App.tsx # 根组件
styles.css # 全局样式
8.3 部署检查清单
| 项目 | 检查项 |
|---|---|
| 钱包 | Keplr/Leap 安装检测、msg-chain-1 网络配置、地址格式 msg1... |
| 合约地址 | 注册表合约、执行器合约、任务管理器合约地址正确配置 |
| RPC 端点 | 主网 RPC、WSS 端点可用性、fallback 节点配置 |
| Gas 费用 | gasPrice 步进配置 (500000000/1000000000/2000000000 attoMSG) |
| 安全性 | 交易签名确认弹窗、预算上限控制、会话过期处理 |
| 用户体验 | 加载骨架屏、空状态提示、错误处理、重连机制 |
| 移动端 | Flutter/RN 组件适配、Deep Link 钱包唤醒、推送通知 |
| 部署 | Vercel/Netlify 环境变量、CORS 配置、CDN 缓存策略 |
附录
完整代码仓库: https://github.com/msgchain/agent-frontend-starter
MSG Chain 开发者文档: https://docs.msgchain.org
CosmJS 参考: https://cosmjs.tech
Keplr Wallet: https://www.keplr.app
License: MIT
