openclaw-plugin-people-memory 0.0.0-stage → 0.0.2
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/DESIGN.md +182 -0
- package/LICENSE +21 -0
- package/README.md +214 -2
- package/assets/icon.png +0 -0
- package/automation/README.md +52 -0
- package/automation/cycle.template.md +51 -0
- package/dist/index.js +17 -0
- package/dist/src/core/db.js +967 -0
- package/dist/src/core/legacy.js +465 -0
- package/dist/src/core/paths.js +100 -0
- package/dist/src/core/platform.js +48 -0
- package/dist/src/core/schema.js +153 -0
- package/dist/src/core/service.js +1293 -0
- package/dist/src/core/time.js +27 -0
- package/dist/src/core/types.js +1 -0
- package/dist/src/plugin/cli.js +177 -0
- package/dist/src/plugin/config.js +64 -0
- package/dist/src/plugin/hooks.js +101 -0
- package/dist/src/plugin/identity.js +77 -0
- package/dist/src/plugin/runtime.js +72 -0
- package/dist/src/plugin/tools.js +649 -0
- package/openclaw.plugin.json +142 -0
- package/package.json +67 -4
- package/skills/people-identifier/SKILL.md +63 -0
|
@@ -0,0 +1,649 @@
|
|
|
1
|
+
import { resolvePlatform } from "../core/platform.js";
|
|
2
|
+
import { nowIso } from "../core/time.js";
|
|
3
|
+
function stringArg(args, key) {
|
|
4
|
+
const value = args[key];
|
|
5
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
6
|
+
}
|
|
7
|
+
function boolArg(args, key) {
|
|
8
|
+
return typeof args[key] === "boolean" ? args[key] : undefined;
|
|
9
|
+
}
|
|
10
|
+
function listArg(args, key) {
|
|
11
|
+
const value = args[key];
|
|
12
|
+
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
13
|
+
}
|
|
14
|
+
function textResult(text) {
|
|
15
|
+
return { content: [{ type: "text", text }], details: {} };
|
|
16
|
+
}
|
|
17
|
+
function rememberResult(label, result) {
|
|
18
|
+
if (result.traitUpdated && result.impressionStored)
|
|
19
|
+
return `已记录到 ${label},并更新了特征维度。`;
|
|
20
|
+
if (result.traitUpdated)
|
|
21
|
+
return `已更新 ${label} 的特征维度。`;
|
|
22
|
+
if (result.impressionStored)
|
|
23
|
+
return `已记录到 ${label}。`;
|
|
24
|
+
return "这条内容已存在,未重复写入。";
|
|
25
|
+
}
|
|
26
|
+
function toolService(runtime, ctx, workspace) {
|
|
27
|
+
return runtime.get({
|
|
28
|
+
workspaceDir: workspace || ctx.workspaceDir,
|
|
29
|
+
agentId: ctx.agentId,
|
|
30
|
+
agentDir: ctx.agentDir,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function currentPlatform(ctx, runtime) {
|
|
34
|
+
return resolvePlatform({
|
|
35
|
+
channel: ctx.messageChannel,
|
|
36
|
+
provider: ctx.messageChannel,
|
|
37
|
+
accountId: ctx.agentAccountId,
|
|
38
|
+
template: runtime.config.platformTemplate,
|
|
39
|
+
overrides: runtime.config.platformOverrides,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function currentAccount(ctx, runtime, service) {
|
|
43
|
+
const senderId = ctx.requesterSenderId?.trim();
|
|
44
|
+
if (!senderId)
|
|
45
|
+
return null;
|
|
46
|
+
return service.resolveAccountForLookup(currentPlatform(ctx, runtime), senderId);
|
|
47
|
+
}
|
|
48
|
+
function requireAccount(ctx, runtime, service) {
|
|
49
|
+
const account = currentAccount(ctx, runtime, service);
|
|
50
|
+
if (!account)
|
|
51
|
+
throw new Error("当前消息没有可识别的发送者,无法确定画像对象。");
|
|
52
|
+
return account;
|
|
53
|
+
}
|
|
54
|
+
function canManage(ctx) {
|
|
55
|
+
return ctx.senderIsOwner === true || !ctx.requesterSenderId?.trim();
|
|
56
|
+
}
|
|
57
|
+
function resolveAccountRef(service, ref) {
|
|
58
|
+
const matches = service.searchAccounts(ref);
|
|
59
|
+
if (matches.length === 0)
|
|
60
|
+
throw new Error(`没有找到账号:${ref}`);
|
|
61
|
+
if (matches.length > 1) {
|
|
62
|
+
const exact = matches.filter((account) => account.account_id === ref ||
|
|
63
|
+
account.display_name === ref ||
|
|
64
|
+
account.confirmed_name === ref ||
|
|
65
|
+
account.sender_id === ref);
|
|
66
|
+
if (exact.length === 1)
|
|
67
|
+
return exact[0];
|
|
68
|
+
throw new Error(`匹配到多个账号,请用 account_id 精确指定:${matches
|
|
69
|
+
.slice(0, 5)
|
|
70
|
+
.map((account) => `${account.account_id}(${account.display_name ?? "?"})`)
|
|
71
|
+
.join(", ")}`);
|
|
72
|
+
}
|
|
73
|
+
return matches[0];
|
|
74
|
+
}
|
|
75
|
+
function formatStatus(service) {
|
|
76
|
+
const status = service.status();
|
|
77
|
+
const counts = status.counts;
|
|
78
|
+
return [
|
|
79
|
+
"识人库状态",
|
|
80
|
+
`工作区:${status.workspaceDir}`,
|
|
81
|
+
`数据目录:${status.dataDir}`,
|
|
82
|
+
`数据库:${status.dbPath}(schema v${status.schemaVersion})`,
|
|
83
|
+
`账号 ${counts.accounts ?? 0} · 人物 ${counts.persons ?? 0} · 绑定 ${counts.person_accounts ?? 0} · 群 ${counts.groups ?? 0}`,
|
|
84
|
+
`群成员 ${counts.group_members ?? 0} · 群记忆 ${counts.group_events ?? 0} · 印象 ${counts.impressions ?? 0} · 特征 ${counts.traits ?? 0}`,
|
|
85
|
+
`活跃维度:${status.dimensions.filter((dimension) => dimension.active).map((dimension) => dimension.key).join(", ")}`,
|
|
86
|
+
`最近全量扫描:${status.lastFullScanAt ?? "无记录"}`,
|
|
87
|
+
`旧 JSON 导入:${status.legacyImportedAt ?? "无"}`,
|
|
88
|
+
].join("\n");
|
|
89
|
+
}
|
|
90
|
+
function formatValidation(report) {
|
|
91
|
+
const lines = [
|
|
92
|
+
`${report.ok ? "✅" : "❌"} 校验 ${report.ok ? "通过" : "失败"}`,
|
|
93
|
+
`数据库:${report.dbPath}`,
|
|
94
|
+
`计数:${Object.entries(report.counts).map(([key, value]) => `${key}=${value}`).join(" ")}`,
|
|
95
|
+
];
|
|
96
|
+
if (report.issues.length === 0) {
|
|
97
|
+
lines.push("没有发现问题。");
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
for (const issue of report.issues.slice(0, 40)) {
|
|
101
|
+
lines.push(`· [${issue.level}] ${issue.code}: ${issue.message}`);
|
|
102
|
+
}
|
|
103
|
+
if (report.issues.length > 40)
|
|
104
|
+
lines.push(`… 另有 ${report.issues.length - 40} 条`);
|
|
105
|
+
}
|
|
106
|
+
return lines.join("\n");
|
|
107
|
+
}
|
|
108
|
+
export function registerTools(api, runtime) {
|
|
109
|
+
const register = (name, description, parameters, handler) => {
|
|
110
|
+
api.registerTool(((ctx) => ({
|
|
111
|
+
name,
|
|
112
|
+
label: name,
|
|
113
|
+
description,
|
|
114
|
+
parameters: parameters,
|
|
115
|
+
execute: async (_toolCallId, rawArgs) => {
|
|
116
|
+
const args = (rawArgs ?? {});
|
|
117
|
+
return (await handler(args, ctx));
|
|
118
|
+
},
|
|
119
|
+
})), { name });
|
|
120
|
+
};
|
|
121
|
+
register("people_status", "查看当前工作区识人库的状态和计数。", { type: "object", additionalProperties: false, properties: { workspace: { type: "string" } } }, (args, ctx) => {
|
|
122
|
+
const service = toolService(runtime, ctx, stringArg(args, "workspace"));
|
|
123
|
+
return textResult(formatStatus(service));
|
|
124
|
+
});
|
|
125
|
+
register("people_lookup", "查询当前对话对象、指定账号、指定人物或群的画像与记忆。默认查自己;查他人仅限 owner。", {
|
|
126
|
+
type: "object",
|
|
127
|
+
additionalProperties: false,
|
|
128
|
+
properties: {
|
|
129
|
+
query: { type: "string", description: "账号 ID、显示名、person_id 或人物名;省略表示当前发送者。" },
|
|
130
|
+
account: { type: "string", description: "账号 ID 或显示名,等价于 query。" },
|
|
131
|
+
person: { type: "string", description: "人物 ID(p_ 开头)或人物名。" },
|
|
132
|
+
group: { type: "string", description: "群 ID 或 chat_id;提供时查群记忆。" },
|
|
133
|
+
full: { type: "boolean", description: "返回完整 traits 和印象列表(含 ID),用于画像归并;普通展示用默认卡片。" },
|
|
134
|
+
},
|
|
135
|
+
}, (args, ctx) => {
|
|
136
|
+
const service = toolService(runtime, ctx);
|
|
137
|
+
const full = boolArg(args, "full") === true;
|
|
138
|
+
const groupRef = stringArg(args, "group");
|
|
139
|
+
if (groupRef) {
|
|
140
|
+
const group = service.resolveGroup(groupRef);
|
|
141
|
+
if (!group)
|
|
142
|
+
throw new Error(`没有找到群:${groupRef}。可以只传数字群号。`);
|
|
143
|
+
return textResult(service.formatGroup(group));
|
|
144
|
+
}
|
|
145
|
+
const self = currentAccount(ctx, runtime, service);
|
|
146
|
+
const query = stringArg(args, "query");
|
|
147
|
+
const accountArg = stringArg(args, "account");
|
|
148
|
+
const personArg = stringArg(args, "person");
|
|
149
|
+
const targetRef = accountArg ?? personArg ?? query;
|
|
150
|
+
if (!targetRef || targetRef === "self" || targetRef === "me" || targetRef === "我") {
|
|
151
|
+
if (!self)
|
|
152
|
+
throw new Error("当前消息没有可识别的发送者。");
|
|
153
|
+
return textResult(service.formatLookup(self, full));
|
|
154
|
+
}
|
|
155
|
+
const isOwner = ctx.senderIsOwner === true;
|
|
156
|
+
// Internal turns (cron, isolated jobs) have no requester sender. They are
|
|
157
|
+
// already trusted to run with the agent's own tool policy, so allow
|
|
158
|
+
// arbitrary lookups instead of treating them as a non-owner stranger.
|
|
159
|
+
const isInternal = !ctx.requesterSenderId?.trim();
|
|
160
|
+
if (!isOwner && !isInternal) {
|
|
161
|
+
if (!self)
|
|
162
|
+
throw new Error("无法识别你的身份,暂时不能查询他人画像。");
|
|
163
|
+
const selfMatches = targetRef === self.account_id ||
|
|
164
|
+
targetRef === self.sender_id ||
|
|
165
|
+
targetRef === self.display_name ||
|
|
166
|
+
targetRef === self.confirmed_name ||
|
|
167
|
+
targetRef === self.call_name ||
|
|
168
|
+
targetRef === self.bound_person_id;
|
|
169
|
+
if (!selfMatches)
|
|
170
|
+
throw new Error("只能查询自己的画像。");
|
|
171
|
+
return textResult(service.formatLookup(self, full));
|
|
172
|
+
}
|
|
173
|
+
const personRef = personArg ?? (query?.startsWith("p_") ? query : undefined);
|
|
174
|
+
if (personRef) {
|
|
175
|
+
let person = personRef.startsWith("p_") ? service.db.getPerson(personRef) : undefined;
|
|
176
|
+
if (!person) {
|
|
177
|
+
const matches = service.db.searchPersons(personRef, 5);
|
|
178
|
+
if (matches.length > 1) {
|
|
179
|
+
throw new Error(`找到多个人物匹配「${personRef}」:${matches.map((item) => `${item.person_id}(${item.call_name || item.name || "未命名"})`).join("、")}。请用 person_id。`);
|
|
180
|
+
}
|
|
181
|
+
person = matches[0];
|
|
182
|
+
}
|
|
183
|
+
if (!person)
|
|
184
|
+
throw new Error(`没有找到人物:${personRef}`);
|
|
185
|
+
return textResult(service.formatPersonLookup(person.person_id, full));
|
|
186
|
+
}
|
|
187
|
+
const accountRef = accountArg ?? query;
|
|
188
|
+
if (!accountRef)
|
|
189
|
+
throw new Error("请提供 query、account、person 或 group。");
|
|
190
|
+
try {
|
|
191
|
+
return textResult(service.formatLookup(resolveAccountRef(service, accountRef), full));
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
const matches = service.db.searchPersons(accountRef, 5);
|
|
195
|
+
if (matches.length === 1) {
|
|
196
|
+
return textResult(service.formatPersonLookup(matches[0].person_id, full));
|
|
197
|
+
}
|
|
198
|
+
if (matches.length > 1) {
|
|
199
|
+
throw new Error(`找到多个人物匹配「${accountRef}」:${matches.map((item) => `${item.person_id}(${item.call_name || item.name || "未命名"})`).join("、")}。请用 person_id。`);
|
|
200
|
+
}
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
register("people_remember", "记录一条关于某人的事实或印象;可选写入某个特征维度。默认记录当前发送者。", {
|
|
205
|
+
type: "object",
|
|
206
|
+
additionalProperties: false,
|
|
207
|
+
required: ["content"],
|
|
208
|
+
properties: {
|
|
209
|
+
content: { type: "string", description: "要记录的事实,尽量保留原始表述。" },
|
|
210
|
+
dimension: { type: "string", description: "可选特征维度 key,例如 basic_origin、preference。" },
|
|
211
|
+
mode: { type: "string", enum: ["append", "replace"], description: "维度值写入方式,默认 append。" },
|
|
212
|
+
account: { type: "string", description: "目标账号;省略表示当前发送者。仅 owner 可指定他人。" },
|
|
213
|
+
person: { type: "string", description: "目标人物 ID(p_ 开头)。仅 owner 或内部维护任务可用,用于整理人物级画像。" },
|
|
214
|
+
source: { type: "string", description: "来源标记,例如 chat、manual、scan。" },
|
|
215
|
+
},
|
|
216
|
+
}, (args, ctx) => {
|
|
217
|
+
const service = toolService(runtime, ctx);
|
|
218
|
+
const content = stringArg(args, "content");
|
|
219
|
+
if (!content)
|
|
220
|
+
throw new Error("content 不能为空。");
|
|
221
|
+
const personId = stringArg(args, "person");
|
|
222
|
+
if (personId) {
|
|
223
|
+
if (!canManage(ctx))
|
|
224
|
+
throw new Error("只有 owner 或内部维护任务可以整理人物级画像。");
|
|
225
|
+
const person = service.db.getPerson(personId);
|
|
226
|
+
if (!person)
|
|
227
|
+
throw new Error(`没有找到人物:${personId}`);
|
|
228
|
+
const modeRaw = stringArg(args, "mode");
|
|
229
|
+
const result = service.remember({
|
|
230
|
+
personId: person.person_id,
|
|
231
|
+
content,
|
|
232
|
+
dimension: stringArg(args, "dimension"),
|
|
233
|
+
mode: modeRaw === "replace" ? "replace" : "append",
|
|
234
|
+
agentId: ctx.agentId,
|
|
235
|
+
source: stringArg(args, "source") ?? "tool",
|
|
236
|
+
});
|
|
237
|
+
return textResult(rememberResult(person.call_name || person.name || person.person_id, result));
|
|
238
|
+
}
|
|
239
|
+
const self = currentAccount(ctx, runtime, service);
|
|
240
|
+
const accountRef = stringArg(args, "account");
|
|
241
|
+
const target = accountRef ? resolveAccountRef(service, accountRef) : self;
|
|
242
|
+
if (!target)
|
|
243
|
+
throw new Error("当前消息没有可识别的发送者,请用 account 指定目标。");
|
|
244
|
+
if (target.account_id !== self?.account_id && !canManage(ctx)) {
|
|
245
|
+
throw new Error("只能记录自己的信息。");
|
|
246
|
+
}
|
|
247
|
+
const modeRaw = stringArg(args, "mode");
|
|
248
|
+
const result = service.remember({
|
|
249
|
+
accountId: target.account_id,
|
|
250
|
+
content,
|
|
251
|
+
dimension: stringArg(args, "dimension"),
|
|
252
|
+
mode: modeRaw === "replace" ? "replace" : "append",
|
|
253
|
+
agentId: ctx.agentId,
|
|
254
|
+
source: stringArg(args, "source") ?? "tool",
|
|
255
|
+
});
|
|
256
|
+
return textResult(rememberResult(target.display_name ?? target.account_id, result));
|
|
257
|
+
});
|
|
258
|
+
register("people_name", "设置称呼:本人钦定的 call_name(优先使用),或 confirmed_name / 人物名。默认操作当前发送者。", {
|
|
259
|
+
type: "object",
|
|
260
|
+
additionalProperties: false,
|
|
261
|
+
properties: {
|
|
262
|
+
call_name: { type: "string", description: "本人指定的称呼,例如「阿龙」。写入后不会被昵称同步覆盖。" },
|
|
263
|
+
confirmed_name: { type: "string", description: "本人确认的名字。" },
|
|
264
|
+
person_name: { type: "string", description: "绑定人物的正式名字(仅 owner)。" },
|
|
265
|
+
account: { type: "string", description: "目标账号;省略表示当前发送者。仅 owner 可指定他人。" },
|
|
266
|
+
},
|
|
267
|
+
}, (args, ctx) => {
|
|
268
|
+
const service = toolService(runtime, ctx);
|
|
269
|
+
const self = currentAccount(ctx, runtime, service);
|
|
270
|
+
const accountRef = stringArg(args, "account");
|
|
271
|
+
const target = accountRef ? resolveAccountRef(service, accountRef) : self;
|
|
272
|
+
if (!target)
|
|
273
|
+
throw new Error("当前消息没有可识别的发送者,请用 account 指定目标。");
|
|
274
|
+
if (target.account_id !== self?.account_id && !canManage(ctx)) {
|
|
275
|
+
throw new Error("只能设置自己的称呼。");
|
|
276
|
+
}
|
|
277
|
+
if (stringArg(args, "person_name") && ctx.senderIsOwner !== true) {
|
|
278
|
+
throw new Error("只有 owner 可以修改人物正式名字。");
|
|
279
|
+
}
|
|
280
|
+
const result = service.setName({
|
|
281
|
+
accountId: target.account_id,
|
|
282
|
+
callName: stringArg(args, "call_name"),
|
|
283
|
+
confirmedName: stringArg(args, "confirmed_name"),
|
|
284
|
+
personName: stringArg(args, "person_name"),
|
|
285
|
+
});
|
|
286
|
+
return textResult(`已更新${result.target === "person" ? "人物" : "账号"}称呼${result.callName ? `:${result.callName}` : "。"}`);
|
|
287
|
+
});
|
|
288
|
+
register("people_note", "向当前群或指定群追加一条群记忆。", {
|
|
289
|
+
type: "object",
|
|
290
|
+
additionalProperties: false,
|
|
291
|
+
required: ["summary"],
|
|
292
|
+
properties: {
|
|
293
|
+
summary: { type: "string" },
|
|
294
|
+
category: { type: "string", description: "milestone / rule / observation / personal_info 等。" },
|
|
295
|
+
tags: { type: "array", items: { type: "string" } },
|
|
296
|
+
group: { type: "string", description: "群 ID 或 chat_id;省略表示当前群。" },
|
|
297
|
+
},
|
|
298
|
+
}, (args, ctx) => {
|
|
299
|
+
const service = toolService(runtime, ctx);
|
|
300
|
+
const summary = stringArg(args, "summary");
|
|
301
|
+
if (!summary)
|
|
302
|
+
throw new Error("summary 不能为空。");
|
|
303
|
+
const groupRef = stringArg(args, "group") ?? ctx.nativeChannelId ?? "";
|
|
304
|
+
if (!groupRef)
|
|
305
|
+
throw new Error("当前会话没有群标识,请显式传 group。");
|
|
306
|
+
const group = service.resolveGroupStrict(groupRef);
|
|
307
|
+
const result = service.note({
|
|
308
|
+
groupId: group.group_id,
|
|
309
|
+
summary,
|
|
310
|
+
category: stringArg(args, "category"),
|
|
311
|
+
tags: listArg(args, "tags"),
|
|
312
|
+
agentId: ctx.agentId,
|
|
313
|
+
});
|
|
314
|
+
return textResult(result.stored ? "已写入群记忆。" : "同一天已有相同记忆,未重复写入。");
|
|
315
|
+
});
|
|
316
|
+
register("people_bind", "把账号绑定到人物(仅 owner)。绑定后画像合并到人物级。", {
|
|
317
|
+
type: "object",
|
|
318
|
+
additionalProperties: false,
|
|
319
|
+
required: ["account"],
|
|
320
|
+
properties: {
|
|
321
|
+
account: { type: "string", description: "账号 ID 或可唯一定位的显示名。" },
|
|
322
|
+
person_id: { type: "string" },
|
|
323
|
+
name: { type: "string", description: "人物名;不传则用账号显示名。" },
|
|
324
|
+
confidence: { type: "string", enum: ["confirmed", "tentative"] },
|
|
325
|
+
},
|
|
326
|
+
}, (args, ctx) => {
|
|
327
|
+
if (ctx.senderIsOwner !== true)
|
|
328
|
+
throw new Error("只有 owner 可以绑定人物。");
|
|
329
|
+
const service = toolService(runtime, ctx);
|
|
330
|
+
const account = resolveAccountRef(service, stringArg(args, "account") ?? "");
|
|
331
|
+
const result = service.bind({
|
|
332
|
+
accountId: account.account_id,
|
|
333
|
+
personId: stringArg(args, "person_id"),
|
|
334
|
+
name: stringArg(args, "name"),
|
|
335
|
+
confidence: stringArg(args, "confidence"),
|
|
336
|
+
});
|
|
337
|
+
return textResult(`已绑定 ${account.account_id} → ${result.personId}${result.createdPerson ? "(新建人物)" : ""};迁移印象 ${result.moved.impressions} 条、特征 ${result.moved.traits} 个。`);
|
|
338
|
+
});
|
|
339
|
+
register("people_forget", "删除匹配到的印象或特征。默认只能操作自己;owner 可指定账号。", {
|
|
340
|
+
type: "object",
|
|
341
|
+
additionalProperties: false,
|
|
342
|
+
required: ["match"],
|
|
343
|
+
properties: {
|
|
344
|
+
match: { type: "string", description: "要删除的内容片段。" },
|
|
345
|
+
account: { type: "string" },
|
|
346
|
+
person: { type: "string", description: "目标人物 ID(p_ 开头)。仅 owner 或内部维护任务可用。" },
|
|
347
|
+
scope: {
|
|
348
|
+
type: "string",
|
|
349
|
+
enum: ["all", "impressions", "traits"],
|
|
350
|
+
description: "删除范围,默认 all。画像归并后用 impressions,避免误删 traits。",
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
}, (args, ctx) => {
|
|
354
|
+
const service = toolService(runtime, ctx);
|
|
355
|
+
const match = stringArg(args, "match");
|
|
356
|
+
if (!match)
|
|
357
|
+
throw new Error("match 不能为空。");
|
|
358
|
+
const scopeRaw = stringArg(args, "scope");
|
|
359
|
+
const scope = scopeRaw === "impressions" || scopeRaw === "traits" ? scopeRaw : "all";
|
|
360
|
+
const personId = stringArg(args, "person");
|
|
361
|
+
if (personId) {
|
|
362
|
+
if (!canManage(ctx))
|
|
363
|
+
throw new Error("只有 owner 或内部维护任务可以整理人物级画像。");
|
|
364
|
+
const person = service.db.getPerson(personId);
|
|
365
|
+
if (!person)
|
|
366
|
+
throw new Error(`没有找到人物:${personId}`);
|
|
367
|
+
const result = service.forget({ personId: person.person_id, match, scope });
|
|
368
|
+
return textResult(`已删除印象 ${result.impressions} 条、特征 ${result.traits} 个。`);
|
|
369
|
+
}
|
|
370
|
+
const self = currentAccount(ctx, runtime, service);
|
|
371
|
+
const accountRef = stringArg(args, "account");
|
|
372
|
+
const target = accountRef ? resolveAccountRef(service, accountRef) : self;
|
|
373
|
+
if (!target)
|
|
374
|
+
throw new Error("当前消息没有可识别的发送者,请用 account 指定目标。");
|
|
375
|
+
if (!canManage(ctx) && target.account_id !== self?.account_id) {
|
|
376
|
+
throw new Error("只能删除自己的信息。");
|
|
377
|
+
}
|
|
378
|
+
const result = service.forget({ accountId: target.account_id, match, scope });
|
|
379
|
+
return textResult(`已删除印象 ${result.impressions} 条、特征 ${result.traits} 个。`);
|
|
380
|
+
});
|
|
381
|
+
register("people_validate", "校验当前工作区识人库的一致性。owner 可选择自动修复可修复项。", {
|
|
382
|
+
type: "object",
|
|
383
|
+
additionalProperties: false,
|
|
384
|
+
properties: {
|
|
385
|
+
fix: { type: "boolean" },
|
|
386
|
+
workspace: { type: "string" },
|
|
387
|
+
},
|
|
388
|
+
}, (args, ctx) => {
|
|
389
|
+
const service = toolService(runtime, ctx, stringArg(args, "workspace"));
|
|
390
|
+
const fix = boolArg(args, "fix") === true;
|
|
391
|
+
if (fix && !canManage(ctx))
|
|
392
|
+
throw new Error("只有 owner 或本工作区的维护任务可以自动修复。");
|
|
393
|
+
const actions = fix ? service.repair() : [];
|
|
394
|
+
const report = service.validate();
|
|
395
|
+
const text = formatValidation(report);
|
|
396
|
+
return textResult(actions.length > 0 ? `${text}\n\n修复:${actions.join(";")}` : text);
|
|
397
|
+
});
|
|
398
|
+
register("people_scan_mark", "记录某个会话/频道的扫描进度;full=true 时同时更新全量扫描时间。供扫描任务调用。", {
|
|
399
|
+
type: "object",
|
|
400
|
+
additionalProperties: false,
|
|
401
|
+
required: ["channel"],
|
|
402
|
+
properties: {
|
|
403
|
+
channel: { type: "string", description: "频道或会话 key。" },
|
|
404
|
+
displayName: { type: "string" },
|
|
405
|
+
kind: { type: "string" },
|
|
406
|
+
platform: { type: "string" },
|
|
407
|
+
group: { type: "string" },
|
|
408
|
+
lastScanAt: { type: "string" },
|
|
409
|
+
full: { type: "boolean" },
|
|
410
|
+
},
|
|
411
|
+
}, (args, ctx) => {
|
|
412
|
+
const service = toolService(runtime, ctx);
|
|
413
|
+
const channel = stringArg(args, "channel");
|
|
414
|
+
if (!channel)
|
|
415
|
+
throw new Error("channel 不能为空。");
|
|
416
|
+
const lastScanAt = stringArg(args, "lastScanAt") ?? nowIso();
|
|
417
|
+
service.db.upsertScanChannel({
|
|
418
|
+
channelKey: channel,
|
|
419
|
+
displayName: stringArg(args, "displayName"),
|
|
420
|
+
kind: stringArg(args, "kind"),
|
|
421
|
+
platform: stringArg(args, "platform"),
|
|
422
|
+
groupId: stringArg(args, "group"),
|
|
423
|
+
lastScanAt,
|
|
424
|
+
});
|
|
425
|
+
if (boolArg(args, "full") === true)
|
|
426
|
+
service.db.setScanMeta(lastScanAt);
|
|
427
|
+
return textResult(`已记录扫描进度:${channel} @ ${lastScanAt}`);
|
|
428
|
+
});
|
|
429
|
+
register("people_bootstrap", "初始化当前工作区的识人库,并导入旧 JSON(如果存在)。可重复执行。", { type: "object", additionalProperties: false, properties: { workspace: { type: "string" } } }, (args, ctx) => {
|
|
430
|
+
const workspace = stringArg(args, "workspace");
|
|
431
|
+
if (workspace && workspace !== ctx.workspaceDir && ctx.senderIsOwner !== true) {
|
|
432
|
+
throw new Error("只有 owner 可以初始化其他工作区。");
|
|
433
|
+
}
|
|
434
|
+
const service = toolService(runtime, ctx, workspace);
|
|
435
|
+
return textResult(`${formatStatus(service)}\n初始化完成。`);
|
|
436
|
+
});
|
|
437
|
+
register("people_backup", "为当前工作区创建一致的 SQLite 与 JSON 备份,并清理超出保留数量的旧备份。", {
|
|
438
|
+
type: "object",
|
|
439
|
+
additionalProperties: false,
|
|
440
|
+
properties: {
|
|
441
|
+
keep: { type: "number", description: "保留最近多少份备份,默认 14,最少 2。" },
|
|
442
|
+
workspace: { type: "string", description: "目标工作区;默认当前工作区。" },
|
|
443
|
+
},
|
|
444
|
+
}, (args, ctx) => {
|
|
445
|
+
const workspace = stringArg(args, "workspace");
|
|
446
|
+
if (workspace && workspace !== ctx.workspaceDir && !canManage(ctx)) {
|
|
447
|
+
throw new Error("只能备份当前工作区的识人库。");
|
|
448
|
+
}
|
|
449
|
+
const service = toolService(runtime, ctx, workspace);
|
|
450
|
+
const keepRaw = typeof args.keep === "number" ? args.keep : 14;
|
|
451
|
+
const report = service.backup(keepRaw);
|
|
452
|
+
return textResult([
|
|
453
|
+
"备份完成。",
|
|
454
|
+
`SQLite:${report.dbBackup}(${report.dbBytes} bytes)`,
|
|
455
|
+
`JSON:${report.jsonBackup}(${report.jsonBytes} bytes)`,
|
|
456
|
+
`保留 ${report.kept} 份${report.removedOld ? `,清理旧文件 ${report.removedOld} 个` : ""}。`,
|
|
457
|
+
].join("\n"));
|
|
458
|
+
});
|
|
459
|
+
register("people_health", "检查当前工作区识人库的维护风险:校验错误、孤儿记录、重复人物、过密画像、缺备份等。只读。", { type: "object", additionalProperties: false, properties: { workspace: { type: "string" } } }, (args, ctx) => {
|
|
460
|
+
const service = toolService(runtime, ctx, stringArg(args, "workspace"));
|
|
461
|
+
const report = service.health();
|
|
462
|
+
const lines = [
|
|
463
|
+
`${report.ok ? "✅" : "❌"} 识人库健康检查`,
|
|
464
|
+
`数据库:${report.dbPath}`,
|
|
465
|
+
`最近备份:${report.lastBackupAt ?? "无"}`,
|
|
466
|
+
`计数:${Object.entries(report.counts).map(([key, value]) => `${key}=${value}`).join(" ")}`,
|
|
467
|
+
];
|
|
468
|
+
if (report.issues.length === 0)
|
|
469
|
+
lines.push("没有发现需要处理的问题。");
|
|
470
|
+
else {
|
|
471
|
+
for (const issue of report.issues.slice(0, 50)) {
|
|
472
|
+
lines.push(`· [${issue.level}] ${issue.code}: ${issue.message}`);
|
|
473
|
+
}
|
|
474
|
+
if (report.issues.length > 50)
|
|
475
|
+
lines.push(`… 另有 ${report.issues.length - 50} 条`);
|
|
476
|
+
}
|
|
477
|
+
return textResult(lines.join("\n"));
|
|
478
|
+
});
|
|
479
|
+
register("people_cycle", "执行一次插件化维护周期:按需备份、自动修复安全项、健康检查,并返回后续需要 agent 处理的归并/合并计划。", {
|
|
480
|
+
type: "object",
|
|
481
|
+
additionalProperties: false,
|
|
482
|
+
properties: {
|
|
483
|
+
workspace: { type: "string" },
|
|
484
|
+
backup_keep: { type: "number" },
|
|
485
|
+
backup_interval_hours: { type: "number" },
|
|
486
|
+
force_backup: { type: "boolean" },
|
|
487
|
+
},
|
|
488
|
+
}, (args, ctx) => {
|
|
489
|
+
if (!canManage(ctx))
|
|
490
|
+
throw new Error("维护周期仅限 owner 或内部维护任务。");
|
|
491
|
+
const workspace = stringArg(args, "workspace");
|
|
492
|
+
if (workspace && workspace !== ctx.workspaceDir && !canManage(ctx)) {
|
|
493
|
+
throw new Error("只能维护当前工作区。");
|
|
494
|
+
}
|
|
495
|
+
const service = toolService(runtime, ctx, workspace);
|
|
496
|
+
const report = service.cycle({
|
|
497
|
+
backupKeep: typeof args.backup_keep === "number" ? args.backup_keep : 14,
|
|
498
|
+
backupIntervalHours: typeof args.backup_interval_hours === "number" ? args.backup_interval_hours : 20,
|
|
499
|
+
forceBackup: boolArg(args, "force_backup") === true,
|
|
500
|
+
});
|
|
501
|
+
const lines = [
|
|
502
|
+
"识人维护周期",
|
|
503
|
+
`数据库:${report.health.dbPath}`,
|
|
504
|
+
report.backup
|
|
505
|
+
? `备份:${report.backup.dbBackup}(${report.backup.dbBytes} bytes)`
|
|
506
|
+
: `备份:跳过${report.backupSkipped ? "(距离上次备份不足设定间隔)" : ""}`,
|
|
507
|
+
`修复:${report.repairActions.length ? report.repairActions.join(";") : "无"}`,
|
|
508
|
+
`健康:${report.health.ok ? "ok" : "有 error"},待处理 ${report.actions.length} 项`,
|
|
509
|
+
];
|
|
510
|
+
for (const action of report.actions) {
|
|
511
|
+
lines.push(`· [${action.kind}] ${action.ownerType ? `${action.ownerType}:` : ""}${action.target}:${action.message}`);
|
|
512
|
+
}
|
|
513
|
+
if (report.actions.length === 0)
|
|
514
|
+
lines.push("没有需要 agent 继续处理的画像任务。");
|
|
515
|
+
return textResult(lines.join("\n"));
|
|
516
|
+
});
|
|
517
|
+
register("people_search", "在当前工作区搜索账号、人物、印象和群事件。仅 owner 或内部维护任务可用。", {
|
|
518
|
+
type: "object",
|
|
519
|
+
additionalProperties: false,
|
|
520
|
+
required: ["query"],
|
|
521
|
+
properties: {
|
|
522
|
+
query: { type: "string" },
|
|
523
|
+
limit: { type: "number" },
|
|
524
|
+
},
|
|
525
|
+
}, (args, ctx) => {
|
|
526
|
+
if (!canManage(ctx))
|
|
527
|
+
throw new Error("搜索全局记忆仅限 owner 或内部维护任务。");
|
|
528
|
+
const service = toolService(runtime, ctx);
|
|
529
|
+
const report = service.search(stringArg(args, "query") ?? "", typeof args.limit === "number" ? args.limit : 10);
|
|
530
|
+
const lines = [`搜索:${stringArg(args, "query")}`];
|
|
531
|
+
if (report.accounts.length) {
|
|
532
|
+
lines.push("", "账号");
|
|
533
|
+
for (const account of report.accounts) {
|
|
534
|
+
lines.push(`· ${account.account_id}(${account.call_name || account.confirmed_name || account.display_name || "未命名"})`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (report.persons.length) {
|
|
538
|
+
lines.push("", "人物");
|
|
539
|
+
for (const person of report.persons) {
|
|
540
|
+
lines.push(`· ${person.person_id}(${person.call_name || person.name || "未命名"})`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
if (report.impressions.length) {
|
|
544
|
+
lines.push("", "印象");
|
|
545
|
+
for (const impression of report.impressions) {
|
|
546
|
+
lines.push(`· ${impression.owner_id}:${impression.content}`);
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (report.groupEvents.length) {
|
|
550
|
+
lines.push("", "群事件");
|
|
551
|
+
for (const event of report.groupEvents) {
|
|
552
|
+
lines.push(`· ${event.group_id} ${event.occurred_at.slice(0, 10)}:${event.summary}`);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
if (lines.length === 1)
|
|
556
|
+
lines.push("没有找到匹配记录。");
|
|
557
|
+
return textResult(lines.join("\n"));
|
|
558
|
+
});
|
|
559
|
+
register("people_dimension", "查看、添加或启停画像特征维度。添加和启停仅 owner 或内部维护任务可用。", {
|
|
560
|
+
type: "object",
|
|
561
|
+
additionalProperties: false,
|
|
562
|
+
properties: {
|
|
563
|
+
action: { type: "string", enum: ["list", "add", "enable", "disable"] },
|
|
564
|
+
key: { type: "string" },
|
|
565
|
+
label: { type: "string" },
|
|
566
|
+
sort: { type: "number" },
|
|
567
|
+
},
|
|
568
|
+
}, (args, ctx) => {
|
|
569
|
+
const service = toolService(runtime, ctx);
|
|
570
|
+
const action = stringArg(args, "action") ?? "list";
|
|
571
|
+
if (action === "list") {
|
|
572
|
+
return textResult(service.db
|
|
573
|
+
.listDimensions()
|
|
574
|
+
.map((dimension) => `${dimension.key} · ${dimension.label} · ${dimension.active ? "启用" : "停用"} · sort=${dimension.sort}`)
|
|
575
|
+
.join("\n"));
|
|
576
|
+
}
|
|
577
|
+
if (!canManage(ctx))
|
|
578
|
+
throw new Error("修改维度仅限 owner 或内部维护任务。");
|
|
579
|
+
const key = stringArg(args, "key");
|
|
580
|
+
if (!key)
|
|
581
|
+
throw new Error("key 不能为空。");
|
|
582
|
+
const existing = service.db.getDimension(key);
|
|
583
|
+
const active = action !== "disable";
|
|
584
|
+
const dimension = service.upsertDimension(key, stringArg(args, "label") ?? existing?.label ?? key, active, typeof args.sort === "number" ? args.sort : existing?.sort ?? 999);
|
|
585
|
+
return textResult(`已更新维度 ${dimension.key}:${dimension.label},${dimension.active ? "启用" : "停用"}。`);
|
|
586
|
+
});
|
|
587
|
+
register("people_merge", "合并两个人物记录到一个目标人物。用于清理重复人物;需要 confirm=true。仅 owner 或内部维护任务可用。", {
|
|
588
|
+
type: "object",
|
|
589
|
+
additionalProperties: false,
|
|
590
|
+
required: ["source", "target"],
|
|
591
|
+
properties: {
|
|
592
|
+
source: { type: "string", description: "被合并的 person_id" },
|
|
593
|
+
target: { type: "string", description: "保留的 person_id" },
|
|
594
|
+
confirm: { type: "boolean" },
|
|
595
|
+
},
|
|
596
|
+
}, (args, ctx) => {
|
|
597
|
+
if (!canManage(ctx))
|
|
598
|
+
throw new Error("合并人物仅限 owner 或内部维护任务。");
|
|
599
|
+
if (boolArg(args, "confirm") !== true)
|
|
600
|
+
throw new Error("合并人物需要 confirm=true。");
|
|
601
|
+
const service = toolService(runtime, ctx);
|
|
602
|
+
const source = stringArg(args, "source");
|
|
603
|
+
const target = stringArg(args, "target");
|
|
604
|
+
if (!source || !target)
|
|
605
|
+
throw new Error("source 和 target 不能为空。");
|
|
606
|
+
const result = service.mergePersons(source, target);
|
|
607
|
+
return textResult(`已合并 ${source} → ${target}:迁移账号 ${result.accounts}、印象 ${result.impressions}、特征 ${result.traits}。`);
|
|
608
|
+
});
|
|
609
|
+
register("people_unbind", "解除账号与人物绑定,默认把人物画像复制回账号,避免解绑即丢画像。需要 confirm=true。仅 owner 或内部维护任务可用。", {
|
|
610
|
+
type: "object",
|
|
611
|
+
additionalProperties: false,
|
|
612
|
+
required: ["account"],
|
|
613
|
+
properties: {
|
|
614
|
+
account: { type: "string" },
|
|
615
|
+
copy_profile: { type: "boolean", description: "默认 true。" },
|
|
616
|
+
confirm: { type: "boolean" },
|
|
617
|
+
},
|
|
618
|
+
}, (args, ctx) => {
|
|
619
|
+
if (!canManage(ctx))
|
|
620
|
+
throw new Error("解绑人物仅限 owner 或内部维护任务。");
|
|
621
|
+
if (boolArg(args, "confirm") !== true)
|
|
622
|
+
throw new Error("解绑人物需要 confirm=true。");
|
|
623
|
+
const service = toolService(runtime, ctx);
|
|
624
|
+
const account = resolveAccountRef(service, stringArg(args, "account") ?? "");
|
|
625
|
+
const result = service.unbind(account.account_id, boolArg(args, "copy_profile") !== false);
|
|
626
|
+
return textResult(`已解除 ${account.account_id} 的人物绑定;复制印象 ${result.impressionsCopied} 条、特征 ${result.traitsCopied} 个。`);
|
|
627
|
+
});
|
|
628
|
+
register("people_merge_account", "把同一人物的平台双份账号归并到一个账号(例如 bncr:x 并入 bncr/tgBot:x)。迁移印象/特征/群成员,必要时迁移人物绑定。需要 confirm=true。仅 owner 或内部维护任务可用。", {
|
|
629
|
+
type: "object",
|
|
630
|
+
additionalProperties: false,
|
|
631
|
+
required: ["source", "target"],
|
|
632
|
+
properties: {
|
|
633
|
+
source: { type: "string", description: "被归并的账号 ID 或显示名" },
|
|
634
|
+
target: { type: "string", description: "保留的账号 ID 或显示名" },
|
|
635
|
+
confirm: { type: "boolean" },
|
|
636
|
+
},
|
|
637
|
+
}, (args, ctx) => {
|
|
638
|
+
if (!canManage(ctx))
|
|
639
|
+
throw new Error("账号归并仅限 owner 或内部维护任务。");
|
|
640
|
+
if (boolArg(args, "confirm") !== true)
|
|
641
|
+
throw new Error("账号归并需要 confirm=true。");
|
|
642
|
+
const service = toolService(runtime, ctx);
|
|
643
|
+
const source = resolveAccountRef(service, stringArg(args, "source") ?? "");
|
|
644
|
+
const target = resolveAccountRef(service, stringArg(args, "target") ?? "");
|
|
645
|
+
const result = service.mergeAccounts(source.account_id, target.account_id);
|
|
646
|
+
return textResult(`已归并 ${source.account_id} → ${target.account_id}:印象 ${result.impressions}、特征 ${result.traits}、群成员 ${result.groupMembers}${result.bindingMoved ? ",并迁移了人物绑定" : ""}。`);
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
export { textResult, currentPlatform };
|