初始化模版工程
This commit is contained in:
41
app/api/remote-control/agent-info/route.ts
Normal file
41
app/api/remote-control/agent-info/route.ts
Normal 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,
|
||||
})
|
||||
}
|
||||
109
app/api/remote-control/config/route.ts
Normal file
109
app/api/remote-control/config/route.ts
Normal 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
|
||||
}
|
||||
33
app/api/remote-control/logs/route.ts
Normal file
33
app/api/remote-control/logs/route.ts
Normal 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: '日志已清空',
|
||||
})
|
||||
}
|
||||
71
app/api/remote-control/status/route.ts
Normal file
71
app/api/remote-control/status/route.ts
Normal 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)
|
||||
}
|
||||
147
app/api/remote-control/test/route.ts
Normal file
147
app/api/remote-control/test/route.ts
Normal 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 })
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user