Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 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
4f14b54
fix: route IPC sendImage through channel-agnostic handler
Fanglinqiang Mar 19, 2026
9329109
wip: stage feature branch changes before upstream merge
Fanglinqiang Mar 23, 2026
74e199b
chore: untrack container/skills (now a symlink to Library)
Fanglinqiang Mar 23, 2026
e76c739
chore: untrack groups symlink files
Fanglinqiang Mar 23, 2026
64471f9
merge: sync upstream/main — WeChat channel, omics runtime pack, trace…
Fanglinqiang Mar 23, 2026
927befe
chore: npm install — add @slack/bolt, weixin-agent-sdk dependencies
Fanglinqiang Mar 23, 2026
a591c99
fix: dashboard/chat multi-model token tracking and group routing
Fanglinqiang Mar 29, 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
17 changes: 17 additions & 0 deletions .claude/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"version": "0.0.1",
"configurations": [
{
"name": "bioclaw-dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
"port": 3847
},
{
"name": "dashboard",
"runtimeExecutable": "tail",
"runtimeArgs": ["-f", "/dev/null"],
"port": 3847
}
]
}
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
6 changes: 6 additions & 0 deletions container/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,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
143 changes: 138 additions & 5 deletions container/agent-runner/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface ContainerInput {
chatJid: string;
isMain: boolean;
isScheduledTask?: boolean;
agentType?: 'claude' | 'minimax' | 'qwen';
secrets?: Record<string, string>;
}

Expand All @@ -37,6 +38,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 +169,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 @@ -212,6 +241,25 @@ function queueIpcImage(chatJid: string, groupFolder: string, filePath: string, c
return filename;
}

function queueIpcFile(chatJid: string, groupFolder: string, filePath: string): string {
const ext = path.extname(filePath) || '';
fs.mkdirSync(IPC_FILES_DIR, { recursive: true });
const filename = `${Date.now()}-${Math.random().toString(36).slice(2, 8)}${ext}`;
const destPath = path.join(IPC_FILES_DIR, filename);
fs.copyFileSync(filePath, destPath);

writeIpcFile(IPC_MESSAGES_DIR, {
type: 'file',
chatJid,
filePath: `files/${filename}`,
originalName: path.basename(filePath),
groupFolder,
timestamp: new Date().toISOString(),
});

return filename;
}

function queueScheduledTask(
containerInput: ContainerInput,
args: { prompt: string; schedule_type: string; schedule_value: string; context_mode?: string; target_group_jid?: string },
Expand All @@ -228,7 +276,25 @@ function queueScheduledTask(
});
}

function resolveProviderConfig(env: Record<string, string | undefined>): ProviderConfig {
function resolveProviderConfig(env: Record<string, string | undefined>, agentType?: string): ProviderConfig {
// Agent-type-specific providers take priority
if (agentType === 'minimax' && env.MINIMAX_API_KEY) {
return {
provider: 'openai-compatible',
apiKey: env.MINIMAX_API_KEY,
baseUrl: env.MINIMAX_BASE_URL || 'https://api.minimax.chat/v1',
model: env.MINIMAX_MODEL || 'MiniMax-M2.5',
};
}
if (agentType === 'qwen' && env.QWEN_AUTH_TOKEN && env.QWEN_API_BASE) {
return {
provider: 'openai-compatible',
apiKey: env.QWEN_AUTH_TOKEN,
baseUrl: env.QWEN_API_BASE,
model: env.QWEN_MODEL || 'qwen-plus',
};
}

const requestedProvider = (env.MODEL_PROVIDER || '').trim().toLowerCase();
const openRouterKey = env.OPENROUTER_API_KEY;
const openCompatibleKey = env.OPENAI_COMPATIBLE_API_KEY;
Expand Down Expand Up @@ -265,6 +331,7 @@ function getBioSystemPrompt(): string {
'Use tools to produce real results. Prefer the Bash tool for running shell commands, Python scripts, and bioinformatics workflows.',
'Save output files to /workspace/group/ so users can access them.',
'When you generate an image file (such as PNG, JPG, or GIF), call the send_image tool so the user receives it in chat instead of only seeing a saved file path.',
'When you generate non-image files (PDF, CSV, MD, DOCX, XLSX, etc.) that users need, call the send_file tool to deliver them in chat.',
"If you generate plots with Chinese labels via matplotlib, configure a Chinese-capable font first (try: 'Noto Sans CJK SC' or 'WenQuanYi Zen Hei') and set axes.unicode_minus=False to avoid missing glyphs and minus-sign issues.",
'Prioritize figures that look scientific, readable on a phone screen, and suitable for demos or slide decks.',
'Avoid overcrowded labels, tiny fonts, excessive legends, rainbow color noise, and default low-quality plotting styles.',
Expand Down Expand Up @@ -615,6 +682,7 @@ async function runQuery(
// BioClaw: inject biology-specific system context
const bioSystemPrompt = getBioSystemPrompt()
.replace('send_image tool', 'mcp__bioclaw__send_image')
.replace('send_file tool', 'mcp__bioclaw__send_file')
.replace('Use tools to produce real results. Prefer the Bash tool for running shell commands, Python scripts, and bioinformatics workflows.', 'Write and execute Python scripts or bash commands to produce real results.');

// Load global CLAUDE.md as additional system context (shared across all groups)
Expand Down Expand Up @@ -716,6 +784,38 @@ async function runQuery(
} catch { /* don't break the loop */ }
}

// 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 @@ -729,11 +829,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 @@ -791,6 +903,21 @@ function getOpenAICompatibleTools() {
},
},
},
{
type: 'function',
function: {
name: 'send_file',
description: 'Send any file (PDF, CSV, MD, DOCX, etc.) from the container to the current chat.',
parameters: {
type: 'object',
properties: {
file_path: { type: 'string', description: 'Absolute file path inside the container.' },
},
required: ['file_path'],
additionalProperties: false,
},
},
},
{
type: 'function',
function: {
Expand Down Expand Up @@ -870,6 +997,11 @@ async function executeOpenAIToolCall(
if (!fs.existsSync(args.file_path)) return `File not found: ${args.file_path}`;
queueIpcImage(containerInput.chatJid, containerInput.groupFolder, args.file_path, args.caption);
return `Image queued for sending from ${args.file_path}`;
case 'send_file':
if (!args.file_path) return 'Missing required argument: file_path';
if (!fs.existsSync(args.file_path)) return `File not found: ${args.file_path}`;
queueIpcFile(containerInput.chatJid, containerInput.groupFolder, args.file_path);
return `File queued for sending: ${path.basename(args.file_path)}`;
case 'schedule_task':
if (!args.prompt || !args.schedule_type || !args.schedule_value) {
return 'Missing one of required arguments: prompt, schedule_type, schedule_value';
Expand Down Expand Up @@ -1013,8 +1145,8 @@ async function main(): Promise<void> {
for (const [key, value] of Object.entries(containerInput.secrets || {})) {
sdkEnv[key] = value;
}
const providerConfig = resolveProviderConfig(sdkEnv);
log(`Using provider: ${providerConfig.provider}${providerConfig.model ? ` (${providerConfig.model})` : ''}`);
const providerConfig = resolveProviderConfig(sdkEnv, containerInput.agentType);
log(`Using provider: ${providerConfig.provider}${providerConfig.model ? ` (${providerConfig.model})` : ''} (agentType: ${containerInput.agentType || 'claude'})`);

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const mcpServerPath = path.join(__dirname, 'ipc-mcp-stdio.js');
Expand Down Expand Up @@ -1110,6 +1242,7 @@ async function main(): Promise<void> {
});
process.exit(1);
}

}

main();
File renamed without changes.
Loading