@aalis/plugin-user-profile 0.11.1 → 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 +16 -11
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +386 -412
- package/dist/index.js.map +1 -1
- package/package.json +27 -14
package/dist/index.js
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { authority, DEFAULT_AUTHORITY } from '@aalis/api-authority';
|
|
2
|
+
import { commands } from '@aalis/api-commands';
|
|
3
|
+
import { contributions } from '@aalis/api-contributions';
|
|
4
|
+
import { hooks } from '@aalis/api-hooks';
|
|
5
|
+
import { llm, resolveLLMModel } from '@aalis/api-llm';
|
|
6
|
+
import { memory } from '@aalis/api-memory';
|
|
7
|
+
import { persona } from '@aalis/api-persona';
|
|
8
|
+
import { tools } from '@aalis/api-tools';
|
|
9
|
+
import { userRelation } from '@aalis/api-user-relation';
|
|
10
|
+
import { config, definePlugin, events, logger, optional } from '@aalis/core';
|
|
4
11
|
import { WellKnownKinds } from '@aalis/schema-message';
|
|
5
12
|
import { parseLLMJsonObject } from '@aalis/util-json-repair';
|
|
6
13
|
// ════════════════════════════════════════════════════════════
|
|
@@ -26,13 +33,6 @@ import { parseLLMJsonObject } from '@aalis/util-json-repair';
|
|
|
26
33
|
// - 上限 100,下限 0
|
|
27
34
|
// 例:群聊纯旁观一条 = +0.1;群聊被 @ 并回复 = +0.1 + 1.5 = +1.6;私聊一条 = +0.1 + 1.0 = +1.1
|
|
28
35
|
// ════════════════════════════════════════════════════════════
|
|
29
|
-
export const name = '@aalis/plugin-user-profile';
|
|
30
|
-
export const displayName = '用户事实档案';
|
|
31
|
-
export const subsystem = 'memory';
|
|
32
|
-
export const inject = {
|
|
33
|
-
required: ['memory', 'llm'],
|
|
34
|
-
optional: ['user-relation', 'tools'],
|
|
35
|
-
};
|
|
36
36
|
const PROFILE_NS = 'user:profile';
|
|
37
37
|
/** Aalis 自档案的 userKey 前缀。userKeyOf 形如 `platform:userId`,
|
|
38
38
|
* 这里用 `__self__:<personaName>` 这种不可能在真实平台出现的形式做隔离。
|
|
@@ -45,7 +45,7 @@ const DEFAULT_PERSONA_NAME = 'Aalis';
|
|
|
45
45
|
* 按 persona 名分堆(key = persona 名),与 selfFacts 的归属方式保持一致:
|
|
46
46
|
* 同名 persona(如 onebot 与 webui 两张都叫 Aalis)共享准则;不同 persona(如 Babel)各自独立。 */
|
|
47
47
|
const INSTRUCTIONS_NS = 'aalis:instructions';
|
|
48
|
-
|
|
48
|
+
const configSchema = {
|
|
49
49
|
extractEveryNMessages: {
|
|
50
50
|
type: 'number',
|
|
51
51
|
label: '每 N 条消息提取一次',
|
|
@@ -212,15 +212,15 @@ const KNOWN_INSTRUCTION_CATEGORIES = [
|
|
|
212
212
|
'安全',
|
|
213
213
|
'其他',
|
|
214
214
|
];
|
|
215
|
-
/** 生成稳定短 ID(6 字符 base36,对 30
|
|
216
|
-
function
|
|
215
|
+
/** 生成稳定短 ID(6 字符 base36,对 30 条以内规模碰撞概率极低);前缀 'f' 为事实、'i' 为指令 */
|
|
216
|
+
function genShortId(prefix, existing) {
|
|
217
217
|
for (let i = 0; i < 8; i++) {
|
|
218
|
-
const id =
|
|
218
|
+
const id = `${prefix}${Math.random().toString(36).slice(2, 7)}`;
|
|
219
219
|
if (!existing.has(id))
|
|
220
220
|
return id;
|
|
221
221
|
}
|
|
222
222
|
// 极端兜底:加时间戳后缀
|
|
223
|
-
return
|
|
223
|
+
return `${prefix}${Date.now().toString(36).slice(-5)}`;
|
|
224
224
|
}
|
|
225
225
|
function decimalPlaces(value) {
|
|
226
226
|
if (!Number.isFinite(value))
|
|
@@ -303,7 +303,29 @@ function renderHistoryForExtract(history, userId, platform) {
|
|
|
303
303
|
.filter(Boolean)
|
|
304
304
|
.join('\n');
|
|
305
305
|
}
|
|
306
|
-
|
|
306
|
+
const uses = {
|
|
307
|
+
memory,
|
|
308
|
+
llm,
|
|
309
|
+
events,
|
|
310
|
+
hooks,
|
|
311
|
+
contributions,
|
|
312
|
+
config,
|
|
313
|
+
logger,
|
|
314
|
+
authority: optional(authority),
|
|
315
|
+
tools: optional(tools),
|
|
316
|
+
commands: optional(commands),
|
|
317
|
+
persona: optional(persona),
|
|
318
|
+
userRelation: optional(userRelation),
|
|
319
|
+
};
|
|
320
|
+
export default definePlugin({
|
|
321
|
+
name: '@aalis/plugin-user-profile',
|
|
322
|
+
displayName: '用户事实档案',
|
|
323
|
+
subsystem: 'memory',
|
|
324
|
+
configSchema,
|
|
325
|
+
uses,
|
|
326
|
+
apply: registerUserProfile,
|
|
327
|
+
});
|
|
328
|
+
function registerUserProfile({ memory, llm, events, hooks, contributions, config, logger, authority, tools, commands, persona, userRelation, }) {
|
|
307
329
|
const cfg = {
|
|
308
330
|
extractEveryNMessages: config.extractEveryNMessages ?? 5,
|
|
309
331
|
historyForExtraction: Math.max(2, config.historyForExtraction ?? 8),
|
|
@@ -350,8 +372,7 @@ export function apply(ctx, config) {
|
|
|
350
372
|
* 若 persona 服务不可用或未配 name,回退到默认 `aalis`。 */
|
|
351
373
|
function getCurrentPersonaName() {
|
|
352
374
|
try {
|
|
353
|
-
const
|
|
354
|
-
const name = persona?.getPersonaName?.()?.trim();
|
|
375
|
+
const name = persona.current?.getPersonaName().trim();
|
|
355
376
|
return name && name.length > 0 ? name : DEFAULT_PERSONA_NAME;
|
|
356
377
|
}
|
|
357
378
|
catch {
|
|
@@ -376,15 +397,6 @@ export function apply(ctx, config) {
|
|
|
376
397
|
function clipInstructionText(s) {
|
|
377
398
|
return s.length > cfg.maxInstructionCharsPerItem ? `${s.slice(0, cfg.maxInstructionCharsPerItem)}…` : s;
|
|
378
399
|
}
|
|
379
|
-
/** 生成 Instruction 短 ID(与 Fact 同前缀但带 'i' 区分,便于日志中识别) */
|
|
380
|
-
function genInstructionId(existing) {
|
|
381
|
-
for (let i = 0; i < 8; i++) {
|
|
382
|
-
const id = `i${Math.random().toString(36).slice(2, 7)}`;
|
|
383
|
-
if (!existing.has(id))
|
|
384
|
-
return id;
|
|
385
|
-
}
|
|
386
|
-
return `i${Date.now().toString(36).slice(-5)}`;
|
|
387
|
-
}
|
|
388
400
|
function parseInstructionArray(raw) {
|
|
389
401
|
const usedIds = new Set();
|
|
390
402
|
const list = [];
|
|
@@ -395,9 +407,9 @@ export function apply(ctx, config) {
|
|
|
395
407
|
const text = typeof obj.text === 'string' ? obj.text.trim() : '';
|
|
396
408
|
if (!text)
|
|
397
409
|
continue;
|
|
398
|
-
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);
|
|
399
411
|
while (usedIds.has(id))
|
|
400
|
-
id =
|
|
412
|
+
id = genShortId('i', usedIds);
|
|
401
413
|
usedIds.add(id);
|
|
402
414
|
const updatedAt = typeof obj.updatedAt === 'number' ? obj.updatedAt : 0;
|
|
403
415
|
const observedAt = typeof obj.observedAt === 'number' ? obj.observedAt : updatedAt || undefined;
|
|
@@ -415,28 +427,35 @@ export function apply(ctx, config) {
|
|
|
415
427
|
}
|
|
416
428
|
return list;
|
|
417
429
|
}
|
|
418
|
-
|
|
419
|
-
|
|
430
|
+
/**
|
|
431
|
+
* 读取指令档案(不存在返回空档案)。读失败原样抛出,理由同 {@link loadProfile};
|
|
432
|
+
* 只读渲染走 {@link loadInstructionsLenient}。
|
|
433
|
+
* mem 由调用方传入:一次操作只认开头取到的实例,理由见 {@link loadProfile}
|
|
434
|
+
*/
|
|
435
|
+
async function loadInstructions(mem) {
|
|
420
436
|
const empty = { instructions: [], updatedAt: 0 };
|
|
421
|
-
if (!
|
|
437
|
+
if (!mem)
|
|
422
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) {
|
|
423
447
|
try {
|
|
424
|
-
|
|
425
|
-
if (!doc)
|
|
426
|
-
return empty;
|
|
427
|
-
const instructions = Array.isArray(doc.instructions) ? parseInstructionArray(doc.instructions) : [];
|
|
428
|
-
return { instructions, updatedAt: typeof doc.updatedAt === 'number' ? doc.updatedAt : 0 };
|
|
448
|
+
return await loadInstructions(mem);
|
|
429
449
|
}
|
|
430
450
|
catch (err) {
|
|
431
|
-
|
|
432
|
-
return
|
|
451
|
+
logger.debug(`加载指令档案失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
452
|
+
return { instructions: [], updatedAt: 0 };
|
|
433
453
|
}
|
|
434
454
|
}
|
|
435
|
-
async function saveInstructions(doc) {
|
|
436
|
-
|
|
437
|
-
if (!memory)
|
|
455
|
+
async function saveInstructions(mem, doc) {
|
|
456
|
+
if (!mem)
|
|
438
457
|
return;
|
|
439
|
-
await
|
|
458
|
+
await mem.saveMetadata(INSTRUCTIONS_NS, getInstructionsKey(), {
|
|
440
459
|
instructions: doc.instructions,
|
|
441
460
|
updatedAt: doc.updatedAt,
|
|
442
461
|
});
|
|
@@ -468,7 +487,7 @@ export function apply(ctx, config) {
|
|
|
468
487
|
const text = clipInstructionText(a.text);
|
|
469
488
|
if (textSet.has(text))
|
|
470
489
|
continue;
|
|
471
|
-
const id =
|
|
490
|
+
const id = genShortId('i', usedIds);
|
|
472
491
|
usedIds.add(id);
|
|
473
492
|
byId.set(id, {
|
|
474
493
|
id,
|
|
@@ -531,70 +550,81 @@ export function apply(ctx, config) {
|
|
|
531
550
|
const s = v.trim();
|
|
532
551
|
return s.length > 0 ? s.slice(0, 40) : undefined;
|
|
533
552
|
}
|
|
534
|
-
/** 解析 metadata
|
|
553
|
+
/** 解析 metadata 中的事实数组;缺正文、id、时效或时间戳的条目跳过 */
|
|
535
554
|
function parseFactArray(raw) {
|
|
536
555
|
const usedIds = new Set();
|
|
537
556
|
const facts = [];
|
|
538
557
|
for (const item of raw) {
|
|
539
|
-
if (typeof item
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
while (usedIds.has(id))
|
|
551
|
-
id = genFactId(usedIds);
|
|
552
|
-
usedIds.add(id);
|
|
553
|
-
const cat = typeof obj.category === 'string' && KNOWN_CATEGORIES.includes(obj.category)
|
|
554
|
-
? obj.category
|
|
555
|
-
: undefined;
|
|
556
|
-
const updatedAt = typeof obj.updatedAt === 'number' ? obj.updatedAt : 0;
|
|
557
|
-
const observedAt = typeof obj.observedAt === 'number' ? obj.observedAt : updatedAt || undefined;
|
|
558
|
-
facts.push({
|
|
559
|
-
id,
|
|
560
|
-
text,
|
|
561
|
-
category: cat,
|
|
562
|
-
temporality: normalizeTemporality(obj.temporality, cat),
|
|
563
|
-
observedAt,
|
|
564
|
-
timeHint: normalizeTextField(obj.timeHint),
|
|
565
|
-
updatedAt,
|
|
566
|
-
});
|
|
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;
|
|
567
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
|
+
});
|
|
568
586
|
}
|
|
569
587
|
return facts;
|
|
570
588
|
}
|
|
571
|
-
/**
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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) {
|
|
601
|
+
if (!mem)
|
|
602
|
+
return undefined;
|
|
603
|
+
const doc = await mem.getMetadata(PROFILE_NS, userKey);
|
|
604
|
+
if (!doc)
|
|
575
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) {
|
|
576
617
|
try {
|
|
577
|
-
|
|
578
|
-
if (!doc)
|
|
579
|
-
return undefined;
|
|
580
|
-
const facts = Array.isArray(doc.facts) ? parseFactArray(doc.facts) : [];
|
|
581
|
-
return {
|
|
582
|
-
facts,
|
|
583
|
-
relationScore: typeof doc.relationScore === 'number' ? Math.min(100, Math.max(0, doc.relationScore)) : 0,
|
|
584
|
-
interactionCount: typeof doc.interactionCount === 'number' ? Math.max(0, Math.floor(doc.interactionCount)) : 0,
|
|
585
|
-
lastInteractionAt: typeof doc.lastInteractionAt === 'number' ? doc.lastInteractionAt : undefined,
|
|
586
|
-
updatedAt: typeof doc.updatedAt === 'number' ? doc.updatedAt : 0,
|
|
587
|
-
};
|
|
618
|
+
return await loadProfile(mem, userKey);
|
|
588
619
|
}
|
|
589
620
|
catch (err) {
|
|
590
|
-
|
|
621
|
+
logger.debug(`加载用户档案失败 (${userKey}): ${err instanceof Error ? err.message : String(err)}`);
|
|
591
622
|
return undefined;
|
|
592
623
|
}
|
|
593
624
|
}
|
|
594
625
|
/** 保存档案(覆盖式) */
|
|
595
|
-
async function saveProfile(userKey, profile) {
|
|
596
|
-
|
|
597
|
-
if (!memory)
|
|
626
|
+
async function saveProfile(mem, userKey, profile) {
|
|
627
|
+
if (!mem)
|
|
598
628
|
return;
|
|
599
629
|
const payload = {
|
|
600
630
|
facts: profile.facts,
|
|
@@ -603,13 +633,67 @@ export function apply(ctx, config) {
|
|
|
603
633
|
lastInteractionAt: profile.lastInteractionAt,
|
|
604
634
|
updatedAt: profile.updatedAt,
|
|
605
635
|
};
|
|
606
|
-
await
|
|
636
|
+
await mem.saveMetadata(PROFILE_NS, userKey, payload);
|
|
607
637
|
}
|
|
608
638
|
function normalizeCategory(v) {
|
|
609
639
|
if (typeof v !== 'string')
|
|
610
640
|
return undefined;
|
|
611
641
|
return KNOWN_CATEGORIES.includes(v) ? v : undefined;
|
|
612
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
|
+
}
|
|
613
697
|
/** 调用 LLM 从历史中提取/修订事实,返回 add / update / remove 三类操作。 */
|
|
614
698
|
async function llmExtractFacts(history, existingFacts, nickname, userId, platform) {
|
|
615
699
|
const empty = {
|
|
@@ -658,7 +742,7 @@ export function apply(ctx, config) {
|
|
|
658
742
|
const user = `# 提取目标\n${who}\n\n# 已知事实(带 id,请在 update/remove 中精确引用 id)\n${factListText}` +
|
|
659
743
|
`\n\n# 会话历史(含多用户,仅供消歧;只能从「目标用户」发言中提取事实)\n${renderedHistory || '(暂无会话历史)'}`;
|
|
660
744
|
// 优先用 cfg.extractLLM 指定的模型;否则取默认 chat-capable LLM。
|
|
661
|
-
const entry = resolveLLMModel(
|
|
745
|
+
const entry = resolveLLMModel(llm, cfg.extractLLM, ['chat']);
|
|
662
746
|
if (!entry)
|
|
663
747
|
return empty;
|
|
664
748
|
const extractLlm = entry.instance;
|
|
@@ -680,7 +764,7 @@ export function apply(ctx, config) {
|
|
|
680
764
|
if (!parsed) {
|
|
681
765
|
// util-json-repair 已尝试剥 fence + 修裸引号 + 补括号;仍失败 → 模型多半
|
|
682
766
|
// 写成了纯文本/markdown。给一次显式反馈再来一次,避免一次失败丢失整批事实。
|
|
683
|
-
|
|
767
|
+
logger.warn(`[user-profile] LLM 输出无法解析为 JSON,尝试重试一次。原文前 200 字:${text.slice(0, 200)}`);
|
|
684
768
|
const retryResp = await extractLlm.chat({
|
|
685
769
|
messages: [
|
|
686
770
|
...baseMessages,
|
|
@@ -700,61 +784,12 @@ export function apply(ctx, config) {
|
|
|
700
784
|
text = (retryResp.content ?? '').trim();
|
|
701
785
|
({ parsed } = parseLLMJsonObject(text));
|
|
702
786
|
if (!parsed) {
|
|
703
|
-
|
|
787
|
+
logger.warn(`[user-profile] LLM 重试后仍无法解析 JSON,放弃本批次。重试原文前 200 字:${text.slice(0, 200)}`);
|
|
704
788
|
return empty;
|
|
705
789
|
}
|
|
706
|
-
|
|
790
|
+
logger.debug('[user-profile] LLM 重试后解析成功');
|
|
707
791
|
}
|
|
708
|
-
const
|
|
709
|
-
const add = Array.isArray(parsedObj.add)
|
|
710
|
-
? parsedObj.add.flatMap(x => {
|
|
711
|
-
if (!x || typeof x !== 'object')
|
|
712
|
-
return [];
|
|
713
|
-
const o = x;
|
|
714
|
-
const t = typeof o.text === 'string' ? o.text.trim() : '';
|
|
715
|
-
if (!t)
|
|
716
|
-
return [];
|
|
717
|
-
const category = normalizeCategory(o.category);
|
|
718
|
-
const sourceQuote = typeof o.sourceQuote === 'string' ? o.sourceQuote.trim() : '';
|
|
719
|
-
return [
|
|
720
|
-
{
|
|
721
|
-
text: t,
|
|
722
|
-
category,
|
|
723
|
-
temporality: normalizeTemporality(o.temporality, category),
|
|
724
|
-
timeHint: normalizeTextField(o.timeHint),
|
|
725
|
-
sourceQuote,
|
|
726
|
-
},
|
|
727
|
-
];
|
|
728
|
-
})
|
|
729
|
-
: [];
|
|
730
|
-
const update = Array.isArray(parsedObj.update)
|
|
731
|
-
? parsedObj.update.flatMap(x => {
|
|
732
|
-
if (!x || typeof x !== 'object')
|
|
733
|
-
return [];
|
|
734
|
-
const o = x;
|
|
735
|
-
const id = typeof o.id === 'string' ? o.id.trim() : '';
|
|
736
|
-
const t = typeof o.text === 'string' ? o.text.trim() : '';
|
|
737
|
-
if (!id || !t)
|
|
738
|
-
return [];
|
|
739
|
-
const category = normalizeCategory(o.category);
|
|
740
|
-
const sourceQuote = typeof o.sourceQuote === 'string' ? o.sourceQuote.trim() : '';
|
|
741
|
-
return [
|
|
742
|
-
{
|
|
743
|
-
id,
|
|
744
|
-
text: t,
|
|
745
|
-
category,
|
|
746
|
-
temporality: normalizeTemporality(o.temporality, category),
|
|
747
|
-
timeHint: normalizeTextField(o.timeHint),
|
|
748
|
-
sourceQuote,
|
|
749
|
-
},
|
|
750
|
-
];
|
|
751
|
-
})
|
|
752
|
-
: [];
|
|
753
|
-
const remove = Array.isArray(parsedObj.remove)
|
|
754
|
-
? parsedObj.remove
|
|
755
|
-
.filter((x) => typeof x === 'string' && x.trim().length > 0)
|
|
756
|
-
.map(s => s.trim())
|
|
757
|
-
: [];
|
|
792
|
+
const { add, update, remove } = parseFactOps(parsed);
|
|
758
793
|
// sourceQuote 校验:quote 可能跨多条消息(LLM 用 \n 拼接),
|
|
759
794
|
// 因此按行拆分,每行(非空)都必须能在「目标用户」语料中找到 normalize 后的子串。
|
|
760
795
|
// 这样即使行与行之间夹着其他用户的消息也不会被错误拒收。
|
|
@@ -762,7 +797,7 @@ export function apply(ctx, config) {
|
|
|
762
797
|
const validateSourceQuote = (item, kind) => {
|
|
763
798
|
const q = item.sourceQuote?.trim() ?? '';
|
|
764
799
|
if (!q) {
|
|
765
|
-
|
|
800
|
+
logger.warn(`[user-profile] 丢弃 ${kind} fact(未提供 sourceQuote): ${item.text}`);
|
|
766
801
|
return false;
|
|
767
802
|
}
|
|
768
803
|
const lines = q
|
|
@@ -775,7 +810,7 @@ export function apply(ctx, config) {
|
|
|
775
810
|
if (!normalized)
|
|
776
811
|
continue;
|
|
777
812
|
if (!targetCorpus.includes(normalized)) {
|
|
778
|
-
|
|
813
|
+
logger.warn(`[user-profile] 丢弃 ${kind} fact(sourceQuote 片段不在目标用户 ${userId} 发言中): text="${item.text}" fragment="${frag}"`);
|
|
779
814
|
return false;
|
|
780
815
|
}
|
|
781
816
|
}
|
|
@@ -783,20 +818,10 @@ export function apply(ctx, config) {
|
|
|
783
818
|
};
|
|
784
819
|
const validatedAdd = add.filter(item => validateSourceQuote(item, 'add'));
|
|
785
820
|
const validatedUpdate = update.filter(item => validateSourceQuote(item, 'update'));
|
|
786
|
-
|
|
787
|
-
const stripQuote = (item) => {
|
|
788
|
-
const { sourceQuote: _omit, ...rest } = item;
|
|
789
|
-
void _omit;
|
|
790
|
-
return rest;
|
|
791
|
-
};
|
|
792
|
-
return {
|
|
793
|
-
add: validatedAdd.map(stripQuote),
|
|
794
|
-
update: validatedUpdate.map(stripQuote),
|
|
795
|
-
remove,
|
|
796
|
-
};
|
|
821
|
+
return { add: validatedAdd, update: validatedUpdate, remove };
|
|
797
822
|
}
|
|
798
823
|
catch (err) {
|
|
799
|
-
|
|
824
|
+
logger.debug(`事实提取 LLM 调用失败:${err instanceof Error ? err.message : String(err)}`);
|
|
800
825
|
return empty;
|
|
801
826
|
}
|
|
802
827
|
}
|
|
@@ -857,14 +882,14 @@ export function apply(ctx, config) {
|
|
|
857
882
|
id: u.id,
|
|
858
883
|
text,
|
|
859
884
|
category: u.category ?? old.category,
|
|
860
|
-
temporality: u.temporality ?? old.temporality
|
|
861
|
-
observedAt: old.observedAt
|
|
885
|
+
temporality: u.temporality ?? old.temporality,
|
|
886
|
+
observedAt: old.observedAt,
|
|
862
887
|
timeHint: u.timeHint ?? old.timeHint,
|
|
863
888
|
updatedAt: now,
|
|
864
889
|
});
|
|
865
890
|
}
|
|
866
891
|
else {
|
|
867
|
-
const id =
|
|
892
|
+
const id = genShortId('f', usedIds);
|
|
868
893
|
usedIds.add(id);
|
|
869
894
|
byId.set(id, {
|
|
870
895
|
id,
|
|
@@ -883,7 +908,7 @@ export function apply(ctx, config) {
|
|
|
883
908
|
const text = clipText(a.text);
|
|
884
909
|
if (textSet.has(text))
|
|
885
910
|
continue;
|
|
886
|
-
const id =
|
|
911
|
+
const id = genShortId('f', usedIds);
|
|
887
912
|
usedIds.add(id);
|
|
888
913
|
byId.set(id, {
|
|
889
914
|
id,
|
|
@@ -905,9 +930,14 @@ export function apply(ctx, config) {
|
|
|
905
930
|
merged.sort((a, b) => a.updatedAt - b.updatedAt);
|
|
906
931
|
return merged;
|
|
907
932
|
}
|
|
908
|
-
/**
|
|
909
|
-
|
|
910
|
-
|
|
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));
|
|
911
941
|
}
|
|
912
942
|
/**
|
|
913
943
|
* 后台触发一次事实提取(并发互斥)。
|
|
@@ -919,38 +949,34 @@ export function apply(ctx, config) {
|
|
|
919
949
|
if (inflightExtractions.has(userKey))
|
|
920
950
|
return;
|
|
921
951
|
inflightExtractions.add(userKey);
|
|
922
|
-
const
|
|
952
|
+
const mem = memory.current;
|
|
923
953
|
try {
|
|
924
|
-
if (!
|
|
954
|
+
if (!mem)
|
|
925
955
|
return;
|
|
926
|
-
const rawHistory = await
|
|
956
|
+
const rawHistory = await mem.getHistory(sessionId, cfg.historyForExtraction);
|
|
927
957
|
// 跨会话委派是另一个 agent 实例发出的指令·不是该用户发言,不能作为他的用户存档提取语料
|
|
928
958
|
const history = rawHistory.filter(m => m.kind !== WellKnownKinds.CrossSessionDelegation);
|
|
929
959
|
// 序列中至少需要一条目标用户发言,否则没有可提取语料
|
|
930
960
|
if (!history.some(m => isTargetUserMessage(m, userId, platform)))
|
|
931
961
|
return;
|
|
932
|
-
const
|
|
933
|
-
|
|
934
|
-
relationScore: 0,
|
|
935
|
-
interactionCount: 0,
|
|
936
|
-
updatedAt: 0,
|
|
937
|
-
};
|
|
938
|
-
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);
|
|
939
964
|
const hasFactOps = ops.add.length > 0 || ops.update.length > 0 || ops.remove.length > 0;
|
|
940
965
|
if (!hasFactOps)
|
|
941
966
|
return;
|
|
942
|
-
|
|
943
|
-
//
|
|
944
|
-
const
|
|
945
|
-
|
|
946
|
-
|
|
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,
|
|
947
973
|
facts: newFacts,
|
|
948
974
|
updatedAt: Date.now(),
|
|
949
975
|
});
|
|
950
|
-
|
|
976
|
+
logger.debug(`用户档案已更新 (${userKey}): facts +${ops.add.length} ~${ops.update.length} -${ops.remove.length} → ${newFacts.length} 条`);
|
|
951
977
|
}
|
|
952
978
|
catch (err) {
|
|
953
|
-
|
|
979
|
+
logger.debug(`事实提取失败 (${userKey}): ${err instanceof Error ? err.message : String(err)}`);
|
|
954
980
|
}
|
|
955
981
|
finally {
|
|
956
982
|
inflightExtractions.delete(userKey);
|
|
@@ -1016,7 +1042,7 @@ export function apply(ctx, config) {
|
|
|
1016
1042
|
.filter(Boolean)
|
|
1017
1043
|
.join('\n');
|
|
1018
1044
|
const user = `# 已知的关于 Aalis 自己的事实(带 id)\n${factListText}\n\n# 最近对话历史\n${renderedHistory || '(暂无)'}`;
|
|
1019
|
-
const entry = resolveLLMModel(
|
|
1045
|
+
const entry = resolveLLMModel(llm, cfg.extractLLM, ['chat']);
|
|
1020
1046
|
if (!entry)
|
|
1021
1047
|
return empty;
|
|
1022
1048
|
try {
|
|
@@ -1034,59 +1060,13 @@ export function apply(ctx, config) {
|
|
|
1034
1060
|
return empty;
|
|
1035
1061
|
const { parsed } = parseLLMJsonObject(text);
|
|
1036
1062
|
if (!parsed) {
|
|
1037
|
-
|
|
1063
|
+
logger.warn(`[user-profile] 自反思 LLM 输出无法解析为 JSON,原文前 200 字:${text.slice(0, 200)}`);
|
|
1038
1064
|
return empty;
|
|
1039
1065
|
}
|
|
1040
|
-
|
|
1041
|
-
const add = Array.isArray(obj.add)
|
|
1042
|
-
? obj.add.flatMap(x => {
|
|
1043
|
-
if (!x || typeof x !== 'object')
|
|
1044
|
-
return [];
|
|
1045
|
-
const o = x;
|
|
1046
|
-
const t = typeof o.text === 'string' ? o.text.trim() : '';
|
|
1047
|
-
if (!t)
|
|
1048
|
-
return [];
|
|
1049
|
-
const category = normalizeCategory(o.category);
|
|
1050
|
-
return [
|
|
1051
|
-
{
|
|
1052
|
-
text: t,
|
|
1053
|
-
category,
|
|
1054
|
-
temporality: normalizeTemporality(o.temporality, category),
|
|
1055
|
-
timeHint: normalizeTextField(o.timeHint),
|
|
1056
|
-
},
|
|
1057
|
-
];
|
|
1058
|
-
})
|
|
1059
|
-
: [];
|
|
1060
|
-
const update = Array.isArray(obj.update)
|
|
1061
|
-
? obj.update.flatMap(x => {
|
|
1062
|
-
if (!x || typeof x !== 'object')
|
|
1063
|
-
return [];
|
|
1064
|
-
const o = x;
|
|
1065
|
-
const id = typeof o.id === 'string' ? o.id.trim() : '';
|
|
1066
|
-
const t = typeof o.text === 'string' ? o.text.trim() : '';
|
|
1067
|
-
if (!id || !t)
|
|
1068
|
-
return [];
|
|
1069
|
-
const category = normalizeCategory(o.category);
|
|
1070
|
-
return [
|
|
1071
|
-
{
|
|
1072
|
-
id,
|
|
1073
|
-
text: t,
|
|
1074
|
-
category,
|
|
1075
|
-
temporality: normalizeTemporality(o.temporality, category),
|
|
1076
|
-
timeHint: normalizeTextField(o.timeHint),
|
|
1077
|
-
},
|
|
1078
|
-
];
|
|
1079
|
-
})
|
|
1080
|
-
: [];
|
|
1081
|
-
const remove = Array.isArray(obj.remove)
|
|
1082
|
-
? obj.remove
|
|
1083
|
-
.filter((x) => typeof x === 'string' && x.trim().length > 0)
|
|
1084
|
-
.map(s => s.trim())
|
|
1085
|
-
: [];
|
|
1086
|
-
return { ...empty, add, update, remove };
|
|
1066
|
+
return parseFactOps(parsed);
|
|
1087
1067
|
}
|
|
1088
1068
|
catch (err) {
|
|
1089
|
-
|
|
1069
|
+
logger.debug(`自反思 LLM 调用失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1090
1070
|
return empty;
|
|
1091
1071
|
}
|
|
1092
1072
|
}
|
|
@@ -1098,31 +1078,27 @@ export function apply(ctx, config) {
|
|
|
1098
1078
|
return;
|
|
1099
1079
|
selfReflectionInflight = true;
|
|
1100
1080
|
try {
|
|
1101
|
-
const
|
|
1102
|
-
if (!
|
|
1081
|
+
const mem = memory.current;
|
|
1082
|
+
if (!mem)
|
|
1103
1083
|
return;
|
|
1104
|
-
const rawHistory = await
|
|
1084
|
+
const rawHistory = await mem.getHistory(sessionId, cfg.selfReflectHistory);
|
|
1105
1085
|
const history = rawHistory.filter(m => m.kind !== WellKnownKinds.CrossSessionDelegation);
|
|
1106
1086
|
// 至少需要一些 assistant 发言作为"自反思"的材料
|
|
1107
1087
|
if (!history.some(m => m.role === 'assistant' && m.content))
|
|
1108
1088
|
return;
|
|
1109
1089
|
const selfKey = getSelfKey();
|
|
1110
|
-
const
|
|
1111
|
-
|
|
1112
|
-
relationScore: 0,
|
|
1113
|
-
interactionCount: 0,
|
|
1114
|
-
updatedAt: 0,
|
|
1115
|
-
};
|
|
1116
|
-
const ops = await llmReflectSelf(history, profile.facts);
|
|
1090
|
+
const snapshotFacts = (await loadProfile(mem, selfKey))?.facts ?? [];
|
|
1091
|
+
const ops = await llmReflectSelf(history, snapshotFacts);
|
|
1117
1092
|
if (ops.add.length === 0 && ops.update.length === 0 && ops.remove.length === 0)
|
|
1118
1093
|
return;
|
|
1119
|
-
|
|
1120
|
-
const fresh = (await loadProfile(selfKey)) ??
|
|
1121
|
-
|
|
1122
|
-
|
|
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() });
|
|
1098
|
+
logger.debug(`Aalis 自档案已更新 (${selfKey}): +${ops.add.length} ~${ops.update.length} -${ops.remove.length} → ${newFacts.length} 条`);
|
|
1123
1099
|
}
|
|
1124
1100
|
catch (err) {
|
|
1125
|
-
|
|
1101
|
+
logger.debug(`自反思失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1126
1102
|
}
|
|
1127
1103
|
finally {
|
|
1128
1104
|
selfReflectionInflight = false;
|
|
@@ -1207,7 +1183,7 @@ export function apply(ctx, config) {
|
|
|
1207
1183
|
.join('\n')
|
|
1208
1184
|
: '(暂无)';
|
|
1209
1185
|
const user = `# 当前 persona\n${getCurrentPersonaName()}\n\n# 已有指令(带 id,可在 update/remove 中精确引用)\n${existingText}\n\n# 最近会话(仅含授权人 + Aalis + 系统通知)\n${rendered}`;
|
|
1210
|
-
const entry = resolveLLMModel(
|
|
1186
|
+
const entry = resolveLLMModel(llm, cfg.extractLLM, ['chat']);
|
|
1211
1187
|
if (!entry)
|
|
1212
1188
|
return empty;
|
|
1213
1189
|
try {
|
|
@@ -1225,7 +1201,7 @@ export function apply(ctx, config) {
|
|
|
1225
1201
|
return empty;
|
|
1226
1202
|
const { parsed } = parseLLMJsonObject(text);
|
|
1227
1203
|
if (!parsed) {
|
|
1228
|
-
|
|
1204
|
+
logger.warn(`[user-profile] 指令提取 LLM 输出无法解析为 JSON,原文前 200 字:${text.slice(0, 200)}`);
|
|
1229
1205
|
return empty;
|
|
1230
1206
|
}
|
|
1231
1207
|
const obj = parsed;
|
|
@@ -1243,7 +1219,7 @@ export function apply(ctx, config) {
|
|
|
1243
1219
|
const srcKey = typeof o.sourceUserKey === 'string' ? o.sourceUserKey.trim() : '';
|
|
1244
1220
|
// 拒收:声称的 sourceUserKey 不在授权人集合里 → 说明 LLM 编造或越权采纳
|
|
1245
1221
|
if (!srcKey || !allowedUserKeys.has(srcKey)) {
|
|
1246
|
-
|
|
1222
|
+
logger.warn(`[user-profile] 丢弃 instruction add(sourceUserKey 不在授权人列表中): text="${t}" claimed="${srcKey}"`);
|
|
1247
1223
|
return [];
|
|
1248
1224
|
}
|
|
1249
1225
|
const srcName = typeof o.sourceUserName === 'string' && o.sourceUserName.trim() ? o.sourceUserName.trim() : '';
|
|
@@ -1286,7 +1262,7 @@ export function apply(ctx, config) {
|
|
|
1286
1262
|
return { add, update, remove };
|
|
1287
1263
|
}
|
|
1288
1264
|
catch (err) {
|
|
1289
|
-
|
|
1265
|
+
logger.debug(`指令提取 LLM 调用失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1290
1266
|
return empty;
|
|
1291
1267
|
}
|
|
1292
1268
|
}
|
|
@@ -1298,53 +1274,58 @@ export function apply(ctx, config) {
|
|
|
1298
1274
|
return;
|
|
1299
1275
|
if (instructionExtractionInflight)
|
|
1300
1276
|
return;
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1277
|
+
// 指令只采信有权下指令的人,门槛是发言人的数字权限等级(owner 视为无穷大)
|
|
1278
|
+
const auth = authority.current;
|
|
1279
|
+
if (!auth)
|
|
1304
1280
|
return;
|
|
1305
|
-
|
|
1306
|
-
|
|
1281
|
+
const authorityFn = (platform, userId) => auth.isOwner(platform, userId)
|
|
1282
|
+
? Number.POSITIVE_INFINITY
|
|
1283
|
+
: (auth.listUsers().find(u => u.platform === platform && u.userId === userId)?.level ?? DEFAULT_AUTHORITY);
|
|
1307
1284
|
instructionExtractionInflight = true;
|
|
1308
1285
|
try {
|
|
1309
|
-
const
|
|
1310
|
-
if (!
|
|
1286
|
+
const mem = memory.current;
|
|
1287
|
+
if (!mem)
|
|
1311
1288
|
return;
|
|
1312
|
-
const rawHistory = await
|
|
1289
|
+
const rawHistory = await mem.getHistory(sessionId, cfg.instructionHistoryForExtraction);
|
|
1313
1290
|
const history = rawHistory.filter(m => m.kind !== WellKnownKinds.CrossSessionDelegation);
|
|
1314
1291
|
if (history.length === 0)
|
|
1315
1292
|
return;
|
|
1316
|
-
const doc = await loadInstructions();
|
|
1293
|
+
const doc = await loadInstructions(mem);
|
|
1317
1294
|
const ops = await llmExtractInstructions(history, doc.instructions, authorityFn);
|
|
1318
1295
|
if (ops.add.length === 0 && ops.update.length === 0 && ops.remove.length === 0)
|
|
1319
1296
|
return;
|
|
1320
|
-
|
|
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 => {
|
|
1321
1301
|
const { id, ...patch } = u;
|
|
1322
1302
|
return { id, patch };
|
|
1323
1303
|
}), ops.remove);
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
...fresh,
|
|
1327
|
-
instructions: newInstructions,
|
|
1328
|
-
updatedAt: Date.now(),
|
|
1329
|
-
});
|
|
1330
|
-
ctx.logger.debug(`指令档案已更新 (${getInstructionsKey()}): +${ops.add.length} ~${ops.update.length} -${ops.remove.length} → ${newInstructions.length} 条`);
|
|
1304
|
+
await saveInstructions(mem, { instructions: newInstructions, updatedAt: Date.now() });
|
|
1305
|
+
logger.debug(`指令档案已更新 (${getInstructionsKey()}): +${ops.add.length} ~${ops.update.length} -${ops.remove.length} → ${newInstructions.length} 条`);
|
|
1331
1306
|
}
|
|
1332
1307
|
catch (err) {
|
|
1333
|
-
|
|
1308
|
+
logger.debug(`指令提取失败:${err instanceof Error ? err.message : String(err)}`);
|
|
1334
1309
|
}
|
|
1335
1310
|
finally {
|
|
1336
1311
|
instructionExtractionInflight = false;
|
|
1337
1312
|
}
|
|
1338
1313
|
}
|
|
1339
1314
|
async function updateRelationForUser(userKey, triggerType, options) {
|
|
1340
|
-
const
|
|
1341
|
-
|
|
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));
|
|
1342
1323
|
}
|
|
1343
1324
|
// ─── 关系分数:在 agent 触发回复路径上更新 ───
|
|
1344
1325
|
// 注:钩子无优先级机制,同相位 handler 按插件激活序执行;本 handler 只做副作用
|
|
1345
1326
|
// (更新关系分数)不改消息,与其它 handler 顺序无关。
|
|
1346
1327
|
// 关系强度与"是否触发回复"绑定,因此仍走 agent:input:before 中间件。
|
|
1347
|
-
|
|
1328
|
+
hooks.middleware('agent:input:before', async (data, next) => {
|
|
1348
1329
|
const { userId, platform, triggerType } = data.message;
|
|
1349
1330
|
if (userId) {
|
|
1350
1331
|
const userKey = userKeyOf(platform, userId);
|
|
@@ -1354,7 +1335,7 @@ export function apply(ctx, config) {
|
|
|
1354
1335
|
await updateRelationForUser(userKey, triggerType, { countInteraction: false });
|
|
1355
1336
|
}
|
|
1356
1337
|
catch (err) {
|
|
1357
|
-
|
|
1338
|
+
logger.debug(`关系强度更新异常 (${userKey}): ${err instanceof Error ? err.message : String(err)}`);
|
|
1358
1339
|
}
|
|
1359
1340
|
}
|
|
1360
1341
|
await next();
|
|
@@ -1362,8 +1343,7 @@ export function apply(ctx, config) {
|
|
|
1362
1343
|
// ─── 事实提取触发:每条入站消息落库后立即计数,与 agent 是否回复无关 ───
|
|
1363
1344
|
// 监听 message-archive 在 archiveIncoming 落库成功后发出的 inbound:message:archived 事件,
|
|
1364
1345
|
// 确保缓冲消息(onebot saveBufferedMessage 等不触发 agent 回复的路径)也能纳入计数。
|
|
1365
|
-
|
|
1366
|
-
const data = args[0];
|
|
1346
|
+
events.on('inbound:message:archived', data => {
|
|
1367
1347
|
const { sessionId, incoming } = data;
|
|
1368
1348
|
const { userId, platform, nickname } = incoming;
|
|
1369
1349
|
if (!userId)
|
|
@@ -1374,25 +1354,25 @@ export function apply(ctx, config) {
|
|
|
1374
1354
|
// lastInteractionAt 的唯一写点,设门会连带关掉计数与时戳(衰减基准、最近互动
|
|
1375
1355
|
// 排序全失效)。增量为 0 时 relationIncrementFor 自然返回 0,不加分即可。
|
|
1376
1356
|
const userKey = userKeyOf(platform, userId);
|
|
1377
|
-
void updateRelationForUser(userKey, 'witness').catch((err) =>
|
|
1357
|
+
void updateRelationForUser(userKey, 'witness').catch((err) => logger.debug(`witness 关系更新异常 (${userKey}): ${err instanceof Error ? err.message : String(err)}`));
|
|
1378
1358
|
const countKey = extractionCountKeyOf(sessionId, platform, userId);
|
|
1379
1359
|
const count = (userMessageCount.get(countKey) ?? 0) + 1;
|
|
1380
1360
|
userMessageCount.set(countKey, count);
|
|
1381
1361
|
if (cfg.extractEveryNMessages > 0 && count % cfg.extractEveryNMessages === 0) {
|
|
1382
|
-
void triggerExtractionForUser(sessionId, userId, platform ?? '', nickname).catch((err) =>
|
|
1362
|
+
void triggerExtractionForUser(sessionId, userId, platform ?? '', nickname).catch((err) => logger.debug(`事实提取异常 (${countKey}): ${err instanceof Error ? err.message : String(err)}`));
|
|
1383
1363
|
}
|
|
1384
1364
|
// 全局自反思:跨会话累计所有入站消息计数,每 N 触发一次。
|
|
1385
1365
|
if (cfg.enableSelfProfile && cfg.selfReflectEveryNMessages > 0) {
|
|
1386
1366
|
globalInboundCount += 1;
|
|
1387
1367
|
if (globalInboundCount % cfg.selfReflectEveryNMessages === 0) {
|
|
1388
|
-
void triggerSelfReflection(sessionId).catch((err) =>
|
|
1368
|
+
void triggerSelfReflection(sessionId).catch((err) => logger.debug(`自反思异常: ${err instanceof Error ? err.message : String(err)}`));
|
|
1389
1369
|
}
|
|
1390
1370
|
}
|
|
1391
1371
|
// 指令提取:独立计数器,与自反思解耦。频率默认更慢(40 条),可单独禁用。
|
|
1392
1372
|
if (cfg.enableInstructions && cfg.instructionExtractEveryNMessages > 0) {
|
|
1393
1373
|
globalInstructionCount += 1;
|
|
1394
1374
|
if (globalInstructionCount % cfg.instructionExtractEveryNMessages === 0) {
|
|
1395
|
-
void triggerInstructionExtraction(sessionId).catch((err) =>
|
|
1375
|
+
void triggerInstructionExtraction(sessionId).catch((err) => logger.debug(`指令提取异常: ${err instanceof Error ? err.message : String(err)}`));
|
|
1396
1376
|
}
|
|
1397
1377
|
}
|
|
1398
1378
|
});
|
|
@@ -1401,10 +1381,7 @@ export function apply(ctx, config) {
|
|
|
1401
1381
|
return true;
|
|
1402
1382
|
if (cfg.temporaryFactMaxAgeDays <= 0)
|
|
1403
1383
|
return true;
|
|
1404
|
-
|
|
1405
|
-
if (!base)
|
|
1406
|
-
return true;
|
|
1407
|
-
return Date.now() - base <= cfg.temporaryFactMaxAgeDays * 86_400_000;
|
|
1384
|
+
return Date.now() - fact.updatedAt <= cfg.temporaryFactMaxAgeDays * 86_400_000;
|
|
1408
1385
|
}
|
|
1409
1386
|
function renderFactLine(fact, includeMeta) {
|
|
1410
1387
|
if (!includeMeta)
|
|
@@ -1412,12 +1389,10 @@ export function apply(ctx, config) {
|
|
|
1412
1389
|
const meta = [];
|
|
1413
1390
|
if (fact.timeHint)
|
|
1414
1391
|
meta.push(`时间线索:${fact.timeHint}`);
|
|
1415
|
-
|
|
1416
|
-
if (observed)
|
|
1417
|
-
meta.push(`记录于:${new Date(observed).toLocaleDateString('zh-CN')}`);
|
|
1392
|
+
meta.push(`记录于:${new Date(fact.observedAt).toLocaleDateString('zh-CN')}`);
|
|
1418
1393
|
if (fact.temporality === 'temporary')
|
|
1419
1394
|
meta.push('临时状态');
|
|
1420
|
-
return
|
|
1395
|
+
return `- ${fact.text}(${meta.join(';')})`;
|
|
1421
1396
|
}
|
|
1422
1397
|
/** 将一个用户的 Fact[] 渲染为分组文本块 */
|
|
1423
1398
|
function renderProfileBlock(facts, label, compact) {
|
|
@@ -1462,19 +1437,20 @@ export function apply(ctx, config) {
|
|
|
1462
1437
|
// interval → 无主发言者(只是恰好撞上频率),所有参与者一律 compact 摘要
|
|
1463
1438
|
// idle → 无 userId,只注入历史 messages 中出现的参与者 compact 摘要
|
|
1464
1439
|
// 多块返回保序共键:准则 → 自档案 → 主发言者档案 → 其他参与者。
|
|
1465
|
-
|
|
1440
|
+
contributions.contribute('agent:prompt', {
|
|
1466
1441
|
id: 'user-profile',
|
|
1467
1442
|
anchor: 'turn-context',
|
|
1468
1443
|
async build(data) {
|
|
1469
1444
|
// 干跑(token 快照)跳过档案加载——该路径 userId 为空串,加载既昂贵又无意义
|
|
1470
1445
|
if (data.dryRun)
|
|
1471
1446
|
return null;
|
|
1447
|
+
const mem = memory.current;
|
|
1472
1448
|
const blocksToInsert = [];
|
|
1473
1449
|
const trigger = data.triggerType ?? 'direct';
|
|
1474
1450
|
const hasPrimarySpeaker = trigger === 'direct' || trigger === 'immediate';
|
|
1475
1451
|
// 0a. 第三方行为指令(最高优先级):作为不可违逆的行为约束放在 selfFacts 之前
|
|
1476
1452
|
if (cfg.enableInstructions) {
|
|
1477
|
-
const insDoc = await
|
|
1453
|
+
const insDoc = await loadInstructionsLenient(mem);
|
|
1478
1454
|
if (insDoc.instructions.length > 0) {
|
|
1479
1455
|
const body = renderInstructionsBlock(insDoc.instructions);
|
|
1480
1456
|
const insBlock = '# 第三方行为准则(最高优先)\n' +
|
|
@@ -1490,7 +1466,7 @@ export function apply(ctx, config) {
|
|
|
1490
1466
|
}
|
|
1491
1467
|
// 0. Aalis 自档案:永远尝试注入到最前段,作为人格延续锚点
|
|
1492
1468
|
if (cfg.enableSelfProfile) {
|
|
1493
|
-
const selfProfile = await
|
|
1469
|
+
const selfProfile = await loadProfileLenient(mem, getSelfKey());
|
|
1494
1470
|
const activeSelfFacts = selfProfile?.facts.filter(isFactActive) ?? [];
|
|
1495
1471
|
if (activeSelfFacts.length > 0) {
|
|
1496
1472
|
const body = renderProfileBlock(activeSelfFacts, 'Aalis', false);
|
|
@@ -1505,16 +1481,16 @@ export function apply(ctx, config) {
|
|
|
1505
1481
|
// 1. 主发言者完整档案:仅在确实有人在「和 Aalis 对话」时注入
|
|
1506
1482
|
if (hasPrimarySpeaker && data.userId) {
|
|
1507
1483
|
const userKey = userKeyOf(data.platform, data.userId);
|
|
1508
|
-
const profile = await
|
|
1484
|
+
const profile = await loadProfileLenient(mem, userKey);
|
|
1509
1485
|
if (profile?.facts.some(isFactActive)) {
|
|
1510
1486
|
const body = renderProfileBlock(profile.facts, data.userId, false);
|
|
1511
1487
|
const relationLine = renderRelationLine(profile);
|
|
1512
|
-
// 可选:从 user-relation
|
|
1488
|
+
// 可选:从 user-relation 服务取「同社群活跃成员」(按配置的社群发现算法所得标签 + PageRank 排序)。
|
|
1513
1489
|
// 服务不存在 / 节点没社群标签 / 报错都静默跳过;该字段是锦上添花,不破坏档案主体。
|
|
1514
1490
|
let communityLine = '';
|
|
1515
1491
|
try {
|
|
1516
|
-
const relation =
|
|
1517
|
-
if (relation
|
|
1492
|
+
const relation = userRelation.current;
|
|
1493
|
+
if (relation) {
|
|
1518
1494
|
const r = await relation.getCommunityPeers(userKey, 5);
|
|
1519
1495
|
if (r.peers.length > 0) {
|
|
1520
1496
|
const names = r.peers.map(p => p.displayName).join('、');
|
|
@@ -1523,7 +1499,7 @@ export function apply(ctx, config) {
|
|
|
1523
1499
|
}
|
|
1524
1500
|
}
|
|
1525
1501
|
catch (err) {
|
|
1526
|
-
|
|
1502
|
+
logger.debug(`读取 community peers 失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
1527
1503
|
}
|
|
1528
1504
|
const block = `# 关于当前对话者(${data.userId})的已知事实\n` +
|
|
1529
1505
|
'以下是你跨会话长期积累的关于该用户的事实,用于让回应更自然贴合其个性。' +
|
|
@@ -1572,10 +1548,9 @@ export function apply(ctx, config) {
|
|
|
1572
1548
|
break;
|
|
1573
1549
|
}
|
|
1574
1550
|
if (cfg.allowGlobalBackfill && others.size < candidateLimit) {
|
|
1575
|
-
|
|
1576
|
-
if (memory) {
|
|
1551
|
+
if (mem) {
|
|
1577
1552
|
try {
|
|
1578
|
-
const globalRecent = await
|
|
1553
|
+
const globalRecent = await mem.listMetadata(PROFILE_NS);
|
|
1579
1554
|
globalRecent.sort((a, b) => {
|
|
1580
1555
|
const at = typeof a.data.lastInteractionAt === 'number' ? a.data.lastInteractionAt : 0;
|
|
1581
1556
|
const bt = typeof b.data.lastInteractionAt === 'number' ? b.data.lastInteractionAt : 0;
|
|
@@ -1596,20 +1571,14 @@ export function apply(ctx, config) {
|
|
|
1596
1571
|
}
|
|
1597
1572
|
}
|
|
1598
1573
|
catch (err) {
|
|
1599
|
-
|
|
1574
|
+
logger.debug(`加载最近互动用户失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
1600
1575
|
}
|
|
1601
1576
|
}
|
|
1602
1577
|
}
|
|
1603
1578
|
if (others.size > 0) {
|
|
1604
1579
|
const snippets = [];
|
|
1605
1580
|
for (const [key, info] of others) {
|
|
1606
|
-
|
|
1607
|
-
try {
|
|
1608
|
-
profile = await loadProfile(key);
|
|
1609
|
-
}
|
|
1610
|
-
catch {
|
|
1611
|
-
/* 静默跳过 */
|
|
1612
|
-
}
|
|
1581
|
+
const profile = await loadProfileLenient(mem, key);
|
|
1613
1582
|
if (!profile?.facts.some(isFactActive))
|
|
1614
1583
|
continue;
|
|
1615
1584
|
const label = info.nickname ? `${info.nickname}(${info.userId})` : info.userId;
|
|
@@ -1634,7 +1603,7 @@ export function apply(ctx, config) {
|
|
|
1634
1603
|
},
|
|
1635
1604
|
});
|
|
1636
1605
|
// ─── 参与统一的 memory:clear ───
|
|
1637
|
-
|
|
1606
|
+
hooks.middleware('memory:clear', async (data, next) => {
|
|
1638
1607
|
if (data.types && !data.types.includes('user-profile')) {
|
|
1639
1608
|
await next();
|
|
1640
1609
|
return;
|
|
@@ -1644,8 +1613,8 @@ export function apply(ctx, config) {
|
|
|
1644
1613
|
await next();
|
|
1645
1614
|
return;
|
|
1646
1615
|
}
|
|
1647
|
-
const
|
|
1648
|
-
if (!
|
|
1616
|
+
const mem = memory.current;
|
|
1617
|
+
if (!mem) {
|
|
1649
1618
|
await next();
|
|
1650
1619
|
return;
|
|
1651
1620
|
}
|
|
@@ -1654,8 +1623,8 @@ export function apply(ctx, config) {
|
|
|
1654
1623
|
// 注意 commitMetadata 的原子性**按后端分档**(见 api-memory 契约)——sqlite/inmemory
|
|
1655
1624
|
// 是真原子,mongodb 只保证按序执行遇错即停,仍可能停在半新半旧。批量的确定收益是
|
|
1656
1625
|
// 一次往返 + 遇错即停,不是无条件的「要么全成」。
|
|
1657
|
-
const items = await
|
|
1658
|
-
await
|
|
1626
|
+
const items = await mem.listMetadata(PROFILE_NS);
|
|
1627
|
+
await mem.commitMetadata(items.map(it => ({ op: 'del', namespace: PROFILE_NS, key: it.key })));
|
|
1659
1628
|
data.results.push({ source: 'user-profile', success: true, message: `用户档案已清空 (${items.length} 条)` });
|
|
1660
1629
|
}
|
|
1661
1630
|
catch (err) {
|
|
@@ -1664,20 +1633,19 @@ export function apply(ctx, config) {
|
|
|
1664
1633
|
}
|
|
1665
1634
|
// 同步清空第三方行为指令档案(per-persona)。指令是档案的兄弟概念,
|
|
1666
1635
|
// memory:clear scope='all' 时一并清理;types 默认包含 user-profile 即也清。
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
}
|
|
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}` });
|
|
1681
1649
|
}
|
|
1682
1650
|
await next();
|
|
1683
1651
|
});
|
|
@@ -1686,12 +1654,12 @@ export function apply(ctx, config) {
|
|
|
1686
1654
|
// 设计要点:
|
|
1687
1655
|
// - 全局可查(用户决定 C):跨平台/跨群均可,含 __self__ 自档案
|
|
1688
1656
|
// - 三种调用方式:① user_key 直传;② platform+user_id 组合;③ self=true(查当前 persona 自档案)
|
|
1689
|
-
|
|
1657
|
+
tools.registerGroup({
|
|
1690
1658
|
name: 'user-profile',
|
|
1691
1659
|
label: '用户档案',
|
|
1692
1660
|
description: '查询 Aalis 已经积累的某人事实档案(包括自档案)。',
|
|
1693
1661
|
});
|
|
1694
|
-
|
|
1662
|
+
tools.register({
|
|
1695
1663
|
definition: {
|
|
1696
1664
|
type: 'function',
|
|
1697
1665
|
function: {
|
|
@@ -1740,7 +1708,7 @@ export function apply(ctx, config) {
|
|
|
1740
1708
|
error: '需提供 user_key 或 (platform + user_id) 或 self=true。',
|
|
1741
1709
|
});
|
|
1742
1710
|
}
|
|
1743
|
-
const profile = await
|
|
1711
|
+
const profile = await loadProfileLenient(memory.current, userKey);
|
|
1744
1712
|
if (!profile || profile.facts.length === 0) {
|
|
1745
1713
|
return JSON.stringify({ ok: true, userKey, found: false, message: '该用户暂无档案数据。' });
|
|
1746
1714
|
}
|
|
@@ -1768,14 +1736,12 @@ export function apply(ctx, config) {
|
|
|
1768
1736
|
// /profile 查看自己的档案
|
|
1769
1737
|
// /profile clear 清除自己的档案(authority=2,自己删自己的)
|
|
1770
1738
|
// /profile clear nuke 清空所有用户档案(authority=3,dangerous)
|
|
1771
|
-
|
|
1772
|
-
.command('profile', '查看你在 Aalis 中的事实档案')
|
|
1773
|
-
.action(async (argv) => {
|
|
1739
|
+
commands.command('profile', '查看你在 Aalis 中的事实档案').action(async (argv) => {
|
|
1774
1740
|
const userId = argv.session.userId;
|
|
1775
1741
|
if (!userId)
|
|
1776
1742
|
return '当前会话未识别用户身份,无法查看档案。';
|
|
1777
1743
|
const userKey = userKeyOf(argv.session.platform, userId);
|
|
1778
|
-
const profile = await
|
|
1744
|
+
const profile = await loadProfileLenient(memory.current, userKey);
|
|
1779
1745
|
if (!profile || profile.facts.length === 0) {
|
|
1780
1746
|
return `📭 暂无档案数据 (${userKey})`;
|
|
1781
1747
|
}
|
|
@@ -1783,21 +1749,19 @@ export function apply(ctx, config) {
|
|
|
1783
1749
|
const meta = `关系强度:${(profile.relationScore ?? 0).toFixed(relationScorePrecision)}/100,互动次数:${profile.interactionCount ?? 0}`;
|
|
1784
1750
|
return `📇 你的档案 (${userKey})\n${meta}\n\n${block}`;
|
|
1785
1751
|
});
|
|
1786
|
-
|
|
1787
|
-
.command('profile.clear', '清除你自己的事实档案', { visibility: 'restricted' })
|
|
1788
|
-
.action(async (argv) => {
|
|
1752
|
+
commands.command('profile.clear', '清除你自己的事实档案', { visibility: 'restricted' }).action(async (argv) => {
|
|
1789
1753
|
const userId = argv.session.userId;
|
|
1790
1754
|
if (!userId)
|
|
1791
1755
|
return '当前会话未识别用户身份,无法清除档案。';
|
|
1792
1756
|
const userKey = userKeyOf(argv.session.platform, userId);
|
|
1793
|
-
const
|
|
1794
|
-
if (!
|
|
1757
|
+
const mem = memory.current;
|
|
1758
|
+
if (!mem)
|
|
1795
1759
|
return '记忆服务不支持档案删除。';
|
|
1796
1760
|
try {
|
|
1797
|
-
const existed = await
|
|
1761
|
+
const existed = await mem.getMetadata(PROFILE_NS, userKey);
|
|
1798
1762
|
if (!existed)
|
|
1799
1763
|
return `📭 你当前没有档案数据 (${userKey})`;
|
|
1800
|
-
await
|
|
1764
|
+
await mem.deleteMetadata(PROFILE_NS, userKey);
|
|
1801
1765
|
return `✅ 已清除你的档案 (${userKey})`;
|
|
1802
1766
|
}
|
|
1803
1767
|
catch (err) {
|
|
@@ -1809,7 +1773,7 @@ export function apply(ctx, config) {
|
|
|
1809
1773
|
// mongosh 手术或 nuke 全清。dangerous → 等级 2 + confirm,与同族 self.clear/nuke 对齐
|
|
1810
1774
|
//(它改的是**他人**档案,且 target 含冒号时直传 key、可触及 __self__ 自档案,
|
|
1811
1775
|
// 必须与 self.clear 同样上确认闸)。id 未命中时列出前若干条供定位。
|
|
1812
|
-
|
|
1776
|
+
commands
|
|
1813
1777
|
.command('profile.forget <target:string> <factId:string>', '定点删除某用户档案中的一条事实(运维除毒)', {
|
|
1814
1778
|
risk: 'dangerous',
|
|
1815
1779
|
})
|
|
@@ -1817,60 +1781,60 @@ export function apply(ctx, config) {
|
|
|
1817
1781
|
const target = String(targetArg ?? '');
|
|
1818
1782
|
const factId = String(factIdArg ?? '');
|
|
1819
1783
|
const userKey = target.includes(':') ? target : userKeyOf(argv.session.platform, target);
|
|
1820
|
-
const
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
const
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
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
|
+
}
|
|
1837
1807
|
});
|
|
1838
|
-
|
|
1839
|
-
.command('profile.self', '查看 Aalis 的自档案(跨会话的内心状态)')
|
|
1840
|
-
.action(async () => {
|
|
1808
|
+
commands.command('profile.self', '查看 Aalis 的自档案(跨会话的内心状态)').action(async () => {
|
|
1841
1809
|
const selfKey = getSelfKey();
|
|
1842
|
-
const profile = await
|
|
1810
|
+
const profile = await loadProfileLenient(memory.current, selfKey);
|
|
1843
1811
|
if (!profile || profile.facts.length === 0) {
|
|
1844
1812
|
return `🌱 ${getCurrentPersonaName()} 还没有积累任何自反思事实。(key=${selfKey})`;
|
|
1845
1813
|
}
|
|
1846
1814
|
const block = renderProfileBlock(profile.facts, getCurrentPersonaName(), false);
|
|
1847
1815
|
return `🪞 ${getCurrentPersonaName()} 自档案(共 ${profile.facts.length} 条,key=${selfKey})\n\n${block}`;
|
|
1848
1816
|
});
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
const memory = ctx.getService('memory');
|
|
1853
|
-
if (!memory)
|
|
1817
|
+
commands.command('profile.self.clear', '【慎用】清空 Aalis 自档案', { risk: 'dangerous' }).action(async () => {
|
|
1818
|
+
const mem = memory.current;
|
|
1819
|
+
if (!mem)
|
|
1854
1820
|
return '记忆服务不支持档案删除。';
|
|
1855
1821
|
const selfKey = getSelfKey();
|
|
1856
1822
|
try {
|
|
1857
|
-
await
|
|
1823
|
+
await mem.deleteMetadata(PROFILE_NS, selfKey);
|
|
1858
1824
|
return `✅ ${getCurrentPersonaName()} 自档案已清空(key=${selfKey})`;
|
|
1859
1825
|
}
|
|
1860
1826
|
catch (err) {
|
|
1861
1827
|
return `❌ 清空失败:${err instanceof Error ? err.message : String(err)}`;
|
|
1862
1828
|
}
|
|
1863
1829
|
});
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
const memory = ctx.getService('memory');
|
|
1868
|
-
if (!memory) {
|
|
1830
|
+
commands.command('profile.clear.nuke', '【危险】清空所有用户档案', { risk: 'dangerous' }).action(async () => {
|
|
1831
|
+
const mem = memory.current;
|
|
1832
|
+
if (!mem) {
|
|
1869
1833
|
return '记忆服务不支持档案批量删除。';
|
|
1870
1834
|
}
|
|
1871
1835
|
try {
|
|
1872
|
-
const items = await
|
|
1873
|
-
await
|
|
1836
|
+
const items = await mem.listMetadata(PROFILE_NS);
|
|
1837
|
+
await mem.commitMetadata(items.map(it => ({ op: 'del', namespace: PROFILE_NS, key: it.key })));
|
|
1874
1838
|
return `✅ 已清空全部用户档案(${items.length} 条)`;
|
|
1875
1839
|
}
|
|
1876
1840
|
catch (err) {
|
|
@@ -1883,15 +1847,13 @@ export function apply(ctx, config) {
|
|
|
1883
1847
|
// /instruct.add <text> 手动添加一条指令(authority=2)
|
|
1884
1848
|
// /instruct.remove <id> 按 id 删除一条(authority=2)
|
|
1885
1849
|
// /instruct.clear 清空当前 persona 的全部指令(authority=3, dangerous)
|
|
1886
|
-
|
|
1887
|
-
.command('instruct', '查看当前 persona 的第三方行为指令')
|
|
1888
|
-
.action(async () => {
|
|
1850
|
+
commands.command('instruct', '查看当前 persona 的第三方行为指令').action(async () => {
|
|
1889
1851
|
if (!cfg.enableInstructions)
|
|
1890
1852
|
return '🚫 第三方行为指令功能未启用。';
|
|
1891
|
-
const
|
|
1892
|
-
const doc = await
|
|
1853
|
+
const personaName = getCurrentPersonaName();
|
|
1854
|
+
const doc = await loadInstructionsLenient(memory.current);
|
|
1893
1855
|
if (doc.instructions.length === 0) {
|
|
1894
|
-
return `📭 ${
|
|
1856
|
+
return `📭 ${personaName} 当前没有任何第三方行为指令。`;
|
|
1895
1857
|
}
|
|
1896
1858
|
const lines = doc.instructions.map(ins => {
|
|
1897
1859
|
const tag = ins.severity === 'must' ? '【必须】' : ins.severity === 'avoid' ? '【避免】' : '【应当】';
|
|
@@ -1901,9 +1863,9 @@ export function apply(ctx, config) {
|
|
|
1901
1863
|
: '';
|
|
1902
1864
|
return ` ${ins.id} ${tag}${cat} ${ins.text}${src}`;
|
|
1903
1865
|
});
|
|
1904
|
-
return `📋 ${
|
|
1866
|
+
return `📋 ${personaName} 的第三方行为指令(共 ${doc.instructions.length} 条)\n${lines.join('\n')}`;
|
|
1905
1867
|
});
|
|
1906
|
-
|
|
1868
|
+
commands
|
|
1907
1869
|
.command('instruct.add <text:text>', '手动添加一条第三方行为指令', { visibility: 'restricted' })
|
|
1908
1870
|
.action(async (argv, text) => {
|
|
1909
1871
|
if (!cfg.enableInstructions)
|
|
@@ -1914,21 +1876,27 @@ export function apply(ctx, config) {
|
|
|
1914
1876
|
const userId = argv.session.userId;
|
|
1915
1877
|
const sourceUserKey = userId ? userKeyOf(argv.session.platform, userId) : undefined;
|
|
1916
1878
|
const sourceUserName = userId ?? '匿名';
|
|
1917
|
-
const
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
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
|
+
}
|
|
1930
1898
|
});
|
|
1931
|
-
|
|
1899
|
+
commands
|
|
1932
1900
|
.command('instruct.remove <id:string>', '按 id 删除一条第三方行为指令', { visibility: 'restricted' })
|
|
1933
1901
|
.action(async (_argv, id) => {
|
|
1934
1902
|
if (!cfg.enableInstructions)
|
|
@@ -1936,34 +1904,40 @@ export function apply(ctx, config) {
|
|
|
1936
1904
|
const targetId = typeof id === 'string' ? id.trim() : '';
|
|
1937
1905
|
if (!targetId)
|
|
1938
1906
|
return '❌ 需要提供指令 id。';
|
|
1939
|
-
const
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
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
|
+
}
|
|
1946
1920
|
});
|
|
1947
|
-
|
|
1921
|
+
commands
|
|
1948
1922
|
.command('instruct.clear', '【危险】清空当前 persona 的全部第三方行为指令', {
|
|
1949
1923
|
visibility: 'restricted',
|
|
1950
1924
|
})
|
|
1951
1925
|
.action(async () => {
|
|
1952
1926
|
if (!cfg.enableInstructions)
|
|
1953
1927
|
return '🚫 第三方行为指令功能未启用。';
|
|
1954
|
-
const
|
|
1955
|
-
if (!
|
|
1928
|
+
const mem = memory.current;
|
|
1929
|
+
if (!mem)
|
|
1956
1930
|
return '记忆服务不支持指令删除。';
|
|
1957
1931
|
try {
|
|
1958
|
-
const before = (await
|
|
1959
|
-
await
|
|
1932
|
+
const before = (await loadInstructionsLenient(mem)).instructions.length;
|
|
1933
|
+
await mem.deleteMetadata(INSTRUCTIONS_NS, getInstructionsKey());
|
|
1960
1934
|
return `✅ ${getCurrentPersonaName()} 的第三方行为指令已清空(删除 ${before} 条)。`;
|
|
1961
1935
|
}
|
|
1962
1936
|
catch (err) {
|
|
1963
1937
|
return `❌ 清空失败:${err instanceof Error ? err.message : String(err)}`;
|
|
1964
1938
|
}
|
|
1965
1939
|
});
|
|
1966
|
-
|
|
1940
|
+
logger.info(`用户事实档案已启用 (every=${cfg.extractEveryNMessages <= 0 ? '禁用提取' : `${cfg.extractEveryNMessages}msgs`}, history=${cfg.historyForExtraction}, ` +
|
|
1967
1941
|
`maxFacts=${cfg.maxFactsPerUser}, ` +
|
|
1968
1942
|
`self=${cfg.enableSelfProfile ? `every ${cfg.selfReflectEveryNMessages}msgs/max ${cfg.maxSelfFacts}, key=${getSelfKey()}` : 'disabled'}, ` +
|
|
1969
1943
|
`instructions=${cfg.enableInstructions ? `enabled(every ${cfg.instructionExtractEveryNMessages}msgs/max ${cfg.maxInstructions}/minAuth ${cfg.instructionMinAuthority}, key=${getInstructionsKey()})` : 'disabled'}, ` +
|