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,27 @@
|
|
|
1
|
+
function pad(value) {
|
|
2
|
+
return String(value).padStart(2, "0");
|
|
3
|
+
}
|
|
4
|
+
/** Local ISO timestamp with an explicit offset, matching the legacy data style. */
|
|
5
|
+
export function nowIso(date = new Date()) {
|
|
6
|
+
const offsetMinutes = -date.getTimezoneOffset();
|
|
7
|
+
const sign = offsetMinutes >= 0 ? "+" : "-";
|
|
8
|
+
const abs = Math.abs(offsetMinutes);
|
|
9
|
+
return (`${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
|
|
10
|
+
`T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}` +
|
|
11
|
+
`${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`);
|
|
12
|
+
}
|
|
13
|
+
export function dayKey(value = new Date()) {
|
|
14
|
+
if (value instanceof Date) {
|
|
15
|
+
return `${value.getFullYear()}-${pad(value.getMonth() + 1)}-${pad(value.getDate())}`;
|
|
16
|
+
}
|
|
17
|
+
const match = /^(\d{4}-\d{2}-\d{2})/.exec(value);
|
|
18
|
+
return match?.[1] ?? value.slice(0, 10);
|
|
19
|
+
}
|
|
20
|
+
export function isOlderThan(value, milliseconds) {
|
|
21
|
+
if (!value)
|
|
22
|
+
return true;
|
|
23
|
+
const parsed = Date.parse(value);
|
|
24
|
+
if (!Number.isFinite(parsed))
|
|
25
|
+
return true;
|
|
26
|
+
return Date.now() - parsed > milliseconds;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { resolveWorkspaceRoot } from "../core/paths.js";
|
|
2
|
+
import { describeCounts } from "../core/service.js";
|
|
3
|
+
function printJson(value) {
|
|
4
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
5
|
+
}
|
|
6
|
+
function workspaceFor(api, options) {
|
|
7
|
+
const agentId = typeof options.agent === "string" ? options.agent : undefined;
|
|
8
|
+
const workspaceDir = (typeof options.workspace === "string" && options.workspace.trim()) ||
|
|
9
|
+
resolveWorkspaceRoot(api.config, agentId);
|
|
10
|
+
return { workspaceDir, agentId };
|
|
11
|
+
}
|
|
12
|
+
export function registerCli(api, runtime) {
|
|
13
|
+
if (typeof api.registerCli !== "function")
|
|
14
|
+
return;
|
|
15
|
+
api.registerCli(({ program }) => {
|
|
16
|
+
const people = program.command("people").description("Workspace-scoped people memory utilities");
|
|
17
|
+
people
|
|
18
|
+
.command("import")
|
|
19
|
+
.description("Re-import legacy JSON into the SQLite store (idempotent)")
|
|
20
|
+
.option("--workspace <path>", "Target workspace directory")
|
|
21
|
+
.option("--agent <id>", "Resolve workspace from an agent id")
|
|
22
|
+
.option("--force", "Re-import even if legacy data was imported before")
|
|
23
|
+
.option("--json", "Print JSON")
|
|
24
|
+
.action(async (options = {}) => {
|
|
25
|
+
const { workspaceDir, agentId } = workspaceFor(api, options);
|
|
26
|
+
const service = runtime.get({ workspaceDir, agentId });
|
|
27
|
+
const report = service.importLegacy(options.force === true);
|
|
28
|
+
if (options.json)
|
|
29
|
+
return printJson(report);
|
|
30
|
+
console.log(`导入: ${report.imported ? "完成" : "跳过"}${report.reason ? `(${report.reason})` : ""}`);
|
|
31
|
+
console.log(`账号 ${report.accounts} · 人物 ${report.persons} · 绑定 ${report.bindings} · 群 ${report.groups} · 群记忆 ${report.groupEvents} · 印象 ${report.impressions} · 特征 ${report.traits}`);
|
|
32
|
+
if (report.warnings.length > 0)
|
|
33
|
+
console.log(`警告 ${report.warnings.length} 条`);
|
|
34
|
+
});
|
|
35
|
+
people
|
|
36
|
+
.command("status")
|
|
37
|
+
.description("Show people database status")
|
|
38
|
+
.option("--workspace <path>", "Target workspace directory")
|
|
39
|
+
.option("--agent <id>", "Resolve workspace from an agent id")
|
|
40
|
+
.option("--json", "Print JSON")
|
|
41
|
+
.action(async (options = {}) => {
|
|
42
|
+
const { workspaceDir, agentId } = workspaceFor(api, options);
|
|
43
|
+
const service = runtime.get({ workspaceDir, agentId });
|
|
44
|
+
const status = service.status();
|
|
45
|
+
if (options.json)
|
|
46
|
+
return printJson(status);
|
|
47
|
+
console.log(`工作区: ${status.workspaceDir}`);
|
|
48
|
+
console.log(`数据目录: ${status.dataDir}`);
|
|
49
|
+
console.log(`数据库: ${status.dbPath} (schema v${status.schemaVersion})`);
|
|
50
|
+
console.log(describeCounts(status.counts));
|
|
51
|
+
console.log(`最近全量扫描: ${status.lastFullScanAt ?? "无"}`);
|
|
52
|
+
console.log(`旧 JSON 导入: ${status.legacyImportedAt ?? "无"}`);
|
|
53
|
+
});
|
|
54
|
+
people
|
|
55
|
+
.command("validate")
|
|
56
|
+
.description("Validate people database integrity")
|
|
57
|
+
.option("--workspace <path>", "Target workspace directory")
|
|
58
|
+
.option("--agent <id>", "Resolve workspace from an agent id")
|
|
59
|
+
.option("--fix", "Repair safe issues")
|
|
60
|
+
.option("--json", "Print JSON")
|
|
61
|
+
.action(async (options = {}) => {
|
|
62
|
+
const { workspaceDir, agentId } = workspaceFor(api, options);
|
|
63
|
+
const service = runtime.get({ workspaceDir, agentId });
|
|
64
|
+
const actions = options.fix ? service.repair() : [];
|
|
65
|
+
const report = service.validate();
|
|
66
|
+
if (options.json)
|
|
67
|
+
return printJson({ ...report, repairActions: actions });
|
|
68
|
+
console.log(`${report.ok ? "OK" : "FAIL"} ${report.dbPath}`);
|
|
69
|
+
console.log(describeCounts(report.counts));
|
|
70
|
+
for (const issue of report.issues)
|
|
71
|
+
console.log(`[${issue.level}] ${issue.code}: ${issue.message}`);
|
|
72
|
+
for (const action of actions)
|
|
73
|
+
console.log(`[fix] ${action}`);
|
|
74
|
+
});
|
|
75
|
+
people
|
|
76
|
+
.command("bootstrap")
|
|
77
|
+
.description("Create the database and import legacy JSON when present")
|
|
78
|
+
.option("--workspace <path>", "Target workspace directory")
|
|
79
|
+
.option("--agent <id>", "Resolve workspace from an agent id")
|
|
80
|
+
.option("--json", "Print JSON")
|
|
81
|
+
.action(async (options = {}) => {
|
|
82
|
+
const { workspaceDir, agentId } = workspaceFor(api, options);
|
|
83
|
+
const status = runtime.get({ workspaceDir, agentId }).status();
|
|
84
|
+
if (options.json)
|
|
85
|
+
return printJson(status);
|
|
86
|
+
console.log(`已初始化: ${status.dbPath}`);
|
|
87
|
+
console.log(describeCounts(status.counts));
|
|
88
|
+
console.log(`旧 JSON 导入: ${status.legacyImportedAt ?? "无"}`);
|
|
89
|
+
});
|
|
90
|
+
people
|
|
91
|
+
.command("backup")
|
|
92
|
+
.description("Back up the SQLite store and export a JSON snapshot")
|
|
93
|
+
.option("--workspace <path>", "Target workspace directory")
|
|
94
|
+
.option("--agent <id>", "Resolve workspace from an agent id")
|
|
95
|
+
.option("--keep <n>", "Number of recent backup pairs to retain")
|
|
96
|
+
.option("--json", "Print JSON")
|
|
97
|
+
.action(async (options = {}) => {
|
|
98
|
+
const { workspaceDir, agentId } = workspaceFor(api, options);
|
|
99
|
+
const keep = options.keep ? Number(options.keep) : 14;
|
|
100
|
+
const report = runtime.get({ workspaceDir, agentId }).backup(keep);
|
|
101
|
+
if (options.json)
|
|
102
|
+
return printJson(report);
|
|
103
|
+
console.log(`备份目录: ${report.backupDir}`);
|
|
104
|
+
console.log(`SQLite: ${report.dbBackup} (${report.dbBytes} bytes)`);
|
|
105
|
+
console.log(`JSON: ${report.jsonBackup} (${report.jsonBytes} bytes)`);
|
|
106
|
+
console.log(`保留 ${report.kept} 份,清理旧文件 ${report.removedOld} 个`);
|
|
107
|
+
});
|
|
108
|
+
people
|
|
109
|
+
.command("health")
|
|
110
|
+
.description("Check maintenance risks without changing data")
|
|
111
|
+
.option("--workspace <path>", "Target workspace directory")
|
|
112
|
+
.option("--agent <id>", "Resolve workspace from an agent id")
|
|
113
|
+
.option("--json", "Print JSON")
|
|
114
|
+
.action(async (options = {}) => {
|
|
115
|
+
const { workspaceDir, agentId } = workspaceFor(api, options);
|
|
116
|
+
const report = runtime.get({ workspaceDir, agentId }).health();
|
|
117
|
+
if (options.json)
|
|
118
|
+
return printJson(report);
|
|
119
|
+
console.log(`${report.ok ? "OK" : "FAIL"} ${report.dbPath}`);
|
|
120
|
+
console.log(`最近备份: ${report.lastBackupAt ?? "无"}`);
|
|
121
|
+
console.log(describeCounts(report.counts));
|
|
122
|
+
for (const issue of report.issues)
|
|
123
|
+
console.log(`[${issue.level}] ${issue.code}: ${issue.message}`);
|
|
124
|
+
});
|
|
125
|
+
people
|
|
126
|
+
.command("cycle")
|
|
127
|
+
.description("Run one plugin-owned maintenance cycle")
|
|
128
|
+
.option("--workspace <path>", "Target workspace directory")
|
|
129
|
+
.option("--agent <id>", "Resolve workspace from an agent id")
|
|
130
|
+
.option("--keep <n>", "Number of recent backup pairs to retain")
|
|
131
|
+
.option("--interval-hours <n>", "Minimum hours between automatic backups")
|
|
132
|
+
.option("--force-backup", "Create a backup even when one is recent")
|
|
133
|
+
.option("--json", "Print JSON")
|
|
134
|
+
.action(async (options = {}) => {
|
|
135
|
+
const { workspaceDir, agentId } = workspaceFor(api, options);
|
|
136
|
+
const report = runtime.get({ workspaceDir, agentId }).cycle({
|
|
137
|
+
backupKeep: options.keep ? Number(options.keep) : 14,
|
|
138
|
+
backupIntervalHours: options.intervalHours ? Number(options.intervalHours) : 20,
|
|
139
|
+
forceBackup: options.forceBackup === true,
|
|
140
|
+
});
|
|
141
|
+
if (options.json)
|
|
142
|
+
return printJson(report);
|
|
143
|
+
console.log(`周期: ${report.cycledAt}`);
|
|
144
|
+
console.log(report.backup ? `备份: ${report.backup.dbBackup}` : "备份: 跳过");
|
|
145
|
+
console.log(`自动修复: ${report.repairActions.length ? report.repairActions.join(";") : "无"}`);
|
|
146
|
+
console.log(`健康: ${report.health.ok ? "ok" : "error"},待办 ${report.actions.length} 项`);
|
|
147
|
+
for (const action of report.actions)
|
|
148
|
+
console.log(`[${action.kind}] ${action.target}: ${action.message}`);
|
|
149
|
+
});
|
|
150
|
+
people
|
|
151
|
+
.command("search")
|
|
152
|
+
.description("Search accounts, persons, impressions and group events")
|
|
153
|
+
.option("--query <text>", "Search text")
|
|
154
|
+
.option("--workspace <path>", "Target workspace directory")
|
|
155
|
+
.option("--agent <id>", "Resolve workspace from an agent id")
|
|
156
|
+
.option("--limit <n>", "Maximum hits per section")
|
|
157
|
+
.option("--json", "Print JSON")
|
|
158
|
+
.action(async (options = {}) => {
|
|
159
|
+
if (!options.query?.trim())
|
|
160
|
+
throw new Error("--query is required");
|
|
161
|
+
const { workspaceDir, agentId } = workspaceFor(api, options);
|
|
162
|
+
const limit = options.limit ? Number(options.limit) : 10;
|
|
163
|
+
const report = runtime.get({ workspaceDir, agentId }).search(options.query, limit);
|
|
164
|
+
if (options.json)
|
|
165
|
+
return printJson(report);
|
|
166
|
+
console.log(`账号 ${report.accounts.length} · 人物 ${report.persons.length} · 印象 ${report.impressions.length} · 群事件 ${report.groupEvents.length}`);
|
|
167
|
+
for (const account of report.accounts)
|
|
168
|
+
console.log(`账号 ${account.account_id} (${account.display_name ?? "?"})`);
|
|
169
|
+
for (const person of report.persons)
|
|
170
|
+
console.log(`人物 ${person.person_id} (${person.name ?? "?"})`);
|
|
171
|
+
for (const impression of report.impressions)
|
|
172
|
+
console.log(`印象 ${impression.owner_id}: ${impression.content}`);
|
|
173
|
+
for (const event of report.groupEvents)
|
|
174
|
+
console.log(`群事件 ${event.group_id}: ${event.summary}`);
|
|
175
|
+
});
|
|
176
|
+
}, { commands: ["people"] });
|
|
177
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export function normalizeConfig(raw) {
|
|
2
|
+
const value = (raw ?? {});
|
|
3
|
+
const str = (key, fallback) => typeof value[key] === "string" && value[key].trim()
|
|
4
|
+
? value[key].trim()
|
|
5
|
+
: fallback;
|
|
6
|
+
const bool = (key, fallback) => typeof value[key] === "boolean" ? value[key] : fallback;
|
|
7
|
+
const list = (key) => Array.isArray(value[key])
|
|
8
|
+
? value[key].filter((item) => typeof item === "string")
|
|
9
|
+
: [];
|
|
10
|
+
const record = (key) => {
|
|
11
|
+
const rawRecord = value[key];
|
|
12
|
+
if (!rawRecord || typeof rawRecord !== "object" || Array.isArray(rawRecord))
|
|
13
|
+
return {};
|
|
14
|
+
return Object.fromEntries(Object.entries(rawRecord).filter((entry) => typeof entry[1] === "string"));
|
|
15
|
+
};
|
|
16
|
+
return {
|
|
17
|
+
dataDir: str("dataDir", "{stateDir}/plugins/people/{workspaceKey}"),
|
|
18
|
+
workspaceDataDirs: record("workspaceDataDirs"),
|
|
19
|
+
databaseFile: str("databaseFile", "people.db"),
|
|
20
|
+
injectContext: bool("injectContext", true),
|
|
21
|
+
injectGroupMemory: bool("injectGroupMemory", true),
|
|
22
|
+
injectMaxChars: typeof value.injectMaxChars === "number" && value.injectMaxChars > 200
|
|
23
|
+
? value.injectMaxChars
|
|
24
|
+
: 1200,
|
|
25
|
+
disabledAgents: list("disabledAgents"),
|
|
26
|
+
capture: bool("capture", true),
|
|
27
|
+
captureGroups: bool("captureGroups", true),
|
|
28
|
+
captureSkipSenders: (() => {
|
|
29
|
+
const skip = list("captureSkipSenders");
|
|
30
|
+
return skip.length ? skip : undefined;
|
|
31
|
+
})(),
|
|
32
|
+
importLegacy: bool("importLegacy", true),
|
|
33
|
+
platformTemplate: str("platformTemplate", "{channel}/{account}"),
|
|
34
|
+
multiAccountChannels: (() => {
|
|
35
|
+
const channels = list("multiAccountChannels");
|
|
36
|
+
return channels.length ? channels : undefined;
|
|
37
|
+
})(),
|
|
38
|
+
platformOverrides: record("platformOverrides"),
|
|
39
|
+
senderFamilies: (() => {
|
|
40
|
+
const raw = value.senderFamilies;
|
|
41
|
+
if (raw === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
if (!Array.isArray(raw))
|
|
44
|
+
return undefined;
|
|
45
|
+
const families = [];
|
|
46
|
+
for (const family of raw) {
|
|
47
|
+
if (!Array.isArray(family))
|
|
48
|
+
continue;
|
|
49
|
+
const labels = family
|
|
50
|
+
.filter((item) => typeof item === "string" && Boolean(item.trim()))
|
|
51
|
+
.map((item) => item.trim());
|
|
52
|
+
if (labels.length >= 2)
|
|
53
|
+
families.push(labels);
|
|
54
|
+
}
|
|
55
|
+
return families.length > 0 ? families : undefined;
|
|
56
|
+
})(),
|
|
57
|
+
debug: bool("debug", false),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function isAgentDisabled(config, agentId) {
|
|
61
|
+
if (!agentId)
|
|
62
|
+
return false;
|
|
63
|
+
return (config.disabledAgents ?? []).some((value) => value.trim() === agentId);
|
|
64
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { isAgentDisabled } from "./config.js";
|
|
2
|
+
import { hookChatId, hookChatTitle, hookIsGroup, hookPlatform, hookSenderName, inboundGroupTitle, inboundPlatform, } from "./identity.js";
|
|
3
|
+
const CAPTURE_INTERVAL_MS = 30_000;
|
|
4
|
+
export function registerHooks(api, runtime) {
|
|
5
|
+
const lastCapture = new Map();
|
|
6
|
+
const pendingIdentity = new Map();
|
|
7
|
+
api.on("inbound_claim", ((event) => {
|
|
8
|
+
try {
|
|
9
|
+
const senderId = event.senderId?.trim();
|
|
10
|
+
if (!senderId)
|
|
11
|
+
return undefined;
|
|
12
|
+
const platform = inboundPlatform(event, runtime.config);
|
|
13
|
+
const at = Date.now();
|
|
14
|
+
const entry = {
|
|
15
|
+
senderName: event.senderName?.trim() || undefined,
|
|
16
|
+
chatTitle: inboundGroupTitle(event),
|
|
17
|
+
isGroup: event.isGroup === true,
|
|
18
|
+
at,
|
|
19
|
+
};
|
|
20
|
+
pendingIdentity.set(`${platform}:${senderId}`, entry);
|
|
21
|
+
if (event.sessionKey)
|
|
22
|
+
pendingIdentity.set(`session:${event.sessionKey}`, entry);
|
|
23
|
+
if (pendingIdentity.size > 500) {
|
|
24
|
+
for (const [key, value] of pendingIdentity) {
|
|
25
|
+
if (at - value.at > 10 * 60_000)
|
|
26
|
+
pendingIdentity.delete(key);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// passive observer only
|
|
32
|
+
}
|
|
33
|
+
return undefined;
|
|
34
|
+
}));
|
|
35
|
+
api.on("before_prompt_build", ((event, ctx) => {
|
|
36
|
+
try {
|
|
37
|
+
if (isAgentDisabled(runtime.config, ctx.agentId))
|
|
38
|
+
return undefined;
|
|
39
|
+
const senderId = ctx.senderId?.trim();
|
|
40
|
+
if (!senderId)
|
|
41
|
+
return undefined;
|
|
42
|
+
const service = runtime.get({
|
|
43
|
+
workspaceDir: ctx.workspaceDir,
|
|
44
|
+
agentId: ctx.agentId,
|
|
45
|
+
agentDir: undefined,
|
|
46
|
+
});
|
|
47
|
+
const platform = hookPlatform(ctx, runtime.config);
|
|
48
|
+
const chatId = hookChatId(ctx);
|
|
49
|
+
const pending = pendingIdentity.get(`${platform}:${senderId}`) ??
|
|
50
|
+
(ctx.sessionKey ? pendingIdentity.get(`session:${ctx.sessionKey}`) : undefined);
|
|
51
|
+
const chatTitle = hookChatTitle(ctx) ?? pending?.chatTitle;
|
|
52
|
+
const isGroup = hookIsGroup(ctx) || pending?.isGroup === true;
|
|
53
|
+
let groupId;
|
|
54
|
+
if (runtime.config.capture !== false) {
|
|
55
|
+
const captureKey = `${service.paths.workspaceKey}|${platform}|${senderId}`;
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const previous = lastCapture.get(captureKey) ?? 0;
|
|
58
|
+
if (now - previous >= CAPTURE_INTERVAL_MS) {
|
|
59
|
+
lastCapture.set(captureKey, now);
|
|
60
|
+
const result = service.capture({
|
|
61
|
+
platform,
|
|
62
|
+
senderId,
|
|
63
|
+
senderName: hookSenderName(ctx) ?? pending?.senderName,
|
|
64
|
+
chatId,
|
|
65
|
+
chatTitle,
|
|
66
|
+
isGroup,
|
|
67
|
+
agentId: ctx.agentId,
|
|
68
|
+
});
|
|
69
|
+
groupId = result.groupId;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (runtime.config.injectContext === false)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (!groupId && chatId) {
|
|
75
|
+
groupId = service.db.findGroup(platform, chatId)?.group_id;
|
|
76
|
+
}
|
|
77
|
+
const accountId = service.resolveAccountForLookup(platform, senderId)?.account_id;
|
|
78
|
+
if (!accountId)
|
|
79
|
+
return undefined;
|
|
80
|
+
const card = service.contextCard({
|
|
81
|
+
accountId,
|
|
82
|
+
groupId,
|
|
83
|
+
maxChars: runtime.config.injectMaxChars,
|
|
84
|
+
includeGroupMemory: runtime.config.injectGroupMemory !== false,
|
|
85
|
+
});
|
|
86
|
+
if (!card)
|
|
87
|
+
return undefined;
|
|
88
|
+
if (runtime.config.debug) {
|
|
89
|
+
runtime.api.logger.info(`[people] inject ${accountId} bytes=${card.length} promptBytes=${event.prompt.length}`);
|
|
90
|
+
}
|
|
91
|
+
return { prependContext: card };
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
runtime.api.logger.warn(`[people] before_prompt_build failed: ${error.message}`);
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
}), { timeoutMs: 3000 });
|
|
98
|
+
api.on("gateway_stop", () => {
|
|
99
|
+
runtime.closeAll();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { resolvePlatform } from "../core/platform.js";
|
|
2
|
+
function record(value) {
|
|
3
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
4
|
+
}
|
|
5
|
+
function firstString(source, keys) {
|
|
6
|
+
for (const key of keys) {
|
|
7
|
+
const value = source[key];
|
|
8
|
+
if (typeof value === "string" && value.trim())
|
|
9
|
+
return value.trim();
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
export function hookSenderName(ctx) {
|
|
14
|
+
const sender = record(ctx.channelContext?.sender);
|
|
15
|
+
return firstString(sender, [
|
|
16
|
+
"name",
|
|
17
|
+
"displayName",
|
|
18
|
+
"display_name",
|
|
19
|
+
"nickname",
|
|
20
|
+
"username",
|
|
21
|
+
"userName",
|
|
22
|
+
"remark",
|
|
23
|
+
]);
|
|
24
|
+
}
|
|
25
|
+
export function hookChatId(ctx) {
|
|
26
|
+
const chat = record(ctx.channelContext?.chat);
|
|
27
|
+
return ctx.chatId?.trim() || firstString(chat, ["id", "chatId", "chat_id", "conversationId"]);
|
|
28
|
+
}
|
|
29
|
+
export function hookChatTitle(ctx) {
|
|
30
|
+
const chat = record(ctx.channelContext?.chat);
|
|
31
|
+
return firstString(chat, [
|
|
32
|
+
"title",
|
|
33
|
+
"name",
|
|
34
|
+
"displayName",
|
|
35
|
+
"display_name",
|
|
36
|
+
"subject",
|
|
37
|
+
"group_subject",
|
|
38
|
+
"groupName",
|
|
39
|
+
]);
|
|
40
|
+
}
|
|
41
|
+
export function hookIsGroup(ctx) {
|
|
42
|
+
const chat = record(ctx.channelContext?.chat);
|
|
43
|
+
if (chat.isGroup === true || chat.is_group_chat === true || chat.isGroupChat === true)
|
|
44
|
+
return true;
|
|
45
|
+
const type = firstString(chat, ["type", "kind", "chat_type", "chatType"]);
|
|
46
|
+
return type === "group" || type === "channel" || type === "supergroup";
|
|
47
|
+
}
|
|
48
|
+
export function hookPlatform(ctx, config) {
|
|
49
|
+
const chat = record(ctx.channelContext?.chat);
|
|
50
|
+
const sender = record(ctx.channelContext?.sender);
|
|
51
|
+
const metadataPlatform = firstString(chat, ["platform", "provider"]) ?? firstString(sender, ["platform", "provider"]);
|
|
52
|
+
return resolvePlatform({
|
|
53
|
+
channel: ctx.channel,
|
|
54
|
+
provider: ctx.messageProvider,
|
|
55
|
+
accountId: ctx.accountId,
|
|
56
|
+
metadataPlatform,
|
|
57
|
+
template: config.platformTemplate,
|
|
58
|
+
overrides: config.platformOverrides,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function inboundPlatform(event, config) {
|
|
62
|
+
const metadata = record(event.metadata);
|
|
63
|
+
const metadataPlatform = firstString(metadata, ["platform", "provider"]) ??
|
|
64
|
+
firstString(record(metadata.chat), ["platform", "provider"]);
|
|
65
|
+
return resolvePlatform({
|
|
66
|
+
channel: event.channel,
|
|
67
|
+
accountId: event.accountId,
|
|
68
|
+
metadataPlatform,
|
|
69
|
+
template: config.platformTemplate,
|
|
70
|
+
overrides: config.platformOverrides,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function inboundGroupTitle(event) {
|
|
74
|
+
const metadata = record(event.metadata);
|
|
75
|
+
return (firstString(metadata, ["group_subject", "groupSubject", "groupName", "group_name"]) ??
|
|
76
|
+
firstString(record(metadata.chat), ["title", "name", "subject"]));
|
|
77
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { resolveStorePaths, resolveWorkspaceRoot } from "../core/paths.js";
|
|
3
|
+
import { PeopleService } from "../core/service.js";
|
|
4
|
+
import { normalizeConfig } from "./config.js";
|
|
5
|
+
export class PeopleRuntime {
|
|
6
|
+
api;
|
|
7
|
+
config;
|
|
8
|
+
cache = new Map();
|
|
9
|
+
constructor(api) {
|
|
10
|
+
this.api = api;
|
|
11
|
+
this.config = normalizeConfig(api.pluginConfig);
|
|
12
|
+
}
|
|
13
|
+
resolveStateDir() {
|
|
14
|
+
try {
|
|
15
|
+
return this.api.runtime?.state?.resolveStateDir?.(process.env);
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
logDebug(message) {
|
|
22
|
+
if (this.config.debug)
|
|
23
|
+
this.api.logger.info(`[people] ${message}`);
|
|
24
|
+
}
|
|
25
|
+
get(context) {
|
|
26
|
+
const workspaceDir = context.workspaceDir?.trim() ||
|
|
27
|
+
resolveWorkspaceRoot(this.api.config, context.agentId);
|
|
28
|
+
const paths = resolveStorePaths({
|
|
29
|
+
workspaceDir,
|
|
30
|
+
agentId: context.agentId,
|
|
31
|
+
agentDir: context.agentDir,
|
|
32
|
+
stateDir: this.resolveStateDir(),
|
|
33
|
+
config: this.config,
|
|
34
|
+
});
|
|
35
|
+
const key = paths.workspaceKey;
|
|
36
|
+
const existing = this.cache.get(key);
|
|
37
|
+
if (existing && fs.existsSync(existing.paths.dbPath))
|
|
38
|
+
return existing;
|
|
39
|
+
if (existing) {
|
|
40
|
+
try {
|
|
41
|
+
existing.close();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// ignore
|
|
45
|
+
}
|
|
46
|
+
this.cache.delete(key);
|
|
47
|
+
}
|
|
48
|
+
this.logDebug(`open ${paths.dbPath} (agent=${context.agentId ?? "?"})`);
|
|
49
|
+
const service = new PeopleService({
|
|
50
|
+
paths,
|
|
51
|
+
config: this.config,
|
|
52
|
+
logger: {
|
|
53
|
+
info: (message) => this.api.logger.info(message),
|
|
54
|
+
warn: (message) => this.api.logger.warn(message),
|
|
55
|
+
debug: (message) => this.logDebug(message),
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
this.cache.set(key, service);
|
|
59
|
+
return service;
|
|
60
|
+
}
|
|
61
|
+
closeAll() {
|
|
62
|
+
for (const service of this.cache.values()) {
|
|
63
|
+
try {
|
|
64
|
+
service.close();
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// ignore
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
this.cache.clear();
|
|
71
|
+
}
|
|
72
|
+
}
|