openclaw-vchat-plugin 0.0.1

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 (70) hide show
  1. package/bin/openclaw-vchat.js +110 -0
  2. package/dist/commands.d.ts +18 -0
  3. package/dist/commands.d.ts.map +1 -0
  4. package/dist/commands.js +509 -0
  5. package/dist/commands.js.map +1 -0
  6. package/dist/constants.d.ts +14 -0
  7. package/dist/constants.d.ts.map +1 -0
  8. package/dist/constants.js +51 -0
  9. package/dist/constants.js.map +1 -0
  10. package/dist/gateway-client.d.ts +43 -0
  11. package/dist/gateway-client.d.ts.map +1 -0
  12. package/dist/gateway-client.js +623 -0
  13. package/dist/gateway-client.js.map +1 -0
  14. package/dist/group-manager.d.ts +30 -0
  15. package/dist/group-manager.d.ts.map +1 -0
  16. package/dist/group-manager.js +107 -0
  17. package/dist/group-manager.js.map +1 -0
  18. package/dist/index.d.ts +2 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +382 -0
  21. package/dist/index.js.map +1 -0
  22. package/dist/media-handler.d.ts +31 -0
  23. package/dist/media-handler.d.ts.map +1 -0
  24. package/dist/media-handler.js +67 -0
  25. package/dist/media-handler.js.map +1 -0
  26. package/dist/message-handler.d.ts +52 -0
  27. package/dist/message-handler.d.ts.map +1 -0
  28. package/dist/message-handler.js +291 -0
  29. package/dist/message-handler.js.map +1 -0
  30. package/dist/relay-server.d.ts +16 -0
  31. package/dist/relay-server.d.ts.map +1 -0
  32. package/dist/relay-server.js +877 -0
  33. package/dist/relay-server.js.map +1 -0
  34. package/dist/routes/config.routes.d.ts +12 -0
  35. package/dist/routes/config.routes.d.ts.map +1 -0
  36. package/dist/routes/config.routes.js +175 -0
  37. package/dist/routes/config.routes.js.map +1 -0
  38. package/dist/services/config.service.d.ts +57 -0
  39. package/dist/services/config.service.d.ts.map +1 -0
  40. package/dist/services/config.service.js +361 -0
  41. package/dist/services/config.service.js.map +1 -0
  42. package/dist/session-key.d.ts +8 -0
  43. package/dist/session-key.d.ts.map +1 -0
  44. package/dist/session-key.js +28 -0
  45. package/dist/session-key.js.map +1 -0
  46. package/dist/session-manager.d.ts +32 -0
  47. package/dist/session-manager.d.ts.map +1 -0
  48. package/dist/session-manager.js +303 -0
  49. package/dist/session-manager.js.map +1 -0
  50. package/dist/types.d.ts +81 -0
  51. package/dist/types.d.ts.map +1 -0
  52. package/dist/types.js +3 -0
  53. package/dist/types.js.map +1 -0
  54. package/nginx-proxy.conf +24 -0
  55. package/package.json +51 -0
  56. package/src/commands.ts +499 -0
  57. package/src/constants.ts +49 -0
  58. package/src/gateway-client.ts +648 -0
  59. package/src/group-manager.ts +119 -0
  60. package/src/index.ts +443 -0
  61. package/src/media-handler.ts +70 -0
  62. package/src/message-handler.ts +419 -0
  63. package/src/relay-server.ts +979 -0
  64. package/src/routes/config.routes.ts +144 -0
  65. package/src/services/config.service.ts +398 -0
  66. package/src/session-key.ts +30 -0
  67. package/src/session-manager.ts +374 -0
  68. package/src/types.ts +96 -0
  69. package/start.sh +5 -0
  70. package/tsconfig.json +26 -0
@@ -0,0 +1,144 @@
1
+ /**
2
+ * 模型配置路由
3
+ * GET /api/config/providers — 提供商列表
4
+ * POST /api/config/providers — 添加提供商
5
+ * DELETE /api/config/providers/:id — 删除提供商
6
+ * POST /api/config/reset — 重置配置
7
+ * GET /api/config/export — 导出配置
8
+ * POST /api/config/import — 导入配置
9
+ */
10
+
11
+ import { Router, Request, Response } from 'express';
12
+ import * as configService from '../services/config.service';
13
+
14
+ const router = Router();
15
+
16
+ /**
17
+ * GET /api/config/providers
18
+ */
19
+ router.get('/providers', (req: Request, res: Response) => {
20
+ try {
21
+ const providers = configService.getProviders();
22
+ res.json({ providers });
23
+ } catch (err: any) {
24
+ console.error('[ConfigRoutes] getProviders 错误:', err);
25
+ res.status(500).json({ error: err.message || '获取提供商列表失败' });
26
+ }
27
+ });
28
+
29
+ /**
30
+ * POST /api/config/providers
31
+ * Body: { preset?, providerId?, baseUrl?, apiKey, name?, api?, authHeader?, headers?, models? }
32
+ */
33
+ router.post('/providers', (req: Request, res: Response) => {
34
+ try {
35
+ const { preset, providerId, baseUrl, apiKey, name, api, authHeader, headers, models } = req.body;
36
+
37
+ if (!apiKey) {
38
+ res.status(400).json({ error: '请提供 API Key' });
39
+ return;
40
+ }
41
+
42
+ const provider = configService.addProvider({
43
+ preset,
44
+ providerId,
45
+ baseUrl,
46
+ apiKey,
47
+ name,
48
+ api,
49
+ authHeader,
50
+ headers,
51
+ models,
52
+ });
53
+ res.json({ provider });
54
+ } catch (err: any) {
55
+ console.error('[ConfigRoutes] addProvider 错误:', err);
56
+ res.status(400).json({ error: err.message || '添加提供商失败' });
57
+ }
58
+ });
59
+
60
+ /**
61
+ * PUT /api/config/providers/:id
62
+ */
63
+ router.put('/providers/:id', (req: Request, res: Response) => {
64
+ try {
65
+ const { id } = req.params;
66
+ const provider = configService.updateProvider(id, req.body);
67
+ res.json({ provider });
68
+ } catch (err: any) {
69
+ console.error('[ConfigRoutes] updateProvider 错误:', err);
70
+ res.status(400).json({ error: err.message || '更新提供商失败' });
71
+ }
72
+ });
73
+
74
+ /**
75
+ * DELETE /api/config/providers/:id
76
+ */
77
+ router.delete('/providers/:id', (req: Request, res: Response) => {
78
+ try {
79
+ const { id } = req.params;
80
+ configService.deleteProvider(id);
81
+ res.json({ message: '已删除' });
82
+ } catch (err: any) {
83
+ console.error('[ConfigRoutes] deleteProvider 错误:', err);
84
+ res.status(400).json({ error: err.message || '删除提供商失败' });
85
+ }
86
+ });
87
+
88
+ /**
89
+ * POST /api/config/reset
90
+ */
91
+ router.post('/reset', (req: Request, res: Response) => {
92
+ try {
93
+ configService.resetConfig();
94
+ res.json({ message: '配置已重置为默认' });
95
+ } catch (err: any) {
96
+ console.error('[ConfigRoutes] reset 错误:', err);
97
+ res.status(500).json({ error: err.message || '重置配置失败' });
98
+ }
99
+ });
100
+
101
+ /**
102
+ * POST /api/config/backup
103
+ */
104
+ router.post('/backup', (req: Request, res: Response) => {
105
+ try {
106
+ configService.backupConfig();
107
+ res.json({ message: '配置已备份' });
108
+ } catch (err: any) {
109
+ console.error('[ConfigRoutes] backup 错误:', err);
110
+ res.status(500).json({ error: err.message || '备份配置失败' });
111
+ }
112
+ });
113
+
114
+ /**
115
+ * GET /api/config/export
116
+ */
117
+ router.get('/export', (req: Request, res: Response) => {
118
+ try {
119
+ const config = configService.exportConfig();
120
+ res.json(config);
121
+ } catch (err: any) {
122
+ console.error('[ConfigRoutes] export 错误:', err);
123
+ res.status(500).json({ error: err.message || '导出配置失败' });
124
+ }
125
+ });
126
+
127
+ /**
128
+ * POST /api/config/import
129
+ */
130
+ router.post('/import', (req: Request, res: Response) => {
131
+ try {
132
+ if (!req.body || Object.keys(req.body).length === 0) {
133
+ res.status(400).json({ error: '请提供配置数据' });
134
+ return;
135
+ }
136
+ configService.importConfig(req.body);
137
+ res.json({ message: '配置已导入' });
138
+ } catch (err: any) {
139
+ console.error('[ConfigRoutes] import 错误:', err);
140
+ res.status(400).json({ error: err.message || '导入配置失败' });
141
+ }
142
+ });
143
+
144
+ export default router;
@@ -0,0 +1,398 @@
1
+ /**
2
+ * 模型提供商配置服务
3
+ * 读写 openclaw.json / 预设模板 / 配置导出导入
4
+ */
5
+
6
+ import fs from 'fs';
7
+ import path from 'path';
8
+ import { Provider } from '../types';
9
+ import { PROVIDER_PRESETS } from '../constants';
10
+
11
+ // OpenClaw 配置文件路径
12
+ const OPENCLAW_DIR = process.env.OPENCLAW_DIR || path.join(require('os').homedir(), '.openclaw');
13
+ const OPENCLAW_CONFIG_PATH = path.join(OPENCLAW_DIR, 'openclaw.json');
14
+ const BACKUP_DIR = path.join(OPENCLAW_DIR, 'backups');
15
+
16
+ // 内置默认配置
17
+ const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', '..', 'default-config.json');
18
+
19
+ /**
20
+ * 读取 openclaw.json
21
+ */
22
+ function readConfig(): any {
23
+ try {
24
+ const content = fs.readFileSync(OPENCLAW_CONFIG_PATH, 'utf-8');
25
+ return JSON.parse(content);
26
+ } catch {
27
+ return { models: { providers: {} } };
28
+ }
29
+ }
30
+
31
+ function ensureProvidersObject(config: any): Record<string, any> {
32
+ if (!config.models || typeof config.models !== 'object') config.models = {};
33
+ const raw = config.models.providers;
34
+
35
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
36
+ config.models.providers = raw;
37
+ return config.models.providers;
38
+ }
39
+
40
+ // 兼容旧格式:providers 是数组
41
+ const normalized: Record<string, any> = {};
42
+ if (Array.isArray(raw)) {
43
+ for (const item of raw) {
44
+ if (!item || typeof item !== 'object') continue;
45
+ const key = makeProviderKey(
46
+ item.id || item.providerId || item.name || item.baseUrl || `provider_${Object.keys(normalized).length + 1}`,
47
+ normalized
48
+ );
49
+ normalized[key] = sanitizeProviderEntry(item);
50
+ }
51
+ }
52
+
53
+ config.models.providers = normalized;
54
+ return config.models.providers;
55
+ }
56
+
57
+ /**
58
+ * 转换字符串模型列表为 OpenClaw 官方对象列表
59
+ */
60
+ function formatModels(models: any[]): any[] {
61
+ if (!models || !Array.isArray(models)) return [];
62
+ return models
63
+ .map((m) => {
64
+ if (typeof m === 'object' && m !== null && m.id) {
65
+ return {
66
+ ...m,
67
+ id: String(m.id),
68
+ name: m.name ? String(m.name) : String(m.id),
69
+ };
70
+ }
71
+ const id = String(m || '').trim();
72
+ if (!id) return null;
73
+ return {
74
+ id,
75
+ name: id,
76
+ reasoning: false,
77
+ input: ['text'],
78
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
79
+ contextWindow: 128000,
80
+ maxTokens: 128000,
81
+ };
82
+ })
83
+ .filter(Boolean);
84
+ }
85
+
86
+ function normalizeApi(api?: string): string {
87
+ const val = String(api || '').trim();
88
+ return val || 'openai-completions';
89
+ }
90
+
91
+ function sanitizeProviderEntry(entry: any): any {
92
+ const normalized: any = {};
93
+
94
+ const baseUrl = String(entry?.baseUrl || entry?.baseURL || '').trim();
95
+ if (baseUrl) normalized.baseUrl = baseUrl;
96
+
97
+ const apiKey = String(entry?.apiKey || '').trim();
98
+ if (apiKey) normalized.apiKey = apiKey;
99
+
100
+ normalized.api = normalizeApi(entry?.api);
101
+
102
+ const authHeader = String(entry?.authHeader || '').trim();
103
+ if (authHeader) normalized.authHeader = authHeader;
104
+
105
+ if (entry?.headers && typeof entry.headers === 'object' && !Array.isArray(entry.headers)) {
106
+ normalized.headers = entry.headers;
107
+ }
108
+
109
+ if (Array.isArray(entry?.models)) {
110
+ normalized.models = formatModels(entry.models);
111
+ } else {
112
+ normalized.models = [];
113
+ }
114
+
115
+ return normalized;
116
+ }
117
+
118
+ function slugify(value: string): string {
119
+ const lowered = String(value || '').trim().toLowerCase();
120
+ const slug = lowered
121
+ .replace(/^https?:\/\//, '')
122
+ .replace(/[^a-z0-9]+/g, '_')
123
+ .replace(/^_+|_+$/g, '');
124
+ return slug || 'provider';
125
+ }
126
+
127
+ function makeProviderKey(seed: string, providers: Record<string, any>): string {
128
+ const base = slugify(seed);
129
+ let key = base;
130
+ let idx = 2;
131
+ while (providers[key]) {
132
+ key = `${base}_${idx}`;
133
+ idx += 1;
134
+ }
135
+ return key;
136
+ }
137
+
138
+ function validateProviderIdOrThrow(value: string): string {
139
+ const trimmed = String(value || '').trim();
140
+ if (!/^[A-Za-z0-9]{1,8}$/.test(trimmed)) {
141
+ throw new Error('providerId 仅允许英文大小写和数字,长度 1-8');
142
+ }
143
+ return trimmed;
144
+ }
145
+
146
+ function pickProviderDisplayName(providerId: string, entry: any): string {
147
+ const explicit = String(entry?.name || '').trim();
148
+ if (explicit) return explicit;
149
+
150
+ const fromPreset = Object.entries(PROVIDER_PRESETS).find(([, preset]) => {
151
+ return preset.baseUrl === entry?.baseUrl;
152
+ });
153
+ if (fromPreset) return fromPreset[1].name;
154
+
155
+ return providerId;
156
+ }
157
+
158
+ /**
159
+ * 写入 openclaw.json
160
+ */
161
+ function writeConfig(config: any): void {
162
+ fs.mkdirSync(path.dirname(OPENCLAW_CONFIG_PATH), { recursive: true });
163
+ fs.writeFileSync(OPENCLAW_CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
164
+ }
165
+
166
+ /**
167
+ * 获取提供商列表(apiKey 脱敏)
168
+ */
169
+ export function getProviders(): Provider[] {
170
+ const config = readConfig();
171
+ const providersObj = ensureProvidersObject(config);
172
+
173
+ return Object.keys(providersObj).map((key) => {
174
+ const raw = providersObj[key] || {};
175
+ const p = sanitizeProviderEntry(raw);
176
+ return {
177
+ id: key,
178
+ name: pickProviderDisplayName(key, raw),
179
+ baseUrl: p.baseUrl || '',
180
+ apiKey: maskApiKey(p.apiKey || ''),
181
+ api: p.api || 'openai-completions',
182
+ authHeader: p.authHeader,
183
+ headers: p.headers,
184
+ models: p.models || [],
185
+ };
186
+ });
187
+ }
188
+
189
+ /**
190
+ * 添加提供商
191
+ * 官方结构:models.providers.<providerId> = { baseUrl, apiKey, api, models, ... }
192
+ */
193
+ export function addProvider(options: {
194
+ preset?: string;
195
+ providerId?: string;
196
+ baseUrl?: string;
197
+ apiKey: string;
198
+ name?: string;
199
+ api?: string;
200
+ authHeader?: string;
201
+ headers?: Record<string, string>;
202
+ models?: string[];
203
+ }): Provider {
204
+ const config = readConfig();
205
+ const providers = ensureProvidersObject(config);
206
+
207
+ const apiKey = String(options.apiKey || '').trim();
208
+ if (!apiKey) throw new Error('请提供 API Key');
209
+
210
+ let providerId = '';
211
+ let providerEntry: any = {};
212
+ let displayName = '';
213
+
214
+ if (options.preset && options.preset in PROVIDER_PRESETS) {
215
+ const template = PROVIDER_PRESETS[options.preset];
216
+ providerId = validateProviderIdOrThrow(options.providerId || options.preset || '');
217
+ if (providers[providerId]) throw new Error(`providerId 已存在: ${providerId}`);
218
+ providerEntry = sanitizeProviderEntry({
219
+ baseUrl: template.baseUrl,
220
+ apiKey,
221
+ api: template.api,
222
+ models: template.models,
223
+ });
224
+ displayName = template.name;
225
+ } else {
226
+ const baseUrl = String(options.baseUrl || '').trim();
227
+ if (!baseUrl) throw new Error('自定义提供商需要指定 baseUrl');
228
+ if (!options.providerId) throw new Error('自定义提供商必须提供 providerId');
229
+ providerId = validateProviderIdOrThrow(options.providerId);
230
+ if (providers[providerId]) throw new Error(`providerId 已存在: ${providerId}`);
231
+
232
+ providerEntry = sanitizeProviderEntry({
233
+ baseUrl,
234
+ apiKey,
235
+ api: options.api,
236
+ authHeader: options.authHeader,
237
+ headers: options.headers,
238
+ models: options.models || [],
239
+ });
240
+
241
+ displayName = String(options.name || providerId).trim() || providerId;
242
+ }
243
+
244
+ providers[providerId] = providerEntry;
245
+ writeConfig(config);
246
+
247
+ console.log(`[ConfigService] 添加提供商: ${displayName} (${providerId})`);
248
+
249
+ return {
250
+ id: providerId,
251
+ name: displayName,
252
+ baseUrl: providerEntry.baseUrl || '',
253
+ apiKey: maskApiKey(providerEntry.apiKey || ''),
254
+ api: providerEntry.api || 'openai-completions',
255
+ authHeader: providerEntry.authHeader,
256
+ headers: providerEntry.headers,
257
+ models: providerEntry.models || [],
258
+ };
259
+ }
260
+
261
+ /**
262
+ * 删除提供商
263
+ */
264
+ export function deleteProvider(providerId: string): void {
265
+ const config = readConfig();
266
+ const providers = ensureProvidersObject(config);
267
+
268
+ if (!providers[providerId]) {
269
+ throw new Error(`提供商不存在: ${providerId}`);
270
+ }
271
+
272
+ delete providers[providerId];
273
+ writeConfig(config);
274
+
275
+ console.log(`[ConfigService] 删除提供商: ${providerId}`);
276
+ }
277
+
278
+ /**
279
+ * 更新提供商
280
+ */
281
+ export function updateProvider(providerId: string, data: Partial<{
282
+ name: string;
283
+ baseUrl: string;
284
+ apiKey: string;
285
+ api: string;
286
+ authHeader: string;
287
+ headers: Record<string, string>;
288
+ models: string[];
289
+ }>): Provider {
290
+ const config = readConfig();
291
+ const providers = ensureProvidersObject(config);
292
+
293
+ const currentRaw = providers[providerId];
294
+ if (!currentRaw) {
295
+ throw new Error(`提供商不存在: ${providerId}`);
296
+ }
297
+
298
+ const current = sanitizeProviderEntry(currentRaw);
299
+ const next = { ...current };
300
+
301
+ if (data.baseUrl !== undefined) next.baseUrl = String(data.baseUrl || '').trim();
302
+ if (data.apiKey !== undefined) next.apiKey = String(data.apiKey || '').trim();
303
+ if (data.api !== undefined) next.api = normalizeApi(data.api);
304
+ if (data.authHeader !== undefined) {
305
+ const authHeader = String(data.authHeader || '').trim();
306
+ if (authHeader) next.authHeader = authHeader;
307
+ else delete next.authHeader;
308
+ }
309
+ if (data.headers !== undefined) {
310
+ if (data.headers && typeof data.headers === 'object' && !Array.isArray(data.headers)) {
311
+ next.headers = data.headers;
312
+ } else {
313
+ delete next.headers;
314
+ }
315
+ }
316
+ if (data.models !== undefined) next.models = formatModels(data.models);
317
+
318
+ providers[providerId] = next;
319
+ writeConfig(config);
320
+
321
+ console.log(`[ConfigService] 更新提供商: ${providerId}`);
322
+
323
+ return {
324
+ id: providerId,
325
+ name: String(data.name || pickProviderDisplayName(providerId, currentRaw) || providerId),
326
+ baseUrl: next.baseUrl || '',
327
+ apiKey: maskApiKey(next.apiKey || ''),
328
+ api: next.api || 'openai-completions',
329
+ authHeader: next.authHeader,
330
+ headers: next.headers,
331
+ models: next.models || [],
332
+ };
333
+ }
334
+
335
+ /**
336
+ * 重置配置(备份当前 → 覆盖为默认)
337
+ */
338
+ export function resetConfig(): void {
339
+ backupConfig();
340
+
341
+ // 用默认配置覆盖
342
+ if (fs.existsSync(DEFAULT_CONFIG_PATH)) {
343
+ const defaultConfig = fs.readFileSync(DEFAULT_CONFIG_PATH, 'utf-8');
344
+ writeConfig(JSON.parse(defaultConfig));
345
+ } else {
346
+ // 无默认配置文件 → 写入空配置
347
+ writeConfig({ models: { providers: {} } });
348
+ }
349
+
350
+ console.log('[ConfigService] 配置已重置为默认');
351
+ }
352
+
353
+ /**
354
+ * 备份配置
355
+ */
356
+ export function backupConfig(): void {
357
+ if (fs.existsSync(OPENCLAW_CONFIG_PATH)) {
358
+ fs.mkdirSync(BACKUP_DIR, { recursive: true });
359
+ // 按北京时间生成友好的文件名,包含小时和分钟
360
+ const d = new Date();
361
+ const dateStr = [d.getFullYear(), String(d.getMonth() + 1).padStart(2, '0'), String(d.getDate()).padStart(2, '0')].join('-');
362
+ const timeStr = [String(d.getHours()).padStart(2, '0'), String(d.getMinutes()).padStart(2, '0'), String(d.getSeconds()).padStart(2, '0')].join('');
363
+ const backupName = `openclaw-backup-${dateStr}-${timeStr}.json`;
364
+ fs.copyFileSync(OPENCLAW_CONFIG_PATH, path.join(BACKUP_DIR, backupName));
365
+ console.log(`[ConfigService] 配置已备份: ${backupName}`);
366
+ }
367
+ }
368
+
369
+ /**
370
+ * 导出配置
371
+ */
372
+ export function exportConfig(): any {
373
+ return readConfig();
374
+ }
375
+
376
+ /**
377
+ * 导入配置
378
+ */
379
+ export function importConfig(configData: any): void {
380
+ // 备份当前配置
381
+ if (fs.existsSync(OPENCLAW_CONFIG_PATH)) {
382
+ fs.mkdirSync(BACKUP_DIR, { recursive: true });
383
+ const backupName = `openclaw-pre-import-${Date.now()}.json`;
384
+ fs.copyFileSync(OPENCLAW_CONFIG_PATH, path.join(BACKUP_DIR, backupName));
385
+ }
386
+
387
+ writeConfig(configData);
388
+ console.log('[ConfigService] 配置已导入');
389
+ }
390
+
391
+ /**
392
+ * API Key 脱敏
393
+ * "sk-1234567890abcdef" → "sk-****cdef"
394
+ */
395
+ function maskApiKey(key: string): string {
396
+ if (!key || key.length <= 8) return '****';
397
+ return key.substring(0, 4) + '****' + key.substring(key.length - 4);
398
+ }
@@ -0,0 +1,30 @@
1
+ const DIRECT_THREAD_KEY_RE = /^agent:([^:]+):wechat:direct:([^:]+):thread:([^:]+)$/i;
2
+
3
+ export function sanitizeWeChatId(value: unknown, fallback: string): string {
4
+ const normalized = String(value || '').trim().toLowerCase();
5
+ return normalized || fallback;
6
+ }
7
+
8
+ export function buildWeChatGatewaySessionKey(
9
+ userId: string,
10
+ sessionId: string,
11
+ agentId = 'main',
12
+ groupId?: string,
13
+ ): string {
14
+ const agent = sanitizeWeChatId(agentId, 'main');
15
+ if (groupId) {
16
+ return `agent:${agent}:wechat:group:${sanitizeWeChatId(groupId, 'unknown')}`;
17
+ }
18
+
19
+ return `agent:${agent}:wechat:direct:${sanitizeWeChatId(userId, 'unknown')}:thread:${sanitizeWeChatId(sessionId, 'unknown')}`;
20
+ }
21
+
22
+ export function parseWeChatDirectThreadSessionKey(key: string): { agentId: string; userId: string; sessionId: string } | null {
23
+ const match = String(key || '').trim().toLowerCase().match(DIRECT_THREAD_KEY_RE);
24
+ if (!match) return null;
25
+ return {
26
+ agentId: match[1],
27
+ userId: match[2],
28
+ sessionId: match[3],
29
+ };
30
+ }