nanoclaw init
Some checks failed
Sync upstream & merge-forward skill branches / sync-and-merge (push) Has been cancelled
Merge-forward skill branches / merge-forward (push) Has been cancelled
Bump version / bump-version (push) Has been cancelled
Update token count / update-tokens (push) Has been cancelled

This commit is contained in:
woozu-shin
2026-03-24 01:43:09 +09:00
parent a1b8c70b5c
commit 4dc270d2dd
10 changed files with 1541 additions and 30 deletions

11
.env Normal file
View File

@@ -0,0 +1,11 @@
ANTHROPIC_BASE_URL=https://api.minimax.io/anthropic
ANTHROPIC_API_KEY=sk-cp-EKy8NUGccnm1P9t2FE1DWBPgaEz8L-P5E_IS0PZpD1bcayv1XKBc5q9W5_ISUSY3dVIEtXAzXfNXVAy_hg4_4I4qJxlR_dyw9gf0KcViIiwCVMXt16pXI_Q
API_TIMEOUT_MS=3000000
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
ANTHROPIC_MODEL=MiniMax-M2.7
ANTHROPIC_SMALL_FAST_MODEL=MiniMax-M2.7
ANTHROPIC_DEFAULT_SONNET_MODEL=MiniMax-M2.7
ANTHROPIC_DEFAULT_OPUS_MODEL=MiniMax-M2.7
ANTHROPIC_DEFAULT_HAIKU_MODEL=MiniMax-M2.7
TELEGRAM_BOT_TOKEN=8113932362:AAGTKSNbeLwWGs3hRuZR6mdHx71snOtXrrw
TELEGRAM_BOT_POOL=8491818721:AAHITsOQsD-Ict61aFby9ya3Kvt7wZXrWVg,8280977567:AAHPS8CqdZkYCZJKj4e9ovtG1cRyuNY2tH0

4
.gitignore vendored
View File

@@ -18,10 +18,6 @@ groups/global/*
!groups/main/CLAUDE.md
!groups/global/CLAUDE.md
# Secrets
*.keys.json
.env
# Temp files
.tmp-*

1355
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,6 +12,98 @@ import {
RegisteredGroup,
} from '../types.js';
// Bot pool for agent teams: send-only Api instances (no polling)
const poolApis: Api[] = [];
// Maps "{groupFolder}:{senderName}" → pool Api index for stable assignment
const senderBotMap = new Map<string, number>();
let nextPoolIndex = 0;
/**
* Initialize send-only Api instances for the bot pool.
* Each pool bot can send messages but doesn't poll for updates.
*/
export async function initBotPool(tokens: string[]): Promise<void> {
for (const token of tokens) {
try {
const api = new Api(token);
const me = await api.getMe();
poolApis.push(api);
logger.info(
{ username: me.username, id: me.id, poolSize: poolApis.length },
'Pool bot initialized',
);
} catch (err) {
logger.error({ err }, 'Failed to initialize pool bot');
}
}
if (poolApis.length > 0) {
logger.info({ count: poolApis.length }, 'Telegram bot pool ready');
}
}
/**
* Send a message via a pool bot assigned to the given sender name.
* Assigns bots round-robin on first use; subsequent messages from the
* same sender in the same group always use the same bot.
* On first assignment, renames the bot to match the sender's role.
*/
export async function sendPoolMessage(
chatId: string,
text: string,
sender: string,
groupFolder: string,
): Promise<void> {
if (poolApis.length === 0) {
// No pool bots — fall back to main bot sendMessage path
return;
}
const key = `${groupFolder}:${sender}`;
let idx = senderBotMap.get(key);
if (idx === undefined) {
idx = nextPoolIndex % poolApis.length;
nextPoolIndex++;
senderBotMap.set(key, idx);
// Rename the bot to match the sender's role, then wait for Telegram to propagate
try {
await poolApis[idx].setMyName(sender);
await new Promise((r) => setTimeout(r, 2000));
logger.info(
{ sender, groupFolder, poolIndex: idx },
'Assigned and renamed pool bot',
);
} catch (err) {
logger.warn(
{ sender, err },
'Failed to rename pool bot (sending anyway)',
);
}
}
const api = poolApis[idx];
try {
const numericId = chatId.replace(/^tg:/, '');
const MAX_LENGTH = 4096;
if (text.length <= MAX_LENGTH) {
await sendTelegramMessage(api, numericId, text);
} else {
for (let i = 0; i < text.length; i += MAX_LENGTH) {
await sendTelegramMessage(
api,
numericId,
text.slice(i, i + MAX_LENGTH),
);
}
}
logger.info(
{ chatId, sender, poolIndex: idx, length: text.length },
'Pool message sent',
);
} catch (err) {
logger.error({ chatId, sender, err }, 'Failed to send pool message');
}
}
export interface TelegramChannelOpts {
onMessage: OnInboundMessage;
onChatMetadata: OnChatMetadata;

View File

@@ -71,3 +71,13 @@ export const TRIGGER_PATTERN = new RegExp(
// Uses system timezone by default
export const TIMEZONE =
process.env.TZ || Intl.DateTimeFormat().resolvedOptions().timeZone;
const telegramPoolEnv = readEnvFile(['TELEGRAM_BOT_POOL']);
export const TELEGRAM_BOT_POOL = (
process.env.TELEGRAM_BOT_POOL ||
telegramPoolEnv.TELEGRAM_BOT_POOL ||
''
)
.split(',')
.map((t) => t.trim())
.filter(Boolean);

View File

@@ -26,6 +26,7 @@ import {
stopContainer,
} from './container-runtime.js';
import { detectAuthMode } from './credential-proxy.js';
import { readEnvFile } from './env.js';
import { validateAdditionalMounts } from './mount-security.js';
import { RegisteredGroup } from './types.js';
@@ -123,12 +124,17 @@ function buildVolumeMounts(
);
fs.mkdirSync(groupSessionsDir, { recursive: true });
const settingsFile = path.join(groupSessionsDir, 'settings.json');
if (!fs.existsSync(settingsFile)) {
fs.writeFileSync(
settingsFile,
JSON.stringify(
{
env: {
// Read model/provider env vars from .env to forward into container settings
const providerEnv = readEnvFile([
'ANTHROPIC_MODEL',
'ANTHROPIC_SMALL_FAST_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
'API_TIMEOUT_MS',
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
]);
const envBlock: Record<string, string> = {
// Enable agent swarms (subagent orchestration)
// https://code.claude.com/docs/en/agent-teams#orchestrate-teams-of-claude-code-sessions
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
@@ -138,13 +144,17 @@ function buildVolumeMounts(
// Enable Claude's memory feature (persists user preferences between sessions)
// https://code.claude.com/docs/en/memory#manage-auto-memory
CLAUDE_CODE_DISABLE_AUTO_MEMORY: '0',
},
},
null,
2,
) + '\n',
);
};
// Merge provider env vars (from .env or process.env)
for (const [key, val] of Object.entries(providerEnv)) {
const effective = process.env[key] || val;
if (effective) envBlock[key] = effective;
}
// Always write settings to pick up env changes (model, provider, etc.)
fs.writeFileSync(
settingsFile,
JSON.stringify({ env: envBlock }, null, 2) + '\n',
);
// Sync skills from container/skills/ into each group's .claude/skills/
const skillsSrc = path.join(process.cwd(), 'container', 'skills');
@@ -238,6 +248,23 @@ function buildContainerArgs(
args.push('-e', 'CLAUDE_CODE_OAUTH_TOKEN=placeholder');
}
// Forward model and SDK configuration from .env to the container.
// These allow custom API providers (e.g. MiniMax) to override model names.
const MODEL_ENV_KEYS = [
'API_TIMEOUT_MS',
'CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC',
'ANTHROPIC_MODEL',
'ANTHROPIC_SMALL_FAST_MODEL',
'ANTHROPIC_DEFAULT_SONNET_MODEL',
'ANTHROPIC_DEFAULT_OPUS_MODEL',
'ANTHROPIC_DEFAULT_HAIKU_MODEL',
] as const;
const modelEnv = readEnvFile([...MODEL_ENV_KEYS]);
for (const key of MODEL_ENV_KEYS) {
const val = process.env[key] || modelEnv[key];
if (val) args.push('-e', `${key}=${val}`);
}
// Runtime-specific args for host gateway resolution
args.push(...hostGatewayArgs());

View File

@@ -41,6 +41,9 @@ export function startCredentialProxy(
const upstreamUrl = new URL(
secrets.ANTHROPIC_BASE_URL || 'https://api.anthropic.com',
);
// Preserve the base URL's pathname prefix (e.g. /anthropic for MiniMax)
// so requests to /v1/messages become /anthropic/v1/messages upstream.
const basePath = upstreamUrl.pathname.replace(/\/+$/, '');
const isHttps = upstreamUrl.protocol === 'https:';
const makeRequest = isHttps ? httpsRequest : httpRequest;
@@ -83,7 +86,7 @@ export function startCredentialProxy(
{
hostname: upstreamUrl.hostname,
port: upstreamUrl.port || (isHttps ? 443 : 80),
path: req.url,
path: basePath + req.url,
method: req.method,
headers,
} as RequestOptions,

View File

@@ -6,6 +6,7 @@ import {
CREDENTIAL_PROXY_PORT,
IDLE_TIMEOUT,
POLL_INTERVAL,
TELEGRAM_BOT_POOL,
TIMEZONE,
TRIGGER_PATTERN,
} from './config.js';
@@ -43,6 +44,7 @@ import {
} from './db.js';
import { GroupQueue } from './group-queue.js';
import { resolveGroupFolderPath } from './group-folder.js';
import { initBotPool } from './channels/telegram.js';
import { startIpcWatcher } from './ipc.js';
import { findChannel, formatMessages, formatOutbound } from './router.js';
import {
@@ -596,6 +598,11 @@ async function main(): Promise<void> {
process.exit(1);
}
// Initialize Telegram bot pool for agent teams (swarm)
if (TELEGRAM_BOT_POOL.length > 0) {
await initBotPool(TELEGRAM_BOT_POOL);
}
// Start subsystems (independently of connection handler)
startSchedulerLoop({
registeredGroups: () => registeredGroups,

View File

@@ -4,6 +4,7 @@ import path from 'path';
import { CronExpressionParser } from 'cron-parser';
import { DATA_DIR, IPC_POLL_INTERVAL, TIMEZONE } from './config.js';
import { sendPoolMessage } from './channels/telegram.js';
import { AvailableGroup } from './container-runner.js';
import { createTask, deleteTask, getTaskById, updateTask } from './db.js';
import { isValidGroupFolder } from './group-folder.js';
@@ -81,9 +82,18 @@ export function startIpcWatcher(deps: IpcDeps): void {
isMain ||
(targetGroup && targetGroup.folder === sourceGroup)
) {
if (data.sender && data.chatJid.startsWith('tg:')) {
await sendPoolMessage(
data.chatJid,
data.text,
data.sender,
sourceGroup,
);
} else {
await deps.sendMessage(data.chatJid, data.text);
}
logger.info(
{ chatJid: data.chatJid, sourceGroup },
{ chatJid: data.chatJid, sourceGroup, sender: data.sender },
'IPC message sent',
);
} else {

4
start.sh Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/bash
set -e
cd "$(dirname "${BASH_SOURCE[0]}")"
npm run build && npm start