dsh-cortex-memory 0.2.1 → 0.3.0

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.
@@ -19,6 +19,15 @@ function ensureTraceColumns(db) {
19
19
  }
20
20
  }
21
21
 
22
+ function ensureSummonColumns(db) {
23
+ const cards = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'summon_cards'").get();
24
+ if (!cards) return;
25
+ const columns = db.prepare('PRAGMA table_info(summon_cards)').all();
26
+ if (!columns.some((column) => column.name === 'manual_json')) {
27
+ db.exec("ALTER TABLE summon_cards ADD COLUMN manual_json TEXT NOT NULL DEFAULT '{}'");
28
+ }
29
+ }
30
+
22
31
  function ensureFactsColumns(db) {
23
32
  const columns = db.prepare('PRAGMA table_info(facts)').all();
24
33
  if (!columns.some((column) => column.name === 'superseded_at')) {
@@ -76,6 +85,7 @@ export function migrate(db, { version = SCHEMA_VERSION } = {}) {
76
85
  db.exec(readSchema());
77
86
  ensureTraceColumns(db);
78
87
  ensureFactsColumns(db);
88
+ ensureSummonColumns(db);
79
89
  const row = db.prepare('SELECT value FROM meta WHERE key = ?').get('schema_version');
80
90
  if (!row) {
81
91
  db.prepare('INSERT INTO meta(key, value, updated_at) VALUES (?, ?, ?)').run('schema_version', String(version), nowIso());
@@ -0,0 +1,138 @@
1
+ import { makeId } from '../../core/ids.js';
2
+ import { nowIso } from '../../core/time.js';
3
+
4
+ function parse(value, fallback) {
5
+ try { return JSON.parse(value); } catch { return fallback; }
6
+ }
7
+
8
+ function rowToCard(row) {
9
+ if (!row) return null;
10
+ const manual = parse(row.manual_json ?? '{}', {});
11
+ return {
12
+ id: row.id,
13
+ scopeId: row.scope_id,
14
+ side: row.side,
15
+ title: manual.title ?? row.title,
16
+ content: manual.content ?? row.content,
17
+ triggers: Array.isArray(manual.triggers) ? manual.triggers : parse(row.triggers_json, []),
18
+ category: row.category,
19
+ sourceKind: row.source_kind,
20
+ sourceId: row.source_id,
21
+ day: row.day,
22
+ importance: manual.importance ?? Number(row.importance),
23
+ manual: manual,
24
+ status: row.status,
25
+ createdAt: row.created_at,
26
+ updatedAt: row.updated_at,
27
+ };
28
+ }
29
+
30
+ function rowToVolume(row) {
31
+ if (!row) return null;
32
+ return {
33
+ id: row.id,
34
+ scopeId: row.scope_id,
35
+ side: row.side,
36
+ title: row.title,
37
+ category: row.category,
38
+ index: row.index_text,
39
+ content: row.content,
40
+ cardIds: parse(row.card_ids_json, []),
41
+ periodStart: row.period_start,
42
+ periodEnd: row.period_end,
43
+ status: row.status,
44
+ createdAt: row.created_at,
45
+ updatedAt: row.updated_at,
46
+ };
47
+ }
48
+
49
+ export function createSummonRepository(db) {
50
+ function upsertCard(card = {}) {
51
+ const existing = db.prepare(
52
+ 'SELECT * FROM summon_cards WHERE scope_id = ? AND source_kind = ? AND source_id = ?',
53
+ ).get(card.scopeId ?? 'global', card.sourceKind ?? null, card.sourceId ?? null);
54
+ const now = nowIso();
55
+ const id = existing?.id ?? card.id ?? makeId('scd');
56
+ db.prepare(`
57
+ INSERT INTO summon_cards(id, scope_id, side, title, content, triggers_json, category,
58
+ source_kind, source_id, day, importance, manual_json, status, created_at, updated_at)
59
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
60
+ ON CONFLICT(scope_id, source_kind, source_id) DO UPDATE SET
61
+ side = excluded.side, title = excluded.title, content = excluded.content,
62
+ triggers_json = excluded.triggers_json, category = excluded.category,
63
+ day = excluded.day, importance = excluded.importance, status = excluded.status,
64
+ updated_at = excluded.updated_at
65
+ `).run(
66
+ id, card.scopeId ?? 'global', card.side, card.title, card.content,
67
+ JSON.stringify(card.triggers ?? []), card.category ?? null,
68
+ card.sourceKind ?? null, card.sourceId ?? null, card.day,
69
+ Number(card.importance ?? 0.5), existing?.manual_json ?? '{}', card.status ?? 'active',
70
+ existing?.created_at ?? now, now,
71
+ );
72
+ return getCard(id);
73
+ }
74
+
75
+ function getCard(id) {
76
+ return rowToCard(db.prepare('SELECT * FROM summon_cards WHERE id = ?').get(id));
77
+ }
78
+
79
+ function listCards({ scopeId = 'global', day = null, side = null, limit = 300 } = {}) {
80
+ const clauses = ['scope_id = ?', "status = 'active'"];
81
+ const values = [scopeId];
82
+ if (day) { clauses.push('day = ?'); values.push(day); }
83
+ if (side) { clauses.push('side = ?'); values.push(side); }
84
+ values.push(Math.max(1, Math.min(1000, Number(limit) || 300)));
85
+ return db.prepare(`SELECT * FROM summon_cards WHERE ${clauses.join(' AND ')} ORDER BY day DESC, importance DESC, created_at DESC LIMIT ?`).all(...values).map(rowToCard);
86
+ }
87
+
88
+ function updateCard(id, patch = {}) {
89
+ const current = getCard(id);
90
+ if (!current) return null;
91
+ const manual = {
92
+ ...(current.manual ?? {}),
93
+ ...(patch.title == null ? {} : { title: String(patch.title) }),
94
+ ...(patch.content == null ? {} : { content: String(patch.content) }),
95
+ ...(Array.isArray(patch.triggers) ? { triggers: patch.triggers.map(String) } : {}),
96
+ ...(patch.importance == null ? {} : { importance: Number(patch.importance) }),
97
+ };
98
+ db.prepare('UPDATE summon_cards SET manual_json = ?, updated_at = ? WHERE id = ?')
99
+ .run(JSON.stringify(manual), nowIso(), id);
100
+ return getCard(id);
101
+ }
102
+
103
+ function upsertVolume(volume = {}) {
104
+ const existing = db.prepare(
105
+ 'SELECT * FROM summon_volumes WHERE scope_id = ? AND side = ? AND category = ? AND period_start = ? AND period_end = ?',
106
+ ).get(volume.scopeId ?? 'global', volume.side, volume.category ?? null, volume.periodStart ?? null, volume.periodEnd ?? null);
107
+ const now = nowIso();
108
+ const id = existing?.id ?? makeId('svl');
109
+ db.prepare(`
110
+ INSERT INTO summon_volumes(id, scope_id, side, title, category, index_text, content,
111
+ card_ids_json, period_start, period_end, status, created_at, updated_at)
112
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
113
+ ON CONFLICT(scope_id, side, category, period_start, period_end) DO UPDATE SET
114
+ title = excluded.title, index_text = excluded.index_text, content = excluded.content,
115
+ card_ids_json = excluded.card_ids_json, status = excluded.status, updated_at = excluded.updated_at
116
+ `).run(
117
+ id, volume.scopeId ?? 'global', volume.side, volume.title, volume.category ?? null,
118
+ volume.index ?? '', volume.content ?? '', JSON.stringify(volume.cardIds ?? []),
119
+ volume.periodStart ?? null, volume.periodEnd ?? null, volume.status ?? 'active',
120
+ existing?.created_at ?? now, now,
121
+ );
122
+ return getVolume(id);
123
+ }
124
+
125
+ function getVolume(id) {
126
+ return rowToVolume(db.prepare('SELECT * FROM summon_volumes WHERE id = ?').get(id));
127
+ }
128
+
129
+ function listVolumes({ scopeId = 'global', side = null, limit = 100 } = {}) {
130
+ const clauses = ['scope_id = ?', "status = 'active'"];
131
+ const values = [scopeId];
132
+ if (side) { clauses.push('side = ?'); values.push(side); }
133
+ values.push(Math.max(1, Math.min(500, Number(limit) || 100)));
134
+ return db.prepare(`SELECT * FROM summon_volumes WHERE ${clauses.join(' AND ')} ORDER BY period_end DESC, updated_at DESC LIMIT ?`).all(...values).map(rowToVolume);
135
+ }
136
+
137
+ return { upsertCard, getCard, listCards, updateCard, upsertVolume, getVolume, listVolumes };
138
+ }
@@ -482,3 +482,48 @@ CREATE INDEX IF NOT EXISTS dream_runs_scope_at ON dream_runs(scope_id, at);
482
482
 
483
483
 
484
484
 
485
+
486
+ -- 召唤卡:Cortex 原生的 L2 卡层,不依赖 shiori。
487
+ -- side: left = 情感卡,right = 工作卡。
488
+ CREATE TABLE IF NOT EXISTS summon_cards (
489
+ id TEXT PRIMARY KEY,
490
+ scope_id TEXT NOT NULL,
491
+ side TEXT NOT NULL CHECK(side IN ('left', 'right')),
492
+ title TEXT NOT NULL,
493
+ content TEXT NOT NULL,
494
+ triggers_json TEXT NOT NULL DEFAULT '[]',
495
+ category TEXT,
496
+ source_kind TEXT,
497
+ source_id TEXT,
498
+ day TEXT NOT NULL,
499
+ importance REAL NOT NULL DEFAULT 0.5,
500
+ manual_json TEXT NOT NULL DEFAULT '{}',
501
+ status TEXT NOT NULL DEFAULT 'active',
502
+ created_at TEXT NOT NULL,
503
+ updated_at TEXT NOT NULL,
504
+ UNIQUE(scope_id, source_kind, source_id)
505
+ );
506
+
507
+ CREATE INDEX IF NOT EXISTS summon_cards_scope_day ON summon_cards(scope_id, day, side);
508
+ CREATE INDEX IF NOT EXISTS summon_cards_scope_status ON summon_cards(scope_id, status);
509
+
510
+ -- 召唤策:按左右脑与事件类别合并的册;index 只放一行索引,正文按需读。
511
+ CREATE TABLE IF NOT EXISTS summon_volumes (
512
+ id TEXT PRIMARY KEY,
513
+ scope_id TEXT NOT NULL,
514
+ side TEXT NOT NULL CHECK(side IN ('left', 'right')),
515
+ title TEXT NOT NULL,
516
+ category TEXT,
517
+ index_text TEXT NOT NULL,
518
+ content TEXT NOT NULL,
519
+ card_ids_json TEXT NOT NULL DEFAULT '[]',
520
+ period_start TEXT,
521
+ period_end TEXT,
522
+ status TEXT NOT NULL DEFAULT 'active',
523
+ created_at TEXT NOT NULL,
524
+ updated_at TEXT NOT NULL,
525
+ UNIQUE(scope_id, side, category, period_start, period_end)
526
+ );
527
+
528
+ CREATE INDEX IF NOT EXISTS summon_volumes_scope_side ON summon_volumes(scope_id, side, status);
529
+ CREATE INDEX IF NOT EXISTS summon_volumes_period ON summon_volumes(period_start, period_end);
@@ -219,7 +219,7 @@ export function createToolDefinitions(service) {
219
219
  additionalProperties: false,
220
220
  required: ['action'],
221
221
  properties: {
222
- action: { type: 'string', enum: ['diary-write', 'diary-list', 'diary-forget', 'dream', 'sleep', 'sleep-runs'] },
222
+ action: { type: 'string', enum: ['diary-generate', 'diary-write', 'diary-list', 'diary-forget', 'dream', 'sleep', 'sleep-runs'] },
223
223
  text: { type: 'string' },
224
224
  mood: { type: 'string' },
225
225
  id: { type: 'string' },
@@ -230,6 +230,11 @@ export function createToolDefinitions(service) {
230
230
  output: TEXT_OUTPUT,
231
231
  isConcurrencySafe: () => false,
232
232
  async execute(args = {}) {
233
+ if (args.action === 'diary-generate') {
234
+ if (!service.scopeWritable(service.settings().scope)) return { text: '未执行:当前 scope 只读共享。' };
235
+ const item = await service.life.diaryGenerate();
236
+ return { text: `日记 ${item.id}(${item.model},来源 ${item.sourceCount} 条):\n${item.text}` };
237
+ }
233
238
  if (args.action === 'diary-write') {
234
239
  if (!service.scopeWritable(service.settings().scope)) return { text: '未执行:当前 scope 只读共享。' };
235
240
  const item = service.life.diaryWrite({ text: args.text, mood: args.mood });
@@ -259,13 +264,13 @@ export function createToolDefinitions(service) {
259
264
  },
260
265
  {
261
266
  name: 'cortex_emotion',
262
- description: 'Detect emotions in text, record an emotion event for a target, show affect attitudes, or list the 24-category lexicon.',
267
+ description: 'Generate an AI-authored feeling, detect emotions in text, record an emotion event for a target, show affect attitudes, or list the 24-category lexicon.',
263
268
  parameters: {
264
269
  type: 'object',
265
270
  additionalProperties: false,
266
271
  required: ['action'],
267
272
  properties: {
268
- action: { type: 'string', enum: ['detect', 'record', 'affect', 'events', 'lexicon'] },
273
+ action: { type: 'string', enum: ['generate', 'detect', 'record', 'affect', 'events', 'lexicon'] },
269
274
  text: { type: 'string' },
270
275
  target: { type: 'string' },
271
276
  traceId: { type: 'string' },
@@ -275,6 +280,11 @@ export function createToolDefinitions(service) {
275
280
  output: TEXT_OUTPUT,
276
281
  isConcurrencySafe: () => false,
277
282
  async execute(args = {}) {
283
+ if (args.action === 'generate') {
284
+ if (!service.scopeWritable(service.settings().scope)) return { text: '未执行:当前 scope 只读共享。' };
285
+ const result = await service.life.feelingGenerate({ source: 'cortex:manual:feeling' });
286
+ return { text: `感受(${result.model},来源 ${result.sourceCount} 条):${result.text}\n情绪:${result.emotions.map((item) => item.name).join('、')} → ${result.target}` };
287
+ }
278
288
  if (args.action === 'detect') {
279
289
  const result = service.emotion.detect(args.text);
280
290
  return { text: result.emotions.length ? result.emotions.map((item) => `${item.name}:${item.score}`).join(' / ') : '没有命中情绪词。' };