@bamdra/bamdra-user-bind 0.1.13 → 0.1.14
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 +3 -0
- package/README.zh-CN.md +3 -0
- package/dist/index.js +938 -321
- package/dist/openclaw.plugin.json +1 -1
- package/dist/package.json +1 -1
- package/package.json +1 -1
- package/skills/bamdra-user-bind-admin/SKILL.md +4 -3
- package/skills/bamdra-user-bind-profile/SKILL.md +30 -5
package/dist/index.js
CHANGED
|
@@ -39,6 +39,9 @@ var import_node_sqlite = require("node:sqlite");
|
|
|
39
39
|
var import_node_url = require("url");
|
|
40
40
|
var PLUGIN_ID = "bamdra-user-bind";
|
|
41
41
|
var GLOBAL_API_KEY = "__OPENCLAW_BAMDRA_USER_BIND__";
|
|
42
|
+
var GLOBAL_RUNTIME_KEY = "__OPENCLAW_BAMDRA_USER_BIND_RUNTIME__";
|
|
43
|
+
var GLOBAL_RUNTIME_BRAND_KEY = "__OPENCLAW_BAMDRA_USER_BIND_RUNTIME_BRAND__";
|
|
44
|
+
var GLOBAL_PENDING_REFINE_KEY = "__OPENCLAW_BAMDRA_USER_BIND_PENDING_REFINE__";
|
|
42
45
|
var PROFILE_SKILL_ID = "bamdra-user-bind-profile";
|
|
43
46
|
var ADMIN_SKILL_ID = "bamdra-user-bind-admin";
|
|
44
47
|
var SELF_TOOL_NAMES = [
|
|
@@ -64,10 +67,11 @@ var TABLES = {
|
|
|
64
67
|
issues: "bamdra_user_bind_issues",
|
|
65
68
|
audits: "bamdra_user_bind_audits"
|
|
66
69
|
};
|
|
67
|
-
var
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
var SEMANTIC_PROFILE_CAPTURE_TIMEOUT_MS = readSemanticProfileCaptureTimeoutMs();
|
|
71
|
+
var SEMANTIC_PROFILE_BATCH_WINDOW_MS = 240;
|
|
72
|
+
var SEMANTIC_PROFILE_BATCH_MAX_FRAGMENTS = 4;
|
|
73
|
+
var SEMANTIC_PROFILE_BATCH_MAX_CHARS = 360;
|
|
74
|
+
var SEMANTIC_PROFILE_RETRY_MAX_ATTEMPTS = 3;
|
|
71
75
|
var CHANNELS_WITH_NATIVE_STABLE_IDS = /* @__PURE__ */ new Set([
|
|
72
76
|
"telegram",
|
|
73
77
|
"whatsapp",
|
|
@@ -100,11 +104,15 @@ var UserBindStore = class {
|
|
|
100
104
|
user_id TEXT PRIMARY KEY,
|
|
101
105
|
name TEXT,
|
|
102
106
|
gender TEXT,
|
|
107
|
+
birth_date TEXT,
|
|
108
|
+
birth_year TEXT,
|
|
109
|
+
age TEXT,
|
|
103
110
|
email TEXT,
|
|
104
111
|
avatar TEXT,
|
|
105
112
|
nickname TEXT,
|
|
106
113
|
preferences TEXT,
|
|
107
114
|
personality TEXT,
|
|
115
|
+
interests TEXT,
|
|
108
116
|
role TEXT,
|
|
109
117
|
timezone TEXT,
|
|
110
118
|
notes TEXT,
|
|
@@ -146,6 +154,10 @@ var UserBindStore = class {
|
|
|
146
154
|
`);
|
|
147
155
|
this.ensureColumn(TABLES.profiles, "timezone", "TEXT");
|
|
148
156
|
this.ensureColumn(TABLES.profiles, "notes", "TEXT");
|
|
157
|
+
this.ensureColumn(TABLES.profiles, "birth_date", "TEXT");
|
|
158
|
+
this.ensureColumn(TABLES.profiles, "birth_year", "TEXT");
|
|
159
|
+
this.ensureColumn(TABLES.profiles, "age", "TEXT");
|
|
160
|
+
this.ensureColumn(TABLES.profiles, "interests", "TEXT");
|
|
149
161
|
this.migrateChannelScopedUserIds();
|
|
150
162
|
}
|
|
151
163
|
db;
|
|
@@ -157,6 +169,16 @@ var UserBindStore = class {
|
|
|
157
169
|
this.syncMarkdownToStore(userId);
|
|
158
170
|
return this.getProfileFromDatabase(userId);
|
|
159
171
|
}
|
|
172
|
+
listProfilesWithPendingSemanticRefine(limit = 20) {
|
|
173
|
+
const rows = this.db.prepare(`
|
|
174
|
+
SELECT user_id, name, gender, birth_date, birth_year, age, email, avatar, nickname, preferences, personality, interests, role, timezone, notes, visibility, source, updated_at
|
|
175
|
+
FROM ${TABLES.profiles}
|
|
176
|
+
WHERE notes LIKE '%[pending-profile-refine:%'
|
|
177
|
+
ORDER BY updated_at DESC
|
|
178
|
+
LIMIT ?
|
|
179
|
+
`).all(limit);
|
|
180
|
+
return rows.map(mapProfileRow);
|
|
181
|
+
}
|
|
160
182
|
findBinding(channelType, openId) {
|
|
161
183
|
if (!openId) {
|
|
162
184
|
return null;
|
|
@@ -231,11 +253,15 @@ var UserBindStore = class {
|
|
|
231
253
|
userId: scopedUserId,
|
|
232
254
|
name: args.profilePatch.name ?? current?.name ?? null,
|
|
233
255
|
gender: args.profilePatch.gender ?? current?.gender ?? null,
|
|
256
|
+
birthDate: args.profilePatch.birthDate ?? current?.birthDate ?? null,
|
|
257
|
+
birthYear: args.profilePatch.birthYear ?? current?.birthYear ?? null,
|
|
258
|
+
age: args.profilePatch.age ?? current?.age ?? null,
|
|
234
259
|
email: args.profilePatch.email ?? current?.email ?? null,
|
|
235
260
|
avatar: args.profilePatch.avatar ?? current?.avatar ?? null,
|
|
236
261
|
nickname: args.profilePatch.nickname ?? current?.nickname ?? null,
|
|
237
262
|
preferences: args.profilePatch.preferences ?? current?.preferences ?? null,
|
|
238
263
|
personality: args.profilePatch.personality ?? current?.personality ?? null,
|
|
264
|
+
interests: args.profilePatch.interests ?? current?.interests ?? null,
|
|
239
265
|
role: args.profilePatch.role ?? current?.role ?? null,
|
|
240
266
|
timezone: args.profilePatch.timezone ?? current?.timezone ?? getServerTimezone(),
|
|
241
267
|
notes: args.profilePatch.notes ?? current?.notes ?? defaultProfileNotes(),
|
|
@@ -244,16 +270,20 @@ var UserBindStore = class {
|
|
|
244
270
|
updatedAt: now
|
|
245
271
|
};
|
|
246
272
|
this.db.prepare(`
|
|
247
|
-
INSERT INTO ${TABLES.profiles} (user_id, name, gender, email, avatar, nickname, preferences, personality, role, timezone, notes, visibility, source, updated_at)
|
|
248
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
273
|
+
INSERT INTO ${TABLES.profiles} (user_id, name, gender, birth_date, birth_year, age, email, avatar, nickname, preferences, personality, interests, role, timezone, notes, visibility, source, updated_at)
|
|
274
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
249
275
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
250
276
|
name = excluded.name,
|
|
251
277
|
gender = excluded.gender,
|
|
278
|
+
birth_date = excluded.birth_date,
|
|
279
|
+
birth_year = excluded.birth_year,
|
|
280
|
+
age = excluded.age,
|
|
252
281
|
email = excluded.email,
|
|
253
282
|
avatar = excluded.avatar,
|
|
254
283
|
nickname = excluded.nickname,
|
|
255
284
|
preferences = excluded.preferences,
|
|
256
285
|
personality = excluded.personality,
|
|
286
|
+
interests = excluded.interests,
|
|
257
287
|
role = excluded.role,
|
|
258
288
|
timezone = excluded.timezone,
|
|
259
289
|
notes = excluded.notes,
|
|
@@ -264,11 +294,15 @@ var UserBindStore = class {
|
|
|
264
294
|
next.userId,
|
|
265
295
|
next.name,
|
|
266
296
|
next.gender,
|
|
297
|
+
next.birthDate,
|
|
298
|
+
next.birthYear,
|
|
299
|
+
next.age,
|
|
267
300
|
next.email,
|
|
268
301
|
next.avatar,
|
|
269
302
|
next.nickname,
|
|
270
303
|
next.preferences,
|
|
271
304
|
next.personality,
|
|
305
|
+
next.interests,
|
|
272
306
|
next.role,
|
|
273
307
|
next.timezone,
|
|
274
308
|
next.notes,
|
|
@@ -318,6 +352,26 @@ var UserBindStore = class {
|
|
|
318
352
|
}
|
|
319
353
|
});
|
|
320
354
|
}
|
|
355
|
+
replaceProfileNotes(userId, notes, source) {
|
|
356
|
+
const current = this.getProfile(userId);
|
|
357
|
+
if (!current) {
|
|
358
|
+
throw new Error(`Unknown user ${userId}`);
|
|
359
|
+
}
|
|
360
|
+
const next = {
|
|
361
|
+
...current,
|
|
362
|
+
notes,
|
|
363
|
+
source,
|
|
364
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
365
|
+
};
|
|
366
|
+
this.db.prepare(`
|
|
367
|
+
UPDATE ${TABLES.profiles}
|
|
368
|
+
SET notes = ?, source = ?, updated_at = ?
|
|
369
|
+
WHERE user_id = ?
|
|
370
|
+
`).run(next.notes, next.source, next.updatedAt, current.userId);
|
|
371
|
+
this.writeProfileMarkdown(next);
|
|
372
|
+
this.writeExports();
|
|
373
|
+
return next;
|
|
374
|
+
}
|
|
321
375
|
mergeUsers(fromUserId, intoUserId) {
|
|
322
376
|
const from = this.getProfile(fromUserId);
|
|
323
377
|
const into = this.getProfile(intoUserId);
|
|
@@ -332,11 +386,15 @@ var UserBindStore = class {
|
|
|
332
386
|
profilePatch: {
|
|
333
387
|
name: into.name ?? from.name,
|
|
334
388
|
gender: into.gender ?? from.gender,
|
|
389
|
+
birthDate: into.birthDate ?? from.birthDate,
|
|
390
|
+
birthYear: into.birthYear ?? from.birthYear,
|
|
391
|
+
age: into.age ?? from.age,
|
|
335
392
|
email: into.email ?? from.email,
|
|
336
393
|
avatar: into.avatar ?? from.avatar,
|
|
337
394
|
nickname: into.nickname ?? from.nickname,
|
|
338
395
|
preferences: into.preferences ?? from.preferences,
|
|
339
396
|
personality: into.personality ?? from.personality,
|
|
397
|
+
interests: into.interests ?? from.interests,
|
|
340
398
|
role: into.role ?? from.role,
|
|
341
399
|
timezone: into.timezone ?? from.timezone,
|
|
342
400
|
notes: joinNotes(into.notes, from.notes),
|
|
@@ -354,7 +412,7 @@ var UserBindStore = class {
|
|
|
354
412
|
}
|
|
355
413
|
getProfileFromDatabase(userId) {
|
|
356
414
|
const row = this.db.prepare(`
|
|
357
|
-
SELECT user_id, name, gender, email, avatar, nickname, preferences, personality, role, timezone, notes, visibility, source, updated_at
|
|
415
|
+
SELECT user_id, name, gender, birth_date, birth_year, age, email, avatar, nickname, preferences, personality, interests, role, timezone, notes, visibility, source, updated_at
|
|
358
416
|
FROM ${TABLES.profiles} WHERE user_id = ?
|
|
359
417
|
`).get(userId);
|
|
360
418
|
return row ? mapProfileRow(row) : null;
|
|
@@ -398,19 +456,29 @@ var UserBindStore = class {
|
|
|
398
456
|
const markdownMtime = (0, import_node_fs.statSync)(markdownPath).mtime.toISOString();
|
|
399
457
|
const markdownHash = computeProfilePayloadHash({
|
|
400
458
|
name: patch.name ?? null,
|
|
459
|
+
gender: patch.gender ?? null,
|
|
460
|
+
birthDate: patch.birthDate ?? null,
|
|
461
|
+
birthYear: patch.birthYear ?? null,
|
|
462
|
+
age: patch.age ?? null,
|
|
401
463
|
nickname: patch.nickname ?? null,
|
|
402
464
|
timezone: patch.timezone ?? null,
|
|
403
465
|
preferences: patch.preferences ?? null,
|
|
404
466
|
personality: patch.personality ?? null,
|
|
467
|
+
interests: patch.interests ?? null,
|
|
405
468
|
role: patch.role ?? null,
|
|
406
469
|
visibility: patch.visibility ?? current.visibility
|
|
407
470
|
}, patch.notes ?? null);
|
|
408
471
|
const currentHash = computeProfilePayloadHash({
|
|
409
472
|
name: current.name,
|
|
473
|
+
gender: current.gender,
|
|
474
|
+
birthDate: current.birthDate,
|
|
475
|
+
birthYear: current.birthYear,
|
|
476
|
+
age: current.age,
|
|
410
477
|
nickname: current.nickname,
|
|
411
478
|
timezone: current.timezone,
|
|
412
479
|
preferences: current.preferences,
|
|
413
480
|
personality: current.personality,
|
|
481
|
+
interests: current.interests,
|
|
414
482
|
role: current.role,
|
|
415
483
|
visibility: current.visibility
|
|
416
484
|
}, current.notes);
|
|
@@ -448,7 +516,7 @@ var UserBindStore = class {
|
|
|
448
516
|
}
|
|
449
517
|
writeExports() {
|
|
450
518
|
const profiles = this.db.prepare(`
|
|
451
|
-
SELECT user_id, name, gender, email, avatar, nickname, preferences, personality, role, timezone, notes, visibility, source, updated_at
|
|
519
|
+
SELECT user_id, name, gender, birth_date, birth_year, age, email, avatar, nickname, preferences, personality, interests, role, timezone, notes, visibility, source, updated_at
|
|
452
520
|
FROM ${TABLES.profiles}
|
|
453
521
|
ORDER BY updated_at DESC
|
|
454
522
|
`).all();
|
|
@@ -509,16 +577,20 @@ var UserBindStore = class {
|
|
|
509
577
|
};
|
|
510
578
|
this.db.prepare(`DELETE FROM ${TABLES.profiles} WHERE user_id = ?`).run(fromUserId);
|
|
511
579
|
this.db.prepare(`
|
|
512
|
-
INSERT INTO ${TABLES.profiles} (user_id, name, gender, email, avatar, nickname, preferences, personality, role, timezone, notes, visibility, source, updated_at)
|
|
513
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
580
|
+
INSERT INTO ${TABLES.profiles} (user_id, name, gender, birth_date, birth_year, age, email, avatar, nickname, preferences, personality, interests, role, timezone, notes, visibility, source, updated_at)
|
|
581
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
514
582
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
515
583
|
name = excluded.name,
|
|
516
584
|
gender = excluded.gender,
|
|
585
|
+
birth_date = excluded.birth_date,
|
|
586
|
+
birth_year = excluded.birth_year,
|
|
587
|
+
age = excluded.age,
|
|
517
588
|
email = excluded.email,
|
|
518
589
|
avatar = excluded.avatar,
|
|
519
590
|
nickname = excluded.nickname,
|
|
520
591
|
preferences = excluded.preferences,
|
|
521
592
|
personality = excluded.personality,
|
|
593
|
+
interests = excluded.interests,
|
|
522
594
|
role = excluded.role,
|
|
523
595
|
timezone = excluded.timezone,
|
|
524
596
|
notes = excluded.notes,
|
|
@@ -529,11 +601,15 @@ var UserBindStore = class {
|
|
|
529
601
|
merged.userId,
|
|
530
602
|
merged.name,
|
|
531
603
|
merged.gender,
|
|
604
|
+
merged.birthDate,
|
|
605
|
+
merged.birthYear,
|
|
606
|
+
merged.age,
|
|
532
607
|
merged.email,
|
|
533
608
|
merged.avatar,
|
|
534
609
|
merged.nickname,
|
|
535
610
|
merged.preferences,
|
|
536
611
|
merged.personality,
|
|
612
|
+
merged.interests,
|
|
537
613
|
merged.role,
|
|
538
614
|
merged.timezone,
|
|
539
615
|
merged.notes,
|
|
@@ -572,13 +648,18 @@ var UserBindRuntime = class {
|
|
|
572
648
|
bindingCache = /* @__PURE__ */ new Map();
|
|
573
649
|
semanticCaptureCache = /* @__PURE__ */ new Map();
|
|
574
650
|
semanticCaptureInFlight = /* @__PURE__ */ new Set();
|
|
651
|
+
semanticCaptureRetryTimers = /* @__PURE__ */ new Map();
|
|
652
|
+
semanticCaptureRetryAttempts = /* @__PURE__ */ new Map();
|
|
653
|
+
pendingSemanticCaptures = /* @__PURE__ */ new Map();
|
|
575
654
|
pendingBindingResolutions = /* @__PURE__ */ new Map();
|
|
576
|
-
feishuScopeStatusCache = /* @__PURE__ */ new Map();
|
|
577
|
-
bitableMirror = null;
|
|
578
655
|
feishuTokenCache = /* @__PURE__ */ new Map();
|
|
656
|
+
globalPendingSemanticRefines = getGlobalPendingSemanticRefines();
|
|
579
657
|
pendingBindingTimer = null;
|
|
580
658
|
pendingBindingKickTimer = null;
|
|
659
|
+
pendingSemanticSweepTimer = null;
|
|
581
660
|
pendingBindingSweepInFlight = false;
|
|
661
|
+
pendingSemanticSweepInFlight = false;
|
|
662
|
+
registered = false;
|
|
582
663
|
close() {
|
|
583
664
|
if (this.pendingBindingTimer) {
|
|
584
665
|
clearInterval(this.pendingBindingTimer);
|
|
@@ -588,9 +669,28 @@ var UserBindRuntime = class {
|
|
|
588
669
|
clearTimeout(this.pendingBindingKickTimer);
|
|
589
670
|
this.pendingBindingKickTimer = null;
|
|
590
671
|
}
|
|
672
|
+
if (this.pendingSemanticSweepTimer) {
|
|
673
|
+
clearTimeout(this.pendingSemanticSweepTimer);
|
|
674
|
+
this.pendingSemanticSweepTimer = null;
|
|
675
|
+
}
|
|
676
|
+
for (const pending of this.pendingSemanticCaptures.values()) {
|
|
677
|
+
if (pending.timer) {
|
|
678
|
+
clearTimeout(pending.timer);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
this.pendingSemanticCaptures.clear();
|
|
682
|
+
for (const timer of this.semanticCaptureRetryTimers.values()) {
|
|
683
|
+
clearTimeout(timer);
|
|
684
|
+
}
|
|
685
|
+
this.semanticCaptureRetryTimers.clear();
|
|
686
|
+
this.semanticCaptureRetryAttempts.clear();
|
|
591
687
|
this.store.close();
|
|
592
688
|
}
|
|
593
689
|
register() {
|
|
690
|
+
if (this.registered) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
this.registered = true;
|
|
594
694
|
queueMicrotask(() => {
|
|
595
695
|
try {
|
|
596
696
|
bootstrapOpenClawHost(this.config);
|
|
@@ -600,12 +700,13 @@ var UserBindRuntime = class {
|
|
|
600
700
|
this.registerHooks();
|
|
601
701
|
this.registerTools();
|
|
602
702
|
this.startPendingBindingWorker();
|
|
703
|
+
this.schedulePendingSemanticSweep();
|
|
603
704
|
exposeGlobalApi(this);
|
|
604
705
|
}
|
|
605
706
|
getIdentityForSession(sessionId) {
|
|
606
707
|
return this.sessionCache.get(sessionId) ?? null;
|
|
607
708
|
}
|
|
608
|
-
async resolveFromContext(context) {
|
|
709
|
+
async resolveFromContext(context, options = {}) {
|
|
609
710
|
const parsed = parseIdentityContext(enrichIdentityContext(context));
|
|
610
711
|
if (parsed.sessionId && !parsed.channelType) {
|
|
611
712
|
const cached2 = this.sessionCache.get(parsed.sessionId) ?? null;
|
|
@@ -643,18 +744,22 @@ var UserBindRuntime = class {
|
|
|
643
744
|
let remoteProfilePatch = {};
|
|
644
745
|
if (parsed.channelType === "feishu" && parsed.openId) {
|
|
645
746
|
if (!userId) {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
747
|
+
if (options.allowRemoteLookup) {
|
|
748
|
+
const remote = await this.tryResolveFeishuUser(parsed.openId);
|
|
749
|
+
if (remote?.userId) {
|
|
750
|
+
userId = remote.userId;
|
|
751
|
+
source = remote.source;
|
|
752
|
+
remoteProfilePatch = remote.profilePatch;
|
|
753
|
+
} else if (remote?.source === "feishu-contact-scope-missing") {
|
|
754
|
+
source = remote.source;
|
|
755
|
+
remoteProfilePatch = remote.profilePatch;
|
|
756
|
+
this.enqueuePendingBindingResolution(parsed.channelType, parsed.openId, remote.source);
|
|
757
|
+
} else {
|
|
758
|
+
this.store.recordIssue("feishu-resolution", `Failed to resolve real user id for ${parsed.openId}`);
|
|
759
|
+
this.enqueuePendingBindingResolution(parsed.channelType, parsed.openId, "feishu-resolution-miss");
|
|
760
|
+
}
|
|
655
761
|
} else {
|
|
656
|
-
this.
|
|
657
|
-
this.enqueuePendingBindingResolution(parsed.channelType, parsed.openId, "feishu-resolution-miss");
|
|
762
|
+
this.enqueuePendingBindingResolution(parsed.channelType, parsed.openId, "feishu-deferred-resolution");
|
|
658
763
|
}
|
|
659
764
|
}
|
|
660
765
|
}
|
|
@@ -720,9 +825,6 @@ var UserBindRuntime = class {
|
|
|
720
825
|
expiresAt: Date.now() + this.config.cacheTtlMs,
|
|
721
826
|
identity
|
|
722
827
|
});
|
|
723
|
-
if (shouldPersistIdentity && parsed.channelType === "feishu") {
|
|
724
|
-
await this.syncFeishuMirror(identity);
|
|
725
|
-
}
|
|
726
828
|
return identity;
|
|
727
829
|
}
|
|
728
830
|
startPendingBindingWorker() {
|
|
@@ -877,22 +979,79 @@ var UserBindRuntime = class {
|
|
|
877
979
|
sessionId: identity.sessionId,
|
|
878
980
|
sender: { id: identity.senderOpenId, name: identity.senderName },
|
|
879
981
|
channel: { type: identity.channelType }
|
|
880
|
-
});
|
|
982
|
+
}, { allowRemoteLookup: true });
|
|
881
983
|
if (!refreshed) {
|
|
882
984
|
throw new Error("Unable to refresh binding for current session");
|
|
883
985
|
}
|
|
884
986
|
return refreshed;
|
|
885
987
|
}
|
|
886
988
|
async captureProfileFromMessage(context, identity) {
|
|
887
|
-
const
|
|
888
|
-
if (!
|
|
989
|
+
const utteranceText = extractUserUtterance(context);
|
|
990
|
+
if (!utteranceText) {
|
|
889
991
|
logUserBindEvent("semantic-profile-capture-skipped-no-utterance", {
|
|
890
992
|
userId: identity.userId,
|
|
891
993
|
sessionId: identity.sessionId
|
|
892
994
|
});
|
|
893
995
|
return;
|
|
894
996
|
}
|
|
895
|
-
const
|
|
997
|
+
const messageText = buildSemanticCaptureInput(context, utteranceText);
|
|
998
|
+
if (!messageText) {
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
if (shouldIgnoreSemanticProfileCaptureCandidate(utteranceText) && messageText === utteranceText) {
|
|
1002
|
+
logUserBindEvent("semantic-profile-capture-skipped-trivial-utterance", {
|
|
1003
|
+
userId: identity.userId,
|
|
1004
|
+
sessionId: identity.sessionId,
|
|
1005
|
+
messagePreview: utteranceText.slice(0, 120)
|
|
1006
|
+
});
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
this.enqueueSemanticProfileCapture(identity, messageText);
|
|
1010
|
+
}
|
|
1011
|
+
enqueueSemanticProfileCapture(identity, messageText) {
|
|
1012
|
+
const sessionId = identity.sessionId;
|
|
1013
|
+
const pending = this.pendingSemanticCaptures.get(sessionId) ?? {
|
|
1014
|
+
identity,
|
|
1015
|
+
messages: [],
|
|
1016
|
+
timer: null
|
|
1017
|
+
};
|
|
1018
|
+
pending.identity = identity;
|
|
1019
|
+
pending.messages = appendSemanticCaptureCandidate(pending.messages, messageText);
|
|
1020
|
+
if (pending.timer) {
|
|
1021
|
+
clearTimeout(pending.timer);
|
|
1022
|
+
}
|
|
1023
|
+
pending.timer = setTimeout(() => {
|
|
1024
|
+
pending.timer = null;
|
|
1025
|
+
void this.flushSemanticProfileCapture(sessionId).catch((error) => {
|
|
1026
|
+
logUserBindEvent("semantic-profile-capture-flush-failed", {
|
|
1027
|
+
userId: identity.userId,
|
|
1028
|
+
sessionId,
|
|
1029
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1030
|
+
});
|
|
1031
|
+
});
|
|
1032
|
+
}, SEMANTIC_PROFILE_BATCH_WINDOW_MS);
|
|
1033
|
+
this.pendingSemanticCaptures.set(sessionId, pending);
|
|
1034
|
+
}
|
|
1035
|
+
async flushSemanticProfileCapture(sessionId) {
|
|
1036
|
+
const pending = this.pendingSemanticCaptures.get(sessionId);
|
|
1037
|
+
if (!pending) {
|
|
1038
|
+
return;
|
|
1039
|
+
}
|
|
1040
|
+
this.pendingSemanticCaptures.delete(sessionId);
|
|
1041
|
+
const identity = pending.identity;
|
|
1042
|
+
const messageText = buildSemanticProfileBatchText(pending.messages);
|
|
1043
|
+
if (!messageText) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (shouldSkipSemanticProfileCapture(messageText)) {
|
|
1047
|
+
logUserBindEvent("semantic-profile-capture-skipped-insufficient-signal", {
|
|
1048
|
+
userId: identity.userId,
|
|
1049
|
+
sessionId,
|
|
1050
|
+
messagePreview: messageText.slice(0, 120)
|
|
1051
|
+
});
|
|
1052
|
+
return;
|
|
1053
|
+
}
|
|
1054
|
+
const fingerprint = hashId(`${sessionId}:${messageText}`);
|
|
896
1055
|
this.pruneSemanticCaptureCache();
|
|
897
1056
|
if (this.semanticCaptureInFlight.has(fingerprint)) {
|
|
898
1057
|
return;
|
|
@@ -901,17 +1060,21 @@ var UserBindRuntime = class {
|
|
|
901
1060
|
if (processedAt && processedAt > Date.now() - 12 * 60 * 60 * 1e3) {
|
|
902
1061
|
return;
|
|
903
1062
|
}
|
|
1063
|
+
await this.runSemanticProfileCapture(identity, sessionId, messageText, fingerprint);
|
|
1064
|
+
}
|
|
1065
|
+
async runSemanticProfileCapture(identity, sessionId, messageText, fingerprint) {
|
|
904
1066
|
this.semanticCaptureInFlight.add(fingerprint);
|
|
905
1067
|
try {
|
|
906
1068
|
const extraction = await inferSemanticProfileExtraction(messageText, identity.profile);
|
|
907
1069
|
if (!extraction?.shouldUpdate) {
|
|
908
1070
|
logUserBindEvent("semantic-profile-capture-noop", {
|
|
909
1071
|
userId: identity.userId,
|
|
910
|
-
sessionId
|
|
1072
|
+
sessionId,
|
|
911
1073
|
confidence: extraction?.confidence ?? 0,
|
|
912
1074
|
messagePreview: messageText.slice(0, 120)
|
|
913
1075
|
});
|
|
914
1076
|
this.semanticCaptureCache.set(fingerprint, Date.now());
|
|
1077
|
+
this.clearSemanticProfileRetryState(fingerprint);
|
|
915
1078
|
return;
|
|
916
1079
|
}
|
|
917
1080
|
const { patch, operations } = cleanupSemanticProfilePatch(
|
|
@@ -921,34 +1084,253 @@ var UserBindRuntime = class {
|
|
|
921
1084
|
);
|
|
922
1085
|
if (Object.keys(patch).length === 0) {
|
|
923
1086
|
this.semanticCaptureCache.set(fingerprint, Date.now());
|
|
1087
|
+
this.clearSemanticProfileRetryState(fingerprint);
|
|
924
1088
|
return;
|
|
925
1089
|
}
|
|
926
1090
|
await this.updateMyProfile(
|
|
927
|
-
{ sessionId
|
|
1091
|
+
{ sessionId },
|
|
928
1092
|
{
|
|
929
1093
|
...patch,
|
|
930
1094
|
source: "semantic-self-update"
|
|
931
1095
|
},
|
|
932
1096
|
operations
|
|
933
1097
|
);
|
|
1098
|
+
await this.removePendingSemanticRefineNote(sessionId, fingerprint);
|
|
934
1099
|
logUserBindEvent("semantic-profile-capture-success", {
|
|
935
1100
|
userId: identity.userId,
|
|
936
|
-
sessionId
|
|
1101
|
+
sessionId,
|
|
937
1102
|
fields: Object.keys(patch),
|
|
938
1103
|
operations,
|
|
939
1104
|
confidence: extraction.confidence
|
|
940
1105
|
});
|
|
941
1106
|
this.semanticCaptureCache.set(fingerprint, Date.now());
|
|
1107
|
+
this.clearSemanticProfileRetryState(fingerprint);
|
|
942
1108
|
} catch (error) {
|
|
1109
|
+
if (this.shouldRetrySemanticProfileCapture(error)) {
|
|
1110
|
+
await this.ensurePendingSemanticRefineNote(sessionId, messageText, fingerprint);
|
|
1111
|
+
this.scheduleSemanticProfileRetry(identity, sessionId, messageText, fingerprint, error);
|
|
1112
|
+
return;
|
|
1113
|
+
}
|
|
943
1114
|
logUserBindEvent("semantic-profile-capture-failed", {
|
|
944
1115
|
userId: identity.userId,
|
|
945
|
-
sessionId
|
|
1116
|
+
sessionId,
|
|
946
1117
|
message: error instanceof Error ? error.message : String(error)
|
|
947
1118
|
});
|
|
948
1119
|
} finally {
|
|
949
1120
|
this.semanticCaptureInFlight.delete(fingerprint);
|
|
950
1121
|
}
|
|
951
1122
|
}
|
|
1123
|
+
scheduleSemanticProfileRetry(identity, sessionId, messageText, fingerprint, error) {
|
|
1124
|
+
const nextAttempt = (this.semanticCaptureRetryAttempts.get(fingerprint) ?? 0) + 1;
|
|
1125
|
+
if (nextAttempt > SEMANTIC_PROFILE_RETRY_MAX_ATTEMPTS) {
|
|
1126
|
+
logUserBindEvent("semantic-profile-capture-failed", {
|
|
1127
|
+
userId: identity.userId,
|
|
1128
|
+
sessionId,
|
|
1129
|
+
attempt: nextAttempt - 1,
|
|
1130
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1131
|
+
});
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
const existing = this.semanticCaptureRetryTimers.get(fingerprint);
|
|
1135
|
+
if (existing) {
|
|
1136
|
+
clearTimeout(existing);
|
|
1137
|
+
}
|
|
1138
|
+
this.semanticCaptureRetryAttempts.set(fingerprint, nextAttempt);
|
|
1139
|
+
const delayMs = computeSemanticProfileRetryDelayMs(nextAttempt);
|
|
1140
|
+
const timer = setTimeout(() => {
|
|
1141
|
+
this.semanticCaptureRetryTimers.delete(fingerprint);
|
|
1142
|
+
void this.runSemanticProfileCapture(identity, sessionId, messageText, fingerprint);
|
|
1143
|
+
}, delayMs);
|
|
1144
|
+
this.semanticCaptureRetryTimers.set(fingerprint, timer);
|
|
1145
|
+
logUserBindEvent("semantic-profile-capture-retry-scheduled", {
|
|
1146
|
+
userId: identity.userId,
|
|
1147
|
+
sessionId,
|
|
1148
|
+
attempt: nextAttempt,
|
|
1149
|
+
delayMs,
|
|
1150
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
1153
|
+
schedulePendingSemanticSweep(delayMs = 1200) {
|
|
1154
|
+
if (this.pendingSemanticSweepTimer) {
|
|
1155
|
+
clearTimeout(this.pendingSemanticSweepTimer);
|
|
1156
|
+
}
|
|
1157
|
+
logUserBindEvent("pending-semantic-refine-sweep-scheduled", { delayMs });
|
|
1158
|
+
this.pendingSemanticSweepTimer = setTimeout(() => {
|
|
1159
|
+
this.pendingSemanticSweepTimer = null;
|
|
1160
|
+
void this.runPendingSemanticSweep().catch((error) => {
|
|
1161
|
+
logUserBindEvent("pending-semantic-refine-sweep-failed", {
|
|
1162
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1163
|
+
});
|
|
1164
|
+
});
|
|
1165
|
+
}, delayMs);
|
|
1166
|
+
}
|
|
1167
|
+
async runPendingSemanticSweep() {
|
|
1168
|
+
if (this.pendingSemanticSweepInFlight) {
|
|
1169
|
+
logUserBindEvent("pending-semantic-refine-sweep-skipped", { reason: "already-in-flight" });
|
|
1170
|
+
return;
|
|
1171
|
+
}
|
|
1172
|
+
this.pendingSemanticSweepInFlight = true;
|
|
1173
|
+
try {
|
|
1174
|
+
const profiles = this.store.listProfilesWithPendingSemanticRefine();
|
|
1175
|
+
logUserBindEvent("pending-semantic-refine-sweep-start", {
|
|
1176
|
+
profileCount: profiles.length,
|
|
1177
|
+
userIds: profiles.slice(0, 10).map((profile) => profile.userId)
|
|
1178
|
+
});
|
|
1179
|
+
for (const profile of profiles) {
|
|
1180
|
+
const entries = extractPendingSemanticRefineEntries(profile.notes);
|
|
1181
|
+
logUserBindEvent("pending-semantic-refine-profile-scan", {
|
|
1182
|
+
userId: profile.userId,
|
|
1183
|
+
entryCount: entries.length
|
|
1184
|
+
});
|
|
1185
|
+
for (const entry of entries) {
|
|
1186
|
+
const cachedAt = this.semanticCaptureCache.get(entry.fingerprint);
|
|
1187
|
+
if (cachedAt && cachedAt > Date.now() - 12 * 60 * 60 * 1e3) {
|
|
1188
|
+
logUserBindEvent("pending-semantic-refine-entry-skipped", {
|
|
1189
|
+
userId: profile.userId,
|
|
1190
|
+
fingerprint: entry.fingerprint,
|
|
1191
|
+
reason: "recently-cached"
|
|
1192
|
+
});
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
if (this.semanticCaptureInFlight.has(entry.fingerprint)) {
|
|
1196
|
+
logUserBindEvent("pending-semantic-refine-entry-skipped", {
|
|
1197
|
+
userId: profile.userId,
|
|
1198
|
+
fingerprint: entry.fingerprint,
|
|
1199
|
+
reason: "already-in-flight"
|
|
1200
|
+
});
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
if (this.globalPendingSemanticRefines.has(entry.fingerprint)) {
|
|
1204
|
+
logUserBindEvent("pending-semantic-refine-entry-skipped", {
|
|
1205
|
+
userId: profile.userId,
|
|
1206
|
+
fingerprint: entry.fingerprint,
|
|
1207
|
+
reason: "process-global-in-flight"
|
|
1208
|
+
});
|
|
1209
|
+
continue;
|
|
1210
|
+
}
|
|
1211
|
+
await this.runPendingSemanticProfileCapture(profile, entry.messageText, entry.fingerprint);
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
} finally {
|
|
1215
|
+
this.pendingSemanticSweepInFlight = false;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
async runPendingSemanticProfileCapture(profile, messageText, fingerprint) {
|
|
1219
|
+
if (this.globalPendingSemanticRefines.has(fingerprint)) {
|
|
1220
|
+
logUserBindEvent("pending-semantic-refine-entry-skipped", {
|
|
1221
|
+
userId: profile.userId,
|
|
1222
|
+
fingerprint,
|
|
1223
|
+
reason: "process-global-in-flight"
|
|
1224
|
+
});
|
|
1225
|
+
return;
|
|
1226
|
+
}
|
|
1227
|
+
this.globalPendingSemanticRefines.add(fingerprint);
|
|
1228
|
+
this.semanticCaptureInFlight.add(fingerprint);
|
|
1229
|
+
try {
|
|
1230
|
+
logUserBindEvent("pending-semantic-refine-entry-start", {
|
|
1231
|
+
userId: profile.userId,
|
|
1232
|
+
fingerprint,
|
|
1233
|
+
messagePreview: messageText.slice(0, 120)
|
|
1234
|
+
});
|
|
1235
|
+
const extraction = await inferSemanticProfileExtraction(messageText, profile);
|
|
1236
|
+
if (!extraction?.shouldUpdate) {
|
|
1237
|
+
logUserBindEvent("pending-semantic-refine-noop", {
|
|
1238
|
+
userId: profile.userId,
|
|
1239
|
+
fingerprint
|
|
1240
|
+
});
|
|
1241
|
+
this.semanticCaptureCache.set(fingerprint, Date.now());
|
|
1242
|
+
this.clearSemanticProfileRetryState(fingerprint);
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
const { patch, operations } = cleanupSemanticProfilePatch(
|
|
1246
|
+
extraction.patch,
|
|
1247
|
+
profile,
|
|
1248
|
+
extraction.operations
|
|
1249
|
+
);
|
|
1250
|
+
const nextPatch = applyProfilePatchOperations(profile, patch, operations);
|
|
1251
|
+
const cleanedNotes = removePendingSemanticRefineEntry(nextPatch.notes ?? profile.notes, fingerprint);
|
|
1252
|
+
const updated = this.store.updateProfile(profile.userId, {
|
|
1253
|
+
...nextPatch,
|
|
1254
|
+
notes: cleanedNotes,
|
|
1255
|
+
source: "semantic-self-update"
|
|
1256
|
+
});
|
|
1257
|
+
const finalProfile = updated.notes?.includes(`[pending-profile-refine:${fingerprint}]`) ? this.store.replaceProfileNotes(
|
|
1258
|
+
profile.userId,
|
|
1259
|
+
removePendingSemanticRefineEntry(updated.notes, fingerprint),
|
|
1260
|
+
"semantic-self-update"
|
|
1261
|
+
) : updated;
|
|
1262
|
+
logUserBindEvent("pending-semantic-refine-recovered", {
|
|
1263
|
+
userId: profile.userId,
|
|
1264
|
+
fields: Object.keys(patch),
|
|
1265
|
+
confidence: extraction.confidence,
|
|
1266
|
+
notesCleared: !finalProfile.notes?.includes(`[pending-profile-refine:${fingerprint}]`)
|
|
1267
|
+
});
|
|
1268
|
+
this.semanticCaptureCache.set(fingerprint, Date.now());
|
|
1269
|
+
this.clearSemanticProfileRetryState(fingerprint);
|
|
1270
|
+
} catch (error) {
|
|
1271
|
+
if (this.shouldRetrySemanticProfileCapture(error)) {
|
|
1272
|
+
const nextAttempt = (this.semanticCaptureRetryAttempts.get(fingerprint) ?? 0) + 1;
|
|
1273
|
+
this.semanticCaptureRetryAttempts.set(fingerprint, nextAttempt);
|
|
1274
|
+
if (nextAttempt <= SEMANTIC_PROFILE_RETRY_MAX_ATTEMPTS) {
|
|
1275
|
+
this.schedulePendingSemanticSweep(computeSemanticProfileRetryDelayMs(nextAttempt));
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
logUserBindEvent("pending-semantic-refine-failed", {
|
|
1280
|
+
userId: profile.userId,
|
|
1281
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1282
|
+
});
|
|
1283
|
+
} finally {
|
|
1284
|
+
this.semanticCaptureInFlight.delete(fingerprint);
|
|
1285
|
+
this.globalPendingSemanticRefines.delete(fingerprint);
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
clearSemanticProfileRetryState(fingerprint) {
|
|
1289
|
+
const timer = this.semanticCaptureRetryTimers.get(fingerprint);
|
|
1290
|
+
if (timer) {
|
|
1291
|
+
clearTimeout(timer);
|
|
1292
|
+
this.semanticCaptureRetryTimers.delete(fingerprint);
|
|
1293
|
+
}
|
|
1294
|
+
this.semanticCaptureRetryAttempts.delete(fingerprint);
|
|
1295
|
+
}
|
|
1296
|
+
shouldRetrySemanticProfileCapture(error) {
|
|
1297
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1298
|
+
const normalized = message.toLowerCase();
|
|
1299
|
+
return normalized.includes("timed out") || normalized.includes("aborted due to timeout") || normalized.includes("network connection error") || normalized.includes("connection error") || normalized.includes("rate limit") || normalized.includes("429") || normalized.includes("503") || normalized.includes("busy");
|
|
1300
|
+
}
|
|
1301
|
+
async ensurePendingSemanticRefineNote(sessionId, messageText, fingerprint) {
|
|
1302
|
+
const note = buildPendingSemanticRefineNote(messageText, fingerprint);
|
|
1303
|
+
const identity = await this.resolveFromContext({ sessionId });
|
|
1304
|
+
if (!identity) {
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (identity.profile.notes?.includes(note)) {
|
|
1308
|
+
return;
|
|
1309
|
+
}
|
|
1310
|
+
await this.updateMyProfile(
|
|
1311
|
+
{ sessionId },
|
|
1312
|
+
{
|
|
1313
|
+
notes: note,
|
|
1314
|
+
source: "semantic-refine-pending"
|
|
1315
|
+
},
|
|
1316
|
+
{ notes: "append" }
|
|
1317
|
+
);
|
|
1318
|
+
}
|
|
1319
|
+
async removePendingSemanticRefineNote(sessionId, fingerprint) {
|
|
1320
|
+
const identity = await this.resolveFromContext({ sessionId });
|
|
1321
|
+
const currentNotes = identity?.profile.notes ?? null;
|
|
1322
|
+
if (!currentNotes || !currentNotes.includes(`[pending-profile-refine:${fingerprint}]`)) {
|
|
1323
|
+
return;
|
|
1324
|
+
}
|
|
1325
|
+
await this.updateMyProfile(
|
|
1326
|
+
{ sessionId },
|
|
1327
|
+
{
|
|
1328
|
+
notes: `[pending-profile-refine:${fingerprint}]`,
|
|
1329
|
+
source: "semantic-self-update"
|
|
1330
|
+
},
|
|
1331
|
+
{ notes: "remove" }
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
952
1334
|
pruneSemanticCaptureCache() {
|
|
953
1335
|
const cutoff = Date.now() - 12 * 60 * 60 * 1e3;
|
|
954
1336
|
for (const [fingerprint, ts] of this.semanticCaptureCache.entries()) {
|
|
@@ -1038,8 +1420,6 @@ var UserBindRuntime = class {
|
|
|
1038
1420
|
if (!identity) {
|
|
1039
1421
|
return;
|
|
1040
1422
|
}
|
|
1041
|
-
const captureContext = mergeSemanticCaptureContext(event, context);
|
|
1042
|
-
await this.captureProfileFromMessage(captureContext, identity);
|
|
1043
1423
|
return {
|
|
1044
1424
|
context: [
|
|
1045
1425
|
{
|
|
@@ -1064,7 +1444,7 @@ var UserBindRuntime = class {
|
|
|
1064
1444
|
const refreshMyBindingExecute = async (_id, params) => asTextResult(await this.refreshMyBinding(params));
|
|
1065
1445
|
registerTool({
|
|
1066
1446
|
name: "bamdra_user_bind_get_my_profile",
|
|
1067
|
-
description: "Get the current user's bound profile before replying so
|
|
1447
|
+
description: "Get the current user's bound profile before replying so identity facts, timezone, and stable personal preferences can be used naturally in the response",
|
|
1068
1448
|
parameters: {
|
|
1069
1449
|
type: "object",
|
|
1070
1450
|
additionalProperties: false,
|
|
@@ -1076,19 +1456,31 @@ var UserBindRuntime = class {
|
|
|
1076
1456
|
});
|
|
1077
1457
|
registerTool({
|
|
1078
1458
|
name: "bamdra_user_bind_update_my_profile",
|
|
1079
|
-
description: "Immediately write the current user's stable
|
|
1459
|
+
description: "Immediately write the current user's stable profile information when they clearly provide it, such as name, nickname, gender, age, birthday, timezone, interests, communication style, role, or durable notes",
|
|
1080
1460
|
parameters: {
|
|
1081
1461
|
type: "object",
|
|
1082
1462
|
additionalProperties: false,
|
|
1083
1463
|
required: ["sessionId"],
|
|
1084
1464
|
properties: {
|
|
1085
1465
|
sessionId: { type: "string" },
|
|
1466
|
+
name: { type: "string" },
|
|
1467
|
+
nameOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1468
|
+
gender: { type: "string" },
|
|
1469
|
+
genderOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1470
|
+
birthDate: { type: "string" },
|
|
1471
|
+
birthDateOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1472
|
+
birthYear: { type: "string" },
|
|
1473
|
+
birthYearOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1474
|
+
age: { type: "string" },
|
|
1475
|
+
ageOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1086
1476
|
nickname: { type: "string" },
|
|
1087
1477
|
nicknameOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1088
1478
|
preferences: { type: "string" },
|
|
1089
1479
|
preferencesOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1090
1480
|
personality: { type: "string" },
|
|
1091
1481
|
personalityOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1482
|
+
interests: { type: "string" },
|
|
1483
|
+
interestsOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1092
1484
|
role: { type: "string" },
|
|
1093
1485
|
roleOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1094
1486
|
timezone: { type: "string" },
|
|
@@ -1265,73 +1657,6 @@ var UserBindRuntime = class {
|
|
|
1265
1657
|
logUserBindEvent("feishu-resolution-empty", { openId });
|
|
1266
1658
|
return null;
|
|
1267
1659
|
}
|
|
1268
|
-
async ensureFeishuScopeStatus(account) {
|
|
1269
|
-
if (account) {
|
|
1270
|
-
const cached = this.feishuScopeStatusCache.get(account.accountId);
|
|
1271
|
-
if (cached) {
|
|
1272
|
-
return cached;
|
|
1273
|
-
}
|
|
1274
|
-
try {
|
|
1275
|
-
const token = await this.getFeishuAppAccessToken(account);
|
|
1276
|
-
const result = await feishuJsonRequest(
|
|
1277
|
-
account,
|
|
1278
|
-
"/open-apis/application/v6/scopes",
|
|
1279
|
-
token
|
|
1280
|
-
);
|
|
1281
|
-
const scopes = extractScopes(result);
|
|
1282
|
-
const status = {
|
|
1283
|
-
scopes,
|
|
1284
|
-
missingIdentityScopes: REQUIRED_FEISHU_IDENTITY_SCOPES.filter((scope) => !scopes.includes(scope)),
|
|
1285
|
-
hasDocumentAccess: scopes.some((scope) => scope.startsWith("bitable:") || scope.startsWith("drive:") || scope.startsWith("docx:") || scope.startsWith("docs:"))
|
|
1286
|
-
};
|
|
1287
|
-
this.feishuScopeStatusCache.set(account.accountId, status);
|
|
1288
|
-
logUserBindEvent("feishu-scopes-read", {
|
|
1289
|
-
accountId: account.accountId,
|
|
1290
|
-
...status
|
|
1291
|
-
});
|
|
1292
|
-
return status;
|
|
1293
|
-
} catch (error) {
|
|
1294
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1295
|
-
logUserBindEvent("feishu-scopes-attempt-failed", { accountId: account.accountId, message });
|
|
1296
|
-
}
|
|
1297
|
-
}
|
|
1298
|
-
const accounts = readFeishuAccountsFromOpenClawConfig();
|
|
1299
|
-
for (const candidate of accounts) {
|
|
1300
|
-
const cached = this.feishuScopeStatusCache.get(candidate.accountId);
|
|
1301
|
-
if (cached?.hasDocumentAccess) {
|
|
1302
|
-
return cached;
|
|
1303
|
-
}
|
|
1304
|
-
}
|
|
1305
|
-
for (const candidate of accounts) {
|
|
1306
|
-
const status = await this.ensureFeishuScopeStatus(candidate);
|
|
1307
|
-
if (status.hasDocumentAccess) {
|
|
1308
|
-
return status;
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
const executor = this.host.callTool ?? this.host.invokeTool;
|
|
1312
|
-
if (typeof executor === "function") {
|
|
1313
|
-
try {
|
|
1314
|
-
const result = await executor.call(this.host, "feishu_app_scopes", {});
|
|
1315
|
-
const scopes = extractScopes(result);
|
|
1316
|
-
const status = {
|
|
1317
|
-
scopes,
|
|
1318
|
-
missingIdentityScopes: REQUIRED_FEISHU_IDENTITY_SCOPES.filter((scope) => !scopes.includes(scope)),
|
|
1319
|
-
hasDocumentAccess: scopes.some((scope) => scope.startsWith("bitable:") || scope.startsWith("drive:") || scope.startsWith("docx:") || scope.startsWith("docs:"))
|
|
1320
|
-
};
|
|
1321
|
-
logUserBindEvent("feishu-scopes-read", status);
|
|
1322
|
-
return status;
|
|
1323
|
-
} catch (error) {
|
|
1324
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1325
|
-
logUserBindEvent("feishu-scopes-failed", { message });
|
|
1326
|
-
this.store.recordIssue("feishu-scope-read", message);
|
|
1327
|
-
}
|
|
1328
|
-
}
|
|
1329
|
-
return {
|
|
1330
|
-
scopes: [],
|
|
1331
|
-
missingIdentityScopes: [...REQUIRED_FEISHU_IDENTITY_SCOPES],
|
|
1332
|
-
hasDocumentAccess: false
|
|
1333
|
-
};
|
|
1334
|
-
}
|
|
1335
1660
|
async getFeishuAppAccessToken(account) {
|
|
1336
1661
|
const cached = this.feishuTokenCache.get(account.accountId);
|
|
1337
1662
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -1363,108 +1688,17 @@ var UserBindRuntime = class {
|
|
|
1363
1688
|
});
|
|
1364
1689
|
return token;
|
|
1365
1690
|
}
|
|
1366
|
-
async syncFeishuMirror(identity) {
|
|
1367
|
-
const scopeStatus = await this.ensureFeishuScopeStatus();
|
|
1368
|
-
if (!scopeStatus.hasDocumentAccess) {
|
|
1369
|
-
return;
|
|
1370
|
-
}
|
|
1371
|
-
const executor = this.host.callTool ?? this.host.invokeTool;
|
|
1372
|
-
if (typeof executor !== "function") {
|
|
1373
|
-
return;
|
|
1374
|
-
}
|
|
1375
|
-
try {
|
|
1376
|
-
const mirror = await this.ensureFeishuBitableMirror(executor.bind(this.host));
|
|
1377
|
-
if (!mirror.appToken || !mirror.tableId) {
|
|
1378
|
-
return;
|
|
1379
|
-
}
|
|
1380
|
-
const existing = await executor.call(this.host, "feishu_bitable_list_records", {
|
|
1381
|
-
app_token: mirror.appToken,
|
|
1382
|
-
table_id: mirror.tableId
|
|
1383
|
-
});
|
|
1384
|
-
const recordId = findBitableRecordId(existing, identity.userId);
|
|
1385
|
-
const fields = {
|
|
1386
|
-
user_id: identity.userId,
|
|
1387
|
-
channel_type: identity.channelType,
|
|
1388
|
-
open_id: identity.senderOpenId,
|
|
1389
|
-
name: identity.profile.name,
|
|
1390
|
-
nickname: identity.profile.nickname,
|
|
1391
|
-
preferences: identity.profile.preferences,
|
|
1392
|
-
personality: identity.profile.personality,
|
|
1393
|
-
role: identity.profile.role,
|
|
1394
|
-
timezone: identity.profile.timezone,
|
|
1395
|
-
email: identity.profile.email,
|
|
1396
|
-
avatar: identity.profile.avatar
|
|
1397
|
-
};
|
|
1398
|
-
if (recordId) {
|
|
1399
|
-
await executor.call(this.host, "feishu_bitable_update_record", {
|
|
1400
|
-
app_token: mirror.appToken,
|
|
1401
|
-
table_id: mirror.tableId,
|
|
1402
|
-
record_id: recordId,
|
|
1403
|
-
fields
|
|
1404
|
-
});
|
|
1405
|
-
} else {
|
|
1406
|
-
await executor.call(this.host, "feishu_bitable_create_record", {
|
|
1407
|
-
app_token: mirror.appToken,
|
|
1408
|
-
table_id: mirror.tableId,
|
|
1409
|
-
fields
|
|
1410
|
-
});
|
|
1411
|
-
}
|
|
1412
|
-
logUserBindEvent("feishu-bitable-sync-success", { userId: identity.userId });
|
|
1413
|
-
} catch (error) {
|
|
1414
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1415
|
-
logUserBindEvent("feishu-bitable-sync-failed", { userId: identity.userId, message });
|
|
1416
|
-
this.store.recordIssue("feishu-bitable-sync", message, identity.userId);
|
|
1417
|
-
}
|
|
1418
|
-
}
|
|
1419
|
-
async ensureFeishuBitableMirror(executor) {
|
|
1420
|
-
if (this.bitableMirror?.appToken && this.bitableMirror?.tableId) {
|
|
1421
|
-
return this.bitableMirror;
|
|
1422
|
-
}
|
|
1423
|
-
try {
|
|
1424
|
-
const app = await executor("feishu_bitable_create_app", { name: "Bamdra User Bind" });
|
|
1425
|
-
const appToken = extractDeepString(app, [
|
|
1426
|
-
["data", "app", "app_token"],
|
|
1427
|
-
["data", "app_token"],
|
|
1428
|
-
["app", "app_token"],
|
|
1429
|
-
["app_token"]
|
|
1430
|
-
]);
|
|
1431
|
-
if (!appToken) {
|
|
1432
|
-
return { appToken: null, tableId: null };
|
|
1433
|
-
}
|
|
1434
|
-
const meta = await executor("feishu_bitable_get_meta", { app_token: appToken });
|
|
1435
|
-
const tableId = extractDeepString(meta, [
|
|
1436
|
-
["data", "tables", "0", "table_id"],
|
|
1437
|
-
["data", "items", "0", "table_id"],
|
|
1438
|
-
["tables", "0", "table_id"]
|
|
1439
|
-
]);
|
|
1440
|
-
if (!tableId) {
|
|
1441
|
-
this.store.recordIssue("feishu-bitable-init", "Unable to determine users table id from Feishu bitable metadata");
|
|
1442
|
-
return { appToken, tableId: null };
|
|
1443
|
-
}
|
|
1444
|
-
for (const fieldName of ["user_id", "channel_type", "open_id", "name", "nickname", "preferences", "personality", "role", "timezone", "email", "avatar"]) {
|
|
1445
|
-
try {
|
|
1446
|
-
await executor("feishu_bitable_create_field", {
|
|
1447
|
-
app_token: appToken,
|
|
1448
|
-
table_id: tableId,
|
|
1449
|
-
field_name: fieldName,
|
|
1450
|
-
type: 1
|
|
1451
|
-
});
|
|
1452
|
-
} catch {
|
|
1453
|
-
}
|
|
1454
|
-
}
|
|
1455
|
-
this.bitableMirror = { appToken, tableId };
|
|
1456
|
-
logUserBindEvent("feishu-bitable-ready", this.bitableMirror);
|
|
1457
|
-
return this.bitableMirror;
|
|
1458
|
-
} catch (error) {
|
|
1459
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
1460
|
-
logUserBindEvent("feishu-bitable-init-failed", { message });
|
|
1461
|
-
this.store.recordIssue("feishu-bitable-init", message);
|
|
1462
|
-
return { appToken: null, tableId: null };
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
1691
|
};
|
|
1466
1692
|
function createUserBindPlugin(api) {
|
|
1467
|
-
|
|
1693
|
+
const globalRecord = globalThis;
|
|
1694
|
+
const existing = globalRecord[GLOBAL_RUNTIME_KEY];
|
|
1695
|
+
if (isUserBindRuntimeLike(existing)) {
|
|
1696
|
+
return existing;
|
|
1697
|
+
}
|
|
1698
|
+
const runtime = new UserBindRuntime(api, api.pluginConfig ?? api.config ?? api.plugin?.config);
|
|
1699
|
+
runtime[GLOBAL_RUNTIME_BRAND_KEY] = true;
|
|
1700
|
+
globalRecord[GLOBAL_RUNTIME_KEY] = runtime;
|
|
1701
|
+
return runtime;
|
|
1468
1702
|
}
|
|
1469
1703
|
function register(api) {
|
|
1470
1704
|
createUserBindPlugin(api).register();
|
|
@@ -1474,12 +1708,12 @@ async function activate(api) {
|
|
|
1474
1708
|
}
|
|
1475
1709
|
function normalizeConfig(input) {
|
|
1476
1710
|
const root = (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "data", "bamdra-user-bind");
|
|
1477
|
-
const storeRoot = input?.localStorePath ?? root;
|
|
1711
|
+
const storeRoot = expandHomePath(input?.localStorePath) ?? root;
|
|
1478
1712
|
return {
|
|
1479
1713
|
enabled: input?.enabled ?? true,
|
|
1480
1714
|
localStorePath: storeRoot,
|
|
1481
|
-
exportPath: input?.exportPath ?? (0, import_node_path.join)(storeRoot, "exports"),
|
|
1482
|
-
profileMarkdownRoot: input?.profileMarkdownRoot ?? (0, import_node_path.join)(storeRoot, "profiles", "private"),
|
|
1715
|
+
exportPath: expandHomePath(input?.exportPath) ?? (0, import_node_path.join)(storeRoot, "exports"),
|
|
1716
|
+
profileMarkdownRoot: expandHomePath(input?.profileMarkdownRoot) ?? (0, import_node_path.join)(storeRoot, "profiles", "private"),
|
|
1483
1717
|
cacheTtlMs: input?.cacheTtlMs ?? 30 * 60 * 1e3,
|
|
1484
1718
|
adminAgents: input?.adminAgents?.length ? input.adminAgents : ["main"]
|
|
1485
1719
|
};
|
|
@@ -1517,17 +1751,18 @@ function bootstrapOpenClawHost(config) {
|
|
|
1517
1751
|
materializeBundledSkill(adminSkillSource, adminSkillTarget);
|
|
1518
1752
|
const original = (0, import_node_fs.readFileSync)(configPath, "utf8");
|
|
1519
1753
|
const parsed = JSON.parse(original);
|
|
1520
|
-
const changed = ensureHostConfig(parsed, config, profileSkillTarget, adminSkillTarget);
|
|
1754
|
+
const changed = ensureHostConfig(parsed, config, packageRoot, profileSkillTarget, adminSkillTarget);
|
|
1521
1755
|
if (!changed) {
|
|
1522
1756
|
return;
|
|
1523
1757
|
}
|
|
1524
1758
|
(0, import_node_fs.writeFileSync)(configPath, `${JSON.stringify(parsed, null, 2)}
|
|
1525
1759
|
`, "utf8");
|
|
1526
1760
|
}
|
|
1527
|
-
function ensureHostConfig(config, pluginConfig, profileSkillTarget, adminSkillTarget) {
|
|
1761
|
+
function ensureHostConfig(config, pluginConfig, packageRoot, profileSkillTarget, adminSkillTarget) {
|
|
1528
1762
|
let changed = false;
|
|
1529
1763
|
const plugins = ensureObject(config, "plugins");
|
|
1530
1764
|
const entries = ensureObject(plugins, "entries");
|
|
1765
|
+
const installs = ensureObject(plugins, "installs");
|
|
1531
1766
|
const load = ensureObject(plugins, "load");
|
|
1532
1767
|
const tools = ensureObject(config, "tools");
|
|
1533
1768
|
const skills = ensureObject(config, "skills");
|
|
@@ -1538,6 +1773,11 @@ function ensureHostConfig(config, pluginConfig, profileSkillTarget, adminSkillTa
|
|
|
1538
1773
|
changed = ensureArrayIncludes(plugins, "allow", PLUGIN_ID) || changed;
|
|
1539
1774
|
changed = ensureArrayIncludes(load, "paths", (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "extensions")) || changed;
|
|
1540
1775
|
changed = ensureArrayIncludes(skillsLoad, "extraDirs", (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "skills")) || changed;
|
|
1776
|
+
changed = ensureInstallMetadata(
|
|
1777
|
+
installs,
|
|
1778
|
+
PLUGIN_ID,
|
|
1779
|
+
readPluginInstallMetadata(PLUGIN_ID, packageRoot, (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "extensions", PLUGIN_ID))
|
|
1780
|
+
) || changed;
|
|
1541
1781
|
if (entry.enabled !== true) {
|
|
1542
1782
|
entry.enabled = true;
|
|
1543
1783
|
changed = true;
|
|
@@ -1577,6 +1817,46 @@ function materializeBundledSkill(sourceDir, targetDir) {
|
|
|
1577
1817
|
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(targetDir), { recursive: true });
|
|
1578
1818
|
(0, import_node_fs.cpSync)(sourceDir, targetDir, { recursive: true });
|
|
1579
1819
|
}
|
|
1820
|
+
function readPluginInstallMetadata(pluginId, packageRoot, installPath) {
|
|
1821
|
+
try {
|
|
1822
|
+
const pkg = JSON.parse((0, import_node_fs.readFileSync)((0, import_node_path.join)(packageRoot, "package.json"), "utf8"));
|
|
1823
|
+
const packageName = typeof pkg.name === "string" ? pkg.name : pluginId;
|
|
1824
|
+
const version = typeof pkg.version === "string" ? pkg.version : "0.0.0";
|
|
1825
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1826
|
+
return {
|
|
1827
|
+
source: "npm",
|
|
1828
|
+
spec: packageName,
|
|
1829
|
+
installPath,
|
|
1830
|
+
version,
|
|
1831
|
+
resolvedName: packageName,
|
|
1832
|
+
resolvedVersion: version,
|
|
1833
|
+
resolvedSpec: `${packageName}@${version}`,
|
|
1834
|
+
resolvedAt: now,
|
|
1835
|
+
installedAt: now
|
|
1836
|
+
};
|
|
1837
|
+
} catch {
|
|
1838
|
+
return null;
|
|
1839
|
+
}
|
|
1840
|
+
}
|
|
1841
|
+
function ensureInstallMetadata(installs, pluginId, metadata) {
|
|
1842
|
+
if (!metadata) {
|
|
1843
|
+
return false;
|
|
1844
|
+
}
|
|
1845
|
+
const current = installs[pluginId];
|
|
1846
|
+
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
1847
|
+
const install = current;
|
|
1848
|
+
let changed = false;
|
|
1849
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
1850
|
+
if (typeof install[key] !== "string" || install[key] === "") {
|
|
1851
|
+
install[key] = value;
|
|
1852
|
+
changed = true;
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
return changed;
|
|
1856
|
+
}
|
|
1857
|
+
installs[pluginId] = metadata;
|
|
1858
|
+
return true;
|
|
1859
|
+
}
|
|
1580
1860
|
function ensureToolNames(tools, values) {
|
|
1581
1861
|
let changed = false;
|
|
1582
1862
|
for (const value of values) {
|
|
@@ -1665,11 +1945,15 @@ function mapProfileRow(row) {
|
|
|
1665
1945
|
userId: String(row.user_id),
|
|
1666
1946
|
name: asNullableString(row.name),
|
|
1667
1947
|
gender: asNullableString(row.gender),
|
|
1948
|
+
birthDate: asNullableString(row.birth_date),
|
|
1949
|
+
birthYear: asNullableString(row.birth_year),
|
|
1950
|
+
age: asNullableString(row.age),
|
|
1668
1951
|
email: asNullableString(row.email),
|
|
1669
1952
|
avatar: asNullableString(row.avatar),
|
|
1670
1953
|
nickname: asNullableString(row.nickname),
|
|
1671
1954
|
preferences: asNullableString(row.preferences),
|
|
1672
1955
|
personality: asNullableString(row.personality),
|
|
1956
|
+
interests: asNullableString(row.interests),
|
|
1673
1957
|
role: asNullableString(row.role),
|
|
1674
1958
|
timezone: asNullableString(row.timezone),
|
|
1675
1959
|
notes: asNullableString(row.notes),
|
|
@@ -1817,6 +2101,78 @@ function extractUserUtterance(context) {
|
|
|
1817
2101
|
}
|
|
1818
2102
|
return stripped;
|
|
1819
2103
|
}
|
|
2104
|
+
function buildSemanticCaptureInput(context, utteranceText) {
|
|
2105
|
+
const record = context && typeof context === "object" ? context : {};
|
|
2106
|
+
const recentDialogue = readRecentProfileCaptureDialogue(record.sessionManager, utteranceText);
|
|
2107
|
+
if (!recentDialogue) {
|
|
2108
|
+
return utteranceText;
|
|
2109
|
+
}
|
|
2110
|
+
return [
|
|
2111
|
+
"Recent profile-collection exchange:",
|
|
2112
|
+
recentDialogue
|
|
2113
|
+
].join("\n");
|
|
2114
|
+
}
|
|
2115
|
+
function shouldSkipSemanticProfileCapture(text) {
|
|
2116
|
+
const normalized = normalizeSemanticCaptureText(text);
|
|
2117
|
+
if (!normalized) {
|
|
2118
|
+
return true;
|
|
2119
|
+
}
|
|
2120
|
+
if (isTrivialSemanticCaptureUtterance(normalized)) {
|
|
2121
|
+
return true;
|
|
2122
|
+
}
|
|
2123
|
+
const tokenCount = normalized.split(/\s+/).filter(Boolean).length;
|
|
2124
|
+
if (normalized.length <= 4) {
|
|
2125
|
+
return true;
|
|
2126
|
+
}
|
|
2127
|
+
return tokenCount <= 1 && !containsCjkCharacters(normalized);
|
|
2128
|
+
}
|
|
2129
|
+
function shouldIgnoreSemanticProfileCaptureCandidate(text) {
|
|
2130
|
+
const normalized = normalizeSemanticCaptureText(text);
|
|
2131
|
+
return !normalized || isTrivialSemanticCaptureUtterance(normalized);
|
|
2132
|
+
}
|
|
2133
|
+
function isTrivialSemanticCaptureUtterance(normalized) {
|
|
2134
|
+
const trivialUtterances = /* @__PURE__ */ new Set([
|
|
2135
|
+
"hi",
|
|
2136
|
+
"hello",
|
|
2137
|
+
"hey",
|
|
2138
|
+
"\u4F60\u597D",
|
|
2139
|
+
"\u60A8\u597D",
|
|
2140
|
+
"\u5728\u5417",
|
|
2141
|
+
"\u5728\u4E48",
|
|
2142
|
+
"\u5728\u4E0D\u5728",
|
|
2143
|
+
"\u6709\u4EBA\u5417",
|
|
2144
|
+
"ping",
|
|
2145
|
+
"test",
|
|
2146
|
+
"\u6D4B\u8BD5"
|
|
2147
|
+
]);
|
|
2148
|
+
return trivialUtterances.has(normalized);
|
|
2149
|
+
}
|
|
2150
|
+
function normalizeSemanticCaptureText(text) {
|
|
2151
|
+
return text.trim().toLowerCase().replace(/[!?.,,。!?、;;::"'`~()\[\]{}<>@#%^&*_+=|\\/.-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
2152
|
+
}
|
|
2153
|
+
function containsCjkCharacters(text) {
|
|
2154
|
+
return /[\u3400-\u9fff\uf900-\ufaff]/.test(text);
|
|
2155
|
+
}
|
|
2156
|
+
function appendSemanticCaptureCandidate(existing, messageText) {
|
|
2157
|
+
const normalizedCandidate = normalizeSemanticCaptureText(messageText);
|
|
2158
|
+
if (!normalizedCandidate) {
|
|
2159
|
+
return existing;
|
|
2160
|
+
}
|
|
2161
|
+
const deduped = existing.filter((item) => normalizeSemanticCaptureText(item) !== normalizedCandidate);
|
|
2162
|
+
deduped.push(messageText.trim());
|
|
2163
|
+
const recent = deduped.slice(-SEMANTIC_PROFILE_BATCH_MAX_FRAGMENTS);
|
|
2164
|
+
while (recent.join("\n").length > SEMANTIC_PROFILE_BATCH_MAX_CHARS && recent.length > 1) {
|
|
2165
|
+
recent.shift();
|
|
2166
|
+
}
|
|
2167
|
+
return recent;
|
|
2168
|
+
}
|
|
2169
|
+
function buildSemanticProfileBatchText(messages) {
|
|
2170
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
2171
|
+
return null;
|
|
2172
|
+
}
|
|
2173
|
+
const combined = messages.map((item) => item.trim()).filter(Boolean).join("\n");
|
|
2174
|
+
return combined || null;
|
|
2175
|
+
}
|
|
1820
2176
|
function extractHookContextText(record) {
|
|
1821
2177
|
const directText = normalizeHookText(
|
|
1822
2178
|
record.bodyForAgent ?? record.body ?? record.prompt ?? record.text ?? findNestedValue(record, ["context", "bodyForAgent"]) ?? findNestedValue(record, ["context", "body"]) ?? findNestedValue(record, ["context", "text"]) ?? findNestedValue(record, ["context", "content"])
|
|
@@ -1901,6 +2257,88 @@ function normalizeHookText(value) {
|
|
|
1901
2257
|
}
|
|
1902
2258
|
return null;
|
|
1903
2259
|
}
|
|
2260
|
+
function readRecentProfileCaptureDialogue(sessionManager, latestUserText) {
|
|
2261
|
+
if (!sessionManager || typeof sessionManager !== "object") {
|
|
2262
|
+
return null;
|
|
2263
|
+
}
|
|
2264
|
+
try {
|
|
2265
|
+
const getBranch = sessionManager.getBranch;
|
|
2266
|
+
const branch = typeof getBranch === "function" ? getBranch() : [];
|
|
2267
|
+
if (!Array.isArray(branch) || branch.length === 0) {
|
|
2268
|
+
return null;
|
|
2269
|
+
}
|
|
2270
|
+
const conversation = [];
|
|
2271
|
+
for (let i = 0; i < branch.length; i += 1) {
|
|
2272
|
+
const entry = branch[i];
|
|
2273
|
+
if (!entry || typeof entry !== "object") {
|
|
2274
|
+
continue;
|
|
2275
|
+
}
|
|
2276
|
+
const item = entry;
|
|
2277
|
+
const message = item.message && typeof item.message === "object" ? item.message : null;
|
|
2278
|
+
if (item.type !== "message" || !message) {
|
|
2279
|
+
continue;
|
|
2280
|
+
}
|
|
2281
|
+
const role = asNullableString(message.role);
|
|
2282
|
+
if (role !== "assistant" && role !== "user") {
|
|
2283
|
+
continue;
|
|
2284
|
+
}
|
|
2285
|
+
const text = extractMessageText(message);
|
|
2286
|
+
if (!text || looksLikeIdentityMetadata(text)) {
|
|
2287
|
+
continue;
|
|
2288
|
+
}
|
|
2289
|
+
conversation.push({ role, text: text.trim() });
|
|
2290
|
+
}
|
|
2291
|
+
for (let i = conversation.length - 1; i >= 0; i -= 1) {
|
|
2292
|
+
const entry = conversation[i];
|
|
2293
|
+
if (entry.role !== "user") {
|
|
2294
|
+
continue;
|
|
2295
|
+
}
|
|
2296
|
+
const previous = conversation[i - 1];
|
|
2297
|
+
if (!previous || previous.role !== "assistant" || !looksLikeProfileCollectionPrompt(previous.text)) {
|
|
2298
|
+
continue;
|
|
2299
|
+
}
|
|
2300
|
+
const normalizedEntry = normalizeSemanticCaptureText(entry.text);
|
|
2301
|
+
const normalizedLatest = normalizeSemanticCaptureText(latestUserText);
|
|
2302
|
+
if (normalizedLatest && normalizedEntry && normalizedEntry !== normalizedLatest) {
|
|
2303
|
+
continue;
|
|
2304
|
+
}
|
|
2305
|
+
return [
|
|
2306
|
+
`assistant: ${previous.text}`,
|
|
2307
|
+
`user: ${latestUserText}`
|
|
2308
|
+
].join("\n");
|
|
2309
|
+
}
|
|
2310
|
+
} catch (error) {
|
|
2311
|
+
logUserBindEvent("session-manager-dialogue-read-failed", {
|
|
2312
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2313
|
+
});
|
|
2314
|
+
}
|
|
2315
|
+
return null;
|
|
2316
|
+
}
|
|
2317
|
+
function looksLikeProfileCollectionPrompt(text) {
|
|
2318
|
+
const normalized = normalizeSemanticCaptureText(text);
|
|
2319
|
+
if (!normalized) {
|
|
2320
|
+
return false;
|
|
2321
|
+
}
|
|
2322
|
+
const keywords = [
|
|
2323
|
+
"\u600E\u4E48\u79F0\u547C\u4F60",
|
|
2324
|
+
"\u5982\u4F55\u79F0\u547C\u4F60",
|
|
2325
|
+
"\u600E\u4E48\u53EB\u4F60",
|
|
2326
|
+
"\u53EB\u4F60\u4EC0\u4E48",
|
|
2327
|
+
"\u79F0\u547C",
|
|
2328
|
+
"\u504F\u597D\u7684\u56DE\u7B54\u98CE\u683C",
|
|
2329
|
+
"\u56DE\u590D\u98CE\u683C",
|
|
2330
|
+
"\u56DE\u7B54\u98CE\u683C",
|
|
2331
|
+
"\u6C9F\u901A\u98CE\u683C",
|
|
2332
|
+
"\u4F60\u559C\u6B22\u6211\u600E\u4E48\u56DE\u590D",
|
|
2333
|
+
"\u4F60\u66F4\u559C\u6B22",
|
|
2334
|
+
"what should i call you",
|
|
2335
|
+
"how should i address you",
|
|
2336
|
+
"preferred style",
|
|
2337
|
+
"reply style",
|
|
2338
|
+
"response style"
|
|
2339
|
+
];
|
|
2340
|
+
return keywords.some((keyword) => normalized.includes(normalizeSemanticCaptureText(keyword)));
|
|
2341
|
+
}
|
|
1904
2342
|
function readLatestUserMessageFromSessionManager(sessionManager) {
|
|
1905
2343
|
if (!sessionManager || typeof sessionManager !== "object") {
|
|
1906
2344
|
return null;
|
|
@@ -1933,24 +2371,6 @@ function readLatestUserMessageFromSessionManager(sessionManager) {
|
|
|
1933
2371
|
}
|
|
1934
2372
|
return null;
|
|
1935
2373
|
}
|
|
1936
|
-
function mergeSemanticCaptureContext(event, context) {
|
|
1937
|
-
const merged = {};
|
|
1938
|
-
if (event && typeof event === "object") {
|
|
1939
|
-
Object.assign(merged, event);
|
|
1940
|
-
}
|
|
1941
|
-
if (context && typeof context === "object") {
|
|
1942
|
-
const record = context;
|
|
1943
|
-
for (const [key, value] of Object.entries(record)) {
|
|
1944
|
-
if (merged[key] === void 0) {
|
|
1945
|
-
merged[key] = value;
|
|
1946
|
-
}
|
|
1947
|
-
}
|
|
1948
|
-
}
|
|
1949
|
-
if (merged.sessionManager === void 0 && context && typeof context === "object") {
|
|
1950
|
-
merged.sessionManager = context.sessionManager;
|
|
1951
|
-
}
|
|
1952
|
-
return merged;
|
|
1953
|
-
}
|
|
1954
2374
|
function stripIdentityMetadata(text) {
|
|
1955
2375
|
const hadMetadata = looksLikeIdentityMetadata(text);
|
|
1956
2376
|
let cleaned = text.replace(/Conversation info \(untrusted metadata\):\s*```json[\s\S]*?```/gi, "").replace(/Sender \(untrusted metadata\):\s*```json[\s\S]*?```/gi, "").replace(/^\s*\[[^\n]*message_id[^\n]*\]\s*$/gim, "").replace(/^\s*\[[^\n]*sender_id[^\n]*\]\s*$/gim, "");
|
|
@@ -2145,9 +2565,15 @@ function getAgentIdFromContext(context) {
|
|
|
2145
2565
|
}
|
|
2146
2566
|
function sanitizeProfilePatch(params) {
|
|
2147
2567
|
return {
|
|
2568
|
+
name: asNullableString(params.name),
|
|
2569
|
+
gender: asNullableString(params.gender),
|
|
2570
|
+
birthDate: asNullableString(params.birthDate) ?? asNullableString(params.birthday),
|
|
2571
|
+
birthYear: asNullableString(params.birthYear),
|
|
2572
|
+
age: asNullableString(params.age),
|
|
2148
2573
|
nickname: asNullableString(params.nickname),
|
|
2149
2574
|
preferences: asNullableString(params.preferences),
|
|
2150
2575
|
personality: asNullableString(params.personality),
|
|
2576
|
+
interests: asNullableString(params.interests),
|
|
2151
2577
|
role: asNullableString(params.role),
|
|
2152
2578
|
timezone: asNullableString(params.timezone),
|
|
2153
2579
|
notes: asNullableString(params.notes)
|
|
@@ -2155,7 +2581,7 @@ function sanitizeProfilePatch(params) {
|
|
|
2155
2581
|
}
|
|
2156
2582
|
function extractProfilePatchOperations(params) {
|
|
2157
2583
|
const operations = {};
|
|
2158
|
-
const fields = ["nickname", "preferences", "personality", "role", "timezone", "notes"];
|
|
2584
|
+
const fields = ["name", "gender", "birthDate", "birthYear", "age", "nickname", "preferences", "personality", "interests", "role", "timezone", "notes"];
|
|
2159
2585
|
for (const field of fields) {
|
|
2160
2586
|
const raw = asNullableString(params[`${field}Operation`]) ?? asNullableString(params[`${field}_operation`]);
|
|
2161
2587
|
if (raw === "replace" || raw === "append" || raw === "remove") {
|
|
@@ -2194,11 +2620,32 @@ function extractUserIds(input) {
|
|
|
2194
2620
|
}
|
|
2195
2621
|
function extractProfilePatch(input) {
|
|
2196
2622
|
const patch = {};
|
|
2623
|
+
const name = input.match(/(?:name|姓名)[=:: ]([^,,]+)/i);
|
|
2624
|
+
const gender = input.match(/(?:gender|性别)[=:: ]([^,,]+)/i);
|
|
2625
|
+
const birthDate = input.match(/(?:birthdate|birthday|生日)[=:: ]([^,,]+)/i);
|
|
2626
|
+
const birthYear = input.match(/(?:birthyear|出生年份|出生年月)[=:: ]([^,,]+)/i);
|
|
2627
|
+
const age = input.match(/(?:age|年龄)[=:: ]([^,,]+)/i);
|
|
2197
2628
|
const nickname = input.match(/(?:nickname|称呼)[=:: ]([^,,]+)$/i) ?? input.match(/(?:nickname|称呼)[=:: ]([^,,]+)/i);
|
|
2198
2629
|
const role = input.match(/(?:role|职责|角色)[=:: ]([^,,]+)/i);
|
|
2199
2630
|
const preferences = input.match(/(?:preferences|偏好)[=:: ]([^,,]+)/i);
|
|
2200
2631
|
const personality = input.match(/(?:personality|性格)[=:: ]([^,,]+)/i);
|
|
2632
|
+
const interests = input.match(/(?:interests|兴趣|爱好)[=:: ]([^,,]+)/i);
|
|
2201
2633
|
const timezone = input.match(/(?:timezone|时区)[=:: ]([^,,]+)/i);
|
|
2634
|
+
if (name) {
|
|
2635
|
+
patch.name = name[1].trim();
|
|
2636
|
+
}
|
|
2637
|
+
if (gender) {
|
|
2638
|
+
patch.gender = gender[1].trim();
|
|
2639
|
+
}
|
|
2640
|
+
if (birthDate) {
|
|
2641
|
+
patch.birthDate = birthDate[1].trim();
|
|
2642
|
+
}
|
|
2643
|
+
if (birthYear) {
|
|
2644
|
+
patch.birthYear = birthYear[1].trim();
|
|
2645
|
+
}
|
|
2646
|
+
if (age) {
|
|
2647
|
+
patch.age = age[1].trim();
|
|
2648
|
+
}
|
|
2202
2649
|
if (nickname) {
|
|
2203
2650
|
patch.nickname = nickname[1].trim();
|
|
2204
2651
|
}
|
|
@@ -2211,6 +2658,9 @@ function extractProfilePatch(input) {
|
|
|
2211
2658
|
if (personality) {
|
|
2212
2659
|
patch.personality = personality[1].trim();
|
|
2213
2660
|
}
|
|
2661
|
+
if (interests) {
|
|
2662
|
+
patch.interests = interests[1].trim();
|
|
2663
|
+
}
|
|
2214
2664
|
if (timezone) {
|
|
2215
2665
|
patch.timezone = timezone[1].trim();
|
|
2216
2666
|
}
|
|
@@ -2224,6 +2674,18 @@ function renderIdentityContext(identity) {
|
|
|
2224
2674
|
if (identity.profile.name) {
|
|
2225
2675
|
lines.push(`Name: ${identity.profile.name}`);
|
|
2226
2676
|
}
|
|
2677
|
+
if (identity.profile.gender) {
|
|
2678
|
+
lines.push(`Gender: ${identity.profile.gender}`);
|
|
2679
|
+
}
|
|
2680
|
+
if (identity.profile.birthDate) {
|
|
2681
|
+
lines.push(`Birth date: ${identity.profile.birthDate}`);
|
|
2682
|
+
}
|
|
2683
|
+
if (identity.profile.birthYear) {
|
|
2684
|
+
lines.push(`Birth year: ${identity.profile.birthYear}`);
|
|
2685
|
+
}
|
|
2686
|
+
if (identity.profile.age) {
|
|
2687
|
+
lines.push(`Age: ${identity.profile.age}`);
|
|
2688
|
+
}
|
|
2227
2689
|
if (identity.profile.nickname) {
|
|
2228
2690
|
lines.push(`Preferred address: ${identity.profile.nickname}`);
|
|
2229
2691
|
}
|
|
@@ -2236,6 +2698,9 @@ function renderIdentityContext(identity) {
|
|
|
2236
2698
|
if (identity.profile.personality) {
|
|
2237
2699
|
lines.push(`Personality: ${identity.profile.personality}`);
|
|
2238
2700
|
}
|
|
2701
|
+
if (identity.profile.interests) {
|
|
2702
|
+
lines.push(`Interests: ${identity.profile.interests}`);
|
|
2703
|
+
}
|
|
2239
2704
|
if (identity.profile.role) {
|
|
2240
2705
|
lines.push(`Role: ${identity.profile.role}`);
|
|
2241
2706
|
}
|
|
@@ -2253,6 +2718,18 @@ function renderProfileMarkdown(profile) {
|
|
|
2253
2718
|
if (profile.name) {
|
|
2254
2719
|
frontmatterLines.push(`name: ${escapeFrontmatter(profile.name)}`);
|
|
2255
2720
|
}
|
|
2721
|
+
if (profile.gender) {
|
|
2722
|
+
frontmatterLines.push(`gender: ${escapeFrontmatter(profile.gender)}`);
|
|
2723
|
+
}
|
|
2724
|
+
if (profile.birthDate) {
|
|
2725
|
+
frontmatterLines.push(`birthDate: ${escapeFrontmatter(profile.birthDate)}`);
|
|
2726
|
+
}
|
|
2727
|
+
if (profile.birthYear) {
|
|
2728
|
+
frontmatterLines.push(`birthYear: ${escapeFrontmatter(profile.birthYear)}`);
|
|
2729
|
+
}
|
|
2730
|
+
if (profile.age) {
|
|
2731
|
+
frontmatterLines.push(`age: ${escapeFrontmatter(profile.age)}`);
|
|
2732
|
+
}
|
|
2256
2733
|
if (profile.nickname) {
|
|
2257
2734
|
frontmatterLines.push(`nickname: ${escapeFrontmatter(profile.nickname)}`);
|
|
2258
2735
|
}
|
|
@@ -2265,6 +2742,9 @@ function renderProfileMarkdown(profile) {
|
|
|
2265
2742
|
if (profile.personality) {
|
|
2266
2743
|
frontmatterLines.push(`personality: ${escapeFrontmatter(profile.personality)}`);
|
|
2267
2744
|
}
|
|
2745
|
+
if (profile.interests) {
|
|
2746
|
+
frontmatterLines.push(`interests: ${escapeFrontmatter(profile.interests)}`);
|
|
2747
|
+
}
|
|
2268
2748
|
if (profile.role) {
|
|
2269
2749
|
frontmatterLines.push(`role: ${escapeFrontmatter(profile.role)}`);
|
|
2270
2750
|
}
|
|
@@ -2274,10 +2754,15 @@ function renderProfileMarkdown(profile) {
|
|
|
2274
2754
|
`updatedAt: ${escapeFrontmatter(profile.updatedAt)}`,
|
|
2275
2755
|
`syncHash: ${escapeFrontmatter(computeProfilePayloadHash({
|
|
2276
2756
|
name: profile.name,
|
|
2757
|
+
gender: profile.gender,
|
|
2758
|
+
birthDate: profile.birthDate,
|
|
2759
|
+
birthYear: profile.birthYear,
|
|
2760
|
+
age: profile.age,
|
|
2277
2761
|
nickname: profile.nickname,
|
|
2278
2762
|
timezone: profile.timezone,
|
|
2279
2763
|
preferences: profile.preferences,
|
|
2280
2764
|
personality: profile.personality,
|
|
2765
|
+
interests: profile.interests,
|
|
2281
2766
|
role: profile.role,
|
|
2282
2767
|
visibility: profile.visibility
|
|
2283
2768
|
}, notes))}`,
|
|
@@ -2305,6 +2790,18 @@ function renderConfirmedProfileSection(profile) {
|
|
|
2305
2790
|
if (profile.name) {
|
|
2306
2791
|
rows.push(`- \u59D3\u540D\uFF1A${profile.name}`);
|
|
2307
2792
|
}
|
|
2793
|
+
if (profile.gender) {
|
|
2794
|
+
rows.push(`- \u6027\u522B\uFF1A${profile.gender}`);
|
|
2795
|
+
}
|
|
2796
|
+
if (profile.birthDate) {
|
|
2797
|
+
rows.push(`- \u751F\u65E5\uFF1A${profile.birthDate}`);
|
|
2798
|
+
}
|
|
2799
|
+
if (profile.birthYear) {
|
|
2800
|
+
rows.push(`- \u51FA\u751F\u5E74\u4EFD\uFF1A${profile.birthYear}`);
|
|
2801
|
+
}
|
|
2802
|
+
if (profile.age) {
|
|
2803
|
+
rows.push(`- \u5E74\u9F84\uFF1A${profile.age}`);
|
|
2804
|
+
}
|
|
2308
2805
|
if (profile.nickname) {
|
|
2309
2806
|
rows.push(`- \u79F0\u547C\uFF1A${profile.nickname}`);
|
|
2310
2807
|
}
|
|
@@ -2314,6 +2811,9 @@ function renderConfirmedProfileSection(profile) {
|
|
|
2314
2811
|
if (profile.personality) {
|
|
2315
2812
|
rows.push(`- \u98CE\u683C\u504F\u597D\uFF1A${profile.personality}`);
|
|
2316
2813
|
}
|
|
2814
|
+
if (profile.interests) {
|
|
2815
|
+
rows.push(`- \u5174\u8DA3\u7231\u597D\uFF1A${profile.interests}`);
|
|
2816
|
+
}
|
|
2317
2817
|
if (profile.role) {
|
|
2318
2818
|
rows.push(`- \u89D2\u8272\u8EAB\u4EFD\uFF1A${profile.role}`);
|
|
2319
2819
|
}
|
|
@@ -2325,10 +2825,15 @@ function renderConfirmedProfileSection(profile) {
|
|
|
2325
2825
|
function computeProfilePayloadHash(patch, notes) {
|
|
2326
2826
|
return hashId(JSON.stringify({
|
|
2327
2827
|
name: patch.name ?? null,
|
|
2828
|
+
gender: patch.gender ?? null,
|
|
2829
|
+
birthDate: patch.birthDate ?? null,
|
|
2830
|
+
birthYear: patch.birthYear ?? null,
|
|
2831
|
+
age: patch.age ?? null,
|
|
2328
2832
|
nickname: patch.nickname ?? null,
|
|
2329
2833
|
timezone: patch.timezone ?? null,
|
|
2330
2834
|
preferences: patch.preferences ?? null,
|
|
2331
2835
|
personality: patch.personality ?? null,
|
|
2836
|
+
interests: patch.interests ?? null,
|
|
2332
2837
|
role: patch.role ?? null,
|
|
2333
2838
|
visibility: patch.visibility ?? "private",
|
|
2334
2839
|
notes: sanitizeProfileNotes(notes) ?? null
|
|
@@ -2383,6 +2888,14 @@ function applyFrontmatterField(patch, key, value) {
|
|
|
2383
2888
|
const normalized = value === "null" ? null : value;
|
|
2384
2889
|
if (key === "name") {
|
|
2385
2890
|
patch.name = normalized;
|
|
2891
|
+
} else if (key === "gender") {
|
|
2892
|
+
patch.gender = normalized;
|
|
2893
|
+
} else if (key === "birthDate") {
|
|
2894
|
+
patch.birthDate = normalized;
|
|
2895
|
+
} else if (key === "birthYear") {
|
|
2896
|
+
patch.birthYear = normalized;
|
|
2897
|
+
} else if (key === "age") {
|
|
2898
|
+
patch.age = normalized;
|
|
2386
2899
|
} else if (key === "nickname") {
|
|
2387
2900
|
patch.nickname = normalized;
|
|
2388
2901
|
} else if (key === "timezone") {
|
|
@@ -2391,6 +2904,8 @@ function applyFrontmatterField(patch, key, value) {
|
|
|
2391
2904
|
patch.preferences = normalized;
|
|
2392
2905
|
} else if (key === "personality") {
|
|
2393
2906
|
patch.personality = normalized;
|
|
2907
|
+
} else if (key === "interests") {
|
|
2908
|
+
patch.interests = normalized;
|
|
2394
2909
|
} else if (key === "role") {
|
|
2395
2910
|
patch.role = normalized;
|
|
2396
2911
|
} else if (key === "visibility") {
|
|
@@ -2441,13 +2956,72 @@ function asTextResult(value) {
|
|
|
2441
2956
|
function asNullableString(value) {
|
|
2442
2957
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
2443
2958
|
}
|
|
2959
|
+
function escapeRegExp(value) {
|
|
2960
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
2961
|
+
}
|
|
2962
|
+
function expandHomePath(value) {
|
|
2963
|
+
const text = asNullableString(value);
|
|
2964
|
+
if (!text) {
|
|
2965
|
+
return null;
|
|
2966
|
+
}
|
|
2967
|
+
if (text === "~") {
|
|
2968
|
+
return (0, import_node_os.homedir)();
|
|
2969
|
+
}
|
|
2970
|
+
if (text.startsWith("~/")) {
|
|
2971
|
+
return (0, import_node_path.join)((0, import_node_os.homedir)(), text.slice(2));
|
|
2972
|
+
}
|
|
2973
|
+
return text;
|
|
2974
|
+
}
|
|
2975
|
+
function computeSemanticProfileRetryDelayMs(attempt) {
|
|
2976
|
+
const base = Number(process.env.BAMDRA_USER_BIND_RETRY_BASE_MS ?? 5e3);
|
|
2977
|
+
const safeBase = Number.isFinite(base) && base > 0 ? base : 5e3;
|
|
2978
|
+
return safeBase * Math.max(1, attempt);
|
|
2979
|
+
}
|
|
2980
|
+
function buildPendingSemanticRefineNote(messageText, fingerprint) {
|
|
2981
|
+
const compact = messageText.replace(/\s+/g, " ").trim().slice(0, 120);
|
|
2982
|
+
return `[pending-profile-refine:${fingerprint}] ${compact}`;
|
|
2983
|
+
}
|
|
2984
|
+
function extractPendingSemanticRefineEntries(notes) {
|
|
2985
|
+
const value = sanitizeProfileNotes(notes);
|
|
2986
|
+
if (!value) {
|
|
2987
|
+
return [];
|
|
2988
|
+
}
|
|
2989
|
+
const entries = [];
|
|
2990
|
+
const pattern = /\[pending-profile-refine:([a-f0-9]+)\]\s*([^\n]+)/g;
|
|
2991
|
+
for (const match of value.matchAll(pattern)) {
|
|
2992
|
+
const fingerprint = match[1]?.trim();
|
|
2993
|
+
const messageText = match[2]?.trim();
|
|
2994
|
+
if (!fingerprint || !messageText) {
|
|
2995
|
+
continue;
|
|
2996
|
+
}
|
|
2997
|
+
entries.push({ fingerprint, messageText });
|
|
2998
|
+
}
|
|
2999
|
+
return entries;
|
|
3000
|
+
}
|
|
3001
|
+
function removePendingSemanticRefineEntry(notes, fingerprint) {
|
|
3002
|
+
const value = sanitizeProfileNotes(notes);
|
|
3003
|
+
if (!value) {
|
|
3004
|
+
return null;
|
|
3005
|
+
}
|
|
3006
|
+
const pattern = new RegExp(`(?:^|\\n)\\[pending-profile-refine:${escapeRegExp(fingerprint)}\\][^\\n]*(?=\\n|$)`, "g");
|
|
3007
|
+
const cleaned = value.replace(pattern, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
3008
|
+
return cleaned || null;
|
|
3009
|
+
}
|
|
2444
3010
|
async function inferSemanticProfileExtraction(messageText, currentProfile) {
|
|
2445
3011
|
const model = readProfileExtractorModelFromOpenClawConfig();
|
|
2446
3012
|
if (!model) {
|
|
2447
3013
|
return null;
|
|
2448
3014
|
}
|
|
3015
|
+
logUserBindEvent("semantic-profile-extractor-request", {
|
|
3016
|
+
providerId: model.providerId,
|
|
3017
|
+
modelId: model.modelId,
|
|
3018
|
+
baseUrl: model.baseUrl,
|
|
3019
|
+
timeoutMs: SEMANTIC_PROFILE_CAPTURE_TIMEOUT_MS,
|
|
3020
|
+
messageChars: messageText.length
|
|
3021
|
+
});
|
|
2449
3022
|
const response = await fetch(`${model.baseUrl}/chat/completions`, {
|
|
2450
3023
|
method: "POST",
|
|
3024
|
+
signal: AbortSignal.timeout(SEMANTIC_PROFILE_CAPTURE_TIMEOUT_MS),
|
|
2451
3025
|
headers: {
|
|
2452
3026
|
authorization: `Bearer ${model.apiKey}`,
|
|
2453
3027
|
"content-type": "application/json; charset=utf-8"
|
|
@@ -2461,32 +3035,38 @@ async function inferSemanticProfileExtraction(messageText, currentProfile) {
|
|
|
2461
3035
|
{
|
|
2462
3036
|
role: "system",
|
|
2463
3037
|
content: [
|
|
2464
|
-
"You extract stable user-profile information from a single user message.",
|
|
3038
|
+
"You extract stable user-profile information from a single user message, a short window of recent user messages, or a brief assistant-question plus user-answer exchange from the same conversation.",
|
|
2465
3039
|
"Return JSON only.",
|
|
2466
|
-
"Only capture durable, reusable
|
|
3040
|
+
"Only capture durable, reusable identity facts, preferences, and personal profile details that should affect future conversations.",
|
|
2467
3041
|
"Ignore transient task requirements, one-off requests, or speculative guesses.",
|
|
2468
|
-
"Prefer precision, but do not miss clear self-descriptions or
|
|
2469
|
-
"Allowed fields: nickname, preferences, personality, role, timezone, notes.",
|
|
3042
|
+
"Prefer precision, but do not miss clear self-descriptions or durable profile facts.",
|
|
3043
|
+
"Allowed fields: name, gender, birthDate, birthYear, age, nickname, preferences, personality, interests, role, timezone, notes.",
|
|
2470
3044
|
"For each field, also decide the update operation: replace, append, or remove.",
|
|
2471
|
-
"
|
|
2472
|
-
"
|
|
2473
|
-
"
|
|
2474
|
-
"Use
|
|
2475
|
-
"Use
|
|
2476
|
-
"
|
|
2477
|
-
"If
|
|
2478
|
-
"
|
|
2479
|
-
"Do not copy placeholders, examples, or template language.",
|
|
2480
|
-
'Return exactly this shape: {"should_update":boolean,"confidence":number,"patch":{"nickname":string?,"preferences":string?,"personality":string?,"role":string?,"timezone":string?,"notes":string?},"operations":{"nickname":"replace|append|remove"?,"preferences":"replace|append|remove"?,"personality":"replace|append|remove"?,"role":"replace|append|remove"?,"timezone":"replace|append|remove"?,"notes":"replace|append|remove"?}}'
|
|
3045
|
+
"Use the structured fields whenever possible instead of dumping transcripts into notes.",
|
|
3046
|
+
"notes is only for durable boundaries or habits that do not fit the structured fields cleanly.",
|
|
3047
|
+
"Use append when the user adds another stable fact or preference without revoking the old one.",
|
|
3048
|
+
"Use replace when the user corrects or changes an existing stable fact or preference.",
|
|
3049
|
+
"Use remove when the user clearly asks to drop a specific old fact or preference.",
|
|
3050
|
+
"Do not require rigid trigger phrases. Judge by meaning, not literal wording.",
|
|
3051
|
+
"If the input includes a recent assistant profile-collection question followed by a short user answer, use that dialogue context to resolve what the user meant.",
|
|
3052
|
+
"Treat meta-instructions about saving or updating the profile as control signals, not profile content themselves.",
|
|
3053
|
+
"Do not copy placeholders, examples, or template language into the patch.",
|
|
3054
|
+
'Return exactly this shape: {"should_update":boolean,"confidence":number,"patch":{"name":string?,"gender":string?,"birthDate":string?,"birthYear":string?,"age":string?,"nickname":string?,"preferences":string?,"personality":string?,"interests":string?,"role":string?,"timezone":string?,"notes":string?},"operations":{"name":"replace|append|remove"?,"gender":"replace|append|remove"?,"birthDate":"replace|append|remove"?,"birthYear":"replace|append|remove"?,"age":"replace|append|remove"?,"nickname":"replace|append|remove"?,"preferences":"replace|append|remove"?,"personality":"replace|append|remove"?,"interests":"replace|append|remove"?,"role":"replace|append|remove"?,"timezone":"replace|append|remove"?,"notes":"replace|append|remove"?}}'
|
|
2481
3055
|
].join("\n")
|
|
2482
3056
|
},
|
|
2483
3057
|
{
|
|
2484
3058
|
role: "user",
|
|
2485
3059
|
content: JSON.stringify({
|
|
2486
3060
|
current_profile: {
|
|
3061
|
+
name: currentProfile.name,
|
|
3062
|
+
gender: currentProfile.gender,
|
|
3063
|
+
birthDate: currentProfile.birthDate,
|
|
3064
|
+
birthYear: currentProfile.birthYear,
|
|
3065
|
+
age: currentProfile.age,
|
|
2487
3066
|
nickname: currentProfile.nickname,
|
|
2488
3067
|
preferences: currentProfile.preferences,
|
|
2489
3068
|
personality: currentProfile.personality,
|
|
3069
|
+
interests: currentProfile.interests,
|
|
2490
3070
|
role: currentProfile.role,
|
|
2491
3071
|
timezone: currentProfile.timezone,
|
|
2492
3072
|
notes: currentProfile.notes
|
|
@@ -2546,9 +3126,15 @@ function parseSemanticExtractionResult(content) {
|
|
|
2546
3126
|
shouldUpdate: parsed.should_update === true || parsed.shouldUpdate === true,
|
|
2547
3127
|
confidence: Number(parsed.confidence ?? 0),
|
|
2548
3128
|
patch: {
|
|
3129
|
+
name: asNullableString(patchInput.name),
|
|
3130
|
+
gender: asNullableString(patchInput.gender),
|
|
3131
|
+
birthDate: asNullableString(patchInput.birthDate),
|
|
3132
|
+
birthYear: asNullableString(patchInput.birthYear),
|
|
3133
|
+
age: asNullableString(patchInput.age),
|
|
2549
3134
|
nickname: asNullableString(patchInput.nickname),
|
|
2550
3135
|
preferences: asNullableString(patchInput.preferences),
|
|
2551
3136
|
personality: asNullableString(patchInput.personality),
|
|
3137
|
+
interests: asNullableString(patchInput.interests),
|
|
2552
3138
|
role: asNullableString(patchInput.role),
|
|
2553
3139
|
timezone: asNullableString(patchInput.timezone),
|
|
2554
3140
|
notes: asNullableString(patchInput.notes)
|
|
@@ -2561,7 +3147,7 @@ function parseSemanticExtractionResult(content) {
|
|
|
2561
3147
|
}
|
|
2562
3148
|
function parseProfilePatchOperations(input) {
|
|
2563
3149
|
const operations = {};
|
|
2564
|
-
for (const field of ["nickname", "preferences", "personality", "role", "timezone", "notes"]) {
|
|
3150
|
+
for (const field of ["name", "gender", "birthDate", "birthYear", "age", "nickname", "preferences", "personality", "interests", "role", "timezone", "notes"]) {
|
|
2565
3151
|
const raw = asNullableString(input[field]);
|
|
2566
3152
|
if (raw === "replace" || raw === "append" || raw === "remove") {
|
|
2567
3153
|
operations[field] = raw;
|
|
@@ -2588,12 +3174,48 @@ function extractJsonObject(content) {
|
|
|
2588
3174
|
function cleanupSemanticProfilePatch(patch, currentProfile, operations) {
|
|
2589
3175
|
const next = {};
|
|
2590
3176
|
const nextOperations = {};
|
|
3177
|
+
const name = asNullableString(patch.name);
|
|
3178
|
+
const gender = asNullableString(patch.gender);
|
|
3179
|
+
const birthDate = asNullableString(patch.birthDate);
|
|
3180
|
+
const birthYear = asNullableString(patch.birthYear);
|
|
3181
|
+
const age = asNullableString(patch.age);
|
|
2591
3182
|
const nickname = asNullableString(patch.nickname);
|
|
2592
3183
|
const preferences = asNullableString(patch.preferences);
|
|
2593
3184
|
const personality = asNullableString(patch.personality);
|
|
3185
|
+
const interests = asNullableString(patch.interests);
|
|
2594
3186
|
const role = asNullableString(patch.role);
|
|
2595
3187
|
const timezone = asNullableString(patch.timezone);
|
|
2596
3188
|
const notes = asNullableString(patch.notes);
|
|
3189
|
+
if (name && name !== currentProfile.name) {
|
|
3190
|
+
next.name = name;
|
|
3191
|
+
if (operations?.name) {
|
|
3192
|
+
nextOperations.name = operations.name;
|
|
3193
|
+
}
|
|
3194
|
+
}
|
|
3195
|
+
if (gender && gender !== currentProfile.gender) {
|
|
3196
|
+
next.gender = gender;
|
|
3197
|
+
if (operations?.gender) {
|
|
3198
|
+
nextOperations.gender = operations.gender;
|
|
3199
|
+
}
|
|
3200
|
+
}
|
|
3201
|
+
if (birthDate && birthDate !== currentProfile.birthDate) {
|
|
3202
|
+
next.birthDate = birthDate;
|
|
3203
|
+
if (operations?.birthDate) {
|
|
3204
|
+
nextOperations.birthDate = operations.birthDate;
|
|
3205
|
+
}
|
|
3206
|
+
}
|
|
3207
|
+
if (birthYear && birthYear !== currentProfile.birthYear) {
|
|
3208
|
+
next.birthYear = birthYear;
|
|
3209
|
+
if (operations?.birthYear) {
|
|
3210
|
+
nextOperations.birthYear = operations.birthYear;
|
|
3211
|
+
}
|
|
3212
|
+
}
|
|
3213
|
+
if (age && age !== currentProfile.age) {
|
|
3214
|
+
next.age = age;
|
|
3215
|
+
if (operations?.age) {
|
|
3216
|
+
nextOperations.age = operations.age;
|
|
3217
|
+
}
|
|
3218
|
+
}
|
|
2597
3219
|
if (nickname && nickname !== currentProfile.nickname) {
|
|
2598
3220
|
next.nickname = nickname;
|
|
2599
3221
|
if (operations?.nickname) {
|
|
@@ -2612,6 +3234,12 @@ function cleanupSemanticProfilePatch(patch, currentProfile, operations) {
|
|
|
2612
3234
|
nextOperations.personality = operations.personality;
|
|
2613
3235
|
}
|
|
2614
3236
|
}
|
|
3237
|
+
if (interests && interests !== currentProfile.interests) {
|
|
3238
|
+
next.interests = interests;
|
|
3239
|
+
if (operations?.interests) {
|
|
3240
|
+
nextOperations.interests = operations.interests;
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
2615
3243
|
if (role && role !== currentProfile.role) {
|
|
2616
3244
|
next.role = role;
|
|
2617
3245
|
if (operations?.role) {
|
|
@@ -2637,7 +3265,7 @@ function cleanupSemanticProfilePatch(patch, currentProfile, operations) {
|
|
|
2637
3265
|
}
|
|
2638
3266
|
function applyProfilePatchOperations(currentProfile, patch, operations) {
|
|
2639
3267
|
const next = {};
|
|
2640
|
-
const fields = ["nickname", "preferences", "personality", "role", "timezone", "notes"];
|
|
3268
|
+
const fields = ["name", "gender", "birthDate", "birthYear", "age", "nickname", "preferences", "personality", "interests", "role", "timezone", "notes"];
|
|
2641
3269
|
for (const field of fields) {
|
|
2642
3270
|
if (patch[field] === void 0) {
|
|
2643
3271
|
continue;
|
|
@@ -2653,7 +3281,7 @@ function applyProfilePatchOperations(currentProfile, patch, operations) {
|
|
|
2653
3281
|
return next;
|
|
2654
3282
|
}
|
|
2655
3283
|
function defaultProfileFieldOperation(field) {
|
|
2656
|
-
if (field === "notes") {
|
|
3284
|
+
if (field === "notes" || field === "interests") {
|
|
2657
3285
|
return "append";
|
|
2658
3286
|
}
|
|
2659
3287
|
return "replace";
|
|
@@ -2729,60 +3357,6 @@ function ensureArrayIncludes(parent, key, value) {
|
|
|
2729
3357
|
parent[key] = current;
|
|
2730
3358
|
return true;
|
|
2731
3359
|
}
|
|
2732
|
-
function extractScopes(result) {
|
|
2733
|
-
const candidates = [
|
|
2734
|
-
findNestedValue(result, ["data", "scopes"]),
|
|
2735
|
-
findNestedValue(result, ["scopes"]),
|
|
2736
|
-
findNestedValue(result, ["data", "items"])
|
|
2737
|
-
];
|
|
2738
|
-
for (const candidate of candidates) {
|
|
2739
|
-
if (!Array.isArray(candidate)) {
|
|
2740
|
-
continue;
|
|
2741
|
-
}
|
|
2742
|
-
const scopes = candidate.map((item) => {
|
|
2743
|
-
if (typeof item === "string") {
|
|
2744
|
-
return item;
|
|
2745
|
-
}
|
|
2746
|
-
if (item && typeof item === "object") {
|
|
2747
|
-
const record = item;
|
|
2748
|
-
const scope = record.scope ?? record.name;
|
|
2749
|
-
return typeof scope === "string" ? scope : "";
|
|
2750
|
-
}
|
|
2751
|
-
return "";
|
|
2752
|
-
}).filter(Boolean);
|
|
2753
|
-
if (scopes.length > 0) {
|
|
2754
|
-
return scopes;
|
|
2755
|
-
}
|
|
2756
|
-
}
|
|
2757
|
-
return [];
|
|
2758
|
-
}
|
|
2759
|
-
function findBitableRecordId(result, userId) {
|
|
2760
|
-
const candidates = [
|
|
2761
|
-
findNestedValue(result, ["data", "items"]),
|
|
2762
|
-
findNestedValue(result, ["items"]),
|
|
2763
|
-
findNestedValue(result, ["data", "records"])
|
|
2764
|
-
];
|
|
2765
|
-
for (const candidate of candidates) {
|
|
2766
|
-
if (!Array.isArray(candidate)) {
|
|
2767
|
-
continue;
|
|
2768
|
-
}
|
|
2769
|
-
for (const item of candidate) {
|
|
2770
|
-
if (!item || typeof item !== "object") {
|
|
2771
|
-
continue;
|
|
2772
|
-
}
|
|
2773
|
-
const record = item;
|
|
2774
|
-
const fields = record.fields && typeof record.fields === "object" ? record.fields : {};
|
|
2775
|
-
if (String(fields.user_id ?? "") !== userId) {
|
|
2776
|
-
continue;
|
|
2777
|
-
}
|
|
2778
|
-
const recordId = record.record_id ?? record.recordId ?? record.id;
|
|
2779
|
-
if (typeof recordId === "string" && recordId.trim()) {
|
|
2780
|
-
return recordId;
|
|
2781
|
-
}
|
|
2782
|
-
}
|
|
2783
|
-
}
|
|
2784
|
-
return null;
|
|
2785
|
-
}
|
|
2786
3360
|
function readFeishuAccountsFromOpenClawConfig() {
|
|
2787
3361
|
const openclawConfigPath = (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "openclaw.json");
|
|
2788
3362
|
if (!(0, import_node_fs.existsSync)(openclawConfigPath)) {
|
|
@@ -2818,27 +3392,37 @@ function readProfileExtractorModelFromOpenClawConfig() {
|
|
|
2818
3392
|
const agents = parsed.agents && typeof parsed.agents === "object" ? parsed.agents : {};
|
|
2819
3393
|
const defaults = agents.defaults && typeof agents.defaults === "object" ? agents.defaults : {};
|
|
2820
3394
|
const defaultModel = defaults.model && typeof defaults.model === "object" ? defaults.model : {};
|
|
2821
|
-
const
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
3395
|
+
const candidates = [
|
|
3396
|
+
...readConfiguredModelRefs(defaultModel.fallback),
|
|
3397
|
+
...readConfiguredModelRefs(defaultModel.fallbacks),
|
|
3398
|
+
...readConfiguredModelRefs(models.fallback),
|
|
3399
|
+
...readConfiguredModelRefs(models.fallbacks),
|
|
3400
|
+
...readConfiguredModelRefs(defaultModel.primary),
|
|
3401
|
+
...readConfiguredModelRefs(models.primary)
|
|
3402
|
+
];
|
|
3403
|
+
for (const configuredModel of candidates) {
|
|
3404
|
+
if (!configuredModel.includes("/")) {
|
|
3405
|
+
continue;
|
|
3406
|
+
}
|
|
3407
|
+
const [providerId, modelId] = configuredModel.split("/", 2);
|
|
3408
|
+
const provider = providers[providerId] && typeof providers[providerId] === "object" ? providers[providerId] : null;
|
|
3409
|
+
if (!provider) {
|
|
3410
|
+
continue;
|
|
3411
|
+
}
|
|
3412
|
+
const api = asNullableString(provider.api);
|
|
3413
|
+
const baseUrl = asNullableString(provider.baseUrl) ?? asNullableString(provider.baseURL);
|
|
3414
|
+
const apiKey = asNullableString(provider.apiKey);
|
|
3415
|
+
if (api !== "openai-completions" || !baseUrl || !apiKey) {
|
|
3416
|
+
continue;
|
|
3417
|
+
}
|
|
3418
|
+
return {
|
|
3419
|
+
providerId,
|
|
3420
|
+
modelId,
|
|
3421
|
+
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
3422
|
+
apiKey
|
|
3423
|
+
};
|
|
2835
3424
|
}
|
|
2836
|
-
return
|
|
2837
|
-
providerId,
|
|
2838
|
-
modelId,
|
|
2839
|
-
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
2840
|
-
apiKey
|
|
2841
|
-
};
|
|
3425
|
+
return null;
|
|
2842
3426
|
} catch (error) {
|
|
2843
3427
|
logUserBindEvent("profile-extractor-config-read-failed", {
|
|
2844
3428
|
message: error instanceof Error ? error.message : String(error)
|
|
@@ -2846,6 +3430,39 @@ function readProfileExtractorModelFromOpenClawConfig() {
|
|
|
2846
3430
|
return null;
|
|
2847
3431
|
}
|
|
2848
3432
|
}
|
|
3433
|
+
function readSemanticProfileCaptureTimeoutMs() {
|
|
3434
|
+
const raw = Number(process.env.BAMDRA_USER_BIND_SEMANTIC_TIMEOUT_MS ?? "12000");
|
|
3435
|
+
if (!Number.isFinite(raw)) {
|
|
3436
|
+
return 12e3;
|
|
3437
|
+
}
|
|
3438
|
+
return Math.max(2500, Math.floor(raw));
|
|
3439
|
+
}
|
|
3440
|
+
function isUserBindRuntimeLike(value) {
|
|
3441
|
+
if (!value || typeof value !== "object") {
|
|
3442
|
+
return false;
|
|
3443
|
+
}
|
|
3444
|
+
const record = value;
|
|
3445
|
+
return record[GLOBAL_RUNTIME_BRAND_KEY] === true && typeof record.register === "function" && typeof record.close === "function";
|
|
3446
|
+
}
|
|
3447
|
+
function getGlobalPendingSemanticRefines() {
|
|
3448
|
+
const globalRecord = globalThis;
|
|
3449
|
+
const existing = globalRecord[GLOBAL_PENDING_REFINE_KEY];
|
|
3450
|
+
if (existing instanceof Set) {
|
|
3451
|
+
return existing;
|
|
3452
|
+
}
|
|
3453
|
+
const created = /* @__PURE__ */ new Set();
|
|
3454
|
+
globalRecord[GLOBAL_PENDING_REFINE_KEY] = created;
|
|
3455
|
+
return created;
|
|
3456
|
+
}
|
|
3457
|
+
function readConfiguredModelRefs(value) {
|
|
3458
|
+
if (typeof value === "string") {
|
|
3459
|
+
return value.trim() ? [value.trim()] : [];
|
|
3460
|
+
}
|
|
3461
|
+
if (Array.isArray(value)) {
|
|
3462
|
+
return value.map((item) => asNullableString(item)).filter((item) => Boolean(item));
|
|
3463
|
+
}
|
|
3464
|
+
return [];
|
|
3465
|
+
}
|
|
2849
3466
|
function normalizeFeishuAccount(accountId, input, fallback) {
|
|
2850
3467
|
const record = input && typeof input === "object" ? input : {};
|
|
2851
3468
|
const enabled = record.enabled !== false && fallback.enabled !== false;
|