@a9i5k4/dsh-auto-memory 0.1.18 → 0.1.19
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/lib/client.js +88 -2
- package/lib/index.js +81 -1
- package/package.json +1 -1
- package/lib/client.js.bak-20260814 +0 -568
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
|
-
//
|
|
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
|
-
//
|
|
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')
|
|
@@ -1385,6 +1390,70 @@ class MemoryEngine {
|
|
|
1385
1390
|
return result
|
|
1386
1391
|
}
|
|
1387
1392
|
|
|
1393
|
+
// ---------- 时间检测(每 15s 心跳 tick):暂离状态 / _lastAgent 定时兜底 / 自动总结时间点 ----------
|
|
1394
|
+
tickTime() {
|
|
1395
|
+
try {
|
|
1396
|
+
// 1) 暂离检测:距上次活动(lastActive)超过 awayMinutes 视为暂离
|
|
1397
|
+
const awayMs = Math.max(Number(this.config.awayMinutes) || 60, 1) * 60000
|
|
1398
|
+
this.state.away = !!(this._lastActiveAt && Date.now() - this._lastActiveAt > awayMs)
|
|
1399
|
+
// 2) _lastAgent 定时兜底(重启恢复会话不触发 session-start;pre-step 兜底之外的第二保险)
|
|
1400
|
+
if (!this._lastAgent) this.restoreLastAgent()
|
|
1401
|
+
// 3) 自动总结时间点:命中配置的 HH:MM 且当天未生成过 → 生成时段总结并标记展示
|
|
1402
|
+
const times = Array.isArray(this.config.autoSummaryTimes) ? this.config.autoSummaryTimes : []
|
|
1403
|
+
if (times.length) {
|
|
1404
|
+
const now = new Date()
|
|
1405
|
+
const hhmm = String(now.getHours()).padStart(2, '0') + ':' + String(now.getMinutes()).padStart(2, '0')
|
|
1406
|
+
const today = this.memToday()
|
|
1407
|
+
for (const t of times) {
|
|
1408
|
+
if (hhmm === t) {
|
|
1409
|
+
const key = today + '|' + t
|
|
1410
|
+
if (this._summaryDone !== key) {
|
|
1411
|
+
this._summaryDone = key
|
|
1412
|
+
void this.triggerAutoSummary(t)
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
} catch (e) {}
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
/** 从 sessions/agent 服务恢复 _lastAgent(定时兜底)。 */
|
|
1421
|
+
restoreLastAgent() {
|
|
1422
|
+
try {
|
|
1423
|
+
const sessionsSvc = this._sessionsSvc
|
|
1424
|
+
const agentSvc = this._agentSvc
|
|
1425
|
+
if (!sessionsSvc || !agentSvc || typeof sessionsSvc.list !== 'function') return
|
|
1426
|
+
const sessions = sessionsSvc.list()
|
|
1427
|
+
if (!sessions || !sessions.length) return
|
|
1428
|
+
// 找最近活跃的顶层会话(log 最后事件时间最大)
|
|
1429
|
+
let best = null, bestTime = 0
|
|
1430
|
+
for (const s of sessions) {
|
|
1431
|
+
let t = 0
|
|
1432
|
+
try { const last = s.log && s.log[s.log.length - 1]; if (last && last.time) t = last.time } catch (e) {}
|
|
1433
|
+
if (t >= bestTime) { bestTime = t; best = s }
|
|
1434
|
+
}
|
|
1435
|
+
if (!best || typeof agentSvc.get !== 'function') return
|
|
1436
|
+
const a = agentSvc.get(best.id)
|
|
1437
|
+
if (a && a.session && a.session.header && a.session.header.parentSession === undefined) this._lastAgent = a
|
|
1438
|
+
} catch (e) {}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/** 自动总结:按时间点推断时段,生成总结并置 pendingSummary(供 client 弹窗)。 */
|
|
1442
|
+
async triggerAutoSummary(timePoint) {
|
|
1443
|
+
try {
|
|
1444
|
+
const h = Number(String(timePoint).slice(0, 2)) || 0
|
|
1445
|
+
const period = h < 6 ? 'morning' : h < 9 ? 'morning' : h < 12 ? 'forenoon' : h < 14 ? 'noon' : h < 18 ? 'afternoon' : 'evening'
|
|
1446
|
+
const out = await this.summarizePeriod(period, undefined, false)
|
|
1447
|
+
if (out && out.summary) {
|
|
1448
|
+
this.state.pendingSummary = {
|
|
1449
|
+
time: timePoint, date: this.memToday(), period,
|
|
1450
|
+
summary: out.summary, works: out.works || [], generatedAt: out.generatedAt || Date.now(),
|
|
1451
|
+
}
|
|
1452
|
+
console.log('[dsh-auto-memory] auto summary triggered at ' + timePoint + ' (period ' + period + ')')
|
|
1453
|
+
}
|
|
1454
|
+
} catch (e) {}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1388
1457
|
/** 今日 AI 问候(按时段,每天每时段生成一次并缓存到 greetings/{date}.json)。 */
|
|
1389
1458
|
async greetToday(agent) {
|
|
1390
1459
|
const hour = new Date().getHours()
|
|
@@ -1816,6 +1885,11 @@ class MemoryEngine {
|
|
|
1816
1885
|
todayEntries,
|
|
1817
1886
|
latestReflectionDate: this.state.latestReflectionDate,
|
|
1818
1887
|
pendingReflection: this.state.pendingReflection ? this.state.pendingReflection.date : undefined,
|
|
1888
|
+
// 时间检测:暂离状态 / 待展示的自动总结 / 相关配置
|
|
1889
|
+
away: !!this.state.away,
|
|
1890
|
+
awayMinutes: Math.max(Number(this.config.awayMinutes) || 60, 1),
|
|
1891
|
+
autoSummaryTimes: Array.isArray(this.config.autoSummaryTimes) ? this.config.autoSummaryTimes : [],
|
|
1892
|
+
pendingSummary: this.state.pendingSummary || null,
|
|
1819
1893
|
autoStats: this.autoStats,
|
|
1820
1894
|
greeting: this.greetingData(),
|
|
1821
1895
|
calendar: this.parseCalendar(this.state.calendarText),
|
|
@@ -2205,6 +2279,9 @@ export function apply(ctx, config) {
|
|
|
2205
2279
|
const sessionQuery = ctx.get('sessionQuery')
|
|
2206
2280
|
engine._sessionQuery = sessionQuery
|
|
2207
2281
|
engine._subagents = ctx.get('subagents') || undefined
|
|
2282
|
+
// 时间检测用的服务(软获取,缺失时定时兜底自动降级)
|
|
2283
|
+
engine._sessionsSvc = ctx.get('sessions') || undefined
|
|
2284
|
+
engine._agentSvc = ctx.get('agent') || undefined
|
|
2208
2285
|
|
|
2209
2286
|
// 生命周期刷新
|
|
2210
2287
|
const refreshAll = (agent) => { void engine.refresh(agent) }
|
|
@@ -2237,6 +2314,9 @@ export function apply(ctx, config) {
|
|
|
2237
2314
|
ctx.on('agent/pre-step', async (payload, next) => {
|
|
2238
2315
|
try {
|
|
2239
2316
|
const agent = (payload && payload.agent) || engine._lastAgent
|
|
2317
|
+
// 兜底恢复 _lastAgent(仅启动后首次为空时设置一次,不每步覆盖):重启后恢复的会话不触发 session-start,
|
|
2318
|
+
// 导致 turn-stopping 自动沉淀拿不到 agent 而永久失效;首次 pre-step 即恢复,之后由 session-start 正常维护
|
|
2319
|
+
try { if (!engine._lastAgent && agent && agent.session && agent.session.header && agent.session.header.parentSession === undefined) engine._lastAgent = agent } catch (e) {}
|
|
2240
2320
|
// 只刷新顶层会话:子代理(自动沉淀/固化的 subagent)session 无 cwd,刷新会把 state 切到错误工作区
|
|
2241
2321
|
let skip = false
|
|
2242
2322
|
try { if (agent && agent.session && agent.session.header && agent.session.header.parentSession) skip = true } catch (e) {}
|
|
@@ -2786,7 +2866,7 @@ export function apply(ctx, config) {
|
|
|
2786
2866
|
})()
|
|
2787
2867
|
}, 5 * 60 * 1000)
|
|
2788
2868
|
// 心跳独立 15 秒一次(对齐 polling-lease 心跳节奏),证明轮询机制活着
|
|
2789
|
-
const heartbeatTimer = setInterval(() => { void writeHeartbeat() }, 15000)
|
|
2869
|
+
const heartbeatTimer = setInterval(() => { void writeHeartbeat(); try { engine.tickTime() } catch (e) {} }, 15000)
|
|
2790
2870
|
void writeHeartbeat() // 立即心跳一次:重启后马上可见轮询存活
|
|
2791
2871
|
|
|
2792
2872
|
// ---------- 注册与清理 ----------
|
package/package.json
CHANGED
|
@@ -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
|
-
})
|