初始化模版工程

This commit is contained in:
Cloud Bot
2026-03-20 07:33:46 +00:00
commit 23717e0ecd
386 changed files with 51675 additions and 0 deletions

View File

@@ -0,0 +1,41 @@
import { NextResponse } from 'next/server'
import { getDefaultAgentId } from '@/app/api/nova-config'
export async function GET() {
const baseUrl = process.env.NOVA_BASE_URL || ''
const agentId = getDefaultAgentId()
const tenantId = process.env.NOVA_TENANT_ID || ''
let stats = {
activeConnections: 0,
totalMessages: 0,
averageResponseTime: 0,
}
try {
const { getStats } = await import('@/remote-control/shared/nova-bridge')
stats = getStats()
} catch {
// nova-bridge 未加载
}
let status: 'connected' | 'disconnected' = 'disconnected'
try {
const response = await fetch(`${baseUrl}/health`, {
signal: AbortSignal.timeout(5000),
})
if (response.ok) {
status = 'connected'
}
} catch {
// 连接失败
}
return NextResponse.json({
baseUrl,
agentId,
tenantId,
status,
...stats,
})
}

View File

@@ -0,0 +1,109 @@
import { NextRequest, NextResponse } from 'next/server'
import { ConfigManager, reconnectAllPlatforms } from '@/remote-control/config/manager'
export async function GET() {
const configManager = ConfigManager.getInstance()
await configManager.ensureLoaded()
const config = configManager.getMasked()
return NextResponse.json(config)
}
export async function PUT(request: NextRequest) {
try {
const body = await request.json()
const configManager = ConfigManager.getInstance()
await configManager.ensureLoaded()
// Replace masked values with existing real values so we don't overwrite secrets
const merged = stripMaskedValues(body, configManager.get())
const validationError = validateConfig(merged)
if (validationError) {
return NextResponse.json(
{ success: false, error: validationError },
{ status: 400 }
)
}
// skipEmit: lifecycle is managed explicitly below, avoid double-triggering
await configManager.update(merged, { skipEmit: true })
// After saving, explicitly manage bot lifecycle:
// - Stop all disabled bots
// - Reconnect (stop + start) all enabled bots
const config = configManager.get()
const errors: string[] = []
await reconnectAllPlatforms(config, errors)
return NextResponse.json({
success: true,
message: errors.length > 0
? `配置已保存,部分渠道连接异常: ${errors.join('; ')}`
: '配置已保存并生效',
})
} catch (error) {
return NextResponse.json(
{ success: false, error: '保存配置失败' },
{ status: 500 }
)
}
}
const MASK_PREFIX = '••••'
/**
* If the frontend sends back a masked value (e.g. "••••pbYI"), replace it
* with the real value from the current config so we never overwrite secrets.
*/
function stripMaskedValues(
incoming: Record<string, Record<string, unknown>>,
current: Record<string, Record<string, unknown>>,
): Record<string, Record<string, unknown>> {
const result: Record<string, Record<string, unknown>> = {}
for (const platform of Object.keys(incoming)) {
const incomingPlatform = incoming[platform]
const currentPlatform = current[platform] ?? {}
const merged: Record<string, unknown> = {}
for (const key of Object.keys(incomingPlatform)) {
const val = incomingPlatform[key]
if (typeof val === 'string' && val.startsWith(MASK_PREFIX)) {
// Keep the real value
merged[key] = currentPlatform[key] ?? ''
} else {
merged[key] = val
}
}
result[platform] = merged
}
return result
}
function validateConfig(config: Record<string, unknown>): string | null {
const discord = config.discord as Record<string, unknown> | undefined
const dingtalk = config.dingtalk as Record<string, unknown> | undefined
const lark = config.lark as Record<string, unknown> | undefined
if (discord?.enabled && !discord?.botToken) {
return 'Discord Bot Token 不能为空'
}
if (dingtalk?.enabled && (!dingtalk?.clientId || !dingtalk?.clientSecret)) {
return '钉钉 Client ID 和 Client Secret 不能为空'
}
if (lark?.enabled) {
if (!lark?.appId || !lark?.appSecret) {
return '飞书 App ID 和 App Secret 不能为空'
}
}
const telegram = config.telegram as Record<string, unknown> | undefined
const slack = config.slack as Record<string, unknown> | undefined
if (telegram?.enabled && !telegram?.botToken) {
return 'Telegram Bot Token 不能为空'
}
if (slack?.enabled && (!slack?.botToken || !slack?.appToken)) {
return 'Slack Bot Token 和 App Token 不能为空'
}
return null
}

View File

@@ -0,0 +1,33 @@
import { NextRequest, NextResponse } from 'next/server'
import { BotLogger } from '@/remote-control/shared/logger'
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const limit = parseInt(searchParams.get('limit') || '100')
const offset = parseInt(searchParams.get('offset') || '0')
const platform = searchParams.get('platform') as 'discord' | 'dingtalk' | null
const eventType = searchParams.get('eventType') || null
const severity = searchParams.get('severity') as 'info' | 'warning' | 'error' | null
const { logs, total } = BotLogger.getLogs({
limit,
offset,
platform: platform || undefined,
eventType: eventType || undefined,
severity: severity || undefined,
})
return NextResponse.json({
logs,
total,
hasMore: offset + logs.length < total,
})
}
export async function DELETE() {
BotLogger.clear()
return NextResponse.json({
success: true,
message: '日志已清空',
})
}

View File

@@ -0,0 +1,71 @@
import { NextResponse } from 'next/server'
import { ConfigManager } from '@/remote-control/config/manager'
export async function GET() {
const mgr = ConfigManager.getInstance()
await mgr.ensureLoaded()
const config = mgr.get()
const statuses: Record<string, unknown> = {}
// Discord Bot 状态
if (config.discord.enabled) {
try {
const discordBot = await import('@/remote-control/bots/discord')
statuses.discord = discordBot.getStatus()
} catch {
statuses.discord = { platform: 'discord', status: 'disconnected', error: 'Bot 模块未加载' }
}
} else {
statuses.discord = { platform: 'discord', status: 'disconnected' }
}
// 钉钉 Bot 状态
if (config.dingtalk.enabled) {
try {
const dingtalkBot = await import('@/remote-control/bots/dingtalk')
statuses.dingtalk = dingtalkBot.getStatus()
} catch {
statuses.dingtalk = { platform: 'dingtalk', status: 'disconnected', error: 'Bot 模块未加载' }
}
} else {
statuses.dingtalk = { platform: 'dingtalk', status: 'disconnected' }
}
// 飞书 Bot 状态
if (config.lark.enabled) {
try {
const larkBot = await import('@/remote-control/bots/lark')
statuses.lark = larkBot.getStatus()
} catch {
statuses.lark = { platform: 'lark', status: 'disconnected', error: 'Bot 模块未加载' }
}
} else {
statuses.lark = { platform: 'lark', status: 'disconnected' }
}
// Telegram Bot 状态
if (config.telegram.enabled) {
try {
const telegramBot = await import('@/remote-control/bots/telegram')
statuses.telegram = telegramBot.getStatus()
} catch {
statuses.telegram = { platform: 'telegram', status: 'disconnected', error: 'Bot 模块未加载' }
}
} else {
statuses.telegram = { platform: 'telegram', status: 'disconnected' }
}
// Slack Bot 状态
if (config.slack.enabled) {
try {
const slackBot = await import('@/remote-control/bots/slack')
statuses.slack = slackBot.getStatus()
} catch {
statuses.slack = { platform: 'slack', status: 'disconnected', error: 'Bot 模块未加载' }
}
} else {
statuses.slack = { platform: 'slack', status: 'disconnected' }
}
return NextResponse.json(statuses)
}

View File

@@ -0,0 +1,147 @@
import { NextRequest, NextResponse } from 'next/server'
import { ConfigManager } from '@/remote-control/config/manager'
function sleep(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms))
}
/**
* Poll a bot's getStatus() until it leaves 'connecting' state,
* or until timeout (default 10s). Returns the final status.
*/
async function waitForConnection(
getStatus: () => { status: string; error?: string },
timeoutMs = 10000,
intervalMs = 500,
): Promise<{ status: string; error?: string }> {
const deadline = Date.now() + timeoutMs
while (Date.now() < deadline) {
const s = getStatus()
// 'connected' or 'disconnected' (with error) means we have a definitive answer
if (s.status !== 'connecting') return s
await sleep(intervalMs)
}
return getStatus()
}
export async function POST(request: NextRequest) {
try {
const { platform } = await request.json()
if (platform !== 'discord' && platform !== 'dingtalk' && platform !== 'lark' && platform !== 'telegram' && platform !== 'slack') {
return NextResponse.json({ success: false, error: '无效的平台' }, { status: 400 })
}
const mgr = ConfigManager.getInstance()
await mgr.ensureLoaded()
const config = mgr.get()
// Reject test for disabled platforms
const platformConfig = config[platform as keyof typeof config] as { enabled: boolean }
if (!platformConfig?.enabled) {
return NextResponse.json({ success: false, error: '该渠道已禁用,请先启用后再测试' }, { status: 400 })
}
if (platform === 'discord') {
if (!config.discord.botToken) {
return NextResponse.json({ success: false, error: 'Discord Bot Token 未配置' })
}
try {
const bot = await import('@/remote-control/bots/discord')
await bot.stopBot()
await bot.startBot(config.discord.botToken)
const status = await waitForConnection(() => bot.getStatus())
return NextResponse.json({
success: status.status === 'connected',
status,
error: status.status !== 'connected' ? (status.error || '连接超时,请检查 Token 是否正确') : undefined,
})
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return NextResponse.json({ success: false, error: `Discord 连接失败: ${msg}` })
}
}
if (platform === 'dingtalk') {
if (!config.dingtalk.clientId || !config.dingtalk.clientSecret) {
return NextResponse.json({ success: false, error: '钉钉 Client ID 或 Client Secret 未配置' })
}
try {
const bot = await import('@/remote-control/bots/dingtalk')
await bot.stopBot()
await bot.startBot(config.dingtalk.clientId, config.dingtalk.clientSecret)
const status = await waitForConnection(() => bot.getStatus())
return NextResponse.json({
success: status.status === 'connected',
status,
error: status.status !== 'connected' ? (status.error || '连接超时,请检查凭证是否正确') : undefined,
})
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return NextResponse.json({ success: false, error: `钉钉连接失败: ${msg}` })
}
}
if (platform === 'lark') {
if (!config.lark.appId || !config.lark.appSecret) {
return NextResponse.json({ success: false, error: '飞书 App ID 或 App Secret 未配置' })
}
try {
const bot = await import('@/remote-control/bots/lark')
await bot.stopBot()
await bot.startBot(config.lark.appId, config.lark.appSecret)
const status = await waitForConnection(() => bot.getStatus())
return NextResponse.json({
success: status.status === 'connected',
status,
error: status.status !== 'connected' ? (status.error || '连接超时,请检查凭证是否正确') : undefined,
})
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return NextResponse.json({ success: false, error: `飞书连接失败: ${msg}` })
}
}
if (platform === 'telegram') {
if (!config.telegram.botToken) {
return NextResponse.json({ success: false, error: 'Telegram Bot Token 未配置' })
}
try {
const bot = await import('@/remote-control/bots/telegram')
await bot.stopBot()
await bot.startBot(config.telegram.botToken)
const status = await waitForConnection(() => bot.getStatus())
return NextResponse.json({
success: status.status === 'connected',
status,
error: status.status !== 'connected' ? (status.error || '连接超时,请检查 Token 是否正确') : undefined,
})
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return NextResponse.json({ success: false, error: `Telegram 连接失败: ${msg}` })
}
}
if (platform === 'slack') {
if (!config.slack.botToken || !config.slack.appToken) {
return NextResponse.json({ success: false, error: 'Slack Bot Token 或 App Token 未配置' })
}
try {
const bot = await import('@/remote-control/bots/slack')
await bot.stopBot()
await bot.startBot(config.slack.botToken, config.slack.appToken)
const status = await waitForConnection(() => bot.getStatus())
return NextResponse.json({
success: status.status === 'connected',
status,
error: status.status !== 'connected' ? (status.error || '连接超时,请检查凭证是否正确') : undefined,
})
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
return NextResponse.json({ success: false, error: `Slack 连接失败: ${msg}` })
}
}
} catch {
return NextResponse.json({ success: false, error: '请求解析失败' }, { status: 400 })
}
}