@avantf/dsh-mem 0.7.0 → 0.8.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.
Files changed (146) hide show
  1. package/README.md +4 -0
  2. package/lib/client.js +548 -1195
  3. package/lib/dsh-build.json +1 -0
  4. package/lib/engine/graph-recall/in-memory.d.ts +93 -0
  5. package/lib/engine/graph-recall/index.d.ts +22 -0
  6. package/lib/engine/graph-recall/traverse.d.ts +119 -0
  7. package/lib/engine/graph-recall/types.d.ts +188 -0
  8. package/lib/engine/mem/config/config_files.d.ts +1 -1
  9. package/lib/engine/mem/config/loader.d.ts +2 -3
  10. package/lib/engine/mem/config/paths.d.ts +2 -13
  11. package/lib/engine/mem/db/dao/contradictions.d.ts +4 -22
  12. package/lib/engine/mem/db/dao/entities.d.ts +80 -7
  13. package/lib/engine/mem/db/dao/fact_sources.d.ts +3 -13
  14. package/lib/engine/mem/db/dao/facts.d.ts +311 -56
  15. package/lib/engine/mem/db/dao/triples.d.ts +1 -26
  16. package/lib/engine/mem/db/graph.d.ts +249 -0
  17. package/lib/engine/mem/db/journal_notes.d.ts +35 -0
  18. package/lib/engine/mem/db/migrations.d.ts +9 -0
  19. package/lib/engine/mem/db/port.d.ts +22 -3
  20. package/lib/engine/mem/db/schema.d.ts +119 -0
  21. package/lib/engine/mem/db/tokenizer.d.ts +9 -0
  22. package/lib/engine/mem/dbdrivers/fake/driver.d.ts +75 -0
  23. package/lib/engine/mem/dbdrivers/fake/index.d.ts +7 -0
  24. package/lib/engine/mem/dbdrivers/fake/surface.d.ts +34 -0
  25. package/lib/engine/mem/dbdrivers/fake/transport.d.ts +61 -0
  26. package/lib/engine/mem/dbdrivers/sqlite/connection.d.ts +14 -0
  27. package/lib/engine/mem/dbdrivers/sqlite/contradictions.d.ts +35 -0
  28. package/lib/engine/mem/dbdrivers/sqlite/dao/index.d.ts +16 -0
  29. package/lib/engine/mem/dbdrivers/sqlite/driver.d.ts +54 -0
  30. package/lib/engine/mem/dbdrivers/sqlite/facts.d.ts +117 -0
  31. package/lib/engine/mem/dbdrivers/sqlite/index.d.ts +6 -0
  32. package/lib/engine/mem/dbdrivers/sqlite/migrations.d.ts +15 -0
  33. package/lib/engine/mem/dbdrivers/sqlite/relations.d.ts +62 -0
  34. package/lib/engine/mem/dbdrivers/sqlite/search.d.ts +56 -0
  35. package/lib/engine/mem/dbdrivers/sqlite/sources.d.ts +37 -0
  36. package/lib/engine/mem/defaults.d.ts +3 -4
  37. package/lib/engine/mem/dispatch.d.ts +7 -32
  38. package/lib/engine/mem/entities/extract.d.ts +44 -47
  39. package/lib/engine/mem/eval/derived_runner.d.ts +6 -1
  40. package/lib/engine/mem/eval/relation_gold.d.ts +251 -0
  41. package/lib/engine/mem/eval/write_metrics.d.ts +1 -1
  42. package/lib/engine/mem/facts_io.d.ts +50 -10
  43. package/lib/engine/mem/index.d.ts +6 -2
  44. package/lib/engine/mem/interfaces/capabilities.d.ts +30 -0
  45. package/lib/engine/mem/interfaces/common.d.ts +11 -0
  46. package/lib/engine/mem/interfaces/errors.d.ts +65 -0
  47. package/lib/engine/mem/interfaces/index.d.ts +18 -0
  48. package/lib/engine/mem/interfaces/migrations.d.ts +21 -0
  49. package/lib/engine/mem/interfaces/repositories.d.ts +471 -0
  50. package/lib/engine/mem/interfaces/search_index.d.ts +113 -0
  51. package/lib/engine/mem/interfaces/storage_backend.d.ts +84 -0
  52. package/lib/engine/mem/interfaces/storage_driver.d.ts +43 -0
  53. package/lib/engine/mem/interfaces/version.d.ts +34 -0
  54. package/lib/engine/mem/lifecycle/contradiction.d.ts +2 -2
  55. package/lib/engine/mem/modelBootstrap.d.ts +1 -1
  56. package/lib/engine/mem/relations/confidence.d.ts +39 -0
  57. package/lib/engine/mem/relations/extract.d.ts +128 -0
  58. package/lib/engine/mem/relations/index.d.ts +32 -0
  59. package/lib/engine/mem/relations/name-quality.d.ts +78 -0
  60. package/lib/engine/mem/relations/normalize.d.ts +275 -0
  61. package/lib/engine/mem/relations/patterns.d.ts +67 -0
  62. package/lib/engine/mem/relations/statement.d.ts +88 -0
  63. package/lib/engine/mem/relations/types.d.ts +79 -0
  64. package/lib/engine/mem/runtime.d.ts +61 -88
  65. package/lib/engine/mem/storage/create.d.ts +42 -0
  66. package/lib/engine/mem/storage/errors.d.ts +65 -0
  67. package/lib/engine/mem/storage/index.d.ts +15 -0
  68. package/lib/engine/mem/storage/ports.d.ts +472 -0
  69. package/lib/engine/mem/storage/registry.d.ts +37 -0
  70. package/lib/engine/mem/storage/sqlite.d.ts +28 -0
  71. package/lib/engine/mem/store/common.d.ts +1 -1
  72. package/lib/engine/mem/store/entity_leg.d.ts +59 -2
  73. package/lib/engine/mem/store/entity_sweep.d.ts +4 -3
  74. package/lib/engine/mem/store/explicit_relations.d.ts +52 -0
  75. package/lib/engine/mem/store/facts_restore.d.ts +10 -2
  76. package/lib/engine/mem/store/floors.d.ts +4 -1
  77. package/lib/engine/mem/store/graph_reader.d.ts +146 -0
  78. package/lib/engine/mem/store/hybrid.d.ts +50 -13
  79. package/lib/engine/mem/store/kinds.d.ts +47 -0
  80. package/lib/engine/mem/store/legs.d.ts +24 -29
  81. package/lib/engine/mem/store/lexical.d.ts +36 -45
  82. package/lib/engine/mem/store/memory.d.ts +471 -83
  83. package/lib/engine/mem/store/recall_policy.d.ts +49 -0
  84. package/lib/engine/mem/store/relation_backfill.d.ts +210 -0
  85. package/lib/engine/mem/store/relation_planner.d.ts +191 -0
  86. package/lib/engine/mem/store/secret_guard.d.ts +0 -1
  87. package/lib/engine/mem/store/self_query.d.ts +4 -2
  88. package/lib/engine/mem/store/time_filter.d.ts +121 -0
  89. package/lib/engine/mem/store/titles.d.ts +52 -0
  90. package/lib/engine/mem/store/vector_repair.d.ts +44 -54
  91. package/lib/engine/mem-contract/config.d.ts +44 -38
  92. package/lib/engine/mem-contract/text.d.ts +1 -1
  93. package/lib/engine/mem-contract/tools.d.ts +145 -132
  94. package/lib/engine/mem-contract/types.d.ts +364 -288
  95. package/lib/index.js +12048 -11337
  96. package/lib/interface-version.json +1 -1
  97. package/lib/types/client/index.d.ts +2 -2
  98. package/lib/types/client/index.d.ts.map +1 -1
  99. package/lib/types/client/paging.d.ts +3 -12
  100. package/lib/types/client/paging.d.ts.map +1 -1
  101. package/lib/types/client/summarize.d.ts.map +1 -1
  102. package/lib/types/envinit-bootstrap.d.ts +1 -1
  103. package/lib/types/envinit.d.ts +2 -6
  104. package/lib/types/envinit.d.ts.map +1 -1
  105. package/lib/types/hints.d.ts +28 -24
  106. package/lib/types/hints.d.ts.map +1 -1
  107. package/lib/types/index.d.ts +21 -40
  108. package/lib/types/index.d.ts.map +1 -1
  109. package/lib/types/inject.d.ts +3 -2
  110. package/lib/types/inject.d.ts.map +1 -1
  111. package/lib/types/interface_gate.d.ts +1 -1
  112. package/lib/types/prompt.d.ts +17 -41
  113. package/lib/types/prompt.d.ts.map +1 -1
  114. package/lib/types/provision.d.ts +3 -0
  115. package/lib/types/provision.d.ts.map +1 -1
  116. package/lib/types/remote.d.ts +14 -7
  117. package/lib/types/remote.d.ts.map +1 -1
  118. package/lib/types/tool_schema.d.ts +4 -0
  119. package/lib/types/tool_schema.d.ts.map +1 -1
  120. package/lib/types/vectorMigration.d.ts +5 -7
  121. package/lib/types/vectorMigration.d.ts.map +1 -1
  122. package/package.json +15 -14
  123. package/lib/engine/mem/config/domains.d.ts +0 -20
  124. package/lib/engine/mem/db/dao/chunks.d.ts +0 -231
  125. package/lib/engine/mem/db/dao/documents.d.ts +0 -51
  126. package/lib/engine/mem/db/hash.d.ts +0 -2
  127. package/lib/engine/mem/db/knowledge.d.ts +0 -17
  128. package/lib/engine/mem/eval/index.d.ts +0 -4
  129. package/lib/engine/mem/git.d.ts +0 -73
  130. package/lib/engine/mem/router.d.ts +0 -51
  131. package/lib/engine/mem/store/doc_files.d.ts +0 -125
  132. package/lib/engine/mem/store/document_text.d.ts +0 -47
  133. package/lib/engine/mem/store/ingest_guard.d.ts +0 -25
  134. package/lib/engine/mem/store/knowledge.d.ts +0 -598
  135. package/lib/engine/mem/store/source_picker.d.ts +0 -43
  136. package/lib/engine/mem-retrieval/adapters/local_reranker.d.ts +0 -52
  137. package/lib/engine/mem-retrieval/adapters/none_reranker.d.ts +0 -10
  138. package/lib/engine/mem-retrieval/rerank.d.ts +0 -13
  139. package/lib/types/client/domains.d.ts +0 -53
  140. package/lib/types/client/domains.d.ts.map +0 -1
  141. package/lib/types/client/ingest.d.ts +0 -24
  142. package/lib/types/client/ingest.d.ts.map +0 -1
  143. package/lib/types/open.d.ts +0 -77
  144. package/lib/types/open.d.ts.map +0 -1
  145. package/lib/types/reconcile.d.ts +0 -64
  146. package/lib/types/reconcile.d.ts.map +0 -1
package/lib/client.js CHANGED
@@ -29,7 +29,7 @@ window.__ModuleLoader__.load({
29
29
  let react = require("react");
30
30
  react = __toESM(react, 1);
31
31
  //#region \0dsh-css:src/client/pages.module.css.mjs
32
- const css = ".HvfIsq_section{color:var(--dsw-alias-label-primary);width:100%;max-width:var(--dsh-chat-content-width);flex-direction:column;gap:12px;margin:0 auto;display:flex}.HvfIsq_headbar{z-index:4;border-bottom:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);flex-wrap:wrap;align-items:flex-end;gap:8px;padding-bottom:8px;display:flex;position:sticky;top:0}.HvfIsq_warn{color:var(--dsw-alias-state-warn-label);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 16%, transparent);border-radius:9px;margin-left:6px;padding:0 6px;font-size:11px;line-height:16px;display:inline-block}.HvfIsq_search{width:100%;color:var(--dsw-alias-label-tertiary);align-items:center;display:flex;position:relative}.HvfIsq_search>svg{pointer-events:none;position:absolute;left:12px}.HvfIsq_search input{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);width:100%;height:36px;color:var(--dsw-alias-label-primary);font:inherit;border-radius:10px;outline:none;padding:0 34px 0 36px;font-size:13px}.HvfIsq_search input::placeholder{color:var(--dsw-alias-label-tertiary)}.HvfIsq_search input:focus-visible{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent)}.HvfIsq_status{color:var(--dsw-alias-label-tertiary);font-size:12px}.HvfIsq_error{color:var(--dsw-alias-label-error);white-space:pre-wrap;font-size:12px}.HvfIsq_list{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.HvfIsq_item{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:8px 10px;font-size:13px;line-height:1.5}.HvfIsq_meta{color:var(--dsw-alias-label-tertiary);margin-top:2px;font-size:12px;display:block}.HvfIsq_detail{flex-direction:column;gap:4px;display:flex}.HvfIsq_more{border:.5px solid var(--dsw-alias-border-l3);height:28px;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;background:0 0;border-radius:14px;justify-content:center;align-self:flex-start;align-items:center;gap:4px;padding:0 12px;font-size:12px;line-height:18px;display:inline-flex}.HvfIsq_more:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.HvfIsq_more:disabled{cursor:not-allowed;opacity:.4}.HvfIsq_toolbar{flex-wrap:wrap;align-items:flex-end;gap:8px;display:flex}.HvfIsq_form{flex-direction:column;gap:8px;display:flex}.HvfIsq_row{flex-wrap:wrap;align-items:center;gap:6px;margin-top:6px;display:flex}.HvfIsq_btn{border:.5px solid var(--dsw-alias-border-l3);height:24px;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;background:0 0;border-radius:12px;justify-content:center;align-items:center;gap:4px;padding:0 10px;font-size:12px;line-height:16px;display:inline-flex}.HvfIsq_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.HvfIsq_btn:disabled{cursor:not-allowed;opacity:.4}.HvfIsq_btnActive{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.HvfIsq_btnDanger{color:var(--dsw-alias-label-error)}.HvfIsq_fieldHead{justify-content:space-between;align-items:center;gap:8px;display:flex}.HvfIsq_fieldHead>*+*{flex:none}.HvfIsq_field{color:var(--dsw-alias-label-tertiary);flex-direction:column;flex:auto;gap:4px;min-width:120px;font-size:12px;display:flex}.HvfIsq_input{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);height:28px;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;outline:none;min-width:0;padding:0 8px;font-size:12px}.HvfIsq_input:focus-visible{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent)}.HvfIsq_textarea{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);font:inherit;resize:vertical;box-sizing:border-box;border-radius:8px;outline:none;margin-top:6px;padding:6px 8px;font-size:12px;line-height:1.5}.HvfIsq_textarea:focus-visible{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent)}.HvfIsq_groupHead{color:var(--dsw-alias-label-secondary);margin:4px 0 2px;font-size:12px;font-weight:600}.HvfIsq_badge{color:var(--dsw-alias-label-tertiary);margin-bottom:2px;font-size:11px;display:inline-block}.HvfIsq_chunkBox{border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;margin-top:6px;padding:6px 8px;font-size:12px;line-height:1.5}.HvfIsq_picker{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);border-radius:10px;flex-direction:column;gap:8px;padding:8px 10px;display:flex}.HvfIsq_pickerHead{flex-direction:column;gap:2px;display:flex}.HvfIsq_pickerPath{overflow-wrap:anywhere;font-family:ui-monospace,monospace;font-size:12px}.HvfIsq_pickerList{border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;flex-direction:column;gap:1px;max-height:260px;margin:0;padding:0;list-style:none;display:flex;overflow:auto}.HvfIsq_pickerEntry{width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;padding:4px 8px;font-size:12px;display:block}.HvfIsq_pickerEntry:hover:not(:disabled){background:color-mix(in srgb, currentColor 8%, transparent)}.HvfIsq_pickerDisabled{opacity:.45;cursor:default}.HvfIsq_hintWarn{color:var(--dsw-alias-state-warn-label)}";
32
+ const css = ".HvfIsq_section{color:var(--dsw-alias-label-primary);width:100%;max-width:var(--dsh-chat-content-width);flex-direction:column;gap:12px;margin:0 auto;display:flex}.HvfIsq_headbar{z-index:4;border-bottom:.5px solid var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-base);flex-wrap:wrap;align-items:flex-end;gap:8px;padding-bottom:8px;display:flex;position:sticky;top:0}.HvfIsq_warn{color:var(--dsw-alias-state-warn-label);background:color-mix(in srgb, var(--dsw-alias-state-warn-primary) 16%, transparent);border-radius:9px;margin-left:6px;padding:0 6px;font-size:11px;line-height:16px;display:inline-block}.HvfIsq_search{width:100%;color:var(--dsw-alias-label-tertiary);align-items:center;display:flex;position:relative}.HvfIsq_searchField{flex:260px;width:auto}.HvfIsq_searchPanel{border:.5px solid var(--dsw-alias-state-business-primary);background:color-mix(in srgb, var(--dsw-alias-state-business-primary) 7%, transparent);border-radius:10px;flex-direction:column;gap:8px;padding:10px;display:flex}.HvfIsq_searchTitle{color:var(--dsw-alias-label-primary);flex-wrap:wrap;align-items:baseline;gap:6px;font-size:13px;font-weight:600;display:flex}.HvfIsq_searchQuery{color:var(--dsw-alias-label-tertiary);font-size:12px;font-weight:400}.HvfIsq_select{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);height:28px;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;outline:none;padding:0 8px;font-size:12px}.HvfIsq_select:focus-visible{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent)}.HvfIsq_search>svg{pointer-events:none;position:absolute;left:12px}.HvfIsq_search input{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);width:100%;height:36px;color:var(--dsw-alias-label-primary);font:inherit;border-radius:10px;outline:none;padding:0 34px 0 36px;font-size:13px}.HvfIsq_search input::placeholder{color:var(--dsw-alias-label-tertiary)}.HvfIsq_search input:focus-visible{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent)}.HvfIsq_clear{width:20px;height:20px;color:var(--dsw-alias-label-tertiary);font:inherit;cursor:pointer;background:0 0;border:0;border-radius:50%;justify-content:center;align-items:center;padding:0;font-size:16px;line-height:1;display:inline-flex;position:absolute;top:50%;right:8px;transform:translateY(-50%)}.HvfIsq_clear:hover{color:var(--dsw-alias-label-primary);background:var(--dsw-alias-interactive-bg-hover)}.HvfIsq_clear:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}.HvfIsq_status{color:var(--dsw-alias-label-tertiary);font-size:12px}.HvfIsq_error{color:var(--dsw-alias-label-error);white-space:pre-wrap;font-size:12px}.HvfIsq_list{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.HvfIsq_item{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);border-radius:10px;padding:8px 10px;font-size:13px;line-height:1.5}.HvfIsq_meta{color:var(--dsw-alias-label-tertiary);margin-top:2px;font-size:12px;display:block}.HvfIsq_detail{flex-direction:column;gap:4px;display:flex}.HvfIsq_more{border:.5px solid var(--dsw-alias-border-l3);height:28px;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;background:0 0;border-radius:14px;justify-content:center;align-self:flex-start;align-items:center;gap:4px;padding:0 12px;font-size:12px;line-height:18px;display:inline-flex}.HvfIsq_more:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.HvfIsq_more:disabled{cursor:not-allowed;opacity:.4}.HvfIsq_toolbar{flex-wrap:wrap;align-items:flex-end;gap:8px;display:flex}.HvfIsq_form{flex-direction:column;gap:8px;display:flex}.HvfIsq_row{flex-wrap:wrap;align-items:center;gap:6px;margin-top:6px;display:flex}.HvfIsq_btn{border:.5px solid var(--dsw-alias-border-l3);height:24px;color:var(--dsw-alias-label-primary);font:inherit;cursor:pointer;background:0 0;border-radius:12px;justify-content:center;align-items:center;gap:4px;padding:0 10px;font-size:12px;line-height:16px;display:inline-flex}.HvfIsq_btn:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}.HvfIsq_btn:disabled{cursor:not-allowed;opacity:.4}.HvfIsq_btnActive{border-color:var(--dsw-alias-state-business-primary);color:var(--dsw-alias-state-business-primary)}.HvfIsq_btnDanger{color:var(--dsw-alias-label-error)}.HvfIsq_fieldHead{justify-content:space-between;align-items:center;gap:8px;display:flex}.HvfIsq_fieldHead>*+*{flex:none}.HvfIsq_field{color:var(--dsw-alias-label-tertiary);flex-direction:column;flex:auto;gap:4px;min-width:120px;font-size:12px;display:flex}.HvfIsq_input{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);height:28px;color:var(--dsw-alias-label-primary);font:inherit;border-radius:8px;outline:none;min-width:0;padding:0 8px;font-size:12px}.HvfIsq_input:focus-visible{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent)}.HvfIsq_textarea{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);width:100%;color:var(--dsw-alias-label-primary);font:inherit;resize:vertical;box-sizing:border-box;border-radius:8px;outline:none;margin-top:6px;padding:6px 8px;font-size:12px;line-height:1.5}.HvfIsq_textarea:focus-visible{border-color:var(--dsw-alias-state-business-primary);box-shadow:0 0 0 2px color-mix(in srgb, var(--dsw-alias-state-business-primary) 18%, transparent)}.HvfIsq_groupHead{color:var(--dsw-alias-label-secondary);margin:4px 0 2px;font-size:12px;font-weight:600}.HvfIsq_badge{color:var(--dsw-alias-label-tertiary);margin-bottom:2px;font-size:11px;display:inline-block}.HvfIsq_chunkBox{border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;margin-top:6px;padding:6px 8px;font-size:12px;line-height:1.5}.HvfIsq_picker{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-1);border-radius:10px;flex-direction:column;gap:8px;padding:8px 10px;display:flex}.HvfIsq_pickerHead{flex-direction:column;gap:2px;display:flex}.HvfIsq_pickerPath{overflow-wrap:anywhere;font-family:ui-monospace,monospace;font-size:12px}.HvfIsq_pickerList{border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;flex-direction:column;gap:1px;max-height:260px;margin:0;padding:0;list-style:none;display:flex;overflow:auto}.HvfIsq_pickerEntry{width:100%;color:inherit;font:inherit;text-align:left;cursor:pointer;background:0 0;border:0;padding:4px 8px;font-size:12px;display:block}.HvfIsq_pickerEntry:hover:not(:disabled){background:color-mix(in srgb, currentColor 8%, transparent)}.HvfIsq_pickerDisabled{opacity:.45;cursor:default}.HvfIsq_hintWarn{color:var(--dsw-alias-state-warn-label)}";
33
33
  const tagId = "@avantf/dsh-mem/pages.module.css";
34
34
  if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=" + JSON.stringify(tagId) + "]") === null) {
35
35
  const tag = document.createElement("style");
@@ -44,6 +44,7 @@ window.__ModuleLoader__.load({
44
44
  "btnActive": "HvfIsq_btnActive",
45
45
  "btnDanger": "HvfIsq_btnDanger",
46
46
  "chunkBox": "HvfIsq_chunkBox",
47
+ "clear": "HvfIsq_clear",
47
48
  "detail": "HvfIsq_detail",
48
49
  "error": "HvfIsq_error",
49
50
  "field": "HvfIsq_field",
@@ -65,14 +66,19 @@ window.__ModuleLoader__.load({
65
66
  "pickerPath": "HvfIsq_pickerPath",
66
67
  "row": "HvfIsq_row",
67
68
  "search": "HvfIsq_search",
69
+ "searchField": "HvfIsq_searchField",
70
+ "searchPanel": "HvfIsq_searchPanel",
71
+ "searchQuery": "HvfIsq_searchQuery",
72
+ "searchTitle": "HvfIsq_searchTitle",
68
73
  "section": "HvfIsq_section",
74
+ "select": "HvfIsq_select",
69
75
  "status": "HvfIsq_status",
70
76
  "textarea": "HvfIsq_textarea",
71
77
  "toolbar": "HvfIsq_toolbar",
72
78
  "warn": "HvfIsq_warn"
73
79
  };
74
80
  //#endregion
75
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
81
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/core.js
76
82
  var _a$1;
77
83
  function $constructor(name, initializer, params) {
78
84
  function init(inst, def) {
@@ -131,7 +137,7 @@ window.__ModuleLoader__.load({
131
137
  return globalConfig;
132
138
  }
133
139
  //#endregion
134
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/util.js
140
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/util.js
135
141
  function getEnumValues(entries) {
136
142
  const numericValues = Object.values(entries).filter((v) => typeof v === "number");
137
143
  return Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v);
@@ -446,7 +452,7 @@ window.__ModuleLoader__.load({
446
452
  return { ...iss };
447
453
  }
448
454
  //#endregion
449
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/errors.js
455
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/errors.js
450
456
  const initializer$1 = (inst, def) => {
451
457
  inst.name = "$ZodError";
452
458
  Object.defineProperty(inst, "_zod", {
@@ -506,7 +512,7 @@ window.__ModuleLoader__.load({
506
512
  return fieldErrors;
507
513
  }
508
514
  //#endregion
509
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/parse.js
515
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/parse.js
510
516
  const _parse = (_Err) => (schema, value, _ctx, _params) => {
511
517
  const ctx = _ctx ? {
512
518
  ..._ctx,
@@ -620,7 +626,7 @@ window.__ModuleLoader__.load({
620
626
  return _safeParseAsync(_Err)(schema, value, _ctx);
621
627
  };
622
628
  //#endregion
623
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/regexes.js
629
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/regexes.js
624
630
  /**
625
631
  * @deprecated CUID v1 is deprecated by its authors due to information leakage
626
632
  * (timestamps embedded in the id). Use {@link cuid2} instead.
@@ -684,7 +690,7 @@ window.__ModuleLoader__.load({
684
690
  const lowercase = /^[^A-Z]*$/;
685
691
  const uppercase = /^[^a-z]*$/;
686
692
  //#endregion
687
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/checks.js
693
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/checks.js
688
694
  const $ZodCheck = /*@__PURE__*/ $constructor("$ZodCheck", (inst, def) => {
689
695
  var _a;
690
696
  inst._zod ?? (inst._zod = {});
@@ -1042,7 +1048,7 @@ window.__ModuleLoader__.load({
1042
1048
  };
1043
1049
  });
1044
1050
  //#endregion
1045
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/doc.js
1051
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/doc.js
1046
1052
  var Doc = class {
1047
1053
  constructor(args = []) {
1048
1054
  this.content = [];
@@ -1073,14 +1079,14 @@ window.__ModuleLoader__.load({
1073
1079
  }
1074
1080
  };
1075
1081
  //#endregion
1076
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/versions.js
1082
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/versions.js
1077
1083
  const version = {
1078
1084
  major: 4,
1079
1085
  minor: 4,
1080
1086
  patch: 3
1081
1087
  };
1082
1088
  //#endregion
1083
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/schemas.js
1089
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/schemas.js
1084
1090
  const $ZodType = /*@__PURE__*/ $constructor("$ZodType", (inst, def) => {
1085
1091
  var _a;
1086
1092
  inst ?? (inst = {});
@@ -2187,7 +2193,7 @@ window.__ModuleLoader__.load({
2187
2193
  }
2188
2194
  }
2189
2195
  //#endregion
2190
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/registries.js
2196
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/registries.js
2191
2197
  var _a;
2192
2198
  var $ZodRegistry = class {
2193
2199
  constructor() {
@@ -2234,7 +2240,7 @@ window.__ModuleLoader__.load({
2234
2240
  (_a = globalThis).__zod_globalRegistry ?? (_a.__zod_globalRegistry = registry());
2235
2241
  const globalRegistry = globalThis.__zod_globalRegistry;
2236
2242
  //#endregion
2237
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/api.js
2243
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/api.js
2238
2244
  // @__NO_SIDE_EFFECTS__
2239
2245
  function _string(Class, params) {
2240
2246
  return new Class({
@@ -2743,7 +2749,7 @@ window.__ModuleLoader__.load({
2743
2749
  return ch;
2744
2750
  }
2745
2751
  //#endregion
2746
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/to-json-schema.js
2752
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/to-json-schema.js
2747
2753
  function initializeContext(params) {
2748
2754
  let target = params?.target ?? "draft-2020-12";
2749
2755
  if (target === "draft-4") target = "draft-04";
@@ -3036,7 +3042,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
3036
3042
  return finalize(ctx, schema);
3037
3043
  };
3038
3044
  //#endregion
3039
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/json-schema-processors.js
3045
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/core/json-schema-processors.js
3040
3046
  const formatMap = {
3041
3047
  guid: "uuid",
3042
3048
  url: "uri",
@@ -3244,7 +3250,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
3244
3250
  seen.ref = def.innerType;
3245
3251
  };
3246
3252
  //#endregion
3247
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/iso.js
3253
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/iso.js
3248
3254
  const ZodISODateTime = /*@__PURE__*/ $constructor("ZodISODateTime", (inst, def) => {
3249
3255
  $ZodISODateTime.init(inst, def);
3250
3256
  ZodStringFormat.init(inst, def);
@@ -3274,7 +3280,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
3274
3280
  return /* @__PURE__ */ _isoDuration(ZodISODuration, params);
3275
3281
  }
3276
3282
  //#endregion
3277
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/errors.js
3283
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/errors.js
3278
3284
  const initializer = (inst, issues) => {
3279
3285
  $ZodError.init(inst, issues);
3280
3286
  inst.name = "ZodError";
@@ -3296,7 +3302,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
3296
3302
  };
3297
3303
  const ZodRealError = /*@__PURE__*/ $constructor("ZodError", initializer, { Parent: Error });
3298
3304
  //#endregion
3299
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/parse.js
3305
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/parse.js
3300
3306
  const parse = /* @__PURE__ */ _parse(ZodRealError);
3301
3307
  const parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);
3302
3308
  const safeParse = /* @__PURE__ */ _safeParse(ZodRealError);
@@ -3310,7 +3316,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
3310
3316
  const safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError);
3311
3317
  const safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);
3312
3318
  //#endregion
3313
- //#region ../../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/schemas.js
3319
+ //#region ../../node_modules/.pnpm/zod@4.4.3/node_modules/zod/v4/classic/schemas.js
3314
3320
  const _installedGroups = /* @__PURE__ */ new WeakMap();
3315
3321
  function _installLazyMethods(inst, group, methods) {
3316
3322
  const proto = Object.getPrototypeOf(inst);
@@ -4087,14 +4093,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4087
4093
  * contribution registered through `ctx.typert.register()` (the documented
4088
4094
  * path for hand-written wire schemas, see `@deepseek-ai/dsh-typert-loader`).
4089
4095
  *
4090
- * The five methods mirror the five agent tools (remember/recall/admin/kb/query)
4091
- * so the 记忆/知识 tabs can do full CRUD + KB management. Args carry the merged
4092
- * optional field set of the contract's action unions; the HOST re-validates
4093
- * every payload with the contract zod schema before dispatch, so the wire face
4094
- * only needs to guarantee JSON shape sanity. Five methods have no tool behind
4095
- * them — `openDoc` (the 知识 tab's 「编辑」/「打开目录」 buttons), `classifySource`
4096
- * and `browseDir` (its source input and 选择 picker), `kbDomains` (its domain
4097
- * dropdown) and `kbAddDomain` (its 「+」 next to that dropdown).
4096
+ * The three methods mirror the three agent tools (remember/recall/admin) so the 记忆 tab can do the
4097
+ * full fact CRUD the operator needs. Args carry the merged optional field set of the contract's
4098
+ * action unions; the HOST re-validates every payload with the contract zod schema before dispatch,
4099
+ * so the wire face only needs to guarantee JSON shape sanity.
4098
4100
  *
4099
4101
  * The host half consumes `hostContribution`; the client half mounts
4100
4102
  * `clientContribution` via `ctx.remote.$mount()`.
@@ -4135,11 +4137,25 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4135
4137
  ...acceptsUndefined === void 0 ? {} : { acceptsUndefined }
4136
4138
  };
4137
4139
  }
4138
- const text = string();
4139
4140
  const count = number().int().optional();
4140
4141
  const optionalText = string().optional();
4141
4142
  const bool = boolean().optional();
4142
- const textArray = array(string()).optional();
4143
+ /**
4144
+ * The `time` filter of `mem_recall` (docs/TIME_SEARCH_AND_CATEGORIES.md §3).
4145
+ *
4146
+ * Declared here for the same reason every other optional field is: the wire codec is `strict`, so an
4147
+ * undeclared key is DROPPED before the host validates — a request that looks sent and arrives empty.
4148
+ * The values are not re-validated here (the contract's zod schema owns that); this only keeps the
4149
+ * field alive across the boundary. The UI does not send it today; the invariant being pinned is
4150
+ * "declared ⊇ contract", which is what keeps the next caller from hitting the silent drop.
4151
+ */
4152
+ const timeFilter = object({
4153
+ field: optionalText,
4154
+ on: optionalText,
4155
+ from: optionalText,
4156
+ to: optionalText,
4157
+ timezone: optionalText
4158
+ }).optional();
4143
4159
  /** One direct Remote method carrying a single `args` object parameter. */
4144
4160
  function direct(method, args) {
4145
4161
  return {
@@ -4157,78 +4173,50 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4157
4173
  action: optionalText.optional(),
4158
4174
  content: optionalText.optional(),
4159
4175
  fact_id: count.optional(),
4160
- category: optionalText.optional(),
4176
+ kind: optionalText.optional(),
4161
4177
  ttl_days: count.optional(),
4162
4178
  reason: optionalText.optional(),
4163
4179
  source_ref: optionalText.optional(),
4164
4180
  event_date: optionalText.optional(),
4165
- valid_until: optionalText.optional()
4181
+ valid_until: optionalText.optional(),
4182
+ title: optionalText.optional(),
4183
+ retitle: bool.optional(),
4184
+ relations: array(object({
4185
+ subject: optionalText,
4186
+ predicate: optionalText,
4187
+ object: optionalText
4188
+ })).optional()
4166
4189
  })),
4167
4190
  direct("recall", object({
4168
4191
  action: optionalText.optional(),
4169
4192
  query: optionalText.optional(),
4170
- entity: optionalText.optional(),
4171
- entities: textArray.optional(),
4172
- subj: optionalText.optional(),
4173
- pred: optionalText.optional(),
4174
- obj: optionalText.optional(),
4175
- second_pred: optionalText.optional(),
4176
- category: optionalText.optional(),
4193
+ kind: optionalText.optional(),
4177
4194
  source: optionalText.optional(),
4178
4195
  include_scores: bool.optional(),
4179
4196
  limit: count.optional(),
4197
+ offset: count.optional(),
4180
4198
  max_tokens: count.optional(),
4181
- floors: optionalText.optional()
4199
+ time: timeFilter,
4200
+ floors: optionalText.optional(),
4201
+ status: optionalText.optional(),
4202
+ include_excerpt: bool.optional()
4182
4203
  })),
4183
4204
  direct("admin", object({
4184
4205
  action: optionalText.optional(),
4185
4206
  fact_id: count.optional(),
4186
- category: optionalText.optional(),
4207
+ kind: optionalText.optional(),
4187
4208
  status: optionalText.optional(),
4188
4209
  source: optionalText.optional(),
4210
+ time: timeFilter,
4189
4211
  limit: count.optional(),
4190
4212
  offset: count.optional(),
4191
4213
  reason: optionalText.optional(),
4192
4214
  dry_run: bool.optional(),
4193
4215
  file: optionalText.optional(),
4194
4216
  confirm: bool.optional(),
4195
- store: optionalText.optional(),
4196
4217
  contradiction_id: count.optional(),
4197
4218
  resolution: optionalText.optional(),
4198
4219
  loser_fact_id: count.optional()
4199
- })),
4200
- direct("kb", object({
4201
- action: optionalText.optional(),
4202
- text: optionalText.optional(),
4203
- source_uri: optionalText.optional(),
4204
- domain: optionalText.optional(),
4205
- source: optionalText.optional(),
4206
- title: optionalText.optional(),
4207
- paths: textArray.optional(),
4208
- doc_id: count.optional(),
4209
- limit: count.optional(),
4210
- offset: count.optional(),
4211
- dry_run: bool.optional(),
4212
- overwrite: bool.optional(),
4213
- adopt: bool.optional()
4214
- })),
4215
- direct("query", object({
4216
- query: text,
4217
- kind: optionalText.optional(),
4218
- domain: optionalText.optional(),
4219
- source: optionalText.optional(),
4220
- limit: count.optional(),
4221
- max_tokens: count.optional(),
4222
- include_scores: bool.optional(),
4223
- floors: optionalText.optional()
4224
- })),
4225
- direct("classifySource", object({ text: optionalText.optional() })),
4226
- direct("browseDir", object({ path: optionalText.optional() })),
4227
- direct("kbDomains", object({})),
4228
- direct("kbAddDomain", object({ domain: text })),
4229
- direct("openDoc", object({
4230
- doc_id: count,
4231
- target: optionalText.optional()
4232
4220
  }))
4233
4221
  ];
4234
4222
  /** Client face: mounted by the client half through `ctx.remote.$mount()`. */
@@ -4377,13 +4365,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4377
4365
  const conflict = conflictSummary(record["contradictions"]);
4378
4366
  if (conflict !== void 0) return conflict;
4379
4367
  if ("is_new" in record) return "";
4380
- if (Array.isArray(record["imported"])) {
4368
+ if (typeof record["imported"] === "number" || typeof record["added"] === "number") {
4369
+ const count = typeof record["imported"] === "number" ? record["imported"] : record["added"];
4381
4370
  const failed = Array.isArray(record["failed"]) ? record["failed"].length : 0;
4382
- const skipped = typeof record["skipped_total"] === "number" ? record["skipped_total"] : 0;
4383
- return `:成功 ${String(record["imported"].length)} 个` + (failed > 0 ? `,失败 ${String(failed)} 个(见返回详情)` : "") + (skipped > 0 ? `,跳过 ${String(skipped)} 个非文本文件` : "");
4371
+ return `:成功 ${String(count)} 条` + (failed > 0 ? `,失败 ${String(failed)} 条(见返回详情)` : "");
4384
4372
  }
4385
- if ("chunks" in record) return `:${String(record["chunks"])} 个切片`;
4386
- if ("doc_id" in record) return `:doc_id=${String(record["doc_id"])}`;
4387
4373
  if ("removed" in record) return `:${record["removed"] === true ? "已删除" : "未找到"}`;
4388
4374
  if ("error" in record) return `:${String(record["error"])}`;
4389
4375
  }
@@ -4397,11 +4383,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4397
4383
  //#endregion
4398
4384
  //#region src/client/paging.ts
4399
4385
  /**
4400
- * Page merging for the two list tabs.
4386
+ * Page merging for the fact list.
4401
4387
  *
4402
4388
  * Kept out of `client/index.ts` so it can be tested without a DOM (the same reason `summarize.ts`
4403
- * lives on its own): these two functions are where page drift is handled, and page drift is the
4404
- * kind of bug that only shows up as a React key warning plus a row nobody can scroll to.
4389
+ * lives on its own): this is where page drift is handled, and page drift is the kind of bug that
4390
+ * only shows up as a React key warning plus a row nobody can scroll to.
4405
4391
  *
4406
4392
  * @module client/paging
4407
4393
  */
@@ -4416,15 +4402,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4416
4402
  function mergePage(previous, page) {
4417
4403
  return mergeById(previous, page, (row) => row.fact_id);
4418
4404
  }
4419
- /**
4420
- * Append one page of documents, dropping ids already present.
4421
- *
4422
- * Same window shift as `mergePage`: `kb.list` pages by `OFFSET` over `updated_at DESC`, which also
4423
- * has second precision, and a document ingested between two requests repeats a row.
4424
- */
4425
- function mergeDocs(previous, page) {
4426
- return mergeById(previous, page, (row) => row.doc_id);
4427
- }
4428
4405
  /** Append the rows whose id is not already loaded, preserving order; identity when nothing is new. */
4429
4406
  function mergeById(previous, page, idOf) {
4430
4407
  const seen = new Set(previous.map(idOf));
@@ -4432,72 +4409,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4432
4409
  return added.length === 0 ? previous : [...previous, ...added];
4433
4410
  }
4434
4411
  //#endregion
4435
- //#region src/client/domains.ts
4436
- /** The empty choice first, then every catalog domain in the host's order. */
4437
- function domainOptions(catalog, emptyLabel) {
4438
- const domains = catalog === null ? [] : catalog.domains;
4439
- return [{
4440
- value: "",
4441
- label: emptyLabel
4442
- }, ...domains.map((domain) => ({
4443
- value: domain,
4444
- label: domain
4445
- }))];
4446
- }
4447
- /**
4448
- * `'select'` when the allowlist is active (non-empty) — the control must not offer free input, or
4449
- * two spellings of one domain can still be created. `'input'` when it is empty (no restriction),
4450
- * and also while the catalog is not loaded yet: the form stays usable and the store is the final
4451
- * guard either way.
4452
- */
4453
- function domainPickerMode(catalog) {
4454
- return catalog !== null && catalog.restricted ? "select" : "input";
4455
- }
4456
- /** Classify one 「+」 draft against the domains the picker currently offers. */
4457
- function checkNewDomain(raw, existing) {
4458
- const name = raw.trim();
4459
- if (name === "") return { kind: "empty" };
4460
- if (name.includes("/") || name.includes("\\")) return {
4461
- kind: "invalid",
4462
- reason: "知识域不能包含 “/” 或 “\\”"
4463
- };
4464
- return existing.includes(name) ? {
4465
- kind: "existing",
4466
- name
4467
- } : {
4468
- kind: "new",
4469
- name
4470
- };
4471
- }
4472
- //#endregion
4473
- //#region src/client/ingest.ts
4474
- /** How many colliding documents the confirm dialog names before it summarizes the rest. */
4475
- const CONFLICT_LISTED = 8;
4476
- /**
4477
- * The host's collision report, or `null` for any other payload.
4478
- *
4479
- * Validated structurally rather than by a bare cast: the value crossed the Remote wire, and a
4480
- * truncated/garbled payload must fall through to the ordinary "完成" path instead of being shown
4481
- * as a confirmation for a conflict that was never reported.
4482
- */
4483
- function ingestConflict(value) {
4484
- if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
4485
- const record = value;
4486
- if (record["conflict"] !== true) return null;
4487
- if (!Array.isArray(record["conflicts"])) return null;
4488
- return record;
4489
- }
4490
- /** The question the 「入库」 form asks before re-running the SAME request with `overwrite`. */
4491
- function conflictMessage(report) {
4492
- const listed = report.conflicts.slice(0, CONFLICT_LISTED);
4493
- const lines = listed.map((conflict) => `· doc_id=${String(conflict.doc_id)}「${conflict.title}」\n ${conflict.path}`);
4494
- const rest = report.conflicts.length - listed.length;
4495
- return `已存在同名文档:将覆盖 ${String(report.would_overwrite)} 篇 / 新增 ${String(report.would_add)} 篇。
4496
-
4497
- 确认覆盖以下文档?
4498
- ` + lines.join("\n") + (rest > 0 ? `\n… 另有 ${String(rest)} 篇` : "") + "\n\n选择「取消」不会写入任何内容。";
4499
- }
4500
- //#endregion
4501
4412
  //#region src/client/wire.ts
4502
4413
  /**
4503
4414
  * The client half's wire-revision diagnostics.
@@ -4524,9 +4435,9 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4524
4435
  * the marker", never a decode failure.
4525
4436
  */
4526
4437
  function hostSkew(hostWire) {
4527
- if (hostWire === 2) return void 0;
4528
- if (hostWire === void 0) return `宿主没有回报 wire 版本(它比本客户端旧):面板可能缺少新字段,重启 dsh web 让两半对齐。(本客户端说 wire=${String(2)})`;
4529
- return `宿主回报的 wire 版本是 ${String(hostWire)},本客户端只认识 ${String(2)}:面板继续显示,但两半可能已经错位——重启 dsh web 后再试。`;
4438
+ if (hostWire === 4) return void 0;
4439
+ if (hostWire === void 0) return `宿主没有回报 wire 版本(它比本客户端旧):面板可能缺少新字段,重启 dsh web 让两半对齐。(本客户端说 wire=${String(4)})`;
4440
+ return `宿主回报的 wire 版本是 ${String(hostWire)},本客户端只认识 ${String(4)}:面板继续显示,但两半可能已经错位——重启 dsh web 后再试。`;
4530
4441
  }
4531
4442
  /**
4532
4443
  * Whether a host reporting `hostWire` can have registered a method introduced at `required`.
@@ -4567,9 +4478,8 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4567
4478
  /**
4568
4479
  * @avantf/dsh-mem — DSH client half.
4569
4480
  *
4570
- * Registers two `conversation.view` tabs (记忆 id=memory, 知识 id=knowledge) that sit
4571
- * next to 对话/轨迹 in the session view strip and talk to the host through the
4572
- * Typert Remote bridge `avantfMem`.
4481
+ * Registers the `conversation.view` tab 记忆 (id=memory) that sits next to 对话/轨迹 in the
4482
+ * session view strip and talks to the host through the Typert Remote bridge `avantfMem`.
4573
4483
  *
4574
4484
  * Design choices worth keeping:
4575
4485
  *
@@ -4582,43 +4492,29 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4582
4492
  * 2. Every remote call is normalized and surfaced: the raw response is logged
4583
4493
  * (prefixed `[avantf-mem]`) and rendered as an explicit status/error line, so
4584
4494
  * an empty result is never indistinguishable from a failed call.
4585
- * 3. Full DESIGN §12 surface: tab A does CRUD (edit/archive/restore/feedback) +
4586
- * fact detail (entities/triples) + open contradictions; tab B does filtered
4587
- * cross-store query (kind/domain/source) with results grouped by
4588
- * `记忆 / domain → source`, plus KB management (ingest/import/list/detail/
4589
- * remove/reindex). Generation stays external (Plan A) — no answer synthesis.
4495
+ * 3. Full DESIGN §12 surface: the tab does CRUD (edit/archive/restore/feedback) + fact detail
4496
+ * (entities/triples) + open contradictions. Generation stays external (Plan A) — no answer
4497
+ * synthesis.
4590
4498
  *
4591
4499
  * Built as a DSH client bundle (`tsdown.config.ts` → `clientBundle`): a CJS
4592
4500
  * factory self-registering through `window.__ModuleLoader__.load`, with `react`
4593
4501
  * resolved from the platform module table.
4594
4502
  */
4595
4503
  const h = react.createElement;
4596
- /**
4597
- * The host's answer for one source string, NORMALIZED at the boundary that receives it.
4598
- *
4599
- * The shape comes from the contract (no hand-mirror to drift), and the three list fields are read
4600
- * through {@link normalizeClassification} rather than assumed: this object is dereferenced inside a
4601
- * render body (`info.paths.length`, `info.missing.join`), and a field the host stopped sending would
4602
- * throw there — which the shell renders as an empty panel with no retry for the session.
4603
- */
4604
- function normalizeClassification(value) {
4605
- if (value === null || value === void 0) return null;
4606
- const list = (candidate) => Array.isArray(candidate) ? candidate.filter((entry) => typeof entry === "string") : [];
4607
- return {
4608
- kind: value.kind,
4609
- paths: list(value.paths),
4610
- missing: list(value.missing),
4611
- reasons: list(value.reasons),
4612
- files: typeof value.files === "number" ? value.files : 0
4613
- };
4614
- }
4615
4504
  const NOT_MOUNTED = "avantfMem Remote 未挂载:本插件自带的 typert contribution 挂载失败(详见控制台错误),暂时无法读取数据。";
4616
4505
  /** One 记忆 tab; the host `admin` list default is the same. */
4617
4506
  const MEMORY_PAGE_SIZE = 50;
4507
+ /** The exact page default; fuzzy is fixed at five by the engine. */
4508
+ const SEARCH_LIMIT = 10;
4509
+ /**
4510
+ * How many `admin.detail` calls one search answer may trigger, to give its results the list's action
4511
+ * row (status / pin state / full text). A hard cap so a large exact page or a wide relation answer
4512
+ * cannot turn one keystroke into an unbounded fan-out; anything past the cap keeps the id-only
4513
+ * actions (详情 / 编辑 when the excerpt is complete / 归档 / 反馈), which still work.
4514
+ */
4515
+ const SEARCH_FACT_LOOKUPS_MAX = 20;
4618
4516
  /** How close (px) the list's end must come to the scrollport before the next page loads. */
4619
4517
  const AUTO_LOAD_MARGIN_PX = 300;
4620
- /** One page of the 知识 tab's document list; the 记忆 tab pages at 50 for the same reason. */
4621
- const DOC_PAGE_SIZE = 50;
4622
4518
  /**
4623
4519
  * The wire revision the host last reported, and whether its absence/mismatch was logged.
4624
4520
  *
@@ -4776,23 +4672,6 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4776
4672
  hooks.setLoading(false);
4777
4673
  }
4778
4674
  }
4779
- /** The settings shell's search glyph, inlined so this bundle needs no shared icon package. */
4780
- function SearchIcon() {
4781
- return h("svg", {
4782
- width: 16,
4783
- height: 16,
4784
- viewBox: "0 0 16 16",
4785
- fill: "none",
4786
- xmlns: "http://www.w3.org/2000/svg",
4787
- "aria-hidden": true
4788
- }, h("path", {
4789
- d: "M11.894845 6.647401C11.894845 3.725463 9.534486 1.356779 6.623219 1.35657C3.711786 1.35657 1.351635 3.725338 1.351635 6.647401C1.351843 9.569296 3.711911 11.938273 6.623219 11.938273C9.534361 11.938064 11.894637 9.569171 11.894845 6.647401ZM13.245462 6.647401C13.245254 10.317935 10.280401 13.293613 6.623219 13.293821C2.965871 13.293821 0.000204 10.31806 0 6.647401C0 2.976574 2.965746 0 6.623219 0C10.280526 0.000205 13.245462 2.9767 13.245462 6.647401Z",
4790
- fill: "currentColor"
4791
- }), h("path", {
4792
- d: "M16.000417 15.041079L15.044449 16.000433L11.530434 12.473588L12.486298 11.514234L16.000417 15.041079Z",
4793
- fill: "currentColor"
4794
- }));
4795
- }
4796
4675
  function Btn(props) {
4797
4676
  return h("button", {
4798
4677
  type: "button",
@@ -4801,135 +4680,85 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4801
4680
  onClick: props.onClick
4802
4681
  }, props.label);
4803
4682
  }
4804
- function Field(props) {
4805
- return h("label", { className: pages_module_css_default.field }, props.label, h("input", {
4806
- className: pages_module_css_default.input,
4807
- type: "text",
4808
- value: props.value,
4809
- placeholder: props.placeholder,
4810
- autoFocus: props.autoFocus === true,
4811
- onKeyDown: props.onKeyDown,
4812
- onChange: (e) => {
4813
- props.onChange(e.target.value);
4814
- }
4815
- }));
4816
- }
4817
4683
  /**
4818
- * The 知识域 control.
4819
- *
4820
- * With a configured `knowledge.domains` allowlist the host returns `restricted: true` and this is
4821
- * a closed `<select>` over the allowlist ∪ the library's existing domains — a new domain is then a
4822
- * config change, not a second spelling. An explicitly empty allowlist means "no restriction", so
4823
- * the control stays a free input (the host's options still show what is already in use).
4684
+ * The minimal relation editor of a NOTE's detail (STRUCTURED_RELATIONS §2/§5).
4824
4685
  *
4825
- * The WRITE form passes `onAdd`, which adds a 「+」 on the field's own LABEL row, right-aligned with
4826
- * the select: adding a domain is a user action (the agent stays bounded by the configured list), so
4827
- * the picker must not force a hand-edit of YAML. It is deliberately absent while the allowlist is
4828
- * `[]`: appending to an unfettered list would silently turn "no restriction" into a restricted one.
4686
+ * Read and write both go through the existing faces: `admin.detail` already carries `relations`
4687
+ * (the explicit edges), and a save is the ordinary `remember` update. There is deliberately no new
4688
+ * action and no separate graph editor — the three slots ARE the graph's vocabulary, and the engine
4689
+ * normalizes them (a name gate refusal comes back as the update's error line).
4829
4690
  *
4830
- * The control carries NO explanatory sentence: the field sits in a row of sibling filters, and one
4831
- * extra line under the select made that row taller than its neighbours. The 「+」 says what it does.
4691
+ * Rendered only for a note (the caller owns that rule): a journal refuses relations, so an editor
4692
+ * there would be a control whose every use fails.
4832
4693
  */
4833
- function DomainField(props) {
4834
- const [adding, setAdding] = react.useState(false);
4835
- const [draft, setDraft] = react.useState("");
4836
- const [addError, setAddError] = react.useState();
4837
- const [addBusy, setAddBusy] = react.useState(false);
4838
- if (domainPickerMode(props.catalog) === "input") return h(Field, {
4839
- label: props.label,
4840
- value: props.value,
4841
- onChange: props.onChange,
4842
- placeholder: props.placeholder
4694
+ function RelationEditor(props) {
4695
+ const relations = props.fact.relations ?? [];
4696
+ const [draft, setDraft] = react.useState({
4697
+ subject: "",
4698
+ predicate: "",
4699
+ object: ""
4843
4700
  });
4844
- const domains = props.catalog?.domains ?? [];
4845
- const closeAdd = () => {
4846
- setAdding(false);
4847
- setDraft("");
4848
- setAddError(void 0);
4849
- };
4850
- const submitAdd = () => {
4851
- const check = checkNewDomain(draft, domains);
4852
- if (check.kind === "empty") {
4853
- setAddError("知识域不能为空");
4854
- return;
4855
- }
4856
- if (check.kind === "invalid") {
4857
- setAddError(check.reason);
4858
- return;
4701
+ const complete = draft.subject.trim() !== "" && draft.predicate.trim() !== "" && draft.object.trim() !== "";
4702
+ const field = (slot, label) => h("input", {
4703
+ className: pages_module_css_default.input,
4704
+ value: draft[slot],
4705
+ placeholder: label,
4706
+ "aria-label": `关系${label}`,
4707
+ disabled: props.disabled,
4708
+ onChange: (e) => {
4709
+ setDraft((current) => ({
4710
+ ...current,
4711
+ [slot]: e.target.value
4712
+ }));
4859
4713
  }
4860
- if (check.kind === "existing") {
4861
- props.onChange(check.name);
4862
- closeAdd();
4863
- return;
4714
+ });
4715
+ return h("div", { className: pages_module_css_default.detail }, h("div", { className: pages_module_css_default.meta }, "声明的显式关系(仅 note;图上的 source=explicit):"), relations.length === 0 ? h("div", { className: pages_module_css_default.status }, "当前没有显式关系。") : h("ul", { className: pages_module_css_default.list }, relations.map((relation, index) => h("li", {
4716
+ key: `relation-${String(index)}`,
4717
+ className: pages_module_css_default.item
4718
+ }, `${relation.subject} —${relation.predicate}→ ${relation.object}`, h("div", { className: pages_module_css_default.row }, h(Btn, {
4719
+ label: "删除",
4720
+ danger: true,
4721
+ disabled: props.disabled,
4722
+ onClick: () => {
4723
+ props.onSave(relations.filter((_, i) => i !== index));
4864
4724
  }
4865
- const onAdd = props.onAdd;
4866
- if (onAdd === void 0) return;
4867
- setAddBusy(true);
4868
- onAdd(check.name).then((addFailure) => {
4869
- setAddBusy(false);
4870
- if (addFailure !== void 0) {
4871
- setAddError(addFailure);
4872
- return;
4873
- }
4874
- props.onChange(check.name);
4875
- closeAdd();
4876
- });
4877
- };
4878
- const addButton = props.onAdd === void 0 || adding ? null : h(Btn, {
4879
- label: "+ 新增领域",
4725
+ }))))), h("div", { className: pages_module_css_default.row }, field("subject", "主语"), field("predicate", "谓词"), field("object", "宾语"), h(Btn, {
4726
+ label: "添加关系",
4727
+ disabled: props.disabled || !complete,
4880
4728
  onClick: () => {
4881
- setAdding(true);
4729
+ props.onSave([...relations.map((relation) => ({
4730
+ subject: relation.subject,
4731
+ predicate: relation.predicate,
4732
+ object: relation.object
4733
+ })), {
4734
+ subject: draft.subject.trim(),
4735
+ predicate: draft.predicate.trim(),
4736
+ object: draft.object.trim()
4737
+ }]);
4738
+ setDraft({
4739
+ subject: "",
4740
+ predicate: "",
4741
+ object: ""
4742
+ });
4882
4743
  }
4883
- });
4884
- return h(react.Fragment, null, h("label", {
4885
- className: pages_module_css_default.field,
4886
- key: "domain"
4887
- }, h("span", { className: pages_module_css_default.fieldHead }, h("span", null, props.label), addButton), h("select", {
4888
- className: pages_module_css_default.input,
4889
- value: props.value,
4890
- onChange: (e) => {
4891
- props.onChange(e.target.value);
4744
+ })));
4745
+ }
4746
+ /**
4747
+ * The detail body of one fact — the SAME view under an admin list row and under the search results
4748
+ * (the search view used to hide the list, which is why its 「详情」 button appeared to do nothing).
4749
+ *
4750
+ * It owns no data of its own: the fact comes from `admin.detail` and every write is delegated back
4751
+ * to the panel, which routes it through the existing `remember`/`admin` actions.
4752
+ */
4753
+ function FactDetailView(props) {
4754
+ const fact = props.fact;
4755
+ return h("div", { className: pages_module_css_default.meta }, `标题:${fact.title ?? "(无标题)"}`, h("br"), `创建于 ${fact.created_at} / 更新于 ${fact.updated_at ?? "—"}`, h("br"), `实体:${(fact.entities ?? []).join("、") || "(无)"}`, h("br"), `三元组:${(fact.triples ?? []).map((t) => `${t.subj} —${t.pred}→ ${t.obj}(${t.confidence.toFixed(2)})`).join(";") || "(无)"}`, h("br"), `检索次数:${String(fact.retrieval_count ?? 0)}${fact.supersedes_id ? ` · 修订自 #${String(fact.supersedes_id)}` : ""} · 被断言 ${String(fact.assert_count ?? 1)} 次`, h("br"), `有效期:${fact.valid_from ?? "未知"} → ${fact.valid_to ?? (fact.status === "active" ? "仍在有效" : "未知")}` + (fact.superseded_by === null || fact.superseded_by === void 0 ? "" : ` · 被 #${String(fact.superseded_by)} 取代`), h("br"), `来源:${(fact.sources ?? []).length === 0 ? "(未记录)" : (fact.sources ?? []).map((s) => `${s.kind}:${s.ref}`).join(";")}`, h("br"), `关系:${(fact.relations ?? []).length === 0 ? "(无)" : (fact.relations ?? []).map((r) => `${r.subject} —${r.predicate}→ ${r.object}`).join(";")}`, fact.kind === "note" && fact.status === "active" ? h(RelationEditor, {
4756
+ fact,
4757
+ disabled: props.disabled,
4758
+ onSave: (relations) => {
4759
+ props.onSaveRelations(fact, relations);
4892
4760
  }
4893
- }, domainOptions(props.catalog, props.emptyLabel).map((option) => h("option", {
4894
- key: option.value,
4895
- value: option.value
4896
- }, option.label)))), props.onAdd === void 0 || !adding ? null : [
4897
- h(Field, {
4898
- key: "add-input",
4899
- label: "新知识域(domain)",
4900
- value: draft,
4901
- onChange: (value) => {
4902
- setDraft(value);
4903
- setAddError(void 0);
4904
- },
4905
- placeholder: "如 legal",
4906
- autoFocus: true,
4907
- onKeyDown: (e) => {
4908
- if (e.key === "Enter") {
4909
- e.preventDefault();
4910
- submitAdd();
4911
- } else if (e.key === "Escape") {
4912
- e.preventDefault();
4913
- closeAdd();
4914
- }
4915
- }
4916
- }),
4917
- h(Btn, {
4918
- key: "add-ok",
4919
- label: addBusy ? "新增中…" : "确定",
4920
- disabled: addBusy,
4921
- onClick: submitAdd
4922
- }),
4923
- h(Btn, {
4924
- key: "add-cancel",
4925
- label: "取消",
4926
- onClick: closeAdd
4927
- }),
4928
- addError === void 0 ? null : h("span", {
4929
- key: "add-error",
4930
- className: pages_module_css_default.error
4931
- }, addError)
4932
- ]);
4761
+ }) : null);
4933
4762
  }
4934
4763
  function MemoryPanel(props) {
4935
4764
  const [statusFilter, setStatusFilter] = react.useState("active");
@@ -4949,9 +4778,23 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4949
4778
  const [detail, setDetail] = react.useState(null);
4950
4779
  const [contradictions, setContradictions] = react.useState(null);
4951
4780
  const [health, setHealth] = react.useState(null);
4781
+ const [searchText, setSearchText] = react.useState("");
4782
+ const [searchKind, setSearchKind] = react.useState("all");
4783
+ const [searchHits, setSearchHits] = react.useState(null);
4784
+ const [searchRelation, setSearchRelation] = react.useState(null);
4785
+ const [searchFactPage, setSearchFactPage] = react.useState(null);
4786
+ /**
4787
+ * What the CURRENT search results need so their action row can match an admin list row:
4788
+ * status (归档 vs 恢复), pin state (永久 vs 解除) and the FULL content (a hit's `text` may be
4789
+ * truncated by the output budget, and editing a truncated excerpt would corrupt the fact).
4790
+ * Read through `admin.detail` — one call per result, bounded by {@link SEARCH_FACT_LOOKUPS_MAX}.
4791
+ */
4792
+ const [searchFacts, setSearchFacts] = react.useState({});
4793
+ const [searching, setSearching] = react.useState(false);
4952
4794
  const detailGuard = useSeqGuard();
4953
4795
  const contradictionGuard = useSeqGuard();
4954
4796
  const healthGuard = useSeqGuard();
4797
+ const searchGuard = useSeqGuard();
4955
4798
  const listGuard = useSeqGuard();
4956
4799
  /**
4957
4800
  * Load one page and append it (offset 0 replaces the list). Resolves once the
@@ -4989,6 +4832,107 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
4989
4832
  react.useEffect(() => {
4990
4833
  load(0, statusFilter);
4991
4834
  }, [load, statusFilter]);
4835
+ /**
4836
+ * Resolve the state of the facts a search answer points at (`admin.detail`, one call each).
4837
+ *
4838
+ * This is what lets a RESULT carry the same actions as a list row. Without it the panel can only
4839
+ * know the id and an excerpt, and would have to guess 「归档/恢复」 and 「永久/解除」 — two labels
4840
+ * whose opposite is a different operation. Results are bounded (search is a fixed shortlist, and
4841
+ * relation evidence is the paths' facts), deduplicated, and dropped if a newer search started.
4842
+ */
4843
+ const loadSearchFacts = (seq, ids) => {
4844
+ const remote = props.remote;
4845
+ if (remote === void 0) return;
4846
+ const unique = [...new Set(ids)].slice(0, SEARCH_FACT_LOOKUPS_MAX);
4847
+ if (unique.length === 0) {
4848
+ setSearchFacts({});
4849
+ return;
4850
+ }
4851
+ Promise.all(unique.map(async (id) => ({
4852
+ id,
4853
+ outcome: await callRemote(`admin.detail(${String(id)})`, remote.admin({
4854
+ action: "detail",
4855
+ fact_id: id
4856
+ }))
4857
+ }))).then((results) => {
4858
+ if (!searchGuard.isCurrent(seq)) return;
4859
+ const next = {};
4860
+ for (const { id, outcome } of results) {
4861
+ if (!outcome.ok) continue;
4862
+ const value = outcome.value;
4863
+ if (value === null || typeof value !== "object" || !("fact_id" in value)) continue;
4864
+ next[id] = {
4865
+ status: value.status,
4866
+ pinned: value.pinned === true,
4867
+ content: value.content
4868
+ };
4869
+ }
4870
+ setSearchFacts(next);
4871
+ });
4872
+ };
4873
+ /** Search memory through the same `recall` remote the model tool uses. */
4874
+ const runSearch = async () => {
4875
+ if (props.remote === void 0) return;
4876
+ const query = searchText.trim();
4877
+ if (query === "") {
4878
+ searchGuard.begin();
4879
+ setSearchHits(null);
4880
+ setSearchRelation(null);
4881
+ setSearchFactPage(null);
4882
+ setSearchFacts({});
4883
+ setSearching(false);
4884
+ setError(void 0);
4885
+ return;
4886
+ }
4887
+ const seq = searchGuard.begin();
4888
+ setSearching(true);
4889
+ setError(void 0);
4890
+ const outcome = await callRemote(`recall.search(${query}${searchKind === "all" ? "" : `, kind=${searchKind}`})`, props.remote.recall({
4891
+ action: "search",
4892
+ query,
4893
+ limit: SEARCH_LIMIT,
4894
+ ...searchKind === "all" ? {} : { kind: searchKind }
4895
+ }));
4896
+ if (!searchGuard.isCurrent(seq)) return;
4897
+ const value = outcome.ok ? outcome.value : void 0;
4898
+ if (value !== void 0 && value.mode === "relation") {
4899
+ setSearchRelation({
4900
+ paths: value.paths,
4901
+ evidence: value.evidence
4902
+ });
4903
+ setSearchHits(null);
4904
+ setSearchFactPage(null);
4905
+ loadSearchFacts(seq, value.evidence.map((entry) => entry.fact_id));
4906
+ } else if (value !== void 0 && value.mode === "facts") {
4907
+ setSearchFactPage(value.facts);
4908
+ setSearchHits(null);
4909
+ setSearchRelation(null);
4910
+ loadSearchFacts(seq, value.facts.map((entry) => entry.fact_id));
4911
+ } else if (value !== void 0) {
4912
+ setSearchHits(value.hits);
4913
+ setSearchRelation(null);
4914
+ setSearchFactPage(null);
4915
+ loadSearchFacts(seq, value.hits.map((hit) => hit.ref_id));
4916
+ } else {
4917
+ setSearchHits([]);
4918
+ setSearchRelation(null);
4919
+ setSearchFactPage(null);
4920
+ setSearchFacts({});
4921
+ if (!outcome.ok) setError(outcome.error);
4922
+ }
4923
+ setSearching(false);
4924
+ };
4925
+ /** Clear the query and the search result view (never the memory rows themselves). */
4926
+ const clearSearch = () => {
4927
+ searchGuard.begin();
4928
+ setSearchText("");
4929
+ setSearchHits(null);
4930
+ setSearchRelation(null);
4931
+ setSearchFactPage(null);
4932
+ setSearchFacts({});
4933
+ setSearching(false);
4934
+ setError(void 0);
4935
+ };
4992
4936
  /** Switch the status tab: clear the other tab's page immediately, and drop its in-flight page. */
4993
4937
  const switchStatus = (next) => {
4994
4938
  if (next === statusFilter) return;
@@ -5021,12 +4965,13 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5021
4965
  load,
5022
4966
  statusFilter
5023
4967
  ]);
5024
- const showDetail = (factId) => {
4968
+ /**
4969
+ * Read one fact's detail. Kept separate from the toggle because a relation mutation changes the
4970
+ * detail it was performed in: the panel stays open and re-reads it, instead of leaving a stale
4971
+ * relation list under a "完成" notice.
4972
+ */
4973
+ const fetchDetail = (factId) => {
5025
4974
  if (props.remote === void 0) return;
5026
- if (detail?.id === factId) {
5027
- setDetail(null);
5028
- return;
5029
- }
5030
4975
  const seq = detailGuard.begin();
5031
4976
  callRemote(`admin.detail(${String(factId)})`, props.remote.admin({
5032
4977
  action: "detail",
@@ -5049,13 +4994,186 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5049
4994
  });
5050
4995
  });
5051
4996
  };
4997
+ const showDetail = (factId) => {
4998
+ if (props.remote === void 0) return;
4999
+ if (detail?.id === factId) {
5000
+ setDetail(null);
5001
+ return;
5002
+ }
5003
+ fetchDetail(factId);
5004
+ };
5005
+ /**
5006
+ * Replace a note's declared relations (STRUCTURED_RELATIONS §2/§5).
5007
+ *
5008
+ * Goes through the EXISTING `remember` update — no new action, no new wire method: a supplied
5009
+ * `relations` list replaces the set, and the content is sent unchanged so the write lands on the
5010
+ * same row (that is also the only way `[]` can delete the last relation). The detail is re-read
5011
+ * after the refresh, so the edge that just disappeared cannot linger on screen.
5012
+ */
5013
+ const saveRelations = (fact, relations) => {
5014
+ mutate(`更新 #${String(fact.fact_id)} 的关系`, (remote) => remote.remember({
5015
+ action: "update",
5016
+ fact_id: fact.fact_id,
5017
+ content: fact.content,
5018
+ relations
5019
+ }), { after: () => {
5020
+ fetchDetail(fact.fact_id);
5021
+ } });
5022
+ };
5023
+ /**
5024
+ * The per-fact action buttons — ONE implementation for the admin list and the search results.
5025
+ *
5026
+ * The regression this replaced: the actions were written inline in the list row, so when
5027
+ * `searchActive` hid the list, a hit's 「详情」 had nowhere to render and there was no way to
5028
+ * edit/archive/feedback a fact found by search. `fromSearch` only decides what happens AFTER the
5029
+ * write: a search view must re-run the query (its results are the thing on screen), while the
5030
+ * list refresh (`mutate`'s `refreshFirst`) stays the same in both cases.
5031
+ *
5032
+ * A field the caller does not know is `undefined` and its button is OMITTED — never shown with a
5033
+ * guessed label (「恢复」 on an active fact or 「永久记忆」 on a pinned one is a different write).
5034
+ */
5035
+ const factButtons = (fact, fromSearch) => {
5036
+ const run = (label, call, summarize = false) => {
5037
+ mutate(label, call, fromSearch ? {
5038
+ ...summarize ? { summarize: true } : {},
5039
+ after: () => {
5040
+ runSearch();
5041
+ }
5042
+ } : summarize ? { summarize: true } : void 0);
5043
+ };
5044
+ if (editing?.id === fact.fact_id) return [h(Btn, {
5045
+ key: "save",
5046
+ label: "保存",
5047
+ disabled: loading || editing.text.trim().length === 0,
5048
+ onClick: () => {
5049
+ const text = editing.text.trim();
5050
+ setEditing(null);
5051
+ run(`更新 #${String(fact.fact_id)}`, (remote) => remote.remember({
5052
+ action: "update",
5053
+ fact_id: fact.fact_id,
5054
+ content: text
5055
+ }), true);
5056
+ }
5057
+ }), h(Btn, {
5058
+ key: "cancel",
5059
+ label: "取消",
5060
+ disabled: loading,
5061
+ onClick: () => {
5062
+ setEditing(null);
5063
+ }
5064
+ })];
5065
+ const content = fact.content;
5066
+ return [
5067
+ h(Btn, {
5068
+ key: "detail",
5069
+ label: detail?.id === fact.fact_id ? "收起详情" : "详情",
5070
+ disabled: loading,
5071
+ onClick: () => {
5072
+ showDetail(fact.fact_id);
5073
+ }
5074
+ }),
5075
+ fact.status === "active" && content !== void 0 ? h(Btn, {
5076
+ key: "edit",
5077
+ label: "编辑",
5078
+ disabled: loading,
5079
+ onClick: () => {
5080
+ setEditing({
5081
+ id: fact.fact_id,
5082
+ text: content
5083
+ });
5084
+ setDetail(null);
5085
+ }
5086
+ }) : null,
5087
+ fact.status === "active" ? h(Btn, {
5088
+ key: "helpful",
5089
+ label: "有用 +1",
5090
+ disabled: loading,
5091
+ onClick: () => {
5092
+ run(`反馈有用 #${String(fact.fact_id)}`, (remote) => remote.remember({
5093
+ action: "helpful",
5094
+ fact_id: fact.fact_id
5095
+ }));
5096
+ }
5097
+ }) : null,
5098
+ fact.status === "active" ? h(Btn, {
5099
+ key: "unhelpful",
5100
+ label: "没用 −1",
5101
+ disabled: loading,
5102
+ onClick: () => {
5103
+ run(`反馈没用 #${String(fact.fact_id)}`, (remote) => remote.remember({
5104
+ action: "unhelpful",
5105
+ fact_id: fact.fact_id
5106
+ }));
5107
+ }
5108
+ }) : null,
5109
+ fact.status === "active" ? h(Btn, {
5110
+ key: "archive",
5111
+ label: "归档",
5112
+ disabled: loading,
5113
+ onClick: () => {
5114
+ run(`归档 #${String(fact.fact_id)}`, (remote) => remote.admin({
5115
+ action: "archive",
5116
+ fact_id: fact.fact_id,
5117
+ reason: "ui"
5118
+ }));
5119
+ }
5120
+ }) : h(Btn, {
5121
+ key: "restore",
5122
+ label: "恢复",
5123
+ disabled: loading,
5124
+ onClick: () => {
5125
+ run(`恢复 #${String(fact.fact_id)}`, (remote) => remote.admin({
5126
+ action: "restore",
5127
+ fact_id: fact.fact_id
5128
+ }));
5129
+ }
5130
+ }),
5131
+ fact.status === "active" && fact.pinned !== void 0 ? fact.pinned ? h(Btn, {
5132
+ key: "unpin",
5133
+ label: "解除永久",
5134
+ disabled: loading,
5135
+ onClick: () => {
5136
+ run(`解除永久 #${String(fact.fact_id)}`, (remote) => remote.admin({
5137
+ action: "unpin",
5138
+ fact_id: fact.fact_id
5139
+ }));
5140
+ }
5141
+ }) : h(Btn, {
5142
+ key: "pin",
5143
+ label: "永久记忆",
5144
+ disabled: loading,
5145
+ onClick: () => {
5146
+ run(`固化永久 #${String(fact.fact_id)}`, (remote) => remote.admin({
5147
+ action: "pin",
5148
+ fact_id: fact.fact_id
5149
+ }));
5150
+ }
5151
+ }) : null
5152
+ ];
5153
+ };
5154
+ /** A result row's action state: the fetched detail when available, the id/excerpt as fallback. */
5155
+ const actionFact = (id, excerpt, truncated) => {
5156
+ const state = searchFacts[id];
5157
+ if (state === void 0) return {
5158
+ fact_id: id,
5159
+ status: "active",
5160
+ content: truncated ? void 0 : excerpt,
5161
+ pinned: void 0
5162
+ };
5163
+ return {
5164
+ fact_id: id,
5165
+ status: state.status,
5166
+ content: state.content,
5167
+ pinned: state.pinned
5168
+ };
5169
+ };
5052
5170
  /** Load the open conflicts. Separate from the toggle so a verdict can re-read the list. */
5053
5171
  const fetchContradictions = react.useCallback(() => {
5054
5172
  const remote = props.remote;
5055
5173
  if (remote === void 0) return;
5056
5174
  const seq = contradictionGuard.begin();
5057
- callRemote("recall.contradict", remote.recall({
5058
- action: "contradict",
5175
+ callRemote("admin.contradictions", remote.admin({
5176
+ action: "contradictions",
5059
5177
  limit: 20
5060
5178
  })).then((outcome) => {
5061
5179
  if (!contradictionGuard.isCurrent(seq)) return;
@@ -5115,9 +5233,11 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5115
5233
  react.useEffect(() => {
5116
5234
  reveal();
5117
5235
  }, []);
5236
+ /** Whether the search view owns the panel (the admin list must not compete with it). */
5237
+ const searchActive = searching || searchHits !== null || searchRelation !== null || searchFactPage !== null;
5118
5238
  /** Load the next page once the list's end is within reach of the scrollport. */
5119
5239
  const maybeLoadMore = () => {
5120
- if (!truncated || loading || props.remote === void 0) return;
5240
+ if (searchActive || !truncated || loading || props.remote === void 0) return;
5121
5241
  load(nextOffset, statusFilter);
5122
5242
  };
5123
5243
  useLoadMoreOnScroll(rootRef, truncated && !loading, maybeLoadMore, rows.length);
@@ -5158,7 +5278,87 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5158
5278
  loadHealth();
5159
5279
  if (opening) reveal();
5160
5280
  }
5161
- }), h("div", { className: pages_module_css_default.status }, loading && shown === 0 ? "读取中…" : summary)), error !== void 0 ? h("div", { className: pages_module_css_default.error }, `错误:${error}`) : null, notice !== void 0 ? h("div", { className: pages_module_css_default.status }, notice) : null, props.remote !== void 0 && !loading && error === void 0 && shown === 0 ? h("div", { className: pages_module_css_default.status }, statusFilter === "active" ? "当前没有活动的事实。可在对话里让 agent 用 mem_remember 写入。" : "当前没有归档的事实。") : null, health !== null ? h(HealthBlock, { stats: health }) : null, contradictions !== null ? contradictions.length === 0 ? h("div", { className: pages_module_css_default.status }, "没有未处理的矛盾。") : h("ul", { className: pages_module_css_default.list }, contradictions.map((c) => h("li", {
5281
+ }), h("div", { className: pages_module_css_default.status }, loading && shown === 0 ? "读取中…" : summary)), props.remote === void 0 ? null : h("div", { className: pages_module_css_default.toolbar }, h("div", { className: `${pages_module_css_default.search} ${pages_module_css_default.searchField}` }, h("input", {
5282
+ value: searchText,
5283
+ placeholder: "搜索记忆…(Enter)",
5284
+ "aria-label": "搜索记忆",
5285
+ onChange: (e) => {
5286
+ setSearchText(e.target.value);
5287
+ },
5288
+ onKeyDown: (e) => {
5289
+ if (e.key === "Enter") {
5290
+ e.preventDefault();
5291
+ runSearch();
5292
+ }
5293
+ }
5294
+ }), searchText !== "" || searchHits !== null || searchFactPage !== null || searchRelation !== null ? h("button", {
5295
+ type: "button",
5296
+ className: pages_module_css_default.clear,
5297
+ "aria-label": "清除搜索",
5298
+ title: "清除搜索",
5299
+ onClick: clearSearch
5300
+ }, "×") : null), h("select", {
5301
+ className: pages_module_css_default.select,
5302
+ value: searchKind,
5303
+ "aria-label": "记忆层",
5304
+ onChange: (e) => {
5305
+ setSearchKind(e.target.value);
5306
+ }
5307
+ }, h("option", { value: "all" }, "全部"), h("option", { value: "journal" }, "journal"), h("option", { value: "note" }, "note"))), searchActive ? h("div", { className: pages_module_css_default.searchPanel }, h("div", { className: pages_module_css_default.searchTitle }, searching ? "搜索中…" : searchFactPage !== null ? `多实体事实:${String(searchFactPage.length)} 条${searchKind === "all" ? "" : `(${searchKind})`}` : searchRelation !== null ? `关系结果:${String(searchRelation.paths.length)} 条路径${searchKind === "all" ? "" : `(${searchKind})`}` : `搜索结果:${String(searchHits?.length ?? 0)} 条${searchKind === "all" ? "" : `(${searchKind})`}`, searchText.trim() === "" ? null : h("span", { className: pages_module_css_default.searchQuery }, `“${searchText.trim()}”`)), searchFactPage !== null ? h("div", null, searchFactPage.length === 0 ? h("div", { className: pages_module_css_default.status }, "没有同时提到这些实体的事实。") : h("ul", { className: pages_module_css_default.list }, searchFactPage.map((entry) => h("li", {
5308
+ key: `facts-${String(entry.fact_id)}`,
5309
+ className: pages_module_css_default.item
5310
+ }, entry.title === void 0 ? null : h("div", { className: pages_module_css_default.meta }, `标题:${entry.title}`), editing?.id === entry.fact_id ? h("textarea", {
5311
+ className: pages_module_css_default.textarea,
5312
+ value: editing.text,
5313
+ rows: 3,
5314
+ onChange: (e) => {
5315
+ setEditing({
5316
+ id: entry.fact_id,
5317
+ text: e.target.value
5318
+ });
5319
+ }
5320
+ }) : entry.content, entry.truncated === true ? h("span", { className: pages_module_css_default.meta }, "(已截断)") : null, h("span", { className: pages_module_css_default.meta }, `#${String(entry.fact_id)} · ${entry.memory_kind} · ${entry.status} · ${entry.event_date ?? entry.created_at}` + (entry.source_ref === void 0 ? "" : ` · ${entry.source_ref}`)), h("div", { className: pages_module_css_default.row }, factButtons(actionFact(entry.fact_id, entry.content, entry.truncated === true), true)), detail?.id === entry.fact_id ? h(FactDetailView, {
5321
+ fact: detail.data,
5322
+ disabled: loading,
5323
+ onSaveRelations: saveRelations
5324
+ }) : null)))) : searchRelation !== null ? h("div", null, searchRelation.paths.length === 0 ? h("div", { className: pages_module_css_default.status }, "没有关系路径。") : h("ul", { className: pages_module_css_default.list }, searchRelation.paths.map((path, index) => h("li", {
5325
+ key: `relation-path-${String(index)}`,
5326
+ className: pages_module_css_default.item
5327
+ }, [path.start.name, ...path.edges.flatMap((edge) => [edge.predicate, edge.object])].join(" → "), h("span", { className: pages_module_css_default.meta }, `${String(path.hops)} 跳 · #${path.edges.map((edge) => String(edge.fact_id)).join(" / ")} · 置信度 ${path.confidence.toFixed(2)}`)))), searchRelation.evidence.length === 0 ? null : h("div", null, h("div", { className: pages_module_css_default.meta }, "证据事实:"), h("ul", { className: pages_module_css_default.list }, searchRelation.evidence.map((entry) => h("li", {
5328
+ key: `relation-evidence-${String(entry.fact_id)}`,
5329
+ className: pages_module_css_default.item
5330
+ }, entry.title === void 0 ? null : h("div", { className: pages_module_css_default.meta }, `标题:${entry.title}`), editing?.id === entry.fact_id ? h("textarea", {
5331
+ className: pages_module_css_default.textarea,
5332
+ value: editing.text,
5333
+ rows: 3,
5334
+ onChange: (e) => {
5335
+ setEditing({
5336
+ id: entry.fact_id,
5337
+ text: e.target.value
5338
+ });
5339
+ }
5340
+ }) : entry.text, entry.truncated === true ? h("span", { className: pages_module_css_default.meta }, "(已截断)") : null, h("span", { className: pages_module_css_default.meta }, `#${String(entry.fact_id)} · ${entry.memory_kind} · ${entry.created_at}`), h("div", { className: pages_module_css_default.row }, factButtons(actionFact(entry.fact_id, entry.text, entry.truncated === true), true)), detail?.id === entry.fact_id ? h(FactDetailView, {
5341
+ fact: detail.data,
5342
+ disabled: loading,
5343
+ onSaveRelations: saveRelations
5344
+ }) : null))))) : searchHits === null || searchHits.length === 0 ? h("div", { className: pages_module_css_default.status }, searching ? "正在检索记忆…" : "没有命中。") : h("ul", { className: pages_module_css_default.list }, searchHits.map((hit) => h("li", {
5345
+ key: hit.ref_id,
5346
+ className: pages_module_css_default.item
5347
+ }, hit.title === void 0 ? null : h("div", { className: pages_module_css_default.meta }, `标题:${hit.title}`), editing?.id === hit.ref_id ? h("textarea", {
5348
+ className: pages_module_css_default.textarea,
5349
+ value: editing.text,
5350
+ rows: 3,
5351
+ onChange: (e) => {
5352
+ setEditing({
5353
+ id: hit.ref_id,
5354
+ text: e.target.value
5355
+ });
5356
+ }
5357
+ }) : hit.text, hit.truncated === true ? h("span", { className: pages_module_css_default.meta }, "(已截断)") : null, h("span", { className: pages_module_css_default.meta }, `#${String(hit.ref_id)} · score ${hit.score.toFixed(2)} · ${hit.source_ref}` + (hit.valid_from === void 0 ? "" : ` · ${hit.valid_from}`)), h("div", { className: pages_module_css_default.row }, factButtons(actionFact(hit.ref_id, hit.text, hit.truncated === true), true)), detail?.id === hit.ref_id ? h(FactDetailView, {
5358
+ fact: detail.data,
5359
+ disabled: loading,
5360
+ onSaveRelations: saveRelations
5361
+ }) : null)))) : null, error !== void 0 ? h("div", { className: pages_module_css_default.error }, `错误:${error}`) : null, notice !== void 0 ? h("div", { className: pages_module_css_default.status }, notice) : null, props.remote !== void 0 && !searchActive && !loading && error === void 0 && shown === 0 ? h("div", { className: pages_module_css_default.status }, statusFilter === "active" ? "当前没有活动的事实。可在对话里让 agent 用 mem_remember 写入。" : "当前没有归档的事实。") : null, health !== null ? h(HealthBlock, { stats: health }) : null, contradictions !== null ? contradictions.length === 0 ? h("div", { className: pages_module_css_default.status }, "没有未处理的矛盾。") : h("ul", { className: pages_module_css_default.list }, contradictions.map((c) => h("li", {
5162
5362
  key: c.contradiction_id,
5163
5363
  className: pages_module_css_default.item
5164
5364
  }, `#${String(c.fact_a)}「${c.content_a}」 ↔ #${String(c.fact_b)}「${c.content_b}」`, h("span", { className: pages_module_css_default.meta }, `score ${c.score.toFixed(2)} · ${c.detected_at ?? ""}`), props.remote !== void 0 ? h("div", { className: pages_module_css_default.row }, h(Btn, {
@@ -5175,10 +5375,10 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5175
5375
  label: "误报(两条都对)",
5176
5376
  disabled: loading,
5177
5377
  onClick: () => resolveConflict(c, "false_positive")
5178
- })) : null))) : null, h("ul", { className: pages_module_css_default.list }, rows.map((f) => h("li", {
5378
+ })) : null))) : null, searchActive ? null : h("ul", { className: pages_module_css_default.list }, rows.map((f) => h("li", {
5179
5379
  key: f.fact_id,
5180
5380
  className: pages_module_css_default.item
5181
- }, editing?.id === f.fact_id ? h("textarea", {
5381
+ }, f.title === null || f.title === void 0 ? f.kind === "journal" ? h("div", { className: pages_module_css_default.meta }, "标题:(无标题)") : null : h("div", { className: pages_module_css_default.meta }, `标题:${f.title}`), editing?.id === f.fact_id ? h("textarea", {
5182
5382
  className: pages_module_css_default.textarea,
5183
5383
  value: editing.text,
5184
5384
  rows: 3,
@@ -5188,120 +5388,23 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5188
5388
  text: e.target.value
5189
5389
  });
5190
5390
  }
5191
- }) : f.content, h("span", { className: pages_module_css_default.meta }, `#${String(f.fact_id)} · ${f.category} · trust ${f.trust_score.toFixed(2)}` + (f.pinned === true ? " · 永久" : typeof f.remaining_days === "number" ? ` · 剩余 ≈${String(Math.ceil(f.remaining_days))} 活跃日` : "") + ` · 有用 ${String(f.helpful_count)} · ${f.created_at}` + (f.status === "archived" && f.archive_reason ? ` · 归档原因 ${f.archive_reason}` : "") + (f.status === "archived" && f.pinned === true ? " · 永久保留(不会被清理)" : "")), h("div", { className: pages_module_css_default.row }, editing?.id === f.fact_id ? [h(Btn, {
5192
- key: "save",
5193
- label: "保存",
5194
- disabled: loading || editing.text.trim().length === 0,
5195
- onClick: () => {
5196
- const text = editing.text.trim();
5197
- setEditing(null);
5198
- mutate(`更新 #${String(f.fact_id)}`, (remote) => remote.remember({
5199
- action: "update",
5200
- fact_id: f.fact_id,
5201
- content: text
5202
- }), { summarize: true });
5203
- }
5204
- }), h(Btn, {
5205
- key: "cancel",
5206
- label: "取消",
5391
+ }) : f.content, h("span", { className: pages_module_css_default.meta }, `#${String(f.fact_id)} · ${f.kind} · trust ${f.trust_score.toFixed(2)}` + (f.pinned === true ? " · 永久" : typeof f.remaining_days === "number" ? ` · 剩余 ≈${String(Math.ceil(f.remaining_days))} 活跃日` : "") + ` · 有用 ${String(f.helpful_count)} · ${f.created_at}` + (f.status === "archived" && f.archive_reason ? ` · 归档原因 ${f.archive_reason}` : "") + (f.status === "archived" && f.pinned === true ? " · 永久保留(不会被清理)" : "")), h("div", { className: pages_module_css_default.row }, factButtons({
5392
+ fact_id: f.fact_id,
5393
+ status: f.status,
5394
+ content: f.content,
5395
+ pinned: f.pinned === true
5396
+ }, false)), detail?.id === f.fact_id ? h(FactDetailView, {
5397
+ fact: detail.data,
5207
5398
  disabled: loading,
5208
- onClick: () => {
5209
- setEditing(null);
5210
- }
5211
- })] : [
5212
- h(Btn, {
5213
- key: "detail",
5214
- label: detail?.id === f.fact_id ? "收起详情" : "详情",
5215
- disabled: loading,
5216
- onClick: () => {
5217
- showDetail(f.fact_id);
5218
- }
5219
- }),
5220
- statusFilter === "active" ? h(Btn, {
5221
- key: "edit",
5222
- label: "编辑",
5223
- disabled: loading,
5224
- onClick: () => {
5225
- setEditing({
5226
- id: f.fact_id,
5227
- text: f.content
5228
- });
5229
- setDetail(null);
5230
- }
5231
- }) : null,
5232
- statusFilter === "active" ? h(Btn, {
5233
- key: "helpful",
5234
- label: "有用 +1",
5235
- disabled: loading,
5236
- onClick: () => {
5237
- mutate(`反馈有用 #${String(f.fact_id)}`, (remote) => remote.remember({
5238
- action: "helpful",
5239
- fact_id: f.fact_id
5240
- }));
5241
- }
5242
- }) : null,
5243
- statusFilter === "active" ? h(Btn, {
5244
- key: "unhelpful",
5245
- label: "没用 −1",
5246
- disabled: loading,
5247
- onClick: () => {
5248
- mutate(`反馈没用 #${String(f.fact_id)}`, (remote) => remote.remember({
5249
- action: "unhelpful",
5250
- fact_id: f.fact_id
5251
- }));
5252
- }
5253
- }) : null,
5254
- statusFilter === "active" ? h(Btn, {
5255
- key: "archive",
5256
- label: "归档",
5257
- disabled: loading,
5258
- onClick: () => {
5259
- mutate(`归档 #${String(f.fact_id)}`, (remote) => remote.admin({
5260
- action: "archive",
5261
- fact_id: f.fact_id,
5262
- reason: "ui"
5263
- }));
5264
- }
5265
- }) : h(Btn, {
5266
- key: "restore",
5267
- label: "恢复",
5268
- disabled: loading,
5269
- onClick: () => {
5270
- mutate(`恢复 #${String(f.fact_id)}`, (remote) => remote.admin({
5271
- action: "restore",
5272
- fact_id: f.fact_id
5273
- }));
5274
- }
5275
- }),
5276
- statusFilter === "active" ? f.pinned === true ? h(Btn, {
5277
- key: "unpin",
5278
- label: "解除永久",
5279
- disabled: loading,
5280
- onClick: () => {
5281
- mutate(`解除永久 #${String(f.fact_id)}`, (remote) => remote.admin({
5282
- action: "unpin",
5283
- fact_id: f.fact_id
5284
- }));
5285
- }
5286
- }) : h(Btn, {
5287
- key: "pin",
5288
- label: "永久记忆",
5289
- disabled: loading,
5290
- onClick: () => {
5291
- mutate(`固化永久 #${String(f.fact_id)}`, (remote) => remote.admin({
5292
- action: "pin",
5293
- fact_id: f.fact_id
5294
- }));
5295
- }
5296
- }) : null
5297
- ]), detail?.id === f.fact_id ? h("div", { className: pages_module_css_default.meta }, `创建于 ${detail.data.created_at} / 更新于 ${detail.data.updated_at ?? "—"}`, h("br"), `实体:${(detail.data.entities ?? []).join("、") || "(无)"}`, h("br"), `三元组:${(detail.data.triples ?? []).map((t) => `${t.subj} —${t.pred}→ ${t.obj}(${t.confidence.toFixed(2)})`).join(";") || "(无)"}`, h("br"), `检索次数:${String(detail.data.retrieval_count ?? 0)}${detail.data.supersedes_id ? ` · 修订自 #${String(detail.data.supersedes_id)}` : ""} · 被断言 ${String(detail.data.assert_count ?? 1)} 次`, h("br"), `有效期:${detail.data.valid_from ?? "未知"} → ${detail.data.valid_to ?? (detail.data.status === "active" ? "仍在有效" : "未知")}` + (detail.data.superseded_by === null || detail.data.superseded_by === void 0 ? "" : ` · 被 #${String(detail.data.superseded_by)} 取代`), h("br"), `来源:${(detail.data.sources ?? []).length === 0 ? "(未记录)" : (detail.data.sources ?? []).map((s) => `${s.kind}:${s.ref}`).join(";")}`) : null))), truncated ? h("button", {
5399
+ onSaveRelations: saveRelations
5400
+ }) : null))), searchActive || !truncated ? null : h("button", {
5298
5401
  type: "button",
5299
5402
  className: pages_module_css_default.more,
5300
5403
  disabled: loading,
5301
5404
  onClick: () => {
5302
5405
  load(nextOffset, statusFilter);
5303
5406
  }
5304
- }, loading ? "加载中…" : total === void 0 ? "加载更多" : `加载更多(还有 ${String(Math.max(total - shown, 0))} 条)`) : null);
5407
+ }, loading ? "加载中…" : total === void 0 ? "加载更多" : `加载更多(还有 ${String(Math.max(total - shown, 0))} 条)`));
5305
5408
  }
5306
5409
  /**
5307
5410
  * Retrieval health (DESIGN §20): the numbers that say whether retrieval is actually working —
@@ -5319,759 +5422,15 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
5319
5422
  const truncated = r.embedding_truncated + r.output_truncated;
5320
5423
  return h("div", { className: pages_module_css_default.detail }, pendingVectors === 0 ? null : h("div", { className: pages_module_css_default.status }, `⚠ 向量空间待迁移:${String(pendingVectors)} 条 ACTIVE 向量来自旧嵌入空间(宽度不符 ${String(vectors?.stale ?? 0)} · 其他表示 ${String(vectors?.space_stale ?? 0)})——语义腿对它们失效,正在后台分批重算;手动入口 \`avantf-mem vectors --fix\``), wal?.warning === null || wal?.warning === void 0 ? null : h("div", { className: pages_module_css_default.status }, `⚠ ${wal.warning}(当前 ${String(wal.bytes)} 字节)`), h("div", { className: pages_module_css_default.status }, `检索 ${String(r.queries)} 次 · 空结果 ${pct(r.zero_result_rate)} · 平均 ${r.avg_latency_ms.toFixed(1)}ms · 峰值 ${r.max_latency_ms.toFixed(1)}ms`), h("div", { className: pages_module_css_default.meta }, `语义腿在线 ${pct(r.semantic_live_rate)} · 平均命中 ${String(r.avg_results_per_query)} 条`), kinds === "" ? null : h("div", { className: pages_module_css_default.meta }, kinds), props.stats.sources === void 0 && props.stats.validity === void 0 ? null : h("div", { className: pages_module_css_default.meta }, `来源覆盖 ${props.stats.sources === void 0 ? "—" : pct(props.stats.sources.coverage)}(${String(props.stats.sources?.facts_with_source ?? 0)}/${String(props.stats.sources?.active ?? 0)}) · 事件时间覆盖 ${props.stats.validity === void 0 ? "—" : pct(props.stats.validity.coverage)}(${String(props.stats.validity?.facts_with_valid_from ?? 0)}/${String(props.stats.validity?.active ?? 0)})`), h("div", { className: pages_module_css_default.meta }, truncated === 0 ? "未被任何预算截断。" : `截断:嵌入 ${String(r.embedding_truncated)} · 输出 ${String(r.output_truncated)}${r.output_truncated > 0 ? "(调大 retrieval.max_output_tokens 或用 max_tokens 参数)" : ""}`), r.updated_at === null ? null : h("div", { className: pages_module_css_default.meta }, `更新于 ${r.updated_at}`));
5321
5424
  }
5322
- /**
5323
- * Group cross-store hits for display: memory facts vs `domain → source`.
5324
- *
5325
- * Reads the host's `domain`/`source` fields. Splitting `source_ref` here (the old
5326
- * `parts[0] → parts[1]`) mis-grouped any document whose domain or source name
5327
- * contains `:` — the host cannot encode the pair unambiguously either way.
5328
- */
5329
- function groupKey(hit) {
5330
- if (hit.kind === "fact") return "记忆事实";
5331
- const domain = hit.domain ?? "";
5332
- const source = hit.source ?? "";
5333
- if (domain !== "" && source !== "") return `${domain} → ${source}`;
5334
- return domain || source || "文档切片";
5335
- }
5336
- /**
5337
- * P-01: render the per-leg evidence the query page explicitly asked for (`include_scores: true`).
5338
- *
5339
- * `scores` is absent for every other caller (the field is default-off), so this returns null and
5340
- * the row renders exactly as before. A leg that did not recall the hit is `null` and prints as
5341
- * `—`, which is the honest reading: the candidate entered through some OTHER leg.
5342
- */
5343
- function legEvidenceLine(hit) {
5344
- const scores = hit.scores;
5345
- if (scores === void 0) return null;
5346
- const label = {
5347
- semantic: "语义",
5348
- fts: "FTS",
5349
- jaccard: "实体",
5350
- hrr: "HRR"
5351
- };
5352
- const parts = Object.entries(scores).map(([leg, score]) => score === null ? `${label[leg] ?? leg} —` : `${label[leg] ?? leg} ${score.normalized.toFixed(2)}(原始 ${score.raw.toFixed(2)})`);
5353
- const final = hit.final === void 0 ? "" : ` · 融合 ${hit.final.toFixed(3)}`;
5354
- return `证据:${parts.join(" · ")}${final}`;
5355
- }
5356
- /**
5357
- * How many candidates one reply's relevance floors removed, across every leg.
5358
- *
5359
- * This is the difference between the two readings of an empty result ("the floors removed N" vs
5360
- * "there was nothing to remove"), and it is what makes the panel's 宽松 hint actionable rather than
5361
- * a suggestion to retry blindly.
5362
- */
5363
- function droppedByFloor(value) {
5364
- const drops = value.dropped_by_floor;
5365
- if (drops === void 0) return 0;
5366
- return drops.semantic + drops.fts + drops.jaccard + drops.hrr;
5367
- }
5368
- function KnowledgePanel(props) {
5369
- const [showQuery, setShowQuery] = react.useState(false);
5370
- const [query, setQuery] = react.useState("");
5371
- const [kind, setKind] = react.useState("all");
5372
- /**
5373
- * The relevance-floor profile (contract `FLOOR_PROFILES`): 严格 = the configured floors with NO
5374
- * automatic fallback, 宽松 = the relaxed floors. Default 严格 — the panel must not silently trade
5375
- * precision for recall, and sending `strict` explicitly is what makes the empty-result hint
5376
- * actionable ("N were dropped, switch to 宽松") instead of the retry having already happened.
5377
- * The value is always sent; an omitted profile is the engine's default policy (auto-relax once),
5378
- * which the 严格/宽松 labels would misdescribe.
5379
- */
5380
- const [floors, setFloors] = react.useState("strict");
5381
- const [domain, setDomain] = react.useState("");
5382
- const [source, setSource] = react.useState("");
5383
- const [hits, setHits] = react.useState([]);
5384
- /** Candidates the last reply's floors removed; drives the 宽松 hint on an empty result. */
5385
- const [floorDropped, setFloorDropped] = react.useState(0);
5386
- const [queryStatus, setQueryStatus] = react.useState("输入关键词后按回车检索");
5387
- const [error, setError] = react.useState();
5388
- const [notice, setNotice] = react.useState();
5389
- const [loading, setLoading] = react.useState(false);
5390
- /** The 查询 call's own in-flight flag — `loading` belongs to the KB mutations. */
5391
- const [queryLoading, setQueryLoading] = react.useState(false);
5392
- const [docs, setDocs] = react.useState(null);
5393
- const [docDetail, setDocumentDetail] = react.useState(null);
5394
- /**
5395
- * The managed files as the host last saw them (`kb sync --dry-run`): which documents' `.md` was
5396
- * edited since it was ingested, which have no file at all, and which files no document claims.
5397
- * Read together with the list — there is deliberately no file watcher (changes are pulled in on
5398
- * demand), so these flags are what tells the user a 「重新摄入」 is due.
5399
- */
5400
- const [fileState, setFileState] = react.useState(null);
5401
- /** Pagination anchor for the document list (rows the host has handed over) + "there may be more". */
5402
- const [docsOffset, setDocsOffset] = react.useState(0);
5403
- const [docsTruncated, setDocsTruncated] = react.useState(false);
5404
- const [showIngest, setShowIngest] = react.useState(false);
5405
- const [iDomain, setIDomain] = react.useState("");
5406
- const [iSource, setISource] = react.useState("");
5407
- const [iTitle, setITitle] = react.useState("");
5408
- const [iText, setIText] = react.useState("");
5409
- /**
5410
- * The 知识域 options the host reported: the configured allowlist ∪ the domains already in the
5411
- * library, plus whether the allowlist is active. `null` = not fetched yet (or the call failed),
5412
- * which falls back to the free input rather than blocking the form.
5413
- */
5414
- const [domainCatalog, setDomainCatalog] = react.useState(null);
5415
- /**
5416
- * ONE source input (URL / local file / local directory / pasted text). What it IS can only be
5417
- * answered by the host, so `sourceInfo` holds its classification and the hint line shows what
5418
- * 入库 will do before it is pressed.
5419
- */
5420
- const [iUri, setIUri] = react.useState("");
5421
- const [sourceInfo, setSourceInfo] = react.useState(null);
5422
- /**
5423
- * Which of the two inputs the form is showing. They are alternatives, not a sequence: document
5424
- * identity is `(domain, source, title)`, so ingesting a file AND pasted text under one title would
5425
- * make the second call REPLACE the first. Rather than pick one silently (or lose a document),
5426
- * only one input exists at a time; the other keeps its value and is marked 有内容 on the toggle.
5427
- */
5428
- const [inputMode, setInputMode] = react.useState("source");
5429
- /** The 选择 picker: `null` closed, otherwise the current listing (or an error to show). */
5430
- const [picker, setPicker] = react.useState(null);
5431
- const docsGuard = useSeqGuard();
5432
- const docDetailGuard = useSeqGuard();
5433
- const queryGuard = useSeqGuard();
5434
- const run = () => {
5435
- const remote = props.remote;
5436
- if (remote === void 0) return;
5437
- const text = query.trim();
5438
- if (text.length === 0) {
5439
- setQueryStatus("请输入查询关键词");
5440
- setHits([]);
5441
- setFloorDropped(0);
5442
- setError(void 0);
5443
- return;
5444
- }
5445
- const seq = queryGuard.begin();
5446
- setQueryLoading(true);
5447
- callRemote("query", remote.query({
5448
- query: text,
5449
- kind,
5450
- domain: domain.trim() || void 0,
5451
- source: source.trim() || void 0,
5452
- limit: 10,
5453
- floors,
5454
- include_scores: true
5455
- })).then((outcome) => {
5456
- if (!queryGuard.isCurrent(seq)) return;
5457
- if (outcome.ok) {
5458
- const value = outcome.value;
5459
- const found = value?.hits ?? [];
5460
- setHits(found);
5461
- setFloorDropped(value === void 0 ? 0 : droppedByFloor(value));
5462
- const degraded = value?.degraded === true ? "(语义降级,仅 FTS+实体)" : "";
5463
- const profile = value?.relaxed === true ? "(已自动放宽门槛)" : floors === "loose" ? "(宽松门槛)" : "";
5464
- setQueryStatus(`命中 ${String(found.length)} 条${profile}${degraded}`);
5465
- setError(void 0);
5466
- } else {
5467
- setHits([]);
5468
- setFloorDropped(0);
5469
- setQueryStatus("查询失败");
5470
- setError(outcome.error);
5471
- }
5472
- }).finally(() => {
5473
- if (queryGuard.isCurrent(seq)) setQueryLoading(false);
5474
- });
5475
- };
5476
- const kbCall = (label, args, after) => {
5477
- const remote = props.remote;
5478
- if (remote === void 0) return;
5479
- runRemoteAction({
5480
- setLoading,
5481
- setNotice,
5482
- setError
5483
- }, label, remote.kb(args), {
5484
- summarize: true,
5485
- after
5486
- });
5487
- };
5488
- const loadDocs = react.useCallback((offset) => {
5489
- const remote = props.remote;
5490
- if (remote === void 0) return;
5491
- const seq = docsGuard.begin();
5492
- callRemote("kb.list", remote.kb({
5493
- action: "list",
5494
- limit: DOC_PAGE_SIZE,
5495
- offset
5496
- })).then((outcome) => {
5497
- if (!docsGuard.isCurrent(seq)) return;
5498
- if (outcome.ok) {
5499
- const page = Array.isArray(outcome.value) ? outcome.value : [];
5500
- setDocs((previous) => offset === 0 || previous === null ? page : mergeDocs(previous, page));
5501
- setDocsOffset(offset + page.length);
5502
- setDocsTruncated(page.length === DOC_PAGE_SIZE);
5503
- setError(void 0);
5504
- } else setError(outcome.error);
5505
- });
5506
- }, [props.remote]);
5507
- react.useEffect(() => {
5508
- loadDocs(0);
5509
- }, [loadDocs]);
5510
- /** Re-read each document's managed file state (`kb sync --dry-run` — never re-ingests). */
5511
- const loadFiles = react.useCallback(() => {
5512
- const remote = props.remote;
5513
- if (remote === void 0) return;
5514
- callRemote("kb.sync(dry_run)", remote.kb({
5515
- action: "sync",
5516
- dry_run: true
5517
- })).then((outcome) => {
5518
- if (outcome.ok) setFileState({
5519
- stale: outcome.value?.stale ?? [],
5520
- missing: outcome.value?.missing ?? [],
5521
- unclaimed: outcome.value?.unclaimed ?? [],
5522
- orphans: outcome.value?.orphans ?? []
5523
- });
5524
- else setError(outcome.error);
5525
- });
5526
- }, [props.remote]);
5527
- react.useEffect(() => {
5528
- loadFiles();
5529
- }, [loadFiles]);
5530
- /** The domain picker's options: fetched once on mount and again whenever the library changes. */
5531
- const loadDomains = react.useCallback(() => {
5532
- const remote = props.remote;
5533
- if (remote === void 0) return;
5534
- callRemote("kb.domains", remote.kbDomains({})).then((outcome) => {
5535
- if (outcome.ok && outcome.value) setDomainCatalog(outcome.value);
5536
- else if (!outcome.ok) setError(outcome.error);
5537
- });
5538
- }, [props.remote]);
5539
- react.useEffect(() => {
5540
- loadDomains();
5541
- }, [loadDomains]);
5542
- /**
5543
- * The write form's 「+」: append one domain to the store config through the UI-only remote
5544
- * method, then fold the returned catalog into state (the host is the source of truth for the
5545
- * order). Returns the error text for the inline field; `undefined` means it landed and the
5546
- * control selects it.
5547
- */
5548
- const addDomain = (domain) => {
5549
- const remote = props.remote;
5550
- if (remote === void 0) return Promise.resolve("未挂载 avantfMem Remote");
5551
- return callRemote("kb.addDomain", remote.kbAddDomain({ domain })).then((outcome) => {
5552
- if (!outcome.ok) return outcome.error;
5553
- if (outcome.value) setDomainCatalog(outcome.value);
5554
- setNotice(`已新增知识域「${domain}」并写入知识库配置`);
5555
- setError(void 0);
5556
- });
5557
- };
5558
- /** The list and the file flags always move together — one refresh for both. */
5559
- const refresh = react.useCallback(() => {
5560
- loadDocs(0);
5561
- loadFiles();
5562
- loadDomains();
5563
- }, [
5564
- loadDocs,
5565
- loadFiles,
5566
- loadDomains
5567
- ]);
5568
- /**
5569
- * Open one document's managed file (or the directory holding it) in the user's editor.
5570
- * The host resolves the path from `doc_id`; the client only says which of the two.
5571
- */
5572
- const openDoc = (docId, target) => {
5573
- const remote = props.remote;
5574
- if (remote === void 0) return;
5575
- callRemote("kb.openDoc", remote.openDoc({
5576
- doc_id: docId,
5577
- target
5578
- })).then((outcome) => {
5579
- if (outcome.ok) {
5580
- const opener = outcome.value?.opener ?? "编辑器";
5581
- setNotice(target === "dir" ? `已用 ${opener} 打开目录:${outcome.value?.path ?? ""}` : `已用 ${opener} 打开:${outcome.value?.path ?? ""}(改完保存后点「重新摄入」)`);
5582
- setError(void 0);
5583
- } else setError(outcome.error);
5584
- });
5585
- };
5586
- /** Pull edited files back into the index: one document, or every stale one. */
5587
- const syncDocs = (docId, adopt) => {
5588
- const remote = props.remote;
5589
- if (remote === void 0) return;
5590
- setLoading(true);
5591
- callRemote("kb.sync", remote.kb(docId === void 0 ? { action: "sync" } : {
5592
- action: "sync",
5593
- doc_id: docId,
5594
- ...adopt === true ? { adopt: true } : {}
5595
- })).then((outcome) => {
5596
- if (outcome.ok) {
5597
- const report = outcome.value;
5598
- const missing = report?.missing.length ?? 0;
5599
- setNotice(adopt === true ? `认领完成:${String(report?.adopted ?? 0)} 篇` : `同步完成:重新摄入 ${String(report?.reingested ?? 0)} 篇` + (missing > 0 ? `;${String(missing)} 篇的受管文件不存在(重新摄入可重建)` : ""));
5600
- setError(void 0);
5601
- } else setError(outcome.error);
5602
- }).finally(() => {
5603
- setLoading(false);
5604
- refresh();
5605
- });
5606
- };
5607
- /** Bring the panel's top into view — used when a block opens below the sticky bar. */
5608
- const rootRef = react.useRef(null);
5609
- const reveal = () => {
5610
- rootRef.current?.scrollIntoView({ block: "start" });
5611
- };
5612
- react.useEffect(() => {
5613
- reveal();
5614
- }, []);
5615
- const maybeLoadMoreDocs = () => {
5616
- if (docsTruncated && !loading) loadDocs(docsOffset);
5617
- };
5618
- useLoadMoreOnScroll(rootRef, docsTruncated && !loading, maybeLoadMoreDocs, docs?.length ?? 0);
5619
- const showDocumentDetail = (docId) => {
5620
- const remote = props.remote;
5621
- if (remote === void 0) return;
5622
- if (docDetail?.id === docId) {
5623
- setDocumentDetail(null);
5624
- return;
5625
- }
5626
- const seq = docDetailGuard.begin();
5627
- callRemote(`kb.detail(${String(docId)})`, remote.kb({
5628
- action: "detail",
5629
- doc_id: docId
5630
- })).then((outcome) => {
5631
- if (!docDetailGuard.isCurrent(seq)) return;
5632
- if (outcome.ok && outcome.value) {
5633
- setDocumentDetail({
5634
- id: docId,
5635
- data: outcome.value
5636
- });
5637
- setError(void 0);
5638
- } else setError(outcome.ok ? "文档详情为空" : outcome.error);
5639
- });
5640
- };
5641
- /**
5642
- * Ask before deleting a document — its chunks/vectors AND its managed file. The dialog names
5643
- * the file when the last status call knew it, and says outright that `source_uri` is untouched:
5644
- * "delete the document" must never read as "delete my file".
5645
- */
5646
- const confirmRemoveDoc = (doc) => {
5647
- const known = [...fileState?.stale ?? [], ...fileState?.missing ?? []].find((file) => file.doc_id === doc.doc_id);
5648
- const message = `删除文档 #${String(doc.doc_id)}「${doc.title}」及其受管文件` + (known === void 0 ? "(knowledge/docs 下的那份副本)" : `:\n${known.path}`) + "\n\n该文档的切片与向量一并删除,此操作不可恢复;source_uri 指向的原文件不会被改动。";
5649
- if (typeof globalThis.confirm === "function" && !globalThis.confirm(message)) return;
5650
- kbCall(`删除文档 #${String(doc.doc_id)}`, {
5651
- action: "remove",
5652
- doc_id: doc.doc_id
5653
- }, () => {
5654
- setDocumentDetail(null);
5655
- refresh();
5656
- });
5657
- };
5658
- const groups = [];
5659
- for (const hit of hits) {
5660
- const key = groupKey(hit);
5661
- const found = groups.find((g) => g[0] === key);
5662
- if (found) found[1].push(hit);
5663
- else groups.push([key, [hit]]);
5664
- }
5665
- const sourceText = iUri.trim();
5666
- const bodyText = iText.trim();
5667
- /**
5668
- * Ask the host what the source input is, debounced so typing a path does not fire a stat per
5669
- * keystroke. The answer drives both the hint line and which `kb_manage` action 入库 sends.
5670
- */
5671
- react.useEffect(() => {
5672
- const remote = props.remote;
5673
- if (remote === void 0) return void 0;
5674
- if (sourceText === "") {
5675
- setSourceInfo(null);
5676
- return;
5677
- }
5678
- setSourceInfo(null);
5679
- const timer = setTimeout(() => {
5680
- callRemote("kb.classifySource", remote.classifySource({ text: sourceText })).then((outcome) => {
5681
- if (outcome.ok) setSourceInfo(normalizeClassification(outcome.value));
5682
- else {
5683
- setSourceInfo(null);
5684
- setError(outcome.error);
5685
- }
5686
- });
5687
- }, 300);
5688
- return () => {
5689
- clearTimeout(timer);
5690
- };
5691
- }, [sourceText, props.remote]);
5692
- /** Load one directory into the picker; `path === undefined` starts at the first allowed root. */
5693
- const browseTo = (path) => {
5694
- const remote = props.remote;
5695
- if (remote === void 0) return;
5696
- setPicker((previous) => ({ listing: previous?.listing ?? null }));
5697
- callRemote("kb.browseDir", remote.browseDir(path === void 0 ? {} : { path })).then((outcome) => {
5698
- if (outcome.ok) setPicker({ listing: outcome.value ?? null });
5699
- else setPicker({
5700
- listing: null,
5701
- error: outcome.error
5702
- });
5703
- });
5704
- };
5705
- /** Put a picked path into the source input (which re-classifies it) and close the picker. */
5706
- const choosePath = (path) => {
5707
- setIUri(path);
5708
- setPicker(null);
5709
- };
5710
- /** The other input still holds something — say so, so提交 does not silently leave it behind. */
5711
- const inactiveNote = inputMode === "source" && bodyText !== "" || inputMode === "text" && sourceText !== "" ? `(${inputMode === "source" ? "粘贴文本" : "来源"}里有内容,本次不会入库)` : "";
5712
- /** What 入库 will do with what is currently filled in — shown BEFORE it is pressed. */
5713
- const ingestHint = (() => {
5714
- if (inputMode === "text") return bodyText !== "" ? `将「粘贴文本」入库${inactiveNote}` : "粘贴文本后即可入库";
5715
- if (sourceText !== "") {
5716
- const info = sourceInfo;
5717
- if (info === null) return "正在识别来源…";
5718
- if (info.kind === "url") return "将从该 URL 抓取内容入库";
5719
- if (info.kind === "file") return info.paths.length > 1 ? `将导入这 ${String(info.paths.length)} 个本地文件` : `将按本地文件入库:${info.paths[0] ?? ""}`;
5720
- if (info.kind === "directory") return `将导入该目录(${String(info.files)} 个可摄入文件,其余跳过)`;
5721
- if (info.kind === "missing") return `找不到:${info.missing.join("、")}${info.reasons[0] === void 0 ? "" : ` —— ${info.reasons[0]}`}`;
5722
- return `将作为文本入库(不是 URL,也不是本地路径)${inactiveNote}`;
5723
- }
5724
- return "填写来源后即可入库";
5725
- })();
5726
- /** True when the hint is a refusal rather than a plan — the row turns warning-coloured. */
5727
- const ingestBlocked = inputMode === "source" ? sourceText !== "" && sourceInfo?.kind === "missing" : false;
5728
- const canIngest = iDomain.trim().length > 0 && (inputMode === "text" ? bodyText !== "" : sourceText !== "" && sourceInfo !== null && sourceInfo.kind !== "missing");
5729
- /**
5730
- * One 入库 request, with the collision step built in.
5731
- *
5732
- * The host's REPLACE mode answers an unconfirmed collision with a report and writes NOTHING, so
5733
- * this asks the user first and only then re-runs the SAME request with `overwrite: true`.
5734
- * Cancelling returns without sending a second request — nothing is written either way.
5735
- */
5736
- const ingestCall = (label, args, after) => {
5737
- const remote = props.remote;
5738
- if (remote === void 0) return;
5739
- setLoading(true);
5740
- callRemote(label, remote.kb(args)).then((outcome) => {
5741
- if (!outcome.ok) {
5742
- setNotice(void 0);
5743
- setError(`${label} 失败:${outcome.error}`);
5744
- return;
5745
- }
5746
- const conflict = ingestConflict(outcome.value);
5747
- if (conflict === null) {
5748
- setNotice(`${label} 完成${summarize(outcome.value)}`);
5749
- setError(void 0);
5750
- after();
5751
- return;
5752
- }
5753
- if (typeof globalThis.confirm !== "function" || !globalThis.confirm(conflictMessage(conflict))) return;
5754
- return callRemote(label, remote.kb({
5755
- ...args,
5756
- overwrite: true
5757
- })).then((retry) => {
5758
- if (!retry.ok) {
5759
- setNotice(void 0);
5760
- setError(`${label} 失败:${retry.error}`);
5761
- return;
5762
- }
5763
- setNotice(`${label} 完成${summarize(retry.value)}`);
5764
- setError(void 0);
5765
- after();
5766
- });
5767
- }).finally(() => {
5768
- setLoading(false);
5769
- });
5770
- };
5771
- /**
5772
- * One 入库 button, four destinations: a URL is fetched, an existing file is read, a directory is
5773
- * imported, and anything that is neither a URL nor an existing path is ingested as text. The
5774
- * classification that decides this came from the host — never from a client-side guess.
5775
- */
5776
- const submitIngest = () => {
5777
- const domain = iDomain.trim();
5778
- const source = iSource.trim() || void 0;
5779
- const title = iTitle.trim() || void 0;
5780
- const after = () => {
5781
- if (inputMode === "text") setIText("");
5782
- else setIUri("");
5783
- refresh();
5784
- };
5785
- if (inputMode === "text") {
5786
- ingestCall("入库", {
5787
- action: "ingest",
5788
- text: bodyText,
5789
- domain,
5790
- source,
5791
- title
5792
- }, after);
5793
- return;
5794
- }
5795
- if (sourceText !== "") {
5796
- const info = sourceInfo;
5797
- if (info === null || info.kind === "missing") return;
5798
- if (info.kind === "directory" || info.paths.length > 1) {
5799
- ingestCall("导入", {
5800
- action: "import",
5801
- paths: info.paths,
5802
- domain,
5803
- source
5804
- }, after);
5805
- return;
5806
- }
5807
- if (info.kind === "file") {
5808
- ingestCall("入库", {
5809
- action: "ingest",
5810
- source_uri: info.paths[0],
5811
- domain,
5812
- source,
5813
- title
5814
- }, after);
5815
- return;
5816
- }
5817
- if (info.kind === "url") {
5818
- ingestCall("入库", {
5819
- action: "ingest",
5820
- source_uri: sourceText,
5821
- domain,
5822
- source,
5823
- title
5824
- }, after);
5825
- return;
5826
- }
5827
- }
5828
- ingestCall("入库", {
5829
- action: "ingest",
5830
- text: sourceText,
5831
- domain,
5832
- source,
5833
- title
5834
- }, after);
5835
- };
5836
- const shown = docs?.length ?? 0;
5837
- const staleIds = new Set((fileState?.stale ?? []).map((file) => file.doc_id));
5838
- const missingIds = new Set((fileState?.missing ?? []).map((file) => file.doc_id));
5839
- const unclaimedIds = new Set((fileState?.unclaimed ?? []).map((file) => file.doc_id));
5840
- const orphans = fileState?.orphans.length ?? 0;
5841
- const summary = docs === null ? "读取中…" : `已加载 ${String(shown)} 篇文档${docsTruncated ? "(继续滚动自动加载)" : ""}` + (staleIds.size > 0 ? ` · ${String(staleIds.size)} 篇待重新摄入` : "") + (orphans > 0 ? ` · ${String(orphans)} 个无主文件` : "");
5842
- return h("div", {
5843
- className: pages_module_css_default.section,
5844
- ref: rootRef
5845
- }, props.remote === void 0 ? h("div", { className: pages_module_css_default.error }, NOT_MOUNTED) : null, props.remote === void 0 ? null : h("div", { className: pages_module_css_default.headbar }, h(Btn, {
5846
- label: showQuery ? "收起查询" : "查询",
5847
- disabled: loading,
5848
- onClick: () => {
5849
- const opening = !showQuery;
5850
- setShowQuery(!showQuery);
5851
- if (opening) reveal();
5852
- }
5853
- }), h(Btn, {
5854
- label: showIngest ? "收起入库" : "入库",
5855
- disabled: loading,
5856
- onClick: () => {
5857
- const opening = !showIngest;
5858
- setShowIngest(opening);
5859
- if (opening) reveal();
5860
- }
5861
- }), h(Btn, {
5862
- label: staleIds.size > 0 ? `同步文件(${String(staleIds.size)})` : "同步文件",
5863
- disabled: loading || staleIds.size === 0,
5864
- onClick: () => {
5865
- syncDocs();
5866
- }
5867
- }), h(Btn, {
5868
- label: domain.trim() ? `重建索引(${domain.trim()})` : "重建索引",
5869
- disabled: loading,
5870
- onClick: () => {
5871
- kbCall("重建索引", {
5872
- action: "reindex",
5873
- domain: domain.trim() || void 0
5874
- }, refresh);
5875
- }
5876
- }), h("div", { className: pages_module_css_default.status }, loading ? "处理中…" : summary)), error !== void 0 ? h("div", { className: pages_module_css_default.error }, `错误:${error}`) : null, notice !== void 0 ? h("div", { className: pages_module_css_default.status }, notice) : null, showQuery ? h("div", { className: pages_module_css_default.item }, react.createElement("label", { className: pages_module_css_default.search }, h(SearchIcon, null), h("input", {
5877
- type: "search",
5878
- value: query,
5879
- disabled: props.remote === void 0,
5880
- placeholder: "搜索记忆与文档(按回车检索)",
5881
- "aria-label": "搜索记忆与文档",
5882
- autoFocus: true,
5883
- onChange: (event) => {
5884
- setQuery(event.target.value);
5885
- },
5886
- onKeyDown: (event) => {
5887
- if (event.key === "Enter") {
5888
- event.preventDefault();
5889
- run();
5890
- }
5891
- }
5892
- })), props.remote === void 0 ? null : h("div", { className: pages_module_css_default.toolbar }, h("label", { className: pages_module_css_default.field }, "类型", h("select", {
5893
- className: pages_module_css_default.input,
5894
- value: kind,
5895
- onChange: (e) => {
5896
- setKind(e.target.value);
5897
- }
5898
- }, h("option", { value: "all" }, "全部"), h("option", { value: "fact" }, "仅记忆"), h("option", { value: "doc_chunk" }, "仅文档"))), h("label", { className: pages_module_css_default.field }, "门槛", h("select", {
5899
- className: pages_module_css_default.input,
5900
- value: floors,
5901
- onChange: (e) => {
5902
- setFloors(e.target.value);
5903
- }
5904
- }, h("option", { value: "strict" }, "严格"), h("option", { value: "loose" }, "宽松"))), h(DomainField, {
5905
- label: "知识域(domain)",
5906
- value: domain,
5907
- onChange: setDomain,
5908
- catalog: domainCatalog,
5909
- emptyLabel: "全部",
5910
- placeholder: "如 design"
5911
- }), h(Field, {
5912
- label: "来源(source)",
5913
- value: source,
5914
- onChange: setSource,
5915
- placeholder: "如 spec"
5916
- }), h(Btn, {
5917
- label: "检索",
5918
- disabled: queryLoading,
5919
- onClick: run
5920
- })), props.remote === void 0 ? null : h("div", { className: pages_module_css_default.status }, queryLoading ? "查询中…" : queryStatus), props.remote !== void 0 && !queryLoading && error === void 0 && hits.length === 0 && queryStatus.startsWith("命中") && floors === "strict" && floorDropped > 0 ? h("div", { className: pages_module_css_default.status }, `没有达到相关性门槛的结果(严格门槛丢弃了 ${String(floorDropped)} 条)——可切「宽松」重试。`) : null, props.remote !== void 0 && !queryLoading && error === void 0 && hits.length === 0 && queryStatus.startsWith("命中") && floors === "loose" && floorDropped > 0 ? h("div", { className: pages_module_css_default.status }, `确实没有相关结果(宽松门槛下仍丢弃了 ${String(floorDropped)} 条)。`) : null, props.remote !== void 0 && !queryLoading && error === void 0 && hits.length === 0 && queryStatus.startsWith("命中") && floorDropped === 0 ? h("div", { className: pages_module_css_default.status }, "没有匹配项。知识库为空时先用「入库/导入」添加文档;记忆里也只包含已写入的事实。") : null, groups.map(([groupName, groupHits]) => h("div", { key: groupName }, h("div", { className: pages_module_css_default.groupHead }, groupName), h("ul", { className: pages_module_css_default.list }, groupHits.map((hit) => h("li", {
5921
- key: `${hit.source_ref}:${String(hit.ref_id)}`,
5922
- className: pages_module_css_default.item
5923
- }, hit.text, h("span", { className: pages_module_css_default.meta }, `${hit.kind} · score ${hit.score.toFixed(3)} · ${hit.source_ref}` + (hit.entities && hit.entities.length > 0 ? ` · 实体:${hit.entities.join("、")}` : "") + ` · 创建于 ${hit.created_at}` + (hit.updated_at !== null && hit.updated_at !== hit.created_at ? ` · 更新于 ${hit.updated_at}` : "") + (legEvidenceLine(hit) === null ? "" : ` · ${legEvidenceLine(hit) ?? ""}`)))))))) : null, showIngest ? h("div", { className: `${pages_module_css_default.item} ${pages_module_css_default.form}` }, h("div", { className: pages_module_css_default.toolbar }, h(DomainField, {
5924
- label: "知识域(domain)",
5925
- value: iDomain,
5926
- onChange: setIDomain,
5927
- catalog: domainCatalog,
5928
- emptyLabel: "请选择知识域",
5929
- placeholder: "如 design",
5930
- onAdd: addDomain
5931
- }), h(Field, {
5932
- label: "来源(source)",
5933
- value: iSource,
5934
- onChange: setISource,
5935
- placeholder: "缺省 default"
5936
- }), h(Field, {
5937
- label: "标题(title,可选)",
5938
- value: iTitle,
5939
- onChange: setITitle,
5940
- placeholder: "缺省取正文首个标题或首行"
5941
- })), h("div", { className: pages_module_css_default.toolbar }, h("button", {
5942
- type: "button",
5943
- className: inputMode === "source" ? `${pages_module_css_default.btn} ${pages_module_css_default.btnActive}` : pages_module_css_default.btn,
5944
- onClick: () => {
5945
- setInputMode("source");
5946
- }
5947
- }, sourceText !== "" && inputMode !== "source" ? "来源 ·有内容" : "来源"), h("button", {
5948
- type: "button",
5949
- className: inputMode === "text" ? `${pages_module_css_default.btn} ${pages_module_css_default.btnActive}` : pages_module_css_default.btn,
5950
- onClick: () => {
5951
- setInputMode("text");
5952
- }
5953
- }, bodyText !== "" && inputMode !== "text" ? "粘贴文本 ·有内容" : "粘贴文本")), inputMode === "source" ? h("div", { className: pages_module_css_default.toolbar }, h(Field, {
5954
- label: "来源(http(s) URL / 本地文件 / 本地目录,多条路径用空格或逗号分隔)",
5955
- value: iUri,
5956
- onChange: setIUri,
5957
- placeholder: "如 https://… 或 ~/docs/spec.md 或 ~/docs"
5958
- }), h(Btn, {
5959
- label: "选择",
5960
- disabled: loading,
5961
- onClick: () => {
5962
- browseTo();
5963
- }
5964
- })) : h("label", { className: pages_module_css_default.field }, "粘贴文本", h("textarea", {
5965
- className: pages_module_css_default.textarea,
5966
- value: iText,
5967
- rows: 4,
5968
- placeholder: "直接粘贴要入库的内容",
5969
- onChange: (e) => {
5970
- setIText(e.target.value);
5971
- }
5972
- })), h("div", { className: pages_module_css_default.row }, h(Btn, {
5973
- label: "入库",
5974
- disabled: loading || !canIngest,
5975
- onClick: submitIngest
5976
- }), h("div", { className: ingestBlocked ? `${pages_module_css_default.status} ${pages_module_css_default.hintWarn}` : pages_module_css_default.status }, ingestHint)), picker === null ? null : h("div", { className: pages_module_css_default.picker }, h("div", { className: pages_module_css_default.pickerHead }, h("code", { className: pages_module_css_default.pickerPath }, picker.listing?.path ?? "读取中…"), h("div", { className: pages_module_css_default.status }, picker.listing === null ? "" : picker.listing.unrestricted ? "不限制范围(knowledge.ingest.allow_outside_workspace = true),可浏览整个文件系统" : `可浏览范围:${picker.listing.roots.join("、")}`)), picker.error === void 0 ? null : h("div", { className: pages_module_css_default.error }, picker.error), picker.listing === null ? null : h("ul", { className: pages_module_css_default.pickerList }, picker.listing.parent === null ? null : h("li", { key: ".." }, h("button", {
5977
- type: "button",
5978
- className: pages_module_css_default.pickerEntry,
5979
- onClick: () => {
5980
- browseTo(picker.listing?.parent ?? void 0);
5981
- }
5982
- }, "../(上级目录)")), picker.listing.entries.map((entry) => h("li", { key: entry.path }, h("button", {
5983
- type: "button",
5984
- className: entry.kind === "other" ? `${pages_module_css_default.pickerEntry} ${pages_module_css_default.pickerDisabled}` : pages_module_css_default.pickerEntry,
5985
- disabled: entry.kind === "other",
5986
- onClick: () => {
5987
- if (entry.kind === "dir") browseTo(entry.path);
5988
- else choosePath(entry.path);
5989
- }
5990
- }, entry.kind === "dir" ? `${entry.name}/` : entry.kind === "ingestable" ? entry.name : `${entry.name}(不可摄入)`)))), h("div", { className: pages_module_css_default.row }, picker.listing === null ? null : h(Btn, {
5991
- label: "选择此目录",
5992
- disabled: loading,
5993
- onClick: () => {
5994
- choosePath(picker.listing?.path ?? "");
5995
- }
5996
- }), h(Btn, {
5997
- label: "取消",
5998
- disabled: loading,
5999
- onClick: () => {
6000
- setPicker(null);
6001
- }
6002
- })))) : null, docs === null ? h("div", { className: pages_module_css_default.status }, error === void 0 ? "读取文档列表…" : "文档列表读取失败。", error === void 0 ? null : h(Btn, {
6003
- label: "重试",
6004
- disabled: loading,
6005
- onClick: () => {
6006
- refresh();
6007
- }
6008
- })) : docs.length === 0 ? h("div", { className: pages_module_css_default.status }, "知识库暂无文档。可用「入库/导入」添加。") : h("ul", { className: pages_module_css_default.list }, docs.map((doc) => h("li", {
6009
- key: doc.doc_id,
6010
- className: pages_module_css_default.item
6011
- }, `${doc.title}(${doc.domain} → ${doc.source})`, staleIds.has(doc.doc_id) ? h("span", { className: pages_module_css_default.warn }, "文件已修改 · 待重新摄入") : null, missingIds.has(doc.doc_id) ? h("span", { className: pages_module_css_default.warn }, unclaimedIds.has(doc.doc_id) ? "文件无 frontmatter · 可认领" : "受管文件缺失") : null, h("span", { className: pages_module_css_default.meta }, `#${String(doc.doc_id)}${doc.source_uri ? ` · ${doc.source_uri}` : ""}${doc.updated_at ? ` · 更新于 ${doc.updated_at}` : ""}`), h("div", { className: pages_module_css_default.row }, h(Btn, {
6012
- key: "detail",
6013
- label: docDetail?.id === doc.doc_id ? "收起切片" : "查看切片",
6014
- disabled: loading,
6015
- onClick: () => {
6016
- showDocumentDetail(doc.doc_id);
6017
- }
6018
- }), h(Btn, {
6019
- key: "edit",
6020
- label: "编辑",
6021
- disabled: loading || missingIds.has(doc.doc_id),
6022
- onClick: () => {
6023
- openDoc(doc.doc_id, "file");
6024
- }
6025
- }), h(Btn, {
6026
- key: "dir",
6027
- label: "打开目录",
6028
- disabled: loading || missingIds.has(doc.doc_id),
6029
- onClick: () => {
6030
- openDoc(doc.doc_id, "dir");
6031
- }
6032
- }), staleIds.has(doc.doc_id) ? h(Btn, {
6033
- key: "resync",
6034
- label: "重新摄入",
6035
- disabled: loading,
6036
- onClick: () => {
6037
- syncDocs(doc.doc_id);
6038
- }
6039
- }) : null, unclaimedIds.has(doc.doc_id) ? h(Btn, {
6040
- key: "adopt",
6041
- label: "认领文件",
6042
- disabled: loading,
6043
- onClick: () => {
6044
- syncDocs(doc.doc_id, true);
6045
- }
6046
- }) : null, h(Btn, {
6047
- key: "remove",
6048
- label: "删除",
6049
- danger: true,
6050
- disabled: loading,
6051
- onClick: () => {
6052
- confirmRemoveDoc(doc);
6053
- }
6054
- })), docDetail?.id === doc.doc_id ? h("div", { className: pages_module_css_default.meta }, `共 ${String(docDetail.data.chunks?.length ?? 0)} 个切片`, (docDetail.data.chunks ?? []).slice(0, 20).map((chunk) => h("div", {
6055
- key: chunk.chunk_id,
6056
- className: pages_module_css_default.chunkBox
6057
- }, h("span", { className: pages_module_css_default.badge }, `#${String(chunk.idx)}${chunk.headings_path ? ` · ${chunk.headings_path}` : ""}`), h("div", null, chunk.text.length > 200 ? `${chunk.text.slice(0, 200)}…` : chunk.text))), (docDetail.data.chunks?.length ?? 0) > 20 ? h("div", null, `……其余 ${String((docDetail.data.chunks?.length ?? 0) - 20)} 个切片省略`) : null) : null))), docsTruncated ? h("button", {
6058
- type: "button",
6059
- className: pages_module_css_default.more,
6060
- disabled: loading,
6061
- onClick: () => {
6062
- loadDocs(docsOffset);
6063
- }
6064
- }, loading ? "加载中…" : "加载更多") : null);
6065
- }
6066
5425
  /** Client cordis-plugin name shown in boot diagnostics. */
6067
5426
  const name = "avantf-mem-client";
6068
5427
  /**
6069
- * Services required to render the two tabs. `remote` (not `remote.avantfMem`)
5428
+ * Services required to render the tab. `remote` (not `remote.avantfMem`)
6070
5429
  * on purpose: a missing namespace must not leave this entry pending, because
6071
5430
  * the client boot rejects the whole tree when any entry is pending.
6072
5431
  */
6073
5432
  const inject = ["slots", "remote"];
6074
- /** Contribute the two `conversation.view` tabs, mounting this package's Remote namespace first. */
5433
+ /** Contribute the `conversation.view` tab, mounting this package's Remote namespace first. */
6075
5434
  async function apply(ctx) {
6076
5435
  let remote;
6077
5436
  try {
@@ -6088,13 +5447,7 @@ Set the \`cycles\` parameter to \`"ref"\` to resolve cyclical schemas with defs.
6088
5447
  order: 20,
6089
5448
  label: () => "记忆"
6090
5449
  }, () => react.createElement(MemoryPanel, { remote })));
6091
- ctx.slots.inject("conversation.view", () => ctx.slots.register({
6092
- name: "conversation.view",
6093
- id: "knowledge",
6094
- order: 30,
6095
- label: () => "知识"
6096
- }, () => react.createElement(KnowledgePanel, { remote })));
6097
- console.log("[avantf-mem] client apply: tabs registered (记忆, 知识) in the conversation view strip");
5450
+ console.log("[avantf-mem] client apply: tab registered (记忆) in the conversation view strip");
6098
5451
  }
6099
5452
  //#endregion
6100
5453
  exports.apply = apply;