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 +5 -1
- package/README.md +6 -2
- package/client.js +32 -10
- package/core.mjs +13 -2
- package/index.js +8 -3
- package/package.json +1 -1
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`(
|
|
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] 在
|
|
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("
|
|
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("
|
|
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
|
-
|
|
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
|
-
/* 树顶部让出胶囊位置:
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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