dsh-plugin-tool-management 0.5.1 → 0.7.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 +131 -0
- package/README.md +185 -238
- package/README_EN.md +185 -304
- package/cordis.patch.yml +9 -55
- package/docs/images/1/345/234/272/346/231/257.png +0 -0
- package/docs/images/1/345/234/272/346/231/257_en.png +0 -0
- package/docs/images/2MCP.png +0 -0
- package/docs/images/2MCP_en.png +0 -0
- package/docs/images/3/346/212/200/350/203/275.png +0 -0
- package/docs/images/3/346/212/200/350/203/275_en.png +0 -0
- package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
- package/docs/images/4/345/255/220/346/231/272/350/203/275/344/275/223_en.png +0 -0
- package/docs/images/5/346/217/220/347/244/272/350/257/215.png +0 -0
- package/docs/images/5/346/217/220/347/244/272/350/257/215_en.png +0 -0
- package/docs/images/6/350/256/260/345/277/206.png +0 -0
- package/docs/images/6/350/256/260/345/277/206_en.png +0 -0
- package/docs/images/7/344/274/232/350/257/235.png +0 -0
- package/docs/images/7/344/274/232/350/257/235_en.png +0 -0
- package/docs/images/8/345/205/274/345/256/271.png +0 -0
- package/docs/images/8/345/205/274/345/256/271_en.png +0 -0
- package/docs/update.md +55 -0
- package/lib/agents-md/preset-id.js +49 -0
- package/lib/agents-md/service.js +180 -57
- package/lib/client.js +4710 -3560
- package/lib/compat/preset-reach.js +425 -0
- package/lib/compat/probe.js +665 -0
- package/lib/history/bridge.js +293 -0
- package/lib/history/workspace.js +498 -52
- package/lib/http-fence.js +94 -0
- package/lib/hub.js +160 -2
- package/lib/index.js +759 -157
- package/lib/mcp/override-blocks.js +195 -0
- package/lib/rules/provider.js +3 -3
- package/lib/rules/service.js +342 -31
- package/lib/scene-prompt-sync.js +112 -0
- package/lib/skills/core.js +123 -35
- package/lib/skills/service.js +9 -1
- package/lib/subagents/service.js +197 -23
- package/lib/subagents/tools.js +8 -2
- package/package.json +6 -3
- package/screenshots.json +10 -9
- package/docs/Changelog.md +0 -517
- package/docs/images/MCP.png +0 -0
- package/docs/images//344/274/232/350/257/235.png +0 -0
- package/docs/images//345/234/272/346/231/257.png +0 -0
- package/docs/images//345/255/220/346/231/272/350/203/275/344/275/223.png +0 -0
- package/docs/images//346/212/200/350/203/275.png +0 -0
- package/docs/images//346/217/220/347/244/272/350/257/215.png +0 -0
- package/docs/images//350/256/260/345/277/206.png +0 -0
package/cordis.patch.yml
CHANGED
|
@@ -1,60 +1,14 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Install only this plugin. History composes the already active host services.
|
|
2
|
+
# Do NOT disable workspace/session-projection-cache, insert competing service
|
|
3
|
+
# providers, or infer capabilities from another package's name.
|
|
4
|
+
# Existing explicit user-layer overrides remain the user's responsibility.
|
|
2
5
|
#
|
|
3
|
-
#
|
|
6
|
+
# Double-mount guard: inspect raw options only. Entry.disabled is a computed
|
|
7
|
+
# getter and reading it from another disabled expression can recurse at boot.
|
|
4
8
|
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# `dsh-plugin-tool-management` to the bundle stack. The profile boot then merges
|
|
8
|
-
# THIS patch (a single `insert` of the plugin row). No profile file edits
|
|
9
|
-
# needed — one command installs and mounts.
|
|
10
|
-
#
|
|
11
|
-
# Double-mount guard: an aggregate bundle (or a hand-written loader row) may
|
|
12
|
-
# already mount this plugin under a different entry id. Two mounts both register the
|
|
13
|
-
# /dsh-plugin-tool-management/api route and fail the whole plugin tree at boot
|
|
14
|
-
# ("duplicate prefix route"). The `!!js` disabled expression backs THIS row
|
|
15
|
-
# off when another *enabled* entry already mounts the plugin; the existing
|
|
16
|
-
# instance then owns the API. The loader evaluates the expression at entry
|
|
17
|
-
# activation — the full entry tree is composed by then, so user-layer
|
|
18
|
-
# (profile cordis.patch.yml) rows are visible too.
|
|
19
|
-
#
|
|
20
|
-
# ORDER MATTERS — do not move `!e.disabled` ahead of the id/name checks.
|
|
21
|
-
# `Entry.disabled` is an uncached getter that re-evaluates the expression,
|
|
22
|
-
# so touching `e.disabled` of THIS entry re-enters this very expression
|
|
23
|
-
# (infinite recursion → "Maximum call stack size exceeded" at boot). The
|
|
24
|
-
# `id !==` check short-circuits on our own row and the `name` check
|
|
25
|
-
# short-circuits on every unrelated entry (e.g. dsh-better-sidebar's own
|
|
26
|
-
# guard), so `!e.disabled` is only ever read on a genuinely matching
|
|
27
|
-
# legacy/aggregate row (a plain boolean, no re-entry).
|
|
28
|
-
# 折叠 dsh-archive-manager:替换官方 workspace 与 session-projection-cache
|
|
29
|
-
# 服务为归档感知子类(永久删除 + unarchive + 批量 + archivedAt 保留期账本)。
|
|
30
|
-
# 官方 ui-workspace 保持启用;归档菜单仍由官方 UI 提供,删除/恢复/保留期
|
|
31
|
-
# 由本插件的 TOOLS → History 页面经 HTTP API 驱动。
|
|
32
|
-
#
|
|
33
|
-
# 冲突告警:不要同时安装独立的 @michengai/dsh-archive-manager——两个补丁都
|
|
34
|
-
# 禁用官方 workspace 行并插入替换,会导致重复服务注册、启动失败。
|
|
35
|
-
#
|
|
36
|
-
# spawn provider(子智能体)挂载:本文件**故意不**插入
|
|
37
|
-
# `@deepseek-ai/dsh-subagent-spawn-in-process` 行。取舍理由:
|
|
38
|
-
# 1) insert 行在启动期解析:换台没有该包的宿主会因单行加载失败拖垮整棵插件树,
|
|
39
|
-
# 而缺 provider 时受影响面只有一个 subagent_run 调用;
|
|
40
|
-
# 2) 该行与本插件运行时的 ctx.plugin 兜底挂载存在双挂载窗口(同名 provider 重复注册)。
|
|
41
|
-
# 改由 src/subagents/service.ts 走官方通道:ctx.subagents.list() 探测到 spawn 即跳过;
|
|
42
|
-
# 缺失才经 createRequire + apply(ctx, {providerName:'spawn'}) 挂载;失败把原因原样抛给模型。
|
|
43
|
-
# 该包在 package.json 声明为**可选** peerDependency(peerDependenciesMeta.optional)——
|
|
44
|
-
# 宿主/profile 自带时可以照常挂载。设计 §3.2 字面要求「挂载声明进 cordis.patch.yml」,
|
|
45
|
-
# 这里按上述风险裁定为有意偏离(记录见 docs/审查/2026-09-13-场景档案v2双轴评审.md)。
|
|
46
|
-
- id: workspace
|
|
47
|
-
disabled: true
|
|
48
|
-
- id: session-projection-cache
|
|
49
|
-
disabled: true
|
|
9
|
+
# The optional subagent spawn provider is resolved at runtime by the subagent
|
|
10
|
+
# service; an unavailable optional package must not prevent the host from booting.
|
|
50
11
|
- insert:
|
|
51
12
|
- id: dsh-plugin-tool-management
|
|
52
13
|
name: 'dsh-plugin-tool-management'
|
|
53
|
-
disabled: !!js "[...ctx.loader.entries()].some((e) => e.options.id !== 'dsh-plugin-tool-management' && e.options.name === 'dsh-plugin-tool-management' &&
|
|
54
|
-
- id: workspace-tool-management-history
|
|
55
|
-
name: 'dsh-plugin-tool-management/workspace'
|
|
56
|
-
- id: session-projection-cache-tool-management-history
|
|
57
|
-
name: 'dsh-plugin-tool-management/projcache'
|
|
58
|
-
config:
|
|
59
|
-
writeEveryEvents: 200
|
|
60
|
-
writeIntervalMs: 5000
|
|
14
|
+
disabled: !!js "[...ctx.loader.entries()].some((e) => e.options.id !== 'dsh-plugin-tool-management' && e.options.name === 'dsh-plugin-tool-management' && e.options.disabled !== true && typeof e.options.disabled !== 'object')"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/update.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# 版本更新概要
|
|
2
|
+
|
|
3
|
+
每个版本大致改了什么——想知道「升上去会变什么」看这里就够了。逐条细节见 [Changelog](../CHANGELOG.md),功能说明见 [README](../README.md) / [README_EN](../README_EN.md)。
|
|
4
|
+
|
|
5
|
+
| 版本 | 发布 | 主题 |
|
|
6
|
+
|---|---|---|
|
|
7
|
+
| [0.7.0](#070---2026-09-15) | 2026-09-15 | 回收站全量 + 预设注入可见性 + 密钥令牌化 |
|
|
8
|
+
| [0.6.0](#060---2026-09-14) | 2026-09-14 | 宿主兼容体检 + 兼容页 |
|
|
9
|
+
| [0.5.1](#051---2026-09-13) | 2026-09-13 | 场景档案默认不勾选 + 场景页排版重做 |
|
|
10
|
+
| [0.5.0](#050---2026-09-13) | 2026-09-13 | 数据目录统一 + 场景显式化 + 界面重做 |
|
|
11
|
+
| [0.2.0](#020---2026-09-12) | 2026-09-12 | 五域合并进一个「工具」面板 |
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 0.7.0 - 2026-09-15
|
|
16
|
+
|
|
17
|
+
- 回收站扩到子智能体 / 场景 / 提示词预设(删除都进回收站,可恢复)。
|
|
18
|
+
- 子智能体工具限制按 Agent 预设(每预设一份白/黑名单)。
|
|
19
|
+
- 兼容页「预设注入边界」五列标签(绿/红/灰框)。
|
|
20
|
+
- 明文密钥改为一律要令牌(没配 `token` 就不给明文)。
|
|
21
|
+
- MCP「整理补丁」重新上线(删多余启停块,状态不变)。
|
|
22
|
+
|
|
23
|
+
## 0.6.0 - 2026-09-14
|
|
24
|
+
|
|
25
|
+
- 新增「兼容」页(第 8 栏)+ doctor/host-deps 脚本。
|
|
26
|
+
- 兼容判定从源码文本比对改为模块实体 + 能力探测。
|
|
27
|
+
- 归档会话不再丢工作区归属(登记被删后重建分组 + 一键重新登记)。
|
|
28
|
+
- 技能来源权限语义修正(默认来源必须读取但技能可删)。
|
|
29
|
+
- 场景可绑提示词预设,切换场景直接改写 AGENTS.md(关掉自动恢复)。
|
|
30
|
+
- 场景启用改单选,新场景默认不启动。
|
|
31
|
+
|
|
32
|
+
## 0.5.1 - 2026-09-13
|
|
33
|
+
|
|
34
|
+
- 场景档案弹窗新加段默认不勾选。
|
|
35
|
+
- 档案「记忆」段只列本场景的记忆。
|
|
36
|
+
- 场景页排版重做(卡片只留名字+描述+动作)。
|
|
37
|
+
- 保留场景「全局」不再出现在场景页与档案弹窗。
|
|
38
|
+
|
|
39
|
+
## 0.5.0 - 2026-09-13
|
|
40
|
+
|
|
41
|
+
- 数据全部收进 `~/.dsh/tool-management/`(旧位置自动搬入)。
|
|
42
|
+
- 场景升级为显式记录;根层裸 `.md` 不再注入。
|
|
43
|
+
- 新增「导入技能」来源 + 「移除来源」操作。
|
|
44
|
+
- 场景档案新增第 4 段「记忆」。
|
|
45
|
+
- 修掉三个真故障:整页白屏、导出静默丢失、Skills 页锁死。
|
|
46
|
+
- 斜杠命令全部下线。
|
|
47
|
+
|
|
48
|
+
## 0.2.0 - 2026-09-12
|
|
49
|
+
|
|
50
|
+
- 五域合并进一个「工具」面板:MCP / Skills / AGENTS.md 预设 / History。
|
|
51
|
+
- MCP 工具级启停、重启只重连、密钥打码、补丁备份。
|
|
52
|
+
- Skills 接入 ~/.agents / ~/.codex / ~/.claude + 自定义目录。
|
|
53
|
+
- History 按项目分组、批量操作、保留期清理、对话导入导出。
|
|
54
|
+
|
|
55
|
+
> 0.1.1 / 0.1.2 / 0.1.3 无独立记录(仅 git tag)。
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// dsh-plugin-tool-management —— 提示词预设 id 的唯一口径。
|
|
2
|
+
//
|
|
3
|
+
// 用户裁定(2026-09-15):「id 应该什么都能写」——不再限定小写字母/数字/连字符,
|
|
4
|
+
// 中文、空格、下划线、点都可以。id 同时也是**磁盘目录名**,所以只保留两类硬约束:
|
|
5
|
+
// ① 文件系统安全:不含路径分隔符与 Windows 非法字符、不以点开头/结尾、
|
|
6
|
+
// 不是 `.` / `..`、不是 Windows 保留设备名;
|
|
7
|
+
// ② 一个保留字:`__last-applied__`(apply 的备份槽,不是用户预设)。
|
|
8
|
+
// 这套口径由 agents-md 服务与场景绑定(`scenes[].prompt`)共用,避免两处漂移。
|
|
9
|
+
// 约定镜像 @deepseek-ai/dsh-agent-presets 的「id 即目录名」,但放宽了字符集。
|
|
10
|
+
/** id 长度上限(字符数;目录名过长在 Windows 上还受 MAX_PATH 约束)。 */
|
|
11
|
+
export const PRESET_ID_MAX = 64;
|
|
12
|
+
/** apply 的备份槽:不是用户预设,任何入口都不得当作预设 id。 */
|
|
13
|
+
export const LAST_APPLIED_PRESET_ID = '__last-applied__';
|
|
14
|
+
/** Windows 保留设备名(不区分大小写,带扩展名同样非法)。 */
|
|
15
|
+
const WINDOWS_RESERVED = /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
16
|
+
/**
|
|
17
|
+
* 校验并归一化一个预设 id。
|
|
18
|
+
* @param raw - 用户输入或来自索引的原始值。
|
|
19
|
+
* @returns `{ ok: true, id }`(已 trim)或 `{ ok: false, error }`(给人看的中文原因)。
|
|
20
|
+
*/
|
|
21
|
+
export function normalizePresetId(raw) {
|
|
22
|
+
const id = String(raw ?? '').trim();
|
|
23
|
+
if (id === '')
|
|
24
|
+
return { ok: false, error: 'id 不能为空' };
|
|
25
|
+
if (id.length > PRESET_ID_MAX)
|
|
26
|
+
return { ok: false, error: `id 过长(≤${PRESET_ID_MAX} 字符,当前 ${id.length})` };
|
|
27
|
+
if (id === LAST_APPLIED_PRESET_ID)
|
|
28
|
+
return { ok: false, error: `「${LAST_APPLIED_PRESET_ID}」是「应用」的备份槽,不能用作预设 id` };
|
|
29
|
+
if (id === '.' || id === '..')
|
|
30
|
+
return { ok: false, error: 'id 不能是「.」或「..」' };
|
|
31
|
+
if (id.startsWith('.'))
|
|
32
|
+
return { ok: false, error: 'id 不能以「.」开头' };
|
|
33
|
+
if (/[\\/]/.test(id))
|
|
34
|
+
return { ok: false, error: 'id 不能含路径分隔符(/ 或 \\)' };
|
|
35
|
+
if (/[<>:"|?*]/.test(id))
|
|
36
|
+
return { ok: false, error: 'id 不能含 < > : " | ? * 这些字符' };
|
|
37
|
+
if (/[.\s]$/.test(id))
|
|
38
|
+
return { ok: false, error: 'id 不能以点或空格结尾(Windows 会静默去掉)' };
|
|
39
|
+
if (WINDOWS_RESERVED.test(id))
|
|
40
|
+
return { ok: false, error: `「${id}」是 Windows 保留设备名` };
|
|
41
|
+
// 控制字符(含换行/制表)会让目录名与日志不可读,直接拒绝。
|
|
42
|
+
if (/[\u0000-\u001f\u007f]/.test(id))
|
|
43
|
+
return { ok: false, error: 'id 不能含控制字符' };
|
|
44
|
+
return { ok: true, id };
|
|
45
|
+
}
|
|
46
|
+
/** 便捷判定(索引解析等只关心"能不能用"的地方)。 */
|
|
47
|
+
export function isValidPresetId(raw) {
|
|
48
|
+
return normalizePresetId(raw).ok;
|
|
49
|
+
}
|
package/lib/agents-md/service.js
CHANGED
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
// dsh-plugin-tool-management —— AGENTS.md 预设库 + 切换 服务层。
|
|
2
2
|
//
|
|
3
3
|
// DSH 全局指令基线只有一个文件 ~/.dsh/AGENTS.md(USER_GLOBAL_FILE 固定),
|
|
4
|
-
// 没有内置的「多份全局 AGENTS.md
|
|
4
|
+
// 没有内置的「多份全局 AGENTS.md 切换」机制。本服务在 hub 内维护一个
|
|
5
5
|
// 预设库(每套一个子目录 + AGENTS.md),「应用」= 把选中预设内容写入
|
|
6
6
|
// ~/.dsh/AGENTS.md,新会话生效(当前会话不变,DSH 本身如此)。
|
|
7
7
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
8
|
+
// id 即目录名:字符集口径见 `./preset-id.ts`(用户裁定「什么都能写」,只留
|
|
9
|
+
// 文件系统安全约束);`__last-applied__` 是备份槽,不算用户预设。
|
|
10
10
|
// 「当前生效」靠比对 ~/.dsh/AGENTS.md 的 sha256 与各预设 sha256 推断,
|
|
11
11
|
// 无状态文件——用户手改全局文件也能如实反映。
|
|
12
|
-
import { mkdir, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
12
|
+
import { mkdir, readdir, readFile, rename, rm, stat, writeFile } from 'node:fs/promises';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { createHash } from 'node:crypto';
|
|
15
|
-
|
|
15
|
+
import { isValidPresetId, LAST_APPLIED_PRESET_ID, normalizePresetId } from './preset-id.js';
|
|
16
|
+
import { listTrashEntries, moveOutOfTrash, moveToTrash, purgeTrashEntry, readTrashEntry } from '../hub.js';
|
|
16
17
|
const FILENAME = 'AGENTS.md';
|
|
17
|
-
const LAST_APPLIED_ID =
|
|
18
|
+
const LAST_APPLIED_ID = LAST_APPLIED_PRESET_ID;
|
|
19
|
+
/** 新建预设的初始正文(空模板);用户也可以直接粘贴自己的内容。 */
|
|
20
|
+
const BLANK_TEMPLATE = '# AGENTS.md\n\n(DSH 全局指令基线预设,待编辑)\n';
|
|
21
|
+
/** 全局 AGENTS.md 的备份代际上限:与 mcpm patch 的 KEEP_PATCH_BACKUPS 同纪律。 */
|
|
22
|
+
const KEEP_GLOBAL_BACKUPS = 5;
|
|
18
23
|
export function createAgentsMdService(_ctx, deps) {
|
|
19
24
|
const message = (e) => String((e && e.message) || e);
|
|
25
|
+
/** 校验 id 并返回错误文本(合法时返回 `null`)。 */
|
|
26
|
+
const idError = (raw) => {
|
|
27
|
+
const result = normalizePresetId(raw);
|
|
28
|
+
return result.ok ? null : result.error;
|
|
29
|
+
};
|
|
20
30
|
async function sha256OfFile(abs) {
|
|
21
31
|
try {
|
|
22
32
|
const buf = await readFile(abs, 'utf8');
|
|
@@ -36,7 +46,7 @@ export function createAgentsMdService(_ctx, deps) {
|
|
|
36
46
|
existing = await readdir(presetsDir, { withFileTypes: true });
|
|
37
47
|
}
|
|
38
48
|
catch { /* 库不存在 */ }
|
|
39
|
-
const hasPreset = existing.some((e) => e.isDirectory() &&
|
|
49
|
+
const hasPreset = existing.some((e) => e.isDirectory() && isValidPresetId(e.name) && e.name !== LAST_APPLIED_ID);
|
|
40
50
|
if (hasPreset)
|
|
41
51
|
return;
|
|
42
52
|
let globalContent;
|
|
@@ -67,7 +77,7 @@ export function createAgentsMdService(_ctx, deps) {
|
|
|
67
77
|
}
|
|
68
78
|
const globalHash = await sha256OfFile(globalPath);
|
|
69
79
|
const ids = entries
|
|
70
|
-
.filter((e) => e.isDirectory() &&
|
|
80
|
+
.filter((e) => e.isDirectory() && isValidPresetId(e.name) && e.name !== LAST_APPLIED_ID)
|
|
71
81
|
.map((e) => e.name);
|
|
72
82
|
const presets = await Promise.all(ids.map(async (id) => {
|
|
73
83
|
const contentHash = await sha256OfFile(join(presetsDir, id, FILENAME));
|
|
@@ -84,11 +94,10 @@ export function createAgentsMdService(_ctx, deps) {
|
|
|
84
94
|
}
|
|
85
95
|
}
|
|
86
96
|
async function read(id) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
return { ok: false, error: '非法 id(仅允许小写字母、数字、连字符):' + safeId };
|
|
97
|
+
const bad = idError(id);
|
|
98
|
+
if (bad)
|
|
99
|
+
return { ok: false, error: '非法 id:' + bad };
|
|
100
|
+
const safeId = String(id ?? '').trim();
|
|
92
101
|
try {
|
|
93
102
|
const content = await readFile(join(deps.presetsDir, safeId, FILENAME), 'utf8');
|
|
94
103
|
return { ok: true, content };
|
|
@@ -97,39 +106,53 @@ export function createAgentsMdService(_ctx, deps) {
|
|
|
97
106
|
return { ok: false, error: '预设不存在:' + safeId };
|
|
98
107
|
}
|
|
99
108
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
109
|
+
/** 新建预设:`content` 优先(用户在新建弹窗里直接写的内容),否则用 `from` 复制,否则空模板。 */
|
|
110
|
+
async function create(id, options) {
|
|
111
|
+
const bad = idError(id);
|
|
112
|
+
if (bad)
|
|
113
|
+
return { ok: false, error: '非法 id:' + bad };
|
|
114
|
+
const safeId = String(id ?? '').trim();
|
|
104
115
|
const dir = join(deps.presetsDir, safeId);
|
|
105
116
|
try {
|
|
106
117
|
await stat(dir);
|
|
107
118
|
return { ok: false, error: 'id 已存在:' + safeId };
|
|
108
119
|
}
|
|
109
120
|
catch { /* 不存在,继续 */ }
|
|
121
|
+
const explicit = options && typeof options.content === 'string' ? options.content : undefined;
|
|
110
122
|
let content;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!PRESET_ID_RE.test(srcId))
|
|
114
|
-
return { ok: false, error: '非法 from id:' + srcId };
|
|
115
|
-
try {
|
|
116
|
-
content = await readFile(join(deps.presetsDir, srcId, FILENAME), 'utf8');
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
return { ok: false, error: '来源预设不存在:' + srcId };
|
|
120
|
-
}
|
|
123
|
+
if (explicit !== undefined) {
|
|
124
|
+
content = explicit;
|
|
121
125
|
}
|
|
122
126
|
else {
|
|
123
|
-
|
|
127
|
+
const srcId = String(options && options.from ? options.from : '').trim();
|
|
128
|
+
if (srcId) {
|
|
129
|
+
const badFrom = idError(srcId);
|
|
130
|
+
if (badFrom)
|
|
131
|
+
return { ok: false, error: '非法来源 id:' + badFrom };
|
|
132
|
+
try {
|
|
133
|
+
content = await readFile(join(deps.presetsDir, srcId, FILENAME), 'utf8');
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return { ok: false, error: '来源预设不存在:' + srcId };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
content = BLANK_TEMPLATE;
|
|
141
|
+
}
|
|
124
142
|
}
|
|
125
143
|
await mkdir(dir, { recursive: true });
|
|
126
144
|
await writeFile(join(dir, FILENAME), content, 'utf8');
|
|
127
145
|
return { ok: true, id: safeId };
|
|
128
146
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
/**
|
|
148
|
+
* 保存预设:正文写入 + 可选**改名**(目录改名,内容随目录一起走)。
|
|
149
|
+
* 改名冲突(目标已存在)直接拒绝,不合并、不覆盖。
|
|
150
|
+
*/
|
|
151
|
+
async function update(id, content, nextId) {
|
|
152
|
+
const bad = idError(id);
|
|
153
|
+
if (bad)
|
|
154
|
+
return { ok: false, error: '非法 id:' + bad };
|
|
155
|
+
const safeId = String(id ?? '').trim();
|
|
133
156
|
const dir = join(deps.presetsDir, safeId);
|
|
134
157
|
try {
|
|
135
158
|
await stat(dir);
|
|
@@ -137,13 +160,64 @@ export function createAgentsMdService(_ctx, deps) {
|
|
|
137
160
|
catch {
|
|
138
161
|
return { ok: false, error: '预设不存在:' + safeId };
|
|
139
162
|
}
|
|
140
|
-
|
|
141
|
-
|
|
163
|
+
let targetId = safeId;
|
|
164
|
+
if (nextId !== undefined && String(nextId).trim() !== safeId) {
|
|
165
|
+
const badNext = idError(nextId);
|
|
166
|
+
if (badNext)
|
|
167
|
+
return { ok: false, error: '非法新 id:' + badNext };
|
|
168
|
+
targetId = String(nextId).trim();
|
|
169
|
+
const nextDir = join(deps.presetsDir, targetId);
|
|
170
|
+
try {
|
|
171
|
+
await stat(nextDir);
|
|
172
|
+
return { ok: false, error: '新 id 已存在:' + targetId };
|
|
173
|
+
}
|
|
174
|
+
catch { /* 可用 */ }
|
|
175
|
+
try {
|
|
176
|
+
await rename(dir, nextDir);
|
|
177
|
+
}
|
|
178
|
+
catch (e) {
|
|
179
|
+
return { ok: false, error: '改名失败:' + message(e) };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
await writeFile(join(deps.presetsDir, targetId, FILENAME), String(content ?? ''), 'utf8');
|
|
183
|
+
return { ok: true, id: targetId, ...(targetId === safeId ? {} : { renamedFrom: safeId }) };
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* 覆盖写全局 `~/.dsh/AGENTS.md` 前的备份(多代 + `__last-applied__` 维护)。
|
|
187
|
+
* **必须留多代**:只有单个 `__last-applied__` 槽位时,连续写两次就会把用户原始手写内容
|
|
188
|
+
* 永久覆盖掉(第一次备份原始内容 → 第二次备份第一个预设),而「应用」只是点两下按钮。
|
|
189
|
+
* @returns 是否真的备份了(当前文件存在才备份)。
|
|
190
|
+
*/
|
|
191
|
+
async function backupGlobal(current) {
|
|
192
|
+
if (current === null)
|
|
193
|
+
return false;
|
|
194
|
+
const backupDir = join(deps.presetsDir, LAST_APPLIED_ID);
|
|
195
|
+
await mkdir(backupDir, { recursive: true });
|
|
196
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
197
|
+
await writeFile(join(backupDir, `${FILENAME}.bak-${stamp}`), current, 'utf8');
|
|
198
|
+
await writeFile(join(backupDir, FILENAME), current, 'utf8');
|
|
199
|
+
try {
|
|
200
|
+
const gens = (await readdir(backupDir)).filter((n) => n.startsWith(FILENAME + '.bak-')).sort();
|
|
201
|
+
for (const stale of gens.slice(0, Math.max(0, gens.length - KEEP_GLOBAL_BACKUPS))) {
|
|
202
|
+
await rm(join(backupDir, stale), { force: true });
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch { /* 轮转失败不影响本次写入 */ }
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
async function readGlobal() {
|
|
209
|
+
try {
|
|
210
|
+
return await readFile(await deps.getGlobalAgentsMdPath(), 'utf8');
|
|
211
|
+
}
|
|
212
|
+
catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
142
215
|
}
|
|
143
216
|
async function apply(id) {
|
|
144
|
-
const
|
|
145
|
-
if (
|
|
146
|
-
return { ok: false, error: '非法 id
|
|
217
|
+
const bad = idError(id);
|
|
218
|
+
if (bad)
|
|
219
|
+
return { ok: false, error: '非法 id:' + bad };
|
|
220
|
+
const safeId = String(id ?? '').trim();
|
|
147
221
|
let content;
|
|
148
222
|
try {
|
|
149
223
|
content = await readFile(join(deps.presetsDir, safeId, FILENAME), 'utf8');
|
|
@@ -151,20 +225,27 @@ export function createAgentsMdService(_ctx, deps) {
|
|
|
151
225
|
catch {
|
|
152
226
|
return { ok: false, error: '预设不存在:' + safeId };
|
|
153
227
|
}
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
228
|
+
const prev = await readGlobal();
|
|
229
|
+
const backedUp = await backupGlobal(prev);
|
|
230
|
+
await writeFile(await deps.getGlobalAgentsMdPath(), content, 'utf8');
|
|
231
|
+
return { ok: true, id: safeId, backedUp };
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* 按**原文**写回全局基线(不经过任何预设)。
|
|
235
|
+
* 用途:场景切换的「关掉场景 → 恢复进场景之前的基线」——那份内容可能是用户手写的,
|
|
236
|
+
* 不一定对应任何预设。备份纪律与 `apply` 完全相同。
|
|
237
|
+
*/
|
|
238
|
+
async function restore(content) {
|
|
239
|
+
const text = String(content ?? '');
|
|
240
|
+
const prev = await readGlobal();
|
|
241
|
+
const backedUp = await backupGlobal(prev);
|
|
157
242
|
try {
|
|
158
|
-
|
|
243
|
+
await writeFile(await deps.getGlobalAgentsMdPath(), text, 'utf8');
|
|
159
244
|
}
|
|
160
|
-
catch {
|
|
161
|
-
|
|
162
|
-
const backupDir = join(deps.presetsDir, LAST_APPLIED_ID);
|
|
163
|
-
await mkdir(backupDir, { recursive: true });
|
|
164
|
-
await writeFile(join(backupDir, FILENAME), prev, 'utf8');
|
|
245
|
+
catch (e) {
|
|
246
|
+
return { ok: false, error: '写入全局 AGENTS.md 失败:' + message(e) };
|
|
165
247
|
}
|
|
166
|
-
|
|
167
|
-
return { ok: true, id: safeId, backedUp: prev !== null };
|
|
248
|
+
return { ok: true, backedUp };
|
|
168
249
|
}
|
|
169
250
|
async function getCurrent() {
|
|
170
251
|
try {
|
|
@@ -185,7 +266,7 @@ export function createAgentsMdService(_ctx, deps) {
|
|
|
185
266
|
}
|
|
186
267
|
catch { /* 库不存在 */ }
|
|
187
268
|
for (const e of entries) {
|
|
188
|
-
if (!e.isDirectory() || !
|
|
269
|
+
if (!e.isDirectory() || !isValidPresetId(e.name) || e.name === LAST_APPLIED_ID)
|
|
189
270
|
continue;
|
|
190
271
|
let c;
|
|
191
272
|
try {
|
|
@@ -205,12 +286,17 @@ export function createAgentsMdService(_ctx, deps) {
|
|
|
205
286
|
return { ok: false, error: message(e) };
|
|
206
287
|
}
|
|
207
288
|
}
|
|
289
|
+
/**
|
|
290
|
+
* 删除预设 = **移入回收站**(整个 `agents-md/<id>/` 目录搬走)。
|
|
291
|
+
* 备份槽 `__last-applied__` 不可删;正在生效的预设由调用方先拦(见 index.ts)。
|
|
292
|
+
*/
|
|
208
293
|
async function remove(id) {
|
|
209
|
-
const safeId = String(id
|
|
294
|
+
const safeId = String(id ?? '').trim();
|
|
210
295
|
if (safeId === LAST_APPLIED_ID)
|
|
211
296
|
return { ok: false, error: '备份槽不可删除' };
|
|
212
|
-
|
|
213
|
-
|
|
297
|
+
const bad = idError(safeId);
|
|
298
|
+
if (bad)
|
|
299
|
+
return { ok: false, error: '非法 id:' + bad };
|
|
214
300
|
const dir = join(deps.presetsDir, safeId);
|
|
215
301
|
try {
|
|
216
302
|
await stat(dir);
|
|
@@ -218,14 +304,51 @@ export function createAgentsMdService(_ctx, deps) {
|
|
|
218
304
|
catch {
|
|
219
305
|
return { ok: false, error: '预设不存在:' + safeId };
|
|
220
306
|
}
|
|
221
|
-
await
|
|
307
|
+
const moved = await moveToTrash('agents-md', safeId, [{ from: dir, dest: 'preset' }]);
|
|
308
|
+
if (moved.ok === false)
|
|
309
|
+
return { ok: false, error: '移入回收站失败:' + moved.error };
|
|
222
310
|
return { ok: true, id: safeId };
|
|
223
311
|
}
|
|
312
|
+
async function trashList() {
|
|
313
|
+
return { ok: true, trash: await listTrashEntries('agents-md') };
|
|
314
|
+
}
|
|
315
|
+
/** 从回收站恢复预设:同 id 已存在时**拒绝**(绝不覆盖)。 */
|
|
316
|
+
async function trashRestore(id) {
|
|
317
|
+
const entry = await readTrashEntry('agents-md', String(id ?? '').trim());
|
|
318
|
+
if (!entry)
|
|
319
|
+
return { ok: false, error: '回收站条目不存在:' + String(id ?? '').trim() };
|
|
320
|
+
const bad = idError(entry.name);
|
|
321
|
+
if (bad)
|
|
322
|
+
return { ok: false, error: '回收站里的 id 不合法:' + bad };
|
|
323
|
+
const target = join(deps.presetsDir, entry.name);
|
|
324
|
+
try {
|
|
325
|
+
await stat(target);
|
|
326
|
+
return { ok: false, error: '无法恢复,同 id 预设已存在:' + entry.name };
|
|
327
|
+
}
|
|
328
|
+
catch { /* 可用 */ }
|
|
329
|
+
try {
|
|
330
|
+
await mkdir(deps.presetsDir, { recursive: true });
|
|
331
|
+
await moveOutOfTrash('agents-md', entry.id, 'preset', target);
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
return { ok: false, error: '恢复失败:' + message(e) };
|
|
335
|
+
}
|
|
336
|
+
await purgeTrashEntry('agents-md', entry.id);
|
|
337
|
+
return { ok: true, id: entry.name };
|
|
338
|
+
}
|
|
339
|
+
async function trashDelete(id) {
|
|
340
|
+
const clean = String(id ?? '').trim();
|
|
341
|
+
const gone = await purgeTrashEntry('agents-md', clean);
|
|
342
|
+
if (!gone)
|
|
343
|
+
return { ok: false, error: '回收站条目不存在:' + clean };
|
|
344
|
+
return { ok: true, id: clean };
|
|
345
|
+
}
|
|
224
346
|
// 从外部文本内容(如导入的 .md 文件)建预设:id 校验 + 重复检查 + 写 AGENTS.md。
|
|
225
347
|
async function importPreset(id, content) {
|
|
226
|
-
const
|
|
227
|
-
if (
|
|
228
|
-
return { ok: false, error: '非法 id
|
|
348
|
+
const bad = idError(id);
|
|
349
|
+
if (bad)
|
|
350
|
+
return { ok: false, error: '非法 id:' + bad };
|
|
351
|
+
const safeId = String(id ?? '').trim();
|
|
229
352
|
const dir = join(deps.presetsDir, safeId);
|
|
230
353
|
try {
|
|
231
354
|
await stat(dir);
|
|
@@ -236,5 +359,5 @@ export function createAgentsMdService(_ctx, deps) {
|
|
|
236
359
|
await writeFile(join(dir, FILENAME), String(content ?? ''), 'utf8');
|
|
237
360
|
return { ok: true, id: safeId };
|
|
238
361
|
}
|
|
239
|
-
return { list, read, create, update, apply, getCurrent, remove, importPreset };
|
|
362
|
+
return { list, read, create, update, apply, restore, getCurrent, remove, trashList, trashRestore, trashDelete, importPreset };
|
|
240
363
|
}
|