@bamdra/bamdra-user-bind 0.1.12 → 0.1.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -0
- package/README.zh-CN.md +31 -0
- package/dist/index.js +1482 -102
- 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 +3 -1
- package/skills/bamdra-user-bind-profile/SKILL.md +97 -1
package/dist/index.js
CHANGED
|
@@ -46,6 +46,11 @@ var SELF_TOOL_NAMES = [
|
|
|
46
46
|
"bamdra_user_bind_update_my_profile",
|
|
47
47
|
"bamdra_user_bind_refresh_my_binding"
|
|
48
48
|
];
|
|
49
|
+
var SELF_TOOL_ALIASES = [
|
|
50
|
+
"user_bind_get_my_profile",
|
|
51
|
+
"user_bind_update_my_profile",
|
|
52
|
+
"user_bind_refresh_my_binding"
|
|
53
|
+
];
|
|
49
54
|
var ADMIN_TOOL_NAMES = [
|
|
50
55
|
"bamdra_user_bind_admin_query",
|
|
51
56
|
"bamdra_user_bind_admin_edit",
|
|
@@ -63,6 +68,17 @@ var REQUIRED_FEISHU_IDENTITY_SCOPES = [
|
|
|
63
68
|
"contact:user.employee_id:readonly",
|
|
64
69
|
"contact:user.base:readonly"
|
|
65
70
|
];
|
|
71
|
+
var CHANNELS_WITH_NATIVE_STABLE_IDS = /* @__PURE__ */ new Set([
|
|
72
|
+
"telegram",
|
|
73
|
+
"whatsapp",
|
|
74
|
+
"discord",
|
|
75
|
+
"googlechat",
|
|
76
|
+
"slack",
|
|
77
|
+
"mattermost",
|
|
78
|
+
"signal",
|
|
79
|
+
"imessage",
|
|
80
|
+
"msteams"
|
|
81
|
+
]);
|
|
66
82
|
function logUserBindEvent(event, details = {}) {
|
|
67
83
|
try {
|
|
68
84
|
console.info("[bamdra-user-bind]", event, JSON.stringify(details));
|
|
@@ -130,6 +146,7 @@ var UserBindStore = class {
|
|
|
130
146
|
`);
|
|
131
147
|
this.ensureColumn(TABLES.profiles, "timezone", "TEXT");
|
|
132
148
|
this.ensureColumn(TABLES.profiles, "notes", "TEXT");
|
|
149
|
+
this.migrateChannelScopedUserIds();
|
|
133
150
|
}
|
|
134
151
|
db;
|
|
135
152
|
markdownSyncing = /* @__PURE__ */ new Set();
|
|
@@ -156,6 +173,22 @@ var UserBindStore = class {
|
|
|
156
173
|
source: row.source
|
|
157
174
|
};
|
|
158
175
|
}
|
|
176
|
+
reconcileProvisionalIdentity(channelType, openId, stableUserId) {
|
|
177
|
+
if (!openId) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
const provisionalUserId = createProvisionalUserId(channelType, openId);
|
|
181
|
+
const scopedStableUserId = scopeUserId(channelType, stableUserId);
|
|
182
|
+
if (provisionalUserId === scopedStableUserId) {
|
|
183
|
+
return this.getProfile(scopedStableUserId);
|
|
184
|
+
}
|
|
185
|
+
const provisional = this.getProfile(provisionalUserId);
|
|
186
|
+
const stable = this.getProfile(scopedStableUserId);
|
|
187
|
+
if (!provisional || !stable) {
|
|
188
|
+
return stable;
|
|
189
|
+
}
|
|
190
|
+
return this.mergeUsers(provisionalUserId, scopedStableUserId);
|
|
191
|
+
}
|
|
159
192
|
listIssues() {
|
|
160
193
|
return this.db.prepare(`
|
|
161
194
|
SELECT id, kind, user_id, details, status, updated_at
|
|
@@ -190,22 +223,21 @@ var UserBindStore = class {
|
|
|
190
223
|
);
|
|
191
224
|
}
|
|
192
225
|
upsertIdentity(args) {
|
|
193
|
-
|
|
194
|
-
this.syncMarkdownToStore(args.userId);
|
|
195
|
-
}
|
|
226
|
+
const scopedUserId = scopeUserId(args.channelType, args.userId);
|
|
196
227
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
197
|
-
const current = this.getProfileFromDatabase(
|
|
228
|
+
const current = this.getProfileFromDatabase(scopedUserId);
|
|
229
|
+
const externalUserId = args.openId ?? getExternalUserId(args.channelType, args.userId);
|
|
198
230
|
const next = {
|
|
199
|
-
userId:
|
|
231
|
+
userId: scopedUserId,
|
|
200
232
|
name: args.profilePatch.name ?? current?.name ?? null,
|
|
201
233
|
gender: args.profilePatch.gender ?? current?.gender ?? null,
|
|
202
234
|
email: args.profilePatch.email ?? current?.email ?? null,
|
|
203
235
|
avatar: args.profilePatch.avatar ?? current?.avatar ?? null,
|
|
204
|
-
nickname: args.profilePatch.nickname ?? current?.nickname ??
|
|
205
|
-
preferences: args.profilePatch.preferences ?? current?.preferences ??
|
|
236
|
+
nickname: args.profilePatch.nickname ?? current?.nickname ?? null,
|
|
237
|
+
preferences: args.profilePatch.preferences ?? current?.preferences ?? null,
|
|
206
238
|
personality: args.profilePatch.personality ?? current?.personality ?? null,
|
|
207
239
|
role: args.profilePatch.role ?? current?.role ?? null,
|
|
208
|
-
timezone: args.profilePatch.timezone ?? current?.timezone ??
|
|
240
|
+
timezone: args.profilePatch.timezone ?? current?.timezone ?? getServerTimezone(),
|
|
209
241
|
notes: args.profilePatch.notes ?? current?.notes ?? defaultProfileNotes(),
|
|
210
242
|
visibility: args.profilePatch.visibility ?? current?.visibility ?? "private",
|
|
211
243
|
source: args.source,
|
|
@@ -257,10 +289,10 @@ var UserBindStore = class {
|
|
|
257
289
|
updated_at = excluded.updated_at
|
|
258
290
|
`).run(
|
|
259
291
|
bindingId,
|
|
260
|
-
|
|
292
|
+
scopedUserId,
|
|
261
293
|
args.channelType,
|
|
262
294
|
args.openId,
|
|
263
|
-
|
|
295
|
+
externalUserId,
|
|
264
296
|
null,
|
|
265
297
|
args.source,
|
|
266
298
|
now
|
|
@@ -346,8 +378,6 @@ var UserBindStore = class {
|
|
|
346
378
|
}
|
|
347
379
|
const current = this.getProfileFromDatabase(userId);
|
|
348
380
|
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
381
|
const patch = {
|
|
352
382
|
...parsed.profilePatch,
|
|
353
383
|
notes: parsed.notes
|
|
@@ -357,7 +387,7 @@ var UserBindStore = class {
|
|
|
357
387
|
userId,
|
|
358
388
|
channelType: "manual",
|
|
359
389
|
openId: null,
|
|
360
|
-
source: "markdown-profile",
|
|
390
|
+
source: parsed.source ?? "markdown-profile",
|
|
361
391
|
profilePatch: {
|
|
362
392
|
...patch,
|
|
363
393
|
visibility: patch.visibility ?? "private"
|
|
@@ -365,14 +395,43 @@ var UserBindStore = class {
|
|
|
365
395
|
});
|
|
366
396
|
return;
|
|
367
397
|
}
|
|
368
|
-
|
|
398
|
+
const markdownMtime = (0, import_node_fs.statSync)(markdownPath).mtime.toISOString();
|
|
399
|
+
const markdownHash = computeProfilePayloadHash({
|
|
400
|
+
name: patch.name ?? null,
|
|
401
|
+
nickname: patch.nickname ?? null,
|
|
402
|
+
timezone: patch.timezone ?? null,
|
|
403
|
+
preferences: patch.preferences ?? null,
|
|
404
|
+
personality: patch.personality ?? null,
|
|
405
|
+
role: patch.role ?? null,
|
|
406
|
+
visibility: patch.visibility ?? current.visibility
|
|
407
|
+
}, patch.notes ?? null);
|
|
408
|
+
const currentHash = computeProfilePayloadHash({
|
|
409
|
+
name: current.name,
|
|
410
|
+
nickname: current.nickname,
|
|
411
|
+
timezone: current.timezone,
|
|
412
|
+
preferences: current.preferences,
|
|
413
|
+
personality: current.personality,
|
|
414
|
+
role: current.role,
|
|
415
|
+
visibility: current.visibility
|
|
416
|
+
}, current.notes);
|
|
417
|
+
if (markdownHash === currentHash) {
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (parsed.syncHash && parsed.syncHash === markdownHash) {
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
const dbTime = current.updatedAt;
|
|
424
|
+
if (parsed.updatedAt && parsed.updatedAt <= dbTime && markdownMtime <= dbTime) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
if (!parsed.syncHash && markdownMtime <= dbTime) {
|
|
369
428
|
return;
|
|
370
429
|
}
|
|
371
430
|
this.upsertIdentity({
|
|
372
431
|
userId,
|
|
373
432
|
channelType: "manual",
|
|
374
433
|
openId: null,
|
|
375
|
-
source: "markdown-profile",
|
|
434
|
+
source: parsed.source ?? "markdown-profile",
|
|
376
435
|
profilePatch: {
|
|
377
436
|
...current,
|
|
378
437
|
...patch
|
|
@@ -408,6 +467,94 @@ var UserBindStore = class {
|
|
|
408
467
|
}
|
|
409
468
|
this.db.exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnName} ${definition}`);
|
|
410
469
|
}
|
|
470
|
+
migrateChannelScopedUserIds() {
|
|
471
|
+
const rows = this.db.prepare(`
|
|
472
|
+
SELECT DISTINCT user_id, channel_type
|
|
473
|
+
FROM ${TABLES.bindings}
|
|
474
|
+
WHERE user_id IS NOT NULL AND channel_type IS NOT NULL
|
|
475
|
+
`).all();
|
|
476
|
+
const renames = rows.map((row) => {
|
|
477
|
+
const currentUserId = String(row.user_id ?? "");
|
|
478
|
+
const channelType = String(row.channel_type ?? "");
|
|
479
|
+
const scopedUserId = scopeUserId(channelType, currentUserId);
|
|
480
|
+
if (!currentUserId || !channelType || scopedUserId === currentUserId) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
return { currentUserId, scopedUserId };
|
|
484
|
+
}).filter((item) => item != null);
|
|
485
|
+
if (renames.length === 0) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
for (const { currentUserId, scopedUserId } of renames) {
|
|
489
|
+
this.renameUserId(currentUserId, scopedUserId);
|
|
490
|
+
}
|
|
491
|
+
this.writeExports();
|
|
492
|
+
}
|
|
493
|
+
renameUserId(fromUserId, toUserId) {
|
|
494
|
+
if (fromUserId === toUserId) {
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
const existingTarget = this.getProfileFromDatabase(toUserId);
|
|
498
|
+
const sourceProfile = this.getProfileFromDatabase(fromUserId);
|
|
499
|
+
if (sourceProfile && !existingTarget) {
|
|
500
|
+
this.db.prepare(`UPDATE ${TABLES.profiles} SET user_id = ? WHERE user_id = ?`).run(toUserId, fromUserId);
|
|
501
|
+
} else if (sourceProfile && existingTarget) {
|
|
502
|
+
const merged = {
|
|
503
|
+
...sourceProfile,
|
|
504
|
+
...existingTarget,
|
|
505
|
+
userId: toUserId,
|
|
506
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
507
|
+
source: existingTarget.source || sourceProfile.source,
|
|
508
|
+
notes: joinNotes(existingTarget.notes, sourceProfile.notes)
|
|
509
|
+
};
|
|
510
|
+
this.db.prepare(`DELETE FROM ${TABLES.profiles} WHERE user_id = ?`).run(fromUserId);
|
|
511
|
+
this.db.prepare(`
|
|
512
|
+
INSERT INTO ${TABLES.profiles} (user_id, name, gender, email, avatar, nickname, preferences, personality, role, timezone, notes, visibility, source, updated_at)
|
|
513
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
514
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
515
|
+
name = excluded.name,
|
|
516
|
+
gender = excluded.gender,
|
|
517
|
+
email = excluded.email,
|
|
518
|
+
avatar = excluded.avatar,
|
|
519
|
+
nickname = excluded.nickname,
|
|
520
|
+
preferences = excluded.preferences,
|
|
521
|
+
personality = excluded.personality,
|
|
522
|
+
role = excluded.role,
|
|
523
|
+
timezone = excluded.timezone,
|
|
524
|
+
notes = excluded.notes,
|
|
525
|
+
visibility = excluded.visibility,
|
|
526
|
+
source = excluded.source,
|
|
527
|
+
updated_at = excluded.updated_at
|
|
528
|
+
`).run(
|
|
529
|
+
merged.userId,
|
|
530
|
+
merged.name,
|
|
531
|
+
merged.gender,
|
|
532
|
+
merged.email,
|
|
533
|
+
merged.avatar,
|
|
534
|
+
merged.nickname,
|
|
535
|
+
merged.preferences,
|
|
536
|
+
merged.personality,
|
|
537
|
+
merged.role,
|
|
538
|
+
merged.timezone,
|
|
539
|
+
merged.notes,
|
|
540
|
+
merged.visibility,
|
|
541
|
+
merged.source,
|
|
542
|
+
merged.updatedAt
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
this.db.prepare(`UPDATE ${TABLES.bindings} SET user_id = ?, external_user_id = ?, updated_at = ? WHERE user_id = ?`).run(
|
|
546
|
+
toUserId,
|
|
547
|
+
getExternalUserId(extractChannelFromScopedUserId(toUserId) ?? "manual", toUserId),
|
|
548
|
+
(/* @__PURE__ */ new Date()).toISOString(),
|
|
549
|
+
fromUserId
|
|
550
|
+
);
|
|
551
|
+
const oldMarkdownPath = this.profileMarkdownPath(fromUserId);
|
|
552
|
+
const newMarkdownPath = this.profileMarkdownPath(toUserId);
|
|
553
|
+
if ((0, import_node_fs.existsSync)(oldMarkdownPath) && !(0, import_node_fs.existsSync)(newMarkdownPath)) {
|
|
554
|
+
(0, import_node_fs.mkdirSync)((0, import_node_path.dirname)(newMarkdownPath), { recursive: true });
|
|
555
|
+
(0, import_node_fs.cpSync)(oldMarkdownPath, newMarkdownPath);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
411
558
|
};
|
|
412
559
|
var UserBindRuntime = class {
|
|
413
560
|
constructor(host, inputConfig) {
|
|
@@ -423,10 +570,24 @@ var UserBindRuntime = class {
|
|
|
423
570
|
config;
|
|
424
571
|
sessionCache = /* @__PURE__ */ new Map();
|
|
425
572
|
bindingCache = /* @__PURE__ */ new Map();
|
|
426
|
-
|
|
573
|
+
semanticCaptureCache = /* @__PURE__ */ new Map();
|
|
574
|
+
semanticCaptureInFlight = /* @__PURE__ */ new Set();
|
|
575
|
+
pendingBindingResolutions = /* @__PURE__ */ new Map();
|
|
576
|
+
feishuScopeStatusCache = /* @__PURE__ */ new Map();
|
|
427
577
|
bitableMirror = null;
|
|
428
578
|
feishuTokenCache = /* @__PURE__ */ new Map();
|
|
579
|
+
pendingBindingTimer = null;
|
|
580
|
+
pendingBindingKickTimer = null;
|
|
581
|
+
pendingBindingSweepInFlight = false;
|
|
429
582
|
close() {
|
|
583
|
+
if (this.pendingBindingTimer) {
|
|
584
|
+
clearInterval(this.pendingBindingTimer);
|
|
585
|
+
this.pendingBindingTimer = null;
|
|
586
|
+
}
|
|
587
|
+
if (this.pendingBindingKickTimer) {
|
|
588
|
+
clearTimeout(this.pendingBindingKickTimer);
|
|
589
|
+
this.pendingBindingKickTimer = null;
|
|
590
|
+
}
|
|
430
591
|
this.store.close();
|
|
431
592
|
}
|
|
432
593
|
register() {
|
|
@@ -438,15 +599,24 @@ var UserBindRuntime = class {
|
|
|
438
599
|
});
|
|
439
600
|
this.registerHooks();
|
|
440
601
|
this.registerTools();
|
|
602
|
+
this.startPendingBindingWorker();
|
|
441
603
|
exposeGlobalApi(this);
|
|
442
604
|
}
|
|
443
605
|
getIdentityForSession(sessionId) {
|
|
444
606
|
return this.sessionCache.get(sessionId) ?? null;
|
|
445
607
|
}
|
|
446
608
|
async resolveFromContext(context) {
|
|
447
|
-
const parsed = parseIdentityContext(context);
|
|
609
|
+
const parsed = parseIdentityContext(enrichIdentityContext(context));
|
|
448
610
|
if (parsed.sessionId && !parsed.channelType) {
|
|
449
|
-
|
|
611
|
+
const cached2 = this.sessionCache.get(parsed.sessionId) ?? null;
|
|
612
|
+
if (cached2 && isProvisionalScopedUserId(cached2.userId) && cached2.senderOpenId) {
|
|
613
|
+
return this.resolveFromContext({
|
|
614
|
+
sessionId: parsed.sessionId,
|
|
615
|
+
channel: { type: cached2.channelType },
|
|
616
|
+
sender: { id: cached2.senderOpenId, name: cached2.senderName }
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return cached2;
|
|
450
620
|
}
|
|
451
621
|
if (!parsed.sessionId || !parsed.channelType) {
|
|
452
622
|
return null;
|
|
@@ -454,47 +624,86 @@ var UserBindRuntime = class {
|
|
|
454
624
|
const cacheKey = `${parsed.channelType}:${parsed.openId ?? parsed.sessionId}`;
|
|
455
625
|
const cached = this.bindingCache.get(cacheKey);
|
|
456
626
|
if (cached && cached.expiresAt > Date.now()) {
|
|
457
|
-
|
|
458
|
-
|
|
627
|
+
if (parsed.openId && (isProvisionalScopedUserId(cached.identity.userId) || cached.identity.source === "synthetic-fallback")) {
|
|
628
|
+
const rebound = this.store.findBinding(parsed.channelType, parsed.openId);
|
|
629
|
+
if (!rebound || rebound.userId === cached.identity.userId) {
|
|
630
|
+
this.sessionCache.set(parsed.sessionId, cached.identity);
|
|
631
|
+
return cached.identity;
|
|
632
|
+
}
|
|
633
|
+
this.bindingCache.delete(cacheKey);
|
|
634
|
+
} else {
|
|
635
|
+
this.sessionCache.set(parsed.sessionId, cached.identity);
|
|
636
|
+
return cached.identity;
|
|
637
|
+
}
|
|
459
638
|
}
|
|
460
639
|
const binding = this.store.findBinding(parsed.channelType, parsed.openId);
|
|
461
640
|
let userId = binding?.userId ?? null;
|
|
462
641
|
let source = binding?.source ?? "local";
|
|
642
|
+
const provisionalUserId = parsed.openId ? createProvisionalUserId(parsed.channelType, parsed.openId) : null;
|
|
463
643
|
let remoteProfilePatch = {};
|
|
464
644
|
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
645
|
if (!userId) {
|
|
475
646
|
const remote = await this.tryResolveFeishuUser(parsed.openId);
|
|
476
647
|
if (remote?.userId) {
|
|
477
648
|
userId = remote.userId;
|
|
478
649
|
source = remote.source;
|
|
479
650
|
remoteProfilePatch = remote.profilePatch;
|
|
651
|
+
} else if (remote?.source === "feishu-contact-scope-missing") {
|
|
652
|
+
source = remote.source;
|
|
653
|
+
remoteProfilePatch = remote.profilePatch;
|
|
654
|
+
this.enqueuePendingBindingResolution(parsed.channelType, parsed.openId, remote.source);
|
|
480
655
|
} else {
|
|
481
656
|
this.store.recordIssue("feishu-resolution", `Failed to resolve real user id for ${parsed.openId}`);
|
|
657
|
+
this.enqueuePendingBindingResolution(parsed.channelType, parsed.openId, "feishu-resolution-miss");
|
|
482
658
|
}
|
|
483
659
|
}
|
|
484
660
|
}
|
|
661
|
+
const profilePatch = {
|
|
662
|
+
name: parsed.senderName,
|
|
663
|
+
...remoteProfilePatch
|
|
664
|
+
};
|
|
665
|
+
if (!userId && parsed.openId && parsed.channelType && CHANNELS_WITH_NATIVE_STABLE_IDS.has(parsed.channelType)) {
|
|
666
|
+
userId = `${parsed.channelType}:${parsed.openId}`;
|
|
667
|
+
source = "channel-native";
|
|
668
|
+
}
|
|
669
|
+
if (!userId && provisionalUserId) {
|
|
670
|
+
userId = provisionalUserId;
|
|
671
|
+
source = "provisional-openid";
|
|
672
|
+
this.enqueuePendingBindingResolution(parsed.channelType, parsed.openId, source);
|
|
673
|
+
}
|
|
674
|
+
const shouldPersistIdentity = Boolean(userId);
|
|
485
675
|
if (!userId) {
|
|
486
|
-
userId =
|
|
676
|
+
userId = createEphemeralUserId(parsed.channelType, parsed.openId, parsed.sessionId);
|
|
487
677
|
source = "synthetic-fallback";
|
|
488
678
|
}
|
|
489
|
-
|
|
679
|
+
if (shouldPersistIdentity) {
|
|
680
|
+
this.store.getProfile(userId);
|
|
681
|
+
}
|
|
682
|
+
const profile = shouldPersistIdentity ? (() => {
|
|
683
|
+
let persisted = this.store.upsertIdentity({
|
|
684
|
+
userId,
|
|
685
|
+
channelType: parsed.channelType,
|
|
686
|
+
openId: parsed.openId,
|
|
687
|
+
source,
|
|
688
|
+
profilePatch
|
|
689
|
+
});
|
|
690
|
+
if (provisionalUserId && persisted.userId !== provisionalUserId && source !== "provisional-openid") {
|
|
691
|
+
const reconciled = this.store.reconcileProvisionalIdentity(parsed.channelType, parsed.openId, persisted.userId);
|
|
692
|
+
if (reconciled) {
|
|
693
|
+
persisted = reconciled;
|
|
694
|
+
logUserBindEvent("provisional-profile-merged", {
|
|
695
|
+
provisionalUserId,
|
|
696
|
+
stableUserId: persisted.userId,
|
|
697
|
+
sessionId: parsed.sessionId
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
return persisted;
|
|
702
|
+
})() : buildLightweightProfile({
|
|
490
703
|
userId,
|
|
491
|
-
channelType: parsed.channelType,
|
|
492
|
-
openId: parsed.openId,
|
|
493
704
|
source,
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
...remoteProfilePatch
|
|
497
|
-
}
|
|
705
|
+
current: null,
|
|
706
|
+
profilePatch
|
|
498
707
|
});
|
|
499
708
|
const identity = {
|
|
500
709
|
sessionId: parsed.sessionId,
|
|
@@ -511,26 +720,154 @@ var UserBindRuntime = class {
|
|
|
511
720
|
expiresAt: Date.now() + this.config.cacheTtlMs,
|
|
512
721
|
identity
|
|
513
722
|
});
|
|
514
|
-
if (parsed.channelType === "feishu") {
|
|
723
|
+
if (shouldPersistIdentity && parsed.channelType === "feishu") {
|
|
515
724
|
await this.syncFeishuMirror(identity);
|
|
516
725
|
}
|
|
517
726
|
return identity;
|
|
518
727
|
}
|
|
728
|
+
startPendingBindingWorker() {
|
|
729
|
+
if (this.pendingBindingTimer) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
this.pendingBindingTimer = setInterval(() => {
|
|
733
|
+
void this.runPendingBindingSweep();
|
|
734
|
+
}, 60 * 1e3);
|
|
735
|
+
this.pendingBindingTimer.unref?.();
|
|
736
|
+
}
|
|
737
|
+
schedulePendingBindingSweep(delayMs) {
|
|
738
|
+
if (this.pendingBindingKickTimer) {
|
|
739
|
+
return;
|
|
740
|
+
}
|
|
741
|
+
this.pendingBindingKickTimer = setTimeout(() => {
|
|
742
|
+
this.pendingBindingKickTimer = null;
|
|
743
|
+
void this.runPendingBindingSweep();
|
|
744
|
+
}, delayMs);
|
|
745
|
+
this.pendingBindingKickTimer.unref?.();
|
|
746
|
+
}
|
|
747
|
+
enqueuePendingBindingResolution(channelType, openId, reason) {
|
|
748
|
+
if (!openId) {
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
const key = `${channelType}:${openId}`;
|
|
752
|
+
const current = this.pendingBindingResolutions.get(key);
|
|
753
|
+
if (current) {
|
|
754
|
+
current.reason = reason;
|
|
755
|
+
current.nextAttemptAt = Math.min(current.nextAttemptAt, Date.now() + 5e3);
|
|
756
|
+
this.pendingBindingResolutions.set(key, current);
|
|
757
|
+
} else {
|
|
758
|
+
this.pendingBindingResolutions.set(key, {
|
|
759
|
+
channelType,
|
|
760
|
+
openId,
|
|
761
|
+
reason,
|
|
762
|
+
attempts: 0,
|
|
763
|
+
nextAttemptAt: Date.now() + 5e3
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
this.schedulePendingBindingSweep(5e3);
|
|
767
|
+
}
|
|
768
|
+
async runPendingBindingSweep() {
|
|
769
|
+
if (this.pendingBindingSweepInFlight || this.pendingBindingResolutions.size === 0) {
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
this.pendingBindingSweepInFlight = true;
|
|
773
|
+
try {
|
|
774
|
+
const now = Date.now();
|
|
775
|
+
for (const [key, pending] of this.pendingBindingResolutions.entries()) {
|
|
776
|
+
if (pending.nextAttemptAt > now) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
const existing = this.store.findBinding(pending.channelType, pending.openId);
|
|
780
|
+
if (existing && !isProvisionalScopedUserId(existing.userId)) {
|
|
781
|
+
this.store.upsertIdentity({
|
|
782
|
+
userId: existing.userId,
|
|
783
|
+
channelType: pending.channelType,
|
|
784
|
+
openId: pending.openId,
|
|
785
|
+
source: existing.source,
|
|
786
|
+
profilePatch: {}
|
|
787
|
+
});
|
|
788
|
+
this.store.reconcileProvisionalIdentity(pending.channelType, pending.openId, existing.userId);
|
|
789
|
+
this.dropCachedOpenIdIdentity(pending.channelType, pending.openId);
|
|
790
|
+
this.pendingBindingResolutions.delete(key);
|
|
791
|
+
logUserBindEvent("pending-binding-reconciled", {
|
|
792
|
+
channelType: pending.channelType,
|
|
793
|
+
openId: pending.openId,
|
|
794
|
+
userId: existing.userId
|
|
795
|
+
});
|
|
796
|
+
continue;
|
|
797
|
+
}
|
|
798
|
+
if (pending.channelType !== "feishu") {
|
|
799
|
+
this.pendingBindingResolutions.delete(key);
|
|
800
|
+
continue;
|
|
801
|
+
}
|
|
802
|
+
const remote = await this.tryResolveFeishuUser(pending.openId);
|
|
803
|
+
if (remote?.userId) {
|
|
804
|
+
this.store.upsertIdentity({
|
|
805
|
+
userId: remote.userId,
|
|
806
|
+
channelType: pending.channelType,
|
|
807
|
+
openId: pending.openId,
|
|
808
|
+
source: remote.source,
|
|
809
|
+
profilePatch: remote.profilePatch
|
|
810
|
+
});
|
|
811
|
+
this.store.reconcileProvisionalIdentity(pending.channelType, pending.openId, remote.userId);
|
|
812
|
+
this.dropCachedOpenIdIdentity(pending.channelType, pending.openId);
|
|
813
|
+
this.pendingBindingResolutions.delete(key);
|
|
814
|
+
logUserBindEvent("pending-binding-resolved", {
|
|
815
|
+
channelType: pending.channelType,
|
|
816
|
+
openId: pending.openId,
|
|
817
|
+
userId: remote.userId,
|
|
818
|
+
source: remote.source
|
|
819
|
+
});
|
|
820
|
+
continue;
|
|
821
|
+
}
|
|
822
|
+
pending.attempts += 1;
|
|
823
|
+
pending.nextAttemptAt = Date.now() + getPendingBindingRetryDelayMs(remote?.source ?? pending.reason, pending.attempts);
|
|
824
|
+
this.pendingBindingResolutions.set(key, pending);
|
|
825
|
+
}
|
|
826
|
+
} finally {
|
|
827
|
+
this.pendingBindingSweepInFlight = false;
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
dropCachedOpenIdIdentity(channelType, openId) {
|
|
831
|
+
const cacheKey = `${channelType}:${openId}`;
|
|
832
|
+
this.bindingCache.delete(cacheKey);
|
|
833
|
+
for (const [sessionId, identity] of this.sessionCache.entries()) {
|
|
834
|
+
if (identity.channelType === channelType && identity.senderOpenId === openId) {
|
|
835
|
+
this.sessionCache.delete(sessionId);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
519
839
|
async getMyProfile(context) {
|
|
520
840
|
const identity = await this.requireCurrentIdentity(context);
|
|
521
841
|
return identity.profile;
|
|
522
842
|
}
|
|
523
|
-
async updateMyProfile(context, patch) {
|
|
843
|
+
async updateMyProfile(context, patch, operations) {
|
|
524
844
|
const identity = await this.requireCurrentIdentity(context);
|
|
525
|
-
const
|
|
526
|
-
|
|
845
|
+
const fallbackOperations = extractProfilePatchOperations(context && typeof context === "object" ? context : {});
|
|
846
|
+
const nextPatch = applyProfilePatchOperations(identity.profile, patch, operations ?? fallbackOperations);
|
|
847
|
+
const updated = identity.source === "synthetic-fallback" ? this.store.upsertIdentity({
|
|
848
|
+
userId: createStableLocalUserId(identity.channelType, identity.senderOpenId, identity.sessionId),
|
|
849
|
+
channelType: identity.channelType,
|
|
850
|
+
openId: identity.senderOpenId,
|
|
851
|
+
source: "self-bootstrap",
|
|
852
|
+
profilePatch: {
|
|
853
|
+
...identity.profile,
|
|
854
|
+
...nextPatch
|
|
855
|
+
}
|
|
856
|
+
}) : this.store.updateProfile(identity.userId, {
|
|
857
|
+
...nextPatch,
|
|
527
858
|
source: "self-update"
|
|
528
859
|
});
|
|
529
860
|
const nextIdentity = {
|
|
530
861
|
...identity,
|
|
862
|
+
userId: updated.userId,
|
|
863
|
+
source: updated.source,
|
|
531
864
|
profile: updated
|
|
532
865
|
};
|
|
533
866
|
this.sessionCache.set(identity.sessionId, nextIdentity);
|
|
867
|
+
this.bindingCache.set(`${identity.channelType}:${identity.senderOpenId ?? identity.sessionId}`, {
|
|
868
|
+
expiresAt: Date.now() + this.config.cacheTtlMs,
|
|
869
|
+
identity: nextIdentity
|
|
870
|
+
});
|
|
534
871
|
return updated;
|
|
535
872
|
}
|
|
536
873
|
async refreshMyBinding(context) {
|
|
@@ -546,6 +883,80 @@ var UserBindRuntime = class {
|
|
|
546
883
|
}
|
|
547
884
|
return refreshed;
|
|
548
885
|
}
|
|
886
|
+
async captureProfileFromMessage(context, identity) {
|
|
887
|
+
const messageText = extractUserUtterance(context);
|
|
888
|
+
if (!messageText) {
|
|
889
|
+
logUserBindEvent("semantic-profile-capture-skipped-no-utterance", {
|
|
890
|
+
userId: identity.userId,
|
|
891
|
+
sessionId: identity.sessionId
|
|
892
|
+
});
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
const fingerprint = hashId(`${identity.sessionId}:${messageText}`);
|
|
896
|
+
this.pruneSemanticCaptureCache();
|
|
897
|
+
if (this.semanticCaptureInFlight.has(fingerprint)) {
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
const processedAt = this.semanticCaptureCache.get(fingerprint);
|
|
901
|
+
if (processedAt && processedAt > Date.now() - 12 * 60 * 60 * 1e3) {
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
this.semanticCaptureInFlight.add(fingerprint);
|
|
905
|
+
try {
|
|
906
|
+
const extraction = await inferSemanticProfileExtraction(messageText, identity.profile);
|
|
907
|
+
if (!extraction?.shouldUpdate) {
|
|
908
|
+
logUserBindEvent("semantic-profile-capture-noop", {
|
|
909
|
+
userId: identity.userId,
|
|
910
|
+
sessionId: identity.sessionId,
|
|
911
|
+
confidence: extraction?.confidence ?? 0,
|
|
912
|
+
messagePreview: messageText.slice(0, 120)
|
|
913
|
+
});
|
|
914
|
+
this.semanticCaptureCache.set(fingerprint, Date.now());
|
|
915
|
+
return;
|
|
916
|
+
}
|
|
917
|
+
const { patch, operations } = cleanupSemanticProfilePatch(
|
|
918
|
+
extraction.patch,
|
|
919
|
+
identity.profile,
|
|
920
|
+
extraction.operations
|
|
921
|
+
);
|
|
922
|
+
if (Object.keys(patch).length === 0) {
|
|
923
|
+
this.semanticCaptureCache.set(fingerprint, Date.now());
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
await this.updateMyProfile(
|
|
927
|
+
{ sessionId: identity.sessionId },
|
|
928
|
+
{
|
|
929
|
+
...patch,
|
|
930
|
+
source: "semantic-self-update"
|
|
931
|
+
},
|
|
932
|
+
operations
|
|
933
|
+
);
|
|
934
|
+
logUserBindEvent("semantic-profile-capture-success", {
|
|
935
|
+
userId: identity.userId,
|
|
936
|
+
sessionId: identity.sessionId,
|
|
937
|
+
fields: Object.keys(patch),
|
|
938
|
+
operations,
|
|
939
|
+
confidence: extraction.confidence
|
|
940
|
+
});
|
|
941
|
+
this.semanticCaptureCache.set(fingerprint, Date.now());
|
|
942
|
+
} catch (error) {
|
|
943
|
+
logUserBindEvent("semantic-profile-capture-failed", {
|
|
944
|
+
userId: identity.userId,
|
|
945
|
+
sessionId: identity.sessionId,
|
|
946
|
+
message: error instanceof Error ? error.message : String(error)
|
|
947
|
+
});
|
|
948
|
+
} finally {
|
|
949
|
+
this.semanticCaptureInFlight.delete(fingerprint);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
pruneSemanticCaptureCache() {
|
|
953
|
+
const cutoff = Date.now() - 12 * 60 * 60 * 1e3;
|
|
954
|
+
for (const [fingerprint, ts] of this.semanticCaptureCache.entries()) {
|
|
955
|
+
if (ts < cutoff) {
|
|
956
|
+
this.semanticCaptureCache.delete(fingerprint);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
549
960
|
async adminInstruction(toolName, instruction, context) {
|
|
550
961
|
const requester = await this.resolveFromContext(context);
|
|
551
962
|
const agentId = getAgentIdFromContext(context);
|
|
@@ -612,18 +1023,23 @@ var UserBindRuntime = class {
|
|
|
612
1023
|
this.host.registerHook?.(
|
|
613
1024
|
["message:received", "message:preprocessed"],
|
|
614
1025
|
async (event) => {
|
|
615
|
-
await this.resolveFromContext(event);
|
|
1026
|
+
const identity = await this.resolveFromContext(event);
|
|
1027
|
+
if (identity) {
|
|
1028
|
+
await this.captureProfileFromMessage(event, identity);
|
|
1029
|
+
}
|
|
616
1030
|
},
|
|
617
1031
|
{
|
|
618
1032
|
name: "bamdra-user-bind-resolve",
|
|
619
1033
|
description: "Resolve runtime identity from channel sender metadata"
|
|
620
1034
|
}
|
|
621
1035
|
);
|
|
622
|
-
this.host.on?.("before_prompt_build", async (
|
|
1036
|
+
this.host.on?.("before_prompt_build", async (event, context) => {
|
|
623
1037
|
const identity = await this.resolveFromContext(context);
|
|
624
1038
|
if (!identity) {
|
|
625
1039
|
return;
|
|
626
1040
|
}
|
|
1041
|
+
const captureContext = mergeSemanticCaptureContext(event, context);
|
|
1042
|
+
await this.captureProfileFromMessage(captureContext, identity);
|
|
627
1043
|
return {
|
|
628
1044
|
context: [
|
|
629
1045
|
{
|
|
@@ -639,9 +1055,16 @@ var UserBindRuntime = class {
|
|
|
639
1055
|
if (!registerTool) {
|
|
640
1056
|
return;
|
|
641
1057
|
}
|
|
1058
|
+
const getMyProfileExecute = async (_id, params) => asTextResult(await this.getMyProfile(params));
|
|
1059
|
+
const updateMyProfileExecute = async (_id, params) => asTextResult(await this.updateMyProfile(
|
|
1060
|
+
params,
|
|
1061
|
+
sanitizeProfilePatch(params),
|
|
1062
|
+
extractProfilePatchOperations(params)
|
|
1063
|
+
));
|
|
1064
|
+
const refreshMyBindingExecute = async (_id, params) => asTextResult(await this.refreshMyBinding(params));
|
|
642
1065
|
registerTool({
|
|
643
1066
|
name: "bamdra_user_bind_get_my_profile",
|
|
644
|
-
description: "Get the current user's bound profile",
|
|
1067
|
+
description: "Get the current user's bound profile before replying so nickname, timezone, and stable preferences can be used naturally in the response",
|
|
645
1068
|
parameters: {
|
|
646
1069
|
type: "object",
|
|
647
1070
|
additionalProperties: false,
|
|
@@ -649,11 +1072,11 @@ var UserBindRuntime = class {
|
|
|
649
1072
|
sessionId: { type: "string" }
|
|
650
1073
|
}
|
|
651
1074
|
},
|
|
652
|
-
execute:
|
|
1075
|
+
execute: getMyProfileExecute
|
|
653
1076
|
});
|
|
654
1077
|
registerTool({
|
|
655
1078
|
name: "bamdra_user_bind_update_my_profile",
|
|
656
|
-
description: "
|
|
1079
|
+
description: "Immediately write the current user's stable preferences into their profile when they clearly provide them, such as nickname, communication style, timezone, role, or durable notes",
|
|
657
1080
|
parameters: {
|
|
658
1081
|
type: "object",
|
|
659
1082
|
additionalProperties: false,
|
|
@@ -661,18 +1084,67 @@ var UserBindRuntime = class {
|
|
|
661
1084
|
properties: {
|
|
662
1085
|
sessionId: { type: "string" },
|
|
663
1086
|
nickname: { type: "string" },
|
|
1087
|
+
nicknameOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
664
1088
|
preferences: { type: "string" },
|
|
1089
|
+
preferencesOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
665
1090
|
personality: { type: "string" },
|
|
1091
|
+
personalityOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
666
1092
|
role: { type: "string" },
|
|
1093
|
+
roleOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
667
1094
|
timezone: { type: "string" },
|
|
668
|
-
|
|
1095
|
+
timezoneOperation: { type: "string", enum: ["replace", "append", "remove"] },
|
|
1096
|
+
notes: { type: "string" },
|
|
1097
|
+
notesOperation: { type: "string", enum: ["replace", "append", "remove"] }
|
|
669
1098
|
}
|
|
670
1099
|
},
|
|
671
|
-
execute:
|
|
1100
|
+
execute: updateMyProfileExecute
|
|
672
1101
|
});
|
|
673
1102
|
registerTool({
|
|
674
1103
|
name: "bamdra_user_bind_refresh_my_binding",
|
|
675
|
-
description: "Refresh the current user's identity binding",
|
|
1104
|
+
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",
|
|
1105
|
+
parameters: {
|
|
1106
|
+
type: "object",
|
|
1107
|
+
additionalProperties: false,
|
|
1108
|
+
properties: {
|
|
1109
|
+
sessionId: { type: "string" }
|
|
1110
|
+
}
|
|
1111
|
+
},
|
|
1112
|
+
execute: refreshMyBindingExecute
|
|
1113
|
+
});
|
|
1114
|
+
registerTool({
|
|
1115
|
+
name: SELF_TOOL_ALIASES[0],
|
|
1116
|
+
description: "Alias of bamdra_user_bind_get_my_profile",
|
|
1117
|
+
parameters: {
|
|
1118
|
+
type: "object",
|
|
1119
|
+
additionalProperties: false,
|
|
1120
|
+
properties: {
|
|
1121
|
+
sessionId: { type: "string" }
|
|
1122
|
+
}
|
|
1123
|
+
},
|
|
1124
|
+
execute: getMyProfileExecute
|
|
1125
|
+
});
|
|
1126
|
+
registerTool({
|
|
1127
|
+
name: SELF_TOOL_ALIASES[1],
|
|
1128
|
+
description: "Alias of bamdra_user_bind_update_my_profile",
|
|
1129
|
+
parameters: {
|
|
1130
|
+
type: "object",
|
|
1131
|
+
additionalProperties: false,
|
|
1132
|
+
required: ["sessionId"],
|
|
1133
|
+
properties: {
|
|
1134
|
+
sessionId: { type: "string" },
|
|
1135
|
+
nickname: { type: "string" },
|
|
1136
|
+
preferences: { type: "string" },
|
|
1137
|
+
personality: { type: "string" },
|
|
1138
|
+
role: { type: "string" },
|
|
1139
|
+
timezone: { type: "string" },
|
|
1140
|
+
notes: { type: "string" }
|
|
1141
|
+
}
|
|
1142
|
+
},
|
|
1143
|
+
execute: updateMyProfileExecute
|
|
1144
|
+
});
|
|
1145
|
+
registerTool({
|
|
1146
|
+
name: SELF_TOOL_ALIASES[2],
|
|
1147
|
+
description: "Alias of bamdra_user_bind_refresh_my_binding",
|
|
676
1148
|
parameters: {
|
|
677
1149
|
type: "object",
|
|
678
1150
|
additionalProperties: false,
|
|
@@ -680,7 +1152,7 @@ var UserBindRuntime = class {
|
|
|
680
1152
|
sessionId: { type: "string" }
|
|
681
1153
|
}
|
|
682
1154
|
},
|
|
683
|
-
execute:
|
|
1155
|
+
execute: refreshMyBindingExecute
|
|
684
1156
|
});
|
|
685
1157
|
for (const toolName of ADMIN_TOOL_NAMES) {
|
|
686
1158
|
registerTool({
|
|
@@ -714,6 +1186,7 @@ var UserBindRuntime = class {
|
|
|
714
1186
|
logUserBindEvent("feishu-resolution-skipped", { reason: "no-feishu-accounts-configured" });
|
|
715
1187
|
return null;
|
|
716
1188
|
}
|
|
1189
|
+
const contactScopeBlockedAccounts = /* @__PURE__ */ new Set();
|
|
717
1190
|
for (const account of accounts) {
|
|
718
1191
|
try {
|
|
719
1192
|
const token = await this.getFeishuAppAccessToken(account);
|
|
@@ -746,6 +1219,9 @@ var UserBindRuntime = class {
|
|
|
746
1219
|
};
|
|
747
1220
|
} catch (error) {
|
|
748
1221
|
const message = error instanceof Error ? error.message : String(error);
|
|
1222
|
+
if (looksLikeFeishuContactScopeError(message)) {
|
|
1223
|
+
contactScopeBlockedAccounts.add(account.accountId);
|
|
1224
|
+
}
|
|
749
1225
|
logUserBindEvent("feishu-resolution-attempt-failed", {
|
|
750
1226
|
accountId: account.accountId,
|
|
751
1227
|
openId,
|
|
@@ -777,15 +1253,24 @@ var UserBindRuntime = class {
|
|
|
777
1253
|
} catch {
|
|
778
1254
|
}
|
|
779
1255
|
}
|
|
1256
|
+
if (contactScopeBlockedAccounts.size > 0) {
|
|
1257
|
+
return {
|
|
1258
|
+
userId: "",
|
|
1259
|
+
source: "feishu-contact-scope-missing",
|
|
1260
|
+
profilePatch: {
|
|
1261
|
+
notes: renderFeishuContactScopeGuidance([...contactScopeBlockedAccounts])
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
780
1265
|
logUserBindEvent("feishu-resolution-empty", { openId });
|
|
781
1266
|
return null;
|
|
782
1267
|
}
|
|
783
|
-
async ensureFeishuScopeStatus() {
|
|
784
|
-
if (
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
1268
|
+
async ensureFeishuScopeStatus(account) {
|
|
1269
|
+
if (account) {
|
|
1270
|
+
const cached = this.feishuScopeStatusCache.get(account.accountId);
|
|
1271
|
+
if (cached) {
|
|
1272
|
+
return cached;
|
|
1273
|
+
}
|
|
789
1274
|
try {
|
|
790
1275
|
const token = await this.getFeishuAppAccessToken(account);
|
|
791
1276
|
const result = await feishuJsonRequest(
|
|
@@ -794,45 +1279,58 @@ var UserBindRuntime = class {
|
|
|
794
1279
|
token
|
|
795
1280
|
);
|
|
796
1281
|
const scopes = extractScopes(result);
|
|
797
|
-
|
|
1282
|
+
const status = {
|
|
798
1283
|
scopes,
|
|
799
1284
|
missingIdentityScopes: REQUIRED_FEISHU_IDENTITY_SCOPES.filter((scope) => !scopes.includes(scope)),
|
|
800
1285
|
hasDocumentAccess: scopes.some((scope) => scope.startsWith("bitable:") || scope.startsWith("drive:") || scope.startsWith("docx:") || scope.startsWith("docs:"))
|
|
801
1286
|
};
|
|
1287
|
+
this.feishuScopeStatusCache.set(account.accountId, status);
|
|
802
1288
|
logUserBindEvent("feishu-scopes-read", {
|
|
803
1289
|
accountId: account.accountId,
|
|
804
|
-
...
|
|
1290
|
+
...status
|
|
805
1291
|
});
|
|
806
|
-
return
|
|
1292
|
+
return status;
|
|
807
1293
|
} catch (error) {
|
|
808
1294
|
const message = error instanceof Error ? error.message : String(error);
|
|
809
1295
|
logUserBindEvent("feishu-scopes-attempt-failed", { accountId: account.accountId, message });
|
|
810
1296
|
}
|
|
811
1297
|
}
|
|
1298
|
+
const accounts = readFeishuAccountsFromOpenClawConfig();
|
|
1299
|
+
for (const candidate of accounts) {
|
|
1300
|
+
const cached = this.feishuScopeStatusCache.get(candidate.accountId);
|
|
1301
|
+
if (cached?.hasDocumentAccess) {
|
|
1302
|
+
return cached;
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
for (const candidate of accounts) {
|
|
1306
|
+
const status = await this.ensureFeishuScopeStatus(candidate);
|
|
1307
|
+
if (status.hasDocumentAccess) {
|
|
1308
|
+
return status;
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
812
1311
|
const executor = this.host.callTool ?? this.host.invokeTool;
|
|
813
1312
|
if (typeof executor === "function") {
|
|
814
1313
|
try {
|
|
815
1314
|
const result = await executor.call(this.host, "feishu_app_scopes", {});
|
|
816
1315
|
const scopes = extractScopes(result);
|
|
817
|
-
|
|
1316
|
+
const status = {
|
|
818
1317
|
scopes,
|
|
819
1318
|
missingIdentityScopes: REQUIRED_FEISHU_IDENTITY_SCOPES.filter((scope) => !scopes.includes(scope)),
|
|
820
1319
|
hasDocumentAccess: scopes.some((scope) => scope.startsWith("bitable:") || scope.startsWith("drive:") || scope.startsWith("docx:") || scope.startsWith("docs:"))
|
|
821
1320
|
};
|
|
822
|
-
logUserBindEvent("feishu-scopes-read",
|
|
823
|
-
return
|
|
1321
|
+
logUserBindEvent("feishu-scopes-read", status);
|
|
1322
|
+
return status;
|
|
824
1323
|
} catch (error) {
|
|
825
1324
|
const message = error instanceof Error ? error.message : String(error);
|
|
826
1325
|
logUserBindEvent("feishu-scopes-failed", { message });
|
|
827
1326
|
this.store.recordIssue("feishu-scope-read", message);
|
|
828
1327
|
}
|
|
829
1328
|
}
|
|
830
|
-
|
|
1329
|
+
return {
|
|
831
1330
|
scopes: [],
|
|
832
1331
|
missingIdentityScopes: [...REQUIRED_FEISHU_IDENTITY_SCOPES],
|
|
833
1332
|
hasDocumentAccess: false
|
|
834
1333
|
};
|
|
835
|
-
return this.feishuScopeStatus;
|
|
836
1334
|
}
|
|
837
1335
|
async getFeishuAppAccessToken(account) {
|
|
838
1336
|
const cached = this.feishuTokenCache.get(account.accountId);
|
|
@@ -993,6 +1491,9 @@ function exposeGlobalApi(runtime) {
|
|
|
993
1491
|
},
|
|
994
1492
|
async resolveIdentity(context) {
|
|
995
1493
|
return runtime.resolveFromContext(context);
|
|
1494
|
+
},
|
|
1495
|
+
async runPendingBindingSweep() {
|
|
1496
|
+
return runtime.runPendingBindingSweep();
|
|
996
1497
|
}
|
|
997
1498
|
};
|
|
998
1499
|
}
|
|
@@ -1084,9 +1585,8 @@ function ensureToolNames(tools, values) {
|
|
|
1084
1585
|
return changed;
|
|
1085
1586
|
}
|
|
1086
1587
|
function ensureAgentSkills(agents, skillId) {
|
|
1087
|
-
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
1088
1588
|
let changed = false;
|
|
1089
|
-
for (const item of
|
|
1589
|
+
for (const item of iterAgentConfigs(agents)) {
|
|
1090
1590
|
if (!item || typeof item !== "object") {
|
|
1091
1591
|
continue;
|
|
1092
1592
|
}
|
|
@@ -1101,10 +1601,9 @@ function ensureAgentSkills(agents, skillId) {
|
|
|
1101
1601
|
return changed;
|
|
1102
1602
|
}
|
|
1103
1603
|
function ensureAdminSkill(agents, skillId, adminAgents) {
|
|
1104
|
-
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
1105
1604
|
let changed = false;
|
|
1106
1605
|
let attached = false;
|
|
1107
|
-
for (const item of
|
|
1606
|
+
for (const item of iterAgentConfigs(agents)) {
|
|
1108
1607
|
if (!item || typeof item !== "object") {
|
|
1109
1608
|
continue;
|
|
1110
1609
|
}
|
|
@@ -1121,6 +1620,7 @@ function ensureAdminSkill(agents, skillId, adminAgents) {
|
|
|
1121
1620
|
}
|
|
1122
1621
|
attached = true;
|
|
1123
1622
|
}
|
|
1623
|
+
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
1124
1624
|
if (!attached && list.length > 0 && list[0] && typeof list[0] === "object") {
|
|
1125
1625
|
const agent = list[0];
|
|
1126
1626
|
const current = Array.isArray(agent.skills) ? [...agent.skills] : [];
|
|
@@ -1132,6 +1632,34 @@ function ensureAdminSkill(agents, skillId, adminAgents) {
|
|
|
1132
1632
|
}
|
|
1133
1633
|
return changed;
|
|
1134
1634
|
}
|
|
1635
|
+
function* iterAgentConfigs(agents) {
|
|
1636
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1637
|
+
const list = Array.isArray(agents.list) ? agents.list : [];
|
|
1638
|
+
for (const item of list) {
|
|
1639
|
+
if (item && typeof item === "object") {
|
|
1640
|
+
const agent = item;
|
|
1641
|
+
seen.add(agent);
|
|
1642
|
+
yield agent;
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
if (list.length > 0) {
|
|
1646
|
+
return;
|
|
1647
|
+
}
|
|
1648
|
+
for (const [key, value] of Object.entries(agents)) {
|
|
1649
|
+
if (key === "list" || key === "defaults" || !value || typeof value !== "object" || Array.isArray(value)) {
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
const agent = value;
|
|
1653
|
+
if (seen.has(agent)) {
|
|
1654
|
+
continue;
|
|
1655
|
+
}
|
|
1656
|
+
seen.add(agent);
|
|
1657
|
+
if (!getConfiguredAgentId(agent)) {
|
|
1658
|
+
agent.id = key;
|
|
1659
|
+
}
|
|
1660
|
+
yield agent;
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
1135
1663
|
function mapProfileRow(row) {
|
|
1136
1664
|
return {
|
|
1137
1665
|
userId: String(row.user_id),
|
|
@@ -1162,12 +1690,12 @@ function parseIdentityContext(context) {
|
|
|
1162
1690
|
const metadataText = asNullableString(record.text) ?? asNullableString(message.text) ?? asNullableString(record.content) ?? asNullableString(metadata.text) ?? asNullableString(input.text) ?? asNullableString(findNestedValue(record, ["message", "content", "text"]));
|
|
1163
1691
|
const conversationInfo = metadataText ? extractTaggedJsonBlock(metadataText, "Conversation info (untrusted metadata)") : null;
|
|
1164
1692
|
const senderInfo = metadataText ? extractTaggedJsonBlock(metadataText, "Sender (untrusted metadata)") : null;
|
|
1165
|
-
const senderIdFromText =
|
|
1693
|
+
const senderIdFromText = extractSenderIdFromMetadataText(metadataText);
|
|
1166
1694
|
const senderNameFromText = metadataText ? extractRegexValue(metadataText, /"sender"\s*:\s*"([^"]+)"/) : null;
|
|
1167
1695
|
const senderNameFromMessageLine = metadataText ? extractRegexValue(metadataText, /\]\s*([^\n::]{1,40})\s*[::]/) : null;
|
|
1168
1696
|
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 =
|
|
1697
|
+
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);
|
|
1698
|
+
const openId = extractSenderId(record, sender, senderInfo, conversationInfo, message, senderIdFromText) ?? extractOpenIdFromSessionId(sessionId);
|
|
1171
1699
|
const senderName = asNullableString(sender.name) ?? asNullableString(sender.display_name) ?? asNullableString(senderInfo?.name) ?? asNullableString(conversationInfo?.sender) ?? senderNameFromText ?? senderNameFromMessageLine;
|
|
1172
1700
|
return {
|
|
1173
1701
|
sessionId,
|
|
@@ -1176,6 +1704,262 @@ function parseIdentityContext(context) {
|
|
|
1176
1704
|
senderName
|
|
1177
1705
|
};
|
|
1178
1706
|
}
|
|
1707
|
+
function enrichIdentityContext(context) {
|
|
1708
|
+
const record = context && typeof context === "object" ? { ...context } : {};
|
|
1709
|
+
const sessionManager = record.sessionManager && typeof record.sessionManager === "object" ? record.sessionManager : null;
|
|
1710
|
+
if (!sessionManager) {
|
|
1711
|
+
return record;
|
|
1712
|
+
}
|
|
1713
|
+
const sessionSnapshot = readSessionManagerSnapshot(sessionManager);
|
|
1714
|
+
if (!sessionSnapshot) {
|
|
1715
|
+
return record;
|
|
1716
|
+
}
|
|
1717
|
+
if (!record.sessionId && sessionSnapshot.sessionId) {
|
|
1718
|
+
record.sessionId = sessionSnapshot.sessionId;
|
|
1719
|
+
}
|
|
1720
|
+
if (!record.text && sessionSnapshot.metadataText) {
|
|
1721
|
+
record.text = sessionSnapshot.metadataText;
|
|
1722
|
+
}
|
|
1723
|
+
const metadata = record.metadata && typeof record.metadata === "object" ? { ...record.metadata } : {};
|
|
1724
|
+
if (!metadata.text && sessionSnapshot.metadataText) {
|
|
1725
|
+
metadata.text = sessionSnapshot.metadataText;
|
|
1726
|
+
}
|
|
1727
|
+
if (!metadata.sessionId && sessionSnapshot.sessionId) {
|
|
1728
|
+
metadata.sessionId = sessionSnapshot.sessionId;
|
|
1729
|
+
}
|
|
1730
|
+
if (Object.keys(metadata).length > 0) {
|
|
1731
|
+
record.metadata = metadata;
|
|
1732
|
+
}
|
|
1733
|
+
if (!record.channelType && sessionSnapshot.channelType) {
|
|
1734
|
+
record.channelType = sessionSnapshot.channelType;
|
|
1735
|
+
}
|
|
1736
|
+
if (!record.openId && sessionSnapshot.openId) {
|
|
1737
|
+
record.openId = sessionSnapshot.openId;
|
|
1738
|
+
}
|
|
1739
|
+
return record;
|
|
1740
|
+
}
|
|
1741
|
+
function readSessionManagerSnapshot(sessionManager) {
|
|
1742
|
+
try {
|
|
1743
|
+
const getSessionId = sessionManager.getSessionId;
|
|
1744
|
+
const getBranch = sessionManager.getBranch;
|
|
1745
|
+
const sessionId = typeof getSessionId === "function" ? asNullableString(getSessionId()) : null;
|
|
1746
|
+
const branch = typeof getBranch === "function" ? getBranch() : [];
|
|
1747
|
+
if (!Array.isArray(branch) || branch.length === 0) {
|
|
1748
|
+
return sessionId ? {
|
|
1749
|
+
sessionId,
|
|
1750
|
+
metadataText: null,
|
|
1751
|
+
channelType: inferChannelTypeFromSessionId(sessionId),
|
|
1752
|
+
openId: extractOpenIdFromSessionId(sessionId)
|
|
1753
|
+
} : null;
|
|
1754
|
+
}
|
|
1755
|
+
for (let i = branch.length - 1; i >= 0; i -= 1) {
|
|
1756
|
+
const entry = branch[i];
|
|
1757
|
+
if (!entry || typeof entry !== "object") {
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
const item = entry;
|
|
1761
|
+
const message = item.message && typeof item.message === "object" ? item.message : null;
|
|
1762
|
+
if (item.type !== "message" || !message || message.role !== "user") {
|
|
1763
|
+
continue;
|
|
1764
|
+
}
|
|
1765
|
+
const metadataText = extractMessageText(message);
|
|
1766
|
+
if (!metadataText || !looksLikeIdentityMetadata(metadataText)) {
|
|
1767
|
+
continue;
|
|
1768
|
+
}
|
|
1769
|
+
const openId = extractSenderIdFromMetadataText(metadataText);
|
|
1770
|
+
const channelType = inferChannelTypeFromSenderId(openId) ?? inferChannelTypeFromSessionId(sessionId);
|
|
1771
|
+
return {
|
|
1772
|
+
sessionId,
|
|
1773
|
+
metadataText,
|
|
1774
|
+
channelType,
|
|
1775
|
+
openId
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
return sessionId ? {
|
|
1779
|
+
sessionId,
|
|
1780
|
+
metadataText: null,
|
|
1781
|
+
channelType: inferChannelTypeFromSessionId(sessionId),
|
|
1782
|
+
openId: extractOpenIdFromSessionId(sessionId)
|
|
1783
|
+
} : null;
|
|
1784
|
+
} catch (error) {
|
|
1785
|
+
logUserBindEvent("session-manager-read-failed", {
|
|
1786
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1787
|
+
});
|
|
1788
|
+
return null;
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
function extractMessageText(message) {
|
|
1792
|
+
const content = message.content;
|
|
1793
|
+
if (typeof content === "string") {
|
|
1794
|
+
return content;
|
|
1795
|
+
}
|
|
1796
|
+
if (!Array.isArray(content)) {
|
|
1797
|
+
return null;
|
|
1798
|
+
}
|
|
1799
|
+
const text = content.filter((part) => part && typeof part === "object" && part.type === "text" && typeof part.text === "string").map((part) => String(part.text)).join("\n");
|
|
1800
|
+
return text || null;
|
|
1801
|
+
}
|
|
1802
|
+
function looksLikeIdentityMetadata(text) {
|
|
1803
|
+
return text.includes("Conversation info (untrusted metadata)") || text.includes("Sender (untrusted metadata)") || /"sender_id"\s*:\s*"/.test(text);
|
|
1804
|
+
}
|
|
1805
|
+
function extractUserUtterance(context) {
|
|
1806
|
+
const record = context && typeof context === "object" ? context : {};
|
|
1807
|
+
const rawText = extractHookContextText(record) ?? readLatestUserMessageFromSessionManager(record.sessionManager);
|
|
1808
|
+
if (!rawText) {
|
|
1809
|
+
return null;
|
|
1810
|
+
}
|
|
1811
|
+
const stripped = stripIdentityMetadata(rawText);
|
|
1812
|
+
if (!stripped) {
|
|
1813
|
+
return null;
|
|
1814
|
+
}
|
|
1815
|
+
if (looksLikeIdentityMetadata(stripped)) {
|
|
1816
|
+
return null;
|
|
1817
|
+
}
|
|
1818
|
+
return stripped;
|
|
1819
|
+
}
|
|
1820
|
+
function extractHookContextText(record) {
|
|
1821
|
+
const directText = normalizeHookText(
|
|
1822
|
+
record.bodyForAgent ?? record.body ?? record.prompt ?? record.text ?? findNestedValue(record, ["context", "bodyForAgent"]) ?? findNestedValue(record, ["context", "body"]) ?? findNestedValue(record, ["context", "text"]) ?? findNestedValue(record, ["context", "content"])
|
|
1823
|
+
);
|
|
1824
|
+
if (directText) {
|
|
1825
|
+
return directText;
|
|
1826
|
+
}
|
|
1827
|
+
const message = findNestedRecord(record, ["message"], ["event", "message"], ["payload", "message"]);
|
|
1828
|
+
const messageText = extractMessageText(message);
|
|
1829
|
+
if (messageText) {
|
|
1830
|
+
return messageText;
|
|
1831
|
+
}
|
|
1832
|
+
const inputText = extractTextFromInput(record.input);
|
|
1833
|
+
if (inputText) {
|
|
1834
|
+
return inputText;
|
|
1835
|
+
}
|
|
1836
|
+
const messages = Array.isArray(record.messages) ? record.messages : [];
|
|
1837
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
1838
|
+
const item = messages[i];
|
|
1839
|
+
if (!item || typeof item !== "object") {
|
|
1840
|
+
continue;
|
|
1841
|
+
}
|
|
1842
|
+
const messageRecord = item;
|
|
1843
|
+
const role = asNullableString(messageRecord.role) ?? "user";
|
|
1844
|
+
if (role !== "user") {
|
|
1845
|
+
continue;
|
|
1846
|
+
}
|
|
1847
|
+
const text = normalizeHookText(messageRecord.text ?? messageRecord.content);
|
|
1848
|
+
if (text) {
|
|
1849
|
+
return text;
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
return null;
|
|
1853
|
+
}
|
|
1854
|
+
function extractTextFromInput(input) {
|
|
1855
|
+
if (typeof input === "string") {
|
|
1856
|
+
return normalizeHookText(input);
|
|
1857
|
+
}
|
|
1858
|
+
if (!input || typeof input !== "object") {
|
|
1859
|
+
return null;
|
|
1860
|
+
}
|
|
1861
|
+
const record = input;
|
|
1862
|
+
const directText = normalizeHookText(record.text ?? record.content);
|
|
1863
|
+
if (directText) {
|
|
1864
|
+
return directText;
|
|
1865
|
+
}
|
|
1866
|
+
const message = record.message && typeof record.message === "object" ? record.message : null;
|
|
1867
|
+
const messageText = normalizeHookText(message?.text ?? message?.content);
|
|
1868
|
+
if (messageText) {
|
|
1869
|
+
return messageText;
|
|
1870
|
+
}
|
|
1871
|
+
const messages = Array.isArray(record.messages) ? record.messages : [];
|
|
1872
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
1873
|
+
const item = messages[i];
|
|
1874
|
+
if (!item || typeof item !== "object") {
|
|
1875
|
+
continue;
|
|
1876
|
+
}
|
|
1877
|
+
const messageRecord = item;
|
|
1878
|
+
const role = asNullableString(messageRecord.role) ?? "user";
|
|
1879
|
+
if (role !== "user") {
|
|
1880
|
+
continue;
|
|
1881
|
+
}
|
|
1882
|
+
const text = normalizeHookText(messageRecord.text ?? messageRecord.content);
|
|
1883
|
+
if (text) {
|
|
1884
|
+
return text;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return null;
|
|
1888
|
+
}
|
|
1889
|
+
function normalizeHookText(value) {
|
|
1890
|
+
if (typeof value === "string") {
|
|
1891
|
+
return value.trim() || null;
|
|
1892
|
+
}
|
|
1893
|
+
if (Array.isArray(value)) {
|
|
1894
|
+
const text = value.map((item) => {
|
|
1895
|
+
if (!item || typeof item !== "object") {
|
|
1896
|
+
return "";
|
|
1897
|
+
}
|
|
1898
|
+
return asNullableString(item.text) ?? "";
|
|
1899
|
+
}).filter(Boolean).join("\n").trim();
|
|
1900
|
+
return text || null;
|
|
1901
|
+
}
|
|
1902
|
+
return null;
|
|
1903
|
+
}
|
|
1904
|
+
function readLatestUserMessageFromSessionManager(sessionManager) {
|
|
1905
|
+
if (!sessionManager || typeof sessionManager !== "object") {
|
|
1906
|
+
return null;
|
|
1907
|
+
}
|
|
1908
|
+
try {
|
|
1909
|
+
const getBranch = sessionManager.getBranch;
|
|
1910
|
+
const branch = typeof getBranch === "function" ? getBranch() : [];
|
|
1911
|
+
if (!Array.isArray(branch) || branch.length === 0) {
|
|
1912
|
+
return null;
|
|
1913
|
+
}
|
|
1914
|
+
for (let i = branch.length - 1; i >= 0; i -= 1) {
|
|
1915
|
+
const entry = branch[i];
|
|
1916
|
+
if (!entry || typeof entry !== "object") {
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
const item = entry;
|
|
1920
|
+
const message = item.message && typeof item.message === "object" ? item.message : null;
|
|
1921
|
+
if (item.type !== "message" || !message || message.role !== "user") {
|
|
1922
|
+
continue;
|
|
1923
|
+
}
|
|
1924
|
+
const text = extractMessageText(message);
|
|
1925
|
+
if (text && !looksLikeIdentityMetadata(text)) {
|
|
1926
|
+
return text.trim();
|
|
1927
|
+
}
|
|
1928
|
+
}
|
|
1929
|
+
} catch (error) {
|
|
1930
|
+
logUserBindEvent("session-manager-user-message-read-failed", {
|
|
1931
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
return null;
|
|
1935
|
+
}
|
|
1936
|
+
function mergeSemanticCaptureContext(event, context) {
|
|
1937
|
+
const merged = {};
|
|
1938
|
+
if (event && typeof event === "object") {
|
|
1939
|
+
Object.assign(merged, event);
|
|
1940
|
+
}
|
|
1941
|
+
if (context && typeof context === "object") {
|
|
1942
|
+
const record = context;
|
|
1943
|
+
for (const [key, value] of Object.entries(record)) {
|
|
1944
|
+
if (merged[key] === void 0) {
|
|
1945
|
+
merged[key] = value;
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
}
|
|
1949
|
+
if (merged.sessionManager === void 0 && context && typeof context === "object") {
|
|
1950
|
+
merged.sessionManager = context.sessionManager;
|
|
1951
|
+
}
|
|
1952
|
+
return merged;
|
|
1953
|
+
}
|
|
1954
|
+
function stripIdentityMetadata(text) {
|
|
1955
|
+
const hadMetadata = looksLikeIdentityMetadata(text);
|
|
1956
|
+
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, "");
|
|
1957
|
+
if (hadMetadata) {
|
|
1958
|
+
cleaned = cleaned.replace(/^\s*[^\n::]{1,40}[::]\s*/m, "");
|
|
1959
|
+
}
|
|
1960
|
+
cleaned = cleaned.trim();
|
|
1961
|
+
return cleaned || null;
|
|
1962
|
+
}
|
|
1179
1963
|
function findNestedRecord(root, ...paths) {
|
|
1180
1964
|
for (const path of paths) {
|
|
1181
1965
|
const value = findNestedValue(root, path);
|
|
@@ -1224,6 +2008,108 @@ function inferChannelTypeFromSessionId(sessionId) {
|
|
|
1224
2008
|
if (sessionId.includes(":whatsapp:")) {
|
|
1225
2009
|
return "whatsapp";
|
|
1226
2010
|
}
|
|
2011
|
+
if (sessionId.includes(":discord:")) {
|
|
2012
|
+
return "discord";
|
|
2013
|
+
}
|
|
2014
|
+
if (sessionId.includes(":googlechat:")) {
|
|
2015
|
+
return "googlechat";
|
|
2016
|
+
}
|
|
2017
|
+
if (sessionId.includes(":slack:")) {
|
|
2018
|
+
return "slack";
|
|
2019
|
+
}
|
|
2020
|
+
if (sessionId.includes(":mattermost:")) {
|
|
2021
|
+
return "mattermost";
|
|
2022
|
+
}
|
|
2023
|
+
if (sessionId.includes(":signal:")) {
|
|
2024
|
+
return "signal";
|
|
2025
|
+
}
|
|
2026
|
+
if (sessionId.includes(":imessage:")) {
|
|
2027
|
+
return "imessage";
|
|
2028
|
+
}
|
|
2029
|
+
if (sessionId.includes(":msteams:")) {
|
|
2030
|
+
return "msteams";
|
|
2031
|
+
}
|
|
2032
|
+
return null;
|
|
2033
|
+
}
|
|
2034
|
+
function inferChannelTypeFromSenderId(senderId) {
|
|
2035
|
+
if (!senderId) {
|
|
2036
|
+
return null;
|
|
2037
|
+
}
|
|
2038
|
+
if (/^(?:ou|oc)_[A-Za-z0-9_-]+$/.test(senderId)) {
|
|
2039
|
+
return "feishu";
|
|
2040
|
+
}
|
|
2041
|
+
if (/@(?:s\.whatsapp\.net|g\.us)$/.test(senderId)) {
|
|
2042
|
+
return "whatsapp";
|
|
2043
|
+
}
|
|
2044
|
+
if (/^users\/.+/.test(senderId) || /^spaces\/.+/.test(senderId)) {
|
|
2045
|
+
return "googlechat";
|
|
2046
|
+
}
|
|
2047
|
+
return null;
|
|
2048
|
+
}
|
|
2049
|
+
function extractSenderId(record, sender, senderInfo, conversationInfo, message, senderIdFromText) {
|
|
2050
|
+
return firstNonEmptyString(
|
|
2051
|
+
asNullableString(record.openId),
|
|
2052
|
+
asNullableString(record.senderId),
|
|
2053
|
+
asNullableString(record.userId),
|
|
2054
|
+
asNullableString(record.fromId),
|
|
2055
|
+
asNullableString(record.participantId),
|
|
2056
|
+
asNullableString(record.authorId),
|
|
2057
|
+
asNullableString(sender.id),
|
|
2058
|
+
asNullableString(sender.open_id),
|
|
2059
|
+
asNullableString(sender.openId),
|
|
2060
|
+
asNullableString(sender.user_id),
|
|
2061
|
+
asNullableString(sender.userId),
|
|
2062
|
+
asNullableString(sender.sender_id),
|
|
2063
|
+
asNullableString(sender.senderId),
|
|
2064
|
+
asNullableString(sender.from_id),
|
|
2065
|
+
asNullableString(sender.fromId),
|
|
2066
|
+
asNullableString(sender.author_id),
|
|
2067
|
+
asNullableString(sender.authorId),
|
|
2068
|
+
asNullableString(sender.chat_id),
|
|
2069
|
+
asNullableString(sender.chatId),
|
|
2070
|
+
asNullableString(sender.participant),
|
|
2071
|
+
asNullableString(sender.participant_id),
|
|
2072
|
+
asNullableString(sender.participantId),
|
|
2073
|
+
asNullableString(sender.jid),
|
|
2074
|
+
asNullableString(sender.handle),
|
|
2075
|
+
asNullableString(sender.username),
|
|
2076
|
+
asNullableString(sender.phone),
|
|
2077
|
+
asNullableString(sender.phone_number),
|
|
2078
|
+
asNullableString(findNestedValue(sender, ["from", "id"])),
|
|
2079
|
+
asNullableString(findNestedValue(sender, ["author", "id"])),
|
|
2080
|
+
asNullableString(findNestedValue(message, ["from", "id"])),
|
|
2081
|
+
asNullableString(findNestedValue(message, ["author", "id"])),
|
|
2082
|
+
asNullableString(findNestedValue(message, ["user", "id"])),
|
|
2083
|
+
asNullableString(senderInfo?.id),
|
|
2084
|
+
asNullableString(senderInfo?.user_id),
|
|
2085
|
+
asNullableString(senderInfo?.sender_id),
|
|
2086
|
+
asNullableString(conversationInfo?.sender_id),
|
|
2087
|
+
asNullableString(conversationInfo?.user_id),
|
|
2088
|
+
asNullableString(conversationInfo?.from_id),
|
|
2089
|
+
senderIdFromText
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
2092
|
+
function extractSenderIdFromMetadataText(metadataText) {
|
|
2093
|
+
if (!metadataText) {
|
|
2094
|
+
return null;
|
|
2095
|
+
}
|
|
2096
|
+
return firstNonEmptyString(
|
|
2097
|
+
extractRegexValue(metadataText, /"sender_id"\s*:\s*"([^"]+)"/),
|
|
2098
|
+
extractRegexValue(metadataText, /"user_id"\s*:\s*"([^"]+)"/),
|
|
2099
|
+
extractRegexValue(metadataText, /"from_id"\s*:\s*"([^"]+)"/),
|
|
2100
|
+
extractRegexValue(metadataText, /"author_id"\s*:\s*"([^"]+)"/),
|
|
2101
|
+
extractRegexValue(metadataText, /"chat_id"\s*:\s*"([^"]+)"/),
|
|
2102
|
+
extractRegexValue(metadataText, /"participant"\s*:\s*"([^"]+)"/),
|
|
2103
|
+
extractRegexValue(metadataText, /"jid"\s*:\s*"([^"]+)"/),
|
|
2104
|
+
extractRegexValue(metadataText, /"id"\s*:\s*"((?:ou|oc)_[^"]+)"/)
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
function firstNonEmptyString(...values) {
|
|
2108
|
+
for (const value of values) {
|
|
2109
|
+
if (typeof value === "string" && value.trim()) {
|
|
2110
|
+
return value.trim();
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
1227
2113
|
return null;
|
|
1228
2114
|
}
|
|
1229
2115
|
function extractRegexValue(text, pattern) {
|
|
@@ -1234,8 +2120,24 @@ function extractOpenIdFromSessionId(sessionId) {
|
|
|
1234
2120
|
if (!sessionId) {
|
|
1235
2121
|
return null;
|
|
1236
2122
|
}
|
|
1237
|
-
const
|
|
1238
|
-
|
|
2123
|
+
for (const channel of ["feishu", "telegram", "whatsapp", "discord", "googlechat", "slack", "mattermost", "signal", "imessage", "msteams"]) {
|
|
2124
|
+
const marker = `:${channel}:`;
|
|
2125
|
+
const channelIndex = sessionId.indexOf(marker);
|
|
2126
|
+
if (channelIndex < 0) {
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
const remainder = sessionId.slice(channelIndex + marker.length);
|
|
2130
|
+
const modeSeparator = remainder.indexOf(":");
|
|
2131
|
+
if (modeSeparator < 0) {
|
|
2132
|
+
continue;
|
|
2133
|
+
}
|
|
2134
|
+
const senderId = remainder.slice(modeSeparator + 1).trim();
|
|
2135
|
+
if (senderId) {
|
|
2136
|
+
return senderId;
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
const match = sessionId.match(/:([^:]+)$/);
|
|
2140
|
+
return match?.[1]?.trim() || null;
|
|
1239
2141
|
}
|
|
1240
2142
|
function getAgentIdFromContext(context) {
|
|
1241
2143
|
const record = context && typeof context === "object" ? context : {};
|
|
@@ -1251,6 +2153,17 @@ function sanitizeProfilePatch(params) {
|
|
|
1251
2153
|
notes: asNullableString(params.notes)
|
|
1252
2154
|
};
|
|
1253
2155
|
}
|
|
2156
|
+
function extractProfilePatchOperations(params) {
|
|
2157
|
+
const operations = {};
|
|
2158
|
+
const fields = ["nickname", "preferences", "personality", "role", "timezone", "notes"];
|
|
2159
|
+
for (const field of fields) {
|
|
2160
|
+
const raw = asNullableString(params[`${field}Operation`]) ?? asNullableString(params[`${field}_operation`]);
|
|
2161
|
+
if (raw === "replace" || raw === "append" || raw === "remove") {
|
|
2162
|
+
operations[field] = raw;
|
|
2163
|
+
}
|
|
2164
|
+
}
|
|
2165
|
+
return Object.keys(operations).length > 0 ? operations : void 0;
|
|
2166
|
+
}
|
|
1254
2167
|
function parseAdminInstruction(instruction) {
|
|
1255
2168
|
const normalized = instruction.trim();
|
|
1256
2169
|
if (/issue|问题|失败/i.test(normalized)) {
|
|
@@ -1332,44 +2245,102 @@ function renderIdentityContext(identity) {
|
|
|
1332
2245
|
return lines.join("\n");
|
|
1333
2246
|
}
|
|
1334
2247
|
function renderProfileMarkdown(profile) {
|
|
1335
|
-
const
|
|
2248
|
+
const notes = sanitizeProfileNotes(profile.notes) ?? defaultProfileNotes();
|
|
2249
|
+
const frontmatterLines = [
|
|
1336
2250
|
"---",
|
|
1337
|
-
`userId: ${escapeFrontmatter(profile.userId)}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
`
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
`
|
|
2251
|
+
`userId: ${escapeFrontmatter(profile.userId)}`
|
|
2252
|
+
];
|
|
2253
|
+
if (profile.name) {
|
|
2254
|
+
frontmatterLines.push(`name: ${escapeFrontmatter(profile.name)}`);
|
|
2255
|
+
}
|
|
2256
|
+
if (profile.nickname) {
|
|
2257
|
+
frontmatterLines.push(`nickname: ${escapeFrontmatter(profile.nickname)}`);
|
|
2258
|
+
}
|
|
2259
|
+
if (profile.timezone) {
|
|
2260
|
+
frontmatterLines.push(`timezone: ${escapeFrontmatter(profile.timezone)}`);
|
|
2261
|
+
}
|
|
2262
|
+
if (profile.preferences) {
|
|
2263
|
+
frontmatterLines.push(`preferences: ${escapeFrontmatter(profile.preferences)}`);
|
|
2264
|
+
}
|
|
2265
|
+
if (profile.personality) {
|
|
2266
|
+
frontmatterLines.push(`personality: ${escapeFrontmatter(profile.personality)}`);
|
|
2267
|
+
}
|
|
2268
|
+
if (profile.role) {
|
|
2269
|
+
frontmatterLines.push(`role: ${escapeFrontmatter(profile.role)}`);
|
|
2270
|
+
}
|
|
2271
|
+
frontmatterLines.push(
|
|
1344
2272
|
`visibility: ${escapeFrontmatter(profile.visibility)}`,
|
|
1345
2273
|
`source: ${escapeFrontmatter(profile.source)}`,
|
|
1346
2274
|
`updatedAt: ${escapeFrontmatter(profile.updatedAt)}`,
|
|
2275
|
+
`syncHash: ${escapeFrontmatter(computeProfilePayloadHash({
|
|
2276
|
+
name: profile.name,
|
|
2277
|
+
nickname: profile.nickname,
|
|
2278
|
+
timezone: profile.timezone,
|
|
2279
|
+
preferences: profile.preferences,
|
|
2280
|
+
personality: profile.personality,
|
|
2281
|
+
role: profile.role,
|
|
2282
|
+
visibility: profile.visibility
|
|
2283
|
+
}, notes))}`,
|
|
1347
2284
|
"---"
|
|
1348
|
-
|
|
1349
|
-
const
|
|
2285
|
+
);
|
|
2286
|
+
const frontmatter = frontmatterLines.join("\n");
|
|
2287
|
+
const confirmedProfileLines = renderConfirmedProfileSection(profile);
|
|
1350
2288
|
return `${frontmatter}
|
|
1351
2289
|
|
|
1352
2290
|
# \u7528\u6237\u753B\u50CF
|
|
1353
2291
|
|
|
1354
|
-
\u8FD9\u4E2A\u6587\u4EF6\u662F\
|
|
2292
|
+
\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
2293
|
|
|
1356
|
-
## \
|
|
2294
|
+
## \u5DF2\u786E\u8BA4\u753B\u50CF
|
|
1357
2295
|
|
|
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
|
|
2296
|
+
${confirmedProfileLines}
|
|
1363
2297
|
|
|
1364
|
-
## \u5907\u6CE8
|
|
2298
|
+
## \u8865\u5145\u5907\u6CE8
|
|
1365
2299
|
|
|
1366
2300
|
${notes}
|
|
1367
2301
|
`;
|
|
1368
2302
|
}
|
|
2303
|
+
function renderConfirmedProfileSection(profile) {
|
|
2304
|
+
const rows = [];
|
|
2305
|
+
if (profile.name) {
|
|
2306
|
+
rows.push(`- \u59D3\u540D\uFF1A${profile.name}`);
|
|
2307
|
+
}
|
|
2308
|
+
if (profile.nickname) {
|
|
2309
|
+
rows.push(`- \u79F0\u547C\uFF1A${profile.nickname}`);
|
|
2310
|
+
}
|
|
2311
|
+
if (profile.preferences) {
|
|
2312
|
+
rows.push(`- \u56DE\u7B54\u504F\u597D\uFF1A${profile.preferences}`);
|
|
2313
|
+
}
|
|
2314
|
+
if (profile.personality) {
|
|
2315
|
+
rows.push(`- \u98CE\u683C\u504F\u597D\uFF1A${profile.personality}`);
|
|
2316
|
+
}
|
|
2317
|
+
if (profile.role) {
|
|
2318
|
+
rows.push(`- \u89D2\u8272\u8EAB\u4EFD\uFF1A${profile.role}`);
|
|
2319
|
+
}
|
|
2320
|
+
if (profile.timezone) {
|
|
2321
|
+
rows.push(`- \u65F6\u533A\uFF1A${profile.timezone}`);
|
|
2322
|
+
}
|
|
2323
|
+
return rows.length > 0 ? rows.join("\n") : "- \u6682\u65E0\u5DF2\u786E\u8BA4\u7684\u7ED3\u6784\u5316\u753B\u50CF\u5B57\u6BB5";
|
|
2324
|
+
}
|
|
2325
|
+
function computeProfilePayloadHash(patch, notes) {
|
|
2326
|
+
return hashId(JSON.stringify({
|
|
2327
|
+
name: patch.name ?? null,
|
|
2328
|
+
nickname: patch.nickname ?? null,
|
|
2329
|
+
timezone: patch.timezone ?? null,
|
|
2330
|
+
preferences: patch.preferences ?? null,
|
|
2331
|
+
personality: patch.personality ?? null,
|
|
2332
|
+
role: patch.role ?? null,
|
|
2333
|
+
visibility: patch.visibility ?? "private",
|
|
2334
|
+
notes: sanitizeProfileNotes(notes) ?? null
|
|
2335
|
+
}));
|
|
2336
|
+
}
|
|
1369
2337
|
function parseProfileMarkdown(markdown) {
|
|
1370
2338
|
const lines = markdown.split(/\r?\n/);
|
|
1371
2339
|
const patch = {};
|
|
1372
2340
|
let notes = null;
|
|
2341
|
+
let updatedAt = null;
|
|
2342
|
+
let source = null;
|
|
2343
|
+
let syncHash = null;
|
|
1373
2344
|
let index = 0;
|
|
1374
2345
|
if (lines[index] === "---") {
|
|
1375
2346
|
index += 1;
|
|
@@ -1379,7 +2350,15 @@ function parseProfileMarkdown(markdown) {
|
|
|
1379
2350
|
if (separatorIndex > 0) {
|
|
1380
2351
|
const key = line.slice(0, separatorIndex).trim();
|
|
1381
2352
|
const value = line.slice(separatorIndex + 1).trim();
|
|
1382
|
-
|
|
2353
|
+
if (key === "updatedAt") {
|
|
2354
|
+
updatedAt = value === "null" ? null : value;
|
|
2355
|
+
} else if (key === "source") {
|
|
2356
|
+
source = value === "null" ? null : value;
|
|
2357
|
+
} else if (key === "syncHash") {
|
|
2358
|
+
syncHash = value === "null" ? null : value;
|
|
2359
|
+
} else {
|
|
2360
|
+
applyFrontmatterField(patch, key, value);
|
|
2361
|
+
}
|
|
1383
2362
|
}
|
|
1384
2363
|
index += 1;
|
|
1385
2364
|
}
|
|
@@ -1388,13 +2367,16 @@ function parseProfileMarkdown(markdown) {
|
|
|
1388
2367
|
}
|
|
1389
2368
|
}
|
|
1390
2369
|
const body = lines.slice(index).join("\n");
|
|
1391
|
-
const notesMatch = body.match(/##\s
|
|
2370
|
+
const notesMatch = body.match(/##\s*(?:补充备注|备注)\s*\n([\s\S]*)$/);
|
|
1392
2371
|
if (notesMatch?.[1]) {
|
|
1393
2372
|
notes = sanitizeProfileNotes(notesMatch[1]);
|
|
1394
2373
|
}
|
|
1395
2374
|
return {
|
|
1396
2375
|
profilePatch: patch,
|
|
1397
|
-
notes
|
|
2376
|
+
notes,
|
|
2377
|
+
updatedAt,
|
|
2378
|
+
source,
|
|
2379
|
+
syncHash
|
|
1398
2380
|
};
|
|
1399
2381
|
}
|
|
1400
2382
|
function applyFrontmatterField(patch, key, value) {
|
|
@@ -1415,10 +2397,6 @@ function applyFrontmatterField(patch, key, value) {
|
|
|
1415
2397
|
patch.visibility = normalized === "shared" ? "shared" : "private";
|
|
1416
2398
|
}
|
|
1417
2399
|
}
|
|
1418
|
-
function hasProfileDifference(current, patch) {
|
|
1419
|
-
const entries = Object.entries(patch);
|
|
1420
|
-
return entries.some(([key, value]) => value != null && current[key] !== value);
|
|
1421
|
-
}
|
|
1422
2400
|
function renderYamlList(rows) {
|
|
1423
2401
|
if (rows.length === 0) {
|
|
1424
2402
|
return "[]\n";
|
|
@@ -1463,6 +2441,273 @@ function asTextResult(value) {
|
|
|
1463
2441
|
function asNullableString(value) {
|
|
1464
2442
|
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
1465
2443
|
}
|
|
2444
|
+
async function inferSemanticProfileExtraction(messageText, currentProfile) {
|
|
2445
|
+
const model = readProfileExtractorModelFromOpenClawConfig();
|
|
2446
|
+
if (!model) {
|
|
2447
|
+
return null;
|
|
2448
|
+
}
|
|
2449
|
+
const response = await fetch(`${model.baseUrl}/chat/completions`, {
|
|
2450
|
+
method: "POST",
|
|
2451
|
+
headers: {
|
|
2452
|
+
authorization: `Bearer ${model.apiKey}`,
|
|
2453
|
+
"content-type": "application/json; charset=utf-8"
|
|
2454
|
+
},
|
|
2455
|
+
body: JSON.stringify({
|
|
2456
|
+
model: model.modelId,
|
|
2457
|
+
temperature: 0,
|
|
2458
|
+
max_tokens: 400,
|
|
2459
|
+
response_format: { type: "json_object" },
|
|
2460
|
+
messages: [
|
|
2461
|
+
{
|
|
2462
|
+
role: "system",
|
|
2463
|
+
content: [
|
|
2464
|
+
"You extract stable user-profile information from a single user message.",
|
|
2465
|
+
"Return JSON only.",
|
|
2466
|
+
"Only capture durable, reusable traits or preferences that should affect future conversations.",
|
|
2467
|
+
"Ignore transient task requirements, one-off requests, or speculative guesses.",
|
|
2468
|
+
"Prefer precision, but do not miss clear self-descriptions or clear addressing / style preferences.",
|
|
2469
|
+
"Allowed fields: nickname, preferences, personality, role, timezone, notes.",
|
|
2470
|
+
"For each field, also decide the update operation: replace, append, or remove.",
|
|
2471
|
+
"notes is only for durable boundaries or habits that do not fit the structured fields.",
|
|
2472
|
+
"A preferred form of address counts as nickname even when phrased naturally, for example: '\u4EE5\u540E\u53EB\u6211\u4E30\u54E5', '\u53EB\u6211\u4EB2\u7231\u7684\u8001\u5927', '\u4F60\u5C31\u53EB\u6211\u963F\u4E30'.",
|
|
2473
|
+
"A reusable response preference counts as preferences, for example: '\u5148\u7ED9\u7ED3\u8BBA\u518D\u5C55\u5F00', '\u8BF4\u8BDD\u76F4\u63A5\u4E00\u70B9\u4F46\u522B\u592A\u786C', '\u522B\u592A\u5B98\u65B9'.",
|
|
2474
|
+
"Use append when the user adds another stable preference without revoking the old one.",
|
|
2475
|
+
"Use replace when the user corrects or changes an existing stable preference.",
|
|
2476
|
+
"Use remove when the user clearly asks to drop a specific old preference or trait.",
|
|
2477
|
+
"If a message clearly contains both an addressing preference and a communication preference, capture both.",
|
|
2478
|
+
"Do not require rigid trigger phrases. Judge by meaning.",
|
|
2479
|
+
"Do not copy placeholders, examples, or template language.",
|
|
2480
|
+
'Return exactly this shape: {"should_update":boolean,"confidence":number,"patch":{"nickname":string?,"preferences":string?,"personality":string?,"role":string?,"timezone":string?,"notes":string?},"operations":{"nickname":"replace|append|remove"?,"preferences":"replace|append|remove"?,"personality":"replace|append|remove"?,"role":"replace|append|remove"?,"timezone":"replace|append|remove"?,"notes":"replace|append|remove"?}}'
|
|
2481
|
+
].join("\n")
|
|
2482
|
+
},
|
|
2483
|
+
{
|
|
2484
|
+
role: "user",
|
|
2485
|
+
content: JSON.stringify({
|
|
2486
|
+
current_profile: {
|
|
2487
|
+
nickname: currentProfile.nickname,
|
|
2488
|
+
preferences: currentProfile.preferences,
|
|
2489
|
+
personality: currentProfile.personality,
|
|
2490
|
+
role: currentProfile.role,
|
|
2491
|
+
timezone: currentProfile.timezone,
|
|
2492
|
+
notes: currentProfile.notes
|
|
2493
|
+
},
|
|
2494
|
+
latest_user_message: messageText
|
|
2495
|
+
})
|
|
2496
|
+
}
|
|
2497
|
+
]
|
|
2498
|
+
})
|
|
2499
|
+
});
|
|
2500
|
+
const payload = await response.json();
|
|
2501
|
+
if (!response.ok) {
|
|
2502
|
+
throw new Error(`semantic profile extractor request failed: ${JSON.stringify(payload)}`);
|
|
2503
|
+
}
|
|
2504
|
+
const content = extractOpenAiMessageContent(payload);
|
|
2505
|
+
const parsed = parseSemanticExtractionResult(content);
|
|
2506
|
+
if (!parsed) {
|
|
2507
|
+
throw new Error(`semantic profile extractor returned unreadable content: ${content}`);
|
|
2508
|
+
}
|
|
2509
|
+
return parsed;
|
|
2510
|
+
}
|
|
2511
|
+
function extractOpenAiMessageContent(payload) {
|
|
2512
|
+
const choices = Array.isArray(payload.choices) ? payload.choices : [];
|
|
2513
|
+
const first = choices[0];
|
|
2514
|
+
if (!first || typeof first !== "object") {
|
|
2515
|
+
return "";
|
|
2516
|
+
}
|
|
2517
|
+
const message = first.message;
|
|
2518
|
+
if (!message || typeof message !== "object") {
|
|
2519
|
+
return "";
|
|
2520
|
+
}
|
|
2521
|
+
const content = message.content;
|
|
2522
|
+
if (typeof content === "string") {
|
|
2523
|
+
return content.trim();
|
|
2524
|
+
}
|
|
2525
|
+
if (!Array.isArray(content)) {
|
|
2526
|
+
return "";
|
|
2527
|
+
}
|
|
2528
|
+
return content.map((item) => {
|
|
2529
|
+
if (!item || typeof item !== "object") {
|
|
2530
|
+
return "";
|
|
2531
|
+
}
|
|
2532
|
+
const record = item;
|
|
2533
|
+
return asNullableString(record.text) ?? "";
|
|
2534
|
+
}).filter(Boolean).join("\n").trim();
|
|
2535
|
+
}
|
|
2536
|
+
function parseSemanticExtractionResult(content) {
|
|
2537
|
+
const jsonText = extractJsonObject(content);
|
|
2538
|
+
if (!jsonText) {
|
|
2539
|
+
return null;
|
|
2540
|
+
}
|
|
2541
|
+
try {
|
|
2542
|
+
const parsed = JSON.parse(jsonText);
|
|
2543
|
+
const patchInput = parsed.patch && typeof parsed.patch === "object" ? parsed.patch : {};
|
|
2544
|
+
const operationsInput = parsed.operations && typeof parsed.operations === "object" ? parsed.operations : {};
|
|
2545
|
+
return {
|
|
2546
|
+
shouldUpdate: parsed.should_update === true || parsed.shouldUpdate === true,
|
|
2547
|
+
confidence: Number(parsed.confidence ?? 0),
|
|
2548
|
+
patch: {
|
|
2549
|
+
nickname: asNullableString(patchInput.nickname),
|
|
2550
|
+
preferences: asNullableString(patchInput.preferences),
|
|
2551
|
+
personality: asNullableString(patchInput.personality),
|
|
2552
|
+
role: asNullableString(patchInput.role),
|
|
2553
|
+
timezone: asNullableString(patchInput.timezone),
|
|
2554
|
+
notes: asNullableString(patchInput.notes)
|
|
2555
|
+
},
|
|
2556
|
+
operations: parseProfilePatchOperations(operationsInput)
|
|
2557
|
+
};
|
|
2558
|
+
} catch {
|
|
2559
|
+
return null;
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
function parseProfilePatchOperations(input) {
|
|
2563
|
+
const operations = {};
|
|
2564
|
+
for (const field of ["nickname", "preferences", "personality", "role", "timezone", "notes"]) {
|
|
2565
|
+
const raw = asNullableString(input[field]);
|
|
2566
|
+
if (raw === "replace" || raw === "append" || raw === "remove") {
|
|
2567
|
+
operations[field] = raw;
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
return Object.keys(operations).length > 0 ? operations : void 0;
|
|
2571
|
+
}
|
|
2572
|
+
function extractJsonObject(content) {
|
|
2573
|
+
const trimmed = content.trim();
|
|
2574
|
+
if (!trimmed) {
|
|
2575
|
+
return null;
|
|
2576
|
+
}
|
|
2577
|
+
const fenced = trimmed.match(/```(?:json)?\s*([\s\S]*?)\s*```/i);
|
|
2578
|
+
if (fenced?.[1]) {
|
|
2579
|
+
return fenced[1].trim();
|
|
2580
|
+
}
|
|
2581
|
+
const start = trimmed.indexOf("{");
|
|
2582
|
+
const end = trimmed.lastIndexOf("}");
|
|
2583
|
+
if (start >= 0 && end > start) {
|
|
2584
|
+
return trimmed.slice(start, end + 1);
|
|
2585
|
+
}
|
|
2586
|
+
return null;
|
|
2587
|
+
}
|
|
2588
|
+
function cleanupSemanticProfilePatch(patch, currentProfile, operations) {
|
|
2589
|
+
const next = {};
|
|
2590
|
+
const nextOperations = {};
|
|
2591
|
+
const nickname = asNullableString(patch.nickname);
|
|
2592
|
+
const preferences = asNullableString(patch.preferences);
|
|
2593
|
+
const personality = asNullableString(patch.personality);
|
|
2594
|
+
const role = asNullableString(patch.role);
|
|
2595
|
+
const timezone = asNullableString(patch.timezone);
|
|
2596
|
+
const notes = asNullableString(patch.notes);
|
|
2597
|
+
if (nickname && nickname !== currentProfile.nickname) {
|
|
2598
|
+
next.nickname = nickname;
|
|
2599
|
+
if (operations?.nickname) {
|
|
2600
|
+
nextOperations.nickname = operations.nickname;
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
if (preferences && preferences !== currentProfile.preferences) {
|
|
2604
|
+
next.preferences = preferences;
|
|
2605
|
+
if (operations?.preferences) {
|
|
2606
|
+
nextOperations.preferences = operations.preferences;
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
if (personality && personality !== currentProfile.personality) {
|
|
2610
|
+
next.personality = personality;
|
|
2611
|
+
if (operations?.personality) {
|
|
2612
|
+
nextOperations.personality = operations.personality;
|
|
2613
|
+
}
|
|
2614
|
+
}
|
|
2615
|
+
if (role && role !== currentProfile.role) {
|
|
2616
|
+
next.role = role;
|
|
2617
|
+
if (operations?.role) {
|
|
2618
|
+
nextOperations.role = operations.role;
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
if (timezone && timezone !== currentProfile.timezone) {
|
|
2622
|
+
next.timezone = timezone;
|
|
2623
|
+
if (operations?.timezone) {
|
|
2624
|
+
nextOperations.timezone = operations.timezone;
|
|
2625
|
+
}
|
|
2626
|
+
}
|
|
2627
|
+
if (notes) {
|
|
2628
|
+
next.notes = notes;
|
|
2629
|
+
if (operations?.notes) {
|
|
2630
|
+
nextOperations.notes = operations.notes;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return {
|
|
2634
|
+
patch: next,
|
|
2635
|
+
operations: Object.keys(nextOperations).length > 0 ? nextOperations : void 0
|
|
2636
|
+
};
|
|
2637
|
+
}
|
|
2638
|
+
function applyProfilePatchOperations(currentProfile, patch, operations) {
|
|
2639
|
+
const next = {};
|
|
2640
|
+
const fields = ["nickname", "preferences", "personality", "role", "timezone", "notes"];
|
|
2641
|
+
for (const field of fields) {
|
|
2642
|
+
if (patch[field] === void 0) {
|
|
2643
|
+
continue;
|
|
2644
|
+
}
|
|
2645
|
+
const currentValue = currentProfile[field];
|
|
2646
|
+
const incomingValue = asNullableString(patch[field]);
|
|
2647
|
+
const operation = operations?.[field] ?? defaultProfileFieldOperation(field);
|
|
2648
|
+
const resolved = resolveProfileFieldUpdate(field, currentValue, incomingValue, operation);
|
|
2649
|
+
if (resolved !== void 0) {
|
|
2650
|
+
next[field] = resolved;
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
return next;
|
|
2654
|
+
}
|
|
2655
|
+
function defaultProfileFieldOperation(field) {
|
|
2656
|
+
if (field === "notes") {
|
|
2657
|
+
return "append";
|
|
2658
|
+
}
|
|
2659
|
+
return "replace";
|
|
2660
|
+
}
|
|
2661
|
+
function resolveProfileFieldUpdate(field, currentValue, incomingValue, operation) {
|
|
2662
|
+
if (operation === "remove") {
|
|
2663
|
+
if (!currentValue) {
|
|
2664
|
+
return void 0;
|
|
2665
|
+
}
|
|
2666
|
+
if (!incomingValue) {
|
|
2667
|
+
return null;
|
|
2668
|
+
}
|
|
2669
|
+
const removed = removeProfileFeatureValue(currentValue, incomingValue);
|
|
2670
|
+
return removed === currentValue ? void 0 : removed;
|
|
2671
|
+
}
|
|
2672
|
+
if (!incomingValue) {
|
|
2673
|
+
return void 0;
|
|
2674
|
+
}
|
|
2675
|
+
if (operation === "append") {
|
|
2676
|
+
if (field === "notes") {
|
|
2677
|
+
const mergedNotes = joinNotes(currentValue, incomingValue);
|
|
2678
|
+
return mergedNotes === currentValue ? void 0 : mergedNotes;
|
|
2679
|
+
}
|
|
2680
|
+
const appended = appendProfileFeatureValue(currentValue, incomingValue);
|
|
2681
|
+
return appended === currentValue ? void 0 : appended;
|
|
2682
|
+
}
|
|
2683
|
+
return incomingValue === currentValue ? void 0 : incomingValue;
|
|
2684
|
+
}
|
|
2685
|
+
function appendProfileFeatureValue(currentValue, incomingValue) {
|
|
2686
|
+
if (!currentValue) {
|
|
2687
|
+
return incomingValue;
|
|
2688
|
+
}
|
|
2689
|
+
const currentItems = splitProfileFeatureValues(currentValue);
|
|
2690
|
+
const incomingItems = splitProfileFeatureValues(incomingValue);
|
|
2691
|
+
const merged = [...currentItems];
|
|
2692
|
+
for (const item of incomingItems) {
|
|
2693
|
+
if (!merged.includes(item)) {
|
|
2694
|
+
merged.push(item);
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
return merged.join("\uFF1B");
|
|
2698
|
+
}
|
|
2699
|
+
function removeProfileFeatureValue(currentValue, incomingValue) {
|
|
2700
|
+
const currentItems = splitProfileFeatureValues(currentValue);
|
|
2701
|
+
const removals = new Set(splitProfileFeatureValues(incomingValue));
|
|
2702
|
+
const nextItems = currentItems.filter((item) => !removals.has(item));
|
|
2703
|
+
if (nextItems.length === currentItems.length) {
|
|
2704
|
+
return currentValue;
|
|
2705
|
+
}
|
|
2706
|
+
return nextItems.length > 0 ? nextItems.join("\uFF1B") : null;
|
|
2707
|
+
}
|
|
2708
|
+
function splitProfileFeatureValues(value) {
|
|
2709
|
+
return value.split(/[\n;;,,、]+/u).map((item) => item.trim()).filter(Boolean);
|
|
2710
|
+
}
|
|
1466
2711
|
function ensureObject(parent, key) {
|
|
1467
2712
|
const current = parent[key];
|
|
1468
2713
|
if (current && typeof current === "object" && !Array.isArray(current)) {
|
|
@@ -1561,6 +2806,46 @@ function readFeishuAccountsFromOpenClawConfig() {
|
|
|
1561
2806
|
return [];
|
|
1562
2807
|
}
|
|
1563
2808
|
}
|
|
2809
|
+
function readProfileExtractorModelFromOpenClawConfig() {
|
|
2810
|
+
const openclawConfigPath = (0, import_node_path.join)((0, import_node_os.homedir)(), ".openclaw", "openclaw.json");
|
|
2811
|
+
if (!(0, import_node_fs.existsSync)(openclawConfigPath)) {
|
|
2812
|
+
return null;
|
|
2813
|
+
}
|
|
2814
|
+
try {
|
|
2815
|
+
const parsed = JSON.parse((0, import_node_fs.readFileSync)(openclawConfigPath, "utf8"));
|
|
2816
|
+
const models = parsed.models && typeof parsed.models === "object" ? parsed.models : {};
|
|
2817
|
+
const providers = models.providers && typeof models.providers === "object" ? models.providers : {};
|
|
2818
|
+
const agents = parsed.agents && typeof parsed.agents === "object" ? parsed.agents : {};
|
|
2819
|
+
const defaults = agents.defaults && typeof agents.defaults === "object" ? agents.defaults : {};
|
|
2820
|
+
const defaultModel = defaults.model && typeof defaults.model === "object" ? defaults.model : {};
|
|
2821
|
+
const configuredModel = asNullableString(defaultModel.primary) ?? asNullableString(models.primary);
|
|
2822
|
+
if (!configuredModel || !configuredModel.includes("/")) {
|
|
2823
|
+
return null;
|
|
2824
|
+
}
|
|
2825
|
+
const [providerId, modelId] = configuredModel.split("/", 2);
|
|
2826
|
+
const provider = providers[providerId] && typeof providers[providerId] === "object" ? providers[providerId] : null;
|
|
2827
|
+
if (!provider) {
|
|
2828
|
+
return null;
|
|
2829
|
+
}
|
|
2830
|
+
const api = asNullableString(provider.api);
|
|
2831
|
+
const baseUrl = asNullableString(provider.baseUrl) ?? asNullableString(provider.baseURL);
|
|
2832
|
+
const apiKey = asNullableString(provider.apiKey);
|
|
2833
|
+
if (api !== "openai-completions" || !baseUrl || !apiKey) {
|
|
2834
|
+
return null;
|
|
2835
|
+
}
|
|
2836
|
+
return {
|
|
2837
|
+
providerId,
|
|
2838
|
+
modelId,
|
|
2839
|
+
baseUrl: baseUrl.replace(/\/+$/, ""),
|
|
2840
|
+
apiKey
|
|
2841
|
+
};
|
|
2842
|
+
} catch (error) {
|
|
2843
|
+
logUserBindEvent("profile-extractor-config-read-failed", {
|
|
2844
|
+
message: error instanceof Error ? error.message : String(error)
|
|
2845
|
+
});
|
|
2846
|
+
return null;
|
|
2847
|
+
}
|
|
2848
|
+
}
|
|
1564
2849
|
function normalizeFeishuAccount(accountId, input, fallback) {
|
|
1565
2850
|
const record = input && typeof input === "object" ? input : {};
|
|
1566
2851
|
const enabled = record.enabled !== false && fallback.enabled !== false;
|
|
@@ -1602,12 +2887,107 @@ function getConfiguredAgentId(agent) {
|
|
|
1602
2887
|
}
|
|
1603
2888
|
function defaultProfileNotes() {
|
|
1604
2889
|
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"
|
|
2890
|
+
"- \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",
|
|
2891
|
+
`- \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`,
|
|
2892
|
+
"- \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
2893
|
].join("\n");
|
|
1610
2894
|
}
|
|
2895
|
+
function createEphemeralUserId(channelType, openId, sessionId) {
|
|
2896
|
+
return `temp_${hashId(`${channelType}:${openId ?? sessionId}`)}`;
|
|
2897
|
+
}
|
|
2898
|
+
function createProvisionalUserId(channelType, openId) {
|
|
2899
|
+
return `${channelType}:oid_${hashId(`${channelType}:${openId}`)}`;
|
|
2900
|
+
}
|
|
2901
|
+
function createStableLocalUserId(channelType, openId, sessionId) {
|
|
2902
|
+
return `${channelType}:ub_${hashId(`${channelType}:${openId ?? sessionId}`)}`;
|
|
2903
|
+
}
|
|
2904
|
+
function isProvisionalScopedUserId(userId) {
|
|
2905
|
+
return /:oid_[a-f0-9]+$/i.test(userId);
|
|
2906
|
+
}
|
|
2907
|
+
function getPendingBindingRetryDelayMs(reason, attempts) {
|
|
2908
|
+
if (reason === "feishu-contact-scope-missing") {
|
|
2909
|
+
return Math.min(30 * 60 * 1e3, Math.max(6e4, attempts * 5 * 6e4));
|
|
2910
|
+
}
|
|
2911
|
+
return Math.min(10 * 60 * 1e3, Math.max(15e3, attempts * 3e4));
|
|
2912
|
+
}
|
|
2913
|
+
function scopeUserId(channelType, userId) {
|
|
2914
|
+
if (!userId) {
|
|
2915
|
+
return userId;
|
|
2916
|
+
}
|
|
2917
|
+
if (channelType === "manual" || channelType === "local") {
|
|
2918
|
+
return userId;
|
|
2919
|
+
}
|
|
2920
|
+
if (userId.startsWith("temp_") || userId.startsWith("ub_")) {
|
|
2921
|
+
return userId;
|
|
2922
|
+
}
|
|
2923
|
+
if (extractChannelFromScopedUserId(userId)) {
|
|
2924
|
+
return userId;
|
|
2925
|
+
}
|
|
2926
|
+
return `${channelType}:${userId}`;
|
|
2927
|
+
}
|
|
2928
|
+
function getExternalUserId(channelType, userId) {
|
|
2929
|
+
if (!userId) {
|
|
2930
|
+
return userId;
|
|
2931
|
+
}
|
|
2932
|
+
if (channelType === "manual" || channelType === "local") {
|
|
2933
|
+
return userId;
|
|
2934
|
+
}
|
|
2935
|
+
if (userId.startsWith("temp_") || userId.startsWith("ub_")) {
|
|
2936
|
+
return userId;
|
|
2937
|
+
}
|
|
2938
|
+
const currentChannel = extractChannelFromScopedUserId(userId);
|
|
2939
|
+
if (currentChannel && userId.startsWith(`${currentChannel}:`)) {
|
|
2940
|
+
return userId.slice(currentChannel.length + 1);
|
|
2941
|
+
}
|
|
2942
|
+
return userId;
|
|
2943
|
+
}
|
|
2944
|
+
function extractChannelFromScopedUserId(userId) {
|
|
2945
|
+
const match = userId.match(/^([a-z][a-z0-9_-]*):(.+)$/i);
|
|
2946
|
+
if (!match) {
|
|
2947
|
+
return null;
|
|
2948
|
+
}
|
|
2949
|
+
return match[1].toLowerCase();
|
|
2950
|
+
}
|
|
2951
|
+
function buildLightweightProfile(args) {
|
|
2952
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2953
|
+
return {
|
|
2954
|
+
userId: args.userId,
|
|
2955
|
+
name: args.profilePatch.name ?? args.current?.name ?? null,
|
|
2956
|
+
gender: args.profilePatch.gender ?? args.current?.gender ?? null,
|
|
2957
|
+
email: args.profilePatch.email ?? args.current?.email ?? null,
|
|
2958
|
+
avatar: args.profilePatch.avatar ?? args.current?.avatar ?? null,
|
|
2959
|
+
nickname: args.profilePatch.nickname ?? args.current?.nickname ?? null,
|
|
2960
|
+
preferences: args.profilePatch.preferences ?? args.current?.preferences ?? null,
|
|
2961
|
+
personality: args.profilePatch.personality ?? args.current?.personality ?? null,
|
|
2962
|
+
role: args.profilePatch.role ?? args.current?.role ?? null,
|
|
2963
|
+
timezone: args.profilePatch.timezone ?? args.current?.timezone ?? getServerTimezone(),
|
|
2964
|
+
notes: args.profilePatch.notes ?? args.current?.notes ?? defaultProfileNotes(),
|
|
2965
|
+
visibility: args.profilePatch.visibility ?? args.current?.visibility ?? "private",
|
|
2966
|
+
source: args.source,
|
|
2967
|
+
updatedAt: now
|
|
2968
|
+
};
|
|
2969
|
+
}
|
|
2970
|
+
function looksLikeFeishuContactScopeError(message) {
|
|
2971
|
+
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");
|
|
2972
|
+
}
|
|
2973
|
+
function renderFeishuContactScopeGuidance(accountIds) {
|
|
2974
|
+
const scopeText = accountIds.length > 0 ? `\u6D89\u53CA\u8D26\u53F7\uFF1A${accountIds.join("\u3001")}\u3002` : "";
|
|
2975
|
+
return [
|
|
2976
|
+
"\u5F53\u524D\u65E0\u6CD5\u5B8C\u6574\u542F\u7528\u7528\u6237\u753B\u50CF\u3002",
|
|
2977
|
+
"\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",
|
|
2978
|
+
`${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`
|
|
2979
|
+
].join(" ");
|
|
2980
|
+
}
|
|
2981
|
+
function getServerTimezone() {
|
|
2982
|
+
try {
|
|
2983
|
+
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
2984
|
+
if (typeof timezone === "string" && timezone.trim()) {
|
|
2985
|
+
return timezone.trim();
|
|
2986
|
+
}
|
|
2987
|
+
} catch {
|
|
2988
|
+
}
|
|
2989
|
+
return "UTC";
|
|
2990
|
+
}
|
|
1611
2991
|
function sanitizeProfileNotes(notes) {
|
|
1612
2992
|
const value = typeof notes === "string" ? notes.trim() : "";
|
|
1613
2993
|
if (!value) {
|