libp2p-mesh 2026.6.22 → 2026.6.24
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/dist/src/instance-router.js +11 -1
- package/dist/src/plugin.js +26 -0
- package/dist/src/prompt-config.js +1 -1
- package/dist/src/types.d.ts +3 -0
- package/dist/src/user-md-openclaw-extractor.d.ts +1 -0
- package/dist/src/user-md-openclaw-extractor.js +60 -26
- package/package.json +1 -1
- package/src/instance-router.ts +12 -1
- package/src/plugin.ts +30 -0
- package/src/prompt-config.ts +1 -1
- package/src/types.ts +3 -0
- package/src/user-md-openclaw-extractor.ts +76 -28
|
@@ -121,6 +121,7 @@ export function createInstanceRouter(options) {
|
|
|
121
121
|
const deliveryCache = new Map();
|
|
122
122
|
const unsubs = [];
|
|
123
123
|
const pendingAttributePeers = new Set();
|
|
124
|
+
let publicAttributeRefreshEnabled = options.publicAttributeRefreshInitiallyEnabled !== false;
|
|
124
125
|
let attributeRefreshPromise;
|
|
125
126
|
function localInstanceId() {
|
|
126
127
|
const identity = mesh.getInstanceIdentity();
|
|
@@ -215,7 +216,9 @@ export function createInstanceRouter(options) {
|
|
|
215
216
|
if (pendingAttributePeers.size === 0 && !attributeRefreshPromise) {
|
|
216
217
|
return;
|
|
217
218
|
}
|
|
218
|
-
|
|
219
|
+
if (publicAttributeRefreshEnabled) {
|
|
220
|
+
drainAttributeRefresh();
|
|
221
|
+
}
|
|
219
222
|
}
|
|
220
223
|
function drainAttributeRefresh() {
|
|
221
224
|
if (!attributeRefreshPromise) {
|
|
@@ -233,6 +236,12 @@ export function createInstanceRouter(options) {
|
|
|
233
236
|
}
|
|
234
237
|
return attributeRefreshPromise;
|
|
235
238
|
}
|
|
239
|
+
function enablePublicAttributeRefresh() {
|
|
240
|
+
publicAttributeRefreshEnabled = true;
|
|
241
|
+
if (pendingAttributePeers.size > 0) {
|
|
242
|
+
drainAttributeRefresh();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
236
245
|
async function announceToPeer(peerId) {
|
|
237
246
|
if (!isNonEmptyString(peerId) || peerId === mesh.getLocalPeerId()) {
|
|
238
247
|
return;
|
|
@@ -667,6 +676,7 @@ export function createInstanceRouter(options) {
|
|
|
667
676
|
stop,
|
|
668
677
|
handleMessage,
|
|
669
678
|
announceToPeer,
|
|
679
|
+
enablePublicAttributeRefresh,
|
|
670
680
|
refreshPublicAttributes,
|
|
671
681
|
listInstances,
|
|
672
682
|
resolveInstance,
|
package/dist/src/plugin.js
CHANGED
|
@@ -10,6 +10,7 @@ import { createOpenClawUserMdAttributeExtractor } from "./user-md-openclaw-extra
|
|
|
10
10
|
import { createUserProfileStore } from "./user-profile-store.js";
|
|
11
11
|
import { buildP2PTools } from "./agent-tools.js";
|
|
12
12
|
import { registerLibp2pMeshCli } from "./profile-cli.js";
|
|
13
|
+
const DEFAULT_PUBLIC_ATTRIBUTE_REFRESH_STARTUP_DELAY_MS = 10000;
|
|
13
14
|
export function registerLibp2pMesh(api) {
|
|
14
15
|
registerLibp2pMeshWithDeps(api);
|
|
15
16
|
}
|
|
@@ -18,6 +19,7 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
|
|
|
18
19
|
let unsubscribeInbound;
|
|
19
20
|
let serviceStarted = false;
|
|
20
21
|
let startPromise;
|
|
22
|
+
let publicAttributeRefreshTimer;
|
|
21
23
|
const mesh = (deps.createMeshNetwork ?? createMeshNetwork)({
|
|
22
24
|
config,
|
|
23
25
|
logger: api.logger,
|
|
@@ -50,6 +52,7 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
|
|
|
50
52
|
userAttributeSource,
|
|
51
53
|
userProfileStore,
|
|
52
54
|
peerLabelStore,
|
|
55
|
+
publicAttributeRefreshInitiallyEnabled: false,
|
|
53
56
|
});
|
|
54
57
|
registerLibp2pMeshCli(api, {
|
|
55
58
|
profile: {
|
|
@@ -91,7 +94,28 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
|
|
|
91
94
|
}
|
|
92
95
|
});
|
|
93
96
|
}
|
|
97
|
+
function publicAttributeRefreshStartupDelayMs() {
|
|
98
|
+
const configured = config?.publicAttributeRefreshStartupDelayMs;
|
|
99
|
+
if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
|
|
100
|
+
return configured;
|
|
101
|
+
}
|
|
102
|
+
return DEFAULT_PUBLIC_ATTRIBUTE_REFRESH_STARTUP_DELAY_MS;
|
|
103
|
+
}
|
|
104
|
+
function clearPublicAttributeRefreshTimer() {
|
|
105
|
+
if (publicAttributeRefreshTimer) {
|
|
106
|
+
clearTimeout(publicAttributeRefreshTimer);
|
|
107
|
+
publicAttributeRefreshTimer = undefined;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function schedulePublicAttributeRefreshEnable() {
|
|
111
|
+
clearPublicAttributeRefreshTimer();
|
|
112
|
+
publicAttributeRefreshTimer = setTimeout(() => {
|
|
113
|
+
publicAttributeRefreshTimer = undefined;
|
|
114
|
+
router.enablePublicAttributeRefresh();
|
|
115
|
+
}, publicAttributeRefreshStartupDelayMs());
|
|
116
|
+
}
|
|
94
117
|
async function cleanupStartupFailure() {
|
|
118
|
+
clearPublicAttributeRefreshTimer();
|
|
95
119
|
try {
|
|
96
120
|
unsubscribeInbound?.();
|
|
97
121
|
}
|
|
@@ -149,6 +173,7 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
|
|
|
149
173
|
api.logger.info?.(`[libp2p-mesh] Active relay reservations: ${nat.reservedRelays.join(", ")}`);
|
|
150
174
|
}
|
|
151
175
|
serviceStarted = true;
|
|
176
|
+
schedulePublicAttributeRefreshEnable();
|
|
152
177
|
}
|
|
153
178
|
catch (error) {
|
|
154
179
|
await cleanupStartupFailure();
|
|
@@ -162,6 +187,7 @@ export function registerLibp2pMeshWithDeps(api, deps = {}) {
|
|
|
162
187
|
},
|
|
163
188
|
stop: async () => {
|
|
164
189
|
await startPromise?.catch(() => undefined);
|
|
190
|
+
clearPublicAttributeRefreshTimer();
|
|
165
191
|
unsubscribeInbound?.();
|
|
166
192
|
unsubscribeInbound = undefined;
|
|
167
193
|
await router.stop();
|
|
@@ -40,7 +40,7 @@ export const LIBP2P_MESH_AGENT_PROMPT = `
|
|
|
40
40
|
|
|
41
41
|
- \`source="USER.md"\` 表示 gateway 使用 OpenClaw 已配置的 agent/API 模型,从 USER.md 异步提取并公开广播的 tag。
|
|
42
42
|
- \`source="profile"\` 表示用户通过 \`openclaw libp2p-mesh profile\` 手动配置的公开结构化属性。
|
|
43
|
-
- gateway 的基础 \`instance-announce\` 可能省略 \`userPublicAttributes\`;这表示本次 announce
|
|
43
|
+
- gateway 的基础 \`instance-announce\` 可能省略 \`userPublicAttributes\`;这表示本次 announce 没有携带属性,不一定表示该用户没有公开属性。启动后 USER.md 属性刷新可能会延迟数秒完成。按公开属性发送前先用 \`p2p_list_instances\` 查看当前实例记录。
|
|
44
44
|
- 普通对话 agent 不应自己读取 USER.md 来决定公开属性。USER.md 属性提取是 gateway 后台职责。
|
|
45
45
|
|
|
46
46
|
3. \`openclaw libp2p-mesh labels\` manages local labels for remote instances. These labels are stored in the local \`peer-labels.json\`, stay on this machine, and are used only when the send tool uses \`scope="local"\` or \`scope="all"\`.
|
package/dist/src/types.d.ts
CHANGED
|
@@ -216,6 +216,7 @@ export type InstanceRouterOptions = {
|
|
|
216
216
|
peerLabelStore?: {
|
|
217
217
|
listLabels(instanceId: string): Promise<LocalPeerLabelAttribute[]>;
|
|
218
218
|
};
|
|
219
|
+
publicAttributeRefreshInitiallyEnabled?: boolean;
|
|
219
220
|
};
|
|
220
221
|
export interface InstanceRouter {
|
|
221
222
|
attachHandlers(): void;
|
|
@@ -224,6 +225,7 @@ export interface InstanceRouter {
|
|
|
224
225
|
stop(): Promise<void>;
|
|
225
226
|
handleMessage(msg: P2PMessage): Promise<void>;
|
|
226
227
|
announceToPeer(peerId: string): Promise<void>;
|
|
228
|
+
enablePublicAttributeRefresh(): void;
|
|
227
229
|
refreshPublicAttributes(): Promise<void>;
|
|
228
230
|
listInstances(): Promise<InstancePeerRecord[]>;
|
|
229
231
|
resolveInstance(instanceId: string): Promise<InstancePeerRecord | undefined>;
|
|
@@ -246,6 +248,7 @@ export interface MeshConfig {
|
|
|
246
248
|
bootstrapList?: string[];
|
|
247
249
|
meshTopic?: string;
|
|
248
250
|
announceLogDetail?: AnnounceLogDetail;
|
|
251
|
+
publicAttributeRefreshStartupDelayMs?: number;
|
|
249
252
|
enableAgentSync?: boolean;
|
|
250
253
|
enableWebSocket?: boolean;
|
|
251
254
|
peerIdPath?: string;
|
|
@@ -6,6 +6,7 @@ type Logger = {
|
|
|
6
6
|
warn?: (message: string) => void;
|
|
7
7
|
};
|
|
8
8
|
export declare function extractLatestAssistantText(messages: unknown[]): string | undefined;
|
|
9
|
+
export declare function extractCompletionText(result: unknown): string | undefined;
|
|
9
10
|
export declare function parseUserMdAttributeResponse(text: string): UserPublicAttribute[];
|
|
10
11
|
export declare function createOpenClawUserMdAttributeExtractor(api: OpenClawPluginApi, options?: {
|
|
11
12
|
timeoutMs?: number;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import { validateExtractedUserMdTags } from "./user-md-agent-attributes.js";
|
|
3
2
|
const EXTRACTION_TIMEOUT_MS = 30000;
|
|
4
|
-
const SESSION_HASH_LENGTH = 16;
|
|
5
3
|
export const USER_MD_ATTRIBUTE_EXTRACTION_PROMPT = [
|
|
6
4
|
"你是 libp2p-mesh 的 USER.md 公开属性提取器。",
|
|
7
5
|
"",
|
|
@@ -16,9 +14,6 @@ export const USER_MD_ATTRIBUTE_EXTRACTION_PROMPT = [
|
|
|
16
14
|
"- 不要提取“刚认识”“还在了解”“随时告诉我”这类无分类价值内容。",
|
|
17
15
|
"- 优先提取稳定身份、技术方向、项目方向、长期偏好。",
|
|
18
16
|
].join("\n");
|
|
19
|
-
function hashForSessionKey(value) {
|
|
20
|
-
return createHash("sha256").update(value, "utf8").digest("hex").slice(0, SESSION_HASH_LENGTH);
|
|
21
|
-
}
|
|
22
17
|
function buildUserMdExtractionMessage(markdown, sourcePath) {
|
|
23
18
|
return [
|
|
24
19
|
`sourcePath: ${sourcePath}`,
|
|
@@ -75,6 +70,42 @@ export function extractLatestAssistantText(messages) {
|
|
|
75
70
|
}
|
|
76
71
|
return undefined;
|
|
77
72
|
}
|
|
73
|
+
export function extractCompletionText(result) {
|
|
74
|
+
if (typeof result === "string") {
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
if (!isRecord(result)) {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
const direct = extractTextFromContent(result.content) ??
|
|
81
|
+
(typeof result.text === "string" ? result.text : undefined) ??
|
|
82
|
+
(typeof result.outputText === "string" ? result.outputText : undefined) ??
|
|
83
|
+
(typeof result.message === "string" ? result.message : undefined);
|
|
84
|
+
if (direct?.trim()) {
|
|
85
|
+
return direct;
|
|
86
|
+
}
|
|
87
|
+
const choices = result.choices;
|
|
88
|
+
if (!Array.isArray(choices)) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
for (const choice of choices) {
|
|
92
|
+
if (!isRecord(choice)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const message = choice.message;
|
|
96
|
+
if (isRecord(message)) {
|
|
97
|
+
const text = extractTextFromContent(message.content);
|
|
98
|
+
if (text?.trim()) {
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const text = extractTextFromContent(choice.content);
|
|
103
|
+
if (text?.trim()) {
|
|
104
|
+
return text;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
78
109
|
function stripJsonFence(text) {
|
|
79
110
|
const trimmed = text.trim();
|
|
80
111
|
const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
@@ -91,32 +122,38 @@ export function parseUserMdAttributeResponse(text) {
|
|
|
91
122
|
function unavailable(reason) {
|
|
92
123
|
return { unavailable: true, reason };
|
|
93
124
|
}
|
|
125
|
+
function runtimeLlm(api) {
|
|
126
|
+
const runtime = api.runtime;
|
|
127
|
+
if (!isRecord(runtime)) {
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
const llm = runtime.llm;
|
|
131
|
+
if (!isRecord(llm) || typeof llm.complete !== "function") {
|
|
132
|
+
return undefined;
|
|
133
|
+
}
|
|
134
|
+
return llm;
|
|
135
|
+
}
|
|
94
136
|
export function createOpenClawUserMdAttributeExtractor(api, options) {
|
|
95
137
|
const timeoutMs = options?.timeoutMs ?? EXTRACTION_TIMEOUT_MS;
|
|
96
138
|
const logger = options?.logger ?? api.logger;
|
|
97
139
|
return {
|
|
98
140
|
async extract({ markdown, sourcePath }) {
|
|
99
|
-
const
|
|
100
|
-
if (!
|
|
101
|
-
return unavailable("OpenClaw
|
|
141
|
+
const llm = runtimeLlm(api);
|
|
142
|
+
if (!llm) {
|
|
143
|
+
return unavailable("OpenClaw runtime llm.complete is unavailable");
|
|
102
144
|
}
|
|
103
|
-
const sessionKey = `libp2p-mesh:user-md-attributes:${hashForSessionKey(sourcePath)}`;
|
|
104
145
|
try {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
146
|
+
const result = await llm.complete({
|
|
147
|
+
messages: [
|
|
148
|
+
{ role: "system", content: USER_MD_ATTRIBUTE_EXTRACTION_PROMPT },
|
|
149
|
+
{ role: "user", content: buildUserMdExtractionMessage(markdown, sourcePath) },
|
|
150
|
+
],
|
|
151
|
+
purpose: "libp2p-mesh.user-md-attributes",
|
|
152
|
+
maxTokens: 512,
|
|
153
|
+
temperature: 0.1,
|
|
154
|
+
timeoutMs,
|
|
113
155
|
});
|
|
114
|
-
const
|
|
115
|
-
if (waitResult.status !== "ok") {
|
|
116
|
-
return unavailable(waitResult.error ?? `OpenClaw subagent extraction ${waitResult.status}`);
|
|
117
|
-
}
|
|
118
|
-
const { messages } = await subagent.getSessionMessages({ sessionKey, limit: 10 });
|
|
119
|
-
const text = extractLatestAssistantText(messages);
|
|
156
|
+
const text = extractCompletionText(result);
|
|
120
157
|
if (!text) {
|
|
121
158
|
return [];
|
|
122
159
|
}
|
|
@@ -127,9 +164,6 @@ export function createOpenClawUserMdAttributeExtractor(api, options) {
|
|
|
127
164
|
logger?.warn?.(`[libp2p-mesh] USER.md agent extraction failed: ${reason}`);
|
|
128
165
|
return unavailable(reason);
|
|
129
166
|
}
|
|
130
|
-
finally {
|
|
131
|
-
await subagent.deleteSession?.({ sessionKey, deleteTranscript: true }).catch(() => undefined);
|
|
132
|
-
}
|
|
133
167
|
},
|
|
134
168
|
};
|
|
135
169
|
}
|
package/package.json
CHANGED
package/src/instance-router.ts
CHANGED
|
@@ -218,6 +218,7 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
218
218
|
const deliveryCache = new Map<string, DeliveryCacheEntry>();
|
|
219
219
|
const unsubs: Array<() => void> = [];
|
|
220
220
|
const pendingAttributePeers = new Set<string>();
|
|
221
|
+
let publicAttributeRefreshEnabled = options.publicAttributeRefreshInitiallyEnabled !== false;
|
|
221
222
|
let attributeRefreshPromise: Promise<void> | undefined;
|
|
222
223
|
|
|
223
224
|
function localInstanceId(): string {
|
|
@@ -335,7 +336,9 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
335
336
|
return;
|
|
336
337
|
}
|
|
337
338
|
|
|
338
|
-
|
|
339
|
+
if (publicAttributeRefreshEnabled) {
|
|
340
|
+
drainAttributeRefresh();
|
|
341
|
+
}
|
|
339
342
|
}
|
|
340
343
|
|
|
341
344
|
function drainAttributeRefresh(): Promise<void> {
|
|
@@ -358,6 +361,13 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
358
361
|
return attributeRefreshPromise;
|
|
359
362
|
}
|
|
360
363
|
|
|
364
|
+
function enablePublicAttributeRefresh(): void {
|
|
365
|
+
publicAttributeRefreshEnabled = true;
|
|
366
|
+
if (pendingAttributePeers.size > 0) {
|
|
367
|
+
drainAttributeRefresh();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
361
371
|
async function announceToPeer(peerId: string): Promise<void> {
|
|
362
372
|
if (!isNonEmptyString(peerId) || peerId === mesh.getLocalPeerId()) {
|
|
363
373
|
return;
|
|
@@ -896,6 +906,7 @@ export function createInstanceRouter(options: InstanceRouterOptions): InstanceRo
|
|
|
896
906
|
stop,
|
|
897
907
|
handleMessage,
|
|
898
908
|
announceToPeer,
|
|
909
|
+
enablePublicAttributeRefresh,
|
|
899
910
|
refreshPublicAttributes,
|
|
900
911
|
listInstances,
|
|
901
912
|
resolveInstance,
|
package/src/plugin.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { registerLibp2pMeshCli } from "./profile-cli.js";
|
|
|
14
14
|
import type { ChannelPlugin } from "openclaw/plugin-sdk/core";
|
|
15
15
|
import type { MeshConfig } from "./types.js";
|
|
16
16
|
|
|
17
|
+
const DEFAULT_PUBLIC_ATTRIBUTE_REFRESH_STARTUP_DELAY_MS = 10000;
|
|
18
|
+
|
|
17
19
|
export type Libp2pMeshPluginDeps = {
|
|
18
20
|
createMeshNetwork?: typeof createMeshNetwork;
|
|
19
21
|
createInstanceRouter?: typeof createInstanceRouter;
|
|
@@ -31,6 +33,7 @@ export function registerLibp2pMeshWithDeps(
|
|
|
31
33
|
let unsubscribeInbound: (() => void) | undefined;
|
|
32
34
|
let serviceStarted = false;
|
|
33
35
|
let startPromise: Promise<void> | undefined;
|
|
36
|
+
let publicAttributeRefreshTimer: ReturnType<typeof setTimeout> | undefined;
|
|
34
37
|
const mesh = (deps.createMeshNetwork ?? createMeshNetwork)({
|
|
35
38
|
config,
|
|
36
39
|
logger: api.logger,
|
|
@@ -65,6 +68,7 @@ export function registerLibp2pMeshWithDeps(
|
|
|
65
68
|
userAttributeSource,
|
|
66
69
|
userProfileStore,
|
|
67
70
|
peerLabelStore,
|
|
71
|
+
publicAttributeRefreshInitiallyEnabled: false,
|
|
68
72
|
});
|
|
69
73
|
|
|
70
74
|
registerLibp2pMeshCli(api, {
|
|
@@ -114,7 +118,31 @@ export function registerLibp2pMeshWithDeps(
|
|
|
114
118
|
});
|
|
115
119
|
}
|
|
116
120
|
|
|
121
|
+
function publicAttributeRefreshStartupDelayMs(): number {
|
|
122
|
+
const configured = config?.publicAttributeRefreshStartupDelayMs;
|
|
123
|
+
if (typeof configured === "number" && Number.isFinite(configured) && configured >= 0) {
|
|
124
|
+
return configured;
|
|
125
|
+
}
|
|
126
|
+
return DEFAULT_PUBLIC_ATTRIBUTE_REFRESH_STARTUP_DELAY_MS;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function clearPublicAttributeRefreshTimer(): void {
|
|
130
|
+
if (publicAttributeRefreshTimer) {
|
|
131
|
+
clearTimeout(publicAttributeRefreshTimer);
|
|
132
|
+
publicAttributeRefreshTimer = undefined;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function schedulePublicAttributeRefreshEnable(): void {
|
|
137
|
+
clearPublicAttributeRefreshTimer();
|
|
138
|
+
publicAttributeRefreshTimer = setTimeout(() => {
|
|
139
|
+
publicAttributeRefreshTimer = undefined;
|
|
140
|
+
router.enablePublicAttributeRefresh();
|
|
141
|
+
}, publicAttributeRefreshStartupDelayMs());
|
|
142
|
+
}
|
|
143
|
+
|
|
117
144
|
async function cleanupStartupFailure(): Promise<void> {
|
|
145
|
+
clearPublicAttributeRefreshTimer();
|
|
118
146
|
try {
|
|
119
147
|
unsubscribeInbound?.();
|
|
120
148
|
} catch (error) {
|
|
@@ -186,6 +214,7 @@ export function registerLibp2pMeshWithDeps(
|
|
|
186
214
|
);
|
|
187
215
|
}
|
|
188
216
|
serviceStarted = true;
|
|
217
|
+
schedulePublicAttributeRefreshEnable();
|
|
189
218
|
} catch (error) {
|
|
190
219
|
await cleanupStartupFailure();
|
|
191
220
|
throw error;
|
|
@@ -198,6 +227,7 @@ export function registerLibp2pMeshWithDeps(
|
|
|
198
227
|
},
|
|
199
228
|
stop: async () => {
|
|
200
229
|
await startPromise?.catch(() => undefined);
|
|
230
|
+
clearPublicAttributeRefreshTimer();
|
|
201
231
|
unsubscribeInbound?.();
|
|
202
232
|
unsubscribeInbound = undefined;
|
|
203
233
|
await router.stop();
|
package/src/prompt-config.ts
CHANGED
|
@@ -42,7 +42,7 @@ export const LIBP2P_MESH_AGENT_PROMPT = `
|
|
|
42
42
|
|
|
43
43
|
- \`source="USER.md"\` 表示 gateway 使用 OpenClaw 已配置的 agent/API 模型,从 USER.md 异步提取并公开广播的 tag。
|
|
44
44
|
- \`source="profile"\` 表示用户通过 \`openclaw libp2p-mesh profile\` 手动配置的公开结构化属性。
|
|
45
|
-
- gateway 的基础 \`instance-announce\` 可能省略 \`userPublicAttributes\`;这表示本次 announce
|
|
45
|
+
- gateway 的基础 \`instance-announce\` 可能省略 \`userPublicAttributes\`;这表示本次 announce 没有携带属性,不一定表示该用户没有公开属性。启动后 USER.md 属性刷新可能会延迟数秒完成。按公开属性发送前先用 \`p2p_list_instances\` 查看当前实例记录。
|
|
46
46
|
- 普通对话 agent 不应自己读取 USER.md 来决定公开属性。USER.md 属性提取是 gateway 后台职责。
|
|
47
47
|
|
|
48
48
|
3. \`openclaw libp2p-mesh labels\` manages local labels for remote instances. These labels are stored in the local \`peer-labels.json\`, stay on this machine, and are used only when the send tool uses \`scope="local"\` or \`scope="all"\`.
|
package/src/types.ts
CHANGED
|
@@ -244,6 +244,7 @@ export type InstanceRouterOptions = {
|
|
|
244
244
|
peerLabelStore?: {
|
|
245
245
|
listLabels(instanceId: string): Promise<LocalPeerLabelAttribute[]>;
|
|
246
246
|
};
|
|
247
|
+
publicAttributeRefreshInitiallyEnabled?: boolean;
|
|
247
248
|
};
|
|
248
249
|
|
|
249
250
|
export interface InstanceRouter {
|
|
@@ -253,6 +254,7 @@ export interface InstanceRouter {
|
|
|
253
254
|
stop(): Promise<void>;
|
|
254
255
|
handleMessage(msg: P2PMessage): Promise<void>;
|
|
255
256
|
announceToPeer(peerId: string): Promise<void>;
|
|
257
|
+
enablePublicAttributeRefresh(): void;
|
|
256
258
|
refreshPublicAttributes(): Promise<void>;
|
|
257
259
|
listInstances(): Promise<InstancePeerRecord[]>;
|
|
258
260
|
resolveInstance(instanceId: string): Promise<InstancePeerRecord | undefined>;
|
|
@@ -280,6 +282,7 @@ export interface MeshConfig {
|
|
|
280
282
|
bootstrapList?: string[];
|
|
281
283
|
meshTopic?: string;
|
|
282
284
|
announceLogDetail?: AnnounceLogDetail;
|
|
285
|
+
publicAttributeRefreshStartupDelayMs?: number;
|
|
283
286
|
enableAgentSync?: boolean;
|
|
284
287
|
enableWebSocket?: boolean;
|
|
285
288
|
peerIdPath?: string;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/core";
|
|
3
2
|
import type { UserPublicAttribute } from "./types.js";
|
|
4
3
|
import type {
|
|
@@ -8,7 +7,6 @@ import type {
|
|
|
8
7
|
import { validateExtractedUserMdTags } from "./user-md-agent-attributes.js";
|
|
9
8
|
|
|
10
9
|
const EXTRACTION_TIMEOUT_MS = 30000;
|
|
11
|
-
const SESSION_HASH_LENGTH = 16;
|
|
12
10
|
|
|
13
11
|
export const USER_MD_ATTRIBUTE_EXTRACTION_PROMPT = [
|
|
14
12
|
"你是 libp2p-mesh 的 USER.md 公开属性提取器。",
|
|
@@ -29,9 +27,15 @@ type Logger = {
|
|
|
29
27
|
warn?: (message: string) => void;
|
|
30
28
|
};
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
30
|
+
type RuntimeLlmComplete = {
|
|
31
|
+
complete(request: {
|
|
32
|
+
messages: Array<{ role: "system" | "user" | "assistant"; content: string }>;
|
|
33
|
+
purpose: string;
|
|
34
|
+
maxTokens?: number;
|
|
35
|
+
temperature?: number;
|
|
36
|
+
timeoutMs?: number;
|
|
37
|
+
}): Promise<unknown>;
|
|
38
|
+
};
|
|
35
39
|
|
|
36
40
|
function buildUserMdExtractionMessage(markdown: string, sourcePath: string): string {
|
|
37
41
|
return [
|
|
@@ -99,6 +103,48 @@ export function extractLatestAssistantText(messages: unknown[]): string | undefi
|
|
|
99
103
|
return undefined;
|
|
100
104
|
}
|
|
101
105
|
|
|
106
|
+
export function extractCompletionText(result: unknown): string | undefined {
|
|
107
|
+
if (typeof result === "string") {
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
if (!isRecord(result)) {
|
|
111
|
+
return undefined;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const direct =
|
|
115
|
+
extractTextFromContent(result.content) ??
|
|
116
|
+
(typeof result.text === "string" ? result.text : undefined) ??
|
|
117
|
+
(typeof result.outputText === "string" ? result.outputText : undefined) ??
|
|
118
|
+
(typeof result.message === "string" ? result.message : undefined);
|
|
119
|
+
if (direct?.trim()) {
|
|
120
|
+
return direct;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const choices = result.choices;
|
|
124
|
+
if (!Array.isArray(choices)) {
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
for (const choice of choices) {
|
|
129
|
+
if (!isRecord(choice)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const message = choice.message;
|
|
133
|
+
if (isRecord(message)) {
|
|
134
|
+
const text = extractTextFromContent(message.content);
|
|
135
|
+
if (text?.trim()) {
|
|
136
|
+
return text;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const text = extractTextFromContent(choice.content);
|
|
140
|
+
if (text?.trim()) {
|
|
141
|
+
return text;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
|
|
102
148
|
function stripJsonFence(text: string): string {
|
|
103
149
|
const trimmed = text.trim();
|
|
104
150
|
const fenced = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
@@ -117,6 +163,18 @@ function unavailable(reason: string): UserMdAttributeExtractionUnavailable {
|
|
|
117
163
|
return { unavailable: true, reason };
|
|
118
164
|
}
|
|
119
165
|
|
|
166
|
+
function runtimeLlm(api: OpenClawPluginApi): RuntimeLlmComplete | undefined {
|
|
167
|
+
const runtime = api.runtime as unknown;
|
|
168
|
+
if (!isRecord(runtime)) {
|
|
169
|
+
return undefined;
|
|
170
|
+
}
|
|
171
|
+
const llm = runtime.llm;
|
|
172
|
+
if (!isRecord(llm) || typeof llm.complete !== "function") {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return llm as RuntimeLlmComplete;
|
|
176
|
+
}
|
|
177
|
+
|
|
120
178
|
export function createOpenClawUserMdAttributeExtractor(
|
|
121
179
|
api: OpenClawPluginApi,
|
|
122
180
|
options?: {
|
|
@@ -129,31 +187,23 @@ export function createOpenClawUserMdAttributeExtractor(
|
|
|
129
187
|
|
|
130
188
|
return {
|
|
131
189
|
async extract({ markdown, sourcePath }) {
|
|
132
|
-
const
|
|
133
|
-
if (!
|
|
134
|
-
return unavailable("OpenClaw
|
|
190
|
+
const llm = runtimeLlm(api);
|
|
191
|
+
if (!llm) {
|
|
192
|
+
return unavailable("OpenClaw runtime llm.complete is unavailable");
|
|
135
193
|
}
|
|
136
194
|
|
|
137
|
-
const sessionKey = `libp2p-mesh:user-md-attributes:${hashForSessionKey(sourcePath)}`;
|
|
138
|
-
|
|
139
195
|
try {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
196
|
+
const result = await llm.complete({
|
|
197
|
+
messages: [
|
|
198
|
+
{ role: "system", content: USER_MD_ATTRIBUTE_EXTRACTION_PROMPT },
|
|
199
|
+
{ role: "user", content: buildUserMdExtractionMessage(markdown, sourcePath) },
|
|
200
|
+
],
|
|
201
|
+
purpose: "libp2p-mesh.user-md-attributes",
|
|
202
|
+
maxTokens: 512,
|
|
203
|
+
temperature: 0.1,
|
|
204
|
+
timeoutMs,
|
|
148
205
|
});
|
|
149
|
-
|
|
150
|
-
const waitResult = await subagent.waitForRun({ runId, timeoutMs });
|
|
151
|
-
if (waitResult.status !== "ok") {
|
|
152
|
-
return unavailable(waitResult.error ?? `OpenClaw subagent extraction ${waitResult.status}`);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const { messages } = await subagent.getSessionMessages({ sessionKey, limit: 10 });
|
|
156
|
-
const text = extractLatestAssistantText(messages);
|
|
206
|
+
const text = extractCompletionText(result);
|
|
157
207
|
if (!text) {
|
|
158
208
|
return [];
|
|
159
209
|
}
|
|
@@ -163,8 +213,6 @@ export function createOpenClawUserMdAttributeExtractor(
|
|
|
163
213
|
const reason = error instanceof Error ? error.message : String(error);
|
|
164
214
|
logger?.warn?.(`[libp2p-mesh] USER.md agent extraction failed: ${reason}`);
|
|
165
215
|
return unavailable(reason);
|
|
166
|
-
} finally {
|
|
167
|
-
await subagent.deleteSession?.({ sessionKey, deleteTranscript: true }).catch(() => undefined);
|
|
168
216
|
}
|
|
169
217
|
},
|
|
170
218
|
};
|