lightclawbot
v1.0.6
Published
LightClawBot channel plugin with message support, cron jobs, and proactive messaging
Maintainers
Readme
LightClaw — OpenClaw Channel 插件
对接 OpenClaw 框架的 LightClaw Bot channel 插件,支持多 apiKey(多账户)模式。
目录
配置
~/.openclaw/openclaw.json 中 channels.lightclawbot 段:
{
"channels": {
"lightclawbot": {
"apiKeys": ["key-1", "key-2", "key-3"],
"enabled": true,
"dmScope": "per-channel-peer"
}
}
}| 字段 | 说明 |
|------|------|
| apiKeys | apiKey 数组,每个 key 对应一个 uin(用户身份)。apiKeys[0] 为主 key,同时作为默认 fallback |
| dmScope | 会话隔离粒度,推荐 "per-channel-peer"(每用户独立 session) |
注意:配置中只有
apiKeys(复数数组),没有apiKey(单数字段)。运行时account.apiKey的值取自apiKeys[0]。
多账户 apiKey 体系
整体架构
多 apiKey 模式下,一个 Bot 可以持有多个 apiKey,每个 apiKey 关联到不同的 uin(用户身份)。插件需要在处理消息和执行工具时,正确选取当前用户对应的 apiKey。
核心挑战:消息处理(inbound) 和 工具执行(tool) 拿到的上下文信息不同:
| 阶段 | 可用标识 | 不可用标识 |
|------|---------|-----------|
| inbound 消息到达 | msg.senderId(uin) | sessionKey(需路由解析后才知道) |
| tool 执行 | ctx.sessionKey | uin(框架不传递) |
因此需要 两级映射 来桥接两个阶段。
数据流全景
┌─────────────────────────────────────────────────────────────────┐
│ Gateway 启动(一次性) │
│ │
│ 1. 读取配置中的 apiKeys 数组 │
│ 2. 对每个 apiKey 调用 /user/current 获取其 uin │
│ 3. 构建 uin→apiKey 映射表(apiKeyMap) │
│ 4. setApiKeyMap(apiKeyMap, apiKeys[0]) │
│ ├─ globalApiKeyMap = { uin_A→key_1, uin_B→key_2, ... } │
│ └─ globalDefaultApiKey = apiKeys[0] (fallback 兜底) │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Inbound 消息处理(每条消息) │
│ │
│ 1. 收到消息,msg.senderId = uin_A │
│ 2. resolveEffectiveApiKey({ senderId: uin_A }) │
│ → 命中 globalApiKeyMap → 返回 key_1 │
│ 3. resolveAgentRoute(...) → 得到 route.sessionKey │
│ 4. setSessionApiKey(route.sessionKey, key_1) │
│ └─ sessionKeyToApiKey = { "agent:main:lc:direct:uin_A"→key_1 } │
│ 5. 处理消息、下载/上传附件(均使用 key_1) │
│ 6. 分发给 AI 引擎 │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Tool 执行(AI 调用工具时) │
│ │
│ 1. 框架传入 ctx.sessionKey = "agent:main:lc:direct:uin_A" │
│ 2. resolveEffectiveApiKey({ sessionKey }) │
│ → 命中 sessionKeyToApiKey → 返回 key_1 │
│ 3. 使用 key_1 上传/下载文件到 COS │
└─────────────────────────────────────────────────────────────────┘两级映射设计
config.ts 中维护的全局状态:
┌─────────────────────────────────────────────────────────┐
│ 第1级:globalApiKeyMap (uin → apiKey) │
│ 写入时机:gateway 启动时(setApiKeyMap,一次性) │
│ 读取时机:inbound 处理消息时 │
│ 数据规模:= apiKeys 数量(固定,不随消息增长) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 第2级:sessionKeyToApiKey (sessionKey → apiKey) │
│ 写入时机:每条消息 inbound 处理时(setSessionApiKey) │
│ 读取时机:tool 执行时 │
│ 数据规模:= 活跃用户数(动态增长,但不会很大) │
└─────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────┐
│ 兜底:globalDefaultApiKey │
│ 值 = apiKeys[0],当以上两级都未命中时使用 │
└─────────────────────────────────────────────────────────┘resolveEffectiveApiKey 统一入口
所有需要获取 apiKey 的地方(inbound / upload-tool / download-tool)统一调用此函数:
resolveEffectiveApiKey(params: {
sessionKey?: string; // tool 执行时传入
senderId?: string; // inbound 处理时传入
}): string查找优先级:
| 优先级 | 数据源 | 场景 |
|:------:|--------|------|
| 1 | sessionKeyToApiKey[sessionKey] | tool 执行时的主路径 |
| 2 | globalApiKeyMap[senderId] | inbound 处理时的主路径 |
| 3 | globalApiKeyMap[extractUinFromSessionKey(sessionKey)] | 兜底:从 sessionKey 解析 uin |
| 4 | globalDefaultApiKey | 最终 fallback |
为什么不能只用一级映射
方案:只保留 sessionKeyToApiKey,去掉 globalApiKeyMap?
不可行,原因是 时序依赖:
用户首条消息到达 inbound:
│
├─ 此时需要 apiKey(用于下载附件等)
│ → 但 sessionKey 尚未算出(要先调 resolveAgentRoute)
│ → 即使 sessionKey 已知,sessionKeyToApiKey 中没有记录(首条消息,从未写入过)
│ → ❌ 无法获取 apiKey
│
└─ 而 uin(msg.senderId)立即可用
→ globalApiKeyMap 在 gateway 启动时就已构建好
→ ✅ 可直接查到 apiKeyglobalApiKeyMap 是冷启动数据源(gateway 启动时预建),sessionKeyToApiKey 是运行时缓存(消息处理时按需写入)。 两者解决不同阶段的问题,缺一不可。
关键模块说明
| 文件 | 职责 | 与 apiKey 体系的关系 |
|------|------|---------------------|
| src/config.ts | 配置解析 + apiKey 映射管理 | 定义 setApiKeyMap、setSessionApiKey、resolveEffectiveApiKey |
| src/gateway.ts | Socket.IO 连接生命周期管理 | 启动时调用 resolveApiKeyIdentities + setApiKeyMap 构建第1级映射 |
| src/inbound.ts | 入站消息处理 | 调用 resolveEffectiveApiKey({ senderId }) 获取 apiKey,再调用 setSessionApiKey 写入第2级映射 |
| src/upload-tool.ts | 文件上传工具 | 调用 resolveEffectiveApiKey({ sessionKey }) 获取 apiKey,传给 COS 上传 |
| src/download-tool.ts | 文件下载/转发工具 | 同上 |
| src/file-storage.ts | COS 文件存储封装 | 接收 { apiKey } 参数,执行实际的 HTTP 上传/下载 |
gateway.ts 中的 resolveApiKeyIdentities
对每个 apiKey 调用 /user/current,一次遍历完成:
- 提取每个 key 对应的 uin,构建 uin→apiKey 映射
- 从第一个成功的 key 中提取 botClientId(所有 key 共享同一个 bot)
- 容错:第一个 key 必须成功(否则无 botId,直接抛异常),后续 key 失败可降级跳过
- 总计 N 次 HTTP 请求,无重复调用inbound.ts 中的 mainSessionKey 安全约束
// ⚠️ 只写入 per-channel-peer 的 sessionKey,不写入 mainSessionKey
// mainSessionKey(= "agent:main:main")是全局共享的,所有用户消息都会覆盖它,
// 导致最后一个用户的 apiKey 覆盖前一个用户,产生并发安全问题。
if (route?.sessionKey) {
setSessionApiKey(route.sessionKey, effectiveApiKey);
}调试指南
查看 apiKey 选取日志
upload-tool 和 download-tool 中保留了 log.warn 级别的调试日志:
[lightclaw_upload_file] sessionKey="agent:main:lightclawbot:direct:12345", accountId="default"
[lightclaw_upload_file] resolved apiKey="key1abcd..."这些日志在终端始终可见(warn 级别不会被框架过滤)。
框架日志级别说明
| 级别 | 终端可见性 | 适用场景 |
|------|-----------|---------|
| log.debug | 通常不可见 | 详细流程追踪 |
| log.info | 取决于配置 | 常规信息 |
| log.warn | 始终可见 | 调试关键路径、apiKey 选取 |
| log.error | 始终可见 | 错误 |
apiKey 日志脱敏
所有日志中 apiKey 只打印前 8 位(如 key1abcd...),避免泄露完整密钥。
已知约束与注意事项
dmScope 必须为
per-channel-peer:确保每个用户有独立的 sessionKey,避免sessionKeyToApiKey并发覆盖。如果使用main(默认值),所有用户共享同一个 sessionKey,多 key 模式会出问题。sessionKeyToApiKey 只增不减:当前实现中没有过期清理机制。在活跃用户数有限的场景下不是问题。如果未来用户量极大,需考虑 LRU 或 TTL 淘汰策略。
gateway 重启会重建 globalApiKeyMap:
setApiKeyMap在每次startGateway时调用,会覆盖之前的映射。但sessionKeyToApiKey是累积的,重启后会被清空(模块级变量)。单 key 模式兼容:当
apiKeys只有一个元素时,globalApiKeyMap只有一条记录,resolveEffectiveApiKey最终都会 fallback 到globalDefaultApiKey,行为与之前一致。环境变量兜底:如果配置文件中没有
apiKeys,会尝试从LIGHTCLAW_API_KEY环境变量读取。
