每日精选 OpenClaw 社区最重要的功能更新和 Bug 修复,带你深入理解开源 AI Agent 框架的演进。
📊 今日概览
| 指标 | 数值 |
|---|---|
| Merged PRs | 62 |
| 主要贡献者 | 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 统一为单一
minimaxprovider,通过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 | 错误类型 | 分类 |
|---|---|---|
| #43823 | HTTP 422 | format(非 billing) |
| #43823 | OpenRouter credits | billing |
| #43884 | z.ai network_error | timeout(可重试) |
| #43917 | ZenMux 402 quota-refresh | rate_limit |
| #42830 | 网络 errno patterns | timeout |
核心思路:精确分类错误类型,让 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 参与讨论。