weixin_clawbot 0.1.0 copy "weixin_clawbot: ^0.1.0" to clipboard
weixin_clawbot: ^0.1.0 copied to clipboard

A Flutter package for binding WeChat via QR code and receiving/sending messages through the WeChat iLink Bot API (OpenClaw ClawBot).

example/lib/main.dart

import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import 'package:weixin_clawbot/weixin_clawbot.dart';

/// Flutter Web 需要通过本地 CORS 代理访问 iLink API。
/// 运行代理服务器:dart run bin/proxy_server.dart
/// 然后用 --dart-define=PROXY_URL=http://localhost:3001 启动 Flutter Web。
const _webProxyUrl = String.fromEnvironment(
  'PROXY_URL',
  defaultValue: 'http://localhost:3001',
);

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'WeChat ClawBot 示例',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
        useMaterial3: true,
      ),
      home: const HomePage(),
    );
  }
}

/// 应用全局唯一的 ClawBot 实例。
/// 在 Flutter Web 上通过本地 CORS 代理转发请求;原生平台直连 iLink API。
final _clawbot = WeixinClawbot(
  proxyBaseUrl: kIsWeb ? _webProxyUrl : null,
);

// ─────────────────────────────────────────────────────────────────────────────

/// 示例应用的首页,整合了扫码绑定、消息接收与发送三个核心流程。
///
/// 页面状态由 [_account] 决定:
/// - `null`  → 显示扫码绑定页([_buildLogin])
/// - 非 null → 显示聊天页([_buildChat])
class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  ClawBotAccount? _account;
  final List<WeixinMessage> _messages = [];
  final _controller = TextEditingController();
  final _toController = TextEditingController();
  bool _sending = false;

  @override
  void initState() {
    super.initState();
    // 应用启动时尝试从本地存储中恢复已绑定账号,若存在则直接重连。
    _init();
  }

  /// 从持久化存储中加载已绑定账号并自动重连。
  /// 如果尚未绑定,[_account] 保持为 null,页面展示扫码入口。
  Future<void> _init() async {
    final account = await _clawbot.loadAccount();
    if (account != null && mounted) {
      _connectAccount(account);
    }
  }

  /// 将 [account] 设为当前账号并启动长轮询消息监听。
  ///
  /// - 更新 UI 状态(切换到聊天页)。
  /// - 若收件人框为空,用 [ClawBotAccount.defaultTo] 预填(绑定时自动写入)。
  /// - 监听 [WeixinClawbot.connect] 返回的消息流,收到消息后追加到列表。
  void _connectAccount(ClawBotAccount account) {
    setState(() => _account = account);
    if (_toController.text.isEmpty && account.defaultTo != null) {
      _toController.text = account.defaultTo!;
    }
    _clawbot.connect(account).listen((msg) {
      if (mounted) {
        setState(() => _messages.add(msg));
      }
    });
  }

  /// 弹出 QR 扫码绑定对话框。
  ///
  /// 用户在微信扫码并确认后,[showQrLoginDialog] 返回新账号;
  /// 此时调用 [_connectAccount] 切换页面并开始接收消息。
  Future<void> _login() async {
    final account = await showQrLoginDialog(
      context: context,
      clawbot: _clawbot,
    );
    if (account != null && mounted) {
      _connectAccount(account);
    }
  }

  /// 发送输入框中的文本消息。
  ///
  /// 校验逻辑:
  /// 1. 消息内容不能为空。
  /// 2. 收件人 ID 不能为空(通常由 [ClawBotAccount.defaultTo] 自动填充)。
  ///
  /// 发送成功后清空消息输入框;失败时通过 [SnackBar] 显示错误原因。
  Future<void> _send() async {
    final text = _controller.text.trim();
    final to = _toController.text.trim();
    if (text.isEmpty) return;
    if (to.isEmpty) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(content: Text('请先填写收件人 ID')),
      );
      return;
    }

    setState(() => _sending = true);
    final result = await _clawbot.sendText(text: text, toUserId: to);
    setState(() => _sending = false);

    if (!mounted) return;
    if (!result.ok) {
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(content: Text('发送失败:${result.error}')),
      );
    } else {
      _controller.clear();
    }
  }

  /// 解绑当前账号:清除持久化凭证、停止消息轮询、重置页面状态。
  Future<void> _logout() async {
    await _clawbot.logout();
    if (mounted) {
      setState(() {
        _account = null;
        _messages.clear();
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('WeChat ClawBot'),
        actions: [
          if (_account != null)
            IconButton(
              icon: const Icon(Icons.logout),
              tooltip: '解绑',
              onPressed: _logout,
            ),
        ],
      ),
      body: _account == null ? _buildLogin() : _buildChat(),
    );
  }

  Widget _buildLogin() {
    return Center(
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (kIsWeb) const _WebProxyBanner(proxyUrl: _webProxyUrl),
          const Icon(Icons.qr_code, size: 64, color: Colors.grey),
          const SizedBox(height: 16),
          const Text('尚未绑定微信账号'),
          const SizedBox(height: 24),
          FilledButton.icon(
            onPressed: _login,
            icon: const Icon(Icons.link),
            label: const Text('扫码绑定微信'),
          ),
        ],
      ),
    );
  }

  Widget _buildChat() {
    return Column(
      children: [
        // Account info banner
        Container(
          color: Colors.green.shade50,
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
          child: Row(
            children: [
              const Icon(Icons.check_circle, color: Colors.green, size: 16),
              const SizedBox(width: 8),
              Expanded(
                child: Text(
                  '已绑定 · Bot: ${_account!.botId}',
                  style: const TextStyle(fontSize: 12),
                  overflow: TextOverflow.ellipsis,
                ),
              ),
            ],
          ),
        ),

        // Message list
        Expanded(
          child: _messages.isEmpty
              ? const Center(
                  child: Text(
                    '暂无消息\n等待微信消息中…',
                    textAlign: TextAlign.center,
                    style: TextStyle(color: Colors.grey),
                  ),
                )
              : ListView.builder(
                  padding: const EdgeInsets.all(8),
                  itemCount: _messages.length,
                  itemBuilder: (_, i) => _MessageBubble(msg: _messages[i]),
                ),
        ),

        // Input bar
        Padding(
          padding: EdgeInsets.fromLTRB(
            8,
            4,
            8,
            MediaQuery.of(context).viewInsets.bottom + 8,
          ),
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              TextField(
                controller: _toController,
                decoration: const InputDecoration(
                  hintText: '收件人 ID(to_user_id)',
                  labelText: '收件人',
                  border: OutlineInputBorder(),
                  contentPadding:
                      EdgeInsets.symmetric(horizontal: 12, vertical: 8),
                  isDense: true,
                ),
                style: const TextStyle(fontSize: 12),
              ),
              const SizedBox(height: 6),
              Row(
                children: [
                  Expanded(
                    child: TextField(
                      controller: _controller,
                      decoration: const InputDecoration(
                        hintText: '发送消息…',
                        border: OutlineInputBorder(),
                        contentPadding:
                            EdgeInsets.symmetric(horizontal: 12, vertical: 8),
                      ),
                      onSubmitted: (_) => _send(),
                    ),
                  ),
                  const SizedBox(width: 8),
                  _sending
                      ? const SizedBox(
                          width: 44,
                          height: 44,
                          child: CircularProgressIndicator(strokeWidth: 2),
                        )
                      : IconButton.filled(
                          onPressed: _send,
                          icon: const Icon(Icons.send),
                        ),
                ],
              ),
            ],
          ),
        ),
      ],
    );
  }

  @override
  void dispose() {
    _controller.dispose();
    _toController.dispose();
    _clawbot.dispose();
    super.dispose();
  }
}

// ─────────────────────────────────────────────────────────────────────────────

class _MessageBubble extends StatelessWidget {
  final WeixinMessage msg;

  const _MessageBubble({required this.msg});

  @override
  Widget build(BuildContext context) {
    final isUser = msg.isFromUser;
    final text = msg.textContent ?? '[非文本消息]';
    final time = DateTime.fromMillisecondsSinceEpoch(msg.createTimeMs);
    final timeStr =
        '${time.hour.toString().padLeft(2, '0')}:${time.minute.toString().padLeft(2, '0')}';

    return Align(
      alignment: isUser ? Alignment.centerLeft : Alignment.centerRight,
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 8),
        padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
        constraints: BoxConstraints(
          maxWidth: MediaQuery.of(context).size.width * 0.72,
        ),
        decoration: BoxDecoration(
          color: isUser ? Colors.grey.shade200 : Colors.green.shade100,
          borderRadius: BorderRadius.circular(12),
        ),
        child: Column(
          crossAxisAlignment:
              isUser ? CrossAxisAlignment.start : CrossAxisAlignment.end,
          children: [
            Text(text),
            const SizedBox(height: 2),
            Text(
              '$timeStr · ${msg.fromUserId.split('@').first}',
              style: const TextStyle(fontSize: 10, color: Colors.grey),
            ),
          ],
        ),
      ),
    );
  }
}

// ─────────────────────────────────────────────────────────────────────────────

/// Shown on Flutter Web to guide the user to start the CORS proxy server.
class _WebProxyBanner extends StatelessWidget {
  final String proxyUrl;

  const _WebProxyBanner({required this.proxyUrl});

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: const EdgeInsets.only(bottom: 24),
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
      decoration: BoxDecoration(
        color: Colors.amber.shade50,
        border: Border.all(color: Colors.amber.shade300),
        borderRadius: BorderRadius.circular(8),
      ),
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        mainAxisSize: MainAxisSize.min,
        children: [
          Row(
            children: [
              Icon(Icons.info_outline, size: 16, color: Colors.amber.shade800),
              const SizedBox(width: 6),
              Text(
                'Flutter Web 需要 CORS 代理',
                style: TextStyle(
                  fontWeight: FontWeight.bold,
                  color: Colors.amber.shade900,
                  fontSize: 13,
                ),
              ),
            ],
          ),
          const SizedBox(height: 6),
          Text(
            '在 example 目录运行:',
            style: TextStyle(fontSize: 12, color: Colors.amber.shade900),
          ),
          const SizedBox(height: 4),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
            decoration: BoxDecoration(
              color: Colors.grey.shade900,
              borderRadius: BorderRadius.circular(4),
            ),
            child: const Text(
              'dart run bin/proxy_server.dart',
              style: TextStyle(
                fontFamily: 'monospace',
                fontSize: 12,
                color: Colors.greenAccent,
              ),
            ),
          ),
          const SizedBox(height: 4),
          Text(
            '代理地址:$proxyUrl',
            style: TextStyle(fontSize: 11, color: Colors.amber.shade700),
          ),
        ],
      ),
    );
  }
}
1
likes
0
points
21
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter package for binding WeChat via QR code and receiving/sending messages through the WeChat iLink Bot API (OpenClaw ClawBot).

Repository (GitHub)
View/report issues

License

unknown (license)

Dependencies

flutter, http, qr_flutter, shared_preferences, uuid

More

Packages that depend on weixin_clawbot