@a9i5k4/dsh-auto-memory 0.1.18 → 0.1.20

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/cordis.patch.yml CHANGED
@@ -5,5 +5,5 @@
5
5
  # the `dsh.client` declaration in package.json makes the browser half
6
6
  # (exports "./client", served at /plugins/<id>/client.js) load in the web GUI.
7
7
  - insert:
8
- - id: auto-memory-dev
9
- name: '@deepseek-ai/dsh-auto-memory'
8
+ - id: auto-memory
9
+ name: '@a9i5k4/dsh-auto-memory'
package/lib/client.js CHANGED
@@ -171,6 +171,10 @@ window.__ModuleLoader__.load({
171
171
  fInject: '注入记忆上下文', fInjectHint: '每次组装提示词时自动注入 <memory_system> 块。',
172
172
  fBudget: '注入预算(字符)', fBudgetHint: '记忆块总预算,超出部分截断。',
173
173
  fDays: '注入最近日志天数', fDaysHint: '会话开始时注入最近 N 天的工作日志尾部。',
174
+ fAway: '暂离阈值(分钟)', fAwayHint: '距上次活动超过该值视为暂离,回归时自动弹出记忆窗口。默认 60。',
175
+ fAutoSum: '自动总结时间点(HH:MM,逗号分隔)', fAutoSumHint: '到点自动生成本时段总结并弹窗展示,如 12:00,18:00,22:00。空=关闭。',
176
+ awayTitle: '欢迎回来', awayMsg: '你离开的这段时间,我已经帮你把日志整理好了。记忆窗口已打开,可以看看这段时间的状况。',
177
+ sumTitle: '时段总结',
174
178
  fReflect: '每日反思', fReflectHint: '昨天有工作日志时,会话首轮主动呈现昨日反思。',
175
179
  fStyle: '反思风格', fStyleHint: '生活化 / 专业性 / 由内容决定。',
176
180
  fLocale: '界面语言', fLocaleHint: '默认跟随 DSH 系统语言;也可手动指定中文 / English。', followSystem: '跟随系统语言',
@@ -261,6 +265,10 @@ window.__ModuleLoader__.load({
261
265
  fInject: 'Inject memory context', fInjectHint: 'Auto-inject <memory_system> block into every prompt.',
262
266
  fBudget: 'Injection budget (chars)', fBudgetHint: 'Total budget for the memory block; excess is truncated.',
263
267
  fDays: 'Recent days injected', fDaysHint: 'Inject tails of the last N days of work logs at session start.',
268
+ fAway: 'Away threshold (minutes)', fAwayHint: 'Marked away when inactive longer than this; the memory panel auto-opens on return. Default 60.',
269
+ fAutoSum: 'Auto summary times (HH:MM, comma-separated)', fAutoSumHint: 'Auto-generate and show a period summary at these times, e.g. 12:00,18:00,22:00. Empty = off.',
270
+ awayTitle: 'Welcome back', awayMsg: 'While you were away, I tidied up your logs. The memory panel is open — take a look at what happened.',
271
+ sumTitle: 'Period summary',
264
272
  fReflect: 'Daily reflection', fReflectHint: 'When yesterday has logs, the agent presents the reflection at session start.',
265
273
  fStyle: 'Reflection style', fStyleHint: 'Life-style / Professional / Auto.',
266
274
  fLocale: 'UI language', fLocaleHint: 'Follows the DSH system language by default; you can also pin Chinese / English.', followSystem: 'Follow system language',
@@ -304,8 +312,11 @@ window.__ModuleLoader__.load({
304
312
  function t(key) { return (I18N[locale] && I18N[locale][key]) || I18N.zh[key] || key }
305
313
  function setLocale(l) { if (l !== 'zh' && l !== 'en') return; if (l === locale) return; locale = l; localeListeners.forEach(function (fn) { try { fn(l) } catch (e) {} }) }
306
314
  function onLocale(fn) { localeListeners.add(fn); return function () { localeListeners.delete(fn) } }
307
- // 暂离检测:距离上次关闭面板超过 1 小时视为暂离回来
315
+ // 暂离检测:优先用 host 定时检测的 away 状态(阈值可配 awayMinutes),回退本地 lastActive 旧逻辑
316
+ var hostAway = false
317
+ var hostAwayReady = false
308
318
  function isAway() {
319
+ if (hostAwayReady) return hostAway
309
320
  var lastSeen = 0
310
321
  try { lastSeen = Number(localStorage.getItem('dsh-auto-memory.lastActive') || 0) } catch (e) {}
311
322
  return lastSeen > 0 && (Date.now() - lastSeen) > 3600000
@@ -313,6 +324,21 @@ window.__ModuleLoader__.load({
313
324
 
314
325
  // ───────────────────────── 更新弹窗 / 首次指导 ─────────────────────────
315
326
  var CHANGELOG = {
327
+ '0.1.19': { zh: [
328
+ '稳定版:整合 0.1.16~0.1.19 全部修复与优化',
329
+ '核心:上下文缓存策略重写——记忆注入迁至运行时上下文快照,系统提示词保持稳定,DeepSeek 前缀缓存全程命中,不再白白消耗 token(命中率恢复 95%+)',
330
+ '新增:时间检测——暂离阈值可配置(回归自动弹出记忆窗口并欢迎),自动总结时间点(到点自动生成时段总结并弹窗展示),自动沉淀定时兜底恢复',
331
+ '新增:动态通知中心——发布者重要提醒(重大 bug/升级建议)自动推送,无需等待发版',
332
+ '修复:自动沉淀在重启恢复会话后失效(agent 引用定时恢复,双保险)',
333
+ '提示:pnpm v11 默认限制安装发布不足 1 天的版本,想立即获取新版请在 pnpm-workspace.yaml 设 minimumReleaseAge: 0 或使用显式版本号',
334
+ ], en: [
335
+ 'Stable release: consolidates 0.1.16~0.1.19 fixes and polish',
336
+ 'Core: context-cache strategy rewrite — memory injection moved to runtime-context snapshot, system prompt stays byte-stable, DeepSeek prefix cache hits throughout, no more wasted tokens (hit rate back to 95%+)',
337
+ 'New: time detection — configurable away threshold (auto-open memory panel with welcome on return), auto summary times (auto-generate period summary popup), timed fallback recovery for auto-consolidation',
338
+ 'New: dynamic notice center — publisher alerts (major bugs / upgrade advice) pushed automatically, no need to wait for a release',
339
+ 'Fix: auto-consolidation failing after restart-resumed sessions (agent reference restored by timer, double insurance)',
340
+ 'Note: pnpm v11 blocks packages published <1 day ago by default; set minimumReleaseAge: 0 in pnpm-workspace.yaml or use an explicit version for the latest immediately',
341
+ ] },
316
342
  '0.1.18': { zh: [
317
343
  '稳定版:整合 0.1.13~0.1.17 全部修复与优化',
318
344
  '核心:上下文缓存策略重写——记忆注入迁至运行时上下文快照,系统提示词保持稳定,DeepSeek 前缀缓存全程命中,不再白白消耗 token(命中率恢复 95%+)',
@@ -1349,6 +1375,26 @@ window.__ModuleLoader__.load({
1349
1375
  closeDialog()
1350
1376
  } }, t('gotIt')))))
1351
1377
  }
1378
+ if (dialogState.kind === 'welcomeBack') {
1379
+ return h('div', { style: overlay },
1380
+ h('div', { style: box },
1381
+ h('div', { style: head }, t('awayTitle')),
1382
+ h('div', { style: { fontSize: 'calc(12px * var(--dam-scale))', lineHeight: 1.6, opacity: .92, marginTop: '4px' } }, t('awayMsg')),
1383
+ h('div', { style: { display: 'flex', justifyContent: 'flex-end', marginTop: '6px' } },
1384
+ h('button', { 'data-dam-btn': '', style: close, onClick: closeDialog }, t('gotIt')))))
1385
+ }
1386
+ if (dialogState.kind === 'summary') {
1387
+ var sum = dialogState.summary || {}
1388
+ return h('div', { style: overlay },
1389
+ h('div', { style: box },
1390
+ h('div', { style: head }, t('sumTitle') + ' ' + (sum.time || '')),
1391
+ h('div', { style: { fontSize: 'calc(12px * var(--dam-scale))', lineHeight: 1.6, opacity: .92, whiteSpace: 'pre-wrap', marginTop: '4px' } }, sum.summary || ''),
1392
+ (sum.works || []).slice(0, 6).map(function (w) {
1393
+ return h('div', { style: item }, h('span', { style: dot }), w.title || '')
1394
+ }),
1395
+ h('div', { style: { display: 'flex', justifyContent: 'flex-end', marginTop: '6px' } },
1396
+ h('button', { 'data-dam-btn': '', style: close, onClick: closeDialog }, t('gotIt')))))
1397
+ }
1352
1398
  var versions = dialogState.versions || []
1353
1399
  var lastV = versions.length ? versions[versions.length - 1].version : ''
1354
1400
  return h('div', { style: overlay },
@@ -1495,6 +1541,8 @@ window.__ModuleLoader__.load({
1495
1541
  field(t('fInject'), h('input', { type: 'checkbox', checked: !!cfg.injectEnabled, onChange: function (e) { set('injectEnabled', e.target.checked) } }), t('fInjectHint')),
1496
1542
  field(t('fBudget'), h('input', { 'data-dam-input': '', type: 'number', min: 400, value: cfg.injectBudgetChars, onChange: function (e) { set('injectBudgetChars', Number(e.target.value) || 2400) } }), t('fBudgetHint')),
1497
1543
  field(t('fDays'), h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 14, value: cfg.recentDaysInjected, onChange: function (e) { set('recentDaysInjected', Number(e.target.value) || 3) } }), t('fDaysHint')),
1544
+ field(t('fAway'), h('input', { 'data-dam-input': '', type: 'number', min: 1, value: cfg.awayMinutes || 60, onChange: function (e) { set('awayMinutes', Number(e.target.value) || 60) } }), t('fAwayHint')),
1545
+ field(t('fAutoSum'), h('input', { 'data-dam-input': '', value: (cfg.autoSummaryTimes || []).join(','), onChange: function (e) { set('autoSummaryTimes', String(e.target.value || '').split(',').map(function (s) { return s.trim() }).filter(Boolean)) } }), t('fAutoSumHint')),
1498
1546
  field(t('fDayBoundary'), h('input', { 'data-dam-input': '', type: 'number', min: 0, max: 1439, value: (cfg.dayBoundaryMinutes === undefined ? 450 : cfg.dayBoundaryMinutes), onChange: function (e) { set('dayBoundaryMinutes', parseInt(e.target.value || '0', 10)) } }), t('fDayBoundaryHint')),
1499
1547
  field(t('fReflect'), h('input', { type: 'checkbox', checked: !!cfg.reflectEnabled, onChange: function (e) { set('reflectEnabled', e.target.checked) } }), t('fReflectHint')),
1500
1548
  field(t('fStyle'), h('select', { 'data-dam-select': '', value: cfg.reflectStyle, onChange: function (e) { set('reflectStyle', e.target.value) } },
@@ -1556,9 +1604,21 @@ window.__ModuleLoader__.load({
1556
1604
  })
1557
1605
  // 语言切换时通知所有订阅者重渲染
1558
1606
  onLocale(function () { emit() })
1559
- // 暂离回来自动弹开记忆窗口:距离上次关闭面板超过 1 小时,页面加载或切回标签页时自动打开(欢迎语在 GreetingCard 展示)
1607
+ // 暂离回来自动弹开记忆窗口:host 判定暂离(阈值可配 awayMinutes),页面加载/切回标签页/轮询发现回归时自动打开
1608
+ // 回归检测:host away 状态从 true → false(暂离结束)时打开并欢迎
1609
+ var prevAwayState = null
1560
1610
  function autoOpenOnReturn() {
1561
1611
  try {
1612
+ if (hostAwayReady && prevAwayState === true && hostAway === false) {
1613
+ // 暂离回归:打开窗口 + 欢迎弹窗(若有待展示总结一并显示)
1614
+ if (!controller.isOpen()) controller.open()
1615
+ var pend = lastPendingSummary
1616
+ if (pend) { openDialog({ kind: 'summary', summary: pend }); lastPendingSummary = null }
1617
+ else if (!document.hidden) openDialog({ kind: 'welcomeBack' })
1618
+ prevAwayState = hostAway
1619
+ return
1620
+ }
1621
+ prevAwayState = hostAwayReady ? hostAway : prevAwayState
1562
1622
  if (isAway() && !controller.isOpen()) controller.open()
1563
1623
  } catch (e) {}
1564
1624
  }
@@ -1604,6 +1664,32 @@ window.__ModuleLoader__.load({
1604
1664
  checkNotices()
1605
1665
  var noticesTimer = setInterval(checkNotices, 3600 * 1000)
1606
1666
 
1667
+ // 时间检测轮询(30s):拉 host 的暂离状态/待展示总结,检测暂离回归并自动弹窗
1668
+ var lastPendingSummary = null
1669
+ var awayPollTimer = null
1670
+ function pollTimeState() {
1671
+ apiGet(API.state).then(function (d) {
1672
+ try {
1673
+ if (d && typeof d.away === 'boolean') {
1674
+ hostAway = d.away
1675
+ hostAwayReady = true
1676
+ }
1677
+ if (d && d.pendingSummary && d.pendingSummary.summary) {
1678
+ var seenKey = 'dsh-auto-memory.seenSummary.' + (d.pendingSummary.date || '') + '.' + (d.pendingSummary.time || '')
1679
+ try {
1680
+ if (!localStorage.getItem(seenKey)) {
1681
+ localStorage.setItem(seenKey, '1')
1682
+ lastPendingSummary = d.pendingSummary
1683
+ }
1684
+ } catch (e2) {}
1685
+ }
1686
+ autoOpenOnReturn()
1687
+ } catch (e) {}
1688
+ }).catch(function () {})
1689
+ }
1690
+ pollTimeState()
1691
+ awayPollTimer = setInterval(pollTimeState, 30000)
1692
+
1607
1693
  var surfaceDisposers = []
1608
1694
  function registerSurfaces() {
1609
1695
  try {
package/lib/index.js CHANGED
@@ -83,6 +83,10 @@ const DEFAULT_CONFIG = {
83
83
  autoConsolidate: true,
84
84
  /** 自动沉淀内容门槛:本轮 user+assistant 文本总字符数低于此值视为寒暄,跳过。 */
85
85
  autoConsolidateMinChars: 60,
86
+ /** 暂离阈值(分钟):距上次活动超过该值视为暂离,回归时自动弹出记忆窗口并欢迎。默认 60。 */
87
+ awayMinutes: 60,
88
+ /** 自动总结时间点(24h "HH:MM" 数组,如 ["12:00","18:00","22:00"]):到点自动生成本时段总结并弹窗展示。空数组=关闭。 */
89
+ autoSummaryTimes: [],
86
90
  /** 日界(分钟,从 0 点起算):凌晨在此之前的活儿归前一天日志;默认 450=早上 7:30 后才进入新一天。 */
87
91
  dayBoundaryMinutes: 450,
88
92
  /** 是否启用每日反思。 */
@@ -146,6 +150,7 @@ class MemoryEngine {
146
150
  reflectionShownSession: undefined,
147
151
  todayGreeting: '', greetingShownSession: undefined,
148
152
  calendarText: '', calendarPath: undefined,
153
+ away: false, pendingSummary: undefined, // 时间检测:暂离标记 / 待展示的自动时段总结
149
154
  loadedAt: 0, loading: undefined, configLoaded: false,
150
155
  }
151
156
  this._configPath = path.join(dshHome(), 'dsh-auto-memory.json')
@@ -837,6 +842,8 @@ class MemoryEngine {
837
842
  async recall(query, limit = 8, agent) {
838
843
  const q = String(query || '').toLowerCase().trim()
839
844
  if (!q) return 'memory_recall: query 为空。'
845
+ // 多词查询:按空白/中文标点分词,任一词命中即算命中(OR),按命中词数排序取相关度最高的
846
+ const terms = q.split(/[\s,,、;;。::]+/).filter((t) => t.length > 0)
840
847
  const p = await this.resolvePaths(agent)
841
848
  const out = []
842
849
  const hits = []
@@ -845,12 +852,17 @@ class MemoryEngine {
845
852
  if (!text) return
846
853
  const matched = []
847
854
  for (const line of text.split('\n')) {
848
- if (line.toLowerCase().includes(q)) {
849
- matched.push(line.trim().slice(0, 200))
850
- if (matched.length >= maxMatches) break
855
+ const low = line.toLowerCase()
856
+ const score = terms.reduce((a, t) => a + (low.includes(t) ? 1 : 0), 0)
857
+ if (score > 0) {
858
+ matched.push({ line: line.trim().slice(0, 200), score })
859
+ if (matched.length >= maxMatches * 4) break
851
860
  }
852
861
  }
853
- if (matched.length) target.push({ where: label, matches: matched })
862
+ if (matched.length) {
863
+ matched.sort((a, b) => b.score - a.score)
864
+ target.push({ where: label, matches: matched.slice(0, maxMatches).map((m) => m.line) })
865
+ }
854
866
  }
855
867
  // 读取顺序:progress(日志/反思)先行,再读 memory(用户级/项目笔记)
856
868
  const logs = await this.listDailyLogs(p.projectDir, 40)
@@ -1385,6 +1397,70 @@ class MemoryEngine {
1385
1397
  return result
1386
1398
  }
1387
1399
 
1400
+ // ---------- 时间检测(每 15s 心跳 tick):暂离状态 / _lastAgent 定时兜底 / 自动总结时间点 ----------
1401
+ tickTime() {
1402
+ try {
1403
+ // 1) 暂离检测:距上次活动(lastActive)超过 awayMinutes 视为暂离
1404
+ const awayMs = Math.max(Number(this.config.awayMinutes) || 60, 1) * 60000
1405
+ this.state.away = !!(this._lastActiveAt && Date.now() - this._lastActiveAt > awayMs)
1406
+ // 2) _lastAgent 定时兜底(重启恢复会话不触发 session-start;pre-step 兜底之外的第二保险)
1407
+ if (!this._lastAgent) this.restoreLastAgent()
1408
+ // 3) 自动总结时间点:命中配置的 HH:MM 且当天未生成过 → 生成时段总结并标记展示
1409
+ const times = Array.isArray(this.config.autoSummaryTimes) ? this.config.autoSummaryTimes : []
1410
+ if (times.length) {
1411
+ const now = new Date()
1412
+ const hhmm = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0')
1413
+ const today = this.memToday()
1414
+ for (const t of times) {
1415
+ if (hhmm === t) {
1416
+ const key = today + '|' + t
1417
+ if (this._summaryDone !== key) {
1418
+ this._summaryDone = key
1419
+ void this.triggerAutoSummary(t)
1420
+ }
1421
+ }
1422
+ }
1423
+ }
1424
+ } catch (e) {}
1425
+ }
1426
+
1427
+ /** 从 sessions/agent 服务恢复 _lastAgent(定时兜底)。 */
1428
+ restoreLastAgent() {
1429
+ try {
1430
+ const sessionsSvc = this._sessionsSvc
1431
+ const agentSvc = this._agentSvc
1432
+ if (!sessionsSvc || !agentSvc || typeof sessionsSvc.list !== 'function') return
1433
+ const sessions = sessionsSvc.list()
1434
+ if (!sessions || !sessions.length) return
1435
+ // 找最近活跃的顶层会话(log 最后事件时间最大)
1436
+ let best = null, bestTime = 0
1437
+ for (const s of sessions) {
1438
+ let t = 0
1439
+ try { const last = s.log && s.log[s.log.length - 1]; if (last && last.time) t = last.time } catch (e) {}
1440
+ if (t >= bestTime) { bestTime = t; best = s }
1441
+ }
1442
+ if (!best || typeof agentSvc.get !== 'function') return
1443
+ const a = agentSvc.get(best.id)
1444
+ if (a && a.session && a.session.header && a.session.header.parentSession === undefined) this._lastAgent = a
1445
+ } catch (e) {}
1446
+ }
1447
+
1448
+ /** 自动总结:按时间点推断时段,生成总结并置 pendingSummary(供 client 弹窗)。 */
1449
+ async triggerAutoSummary(timePoint) {
1450
+ try {
1451
+ const h = Number(String(timePoint).slice(0, 2)) || 0
1452
+ const period = h < 6 ? 'morning' : h < 9 ? 'morning' : h < 12 ? 'forenoon' : h < 14 ? 'noon' : h < 18 ? 'afternoon' : 'evening'
1453
+ const out = await this.summarizePeriod(period, undefined, false)
1454
+ if (out && out.summary) {
1455
+ this.state.pendingSummary = {
1456
+ time: timePoint, date: this.memToday(), period,
1457
+ summary: out.summary, works: out.works || [], generatedAt: out.generatedAt || Date.now(),
1458
+ }
1459
+ console.log('[dsh-auto-memory] auto summary triggered at ' + timePoint + ' (period ' + period + ')')
1460
+ }
1461
+ } catch (e) {}
1462
+ }
1463
+
1388
1464
  /** 今日 AI 问候(按时段,每天每时段生成一次并缓存到 greetings/{date}.json)。 */
1389
1465
  async greetToday(agent) {
1390
1466
  const hour = new Date().getHours()
@@ -1816,6 +1892,11 @@ class MemoryEngine {
1816
1892
  todayEntries,
1817
1893
  latestReflectionDate: this.state.latestReflectionDate,
1818
1894
  pendingReflection: this.state.pendingReflection ? this.state.pendingReflection.date : undefined,
1895
+ // 时间检测:暂离状态 / 待展示的自动总结 / 相关配置
1896
+ away: !!this.state.away,
1897
+ awayMinutes: Math.max(Number(this.config.awayMinutes) || 60, 1),
1898
+ autoSummaryTimes: Array.isArray(this.config.autoSummaryTimes) ? this.config.autoSummaryTimes : [],
1899
+ pendingSummary: this.state.pendingSummary || null,
1819
1900
  autoStats: this.autoStats,
1820
1901
  greeting: this.greetingData(),
1821
1902
  calendar: this.parseCalendar(this.state.calendarText),
@@ -2062,6 +2143,12 @@ class ExternalMemory {
2062
2143
  async search(query, limit = 6) {
2063
2144
  const q = String(query || '').toLowerCase().trim()
2064
2145
  if (!q) return []
2146
+ // 多词查询:任一词命中即算命中(OR),按命中词数排序取相关度最高的
2147
+ const terms = q.split(/[\s,,、;;。::]+/).filter((t) => t.length > 0)
2148
+ const scoreLine = (line) => {
2149
+ const low = line.toLowerCase()
2150
+ return terms.reduce((a, t) => a + (low.includes(t) ? 1 : 0), 0)
2151
+ }
2065
2152
  const srcs = await this.discover(false)
2066
2153
  const out = []
2067
2154
  for (const s of srcs) {
@@ -2073,22 +2160,28 @@ class ExternalMemory {
2073
2160
  if (hits.length >= 3 || scanned >= 8 || out.length >= limit) break
2074
2161
  scanned++
2075
2162
  const text = await this.extractSessionText(f.path)
2163
+ const matched = []
2076
2164
  for (const line of text.split('\n')) {
2077
- if (line.toLowerCase().includes(q)) {
2078
- hits.push('(' + path.basename(f.path).slice(0, 20) + ') ' + line.trim().slice(0, 200))
2079
- if (hits.length >= 3) break
2165
+ const score = scoreLine(line)
2166
+ if (score > 0) {
2167
+ matched.push({ line: '(' + path.basename(f.path).slice(0, 20) + ') ' + line.trim().slice(0, 200), score })
2168
+ if (matched.length >= 6) break
2080
2169
  }
2081
2170
  }
2171
+ matched.sort((a, b) => b.score - a.score)
2172
+ hits.push(...matched.slice(0, 3).map((m) => m.line))
2082
2173
  }
2083
2174
  } else {
2084
2175
  const matched = []
2085
2176
  for (const line of s.content.split('\n')) {
2086
- if (line.toLowerCase().includes(q)) {
2087
- matched.push(line.trim().slice(0, 200))
2088
- if (matched.length >= 3) break
2177
+ const score = scoreLine(line)
2178
+ if (score > 0) {
2179
+ matched.push({ line: line.trim().slice(0, 200), score })
2180
+ if (matched.length >= 9) break
2089
2181
  }
2090
2182
  }
2091
- hits.push(...matched)
2183
+ matched.sort((a, b) => b.score - a.score)
2184
+ hits.push(...matched.slice(0, 3).map((m) => m.line))
2092
2185
  }
2093
2186
  if (hits.length) out.push({ source: s.name, tool: s.tool, kind: s.kind, lines: hits })
2094
2187
  }
@@ -2205,6 +2298,9 @@ export function apply(ctx, config) {
2205
2298
  const sessionQuery = ctx.get('sessionQuery')
2206
2299
  engine._sessionQuery = sessionQuery
2207
2300
  engine._subagents = ctx.get('subagents') || undefined
2301
+ // 时间检测用的服务(软获取,缺失时定时兜底自动降级)
2302
+ engine._sessionsSvc = ctx.get('sessions') || undefined
2303
+ engine._agentSvc = ctx.get('agent') || undefined
2208
2304
 
2209
2305
  // 生命周期刷新
2210
2306
  const refreshAll = (agent) => { void engine.refresh(agent) }
@@ -2237,6 +2333,9 @@ export function apply(ctx, config) {
2237
2333
  ctx.on('agent/pre-step', async (payload, next) => {
2238
2334
  try {
2239
2335
  const agent = (payload && payload.agent) || engine._lastAgent
2336
+ // 兜底恢复 _lastAgent(仅启动后首次为空时设置一次,不每步覆盖):重启后恢复的会话不触发 session-start,
2337
+ // 导致 turn-stopping 自动沉淀拿不到 agent 而永久失效;首次 pre-step 即恢复,之后由 session-start 正常维护
2338
+ try { if (!engine._lastAgent && agent && agent.session && agent.session.header && agent.session.header.parentSession === undefined) engine._lastAgent = agent } catch (e) {}
2240
2339
  // 只刷新顶层会话:子代理(自动沉淀/固化的 subagent)session 无 cwd,刷新会把 state 切到错误工作区
2241
2340
  let skip = false
2242
2341
  try { if (agent && agent.session && agent.session.header && agent.session.header.parentSession) skip = true } catch (e) {}
@@ -2786,7 +2885,7 @@ export function apply(ctx, config) {
2786
2885
  })()
2787
2886
  }, 5 * 60 * 1000)
2788
2887
  // 心跳独立 15 秒一次(对齐 polling-lease 心跳节奏),证明轮询机制活着
2789
- const heartbeatTimer = setInterval(() => { void writeHeartbeat() }, 15000)
2888
+ const heartbeatTimer = setInterval(() => { void writeHeartbeat(); try { engine.tickTime() } catch (e) {} }, 15000)
2790
2889
  void writeHeartbeat() // 立即心跳一次:重启后马上可见轮询存活
2791
2890
 
2792
2891
  // ---------- 注册与清理 ----------
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@a9i5k4/dsh-auto-memory",
3
3
  "description": "DSH 自动记忆插件:三层记忆(用户级/项目笔记/每日日志)自动注入与检索、每轮对话自动沉淀、每日反思、可视化面板与设置页,支持继承其他 AI 工具的记忆。",
4
- "version": "0.1.18",
4
+ "version": "0.1.20",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {
@@ -1,568 +0,0 @@
1
- /* dsh-auto-memory — browser half (hand-written __ModuleLoader__ bundle).
2
- * Registers three additive surfaces:
3
- * 1. sidebar.footer.action — 「记忆」入口按钮(开关左下角浮层面板)
4
- * 2. shell.overlay — 记忆面板:概览 / 日志 / 笔记 / 反思 / 检索
5
- * 液态玻璃视觉(backdrop-filter + --dsw-alias-* 主题令牌),
6
- * 可拖动 / 右下角缩放 / 开关缩放动画 / 位置大小持久化。
7
- * 3. settings.section — 自动记忆设置页(存储位置、注入、反思风格)
8
- * Data flows over /api/dsh-auto-memory/* (loopback-only host routes).
9
- */
10
- window.__ModuleLoader__.load({
11
- id: '@deepseek-ai/dsh-auto-memory',
12
- factory: (require) => {
13
- var module = { exports: {} }
14
- var exports = module.exports
15
- var React = require('react')
16
- var h = React.createElement
17
- var useState = React.useState
18
- var useEffect = React.useEffect
19
- var useReducer = React.useReducer
20
-
21
- // ───────────────────────── 控制器 ─────────────────────────
22
- var listeners = new Set()
23
- var panelOpen = false
24
- var panelClosing = false
25
- var closeTimer = null
26
- // 几何状态:left/top/width/height,持久化到 localStorage(用户可拖动/缩放)
27
- var GEOM_KEY = 'dsh-auto-memory.panel.geom'
28
- var DEFAULT_W = 440
29
- var DEFAULT_H = 560
30
- var DEFAULT_GAP = 16
31
- function defaultGeom() {
32
- var vh = window.innerHeight || 800
33
- return {
34
- left: DEFAULT_GAP,
35
- top: Math.max(DEFAULT_GAP, vh - DEFAULT_H - DEFAULT_GAP),
36
- width: DEFAULT_W,
37
- height: DEFAULT_H,
38
- }
39
- }
40
- var geom = null
41
- function clampGeom(g) {
42
- var vw = window.innerWidth || 1280
43
- var vh = window.innerHeight || 800
44
- var w = Math.max(300, Math.min(g.width || DEFAULT_W, vw - DEFAULT_GAP * 2))
45
- var h = Math.max(240, Math.min(g.height || DEFAULT_H, vh - DEFAULT_GAP * 2))
46
- return {
47
- left: Math.max(DEFAULT_GAP, Math.min(g.left !== undefined ? g.left : DEFAULT_GAP, vw - w - DEFAULT_GAP)),
48
- top: Math.max(DEFAULT_GAP, Math.min(g.top !== undefined ? g.top : DEFAULT_GAP, vh - h - DEFAULT_GAP)),
49
- width: w,
50
- height: h,
51
- }
52
- }
53
- function loadGeom() {
54
- try {
55
- var raw = localStorage.getItem(GEOM_KEY)
56
- if (raw) return clampGeom(JSON.parse(raw))
57
- } catch (e) {}
58
- return clampGeom(defaultGeom())
59
- }
60
- function persistGeom() { if (geom) { try { localStorage.setItem(GEOM_KEY, JSON.stringify(geom)) } catch (e) {} } }
61
- function emit() { listeners.forEach(function (fn) { try { fn() } catch (e) {} }) }
62
- var controller = {
63
- isOpen: function () { return panelOpen },
64
- isClosing: function () { return panelClosing },
65
- geom: function () { if (!geom) geom = loadGeom(); return geom },
66
- setGeom: function (partial) {
67
- var cur = controller.geom()
68
- geom = clampGeom({
69
- left: partial.left !== undefined ? partial.left : cur.left,
70
- top: partial.top !== undefined ? partial.top : cur.top,
71
- width: partial.width !== undefined ? partial.width : cur.width,
72
- height: partial.height !== undefined ? partial.height : cur.height,
73
- })
74
- emit()
75
- },
76
- flushGeom: function () { persistGeom() },
77
- resetGeom: function () { geom = clampGeom(defaultGeom()); persistGeom(); emit() },
78
- toggle: function () { if (panelOpen) controller.close(); else controller.open() },
79
- open: function () {
80
- if (closeTimer) { clearTimeout(closeTimer); closeTimer = null }
81
- panelOpen = true; panelClosing = false; emit()
82
- },
83
- close: function () {
84
- if (!panelOpen || panelClosing) return
85
- panelClosing = true; emit()
86
- closeTimer = setTimeout(function () {
87
- panelOpen = false; panelClosing = false; closeTimer = null; emit()
88
- }, 170)
89
- },
90
- subscribe: function (fn) { listeners.add(fn); return function () { listeners.delete(fn) } },
91
- }
92
-
93
- // ───────────────────────── API ─────────────────────────
94
- var API = {
95
- state: '/api/dsh-auto-memory/state',
96
- list: '/api/dsh-auto-memory/list',
97
- file: '/api/dsh-auto-memory/file',
98
- recall: '/api/dsh-auto-memory/recall',
99
- config: '/api/dsh-auto-memory/config',
100
- reflect: '/api/dsh-auto-memory/reflect',
101
- note: '/api/dsh-auto-memory/note',
102
- external: '/api/dsh-auto-memory/external',
103
- externalImport: '/api/dsh-auto-memory/external-import',
104
- }
105
- function query(params) {
106
- var search = new URLSearchParams()
107
- for (var key in params) if (params[key] !== undefined && params[key] !== '') search.set(key, String(params[key]))
108
- var text = search.toString()
109
- return text ? '?' + text : ''
110
- }
111
- async function apiGet(path, params) {
112
- var res = await fetch(path + query(params))
113
- var body = await res.json().catch(function () { return {} })
114
- if (!res.ok) throw new Error(body.error || ('HTTP ' + res.status))
115
- return body
116
- }
117
- async function apiPost(path, payload) {
118
- var res = await fetch(path, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(payload) })
119
- var body = await res.json().catch(function () { return {} })
120
- if (!res.ok) throw new Error(body.error || ('HTTP ' + res.status))
121
- return body
122
- }
123
-
124
- // ───────────────────────── 样式 ─────────────────────────
125
- // 视觉:液态玻璃(毛玻璃)—— backdrop-filter + DSH 主题令牌(--dsw-alias-*),
126
- // 跟随亮/暗主题与页面背景自适应;位置/尺寸由 JS 几何状态驱动(可拖动、可缩放)。
127
- var CSS = [
128
- '[data-dam-panel] { position: fixed; left: 16px; width: 440px; height: 560px;',
129
- ' max-width: calc(100vw - 32px); max-height: calc(100vh - 32px);',
130
- ' display: flex; flex-direction: column; overflow: hidden; z-index: 3000; pointer-events: auto;',
131
- ' border-radius: 16px; font: 13px/1.55 system-ui, "Segoe UI", sans-serif;',
132
- ' background: color-mix(in srgb, var(--dsw-alias-bg-overlay, rgba(255,255,255,.86)) 58%, transparent);',
133
- ' -webkit-backdrop-filter: blur(28px) saturate(1.55); backdrop-filter: blur(28px) saturate(1.55);',
134
- ' border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.35)) 65%, transparent);',
135
- ' box-shadow: 0 24px 64px rgba(0,0,0,.22), 0 4px 16px rgba(0,0,0,.10), inset 0 1px 0 rgba(255,255,255,.22);',
136
- ' color: var(--dsw-alias-label-primary, #1f2328);',
137
- ' animation: dam-in .2s cubic-bezier(.2,.9,.3,1.15) both; transform-origin: left bottom; }',
138
- '@keyframes dam-in { from { opacity: 0; transform: scale(.92) translateY(10px); } to { opacity: 1; transform: none; } }',
139
- '@keyframes dam-out { from { opacity: 1; transform: none; } to { opacity: 0; transform: scale(.95) translateY(6px); } }',
140
- '[data-dam-panel][data-closing="true"] { animation: dam-out .16s ease-in both; }',
141
- '[data-dam-panel]::before { content: ""; position: absolute; inset: 0 0 auto 0; height: 64%; pointer-events: none;',
142
- ' background: linear-gradient(180deg, rgba(255,255,255,.13), rgba(255,255,255,0) 70%); border-radius: 16px 16px 0 0; }',
143
- '[data-dam-panel][data-dragging="true"] { user-select: none; }',
144
- '[data-dam-panel] header { display: flex; align-items: center; gap: 8px; padding: 10px 14px; cursor: grab;',
145
- ' border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.25)) 55%, transparent); }',
146
- '[data-dam-panel][data-dragging="true"] header { cursor: grabbing; }',
147
- '[data-dam-panel] header strong { font-size: 14px; }',
148
- '[data-dam-panel] header .dam-spacer { flex: 1; }',
149
- '[data-dam-resize] { position: absolute; right: 0; bottom: 0; width: 22px; height: 22px; cursor: nwse-resize;',
150
- ' opacity: .55; z-index: 2; border-radius: 0 0 16px 0;',
151
- ' background: linear-gradient(135deg, transparent 50%, color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 45%, transparent) 50%); }',
152
- '[data-dam-resize]:hover { opacity: 1; }',
153
- '[data-dam-btn] { border: none; background: transparent; cursor: pointer; color: inherit; opacity: .75; font-size: 13px; padding: 4px 8px; border-radius: 6px; }',
154
- '[data-dam-btn]:hover { opacity: 1; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 16%, transparent); }',
155
- '[data-dam-tabs] { display: flex; gap: 2px; padding: 6px 10px; border-bottom: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.2)) 55%, transparent); }',
156
- '[data-dam-tab] { border: none; background: transparent; cursor: pointer; color: inherit; opacity: .6; padding: 5px 10px; border-radius: 7px; font-size: 12.5px; }',
157
- '[data-dam-tab][data-active="true"] { opacity: 1; background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 18%, transparent); font-weight: 600; }',
158
- '[data-dam-body] { flex: 1; overflow: auto; padding: 12px 14px; }',
159
- '[data-dam-kv] { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; font-size: 12.5px; }',
160
- '[data-dam-kv] b { opacity: .55; font-weight: 500; }',
161
- '[data-dam-card] { border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.22)) 60%, transparent); border-radius: 9px; padding: 8px 10px; margin-bottom: 8px; font-size: 12.5px;',
162
- ' background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 40%, transparent); }',
163
- '[data-dam-card] .dam-date { font-weight: 600; margin-bottom: 4px; }',
164
- '[data-dam-card] .dam-content { white-space: pre-wrap; word-break: break-word; max-height: 220px; overflow: auto; }',
165
- '[data-dam-banner] { border: 1px solid color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e6a23c) 55%, transparent);',
166
- ' background: color-mix(in srgb, var(--dsw-alias-state-warn-primary, #e6a23c) 13%, transparent); border-radius: 9px; padding: 8px 10px; margin-bottom: 10px; font-size: 12.5px; }',
167
- '[data-dam-input], [data-dam-select] { width: 100%; box-sizing: border-box; background: color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.08)) 45%, transparent); color: inherit; border: 1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.3)) 60%, transparent); border-radius: 7px; padding: 6px 8px; font: inherit; }',
168
- '[data-dam-row] { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }',
169
- '[data-dam-row] label { flex: 0 0 110px; opacity: .8; font-size: 12.5px; }',
170
- '[data-dam-hint] { opacity: .5; font-size: 11.5px; margin-top: 2px; }',
171
- '[data-dam-sidebar-btn] { display: flex; align-items: center; gap: 6px; width: 100%; border: none; background: transparent; color: inherit; cursor: pointer; padding: 6px 10px; border-radius: 8px; font: inherit; font-size: 13px; opacity: .8; }',
172
- '[data-dam-sidebar-btn]:hover { background: color-mix(in srgb, var(--dsw-alias-label-secondary, #666) 14%, transparent); opacity: 1; }',
173
- '[data-dam-sidebar-btn][data-active="true"] { opacity: 1; background: color-mix(in srgb, var(--dsw-alias-brand-primary, #4f7cff) 16%, transparent); color: var(--dsw-alias-brand-primary, #4f7cff); }',
174
- '[data-dam-error] { color: var(--dsw-alias-state-error-primary, #d64545); font-size: 12px; margin-top: 6px; white-space: pre-wrap; }',
175
- '[data-dam-muted] { opacity: .55; }',
176
- '@supports not ((backdrop-filter: blur(1px)) or (-webkit-backdrop-filter: blur(1px))) {',
177
- ' [data-dam-panel] { background: var(--dsw-alias-bg-overlay, #ffffff); } }',
178
- ].join('\n')
179
- var STYLE_ID = 'dsh-auto-memory-css'
180
- function ensureStyle() {
181
- if (document.getElementById(STYLE_ID)) return
182
- var tag = document.createElement('style')
183
- tag.id = STYLE_ID
184
- tag.dataset.plugin = '@deepseek-ai/dsh-auto-memory'
185
- tag.textContent = CSS
186
- document.head.appendChild(tag)
187
- }
188
-
189
- // ───────────────────────── 通用小组件 ─────────────────────────
190
- function useTick() { return useReducer(function (x) { return x + 1 }, 0) }
191
-
192
- function Banner(props) {
193
- return h('div', { 'data-dam-banner': '' }, props.children)
194
- }
195
-
196
- function Card(props) {
197
- return h('div', { 'data-dam-card': '' },
198
- h('div', { className: 'dam-date' }, props.title),
199
- h('div', { className: 'dam-content' }, props.children))
200
- }
201
-
202
- function Loading() { return h('div', { 'data-dam-muted': '' }, '加载中…') }
203
-
204
- // ───────────────────────── 侧边栏入口 ─────────────────────────
205
- function SidebarButton() {
206
- var tick = useTick()
207
- useEffect(function () { return controller.subscribe(tick[1]) }, [])
208
- return h('button', {
209
- 'data-dam-sidebar-btn': '',
210
- title: '记忆面板',
211
- 'data-active': (panelOpen || panelClosing) ? 'true' : undefined,
212
- onClick: function () { controller.toggle() },
213
- }, h('span', null, '记忆'))
214
- }
215
-
216
- // ───────────────────────── 记忆面板 ─────────────────────────
217
- function fmtSize(n) {
218
- if (n >= 1024 * 1024) return (n / 1024 / 1024).toFixed(1) + ' MB'
219
- if (n >= 1024) return (n / 1024).toFixed(1) + ' KB'
220
- return n + ' B'
221
- }
222
-
223
- function OverviewTab() {
224
- var tick = useTick()
225
- var state = null
226
- useEffect(function () {
227
- var alive = true
228
- apiGet(API.state).then(function (s) { if (alive) { state = s; tick[1]() } }).catch(function () {})
229
- return function () { alive = false }
230
- }, [])
231
- if (!state) return h(Loading)
232
- return h('div', null,
233
- state.pendingReflection
234
- ? h(Banner, null, '待生成反思: ' + state.pendingReflection + ' —— 打开/继续会话,agent 会在会话开始时主动呈现昨日反思。')
235
- : null,
236
- h('div', { 'data-dam-kv': '' },
237
- h('b', null, '工作区'), h('span', null, state.ws),
238
- h('b', null, '用户级记忆'), h('span', null, state.userFile + ' (' + fmtSize(state.sizes.user) + ')'),
239
- h('b', null, '项目笔记'), h('span', null, state.notesPath + ' (' + fmtSize(state.sizes.notes) + ')'),
240
- h('b', null, '今日日志'), h('span', null, state.logPath + ' · ' + state.todayEntries + ' 条 (' + fmtSize(state.sizes.log) + ')'),
241
- h('b', null, '最近反思'), h('span', null, state.latestReflectionDate || '(无)'),
242
- h('b', null, '配置文件'), h('span', null, state.configReadError ? ('读取失败: ' + state.configReadError) : '正常')),
243
- h('div', { 'data-dam-hint': '' }, '上次刷新: ' + (state.refreshedAt ? new Date(state.refreshedAt).toLocaleString() : '尚未')))
244
- }
245
-
246
- function LogsTab() {
247
- var tick = useTick()
248
- var data = null
249
- var open = null
250
- var content = ''
251
- useEffect(function () {
252
- var alive = true
253
- apiGet(API.list).then(function (d) { if (alive) { data = d; tick[1]() } }).catch(function () {})
254
- return function () { alive = false }
255
- }, [])
256
- useEffect(function () {
257
- if (!open) return
258
- var alive = true
259
- apiGet(API.file, { path: open }).then(function (d) { if (alive) { content = d.content; tick[1]() } }).catch(function () {})
260
- return function () { alive = false }
261
- }, [open])
262
- if (!data) return h(Loading)
263
- var rows = []
264
- if (open) {
265
- rows.push(h(Card, { title: pathName(open) },
266
- h('div', { 'data-dam-content': '' }, content || '(空)'),
267
- h('button', { 'data-dam-btn': '', onClick: function () { open = null; content = ''; tick[1]() } }, '← 返回')))
268
- } else {
269
- rows.push(h('div', { 'data-dam-hint': '' }, '点击日期查看当日日志(append-only):'))
270
- for (var i = 0; i < data.logs.length; i++) {
271
- (function (log) {
272
- rows.push(h(Card, { title: log.date + ' · ' + fmtSize(log.size) },
273
- h('button', { 'data-dam-btn': '', onClick: function () { open = log.date + '.md'; tick[1]() } }, '查看')))
274
- })(data.logs[i])
275
- }
276
- }
277
- return h('div', null, rows)
278
- }
279
- function pathName(p) { var parts = String(p).split(/[\\/]/); return parts[parts.length - 1] }
280
-
281
- function NotesTab() {
282
- var tick = useTick()
283
- var data = null
284
- var draft = ''
285
- var saving = false
286
- var msg = ''
287
- var err = ''
288
- if (!data) {
289
- apiGet(API.state).then(function (s) { data = s; tick[1]() }).catch(function () {})
290
- return h(Loading)
291
- }
292
- function save() {
293
- if (!draft.trim() || saving) return
294
- saving = true; msg = ''; err = ''
295
- apiPost(API.note, { content: draft.trim() }).then(function (d) {
296
- msg = d.result || '已追加 ✓'; draft = ''; saving = false; tick[1]()
297
- }).catch(function (e) { err = e.message; saving = false; tick[1]() })
298
- }
299
- return h('div', null,
300
- h('div', { 'data-dam-hint': '' }, '项目笔记: ' + data.notesPath),
301
- h('textarea', { 'data-dam-input': '', rows: 6, placeholder: '想追加到项目笔记的内容…(保存时自动带日期标题)', value: draft, onChange: function (e) { draft = e.target.value; tick[1]() } }),
302
- h('div', { 'data-dam-row': '' },
303
- h('button', { 'data-dam-btn': '', onClick: save, disabled: saving }, saving ? '保存中…' : '追加'),
304
- h('span', { 'data-dam-hint': '' }, '建议直接用对话让 agent 调 memory_note;此处为手动追加。')),
305
- msg ? h('div', null, msg) : null,
306
- err ? h('div', { 'data-dam-error': '' }, err) : null)
307
- }
308
-
309
- function ReflectionsTab() {
310
- var tick = useTick()
311
- var data = null
312
- var open = null
313
- var content = ''
314
- useEffect(function () {
315
- var alive = true
316
- apiGet(API.list).then(function (d) { if (alive) { data = d; tick[1]() } }).catch(function () {})
317
- return function () { alive = false }
318
- }, [])
319
- useEffect(function () {
320
- if (!open) return
321
- var alive = true
322
- apiGet(API.file, { path: open }).then(function (d) { if (alive) { content = d.content; tick[1]() } }).catch(function () {})
323
- return function () { alive = false }
324
- }, [open])
325
- if (!data) return h(Loading)
326
- var rows = []
327
- if (open) {
328
- rows.push(h(Card, { title: '反思 ' + pathName(open) }, h('div', { 'data-dam-content': '' }, content || '(空)')))
329
- rows.push(h('button', { 'data-dam-btn': '', onClick: function () { open = null; content = ''; tick[1]() } }, '← 返回'))
330
- } else {
331
- if (!data.reflections.length) rows.push(h('div', { 'data-dam-muted': '' }, '还没有反思。每天第一次会话时,agent 会主动呈现前一天的工作反思。'))
332
- for (var i = 0; i < data.reflections.length; i++) {
333
- (function (r) {
334
- rows.push(h(Card, { title: r.date + ' · ' + fmtSize(r.size) },
335
- h('button', { 'data-dam-btn': '', onClick: function () { open = 'reflections/' + r.name; tick[1]() } }, '查看')))
336
- })(data.reflections[i])
337
- }
338
- }
339
- return h('div', null, rows)
340
- }
341
-
342
- function SearchTab() {
343
- var tick = useTick()
344
- var q = ''
345
- var result = null
346
- var busy = false
347
- function search() {
348
- if (!q.trim() || busy) return
349
- busy = true; result = null; tick[1]()
350
- apiPost(API.recall, { query: q.trim() }).then(function (d) { result = d.result; busy = false; tick[1]() })
351
- .catch(function (e) { result = '检索失败: ' + e.message; busy = false; tick[1]() })
352
- }
353
- return h('div', null,
354
- h('div', { 'data-dam-row': '' },
355
- h('input', { 'data-dam-input': '', placeholder: '搜记忆:关键词或自包含描述…', value: q, onChange: function (e) { q = e.target.value; tick[1]() }, onKeyDown: function (e) { if (e.key === 'Enter') search() } }),
356
- h('button', { 'data-dam-btn': '', onClick: search, disabled: busy }, '检索')),
357
- result ? h(Card, { title: '结果' }, h('div', { 'data-dam-content': '' }, result)) : null)
358
- }
359
-
360
- var TOOL_LABEL = { workbuddy: 'WorkBuddy', codebuddy: 'CodeBuddy', claude: 'Claude Code', codex: 'Codex', 'project-files': '项目文件' }
361
- function ConnectTab() {
362
- var tick = useTick()
363
- var sources = null
364
- var busy = {}
365
- var msg = ''
366
- var err = ''
367
- function load() {
368
- apiGet(API.external).then(function (d) { sources = d.sources || []; tick[1]() }).catch(function (e) { err = e.message; tick[1]() })
369
- }
370
- useEffect(function () { load(); var t = setInterval(load, 60000); return function () { clearInterval(t) } }, [])
371
- if (!sources) return err ? h('div', { 'data-dam-error': '' }, err) : h(Loading)
372
- function doImport(source, target) {
373
- busy[source] = true; msg = ''; err = ''
374
- apiPost(API.externalImport, { source: source, target: target }).then(function (d) {
375
- msg = d.result || '接入完成'; busy[source] = false; tick[1]()
376
- }).catch(function (e) { err = e.message; busy[source] = false; tick[1]() })
377
- }
378
- function importAll() {
379
- var md = sources.filter(function (s) { return s.kind !== 'sessions' && s.enabled !== false })
380
- msg = '正在接入 ' + md.length + ' 个源…'; err = ''
381
- var chain = Promise.resolve()
382
- md.forEach(function (s) { chain = chain.then(function () { return apiPost(API.externalImport, { source: s.id, target: 'project' }) }) })
383
- chain.then(function () { msg = '全部接入完成 ✓'; tick[1]() }).catch(function (e) { err = e.message; tick[1]() })
384
- }
385
- var cards = []
386
- if (!sources.length) {
387
- cards.push(h('div', { 'data-dam-muted': '' }, '未检测到其他 AI 工具的记忆文件(WorkBuddy/CodeBuddy/Claude Code/Codex)。'))
388
- }
389
- for (var i = 0; i < sources.length; i++) {
390
- (function (s) {
391
- var actions = []
392
- if (s.kind === 'sessions') {
393
- actions.push(h('span', { 'data-dam-hint': '' }, '会话源:共 ' + s.fileCount + ' 个会话文件,用 memory_recall 按需检索'))
394
- } else {
395
- actions.push(h('button', { 'data-dam-btn': '', disabled: !!busy[s.id], onClick: function () { doImport(s.id, 'project') } }, busy[s.id] ? '接入中…' : '接入项目笔记'))
396
- actions.push(h('button', { 'data-dam-btn': '', disabled: !!busy[s.id], onClick: function () { doImport(s.id, 'user') } }, '接入用户记忆'))
397
- }
398
- cards.push(h(Card, { title: s.name + ' · ' + s.tool + (s.enabled === false ? ' · 已停用' : '') },
399
- h('div', { 'data-dam-hint': '' }, s.kind + ' · ' + s.fileCount + ' 个文件 · ' + fmtSize(s.size)),
400
- s.preview ? h('div', { 'data-dam-content': '' }, s.preview) : null,
401
- h('div', { 'data-dam-row': '' }, actions)))
402
- })(sources[i])
403
- }
404
- return h('div', null,
405
- h('div', { 'data-dam-hint': '' }, '把其他 AI 工具(WorkBuddy / CodeBuddy / Claude Code / Codex / 项目约定文件)积累的记忆接入当前 DSH 工作。接入后内容写入本地记忆并自动标注来源,后续会随会话自动注入。'),
406
- h('div', { 'data-dam-row': '' },
407
- h('button', { 'data-dam-btn': '', onClick: importAll }, '一键接入全部'),
408
- h('button', { 'data-dam-btn': '', onClick: load }, '重新扫描')),
409
- msg ? h('div', null, msg) : null,
410
- err ? h('div', { 'data-dam-error': '' }, err) : null,
411
- cards)
412
- }
413
-
414
- // ───────────────────────── 拖动/缩放辅助 ─────────────────────────
415
- var dragActive = false
416
- function startPointerDrag(onMove, interactiveSel) {
417
- return function (e) {
418
- // 命中交互控件(按钮/输入框等)时不启动拖动
419
- if (interactiveSel && e.target && e.target.closest && e.target.closest(interactiveSel)) return
420
- e.preventDefault()
421
- e.stopPropagation()
422
- var startX = e.clientX
423
- var startY = e.clientY
424
- var moved = false
425
- function move(ev) {
426
- var dx = ev.clientX - startX
427
- var dy = ev.clientY - startY
428
- if (!moved && Math.abs(dx) < 2 && Math.abs(dy) < 2) return
429
- moved = true
430
- if (!dragActive) { dragActive = true; emit() }
431
- onMove(dx, dy)
432
- }
433
- function up() {
434
- window.removeEventListener('pointermove', move)
435
- window.removeEventListener('pointerup', up)
436
- if (dragActive) { dragActive = false; emit() }
437
- controller.flushGeom()
438
- }
439
- window.addEventListener('pointermove', move)
440
- window.addEventListener('pointerup', up)
441
- }
442
- }
443
-
444
- function MemoryPanel() {
445
- var tick = useTick()
446
- var tab = 'overview'
447
- var g = controller.geom()
448
- useEffect(function () { return controller.subscribe(tick[1]) }, [])
449
- if (!panelOpen && !panelClosing) return null
450
- var body
451
- if (tab === 'overview') body = h(OverviewTab)
452
- else if (tab === 'logs') body = h(LogsTab)
453
- else if (tab === 'notes') body = h(NotesTab)
454
- else if (tab === 'reflections') body = h(ReflectionsTab)
455
- else if (tab === 'connect') body = h(ConnectTab)
456
- else body = h(SearchTab)
457
- var tabs = [['overview', '概览'], ['logs', '日志'], ['notes', '笔记'], ['reflections', '反思'], ['connect', '接续'], ['search', '检索']]
458
- var style = {
459
- left: g.left + 'px',
460
- top: g.top + 'px',
461
- width: g.width + 'px',
462
- height: g.height + 'px',
463
- }
464
- var dragMove = startPointerDrag(function (dx, dy) {
465
- controller.setGeom({ left: g.left + dx, top: g.top + dy })
466
- }, '[data-dam-btn], [data-dam-tab], [data-dam-input], [data-dam-select], textarea')
467
- var resizeMove = startPointerDrag(function (dx, dy) {
468
- controller.setGeom({ width: g.width + dx, height: g.height + dy })
469
- })
470
- return h('div', {
471
- 'data-dam-panel': '',
472
- style: style,
473
- 'data-closing': panelClosing ? 'true' : undefined,
474
- 'data-dragging': dragActive ? 'true' : undefined,
475
- },
476
- h('header', {
477
- title: '拖动移动',
478
- onPointerDown: dragMove,
479
- },
480
- h('strong', null, '自动记忆'),
481
- h('span', { className: 'dam-spacer' }),
482
- h('button', { 'data-dam-btn': '', title: '恢复默认位置', onClick: function () { controller.resetGeom() } }, '⤾'),
483
- h('button', { 'data-dam-btn': '', title: '刷新', onClick: function () { tick[1]() } }, '⟳'),
484
- h('button', { 'data-dam-btn': '', title: '关闭', onClick: function () { controller.close() } }, '✕')),
485
- h('div', { 'data-dam-tabs': '' }, tabs.map(function (t) {
486
- return h('button', { key: t[0], 'data-dam-tab': '', 'data-active': tab === t[0] ? 'true' : undefined, onClick: function () { tab = t[0]; tick[1]() } }, t[1])
487
- })),
488
- h('div', { 'data-dam-body': '' }, body),
489
- h('div', { 'data-dam-resize': '', title: '拖动调整大小', onPointerDown: resizeMove }))
490
- }
491
-
492
- // ───────────────────────── 设置页 ─────────────────────────
493
- var STYLE_OPTIONS = [['auto', '由内容决定'], ['life', '生活化'], ['professional', '专业性']]
494
- function SettingsPage() {
495
- var tick = useTick()
496
- var cfg = null
497
- var busy = false
498
- var msg = ''
499
- var err = ''
500
- useEffect(function () {
501
- var alive = true
502
- apiGet(API.config).then(function (d) { if (alive) { cfg = d.config; tick[1]() } }).catch(function (e) { err = e.message; tick[1]() })
503
- return function () { alive = false }
504
- }, [])
505
- if (!cfg) return err ? h('div', { 'data-dam-error': '' }, err) : h(Loading)
506
- function set(key, value) { cfg[key] = value; tick[1]() }
507
- function save() {
508
- if (busy) return
509
- busy = true; msg = ''; err = ''
510
- apiPost(API.config, cfg).then(function (d) { cfg = d.config; msg = '已保存 ✓'; busy = false; tick[1]() })
511
- .catch(function (e) { err = e.message; busy = false; tick[1]() })
512
- }
513
- function field(label, control, hint) {
514
- return h('div', null,
515
- h('div', { 'data-dam-row': '' }, h('label', null, label), control),
516
- hint ? h('div', { 'data-dam-hint': '' }, hint) : null)
517
- }
518
- return h('div', null,
519
- h('div', { 'data-dam-hint': '' }, '记忆存储与行为设置(保存到 DSH 主目录 dsh-auto-memory.json):'),
520
- field('用户记忆目录', h('input', { 'data-dam-input': '', value: cfg.userMemoryDir, onChange: function (e) { set('userMemoryDir', e.target.value) } }), '跨项目规则存放处,支持 ~ 开头;需有文件写权限。'),
521
- field('项目记忆目录', h('input', { 'data-dam-input': '', value: cfg.projectMemoryDir, onChange: function (e) { set('projectMemoryDir', e.target.value) } }), '相对各工作区的目录名(默认 .dsh-memory)。'),
522
- field('注入记忆上下文', h('input', { type: 'checkbox', checked: !!cfg.injectEnabled, onChange: function (e) { set('injectEnabled', e.target.checked) } }), '每次组装提示词时自动注入 <memory_system> 块。'),
523
- field('注入预算(字符)', h('input', { 'data-dam-input': '', type: 'number', min: 400, value: cfg.injectBudgetChars, onChange: function (e) { set('injectBudgetChars', Number(e.target.value) || 2400) } }), '记忆块总预算,超出部分截断。'),
524
- field('注入最近日志天数', h('input', { 'data-dam-input': '', type: 'number', min: 1, max: 14, value: cfg.recentDaysInjected, onChange: function (e) { set('recentDaysInjected', Number(e.target.value) || 3) } }), '会话开始时注入最近 N 天的工作日志尾部。'),
525
- field('每日反思', h('input', { type: 'checkbox', checked: !!cfg.reflectEnabled, onChange: function (e) { set('reflectEnabled', e.target.checked) } }), '昨天有工作日志时,会话首轮主动呈现昨日反思。'),
526
- field('反思风格', h('select', { 'data-dam-select': '', value: cfg.reflectStyle, onChange: function (e) { set('reflectStyle', e.target.value) } },
527
- STYLE_OPTIONS.map(function (o) { return h('option', { key: o[0], value: o[0] }, o[1]) })), '生活化 / 专业性 / 由内容决定。'),
528
- h('div', { 'data-dam-row': '' },
529
- h('button', { 'data-dam-btn': '', onClick: save, disabled: busy }, busy ? '保存中…' : '保存设置'),
530
- msg ? h('span', null, msg) : null),
531
- err ? h('div', { 'data-dam-error': '' }, err) : null)
532
- }
533
-
534
- // ───────────────────────── 插件挂载 ─────────────────────────
535
- function apply(ctx) {
536
- try { ensureStyle() } catch (e) { console.warn('[dsh-auto-memory] style inject failed', e) }
537
- ctx.effect(function () {
538
- return function () {
539
- var tag = document.getElementById(STYLE_ID)
540
- if (tag) tag.remove()
541
- if (closeTimer) { clearTimeout(closeTimer); closeTimer = null }
542
- }
543
- }, 'dsh-auto-memory: styles')
544
-
545
- var slots = ctx.slots
546
- if (!slots) { console.warn('[dsh-auto-memory] slots service unavailable'); return }
547
-
548
- try {
549
- slots.inject('sidebar.footer.action', function () {
550
- return slots.register({ name: 'sidebar.footer.action', id: 'auto-memory', order: 5, label: '记忆' }, function () { return h(SidebarButton) })
551
- })
552
- slots.inject('shell.overlay', function () {
553
- return slots.register({ name: 'shell.overlay', id: 'auto-memory', order: 5 }, function () { return h(MemoryPanel) })
554
- })
555
- slots.inject('settings.section', function () {
556
- return slots.register({ name: 'settings.section', id: 'auto-memory', order: 25, label: '自动记忆' }, function (props) { return h(SettingsPage, { close: props && props.close }) })
557
- })
558
- } catch (e) {
559
- console.warn('[dsh-auto-memory] slot registration failed', e)
560
- }
561
- console.log('[dsh-auto-memory] client ready: sidebar entry + panel + settings page')
562
- }
563
-
564
- exports.inject = ['slots']
565
- exports.apply = apply
566
- return module.exports
567
- },
568
- })