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.
Files changed (4) hide show
  1. package/filters.ts +2 -2
  2. package/index.ts +37 -6
  3. package/package.json +1 -1
  4. 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
- // 私聊中访问群记忆:按 _members.json 时间线过滤
119
+ // 私聊中访问群记忆:通过 API 获取成员资格,按时间线过滤
120
120
  const fileDate = extractDateFromPath(relPath);
121
121
  if (!fileDate) return false;
122
- return isWithinMembershipPeriod(workspaceDir, groupId, identity.peerId, fileDate);
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
- `[SYSTEM] 你是主人的私人助理,像正常对话一样自然回复。` +
78
+ `` +
79
79
  memoryBlock,
80
80
  };
81
81
 
82
82
  case "peer":
83
83
  return {
84
84
  prependContext:
85
- `[SYSTEM] 你是一个私人助理,像正常对话一样自然回复。\n` +
85
+ `` +
86
86
  memoryBlock,
87
87
  };
88
88
 
89
89
  case "group":
90
90
  return {
91
91
  prependContext:
92
- `[SYSTEM] 你是群助理,像正常对话一样自然回复。\n` +
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!, `[${time}] ${getUserId(identity)}: ${userText}\n`);
474
- if (assistantText) fs.appendFileSync(filePath!, `[${time}] 某只虾: ${assistantText}\n`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory-privacy",
3
- "version": "1.2.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "main": "index.ts",
6
6
  "files": [
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 MemberInfo {
14
- displayName: string;
13
+ export interface MemberGroupInfo {
14
+ group_id: string;
15
15
  periods: MemberPeriod[];
16
16
  }
17
17
 
18
- export interface MembersJson {
19
- groupName?: string;
20
- members: Record<string, MemberInfo>;
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
- * 加载 _members.json
73
+ * 根据 HELM_ENV 环境变量返回 API base URL
74
74
  */
75
- export function loadMembersRegistry(filePath: string): MembersJson | null {
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
- if (fs.existsSync(filePath)) {
78
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
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
- // ignore parse errors
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 registryPath = path.join(workspaceDir, "memory", "groups", groupId, "_members.json");
124
- const registry = loadMembersRegistry(registryPath);
125
-
126
- if (!registry) return false;
138
+ const response = fetchMemberGroupsSync(peerId);
139
+ if (!response || !response.groups) return false;
127
140
 
128
- // OpenClaw sessionKey 转小写,但 _members.json 里的 key 保留原始大小写
129
- const lowerPeerId = peerId.toLowerCase();
130
- const memberKey = Object.keys(registry.members).find(k => k.toLowerCase() === lowerPeerId);
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 member.periods.some((period) => {
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 的知识库 + _members.json 中当前在群的群组
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. 群组:_members.json members 的 key 匹配且 left === null
195
- const groupsDir = path.join(memDir, "groups");
196
- try {
197
- if (fs.existsSync(groupsDir)) {
198
- for (const groupId of fs.readdirSync(groupsDir)) {
199
- const groupPath = path.join(groupsDir, groupId);
200
- if (!fs.statSync(groupPath).isDirectory()) continue;
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
- } catch { /* ignore fs errors */ }
214
+ }
217
215
  }
218
216
 
219
217
  if (identity.type === "group") {