@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/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 REQUIRED_FEISHU_IDENTITY_SCOPES = [
68
- "contact:user.employee_id:readonly",
69
- "contact:user.base:readonly"
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
- const remote = await this.tryResolveFeishuUser(parsed.openId);
647
- if (remote?.userId) {
648
- userId = remote.userId;
649
- source = remote.source;
650
- remoteProfilePatch = remote.profilePatch;
651
- } else if (remote?.source === "feishu-contact-scope-missing") {
652
- source = remote.source;
653
- remoteProfilePatch = remote.profilePatch;
654
- this.enqueuePendingBindingResolution(parsed.channelType, parsed.openId, remote.source);
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.store.recordIssue("feishu-resolution", `Failed to resolve real user id for ${parsed.openId}`);
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 messageText = extractUserUtterance(context);
888
- if (!messageText) {
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 fingerprint = hashId(`${identity.sessionId}:${messageText}`);
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: identity.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: identity.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: identity.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: identity.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 nickname, timezone, and stable preferences can be used naturally in the response",
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 preferences into their profile when they clearly provide them, such as nickname, communication style, timezone, role, or durable notes",
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
- return new UserBindRuntime(api, api.pluginConfig ?? api.config ?? api.plugin?.config);
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 traits or preferences that should affect future conversations.",
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 clear addressing / style preferences.",
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
- "notes is only for durable boundaries or habits that do not fit the structured fields.",
2472
- "A preferred form of address counts as nickname even when phrased naturally, for example: '\u4EE5\u540E\u53EB\u6211\u4E30\u54E5', '\u53EB\u6211\u4EB2\u7231\u7684\u8001\u5927', '\u4F60\u5C31\u53EB\u6211\u963F\u4E30'.",
2473
- "A reusable response preference counts as preferences, for example: '\u5148\u7ED9\u7ED3\u8BBA\u518D\u5C55\u5F00', '\u8BF4\u8BDD\u76F4\u63A5\u4E00\u70B9\u4F46\u522B\u592A\u786C', '\u522B\u592A\u5B98\u65B9'.",
2474
- "Use append when the user adds another stable preference without revoking the old one.",
2475
- "Use replace when the user corrects or changes an existing stable preference.",
2476
- "Use remove when the user clearly asks to drop a specific old preference or trait.",
2477
- "If a message clearly contains both an addressing preference and a communication preference, capture both.",
2478
- "Do not require rigid trigger phrases. Judge by meaning.",
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 configuredModel = asNullableString(defaultModel.primary) ?? asNullableString(models.primary);
2822
- if (!configuredModel || !configuredModel.includes("/")) {
2823
- return null;
2824
- }
2825
- const [providerId, modelId] = configuredModel.split("/", 2);
2826
- const provider = providers[providerId] && typeof providers[providerId] === "object" ? providers[providerId] : null;
2827
- if (!provider) {
2828
- return null;
2829
- }
2830
- const api = asNullableString(provider.api);
2831
- const baseUrl = asNullableString(provider.baseUrl) ?? asNullableString(provider.baseURL);
2832
- const apiKey = asNullableString(provider.apiKey);
2833
- if (api !== "openai-completions" || !baseUrl || !apiKey) {
2834
- return null;
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;