libp2p-mesh 2026.6.12 → 2026.6.13
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/README.md +124 -0
- package/dist/src/agent-tools.d.ts +120 -1
- package/dist/src/agent-tools.js +153 -0
- package/dist/src/instance-peer-store.js +35 -1
- package/dist/src/instance-router.d.ts +2 -8
- package/dist/src/instance-router.js +94 -2
- package/dist/src/plugin.js +8 -2
- package/dist/src/profile-cli.d.ts +27 -0
- package/dist/src/profile-cli.js +47 -0
- package/dist/src/profile-wizard.d.ts +20 -0
- package/dist/src/profile-wizard.js +141 -0
- package/dist/src/setup-cli.d.ts +19 -0
- package/dist/src/setup-cli.js +32 -28
- package/dist/src/types.d.ts +63 -0
- package/dist/src/user-attributes.d.ts +6 -0
- package/dist/src/user-attributes.js +92 -0
- package/dist/src/user-md-attributes.d.ts +12 -0
- package/dist/src/user-md-attributes.js +202 -0
- package/dist/src/user-profile-store.d.ts +25 -0
- package/dist/src/user-profile-store.js +187 -0
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/src/agent-tools.ts +187 -1
- package/src/instance-peer-store.ts +41 -1
- package/src/instance-router.ts +121 -12
- package/src/plugin.ts +8 -2
- package/src/profile-cli.ts +85 -0
- package/src/profile-wizard.ts +204 -0
- package/src/setup-cli.ts +40 -29
- package/src/types.ts +68 -0
- package/src/user-attributes.ts +122 -0
- package/src/user-md-attributes.ts +256 -0
- package/src/user-profile-store.ts +259 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
function isRecord(value) {
|
|
2
|
+
return typeof value === "object" && value !== null;
|
|
3
|
+
}
|
|
4
|
+
function trimmedString(value) {
|
|
5
|
+
if (typeof value !== "string") {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const trimmed = value.trim();
|
|
9
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
10
|
+
}
|
|
11
|
+
export function normalizeAttributeValue(value) {
|
|
12
|
+
return value.trim().toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
export function normalizeAttributeKey(key) {
|
|
15
|
+
return key.trim().toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
export function normalizeUserPublicAttribute(value) {
|
|
18
|
+
if (!isRecord(value)) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
if (value.kind === "tag") {
|
|
22
|
+
if (value.source !== "USER.md") {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const attributeValue = trimmedString(value.value);
|
|
26
|
+
const label = trimmedString(value.label);
|
|
27
|
+
if (!attributeValue || !label) {
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
kind: "tag",
|
|
32
|
+
value: attributeValue,
|
|
33
|
+
label,
|
|
34
|
+
source: "USER.md",
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (value.kind === "structured") {
|
|
38
|
+
if (value.source !== "profile") {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const key = trimmedString(value.key);
|
|
42
|
+
const attributeValue = trimmedString(value.value);
|
|
43
|
+
const label = trimmedString(value.label);
|
|
44
|
+
if (!key || !attributeValue || !label) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
kind: "structured",
|
|
49
|
+
key: normalizeAttributeKey(key),
|
|
50
|
+
value: attributeValue,
|
|
51
|
+
label,
|
|
52
|
+
source: "profile",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
function attributeDedupeKey(attribute) {
|
|
58
|
+
if (attribute.kind === "tag") {
|
|
59
|
+
return `tag:${normalizeAttributeValue(attribute.value)}`;
|
|
60
|
+
}
|
|
61
|
+
return `structured:${normalizeAttributeKey(attribute.key)}:${normalizeAttributeValue(attribute.value)}`;
|
|
62
|
+
}
|
|
63
|
+
export function mergeUserPublicAttributes(userMdTags, profileAttributes) {
|
|
64
|
+
const merged = [];
|
|
65
|
+
const seen = new Set();
|
|
66
|
+
for (const rawAttribute of [...userMdTags, ...profileAttributes]) {
|
|
67
|
+
const attribute = normalizeUserPublicAttribute(rawAttribute);
|
|
68
|
+
if (!attribute) {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const key = attributeDedupeKey(attribute);
|
|
72
|
+
if (seen.has(key)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
seen.add(key);
|
|
76
|
+
merged.push(attribute);
|
|
77
|
+
}
|
|
78
|
+
return merged;
|
|
79
|
+
}
|
|
80
|
+
export function matchesUserAttribute(attribute, match) {
|
|
81
|
+
if (attribute.kind !== match.kind) {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
if (attribute.kind === "tag") {
|
|
85
|
+
return normalizeAttributeValue(attribute.value) === normalizeAttributeValue(match.value);
|
|
86
|
+
}
|
|
87
|
+
if (match.kind !== "structured") {
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
return (normalizeAttributeKey(attribute.key) === normalizeAttributeKey(match.key) &&
|
|
91
|
+
normalizeAttributeValue(attribute.value) === normalizeAttributeValue(match.value));
|
|
92
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { UserPublicAttribute } from "./types.js";
|
|
2
|
+
export type UserMdAttributeSource = {
|
|
3
|
+
path?: string;
|
|
4
|
+
logger?: {
|
|
5
|
+
debug?: (message: string) => void;
|
|
6
|
+
warn?: (message: string) => void;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
export declare function extractUserMdTags(markdown: string): UserPublicAttribute[];
|
|
10
|
+
export declare function createUserMdAttributeSource(options?: UserMdAttributeSource): {
|
|
11
|
+
loadTags(): Promise<UserPublicAttribute[]>;
|
|
12
|
+
};
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { normalizeAttributeValue } from "./user-attributes.js";
|
|
4
|
+
const MAX_TAGS = 10;
|
|
5
|
+
const MAX_TAG_LENGTH = 40;
|
|
6
|
+
const FIELD_PREFIX_PATTERN = /^(?:[-*]\s*)?(?:#{1,6}\s*)?(?:name|what to call them|notes?|context|project|projects|skills?|interests?)\s*[::-]\s*/i;
|
|
7
|
+
const FIELD_LINE_PATTERN = /^(?:[-*]\s*)?(?:#{1,6}\s*)?(name|what to call them|notes?|context|project|projects|skills?|interests?)\s*[::-]\s*(.*)$/i;
|
|
8
|
+
const TEMPLATE_PATTERN = /\b(?:todo|tbd|n\/a|none|unknown|your name|add notes here|template placeholder|placeholder)\b/i;
|
|
9
|
+
const EXPLICIT_LIST_SEPARATOR_PATTERN = /[,,、;;|/]/;
|
|
10
|
+
const ENGLISH_TAG_PATTERN = /^(?:[A-Za-z][A-Za-z0-9]*(?:\.[A-Za-z0-9]+)?|libp2p|node\.js)$/i;
|
|
11
|
+
const CHINESE_TAG_PATTERN = /[\p{Script=Han}]{2,8}/gu;
|
|
12
|
+
const CHINESE_CONTEXT_PATTERNS = [
|
|
13
|
+
/(?:在|来自|加入|参与|负责)([\p{Script=Han}]{2,8})(?:做|写|用|项目|团队|实验室|方向)?/gu,
|
|
14
|
+
/(?:做|写|维护|负责|参与)([\p{Script=Han}]{2,8})(?:项目|插件|工具|方向)?/gu,
|
|
15
|
+
];
|
|
16
|
+
const CHINESE_SENTENCE_WORD_PATTERN = /(?:今天|明天|昨天|今晚|晚上|早上|上午|下午|八点|同步|一下|进展|开会|讨论|安排|提醒|需要|已经|可以|应该|我们|你们|他们)/u;
|
|
17
|
+
const COMMON_WORDS = new Set([
|
|
18
|
+
"and",
|
|
19
|
+
"also",
|
|
20
|
+
"with",
|
|
21
|
+
"works",
|
|
22
|
+
"work",
|
|
23
|
+
"interested",
|
|
24
|
+
"in",
|
|
25
|
+
"the",
|
|
26
|
+
"for",
|
|
27
|
+
"user",
|
|
28
|
+
"name",
|
|
29
|
+
"notes",
|
|
30
|
+
"context",
|
|
31
|
+
"project",
|
|
32
|
+
"projects",
|
|
33
|
+
"skills",
|
|
34
|
+
]);
|
|
35
|
+
const ENGLISH_TAG_FIELDS = new Set([
|
|
36
|
+
"name",
|
|
37
|
+
"what to call them",
|
|
38
|
+
"project",
|
|
39
|
+
"projects",
|
|
40
|
+
"skill",
|
|
41
|
+
"skills",
|
|
42
|
+
"interest",
|
|
43
|
+
"interests",
|
|
44
|
+
]);
|
|
45
|
+
function isTemplateText(value) {
|
|
46
|
+
const trimmed = value.trim();
|
|
47
|
+
return trimmed.length === 0 || TEMPLATE_PATTERN.test(trimmed) || /^\[[^\]]+\]$/.test(trimmed);
|
|
48
|
+
}
|
|
49
|
+
function stripMarkdown(line) {
|
|
50
|
+
return line
|
|
51
|
+
.replace(FIELD_PREFIX_PATTERN, "")
|
|
52
|
+
.replace(/`([^`]+)`/g, "$1")
|
|
53
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
|
54
|
+
.replace(/[*_~#>]/g, " ")
|
|
55
|
+
.trim();
|
|
56
|
+
}
|
|
57
|
+
function trimCandidate(value) {
|
|
58
|
+
return value
|
|
59
|
+
.replace(/^[\s"'“”‘’()[\]{}<>,。;:、,.;:!?/\\|-]+/, "")
|
|
60
|
+
.replace(/[\s"'“”‘’()[\]{}<>,。;:、,.;:!?/\\|-]+$/, "")
|
|
61
|
+
.trim();
|
|
62
|
+
}
|
|
63
|
+
function trimChineseCandidate(value) {
|
|
64
|
+
return trimCandidate(value)
|
|
65
|
+
.replace(/^(?:我|俺|在|和|与|跟|做|写|用|是|的|也|经常|正在|参与)+/u, "")
|
|
66
|
+
.replace(/(?:做|写|用|是|的|了|中|相关|项目|插件)+$/u, "")
|
|
67
|
+
.trim();
|
|
68
|
+
}
|
|
69
|
+
function isStableChineseCandidate(value) {
|
|
70
|
+
return (/^[\p{Script=Han}]{2,8}$/u.test(value) &&
|
|
71
|
+
!CHINESE_SENTENCE_WORD_PATTERN.test(value));
|
|
72
|
+
}
|
|
73
|
+
function collectChineseCandidates(value) {
|
|
74
|
+
const candidates = [];
|
|
75
|
+
for (const match of value.matchAll(CHINESE_TAG_PATTERN)) {
|
|
76
|
+
const candidate = trimChineseCandidate(match[0]);
|
|
77
|
+
if (isStableChineseCandidate(candidate)) {
|
|
78
|
+
candidates.push(candidate);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return candidates;
|
|
82
|
+
}
|
|
83
|
+
function looksLikeSentence(value) {
|
|
84
|
+
return /[。.!?]/.test(value) || value.split(/\s+/).filter(Boolean).length > 4;
|
|
85
|
+
}
|
|
86
|
+
function fieldValue(line) {
|
|
87
|
+
const match = line.match(FIELD_LINE_PATTERN);
|
|
88
|
+
if (!match) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
field: (match[1] ?? "").toLowerCase(),
|
|
93
|
+
value: stripMarkdown(match[2] ?? ""),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function isStableEnglishCandidate(value) {
|
|
97
|
+
const candidate = trimCandidate(value).replace(/^(?:and|or)\s+/i, "");
|
|
98
|
+
return (ENGLISH_TAG_PATTERN.test(candidate) &&
|
|
99
|
+
!COMMON_WORDS.has(candidate.toLowerCase()) &&
|
|
100
|
+
!/^\d+$/.test(candidate));
|
|
101
|
+
}
|
|
102
|
+
function allowsEnglishTags(field) {
|
|
103
|
+
return field !== undefined && ENGLISH_TAG_FIELDS.has(field);
|
|
104
|
+
}
|
|
105
|
+
function isValidTagCandidate(value) {
|
|
106
|
+
if (!value || value.length > MAX_TAG_LENGTH || isTemplateText(value) || looksLikeSentence(value)) {
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
if (/^\d+$/.test(value)) {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
function pushTag(tags, seen, rawValue) {
|
|
115
|
+
const value = trimCandidate(rawValue);
|
|
116
|
+
if (!isValidTagCandidate(value)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const key = normalizeAttributeValue(value);
|
|
120
|
+
if (seen.has(key)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
seen.add(key);
|
|
124
|
+
tags.push({
|
|
125
|
+
kind: "tag",
|
|
126
|
+
value,
|
|
127
|
+
label: value,
|
|
128
|
+
source: "USER.md",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
function collectCandidates(line) {
|
|
132
|
+
const candidates = [];
|
|
133
|
+
const text = stripMarkdown(line);
|
|
134
|
+
if (isTemplateText(text)) {
|
|
135
|
+
return candidates;
|
|
136
|
+
}
|
|
137
|
+
const explicitField = fieldValue(line);
|
|
138
|
+
const explicitFieldValue = explicitField?.value;
|
|
139
|
+
if (/^[\p{Script=Han}]{2,8}$/u.test(text) && isStableChineseCandidate(text)) {
|
|
140
|
+
candidates.push(text);
|
|
141
|
+
}
|
|
142
|
+
if (EXPLICIT_LIST_SEPARATOR_PATTERN.test(text)) {
|
|
143
|
+
for (const part of text.split(EXPLICIT_LIST_SEPARATOR_PATTERN)) {
|
|
144
|
+
candidates.push(...collectChineseCandidates(part));
|
|
145
|
+
if (explicitFieldValue && allowsEnglishTags(explicitField?.field)) {
|
|
146
|
+
const value = trimCandidate(part).replace(/^(?:and|or)\s+/i, "");
|
|
147
|
+
if (isStableEnglishCandidate(value)) {
|
|
148
|
+
candidates.push(value);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const pattern of CHINESE_CONTEXT_PATTERNS) {
|
|
154
|
+
for (const match of text.matchAll(pattern)) {
|
|
155
|
+
const value = trimChineseCandidate(match[1] ?? "");
|
|
156
|
+
if (isStableChineseCandidate(value)) {
|
|
157
|
+
candidates.push(value);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (explicitField &&
|
|
162
|
+
allowsEnglishTags(explicitField.field) &&
|
|
163
|
+
!EXPLICIT_LIST_SEPARATOR_PATTERN.test(explicitField.value) &&
|
|
164
|
+
!looksLikeSentence(explicitField.value)) {
|
|
165
|
+
const value = trimCandidate(explicitField.value).replace(/^(?:and|or)\s+/i, "");
|
|
166
|
+
if (isStableEnglishCandidate(value)) {
|
|
167
|
+
candidates.push(value);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return candidates;
|
|
171
|
+
}
|
|
172
|
+
export function extractUserMdTags(markdown) {
|
|
173
|
+
const tags = [];
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
for (const line of markdown.split(/\r?\n/)) {
|
|
176
|
+
for (const candidate of collectCandidates(line)) {
|
|
177
|
+
pushTag(tags, seen, candidate);
|
|
178
|
+
if (tags.length >= MAX_TAGS) {
|
|
179
|
+
return tags;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return tags;
|
|
184
|
+
}
|
|
185
|
+
export function createUserMdAttributeSource(options) {
|
|
186
|
+
const filePath = options?.path ?? path.join(process.cwd(), "USER.md");
|
|
187
|
+
const logger = options?.logger;
|
|
188
|
+
return {
|
|
189
|
+
async loadTags() {
|
|
190
|
+
try {
|
|
191
|
+
return extractUserMdTags(await readFile(filePath, "utf8"));
|
|
192
|
+
}
|
|
193
|
+
catch (error) {
|
|
194
|
+
if (error.code === "ENOENT") {
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
logger?.warn?.(`[libp2p-mesh] Failed to read USER.md at ${filePath}: ${error.message}`);
|
|
198
|
+
return [];
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
};
|
|
202
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { UserPublicAttribute } from "./types.js";
|
|
2
|
+
export type UserProfile = {
|
|
3
|
+
version: 1;
|
|
4
|
+
updatedAt: number;
|
|
5
|
+
attributes: UserPublicAttribute[];
|
|
6
|
+
};
|
|
7
|
+
export type UserProfileLogger = {
|
|
8
|
+
debug?: (message: string) => void;
|
|
9
|
+
warn?: (message: string) => void;
|
|
10
|
+
};
|
|
11
|
+
export type UserProfileAttributeTarget = string | number;
|
|
12
|
+
export type UserProfileStore = {
|
|
13
|
+
load(): Promise<UserProfile>;
|
|
14
|
+
save(profile: UserProfile): Promise<UserProfile>;
|
|
15
|
+
listAttributes(): Promise<UserPublicAttribute[]>;
|
|
16
|
+
replaceAttributes(attributes: UserPublicAttribute[]): Promise<UserProfile>;
|
|
17
|
+
updateAttribute(target: UserProfileAttributeTarget, attribute: UserPublicAttribute): Promise<UserProfile>;
|
|
18
|
+
removeAttribute(target: UserProfileAttributeTarget): Promise<UserProfile>;
|
|
19
|
+
};
|
|
20
|
+
export declare function resolveUserProfilePath(customPath?: string): string;
|
|
21
|
+
export declare function getUserProfileAttributeId(attribute: UserPublicAttribute): string;
|
|
22
|
+
export declare function createUserProfileStore(options?: {
|
|
23
|
+
path?: string;
|
|
24
|
+
logger?: UserProfileLogger;
|
|
25
|
+
}): UserProfileStore;
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { normalizeAttributeKey, normalizeAttributeValue, } from "./user-attributes.js";
|
|
5
|
+
export function resolveUserProfilePath(customPath) {
|
|
6
|
+
if (customPath)
|
|
7
|
+
return customPath;
|
|
8
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR;
|
|
9
|
+
if (stateDir) {
|
|
10
|
+
return path.join(stateDir, "libp2p", "user-profile.json");
|
|
11
|
+
}
|
|
12
|
+
return path.join(homedir(), ".openclaw", "libp2p", "user-profile.json");
|
|
13
|
+
}
|
|
14
|
+
export function getUserProfileAttributeId(attribute) {
|
|
15
|
+
if (attribute.kind !== "structured") {
|
|
16
|
+
return "";
|
|
17
|
+
}
|
|
18
|
+
return `structured:${normalizeAttributeKey(attribute.key)}:${normalizeAttributeValue(attribute.value)}`;
|
|
19
|
+
}
|
|
20
|
+
function emptyProfile() {
|
|
21
|
+
return {
|
|
22
|
+
version: 1,
|
|
23
|
+
updatedAt: Date.now(),
|
|
24
|
+
attributes: [],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function isRecord(value) {
|
|
28
|
+
return typeof value === "object" && value !== null;
|
|
29
|
+
}
|
|
30
|
+
function trimmedString(value) {
|
|
31
|
+
if (typeof value !== "string") {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
const trimmed = value.trim();
|
|
35
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
36
|
+
}
|
|
37
|
+
function normalizeProfileAttribute(value) {
|
|
38
|
+
if (!isRecord(value) || value.kind !== "structured") {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (value.source !== undefined && value.source !== "profile") {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const key = trimmedString(value.key);
|
|
45
|
+
const attributeValue = trimmedString(value.value);
|
|
46
|
+
const label = trimmedString(value.label);
|
|
47
|
+
if (!key || !attributeValue || !label) {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
kind: "structured",
|
|
52
|
+
key: normalizeAttributeKey(key),
|
|
53
|
+
value: attributeValue,
|
|
54
|
+
label,
|
|
55
|
+
source: "profile",
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function normalizeProfileAttributes(attributes) {
|
|
59
|
+
if (!Array.isArray(attributes)) {
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
const normalized = [];
|
|
63
|
+
const seen = new Set();
|
|
64
|
+
for (const value of attributes) {
|
|
65
|
+
const attribute = normalizeProfileAttribute(value);
|
|
66
|
+
if (!attribute) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
const id = getUserProfileAttributeId(attribute);
|
|
70
|
+
if (seen.has(id)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
seen.add(id);
|
|
74
|
+
normalized.push(attribute);
|
|
75
|
+
}
|
|
76
|
+
return normalized;
|
|
77
|
+
}
|
|
78
|
+
function normalizeProfile(value) {
|
|
79
|
+
const candidate = isRecord(value) ? value : {};
|
|
80
|
+
return {
|
|
81
|
+
version: 1,
|
|
82
|
+
updatedAt: typeof candidate.updatedAt === "number" ? candidate.updatedAt : Date.now(),
|
|
83
|
+
attributes: normalizeProfileAttributes(candidate.attributes),
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function resolveTargetIndex(attributes, target) {
|
|
87
|
+
if (typeof target === "number") {
|
|
88
|
+
return Number.isInteger(target) && target >= 0 && target < attributes.length ? target : -1;
|
|
89
|
+
}
|
|
90
|
+
return attributes.findIndex((attribute) => getUserProfileAttributeId(attribute) === target);
|
|
91
|
+
}
|
|
92
|
+
export function createUserProfileStore(options) {
|
|
93
|
+
const filePath = resolveUserProfilePath(options?.path);
|
|
94
|
+
const logger = options?.logger;
|
|
95
|
+
let cached;
|
|
96
|
+
let mutationQueue = Promise.resolve();
|
|
97
|
+
async function load() {
|
|
98
|
+
try {
|
|
99
|
+
const raw = await readFile(filePath, "utf8");
|
|
100
|
+
cached = normalizeProfile(JSON.parse(raw));
|
|
101
|
+
return cached;
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
const code = error.code;
|
|
105
|
+
if (code === "ENOENT") {
|
|
106
|
+
cached = emptyProfile();
|
|
107
|
+
return cached;
|
|
108
|
+
}
|
|
109
|
+
const backupPath = `${filePath}.corrupt-${Date.now()}`;
|
|
110
|
+
try {
|
|
111
|
+
await mkdir(path.dirname(filePath), { recursive: true });
|
|
112
|
+
await rename(filePath, backupPath);
|
|
113
|
+
logger?.warn?.(`[libp2p-mesh] User profile store unreadable; moved to ${backupPath}`);
|
|
114
|
+
}
|
|
115
|
+
catch (renameError) {
|
|
116
|
+
logger?.warn?.(`[libp2p-mesh] User profile store unreadable; failed to move corrupt file to ${backupPath}: ${renameError.message}`);
|
|
117
|
+
}
|
|
118
|
+
cached = emptyProfile();
|
|
119
|
+
return cached;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
async function save(profile) {
|
|
123
|
+
const nextProfile = {
|
|
124
|
+
version: 1,
|
|
125
|
+
updatedAt: Date.now(),
|
|
126
|
+
attributes: normalizeProfileAttributes(profile.attributes),
|
|
127
|
+
};
|
|
128
|
+
const dir = path.dirname(filePath);
|
|
129
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
130
|
+
await mkdir(dir, { recursive: true });
|
|
131
|
+
await writeFile(tmpPath, `${JSON.stringify(nextProfile, null, 2)}\n`, "utf8");
|
|
132
|
+
await rename(tmpPath, filePath);
|
|
133
|
+
cached = nextProfile;
|
|
134
|
+
logger?.debug?.(`[libp2p-mesh] Saved user profile store to ${filePath}`);
|
|
135
|
+
return nextProfile;
|
|
136
|
+
}
|
|
137
|
+
async function runMutation(fn) {
|
|
138
|
+
const next = mutationQueue.then(fn, fn);
|
|
139
|
+
mutationQueue = next.then(() => undefined, () => undefined);
|
|
140
|
+
return next;
|
|
141
|
+
}
|
|
142
|
+
return {
|
|
143
|
+
load,
|
|
144
|
+
save,
|
|
145
|
+
async listAttributes() {
|
|
146
|
+
const profile = cached ?? (await load());
|
|
147
|
+
return [...profile.attributes];
|
|
148
|
+
},
|
|
149
|
+
async replaceAttributes(attributes) {
|
|
150
|
+
return runMutation(async () => {
|
|
151
|
+
const profile = await load();
|
|
152
|
+
return save({
|
|
153
|
+
...profile,
|
|
154
|
+
attributes,
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
},
|
|
158
|
+
async updateAttribute(target, attribute) {
|
|
159
|
+
return runMutation(async () => {
|
|
160
|
+
const profile = await load();
|
|
161
|
+
const index = resolveTargetIndex(profile.attributes, target);
|
|
162
|
+
if (index < 0) {
|
|
163
|
+
throw new RangeError(`User profile attribute not found: ${String(target)}`);
|
|
164
|
+
}
|
|
165
|
+
const attributes = [...profile.attributes];
|
|
166
|
+
attributes[index] = attribute;
|
|
167
|
+
return save({
|
|
168
|
+
...profile,
|
|
169
|
+
attributes,
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
},
|
|
173
|
+
async removeAttribute(target) {
|
|
174
|
+
return runMutation(async () => {
|
|
175
|
+
const profile = await load();
|
|
176
|
+
const index = resolveTargetIndex(profile.attributes, target);
|
|
177
|
+
if (index < 0) {
|
|
178
|
+
throw new RangeError(`User profile attribute not found: ${String(target)}`);
|
|
179
|
+
}
|
|
180
|
+
return save({
|
|
181
|
+
...profile,
|
|
182
|
+
attributes: profile.attributes.filter((_, attributeIndex) => attributeIndex !== index),
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
package/openclaw.plugin.json
CHANGED