dsh-lost-and-found 0.1.1 → 0.1.3

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
@@ -90,7 +90,9 @@ Requires DSH with the web profile and Node.js `^22` or `>=24`. Windows-focused
90
90
 
91
91
  ## 环境要求
92
92
 
93
- - DSH,使用 `web` profile
93
+ - DSH **0.1.7 或更高**,使用 `web` profile
94
+ (设置页依赖 0.1.7 才有的 `configForms` 客户端服务与 `Config` 导出契约。更早的 0.1.x 上
95
+ **host 功能仍然可用** —— 扫描、检索、工具调用都不受影响 —— 但设置页不会出现)
94
96
  - Node.js `^22` 或 `>=24`(用到内置 `node:sqlite`)
95
97
  - 无需 API key、无需联网;扫描与检索全部在本机完成
96
98
 
@@ -122,7 +124,7 @@ dsh plugin --profile web add link:/path/to/dsh-lost-and-found
122
124
  | --- | --- | --- |
123
125
  | `enabled` | `true` | 关闭后不再自动扫描,也不再提供找文件工具 |
124
126
  | `roots` | `[]` | 要监视的目录,每个目录带自己的 `policy` |
125
- | `dbPath` | 空 | 索引库位置;留空用 `%LOCALAPPDATA%\dsh-lost-and-found\index.db` |
127
+ | `dbPath` | 空 | 索引库位置;留空用 `%LOCALAPPDATA%\dsh-lost-and-found\index.db`。可以填**文件**路径,也可以直接填**文件夹**(会用该文件夹下的 `index.db`) |
126
128
  | `intervalDays` | `1` | 自动扫描间隔;`0` = 只手动 |
127
129
  | `firstScanMode` | `full` | 一个目录**第一次**被扫时收多久:`full` 全部历史 / `window` 最近 `firstRunWindowDays` 天 / `none` 只收今后新增 |
128
130
  | `firstRunWindowDays` | `7` | 仅当 `firstScanMode = window` 时生效 |
@@ -132,6 +134,18 @@ dsh plugin --profile web add link:/path/to/dsh-lost-and-found
132
134
  | `backupKeep` | `7` | 每日备份保留份数,`0` = 不备份 |
133
135
  | `extraExcludeDirs` / `extraExcludePatterns` | `[]` | 追加的排除目录名 / 路径片段(`re:` 前缀表示正则) |
134
136
 
137
+ ### 配置存在哪(DSH 0.1.7 起)
138
+
139
+ DSH 0.1.7 起,设置服务**不再替插件另存一份配置** —— 它只把 Loader 里本插件条目的
140
+ `Config` *投影*成表单,权威值始终在 profile 的补丁文件里:
141
+
142
+ - 设置页里改的值会写进 `$DSH_HOME/profiles/web/cordis.patch.yml` 中 `id: dsh-lost-and-found` 那段
143
+ - 想手改也行,直接编辑那个文件,效果与设置页完全一致
144
+ - **改完立刻生效,不需要重启 DSH**:0.1.7 移除了旧版的 `watch`,插件改为在**每次用到配置之前**
145
+ 重新读一遍投影(`settings.describe()`),所以面板里加完目录就能直接点「立即扫描」
146
+ - 从 0.1.2 升级上来的话注意:0.1.2 只在插件加载那一刻读一次配置,会表现为
147
+ 「面板里明明加了文件夹,点扫描却说还没设置」—— 0.1.3 修的就是这个
148
+
135
149
  ## 扫描目录的内容深度(每个目录可不同)
136
150
 
137
151
  - **完整**:记基本信息,并允许把图片交给模型看过之后写回描述(默认)
package/client.js CHANGED
@@ -45,7 +45,7 @@ window.__ModuleLoader__.load({
45
45
  policyMeta: "只记基本信息",
46
46
  storageTitle: "记录存放在哪",
47
47
  dbPath: "记忆库文件位置",
48
- dbPathHint: "留空使用默认位置(在系统盘的 AppData\\Local\\dsh-lost-and-found\\index.db)。想放到别的盘就填完整路径,例如 D:\\file-index\\index.db。",
48
+ dbPathHint: "留空使用默认位置(在系统盘的 AppData\\Local\\dsh-lost-and-found\\index.db)。想换地方可以填文件路径,例如 D:\\file-index\\index.db;填一个文件夹也行,会用该文件夹下的 index.db。",
49
49
  freqTitle: "多久看一次",
50
50
  interval: "自动扫描间隔",
51
51
  freqHint: "打开 DSH 时会检查:距离上次扫描超过这个间隔,就自动补扫一次。",
@@ -97,7 +97,7 @@ window.__ModuleLoader__.load({
97
97
  policyMeta: "Metadata only",
98
98
  storageTitle: "Where records are stored",
99
99
  dbPath: "Database file",
100
- dbPathHint: "Leave empty for the default location under AppData\\Local\\dsh-lost-and-found\\. Or give a full path such as D:\\file-index\\index.db.",
100
+ dbPathHint: "Leave empty for the default location under AppData\\Local\\dsh-lost-and-found\\. You can also give a file path such as D:\\file-index\\index.db, or just a folder \u2014 the index.db inside it will be used.",
101
101
  freqTitle: "How often to look",
102
102
  interval: "Automatic scan interval",
103
103
  freqHint: "When you open DSH it checks whether this interval has passed, and catches up if needed.",
@@ -323,7 +323,10 @@ window.__ModuleLoader__.load({
323
323
  ctx.effect(() => ctx.locale.register(NS, { zh, en }), "dsh-lost-and-found: dictionaries");
324
324
 
325
325
  const t = ctx.locale.bind(NS);
326
- const scope = ctx.settingsScope.bind({ namespace: SETTINGS_NAMESPACE });
326
+ // 0.1.7:`ctx.settingsScope` 已被整个移除,改为 `ctx.configForms.get(条目 id)`。
327
+ // 新 API 与旧 API 同名同义(getSnapshot / subscribe / set / unset / mutate),
328
+ // 快照结构 `{status,value,base,user,revision,writable,mode}` 也没变,所以组件体无需改动。
329
+ const scope = ctx.configForms.get(SETTINGS_NAMESPACE);
327
330
  // 客户端远程命名空间挂在 `remote.<namespace>` 服务上(DSH 0.1.5-rc.1 起;
328
331
  // 旧的 connection.api.* 已不存在,写它会报 reading 'sessions')。
329
332
  // 触发斜杠命令必须走 remote.commands.execute:往会话里塞一条 "/cmd" 文本
@@ -399,7 +402,7 @@ window.__ModuleLoader__.load({
399
402
 
400
403
  module.exports = {
401
404
  name: "dsh-lost-and-found",
402
- inject: ["slots", "locale", "settingsScope", "remote", "remote.commands", "sessions"],
405
+ inject: ["slots", "locale", "configForms", "remote", "remote.commands", "sessions"],
403
406
  apply,
404
407
  };
405
408
  return module.exports;
package/config.mjs CHANGED
@@ -7,9 +7,9 @@
7
7
  * 会把一份「运行快照」写到 ~/.dsh-lost-and-found/run.json。
8
8
  * - 用户数据目录只放运行快照/日志,索引库默认在 %LOCALAPPDATA%\dsh-lost-and-found\。
9
9
  */
10
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
10
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
11
11
  import { homedir } from "node:os";
12
- import { join } from "node:path";
12
+ import { basename, join } from "node:path";
13
13
 
14
14
  /** 每个扫描目录的内容深度策略 */
15
15
  export const POLICY = Object.freeze({
@@ -120,9 +120,28 @@ export function defaultDbPath() {
120
120
  return join(base, "dsh-lost-and-found", "index.db");
121
121
  }
122
122
 
123
+ /**
124
+ * 用户填的「记忆库位置」是不是一个**目录**(而不是库文件本身)?
125
+ * 实测坑(v0.1.3):那一格要的是文件路径,但很自然会填成文件夹(如 `D:\findtext`),
126
+ * 而 `new DatabaseSync("D:\findtext")` 会直接抛 `unable to open database file` —— 扫描整个失败。
127
+ */
128
+ export function isDirDbPath(p) {
129
+ if (/[\\/]$/.test(p)) return true;
130
+ try {
131
+ if (existsSync(p) && statSync(p).isDirectory()) return true;
132
+ } catch { /* 读不到就按文件名处理 */ }
133
+ const base = basename(p);
134
+ return base !== "" && !base.includes(".");
135
+ }
136
+
137
+ /**
138
+ * 把「记忆库位置」规整成一个**文件**路径:
139
+ * 空 → 默认位置;看着像目录(已存在的目录 / 以分隔符结尾 / 没有扩展名)→ 用该目录下的 index.db。
140
+ */
123
141
  export function resolveDbPath(config) {
124
142
  const p = (config && config.dbPath ? String(config.dbPath) : "").trim();
125
- return p || defaultDbPath();
143
+ if (!p) return defaultDbPath();
144
+ return isDirDbPath(p) ? join(p.replace(/[\\/]+$/, ""), "index.db") : p;
126
145
  }
127
146
 
128
147
  /** 规整扫描目录列表:去空、去重、补默认策略 */
package/index.mjs CHANGED
@@ -41,20 +41,30 @@ export const inject = ["tools", "commands"];
41
41
  const SETTINGS_NS = "dsh-lost-and-found";
42
42
  const AUTO_CHECK_THROTTLE_MS = 15 * 60 * 1000;
43
43
 
44
- const settingsSchema = z.object({
45
- enabled: z.boolean().default(true),
46
- dbPath: z.string().default(""),
47
- intervalDays: z.number().min(0).max(365).default(1),
48
- firstRunWindowDays: z.number().min(0).max(365).default(7),
44
+ /**
45
+ * 0.1.7 设置契约:插件用**纯具名导出**的 `Config` 声明可配置项,settings 服务照着它投影出表单。
46
+ * 两条硬要求:
47
+ * ① 不能有 `export default` —— 剥壳后会把 `Config` 一起丢掉,条目被静默过滤;
48
+ * ② 每个可写字段都要标 `.volatile()`(= 「能现场改、改完不用重挂载」),否则 `volatileForm()`
49
+ * 返回 undefined,整个条目被 `describe()` **静默过滤**(面板消失,且不报任何错)。
50
+ * `.volatile()` 需要 schemastery >= 3.18.4;下面降级包装保证在老版本上仍能正常加载。
51
+ */
52
+ const vol = (schema) => (typeof schema.volatile === "function" ? schema.volatile() : schema);
53
+
54
+ export const Config = z.object({
55
+ enabled: vol(z.boolean().default(true)),
56
+ dbPath: vol(z.string().default("")),
57
+ intervalDays: vol(z.number().min(0).max(365).default(1)),
58
+ firstRunWindowDays: vol(z.number().min(0).max(365).default(7)),
49
59
  /** 目录首次扫描收多久:full=全部历史(默认)/ window=最近 N 天 / none=只收今后新增 */
50
- firstScanMode: z.string().default("full"),
51
- maxFileMB: z.number().min(0).max(10240).default(50),
52
- patrolEnabled: z.boolean().default(true),
53
- imageQuotaPerRun: z.number().min(0).max(500).default(20),
54
- backupKeep: z.number().min(0).max(60).default(7),
55
- roots: z.array(z.object({ path: z.string(), policy: z.string().default("full") })).default([]),
56
- extraExcludeDirs: z.array(z.string()).default([]),
57
- extraExcludePatterns: z.array(z.string()).default([]),
60
+ firstScanMode: vol(z.string().default("full")),
61
+ maxFileMB: vol(z.number().min(0).max(10240).default(50)),
62
+ patrolEnabled: vol(z.boolean().default(true)),
63
+ imageQuotaPerRun: vol(z.number().min(0).max(500).default(20)),
64
+ backupKeep: vol(z.number().min(0).max(60).default(7)),
65
+ roots: vol(z.array(z.object({ path: z.string(), policy: z.string().default("full") })).default([])),
66
+ extraExcludeDirs: vol(z.array(z.string()).default([])),
67
+ extraExcludePatterns: vol(z.array(z.string()).default([])),
58
68
  });
59
69
 
60
70
  function textTool(definition) {
@@ -105,23 +115,48 @@ export function apply(ctx, input = {}) {
105
115
  let lastAutoCheck = 0;
106
116
  let childRunning = false;
107
117
 
108
- // ---------- 设置命名空间(Web 设置页读写;用户配置的唯一真相来源) ----------
109
- ctx.inject(["settings"], (settingsCtx) => {
118
+ // ---------- 设置读取(0.1.7 契约) ----------
119
+ // 旧写法 `settings.register(ns, schema, {base})` + `scope.get()` + `scope.watch()` 在 0.1.7 已废:
120
+ // settings 服务改为**投影** Loader 里本插件条目的 `Config`(命名空间 = patch 条目的 id,即
121
+ // `SETTINGS_NS`),不再维护独立的值,也不再提供 watch。所以读改走 `describe()`。
122
+ //
123
+ // ⚠️ 0.1.7 实测更正(v0.1.3):面板写入**不会**让宿主重新 apply 本插件 —— 写进去的值只落在
124
+ // profile 的 `cordis.patch.yml` 上,本进程内存里的 `liveConfig` 一直是加载那一刻的旧值。
125
+ // 后果不是「显示没跟上」这种小事,而是功能级故障:面板里刚加的扫描目录,点「立即扫描」会报
126
+ // 「还没设置要扫描的文件夹」;改了库位置也仍然读写旧库。所以下面所有**入口**都先过 `freshConfig()`。
127
+ let settingsService = null;
128
+
129
+ function syncFromSettings() {
130
+ if (!settingsService) return false;
110
131
  try {
111
- const scope = settingsCtx.settings.register(SETTINGS_NS, settingsSchema, { base: liveConfig });
112
- const resolved = scope.get();
113
- if (resolved) liveConfig = { ...liveConfig, ...resolved, roots: normalizeRoots(resolved.roots) };
114
- scope.watch((next) => {
115
- if (!next) return;
116
- liveConfig = { ...liveConfig, ...next, roots: normalizeRoots(next.roots) };
117
- });
132
+ const row = settingsService.describe().find((it) => it && it.ns === SETTINGS_NS);
133
+ if (!row || row.value === undefined || row.value === null) return false;
134
+ liveConfig = { ...liveConfig, ...row.value, roots: normalizeRoots(row.value.roots) };
135
+ return true;
118
136
  } catch (err) {
119
- ctx.logger.warn(`dsh-lost-and-found: 设置命名空间注册失败:${err.message}`);
137
+ ctx.logger.warn(`dsh-lost-and-found: 读取设置失败(继续用传入配置) - ${err.message}`);
138
+ return false;
120
139
  }
140
+ }
141
+
142
+ /**
143
+ * 取「当前最新」的配置:每次调用都重读一遍设置投影。
144
+ * 面板改完立刻生效,不需要重启 DSH(`describe()` 读的就是刚写入的权威值,纯内存操作,成本可忽略)。
145
+ * 所有对外入口(工具 / 斜杠命令 / 自动扫描检查)都必须先走这里,不要直接读 `liveConfig` 的闭包快照。
146
+ */
147
+ function freshConfig() {
148
+ syncFromSettings();
149
+ return liveConfig;
150
+ }
151
+
152
+ ctx.inject(["settings"], (settingsCtx) => {
153
+ settingsService = settingsCtx.settings;
154
+ freshConfig();
121
155
  });
122
156
 
123
157
  // ---------- 派生扫描子进程 ----------
124
158
  async function spawnScan({ trigger = "manual", wait = false, forceFull = false } = {}) {
159
+ freshConfig();
125
160
  if (childRunning && wait) throw new Error("已有扫描在进行中");
126
161
  if (!liveConfig.enabled) throw new Error("插件已关闭(可在设置页打开)");
127
162
  const roots = normalizeRoots(liveConfig.roots);
@@ -192,6 +227,7 @@ export function apply(ctx, input = {}) {
192
227
  // ---------- 到期自动扫描:会话一开就检查(用户选了「每天」) ----------
193
228
  function maybeAutoScan(reason) {
194
229
  try {
230
+ freshConfig();
195
231
  if (!liveConfig.enabled) return;
196
232
  const days = Number(liveConfig.intervalDays);
197
233
  if (!days || days <= 0) return;
@@ -241,7 +277,7 @@ export function apply(ctx, input = {}) {
241
277
  fallback: { type: "boolean", description: "索引没命中时是否实时扫描扫描目录兜底,默认 true" },
242
278
  },
243
279
  async execute(args = {}) {
244
- const cfg = liveConfig;
280
+ const cfg = freshConfig();
245
281
  const roots = normalizeRoots(cfg.roots);
246
282
  const opts = {
247
283
  keywords: args.keywords,
@@ -318,7 +354,7 @@ export function apply(ctx, input = {}) {
318
354
  full: { type: "boolean", description: "强制全量重扫(忽略已有锚点),默认 false" },
319
355
  },
320
356
  async execute(args = {}) {
321
- const cfg = liveConfig;
357
+ const cfg = freshConfig();
322
358
  if (!normalizeRoots(cfg.roots).length) {
323
359
  return "还没设置要扫描的文件夹。请在插件设置页点「一键扫描」或手动添加目录后再试。";
324
360
  }
@@ -351,7 +387,7 @@ export function apply(ctx, input = {}) {
351
387
  description: "查看「文件快速寻回」的概况:上次扫描时间、库内文件数、扫描目录、待处理项、野文件提示、库位置与错误。",
352
388
  parameters: {},
353
389
  async execute() {
354
- const cfg = liveConfig;
390
+ const cfg = freshConfig();
355
391
  const dbPath = resolveDbPath(cfg);
356
392
  const { db } = openLiveDb(cfg);
357
393
  try {
@@ -429,7 +465,8 @@ export function apply(ctx, input = {}) {
429
465
  async execute(args = {}) {
430
466
  const paths = (args.paths || []).map((x) => (typeof x === "string" ? x : x && x.path)).filter(Boolean);
431
467
  if (!paths.length) return "请提供要忘掉的路径";
432
- const { db } = openLiveDb(liveConfig);
468
+ const cfg = freshConfig();
469
+ const { db } = openLiveDb(cfg);
433
470
  try {
434
471
  const n = forgetPaths(db, paths);
435
472
  return `已从记忆库删除 ${n} 条记录(用户文件未被改动)。`;
@@ -445,7 +482,8 @@ export function apply(ctx, input = {}) {
445
482
  description: "手动检查一批已记录文件是否还在原处(用于清理「已经不在了」的记录)。",
446
483
  parameters: { limit: { type: "number", description: "本批检查条数,默认 500" } },
447
484
  async execute(args = {}) {
448
- const { db } = openLiveDb(liveConfig);
485
+ const cfg = freshConfig();
486
+ const { db } = openLiveDb(cfg);
449
487
  try {
450
488
  const r = verifyBatch(db, Number(args.limit) || 500);
451
489
  return `检查了 ${r.checked} 个文件,其中 ${r.missing} 个已不在原处(连续两次确认后才会标记)。`;
@@ -462,8 +500,9 @@ export function apply(ctx, input = {}) {
462
500
  "取出「还没看过内容」的图片清单(供模型用 read_image 逐张看图并写描述)。返回路径+名字+时间。",
463
501
  parameters: { limit: { type: "number", description: "本次取几张,默认用配置里的看图配额" } },
464
502
  async execute(args = {}) {
465
- const limit = Number(args.limit) || Number(liveConfig.imageQuotaPerRun) || 20;
466
- const { db } = openLiveDb(liveConfig);
503
+ const cfg = freshConfig();
504
+ const limit = Number(args.limit) || Number(cfg.imageQuotaPerRun) || 20;
505
+ const { db } = openLiveDb(cfg);
467
506
  try {
468
507
  const rows = db.prepare(`
469
508
  SELECT path, name, ext, size, appeared_ms, origin FROM files
@@ -495,7 +534,8 @@ export function apply(ctx, input = {}) {
495
534
  async execute(args = {}) {
496
535
  const p = String(args.path || "").trim();
497
536
  if (!p) return "请提供图片路径";
498
- const { db } = openLiveDb(liveConfig);
537
+ const cfg = freshConfig();
538
+ const { db } = openLiveDb(cfg);
499
539
  try {
500
540
  const r = db.prepare("UPDATE files SET image_desc = ?, tags = ?, enrich = 'described', last_seen_ms = ? WHERE path = ?")
501
541
  .run(String(args.description || ""), String(args.tags || ""), Date.now(), p);
@@ -514,7 +554,7 @@ export function apply(ctx, input = {}) {
514
554
  input: { hint: "扫描一次:新目录首次扫描会回填其历史文件,其余只收新增。", images: false },
515
555
  handler: async () => {
516
556
  try {
517
- const roots = normalizeRoots(liveConfig.roots);
557
+ const roots = normalizeRoots(freshConfig().roots);
518
558
  if (!roots.length) return { kind: "error", text: "还没设置要扫描的文件夹:请在插件设置页点「一键扫描」或手动添加目录。" };
519
559
  const r = await spawnScan({ trigger: "settings", wait: true });
520
560
  if (!r.result || r.result.ok === false) {
@@ -544,7 +584,7 @@ export function apply(ctx, input = {}) {
544
584
  input: { hint: "查看上次扫描时间、库内文件数、扫描目录。", images: false },
545
585
  handler: async () => {
546
586
  try {
547
- const cfg = liveConfig;
587
+ const cfg = freshConfig();
548
588
  const dbPath = resolveDbPath(cfg);
549
589
  const { db } = openLiveDb(cfg);
550
590
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-lost-and-found",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Local file recall for DeepSeek Harness: it watches the folders you choose and keeps a SQLite index of what appeared — name, type, size, appeared-at, location and origin — so \"where did I put that file?\" becomes a single question. Every folder carries its own incremental anchor, and a folder added later is backfilled in full on its first scan. Search is SQL hard filtering plus multi-keyword hit weighting, with a live fallback scan when the index has no hit. Read-only: it never modifies, moves or deletes your files. 文件快速寻回:定期扫描你指定的文件夹,把新出现的文件(名字/类型/大小/出现时间/位置/来源)记进本地 SQLite 索引,忘了东西放哪问一句就能找回。每个文件夹各有自己的增量锚点,新加入的文件夹第一次会回填全部历史;检索用 SQL 硬过滤加多关键词加权,索引没命中时当场兜底实时扫描一遍。只读,绝不修改、移动或删除你的文件。",
5
5
  "type": "module",
6
6
  "main": "index.mjs",
@@ -27,7 +27,6 @@
27
27
  "client": {
28
28
  "inject": [
29
29
  "@deepseek-ai/dsh-client-locale",
30
- "@deepseek-ai/dsh-client-runtime",
31
30
  "@deepseek-ai/dsh-client-ui-settings"
32
31
  ],
33
32
  "platform": "web"
@@ -48,7 +47,7 @@
48
47
  },
49
48
  "devDependencies": {
50
49
  "@deepseek-ai/dsh-tools": "0.1.5-rc.1",
51
- "@deepseek-ai/schemastery": "^3.18.1"
50
+ "@deepseek-ai/schemastery": "^3.18.4"
52
51
  },
53
52
  "engines": {
54
53
  "node": "^22.0.0 || >=24"