dsh-meili-search 0.2.0 → 1.0.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 CHANGED
@@ -26,16 +26,27 @@
26
26
 
27
27
  ## 数据模型
28
28
 
29
- 索引 `dsh_sessions`(固定)的文档 = 每条 user/assistant 消息按 ≤4000 码元切块:
29
+ 索引 `dsh_sessions`(固定)的文档 = 消息/思考/工具调用按 ≤4000 码元切块:
30
30
 
31
31
  ```
32
- { id: "<sessionId>-<chunk>", sessionId, workspace, seq, mseq, role, text }
32
+ { id: "<sessionId>-<chunk>", sessionId, workspace, seq, mseq, role, subagent, text }
33
33
  ```
34
34
 
35
35
  - `mseq`: 消息在会话 zstd 日志中的顶层行 seq(客户端跳转定位用)
36
- - `role`: user | assistant
36
+ - `role`: `user` | `assistant` | `reasoning` | `tool`(四桶可配置, 见下)
37
+ - `subagent`: 该分块所属会话是否为 subagent 会话(委托深度>0 / origin=subagent / 无 `session-` 前缀目录)
37
38
  - 检索开启 `attributesToHighlight`(`«»` 标记)与 `showMatchesPosition`(`_matchesPosition` 为 UTF-8 字节偏移)
38
39
 
40
+ ### 索引范围(可配置)
41
+
42
+ - 默认只收 **user(用户输入) + assistant(agent 回复正文)**; `reasoning`(agent 思考块)与 `tool`(工具调用名+参数)为**备选桶**, 默认不收。
43
+ - 配置: `Settings → General` 或 `PUT /dsh-meili/v1/settings` 提交
44
+ `{"indexScope": ["user","assistant","reasoning","tool"], "includeSubagents": true}`;
45
+ 环境变量 `MEILI_SCOPE="user,assistant,reasoning,tool"`、`MEILI_INCLUDE_SUBAGENTS=false` 亦可。
46
+ - `includeSubagents` 默认 true(subagent 会话也纳入索引); false 则扫描时跳过。
47
+ - 修改范围/开关后会自动清空同步游标并全量重灌, 使旧角色文档退出索引。
48
+ - `role`/`subagent` 为 filterable, 可在检索时过滤(如排除思考、只查 subagent)。
49
+
39
50
  ## 同步机制
40
51
 
41
52
  ```
package/client.js CHANGED
@@ -14,7 +14,7 @@ window.__ModuleLoader__.load({
14
14
  var exports = module.exports;
15
15
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
16
16
  const React = require("react");
17
- const { useState, useEffect, useRef } = React;
17
+ const { useState, useEffect, useRef, useLayoutEffect } = React;
18
18
 
19
19
  const API = "/dsh-meili/v1/search";
20
20
  const INDEX = "dsh_sessions";
@@ -260,11 +260,11 @@ window.__ModuleLoader__.load({
260
260
  return s <= 0 ? `${n}${unit}前` : `${n}${unit}后`;
261
261
  }
262
262
  function settingsRow(label, hint, control) {
263
- return React.createElement("div", { style: { display: "flex", gap: 12, alignItems: "flex-start", padding: "10px 0", borderTop: "1px solid rgba(128,132,148,.18)" } },
264
- React.createElement("div", { style: { flex: "0 0 190px" } },
265
- React.createElement("div", { style: { fontWeight: 600 } }, label),
266
- hint ? React.createElement("div", { style: { color: "var(--dsh-text-secondary, #8a90a5)", fontSize: 12, marginTop: 2 } }, hint) : null),
267
- React.createElement("div", { style: { flex: 1, minWidth: 0 } }, control));
263
+ return React.createElement("div", { style: { display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: 24, padding: "16px 0", borderBottom: "1px solid var(--dsw-alias-border-l2, rgba(128,132,148,.18))" } },
264
+ React.createElement("div", { style: { flex: 1, minWidth: 0 } },
265
+ React.createElement("div", { style: { fontSize: 14, lineHeight: "22px", color: "var(--dsw-alias-label-primary)" } }, label),
266
+ hint ? React.createElement("div", { style: { color: "var(--dsh-text-secondary, #8a90a5)", fontSize: 12, lineHeight: "18px", marginTop: 4 } }, hint) : null),
267
+ React.createElement("div", { style: { flex: "none", minWidth: 0, maxWidth: "72%", paddingTop: 1 } }, control));
268
268
  }
269
269
  function MeiliSettingsCard() {
270
270
  const [cfg, setCfg] = useState(null);
@@ -273,10 +273,53 @@ window.__ModuleLoader__.load({
273
273
  const [keyText, setKeyText] = useState("");
274
274
  const keyDirty = useRef(false);
275
275
  const [stat, setStat] = useState(null);
276
+ const [hist, setHist] = useState([]); // 文档数历史 [[ts,docs],...]
276
277
  const [busy, setBusy] = useState(false);
277
278
  const [saved, setSaved] = useState(false);
278
279
  const [reidx, setReidx] = useState({ busy: false, msg: "" });
280
+ const [scope, setScope] = useState(null); // null=未加载; 数组=当前选中的 role 桶
281
+ const [subInc, setSubInc] = useState(true); // 是否包含 subagent 会话
282
+ const [scopeDirty, setScopeDirty] = useState(false);
283
+ const [scopeMsg, setScopeMsg] = useState("");
284
+ const [scopeTick, setScopeTick] = useState(0); // 每次范围变更 +1
285
+ const [throttleMs, setThrottleMs] = useState(250); // 批次间停顿(写入限速); 0=不限速
286
+ const [throttleText, setThrottleText] = useState("250"); // 数字框可编辑文本
287
+ const [fx, setFx] = useState([]); // 方向性进出场瞬态动画层
288
+ const fxId = useRef(0);
289
+ const scopeMirror = useRef(["user", "assistant"]); // scope 的同步镜像, 供 toggle 计算方向
290
+ const lastScope = useRef(null);
291
+ const lastSub = useRef(true);
279
292
  const saveTimer = useRef(null);
293
+ const barRef = useRef(null);
294
+ const [barW, setBarW] = useState(0); // 轨道实际像素宽(决定格子/蓝块几何, 全数值化避免 calc 误差)
295
+ const labelW = useRef(null); // 各标签 13px 600 实测宽度: {user: px, ...}
296
+ useLayoutEffect(() => {
297
+ const el = barRef.current;
298
+ if (!el) return undefined;
299
+ const upd = () => setBarW(el.offsetWidth || 0);
300
+ upd();
301
+ const ro = new ResizeObserver(upd);
302
+ ro.observe(el);
303
+ return () => ro.disconnect();
304
+ }, []);
305
+ useEffect(() => {
306
+ try {
307
+ const c = document.createElement("canvas");
308
+ const ctx = c.getContext("2d");
309
+ const m = {};
310
+ SCOPE_ITEMS.forEach((it) => { ctx.font = "600 13px ui-sans-serif, system-ui, sans-serif"; m[it.k] = Math.ceil(ctx.measureText(it.label).width); });
311
+ labelW.current = m;
312
+ } catch { labelW.current = {}; }
313
+ // 触发一次重绘使用实测宽度
314
+ setBarW((w) => w || 336);
315
+ }, []);
316
+ const SCOPE_ITEMS = [
317
+ { k: "user", label: "user" },
318
+ { k: "assistant", label: "assistant" },
319
+ { k: "reasoning", label: "reasoning" },
320
+ { k: "tool", label: "tool" },
321
+ ];
322
+ const GAP = 25; // 角色间距 = 3.5 × 字母 a 宽(12px 字号 a≈7px)
280
323
 
281
324
  useEffect(() => {
282
325
  (async () => {
@@ -287,16 +330,26 @@ window.__ModuleLoader__.load({
287
330
  setCfg(b.settings);
288
331
  setAutoText(String(b.settings.autoSyncMs));
289
332
  setUrlText(b.settings.url || "");
333
+ if (Number.isFinite(b.settings.throttleMs)) { setThrottleMs(b.settings.throttleMs); setThrottleText(String(b.settings.throttleMs)); }
334
+ if (Array.isArray(b.settings.indexScope)) { setScope(b.settings.indexScope.slice()); scopeMirror.current = b.settings.indexScope.slice(); }
335
+ if (typeof b.settings.includeSubagents === "boolean") { setSubInc(b.settings.includeSubagents); lastSub.current = b.settings.includeSubagents; }
290
336
  }
291
337
  } catch {}
292
338
  })();
293
- const t = setInterval(async () => {
339
+ const poll = async () => {
294
340
  try {
295
341
  const r = await fetch(STATUS_API);
296
342
  const b = await r.json();
297
343
  if (b.ok) setStat(b);
298
344
  } catch {}
299
- }, 3000);
345
+ try {
346
+ const r2 = await fetch("/dsh-meili/v1/docs-history");
347
+ const b2 = await r2.json();
348
+ if (b2.ok && Array.isArray(b2.points)) setHist(b2.points);
349
+ } catch {}
350
+ };
351
+ poll(); // 挂载立即拉一次, 避免首帧长时间处于“…”占位
352
+ const t = setInterval(poll, 3000);
300
353
  return () => { clearInterval(t); clearTimeout(saveTimer.current); };
301
354
  }, []);
302
355
 
@@ -336,9 +389,89 @@ window.__ModuleLoader__.load({
336
389
  } catch (e) { setReidx({ busy: false, msg: "失败: " + String((e && e.message) || e) }); }
337
390
  };
338
391
 
392
+ const toggleScope = (k) => {
393
+ const prev = scopeMirror.current; // 点击前的真实选中集
394
+ const next = prev.slice();
395
+ const i = next.indexOf(k);
396
+ const added = i < 0;
397
+ if (added) next.push(k); else if (next.length > 1) next.splice(i, 1); // 最少保留一格
398
+ lastScope.current = prev;
399
+ scopeMirror.current = next;
400
+ setScope(next);
401
+ setScopeDirty(true);
402
+ setScopeMsg("");
403
+ setScopeTick((t) => t + 1);
404
+ // 方向性进出场: 邻左→从左延伸/收缩; 邻右→从右; 两侧→两边向中间; 孤立→淡入淡出
405
+ const idx = SCOPE_ITEMS.findIndex((it) => it.k === k);
406
+ const hasL = (arr) => idx > 0 && arr.includes(SCOPE_ITEMS[idx - 1].k);
407
+ const hasR = (arr) => idx < SCOPE_ITEMS.length - 1 && arr.includes(SCOPE_ITEMS[idx + 1].k);
408
+ const l = hasL(next), r = hasR(next);
409
+ const dir = (!l && !r) ? "I" : (l && r ? "B" : (l ? "L" : "R"));
410
+ const id = ++fxId.current;
411
+ setFx((f) => [...f, { id, idx, mode: added ? "in" : "out", dir }]);
412
+ setTimeout(() => setFx((f) => f.filter((x) => x.id !== id)), 380);
413
+ };
414
+ const saveScope = async () => {
415
+ const cur = (scope && scope.length) ? scope : ["user", "assistant"];
416
+ setBusy(true);
417
+ try {
418
+ const r = await fetch(SETTINGS_API, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ indexScope: cur, includeSubagents: subInc }) });
419
+ const b = await r.json();
420
+ if (b.ok && b.settings) {
421
+ setCfg(b.settings);
422
+ if (Array.isArray(b.settings.indexScope)) { setScope(b.settings.indexScope.slice()); scopeMirror.current = b.settings.indexScope.slice(); }
423
+ if (typeof b.settings.includeSubagents === "boolean") setSubInc(b.settings.includeSubagents);
424
+ setScopeDirty(false);
425
+ setScopeMsg("已自动保存,10 秒无操作后更新索引");
426
+ } else setScopeMsg("保存失败: " + (b && b.error || r.status));
427
+ } catch (e) { setScopeMsg("保存失败: " + String((e && e.message) || e)); }
428
+ finally { setBusy(false); }
429
+ };
430
+ // 自动保存: 范围/subagent 变化后 600ms 防抖 PUT(索引变更由服务端按角色差集 + 10s 防抖执行)
431
+ const scopeSaveTimer = useRef(null);
432
+ useEffect(() => {
433
+ if (scopeDirty && scope !== null) {
434
+ if (scopeSaveTimer.current) clearTimeout(scopeSaveTimer.current);
435
+ scopeSaveTimer.current = setTimeout(() => saveScope(), 600);
436
+ setScopeMsg("将自动保存…");
437
+ }
438
+ return () => { if (scopeSaveTimer.current) clearTimeout(scopeSaveTimer.current); };
439
+ }, [scope, subInc, scopeDirty]);
440
+
441
+ // ---- 写入限速滑杆: 批次间停顿 ms(p)=2250*(1-p)^2 → p=2/3 处恰 250ms(本机推荐); 右端 0ms=不限速 ----
442
+ const THROTTLE_MAX = 2250;
443
+ const msFromP = (p) => Math.round(THROTTLE_MAX * Math.pow(1 - Math.min(1, Math.max(0, p)), 2));
444
+ const pFromMs = (ms) => 1 - Math.sqrt(Math.min(THROTTLE_MAX, Math.max(0, ms)) / THROTTLE_MAX);
445
+ const saveThrottle = async (v) => {
446
+ try {
447
+ await fetch(SETTINGS_API, { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ throttleMs: v }) });
448
+ } catch {}
449
+ };
450
+ const throttleTimer = useRef(null);
451
+ const applyThrottle = (ms) => {
452
+ const v = Math.max(0, Math.min(THROTTLE_MAX, Math.round(ms)));
453
+ setThrottleMs(v);
454
+ setThrottleText(String(v));
455
+ if (throttleTimer.current) clearTimeout(throttleTimer.current);
456
+ throttleTimer.current = setTimeout(() => saveThrottle(v), 800); // 停手 800ms 后自动保存
457
+ };
458
+ const onThrottleText = (raw) => {
459
+ setThrottleText(raw);
460
+ if (raw === "" || !Number.isFinite(Number(raw))) return; // 编辑中/非法: 等失焦
461
+ applyThrottle(Number(raw));
462
+ };
463
+ const commitThrottleText = () => {
464
+ const n = Number(throttleText);
465
+ applyThrottle(Number.isFinite(n) ? n : throttleMs); // 失焦时提交或回退
466
+ };
467
+ useEffect(() => () => { if (throttleTimer.current) clearTimeout(throttleTimer.current); }, []);
468
+
339
469
  const idx = (cfg && cfg.index) || INDEX;
340
470
  const docs = stat && stat.indexes ? stat.indexes[idx] : undefined;
341
- const meiliOk = typeof docs === "number" && docs >= 0;
471
+ const loading = stat === null; // 首帧/加载中: 未拿到 /status 前不渲染“失败”
472
+ const meiliOk = !loading && typeof docs === "number" && docs >= 0;
473
+ const docsView = loading ? "…" : (typeof docs === "number" ? (docs >= 0 ? String(docs) : "连接失败") : "—");
474
+ const docsColor = meiliOk || loading ? "inherit" : "var(--dsh-state-error-primary, #e5534b)";
342
475
 
343
476
  const autoControl = React.createElement("div", null, [
344
477
  React.createElement("div", { key: "row", style: { display: "flex", gap: 8, alignItems: "center", flexWrap: "wrap" } },
@@ -347,13 +480,49 @@ window.__ModuleLoader__.load({
347
480
  React.createElement("div", { key: "hint", style: { color: "var(--dsh-text-secondary, #8a90a5)", fontSize: 12, marginTop: 4 } },
348
481
  busy ? "保存中…" : (saved ? "已保存 ✓" : "增量同步周期(ms); 0 = 关闭(仅手动)。改动约 0.5s 后自动保存并热生效。")),
349
482
  ]);
350
- const statusControl = React.createElement("div", { style: { ...monoStyle, color: "var(--dsh-text-secondary, #8a90a5)", display: "grid", gridTemplateColumns: "auto 1fr", gap: "2px 14px" } }, [
351
- React.createElement("span", { key: "a" }, "文档数"), React.createElement("span", { key: "b", style: { color: meiliOk ? "inherit" : "var(--dsh-state-error-primary, #e5534b)" } }, meiliOk ? String(docs) : "连接失败"),
352
- React.createElement("span", { key: "c" }, "会话游标"), React.createElement("span", { key: "d" }, stat ? String(stat.stateFiles) : "…"),
353
- React.createElement("span", { key: "e" }, "上次同步"), React.createElement("span", { key: "f" }, fmtWhen(stat && stat.lastSync)),
354
- React.createElement("span", { key: "g" }, "下次同步"), React.createElement("span", { key: "h" }, stat ? (stat.autoSyncMs > 0 ? fmtWhen(stat.nextRunAt) : "已关闭") : "…"),
355
- React.createElement("span", { key: "i" }, "同步状态"), React.createElement("span", { key: "j" }, stat ? (stat.syncing ? "同步中…" : (meiliOk ? "正常" : "Meili 不可达")) : "…"),
356
- ]);
483
+ const rate = stat && stat.lastRound ? stat.lastRound : null;
484
+ const rateColor = !rate ? "var(--dsh-text-secondary, #8a90a5)" : (rate.docsPerSec > 50 ? "var(--dsh-state-success-primary, #4bc17c)" : (rate.docsPerSec > 0 ? "var(--dsh-state-warn-label, #d9a406)" : "var(--dsh-text-secondary, #8a90a5)"));
485
+ const rateView = loading ? "…" : rate ? `${rate.docsPerSec} docs/s · +${rate.added} (${rate.kind === "delta" ? "回填" : "同步"}, ${Math.round(rate.seconds)}s)` : "—";
486
+ const metricRows = [
487
+ ["文档数", React.createElement("span", { key: "v", style: { color: docsColor, fontWeight: 600 } }, docsView)],
488
+ ["会话游标", React.createElement("span", { key: "v" }, loading ? "…" : (stat ? String(stat.stateFiles) : "—"))],
489
+ ["最近一轮", React.createElement("span", { key: "v", style: { color: rateColor, fontWeight: rate ? 600 : 400 } }, rateView)],
490
+ ["上次同步", React.createElement("span", { key: "v" }, loading ? "…" : fmtWhen(stat && stat.lastSync))],
491
+ ["下次同步", React.createElement("span", { key: "v" }, stat ? (stat.autoSyncMs > 0 ? fmtWhen(stat.nextRunAt) : "已关闭") : (loading ? "…" : "—"))],
492
+ ["同步状态", React.createElement("span", { key: "v", style: { color: stat && stat.syncing ? "var(--dsh-state-warn-label, #d9a406)" : "var(--dsh-state-success-primary, #4bc17c)" } }, loading ? "…" : (stat ? (stat.syncing ? "同步中…" : (meiliOk ? "正常" : "Meili 不可达")) : "—"))],
493
+ ["限速", React.createElement("span", { key: "v" }, stat ? `${stat.throttleMs ?? 250} ms/批 ${stat.addBatch ?? 200}` : (loading ? "…" : "—"))],
494
+ ];
495
+ const statusControl = React.createElement("div", { style: { display: "flex", flexDirection: "column", justifyContent: "space-between", width: 180, height: 155, flex: "none" } }, metricRows.map(([, node], i) =>
496
+ React.createElement("div", { key: i, style: { ...monoStyle, fontSize: 12, overflow: "hidden" } },
497
+ React.createElement("span", { style: { float: "left", width: 62, color: "var(--dsh-text-secondary, #8a90a5)", whiteSpace: "nowrap" } }, metricRows[i][0]),
498
+ React.createElement("div", { style: { display: "block", textAlign: "right", color: "var(--dsh-text-secondary, #8a90a5)" } }, node))));
499
+ // ---- 文档数趋势曲线(设计器参数: H155/线2.5/alpha.12/放缩0.19/fs12/圆角/千分位; 当前值右上, 初始值左下) ----
500
+ const histControl = (() => {
501
+ if (!hist || hist.length < 2) {
502
+ return React.createElement("div", { style: { ...monoStyle, color: "var(--dsh-text-secondary, #8a90a5)", fontSize: 12, height: 155, display: "flex", alignItems: "center" } }, "采集文档数趋势中…(≥2 个采样点后显示)");
503
+ }
504
+ const vals = hist.map((p) => Number(p[1]));
505
+ const W = 560, H = 155, px = 8, pt = 8, pb = 2, expand = 0.19;
506
+ let lo = Math.min(...vals), hi = Math.max(...vals), mid = (lo + hi) / 2;
507
+ if (hi - lo < Math.max(1, mid * expand)) { lo = mid - (mid * expand) / 2; hi = mid + (mid * expand) / 2; }
508
+ const sp = Math.max(1, hi - lo);
509
+ const x = (i) => px + (W - 2 * px) * (i / (vals.length - 1));
510
+ const y = (v) => (H - pb) - (H - pb - pt) * ((v - lo) / sp);
511
+ const line = vals.map((v, i) => `${x(i).toFixed(1)},${y(v).toFixed(1)}`).join(" ");
512
+ const area = `${x(0).toFixed(1)},${H - pb} ${line} ${x(vals.length - 1).toFixed(1)},${H - pb}`;
513
+ const fmtV = (n) => n.toLocaleString();
514
+ return React.createElement("div", { style: { position: "relative", flex: 1, minWidth: 0, height: 155 } }, [
515
+ React.createElement("svg", { key: "s", viewBox: `0 0 ${W} ${H}`, preserveAspectRatio: "none", style: { position: "absolute", inset: 0, width: "100%", height: "100%", display: "block" }, role: "img", "aria-label": "文档数趋势" },
516
+ React.createElement("defs", null,
517
+ React.createElement("linearGradient", { id: "dshMeiliHistFill", x1: 0, y1: 0, x2: 0, y2: 1 },
518
+ React.createElement("stop", { offset: "0%", stopColor: "#1f6feb", stopOpacity: 0.12 }),
519
+ React.createElement("stop", { offset: "100%", stopColor: "#1f6feb", stopOpacity: 0 }))),
520
+ React.createElement("polygon", { points: area, fill: "url(#dshMeiliHistFill)" }),
521
+ React.createElement("polyline", { points: line, fill: "none", stroke: "#1f6feb", strokeWidth: 2.5, strokeLinejoin: "round", strokeLinecap: "round" })),
522
+ React.createElement("div", { key: "cur", style: { position: "absolute", top: 0, right: 2, font: "600 12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace", color: "var(--dsw-alias-label-primary)" } }, fmtV(vals[vals.length - 1])),
523
+ React.createElement("div", { key: "ini", style: { position: "absolute", left: 2, bottom: 0, font: "400 12px/1.5 ui-monospace,SFMono-Regular,Menlo,monospace", color: "var(--dsh-text-secondary, #8a90a5)" } }, fmtV(vals[0])),
524
+ ]);
525
+ })();
357
526
  const actionControl = React.createElement("div", null, [
358
527
  React.createElement("div", { key: "b", style: { display: "flex", gap: 8, alignItems: "center" } },
359
528
  React.createElement("button", { type: "button", disabled: reidx.busy, onClick: runReindex, style: { ...btnStyle, background: reidx.busy ? "rgba(128,132,148,.4)" : "#1f6feb" } }, reidx.busy ? "同步中…" : "立即同步一轮"),
@@ -361,6 +530,175 @@ window.__ModuleLoader__.load({
361
530
  React.createElement("div", { key: "t", style: { color: "var(--dsh-text-secondary, #8a90a5)", fontSize: 12, marginTop: 4 } }, "等于服务端增量同步(按会话日志 seq/时间戳追加新尾部)"),
362
531
  ]);
363
532
  const keyControl = React.createElement("input", { type: "password", value: keyText, placeholder: cfg && cfg.hasKey ? "已设置(留空并失焦保存可清除)" : "无(未设置)", onChange: (e) => onKeyChange(e.target.value), onBlur: onKeyBlur, style: { ...inpStyle, width: 280 }, spellCheck: false });
533
+ // ---- 索引范围分段选择(数值像素几何 + 覆盖层连体 + 文字等距内边距) ----
534
+ // 轨道实测像素宽 bw → 格宽 = (bw - 3*GAP)/4; 蓝块按连段合并跨间距;
535
+ // 连段内每段宽度 = 文字实测宽 + 2*等宽内边距(pad) → 每个文字左右到蓝块边框距离完全相等。
536
+ const selScope = () => (scope && scope.length) ? scope : ["user", "assistant"];
537
+ const bw = Math.max(barW || 0, 336);
538
+ const cellWpx = (bw - 3 * GAP) / 4;
539
+ const cellW = `${cellWpx}px`;
540
+ const leftPx = (i) => i * (cellWpx + GAP);
541
+ const leftCss = (i) => `${leftPx(i)}px`;
542
+ const widthCss = (len) => `${len * cellWpx + (len - 1) * GAP}px`;
543
+ const textW = (k) => (labelW.current && labelW.current[k]) || (k.length * 7.6);
544
+ // 连段内等边距分配: 每段 = 文字宽 + 2*pad; pad 由剩余空间均分, 最小 8px
545
+ const runPad = (s, len) => {
546
+ const items = SCOPE_ITEMS.slice(s, s + len);
547
+ const sumW = items.reduce((a, it) => a + textW(it.k), 0);
548
+ const runWpx = len * cellWpx + (len - 1) * GAP;
549
+ return Math.max(8, (runWpx - sumW) / (2 * len));
550
+ };
551
+ const scopeRuns = (() => {
552
+ const sc = selScope();
553
+ const runs = [];
554
+ let cur = null;
555
+ SCOPE_ITEMS.forEach((it, i) => {
556
+ const on = sc.includes(it.k);
557
+ if (on && !cur) cur = { s: i };
558
+ if (on) cur = { s: cur.s, e: i };
559
+ if (!on && cur) { runs.push(cur); cur = null; }
560
+ });
561
+ if (cur) runs.push(cur);
562
+ return runs;
563
+ })();
564
+ // 标签层: 永远固定在各自格心(文字不固定问题根治; 无 left/width 过渡)
565
+ const scopeCells = SCOPE_ITEMS.map((it, i) => {
566
+ const sel = selScope().includes(it.k);
567
+ return React.createElement("div", {
568
+ key: it.k,
569
+ onClick: () => toggleScope(it.k),
570
+ style: {
571
+ position: "absolute",
572
+ top: 0,
573
+ height: 40,
574
+ lineHeight: "40px",
575
+ textAlign: "center",
576
+ whiteSpace: "nowrap",
577
+ left: leftPx(i),
578
+ width: cellWpx,
579
+ zIndex: 1,
580
+ color: sel ? "#fff" : "var(--dsh-text-secondary, #8a90a5)",
581
+ fontWeight: sel ? 600 : 400,
582
+ fontSize: sel ? 13 : 12.5,
583
+ cursor: "pointer",
584
+ userSelect: "none",
585
+ transition: "color .25s ease",
586
+ },
587
+ title: it.k === "user" ? "用户输入" : it.k === "assistant" ? "agent 回复" : it.k === "reasoning" ? "agent 思考" : "工具调用",
588
+ }, it.label);
589
+ });
590
+ // 覆盖层: 每选中格一个独立蓝块(文字在块内左右对称=边距对齐) + 相邻选中在格间补"桥"
591
+ // → 视觉跨间距连体, 但不侵占文字两侧边距, 文字位置零扰动
592
+ const selIdx = selScope()
593
+ .map((k) => SCOPE_ITEMS.findIndex((it) => it.k === k))
594
+ .filter((i) => i >= 0)
595
+ .sort((a, b) => a - b);
596
+ const scopeOverlay = [];
597
+ selIdx.forEach((i, p) => {
598
+ const adjL = p > 0 && selIdx[p - 1] === i - 1;
599
+ const adjR = p < selIdx.length - 1 && selIdx[p + 1] === i + 1;
600
+ scopeOverlay.push(React.createElement("div", {
601
+ key: `cell-${i}`,
602
+ style: {
603
+ position: "absolute", top: 0, bottom: 0, left: leftPx(i), width: cellWpx,
604
+ borderTopLeftRadius: adjL ? 0 : 12,
605
+ borderBottomLeftRadius: adjL ? 0 : 12,
606
+ borderTopRightRadius: adjR ? 0 : 12,
607
+ borderBottomRightRadius: adjR ? 0 : 12,
608
+ background: "#1f6feb", zIndex: 0, pointerEvents: "none",
609
+ },
610
+ }));
611
+ if (adjR) scopeOverlay.push(React.createElement("div", {
612
+ key: `bridge-${i}`,
613
+ style: {
614
+ position: "absolute", top: 0, bottom: 0, left: leftPx(i) + cellWpx, width: GAP,
615
+ background: "#1f6feb", zIndex: 0, pointerEvents: "none",
616
+ },
617
+ }));
618
+ });
619
+ // 方向性进出场瞬态层: 覆盖在刚点的那一格上, 播放完消失(底色与运行态一致, 无缝衔接)
620
+ const fxLayer = fx.map((f) => {
621
+ const base = { position: "absolute", top: 0, bottom: 0, left: leftCss(f.idx), width: cellW, borderRadius: 12, pointerEvents: "none", overflow: "hidden" };
622
+ if (f.dir === "B") {
623
+ const half = { position: "absolute", top: 0, bottom: 0, width: "50%", background: "#1f6feb" };
624
+ const anim = f.mode === "in" ? "dshMeiliGrow" : "dshMeiliShrink";
625
+ return React.createElement("div", { key: f.id, style: base }, [
626
+ React.createElement("div", { key: "l", style: { ...half, left: 0, animation: `${anim} .3s ease forwards`, transformOrigin: "left center" } }),
627
+ React.createElement("div", { key: "r", style: { ...half, right: 0, animation: `${anim} .3s ease forwards`, transformOrigin: "right center" } }),
628
+ ]);
629
+ }
630
+ const blue = { background: "#1f6feb" };
631
+ if (f.dir === "I") {
632
+ return React.createElement("div", { key: f.id, style: { ...base, ...blue, animation: (f.mode === "in" ? "dshMeiliFadeIn" : "dshMeiliOut") + " .3s ease forwards" } });
633
+ }
634
+ return React.createElement("div", {
635
+ key: f.id,
636
+ style: {
637
+ ...base, ...blue,
638
+ animation: (f.mode === "in" ? "dshMeiliGrow" : "dshMeiliShrink") + " .3s ease forwards",
639
+ transformOrigin: (f.mode === "in") ? (f.dir === "L" ? "left center" : "right center") : (f.dir === "L" ? "right center" : "left center"),
640
+ },
641
+ });
642
+ });
643
+ const scopeControl = React.createElement("div", null, [
644
+ React.createElement("div", { key: "bar", ref: barRef, style: { position: "relative", height: 40, minWidth: 336, border: "1px solid rgba(128,132,148,.35)", borderRadius: 12, overflow: "hidden", background: "rgba(12,14,20,.5)" } },
645
+ scopeCells,
646
+ scopeOverlay,
647
+ fxLayer),
648
+ React.createElement("div", { key: "foot", style: { display: "flex", alignItems: "center", gap: 10, marginTop: 6, minHeight: 22 } },
649
+ React.createElement("span", { key: "msg", style: { color: "var(--dsh-text-secondary,#8a90a5)", fontSize: 11.5 } }, scopeMsg || (scopeDirty ? "自动保存中…" : `当前: ${selScope().join(" + ")}`)),
650
+ ),
651
+ ]);
652
+ // subagent 包含开关(同款固定坐标+覆盖层; 选中态由 subInc 决定)
653
+ const subCells = [true, false].map((v, i) => React.createElement("div", {
654
+ key: i,
655
+ onClick: () => { lastSub.current = subInc; setSubInc(v); setScopeDirty(true); setScopeMsg(""); setScopeTick((t) => t + 1); },
656
+ style: {
657
+ width: "50%",
658
+ height: 40,
659
+ lineHeight: "40px",
660
+ textAlign: "center",
661
+ position: "relative",
662
+ zIndex: 1,
663
+ color: subInc === v ? "#fff" : "var(--dsh-text-secondary, #8a90a5)",
664
+ fontWeight: subInc === v ? 600 : 400,
665
+ fontSize: subInc === v ? 13 : 12.5,
666
+ cursor: "pointer",
667
+ userSelect: "none",
668
+ transition: "color .25s ease",
669
+ },
670
+ }, v ? "包含" : "排除"));
671
+ const subOverlay = React.createElement("div", {
672
+ key: `sub-${subInc}`,
673
+ style: {
674
+ position: "absolute",
675
+ top: 0,
676
+ bottom: 0,
677
+ left: subInc ? 0 : "50%",
678
+ width: "50%",
679
+ borderRadius: 12,
680
+ background: "#1f6feb",
681
+ zIndex: 0,
682
+ pointerEvents: "none",
683
+ // 开关: 直接跳边, 不做位移动画
684
+ },
685
+ });
686
+ const subControl = React.createElement("div", { style: { position: "relative", height: 40, width: 186, border: "1px solid rgba(128,132,148,.35)", borderRadius: 12, overflow: "hidden", background: "rgba(12,14,20,.5)" } },
687
+ React.createElement("div", { style: { display: "flex", height: 40 } }, subCells),
688
+ subOverlay);
689
+ // 写入限速滑杆(白块内嵌渐变条; 数字框在左; 释放自动保存)
690
+ const throttleControl = React.createElement("div", { style: { display: "flex", alignItems: "center", gap: 12 } }, [
691
+ React.createElement("input", { key: "v", type: "number", min: 0, max: THROTTLE_MAX, step: 10, value: throttleText, onChange: (e) => onThrottleText(e.target.value), onBlur: commitThrottleText, style: { width: 88, font: "700 15px/1.4 ui-monospace,SFMono-Regular,Menlo,monospace", color: "var(--dsw-alias-label-primary)", background: "transparent", border: "1px solid rgba(128,132,148,.35)", borderRadius: 8, padding: "5px 9px", textAlign: "center", outline: "none" }, "aria-label": "写入限速批次间隔 ms" }),
692
+ React.createElement("div", { key: "bar", style: { position: "relative", flex: 1, minWidth: 260 } }, [
693
+ React.createElement("input", { key: "i", type: "range", className: "dshMeiliThrottle", min: 0, max: 1000, step: 1, value: Math.round(pFromMs(throttleMs) * 1000), onChange: (e) => applyThrottle(msFromP(e.target.value / 1000)), "aria-label": "写入限速批次间隔 ms" }),
694
+ React.createElement("div", { key: "l", style: { position: "absolute", left: 10, top: "50%", transform: "translateY(-50%)", fontSize: 10, color: "rgba(255,255,255,.92)", pointerEvents: "none" } }, "最慢"),
695
+ React.createElement("div", { key: "r", style: { position: "absolute", right: 10, top: "50%", transform: "translateY(-50%)", fontSize: 10, color: "rgba(255,255,255,.92)", pointerEvents: "none" } }, "不限速"),
696
+ ]),
697
+ ]);
698
+
699
+ const statusBlock = React.createElement("div", { style: { borderTop: "1px solid var(--dsw-alias-border-l2, rgba(128,132,148,.18))", marginTop: 4, paddingTop: 16 } },
700
+ React.createElement("div", { style: { fontWeight: 700, fontSize: 14, marginBottom: 10 } }, "实时状态"),
701
+ React.createElement("div", { style: { display: "flex", flexDirection: "row", gap: 16, alignItems: "flex-start" } }, [histControl, statusControl]));
364
702
 
365
703
  return React.createElement("div", { style: { padding: "2px 0 6px" } },
366
704
  React.createElement("div", { style: { display: "flex", alignItems: "baseline", justifyContent: "space-between", marginBottom: 2 } },
@@ -371,12 +709,42 @@ window.__ModuleLoader__.load({
371
709
  settingsRow("Meilisearch 地址", "http(s)://host:port; 失焦自动保存并立即生效", React.createElement("input", { type: "text", value: urlText, placeholder: "http://192.168.43.2:7700", onChange: (e) => setUrlText(e.target.value), onBlur: onUrlBlur, style: { ...inpStyle, width: 320 }, spellCheck: false })),
372
710
  settingsRow("Master Key", "访问 meili 的密钥(默认无); 留空失焦保存即清除", keyControl),
373
711
  settingsRow("索引 (只读)", "当前检索/同步使用的索引", React.createElement("code", { style: { ...monoStyle, color: "var(--dsh-text-secondary,#8a90a5)" } }, idx)),
712
+ settingsRow("索引内容范围", "点击切换收录角色; 自动保存, 10 秒无操作后按角色增量更新索引(移除秒删/新增后台回填)", scopeControl),
713
+ settingsRow("包含 subagent 会话", "委托子代理会话是否纳入索引", subControl),
714
+ settingsRow("写入限速", "0ms=不限速; 2/3 处≈250ms; 拖动自动保存", throttleControl),
374
715
  settingsRow("手动同步", "立即触发一轮增量同步", actionControl),
375
- settingsRow("实时状态", "文档数 / 游标 / 上次与下次同步(自动刷新)", statusControl));
716
+ statusBlock);
376
717
  }
377
718
 
378
719
  function apply(ctx) {
379
720
  activeCtx = ctx;
721
+ if (!window.__dshMeiliCssInjected) {
722
+ window.__dshMeiliCssInjected = true;
723
+ const st = document.createElement("style");
724
+ st.textContent = `
725
+ .dshMeiliSeg { position: relative; }
726
+ .dshMeiliSegSel { animation: dshMeiliGrow .28s ease; }
727
+ .dshMeiliSegSelL { transform-origin: left center; }
728
+ .dshMeiliSegSelR { transform-origin: right center; }
729
+ .dshMeiliSegSelC { transform-origin: center; }
730
+ @keyframes dshMeiliGrow { from { transform: scaleX(.25); opacity: .2; } to { transform: scaleX(1); opacity: 1; } }
731
+ .dshMeiliSegFadeIn { animation: dshMeiliFadeIn .3s ease; }
732
+ @keyframes dshMeiliFadeIn { from { background-color: transparent; opacity: .15; transform: scale(.85); } to { background-color: #1f6feb; opacity: 1; transform: scale(1); } }
733
+ .dshMeiliSegFade { animation: dshMeiliFade .35s ease; }
734
+ @keyframes dshMeiliFade { from { background-color: #1f6feb; color: #fff; opacity: 1; } to { background-color: transparent; opacity: .35; } }
735
+ .dshMeiliSegFadeIn { animation: dshMeiliFadeIn .3s ease; }
736
+ @keyframes dshMeiliFadeIn { from { background-color: transparent; opacity: .15; transform: scale(.85); } to { background-color: #1f6feb; opacity: 1; transform: scale(1); } }
737
+ @keyframes dshMeiliShrink { from { transform: scaleX(1); opacity: 1; } to { transform: scaleX(0); opacity: .15; } }
738
+ @keyframes dshMeiliOut { from { opacity: 1; transform: scale(1); } to { opacity: 0; transform: scale(.9); } }
739
+ input.dshMeiliThrottle { -webkit-appearance: none; appearance: none; width: 100%; height: 30px; border-radius: 15px; margin: 0; cursor: pointer; background: transparent; }
740
+ input.dshMeiliThrottle::-webkit-slider-runnable-track { height: 30px; border-radius: 15px; background: linear-gradient(90deg, #22c55e 0%, #eab308 50%, #ef4444 100%); box-shadow: inset 0 1px 3px rgba(0,0,0,.45); }
741
+ input.dshMeiliThrottle::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 7px; background: #fff; margin-top: 5px; box-shadow: 0 1px 3px rgba(0,0,0,.4); cursor: pointer; }
742
+ input.dshMeiliThrottle::-moz-range-track { height: 30px; border-radius: 15px; background: linear-gradient(90deg, #22c55e 0%, #eab308 50%, #ef4444 100%); }
743
+ input.dshMeiliThrottle::-moz-range-thumb { width: 20px; height: 20px; border: 0; border-radius: 7px; background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,.4); cursor: pointer; }
744
+ input.dshMeiliThrottle:focus-visible { outline: 1px solid #1f6feb; outline-offset: 2px; }
745
+ `;
746
+ document.head.appendChild(st);
747
+ }
380
748
  const patch = (attempt) => {
381
749
  let rt = null;
382
750
  try { rt = ctx.sessions; } catch { rt = null; }
@@ -407,12 +775,14 @@ window.__ModuleLoader__.load({
407
775
  };
408
776
  patch(0);
409
777
  try {
410
- ctx.slots.inject("settings.general.item", () => ctx.slots.register({
411
- name: "settings.general.item",
412
- id: "dsh-meili-search-settings",
413
- order: 642,
778
+ // 独立设置小节: 注册到 settings.section → Settings 左侧导航出现「会话内容检索」项
779
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
780
+ name: "settings.section",
781
+ id: "dsh-meili-search",
782
+ order: 700,
783
+ label: "会话内容检索",
414
784
  }, MeiliSettingsCard));
415
- } catch (e) { console.warn("[dsh-meili] settings card 注册失败:", String((e && e.message) || e)); }
785
+ } catch (e) { console.warn("[dsh-meili] settings section 注册失败:", String((e && e.message) || e)); }
416
786
  }
417
787
  const inject = ["sessions", "slots"];
418
788
  exports.apply = apply;
package/cordis.patch.yml CHANGED
@@ -4,3 +4,7 @@
4
4
  config:
5
5
  autoSyncMs: 300000 # 增量定时同步周期(ms): 300000=5 分钟; 0=关闭(仅手动 /reindex)
6
6
  index: dsh_sessions # 正式索引
7
+ indexScope: # 索引范围(可经 /dsh-meili/v1/settings 热改):
8
+ - user # 用户输入
9
+ - assistant # agent 回复(默认含此两者; 加 reasoning=思考, tool=工具调用)
10
+ includeSubagents: true # false 则排除 subagent 会话