dsh-cortex-memory 0.1.0 → 0.2.0-rc.13

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.
Files changed (56) hide show
  1. package/LICENSE +3 -3
  2. package/README.md +85 -146
  3. package/bench/ab-engine.mjs +60 -0
  4. package/bench/ann-bench.mjs +78 -0
  5. package/bench/hnsw-bench.mjs +79 -0
  6. package/bench/metrics.js +8 -0
  7. package/bench/migration-bench.mjs +69 -0
  8. package/bench/run.js +5 -2
  9. package/bench/synthetic.js +51 -7
  10. package/bench/vector-ab.mjs +76 -0
  11. package/bin/cortex.mjs +2 -3
  12. package/client.js +286 -4
  13. package/cordis.patch.yml +1 -1
  14. package/docs/install-windows.md +115 -27
  15. package/docs/models.md +165 -30
  16. package/docs/multimodal.md +112 -0
  17. package/docs/scopes.md +142 -0
  18. package/examples/README.md +72 -0
  19. package/examples/setup-cortex-bge.ps1 +66 -0
  20. package/examples/uninstall-cortex.ps1 +91 -0
  21. package/index.js +15 -1
  22. package/package.json +5 -12
  23. package/src/adapters/markdown-import.js +20 -1
  24. package/src/adapters/markdown.js +12 -2
  25. package/src/adapters/shiori.js +1 -1
  26. package/src/api/routes.js +148 -4
  27. package/src/api/validate.js +26 -0
  28. package/src/core/attachment-reader.js +31 -0
  29. package/src/core/attachments.js +39 -0
  30. package/src/core/config.js +36 -1
  31. package/src/core/model-config.js +3 -1
  32. package/src/core/profile.js +32 -0
  33. package/src/engines/develop.js +1 -1
  34. package/src/engines/encode.js +68 -30
  35. package/src/engines/extract.js +13 -8
  36. package/src/engines/growth.js +1 -1
  37. package/src/engines/reflect.js +1 -1
  38. package/src/engines/retrieve.js +44 -13
  39. package/src/host/runtime.js +1 -1
  40. package/src/memory/query.js +1 -1
  41. package/src/memory/tokenizer.js +19 -4
  42. package/src/security/scope-grants.js +73 -0
  43. package/src/self/appraisal.js +3 -3
  44. package/src/self/emotion.js +1 -1
  45. package/src/self/identity.js +1 -1
  46. package/src/self/relationship.js +2 -2
  47. package/src/service/cortex.js +191 -3
  48. package/src/store/database.js +33 -1
  49. package/src/store/repositories/edges.js +48 -4
  50. package/src/store/repositories/traces.js +23 -12
  51. package/src/store/schema.sql +1 -0
  52. package/src/tools/definitions.js +64 -5
  53. package/src/vector/external-ann.js +37 -0
  54. package/src/vector/hnsw.js +246 -0
  55. package/src/vector/index.js +85 -2
  56. package/docs/compatibility.md +0 -39
@@ -0,0 +1,72 @@
1
+ # Cortex bge 可选 profile
2
+
3
+ 默认 **不开启** bge。这个示例只给想要本地语义召回的人用:
4
+
5
+ - 需要本地 Ollama / OpenAI-compatible embedding 服务
6
+ - 需要 `bge-m3`(1024 维)
7
+ - 查询延迟会明显高于 FTS
8
+ - 没有服务时不要开;失败会退开放,不会假装成功
9
+
10
+ ## 一键创建独立 profile
11
+
12
+ ```powershell
13
+ powershell -ExecutionPolicy Bypass -File .\examples\setup-cortex-bge.ps1 `
14
+ -Dsh "D:\path\to\dsh.cmd" `
15
+ -Profile cortex-bge `
16
+ -Package "C:\path\to\dsh-cortex-memory-0.1.0.tgz"
17
+ ```
18
+
19
+ 首次先用 Ollama 启动模型服务:
20
+
21
+ ```powershell
22
+ ollama serve
23
+ ollama pull bge-m3
24
+ ```
25
+
26
+ 脚本会:
27
+
28
+ 1. 用 DSH 的 `web` 模板新建一个空 profile
29
+ 2. 安装 cortex 包
30
+ 3. 写独立的 `dataRoot/dbFile`
31
+ 4. 写 `features.vector: true`
32
+ 5. 写 `model-config.json`(local / bge-m3 / 1024)
33
+
34
+ 它**不会**改 desktop,也不会自动启动服务。
35
+
36
+ ## 启动
37
+
38
+ ```powershell
39
+ & $Dsh --profile cortex-bge --port 0 --no-open
40
+ ```
41
+
42
+ 打开后到 Cortex 设置里确认:
43
+
44
+ ```text
45
+ embedding mode: local
46
+ model: bge-m3
47
+ dim: 1024
48
+ ```
49
+
50
+ ## 关闭
51
+
52
+ 移除 profile 或把 `features.vector` 改回 `false`。
53
+ 不想用语义模型时,FTS 仍是默认路径。
54
+
55
+ ## 卸载 cortex
56
+
57
+ `dsh plugin remove` 不会删手写 patch。用:
58
+
59
+ ```powershell
60
+ powershell -ExecutionPolicy Bypass -File .\examples\uninstall-cortex.ps1 `
61
+ -Dsh "D:\path\to\dsh.cmd" `
62
+ -Profile cortex-bge
63
+ ```
64
+
65
+ 脚本会:
66
+
67
+ 1. 若已安装则 `plugin remove`
68
+ 2. 只删除 `- id: cortex` 段,保留其他 patch
69
+ 3. 跑一次 `--dump-config`
70
+ 4. **不删 dataRoot**
71
+
72
+ 如果已经手动移除,脚本会跳过 remove,仍清理残留 patch。
@@ -0,0 +1,66 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [string]$Dsh = 'dsh',
4
+ [string]$Profile = 'cortex-bge',
5
+ [string]$Package = 'dsh-cortex-memory',
6
+ [string]$DataRoot = (Join-Path $HOME '.dsh\cortex-bge'),
7
+ [string]$BaseUrl = 'http://127.0.0.1:11434/v1',
8
+ [string]$Model = 'bge-m3',
9
+ [int]$Dim = 1024
10
+ )
11
+
12
+ $ErrorActionPreference = 'Stop'
13
+ $dshCommand = Get-Command $Dsh -ErrorAction SilentlyContinue
14
+ if (-not $dshCommand) { throw "dsh command not found: $Dsh. Pass -Dsh with the path to dsh.cmd." }
15
+ $dshPath = $dshCommand.Source
16
+ $profilePath = Join-Path (Join-Path $HOME '.dsh\profiles') $Profile
17
+ if (Test-Path $profilePath) { throw "profile already exists: $profilePath. Choose another -Profile or remove it first." }
18
+
19
+ if ($Package -match '\.tgz$') {
20
+ $Package = (Resolve-Path $Package).Path
21
+ }
22
+
23
+ & $dshPath --profile $Profile --from-default-profile web --dump-config | Out-Null
24
+ & $dshPath plugin --profile $Profile add $Package
25
+ if ($LASTEXITCODE -ne 0) { throw "plugin install failed with exit code $LASTEXITCODE" }
26
+
27
+ New-Item -ItemType Directory -Force -Path $DataRoot | Out-Null
28
+ $modelConfig = [ordered]@{
29
+ embedding = [ordered]@{
30
+ mode = 'local'
31
+ baseUrl = $BaseUrl
32
+ model = $Model
33
+ dim = $Dim
34
+ apiKey = ''
35
+ }
36
+ rerank = [ordered]@{
37
+ enabled = $false
38
+ baseUrl = ''
39
+ model = ''
40
+ apiKey = ''
41
+ }
42
+ }
43
+ $utf8 = New-Object System.Text.UTF8Encoding($false)
44
+ [System.IO.File]::WriteAllText(
45
+ (Join-Path $DataRoot 'model-config.json'),
46
+ (($modelConfig | ConvertTo-Json -Depth 5) + "`n"),
47
+ $utf8
48
+ )
49
+
50
+ $patchFile = Join-Path $profilePath 'cordis.patch.yml'
51
+ $patch = @"
52
+ # bge profile generated by examples/setup-cortex-bge.ps1
53
+ - id: cortex
54
+ name: dsh-cortex
55
+ config:
56
+ dataRoot: '$($DataRoot.Replace('\','/'))'
57
+ dbFile: '$((Join-Path $DataRoot 'cortex.sqlite').Replace('\','/'))'
58
+ features:
59
+ vector: true
60
+ "@
61
+ [System.IO.File]::WriteAllText($patchFile, $patch + "`n", $utf8)
62
+
63
+ Write-Host "profile: $profilePath"
64
+ Write-Host "dataRoot: $DataRoot"
65
+ Write-Host "Next: & '$dshPath' --profile $Profile --port 0 --no-open"
66
+ Write-Host "Start the local embedding service first: $BaseUrl"
@@ -0,0 +1,91 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [string]$Dsh = 'dsh',
4
+ [string]$Profile = 'desktop',
5
+ [string]$Package = 'dsh-cortex',
6
+ [switch]$KeepPatch
7
+ )
8
+
9
+ $ErrorActionPreference = 'Stop'
10
+ $dshCommand = Get-Command $Dsh -ErrorAction SilentlyContinue
11
+ if (-not $dshCommand) { throw "dsh command not found: $Dsh. Pass -Dsh with the path to dsh.cmd." }
12
+ $dshPath = $dshCommand.Source
13
+ $profilePath = Join-Path (Join-Path $HOME '.dsh\profiles') $Profile
14
+ if (-not (Test-Path $profilePath)) { throw "profile not found: $profilePath" }
15
+
16
+ $packageJson = Join-Path $profilePath 'package.json'
17
+ $installed = $false
18
+ if (Test-Path $packageJson) {
19
+ try {
20
+ $manifest = Get-Content -LiteralPath $packageJson -Raw | ConvertFrom-Json
21
+ $installed = $null -ne $manifest.dependencies.$Package
22
+ } catch { $installed = $false }
23
+ }
24
+ if ($installed) {
25
+ & $dshPath plugin --profile $Profile remove $Package
26
+ if ($LASTEXITCODE -ne 0) { throw "plugin remove failed with exit code $LASTEXITCODE" }
27
+ } else {
28
+ Write-Host "$Package is not installed in $Profile; skipping remove"
29
+ }
30
+
31
+ $patchFile = Join-Path $profilePath 'cordis.patch.yml'
32
+ $configuredDataRoot = $null
33
+ if (Test-Path $patchFile) {
34
+ foreach ($line in [System.IO.File]::ReadAllLines($patchFile)) {
35
+ if ($line -match '^\s*dataRoot:\s*(.+)$') {
36
+ $configuredDataRoot = $matches[1].Trim().Trim("'").Trim('"')
37
+ break
38
+ }
39
+ }
40
+ }
41
+ if (-not $configuredDataRoot) {
42
+ $dshHome = Join-Path $HOME '.dsh'
43
+ $configuredDataRoot = if ($Profile -eq 'desktop') { Join-Path $dshHome 'cortex' } else { Join-Path (Join-Path $dshHome 'cortex\profiles') $Profile }
44
+ }
45
+
46
+ if (-not $KeepPatch) {
47
+ if (Test-Path $patchFile) {
48
+ $lines = [System.IO.File]::ReadAllLines($patchFile)
49
+ $ranges = New-Object System.Collections.Generic.List[object]
50
+ $start = -1
51
+ for ($i = 0; $i -lt $lines.Count; $i += 1) {
52
+ if ($lines[$i] -match '^- ') {
53
+ if ($start -ge 0) { $ranges.Add([pscustomobject]@{ From = $start; To = ($i - 1) }) }
54
+ $start = $i
55
+ }
56
+ }
57
+ if ($start -ge 0) { $ranges.Add([pscustomobject]@{ From = $start; To = ($lines.Count - 1) }) }
58
+
59
+ $drop = @{}
60
+ foreach ($range in $ranges) {
61
+ $from = [int]$range.From
62
+ $to = [int]$range.To
63
+ $header = $lines[$from]
64
+ $body = ($lines[$from..$to] -join "`n")
65
+ if ($header -match '^- id:\s*cortex\s*$' -and $body -match 'name:\s*[''"]?dsh-cortex') {
66
+ for ($i = $from; $i -le $to; $i += 1) { $drop[$i] = $true }
67
+ }
68
+ }
69
+
70
+ if ($drop.Count -gt 0) {
71
+ $remaining = @()
72
+ for ($i = 0; $i -lt $lines.Count; $i += 1) {
73
+ if (-not $drop.ContainsKey($i)) { $remaining += $lines[$i] }
74
+ }
75
+ $meaningful = @($remaining | Where-Object { $_.Trim() -and -not $_.Trim().StartsWith('#') })
76
+ $text = if ($meaningful.Count -eq 0) { "[]`n" } else { ($remaining -join "`n") + "`n" }
77
+ $utf8 = New-Object System.Text.UTF8Encoding($false)
78
+ [System.IO.File]::WriteAllText($patchFile, $text, $utf8)
79
+ Write-Host "removed cortex patch block from $patchFile"
80
+ } else {
81
+ Write-Host "no cortex patch block found in $patchFile"
82
+ }
83
+ }
84
+
85
+ & $dshPath --profile $Profile --dump-config | Out-Null
86
+ if ($LASTEXITCODE -ne 0) { Write-Warning "dump-config still fails; inspect $profilePath\cordis.patch.yml" }
87
+ else { Write-Host "dump-config ok" }
88
+ }
89
+
90
+ Write-Host "profile left in place: $profilePath"
91
+ Write-Host "cortex data was NOT deleted: $configuredDataRoot"
package/index.js CHANGED
@@ -22,12 +22,18 @@ export const Config = Schema ? Schema.object({
22
22
  dataRoot: Schema.string().description('Cortex 数据目录(默认 DSH_HOME/cortex)'),
23
23
  dbFile: Schema.string().description('SQLite 文件;默认 dataRoot/cortex.sqlite'),
24
24
  scope: Schema.string().default('global').description('默认 scope'),
25
+ readOnlyScopes: Schema.array(Schema.string()).default([]).description('只读共享 scope 列表'),
26
+ scopeWriteGrants: Schema.array(Schema.any()).default([]).description('显式写授权:scope:agent 或 {grant, until}'),
25
27
  contextBudget: Schema.natural().default(2200).description('检索注入的 token 预算'),
26
28
  promptBudget: Schema.natural().default(1600).description('常驻自我/工作记忆提示词预算'),
27
29
  recallTimeoutMs: Schema.natural().default(1200).description('前台召回硬截止'),
28
30
  asyncJobs: Schema.boolean().default(true).description('允许后台任务'),
29
31
  captureSubagents: Schema.boolean().default(false).description('是否把子代理会话也写入记忆'),
30
32
  vectorEnabled: Schema.boolean().default(false).description('启用向量检索(默认用离线 hash embedder 兜底)'),
33
+ vectorAnnMin: Schema.natural().default(20000).description('向量 ANN 生效的最小条数'),
34
+ vectorAnnEngine: Schema.string().default('hnsw').description('ANN 引擎:hnsw(默认)或 nsw(旧实现)'),
35
+ vectorAnnEndpoint: Schema.string().default('').description('外置 ANN HTTP endpoint;空则不用外置'),
36
+ vectorAnnTimeoutMs: Schema.natural().default(800).description('外置 ANN 超时毫秒'),
31
37
  scheduler: Schema.boolean().default(true).description('启用宿主心跳(安全自主 tick/作业/保留计划)'),
32
38
  schedulerIntervalMs: Schema.natural().default(60000).description('心跳间隔毫秒'),
33
39
  maxCallsPerHour: Schema.natural().default(120).description('检索调用预算(宿主 prefetch 不计入)'),
@@ -48,6 +54,9 @@ export const Config = Schema ? Schema.object({
48
54
  reflect: Schema.boolean().default(true),
49
55
  growth: Schema.boolean().default(true),
50
56
  vector: Schema.boolean().default(false),
57
+ vectorAnn: Schema.boolean().default(false),
58
+ vectorAnnExternal: Schema.boolean().default(false),
59
+ attachmentReader: Schema.boolean().default(false),
51
60
  graph: Schema.boolean().default(true),
52
61
  llm: Schema.boolean().default(false),
53
62
  api: Schema.boolean().default(true),
@@ -109,7 +118,11 @@ export async function apply(ctx, config = {}) {
109
118
  for (const tool of createToolDefinitions(cortex)) disposers.push(ctx.tools.register(tool));
110
119
  if (runtime) disposers.push(runtime.attach(ctx));
111
120
  if (scheduler) scheduler.start();
112
- if (config.features?.api !== false) disposers.push(mountApi(ctx, { cortex, logger: ctx.logger }));
121
+ if (config.features?.api !== false) disposers.push(mountApi(ctx, {
122
+ cortex,
123
+ logger: ctx.logger,
124
+ getExtractor: () => ctx.get?.('cortexExtractor') ?? null,
125
+ }));
113
126
  if (typeof ctx.systemPrompt?.section === 'function') {
114
127
  disposers.push(ctx.systemPrompt.section({
115
128
  name: 'cortex-self',
@@ -125,6 +138,7 @@ export async function apply(ctx, config = {}) {
125
138
  search: cortex.search,
126
139
  get: cortex.get,
127
140
  write: cortex.write,
141
+ ingestExtractedText: cortex.ingestExtractedText,
128
142
  self: cortex.self,
129
143
  health: cortex.health,
130
144
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-cortex-memory",
3
- "version": "0.1.0",
3
+ "version": "0.2.0-rc.13",
4
4
  "description": "A layered, temporal, self-developing memory engine for DeepSeek Harness: working, episodic, semantic, procedural and reflective memory with hybrid retrieval, DSR forgetting, reflection, consolidation, gated self-evolution and guardian.",
5
5
  "type": "module",
6
6
  "main": "./index.js",
@@ -28,7 +28,9 @@
28
28
  "README.md",
29
29
  "docs/install-windows.md",
30
30
  "docs/models.md",
31
- "docs/compatibility.md",
31
+ "docs/scopes.md",
32
+ "docs/multimodal.md",
33
+ "examples",
32
34
  "LICENSE"
33
35
  ],
34
36
  "scripts": {
@@ -72,14 +74,5 @@
72
74
  "self-evolving",
73
75
  "sqlite"
74
76
  ],
75
- "license": "MIT",
76
- "author": "dsh-cortex-memory contributors",
77
- "repository": {
78
- "type": "git",
79
- "url": "https://github.com/Lh1314-China/dsh-cortex-memory.git"
80
- },
81
- "homepage": "https://github.com/Lh1314-China/dsh-cortex-memory#readme",
82
- "bugs": {
83
- "url": "https://github.com/Lh1314-China/dsh-cortex-memory/issues"
84
- }
77
+ "license": "MIT"
85
78
  }
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { hashText } from '../core/ids.js';
4
+ import { normalizeAttachments } from '../core/attachments.js';
4
5
 
5
6
  function parseFrontmatter(text) {
6
7
  const source = String(text ?? '');
@@ -14,6 +15,10 @@ function parseFrontmatter(text) {
14
15
  const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
15
16
  if (!match) continue;
16
17
  let value = match[2].trim();
18
+ if (match[1] === 'attachments') {
19
+ meta.attachments = value;
20
+ continue;
21
+ }
17
22
  if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) value = value.slice(1, -1);
18
23
  if (value.startsWith('[') && value.endsWith(']')) value = value.slice(1, -1).split(',').map((item) => item.trim().replace(/^["']|["']$/g, '')).filter(Boolean);
19
24
  meta[match[1]] = value;
@@ -24,6 +29,14 @@ function parseFrontmatter(text) {
24
29
  export function parseMemoryMarkdown(text) {
25
30
  const { meta, body } = parseFrontmatter(text);
26
31
  const title = meta.title || body.split(/\r?\n/).find((line) => line.startsWith('# '))?.slice(2).trim() || '未命名记忆';
32
+ let attachments = [];
33
+ if (meta.attachments) {
34
+ try {
35
+ attachments = normalizeAttachments(JSON.parse(meta.attachments));
36
+ } catch {
37
+ attachments = [];
38
+ }
39
+ }
27
40
  return {
28
41
  id: meta.id || null,
29
42
  kind: meta.kind || 'utterance',
@@ -33,6 +46,7 @@ export function parseMemoryMarkdown(text) {
33
46
  importance: Number(meta.importance ?? 0.5),
34
47
  recordedAt: meta.recorded_at || meta.date || null,
35
48
  state: meta.state || 'active',
49
+ attachments,
36
50
  };
37
51
  }
38
52
 
@@ -103,6 +117,7 @@ export function importMarkdown(cortex, root, { dryRun = false, scopeId = 'global
103
117
  kind: parsed.kind,
104
118
  tags: parsed.tags,
105
119
  importance: parsed.importance,
120
+ attachments: parsed.attachments,
106
121
  explicit: true,
107
122
  operationId: `markdown:${parsed.id ?? hashText(`${file}:${parsed.title}:${parsed.content}`)}`,
108
123
  now: parsed.recordedAt ? Date.parse(parsed.recordedAt) : undefined,
@@ -142,10 +157,12 @@ export function syncMarkdown(cortex, root, { dryRun = false, scopeId = 'global',
142
157
  if (parsed.id) {
143
158
  const current = cortex.repos.traces.get(parsed.id);
144
159
  if (current) {
160
+ const currentAttachments = current.metadata?.attachments ?? [];
145
161
  const changed = current.content.trim() !== parsed.content.trim()
146
162
  || (current.title ?? '') !== (parsed.title ?? '')
147
163
  || JSON.stringify(current.tags ?? []) !== JSON.stringify(parsed.tags ?? [])
148
- || Number(current.importance) !== Number(parsed.importance);
164
+ || Number(current.importance) !== Number(parsed.importance)
165
+ || JSON.stringify(currentAttachments) !== JSON.stringify(parsed.attachments ?? []);
149
166
  if (!changed) {
150
167
  items.push({ file, action: 'unchanged', traceId: parsed.id });
151
168
  continue;
@@ -156,6 +173,7 @@ export function syncMarkdown(cortex, root, { dryRun = false, scopeId = 'global',
156
173
  content: parsed.content,
157
174
  tags: parsed.tags,
158
175
  importance: parsed.importance,
176
+ metadata: { ...(current.metadata ?? {}), attachments: parsed.attachments },
159
177
  });
160
178
  cortex.repos.audit?.record?.({ actor: 'user', action: 'markdown.sync.update', targetType: 'trace', targetId: parsed.id, detail: { file } });
161
179
  }
@@ -171,6 +189,7 @@ export function syncMarkdown(cortex, root, { dryRun = false, scopeId = 'global',
171
189
  kind: parsed.kind,
172
190
  tags: parsed.tags,
173
191
  importance: parsed.importance,
192
+ attachments: parsed.attachments,
174
193
  explicit: true,
175
194
  operationId: `markdown:${parsed.id ?? hashText(`${file}:${parsed.title}:${parsed.content}`)}`,
176
195
  now: parsed.recordedAt ? Date.parse(parsed.recordedAt) : undefined,
@@ -14,7 +14,8 @@ function writeAtomic(file, content) {
14
14
  }
15
15
 
16
16
  export function traceToMarkdown(trace) {
17
- return [
17
+ const attachments = Array.isArray(trace.metadata?.attachments) ? trace.metadata.attachments : [];
18
+ const lines = [
18
19
  '---',
19
20
  `id: "${front(trace.id)}"`,
20
21
  `kind: "${front(trace.kind)}"`,
@@ -26,11 +27,20 @@ export function traceToMarkdown(trace) {
26
27
  `importance: ${Number(trace.importance ?? 0)}`,
27
28
  `confidence: ${Number(trace.confidence ?? 0)}`,
28
29
  `tags: [${(trace.tags ?? []).map((tag) => `"${front(tag)}"`).join(', ')}]`,
30
+ ...(attachments.length ? [`attachments: ${JSON.stringify(attachments)}`] : []),
29
31
  '---',
30
32
  '',
31
33
  trace.content ?? '',
32
34
  '',
33
- ].join('\n');
35
+ ];
36
+ if (attachments.length) {
37
+ lines.push('## Attachments (metadata only)', '');
38
+ for (const item of attachments) {
39
+ lines.push(`- ${item.kind}: ${item.name ?? item.uri}${item.mime ? ` (${item.mime})` : ''}${item.bytes ? ` · ${item.bytes} B` : ''}`);
40
+ }
41
+ lines.push('');
42
+ }
43
+ return lines.join('\n');
34
44
  }
35
45
 
36
46
  export function exportMarkdown(cortex, targetDir, { scopeId = 'global' } = {}) {
@@ -43,7 +43,7 @@ export function classifySparkCard(card = {}) {
43
43
  const text = `${card.title ?? ''} ${card.content ?? card.contentPreview ?? card.summary ?? ''} ${(card.triggers ?? []).join(' ')}`;
44
44
  if (/应该|必须|不要|先|再|每次|以后|记得|避免|优先|禁止|务必|规则|教训/.test(text)) return 'instruction';
45
45
  if (/插件|安装|配置|命令|路径|报错|测试|接口|版本|构建|项目|函数|文件|系统|git|npm|dsh|api|bug|fix|build|deploy|代码|模块|记忆区域|海马体|语义|面板|开关|检索|时间线|叙事|焦点|健康|体检|策略|权限|工具|上下文|注入|向量|图谱|工作卡|火花|协议|服务|进程|数据库|SQLite|脚本|日志/i.test(text)) return 'observation';
46
- if (/用户|助手|喜欢|爱|难过|开心|心疼|陪|声音|米饭|抱|想|梦|日记|称呼|相处|情绪|温暖|孤单|甜|笑|哭|晚安|早安/.test(text)) return 'preference';
46
+ if (/主人|本鲸|喜欢|爱|难过|开心|心疼|陪|声音|白饭|抱|想|梦|日记|称呼|相处|情绪|温暖|孤单|甜|笑|哭|晚安|早安/.test(text)) return 'preference';
47
47
  return 'utterance';
48
48
  }
49
49