dsh-meili-search 1.1.2 → 1.2.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/AGENTS.md CHANGED
@@ -9,7 +9,9 @@ DSH(DeepSeek Harness)web 插件的会话**内容搜索**增强:
9
9
  - 服务端插件(`index.js` + `core.mjs` + `indexer.mjs`):把会话 zstd 日志索引进 Meilisearch(增量同步),并提供同源 `/dsh-meili/v1/*` API。
10
10
  - 客户端插件(`client.js`):在浏览器里 hook 原版侧栏搜索的内容检索、接管内容命中行的点击(跳转到消息 + 高亮)、并在 Settings→General 渲染配置卡。
11
11
  - 索引/状态/设置文件(默认,跟随 `DSH_HOME=/root/.dsh`):
12
- - 会话源: `/root/.dsh/sessions/**/session.jsonl.zstd`(每行 `{type,seq,time,data,surfaceOp}`,只索引 `user/message` 与 `assistant/message`)
12
+ - 会话源: `/root/.dsh/sessions/**/session.v3.jsonl.zstd`(新格式, 优先)与 `session.jsonl.zstd`(旧格式, 回退);
13
+ 每会话目录只取一个文件 —— v3 存在时用 v3(DSH 升级后活跃写入的是 v3, v2 会冻结)。
14
+ 每行 `{type,seq,time,data,surfaceOp}`,只索引 `user/message` 与 `assistant/message`
13
15
  - 游标状态: `/root/.dsh/meili-index-state.json`(schema v2)
14
16
  - 设置: `/root/.dsh/dsh-meili-settings.json`
15
17
  - Meili: `http://192.168.43.2:7700`,索引 `dsh_sessions`(无 master key)
@@ -58,6 +60,8 @@ node --check core.mjs index.js indexer.mjs client.js
58
60
  # node indexer.mjs --once # 期望 full
59
61
  # 换更大文件(追加 2 条)再跑 # 期望 append, added=新消息分块数
60
62
  # 再跑一次 # 期望 unchanged
63
+ # 2b) 双格式目录(同一会话目录 v2+v3 并存): 期望只收录 v3 一个文件(v2 键从 state 消失时
64
+ # 因同 sid 仍有活文件而**不**删文档), 且不出现重复/互相覆盖的文档。
61
65
  # 收尾: curl -X DELETE <meili>/indexes/dsh_sync_test; rm -rf /tmp/synctest
62
66
 
63
67
  # 3) API
package/README.md CHANGED
@@ -50,14 +50,18 @@
50
50
  ## 同步机制
51
51
 
52
52
  ```
53
- /root/.dsh/sessions/**/session.jsonl.zstd
53
+ /root/.dsh/sessions/**/(session.v3.jsonl.zstd | session.jsonl.zstd)
54
+ └─ 每会话目录只取一个数据文件: 优先 v3 新格式(DSH 活跃写入), 无 v3 时回退 v2 旧格式
55
+ (二者并存时不能都收录 —— 分块 id 相同会互相覆盖)
54
56
  └─ 每轮(定时/手动/CLI):
55
57
  文件未变(mtime+size) ──────────────→ 跳过
56
58
  文件增长 & v2 游标 ────────────────→ 解析 seq>lastSeq 尾部, 追加新分块(续号)
57
59
  首次 / 旧版游标 / 文件缩小(重写) ──→ 整会话重灌(先删该 sessionId 再全量 add)
58
60
  ```
59
61
 
60
- 游标状态: `/root/.dsh/meili-index-state.json`;每轮结束写回。被删除的会话文件按 sessionId 清理。
62
+ 游标状态: `/root/.dsh/meili-index-state.json`;每轮结束写回。被删除的会话文件按 sessionId 清理;
63
+ 旧键(session.jsonl.zstd)消失但同目录仍有 v3 文件时**不**按 sessionId 删文档 —— 避免 v2→v3
64
+ 切换期把刚灌入的文档误删。每个会话目录首次从 v2 切到 v3 会触发一次该会话整会话重灌(一次性)。
61
65
 
62
66
  ## API(同源,经 DSH web 服务)
63
67
 
package/client.js CHANGED
@@ -423,10 +423,12 @@ window.__ModuleLoader__.load({
423
423
  const attempt = () => {
424
424
  // 防重: HMR/重复 apply 时 DOM 已存在旧实例则跳过
425
425
  if (document.querySelector(".dshMeiliUI")) return;
426
- // 会话树: [role=tree] 在 .qDHVXG_treeBody(position:relative, flex:1) 内
426
+ // 会话树: [role=tree] 在 *_treeBody(position:relative, flex:1) 内。
427
+ // dsh 0.1.5 起 CSS Module 的哈希前缀随构建变化(0.1.1 = qDHVXG_, 0.1.5 = bhn1Oq_),
428
+ // 写死前缀必然失配(0.1.5 升级后胶囊压住树首行就是这个原因), 一律改用 "_后缀" 匹配。
427
429
  const tree = document.querySelector('[role="tree"]');
428
430
  if (!tree) { if (attempts < 100) { attempts += 1; setTimeout(attempt, 250); } return; }
429
- const treeBody = tree.closest(".qDHVXG_treeBody") || tree.parentElement;
431
+ const treeBody = tree.closest('[class*="_treeBody"]') || tree.parentElement;
430
432
  if (!treeBody) { if (attempts < 100) { attempts += 1; setTimeout(attempt, 250); } return; }
431
433
  if (uiRoot && uiRoot.isConnected) return;
432
434
  const wrap = document.createElement("div");
@@ -498,20 +500,38 @@ window.__ModuleLoader__.load({
498
500
  // 插入: 绝对定位覆盖层, 不移动/不包裹任何 React 子节点
499
501
  // (此前把 treeBody 移入 wrapper 破坏了 React 子树所有权 → 官方搜索激活渲染
500
502
  // SearchResults 替换 FlatList 时 React DOM 协调异常, 侧栏卸载消失)
501
- const rootEl = treeBody.closest(".qDHVXG_root") || treeBody.parentElement;
503
+ const rootEl = treeBody.closest('[class*="_root"]') || treeBody.parentElement;
502
504
  if (!rootEl) { if (attempts < 100) { attempts += 1; setTimeout(attempt, 250); } return; }
503
505
  // root 需为定位基准(核心未设 position, overlay absolute 会错位到页面别处)
504
506
  if (getComputedStyle(rootEl).position === "static") rootEl.style.position = "relative";
507
+ // 胶囊纵向基准 = root 内“树之前”的全部兄弟块高度和 + 4px 间隙。
508
+ // 0.1.5: root 含 sectionHeader(Workspaces 标题行 36px) → 40px; 与旧版 40px 一致,
509
+ // 这样树列表只需恒定下推 34px(胶囊 30 + 间隙 4), 胶囊正好落在标题与树之间的空档。
510
+ let capTop = 40;
511
+ try {
512
+ let acc = 0;
513
+ for (const child of Array.from(rootEl.children)) {
514
+ if (child.contains(treeBody)) break;
515
+ if (child.classList && child.classList.contains("dshMeiliOverlay")) continue;
516
+ acc += child.getBoundingClientRect().height;
517
+ }
518
+ capTop = Math.round(acc) + 4;
519
+ } catch {}
505
520
  const overlay = document.createElement("div");
506
521
  overlay.className = "dshMeiliOverlay";
522
+ overlay.style.setProperty("--dshMeiliCapTop", capTop + "px");
507
523
  rootEl.appendChild(overlay);
508
524
  overlay.appendChild(wrap);
509
525
  overlay.appendChild(panel);
510
526
  // 原版搜索激活(SearchResults 视图)时隐藏胶囊: 胶囊是 root 级覆盖层,
511
527
  // 官方搜索结果视图会替换树内容, 若胶囊不隐藏会叠在原版结果上方
512
528
  const syncVis = () => {
513
- const inOrigSearch = !!rootEl.querySelector(".qDHVXG_searchTree");
529
+ // 原版搜索视图(SearchResults)判据同样不能写死哈希前缀
530
+ const inOrigSearch = !!(rootEl.querySelector('[class*="_searchTree"]') || rootEl.querySelector(".qDHVXG_searchTree"));
514
531
  wrap.style.display = inOrigSearch ? "none" : "";
532
+ // 让位/取消让位经 body 级类名切换(CSS 用 body:not(.dshMeiliSearchView) 匹配),
533
+ // 不去改 React 管理的 root 类名(重渲染会覆盖)
534
+ try { document.body.classList.toggle("dshMeiliSearchView", inOrigSearch); } catch {}
515
535
  if (inOrigSearch) hide();
516
536
  };
517
537
  syncVis();
@@ -1053,17 +1073,19 @@ window.__ModuleLoader__.load({
1053
1073
  input.dshMeiliThrottle:focus-visible { outline: 1px solid #1f6feb; outline-offset: 2px; }
1054
1074
  /* ---- 工作区自建内容搜索 UI ---- */
1055
1075
  .dshMeiliOverlay { position: absolute; left: 0; right: 0; top: 0; bottom: 0; z-index: 45; pointer-events: none; }
1056
- /* 树顶部让出胶囊位置: list 本身从 root 40px(标题区)起, 胶囊占 40-70px, 故 padding-top 30+间隙4 = 34 */
1057
- .qDHVXG_root .qDHVXG_listArea .qDHVXG_list { padding-top: 34px; }
1058
- /* 原版搜索结果视图(SearchResults, 含 .qDHVXG_searchTree): 胶囊隐藏 + 让位取消 */
1059
- .qDHVXG_root:has(.qDHVXG_searchTree) .qDHVXG_listArea .qDHVXG_list { padding-top: 0; }
1060
- .dshMeiliOverlay .dshMeiliUI { position: absolute; left: 0; right: 12px; top: 40px; pointer-events: auto; }
1076
+ /* 树顶部让出胶囊位置: 胶囊高 30 + 间隙 4 = 34。
1077
+ 类名哈希随 dsh 版本变化(0.1.1 qDHVXG_ / 0.1.5 bhn1Oq_), 故用 "_后缀" 匹配:
1078
+ _listArea 是容器, 其中要下推的是 _list(排除 _listArea 自身的嵌套)。 */
1079
+ body:not(.dshMeiliSearchView) [class*="_listArea"] [class*="_list"]:not([class*="_listArea"]) { padding-top: 34px; }
1080
+ /* 原版搜索结果视图(SearchResults, *_searchTree): 取消让位(胶囊由 JS 隐藏) */
1081
+ body.dshMeiliSearchView [class*="_listArea"] [class*="_list"]:not([class*="_listArea"]) { padding-top: 0; }
1082
+ .dshMeiliOverlay .dshMeiliUI { position: absolute; left: 0; right: 12px; top: var(--dshMeiliCapTop, 40px); pointer-events: auto; }
1061
1083
  .dshMeiliCap { box-sizing: border-box; display: flex; align-items: center; gap: 6px; width: 100%; height: 30px; padding: 0 4px 0 10px; border-radius: 10px; background: rgba(255,255,255,.62); -webkit-backdrop-filter: blur(18px) saturate(1.4); backdrop-filter: blur(18px) saturate(1.4); border: 1px solid var(--dsw-alias-border-l2, rgba(128,132,148,.35)); color: var(--dsw-alias-label-caption, var(--dsh-text-secondary, #8a90a5)); transition: border-color .15s, box-shadow .15s; }
1062
1084
  body[data-ds-dark-theme] .dshMeiliCap { background: rgba(24,27,36,.66); }
1063
1085
  .dshMeiliCap:focus-within { border-color: var(--dsw-alias-acc-primary, #1f6feb); }
1064
1086
  .dshMeiliCap input { flex: 1; min-width: 0; background: transparent; border: 0; outline: 0; color: var(--dsw-alias-label-primary, inherit); font: 13px/18px inherit; }
1065
1087
  .dshMeiliCap input::placeholder { color: var(--dsw-alias-label-tertiary, var(--dsh-text-secondary, #8a90a5)); }
1066
- .dshMeiliPanel { position: absolute; left: 0; right: 0; top: 78px; bottom: 0; z-index: 50; overflow: hidden; border-radius: 12px; background: var(--dsw-specific-sidebar-fill, #0c0f16); pointer-events: auto; }
1088
+ .dshMeiliPanel { position: absolute; left: 0; right: 0; top: calc(var(--dshMeiliCapTop, 40px) + 38px); bottom: 0; z-index: 50; overflow: hidden; border-radius: 12px; background: var(--dsw-specific-sidebar-fill, #0c0f16); pointer-events: auto; }
1067
1089
  .dshMeiliPanel .dshMeiliScroll { z-index: 1; }
1068
1090
  .dshMeiliPanel[hidden] { display: none; }
1069
1091
  .dshMeiliScroll { position: absolute; inset: 0; overflow-y: auto; padding: 2px 28px 30px 2px; display: flex; flex-direction: column; gap: 9px; }
package/core.mjs CHANGED
@@ -52,11 +52,19 @@ async function deleteSessionDocs(cfg, sid) {
52
52
  export function sessionFiles(root) {
53
53
  const out = [];
54
54
  const walk = (dir) => {
55
+ // 每会话目录只收录一个数据文件: 优先新格式 session.v3.jsonl.zstd(活跃写入),
56
+ // 无 v3 时回退旧格式 session.jsonl.zstd —— 二者并存时若都收录, 分块 id 会撞号互相覆盖。
57
+ const v3 = join(dir, "session.v3.jsonl.zstd");
58
+ if (existsSync(v3)) {
59
+ out.push(v3);
60
+ } else {
61
+ const v2 = join(dir, "session.jsonl.zstd");
62
+ if (existsSync(v2)) out.push(v2);
63
+ }
55
64
  for (const name of readdirSync(dir)) {
56
65
  const p = join(dir, name);
57
66
  const st = statSync(p);
58
67
  if (st.isDirectory()) walk(p);
59
- else if (name === "session.jsonl.zstd") out.push(p);
60
68
  }
61
69
  };
62
70
  walk(root);
@@ -352,7 +360,10 @@ export async function syncOnce(cfg, opts = {}) {
352
360
  }
353
361
  let goneCount = 0;
354
362
  if (!opts.noGone) {
355
- const gone = Object.keys(cur).filter((k) => !seen.has(k));
363
+ // 同一会话目录可能先后存在 v2/v3 两个文件名: 旧键从 state 消失不代表会话消失。
364
+ // 只要本轮仍有文件映射到同一 sid, 就不能按 sid 删文档 —— 否则会把刚灌入的新文档误删。
365
+ const liveSids = new Set(files.map((f) => asciiSessionId(f, root)));
366
+ const gone = Object.keys(cur).filter((k) => !seen.has(k) && !liveSids.has(asciiSessionId(k, root)));
356
367
  goneCount = gone.length;
357
368
  for (const g of gone) {
358
369
  if (opts.dry) continue;
package/index.js CHANGED
@@ -128,6 +128,11 @@ const sessionHeader = (file) => {
128
128
  return o && o.type === "session" ? o : null;
129
129
  } catch { return null; }
130
130
  };
131
+ /** 会话目录内的实际数据文件: 优先 v3 新格式(活跃写入), 回退 v2 旧格式 */
132
+ const sessionDataFile = (dir) => {
133
+ const v3 = join(dir, "session.v3.jsonl.zstd");
134
+ return existsSync(v3) ? v3 : join(dir, "session.jsonl.zstd");
135
+ };
131
136
  /** 枚举所有会话目录: 顶层(目录名=id)与子代理候选 */
132
137
  function listSessionDirs(root) {
133
138
  const out = [];
@@ -140,7 +145,7 @@ function listSessionDirs(root) {
140
145
  for (const nm of names) {
141
146
  const d = join(wsp, nm);
142
147
  let st = null; try { st = statSync(d); } catch { continue; }
143
- if (!st.isDirectory() || !existsSync(join(d, "session.jsonl.zstd"))) continue;
148
+ if (!st.isDirectory() || !existsSync(sessionDataFile(d))) continue;
144
149
  out.push({ dir: d, id: nm });
145
150
  }
146
151
  }
@@ -161,7 +166,7 @@ function planCleanup(cfgRoot, minDays, apply) {
161
166
  const childParent = new Map(); // dir → parentSession
162
167
  for (const d of dirs) {
163
168
  if (d.id.startsWith("session-")) continue;
164
- const h = sessionHeader(join(d.dir, "session.jsonl.zstd"));
169
+ const h = sessionHeader(sessionDataFile(d.dir));
165
170
  if (h && typeof h.parentSession === "string") childParent.set(d.id, h.parentSession);
166
171
  }
167
172
  const aged = [];
@@ -230,7 +235,7 @@ function archivedSkipIds(root) {
230
235
  const childParent = new Map();
231
236
  for (const d of dirs) {
232
237
  if (d.id.startsWith("session-")) continue;
233
- const h = sessionHeader(join(d.dir, "session.jsonl.zstd"));
238
+ const h = sessionHeader(sessionDataFile(d.dir));
234
239
  if (h && typeof h.parentSession === "string") childParent.set(d.id, h.parentSession);
235
240
  }
236
241
  const out = new Set();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-meili-search",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "DSH 会话内容检索(Meilisearch): 增量索引同步(按 seq/时间戳) + 工作区自建内容搜索面板(每会话命中块/毛玻璃卡片/实时搜索) + 命中跳转定位与分词高亮 + Settings 配置卡",
5
5
  "type": "module",
6
6
  "main": "index.js",