@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.js CHANGED
@@ -1,6 +1,13 @@
1
- import { useCommandService } from '@aalis/api-commands';
2
- import { resolveLLMModel } from '@aalis/api-llm';
3
- import { useToolService } from '@aalis/api-tools';
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
- export const configSchema = {
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 genFactId(existing) {
215
+ /** 生成稳定短 ID(6 字符 base36,对 30 条以内规模碰撞概率极低);前缀 'f' 为事实、'i' 为指令 */
216
+ function genShortId(prefix, existing) {
217
217
  for (let i = 0; i < 8; i++) {
218
- const id = `f${Math.random().toString(36).slice(2, 7)}`;
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 `f${Date.now().toString(36).slice(-5)}`;
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
- export function apply(ctx, config) {
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 persona = ctx.getService('persona');
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() : genInstructionId(usedIds);
410
+ let id = typeof obj.id === 'string' && obj.id.trim() ? obj.id.trim() : genShortId('i', usedIds);
399
411
  while (usedIds.has(id))
400
- id = genInstructionId(usedIds);
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
- async function loadInstructions() {
419
- const memory = ctx.getService('memory');
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 (!memory)
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
- const doc = await memory.getMetadata(INSTRUCTIONS_NS, getInstructionsKey());
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
- ctx.logger.debug(`加载指令档案失败: ${err instanceof Error ? err.message : String(err)}`);
432
- return empty;
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
- const memory = ctx.getService('memory');
437
- if (!memory)
455
+ async function saveInstructions(mem, doc) {
456
+ if (!mem)
438
457
  return;
439
- await memory.saveMetadata(INSTRUCTIONS_NS, getInstructionsKey(), {
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 = genInstructionId(usedIds);
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 中的事实数组(兼容旧格式 string[],自动迁移) */
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 === 'string' && item.trim()) {
540
- const id = genFactId(usedIds);
541
- usedIds.add(id);
542
- facts.push({ id, text: item.trim(), temporality: 'permanent', updatedAt: 0 });
543
- }
544
- else if (item && typeof item === 'object') {
545
- const obj = item;
546
- const text = typeof obj.text === 'string' ? obj.text.trim() : '';
547
- if (!text)
548
- continue;
549
- let id = typeof obj.id === 'string' && obj.id.trim() ? obj.id.trim() : genFactId(usedIds);
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
- /** 读取一个用户的现有档案(不存在返回 undefined)。兼容旧格式 string[],自动迁移 */
572
- async function loadProfile(userKey) {
573
- const memory = ctx.getService('memory');
574
- if (!memory)
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
- const doc = await memory.getMetadata(PROFILE_NS, userKey);
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
- ctx.logger.debug(`加载用户档案失败 (${userKey}): ${err instanceof Error ? err.message : String(err)}`);
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
- const memory = ctx.getService('memory');
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 memory.saveMetadata(PROFILE_NS, userKey, payload);
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(ctx, cfg.extractLLM, ['chat']);
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
- ctx.logger.warn(`[user-profile] LLM 输出无法解析为 JSON,尝试重试一次。原文前 200 字:${text.slice(0, 200)}`);
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
- ctx.logger.warn(`[user-profile] LLM 重试后仍无法解析 JSON,放弃本批次。重试原文前 200 字:${text.slice(0, 200)}`);
787
+ logger.warn(`[user-profile] LLM 重试后仍无法解析 JSON,放弃本批次。重试原文前 200 字:${text.slice(0, 200)}`);
704
788
  return empty;
705
789
  }
706
- ctx.logger.debug('[user-profile] LLM 重试后解析成功');
790
+ logger.debug('[user-profile] LLM 重试后解析成功');
707
791
  }
708
- const parsedObj = parsed;
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
- ctx.logger.warn(`[user-profile] 丢弃 ${kind} fact(未提供 sourceQuote): ${item.text}`);
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
- ctx.logger.warn(`[user-profile] 丢弃 ${kind} fact(sourceQuote 片段不在目标用户 ${userId} 发言中): text="${item.text}" fragment="${frag}"`);
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
- // 剥除 sourceQuote,不入库
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
- ctx.logger.debug(`事实提取 LLM 调用失败:${err instanceof Error ? err.message : String(err)}`);
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 ?? normalizeTemporality(undefined, u.category ?? old.category),
861
- observedAt: old.observedAt ?? now,
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 = genFactId(usedIds);
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 = genFactId(usedIds);
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
- /** 合并 facts 部分(向后兼容包装) */
909
- function mergeFacts(existing, ops) {
910
- return mergeFactList(existing, ops.add, ops.update, ops.remove, cfg.maxFactsPerUser);
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 memory = ctx.getService('memory');
952
+ const mem = memory.current;
923
953
  try {
924
- if (!memory?.getHistory)
954
+ if (!mem)
925
955
  return;
926
- const rawHistory = await memory.getHistory(sessionId, cfg.historyForExtraction);
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 profile = (await loadProfile(userKey)) ?? {
933
- facts: [],
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
- const newFacts = mergeFacts(profile.facts, ops);
943
- // 重新读取最新档案,避免覆盖提取期间(LLM 调用时)已写入的 relationScore 等字段
944
- const freshProfile = (await loadProfile(userKey)) ?? profile;
945
- await saveProfile(userKey, {
946
- ...freshProfile,
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
- ctx.logger.debug(`用户档案已更新 (${userKey}): facts +${ops.add.length} ~${ops.update.length} -${ops.remove.length} → ${newFacts.length} 条`);
976
+ logger.debug(`用户档案已更新 (${userKey}): facts +${ops.add.length} ~${ops.update.length} -${ops.remove.length} → ${newFacts.length} 条`);
951
977
  }
952
978
  catch (err) {
953
- ctx.logger.debug(`事实提取失败 (${userKey}): ${err instanceof Error ? err.message : String(err)}`);
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(ctx, cfg.extractLLM, ['chat']);
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
- ctx.logger.warn(`[user-profile] 自反思 LLM 输出无法解析为 JSON,原文前 200 字:${text.slice(0, 200)}`);
1063
+ logger.warn(`[user-profile] 自反思 LLM 输出无法解析为 JSON,原文前 200 字:${text.slice(0, 200)}`);
1038
1064
  return empty;
1039
1065
  }
1040
- const obj = parsed;
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
- ctx.logger.debug(`自反思 LLM 调用失败:${err instanceof Error ? err.message : String(err)}`);
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 memory = ctx.getService('memory');
1102
- if (!memory?.getHistory)
1081
+ const mem = memory.current;
1082
+ if (!mem)
1103
1083
  return;
1104
- const rawHistory = await memory.getHistory(sessionId, cfg.selfReflectHistory);
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 profile = (await loadProfile(selfKey)) ?? {
1111
- facts: [],
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
- const newFacts = mergeFactList(profile.facts, ops.add, ops.update, ops.remove, cfg.maxSelfFacts);
1120
- const fresh = (await loadProfile(selfKey)) ?? profile;
1121
- await saveProfile(selfKey, { ...fresh, facts: newFacts, updatedAt: Date.now() });
1122
- ctx.logger.debug(`Aalis 自档案已更新 (${selfKey}): +${ops.add.length} ~${ops.update.length} -${ops.remove.length} → ${newFacts.length} 条`);
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
- ctx.logger.debug(`自反思失败:${err instanceof Error ? err.message : String(err)}`);
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(ctx, cfg.extractLLM, ['chat']);
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
- ctx.logger.warn(`[user-profile] 指令提取 LLM 输出无法解析为 JSON,原文前 200 字:${text.slice(0, 200)}`);
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
- ctx.logger.warn(`[user-profile] 丢弃 instruction add(sourceUserKey 不在授权人列表中): text="${t}" claimed="${srcKey}"`);
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
- ctx.logger.debug(`指令提取 LLM 调用失败:${err instanceof Error ? err.message : String(err)}`);
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
- const authority = ctx.getService('authority');
1302
- if (!authority?.getAuthority) {
1303
- // authority 不可用 → 无法验证权限,跳过 LLM 自动提取(命令通道仍可用)
1277
+ // 指令只采信有权下指令的人,门槛是发言人的数字权限等级(owner 视为无穷大)
1278
+ const auth = authority.current;
1279
+ if (!auth)
1304
1280
  return;
1305
- }
1306
- const authorityFn = (platform, userId) => authority.getAuthority(platform, userId);
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 memory = ctx.getService('memory');
1310
- if (!memory?.getHistory)
1286
+ const mem = memory.current;
1287
+ if (!mem)
1311
1288
  return;
1312
- const rawHistory = await memory.getHistory(sessionId, cfg.instructionHistoryForExtraction);
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
- const newInstructions = mergeInstructions(doc.instructions, ops.add.map(a => ({ ...a, sourceChannel: 'llm' })), ops.update.map(u => {
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
- const fresh = await loadInstructions();
1325
- await saveInstructions({
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
- ctx.logger.debug(`指令提取失败:${err instanceof Error ? err.message : String(err)}`);
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 profile = (await loadProfile(userKey)) ?? { facts: [], relationScore: 0, interactionCount: 0, updatedAt: 0 };
1341
- await saveProfile(userKey, applyRelationUpdate(profile, triggerType, options));
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
- ctx.middleware('agent:input:before', async (data, next) => {
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
- ctx.logger.debug(`关系强度更新异常 (${userKey}): ${err instanceof Error ? err.message : String(err)}`);
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
- ctx.on('inbound:message:archived', (...args) => {
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) => ctx.logger.debug(`witness 关系更新异常 (${userKey}): ${err instanceof Error ? err.message : String(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) => ctx.logger.debug(`事实提取异常 (${countKey}): ${err instanceof Error ? err.message : String(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) => ctx.logger.debug(`自反思异常: ${err instanceof Error ? err.message : String(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) => ctx.logger.debug(`指令提取异常: ${err instanceof Error ? err.message : String(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
- const base = fact.updatedAt || fact.observedAt || 0;
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
- const observed = fact.observedAt || fact.updatedAt;
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 meta.length > 0 ? `- ${fact.text}(${meta.join(';')})` : `- ${fact.text}`;
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
- ctx.contribute('agent:prompt', {
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 loadInstructions();
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 loadProfile(getSelfKey());
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 loadProfile(userKey);
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 服务取「同社群活跃成员」(Louvain 标签 + PageRank 排序)。
1488
+ // 可选:从 user-relation 服务取「同社群活跃成员」(按配置的社群发现算法所得标签 + PageRank 排序)。
1513
1489
  // 服务不存在 / 节点没社群标签 / 报错都静默跳过;该字段是锦上添花,不破坏档案主体。
1514
1490
  let communityLine = '';
1515
1491
  try {
1516
- const relation = ctx.getService('user-relation');
1517
- if (relation?.getCommunityPeers) {
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
- ctx.logger.debug(`读取 community peers 失败: ${err instanceof Error ? err.message : String(err)}`);
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
- const memory = ctx.getService('memory');
1576
- if (memory) {
1551
+ if (mem) {
1577
1552
  try {
1578
- const globalRecent = await memory.listMetadata(PROFILE_NS);
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
- ctx.logger.debug(`加载最近互动用户失败: ${err instanceof Error ? err.message : String(err)}`);
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
- let profile;
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
- ctx.middleware('memory:clear', async (data, next) => {
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 memory = ctx.getService('memory');
1648
- if (!memory) {
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 memory.listMetadata(PROFILE_NS);
1658
- await memory.commitMetadata(items.map(it => ({ op: 'del', namespace: PROFILE_NS, key: it.key })));
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
- if (cfg.enableInstructions) {
1668
- try {
1669
- const insItems = await memory.listMetadata(INSTRUCTIONS_NS);
1670
- await memory.commitMetadata(insItems.map(it => ({ op: 'del', namespace: INSTRUCTIONS_NS, key: it.key })));
1671
- data.results.push({
1672
- source: 'user-profile-instructions',
1673
- success: true,
1674
- message: `第三方行为指令已清空 (${insItems.length} 条)`,
1675
- });
1676
- }
1677
- catch (err) {
1678
- const m = err instanceof Error ? err.message : String(err);
1679
- data.results.push({ source: 'user-profile-instructions', success: false, message: `指令清空失败: ${m}` });
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
- useToolService(ctx).registerGroup({
1657
+ tools.registerGroup({
1690
1658
  name: 'user-profile',
1691
1659
  label: '用户档案',
1692
1660
  description: '查询 Aalis 已经积累的某人事实档案(包括自档案)。',
1693
1661
  });
1694
- useToolService(ctx).register({
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 loadProfile(userKey);
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
- useCommandService(ctx)
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 loadProfile(userKey);
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
- useCommandService(ctx)
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 memory = ctx.getService('memory');
1794
- if (!memory)
1757
+ const mem = memory.current;
1758
+ if (!mem)
1795
1759
  return '记忆服务不支持档案删除。';
1796
1760
  try {
1797
- const existed = await memory.getMetadata(PROFILE_NS, userKey);
1761
+ const existed = await mem.getMetadata(PROFILE_NS, userKey);
1798
1762
  if (!existed)
1799
1763
  return `📭 你当前没有档案数据 (${userKey})`;
1800
- await memory.deleteMetadata(PROFILE_NS, userKey);
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
- useCommandService(ctx)
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 profile = await loadProfile(userKey);
1821
- if (!profile || profile.facts.length === 0)
1822
- return `📭 无档案数据 (${userKey})`;
1823
- const hit = profile.facts.find(f => f.id === factId);
1824
- if (!hit) {
1825
- const listing = profile.facts
1826
- .slice(0, 20)
1827
- .map(f => ` [${f.id}] ${f.text.slice(0, 50)}`)
1828
- .join('\n');
1829
- return `未找到事实 [${factId}]。${userKey} 现有 ${profile.facts.length} 条:\n${listing}`;
1830
- }
1831
- await saveProfile(userKey, {
1832
- ...profile,
1833
- facts: profile.facts.filter(f => f.id !== factId),
1834
- updatedAt: Date.now(),
1835
- });
1836
- return `✅ 已删除 ${userKey} 的事实 [${factId}]:${hit.text.slice(0, 60)}`;
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
- useCommandService(ctx)
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 loadProfile(selfKey);
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
- useCommandService(ctx)
1850
- .command('profile.self.clear', '【慎用】清空 Aalis 自档案', { risk: 'dangerous' })
1851
- .action(async () => {
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 memory.deleteMetadata(PROFILE_NS, selfKey);
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
- useCommandService(ctx)
1865
- .command('profile.clear.nuke', '【危险】清空所有用户档案', { risk: 'dangerous' })
1866
- .action(async () => {
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 memory.listMetadata(PROFILE_NS);
1873
- await memory.commitMetadata(items.map(it => ({ op: 'del', namespace: PROFILE_NS, key: it.key })));
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
- useCommandService(ctx)
1887
- .command('instruct', '查看当前 persona 的第三方行为指令')
1888
- .action(async () => {
1850
+ commands.command('instruct', '查看当前 persona 的第三方行为指令').action(async () => {
1889
1851
  if (!cfg.enableInstructions)
1890
1852
  return '🚫 第三方行为指令功能未启用。';
1891
- const persona = getCurrentPersonaName();
1892
- const doc = await loadInstructions();
1853
+ const personaName = getCurrentPersonaName();
1854
+ const doc = await loadInstructionsLenient(memory.current);
1893
1855
  if (doc.instructions.length === 0) {
1894
- return `📭 ${persona} 当前没有任何第三方行为指令。`;
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 `📋 ${persona} 的第三方行为指令(共 ${doc.instructions.length} 条)\n${lines.join('\n')}`;
1866
+ return `📋 ${personaName} 的第三方行为指令(共 ${doc.instructions.length} 条)\n${lines.join('\n')}`;
1905
1867
  });
1906
- useCommandService(ctx)
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 doc = await loadInstructions();
1918
- const merged = mergeInstructions(doc.instructions, [
1919
- {
1920
- text: body,
1921
- severity: 'should',
1922
- sourceUserKey,
1923
- sourceUserName,
1924
- sourceChannel: 'command',
1925
- },
1926
- ], [], []);
1927
- await saveInstructions({ instructions: merged, updatedAt: Date.now() });
1928
- const added = merged.find(m => m.text === clipInstructionText(body));
1929
- return `✅ 已添加行为指令${added ? `(id=${added.id})` : ''}:${clipInstructionText(body)}`;
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
- useCommandService(ctx)
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 doc = await loadInstructions();
1940
- const target = doc.instructions.find(i => i.id === targetId);
1941
- if (!target)
1942
- return `❌ 未找到 id=${targetId} 的指令。`;
1943
- const merged = mergeInstructions(doc.instructions, [], [], [targetId]);
1944
- await saveInstructions({ instructions: merged, updatedAt: Date.now() });
1945
- return `✅ 已删除指令(id=${targetId}):${target.text}`;
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
- useCommandService(ctx)
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 memory = ctx.getService('memory');
1955
- if (!memory)
1928
+ const mem = memory.current;
1929
+ if (!mem)
1956
1930
  return '记忆服务不支持指令删除。';
1957
1931
  try {
1958
- const before = (await loadInstructions()).instructions.length;
1959
- await memory.deleteMetadata(INSTRUCTIONS_NS, getInstructionsKey());
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
- ctx.logger.info(`用户事实档案已启用 (every=${cfg.extractEveryNMessages <= 0 ? '禁用提取' : `${cfg.extractEveryNMessages}msgs`}, history=${cfg.historyForExtraction}, ` +
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'}, ` +