@xpert-ai/plugin-community-wechat 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +353 -0
  2. package/dist/index.d.ts +20 -0
  3. package/dist/index.js +139 -0
  4. package/dist/lib/constants.d.ts +23 -0
  5. package/dist/lib/constants.js +23 -0
  6. package/dist/lib/conversation-user-key.d.ts +13 -0
  7. package/dist/lib/conversation-user-key.js +28 -0
  8. package/dist/lib/conversation.service.d.ts +215 -0
  9. package/dist/lib/conversation.service.js +1179 -0
  10. package/dist/lib/decorators.d.ts +2 -0
  11. package/dist/lib/decorators.js +3 -0
  12. package/dist/lib/entities/index.d.ts +4 -0
  13. package/dist/lib/entities/index.js +4 -0
  14. package/dist/lib/entities/wechat-personal-account.entity.d.ts +19 -0
  15. package/dist/lib/entities/wechat-personal-account.entity.js +83 -0
  16. package/dist/lib/entities/wechat-personal-conversation-binding.entity.d.ts +14 -0
  17. package/dist/lib/entities/wechat-personal-conversation-binding.entity.js +65 -0
  18. package/dist/lib/entities/wechat-personal-message-log.entity.d.ts +27 -0
  19. package/dist/lib/entities/wechat-personal-message-log.entity.js +108 -0
  20. package/dist/lib/entities/wechat-personal-trigger-binding.entity.d.ts +17 -0
  21. package/dist/lib/entities/wechat-personal-trigger-binding.entity.js +71 -0
  22. package/dist/lib/handoff/index.d.ts +4 -0
  23. package/dist/lib/handoff/index.js +4 -0
  24. package/dist/lib/handoff/wechat-personal-chat-callback.processor.d.ts +26 -0
  25. package/dist/lib/handoff/wechat-personal-chat-callback.processor.js +312 -0
  26. package/dist/lib/handoff/wechat-personal-chat-dispatch.service.d.ts +26 -0
  27. package/dist/lib/handoff/wechat-personal-chat-dispatch.service.js +187 -0
  28. package/dist/lib/handoff/wechat-personal-chat-run-state.service.d.ts +21 -0
  29. package/dist/lib/handoff/wechat-personal-chat-run-state.service.js +39 -0
  30. package/dist/lib/handoff/wechat-personal-chat.types.d.ts +69 -0
  31. package/dist/lib/handoff/wechat-personal-chat.types.js +2 -0
  32. package/dist/lib/message.d.ts +49 -0
  33. package/dist/lib/message.js +64 -0
  34. package/dist/lib/remote-components/wechat-personal-workbench/app.js +1831 -0
  35. package/dist/lib/tokens.d.ts +1 -0
  36. package/dist/lib/tokens.js +1 -0
  37. package/dist/lib/types.d.ts +48 -0
  38. package/dist/lib/types.js +365 -0
  39. package/dist/lib/views/wechat-personal-view.provider.d.ts +17 -0
  40. package/dist/lib/views/wechat-personal-view.provider.js +441 -0
  41. package/dist/lib/wechat-personal-channel.strategy.d.ts +33 -0
  42. package/dist/lib/wechat-personal-channel.strategy.js +197 -0
  43. package/dist/lib/wechat-personal-integration.strategy.d.ts +56 -0
  44. package/dist/lib/wechat-personal-integration.strategy.js +217 -0
  45. package/dist/lib/wechat-personal.client.d.ts +29 -0
  46. package/dist/lib/wechat-personal.client.js +146 -0
  47. package/dist/lib/wechat-personal.controller.d.ts +50 -0
  48. package/dist/lib/wechat-personal.controller.js +270 -0
  49. package/dist/lib/wechat-personal.middleware.d.ts +20 -0
  50. package/dist/lib/wechat-personal.middleware.js +267 -0
  51. package/dist/lib/wechat-personal.plugin.d.ts +2 -0
  52. package/dist/lib/wechat-personal.plugin.js +58 -0
  53. package/dist/lib/wechat-personal.templates.d.ts +2 -0
  54. package/dist/lib/wechat-personal.templates.js +100 -0
  55. package/dist/lib/workflow/index.d.ts +5 -0
  56. package/dist/lib/workflow/index.js +5 -0
  57. package/dist/lib/workflow/wechat-personal-trigger-aggregation.service.d.ts +10 -0
  58. package/dist/lib/workflow/wechat-personal-trigger-aggregation.service.js +39 -0
  59. package/dist/lib/workflow/wechat-personal-trigger-aggregation.types.d.ts +30 -0
  60. package/dist/lib/workflow/wechat-personal-trigger-aggregation.types.js +2 -0
  61. package/dist/lib/workflow/wechat-personal-trigger-flush.processor.d.ts +8 -0
  62. package/dist/lib/workflow/wechat-personal-trigger-flush.processor.js +39 -0
  63. package/dist/lib/workflow/wechat-personal-trigger.strategy.d.ts +65 -0
  64. package/dist/lib/workflow/wechat-personal-trigger.strategy.js +511 -0
  65. package/dist/lib/workflow/wechat-personal-trigger.types.d.ts +10 -0
  66. package/dist/lib/workflow/wechat-personal-trigger.types.js +2 -0
  67. package/dist/xpert-wechat-personal-admin-assistant.yaml +103 -0
  68. package/dist/xpert-wechat-personal-user-assistant.yaml +127 -0
  69. package/package.json +79 -0
@@ -0,0 +1,1179 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ var WechatPersonalConversationService_1;
14
+ import { CACHE_MANAGER } from '@nestjs/cache-manager';
15
+ import { Inject, Injectable, Logger } from '@nestjs/common';
16
+ import { InjectRepository } from '@nestjs/typeorm';
17
+ import { INTEGRATION_PERMISSION_SERVICE_TOKEN, RequestContext } from '@xpert-ai/plugin-sdk';
18
+ import { MoreThan, Repository } from 'typeorm';
19
+ import { normalizeConversationKey, parseWechatPersonalConversationUserKey, resolveWechatPersonalConversationUserKey } from './conversation-user-key.js';
20
+ import { summarizePayload, shouldDispatchWechatPersonalMessage } from './types.js';
21
+ import { WECHAT_PERSONAL_PLUGIN_CONTEXT } from './tokens.js';
22
+ import { WECHAT_PERSONAL_PROVIDER_KEY } from './constants.js';
23
+ import { WechatPersonalMessage } from './message.js';
24
+ import { WechatPersonalChannelStrategy } from './wechat-personal-channel.strategy.js';
25
+ import { WechatPersonalAccountEntity, WechatPersonalConversationBindingEntity, WechatPersonalMessageLogEntity } from './entities/index.js';
26
+ import { WechatPersonalTriggerStrategy } from './workflow/wechat-personal-trigger.strategy.js';
27
+ const CACHE_TTL_MS = 10 * 60 * 1000;
28
+ const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
29
+ let WechatPersonalConversationService = WechatPersonalConversationService_1 = class WechatPersonalConversationService {
30
+ constructor(wechatChannel, triggerStrategy, cacheManager, conversationBindingRepository, accountRepository, messageLogRepository, pluginContext) {
31
+ this.wechatChannel = wechatChannel;
32
+ this.triggerStrategy = triggerStrategy;
33
+ this.cacheManager = cacheManager;
34
+ this.conversationBindingRepository = conversationBindingRepository;
35
+ this.accountRepository = accountRepository;
36
+ this.messageLogRepository = messageLogRepository;
37
+ this.pluginContext = pluginContext;
38
+ this.logger = new Logger(WechatPersonalConversationService_1.name);
39
+ }
40
+ get integrationPermissionService() {
41
+ if (!this._integrationPermissionService) {
42
+ this._integrationPermissionService = this.pluginContext.resolve(INTEGRATION_PERMISSION_SERVICE_TOKEN);
43
+ }
44
+ return this._integrationPermissionService;
45
+ }
46
+ async getConversationState(conversationUserKey, xpertId, scopeOverride) {
47
+ const normalizedUserKey = normalizeConversationKey(conversationUserKey);
48
+ const normalizedXpertId = normalizeConversationKey(xpertId);
49
+ if (!normalizedUserKey || !normalizedXpertId) {
50
+ return undefined;
51
+ }
52
+ const scope = this.resolveTenantScope(scopeOverride);
53
+ const cached = await this.cacheManager.get(this.getConversationCacheKey(normalizedUserKey, normalizedXpertId, scope));
54
+ if (cached && typeof cached === 'object') {
55
+ const conversationId = normalizeConversationKey(cached.conversationId);
56
+ if (conversationId) {
57
+ return {
58
+ conversationId,
59
+ lastActiveAt: this.normalizeDate(cached.lastActiveAt)
60
+ };
61
+ }
62
+ }
63
+ const binding = await this.conversationBindingRepository.findOne({
64
+ where: this.scopedWhere({
65
+ conversationUserKey: normalizedUserKey,
66
+ xpertId: normalizedXpertId
67
+ }, scope)
68
+ });
69
+ const conversationId = normalizeConversationKey(binding?.conversationId);
70
+ if (!conversationId) {
71
+ return undefined;
72
+ }
73
+ const lastActiveAt = this.normalizeDate(binding?.lastActiveAt) ?? this.normalizeDate(binding?.updatedAt);
74
+ await this.cacheConversation(normalizedUserKey, normalizedXpertId, conversationId, lastActiveAt, scope);
75
+ return {
76
+ conversationId,
77
+ lastActiveAt
78
+ };
79
+ }
80
+ async setConversation(conversationUserKey, xpertId, conversationId, lastActiveAt = new Date(), scopeOverride) {
81
+ const normalizedUserKey = normalizeConversationKey(conversationUserKey);
82
+ const normalizedXpertId = normalizeConversationKey(xpertId);
83
+ const normalizedConversationId = normalizeConversationKey(conversationId);
84
+ if (!normalizedUserKey || !normalizedXpertId || !normalizedConversationId) {
85
+ return;
86
+ }
87
+ const resolvedLastActiveAt = this.normalizeDate(lastActiveAt) ?? new Date();
88
+ const bindingContext = this.resolveBindingContext();
89
+ const scope = this.resolveTenantScope(scopeOverride, bindingContext);
90
+ await this.cacheConversation(normalizedUserKey, normalizedXpertId, normalizedConversationId, resolvedLastActiveAt, scope);
91
+ await this.conversationBindingRepository.upsert({
92
+ conversationUserKey: normalizedUserKey,
93
+ xpertId: normalizedXpertId,
94
+ conversationId: normalizedConversationId,
95
+ lastActiveAt: resolvedLastActiveAt,
96
+ tenantId: scope.tenantId ?? null,
97
+ organizationId: scope.organizationId ?? null,
98
+ createdById: bindingContext.createdById ?? null,
99
+ updatedById: bindingContext.updatedById ?? null
100
+ }, ['conversationUserKey', 'xpertId']);
101
+ }
102
+ async touchConversation(conversationUserKey, xpertId, lastActiveAt = new Date(), scopeOverride) {
103
+ const current = await this.getConversationState(conversationUserKey, xpertId, scopeOverride);
104
+ if (!current?.conversationId) {
105
+ return;
106
+ }
107
+ await this.setConversation(conversationUserKey, xpertId, current.conversationId, lastActiveAt, scopeOverride);
108
+ }
109
+ async clearConversation(conversationUserKey, xpertId, scopeOverride) {
110
+ const normalizedUserKey = normalizeConversationKey(conversationUserKey);
111
+ const normalizedXpertId = normalizeConversationKey(xpertId);
112
+ if (!normalizedUserKey || !normalizedXpertId) {
113
+ return;
114
+ }
115
+ const scope = this.resolveTenantScope(scopeOverride);
116
+ await this.cacheManager.del(this.getConversationCacheKey(normalizedUserKey, normalizedXpertId, scope));
117
+ await this.conversationBindingRepository.delete(this.scopedWhere({
118
+ conversationUserKey: normalizedUserKey,
119
+ xpertId: normalizedXpertId
120
+ }, scope));
121
+ }
122
+ async restartConversationBinding(integrationId, bindingId) {
123
+ const normalizedIntegrationId = normalizeConversationKey(integrationId);
124
+ const scope = await this.readIntegrationTenantScope(normalizedIntegrationId);
125
+ const binding = await this.conversationBindingRepository.findOne({
126
+ where: this.scopedWhere({
127
+ id: normalizeConversationKey(bindingId)
128
+ }, scope)
129
+ });
130
+ if (!binding) {
131
+ throw new Error('该微信会话不存在或已被重置。');
132
+ }
133
+ const parsed = parseWechatPersonalConversationUserKey(binding.conversationUserKey);
134
+ if (!parsed || parsed.integrationId !== normalizedIntegrationId) {
135
+ throw new Error('该微信会话不属于当前个人微信集成。');
136
+ }
137
+ await this.clearConversation(binding.conversationUserKey, binding.xpertId, scope);
138
+ }
139
+ async handleInboundEvent(event, ctx) {
140
+ const integration = await this.integrationPermissionService.read(ctx.integration.id, {
141
+ relations: ['tenant']
142
+ });
143
+ if (!integration) {
144
+ this.logger.error(`Integration ${ctx.integration.id} not found`);
145
+ return { handled: false, reason: 'integration_not_found' };
146
+ }
147
+ const eventScope = this.resolveTenantScope(integration, ctx);
148
+ const accountState = await this.upsertAccount(integration, event, ctx);
149
+ if (accountState.enabled === false) {
150
+ await this.logInbound(integration, event, 'skipped', {
151
+ error: 'account_disabled'
152
+ });
153
+ return { handled: false, reason: 'account_disabled' };
154
+ }
155
+ const duplicate = await this.isDuplicateInbound(integration.id, event, eventScope);
156
+ const inboundLog = await this.logInbound(integration, event, duplicate ? 'skipped' : 'received', {
157
+ error: duplicate ? 'duplicate_message_id' : undefined
158
+ });
159
+ if (duplicate) {
160
+ return { handled: false, reason: 'duplicate' };
161
+ }
162
+ const binding = await this.triggerStrategy.getBinding(integration.id, eventScope);
163
+ if (!binding?.xpertId) {
164
+ await this.updateLog(inboundLog.id, {
165
+ status: 'skipped',
166
+ error: 'trigger_binding_missing'
167
+ }, eventScope);
168
+ return { handled: false, reason: 'trigger_binding_missing' };
169
+ }
170
+ const dispatchable = shouldDispatchWechatPersonalMessage(event, {
171
+ ignoreSelfMessages: integration.options?.ignoreSelfMessages,
172
+ groupTriggerMode: binding.groupTriggerMode || integration.options?.groupTriggerMode,
173
+ groupKeywords: binding.groupKeywords?.length ? binding.groupKeywords : integration.options?.groupKeywords
174
+ });
175
+ if (!dispatchable) {
176
+ await this.updateLog(inboundLog.id, {
177
+ status: 'skipped',
178
+ xpertId: binding.xpertId,
179
+ error: 'filtered_by_trigger_policy'
180
+ }, eventScope);
181
+ return { handled: false, reason: 'filtered' };
182
+ }
183
+ const executorUserId = this.resolveExecutionUserId(integration);
184
+ const conversationUserKey = resolveWechatPersonalConversationUserKey({
185
+ integrationId: integration.id,
186
+ uuid: event.uuid,
187
+ contactId: event.contactId,
188
+ senderId: event.senderId || event.contactId
189
+ });
190
+ const wechatMessage = new WechatPersonalMessage({
191
+ integrationId: integration.id,
192
+ uuid: event.uuid,
193
+ ownerWxid: event.ownerWxid,
194
+ contactId: event.contactId,
195
+ chatType: event.chatType,
196
+ senderId: event.senderId,
197
+ wechatChannel: this.wechatChannel
198
+ }, {
199
+ messageId: event.messageId,
200
+ status: 'thinking',
201
+ language: integration.options?.preferLanguage
202
+ });
203
+ const newSessionCommand = this.parseNewSessionCommand(dispatchable.input);
204
+ if (newSessionCommand.matched && conversationUserKey) {
205
+ await this.clearConversation(conversationUserKey, binding.xpertId, eventScope);
206
+ if (!newSessionCommand.input) {
207
+ await wechatMessage.reply(this.getNewConversationStartedText(integration.options?.preferLanguage));
208
+ await this.updateLog(inboundLog.id, {
209
+ status: 'dispatched',
210
+ xpertId: binding.xpertId,
211
+ conversationUserKey
212
+ }, eventScope);
213
+ return { handled: true, reason: 'new_session_only' };
214
+ }
215
+ }
216
+ let conversationId;
217
+ if (conversationUserKey && !newSessionCommand.matched) {
218
+ const currentConversation = await this.getConversationState(conversationUserKey, binding.xpertId, eventScope);
219
+ const sessionTimeoutMs = this.resolveSessionTimeoutMs(binding.sessionTimeoutSeconds);
220
+ if (currentConversation?.conversationId) {
221
+ if (this.isConversationExpired(currentConversation.lastActiveAt, sessionTimeoutMs)) {
222
+ await this.clearConversation(conversationUserKey, binding.xpertId, eventScope);
223
+ }
224
+ else {
225
+ conversationId = currentConversation.conversationId;
226
+ await this.touchConversation(conversationUserKey, binding.xpertId, new Date(), eventScope);
227
+ }
228
+ }
229
+ }
230
+ const handled = await this.triggerStrategy.handleInboundMessage({
231
+ integrationId: integration.id,
232
+ input: newSessionCommand.matched ? newSessionCommand.input : dispatchable.input,
233
+ wechatMessage,
234
+ conversationId,
235
+ conversationUserKey,
236
+ tenantId: integration.tenantId || ctx.tenantId,
237
+ organizationId: integration.organizationId || ctx.organizationId,
238
+ executorUserId,
239
+ endUserId: event.senderId
240
+ });
241
+ await this.updateLog(inboundLog.id, {
242
+ status: handled ? 'dispatched' : 'failed',
243
+ xpertId: binding.xpertId,
244
+ conversationId,
245
+ conversationUserKey,
246
+ error: handled ? undefined : 'handoff_dispatch_failed'
247
+ }, eventScope);
248
+ return { handled, reason: handled ? 'dispatched' : 'dispatch_failed' };
249
+ }
250
+ async logOutbound(params) {
251
+ const context = params.context;
252
+ const bindingContext = this.resolveBindingContext();
253
+ const scope = this.resolveTenantScope(context, bindingContext);
254
+ await this.messageLogRepository.save({
255
+ integrationId: context.integrationId,
256
+ uuid: context.uuid,
257
+ ownerWxid: context.ownerWxid,
258
+ contactId: context.contactId,
259
+ senderId: context.senderId,
260
+ chatType: context.chatType,
261
+ messageId: params.messageId,
262
+ direction: 'outbound',
263
+ status: params.status,
264
+ content: params.content,
265
+ error: params.error,
266
+ xpertId: context.xpertId,
267
+ conversationId: context.conversationId,
268
+ conversationUserKey: context.conversationUserKey,
269
+ tenantId: scope.tenantId ?? null,
270
+ organizationId: scope.organizationId ?? null,
271
+ createdById: bindingContext.createdById ?? null,
272
+ updatedById: bindingContext.updatedById ?? null
273
+ });
274
+ await this.accountRepository.update(this.scopedWhere({
275
+ integrationId: context.integrationId,
276
+ uuid: context.uuid
277
+ }, scope), {
278
+ lastSendAt: new Date(),
279
+ status: params.status === 'failed' ? 'error' : 'online',
280
+ lastError: params.error ?? null
281
+ });
282
+ }
283
+ async setAccountEnabled(integrationId, uuid, enabled) {
284
+ const normalizedIntegrationId = normalizeConversationKey(integrationId);
285
+ const normalizedUuid = normalizeConversationKey(uuid);
286
+ if (!normalizedIntegrationId || !normalizedUuid) {
287
+ throw new Error('缺少个人微信账号标识。');
288
+ }
289
+ const scope = await this.readIntegrationTenantScope(normalizedIntegrationId);
290
+ await this.accountRepository.update(this.scopedWhere({
291
+ integrationId: normalizedIntegrationId,
292
+ uuid: normalizedUuid
293
+ }, scope), {
294
+ enabled,
295
+ status: enabled ? 'unknown' : 'disabled',
296
+ lastError: null
297
+ });
298
+ }
299
+ async resendOutboundMessage(integrationId, logId) {
300
+ const normalizedIntegrationId = normalizeConversationKey(integrationId);
301
+ if (!normalizedIntegrationId) {
302
+ throw new Error('缺少个人微信集成标识。');
303
+ }
304
+ const scope = await this.readIntegrationTenantScope(normalizedIntegrationId);
305
+ const target = logId
306
+ ? await this.messageLogRepository.findOne({
307
+ where: this.scopedWhere({
308
+ id: normalizeConversationKey(logId),
309
+ integrationId: normalizedIntegrationId,
310
+ direction: 'outbound'
311
+ }, scope)
312
+ })
313
+ : await this.messageLogRepository.findOne({
314
+ where: this.scopedWhere({
315
+ integrationId: normalizedIntegrationId,
316
+ direction: 'outbound'
317
+ }, scope),
318
+ order: {
319
+ createdAt: 'DESC'
320
+ }
321
+ });
322
+ if (!target?.uuid || !target.contactId || !target.content) {
323
+ throw new Error('没有可重发的 AI 文本回复。');
324
+ }
325
+ const result = await this.wechatChannel.sendTextByIntegrationId(normalizedIntegrationId, {
326
+ uuid: target.uuid,
327
+ contactId: target.contactId,
328
+ content: target.content
329
+ });
330
+ await this.messageLogRepository.save({
331
+ integrationId: normalizedIntegrationId,
332
+ uuid: target.uuid,
333
+ ownerWxid: target.ownerWxid,
334
+ contactId: target.contactId,
335
+ senderId: target.senderId,
336
+ messageId: result.messageId,
337
+ chatType: target.chatType,
338
+ direction: 'outbound',
339
+ status: result.success ? 'sent' : 'failed',
340
+ content: target.content,
341
+ error: result.error,
342
+ xpertId: target.xpertId,
343
+ conversationId: target.conversationId,
344
+ conversationUserKey: target.conversationUserKey,
345
+ tenantId: target.tenantId ?? scope.tenantId ?? null,
346
+ organizationId: target.organizationId ?? scope.organizationId ?? null
347
+ });
348
+ return result;
349
+ }
350
+ async getWorkbenchData(integrationId, query = {}) {
351
+ const integration = await this.integrationPermissionService.read(integrationId, {
352
+ relations: ['tenant']
353
+ });
354
+ const normalizedIntegrationId = normalizeConversationKey(integrationId);
355
+ if (!integration || !normalizedIntegrationId) {
356
+ throw new Error('个人微信集成不存在或无权访问。');
357
+ }
358
+ const pageSize = this.normalizePositiveInt(query.pageSize) ?? 30;
359
+ const search = this.normalizeListSearch(query.search);
360
+ const scope = this.resolveTenantScope(integration);
361
+ const [accounts, logs, bindings] = await Promise.all([
362
+ this.accountRepository.find({
363
+ where: this.scopedWhere({ integrationId: normalizedIntegrationId }, scope),
364
+ order: { updatedAt: 'DESC' },
365
+ take: 100
366
+ }),
367
+ this.messageLogRepository.find({
368
+ where: this.scopedWhere({ integrationId: normalizedIntegrationId }, scope),
369
+ order: { createdAt: 'DESC' },
370
+ take: 200
371
+ }),
372
+ this.conversationBindingRepository.find({
373
+ where: this.scopedWhere({}, scope),
374
+ order: { updatedAt: 'DESC' }
375
+ })
376
+ ]);
377
+ const conversations = bindings
378
+ .map((binding) => this.toConversationListItem(binding, normalizedIntegrationId))
379
+ .filter((item) => Boolean(item))
380
+ .filter((item) => {
381
+ if (!search) {
382
+ return true;
383
+ }
384
+ return [item.uuid, item.contactId, item.senderId, item.xpertId, item.conversationId].some((value) => this.normalizeListSearch(value)?.includes(search));
385
+ })
386
+ .slice(0, pageSize);
387
+ const filteredLogs = logs
388
+ .filter((log) => {
389
+ if (!search) {
390
+ return true;
391
+ }
392
+ return [log.uuid, log.contactId, log.senderId, log.content, log.error, log.status].some((value) => this.normalizeListSearch(value)?.includes(search));
393
+ })
394
+ .slice(0, pageSize);
395
+ return {
396
+ scope: 'integration',
397
+ integrationId: normalizedIntegrationId,
398
+ integrations: [
399
+ this.toIntegrationWorkbenchItem(integration, {
400
+ accounts,
401
+ conversations,
402
+ logs
403
+ })
404
+ ],
405
+ callbackConfig: this.buildCallbackConfig(integrationId, integration?.options?.callbackSecret),
406
+ summary: {
407
+ integrationCount: 1,
408
+ accountCount: accounts.length,
409
+ conversationCount: conversations.length,
410
+ recentMessageCount: logs.length,
411
+ errorCount: logs.filter((log) => log.status === 'failed' || log.error).length
412
+ },
413
+ accounts,
414
+ conversations,
415
+ messages: filteredLogs,
416
+ logs: filteredLogs,
417
+ config: {
418
+ baseUrl: integration?.options?.baseUrl,
419
+ apiVersion: integration?.options?.apiVersion ?? '/v1/',
420
+ timeoutMs: integration?.options?.timeoutMs ?? 10000,
421
+ preferLanguage: integration?.options?.preferLanguage,
422
+ groupTriggerMode: integration?.options?.groupTriggerMode ?? 'mention_or_keywords',
423
+ groupKeywords: integration?.options?.groupKeywords ?? [],
424
+ ignoreSelfMessages: integration?.options?.ignoreSelfMessages ?? true,
425
+ fallbackToLegacySendText: integration?.options?.fallbackToLegacySendText !== false,
426
+ callbackSecret: integration?.options?.callbackSecret ? '******' : ''
427
+ }
428
+ };
429
+ }
430
+ async getOrganizationWorkbenchData(query = {}) {
431
+ const integrations = await this.listWechatPersonalIntegrations();
432
+ const integrationIds = integrations.map((integration) => normalizeConversationKey(integration.id)).filter(Boolean);
433
+ const pageSize = this.normalizePositiveInt(query.pageSize) ?? 30;
434
+ const search = this.normalizeListSearch(query.search);
435
+ const scope = this.resolveTenantScope(integrations[0]);
436
+ if (!integrationIds.length) {
437
+ return {
438
+ scope: 'organization',
439
+ integrationId: null,
440
+ integrations: [],
441
+ callbackConfig: this.emptyCallbackConfig(),
442
+ summary: {
443
+ integrationCount: 0,
444
+ accountCount: 0,
445
+ conversationCount: 0,
446
+ recentMessageCount: 0,
447
+ errorCount: 0
448
+ },
449
+ accounts: [],
450
+ conversations: [],
451
+ messages: [],
452
+ logs: [],
453
+ config: {
454
+ organizationScope: true,
455
+ integrationCount: 0
456
+ }
457
+ };
458
+ }
459
+ const [accounts, logs, bindings] = await Promise.all([
460
+ this.accountRepository.find({
461
+ where: integrationIds.map((integrationId) => this.scopedWhere({ integrationId }, scope)),
462
+ order: { updatedAt: 'DESC' },
463
+ take: 500
464
+ }),
465
+ this.messageLogRepository.find({
466
+ where: integrationIds.map((integrationId) => this.scopedWhere({ integrationId }, scope)),
467
+ order: { createdAt: 'DESC' },
468
+ take: 1000
469
+ }),
470
+ this.conversationBindingRepository.find({
471
+ where: this.scopedWhere({}, scope),
472
+ order: { updatedAt: 'DESC' },
473
+ take: 1500
474
+ })
475
+ ]);
476
+ const integrationIdSet = new Set(integrationIds);
477
+ const allConversations = bindings
478
+ .map((binding) => {
479
+ const parsed = parseWechatPersonalConversationUserKey(binding.conversationUserKey);
480
+ if (!parsed || !integrationIdSet.has(parsed.integrationId)) {
481
+ return null;
482
+ }
483
+ return this.toConversationListItem(binding, parsed.integrationId);
484
+ })
485
+ .filter((item) => Boolean(item));
486
+ const conversations = allConversations
487
+ .filter((item) => this.matchesConversationSearch(item, search))
488
+ .slice(0, pageSize);
489
+ const filteredLogs = logs
490
+ .filter((log) => this.matchesLogSearch(log, search))
491
+ .slice(0, pageSize);
492
+ return {
493
+ scope: 'organization',
494
+ integrationId: null,
495
+ integrations: integrations.map((integration) => this.toIntegrationWorkbenchItem(integration, {
496
+ accounts: accounts.filter((account) => account.integrationId === integration.id),
497
+ conversations: allConversations.filter((conversation) => conversation.integrationId === integration.id),
498
+ logs: logs.filter((log) => log.integrationId === integration.id)
499
+ })),
500
+ callbackConfig: this.emptyCallbackConfig(),
501
+ summary: {
502
+ integrationCount: integrations.length,
503
+ accountCount: accounts.length,
504
+ conversationCount: allConversations.length,
505
+ recentMessageCount: logs.length,
506
+ errorCount: logs.filter((log) => log.status === 'failed' || log.error).length
507
+ },
508
+ accounts,
509
+ conversations,
510
+ messages: filteredLogs,
511
+ logs: filteredLogs,
512
+ config: {
513
+ organizationScope: true,
514
+ integrationCount: integrations.length
515
+ }
516
+ };
517
+ }
518
+ async getRuntimeStatus(integrationId) {
519
+ const normalizedIntegrationId = normalizeConversationKey(integrationId);
520
+ if (!normalizedIntegrationId) {
521
+ throw new Error('缺少个人微信集成标识。');
522
+ }
523
+ const [workbenchData, triggerBinding] = await Promise.all([
524
+ this.getWorkbenchData(normalizedIntegrationId, { pageSize: 20 }),
525
+ this.triggerStrategy.getBinding(normalizedIntegrationId)
526
+ ]);
527
+ return {
528
+ callbackConfig: workbenchData.callbackConfig,
529
+ summary: workbenchData.summary,
530
+ triggerBinding: triggerBinding
531
+ ? {
532
+ integrationId: triggerBinding.integrationId,
533
+ xpertId: triggerBinding.xpertId,
534
+ sessionTimeoutSeconds: triggerBinding.sessionTimeoutSeconds,
535
+ summaryWindowSeconds: triggerBinding.summaryWindowSeconds,
536
+ groupTriggerMode: triggerBinding.groupTriggerMode,
537
+ groupKeywords: triggerBinding.groupKeywords ?? [],
538
+ updatedAt: this.normalizeDate(triggerBinding.updatedAt) ?? null
539
+ }
540
+ : null,
541
+ accounts: workbenchData.accounts.slice(0, 10),
542
+ recentErrors: workbenchData.logs
543
+ .filter((log) => log.status === 'failed' || Boolean(log.error))
544
+ .slice(0, 10),
545
+ config: workbenchData.config
546
+ };
547
+ }
548
+ async getOrganizationRuntimeStatus() {
549
+ const workbenchData = await this.getOrganizationWorkbenchData({ pageSize: 20 });
550
+ return {
551
+ scope: 'organization',
552
+ integrations: workbenchData.integrations,
553
+ callbackConfig: workbenchData.callbackConfig,
554
+ summary: workbenchData.summary,
555
+ triggerBinding: null,
556
+ accounts: workbenchData.accounts.slice(0, 10),
557
+ recentErrors: workbenchData.logs
558
+ .filter((log) => log.status === 'failed' || Boolean(log.error))
559
+ .slice(0, 10),
560
+ config: workbenchData.config
561
+ };
562
+ }
563
+ async getBoundIntegrationIdForXpert(xpertId) {
564
+ const normalizedXpertId = normalizeConversationKey(xpertId);
565
+ if (!normalizedXpertId) {
566
+ return null;
567
+ }
568
+ return this.triggerStrategy.getBoundIntegrationId(normalizedXpertId);
569
+ }
570
+ async getWorkbenchTableData(integrationId, table, query = {}) {
571
+ if (table === 'accounts') {
572
+ return { key: table, ...(await this.listAccounts(integrationId, query)) };
573
+ }
574
+ if (table === 'conversations') {
575
+ return { key: table, ...(await this.listConversations(integrationId, query)) };
576
+ }
577
+ return { key: table, ...(await this.searchMessageLogs(integrationId, query)) };
578
+ }
579
+ async getOrganizationWorkbenchTableData(table, query = {}) {
580
+ if (table === 'accounts') {
581
+ return { key: table, ...(await this.listOrganizationAccounts(query)) };
582
+ }
583
+ if (table === 'conversations') {
584
+ return { key: table, ...(await this.listOrganizationConversations(query)) };
585
+ }
586
+ return { key: table, ...(await this.searchOrganizationMessageLogs(query)) };
587
+ }
588
+ async listAccounts(integrationId, query = {}) {
589
+ const normalizedIntegrationId = normalizeConversationKey(integrationId);
590
+ if (!normalizedIntegrationId) {
591
+ throw new Error('缺少个人微信集成标识。');
592
+ }
593
+ const page = this.normalizePage(query.page);
594
+ const pageSize = this.normalizePageSize(query.pageSize, 50);
595
+ const search = this.normalizeListSearch(query.search);
596
+ const filters = this.normalizeFilters(query.filters);
597
+ const scope = await this.readIntegrationTenantScope(normalizedIntegrationId);
598
+ const accounts = await this.accountRepository.find({
599
+ where: this.scopedWhere({ integrationId: normalizedIntegrationId }, scope),
600
+ order: { updatedAt: 'DESC' },
601
+ take: 500
602
+ });
603
+ const filtered = accounts.filter((account) => {
604
+ if (!this.matchesAccountFilters(account, filters)) {
605
+ return false;
606
+ }
607
+ if (!search) {
608
+ return true;
609
+ }
610
+ return [
611
+ account.uuid,
612
+ account.ownerWxid,
613
+ account.displayName,
614
+ account.status,
615
+ account.lastError
616
+ ].some((value) => this.normalizeListSearch(value)?.includes(search));
617
+ });
618
+ return this.paginateItems(filtered, page, pageSize);
619
+ }
620
+ async listOrganizationAccounts(query = {}) {
621
+ const data = await this.getOrganizationWorkbenchData({ pageSize: 500 });
622
+ const page = this.normalizePage(query.page);
623
+ const pageSize = this.normalizePageSize(query.pageSize, 50);
624
+ const search = this.normalizeListSearch(query.search);
625
+ const filters = this.normalizeFilters(query.filters);
626
+ const filtered = data.accounts.filter((account) => {
627
+ if (!this.matchesAccountFilters(account, filters)) {
628
+ return false;
629
+ }
630
+ if (!search) {
631
+ return true;
632
+ }
633
+ return [
634
+ account.integrationId,
635
+ account.uuid,
636
+ account.ownerWxid,
637
+ account.displayName,
638
+ account.status,
639
+ account.lastError
640
+ ].some((value) => this.normalizeListSearch(value)?.includes(search));
641
+ });
642
+ return this.paginateItems(filtered, page, pageSize);
643
+ }
644
+ async listConversations(integrationId, query = {}) {
645
+ const normalizedIntegrationId = normalizeConversationKey(integrationId);
646
+ if (!normalizedIntegrationId) {
647
+ throw new Error('缺少个人微信集成标识。');
648
+ }
649
+ const page = this.normalizePage(query.page);
650
+ const pageSize = this.normalizePageSize(query.pageSize, 50);
651
+ const search = this.normalizeListSearch(query.search);
652
+ const filters = this.normalizeFilters(query.filters);
653
+ const scope = await this.readIntegrationTenantScope(normalizedIntegrationId);
654
+ const bindings = await this.conversationBindingRepository.find({
655
+ where: this.scopedWhere({}, scope),
656
+ order: { updatedAt: 'DESC' },
657
+ take: 1000
658
+ });
659
+ const conversations = bindings
660
+ .map((binding) => this.toConversationListItem(binding, normalizedIntegrationId))
661
+ .filter((item) => Boolean(item))
662
+ .filter((item) => this.matchesConversationFilters(item, filters))
663
+ .filter((item) => {
664
+ if (!search) {
665
+ return true;
666
+ }
667
+ return [
668
+ item.id,
669
+ item.uuid,
670
+ item.contactId,
671
+ item.senderId,
672
+ item.xpertId,
673
+ item.conversationId
674
+ ].some((value) => this.normalizeListSearch(value)?.includes(search));
675
+ });
676
+ return this.paginateItems(conversations, page, pageSize);
677
+ }
678
+ async listOrganizationConversations(query = {}) {
679
+ const data = await this.getOrganizationWorkbenchData({ pageSize: 1000 });
680
+ const page = this.normalizePage(query.page);
681
+ const pageSize = this.normalizePageSize(query.pageSize, 50);
682
+ const search = this.normalizeListSearch(query.search);
683
+ const filters = this.normalizeFilters(query.filters);
684
+ const filtered = data.conversations
685
+ .filter((item) => this.matchesConversationFilters(item, filters))
686
+ .filter((item) => this.matchesConversationSearch(item, search));
687
+ return this.paginateItems(filtered, page, pageSize);
688
+ }
689
+ async searchMessageLogs(integrationId, query = {}) {
690
+ const normalizedIntegrationId = normalizeConversationKey(integrationId);
691
+ if (!normalizedIntegrationId) {
692
+ throw new Error('缺少个人微信集成标识。');
693
+ }
694
+ const page = this.normalizePage(query.page);
695
+ const pageSize = this.normalizePageSize(query.pageSize, 50);
696
+ const search = this.normalizeListSearch(query.search);
697
+ const filters = this.normalizeFilters(query.filters);
698
+ const direction = this.normalizeDirection(query.direction ?? filters.direction);
699
+ const status = this.normalizeLogStatus(query.status ?? filters.status);
700
+ const scope = await this.readIntegrationTenantScope(normalizedIntegrationId);
701
+ const logs = await this.messageLogRepository.find({
702
+ where: this.scopedWhere({ integrationId: normalizedIntegrationId }, scope),
703
+ order: { createdAt: 'DESC' },
704
+ take: 1000
705
+ });
706
+ const filtered = logs.filter((log) => {
707
+ if (!this.matchesLogFilters(log, filters)) {
708
+ return false;
709
+ }
710
+ if (direction && log.direction !== direction) {
711
+ return false;
712
+ }
713
+ if (status && log.status !== status) {
714
+ return false;
715
+ }
716
+ if (!search) {
717
+ return true;
718
+ }
719
+ return [
720
+ log.uuid,
721
+ log.ownerWxid,
722
+ log.contactId,
723
+ log.senderId,
724
+ log.messageId,
725
+ log.content,
726
+ log.error,
727
+ log.xpertId,
728
+ log.conversationId,
729
+ log.conversationUserKey
730
+ ].some((value) => this.normalizeListSearch(value)?.includes(search));
731
+ });
732
+ return this.paginateItems(filtered, page, pageSize);
733
+ }
734
+ async searchOrganizationMessageLogs(query = {}) {
735
+ const data = await this.getOrganizationWorkbenchData({ pageSize: 1000 });
736
+ const page = this.normalizePage(query.page);
737
+ const pageSize = this.normalizePageSize(query.pageSize, 50);
738
+ const search = this.normalizeListSearch(query.search);
739
+ const filters = this.normalizeFilters(query.filters);
740
+ const direction = this.normalizeDirection(query.direction ?? filters.direction);
741
+ const status = this.normalizeLogStatus(query.status ?? filters.status);
742
+ const filtered = data.logs.filter((log) => {
743
+ if (!this.matchesLogFilters(log, filters)) {
744
+ return false;
745
+ }
746
+ if (direction && log.direction !== direction) {
747
+ return false;
748
+ }
749
+ if (status && log.status !== status) {
750
+ return false;
751
+ }
752
+ return this.matchesLogSearch(log, search);
753
+ });
754
+ return this.paginateItems(filtered, page, pageSize);
755
+ }
756
+ buildCallbackConfig(integrationId, callbackSecret) {
757
+ const apiBaseUrl = (process.env.API_BASE_URL || '').replace(/\/+$/, '');
758
+ const id = normalizeConversationKey(integrationId) || '<integrationId>';
759
+ const webhookUrl = `${apiBaseUrl}/api/wechat-personal/webhook/${id}${callbackSecret ? `?secret=${encodeURIComponent(callbackSecret)}` : ''}`;
760
+ const setCallbackUrlTemplate = `${apiBaseUrl}/api/wechat-personal/webhook/${id}${callbackSecret ? '?secret=***' : ''}`;
761
+ return {
762
+ webhookUrl,
763
+ globalWebhookUrl: webhookUrl,
764
+ setCallbackUrlTemplate,
765
+ setCallbackCurlTemplate: `curl -X POST "$WX2_BASE_URL/message/SetCallback?key=<uuid>" ` +
766
+ `-H "Content-Type: application/json" ` +
767
+ `-d '{"CallbackURL":"${setCallbackUrlTemplate}","Enabled":true}'`
768
+ };
769
+ }
770
+ async upsertAccount(integration, event, ctx) {
771
+ const bindingContext = this.resolveBindingContext();
772
+ const scope = this.resolveTenantScope(integration, ctx);
773
+ const existing = await this.accountRepository.findOne({
774
+ where: this.scopedWhere({
775
+ integrationId: integration.id,
776
+ uuid: event.uuid
777
+ }, scope)
778
+ });
779
+ const enabled = existing?.enabled !== false;
780
+ await this.accountRepository.upsert({
781
+ integrationId: integration.id,
782
+ uuid: event.uuid,
783
+ ownerWxid: event.ownerWxid || null,
784
+ displayName: event.ownerName || event.ownerWxid || null,
785
+ status: enabled ? 'online' : 'disabled',
786
+ enabled,
787
+ lastCallbackAt: new Date(),
788
+ lastError: null,
789
+ tenantId: scope.tenantId ?? null,
790
+ organizationId: scope.organizationId ?? null,
791
+ createdById: bindingContext.createdById ?? null,
792
+ updatedById: bindingContext.updatedById ?? null
793
+ }, ['integrationId', 'uuid']);
794
+ return { enabled };
795
+ }
796
+ async isDuplicateInbound(integrationId, event, scope) {
797
+ const messageId = event.messageId;
798
+ if (!normalizeConversationKey(messageId)) {
799
+ return false;
800
+ }
801
+ const exactCount = await this.messageLogRepository.count({
802
+ where: this.scopedWhere({
803
+ integrationId,
804
+ messageId,
805
+ direction: 'inbound'
806
+ }, scope)
807
+ });
808
+ if (exactCount > 0) {
809
+ return true;
810
+ }
811
+ const content = normalizeConversationKey(event.content);
812
+ if (!content) {
813
+ return false;
814
+ }
815
+ const recentEquivalentCount = await this.messageLogRepository.count({
816
+ where: this.scopedWhere({
817
+ integrationId,
818
+ uuid: event.uuid,
819
+ contactId: event.contactId,
820
+ senderId: event.senderId,
821
+ direction: 'inbound',
822
+ content,
823
+ createdAt: MoreThan(new Date(Date.now() - 5_000))
824
+ }, scope)
825
+ });
826
+ return recentEquivalentCount > 0;
827
+ }
828
+ async logInbound(integration, event, status, params = {}) {
829
+ const bindingContext = this.resolveBindingContext();
830
+ const scope = this.resolveTenantScope(integration, bindingContext);
831
+ return this.messageLogRepository.save({
832
+ integrationId: integration.id,
833
+ uuid: event.uuid,
834
+ ownerWxid: event.ownerWxid,
835
+ contactId: event.contactId,
836
+ senderId: event.senderId,
837
+ messageId: event.messageId,
838
+ chatType: event.chatType,
839
+ direction: 'inbound',
840
+ status,
841
+ content: event.content,
842
+ payloadSummary: summarizePayload(event.rawPayload),
843
+ error: params.error,
844
+ tenantId: scope.tenantId ?? null,
845
+ organizationId: scope.organizationId ?? null,
846
+ createdById: bindingContext.createdById ?? null,
847
+ updatedById: bindingContext.updatedById ?? null
848
+ });
849
+ }
850
+ async updateLog(id, patch, scope) {
851
+ if (!id) {
852
+ return;
853
+ }
854
+ await this.messageLogRepository.update(this.scopedWhere({ id }, scope), patch);
855
+ }
856
+ getConversationCacheKey(conversationUserKey, xpertId, scope) {
857
+ const tenantKey = scope?.tenantId ? `tenant:${scope.tenantId}` : 'tenant:any';
858
+ const organizationKey = scope?.organizationId ? `org:${scope.organizationId}` : 'org:any';
859
+ return `wechat-personal:chat:${tenantKey}:${organizationKey}:${conversationUserKey}:${xpertId}`;
860
+ }
861
+ async cacheConversation(conversationUserKey, xpertId, conversationId, lastActiveAt, scope) {
862
+ await this.cacheManager.set(this.getConversationCacheKey(conversationUserKey, xpertId, scope), {
863
+ conversationId,
864
+ lastActiveAt: lastActiveAt?.toISOString()
865
+ }, CACHE_TTL_MS);
866
+ }
867
+ resolveTenantScope(primary, fallback) {
868
+ const bindingContext = this.resolveBindingContext();
869
+ return {
870
+ tenantId: primary?.tenantId ?? fallback?.tenantId ?? bindingContext.tenantId ?? null,
871
+ organizationId: primary?.organizationId ?? fallback?.organizationId ?? bindingContext.organizationId ?? null
872
+ };
873
+ }
874
+ async readIntegrationTenantScope(integrationId) {
875
+ const normalizedIntegrationId = normalizeConversationKey(integrationId);
876
+ if (!normalizedIntegrationId) {
877
+ throw new Error('缺少个人微信集成标识。');
878
+ }
879
+ const integration = await this.integrationPermissionService.read(normalizedIntegrationId, {
880
+ relations: ['tenant']
881
+ });
882
+ if (!integration) {
883
+ throw new Error('个人微信集成不存在或无权访问。');
884
+ }
885
+ return this.resolveTenantScope(integration);
886
+ }
887
+ scopedWhere(where, scope) {
888
+ const scoped = { ...where };
889
+ if (scope?.tenantId) {
890
+ scoped.tenantId = scope.tenantId;
891
+ }
892
+ if (scope?.organizationId) {
893
+ scoped.organizationId = scope.organizationId;
894
+ }
895
+ return scoped;
896
+ }
897
+ resolveBindingContext() {
898
+ const tenantId = RequestContext.currentTenantId();
899
+ const organizationId = RequestContext.getOrganizationId();
900
+ const userId = normalizeConversationKey(RequestContext.currentUserId());
901
+ const executionUserId = userId && UUID_PATTERN.test(userId) ? userId : null;
902
+ return {
903
+ tenantId: tenantId ?? null,
904
+ organizationId: organizationId ?? null,
905
+ createdById: executionUserId,
906
+ updatedById: executionUserId
907
+ };
908
+ }
909
+ resolveExecutionUserId(integration) {
910
+ const candidates = [RequestContext.currentUserId(), integration?.createdById, integration?.updatedById]
911
+ .map((value) => normalizeConversationKey(value))
912
+ .filter((value) => Boolean(value));
913
+ const uuidMatched = candidates.find((value) => UUID_PATTERN.test(value));
914
+ return uuidMatched ?? candidates[0];
915
+ }
916
+ async listWechatPersonalIntegrations() {
917
+ const result = await this.integrationPermissionService.findAll({
918
+ where: {
919
+ provider: WECHAT_PERSONAL_PROVIDER_KEY
920
+ },
921
+ relations: ['tenant'],
922
+ order: {
923
+ updatedAt: 'DESC'
924
+ },
925
+ take: 100
926
+ });
927
+ return (result.items ?? []).filter((integration) => integration?.provider === WECHAT_PERSONAL_PROVIDER_KEY);
928
+ }
929
+ toIntegrationWorkbenchItem(integration, stats) {
930
+ return {
931
+ id: integration.id,
932
+ name: integration.name,
933
+ description: integration.description,
934
+ slug: integration.slug,
935
+ callbackConfig: this.buildCallbackConfig(integration.id, integration.options?.callbackSecret),
936
+ accountCount: stats.accounts.length,
937
+ conversationCount: stats.conversations.length,
938
+ recentMessageCount: stats.logs.length,
939
+ errorCount: stats.logs.filter((log) => log.status === 'failed' || log.error).length,
940
+ config: this.sanitizeIntegrationConfig(integration.options)
941
+ };
942
+ }
943
+ sanitizeIntegrationConfig(options) {
944
+ return {
945
+ baseUrl: options?.baseUrl,
946
+ apiVersion: options?.apiVersion ?? '/v1/',
947
+ timeoutMs: options?.timeoutMs ?? 10000,
948
+ preferLanguage: options?.preferLanguage,
949
+ groupTriggerMode: options?.groupTriggerMode ?? 'mention_or_keywords',
950
+ groupKeywords: options?.groupKeywords ?? [],
951
+ ignoreSelfMessages: options?.ignoreSelfMessages ?? true,
952
+ fallbackToLegacySendText: options?.fallbackToLegacySendText !== false,
953
+ callbackSecret: options?.callbackSecret ? '******' : ''
954
+ };
955
+ }
956
+ emptyCallbackConfig() {
957
+ return {
958
+ webhookUrl: '',
959
+ globalWebhookUrl: '',
960
+ setCallbackUrlTemplate: '',
961
+ setCallbackCurlTemplate: ''
962
+ };
963
+ }
964
+ matchesAccountFilters(account, filters) {
965
+ const status = this.normalizeListSearch(filters.status);
966
+ const enabled = this.normalizeBooleanFilter(filters.enabled);
967
+ if (status && this.normalizeListSearch(account.status) !== status) {
968
+ return false;
969
+ }
970
+ if (enabled !== null && account.enabled !== enabled) {
971
+ return false;
972
+ }
973
+ return (this.matchesFilter(account.integrationId, filters.integrationId) &&
974
+ this.matchesFilter(account.uuid, filters.uuid) &&
975
+ this.matchesFilter(account.ownerWxid, filters.ownerWxid));
976
+ }
977
+ matchesConversationFilters(item, filters) {
978
+ const chatType = this.normalizeChatType(filters.chatType);
979
+ if (chatType && item.chatType !== chatType) {
980
+ return false;
981
+ }
982
+ return (this.matchesFilter(item.integrationId, filters.integrationId) &&
983
+ this.matchesFilter(item.uuid, filters.uuid) &&
984
+ this.matchesFilter(item.contactId, filters.contactId) &&
985
+ this.matchesFilter(item.senderId, filters.senderId) &&
986
+ this.matchesFilter(item.xpertId, filters.xpertId));
987
+ }
988
+ matchesLogFilters(log, filters) {
989
+ const chatType = this.normalizeChatType(filters.chatType);
990
+ const level = this.normalizeListSearch(filters.level);
991
+ if (chatType && log.chatType !== chatType) {
992
+ return false;
993
+ }
994
+ if (level === 'error' && !(log.status === 'failed' || log.error)) {
995
+ return false;
996
+ }
997
+ if (level === 'info' && (log.status === 'failed' || log.error)) {
998
+ return false;
999
+ }
1000
+ return (this.matchesFilter(log.integrationId, filters.integrationId) &&
1001
+ this.matchesFilter(log.uuid, filters.uuid) &&
1002
+ this.matchesFilter(log.ownerWxid, filters.ownerWxid) &&
1003
+ this.matchesFilter(log.contactId, filters.contactId) &&
1004
+ this.matchesFilter(log.senderId, filters.senderId) &&
1005
+ this.matchesFilter(log.xpertId, filters.xpertId) &&
1006
+ this.matchesFilter(log.conversationId, filters.conversationId));
1007
+ }
1008
+ normalizeFilters(value) {
1009
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
1010
+ }
1011
+ matchesFilter(value, filter) {
1012
+ const normalizedFilter = this.normalizeListSearch(filter);
1013
+ if (!normalizedFilter || normalizedFilter === 'all') {
1014
+ return true;
1015
+ }
1016
+ return Boolean(this.normalizeListSearch(value)?.includes(normalizedFilter));
1017
+ }
1018
+ normalizeBooleanFilter(value) {
1019
+ if (typeof value === 'boolean') {
1020
+ return value;
1021
+ }
1022
+ const normalized = this.normalizeListSearch(value);
1023
+ if (!normalized || normalized === 'all') {
1024
+ return null;
1025
+ }
1026
+ if (['true', '1', 'yes', 'enabled'].includes(normalized)) {
1027
+ return true;
1028
+ }
1029
+ if (['false', '0', 'no', 'disabled'].includes(normalized)) {
1030
+ return false;
1031
+ }
1032
+ return null;
1033
+ }
1034
+ normalizeDirection(value) {
1035
+ const normalized = this.normalizeListSearch(value);
1036
+ return normalized === 'inbound' || normalized === 'outbound' || normalized === 'system' ? normalized : null;
1037
+ }
1038
+ normalizeLogStatus(value) {
1039
+ const normalized = this.normalizeListSearch(value);
1040
+ return ['received', 'dispatched', 'sent', 'skipped', 'failed'].includes(normalized || '')
1041
+ ? normalized
1042
+ : null;
1043
+ }
1044
+ normalizeChatType(value) {
1045
+ const normalized = this.normalizeListSearch(value);
1046
+ return normalized === 'private' || normalized === 'group' ? normalized : null;
1047
+ }
1048
+ matchesConversationSearch(item, search) {
1049
+ if (!search) {
1050
+ return true;
1051
+ }
1052
+ return [item.integrationId, item.uuid, item.contactId, item.senderId, item.xpertId, item.conversationId].some((value) => this.normalizeListSearch(value)?.includes(search));
1053
+ }
1054
+ matchesLogSearch(log, search) {
1055
+ if (!search) {
1056
+ return true;
1057
+ }
1058
+ return [
1059
+ log.integrationId,
1060
+ log.uuid,
1061
+ log.contactId,
1062
+ log.senderId,
1063
+ log.content,
1064
+ log.error,
1065
+ log.status
1066
+ ].some((value) => this.normalizeListSearch(value)?.includes(search));
1067
+ }
1068
+ toConversationListItem(binding, integrationId) {
1069
+ const parsed = parseWechatPersonalConversationUserKey(binding.conversationUserKey);
1070
+ if (!parsed || parsed.integrationId !== integrationId) {
1071
+ return null;
1072
+ }
1073
+ return {
1074
+ id: binding.id,
1075
+ integrationId: parsed.integrationId,
1076
+ uuid: parsed.uuid,
1077
+ contactId: parsed.contactId,
1078
+ senderId: parsed.senderId,
1079
+ chatType: parsed.contactId.endsWith('@chatroom') ? 'group' : 'private',
1080
+ xpertId: binding.xpertId,
1081
+ conversationId: binding.conversationId,
1082
+ updatedAt: this.normalizeDate(binding.lastActiveAt) ?? this.normalizeDate(binding.updatedAt) ?? null
1083
+ };
1084
+ }
1085
+ normalizePositiveInt(value) {
1086
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
1087
+ return value;
1088
+ }
1089
+ return null;
1090
+ }
1091
+ normalizePage(value) {
1092
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
1093
+ return value;
1094
+ }
1095
+ return 1;
1096
+ }
1097
+ normalizePageSize(value, fallback = 30) {
1098
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
1099
+ return Math.min(value, 100);
1100
+ }
1101
+ return fallback;
1102
+ }
1103
+ paginateItems(items, page, pageSize) {
1104
+ const start = (page - 1) * pageSize;
1105
+ return {
1106
+ items: items.slice(start, start + pageSize),
1107
+ total: items.length,
1108
+ page,
1109
+ pageSize
1110
+ };
1111
+ }
1112
+ normalizeListSearch(value) {
1113
+ const normalized = normalizeConversationKey(value);
1114
+ return normalized ? normalized.toLocaleLowerCase() : null;
1115
+ }
1116
+ normalizeDate(value) {
1117
+ if (!value) {
1118
+ return undefined;
1119
+ }
1120
+ if (value instanceof Date) {
1121
+ return Number.isNaN(value.getTime()) ? undefined : value;
1122
+ }
1123
+ if (typeof value === 'string' || typeof value === 'number') {
1124
+ const date = new Date(value);
1125
+ return Number.isNaN(date.getTime()) ? undefined : date;
1126
+ }
1127
+ return undefined;
1128
+ }
1129
+ resolveSessionTimeoutMs(value) {
1130
+ if (typeof value === 'number' && Number.isFinite(value) && value > 0) {
1131
+ return Math.floor(value) * 1000;
1132
+ }
1133
+ return 3600 * 1000;
1134
+ }
1135
+ isConversationExpired(lastActiveAt, sessionTimeoutMs) {
1136
+ if (!lastActiveAt) {
1137
+ return false;
1138
+ }
1139
+ return Date.now() - lastActiveAt.getTime() > sessionTimeoutMs;
1140
+ }
1141
+ parseNewSessionCommand(input) {
1142
+ const trimmedInput = input.trim();
1143
+ if (!trimmedInput.startsWith('/new')) {
1144
+ return {
1145
+ matched: false,
1146
+ input
1147
+ };
1148
+ }
1149
+ const nextCharacter = trimmedInput.charAt('/new'.length);
1150
+ if (nextCharacter && !/\s/.test(nextCharacter)) {
1151
+ return {
1152
+ matched: false,
1153
+ input
1154
+ };
1155
+ }
1156
+ return {
1157
+ matched: true,
1158
+ input: trimmedInput.slice('/new'.length).trim()
1159
+ };
1160
+ }
1161
+ getNewConversationStartedText(language) {
1162
+ return language === 'en'
1163
+ ? 'A new conversation has started. Please continue sending your message.'
1164
+ : '已开启新会话,请继续发送消息。';
1165
+ }
1166
+ };
1167
+ WechatPersonalConversationService = WechatPersonalConversationService_1 = __decorate([
1168
+ Injectable(),
1169
+ __param(2, Inject(CACHE_MANAGER)),
1170
+ __param(3, InjectRepository(WechatPersonalConversationBindingEntity)),
1171
+ __param(4, InjectRepository(WechatPersonalAccountEntity)),
1172
+ __param(5, InjectRepository(WechatPersonalMessageLogEntity)),
1173
+ __param(6, Inject(WECHAT_PERSONAL_PLUGIN_CONTEXT)),
1174
+ __metadata("design:paramtypes", [WechatPersonalChannelStrategy,
1175
+ WechatPersonalTriggerStrategy, Object, Repository,
1176
+ Repository,
1177
+ Repository, Object])
1178
+ ], WechatPersonalConversationService);
1179
+ export { WechatPersonalConversationService };