CommentService.ts 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659
  1. import { AppDataSource, Comment, PlatformAccount, Work } from '../models/index.js';
  2. import { AppError } from '../middleware/error.js';
  3. import { ERROR_CODES, HTTP_STATUS, WS_EVENTS } from '@media-manager/shared';
  4. import type {
  5. Comment as CommentType,
  6. CommentStats,
  7. PaginatedData,
  8. PlatformType,
  9. } from '@media-manager/shared';
  10. import { wsManager } from '../websocket/index.js';
  11. import { headlessBrowserService, type WorkComments, type CookieData } from './HeadlessBrowserService.js';
  12. import { CookieManager } from '../automation/cookie.js';
  13. import { logger } from '../utils/logger.js';
  14. interface GetCommentsParams {
  15. page: number;
  16. pageSize: number;
  17. accountId?: number;
  18. workId?: number;
  19. platform?: string;
  20. isRead?: boolean;
  21. keyword?: string;
  22. }
  23. export class CommentService {
  24. private commentRepository = AppDataSource.getRepository(Comment);
  25. async getComments(userId: number, params: GetCommentsParams): Promise<PaginatedData<CommentType>> {
  26. const { page, pageSize, accountId, workId, platform, isRead, keyword } = params;
  27. const skip = (page - 1) * pageSize;
  28. const queryBuilder = this.commentRepository
  29. .createQueryBuilder('comment')
  30. .where('comment.userId = :userId', { userId });
  31. if (accountId) {
  32. queryBuilder.andWhere('comment.accountId = :accountId', { accountId });
  33. }
  34. if (workId) {
  35. // 直接使用 workId 查询
  36. queryBuilder.andWhere('comment.workId = :workId', { workId });
  37. logger.info(`Querying comments for workId: ${workId}`);
  38. }
  39. if (platform) {
  40. queryBuilder.andWhere('comment.platform = :platform', { platform });
  41. }
  42. if (isRead !== undefined) {
  43. queryBuilder.andWhere('comment.isRead = :isRead', { isRead });
  44. }
  45. if (keyword) {
  46. queryBuilder.andWhere(
  47. '(comment.content LIKE :keyword OR comment.authorName LIKE :keyword)',
  48. { keyword: `%${keyword}%` }
  49. );
  50. }
  51. // 打印查询 SQL 用于调试
  52. logger.info(`Comment query: userId=${userId}, accountId=${accountId}, workId=${workId}, platform=${platform}`);
  53. const [comments, total] = await queryBuilder
  54. .orderBy('comment.commentTime', 'DESC')
  55. .skip(skip)
  56. .take(pageSize)
  57. .getManyAndCount();
  58. logger.info(`Found ${total} comments`);
  59. return {
  60. items: comments.map(this.formatComment),
  61. total,
  62. page,
  63. pageSize,
  64. totalPages: Math.ceil(total / pageSize),
  65. };
  66. }
  67. async getStats(userId: number): Promise<CommentStats> {
  68. const totalCount = await this.commentRepository.count({ where: { userId } });
  69. const unreadCount = await this.commentRepository.count({ where: { userId, isRead: false } });
  70. const unrepliedCount = await this.commentRepository
  71. .createQueryBuilder('comment')
  72. .where('comment.userId = :userId', { userId })
  73. .andWhere('comment.replyContent IS NULL')
  74. .getCount();
  75. const today = new Date();
  76. today.setHours(0, 0, 0, 0);
  77. const todayCount = await this.commentRepository
  78. .createQueryBuilder('comment')
  79. .where('comment.userId = :userId', { userId })
  80. .andWhere('comment.createdAt >= :today', { today })
  81. .getCount();
  82. return { totalCount, unreadCount, unrepliedCount, todayCount };
  83. }
  84. async markAsRead(userId: number, commentIds: number[]): Promise<void> {
  85. await this.commentRepository
  86. .createQueryBuilder()
  87. .update(Comment)
  88. .set({ isRead: true })
  89. .where('userId = :userId AND id IN (:...commentIds)', { userId, commentIds })
  90. .execute();
  91. }
  92. async replyComment(userId: number, commentId: number, content: string): Promise<CommentType> {
  93. const comment = await this.commentRepository.findOne({
  94. where: { id: commentId, userId },
  95. });
  96. if (!comment) {
  97. throw new AppError('评论不存在', HTTP_STATUS.NOT_FOUND, ERROR_CODES.COMMENT_NOT_FOUND);
  98. }
  99. // TODO: 调用平台适配器发送回复
  100. // 这里只更新本地记录
  101. await this.commentRepository.update(commentId, {
  102. replyContent: content,
  103. repliedAt: new Date(),
  104. isRead: true,
  105. });
  106. const updated = await this.commentRepository.findOne({ where: { id: commentId } });
  107. wsManager.sendToUser(userId, WS_EVENTS.COMMENT_REPLIED, {
  108. comment: this.formatComment(updated!),
  109. });
  110. return this.formatComment(updated!);
  111. }
  112. async batchReply(
  113. userId: number,
  114. commentIds: number[],
  115. content: string
  116. ): Promise<{ success: number; failed: number }> {
  117. let success = 0;
  118. let failed = 0;
  119. for (const commentId of commentIds) {
  120. try {
  121. await this.replyComment(userId, commentId, content);
  122. success++;
  123. } catch {
  124. failed++;
  125. }
  126. }
  127. return { success, failed };
  128. }
  129. /**
  130. * 异步同步评论(后台执行,通过 WebSocket 通知结果)
  131. */
  132. syncCommentsAsync(userId: number, accountId?: number): void {
  133. // 通知用户同步已开始 - 直接使用字符串,并在 payload 中加入 event 字段
  134. wsManager.sendToUser(userId, 'comment:sync_started', {
  135. event: 'sync_started',
  136. accountId,
  137. message: '正在同步评论...',
  138. });
  139. // 进度回调:在处理每个作品时发送进度更新
  140. const onProgress = (current: number, total: number, workTitle: string) => {
  141. const progress = total > 0 ? Math.round((current / total) * 100) : 0;
  142. wsManager.sendToUser(userId, 'comment:sync_progress', {
  143. event: 'sync_progress',
  144. accountId,
  145. current,
  146. total,
  147. progress,
  148. workTitle,
  149. message: `正在同步: ${workTitle || `作品 ${current}/${total}`}`,
  150. });
  151. };
  152. // 后台执行同步任务
  153. this.syncComments(userId, accountId, onProgress)
  154. .then((result) => {
  155. logger.info(`Comment sync completed: synced ${result.synced} comments from ${result.accounts} accounts`);
  156. // 同步完成,通知用户
  157. wsManager.sendToUser(userId, 'comment:synced', {
  158. event: 'synced',
  159. accountId,
  160. syncedCount: result.synced,
  161. accountCount: result.accounts,
  162. message: `同步完成,共同步 ${result.synced} 条评论`,
  163. });
  164. })
  165. .catch((error) => {
  166. logger.error('Comment sync failed:', error);
  167. // 同步失败,通知用户
  168. wsManager.sendToUser(userId, 'comment:sync_failed', {
  169. event: 'sync_failed',
  170. accountId,
  171. message: error instanceof Error ? error.message : '同步失败,请稍后重试',
  172. });
  173. });
  174. }
  175. /**
  176. * 同步指定账号的评论
  177. * @param onProgress 进度回调 (current, total, workTitle)
  178. */
  179. async syncComments(
  180. userId: number,
  181. accountId?: number,
  182. onProgress?: (current: number, total: number, workTitle: string) => void
  183. ): Promise<{ synced: number; accounts: number }> {
  184. const accountRepository = AppDataSource.getRepository(PlatformAccount);
  185. // 获取需要同步的账号列表
  186. const whereCondition: { userId: number; id?: number; platform?: string } = { userId };
  187. if (accountId) {
  188. whereCondition.id = accountId;
  189. }
  190. const accounts = await accountRepository.find({ where: whereCondition });
  191. if (accounts.length === 0) {
  192. throw new AppError('没有找到可同步的账号', HTTP_STATUS.NOT_FOUND, ERROR_CODES.ACCOUNT_NOT_FOUND);
  193. }
  194. let totalSynced = 0;
  195. let syncedAccounts = 0;
  196. for (const account of accounts) {
  197. try {
  198. // 只处理支持的平台
  199. if (account.platform !== 'douyin' && account.platform !== 'xiaohongshu' && account.platform !== 'weixin_video') {
  200. logger.info(`Skipping unsupported platform: ${account.platform}`);
  201. continue;
  202. }
  203. // 解密 Cookie
  204. if (!account.cookieData) {
  205. logger.warn(`Account ${account.id} has no cookies`);
  206. continue;
  207. }
  208. let decryptedCookies: string;
  209. try {
  210. decryptedCookies = CookieManager.decrypt(account.cookieData);
  211. } catch {
  212. decryptedCookies = account.cookieData;
  213. }
  214. // 解析 Cookie - 支持两种格式
  215. let cookies: CookieData[];
  216. try {
  217. // 先尝试 JSON 格式
  218. cookies = JSON.parse(decryptedCookies);
  219. } catch {
  220. // 如果 JSON 解析失败,尝试解析 "name=value; name2=value2" 格式
  221. cookies = this.parseCookieString(decryptedCookies, account.platform);
  222. if (cookies.length === 0) {
  223. logger.error(`Invalid cookie format for account ${account.id}`);
  224. continue;
  225. }
  226. }
  227. // 获取评论数据 - 根据平台类型调用不同方法
  228. logger.info(`Syncing comments for account ${account.id} (${account.platform})...`);
  229. let workComments: Array<{
  230. videoId: string;
  231. videoTitle: string;
  232. comments: Array<{
  233. commentId: string;
  234. authorId: string;
  235. authorName: string;
  236. authorAvatar: string;
  237. content: string;
  238. likeCount: number;
  239. commentTime: string;
  240. replyCount?: number;
  241. parentCommentId?: string;
  242. }>;
  243. }> = [];
  244. if (account.platform === 'douyin') {
  245. workComments = await headlessBrowserService.fetchDouyinCommentsViaApi(cookies);
  246. } else if (account.platform === 'xiaohongshu') {
  247. workComments = await headlessBrowserService.fetchXiaohongshuCommentsViaApi(cookies);
  248. } else if (account.platform === 'weixin_video') {
  249. workComments = await headlessBrowserService.fetchWeixinVideoCommentsViaApi(cookies);
  250. }
  251. // 获取该账号的所有作品,用于关联
  252. const workRepository = AppDataSource.getRepository(Work);
  253. const accountWorks = await workRepository.find({
  254. where: { userId, accountId: account.id },
  255. });
  256. logger.info(`Found ${accountWorks.length} works for account ${account.id}`);
  257. // 保存评论到数据库
  258. let accountSynced = 0;
  259. const totalWorks = workComments.length;
  260. // 创建 platformVideoId -> workId 的快速映射
  261. const videoIdToWorkMap = new Map<string, { id: number; title: string }>();
  262. for (const work of accountWorks) {
  263. if (work.platformVideoId) {
  264. videoIdToWorkMap.set(work.platformVideoId, { id: work.id, title: work.title });
  265. // 同时存储不带前缀的版本(如果 platformVideoId 是 "douyin_xxx" 格式)
  266. if (work.platformVideoId.includes('_')) {
  267. const parts = work.platformVideoId.split('_');
  268. if (parts.length >= 2) {
  269. videoIdToWorkMap.set(parts.slice(1).join('_'), { id: work.id, title: work.title });
  270. }
  271. }
  272. }
  273. }
  274. logger.info(`Created videoId mapping with ${videoIdToWorkMap.size} entries`);
  275. for (let workIndex = 0; workIndex < workComments.length; workIndex++) {
  276. const workComment = workComments[workIndex];
  277. // 发送进度更新
  278. if (onProgress) {
  279. onProgress(workIndex + 1, totalWorks, workComment.videoTitle || `作品 ${workIndex + 1}`);
  280. }
  281. let workId: number | null = null;
  282. const commentVideoId = workComment.videoId?.toString() || '';
  283. const commentVideoTitle = workComment.videoTitle?.trim() || '';
  284. logger.info(`Trying to match work for videoId: "${commentVideoId}", title: "${commentVideoTitle}"`);
  285. // 1. 【首选】通过 platformVideoId (aweme_id) 匹配 - 最可靠的方式
  286. if (commentVideoId) {
  287. // 直接匹配
  288. if (videoIdToWorkMap.has(commentVideoId)) {
  289. const matched = videoIdToWorkMap.get(commentVideoId)!;
  290. workId = matched.id;
  291. logger.info(`Matched work by videoId: ${commentVideoId} -> workId: ${workId}, title: "${matched.title}"`);
  292. }
  293. // 尝试带平台前缀匹配
  294. if (!workId) {
  295. const prefixedId = `douyin_${commentVideoId}`;
  296. if (videoIdToWorkMap.has(prefixedId)) {
  297. const matched = videoIdToWorkMap.get(prefixedId)!;
  298. workId = matched.id;
  299. logger.info(`Matched work by prefixed videoId: ${prefixedId} -> workId: ${workId}`);
  300. }
  301. }
  302. // 遍历匹配(处理各种格式)
  303. if (!workId) {
  304. const matchedWork = accountWorks.find(w => {
  305. if (!w.platformVideoId) return false;
  306. // 尝试各种匹配方式
  307. return w.platformVideoId === commentVideoId ||
  308. w.platformVideoId === `douyin_${commentVideoId}` ||
  309. w.platformVideoId.endsWith(`_${commentVideoId}`) ||
  310. w.platformVideoId.includes(commentVideoId);
  311. });
  312. if (matchedWork) {
  313. workId = matchedWork.id;
  314. logger.info(`Matched work by videoId iteration: ${commentVideoId} -> workId: ${workId}, platformVideoId: ${matchedWork.platformVideoId}`);
  315. }
  316. }
  317. }
  318. // 2. 通过标题精确匹配
  319. if (!workId && commentVideoTitle) {
  320. let matchedWork = accountWorks.find(w => {
  321. if (!w.title) return false;
  322. return w.title.trim() === commentVideoTitle;
  323. });
  324. // 去除空白字符后匹配
  325. if (!matchedWork) {
  326. const normalizedCommentTitle = commentVideoTitle.replace(/\s+/g, '');
  327. matchedWork = accountWorks.find(w => {
  328. if (!w.title) return false;
  329. return w.title.trim().replace(/\s+/g, '') === normalizedCommentTitle;
  330. });
  331. }
  332. // 包含匹配
  333. if (!matchedWork) {
  334. matchedWork = accountWorks.find(w => {
  335. if (!w.title) return false;
  336. const workTitle = w.title.trim();
  337. const shortCommentTitle = commentVideoTitle.slice(0, 50);
  338. const shortWorkTitle = workTitle.slice(0, 50);
  339. return workTitle.includes(shortCommentTitle) ||
  340. commentVideoTitle.includes(shortWorkTitle) ||
  341. shortWorkTitle.includes(shortCommentTitle) ||
  342. shortCommentTitle.includes(shortWorkTitle);
  343. });
  344. }
  345. // 模糊匹配
  346. if (!matchedWork) {
  347. const normalizeTitle = (title: string) => {
  348. return title.replace(/[^\u4e00-\u9fa5a-zA-Z0-9]/g, '').toLowerCase();
  349. };
  350. const normalizedCommentTitle = normalizeTitle(commentVideoTitle);
  351. matchedWork = accountWorks.find(w => {
  352. if (!w.title) return false;
  353. const normalizedWorkTitle = normalizeTitle(w.title);
  354. return normalizedWorkTitle.slice(0, 40) === normalizedCommentTitle.slice(0, 40) ||
  355. normalizedWorkTitle.includes(normalizedCommentTitle.slice(0, 30)) ||
  356. normalizedCommentTitle.includes(normalizedWorkTitle.slice(0, 30));
  357. });
  358. }
  359. if (matchedWork) {
  360. workId = matchedWork.id;
  361. logger.info(`Matched work by title: "${matchedWork.title}" -> workId: ${workId}`);
  362. }
  363. }
  364. // 3. 如果只有一个作品,直接关联
  365. if (!workId && accountWorks.length === 1) {
  366. workId = accountWorks[0].id;
  367. logger.info(`Only one work, using default: workId: ${workId}`);
  368. }
  369. logger.info(`Final work mapping: videoId="${commentVideoId}", title="${commentVideoTitle}", workId=${workId}`);
  370. for (const comment of workComment.comments) {
  371. try {
  372. // 过滤无效评论内容 - 放宽限制,只过滤纯操作按钮文本
  373. if (!comment.content ||
  374. /^(回复|删除|举报|点赞|分享|收藏)$/.test(comment.content.trim())) {
  375. logger.debug(`Skipping invalid comment content: ${comment.content}`);
  376. continue;
  377. }
  378. // 检查评论是否已存在(基于内容+作者+账号的去重)
  379. const existing = await this.commentRepository
  380. .createQueryBuilder('comment')
  381. .where('comment.accountId = :accountId', { accountId: account.id })
  382. .andWhere('comment.authorName = :authorName', { authorName: comment.authorName })
  383. .andWhere('comment.content = :content', { content: comment.content })
  384. .getOne();
  385. if (!existing) {
  386. const newComment = this.commentRepository.create({
  387. userId,
  388. accountId: account.id,
  389. workId, // 关联作品 ID
  390. platform: account.platform as PlatformType,
  391. videoId: workComment.videoId,
  392. commentId: comment.commentId,
  393. authorId: comment.authorId,
  394. authorName: comment.authorName,
  395. authorAvatar: comment.authorAvatar,
  396. content: comment.content,
  397. likeCount: comment.likeCount,
  398. commentTime: comment.commentTime ? new Date(comment.commentTime) : new Date(),
  399. isRead: false,
  400. isTop: false,
  401. });
  402. await this.commentRepository.save(newComment);
  403. accountSynced++;
  404. logger.info(`Saved comment: "${comment.content.slice(0, 30)}..." -> workId: ${workId}`);
  405. } else {
  406. // 如果评论已存在但没有 workId,更新它
  407. if (!existing.workId && workId) {
  408. await this.commentRepository.update(existing.id, { workId });
  409. logger.info(`Updated existing comment workId: ${existing.id} -> ${workId}`);
  410. }
  411. }
  412. } catch (saveError) {
  413. logger.warn(`Failed to save comment ${comment.commentId}:`, saveError);
  414. }
  415. }
  416. }
  417. if (accountSynced > 0) {
  418. totalSynced += accountSynced;
  419. syncedAccounts++;
  420. logger.info(`Synced ${accountSynced} comments for account ${account.id}`);
  421. // 注意:不在这里发送 COMMENT_SYNCED,而是由 syncCommentsAsync 统一发送
  422. }
  423. } catch (accountError) {
  424. logger.error(`Failed to sync comments for account ${account.id}:`, accountError);
  425. }
  426. }
  427. // 尝试修复没有 workId 的现有评论
  428. await this.fixOrphanedComments(userId);
  429. return { synced: totalSynced, accounts: syncedAccounts };
  430. }
  431. /**
  432. * 修复没有 workId 的评论
  433. */
  434. private async fixOrphanedComments(userId: number): Promise<void> {
  435. try {
  436. const workRepository = AppDataSource.getRepository(Work);
  437. // 获取所有没有 workId 的评论
  438. const orphanedComments = await this.commentRepository.find({
  439. where: { userId, workId: undefined as unknown as number },
  440. });
  441. if (orphanedComments.length === 0) return;
  442. logger.info(`Found ${orphanedComments.length} comments without workId, trying to fix...`);
  443. // 获取用户的所有作品
  444. const works = await workRepository.find({ where: { userId } });
  445. // 创建多种格式的 videoId -> workId 映射
  446. const videoIdToWork = new Map<string, { id: number; title: string }>();
  447. for (const work of works) {
  448. if (work.platformVideoId) {
  449. // 存储原始 platformVideoId
  450. videoIdToWork.set(work.platformVideoId, { id: work.id, title: work.title });
  451. // 如果是 "douyin_xxx" 格式,也存储纯 ID
  452. if (work.platformVideoId.startsWith('douyin_')) {
  453. const pureId = work.platformVideoId.replace('douyin_', '');
  454. videoIdToWork.set(pureId, { id: work.id, title: work.title });
  455. }
  456. // 如果是纯数字 ID,也存储带前缀的版本
  457. if (/^\d+$/.test(work.platformVideoId)) {
  458. videoIdToWork.set(`douyin_${work.platformVideoId}`, { id: work.id, title: work.title });
  459. }
  460. }
  461. }
  462. let fixedCount = 0;
  463. for (const comment of orphanedComments) {
  464. let matchedWorkId: number | null = null;
  465. // 1. 尝试通过 videoId 精确匹配
  466. if (comment.videoId) {
  467. if (videoIdToWork.has(comment.videoId)) {
  468. matchedWorkId = videoIdToWork.get(comment.videoId)!.id;
  469. }
  470. // 尝试带前缀匹配
  471. if (!matchedWorkId) {
  472. const prefixedId = `douyin_${comment.videoId}`;
  473. if (videoIdToWork.has(prefixedId)) {
  474. matchedWorkId = videoIdToWork.get(prefixedId)!.id;
  475. }
  476. }
  477. // 尝试去掉前缀匹配
  478. if (!matchedWorkId && comment.videoId.includes('_')) {
  479. const pureId = comment.videoId.split('_').pop()!;
  480. if (videoIdToWork.has(pureId)) {
  481. matchedWorkId = videoIdToWork.get(pureId)!.id;
  482. }
  483. }
  484. // 遍历查找包含关系
  485. if (!matchedWorkId) {
  486. const matchedWork = works.find(w =>
  487. w.platformVideoId?.includes(comment.videoId!) ||
  488. comment.videoId!.includes(w.platformVideoId || '')
  489. );
  490. if (matchedWork) {
  491. matchedWorkId = matchedWork.id;
  492. }
  493. }
  494. }
  495. // 2. 尝试通过账号匹配(如果该账号只有一个作品)
  496. if (!matchedWorkId) {
  497. const accountWorks = works.filter(w => w.accountId === comment.accountId);
  498. if (accountWorks.length === 1) {
  499. matchedWorkId = accountWorks[0].id;
  500. }
  501. }
  502. if (matchedWorkId) {
  503. await this.commentRepository.update(comment.id, { workId: matchedWorkId });
  504. fixedCount++;
  505. logger.info(`Fixed comment ${comment.id} (videoId: ${comment.videoId}) -> workId: ${matchedWorkId}`);
  506. }
  507. }
  508. logger.info(`Fixed ${fixedCount}/${orphanedComments.length} orphaned comments`);
  509. } catch (error) {
  510. logger.warn('Failed to fix orphaned comments:', error);
  511. }
  512. }
  513. /**
  514. * 将 cookie 字符串解析为 cookie 列表
  515. */
  516. private parseCookieString(cookieString: string, platform: string): CookieData[] {
  517. // 获取平台对应的域名
  518. const domainMap: Record<string, string> = {
  519. douyin: '.douyin.com',
  520. kuaishou: '.kuaishou.com',
  521. xiaohongshu: '.xiaohongshu.com',
  522. weixin_video: '.qq.com',
  523. bilibili: '.bilibili.com',
  524. toutiao: '.toutiao.com',
  525. baijiahao: '.baidu.com',
  526. qie: '.qq.com',
  527. dayuhao: '.alibaba.com',
  528. };
  529. const domain = domainMap[platform] || `.${platform}.com`;
  530. // 解析 "name=value; name2=value2" 格式的 cookie 字符串
  531. const cookies: CookieData[] = [];
  532. const pairs = cookieString.split(';');
  533. for (const pair of pairs) {
  534. const trimmed = pair.trim();
  535. if (!trimmed) continue;
  536. const eqIndex = trimmed.indexOf('=');
  537. if (eqIndex === -1) continue;
  538. const name = trimmed.substring(0, eqIndex).trim();
  539. const value = trimmed.substring(eqIndex + 1).trim();
  540. if (name && value) {
  541. cookies.push({
  542. name,
  543. value,
  544. domain,
  545. path: '/',
  546. });
  547. }
  548. }
  549. return cookies;
  550. }
  551. private formatComment(comment: Comment): CommentType {
  552. return {
  553. id: comment.id,
  554. userId: comment.userId,
  555. accountId: comment.accountId,
  556. workId: comment.workId || undefined,
  557. platform: comment.platform!,
  558. videoId: comment.videoId || '',
  559. platformVideoUrl: comment.platformVideoUrl,
  560. commentId: comment.commentId,
  561. parentCommentId: comment.parentCommentId,
  562. authorId: comment.authorId || '',
  563. authorName: comment.authorName || '',
  564. authorAvatar: comment.authorAvatar,
  565. content: comment.content || '',
  566. likeCount: comment.likeCount,
  567. replyContent: comment.replyContent,
  568. repliedAt: comment.repliedAt?.toISOString() || null,
  569. isRead: comment.isRead,
  570. isTop: comment.isTop,
  571. commentTime: comment.commentTime?.toISOString() || '',
  572. createdAt: comment.createdAt.toISOString(),
  573. };
  574. }
  575. }