@aalis/plugin-user-profile 0.12.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +9 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +266 -301
- package/dist/index.js.map +1 -1
- package/package.json +16 -14
package/dist/index.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { authority, DEFAULT_AUTHORITY } from '@aalis/api-authority';
|
|
2
2
|
import { commands } from '@aalis/api-commands';
|
|
3
|
+
import { contributions } from '@aalis/api-contributions';
|
|
4
|
+
import { hooks } from '@aalis/api-hooks';
|
|
3
5
|
import { llm, resolveLLMModel } from '@aalis/api-llm';
|
|
4
6
|
import { memory } from '@aalis/api-memory';
|
|
5
7
|
import { persona } from '@aalis/api-persona';
|
|
6
8
|
import { tools } from '@aalis/api-tools';
|
|
7
|
-
import {
|
|
9
|
+
import { userRelation } from '@aalis/api-user-relation';
|
|
10
|
+
import { config, definePlugin, events, logger, optional } from '@aalis/core';
|
|
8
11
|
import { WellKnownKinds } from '@aalis/schema-message';
|
|
9
12
|
import { parseLLMJsonObject } from '@aalis/util-json-repair';
|
|
10
13
|
// ════════════════════════════════════════════════════════════
|
|
@@ -209,15 +212,15 @@ const KNOWN_INSTRUCTION_CATEGORIES = [
|
|
|
209
212
|
'安全',
|
|
210
213
|
'其他',
|
|
211
214
|
];
|
|
212
|
-
/** 生成稳定短 ID(6 字符 base36,对 30
|
|
213
|
-
function
|
|
215
|
+
/** 生成稳定短 ID(6 字符 base36,对 30 条以内规模碰撞概率极低);前缀 'f' 为事实、'i' 为指令 */
|
|
216
|
+
function genShortId(prefix, existing) {
|
|
214
217
|
for (let i = 0; i < 8; i++) {
|
|
215
|
-
const id =
|
|
218
|
+
const id = `${prefix}${Math.random().toString(36).slice(2, 7)}`;
|
|
216
219
|
if (!existing.has(id))
|
|
217
220
|
return id;
|
|
218
221
|
}
|
|
219
222
|
// 极端兜底:加时间戳后缀
|
|
220
|
-
return
|
|
223
|
+
return `${prefix}${Date.now().toString(36).slice(-5)}`;
|
|
221
224
|
}
|
|
222
225
|
function decimalPlaces(value) {
|
|
223
226
|
if (!Number.isFinite(value))
|
|
@@ -300,8 +303,6 @@ function renderHistoryForExtract(history, userId, platform) {
|
|
|
300
303
|
.filter(Boolean)
|
|
301
304
|
.join('\n');
|
|
302
305
|
}
|
|
303
|
-
// 同名描述符即同一服务:本地声明 optional 依赖,类型来自兄弟插件的 type-only 导入,不构成运行时包依赖。
|
|
304
|
-
const userRelation = defineService('user-relation');
|
|
305
306
|
const uses = {
|
|
306
307
|
memory,
|
|
307
308
|
llm,
|
|
@@ -396,15 +397,6 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
396
397
|
function clipInstructionText(s) {
|
|
397
398
|
return s.length > cfg.maxInstructionCharsPerItem ? `${s.slice(0, cfg.maxInstructionCharsPerItem)}…` : s;
|
|
398
399
|
}
|
|
399
|
-
/** 生成 Instruction 短 ID(与 Fact 同前缀但带 'i' 区分,便于日志中识别) */
|
|
400
|
-
function genInstructionId(existing) {
|
|
401
|
-
for (let i = 0; i < 8; i++) {
|
|
402
|
-
const id = `i${Math.random().toString(36).slice(2, 7)}`;
|
|
403
|
-
if (!existing.has(id))
|
|
404
|
-
return id;
|
|
405
|
-
}
|
|
406
|
-
return `i${Date.now().toString(36).slice(-5)}`;
|
|
407
|
-
}
|
|
408
400
|
function parseInstructionArray(raw) {
|
|
409
401
|
const usedIds = new Set();
|
|
410
402
|
const list = [];
|
|
@@ -415,9 +407,9 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
415
407
|
const text = typeof obj.text === 'string' ? obj.text.trim() : '';
|
|
416
408
|
if (!text)
|
|
417
409
|
continue;
|
|
418
|
-
let id = typeof obj.id === 'string' && obj.id.trim() ? obj.id.trim() :
|
|
410
|
+
let id = typeof obj.id === 'string' && obj.id.trim() ? obj.id.trim() : genShortId('i', usedIds);
|
|
419
411
|
while (usedIds.has(id))
|
|
420
|
-
id =
|
|
412
|
+
id = genShortId('i', usedIds);
|
|
421
413
|
usedIds.add(id);
|
|
422
414
|
const updatedAt = typeof obj.updatedAt === 'number' ? obj.updatedAt : 0;
|
|
423
415
|
const observedAt = typeof obj.observedAt === 'number' ? obj.observedAt : updatedAt || undefined;
|
|
@@ -435,25 +427,32 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
435
427
|
}
|
|
436
428
|
return list;
|
|
437
429
|
}
|
|
438
|
-
|
|
439
|
-
|
|
430
|
+
/**
|
|
431
|
+
* 读取指令档案(不存在返回空档案)。读失败原样抛出,理由同 {@link loadProfile};
|
|
432
|
+
* 只读渲染走 {@link loadInstructionsLenient}。
|
|
433
|
+
* mem 由调用方传入:一次操作只认开头取到的实例,理由见 {@link loadProfile}
|
|
434
|
+
*/
|
|
435
|
+
async function loadInstructions(mem) {
|
|
440
436
|
const empty = { instructions: [], updatedAt: 0 };
|
|
441
437
|
if (!mem)
|
|
442
438
|
return empty;
|
|
439
|
+
const doc = await mem.getMetadata(INSTRUCTIONS_NS, getInstructionsKey());
|
|
440
|
+
if (!doc)
|
|
441
|
+
return empty;
|
|
442
|
+
const instructions = Array.isArray(doc.instructions) ? parseInstructionArray(doc.instructions) : [];
|
|
443
|
+
return { instructions, updatedAt: typeof doc.updatedAt === 'number' ? doc.updatedAt : 0 };
|
|
444
|
+
}
|
|
445
|
+
/** 只读渲染用:读失败按空档案处理,只记 debug(结果不会被写回) */
|
|
446
|
+
async function loadInstructionsLenient(mem) {
|
|
443
447
|
try {
|
|
444
|
-
|
|
445
|
-
if (!doc)
|
|
446
|
-
return empty;
|
|
447
|
-
const instructions = Array.isArray(doc.instructions) ? parseInstructionArray(doc.instructions) : [];
|
|
448
|
-
return { instructions, updatedAt: typeof doc.updatedAt === 'number' ? doc.updatedAt : 0 };
|
|
448
|
+
return await loadInstructions(mem);
|
|
449
449
|
}
|
|
450
450
|
catch (err) {
|
|
451
451
|
logger.debug(`加载指令档案失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
452
|
-
return
|
|
452
|
+
return { instructions: [], updatedAt: 0 };
|
|
453
453
|
}
|
|
454
454
|
}
|
|
455
|
-
async function saveInstructions(doc) {
|
|
456
|
-
const mem = memory.current;
|
|
455
|
+
async function saveInstructions(mem, doc) {
|
|
457
456
|
if (!mem)
|
|
458
457
|
return;
|
|
459
458
|
await mem.saveMetadata(INSTRUCTIONS_NS, getInstructionsKey(), {
|
|
@@ -488,7 +487,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
488
487
|
const text = clipInstructionText(a.text);
|
|
489
488
|
if (textSet.has(text))
|
|
490
489
|
continue;
|
|
491
|
-
const id =
|
|
490
|
+
const id = genShortId('i', usedIds);
|
|
492
491
|
usedIds.add(id);
|
|
493
492
|
byId.set(id, {
|
|
494
493
|
id,
|
|
@@ -551,60 +550,72 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
551
550
|
const s = v.trim();
|
|
552
551
|
return s.length > 0 ? s.slice(0, 40) : undefined;
|
|
553
552
|
}
|
|
554
|
-
/** 解析 metadata
|
|
553
|
+
/** 解析 metadata 中的事实数组;缺正文、id、时效或时间戳的条目跳过 */
|
|
555
554
|
function parseFactArray(raw) {
|
|
556
555
|
const usedIds = new Set();
|
|
557
556
|
const facts = [];
|
|
558
557
|
for (const item of raw) {
|
|
559
|
-
if (typeof item
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
while (usedIds.has(id))
|
|
571
|
-
id = genFactId(usedIds);
|
|
572
|
-
usedIds.add(id);
|
|
573
|
-
const cat = typeof obj.category === 'string' && KNOWN_CATEGORIES.includes(obj.category)
|
|
574
|
-
? obj.category
|
|
575
|
-
: undefined;
|
|
576
|
-
const updatedAt = typeof obj.updatedAt === 'number' ? obj.updatedAt : 0;
|
|
577
|
-
const observedAt = typeof obj.observedAt === 'number' ? obj.observedAt : updatedAt || undefined;
|
|
578
|
-
facts.push({
|
|
579
|
-
id,
|
|
580
|
-
text,
|
|
581
|
-
category: cat,
|
|
582
|
-
temporality: normalizeTemporality(obj.temporality, cat),
|
|
583
|
-
observedAt,
|
|
584
|
-
timeHint: normalizeTextField(obj.timeHint),
|
|
585
|
-
updatedAt,
|
|
586
|
-
});
|
|
558
|
+
if (!item || typeof item !== 'object')
|
|
559
|
+
continue;
|
|
560
|
+
const obj = item;
|
|
561
|
+
const text = typeof obj.text === 'string' ? obj.text.trim() : '';
|
|
562
|
+
if (!text ||
|
|
563
|
+
typeof obj.id !== 'string' ||
|
|
564
|
+
!obj.id.trim() ||
|
|
565
|
+
(obj.temporality !== 'permanent' && obj.temporality !== 'temporary') ||
|
|
566
|
+
typeof obj.observedAt !== 'number' ||
|
|
567
|
+
typeof obj.updatedAt !== 'number') {
|
|
568
|
+
continue;
|
|
587
569
|
}
|
|
570
|
+
let id = obj.id.trim();
|
|
571
|
+
while (usedIds.has(id))
|
|
572
|
+
id = genShortId('f', usedIds);
|
|
573
|
+
usedIds.add(id);
|
|
574
|
+
const cat = typeof obj.category === 'string' && KNOWN_CATEGORIES.includes(obj.category)
|
|
575
|
+
? obj.category
|
|
576
|
+
: undefined;
|
|
577
|
+
facts.push({
|
|
578
|
+
id,
|
|
579
|
+
text,
|
|
580
|
+
category: cat,
|
|
581
|
+
temporality: obj.temporality,
|
|
582
|
+
observedAt: obj.observedAt,
|
|
583
|
+
timeHint: normalizeTextField(obj.timeHint),
|
|
584
|
+
updatedAt: obj.updatedAt,
|
|
585
|
+
});
|
|
588
586
|
}
|
|
589
587
|
return facts;
|
|
590
588
|
}
|
|
591
|
-
/**
|
|
592
|
-
|
|
593
|
-
|
|
589
|
+
/**
|
|
590
|
+
* 读取一个用户的现有档案(getMetadata 返回空才算不存在,返回 undefined)。
|
|
591
|
+
*
|
|
592
|
+
* 读失败原样抛出:先读后改再写的一方(关系分、事实提取、自反思、定点删事实)据此中止本次写入。
|
|
593
|
+
* 若把「读不出」当成「不存在」,调用方会用默认值覆盖写回,一次瞬时读错就清空整份档案。
|
|
594
|
+
* 只读渲染走 {@link loadProfileLenient}。
|
|
595
|
+
*
|
|
596
|
+
* 档案与指令的读写函数都由调用方传入 mem,一次操作(读 → 调 LLM → 重读 → 覆盖写)全程只认
|
|
597
|
+
* 开头取到的实例:memory 胜者可能在 LLM 调用期间换人而本插件不重启,每步现取胜者会把 A 上
|
|
598
|
+
* 合并出的整张表覆盖写进 B,B 原有的条目被删。
|
|
599
|
+
*/
|
|
600
|
+
async function loadProfile(mem, userKey) {
|
|
594
601
|
if (!mem)
|
|
595
602
|
return undefined;
|
|
603
|
+
const doc = await mem.getMetadata(PROFILE_NS, userKey);
|
|
604
|
+
if (!doc)
|
|
605
|
+
return undefined;
|
|
606
|
+
const facts = Array.isArray(doc.facts) ? parseFactArray(doc.facts) : [];
|
|
607
|
+
return {
|
|
608
|
+
facts,
|
|
609
|
+
relationScore: typeof doc.relationScore === 'number' ? Math.min(100, Math.max(0, doc.relationScore)) : 0,
|
|
610
|
+
interactionCount: typeof doc.interactionCount === 'number' ? Math.max(0, Math.floor(doc.interactionCount)) : 0,
|
|
611
|
+
lastInteractionAt: typeof doc.lastInteractionAt === 'number' ? doc.lastInteractionAt : undefined,
|
|
612
|
+
updatedAt: typeof doc.updatedAt === 'number' ? doc.updatedAt : 0,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
/** 只读渲染用:读失败按「暂无档案」处理,只记 debug(结果不会被写回) */
|
|
616
|
+
async function loadProfileLenient(mem, userKey) {
|
|
596
617
|
try {
|
|
597
|
-
|
|
598
|
-
if (!doc)
|
|
599
|
-
return undefined;
|
|
600
|
-
const facts = Array.isArray(doc.facts) ? parseFactArray(doc.facts) : [];
|
|
601
|
-
return {
|
|
602
|
-
facts,
|
|
603
|
-
relationScore: typeof doc.relationScore === 'number' ? Math.min(100, Math.max(0, doc.relationScore)) : 0,
|
|
604
|
-
interactionCount: typeof doc.interactionCount === 'number' ? Math.max(0, Math.floor(doc.interactionCount)) : 0,
|
|
605
|
-
lastInteractionAt: typeof doc.lastInteractionAt === 'number' ? doc.lastInteractionAt : undefined,
|
|
606
|
-
updatedAt: typeof doc.updatedAt === 'number' ? doc.updatedAt : 0,
|
|
607
|
-
};
|
|
618
|
+
return await loadProfile(mem, userKey);
|
|
608
619
|
}
|
|
609
620
|
catch (err) {
|
|
610
621
|
logger.debug(`加载用户档案失败 (${userKey}): ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -612,8 +623,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
612
623
|
}
|
|
613
624
|
}
|
|
614
625
|
/** 保存档案(覆盖式) */
|
|
615
|
-
async function saveProfile(userKey, profile) {
|
|
616
|
-
const mem = memory.current;
|
|
626
|
+
async function saveProfile(mem, userKey, profile) {
|
|
617
627
|
if (!mem)
|
|
618
628
|
return;
|
|
619
629
|
const payload = {
|
|
@@ -630,6 +640,60 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
630
640
|
return undefined;
|
|
631
641
|
return KNOWN_CATEGORIES.includes(v) ? v : undefined;
|
|
632
642
|
}
|
|
643
|
+
/** 解析 LLM 输出里的 add / update / remove 三类事实操作。sourceQuote 原样带出,是否校验由调用方决定;它不入库(mergeFactList 逐字段构造 Fact) */
|
|
644
|
+
function parseFactOps(parsed) {
|
|
645
|
+
const obj = parsed;
|
|
646
|
+
const add = Array.isArray(obj.add)
|
|
647
|
+
? obj.add.flatMap(x => {
|
|
648
|
+
if (!x || typeof x !== 'object')
|
|
649
|
+
return [];
|
|
650
|
+
const o = x;
|
|
651
|
+
const t = typeof o.text === 'string' ? o.text.trim() : '';
|
|
652
|
+
if (!t)
|
|
653
|
+
return [];
|
|
654
|
+
const category = normalizeCategory(o.category);
|
|
655
|
+
const sourceQuote = typeof o.sourceQuote === 'string' ? o.sourceQuote.trim() : '';
|
|
656
|
+
return [
|
|
657
|
+
{
|
|
658
|
+
text: t,
|
|
659
|
+
category,
|
|
660
|
+
temporality: normalizeTemporality(o.temporality, category),
|
|
661
|
+
timeHint: normalizeTextField(o.timeHint),
|
|
662
|
+
sourceQuote,
|
|
663
|
+
},
|
|
664
|
+
];
|
|
665
|
+
})
|
|
666
|
+
: [];
|
|
667
|
+
const update = Array.isArray(obj.update)
|
|
668
|
+
? obj.update.flatMap(x => {
|
|
669
|
+
if (!x || typeof x !== 'object')
|
|
670
|
+
return [];
|
|
671
|
+
const o = x;
|
|
672
|
+
const id = typeof o.id === 'string' ? o.id.trim() : '';
|
|
673
|
+
const t = typeof o.text === 'string' ? o.text.trim() : '';
|
|
674
|
+
if (!id || !t)
|
|
675
|
+
return [];
|
|
676
|
+
const category = normalizeCategory(o.category);
|
|
677
|
+
const sourceQuote = typeof o.sourceQuote === 'string' ? o.sourceQuote.trim() : '';
|
|
678
|
+
return [
|
|
679
|
+
{
|
|
680
|
+
id,
|
|
681
|
+
text: t,
|
|
682
|
+
category,
|
|
683
|
+
temporality: normalizeTemporality(o.temporality, category),
|
|
684
|
+
timeHint: normalizeTextField(o.timeHint),
|
|
685
|
+
sourceQuote,
|
|
686
|
+
},
|
|
687
|
+
];
|
|
688
|
+
})
|
|
689
|
+
: [];
|
|
690
|
+
const remove = Array.isArray(obj.remove)
|
|
691
|
+
? obj.remove
|
|
692
|
+
.filter((x) => typeof x === 'string' && x.trim().length > 0)
|
|
693
|
+
.map(s => s.trim())
|
|
694
|
+
: [];
|
|
695
|
+
return { add, update, remove };
|
|
696
|
+
}
|
|
633
697
|
/** 调用 LLM 从历史中提取/修订事实,返回 add / update / remove 三类操作。 */
|
|
634
698
|
async function llmExtractFacts(history, existingFacts, nickname, userId, platform) {
|
|
635
699
|
const empty = {
|
|
@@ -725,56 +789,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
725
789
|
}
|
|
726
790
|
logger.debug('[user-profile] LLM 重试后解析成功');
|
|
727
791
|
}
|
|
728
|
-
const
|
|
729
|
-
const add = Array.isArray(parsedObj.add)
|
|
730
|
-
? parsedObj.add.flatMap(x => {
|
|
731
|
-
if (!x || typeof x !== 'object')
|
|
732
|
-
return [];
|
|
733
|
-
const o = x;
|
|
734
|
-
const t = typeof o.text === 'string' ? o.text.trim() : '';
|
|
735
|
-
if (!t)
|
|
736
|
-
return [];
|
|
737
|
-
const category = normalizeCategory(o.category);
|
|
738
|
-
const sourceQuote = typeof o.sourceQuote === 'string' ? o.sourceQuote.trim() : '';
|
|
739
|
-
return [
|
|
740
|
-
{
|
|
741
|
-
text: t,
|
|
742
|
-
category,
|
|
743
|
-
temporality: normalizeTemporality(o.temporality, category),
|
|
744
|
-
timeHint: normalizeTextField(o.timeHint),
|
|
745
|
-
sourceQuote,
|
|
746
|
-
},
|
|
747
|
-
];
|
|
748
|
-
})
|
|
749
|
-
: [];
|
|
750
|
-
const update = Array.isArray(parsedObj.update)
|
|
751
|
-
? parsedObj.update.flatMap(x => {
|
|
752
|
-
if (!x || typeof x !== 'object')
|
|
753
|
-
return [];
|
|
754
|
-
const o = x;
|
|
755
|
-
const id = typeof o.id === 'string' ? o.id.trim() : '';
|
|
756
|
-
const t = typeof o.text === 'string' ? o.text.trim() : '';
|
|
757
|
-
if (!id || !t)
|
|
758
|
-
return [];
|
|
759
|
-
const category = normalizeCategory(o.category);
|
|
760
|
-
const sourceQuote = typeof o.sourceQuote === 'string' ? o.sourceQuote.trim() : '';
|
|
761
|
-
return [
|
|
762
|
-
{
|
|
763
|
-
id,
|
|
764
|
-
text: t,
|
|
765
|
-
category,
|
|
766
|
-
temporality: normalizeTemporality(o.temporality, category),
|
|
767
|
-
timeHint: normalizeTextField(o.timeHint),
|
|
768
|
-
sourceQuote,
|
|
769
|
-
},
|
|
770
|
-
];
|
|
771
|
-
})
|
|
772
|
-
: [];
|
|
773
|
-
const remove = Array.isArray(parsedObj.remove)
|
|
774
|
-
? parsedObj.remove
|
|
775
|
-
.filter((x) => typeof x === 'string' && x.trim().length > 0)
|
|
776
|
-
.map(s => s.trim())
|
|
777
|
-
: [];
|
|
792
|
+
const { add, update, remove } = parseFactOps(parsed);
|
|
778
793
|
// sourceQuote 校验:quote 可能跨多条消息(LLM 用 \n 拼接),
|
|
779
794
|
// 因此按行拆分,每行(非空)都必须能在「目标用户」语料中找到 normalize 后的子串。
|
|
780
795
|
// 这样即使行与行之间夹着其他用户的消息也不会被错误拒收。
|
|
@@ -803,17 +818,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
803
818
|
};
|
|
804
819
|
const validatedAdd = add.filter(item => validateSourceQuote(item, 'add'));
|
|
805
820
|
const validatedUpdate = update.filter(item => validateSourceQuote(item, 'update'));
|
|
806
|
-
|
|
807
|
-
const stripQuote = (item) => {
|
|
808
|
-
const { sourceQuote: _omit, ...rest } = item;
|
|
809
|
-
void _omit;
|
|
810
|
-
return rest;
|
|
811
|
-
};
|
|
812
|
-
return {
|
|
813
|
-
add: validatedAdd.map(stripQuote),
|
|
814
|
-
update: validatedUpdate.map(stripQuote),
|
|
815
|
-
remove,
|
|
816
|
-
};
|
|
821
|
+
return { add: validatedAdd, update: validatedUpdate, remove };
|
|
817
822
|
}
|
|
818
823
|
catch (err) {
|
|
819
824
|
logger.debug(`事实提取 LLM 调用失败:${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -877,14 +882,14 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
877
882
|
id: u.id,
|
|
878
883
|
text,
|
|
879
884
|
category: u.category ?? old.category,
|
|
880
|
-
temporality: u.temporality ?? old.temporality
|
|
881
|
-
observedAt: old.observedAt
|
|
885
|
+
temporality: u.temporality ?? old.temporality,
|
|
886
|
+
observedAt: old.observedAt,
|
|
882
887
|
timeHint: u.timeHint ?? old.timeHint,
|
|
883
888
|
updatedAt: now,
|
|
884
889
|
});
|
|
885
890
|
}
|
|
886
891
|
else {
|
|
887
|
-
const id =
|
|
892
|
+
const id = genShortId('f', usedIds);
|
|
888
893
|
usedIds.add(id);
|
|
889
894
|
byId.set(id, {
|
|
890
895
|
id,
|
|
@@ -903,7 +908,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
903
908
|
const text = clipText(a.text);
|
|
904
909
|
if (textSet.has(text))
|
|
905
910
|
continue;
|
|
906
|
-
const id =
|
|
911
|
+
const id = genShortId('f', usedIds);
|
|
907
912
|
usedIds.add(id);
|
|
908
913
|
byId.set(id, {
|
|
909
914
|
id,
|
|
@@ -925,9 +930,14 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
925
930
|
merged.sort((a, b) => a.updatedAt - b.updatedAt);
|
|
926
931
|
return merged;
|
|
927
932
|
}
|
|
928
|
-
/**
|
|
929
|
-
|
|
930
|
-
|
|
933
|
+
/**
|
|
934
|
+
* LLM 的 update 针对调用前快照里的 id。快照里有、重读后已不在的,说明调用期间被删(清空或定点删除),
|
|
935
|
+
* 丢弃:否则 mergeFactList 把未知 id 当新增,删掉的事实换个 id 写回。快照里本就没有的 id 照旧按新增处理
|
|
936
|
+
*/
|
|
937
|
+
function dropUpdatesOfDeletedFacts(update, snapshot, fresh) {
|
|
938
|
+
const snapshotIds = new Set(snapshot.map(f => f.id));
|
|
939
|
+
const freshIds = new Set(fresh.map(f => f.id));
|
|
940
|
+
return update.filter(u => !snapshotIds.has(u.id) || freshIds.has(u.id));
|
|
931
941
|
}
|
|
932
942
|
/**
|
|
933
943
|
* 后台触发一次事实提取(并发互斥)。
|
|
@@ -941,7 +951,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
941
951
|
inflightExtractions.add(userKey);
|
|
942
952
|
const mem = memory.current;
|
|
943
953
|
try {
|
|
944
|
-
if (!mem
|
|
954
|
+
if (!mem)
|
|
945
955
|
return;
|
|
946
956
|
const rawHistory = await mem.getHistory(sessionId, cfg.historyForExtraction);
|
|
947
957
|
// 跨会话委派是另一个 agent 实例发出的指令·不是该用户发言,不能作为他的用户存档提取语料
|
|
@@ -949,21 +959,17 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
949
959
|
// 序列中至少需要一条目标用户发言,否则没有可提取语料
|
|
950
960
|
if (!history.some(m => isTargetUserMessage(m, userId, platform)))
|
|
951
961
|
return;
|
|
952
|
-
const
|
|
953
|
-
|
|
954
|
-
relationScore: 0,
|
|
955
|
-
interactionCount: 0,
|
|
956
|
-
updatedAt: 0,
|
|
957
|
-
};
|
|
958
|
-
const ops = await llmExtractFacts(history, profile.facts, nickname, userId, platform);
|
|
962
|
+
const snapshotFacts = (await loadProfile(mem, userKey))?.facts ?? [];
|
|
963
|
+
const ops = await llmExtractFacts(history, snapshotFacts, nickname, userId, platform);
|
|
959
964
|
const hasFactOps = ops.add.length > 0 || ops.update.length > 0 || ops.remove.length > 0;
|
|
960
965
|
if (!hasFactOps)
|
|
961
966
|
return;
|
|
962
|
-
|
|
963
|
-
//
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
|
|
967
|
+
// 以 LLM 返回后重读的档案为合并基底:调用期间档案可能被清空或删了事实,以调用前的快照
|
|
968
|
+
// 为基底会把删掉的内容整份写回;重读也保住期间写入的 relationScore 等字段
|
|
969
|
+
const fresh = (await loadProfile(mem, userKey)) ?? { facts: [], updatedAt: 0 };
|
|
970
|
+
const newFacts = mergeFactList(fresh.facts, ops.add, dropUpdatesOfDeletedFacts(ops.update, snapshotFacts, fresh.facts), ops.remove, cfg.maxFactsPerUser);
|
|
971
|
+
await saveProfile(mem, userKey, {
|
|
972
|
+
...fresh,
|
|
967
973
|
facts: newFacts,
|
|
968
974
|
updatedAt: Date.now(),
|
|
969
975
|
});
|
|
@@ -1057,53 +1063,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1057
1063
|
logger.warn(`[user-profile] 自反思 LLM 输出无法解析为 JSON,原文前 200 字:${text.slice(0, 200)}`);
|
|
1058
1064
|
return empty;
|
|
1059
1065
|
}
|
|
1060
|
-
|
|
1061
|
-
const add = Array.isArray(obj.add)
|
|
1062
|
-
? obj.add.flatMap(x => {
|
|
1063
|
-
if (!x || typeof x !== 'object')
|
|
1064
|
-
return [];
|
|
1065
|
-
const o = x;
|
|
1066
|
-
const t = typeof o.text === 'string' ? o.text.trim() : '';
|
|
1067
|
-
if (!t)
|
|
1068
|
-
return [];
|
|
1069
|
-
const category = normalizeCategory(o.category);
|
|
1070
|
-
return [
|
|
1071
|
-
{
|
|
1072
|
-
text: t,
|
|
1073
|
-
category,
|
|
1074
|
-
temporality: normalizeTemporality(o.temporality, category),
|
|
1075
|
-
timeHint: normalizeTextField(o.timeHint),
|
|
1076
|
-
},
|
|
1077
|
-
];
|
|
1078
|
-
})
|
|
1079
|
-
: [];
|
|
1080
|
-
const update = Array.isArray(obj.update)
|
|
1081
|
-
? obj.update.flatMap(x => {
|
|
1082
|
-
if (!x || typeof x !== 'object')
|
|
1083
|
-
return [];
|
|
1084
|
-
const o = x;
|
|
1085
|
-
const id = typeof o.id === 'string' ? o.id.trim() : '';
|
|
1086
|
-
const t = typeof o.text === 'string' ? o.text.trim() : '';
|
|
1087
|
-
if (!id || !t)
|
|
1088
|
-
return [];
|
|
1089
|
-
const category = normalizeCategory(o.category);
|
|
1090
|
-
return [
|
|
1091
|
-
{
|
|
1092
|
-
id,
|
|
1093
|
-
text: t,
|
|
1094
|
-
category,
|
|
1095
|
-
temporality: normalizeTemporality(o.temporality, category),
|
|
1096
|
-
timeHint: normalizeTextField(o.timeHint),
|
|
1097
|
-
},
|
|
1098
|
-
];
|
|
1099
|
-
})
|
|
1100
|
-
: [];
|
|
1101
|
-
const remove = Array.isArray(obj.remove)
|
|
1102
|
-
? obj.remove
|
|
1103
|
-
.filter((x) => typeof x === 'string' && x.trim().length > 0)
|
|
1104
|
-
.map(s => s.trim())
|
|
1105
|
-
: [];
|
|
1106
|
-
return { ...empty, add, update, remove };
|
|
1066
|
+
return parseFactOps(parsed);
|
|
1107
1067
|
}
|
|
1108
1068
|
catch (err) {
|
|
1109
1069
|
logger.debug(`自反思 LLM 调用失败:${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -1119,7 +1079,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1119
1079
|
selfReflectionInflight = true;
|
|
1120
1080
|
try {
|
|
1121
1081
|
const mem = memory.current;
|
|
1122
|
-
if (!mem
|
|
1082
|
+
if (!mem)
|
|
1123
1083
|
return;
|
|
1124
1084
|
const rawHistory = await mem.getHistory(sessionId, cfg.selfReflectHistory);
|
|
1125
1085
|
const history = rawHistory.filter(m => m.kind !== WellKnownKinds.CrossSessionDelegation);
|
|
@@ -1127,18 +1087,14 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1127
1087
|
if (!history.some(m => m.role === 'assistant' && m.content))
|
|
1128
1088
|
return;
|
|
1129
1089
|
const selfKey = getSelfKey();
|
|
1130
|
-
const
|
|
1131
|
-
|
|
1132
|
-
relationScore: 0,
|
|
1133
|
-
interactionCount: 0,
|
|
1134
|
-
updatedAt: 0,
|
|
1135
|
-
};
|
|
1136
|
-
const ops = await llmReflectSelf(history, profile.facts);
|
|
1090
|
+
const snapshotFacts = (await loadProfile(mem, selfKey))?.facts ?? [];
|
|
1091
|
+
const ops = await llmReflectSelf(history, snapshotFacts);
|
|
1137
1092
|
if (ops.add.length === 0 && ops.update.length === 0 && ops.remove.length === 0)
|
|
1138
1093
|
return;
|
|
1139
|
-
|
|
1140
|
-
const fresh = (await loadProfile(selfKey)) ??
|
|
1141
|
-
|
|
1094
|
+
// 合并基底取重读结果,理由同 triggerExtractionForUser
|
|
1095
|
+
const fresh = (await loadProfile(mem, selfKey)) ?? { facts: [], updatedAt: 0 };
|
|
1096
|
+
const newFacts = mergeFactList(fresh.facts, ops.add, dropUpdatesOfDeletedFacts(ops.update, snapshotFacts, fresh.facts), ops.remove, cfg.maxSelfFacts);
|
|
1097
|
+
await saveProfile(mem, selfKey, { ...fresh, facts: newFacts, updatedAt: Date.now() });
|
|
1142
1098
|
logger.debug(`Aalis 自档案已更新 (${selfKey}): +${ops.add.length} ~${ops.update.length} -${ops.remove.length} → ${newFacts.length} 条`);
|
|
1143
1099
|
}
|
|
1144
1100
|
catch (err) {
|
|
@@ -1328,26 +1284,24 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1328
1284
|
instructionExtractionInflight = true;
|
|
1329
1285
|
try {
|
|
1330
1286
|
const mem = memory.current;
|
|
1331
|
-
if (!mem
|
|
1287
|
+
if (!mem)
|
|
1332
1288
|
return;
|
|
1333
1289
|
const rawHistory = await mem.getHistory(sessionId, cfg.instructionHistoryForExtraction);
|
|
1334
1290
|
const history = rawHistory.filter(m => m.kind !== WellKnownKinds.CrossSessionDelegation);
|
|
1335
1291
|
if (history.length === 0)
|
|
1336
1292
|
return;
|
|
1337
|
-
const doc = await loadInstructions();
|
|
1293
|
+
const doc = await loadInstructions(mem);
|
|
1338
1294
|
const ops = await llmExtractInstructions(history, doc.instructions, authorityFn);
|
|
1339
1295
|
if (ops.add.length === 0 && ops.update.length === 0 && ops.remove.length === 0)
|
|
1340
1296
|
return;
|
|
1341
|
-
|
|
1297
|
+
// 合并基底取 LLM 返回后重读的指令表:调用期间被清掉或删掉的指令不随调用前的快照写回;
|
|
1298
|
+
// update 指向已不在的 id 时 mergeInstructions 本就跳过
|
|
1299
|
+
const fresh = await loadInstructions(mem);
|
|
1300
|
+
const newInstructions = mergeInstructions(fresh.instructions, ops.add.map(a => ({ ...a, sourceChannel: 'llm' })), ops.update.map(u => {
|
|
1342
1301
|
const { id, ...patch } = u;
|
|
1343
1302
|
return { id, patch };
|
|
1344
1303
|
}), ops.remove);
|
|
1345
|
-
|
|
1346
|
-
await saveInstructions({
|
|
1347
|
-
...fresh,
|
|
1348
|
-
instructions: newInstructions,
|
|
1349
|
-
updatedAt: Date.now(),
|
|
1350
|
-
});
|
|
1304
|
+
await saveInstructions(mem, { instructions: newInstructions, updatedAt: Date.now() });
|
|
1351
1305
|
logger.debug(`指令档案已更新 (${getInstructionsKey()}): +${ops.add.length} ~${ops.update.length} -${ops.remove.length} → ${newInstructions.length} 条`);
|
|
1352
1306
|
}
|
|
1353
1307
|
catch (err) {
|
|
@@ -1358,8 +1312,14 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1358
1312
|
}
|
|
1359
1313
|
}
|
|
1360
1314
|
async function updateRelationForUser(userKey, triggerType, options) {
|
|
1361
|
-
const
|
|
1362
|
-
|
|
1315
|
+
const mem = memory.current;
|
|
1316
|
+
const profile = (await loadProfile(mem, userKey)) ?? {
|
|
1317
|
+
facts: [],
|
|
1318
|
+
relationScore: 0,
|
|
1319
|
+
interactionCount: 0,
|
|
1320
|
+
updatedAt: 0,
|
|
1321
|
+
};
|
|
1322
|
+
await saveProfile(mem, userKey, applyRelationUpdate(profile, triggerType, options));
|
|
1363
1323
|
}
|
|
1364
1324
|
// ─── 关系分数:在 agent 触发回复路径上更新 ───
|
|
1365
1325
|
// 注:钩子无优先级机制,同相位 handler 按插件激活序执行;本 handler 只做副作用
|
|
@@ -1421,10 +1381,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1421
1381
|
return true;
|
|
1422
1382
|
if (cfg.temporaryFactMaxAgeDays <= 0)
|
|
1423
1383
|
return true;
|
|
1424
|
-
|
|
1425
|
-
if (!base)
|
|
1426
|
-
return true;
|
|
1427
|
-
return Date.now() - base <= cfg.temporaryFactMaxAgeDays * 86_400_000;
|
|
1384
|
+
return Date.now() - fact.updatedAt <= cfg.temporaryFactMaxAgeDays * 86_400_000;
|
|
1428
1385
|
}
|
|
1429
1386
|
function renderFactLine(fact, includeMeta) {
|
|
1430
1387
|
if (!includeMeta)
|
|
@@ -1432,12 +1389,10 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1432
1389
|
const meta = [];
|
|
1433
1390
|
if (fact.timeHint)
|
|
1434
1391
|
meta.push(`时间线索:${fact.timeHint}`);
|
|
1435
|
-
|
|
1436
|
-
if (observed)
|
|
1437
|
-
meta.push(`记录于:${new Date(observed).toLocaleDateString('zh-CN')}`);
|
|
1392
|
+
meta.push(`记录于:${new Date(fact.observedAt).toLocaleDateString('zh-CN')}`);
|
|
1438
1393
|
if (fact.temporality === 'temporary')
|
|
1439
1394
|
meta.push('临时状态');
|
|
1440
|
-
return
|
|
1395
|
+
return `- ${fact.text}(${meta.join(';')})`;
|
|
1441
1396
|
}
|
|
1442
1397
|
/** 将一个用户的 Fact[] 渲染为分组文本块 */
|
|
1443
1398
|
function renderProfileBlock(facts, label, compact) {
|
|
@@ -1489,12 +1444,13 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1489
1444
|
// 干跑(token 快照)跳过档案加载——该路径 userId 为空串,加载既昂贵又无意义
|
|
1490
1445
|
if (data.dryRun)
|
|
1491
1446
|
return null;
|
|
1447
|
+
const mem = memory.current;
|
|
1492
1448
|
const blocksToInsert = [];
|
|
1493
1449
|
const trigger = data.triggerType ?? 'direct';
|
|
1494
1450
|
const hasPrimarySpeaker = trigger === 'direct' || trigger === 'immediate';
|
|
1495
1451
|
// 0a. 第三方行为指令(最高优先级):作为不可违逆的行为约束放在 selfFacts 之前
|
|
1496
1452
|
if (cfg.enableInstructions) {
|
|
1497
|
-
const insDoc = await
|
|
1453
|
+
const insDoc = await loadInstructionsLenient(mem);
|
|
1498
1454
|
if (insDoc.instructions.length > 0) {
|
|
1499
1455
|
const body = renderInstructionsBlock(insDoc.instructions);
|
|
1500
1456
|
const insBlock = '# 第三方行为准则(最高优先)\n' +
|
|
@@ -1510,7 +1466,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1510
1466
|
}
|
|
1511
1467
|
// 0. Aalis 自档案:永远尝试注入到最前段,作为人格延续锚点
|
|
1512
1468
|
if (cfg.enableSelfProfile) {
|
|
1513
|
-
const selfProfile = await
|
|
1469
|
+
const selfProfile = await loadProfileLenient(mem, getSelfKey());
|
|
1514
1470
|
const activeSelfFacts = selfProfile?.facts.filter(isFactActive) ?? [];
|
|
1515
1471
|
if (activeSelfFacts.length > 0) {
|
|
1516
1472
|
const body = renderProfileBlock(activeSelfFacts, 'Aalis', false);
|
|
@@ -1525,13 +1481,12 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1525
1481
|
// 1. 主发言者完整档案:仅在确实有人在「和 Aalis 对话」时注入
|
|
1526
1482
|
if (hasPrimarySpeaker && data.userId) {
|
|
1527
1483
|
const userKey = userKeyOf(data.platform, data.userId);
|
|
1528
|
-
const profile = await
|
|
1484
|
+
const profile = await loadProfileLenient(mem, userKey);
|
|
1529
1485
|
if (profile?.facts.some(isFactActive)) {
|
|
1530
1486
|
const body = renderProfileBlock(profile.facts, data.userId, false);
|
|
1531
1487
|
const relationLine = renderRelationLine(profile);
|
|
1532
|
-
// 可选:从 user-relation
|
|
1488
|
+
// 可选:从 user-relation 服务取「同社群活跃成员」(按配置的社群发现算法所得标签 + PageRank 排序)。
|
|
1533
1489
|
// 服务不存在 / 节点没社群标签 / 报错都静默跳过;该字段是锦上添花,不破坏档案主体。
|
|
1534
|
-
// 同名描述符声明 optional 依赖,类型来自兄弟插件的 type-only 导入,不构成运行时包依赖。
|
|
1535
1490
|
let communityLine = '';
|
|
1536
1491
|
try {
|
|
1537
1492
|
const relation = userRelation.current;
|
|
@@ -1593,7 +1548,6 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1593
1548
|
break;
|
|
1594
1549
|
}
|
|
1595
1550
|
if (cfg.allowGlobalBackfill && others.size < candidateLimit) {
|
|
1596
|
-
const mem = memory.current;
|
|
1597
1551
|
if (mem) {
|
|
1598
1552
|
try {
|
|
1599
1553
|
const globalRecent = await mem.listMetadata(PROFILE_NS);
|
|
@@ -1624,13 +1578,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1624
1578
|
if (others.size > 0) {
|
|
1625
1579
|
const snippets = [];
|
|
1626
1580
|
for (const [key, info] of others) {
|
|
1627
|
-
|
|
1628
|
-
try {
|
|
1629
|
-
profile = await loadProfile(key);
|
|
1630
|
-
}
|
|
1631
|
-
catch {
|
|
1632
|
-
/* 静默跳过 */
|
|
1633
|
-
}
|
|
1581
|
+
const profile = await loadProfileLenient(mem, key);
|
|
1634
1582
|
if (!profile?.facts.some(isFactActive))
|
|
1635
1583
|
continue;
|
|
1636
1584
|
const label = info.nickname ? `${info.nickname}(${info.userId})` : info.userId;
|
|
@@ -1685,20 +1633,19 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1685
1633
|
}
|
|
1686
1634
|
// 同步清空第三方行为指令档案(per-persona)。指令是档案的兄弟概念,
|
|
1687
1635
|
// memory:clear scope='all' 时一并清理;types 默认包含 user-profile 即也清。
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
}
|
|
1636
|
+
// 不看 enableInstructions:开关管功能用不用,不管已有数据能不能清。
|
|
1637
|
+
try {
|
|
1638
|
+
const insItems = await mem.listMetadata(INSTRUCTIONS_NS);
|
|
1639
|
+
await mem.commitMetadata(insItems.map(it => ({ op: 'del', namespace: INSTRUCTIONS_NS, key: it.key })));
|
|
1640
|
+
data.results.push({
|
|
1641
|
+
source: 'user-profile-instructions',
|
|
1642
|
+
success: true,
|
|
1643
|
+
message: `第三方行为指令已清空 (${insItems.length} 条)`,
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
catch (err) {
|
|
1647
|
+
const m = err instanceof Error ? err.message : String(err);
|
|
1648
|
+
data.results.push({ source: 'user-profile-instructions', success: false, message: `指令清空失败: ${m}` });
|
|
1702
1649
|
}
|
|
1703
1650
|
await next();
|
|
1704
1651
|
});
|
|
@@ -1761,7 +1708,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1761
1708
|
error: '需提供 user_key 或 (platform + user_id) 或 self=true。',
|
|
1762
1709
|
});
|
|
1763
1710
|
}
|
|
1764
|
-
const profile = await
|
|
1711
|
+
const profile = await loadProfileLenient(memory.current, userKey);
|
|
1765
1712
|
if (!profile || profile.facts.length === 0) {
|
|
1766
1713
|
return JSON.stringify({ ok: true, userKey, found: false, message: '该用户暂无档案数据。' });
|
|
1767
1714
|
}
|
|
@@ -1794,7 +1741,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1794
1741
|
if (!userId)
|
|
1795
1742
|
return '当前会话未识别用户身份,无法查看档案。';
|
|
1796
1743
|
const userKey = userKeyOf(argv.session.platform, userId);
|
|
1797
|
-
const profile = await
|
|
1744
|
+
const profile = await loadProfileLenient(memory.current, userKey);
|
|
1798
1745
|
if (!profile || profile.facts.length === 0) {
|
|
1799
1746
|
return `📭 暂无档案数据 (${userKey})`;
|
|
1800
1747
|
}
|
|
@@ -1834,27 +1781,33 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1834
1781
|
const target = String(targetArg ?? '');
|
|
1835
1782
|
const factId = String(factIdArg ?? '');
|
|
1836
1783
|
const userKey = target.includes(':') ? target : userKeyOf(argv.session.platform, target);
|
|
1837
|
-
const
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
const
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1784
|
+
const mem = memory.current;
|
|
1785
|
+
try {
|
|
1786
|
+
const profile = await loadProfile(mem, userKey);
|
|
1787
|
+
if (!profile || profile.facts.length === 0)
|
|
1788
|
+
return `📭 无档案数据 (${userKey})`;
|
|
1789
|
+
const hit = profile.facts.find(f => f.id === factId);
|
|
1790
|
+
if (!hit) {
|
|
1791
|
+
const listing = profile.facts
|
|
1792
|
+
.slice(0, 20)
|
|
1793
|
+
.map(f => ` [${f.id}] ${f.text.slice(0, 50)}`)
|
|
1794
|
+
.join('\n');
|
|
1795
|
+
return `未找到事实 [${factId}]。${userKey} 现有 ${profile.facts.length} 条:\n${listing}`;
|
|
1796
|
+
}
|
|
1797
|
+
await saveProfile(mem, userKey, {
|
|
1798
|
+
...profile,
|
|
1799
|
+
facts: profile.facts.filter(f => f.id !== factId),
|
|
1800
|
+
updatedAt: Date.now(),
|
|
1801
|
+
});
|
|
1802
|
+
return `✅ 已删除 ${userKey} 的事实 [${factId}]:${hit.text.slice(0, 60)}`;
|
|
1803
|
+
}
|
|
1804
|
+
catch (err) {
|
|
1805
|
+
return `❌ 删除失败:${err instanceof Error ? err.message : String(err)}`;
|
|
1806
|
+
}
|
|
1854
1807
|
});
|
|
1855
1808
|
commands.command('profile.self', '查看 Aalis 的自档案(跨会话的内心状态)').action(async () => {
|
|
1856
1809
|
const selfKey = getSelfKey();
|
|
1857
|
-
const profile = await
|
|
1810
|
+
const profile = await loadProfileLenient(memory.current, selfKey);
|
|
1858
1811
|
if (!profile || profile.facts.length === 0) {
|
|
1859
1812
|
return `🌱 ${getCurrentPersonaName()} 还没有积累任何自反思事实。(key=${selfKey})`;
|
|
1860
1813
|
}
|
|
@@ -1898,7 +1851,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1898
1851
|
if (!cfg.enableInstructions)
|
|
1899
1852
|
return '🚫 第三方行为指令功能未启用。';
|
|
1900
1853
|
const personaName = getCurrentPersonaName();
|
|
1901
|
-
const doc = await
|
|
1854
|
+
const doc = await loadInstructionsLenient(memory.current);
|
|
1902
1855
|
if (doc.instructions.length === 0) {
|
|
1903
1856
|
return `📭 ${personaName} 当前没有任何第三方行为指令。`;
|
|
1904
1857
|
}
|
|
@@ -1923,19 +1876,25 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1923
1876
|
const userId = argv.session.userId;
|
|
1924
1877
|
const sourceUserKey = userId ? userKeyOf(argv.session.platform, userId) : undefined;
|
|
1925
1878
|
const sourceUserName = userId ?? '匿名';
|
|
1926
|
-
const
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1879
|
+
const mem = memory.current;
|
|
1880
|
+
try {
|
|
1881
|
+
const doc = await loadInstructions(mem);
|
|
1882
|
+
const merged = mergeInstructions(doc.instructions, [
|
|
1883
|
+
{
|
|
1884
|
+
text: body,
|
|
1885
|
+
severity: 'should',
|
|
1886
|
+
sourceUserKey,
|
|
1887
|
+
sourceUserName,
|
|
1888
|
+
sourceChannel: 'command',
|
|
1889
|
+
},
|
|
1890
|
+
], [], []);
|
|
1891
|
+
await saveInstructions(mem, { instructions: merged, updatedAt: Date.now() });
|
|
1892
|
+
const added = merged.find(m => m.text === clipInstructionText(body));
|
|
1893
|
+
return `✅ 已添加行为指令${added ? `(id=${added.id})` : ''}:${clipInstructionText(body)}`;
|
|
1894
|
+
}
|
|
1895
|
+
catch (err) {
|
|
1896
|
+
return `❌ 添加失败:${err instanceof Error ? err.message : String(err)}`;
|
|
1897
|
+
}
|
|
1939
1898
|
});
|
|
1940
1899
|
commands
|
|
1941
1900
|
.command('instruct.remove <id:string>', '按 id 删除一条第三方行为指令', { visibility: 'restricted' })
|
|
@@ -1945,13 +1904,19 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1945
1904
|
const targetId = typeof id === 'string' ? id.trim() : '';
|
|
1946
1905
|
if (!targetId)
|
|
1947
1906
|
return '❌ 需要提供指令 id。';
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1907
|
+
const mem = memory.current;
|
|
1908
|
+
try {
|
|
1909
|
+
const doc = await loadInstructions(mem);
|
|
1910
|
+
const target = doc.instructions.find(i => i.id === targetId);
|
|
1911
|
+
if (!target)
|
|
1912
|
+
return `❌ 未找到 id=${targetId} 的指令。`;
|
|
1913
|
+
const merged = mergeInstructions(doc.instructions, [], [], [targetId]);
|
|
1914
|
+
await saveInstructions(mem, { instructions: merged, updatedAt: Date.now() });
|
|
1915
|
+
return `✅ 已删除指令(id=${targetId}):${target.text}`;
|
|
1916
|
+
}
|
|
1917
|
+
catch (err) {
|
|
1918
|
+
return `❌ 删除失败:${err instanceof Error ? err.message : String(err)}`;
|
|
1919
|
+
}
|
|
1955
1920
|
});
|
|
1956
1921
|
commands
|
|
1957
1922
|
.command('instruct.clear', '【危险】清空当前 persona 的全部第三方行为指令', {
|
|
@@ -1964,7 +1929,7 @@ function registerUserProfile({ memory, llm, events, hooks, contributions, config
|
|
|
1964
1929
|
if (!mem)
|
|
1965
1930
|
return '记忆服务不支持指令删除。';
|
|
1966
1931
|
try {
|
|
1967
|
-
const before = (await
|
|
1932
|
+
const before = (await loadInstructionsLenient(mem)).instructions.length;
|
|
1968
1933
|
await mem.deleteMetadata(INSTRUCTIONS_NS, getInstructionsKey());
|
|
1969
1934
|
return `✅ ${getCurrentPersonaName()} 的第三方行为指令已清空(删除 ${before} 条)。`;
|
|
1970
1935
|
}
|