@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.
- package/README.md +4 -0
- package/lib/client.js +548 -1195
- package/lib/dsh-build.json +1 -0
- package/lib/engine/graph-recall/in-memory.d.ts +93 -0
- package/lib/engine/graph-recall/index.d.ts +22 -0
- package/lib/engine/graph-recall/traverse.d.ts +119 -0
- package/lib/engine/graph-recall/types.d.ts +188 -0
- package/lib/engine/mem/config/config_files.d.ts +1 -1
- package/lib/engine/mem/config/loader.d.ts +2 -3
- package/lib/engine/mem/config/paths.d.ts +2 -13
- package/lib/engine/mem/db/dao/contradictions.d.ts +4 -22
- package/lib/engine/mem/db/dao/entities.d.ts +80 -7
- package/lib/engine/mem/db/dao/fact_sources.d.ts +3 -13
- package/lib/engine/mem/db/dao/facts.d.ts +311 -56
- package/lib/engine/mem/db/dao/triples.d.ts +1 -26
- package/lib/engine/mem/db/graph.d.ts +249 -0
- package/lib/engine/mem/db/journal_notes.d.ts +35 -0
- package/lib/engine/mem/db/migrations.d.ts +9 -0
- package/lib/engine/mem/db/port.d.ts +22 -3
- package/lib/engine/mem/db/schema.d.ts +119 -0
- package/lib/engine/mem/db/tokenizer.d.ts +9 -0
- package/lib/engine/mem/dbdrivers/fake/driver.d.ts +75 -0
- package/lib/engine/mem/dbdrivers/fake/index.d.ts +7 -0
- package/lib/engine/mem/dbdrivers/fake/surface.d.ts +34 -0
- package/lib/engine/mem/dbdrivers/fake/transport.d.ts +61 -0
- package/lib/engine/mem/dbdrivers/sqlite/connection.d.ts +14 -0
- package/lib/engine/mem/dbdrivers/sqlite/contradictions.d.ts +35 -0
- package/lib/engine/mem/dbdrivers/sqlite/dao/index.d.ts +16 -0
- package/lib/engine/mem/dbdrivers/sqlite/driver.d.ts +54 -0
- package/lib/engine/mem/dbdrivers/sqlite/facts.d.ts +117 -0
- package/lib/engine/mem/dbdrivers/sqlite/index.d.ts +6 -0
- package/lib/engine/mem/dbdrivers/sqlite/migrations.d.ts +15 -0
- package/lib/engine/mem/dbdrivers/sqlite/relations.d.ts +62 -0
- package/lib/engine/mem/dbdrivers/sqlite/search.d.ts +56 -0
- package/lib/engine/mem/dbdrivers/sqlite/sources.d.ts +37 -0
- package/lib/engine/mem/defaults.d.ts +3 -4
- package/lib/engine/mem/dispatch.d.ts +7 -32
- package/lib/engine/mem/entities/extract.d.ts +44 -47
- package/lib/engine/mem/eval/derived_runner.d.ts +6 -1
- package/lib/engine/mem/eval/relation_gold.d.ts +251 -0
- package/lib/engine/mem/eval/write_metrics.d.ts +1 -1
- package/lib/engine/mem/facts_io.d.ts +50 -10
- package/lib/engine/mem/index.d.ts +6 -2
- package/lib/engine/mem/interfaces/capabilities.d.ts +30 -0
- package/lib/engine/mem/interfaces/common.d.ts +11 -0
- package/lib/engine/mem/interfaces/errors.d.ts +65 -0
- package/lib/engine/mem/interfaces/index.d.ts +18 -0
- package/lib/engine/mem/interfaces/migrations.d.ts +21 -0
- package/lib/engine/mem/interfaces/repositories.d.ts +471 -0
- package/lib/engine/mem/interfaces/search_index.d.ts +113 -0
- package/lib/engine/mem/interfaces/storage_backend.d.ts +84 -0
- package/lib/engine/mem/interfaces/storage_driver.d.ts +43 -0
- package/lib/engine/mem/interfaces/version.d.ts +34 -0
- package/lib/engine/mem/lifecycle/contradiction.d.ts +2 -2
- package/lib/engine/mem/modelBootstrap.d.ts +1 -1
- package/lib/engine/mem/relations/confidence.d.ts +39 -0
- package/lib/engine/mem/relations/extract.d.ts +128 -0
- package/lib/engine/mem/relations/index.d.ts +32 -0
- package/lib/engine/mem/relations/name-quality.d.ts +78 -0
- package/lib/engine/mem/relations/normalize.d.ts +275 -0
- package/lib/engine/mem/relations/patterns.d.ts +67 -0
- package/lib/engine/mem/relations/statement.d.ts +88 -0
- package/lib/engine/mem/relations/types.d.ts +79 -0
- package/lib/engine/mem/runtime.d.ts +61 -88
- package/lib/engine/mem/storage/create.d.ts +42 -0
- package/lib/engine/mem/storage/errors.d.ts +65 -0
- package/lib/engine/mem/storage/index.d.ts +15 -0
- package/lib/engine/mem/storage/ports.d.ts +472 -0
- package/lib/engine/mem/storage/registry.d.ts +37 -0
- package/lib/engine/mem/storage/sqlite.d.ts +28 -0
- package/lib/engine/mem/store/common.d.ts +1 -1
- package/lib/engine/mem/store/entity_leg.d.ts +59 -2
- package/lib/engine/mem/store/entity_sweep.d.ts +4 -3
- package/lib/engine/mem/store/explicit_relations.d.ts +52 -0
- package/lib/engine/mem/store/facts_restore.d.ts +10 -2
- package/lib/engine/mem/store/floors.d.ts +4 -1
- package/lib/engine/mem/store/graph_reader.d.ts +146 -0
- package/lib/engine/mem/store/hybrid.d.ts +50 -13
- package/lib/engine/mem/store/kinds.d.ts +47 -0
- package/lib/engine/mem/store/legs.d.ts +24 -29
- package/lib/engine/mem/store/lexical.d.ts +36 -45
- package/lib/engine/mem/store/memory.d.ts +471 -83
- package/lib/engine/mem/store/recall_policy.d.ts +49 -0
- package/lib/engine/mem/store/relation_backfill.d.ts +210 -0
- package/lib/engine/mem/store/relation_planner.d.ts +191 -0
- package/lib/engine/mem/store/secret_guard.d.ts +0 -1
- package/lib/engine/mem/store/self_query.d.ts +4 -2
- package/lib/engine/mem/store/time_filter.d.ts +121 -0
- package/lib/engine/mem/store/titles.d.ts +52 -0
- package/lib/engine/mem/store/vector_repair.d.ts +44 -54
- package/lib/engine/mem-contract/config.d.ts +44 -38
- package/lib/engine/mem-contract/text.d.ts +1 -1
- package/lib/engine/mem-contract/tools.d.ts +145 -132
- package/lib/engine/mem-contract/types.d.ts +364 -288
- package/lib/index.js +12048 -11337
- package/lib/interface-version.json +1 -1
- package/lib/types/client/index.d.ts +2 -2
- package/lib/types/client/index.d.ts.map +1 -1
- package/lib/types/client/paging.d.ts +3 -12
- package/lib/types/client/paging.d.ts.map +1 -1
- package/lib/types/client/summarize.d.ts.map +1 -1
- package/lib/types/envinit-bootstrap.d.ts +1 -1
- package/lib/types/envinit.d.ts +2 -6
- package/lib/types/envinit.d.ts.map +1 -1
- package/lib/types/hints.d.ts +28 -24
- package/lib/types/hints.d.ts.map +1 -1
- package/lib/types/index.d.ts +21 -40
- package/lib/types/index.d.ts.map +1 -1
- package/lib/types/inject.d.ts +3 -2
- package/lib/types/inject.d.ts.map +1 -1
- package/lib/types/interface_gate.d.ts +1 -1
- package/lib/types/prompt.d.ts +17 -41
- package/lib/types/prompt.d.ts.map +1 -1
- package/lib/types/provision.d.ts +3 -0
- package/lib/types/provision.d.ts.map +1 -1
- package/lib/types/remote.d.ts +14 -7
- package/lib/types/remote.d.ts.map +1 -1
- package/lib/types/tool_schema.d.ts +4 -0
- package/lib/types/tool_schema.d.ts.map +1 -1
- package/lib/types/vectorMigration.d.ts +5 -7
- package/lib/types/vectorMigration.d.ts.map +1 -1
- package/package.json +15 -14
- package/lib/engine/mem/config/domains.d.ts +0 -20
- package/lib/engine/mem/db/dao/chunks.d.ts +0 -231
- package/lib/engine/mem/db/dao/documents.d.ts +0 -51
- package/lib/engine/mem/db/hash.d.ts +0 -2
- package/lib/engine/mem/db/knowledge.d.ts +0 -17
- package/lib/engine/mem/eval/index.d.ts +0 -4
- package/lib/engine/mem/git.d.ts +0 -73
- package/lib/engine/mem/router.d.ts +0 -51
- package/lib/engine/mem/store/doc_files.d.ts +0 -125
- package/lib/engine/mem/store/document_text.d.ts +0 -47
- package/lib/engine/mem/store/ingest_guard.d.ts +0 -25
- package/lib/engine/mem/store/knowledge.d.ts +0 -598
- package/lib/engine/mem/store/source_picker.d.ts +0 -43
- package/lib/engine/mem-retrieval/adapters/local_reranker.d.ts +0 -52
- package/lib/engine/mem-retrieval/adapters/none_reranker.d.ts +0 -10
- package/lib/engine/mem-retrieval/rerank.d.ts +0 -13
- package/lib/types/client/domains.d.ts +0 -53
- package/lib/types/client/domains.d.ts.map +0 -1
- package/lib/types/client/ingest.d.ts +0 -24
- package/lib/types/client/ingest.d.ts.map +0 -1
- package/lib/types/open.d.ts +0 -77
- package/lib/types/open.d.ts.map +0 -1
- package/lib/types/reconcile.d.ts +0 -64
- 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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
4091
|
-
*
|
|
4092
|
-
*
|
|
4093
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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):
|
|
4404
|
-
*
|
|
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 ===
|
|
4528
|
-
if (hostWire === void 0) return `宿主没有回报 wire 版本(它比本客户端旧):面板可能缺少新字段,重启 dsh web 让两半对齐。(本客户端说 wire=${String(
|
|
4529
|
-
return `宿主回报的 wire 版本是 ${String(hostWire)},本客户端只认识 ${String(
|
|
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
|
|
4571
|
-
*
|
|
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
|
|
4586
|
-
*
|
|
4587
|
-
*
|
|
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
|
|
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
|
-
*
|
|
4826
|
-
* the
|
|
4827
|
-
*
|
|
4828
|
-
*
|
|
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
|
-
*
|
|
4831
|
-
*
|
|
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
|
|
4834
|
-
const
|
|
4835
|
-
const [draft, setDraft] = react.useState(
|
|
4836
|
-
|
|
4837
|
-
|
|
4838
|
-
|
|
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
|
|
4845
|
-
const
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
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
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
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
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
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
|
-
}
|
|
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
|
-
|
|
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("
|
|
5058
|
-
action: "
|
|
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)),
|
|
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.
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
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
|
-
|
|
5209
|
-
|
|
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))} 条)`)
|
|
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
|
|
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
|
|
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
|
-
|
|
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;
|