Просмотр исходного кода

添加测试功能,包含新的 TestController 和 TestService,支持发送消息、创建聊天群和邀请成员的接口,同时更新相关路由和 SenderService 的 JSON 导入功能,增强错误处理和日志记录。

wuyi 1 месяц назад
Родитель
Сommit
6928f5f608

+ 2 - 0
src/app.ts

@@ -16,6 +16,7 @@ import fishFriendsRoutes from './routes/fish-friends.routes'
 import messagesRoutes from './routes/messages.routes'
 import tgMsgSendRoutes from './routes/tg-msg-send.routes'
 import taskRoutes from './routes/task.routes'
+import testRoutes from './routes/test.routes'
 import senderRoutes from './routes/sender.routes'
 import chatGroupRoutes from './routes/chat-group.routes'
 
@@ -92,6 +93,7 @@ export const createApp = async () => {
   app.register(taskRoutes, { prefix: '/api/tasks' })
   app.register(senderRoutes, { prefix: '/api/senders' })
   app.register(chatGroupRoutes, { prefix: '/api/chat-groups' })
+  app.register(testRoutes, { prefix: '/api/test' })
   const dataSource = createDataSource(app)
   await dataSource.initialize()
   app.decorate('dataSource', dataSource)

+ 60 - 0
src/controllers/sender.controller.ts

@@ -1,4 +1,6 @@
 import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
+import fs from 'fs/promises'
+import path from 'path'
 import { SenderService } from '../services/sender.service'
 import { CreateSenderBody, UpdateSenderBody, ListSenderQuery } from '../dto/sender.dto'
 
@@ -9,6 +11,64 @@ export class SenderController {
     this.senderService = new SenderService(app)
   }
 
+  async importFromJson(_: FastifyRequest, reply: FastifyReply) {
+    try {
+      const filePath = path.join(process.cwd(), 'robin_tg_user.json')
+      const fileContent = await fs.readFile(filePath, 'utf-8')
+      const parsed = JSON.parse(fileContent)
+
+      if (!Array.isArray(parsed)) {
+        return reply.code(400).send({ message: '文件格式错误,应为数组' })
+      }
+
+      const importList: { id: string; dcId?: number; authKey?: string }[] = []
+      let parseFailed = 0
+
+      for (const item of parsed) {
+        try {
+          const { id, token } = item ?? {}
+          if (!id || !token) {
+            parseFailed++
+            continue
+          }
+
+          const tokenData = typeof token === 'string' ? JSON.parse(token) : token
+          const dcId = tokenData?.dcId
+          const authKey = tokenData?.authKey
+
+          if (dcId === undefined || !authKey) {
+            parseFailed++
+            continue
+          }
+
+          importList.push({
+            id: String(id),
+            dcId: Number(dcId),
+            authKey: String(authKey)
+          })
+        } catch {
+          parseFailed++
+        }
+      }
+
+      const result = await this.senderService.importFromJson(importList)
+
+      return reply.send({
+        message: '转换完成',
+        data: {
+          fileTotal: parsed.length,
+          parsedTotal: importList.length,
+          parseFailed,
+          ...result
+        }
+      })
+    } catch (error) {
+      return reply
+        .code(500)
+        .send({ message: '转换失败', error: error instanceof Error ? error.message : String(error) })
+    }
+  }
+
   async create(request: FastifyRequest<{ Body: CreateSenderBody }>, reply: FastifyReply) {
     try {
       const { id, dcId, authKey, sessionStr } = request.body

+ 8 - 88
src/controllers/task.controller.ts

@@ -1,13 +1,6 @@
 import { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify'
 import { TaskService } from '../services/task.service'
-import {
-  UpdateTaskBody,
-  ListTaskQuery,
-  ListTaskItemQuery,
-  SendMessageBody,
-  CreateChatGroupBody,
-  InviteMembersBody
-} from '../dto/task.dto'
+import { UpdateTaskBody, ListTaskQuery, ListTaskItemQuery } from '../dto/task.dto'
 import { Task } from '../entities/task.entity'
 
 export class TaskController {
@@ -169,92 +162,19 @@ export class TaskController {
     }
   }
 
-  async testSendMessage(request: FastifyRequest<{ Body: SendMessageBody }>, reply: FastifyReply) {
+  async pauseTask(request: FastifyRequest<{ Params: { id: string } }>, reply: FastifyReply) {
     try {
-      const { senderId, taskId, delay, count, session, dcId, authKey, sendToVerifyAccounts, verifyAccounts } =
-        request.body
-
-      if (!senderId || !taskId) {
-        return reply.code(400).send({
-          success: false,
-          message: 'senderId 和 taskId 不能为空'
-        })
+      const id = parseInt(request.params.id)
+      if (isNaN(id)) {
+        return reply.code(400).send({ message: '无效的任务ID' })
       }
-
-      const result = await this.taskService.testSendMessage(
-        senderId,
-        taskId,
-        delay,
-        count,
-        session,
-        dcId,
-        authKey,
-        sendToVerifyAccounts,
-        verifyAccounts
-      )
-
-      return reply.code(200).send(result)
+      await this.taskService.pauseTask(id)
+      return reply.send({ message: '任务已暂停' })
     } catch (error) {
       return reply.code(500).send({
-        success: false,
-        message: '测试发送消息时发生错误',
+        message: '暂停任务失败',
         error: error instanceof Error ? error.message : '未知错误'
       })
     }
   }
-
-  async testCreateChatGroup(request: FastifyRequest<{ Body: CreateChatGroupBody }>, reply: FastifyReply) {
-    const { senderId, session, dcId, authKey, groupName, groupDescription, groupType, initMsg } = request.body
-
-    if (!senderId || !groupName || !groupType) {
-      return reply.code(400).send({
-        success: false,
-        message: 'senderId, groupName 和 groupType 为必填参数'
-      })
-    }
-
-    const validGroupTypes = ['megagroup', 'channel']
-    if (!validGroupTypes.includes(groupType)) {
-      return reply.code(400).send({
-        success: false,
-        message: `groupType 必须是以下之一: ${validGroupTypes.join(', ')}`
-      })
-    }
-
-    const result = await this.taskService.testCreateChatGroup(
-      senderId,
-      groupName,
-      groupDescription,
-      groupType,
-      session,
-      dcId,
-      authKey,
-      initMsg
-    )
-
-    if (result.success) {
-      return reply.code(200).send(result)
-    } else {
-      return reply.code(500).send(result)
-    }
-  }
-
-  async testInviteMembersToChat(request: FastifyRequest<{ Body: InviteMembersBody }>, reply: FastifyReply) {
-    const { senderId, chatId, accessHash, members } = request.body
-
-    if (!senderId || !chatId || !members || members.length === 0) {
-      return reply.code(400).send({
-        success: false,
-        message: 'senderId, chatId 和 members 为必填参数'
-      })
-    }
-
-    const result = await this.taskService.testInviteMembersToChat(senderId, chatId, members, accessHash)
-
-    if (result.success) {
-      return reply.code(200).send(result)
-    } else {
-      return reply.code(500).send(result)
-    }
-  }
 }

+ 100 - 0
src/controllers/test.controller.ts

@@ -0,0 +1,100 @@
+import { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify'
+import { SendMessageBody, CreateChatGroupBody, InviteMembersBody } from '../dto/task.dto'
+import { TestService } from '../services/test.service'
+
+export class TestController {
+  private readonly testService: TestService
+
+  constructor(app: FastifyInstance) {
+    this.testService = new TestService(app)
+  }
+
+  async testSendMessage(request: FastifyRequest<{ Body: SendMessageBody }>, reply: FastifyReply) {
+    try {
+      const { senderId, taskId, delay, count, session, dcId, authKey, sendToVerifyAccounts, verifyAccounts } =
+        request.body
+
+      if (!senderId || !taskId) {
+        return reply.code(400).send({
+          success: false,
+          message: 'senderId 和 taskId 不能为空'
+        })
+      }
+
+      const result = await this.testService.testSendMessage(
+        senderId,
+        taskId,
+        delay,
+        count,
+        session,
+        dcId,
+        authKey,
+        sendToVerifyAccounts,
+        verifyAccounts
+      )
+
+      return reply.code(200).send(result)
+    } catch (error) {
+      return reply.code(500).send({
+        success: false,
+        message: '测试发送消息时发生错误',
+        error: error instanceof Error ? error.message : '未知错误'
+      })
+    }
+  }
+
+  async testCreateChatGroup(request: FastifyRequest<{ Body: CreateChatGroupBody }>, reply: FastifyReply) {
+    const { senderId, session, dcId, authKey, groupName, groupDescription, groupType, initMsg } = request.body
+
+    if (!senderId || !groupName || !groupType) {
+      return reply.code(400).send({
+        success: false,
+        message: 'senderId, groupName 和 groupType 为必填参数'
+      })
+    }
+
+    const validGroupTypes = ['megagroup', 'channel']
+    if (!validGroupTypes.includes(groupType)) {
+      return reply.code(400).send({
+        success: false,
+        message: `groupType 必须是以下之一: ${validGroupTypes.join(', ')}`
+      })
+    }
+
+    const result = await this.testService.testCreateChatGroup(
+      senderId,
+      groupName,
+      groupDescription,
+      groupType,
+      session,
+      dcId,
+      authKey,
+      initMsg
+    )
+
+    if (result.success) {
+      return reply.code(200).send(result)
+    } else {
+      return reply.code(500).send(result)
+    }
+  }
+
+  async testInviteMembersToChat(request: FastifyRequest<{ Body: InviteMembersBody }>, reply: FastifyReply) {
+    const { senderId, chatId, accessHash, members } = request.body
+
+    if (!senderId || !chatId || !members || members.length === 0) {
+      return reply.code(400).send({
+        success: false,
+        message: 'senderId, chatId 和 members 为必填参数'
+      })
+    }
+
+    const result = await this.testService.testInviteMembersToChat(senderId, chatId, members, accessHash)
+
+    if (result.success) {
+      return reply.code(200).send(result)
+    } else {
+      return reply.code(500).send(result)
+    }
+  }
+}

+ 14 - 0
src/entities/task.entity.ts

@@ -1,5 +1,12 @@
 import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'
 
+export enum TaskStatus {
+  PENDING = 'pending',
+  SENDING = 'sending',
+  PAUSED = 'paused',
+  COMPLETED = 'completed'
+}
+
 @Entity()
 export class Task {
   @PrimaryGeneratedColumn()
@@ -26,6 +33,13 @@ export class Task {
   @Column({ default: false })
   delFlag: boolean
 
+  @Column({
+    type: 'enum',
+    enum: TaskStatus,
+    default: TaskStatus.PENDING
+  })
+  status: TaskStatus
+
   @Column({ type: 'datetime', precision: 6, default: null })
   startedAt: Date
 

+ 2 - 0
src/routes/sender.routes.ts

@@ -42,4 +42,6 @@ export default async function senderRoutes(fastify: FastifyInstance) {
     { onRequest: [authenticate, hasRole(UserRole.ADMIN)] },
     senderController.hardDelete.bind(senderController)
   )
+
+  fastify.post('/import/json', senderController.importFromJson.bind(senderController))
 }

+ 13 - 16
src/routes/task.routes.ts

@@ -4,10 +4,7 @@ import { hasRole } from '../middlewares/auth.middleware'
 import {
   UpdateTaskBody,
   ListTaskQuery,
-  ListTaskItemQuery,
-  SendMessageBody,
-  CreateChatGroupBody,
-  InviteMembersBody
+  ListTaskItemQuery
 } from '../dto/task.dto'
 import { UserRole } from '../entities/user.entity'
 
@@ -28,6 +25,18 @@ export default async function taskRoutes(fastify: FastifyInstance) {
     taskController.getById.bind(taskController)
   )
 
+  fastify.post<{ Params: { id: string } }>(
+    '/:id/start',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    taskController.startTask.bind(taskController)
+  )
+
+  fastify.post<{ Params: { id: string } }>(
+    '/:id/pause',
+    { onRequest: [hasRole(UserRole.ADMIN)] },
+    taskController.pauseTask.bind(taskController)
+  )
+
   fastify.post<{ Body: UpdateTaskBody }>(
     '/update',
     { onRequest: [hasRole(UserRole.ADMIN)] },
@@ -45,16 +54,4 @@ export default async function taskRoutes(fastify: FastifyInstance) {
     { onRequest: [hasRole(UserRole.ADMIN)] },
     taskController.listTaskItems.bind(taskController)
   )
-
-  fastify.post<{ Body: SendMessageBody }>('/test/send', taskController.testSendMessage.bind(taskController))
-
-  fastify.post<{ Body: CreateChatGroupBody }>(
-    '/test/create-chat-group',
-    taskController.testCreateChatGroup.bind(taskController)
-  )
-
-  fastify.post<{ Body: InviteMembersBody }>(
-    '/test/invite-members',
-    taskController.testInviteMembersToChat.bind(taskController)
-  )
 }

+ 19 - 0
src/routes/test.routes.ts

@@ -0,0 +1,19 @@
+import { FastifyInstance } from 'fastify'
+import { TestController } from '../controllers/test.controller'
+import { SendMessageBody, CreateChatGroupBody, InviteMembersBody } from '../dto/task.dto'
+
+export default async function testRoutes(fastify: FastifyInstance) {
+  const testController = new TestController(fastify)
+
+  fastify.post<{ Body: SendMessageBody }>('/task/send', testController.testSendMessage.bind(testController))
+
+  fastify.post<{ Body: CreateChatGroupBody }>(
+    '/task/create-chat-group',
+    testController.testCreateChatGroup.bind(testController)
+  )
+
+  fastify.post<{ Body: InviteMembersBody }>(
+    '/task/invite-members',
+    testController.testInviteMembersToChat.bind(testController)
+  )
+}

+ 47 - 1
src/services/sender.service.ts

@@ -2,6 +2,7 @@ import { Repository } from 'typeorm'
 import { FastifyInstance } from 'fastify'
 import { Sender } from '../entities/sender.entity'
 import { PaginationResponse } from '../dto/common.dto'
+import { buildStringSessionByDcIdAndAuthKey } from '../utils/tg.util'
 
 export class SenderService {
   private senderRepository: Repository<Sender>
@@ -78,5 +79,50 @@ export class SenderService {
     sender.lastUsageTime = new Date()
     return this.senderRepository.save(sender)
   }
-}
 
+  async importFromJson(
+    senders: Array<{ id: string; dcId?: number; authKey?: string; sessionStr?: string }>
+  ): Promise<{ created: number; updated: number; failed: number; total: number }> {
+    let created = 0
+    let updated = 0
+    let failed = 0
+
+    for (const sender of senders) {
+      try {
+        if (!sender.id) {
+          failed++
+          continue
+        }
+
+        let sessionStr = sender.sessionStr
+        if (!sessionStr && sender.dcId !== undefined && sender.authKey) {
+          try {
+            sessionStr = buildStringSessionByDcIdAndAuthKey(sender.dcId, sender.authKey)
+          } catch {
+            // 构建失败则标记失败并跳过
+            failed++
+            continue
+          }
+        }
+
+        const existing = await this.findById(sender.id)
+
+        if (existing) {
+          await this.senderRepository.update(sender.id, {
+            dcId: sender.dcId ?? existing.dcId,
+            authKey: sender.authKey ?? existing.authKey,
+            sessionStr: sessionStr ?? existing.sessionStr
+          })
+          updated++
+        } else {
+          await this.create(sender.id, sender.dcId, sender.authKey, sessionStr)
+          created++
+        }
+      } catch {
+        failed++
+      }
+    }
+
+    return { created, updated, failed, total: senders.length }
+  }
+}

Разница между файлами не показана из-за своего большого размера
+ 201 - 875
src/services/task.service.ts


+ 980 - 0
src/services/test.service.ts

@@ -0,0 +1,980 @@
+import { FastifyInstance } from 'fastify'
+import { Repository } from 'typeorm'
+import { Api, TelegramClient } from 'telegram'
+import { Task, TaskStatus } from '../entities/task.entity'
+import { TaskItem, TaskItemStatus } from '../entities/task-item.entity'
+import { Sender } from '../entities/sender.entity'
+import { SenderService } from './sender.service'
+import { TgClientService } from './tgClient.service'
+import { ChatGroupService } from './chat-group.service'
+import { buildStringSession, buildStringSessionByDcIdAndAuthKey } from '../utils/tg.util'
+
+export class TestService {
+  private readonly app: FastifyInstance
+  private readonly taskRepository: Repository<Task>
+  private readonly taskItemRepository: Repository<TaskItem>
+  private readonly senderRepository: Repository<Sender>
+  private readonly senderService: SenderService
+  private readonly tgClientService: TgClientService
+  private readonly chatGroupService: ChatGroupService
+
+  constructor(app: FastifyInstance) {
+    this.app = app
+    this.taskRepository = app.dataSource.getRepository(Task)
+    this.taskItemRepository = app.dataSource.getRepository(TaskItem)
+    this.senderRepository = app.dataSource.getRepository(Sender)
+    this.senderService = new SenderService(app)
+    this.tgClientService = TgClientService.getInstance()
+    this.chatGroupService = new ChatGroupService(app)
+  }
+
+  async findTaskById(id: number): Promise<Task | null> {
+    return this.taskRepository.findOne({ where: { id, delFlag: false } })
+  }
+
+  async testSendMessage(
+    senderId: string,
+    taskId: number,
+    delay?: number,
+    count?: number,
+    session?: string,
+    dcId?: number,
+    authKey?: string,
+    sendToVerifyAccounts?: boolean,
+    verifyAccounts?: string[]
+  ): Promise<{
+    success: boolean
+    message: string
+    data?: {
+      sender: { id: string }
+      task: { id: number; name: string; message: string }
+      totalSent: number
+      successCount: number
+      failedCount: number
+    }
+    error?: string
+  }> {
+    let client: TelegramClient | null = null
+
+    try {
+      if ((dcId !== undefined && authKey === undefined) || (dcId === undefined && authKey !== undefined)) {
+        return {
+          success: false,
+          message: 'dcId 和 authKey 必须同时传参'
+        }
+      }
+      let sender: Sender | null = null
+      let sessionString: string | null = null
+
+      if (session) {
+        try {
+          sessionString = buildStringSession(session)
+          const existingSender = await this.senderRepository.findOne({ where: { id: senderId } })
+          if (existingSender) {
+            await this.senderRepository.update(senderId, { sessionStr: sessionString })
+            sender = await this.senderRepository.findOne({ where: { id: senderId } })
+          } else {
+            sender = await this.senderService.create(senderId, undefined, undefined, sessionString)
+          }
+          if (!sender) {
+            return {
+              success: false,
+              message: '创建或更新 sender 失败'
+            }
+          }
+        } catch (error) {
+          const errorMessage = error instanceof Error ? error.message : '未知错误'
+          return {
+            success: false,
+            message: `解析 session 失败: ${errorMessage}`
+          }
+        }
+      } else if (dcId !== undefined && authKey !== undefined) {
+        try {
+          sessionString = buildStringSessionByDcIdAndAuthKey(dcId, authKey)
+          const existingSender = await this.senderRepository.findOne({ where: { id: senderId } })
+          if (existingSender) {
+            await this.senderRepository.update(senderId, { dcId, authKey, sessionStr: sessionString })
+            sender = await this.senderRepository.findOne({ where: { id: senderId } })
+          } else {
+            sender = await this.senderService.create(senderId, dcId, authKey, sessionString)
+          }
+          if (!sender) {
+            return {
+              success: false,
+              message: '创建或更新 sender 失败'
+            }
+          }
+        } catch (error) {
+          const errorMessage = error instanceof Error ? error.message : '未知错误'
+          return {
+            success: false,
+            message: `解析 session 失败: ${errorMessage}`
+          }
+        }
+      } else {
+        sender = await this.senderRepository.findOne({ where: { id: senderId, delFlag: false } })
+        if (!sender) {
+          return {
+            success: false,
+            message: '发送账号不存在或已被删除'
+          }
+        }
+      }
+
+      const task = await this.findTaskById(taskId)
+      if (!task) {
+        return {
+          success: false,
+          message: '任务不存在'
+        }
+      }
+
+      const queryOptions: any = {
+        where: { taskId, status: TaskItemStatus.PENDING },
+        order: { id: 'ASC' }
+      }
+
+      queryOptions.take = count && count > 0 ? count : 2
+
+      const allTaskItems = await this.taskItemRepository.find(queryOptions)
+
+      if (sendToVerifyAccounts && verifyAccounts && verifyAccounts.length > 0) {
+        const verifyTaskItems = verifyAccounts.map(
+          verifyAccount =>
+            ({
+              target: verifyAccount.trim(),
+              isVerifyAccount: true
+            } as any)
+        )
+
+        if (verifyTaskItems.length >= 1) {
+          const firstVerifyAccount = verifyTaskItems[0]
+          const insertIndex1 = Math.min(1, allTaskItems.length)
+          allTaskItems.splice(insertIndex1, 0, firstVerifyAccount)
+          this.app.log.info(`已将验证账户插入到第二个位置: ${firstVerifyAccount.target}`)
+        }
+
+        if (verifyTaskItems.length >= 2) {
+          const secondVerifyAccount = verifyTaskItems[1]
+          const insertIndex2 = Math.max(0, allTaskItems.length - 2)
+          allTaskItems.splice(insertIndex2, 0, secondVerifyAccount)
+          this.app.log.info(`已将验证账户插入到倒数第二个位置: ${secondVerifyAccount.target}`)
+        }
+
+        if (verifyTaskItems.length > 2) {
+          const remainingVerifyAccounts = verifyTaskItems.slice(2)
+          allTaskItems.push(...remainingVerifyAccounts)
+          this.app.log.info(`已将 ${remainingVerifyAccounts.length} 个额外验证账户追加到末尾`)
+        }
+
+        this.app.log.info(`已将 ${verifyTaskItems.length} 个验证账户插入到任务列表中`)
+      }
+
+      if (!allTaskItems || allTaskItems.length === 0) {
+        return {
+          success: false,
+          message: '该任务没有目标账户'
+        }
+      }
+
+      if (!sessionString) {
+        sessionString = sender!.sessionStr
+        if (!sessionString) {
+          if (!sender!.dcId || !sender!.authKey) {
+            return {
+              success: false,
+              message: 'Sender 缺少 dcId 或 authKey 信息'
+            }
+          }
+          sessionString = buildStringSessionByDcIdAndAuthKey(sender!.dcId, sender!.authKey)
+          await this.senderRepository.update(sender!.id, { sessionStr: sessionString })
+        }
+      }
+
+      this.app.log.info('正在连接 TelegramClient...')
+      client = await this.tgClientService.connect(sessionString)
+      this.app.log.info('TelegramClient 连接完成')
+
+      const waitTime = Math.floor(Math.random() * 21) + 20
+      this.app.log.info(`连接成功后等待 ${waitTime} 秒,避免新 Session 被限制`)
+      await new Promise(resolve => setTimeout(resolve, waitTime * 1000))
+      this.app.log.info(`等待完成,开始发送消息`)
+
+      let totalSuccessCount = 0
+      let totalFailedCount = 0
+      let totalSentCount = 0
+
+      const useRandomDelay = delay === undefined || delay <= 0
+      const fixedDelay = delay && delay > 0 ? delay : 0
+
+      this.app.log.info(
+        `开始测试发送: sender=${senderId}, task=${taskId}, 目标数=${allTaskItems.length}, 延迟=${
+          useRandomDelay ? '随机(3-10秒)' : `${fixedDelay}秒`
+        }`
+      )
+
+      for (const taskItem of allTaskItems) {
+        try {
+          const parsedTarget = this.parseTarget(taskItem.target)
+          if (!parsedTarget) {
+            throw new Error('target 格式错误,请检查是否正确')
+          }
+
+          const targetPeer = await this.tgClientService.getTargetPeer(client, parsedTarget)
+          if (!targetPeer) {
+            throw new Error('target 无效,无法获取目标信息')
+          }
+
+          const canSendMessage = await this.checkCanSendMessage(client, targetPeer)
+          if (!canSendMessage) {
+            throw new Error('目标用户不允许接收消息或已被限制')
+          }
+
+          await this.tgClientService.sendMessageToPeer(client, targetPeer, task.message)
+
+          try {
+            await this.tgClientService.clearConversation(client, targetPeer)
+          } catch (clearError) {
+            this.app.log.warn(
+              `清除会话失败 [${taskItem.target}]: ${clearError instanceof Error ? clearError.message : '未知错误'}`
+            )
+          }
+
+          try {
+            await this.tgClientService.deleteTempContact(client, targetPeer.id)
+          } catch (deleteError) {
+            this.app.log.warn(
+              `删除临时联系人失败 [${taskItem.target}]: ${
+                deleteError instanceof Error ? deleteError.message : '未知错误'
+              }`
+            )
+          }
+
+          const isVerifyAccount = (taskItem as any).isVerifyAccount === true
+
+          if (isVerifyAccount) {
+            this.app.log.info(`[验证账户] 发送成功: ${taskItem.target}`)
+          } else {
+            if (taskItem.id) {
+              try {
+                await this.taskItemRepository.update(taskItem.id, {
+                  status: TaskItemStatus.SUCCESS,
+                  sentAt: new Date()
+                })
+              } catch (updateError) {
+                const updateErrorMessage = updateError instanceof Error ? updateError.message : '未知错误'
+                this.app.log.warn(`更新 taskItem 状态失败 [${taskItem.target}]: ${updateErrorMessage}`)
+              }
+            }
+          }
+          totalSuccessCount++
+        } catch (error) {
+          const isVerifyAccount = (taskItem as any).isVerifyAccount === true
+          const errorMessage = error instanceof Error ? error.message : '未知错误'
+
+          if (isVerifyAccount) {
+            this.app.log.warn(`[验证账户] 发送失败 [${taskItem.target}]: ${errorMessage}`)
+          } else {
+            if (taskItem.id) {
+              try {
+                await this.taskItemRepository.update(taskItem.id, {
+                  status: TaskItemStatus.FAILED,
+                  sentAt: new Date()
+                })
+              } catch (updateError) {
+                const updateErrorMessage = updateError instanceof Error ? updateError.message : '未知错误'
+                this.app.log.warn(`更新 taskItem 失败状态失败 [${taskItem.target}]: ${updateErrorMessage}`)
+              }
+            }
+            this.app.log.warn(`发送失败 [${taskItem.target}]: ${errorMessage}`)
+          }
+          totalFailedCount++
+        }
+
+        try {
+          await this.senderService.incrementUsageCount(senderId)
+        } catch (error) {
+          this.app.log.warn(
+            `更新 sender usageCount 失败 [${senderId}]: ${error instanceof Error ? error.message : '未知错误'}`
+          )
+        }
+
+        totalSentCount++
+
+        if (totalSentCount < allTaskItems.length) {
+          let actualDelay: number
+          if (useRandomDelay) {
+            actualDelay = Math.floor(Math.random() * 8) + 3
+          } else {
+            actualDelay = fixedDelay
+          }
+          this.app.log.info(`等待延迟 ${actualDelay} 秒后发送下一条消息`)
+          await new Promise(resolve => setTimeout(resolve, actualDelay * 1000))
+        }
+      }
+
+      this.app.log.info(`所有消息发送完成: 总计=${totalSentCount}, 成功=${totalSuccessCount}, 失败=${totalFailedCount}`)
+
+      return {
+        success: true,
+        message: `测试发送完成,共发送 ${totalSentCount} 条,成功 ${totalSuccessCount} 条,失败 ${totalFailedCount} 条`,
+        data: {
+          sender: { id: sender!.id },
+          task: { id: task.id, name: task.name, message: task.message },
+          totalSent: totalSentCount,
+          successCount: totalSuccessCount,
+          failedCount: totalFailedCount
+        }
+      }
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '未知错误'
+      this.app.log.error(`测试发送失败: ${errorMessage}`)
+      return {
+        success: false,
+        message: '测试发送消息时发生错误',
+        error: errorMessage
+      }
+    } finally {
+      if (client) {
+        try {
+          this.app.log.info(`正在断开连接并销毁 client`)
+          await this.tgClientService.disconnect()
+          this.app.log.info(`连接已断开,client 已销毁`)
+        } catch (error) {
+          const errorMessage = error instanceof Error ? error.message : '未知错误'
+          this.app.log.error(`断开连接失败: ${errorMessage}`)
+        }
+      }
+    }
+  }
+
+  async testCreateChatGroup(
+    senderId: string,
+    groupName: string,
+    groupDescription: string,
+    groupType: string,
+    session?: string,
+    dcId?: number,
+    authKey?: string,
+    initMsg?: string
+  ): Promise<any> {
+    let client: TelegramClient | null = null
+
+    try {
+      const sessionResult = await this.resolveSenderSession(senderId, session, dcId, authKey)
+      if ('error' in sessionResult) {
+        return sessionResult.error
+      }
+      const { sessionString } = sessionResult
+
+      this.app.log.info('正在连接 TelegramClient...')
+      client = await this.tgClientService.connect(sessionString)
+      this.app.log.info('TelegramClient 连接完成')
+
+      const waitTime = Math.floor(Math.random() * 21) + 20
+      this.app.log.info(`连接成功后等待 ${waitTime} 秒,避免新 Session 被限制`)
+      await new Promise(resolve => setTimeout(resolve, waitTime * 1000))
+
+      this.app.log.info(`等待完成,开始创建群组`)
+
+      if (groupType !== 'channel' && groupType !== 'megagroup') {
+        return {
+          success: false,
+          message: `不支持的群组类型: ${groupType},仅支持: megagroup(超级群组), channel(频道)`
+        }
+      }
+
+      this.app.log.info(`正在创建${groupType === 'channel' ? '频道' : '超级群组'}: ${groupName}`)
+
+      const result = await client.invoke(
+        new Api.channels.CreateChannel({
+          title: groupName,
+          about: groupDescription || '',
+          megagroup: groupType === 'megagroup'
+        })
+      )
+
+      const updates = result as any
+      const createdChat = updates.chats?.[0]
+
+      if (!createdChat) {
+        return {
+          success: false,
+          message: '创建群组失败,未返回群组信息'
+        }
+      }
+
+      const chatId = createdChat.id
+      const accessHash = createdChat.accessHash
+
+      this.app.log.info(`群组创建成功,ID: ${chatId}, accessHash: ${accessHash}`)
+
+      const inputChannel = new Api.InputChannel({
+        channelId: chatId,
+        accessHash: accessHash
+      })
+
+      const shouldSendInit = typeof initMsg === 'string' && initMsg.trim().length > 0
+      if (shouldSendInit) {
+        try {
+          await client.sendMessage(inputChannel, {
+            message: initMsg.trim()
+          })
+          this.app.log.info('已向群组发送自定义欢迎消息')
+        } catch (e) {
+          this.app.log.warn('发送欢迎消息失败: ' + (e instanceof Error ? e.message : '未知错误'))
+        }
+      } else {
+        this.app.log.info('未提供 initMsg,跳过欢迎消息发送')
+      }
+
+      let publicLink: string | null = null
+      let inviteLinkPermanent: string | null = null
+      try {
+        const fullChannel = await client.invoke(
+          new Api.channels.GetFullChannel({
+            channel: inputChannel
+          })
+        )
+
+        const channel = fullChannel.chats?.[0] as any
+        let channelUsername: string | undefined = channel?.username
+
+        if (!channelUsername) {
+          const candidate = `group_${Date.now()}`
+          let desiredHandle = candidate.toLowerCase()
+          if (desiredHandle.length > 32) desiredHandle = desiredHandle.slice(0, 32)
+          if (desiredHandle.length < 5) desiredHandle = desiredHandle.padEnd(5, '0')
+
+          try {
+            await client.invoke(
+              new Api.channels.UpdateUsername({
+                channel: inputChannel,
+                username: desiredHandle
+              })
+            )
+            channelUsername = desiredHandle
+            this.app.log.info(`已为群设置用户名: ${channelUsername}`)
+          } catch (setError) {
+            const msg = setError instanceof Error ? setError.message : '未知错误'
+            this.app.log.warn(`为群设置用户名失败: ${msg}`)
+          }
+        }
+
+        if (channelUsername) {
+          publicLink = `https://t.me/${channelUsername}`
+          this.app.log.info(`群组公开链接(永久): ${publicLink}`)
+        }
+
+        try {
+          const inviteLink = await client.invoke(
+            new Api.messages.ExportChatInvite({
+              peer: inputChannel
+            })
+          )
+
+          const invite = inviteLink as any
+          if (invite?.link) {
+            inviteLinkPermanent = invite.link
+            this.app.log.info(`群组邀请链接(永久): ${inviteLinkPermanent}`)
+          } else {
+            this.app.log.warn('未能获取群组邀请链接')
+          }
+        } catch (inviteError) {
+          const msg = inviteError instanceof Error ? inviteError.message : '未知错误'
+          this.app.log.warn(`获取群组邀请链接失败: ${msg}`)
+        }
+      } catch (error) {
+        const errorMessage = error instanceof Error ? error.message : '未知错误'
+        this.app.log.warn(`获取群组链接失败: ${errorMessage}`)
+      }
+
+      try {
+        await this.chatGroupService.upsertGroup({
+          chatId: chatId.toString(),
+          accessHash: accessHash?.toString() || '',
+          name: createdChat.title || groupName,
+          groupType,
+          publicLink: publicLink || undefined,
+          inviteLink: inviteLinkPermanent || undefined,
+          senderId
+        })
+        this.app.log.info('群组信息已保存到数据库')
+      } catch (saveError) {
+        const msg = saveError instanceof Error ? saveError.message : '未知错误'
+        this.app.log.warn(`保存群组信息失败: ${msg}`)
+      }
+
+      return {
+        success: true,
+        message: '群组创建成功',
+        data: {
+          chatId: chatId.toString(),
+          chatTitle: groupName,
+          chatType: groupType,
+          groupLinkPublic: publicLink || null,
+          groupInviteLink: inviteLinkPermanent || null
+        }
+      }
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '未知错误'
+      this.app.log.error(`创建聊天群失败: ${errorMessage}`)
+      return {
+        success: false,
+        message: `创建聊天群失败: ${errorMessage}`
+      }
+    } finally {
+      if (client) {
+        try {
+          await this.tgClientService.disconnect()
+          this.app.log.info('TelegramClient 已断开连接')
+        } catch (error) {
+          const errorMessage = error instanceof Error ? error.message : '未知错误'
+          this.app.log.error(`断开连接失败: ${errorMessage}`)
+        }
+      }
+    }
+  }
+
+  async testInviteMembersToChat(
+    senderId: string,
+    chatId: string,
+    members: string[],
+    accessHash?: string
+  ): Promise<{
+    success: boolean
+    message: string
+    data?: { chatId: string; inviteResults: { total: number; success: number; failed: number; errors: string[] } }
+  }> {
+    let client: TelegramClient | null = null
+
+    try {
+      if (!members || members.length === 0) {
+        return { success: false, message: '成员列表不能为空' }
+      }
+
+      if (!chatId || !chatId.trim()) {
+        return { success: false, message: 'chatId 不能为空' }
+      }
+
+      const sessionResult = await this.resolveSenderSession(senderId)
+      if ('error' in sessionResult) {
+        return sessionResult.error
+      }
+      const { sessionString } = sessionResult
+
+      this.app.log.info('正在连接 TelegramClient...')
+      client = await this.tgClientService.connect(sessionString)
+      this.app.log.info('TelegramClient 连接完成')
+
+      const waitTime = Math.floor(Math.random() * 21) + 20
+      this.app.log.info(`连接成功后等待 ${waitTime} 秒,避免新 Session 被限制`)
+      await new Promise(resolve => setTimeout(resolve, waitTime * 1000))
+      this.app.log.info('等待完成,开始邀请成员')
+
+      let successCount = 0
+      let failedCount = 0
+      const errors: string[] = []
+
+      const chatTarget = chatId.trim()
+      const accessHashTrimmed = accessHash?.trim()
+      const channelIdWithHashMatch = chatTarget.match(/^(-?\d+)[,:#](\d+)$/)
+      let chatEntity: any
+      try {
+        if (channelIdWithHashMatch) {
+          const [, idStr, accessHashStr] = channelIdWithHashMatch
+          chatEntity = {
+            id: BigInt(idStr),
+            accessHash: BigInt(accessHashStr),
+            className: 'Channel',
+            title: chatTarget
+          }
+          this.app.log.info('已使用显式 channelId + accessHash 初始化频道实体')
+        } else if (accessHashTrimmed && /^-?\d+$/.test(chatTarget)) {
+          chatEntity = {
+            id: BigInt(chatTarget),
+            accessHash: BigInt(accessHashTrimmed),
+            className: 'Channel',
+            title: chatTarget
+          }
+          this.app.log.info('已使用 chatId + accessHash 初始化频道实体')
+        } else {
+          const parsedChatId = /^-?\d+$/.test(chatTarget) ? Number(chatTarget) : chatTarget
+          chatEntity = await client.getEntity(parsedChatId)
+        }
+      } catch (error) {
+        if (/^\d+$/.test(chatTarget)) {
+          try {
+            const prefixedChannelId = BigInt(`-100${chatTarget}`)
+            chatEntity = await client.getEntity(prefixedChannelId.toString())
+            this.app.log.info(`通过 -100 前缀重试获取群组成功: ${prefixedChannelId.toString()}`)
+          } catch (error2) {
+            const errorMessage2 = error2 instanceof Error ? error2.message : '未知错误'
+            this.app.log.error(`获取群组信息失败(二次尝试): ${errorMessage2}`)
+            return { success: false, message: `获取群组信息失败: ${errorMessage2}` }
+          }
+        } else {
+          const errorMessage = error instanceof Error ? error.message : '未知错误'
+          this.app.log.error(`获取群组信息失败: ${errorMessage}`)
+          return { success: false, message: `获取群组信息失败: ${errorMessage}` }
+        }
+      }
+
+      this.app.log.info(
+        `已获取群组信息: ${chatEntity.title || chatEntity.username || chatTarget} (class: ${chatEntity.className})`
+      )
+
+      const isChannel = chatEntity?.className === 'Channel'
+      const isChat = chatEntity?.className === 'Chat'
+      if (!isChannel && !isChat) {
+        return { success: false, message: '目标并非群组或频道,无法邀请成员' }
+      }
+
+      const inviteToGroup = async (inputUser: Api.InputUser) => {
+        if (isChannel) {
+          if (!chatEntity?.accessHash) {
+            throw new Error('缺少 accessHash,无法邀请到频道/超级群组')
+          }
+          const inputChannel = new Api.InputChannel({
+            channelId: chatEntity.id,
+            accessHash: chatEntity.accessHash
+          })
+          await client!.invoke(
+            new Api.channels.InviteToChannel({
+              channel: inputChannel,
+              users: [inputUser]
+            })
+          )
+        } else {
+          await client!.invoke(
+            new Api.messages.AddChatUser({
+              chatId: chatEntity.id,
+              userId: inputUser,
+              fwdLimit: 0
+            })
+          )
+        }
+      }
+
+      for (let i = 0; i < members.length; i++) {
+        const member = members[i]
+        let lastError: string | null = null
+
+        this.app.log.info(`正在邀请第 ${i + 1}/${members.length} 个成员: ${member}`)
+
+        try {
+          const parsedTarget = this.parseTarget(member)
+          if (!parsedTarget) {
+            throw new Error(`成员格式错误: ${member}`)
+          }
+
+          let targetUser: any
+          try {
+            targetUser = await this.tgClientService.getTargetPeer(client, parsedTarget)
+            if (!targetUser) {
+              throw new Error('无法获取用户信息')
+            }
+          } catch (error) {
+            throw new Error(`获取用户信息失败: ${error instanceof Error ? error.message : '未知错误'}`)
+          }
+
+          const inputUser = new Api.InputUser({
+            userId: targetUser.id,
+            accessHash: targetUser.accessHash || BigInt(0)
+          })
+
+          try {
+            await inviteToGroup(inputUser)
+            this.app.log.info(`成功邀请成员: ${member}`)
+            successCount++
+          } catch (inviteError) {
+            const inviteErrorMessage = inviteError instanceof Error ? inviteError.message : '未知错误'
+
+            if (inviteErrorMessage.includes('USER_ALREADY_PARTICIPANT')) {
+              this.app.log.info(`成员已在群组中: ${member}`)
+              successCount++
+            } else if (inviteErrorMessage.includes('PEER_FLOOD')) {
+              throw new Error('邀请频率过快,触发 PEER_FLOOD 限制,请降低邀请频率')
+            } else if (inviteErrorMessage.includes('USER_PRIVACY_RESTRICTED')) {
+              throw new Error('用户隐私设置不允许被邀请')
+            } else if (inviteErrorMessage.includes('USER_NOT_MUTUAL_CONTACT')) {
+              throw new Error('用户不是双向联系人,无法邀请')
+            } else if (inviteErrorMessage.includes('USER_CHANNELS_TOO_MUCH')) {
+              throw new Error('用户加入的群组/频道数量已达上限')
+            } else if (inviteErrorMessage.includes('INVITE_REQUEST_SENT')) {
+              this.app.log.info(`已向用户发送邀请请求: ${member}`)
+              successCount++
+            } else {
+              throw inviteError
+            }
+          }
+        } catch (error) {
+          const errorMessage = error instanceof Error ? error.message : '未知错误'
+          const errorDetail = `${member}: ${errorMessage}`
+          this.app.log.error(`邀请成员失败: ${errorDetail}`)
+          errors.push(errorDetail)
+          failedCount++
+          lastError = errorMessage
+        } finally {
+          if (i < members.length - 1) {
+            let delayTime: number
+            let delayReason: string
+
+            if (lastError) {
+              if (
+                lastError.includes('FLOOD_WAIT') ||
+                lastError.includes('SLOWMODE_WAIT') ||
+                lastError.includes('PEER_FLOOD') ||
+                lastError.includes('Too Many Requests')
+              ) {
+                delayTime = 120
+                delayReason = `检测到限制(${lastError.includes('PEER_FLOOD') ? 'PEER_FLOOD' : 'FLOOD_WAIT'})`
+              } else {
+                delayTime = Math.floor(Math.random() * 11) + 30
+                delayReason = '普通错误后等待'
+              }
+            } else {
+              delayTime = Math.floor(Math.random() * 11) + 30
+              delayReason = '避免 Peer Flood'
+            }
+
+            this.app.log.info(`${delayReason},等待 ${delayTime} 秒后继续邀请下一个成员`)
+            await new Promise(resolve => setTimeout(resolve, delayTime * 1000))
+          }
+        }
+      }
+
+      try {
+        await this.chatGroupService.upsertGroup({
+          chatId: chatEntity.id.toString(),
+          accessHash: (chatEntity.accessHash || '').toString(),
+          name: chatEntity.title || chatEntity.username || chatTarget,
+          groupType: isChannel ? 'channel' : 'chat',
+          senderId
+        })
+      } catch (saveError) {
+        const msg = saveError instanceof Error ? saveError.message : '未知错误'
+        this.app.log.warn(`保存群组信息失败: ${msg}`)
+      }
+
+      return {
+        success: true,
+        message: '邀请成员完成',
+        data: {
+          chatId: chatTarget,
+          inviteResults: {
+            total: members.length,
+            success: successCount,
+            failed: failedCount,
+            errors
+          }
+        }
+      }
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '未知错误'
+      this.app.log.error(`邀请成员到群失败: ${errorMessage}`)
+      return { success: false, message: `邀请成员失败: ${errorMessage}` }
+    } finally {
+      if (client) {
+        try {
+          await this.tgClientService.disconnect()
+          this.app.log.info('TelegramClient 已断开连接')
+        } catch (error) {
+          const errorMessage = error instanceof Error ? error.message : '未知错误'
+          this.app.log.error(`断开连接失败: ${errorMessage}`)
+        }
+      }
+    }
+  }
+
+  private async resolveSenderSession(
+    senderId: string,
+    session?: string,
+    dcId?: number,
+    authKey?: string
+  ): Promise<{ sender: Sender; sessionString: string } | { error: { success: false; message: string } }> {
+    if ((dcId !== undefined && authKey === undefined) || (dcId === undefined && authKey !== undefined)) {
+      return {
+        error: {
+          success: false,
+          message: 'dcId 和 authKey 必须同时传参'
+        }
+      }
+    }
+
+    let sender: Sender | null = null
+    let sessionString: string | null = null
+
+    if (session) {
+      try {
+        sessionString = buildStringSession(session)
+      } catch (error) {
+        const errorMessage = error instanceof Error ? error.message : '未知错误'
+        return {
+          error: {
+            success: false,
+            message: `解析 session 失败: ${errorMessage}`
+          }
+        }
+      }
+
+      const existingSender = await this.senderRepository.findOne({ where: { id: senderId } })
+      if (existingSender) {
+        await this.senderRepository.update(senderId, { sessionStr: sessionString })
+        sender = await this.senderRepository.findOne({ where: { id: senderId } })
+      } else {
+        sender = await this.senderService.create(senderId, undefined, undefined, sessionString)
+      }
+
+      if (!sender) {
+        return {
+          error: {
+            success: false,
+            message: '创建或更新 sender 失败'
+          }
+        }
+      }
+    } else if (dcId !== undefined && authKey !== undefined) {
+      try {
+        sessionString = buildStringSessionByDcIdAndAuthKey(dcId, authKey)
+      } catch (error) {
+        const errorMessage = error instanceof Error ? error.message : '未知错误'
+        return {
+          error: {
+            success: false,
+            message: `解析 session 失败: ${errorMessage}`
+          }
+        }
+      }
+
+      const existingSender = await this.senderRepository.findOne({ where: { id: senderId } })
+      if (existingSender) {
+        await this.senderRepository.update(senderId, { dcId, authKey, sessionStr: sessionString })
+        sender = await this.senderRepository.findOne({ where: { id: senderId } })
+      } else {
+        sender = await this.senderService.create(senderId, dcId, authKey, sessionString)
+      }
+
+      if (!sender) {
+        return {
+          error: {
+            success: false,
+            message: '创建或更新 sender 失败'
+          }
+        }
+      }
+    } else {
+      sender = await this.senderRepository.findOne({ where: { id: senderId, delFlag: false } })
+      if (!sender) {
+        return {
+          error: {
+            success: false,
+            message: '发送账号不存在或已被删除'
+          }
+        }
+      }
+    }
+
+    if (!sender) {
+      return {
+        error: {
+          success: false,
+          message: '发送账号不存在或已被删除'
+        }
+      }
+    }
+
+    if (!sessionString) {
+      sessionString = sender.sessionStr
+      if (!sessionString) {
+        if (!sender.dcId || !sender.authKey) {
+          return {
+            error: {
+              success: false,
+              message: 'Sender 缺少 dcId 或 authKey 信息'
+            }
+          }
+        }
+        sessionString = buildStringSessionByDcIdAndAuthKey(sender.dcId, sender.authKey)
+        await this.senderRepository.update(sender.id, { sessionStr: sessionString })
+      }
+    }
+
+    if (!sessionString) {
+      return {
+        error: {
+          success: false,
+          message: '无法生成 session 信息'
+        }
+      }
+    }
+
+    return {
+      sender,
+      sessionString
+    }
+  }
+
+  private parseTarget(targetId: string): string | number | null {
+    const trimmed = targetId.trim()
+
+    if (trimmed.startsWith('@') || trimmed.startsWith('+')) {
+      return trimmed
+    }
+
+    const phoneRegex = /^\d+$/
+    if (phoneRegex.test(trimmed)) {
+      return `+${trimmed}`
+    }
+
+    const integerRegex = /^-?\d+$/
+    if (integerRegex.test(trimmed)) {
+      return Number(trimmed)
+    }
+
+    return null
+  }
+
+  private async checkCanSendMessage(client: TelegramClient, targetPeer: any): Promise<boolean> {
+    try {
+      const fullUser = await client.invoke(
+        new Api.users.GetFullUser({
+          id: targetPeer
+        })
+      )
+
+      const fullUserData = fullUser.fullUser as any
+
+      if (fullUserData?.blocked) {
+        return false
+      }
+
+      if (targetPeer.bot && targetPeer.botChatHistory === false) {
+        return false
+      }
+
+      if (targetPeer.deleted) {
+        return false
+      }
+
+      if (targetPeer.fake || targetPeer.scam) {
+        return false
+      }
+
+      return true
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '未知错误'
+
+      if (errorMessage.includes('AUTH_KEY_UNREGISTERED')) {
+        throw new Error('认证密钥未注册,请检查 session 是否有效或需要重新授权')
+      }
+
+      if (errorMessage.includes('PRIVACY') || errorMessage.includes('USER_PRIVACY_RESTRICTED')) {
+        return false
+      }
+
+      return true
+    }
+  }
+}

Некоторые файлы не были показаны из-за большого количества измененных файлов