dApp Docs/AI Agent 前端与dApp集成指南
Development reference. Not independently verified for production.

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. 概述
  2. 钱包连接与身份认证
  3. Agent 查询与展示
  4. Agent 交互界面
  5. Agent 任务面板
  6. 实时事件推送
  7. 移动端集成
  8. 完整 dApp 示例

1. 概述

1.1 为什么前端集成对 AI Agent 至关重要

AI Agent 是 MSG Chain 上的自主执行程序——它们可以分析数据、执行交易、生成内容,甚至与其他 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