diff --git a/.gitignore b/.gitignore
index 8c2c94e..ed9e9f7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,4 +6,11 @@ dist/
*.tsbuildinfo
config/
soul/
-memory/
\ No newline at end of file
+memory/
+.claude/
+
+CLAUDE.md
+COPILOT.md
+AGENTS.md
+
+docs/superpowers/*
\ No newline at end of file
diff --git a/.npmignore b/.npmignore
index c5561b4..fdcfa14 100644
--- a/.npmignore
+++ b/.npmignore
@@ -15,4 +15,5 @@ RESEARCH.md
.env.example
*.log
.DS_Store
-*.tsbuildinfo
\ No newline at end of file
+*.tsbuildinfo
+CLAUDE.md
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..bf08e2f
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,79 @@
+# CLAUDE.md
+
+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+
+## 项目概述
+
+Mercury 是一个 soul-driven 的 AI agent,运行在 Node.js 上,使用 Vercel AI SDK 的 `generateText()` 实现 10 步 agentic loop。支持 CLI 和 Telegram 两种通道,通过权限系统(filesystem scoping + shell blocklist)实现 permission-hardened tools。
+
+## 常用命令
+
+```bash
+npm run build # 构建 (tsup)
+npm run dev # 开发模式 (tsup --watch)
+npm run lint # 类型检查 (tsc --noEmit)
+npm run test # 测试 (vitest run)
+npm run test:watch # 测试 (watch 模式)
+```
+
+单文件构建验证:
+```bash
+npx tsc --noEmit
+```
+
+## 架构要点
+
+### 核心循环(Agent Loop)
+`generateText({ tools, maxSteps: 10 })` → LLM 决定 respond 或 call tool → 权限检查 → 执行 → 继续或返回
+
+### 通道系统(Channels)
+- `src/channels/cli.ts` — Readline 交互,内联权限提示
+- `src/channels/telegram.ts` — grammY 框架,流式响应,inline keyboard
+- `src/channels/registry.ts` — 通道管理器
+
+### 工具注册(Capabilities)
+- 所有工具通过 `src/capabilities/registry.ts` 注册
+- 权限检查在 tool 执行前进行(filesystem scope / shell blocklist)
+- 子命令上下文通过 `capabilities.getChatCommandContext()` 传递给 channel
+
+### 内存层级
+- `ShortTermMemory` — 每轮对话的 JSON 文件
+- `LongTermMemory` — 自动提取的事实(JSONL)
+- `EpisodicMemory` — 带时间戳的事件日志(JSONL)
+- `UserMemoryStore`(Second Brain)— SQLite + FTS5,10 种记忆类型,自主学习
+
+### 子 Agent 系统(Subagents)
+- `src/core/sub-agent.ts` — 独立 worker,隔离的 agentic loop
+- `src/core/supervisor.ts` — 协调器,负责 spawn/halt/queue
+- `src/core/file-lock.ts` — 读写锁(多读单写),自动释放,死锁检测
+- `src/core/task-board.ts` — 共享任务状态,持久化到磁盘
+
+### Provider 系统
+`src/providers/registry.ts` — 多 provider 自动 fallback(DeepSeek → OpenAI → Anthropic → ...)
+
+### Soul 系统
+`soul/*.md` 文件定义人格,只有 name + description 加载到启动时,full instructions 按需加载。
+
+### 编程模式
+`/code plan` → 分析代码库,呈现方案,不写代码
+`/code execute` → 逐步执行计划,build/test 后提交
+
+## 运行时数据位置
+
+所有数据在 `~/.mercury/`,不是项目目录:
+- `~/.mercury/mercury.yaml` — 主配置
+- `~/.mercury/soul/*.md` — Soul 文件
+- `~/.mercury/memory/` — 记忆存储
+- `~/.mercury/permissions.yaml` — 权限清单
+- `~/.mercury/schedules.yaml` — 定时任务
+
+## 配置结构
+
+`src/utils/config.ts` 中的 `MercuryConfig` 接口定义了所有配置项,包括:
+- `identity` — 名称、所有者、创建者
+- `providers` — 多个 LLM provider 配置
+- `channels.telegram` — Telegram bot token 和访问控制
+- `memory.secondBrain` — Second Brain 配置
+- `subagents` — 子 agent 并发配置
+- `spotify` — Spotify OAuth 配置
+- `github` — GitHub 集成配置
\ No newline at end of file
diff --git a/DECISIONS.md b/DECISIONS.md
index 5f10557..133fd04 100644
--- a/DECISIONS.md
+++ b/DECISIONS.md
@@ -2,6 +2,8 @@
> Architecture Decision Records. New ones appended as we go.
+[中文版](./DECISIONS.zh-CN.md)
+
## ADR-001: TypeScript + Node.js
- **Context**: Need a runtime for 24/7 headless agent with future GUI, mobile, and chat integrations.
diff --git a/DECISIONS.zh-CN.md b/DECISIONS.zh-CN.md
new file mode 100644
index 0000000..9f4336b
--- /dev/null
+++ b/DECISIONS.zh-CN.md
@@ -0,0 +1,85 @@
+# Mercury — 架构决策
+
+> 架构决策记录。新的决策将随时追加。
+
+[English](./DECISIONS.md)
+
+## ADR-001: TypeScript + Node.js
+
+- **背景**: 需要一个 24/7 无头代理运行时,同时考虑未来集成 GUI、移动端和聊天渠道。
+- **决策**: 使用 TypeScript + Node.js。
+- **结果**: 最佳的 AI SDK 生态系统(Vercel AI SDK)、Ink 用于 TUI、grammY 用于 Telegram,最易扩展至所有未来渠道。
+
+## ADR-002: Ink 用于 TUI
+
+- **背景**: CLI 需要生动有趣 — 动画、进度条、打字机效果。
+- **决策**: Ink + React 用于终端 UI。
+- **结果**: 比 Commander 学习曲线更陡,但体验卓越。初期 CLI 使用 readline;Ink 在第二阶段引入。
+
+## ADR-003: 平面文件存储
+
+- **背景**: 内存需要简单、可检查、对 Git 友好。
+- **决策**: 长期/情景记忆用 JSONL,短期记忆用 JSON。
+- **结果**: 易于调试,无需数据库依赖。后续可能需要 SQLite 实现语义搜索。
+
+## ADR-004: grammY 用于 Telegram
+
+- **背景**: 需要 Telegram 集成,支持流式输出和打字状态。
+- **决策**: grammY + @grammyjs/stream + @grammyjs/auto-retry。
+- **结果**: 最好的 TypeScript Telegram 框架。内置流式支持,社区活跃。
+
+## ADR-005: Vercel AI SDK 用于 LLM
+
+- **背景**: 需要支持多个提供商(OpenAI、Anthropic、DeepSeek)并实现流式输出。
+- **决策**: 使用 Vercel AI SDK(`ai` 包)配合各提供商适配器。
+- **结果**: 统一 API、内置流式输出、工具调用。切换提供商只需改一行代码。
+
+## ADR-006: Soul 分离为独立 Markdown 文件
+
+- **背景**: 代理人格需要可编辑、可版本化、令牌高效。
+- **决策**: 四个独立 Markdown 文件:soul.md、persona.md、taste.md、heartbeat.md。每次请求只注入 soul + persona;taste + heartbeat 选择性注入。
+- **结果**: 身份基线约 350 tokens。主人可以在不修改代码的情况下编辑人格。
+
+## ADR-007: Agent Skills 规范
+
+- **背景**: Skills 需要模块化、可运行时安装、令牌高效。
+- **决策**: 采用 Agent Skills 规范(agentskills.io)。Skills 使用带有 YAML frontmatter 的 `SKILL.md` + markdown 说明。存储在 `~/.mercury/skills/`。渐进披露:启动时只加载 name + description;调用时才加载完整说明。
+- **结果**: Skills 是人类可读的 markdown,无需代码。令牌预算保持低位。通过粘贴内容或 URL 安装。
+
+## ADR-008: 支持 YAML 持久化的调度器
+
+- **背景**: Mercury 需要设置提醒、执行周期性任务、按计划触发 skills。
+- **决策**: 将 `schedule_task`、`list_scheduled_tasks`、`cancel_scheduled_task` 暴露为 AI 可调用工具。将计划任务持久化到 `~/.mercury/schedules.yaml`。启动时恢复。任务作为内部(非渠道)消息通过代理循环触发。
+- **结果**: Mercury 可以自主调度工作。任务在重启后存活。内部执行使计划任务对渠道不可见,除非代理显式发送输出。
+
+## ADR-009: 自定义混合Daemon化方案
+
+- **背景**: Mercury 24/7 运行但目前仅前台模式。关闭终端会终止进程,导致 Telegram、定时任务和心跳中断。非技术用户不应需要手动安装 PM2/forever/systemd 脚本。
+- **决策**: 在 Mercury 中原生构建自定义混合 daemon 管理器。无外部依赖。分三层:
+ 1. **后台启动** — `child_process.spawn({detached: true})` + PID 文件 + 日志重定向。通过 `mercury start -d` 激活。
+ 2. **看门狗** — 内置指数退避崩溃恢复(基础 1s,1.25 倍,最多 10 次重启/60s)。仅在 daemon 模式激活。
+ 3. **平台服务生成器** — `mercury service install` 检测操作系统并生成相应配置:Linux 上为 `systemd --user` unit,macOS 上为 `~/Library/LaunchAgents` plist,Windows 上为启动快捷方式。Mac/Linux 无需 root。
+- **备选方案考虑**:
+ - `node-windows/mac/linux` 三件套 — 部分已停止维护,Mac 上需要 sudo,node-linux 已停止开发
+ - PM2 作为依赖 — 15MB,50+ 依赖,AGPL-3.0 许可证
+ - PM2 作为用户安装 — 要求非技术用户学习单独工具
+ - `forever` — 已被官方弃用
+ - 仅原生后台模式 — 无崩溃恢复,无启动自启
+- **结果**: 核心 daemon 化零外部依赖。启动服务为用户级(Mac/Linux 无需 sudo)。Windows 获取后台模式 + 文档化的 PM2 路径。前台模式不变 — daemon 模式为可选。在 daemon 模式下,CLI 变为仅日志;Telegram(或其他远程渠道)为交互界面。
+
+## ADR-010: 第二大脑 — SQLite 支持的自主结构化记忆
+
+- **背景**: Mercury 需要一个持久的用户模型,从对话中学习。此前的 LongTermMemory(平面 JSONL)太简单 — 仅关键字搜索,无结构,无合并,无冲突处理,无层级。第二大脑已有部分实现(用于 SQLite 的 second-brain-db.ts,用于 JSON 的 user-memory.ts),但两者都是断开的死代码。
+- **决策**: 使用 SQLite(better-sqlite3)作为存储后端,结合 UserMemoryStore 业务逻辑层构建统一第二大脑。关键原则:
+ - **自主**: 无审核队列,无用户审批。记忆通过置信度自动存储、合并、去冲突。弱记忆以低分保留,自然衰减。
+ - **自动冲突解决**: 检测到极性冲突时(如"偏好 X"vs"不偏好 X"),高置信度记忆静默胜出。置信度相等时 → 较新的胜出。
+ - **自动分层**: 目标和项目等记忆类型初始为 `active`(有时限);身份和偏好初始为 `durable`。强化 3+ 次的记忆从 active 晋升为 durable。
+ - **过期清理**: 21 天未见的 active 推断记忆被清除。120 天无强化的 durable 推断记忆置信度衰减;低于 0.3 时被清除。
+ - **对用户不可见**: 记忆提取在响应发送后作为后台任务执行。代理循环中无工具调用,无状态消息。用户无需等待。
+ - **10 种记忆类型**: identity、preference、goal、project、habit、decision、constraint、relationship、episode、reflection。
+ - **FTS5 全文搜索** 用于 `/memory search` 命令。
+- **备选方案考虑**:
+ - 仅 JSON(UserMemoryStore 原样)— 逻辑好但无搜索,扩展性差
+ - 仅 SQLite(SecondBrainDB 原样)— 存储好但无合并/冲突/反思逻辑
+ - 向量嵌入 — 对当前规模过于奢侈,增加重量级依赖
+- **结果**: WAL 模式的 SQLite 为提示注入提供快速读取(微秒级)。FTS5 支持快速搜索。业务逻辑(合并、冲突、反思、分层、过期)继承自 UserMemoryStore。一个原生依赖(better-sqlite3)。用户的唯一控制:观察(概览、最近、搜索)、暂停/恢复学习、清除全部。
diff --git a/README.md b/README.md
index fd11b55..9cc2874 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
- Remembers what matters. Asks before it acts. Runs 24/7 from CLI or Telegram. 31 built-in tools, extensible skills, SQLite-backed Second Brain memory.
+ Remembers what matters. Asks before it acts. Runs 24/7 from CLI, Telegram, or Feishu. 31 built-in tools, extensible skills, SQLite-backed Second Brain memory.
@@ -39,7 +39,7 @@ npm i -g @cosmicstack/mercury-agent
mercury
```
-First run triggers the setup wizard — enter your name, an API key, and optionally a Telegram bot token. Takes 30 seconds.
+First run triggers the setup wizard — enter your name, an API key, and optionally Telegram/Feishu tokens. Takes 30 seconds.
To reconfigure later (change keys, name, settings):
@@ -127,6 +127,13 @@ In daemon mode, Telegram becomes your primary channel — CLI is log-only since
| `mercury telegram promote ` | Promote a Telegram member to admin |
| `mercury telegram demote ` | Demote a Telegram admin to member |
| `mercury telegram reset` | Clear all Telegram access and start fresh |
+| `mercury feishu list` | List approved and pending Feishu users |
+| `mercury feishu approve ` | Approve a pending Feishu access request |
+| `mercury feishu reject ` | Reject a pending Feishu access request |
+| `mercury feishu remove ` | Remove an approved Feishu user |
+| `mercury feishu promote ` | Promote a Feishu member to admin |
+| `mercury feishu demote ` | Demote a Feishu admin to member |
+| `mercury feishu reset` | Clear all Feishu access and start fresh |
| `mercury service install` | Install as system service (auto-start on boot) |
| `mercury service uninstall` | Uninstall system service |
| `mercury service status` | Show system service status |
@@ -172,6 +179,7 @@ Type these during a conversation — they don't consume API tokens. Work on both
|---------|----------|
| **CLI** | Readline prompt, arrow-key command menus, real-time text streaming with markdown re-rendering, permission mode picker |
| **Telegram** | HTML formatting, editable streaming messages, file uploads, typing indicators, multi-user access with admin/member roles |
+| **Feishu** | Private chat support, auto-allowed user list, multi-user access with admin/member roles |
### Telegram Access
@@ -185,6 +193,18 @@ Mercury uses an **organization access model** with admins and members.
CLI commands: `mercury telegram list|approve|reject|remove|promote|demote|reset`
+### Feishu Access
+
+Feishu uses the same **organization access model** with admins and members.
+
+- **First-time setup:** During `mercury setup`, enter your Feishu App ID and App Secret. Optionally add comma-separated user IDs to auto-allow.
+- **Additional users:** Users send a message to your Feishu bot. Admins approve or reject from the CLI with `mercury feishu approve `.
+- **Roles:** Admins can approve/reject requests, promote/demote users, and reset access. Members can chat with Mercury.
+- **Reset:** Run `mercury feishu reset` in the CLI to clear all access and start fresh.
+- Private chats only — group messages are not supported in MVP.
+
+CLI commands: `mercury feishu list|approve|reject|remove|promote|demote|reset`
+
## Scheduler
- **Recurring**: `schedule_task` with cron expressions (`0 9 * * *` for daily at 9am)
@@ -236,6 +256,7 @@ Configure multiple LLM providers. Mercury tries them in order and falls back aut
| **DeepSeek** | deepseek-chat | `DEEPSEEK_API_KEY` | Default, cost-effective |
| **OpenAI** | gpt-4o-mini | `OPENAI_API_KEY` | GPT-4o, o3, etc. |
| **Anthropic** | claude-sonnet-4 | `ANTHROPIC_API_KEY` | Claude Sonnet, Haiku, Opus |
+| **MiniMax** | (dynamic) | `MINIMAX_API_KEY` | Anthropic-compatible, dynamic model fetch |
| **Grok (xAI)** | grok-4 | `GROK_API_KEY` | OpenAI-compatible endpoint |
| **Ollama Cloud** | gpt-oss:120b | `OLLAMA_CLOUD_API_KEY` | Remote Ollama via API |
| **Ollama Local** | gpt-oss:20b | No key needed | Local Ollama instance |
diff --git a/README.zh-CN.md b/README.zh-CN.md
index c7f8099..7f2fc83 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -4,7 +4,7 @@
[English](README.md) | 简体中文
-Mercury 会记住重要信息,在执行有风险的操作前先请求确认,并且可以通过 CLI 或 Telegram 以 24/7 后台进程运行。它适合需要本地文件操作、命令执行、长期记忆、定时任务和多模型兜底能力的个人 AI 助手场景。
+Mercury 会记住重要信息,在执行有风险的操作前先请求确认,并且可以通过 CLI、Telegram 或飞书以 24/7 后台进程运行。它适合需要本地文件操作、命令执行、长期记忆、定时任务和多模型兜底能力的个人 AI 助手场景。
## 快速开始
@@ -21,7 +21,7 @@ npm i -g @cosmicstack/mercury-agent
mercury
```
-首次运行会启动配置向导。你需要输入姓名、模型 API Key,并可选择配置 Telegram Bot Token。之后如需重新配置:
+首次运行会启动配置向导。你需要输入姓名、模型 API Key,并可选择配置 Telegram/飞书 Token。之后如需重新配置:
```bash
mercury doctor
@@ -47,7 +47,7 @@ mercury up
该命令会安装系统服务、启动后台守护进程,并确保 Mercury 正在运行。如果 Mercury 已经运行,它只会确认状态并显示 PID。
-常用命令:
+守护进程模式包含内置崩溃恢复——如果进程崩溃,会自动重启,采用指数退避策略(最多每分钟 10 次重启)。
```bash
mercury restart # 重启后台进程
@@ -88,6 +88,13 @@ mercury status # 查看运行状态
| `mercury telegram promote ` | 将 Telegram 成员提升为管理员 |
| `mercury telegram demote ` | 将 Telegram 管理员降级为成员 |
| `mercury telegram reset` | 清空 Telegram 访问状态并重新开始 |
+| `mercury feishu list` | 查看已批准和待处理的飞书用户 |
+| `mercury feishu approve ` | 批准飞书访问请求 |
+| `mercury feishu reject ` | 拒绝飞书访问请求 |
+| `mercury feishu remove ` | 移除已批准飞书用户 |
+| `mercury feishu promote ` | 将飞书成员提升为管理员 |
+| `mercury feishu demote ` | 将飞书管理员降级为成员 |
+| `mercury feishu reset` | 清空飞书访问状态并重新开始 |
| `mercury service install` | 安装开机自启系统服务 |
| `mercury service uninstall` | 卸载系统服务 |
| `mercury service status` | 查看系统服务状态 |
@@ -133,6 +140,7 @@ mercury status # 查看运行状态
|------|------|
| CLI | Readline 提示符、方向键命令菜单、实时文本流、Markdown 重渲染、权限模式选择 |
| Telegram | HTML 格式化、可编辑流式消息、文件上传、输入状态、多用户访问和管理员/成员角色 |
+| 飞书 | 私聊支持、自动批准用户列表、管理员/成员角色 |
### Telegram 访问模型
@@ -144,6 +152,20 @@ Mercury 使用组织式访问模型,包含管理员和成员。
- 重置:管理员可在 Telegram 发送 `/unpair`,或在 CLI 中执行 `mercury telegram reset`。
- 仅支持私聊,群聊消息会被忽略。
+CLI 命令:`mercury telegram list|approve|reject|remove|promote|demote|reset`
+
+### 飞书访问模型
+
+飞书使用同样的组织式访问模型,包含管理员和成员。
+
+- 首次设置:在 `mercury setup` 中输入飞书 App ID 和 App Secret。可选填逗号分隔的用户 ID 列表实现自动批准。
+- 新用户:向你的飞书机器人发送消息请求访问,由管理员在 CLI 中批准或拒绝。
+- 角色:管理员可以批准、拒绝、提升、降级和重置访问;成员可以与 Mercury 对话。
+- 重置:在 CLI 中执行 `mercury feishu reset` 清空所有访问状态。
+- 仅支持私聊,群聊暂不支持(MVP)。
+
+CLI 命令:`mercury feishu list|approve|reject|remove|promote|demote|reset`
+
## 调度器
- **周期任务**:使用 cron 表达式,例如 `0 9 * * *` 表示每天 9 点。
@@ -195,36 +217,85 @@ Mercury 可以配置多个 LLM 提供商,并按顺序自动尝试。如果某
| DeepSeek | `deepseek-chat` | `DEEPSEEK_API_KEY` | 默认、成本较低 |
| OpenAI | `gpt-4o-mini` | `OPENAI_API_KEY` | 支持 GPT-4o、o3 等 |
| Anthropic | `claude-sonnet-4` | `ANTHROPIC_API_KEY` | Claude Sonnet、Haiku、Opus |
+| MiniMax | `动态获取` | `MINIMAX_API_KEY` | Anthropic 兼容接口,动态获取模型列表 |
| Grok (xAI) | `grok-4` | `GROK_API_KEY` | OpenAI 兼容接口 |
| Ollama Cloud | `gpt-oss:120b` | `OLLAMA_CLOUD_API_KEY` | 远程 Ollama API |
| Ollama Local | `gpt-oss:20b` | 无需 Key | 本地 Ollama 实例 |
## 架构
-- TypeScript + Node.js 20+
-- Vercel AI SDK v4,支持 `generateText`、`streamText` 和多步 Agent 循环
-- grammY Telegram Bot
-- SQLite + FTS5 Second Brain
-- JSONL 短期、长期和情景记忆
-- 后台守护进程、PID 文件和崩溃恢复
-- macOS、Linux、Windows 系统服务
+- **TypeScript + Node.js 18+** — ESM, tsup build
+- **Vercel AI SDK v4** — `generateText` + `streamText`,10步 Agentic 循环,提供商兜底
+- **grammY** — Telegram Bot,支持打字指示器、可编辑流式消息和文件上传
+- **SQLite + FTS5** — Second Brain 全文本搜索、冲突解决、自动整理
+- **JSONL** — 短期、长期和情景对话记忆
+- **后台守护进程** — 后台生成 + PID 文件 + 看门狗崩溃恢复(指数退避,最多每分钟 10 次重启)
+- **系统服务** — macOS LaunchAgent、Linux systemd、Windows Task Scheduler
## 参与贡献
-欢迎贡献修复、工具、记忆能力、渠道能力或文档改进。请保持 PR 聚焦,并在提交前运行:
+欢迎贡献修复、工具、记忆能力、渠道能力或文档改进。Mercury 是为进化而构建的,我们欢迎社区的帮助。无论是修复 bug、添加工具、改进记忆还是优化灵魂——所有高质量的贡献都会被欣赏。
-```bash
-npm install
-npm run build
-```
+### Agentic 专业能力 — 贡献者必须具备
-贡献 Mercury 时请特别注意:
+Mercury 不只是一个开源项目——它是一个 **灵魂驱动的 Agent**,全天候运行,管理权限、记住上下文并在多个渠道交互。如果你正在贡献,你必须像 Agent 构建者一样思考,而不仅仅是库贡献者。以下是每个贡献者都应该内化的不可协商的原则:
-- 工具必须走权限系统,不能绕过审批。
-- 面向 Agent 循环设计,尽量保持幂等。
-- 避免冗长输出和过度日志,Token 预算是核心约束。
-- CLI 和 Telegram 行为应尽量一致。
-- 新增依赖、破坏性变更和 soul/persona 系统调整应先讨论。
+| 原则 | 含义 |
+|------|------|
+| 🧠 **循环思维** | Mercury 在 10 步 Agentic 循环中运行。你的工具或功能每次对话会被调用多次。尽可能保持幂等。 |
+| 🔐 **权限优先** | 每个接触外部世界的操作(文件、Shell、网络、Git)都必须经过权限系统。不要假设会获得批准。 |
+| 💾 **内存感知** | 如果你的功能生成了关于用户的事实,考虑接入 Second Brain。如果它读取用户数据,先检查记忆。 |
+| 📏 **Token 意识** | Mercury 有每日 Token 预算。日志、冗长输出和大上下文转储会快速消耗 Token。保持精简。 |
+| 🔌 **渠道无关** | 工具应该在 CLI 和 Telegram 上行为一致。不要假设有终端、键盘或对面有人。 |
+| 🔁 **优雅降级** | 如果提供商失败、工具出错或文件不存在——Mercury 应该恢复,而不是崩溃。始终处理边缘情况。 |
+| 📋 **自我文档化** | 你的工具名称和描述是 Mercury 决定何时使用它的依据。让它们清晰、具体和面向行动。 |
+| 🧪 **测试循环,而不仅仅是函数** | 一个在孤立状态下工作的工具在 Agentic 循环中可能会失败(例如返回太多数据、阻塞下一步)。端到端测试。 |
+
+### 代码质量 — 应该做
+
+| 应该 | 为什么 |
+|------|--------|
+| ✅ 编写清晰、可读的 TypeScript,带显式类型 | Mercury's 代码库是类型安全的——保持这种方式 |
+| ✅ 在公共函数和工具上添加 JSDoc 注释 | 帮助其他贡献者和 Agent 理解意图 |
+| ✅ 保持函数小而单一职责 | 更容易测试、审查和推理 |
+| ✅ 使用 async/await 而不是原始 Promise | 一致的错误处理和可读性 |
+| ✅ 为新工具和内存功能编写测试 | 对于 24/7 Agent 来说可靠性很重要 |
+| ✅ 遵循现有项目结构(`src/tools/`、`src/memory/`、`src/channels/`) | 保持代码库可导航 |
+| ✅ 使用 Agent Skills 规范用于新的基于技能的功能 | 确保与技能生态系统的兼容性 |
+| ✅ 在 PR 描述中记录破坏性变更 | 帮助维护者正确版本管理 |
+
+### 代码质量 — 不应该做
+
+| 不应该 | 为什么 |
+|--------|--------|
+| ❌ 未经讨论不要添加依赖 | Mercury 是精简的——每个依赖都增加表面积 |
+| ❌ 不要硬编码 API Key、Token 或路径 | 像代码库其他地方一样使用 config/env 变量 |
+| ❌ 不要绕过权限系统 | 工具必须先请求再行动——这是 Mercury 的核心承诺 |
+| ❌ 不要在热路径中引入同步/阻塞 I/O | Mercury 是异步优先的,有原因 |
+| ❌ 不要提交大二进制文件或 secrets | 使用 `.gitignore` 和 env 文件 |
+| ❌ 不要在没有讨论的情况下更改 soul/persona 系统 | 它是 Mercury 的核心——更改需要谨慎 |
+| ❌ 不要提交未经测试的 Telegram 或守护进程更改 | 这些在合并后很难调试 |
+| ❌ 不要忽视 Token 预算系统 | 每个工具都应该注意 Token 消耗 |
+
+### 快速开始
+
+1. Fork 仓库
+2. 运行 `npm install`
+3. 进行你的更改
+4. 运行 `npm run build` 验证编译
+5. 本地使用 `mercury` 测试
+6. 打开 PR,清晰描述你更改的内容和原因
+
+### PR 指南
+
+- 保持 PR 聚焦——每个 PR 一个功能/修复
+- 在描述中包含更改前后的行为
+- 如适用,标记相关 issue
+- 响应审查反馈
+
+### 需要帮助?
+
+打开 issue 或发送邮件至 [mercury@cosmicstack.org](mailto:mercury@cosmicstack.org)。我们很友好。
## 许可证
diff --git a/docs/superpowers/plans/2026-05-03-feishu-channel.md b/docs/superpowers/plans/2026-05-03-feishu-channel.md
new file mode 100644
index 0000000..f0a1fa7
--- /dev/null
+++ b/docs/superpowers/plans/2026-05-03-feishu-channel.md
@@ -0,0 +1,797 @@
+# Feishu Channel Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** Add a Feishu channel that accepts private messages, returns text replies, and enforces a minimal approval flow without disturbing CLI or Telegram.
+
+**Architecture:** Keep Mercury’s existing channel abstraction intact and add Feishu as one more `Channel` implementation. Use a small Feishu-specific access store in `src/utils/config.ts`, a dedicated adapter in `src/channels/feishu.ts`, and a tiny outbound-routing helper so replies always go back to the channel that received the message. Keep the first release narrow: private chat only, text only, CLI approval only.
+
+**Tech Stack:** TypeScript, Node.js 20, existing Mercury channel framework, `@larksuiteoapi/node-sdk`, Vitest.
+
+---
+
+### Task 1: Add Feishu access state and config helpers
+
+**Files:**
+- Modify: `src/utils/config.ts`
+- Create: `src/utils/feishu-access.test.ts`
+
+- [ ] **Step 1: Write the failing test**
+
+```ts
+import { describe, expect, it } from 'vitest';
+import {
+ addFeishuPendingRequest,
+ approveFeishuPendingRequest,
+ clearFeishuAccess,
+ demoteFeishuAdmin,
+ getDefaultConfig,
+ getFeishuAdmins,
+ getFeishuAccessSummary,
+ getFeishuApprovedUsers,
+ getFeishuPendingRequests,
+ findFeishuApprovedUser,
+ findFeishuPendingRequest,
+ hasFeishuAdmins,
+ isFeishuAutoAllowed,
+ promoteFeishuUserToAdmin,
+ rejectFeishuPendingRequest,
+ removeFeishuUser,
+} from './config.js';
+
+describe('feishu access config helpers', () => {
+ it('creates, approves, promotes, demotes, rejects, and clears Feishu access', () => {
+ const config = getDefaultConfig();
+ config.channels.feishu.allowedUserIds = ['ou_allow'];
+
+ addFeishuPendingRequest(config, { openId: 'ou_1', chatId: 'oc_1', displayName: 'alpha' });
+ addFeishuPendingRequest(config, { openId: 'ou_2', chatId: 'oc_2', displayName: 'beta' });
+
+ expect(isFeishuAutoAllowed(config, 'ou_allow')).toBe(true);
+ expect(approveFeishuPendingRequest(config, 'ou_1', 'admin')?.openId).toBe('ou_1');
+ expect(approveFeishuPendingRequest(config, 'ou_2', 'member')?.openId).toBe('ou_2');
+
+ expect(promoteFeishuUserToAdmin(config, 'ou_2')?.openId).toBe('ou_2');
+ expect(demoteFeishuAdmin(config, 'ou_1')?.openId).toBe('ou_1');
+ expect(rejectFeishuPendingRequest(config, 'ou_missing')).toBeNull();
+ expect(removeFeishuUser(config, 'ou_2')?.openId).toBe('ou_2');
+
+ clearFeishuAccess(config);
+ expect(getFeishuAccessSummary(config)).toBe('0 admins, 0 members, 0 pending');
+ });
+});
+```
+
+- [ ] **Step 2: Run the test and confirm it fails**
+
+Run: `npx vitest run src/utils/feishu-access.test.ts -t "feishu access config helpers"`
+Expected: fail with missing Feishu helper functions and/or missing `channels.feishu` config fields.
+
+- [ ] **Step 3: Implement the minimal config helpers**
+
+Add a Feishu section to `MercuryConfig.channels` and `getDefaultConfig()`:
+
+```ts
+feishu: {
+ enabled: getEnvBool('FEISHU_ENABLED', false),
+ appId: getEnv('FEISHU_APP_ID', ''),
+ appSecret: getEnv('FEISHU_APP_SECRET', ''),
+ allowedUserIds: getEnv('FEISHU_ALLOWED_USER_IDS', '')
+ .split(',')
+ .filter(Boolean)
+ .map((value) => value.trim()),
+ admins: [],
+ members: [],
+ pending: [],
+},
+```
+
+Add these helpers next to the Telegram helpers in `src/utils/config.ts`:
+
+```ts
+export interface FeishuAccessUser {
+ openId: string;
+ chatId: string;
+ displayName?: string;
+ requestedAt?: string;
+ approvedAt: string;
+}
+
+export interface FeishuPendingRequest {
+ openId: string;
+ chatId: string;
+ displayName?: string;
+ requestedAt: string;
+}
+
+/** Return the approved Feishu users. */
+export function getFeishuApprovedUsers(config: MercuryConfig): FeishuAccessUser[] {
+ return [...config.channels.feishu.admins, ...config.channels.feishu.members];
+}
+
+/** Return the Feishu admins. */
+export function getFeishuAdmins(config: MercuryConfig): FeishuAccessUser[] {
+ return config.channels.feishu.admins;
+}
+
+/** Return the pending Feishu requests. */
+export function getFeishuPendingRequests(config: MercuryConfig): FeishuPendingRequest[] {
+ return config.channels.feishu.pending;
+}
+
+/** Find an approved Feishu user by openId. */
+export function findFeishuApprovedUser(config: MercuryConfig, openId: string): FeishuAccessUser | undefined {
+ return getFeishuApprovedUsers(config).find((user) => user.openId === openId);
+}
+
+/** Find a pending Feishu request by openId. */
+export function findFeishuPendingRequest(config: MercuryConfig, openId: string): FeishuPendingRequest | undefined {
+ return config.channels.feishu.pending.find((request) => request.openId === openId);
+}
+
+/** Check whether the config already has a Feishu admin. */
+export function hasFeishuAdmins(config: MercuryConfig): boolean {
+ return config.channels.feishu.admins.length > 0;
+}
+
+/** Check whether a Feishu openId is auto-allowed. */
+export function isFeishuAutoAllowed(config: MercuryConfig, openId: string): boolean {
+ return config.channels.feishu.allowedUserIds.includes(openId);
+}
+
+/** Summarize Feishu access state. */
+export function getFeishuAccessSummary(config: MercuryConfig): string {
+ return `${config.channels.feishu.admins.length} admin${config.channels.feishu.admins.length === 1 ? '' : 's'}, `
+ + `${config.channels.feishu.members.length} member${config.channels.feishu.members.length === 1 ? '' : 's'}, `
+ + `${config.channels.feishu.pending.length} pending`;
+}
+
+/** Add a Feishu pending request. */
+export function addFeishuPendingRequest(
+ config: MercuryConfig,
+ request: Omit & { requestedAt?: string },
+): FeishuPendingRequest {
+ const existing = findFeishuPendingRequest(config, request.openId);
+ if (existing) {
+ existing.chatId = request.chatId;
+ existing.displayName = request.displayName || existing.displayName;
+ return existing;
+ }
+
+ const created: FeishuPendingRequest = {
+ ...request,
+ requestedAt: request.requestedAt || new Date().toISOString(),
+ };
+ config.channels.feishu.pending.push(created);
+ return created;
+}
+
+/** Approve a Feishu pending request. */
+export function approveFeishuPendingRequest(
+ config: MercuryConfig,
+ openId: string,
+ role: 'admin' | 'member' = 'member',
+): FeishuAccessUser | null {
+ const request = findFeishuPendingRequest(config, openId);
+ if (!request) return null;
+
+ const approvedUser: FeishuAccessUser = {
+ openId: request.openId,
+ chatId: request.chatId,
+ displayName: request.displayName,
+ requestedAt: request.requestedAt,
+ approvedAt: new Date().toISOString(),
+ };
+
+ config.channels.feishu.pending = config.channels.feishu.pending.filter((entry) => entry.openId !== openId);
+ config.channels.feishu.admins = config.channels.feishu.admins.filter((entry) => entry.openId !== openId);
+ config.channels.feishu.members = config.channels.feishu.members.filter((entry) => entry.openId !== openId);
+
+ if (role === 'admin') {
+ config.channels.feishu.admins.push(approvedUser);
+ } else {
+ config.channels.feishu.members.push(approvedUser);
+ }
+
+ return approvedUser;
+}
+
+/** Reject a Feishu pending request. */
+export function rejectFeishuPendingRequest(config: MercuryConfig, openId: string): FeishuPendingRequest | null {
+ const request = findFeishuPendingRequest(config, openId);
+ if (!request) return null;
+ config.channels.feishu.pending = config.channels.feishu.pending.filter((entry) => entry.openId !== openId);
+ return request;
+}
+
+/** Remove a Feishu user from approved access. */
+export function removeFeishuUser(config: MercuryConfig, openId: string): FeishuAccessUser | null {
+ const admin = config.channels.feishu.admins.find((entry) => entry.openId === openId);
+ if (admin) {
+ config.channels.feishu.admins = config.channels.feishu.admins.filter((entry) => entry.openId !== openId);
+ return admin;
+ }
+
+ const member = config.channels.feishu.members.find((entry) => entry.openId === openId);
+ if (member) {
+ config.channels.feishu.members = config.channels.feishu.members.filter((entry) => entry.openId !== openId);
+ return member;
+ }
+
+ return null;
+}
+
+/** Promote a Feishu member to admin. */
+export function promoteFeishuUserToAdmin(config: MercuryConfig, openId: string): FeishuAccessUser | null {
+ const member = config.channels.feishu.members.find((entry) => entry.openId === openId);
+ if (!member) return null;
+ config.channels.feishu.members = config.channels.feishu.members.filter((entry) => entry.openId !== openId);
+ config.channels.feishu.admins.push(member);
+ return member;
+}
+
+/** Demote a Feishu admin to member. */
+export function demoteFeishuAdmin(config: MercuryConfig, openId: string): FeishuAccessUser | null {
+ if (config.channels.feishu.admins.length <= 1) {
+ return null;
+ }
+
+ const admin = config.channels.feishu.admins.find((entry) => entry.openId === openId);
+ if (!admin) return null;
+ config.channels.feishu.admins = config.channels.feishu.admins.filter((entry) => entry.openId !== openId);
+ config.channels.feishu.members.push(admin);
+ return admin;
+}
+
+/** Clear all Feishu access state. */
+export function clearFeishuAccess(config: MercuryConfig): MercuryConfig {
+ config.channels.feishu.admins = [];
+ config.channels.feishu.members = [];
+ config.channels.feishu.pending = [];
+ return config;
+}
+```
+
+Keep Telegram helpers unchanged; do not refactor them as part of this task.
+
+- [ ] **Step 4: Run the test and confirm it passes**
+
+Run: `npx vitest run src/utils/feishu-access.test.ts -t "feishu access config helpers"`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/utils/config.ts src/utils/feishu-access.test.ts
+git commit -m "feat: add feishu access helpers"
+```
+
+---
+
+### Task 2: Add the Feishu channel adapter and registry wiring
+
+**Files:**
+- Create: `src/channels/feishu.ts`
+- Create: `src/channels/feishu.test.ts`
+- Modify: `src/channels/index.ts`
+- Modify: `src/channels/registry.ts`
+- Modify: `package.json`
+
+- [ ] **Step 1: Write the failing test**
+
+```ts
+import { describe, expect, it } from 'vitest';
+import { normalizeFeishuEvent, resolveFeishuTargetId } from './feishu.js';
+
+describe('feishu adapter helpers', () => {
+ it('normalizes a private text event to a Mercury channel message', () => {
+ const message = normalizeFeishuEvent({
+ header: {
+ event_id: 'evt_1',
+ event_type: 'im.message.receive_v1',
+ create_time: '1710000000000',
+ token: 'token',
+ },
+ event: {
+ sender: { sender_id: { open_id: 'ou_1' } },
+ message: {
+ message_id: 'msg_1',
+ chat_id: 'oc_1',
+ message_type: 'text',
+ content: '{"text":"hello"}',
+ },
+ },
+ });
+
+ expect(message).toMatchObject({
+ channelType: 'feishu',
+ channelId: 'feishu:oc_1',
+ senderId: 'ou_1',
+ content: 'hello',
+ });
+ });
+
+ it('resolves Mercury target ids back to Feishu chat ids', () => {
+ expect(resolveFeishuTargetId('feishu:oc_2')).toBe('oc_2');
+ expect(resolveFeishuTargetId('oc_3')).toBe('oc_3');
+ });
+});
+```
+
+- [ ] **Step 2: Run the test and confirm it fails**
+
+Run: `npx vitest run src/channels/feishu.test.ts -t "feishu adapter helpers"`
+Expected: fail because the adapter helpers and channel class do not exist yet.
+
+- [ ] **Step 3: Install the official Feishu SDK**
+
+Run: `npm install @larksuiteoapi/node-sdk`
+Expected: `package.json` and the lockfile record the official Feishu SDK dependency.
+
+- [ ] **Step 4: Implement the Feishu adapter behind a thin transport interface**
+
+Create `src/channels/feishu.ts` with a thin wrapper around the official SDK so the Mercury channel can be tested without a live bot:
+
+```ts
+import * as Lark from '@larksuiteoapi/node-sdk';
+import type { ChannelMessage } from '../types/channel.js';
+import type { MercuryConfig } from '../utils/config.js';
+import {
+ addFeishuPendingRequest,
+ approveFeishuPendingRequest,
+ findFeishuApprovedUser,
+ isFeishuAutoAllowed,
+ saveConfig,
+} from '../utils/config.js';
+import { BaseChannel } from './base.js';
+
+export interface FeishuEventEnvelope { /* event_id, chat_id, open_id, text content */ }
+
+export interface FeishuTransport {
+ start(onEvent: (event: FeishuEventEnvelope) => Promise): Promise;
+ stop(): Promise;
+ sendText(chatId: string, content: string): Promise;
+}
+
+export function resolveFeishuTargetId(targetId?: string): string | undefined {
+ if (!targetId) return undefined;
+ return targetId.startsWith('feishu:') ? targetId.slice('feishu:'.length) : targetId;
+}
+
+export function normalizeFeishuEvent(envelope: FeishuEventEnvelope): ChannelMessage | null {
+ const chatId = envelope.event?.message?.chat_id;
+ const openId = envelope.event?.sender?.sender_id?.open_id;
+ const rawContent = envelope.event?.message?.content;
+ const messageType = envelope.event?.message?.message_type;
+
+ if (!chatId || !openId || messageType !== 'text' || !rawContent) return null;
+
+ let parsed: { text?: string };
+ try {
+ parsed = JSON.parse(rawContent) as { text?: string };
+ } catch {
+ return null;
+ }
+
+ const text = typeof parsed.text === 'string' ? parsed.text.trim() : '';
+ if (!text) return null;
+
+ return {
+ id: envelope.header.event_id,
+ channelId: `feishu:${chatId}`,
+ channelType: 'feishu',
+ senderId: openId,
+ content: text,
+ timestamp: Number(envelope.header.create_time),
+ metadata: {
+ chatId,
+ eventId: envelope.header.event_id,
+ messageId: envelope.event.message?.message_id,
+ },
+ };
+}
+
+export class FeishuChannel extends BaseChannel {
+ readonly type = 'feishu' as const;
+ private transport: FeishuTransport;
+
+ constructor(private config: MercuryConfig) {
+ super();
+ this.transport = createFeishuTransport(config);
+ }
+
+ async start(): Promise {
+ await this.transport.start(async (envelope) => {
+ const message = normalizeFeishuEvent(envelope);
+ if (!message) return;
+
+ if (isFeishuAutoAllowed(this.config, message.senderId) || findFeishuApprovedUser(this.config, message.senderId)) {
+ this.emit(message);
+ return;
+ }
+
+ addFeishuPendingRequest(this.config, {
+ openId: message.senderId,
+ chatId: message.channelId.slice('feishu:'.length),
+ displayName: message.senderName,
+ });
+ saveConfig(this.config);
+ await this.transport.sendText(resolveFeishuTargetId(message.channelId)!, 'Access pending. Ask Mercury CLI to approve this Feishu user.');
+ });
+ this.ready = true;
+ }
+
+ async stop(): Promise {
+ await this.transport.stop();
+ this.ready = false;
+ }
+
+ async send(content: string, targetId?: string): Promise {
+ const chatId = resolveFeishuTargetId(targetId);
+ if (!chatId) return;
+ await this.transport.sendText(chatId, content);
+ }
+
+ async sendFile(): Promise {
+ throw new Error('Feishu file sending is not part of the MVP');
+ }
+
+ async stream(content: AsyncIterable, targetId?: string): Promise {
+ let full = '';
+ for await (const chunk of content) full += chunk;
+ await this.send(full, targetId);
+ return full;
+ }
+
+ async typing(): Promise {
+ return;
+ }
+
+ async askToContinue(): Promise {
+ return true;
+ }
+}
+
+function createFeishuTransport(config: MercuryConfig): FeishuTransport {
+ const client = new Lark.Client({
+ appId: config.channels.feishu.appId,
+ appSecret: config.channels.feishu.appSecret,
+ });
+ const wsClient = new Lark.WSClient({
+ appId: config.channels.feishu.appId,
+ appSecret: config.channels.feishu.appSecret,
+ });
+
+ return {
+ start: async (onEvent) => {
+ wsClient.start({
+ eventDispatcher: new Lark.EventDispatcher({}).register({
+ 'im.message.receive_v1': async (event: FeishuEventEnvelope) => {
+ await onEvent(event);
+ },
+ }),
+ });
+ },
+ stop: async () => {
+ await wsClient.stop();
+ },
+ sendText: async (chatId, content) => {
+ await client.im.v1.message.create({
+ params: { receive_id_type: 'chat_id' },
+ data: {
+ receive_id: chatId,
+ msg_type: 'text',
+ content: JSON.stringify({ text: content }),
+ },
+ });
+ },
+ };
+}
+```
+
+Before coding, verify the exact `@larksuiteoapi/node-sdk` shapes for `WSClient.start`, `EventDispatcher.register`, and `client.im.v1.message.create` in the official docs, then keep this wrapper thin and match those documented parameter names exactly.
+
+Wire it into the registry and exports:
+
+```ts
+// src/channels/index.ts
+export { FeishuChannel } from './feishu.js';
+
+// src/channels/registry.ts
+import { FeishuChannel } from './feishu.js';
+
+if (config.channels.feishu.enabled && config.channels.feishu.appId && config.channels.feishu.appSecret) {
+ this.register('feishu', new FeishuChannel(config));
+}
+```
+
+Add a one-line JSDoc comment to each exported helper and to `FeishuChannel` so the new public surface matches the repository’s code-quality rule.
+
+- [ ] **Step 5: Run the test and confirm it passes**
+
+Run: `npx vitest run src/channels/feishu.test.ts -t "feishu adapter helpers"`
+Expected: PASS.
+
+- [ ] **Step 6: Commit**
+
+```bash
+git add package.json package-lock.json src/channels/feishu.ts src/channels/feishu.test.ts src/channels/index.ts src/channels/registry.ts
+git commit -m "feat: add feishu channel adapter"
+```
+
+---
+
+### Task 3: Route replies through the source channel and expose Feishu CLI controls
+
+**Files:**
+- Create: `src/core/channel-routing.ts`
+- Create: `src/core/channel-routing.test.ts`
+- Modify: `src/index.ts`
+
+- [ ] **Step 1: Write the failing test**
+
+```ts
+import { describe, expect, it } from 'vitest';
+import { pickOutboundChannelType } from './channel-routing.js';
+
+describe('pickOutboundChannelType', () => {
+ it('prefers the current ready channel', () => {
+ expect(
+ pickOutboundChannelType({
+ currentChannelType: 'feishu',
+ readyChannels: ['cli', 'feishu'],
+ fallbackChannel: 'cli',
+ }),
+ ).toBe('feishu');
+ });
+
+ it('falls back to the notification channel when the current channel is not ready', () => {
+ expect(
+ pickOutboundChannelType({
+ currentChannelType: 'feishu',
+ readyChannels: ['cli'],
+ fallbackChannel: 'cli',
+ }),
+ ).toBe('cli');
+ });
+});
+```
+
+- [ ] **Step 2: Run the test and confirm it fails**
+
+Run: `npx vitest run src/core/channel-routing.test.ts -t "pickOutboundChannelType"`
+Expected: fail because the helper does not exist yet.
+
+- [ ] **Step 3: Implement the outbound routing helper and use it from `src/index.ts`**
+
+Add `src/core/channel-routing.ts`:
+
+```ts
+import type { ChannelType } from '../types/channel.js';
+
+export interface OutboundChannelContext {
+ currentChannelType: ChannelType;
+ readyChannels: ChannelType[];
+ fallbackChannel: ChannelType;
+}
+
+export function pickOutboundChannelType(context: OutboundChannelContext): ChannelType {
+ if (context.readyChannels.includes(context.currentChannelType)) {
+ return context.currentChannelType;
+ }
+ if (context.readyChannels.includes(context.fallbackChannel)) {
+ return context.fallbackChannel;
+ }
+ return context.readyChannels[0] ?? context.fallbackChannel;
+}
+```
+
+Then in `src/index.ts`:
+
+```ts
+capabilities.setSendFileHandler(async (filePath: string) => {
+ const { channelId, channelType } = capabilities.getChannelContext();
+ const targetType = pickOutboundChannelType({
+ currentChannelType: channelType as ChannelType,
+ readyChannels: channels.getActiveChannels(),
+ fallbackChannel: 'cli',
+ });
+ const targetChannel = channels.get(targetType);
+ if (targetChannel) {
+ await targetChannel.sendFile(filePath, channelId);
+ return;
+ }
+ throw new Error(`No outbound channel available for ${filePath}`);
+});
+
+capabilities.setSendMessageHandler(async (content: string) => {
+ const { channelId, channelType } = capabilities.getChannelContext();
+ const targetType = pickOutboundChannelType({
+ currentChannelType: channelType as ChannelType,
+ readyChannels: channels.getActiveChannels(),
+ fallbackChannel: 'cli',
+ });
+ const targetChannel = channels.get(targetType);
+ if (!targetChannel) {
+ throw new Error('No outbound channel available.');
+ }
+ await targetChannel.send(content, channelId);
+});
+```
+
+Add Feishu setup/status output next to Telegram in the CLI wizard and `status` command:
+
+```ts
+console.log(chalk.bold.white(' Feishu (optional)'));
+console.log(chalk.dim(' Mercury can also connect to Feishu private chats.'));
+console.log(chalk.dim(' Leave empty to skip. You can add it later with mercury doctor.'));
+
+const feishuAppId = await ask(chalk.white(' Feishu App ID: '));
+const feishuAppSecret = await ask(chalk.white(' Feishu App Secret: '));
+const feishuAllowed = await ask(chalk.white(' Feishu Allowed User IDs (comma-separated, optional): '));
+```
+
+Add a Feishu command group that mirrors Telegram access management:
+
+```ts
+const feishuCmd = program.command('feishu').description('Manage Feishu access approvals and admins');
+feishuCmd.command('list')
+ .description('Show approved Feishu users and pending requests')
+ .action(() => {
+ const config = loadConfig();
+ console.log('');
+ console.log(` Feishu Access: ${chalk.white(getFeishuAccessSummary(config))}`);
+ console.log(` Admins: ${config.channels.feishu.admins.length > 0 ? chalk.green(getFeishuAdmins(config).map(formatFeishuUser).join(', ')) : chalk.dim('none')}`);
+ console.log(` Members: ${config.channels.feishu.members.length > 0 ? chalk.green(getFeishuApprovedUsers(config).filter((user) => !config.channels.feishu.admins.some((admin) => admin.openId === user.openId)).map(formatFeishuUser).join(', ')) : chalk.dim('none')}`);
+ console.log(` Pending: ${config.channels.feishu.pending.length > 0 ? chalk.yellow(getFeishuPendingRequests(config).map(formatFeishuPending).join(', ')) : chalk.dim('none')}`);
+ console.log('');
+ });
+
+feishuCmd.command('approve ').description('Approve a pending Feishu access request by openId').action((openId: string) => {
+ const config = loadConfig();
+ const approved = approveFeishuPendingRequest(config, openId, hasFeishuAdmins(config) ? 'member' : 'admin');
+ if (!approved) {
+ console.log('');
+ console.log(chalk.red(` No pending Feishu request found for openId ${openId}.`));
+ console.log('');
+ return;
+ }
+ saveConfig(config);
+ console.log('');
+ console.log(chalk.green(` ✓ Approved Feishu ${formatFeishuUser(approved)}.`));
+ restartDaemonIfRunning('Restarting the background daemon to apply the change immediately...');
+ console.log('');
+});
+feishuCmd.command('reject ').description('Reject a pending Feishu access request').action((openId: string) => {
+ const config = loadConfig();
+ const rejected = rejectFeishuPendingRequest(config, openId);
+ if (!rejected) {
+ console.log('');
+ console.log(chalk.red(` No pending Feishu request found for openId ${openId}.`));
+ console.log('');
+ return;
+ }
+ saveConfig(config);
+ console.log('');
+ console.log(chalk.green(` ✓ Rejected Feishu request for ${formatFeishuPending(rejected)}.`));
+ restartDaemonIfRunning('Restarting the background daemon to apply the change immediately...');
+ console.log('');
+});
+feishuCmd.command('remove ').description('Remove an approved Feishu admin or member').action((openId: string) => {
+ const config = loadConfig();
+ const removed = removeFeishuUser(config, openId);
+ if (!removed) {
+ console.log('');
+ console.log(chalk.red(` No approved Feishu user found for openId ${openId}.`));
+ console.log('');
+ return;
+ }
+ saveConfig(config);
+ console.log('');
+ console.log(chalk.green(` ✓ Removed Feishu access for ${formatFeishuUser(removed)}.`));
+ restartDaemonIfRunning('Restarting the background daemon to apply the change immediately...');
+ console.log('');
+});
+feishuCmd.command('promote ').description('Promote a Feishu member to admin').action((openId: string) => {
+ const config = loadConfig();
+ const promoted = promoteFeishuUserToAdmin(config, openId);
+ if (!promoted) {
+ console.log('');
+ console.log(chalk.red(` No Feishu member found for openId ${openId}.`));
+ console.log('');
+ return;
+ }
+ saveConfig(config);
+ console.log('');
+ console.log(chalk.green(` ✓ Promoted ${formatFeishuUser(promoted)} to Feishu admin.`));
+ restartDaemonIfRunning('Restarting the background daemon to apply the change immediately...');
+ console.log('');
+});
+feishuCmd.command('demote ').description('Demote a Feishu admin to member').action((openId: string) => {
+ const config = loadConfig();
+ const demoted = demoteFeishuAdmin(config, openId);
+ if (!demoted) {
+ console.log('');
+ console.log(chalk.red(` No Feishu admin found for openId ${openId}, or this is the last admin.`));
+ console.log('');
+ return;
+ }
+ saveConfig(config);
+ console.log('');
+ console.log(chalk.green(` ✓ Demoted ${formatFeishuUser(demoted)} to Feishu member.`));
+ restartDaemonIfRunning('Restarting the background daemon to apply the change immediately...');
+ console.log('');
+});
+feishuCmd.command('reset').description('Clear all Feishu access state').action(() => {
+ const config = loadConfig();
+ clearFeishuAccess(config);
+ saveConfig(config);
+ console.log('');
+ console.log(chalk.green(' ✓ Cleared all Feishu access state.'));
+ restartDaemonIfRunning('Restarting the background daemon to apply the change immediately...');
+ console.log('');
+});
+```
+
+Keep the CLI behavior aligned with Telegram: every Feishu command should print a blank line before and after, should save config only after a successful mutation, and should show an explicit error message when the target `openId` is not found or a demotion would leave zero admins.
+
+- [ ] **Step 4: Run the test and confirm it passes**
+
+Run: `npx vitest run src/core/channel-routing.test.ts -t "pickOutboundChannelType"`
+Expected: PASS.
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add src/core/channel-routing.ts src/core/channel-routing.test.ts src/index.ts
+git commit -m "feat: route replies through active channel"
+```
+
+---
+
+### Task 4: Verify the complete Feishu MVP path
+
+**Files:**
+- No new files; verify the files changed in Tasks 1–3.
+
+- [ ] **Step 1: Run the full build**
+
+Run: `npm run build`
+Expected: TypeScript builds successfully and tsup completes without errors.
+
+- [ ] **Step 2: Run lint/type-check**
+
+Run: `npm run lint`
+Expected: `tsc --noEmit` passes with no type errors.
+
+- [ ] **Step 3: Run the test suite**
+
+Run: `npm run test`
+Expected: All Vitest tests pass, including the new Feishu helper tests.
+
+- [ ] **Step 4: Run the CLI and daemon checks**
+
+Run: `mercury status`
+Expected: output shows Feishu enabled/disabled state and Feishu access summary.
+
+Run: `mercury feishu list`
+Expected: output lists approved and pending Feishu users without throwing.
+
+Run: `mercury start`
+Expected: Mercury starts normally with Feishu configured or disabled.
+
+- [ ] **Step 5: Perform a manual smoke check with a configured Feishu bot**
+
+1. Start Mercury with Feishu enabled.
+2. Send a private message from an unapproved Feishu user.
+3. Confirm the user appears in `feishu list` as pending.
+4. Approve that `openId` with the CLI command.
+5. Send another message from the same Feishu chat.
+6. Confirm Mercury replies back into the same Feishu chat.
+
+- [ ] **Step 6: Commit the verified implementation**
+
+```bash
+git add src/utils/config.ts src/utils/feishu-access.test.ts src/channels/feishu.ts src/channels/feishu.test.ts src/channels/index.ts src/channels/registry.ts src/core/channel-routing.ts src/core/channel-routing.test.ts src/index.ts
+git commit -m "feat: add feishu channel MVP"
+```
diff --git a/docs/superpowers/specs/2026-05-03-feishu-channel-design.md b/docs/superpowers/specs/2026-05-03-feishu-channel-design.md
new file mode 100644
index 0000000..291027a
--- /dev/null
+++ b/docs/superpowers/specs/2026-05-03-feishu-channel-design.md
@@ -0,0 +1,211 @@
+# Feishu 渠道接入设计草案(MVP)
+
+**日期**:2026-05-03
+**分支**:`feature/feishu-channel`
+
+## 1. 背景
+
+Mercury 目前已经有 CLI 和 Telegram 两个渠道,且通道抽象、消息路由、能力注册都已成型。现有实现说明:
+
+- `Channel` 接口已经统一了 `start/stop/send/sendFile/stream/typing/onMessage`。
+- `ChannelRegistry` 负责按配置注册通道并分发入站消息。
+- `Agent` 主循环按 `channelId` 将结果发回源渠道。
+
+这意味着 Feishu 不需要重写 Agent,只需要补一个新的通道实现,并把配置与路由接上。
+
+## 2. 目标
+
+本阶段只做 **Feishu 私聊文本 + 基础访问控制**。
+
+### 成功标准
+
+- 能在 Feishu 私聊中接收用户消息。
+- 已批准用户的消息能进入 Mercury 主循环。
+- Mercury 的回复能回到原 Feishu 会话。
+- 未批准用户只能进入 pending,不会直接和 Agent 交互。
+- Feishu 通道配置缺失时,不能影响 CLI / Telegram 启动。
+
+## 3. 非目标
+
+本阶段明确不做:
+
+- 群聊 @ 处理
+- 卡片交互
+- 文件上传/发送
+- 流式逐字编辑
+- 多租户支持
+- 复杂 RBAC
+
+## 4. 方案对比
+
+### 方案 A:长连接事件订阅
+
+由 Feishu 官方长连接/事件订阅方式接收消息,Mercury 常驻进程直接消费事件。
+
+**优点**
+- 更适合常驻 Agent。
+- 事件入口与进程生命周期一致。
+- 不需要额外公网 Webhook 入口的复杂部署。
+
+**缺点**
+- 依赖 Feishu 事件订阅能力与 SDK 接入方式。
+- 需要处理事件幂等与会话映射。
+
+### 方案 B:Webhook 事件回调
+
+Feishu 将事件 POST 到 Mercury 暴露的 HTTP endpoint。
+
+**优点**
+- 实现概念清晰。
+- 如果已有公网入口,接入路径直接。
+
+**缺点**
+- 需要验签、解密、重试、幂等处理。
+- 部署条件更苛刻,对本地/自托管不友好。
+
+### 方案 C:双模式兼容
+
+同时支持长连接和 Webhook。
+
+**优点**
+- 覆盖最广。
+
+**缺点**
+- 首版复杂度明显上升。
+- 会把 MVP 的开发和测试面拉大。
+
+### 推荐
+
+**推荐方案 A**。原因是 Mercury 本身就是一个 24/7 常驻 Agent,长连接和它的运行模型最一致,也最适合先验证 Feishu 渠道是否值得长期维护。
+
+## 5. 总体设计
+
+### 5.1 新增通道实现
+
+新增 `src/channels/feishu.ts`,实现现有 `Channel` 接口。
+
+MVP 需要的方法:
+
+- `start()`:初始化 Feishu 连接和事件监听。
+- `stop()`:停止监听并释放资源。
+- `send()`:发送纯文本回复。
+- `stream()`:先退化为一次性发送。
+- `onMessage()`:把 Feishu 入站消息转换为 `ChannelMessage`。
+- `isReady()`:暴露通道就绪状态。
+
+暂不实现:
+
+- `sendFile()`
+- 复杂 `typing()`
+- 卡片消息流式编辑
+
+### 5.2 注册与路由
+
+- 在 `src/channels/registry.ts` 中按 `channels.feishu.enabled` 和必要凭据注册 `FeishuChannel`。
+- 在 `src/index.ts` 中将 `send_message` / `send_file` 的目标通道路由改为“优先回到当前消息来源渠道”。
+- 保持 CLI 和 Telegram 现有行为不变。
+
+### 5.3 配置扩展
+
+在 `src/utils/config.ts` 扩展 `MercuryConfig.channels`:
+
+```ts
+channels: {
+ telegram: { ... },
+ feishu: {
+ enabled: boolean;
+ appId: string;
+ appSecret: string;
+ allowedUserIds: string[];
+ admins: FeishuAccessUser[];
+ members: FeishuAccessUser[];
+ pending: FeishuPendingRequest[];
+ }
+}
+```
+
+说明:
+
+- `allowedUserIds` 是可选白名单,作为最小 ACL。
+- `admins / members / pending` 的结构只用于 Feishu,不与 Telegram 共享。
+- 用户主键使用 Feishu 的稳定用户 ID,不使用昵称。
+
+### 5.4 消息模型
+
+Feishu 入站消息统一转换为 `ChannelMessage`,字段要求:
+
+- `channelType = 'feishu'`
+- `channelId` 使用 Feishu 会话标识
+- `senderId` 使用 Feishu 用户唯一 ID
+- `senderName` 作为辅助显示,不作为身份依据
+- `metadata` 保存 Feishu 原始事件中的必要标识,用于幂等和定位
+
+### 5.5 访问控制
+
+MVP 采用与 Telegram 类似的三段式状态:
+
+1. 新用户消息进入 pending。
+2. CLI 侧审核通过后进入 members 或 admins。
+3. 只有已批准用户可以进入 Agent 主循环。
+
+首版不做 Feishu 内部审批 UI,审批动作仍由 CLI 侧完成,保持现有 Mercury 的运维路径一致。
+
+## 6. 数据流
+
+1. Feishu 事件到达 `FeishuChannel`。
+2. 通道层先做基础校验和幂等判断。
+3. 未授权用户进入 pending;已授权用户被封装为 `ChannelMessage`。
+4. `ChannelRegistry` 将消息转交 `Agent`。
+5. `Agent` 执行工具调用与推理。
+6. `send()` 按消息来源渠道把结果发回 Feishu。
+
+## 7. 错误处理与安全
+
+### 7.1 错误处理
+
+- 配置缺失:跳过 Feishu 注册,不影响其他通道。
+- 外部接口失败:记录日志并降级,不让主进程崩溃。
+- 重复事件:以事件 ID 做幂等,防止重复入队。
+- 非私聊消息:先忽略,不扩大首版范围。
+- 文本过长:先做简单分段或截断,保证可发送。
+
+### 7.2 安全
+
+- 用户身份只信任 Feishu 稳定 ID,不信任昵称或展示名。
+- 如果后续启用 Webhook,再补验签与解密。
+- 只保留最小必要的事件字段,避免把原始 payload 大量落日志。
+
+## 8. 测试计划
+
+### 单元/集成检查
+
+- 通道注册:Feishu enabled 时能注册,disabled 时不注册。
+- 路由:Feishu 入站消息回复仍回到 Feishu。
+- 访问控制:未批准用户只会进入 pending。
+- 幂等:重复事件不会导致重复处理。
+- 配置回归:现有 CLI / Telegram 行为不变。
+
+### 基线验证
+
+- `npm run build`
+- `npm run lint`
+- `npm run test`
+
+## 9. 交付边界
+
+第一阶段交付只要求:
+
+- Feishu 私聊能与 Mercury 互通文本。
+- 基础访问控制可用。
+- 不破坏现有 CLI / Telegram。
+
+后续可在同一分支继续扩展:
+
+- 群聊 @
+- 卡片交互
+- 文件发送
+- 更完整的 Feishu 事件与状态管理
+
+## 10. 结论
+
+Feishu 渠道适合以独立通道方式接入,且第一版应该尽量收敛到 **私聊文本 + 基础访问控制**。这样可以快速验证渠道价值,同时把风险控制在可回滚范围内。
diff --git a/package-lock.json b/package-lock.json
index e6c412d..89f43b1 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
"@ai-sdk/deepseek": "^2.0.29",
"@ai-sdk/openai": "^3.0.53",
"@grammyjs/auto-retry": "^2.0.2",
+ "@larksuiteoapi/node-sdk": "^1.62.1",
"ai": "^6.0.168",
"chalk": "^5.4.0",
"commander": "^12.1.0",
@@ -23,7 +24,7 @@
"node-cron": "^3.0.3",
"ollama-ai-provider": "^1.2.0",
"pino": "^9.6.0",
- "yaml": "^2.7.0",
+ "yaml": "^2.8.4",
"zod": "^3.25.76"
},
"bin": {
@@ -723,6 +724,21 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "node_modules/@larksuiteoapi/node-sdk": {
+ "version": "1.62.1",
+ "resolved": "https://registry.npmmirror.com/@larksuiteoapi/node-sdk/-/node-sdk-1.62.1.tgz",
+ "integrity": "sha512-o9oAjv5Ffnp/6iXIJLHrO6N0US/r2ZZy3xmO6ylGegjuVSC05cx0fADA38Dc1h0FV8T9BDK+ariWk84TNMGbKg==",
+ "license": "MIT",
+ "dependencies": {
+ "axios": "~1.13.3",
+ "lodash.identity": "^3.0.0",
+ "lodash.merge": "^4.6.2",
+ "lodash.pickby": "^4.6.0",
+ "protobufjs": "^7.2.6",
+ "qs": "^6.14.2",
+ "ws": "^8.19.0"
+ }
+ },
"node_modules/@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
@@ -736,6 +752,70 @@
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="
},
+ "node_modules/@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/codegen": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.5.tgz",
+ "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "node_modules/@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/inquire": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.1.tgz",
+ "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@protobufjs/utf8": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.1.tgz",
+ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==",
+ "license": "BSD-3-Clause"
+ },
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
@@ -1102,7 +1182,6 @@
"version": "22.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
"integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
- "dev": true,
"dependencies": {
"undici-types": "~6.21.0"
}
@@ -1331,6 +1410,12 @@
"node": ">=12"
}
},
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
"node_modules/atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
@@ -1339,6 +1424,17 @@
"node": ">=8.0.0"
}
},
+ "node_modules/axios": {
+ "version": "1.13.6",
+ "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.6.tgz",
+ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -1443,6 +1539,35 @@
"node": ">=8"
}
},
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/chai": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
@@ -1501,6 +1626,18 @@
"license": "ISC",
"optional": true
},
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -1581,6 +1718,15 @@
"node": ">=4.0.0"
}
},
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1602,6 +1748,20 @@
"url": "https://dotenvx.com"
}
},
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -1612,12 +1772,57 @@
"once": "^1.4.0"
}
},
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true
},
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
@@ -1739,6 +1944,42 @@
"rollup": "^4.34.8"
}
},
+ "node_modules/follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
"node_modules/fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -1760,6 +2001,52 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -1767,6 +2054,18 @@
"license": "MIT",
"optional": true
},
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/grammy": {
"version": "1.42.0",
"resolved": "https://registry.npmjs.org/grammy/-/grammy-1.42.0.tgz",
@@ -1781,6 +2080,45 @@
"node": "^12.20.0 || >=14.13.1"
}
},
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -1865,6 +2203,30 @@
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
}
},
+ "node_modules/lodash.identity": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/lodash.identity/-/lodash.identity-3.0.0.tgz",
+ "integrity": "sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.pickby": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmmirror.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
+ "integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q==",
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
"node_modules/loupe": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
@@ -1891,6 +2253,36 @@
"node": ">= 18"
}
},
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
"node_modules/mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
@@ -2025,6 +2417,18 @@
"node": ">=0.10.0"
}
},
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/ollama-ai-provider": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ollama-ai-provider/-/ollama-ai-provider-1.2.0.tgz",
@@ -2271,6 +2675,36 @@
}
]
},
+ "node_modules/protobufjs": {
+ "version": "7.5.6",
+ "resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.5.6.tgz",
+ "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==",
+ "hasInstallScript": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.5",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.1",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.1",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
"node_modules/pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
@@ -2282,6 +2716,21 @@
"once": "^1.3.1"
}
},
+ "node_modules/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
@@ -2439,6 +2888,78 @@
"node": ">=10"
}
},
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
"node_modules/siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -2828,8 +3349,7 @@
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
"node_modules/util-deprecate": {
"version": "1.0.2",
@@ -3508,10 +4028,32 @@
"license": "ISC",
"optional": true
},
+ "node_modules/ws": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.0.tgz",
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/yaml": {
- "version": "2.8.3",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
- "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
+ "version": "2.8.4",
+ "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.4.tgz",
+ "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==",
+ "license": "ISC",
"bin": {
"yaml": "bin.mjs"
},
@@ -3894,6 +4436,20 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
+ "@larksuiteoapi/node-sdk": {
+ "version": "1.62.1",
+ "resolved": "https://registry.npmmirror.com/@larksuiteoapi/node-sdk/-/node-sdk-1.62.1.tgz",
+ "integrity": "sha512-o9oAjv5Ffnp/6iXIJLHrO6N0US/r2ZZy3xmO6ylGegjuVSC05cx0fADA38Dc1h0FV8T9BDK+ariWk84TNMGbKg==",
+ "requires": {
+ "axios": "~1.13.3",
+ "lodash.identity": "^3.0.0",
+ "lodash.merge": "^4.6.2",
+ "lodash.pickby": "^4.6.0",
+ "protobufjs": "^7.2.6",
+ "qs": "^6.14.2",
+ "ws": "^8.19.0"
+ }
+ },
"@opentelemetry/api": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz",
@@ -3904,6 +4460,60 @@
"resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz",
"integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg=="
},
+ "@protobufjs/aspromise": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
+ "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="
+ },
+ "@protobufjs/base64": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz",
+ "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="
+ },
+ "@protobufjs/codegen": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.5.tgz",
+ "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="
+ },
+ "@protobufjs/eventemitter": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz",
+ "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="
+ },
+ "@protobufjs/fetch": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.0.tgz",
+ "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==",
+ "requires": {
+ "@protobufjs/aspromise": "^1.1.1",
+ "@protobufjs/inquire": "^1.1.0"
+ }
+ },
+ "@protobufjs/float": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz",
+ "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="
+ },
+ "@protobufjs/inquire": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.1.tgz",
+ "integrity": "sha512-mnzgDV26ueAvk7rsbt9L7bE0SuAoqyuys/sMMrmVcN5x9VsxpcG3rqAUSgDyLp0UZlmNfIbQ4fHfCtreVBk8Ew=="
+ },
+ "@protobufjs/path": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz",
+ "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="
+ },
+ "@protobufjs/pool": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz",
+ "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="
+ },
+ "@protobufjs/utf8": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.1.tgz",
+ "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg=="
+ },
"@rollup/rollup-android-arm-eabi": {
"version": "4.60.2",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz",
@@ -4119,7 +4729,6 @@
"version": "22.19.17",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz",
"integrity": "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q==",
- "dev": true,
"requires": {
"undici-types": "~6.21.0"
}
@@ -4283,11 +4892,26 @@
"integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
"dev": true
},
+ "asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
+ },
"atomic-sleep": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz",
"integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="
},
+ "axios": {
+ "version": "1.13.6",
+ "resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.6.tgz",
+ "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+ "requires": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
"base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -4348,6 +4972,24 @@
"integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==",
"dev": true
},
+ "call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "requires": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ }
+ },
+ "call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "requires": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ }
+ },
"chai": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz",
@@ -4387,6 +5029,14 @@
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
"optional": true
},
+ "combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "requires": {
+ "delayed-stream": "~1.0.0"
+ }
+ },
"commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
@@ -4439,6 +5089,11 @@
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
"optional": true
},
+ "delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="
+ },
"detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -4450,6 +5105,16 @@
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz",
"integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="
},
+ "dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "requires": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ }
+ },
"end-of-stream": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz",
@@ -4459,12 +5124,41 @@
"once": "^1.4.0"
}
},
+ "es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="
+ },
+ "es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="
+ },
"es-module-lexer": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz",
"integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==",
"dev": true
},
+ "es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "requires": {
+ "es-errors": "^1.3.0"
+ }
+ },
+ "es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "requires": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ }
+ },
"esbuild": {
"version": "0.27.7",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
@@ -4554,6 +5248,23 @@
"rollup": "^4.34.8"
}
},
+ "follow-redirects": {
+ "version": "1.16.0",
+ "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="
+ },
+ "form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "requires": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ }
+ },
"fs-constants": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
@@ -4567,12 +5278,48 @@
"dev": true,
"optional": true
},
+ "function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="
+ },
+ "get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "requires": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ }
+ },
+ "get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "requires": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ }
+ },
"github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
"optional": true
},
+ "gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="
+ },
"grammy": {
"version": "1.42.0",
"resolved": "https://registry.npmjs.org/grammy/-/grammy-1.42.0.tgz",
@@ -4584,6 +5331,27 @@
"node-fetch": "^2.7.0"
}
},
+ "has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="
+ },
+ "has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "requires": {
+ "has-symbols": "^1.0.3"
+ }
+ },
+ "hasown": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.3.tgz",
+ "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
+ "requires": {
+ "function-bind": "^1.1.2"
+ }
+ },
"ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -4639,6 +5407,26 @@
"integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==",
"dev": true
},
+ "lodash.identity": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/lodash.identity/-/lodash.identity-3.0.0.tgz",
+ "integrity": "sha512-AupTIzdLQxJS5wIYUQlgGyk2XRTfGXA+MCghDHqZk0pzUNYvd3EESS6dkChNauNYVIutcb0dfHw1ri9Q1yPV8Q=="
+ },
+ "lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="
+ },
+ "lodash.pickby": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmmirror.com/lodash.pickby/-/lodash.pickby-4.6.0.tgz",
+ "integrity": "sha512-AZV+GsS/6ckvPOVQPXSiFFacKvKB4kOQu6ynt9wz0F3LO4R9Ij4K1ddYsIytDpSgLz88JHd9P+oaLeej5/Sl7Q=="
+ },
+ "long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="
+ },
"loupe": {
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz",
@@ -4659,6 +5447,24 @@
"resolved": "https://registry.npmjs.org/marked/-/marked-14.1.4.tgz",
"integrity": "sha512-vkVZ8ONmUdPnjCKc5uTRvmkRbx4EAi2OkTOXmfTDhZz3OFqMNBM1oTTWwTr4HY4uAEojhzPf+Fy8F1DWa3Sndg=="
},
+ "math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="
+ },
+ "mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="
+ },
+ "mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "requires": {
+ "mime-db": "1.52.0"
+ }
+ },
"mimic-response": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
@@ -4747,6 +5553,11 @@
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"dev": true
},
+ "object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="
+ },
"ollama-ai-provider": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ollama-ai-provider/-/ollama-ai-provider-1.2.0.tgz",
@@ -4893,6 +5704,30 @@
"resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz",
"integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="
},
+ "protobufjs": {
+ "version": "7.5.6",
+ "resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.5.6.tgz",
+ "integrity": "sha512-M71sTMB146U3u0di3yup8iM+zv8yPRNQVr1KK4tyBitl3qFvEGucq/rGDRShD2rsJhtN02RJaJ7j5X5hmy8SJg==",
+ "requires": {
+ "@protobufjs/aspromise": "^1.1.2",
+ "@protobufjs/base64": "^1.1.2",
+ "@protobufjs/codegen": "^2.0.5",
+ "@protobufjs/eventemitter": "^1.1.0",
+ "@protobufjs/fetch": "^1.1.0",
+ "@protobufjs/float": "^1.0.2",
+ "@protobufjs/inquire": "^1.1.1",
+ "@protobufjs/path": "^1.1.2",
+ "@protobufjs/pool": "^1.1.0",
+ "@protobufjs/utf8": "^1.1.1",
+ "@types/node": ">=13.7.0",
+ "long": "^5.0.0"
+ }
+ },
+ "proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
+ },
"pump": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz",
@@ -4903,6 +5738,14 @@
"once": "^1.3.1"
}
},
+ "qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmmirror.com/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "requires": {
+ "side-channel": "^1.1.0"
+ }
+ },
"quick-format-unescaped": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz",
@@ -5005,6 +5848,50 @@
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"optional": true
},
+ "side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "requires": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ }
+ },
+ "side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "requires": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ }
+ },
+ "side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "requires": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ }
+ },
+ "side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "requires": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ }
+ },
"siginfo": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
@@ -5277,8 +6164,7 @@
"undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
- "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
- "dev": true
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="
},
"util-deprecate": {
"version": "1.0.2",
@@ -5598,10 +6484,16 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"optional": true
},
+ "ws": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmmirror.com/ws/-/ws-8.20.0.tgz",
+ "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==",
+ "requires": {}
+ },
"yaml": {
- "version": "2.8.3",
- "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
- "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="
+ "version": "2.8.4",
+ "resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.4.tgz",
+ "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog=="
},
"zod": {
"version": "3.25.76",
diff --git a/package.json b/package.json
index 710dc59..b1a2cc7 100644
--- a/package.json
+++ b/package.json
@@ -58,6 +58,7 @@
"@ai-sdk/deepseek": "^2.0.29",
"@ai-sdk/openai": "^3.0.53",
"@grammyjs/auto-retry": "^2.0.2",
+ "@larksuiteoapi/node-sdk": "^1.62.1",
"ai": "^6.0.168",
"chalk": "^5.4.0",
"commander": "^12.1.0",
@@ -68,7 +69,7 @@
"node-cron": "^3.0.3",
"ollama-ai-provider": "^1.2.0",
"pino": "^9.6.0",
- "yaml": "^2.7.0",
+ "yaml": "^2.8.4",
"zod": "^3.25.76"
},
"optionalDependencies": {
diff --git a/src/channels/feishu.test.ts b/src/channels/feishu.test.ts
new file mode 100644
index 0000000..2d6f789
--- /dev/null
+++ b/src/channels/feishu.test.ts
@@ -0,0 +1,32 @@
+import { describe, expect, it } from 'vitest';
+import { normalizeFeishuEvent, resolveFeishuTargetId } from './feishu.js';
+
+describe('feishu adapter helpers', () => {
+ it('normalizes a private text event to a Mercury channel message', () => {
+ const message = normalizeFeishuEvent({
+ event_id: 'evt_1',
+ event_type: 'im.message.receive_v1',
+ create_time: '1710000000000',
+ token: 'token',
+ sender: { sender_id: { open_id: 'ou_1' } },
+ message: {
+ message_id: 'msg_1',
+ chat_id: 'oc_1',
+ message_type: 'text',
+ content: '{"text":"hello"}',
+ },
+ });
+
+ expect(message).toMatchObject({
+ channelType: 'feishu',
+ channelId: 'feishu:oc_1',
+ senderId: 'ou_1',
+ content: 'hello',
+ });
+ });
+
+ it('resolves Mercury target ids back to Feishu chat ids', () => {
+ expect(resolveFeishuTargetId('feishu:oc_2')).toBe('oc_2');
+ expect(resolveFeishuTargetId('oc_3')).toBe('oc_3');
+ });
+});
diff --git a/src/channels/feishu.ts b/src/channels/feishu.ts
new file mode 100644
index 0000000..cdad0ac
--- /dev/null
+++ b/src/channels/feishu.ts
@@ -0,0 +1,201 @@
+import * as Lark from '@larksuiteoapi/node-sdk';
+import type { ChannelMessage } from '../types/channel.js';
+import type { MercuryConfig } from '../utils/config.js';
+import {
+ addFeishuPendingRequest,
+ findFeishuApprovedUser,
+ isFeishuAutoAllowed,
+ saveConfig,
+} from '../utils/config.js';
+import { logger } from '../utils/logger.js';
+import { BaseChannel } from './base.js';
+
+/** Feishu event envelope for message receive events (SDK v1 format). */
+export interface FeishuEventEnvelope {
+ event_id?: string;
+ token?: string;
+ create_time?: string;
+ event_type?: string;
+ tenant_key?: string;
+ ts?: string;
+ uuid?: string;
+ type?: string;
+ app_id?: string;
+ sender?: {
+ sender_id?: { union_id?: string; user_id?: string; open_id?: string };
+ sender_type?: string;
+ tenant_key?: string;
+ };
+ message?: {
+ message_id?: string;
+ root_id?: string;
+ parent_id?: string;
+ create_time?: string;
+ chat_id?: string;
+ chat_type?: string;
+ message_type?: string;
+ content?: string;
+ };
+}
+
+/** Transport interface for Feishu event handling. */
+export interface FeishuTransport {
+ start(onEvent: (event: FeishuEventEnvelope) => Promise): Promise;
+ stop(): Promise;
+ sendText(chatId: string, content: string): Promise;
+}
+
+/** Resolve Mercury target ids back to Feishu chat ids. */
+export function resolveFeishuTargetId(targetId?: string): string | undefined {
+ if (!targetId) return undefined;
+ return targetId.startsWith('feishu:') ? targetId.slice('feishu:'.length) : targetId;
+}
+
+/** Normalize a Feishu event envelope to a Mercury channel message. */
+export function normalizeFeishuEvent(envelope: FeishuEventEnvelope): ChannelMessage | null {
+ const chatId = envelope.message?.chat_id;
+ const openId = envelope.sender?.sender_id?.open_id;
+ const rawContent = envelope.message?.content;
+ const messageType = envelope.message?.message_type;
+
+ // Only process text messages, ignore others silently
+ if (messageType !== 'text' || !rawContent) return null;
+ if (!chatId || !openId) return null;
+
+ let parsed: { text?: string };
+ try {
+ parsed = JSON.parse(rawContent) as { text?: string };
+ if (!parsed || typeof parsed !== 'object' || typeof parsed.text !== 'string') {
+ return null;
+ }
+ } catch {
+ return null;
+ }
+
+ const text = parsed.text.trim();
+ if (!text) return null;
+
+ return {
+ id: envelope.event_id || envelope.uuid || '',
+ channelId: `feishu:${chatId}`,
+ channelType: 'feishu',
+ senderId: openId,
+ content: text,
+ timestamp: Number(envelope.create_time || Date.now()),
+ metadata: {
+ chatId,
+ eventId: envelope.event_id,
+ messageId: envelope.message?.message_id,
+ },
+ };
+}
+
+/** Feishu channel implementation. */
+export class FeishuChannel extends BaseChannel {
+ readonly type = 'feishu' as const;
+ private transport: FeishuTransport;
+
+ constructor(private config: MercuryConfig) {
+ super();
+ this.transport = createFeishuTransport(config);
+ }
+
+ async start(): Promise {
+ await this.transport.start(async (envelope) => {
+ const message = normalizeFeishuEvent(envelope);
+ if (!message) return;
+
+ if (isFeishuAutoAllowed(this.config, message.senderId) || findFeishuApprovedUser(this.config, message.senderId)) {
+ this.emit(message);
+ return;
+ }
+
+ addFeishuPendingRequest(this.config, {
+ openId: message.senderId,
+ chatId: message.channelId.slice('feishu:'.length),
+ displayName: message.senderName,
+ });
+ saveConfig(this.config);
+ const targetChatId = resolveFeishuTargetId(message.channelId);
+ if (targetChatId) {
+ await this.transport.sendText(
+ targetChatId,
+ 'Access pending. Ask Mercury CLI to approve this Feishu user.',
+ );
+ }
+ });
+ this.ready = true;
+ }
+
+ async stop(): Promise {
+ await this.transport.stop();
+ this.ready = false;
+ }
+
+ async send(content: string, targetId?: string, _elapsedMs?: number): Promise {
+ const chatId = resolveFeishuTargetId(targetId);
+ if (!chatId) return;
+ await this.transport.sendText(chatId, content);
+ }
+
+ async sendFile(_filePath: string, _targetId?: string): Promise {
+ // Feishu file sending is not part of the MVP - no-op
+ }
+
+ async stream(content: AsyncIterable, targetId?: string): Promise {
+ let full = '';
+ for await (const chunk of content) full += chunk;
+ await this.send(full, targetId);
+ return full;
+ }
+
+ async typing(_targetId?: string): Promise {
+ return;
+ }
+
+ async askToContinue(_question: string, _targetId?: string): Promise {
+ return true;
+ }
+}
+
+/** Create a Feishu transport using the official SDK. */
+function createFeishuTransport(config: MercuryConfig): FeishuTransport {
+ const client = new Lark.Client({
+ appId: config.channels.feishu.appId,
+ appSecret: config.channels.feishu.appSecret,
+ });
+ const wsClient = new Lark.WSClient({
+ appId: config.channels.feishu.appId,
+ appSecret: config.channels.feishu.appSecret,
+ });
+
+ return {
+ start: async (onEvent) => {
+ try {
+ wsClient.start({
+ eventDispatcher: new Lark.EventDispatcher({}).register({
+ 'im.message.receive_v1': async (event: FeishuEventEnvelope) => {
+ await onEvent(event);
+ },
+ }),
+ });
+ } catch (err) {
+ logger.error({ err }, 'Failed to start Feishu WebSocket');
+ throw err;
+ }
+ },
+ stop: async () => {
+ wsClient.close();
+ },
+ sendText: async (chatId, content) => {
+ await client.im.v1.message.create({
+ params: { receive_id_type: 'chat_id' },
+ data: {
+ receive_id: chatId,
+ msg_type: 'text',
+ content: JSON.stringify({ text: content }),
+ },
+ });
+ },
+ };
+}
diff --git a/src/channels/index.ts b/src/channels/index.ts
index 6d1dffc..4a75e7c 100644
--- a/src/channels/index.ts
+++ b/src/channels/index.ts
@@ -1,5 +1,6 @@
export { BaseChannel } from './base.js';
export type { Channel } from './base.js';
export { CLIChannel } from './cli.js';
+export { FeishuChannel } from './feishu.js';
export { TelegramChannel } from './telegram.js';
export { ChannelRegistry } from './registry.js';
\ No newline at end of file
diff --git a/src/channels/registry.ts b/src/channels/registry.ts
index 9768c54..15520a4 100644
--- a/src/channels/registry.ts
+++ b/src/channels/registry.ts
@@ -1,6 +1,7 @@
import type { Channel } from './base.js';
import type { ChannelMessage, ChannelType } from '../types/channel.js';
import { CLIChannel } from './cli.js';
+import { FeishuChannel } from './feishu.js';
import { TelegramChannel } from './telegram.js';
import type { MercuryConfig } from '../utils/config.js';
import { logger } from '../utils/logger.js';
@@ -14,6 +15,10 @@ export class ChannelRegistry {
if (config.channels.telegram.enabled && config.channels.telegram.botToken) {
this.register('telegram', new TelegramChannel(config));
}
+
+ if (config.channels.feishu.enabled && config.channels.feishu.appId && config.channels.feishu.appSecret) {
+ this.register('feishu', new FeishuChannel(config));
+ }
}
register(type: ChannelType, channel: Channel): void {
diff --git a/src/core/channel-routing.test.ts b/src/core/channel-routing.test.ts
new file mode 100644
index 0000000..edb9494
--- /dev/null
+++ b/src/core/channel-routing.test.ts
@@ -0,0 +1,24 @@
+import { describe, expect, it } from 'vitest';
+import { pickOutboundChannelType } from './channel-routing.js';
+
+describe('pickOutboundChannelType', () => {
+ it('prefers the current ready channel', () => {
+ expect(
+ pickOutboundChannelType({
+ currentChannelType: 'feishu',
+ readyChannels: ['cli', 'feishu'],
+ fallbackChannel: 'cli',
+ }),
+ ).toBe('feishu');
+ });
+
+ it('falls back to the notification channel when the current channel is not ready', () => {
+ expect(
+ pickOutboundChannelType({
+ currentChannelType: 'feishu',
+ readyChannels: ['cli'],
+ fallbackChannel: 'cli',
+ }),
+ ).toBe('cli');
+ });
+});
diff --git a/src/core/channel-routing.ts b/src/core/channel-routing.ts
new file mode 100644
index 0000000..b70a463
--- /dev/null
+++ b/src/core/channel-routing.ts
@@ -0,0 +1,18 @@
+import type { ChannelType } from '../types/channel.js';
+
+export interface OutboundChannelContext {
+ currentChannelType: ChannelType;
+ readyChannels: ChannelType[];
+ fallbackChannel: ChannelType;
+}
+
+/** Pick the outbound channel type based on context. */
+export function pickOutboundChannelType(context: OutboundChannelContext): ChannelType {
+ if (context.readyChannels.includes(context.currentChannelType)) {
+ return context.currentChannelType;
+ }
+ if (context.readyChannels.includes(context.fallbackChannel)) {
+ return context.fallbackChannel;
+ }
+ return context.readyChannels[0] ?? context.fallbackChannel;
+}
diff --git a/src/core/supervisor.ts b/src/core/supervisor.ts
index 4e7aad7..1eda6f1 100644
--- a/src/core/supervisor.ts
+++ b/src/core/supervisor.ts
@@ -21,6 +21,7 @@ export class SubAgentSupervisor {
private fileLockManager: FileLockManager;
private taskBoard: TaskBoard;
private resourceManager: ResourceManager;
+ private scheduleLock = false;
private agentConfig: MercuryConfig;
private providers: ProviderRegistry;
@@ -177,6 +178,7 @@ export class SubAgentSupervisor {
this.activeAgents.delete(config.id);
this.fileLockManager.releaseAll(config.id);
this.pausedAgents.delete(config.id);
+ this.scheduleLock = false;
await this.processWaitQueue();
});
}
@@ -208,13 +210,19 @@ export class SubAgentSupervisor {
}
private async processWaitQueue(): Promise {
- while (this.waitQueue.length > 0) {
- const running = this.getRunningCount();
- if (running >= this.resourceManager.getMaxConcurrent()) break;
-
- const nextConfig = this.waitQueue.shift()!;
- this.taskBoard.update(nextConfig.id, { status: 'running', progress: 'Starting...' });
- this.startAgentInBackground(nextConfig);
+ if (this.scheduleLock) return;
+ this.scheduleLock = true;
+ try {
+ while (this.waitQueue.length > 0) {
+ const running = this.getRunningCount();
+ if (running >= this.resourceManager.getMaxConcurrent()) break;
+
+ const nextConfig = this.waitQueue.shift()!;
+ this.taskBoard.update(nextConfig.id, { status: 'running', progress: 'Starting...' });
+ this.startAgentInBackground(nextConfig);
+ }
+ } finally {
+ this.scheduleLock = false;
}
}
diff --git a/src/index.ts b/src/index.ts
index e2225bb..567678a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,6 +12,7 @@ import {
getMercuryHome,
ensureCreatorField,
clearTelegramAccess,
+ clearFeishuAccess,
isProviderConfigured,
getTelegramAccessSummary,
getTelegramApprovedUsers,
@@ -23,6 +24,7 @@ import {
promoteTelegramUserToAdmin,
demoteTelegramAdmin,
hasTelegramAdmins,
+ getFeishuAccessSummary,
} from './utils/config.js';
import type { MercuryConfig } from './utils/config.js';
import type { ProviderName } from './utils/config.js';
@@ -114,6 +116,7 @@ const PROVIDER_OPTIONS: Array<{ key: ProviderName; label: string }> = [
{ key: 'openaiCompat', label: 'OpenAI Compilations' },
{ key: 'mimo', label: 'MiMo (Xiaomi)' },
{ key: 'mimoTokenPlan', label: 'MiMo Token Plan (Xiaomi)' },
+ { key: 'minimax', label: 'MiniMax' },
];
function getConfiguredProviderNames(config: MercuryConfig): ProviderName[] {
@@ -264,6 +267,12 @@ function validateApiKey(provider: ProviderName, value: string): string | null {
: 'MiMo Token Plan keys must start with `tp-`.';
}
+ if (provider === 'minimax') {
+ return looksLikeToken(value)
+ ? null
+ : 'MiniMax keys must look like a real API token: long, no spaces, and not plain text.';
+ }
+
return null;
}
@@ -817,6 +826,32 @@ async function configure(existingConfig?: MercuryConfig): Promise {
config.providers.mimoTokenPlan.enabled = true;
}
}
+
+ if (provider === 'minimax') {
+ const mask = isReconfig && config.providers.minimax.apiKey ? ` [${maskKey(config.providers.minimax.apiKey)}]` : '';
+ const result = await promptApiKeyWithModelSelection(
+ config,
+ 'minimax',
+ 'MiniMax',
+ chalk.white(` MiniMax API key${mask}${isReconfig ? '' : ' (Enter to skip)'}: `),
+ isReconfig,
+ );
+ if (!result.skipped && result.apiKey && result.model) {
+ config.providers.minimax.apiKey = result.apiKey;
+ config.providers.minimax.model = result.model;
+ // Let user choose endpoint
+ console.log(chalk.white(' MiniMax endpoint:'));
+ console.log(chalk.white(' 1. International - https://api.minimax.io/anthropic/v1'));
+ console.log(chalk.white(' 2. China (Mainland) - https://api.minimaxi.com/anthropic/v1'));
+ const endpointChoice = await ask(chalk.white(' Choose endpoint (1 or 2): '));
+ if (endpointChoice === '2') {
+ config.providers.minimax.baseUrl = 'https://api.minimaxi.com/anthropic/v1';
+ } else {
+ config.providers.minimax.baseUrl = 'https://api.minimax.io/anthropic/v1';
+ }
+ config.providers.minimax.enabled = true;
+ }
+ }
}
const configuredProviders = getConfiguredProviderNames(config);
@@ -883,6 +918,44 @@ async function configure(existingConfig?: MercuryConfig): Promise {
await completeInitialTelegramPairing(config);
+ hr();
+ console.log('');
+ console.log(chalk.bold.white(' Feishu (optional)'));
+ if (isReconfig) {
+ console.log(chalk.dim(' Leave empty to keep current value. Enter "none" to disable.'));
+ } else {
+ console.log(chalk.dim(' Leave empty to skip. You can add it later with mercury doctor.'));
+ console.log(chalk.dim(' To set up a Feishu bot:'));
+ console.log(chalk.dim(' 1. Go to https://open.feishu.cn/app and create an app'));
+ console.log(chalk.dim(' 2. Enable "Bot" capability and "Subscribe to messages"'));
+ console.log(chalk.dim(' 3. Get App ID and App Secret from the app credentials page'));
+ console.log(chalk.dim(' After setup, users send a message to request access.'));
+ console.log(chalk.dim(' Approve access from the CLI with: mercury feishu approve '));
+ }
+ console.log('');
+
+ const fsMask = isReconfig && config.channels.feishu.appId ? ` [${maskKey(config.channels.feishu.appId)}]` : '';
+ const feishuAppId = await ask(chalk.white(` Feishu App ID${fsMask}: `));
+ if (isReconfig && feishuAppId.toLowerCase() === 'none') {
+ config.channels.feishu.enabled = false;
+ config.channels.feishu.appId = '';
+ config.channels.feishu.appSecret = '';
+ clearFeishuAccess(config);
+ } else if (feishuAppId) {
+ if (feishuAppId !== config.channels.feishu.appId) {
+ clearFeishuAccess(config);
+ }
+ config.channels.feishu.appId = feishuAppId;
+ const feishuAppSecret = await ask(chalk.white(' Feishu App Secret: '));
+ config.channels.feishu.appSecret = feishuAppSecret;
+ config.channels.feishu.enabled = true;
+
+ const feishuAllowed = await ask(chalk.white(' Auto-allowed User IDs (comma-separated, optional): '));
+ if (feishuAllowed) {
+ config.channels.feishu.allowedUserIds = feishuAllowed.split(',').map((id) => id.trim()).filter(Boolean);
+ }
+ }
+
hr();
console.log('');
console.log(chalk.bold.white(' GitHub Integration (optional)'));
diff --git a/src/memory/store.ts b/src/memory/store.ts
index 9abcff9..ca83145 100644
--- a/src/memory/store.ts
+++ b/src/memory/store.ts
@@ -110,7 +110,11 @@ export class ShortTermMemory {
private saveToDisk(conversationId: string, messages: MemoryEntry[]): void {
const filepath = join(this.dir, `${conversationId}.json`);
- writeFileSync(filepath, JSON.stringify(messages), 'utf-8');
+ try {
+ writeFileSync(filepath, JSON.stringify(messages), 'utf-8');
+ } catch (err) {
+ logger.error({ err, filepath }, 'Failed to save short-term memory to disk');
+ }
}
}
diff --git a/src/providers/index.ts b/src/providers/index.ts
index 568dbe4..0c54009 100644
--- a/src/providers/index.ts
+++ b/src/providers/index.ts
@@ -4,5 +4,6 @@ export { AnthropicProvider } from './anthropic.js';
export { DeepSeekProvider } from './deepseek.js';
export { OllamaProvider } from './ollama.js';
export { MiMoProvider } from './mimo.js';
+export { MiniMaxProvider } from './minimax.js';
export { ProviderRegistry } from './registry.js';
export type { LLMResponse, LLMStreamChunk } from './base.js';
diff --git a/src/providers/minimax.ts b/src/providers/minimax.ts
new file mode 100644
index 0000000..61a5e2e
--- /dev/null
+++ b/src/providers/minimax.ts
@@ -0,0 +1,61 @@
+import { createAnthropic } from '@ai-sdk/anthropic';
+import { generateText, streamText } from 'ai';
+import { BaseProvider } from './base.js';
+import type { ProviderConfig } from '../utils/config.js';
+import type { LLMResponse, LLMStreamChunk } from './base.js';
+
+export class MiniMaxProvider extends BaseProvider {
+ readonly name = 'minimax';
+ readonly model: string;
+ private client: ReturnType;
+ private modelInstance: ReturnType['languageModel']>;
+
+ constructor(config: ProviderConfig) {
+ super(config);
+ this.model = config.model;
+
+ this.client = createAnthropic({
+ apiKey: config.apiKey,
+ baseURL: config.baseUrl || 'https://api.minimax.io/anthropic/v1',
+ });
+ this.modelInstance = this.client(config.model);
+ }
+
+ async generateText(prompt: string, systemPrompt: string): Promise {
+ const result = await generateText({
+ model: this.modelInstance,
+ system: systemPrompt,
+ prompt,
+ });
+
+ return {
+ text: result.text,
+ inputTokens: result.usage?.inputTokens ?? 0,
+ outputTokens: result.usage?.outputTokens ?? 0,
+ totalTokens: (result.usage?.inputTokens ?? 0) + (result.usage?.outputTokens ?? 0),
+ model: this.model,
+ provider: this.name,
+ };
+ }
+
+ async *streamText(prompt: string, systemPrompt: string): AsyncIterable {
+ const result = streamText({
+ model: this.modelInstance,
+ system: systemPrompt,
+ prompt,
+ });
+
+ for await (const chunk of result.textStream) {
+ yield { text: chunk, done: false };
+ }
+ yield { text: '', done: true };
+ }
+
+ isAvailable(): boolean {
+ return this.config.apiKey.length > 0;
+ }
+
+ getModelInstance(): any {
+ return this.modelInstance;
+ }
+}
\ No newline at end of file
diff --git a/src/providers/registry.ts b/src/providers/registry.ts
index 16a52ee..b10c267 100644
--- a/src/providers/registry.ts
+++ b/src/providers/registry.ts
@@ -6,6 +6,7 @@ import { AnthropicProvider } from './anthropic.js';
import { DeepSeekProvider } from './deepseek.js';
import { OllamaProvider } from './ollama.js';
import { MiMoProvider } from './mimo.js';
+import { MiniMaxProvider } from './minimax.js';
import { logger } from '../utils/logger.js';
export class ProviderRegistry {
@@ -26,6 +27,7 @@ export class ProviderRegistry {
config.providers.openaiCompat,
config.providers.mimo,
config.providers.mimoTokenPlan,
+ config.providers.minimax,
];
for (const pc of entries) {
@@ -44,6 +46,8 @@ export class ProviderRegistry {
provider = new OpenAICompatProvider(pc, { useChatApi: true });
} else if (pc.name === 'mimo' || pc.name === 'mimoTokenPlan') {
provider = new MiMoProvider(pc);
+ } else if (pc.name === 'minimax') {
+ provider = new MiniMaxProvider(pc);
} else {
provider = new OpenAICompatProvider(pc);
}
diff --git a/src/types/channel.ts b/src/types/channel.ts
index 2b494dc..089bcc9 100644
--- a/src/types/channel.ts
+++ b/src/types/channel.ts
@@ -16,7 +16,7 @@ export interface TelegramPendingRequest {
pairingCode?: string;
}
-export type ChannelType = 'cli' | 'telegram' | 'internal' | 'signal' | 'discord' | 'slack' | 'whatsapp';
+export type ChannelType = 'cli' | 'telegram' | 'feishu' | 'internal' | 'signal' | 'discord' | 'slack' | 'whatsapp';
export interface ChannelMessage {
id: string;
diff --git a/src/utils/config.ts b/src/utils/config.ts
index a219d25..833e421 100644
--- a/src/utils/config.ts
+++ b/src/utils/config.ts
@@ -46,6 +46,21 @@ export interface TelegramPendingRequest {
pairingCode?: string;
}
+export interface FeishuAccessUser {
+ openId: string;
+ chatId: string;
+ displayName?: string;
+ requestedAt?: string;
+ approvedAt: string;
+}
+
+export interface FeishuPendingRequest {
+ openId: string;
+ chatId: string;
+ displayName?: string;
+ requestedAt: string;
+}
+
export type ProviderName =
| 'openai'
| 'anthropic'
@@ -55,7 +70,8 @@ export type ProviderName =
| 'ollamaLocal'
| 'openaiCompat'
| 'mimo'
- | 'mimoTokenPlan';
+ | 'mimoTokenPlan'
+ | 'minimax';
export interface MercuryConfig {
identity: {
@@ -74,6 +90,7 @@ export interface MercuryConfig {
openaiCompat: ProviderConfig;
mimo: ProviderConfig;
mimoTokenPlan: ProviderConfig;
+ minimax: ProviderConfig;
};
channels: {
telegram: {
@@ -89,6 +106,15 @@ export interface MercuryConfig {
pairedChatId?: number;
pairedUsername?: string;
};
+ feishu: {
+ enabled: boolean;
+ appId: string;
+ appSecret: string;
+ allowedUserIds: string[];
+ admins: FeishuAccessUser[];
+ members: FeishuAccessUser[];
+ pending: FeishuPendingRequest[];
+ };
};
github: {
username: string;
@@ -216,6 +242,13 @@ export function getDefaultConfig(): MercuryConfig {
model: getEnv('MIMO_TOKEN_PLAN_MODEL', 'mimo-v2.5-pro'),
enabled: getEnvBool('MIMO_TOKEN_PLAN_ENABLED', false),
},
+ minimax: {
+ name: 'minimax',
+ apiKey: getEnv('MINIMAX_API_KEY', ''),
+ baseUrl: getEnv('MINIMAX_BASE_URL', 'https://api.minimax.io/anthropic/v1'),
+ model: getEnv('MINIMAX_MODEL', ''),
+ enabled: getEnvBool('MINIMAX_ENABLED', true),
+ },
},
channels: {
telegram: {
@@ -231,6 +264,18 @@ export function getDefaultConfig(): MercuryConfig {
members: [],
pending: [],
},
+ feishu: {
+ enabled: getEnvBool('FEISHU_ENABLED', false),
+ appId: getEnv('FEISHU_APP_ID', ''),
+ appSecret: getEnv('FEISHU_APP_SECRET', ''),
+ allowedUserIds: getEnv('FEISHU_ALLOWED_USER_IDS', '')
+ .split(',')
+ .filter(Boolean)
+ .map((value) => value.trim()),
+ admins: [],
+ members: [],
+ pending: [],
+ },
},
github: {
username: getEnv('GITHUB_USERNAME', ''),
@@ -517,6 +562,155 @@ export function clearTelegramAccess(config: MercuryConfig): MercuryConfig {
return config;
}
+// Feishu access helpers
+
+/** Return the approved Feishu users. */
+export function getFeishuApprovedUsers(config: MercuryConfig): FeishuAccessUser[] {
+ return [...config.channels.feishu.admins, ...config.channels.feishu.members];
+}
+
+/** Return the Feishu admins. */
+export function getFeishuAdmins(config: MercuryConfig): FeishuAccessUser[] {
+ return config.channels.feishu.admins;
+}
+
+/** Return the pending Feishu requests. */
+export function getFeishuPendingRequests(config: MercuryConfig): FeishuPendingRequest[] {
+ return config.channels.feishu.pending;
+}
+
+/** Find an approved Feishu user by openId. */
+export function findFeishuApprovedUser(config: MercuryConfig, openId: string): FeishuAccessUser | undefined {
+ return getFeishuApprovedUsers(config).find((user) => user.openId === openId);
+}
+
+/** Find a pending Feishu request by openId. */
+export function findFeishuPendingRequest(config: MercuryConfig, openId: string): FeishuPendingRequest | undefined {
+ return config.channels.feishu.pending.find((request) => request.openId === openId);
+}
+
+/** Check whether the config already has a Feishu admin. */
+export function hasFeishuAdmins(config: MercuryConfig): boolean {
+ return config.channels.feishu.admins.length > 0;
+}
+
+/** Check whether a Feishu openId is auto-allowed. */
+export function isFeishuAutoAllowed(config: MercuryConfig, openId: string): boolean {
+ return config.channels.feishu.allowedUserIds.includes(openId);
+}
+
+/** Summarize Feishu access state. */
+export function getFeishuAccessSummary(config: MercuryConfig): string {
+ return `${config.channels.feishu.admins.length} admin${config.channels.feishu.admins.length === 1 ? '' : 's'}, `
+ + `${config.channels.feishu.members.length} member${config.channels.feishu.members.length === 1 ? '' : 's'}, `
+ + `${config.channels.feishu.pending.length} pending`;
+}
+
+/** Add a Feishu pending request. */
+export function addFeishuPendingRequest(
+ config: MercuryConfig,
+ request: Omit & { requestedAt?: string },
+): FeishuPendingRequest {
+ const existing = findFeishuPendingRequest(config, request.openId);
+ if (existing) {
+ existing.chatId = request.chatId;
+ existing.displayName = request.displayName || existing.displayName;
+ return existing;
+ }
+
+ const created: FeishuPendingRequest = {
+ ...request,
+ requestedAt: request.requestedAt || new Date().toISOString(),
+ };
+ config.channels.feishu.pending.push(created);
+ return created;
+}
+
+/** Approve a Feishu pending request. */
+export function approveFeishuPendingRequest(
+ config: MercuryConfig,
+ openId: string,
+ role: 'admin' | 'member' = 'member',
+): FeishuAccessUser | null {
+ const request = findFeishuPendingRequest(config, openId);
+ if (!request) return null;
+
+ const approvedUser: FeishuAccessUser = {
+ openId: request.openId,
+ chatId: request.chatId,
+ displayName: request.displayName,
+ requestedAt: request.requestedAt,
+ approvedAt: new Date().toISOString(),
+ };
+
+ config.channels.feishu.pending = config.channels.feishu.pending.filter((entry) => entry.openId !== openId);
+ config.channels.feishu.admins = config.channels.feishu.admins.filter((entry) => entry.openId !== openId);
+ config.channels.feishu.members = config.channels.feishu.members.filter((entry) => entry.openId !== openId);
+
+ if (role === 'admin') {
+ config.channels.feishu.admins.push(approvedUser);
+ } else {
+ config.channels.feishu.members.push(approvedUser);
+ }
+
+ return approvedUser;
+}
+
+/** Reject a Feishu pending request. */
+export function rejectFeishuPendingRequest(config: MercuryConfig, openId: string): FeishuPendingRequest | null {
+ const request = findFeishuPendingRequest(config, openId);
+ if (!request) return null;
+ config.channels.feishu.pending = config.channels.feishu.pending.filter((entry) => entry.openId !== openId);
+ return request;
+}
+
+/** Remove a Feishu user from approved access. */
+export function removeFeishuUser(config: MercuryConfig, openId: string): FeishuAccessUser | null {
+ const admin = config.channels.feishu.admins.find((entry) => entry.openId === openId);
+ if (admin) {
+ config.channels.feishu.admins = config.channels.feishu.admins.filter((entry) => entry.openId !== openId);
+ return admin;
+ }
+
+ const member = config.channels.feishu.members.find((entry) => entry.openId === openId);
+ if (member) {
+ config.channels.feishu.members = config.channels.feishu.members.filter((entry) => entry.openId !== openId);
+ return member;
+ }
+
+ return null;
+}
+
+/** Promote a Feishu member to admin. */
+export function promoteFeishuUserToAdmin(config: MercuryConfig, openId: string): FeishuAccessUser | null {
+ const member = config.channels.feishu.members.find((entry) => entry.openId === openId);
+ if (!member) return null;
+ config.channels.feishu.members = config.channels.feishu.members.filter((entry) => entry.openId !== openId);
+ config.channels.feishu.admins.push(member);
+ return member;
+}
+
+/** Demote a Feishu admin to member. */
+export function demoteFeishuAdmin(config: MercuryConfig, openId: string): FeishuAccessUser | null {
+ if (config.channels.feishu.admins.length <= 1) {
+ return null;
+ }
+
+ const admin = config.channels.feishu.admins.find((entry) => entry.openId === openId);
+ if (!admin) return null;
+ config.channels.feishu.admins = config.channels.feishu.admins.filter((entry) => entry.openId !== openId);
+ config.channels.feishu.members.push(admin);
+ return admin;
+}
+
+/** Clear all Feishu access state. */
+export function clearFeishuAccess(config: MercuryConfig): MercuryConfig {
+ config.channels.feishu.admins = [];
+ config.channels.feishu.members = [];
+ config.channels.feishu.pending = [];
+ return config;
+}
+
export function clearTelegramPairing(config: MercuryConfig): MercuryConfig {
return clearTelegramAccess(config);
}
diff --git a/src/utils/feishu-access.test.ts b/src/utils/feishu-access.test.ts
new file mode 100644
index 0000000..6345bdc
--- /dev/null
+++ b/src/utils/feishu-access.test.ts
@@ -0,0 +1,41 @@
+import { describe, expect, it } from 'vitest';
+import {
+ addFeishuPendingRequest,
+ approveFeishuPendingRequest,
+ clearFeishuAccess,
+ demoteFeishuAdmin,
+ getDefaultConfig,
+ getFeishuAdmins,
+ getFeishuAccessSummary,
+ getFeishuApprovedUsers,
+ getFeishuPendingRequests,
+ findFeishuApprovedUser,
+ findFeishuPendingRequest,
+ hasFeishuAdmins,
+ isFeishuAutoAllowed,
+ promoteFeishuUserToAdmin,
+ rejectFeishuPendingRequest,
+ removeFeishuUser,
+} from './config.js';
+
+describe('feishu access config helpers', () => {
+ it('creates, approves, promotes, demotes, rejects, and clears Feishu access', () => {
+ const config = getDefaultConfig();
+ config.channels.feishu.allowedUserIds = ['ou_allow'];
+
+ addFeishuPendingRequest(config, { openId: 'ou_1', chatId: 'oc_1', displayName: 'alpha' });
+ addFeishuPendingRequest(config, { openId: 'ou_2', chatId: 'oc_2', displayName: 'beta' });
+
+ expect(isFeishuAutoAllowed(config, 'ou_allow')).toBe(true);
+ expect(approveFeishuPendingRequest(config, 'ou_1', 'admin')?.openId).toBe('ou_1');
+ expect(approveFeishuPendingRequest(config, 'ou_2', 'member')?.openId).toBe('ou_2');
+
+ expect(promoteFeishuUserToAdmin(config, 'ou_2')?.openId).toBe('ou_2');
+ expect(demoteFeishuAdmin(config, 'ou_1')?.openId).toBe('ou_1');
+ expect(rejectFeishuPendingRequest(config, 'ou_missing')).toBeNull();
+ expect(removeFeishuUser(config, 'ou_2')?.openId).toBe('ou_2');
+
+ clearFeishuAccess(config);
+ expect(getFeishuAccessSummary(config)).toBe('0 admins, 0 members, 0 pending');
+ });
+});
diff --git a/src/utils/provider-models.ts b/src/utils/provider-models.ts
index 8a4df08..5439671 100644
--- a/src/utils/provider-models.ts
+++ b/src/utils/provider-models.ts
@@ -71,6 +71,8 @@ const MIMO_TOKEN_PLAN_PREFERRED_MODELS = MIMO_PREFERRED_MODELS;
const OPENAI_COMPAT_PREFERRED_MODELS = [] as const;
+const MINIMAX_PREFERRED_MODELS = [] as const;
+
export class ProviderModelFetchError extends Error {
constructor(message: string) {
super(message);
@@ -177,6 +179,7 @@ function chooseRecommendedModel(
openaiCompat: OPENAI_COMPAT_PREFERRED_MODELS,
mimo: MIMO_PREFERRED_MODELS,
mimoTokenPlan: MIMO_TOKEN_PLAN_PREFERRED_MODELS,
+ minimax: MINIMAX_PREFERRED_MODELS,
};
for (const candidate of preferredByProvider[provider]) {
@@ -213,6 +216,7 @@ export function buildModelCatalog(
openaiCompat: OPENAI_COMPAT_PREFERRED_MODELS,
mimo: MIMO_PREFERRED_MODELS,
mimoTokenPlan: MIMO_TOKEN_PLAN_PREFERRED_MODELS,
+ minimax: MINIMAX_PREFERRED_MODELS,
};
const withoutRecommended = filtered.filter((model) => model !== recommendedModel);
@@ -374,6 +378,25 @@ async function fetchMiMoTokenPlanModels(config: ProviderConfig): Promise {
+ const data = await fetchJson(
+ 'https://api.minimaxi.com/anthropic/v1/models',
+ {
+ headers: {
+ 'x-api-key': config.apiKey,
+ 'anthropic-version': '2023-06-01',
+ },
+ },
+ 'Mercury could not fetch models for this MiniMax key. Please re-enter it.',
+ );
+
+ const ids = (data.data ?? [])
+ .map((model) => model.id?.trim() ?? '')
+ .filter((id) => id.startsWith('MiniMax-'));
+
+ return buildModelCatalog('minimax', ids, config.model);
+}
+
export async function fetchProviderModelCatalog(
provider: ProviderName,
config: ProviderConfig,
@@ -406,5 +429,9 @@ export async function fetchProviderModelCatalog(
return fetchMiMoTokenPlanModels(config);
}
+ if (provider === 'minimax') {
+ return fetchMiniMaxModels(config);
+ }
+
return fetchOpenAICompatModels(provider, config);
}