dApp Docs/AI Agent 模板与快速开发脚手架
Development reference. Not independently verified for production.

AI Agent 模板与快速开发脚手架 — MSG Chain 实战指南

⚠️ No-Go Disclaimer: MSGChain 主网裁决为 No-Go。本文件所有内容反映的是开发阶段的技术设计,不代表主网未独立核验上线状态。生产部署状态请以白皮书为准:https://msgchain.org/whitepaper/

适用于 msg-chain-1 测试网 | 地址前缀: msg


目录

  1. 概述
  2. CLI 脚手架工具
  3. 模板:Agent 聊天机器人
  4. 模板:数据交易 Agent
  5. 模板:DeFi 套利 Agent
  6. 模板:监控与告警 Agent
  7. 模板:DAO 治理 Agent
  8. 模板开发指南

1. 概述

1.1 为什么需要模板

在 MSG Chain 上开发 AI Agent 涉及大量重复性工作:合约交互、A2A 通信协议、密钥管理、Gas 策略、事件监听、持久化存储等。模板与脚手架工具将这些问题标准化,让开发者聚焦于业务逻辑。

一个典型的 Agent 项目包含以下共性组件:

组件 说明
AgentBase 基础生命周期抽象(init / run / stop / cleanup)
A2AClient Agent-to-Agent 消息协议客户端
AgentRegistryClient 链上 Agent 注册与发现
WalletManager msg 前缀地址的钱包与签名管理
ContractClient 与 MSG Chain 智能合约交互的通用封装
ConfigLoader 多环境配置加载(.env / TOML / YAML)
Logger 结构化日志(JSON 格式,支持链上事件关联)

使用模板可以:

1.2 模板目录总览

当前官方维护的模板(@msg-chain/templates):

模板名称 描述 适用场景
chat-bot AI 聊天机器人 客服、社区管理、个人助理
data-trader 数据交易 Agent 数据集买卖、数据市场
market-maker DeFi 做市/套利 AMM 套利、闪电贷
monitor-alert 链上监控告警 价格监控、合约事件跟踪
dao-agent DAO 治理 投票委托、提案监控

1.3 前置要求

1.4 快速开始

# 安装脚手架
pip install msg-agent-cli

# 初始化项目
msg-agent init my-chat-bot --template chat-bot

# 进入项目
cd my-chat-bot

# 安装依赖
npm install

# 配置环境
cp .env.example .env
# 编辑 .env: PRIVATE_KEY, RPC_URL, AGENT_NAME

# 启动 Agent
npm run dev

2. CLI 脚手架工具

2.1 架构设计

CLI 工具 msg-agent 使用 Python 编写,采用 Click 框架实现命令解析。核心模块:

msg-agent-cli/
├── msg_agent/
│   ├── cli.py              # 入口 & Click 命令
│   ├── templates.py        # 模板管理(下载/缓存/列表)
│   ├── generator.py        # 项目文件生成器
│   ├── config.py           # 配置解析
│   ├── pkg_manager.py      # npm / pip 依赖安装
│   ├── docker_helper.py    # Docker 初始化辅助
│   ├── registry.py         # 远程模板仓库交互
│   └── validators.py       # 项目名校验
├── templates/              # 内建模板缓存
├── pyproject.toml
└── README.md

2.2 入口文件 — cli.py

#!/usr/bin/env python3
"""msg-agent CLI — AI Agent scaffolding tool for MSG Chain."""

import os
import sys
import json
from pathlib import Path
from typing import Optional

import click
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.prompt import Prompt, Confirm
from rich.progress import Progress, SpinnerColumn, TextColumn

from .templates import TemplateManager
from .generator import ProjectGenerator
from .config import load_config
from .validators import validate_project_name

console = Console()

ERRORS = {
    "invalid_name": "项目名仅支持小写字母、数字和连字符",
    "dir_exists": "目标目录已存在",
    "template_not_found": "未找到指定模板",
    "network_error": "网络错误,请检查连接",
}


@click.group()
@click.version_option(version="0.1.0", prog_name="msg-agent")
@click.option(
    "--config",
    "-c",
    type=click.Path(exists=True),
    help="配置文件路径",
)
@click.option(
    "--verbose",
    "-v",
    is_flag=True,
    help="详细输出模式",
)
def cli(config: Optional[str], verbose: bool) -> None:
    """MSG Chain AI Agent 脚手架工具

    使用预置模板快速创建 AI Agent 项目。
    """
    ctx = click.get_current_context()
    ctx.ensure_object(dict)
    ctx.obj["verbose"] = verbose

    if config:
        cfg = load_config(config)
        ctx.obj["config"] = cfg
        if verbose:
            console.print(f"[dim]已加载配置文件: {config}[/dim]")


@cli.command()
@click.argument("project_name", required=False)
@click.option(
    "--template",
    "-t",
    default="chat-bot",
    show_default=True,
    help="模板名称(参见 list 命令)",
)
@click.option(
    "--dir",
    "-d",
    "output_dir",
    default=".",
    show_default=True,
    help="项目输出目录",
)
@click.option(
    "--skip-install",
    is_flag=True,
    help="跳过依赖安装",
)
@click.option(
    "--skip-docker",
    is_flag=True,
    help="跳过 Docker 初始化",
)
@click.option(
    "--yes",
    "-y",
    is_flag=True,
    help="跳过交互式确认",
)
def init(
    project_name: Optional[str],
    template: str,
    output_dir: str,
    skip_install: bool,
    skip_docker: bool,
    yes: bool,
) -> None:
    """创建一个新的 AI Agent 项目

    PROJECT_NAME 为项目名称,支持小写字母、数字和连字符。
    """
    ctx = click.get_current_context()
    verbose = ctx.obj.get("verbose", False)

    if not project_name:
        project_name = Prompt.ask(
            "请输入项目名称",
            default="my-agent",
            validate=validate_project_name,
        )

    if not validate_project_name(project_name):
        console.print(f"[bold red]错误:[/] {ERRORS[\"invalid_name\"]}")
        sys.exit(1)

    target_path = Path(output_dir).resolve() / project_name

    if target_path.exists():
        if not yes:
            overwrite = Confirm.ask(
                f"目录 [bold]{target_path}[/] 已存在,是否覆盖?",
                default=False,
            )
            if not overwrite:
                console.print("[yellow]已取消[/]")
                sys.exit(0)
        else:
            console.print(f"[yellow]目录 {target_path} 已存在,将覆盖写入[/]")

    mgr = TemplateManager()
    available = mgr.list_templates()

    names = {t["name"] for t in available}
    if template not in names:
        console.print(f"[yellow]本地未找到模板 '{template}',尝试远程下载...[/]")
        success = mgr.fetch_remote(template)
        if not success:
            console.print(f"[bold red]错误:[/] {ERRORS[\"template_not_found\"]}")
            sys.exit(1)

    config_vars = _interactive_config(template, yes)

    with Progress(
        SpinnerColumn(),
        TextColumn("[progress.description]{task.description}"),
        transient=True,
        console=console,
    ) as progress:
        task = progress.add_task("正在生成项目文件...", total=None)

        generator = ProjectGenerator(
            template_name=template,
            project_name=project_name,
            target_dir=target_path,
            config_vars=config_vars,
        )
        generator.generate()

        progress.update(task, completed=True)

    if not skip_install:
        _install_dependencies(target_path, verbose)
    else:
        console.print("[dim]已跳过依赖安装[/]")

    if not skip_docker:
        _init_docker(target_path, verbose)

    _print_summary(project_name, target_path, template)


def _interactive_config(template: str, yes: bool) -> dict:
    """交互式模板配置"""
    prompts_map = {
        "chat-bot": [
            {"key": "agent_name", "label": "Agent 名称", "default": "ChatBot"},
            {"key": "description", "label": "Agent 描述", "default": "AI 聊天机器人"},
            {"key": "llm_provider", "label": "LLM 提供商", "default": "openai"},
            {"key": "model_name", "label": "模型名称", "default": "gpt-4o"},
        ],
        "data-trader": [
            {"key": "agent_name", "label": "Agent 名称", "default": "DataTrader"},
            {"key": "fee_percent", "label": "交易手续费 (%)", "default": "1.0"},
            {"key": "markets", "label": "监听市场地址(逗号分隔)", "default": ""},
        ],
        "market-maker": [
            {"key": "agent_name", "label": "Agent 名称", "default": "MarketMaker"},
            {"key": "min_profit_bps", "label": "最小套利利润 (bps)", "default": "10"},
            {"key": "max_slippage_bps", "label": "最大滑点 (bps)", "default": "50"},
            {"key": "flashloan_provider", "label": "闪电贷提供商", "default": "balancer"},
        ],
        "monitor-alert": [
            {"key": "agent_name", "label": "Agent 名称", "default": "MonitorBot"},
            {"key": "alert_channels", "label": "告警渠道(telegram/discord/slack)", "default": "telegram"},
            {"key": "check_interval_sec", "label": "检查间隔 (秒)", "default": "60"},
        ],
        "dao-agent": [
            {"key": "agent_name", "label": "Agent 名称", "default": "DAOMonitor"},
            {"key": "dao_address", "label": "DAO 合约地址", "default": ""},
            {"key": "min_vote_power", "label": "最低投票委托量", "default": "1000"},
        ],
    }

    prompts = prompts_map.get(template, [])
    config_vars = {}

    for p in prompts:
        if yes:
            config_vars[p["key"]] = p["default"]
        else:
            value = Prompt.ask(f"  {p[\"label\"]}", default=p["default"])
            config_vars[p["key"]] = value

    return config_vars


def _install_dependencies(target_path: Path, verbose: bool) -> None:
    """安装项目依赖"""
    pkg_file = target_path / "package.json"
    if pkg_file.exists():
        console.print("\n[bold]正在安装 npm 依赖...[/]")
        rc = os.system(f"cd {target_path} && npm install 2>&1")
        if rc != 0:
            console.print("[yellow]npm install 警告:部分依赖安装可能有问题[/]")
    else:
        console.print("[dim]未发现 package.json,跳过 npm install[/]")


def _init_docker(target_path: Path, verbose: bool) -> None:
    """初始化 Docker 环境"""
    compose_file = target_path / "docker-compose.yml"
    if compose_file.exists():
        console.print("\n[bold]Docker Compose 文件已就绪,运行以下命令启动:[/]")
        console.print(f"  cd {target_path} && docker-compose up -d")


def _print_summary(name: str, path: Path, template: str) -> None:
    """打印项目创建摘要"""
    table = Table(title="🎉 Agent 项目创建成功", show_header=False)
    table.add_column("属性", style="bold cyan")
    table.add_column("值")
    table.add_row("项目名称", name)
    table.add_row("模板类型", template)
    table.add_row("路径", str(path))
    table.add_row("启动命令", f"cd {path} && npm run dev")
    console.print(table)
    console.print(
        Panel(
            "[bold]下一步:[/]\n"
            + f"  1. cd {path}\n"
            + "  2. 编辑 .env 文件配置密钥\n"
            + "  3. npm run dev 启动开发模式\n"
            + "  4. npm run build && npm start 生产部署",
            title="指南",
        )
    )


@cli.command()
def list() -> None:
    """列出所有可用模板"""
    mgr = TemplateManager()
    templates = mgr.list_templates()

    if not templates:
        console.print("[yellow]本地模板列表为空,正在从远程获取...[/]")
        mgr.sync_remote()
        templates = mgr.list_templates()

    table = Table(title="可用模板")
    table.add_column("名称", style="cyan")
    table.add_column("描述")
    table.add_column("版本")
    table.add_column("来源")

    for t in templates:
        table.add_row(t["name"], t.get("description", ""), t.get("version", "-"),
                   "📦 本地" if t.get("local") else "🌐 远程")

    console.print(table)


@cli.command()
@click.argument("template_name")
@click.option("--force", is_flag=True, help="强制覆盖已下载的模板")
def fetch(template_name: str, force: bool) -> None:
    """从远程仓库下载指定模板"""
    mgr = TemplateManager()
    success = mgr.fetch_remote(template_name, force=force)

    if success:
        console.print(f"[green]✓ 模板 '{template_name}' 下载成功[/]")
    else:
        console.print(f"[bold red]✗ 模板 '{template_name}' 下载失败[/]")
        sys.exit(1)


@cli.command()
@click.argument("template_name")
@click.argument("path", type=click.Path(exists=True))
def add(template_name: str, path: str) -> None:
    """注册本地自定义模板"""
    mgr = TemplateManager()
    mgr.add_local_template(template_name, Path(path))
    console.print(f"[green]✓ 本地模板 '{template_name}' 已注册[/]")


@cli.command()
def upgrade() -> None:
    """检查并升级 CLI 工具版本"""
    console.print("[dim]正在检查更新...[/]")
    console.print("[green]✓ 当前已是最新版本 (0.1.0)[/]")

### 2.3 模板管理器 — templates.py

```python
"""Template management for msg-agent CLI."""

import os
import json
import shutil
import tempfile
import zipfile
from pathlib import Path
from typing import Optional
from dataclasses import dataclass, asdict

import requests

TEMPLATES_DIR = Path.home() / ".msg-agent" / "templates"
REGISTRY_URL = "https://registry.msg-chain.dev/templates/index.json"


@dataclass
class TemplateMetadata:
    """模板元数据"""
    name: str
    description: str
    版本: str = "0.1.0"
    author: str = "MSG Chain"
    license: str = "MIT"
    engine: str = "typescript"
    tags: list[str] = None
    dependencies: dict[str, str] = None
    hooks: dict[str, str] = None

    def __post_init__(self):
        self.tags = self.tags or []
        self.dependencies = self.dependencies or {}
        self.hooks = self.hooks or {}


class TemplateManager:
    """模板管理:本地缓存 + 远程拉取"""

    def __init__(self, templates_dir: Path = None):
        self.templates_dir = templates_dir or TEMPLATES_DIR
        self.templates_dir.mkdir(parents=True, exist_ok=True)
        self._index_cache: Optional[dict] = None

    def list_templates(self) -> list[dict]:
        """列出所有本地可用模板"""
        templates = []
        for item in self.templates_dir.iterdir():
            if item.is_dir():
                meta_file = item / "template.json"
                if meta_file.exists():
                    with open(meta_file, "r") as f:
                        meta = json.load(f)
                    meta["local"] = True
                    templates.append(meta)
                else:
                    templates.append({
                        "name": item.name,
                        "description": "(无元数据)",
                        "version": "0.0.0",
                        "local": True,
                    })
        remote_index = self._get_remote_index()
        remote_names = {t["name"] for t in templates}
        for r in remote_index:
            if r["name"] not in remote_names:
                r["local"] = False
                templates.append(r)
        return templates

    def get_template(self, name: str) -> Optional[Path]:
        """获取模板路径"""
        template_path = self.templates_dir / name
        if template_path.exists():
            return template_path
        return None

    def fetch_remote(self, name: str, force: bool = False) -> bool:
        """从远程下载模板"""
        remote_index = self._get_remote_index()
        entry = next((e for e in remote_index if e["name"] == name), None)
        if not entry:
            return False

        target_dir = self.templates_dir / name
        if target_dir.exists():
            if not force:
                return True
            shutil.rmtree(target_dir)

        url = entry.get("url")
        if not url:
            return False

        try:
            resp = requests.get(url, timeout=60)
            resp.raise_for_status()

            with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
                tmp.write(resp.content)
                zip_path = tmp.name

            with zipfile.ZipFile(zip_path, "r") as zf:
                zf.extractall(target_dir)

            os.unlink(zip_path)
            return True
        except requests.RequestException:
            return False

    def add_local_template(self, name: str, path: Path) -> None:
        """注册本地模板"""
        target = self.templates_dir / name
        if target.exists():
            shutil.rmtree(target)
        shutil.copytree(path, target)

    def sync_remote(self) -> int:
        """同步远程模板索引"""
        index = self._get_remote_index(force_refresh=True)
        count = 0
        for entry in index:
            if not (self.templates_dir / entry["name"]).exists():
                if self.fetch_remote(entry["name"]):
                    count += 1
        return count

    def _get_remote_index(self, force_refresh: bool = False) -> list[dict]:
        """获取远程模板索引"""
        if self._index_cache and not force_refresh:
            return self._index_cache

        try:
            resp = requests.get(REGISTRY_URL, timeout=15)
            resp.raise_for_status()
            self._index_cache = resp.json().get("templates", [])
        except requests.RequestException:
            self._index_cache = []
        return self._index_cache


# 内置模板元数据
BUILTIN_TEMPLATES = {
    "chat-bot": TemplateMetadata(
        name="chat-bot",
        description="AI 聊天机器人 — A2A 通信、LLM 集成、对话历史管理",
        version="0.1.0",
        tags=["ai", "chat", "a2a"],
        dependencies={
            "@msg-chain/agent-sdk": "^0.2.0",
            "openai": "^4.0.0",
            "langchain": "^0.1.0",
        },
    ),
    "data-trader": TemplateMetadata(
        name="data-trader",
        description="数据交易 Agent — 数据集上架、定价、交易结算",
        version="0.1.0",
        tags=["data", "marketplace", "nft"],
        dependencies={
            "@msg-chain/agent-sdk": "^0.2.0",
            "@msg-chain/contracts": "^0.1.0",
        },
    ),
    "market-maker": TemplateMetadata(
        name="market-maker",
        description="DeFi 套利 Agent — AMM 套利、闪电贷、Gas 优化",
        version="0.1.0",
        tags=["defi", "amm", "flashloan", "arbitrage"],
        dependencies={
            "@msg-chain/agent-sdk": "^0.2.0",
            "ethers": "^6.0.0",
        },
    ),
    "monitor-alert": TemplateMetadata(
        name="monitor-alert",
        description="链上监控告警 Agent — 事件监听、价格追踪、多渠道通知",
        version="0.1.0",
        tags=["monitor", "alert", "telegram", "discord"],
        dependencies={
            "@msg-chain/agent-sdk": "^0.2.0",
        },
    ),
    "dao-agent": TemplateMetadata(
        name="dao-agent",
        description="DAO 治理 Agent — 提案监控、自动投票、委托管理",
        version="0.1.0",
        tags=["dao", "governance", "voting"],
        dependencies={
            "@msg-chain/agent-sdk": "^0.2.0",
        },
    ),
}

2.4 项目生成器 — generator.py

"""Project file generator for msg-agent scaffolding."""

import os
import re
import json
import shutil
from pathlib import Path
from typing import Optional
from string import Template


class ProjectGenerator:
    """根据模板和配置变量生成完整的 Agent 项目"""

    TEMPLATE_EXTENSIONS = {
        ".ts", ".tsx", ".json", ".yaml", ".yml",
        ".toml", ".env", ".txt", ".md", ".cfg",
    }

    BINARY_EXTENSIONS = {".png", ".jpg", ".ico", ".woff2", ".ttf"}

    def __init__(
        self,
        template_name: str,
        project_name: str,
        target_dir: Path,
        config_vars: dict | None = None,
    ):
        self.template_name = template_name
        self.project_name = project_name
        self.target_dir = target_dir
        self.config_vars = config_vars or {}

        self.config_vars.update({
            "project_name": project_name,
            "project_name_snake": self._to_snake_case(project_name),
            "project_name_pascal": self._to_pascal_case(project_name),
            "project_name_kebab": project_name,
            "bech32_prefix": "msg",
            "chain_id": "msg-chain-1",
            "rpc_url": "https://rpc.msg-chain-1.msg.network",
            "rest_url": "https://rest.msg-chain-1.msg.network",
        })

        self.template_path = self._find_template(template_name)

    def _find_template(self, name: str) -> Path:
        """查找模板目录"""
        cli_dir = Path(__file__).parent.parent
        builtin = cli_dir / "templates" / name
        if builtin.exists():
            return builtin

        user_tpl = Path.home() / ".msg-agent" / "templates" / name
        if user_tpl.exists():
            return user_tpl

        raise FileNotFoundError(f"模板 '{name}' 未找到")

    def generate(self) -> None:
        """生成项目文件"""
        self.target_dir.mkdir(parents=True, exist_ok=True)

        for src_path in self.template_path.rglob("*"):
            if self._should_ignore(src_path):
                continue

            rel_path = src_path.relative_to(self.template_path)
            rel_path_str = self._apply_template(str(rel_path))
            dst_path = self.target_dir / rel_path_str

            if src_path.is_dir():
                dst_path.mkdir(parents=True, exist_ok=True)
            else:
                dst_path.parent.mkdir(parents=True, exist_ok=True)
                self._process_file(src_path, dst_path)

        self._run_post_generate_hooks()

    def _should_ignore(self, path: Path) -> bool:
        """检查是否应忽略该文件"""
        ignore_patterns = {
            "__pycache__", ".git", ".DS_Store",
            "node_modules", ".npm", ".cache",
        }
        for part in path.parts:
            if part in ignore_patterns:
                return True
        if path.name.endswith(".pyc"):
            return True
        return False

    def _process_file(self, src: Path, dst: Path) -> None:
        """处理单个文件"""
        ext = src.suffix.lower()

        if ext in self.BINARY_EXTENSIONS:
            shutil.copy2(src, dst)
            return

        if ext in self.TEMPLATE_EXTENSIONS or src.name.startswith("."):
            content = src.read_text(encoding="utf-8")
            content = self._apply_template(content)
            dst.write_text(content, encoding="utf-8")
        else:
            shutil.copy2(src, dst)

    def _apply_template(self, text: str) -> str:
        """对文本做 $variable 模板替换"""
        for key, value in self.config_vars.items():
            text = text.replace(f"${{{key}}}", str(value))
            text = text.replace(f"${key}", str(value))
        return text

    def _run_post_generate_hooks(self) -> None:
        """执行后置脚本"""
        meta_file = self.template_path / "template.json"
        if not meta_file.exists():
            return

        with open(meta_file, "r") as f:
            meta = json.load(f)

        hooks = meta.get("hooks", {})
        post_gen = hooks.get("post-generate")
        if post_gen:
            import subprocess
            subprocess.run(
                post_gen,
                shell=True,
                cwd=str(self.target_dir),
                env={**os.environ, **self.config_vars},
            )

    @staticmethod
    def _to_snake_case(name: str) -> str:
        s = re.sub(r"[-\s]", "_", name)
        return s.lower()

    @staticmethod
    def _to_pascal_case(name: str) -> str:
        s = re.sub(r"[^a-zA-Z0-9]", " ", name)
        return "".join(word.capitalize() for word in s.split())


class FileRenderer:
    """细粒度文件渲染器,支持条件块和循环"""

    CONDITIONAL_PATTERN = re.compile(r"\{#if (\w+)\}(.*?)\{\/if\}")
    LOOP_PATTERN = re.compile(r"\{#each (\w+) as (\w+)\}(.*?)\{\/each\}")

    @classmethod
    def render(cls, content: str, vars: dict) -> str:
        content = cls._render_conditionals(content, vars)
        content = cls._render_loops(content, vars)
        return Template(content).safe_substitute(vars)

    @classmethod
    def _render_conditionals(cls, content: str, vars: dict) -> str:
        def _replace(m):
            key = m.group(1)
            block = m.group(2)
            if vars.get(key):
                return block
            return ""
        return cls.CONDITIONAL_PATTERN.sub(_replace, content)

    @classmethod
    def _render_loops(cls, content: str, vars: dict) -> str:
        def _replace(m):
            list_key = m.group(1)
            item_key = m.group(2)
            block = m.group(3)
            items = vars.get(list_key, [])
            result = []
            for item in items:
                item_vars = {**vars, item_key: item}
                result.append(Template(block).safe_substitute(item_vars))
            return "".join(result)
        return cls.LOOP_PATTERN.sub(_replace, content)

2.5 配置与校验

# config.py
"""Configuration loading for msg-agent CLI."""
import os
from pathlib import Path
from typing import Any

try:
    import tomllib
except ImportError:
    import tomli as tomllib

DEFAULT_CONFIG = {
    "registry_url": "https://registry.msg-chain.dev/templates/index.json",
    "templates_dir": str(Path.home() / ".msg-agent" / "templates"),
    "default_rpc": "https://rpc.msg-chain-1.msg.network",
    "default_rest": "https://rest.msg-chain-1.msg.network",
    "bech32_prefix": "msg",
    "chain_id": "msg-chain-1",
    "npm_registry": "https://registry.npmjs.org",
    "proxy": None,
    "timeout": 30,
}


def load_config(path: str) -> dict:
    """加载 TOML 或 JSON 配置文件"""
    p = Path(path)
    if not p.exists():
        return dict(DEFAULT_CONFIG)
    raw = p.read_text(encoding="utf-8")
    if p.suffix in (".toml",):
        data = tomllib.loads(raw)
    elif p.suffix in (".json", ".jsonc"):
        import json
        data = json.loads(raw)
    else:
        raise ValueError(f"不支持的配置文件格式: {p.suffix}")
    config = dict(DEFAULT_CONFIG)
    config.update(data)
    return config
# validators.py
"""Input validators for msg-agent CLI."""
import re

PROJECT_NAME_REGEX = re.compile(r"^[a-z][a-z0-9\-]{2,48}[a-z0-9]$")


def validate_project_name(name: str) -> bool:
    """校验项目名"""
    if not name:
        return False
    return bool(PROJECT_NAME_REGEX.match(name))


def validate_address(address: str, prefix: str = "msg") -> bool:
    """校验 MSG Chain 地址格式"""
    pattern = re.compile(rf"^{prefix}1[0-9a-z]{{38}}$")
    return bool(pattern.match(address))


def validate_bps(value: str) -> bool:
    """校验基点"""
    try:
        v = int(value)
        return 0 <= v <= 10000
    except (ValueError, TypeError):
        return False

2.6 pyproject.toml

[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.backends._legacy:_Backend"

[project]
name = "msg-agent-cli"
version = "0.1.0"
description = "MSG Chain AI Agent 脚手架工具 — 快速创建和部署 AI Agent 项目"
readme = "README.md"
requires-python = ">=3.10"
license = {text = "MIT"}
authors = [
    {name = "MSG Chain Dev", email = "dev@msg-chain.dev"},
]
keywords = ["msg-chain", "agent", "ai", "blockchain", "scaffold"]

dependencies = [
    "click>=8.1.0",
    "rich>=13.0.0",
    "requests>=2.31.0",
    "tomli>=2.0.0; python_version < '3.11'",
]

[project.scripts]
msg-agent = "msg_agent.cli:cli"

[project.urls]
Homepage = "https://msg-chain.dev"
Repository = "https://github.com/msg-chain/msg-agent-cli"
Documentation = "https://docs.msg-chain.dev/agent-templates"

[tool.setuptools.packages.find]
include = ["msg_agent*"]
exclude = ["tests*", "templates*"]

3. 模板:Agent 聊天机器人

3.1 目录结构

my-chat-bot/
├── src/
│   ├── agent.ts              # 核心 Agent 实现
│   ├── agents.config.ts      # Agent 配置
│   ├── llm/
│   │   ├── provider.ts       # LLM 提供商抽象
│   │   ├── openai.ts         # OpenAI 适配器
│   │   └── anthropic.ts      # Anthropic 适配器
│   ├── memory/
│   │   ├── conversation.ts   # 对话历史管理
│   │   └── store.ts          # 持久化存储
│   ├── skills/
│   │   ├── registry.ts       # 技能注册中心
│   │   ├── weather.ts        # 天气查询技能
│   │   └── token.ts          # 代币信息技能
│   └── index.ts              # 入口
├── tests/
│   ├── agent.test.ts
│   └── llm.test.ts
├── .env.example
├── Dockerfile
├── docker-compose.yml
├── package.json
└── tsconfig.json

3.2 核心 Agent — src/agent.ts

import {
  AgentBase,
  AgentConfig,
  A2AMessage,
  A2AClient,
  AgentRegistryClient,
  MessageRole,
  Logger,
} from '@msg-chain/agent-sdk';
import { LLMProvider, createProvider } from './llm/provider';
import { ConversationMemory } from './memory/conversation';
import { SkillRegistry } from './skills/registry';
import { agentsConfig } from './agents.config';

export class ChatBotAgent extends AgentBase {
  private llm: LLMProvider;
  private memory: ConversationMemory;
  private skills: SkillRegistry;
  private a2aClient: A2AClient;
  private registryClient: AgentRegistryClient;
  private logger: Logger;

  constructor(config?: Partial<AgentConfig>) {
    super({
      name: agentsConfig.name,
      version: agentsConfig.version,
      addressPrefix: 'msg',
      chainId: 'msg-chain-1',
      ...config,
    });

    this.logger = new Logger({ name: agentsConfig.name, level: agentsConfig.logLevel });
    this.llm = createProvider(agentsConfig.llm);
    this.memory = new ConversationMemory({
      maxHistory: agentsConfig.memory.maxConversationPairs,
      storageType: agentsConfig.memory.storageType,
    });
    this.skills = new SkillRegistry(agentsConfig.skills);
    this.a2aClient = new A2AClient({
      endpoint: agentsConfig.a2aEndpoint,
      timeout: 30000,
    });
    this.registryClient = new AgentRegistryClient({
      rpcUrl: agentsConfig.rpcUrl,
      registryAddress: agentsConfig.registryAddress,
    });
  }

  async onStart(): Promise<void> {
    this.logger.info('ChatBot agent starting', {
      agent: agentsConfig.name,
      llm: agentsConfig.llm.provider,
      model: agentsConfig.llm.model,
      skills: this.skills.list().length,
    });

    await this.registryClient.register({
      name: agentsConfig.name,
      description: agentsConfig.description,
      endpoint: agentsConfig.a2aEndpoint,
      metadata: {
        type: 'chat-bot',
        version: agentsConfig.version,
        skills: this.skills.list().map(s => s.name),
      },
    });
    this.logger.info('Agent registered on chain');
  }

  async onStop(): Promise<void> {
    this.logger.info('ChatBot agent stopping');
    await this.registryClient.deregister();
    await this.memory.flush();
    this.logger.info('ChatBot agent stopped');
  }

  async handleMessage(message: A2AMessage): Promise<A2AMessage> {
    const startTime = Date.now();
    this.logger.debug('Received message', {
      sender: message.sender,
      type: message.type,
      contentLength: message.content.length,
    });

    try {
      const senderId = message.sender || 'unknown';
      const history = await this.memory.getHistory(senderId);
      const enrichedContext = await this.enrichWithSkills(message.content, history);
      const systemPrompt = this.buildSystemPrompt(senderId);

      const responseContent = await this.llm.generate({
        system: systemPrompt,
        messages: [
          ...history,
          { role: MessageRole.User, content: message.content },
        ],
        context: enrichedContext,
        temperature: agentsConfig.llm.temperature,
        maxTokens: agentsConfig.llm.maxTokens,
      });

      await this.memory.addPair(senderId, {
        user: message.content,
        assistant: responseContent,
      });

      const elapsed = Date.now() - startTime;
      this.logger.info('Message handled', {
        sender: senderId,
        elapsed: `${elapsed}ms`,
        responseLength: responseContent.length,
      });

      return this.createResponse(message, responseContent, {
        processedBy: agentsConfig.name,
        elapsedMs: elapsed,
      });
    } catch (error) {
      this.logger.error('Error handling message', {
        error: error instanceof Error ? error.message : String(error),
        sender: message.sender,
      });
      return this.createResponse(message, '抱歉,处理您消息时遇到错误。', {
        error: true,
      });
    }
  }

  private buildSystemPrompt(senderId: string): string {
    return [
      `你是 ${agentsConfig.name},一个运行在 MSG Chain 上的 AI 聊天助手。`,
      `当前时间: ${new Date().toISOString()}`,
      `你的能力包括: ${this.skills.list().map(s => s.description).join('、')}`,
      '请提供准确、有帮助的回答。',
    ].join('\n');
  }

  private async enrichWithSkills(
    content: string,
    history: Array<{ role: string; content: string }>,
  ): Promise<Record<string, unknown>> {
    const context: Record<string, unknown> = {};
    for (const skill of this.skills.list()) {
      if (skill.matcher(content)) {
        this.logger.debug(`Triggering skill: ${skill.name}`);
        try {
          const result = await skill.execute({ content, history });
          context[skill.name] = result;
        } catch (err) {
          this.logger.warn(`Skill ${skill.name} failed`, {
            error: err instanceof Error ? err.message : String(err),
          });
        }
      }
    }
    return context;
  }

  async sendMessage(targetAgent: string, content: string): Promise<A2AMessage> {
    return this.a2aClient.send({
      id: crypto.randomUUID(),
      sender: agentsConfig.name,
      target: targetAgent,
      content,
      type: 'a2a/chat',
      version: '1.0',
      timestamp: Date.now(),
    });
  }

  getStats(): Record<string, unknown> {
    return {
      name: agentsConfig.name,
      uptime: process.uptime(),
      conversations: this.memory.size(),
      skills: this.skills.list().length,
    };
  }
}

3.3 Agent 配置 — src/agents.config.ts

export const agentsConfig = {
  name: process.env.AGENT_NAME || 'ChatBot',
  version: process.env.AGENT_VERSION || '1.0.0',
  description: process.env.AGENT_DESCRIPTION || 'AI-powered chat assistant on MSG Chain',

  rpcUrl: process.env.MSG_RPC_URL || 'https://rpc.msg-chain-1.msg.network',
  restUrl: process.env.MSG_REST_URL || 'https://rest.msg-chain-1.msg.network',
  registryAddress: process.env.AGENT_REGISTRY_ADDRESS || '',
  a2aEndpoint: process.env.A2A_ENDPOINT || '/a2a/message',
  logLevel: (process.env.LOG_LEVEL as 'debug' | 'info' | 'warn' | 'error') || 'info',

  llm: {
    provider: process.env.LLM_PROVIDER || 'openai',
    model: process.env.LLM_MODEL || 'gpt-4o',
    apiKey: process.env.LLM_API_KEY || '',
    temperature: Number(process.env.LLM_TEMPERATURE) || 0.7,
    maxTokens: Number(process.env.LLM_MAX_TOKENS) || 2048,
  },

  memory: {
    maxConversationPairs: Number(process.env.MEMORY_MAX_PAIRS) || 50,
    storageType: (process.env.MEMORY_STORAGE as 'memory' | 'redis' | 'sqlite') || 'sqlite',
    dbPath: process.env.DB_PATH || './data/conversations.db',
  },

  skills: process.env.ENABLED_SKILLS
    ? process.env.ENABLED_SKILLS.split(',').map(s => s.trim())
    : ['token', 'weather'],

  wallet: {
    privateKey: process.env.PRIVATE_KEY || '',
    hdPath: process.env.HD_PATH || "m/44'/60'/0'/0/0",
  },
};

3.4 LLM 提供商抽象 — src/llm/provider.ts

export interface LLMGenerateParams {
  system: string;
  messages: Array<{ role: string; content: string }>;
  context?: Record<string, unknown>;
  temperature?: number;
  maxTokens?: number;
}

export interface LLMProvider {
  readonly name: string;
  generate(params: LLMGenerateParams): Promise<string>;
  generateStream(params: LLMGenerateParams): AsyncIterable<string>;
  embed(text: string): Promise<number[]>;
  health(): Promise<boolean>;
}

export function createProvider(config: {
  provider: string;
  model: string;
  apiKey: string;
}): LLMProvider {
  switch (config.provider) {
    case 'openai':
      return new (require('./openai').OpenAIProvider)(config);
    case 'anthropic':
      return new (require('./anthropic').AnthropicProvider)(config);
    default:
      throw new Error(`Unsupported LLM provider: ${config.provider}`);
  }
}

3.5 OpenAI 适配器 — src/llm/openai.ts

import OpenAI from 'openai';
import { LLMProvider, LLMGenerateParams } from './provider';

export class OpenAIProvider implements LLMProvider {
  readonly name = 'openai';
  private client: OpenAI;
  private model: string;

  constructor(config: { apiKey: string; model: string }) {
    this.client = new OpenAI({ apiKey: config.apiKey });
    this.model = config.model;
  }

  async generate(params: LLMGenerateParams): Promise<string> {
    const messages = [
      { role: 'system' as const, content: params.system },
      ...params.messages.map(m => ({
        role: m.role as 'user' | 'assistant',
        content: m.content,
      })),
    ];
    const response = await this.client.chat.completions.create({
      model: this.model,
      messages,
      temperature: params.temperature ?? 0.7,
      max_tokens: params.maxTokens ?? 2048,
    });
    return response.choices[0]?.message?.content || '';
  }

  async *generateStream(params: LLMGenerateParams): AsyncIterable<string> {
    const messages = [
      { role: 'system', content: params.system },
      ...params.messages.map(m => ({ role: m.role, content: m.content })),
    ];
    const stream = await this.client.chat.completions.create({
      model: this.model,
      messages,
      stream: true,
    });
    for await (const chunk of stream) {
      const content = chunk.choices[0]?.delta?.content || '';
      if (content) yield content;
    }
  }

  async embed(text: string): Promise<number[]> {
    const response = await this.client.embeddings.create({
      model: 'text-embedding-3-small', input: text,
    });
    return response.data[0].embedding;
  }

  async health(): Promise<boolean> {
    try { await this.client.models.retrieve(this.model); return true; }
    catch { return false; }
  }
}

3.6 对话记忆 — src/memory/conversation.ts

import path from 'path';
import fs from 'fs/promises';

export interface ConversationEntry {
  role: 'user' | 'assistant' | 'system';
  content: string;
  timestamp: number;
}

export interface ConversationPair {
  user: string;
  assistant: string;
}

export class ConversationMemory {
  private stores: Map<string, ConversationEntry[]>;
  private maxHistory: number;

  constructor(config: { maxHistory: number; storageType: string }) {
    this.maxHistory = config.maxHistory;
    this.stores = new Map();
  }

  async getHistory(senderId: string): Promise<ConversationEntry[]> {
    return (this.stores.get(senderId) || []).slice(-this.maxHistory * 2);
  }

  async addPair(senderId: string, pair: ConversationPair): Promise<void> {
    const now = Date.now();
    const entries: ConversationEntry[] = [
      { role: 'user', content: pair.user, timestamp: now },
      { role: 'assistant', content: pair.assistant, timestamp: now + 1 },
    ];
    const history = this.stores.get(senderId) || [];
    history.push(...entries);
    if (history.length > this.maxHistory * 2)
      history.splice(0, history.length - this.maxHistory * 2);
    this.stores.set(senderId, history);
  }

  size(): number { return this.stores.size; }
  async flush(): Promise<void> { this.stores.clear(); }
}

3.7 技能注册中心 — src/skills/registry.ts

export interface SkillContext {
  content: string;
  history: Array<{ role: string; content: string }>;
}

export interface Skill {
  name: string;
  description: string;
  matcher: (input: string) => boolean;
  execute: (ctx: SkillContext) => Promise<unknown>;
}

export class SkillRegistry {
  private skills: Map<string, Skill> = new Map();

  constructor(enabledSkills: string[]) {
    this.loadBuiltins(enabledSkills);
  }

  private loadBuiltins(enabled: string[]): void {
    const builtins: Record<string, () => Skill> = {
      token: () => ({
        name: 'token',
        description: '查询 MSG Chain 代币信息',
        matcher: (input) => /代币|token|价格|price|余额|balance/i.test(input),
        execute: async (ctx) => ({ price: '1.23 MSG' }),
      }),
      weather: () => ({
        name: 'weather',
        description: '查询天气',
        matcher: (input) => /天气|weather|温度|气温/i.test(input),
        execute: async (ctx) => ({ temp: '22°C', condition: 'sunny' }),
      }),
    };
    for (const name of enabled) {
      const factory = builtins[name];
      if (factory) this.skills.set(name, factory());
    }
  }

  register(skill: Skill): void { this.skills.set(skill.name, skill); }
  list(): Skill[] { return Array.from(this.skills.values()); }
  get(name: string): Skill | undefined { return this.skills.get(name); }
}

3.8 入口 — src/index.ts

import { ChatBotAgent } from './agent';
import { Logger } from '@msg-chain/agent-sdk';

const logger = new Logger({ name: 'chat-bot-agent' });

async function main() {
  const agent = new ChatBotAgent();

  process.on('SIGTERM', async () => {
    logger.info('Received SIGTERM, shutting down...');
    await agent.stop();
    process.exit(0);
  });
  process.on('SIGINT', async () => {
    logger.info('Received SIGINT, shutting down...');
    await agent.stop();
    process.exit(0);
  });

  await agent.start();
  logger.info('ChatBot agent started');
}

main().catch((err) => {
  logger.error('Fatal error', {
    error: err instanceof Error ? err.message : String(err),
  });
  process.exit(1);
});

3.9 package.json

{
  "name": "my-chat-bot",
  "version": "0.1.0",
  "description": "MSG Chain AI Chat Bot Agent",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js",
    "test": "vitest run",
    "docker:build": "docker build -t my-chat-bot ."
  },
  "dependencies": {
    "@msg-chain/agent-sdk": "^0.2.0",
    "openai": "^4.0.0",
    "better-sqlite3": "^11.0.0"
  },
  "devDependencies": {
    "@types/node": "^20.0.0",
    "tsx": "^4.0.0",
    "typescript": "^5.3.0",
    "vitest": "^1.0.0"
  }
}

3.10 Dockerfile

FROM node:20-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM node:20-alpine AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs && \
    adduser --system --uid 1001 agent
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER agent
ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
  CMD node dist/health.js || exit 1
CMD ["node", "dist/index.js"]

3.11 docker-compose.yml

version: '3.8'
services:
  chat-bot:
    build: .
    container_name: msg-chat-bot
    ports:
      - "3000:3000"
    env_file:
      - .env
    volumes:
      - ./data:/app/data
    restart: unless-stopped
    depends_on:
      redis:
        condition: service_started

  redis:
    image: redis:7-alpine
    container_name: msg-chat-bot-redis
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    restart: unless-stopped

volumes:
  redis-data:

3.12 .env.example

# MSG Chain 配置
MSG_RPC_URL=https://rpc.msg-chain-1.msg.network
MSG_REST_URL=https://rest.msg-chain-1.msg.network
CHAIN_ID=msg-chain-1
BECH32_PREFIX=msg
# Agent 配置
AGENT_NAME=ChatBot
AGENT_DESCRIPTION=AI 聊天助手
# 钱包
PRIVATE_KEY=0x...
# LLM 配置
LLM_PROVIDER=openai
LLM_MODEL=gpt-4o
LLM_API_KEY=sk-...
# 日志
LOG_LEVEL=info

3.13 tsconfig.json

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}

4. 模板:数据交易 Agent

4.1 模板概览

数据交易 Agent 允许用户在 MSG Chain 上买卖数据集。核心功能包括数据集上架、定价、数据预览、交易结算和访问控制。

4.2 目录结构

my-data-trader/
├── src/
│   ├── agent.ts               # 核心 Agent
│   ├── agents.config.ts       # 配置
│   ├── contracts/
│   │   ├── data-market.ts     # DataMarket 合约交互
│   │   └── types.ts           # 合约类型定义
│   ├── storage/
│   │   ├── ipfs.ts            # IPFS 存储
│   │   └── encryption.ts      # 数据加密
│   ├── pricing/
│   │   ├── oracle.ts          # 价格预言机
│   │   └── strategy.ts        # 定价策略
│   ├── validation/
│   │   └── schema.ts          # 数据集 schema 校验
│   └── index.ts               # 入口
├── tests/
├── .env.example
├── Dockerfile
├── package.json
└── tsconfig.json

4.3 核心 Agent — src/agent.ts

import {
  AgentBase,
  A2AMessage,
  A2AClient,
  AgentRegistryClient,
  Logger,
  WalletManager,
} from '@msg-chain/agent-sdk';
import { DataMarketContract } from './contracts/data-market';
import { IPFSStorage } from './storage/ipfs';
import { PricingOracle } from './pricing/oracle';
import { PricingStrategy } from './pricing/strategy';
import { agentsConfig } from './agents.config';

export interface Dataset {
  id: string;
  name: string;
  description: string;
  owner: string;
  price: string;
  priceDenom: string;
  uri: string;
  checksum: string;
  schema: Record<string, unknown>;
  sizeBytes: number;
  category: string;
  license: string;
  sampleUri: string;
  createdAt: number;
}

export class DataTraderAgent extends AgentBase {
  private dataMarket: DataMarketContract;
  private storage: IPFSStorage;
  private pricing: PricingOracle;
  private pricingStrategy: PricingStrategy;
  private a2aClient: A2AClient;
  private registryClient: AgentRegistryClient;
  private wallet: WalletManager;
  private logger: Logger;
  private config: typeof agentsConfig;
  private listedDatasets: Map<string, Dataset>;

  constructor() {
    super({
      name: agentsConfig.name,
      version: agentsConfig.version,
      addressPrefix: 'msg',
      chainId: 'msg-chain-1',
    });
    this.config = agentsConfig;
    this.logger = new Logger({ name: this.config.name, level: this.config.logLevel });
    this.listedDatasets = new Map();
    this.wallet = new WalletManager({
      privateKey: this.config.wallet.privateKey,
      bech32Prefix: 'msg',
    });
    this.dataMarket = new DataMarketContract({
      rpcUrl: this.config.rpcUrl,
      contractAddress: this.config.dataMarketAddress,
      wallet: this.wallet,
    });
    this.storage = new IPFSStorage({
      endpoint: this.config.ipfsEndpoint,
    });
    this.pricing = new PricingOracle({ rpcUrl: this.config.rpcUrl });
    this.pricingStrategy = new PricingStrategy({
      defaultFeePercent: this.config.feePercent,
    });
    this.a2aClient = new A2AClient({
      endpoint: this.config.a2aEndpoint,
    });
    this.registryClient = new AgentRegistryClient({
      rpcUrl: this.config.rpcUrl,
      registryAddress: this.config.registryAddress,
    });
  }

  async onStart(): Promise<void> {
    this.logger.info('DataTrader agent starting');
    await this.registryClient.register({
      name: this.config.name,
      description: this.config.description,
      metadata: { type: 'data-trader', version: this.config.version },
    });
    await this.syncExistingListings();
  }

  async onStop(): Promise<void> {
    await this.registryClient.deregister();
  }

  async handleMessage(message: A2AMessage): Promise<A2AMessage> {
    switch (message.type) {
      case 'data/query':
        return this.handleQuery(message.content);
      case 'data/purchase':
        return this.handlePurchase(message.content);
      case 'data/list':
        return this.handleList(message.content);
      case 'data/unlist':
        return this.handleUnlist(message.content);
      default:
        return this.createResponse(message, 'Unknown type', { error: true });
    }
  }

  async listDataset(dataset: Dataset): Promise<string> {
    this.logger.info('Listing dataset', { name: dataset.name });
    const checksum = await this.storage.upload(dataset.uri);
    const listing = await this.dataMarket.listDataset({
      name: dataset.name,
      description: dataset.description,
      price: dataset.price,
      priceDenom: dataset.priceDenom || 'umsg',
      dataUri: checksum,
      schema: JSON.stringify(dataset.schema),
      category: dataset.category,
      license: dataset.license,
      sizeBytes: dataset.sizeBytes,
    });
    this.listedDatasets.set(listing.datasetId, {
      ...dataset, id: listing.datasetId
    });
    return listing.datasetId;
  }

  async buyDataset(datasetId: string): Promise<void> {
    const dataset = await this.dataMarket.getDataset(datasetId);
    if (!dataset) throw new Error(`Dataset ${datasetId} not found`);
    await this.dataMarket.purchaseDataset(datasetId, {
      value: dataset.price, denom: dataset.priceDenom,
    });
  }

  async searchDatasets(query: {
    category?: string; keyword?: string;
    limit?: number; offset?: number;
  }): Promise<Dataset[]> {
    let results = await this.dataMarket.listActiveDatasets();
    if (query.category)
      results = results.filter(d => d.category === query.category);
    if (query.keyword) {
      const kw = query.keyword.toLowerCase();
      results = results.filter(d =>
        d.name.toLowerCase().includes(kw) ||
        d.description.toLowerCase().includes(kw));
    }
    const offset = query.offset || 0;
    const limit = query.limit || 20;
    return results.slice(offset, offset + limit);
  }

  async updatePricing(datasetId: string, newPrice: string): Promise<void> {
    await this.dataMarket.updatePrice(datasetId, newPrice, 'umsg');
  }

  private async handleQuery(content: string): Promise<A2AMessage> {
    const query = JSON.parse(content);
    const results = await this.searchDatasets(query);
    return {
      id: crypto.randomUUID(), sender: this.config.name,
      content: JSON.stringify(results),
      type: 'data/query-result', version: '1.0', timestamp: Date.now(),
    };
  }

  private async handlePurchase(content: string): Promise<A2AMessage> {
    const { datasetId } = JSON.parse(content);
    await this.buyDataset(datasetId);
    return {
      id: crypto.randomUUID(), sender: this.config.name,
      content: JSON.stringify({ datasetId, status: 'purchased' }),
      type: 'data/purchase-confirmed', version: '1.0', timestamp: Date.now(),
    };
  }

  private async handleList(content: string): Promise<A2AMessage> {
    const dataset: Dataset = JSON.parse(content);
    const datasetId = await this.listDataset(dataset);
    return {
      id: crypto.randomUUID(), sender: this.config.name,
      content: JSON.stringify({ datasetId, status: 'listed' }),
      type: 'data/list-confirmed', version: '1.0', timestamp: Date.now(),
    };
  }

  private async handleUnlist(content: string): Promise<A2AMessage> {
    const { datasetId } = JSON.parse(content);
    await this.dataMarket.unlistDataset(datasetId);
    return {
      id: crypto.randomUUID(), sender: this.config.name,
      content: JSON.stringify({ datasetId, status: 'unlisted' }),
      type: 'data/unlist-confirmed', version: '1.0', timestamp: Date.now(),
    };
  }

  private async syncExistingListings(): Promise<void> {
    const myListings = await this.dataMarket
      .getListingsByOwner(this.wallet.getAddress());
    for (const l of myListings) this.listedDatasets.set(l.id, l);
  }

  getStats(): Record<string, unknown> {
    return {
      name: this.config.name,
      listedDatasets: this.listedDatasets.size,
      address: this.wallet.getAddress(),
    };
  }
}

4.4 DataMarket 合约交互 — src/contracts/data-market.ts

import { ethers } from 'ethers';
import { WalletManager } from '@msg-chain/agent-sdk';

const DATAMARKET_ABI = [
  'function listDataset(string memory name, string memory description, string memory price, string memory dataUri, string memory schema, string memory category, string memory license, uint256 sizeBytes) returns (string memory datasetId)',
  'function purchaseDataset(string memory datasetId) payable returns (bool)',
  'function updatePrice(string memory datasetId, string memory newPrice, string memory denom)',
  'function unlistDataset(string memory datasetId)',
  'function getDataset(string memory datasetId) view returns (tuple(...))',
  'function getListingsByOwner(address owner) view returns (tuple(...)[])',
  'function listActiveDatasets() view returns (tuple(...)[])',
  'event DatasetListed(string indexed datasetId, address indexed seller, string price, uint256 timestamp)',
  'event DatasetPurchased(string indexed datasetId, address indexed buyer, address indexed seller, string amount, uint256 timestamp)',
];

export class DataMarketContract {
  private provider: ethers.JsonRpcProvider;
  private contract: ethers.Contract;
  private wallet: WalletManager;

  constructor(config: {
    rpcUrl: string; contractAddress: string; wallet: WalletManager;
  }) {
    this.provider = new ethers.JsonRpcProvider(config.rpcUrl);
    this.wallet = config.wallet;
    this.contract = new ethers.Contract(
      config.contractAddress, DATAMARKET_ABI, this.provider);
  }

  async listDataset(params: any): Promise<{ datasetId: string; txHash: string }> {
    const signer = await this.wallet.getSigner(this.provider);
    const c = this.contract.connect(signer) as ethers.Contract;
    const tx = await c.listDataset(
      params.name, params.description, params.price,
      params.dataUri, params.schema, params.category,
      params.license, params.sizeBytes);
    const receipt = await tx.wait();
    return { datasetId: '', txHash: receipt.hash };
  }

  async purchaseDataset(datasetId: string, payment: { value: string; denom: string }) {
    const signer = await this.wallet.getSigner(this.provider);
    const c = this.contract.connect(signer) as ethers.Contract;
    const tx = await c.purchaseDataset(datasetId, {
      value: ethers.parseEther(payment.value),
    });
    return tx.wait();
  }

  async updatePrice(datasetId: string, newPrice: string, denom: string) {
    const signer = await this.wallet.getSigner(this.provider);
    const c = this.contract.connect(signer) as ethers.Contract;
    const tx = await c.updatePrice(datasetId, newPrice, denom);
    return (await tx.wait()).hash;
  }

  async unlistDataset(datasetId: string) {
    const signer = await this.wallet.getSigner(this.provider);
    const c = this.contract.connect(signer) as ethers.Contract;
    const tx = await c.unlistDataset(datasetId);
    return (await tx.wait()).hash;
  }

  async getDataset(datasetId: string) {
    return this.contract.getDataset(datasetId);
  }

  async getListingsByOwner(owner: string) {
    return this.contract.getListingsByOwner(owner);
  }

  async listActiveDatasets() {
    return this.contract.listActiveDatasets();
  }
}

4.5 IPFS 存储 — src/storage/ipfs.ts

export class IPFSStorage {
  private endpoint: string;

  constructor(config: { endpoint: string }) {
    this.endpoint = config.endpoint;
  }

  async upload(data: string): Promise<string> {
    const formData = new FormData();
    formData.append('file', new Blob([data], { type: 'application/octet-stream' }));
    const resp = await fetch(`${this.endpoint}/api/v0/add`, {
      method: 'POST', body: formData,
    });
    if (!resp.ok) throw new Error(`IPFS upload failed: ${resp.statusText}`);
    return ((await resp.json()) as { Hash: string }).Hash;
  }

  async download(cid: string): Promise<string> {
    const resp = await fetch(`${this.endpoint}/api/v0/cat?arg=${cid}`);
    return resp.text();
  }
}

4.6 定价策略 — src/pricing/strategy.ts

export class PricingStrategy {
  private feePercent: number;

  constructor(config: { defaultFeePercent: number }) {
    this.feePercent = config.defaultFeePercent;
  }

  suggestPrice(params: {
    sizeBytes: number; category: string; quality: number; isExclusive: boolean;
  }) {
    const basePrices: Record<string, bigint> = {
      'tabular': BigInt(100_000_000),
      'text': BigInt(50_000_000),
      'image': BigInt(200_000_000),
      'model': BigInt(1_000_000_000),
    };
    const base = basePrices[params.category] || BigInt(50_000_000);
    const suggested = base * BigInt(Math.floor(params.quality * 100)) / BigInt(50);
    return { suggestedPrice: suggested.toString(), currency: 'umsg' };
  }

  calculateFee(price: string): string {
    const bp = BigInt(price) * BigInt(Math.floor(this.feePercent * 100)) / BigInt(10000);
    return bp.toString();
  }
}

4.7 合约类型 — src/contracts/types.ts

export interface DatasetOnChain {
  id: string;
  name: string;
  description: string;
  owner: string;
  price: string;
  priceDenom: string;
  dataUri: string;
  schema: string;
  category: string;
  license: string;
  sizeBytes: bigint;
  createdAt: bigint;
}

export interface MarketplaceStats {
  totalDatasets: number;
  totalTradingVolume: string;
  activeSellers: number;
  averagePrice: string;
}

5. 模板:DeFi 套利 Agent

5.1 模板概览

DeFi 套利 Agent 监控 MSG Chain 上的 AMM 流动性池,发现价格差并执行套利交易,支持闪电贷放大收益。

5.2 目录结构

my-market-maker/
├── src/
│   ├── agent.ts                    # 核心 Agent
│   ├── agents.config.ts            # 配置
│   ├── amm/
│   │   ├── pool.ts                 # AMM 池抽象
│   │   ├── price-feed.ts           # 价格喂价
│   │   └── router.ts               # 路由寻路
│   ├── arbitrage/
│   │   ├── scanner.ts              # 套利机会扫描
│   │   ├── executor.ts             # 交易执行器
│   │   └── types.ts                # 类型定义
│   ├── flashloan/
│   │   ├── balancer.ts             # Balancer 闪电贷
│   │   └── provider.ts             # 闪电贷抽象
│   ├── gas/
│   │   ├── estimator.ts            # Gas 估算
│   │   └── priority.ts             # Gas 优先级策略
│   ├── risk/
│   │   ├── manager.ts              # 风险管理
│   │   └── slippage.ts             # 滑点保护
│   └── index.ts
├── tests/
├── .env.example
├── Dockerfile
├── package.json
└── tsconfig.json

5.3 核心 Agent — src/agent.ts

import {
  AgentBase, AgentConfig, A2AMessage, Logger, WalletManager,
} from '@msg-chain/agent-sdk';
import { PoolManager } from './amm/pool';
import { PriceFeed } from './amm/price-feed';
import { ArbitrageScanner } from './arbitrage/scanner';
import { ArbitrageExecutor } from './arbitrage/executor';
import { FlashloanProvider } from './flashloan/provider';
import { GasEstimator } from './gas/estimator';
import { RiskManager } from './risk/manager';
import { agentsConfig } from './agents.config';

export interface ArbitrageOpportunity {
  id: string;
  pair: string;
  buyPool: string;
  sellPool: string;
  buyPrice: bigint;
  sellPrice: bigint;
  profitBps: number;
  estimatedProfit: bigint;
  requiredCapital: bigint;
  flashLoanSupported: boolean;
  gasEstimate: bigint;
  netProfit: bigint;
  confidence: number;
  timestamp: number;
  path: string[];
}

export class MarketMakerAgent extends AgentBase {
  private poolManager: PoolManager;
  private priceFeed: PriceFeed;
  private scanner: ArbitrageScanner;
  private executor: ArbitrageExecutor;
  private flashloan: FlashloanProvider;
  private gasEstimator: GasEstimator;
  private riskManager: RiskManager;
  private wallet: WalletManager;
  private logger: Logger;
  private config: typeof agentsConfig;
  private scanInterval: ReturnType<typeof setInterval> | null;
  private opportunities: Map<string, ArbitrageOpportunity>;
  private tradeHistory: Array<{ success: boolean; profit: string; txHash: string }>;
  private isScanning: boolean;
  private totalProfit: bigint;

  constructor() {
    super({
      name: agentsConfig.name, version: agentsConfig.version,
      addressPrefix: 'msg', chainId: 'msg-chain-1',
    });
    this.config = agentsConfig;
    this.logger = new Logger({ name: this.config.name, level: this.config.logLevel });
    this.opportunities = new Map();
    this.tradeHistory = [];
    this.totalProfit = BigInt(0);
    this.scanInterval = null;
    this.isScanning = false;

    this.wallet = new WalletManager({
      privateKey: this.config.wallet.privateKey, bech32Prefix: 'msg',
    });
    this.poolManager = new PoolManager({
      rpcUrl: this.config.rpcUrl, pools: this.config.pools,
    });
    this.priceFeed = new PriceFeed({
      rpcUrl: this.config.rpcUrl, refreshIntervalMs: this.config.priceRefreshMs,
    });
    this.gasEstimator = new GasEstimator({ rpcUrl: this.config.rpcUrl });
    this.riskManager = new RiskManager({
      maxPositionSize: this.config.maxPositionSize,
      maxSlippageBps: this.config.maxSlippageBps,
      minProfitBps: this.config.minProfitBps,
      maxConcurrentTrades: this.config.maxConcurrentTrades,
      dailyLossLimit: this.config.dailyLossLimit,
    });
    this.flashloan = new FlashloanProvider({
      provider: this.config.flashloanProvider,
      rpcUrl: this.config.rpcUrl, wallet: this.wallet,
    });
    this.executor = new ArbitrageExecutor({
      wallet: this.wallet, flashloan: this.flashloan,
      gasEstimator: this.gasEstimator, riskManager: this.riskManager,
      maxSlippageBps: this.config.maxSlippageBps,
    });
    this.scanner = new ArbitrageScanner({
      poolManager: this.poolManager, priceFeed: this.priceFeed,
      gasEstimator: this.gasEstimator, minProfitBps: this.config.minProfitBps,
    });
  }

  async onStart(): Promise<void> {
    this.logger.info('MarketMaker starting', {
      pools: this.config.pools.length,
      minProfitBps: this.config.minProfitBps,
    });
    await this.poolManager.initialize();
    await this.priceFeed.start();
    this.scanInterval = setInterval(
      () => this.scanAndTrade(), this.config.scanIntervalMs);
    this.scanAndTrade();
  }

  async onStop(): Promise<void> {
    if (this.scanInterval) clearInterval(this.scanInterval);
    await this.priceFeed.stop();
  }

  async handleMessage(message: A2AMessage): Promise<A2AMessage> {
    switch (message.type) {
      case 'market/stats':
        return this.createResponse(message, JSON.stringify(this.getStats()));
      case 'market/trades':
        return this.createResponse(
          message, JSON.stringify(this.tradeHistory.slice(-20)));
      default:
        return this.createResponse(message, 'Unknown', { error: true });
    }
  }

  private async scanAndTrade(): Promise<void> {
    if (this.isScanning) return;
    this.isScanning = true;
    try {
      const opps = await this.scanner.scan();
      for (const o of opps) this.opportunities.set(o.id, o);
      const best = opps
        .filter(o => o.confidence >= this.config.minConfidence)
        .sort((a, b) => Number(b.netProfit - a.netProfit))
        .slice(0, this.config.maxConcurrentTrades);
      for (const opp of best) {
        if (!this.riskManager.canTrade()) break;
        const start = Date.now();
        try {
          let txHash: string;
          if (opp.flashLoanSupported &&
              opp.requiredCapital > this.wallet.getBalance()) {
            txHash = (await this.executor.executeWithFlashLoan(opp)).txHash;
          } else {
            txHash = (await this.executor.executeDirect(opp)).txHash;
          }
          this.totalProfit += opp.estimatedProfit;
          this.tradeHistory.push({ success: true, profit: opp.estimatedProfit.toString(), txHash });
          this.riskManager.recordTrade({ txHash, profit: opp.estimatedProfit.toString(), timestamp: Date.now() });
        } catch (err) {
          this.tradeHistory.push({ success: false, profit: '0', txHash: '' });
        }
      }
    } finally {
      this.isScanning = false;
    }
  }

  getStats(): Record<string, unknown> {
    return {
      name: this.config.name,
      pools: this.config.pools.length,
      totalTrades: this.tradeHistory.length,
      totalProfit: this.totalProfit.toString(),
      address: this.wallet.getAddress(),
    };
  }
}

5.4 AMM 池抽象 — src/amm/pool.ts

import { ethers } from 'ethers';

interface PoolState {
  address: string;
  reserve0: bigint;
  reserve1: bigint;
  token0: string;
  token1: string;
  feeBps: number;
  price: bigint;
}

const POOL_ABI = [
  'function getReserves() view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast)',
  'function token0() view returns (address)',
  'function token1() view returns (address)',
  'function swap(uint256 amount0Out, uint256 amount1Out, address to, bytes calldata data)',
];

export class PoolManager {
  private provider: ethers.JsonRpcProvider;
  private pools: Map<string, any>;
  private states: Map<string, PoolState>;

  constructor(config: { rpcUrl: string; pools: any[] }) {
    this.provider = new ethers.JsonRpcProvider(config.rpcUrl);
    this.pools = new Map();
    this.states = new Map();
    for (const p of config.pools) this.pools.set(p.address.toLowerCase(), p);
  }

  async initialize(): Promise<void> {
    const promises = Array.from(this.pools.keys()).map(a => this.refreshPoolState(a));
    await Promise.all(promises);
  }

  async refreshPoolState(address: string): Promise<PoolState> {
    const contract = new ethers.Contract(address, POOL_ABI, this.provider);
    const [reserves, token0, token1] = await Promise.all([
      contract.getReserves(), contract.token0(), contract.token1(),
    ]);
    const r0 = BigInt(reserves.reserve0);
    const r1 = BigInt(reserves.reserve1);
    const price = r0 > 0 ? (r1 * BigInt(10) ** BigInt(18)) / r0 : BigInt(0);
    const state: PoolState = {
      address: address.toLowerCase(),
      reserve0: r0, reserve1: r1,
      token0, token1, feeBps: 30, price,
    };
    this.states.set(address.toLowerCase(), state);
    return state;
  }

  async refreshAll(): Promise<void> {
    await Promise.all(Array.from(this.pools.keys()).map(a => this.refreshPoolState(a)));
  }

  getPool(address: string): PoolState | undefined {
    return this.states.get(address.toLowerCase());
  }

  getAllPools(): PoolState[] {
    return Array.from(this.states.values());
  }
}

5.5 套利扫描器 — src/arbitrage/scanner.ts

import { PoolManager } from '../amm/pool';
import { PriceFeed } from '../amm/price-feed';
import { GasEstimator } from '../gas/estimator';
import { ArbitrageOpportunity } from '../agent';

export class ArbitrageScanner {
  private poolManager: PoolManager;
  private priceFeed: PriceFeed;
  private gasEstimator: GasEstimator;
  private minProfitBps: number;
  private counter: number = 0;

  constructor(config: {
    poolManager: PoolManager; priceFeed: PriceFeed;
    gasEstimator: GasEstimator; minProfitBps: number;
  }) {
    this.poolManager = config.poolManager;
    this.priceFeed = config.priceFeed;
    this.gasEstimator = config.gasEstimator;
    this.minProfitBps = config.minProfitBps;
  }

  async scan(): Promise<ArbitrageOpportunity[]> {
    const pools = this.poolManager.getAllPools();
    const opportunities: ArbitrageOpportunity[] = [];

    for (let i = 0; i < pools.length; i++) {
      for (let j = i + 1; j < pools.length; j++) {
        const a = pools[i], b = pools[j];
        if (!this.isSamePair(a, b)) continue;

        const opp = await this.evaluate(a, b);
        if (opp) opportunities.push(opp);
        const rev = await this.evaluate(b, a);
        if (rev) opportunities.push(rev);
      }
    }
    return opportunities;
  }

  private isSamePair(a: any, b: any): boolean {
    const at = [a.token0.toLowerCase(), a.token1.toLowerCase()].sort();
    const bt = [b.token0.toLowerCase(), b.token1.toLowerCase()].sort();
    return at[0] === bt[0] && at[1] === bt[1];
  }

  private async evaluate(
    buy: any, sell: any
  ): Promise<ArbitrageOpportunity | null> {
    const base = BigInt(1000) * BigInt(10) ** BigInt(18);
    const out = (buy.reserve0 > 0)
      ? (base * buy.reserve1 * BigInt(9970)) / (buy.reserve0 * BigInt(10000) + base * BigInt(9970))
      : BigInt(0);
    const back = (out > 0 && sell.reserve1 > 0)
      ? (out * sell.reserve0 * BigInt(9970)) / (sell.reserve1 * BigInt(10000) + out * BigInt(9970))
      : BigInt(0);
    if (back <= base) return null;

    const profitBps = Number((back - base) * BigInt(10000) / base);
    if (profitBps < this.minProfitBps) return null;

    this.counter++;
    return {
      id: `opp-${Date.now()}-${this.counter}`,
      pair: `${buy.token0.slice(0,6)}-${buy.token1.slice(0,6)}`,
      buyPool: buy.address, sellPool: sell.address,
      buyPrice: buy.price, sellPrice: sell.price,
      profitBps, estimatedProfit: back - base, requiredCapital: base,
      flashLoanSupported: true, gasEstimate: BigInt(500000),
      netProfit: back - base - BigInt(500000) * BigInt(500000000),
      confidence: Math.min(100, profitBps > 50 ? 80 : 50),
      timestamp: Date.now(), path: [buy.token0, buy.token1],
    };
  }
}

5.6 闪电贷集成 — src/flashloan/provider.ts

import { ethers } from 'ethers';
import { WalletManager } from '@msg-chain/agent-sdk';

const BALANCER_VAULT_ABI = [
  'function flashLoan(address recipient, address[] memory tokens, uint256[] memory amounts, bytes memory userData) external',
];

export class FlashloanProvider {
  private provider: ethers.JsonRpcProvider;
  private wallet: WalletManager;
  private vaultAddress: string;

  constructor(config: { provider: string; rpcUrl: string; wallet: WalletManager }) {
    this.provider = new ethers.JsonRpcProvider(config.rpcUrl);
    this.wallet = config.wallet;
    this.vaultAddress = '0xBA12222222228d8Ba445958a75a0704d566BF2C8';
  }

  async executeFlashLoan(request: {
    token: string; amount: bigint; targetContract: string; calldata: string;
  }) {
    const signer = await this.wallet.getSigner(this.provider);
    const vault = new ethers.Contract(this.vaultAddress, BALANCER_VAULT_ABI, signer);
    const tx = await vault.flashLoan(
      [request.targetContract],
      [request.token],
      [request.amount],
      ethers.hexlify(ethers.toUtf8Bytes(JSON.stringify({
        target: request.targetContract, calldata: request.calldata,
      }))),
      { gasLimit: 3_000_000 }
    );
    const receipt = await tx.wait();
    return { success: receipt.status === 1, txHash: receipt.hash, fee: request.amount * BigInt(5) / BigInt(10000) };
  }

  getFee(amount: bigint): bigint {
    return amount * BigInt(5) / BigInt(10000);
  }
}

5.7 Gas 估算 — src/gas/estimator.ts

import { ethers } from 'ethers';

export class GasEstimator {
  private provider: ethers.JsonRpcProvider;
  private cache: bigint = BigInt(0);
  private lastUpdate: number = 0;

  constructor(config: { rpcUrl: string }) {
    this.provider = new ethers.JsonRpcProvider(config.rpcUrl);
  }

  async refresh(): Promise<bigint> {
    const fee = await this.provider.getFeeData();
    this.cache = fee.gasPrice || BigInt(500_000_000);
    this.lastUpdate = Date.now();
    return this.cache;
  }

  async getOptimalGasPrice(priority: 'low' | 'medium' | 'high' = 'medium'): Promise<bigint> {
    if (Date.now() - this.lastUpdate > 15000) await this.refresh();
    const mult = priority === 'low' ? 90 : priority === 'high' ? 120 : 100;
    return this.cache * BigInt(mult) / BigInt(100);
  }
}

5.8 风险管理 — src/risk/manager.ts

export class RiskManager {
  private maxPositionSize: string;
  private maxSlippageBps: number;
  private minProfitBps: number;
  private maxConcurrentTrades: number;
  private dailyLossLimit: string;
  private activeTrades: number = 0;
  private dailyTrades: Array<{ profit: string; timestamp: number }> = [];
  private dailyStart: number = Date.now();

  constructor(config: {
    maxPositionSize: string; maxSlippageBps: number;
    minProfitBps: number; maxConcurrentTrades: number; dailyLossLimit: string;
  }) {
    this.maxPositionSize = config.maxPositionSize;
    this.maxSlippageBps = config.maxSlippageBps;
    this.minProfitBps = config.minProfitBps;
    this.maxConcurrentTrades = config.maxConcurrentTrades;
    this.dailyLossLimit = config.dailyLossLimit;
  }

  canTrade(): boolean {
    if (this.activeTrades >= this.maxConcurrentTrades) return false;
    this._resetDailyIfNeeded();
    const loss = this.dailyTrades
      .filter(t => BigInt(t.profit) < 0)
      .reduce((a, t) => a + -BigInt(t.profit), BigInt(0));
    return loss < BigInt(this.dailyLossLimit);
  }

  canExecuteTrade(requiredCapital: bigint, estimatedProfit: bigint, slippageBps: number) {
    if (requiredCapital > BigInt(this.maxPositionSize))
      return { allowed: false, reason: 'Exceeds max position' };
    if (slippageBps > this.maxSlippageBps)
      return { allowed: false, reason: 'Exceeds max slippage' };
    const profitBps = Number(estimatedProfit * BigInt(10000) / requiredCapital);
    if (profitBps < this.minProfitBps)
      return { allowed: false, reason: 'Below min profit' };
    return { allowed: true };
  }

  recordTrade(trade: { profit: string; timestamp: number }): void {
    this._resetDailyIfNeeded();
    this.dailyTrades.push(trade);
  }

  private _resetDailyIfNeeded(): void {
    if (Date.now() - this.dailyStart > 86400000) {
      this.dailyTrades = [];
      this.dailyStart = Date.now();
    }
  }
}

5.9 Agent 配置 — src/agents.config.ts

export const agentsConfig = {
  name: process.env.AGENT_NAME || 'MarketMaker',
  version: process.env.AGENT_VERSION || '1.0.0',
  description: 'DeFi arbitrage bot on MSG Chain',
  rpcUrl: process.env.MSG_RPC_URL || 'https://rpc.msg-chain-1.msg.network',
  logLevel: (process.env.LOG_LEVEL as any) || 'info',
  minProfitBps: Number(process.env.MIN_PROFIT_BPS) || 10,
  maxSlippageBps: Number(process.env.MAX_SLIPPAGE_BPS) || 50,
  minConfidence: Number(process.env.MIN_CONFIDENCE) || 60,
  scanIntervalMs: Number(process.env.SCAN_INTERVAL_MS) || 10000,
  priceRefreshMs: Number(process.env.PRICE_REFRESH_MS) || 5000,
  maxConcurrentTrades: Number(process.env.MAX_CONCURRENT_TRADES) || 3,
  maxPositionSize: process.env.MAX_POSITION_SIZE || '100000000000000000000',
  dailyLossLimit: process.env.DAILY_LOSS_LIMIT || '50000000000000000000',
  flashloanProvider: process.env.FLASHLOAN_PROVIDER || 'balancer',
  pools: (process.env.WATCHED_POOLS || '').split(',').filter(Boolean)
    .map(addr => ({ address: addr })),
  wallet: { privateKey: process.env.PRIVATE_KEY || '', hdPath: process.env.HD_PATH || "m/44'/60'/0'/0/0" },
};

6. 模板:监控与告警 Agent

6.1 模板概览

监控 Agent 实时跟踪 MSG Chain 上的链上活动(区块、交易、合约事件、代币价格),并通过 Telegram、Discord、Slack 等渠道发送告警通知。

6.2 目录结构

my-monitor-alert/
├── src/
│   ├── agent.ts                       # 核心 Agent
│   ├── agents.config.ts               # 配置
│   ├── monitors/
│   │   ├── block.ts                   # 区块监控
│   │   ├── transaction.ts             # 交易监控
│   │   ├── event.ts                   # 合约事件监控
│   │   ├── price.ts                   # 价格监控
│   │   └── validator.ts               # 验证人监控
│   ├── alerts/
│   │   ├── dispatcher.ts              # 告警分发
│   │   ├── telegram.ts                # Telegram 通知
│   │   ├── discord.ts                 # Discord 通知
│   │   └── webhook.ts                 # Webhook 通知
│   ├── filters/
│   │   ├── rule-engine.ts             # 规则引擎
│   │   └── templates.ts              # 告警模板
│   └── index.ts
├── tests/
├── .env.example
├── Dockerfile
├── package.json
└── tsconfig.json

6.3 核心 Agent — src/agent.ts

import { AgentBase, A2AMessage, Logger } from '@msg-chain/agent-sdk';
import { BlockMonitor } from './monitors/block';
import { TransactionMonitor } from './monitors/transaction';
import { EventMonitor } from './monitors/event';
import { PriceMonitor } from './monitors/price';
import { ValidatorMonitor } from './monitors/validator';
import { AlertDispatcher } from './alerts/dispatcher';
import { RuleEngine, AlertRule } from './filters/rule-engine';
import { agentsConfig } from './agents.config';

export interface AlertEvent {
  id: string;
  type: 'block' | 'transaction' | 'event' | 'price' | 'validator';
  severity: 'info' | 'warning' | 'critical';
  title: string;
  message: string;
  details: Record<string, unknown>;
  timestamp: number;
  chainId: string;
  txHash?: string;
  blockHeight?: number;
}

export class MonitorAlertAgent extends AgentBase {
  private blockMonitor: BlockMonitor;
  private txMonitor: TransactionMonitor;
  private eventMonitor: EventMonitor;
  private priceMonitor: PriceMonitor;
  private validatorMonitor: ValidatorMonitor;
  private alertDispatcher: AlertDispatcher;
  private ruleEngine: RuleEngine;
  private logger: Logger;
  private config: typeof agentsConfig;
  private alertHistory: AlertEvent[];
  private isPaused: boolean;

  constructor() {
    super({ name: agentsConfig.name, version: agentsConfig.version, addressPrefix: 'msg', chainId: 'msg-chain-1' });
    this.config = agentsConfig;
    this.logger = new Logger({ name: this.config.name, level: this.config.logLevel });
    this.alertHistory = [];
    this.isPaused = false;

    this.ruleEngine = new RuleEngine(this.config.rules);
    this.alertDispatcher = new AlertDispatcher({
      channels: {
        telegram: this.config.channels.telegram,
        discord: this.config.channels.discord,
        webhook: this.config.channels.webhook,
      },
    });

    this.blockMonitor = new BlockMonitor({
      rpcUrl: this.config.rpcUrl, restUrl: this.config.restUrl,
      checkIntervalMs: this.config.blockCheckInterval,
      onNewBlock: (block) => this.handleNewBlock(block),
    });
    this.txMonitor = new TransactionMonitor({
      rpcUrl: this.config.rpcUrl, watchedAddresses: this.config.watchedAddresses,
      onTransaction: (tx) => this.handleTransaction(tx),
    });
    this.eventMonitor = new EventMonitor({
      rpcUrl: this.config.rpcUrl, contracts: this.config.watchedContracts,
      onEvent: (event) => this.handleContractEvent(event),
    });
    this.priceMonitor = new PriceMonitor({
      rpcUrl: this.config.rpcUrl, tokens: this.config.watchedTokens,
      priceChangeThresholdBps: this.config.priceChangeThresholdBps,
      checkIntervalMs: this.config.priceCheckInterval,
      onPriceChange: (change) => this.handlePriceChange(change),
    });
    this.validatorMonitor = new ValidatorMonitor({
      restUrl: this.config.restUrl, checkIntervalMs: this.config.validatorCheckInterval,
      onValidatorChange: (change) => this.handleValidatorChange(change),
    });
  }

  async onStart(): Promise<void> {
    this.logger.info('MonitorAlert starting');
    await this.alertDispatcher.testAll();
    this.blockMonitor.start();
    this.txMonitor.start();
    this.eventMonitor.start();
    this.priceMonitor.start();
    this.validatorMonitor.start();
  }

  async onStop(): Promise<void> {
    this.blockMonitor.stop(); this.txMonitor.stop();
    this.eventMonitor.stop(); this.priceMonitor.stop(); this.validatorMonitor.stop();
  }

  async handleMessage(message: A2AMessage): Promise<A2AMessage> {
    switch (message.type) {
      case 'monitor/pause':
        this.isPaused = true;
        return this.createResponse(message, 'Monitor paused');
      case 'monitor/resume':
        this.isPaused = false;
        return this.createResponse(message, 'Monitor resumed');
      case 'monitor/status':
        return this.createResponse(message, JSON.stringify(this.getStatus()));
      case 'monitor/history':
        return this.createResponse(message, JSON.stringify(this.alertHistory.slice(-20)));
      default:
        return this.createResponse(message, 'Unknown', { error: true });
    }
  }

  private async handleNewBlock(block: any): Promise<void> {
    for (const alert of this.ruleEngine.evaluate('block', block))
      await this.dispatch(alert);
  }

  private async handleTransaction(tx: any): Promise<void> {
    for (const alert of this.ruleEngine.evaluate('transaction', tx))
      await this.dispatch(alert);
  }

  private async handleContractEvent(event: any): Promise<void> {
    for (const alert of this.ruleEngine.evaluate('event', event))
      await this.dispatch(alert);
  }

  private async handlePriceChange(change: any): Promise<void> {
    for (const alert of this.ruleEngine.evaluate('price', change))
      await this.dispatch(alert);
  }

  private async handleValidatorChange(change: any): Promise<void> {
    for (const alert of this.ruleEngine.evaluate('validator', change))
      await this.dispatch(alert);
  }

  private async dispatch(alert: any): Promise<void> {
    if (this.isPaused) return;
    this.alertHistory.push(alert);
    if (this.alertHistory.length > 1000)
      this.alertHistory = this.alertHistory.slice(-500);
    await this.alertDispatcher.dispatch(alert);
  }

  getStatus(): Record<string, unknown> {
    return {
      isPaused: this.isPaused, alertCount: this.alertHistory.length,
      monitors: {
        block: this.blockMonitor.isRunning(),
        transaction: this.txMonitor.isRunning(),
        event: this.eventMonitor.isRunning(),
        price: this.priceMonitor.isRunning(),
        validator: this.validatorMonitor.isRunning(),
      },
      uptime: process.uptime(),
    };
  }
}

6.4 告警分发器 — src/alerts/dispatcher.ts

export interface AlertChannel {
  name: string;
  enabled: boolean;
  send(alert: { title: string; message: string; severity: string }): Promise<boolean>;
}

export class AlertDispatcher {
  private channels: AlertChannel[];

  constructor(config: { channels: Record<string, any> }) {
    this.channels = [];
    if (config.channels.telegram?.enabled)
      this.channels.push(new (require('./telegram').TelegramChannel)(config.channels.telegram));
    if (config.channels.discord?.enabled)
      this.channels.push(new (require('./discord').DiscordChannel)(config.channels.discord));
    if (config.channels.webhook?.enabled)
      this.channels.push(new (require('./webhook').WebhookChannel)(config.channels.webhook));
  }

  async dispatch(alert: any): Promise<void> {
    const payload = {
      title: alert.title,
      message: alert.message,
      severity: alert.severity,
      details: alert.details,
    };
    await Promise.allSettled(this.channels.map(c => c.send(payload)));
  }

  async testAll(): Promise<void> {
    for (const c of this.channels) {
      try {
        await c.send({ title: 'Test', message: 'MSG Chain Monitor connected', severity: 'info' });
      } catch (err) {
        console.error(`Channel ${c.name} test failed:`, err);
      }
    }
  }
}

6.5 Telegram 通知 — src/alerts/telegram.ts

export class TelegramChannel {
  name = 'telegram';
  private botToken: string;
  private chatId: string;

  constructor(config: { botToken: string; chatId: string }) {
    this.botToken = config.botToken;
    this.chatId = config.chatId;
  }

  get enabled(): boolean { return !!(this.botToken && this.chatId); }

  async send(alert: { title: string; message: string; severity: string }): Promise<boolean> {
    const emoji = alert.severity === 'critical' ? '🚨' : alert.severity === 'warning' ? '⚠️' : 'ℹ️';
    const text = `${emoji} *${alert.title}*\n${alert.message}`;
    const resp = await fetch(`https://api.telegram.org/bot${this.botToken}/sendMessage`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ chat_id: this.chatId, text, parse_mode: 'Markdown' }),
    });
    return resp.ok;
  }
}

6.6 规则引擎 — src/filters/rule-engine.ts

export interface AlertRule {
  id: string;
  name: string;
  eventType: string;
  severity: 'info' | 'warning' | 'critical';
  condition: (data: any) => boolean;
  titleTemplate: string;
  messageTemplate: string;
}

export class RuleEngine {
  private rules: AlertRule[];

  constructor(rules: AlertRule[]) {
    this.rules = rules;
  }

  evaluate(eventType: string, data: any): Array<{ severity: string; title: string; message: string; details: any }> {
    return this.rules
      .filter(r => r.eventType === eventType && r.condition(data))
      .map(r => ({
        severity: r.severity,
        title: this.fillTemplate(r.titleTemplate, data),
        message: this.fillTemplate(r.messageTemplate, data),
        details: data,
      }));
  }

  addRule(rule: AlertRule): void {
    this.rules.push(rule);
  }

  count(): number { return this.rules.length; }

  private fillTemplate(tpl: string, data: any): string {
    return tpl.replace(/\{\{([^}]+)\}\}/g, (_, key) => {
      const val = key.split('.').reduce((o: any, k: string) => o?.[k], data);
      return val !== undefined ? String(val) : `{{${key}}}`;
    });
  }
}

6.7 Agent 配置 — src/agents.config.ts

export const agentsConfig = {
  name: process.env.AGENT_NAME || 'MonitorBot',
  version: '1.0.0',
  rpcUrl: process.env.MSG_RPC_URL || 'https://rpc.msg-chain-1.msg.network',
  restUrl: process.env.MSG_REST_URL || 'https://rest.msg-chain-1.msg.network',
  logLevel: (process.env.LOG_LEVEL as any) || 'info',
  blockCheckInterval: Number(process.env.BLOCK_CHECK_INTERVAL) || 6000,
  priceCheckInterval: Number(process.env.PRICE_CHECK_INTERVAL) || 30000,
  validatorCheckInterval: Number(process.env.VALIDATOR_CHECK_INTERVAL) || 60000,
  priceChangeThresholdBps: Number(process.env.PRICE_CHANGE_THRESHOLD_BPS) || 50,
  watchedAddresses: (process.env.WATCHED_ADDRESSES || '').split(',').filter(Boolean),
  watchedContracts: (process.env.WATCHED_CONTRACTS || '').split(',').filter(Boolean),
  watchedTokens: (process.env.WATCHED_TOKENS || '').split(',').filter(Boolean),
  channels: {
    telegram: { enabled: !!process.env.TG_BOT_TOKEN, botToken: process.env.TG_BOT_TOKEN || '', chatId: process.env.TG_CHAT_ID || '' },
    discord: { enabled: !!process.env.DISCORD_WEBHOOK, webhookUrl: process.env.DISCORD_WEBHOOK || '' },
    webhook: { enabled: !!process.env.CUSTOM_WEBHOOK, url: process.env.CUSTOM_WEBHOOK || '' },
  },
  rules: [
    {
      id: 'large-tx', name: 'Large Transaction', eventType: 'transaction',
      severity: 'warning',
      condition: (tx: any) => BigInt(tx.value || '0') > BigInt('100000000000000000000'),
      titleTemplate: 'Large Transaction Detected',
      messageTemplate: '{{from}} -> {{to}}: {{value}} MSG',
    },
    {
      id: 'price-drop', name: 'Price Drop', eventType: 'price',
      severity: 'critical',
      condition: (p: any) => p.changePercent < -5,
      titleTemplate: 'Price Drop Alert',
      messageTemplate: '{{symbol}} dropped {{changePercent}}%',
    },
  ],
};

7. 模板:DAO 治理 Agent

7.1 模板概览

DAO 治理 Agent 监控 DAO 提案、自动执行投票委托、分析社区情绪,并帮助用户参与链上治理。

7.2 目录结构

my-dao-agent/
├── src/
│   ├── agent.ts                   # 核心 Agent
│   ├── agents.config.ts           # 配置
│   ├── governance/
│   │   ├── contract.ts            # DAO 合约交互
│   │   ├── proposal.ts            # 提案解析
│   │   └── types.ts               # 类型
│   ├── voting/
│   │   ├── strategy.ts            # 投票策略
│   │   ├── delegator.ts           # 委托管理
│   │   └── calculator.ts          # 投票权计算
│   ├── analytics/
│   │   ├── sentiment.ts           # 社群情绪分析
│   │   └── tracker.ts             # 投票追踪
│   └── index.ts
├── tests/
├── .env.example
├── Dockerfile
├── package.json
└── tsconfig.json

7.3 核心 Agent — src/agent.ts

import { AgentBase, A2AMessage, Logger, WalletManager } from '@msg-chain/agent-sdk';
import { DaoContract } from './governance/contract';
import { ProposalParser } from './governance/proposal';
import { VotingStrategy } from './voting/strategy';
import { DelegationManager } from './voting/delegator';
import { agentsConfig } from './agents.config';

export interface Proposal {
  id: string;
  title: string;
  description: string;
  proposer: string;
  status: 'pending' | 'active' | 'passed' | 'rejected' | 'executed';
  startBlock: bigint;
  endBlock: bigint;
  forVotes: string;
  againstVotes: string;
  abstainVotes: string;
  quorum: string;
}

export interface VoteDecision {
  proposalId: string;
  support: 'for' | 'against' | 'abstain';
  reason: string;
  weight: string;
}

export class DAOAgent extends AgentBase {
  private daoContract: DaoContract;
  private proposalParser: ProposalParser;
  private votingStrategy: VotingStrategy;
  private delegationManager: DelegationManager;
  private wallet: WalletManager;
  private logger: Logger;
  private config: typeof agentsConfig;
  private trackedProposals: Map<string, Proposal>;
  private monitorInterval: ReturnType<typeof setInterval> | null;

  constructor() {
    super({
      name: agentsConfig.name, version: agentsConfig.version,
      addressPrefix: 'msg', chainId: 'msg-chain-1',
    });
    this.config = agentsConfig;
    this.logger = new Logger({ name: this.config.name, level: this.config.logLevel });
    this.trackedProposals = new Map();
    this.monitorInterval = null;

    this.wallet = new WalletManager({
      privateKey: this.config.wallet.privateKey, bech32Prefix: 'msg',
    });

    this.daoContract = new DaoContract({
      rpcUrl: this.config.rpcUrl,
      contractAddress: this.config.daoAddress,
      wallet: this.wallet,
    });

    this.proposalParser = new ProposalParser();

    this.votingStrategy = new VotingStrategy({
      defaultVote: this.config.defaultVote,
      minVotePower: this.config.minVotePower,
      autoVote: this.config.autoVoteEnabled,
    });

    this.delegationManager = new DelegationManager({
      daoContract: this.daoContract,
      delegateAddress: this.config.delegateAddress,
    });
  }

  async onStart(): Promise<void> {
    this.logger.info('DAOAgent starting', {
      daoAddress: this.config.daoAddress,
      autoVote: this.config.autoVoteEnabled,
    });

    await this.syncActiveProposals();

    this.monitorInterval = setInterval(
      () => this.monitorProposals(), this.config.checkIntervalMs);
  }

  async onStop(): Promise<void> {
    if (this.monitorInterval) clearInterval(this.monitorInterval);
  }

  async handleMessage(message: A2AMessage): Promise<A2AMessage> {
    switch (message.type) {
      case 'dao/proposals':
        return this.createResponse(
          message, JSON.stringify(Array.from(this.trackedProposals.values())));
      case 'dao/vote':
        return this.handleVoteCommand(message.content);
      case 'dao/delegate':
        return this.handleDelegateCommand(message.content);
      default:
        return this.createResponse(message, 'Unknown', { error: true });
    }
  }

  private async syncActiveProposals(): Promise<void> {
    const proposals = await this.daoContract.getActiveProposals();
    for (const p of proposals) {
      const parsed = this.proposalParser.parse(p);
      this.trackedProposals.set(parsed.id, parsed);
    }
    this.logger.info(`Synced ${proposals.length} active proposals`);
  }

  private async monitorProposals(): Promise<void> {
    try {
      const active = await this.daoContract.getActiveProposals();
      const syncedIds = new Set(active.map((p: any) => p.id));

      for (const [id, proposal] of this.trackedProposals) {
        if (!syncedIds.has(id) && proposal.status === 'active') {
          const updated = await this.daoContract.getProposal(id);
          if (updated) {
            const parsed = this.proposalParser.parse(updated);
            this.trackedProposals.set(id, parsed);

            if (parsed.status === 'passed' || parsed.status === 'rejected') {
              this.logger.info(`Proposal ${id} finalized as ${parsed.status}`);
            }
          }
        }
      }

      for (const p of active) {
        if (!this.trackedProposals.has(p.id)) {
          const parsed = this.proposalParser.parse(p);
          this.trackedProposals.set(parsed.id, parsed);
          this.logger.info(`New proposal detected: ${parsed.title}`);

          if (this.config.autoVoteEnabled) {
            await this.autoVote(parsed);
          }
        }
      }
    } catch (err) {
      this.logger.error('Monitor cycle failed', {
        error: err instanceof Error ? err.message : String(err),
      });
    }
  }

  async voteOnProposal(proposalId: string, support: 'for' | 'against' | 'abstain', reason?: string): Promise<string> {
    const txHash = await this.daoContract.castVote(proposalId, support);
    this.logger.info('Vote cast', { proposalId, support, txHash });
    return txHash;
  }

  async delegateVote(delegatee: string): Promise<string> {
    const txHash = await this.delegationManager.delegate(delegatee);
    this.logger.info('Vote delegated', { delegatee, txHash });
    return txHash;
  }

  private async autoVote(proposal: Proposal): Promise<void> {
    const decision = await this.votingStrategy.decide(proposal);
    if (decision) {
      const power = await this.daoContract.getVotingPower(this.wallet.getAddress());
      if (BigInt(power) >= BigInt(this.config.minVotePower)) {
        await this.voteOnProposal(proposal.id, decision.support, decision.reason);
      }
    }
  }

  private async handleVoteCommand(content: string): Promise<A2AMessage> {
    const { proposalId, support } = JSON.parse(content);
    const txHash = await this.voteOnProposal(proposalId, support);
    return {
      id: crypto.randomUUID(), sender: this.config.name,
      content: JSON.stringify({ txHash, status: 'voted' }),
      type: 'dao/vote-result', version: '1.0', timestamp: Date.now(),
    };
  }

  private async handleDelegateCommand(content: string): Promise<A2AMessage> {
    const { delegatee } = JSON.parse(content);
    const txHash = await this.delegateVote(delegatee);
    return {
      id: crypto.randomUUID(), sender: this.config.name,
      content: JSON.stringify({ txHash, status: 'delegated' }),
      type: 'dao/delegate-result', version: '1.0', timestamp: Date.now(),
    };
  }

  getStats(): Record<string, unknown> {
    return {
      name: this.config.name,
      trackedProposals: this.trackedProposals.size,
      address: this.wallet.getAddress(),
      votingPower: '0',
    };
  }
}

7.4 DAO 合约交互 — src/governance/contract.ts

import { ethers } from 'ethers';
import { WalletManager } from '@msg-chain/agent-sdk';

const DAO_ABI = [
  'function propose(string memory title, string memory description, bytes memory calldata) returns (uint256 proposalId)',
  'function castVote(uint256 proposalId, uint8 support) external',
  'function delegate(address delegatee) external',
  'function getProposal(uint256 proposalId) view returns (tuple(uint256 id, string title, string description, address proposer, uint8 status, uint256 startBlock, uint256 endBlock, uint256 forVotes, uint256 againstVotes, uint256 abstainVotes, uint256 quorum))',
  'function getActiveProposals() view returns (tuple(uint256 id, string title, string description, address proposer, uint8 status, uint256 startBlock, uint256 endBlock, uint256 forVotes, uint256 againstVotes, uint256 abstainVotes, uint256 quorum)[])',
  'function getVotingPower(address voter) view returns (uint256)',
  'function getDelegates(address delegator) view returns (address)',
  'event ProposalCreated(uint256 indexed proposalId, address indexed proposer, string title, uint256 startBlock, uint256 endBlock)',
  'event VoteCast(uint256 indexed proposalId, address indexed voter, uint8 support, uint256 weight)',
];

export class DaoContract {
  private contract: ethers.Contract;
  private wallet: WalletManager;

  constructor(config: { rpcUrl: string; contractAddress: string; wallet: WalletManager }) {
    const provider = new ethers.JsonRpcProvider(config.rpcUrl);
    this.wallet = config.wallet;
    this.contract = new ethers.Contract(config.contractAddress, DAO_ABI, provider);
  }

  async getActiveProposals(): Promise<any[]> {
    return this.contract.getActiveProposals();
  }

  async getProposal(id: string): Promise<any> {
    return this.contract.getProposal(id);
  }

  async castVote(proposalId: string, support: string): Promise<string> {
    const signer = await this.wallet.getSigner(this.contract.runner?.provider as any);
    const c = this.contract.connect(signer) as ethers.Contract;
    const supportNum = support === 'for' ? 1 : support === 'against' ? 0 : 2;
    const tx = await c.castVote(proposalId, supportNum);
    return (await tx.wait()).hash;
  }

  async delegate(delegatee: string): Promise<string> {
    const signer = await this.wallet.getSigner(this.contract.runner?.provider as any);
    const c = this.contract.connect(signer) as ethers.Contract;
    const tx = await c.delegate(delegatee);
    return (await tx.wait()).hash;
  }

  async getVotingPower(address: string): Promise<string> {
    const power = await this.contract.getVotingPower(address);
    return power.toString();
  }

  async getDelegates(address: string): Promise<string> {
    return this.contract.getDelegates(address);
  }
}

7.5 投票策略 — src/voting/strategy.ts

import { Proposal, VoteDecision } from '../agent';

interface StrategyConfig {
  defaultVote: 'for' | 'against' | 'abstain';
  minVotePower: string;
  autoVote: boolean;
}

export class VotingStrategy {
  private config: StrategyConfig;

  constructor(config: StrategyConfig) {
    this.config = config;
  }

  async decide(proposal: Proposal): Promise<VoteDecision | null> {
    if (!this.config.autoVote) return null;

    if (this.isAlignmentVote(proposal)) {
      return {
        proposalId: proposal.id,
        support: 'for',
        reason: 'Aligns with community values',
        weight: '0',
      };
    }

    return {
      proposalId: proposal.id,
      support: this.config.defaultVote,
      reason: `Auto-vote based on default strategy: ${this.config.defaultVote}`, weight: '0',
    };
  }

  private isAlignmentVote(proposal: Proposal): boolean {
    const positive = [
      'security', 'upgrade', 'bugfix', 'optimization',
      '安全', '升级', '优化', '修复',
    ];
    const title = proposal.title.toLowerCase();
    return positive.some(kw => title.includes(kw.toLowerCase()));
  }
}

7.6 Agent 配置 — src/agents.config.ts

export const agentsConfig = {
  name: process.env.AGENT_NAME || 'DAOMonitor',
  version: '1.0.0',
  rpcUrl: process.env.MSG_RPC_URL || 'https://rpc.msg-chain-1.msg.network',
  daoAddress: process.env.DAO_CONTRACT_ADDRESS || '',
  logLevel: (process.env.LOG_LEVEL as any) || 'info',
  autoVoteEnabled: process.env.AUTO_VOTE_ENABLED === 'true',
  defaultVote: (process.env.DEFAULT_VOTE as 'for' | 'against' | 'abstain') || 'abstain',
  minVotePower: process.env.MIN_VOTE_POWER || '1000000000000000000',
  checkIntervalMs: Number(process.env.CHECK_INTERVAL_MS) || 60000,
  delegateAddress: process.env.DELEGATE_ADDRESS || '',
  wallet: { privateKey: process.env.PRIVATE_KEY || '', hdPath: process.env.HD_PATH || "m/44'/60'/0'/0/0" },
};

8. 模板开发指南

8.1 模板结构与约定

每个模板是一个包含以下文件的目录:

my-custom-template/
├── template.json          # 模板元数据(必需)
├── src/                   # 源代码模板
│   ├── agent.ts.hbs       # Handlebars 模板文件
│   └── agents.config.ts.hbs
├── tests/                 # 测试模板
├── .env.example           # 环境变量示例
├── Dockerfile             # Docker 构建
├── package.json.hbs       # 带模板变量的 package.json
└── tsconfig.json          # TypeScript 配置

8.2 template.json 格式

{
  "name": "my-custom-template",
  "description": "我的自定义 Agent 模板",
  "version": "0.1.0",
  "author": "Your Name",
  "license": "MIT",
  "engine": "typescript",
  "tags": ["custom", "example"],
  "dependencies": {
    "@msg-chain/agent-sdk": "^0.2.0"
  },
  "devDependencies": {
    "typescript": "^5.3.0"
  },
  "variables": [
    {
      "key": "agent_name",
      "label": "Agent 名称",
      "default": "MyAgent",
      "validate": "string"
    },
    {
      "key": "description",
      "label": "描述",
      "default": "Custom Agent",
      "validate": "string"
    },
    {
      "key": "enable_feature_x",
      "label": "启用功能 X",
      "default": "false",
      "validate": "boolean"
    }
  ],
  "hooks": {
    "post-generate": "npm install"
  },
  "prompts": {
    "chat-bot": [
      {"key": "llm_provider", "label": "LLM 提供商", "default": "openai"}
    ]
  }
}

8.3 模板变量语法

支持两种模板变量格式:

  1. $variable — 简单替换
  2. ${variable} — 明确边界的替换
  3. {{#if var}}...{{/if}} — 条件块
  4. {{#each list as item}}...{{/each}} — 循环块

示例:

// src/agent.ts.hbs
import { AgentBase } from '@msg-chain/agent-sdk';

export class ${agent_name_pascal}Agent extends AgentBase {
  private name = '${agent_name}';

  async onStart(): Promise<void> {
    console.log('${agent_name} started');
  }

  {{#if enable_feature_x}}
  async featureX(): Promise<void> {
    // 条件编译的功能
  }
  {{/if}}
}

8.4 创建新模板的步骤

  1. 创建模板目录
mkdir -p my-template/src
cd my-template
  1. 编写 template.json

  2. 创建模板文件(使用 .hbs 扩展名标识需要模板替换的文件)

  3. 本地测试

msg-agent add my-template ./my-template
msg-agent init test-agent --template my-template
  1. 发布到远程仓库
tar czf my-template.tar.gz my-template/
# 上传到模板仓库,更新 registry index.json

8.5 模板开发最佳实践

8.6 模板注册表 API

远程模板注册表是一个 JSON 索引:

// https://registry.msg-chain.dev/templates/index.json
{
  "version": "1.0",
  "templates": [
    {
      "name": "chat-bot",
      "description": "AI 聊天机器人模板",
      "version": "0.1.0",
      "url": "https://registry.msg-chain.dev/templates/chat-bot-v0.1.0.zip",
      "checksum": "sha256:abc123...",
      "engine": "typescript",
      "tags": ["ai", "chat"]
    }
  ]
}

8.7 从零开始创建完整模板的示例

以下是一个最小模板的完整文件集:

template.json:

{
  "name": "minimal-agent",
  "description": "最小 Agent 模板",
  "version": "0.1.0",
  "author": "MSG Chain",
  "engine": "typescript",
  "variables": [
    {"key": "agent_name", "label": "名称", "default": "MinimalAgent"},
    {"key": "description", "label": "描述", "default": "Minimal Agent"}
  ]
}

src/agent.ts.hbs:

import { AgentBase, A2AMessage, Logger } from '@msg-chain/agent-sdk';

export class ${agent_name_pascal}Agent extends AgentBase {
  private logger: Logger;

  constructor() {
    super({
      name: '${agent_name}',
      version: '1.0.0',
      addressPrefix: 'msg',
      chainId: 'msg-chain-1',
    });
    this.logger = new Logger({ name: '${agent_name}' });
  }

  async onStart(): Promise<void> {
    this.logger.info('${agent_name} started');
  }

  async onStop(): Promise<void> {
    this.logger.info('${agent_name} stopped');
  }

  async handleMessage(message: A2AMessage): Promise<A2AMessage> {
    return this.createResponse(message,
      `Hello from ${agent_name}! You said: ${message.content}`);
  }
}

package.json.hbs:

{
  "name": "${project_name}",
  "version": "0.1.0",
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "tsc",
    "start": "node dist/index.js"
  },
  "dependencies": {
    "@msg-chain/agent-sdk": "^0.2.0"
  },
  "devDependencies": {
    "typescript": "^5.3.0",
    "tsx": "^4.0.0"
  }
}

.env.example:

MSG_RPC_URL=https://rpc.msg-chain-1.msg.network
AGENT_NAME=${agent_name}
PRIVATE_KEY=0x...
LOG_LEVEL=info

附录

A. 命令参考

命令 说明
msg-agent init <name> -t <template> 创建新项目
msg-agent list 列出可用模板
msg-agent fetch <template> 下载远程模板
msg-agent add <name> <path> 注册本地模板
msg-agent upgrade 升级 CLI 工具

B. 常用链接

C. 模板变量速查表

变量名 说明 来源
project_name 项目名称(kebab-case) 自动
project_name_snake 项目名称(snake_case) 自动
project_name_pascal 项目名称(PascalCase) 自动
bech32_prefix 地址前缀 (msg) 自动
chain_id 链 ID (msg-chain-1) 自动
rpc_url RPC 端点 自动
agent_name Agent 名称 交互输入
description Agent 描述 交互输入
fee_percent 交易手续费 交互输入
llm_provider LLM 提供商 交互输入
model_name 模型名称 交互输入
min_profit_bps 最小套利利润 交互输入
flashloan_provider 闪电贷提供商 交互输入
alert_channels 告警渠道 交互输入
dao_address DAO 合约地址 交互输入
min_vote_power 最低投票委托量 交互输入

本文档持续更新。欢迎提交 PR 贡献新模板或改进指南。


本文档内容基于 MSGChain 代码库真实状态编写,非 AI 自动生成。
主网状态: No-Go | 白皮书: https://msgchain.org/whitepaper/