dsh-lite-memory 0.15.0 → 0.16.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.
package/README.md CHANGED
@@ -23,7 +23,7 @@ Lightweight memory for the DeepSeek Harness. No standing system prompt, nothing
23
23
 
24
24
  写入时分两层:默认只在这个项目生效;用户说「以后所有项目都这样」才进全局层(200 条起提示,硬上限 1000 条),检索时一起查。同一主题在不同前提下用 `when` 区分,各存一条。
25
25
 
26
- 写入前先蒸馏:标题是结论,正文只留原则、判断规则和触发条件,不把具体尺寸、字号、层级、颜色或一次性数字记进来;需要这些值时按记忆里的指针查当前代码或文档。单条正文超过 1200 字节会被拒绝,先删细节再存。
26
+ 写入前先蒸馏:标题是结论,正文只留原则、判断规则和触发条件,不把具体尺寸、字号、层级、颜色或一次性数字记进来;需要这些值时按记忆里的指针查当前代码或文档。单条正文超过 4096 字节会被拒绝,先删细节再存;摘要超长自动截到 60 字。
27
27
 
28
28
  触发词:`记住`、`记一下`、`上次`、`规范`、`约定`、`路径`、`凭据怎么取`。
29
29
 
@@ -39,7 +39,7 @@ Those lines belong to the turn that made them: each turn is rebuilt from its own
39
39
 
40
40
  Writes land in one of two layers: the current project by default, or the global layer (warns past 200, hard ceiling 1000) when the user says the rule holds for every project. Both layers are searched together, and a `when` field separates the same subject under different preconditions.
41
41
 
42
- Before writing, distill: the title is the conclusion and the body keeps only the principle, decision rule and trigger condition. Concrete dimensions, font sizes, z-index, colors and one-off numbers stay out; the memory points at where to read the current value instead. A body over 1200 bytes is refused, so drop detail before saving.
42
+ Before writing, distill: the title is the conclusion and the body keeps only the principle, decision rule and trigger condition. Concrete dimensions, font sizes, z-index, colors and one-off numbers stay out; the memory points at where to read the current value instead. A body over 4096 bytes is refused, so drop detail before saving; an over-long summary is truncated to 60 characters.
43
43
 
44
44
  ## 点一下就存 / One click, no turn
45
45
 
@@ -80,13 +80,13 @@ On the Memories tab, the search row also switches between list and graph; either
80
80
  - **0 依赖**,存储层无定时器、无监听、无网络,只有同步单文件写;写入用一把有超时的锁,别的进程正在写就明确报「忙」,不抢别人的锁。
81
81
  - 搜索最多 4 条、400 字;正文只在 `memory_read` 里出现,全文永不进上下文。
82
82
  - 普通检索只列当前有效的记录;已经归档或被更新替换的旧说法不混进来,要查历史得在 `memory_search` 里显式要 `history`。
83
- - 上限:单条正文 1200 字节、整库正文 512 KB、每层 1000 条、标题与摘要各 60 字;每层 200 条只当提示线。90 天未用只留 `id + 标题`。到了硬上限不拒绝新记忆:把同层最久没被读的一条移进归档,保存结果里写明是哪条(正文与完整记录都留着,可人工取回),单条本身超预算才报错;用户明确说「别记」也进归档。文件从不删除。
83
+ - 上限:单条正文 4096 字节、整库正文 1 MB、每层 1000 条、标题与摘要各 60 字;每层 200 条只当提示线。90 天未用只留 `id + 标题`。到了硬上限不拒绝新记忆:把同层最久没被读的一条移进归档,用户固定过的规则不动,保存结果里写明是哪条(正文与完整记录都留着,可人工取回),单条本身超预算才报错;用户明确说「别记」也进归档。文件从不删除。
84
84
  - 一个项目一个目录,项目之间互相看不见。
85
85
 
86
86
  - **Zero dependencies.** The store has no timers, no listeners, no network — just synchronous single-file writes, guarded by a lock that gives up after 500 ms instead of stealing it.
87
87
  - Search returns at most 4 hits and 400 characters. Full text only appears in `memory_read`.
88
88
  - Normal search lists current entries only; archived notes and superseded wording stay out unless `memory_search` asks for `history`.
89
- - Limits: 1200 bytes per body, 512 KB of bodies per store, 1000 entries per layer, 60 characters per title or summary; 200 entries per layer is just a warning line. At the hard ceiling a save is never refused: the least recently read entry in the same layer moves to archive and the result names it (body and full record kept, restorable by hand); only an entry that alone exceeds the budget fails. Saying 「别记」 also archives. Files are never deleted.
89
+ - Limits: 4096 bytes per body, 1 MB of bodies per store, 1000 entries per layer, 60 characters per title or summary; 200 entries per layer is just a warning line. At the hard ceiling a save is never refused: the least recently read entry in the same layer moves to archive (pinned rules are never evicted) and the result names it (body and full record kept, restorable by hand); only an entry that alone exceeds the budget fails. Saying 「别记」 also archives. Files are never deleted.
90
90
  - One directory per project, so projects never see each other's notes.
91
91
 
92
92
  ## 项目自进化〔实验〕 / Project self-evolution (experimental)
package/anchor.js CHANGED
@@ -16,12 +16,22 @@ const REFERENCE_WORDS = /(这个|这条|这次|上次|之前|刚才|上面|前
16
16
  const TASK_STOP = new Set(['一个','一下','这个','那个','我们','你们','他们','什么','怎么','可以','需要','帮我','帮忙','请问','然后','现在','还是','就是','没有','不是','以及','关于','进行','使用','问题','任务','内容','时候','已经','如果','因为','所以','但是','而且','或者','这些','那些','这样','那样','一下','一点','一些']);
17
17
  /** 判断任务是否换了对象时,这些词只表示动作或指代,不算对象证据。 */
18
18
  const TASK_NOISE = /(继续|再做一期|下一期|再来一期|再开一期|续一集|再拍一期|接着|然后|接下来|现在|帮我|帮忙|这个|这条|这次|上次|之前|刚才|上面|前面|下面|后面|这里|还是|同样|再改|排查|处理|检查|看看|看一下|看下|修复|解决|优化|调整|修改|分析|研究|对比|评估|调研|测试|设计|实现|开发|搭建|部署|定位|复现|查一下|找一下|问题|任务|内容|一下)/g;
19
- const NEW_OBJECT_VERB = /(排查|检查|看看|看一下|看下|分析|研究|对比|评估|调研|测试|设计|实现|开发|搭建|部署|修复|解决|处理|定位|复现|查一下|找一下)/;
20
- const NEW_TASK_MARKER = /(现在|接下来|然后|下面|新任务|另一个|重新|改做|换个|换一个)/;
21
19
  /** 续接词后面还得跟一个动作,才算真正在改旧稿;单说「继续 / 这个 / 之前」不构成复用依据。 */
22
20
  const CONTINUATION_ACTION = /(继续|接着|然后|再|还是|同样)\s*(做|改|调|弄|看|写|处理|优化|调整|检查|完成)/;
21
+ /** 明确指向刚才那一版作品的说法,是沿用旧范围的正面证据;「这版/那版」只是指示,不算旧稿。 */
22
+ const OLD_WORK_REFERENCE = /(刚才那版|之前那版|上一版|原稿|旧稿)/;
23
+ /** 旧稿被放下、否定或只作为话题提起时,不能因为它出现就恢复旧范围。 */
24
+ const OLD_WORK_PAUSE = /(先放着|放着|放一边|先不|先别|别管|不用管|搁置|暂停|不做了|先不做|不用了)/;
25
+ const OLD_WORK_ACTION = /(继续|接着|再改|改成|改一下|调整|优化|改改|开头|结尾|结构|节奏|语气|口吻|文案|措辞|细节|版本)/;
26
+ /** 创作类与工程类任务的默认判断不同;同一主题从写口播切到排查代码,不能沿用旧栏目。 */
27
+ const CONTENT_INTENT = /(口播|拍摄|录制|剪辑|文案|封面|字幕|直播|视频|选题|脚本|一期|栏目)/;
28
+ const ENGINEERING_INTENT = /(代码|排查|故障|部署|调试|重构|接口|数据库|查询|报错|日志|服务|环境|依赖|测试|配置|修复)/;
29
+ /** 这些词描述的是旧稿的某个方面,不是新对象;不该因为它们出现就撤销旧范围。 */
30
+ const ASPECT_NOISE = /(节奏|速度|风格|语气|口吻|结构|长度|时长|篇幅|配色|字号|字体|标题|文案|措辞|逻辑|细节|开头|结尾|过渡|画面|镜头|音乐|音效|字幕|参数|格式|排版)/g;
31
+ /** 程度、指代和虚词组成的二字片段不是对象证据。 */
32
+ const GENERIC_CHARS = /[再更稍很太挺点些快慢多少好坏大小高低长短早晚新旧一二三四五六七八九十百千万个把的得地了着过吗呢吧啊]/;
23
33
  function taskTerms(value) {
24
- const plain = text(value, 200).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').replace(TASK_NOISE, ' ').replace(/\s+/g, ' ').trim();
34
+ const plain = text(value, 200).toLowerCase().replace(/[^\p{L}\p{N}]+/gu, ' ').replace(ASPECT_NOISE, ' ').replace(TASK_NOISE, ' ').replace(/\s+/g, ' ').trim();
25
35
  const terms = new Set();
26
36
  for (const word of plain.match(/[a-z][a-z0-9_-]{2,}/g) || []) terms.add(word);
27
37
  const runs = plain.match(/[\u4e00-\u9fff]+/g) || [];
@@ -29,7 +39,8 @@ function taskTerms(value) {
29
39
  if (TASK_STOP.has(run)) continue;
30
40
  for (let i = 0; i + 2 <= run.length; i += 1) {
31
41
  const gram = run.slice(i, i + 2);
32
- if (!TASK_STOP.has(gram)) terms.add(gram);
42
+ if (TASK_STOP.has(gram) || GENERIC_CHARS.test(gram)) continue;
43
+ terms.add(gram);
33
44
  }
34
45
  }
35
46
  return terms;
@@ -37,19 +48,28 @@ function taskTerms(value) {
37
48
  /** 继承旧任务要有正面证据:新要求与本轮任务没有实质对象交集时,先按新任务处理。 */
38
49
  export function looksLikeNewTask(query, previousText) {
39
50
  const current = text(query, 200);
40
- if (!current || isThanks(current) || isRevision(current)) return false;
51
+ if (!current || isThanks(current)) return false;
41
52
  const next = taskTerms(current);
42
53
  const previous = taskTerms(previousText);
43
54
  const cue = isContinuation(current) || REFERENCE_WORDS.test(current);
44
55
  /** 只剩指代或续接词时,必须另有动作才算续接;否则宁可换任务也不沿用旧范围。 */
45
56
  if (!next.size) return cue ? !CONTINUATION_ACTION.test(current) : false;
46
57
  if (!previous.size) return false;
58
+ /** 同一主题换了工作类型(口播 → 排查代码)是新任务,不靠对象词重叠判续作。 */
59
+ const nextKind = CONTENT_INTENT.test(current) ? (ENGINEERING_INTENT.test(current) ? '' : 'content')
60
+ : (ENGINEERING_INTENT.test(current) ? 'engineering' : '');
61
+ const previousKind = CONTENT_INTENT.test(previousText) ? (ENGINEERING_INTENT.test(previousText) ? '' : 'content')
62
+ : (ENGINEERING_INTENT.test(previousText) ? 'engineering' : '');
63
+ if (nextKind && previousKind && nextKind !== previousKind) return true;
47
64
  for (const term of next) if (previous.has(term)) return false;
48
- /** 只有指代或「继续」时,还要有明确的新对象动作、转向词或新英文对象才换任务。 */
49
- if (!cue) return true;
50
- if (NEW_OBJECT_VERB.test(current) || NEW_TASK_MARKER.test(current)) return true;
51
- for (const term of next) if (/^[a-z]/.test(term)) return true;
52
- return false;
65
+ /** 指向旧稿还要有继续改它的动作;「旧稿先放着」「这版数据库配置怎么改」都算新任务。 */
66
+ if (OLD_WORK_REFERENCE.test(current) && !OLD_WORK_PAUSE.test(current) && OLD_WORK_ACTION.test(current)) return false;
67
+ /**
68
+ * 有实质对象词却跟旧任务没有任何交集:这就是新任务,不因为句首带了
69
+ * 「继续」「这个」就把旧范围连同正文一起搬过来。继承只认对象交集、
70
+ * 明确的旧稿指代,或者除动作/指代外没有新对象(上面两条)。
71
+ */
72
+ return true;
53
73
  }
54
74
 
55
75
  export function emptyTasks() {
@@ -76,6 +96,27 @@ export function isRevision(query) {
76
96
  return REVISE_WORDS.test(text(query, 160));
77
97
  }
78
98
 
99
+ const NEGATED_SERIES = /(不要|不用|别|不按|不照|不是|并非|先不|不做|不搞|忽略|跳过|不管|别提)[^,。;!?\n]{0,10}$/;
100
+ const MENTION_SERIES = /(日志|报错|错误|信息|提到|说到|包含|关键词|这个词|源码|代码|插件|文档|规则名|栏目名|分析)[^,。;!?\n]{0,10}$/;
101
+ const PAUSED_SERIES = /^(先放着|先放|放一边|先不做|不做了|不用了|暂停)/;
102
+ const PAUSED_BEFORE = /(先放着|先放|放一边|先不做|不做了|不用了|暂停)[^,。;!?\n]{0,4}$/;
103
+ /** 提到栏目名不等于选中它:否定、引用、看源码或「先放着」都只是话题,不能建绑定。 */
104
+ export function seriesChosenBy(query, series) {
105
+ const value = text(query, 400);
106
+ const name = text(series, 40);
107
+ if (!value || !name) return false;
108
+ let from = 0;
109
+ while (from <= value.length) {
110
+ const at = value.indexOf(name, from);
111
+ if (at < 0) return false;
112
+ const before = value.slice(Math.max(0, at - 14), at);
113
+ const after = value.slice(at + name.length);
114
+ if (!NEGATED_SERIES.test(before) && !MENTION_SERIES.test(before) && !PAUSED_BEFORE.test(before) && !PAUSED_SERIES.test(after)) return true;
115
+ from = at + name.length;
116
+ }
117
+ return false;
118
+ }
119
+
79
120
  export function requestBatchKey(messageIds) {
80
121
  const ids = [...new Set((Array.isArray(messageIds) ? messageIds : []).map((id) => String(id || '')).filter(Boolean))].sort();
81
122
  if (!ids.length) return '';
@@ -271,7 +312,7 @@ export function routeTask(tasksInput, input = {}) {
271
312
  }
272
313
  const registered = (Array.isArray(input.registeredSeries) ? input.registeredSeries : []).map((item) => text(item, 40)).filter(Boolean);
273
314
  /** 用户在这句话里点名的栏目优先于旧栏目和项目默认值。 */
274
- const namedSeries = registered.find((series) => series && query.includes(series)) || '';
315
+ const namedSeries = registered.find((series) => seriesChosenBy(query, series)) || '';
275
316
  const changedSeries = current && namedSeries && namedSeries !== (current.seriesId || '') ? namedSeries : '';
276
317
  const fallbackKind = (currentEpisode && currentEpisode.taskKind)
277
318
  || (input.defaults && input.defaults.taskKind) || '';
@@ -311,9 +352,10 @@ export function routeTask(tasksInput, input = {}) {
311
352
  return { tasks, anchor: current, episode: currentEpisode, decision: { kind: 'none', reason: 'thanks', pointer: true }, changed: false };
312
353
  }
313
354
 
314
- /** 先认新对象,再认续作:带「继续 / 这个」但换了对象,也必须撤销旧范围。 */
355
+ /** 先认新对象,再认续作:带「继续 / 这个」但换了对象,也必须撤销旧范围。
356
+ * 本句点名了已登记栏目(「再做一期口播」)就是明确的对象证据,不算换任务。 */
315
357
  if (current && currentEpisode && currentEpisode.status !== 'unbound'
316
- && !SWITCH_WORDS.test(query) && !changedSeries
358
+ && !SWITCH_WORDS.test(query) && !changedSeries && !namedSeries
317
359
  && looksLikeNewTask(query, currentEpisode.taskText || '')) {
318
360
  const transitionId = transitionIdFor(batchKey, tasks.revision);
319
361
  const existing = Object.values(tasks.episodes).find((episode) => episode.transitionId === transitionId);
package/client.js CHANGED
@@ -315,6 +315,7 @@ window.__ModuleLoader__.load({
315
315
  return h('div', { 'data-tool': props.toolName, style: ROW }, h('span', { style: DOT }, '·'), h('span', { style: LABEL }, label), button);
316
316
  };
317
317
 
318
+ const scopeTag = (scope) => (scope === 'global' ? '[全局]' : '[项目]');
318
319
  const versionLine = (shown) => {
319
320
  const scope = shown.scopeLabel || (shown.scope === 'global' ? '全局' : '项目');
320
321
  const archived = shown.status === 'archived' ? ' · 已归档' : '';
@@ -389,7 +390,7 @@ window.__ModuleLoader__.load({
389
390
  'aria-label': `查看记忆 ${item.title || item.id}`,
390
391
  title: item.title || item.id,
391
392
  style: ITEM, // ui-copy-ignore
392
- }, `${item.title || item.id}${item.origin === 'project_evolution' ? ' · 实验' : ''}`)) : [];
393
+ }, `${scopeTag(item.scope)} ${item.title || item.id}${item.origin === 'project_evolution' ? ' · 实验' : ''}`)) : [];
393
394
  const detail = shown && shown.ref && items.some((item) => item.id === shown.id && item.revision === shown.ref.revision)
394
395
  ? h('div', { style: BODY },
395
396
  shown.loading ? '读取中' : null,
@@ -686,8 +687,10 @@ window.__ModuleLoader__.load({
686
687
  const primaryKind = props.primaryKind || 'memory';
687
688
  const graph = React.useMemo(() => (props.graphOf || graphOf)(props.records), [props.records, props.graphOf]);
688
689
  const [zoom, setZoom] = React.useState(1);
690
+ const [hoveredId, setHoveredId] = React.useState('');
689
691
  const selectedId = props.selectedId;
690
- const labelSet = graph.people <= 10;
692
+ const labelSet = graph.people <= 40;
693
+ const pickHub = props.onPickHub;
691
694
  const stepZoom = (delta) => setZoom((value) => Math.min(2.5, Math.max(0.75, Math.round((value + delta) * 100) / 100)));
692
695
  const frame = React.useMemo(() => {
693
696
  const list = graph.nodes;
@@ -765,26 +768,48 @@ window.__ModuleLoader__.load({
765
768
  h('g', null, graph.nodes.map((node) => {
766
769
  const isPrimary = node.kind === primaryKind;
767
770
  const active = node.id === selectedId;
771
+ const hovered = node.id === hoveredId;
772
+ const pick = isPrimary ? props.onPick : (pickHub || undefined);
768
773
  const fill = !isPrimary ? 'var(--dsw-alias-label-secondary)'
769
774
  : node.state ? (STAGE_TONE[node.state] || 'var(--dsw-alias-brand-primary)')
770
775
  : node.experimental ? 'var(--dsw-alias-state-warn-primary)' : 'var(--dsw-alias-brand-primary)';
771
- const showLabel = active || (isPrimary && labelSet) || !isPrimary;
776
+ const showLabel = active || hovered || (isPrimary && labelSet) || !isPrimary;
777
+ const nameChars = active || hovered ? 14 : isPrimary ? 7 : 10;
772
778
  return h('g', {
773
779
  key: node.id,
774
780
  transform: `translate(${node.x} ${node.y})`,
775
- onClick: isPrimary ? () => props.onPick(node) : undefined,
776
- style: { cursor: isPrimary ? 'pointer' : 'default' },
781
+ onClick: pick ? () => pick(node) : undefined,
782
+ onMouseEnter: () => setHoveredId(node.id),
783
+ onMouseLeave: () => setHoveredId((value) => (value === node.id ? '' : value)),
784
+ style: { cursor: pick ? 'pointer' : 'default' },
777
785
  },
786
+ h('circle', { r: Math.max(12, node.radius + 6), fill: 'transparent' }),
778
787
  active ? h('circle', { r: node.radius + 5, fill: 'none', stroke: 'var(--dsw-alias-brand-primary)', strokeWidth: 1.4, opacity: 0.5 }) : null,
779
- h('circle', { r: node.radius, fill, opacity: isPrimary ? 0.9 : 0.5 }),
788
+ h('circle', {
789
+ r: node.radius, fill,
790
+ opacity: active ? 1 : isPrimary ? 0.9 : 0.5,
791
+ stroke: hovered ? 'var(--dsw-alias-brand-primary)' : 'none',
792
+ strokeWidth: hovered ? 1.2 : 0,
793
+ }),
780
794
  showLabel ? h('text', {
781
795
  y: node.radius + 12, textAnchor: 'middle',
782
796
  fill: active ? 'var(--dsw-alias-label-primary)' : 'var(--dsw-alias-label-secondary)',
783
- fontSize: isPrimary ? 10 : 8.5, opacity: active ? 0.95 : 0.68,
784
- }, SHORT(node.label, isPrimary ? 9 : 10)) : null,
797
+ fontSize: isPrimary ? 9.5 : 8.5, opacity: active ? 0.95 : 0.72,
798
+ paintOrder: 'stroke', stroke: 'var(--dsw-alias-bg-layer-1)', strokeWidth: 3,
799
+ strokeLinejoin: 'round',
800
+ }, SHORT(node.label, nameChars)) : null,
785
801
  h('title', null, node.title || node.label),
786
802
  );
787
803
  }))),
804
+ h('div', {
805
+ style: {
806
+ position: 'absolute', left: '10px', bottom: '8px', zIndex: 1,
807
+ maxWidth: 'calc(100% - 60px)', padding: '2px 6px', borderRadius: '6px',
808
+ background: 'var(--dsw-alias-bg-layer-1)',
809
+ color: 'var(--dsw-alias-label-secondary)', fontSize: '11px', lineHeight: '1.5',
810
+ pointerEvents: 'none',
811
+ },
812
+ }, props.legend || '圆点 记忆 · 小点 标签/主题 · 虚线 关联'),
788
813
  );
789
814
  };
790
815
 
@@ -852,7 +877,7 @@ window.__ModuleLoader__.load({
852
877
  const unresolved = rule.applicability === 'unresolved'
853
878
  || (rule.applicability === 'conditional' && (!Array.isArray(rule.conditions) || rule.conditions.length === 0));
854
879
  if (unresolved) return '适用范围待确认,暂不参与回答。';
855
- if (rule.state === 'pending') return '尚未参与回答,确认后才会试用。';
880
+ if (rule.state === 'pending') return '还没生效,采纳后才参与回答。';
856
881
  if (rule.state === 'trial') return '在条件匹配的任务中参与参考,到期后暂停。';
857
882
  if (rule.state === 'active') return '在适用任务中作为默认参考,当前明确要求优先。';
858
883
  return '';
@@ -865,7 +890,7 @@ window.__ModuleLoader__.load({
865
890
  if (conditions.length === 0) return '待确认';
866
891
  return conditions.map((item) => `${CONDITION_LABEL[item.key] || item.key} ${item.value}`).join('、');
867
892
  }
868
- return '所有项目';
893
+ return '本项目内';
869
894
  };
870
895
 
871
896
  const evidenceTextOf = (row) => {
@@ -1126,6 +1151,11 @@ window.__ModuleLoader__.load({
1126
1151
  const changedCount = activity && typeof activity === 'object' && Number.isInteger(activity.changed)
1127
1152
  ? activity.changed
1128
1153
  : (Number.isInteger(activity && activity.count) ? activity.count : null);
1154
+ const activityReason = activity && typeof activity === 'object' && typeof activity.reason === 'string'
1155
+ ? activity.reason
1156
+ : (latestReceipt && typeof latestReceipt.reason === 'string' ? latestReceipt.reason : '');
1157
+ const staleLearning = /没采用|作废|已更新|换到别的作品/.test(activityReason);
1158
+ const activityReasonText = staleLearning ? '整理过程中反馈或作品变了,这轮结果没采用。' : activityReason;
1129
1159
  let activityText = '';
1130
1160
  if (!evolution && memoryOn) {
1131
1161
  activityText = '读不到自进化状态';
@@ -1142,7 +1172,7 @@ window.__ModuleLoader__.load({
1142
1172
  } else if (activityCode === 'quota' || activityCode === 'exhausted') {
1143
1173
  activityText = '本轮整理额度已用完';
1144
1174
  } else if (activityCode === 'failed' || activityCode === 'error') {
1145
- activityText = '最近整理失败,可查看原因';
1175
+ activityText = staleLearning ? '这轮整理没采用' : '最近整理失败,可查看原因';
1146
1176
  } else if (activityCode === 'waiting' || activityCode === 'idle' || activityCode === 'empty') {
1147
1177
  activityText = '等待可学习的反馈';
1148
1178
  }
@@ -1155,16 +1185,12 @@ window.__ModuleLoader__.load({
1155
1185
  else if (rules.length === 0) activityText = countsUnknown ? '等待可学习的反馈' : '还没有整理出经验';
1156
1186
  else activityText = '暂无整理记录';
1157
1187
  }
1158
- const activityReason = activity && typeof activity === 'object' && typeof activity.reason === 'string'
1159
- ? activity.reason
1160
- : (latestReceipt && typeof latestReceipt.reason === 'string' ? latestReceipt.reason : '');
1161
-
1162
1188
  const filtered = React.useMemo(() => {
1163
1189
  const needle = query.trim().toLowerCase();
1164
1190
  if (!needle) return records;
1165
1191
  return records.filter((record) => {
1166
1192
  const tags = Array.isArray(record.tags) ? record.tags.join(' ') : '';
1167
- return `${record.title} ${record.summary} ${tags} ${KIND_LABEL[record.kind] || ''}`.toLowerCase().includes(needle);
1193
+ return `${record.title} ${record.summary} ${tags} ${record.topicKey || ''} ${KIND_LABEL[record.kind] || ''}`.toLowerCase().includes(needle);
1168
1194
  });
1169
1195
  }, [records, query]);
1170
1196
  const filteredRules = React.useMemo(() => {
@@ -1267,7 +1293,7 @@ window.__ModuleLoader__.load({
1267
1293
  h(Toggle, { ariaLabel: '自进化', on: Boolean(memoryOn && evolution && evolution.enabled === true), busy: busy || !evolution || !canToggle || !memoryOn, onClick: canToggle && memoryOn ? toggleEvolution : undefined }),
1268
1294
  ),
1269
1295
  h('div', { style: PANEL_MUTED }, '从反馈中整理经验,按需使用'), // ui-copy-ignore 用户指定文案
1270
- h('div', { style: PANEL_MUTED }, '实验功能'), // ui-copy-ignore 用户指定文案
1296
+ h('div', { style: PANEL_MUTED }, '实验功能,会增加 token 消耗'), // ui-copy-ignore 用户指定文案
1271
1297
  );
1272
1298
  const switchBar = h('div', { style: { display: 'flex', alignItems: 'stretch', gap: '10px', flexWrap: 'wrap', padding: '12px 18px', borderBottom: '1px solid var(--dsw-alias-border-l1)', flex: '0 0 auto' } },
1273
1299
  globalView ? h('div', { style: { ...PANEL_MUTED, padding: '2px 0' } }, '全局记忆只读') : memorySwitch,
@@ -1355,7 +1381,15 @@ window.__ModuleLoader__.load({
1355
1381
  const memoryGraphPane = h('div', {
1356
1382
  style: { ...PANEL_CARD, padding: 0, minHeight: 0, overflow: 'hidden', display: 'flex' },
1357
1383
  },
1358
- h(MemoryGraph, { records: filtered, selectedId: selectedMemory, onPick: onPickNode, emptyText: query ? '没有匹配的记忆' : '还没有记忆', ariaLabel: '记忆关系图' }));
1384
+ h(MemoryGraph, {
1385
+ records: filtered, selectedId: selectedMemory, onPick: onPickNode,
1386
+ onPickHub: (node) => {
1387
+ setQuery(node.kind === 'tag' ? node.label.replace(/^#/, '') : node.label);
1388
+ setMemoryView('list');
1389
+ },
1390
+ emptyText: query ? '没有匹配的记忆' : '还没有记忆', ariaLabel: '记忆关系图',
1391
+ legend: '圆点 记忆 · 小点 标签/主题 · 虚线 关联 · 点任意点筛选或查看',
1392
+ }));
1359
1393
  const memoryTab = h('div', { role: 'tabpanel', 'aria-label': '记忆', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
1360
1394
  h('div', { style: { display: 'flex', alignItems: 'center', gap: '8px', flex: '0 0 auto' } },
1361
1395
  h('input', {
@@ -1452,6 +1486,7 @@ window.__ModuleLoader__.load({
1452
1486
  records: filteredRules, graphOf: ruleGraphOf, primaryKind: 'rule',
1453
1487
  selectedId: selectedRule, onPick: onPickRuleNode,
1454
1488
  emptyText: evolutionEmptyText, ariaLabel: '经验关系图',
1489
+ legend: '圆点 经验 · 小点 条件 · 虚线 共用条件 · 点圆点查看',
1455
1490
  }));
1456
1491
  const evolutionTab = evolutionFailed ? evolutionFailed : h('div', { role: 'tabpanel', 'aria-label': '自进化', style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column', gap: '9px' } },
1457
1492
  h('div', { style: { ...PANEL_CARD, padding: '9px 11px', display: 'flex', alignItems: 'flex-start', gap: '8px', flex: '0 0 auto' } },
@@ -1459,9 +1494,9 @@ window.__ModuleLoader__.load({
1459
1494
  h('div', { style: { display: 'flex', flexDirection: 'column', gap: '2px', minWidth: 0 } },
1460
1495
  h('div', { style: { fontSize: '12px', lineHeight: '1.55' } }, activityText || (rules.length === 0 ? '还没有整理出经验' : '暂无整理记录')),
1461
1496
  countsUnknown ? h('div', { style: PANEL_MUTED }, '数量未知') : null,
1462
- activityReason
1497
+ activityReasonText
1463
1498
  ? (showReason
1464
- ? h('div', { style: { ...PANEL_MUTED, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, activityReason)
1499
+ ? h('div', { style: { ...PANEL_MUTED, whiteSpace: 'pre-wrap', wordBreak: 'break-word' } }, activityReasonText)
1465
1500
  : h(Action, { onClick: () => setShowReason(true) }, '查看原因'))
1466
1501
  : null,
1467
1502
  ),
@@ -1528,14 +1563,16 @@ window.__ModuleLoader__.load({
1528
1563
  const groups = React.useMemo(() => {
1529
1564
  const list = Array.isArray(refs) ? refs : EMPTY;
1530
1565
  return {
1531
- reference: list.filter((item) => item.exposure === 'reference'),
1566
+ found: list.filter((item) => item.exposure === 'reference' && item.kind === 'memory-search'),
1567
+ provided: list.filter((item) => item.exposure === 'reference' && item.kind === 'memory-read'),
1532
1568
  update: list.filter((item) => item.exposure === 'update'),
1533
1569
  };
1534
1570
  }, [refs]);
1535
1571
  if (!sessionId && refs.length === 0) return null;
1536
- if (groups.reference.length === 0 && groups.update.length === 0) return null;
1572
+ if (groups.found.length === 0 && groups.provided.length === 0 && groups.update.length === 0) return null;
1537
1573
  return h('div', { style: TAIL },
1538
- h(RefSection, { label: '本轮参考', items: groups.reference, sessionId, turnKey: turn }),
1574
+ h(RefSection, { label: '搜索命中', items: groups.found, sessionId, turnKey: turn }),
1575
+ h(RefSection, { label: '读过正文', items: groups.provided, sessionId, turnKey: turn }),
1539
1576
  h(RefSection, { label: '本轮更新', items: groups.update, sessionId, turnKey: turn }),
1540
1577
  );
1541
1578
  };
@@ -0,0 +1,10 @@
1
+ # 0.16.0
2
+
3
+ - 任务切换不再把上一条任务的规则带进新任务:带明确对象的新要求按新任务处理,只有「继续改」这类没有对象的短句才继承。
4
+ - 自进化关掉或换代后,已经准备好的实验正文和 `contextId` 立即失效;重新打开也不复活旧正文。
5
+ - 试用到期的学习结果不再自动注入,检索、读取、自动注入共用同一档可用性判断。
6
+ - 检索先过滤不可用条目再截断,不会被待确认的候选挤掉真正的命中;只有待确认候选时明确返回「适用条件还没确认」。
7
+ - 写入口径放宽:正文上限 4096 字节,整库 1 MB;摘要超长自动截断;不再因为正文提到像素、字号或写成指令句而拒绝保存。
8
+ - 只改标题的学习规则保持 `active` 和已有证据,不再降成试用。
9
+ - 容量腾挪不归档用户固定过的规则;实验规则与待确认候选的额度不再重复计数。
10
+ - 面板:作用范围改成「本项目内」,候选状态与引用分组的文案对齐实际行为,自进化开关标明会增加消耗。