@aalis/plugin-user-profile 0.12.0 → 0.13.1

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