Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
38e2e27
feat: add web dashboard, Telegram channel, and MiniMax/Qwen support
Fanglinqiang Mar 15, 2026
84bdefa
remove start.sh: contains local paths, not suitable for public repo
Fanglinqiang Mar 15, 2026
20ec825
gitignore: exclude local start.sh
Fanglinqiang Mar 15, 2026
a64d0f3
docs: fix Tasks tab description in dashboard section
Fanglinqiang Mar 15, 2026
8525089
feat: add bio-research-pipeline skill for hypothesis generation
Fanglinqiang Mar 15, 2026
3b5ab52
feat: bio-research-pipeline uses Claude + MiniMax + Qwen in parallel
Fanglinqiang Mar 15, 2026
3e9ab75
fix: bio-research-pipeline skill detection for Chinese input
Fanglinqiang Mar 15, 2026
bd8b38c
feat: dashboard chat supports all agent types via container runner
Fanglinqiang Mar 17, 2026
8aedd13
fix: prevent duplicate text in dashboard chat responses
Fanglinqiang Mar 17, 2026
19f15ba
feat: add WeCom channel support and fix dashboard chat dedup
Fanglinqiang Mar 17, 2026
1044838
feat: Feishu channel with auto-registration, image/file download, mul…
Fanglinqiang Mar 18, 2026
10625ae
wip: stage remaining modified files before upstream merge
Fanglinqiang Mar 18, 2026
d6f9a05
merge: sync upstream/main — OpenRouter, memory improvements, cnsplots…
Fanglinqiang Mar 18, 2026
409c54e
feat: Feishu channel, dashboard chat fixes, scheduler dedup
Fanglinqiang Mar 19, 2026
ca83883
feat: add 50+ bioinformatics Claude skills for container agents
Fanglinqiang Mar 19, 2026
5f229d8
Merge pull request #1 from Fanglinqiang/feat/dashboard-chat-multimodel
Fanglinqiang Mar 19, 2026
55c7fdc
feat: route foreign group responses back to local-web chat
Fanglinqiang Mar 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "bioclaw-dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 3847
}
]
}
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ WECOM_SECRET=your-secret
# WECOM_AGENT_ID=your-agent-id
# WECOM_CORP_SECRET=your-corp-secret

# ─── Feishu / Lark (optional) ─────────────────
# Create a self-built app at https://open.feishu.cn/
# Enable: Bot capability, im:message, im:message:send_as_bot, im:resource
# Under Events & Callbacks → choose "Long connection" (长连接)
FEISHU_APP_ID=cli_your_app_id
FEISHU_APP_SECRET=your_app_secret

# ─── Discord — Optional ──────────────────────

# To enable Discord bot:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ groups/global/*
*.keys.json
.env

# Local startup scripts (contain machine-specific paths)
start.sh

# OS
.DS_Store

Expand Down
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,46 @@ WECOM_CORP_SECRET=your-corp-secret
```
The server IP must be added to the app's trusted IP whitelist.

### Feishu / Lark (飞书)

1. Go to the [Feishu Open Platform](https://open.feishu.cn/) and create a **self-built app** (企业自建应用)
2. Enable **Bot** capability under **Add Capabilities**
3. Under **Permissions & Scopes**, grant:
- `im:message` — Receive messages
- `im:message:send_as_bot` — Send messages as bot
- `im:resource` — Download images/files from messages
- `im:message.group_msg` — Receive group messages (if using in groups)
4. Under **Events & Callbacks**, select **Long Connection** (长连接) mode
5. Subscribe to event: `im.message.receive_v1`
6. Copy the **App ID** and **App Secret**, add to `.env`:
```
FEISHU_APP_ID=cli_your_app_id
FEISHU_APP_SECRET=your_app_secret
```
7. Publish the app version and have the admin approve it
8. Add the bot to a group or send it a direct message to start chatting

**Auto-registration:** New chats are automatically registered — no manual setup needed. By default, they use the `main` group folder. Override with:
```
FEISHU_DEFAULT_FOLDER=my-folder
```

**Multi-bot support:** Up to 3 Feishu bots can run simultaneously (e.g., different agents for different groups):
```
FEISHU2_APP_ID=cli_second_app_id
FEISHU2_APP_SECRET=second_app_secret
FEISHU2_DEFAULT_FOLDER=literature

FEISHU3_APP_ID=cli_third_app_id
FEISHU3_APP_SECRET=third_app_secret
FEISHU3_DEFAULT_FOLDER=qwen-agent
```

<p align="center">
<img src="docs/images/lark/lark-deepseek-1.jpg" width="45%" alt="Feishu Chat Example 1"/>
<img src="docs/images/lark/lark-deepseek-2.jpg" width="45%" alt="Feishu Chat Example 2"/>
</p>

### Discord

1. Go to the [Discord Developer Portal](https://discord.com/developers/applications)
Expand Down
39 changes: 37 additions & 2 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ BioClaw 将常见的生物信息学任务带到聊天界面中。研究者可以

## 快速开始

> 说明:当前仓库中已经实现的消息通道是 WhatsApp。文档中的 QQ / 飞书截图展示的是扩展方向,不代表仓库里已经内置了可直接运行的 QQ / 飞书通道
> 说明:当前仓库中已实现的消息通道包括 WhatsApp、WeCom(企业微信)、飞书(Lark)和 Discord。QQ 通道展示的是扩展方向,尚未内置

> 现在也支持一个更适合 Windows 用户的本地网页聊天入口。若你在中国、或者暂时不想接 WhatsApp,可直接走 `HTTP webhook + 本地网页聊天`。

Expand Down Expand Up @@ -147,6 +147,41 @@ WECOM_CORP_SECRET=应用Secret
```
服务器 IP 需加入应用的企业可信 IP 白名单。

### 飞书(Lark)

1. 前往 [飞书开放平台](https://open.feishu.cn/) 创建 **企业自建应用**
2. 在 **添加应用能力** 中启用 **机器人**
3. 在 **权限管理** 中开通以下权限:
- `im:message` — 接收消息
- `im:message:send_as_bot` — 以机器人身份发送消息
- `im:resource` — 下载消息中的图片和文件
- `im:message.group_msg` — 接收群聊消息
4. 在 **事件与回调** 中选择 **长连接** 模式
5. 订阅事件:`im.message.receive_v1`
6. 复制 **App ID** 和 **App Secret**,添加到 `.env`:
```
FEISHU_APP_ID=cli_your_app_id
FEISHU_APP_SECRET=your_app_secret
```
7. 发布应用版本并通过管理员审批
8. 将机器人添加到群聊或直接发送私聊消息即可开始对话

**自动注册:** 新对话会自动注册,无需手动配置。默认使用 `main` 文件夹,可通过以下配置覆盖:
```
FEISHU_DEFAULT_FOLDER=my-folder
```

**多机器人支持:** 最多可同时运行 3 个飞书机器人(例如不同群使用不同 agent):
```
FEISHU2_APP_ID=cli_second_app_id
FEISHU2_APP_SECRET=second_app_secret
FEISHU2_DEFAULT_FOLDER=literature

FEISHU3_APP_ID=cli_third_app_id
FEISHU3_APP_SECRET=third_app_secret
FEISHU3_DEFAULT_FOLDER=qwen-agent
```

### Discord

1. 打开 [Discord Developer Portal](https://discord.com/developers/applications)
Expand Down Expand Up @@ -195,7 +230,7 @@ install https://github.com/Runchuan-BU/BioClaw

更多任务示例见 [ExampleTask/ExampleTask.md](ExampleTask/ExampleTask.md)。

> 注意:上面的 QQ / 飞书图片目前是产品展示示例,不是仓库内现成可启用的接入实现
> 注意:QQ 通道目前是展示示例,尚未内置实现。飞书通道已内置支持,配置 `.env` 即可使用

## 系统架构

Expand Down
6 changes: 6 additions & 0 deletions container/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,12 @@ RUN pip3 install --no-cache-dir --break-system-packages \
cnsplots \
pyGenomeTracks

# Install AutoResearchClaw and LiteLLM (OpenAI-compat proxy for Anthropic API)
RUN pip3 install --no-cache-dir --break-system-packages \
litellm \
pyyaml \
git+https://github.com/aiming-lab/AutoResearchClaw.git

# Install PyMOL (headless) via apt
RUN apt-get update && apt-get install -y \
pymol \
Expand Down
77 changes: 75 additions & 2 deletions container/agent-runner/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ interface ContainerOutput {
result: string | null;
newSessionId?: string;
error?: string;
usage?: TokenUsageSummary;
}

interface TokenUsageSummary {
input_tokens: number;
output_tokens: number;
cache_read_tokens: number;
cache_creation_tokens: number;
cost_usd: number;
duration_ms: number;
num_turns: number;
}

interface SessionEntry {
Expand Down Expand Up @@ -157,13 +168,30 @@ async function readStdin(): Promise<string> {

const OUTPUT_START_MARKER = '---BIOCLAW_OUTPUT_START---';
const OUTPUT_END_MARKER = '---BIOCLAW_OUTPUT_END---';
const EVENT_START_MARKER = '---BIOCLAW_EVENT_START---';
const EVENT_END_MARKER = '---BIOCLAW_EVENT_END---';

interface ContainerEvent {
type: 'tool_call' | 'tool_result' | 'text';
id?: string;
tool?: string;
input?: Record<string, unknown>;
output?: string;
text?: string;
}

function writeOutput(output: ContainerOutput): void {
console.log(OUTPUT_START_MARKER);
console.log(JSON.stringify(output));
console.log(OUTPUT_END_MARKER);
}

function writeEvent(event: ContainerEvent): void {
console.log(EVENT_START_MARKER);
console.log(JSON.stringify(event));
console.log(EVENT_END_MARKER);
}

function log(message: string): void {
console.error(`[agent-runner] ${message}`);
}
Expand Down Expand Up @@ -680,6 +708,38 @@ async function runQuery(
lastAssistantUuid = (message as { uuid: string }).uuid;
}

// Emit events for display in dashboard chat
if (message.type === 'assistant') {
const content = (message as any).message?.content;
if (Array.isArray(content)) {
for (const block of content) {
if (block.type === 'tool_use') {
writeEvent({ type: 'tool_call', id: block.id, tool: block.name, input: block.input });
} else if (block.type === 'text' && block.text && !block.text.includes('No response requested')) {
writeEvent({ type: 'text', text: block.text });
}
}
}
}

// Emit tool result events
if (message.type === 'user') {
const content = (message as any).message?.content;
if (Array.isArray(content)) {
for (const block of content) {
if (block.type === 'tool_result') {
const rawOutput = block.content;
const outputText = typeof rawOutput === 'string'
? rawOutput
: Array.isArray(rawOutput)
? rawOutput.map((c: any) => c.text || '').join('')
: JSON.stringify(rawOutput);
writeEvent({ type: 'tool_result', id: block.tool_use_id, output: (outputText || '').slice(0, 3000) });
}
}
}
}

if (message.type === 'system' && message.subtype === 'init') {
newSessionId = message.session_id;
log(`Session initialized: ${newSessionId}`);
Expand All @@ -693,11 +753,23 @@ async function runQuery(
if (message.type === 'result') {
resultCount++;
const textResult = 'result' in message ? (message as { result?: string }).result : null;
log(`Result #${resultCount}: subtype=${message.subtype}${textResult ? ` text=${textResult.slice(0, 200)}` : ''}`);
const resultMsg = message as any;
// Build per-result usage from this result message
const resultUsage: TokenUsageSummary = {
input_tokens: resultMsg.usage?.input_tokens || 0,
output_tokens: resultMsg.usage?.output_tokens || 0,
cache_read_tokens: resultMsg.usage?.cache_read_input_tokens || 0,
cache_creation_tokens: resultMsg.usage?.cache_creation_input_tokens || 0,
cost_usd: resultMsg.total_cost_usd || 0,
duration_ms: resultMsg.duration_ms || 0,
num_turns: resultMsg.num_turns || 0,
};
log(`Result #${resultCount}: subtype=${message.subtype} tokens=${resultUsage.input_tokens}/${resultUsage.output_tokens} cost=$${resultUsage.cost_usd.toFixed(4)}${textResult ? ` text=${textResult.slice(0, 200)}` : ''}`);
writeOutput({
status: 'success',
result: textResult || null,
newSessionId
newSessionId,
usage: (resultUsage.input_tokens > 0 || resultUsage.output_tokens > 0) ? resultUsage : undefined,
});
}
}
Expand Down Expand Up @@ -1060,6 +1132,7 @@ async function main(): Promise<void> {
});
process.exit(1);
}

}

main();
Loading