npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

lightclawbot

v1.0.6

Published

LightClawBot channel plugin with message support, cron jobs, and proactive messaging

Readme

LightClaw — OpenClaw Channel 插件

对接 OpenClaw 框架的 LightClaw Bot channel 插件,支持多 apiKey(多账户)模式。


目录


配置

~/.openclaw/openclaw.jsonchannels.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 启动时就已构建好
     → ✅ 可直接查到 apiKey

globalApiKeyMap 是冷启动数据源(gateway 启动时预建),sessionKeyToApiKey 是运行时缓存(消息处理时按需写入)。 两者解决不同阶段的问题,缺一不可。


关键模块说明

| 文件 | 职责 | 与 apiKey 体系的关系 | |------|------|---------------------| | src/config.ts | 配置解析 + apiKey 映射管理 | 定义 setApiKeyMapsetSessionApiKeyresolveEffectiveApiKey | | 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...),避免泄露完整密钥。


已知约束与注意事项

  1. dmScope 必须为 per-channel-peer:确保每个用户有独立的 sessionKey,避免 sessionKeyToApiKey 并发覆盖。如果使用 main(默认值),所有用户共享同一个 sessionKey,多 key 模式会出问题。

  2. sessionKeyToApiKey 只增不减:当前实现中没有过期清理机制。在活跃用户数有限的场景下不是问题。如果未来用户量极大,需考虑 LRU 或 TTL 淘汰策略。

  3. gateway 重启会重建 globalApiKeyMapsetApiKeyMap 在每次 startGateway 时调用,会覆盖之前的映射。但 sessionKeyToApiKey 是累积的,重启后会被清空(模块级变量)。

  4. 单 key 模式兼容:当 apiKeys 只有一个元素时,globalApiKeyMap 只有一条记录,resolveEffectiveApiKey 最终都会 fallback 到 globalDefaultApiKey,行为与之前一致。

  5. 环境变量兜底:如果配置文件中没有 apiKeys,会尝试从 LIGHTCLAW_API_KEY 环境变量读取。