dsh-fish-memory 1.5.2 → 1.5.4

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.
Files changed (3) hide show
  1. package/lib/client.js +366 -71
  2. package/lib/index.js +245 -41
  3. package/package.json +2 -2
package/lib/client.js CHANGED
@@ -193,10 +193,13 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
193
193
  const [confirm, setConfirm] = React.useState(null) // 正在等确认的删除(二次确认,手滑不了)
194
194
  const [并入, set并入] = React.useState(null) // ⭐ 1.4.5:切成按工作区前的「合并预览」
195
195
  const [showDeleted, setShowDeleted] = React.useState(false)
196
- const [selfDraft, setSelfDraft] = React.useState({}) // 每个会话「她自己叫什么」的草稿
196
+ const [selfDraft, setSelfDraft] = React.useState({}) // 每个会话「它自己叫什么」的草稿
197
+ const [称草稿, set称草稿] = React.useState({}) // 每个会话「记忆里指对方用哪个字」
198
+ const [自草稿, set自草稿] = React.useState({}) // 每个会话「它自称什么」
197
199
  const [ownerDraft, setOwnerDraft] = React.useState({}) // 每个会话「怎么称呼对方」的草稿
198
- const [pronounDraft, setPronounDraft] = React.useState(null) // 用户的称呼草稿
199
200
  const [edit, setEdit] = React.useState(null) // 正在改的那条:{id, text, importance}
201
+ // ⭐ 检索词当场改:{记忆id: 草稿}(模型写的词常常不靠谱,改一次比重新扩一遍快)
202
+ const [改词, set改词] = React.useState({})
200
203
  const [pronounAsk, setPronounAsk] = React.useState(null) // 改了称呼,问要不要连旧记忆一起改
201
204
  const [imp, setImp] = React.useState(null) // 档案导入:扫描结果 + 勾选状态
202
205
  const [impBusy, setImpBusy] = React.useState(false)
@@ -260,6 +263,8 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
260
263
  // 用户导入自己的会话 → 模型读一遍 → 出一份档案 → 再走「从旧档案导入」变成条目
261
264
  // ⚠ 四步分开,因为**第三步调模型、花钱**,得一批一批停下来看 ✓
262
265
  const [生成开, set生成开] = React.useState(false) // 面板展开没有
266
+ // ⭐ 2026-11-04:停用后面板不开了,改成一个**贴在按钮下面**的小提示(不进 notes,免得看不见)
267
+ const [星汐提示, set星汐提示] = React.useState(false)
263
268
  const [生成会话, set生成会话] = React.useState(null) // 这个工作区有哪些会话
264
269
  const [生成选, set生成选] = React.useState({}) // 勾了哪几个
265
270
  const [生成计划, set生成计划] = React.useState(null) // 算出来的:几批、多少字、多少 token
@@ -548,6 +553,26 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
548
553
  )
549
554
 
550
555
  // 改一条:文本框 + 重要度 + 存 / 算了
556
+ // 检索词:空格 / 逗号 / 顿号分开,最多 12 个
557
+ const 拆词 = (文) =>
558
+ String(文 || '')
559
+ .split(/[\s,,、]+/)
560
+ .map((x) => x.trim())
561
+ .filter(Boolean)
562
+ .slice(0, 12)
563
+
564
+ // 小按钮长相:跟文字分开,别让人以为它是检索词里的一个字
565
+ const 词按钮 = {
566
+ flex: 'none',
567
+ fontSize: 11,
568
+ padding: '1px 8px',
569
+ borderRadius: 10,
570
+ border: '1px solid rgba(122,170,220,.55)',
571
+ color: '#7aaadc',
572
+ cursor: 'pointer',
573
+ whiteSpace: 'nowrap',
574
+ }
575
+
551
576
  const 改一条 = (m) =>
552
577
  React.createElement(
553
578
  'div',
@@ -580,7 +605,13 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
580
605
  style: S.btn,
581
606
  disabled: !String(edit.text || '').trim(),
582
607
  onClick: () =>
583
- post('/edit', { session: open, id: m.id, text: edit.text, importance: edit.importance }).then(() => {
608
+ post('/edit', {
609
+ session: open,
610
+ id: m.id,
611
+ text: edit.text,
612
+ importance: edit.importance,
613
+ related: 拆词(edit.related !== undefined ? edit.related : (Array.isArray(m.related) ? m.related.join(' ') : '')),
614
+ }).then(() => {
584
615
  setEdit(null)
585
616
  load()
586
617
  }),
@@ -589,7 +620,84 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
589
620
  ),
590
621
  React.createElement('button', { style: S.btn, onClick: () => setEdit(null) }, '算了'),
591
622
  ),
623
+ // ⭐ 1.5.4:改正文这个框里也一起给了检索词 —— 以前只能改正文,
624
+ // 想改检索词得再去点下面那行,容易找不到
625
+ React.createElement(
626
+ 'div',
627
+ { style: { display: 'flex', gap: 8, alignItems: 'center', marginTop: 6 } },
628
+ React.createElement('span', { style: { flex: 'none', ...S.dim } }, '检索词'),
629
+ React.createElement('input', {
630
+ style: Object.assign({}, S.inp, { flex: 1 }),
631
+ placeholder: '空格或逗号分开,最多 12 个',
632
+ value: edit.related !== undefined ? edit.related : (Array.isArray(m.related) ? m.related.join(' ') : ''),
633
+ onChange: (e) => setEdit(Object.assign({}, edit, { related: e.target.value })),
634
+ }),
635
+ React.createElement('span', { style: Object.assign({}, S.dim, { fontSize: 12, flex: 'none' }) }, '以后她说这些词就能捞到这条'),
636
+ ),
637
+ )
638
+
639
+ // ⭐ 检索词能当场改:模型写的词经常不靠谱,改一次比重新扩一遍快,也不动正文
640
+ const 存检索词 = (m, 文本) => {
641
+ const 词 = 拆词(文本)
642
+ post('/edit', {
643
+ session: open,
644
+ id: m.id,
645
+ text: m.text,
646
+ importance: typeof m.importance === 'number' ? m.importance : 0.5,
647
+ related: 词,
648
+ }).then(() => {
649
+ const 新 = Object.assign({}, 改词)
650
+ delete 新[m.id]
651
+ set改词(新)
652
+ load()
653
+ })
654
+ }
655
+ const 关掉改词 = (id) => {
656
+ const 新 = Object.assign({}, 改词)
657
+ delete 新[id]
658
+ set改词(新)
659
+ }
660
+ const 检索词行 = (m) => {
661
+ const 在改 = 改词[m.id] !== undefined
662
+ const 有 = Array.isArray(m.related) && m.related.length
663
+ if (在改) {
664
+ return React.createElement(
665
+ 'div',
666
+ { style: Object.assign({}, S.dim, { margin: '0 0 6px 104px', fontSize: 11, display: 'flex', gap: 6, alignItems: 'center', flexWrap: 'wrap' }) },
667
+ React.createElement('span', null, '能搜到它的词:'),
668
+ React.createElement('input', {
669
+ style: Object.assign({}, S.inp, { width: 340, fontSize: 11 }),
670
+ value: 改词[m.id],
671
+ placeholder: '空格或逗号分开,最多 12 个',
672
+ onChange: (e) => set改词(Object.assign({}, 改词, { [m.id]: e.target.value })),
673
+ }),
674
+ React.createElement('span', { style: Object.assign({}, S.link, { fontSize: 11 }), onClick: () => 存检索词(m, 改词[m.id]) }, '存'),
675
+ React.createElement('span', { style: Object.assign({}, S.link, { fontSize: 11 }), onClick: () => 关掉改词(m.id) }, '算了'),
676
+ )
677
+ }
678
+ if (!有) {
679
+ return React.createElement(
680
+ 'div',
681
+ { style: Object.assign({}, S.dim, { margin: '0 0 6px 104px', fontSize: 11, display: 'flex', gap: 6, alignItems: 'center' }) },
682
+ React.createElement('span', null, '还没有检索词'),
683
+ React.createElement(
684
+ 'span',
685
+ { style: 词按钮, onClick: () => set改词(Object.assign({}, 改词, { [m.id]: '' })) },
686
+ '+ 加检索词',
687
+ ),
688
+ )
689
+ }
690
+ return React.createElement(
691
+ 'div',
692
+ { style: Object.assign({}, S.dim, { margin: '0 0 6px 104px', fontSize: 11, overflowWrap: 'anywhere', wordBreak: 'break-word', display: 'flex', gap: 8, alignItems: 'center' }) },
693
+ React.createElement('span', { style: { flex: 1 } }, '能搜到它的词:' + m.related.join(' ')),
694
+ React.createElement(
695
+ 'span',
696
+ { style: 词按钮, onClick: () => set改词(Object.assign({}, 改词, { [m.id]: m.related.join(' ') })) },
697
+ '改检索词',
698
+ ),
592
699
  )
700
+ }
593
701
 
594
702
  const 一条记忆 = (m) =>
595
703
  React.createElement(
@@ -721,13 +829,7 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
721
829
  '提炼前:' + m.textBefore,
722
830
  )
723
831
  : null,
724
- m.related && m.related.length
725
- ? React.createElement(
726
- 'div',
727
- { style: Object.assign({}, S.dim, { margin: '0 0 6px 104px', fontSize: 11, overflowWrap: 'anywhere', wordBreak: 'break-word' }) },
728
- '能搜到它的词:' + m.related.join(' '),
729
- )
730
- : null,
832
+ 检索词行(m),
731
833
  // ⭐⭐ 2026-09-23:**「这条讲的是哪件事」** —— 挂在这儿(第二行小字),不占行里的宽度 ✓
732
834
  // ⚠⚠ 别用「事实键」这种我自己造的词 ——
733
835
  // 她说了「让人看不懂,写通俗一点的」✗
@@ -1296,7 +1398,19 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
1296
1398
  // ④ 存成档案文件 → 再走已有的「从旧档案导入」变成记忆条目
1297
1399
  // ═══════════════════════════════════════════════════════════════
1298
1400
  const 开生成 = () => {
1299
- set生成开(true)
1401
+ // ⭐ 2026-11-04:这个自带流程**停用**了。
1402
+ // 它一直有 bug(会话列表筛不准、批次落盘不稳),而且她几乎没用过。
1403
+ // 生成档案这件事已经由独立插件「星汐」(dsh-xingxi)接管 ——
1404
+ // 那边能绑文件夹、按天分批、原文留底,比这里靠谱。
1405
+ // 点按钮不再进旧流程,只弹一句提示(旧面板代码留着,但这里不再打开)。
1406
+ set生成开(false)
1407
+ setNotes([
1408
+ '「从会话生成档案」已经停用 —— 请改用它:**星汐(dsh-xingxi)**。',
1409
+ '在 DSH 里装:dsh plugin add dsh-xingxi;装完侧栏会多一个「星汐」。',
1410
+ '旧流程有 bug(会话列表筛不准、批次落盘不稳),这里不再提供入口。',
1411
+ ])
1412
+ return
1413
+ /* eslint-disable no-unreachable */
1300
1414
  set生成会话(null)
1301
1415
  set生成计划(null)
1302
1416
  set生成条目([])
@@ -2353,43 +2467,69 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
2353
2467
  return 小件
2354
2468
  }
2355
2469
 
2470
+ // ⭐ 四格排成同一个样子:左边标签等宽、中间输入框、右边说明(字一样大)
2471
+ const 一格 = (标签, 节点, 说明) =>
2472
+ React.createElement(
2473
+ 'div',
2474
+ { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 } },
2475
+ React.createElement('span', { style: { flex: 'none', width: 110, fontSize: 13 } }, 标签),
2476
+ 节点,
2477
+ React.createElement('span', { style: Object.assign({}, S.dim, { fontSize: 12, flex: 1, lineHeight: 1.5 }) }, 说明),
2478
+ )
2479
+
2356
2480
  return React.createElement(
2357
2481
  'div',
2358
2482
  { style: S.wrap },
2359
2483
  React.createElement('div', { style: S.link, onClick: () => setOpen(null) }, '← 所有会话'),
2360
2484
  React.createElement('div', { style: S.h }, s.title || s.id),
2361
- React.createElement('div', { style: S.dim }, s.id + ' ' + s.memories.length + ' 条记忆' + (已删.length ? ' (另外还收着 ' + 已删.length + ' 条)' : '')),
2485
+ React.createElement('div', { style: S.dim }, s.id + ' ' + s.memories.length + ' 条记忆' + (已删.length ? ' (另外还收着 ' + 已删.length + ' 条)' : '') + (d.模式 === 'workspace' && s.工作区 ? ' · 按工作区共用一份(' + (s.工作区名 || s.工作区) + ')' : '')),
2362
2486
  React.createElement(
2363
2487
  'div',
2364
2488
  { style: { padding: '10px 12px', borderRadius: 8, border: '1px solid rgba(128,128,128,.22)', background: 'rgba(127,127,127,.05)' } },
2365
- React.createElement(
2366
- 'div',
2367
- { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 } },
2368
- React.createElement('span', { style: { flex: 'none', width: 96, fontSize: 13 } }, '她自己叫什么'),
2489
+ // ⭐ 2026-11-04:称呼和自称都并进这张卡(以前吊在外面的「哪些会话用记忆」下面,两边说法还不一样)
2490
+ 一格(
2491
+ '它自己叫什么',
2369
2492
  React.createElement('input', {
2370
2493
  style: Object.assign({}, S.inp, { width: 160 }),
2371
- placeholder: '她在这儿的名字',
2494
+ placeholder: '它在这儿的名字',
2372
2495
  value: selfDraft[open] !== undefined ? selfDraft[open] : s.selfName || s.titleName || '',
2373
2496
  onChange: (e) => setSelfDraft(Object.assign({}, selfDraft, { [open]: e.target.value })),
2374
2497
  }),
2375
- s.selfName
2376
- ? null
2377
- : (s.titleName ? React.createElement('span', { style: Object.assign({}, S.dim, { fontSize: 12 }) }, '(先填的是会话标题,确认没问题再点「存」)') : null),
2498
+ s.selfName ? '记忆里,它的名字会换成「我」' : s.titleName ? '先填的是会话标题,确认没问题再点「存」' : '填了才会自动换人称',
2378
2499
  ),
2379
- React.createElement(
2380
- 'div',
2381
- { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 } },
2382
- React.createElement('span', { style: { flex: 'none', width: 96, fontSize: 13 } }, '她怎么称呼你'),
2500
+ 一格(
2501
+ '它怎么称呼你',
2383
2502
  React.createElement('input', {
2384
2503
  style: Object.assign({}, S.inp, { width: 160 }),
2385
- placeholder: '她平时怎么叫你(小名、昵称都行)',
2504
+ placeholder: '它平时怎么叫你(小名、昵称都行)',
2386
2505
  value: ownerDraft[open] !== undefined ? ownerDraft[open] : s.ownerName || '',
2387
2506
  onChange: (e) => setOwnerDraft(Object.assign({}, ownerDraft, { [open]: e.target.value })),
2388
2507
  }),
2508
+ '记忆里,这个叫法会换成「你」',
2509
+ ),
2510
+ 一格(
2511
+ '记忆里指对方用',
2512
+ React.createElement('input', {
2513
+ style: Object.assign({}, S.inp, { width: 90 }),
2514
+ placeholder: 'ta',
2515
+ value: 称草稿[open] !== undefined ? 称草稿[open] : s.pronoun || 'ta',
2516
+ onChange: (e) => set称草稿(Object.assign({}, 称草稿, { [open]: e.target.value })),
2517
+ }),
2518
+ 'ta / 她 / 他 —— 默认 ta,设成哪个就按哪个记',
2519
+ ),
2520
+ 一格(
2521
+ '它自称什么',
2522
+ React.createElement('input', {
2523
+ style: Object.assign({}, S.inp, { width: 90 }),
2524
+ placeholder: '我',
2525
+ value: 自草稿[open] !== undefined ? 自草稿[open] : s.selfPronoun || '我',
2526
+ onChange: (e) => set自草稿(Object.assign({}, 自草稿, { [open]: e.target.value })),
2527
+ }),
2528
+ '它平时自称什么(本小姐、在下…),默认「我」',
2389
2529
  ),
2390
2530
  React.createElement(
2391
2531
  'div',
2392
- { style: { display: 'flex', alignItems: 'center', gap: 10 } },
2532
+ { style: { display: 'flex', alignItems: 'center', gap: 10, marginTop: 4 } },
2393
2533
  React.createElement(
2394
2534
  'button',
2395
2535
  {
@@ -2399,9 +2539,19 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
2399
2539
  session: open,
2400
2540
  selfName: (selfDraft[open] !== undefined ? selfDraft[open] : s.selfName) || '',
2401
2541
  ownerName: (ownerDraft[open] !== undefined ? ownerDraft[open] : s.ownerName) || '',
2402
- }).then(() => {
2542
+ pronoun: (称草稿[open] !== undefined ? 称草稿[open] : s.pronoun) || 'ta',
2543
+ selfPronoun: (自草稿[open] !== undefined ? 自草稿[open] : s.selfPronoun) || '我',
2544
+ }).then((r) => {
2403
2545
  setSelfDraft({})
2404
2546
  setOwnerDraft({})
2547
+ set称草稿({})
2548
+ set自草稿({})
2549
+ // 影响到旧记忆就先问一句,不偷偷改 ✓
2550
+ if (r && r.称呼影响几条 > 0 && !r.称呼已改) {
2551
+ setPronounAsk({ n: r.称呼影响几条, to: r.称呼 || '', 例子: r.称呼例子 || [], session: open })
2552
+ } else {
2553
+ setPronounAsk(null)
2554
+ }
2405
2555
  load()
2406
2556
  }),
2407
2557
  },
@@ -2410,10 +2560,38 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
2410
2560
  React.createElement(
2411
2561
  'span',
2412
2562
  { style: Object.assign({}, S.dim, { fontSize: 12, flex: 1, lineHeight: 1.5 }) },
2413
- '她自己的名字写进记忆时会换成「我」;她怎么称呼你,会换成「你」。已有的记忆也会顺手洗一遍。',
2563
+ '只改这个会话,别的会话各算各的;已有的记忆也会顺手洗一遍。',
2414
2564
  ),
2415
2565
  ),
2416
2566
  ),
2567
+ // ⭐ 改称呼影响到旧记忆时,就地在这个会话里问一句(不偷偷改)
2568
+ pronounAsk && pronounAsk.session === open
2569
+ ? React.createElement(
2570
+ 'div',
2571
+ { style: Object.assign({}, S.askBox, { marginTop: 10 }) },
2572
+ React.createElement(
2573
+ 'div',
2574
+ { style: { flex: 1 } },
2575
+ React.createElement('div', null, '这个会话里有 ' + pronounAsk.n + ' 条旧记忆还写着旧的字。要一起改成「' + pronounAsk.to + '」吗?'),
2576
+ (pronounAsk.例子 || []).slice(0, 2).map((e, i) =>
2577
+ React.createElement('div', { key: i, style: Object.assign({}, S.dim, { fontSize: 12, marginTop: 2 }) }, ' 「' + e.旧 + '」→「' + e.新 + '」'),
2578
+ ),
2579
+ ),
2580
+ React.createElement(
2581
+ 'button',
2582
+ {
2583
+ style: S.btn,
2584
+ onClick: () =>
2585
+ post('/names', { session: open, pronoun: pronounAsk.to, applyToExisting: true }).then(() => {
2586
+ setPronounAsk(null)
2587
+ load()
2588
+ }),
2589
+ },
2590
+ '一起改',
2591
+ ),
2592
+ React.createElement('button', { style: S.btn, onClick: () => setPronounAsk(null) }, '以后再说'),
2593
+ )
2594
+ : null,
2417
2595
  // ---- 2026-09-21 新加:她自己后台手动加一条 ----
2418
2596
  // 跟模型自己记是两回事:它记受上面那个开关管,**手动加不受管** ✓
2419
2597
  // (开关管的是「他自己记不记、注不注入」,管不到她手动写)
@@ -2539,19 +2717,44 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
2539
2717
  '查重',
2540
2718
  ),
2541
2719
  // ⭐⭐ 2026-09-22(她的第十件):**从会话生成档案**
2542
- // 「别的用户导入自己的会话 → 模型读一遍 → 出一份档案」
2543
- // ⚠ 这是**给别人用的功能**,跟「手工维护一份档案」是两码事 ✓
2720
+ // ⚠ 2026-11-04 停用:这条流程一直有 bug,已经由独立插件「星汐」(dsh-xingxi) 接管。
2721
+ // 点按钮**就在它下面**展开一小段提示 —— 不再写进 notes
2722
+ // (那玩意儿在列表底下,列表没展开根本看不见,橙色字也太淡)。
2544
2723
  React.createElement(
2545
- 'button',
2546
- {
2547
- style: S.btn,
2548
- title:
2549
- '把你自己的会话读一遍,提炼出一份长期档案(分九章)。' +
2550
- '出好的档案再走「从旧档案导入」变成记忆条目。' +
2551
- '会调用模型、会花钱,所以分四步:先看会话 → 算一下要多少 → 一批一批跑 → 存文件',
2552
- onClick: () => 开生成(),
2553
- },
2554
- '从会话生成档案',
2724
+ 'div',
2725
+ { style: { display: 'flex', flexDirection: 'column', gap: 6, maxWidth: 320 } },
2726
+ React.createElement(
2727
+ 'button',
2728
+ {
2729
+ style: S.btn,
2730
+ title: '生成档案:点一下看怎么做',
2731
+ onClick: () => set星汐提示((v) => !v),
2732
+ },
2733
+ '从会话生成档案',
2734
+ ),
2735
+ 星汐提示
2736
+ ? React.createElement(
2737
+ 'div',
2738
+ {
2739
+ style: {
2740
+ fontSize: 11.5,
2741
+ lineHeight: 1.7,
2742
+ color: 'rgba(196,210,236,0.92)',
2743
+ background: 'rgba(28,38,58,0.55)',
2744
+ border: '1px solid rgba(120,150,205,0.25)',
2745
+ borderRadius: 8,
2746
+ padding: '7px 9px',
2747
+ },
2748
+ },
2749
+ React.createElement('div', null, '改用独立插件「星汐」——它专门干这件事。'),
2750
+ React.createElement(
2751
+ 'div',
2752
+ { style: { marginTop: 4, fontFamily: 'monospace', fontSize: 11, color: '#9fd8ff' } },
2753
+ 'dsh plugin add dsh-xingxi',
2754
+ ),
2755
+ React.createElement('div', { style: { marginTop: 4, opacity: 0.75 } }, '装完侧栏会多一个「星汐」。'),
2756
+ )
2757
+ : null,
2555
2758
  ),
2556
2759
  // ⭐⭐ 2026-09-22(她要的):**批量删除** —— 勾了几条就显示几条
2557
2760
  // ⚠ 二次确认:点这里**不删**,只出确认条;确认条上再点「确定删」才真删 ✓
@@ -3881,6 +4084,123 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
3881
4084
  })
3882
4085
  }
3883
4086
 
4087
+ // ⭐ 2026-11-04:工作区模式下,称呼/自称按**工作区**改(同一个工作区共用一份记忆)
4088
+ // 打开工作区模式就整块展开,不再折在「改」后面
4089
+ const 工作区一格 = (标签, 节点, 说明) =>
4090
+ React.createElement(
4091
+ 'div',
4092
+ { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 } },
4093
+ React.createElement('span', { style: { flex: 'none', width: 110, fontSize: 13 } }, 标签),
4094
+ 节点,
4095
+ React.createElement('span', { style: Object.assign({}, S.dim, { fontSize: 12, flex: 1, lineHeight: 1.5 }) }, 说明),
4096
+ )
4097
+ const 工作区块 =
4098
+ d.模式 === 'workspace' && (d.工作区们 || []).length
4099
+ ? [
4100
+ React.createElement('div', { key: 'wsh', style: S.h }, '按工作区改'),
4101
+ React.createElement(
4102
+ 'div',
4103
+ { key: 'wst', style: S.dim },
4104
+ '工作区模式下,同一个工作区的会话共用一份记忆 —— 昵称、称呼、人称也跟着共用一份,在这里改一次就够。',
4105
+ ),
4106
+ ...(d.工作区们 || []).map((g) => {
4107
+ const 取名 = () => (selfDraft[g.键] !== undefined ? selfDraft[g.键] : g.selfName || '')
4108
+ const 取称呼 = () => (ownerDraft[g.键] !== undefined ? ownerDraft[g.键] : g.ownerName || '')
4109
+ const 取人称 = () => (称草稿[g.键] !== undefined ? 称草稿[g.键] : g.pronoun || 'ta')
4110
+ const 取自称 = () => (自草稿[g.键] !== undefined ? 自草稿[g.键] : g.selfPronoun || '我')
4111
+ return React.createElement(
4112
+ 'div',
4113
+ { key: 'ws-' + g.键, style: Object.assign({}, S.条, { display: 'block', padding: '8px 0' }) },
4114
+ React.createElement(
4115
+ 'div',
4116
+ { style: { display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8 } },
4117
+ React.createElement('span', { style: { fontSize: 13, fontWeight: 600 } }, g.名 || g.工作区),
4118
+ React.createElement('span', { style: S.dim, title: g.路径 || '' }, (g.会话 || []).length + ' 间会话 · ' + (g.条数 || 0) + ' 条记忆'),
4119
+ ),
4120
+ React.createElement(
4121
+ 'div',
4122
+ { style: { padding: '10px 12px', borderRadius: 8, border: '1px solid rgba(128,128,128,.22)', background: 'rgba(127,127,127,.05)' } },
4123
+ 工作区一格(
4124
+ '它自己叫什么',
4125
+ React.createElement('input', {
4126
+ style: Object.assign({}, S.inp, { width: 160 }),
4127
+ placeholder: '它在这儿的名字',
4128
+ value: 取名(),
4129
+ onChange: (e) => setSelfDraft(Object.assign({}, selfDraft, { [g.键]: e.target.value })),
4130
+ }),
4131
+ '记忆里,它的名字会换成「我」',
4132
+ ),
4133
+ 工作区一格(
4134
+ '它怎么称呼你',
4135
+ React.createElement('input', {
4136
+ style: Object.assign({}, S.inp, { width: 160 }),
4137
+ placeholder: '它平时怎么叫你(小名、昵称都行)',
4138
+ value: 取称呼(),
4139
+ onChange: (e) => setOwnerDraft(Object.assign({}, ownerDraft, { [g.键]: e.target.value })),
4140
+ }),
4141
+ '记忆里,这个叫法会换成「你」',
4142
+ ),
4143
+ 工作区一格(
4144
+ '记忆里指对方用',
4145
+ React.createElement('input', {
4146
+ style: Object.assign({}, S.inp, { width: 90 }),
4147
+ placeholder: 'ta',
4148
+ value: 取人称(),
4149
+ onChange: (e) => set称草稿(Object.assign({}, 称草稿, { [g.键]: e.target.value })),
4150
+ }),
4151
+ 'ta / 她 / 他 —— 默认 ta',
4152
+ ),
4153
+ 工作区一格(
4154
+ '它自称什么',
4155
+ React.createElement('input', {
4156
+ style: Object.assign({}, S.inp, { width: 90 }),
4157
+ placeholder: '我',
4158
+ value: 取自称(),
4159
+ onChange: (e) => set自草稿(Object.assign({}, 自草稿, { [g.键]: e.target.value })),
4160
+ }),
4161
+ '它平时自称什么(本小姐、在下…),默认「我」',
4162
+ ),
4163
+ React.createElement(
4164
+ 'div',
4165
+ { style: { display: 'flex', alignItems: 'center', gap: 10, marginTop: 4 } },
4166
+ React.createElement(
4167
+ 'button',
4168
+ {
4169
+ style: S.btn,
4170
+ onClick: () =>
4171
+ post('/names', {
4172
+ session: g.键,
4173
+ selfName: 取名(),
4174
+ ownerName: 取称呼(),
4175
+ pronoun: 取人称() || 'ta',
4176
+ selfPronoun: 取自称() || '我',
4177
+ }).then((r) => {
4178
+ setSelfDraft({})
4179
+ setOwnerDraft({})
4180
+ set称草稿({})
4181
+ set自草稿({})
4182
+ if (r && r.称呼影响几条 > 0 && !r.称呼已改) {
4183
+ setPronounAsk({ n: r.称呼影响几条, to: r.称呼 || '', 例子: r.称呼例子 || [] })
4184
+ } else {
4185
+ setPronounAsk(null)
4186
+ }
4187
+ load()
4188
+ }),
4189
+ },
4190
+ '存',
4191
+ ),
4192
+ React.createElement(
4193
+ 'span',
4194
+ { style: Object.assign({}, S.dim, { fontSize: 12, flex: 1, lineHeight: 1.5 }) },
4195
+ '这个工作区里所有会话一起生效;已有的记忆也会顺手洗一遍。',
4196
+ ),
4197
+ ),
4198
+ ),
4199
+ )
4200
+ }),
4201
+ ]
4202
+ : []
4203
+
3884
4204
  const blocks = []
3885
4205
  for (const [gid, gname, gtip] of GROUPS) {
3886
4206
  const rows = spec.filter((s) => s.group === gid)
@@ -3918,36 +4238,11 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
3918
4238
  { style: S.dim },
3919
4239
  '装好就是开着的 —— 每个会话都能记、也能读。想给某个会话单独关掉,点它右边那个「关」。每个会话一份记忆,淡了只是不容易被想起来,不会删。',
3920
4240
  ),
3921
- React.createElement(
3922
- 'div',
3923
- { style: S.nameBox },
3924
- React.createElement('div', { style: { flex: 'none', width: 132 } }, '用户的称呼'),
3925
- React.createElement('input', {
3926
- style: Object.assign({}, S.inp, { width: 90 }),
3927
- placeholder: 'ta / 她 / 他',
3928
- value: pronounDraft !== null ? pronounDraft : d.userPronoun || 'ta',
3929
- onChange: (e) => setPronounDraft(e.target.value),
3930
- }),
3931
- React.createElement(
3932
- 'button',
3933
- {
3934
- style: S.btn,
3935
- onClick: () => {
3936
- const p = pronounDraft !== null ? pronounDraft : d.userPronoun || 'ta'
3937
- post('/names', { userPronoun: p }).then((r) => {
3938
- setPronounDraft(null)
3939
- // 影响到了旧记忆就先问一句,不偷偷改 ✓
3940
- if (r && r.称呼影响几条 > 0 && !r.称呼已改) setPronounAsk({ n: r.称呼影响几条, to: p, 例子: r.称呼例子 || [] })
3941
- else setPronounAsk(null)
3942
- load()
3943
- })
3944
- },
3945
- },
3946
- '存',
3947
- ),
3948
- React.createElement('span', { style: Object.assign({}, S.dim, { fontSize: 12, flex: 1 }) }, '记忆里提到你时用哪个字。默认「ta」;设成「她」或「他」就按你设的记。'),
3949
- ),
3950
- pronounAsk
4241
+ // ⭐ 工作区模式:按工作区改称呼(同一个工作区共用一份记忆)
4242
+ ...工作区块,
4243
+ // ⭐ 2026-11-04:这两格搬到「每个会话」那张卡里去了 ——
4244
+ // 一般人都是一间会话一个对象,吊在外面既不好找,也跟会话卡里的说法对不上。
4245
+ pronounAsk && !open
3951
4246
  ? React.createElement(
3952
4247
  'div',
3953
4248
  { style: S.askBox },
@@ -3964,7 +4259,7 @@ const 洗键进度 = { 已: 0, 总: 0, 在跑: false, 干跑: false }
3964
4259
  {
3965
4260
  style: S.btn,
3966
4261
  onClick: () =>
3967
- post('/names', { userPronoun: pronounAsk.to, applyToExisting: true }).then(() => {
4262
+ post('/names', { session: pronounAsk.session || '', pronoun: pronounAsk.to, applyToExisting: true }).then(() => {
3968
4263
  setPronounAsk(null)
3969
4264
  load()
3970
4265
  }),
package/lib/index.js CHANGED
@@ -78,9 +78,31 @@ export function 工作区目录名(p) {
78
78
  return '--' + 可读.slice(0, 251) + '--'
79
79
  }
80
80
 
81
- /** 工作区的可读路径:从会话目录名反查不回来,就直接给目录名 */
81
+ /** 工作区的可读名:DSH 自己存着「工作区 id → path / title」,拿它换个看得懂的名字 */
82
+ function 工作区信息(键) {
83
+ const k = String(键 || '')
84
+ if (!k) return { 名: '(未分组)', 路径: '' }
85
+ try {
86
+ const p = join(CFG_DIR, 'storages', 'workspace.json')
87
+ const d = JSON.parse(readFileSync(p, 'utf8'))
88
+ const 表 = (d && d.tables && d.tables.workspaces) || {}
89
+ const 本区会话 = 扫会话目录().byWorkspace.get(k) || []
90
+ for (const w of Object.values(表)) {
91
+ const ids = w && Array.isArray(w.sessionIds) ? w.sessionIds : []
92
+ if (!ids.length) continue
93
+ if (!本区会话.some((s) => ids.includes(s))) continue
94
+ const 路径 = String(w.path || '')
95
+ const 段 = 路径.split(/[\\/]/).filter(Boolean)
96
+ // 名字取最后两段(「上一级\当前目录」这种),只有一段就那一段;完整路径放在 title 里
97
+ const 名 = 段.length >= 2 ? 段.slice(-2).join('\\') : 段[0] || String(w.title || '')
98
+ if (名) return { 名, 路径 }
99
+ }
100
+ } catch (error) {}
101
+ return { 名: String(k).replace(/^--/, '').replace(/--$/, '') || '(未分组)', 路径: '' }
102
+ }
103
+
82
104
  function 工作区可读名(键) {
83
- return String(键 || '').replace(/^--/, '').replace(/--$/, '') || '(未分组)'
105
+ return 工作区信息(键).名
84
106
  }
85
107
 
86
108
  const DAY = 86400000
@@ -834,9 +856,12 @@ function migrateSwitch() {
834
856
  // 记的时候、注入的时候,都把「她自己的名字」换成「我」✓
835
857
 
836
858
  // 她自己叫什么(每个会话一个)。没设 = 不替换。
859
+ // ⭐ 1.5.4:工作区模式下按工作区取(同一个工作区的会话共用一份),老数据仍按会话 id 存着,两种都认。
837
860
  function selfNameOf(sid) {
838
861
  const c = loadCfg()
839
- const n = c.selfNames && c.selfNames[String(sid)]
862
+ const 表 = c.selfNames && typeof c.selfNames === 'object' ? c.selfNames : {}
863
+ const k = 人称键(sid)
864
+ const n = (k && 表[k]) || 表[String(sid)]
840
865
  return typeof n === 'string' ? n.trim() : ''
841
866
  }
842
867
 
@@ -855,7 +880,9 @@ function titleNameOf(ctx, sid) {
855
880
  // 没设就退回全局的 userPronoun(默认 ta)。
856
881
  function ownerNameOf(sid) {
857
882
  const c = loadCfg()
858
- const n = c.ownerNames && c.ownerNames[String(sid)]
883
+ const 表 = c.ownerNames && typeof c.ownerNames === 'object' ? c.ownerNames : {}
884
+ const k = 人称键(sid)
885
+ const n = (k && 表[k]) || 表[String(sid)]
859
886
  return typeof n === 'string' ? n.trim() : ''
860
887
  }
861
888
 
@@ -866,6 +893,60 @@ function userPronoun() {
866
893
  return p || 'ta'
867
894
  }
868
895
 
896
+ // ⭐ 它自己自称什么:默认「我」。
897
+ // 跑角色设定比较重的模型时,它会用「本小姐」「在下」这类自称 ——
898
+ // 记忆里跟着它自己的说法写,比一律写「我」更像它自己。
899
+ function selfPronoun() {
900
+ const c = loadCfg()
901
+ const p = typeof c.selfPronoun === 'string' ? c.selfPronoun.trim() : ''
902
+ return p || '我'
903
+ }
904
+
905
+ // ⭐ 2026-11-04:这两样也能按会话设(工作区模式按工作区设)。
906
+ // 一般人都是一间会话一个对象,所以默认就跟着会话走;没设过才落回全局那个值。
907
+ function 人称键(sid) {
908
+ const s = String(sid || '')
909
+ if (!s) return ''
910
+ try {
911
+ return String(库名(s, { 模式: 读模式() }) || s)
912
+ } catch (error) {
913
+ return s
914
+ }
915
+ }
916
+
917
+ // 记忆里指对方用哪个字(ta / 她 / 他)
918
+ function pronounOf(sid) {
919
+ const c = loadCfg()
920
+ const k = 人称键(sid)
921
+ const v = k && c.pronouns && typeof c.pronouns === 'object' ? c.pronouns[k] : ''
922
+ return (typeof v === 'string' && v.trim()) || userPronoun()
923
+ }
924
+
925
+ // 记忆里它自己自称什么(我 / 本小姐 / 在下……)
926
+ function selfPronounOf(sid) {
927
+ const c = loadCfg()
928
+ const k = 人称键(sid)
929
+ const v = k && c.selfPronouns && typeof c.selfPronouns === 'object' ? c.selfPronouns[k] : ''
930
+ return (typeof v === 'string' && v.trim()) || selfPronoun()
931
+ }
932
+
933
+ // 提示词里凡是拿「我」写它自己的地方,换成它自己的自称(「我们」不算)
934
+ function 换自称成(文, 自称) {
935
+ const s = String(自称 || '').trim()
936
+ if (!s || s === '我') return 文
937
+ return String(文 || '').replace(/我(?!们)/g, s)
938
+ }
939
+ function 换自称(文) {
940
+ return 换自称成(文, selfPronoun())
941
+ }
942
+
943
+ // 提示词里拿「她」写对方的地方,换成这个会话设的那个字(ta / 她 / 他)
944
+ function 换称呼(文, 称呼) {
945
+ const s = String(称呼 || '').trim()
946
+ if (!s || s === '她') return 文
947
+ return String(文 || '').split('她').join(s)
948
+ }
949
+
869
950
  // 这些词不能当名字换(不然代词会被换坏)
870
951
  const NOT_A_NAME = new Set(['她', '他', '我', '你', 'ta', 'TA', 'Ta', 'tA', '用户', '助手', 'AI', 'ai', '人', '自己'])
871
952
 
@@ -889,7 +970,7 @@ function normalizeOwner(text, sid) {
889
970
  }
890
971
 
891
972
  function normalizePerson(text, name) {
892
- return normalizePersonAs(text, name, '我')
973
+ return normalizePersonAs(text, name, selfPronoun())
893
974
  }
894
975
 
895
976
  // 同一套「认边界」的替换:换成「我」(她自己)还是「你」(用户),由调用方定
@@ -920,14 +1001,23 @@ function normalizePersonAs(text, name, 换成) {
920
1001
 
921
1002
  // 用户的称呼改了,**已有记忆里那个字也要跟着变** ✓
922
1003
  // ⚠ 两种不能动:「其他」「其它」里的「他」不是称呼;英文单词里的 ta 也别切 ✗
923
- const PRONOUN_FORMS = ['ta', 'Ta', 'TA', 'tA', '他', '她']
1004
+ // ⚠ 只换「旧称呼」本身(config 里那个值,默认 ta)。
1005
+ // 「他 / 她」**不是称呼**,是代词 —— 书名、小说人物、引用里的他/她都不能动。
1006
+ // (2026-11-04 修:以前把它们也当称呼,改一次称呼就把《没有人给他写信的上校》改成
1007
+ // 《没有人给老猫写信的上校》,把小说人物也换成用户的名字,而且是直接写盘的。)
924
1008
  const ASCII_LETTER = /[a-zA-Z]/
925
1009
 
926
- function changeUserPronoun(text, to) {
1010
+ const __旧称呼表 = (旧称呼) => {
1011
+ const 旧 = String(旧称呼 || 'ta')
1012
+ return [...new Set([旧, 旧.toLowerCase(), 旧.toUpperCase()])]
1013
+ }
1014
+
1015
+ function changeUserPronoun(text, to, 旧称呼 = 'ta') {
927
1016
  let t = String(text || '')
928
1017
  if (!to) return t
929
- for (const from of PRONOUN_FORMS) {
930
- if (from === to || !t.includes(from)) continue
1018
+ for (const from of __旧称呼表(旧称呼)) {
1019
+ if (!from || from === to || !t.includes(from)) continue
1020
+ const 是英文 = /^[a-zA-Z]+$/.test(from)
931
1021
  let out = ''
932
1022
  let i = 0
933
1023
  for (;;) {
@@ -938,9 +1028,8 @@ function changeUserPronoun(text, to) {
938
1028
  }
939
1029
  const 前 = j > 0 ? t[j - 1] : ''
940
1030
  const 后 = t[j + from.length] || ''
941
- let 别动 = false
942
- if (from === '他' && 前 === '其') 别动 = true // 其他 / 其它
943
- if (/^[a-zA-Z]+$/.test(from) && (ASCII_LETTER.test(前) || ASCII_LETTER.test(后))) 别动 = true // 别切英文单词
1031
+ // 英文/拼音的 ta 别切单词内部;中文称呼直接换
1032
+ const 别动 = 是英文 && (ASCII_LETTER.test(前) || ASCII_LETTER.test(后))
944
1033
  out += t.slice(i, j) + (别动 ? from : to)
945
1034
  i = j + from.length
946
1035
  }
@@ -949,6 +1038,7 @@ function changeUserPronoun(text, to) {
949
1038
  return t
950
1039
  }
951
1040
 
1041
+
952
1042
  // ---------- 旧档案导入 ----------
953
1043
  // 设计参考 AAAAGENT 的「导入旧聊天」,几条原则照抄:
954
1044
  // ① 原日期保留(猜不到就不写,绝不编)② 助手的话只作语境、不当事实
@@ -1308,7 +1398,7 @@ function 换用户名字(text, 名字, to) {
1308
1398
  // 正文过一遍人称:她自己 →「我」,用户的名字 → 称呼(跟记的时候同一套规矩)
1309
1399
  function 导入时改人称(text, sid, 用户名字们) {
1310
1400
  let t = normalizePerson(text, selfNameOf(sid))
1311
- const p = userPronoun()
1401
+ const p = pronounOf(sid)
1312
1402
  for (const n of 用户名字们) {
1313
1403
  if (!n || n.length < 2) continue
1314
1404
  t = 换用户名字(t, n, p)
@@ -1671,7 +1761,7 @@ async function 自动扩词(ctx, sid, 上限 = 2) {
1671
1761
  if (!agent) { diag('自动扩词:' + sid + ' 这会儿没有活着的 agent,跳过'); return 0 }
1672
1762
  const 输入 = 要扩.map((m, k) => k + 1 + '|' + String(m.text || '')).join('\n')
1673
1763
  const t0 = Date.now()
1674
- const r = await 问模型(ctx, agent, 扩词系统_v2, 输入, 6000)
1764
+ const r = await 问模型(ctx, agent, 换自称成(换称呼(扩词系统_v2, pronounOf(sid)), selfPronounOf(sid)), 输入, 6000)
1675
1765
  if (!r || !r.ok) { diag('自动扩词:模型没回(' + ((r && r.message) || '?') + ')'); return 0 }
1676
1766
  const 表 = 解析扩词(r.text, 要扩.length)
1677
1767
  // ⚠⚠ 2026-10-24 修(用户报「记忆静默丢失」的**真凶**就在这几行):
@@ -1803,12 +1893,16 @@ function 提炼系统(称呼) {
1803
1893
  // ⭐⭐ 2026-09-22(反馈):这份清单**漏了「重要的日子」** ——
1804
1894
  // 实测 135 条该「永远不淡」的被标成会淡,重要的日子就在里面。
1805
1895
  // 原因就是这里没写「生日/纪念日/结婚」,模型不知道那也算「要一直记得的」✗
1806
- // ⚠ 措辞尽量短(「提示词、关键词得写得好一点」,token 也是钱)
1807
- ' 1 = 要一直记得的(性格、关系、约定和承诺、称呼和名字、喜好、身体和病史、' +
1808
- '**重要的日子:生日、纪念日、结婚、第一次**、长期的经济状况、家里的情况)\n' +
1809
- ' 0.5 = 平常的(发生过的事、当时的想法、一时的心情)\n' +
1896
+ // ⚠ 措辞尽量短(「提示词、关键词得写得好一点」,token 也是钱)
1897
+ // ⭐ 2026-11-04 修(反馈):这一格说的是「要不要每轮都带上」,不是「重不重要」。
1898
+ // 以前写成「重要 = 1」,模型就把「爱读什么书」「玩什么游戏」也打 1,
1899
+ // 常驻位只有 2~3 个,被日常喜好占满,真正该一直挂着的(忌讳、约定)反而进不来。
1900
+ ' 1 = **每轮都要挂在她眼前、不主动提就会出事**的(称呼和忌讳、明确的约定和承诺、' +
1901
+ '身体禁忌与过敏、重要的日子:生日、纪念日、结婚、第一次)\n' +
1902
+ ' 0.5 = 其余全部(性格、喜好、书单、游戏、发生过的事、当时的想法)—— **这些不需要每轮在场**,' +
1903
+ '聊到了自然会被搜回来\n' +
1810
1904
  ' 拿不准就填 0.5。这一格**必须填**,不许留空、不许写别的数。\n' +
1811
- ' ⚠ 填 1 的会**永远不淡**(一直想得起来),填 0.5 的会**慢慢淡掉** —— 所以重要的日子别漏。\n' +
1905
+ ' ⚠ 填 1 的会**每轮固定注入**(位置很少,只有两三个),填 0.5 的按需检索 —— 别把喜好占进去。\n' +
1812
1906
  // ⭐⭐ 2026-09-23(反馈第二点):**事实键** —— 「同一个 key 只留最新一条」✓
1813
1907
  // 根子:句子像不像**本来就不该拿来判「是不是同一件事」**。
1814
1908
  // 「她身高一米七」和「她长胖到 55 公斤」几乎是两件事,词面上却很像;
@@ -2184,8 +2278,12 @@ export function 补线索(query, 上文们, 最短 = 6, 上限 = 240) {
2184
2278
  export function 选查询词(会话里那句, 监听那句) {
2185
2279
  const 会话 = String(会话里那句 == null ? '' : 会话里那句).trim()
2186
2280
  const 监听 = String(监听那句 == null ? '' : 监听那句).trim()
2187
- if (会话) return 会话
2188
- return 监听
2281
+ // ⚠ 2026-11-04 修(反馈:检索永远慢一轮):
2282
+ // systemPrompt.context 的 text() 是在**请求装配时**求值的,而这一轮的 user 消息
2283
+ // 要等装配完才落进 session.snapshotEvents()。所以「会话里最后一句」拿到的是**上一轮**。
2284
+ // 监听 agent/inbox/inserted 拿到的 lastText 才是这一轮真话,必须优先。
2285
+ if (监听) return 监听
2286
+ return 会话
2189
2287
  }
2190
2288
 
2191
2289
  /**
@@ -3552,7 +3650,9 @@ function makeRememberTool(ctx) {
3552
3650
  },
3553
3651
  importance: {
3554
3652
  type: 'number',
3555
- description: '0 = 随口提过;0.5 = 比较重要;1 = 非常重要(约定、关系、她的核心状态)。不确定就写 0.5。',
3653
+ description:
3654
+ '**要不要每轮都挂在她眼前**(不是「重不重要」):1 = 不主动提就会出事的(称呼忌讳、约定承诺、' +
3655
+ '身体禁忌、生日纪念日);0.5 = 其余全部(性格、喜好、书单、游戏、发生过的事)。拿不准写 0.5。',
3556
3656
  },
3557
3657
  tags: {
3558
3658
  type: 'array',
@@ -3931,9 +4031,16 @@ function buildMemoryList(ctx) {
3931
4031
  out.sessions.push({
3932
4032
  id: sid,
3933
4033
  title: titleOfSession(ctx, sid),
4034
+ // ⭐ 1.5.4:工作区模式下,设置页按「工作区」分组改 —— 这两个字段给它用
4035
+ 工作区: 会话的工作区(sid) || '',
4036
+ 工作区名: 会话的工作区(sid) ? 工作区可读名(会话的工作区(sid)) : '',
4037
+ 工作区路径: 会话的工作区(sid) ? 工作区信息(会话的工作区(sid)).路径 : '',
3934
4038
  // ⭐ 1.2.9:这个会话里「怎么称呼对方」+「她自己叫什么」,设置页两个输入框要回显
3935
4039
  ownerName: ownerNameOf(sid),
3936
4040
  selfName: selfNameOf(sid),
4041
+ // ⭐ 1.5.5:这个会话(工作区模式按工作区)记忆里用哪个人称
4042
+ pronoun: pronounOf(sid),
4043
+ selfPronoun: selfPronounOf(sid),
3937
4044
  enabled: isEnabled(sid),
3938
4045
  live: liveIds.has(sid),
3939
4046
  parent: 分支来处Of(ctx, sid),
@@ -4018,6 +4125,30 @@ function buildMemoryList(ctx) {
4018
4125
  out.defaults = { ...DEFAULT_POLICY }
4019
4126
  out.version = VERSION
4020
4127
  out.userPronoun = userPronoun()
4128
+ out.selfPronoun = selfPronoun()
4129
+ // ⭐ 1.5.4:工作区模式下,设置页要按「工作区」单独改 —— 给它一份工作区清单
4130
+ try {
4131
+ if (out.模式 === 'workspace') {
4132
+ const 组 = new Map()
4133
+ for (const s of out.sessions) {
4134
+ const k = s.工作区 || ''
4135
+ if (!k) continue
4136
+ if (!组.has(k)) 组.set(k, { 键: 'ws_' + k, 工作区: k, 名: s.工作区名 || k, 路径: s.工作区路径 || '', 会话: [], 条数: 0 })
4137
+ const g = 组.get(k)
4138
+ g.会话.push(s.id)
4139
+ g.条数 = Math.max(g.条数, Number(s.count) || 0)
4140
+ }
4141
+ out.工作区们 = [...组.values()].map((g) => ({
4142
+ ...g,
4143
+ selfName: selfNameOf('ws_' + g.工作区),
4144
+ ownerName: ownerNameOf('ws_' + g.工作区),
4145
+ pronoun: pronounOf('ws_' + g.工作区),
4146
+ selfPronoun: selfPronounOf('ws_' + g.工作区),
4147
+ }))
4148
+ }
4149
+ } catch (error) {
4150
+ diag('工作区清点失败:' + ((error && error.message) || error))
4151
+ }
4021
4152
  return out
4022
4153
  }
4023
4154
 
@@ -4241,13 +4372,13 @@ export function apply(ctx) {
4241
4372
  // (这里会把用户设的「称呼」拼进去,称呼里带 {{ }} 一样会炸)
4242
4373
  interpolate: false,
4243
4374
  text: () => {
4244
- const p = ownerNameOf(sid) || userPronoun()
4375
+ const p = ownerNameOf(sid) || pronounOf(sid)
4245
4376
  const self = selfNameOf(sid)
4246
4377
  return (
4247
4378
  '【记忆】这一轮如果有值得长期记住的事(' + p + '重要的话、约定、' + p + '的喜好、' + p + '的状态),' +
4248
4379
  '直接调用 remember 工具记下来 —— **不用等' + p + '提醒**。' +
4249
4380
  '日常琐事(吃了什么、今天天气)不用记。' +
4250
- '**人称**:你自己是「我」,用户是「' + p + '」,别人和别的 AI 直接写名字' +
4381
+ '**人称**:你自己是「' + selfPronounOf(sid) + '」,用户是「' + p + '」,别人和别的 AI 直接写名字' +
4251
4382
  (self ? '(你写成「' + self + '」也会自动换成「我」)' : '') +
4252
4383
  ';也不要写「今天/昨天」,时间交给系统。'
4253
4384
  )
@@ -4287,8 +4418,11 @@ export function apply(ctx) {
4287
4418
  text: () => {
4288
4419
  try {
4289
4420
  const now = Date.now()
4290
- // ⭐ 1.4.0:**这一轮真正的那句优先**(会话里最后一条 user 消息);
4291
- // 监听那句只在会话读不出来时兜底 —— 见 选查询词() 上面的说明。
4421
+ // ⚠ 2026-11-04 修:**监听那句(这一轮刚收到的)优先**。
4422
+ // 原来的注释假设「会话里最后一条 user 消息就是这一轮」,在 DSH 这个时序下不成立:
4423
+ // systemPrompt.context 在请求装配时求值,而 user 消息要等装配完才落进 session,
4424
+ // 于是永远拿到上一轮 —— 实测问「莱拉是谁」,拿去检索的是前几轮那句 49 字的闲聊。
4425
+ // 会话那句只在监听为空时兜底(插件中途挂上、重启后第一轮还没收到消息)。
4292
4426
  let query = 清掉注入上下文(选查询词(lastUserTextOf(agent.session), lastText))
4293
4427
  // ⭐ 1.4.2:一两个字的句子(「推」「在吗」)单独去搜什么都搜不到 —— 借上文一起搜
4294
4428
  try {
@@ -5076,10 +5210,11 @@ export function apply(ctx) {
5076
5210
  '你是记忆整理助手。下面每条是一条记忆,格式是「编号|内容」。\n' +
5077
5211
  '请判断每条**该不该一直记得**(永远不淡),只回两档。\n' +
5078
5212
  '\n' +
5079
- '判据只有一条:**这条记的是「一直成立的」,还是「某天发生的」?**\n' +
5080
- '1 = **一直成立**:性格、关系、称呼和名字、喜好、身体和病史、\n' +
5081
- ' 约定和承诺本身、长期的经济状况、家里的情况、长期的住处或工作。\n' +
5082
- '0.5 = **某天发生的**:带日期的流水账、当时的心情、一次性的经历、某次对话。\n' +
5213
+ '判据只有一条:**不主动提,会不会出事?** 常驻位只有两三个。\n' +
5214
+ '1 = **每轮都得在场**:称呼和忌讳(不能叫错、不能碰的)、明确的约定和承诺、\n' +
5215
+ ' 身体禁忌与过敏、一直成立的硬事实(名字、家里的结构)。\n' +
5216
+ '0.5 = **其余全部**:性格、喜好、书单、游戏、长期的住处或工作、某天发生的任何事。\n' +
5217
+ ' (这些聊到了自然会被搜回来,不需要每轮挂着。)\n' +
5083
5218
  '\n' +
5084
5219
  '⚠⚠ **以日期开头的(09-17 / 8月26日 / 2026-09-04…),默认一律 0.5。**\n' +
5085
5220
  ' 哪怕那件事很重要、很感人、是第一次、是求婚、是婚礼 —— 都算「某天发生的」。\n' +
@@ -5203,7 +5338,7 @@ export function apply(ctx) {
5203
5338
  const c = loadCfg()
5204
5339
  const 改了几条 = []
5205
5340
  const 换了称呼的 = []
5206
- const 称呼旧值 = userPronoun()
5341
+ const 称呼旧值 = b.session ? pronounOf(String(b.session)) : userPronoun()
5207
5342
  let 称呼新值 = null
5208
5343
  if (typeof b.userPronoun === 'string') {
5209
5344
  const p = b.userPronoun.trim()
@@ -5218,6 +5353,34 @@ export function apply(ctx) {
5218
5353
  else delete c.archiveDir
5219
5354
  diag('档案目录设成:' + (c.archiveDir || '(还没选)'))
5220
5355
  }
5356
+ // ⭐ 它自己自称什么(默认「我」)。
5357
+ // 跟会话一起传过来 = 这个会话(工作区模式是这个工作区)的;不带会话 = 全局默认。
5358
+ if (typeof b.selfPronoun === 'string') {
5359
+ const p = b.selfPronoun.trim()
5360
+ const k0 = b.session ? 人称键(String(b.session)) : ''
5361
+ if (k0) {
5362
+ if (!c.selfPronouns || typeof c.selfPronouns !== 'object') c.selfPronouns = {}
5363
+ if (p && p !== '我') c.selfPronouns[k0] = p
5364
+ else delete c.selfPronouns[k0]
5365
+ diag('这个会话的它自称:' + k0 + ' → 「' + (p || '(默认 我)') + '」')
5366
+ } else if (p && p !== '我') {
5367
+ c.selfPronoun = p
5368
+ diag('它的自称设成:' + c.selfPronoun)
5369
+ } else {
5370
+ delete c.selfPronoun
5371
+ diag('它的自称设成:' + '(默认 我)')
5372
+ }
5373
+ }
5374
+ // ⭐ 记忆里指对方用哪个字,也能按会话设
5375
+ if (b.session && typeof b.pronoun === 'string') {
5376
+ const k0 = 人称键(String(b.session))
5377
+ if (!c.pronouns || typeof c.pronouns !== 'object') c.pronouns = {}
5378
+ const p = b.pronoun.trim()
5379
+ if (p && p !== 'ta') c.pronouns[k0] = p
5380
+ else delete c.pronouns[k0]
5381
+ 称呼新值 = p || 'ta'
5382
+ diag('这个会话记忆里指对方用:' + k0 + ' → 「' + (p || '(默认 ta)') + '」')
5383
+ }
5221
5384
  if (Array.isArray(b.notNames)) {
5222
5385
  c.notNames = b.notNames
5223
5386
  .filter((x) => typeof x === 'string' && x.trim())
@@ -5283,17 +5446,31 @@ export function apply(ctx) {
5283
5446
  let 称呼影响几条 = 0
5284
5447
  let 称呼例子 = []
5285
5448
  const 要改 = b.applyToExisting === true
5286
- const 目标称呼 = 要改 ? userPronoun() : 称呼新值 !== null && 称呼新值 !== 称呼旧值 ? 称呼新值 : null
5449
+ const 会话号 = String(b.session || '')
5450
+ const 目标称呼 = 要改
5451
+ ? 会话号
5452
+ ? pronounOf(会话号)
5453
+ : userPronoun()
5454
+ : 称呼新值 !== null && 称呼新值 !== 称呼旧值
5455
+ ? 称呼新值
5456
+ : null
5287
5457
  if (目标称呼 !== null) {
5288
5458
  try {
5289
- for (const fn of readdirSync(STORE_DIR)) {
5290
- if (!fn.endsWith('.json')) continue
5291
- const sid = fn.replace(/\.json$/, '')
5459
+ // ⭐ 1.5.5:改成**只洗这个会话(工作区模式是这个工作区)的库**。
5460
+ // 以前扫全部库 —— A 会话改个叫法,B 会话的记忆也被一起洗了。
5461
+ const 只洗 = 会话号 ? String(库名(会话号, { 模式: 读模式() }) || 会话号) : ''
5462
+ const 名单 = 只洗
5463
+ ? [只洗]
5464
+ : readdirSync(STORE_DIR)
5465
+ .filter((fn) => fn.endsWith('.json'))
5466
+ .map((fn) => fn.replace(/\.json$/, ''))
5467
+ for (const sid of 名单) {
5468
+ const fn = sid + '.json'
5292
5469
  const data = loadStore(sid)
5293
5470
  let 动 = false
5294
5471
  for (const 区 of ['memories', 'deleted']) {
5295
5472
  for (const m of data[区] || []) {
5296
- const 新 = changeUserPronoun(m.text, 目标称呼)
5473
+ const 新 = changeUserPronoun(m.text, 目标称呼, 称呼旧值 || 'ta')
5297
5474
  if (新 !== m.text) {
5298
5475
  称呼影响几条++
5299
5476
  if (称呼例子.length < 3) 称呼例子.push({ id: m.id, 旧: m.text, 新: 新 })
@@ -5304,7 +5481,17 @@ export function apply(ctx) {
5304
5481
  }
5305
5482
  }
5306
5483
  }
5307
- if (要改 && 动) saveStore(sid, data)
5484
+ if (要改 && 动) {
5485
+ // ⚠ 改称呼是**永久写盘**,先留一份快照,出问题能一键回退
5486
+ try {
5487
+ const 原文件 = join(STORE_DIR, sid + '.json')
5488
+ if (existsSync(原文件)) {
5489
+ const 时 = new Date().toISOString().replace(/[:.]/g, '-')
5490
+ copyFileSync(原文件, 原文件 + '.bak-person-' + 时)
5491
+ }
5492
+ } catch {}
5493
+ saveStore(sid, data)
5494
+ }
5308
5495
  }
5309
5496
  } catch (error) {}
5310
5497
  if (要改) diag('称呼改成「' + 目标称呼 + '」,顺手改了 ' + 称呼影响几条 + ' 条记忆')
@@ -5320,6 +5507,7 @@ export function apply(ctx) {
5320
5507
  userNames: 用户名字列表(),
5321
5508
  llmModel: 选定模型(),
5322
5509
  洗了: 改了几条.length,
5510
+ 称呼: b.session ? pronounOf(String(b.session)) : userPronoun(),
5323
5511
  称呼影响几条,
5324
5512
  称呼例子,
5325
5513
  称呼已改: b.applyToExisting === true && 称呼影响几条 > 0,
@@ -5714,9 +5902,17 @@ export function apply(ctx) {
5714
5902
  m.importance = imp
5715
5903
  m.category = imp >= 1 ? 'stable_profile' : 'event'
5716
5904
  }
5905
+ // ⭐ 检索词能当场改:模型给的词经常不靠谱,人直接改比重新扩一遍快,也不动正文
5906
+ if (Array.isArray(b.related)) {
5907
+ const 词 = b.related.map((x) => String(x || '').trim()).filter(Boolean).slice(0, 12)
5908
+ m.related = 词
5909
+ m.relatedAt = new Date().toISOString()
5910
+ m.relatedBy = '手动'
5911
+ diag('改检索词:' + sid + ' / ' + id + ' → ' + 词.join(' '))
5912
+ }
5717
5913
  saveStore(sid, data)
5718
5914
  diag('改了一条:' + sid + ' / ' + id + ' 「' + 旧.slice(0, 30) + '」→「' + m.text.slice(0, 30) + '」')
5719
- out = { ok: true, message: '改好了', text: m.text, importance: m.importance, category: m.category }
5915
+ out = { ok: true, message: '改好了', text: m.text, importance: m.importance, category: m.category, related: Array.isArray(m.related) ? m.related : null }
5720
5916
  }
5721
5917
  } catch (error) {
5722
5918
  out = { ok: false, message: String((error && error.message) || error) }
@@ -6368,7 +6564,7 @@ export function apply(ctx) {
6368
6564
  const t0 = Date.now()
6369
6565
  // ⚠ 预算要给够:模型会先「思考」,思考的 token 也算在里面。
6370
6566
  // 给 2400 的时候实测被思考吃光、正文长度 0、整批被误判成「跳过」✗
6371
- const r = await 问模型(ctx, agent, 提炼系统(userPronoun()), 输入, 6000)
6567
+ const r = await 问模型(ctx, agent, 换自称成(提炼系统(pronounOf(sid)), selfPronounOf(sid)), 输入, 6000)
6372
6568
  const ms = Date.now() - t0
6373
6569
  if (!r.ok) {
6374
6570
  out = { ok: false, message: r.message, 剩: 要提.length, provider: r.provider, model: r.model, ms: ms }
@@ -6555,7 +6751,7 @@ mroute('/dsh-fish-memory/wash-keys', async (req, res) => {
6555
6751
  已有键.length = Math.min(已有键.length, 400)
6556
6752
  }
6557
6753
  const t0 = Date.now()
6558
- const r = await 问模型(ctx, agent, 洗键系统(userPronoun(), 已有键), 输入, 6000)
6754
+ const r = await 问模型(ctx, agent, 换自称成(洗键系统(pronounOf(sid), 已有键), selfPronounOf(sid)), 输入, 6000)
6559
6755
  const ms = Date.now() - t0
6560
6756
  if (!r.ok) {
6561
6757
  out = { ok: false, message: r.message, 剩: 要洗.length, provider: r.provider, model: r.model, ms: ms }
@@ -6880,7 +7076,7 @@ mroute('/dsh-fish-memory/wash-keys/undo', async (req, res) => {
6880
7076
  // 实测 6 条的时候:**正文 167 字,思考 2912 字**(思考是答案的 17 倍)——
6881
7077
  // 原来只给 900,连思考都不够,正文永远是空的,
6882
7078
  // 表现就是「连着几批没回可用的词,自己停了」✗(2026-09-20 踩到)
6883
- const r = await 问模型(ctx, agent, 扩词系统_v2, 输入, 6000)
7079
+ const r = await 问模型(ctx, agent, 换自称成(换称呼(扩词系统_v2, pronounOf(sid)), selfPronounOf(sid)), 输入, 6000)
6884
7080
  const ms = Date.now() - t0
6885
7081
  if (!r.ok) {
6886
7082
  out = { ok: false, message: r.message, 剩: 要扩.length, provider: r.provider, model: r.model, ms: ms }
@@ -7041,4 +7237,12 @@ export const __测试 = {
7041
7237
  库名,
7042
7238
  读模式,
7043
7239
  合并工作区记忆,
7240
+ // ⭐ 1.5.4:「它自称什么」是纯字符串替换,单独露出来好单测
7241
+ 换自称成,
7242
+ 换称呼,
7243
+ // ⭐ 1.5.4:人称现在跟着会话走,这两条是「会话 → 库」的取值口
7244
+ 人称键,
7245
+ pronounOf,
7246
+ selfPronounOf,
7247
+ 自称: () => selfPronoun(),
7044
7248
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-fish-memory",
3
- "version": "1.5.2",
3
+ "version": "1.5.4",
4
4
  "description": "给 DSH 里的会话一份会淡忘的长期记忆,每条都带时间。",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "module",
@@ -57,4 +57,4 @@
57
57
  "optional": true
58
58
  }
59
59
  }
60
- }
60
+ }