dsh-doc-preview 0.1.0 → 0.1.2

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
@@ -18,20 +18,37 @@ http://<host>/docs/<工作区id>/<相对路径>.md -> 渲染该工作区内的
18
18
 
19
19
  | 工作区目录 | 工作区 id | 示例地址 |
20
20
  |---|---|---|
21
- | `/home/lc/deepseek-no1` | `home-lc-deepseek-no1` | `/docs/home-lc-deepseek-no1/docs-design/调研.md` |
22
- | `/root/documents` | `root-documents` | `/docs/root-documents/报告.md` |
21
+ | `/Users/alice/work/project` | `Users-alice-work-project` | `/docs/Users-alice-work-project/docs/调研.md` |
22
+ | `/Users/alice/work/reports` | `Users-alice-work-reports` | `/docs/Users-alice-work-reports/报告.md` |
23
23
 
24
24
  - 工作区列表来自 `ctx.workspaceRegistry.list()`(持久化于 `storages/workspace.json`),**新登记的工作区自动出现**,无需改插件。
25
25
  - **隔离**:请求被限制在对应工作区目录内;未知工作区、路径穿越(`..`)、畸形多斜杠一律 404。
26
26
 
27
+ ## 宿主端工具 `doc_preview_links`
28
+
29
+ 插件还会注册一个**宿主端工具** `doc_preview_links`,让模型在会话中调用即可拿到「当前会话工作区」的
30
+ `/docs` 链接,用户在 Web 里点开即预览。
31
+
32
+ - **解析当前工作区**:工具通过 `exec.agent.session.header.cwd` 感知当前会话,再与
33
+ `ctx.workspaceRegistry.list()` 匹配(精确 path 匹配优先,其次最长祖先匹配)—**无需模型传参**。
34
+ - **返回**(JSON):
35
+ ```jsonc
36
+ { "workspace": { "slug": "Users-alice-work-project", "path": "/Users/alice/work/project", "title": "project" },
37
+ "workspaceUrl": "/docs/Users-alice-work-project/",
38
+ "docs": [ { "name": "调研.md", "url": "/docs/Users-alice-work-project/docs/调研.md" } ] }
39
+ ```
40
+ 传可选参数 `rel` 时,`docs` 列出该相对目录下 `.md/.markdown/.html` 的链接;不传则只给工作区根链接。
41
+ - **优雅降级**:缺 `cwd` / 当前 cwd 未登记 / 无 registry 时不抛错,返回 `{ workspaceUrl: null, note: '…' }`。
42
+ - **相对路径**:返回 `/docs/...` 相对路径(前缀由宿主 Web 提供);仅 Web 形态适用。
43
+
27
44
  ## 安装
28
45
 
29
46
  ```sh
30
47
  dsh plugin --profile web add /path/to/docs-preview-plugin
31
48
  ```
32
49
 
33
- 依赖:必须运行在**已加载 `ctx.webServer` 与 `ctx.workspaceRegistry`** 的 profile(即 `web` 形态,
34
- dsh-web-app 自带这两个服务)。headless / sdk 这类没有 web host 的 profile 不适用。
50
+ 依赖:必须运行在**已加载 `ctx.webServer`、`ctx.workspaceRegistry` 与 `ctx.tools`** 的 profile(即 `web`
51
+ 形态,dsh-web-app 自带这些服务)。headless / sdk 这类没有 web host 的 profile 不适用。
35
52
 
36
53
  ## 配置
37
54
 
@@ -44,10 +61,11 @@ dsh-web-app 自带这两个服务)。headless / sdk 这类没有 web host 的
44
61
  ## 文件结构
45
62
 
46
63
  ```
47
- src/index.ts # 插件源(name/inject/apply,路由 + md 渲染)
64
+ src/index.ts # 插件源(name/inject/apply,/docs 路由 + md 渲染 + doc_preview_links 工具)
48
65
  lib/index.js # 构建产物(Loader 实际加载)
49
- cordis.patch.yml # 插入 doc-preview 行,inject webServer + workspaceRegistry
66
+ cordis.patch.yml # 插入 doc-preview 行,inject webServer + tools + workspaceRegistry
50
67
  tests/drive-route.mjs # 路由自测(mock workspaceRegistry)
68
+ tests/tools-test.mjs # doc_preview_links 工具自测(mock registry + exec.agent)
51
69
  tests/render-test.mjs # md 渲染自测
52
70
  tests/logic-test.mjs # 渲染边界 + 穿越防护自测
53
71
  README.md / ENABLE.md
@@ -56,7 +74,9 @@ README.md / ENABLE.md
56
74
  ## 自测
57
75
 
58
76
  ```sh
77
+ npm test # build + 全部四套自测
59
78
  node tests/drive-route.mjs # 工作区映射/隔离/404
79
+ node tests/tools-test.mjs # doc_preview_links 工具
60
80
  tsx --tsconfig tsconfig.json tests/render-test.mjs
61
81
  tsx --tsconfig tsconfig.json tests/logic-test.mjs
62
82
  ```
package/cordis.patch.yml CHANGED
@@ -6,12 +6,14 @@
6
6
  - insert:
7
7
  - id: doc-preview
8
8
  name: 'dsh-doc-preview'
9
- inject: [webServer, workspaceRegistry]
9
+ inject: [webServer, tools, workspaceRegistry]
10
10
  config:
11
11
  # /docs routes by real workspace, resolved from ctx.workspaceRegistry
12
12
  # (the dsh-host-workspace service, present in every web profile).
13
13
  # /docs/ -> index of registered workspaces
14
14
  # /docs/<workspaceId>/<rel> -> confined to that workspace directory
15
15
  # workspaceId = workspace path with leading "/" dropped and "/" -> "-",
16
- # e.g. /home/lc/deepseek-no1 -> home-lc-deepseek-no1.
17
- # Example: /docs/home-lc-deepseek-no1/docs-design/调研.md
16
+ # e.g. /Users/alice/work/project -> Users-alice-work-project.
17
+ # Example: /docs/Users-alice-work-project/docs/调研.md
18
+ # Additionally registers the host tool `doc_preview_links` (via ctx.tools)
19
+ # so the agent can report the current workspace's /docs links.
package/lib/index.js CHANGED
@@ -17,7 +17,7 @@
17
17
  import { readdir, readFile, stat } from 'node:fs/promises';
18
18
  import { basename, extname, resolve, sep } from 'node:path';
19
19
  export const name = 'doc-preview';
20
- export const inject = ['webServer', 'workspaceRegistry'];
20
+ export const inject = ['webServer', 'tools', 'workspaceRegistry'];
21
21
  const MIME = {
22
22
  '.html': 'text/html; charset=utf-8',
23
23
  '.md': 'text/html; charset=utf-8',
@@ -211,7 +211,7 @@ function page(title, body) {
211
211
  * /docs/<workspaceId>/ -> directory listing of that workspace root
212
212
  * /docs/<workspaceId>/<rel>.md -> rendered doc
213
213
  * `<workspaceId>` is the workspace path with the leading "/" removed and "/"
214
- * replaced by "-" (e.g. /home/lc/deepseek-no1 -> home-lc-deepseek-no1),
214
+ * replaced by "-" (e.g. /Users/alice/work/project -> Users-alice-work-project),
215
215
  * derived from ctx.workspaceRegistry.list(). Only registered workspaces
216
216
  * resolve; each request is confined to that workspace's directory.
217
217
  */
@@ -268,6 +268,111 @@ function listWorkspaces(wsRegistry) {
268
268
  return [];
269
269
  }
270
270
  }
271
+ /** Resolve the current session workspace by matching `cwd` against the registry. */
272
+ function matchWorkspace(cwd, wsRegistry) {
273
+ if (!cwd)
274
+ return null;
275
+ let workspaces = [];
276
+ try {
277
+ workspaces = (wsRegistry?.list?.() ?? []).map(w => ({
278
+ slug: slugOf(w.path),
279
+ path: w.path,
280
+ title: w.title || w.path,
281
+ }));
282
+ }
283
+ catch {
284
+ return null;
285
+ }
286
+ // Exact path match first; fall back to a trailing-segment match so a cwd that
287
+ // is a descendant of a registered workspace resolves to the nearest workspace.
288
+ const exact = workspaces.find(w => w.path === cwd);
289
+ if (exact)
290
+ return exact;
291
+ const ancestor = workspaces
292
+ .filter(w => cwd === w.path || cwd.startsWith(w.path.endsWith(sep) ? w.path : w.path + sep))
293
+ .sort((a, b) => b.path.length - a.path.length)[0];
294
+ return ancestor ?? null;
295
+ }
296
+ /** Build a /docs URL for a workspace slug + optional relative path. */
297
+ function docUrl(slug, rel) {
298
+ const root = `/docs/${slug}/`;
299
+ if (!rel)
300
+ return root;
301
+ return root + rel.replace(/^\/+/, '');
302
+ }
303
+ function renderJson(_args, value) {
304
+ return [{ type: 'text', text: JSON.stringify(value, null, 2) }];
305
+ }
306
+ function createDocPreviewLinks(wsRegistry) {
307
+ return {
308
+ name: 'doc_preview_links',
309
+ description: 'Return the /docs preview URLs for the current session\u2019s workspace (Web GUI only). ' +
310
+ 'Use when the user wants to preview local Markdown/HTML documents: it reports the workspace\u2019s ' +
311
+ '/docs home URL and, when a `rel` path is given, the document links under that directory.',
312
+ parameters: {
313
+ rel: {
314
+ type: 'string',
315
+ description: 'Optional relative path under the workspace root whose document links to list.',
316
+ optional: true,
317
+ },
318
+ },
319
+ output: {
320
+ schema: { type: 'json' },
321
+ render: renderJson,
322
+ },
323
+ async execute(args, exec) {
324
+ const agent = exec.agent;
325
+ const cwd = agent?.session?.header?.cwd;
326
+ const ws = matchWorkspace(cwd, wsRegistry);
327
+ if (ws === null) {
328
+ return {
329
+ workspaceUrl: null,
330
+ workspace: null,
331
+ docs: [],
332
+ note: cwd
333
+ ? 'workspace not registered for this cwd (doc-preview resolves via workspaceRegistry)'
334
+ : 'no session cwd available (doc-preview links require the Web workspace registry)',
335
+ };
336
+ }
337
+ const result = {
338
+ workspace: ws,
339
+ workspaceUrl: docUrl(ws.slug),
340
+ docs: [],
341
+ };
342
+ if (args.rel) {
343
+ const entries = await listDocs(ws.path, args.rel);
344
+ result.docs = entries.map(e => ({ name: e.name, url: docUrl(ws.slug, e.rel) }));
345
+ }
346
+ return result;
347
+ },
348
+ };
349
+ }
350
+ /** List .md/.markdown/.html files under a workspace-relative directory. */
351
+ async function listDocs(wsPath, rel) {
352
+ const dir = safeJoin(wsPath, rel);
353
+ if (dir === null)
354
+ return [];
355
+ try {
356
+ const info = await stat(dir);
357
+ if (!info.isDirectory())
358
+ return [];
359
+ const entries = await readdir(dir);
360
+ entries.sort();
361
+ const out = [];
362
+ for (const e of entries) {
363
+ if (e.startsWith('.'))
364
+ continue; // skip hidden entries
365
+ const ext = extname(e).toLowerCase();
366
+ if (ext !== '.md' && ext !== '.markdown' && ext !== '.html')
367
+ continue;
368
+ out.push({ name: e, rel: (rel ? rel.replace(/\/$/, '') + '/' : '') + e });
369
+ }
370
+ return out;
371
+ }
372
+ catch {
373
+ return [];
374
+ }
375
+ }
271
376
  async function serve(req, res, config, wsRegistry) {
272
377
  const pathname = req.url.split('?')[0] ?? '/docs/';
273
378
  const { slug, rel } = splitWsRel(pathname);
@@ -331,4 +436,11 @@ export function apply(ctx, config) {
331
436
  path: '/docs',
332
437
  handler: (req, res) => serve(req, res, config ?? {}, wsRegistry),
333
438
  }));
439
+ // Host-side tool: let the agent report the current workspace's /docs links so
440
+ // the user can jump straight to previewing their docs. Registered globally;
441
+ // exec.agent is still the calling agent (injected by the agent loop).
442
+ // Guarded on ctx.tools so routes still work in hosts without a tool registry.
443
+ if (ctx.tools) {
444
+ ctx.effect(() => ctx.tools.register(createDocPreviewLinks(wsRegistry)));
445
+ }
334
446
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-doc-preview",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "A dsh bundle plugin: preview local Markdown/HTML documents per workspace in the Web GUI via a /docs route on ctx.webServer + ctx.workspaceRegistry.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -63,6 +63,7 @@
63
63
  "render:test": "tsx --tsconfig ./tsconfig.json tests/render-test.mjs",
64
64
  "logic:test": "tsx --tsconfig ./tsconfig.json tests/logic-test.mjs",
65
65
  "route:test": "node tests/drive-route.mjs",
66
- "test": "node scripts/build.mjs && node tests/drive-route.mjs && tsx --tsconfig ./tsconfig.json tests/render-test.mjs && tsx --tsconfig ./tsconfig.json tests/logic-test.mjs"
66
+ "tools:test": "node tests/tools-test.mjs",
67
+ "test": "node scripts/build.mjs && node tests/drive-route.mjs && node tests/tools-test.mjs && tsx --tsconfig ./tsconfig.json tests/render-test.mjs && tsx --tsconfig ./tsconfig.json tests/logic-test.mjs"
67
68
  }
68
69
  }