@bamdra/bamdra-user-bind 0.1.12 → 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 +34 -0
- package/README.zh-CN.md +34 -0
- package/dist/index.js +2410 -413
- package/dist/openclaw.plugin.json +1 -1
- package/dist/package.json +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/bamdra-user-bind-admin/SKILL.md +6 -3
- package/skills/bamdra-user-bind-profile/SKILL.md +123 -2
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 = [
|
|
@@ -46,6 +49,11 @@ var SELF_TOOL_NAMES = [
|
|
|
46
49
|
"bamdra_user_bind_update_my_profile",
|
|
47
50
|
"bamdra_user_bind_refresh_my_binding"
|
|
48
51
|
];
|
|
52
|
+
var SELF_TOOL_ALIASES = [
|
|
53
|
+
"user_bind_get_my_profile",
|
|
54
|
+
"user_bind_update_my_profile",
|
|
55
|
+
"user_bind_refresh_my_binding"
|
|
56
|
+
];
|
|
49
57
|
var ADMIN_TOOL_NAMES = [
|
|
50
58
|
"bamdra_user_bind_admin_query",
|
|
51
59
|
"bamdra_user_bind_admin_edit",
|
|
@@ -59,10 +67,22 @@ var TABLES = {
|
|
|
59
67
|
issues: "bamdra_user_bind_issues",
|
|
60
68
|
audits: "bamdra_user_bind_audits"
|
|
61
69
|
};
|
|
62
|
-
var
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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;
|
|
75
|
+
var CHANNELS_WITH_NATIVE_STABLE_IDS = /* @__PURE__ */ new Set([
|
|
76
|
+
"telegram",
|
|
77
|
+
"whatsapp",
|
|
78
|
+
"discord",
|
|
79
|
+
"googlechat",
|
|
80
|
+
"slack",
|
|
81
|
+
"mattermost",
|
|
82
|
+
"signal",
|
|
83
|
+
"imessage",
|
|
84
|
+
"msteams"
|
|
85
|
+
]);
|
|
66
86
|
function logUserBindEvent(event, details = {}) {
|
|
67
87
|
try {
|
|
68
88
|
console.info("[bamdra-user-bind]", event, JSON.stringify(details));
|
|
@@ -84,11 +104,15 @@ var UserBindStore = class {
|
|
|
84
104
|
user_id TEXT PRIMARY KEY,
|
|
85
105
|
name TEXT,
|
|
86
106
|
gender TEXT,
|
|
107
|
+
birth_date TEXT,
|
|
108
|
+
birth_year TEXT,
|
|
109
|
+
age TEXT,
|
|
87
110
|
email TEXT,
|
|
88
111
|
avatar TEXT,
|
|
89
112
|
nickname TEXT,
|
|
90
113
|
preferences TEXT,
|
|
91
114
|
personality TEXT,
|
|
115
|
+
interests TEXT,
|
|
92
116
|
role TEXT,
|
|
93
117
|
timezone TEXT,
|
|
94
118
|
notes TEXT,
|
|
@@ -130,6 +154,11 @@ var UserBindStore = class {
|
|
|
130
154
|
`);
|
|
131
155
|
this.ensureColumn(TABLES.profiles, "timezone", "TEXT");
|
|
132
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");
|
|
161
|
+
this.migrateChannelScopedUserIds();
|
|
133
162
|
}
|
|
134
163
|
db;
|
|
135
164
|
markdownSyncing = /* @__PURE__ */ new Set();
|
|
@@ -140,6 +169,16 @@ var UserBindStore = class {
|
|
|
140
169
|
this.syncMarkdownToStore(userId);
|
|
141
170
|
return this.getProfileFromDatabase(userId);
|
|
142
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
|
+
}
|
|
143
182
|
findBinding(channelType, openId) {
|
|
144
183
|
if (!openId) {
|
|
145
184
|
return null;
|
|
@@ -156,6 +195,22 @@ var UserBindStore = class {
|
|
|
156
195
|
source: row.source
|
|
157
196
|
};
|
|
158
197
|
}
|
|
198
|
+
reconcileProvisionalIdentity(channelType, openId, stableUserId) {
|
|
199
|
+
if (!openId) {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
const provisionalUserId = createProvisionalUserId(channelType, openId);
|
|
203
|
+
const scopedStableUserId = scopeUserId(channelType, stableUserId);
|
|
204
|
+
if (provisionalUserId === scopedStableUserId) {
|
|
205
|
+
return this.getProfile(scopedStableUserId);
|
|
206
|
+
}
|
|
207
|
+
const provisional = this.getProfile(provisionalUserId);
|
|
208
|
+
const stable = this.getProfile(scopedStableUserId);
|
|
209
|
+
if (!provisional || !stable) {
|
|
210
|
+
return stable;
|
|
211
|
+
}
|
|
212
|
+
return this.mergeUsers(provisionalUserId, scopedStableUserId);
|
|
213
|
+
}
|
|
159
214
|
listIssues() {
|
|
160
215
|
return this.db.prepare(`
|
|
161
216
|
SELECT id, kind, user_id, details, status, updated_at
|
|
@@ -190,38 +245,45 @@ var UserBindStore = class {
|
|
|
190
245
|
);
|
|
191
246
|
}
|
|
192
247
|
upsertIdentity(args) {
|
|
193
|
-
|
|
194
|
-
this.syncMarkdownToStore(args.userId);
|
|
195
|
-
}
|
|
248
|
+
const scopedUserId = scopeUserId(args.channelType, args.userId);
|
|
196
249
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
197
|
-
const current = this.getProfileFromDatabase(
|
|
250
|
+
const current = this.getProfileFromDatabase(scopedUserId);
|
|
251
|
+
const externalUserId = args.openId ?? getExternalUserId(args.channelType, args.userId);
|
|
198
252
|
const next = {
|
|
199
|
-
userId:
|
|
253
|
+
userId: scopedUserId,
|
|
200
254
|
name: args.profilePatch.name ?? current?.name ?? null,
|
|
201
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,
|
|
202
259
|
email: args.profilePatch.email ?? current?.email ?? null,
|
|
203
260
|
avatar: args.profilePatch.avatar ?? current?.avatar ?? null,
|
|
204
|
-
nickname: args.profilePatch.nickname ?? current?.nickname ??
|
|
205
|
-
preferences: args.profilePatch.preferences ?? current?.preferences ??
|
|
261
|
+
nickname: args.profilePatch.nickname ?? current?.nickname ?? null,
|
|
262
|
+
preferences: args.profilePatch.preferences ?? current?.preferences ?? null,
|
|
206
263
|
personality: args.profilePatch.personality ?? current?.personality ?? null,
|
|
264
|
+
interests: args.profilePatch.interests ?? current?.interests ?? null,
|
|
207
265
|
role: args.profilePatch.role ?? current?.role ?? null,
|
|
208
|
-
timezone: args.profilePatch.timezone ?? current?.timezone ??
|
|
266
|
+
timezone: args.profilePatch.timezone ?? current?.timezone ?? getServerTimezone(),
|
|
209
267
|
notes: args.profilePatch.notes ?? current?.notes ?? defaultProfileNotes(),
|
|
210
268
|
visibility: args.profilePatch.visibility ?? current?.visibility ?? "private",
|
|
211
269
|
source: args.source,
|
|
212
270
|
updatedAt: now
|
|
213
271
|
};
|
|
214
272
|
this.db.prepare(`
|
|
215
|
-
INSERT INTO ${TABLES.profiles} (user_id, name, gender, email, avatar, nickname, preferences, personality, role, timezone, notes, visibility, source, updated_at)
|
|
216
|
-
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
217
275
|
ON CONFLICT(user_id) DO UPDATE SET
|
|
218
276
|
name = excluded.name,
|
|
219
277
|
gender = excluded.gender,
|
|
278
|
+
birth_date = excluded.birth_date,
|
|
279
|
+
birth_year = excluded.birth_year,
|
|
280
|
+
age = excluded.age,
|
|
220
281
|
email = excluded.email,
|
|
221
282
|
avatar = excluded.avatar,
|
|
222
283
|
nickname = excluded.nickname,
|
|
223
284
|
preferences = excluded.preferences,
|
|
224
285
|
personality = excluded.personality,
|
|
286
|
+
interests = excluded.interests,
|
|
225
287
|
role = excluded.role,
|
|
226
288
|
timezone = excluded.timezone,
|
|
227
289
|
notes = excluded.notes,
|
|
@@ -232,11 +294,15 @@ var UserBindStore = class {
|
|
|
232
294
|
next.userId,
|
|
233
295
|
next.name,
|
|
234
296
|
next.gender,
|
|
297
|
+
next.birthDate,
|
|
298
|
+
next.birthYear,
|
|
299
|
+
next.age,
|
|
235
300
|
next.email,
|
|
236
301
|
next.avatar,
|
|
237
302
|
next.nickname,
|
|
238
303
|
next.preferences,
|
|
239
304
|
next.personality,
|
|
305
|
+
next.interests,
|
|
240
306
|
next.role,
|
|
241
307
|
next.timezone,
|
|
242
308
|
next.notes,
|
|
@@ -257,10 +323,10 @@ var UserBindStore = class {
|
|
|
257
323
|
updated_at = excluded.updated_at
|
|
258
324
|
`).run(
|
|
259
325
|
bindingId,
|
|
260
|
-
|
|
326
|
+
scopedUserId,
|
|
261
327
|
args.channelType,
|
|
262
328
|
args.openId,
|
|
263
|
-
|
|
329
|
+
externalUserId,
|
|
264
330
|
null,
|
|
265
331
|
args.source,
|
|
266
332
|
now
|
|
@@ -286,6 +352,26 @@ var UserBindStore = class {
|
|
|
286
352
|
}
|
|
287
353
|
});
|
|
288
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
|
+
}
|
|
289
375
|
mergeUsers(fromUserId, intoUserId) {
|
|
290
376
|
const from = this.getProfile(fromUserId);
|
|
291
377
|
const into = this.getProfile(intoUserId);
|
|
@@ -300,11 +386,15 @@ var UserBindStore = class {
|
|
|
300
386
|
profilePatch: {
|
|
301
387
|
name: into.name ?? from.name,
|
|
302
388
|
gender: into.gender ?? from.gender,
|
|
389
|
+
birthDate: into.birthDate ?? from.birthDate,
|
|
390
|
+
birthYear: into.birthYear ?? from.birthYear,
|
|
391
|
+
age: into.age ?? from.age,
|
|
303
392
|
email: into.email ?? from.email,
|
|
304
393
|
avatar: into.avatar ?? from.avatar,
|
|
305
394
|
nickname: into.nickname ?? from.nickname,
|
|
306
395
|
preferences: into.preferences ?? from.preferences,
|
|
307
396
|
personality: into.personality ?? from.personality,
|
|
397
|
+
interests: into.interests ?? from.interests,
|
|
308
398
|
role: into.role ?? from.role,
|
|
309
399
|
timezone: into.timezone ?? from.timezone,
|
|
310
400
|
notes: joinNotes(into.notes, from.notes),
|
|
@@ -322,7 +412,7 @@ var UserBindStore = class {
|
|
|
322
412
|
}
|
|
323
413
|
getProfileFromDatabase(userId) {
|
|
324
414
|
const row = this.db.prepare(`
|
|
325
|
-
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
|
|
326
416
|
FROM ${TABLES.profiles} WHERE user_id = ?
|
|
327
417
|
`).get(userId);
|
|
328
418
|
return row ? mapProfileRow(row) : null;
|
|
@@ -346,8 +436,6 @@ var UserBindStore = class {
|
|
|
346
436
|
}
|
|
347
437
|
const current = this.getProfileFromDatabase(userId);
|
|
348
438
|
const parsed = parseProfileMarkdown((0, import_node_fs.readFileSync)(markdownPath, "utf8"));
|
|
349
|
-
const markdownMtime = (0, import_node_fs.statSync)(markdownPath).mtime.toISOString();
|
|
350
|
-
const dbTime = current?.updatedAt ?? (/* @__PURE__ */ new Date(0)).toISOString();
|
|
351
439
|
const patch = {
|
|
352
440
|
...parsed.profilePatch,
|
|
353
441
|
notes: parsed.notes
|
|
@@ -357,7 +445,7 @@ var UserBindStore = class {
|
|
|
357
445
|
userId,
|
|
358
446
|
channelType: "manual",
|
|
359
447
|
openId: null,
|
|
360
|
-
source: "markdown-profile",
|
|
448
|
+
source: parsed.source ?? "markdown-profile",
|
|
361
449
|
profilePatch: {
|
|
362
450
|
...patch,
|
|
363
451
|
visibility: patch.visibility ?? "private"
|
|
@@ -365,14 +453,53 @@ var UserBindStore = class {
|
|
|
365
453
|
});
|
|
366
454
|
return;
|
|
367
455
|
}
|
|
368
|
-
|
|
456
|
+
const markdownMtime = (0, import_node_fs.statSync)(markdownPath).mtime.toISOString();
|
|
457
|
+
const markdownHash = computeProfilePayloadHash({
|
|
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,
|
|
463
|
+
nickname: patch.nickname ?? null,
|
|
464
|
+
timezone: patch.timezone ?? null,
|
|
465
|
+
preferences: patch.preferences ?? null,
|
|
466
|
+
personality: patch.personality ?? null,
|
|
467
|
+
interests: patch.interests ?? null,
|
|
468
|
+
role: patch.role ?? null,
|
|
469
|
+
visibility: patch.visibility ?? current.visibility
|
|
470
|
+
}, patch.notes ?? null);
|
|
471
|
+
const currentHash = computeProfilePayloadHash({
|
|
472
|
+
name: current.name,
|
|
473
|
+
gender: current.gender,
|
|
474
|
+
birthDate: current.birthDate,
|
|
475
|
+
birthYear: current.birthYear,
|
|
476
|
+
age: current.age,
|
|
477
|
+
nickname: current.nickname,
|
|
478
|
+
timezone: current.timezone,
|
|
479
|
+
preferences: current.preferences,
|
|
480
|
+
personality: current.personality,
|
|
481
|
+
interests: current.interests,
|
|
482
|
+
role: current.role,
|
|
483
|
+
visibility: current.visibility
|
|
484
|
+
}, current.notes);
|
|
485
|
+
if (markdownHash === currentHash) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
if (parsed.syncHash && parsed.syncHash === markdownHash) {
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const dbTime = current.updatedAt;
|
|
492
|
+
if (parsed.updatedAt && parsed.updatedAt <= dbTime && markdownMtime <= dbTime) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
if (!parsed.syncHash && markdownMtime <= dbTime) {
|
|
369
496
|
return;
|
|
370
497
|
}
|
|
371
498
|
this.upsertIdentity({
|
|
372
499
|
userId,
|
|
373
500
|
channelType: "manual",
|
|
374
501
|
openId: null,
|
|
375
|
-
source: "markdown-profile",
|
|
502
|
+
source: parsed.source ?? "markdown-profile",
|
|
376
503
|
profilePatch: {
|
|
377
504
|
...current,
|
|
378
505
|
...patch
|
|
@@ -389,7 +516,7 @@ var UserBindStore = class {
|
|
|
389
516
|
}
|
|
390
517
|
writeExports() {
|
|
391
518
|
const profiles = this.db.prepare(`
|
|
392
|
-
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
|
|
393
520
|
FROM ${TABLES.profiles}
|
|
394
521
|
ORDER BY updated_at DESC
|
|
395
522
|
`).all();
|
|
@@ -408,6 +535,102 @@ var UserBindStore = class {
|
|
|
408
535
|
}
|
|
409
536
|
this.db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
|
|
410
537
|
}
|
|
538
|
+
migrateChannelScopedUserIds() {
|
|
539
|
+
const rows = this.db.prepare(`
|
|
540
|
+
SELECT DISTINCT user_id, channel_type
|
|
541
|
+
FROM ${TABLES.bindings}
|
|
542
|
+
WHERE user_id IS NOT NULL AND channel_type IS NOT NULL
|
|
543
|
+
`).all();
|
|
544
|
+
const renames = rows.map((row) => {
|
|
545
|
+
const currentUserId = String(row.user_id ?? "");
|
|
546
|
+
const channelType = String(row.channel_type ?? "");
|
|
547
|
+
const scopedUserId = scopeUserId(channelType, currentUserId);
|
|
548
|
+
if (!currentUserId || !channelType || scopedUserId === currentUserId) {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
return { currentUserId, scopedUserId };
|
|
552
|
+
}).filter((item) => item != null);
|
|
553
|
+
if (renames.length === 0) {
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
for (const { currentUserId, scopedUserId } of renames) {
|
|
557
|
+
this.renameUserId(currentUserId, scopedUserId);
|
|
558
|
+
}
|
|
559
|
+
this.writeExports();
|
|
560
|
+
}
|
|
561
|
+
renameUserId(fromUserId, toUserId) {
|
|
562
|
+
if (fromUserId === toUserId) {
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
const existingTarget = this.getProfileFromDatabase(toUserId);
|
|
566
|
+
const sourceProfile = this.getProfileFromDatabase(fromUserId);
|
|
567
|
+
if (sourceProfile && !existingTarget) {
|
|
568
|
+
this.db.prepare(`UPDATE ${TABLES.profiles} SET user_id = ? WHERE user_id = ?`).run(toUserId, fromUserId);
|
|
569
|
+
} else if (sourceProfile && existingTarget) {
|
|
570
|
+
const merged = {
|
|
571
|
+
...sourceProfile,
|
|
572
|
+
...existingTarget,
|
|
573
|
+
userId: toUserId,
|
|
574
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
575
|
+
source: existingTarget.source || sourceProfile.source,
|
|
576
|
+
notes: joinNotes(existingTarget.notes, sourceProfile.notes)
|
|
577
|
+
};
|
|
578
|
+
this.db.prepare(`DELETE FROM ${TABLES.profiles} WHERE user_id = ?`).run(fromUserId);
|
|
579
|
+
this.db.prepare(`
|
|
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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
582
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
583
|
+
name = excluded.name,
|
|
584
|
+
gender = excluded.gender,
|
|
585
|
+
birth_date = excluded.birth_date,
|
|
586
|
+
birth_year = excluded.birth_year,
|
|
587
|
+
age = excluded.age,
|
|
588
|
+
email = excluded.email,
|
|
589
|
+
avatar = excluded.avatar,
|
|
590
|
+
nickname = excluded.nickname,
|
|
591
|
+
preferences = excluded.preferences,
|
|
592
|
+
personality = excluded.personality,
|
|
593
|
+
interests = excluded.interests,
|
|
594
|
+
role = excluded.role,
|
|
595
|
+
timezone = excluded.timezone,
|
|
596
|
+
notes = excluded.notes,
|
|
597
|
+
visibility = excluded.visibility,
|
|
598
|
+
source = excluded.source,
|
|
599
|
+
updated_at = excluded.updated_at
|
|
600
|
+
`).run(
|
|
601
|
+
merged.userId,
|
|
602
|
+
merged.name,
|
|
603
|
+
merged.gender,
|
|
604
|
+
merged.birthDate,
|
|
605
|
+
merged.birthYear,
|
|
606
|
+
merged.age,
|
|
607
|
+
merged.email,
|
|
608
|
+
merged.avatar,
|
|
609
|
+
merged.nickname,
|
|
610
|
+
merged.preferences,
|
|
611
|
+
merged.personality,
|
|
612
|
+
merged.interests,
|
|
613
|
+
merged.role,
|
|
614
|
+
merged.timezone,
|
|
615
|
+
merged.notes,
|
|
616
|
+
merged.visibility,
|
|
617
|
+
merged.source,
|
|
618
|
+
merged.updatedAt
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
this.db.prepare(`UPDATE ${TABLES.bindings} SET user_id = ?, external_user_id = ?, updated_at = ? WHERE user_id = ?`).run(
|
|
622
|
+
toUserId,
|
|
623
|
+
getExternalUserId(extractChannelFromScopedUserId(toUserId) ?? "manual", toUserId),
|
|
624
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
625
|
+
fromUserId
|
|
626
|
+
);
|
|
627
|
+
const oldMarkdownPath = this.profileMarkdownPath(fromUserId);
|
|
628
|
+
const newMarkdownPath = this.profileMarkdownPath(toUserId);
|
|
629
|
+
if ((0, import_node_fs.existsSync)(oldMarkdownPath) && !(0, import_node_fs.existsSync)(newMarkdownPath)) {
|
|
630
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(newMarkdownPath), { recursive: true });
|
|
631
|
+
(0, import_node_fs.cpSync)(oldMarkdownPath, newMarkdownPath);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
411
634
|
};
|
|
412
635
|
var UserBindRuntime = class {
|
|
413
636
|
constructor(host, inputConfig) {
|
|
@@ -423,13 +646,51 @@ var UserBindRuntime = class {
|
|
|
423
646
|
config;
|
|
424
647
|
sessionCache = /* @__PURE__ */ new Map();
|
|
425
648
|
bindingCache = /* @__PURE__ */ new Map();
|
|
426
|
-
|
|
427
|
-
|
|
649
|
+
semanticCaptureCache = /* @__PURE__ */ new Map();
|
|
650
|
+
semanticCaptureInFlight = /* @__PURE__ */ new Set();
|
|
651
|
+
semanticCaptureRetryTimers = /* @__PURE__ */ new Map();
|
|
652
|
+
semanticCaptureRetryAttempts = /* @__PURE__ */ new Map();
|
|
653
|
+
pendingSemanticCaptures = /* @__PURE__ */ new Map();
|
|
654
|
+
pendingBindingResolutions = /* @__PURE__ */ new Map();
|
|
428
655
|
feishuTokenCache = /* @__PURE__ */ new Map();
|
|
656
|
+
globalPendingSemanticRefines = getGlobalPendingSemanticRefines();
|
|
657
|
+
pendingBindingTimer = null;
|
|
658
|
+
pendingBindingKickTimer = null;
|
|
659
|
+
pendingSemanticSweepTimer = null;
|
|
660
|
+
pendingBindingSweepInFlight = false;
|
|
661
|
+
pendingSemanticSweepInFlight = false;
|
|
662
|
+
registered = false;
|
|
429
663
|
close() {
|
|
664
|
+
if (this.pendingBindingTimer) {
|
|
665
|
+
clearInterval(this.pendingBindingTimer);
|
|
666
|
+
this.pendingBindingTimer = null;
|
|
667
|
+
}
|
|
668
|
+
if (this.pendingBindingKickTimer) {
|
|
669
|
+
clearTimeout(this.pendingBindingKickTimer);
|
|
670
|
+
this.pendingBindingKickTimer = null;
|
|
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();
|
|
430
687
|
this.store.close();
|
|
431
688
|
}
|
|
432
689
|
register() {
|
|
690
|
+
if (this.registered) {
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
this.registered = true;
|
|
433
694
|
queueMicrotask(() => {
|
|
434
695
|
try {
|
|
435
696
|
bootstrapOpenClawHost(this.config);
|
|
@@ -438,15 +699,25 @@ var UserBindRuntime = class {
|
|
|
438
699
|
});
|
|
439
700
|
this.registerHooks();
|
|
440
701
|
this.registerTools();
|
|
702
|
+
this.startPendingBindingWorker();
|
|
703
|
+
this.schedulePendingSemanticSweep();
|
|
441
704
|
exposeGlobalApi(this);
|
|
442
705
|
}
|
|
443
706
|
getIdentityForSession(sessionId) {
|
|
444
707
|
return this.sessionCache.get(sessionId) ?? null;
|
|
445
708
|
}
|
|
446
|
-
async resolveFromContext(context) {
|
|
447
|
-
const parsed = parseIdentityContext(context);
|
|
709
|
+
async resolveFromContext(context, options = {}) {
|
|
710
|
+
const parsed = parseIdentityContext(enrichIdentityContext(context));
|
|
448
711
|
if (parsed.sessionId && !parsed.channelType) {
|
|
449
|
-
|
|
712
|
+
const cached2 = this.sessionCache.get(parsed.sessionId) ?? null;
|
|
713
|
+
if (cached2 && isProvisionalScopedUserId(cached2.userId) && cached2.senderOpenId) {
|
|
714
|
+
return this.resolveFromContext({
|
|
715
|
+
sessionId: parsed.sessionId,
|
|
716
|
+
channel: { type: cached2.channelType },
|
|
717
|
+
sender: { id: cached2.senderOpenId, name: cached2.senderName }
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
return cached2;
|
|
450
721
|
}
|
|
451
722
|
if (!parsed.sessionId || !parsed.channelType) {
|
|
452
723
|
return null;
|
|
@@ -454,47 +725,90 @@ var UserBindRuntime = class {
|
|
|
454
725
|
const cacheKey = `${parsed.channelType}:${parsed.openId ?? parsed.sessionId}`;
|
|
455
726
|
const cached = this.bindingCache.get(cacheKey);
|
|
456
727
|
if (cached && cached.expiresAt > Date.now()) {
|
|
457
|
-
|
|
458
|
-
|
|
728
|
+
if (parsed.openId && (isProvisionalScopedUserId(cached.identity.userId) || cached.identity.source === "synthetic-fallback")) {
|
|
729
|
+
const rebound = this.store.findBinding(parsed.channelType, parsed.openId);
|
|
730
|
+
if (!rebound || rebound.userId === cached.identity.userId) {
|
|
731
|
+
this.sessionCache.set(parsed.sessionId, cached.identity);
|
|
732
|
+
return cached.identity;
|
|
733
|
+
}
|
|
734
|
+
this.bindingCache.delete(cacheKey);
|
|
735
|
+
} else {
|
|
736
|
+
this.sessionCache.set(parsed.sessionId, cached.identity);
|
|
737
|
+
return cached.identity;
|
|
738
|
+
}
|
|
459
739
|
}
|
|
460
740
|
const binding = this.store.findBinding(parsed.channelType, parsed.openId);
|
|
461
741
|
let userId = binding?.userId ?? null;
|
|
462
742
|
let source = binding?.source ?? "local";
|
|
743
|
+
const provisionalUserId = parsed.openId ? createProvisionalUserId(parsed.channelType, parsed.openId) : null;
|
|
463
744
|
let remoteProfilePatch = {};
|
|
464
745
|
if (parsed.channelType === "feishu" && parsed.openId) {
|
|
465
|
-
const scopeStatus = await this.ensureFeishuScopeStatus();
|
|
466
|
-
if (scopeStatus.missingIdentityScopes.length > 0) {
|
|
467
|
-
const details = `Missing Feishu scopes: ${scopeStatus.missingIdentityScopes.join(", ")}`;
|
|
468
|
-
logUserBindEvent("feishu-scope-missing", {
|
|
469
|
-
openId: parsed.openId,
|
|
470
|
-
missingScopes: scopeStatus.missingIdentityScopes
|
|
471
|
-
});
|
|
472
|
-
this.store.recordIssue("feishu-scope-missing", details);
|
|
473
|
-
}
|
|
474
746
|
if (!userId) {
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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
|
+
}
|
|
480
761
|
} else {
|
|
481
|
-
this.
|
|
762
|
+
this.enqueuePendingBindingResolution(parsed.channelType, parsed.openId, "feishu-deferred-resolution");
|
|
482
763
|
}
|
|
483
764
|
}
|
|
484
765
|
}
|
|
766
|
+
const profilePatch = {
|
|
767
|
+
name: parsed.senderName,
|
|
768
|
+
...remoteProfilePatch
|
|
769
|
+
};
|
|
770
|
+
if (!userId && parsed.openId && parsed.channelType && CHANNELS_WITH_NATIVE_STABLE_IDS.has(parsed.channelType)) {
|
|
771
|
+
userId = `${parsed.channelType}:${parsed.openId}`;
|
|
772
|
+
source = "channel-native";
|
|
773
|
+
}
|
|
774
|
+
if (!userId && provisionalUserId) {
|
|
775
|
+
userId = provisionalUserId;
|
|
776
|
+
source = "provisional-openid";
|
|
777
|
+
this.enqueuePendingBindingResolution(parsed.channelType, parsed.openId, source);
|
|
778
|
+
}
|
|
779
|
+
const shouldPersistIdentity = Boolean(userId);
|
|
485
780
|
if (!userId) {
|
|
486
|
-
userId =
|
|
781
|
+
userId = createEphemeralUserId(parsed.channelType, parsed.openId, parsed.sessionId);
|
|
487
782
|
source = "synthetic-fallback";
|
|
488
783
|
}
|
|
489
|
-
|
|
784
|
+
if (shouldPersistIdentity) {
|
|
785
|
+
this.store.getProfile(userId);
|
|
786
|
+
}
|
|
787
|
+
const profile = shouldPersistIdentity ? (() => {
|
|
788
|
+
let persisted = this.store.upsertIdentity({
|
|
789
|
+
userId,
|
|
790
|
+
channelType: parsed.channelType,
|
|
791
|
+
openId: parsed.openId,
|
|
792
|
+
source,
|
|
793
|
+
profilePatch
|
|
794
|
+
});
|
|
795
|
+
if (provisionalUserId && persisted.userId !== provisionalUserId && source !== "provisional-openid") {
|
|
796
|
+
const reconciled = this.store.reconcileProvisionalIdentity(parsed.channelType, parsed.openId, persisted.userId);
|
|
797
|
+
if (reconciled) {
|
|
798
|
+
persisted = reconciled;
|
|
799
|
+
logUserBindEvent("provisional-profile-merged", {
|
|
800
|
+
provisionalUserId,
|
|
801
|
+
stableUserId: persisted.userId,
|
|
802
|
+
sessionId: parsed.sessionId
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
return persisted;
|
|
807
|
+
})() : buildLightweightProfile({
|
|
490
808
|
userId,
|
|
491
|
-
channelType: parsed.channelType,
|
|
492
|
-
openId: parsed.openId,
|
|
493
809
|
source,
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
...remoteProfilePatch
|
|
497
|
-
}
|
|
810
|
+
current: null,
|
|
811
|
+
profilePatch
|
|
498
812
|
});
|
|
499
813
|
const identity = {
|
|
500
814
|
sessionId: parsed.sessionId,
|
|
@@ -511,26 +825,151 @@ var UserBindRuntime = class {
|
|
|
511
825
|
expiresAt: Date.now() + this.config.cacheTtlMs,
|
|
512
826
|
identity
|
|
513
827
|
});
|
|
514
|
-
if (parsed.channelType === "feishu") {
|
|
515
|
-
await this.syncFeishuMirror(identity);
|
|
516
|
-
}
|
|
517
828
|
return identity;
|
|
518
829
|
}
|
|
830
|
+
startPendingBindingWorker() {
|
|
831
|
+
if (this.pendingBindingTimer) {
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
this.pendingBindingTimer = setInterval(() => {
|
|
835
|
+
void this.runPendingBindingSweep();
|
|
836
|
+
}, 60 * 1e3);
|
|
837
|
+
this.pendingBindingTimer.unref?.();
|
|
838
|
+
}
|
|
839
|
+
schedulePendingBindingSweep(delayMs) {
|
|
840
|
+
if (this.pendingBindingKickTimer) {
|
|
841
|
+
return;
|
|
842
|
+
}
|
|
843
|
+
this.pendingBindingKickTimer = setTimeout(() => {
|
|
844
|
+
this.pendingBindingKickTimer = null;
|
|
845
|
+
void this.runPendingBindingSweep();
|
|
846
|
+
}, delayMs);
|
|
847
|
+
this.pendingBindingKickTimer.unref?.();
|
|
848
|
+
}
|
|
849
|
+
enqueuePendingBindingResolution(channelType, openId, reason) {
|
|
850
|
+
if (!openId) {
|
|
851
|
+
return;
|
|
852
|
+
}
|
|
853
|
+
const key = `${channelType}:${openId}`;
|
|
854
|
+
const current = this.pendingBindingResolutions.get(key);
|
|
855
|
+
if (current) {
|
|
856
|
+
current.reason = reason;
|
|
857
|
+
current.nextAttemptAt = Math.min(current.nextAttemptAt, Date.now() + 5e3);
|
|
858
|
+
this.pendingBindingResolutions.set(key, current);
|
|
859
|
+
} else {
|
|
860
|
+
this.pendingBindingResolutions.set(key, {
|
|
861
|
+
channelType,
|
|
862
|
+
openId,
|
|
863
|
+
reason,
|
|
864
|
+
attempts: 0,
|
|
865
|
+
nextAttemptAt: Date.now() + 5e3
|
|
866
|
+
});
|
|
867
|
+
}
|
|
868
|
+
this.schedulePendingBindingSweep(5e3);
|
|
869
|
+
}
|
|
870
|
+
async runPendingBindingSweep() {
|
|
871
|
+
if (this.pendingBindingSweepInFlight || this.pendingBindingResolutions.size === 0) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
this.pendingBindingSweepInFlight = true;
|
|
875
|
+
try {
|
|
876
|
+
const now = Date.now();
|
|
877
|
+
for (const [key, pending] of this.pendingBindingResolutions.entries()) {
|
|
878
|
+
if (pending.nextAttemptAt > now) {
|
|
879
|
+
continue;
|
|
880
|
+
}
|
|
881
|
+
const existing = this.store.findBinding(pending.channelType, pending.openId);
|
|
882
|
+
if (existing && !isProvisionalScopedUserId(existing.userId)) {
|
|
883
|
+
this.store.upsertIdentity({
|
|
884
|
+
userId: existing.userId,
|
|
885
|
+
channelType: pending.channelType,
|
|
886
|
+
openId: pending.openId,
|
|
887
|
+
source: existing.source,
|
|
888
|
+
profilePatch: {}
|
|
889
|
+
});
|
|
890
|
+
this.store.reconcileProvisionalIdentity(pending.channelType, pending.openId, existing.userId);
|
|
891
|
+
this.dropCachedOpenIdIdentity(pending.channelType, pending.openId);
|
|
892
|
+
this.pendingBindingResolutions.delete(key);
|
|
893
|
+
logUserBindEvent("pending-binding-reconciled", {
|
|
894
|
+
channelType: pending.channelType,
|
|
895
|
+
openId: pending.openId,
|
|
896
|
+
userId: existing.userId
|
|
897
|
+
});
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (pending.channelType !== "feishu") {
|
|
901
|
+
this.pendingBindingResolutions.delete(key);
|
|
902
|
+
continue;
|
|
903
|
+
}
|
|
904
|
+
const remote = await this.tryResolveFeishuUser(pending.openId);
|
|
905
|
+
if (remote?.userId) {
|
|
906
|
+
this.store.upsertIdentity({
|
|
907
|
+
userId: remote.userId,
|
|
908
|
+
channelType: pending.channelType,
|
|
909
|
+
openId: pending.openId,
|
|
910
|
+
source: remote.source,
|
|
911
|
+
profilePatch: remote.profilePatch
|
|
912
|
+
});
|
|
913
|
+
this.store.reconcileProvisionalIdentity(pending.channelType, pending.openId, remote.userId);
|
|
914
|
+
this.dropCachedOpenIdIdentity(pending.channelType, pending.openId);
|
|
915
|
+
this.pendingBindingResolutions.delete(key);
|
|
916
|
+
logUserBindEvent("pending-binding-resolved", {
|
|
917
|
+
channelType: pending.channelType,
|
|
918
|
+
openId: pending.openId,
|
|
919
|
+
userId: remote.userId,
|
|
920
|
+
source: remote.source
|
|
921
|
+
});
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
pending.attempts += 1;
|
|
925
|
+
pending.nextAttemptAt = Date.now() + getPendingBindingRetryDelayMs(remote?.source ?? pending.reason, pending.attempts);
|
|
926
|
+
this.pendingBindingResolutions.set(key, pending);
|
|
927
|
+
}
|
|
928
|
+
} finally {
|
|
929
|
+
this.pendingBindingSweepInFlight = false;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
dropCachedOpenIdIdentity(channelType, openId) {
|
|
933
|
+
const cacheKey = `${channelType}:${openId}`;
|
|
934
|
+
this.bindingCache.delete(cacheKey);
|
|
935
|
+
for (const [sessionId, identity] of this.sessionCache.entries()) {
|
|
936
|
+
if (identity.channelType === channelType && identity.senderOpenId === openId) {
|
|
937
|
+
this.sessionCache.delete(sessionId);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
519
941
|
async getMyProfile(context) {
|
|
520
942
|
const identity = await this.requireCurrentIdentity(context);
|
|
521
943
|
return identity.profile;
|
|
522
944
|
}
|
|
523
|
-
async updateMyProfile(context, patch) {
|
|
945
|
+
async updateMyProfile(context, patch, operations) {
|
|
524
946
|
const identity = await this.requireCurrentIdentity(context);
|
|
525
|
-
const
|
|
526
|
-
|
|
947
|
+
const fallbackOperations = extractProfilePatchOperations(context && typeof context === "object" ? context : {});
|
|
948
|
+
const nextPatch = applyProfilePatchOperations(identity.profile, patch, operations ?? fallbackOperations);
|
|
949
|
+
const updated = identity.source === "synthetic-fallback" ? this.store.upsertIdentity({
|
|
950
|
+
userId: createStableLocalUserId(identity.channelType, identity.senderOpenId, identity.sessionId),
|
|
951
|
+
channelType: identity.channelType,
|
|
952
|
+
openId: identity.senderOpenId,
|
|
953
|
+
source: "self-bootstrap",
|
|
954
|
+
profilePatch: {
|
|
955
|
+
...identity.profile,
|
|
956
|
+
...nextPatch
|
|
957
|
+
}
|
|
958
|
+
}) : this.store.updateProfile(identity.userId, {
|
|
959
|
+
...nextPatch,
|
|
527
960
|
source: "self-update"
|
|
528
961
|
});
|
|
529
962
|
const nextIdentity = {
|
|
530
963
|
...identity,
|
|
964
|
+
userId: updated.userId,
|
|
965
|
+
source: updated.source,
|
|
531
966
|
profile: updated
|
|
532
967
|
};
|
|
533
968
|
this.sessionCache.set(identity.sessionId, nextIdentity);
|
|
969
|
+
this.bindingCache.set(`${identity.channelType}:${identity.senderOpenId ?? identity.sessionId}`, {
|
|
970
|
+
expiresAt: Date.now() + this.config.cacheTtlMs,
|
|
971
|
+
identity: nextIdentity
|
|
972
|
+
});
|
|
534
973
|
return updated;
|
|
535
974
|
}
|
|
536
975
|
async refreshMyBinding(context) {
|
|
@@ -540,12 +979,366 @@ var UserBindRuntime = class {
|
|
|
540
979
|
sessionId: identity.sessionId,
|
|
541
980
|
sender: { id: identity.senderOpenId, name: identity.senderName },
|
|
542
981
|
channel: { type: identity.channelType }
|
|
543
|
-
});
|
|
982
|
+
}, { allowRemoteLookup: true });
|
|
544
983
|
if (!refreshed) {
|
|
545
984
|
throw new Error("Unable to refresh binding for current session");
|
|
546
985
|
}
|
|
547
986
|
return refreshed;
|
|
548
987
|
}
|
|
988
|
+
async captureProfileFromMessage(context, identity) {
|
|
989
|
+
const utteranceText = extractUserUtterance(context);
|
|
990
|
+
if (!utteranceText) {
|
|
991
|
+
logUserBindEvent("semantic-profile-capture-skipped-no-utterance", {
|
|
992
|
+
userId: identity.userId,
|
|
993
|
+
sessionId: identity.sessionId
|
|
994
|
+
});
|
|
995
|
+
return;
|
|
996
|
+
}
|
|
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}`);
|
|
1055
|
+
this.pruneSemanticCaptureCache();
|
|
1056
|
+
if (this.semanticCaptureInFlight.has(fingerprint)) {
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
const processedAt = this.semanticCaptureCache.get(fingerprint);
|
|
1060
|
+
if (processedAt && processedAt > Date.now() - 12 * 60 * 60 * 1e3) {
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
await this.runSemanticProfileCapture(identity, sessionId, messageText, fingerprint);
|
|
1064
|
+
}
|
|
1065
|
+
async runSemanticProfileCapture(identity, sessionId, messageText, fingerprint) {
|
|
1066
|
+
this.semanticCaptureInFlight.add(fingerprint);
|
|
1067
|
+
try {
|
|
1068
|
+
const extraction = await inferSemanticProfileExtraction(messageText, identity.profile);
|
|
1069
|
+
if (!extraction?.shouldUpdate) {
|
|
1070
|
+
logUserBindEvent("semantic-profile-capture-noop", {
|
|
1071
|
+
userId: identity.userId,
|
|
1072
|
+
sessionId,
|
|
1073
|
+
confidence: extraction?.confidence ?? 0,
|
|
1074
|
+
messagePreview: messageText.slice(0, 120)
|
|
1075
|
+
});
|
|
1076
|
+
this.semanticCaptureCache.set(fingerprint, Date.now());
|
|
1077
|
+
this.clearSemanticProfileRetryState(fingerprint);
|
|
1078
|
+
return;
|
|
1079
|
+
}
|
|
1080
|
+
const { patch, operations } = cleanupSemanticProfilePatch(
|
|
1081
|
+
extraction.patch,
|
|
1082
|
+
identity.profile,
|
|
1083
|
+
extraction.operations
|
|
1084
|
+
);
|
|
1085
|
+
if (Object.keys(patch).length === 0) {
|
|
1086
|
+
this.semanticCaptureCache.set(fingerprint, Date.now());
|
|
1087
|
+
this.clearSemanticProfileRetryState(fingerprint);
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
await this.updateMyProfile(
|
|
1091
|
+
{ sessionId },
|
|
1092
|
+
{
|
|
1093
|
+
...patch,
|
|
1094
|
+
source: "semantic-self-update"
|
|
1095
|
+
},
|
|
1096
|
+
operations
|
|
1097
|
+
);
|
|
1098
|
+
await this.removePendingSemanticRefineNote(sessionId, fingerprint);
|
|
1099
|
+
logUserBindEvent("semantic-profile-capture-success", {
|
|
1100
|
+
userId: identity.userId,
|
|
1101
|
+
sessionId,
|
|
1102
|
+
fields: Object.keys(patch),
|
|
1103
|
+
operations,
|
|
1104
|
+
confidence: extraction.confidence
|
|
1105
|
+
});
|
|
1106
|
+
this.semanticCaptureCache.set(fingerprint, Date.now());
|
|
1107
|
+
this.clearSemanticProfileRetryState(fingerprint);
|
|
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
|
+
}
|
|
1114
|
+
logUserBindEvent("semantic-profile-capture-failed", {
|
|
1115
|
+
userId: identity.userId,
|
|
1116
|
+
sessionId,
|
|
1117
|
+
message: error instanceof Error ? error.message : String(error)
|
|
1118
|
+
});
|
|
1119
|
+
} finally {
|
|
1120
|
+
this.semanticCaptureInFlight.delete(fingerprint);
|
|
1121
|
+
}
|
|
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
|
+
}
|
|
1334
|
+
pruneSemanticCaptureCache() {
|
|
1335
|
+
const cutoff = Date.now() - 12 * 60 * 60 * 1e3;
|
|
1336
|
+
for (const [fingerprint, ts] of this.semanticCaptureCache.entries()) {
|
|
1337
|
+
if (ts < cutoff) {
|
|
1338
|
+
this.semanticCaptureCache.delete(fingerprint);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
549
1342
|
async adminInstruction(toolName, instruction, context) {
|
|
550
1343
|
const requester = await this.resolveFromContext(context);
|
|
551
1344
|
const agentId = getAgentIdFromContext(context);
|
|
@@ -612,14 +1405,17 @@ var UserBindRuntime = class {
|
|
|
612
1405
|
this.host.registerHook?.(
|
|
613
1406
|
["message:received", "message:preprocessed"],
|
|
614
1407
|
async (event) => {
|
|
615
|
-
await this.resolveFromContext(event);
|
|
1408
|
+
const identity = await this.resolveFromContext(event);
|
|
1409
|
+
if (identity) {
|
|
1410
|
+
await this.captureProfileFromMessage(event, identity);
|
|
1411
|
+
}
|
|
616
1412
|
},
|
|
617
1413
|
{
|
|
618
1414
|
name: "bamdra-user-bind-resolve",
|
|
619
1415
|
description: "Resolve runtime identity from channel sender metadata"
|
|
620
1416
|
}
|
|
621
1417
|
);
|
|
622
|
-
this.host.on?.("before_prompt_build", async (
|
|
1418
|
+
this.host.on?.("before_prompt_build", async (event, context) => {
|
|
623
1419
|
const identity = await this.resolveFromContext(context);
|
|
624
1420
|
if (!identity) {
|
|
625
1421
|
return;
|
|
@@ -639,9 +1435,16 @@ var UserBindRuntime = class {
|
|
|
639
1435
|
if (!registerTool) {
|
|
640
1436
|
return;
|
|
641
1437
|
}
|
|
1438
|
+
const getMyProfileExecute = async (_id, params) => asTextResult(await this.getMyProfile(params));
|
|
1439
|
+
const updateMyProfileExecute = async (_id, params) => asTextResult(await this.updateMyProfile(
|
|
1440
|
+
params,
|
|
1441
|
+
sanitizeProfilePatch(params),
|
|
1442
|
+
extractProfilePatchOperations(params)
|
|
1443
|
+
));
|
|
1444
|
+
const refreshMyBindingExecute = async (_id, params) => asTextResult(await this.refreshMyBinding(params));
|
|
642
1445
|
registerTool({
|
|
643
1446
|
name: "bamdra_user_bind_get_my_profile",
|
|
644
|
-
description: "Get the current user's bound profile",
|
|
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",
|
|
645
1448
|
parameters: {
|
|
646
1449
|
type: "object",
|
|
647
1450
|
additionalProperties: false,
|
|
@@ -649,30 +1452,48 @@ var UserBindRuntime = class {
|
|
|
649
1452
|
sessionId: { type: "string" }
|
|
650
1453
|
}
|
|
651
1454
|
},
|
|
652
|
-
execute:
|
|
1455
|
+
execute: getMyProfileExecute
|
|
653
1456
|
});
|
|
654
1457
|
registerTool({
|
|
655
1458
|
name: "bamdra_user_bind_update_my_profile",
|
|
656
|
-
description: "
|
|
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",
|
|
657
1460
|
parameters: {
|
|
658
1461
|
type: "object",
|
|
659
1462
|
additionalProperties: false,
|
|
660
1463
|
required: ["sessionId"],
|
|
661
1464
|
properties: {
|
|
662
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"] },
|
|
663
1476
|
nickname: { type: "string" },
|
|
1477
|
+
nicknameOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
664
1478
|
preferences: { type: "string" },
|
|
1479
|
+
preferencesOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
665
1480
|
personality: { type: "string" },
|
|
1481
|
+
personalityOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1482
|
+
interests: { type: "string" },
|
|
1483
|
+
interestsOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
666
1484
|
role: { type: "string" },
|
|
1485
|
+
roleOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
667
1486
|
timezone: { type: "string" },
|
|
668
|
-
|
|
1487
|
+
timezoneOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1488
|
+
notes: { type: "string" },
|
|
1489
|
+
notesOperation: { type: "string", enum: ["replace", "append", "remove"] }
|
|
669
1490
|
}
|
|
670
1491
|
},
|
|
671
|
-
execute:
|
|
1492
|
+
execute: updateMyProfileExecute
|
|
672
1493
|
});
|
|
673
1494
|
registerTool({
|
|
674
1495
|
name: "bamdra_user_bind_refresh_my_binding",
|
|
675
|
-
description: "Refresh the current user's identity binding",
|
|
1496
|
+
description: "Refresh the current user's identity binding when the session looks unresolved, stale, or mapped to the wrong external identity, then fetch the profile again",
|
|
676
1497
|
parameters: {
|
|
677
1498
|
type: "object",
|
|
678
1499
|
additionalProperties: false,
|
|
@@ -680,32 +1501,75 @@ var UserBindRuntime = class {
|
|
|
680
1501
|
sessionId: { type: "string" }
|
|
681
1502
|
}
|
|
682
1503
|
},
|
|
683
|
-
execute:
|
|
1504
|
+
execute: refreshMyBindingExecute
|
|
684
1505
|
});
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
1506
|
+
registerTool({
|
|
1507
|
+
name: SELF_TOOL_ALIASES[0],
|
|
1508
|
+
description: "Alias of bamdra_user_bind_get_my_profile",
|
|
1509
|
+
parameters: {
|
|
1510
|
+
type: "object",
|
|
1511
|
+
additionalProperties: false,
|
|
1512
|
+
properties: {
|
|
1513
|
+
sessionId: { type: "string" }
|
|
1514
|
+
}
|
|
1515
|
+
},
|
|
1516
|
+
execute: getMyProfileExecute
|
|
1517
|
+
});
|
|
1518
|
+
registerTool({
|
|
1519
|
+
name: SELF_TOOL_ALIASES[1],
|
|
1520
|
+
description: "Alias of bamdra_user_bind_update_my_profile",
|
|
1521
|
+
parameters: {
|
|
1522
|
+
type: "object",
|
|
1523
|
+
additionalProperties: false,
|
|
1524
|
+
required: ["sessionId"],
|
|
1525
|
+
properties: {
|
|
1526
|
+
sessionId: { type: "string" },
|
|
1527
|
+
nickname: { type: "string" },
|
|
1528
|
+
preferences: { type: "string" },
|
|
1529
|
+
personality: { type: "string" },
|
|
1530
|
+
role: { type: "string" },
|
|
1531
|
+
timezone: { type: "string" },
|
|
1532
|
+
notes: { type: "string" }
|
|
1533
|
+
}
|
|
1534
|
+
},
|
|
1535
|
+
execute: updateMyProfileExecute
|
|
1536
|
+
});
|
|
1537
|
+
registerTool({
|
|
1538
|
+
name: SELF_TOOL_ALIASES[2],
|
|
1539
|
+
description: "Alias of bamdra_user_bind_refresh_my_binding",
|
|
1540
|
+
parameters: {
|
|
1541
|
+
type: "object",
|
|
1542
|
+
additionalProperties: false,
|
|
1543
|
+
properties: {
|
|
1544
|
+
sessionId: { type: "string" }
|
|
1545
|
+
}
|
|
1546
|
+
},
|
|
1547
|
+
execute: refreshMyBindingExecute
|
|
1548
|
+
});
|
|
1549
|
+
for (const toolName of ADMIN_TOOL_NAMES) {
|
|
1550
|
+
registerTool({
|
|
1551
|
+
name: toolName,
|
|
1552
|
+
description: `Administrative natural-language tool for ${toolName.replace("bamdra_user_bind_admin_", "")}`,
|
|
1553
|
+
parameters: {
|
|
1554
|
+
type: "object",
|
|
1555
|
+
additionalProperties: false,
|
|
1556
|
+
required: ["instruction"],
|
|
1557
|
+
properties: {
|
|
1558
|
+
instruction: { type: "string" },
|
|
1559
|
+
sessionId: { type: "string" },
|
|
1560
|
+
agentId: { type: "string" }
|
|
1561
|
+
}
|
|
1562
|
+
},
|
|
1563
|
+
execute: async (_id, params) => asTextResult(await this.adminInstruction(toolName, String(params.instruction ?? ""), params))
|
|
1564
|
+
});
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
async requireCurrentIdentity(context) {
|
|
1568
|
+
const identity = await this.resolveFromContext(context);
|
|
1569
|
+
if (!identity) {
|
|
1570
|
+
throw new Error("Unable to resolve current user identity");
|
|
1571
|
+
}
|
|
1572
|
+
return identity;
|
|
709
1573
|
}
|
|
710
1574
|
async tryResolveFeishuUser(openId) {
|
|
711
1575
|
logUserBindEvent("feishu-resolution-start", { openId });
|
|
@@ -714,6 +1578,7 @@ var UserBindRuntime = class {
|
|
|
714
1578
|
logUserBindEvent("feishu-resolution-skipped", { reason: "no-feishu-accounts-configured" });
|
|
715
1579
|
return null;
|
|
716
1580
|
}
|
|
1581
|
+
const contactScopeBlockedAccounts = /* @__PURE__ */ new Set();
|
|
717
1582
|
for (const account of accounts) {
|
|
718
1583
|
try {
|
|
719
1584
|
const token = await this.getFeishuAppAccessToken(account);
|
|
@@ -746,6 +1611,9 @@ var UserBindRuntime = class {
|
|
|
746
1611
|
};
|
|
747
1612
|
} catch (error) {
|
|
748
1613
|
const message = error instanceof Error ? error.message : String(error);
|
|
1614
|
+
if (looksLikeFeishuContactScopeError(message)) {
|
|
1615
|
+
contactScopeBlockedAccounts.add(account.accountId);
|
|
1616
|
+
}
|
|
749
1617
|
logUserBindEvent("feishu-resolution-attempt-failed", {
|
|
750
1618
|
accountId: account.accountId,
|
|
751
1619
|
openId,
|
|
@@ -777,63 +1645,18 @@ var UserBindRuntime = class {
|
|
|
777
1645
|
} catch {
|
|
778
1646
|
}
|
|
779
1647
|
}
|
|
1648
|
+
if (contactScopeBlockedAccounts.size > 0) {
|
|
1649
|
+
return {
|
|
1650
|
+
userId: "",
|
|
1651
|
+
source: "feishu-contact-scope-missing",
|
|
1652
|
+
profilePatch: {
|
|
1653
|
+
notes: renderFeishuContactScopeGuidance([...contactScopeBlockedAccounts])
|
|
1654
|
+
}
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
780
1657
|
logUserBindEvent("feishu-resolution-empty", { openId });
|
|
781
1658
|
return null;
|
|
782
1659
|
}
|
|
783
|
-
async ensureFeishuScopeStatus() {
|
|
784
|
-
if (this.feishuScopeStatus) {
|
|
785
|
-
return this.feishuScopeStatus;
|
|
786
|
-
}
|
|
787
|
-
const accounts = readFeishuAccountsFromOpenClawConfig();
|
|
788
|
-
for (const account of accounts) {
|
|
789
|
-
try {
|
|
790
|
-
const token = await this.getFeishuAppAccessToken(account);
|
|
791
|
-
const result = await feishuJsonRequest(
|
|
792
|
-
account,
|
|
793
|
-
"/open-apis/application/v6/scopes",
|
|
794
|
-
token
|
|
795
|
-
);
|
|
796
|
-
const scopes = extractScopes(result);
|
|
797
|
-
this.feishuScopeStatus = {
|
|
798
|
-
scopes,
|
|
799
|
-
missingIdentityScopes: REQUIRED_FEISHU_IDENTITY_SCOPES.filter((scope) => !scopes.includes(scope)),
|
|
800
|
-
hasDocumentAccess: scopes.some((scope) => scope.startsWith("bitable:") || scope.startsWith("drive:") || scope.startsWith("docx:") || scope.startsWith("docs:"))
|
|
801
|
-
};
|
|
802
|
-
logUserBindEvent("feishu-scopes-read", {
|
|
803
|
-
accountId: account.accountId,
|
|
804
|
-
...this.feishuScopeStatus
|
|
805
|
-
});
|
|
806
|
-
return this.feishuScopeStatus;
|
|
807
|
-
} catch (error) {
|
|
808
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
809
|
-
logUserBindEvent("feishu-scopes-attempt-failed", { accountId: account.accountId, message });
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
const executor = this.host.callTool ?? this.host.invokeTool;
|
|
813
|
-
if (typeof executor === "function") {
|
|
814
|
-
try {
|
|
815
|
-
const result = await executor.call(this.host, "feishu_app_scopes", {});
|
|
816
|
-
const scopes = extractScopes(result);
|
|
817
|
-
this.feishuScopeStatus = {
|
|
818
|
-
scopes,
|
|
819
|
-
missingIdentityScopes: REQUIRED_FEISHU_IDENTITY_SCOPES.filter((scope) => !scopes.includes(scope)),
|
|
820
|
-
hasDocumentAccess: scopes.some((scope) => scope.startsWith("bitable:") || scope.startsWith("drive:") || scope.startsWith("docx:") || scope.startsWith("docs:"))
|
|
821
|
-
};
|
|
822
|
-
logUserBindEvent("feishu-scopes-read", this.feishuScopeStatus);
|
|
823
|
-
return this.feishuScopeStatus;
|
|
824
|
-
} catch (error) {
|
|
825
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
826
|
-
logUserBindEvent("feishu-scopes-failed", { message });
|
|
827
|
-
this.store.recordIssue("feishu-scope-read", message);
|
|
828
|
-
}
|
|
829
|
-
}
|
|
830
|
-
this.feishuScopeStatus = {
|
|
831
|
-
scopes: [],
|
|
832
|
-
missingIdentityScopes: [...REQUIRED_FEISHU_IDENTITY_SCOPES],
|
|
833
|
-
hasDocumentAccess: false
|
|
834
|
-
};
|
|
835
|
-
return this.feishuScopeStatus;
|
|
836
|
-
}
|
|
837
1660
|
async getFeishuAppAccessToken(account) {
|
|
838
1661
|
const cached = this.feishuTokenCache.get(account.accountId);
|
|
839
1662
|
if (cached && cached.expiresAt > Date.now()) {
|
|
@@ -865,108 +1688,17 @@ var UserBindRuntime = class {
|
|
|
865
1688
|
});
|
|
866
1689
|
return token;
|
|
867
1690
|
}
|
|
868
|
-
async syncFeishuMirror(identity) {
|
|
869
|
-
const scopeStatus = await this.ensureFeishuScopeStatus();
|
|
870
|
-
if (!scopeStatus.hasDocumentAccess) {
|
|
871
|
-
return;
|
|
872
|
-
}
|
|
873
|
-
const executor = this.host.callTool ?? this.host.invokeTool;
|
|
874
|
-
if (typeof executor !== "function") {
|
|
875
|
-
return;
|
|
876
|
-
}
|
|
877
|
-
try {
|
|
878
|
-
const mirror = await this.ensureFeishuBitableMirror(executor.bind(this.host));
|
|
879
|
-
if (!mirror.appToken || !mirror.tableId) {
|
|
880
|
-
return;
|
|
881
|
-
}
|
|
882
|
-
const existing = await executor.call(this.host, "feishu_bitable_list_records", {
|
|
883
|
-
app_token: mirror.appToken,
|
|
884
|
-
table_id: mirror.tableId
|
|
885
|
-
});
|
|
886
|
-
const recordId = findBitableRecordId(existing, identity.userId);
|
|
887
|
-
const fields = {
|
|
888
|
-
user_id: identity.userId,
|
|
889
|
-
channel_type: identity.channelType,
|
|
890
|
-
open_id: identity.senderOpenId,
|
|
891
|
-
name: identity.profile.name,
|
|
892
|
-
nickname: identity.profile.nickname,
|
|
893
|
-
preferences: identity.profile.preferences,
|
|
894
|
-
personality: identity.profile.personality,
|
|
895
|
-
role: identity.profile.role,
|
|
896
|
-
timezone: identity.profile.timezone,
|
|
897
|
-
email: identity.profile.email,
|
|
898
|
-
avatar: identity.profile.avatar
|
|
899
|
-
};
|
|
900
|
-
if (recordId) {
|
|
901
|
-
await executor.call(this.host, "feishu_bitable_update_record", {
|
|
902
|
-
app_token: mirror.appToken,
|
|
903
|
-
table_id: mirror.tableId,
|
|
904
|
-
record_id: recordId,
|
|
905
|
-
fields
|
|
906
|
-
});
|
|
907
|
-
} else {
|
|
908
|
-
await executor.call(this.host, "feishu_bitable_create_record", {
|
|
909
|
-
app_token: mirror.appToken,
|
|
910
|
-
table_id: mirror.tableId,
|
|
911
|
-
fields
|
|
912
|
-
});
|
|
913
|
-
}
|
|
914
|
-
logUserBindEvent("feishu-bitable-sync-success", { userId: identity.userId });
|
|
915
|
-
} catch (error) {
|
|
916
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
917
|
-
logUserBindEvent("feishu-bitable-sync-failed", { userId: identity.userId, message });
|
|
918
|
-
this.store.recordIssue("feishu-bitable-sync", message, identity.userId);
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
async ensureFeishuBitableMirror(executor) {
|
|
922
|
-
if (this.bitableMirror?.appToken && this.bitableMirror?.tableId) {
|
|
923
|
-
return this.bitableMirror;
|
|
924
|
-
}
|
|
925
|
-
try {
|
|
926
|
-
const app = await executor("feishu_bitable_create_app", { name: "Bamdra User Bind" });
|
|
927
|
-
const appToken = extractDeepString(app, [
|
|
928
|
-
["data", "app", "app_token"],
|
|
929
|
-
["data", "app_token"],
|
|
930
|
-
["app", "app_token"],
|
|
931
|
-
["app_token"]
|
|
932
|
-
]);
|
|
933
|
-
if (!appToken) {
|
|
934
|
-
return { appToken: null, tableId: null };
|
|
935
|
-
}
|
|
936
|
-
const meta = await executor("feishu_bitable_get_meta", { app_token: appToken });
|
|
937
|
-
const tableId = extractDeepString(meta, [
|
|
938
|
-
["data", "tables", "0", "table_id"],
|
|
939
|
-
["data", "items", "0", "table_id"],
|
|
940
|
-
["tables", "0", "table_id"]
|
|
941
|
-
]);
|
|
942
|
-
if (!tableId) {
|
|
943
|
-
this.store.recordIssue("feishu-bitable-init", "Unable to determine users table id from Feishu bitable metadata");
|
|
944
|
-
return { appToken, tableId: null };
|
|
945
|
-
}
|
|
946
|
-
for (const fieldName of ["user_id", "channel_type", "open_id", "name", "nickname", "preferences", "personality", "role", "timezone", "email", "avatar"]) {
|
|
947
|
-
try {
|
|
948
|
-
await executor("feishu_bitable_create_field", {
|
|
949
|
-
app_token: appToken,
|
|
950
|
-
table_id: tableId,
|
|
951
|
-
field_name: fieldName,
|
|
952
|
-
type: 1
|
|
953
|
-
});
|
|
954
|
-
} catch {
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
this.bitableMirror = { appToken, tableId };
|
|
958
|
-
logUserBindEvent("feishu-bitable-ready", this.bitableMirror);
|
|
959
|
-
return this.bitableMirror;
|
|
960
|
-
} catch (error) {
|
|
961
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
962
|
-
logUserBindEvent("feishu-bitable-init-failed", { message });
|
|
963
|
-
this.store.recordIssue("feishu-bitable-init", message);
|
|
964
|
-
return { appToken: null, tableId: null };
|
|
965
|
-
}
|
|
966
|
-
}
|
|
967
1691
|
};
|
|
968
1692
|
function createUserBindPlugin(api) {
|
|
969
|
-
|
|
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;
|
|
970
1702
|
}
|
|
971
1703
|
function register(api) {
|
|
972
1704
|
createUserBindPlugin(api).register();
|
|
@@ -976,12 +1708,12 @@ async function activate(api) {
|
|
|
976
1708
|
}
|
|
977
1709
|
function normalizeConfig(input) {
|
|
978
1710
|
const root = (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "data", "bamdra-user-bind");
|
|
979
|
-
const storeRoot = input?.localStorePath ?? root;
|
|
1711
|
+
const storeRoot = expandHomePath(input?.localStorePath) ?? root;
|
|
980
1712
|
return {
|
|
981
1713
|
enabled: input?.enabled ?? true,
|
|
982
1714
|
localStorePath: storeRoot,
|
|
983
|
-
exportPath: input?.exportPath ?? (0, import_node_path.join)(storeRoot, "exports"),
|
|
984
|
-
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"),
|
|
985
1717
|
cacheTtlMs: input?.cacheTtlMs ?? 30 * 60 * 1e3,
|
|
986
1718
|
adminAgents: input?.adminAgents?.length ? input.adminAgents : ["main"]
|
|
987
1719
|
};
|
|
@@ -993,6 +1725,9 @@ function exposeGlobalApi(runtime) {
|
|
|
993
1725
|
},
|
|
994
1726
|
async resolveIdentity(context) {
|
|
995
1727
|
return runtime.resolveFromContext(context);
|
|
1728
|
+
},
|
|
1729
|
+
async runPendingBindingSweep() {
|
|
1730
|
+
return runtime.runPendingBindingSweep();
|
|
996
1731
|
}
|
|
997
1732
|
};
|
|
998
1733
|
}
|
|
@@ -1016,17 +1751,18 @@ function bootstrapOpenClawHost(config) {
|
|
|
1016
1751
|
materializeBundledSkill(adminSkillSource, adminSkillTarget);
|
|
1017
1752
|
const original = (0, import_node_fs.readFileSync)(configPath, "utf8");
|
|
1018
1753
|
const parsed = JSON.parse(original);
|
|
1019
|
-
const changed = ensureHostConfig(parsed, config, profileSkillTarget, adminSkillTarget);
|
|
1754
|
+
const changed = ensureHostConfig(parsed, config, packageRoot, profileSkillTarget, adminSkillTarget);
|
|
1020
1755
|
if (!changed) {
|
|
1021
1756
|
return;
|
|
1022
1757
|
}
|
|
1023
1758
|
(0, import_node_fs.writeFileSync)(configPath, `${JSON.stringify(parsed, null, 2)}
|
|
1024
1759
|
`, "utf8");
|
|
1025
1760
|
}
|
|
1026
|
-
function ensureHostConfig(config, pluginConfig, profileSkillTarget, adminSkillTarget) {
|
|
1761
|
+
function ensureHostConfig(config, pluginConfig, packageRoot, profileSkillTarget, adminSkillTarget) {
|
|
1027
1762
|
let changed = false;
|
|
1028
1763
|
const plugins = ensureObject(config, "plugins");
|
|
1029
1764
|
const entries = ensureObject(plugins, "entries");
|
|
1765
|
+
const installs = ensureObject(plugins, "installs");
|
|
1030
1766
|
const load = ensureObject(plugins, "load");
|
|
1031
1767
|
const tools = ensureObject(config, "tools");
|
|
1032
1768
|
const skills = ensureObject(config, "skills");
|
|
@@ -1037,6 +1773,11 @@ function ensureHostConfig(config, pluginConfig, profileSkillTarget, adminSkillTa
|
|
|
1037
1773
|
changed = ensureArrayIncludes(plugins, "allow", PLUGIN_ID) || changed;
|
|
1038
1774
|
changed = ensureArrayIncludes(load, "paths", (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "extensions")) || changed;
|
|
1039
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;
|
|
1040
1781
|
if (entry.enabled !== true) {
|
|
1041
1782
|
entry.enabled = true;
|
|
1042
1783
|
changed = true;
|
|
@@ -1076,6 +1817,46 @@ function materializeBundledSkill(sourceDir, targetDir) {
|
|
|
1076
1817
|
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(targetDir), { recursive: true });
|
|
1077
1818
|
(0, import_node_fs.cpSync)(sourceDir, targetDir, { recursive: true });
|
|
1078
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
|
+
}
|
|
1079
1860
|
function ensureToolNames(tools, values) {
|
|
1080
1861
|
let changed = false;
|
|
1081
1862
|
for (const value of values) {
|
|
@@ -1084,9 +1865,8 @@ function ensureToolNames(tools, values) {
|
|
|
1084
1865
|
return changed;
|
|
1085
1866
|
}
|
|
1086
1867
|
function ensureAgentSkills(agents, skillId) {
|
|
1087
|
-
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
1088
1868
|
let changed = false;
|
|
1089
|
-
for (const item of
|
|
1869
|
+
for (const item of iterAgentConfigs(agents)) {
|
|
1090
1870
|
if (!item || typeof item !== "object") {
|
|
1091
1871
|
continue;
|
|
1092
1872
|
}
|
|
@@ -1101,10 +1881,9 @@ function ensureAgentSkills(agents, skillId) {
|
|
|
1101
1881
|
return changed;
|
|
1102
1882
|
}
|
|
1103
1883
|
function ensureAdminSkill(agents, skillId, adminAgents) {
|
|
1104
|
-
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
1105
1884
|
let changed = false;
|
|
1106
1885
|
let attached = false;
|
|
1107
|
-
for (const item of
|
|
1886
|
+
for (const item of iterAgentConfigs(agents)) {
|
|
1108
1887
|
if (!item || typeof item !== "object") {
|
|
1109
1888
|
continue;
|
|
1110
1889
|
}
|
|
@@ -1121,6 +1900,7 @@ function ensureAdminSkill(agents, skillId, adminAgents) {
|
|
|
1121
1900
|
}
|
|
1122
1901
|
attached = true;
|
|
1123
1902
|
}
|
|
1903
|
+
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
1124
1904
|
if (!attached && list.length > 0 && list[0] && typeof list[0] === "object") {
|
|
1125
1905
|
const agent = list[0];
|
|
1126
1906
|
const current = Array.isArray(agent.skills) ? [...agent.skills] : [];
|
|
@@ -1132,16 +1912,48 @@ function ensureAdminSkill(agents, skillId, adminAgents) {
|
|
|
1132
1912
|
}
|
|
1133
1913
|
return changed;
|
|
1134
1914
|
}
|
|
1915
|
+
function* iterAgentConfigs(agents) {
|
|
1916
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1917
|
+
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
1918
|
+
for (const item of list) {
|
|
1919
|
+
if (item && typeof item === "object") {
|
|
1920
|
+
const agent = item;
|
|
1921
|
+
seen.add(agent);
|
|
1922
|
+
yield agent;
|
|
1923
|
+
}
|
|
1924
|
+
}
|
|
1925
|
+
if (list.length > 0) {
|
|
1926
|
+
return;
|
|
1927
|
+
}
|
|
1928
|
+
for (const [key, value] of Object.entries(agents)) {
|
|
1929
|
+
if (key === "list" || key === "defaults" || !value || typeof value !== "object" || Array.isArray(value)) {
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
const agent = value;
|
|
1933
|
+
if (seen.has(agent)) {
|
|
1934
|
+
continue;
|
|
1935
|
+
}
|
|
1936
|
+
seen.add(agent);
|
|
1937
|
+
if (!getConfiguredAgentId(agent)) {
|
|
1938
|
+
agent.id = key;
|
|
1939
|
+
}
|
|
1940
|
+
yield agent;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1135
1943
|
function mapProfileRow(row) {
|
|
1136
1944
|
return {
|
|
1137
1945
|
userId: String(row.user_id),
|
|
1138
1946
|
name: asNullableString(row.name),
|
|
1139
1947
|
gender: asNullableString(row.gender),
|
|
1948
|
+
birthDate: asNullableString(row.birth_date),
|
|
1949
|
+
birthYear: asNullableString(row.birth_year),
|
|
1950
|
+
age: asNullableString(row.age),
|
|
1140
1951
|
email: asNullableString(row.email),
|
|
1141
1952
|
avatar: asNullableString(row.avatar),
|
|
1142
1953
|
nickname: asNullableString(row.nickname),
|
|
1143
1954
|
preferences: asNullableString(row.preferences),
|
|
1144
1955
|
personality: asNullableString(row.personality),
|
|
1956
|
+
interests: asNullableString(row.interests),
|
|
1145
1957
|
role: asNullableString(row.role),
|
|
1146
1958
|
timezone: asNullableString(row.timezone),
|
|
1147
1959
|
notes: asNullableString(row.notes),
|
|
@@ -1162,12 +1974,12 @@ function parseIdentityContext(context) {
|
|
|
1162
1974
|
const metadataText = asNullableString(record.text) ?? asNullableString(message.text) ?? asNullableString(record.content) ?? asNullableString(metadata.text) ?? asNullableString(input.text) ?? asNullableString(findNestedValue(record, ["message", "content", "text"]));
|
|
1163
1975
|
const conversationInfo = metadataText ? extractTaggedJsonBlock(metadataText, "Conversation info (untrusted metadata)") : null;
|
|
1164
1976
|
const senderInfo = metadataText ? extractTaggedJsonBlock(metadataText, "Sender (untrusted metadata)") : null;
|
|
1165
|
-
const senderIdFromText =
|
|
1977
|
+
const senderIdFromText = extractSenderIdFromMetadataText(metadataText);
|
|
1166
1978
|
const senderNameFromText = metadataText ? extractRegexValue(metadataText, /"sender"\s*:\s*"([^"]+)"/) : null;
|
|
1167
1979
|
const senderNameFromMessageLine = metadataText ? extractRegexValue(metadataText, /\]\s*([^\n::]{1,40})\s*[::]/) : null;
|
|
1168
1980
|
const sessionId = asNullableString(record.sessionKey) ?? asNullableString(record.sessionId) ?? asNullableString(session.id) ?? asNullableString(conversation.id) ?? asNullableString(metadata.sessionId) ?? asNullableString(input.sessionId) ?? asNullableString(input.session?.id) ?? asNullableString(record.context?.sessionId) ?? asNullableString(conversationInfo?.session_id) ?? asNullableString(conversationInfo?.message_id);
|
|
1169
|
-
const channelType = asNullableString(record.channelType) ?? asNullableString(channel.type) ?? asNullableString(metadata.channelType) ?? asNullableString(conversation?.provider) ?? asNullableString(record.provider) ?? asNullableString(conversationInfo?.provider) ?? inferChannelTypeFromSessionId(sessionId);
|
|
1170
|
-
const openId =
|
|
1981
|
+
const channelType = asNullableString(record.channelType) ?? asNullableString(channel.type) ?? asNullableString(metadata.channelType) ?? asNullableString(conversation?.provider) ?? asNullableString(record.provider) ?? asNullableString(conversationInfo?.provider) ?? inferChannelTypeFromSenderId(extractSenderId(record, sender, senderInfo, conversationInfo, message, senderIdFromText)) ?? inferChannelTypeFromSessionId(sessionId);
|
|
1982
|
+
const openId = extractSenderId(record, sender, senderInfo, conversationInfo, message, senderIdFromText) ?? extractOpenIdFromSessionId(sessionId);
|
|
1171
1983
|
const senderName = asNullableString(sender.name) ?? asNullableString(sender.display_name) ?? asNullableString(senderInfo?.name) ?? asNullableString(conversationInfo?.sender) ?? senderNameFromText ?? senderNameFromMessageLine;
|
|
1172
1984
|
return {
|
|
1173
1985
|
sessionId,
|
|
@@ -1176,102 +1988,629 @@ function parseIdentityContext(context) {
|
|
|
1176
1988
|
senderName
|
|
1177
1989
|
};
|
|
1178
1990
|
}
|
|
1179
|
-
function
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
}
|
|
1991
|
+
function enrichIdentityContext(context) {
|
|
1992
|
+
const record = context && typeof context === "object" ? { ...context } : {};
|
|
1993
|
+
const sessionManager = record.sessionManager && typeof record.sessionManager === "object" ? record.sessionManager : null;
|
|
1994
|
+
if (!sessionManager) {
|
|
1995
|
+
return record;
|
|
1185
1996
|
}
|
|
1186
|
-
|
|
1997
|
+
const sessionSnapshot = readSessionManagerSnapshot(sessionManager);
|
|
1998
|
+
if (!sessionSnapshot) {
|
|
1999
|
+
return record;
|
|
2000
|
+
}
|
|
2001
|
+
if (!record.sessionId && sessionSnapshot.sessionId) {
|
|
2002
|
+
record.sessionId = sessionSnapshot.sessionId;
|
|
2003
|
+
}
|
|
2004
|
+
if (!record.text && sessionSnapshot.metadataText) {
|
|
2005
|
+
record.text = sessionSnapshot.metadataText;
|
|
2006
|
+
}
|
|
2007
|
+
const metadata = record.metadata && typeof record.metadata === "object" ? { ...record.metadata } : {};
|
|
2008
|
+
if (!metadata.text && sessionSnapshot.metadataText) {
|
|
2009
|
+
metadata.text = sessionSnapshot.metadataText;
|
|
2010
|
+
}
|
|
2011
|
+
if (!metadata.sessionId && sessionSnapshot.sessionId) {
|
|
2012
|
+
metadata.sessionId = sessionSnapshot.sessionId;
|
|
2013
|
+
}
|
|
2014
|
+
if (Object.keys(metadata).length > 0) {
|
|
2015
|
+
record.metadata = metadata;
|
|
2016
|
+
}
|
|
2017
|
+
if (!record.channelType && sessionSnapshot.channelType) {
|
|
2018
|
+
record.channelType = sessionSnapshot.channelType;
|
|
2019
|
+
}
|
|
2020
|
+
if (!record.openId && sessionSnapshot.openId) {
|
|
2021
|
+
record.openId = sessionSnapshot.openId;
|
|
2022
|
+
}
|
|
2023
|
+
return record;
|
|
1187
2024
|
}
|
|
1188
|
-
function
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
2025
|
+
function readSessionManagerSnapshot(sessionManager) {
|
|
2026
|
+
try {
|
|
2027
|
+
const getSessionId = sessionManager.getSessionId;
|
|
2028
|
+
const getBranch = sessionManager.getBranch;
|
|
2029
|
+
const sessionId = typeof getSessionId === "function" ? asNullableString(getSessionId()) : null;
|
|
2030
|
+
const branch = typeof getBranch === "function" ? getBranch() : [];
|
|
2031
|
+
if (!Array.isArray(branch) || branch.length === 0) {
|
|
2032
|
+
return sessionId ? {
|
|
2033
|
+
sessionId,
|
|
2034
|
+
metadataText: null,
|
|
2035
|
+
channelType: inferChannelTypeFromSessionId(sessionId),
|
|
2036
|
+
openId: extractOpenIdFromSessionId(sessionId)
|
|
2037
|
+
} : null;
|
|
1193
2038
|
}
|
|
1194
|
-
|
|
2039
|
+
for (let i = branch.length - 1; i >= 0; i -= 1) {
|
|
2040
|
+
const entry = branch[i];
|
|
2041
|
+
if (!entry || typeof entry !== "object") {
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
const item = entry;
|
|
2045
|
+
const message = item.message && typeof item.message === "object" ? item.message : null;
|
|
2046
|
+
if (item.type !== "message" || !message || message.role !== "user") {
|
|
2047
|
+
continue;
|
|
2048
|
+
}
|
|
2049
|
+
const metadataText = extractMessageText(message);
|
|
2050
|
+
if (!metadataText || !looksLikeIdentityMetadata(metadataText)) {
|
|
2051
|
+
continue;
|
|
2052
|
+
}
|
|
2053
|
+
const openId = extractSenderIdFromMetadataText(metadataText);
|
|
2054
|
+
const channelType = inferChannelTypeFromSenderId(openId) ?? inferChannelTypeFromSessionId(sessionId);
|
|
2055
|
+
return {
|
|
2056
|
+
sessionId,
|
|
2057
|
+
metadataText,
|
|
2058
|
+
channelType,
|
|
2059
|
+
openId
|
|
2060
|
+
};
|
|
2061
|
+
}
|
|
2062
|
+
return sessionId ? {
|
|
2063
|
+
sessionId,
|
|
2064
|
+
metadataText: null,
|
|
2065
|
+
channelType: inferChannelTypeFromSessionId(sessionId),
|
|
2066
|
+
openId: extractOpenIdFromSessionId(sessionId)
|
|
2067
|
+
} : null;
|
|
2068
|
+
} catch (error) {
|
|
2069
|
+
logUserBindEvent("session-manager-read-failed", {
|
|
2070
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2071
|
+
});
|
|
2072
|
+
return null;
|
|
1195
2073
|
}
|
|
1196
|
-
return current;
|
|
1197
2074
|
}
|
|
1198
|
-
function
|
|
1199
|
-
const
|
|
1200
|
-
if (
|
|
2075
|
+
function extractMessageText(message) {
|
|
2076
|
+
const content = message.content;
|
|
2077
|
+
if (typeof content === "string") {
|
|
2078
|
+
return content;
|
|
2079
|
+
}
|
|
2080
|
+
if (!Array.isArray(content)) {
|
|
1201
2081
|
return null;
|
|
1202
2082
|
}
|
|
1203
|
-
const
|
|
1204
|
-
|
|
2083
|
+
const text = content.filter((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string").map((part) => String(part.text)).join("\n");
|
|
2084
|
+
return text || null;
|
|
2085
|
+
}
|
|
2086
|
+
function looksLikeIdentityMetadata(text) {
|
|
2087
|
+
return text.includes("Conversation info (untrusted metadata)") || text.includes("Sender (untrusted metadata)") || /"sender_id"\s*:\s*"/.test(text);
|
|
2088
|
+
}
|
|
2089
|
+
function extractUserUtterance(context) {
|
|
2090
|
+
const record = context && typeof context === "object" ? context : {};
|
|
2091
|
+
const rawText = extractHookContextText(record) ?? readLatestUserMessageFromSessionManager(record.sessionManager);
|
|
2092
|
+
if (!rawText) {
|
|
1205
2093
|
return null;
|
|
1206
2094
|
}
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
return parsed && typeof parsed === "object" ? parsed : null;
|
|
1210
|
-
} catch {
|
|
2095
|
+
const stripped = stripIdentityMetadata(rawText);
|
|
2096
|
+
if (!stripped) {
|
|
1211
2097
|
return null;
|
|
1212
2098
|
}
|
|
1213
|
-
|
|
1214
|
-
function inferChannelTypeFromSessionId(sessionId) {
|
|
1215
|
-
if (!sessionId) {
|
|
2099
|
+
if (looksLikeIdentityMetadata(stripped)) {
|
|
1216
2100
|
return null;
|
|
1217
2101
|
}
|
|
1218
|
-
|
|
1219
|
-
|
|
2102
|
+
return stripped;
|
|
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;
|
|
1220
2109
|
}
|
|
1221
|
-
|
|
1222
|
-
|
|
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;
|
|
1223
2119
|
}
|
|
1224
|
-
if (
|
|
1225
|
-
return
|
|
2120
|
+
if (isTrivialSemanticCaptureUtterance(normalized)) {
|
|
2121
|
+
return true;
|
|
1226
2122
|
}
|
|
1227
|
-
|
|
2123
|
+
const tokenCount = normalized.split(/\s+/).filter(Boolean).length;
|
|
2124
|
+
if (normalized.length <= 4) {
|
|
2125
|
+
return true;
|
|
2126
|
+
}
|
|
2127
|
+
return tokenCount <= 1 && !containsCjkCharacters(normalized);
|
|
1228
2128
|
}
|
|
1229
|
-
function
|
|
1230
|
-
const
|
|
1231
|
-
return
|
|
2129
|
+
function shouldIgnoreSemanticProfileCaptureCandidate(text) {
|
|
2130
|
+
const normalized = normalizeSemanticCaptureText(text);
|
|
2131
|
+
return !normalized || isTrivialSemanticCaptureUtterance(normalized);
|
|
1232
2132
|
}
|
|
1233
|
-
function
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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);
|
|
1239
2149
|
}
|
|
1240
|
-
function
|
|
1241
|
-
|
|
1242
|
-
return asNullableString(record.agentId) ?? asNullableString(record.agent?.id) ?? asNullableString(record.agent?.name);
|
|
2150
|
+
function normalizeSemanticCaptureText(text) {
|
|
2151
|
+
return text.trim().toLowerCase().replace(/[!?.,,。!?、;;::"'`~()\[\]{}<>@#%^&*_+=|\\/.-]+/g, " ").replace(/\s+/g, " ").trim();
|
|
1243
2152
|
}
|
|
1244
|
-
function
|
|
1245
|
-
return
|
|
1246
|
-
nickname: asNullableString(params.nickname),
|
|
1247
|
-
preferences: asNullableString(params.preferences),
|
|
1248
|
-
personality: asNullableString(params.personality),
|
|
1249
|
-
role: asNullableString(params.role),
|
|
1250
|
-
timezone: asNullableString(params.timezone),
|
|
1251
|
-
notes: asNullableString(params.notes)
|
|
1252
|
-
};
|
|
2153
|
+
function containsCjkCharacters(text) {
|
|
2154
|
+
return /[\u3400-\u9fff\uf900-\ufaff]/.test(text);
|
|
1253
2155
|
}
|
|
1254
|
-
function
|
|
1255
|
-
const
|
|
1256
|
-
if (
|
|
1257
|
-
return
|
|
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;
|
|
1258
2172
|
}
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
2173
|
+
const combined = messages.map((item) => item.trim()).filter(Boolean).join("\n");
|
|
2174
|
+
return combined || null;
|
|
2175
|
+
}
|
|
2176
|
+
function extractHookContextText(record) {
|
|
2177
|
+
const directText = normalizeHookText(
|
|
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"])
|
|
2179
|
+
);
|
|
2180
|
+
if (directText) {
|
|
2181
|
+
return directText;
|
|
1262
2182
|
}
|
|
1263
|
-
|
|
1264
|
-
|
|
2183
|
+
const message = findNestedRecord(record, ["message"], ["event", "message"], ["payload", "message"]);
|
|
2184
|
+
const messageText = extractMessageText(message);
|
|
2185
|
+
if (messageText) {
|
|
2186
|
+
return messageText;
|
|
1265
2187
|
}
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
userIds: extractUserIds(normalized),
|
|
1270
|
-
patch: extractProfilePatch(normalized)
|
|
1271
|
-
};
|
|
2188
|
+
const inputText = extractTextFromInput(record.input);
|
|
2189
|
+
if (inputText) {
|
|
2190
|
+
return inputText;
|
|
1272
2191
|
}
|
|
1273
|
-
|
|
1274
|
-
|
|
2192
|
+
const messages = Array.isArray(record.messages) ? record.messages : [];
|
|
2193
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
2194
|
+
const item = messages[i];
|
|
2195
|
+
if (!item || typeof item !== "object") {
|
|
2196
|
+
continue;
|
|
2197
|
+
}
|
|
2198
|
+
const messageRecord = item;
|
|
2199
|
+
const role = asNullableString(messageRecord.role) ?? "user";
|
|
2200
|
+
if (role !== "user") {
|
|
2201
|
+
continue;
|
|
2202
|
+
}
|
|
2203
|
+
const text = normalizeHookText(messageRecord.text ?? messageRecord.content);
|
|
2204
|
+
if (text) {
|
|
2205
|
+
return text;
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
return null;
|
|
2209
|
+
}
|
|
2210
|
+
function extractTextFromInput(input) {
|
|
2211
|
+
if (typeof input === "string") {
|
|
2212
|
+
return normalizeHookText(input);
|
|
2213
|
+
}
|
|
2214
|
+
if (!input || typeof input !== "object") {
|
|
2215
|
+
return null;
|
|
2216
|
+
}
|
|
2217
|
+
const record = input;
|
|
2218
|
+
const directText = normalizeHookText(record.text ?? record.content);
|
|
2219
|
+
if (directText) {
|
|
2220
|
+
return directText;
|
|
2221
|
+
}
|
|
2222
|
+
const message = record.message && typeof record.message === "object" ? record.message : null;
|
|
2223
|
+
const messageText = normalizeHookText(message?.text ?? message?.content);
|
|
2224
|
+
if (messageText) {
|
|
2225
|
+
return messageText;
|
|
2226
|
+
}
|
|
2227
|
+
const messages = Array.isArray(record.messages) ? record.messages : [];
|
|
2228
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
2229
|
+
const item = messages[i];
|
|
2230
|
+
if (!item || typeof item !== "object") {
|
|
2231
|
+
continue;
|
|
2232
|
+
}
|
|
2233
|
+
const messageRecord = item;
|
|
2234
|
+
const role = asNullableString(messageRecord.role) ?? "user";
|
|
2235
|
+
if (role !== "user") {
|
|
2236
|
+
continue;
|
|
2237
|
+
}
|
|
2238
|
+
const text = normalizeHookText(messageRecord.text ?? messageRecord.content);
|
|
2239
|
+
if (text) {
|
|
2240
|
+
return text;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
return null;
|
|
2244
|
+
}
|
|
2245
|
+
function normalizeHookText(value) {
|
|
2246
|
+
if (typeof value === "string") {
|
|
2247
|
+
return value.trim() || null;
|
|
2248
|
+
}
|
|
2249
|
+
if (Array.isArray(value)) {
|
|
2250
|
+
const text = value.map((item) => {
|
|
2251
|
+
if (!item || typeof item !== "object") {
|
|
2252
|
+
return "";
|
|
2253
|
+
}
|
|
2254
|
+
return asNullableString(item.text) ?? "";
|
|
2255
|
+
}).filter(Boolean).join("\n").trim();
|
|
2256
|
+
return text || null;
|
|
2257
|
+
}
|
|
2258
|
+
return null;
|
|
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
|
+
}
|
|
2342
|
+
function readLatestUserMessageFromSessionManager(sessionManager) {
|
|
2343
|
+
if (!sessionManager || typeof sessionManager !== "object") {
|
|
2344
|
+
return null;
|
|
2345
|
+
}
|
|
2346
|
+
try {
|
|
2347
|
+
const getBranch = sessionManager.getBranch;
|
|
2348
|
+
const branch = typeof getBranch === "function" ? getBranch() : [];
|
|
2349
|
+
if (!Array.isArray(branch) || branch.length === 0) {
|
|
2350
|
+
return null;
|
|
2351
|
+
}
|
|
2352
|
+
for (let i = branch.length - 1; i >= 0; i -= 1) {
|
|
2353
|
+
const entry = branch[i];
|
|
2354
|
+
if (!entry || typeof entry !== "object") {
|
|
2355
|
+
continue;
|
|
2356
|
+
}
|
|
2357
|
+
const item = entry;
|
|
2358
|
+
const message = item.message && typeof item.message === "object" ? item.message : null;
|
|
2359
|
+
if (item.type !== "message" || !message || message.role !== "user") {
|
|
2360
|
+
continue;
|
|
2361
|
+
}
|
|
2362
|
+
const text = extractMessageText(message);
|
|
2363
|
+
if (text && !looksLikeIdentityMetadata(text)) {
|
|
2364
|
+
return text.trim();
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
} catch (error) {
|
|
2368
|
+
logUserBindEvent("session-manager-user-message-read-failed", {
|
|
2369
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2370
|
+
});
|
|
2371
|
+
}
|
|
2372
|
+
return null;
|
|
2373
|
+
}
|
|
2374
|
+
function stripIdentityMetadata(text) {
|
|
2375
|
+
const hadMetadata = looksLikeIdentityMetadata(text);
|
|
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, "");
|
|
2377
|
+
if (hadMetadata) {
|
|
2378
|
+
cleaned = cleaned.replace(/^\s*[^\n::]{1,40}[::]\s*/m, "");
|
|
2379
|
+
}
|
|
2380
|
+
cleaned = cleaned.trim();
|
|
2381
|
+
return cleaned || null;
|
|
2382
|
+
}
|
|
2383
|
+
function findNestedRecord(root, ...paths) {
|
|
2384
|
+
for (const path of paths) {
|
|
2385
|
+
const value = findNestedValue(root, path);
|
|
2386
|
+
if (value && typeof value === "object") {
|
|
2387
|
+
return value;
|
|
2388
|
+
}
|
|
2389
|
+
}
|
|
2390
|
+
return {};
|
|
2391
|
+
}
|
|
2392
|
+
function findNestedValue(root, path) {
|
|
2393
|
+
let current = root;
|
|
2394
|
+
for (const part of path) {
|
|
2395
|
+
if (!current || typeof current !== "object") {
|
|
2396
|
+
return null;
|
|
2397
|
+
}
|
|
2398
|
+
current = current[part];
|
|
2399
|
+
}
|
|
2400
|
+
return current;
|
|
2401
|
+
}
|
|
2402
|
+
function extractTaggedJsonBlock(text, label) {
|
|
2403
|
+
const start = text.indexOf(label);
|
|
2404
|
+
if (start < 0) {
|
|
2405
|
+
return null;
|
|
2406
|
+
}
|
|
2407
|
+
const block = text.slice(start).match(/```json\s*([\s\S]*?)\s*```/i);
|
|
2408
|
+
if (!block) {
|
|
2409
|
+
return null;
|
|
2410
|
+
}
|
|
2411
|
+
try {
|
|
2412
|
+
const parsed = JSON.parse(block[1]);
|
|
2413
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
2414
|
+
} catch {
|
|
2415
|
+
return null;
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
function inferChannelTypeFromSessionId(sessionId) {
|
|
2419
|
+
if (!sessionId) {
|
|
2420
|
+
return null;
|
|
2421
|
+
}
|
|
2422
|
+
if (sessionId.includes(":feishu:")) {
|
|
2423
|
+
return "feishu";
|
|
2424
|
+
}
|
|
2425
|
+
if (sessionId.includes(":telegram:")) {
|
|
2426
|
+
return "telegram";
|
|
2427
|
+
}
|
|
2428
|
+
if (sessionId.includes(":whatsapp:")) {
|
|
2429
|
+
return "whatsapp";
|
|
2430
|
+
}
|
|
2431
|
+
if (sessionId.includes(":discord:")) {
|
|
2432
|
+
return "discord";
|
|
2433
|
+
}
|
|
2434
|
+
if (sessionId.includes(":googlechat:")) {
|
|
2435
|
+
return "googlechat";
|
|
2436
|
+
}
|
|
2437
|
+
if (sessionId.includes(":slack:")) {
|
|
2438
|
+
return "slack";
|
|
2439
|
+
}
|
|
2440
|
+
if (sessionId.includes(":mattermost:")) {
|
|
2441
|
+
return "mattermost";
|
|
2442
|
+
}
|
|
2443
|
+
if (sessionId.includes(":signal:")) {
|
|
2444
|
+
return "signal";
|
|
2445
|
+
}
|
|
2446
|
+
if (sessionId.includes(":imessage:")) {
|
|
2447
|
+
return "imessage";
|
|
2448
|
+
}
|
|
2449
|
+
if (sessionId.includes(":msteams:")) {
|
|
2450
|
+
return "msteams";
|
|
2451
|
+
}
|
|
2452
|
+
return null;
|
|
2453
|
+
}
|
|
2454
|
+
function inferChannelTypeFromSenderId(senderId) {
|
|
2455
|
+
if (!senderId) {
|
|
2456
|
+
return null;
|
|
2457
|
+
}
|
|
2458
|
+
if (/^(?:ou|oc)_[A-Za-z0-9_-]+$/.test(senderId)) {
|
|
2459
|
+
return "feishu";
|
|
2460
|
+
}
|
|
2461
|
+
if (/@(?:s\.whatsapp\.net|g\.us)$/.test(senderId)) {
|
|
2462
|
+
return "whatsapp";
|
|
2463
|
+
}
|
|
2464
|
+
if (/^users\/.+/.test(senderId) || /^spaces\/.+/.test(senderId)) {
|
|
2465
|
+
return "googlechat";
|
|
2466
|
+
}
|
|
2467
|
+
return null;
|
|
2468
|
+
}
|
|
2469
|
+
function extractSenderId(record, sender, senderInfo, conversationInfo, message, senderIdFromText) {
|
|
2470
|
+
return firstNonEmptyString(
|
|
2471
|
+
asNullableString(record.openId),
|
|
2472
|
+
asNullableString(record.senderId),
|
|
2473
|
+
asNullableString(record.userId),
|
|
2474
|
+
asNullableString(record.fromId),
|
|
2475
|
+
asNullableString(record.participantId),
|
|
2476
|
+
asNullableString(record.authorId),
|
|
2477
|
+
asNullableString(sender.id),
|
|
2478
|
+
asNullableString(sender.open_id),
|
|
2479
|
+
asNullableString(sender.openId),
|
|
2480
|
+
asNullableString(sender.user_id),
|
|
2481
|
+
asNullableString(sender.userId),
|
|
2482
|
+
asNullableString(sender.sender_id),
|
|
2483
|
+
asNullableString(sender.senderId),
|
|
2484
|
+
asNullableString(sender.from_id),
|
|
2485
|
+
asNullableString(sender.fromId),
|
|
2486
|
+
asNullableString(sender.author_id),
|
|
2487
|
+
asNullableString(sender.authorId),
|
|
2488
|
+
asNullableString(sender.chat_id),
|
|
2489
|
+
asNullableString(sender.chatId),
|
|
2490
|
+
asNullableString(sender.participant),
|
|
2491
|
+
asNullableString(sender.participant_id),
|
|
2492
|
+
asNullableString(sender.participantId),
|
|
2493
|
+
asNullableString(sender.jid),
|
|
2494
|
+
asNullableString(sender.handle),
|
|
2495
|
+
asNullableString(sender.username),
|
|
2496
|
+
asNullableString(sender.phone),
|
|
2497
|
+
asNullableString(sender.phone_number),
|
|
2498
|
+
asNullableString(findNestedValue(sender, ["from", "id"])),
|
|
2499
|
+
asNullableString(findNestedValue(sender, ["author", "id"])),
|
|
2500
|
+
asNullableString(findNestedValue(message, ["from", "id"])),
|
|
2501
|
+
asNullableString(findNestedValue(message, ["author", "id"])),
|
|
2502
|
+
asNullableString(findNestedValue(message, ["user", "id"])),
|
|
2503
|
+
asNullableString(senderInfo?.id),
|
|
2504
|
+
asNullableString(senderInfo?.user_id),
|
|
2505
|
+
asNullableString(senderInfo?.sender_id),
|
|
2506
|
+
asNullableString(conversationInfo?.sender_id),
|
|
2507
|
+
asNullableString(conversationInfo?.user_id),
|
|
2508
|
+
asNullableString(conversationInfo?.from_id),
|
|
2509
|
+
senderIdFromText
|
|
2510
|
+
);
|
|
2511
|
+
}
|
|
2512
|
+
function extractSenderIdFromMetadataText(metadataText) {
|
|
2513
|
+
if (!metadataText) {
|
|
2514
|
+
return null;
|
|
2515
|
+
}
|
|
2516
|
+
return firstNonEmptyString(
|
|
2517
|
+
extractRegexValue(metadataText, /"sender_id"\s*:\s*"([^"]+)"/),
|
|
2518
|
+
extractRegexValue(metadataText, /"user_id"\s*:\s*"([^"]+)"/),
|
|
2519
|
+
extractRegexValue(metadataText, /"from_id"\s*:\s*"([^"]+)"/),
|
|
2520
|
+
extractRegexValue(metadataText, /"author_id"\s*:\s*"([^"]+)"/),
|
|
2521
|
+
extractRegexValue(metadataText, /"chat_id"\s*:\s*"([^"]+)"/),
|
|
2522
|
+
extractRegexValue(metadataText, /"participant"\s*:\s*"([^"]+)"/),
|
|
2523
|
+
extractRegexValue(metadataText, /"jid"\s*:\s*"([^"]+)"/),
|
|
2524
|
+
extractRegexValue(metadataText, /"id"\s*:\s*"((?:ou|oc)_[^"]+)"/)
|
|
2525
|
+
);
|
|
2526
|
+
}
|
|
2527
|
+
function firstNonEmptyString(...values) {
|
|
2528
|
+
for (const value of values) {
|
|
2529
|
+
if (typeof value === "string" && value.trim()) {
|
|
2530
|
+
return value.trim();
|
|
2531
|
+
}
|
|
2532
|
+
}
|
|
2533
|
+
return null;
|
|
2534
|
+
}
|
|
2535
|
+
function extractRegexValue(text, pattern) {
|
|
2536
|
+
const match = text.match(pattern);
|
|
2537
|
+
return match?.[1]?.trim() || null;
|
|
2538
|
+
}
|
|
2539
|
+
function extractOpenIdFromSessionId(sessionId) {
|
|
2540
|
+
if (!sessionId) {
|
|
2541
|
+
return null;
|
|
2542
|
+
}
|
|
2543
|
+
for (const channel of ["feishu", "telegram", "whatsapp", "discord", "googlechat", "slack", "mattermost", "signal", "imessage", "msteams"]) {
|
|
2544
|
+
const marker = `:${channel}:`;
|
|
2545
|
+
const channelIndex = sessionId.indexOf(marker);
|
|
2546
|
+
if (channelIndex < 0) {
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
const remainder = sessionId.slice(channelIndex + marker.length);
|
|
2550
|
+
const modeSeparator = remainder.indexOf(":");
|
|
2551
|
+
if (modeSeparator < 0) {
|
|
2552
|
+
continue;
|
|
2553
|
+
}
|
|
2554
|
+
const senderId = remainder.slice(modeSeparator + 1).trim();
|
|
2555
|
+
if (senderId) {
|
|
2556
|
+
return senderId;
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
const match = sessionId.match(/:([^:]+)$/);
|
|
2560
|
+
return match?.[1]?.trim() || null;
|
|
2561
|
+
}
|
|
2562
|
+
function getAgentIdFromContext(context) {
|
|
2563
|
+
const record = context && typeof context === "object" ? context : {};
|
|
2564
|
+
return asNullableString(record.agentId) ?? asNullableString(record.agent?.id) ?? asNullableString(record.agent?.name);
|
|
2565
|
+
}
|
|
2566
|
+
function sanitizeProfilePatch(params) {
|
|
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),
|
|
2573
|
+
nickname: asNullableString(params.nickname),
|
|
2574
|
+
preferences: asNullableString(params.preferences),
|
|
2575
|
+
personality: asNullableString(params.personality),
|
|
2576
|
+
interests: asNullableString(params.interests),
|
|
2577
|
+
role: asNullableString(params.role),
|
|
2578
|
+
timezone: asNullableString(params.timezone),
|
|
2579
|
+
notes: asNullableString(params.notes)
|
|
2580
|
+
};
|
|
2581
|
+
}
|
|
2582
|
+
function extractProfilePatchOperations(params) {
|
|
2583
|
+
const operations = {};
|
|
2584
|
+
const fields = ["name", "gender", "birthDate", "birthYear", "age", "nickname", "preferences", "personality", "interests", "role", "timezone", "notes"];
|
|
2585
|
+
for (const field of fields) {
|
|
2586
|
+
const raw = asNullableString(params[`${field}Operation`]) ?? asNullableString(params[`${field}_operation`]);
|
|
2587
|
+
if (raw === "replace" || raw === "append" || raw === "remove") {
|
|
2588
|
+
operations[field] = raw;
|
|
2589
|
+
}
|
|
2590
|
+
}
|
|
2591
|
+
return Object.keys(operations).length > 0 ? operations : void 0;
|
|
2592
|
+
}
|
|
2593
|
+
function parseAdminInstruction(instruction) {
|
|
2594
|
+
const normalized = instruction.trim();
|
|
2595
|
+
if (/issue|问题|失败/i.test(normalized)) {
|
|
2596
|
+
return { action: "list_issues", userIds: [], patch: {} };
|
|
2597
|
+
}
|
|
2598
|
+
if (/merge|合并/i.test(normalized)) {
|
|
2599
|
+
const userIds = normalized.match(/user[:=]([A-Za-z0-9:_-]+)/g)?.map((item) => item.split(/[:=]/)[1]) ?? [];
|
|
2600
|
+
return { action: "merge", userIds, patch: {} };
|
|
2601
|
+
}
|
|
2602
|
+
if (/sync|重同步|同步/i.test(normalized)) {
|
|
2603
|
+
return { action: "sync", userIds: extractUserIds(normalized), patch: {} };
|
|
2604
|
+
}
|
|
2605
|
+
if (/edit|修改|改成|更新/i.test(normalized)) {
|
|
2606
|
+
return {
|
|
2607
|
+
action: "edit",
|
|
2608
|
+
userIds: extractUserIds(normalized),
|
|
2609
|
+
patch: extractProfilePatch(normalized)
|
|
2610
|
+
};
|
|
2611
|
+
}
|
|
2612
|
+
return {
|
|
2613
|
+
action: "query",
|
|
1275
2614
|
userIds: extractUserIds(normalized),
|
|
1276
2615
|
patch: {}
|
|
1277
2616
|
};
|
|
@@ -1281,11 +2620,32 @@ function extractUserIds(input) {
|
|
|
1281
2620
|
}
|
|
1282
2621
|
function extractProfilePatch(input) {
|
|
1283
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);
|
|
1284
2628
|
const nickname = input.match(/(?:nickname|称呼)[=:: ]([^,,]+)$/i) ?? input.match(/(?:nickname|称呼)[=:: ]([^,,]+)/i);
|
|
1285
2629
|
const role = input.match(/(?:role|职责|角色)[=:: ]([^,,]+)/i);
|
|
1286
2630
|
const preferences = input.match(/(?:preferences|偏好)[=:: ]([^,,]+)/i);
|
|
1287
2631
|
const personality = input.match(/(?:personality|性格)[=:: ]([^,,]+)/i);
|
|
2632
|
+
const interests = input.match(/(?:interests|兴趣|爱好)[=:: ]([^,,]+)/i);
|
|
1288
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
|
+
}
|
|
1289
2649
|
if (nickname) {
|
|
1290
2650
|
patch.nickname = nickname[1].trim();
|
|
1291
2651
|
}
|
|
@@ -1298,6 +2658,9 @@ function extractProfilePatch(input) {
|
|
|
1298
2658
|
if (personality) {
|
|
1299
2659
|
patch.personality = personality[1].trim();
|
|
1300
2660
|
}
|
|
2661
|
+
if (interests) {
|
|
2662
|
+
patch.interests = interests[1].trim();
|
|
2663
|
+
}
|
|
1301
2664
|
if (timezone) {
|
|
1302
2665
|
patch.timezone = timezone[1].trim();
|
|
1303
2666
|
}
|
|
@@ -1311,6 +2674,18 @@ function renderIdentityContext(identity) {
|
|
|
1311
2674
|
if (identity.profile.name) {
|
|
1312
2675
|
lines.push(`Name: ${identity.profile.name}`);
|
|
1313
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
|
+
}
|
|
1314
2689
|
if (identity.profile.nickname) {
|
|
1315
2690
|
lines.push(`Preferred address: ${identity.profile.nickname}`);
|
|
1316
2691
|
}
|
|
@@ -1323,6 +2698,9 @@ function renderIdentityContext(identity) {
|
|
|
1323
2698
|
if (identity.profile.personality) {
|
|
1324
2699
|
lines.push(`Personality: ${identity.profile.personality}`);
|
|
1325
2700
|
}
|
|
2701
|
+
if (identity.profile.interests) {
|
|
2702
|
+
lines.push(`Interests: ${identity.profile.interests}`);
|
|
2703
|
+
}
|
|
1326
2704
|
if (identity.profile.role) {
|
|
1327
2705
|
lines.push(`Role: ${identity.profile.role}`);
|
|
1328
2706
|
}
|
|
@@ -1332,44 +2710,142 @@ function renderIdentityContext(identity) {
|
|
|
1332
2710
|
return lines.join("\n");
|
|
1333
2711
|
}
|
|
1334
2712
|
function renderProfileMarkdown(profile) {
|
|
1335
|
-
const
|
|
2713
|
+
const notes = sanitizeProfileNotes(profile.notes) ?? defaultProfileNotes();
|
|
2714
|
+
const frontmatterLines = [
|
|
1336
2715
|
"---",
|
|
1337
|
-
`userId: ${escapeFrontmatter(profile.userId)}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
`
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
`
|
|
2716
|
+
`userId: ${escapeFrontmatter(profile.userId)}`
|
|
2717
|
+
];
|
|
2718
|
+
if (profile.name) {
|
|
2719
|
+
frontmatterLines.push(`name: ${escapeFrontmatter(profile.name)}`);
|
|
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
|
+
}
|
|
2733
|
+
if (profile.nickname) {
|
|
2734
|
+
frontmatterLines.push(`nickname: ${escapeFrontmatter(profile.nickname)}`);
|
|
2735
|
+
}
|
|
2736
|
+
if (profile.timezone) {
|
|
2737
|
+
frontmatterLines.push(`timezone: ${escapeFrontmatter(profile.timezone)}`);
|
|
2738
|
+
}
|
|
2739
|
+
if (profile.preferences) {
|
|
2740
|
+
frontmatterLines.push(`preferences: ${escapeFrontmatter(profile.preferences)}`);
|
|
2741
|
+
}
|
|
2742
|
+
if (profile.personality) {
|
|
2743
|
+
frontmatterLines.push(`personality: ${escapeFrontmatter(profile.personality)}`);
|
|
2744
|
+
}
|
|
2745
|
+
if (profile.interests) {
|
|
2746
|
+
frontmatterLines.push(`interests: ${escapeFrontmatter(profile.interests)}`);
|
|
2747
|
+
}
|
|
2748
|
+
if (profile.role) {
|
|
2749
|
+
frontmatterLines.push(`role: ${escapeFrontmatter(profile.role)}`);
|
|
2750
|
+
}
|
|
2751
|
+
frontmatterLines.push(
|
|
1344
2752
|
`visibility: ${escapeFrontmatter(profile.visibility)}`,
|
|
1345
2753
|
`source: ${escapeFrontmatter(profile.source)}`,
|
|
1346
2754
|
`updatedAt: ${escapeFrontmatter(profile.updatedAt)}`,
|
|
2755
|
+
`syncHash: ${escapeFrontmatter(computeProfilePayloadHash({
|
|
2756
|
+
name: profile.name,
|
|
2757
|
+
gender: profile.gender,
|
|
2758
|
+
birthDate: profile.birthDate,
|
|
2759
|
+
birthYear: profile.birthYear,
|
|
2760
|
+
age: profile.age,
|
|
2761
|
+
nickname: profile.nickname,
|
|
2762
|
+
timezone: profile.timezone,
|
|
2763
|
+
preferences: profile.preferences,
|
|
2764
|
+
personality: profile.personality,
|
|
2765
|
+
interests: profile.interests,
|
|
2766
|
+
role: profile.role,
|
|
2767
|
+
visibility: profile.visibility
|
|
2768
|
+
}, notes))}`,
|
|
1347
2769
|
"---"
|
|
1348
|
-
|
|
1349
|
-
const
|
|
2770
|
+
);
|
|
2771
|
+
const frontmatter = frontmatterLines.join("\n");
|
|
2772
|
+
const confirmedProfileLines = renderConfirmedProfileSection(profile);
|
|
1350
2773
|
return `${frontmatter}
|
|
1351
2774
|
|
|
1352
2775
|
# \u7528\u6237\u753B\u50CF
|
|
1353
2776
|
|
|
1354
|
-
\u8FD9\u4E2A\u6587\u4EF6\u662F\
|
|
2777
|
+
\u8FD9\u4E2A\u6587\u4EF6\u7531\u7CFB\u7EDF\u7EF4\u62A4\u3002Frontmatter \u662F\u7CFB\u7EDF\u673A\u5668\u8BFB\u5199\u7684\u7ED3\u6784\u5316\u6E90\uFF1B\u4E0B\u9762\u7684\u201C\u5DF2\u786E\u8BA4\u753B\u50CF\u201D\u662F\u7ED9\u4EBA\u770B\u7684\u540C\u6B65\u955C\u50CF\u3002\u6CA1\u6709\u51FA\u73B0\u7684\u5B57\u6BB5\uFF0C\u8868\u793A\u5F53\u524D\u8FD8\u6CA1\u6709\u786E\u8BA4\uFF0C\u4E0D\u4EE3\u8868\u7A7A\u503C\u7ED3\u8BBA\u3002
|
|
1355
2778
|
|
|
1356
|
-
## \
|
|
2779
|
+
## \u5DF2\u786E\u8BA4\u753B\u50CF
|
|
1357
2780
|
|
|
1358
|
-
|
|
1359
|
-
- \u65F6\u533A\uFF1A\u4F8B\u5982 Asia/Shanghai
|
|
1360
|
-
- \u98CE\u683C\u504F\u597D\uFF1A\u4F8B\u5982\u201C\u5E7D\u9ED8\u8BD9\u8C10\u7684\u5BF9\u8BDD\u98CE\u683C\uFF0C\u4F46\u662F\u4E0D\u8FC7\u5206\u201D
|
|
1361
|
-
- \u89D2\u8272\u4FE1\u606F\uFF1A\u4F8B\u5982\u5DE5\u4F5C\u804C\u8D23\u3001\u534F\u4F5C\u8FB9\u754C\u3001\u5E38\u89C1\u4EFB\u52A1
|
|
1362
|
-
- \u5176\u4ED6\u5907\u6CE8\uFF1A\u4F8B\u5982\u7981\u5FCC\u3001\u4E60\u60EF\u3001\u8F93\u51FA\u504F\u597D
|
|
2781
|
+
${confirmedProfileLines}
|
|
1363
2782
|
|
|
1364
|
-
## \u5907\u6CE8
|
|
2783
|
+
## \u8865\u5145\u5907\u6CE8
|
|
1365
2784
|
|
|
1366
2785
|
${notes}
|
|
1367
2786
|
`;
|
|
1368
2787
|
}
|
|
2788
|
+
function renderConfirmedProfileSection(profile) {
|
|
2789
|
+
const rows = [];
|
|
2790
|
+
if (profile.name) {
|
|
2791
|
+
rows.push(`- \u59D3\u540D\uFF1A${profile.name}`);
|
|
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
|
+
}
|
|
2805
|
+
if (profile.nickname) {
|
|
2806
|
+
rows.push(`- \u79F0\u547C\uFF1A${profile.nickname}`);
|
|
2807
|
+
}
|
|
2808
|
+
if (profile.preferences) {
|
|
2809
|
+
rows.push(`- \u56DE\u7B54\u504F\u597D\uFF1A${profile.preferences}`);
|
|
2810
|
+
}
|
|
2811
|
+
if (profile.personality) {
|
|
2812
|
+
rows.push(`- \u98CE\u683C\u504F\u597D\uFF1A${profile.personality}`);
|
|
2813
|
+
}
|
|
2814
|
+
if (profile.interests) {
|
|
2815
|
+
rows.push(`- \u5174\u8DA3\u7231\u597D\uFF1A${profile.interests}`);
|
|
2816
|
+
}
|
|
2817
|
+
if (profile.role) {
|
|
2818
|
+
rows.push(`- \u89D2\u8272\u8EAB\u4EFD\uFF1A${profile.role}`);
|
|
2819
|
+
}
|
|
2820
|
+
if (profile.timezone) {
|
|
2821
|
+
rows.push(`- \u65F6\u533A\uFF1A${profile.timezone}`);
|
|
2822
|
+
}
|
|
2823
|
+
return rows.length > 0 ? rows.join("\n") : "- \u6682\u65E0\u5DF2\u786E\u8BA4\u7684\u7ED3\u6784\u5316\u753B\u50CF\u5B57\u6BB5";
|
|
2824
|
+
}
|
|
2825
|
+
function computeProfilePayloadHash(patch, notes) {
|
|
2826
|
+
return hashId(JSON.stringify({
|
|
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,
|
|
2832
|
+
nickname: patch.nickname ?? null,
|
|
2833
|
+
timezone: patch.timezone ?? null,
|
|
2834
|
+
preferences: patch.preferences ?? null,
|
|
2835
|
+
personality: patch.personality ?? null,
|
|
2836
|
+
interests: patch.interests ?? null,
|
|
2837
|
+
role: patch.role ?? null,
|
|
2838
|
+
visibility: patch.visibility ?? "private",
|
|
2839
|
+
notes: sanitizeProfileNotes(notes) ?? null
|
|
2840
|
+
}));
|
|
2841
|
+
}
|
|
1369
2842
|
function parseProfileMarkdown(markdown) {
|
|
1370
2843
|
const lines = markdown.split(/\r?\n/);
|
|
1371
2844
|
const patch = {};
|
|
1372
2845
|
let notes = null;
|
|
2846
|
+
let updatedAt = null;
|
|
2847
|
+
let source = null;
|
|
2848
|
+
let syncHash = null;
|
|
1373
2849
|
let index = 0;
|
|
1374
2850
|
if (lines[index] === "---") {
|
|
1375
2851
|
index += 1;
|
|
@@ -1379,7 +2855,15 @@ function parseProfileMarkdown(markdown) {
|
|
|
1379
2855
|
if (separatorIndex > 0) {
|
|
1380
2856
|
const key = line.slice(0, separatorIndex).trim();
|
|
1381
2857
|
const value = line.slice(separatorIndex + 1).trim();
|
|
1382
|
-
|
|
2858
|
+
if (key === "updatedAt") {
|
|
2859
|
+
updatedAt = value === "null" ? null : value;
|
|
2860
|
+
} else if (key === "source") {
|
|
2861
|
+
source = value === "null" ? null : value;
|
|
2862
|
+
} else if (key === "syncHash") {
|
|
2863
|
+
syncHash = value === "null" ? null : value;
|
|
2864
|
+
} else {
|
|
2865
|
+
applyFrontmatterField(patch, key, value);
|
|
2866
|
+
}
|
|
1383
2867
|
}
|
|
1384
2868
|
index += 1;
|
|
1385
2869
|
}
|
|
@@ -1388,19 +2872,30 @@ function parseProfileMarkdown(markdown) {
|
|
|
1388
2872
|
}
|
|
1389
2873
|
}
|
|
1390
2874
|
const body = lines.slice(index).join("\n");
|
|
1391
|
-
const notesMatch = body.match(/##\s
|
|
2875
|
+
const notesMatch = body.match(/##\s*(?:补充备注|备注)\s*\n([\s\S]*)$/);
|
|
1392
2876
|
if (notesMatch?.[1]) {
|
|
1393
2877
|
notes = sanitizeProfileNotes(notesMatch[1]);
|
|
1394
2878
|
}
|
|
1395
2879
|
return {
|
|
1396
2880
|
profilePatch: patch,
|
|
1397
|
-
notes
|
|
2881
|
+
notes,
|
|
2882
|
+
updatedAt,
|
|
2883
|
+
source,
|
|
2884
|
+
syncHash
|
|
1398
2885
|
};
|
|
1399
2886
|
}
|
|
1400
2887
|
function applyFrontmatterField(patch, key, value) {
|
|
1401
2888
|
const normalized = value === "null" ? null : value;
|
|
1402
2889
|
if (key === "name") {
|
|
1403
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;
|
|
1404
2899
|
} else if (key === "nickname") {
|
|
1405
2900
|
patch.nickname = normalized;
|
|
1406
2901
|
} else if (key === "timezone") {
|
|
@@ -1409,16 +2904,14 @@ function applyFrontmatterField(patch, key, value) {
|
|
|
1409
2904
|
patch.preferences = normalized;
|
|
1410
2905
|
} else if (key === "personality") {
|
|
1411
2906
|
patch.personality = normalized;
|
|
2907
|
+
} else if (key === "interests") {
|
|
2908
|
+
patch.interests = normalized;
|
|
1412
2909
|
} else if (key === "role") {
|
|
1413
2910
|
patch.role = normalized;
|
|
1414
2911
|
} else if (key === "visibility") {
|
|
1415
2912
|
patch.visibility = normalized === "shared" ? "shared" : "private";
|
|
1416
2913
|
}
|
|
1417
2914
|
}
|
|
1418
|
-
function hasProfileDifference(current, patch) {
|
|
1419
|
-
const entries = Object.entries(patch);
|
|
1420
|
-
return entries.some(([key, value]) => value != null && current[key] !== value);
|
|
1421
|
-
}
|
|
1422
2915
|
function renderYamlList(rows) {
|
|
1423
2916
|
if (rows.length === 0) {
|
|
1424
2917
|
return "[]\n";
|
|
@@ -1463,6 +2956,386 @@ function asTextResult(value) {
|
|
|
1463
2956
|
function asNullableString(value) {
|
|
1464
2957
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1465
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
|
+
}
|
|
3010
|
+
async function inferSemanticProfileExtraction(messageText, currentProfile) {
|
|
3011
|
+
const model = readProfileExtractorModelFromOpenClawConfig();
|
|
3012
|
+
if (!model) {
|
|
3013
|
+
return null;
|
|
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
|
+
});
|
|
3022
|
+
const response = await fetch(`${model.baseUrl}/chat/completions`, {
|
|
3023
|
+
method: "POST",
|
|
3024
|
+
signal: AbortSignal.timeout(SEMANTIC_PROFILE_CAPTURE_TIMEOUT_MS),
|
|
3025
|
+
headers: {
|
|
3026
|
+
authorization: `Bearer ${model.apiKey}`,
|
|
3027
|
+
"content-type": "application/json; charset=utf-8"
|
|
3028
|
+
},
|
|
3029
|
+
body: JSON.stringify({
|
|
3030
|
+
model: model.modelId,
|
|
3031
|
+
temperature: 0,
|
|
3032
|
+
max_tokens: 400,
|
|
3033
|
+
response_format: { type: "json_object" },
|
|
3034
|
+
messages: [
|
|
3035
|
+
{
|
|
3036
|
+
role: "system",
|
|
3037
|
+
content: [
|
|
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.",
|
|
3039
|
+
"Return JSON only.",
|
|
3040
|
+
"Only capture durable, reusable identity facts, preferences, and personal profile details that should affect future conversations.",
|
|
3041
|
+
"Ignore transient task requirements, one-off requests, or speculative guesses.",
|
|
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.",
|
|
3044
|
+
"For each field, also decide the update operation: replace, append, or 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"?}}'
|
|
3055
|
+
].join("\n")
|
|
3056
|
+
},
|
|
3057
|
+
{
|
|
3058
|
+
role: "user",
|
|
3059
|
+
content: JSON.stringify({
|
|
3060
|
+
current_profile: {
|
|
3061
|
+
name: currentProfile.name,
|
|
3062
|
+
gender: currentProfile.gender,
|
|
3063
|
+
birthDate: currentProfile.birthDate,
|
|
3064
|
+
birthYear: currentProfile.birthYear,
|
|
3065
|
+
age: currentProfile.age,
|
|
3066
|
+
nickname: currentProfile.nickname,
|
|
3067
|
+
preferences: currentProfile.preferences,
|
|
3068
|
+
personality: currentProfile.personality,
|
|
3069
|
+
interests: currentProfile.interests,
|
|
3070
|
+
role: currentProfile.role,
|
|
3071
|
+
timezone: currentProfile.timezone,
|
|
3072
|
+
notes: currentProfile.notes
|
|
3073
|
+
},
|
|
3074
|
+
latest_user_message: messageText
|
|
3075
|
+
})
|
|
3076
|
+
}
|
|
3077
|
+
]
|
|
3078
|
+
})
|
|
3079
|
+
});
|
|
3080
|
+
const payload = await response.json();
|
|
3081
|
+
if (!response.ok) {
|
|
3082
|
+
throw new Error(`semantic profile extractor request failed: ${JSON.stringify(payload)}`);
|
|
3083
|
+
}
|
|
3084
|
+
const content = extractOpenAiMessageContent(payload);
|
|
3085
|
+
const parsed = parseSemanticExtractionResult(content);
|
|
3086
|
+
if (!parsed) {
|
|
3087
|
+
throw new Error(`semantic profile extractor returned unreadable content: ${content}`);
|
|
3088
|
+
}
|
|
3089
|
+
return parsed;
|
|
3090
|
+
}
|
|
3091
|
+
function extractOpenAiMessageContent(payload) {
|
|
3092
|
+
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
3093
|
+
const first = choices[0];
|
|
3094
|
+
if (!first || typeof first !== "object") {
|
|
3095
|
+
return "";
|
|
3096
|
+
}
|
|
3097
|
+
const message = first.message;
|
|
3098
|
+
if (!message || typeof message !== "object") {
|
|
3099
|
+
return "";
|
|
3100
|
+
}
|
|
3101
|
+
const content = message.content;
|
|
3102
|
+
if (typeof content === "string") {
|
|
3103
|
+
return content.trim();
|
|
3104
|
+
}
|
|
3105
|
+
if (!Array.isArray(content)) {
|
|
3106
|
+
return "";
|
|
3107
|
+
}
|
|
3108
|
+
return content.map((item) => {
|
|
3109
|
+
if (!item || typeof item !== "object") {
|
|
3110
|
+
return "";
|
|
3111
|
+
}
|
|
3112
|
+
const record = item;
|
|
3113
|
+
return asNullableString(record.text) ?? "";
|
|
3114
|
+
}).filter(Boolean).join("\n").trim();
|
|
3115
|
+
}
|
|
3116
|
+
function parseSemanticExtractionResult(content) {
|
|
3117
|
+
const jsonText = extractJsonObject(content);
|
|
3118
|
+
if (!jsonText) {
|
|
3119
|
+
return null;
|
|
3120
|
+
}
|
|
3121
|
+
try {
|
|
3122
|
+
const parsed = JSON.parse(jsonText);
|
|
3123
|
+
const patchInput = parsed.patch && typeof parsed.patch === "object" ? parsed.patch : {};
|
|
3124
|
+
const operationsInput = parsed.operations && typeof parsed.operations === "object" ? parsed.operations : {};
|
|
3125
|
+
return {
|
|
3126
|
+
shouldUpdate: parsed.should_update === true || parsed.shouldUpdate === true,
|
|
3127
|
+
confidence: Number(parsed.confidence ?? 0),
|
|
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),
|
|
3134
|
+
nickname: asNullableString(patchInput.nickname),
|
|
3135
|
+
preferences: asNullableString(patchInput.preferences),
|
|
3136
|
+
personality: asNullableString(patchInput.personality),
|
|
3137
|
+
interests: asNullableString(patchInput.interests),
|
|
3138
|
+
role: asNullableString(patchInput.role),
|
|
3139
|
+
timezone: asNullableString(patchInput.timezone),
|
|
3140
|
+
notes: asNullableString(patchInput.notes)
|
|
3141
|
+
},
|
|
3142
|
+
operations: parseProfilePatchOperations(operationsInput)
|
|
3143
|
+
};
|
|
3144
|
+
} catch {
|
|
3145
|
+
return null;
|
|
3146
|
+
}
|
|
3147
|
+
}
|
|
3148
|
+
function parseProfilePatchOperations(input) {
|
|
3149
|
+
const operations = {};
|
|
3150
|
+
for (const field of ["name", "gender", "birthDate", "birthYear", "age", "nickname", "preferences", "personality", "interests", "role", "timezone", "notes"]) {
|
|
3151
|
+
const raw = asNullableString(input[field]);
|
|
3152
|
+
if (raw === "replace" || raw === "append" || raw === "remove") {
|
|
3153
|
+
operations[field] = raw;
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
return Object.keys(operations).length > 0 ? operations : void 0;
|
|
3157
|
+
}
|
|
3158
|
+
function extractJsonObject(content) {
|
|
3159
|
+
const trimmed = content.trim();
|
|
3160
|
+
if (!trimmed) {
|
|
3161
|
+
return null;
|
|
3162
|
+
}
|
|
3163
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
3164
|
+
if (fenced?.[1]) {
|
|
3165
|
+
return fenced[1].trim();
|
|
3166
|
+
}
|
|
3167
|
+
const start = trimmed.indexOf("{");
|
|
3168
|
+
const end = trimmed.lastIndexOf("}");
|
|
3169
|
+
if (start >= 0 && end > start) {
|
|
3170
|
+
return trimmed.slice(start, end + 1);
|
|
3171
|
+
}
|
|
3172
|
+
return null;
|
|
3173
|
+
}
|
|
3174
|
+
function cleanupSemanticProfilePatch(patch, currentProfile, operations) {
|
|
3175
|
+
const next = {};
|
|
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);
|
|
3182
|
+
const nickname = asNullableString(patch.nickname);
|
|
3183
|
+
const preferences = asNullableString(patch.preferences);
|
|
3184
|
+
const personality = asNullableString(patch.personality);
|
|
3185
|
+
const interests = asNullableString(patch.interests);
|
|
3186
|
+
const role = asNullableString(patch.role);
|
|
3187
|
+
const timezone = asNullableString(patch.timezone);
|
|
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
|
+
}
|
|
3219
|
+
if (nickname && nickname !== currentProfile.nickname) {
|
|
3220
|
+
next.nickname = nickname;
|
|
3221
|
+
if (operations?.nickname) {
|
|
3222
|
+
nextOperations.nickname = operations.nickname;
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
if (preferences && preferences !== currentProfile.preferences) {
|
|
3226
|
+
next.preferences = preferences;
|
|
3227
|
+
if (operations?.preferences) {
|
|
3228
|
+
nextOperations.preferences = operations.preferences;
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
if (personality && personality !== currentProfile.personality) {
|
|
3232
|
+
next.personality = personality;
|
|
3233
|
+
if (operations?.personality) {
|
|
3234
|
+
nextOperations.personality = operations.personality;
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
if (interests && interests !== currentProfile.interests) {
|
|
3238
|
+
next.interests = interests;
|
|
3239
|
+
if (operations?.interests) {
|
|
3240
|
+
nextOperations.interests = operations.interests;
|
|
3241
|
+
}
|
|
3242
|
+
}
|
|
3243
|
+
if (role && role !== currentProfile.role) {
|
|
3244
|
+
next.role = role;
|
|
3245
|
+
if (operations?.role) {
|
|
3246
|
+
nextOperations.role = operations.role;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
if (timezone && timezone !== currentProfile.timezone) {
|
|
3250
|
+
next.timezone = timezone;
|
|
3251
|
+
if (operations?.timezone) {
|
|
3252
|
+
nextOperations.timezone = operations.timezone;
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
if (notes) {
|
|
3256
|
+
next.notes = notes;
|
|
3257
|
+
if (operations?.notes) {
|
|
3258
|
+
nextOperations.notes = operations.notes;
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
return {
|
|
3262
|
+
patch: next,
|
|
3263
|
+
operations: Object.keys(nextOperations).length > 0 ? nextOperations : void 0
|
|
3264
|
+
};
|
|
3265
|
+
}
|
|
3266
|
+
function applyProfilePatchOperations(currentProfile, patch, operations) {
|
|
3267
|
+
const next = {};
|
|
3268
|
+
const fields = ["name", "gender", "birthDate", "birthYear", "age", "nickname", "preferences", "personality", "interests", "role", "timezone", "notes"];
|
|
3269
|
+
for (const field of fields) {
|
|
3270
|
+
if (patch[field] === void 0) {
|
|
3271
|
+
continue;
|
|
3272
|
+
}
|
|
3273
|
+
const currentValue = currentProfile[field];
|
|
3274
|
+
const incomingValue = asNullableString(patch[field]);
|
|
3275
|
+
const operation = operations?.[field] ?? defaultProfileFieldOperation(field);
|
|
3276
|
+
const resolved = resolveProfileFieldUpdate(field, currentValue, incomingValue, operation);
|
|
3277
|
+
if (resolved !== void 0) {
|
|
3278
|
+
next[field] = resolved;
|
|
3279
|
+
}
|
|
3280
|
+
}
|
|
3281
|
+
return next;
|
|
3282
|
+
}
|
|
3283
|
+
function defaultProfileFieldOperation(field) {
|
|
3284
|
+
if (field === "notes" || field === "interests") {
|
|
3285
|
+
return "append";
|
|
3286
|
+
}
|
|
3287
|
+
return "replace";
|
|
3288
|
+
}
|
|
3289
|
+
function resolveProfileFieldUpdate(field, currentValue, incomingValue, operation) {
|
|
3290
|
+
if (operation === "remove") {
|
|
3291
|
+
if (!currentValue) {
|
|
3292
|
+
return void 0;
|
|
3293
|
+
}
|
|
3294
|
+
if (!incomingValue) {
|
|
3295
|
+
return null;
|
|
3296
|
+
}
|
|
3297
|
+
const removed = removeProfileFeatureValue(currentValue, incomingValue);
|
|
3298
|
+
return removed === currentValue ? void 0 : removed;
|
|
3299
|
+
}
|
|
3300
|
+
if (!incomingValue) {
|
|
3301
|
+
return void 0;
|
|
3302
|
+
}
|
|
3303
|
+
if (operation === "append") {
|
|
3304
|
+
if (field === "notes") {
|
|
3305
|
+
const mergedNotes = joinNotes(currentValue, incomingValue);
|
|
3306
|
+
return mergedNotes === currentValue ? void 0 : mergedNotes;
|
|
3307
|
+
}
|
|
3308
|
+
const appended = appendProfileFeatureValue(currentValue, incomingValue);
|
|
3309
|
+
return appended === currentValue ? void 0 : appended;
|
|
3310
|
+
}
|
|
3311
|
+
return incomingValue === currentValue ? void 0 : incomingValue;
|
|
3312
|
+
}
|
|
3313
|
+
function appendProfileFeatureValue(currentValue, incomingValue) {
|
|
3314
|
+
if (!currentValue) {
|
|
3315
|
+
return incomingValue;
|
|
3316
|
+
}
|
|
3317
|
+
const currentItems = splitProfileFeatureValues(currentValue);
|
|
3318
|
+
const incomingItems = splitProfileFeatureValues(incomingValue);
|
|
3319
|
+
const merged = [...currentItems];
|
|
3320
|
+
for (const item of incomingItems) {
|
|
3321
|
+
if (!merged.includes(item)) {
|
|
3322
|
+
merged.push(item);
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
return merged.join("\uFF1B");
|
|
3326
|
+
}
|
|
3327
|
+
function removeProfileFeatureValue(currentValue, incomingValue) {
|
|
3328
|
+
const currentItems = splitProfileFeatureValues(currentValue);
|
|
3329
|
+
const removals = new Set(splitProfileFeatureValues(incomingValue));
|
|
3330
|
+
const nextItems = currentItems.filter((item) => !removals.has(item));
|
|
3331
|
+
if (nextItems.length === currentItems.length) {
|
|
3332
|
+
return currentValue;
|
|
3333
|
+
}
|
|
3334
|
+
return nextItems.length > 0 ? nextItems.join("\uFF1B") : null;
|
|
3335
|
+
}
|
|
3336
|
+
function splitProfileFeatureValues(value) {
|
|
3337
|
+
return value.split(/[\n;;,,、]+/u).map((item) => item.trim()).filter(Boolean);
|
|
3338
|
+
}
|
|
1466
3339
|
function ensureObject(parent, key) {
|
|
1467
3340
|
const current = parent[key];
|
|
1468
3341
|
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
@@ -1484,60 +3357,6 @@ function ensureArrayIncludes(parent, key, value) {
|
|
|
1484
3357
|
parent[key] = current;
|
|
1485
3358
|
return true;
|
|
1486
3359
|
}
|
|
1487
|
-
function extractScopes(result) {
|
|
1488
|
-
const candidates = [
|
|
1489
|
-
findNestedValue(result, ["data", "scopes"]),
|
|
1490
|
-
findNestedValue(result, ["scopes"]),
|
|
1491
|
-
findNestedValue(result, ["data", "items"])
|
|
1492
|
-
];
|
|
1493
|
-
for (const candidate of candidates) {
|
|
1494
|
-
if (!Array.isArray(candidate)) {
|
|
1495
|
-
continue;
|
|
1496
|
-
}
|
|
1497
|
-
const scopes = candidate.map((item) => {
|
|
1498
|
-
if (typeof item === "string") {
|
|
1499
|
-
return item;
|
|
1500
|
-
}
|
|
1501
|
-
if (item && typeof item === "object") {
|
|
1502
|
-
const record = item;
|
|
1503
|
-
const scope = record.scope ?? record.name;
|
|
1504
|
-
return typeof scope === "string" ? scope : "";
|
|
1505
|
-
}
|
|
1506
|
-
return "";
|
|
1507
|
-
}).filter(Boolean);
|
|
1508
|
-
if (scopes.length > 0) {
|
|
1509
|
-
return scopes;
|
|
1510
|
-
}
|
|
1511
|
-
}
|
|
1512
|
-
return [];
|
|
1513
|
-
}
|
|
1514
|
-
function findBitableRecordId(result, userId) {
|
|
1515
|
-
const candidates = [
|
|
1516
|
-
findNestedValue(result, ["data", "items"]),
|
|
1517
|
-
findNestedValue(result, ["items"]),
|
|
1518
|
-
findNestedValue(result, ["data", "records"])
|
|
1519
|
-
];
|
|
1520
|
-
for (const candidate of candidates) {
|
|
1521
|
-
if (!Array.isArray(candidate)) {
|
|
1522
|
-
continue;
|
|
1523
|
-
}
|
|
1524
|
-
for (const item of candidate) {
|
|
1525
|
-
if (!item || typeof item !== "object") {
|
|
1526
|
-
continue;
|
|
1527
|
-
}
|
|
1528
|
-
const record = item;
|
|
1529
|
-
const fields = record.fields && typeof record.fields === "object" ? record.fields : {};
|
|
1530
|
-
if (String(fields.user_id ?? "") !== userId) {
|
|
1531
|
-
continue;
|
|
1532
|
-
}
|
|
1533
|
-
const recordId = record.record_id ?? record.recordId ?? record.id;
|
|
1534
|
-
if (typeof recordId === "string" && recordId.trim()) {
|
|
1535
|
-
return recordId;
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
}
|
|
1539
|
-
return null;
|
|
1540
|
-
}
|
|
1541
3360
|
function readFeishuAccountsFromOpenClawConfig() {
|
|
1542
3361
|
const openclawConfigPath = (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "openclaw.json");
|
|
1543
3362
|
if (!(0, import_node_fs.existsSync)(openclawConfigPath)) {
|
|
@@ -1561,6 +3380,89 @@ function readFeishuAccountsFromOpenClawConfig() {
|
|
|
1561
3380
|
return [];
|
|
1562
3381
|
}
|
|
1563
3382
|
}
|
|
3383
|
+
function readProfileExtractorModelFromOpenClawConfig() {
|
|
3384
|
+
const openclawConfigPath = (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "openclaw.json");
|
|
3385
|
+
if (!(0, import_node_fs.existsSync)(openclawConfigPath)) {
|
|
3386
|
+
return null;
|
|
3387
|
+
}
|
|
3388
|
+
try {
|
|
3389
|
+
const parsed = JSON.parse((0, import_node_fs.readFileSync)(openclawConfigPath, "utf8"));
|
|
3390
|
+
const models = parsed.models && typeof parsed.models === "object" ? parsed.models : {};
|
|
3391
|
+
const providers = models.providers && typeof models.providers === "object" ? models.providers : {};
|
|
3392
|
+
const agents = parsed.agents && typeof parsed.agents === "object" ? parsed.agents : {};
|
|
3393
|
+
const defaults = agents.defaults && typeof agents.defaults === "object" ? agents.defaults : {};
|
|
3394
|
+
const defaultModel = defaults.model && typeof defaults.model === "object" ? defaults.model : {};
|
|
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
|
+
};
|
|
3424
|
+
}
|
|
3425
|
+
return null;
|
|
3426
|
+
} catch (error) {
|
|
3427
|
+
logUserBindEvent("profile-extractor-config-read-failed", {
|
|
3428
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3429
|
+
});
|
|
3430
|
+
return null;
|
|
3431
|
+
}
|
|
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
|
+
}
|
|
1564
3466
|
function normalizeFeishuAccount(accountId, input, fallback) {
|
|
1565
3467
|
const record = input && typeof input === "object" ? input : {};
|
|
1566
3468
|
const enabled = record.enabled !== false && fallback.enabled !== false;
|
|
@@ -1602,12 +3504,107 @@ function getConfiguredAgentId(agent) {
|
|
|
1602
3504
|
}
|
|
1603
3505
|
function defaultProfileNotes() {
|
|
1604
3506
|
return [
|
|
1605
|
-
"- \
|
|
1606
|
-
|
|
1607
|
-
"- \
|
|
1608
|
-
"- \u4F60\u53EF\u4EE5\u5728\u8FD9\u91CC\u7EE7\u7EED\u8865\u5145\u5DE5\u4F5C\u80CC\u666F\u3001\u8868\u8FBE\u4E60\u60EF\u3001\u7981\u5FCC\u548C\u957F\u671F\u504F\u597D\u3002"
|
|
3507
|
+
"- \u8FD9\u91CC\u53EA\u8BB0\u5F55\u5DF2\u7ECF\u786E\u8BA4\u7684\u957F\u671F\u753B\u50CF\u4FE1\u606F\uFF0C\u4E0D\u8BB0\u5F55\u4E34\u65F6\u4EFB\u52A1\u8981\u6C42\u3002",
|
|
3508
|
+
`- \u82E5\u7528\u6237\u5C1A\u672A\u660E\u786E\u63D0\u4F9B\u65F6\u533A\uFF0C\u9ED8\u8BA4\u8DDF\u968F\u5F53\u524D\u670D\u52A1\u5668\u65F6\u533A\uFF08\u5F53\u524D\u68C0\u6D4B\u503C\uFF1A${getServerTimezone()}\uFF09\u3002`,
|
|
3509
|
+
"- \u5907\u6CE8\u91CC\u9002\u5408\u5199\u6682\u65F6\u8FD8\u4E0D\u4FBF\u7ED3\u6784\u5316\u3001\u4F46\u5DF2\u7ECF\u786E\u8BA4\u7684\u957F\u671F\u4E60\u60EF\u3001\u8FB9\u754C\u548C\u534F\u4F5C\u504F\u597D\u3002"
|
|
1609
3510
|
].join("\n");
|
|
1610
3511
|
}
|
|
3512
|
+
function createEphemeralUserId(channelType, openId, sessionId) {
|
|
3513
|
+
return `temp_${hashId(`${channelType}:${openId ?? sessionId}`)}`;
|
|
3514
|
+
}
|
|
3515
|
+
function createProvisionalUserId(channelType, openId) {
|
|
3516
|
+
return `${channelType}:oid_${hashId(`${channelType}:${openId}`)}`;
|
|
3517
|
+
}
|
|
3518
|
+
function createStableLocalUserId(channelType, openId, sessionId) {
|
|
3519
|
+
return `${channelType}:ub_${hashId(`${channelType}:${openId ?? sessionId}`)}`;
|
|
3520
|
+
}
|
|
3521
|
+
function isProvisionalScopedUserId(userId) {
|
|
3522
|
+
return /:oid_[a-f0-9]+$/i.test(userId);
|
|
3523
|
+
}
|
|
3524
|
+
function getPendingBindingRetryDelayMs(reason, attempts) {
|
|
3525
|
+
if (reason === "feishu-contact-scope-missing") {
|
|
3526
|
+
return Math.min(30 * 60 * 1e3, Math.max(6e4, attempts * 5 * 6e4));
|
|
3527
|
+
}
|
|
3528
|
+
return Math.min(10 * 60 * 1e3, Math.max(15e3, attempts * 3e4));
|
|
3529
|
+
}
|
|
3530
|
+
function scopeUserId(channelType, userId) {
|
|
3531
|
+
if (!userId) {
|
|
3532
|
+
return userId;
|
|
3533
|
+
}
|
|
3534
|
+
if (channelType === "manual" || channelType === "local") {
|
|
3535
|
+
return userId;
|
|
3536
|
+
}
|
|
3537
|
+
if (userId.startsWith("temp_") || userId.startsWith("ub_")) {
|
|
3538
|
+
return userId;
|
|
3539
|
+
}
|
|
3540
|
+
if (extractChannelFromScopedUserId(userId)) {
|
|
3541
|
+
return userId;
|
|
3542
|
+
}
|
|
3543
|
+
return `${channelType}:${userId}`;
|
|
3544
|
+
}
|
|
3545
|
+
function getExternalUserId(channelType, userId) {
|
|
3546
|
+
if (!userId) {
|
|
3547
|
+
return userId;
|
|
3548
|
+
}
|
|
3549
|
+
if (channelType === "manual" || channelType === "local") {
|
|
3550
|
+
return userId;
|
|
3551
|
+
}
|
|
3552
|
+
if (userId.startsWith("temp_") || userId.startsWith("ub_")) {
|
|
3553
|
+
return userId;
|
|
3554
|
+
}
|
|
3555
|
+
const currentChannel = extractChannelFromScopedUserId(userId);
|
|
3556
|
+
if (currentChannel && userId.startsWith(`${currentChannel}:`)) {
|
|
3557
|
+
return userId.slice(currentChannel.length + 1);
|
|
3558
|
+
}
|
|
3559
|
+
return userId;
|
|
3560
|
+
}
|
|
3561
|
+
function extractChannelFromScopedUserId(userId) {
|
|
3562
|
+
const match = userId.match(/^([a-z][a-z0-9_-]*):(.+)$/i);
|
|
3563
|
+
if (!match) {
|
|
3564
|
+
return null;
|
|
3565
|
+
}
|
|
3566
|
+
return match[1].toLowerCase();
|
|
3567
|
+
}
|
|
3568
|
+
function buildLightweightProfile(args) {
|
|
3569
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
3570
|
+
return {
|
|
3571
|
+
userId: args.userId,
|
|
3572
|
+
name: args.profilePatch.name ?? args.current?.name ?? null,
|
|
3573
|
+
gender: args.profilePatch.gender ?? args.current?.gender ?? null,
|
|
3574
|
+
email: args.profilePatch.email ?? args.current?.email ?? null,
|
|
3575
|
+
avatar: args.profilePatch.avatar ?? args.current?.avatar ?? null,
|
|
3576
|
+
nickname: args.profilePatch.nickname ?? args.current?.nickname ?? null,
|
|
3577
|
+
preferences: args.profilePatch.preferences ?? args.current?.preferences ?? null,
|
|
3578
|
+
personality: args.profilePatch.personality ?? args.current?.personality ?? null,
|
|
3579
|
+
role: args.profilePatch.role ?? args.current?.role ?? null,
|
|
3580
|
+
timezone: args.profilePatch.timezone ?? args.current?.timezone ?? getServerTimezone(),
|
|
3581
|
+
notes: args.profilePatch.notes ?? args.current?.notes ?? defaultProfileNotes(),
|
|
3582
|
+
visibility: args.profilePatch.visibility ?? args.current?.visibility ?? "private",
|
|
3583
|
+
source: args.source,
|
|
3584
|
+
updatedAt: now
|
|
3585
|
+
};
|
|
3586
|
+
}
|
|
3587
|
+
function looksLikeFeishuContactScopeError(message) {
|
|
3588
|
+
return message.includes("99991672") || message.includes("permission_violations") || message.includes("contact:contact.base:readonly") || message.includes("contact:contact:access_as_app") || message.includes("contact:contact:readonly");
|
|
3589
|
+
}
|
|
3590
|
+
function renderFeishuContactScopeGuidance(accountIds) {
|
|
3591
|
+
const scopeText = accountIds.length > 0 ? `\u6D89\u53CA\u8D26\u53F7\uFF1A${accountIds.join("\u3001")}\u3002` : "";
|
|
3592
|
+
return [
|
|
3593
|
+
"\u5F53\u524D\u65E0\u6CD5\u5B8C\u6574\u542F\u7528\u7528\u6237\u753B\u50CF\u3002",
|
|
3594
|
+
"\u539F\u56E0\uFF1A\u5BF9\u5E94\u7684 Feishu App \u7F3A\u5C11\u8054\u7CFB\u4EBA\u6743\u9650\uFF0C\u6682\u65F6\u65E0\u6CD5\u7A33\u5B9A\u89E3\u6790\u771F\u5B9E\u7528\u6237\u8EAB\u4EFD\u3002",
|
|
3595
|
+
`${scopeText}\u8BF7\u8054\u7CFB\u7BA1\u7406\u5458\u5F00\u901A\u8054\u7CFB\u4EBA\u76F8\u5173\u6743\u9650\uFF0C\u4EE5\u4FDD\u8BC1\u7528\u6237\u753B\u50CF\u53EF\u7528\u3002`
|
|
3596
|
+
].join(" ");
|
|
3597
|
+
}
|
|
3598
|
+
function getServerTimezone() {
|
|
3599
|
+
try {
|
|
3600
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
3601
|
+
if (typeof timezone === "string" && timezone.trim()) {
|
|
3602
|
+
return timezone.trim();
|
|
3603
|
+
}
|
|
3604
|
+
} catch {
|
|
3605
|
+
}
|
|
3606
|
+
return "UTC";
|
|
3607
|
+
}
|
|
1611
3608
|
function sanitizeProfileNotes(notes) {
|
|
1612
3609
|
const value = typeof notes === "string" ? notes.trim() : "";
|
|
1613
3610
|
if (!value) {
|