@yolk_vat-y/dsh-project-memory 0.5.4 → 0.5.6

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.
@@ -75,7 +75,8 @@ function TaskPanelView({ ctx }: { ctx: any }) {
75
75
  const [syncing, setSyncing] = useState(false)
76
76
  const [syncedAt, setSyncedAt] = useState(0)
77
77
  const [syncError, setSyncError] = useState<string | null>(null)
78
- const [showHints, setShowHints] = useState(true)
78
+ // 提示信息开关落在 UI store(持久化):关掉后面板内所有悬停 title 气泡都不再弹出。
79
+ const showHints = ui.showHints
79
80
  const [view, setView] = useState<PanelView>('task')
80
81
  const cycleView = () => {
81
82
  const i = VIEW_CYCLE.indexOf(view)
@@ -288,6 +289,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
288
289
  <MiniBar
289
290
  label={label}
290
291
  hint={t('minibar.click-expand')}
292
+ showHints={showHints}
291
293
  position={ui.panelPosition}
292
294
  theme={style}
293
295
  onMove={uiActions.setPanelPosition}
@@ -308,7 +310,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
308
310
  style={{ left: ui.panelPosition.x, top: ui.panelPosition.y }}
309
311
  data-theme={style === 'native' ? undefined : style}
310
312
  >
311
- <div className={css.dragHandle} onMouseDown={handleDragStart} title={t('task.drag')}>
313
+ <div className={css.dragHandle} onMouseDown={handleDragStart} title={showHints ? t('task.drag') : undefined}>
312
314
  <div className={css.handleGrip} />
313
315
  </div>
314
316
  <header className={css.header}>
@@ -317,7 +319,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
317
319
  type="button"
318
320
  className={css.headerIconBtn}
319
321
  onClick={cycleTheme}
320
- title={styleLabel}
322
+ title={showHints ? styleLabel : undefined}
321
323
  aria-label={styleLabel}
322
324
  >
323
325
  <IconFolderOpenOutline16 className={css.headerIcon} />
@@ -330,7 +332,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
330
332
  size="sm"
331
333
  onClick={cycleView}
332
334
  aria-label={t('view.cycle')}
333
- title={`${t('view.cycle')}(${view} → ${VIEW_CYCLE[(VIEW_CYCLE.indexOf(view) + 1) % VIEW_CYCLE.length]})`}
335
+ title={showHints ? `${t('view.cycle')}(${view} → ${VIEW_CYCLE[(VIEW_CYCLE.indexOf(view) + 1) % VIEW_CYCLE.length]})` : undefined}
334
336
  >
335
337
  {/* lucide notebook-tabs(视图循环图标) */}
336
338
  <svg
@@ -365,7 +367,9 @@ function TaskPanelView({ ctx }: { ctx: any }) {
365
367
  <Button
366
368
  variant="ghost"
367
369
  size="sm"
368
- onClick={() => setShowHints(!showHints)}
370
+ className={showHints ? undefined : css.hintsOff}
371
+ onClick={() => uiActions.toggleHints()}
372
+ aria-pressed={!showHints}
369
373
  aria-label={showHints ? t('panel.hints-off') : t('panel.hints-on')}
370
374
  title={showHints ? t('panel.hints-off') : t('panel.hints-on')}
371
375
  >
@@ -376,7 +380,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
376
380
  size="sm"
377
381
  onClick={() => uiActions.minimize()}
378
382
  aria-label={t('panel.minimize')}
379
- title={t('panel.minimize')}
383
+ title={showHints ? t('panel.minimize') : undefined}
380
384
  >
381
385
  <IconChevronDownOutline14 />
382
386
  </Button>
@@ -385,7 +389,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
385
389
  size="sm"
386
390
  onClick={() => uiActions.close()}
387
391
  aria-label={t('panel.close')}
388
- title={t('panel.close')}
392
+ title={showHints ? t('panel.close') : undefined}
389
393
  >
390
394
  <IconCloseOutline16 />
391
395
  </Button>
@@ -396,7 +400,7 @@ function TaskPanelView({ ctx }: { ctx: any }) {
396
400
  {syncError && <div className={css.notice}>{t('panel.sync-failed')}: {syncError}</div>}
397
401
 
398
402
  {view !== 'task' ? (
399
- <MemoryView ctx={ctx} sessionId={sessionId} scope={view as InsightScope} boundTaskId={data.boundTaskId} t={t} />
403
+ <MemoryView ctx={ctx} sessionId={sessionId} scope={view as InsightScope} boundTaskId={data.boundTaskId} showHints={showHints} t={t} />
400
404
  ) : activeTasks.length === 0 ? (
401
405
  <div className={css.emptyState}>
402
406
  <p className={css.emptyTitle}>{t('panel.empty')}</p>
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Task UI Store — 仅管本地 UI 状态(closed、minimized、position、expandedIds、theme)
2
+ * Task UI Store — 仅管本地 UI 状态(closed、minimized、position、expandedIds、theme、showHints)
3
3
  * - 持久化到 localStorage(key: dsh-pm-task-panel-ui)
4
4
  * - 不跨标签页同步(每个标签页独立)
5
5
  * - 启动/刷新强制 closed: true,面板默认不显示
@@ -14,6 +14,8 @@ interface TaskUIState {
14
14
  minimized: boolean
15
15
  closed: boolean
16
16
  theme: string
17
+ /** 是否显示悬停提示信息(title 气泡);false 时面板内所有功能提示都不弹出。 */
18
+ showHints: boolean
17
19
  }
18
20
 
19
21
  function defaultPosition(): { x: number; y: number } {
@@ -40,6 +42,7 @@ function getDefaultUIState(): TaskUIState {
40
42
  minimized: parsed.minimized !== false,
41
43
  closed: true,
42
44
  theme: typeof parsed.theme === 'string' ? parsed.theme : 'native',
45
+ showHints: parsed.showHints !== false,
43
46
  }
44
47
  }
45
48
  } catch { /* corrupt state — fall through to defaults */ }
@@ -50,6 +53,7 @@ function getDefaultUIState(): TaskUIState {
50
53
  minimized: true,
51
54
  closed: true,
52
55
  theme: 'native',
56
+ showHints: true,
53
57
  }
54
58
  }
55
59
 
@@ -103,6 +107,12 @@ export const taskUIStore = {
103
107
  setTheme(theme: string): void {
104
108
  setUIState({ ...uiState, theme })
105
109
  },
110
+ toggleHints(): void {
111
+ setUIState({ ...uiState, showHints: !uiState.showHints })
112
+ },
113
+ setShowHints(showHints: boolean): void {
114
+ setUIState({ ...uiState, showHints })
115
+ },
106
116
  reset(): void {
107
117
  setUIState(getDefaultUIState())
108
118
  },
package/src/index.js CHANGED
@@ -49,7 +49,7 @@ export const Config = Schema.object({
49
49
  // 任务成为会话绑定(select_task / /task switch)时,把任务步骤推成宿主 todo/write 快照
50
50
  syncHostOnAdopt: Schema.boolean().default(true),
51
51
  }).default({}),
52
- // v0.5 单一 insight 实体:分层去重/强化/提升/容量/归档(详见 PLAN-v0.5.0.md)
52
+ // v0.5 单一 insight 实体:分层去重/强化/提升/容量/归档
53
53
  insight: Schema.object({
54
54
  dedupOverlap: Schema.number().default(0.7),
55
55
  reinforceBand: Schema.number().default(0.65),
@@ -72,7 +72,24 @@ export const Config = Schema.object({
72
72
  enabled: Schema.boolean().default(true),
73
73
  entryOn: Schema.boolean().default(true),
74
74
  maxTokens: Schema.number().default(400),
75
- relevanceMin: Schema.number().default(0.25),
75
+ entryMaxInsights: Schema.number().default(6),
76
+ // 提示通道的相对阈值(层内最高分的比例)——出厂判据
77
+ signalMinRatio: Schema.number().default(0.5),
78
+ // resident 任务卡最多显示几个「编辑中」文件
79
+ editedMax: Schema.number().default(3),
80
+ // 模型自己维护清单且之后没有新人类消息时,不把任务卡回声给模型
81
+ skipEchoSelfTodo: Schema.boolean().default(true),
82
+ // ⚠️ 旧兼容闸门,**刻意不给默认值**:cfgEngine 以「relevanceMin 是否为 number」选分支,
83
+ // 一旦有默认值就会永远走旧的绝对 overlap 判据——而它对长消息实测只有 0.014~0.057,
84
+ // 必然过不了,相对阈值 signalMinRatio 就变成死代码。只有用户显式配置才保留旧行为。
85
+ relevanceMin: Schema.number(),
86
+ // 预算审计日志(stderr)**默认 off**:终端是用户可见面,而"预算挤掉低优先级条目"是
87
+ // 正常降级、不是故障——默认打印会让一次 dsh web 启动刷出多行,用户的第一反应是卸载插件。
88
+ // off(默认,永不打印)/ once(每个会话最多一行,首次出现丢弃时)/ all(丢弃组合每变化一次一行,作者排查)
89
+ budgetLog: Schema.union(['off', 'once', 'all']).default('off'),
90
+ // 同一条 insight 在本会话里重复注入的冷却(pre-step 步数)。0(默认)= 正文没变就不再注入:
91
+ // 注入消息留在会话历史里(宿主只追加不压缩),整块重发只是重复占位。>0 用于外部裁剪历史的场景。
92
+ reinjectItemsAfter: Schema.number().default(0),
76
93
  }).default({}),
77
94
  })
78
95
 
@@ -98,10 +98,32 @@ export function normalizeInsight(raw, extra = {}) {
98
98
  if (Array.isArray(raw[f]) && raw[f].length) ins[f] = [...new Set(raw[f].map((x) => String(x)))]
99
99
  }
100
100
  if (raw.trigger && typeof raw.trigger === 'object') {
101
- const tr = { keywords: Array.isArray(raw.trigger.keywords) ? raw.trigger.keywords.map(String) : [] }
102
- if (Array.isArray(raw.trigger.symbols)) tr.symbols = raw.trigger.symbols.map(String)
103
- if (Array.isArray(raw.trigger.scope)) tr.scope = raw.trigger.scope.map(String)
104
- if (tr.keywords.length) ins.trigger = tr
101
+ // 所有 kind 通用。准入化 schema:when(唯一触发面)/ guard(只收窄)/ prevents(准入条件);
102
+ // 旧字段 keywords / symbols / actions / paths / scope 继续接受(readiness 会内存内迁移)。
103
+ const tr = {}
104
+ for (const f of ['keywords', 'symbols', 'actions', 'paths', 'scope']) {
105
+ if (Array.isArray(raw.trigger[f]) && raw.trigger[f].length) tr[f] = raw.trigger[f].map(String)
106
+ }
107
+ const when = raw.trigger.when
108
+ if (when && typeof when === 'object') {
109
+ const w = {}
110
+ for (const f of ['ops', 'writes', 'intents']) {
111
+ if (Array.isArray(when[f]) && when[f].length) w[f] = when[f].map(String)
112
+ }
113
+ if (Object.keys(w).length) tr.when = w
114
+ }
115
+ const guard = raw.trigger.guard
116
+ if (guard && typeof guard === 'object') {
117
+ const g = {}
118
+ for (const f of ['paths', 'not_paths', 'hosts', 'tags']) {
119
+ if (Array.isArray(guard[f]) && guard[f].length) g[f] = guard[f].map(String)
120
+ }
121
+ if (Object.keys(g).length) tr.guard = g
122
+ }
123
+ if (typeof raw.trigger.prevents === 'string' && raw.trigger.prevents.trim()) {
124
+ tr.prevents = raw.trigger.prevents.trim()
125
+ }
126
+ if (Object.keys(tr).length) ins.trigger = tr
105
127
  }
106
128
  return ins
107
129
  }
package/src/ops.js ADDED
@@ -0,0 +1,210 @@
1
+ // 动作平面(PLAN S1):把"这一步在做什么"从工具调用解析成有限的 op + 写目标 + 主机环境。
2
+ //
3
+ // 为什么不能继续用文本匹配:语料(路径、命令、被读文件正文)里的字符串可以冒充动作。
4
+ // 实测病征是 `keyword:rm` 命中 `dcterms`、`keyword:pptx` 命中一个文件扩展名。
5
+ // op 与 target 是低维、可枚举、可审计的:不随文本长度变化,也不会被正文污染。
6
+ //
7
+ // 分工:ops.js 只负责"把调用读成什么动作",不负责决定注入什么(那是 readiness/auto-inject)。
8
+ // 刻意不 import readiness:readiness 要 import 本模块的 op 闭集,反向依赖会造成循环。
9
+
10
+ /** 带扩展名的路径 token(与 readiness.extractPaths 同规则;这里就地实现以免循环依赖)。 */
11
+ const PATH_TOKEN = /(?:[A-Za-z0-9_.@-]+\/)*[A-Za-z0-9_.@-]+\.[A-Za-z][A-Za-z0-9]{0,7}\b/g
12
+
13
+ function extractPaths(text) {
14
+ const s = String(text || '')
15
+ if (!s) return []
16
+ return [...new Set([...s.matchAll(PATH_TOKEN)].map((m) => m[0]))]
17
+ }
18
+
19
+ /** op 闭集。新增一个 op 必须同时想清楚:它是动作,不是话题。 */
20
+ export const OP_IDS = [
21
+ 'file-write',
22
+ 'file-delete',
23
+ 'shell-run',
24
+ 'git-commit',
25
+ 'git-push',
26
+ 'release',
27
+ 'npm-publish',
28
+ 'deploy',
29
+ 'migrate',
30
+ 'go-public',
31
+ 'render-doc',
32
+ 'index-doc',
33
+ 'read-image',
34
+ 'fetch-web',
35
+ 'run-bench',
36
+ 'query-memory',
37
+ ]
38
+
39
+ /**
40
+ * 兜底 op:它们描述的是"发生了某类操作",信息量低、假阳性高。
41
+ * 允许写进 `when.ops`,但自检会 warn——兜底 op 只该命中兜底级记忆。
42
+ */
43
+ export const FALLBACK_OPS = new Set(['shell-run', 'file-write', 'query-memory'])
44
+
45
+ /**
46
+ * 弱 op:日常动作。它本身不构成边界(每天都在提交),所以当条目**已经有更精确的 writes**
47
+ * 时,迁移会把弱 op 丢掉——留着它只会把"改这个文件时"扩大成"任何一次提交时"。
48
+ * 实测:`git-commit` 让一条"别删内部文档"的教训在发版场景里被注入。
49
+ */
50
+ export const WEAK_OPS = new Set([...FALLBACK_OPS, 'git-commit', 'git-push'])
51
+
52
+ /** 旧 action id → op。旧值里有一批从来没进过 ACTION_LEXICON 的死值,这里给它们一个归宿。 */
53
+ export const LEGACY_ACTION_ALIAS = {
54
+ 'git-add': 'git-commit',
55
+ 'npm-pack': 'npm-publish',
56
+ 'release': 'release',
57
+ 'generate-pptx': 'render-doc',
58
+ 'render': 'render-doc',
59
+ 'read-image': 'read-image',
60
+ 'web-fetch': 'fetch-web',
61
+ 'benchmark': 'run-bench',
62
+ 'cleanup': 'file-delete',
63
+ 'delete': 'file-delete',
64
+ 'recover': null, // 没有对应动作:恢复是意图,不是工具动作
65
+ 'research': null,
66
+ 'survey': null,
67
+ 'interview-prep': null,
68
+ 'write-resume': null,
69
+ 'report-metrics': null,
70
+ 'write-docs': null,
71
+ }
72
+
73
+ const WRITE_TOOLS = new Set(['write', 'edit', 'multi_edit', 'apply_patch', 'str_replace', 'create_file', 'notebook_edit'])
74
+ const READ_IMAGE_TOOLS = new Set(['read_image'])
75
+ const INDEX_TOOLS = new Set(['index_doc', 'index_repo', 'watch_repo'])
76
+ const FETCH_TOOLS = new Set(['web_fetch', 'web_search'])
77
+ const SHELL_TOOLS = new Set(['bash', 'shell', 'pwsh', 'powershell', 'run', 'exec', 'job_run'])
78
+ const QUERY_TOOLS = new Set(['query_memory', 'recall'])
79
+
80
+ /** bash 命令 → op。顺序有意义:先具体后兜底。 */
81
+ const SHELL_OP_RULES = [
82
+ [/\bnpm\s+(publish|pack)\b/, 'npm-publish'],
83
+ [/\bnpm\s+version\b/, 'release'],
84
+ [/\bgit\s+tag\b/, 'release'],
85
+ [/\bgit\s+push\b/, 'git-push'],
86
+ [/\bgit\s+(commit|add)\b/, 'git-commit'],
87
+ [/\bgit\s+(checkout|restore|reset)\b/, 'file-write'],
88
+ // `\brm\b` 而不是 `rm\s+-[rf]`:后者被 `\b` 卡在 'rm -rf' 的 r 后面(f 还是词字符),永远匹配不上。
89
+ [/\b(rm|git\s+rm|unlink|rmdir|shred)\b/, 'file-delete'],
90
+ [/\b(kubectl|docker\s+push|systemctl\s+restart|pm2\s+(deploy|restart)|vercel|netlify\s+deploy)\b/, 'deploy'],
91
+ [/\b(migrate|alembic|prisma\s+migrate|db:migrate)\b/, 'migrate'],
92
+ // 注意:只认"渲染器",不认 .pptx 扩展名——否则给 pptx 改个时间戳也会命中"做 PPT"
93
+ [/(pptxgenjs|soffice|libreoffice|render\.sh|marp|pandoc|slidev)/, 'render-doc'],
94
+ [/(bench\.mjs|\bbench(mark)?\b)/, 'run-bench'],
95
+ [/\b(curl|wget|web_fetch)\b/, 'fetch-web'],
96
+ ]
97
+
98
+ /** 会改动文件系统的 shell 动作前缀:只有这些命令的路径才算"写目标"。 */
99
+ const SHELL_WRITE_RE = /\b(rm|rmdir|unlink|shred|mv|cp|touch|tee|truncate|mkdir)\b|>>?\s*\S|\bsed\s+-i\b/
100
+
101
+ const WSL_HINT_RE = /\/mnt\/[a-z]\//i
102
+
103
+ function safeJson(text) {
104
+ if (typeof text !== 'string' || !text.trim()) return null
105
+ try {
106
+ const v = JSON.parse(text)
107
+ return v && typeof v === 'object' ? v : null
108
+ } catch {
109
+ return null
110
+ }
111
+ }
112
+
113
+ function argsFields(argsText) {
114
+ const obj = safeJson(argsText)
115
+ if (!obj) return {}
116
+ const pick = (...keys) => keys
117
+ .map((k) => obj[k])
118
+ .filter((v) => typeof v === 'string' && v)
119
+ .join(' ')
120
+ return {
121
+ command: pick('command', 'cmd', 'script', 'code'),
122
+ path: pick('file_path', 'path', 'file', 'target', 'notebook_path'),
123
+ }
124
+ }
125
+
126
+ /** bash 命令 → { ops, targets, hosts }。 */
127
+ export function classifyShellCommand(command) {
128
+ const cmd = String(command || '')
129
+ const ops = []
130
+ if (cmd) {
131
+ for (const [re, op] of SHELL_OP_RULES) {
132
+ if (re.test(cmd) && !ops.includes(op)) ops.push(op)
133
+ }
134
+ }
135
+ if (!ops.length) ops.push('shell-run')
136
+ const targets = SHELL_WRITE_RE.test(cmd) ? extractPaths(cmd) : []
137
+ const hosts = WSL_HINT_RE.test(cmd) || /\b(powershell|pwsh|cmd)\.exe\b/i.test(cmd) ? ['wsl'] : []
138
+ return { ops, targets, hosts }
139
+ }
140
+
141
+ /**
142
+ * 一次工具调用 → { ops, targets, hosts }。
143
+ * @param {string} name 工具名
144
+ * @param {string} argsText 工具参数(JSON 字符串或裸文本)
145
+ */
146
+ export function classifyToolCall(name, argsText) {
147
+ const tool = String(name || '').toLowerCase()
148
+ const { command, path } = argsFields(argsText)
149
+ if (SHELL_TOOLS.has(tool)) return classifyShellCommand(command || String(argsText || ''))
150
+ if (READ_IMAGE_TOOLS.has(tool)) return { ops: ['read-image'], targets: [], hosts: [] }
151
+ if (INDEX_TOOLS.has(tool)) return { ops: ['index-doc'], targets: [], hosts: [] }
152
+ if (FETCH_TOOLS.has(tool)) return { ops: ['fetch-web'], targets: [], hosts: [] }
153
+ if (QUERY_TOOLS.has(tool)) return { ops: ['query-memory'], targets: [], hosts: [] }
154
+ if (WRITE_TOOLS.has(tool)) {
155
+ const targets = path ? [path, ...extractPaths(path)] : []
156
+ const hosts = targets.some((t) => WSL_HINT_RE.test(t)) ? ['wsl'] : []
157
+ return { ops: ['file-write'], targets: [...new Set(targets)], hosts }
158
+ }
159
+ return { ops: [], targets: [], hosts: [] }
160
+ }
161
+
162
+ /**
163
+ * 把一组已观察到的工具调用合成这一个线程的动态面。
164
+ * @param {{name?: string, arguments?: string}[]} calls
165
+ */
166
+ export function activityFromCalls(calls) {
167
+ const ops = new Set()
168
+ const targets = new Set()
169
+ const hosts = new Set()
170
+ for (const call of calls || []) {
171
+ const r = classifyToolCall(call?.name, call?.arguments)
172
+ for (const op of r.ops) ops.add(op)
173
+ for (const t of r.targets) targets.add(t)
174
+ for (const h of r.hosts) hosts.add(h)
175
+ }
176
+ return { ops: [...ops], targets: [...targets], hosts: [...hosts] }
177
+ }
178
+
179
+ /**
180
+ * 没拿到结构化调用时的兜底:直接对一段文本跑 shell 规则。
181
+ * 用于 `actionText` 形式的就绪上下文(测试与老宿主),以及人类消息里的动作词。
182
+ */
183
+ export function activityFromText(text) {
184
+ const s = String(text || '')
185
+ if (!s) return { ops: [], targets: [], hosts: [] }
186
+ const lines = s.split('\n').filter(Boolean)
187
+ const ops = new Set()
188
+ const targets = new Set()
189
+ const hosts = new Set()
190
+ for (const line of lines) {
191
+ // 先按"工具调用"解析(`edit {"file_path":"..."}`):结构化参数比正则猜命令可靠得多。
192
+ const call = /^\s*([A-Za-z_][\w-]*)\s+(\{[\s\S]*\})\s*$/.exec(line)
193
+ const r = call ? classifyToolCall(call[1], call[2]) : classifyShellCommand(line)
194
+ // 纯文本行里没有 shell 动作时不要退化成 shell-run(那是噪音源)
195
+ if (r.ops.length === 1 && r.ops[0] === 'shell-run' && !/\b\w+\s+-/.test(line)) continue
196
+ for (const op of r.ops) ops.add(op)
197
+ for (const t of r.targets) targets.add(t)
198
+ for (const h of r.hosts) hosts.add(h)
199
+ }
200
+ return { ops: [...ops], targets: [...targets], hosts: [...hosts] }
201
+ }
202
+
203
+ /** 旧 action id(含死值)→ op;无法映射返回 null。 */
204
+ export function opForLegacyAction(action) {
205
+ const a = String(action || '')
206
+ if (!a) return null
207
+ if (OP_IDS.includes(a)) return a
208
+ if (a in LEGACY_ACTION_ALIAS) return LEGACY_ACTION_ALIAS[a]
209
+ return null
210
+ }
@@ -25,6 +25,9 @@ const PDFJS_OPTIONS = {
25
25
  isEvalSupported: false,
26
26
  useWorkerFetch: false,
27
27
  useWorker: false,
28
+ // 0 = ERRORS。损坏/线性化缺失的 PDF 会让 pdf.js 打 "Warning: Indexing all PDF objects"
29
+ // 之类的告警——那是它自己的恢复路径,对使用者没有可操作性,静音。
30
+ verbosity: 0,
28
31
  }
29
32
 
30
33
  function buildMarkdown(pages) {