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
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:
11
.env
Normal file
11
.env
Normal 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
4
.gitignore
vendored
@@ -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
1355
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,28 +124,37 @@ function buildVolumeMounts(
|
||||
);
|
||||
fs.mkdirSync(groupSessionsDir, { recursive: true });
|
||||
const settingsFile = path.join(groupSessionsDir, 'settings.json');
|
||||
if (!fs.existsSync(settingsFile)) {
|
||||
fs.writeFileSync(
|
||||
settingsFile,
|
||||
JSON.stringify(
|
||||
{
|
||||
env: {
|
||||
// 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',
|
||||
// Load CLAUDE.md from additional mounted directories
|
||||
// https://code.claude.com/docs/en/memory#load-memory-from-additional-directories
|
||||
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1',
|
||||
// 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',
|
||||
);
|
||||
// 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',
|
||||
// Load CLAUDE.md from additional mounted directories
|
||||
// https://code.claude.com/docs/en/memory#load-memory-from-additional-directories
|
||||
CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD: '1',
|
||||
// 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',
|
||||
};
|
||||
// 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());
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
14
src/ipc.ts
14
src/ipc.ts
@@ -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)
|
||||
) {
|
||||
await deps.sendMessage(data.chatJid, data.text);
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user