@gloablehive/celphone-wechat-plugin 1.0.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 (45) hide show
  1. package/INSTALL.md +231 -0
  2. package/README.md +259 -0
  3. package/dist/index-simple.js +9 -0
  4. package/dist/index.d.ts +16 -0
  5. package/dist/index.js +77 -0
  6. package/dist/mock-server.d.ts +6 -0
  7. package/dist/mock-server.js +203 -0
  8. package/dist/openclaw.plugin.json +96 -0
  9. package/dist/setup-entry.d.ts +9 -0
  10. package/dist/setup-entry.js +8 -0
  11. package/dist/src/cache/compactor.d.ts +36 -0
  12. package/dist/src/cache/compactor.js +154 -0
  13. package/dist/src/cache/extractor.d.ts +48 -0
  14. package/dist/src/cache/extractor.js +120 -0
  15. package/dist/src/cache/index.d.ts +15 -0
  16. package/dist/src/cache/index.js +16 -0
  17. package/dist/src/cache/indexer.d.ts +41 -0
  18. package/dist/src/cache/indexer.js +262 -0
  19. package/dist/src/cache/manager.d.ts +113 -0
  20. package/dist/src/cache/manager.js +271 -0
  21. package/dist/src/cache/message-queue.d.ts +59 -0
  22. package/dist/src/cache/message-queue.js +147 -0
  23. package/dist/src/cache/saas-connector.d.ts +94 -0
  24. package/dist/src/cache/saas-connector.js +289 -0
  25. package/dist/src/cache/syncer.d.ts +60 -0
  26. package/dist/src/cache/syncer.js +177 -0
  27. package/dist/src/cache/types.d.ts +198 -0
  28. package/dist/src/cache/types.js +43 -0
  29. package/dist/src/cache/writer.d.ts +81 -0
  30. package/dist/src/cache/writer.js +461 -0
  31. package/dist/src/channel.d.ts +65 -0
  32. package/dist/src/channel.js +334 -0
  33. package/dist/src/client.d.ts +280 -0
  34. package/dist/src/client.js +248 -0
  35. package/index-simple.ts +11 -0
  36. package/index.ts +89 -0
  37. package/mock-server.ts +237 -0
  38. package/openclaw.plugin.json +98 -0
  39. package/package.json +37 -0
  40. package/setup-entry.ts +10 -0
  41. package/src/channel.ts +398 -0
  42. package/src/client.ts +412 -0
  43. package/test-cache.ts +260 -0
  44. package/test-integration.ts +319 -0
  45. package/tsconfig.json +22 -0
@@ -0,0 +1,461 @@
1
+ /**
2
+ * Cache Writer - File writing with monthly partition
3
+ *
4
+ * Aligned with Claude Code memory system:
5
+ * - YAML frontmatter for metadata
6
+ * - Markdown content
7
+ * - Monthly file partitioning
8
+ */
9
+ import * as fs from 'fs/promises';
10
+ import * as path from 'path';
11
+ import { getMonthlyFilePath, } from './types.js';
12
+ // ========== Constants ==========
13
+ const ENCODING = 'utf-8';
14
+ // ========== Frontmatter Helpers ==========
15
+ function generateConversationFrontmatter(wechatId, accountId, accountWechatId, subtype, messageCount) {
16
+ const now = new Date().toISOString();
17
+ return {
18
+ name: `与 ${wechatId} 的对话`,
19
+ description: `与 ${wechatId} 的对话记录`,
20
+ type: 'conversation',
21
+ subtype,
22
+ wechatId,
23
+ accountId,
24
+ accountWechatId,
25
+ lastContact: now,
26
+ messageCount,
27
+ createdAt: now,
28
+ updatedAt: now,
29
+ };
30
+ }
31
+ function generateUserProfileFrontmatter(wechatId, accountId, tags) {
32
+ const now = new Date().toISOString();
33
+ return {
34
+ name: `${wechatId} 用户画像`,
35
+ description: `用户画像信息`,
36
+ type: 'user',
37
+ wechatId,
38
+ accountId,
39
+ tags,
40
+ addedAt: now,
41
+ lastContact: now,
42
+ createdAt: now,
43
+ updatedAt: now,
44
+ };
45
+ }
46
+ function frontmatterToString(frontmatter) {
47
+ const lines = ['---'];
48
+ for (const [key, value] of Object.entries(frontmatter)) {
49
+ if (Array.isArray(value)) {
50
+ lines.push(`${key}: [${value.map(v => `"${v}"`).join(', ')}]`);
51
+ }
52
+ else if (typeof value === 'object' && value !== null) {
53
+ lines.push(`${key}: ${JSON.stringify(value)}`);
54
+ }
55
+ else {
56
+ lines.push(`${key}: ${value}`);
57
+ }
58
+ }
59
+ lines.push('---');
60
+ return lines.join('\n');
61
+ }
62
+ // ========== Conversation File Operations ==========
63
+ /**
64
+ * Get or create conversation directory
65
+ */
66
+ export async function ensureConversationDir(basePath, accountId, subtype, conversationId) {
67
+ const dir = subtype === 'friend'
68
+ ? `${basePath}/accounts/${accountId}/friends/${conversationId}/memory`
69
+ : `${basePath}/accounts/${accountId}/chatrooms/${conversationId}/memory`;
70
+ await fs.mkdir(dir, { recursive: true });
71
+ return dir;
72
+ }
73
+ /**
74
+ * Get monthly conversation file path
75
+ */
76
+ export function getConversationFilePath(basePath, accountId, subtype, conversationId, date) {
77
+ const monthlyFile = getMonthlyFilePath(conversationId, date);
78
+ const dir = subtype === 'friend'
79
+ ? `${basePath}/accounts/${accountId}/friends/${conversationId}/memory`
80
+ : `${basePath}/accounts/${accountId}/chatrooms/${conversationId}/memory`;
81
+ return path.join(dir, monthlyFile);
82
+ }
83
+ /**
84
+ * Check if conversation file exists
85
+ */
86
+ export async function conversationFileExists(filePath) {
87
+ try {
88
+ await fs.access(filePath);
89
+ return true;
90
+ }
91
+ catch {
92
+ return false;
93
+ }
94
+ }
95
+ /**
96
+ * Read conversation file
97
+ */
98
+ export async function readConversationFile(filePath) {
99
+ try {
100
+ return await fs.readFile(filePath, ENCODING);
101
+ }
102
+ catch (error) {
103
+ throw new Error(`Failed to read conversation file: ${error}`);
104
+ }
105
+ }
106
+ /**
107
+ * Create new conversation file with frontmatter
108
+ */
109
+ export async function createConversationFile(filePath, wechatId, accountId, accountWechatId, subtype) {
110
+ const frontmatter = generateConversationFrontmatter(wechatId, accountId, accountWechatId, subtype, 0);
111
+ const content = frontmatterToString(frontmatter) + '\n\n# Conversation\n\n';
112
+ await fs.writeFile(filePath, content, ENCODING);
113
+ }
114
+ /**
115
+ * Append message to conversation file
116
+ */
117
+ export async function appendMessageToConversation(filePath, message, remark) {
118
+ let content;
119
+ try {
120
+ content = await fs.readFile(filePath, ENCODING);
121
+ }
122
+ catch {
123
+ // File doesn't exist, create it
124
+ content = '';
125
+ }
126
+ // Parse existing frontmatter
127
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
128
+ let frontmatter;
129
+ let existingContent = content;
130
+ if (frontmatterMatch) {
131
+ // Parse existing frontmatter
132
+ const fmLines = frontmatterMatch[1].split('\n');
133
+ const fm = {};
134
+ for (const line of fmLines) {
135
+ const [key, ...valueParts] = line.split(':');
136
+ if (key && valueParts.length) {
137
+ const value = valueParts.join(':').trim();
138
+ if (value.startsWith('[')) {
139
+ // Array
140
+ fm[key.trim()] = value.slice(1, -1).split(',').map(v => v.trim().replace(/"/g, ''));
141
+ }
142
+ else if (value === 'true' || value === 'false') {
143
+ fm[key.trim()] = value === 'true';
144
+ }
145
+ else {
146
+ fm[key.trim()] = value.replace(/"/g, '');
147
+ }
148
+ }
149
+ }
150
+ frontmatter = fm;
151
+ frontmatter.messageCount = (frontmatter.messageCount || 0) + 1;
152
+ frontmatter.lastContact = new Date(message.timestamp).toISOString();
153
+ frontmatter.updatedAt = new Date().toISOString();
154
+ if (remark) {
155
+ frontmatter.name = `与 ${remark} (${frontmatter.wechatId}) 的对话`;
156
+ }
157
+ }
158
+ else {
159
+ // Create new frontmatter
160
+ frontmatter = generateConversationFrontmatter(message.conversationId, message.accountId, '', message.conversationType, 1);
161
+ }
162
+ // Build message block
163
+ const time = new Date(message.timestamp);
164
+ const timeStr = `[${time.toISOString().split('T')[0]}]`;
165
+ const sender = message.isSelf ? '我' : (remark || message.senderId);
166
+ const direction = message.isSelf ? '' : ''; // 可以添加方向标记
167
+ const messageBlock = `\n${timeStr}\n\n**[${sender}]**: ${message.content}\n`;
168
+ // Rebuild file
169
+ const newContent = frontmatterToString(frontmatter) + '\n\n' + messageBlock + '\n' + existingContent.replace(/^---[\s\S]*?---\n\n/, '');
170
+ await fs.writeFile(filePath, newContent, ENCODING);
171
+ }
172
+ // ========== Profile File Operations ==========
173
+ /**
174
+ * Get profile file path
175
+ */
176
+ export function getProfileFilePath(basePath, accountId, wechatId) {
177
+ return `${basePath}/accounts/${accountId}/profiles/${wechatId}.md`;
178
+ }
179
+ /**
180
+ * Create or update profile file
181
+ */
182
+ export async function writeProfile(filePath, profile) {
183
+ const frontmatter = {
184
+ name: `${profile.remark || profile.wechatId} 用户画像`,
185
+ description: `用户画像信息 - ${profile.tags.join(', ')}`,
186
+ type: 'user',
187
+ wechatId: profile.wechatId,
188
+ accountId: profile.accountId,
189
+ tags: profile.tags,
190
+ addedAt: profile.addedAt,
191
+ lastContact: profile.lastContact,
192
+ createdAt: profile.addedAt,
193
+ updatedAt: new Date().toISOString(),
194
+ };
195
+ let content = frontmatterToString(frontmatter) + '\n\n';
196
+ // Add profile sections
197
+ content += '## 基础信息\n';
198
+ if (profile.remark)
199
+ content += `- 备注: ${profile.remark}\n`;
200
+ if (profile.nickName)
201
+ content += `- 昵称: ${profile.nickName}\n`;
202
+ if (profile.avatarUrl)
203
+ content += `- 头像: ${profile.avatarUrl}\n`;
204
+ content += '\n## 画像标签\n';
205
+ for (const tag of profile.tags) {
206
+ content += `- ${tag}\n`;
207
+ }
208
+ if (profile.customFields && Object.keys(profile.customFields).length > 0) {
209
+ content += '\n## 自定义字段\n';
210
+ for (const [key, value] of Object.entries(profile.customFields)) {
211
+ content += `- ${key}: ${value}\n`;
212
+ }
213
+ }
214
+ if (profile.conversationSummary) {
215
+ content += '\n## 对话摘要\n' + profile.conversationSummary + '\n';
216
+ }
217
+ if (profile.preferenceHints) {
218
+ content += '\n## 偏好提示\n' + profile.preferenceHints + '\n';
219
+ }
220
+ if (profile.riskFlags && profile.riskFlags.length > 0) {
221
+ content += '\n## 风险标记\n';
222
+ for (const flag of profile.riskFlags) {
223
+ content += `- ${flag}\n`;
224
+ }
225
+ }
226
+ await fs.writeFile(filePath, content, ENCODING);
227
+ }
228
+ /**
229
+ * Read profile file
230
+ */
231
+ export async function readProfile(filePath) {
232
+ try {
233
+ const content = await fs.readFile(filePath, ENCODING);
234
+ // Parse frontmatter
235
+ const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
236
+ if (!fmMatch)
237
+ return null;
238
+ const fmLines = fmMatch[1].split('\n');
239
+ const profile = {
240
+ tags: [],
241
+ riskFlags: [],
242
+ customFields: {},
243
+ };
244
+ for (const line of fmLines) {
245
+ const colonIdx = line.indexOf(':');
246
+ if (colonIdx === -1)
247
+ continue;
248
+ const key = line.slice(0, colonIdx).trim();
249
+ const value = line.slice(colonIdx + 1).trim();
250
+ switch (key) {
251
+ case 'wechatId':
252
+ profile.wechatId = value.replace(/"/g, '');
253
+ break;
254
+ case 'accountId':
255
+ profile.accountId = value.replace(/"/g, '');
256
+ break;
257
+ case 'remark':
258
+ profile.remark = value.replace(/"/g, '');
259
+ break;
260
+ case 'nickName':
261
+ profile.nickName = value.replace(/"/g, '');
262
+ break;
263
+ case 'tags':
264
+ profile.tags = value.slice(1, -1).split(',').map(v => v.trim().replace(/"/g, ''));
265
+ break;
266
+ case 'addedAt':
267
+ profile.addedAt = value.replace(/"/g, '');
268
+ break;
269
+ case 'lastContact':
270
+ profile.lastContact = value.replace(/"/g, '');
271
+ break;
272
+ }
273
+ }
274
+ return profile;
275
+ }
276
+ catch {
277
+ return null;
278
+ }
279
+ }
280
+ /**
281
+ * Update profile fields
282
+ */
283
+ export async function updateProfileFields(filePath, updates) {
284
+ const existing = await readProfile(filePath);
285
+ if (!existing) {
286
+ // Create new profile
287
+ const newProfile = {
288
+ wechatId: updates.wechatId || '',
289
+ accountId: updates.accountId || '',
290
+ remark: updates.remark || '',
291
+ nickName: updates.nickName || '',
292
+ tags: updates.tags || [],
293
+ customFields: updates.customFields || {},
294
+ riskFlags: updates.riskFlags || [],
295
+ addedAt: new Date().toISOString(),
296
+ lastContact: new Date().toISOString(),
297
+ };
298
+ await writeProfile(filePath, newProfile);
299
+ return;
300
+ }
301
+ // Merge updates
302
+ const updated = {
303
+ ...existing,
304
+ ...updates,
305
+ lastContact: new Date().toISOString(),
306
+ updatedAt: new Date().toISOString(),
307
+ };
308
+ await writeProfile(filePath, updated);
309
+ }
310
+ // ========== Session Memory Operations ==========
311
+ /**
312
+ * Get session memory file path
313
+ */
314
+ export function getSessionMemoryPath(basePath, accountId, conversationId) {
315
+ return `${basePath}/accounts/${accountId}/friends/${conversationId}/session.md`;
316
+ }
317
+ /**
318
+ * Write session memory (aligned with Claude Code 10-section template)
319
+ */
320
+ export async function writeSessionMemory(filePath, session) {
321
+ const content = `# Session Memory - ${session.sessionTitle}
322
+
323
+ ## Session Title
324
+ _${session.sessionTitle}_
325
+
326
+ ## Current State
327
+ _${session.currentState}_
328
+
329
+ ## Task specification
330
+ _${session.taskSpecification}_
331
+
332
+ ## Files and Functions
333
+ _${session.filesAndFunctions}_
334
+
335
+ ## Workflow
336
+ _${session.workflow}_
337
+
338
+ ## Errors & Corrections
339
+ _${session.errorsAndCorrections}_
340
+
341
+ ## Codebase and System Documentation
342
+ _${session.codebaseDocumentation}_
343
+
344
+ ## Learnings
345
+ _${session.learnings}_
346
+
347
+ ## Key results
348
+ _${session.keyResults}_
349
+
350
+ ## Worklog
351
+ _${session.worklog}_
352
+ `;
353
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
354
+ await fs.writeFile(filePath, content, ENCODING);
355
+ }
356
+ /**
357
+ * Read session memory
358
+ */
359
+ export async function readSessionMemory(filePath) {
360
+ try {
361
+ const content = await fs.readFile(filePath, ENCODING);
362
+ // Parse sections
363
+ const session = {
364
+ sessionTitle: extractSection(content, 'Session Title') || '',
365
+ currentState: extractSection(content, 'Current State') || '',
366
+ taskSpecification: extractSection(content, 'Task specification') || '',
367
+ filesAndFunctions: extractSection(content, 'Files and Functions') || '',
368
+ workflow: extractSection(content, 'Workflow') || '',
369
+ errorsAndCorrections: extractSection(content, 'Errors & Corrections') || '',
370
+ codebaseDocumentation: extractSection(content, 'Codebase and System Documentation') || '',
371
+ learnings: extractSection(content, 'Learnings') || '',
372
+ keyResults: extractSection(content, 'Key results') || '',
373
+ worklog: extractSection(content, 'Worklog') || '',
374
+ };
375
+ return session;
376
+ }
377
+ catch {
378
+ return null;
379
+ }
380
+ }
381
+ function extractSection(content, sectionName) {
382
+ const regex = new RegExp(`## ${sectionName}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
383
+ const match = content.match(regex);
384
+ return match ? match[1].trim().replace(/^_|_$/g, '') : '';
385
+ }
386
+ // ========== Summary Operations ==========
387
+ /**
388
+ * Get summary file path
389
+ */
390
+ export function getSummaryPath(basePath, accountId, subtype, conversationId, yearMonth) {
391
+ return `${basePath}/accounts/${accountId}/${subtype === 'friend' ? 'friends' : 'chatrooms'}/${conversationId}/memory/summaries/${yearMonth}-summary.md`;
392
+ }
393
+ /**
394
+ * Write AI summary
395
+ */
396
+ export async function writeSummary(filePath, summary, keyPoints, learnings, actionItems) {
397
+ const now = new Date().toISOString();
398
+ const content = `---
399
+ name: 对话摘要
400
+ description: AI 生成的对话摘要
401
+ type: conversation
402
+ summary: true
403
+ generatedAt: ${now}
404
+ ---
405
+
406
+ # 对话摘要
407
+
408
+ ${summary}
409
+
410
+ ## 关键点
411
+
412
+ ${keyPoints.map(p => `- ${p}`).join('\n')}
413
+
414
+ ## 学习到的信息
415
+
416
+ ${learnings}
417
+
418
+ ${actionItems && actionItems.length > 0 ? `## 待跟进事项
419
+
420
+ ${actionItems.map(p => `- ${p}`).join('\n')}` : ''}
421
+ `;
422
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
423
+ await fs.writeFile(filePath, content, ENCODING);
424
+ }
425
+ // ========== Utility Functions ==========
426
+ /**
427
+ * Ensure directory exists
428
+ */
429
+ export async function ensureDir(dirPath) {
430
+ await fs.mkdir(dirPath, { recursive: true });
431
+ }
432
+ /**
433
+ * Delete file if exists
434
+ */
435
+ export async function deleteFile(filePath) {
436
+ try {
437
+ await fs.unlink(filePath);
438
+ }
439
+ catch (error) {
440
+ if (error.code !== 'ENOENT') {
441
+ throw error;
442
+ }
443
+ }
444
+ }
445
+ /**
446
+ * List files in directory
447
+ */
448
+ export async function listFiles(dirPath, pattern = '*') {
449
+ try {
450
+ const files = await fs.readdir(dirPath);
451
+ if (pattern === '*') {
452
+ return files;
453
+ }
454
+ // Simple glob matching
455
+ const regex = new RegExp(pattern.replace(/\*/g, '.*'));
456
+ return files.filter(f => regex.test(f));
457
+ }
458
+ catch {
459
+ return [];
460
+ }
461
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * WorkPhone WeChat Channel Plugin for OpenClaw
3
+ *
4
+ * This channel plugin connects OpenClaw to WorkPhone's WeChat API,
5
+ * enabling sending and receiving WeChat messages through the WorkPhone platform.
6
+ *
7
+ * IMPORTANT - Human Account Model:
8
+ * Unlike Telegram/WhatsApp bots, this connects to a real human WeChat account.
9
+ * The agent communicates with ALL friends and groups under that WeChat account,
10
+ * not just one DM context. This is "human mode" vs "bot mode".
11
+ *
12
+ * Includes Local Cache:
13
+ * - Per-account, per-user/conversation MD files
14
+ * - YAML frontmatter (aligned with Claude Code)
15
+ * - MEMORY.md indexing
16
+ * - 4-layer compression
17
+ * - AI summary extraction
18
+ * - SAAS connectivity + offline fallback
19
+ * - Cloud sync
20
+ */
21
+ import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
22
+ import { type WebhookPayload } from "./client.js";
23
+ export interface CelPhoneWeChatResolvedAccount {
24
+ accountId: string | null;
25
+ apiKey: string;
26
+ baseUrl: string;
27
+ wechatAccountId: string;
28
+ wechatId: string;
29
+ nickName: string;
30
+ allowFrom: string[];
31
+ dmPolicy: string | undefined;
32
+ }
33
+ /**
34
+ * Create the channel plugin
35
+ */
36
+ export declare const celPhoneWeChatPlugin: import("openclaw/plugin-sdk/core").ChannelPlugin<CelPhoneWeChatResolvedAccount, unknown, unknown>;
37
+ /**
38
+ * Helper function to handle inbound webhook messages
39
+ * This should be called from your HTTP route handler
40
+ *
41
+ * Integrates with Cache Manager for:
42
+ * - Local MD file caching
43
+ * - User profile storage
44
+ * - Session memory
45
+ * - Cloud sync
46
+ * - Offline fallback
47
+ */
48
+ export declare function handleInboundMessage(api: any, payload: WebhookPayload, cfg?: OpenClawConfig): Promise<void>;
49
+ /**
50
+ * Get user profile from cache (for agent use)
51
+ */
52
+ export declare function getUserProfile(accountId: string, wechatId: string): Promise<any>;
53
+ /**
54
+ * Update user profile in cache
55
+ */
56
+ export declare function updateUserProfile(accountId: string, wechatId: string, updates: any): Promise<void>;
57
+ /**
58
+ * Get SAAS connection status
59
+ */
60
+ export declare function getConnectionStatus(): import("./cache/types.js").ConnectionStatus;
61
+ /**
62
+ * Check if system is online (SAAS connected)
63
+ */
64
+ export declare function isOnline(): boolean;
65
+ export default celPhoneWeChatPlugin;