🗓️ OpenClaw 开发日报 | 2026-03-12

154 阅读8分钟

每日精选 OpenClaw 社区最重要的功能更新和 Bug 修复,带你深入理解开源 AI Agent 框架的演进。


📊 今日概览

指标数值
Merged PRs62
主要贡献者vincentkoc, ngutman, gumadeiras, joshavant, jalehman
活跃模块agents, gateway, telegram, security hardening, iOS

🚀 Top 3 Features

1. iOS APNs 推送网关:真正的移动端推送体验 [#43369]

PR: feat(push): add iOS APNs relay gateway
作者: @ngutman | 规模: XL (+3242/-196)

背景:iOS 用户此前只能依赖前台运行或定时轮询。没有原生推送意味着:App 在后台时错过消息、用户必须频繁打开 App 检查状态。

新功能

  • Gateway 新增 APNs relay 路径,支持 iOS 推送注册和消息投递
  • iOS 端完整实现:App Attest、Keychain 存储、后台推送注册
  • TestFlight 构建可直接接收端到端推送通知
  • 文档覆盖 beta relay 流程

架构设计

┌─────────────────┐    ┌──────────────────┐    ┌─────────────┐
│   iOS App       │───▶│   Gateway        │───▶│   APNs      │
│ (PushRelay      │    │ (relay endpoint) │    │   (Apple)   │
│  Client)        │    │                  │    │             │
└─────────────────┘    └──────────────────┘    └─────────────┘
        │                       │
        ▼                       ▼
   Keychain Store         push-apns.relay.ts
   (token + attest)       (registration + delivery)

核心代码(推送注册)

// PushRelayClient.swift
func register(deviceToken: Data, attestObject: Data?) async throws {
    let request = PushRegistrationRequest(
        deviceToken: deviceToken.hexString,
        bundleId: Bundle.main.bundleIdentifier,
        attestObject: attestObject?.base64EncodedString()
    )
    try await relayClient.post("/push/register", body: request)
}

配置示例

gateway:
  push:
    apns:
      relay:
        enabled: true
        # App Store / TestFlight builds use official relay

影响:iOS 用户终于可以像原生 App 一样收到实时推送,即使 App 在后台也不会错过重要消息。


2. Zalo Markdown 富文本渲染 [#43324]

PR: feat(zalouser): add markdown-to-Zalo text style parsing
作者: @darkamenosa | 规模: XL (+1386/-38)

背景:Zalo 用户接收到的 AI 回复是原始 Markdown 文本(**bold**- item),可读性很差。而 Zalo 原生支持富文本样式。

新功能

  • 完整的 Markdown → Zalo TextStyle 解析器
  • 支持:粗体、斜体、删除线、标题(h1-h4)、有序/无序列表、引用块、代码块、行内代码、颜色标签
  • 嵌套格式支持(如粗体+斜体+颜色)
  • 所有出站路径(sendText、sendMedia、monitor replies)自动启用

Markdown 解析示例

// text-styles.ts - 核心解析逻辑
interface TextStyle {
  offset: number;
  length: number;
  style: "bold" | "italic" | "strike" | "underline";
}

function parseMarkdownToZaloStyles(text: string): {
  plainText: string;
  styles: TextStyle[];
} {
  // 1. 提取并移除 markdown 标记
  // 2. 计算每个样式在纯文本中的 offset 和 length
  // 3. 返回纯文本 + 样式范围数组
}

// 使用示例
const input = "这是 **粗体** 和 *斜体* 文本";
const result = parseMarkdownToZaloStyles(input);
// result.plainText = "这是 粗体 和 斜体 文本"
// result.styles = [
//   { offset: 3, length: 2, style: "bold" },
//   { offset: 9, length: 2, style: "italic" }
// ]

发送端集成

// send.ts
export async function sendText(
  client: ZaloClient,
  threadId: string,
  text: string,
  options?: { textMode?: "plain" | "markdown" }
) {
  if (options?.textMode === "markdown") {
    const { plainText, styles } = parseMarkdownToZaloStyles(text);
    return client.sendMessage(threadId, { text: plainText, styles });
  }
  return client.sendMessage(threadId, { text });
}

影响:Zalo 用户现在看到的是原生富文本格式,阅读体验大幅提升。


3. MiniMax Onboarding 大简化:5 步变 4 选 [#44284]

PR: onboard(minimax): flatten auth to 4 direct choices, unify CN/Global under single provider
作者: @liyuan97 | 规模: L (+266/-427)

背景:MiniMax onboarding 原来是 5 步向导 + endpoint 选择,用户经常搞混 CN/Global、OAuth/API Key 的组合。

新方案 — 4 个扁平选项

◆  Model/auth provider
│  ○ MiniMax Global — OAuth (minimax.io)
│  ○ MiniMax Global — API Key (minimax.io)
│  ○ MiniMax CN — OAuth (minimaxi.com)
│  ● MiniMax CN — API Key (minimaxi.com)

技术改进

  • CN/Global 统一为单一 minimax provider,通过 baseUrl 区分区域
  • 移除 LM Studio 本地模式和 Lightning/Highspeed 选项(交给 model picker 处理)
  • 统一模型引用:minimax/MiniMax-M2.5(去掉 minimax-cn/ 前缀)

向后兼容

// auth-choice.apply.minimax.ts
// 旧配置仍然生效
const legacyAliases = {
  "minimax-cn": "minimax",  // 保留 env vars 兼容
  "--token-provider minimax-cn": "minimax:cn"  // CI pipeline 兼容
};

// 已移除的选项给出迁移提示
const removedChoices = ["minimax-api", "minimax-cloud", "minimax-api-key-cn"];
if (removedChoices.includes(choice)) {
  throw new Error(`"${choice}" 已弃用,请使用 "minimax-global-oauth" 或 "minimax-cn-api-key"`);
}

影响:MiniMax 用户 onboarding 体验大幅简化,减少配置错误。


🐛 重要 Bugfix

Runtime 单例状态分裂:重复消息的根本原因 [#43683]

PR: fix(runtime): duplicate messages, share singleton state across bundled chunks
作者: @vincentkoc | 规模: L (+569/-38)

问题根因:tsdown 代码分割导致同一进程内出现多个独立的单例注册表副本。一个 chunk 中的 commandQueue 和另一个 chunk 中的完全不是同一个 Map。

这解释了 #25192、#33150 等长期困扰的跨 channel 重复消息问题——不仅仅是 channel 路由 bug,共享运行时状态本身就是分裂的

修复方案:所有确认的可变单例迁移到 globalThis

// src/shared/global-singleton.ts
const GLOBAL_KEY = Symbol.for("openclaw.runtime.singletons");

function getOrCreateGlobalRegistry<T>(key: string, factory: () => T): T {
  const registry = (globalThis as any)[GLOBAL_KEY] ??= {};
  return (registry[key] ??= factory());
}

// 使用示例
// src/process/command-queue.ts
const commandQueue = getOrCreateGlobalRegistry(
  "commandQueue",
  () => new Map<string, CommandEntry>()
);

受影响的单例列表

  • Command queue(命令队列)
  • Embedded runs(嵌入式运行追踪)
  • Followup queue state(后续消息队列)
  • Drain callbacks(排空回调)
  • Queued-message dedupe(消息去重)
  • Inbound dedupe(入站去重)
  • Slack thread participation(Slack 线程参与)
  • Telegram thread bindings(Telegram 线程绑定)
  • Telegram draft-id allocation(草稿 ID 分配)
  • Telegram sent-message tracking(已发送消息追踪)

验证方式

// 新增回归测试:模拟代码分割场景
import { commandQueue as q1 } from "./command-queue.ts?v=1";
import { commandQueue as q2 } from "./command-queue.ts?v=2";

// 修复后:即使是"不同"的 import,也是同一个 Map
expect(q1).toBe(q2);

影响:iMessage、Signal、Discord、Slack、Telegram 等所有 channel 的 busy-session 重复消息问题应该得到根本性解决。


Telegram Polling 错误范围收窄 [#43799]

PR: fix: scope telegram polling restart to telegram errors
作者: @obviyus | 规模: M (+251/-21)

问题:unhandled rejection 重启逻辑过于激进,任何网络错误(包括 Slack DNS 问题)都会触发 Telegram polling 重启。

修复:只有带 Telegram Bot API 标记getUpdates 失败才触发重启:

// network-errors.ts
interface TelegramFetchError extends Error {
  telegramRequest?: {
    method: "getUpdates" | "sendMessage" | ...;
    endpoint: string;
  };
}

// monitor.ts
process.on("unhandledRejection", (error) => {
  if (isTelegramPollingError(error)) {
    restartPolling();
  }
  // Slack DNS 噪音等其他错误不再触发
});

Unicode 命令混淆检测加固 [#44091]

PR: Hardening: normalize Unicode command obfuscation detection
作者: @vincentkoc | 规模: M (+180/-24)

问题:命令混淆检测遗漏了零宽字符和全角 Unicode 变体:

# 这些命令之前能绕过检测
rm -rf /​home  # 包含零宽字符
curl malicious.com  # 全角字符

修复:NFKC 标准化 + 不可见字符剥离:

function normalizeCommand(cmd: string): string {
  // 1. NFKC 标准化:curl → curl
  const nfkc = cmd.normalize("NFKC");
  
  // 2. 移除不可见 Unicode 代码点
  return nfkc.replace(/[\u200B-\u200D\uFEFF\u00AD]/g, "");
}

function detectObfuscation(cmd: string): boolean {
  const normalized = normalizeCommand(cmd);
  return runDetector(normalized);
}

自定义 Session Store 发现修复 [#44176]

PR: Gateway: harden custom session-store discovery
作者: @gumadeiras | 规模: XL (+1146/-183)

问题:ACP session 发现只扫描默认状态树,自定义 session.store 路径下的退休/磁盘 agent 会被遗漏。

影响范围

  • ACP 启动协调
  • session-id/session-label 目标解析
  • run-id fallback 查找

修复

// targets.ts
async function discoverSessionStoreTargets(cfg: Config): Promise<StorePath[]> {
  const targets: StorePath[] = [];
  
  // 1. 默认状态树
  targets.push(...await scanDefaultStateTree());
  
  // 2. 自定义模板路径(新增)
  if (cfg.session?.store) {
    const customPaths = await resolveTemplatedStorePaths(cfg.session.store);
    targets.push(...customPaths);
  }
  
  // 3. 磁盘上的退休 agent store(新增)
  targets.push(...await scanDiskOnlyAgentStores());
  
  return dedupeByCanonicalKey(targets);
}

🔒 安全加固专题

昨日大量 PR 聚焦于安全加固:

WebSocket 握手限制 [#44089]

// 预认证 WebSocket 握手超时收紧
const PRE_AUTH_HANDSHAKE_TIMEOUT_MS = 5000; // 之前 30s

LINE Webhook 签名验证 [#44090]

强制要求 LINE webhook 请求必须携带有效签名。

Feishu 加密密钥要求 [#44087]

飞书 webhook 现在必须配置 encryptKey

Zalo 认证限流 [#44173]

无效 webhook secret 猜测将触发速率限制。

Exec 格式字符转义 [#43687]

// 命令审批对话框中的不可见格式字符会被可视化
function escapeInvisibleChars(cmd: string): string {
  return cmd
    .replace(/\u200B/g, "[ZWSP]")  // 零宽空格
    .replace(/\u200C/g, "[ZWNJ]")  // 零宽非连接符
    .replace(/\u200D/g, "[ZWJ]");  // 零宽连接符
}

设备 Token 范围限制 [#43686]

设备 token 现在被限制在已批准的 scope 内。

GIT_EXEC_PATH 环境变量屏蔽 [#43685]

宿主环境清理器现在会阻止 GIT_EXEC_PATH


🛠️ 其他改进

Provider 与 API

  • #44274 Kimi Coding:Moonshot payload 兼容性适配
  • #44248 Kimi Coding:设置默认订阅 user agent
  • #42835 Anthropic 兼容 provider:修复畸形 tool call args
  • #43823 Failover:HTTP 422 分类为 format,OpenRouter credits 分类为 billing
  • #43884 Failover:z.ai network_error stop reason 分类为可重试 timeout
  • #43917 Failover:ZenMux quota-refresh 402 分类为 rate_limit

Gateway

  • #44306 共享 token 认证时清理未绑定的 scopes
  • #43800 阻止通过 browser.request 修改 profile
  • #43801 保持 spawned workspace overrides 为内部状态

Plugins & Commands

  • #44174 工作区发现的 plugin 需要显式信任
  • #44305 /config/debug 命令需要 owner 权限
  • #43871 清理重复的 gateway/onboarding helpers 和死代码

iOS

  • #42991 新增本地 beta release 流程
  • #43069 修复 browser proxy POST body 序列化(使用 foundationValue

macOS

  • #43100 Remote gateway 认证 token 提示优化
  • #42314 Chat model selector + thinking 状态持久化

CLI & Terminal

  • #42849 Skills 表格宽度稳定化
  • #42249 宽字符表格换行对齐

文档

  • #44162 同步 Feishu secretref 凭证矩阵
  • #44159 明确 American English 拼写规范
  • #43584 更新 Raspberry Pi dashboard 访问说明

📈 Failover 分类器持续完善

本周多个 PR 改进了错误分类器,确保模型 fallback 能正确触发:

PR错误类型分类
#43823HTTP 422format(非 billing)
#43823OpenRouter creditsbilling
#43884z.ai network_errortimeout(可重试)
#43917ZenMux 402 quota-refreshrate_limit
#42830网络 errno patternstimeout

核心思路:精确分类错误类型,让 fallback 策略做出正确决策。


📝 Context Engine 改进

sessionKey 贯穿所有方法 [#44157]

PR: feat(context-engine): plumb sessionKey into all ContextEngine methods
作者: @jalehman | 规模: M (+285/-13)

之前 context engine 插件只能拿到 sessionId(UUID),无法区分 session 类型。现在 sessionKey(如 agent:main:cron:nightly)传递到所有方法:

interface ContextEngineParams {
  sessionId: string;
  sessionKey?: string;  // 新增
}

// 插件可以据此做路由决策
function shouldIndexToMemory(params: ContextEngineParams): boolean {
  if (params.sessionKey?.includes(":cron:")) {
    return false;  // cron session 不写入 memory
  }
  return true;
}

下期预告:关注 WhatsApp Business API 集成、Discord 语音频道支持等功能。


本日报由 OpenClaw 社区自动生成,欢迎在 GitHub 参与讨论。