dsh-data-domain-knowledge 0.1.0-alpha.11 → 0.1.0-alpha.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 (147) hide show
  1. package/README.md +64 -13
  2. package/eval/README.md +19 -8
  3. package/eval/evaluate-retrieval.mjs +17 -8
  4. package/eval/queries.example.jsonl +1 -0
  5. package/lib/client.js +2294 -2096
  6. package/lib/doc-page.js +1 -1
  7. package/lib/pages.js +2 -2
  8. package/lib/types/axis-registry.d.ts +9 -4
  9. package/lib/types/axis-registry.d.ts.map +1 -1
  10. package/lib/types/axis-registry.js +62 -23
  11. package/lib/types/axis-registry.js.map +1 -1
  12. package/lib/types/card-organize.d.ts +5 -0
  13. package/lib/types/card-organize.d.ts.map +1 -1
  14. package/lib/types/card-organize.js +15 -0
  15. package/lib/types/card-organize.js.map +1 -1
  16. package/lib/types/compiler/embedding-identity.d.ts +3 -0
  17. package/lib/types/compiler/embedding-identity.d.ts.map +1 -0
  18. package/lib/types/compiler/embedding-identity.js +24 -0
  19. package/lib/types/compiler/embedding-identity.js.map +1 -0
  20. package/lib/types/compiler/snapshot.d.ts +16 -0
  21. package/lib/types/compiler/snapshot.d.ts.map +1 -0
  22. package/lib/types/compiler/snapshot.js +2 -0
  23. package/lib/types/compiler/snapshot.js.map +1 -0
  24. package/lib/types/diagnosis/store.d.ts.map +1 -1
  25. package/lib/types/diagnosis/store.js +2 -0
  26. package/lib/types/diagnosis/store.js.map +1 -1
  27. package/lib/types/environment/install.d.ts +7 -0
  28. package/lib/types/environment/install.d.ts.map +1 -0
  29. package/lib/types/environment/install.js +172 -0
  30. package/lib/types/environment/install.js.map +1 -0
  31. package/lib/types/environment/manifest.d.ts +208 -0
  32. package/lib/types/environment/manifest.d.ts.map +1 -0
  33. package/lib/types/environment/manifest.js +26 -0
  34. package/lib/types/environment/manifest.js.map +1 -0
  35. package/lib/types/environment/model-catalog.d.ts +191 -0
  36. package/lib/types/environment/model-catalog.d.ts.map +1 -0
  37. package/lib/types/environment/model-catalog.js +91 -0
  38. package/lib/types/environment/model-catalog.js.map +1 -0
  39. package/lib/types/environment/process.d.ts +10 -0
  40. package/lib/types/environment/process.d.ts.map +1 -0
  41. package/lib/types/environment/process.js +54 -0
  42. package/lib/types/environment/process.js.map +1 -0
  43. package/lib/types/environment/selection.d.ts +9 -0
  44. package/lib/types/environment/selection.d.ts.map +1 -0
  45. package/lib/types/environment/selection.js +29 -0
  46. package/lib/types/environment/selection.js.map +1 -0
  47. package/lib/types/environment/service.d.ts +47 -0
  48. package/lib/types/environment/service.d.ts.map +1 -0
  49. package/lib/types/environment/service.js +253 -0
  50. package/lib/types/environment/service.js.map +1 -0
  51. package/lib/types/environment/types.d.ts +37 -0
  52. package/lib/types/environment/types.d.ts.map +1 -0
  53. package/lib/types/environment/types.js +2 -0
  54. package/lib/types/environment/types.js.map +1 -0
  55. package/lib/types/events.d.ts +10 -0
  56. package/lib/types/events.d.ts.map +1 -1
  57. package/lib/types/events.js +38 -1
  58. package/lib/types/events.js.map +1 -1
  59. package/lib/types/images.d.ts +26 -0
  60. package/lib/types/images.d.ts.map +1 -0
  61. package/lib/types/images.js +196 -0
  62. package/lib/types/images.js.map +1 -0
  63. package/lib/types/index.d.ts.map +1 -1
  64. package/lib/types/index.js +59 -3
  65. package/lib/types/index.js.map +1 -1
  66. package/lib/types/knowledge-bases.d.ts +3 -0
  67. package/lib/types/knowledge-bases.d.ts.map +1 -1
  68. package/lib/types/knowledge-bases.js +9 -0
  69. package/lib/types/knowledge-bases.js.map +1 -1
  70. package/lib/types/knowledge.d.ts +14 -6
  71. package/lib/types/knowledge.d.ts.map +1 -1
  72. package/lib/types/knowledge.js +56 -39
  73. package/lib/types/knowledge.js.map +1 -1
  74. package/lib/types/layout.d.ts +2 -0
  75. package/lib/types/layout.d.ts.map +1 -1
  76. package/lib/types/layout.js +2 -0
  77. package/lib/types/layout.js.map +1 -1
  78. package/lib/types/retrieval/vector-index.d.ts +31 -0
  79. package/lib/types/retrieval/vector-index.d.ts.map +1 -0
  80. package/lib/types/retrieval/vector-index.js +15 -0
  81. package/lib/types/retrieval/vector-index.js.map +1 -0
  82. package/lib/types/retrieval/visibility.d.ts +6 -0
  83. package/lib/types/retrieval/visibility.d.ts.map +1 -0
  84. package/lib/types/retrieval/visibility.js +11 -0
  85. package/lib/types/retrieval/visibility.js.map +1 -0
  86. package/lib/types/runtime.d.ts +36 -6
  87. package/lib/types/runtime.d.ts.map +1 -1
  88. package/lib/types/runtime.js +520 -190
  89. package/lib/types/runtime.js.map +1 -1
  90. package/lib/types/source-evidence.d.ts +6 -0
  91. package/lib/types/source-evidence.d.ts.map +1 -0
  92. package/lib/types/source-evidence.js +28 -0
  93. package/lib/types/source-evidence.js.map +1 -0
  94. package/lib/types/store/axis-references.d.ts +5 -0
  95. package/lib/types/store/axis-references.d.ts.map +1 -0
  96. package/lib/types/store/axis-references.js +106 -0
  97. package/lib/types/store/axis-references.js.map +1 -0
  98. package/lib/types/store/derived.d.ts +11 -0
  99. package/lib/types/store/derived.d.ts.map +1 -1
  100. package/lib/types/store/derived.js +31 -6
  101. package/lib/types/store/derived.js.map +1 -1
  102. package/lib/types/store/knowledge-db.d.ts +4 -1
  103. package/lib/types/store/knowledge-db.d.ts.map +1 -1
  104. package/lib/types/store/knowledge-db.js +15 -9
  105. package/lib/types/store/knowledge-db.js.map +1 -1
  106. package/lib/types/store/migrate.d.ts.map +1 -1
  107. package/lib/types/store/migrate.js +16 -5
  108. package/lib/types/store/migrate.js.map +1 -1
  109. package/lib/types/types.d.ts +7 -1
  110. package/lib/types/types.d.ts.map +1 -1
  111. package/lib/types/types.js +3 -4
  112. package/lib/types/types.js.map +1 -1
  113. package/package.json +3 -1
  114. package/python/prepare-models.py +19 -0
  115. package/python/requirements.txt +8 -6
  116. package/scripts/doctor.mjs +14 -2
  117. package/scripts/environment.mjs +29 -0
  118. package/scripts/setup-embedding.sh +11 -2
  119. package/src/axis-registry.ts +59 -23
  120. package/src/card-organize.ts +15 -0
  121. package/src/client/EnvironmentSetup.tsx +145 -0
  122. package/src/client/KnowledgePanel.tsx +9 -4
  123. package/src/compiler/embedding-identity.ts +21 -0
  124. package/src/compiler/snapshot.ts +17 -0
  125. package/src/diagnosis/store.ts +1 -0
  126. package/src/environment/install.ts +129 -0
  127. package/src/environment/manifest.ts +28 -0
  128. package/src/environment/model-catalog.ts +105 -0
  129. package/src/environment/process.ts +54 -0
  130. package/src/environment/selection.ts +29 -0
  131. package/src/environment/service.ts +211 -0
  132. package/src/environment/types.ts +34 -0
  133. package/src/events.ts +46 -0
  134. package/src/images.ts +203 -0
  135. package/src/index.ts +54 -3
  136. package/src/knowledge-bases.ts +9 -0
  137. package/src/knowledge.ts +56 -40
  138. package/src/layout.ts +2 -0
  139. package/src/retrieval/vector-index.ts +44 -0
  140. package/src/retrieval/visibility.ts +14 -0
  141. package/src/runtime.ts +473 -233
  142. package/src/source-evidence.ts +30 -0
  143. package/src/store/axis-references.ts +96 -0
  144. package/src/store/derived.ts +31 -6
  145. package/src/store/knowledge-db.ts +16 -9
  146. package/src/store/migrate.ts +15 -5
  147. package/src/types.ts +10 -5
@@ -0,0 +1,29 @@
1
+ import { KnowledgeEnvironment } from '../lib/types/environment/service.js'
2
+ import { KnowledgeRuntime, defaultKnowledgeRoot } from '../lib/types/runtime.js'
3
+ import { defaultAgentRoot, resourceRoot } from '../lib/types/layout.js'
4
+
5
+ const runtime = new KnowledgeRuntime(defaultKnowledgeRoot(), {}, { resourceRoot: resourceRoot(defaultAgentRoot()) })
6
+ const environment = new KnowledgeEnvironment({ resourceRoot: resourceRoot(defaultAgentRoot()),
7
+ snapshot: () => ({ config: runtime.config, paths: runtime.resourcePaths() }),
8
+ probe: kind => runtime.environmentProbe(kind), reload: () => runtime.reloadResources(),
9
+ })
10
+ const stop = () => environment.cancel()
11
+ process.once('SIGINT', stop)
12
+ process.once('SIGTERM', stop)
13
+ const action = process.argv.includes('--install') ? 'install' : 'check'
14
+ environment.start(action)
15
+ let last = ''
16
+ const timer = setInterval(() => {
17
+ const view = environment.view()
18
+ const line = `${view.job?.stage ?? ''}: ${view.job?.logs.at(-1) ?? ''}`
19
+ if (line !== last) { process.stderr.write(`${line}\n`); last = line }
20
+ }, 1000)
21
+ try {
22
+ await environment.wait()
23
+ const view = environment.view()
24
+ console.log(JSON.stringify(view, null, 2))
25
+ if (view.job?.state !== 'succeeded') process.exitCode = 1
26
+ } finally {
27
+ clearInterval(timer); environment.close(); runtime.close()
28
+ process.removeListener('SIGINT', stop); process.removeListener('SIGTERM', stop)
29
+ }
@@ -9,11 +9,20 @@ DSH_HOME_ROOT="${DSH_HOME:-$HOME/.dsh}"
9
9
  TITA_HOME="$DSH_HOME_ROOT/tita-agent"
10
10
  VENV="${DSH_EMBEDDING_VENV:-$TITA_HOME/resource/python}"
11
11
  MODEL_DIR="${DSH_EMBEDDING_MODEL_PATH:-$TITA_HOME/resource/models/WeMM-Embedding-2B-Quantized}"
12
+ BASE_PYTHON="${DSH_PYTHON:-python3}"
12
13
  PYTHON_BIN="$VENV/bin/python"
13
14
 
14
15
  mkdir -p "$TITA_HOME" "$MODEL_DIR"
15
- if [ ! -x "$PYTHON_BIN" ]; then
16
- uv venv --python 3.11 "$VENV"
16
+ BASE_VERSION="$("$BASE_PYTHON" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")')"
17
+ if [[ ! "$BASE_VERSION" =~ ^3\.(10|11|12|13)$ ]]; then
18
+ echo "Unsupported base Python version: $BASE_VERSION (need 3.10–3.13)" >&2
19
+ exit 1
20
+ fi
21
+ if [ ! -x "$PYTHON_BIN" ] || [ "$("$PYTHON_BIN" -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || true)" != "$BASE_VERSION" ]; then
22
+ if [ -d "$VENV" ]; then
23
+ mv "$VENV" "$VENV-backup-$(date +%Y%m%d%H%M%S)-$$"
24
+ fi
25
+ uv venv --python "$BASE_PYTHON" --python-preference only-system "$VENV"
17
26
  fi
18
27
  uv pip install --python "$PYTHON_BIN" -r "$ROOT/python/requirements.txt"
19
28
 
@@ -34,14 +34,13 @@
34
34
  * ## 为什么存在记忆段
35
35
  *
36
36
  * 注册表**不可重建**:它是人确认过的命名决定(改 canonical name、并两个实体、加一个别名),
37
- * 重跑一次抽取不会得到同样的结果。所以它和操作日志、抽取轨迹一样落在
38
- * `<memoryDir>/knowledge.sqlite`,不在 `03_compiled/`。
37
+ * 重跑一次抽取不会得到同样的结果。所以它与 Knowledge 共享 `knowledge.db`,
38
+ * 经过同一个属主与 schema 版本入口校验。
39
39
  */
40
40
 
41
41
  import { createHash } from 'node:crypto'
42
- import { mkdir } from 'node:fs/promises'
43
- import { dirname } from 'node:path'
44
42
  import { loadSqlite, type SqliteDatabase } from './fts.js'
43
+ import { KnowledgeDb } from './store/knowledge-db.js'
45
44
 
46
45
  /** 三个轴。三者地位相同(§3.1)—— 没有哪个更特殊。 */
47
46
  export type AxisKind = 'object' | 'action' | 'scene'
@@ -252,20 +251,27 @@ CREATE INDEX IF NOT EXISTS idx_axis_relation_to ON axis_relations(to_id, relatio
252
251
  */
253
252
  export class AxisRegistry {
254
253
  private db: SqliteDatabase | null = null
254
+ private owner: KnowledgeDb | null = null
255
255
 
256
- constructor(private readonly file: string) {}
256
+ constructor(private readonly file: string, private readonly shared?: SqliteDatabase) {}
257
257
 
258
258
  async open(): Promise<boolean> {
259
259
  if (this.db !== null) return true
260
260
  const sqlite = await loadSqlite()
261
261
  if (sqlite === null) return false
262
262
  try {
263
- await mkdir(dirname(this.file), { recursive: true })
264
- this.db = new sqlite.DatabaseSync(this.file)
263
+ if (this.shared) this.db = this.shared
264
+ else {
265
+ const opened = await KnowledgeDb.open(this.file, sqlite.DatabaseSync)
266
+ if (!opened.ok) return false
267
+ this.owner = opened.db
268
+ this.db = opened.db.connection()
269
+ }
265
270
  this.db.exec(SCHEMA)
271
+ this.db.exec('CREATE TABLE IF NOT EXISTS axis_input_revision (id INTEGER PRIMARY KEY CHECK(id = 1), revision INTEGER NOT NULL); INSERT OR IGNORE INTO axis_input_revision VALUES (1, 0)')
266
272
  /*
267
- * 补列。这个库**没有 `user_version` 那一套**(它是记忆段里的一张注册表,不是知识本体),
268
- * 所以判据落在列上:缺就补,补齐之后什么都不做。`CREATE TABLE IF NOT EXISTS` 对
273
+ * 主库已通过 `user_version` 校验,再幂等补齐存量注册表的扩展列。缺就补。
274
+ * `CREATE TABLE IF NOT EXISTS` 对
269
275
  * 已经存在的表一个字都不改 —— 少了这一段,0068 之前建的库会永远没有这两列,
270
276
  * 而症状是「定义存进去了,下次打开又没了」。
271
277
  */
@@ -277,11 +283,19 @@ export class AxisRegistry {
277
283
  }
278
284
  return true
279
285
  } catch {
286
+ this.owner?.close()
287
+ this.owner = null
280
288
  this.db = null
281
289
  return false
282
290
  }
283
291
  }
284
292
 
293
+ inputRevision(): number {
294
+ return Number((this.db?.prepare('SELECT revision FROM axis_input_revision WHERE id = 1').get() as { revision?: number } | undefined)?.revision ?? 0)
295
+ }
296
+
297
+ private changedInput(): void { this.db?.prepare('UPDATE axis_input_revision SET revision = revision + 1 WHERE id = 1').run() }
298
+
285
299
  available(): boolean {
286
300
  return this.db !== null
287
301
  }
@@ -343,7 +357,7 @@ export class AxisRegistry {
343
357
 
344
358
  // 2 · 作用域内 alias
345
359
  if (parentId) {
346
- const aliased = this.findAlias(normalized, parentId)
360
+ const aliased = this.findAlias(input.kind, normalized, parentId)
347
361
  if (aliased) {
348
362
  this.learn(aliased, mention, normalized, parentId, input.description)
349
363
  return { mention, normalized, outcome: 'matched', entity: aliased, how: 'scoped-alias' }
@@ -557,6 +571,7 @@ export class AxisRegistry {
557
571
  const now = new Date().toISOString()
558
572
  this.db.prepare('UPDATE axis_entities SET name = ?, updated_at = ? WHERE id = ?').run(next, now, id)
559
573
  this.insertAlias(id, current.name, normalizeAxisName(current.name), current.scopeId, now)
574
+ this.changedInput()
560
575
  return this.get(id)
561
576
  }
562
577
 
@@ -572,6 +587,7 @@ export class AxisRegistry {
572
587
  const normalized = normalizeAxisName(alias)
573
588
  if (!normalized) return false
574
589
  this.insertAlias(id, alias.trim(), normalized, scopeId.trim(), new Date().toISOString())
590
+ this.changedInput()
575
591
  return true
576
592
  }
577
593
 
@@ -583,6 +599,7 @@ export class AxisRegistry {
583
599
  async setStatus(id: string, status: AxisStatus): Promise<AxisEntity | null> {
584
600
  if (this.db === null) throw new Error('AXIS_REGISTRY_UNAVAILABLE')
585
601
  this.db.prepare('UPDATE axis_entities SET status = ?, updated_at = ? WHERE id = ?').run(status, new Date().toISOString(), id)
602
+ this.changedInput()
586
603
  return this.get(id)
587
604
  }
588
605
 
@@ -593,19 +610,34 @@ export class AxisRegistry {
593
610
  * 它转成 `merged`,`scopeId` 改存目标 id —— 复用这一列而不是新加一列,因为
594
611
  * 「它现在归谁」与「它在谁的下面」对解析来说问的是同一件事。
595
612
  */
596
- async merge(fromId: string, intoId: string): Promise<boolean> {
613
+ async merge(fromId: string, intoId: string, redirect?: (kind: AxisKind) => void): Promise<boolean> {
597
614
  if (this.db === null) throw new Error('AXIS_REGISTRY_UNAVAILABLE')
598
615
  if (fromId === intoId) return false
599
616
  const [from, into] = [await this.get(fromId), await this.get(intoId)]
600
- if (from === null || into === null || from.kind !== into.kind) return false
617
+ if (from === null || into === null || from.kind !== into.kind || from.status === 'merged' || into.status === 'merged') return false
601
618
  const now = new Date().toISOString()
602
- this.db.prepare('UPDATE axis_entities SET status = ?, scope_id = ?, updated_at = ? WHERE id = ?').run('merged', intoId, now, fromId)
603
- // 被并者的别名与名字一起搬过去:按旧名、旧别名都要搜得到。
604
- this.insertAlias(intoId, from.name, normalizeAxisName(from.name), into.scopeId, now)
605
- const aliases = this.db.prepare('SELECT alias, scope_id FROM axis_aliases WHERE entity_id = ?').all(fromId) as Array<Record<string, unknown>>
606
- for (const row of aliases) this.insertAlias(intoId, String(row.alias), normalizeAxisName(String(row.alias)), String(row.scope_id ?? ''), now)
607
- this.redirectRelations(fromId, intoId, now)
608
- return true
619
+ this.db.exec('SAVEPOINT axis_merge')
620
+ try {
621
+ this.db.prepare('UPDATE axis_entities SET status = ?, scope_id = ?, updated_at = ? WHERE id = ?').run('merged', intoId, now, fromId)
622
+ // 被并者的别名与名字一起搬过去:按旧名、旧别名都要搜得到。
623
+ this.insertAlias(intoId, from.name, normalizeAxisName(from.name), into.scopeId, now)
624
+ const aliases = this.db.prepare('SELECT alias, scope_id FROM axis_aliases WHERE entity_id = ?').all(fromId) as Array<Record<string, unknown>>
625
+ for (const row of aliases) this.insertAlias(intoId, String(row.alias), normalizeAxisName(String(row.alias)), String(row.scope_id ?? ''), now)
626
+ this.redirectRelations(fromId, intoId, now)
627
+ redirect?.(from.kind)
628
+ // Scope references on both canonical names and aliases move together.
629
+ this.db.prepare("UPDATE axis_entities SET scope_id = ? WHERE scope_id = ? AND status <> 'merged'").run(intoId, fromId)
630
+ this.db.prepare(`INSERT OR IGNORE INTO axis_aliases (entity_id, alias, normalized, scope_id, created_at)
631
+ SELECT entity_id, alias, normalized, ?, created_at FROM axis_aliases WHERE scope_id = ?`).run(intoId, fromId)
632
+ this.db.prepare('DELETE FROM axis_aliases WHERE scope_id = ?').run(fromId)
633
+ this.db.prepare('UPDATE entity_guidance SET entity_id = ? WHERE entity_id = ?').run(intoId, fromId)
634
+ this.changedInput()
635
+ this.db.exec('RELEASE axis_merge')
636
+ return true
637
+ } catch (cause) {
638
+ this.db.exec('ROLLBACK TO axis_merge; RELEASE axis_merge')
639
+ throw cause
640
+ }
609
641
  }
610
642
 
611
643
  /** 合并轴实体时转移相关关系,并折叠重复边与自环。 */
@@ -642,6 +674,7 @@ export class AxisRegistry {
642
674
  const next = text.trim()
643
675
  this.db.prepare('UPDATE axis_entities SET description = ?, description_source = ?, updated_at = ? WHERE id = ?')
644
676
  .run(next, next ? 'human' : '', new Date().toISOString(), id)
677
+ this.changedInput()
645
678
  return this.get(id)
646
679
  }
647
680
 
@@ -732,6 +765,7 @@ export class AxisRegistry {
732
765
  }
733
766
  this.db.prepare('INSERT INTO entity_guidance (id, entity_id, kind, text, author, created_at) VALUES (?, ?, ?, ?, ?, ?)')
734
767
  .run(guidance.id, guidance.entityId, guidance.kind, guidance.text, guidance.author, guidance.createdAt)
768
+ this.changedInput()
735
769
  return guidance
736
770
  }
737
771
 
@@ -741,6 +775,7 @@ export class AxisRegistry {
741
775
  const before = this.db.prepare('SELECT id FROM entity_guidance WHERE id = ?').get(id) as { id?: unknown } | undefined
742
776
  if (before === undefined) return false
743
777
  this.db.prepare('DELETE FROM entity_guidance WHERE id = ?').run(id)
778
+ this.changedInput()
744
779
  return true
745
780
  }
746
781
 
@@ -829,7 +864,8 @@ export class AxisRegistry {
829
864
  }
830
865
 
831
866
  close(): void {
832
- this.db?.close()
867
+ this.owner?.close()
868
+ this.owner = null
833
869
  this.db = null
834
870
  }
835
871
 
@@ -843,10 +879,10 @@ export class AxisRegistry {
843
879
  return hit === undefined ? undefined : rowToEntity(hit)
844
880
  }
845
881
 
846
- private findAlias(normalized: string, scopeId: string): AxisEntity | undefined {
882
+ private findAlias(kind: AxisKind, normalized: string, scopeId: string): AxisEntity | undefined {
847
883
  const row = this.db!.prepare(
848
- "SELECT e.* FROM axis_aliases a JOIN axis_entities e ON e.id = a.entity_id WHERE a.normalized = ? AND a.scope_id = ? AND e.status <> 'merged' LIMIT 1",
849
- ).get(normalized, scopeId) as Record<string, unknown> | undefined
884
+ "SELECT e.* FROM axis_aliases a JOIN axis_entities e ON e.id = a.entity_id WHERE a.normalized = ? AND a.scope_id = ? AND e.kind = ? AND e.status <> 'merged' LIMIT 1",
885
+ ).get(normalized, scopeId, kind) as Record<string, unknown> | undefined
850
886
  return row === undefined ? undefined : rowToEntity(row)
851
887
  }
852
888
 
@@ -428,6 +428,8 @@ export function checkOrganize(
428
428
  plan: OrganizePlan,
429
429
  known: string[],
430
430
  options: {
431
+ /** Check literal changes; this does not prove semantic entailment. */
432
+ knowledge?: Array<{ id: string; text: string }>
431
433
  /**
432
434
  * 正文里**可以**指向的其他卡片(`buildOrganizePrompt` 下发的那一份)。
433
435
  *
@@ -442,11 +444,24 @@ export function checkOrganize(
442
444
  const knownSet = new Set(known)
443
445
  const cited: string[] = []
444
446
  const seen = new Set<string>()
447
+ const byId = new Map(options.knowledge?.map(item => [item.id, item.text]) ?? [])
448
+ const literals = (text: string): string[] => [...text.replace(/\[\^[^\]]+\]/g, '').matchAll(/(?<![A-Za-z0-9_])(?:[<>≤≥]=?\s*)?[+-]?\d+(?:\.\d+)?(?:%|%)?/g)].map(match => match[0].replace(/\s/g, '').replace('%', '%'))
449
+ if (options.knowledge) {
450
+ const supported = new Set(literals(citationsIn(plan.summary).map(id => byId.get(id) ?? '').join('\n')))
451
+ const unsupported = literals(plan.summary).filter(value => !supported.has(value))
452
+ if (unsupported.length > 0) problems.push({ code: 'ORGANIZE_UNSUPPORTED_LITERAL', message: `概述的数值或比较条件没有引用依据:${[...new Set(unsupported)].join('、')}` })
453
+ }
445
454
 
446
455
  // 1 + 2:正文里的引用必须都存在,且每条知识都要落地。
447
456
  for (const section of plan.sections) {
448
457
  for (const block of section.blocks) {
449
458
  for (const text of blockTexts(block)) {
459
+ if (options.knowledge) {
460
+ const citedText = citationsIn(text).map(id => byId.get(id) ?? '').join('\n')
461
+ const supported = new Set(literals(citedText))
462
+ const unsupported = literals(text).filter(value => !supported.has(value))
463
+ if (unsupported.length > 0) problems.push({ code: 'ORGANIZE_UNSUPPORTED_LITERAL', message: `小节「${section.title}」的数值或比较条件没有引用依据:${[...new Set(unsupported)].join('、')}` })
464
+ }
450
465
  for (const id of citationsIn(text)) {
451
466
  if (!knownSet.has(id)) {
452
467
  problems.push({ code: 'ORGANIZE_UNKNOWN_CITATION', message: `小节「${section.title}」引用了这批知识里没有的 id「${id}」` })
@@ -0,0 +1,145 @@
1
+ import { useCallback, useEffect, useState, type ReactNode } from 'react'
2
+ import { Button, cn, Select, SelectContent, SelectItem, SelectLabel, SelectGroup, SelectTrigger, SelectValue } from '@tita-agent/ui/components'
3
+ import { AlertCircle, Check, ChevronDown, Download, LoaderCircle, RefreshCw } from '@tita-agent/ui/icons'
4
+ import type { ModelSelection, ModelOptionView } from '../environment/model-catalog.js'
5
+ import type { EnvironmentView } from '../environment/types.js'
6
+
7
+ async function environmentRequest(init?: RequestInit): Promise<EnvironmentView> {
8
+ const response = await fetch(init ? '/knowledge/environment/prepare' : '/knowledge/environment', init)
9
+ const payload = await response.json() as { ok?: boolean; data?: EnvironmentView; error?: { message?: string } }
10
+ if (!response.ok || !payload.ok || !payload.data) throw new Error(payload.error?.message ?? `环境服务响应失败(${response.status})`)
11
+ return payload.data
12
+ }
13
+
14
+ export function useKnowledgeEnvironment(visible: boolean, revision: number) {
15
+ const [view, setView] = useState<EnvironmentView>()
16
+ const [error, setError] = useState<string>()
17
+ const [submitting, setSubmitting] = useState(false)
18
+ const refresh = useCallback(async (): Promise<void> => {
19
+ try { setView(await environmentRequest()); setError(undefined) }
20
+ catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)) }
21
+ }, [])
22
+ const busy = submitting || view?.job?.state === 'running' || view?.report.state === 'checking'
23
+ useEffect(() => {
24
+ if (!visible) return
25
+ void refresh()
26
+ const onFocus = (): void => { void refresh() }
27
+ window.addEventListener('focus', onFocus)
28
+ const timer = busy ? setInterval(() => { void refresh() }, 1500) : undefined
29
+ return () => { if (timer) clearInterval(timer); window.removeEventListener('focus', onFocus) }
30
+ }, [visible, revision, busy, refresh])
31
+ const prepare = useCallback(async (action: 'install' | 'check' | 'cancel', selection?: ModelSelection): Promise<void> => {
32
+ setSubmitting(true)
33
+ try {
34
+ setView(await environmentRequest({ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action, ...(selection ? { selection } : {}) }) }))
35
+ setError(undefined)
36
+ } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)) }
37
+ finally { setSubmitting(false) }
38
+ }, [])
39
+ return { view, error, busy, prepare }
40
+ }
41
+
42
+ type EnvironmentController = ReturnType<typeof useKnowledgeEnvironment>
43
+
44
+ function ModelSelect({ id, label, value, options, disabled, onChange }: {
45
+ id: string
46
+ label: string
47
+ value: string
48
+ options: ModelOptionView[]
49
+ disabled: boolean
50
+ onChange: (value: string) => void
51
+ }): ReactNode {
52
+ const chosen = options.find(option => option.id === value)
53
+ const descriptionId = `${id}-description`
54
+ return <div className="grid min-w-0 gap-1.5">
55
+ <label htmlFor={id} className="text-xs font-medium text-tita-ink">{label}</label>
56
+ <Select value={value} onValueChange={onChange} disabled={disabled}>
57
+ <SelectTrigger id={id} aria-describedby={descriptionId} className="w-full"><SelectValue /></SelectTrigger>
58
+ <SelectContent position="popper" align="start" className="max-h-64">
59
+ <SelectGroup>
60
+ <SelectLabel>按资源占用从低到高</SelectLabel>
61
+ {options.map(option => <SelectItem key={option.id} value={option.id}>
62
+ {option.label} · {option.download}
63
+ </SelectItem>)}
64
+ </SelectGroup>
65
+ </SelectContent>
66
+ </Select>
67
+ <p id={descriptionId} className="m-0 text-xs leading-relaxed text-tita-ink-muted">
68
+ {chosen ? `${chosen.description} 下载约 ${chosen.download}${chosen.dimension ? `,输出 ${chosen.dimension} 维` : ''}。` : '请选择模型。'}
69
+ </p>
70
+ {chosen?.modelCardUrl && <a href={chosen.modelCardUrl} target="_blank" rel="noreferrer" className="w-fit text-xs text-tita-ink underline underline-offset-2">查看模型卡与许可</a>}
71
+ </div>
72
+ }
73
+
74
+ export function EnvironmentNotice({ controller, onOpen }: { controller: EnvironmentController; onOpen: () => void }): ReactNode {
75
+ const { view, error, busy } = controller
76
+ if (!error && (!view || view.report.state === 'ready')) return null
77
+ return <div className="mx-3 mt-2 flex shrink-0 items-center gap-2 border-b border-tita-line pb-2 text-xs text-tita-ink" role="status">
78
+ {busy ? <LoaderCircle size={14} className="tita-spin shrink-0" /> : <AlertCircle size={14} className="shrink-0 text-tita-warning" />}
79
+ <span className="min-w-0 flex-1">{error ? '环境状态读取失败' : busy ? view?.job?.stage ?? '正在检查运行环境' : '首次使用前,请准备向量和重排模型'}</span>
80
+ <Button variant="outline" size="sm" onClick={onOpen}>环境准备</Button>
81
+ </div>
82
+ }
83
+
84
+ export function EnvironmentSetup({ controller }: { controller: EnvironmentController }): ReactNode {
85
+ const { view, error, busy, prepare } = controller
86
+ const [details, setDetails] = useState(false)
87
+ const [selection, setSelection] = useState<ModelSelection>()
88
+ useEffect(() => {
89
+ if (view) setSelection(view.selection)
90
+ }, [view?.selection.embedding, view?.selection.rerank])
91
+ const ready = view?.report.state === 'ready'
92
+ const job = view?.job
93
+ const running = job?.state === 'running'
94
+ const failure = job?.state === 'failed' ? job.error : undefined
95
+ const chosen = selection ?? view?.selection
96
+ const selectionChanged = Boolean(chosen && view && (chosen.embedding !== view.selection.embedding || chosen.rerank !== view.selection.rerank))
97
+ return <section id="tita-environment" tabIndex={-1} aria-labelledby="tita-environment-title" className="tita-settingsSection">
98
+ <div className="tita-settingsSectionHeader">
99
+ <h3 id="tita-environment-title">环境准备</h3>
100
+ <span className={cn('text-xs', ready ? 'text-tita-success' : 'text-tita-ink-muted')} role="status" aria-live="polite">
101
+ {ready ? '已就绪' : busy ? '进行中' : '必需'}
102
+ </span>
103
+ </div>
104
+ <p className="m-0 mb-3 text-xs leading-relaxed text-tita-ink-muted">首次准备需要联网下载所选模型和推理依赖。模型保存在 Tita 专属目录,并通过实际推理自检后启用。</p>
105
+ <ul className="m-0 grid list-none gap-3 p-0" aria-label="运行环境检查项">
106
+ {(view?.report.items ?? []).map(item => <li key={item.id} className="flex min-w-0 items-start gap-2">
107
+ {item.state === 'ready' ? <Check size={14} className="mt-0.5 shrink-0 text-tita-success" /> : item.state === 'checking'
108
+ ? <LoaderCircle size={14} className="tita-spin mt-0.5 shrink-0" /> : <AlertCircle size={14} className="mt-0.5 shrink-0 text-tita-warning" />}
109
+ <div className="min-w-0 flex-1 text-xs leading-relaxed">
110
+ <span className="font-medium text-tita-ink">{item.label}</span>
111
+ <p className="m-0 break-words text-tita-ink-muted">{item.message}</p>
112
+ </div>
113
+ </li>)}
114
+ </ul>
115
+ {view && chosen && <div className="my-4 grid gap-3" aria-label="模型选择">
116
+ <ModelSelect id="tita-embedding-model" label="向量模型" value={chosen.embedding} options={view.models.embedding} disabled={busy}
117
+ onChange={value => setSelection(current => ({ ...(current ?? view.selection), embedding: value as ModelSelection['embedding'] }))} />
118
+ <ModelSelect id="tita-rerank-model" label="重排模型" value={chosen.rerank} options={view.models.rerank} disabled={busy}
119
+ onChange={value => setSelection(current => ({ ...(current ?? view.selection), rerank: value as ModelSelection['rerank'] }))} />
120
+ {selectionChanged && <p className="m-0 text-xs leading-relaxed text-tita-warning" role="status">
121
+ 安装并通过实际推理自检后才会切换当前模型。更改向量模型后,已有知识需要重新编译。
122
+ </p>}
123
+ </div>}
124
+ {!view && !error && <p className="text-xs text-tita-ink-muted" role="status">正在读取环境状态…</p>}
125
+ {running && <p className="my-3 text-xs text-tita-ink" role="status" aria-live="polite">{job.stage}。你可以离开此面板,任务会继续。</p>}
126
+ {(error || failure) && <p className="my-3 break-words text-xs text-tita-danger" role="alert">{error ?? failure}</p>}
127
+ {job?.state === 'cancelled' && <p className="my-3 text-xs text-tita-ink-muted" role="status">{job.error}</p>}
128
+ <div className="mt-4 flex flex-wrap gap-2">
129
+ <Button size="sm" variant={ready ? 'outline' : 'default'} disabled={busy} onClick={() => { void prepare('install', chosen) }}>
130
+ <Download size={13} />{selectionChanged ? '安装所选模型' : ready ? '修复当前环境' : job?.state === 'failed' || job?.state === 'cancelled' ? '重试准备' : '一键安装 / 修复'}
131
+ </Button>
132
+ <Button variant="outline" size="sm" disabled={busy || selectionChanged} onClick={() => { void prepare('check') }}><RefreshCw size={13} />重新自检</Button>
133
+ {running && <Button variant="ghost" size="sm" disabled={controller.busy && !running} onClick={() => { void prepare('cancel') }}>停止</Button>}
134
+ </div>
135
+ {ready && <p className="mb-0 mt-3 text-xs text-tita-ink-muted">环境自检通过。已有知识请重新编译,以生成当前模型的向量索引。</p>}
136
+ <Button variant="ghost" size="sm" className="mt-2" aria-expanded={details} aria-controls="tita-environment-details" onClick={() => setDetails(value => !value)}>
137
+ <ChevronDown size={13} />{details ? '收起检查详情' : '检查详情与日志'}
138
+ </Button>
139
+ {details && <div id="tita-environment-details" className="mt-2 grid gap-2 text-xs leading-relaxed text-tita-ink-muted">
140
+ {view?.report.checkedAt && <p className="m-0">最近自检:{new Date(view.report.checkedAt).toLocaleString()}</p>}
141
+ {view?.report.items.map(item => item.path && <p key={item.id} className="m-0 break-all">{item.label}:{item.path}</p>)}
142
+ {job?.logs.length ? <pre className="m-0 max-h-40 overflow-auto whitespace-pre-wrap break-all rounded-tita-control bg-tita-surface-muted p-2 font-mono">{job.logs.join('\n')}</pre> : null}
143
+ </div>}
144
+ </section>
145
+ }
@@ -11,6 +11,7 @@ import {
11
11
  import { KNOWLEDGE_SETTINGS_ORDER, NodeSettingsPanel, settingsAnchorId, useScope, type SettingsNode, type SettingsScopeLike } from './NodeSettings.tsx'
12
12
  import { useAgentLayout } from './DirectoryPicker.tsx'
13
13
  import { AgentPresetCard, type AgentPresetRemote } from './AgentPresetCard.tsx'
14
+ import { EnvironmentNotice, EnvironmentSetup, useKnowledgeEnvironment } from './EnvironmentSetup.tsx'
14
15
  import {
15
16
  NODE_META, NODE_ORDER, NODE_STATUS_LABEL, noteCountOf, sourceCountOf,
16
17
  type KnowledgeStats, type KnowledgeTab, type NodeId, type PanelTab, type Tone, type WorkspaceTabKind,
@@ -212,7 +213,7 @@ export function KnowledgePanel({ visible, currentQuestion, onOpenTab, settings,
212
213
  /** 从架构图节点跳到知识库分栏:tab + 一个递增的 nonce(同栏再点也要生效)。 */
213
214
  const [knowledgeFocus, setKnowledgeFocus] = useState<{ tab: KnowledgeTab; nonce: number } | undefined>(undefined)
214
215
  /** 设置抽屉入口。`null` = 关闭;否则定位到对应分组。 */
215
- const [settingsTarget, setSettingsTarget] = useState<SettingsNode | null>(null)
216
+ const [settingsTarget, setSettingsTarget] = useState<SettingsNode | 'environment' | null>(null)
216
217
  const [showAdvancedSettings, setShowAdvancedSettings] = useState(false)
217
218
  /**
218
219
  * 固定 Agent 目录 + 家当解析结果。它们是**服务端算出来的**(`layout.ts` 是唯一真源),
@@ -223,6 +224,7 @@ export function KnowledgePanel({ visible, currentQuestion, onOpenTab, settings,
223
224
  */
224
225
  const knowledgeSnapshot = useScope(settings?.knowledge)
225
226
  const { layout } = useAgentLayout(knowledgeSnapshot.revision ?? 0)
227
+ const environment = useKnowledgeEnvironment(visible, knowledgeSnapshot.revision ?? 0)
226
228
 
227
229
  /**
228
230
  * 只报**失败**的两条(2026-10-06)。
@@ -252,7 +254,7 @@ export function KnowledgePanel({ visible, currentQuestion, onOpenTab, settings,
252
254
  useEffect(() => {
253
255
  if (!settingsTarget) return
254
256
  const frame = requestAnimationFrame(() => {
255
- const section = document.getElementById(settingsAnchorId(settingsTarget))
257
+ const section = document.getElementById(settingsTarget === 'environment' ? 'tita-environment' : settingsAnchorId(settingsTarget))
256
258
  section?.scrollIntoView({ block: 'start' })
257
259
  section?.focus({ preventScroll: true })
258
260
  })
@@ -260,7 +262,7 @@ export function KnowledgePanel({ visible, currentQuestion, onOpenTab, settings,
260
262
  }, [settingsTarget])
261
263
 
262
264
  /** 打开设置抽屉并落到某一块。传 `null` 就是关。 */
263
- const openSettings = useCallback((target: SettingsNode | null): void => { setSettingsTarget(target) }, [])
265
+ const openSettings = useCallback((target: SettingsNode | 'environment' | null): void => { setSettingsTarget(target) }, [])
264
266
 
265
267
  const refreshStats = useCallback(async (): Promise<void> => {
266
268
  setStatsLoading(true); setStatsError(null)
@@ -330,10 +332,12 @@ export function KnowledgePanel({ visible, currentQuestion, onOpenTab, settings,
330
332
  <Button
331
333
  variant="ghost" size="icon" className={styles.tabBarSettings}
332
334
  aria-label="设置" title="设置 · 本方案参数"
333
- onClick={() => openSettings('storage')}
335
+ onClick={() => openSettings('environment')}
334
336
  ><Settings2 size={15} /></Button>
335
337
  </div>
336
338
 
339
+ <EnvironmentNotice controller={environment} onOpen={() => openSettings('environment')} />
340
+
337
341
  {/* 两栏走「舞台」版式(自己占满高度、不参与面板滚动):智能归因是图 + 运行面板,
338
342
  概览是画布 + 画布内浮层。同一套语汇,同一个理由 —— 它们的内容都要自己撑满
339
343
  可视区,让面板替它们滚会把画布推成一条可以滚出视口的纸。 */}
@@ -436,6 +440,7 @@ export function KnowledgePanel({ visible, currentQuestion, onOpenTab, settings,
436
440
  <DrawerBody>
437
441
  {/* 设置按数据位置、向量模型、检索与上下文分组。坐标定义按知识库保存,入口在工具栏。 */}
438
442
  <div className={styles.settingsStack}>
443
+ <EnvironmentSetup controller={environment} />
439
444
  {/* 故障排在最前面(同一条纪律:对不上账的事排在「有什么」之前)。 */}
440
445
  {layoutFailures.map(text => <div key={text} className="tita-settingsError" role="alert">
441
446
  <AlertCircle size={13} /><span>{text}</span>
@@ -0,0 +1,21 @@
1
+ import { createHash } from 'node:crypto'
2
+ import { readdirSync, statSync } from 'node:fs'
3
+ import { isAbsolute, join, relative, resolve, sep } from 'node:path'
4
+
5
+ /** Cache compatibility includes the checkpoint location and its local revision. */
6
+ export function embeddingFingerprint(model: string, resourceRoot?: string, endpoint?: string): string {
7
+ let location = model
8
+ const files: Array<[string, number, number]> = []
9
+ if (isAbsolute(model)) {
10
+ const root = resourceRoot && resolve(resourceRoot)
11
+ location = root && model.startsWith(`${root}${sep}`) ? `resource:${relative(root, model)}` : resolve(model)
12
+ try {
13
+ for (const name of readdirSync(model).sort()) {
14
+ if (!/\.(?:json|safetensors|bin|model)$/.test(name)) continue
15
+ const stat = statSync(join(model, name))
16
+ if (stat.isFile()) files.push([name, stat.size, stat.mtimeMs])
17
+ }
18
+ } catch { /* The normal model loader reports missing or inaccessible checkpoints. */ }
19
+ }
20
+ return createHash('sha256').update(JSON.stringify({ location, files, endpoint })).digest('hex')
21
+ }
@@ -0,0 +1,17 @@
1
+ import type { Knowledge } from '../knowledge.js'
2
+ import type { CardKnowledgeRow } from '../store/knowledge-db.js'
3
+ import type { ContextGraph, KnowledgeCard, WikiNote } from '../types.js'
4
+
5
+ /** One published generation; retrieval captures this value before any model call. */
6
+ export interface CompileSnapshot {
7
+ version: number
8
+ compileRunId: string
9
+ cards: KnowledgeCard[]
10
+ notes: WikiNote[]
11
+ graph: ContextGraph
12
+ diagnosisGraph: ContextGraph
13
+ placements: Array<[string, CardKnowledgeRow[]]>
14
+ knowledge: Knowledge[]
15
+ }
16
+
17
+ export const COMPILE_SNAPSHOT = 'recall-snapshot.json'
@@ -317,6 +317,7 @@ export class DiagnosisStore {
317
317
  const records: DiagnosisRunRecord[] = []
318
318
  const problems: string[] = []
319
319
  for (const source of await this.host.loadSources()) {
320
+ if (!source.sourceId.startsWith('diagnosis:') && !source.content.includes('```json diagnosis-run')) continue
320
321
  const record = parseRunSource(source.content)
321
322
  if (record) records.push(record)
322
323
  else problems.push(`诊断日志 ${source.sourceId} 里没有可解析的 \`json diagnosis-run\` 块,已跳过`)