dsh-mindmap 0.3.0 → 0.5.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/CHANGELOG.md +17 -0
- package/LICENSE +21 -0
- package/README.md +4 -2
- package/README.zh-CN.md +4 -2
- package/client.js +1702 -265
- package/index.js +51 -11
- package/package.json +3 -1
package/index.js
CHANGED
|
@@ -14,8 +14,8 @@
|
|
|
14
14
|
// namespace 可在设置面板运行时切换(见 SETTINGS_NAMESPACE/Config)。
|
|
15
15
|
// - 依赖:仅 @deepseek-ai/schemastery(settings schema;发布包正常解析,
|
|
16
16
|
// link 开发需先 npm i)。工具参数 schema 仍手写 JSON Schema(003 偏差 1)。
|
|
17
|
-
import { access, opendir, readFile, rename, writeFile } from 'node:fs/promises'
|
|
18
|
-
import { dirname, isAbsolute, join, relative, resolve as resolvePath } from 'node:path'
|
|
17
|
+
import { access, opendir, readFile, rename, stat, writeFile } from 'node:fs/promises'
|
|
18
|
+
import { dirname, isAbsolute, join, relative, resolve as resolvePath, sep } from 'node:path'
|
|
19
19
|
import Schema from '@deepseek-ai/schemastery'
|
|
20
20
|
|
|
21
21
|
export const name = 'mindmap'
|
|
@@ -31,6 +31,7 @@ export const Config = Schema.object({
|
|
|
31
31
|
lineStyle: Schema.union(['curve', 'elbow']).default('elbow').description('Connector line style between nodes: curve (bezier) or elbow (orthogonal).'),
|
|
32
32
|
cardStyle: Schema.union(['rounded', 'square']).default('rounded').description('Node card corner style.'),
|
|
33
33
|
colorTheme: Schema.union(['ocean', 'sunset', 'forest']).default('ocean').description('Node color theme.'),
|
|
34
|
+
growthAnimation: Schema.boolean().default(true).description('Progressive growth animation: newly added/changed nodes fade in one by one after each update (total capped at ~2s). Turn off for instant full render.'),
|
|
34
35
|
})
|
|
35
36
|
|
|
36
37
|
const MAX_CONTENT_BYTES = 2 * 1024 * 1024
|
|
@@ -53,7 +54,8 @@ Markdown mapping (the panel's parser): headings nest by level (H1 are root child
|
|
|
53
54
|
|
|
54
55
|
Behavior rules:
|
|
55
56
|
- When the user asks to create a mindmap, call mindmap_create. When the user asks to open, view, show, or switch to an existing mindmap, call mindmap_open (do not use mindmap_get alone). Both operations bring that document to the visible mindmap panel automatically.
|
|
56
|
-
- Always mindmap_get before editing, then send the complete updated document to mindmap_update.
|
|
57
|
+
- Always mindmap_get before editing, then send the complete updated document to mindmap_update. Every call must carry the FULL document, never a fragment.
|
|
58
|
+
- Update step by step: whenever the request involves several parts, call mindmap_update as soon as each part is ready — several small updates beat one giant update at the end. The panel plays a growth animation on newly added/changed nodes, so step-by-step updates make the tree visibly grow while you work. Do not call mindmap_update twice in a row with identical content.
|
|
57
59
|
- Never delete the whole document or restructure it without an explicit user request. Make the smallest change that answers the request.
|
|
58
60
|
- When the user steps away or pauses (e.g. "我去买咖啡"), stop all mindmap edits immediately and wait — never continue autonomously.
|
|
59
61
|
- Never run any git command for these files. The user commits themselves.
|
|
@@ -105,6 +107,12 @@ function sessionCwdOf(sessions, sessionId) {
|
|
|
105
107
|
return typeof cwd === 'string' && cwd ? cwd : null
|
|
106
108
|
}
|
|
107
109
|
|
|
110
|
+
/** 相对路径是否越出 base(`..` 本身或以 `..` + 分隔符开头;不能只看 `..` 前缀——
|
|
111
|
+
* `..notes.md` 这类文件名会被误判)。 */
|
|
112
|
+
function escapesBase(rel) {
|
|
113
|
+
return rel === '..' || rel.startsWith(`..${sep}`) || isAbsolute(rel)
|
|
114
|
+
}
|
|
115
|
+
|
|
108
116
|
/** 请求路径校验:缺省 = 根 cwd;显式路径必须绝对且落在 cwd 内。 */
|
|
109
117
|
function resolveTreePath(cwd, input) {
|
|
110
118
|
if (!cwd) throw httpError(400, 'no-cwd', 'session has no working directory')
|
|
@@ -113,7 +121,7 @@ function resolveTreePath(cwd, input) {
|
|
|
113
121
|
if (!isAbsolute(p)) throw httpError(400, 'bad-request', `path must be absolute: ${JSON.stringify(p)}`)
|
|
114
122
|
const resolved = resolvePath(p)
|
|
115
123
|
const rel = relative(cwd, resolved)
|
|
116
|
-
if (
|
|
124
|
+
if (escapesBase(rel)) {
|
|
117
125
|
throw httpError(400, 'bad-request', `path must stay inside the session working directory (${cwd})`)
|
|
118
126
|
}
|
|
119
127
|
return resolved
|
|
@@ -206,7 +214,7 @@ function resolveMindmapPath(cwd, input) {
|
|
|
206
214
|
}
|
|
207
215
|
const resolved = resolvePath(cwd, p)
|
|
208
216
|
const rel = relative(cwd, resolved)
|
|
209
|
-
if (rel === '' ||
|
|
217
|
+
if (rel === '' || escapesBase(rel)) {
|
|
210
218
|
throw new Error(`mindmap path must stay inside the session working directory (${cwd}).`)
|
|
211
219
|
}
|
|
212
220
|
return resolved
|
|
@@ -221,6 +229,20 @@ async function pathExists(p) {
|
|
|
221
229
|
}
|
|
222
230
|
}
|
|
223
231
|
|
|
232
|
+
/**
|
|
233
|
+
* 两个路径是否指向同一个文件(dev + inode 比较)。大小写不敏感 FS(macOS/
|
|
234
|
+
* Windows)上仅大小写不同的路径命中同一文件——case-only 改名时据此区分
|
|
235
|
+
* 「目标就是自己」(放行)与「真有另一个同名文件」(碰撞报错)。
|
|
236
|
+
*/
|
|
237
|
+
async function sameFile(a, b) {
|
|
238
|
+
try {
|
|
239
|
+
const [sa, sb] = await Promise.all([stat(a), stat(b)])
|
|
240
|
+
return sa.dev === sb.dev && sa.ino === sb.ino
|
|
241
|
+
} catch {
|
|
242
|
+
return false
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
224
246
|
function byteLength(value) {
|
|
225
247
|
return new TextEncoder().encode(value).byteLength
|
|
226
248
|
}
|
|
@@ -238,7 +260,16 @@ function defineTool(spec) {
|
|
|
238
260
|
}
|
|
239
261
|
|
|
240
262
|
export function apply(ctx, config = {}) {
|
|
241
|
-
|
|
263
|
+
// 入口配置作为 settings 组合层的 base:视觉三件套与默认宽度在这里
|
|
264
|
+
// 透传(带 schema 同款默认值),用户设置层仍可在设置面板覆盖。
|
|
265
|
+
const entryConfig = {
|
|
266
|
+
requireApproval: config.requireApproval === true,
|
|
267
|
+
defaultPanelWidth: typeof config.defaultPanelWidth === 'number' ? config.defaultPanelWidth : 42,
|
|
268
|
+
lineStyle: config.lineStyle === 'curve' ? 'curve' : 'elbow',
|
|
269
|
+
cardStyle: config.cardStyle === 'square' ? 'square' : 'rounded',
|
|
270
|
+
colorTheme: config.colorTheme === 'sunset' || config.colorTheme === 'forest' ? config.colorTheme : 'ocean',
|
|
271
|
+
growthAnimation: config.growthAnimation !== false,
|
|
272
|
+
}
|
|
242
273
|
|
|
243
274
|
// 015 设置面板:settings 服务可用时以命名空间解析值为准
|
|
244
275
|
// (schema 默认 → 组合层 base → 用户设置层),否则回退入口配置
|
|
@@ -288,8 +319,15 @@ export function apply(ctx, config = {}) {
|
|
|
288
319
|
if (!cwd) throw new Error('The session has no working directory; cannot create a mindmap.')
|
|
289
320
|
const stem = sanitizeStem(args?.name)
|
|
290
321
|
const path = resolveMindmapPath(cwd, `${stem}.md`)
|
|
291
|
-
|
|
292
|
-
|
|
322
|
+
// wx = 不存在才创建:原子拒绝已存在(含并发竞态)与同名目录,无 TOCTOU 窗口。
|
|
323
|
+
try {
|
|
324
|
+
await writeFile(path, '', { encoding: 'utf8', flag: 'wx' })
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (error && (error.code === 'EEXIST' || error.code === 'EISDIR')) {
|
|
327
|
+
throw new Error(`Mindmap already exists: ${JSON.stringify(path)}. Open it with mindmap_open instead.`)
|
|
328
|
+
}
|
|
329
|
+
throw error
|
|
330
|
+
}
|
|
293
331
|
return buildResult('create', path, { content: '', created: true })
|
|
294
332
|
},
|
|
295
333
|
}))
|
|
@@ -334,7 +372,7 @@ export function apply(ctx, config = {}) {
|
|
|
334
372
|
|
|
335
373
|
ctx.tools.register(defineTool({
|
|
336
374
|
name: 'mindmap_update',
|
|
337
|
-
description: 'Write the FULL updated markdown of a mindmap document. Call mindmap_get first, then send the complete new content so the panel
|
|
375
|
+
description: 'Write the FULL updated markdown of a mindmap document. Call mindmap_get first, then send the complete new content. When the edit has several parts, call once per finished part (always full content) so the panel grows the new nodes step by step. Optionally renameRoot to change the root title (renames the file; fails on name collision).',
|
|
338
376
|
parameters: {
|
|
339
377
|
type: 'object',
|
|
340
378
|
properties: {
|
|
@@ -366,7 +404,9 @@ export function apply(ctx, config = {}) {
|
|
|
366
404
|
// cwd 缺失的绝对路径场景同样成立)。
|
|
367
405
|
const target = resolvePath(dirname(path), `${stem}.md`)
|
|
368
406
|
if (target !== path) {
|
|
369
|
-
|
|
407
|
+
// case-only 改名(如 Plan → plan)在大小写不敏感 FS 上 pathExists(target)
|
|
408
|
+
// 命中的就是自己——用 sameFile 放行;真碰撞(不同文件)才报错。
|
|
409
|
+
if (await pathExists(target) && !(await sameFile(path, target))) {
|
|
370
410
|
throw new Error(`Cannot rename root: ${JSON.stringify(target)} already exists. Pick another name.`)
|
|
371
411
|
}
|
|
372
412
|
await rename(path, target)
|
|
@@ -398,7 +438,7 @@ export function apply(ctx, config = {}) {
|
|
|
398
438
|
}
|
|
399
439
|
try {
|
|
400
440
|
const method = new URL(req.url ?? '/', 'http://dsh.internal').pathname.slice('/mindmap/api/'.length)
|
|
401
|
-
if (method !== 'tree'
|
|
441
|
+
if (method !== 'tree') {
|
|
402
442
|
sendJson(res, 404, { ok: false, error: { code: 'not-found', message: `unknown mindmap API method ${JSON.stringify(method)}` } })
|
|
403
443
|
return
|
|
404
444
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-mindmap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Mindmap plugin for DeepSeek Harness: a plain markdown file in the working directory IS the mindmap; the chat edits it step by step and the right-side panel follows live.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"publishConfig": {
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
|
+
"author": "guhanfei-ai",
|
|
17
|
+
"license": "MIT",
|
|
16
18
|
"type": "module",
|
|
17
19
|
"main": "index.js",
|
|
18
20
|
"scripts": {
|