@a9i5k4/dsh-auto-memory 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # dsh-auto-memory — DSH Auto Memory Plugin / DSH 自动记忆插件
2
2
 
3
- An auto-memory plugin for the DeepSeek Harness Web GUI: three-layer memory (user-level / project notes / daily logs) with automatic injection and retrieval, daily reflections, a visual panel and settings page, and inheritance of memories from other AI tools.
3
+ An auto-memory plugin for the DeepSeek Harness Web GUI: three-layer memory (user-level / project notes / daily logs) with automatic injection and retrieval, daily reflections, smart period greetings, a calendar view and settings page, and inheritance of memories from other AI tools.
4
4
 
5
- DSH Web GUI 的记忆插件:三层记忆(用户级 / 项目笔记 / 每日日志)自动注入与检索、每日反思、可视化面板与设置页,支持继承其他 AI 工具的历史记忆。
5
+ DSH Web GUI 的记忆插件:三层记忆自动注入与检索、每日反思、智能时段问候、日历视图与设置页,支持继承其他 AI 工具的历史记忆。
6
6
 
7
7
  [**English**](README.md) | [中文版](README.zh-CN.md)
8
8
 
@@ -10,18 +10,45 @@ DSH Web GUI 的记忆插件:三层记忆(用户级 / 项目笔记 / 每日
10
10
 
11
11
  ## Features
12
12
 
13
+ ### Three-layer Memory
14
+
13
15
  | Layer | Location | Description |
14
16
  |---|---|---|
15
17
  | User-level memory | `~/.dsh/memory/MEMORY.md` | Cross-project rules & preferences |
16
18
  | Project notes | `{workspace}/.dsh-memory/MEMORY.md` | Project conventions & decisions |
17
19
  | Daily logs | `{workspace}/.dsh-memory/YYYY-MM-DD.md` | Append-only work log |
18
- | Reflections | `{workspace}/.dsh-memory/reflections/YYYY-MM-DD.md` | Daily reflection, auto-prompted |
20
+ | Reflections | `{workspace}/.dsh-memory/reflections/YYYY-MM-DD.md` | Daily reflection (structured, kept in background) |
21
+
22
+ - **Auto injection**: injects a `<memory_system>` block into every system prompt (user rules + project notes + recent reflections + recent N days of log tails + pending calendar items + writing discipline)
23
+ - **Visible memory ops**: when the AI updates or searches memory, it says so in plain text in the chat reply (e.g. "Logged X to today's journal", "I checked memory and found..."), not hidden inside tool calls
24
+
25
+ ### Smart Period Greetings (Overview page)
26
+
27
+ The first thing you see when opening the memory panel is a friendly period greeting, not technical info:
28
+
29
+ - **Multiple times a day**: morning shows "yesterday summary + early morning", forenoon shows the morning's work, noon/afternoon/evening follow suit
30
+ - **Smart timing**: if you were away for more than 1 hour (off work / stepped away) and come back, it automatically shows "Welcome back" with what was finished meanwhile
31
+ - **Drawer layout**: the outer layer stays casual and greeting-like; each period is a drawer — tap to expand the professional details
32
+ - **Daily reflection stays in background**: structured reflections (results / lessons / next steps) are kept, while the front page only shows a light greeting
33
+
34
+ ### Calendar View (Four Quadrants)
35
+
36
+ New 「Calendar」tab (liquid-glass monthly view):
37
+
38
+ - Monthly grid, today highlighted, click any date to add an item
39
+ - **Four-quadrant colors**: Urgent & Important (red) / Important (blue) / Urgent (orange) / Neither (gray)
40
+ - Click an item to toggle done, click again to delete; legend + weekday header
41
+ - **Cross-conversation persistence**: data lives at user level `~/.dsh/memory/CALENDAR.md`, shared across workspaces, survives DSH reinstall
42
+ - **AI-maintained**: the AI extracts deadlines and appointments from conversations and writes them to the calendar automatically (`calendar_add` / `calendar_list` / `calendar_done` / `calendar_remove`), restating it in plain text; pending items are injected into every session's system prompt
43
+
44
+ ### Agent Tools
45
+
46
+ `memory_log` / `memory_note` / `memory_user` / `memory_recall` / `memory_external` / `memory_maintain` / `memory_status` / `memory_reflect` / `calendar_add` / `calendar_list` / `calendar_done` / `calendar_remove`
47
+
48
+ ### UI
19
49
 
20
- - **Auto injection**: injects a `<memory_system>` block into every system prompt (user rules + project notes + recent reflections + recent N days of log tails + writing discipline)
21
- - **Daily reflection**: when yesterday has logs but no reflection, the agent presents one at session start
22
- - **Agent tools**: `memory_log` / `memory_note` / `memory_user` / `memory_recall` / `memory_external` / `memory_maintain` / `memory_status` / `memory_reflect`
23
- - **GUI**: sidebar 「Memory」entry → floating panel (Overview / Logs / Notes / Reflections / Connect / Search); Settings page (Settings → Auto Memory)
24
- - **UI language**: switch between 中文 / English in Settings → Auto Memory → UI language
50
+ - Sidebar 「Memory」entry floating panel (Overview / Logs / Notes / Reflections / Connect / Calendar / Search)
51
+ - Settings page (Settings Auto Memory): storage paths, injection budget, reflection style, **UI language (中文 / English)**
25
52
  - **External memory inheritance**: import memories accumulated by other AI tools (CodeBuddy / Claude Code / Codex / project convention files)
26
53
 
27
54
  ---
@@ -86,7 +113,7 @@ Adjustable in the GUI (Settings → Auto Memory), including the UI language (zh
86
113
  ## Structure
87
114
 
88
115
  - `lib/index.js` — Host half: engine, injection, tools, routes (zero runtime deps, Node built-ins only)
89
- - `lib/client.js` — Browser half: memory panel + settings page (built-in zh/en i18n)
116
+ - `lib/client.js` — Browser half: memory panel (with calendar view) + settings page (built-in zh/en i18n)
90
117
  - `cordis.patch.yml` — Plugin row (`auto-memory`)
91
118
 
92
119
  ---
package/README.zh-CN.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # dsh-auto-memory — DSH 自动记忆插件
2
2
 
3
- DSH Web GUI 的记忆插件:三层记忆(用户级 / 项目笔记 / 每日日志)自动注入与检索、每日反思、可视化面板与设置页,支持继承其他 AI 工具的历史记忆。
3
+ DSH Web GUI 的记忆插件:三层记忆(用户级 / 项目笔记 / 每日日志)自动注入与检索、每日反思、智能时段问候、日历视图与设置页,支持继承其他 AI 工具的历史记忆。
4
4
 
5
5
  [English](README.md) | [中文版](README.zh-CN.md)
6
6
 
@@ -8,18 +8,45 @@ DSH Web GUI 的记忆插件:三层记忆(用户级 / 项目笔记 / 每日
8
8
 
9
9
  ## 功能
10
10
 
11
+ ### 三层记忆
12
+
11
13
  | 层 | 位置 | 说明 |
12
14
  |---|---|---|
13
15
  | 用户级记忆 | `~/.dsh/memory/MEMORY.md` | 跨项目规则/偏好(用户明确要求时写) |
14
16
  | 项目笔记 | `{工作区}/.dsh-memory/MEMORY.md` | 项目长期约定、决策、架构要点 |
15
17
  | 每日日志 | `{工作区}/.dsh-memory/YYYY-MM-DD.md` | append-only 工作日志 |
16
- | 反思 | `{工作区}/.dsh-memory/reflections/YYYY-MM-DD.md` | 每日反思(自动提示生成) |
18
+ | 反思 | `{工作区}/.dsh-memory/reflections/YYYY-MM-DD.md` | 每日反思(后台结构化积累) |
19
+
20
+ - **自动注入**:每次组装系统提示词时注入 `<memory_system>` 块(用户规则 + 项目笔记 + 最近反思 + 最近 N 天日志尾部 + 未完成日历事项 + 写入纪律)
21
+ - **记忆操作可见**:更新/检索记忆时,AI 会在对话正文中明文说明(如"已把 X 记入今日日志""我查了记忆,发现…"),不藏在工具调用里
22
+
23
+ ### 智能时段问候(概览页)
24
+
25
+ 打开记忆面板第一眼看到的是生活化的时段问候,而不是严肃的技术信息:
26
+
27
+ - **一天多次**:早晨显示"昨天摘要 + 今天早晨"、上午显示上午的工作、中午/下午/晚上依次类推
28
+ - **智能时机**:离开超过 1 小时(下班/暂离)再打开,自动显示"欢迎回来"并列出期间的完成事项
29
+ - **抽屉式展开**:外层保持生活化问候风格;每个时段做成抽屉,点开才显示专业的工作明细
30
+ - **每日反思**:后台保留结构化反思(成果/教训/要点),前台只有轻松问候
31
+
32
+ ### 日历视图(四象限)
33
+
34
+ 新增「日历」页签(液态玻璃风格月视图):
35
+
36
+ - 月视图网格,今日高亮,点击任意日期添加事项
37
+ - **四象限色标**:重要紧急(红)/ 重要不紧急(蓝)/ 紧急不重要(橙)/ 不重要不紧急(灰)
38
+ - 点条目切换完成状态,再点删除;图例 + 星期头
39
+ - **跨对话持久**:数据存用户级 `~/.dsh/memory/CALENDAR.md`,所有工作区共享,重装 DSH 不丢
40
+ - **AI 主动维护**:AI 会从对话中提取 deadline、约定时间等自动写入日历(`calendar_add` / `calendar_list` / `calendar_done` / `calendar_remove`),并在正文转述;未完成事项注入每次会话的系统提示词
41
+
42
+ ### Agent 工具
43
+
44
+ `memory_log` / `memory_note` / `memory_user` / `memory_recall` / `memory_external` / `memory_maintain` / `memory_status` / `memory_reflect` / `calendar_add` / `calendar_list` / `calendar_done` / `calendar_remove`
45
+
46
+ ### 界面
17
47
 
18
- - **自动注入**:每次组装系统提示词时注入 `<memory_system>` 块(用户规则 + 项目笔记 + 最近反思 + 最近 N 天日志尾部 + 写入纪律)
19
- - **每日反思**:检测到"昨天有日志但未生成反思"时,会话首轮自动请求 agent 生成昨日反思
20
- - **Agent 工具**:`memory_log` / `memory_note` / `memory_user` / `memory_recall` / `memory_external` / `memory_maintain` / `memory_status` / `memory_reflect`
21
- - **GUI**:侧边栏「记忆」入口 → 浮层面板(概览/日志/笔记/反思/接续/检索);设置页(设置 → 自动记忆)
22
- - **界面语言**:中文 / English 可切换(设置 → 自动记忆 → 界面语言)
48
+ - 侧边栏「记忆」入口 浮层面板(概览/日志/笔记/反思/接续/日历/检索)
49
+ - 设置页(设置 自动记忆):存储位置、注入预算、反思风格、**界面语言(中文 / English)**
23
50
  - **外部记忆继承**:接入其他 AI 工具(CodeBuddy / Claude Code / Codex / 项目约定文件)积累的记忆
24
51
 
25
52
  ---
@@ -84,7 +111,7 @@ pnpm add @a9i5k4/dsh-auto-memory
84
111
  ## 结构
85
112
 
86
113
  - `lib/index.js` — Host 半:引擎、注入、工具、路由(零运行时依赖,仅 node 内置模块)
87
- - `lib/client.js` — 浏览器半:记忆面板 + 设置页(内置中英双语)
114
+ - `lib/client.js` — 浏览器半:记忆面板(含日历视图)+ 设置页(内置中英双语)
88
115
  - `cordis.patch.yml` — 插件行(`auto-memory`)
89
116
 
90
117
  ---
package/lib/client.js CHANGED
@@ -130,7 +130,8 @@ window.__ModuleLoader__.load({
130
130
  connectHint: '把其他 AI 工具(CodeBuddy / Claude Code / Codex / 项目约定文件等)积累的记忆接入当前 DSH 工作。接入后内容写入本地记忆并自动标注来源,后续会随会话自动注入。',
131
131
  importAll: '一键接入全部', rescan: '重新扫描',
132
132
  styleAuto: '由内容决定', styleLife: '生活化', styleProfessional: '专业性',
133
- todayGreetingTitle: '今日问候',
133
+ todayGreetingTitle: '问候',
134
+ yesterdayTimeline: '昨天(', pendingReflectionShort: '昨天的工作还没复盘(',
134
135
  saved: '已保存',
135
136
  settingsHeader: '记忆存储与行为设置(保存到 DSH 主目录 dsh-auto-memory.json):',
136
137
  fUserDir: '用户记忆目录', fUserDirHint: '跨项目规则存放处,支持 ~ 开头;需有文件写权限。',
@@ -143,6 +144,14 @@ window.__ModuleLoader__.load({
143
144
  fLocale: '界面语言', fLocaleHint: '切换插件面板与设置页的显示语言。',
144
145
  saveSettings: '保存设置',
145
146
  zh: '中文', en: 'English',
147
+ calendar: '日历', addItem: '添加', save: '保存', cancel: '取消', needTitle: '请填写事项标题', itemTitle: '事项标题…',
148
+ segMorning: '早晨', segForenoon: '上午', segNoon: '中午', segAfternoon: '下午', segEvening: '晚上',
149
+ segPrefix: '今日', segMorningHint: '(昨日摘要)',
150
+ welcomeBack: '欢迎回来!这段时间你完成了这些工作:',
151
+ yesterdayDrawer: '昨天',
152
+ greetMorning: '新的一天开始啦,先看看昨天做了什么。', greetForenoon: '上午好,今天也在稳步推进。', greetNoon: '中午好,歇口气再继续。', greetAfternoon: '下午好,下午也要元气满满。', greetEvening: '晚上好,辛苦一天了。',
153
+ greetSummary: '今天已经完成了 ', greetThings: ' 件工作,点开看看:',
154
+ qUrgentImportant: '重要紧急', qImportant: '重要不紧急', qUrgent: '紧急不重要', qNone: '不重要不紧急', qUncategorized: '未分类',
146
155
  },
147
156
  en: {
148
157
  loading: 'Loading…',
@@ -182,7 +191,8 @@ window.__ModuleLoader__.load({
182
191
  connectHint: 'Import memories accumulated by other AI tools (CodeBuddy / Claude Code / Codex / project convention files) into the current DSH workspace. Imported content is written to local memory with its source noted, and is auto-injected in future sessions.',
183
192
  importAll: 'Import all', rescan: 'Rescan',
184
193
  styleAuto: 'Auto', styleLife: 'Life-style', styleProfessional: 'Professional',
185
- todayGreetingTitle: 'Today\'s Greeting',
194
+ todayGreetingTitle: 'Greeting',
195
+ yesterdayTimeline: 'Yesterday (', pendingReflectionShort: 'Yesterday\'s work not reviewed (',
186
196
  saved: 'Saved',
187
197
  settingsHeader: 'Memory storage & behavior (saved to ~/.dsh/dsh-auto-memory.json):',
188
198
  fUserDir: 'User memory dir', fUserDirHint: 'Cross-project rules; supports ~ prefix; needs write permission.',
@@ -195,6 +205,14 @@ window.__ModuleLoader__.load({
195
205
  fLocale: 'UI language', fLocaleHint: 'Switch the display language of the panel and settings page.',
196
206
  saveSettings: 'Save settings',
197
207
  zh: '中文', en: 'English',
208
+ calendar: 'Calendar', addItem: 'Add', save: 'Save', cancel: 'Cancel', needTitle: 'Title is required', itemTitle: 'Item title…',
209
+ segMorning: 'Morning', segForenoon: 'Forenoon', segNoon: 'Noon', segAfternoon: 'Afternoon', segEvening: 'Evening',
210
+ segPrefix: 'Today ', segMorningHint: ' (yesterday summary)',
211
+ welcomeBack: 'Welcome back! While you were away, you finished:',
212
+ yesterdayDrawer: 'Yesterday',
213
+ greetMorning: 'A fresh day! Let us look back at yesterday first.', greetForenoon: 'Good morning, steady progress today.', greetNoon: 'Good noon, take a break and keep going.', greetAfternoon: 'Good afternoon, keep up the good energy.', greetEvening: 'Good evening, well done today.',
214
+ greetSummary: 'You have completed ', greetThings: ' things today. Tap to expand:',
215
+ qUrgentImportant: 'Urgent & Important', qImportant: 'Important', qUrgent: 'Urgent', qNone: 'Neither', qUncategorized: 'Uncategorized',
198
216
  }
199
217
  }
200
218
  var locale = 'zh'
@@ -334,6 +352,80 @@ window.__ModuleLoader__.load({
334
352
  return n + ' B'
335
353
  }
336
354
 
355
+ function GreetingCard(props) {
356
+ var g = props.greeting
357
+ var ps = props.periodSummary
358
+ if (!g) return null
359
+ var openPair = useState({})
360
+ var openMap = openPair[0]
361
+ var setOpenMap = openPair[1]
362
+ // 智能时段判定
363
+ var hour = new Date().getHours()
364
+ var seg = hour < 9 ? 'morning' : hour < 12 ? 'forenoon' : hour < 14 ? 'noon' : hour < 18 ? 'afternoon' : 'evening'
365
+ var segLabel = { morning: t('segMorning'), forenoon: t('segForenoon'), noon: t('segNoon'), afternoon: t('segAfternoon'), evening: t('segEvening') }[seg]
366
+ // 收集抽屉: {key, label, hint, items, withTime}
367
+ var drawers = []
368
+ var curSegName = { morning: '早晨', forenoon: '上午', noon: '中午', afternoon: '下午', evening: '晚上' }[seg]
369
+ // 昨天抽屉(早晨显示)
370
+ if (seg === 'morning' && g.entries.length) {
371
+ drawers.push({ key: 'yesterday', label: t('yesterdayDrawer') + (g.yesterdayDate || ''), hint: '', items: g.entries, withTime: true, defaultOpen: true })
372
+ }
373
+ // 今天各时段抽屉(已过的时段)
374
+ var seenSeg = { '早晨': seg !== 'morning', '上午': (seg === 'forenoon' || seg === 'noon' || seg === 'afternoon' || seg === 'evening'), '中午': (seg === 'noon' || seg === 'afternoon' || seg === 'evening'), '下午': (seg === 'afternoon' || seg === 'evening'), '晚上': seg === 'evening' }
375
+ var segOrder = ['早晨', '上午', '中午', '下午', '晚上']
376
+ for (var si = 0; si < segOrder.length; si++) {
377
+ var sname = segOrder[si]
378
+ if (!seenSeg[sname]) continue
379
+ var items = (ps && ps.groups && ps.groups[sname]) || []
380
+ if (!items.length) continue
381
+ drawers.push({ key: 'seg-' + sname, label: t('segPrefix') + sname, hint: '', items: items.map(function (x) { return { time: '', text: x } }), withTime: false, defaultOpen: sname === curSegName })
382
+ }
383
+ // 外层:生活化问候
384
+ var totalCount = 0
385
+ for (var d0 = 0; d0 < drawers.length; d0++) totalCount += drawers[d0].items.length
386
+ var greetLine = g.greeting && String(g.greeting).trim()
387
+ ? String(g.greeting).trim()
388
+ : (function () {
389
+ if (away()) return t('welcomeBack')
390
+ var base = { morning: t('greetMorning'), forenoon: t('greetForenoon'), noon: t('greetNoon'), afternoon: t('greetAfternoon'), evening: t('greetEvening') }[seg]
391
+ if (totalCount > 0) base += t('greetSummary') + totalCount + t('greetThings')
392
+ return base
393
+ })()
394
+ // 离开>1小时后回来
395
+ function away() {
396
+ var lastSeen = 0
397
+ try { lastSeen = Number(localStorage.getItem('dsh-auto-memory.lastActive') || 0) } catch (e) {}
398
+ return lastSeen > 0 && (Date.now() - lastSeen) > 3600000
399
+ }
400
+ var rows = []
401
+ rows.push(h('div', { 'data-dam-content': '' }, greetLine))
402
+ // 抽屉列表
403
+ for (var d = 0; d < drawers.length; d++) {
404
+ (function (dr) {
405
+ var isOpen = openMap[dr.key] !== undefined ? openMap[dr.key] : dr.defaultOpen
406
+ rows.push(h('div', { key: dr.key, style: { marginTop: '8px' } },
407
+ h('button', {
408
+ 'data-dam-btn': '',
409
+ onClick: function () { var n = Object.assign({}, openMap); n[dr.key] = !isOpen; setOpenMap(n) },
410
+ style: { width: '100%', display: 'flex', justifyContent: 'space-between', alignItems: 'center', padding: '5px 8px', borderRadius: '7px', background: 'color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.08)) 45%, transparent)', border: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.3)) 50%, transparent)' },
411
+ },
412
+ h('span', { style: { fontWeight: 600, fontSize: '12.5px' } }, (isOpen ? '▼ ' : '▶ ') + dr.label),
413
+ h('span', { 'data-dam-muted': '', style: { fontSize: '11.5px' } }, dr.items.length + t('logEntries'))),
414
+ isOpen ? h('div', { style: { padding: '6px 4px 2px 10px' } },
415
+ dr.items.map(function (en) {
416
+ return h('div', { key: (en.time || '') + en.text, style: { marginBottom: '3px', fontSize: '12.5px', lineHeight: 1.5 } },
417
+ dr.withTime && en.time ? h('span', { 'data-dam-muted': '', style: { fontFamily: 'monospace', fontSize: '11px', marginRight: '6px' } }, en.time) : null,
418
+ h('span', { style: { wordBreak: 'break-word' } }, en.text))
419
+ })) : null))
420
+ })(drawers[d])
421
+ }
422
+ // 待反思提醒
423
+ if (g.pendingReflectionDate) {
424
+ rows.push(h('div', { 'data-dam-hint': '', style: { marginTop: '8px' } }, t('pendingReflectionShort') + g.pendingReflectionDate))
425
+ }
426
+ return h(Card, { title: (g.period || '') + ' · ' + segLabel }, rows)
427
+ }
428
+
337
429
  function OverviewTab() {
338
430
  var statePair = useState(null)
339
431
  var state = statePair[0]
@@ -352,6 +444,10 @@ window.__ModuleLoader__.load({
352
444
  apiGet(API.state).then(function (s) { if (alive) setState(s) }).catch(function () {})
353
445
  return function () { alive = false }
354
446
  }, [])
447
+ useEffect(function () {
448
+ // 记录本次活动时间;下次打开若相隔>1小时,GreetingCard 显示"欢迎回来"
449
+ try { localStorage.setItem('dsh-auto-memory.lastActive', String(Date.now())) } catch (e) {}
450
+ }, [])
355
451
  if (!state) return h(Loading)
356
452
  function oneClickReflect() {
357
453
  if (reflectBusy) return
@@ -362,10 +458,8 @@ window.__ModuleLoader__.load({
362
458
  }).catch(function (e) { setActMsg(t('failed') + e.message); setReflectBusy(false) })
363
459
  }
364
460
  return h('div', null,
365
- // 今日拟人化问候(每天首次生成后常驻显示)
366
- state.todayGreeting
367
- ? h(Card, { title: t('todayGreetingTitle') }, h('div', { 'data-dam-content': '' }, state.todayGreeting))
368
- : null,
461
+ // 今日问候卡:问候语 + 昨天时间轴 + 提醒(纯 GUI 渲染,不干扰对话流)
462
+ h(GreetingCard, { greeting: state.greeting, periodSummary: state.periodSummary, t: t }),
369
463
  state.pendingReflection
370
464
  ? h(Banner, null, t('pendingReflection') + state.pendingReflection + t('pendingReflectionHint'))
371
465
  : null,
@@ -549,6 +643,148 @@ window.__ModuleLoader__.load({
549
643
  result ? h(Card, { title: t('resultTitle') }, h('div', { 'data-dam-content': '' }, result)) : null)
550
644
  }
551
645
 
646
+ // ───────────────────────── 日历页签 ─────────────────────────
647
+ var QUADRANT_STYLE = {
648
+ '重要紧急': { color: 'var(--dsw-alias-state-error-primary, #d64545)', label: t('qUrgentImportant') },
649
+ '重要不紧急': { color: 'var(--dsw-alias-brand-primary, #4f7cff)', label: t('qImportant') },
650
+ '紧急不重要': { color: 'var(--dsw-alias-state-warn-primary, #e6a23c)', label: t('qUrgent') },
651
+ '不重要不紧急': { color: 'var(--dsw-alias-label-secondary, #8a94a6)', label: t('qNone') },
652
+ '未分类': { color: 'var(--dsw-alias-label-secondary, #8a94a6)', label: t('qUncategorized') },
653
+ }
654
+ function CalendarTab() {
655
+ var dataPair = useState(null)
656
+ var data = dataPair[0]
657
+ var setData = dataPair[1]
658
+ var monthPair = useState(null)
659
+ var month = monthPair[0] // {year, mon} 1-12
660
+ var setMonth = monthPair[1]
661
+ var draftPair = useState(null)
662
+ var draft = draftPair[0] // {date, time, quadrant, title}
663
+ var setDraft = draftPair[1]
664
+ var msgPair = useState('')
665
+ var msg = msgPair[0]
666
+ var setMsg = msgPair[1]
667
+ function load() {
668
+ apiGet(API.calendar).then(function (d) { if (d) setData(d) }).catch(function () {})
669
+ }
670
+ useEffect(function () {
671
+ load()
672
+ var now = new Date()
673
+ setMonth({ year: now.getFullYear(), mon: now.getMonth() + 1 })
674
+ }, [])
675
+ if (!data || !month) return h(Loading)
676
+ function dayEntries(date) {
677
+ return (data.entries || []).filter(function (en) { return en.date === date })
678
+ }
679
+ function moveMonth(delta) {
680
+ var y = month.year, m = month.mon + delta
681
+ if (m < 1) { m = 12; y-- }
682
+ if (m > 12) { m = 1; y++ }
683
+ setMonth({ year: y, mon: m })
684
+ }
685
+ function fmtDate(y, m, d) { return y + '-' + String(m).padStart(2, '0') + '-' + String(d).padStart(2, '0') }
686
+ // 月网格
687
+ var firstDay = new Date(month.year, month.mon - 1, 1)
688
+ var startDow = firstDay.getDay()
689
+ var daysInMonth = new Date(month.year, month.mon, 0).getDate()
690
+ var cells = []
691
+ var today = fmtDate(new Date().getFullYear(), new Date().getMonth() + 1, new Date().getDate())
692
+ for (var i = 0; i < startDow; i++) cells.push(null)
693
+ for (var d = 1; d <= daysInMonth; d++) cells.push(fmtDate(month.year, month.mon, d))
694
+ var dowLabels = locale === 'zh' ? ['日', '一', '二', '三', '四', '五', '六'] : ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa']
695
+ var rows = []
696
+ // 顶部:月份切换 + 添加按钮
697
+ rows.push(h('div', { 'data-dam-row': '' },
698
+ h('button', { 'data-dam-btn': '', onClick: function () { moveMonth(-1) } }, '◀'),
699
+ h('span', { style: { fontWeight: 700, flex: 1, textAlign: 'center' } }, month.year + ' / ' + String(month.mon).padStart(2, '0')),
700
+ h('button', { 'data-dam-btn': '', onClick: function () { moveMonth(1) } }, '▶'),
701
+ h('button', { 'data-dam-btn': '', onClick: function () { setDraft({ date: today, time: '09:00', quadrant: '重要不紧急', title: '' }) } }, '+ ' + t('addItem'))))
702
+ // 图例
703
+ rows.push(h('div', { 'data-dam-row': '', style: { flexWrap: 'wrap' } },
704
+ Object.keys(QUADRANT_STYLE).map(function (q) {
705
+ return h('span', { key: q, style: { fontSize: '11px', opacity: 0.8, marginRight: '10px' } },
706
+ h('span', { style: { display: 'inline-block', width: 8, height: 8, borderRadius: 2, background: QUADRANT_STYLE[q].color, marginRight: 4 } }), QUADRANT_STYLE[q].label)
707
+ })))
708
+ // 星期表头
709
+ rows.push(h('div', { style: { display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '3px', marginBottom: '4px' } },
710
+ dowLabels.map(function (dl) { return h('div', { key: dl, style: { textAlign: 'center', fontSize: '11px', opacity: 0.55 } }, dl) })))
711
+ // 日历格子
712
+ var gridRows = []
713
+ for (var ci = 0; ci < cells.length; ci += 7) {
714
+ var week = []
715
+ for (var cj = 0; cj < 7; cj++) {
716
+ (function (cell) {
717
+ if (!cell) { week.push(h('div', { key: 'empty' + cj })); return }
718
+ var es = dayEntries(cell)
719
+ var isToday = cell === today
720
+ var dayNum = Number(cell.slice(8))
721
+ week.push(h('div', {
722
+ key: cell,
723
+ onClick: function () { setDraft({ date: cell, time: '09:00', quadrant: '重要不紧急', title: '' }) },
724
+ style: {
725
+ minHeight: '64px', padding: '4px', cursor: 'pointer', borderRadius: '8px',
726
+ border: '1px solid ' + (isToday ? 'var(--dsw-alias-brand-primary, #4f7cff)' : 'color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.3)) 55%, transparent)'),
727
+ background: isToday ? 'color-mix(in srgb, var(--dsw-alias-brand-primary, #4f7cff) 10%, transparent)' : 'color-mix(in srgb, var(--dsw-alias-bg-layer-1, rgba(128,128,128,.06)) 40%, transparent)',
728
+ overflow: 'hidden',
729
+ },
730
+ },
731
+ h('div', { style: { fontSize: '11.5px', fontWeight: isToday ? 700 : 500, opacity: isToday ? 1 : 0.7, marginBottom: '3px' } }, dayNum),
732
+ es.slice(0, 3).map(function (en) {
733
+ var qs = QUADRANT_STYLE[en.quadrant] || QUADRANT_STYLE['未分类']
734
+ return h('div', {
735
+ key: en.time + en.title,
736
+ title: en.time + ' ' + en.title,
737
+ onClick: function (ev) { ev.stopPropagation(); toggleDone(en) },
738
+ style: {
739
+ fontSize: '10.5px', lineHeight: 1.35, padding: '1px 4px', borderRadius: 4, marginBottom: 2,
740
+ background: 'color-mix(in srgb, ' + qs.color + ' 18%, transparent)',
741
+ color: qs.color, textDecoration: en.done ? 'line-through' : 'none',
742
+ opacity: en.done ? 0.5 : 1, cursor: 'pointer', whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis',
743
+ },
744
+ }, (en.time && en.time !== '--:--' ? en.time + ' ' : '') + en.title)
745
+ }),
746
+ es.length > 3 ? h('div', { style: { fontSize: '10px', opacity: 0.5 } }, '+' + (es.length - 3)) : null))
747
+ })(cells[ci + cj])
748
+ }
749
+ gridRows.push(h('div', { key: 'w' + ci, style: { display: 'grid', gridTemplateColumns: 'repeat(7, 1fr)', gap: '3px' } }, week))
750
+ }
751
+ rows.push(h('div', null, gridRows))
752
+ function toggleDone(en) {
753
+ apiPost(API.calendar, { action: en.done ? 'remove' : 'done', date: en.date, time: en.time, title: en.title }).then(function (d) {
754
+ setMsg(d.result || ''); load()
755
+ }).catch(function (e) { setMsg(t('failed') + e.message) })
756
+ }
757
+ function saveDraft() {
758
+ if (!draft || !draft.title.trim()) { setMsg(t('needTitle')); return }
759
+ apiPost(API.calendar, { date: draft.date, time: draft.time, quadrant: draft.quadrant, title: draft.title.trim(), note: draft.note || '' }).then(function (d) {
760
+ setMsg(d.result || t('saved')); setDraft(null); load()
761
+ }).catch(function (e) { setMsg(t('failed') + e.message) })
762
+ }
763
+ // 添加/编辑浮层(液态玻璃)
764
+ if (draft) {
765
+ rows.push(h('div', {
766
+ style: {
767
+ position: 'absolute', inset: 0, zIndex: 10, borderRadius: '16px',
768
+ background: 'color-mix(in srgb, var(--dsw-alias-bg-overlay, rgba(255,255,255,.9)) 70%, transparent)',
769
+ backdropFilter: 'blur(20px) saturate(1.4)', WebkitBackdropFilter: 'blur(20px) saturate(1.4)',
770
+ display: 'flex', flexDirection: 'column', padding: '18px', gap: '10px',
771
+ border: '1px solid color-mix(in srgb, var(--dsw-alias-border-l1, rgba(128,128,128,.35)) 60%, transparent)',
772
+ },
773
+ },
774
+ h('div', { style: { fontWeight: 700 } }, t('addItem') + ' · ' + draft.date),
775
+ h('input', { 'data-dam-input': '', placeholder: t('itemTitle'), value: draft.title, autoFocus: true, onChange: function (e) { var n = Object.assign({}, draft); n.title = e.target.value; setDraft(n) } }),
776
+ h('div', { 'data-dam-row': '' },
777
+ h('input', { 'data-dam-input': '', type: 'time', value: draft.time, onChange: function (e) { var n = Object.assign({}, draft); n.time = e.target.value || '09:00'; setDraft(n) } }),
778
+ h('select', { 'data-dam-select': '', value: draft.quadrant, onChange: function (e) { var n = Object.assign({}, draft); n.quadrant = e.target.value; setDraft(n) } },
779
+ ['重要紧急', '重要不紧急', '紧急不重要', '不重要不紧急'].map(function (q) { return h('option', { key: q, value: q }, QUADRANT_STYLE[q].label) }))),
780
+ h('div', { 'data-dam-row': '' },
781
+ h('button', { 'data-dam-btn': '', onClick: saveDraft }, t('save')),
782
+ h('button', { 'data-dam-btn': '', onClick: function () { setDraft(null) } }, t('cancel')))))
783
+ }
784
+ msg ? h('div', { 'data-dam-hint': '' }, msg) : null
785
+ return h('div', { style: { position: 'relative' } }, rows)
786
+ }
787
+
552
788
  var TOOL_LABEL = { workbuddy: t('aiAssistant'), codebuddy: 'CodeBuddy', claude: 'Claude Code', codex: 'Codex', 'project-files': t('projectFiles') }
553
789
  function ConnectTab() {
554
790
  var sourcesPair = useState(null)
@@ -656,8 +892,9 @@ window.__ModuleLoader__.load({
656
892
  else if (tab === 'notes') body = h(NotesTab)
657
893
  else if (tab === 'reflections') body = h(ReflectionsTab)
658
894
  else if (tab === 'connect') body = h(ConnectTab)
895
+ else if (tab === 'calendar') body = h(CalendarTab)
659
896
  else body = h(SearchTab)
660
- var tabs = [['overview', t('overview')], ['logs', t('logs')], ['notes', t('notes')], ['reflections', t('reflections')], ['connect', t('connect')], ['search', t('search')]]
897
+ var tabs = [['overview', t('overview')], ['logs', t('logs')], ['notes', t('notes')], ['reflections', t('reflections')], ['connect', t('connect')], ['calendar', t('calendar')], ['search', t('search')]]
661
898
  var style = {
662
899
  left: g.left + 'px',
663
900
  top: g.top + 'px',
package/lib/index.js CHANGED
@@ -44,6 +44,7 @@ export const API = {
44
44
  note: '/api/dsh-auto-memory/note',
45
45
  external: '/api/dsh-auto-memory/external',
46
46
  'external-import': '/api/dsh-auto-memory/external-import',
47
+ calendar: '/api/dsh-auto-memory/calendar',
47
48
  }
48
49
 
49
50
  const DEFAULT_CONFIG = {
@@ -107,6 +108,7 @@ class MemoryEngine {
107
108
  pendingReflection: undefined, // {date, text}
108
109
  reflectionShownSession: undefined,
109
110
  todayGreeting: '', greetingShownSession: undefined,
111
+ calendarText: '', calendarPath: undefined,
110
112
  loadedAt: 0, loading: undefined, configLoaded: false,
111
113
  }
112
114
  this._configPath = path.join(dshHome(), 'dsh-auto-memory.json')
@@ -165,6 +167,7 @@ class MemoryEngine {
165
167
  ws,
166
168
  userDir,
167
169
  userFile: path.join(userDir, 'MEMORY.md'),
170
+ calendarPath: path.join(userDir, 'CALENDAR.md'),
168
171
  projectDir,
169
172
  notesPath: path.join(projectDir, 'MEMORY.md'),
170
173
  logPath: path.join(projectDir, `${todayStr()}.md`),
@@ -280,6 +283,9 @@ class MemoryEngine {
280
283
  }
281
284
  // 今日拟人化问候(每天首会话展示一次)
282
285
  this.state.todayGreeting = await this.readTextSafe(p.greetPath)
286
+ // 日历/日程(用户级,跨工作区与重装保留)
287
+ this.state.calendarPath = p.calendarPath
288
+ this.state.calendarText = await this.readTextSafe(p.calendarPath)
283
289
  // 外部记忆探测(后台,结果进缓存)
284
290
  if (this.config.externalSources) void this.external.discover(true)
285
291
  this.state.loadedAt = Date.now()
@@ -327,13 +333,21 @@ class MemoryEngine {
327
333
  lines.push('· 历史会话索引: ' + sess.map((x) => x.name + ' ' + x.files.length + ' 个').join(', ') + ' —— 需要时用 memory_recall 检索。')
328
334
  }
329
335
  }
336
+ // 日历/日程注入(让 AI 主动感知 deadline/约定)
337
+ if (this.state.calendarText && this.state.calendarText.trim()) {
338
+ const calEntries = this.parseCalendar(this.state.calendarText).filter((en) => !en.done && en.date >= todayStr()).slice(0, 10)
339
+ if (calEntries.length) {
340
+ const calLines = calEntries.map((en) => '· ' + en.date + ' ' + en.time + ' | ' + en.quadrant + ' | ' + en.title).join('\n')
341
+ lines.push('\n[日历与日程(未完成)]\n' + calLines + '\n主动关注这些安排:对话中若提及相关时间点,主动用 calendar_add 补充新事项、calendar_done 标记完成;回复正文中向用户转述日历变更。')
342
+ }
343
+ }
330
344
  lines.push('\n[记忆写入纪律 — 必须遵守]')
331
345
  lines.push('- 会话开始:若任务与历史工作/历史决策相关,先回顾以上记忆;**遇到不熟悉的代码、领域或项目时,主动调用 memory_recall 检索本机所有 AI 工具的历史记忆(CodeBuddy/Claude Code/Codex 等会话),不要凭空猜测**。')
332
346
  lines.push('- 新工作区(无历史日志/笔记):主动用 memory_recall 探索本机历史,判断该项目是否曾在其他 AI 工具中工作过;也可调用 memory_external 查看并接入外部记忆;检索时在正文中说明"我先查一下之前的记录"。')
333
347
  lines.push('- 完成实质性工作后立即调用 memory_log 追加今日日志(append-only,绝不覆盖):建/改应用、修 bug、写文档、重构、技术选型、用户约定或偏好。')
334
348
  lines.push('- progress 与 memory 一起写:写日志的同时,把有跨会话长期价值的内容一并写入记忆——跨项目规则 → memory_user,仅本项目 → memory_note;两者在同一轮完成,互不冲突、不遗漏。')
335
349
  lines.push('- 只记录有跨会话长期价值的;不记临时信息(搜索结果、临时路径、工具报错)。')
336
- lines.push('- **记忆操作对用户可见**:调用 memory_log/note/user/reflect 更新记忆时,在同一轮回复的正文中(非工具调用内部)用一句话向用户明文说明——"正在更新哪份文档、加入什么内容";调用 memory_recall/memory_external 检索时,也在正文中说明检索了什么、找到了什么,让用户直观看到记忆在工作。')
350
+ lines.push('- **记忆操作必须在正文可见(摘要链)**:调用 memory_log/note/user/reflect 更新记忆后,必须把结果写进本轮回复的正文文本(用户直接看到的那段文字,不是工具调用区),例如:"已更新今日日志,新增:修复了XXX";调用 memory_recall/memory_external 检索时,同样在正文开头写明"我查了记忆,发现..."。工具返回值只是辅助,正文转述是强制要求。')
337
351
  lines.push('- 用户明确要求长期记住:跨项目规则 → memory_user;仅本项目 → memory_note。')
338
352
  lines.push('- 定期调用 memory_maintain 归档 30 天前日志;不存密钥,除非用户明确要求。')
339
353
  lines.push('- 记忆仅作补充,不替代正常回复与交付物。')
@@ -363,24 +377,26 @@ class MemoryEngine {
363
377
  ].join('\n')
364
378
  }
365
379
 
366
- /** 拟人化问候请求块:每天首次会话注入一次(用户一打开就能看到)。 */
367
- renderGreetingRequest() {
368
- if (this.state.todayGreeting) return ''
369
- if (this.state.greetingShownSession === todayStr()) return ''
370
- this.state.greetingShownSession = todayStr()
371
- const lines = this.state.recentLogs[0] ? this.state.recentLogs[0].text : ''
380
+ /** 今日问候数据(纯数据,供 GUI 概览页渲染,不注入对话流)。 */
381
+ greetingData() {
372
382
  const hour = new Date().getHours()
373
383
  const period = hour < 6 ? '凌晨' : hour < 9 ? '早上好' : hour < 12 ? '上午好' : hour < 14 ? '中午好' : hour < 18 ? '下午好' : hour < 22 ? '晚上好' : '夜深了'
374
- return [
375
- '\n\n[晨间问候 拟人化,请生成]',
376
- '请以' + period + '的拟人化口吻,在本轮回复开头向用户说一段简短温暖的问候(2-4 句,像朋友聊天,不要列条框、不要用"反思"字样)。内容应自然融合:',
377
- '- 昨天/最近的经历:根据以下工作回顾,自然提起"昨天你做了..."( 1-2 件最值得说的,轻描淡写即可)',
378
- lines ? '昨天回顾:\n' + lines : '(暂无昨日日志)',
379
- '- 善意提醒:如果从回顾里看出可能遗漏的事(未完成的计划、埋的坑、待办),轻轻提醒一句;没有就不提。',
380
- '- 日常关心:可以自然地带一句时间/作息/状态相关的关心(如"记得吃早饭""别熬太晚"),如果有用户习惯(从记忆里看)就贴合习惯。',
381
- '- 语气:温暖、自然、像朋友,不要官方、不要严肃、不要长篇大论。',
382
- '生成后调用 memory_greet(date="' + todayStr() + '", text=完整问候内容)保存,之后今天不再提示。',
383
- ].join('\n')
384
+ // 昨天 = 最近一条日志(今天之前的);今天有条目也算最近
385
+ const recent = this.state.recentLogs[0] || null
386
+ const entries = recent ? recent.text.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('- ')).map((l) => {
387
+ const m = l.match(/^- (\d{2}:\d{2}) (.*)$/)
388
+ return m ? { time: m[1], text: m[2] } : { time: '', text: l.replace(/^- /, '') }
389
+ }) : []
390
+ return {
391
+ period,
392
+ date: todayStr(),
393
+ hasGreeting: !!this.state.todayGreeting,
394
+ greeting: this.state.todayGreeting,
395
+ yesterdayDate: recent ? recent.date : '',
396
+ entries,
397
+ hasPendingReflection: !!this.state.pendingReflection,
398
+ pendingReflectionDate: this.state.pendingReflection ? this.state.pendingReflection.date : '',
399
+ }
384
400
  }
385
401
 
386
402
  // ---------- 写操作 ----------
@@ -479,6 +495,98 @@ class MemoryEngine {
479
495
  return '已保存反思 ' + file
480
496
  }
481
497
 
498
+ // ---------- 日历/日程(用户级 CALENDAR.md) ----------
499
+ /** 解析 CALENDAR.md 为条目数组。 */
500
+ parseCalendar(text) {
501
+ const out = []
502
+ let curDate = ''
503
+ for (const raw of String(text || '').split('\n')) {
504
+ const line = raw.trim()
505
+ if (!line) continue
506
+ const dm = line.match(/^## (\d{4}-\d{2}-\d{2})/)
507
+ if (dm) { curDate = dm[1]; continue }
508
+ // - [x] HH:MM | 象限 | 标题 | (备注)
509
+ const m = line.match(/^- \[([ xX])\] (\d{1,2}:\d{2}) \| (重要紧急|重要不紧急|紧急不重要|不重要不紧急|未分类) \| (.+?)(?: \| (.*))?$/)
510
+ if (m) {
511
+ out.push({
512
+ date: curDate, done: m[1] !== ' ', time: m[2], quadrant: m[3], title: m[4].trim(), note: (m[5] || '').trim(),
513
+ })
514
+ }
515
+ }
516
+ return out
517
+ }
518
+
519
+ /** 序列化条目为 CALENDAR.md 文本。 */
520
+ renderCalendar(entries) {
521
+ const byDate = {}
522
+ for (const en of entries) { (byDate[en.date] ||= []).push(en) }
523
+ const dates = Object.keys(byDate).sort()
524
+ const lines = ['# 日历与日程 (CALENDAR)', '', '> 由 dsh-auto-memory 维护;AI 可从对话中提取 deadline/约定写入,用户也可在 GUI 操作。', '']
525
+ for (const date of dates) {
526
+ lines.push('## ' + date)
527
+ for (const en of byDate[date].sort((a, b) => (a.time || '').localeCompare(b.time || ''))) {
528
+ const mark = en.done ? 'x' : ' '
529
+ const note = en.note ? ' | ' + en.note : ''
530
+ lines.push('- [' + mark + '] ' + (en.time || '--:--') + ' | ' + (en.quadrant || '未分类') + ' | ' + en.title + note)
531
+ }
532
+ lines.push('')
533
+ }
534
+ return lines.join('\n')
535
+ }
536
+
537
+ /** 添加/更新日历条目并落盘(用户级)。 */
538
+ async calendarAdd(item, agent) {
539
+ const p = await this.resolvePaths(agent)
540
+ const entries = this.parseCalendar(this.state.calendarText || await this.readTextSafe(p.calendarPath))
541
+ entries.push({
542
+ date: item.date || todayStr(), done: !!item.done, time: item.time || '--:--',
543
+ quadrant: item.quadrant || '未分类', title: String(item.title || '').trim(), note: String(item.note || '').trim(),
544
+ })
545
+ const body = this.renderCalendar(entries)
546
+ await this.writeFull(p.calendarPath, body)
547
+ this.state.calendarText = body; this.state.loadedAt = Date.now()
548
+ return '已加入日历: ' + item.date + ' ' + (item.time || '') + ' ' + item.title + ' (' + (item.quadrant || '未分类') + ')'
549
+ }
550
+
551
+ /** 标记条目完成。 */
552
+ async calendarDone(date, time, title, agent) {
553
+ const p = await this.resolvePaths(agent)
554
+ const entries = this.parseCalendar(this.state.calendarText || await this.readTextSafe(p.calendarPath))
555
+ const hit = entries.find((en) => en.date === date && en.time === time && en.title === title)
556
+ if (!hit) return '未找到该日历条目: ' + date + ' ' + time + ' ' + title
557
+ hit.done = true
558
+ const body = this.renderCalendar(entries)
559
+ await this.writeFull(p.calendarPath, body)
560
+ this.state.calendarText = body; this.state.loadedAt = Date.now()
561
+ return '已标记完成: ' + date + ' ' + title
562
+ }
563
+
564
+ /** 删除条目。 */
565
+ async calendarRemove(date, time, title, agent) {
566
+ const p = await this.resolvePaths(agent)
567
+ const entries = this.parseCalendar(this.state.calendarText || await this.readTextSafe(p.calendarPath))
568
+ const before = entries.length
569
+ const kept = entries.filter((en) => !(en.date === date && en.time === time && en.title === title))
570
+ if (kept.length === before) return '未找到该日历条目: ' + date + ' ' + time + ' ' + title
571
+ const body = this.renderCalendar(kept)
572
+ await this.writeFull(p.calendarPath, body)
573
+ this.state.calendarText = body; this.state.loadedAt = Date.now()
574
+ return '已删除日历条目: ' + date + ' ' + title
575
+ }
576
+
577
+ /** 时段摘要:把今日日志按 时段(早晨/上午/下午/晚上)切分。 */
578
+ periodSummary() {
579
+ const today = this.state.logText || ''
580
+ const entries = today.split('\n').map((l) => l.trim()).filter((l) => l.startsWith('- ')).map((l) => {
581
+ const m = l.match(/^- (\d{2}):(\d{2}) (.*)$/)
582
+ return m ? { h: Number(m[1]), text: m[3] } : null
583
+ }).filter(Boolean)
584
+ const bucket = (h) => h < 5 ? '凌晨' : h < 9 ? '早晨' : h < 12 ? '上午' : h < 14 ? '中午' : h < 18 ? '下午' : '晚上'
585
+ const groups = { '凌晨': [], '早晨': [], '上午': [], '中午': [], '下午': [], '晚上': [] }
586
+ for (const en of entries) { (groups[bucket(en.h)] ||= []).push(en.text) }
587
+ return { entries, groups, todayDate: todayStr() }
588
+ }
589
+
482
590
  /** 一键反思:自动取"有日志但无反思"的最早日期,按日志条目生成反思草稿并落盘。 */
483
591
  async reflectAuto(agent) {
484
592
  const p = await this.resolvePaths(agent)
@@ -559,7 +667,10 @@ class MemoryEngine {
559
667
  todayEntries,
560
668
  latestReflectionDate: this.state.latestReflectionDate,
561
669
  pendingReflection: this.state.pendingReflection ? this.state.pendingReflection.date : undefined,
562
- todayGreeting: this.state.todayGreeting,
670
+ greeting: this.greetingData(),
671
+ calendar: this.parseCalendar(this.state.calendarText),
672
+ calendarPath: this.state.calendarPath,
673
+ periodSummary: this.periodSummary(),
563
674
  refreshedAt: this.state.loadedAt,
564
675
  configReadError: this._readError,
565
676
  }
@@ -884,7 +995,7 @@ async function globOne(dir, re, limit) {
884
995
  return out
885
996
  }
886
997
 
887
- /** 从一条 jsonl 会话行提取文本片段(兼容 claude/codex 等格式)。 */
998
+ /** 从一条 jsonl 会话行提取文本片段(兼容 claude/codex/workbuddy 格式)。 */
888
999
  function extractJsonText(line) {
889
1000
  try {
890
1001
  const obj = JSON.parse(line)
@@ -934,14 +1045,14 @@ export function apply(ctx, config) {
934
1045
  if (!engine.state.loadedAt || Date.now() - engine.state.loadedAt > 60000) {
935
1046
  void engine.refresh(agent)
936
1047
  }
937
- return engine.renderMemory(context) + engine.renderReflectionRequest() + engine.renderGreetingRequest()
1048
+ return engine.renderMemory(context) + engine.renderReflectionRequest()
938
1049
  } catch (e) { return '' }
939
1050
  },
940
1051
  })
941
1052
 
942
1053
  // ---------- 工具 ----------
943
1054
  const tools = [
944
- defineTool('memory_log', '向当前工作区的 .dsh-memory/ 今日日志追加一条工作记录(append-only,自动建目录/文件)。完成实质性工作(改代码/修 bug/写文档/重构/技术选型/用户偏好约定)后必须调用;有跨会话长期价值的内容在同一轮内一并写入记忆(memory_note 项目/ memory_user 跨项目),progress 与 memory 一起写;不要记录临时信息。', {
1055
+ defineTool('memory_log', '向当前工作区的 .dsh-memory/ 今日日志追加一条工作记录(append-only,自动建目录/文件)。完成实质性工作(改代码/修 bug/写文档/重构/技术选型/用户偏好约定)后必须调用;有跨会话长期价值的内容在同一轮内一并写入记忆(memory_note 项目/ memory_user 跨项目),progress 与 memory 一起写;不要记录临时信息。**调用后必须在本轮回复正文(摘要可见的正文,不是工具调用区)中向用户转述一句:如"已把 X 记入今日日志"**。', {
945
1056
  note: { type: 'string', required: true, description: '简短条目:一句话概括做了什么、结果如何。' },
946
1057
  date: { type: 'string', description: '日志日期 YYYY-MM-DD,缺省今天。' },
947
1058
  }, async (args, exec) => {
@@ -954,7 +1065,7 @@ export function apply(ctx, config) {
954
1065
  return '已更新记忆文档: ' + logPath + '\n' + entry
955
1066
  }),
956
1067
 
957
- defineTool('memory_note', '更新当前项目长期笔记 .dsh-memory/MEMORY.md(本项目专属的约定、决策、架构要点)。action=append 追加一段(自动带日期标题);action=replace 整体替换(需先基于注入内容或 memory_recall 结果给出完整新内容)', {
1068
+ defineTool('memory_note', '更新当前项目长期笔记 .dsh-memory/MEMORY.md(本项目专属的约定、决策、架构要点)。action=append 追加一段(自动带日期标题);action=replace 整体替换(需先基于注入内容或 memory_recall 结果给出完整新内容)。**调用后必须在本轮回复正文中向用户转述:更新了项目笔记、加入什么要点**。', {
958
1069
  content: { type: 'string', required: true, description: '笔记内容。' },
959
1070
  action: { type: 'string', enum: ['append', 'replace'], required: true, description: 'append=追加, replace=整体替换。' },
960
1071
  }, async (args, exec) => {
@@ -972,7 +1083,7 @@ export function apply(ctx, config) {
972
1083
  return '已更新项目笔记: ' + p.notesPath + '\n追加内容:\n' + content
973
1084
  }),
974
1085
 
975
- defineTool('memory_user', '更新用户级记忆 ~/.dsh/memory/MEMORY.md(跨所有项目的长期规则/偏好,用户明确要求记住时用)。action=append 追加;action=replace 整体替换。', {
1086
+ defineTool('memory_user', '更新用户级记忆 ~/.dsh/memory/MEMORY.md(跨所有项目的长期规则/偏好,用户明确要求记住时用)。action=append 追加;action=replace 整体替换。**调用后必须在本轮回复正文中向用户转述:已记住该规则/偏好**。', {
976
1087
  content: { type: 'string', required: true, description: '要记住的规则或偏好内容。' },
977
1088
  action: { type: 'string', enum: ['append', 'replace'], required: true, description: 'append=追加, replace=整体替换。' },
978
1089
  }, async (args, exec) => {
@@ -990,7 +1101,7 @@ export function apply(ctx, config) {
990
1101
  return '已更新用户级记忆: ' + p.userFile + '\n追加内容:\n' + content
991
1102
  }),
992
1103
 
993
- defineTool('memory_recall', '检索记忆:本地记忆文件(每日日志、项目笔记、用户级记忆、反思)关键词匹配 + 历史 DSH 会话全文检索(如部署启用)。用户提到过去的做法/讨论/决定而当前上下文没有时调用,查询必须自包含。', {
1104
+ defineTool('memory_recall', '检索记忆:本地记忆文件(每日日志、项目笔记、用户级记忆、反思)关键词匹配 + 历史 DSH 会话全文检索(如部署启用)。用户提到过去的做法/讨论/决定而当前上下文没有时调用,查询必须自包含。**检索后必须在本轮回复正文中向用户转述:检索了什么、找到什么(或没找到)**。', {
994
1105
  query: { type: 'string', required: true, description: '检索关键词或自包含描述。' },
995
1106
  limit: { type: 'integer', description: '最多返回条数,缺省 8。' },
996
1107
  }, async (args, exec) => engine.recall(args.query, args.limit, exec.agent)),
@@ -1016,18 +1127,6 @@ export function apply(ctx, config) {
1016
1127
  text: { type: 'string', required: true, description: '完整反思内容:成果回顾 / 教训改进 / 今日可延续要点。' },
1017
1128
  }, async (args, exec) => engine.saveReflection(args.date, args.text, exec.agent)),
1018
1129
 
1019
- defineTool('memory_greet', '保存今日拟人化晨间问候到 .dsh-memory/greetings/YYYY-MM-DD.md(每天首次会话收到「晨间问候」提示后调用)。问候内容为面向用户的拟人化短句(温暖自然、朋友口吻),不要写反思类结构化内容。', {
1020
- date: { type: 'string', required: true, description: '问候日期 YYYY-MM-DD。' },
1021
- text: { type: 'string', required: true, description: '完整问候内容。' },
1022
- }, async (args, exec) => {
1023
- const date = DATE_RE.test(args.date || '') ? args.date : todayStr()
1024
- const p = await engine.resolvePaths(exec.agent)
1025
- const file = path.join(p.greetDir, date + '.md')
1026
- await engine.writeFull(file, args.text + '\n')
1027
- if (date === todayStr()) { engine.state.todayGreeting = String(args.text).trim(); engine.state.loadedAt = Date.now() }
1028
- return '已保存晨间问候 ' + file + '\n' + String(args.text).trim()
1029
- }),
1030
-
1031
1130
  defineTool('memory_external', '查看/接入其他 AI 工具(CodeBuddy/Claude Code/Codex/项目约定文件等)的记忆。action=list 列出全部检测到的外部记忆源(路径/大小/预览/会话数);action=import 把某源内容整体接入本地记忆(source 为源 id,target=project 接进项目笔记 / user 接进用户级记忆,自动标注来源)。首次在新工作区工作、或用户提到其他软件里做过的事时调用。', {
1032
1131
  action: { type: 'string', enum: ['list', 'import'], required: true, description: 'list=列出外部记忆源; import=接入指定源。' },
1033
1132
  source: { type: 'string', description: '要接入的源 id(action=import 时必填,来自 list 结果)。' },
@@ -1048,6 +1147,40 @@ export function apply(ctx, config) {
1048
1147
  }
1049
1148
  return engine.external.importInto(String(args.source || ''), args.target === 'user' ? 'user' : 'project', engine, exec.agent)
1050
1149
  }),
1150
+
1151
+ defineTool('calendar_add', '向用户级日历(~/.dsh/memory/CALENDAR.md)添加日程/事项。主动从对话中提取 deadline、约定时间、任务节点等信息写入日历(跨对话有效、重装不丢)。调用后必须在本轮回复正文中向用户转述:已把 X 记入日历。', {
1152
+ date: { type: 'string', description: '日期 YYYY-MM-DD,缺省今天。' },
1153
+ time: { type: 'string', description: '时间 HH:MM,无则 --:--。' },
1154
+ quadrant: { type: 'string', enum: ['重要紧急', '重要不紧急', '紧急不重要', '不重要不紧急'], description: '四象限分类,缺省重要不紧急。' },
1155
+ title: { type: 'string', required: true, description: '事项标题。' },
1156
+ note: { type: 'string', description: '备注/来源,如"来自对话:用户说周五交报告"。' },
1157
+ }, async (args, exec) => engine.calendarAdd({ date: args.date, time: args.time, quadrant: args.quadrant, title: args.title, note: args.note }, exec.agent)),
1158
+
1159
+ defineTool('calendar_list', '列出日历条目(可按日期过滤、含完成状态)。用于查看已有安排、回答"我最近有什么安排"等问题。', {
1160
+ date: { type: 'string', description: '过滤日期 YYYY-MM-DD,缺省全部(近 60 天)。' },
1161
+ }, async (args, exec) => {
1162
+ const entries = engine.parseCalendar(engine.state.calendarText)
1163
+ const target = args.date
1164
+ const list = entries.filter((en) => !target || en.date === target)
1165
+ if (!list.length) return '日历为空' + (target ? ' (' + target + ')' : '') + '。'
1166
+ const lines = ['日历条目(' + list.length + ' 个):']
1167
+ for (const en of list.sort((a, b) => (a.date + a.time).localeCompare(b.date + b.time)).slice(0, 40)) {
1168
+ lines.push('· ' + (en.done ? '[完成] ' : '[待办] ') + en.date + ' ' + en.time + ' | ' + en.quadrant + ' | ' + en.title + (en.note ? ' (' + en.note + ')' : ''))
1169
+ }
1170
+ return lines.join('\n')
1171
+ }),
1172
+
1173
+ defineTool('calendar_done', '标记日历条目完成。参数需与 calendar_list 结果一致(date/time/title)。', {
1174
+ date: { type: 'string', required: true, description: '日期 YYYY-MM-DD。' },
1175
+ time: { type: 'string', required: true, description: '时间 HH:MM 或 --:--。' },
1176
+ title: { type: 'string', required: true, description: '事项标题。' },
1177
+ }, async (args, exec) => engine.calendarDone(args.date, args.time, args.title, exec.agent)),
1178
+
1179
+ defineTool('calendar_remove', '删除日历条目。', {
1180
+ date: { type: 'string', required: true, description: '日期 YYYY-MM-DD。' },
1181
+ time: { type: 'string', required: true, description: '时间 HH:MM 或 --:--。' },
1182
+ title: { type: 'string', required: true, description: '事项标题。' },
1183
+ }, async (args, exec) => engine.calendarRemove(args.date, args.time, args.title, exec.agent)),
1051
1184
  ]
1052
1185
 
1053
1186
  // ---------- 路由 ----------
@@ -1198,6 +1331,30 @@ export function apply(ctx, config) {
1198
1331
  try { writeJson(res, 200, { result: await engine.reflectAuto() }) } catch (e) { writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
1199
1332
  },
1200
1333
  },
1334
+ {
1335
+ kind: 'exact',
1336
+ path: API.calendar,
1337
+ handler: async (req, res) => {
1338
+ if (!isLoopbackRequest(req)) return writeJson(res, 403, { error: 'forbidden: loopback-only' })
1339
+ const method = req.method || 'GET'
1340
+ if (method === 'GET') {
1341
+ const entries = engine.parseCalendar(engine.state.calendarText)
1342
+ writeJson(res, 200, { entries, path: engine.state.calendarPath || '' })
1343
+ return
1344
+ }
1345
+ if (method === 'POST') {
1346
+ const body = await readJsonBody(req)
1347
+ if (!body) return writeJson(res, 400, { error: 'invalid body' })
1348
+ try {
1349
+ if (body.action === 'done') writeJson(res, 200, { result: await engine.calendarDone(body.date, body.time, body.title) })
1350
+ else if (body.action === 'remove') writeJson(res, 200, { result: await engine.calendarRemove(body.date, body.time, body.title) })
1351
+ else writeJson(res, 200, { result: await engine.calendarAdd(body) })
1352
+ } catch (e) { writeJson(res, 500, { error: String(e && e.message ? e.message : e) }) }
1353
+ return
1354
+ }
1355
+ writeJson(res, 405, { error: 'method not allowed' })
1356
+ },
1357
+ },
1201
1358
  ]
1202
1359
 
1203
1360
  // ---------- 注册与清理 ----------
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.5",
4
+ "version": "0.1.7",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {