feat(telegram): add AI agent bot with streaming and account context

- Add Telegram bot with long-polling and AI agent loop (api_call tool)
- SSE streaming with real-time message editing and  placeholder
- Account state injection at conversation start (models, exchanges,
  strategies, traders, per-trader PnL and statistics)
- Lane semaphore per chat serializes concurrent messages (60s timeout)
- Idle timeout watchdog (60s) prevents hung streaming connections
- Look-ahead buffer prevents partial <api_call> tag leaking to user
- Fix PUT /strategies/:id to merge config (read-then-merge pattern)
- Add route registry with full API schema for LLM documentation
- Add TelegramConfig store and Web UI config modal
- Add GetAnyEnabled to AIModel store for bot LLM client selection
This commit is contained in:
tinkle-community
2026-03-08 00:19:38 +08:00
parent 73f1fe105d
commit 3168a18c0d
28 changed files with 4673 additions and 118 deletions

View File

@@ -16,6 +16,7 @@ import { getModelIcon } from './ModelIcons'
import { TraderConfigModal } from './TraderConfigModal'
import { DeepVoidBackground } from './DeepVoidBackground'
import { ExchangeConfigModal } from './traders/ExchangeConfigModal'
import { TelegramConfigModal } from './traders/TelegramConfigModal'
import { PunkAvatar, getTraderAvatar } from './PunkAvatar'
import {
Bot,
@@ -31,6 +32,7 @@ import {
ExternalLink,
Copy,
Check,
MessageCircle,
} from 'lucide-react'
import { confirmToast } from '../lib/notify'
import { toast } from 'sonner'
@@ -148,6 +150,7 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
const [showEditModal, setShowEditModal] = useState(false)
const [showModelModal, setShowModelModal] = useState(false)
const [showExchangeModal, setShowExchangeModal] = useState(false)
const [showTelegramModal, setShowTelegramModal] = useState(false)
const [editingModel, setEditingModel] = useState<string | null>(null)
const [editingExchange, setEditingExchange] = useState<string | null>(null)
const [editingTrader, setEditingTrader] = useState<any>(null)
@@ -849,6 +852,16 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
</div>
</button>
<button
onClick={() => setShowTelegramModal(true)}
className="px-4 py-2 rounded text-xs font-mono uppercase tracking-wider transition-all border border-sky-900/50 bg-black/20 text-sky-500 hover:text-sky-300 hover:border-sky-700 whitespace-nowrap backdrop-blur-sm"
>
<div className="flex items-center gap-2">
<MessageCircle className="w-3 h-3" />
<span>TELEGRAM_BOT</span>
</div>
</button>
<button
onClick={() => setShowCreateModal(true)}
disabled={configuredModels.length === 0 || configuredExchanges.length === 0}
@@ -1379,6 +1392,14 @@ export function AITradersPage({ onTraderSelect }: AITradersPageProps) {
language={language}
/>
)}
{/* Telegram Bot Modal */}
{showTelegramModal && (
<TelegramConfigModal
onClose={() => setShowTelegramModal(false)}
language={language}
/>
)}
</div>
</DeepVoidBackground>
)

View File

@@ -336,9 +336,9 @@ export function TraderConfigModal({
<option value="">{t('noStrategyManual', language)}</option>
{strategies.map((strategy) => (
<option key={strategy.id} value={strategy.id}>
{selectedStrategy.name}
{selectedStrategy.is_active ? t('active', language) : ''}
{selectedStrategy.is_default ? t('default', language) : ''}
{strategy.name}
{strategy.is_active ? t('strategyActive', language) : ''}
{strategy.is_default ? t('strategyDefault', language) : ''}
</option>
))}
</select>

View File

@@ -0,0 +1,530 @@
import React, { useState, useEffect } from 'react'
import { Check, ChevronLeft, ExternalLink, MessageCircle, Unlink, ArrowRight } from 'lucide-react'
import { toast } from 'sonner'
import { api } from '../../lib/api'
import type { TelegramConfig, AIModel } from '../../types'
import type { Language } from '../../i18n/translations'
// Step indicator (reused pattern from ExchangeConfigModal)
function StepIndicator({ currentStep, labels }: { currentStep: number; labels: string[] }) {
return (
<div className="flex items-center justify-center gap-2 mb-6">
{labels.map((label, index) => (
<React.Fragment key={index}>
<div className="flex items-center gap-2">
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-sm font-bold transition-all"
style={{
background: index < currentStep ? '#0ECB81' : index === currentStep ? '#2AABEE' : '#2B3139',
color: index <= currentStep ? '#000' : '#848E9C',
}}
>
{index < currentStep ? <Check className="w-4 h-4" /> : index + 1}
</div>
<span
className="text-xs font-medium hidden sm:block"
style={{ color: index === currentStep ? '#EAECEF' : '#848E9C' }}
>
{label}
</span>
</div>
{index < labels.length - 1 && (
<div
className="w-8 h-0.5 mx-1"
style={{ background: index < currentStep ? '#0ECB81' : '#2B3139' }}
/>
)}
</React.Fragment>
))}
</div>
)
}
interface TelegramConfigModalProps {
onClose: () => void
language: Language
}
export function TelegramConfigModal({ onClose, language }: TelegramConfigModalProps) {
const [step, setStep] = useState(0)
const [token, setToken] = useState('')
const [selectedModelId, setSelectedModelId] = useState('')
const [isSaving, setIsSaving] = useState(false)
const [config, setConfig] = useState<TelegramConfig | null>(null)
const [models, setModels] = useState<AIModel[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isUnbinding, setIsUnbinding] = useState(false)
const zh = language === 'zh'
// Load current config and available models
useEffect(() => {
Promise.all([
api.getTelegramConfig().catch(() => null),
api.getModelConfigs().catch(() => [] as AIModel[]),
]).then(([cfg, allModels]) => {
const enabledModels = allModels.filter((m) => m.enabled)
setModels(enabledModels)
if (cfg) {
setConfig(cfg)
setSelectedModelId(cfg.model_id ?? '')
if (cfg.is_bound) {
setStep(2)
} else if (cfg.token_masked && cfg.token_masked !== '') {
setStep(1)
}
}
}).finally(() => setIsLoading(false))
}, [])
const handleSaveToken = async () => {
if (!token.trim()) return
if (isSaving) return
// Basic format validation: looks like "123456789:ABCdef..."
if (!/^\d+:[A-Za-z0-9_-]{35,}$/.test(token.trim())) {
toast.error(zh ? 'Bot Token 格式不正确,应为 "数字:字母数字串"' : 'Invalid Bot Token format. Expected "numbers:alphanumeric"')
return
}
setIsSaving(true)
try {
await api.updateTelegramConfig(token.trim(), selectedModelId || undefined)
toast.success(zh ? 'Bot Token 已保存,等待绑定' : 'Bot Token saved, waiting for binding')
const updated = await api.getTelegramConfig()
setConfig(updated)
setToken('')
setStep(1)
} catch (err) {
toast.error(zh ? '保存失败,请检查 Token 是否正确' : 'Save failed, please verify the token')
} finally {
setIsSaving(false)
}
}
const handleUnbind = async () => {
if (isUnbinding) return
setIsUnbinding(true)
try {
await api.unbindTelegram()
toast.success(zh ? '已解绑 Telegram 账号' : 'Telegram account unbound')
const updated = await api.getTelegramConfig()
setConfig(updated)
setStep(updated.token_masked ? 1 : 0)
} catch {
toast.error(zh ? '解绑失败' : 'Unbind failed')
} finally {
setIsUnbinding(false)
}
}
const stepLabels = zh
? ['创建 Bot', '绑定账号', '完成']
: ['Create Bot', 'Bind Account', 'Done']
// Model selector shared between steps
const ModelSelector = () => (
<div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{zh ? '选择 AI 模型(可选)' : 'Select AI Model (optional)'}
</label>
{models.length === 0 ? (
<div
className="px-4 py-3 rounded-xl text-xs"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#848E9C' }}
>
{zh ? '暂无启用的模型请先在「AI 模型」中配置' : 'No enabled models. Configure one in AI Models first.'}
</div>
) : (
<select
value={selectedModelId}
onChange={(e) => setSelectedModelId(e.target.value)}
className="w-full px-4 py-3 rounded-xl text-sm appearance-none"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: selectedModelId ? '#EAECEF' : '#848E9C',
}}
>
<option value="">{zh ? '— 自动选择(推荐)' : '— Auto-select (recommended)'}</option>
{models.map((m) => (
<option key={m.id} value={m.id}>
{m.name} ({m.provider}{m.customModelName ? ` · ${m.customModelName}` : ''})
</option>
))}
</select>
)}
<div className="text-xs" style={{ color: '#848E9C' }}>
{zh
? '不选则自动使用已启用的模型'
: 'Leave blank to auto-use any enabled model'}
</div>
</div>
)
return (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 p-4 overflow-y-auto backdrop-blur-sm">
<div
className="rounded-2xl w-full max-w-lg relative my-8 shadow-2xl"
style={{ background: 'linear-gradient(180deg, #1E2329 0%, #181A20 100%)' }}
>
{/* Header */}
<div className="flex items-center justify-between p-6 pb-2">
<div className="flex items-center gap-3">
{step > 0 && !config?.is_bound && (
<button
type="button"
onClick={() => setStep(step - 1)}
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
>
<ChevronLeft className="w-5 h-5" style={{ color: '#848E9C' }} />
</button>
)}
<div className="flex items-center gap-2">
<MessageCircle className="w-6 h-6" style={{ color: '#2AABEE' }} />
<h3 className="text-xl font-bold" style={{ color: '#EAECEF' }}>
{zh ? 'Telegram Bot 配置' : 'Telegram Bot Setup'}
</h3>
</div>
</div>
<button
type="button"
onClick={onClose}
className="p-2 rounded-lg hover:bg-white/10 transition-colors"
style={{ color: '#848E9C' }}
>
</button>
</div>
{/* Step Indicator */}
<div className="px-6 pt-4">
<StepIndicator currentStep={step} labels={stepLabels} />
</div>
{/* Content */}
<div className="px-6 pb-6 space-y-5">
{isLoading ? (
<div className="text-center py-8 text-zinc-500 text-sm font-mono">
{zh ? '加载中...' : 'Loading...'}
</div>
) : (
<>
{/* Step 0: Create bot via BotFather */}
{step === 0 && (
<div className="space-y-5">
<div
className="p-4 rounded-xl space-y-3"
style={{ background: 'rgba(42, 171, 238, 0.1)', border: '1px solid rgba(42, 171, 238, 0.3)' }}
>
<div className="flex items-start gap-3">
<span className="text-2xl">🤖</span>
<div>
<div className="font-semibold mb-1" style={{ color: '#2AABEE' }}>
{zh ? '第一步:在 Telegram 创建你的 Bot' : 'Step 1: Create your Bot in Telegram'}
</div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div>1. {zh ? '打开 Telegram搜索' : 'Open Telegram, search for'} <code className="text-blue-400">@BotFather</code></div>
<div>2. {zh ? '发送' : 'Send'} <code className="text-blue-400">/newbot</code> {zh ? '命令' : 'command'}</div>
<div>3. {zh ? '按提示输入 Bot 名称和用户名' : 'Follow prompts to set bot name and username'}</div>
<div>4. {zh ? 'BotFather 会返回一个 Token复制它' : 'BotFather will return a Token, copy it'}</div>
</div>
</div>
</div>
</div>
<a
href="https://t.me/BotFather"
target="_blank"
rel="noopener noreferrer"
className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl font-semibold transition-all hover:scale-[1.02]"
style={{ background: '#2AABEE', color: '#000' }}
>
<ExternalLink className="w-4 h-4" />
{zh ? '打开 @BotFather' : 'Open @BotFather'}
</a>
<div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{zh ? '粘贴 Bot Token' : 'Paste Bot Token'}
</label>
<input
type="password"
value={token}
onChange={(e) => setToken(e.target.value)}
placeholder="123456789:ABCdefGHIjklmNOPQRstuvwxYZ"
className="w-full px-4 py-3 rounded-xl font-mono text-sm"
style={{ background: '#0B0E11', border: '1px solid #2B3139', color: '#EAECEF' }}
/>
<div className="text-xs" style={{ color: '#848E9C' }}>
{zh ? 'Token 格式:数字:字母数字串,如 123456789:ABCdef...' : 'Format: numbers:alphanumeric, e.g. 123456789:ABCdef...'}
</div>
</div>
<ModelSelector />
<button
onClick={handleSaveToken}
disabled={isSaving || !token.trim()}
className="w-full flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-50 disabled:cursor-not-allowed"
style={{ background: '#2AABEE', color: '#000' }}
>
{isSaving
? (zh ? '保存中...' : 'Saving...')
: (<>{zh ? '保存并继续' : 'Save & Continue'} <ArrowRight className="w-4 h-4" /></>)
}
</button>
</div>
)}
{/* Step 1: Send /start to activate */}
{step === 1 && (
<div className="space-y-5">
<div
className="p-4 rounded-xl space-y-3"
style={{ background: 'rgba(14, 203, 129, 0.1)', border: '1px solid rgba(14, 203, 129, 0.3)' }}
>
<div className="flex items-start gap-3">
<span className="text-2xl">📱</span>
<div>
<div className="font-semibold mb-1" style={{ color: '#0ECB81' }}>
{zh ? '第二步:向你的 Bot 发送 /start' : 'Step 2: Send /start to your Bot'}
</div>
<div className="text-xs space-y-1" style={{ color: '#848E9C' }}>
<div>1. {zh ? '在 Telegram 中搜索你刚创建的 Bot' : 'Search for your newly created Bot in Telegram'}</div>
<div>2. {zh ? '点击 Start 或发送' : 'Click Start or send'} <code className="text-green-400">/start</code></div>
<div>3. {zh ? 'Bot 会自动绑定到你的账号' : 'Bot will automatically bind to your account'}</div>
</div>
</div>
</div>
</div>
{config?.token_masked && (
<div
className="p-3 rounded-xl flex items-center gap-3"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<div className="w-2 h-2 rounded-full bg-yellow-500 animate-pulse flex-shrink-0" />
<div>
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
{zh ? '当前 Token' : 'Current Token'}
</div>
<div className="text-sm font-mono" style={{ color: '#EAECEF' }}>
{config.token_masked}
</div>
</div>
</div>
)}
<div
className="p-3 rounded-xl text-center"
style={{ background: 'rgba(240, 185, 11, 0.08)', border: '1px solid rgba(240, 185, 11, 0.2)' }}
>
<div className="text-xs" style={{ color: '#F0B90B' }}>
{zh
? '⏳ 等待你发送 /start... 发送后刷新页面查看状态'
: '⏳ Waiting for you to send /start... Refresh page after sending'}
</div>
</div>
<div className="flex gap-3">
<button
onClick={() => { setStep(0); setToken('') }}
className="flex-1 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5"
style={{ background: '#2B3139', color: '#848E9C' }}
>
{zh ? '重新配置 Token' : 'Reconfigure Token'}
</button>
<button
onClick={async () => {
try {
const updated = await api.getTelegramConfig()
setConfig(updated)
if (updated.is_bound) {
setStep(2)
toast.success(zh ? '绑定成功!' : 'Bound successfully!')
} else {
toast.info(zh ? '尚未收到 /start请先向 Bot 发送 /start' : 'No /start received yet. Please send /start to your Bot first')
}
} catch {
toast.error(zh ? '检查失败' : 'Check failed')
}
}}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
style={{ background: '#0ECB81', color: '#000' }}
>
<Check className="w-4 h-4" />
{zh ? '检查绑定状态' : 'Check Status'}
</button>
</div>
</div>
)}
{/* Step 2: Bound & active */}
{step === 2 && (
<div className="space-y-5">
<div
className="p-5 rounded-xl text-center space-y-3"
style={{ background: 'rgba(14, 203, 129, 0.1)', border: '1px solid rgba(14, 203, 129, 0.3)' }}
>
<div className="text-4xl">🎉</div>
<div className="font-bold text-lg" style={{ color: '#0ECB81' }}>
{zh ? 'Telegram Bot 已绑定!' : 'Telegram Bot is Active!'}
</div>
<div className="text-xs" style={{ color: '#848E9C' }}>
{zh
? '你现在可以通过 Telegram 用自然语言控制交易系统'
: 'You can now control the trading system via natural language in Telegram'}
</div>
</div>
{config?.token_masked && (
<div
className="p-3 rounded-xl flex items-center gap-3"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<div className="w-2 h-2 rounded-full bg-green-500 flex-shrink-0" />
<div className="min-w-0">
<div className="text-xs font-mono" style={{ color: '#848E9C' }}>
{zh ? 'Bot Token' : 'Bot Token'}
</div>
<div className="text-sm font-mono truncate" style={{ color: '#EAECEF' }}>
{config.token_masked}
</div>
</div>
</div>
)}
{/* AI Model selector — works on active bot */}
<BoundModelSelector
zh={zh}
models={models}
currentModelId={config?.model_id ?? ''}
onSaved={(modelId) => {
setConfig((prev) => prev ? { ...prev, model_id: modelId } : prev)
}}
/>
{/* What you can do */}
<div
className="p-4 rounded-xl space-y-2"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<div className="text-xs font-semibold uppercase tracking-wide mb-2" style={{ color: '#848E9C' }}>
{zh ? '支持的命令' : 'Supported Commands'}
</div>
{[
{ cmd: '/help', desc: zh ? '查看所有命令' : 'Show all commands' },
{ cmd: zh ? '查看交易员状态' : 'Show trader status', desc: zh ? '自然语言查询' : 'Natural language' },
{ cmd: zh ? '启动/停止交易员' : 'Start/stop trader', desc: zh ? '自然语言控制' : 'Natural language control' },
{ cmd: zh ? '查看持仓' : 'View positions', desc: zh ? '实时持仓查询' : 'Real-time position query' },
{ cmd: zh ? '配置策略' : 'Configure strategy', desc: zh ? '修改交易策略' : 'Modify trading strategy' },
].map((item, i) => (
<div key={i} className="flex items-start gap-2 text-xs">
<code className="font-mono px-1.5 py-0.5 rounded flex-shrink-0" style={{ background: '#1E2329', color: '#2AABEE' }}>
{item.cmd}
</code>
<span style={{ color: '#848E9C' }}>{item.desc}</span>
</div>
))}
</div>
<div className="flex gap-3">
<button
onClick={handleUnbind}
disabled={isUnbinding}
className="flex-1 flex items-center justify-center gap-2 px-4 py-3 rounded-xl text-sm font-semibold transition-all hover:bg-white/5 disabled:opacity-50"
style={{ background: 'rgba(246, 70, 93, 0.1)', color: '#F6465D', border: '1px solid rgba(246, 70, 93, 0.2)' }}
>
<Unlink className="w-4 h-4" />
{isUnbinding ? (zh ? '解绑中...' : 'Unbinding...') : (zh ? '解绑账号' : 'Unbind Account')}
</button>
<button
onClick={onClose}
className="flex-1 px-4 py-3 rounded-xl text-sm font-bold transition-all hover:scale-[1.02]"
style={{ background: '#2AABEE', color: '#000' }}
>
{zh ? '完成' : 'Done'}
</button>
</div>
</div>
)}
</>
)}
</div>
</div>
</div>
)
}
// BoundModelSelector — lets the user change the AI model when the bot is already active.
// It updates the model_id without requiring re-entry of the bot token.
function BoundModelSelector({
zh,
models,
currentModelId,
onSaved,
}: {
zh: boolean
models: AIModel[]
currentModelId: string
onSaved: (modelId: string) => void
}) {
const [modelId, setModelId] = useState(currentModelId)
const [isSaving, setIsSaving] = useState(false)
// Keep in sync if parent updates
useEffect(() => { setModelId(currentModelId) }, [currentModelId])
const handleSave = async () => {
setIsSaving(true)
try {
// POST /api/telegram/model — lightweight endpoint for model-only update
await api.updateTelegramModel(modelId)
onSaved(modelId)
toast.success(zh ? 'AI 模型已更新' : 'AI model updated')
} catch {
toast.error(zh ? '更新失败' : 'Update failed')
} finally {
setIsSaving(false)
}
}
if (models.length === 0) return null
return (
<div className="space-y-2">
<label className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
{zh ? 'AI 模型(用于自然语言解析)' : 'AI Model (for natural language)'}
</label>
<div className="flex gap-2">
<select
value={modelId}
onChange={(e) => setModelId(e.target.value)}
className="flex-1 px-3 py-2.5 rounded-xl text-sm appearance-none"
style={{
background: '#0B0E11',
border: '1px solid #2B3139',
color: modelId ? '#EAECEF' : '#848E9C',
}}
>
<option value="">{zh ? '— 自动选择' : '— Auto-select'}</option>
{models.map((m) => (
<option key={m.id} value={m.id}>
{m.name}{m.customModelName ? ` · ${m.customModelName}` : ''}
</option>
))}
</select>
<button
onClick={handleSave}
disabled={isSaving || modelId === currentModelId}
className="px-4 py-2.5 rounded-xl text-sm font-bold transition-all hover:scale-[1.02] disabled:opacity-40 disabled:cursor-not-allowed"
style={{ background: '#F0B90B', color: '#000', whiteSpace: 'nowrap' }}
>
{isSaving ? '...' : (zh ? '保存' : 'Save')}
</button>
</div>
</div>
)
}

View File

@@ -25,9 +25,6 @@ interface AuthContextType {
email: string,
password: string,
betaCode?: string
) => Promise<{
success: boolean
message?: string
) => Promise<{ success: boolean; message?: string }>
resetPassword: (
email: string,

View File

@@ -1,4 +1,4 @@
NOFX i18n Consolidation - Translation Keys
// NOFX i18n Consolidation - Translation Keys
// Generated by Atlas Orchestrator
// Branch: feat/i18n-consolidation-patch
// Purpose: Centralize scattered i18n strings from 8 strategy components

View File

@@ -339,8 +339,8 @@ export const translations = {
selectTradingStrategy: 'Select Trading Strategy',
useStrategy: 'Use Strategy',
noStrategyManual: '-- No Strategy (Manual Configuration) --',
active: ' (Active)',
default: ' [Default]',
strategyActive: ' (Active)',
strategyDefault: ' [Default]',
noStrategyHint: 'No strategies yet, please create in Strategy Studio first',
strategyDetails: 'Strategy Details',
activating: 'Activating',
@@ -1563,8 +1563,8 @@ export const translations = {
selectTradingStrategy: '选择交易策略',
useStrategy: '使用策略',
noStrategyManual: '-- 不使用策略(手动配置) --',
active: ' (当前激活)',
default: ' [默认]',
strategyActive: ' (当前激活)',
strategyDefault: ' [默认]',
noStrategyHint: '暂无策略,请先在策略工作室创建策略',
strategyDetails: '策略详情',
activating: '激活中',
@@ -2734,8 +2734,8 @@ export const translations = {
selectTradingStrategy: 'Pilih Strategi Trading',
useStrategy: 'Gunakan Strategi',
noStrategyManual: '-- Tanpa Strategi (Konfigurasi Manual) --',
active: ' (Aktif)',
default: ' [Default]',
strategyActive: ' (Aktif)',
strategyDefault: ' [Default]',
noStrategyHint: 'Belum ada strategi, buat di Strategy Studio terlebih dahulu',
strategyDetails: 'Detail Strategi',
activating: 'Mengaktifkan',

View File

@@ -8,6 +8,7 @@ import type {
TraderConfigData,
AIModel,
Exchange,
TelegramConfig,
CreateTraderRequest,
CreateExchangeRequest,
UpdateModelConfigRequest,
@@ -785,4 +786,26 @@ export const api = {
if (!result.success) throw new Error('获取历史仓位失败')
return result.data!
},
// Telegram Bot API
async getTelegramConfig(): Promise<TelegramConfig> {
const result = await httpClient.get<TelegramConfig>(`${API_BASE}/telegram`)
if (!result.success) throw new Error('获取Telegram配置失败')
return result.data!
},
async updateTelegramConfig(token: string, modelId?: string): Promise<void> {
const result = await httpClient.post(`${API_BASE}/telegram`, { bot_token: token, model_id: modelId ?? '' })
if (!result.success) throw new Error('保存Telegram配置失败')
},
async unbindTelegram(): Promise<void> {
const result = await httpClient.delete(`${API_BASE}/telegram/binding`)
if (!result.success) throw new Error('解绑Telegram失败')
},
async updateTelegramModel(modelId: string): Promise<void> {
const result = await httpClient.post(`${API_BASE}/telegram/model`, { model_id: modelId })
if (!result.success) throw new Error('更新Telegram模型失败')
},
}

View File

@@ -116,6 +116,13 @@ export interface AIModel {
customModelName?: string
}
export interface TelegramConfig {
token_masked: string // Masked token like "123456:ABC***XYZ"
is_bound: boolean // Whether a user has sent /start
bound_chat_id?: number // The bound chat ID (if any)
model_id?: string // AI model selected for Telegram replies
}
export interface Exchange {
id: string // UUID (empty for supported exchange templates)
exchange_type: string // "binance", "bybit", "okx", "hyperliquid", "aster", "lighter"