@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.
- package/INSTALL.md +231 -0
- package/README.md +259 -0
- package/dist/index-simple.js +9 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.js +77 -0
- package/dist/mock-server.d.ts +6 -0
- package/dist/mock-server.js +203 -0
- package/dist/openclaw.plugin.json +96 -0
- package/dist/setup-entry.d.ts +9 -0
- package/dist/setup-entry.js +8 -0
- package/dist/src/cache/compactor.d.ts +36 -0
- package/dist/src/cache/compactor.js +154 -0
- package/dist/src/cache/extractor.d.ts +48 -0
- package/dist/src/cache/extractor.js +120 -0
- package/dist/src/cache/index.d.ts +15 -0
- package/dist/src/cache/index.js +16 -0
- package/dist/src/cache/indexer.d.ts +41 -0
- package/dist/src/cache/indexer.js +262 -0
- package/dist/src/cache/manager.d.ts +113 -0
- package/dist/src/cache/manager.js +271 -0
- package/dist/src/cache/message-queue.d.ts +59 -0
- package/dist/src/cache/message-queue.js +147 -0
- package/dist/src/cache/saas-connector.d.ts +94 -0
- package/dist/src/cache/saas-connector.js +289 -0
- package/dist/src/cache/syncer.d.ts +60 -0
- package/dist/src/cache/syncer.js +177 -0
- package/dist/src/cache/types.d.ts +198 -0
- package/dist/src/cache/types.js +43 -0
- package/dist/src/cache/writer.d.ts +81 -0
- package/dist/src/cache/writer.js +461 -0
- package/dist/src/channel.d.ts +65 -0
- package/dist/src/channel.js +334 -0
- package/dist/src/client.d.ts +280 -0
- package/dist/src/client.js +248 -0
- package/index-simple.ts +11 -0
- package/index.ts +89 -0
- package/mock-server.ts +237 -0
- package/openclaw.plugin.json +98 -0
- package/package.json +37 -0
- package/setup-entry.ts +10 -0
- package/src/channel.ts +398 -0
- package/src/client.ts +412 -0
- package/test-cache.ts +260 -0
- package/test-integration.ts +319 -0
- package/tsconfig.json +22 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mock WorkPhone API Server for Testing
|
|
3
|
+
*
|
|
4
|
+
* 模拟 WorkPhone 的 API 端点,用于本地测试
|
|
5
|
+
*/
|
|
6
|
+
import express from 'express';
|
|
7
|
+
import { createServer } from 'http';
|
|
8
|
+
const app = express();
|
|
9
|
+
app.use(express.json());
|
|
10
|
+
// 配置
|
|
11
|
+
const MOCK_CONFIG = {
|
|
12
|
+
apiKey: 'test-api-key-12345',
|
|
13
|
+
wechatAccountId: 'mock-wechat-001',
|
|
14
|
+
wechatId: 'wxid_mock001',
|
|
15
|
+
baseUrl: 'http://localhost:18999',
|
|
16
|
+
};
|
|
17
|
+
// 存储消息
|
|
18
|
+
const messages = [];
|
|
19
|
+
const friends = [
|
|
20
|
+
{ wechatId: 'wxid_customer001', remark: '客户A', nickname: '张三' },
|
|
21
|
+
{ wechatId: 'wxid_customer002', remark: '客户B', nickname: '李四' },
|
|
22
|
+
{ wechatId: 'wxid_customer003', remark: '潜在客户', nickname: '王五' },
|
|
23
|
+
];
|
|
24
|
+
// ============ API 端点 ============
|
|
25
|
+
// 健康检查
|
|
26
|
+
app.get('/health', (req, res) => {
|
|
27
|
+
res.json({ status: 'ok' });
|
|
28
|
+
});
|
|
29
|
+
// 获取账号列表
|
|
30
|
+
app.get('/api/v1/wechat/accounts', (req, res) => {
|
|
31
|
+
const auth = req.headers.authorization;
|
|
32
|
+
if (auth !== `Bearer ${MOCK_CONFIG.apiKey}`) {
|
|
33
|
+
return res.status(401).json({ error: 'unauthorized' });
|
|
34
|
+
}
|
|
35
|
+
res.json({
|
|
36
|
+
data: [
|
|
37
|
+
{
|
|
38
|
+
accountId: MOCK_CONFIG.wechatAccountId,
|
|
39
|
+
wechatId: MOCK_CONFIG.wechatId,
|
|
40
|
+
nickname: '测试客服',
|
|
41
|
+
status: 'online',
|
|
42
|
+
}
|
|
43
|
+
]
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
// 获取好友列表
|
|
47
|
+
app.get(`/api/v1/wechat/accounts/${MOCK_CONFIG.wechatAccountId}/friends`, (req, res) => {
|
|
48
|
+
const auth = req.headers.authorization;
|
|
49
|
+
if (auth !== `Bearer ${MOCK_CONFIG.apiKey}`) {
|
|
50
|
+
return res.status(401).json({ error: 'unauthorized' });
|
|
51
|
+
}
|
|
52
|
+
res.json({ data: friends });
|
|
53
|
+
});
|
|
54
|
+
// 发送好友消息
|
|
55
|
+
app.post(`/api/v1/wechat/accounts/${MOCK_CONFIG.wechatAccountId}/messages/friend`, (req, res) => {
|
|
56
|
+
const auth = req.headers.authorization;
|
|
57
|
+
if (auth !== `Bearer ${MOCK_CONFIG.apiKey}`) {
|
|
58
|
+
return res.status(401).json({ error: 'unauthorized' });
|
|
59
|
+
}
|
|
60
|
+
const { friendWechatId, content, type } = req.body;
|
|
61
|
+
console.log(`[Mock] 发送消息给 ${friendWechatId}: ${content}`);
|
|
62
|
+
const messageId = `msg_${Date.now()}`;
|
|
63
|
+
messages.push({
|
|
64
|
+
messageId,
|
|
65
|
+
friendWechatId,
|
|
66
|
+
content,
|
|
67
|
+
type,
|
|
68
|
+
timestamp: Date.now(),
|
|
69
|
+
direction: 'outbound',
|
|
70
|
+
});
|
|
71
|
+
res.json({
|
|
72
|
+
code: 0,
|
|
73
|
+
message: 'success',
|
|
74
|
+
data: {
|
|
75
|
+
messageId,
|
|
76
|
+
msgSvrId: `svr_${Date.now()}`,
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
// 发送群消息
|
|
81
|
+
app.post(`/api/v1/wechat/accounts/${MOCK_CONFIG.wechatAccountId}/messages/chatroom`, (req, res) => {
|
|
82
|
+
const auth = req.headers.authorization;
|
|
83
|
+
if (auth !== `Bearer ${MOCK_CONFIG.apiKey}`) {
|
|
84
|
+
return res.status(401).json({ error: 'unauthorized' });
|
|
85
|
+
}
|
|
86
|
+
const { chatroomId, content, type, atUsers } = req.body;
|
|
87
|
+
console.log(`[Mock] 发送消息到群 ${chatroomId}: ${content}`);
|
|
88
|
+
const messageId = `msg_${Date.now()}`;
|
|
89
|
+
messages.push({
|
|
90
|
+
messageId,
|
|
91
|
+
chatroomId,
|
|
92
|
+
content,
|
|
93
|
+
type,
|
|
94
|
+
atUsers,
|
|
95
|
+
timestamp: Date.now(),
|
|
96
|
+
direction: 'outbound',
|
|
97
|
+
});
|
|
98
|
+
res.json({
|
|
99
|
+
code: 0,
|
|
100
|
+
message: 'success',
|
|
101
|
+
data: {
|
|
102
|
+
messageId,
|
|
103
|
+
msgSvrId: `svr_${Date.now()}`,
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
// Webhook 配置(返回当前 webhook 地址)
|
|
108
|
+
app.get(`/api/v1/wechat/accounts/${MOCK_CONFIG.wechatAccountId}/webhook`, (req, res) => {
|
|
109
|
+
res.json({
|
|
110
|
+
data: {
|
|
111
|
+
url: 'http://localhost:18999/webhook',
|
|
112
|
+
enabled: true,
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
// 设置 webhook
|
|
117
|
+
app.put(`/api/v1/wechat/accounts/${MOCK_CONFIG.wechatAccountId}/webhook`, (req, res) => {
|
|
118
|
+
const { url } = req.body;
|
|
119
|
+
console.log(`[Mock] Webhook 设置为: ${url}`);
|
|
120
|
+
res.json({ code: 0, message: 'success' });
|
|
121
|
+
});
|
|
122
|
+
// ============ Webhook 模拟触发 ============
|
|
123
|
+
// 模拟收到好友消息
|
|
124
|
+
app.post('/mock/trigger/friend-message', (req, res) => {
|
|
125
|
+
const { wechatId, content } = req.body;
|
|
126
|
+
const payload = {
|
|
127
|
+
event: 'message',
|
|
128
|
+
accountId: MOCK_CONFIG.wechatAccountId,
|
|
129
|
+
wechatAccountId: MOCK_CONFIG.wechatAccountId,
|
|
130
|
+
message: {
|
|
131
|
+
messageId: `in_msg_${Date.now()}`,
|
|
132
|
+
msgSvrId: `svr_${Date.now()}`,
|
|
133
|
+
fromUser: wechatId || 'wxid_customer001',
|
|
134
|
+
toUser: MOCK_CONFIG.wechatId,
|
|
135
|
+
content: content || '你好,这是测试消息',
|
|
136
|
+
type: 1,
|
|
137
|
+
timestamp: Date.now(),
|
|
138
|
+
isSelf: false,
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
// 回调(如果配置了 webhook)
|
|
142
|
+
// 实际使用时会让用户配置真实的 webhook URL
|
|
143
|
+
res.json({
|
|
144
|
+
code: 0,
|
|
145
|
+
message: 'triggered',
|
|
146
|
+
payload
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
// 模拟收到群消息
|
|
150
|
+
app.post('/mock/trigger/chatroom-message', (req, res) => {
|
|
151
|
+
const { chatroomId, content } = req.body;
|
|
152
|
+
const payload = {
|
|
153
|
+
event: 'message',
|
|
154
|
+
accountId: MOCK_CONFIG.wechatAccountId,
|
|
155
|
+
wechatAccountId: MOCK_CONFIG.wechatAccountId,
|
|
156
|
+
message: {
|
|
157
|
+
messageId: `in_msg_${Date.now()}`,
|
|
158
|
+
msgSvrId: `svr_${Date.now()}`,
|
|
159
|
+
fromUser: 'wxid_customer001',
|
|
160
|
+
chatroomId: chatroomId || '123456789@chatroom',
|
|
161
|
+
content: content || '大家好',
|
|
162
|
+
type: 1,
|
|
163
|
+
timestamp: Date.now(),
|
|
164
|
+
isSelf: false,
|
|
165
|
+
}
|
|
166
|
+
};
|
|
167
|
+
res.json({
|
|
168
|
+
code: 0,
|
|
169
|
+
message: 'triggered',
|
|
170
|
+
payload
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
// 获取消息历史
|
|
174
|
+
app.get('/mock/messages', (req, res) => {
|
|
175
|
+
res.json({ data: messages });
|
|
176
|
+
});
|
|
177
|
+
// 启动服务器
|
|
178
|
+
const PORT = 18999;
|
|
179
|
+
const server = createServer(app);
|
|
180
|
+
server.listen(PORT, () => {
|
|
181
|
+
console.log(`
|
|
182
|
+
╔═══════════════════════════════════════════════════════════╗
|
|
183
|
+
║ Mock WorkPhone API Server ║
|
|
184
|
+
╠═══════════════════════════════════════════════════════════╣
|
|
185
|
+
║ Base URL: http://localhost:${PORT} ║
|
|
186
|
+
║ API Key: ${MOCK_CONFIG.apiKey} ║
|
|
187
|
+
║ WeChat Account: ${MOCK_CONFIG.wechatAccountId} ║
|
|
188
|
+
╠═══════════════════════════════════════════════════════════╣
|
|
189
|
+
║ 测试端点: ║
|
|
190
|
+
║ - POST /mock/trigger/friend-message 模拟好友消息 ║
|
|
191
|
+
║ - POST /mock/trigger/chatroom-message 模拟群消息 ║
|
|
192
|
+
║ - GET /mock/messages 查看消息历史 ║
|
|
193
|
+
╚═══════════════════════════════════════════════════════════╝
|
|
194
|
+
`);
|
|
195
|
+
});
|
|
196
|
+
// 优雅关闭
|
|
197
|
+
process.on('SIGINT', () => {
|
|
198
|
+
console.log('\n[Mock] 关闭服务器...');
|
|
199
|
+
server.close(() => {
|
|
200
|
+
console.log('[Mock] 服务器已关闭');
|
|
201
|
+
process.exit(0);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://openclaw.ai/schema/plugin.json",
|
|
3
|
+
"id": "celphone-wechat",
|
|
4
|
+
"name": "WorkPhone WeChat",
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"description": "Connect OpenClaw to WorkPhone WeChat API for sending and receiving WeChat messages",
|
|
7
|
+
"author": {
|
|
8
|
+
"name": "GolaBeHive",
|
|
9
|
+
"url": "https://github.com/golabehive"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/golabehive/channels/celphone-wechat-plugin"
|
|
14
|
+
},
|
|
15
|
+
"keywords": ["openclaw", "channel", "wechat", "workphone", "messaging"],
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"type": "channel",
|
|
18
|
+
"capabilities": {
|
|
19
|
+
"inbound": {
|
|
20
|
+
"message": true,
|
|
21
|
+
"friend_request": true,
|
|
22
|
+
"group_invite": false
|
|
23
|
+
},
|
|
24
|
+
"outbound": {
|
|
25
|
+
"text": true,
|
|
26
|
+
"media": true,
|
|
27
|
+
"link": true,
|
|
28
|
+
"location": true,
|
|
29
|
+
"contact": true
|
|
30
|
+
},
|
|
31
|
+
"features": {
|
|
32
|
+
"pairing": false,
|
|
33
|
+
"reactions": false,
|
|
34
|
+
"editing": false,
|
|
35
|
+
"deleting": false,
|
|
36
|
+
"threads": true
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"configSchema": {
|
|
40
|
+
"type": "object",
|
|
41
|
+
"properties": {
|
|
42
|
+
"apiKey": {
|
|
43
|
+
"type": "string",
|
|
44
|
+
"description": "WorkPhone API key for authentication",
|
|
45
|
+
"secret": true
|
|
46
|
+
},
|
|
47
|
+
"baseUrl": {
|
|
48
|
+
"type": "string",
|
|
49
|
+
"description": "WorkPhone API base URL",
|
|
50
|
+
"default": "https://api.workphone.example.com"
|
|
51
|
+
},
|
|
52
|
+
"accountId": {
|
|
53
|
+
"type": "string",
|
|
54
|
+
"description": "WorkPhone account ID"
|
|
55
|
+
},
|
|
56
|
+
"wechatAccountId": {
|
|
57
|
+
"type": "string",
|
|
58
|
+
"description": "WeChat account ID to use (from WorkPhone) - REQUIRED"
|
|
59
|
+
},
|
|
60
|
+
"wechatId": {
|
|
61
|
+
"type": "string",
|
|
62
|
+
"description": "The actual WeChat ID (wxid_xxx) for this account"
|
|
63
|
+
},
|
|
64
|
+
"nickName": {
|
|
65
|
+
"type": "string",
|
|
66
|
+
"description": "Display name for this WeChat account"
|
|
67
|
+
},
|
|
68
|
+
"allowFrom": {
|
|
69
|
+
"type": "array",
|
|
70
|
+
"items": { "type": "string" },
|
|
71
|
+
"description": "List of WeChat IDs that can DM the agent",
|
|
72
|
+
"default": []
|
|
73
|
+
},
|
|
74
|
+
"dmSecurity": {
|
|
75
|
+
"type": "string",
|
|
76
|
+
"enum": ["allowlist", "blocklist", "allowall"],
|
|
77
|
+
"description": "DM security policy",
|
|
78
|
+
"default": "allowlist"
|
|
79
|
+
},
|
|
80
|
+
"webhookSecret": {
|
|
81
|
+
"type": "string",
|
|
82
|
+
"description": "Secret for verifying webhook requests",
|
|
83
|
+
"secret": true
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"required": ["apiKey", "wechatAccountId"]
|
|
87
|
+
},
|
|
88
|
+
"permissions": {
|
|
89
|
+
"channels": ["celphone-wechat"],
|
|
90
|
+
"http": {
|
|
91
|
+
"outbound": ["*"],
|
|
92
|
+
"inbound": ["/celphone-wechat/webhook"]
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"apiVersion": "1.0.0"
|
|
96
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Lightweight entry for loading during onboarding/config when channel is disabled
|
|
5
|
+
*/
|
|
6
|
+
declare const _default: {
|
|
7
|
+
plugin: import("openclaw/plugin-sdk/core").ChannelPlugin<import("./src/channel.js").CelPhoneWeChatResolvedAccount, unknown, unknown>;
|
|
8
|
+
};
|
|
9
|
+
export default _default;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setup Entry Point
|
|
3
|
+
*
|
|
4
|
+
* Lightweight entry for loading during onboarding/config when channel is disabled
|
|
5
|
+
*/
|
|
6
|
+
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
|
|
7
|
+
import { celPhoneWeChatPlugin } from "./src/channel.js";
|
|
8
|
+
export default defineSetupPluginEntry(celPhoneWeChatPlugin);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compactor - 4-layer compression system
|
|
3
|
+
*
|
|
4
|
+
* Aligned with Claude Code compression:
|
|
5
|
+
* - Layer 1: Microcompact (clean old tool results)
|
|
6
|
+
* - Layer 2: Auto-compact (context threshold)
|
|
7
|
+
* - Layer 3: Full compact (Fork Agent summary)
|
|
8
|
+
* - Layer 4: Session compact (use session.md)
|
|
9
|
+
*/
|
|
10
|
+
import type { CompactConfig } from './types.js';
|
|
11
|
+
import { DEFAULT_COMPACT_CONFIG } from './types.js';
|
|
12
|
+
/**
|
|
13
|
+
* Layer 1: Microcompact - clean old messages, keep recent N intact
|
|
14
|
+
*/
|
|
15
|
+
export declare function microCompact(filePath: string, keepCount?: number): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Layer 2: Auto-compact - check if context threshold exceeded
|
|
18
|
+
*/
|
|
19
|
+
export declare function shouldAutoCompact(config: CompactConfig, tokenEstimate: number): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Layer 3: Full compact - generate summary via LLM
|
|
22
|
+
*/
|
|
23
|
+
export declare function fullCompact(conversationPath: string, summary: string): Promise<boolean>;
|
|
24
|
+
/**
|
|
25
|
+
* Layer 4: Session compact - use session.md as summary
|
|
26
|
+
*/
|
|
27
|
+
export declare function getSessionSummary(sessionPath: string): string | null;
|
|
28
|
+
/**
|
|
29
|
+
* Determine which compaction layer to use
|
|
30
|
+
*/
|
|
31
|
+
export declare function runCompaction(config: CompactConfig, conversationPath: string, tokenEstimate: number, sessionPath?: string): Promise<'none' | 'micro' | 'auto' | 'full' | 'session'>;
|
|
32
|
+
/**
|
|
33
|
+
* Get token estimate for conversation
|
|
34
|
+
*/
|
|
35
|
+
export declare function estimateConversationTokens(conversationPath: string): Promise<number>;
|
|
36
|
+
export { CompactConfig, DEFAULT_COMPACT_CONFIG };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compactor - 4-layer compression system
|
|
3
|
+
*
|
|
4
|
+
* Aligned with Claude Code compression:
|
|
5
|
+
* - Layer 1: Microcompact (clean old tool results)
|
|
6
|
+
* - Layer 2: Auto-compact (context threshold)
|
|
7
|
+
* - Layer 3: Full compact (Fork Agent summary)
|
|
8
|
+
* - Layer 4: Session compact (use session.md)
|
|
9
|
+
*/
|
|
10
|
+
import { DEFAULT_COMPACT_CONFIG, } from './types.js';
|
|
11
|
+
import * as fs from 'fs/promises';
|
|
12
|
+
const ENCODING = 'utf-8';
|
|
13
|
+
const CONTENT_CLEARED = '[Old content cleared]';
|
|
14
|
+
/**
|
|
15
|
+
* Layer 1: Microcompact - clean old messages, keep recent N intact
|
|
16
|
+
*/
|
|
17
|
+
export async function microCompact(filePath, keepCount = 10) {
|
|
18
|
+
try {
|
|
19
|
+
const content = await fs.readFile(filePath, ENCODING);
|
|
20
|
+
const lines = content.split('\n');
|
|
21
|
+
// Find message blocks (lines starting with **[timestamp])
|
|
22
|
+
const messageLines = [];
|
|
23
|
+
for (let i = 0; i < lines.length; i++) {
|
|
24
|
+
if (lines[i].match(/^\*\*\[/)) {
|
|
25
|
+
messageLines.push(i);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (messageLines.length <= keepCount) {
|
|
29
|
+
return false; // Nothing to compact
|
|
30
|
+
}
|
|
31
|
+
// Clear old messages
|
|
32
|
+
const toKeep = messageLines.slice(-keepCount);
|
|
33
|
+
const toClear = messageLines.slice(0, -keepCount);
|
|
34
|
+
let newContent = lines.slice();
|
|
35
|
+
for (const idx of toClear) {
|
|
36
|
+
// Find the end of this message block
|
|
37
|
+
let endIdx = idx + 1;
|
|
38
|
+
while (endIdx < lines.length && lines[endIdx].trim() !== '' && !lines[endIdx].match(/^\*\*\[/)) {
|
|
39
|
+
endIdx++;
|
|
40
|
+
}
|
|
41
|
+
// Replace with cleared marker
|
|
42
|
+
newContent[idx] = CONTENT_CLEARED;
|
|
43
|
+
for (let i = idx + 1; i < endIdx; i++) {
|
|
44
|
+
newContent[i] = '';
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
await fs.writeFile(filePath, newContent.filter(l => l !== '').join('\n'), ENCODING);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
console.error('[Compactor] Microcompact failed:', error);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Layer 2: Auto-compact - check if context threshold exceeded
|
|
57
|
+
*/
|
|
58
|
+
export function shouldAutoCompact(config, tokenEstimate) {
|
|
59
|
+
return tokenEstimate >= config.autoCompactThreshold;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Layer 3: Full compact - generate summary via LLM
|
|
63
|
+
*/
|
|
64
|
+
export async function fullCompact(conversationPath, summary) {
|
|
65
|
+
try {
|
|
66
|
+
const compactedPath = conversationPath.replace('.md', '.compacted.md');
|
|
67
|
+
// Read existing content
|
|
68
|
+
const content = await fs.readFile(conversationPath, ENCODING);
|
|
69
|
+
// Write compacted version with summary
|
|
70
|
+
const compactedContent = `---
|
|
71
|
+
name: 压缩后的对话
|
|
72
|
+
description: 已压缩的对话记录
|
|
73
|
+
type: conversation
|
|
74
|
+
compacted: true
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
# 对话摘要
|
|
78
|
+
|
|
79
|
+
${summary}
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
# 原始记录(已压缩)
|
|
84
|
+
|
|
85
|
+
${content.slice(0, 5000)}... (see original file for full content)
|
|
86
|
+
`;
|
|
87
|
+
await fs.writeFile(compactedPath, compactedContent, ENCODING);
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
console.error('[Compactor] Full compact failed:', error);
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Layer 4: Session compact - use session.md as summary
|
|
97
|
+
*/
|
|
98
|
+
export function getSessionSummary(sessionPath) {
|
|
99
|
+
// Would read session.md and extract key information
|
|
100
|
+
// This is the fastest - no LLM needed
|
|
101
|
+
try {
|
|
102
|
+
const content = require('fs').readFileSync(sessionPath, ENCODING);
|
|
103
|
+
// Extract key sections
|
|
104
|
+
const learnings = extractSection(content, '## Learnings');
|
|
105
|
+
const keyResults = extractSection(content, '## Key results');
|
|
106
|
+
const currentState = extractSection(content, '## Current State');
|
|
107
|
+
return [currentState, learnings, keyResults].filter(Boolean).join('\n\n');
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function extractSection(content, section) {
|
|
114
|
+
const regex = new RegExp(`${section}\\s*\\n([\\s\\S]*?)(?=\\n## |$)`);
|
|
115
|
+
const match = content.match(regex);
|
|
116
|
+
return match ? match[1].trim() : null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Determine which compaction layer to use
|
|
120
|
+
*/
|
|
121
|
+
export async function runCompaction(config, conversationPath, tokenEstimate, sessionPath) {
|
|
122
|
+
// Layer 4: Session compact (fastest, preferred)
|
|
123
|
+
if (sessionPath && tokenEstimate >= config.sessionCompactThreshold) {
|
|
124
|
+
const sessionSummary = getSessionSummary(sessionPath);
|
|
125
|
+
if (sessionSummary) {
|
|
126
|
+
return 'session';
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Layer 3: Full compact
|
|
130
|
+
if (tokenEstimate >= config.fullCompactThreshold) {
|
|
131
|
+
return 'full';
|
|
132
|
+
}
|
|
133
|
+
// Layer 2: Auto compact
|
|
134
|
+
if (tokenEstimate >= config.autoCompactThreshold) {
|
|
135
|
+
return 'auto';
|
|
136
|
+
}
|
|
137
|
+
// Layer 1: Micro compact
|
|
138
|
+
const result = await microCompact(conversationPath, config.microCompactKeep);
|
|
139
|
+
return result ? 'micro' : 'none';
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Get token estimate for conversation
|
|
143
|
+
*/
|
|
144
|
+
export async function estimateConversationTokens(conversationPath) {
|
|
145
|
+
try {
|
|
146
|
+
const content = await fs.readFile(conversationPath, ENCODING);
|
|
147
|
+
// Rough estimate: 4 characters per token
|
|
148
|
+
return Math.ceil(content.length / 4);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export { DEFAULT_COMPACT_CONFIG };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Summary Extractor - Extract memories from conversations
|
|
3
|
+
*
|
|
4
|
+
* Aligned with Claude Code extractMemories:
|
|
5
|
+
* - Fork Agent execution
|
|
6
|
+
* - Tool permission whitelist
|
|
7
|
+
* - Trigger on conversation milestones
|
|
8
|
+
*/
|
|
9
|
+
import { ExtractionResult, WeChatMessage, ConversationSubtype } from './types.js';
|
|
10
|
+
export interface ExtractorConfig {
|
|
11
|
+
enabled: boolean;
|
|
12
|
+
triggerAfterMessages: number;
|
|
13
|
+
triggerAfterTokens: number;
|
|
14
|
+
maxTurns: number;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Check if extraction should be triggered
|
|
18
|
+
*/
|
|
19
|
+
export declare function shouldExtract(config: ExtractorConfig, messageCount: number, tokenEstimate: number): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Build extraction prompt for AI
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildExtractionPrompt(conversationId: string, messages: WeChatMessage[], subtype: ConversationSubtype): string;
|
|
24
|
+
/**
|
|
25
|
+
* Parse extraction result from AI response
|
|
26
|
+
*/
|
|
27
|
+
export declare function parseExtractionResult(response: string): ExtractionResult | null;
|
|
28
|
+
/**
|
|
29
|
+
* Simulate extraction (placeholder for actual LLM call)
|
|
30
|
+
* In real implementation, this would call the LLM via Fork Agent
|
|
31
|
+
*/
|
|
32
|
+
export declare function extractWithLLM(prompt: string, apiKey?: string): Promise<ExtractionResult | null>;
|
|
33
|
+
/**
|
|
34
|
+
* Validate tool is allowed (aligned with Claude Code)
|
|
35
|
+
*/
|
|
36
|
+
export declare function isToolAllowed(toolName: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Validate bash command is allowed (read-only)
|
|
39
|
+
*/
|
|
40
|
+
export declare function isBashCommandAllowed(command: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Get extraction trigger status
|
|
43
|
+
*/
|
|
44
|
+
export declare function getExtractionStatus(config: ExtractorConfig, lastExtractionMessageCount: number, currentMessageCount: number): {
|
|
45
|
+
shouldExtract: boolean;
|
|
46
|
+
messagesSinceLastExtraction: number;
|
|
47
|
+
};
|
|
48
|
+
export declare const DEFAULT_EXTRACTOR_CONFIG: ExtractorConfig;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Summary Extractor - Extract memories from conversations
|
|
3
|
+
*
|
|
4
|
+
* Aligned with Claude Code extractMemories:
|
|
5
|
+
* - Fork Agent execution
|
|
6
|
+
* - Tool permission whitelist
|
|
7
|
+
* - Trigger on conversation milestones
|
|
8
|
+
*/
|
|
9
|
+
// ========== Constants ==========
|
|
10
|
+
const ALLOWED_TOOLS = ['Read', 'Grep', 'Glob', 'Edit', 'Write'];
|
|
11
|
+
const ALLOWED_BASH_COMMANDS = ['ls', 'find', 'grep', 'cat', 'stat', 'wc', 'head', 'tail'];
|
|
12
|
+
/**
|
|
13
|
+
* Check if extraction should be triggered
|
|
14
|
+
*/
|
|
15
|
+
export function shouldExtract(config, messageCount, tokenEstimate) {
|
|
16
|
+
if (!config.enabled)
|
|
17
|
+
return false;
|
|
18
|
+
return (messageCount >= config.triggerAfterMessages ||
|
|
19
|
+
tokenEstimate >= config.triggerAfterTokens);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Build extraction prompt for AI
|
|
23
|
+
*/
|
|
24
|
+
export function buildExtractionPrompt(conversationId, messages, subtype) {
|
|
25
|
+
const messageList = messages
|
|
26
|
+
.slice(-20) // Last 20 messages
|
|
27
|
+
.map(m => {
|
|
28
|
+
const sender = m.isSelf ? '我' : (subtype === 'friend' ? '对方' : '群成员');
|
|
29
|
+
return `[${new Date(m.timestamp).toLocaleString()}] ${sender}: ${m.content}`;
|
|
30
|
+
})
|
|
31
|
+
.join('\n');
|
|
32
|
+
return `## 任务
|
|
33
|
+
|
|
34
|
+
分析以下与 ${conversationId} 的对话,提取关键信息并生成摘要。
|
|
35
|
+
|
|
36
|
+
## 对话内容
|
|
37
|
+
|
|
38
|
+
${messageList}
|
|
39
|
+
|
|
40
|
+
## 输出要求
|
|
41
|
+
|
|
42
|
+
请返回以下格式的 JSON:
|
|
43
|
+
|
|
44
|
+
{
|
|
45
|
+
"summary": "对话摘要 (2-3句话)",
|
|
46
|
+
"keyPoints": ["关键点1", "关键点2", "关键点3"],
|
|
47
|
+
"userProfileUpdate": {
|
|
48
|
+
"tags": ["如果发现新标签"],
|
|
49
|
+
"preferenceHints": "用户偏好提示",
|
|
50
|
+
"riskFlags": ["如果发现风险标记"]
|
|
51
|
+
},
|
|
52
|
+
"learnings": "从对话中学到的信息",
|
|
53
|
+
"actionItems": ["待跟进事项1", "待跟进事项2"]
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
只返回 JSON,不要其他内容。`;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Parse extraction result from AI response
|
|
60
|
+
*/
|
|
61
|
+
export function parseExtractionResult(response) {
|
|
62
|
+
try {
|
|
63
|
+
// Try to extract JSON from response
|
|
64
|
+
const jsonMatch = response.match(/\{[\s\S]*\}/);
|
|
65
|
+
if (!jsonMatch)
|
|
66
|
+
return null;
|
|
67
|
+
const result = JSON.parse(jsonMatch[0]);
|
|
68
|
+
return {
|
|
69
|
+
summary: result.summary || '',
|
|
70
|
+
keyPoints: result.keyPoints || [],
|
|
71
|
+
userProfileUpdate: result.userProfileUpdate || {},
|
|
72
|
+
learnings: result.learnings || '',
|
|
73
|
+
actionItems: result.actionItems || [],
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Simulate extraction (placeholder for actual LLM call)
|
|
82
|
+
* In real implementation, this would call the LLM via Fork Agent
|
|
83
|
+
*/
|
|
84
|
+
export async function extractWithLLM(prompt, apiKey) {
|
|
85
|
+
// Placeholder: In real implementation, this would:
|
|
86
|
+
// 1. Create Fork Agent with limited tools
|
|
87
|
+
// 2. Run with the prompt
|
|
88
|
+
// 3. Parse result
|
|
89
|
+
console.log('[Extractor] Would run extraction with prompt:', prompt.substring(0, 100) + '...');
|
|
90
|
+
// Return null for now - real implementation would call LLM
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Validate tool is allowed (aligned with Claude Code)
|
|
95
|
+
*/
|
|
96
|
+
export function isToolAllowed(toolName) {
|
|
97
|
+
return ALLOWED_TOOLS.includes(toolName);
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Validate bash command is allowed (read-only)
|
|
101
|
+
*/
|
|
102
|
+
export function isBashCommandAllowed(command) {
|
|
103
|
+
const cmd = command.split(' ')[0];
|
|
104
|
+
return ALLOWED_BASH_COMMANDS.includes(cmd);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get extraction trigger status
|
|
108
|
+
*/
|
|
109
|
+
export function getExtractionStatus(config, lastExtractionMessageCount, currentMessageCount) {
|
|
110
|
+
const messagesSinceLastExtraction = currentMessageCount - lastExtractionMessageCount;
|
|
111
|
+
const shouldExtract = shouldExtract(config, messagesSinceLastExtraction, 0 // Would calculate token estimate
|
|
112
|
+
);
|
|
113
|
+
return { shouldExtract, messagesSinceLastExtraction };
|
|
114
|
+
}
|
|
115
|
+
export const DEFAULT_EXTRACTOR_CONFIG = {
|
|
116
|
+
enabled: true,
|
|
117
|
+
triggerAfterMessages: 10,
|
|
118
|
+
triggerAfterTokens: 5000,
|
|
119
|
+
maxTurns: 5,
|
|
120
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cache Module Exports
|
|
3
|
+
*
|
|
4
|
+
* All cache-related modules for WeChat Channel Plugin
|
|
5
|
+
*/
|
|
6
|
+
export * from './types.js';
|
|
7
|
+
export * from './writer.js';
|
|
8
|
+
export * from './indexer.js';
|
|
9
|
+
export * from './extractor.js';
|
|
10
|
+
export * from './compactor.js';
|
|
11
|
+
export * from './syncer.js';
|
|
12
|
+
export * from './saas-connector.js';
|
|
13
|
+
export * from './message-queue.js';
|
|
14
|
+
export * from './manager.js';
|
|
15
|
+
export { createCacheManager, CacheManager } from './manager.js';
|