memory-privacy 1.2.0 → 1.5.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/filters.ts +2 -2
- package/index.ts +37 -6
- package/package.json +1 -1
- package/utils.ts +45 -47
package/filters.ts
CHANGED
|
@@ -116,10 +116,10 @@ function isPathAllowedRelative(
|
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
if (identity.type === "peer") {
|
|
119
|
-
//
|
|
119
|
+
// 私聊中访问群记忆:通过 API 获取成员资格,按时间线过滤
|
|
120
120
|
const fileDate = extractDateFromPath(relPath);
|
|
121
121
|
if (!fileDate) return false;
|
|
122
|
-
return isWithinMembershipPeriod(
|
|
122
|
+
return isWithinMembershipPeriod(groupId, identity.peerId, fileDate);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
125
|
return false;
|
package/index.ts
CHANGED
|
@@ -75,21 +75,21 @@ export default {
|
|
|
75
75
|
case "owner":
|
|
76
76
|
return {
|
|
77
77
|
prependContext:
|
|
78
|
-
|
|
78
|
+
`` +
|
|
79
79
|
memoryBlock,
|
|
80
80
|
};
|
|
81
81
|
|
|
82
82
|
case "peer":
|
|
83
83
|
return {
|
|
84
84
|
prependContext:
|
|
85
|
-
|
|
85
|
+
`` +
|
|
86
86
|
memoryBlock,
|
|
87
87
|
};
|
|
88
88
|
|
|
89
89
|
case "group":
|
|
90
90
|
return {
|
|
91
91
|
prependContext:
|
|
92
|
-
|
|
92
|
+
`` +
|
|
93
93
|
memoryBlock,
|
|
94
94
|
};
|
|
95
95
|
}
|
|
@@ -441,7 +441,7 @@ export default {
|
|
|
441
441
|
// 提取虾的最新回复(只取纯文本,过滤 toolCall)
|
|
442
442
|
const lastAssistantMsg = [...messages].reverse().find((m: any) => m.role === "assistant");
|
|
443
443
|
|
|
444
|
-
const userText = extractText(lastUserMsg);
|
|
444
|
+
const userText = cleanMessageContent(extractText(lastUserMsg));
|
|
445
445
|
const assistantText = extractText(lastAssistantMsg);
|
|
446
446
|
if (!userText && !assistantText) return;
|
|
447
447
|
|
|
@@ -470,14 +470,45 @@ export default {
|
|
|
470
470
|
// 文件不存在则创建(含目录)
|
|
471
471
|
ensureFileWithHeader(filePath!, header!);
|
|
472
472
|
// 追加写入(格式与 im-sync 一致)
|
|
473
|
-
if (userText) fs.appendFileSync(filePath!, `[${
|
|
474
|
-
if (assistantText) fs.appendFileSync(filePath!, `[${time}]
|
|
473
|
+
if (userText) fs.appendFileSync(filePath!, `[${getUserId(identity)}] ${userText}\n`);
|
|
474
|
+
if (assistantText) fs.appendFileSync(filePath!, `[${time}] AI助理: ${assistantText}\n`);
|
|
475
475
|
});
|
|
476
476
|
|
|
477
477
|
api.logger.info("[memory-privacy] Plugin registered successfully.");
|
|
478
478
|
},
|
|
479
479
|
};
|
|
480
480
|
|
|
481
|
+
/** 清洗消息内容,去除系统提示和 IM 元数据,只保留真实对话 */
|
|
482
|
+
function cleanMessageContent(text: string): string {
|
|
483
|
+
if (!text) return "";
|
|
484
|
+
|
|
485
|
+
// 策略:找到最后一个 IM 时间戳 [Day YYYY-MM-DD HH:MM GMT+N],取其后的内容
|
|
486
|
+
const imTsPattern = /\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun)\s+\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}\s+GMT[+-]\d+\]\s*/g;
|
|
487
|
+
let lastMatch: RegExpExecArray | null = null;
|
|
488
|
+
let m: RegExpExecArray | null;
|
|
489
|
+
while ((m = imTsPattern.exec(text)) !== null) {
|
|
490
|
+
lastMatch = m;
|
|
491
|
+
}
|
|
492
|
+
if (lastMatch) {
|
|
493
|
+
return text.slice(lastMatch.index + lastMatch[0].length).trim();
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 回退:逐步清除已知的元数据模式
|
|
497
|
+
let cleaned = text;
|
|
498
|
+
// [SYSTEM] 行
|
|
499
|
+
cleaned = cleaned.replace(/^\[SYSTEM\].*$/gm, "");
|
|
500
|
+
// Sender / Conversation info 元数据块(含 ```json...``` 代码块)
|
|
501
|
+
cleaned = cleaned.replace(/^(?:Sender|Conversation info) \(untrusted metadata\):\s*\n```(?:json)?\s*\n[\s\S]*?\n```/gm, "");
|
|
502
|
+
// 对话标题行
|
|
503
|
+
cleaned = cleaned.replace(/^#\s+.+$/gm, "");
|
|
504
|
+
// 历史记录行 [HH:MM] xxx: ...
|
|
505
|
+
cleaned = cleaned.replace(/^\[\d{2}:\d{2}\]\s+\S+?:.*$/gm, "");
|
|
506
|
+
// 压缩空行
|
|
507
|
+
cleaned = cleaned.replace(/\n{3,}/g, "\n\n").trim();
|
|
508
|
+
|
|
509
|
+
return cleaned;
|
|
510
|
+
}
|
|
511
|
+
|
|
481
512
|
/** 从消息中提取纯文本(过滤 toolCall) */
|
|
482
513
|
function extractText(msg: any): string {
|
|
483
514
|
if (!msg) return "";
|
package/package.json
CHANGED
package/utils.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "fs";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import { execSync } from "child_process";
|
|
3
4
|
import type { SessionIdentity } from "./identity.js";
|
|
4
5
|
|
|
5
6
|
// ========== 类型定义 ==========
|
|
@@ -7,17 +8,16 @@ import type { SessionIdentity } from "./identity.js";
|
|
|
7
8
|
export interface MemberPeriod {
|
|
8
9
|
joined: number; // Unix 毫秒时间戳
|
|
9
10
|
left: number | null;
|
|
10
|
-
reason?: string;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
export interface
|
|
14
|
-
|
|
13
|
+
export interface MemberGroupInfo {
|
|
14
|
+
group_id: string;
|
|
15
15
|
periods: MemberPeriod[];
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
-
export interface
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export interface MemberApiResponse {
|
|
19
|
+
user_id: string;
|
|
20
|
+
groups: MemberGroupInfo[];
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
export interface KnowledgeBaseRule {
|
|
@@ -67,22 +67,37 @@ export function extractKnowledgeBaseId(filePath: string): string {
|
|
|
67
67
|
return knowledgeIndex >= 0 && parts[knowledgeIndex + 1] ? parts[knowledgeIndex + 1] : "unknown-kb";
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
// ==========
|
|
70
|
+
// ========== API 调用 ==========
|
|
71
71
|
|
|
72
72
|
/**
|
|
73
|
-
*
|
|
73
|
+
* 根据 HELM_ENV 环境变量返回 API base URL
|
|
74
74
|
*/
|
|
75
|
-
export function
|
|
75
|
+
export function getApiBaseUrl(): string {
|
|
76
|
+
const env = (process.env.HELM_ENV || "dev").toLowerCase();
|
|
77
|
+
switch (env) {
|
|
78
|
+
case "prod": return "https://claw-server.csagentai.com";
|
|
79
|
+
case "staging": return "https://claw-server.csjkagent.com";
|
|
80
|
+
default: return "https://claw-server.csaiagent.com";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 同步调用 API 获取用户的群成员资格数据
|
|
86
|
+
* 使用 execSync + curl 实现同步 HTTP 请求
|
|
87
|
+
*/
|
|
88
|
+
export function fetchMemberGroupsSync(userId: string): MemberApiResponse | null {
|
|
76
89
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
}
|
|
90
|
+
const baseUrl = getApiBaseUrl();
|
|
91
|
+
const url = `${baseUrl}/api/members?user_id=${encodeURIComponent(userId)}`;
|
|
92
|
+
const result = execSync(`curl -s --max-time 5 "${url}"`, { encoding: "utf-8" });
|
|
93
|
+
return JSON.parse(result) as MemberApiResponse;
|
|
80
94
|
} catch {
|
|
81
|
-
|
|
95
|
+
return null;
|
|
82
96
|
}
|
|
83
|
-
return null;
|
|
84
97
|
}
|
|
85
98
|
|
|
99
|
+
// ========== 文件加载 ==========
|
|
100
|
+
|
|
86
101
|
/**
|
|
87
102
|
* 加载知识库 ACL(带 mtime 缓存)
|
|
88
103
|
*/
|
|
@@ -111,29 +126,25 @@ export function loadKnowledgeACL(workspaceDir: string): KnowledgeACL {
|
|
|
111
126
|
/**
|
|
112
127
|
* 检查某个用户在某个日期是否处于群成员期内
|
|
113
128
|
*
|
|
129
|
+
* 通过外部 API 获取成员资格数据
|
|
114
130
|
* 规则:fileTimestamp >= joined && (left === null || fileTimestamp < left)
|
|
115
131
|
* fileTimestamp = 文件日期 00:00:00 UTC
|
|
116
132
|
*/
|
|
117
133
|
export function isWithinMembershipPeriod(
|
|
118
|
-
workspaceDir: string,
|
|
119
134
|
groupId: string,
|
|
120
135
|
peerId: string,
|
|
121
136
|
fileDate: string // "2026-03-17"
|
|
122
137
|
): boolean {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (!registry) return false;
|
|
138
|
+
const response = fetchMemberGroupsSync(peerId);
|
|
139
|
+
if (!response || !response.groups) return false;
|
|
127
140
|
|
|
128
|
-
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
const member = memberKey ? registry.members[memberKey] : undefined;
|
|
132
|
-
if (!member || !member.periods || member.periods.length === 0) return false;
|
|
141
|
+
const lowerGroupId = groupId.toLowerCase();
|
|
142
|
+
const group = response.groups.find(g => g.group_id.toLowerCase() === lowerGroupId);
|
|
143
|
+
if (!group || !group.periods || group.periods.length === 0) return false;
|
|
133
144
|
|
|
134
145
|
const fileTimestamp = new Date(`${fileDate}T00:00:00Z`).getTime();
|
|
135
146
|
|
|
136
|
-
return
|
|
147
|
+
return group.periods.some((period) => {
|
|
137
148
|
const afterJoin = fileTimestamp >= period.joined;
|
|
138
149
|
const beforeLeft = period.left === null || fileTimestamp < period.left;
|
|
139
150
|
return afterJoin && beforeLeft;
|
|
@@ -160,7 +171,7 @@ export function toRelativeMemoryPath(absolutePath: string, workspaceDir: string)
|
|
|
160
171
|
/**
|
|
161
172
|
* 获取某个身份有权限访问的所有 memory 子目录(绝对路径列表)
|
|
162
173
|
*
|
|
163
|
-
* peer: peers/{peerId}/ + _acl.json 中 userids 包含 peerId 的知识库 +
|
|
174
|
+
* peer: peers/{peerId}/ + _acl.json 中 userids 包含 peerId 的知识库 + API 返回当前在群的群组
|
|
164
175
|
* group: groups/{groupId}/ + _acl.json 中 allowGroups 包含 groupId 的知识库
|
|
165
176
|
* owner: memory/ 整个目录
|
|
166
177
|
*/
|
|
@@ -191,29 +202,16 @@ export function getAllowedDirs(wsDir: string, identity: SessionIdentity): string
|
|
|
191
202
|
}
|
|
192
203
|
}
|
|
193
204
|
|
|
194
|
-
// 3.
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const membersPath = path.join(groupPath, "_members.json");
|
|
202
|
-
const registry = loadMembersRegistry(membersPath);
|
|
203
|
-
if (!registry) continue;
|
|
204
|
-
|
|
205
|
-
const memberKey = Object.keys(registry.members)
|
|
206
|
-
.find(k => k.toLowerCase() === lowerPeerId);
|
|
207
|
-
if (!memberKey) continue;
|
|
208
|
-
|
|
209
|
-
const member = registry.members[memberKey];
|
|
210
|
-
const lastPeriod = member.periods?.[member.periods.length - 1];
|
|
211
|
-
if (lastPeriod && lastPeriod.left === null) {
|
|
212
|
-
dirs.push(groupPath);
|
|
213
|
-
}
|
|
205
|
+
// 3. 群组:通过 API 获取当前在群的群组(last period.left === null)
|
|
206
|
+
const response = fetchMemberGroupsSync(identity.peerId);
|
|
207
|
+
if (response && response.groups) {
|
|
208
|
+
for (const group of response.groups) {
|
|
209
|
+
const lastPeriod = group.periods?.[group.periods.length - 1];
|
|
210
|
+
if (lastPeriod && lastPeriod.left === null) {
|
|
211
|
+
dirs.push(path.join(memDir, "groups", group.group_id));
|
|
214
212
|
}
|
|
215
213
|
}
|
|
216
|
-
}
|
|
214
|
+
}
|
|
217
215
|
}
|
|
218
216
|
|
|
219
217
|
if (identity.type === "group") {
|