@michengai/dsh-skills-manager 0.1.31 → 0.1.32
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 +7 -0
- package/CHANGELOG.zh-CN.md +7 -0
- package/README.md +11 -11
- package/README.zh-CN.md +10 -10
- package/assets/screenshots/skills-manager-v2-preview.png +0 -0
- package/lib/client.js +29 -27
- package/lib/core.js +160 -69
- package/lib/index.js +4 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,13 @@ The five most recent published versions are listed below.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.1.32 — 2026-08-31
|
|
10
|
+
|
|
11
|
+
- Unified every per-Skill enable/disable action—user DSH, shared Agents, Codex, Claude, Gemini, OpenCode, project DSH, and project Agents—under manager-local tri-state policy; toggles never rewrite a source Skill file.
|
|
12
|
+
- Added explicit enable overrides for source-disabled or invalid invocation fields, enforced by user DSH rank 399 and project rank 99/199 policy candidates; legacy state files migrate without losing existing disabled entries.
|
|
13
|
+
- Replaced ambiguous **Loaded/Discovered** UI and API counters with **Enabled/Disabled** invocation-policy terminology; this no longer implies that a Skill body has already been fetched into a Session.
|
|
14
|
+
- Expanded i18n contract tests to cover every literal client key, every host error/warning/diagnostic code, placeholder parity, and removal of legacy loading terminology in both languages.
|
|
15
|
+
|
|
9
16
|
## 0.1.31 — 2026-08-30
|
|
10
17
|
|
|
11
18
|
- Added project Skill discovery for active Session workspaces, covering project-root `.dsh/skills` and `.agents/skills` with stable workspace-specific identities and official rank order.
|
package/CHANGELOG.zh-CN.md
CHANGED
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
## 未发布
|
|
8
8
|
|
|
9
|
+
## 0.1.32 — 2026-08-31
|
|
10
|
+
|
|
11
|
+
- 将用户 DSH、公共 Agent、Codex、Claude、Gemini、OpenCode、项目 DSH 与项目 Agent 的逐 Skill 启停全部统一为 manager 本地三态策略;启停不再改写任何来源 Skill 文件。
|
|
12
|
+
- 新增对来源原本停用或调用字段非法 Skill 的显式启用覆盖,通过用户 DSH rank 399、项目 rank 99/199 策略候选执行;旧版状态文件可无损迁移已有停用项。
|
|
13
|
+
- 将容易误解的“已加载/已发现”界面与 API 统计统一为“已启用/已停用”调用策略语义,不再暗示 Skill 正文已经进入某个 Session。
|
|
14
|
+
- 扩展 i18n 契约测试,覆盖所有客户端字面量 key、宿主错误/警告/诊断代码、占位符一致性及中英文旧加载术语清理。
|
|
15
|
+
|
|
9
16
|
## 0.1.31 — 2026-08-30
|
|
10
17
|
|
|
11
18
|
- 新增活动 Session 工作区的项目 Skill 发现,覆盖项目根 `.dsh/skills` 与 `.agents/skills`,使用稳定的 workspace 专属身份并遵循官方优先级。
|
package/README.md
CHANGED
|
@@ -23,16 +23,16 @@
|
|
|
23
23
|
## Features
|
|
24
24
|
|
|
25
25
|
- Discover and load user-level skills from `.agents`, Codex, Claude, Gemini, and OpenCode into DSH.
|
|
26
|
-
- Discover project-level `.dsh/skills` and `.agents/skills` from active Session workspaces, grouped by project;
|
|
27
|
-
- Persist
|
|
28
|
-
- Inspect Markdown bodies, frontmatter,
|
|
26
|
+
- Discover project-level `.dsh/skills` and `.agents/skills` from active Session workspaces, grouped by project; every valid Skill can be enabled or disabled through non-mutating local policy.
|
|
27
|
+
- Persist every per-Skill toggle under `$DSH_HOME/skills-manager/state.json` without rewriting any source Skill file.
|
|
28
|
+
- Inspect Markdown bodies, frontmatter, invocation state, duplicate shadowing, and format diagnostics in a source-first UI.
|
|
29
29
|
- Create a user- or project-level DSH skill from Settings; conversational creation remains user-level and requires approval.
|
|
30
30
|
- Move user- and project-level DSH skills to recoverable Trash, then restore them to the original source or permanently delete them.
|
|
31
31
|
- Import `.zip` archives, skill folders, or a single `SKILL.md` safely into `$DSH_HOME/skills`.
|
|
32
32
|
|
|
33
33
|
## Screenshots
|
|
34
34
|
|
|
35
|
-
Browse by source or search in **Settings → Skills**. External Agent sources are
|
|
35
|
+
Browse by source or search in **Settings → Skills**. External Agent sources are made available through manager policy while their files stay read-only:
|
|
36
36
|
|
|
37
37
|

|
|
38
38
|
|
|
@@ -137,12 +137,12 @@ Open **Settings → Skills**, then use the panel as follows:
|
|
|
137
137
|
| --- | --- | --- |
|
|
138
138
|
| Search or filter | Narrow by source, name, or description. | All sources |
|
|
139
139
|
| Inspect details | Review body, frontmatter, path, format diagnostics, and duplicate shadowing. | All sources |
|
|
140
|
-
| Enable or disable |
|
|
140
|
+
| Enable or disable | Update manager-local invocation policy without modifying the source Skill file. | All valid Skills in user and active-project sources |
|
|
141
141
|
| Create or import | Choose a user or active project DSH destination when creating in Settings; imports remain user-level. | `$DSH_HOME/skills`, active `<project>/.dsh/skills` for creation |
|
|
142
142
|
| Create from conversation | Let an Agent call `create_skill`; DSH asks for approval before writing. | `$DSH_HOME/skills` |
|
|
143
143
|
| Delete and recover | Move to Trash, restore to the original source, or permanently delete in a second step. | User and active-project DSH skills |
|
|
144
144
|
|
|
145
|
-
> Toggling a
|
|
145
|
+
> Toggling never changes a source Skill file; only user or project DSH skills can move to Trash.
|
|
146
146
|
|
|
147
147
|
Escape closes only the frontmost upload or confirmation dialog and leaves Settings open.
|
|
148
148
|
|
|
@@ -150,19 +150,19 @@ Escape closes only the frontmost upload or confirmation dialog and leaves Settin
|
|
|
150
150
|
|
|
151
151
|
| Directory | View/load | Enable or disable | Create/import | Delete |
|
|
152
152
|
| --- | --- | --- | --- | --- |
|
|
153
|
-
| `$DSH_HOME\skills` | Yes |
|
|
153
|
+
| `$DSH_HOME\skills` | Yes | Manager state only | Yes | Moves to Trash |
|
|
154
154
|
| `$DSH_AGENTS_HOME\skills` | Yes | Manager state only | No | No |
|
|
155
155
|
| `~/.codex/skills`, `~/.claude/skills`, `~/.gemini/skills`, `~/.config/opencode/skills` | Yes | Manager state only | No | No |
|
|
156
|
-
| `<project>/.dsh/skills` | Yes, for active Session workspaces |
|
|
157
|
-
| `<project>/.agents/skills` | Yes, for active Session workspaces |
|
|
156
|
+
| `<project>/.dsh/skills` | Yes, for active Session workspaces | Manager state only | Create in Settings | Moves to Trash and restores to the original project |
|
|
157
|
+
| `<project>/.agents/skills` | Yes, for active Session workspaces | Manager state only | No | No |
|
|
158
158
|
|
|
159
159
|
- Enable, disable, and delete accept only one ordinary skill-name path segment.
|
|
160
160
|
- Project roots are derived only from active Session `cwd` values; client requests carry an opaque source key and cannot nominate an arbitrary workspace path.
|
|
161
|
-
- Project sources follow DSH's nearest-`.git` root convention and rank order (`project-dsh` 100 before `project-agents` 200). The manager re-scans for each state/detail request
|
|
161
|
+
- Project sources follow DSH's nearest-`.git` root convention and rank order (`project-dsh` 100 before `project-agents` 200). The manager re-scans for each state/detail request. Explicit project policy is enforced by workspace-scoped rank 99/199 overlays; user DSH policy uses rank 399. With no override, DSH's official provider remains the owner. Toggle writes are limited to manager state; project file writes occur only for explicit create/Trash/restore actions under `.dsh/skills`.
|
|
162
162
|
- Trash falls back to copy-then-hide when a project and `$DSH_HOME` are on different volumes; restore uses the same guarded cross-volume path in reverse.
|
|
163
163
|
- Project Trash entries retain their original opaque source identity. Restore is allowed only while that original project is still represented by an active Session workspace; the client cannot nominate a replacement path.
|
|
164
164
|
- Project writes reject linked `.dsh` or `.dsh/skills` directories so a repository cannot redirect creation, deletion, or restore outside its own project root.
|
|
165
|
-
-
|
|
165
|
+
- Rows and summaries say **Enabled/Disabled**, not **Loaded**: these labels describe invocation policy, while full Skill bodies are loaded on demand by DSH. Use Refresh after IDE, Git, or shell changes; the official provider remains responsible for project catalog watching and invalidation.
|
|
166
166
|
- Empty project roots stay out of the main source list to reduce noise, but remain selectable in Create Skill so the first project Skill can still be created. Project DSH supports per-Skill toggles only, not a source-wide switch.
|
|
167
167
|
- Replacements copy to a temporary sibling path first and keep the original until that succeeds.
|
|
168
168
|
- Every endpoint, including GET `/state`, accepts only a loopback `Host` or a canonical `host[:port]` that the DSH Web runtime already trusts through its LAN bind and `--trusted-host`; unknown hosts still receive 403.
|
package/README.zh-CN.md
CHANGED
|
@@ -23,9 +23,9 @@
|
|
|
23
23
|
## 功能概览
|
|
24
24
|
|
|
25
25
|
- 自动发现并把 `.agents`、Codex、Claude、Gemini 和 OpenCode 的用户级技能真正加载进 DSH。
|
|
26
|
-
- 从活动 Session 工作区发现项目级 `.dsh/skills` 与 `.agents/skills`
|
|
27
|
-
-
|
|
28
|
-
- 按来源折叠、搜索和筛选,并查看技能正文、frontmatter
|
|
26
|
+
- 从活动 Session 工作区发现项目级 `.dsh/skills` 与 `.agents/skills` 并按项目分组;所有有效 Skill 都可通过不改源文件的本地策略启停。
|
|
27
|
+
- 所有逐 Skill 启停都只写入 `$DSH_HOME\skills-manager\state.json`,绝不改写任何来源 Skill 文件。
|
|
28
|
+
- 按来源折叠、搜索和筛选,并查看技能正文、frontmatter、调用状态、重名遮蔽与格式诊断。
|
|
29
29
|
- 在设置页创建用户级或项目级 DSH Skill;对话创建仍为用户级,并在写入前请求确认。
|
|
30
30
|
- 用户级和项目级 DSH Skill 删除后先进入回收站,可恢复到原来源或二次永久删除。
|
|
31
31
|
- 支持把 `.zip`、包含 `SKILL.md` 的技能文件夹或单个 `SKILL.md` 安全导入 `$DSH_HOME\skills`。
|
|
@@ -137,12 +137,12 @@ dsh --profile web --dump-config
|
|
|
137
137
|
| --- | --- | --- |
|
|
138
138
|
| 搜索或筛选 | 按来源、名称或简介收窄列表。 | 全部来源 |
|
|
139
139
|
| 查看详情与诊断 | 查看正文、frontmatter、源文件路径、格式问题和重名遮蔽。 | 全部来源 |
|
|
140
|
-
| 启用或停用 |
|
|
140
|
+
| 启用或停用 | 只更新 manager 本地调用策略,不修改来源 Skill 文件。 | 全部用户级来源与活动项目来源中的有效 Skill |
|
|
141
141
|
| 创建或导入 | 在设置页创建时选择用户 DSH 或活动项目 DSH;导入仍为用户级。 | `$DSH_HOME\skills`,创建可选活动 `<project>/.dsh/skills` |
|
|
142
142
|
| 从对话创建 | 让 Agent 调用 `create_skill`;写入前由 DSH 审批界面确认。 | `$DSH_HOME\skills` |
|
|
143
143
|
| 删除与恢复 | 删除先进入回收站;可恢复到原来源或永久二次删除。 | 用户级与活动项目级 DSH Skill |
|
|
144
144
|
|
|
145
|
-
>
|
|
145
|
+
> 任何来源的启停都不会修改 Skill 文件;只有用户级或项目级 DSH Skill 可以移入回收站。
|
|
146
146
|
|
|
147
147
|
按 ESC 只关闭最上层上传框或确认框,设置页会保持打开。
|
|
148
148
|
|
|
@@ -150,19 +150,19 @@ dsh --profile web --dump-config
|
|
|
150
150
|
|
|
151
151
|
| 目录 | 查看/加载 | 启用或停用 | 创建/导入 | 删除 |
|
|
152
152
|
| --- | --- | --- | --- | --- |
|
|
153
|
-
| `$DSH_HOME\skills` | 支持 |
|
|
153
|
+
| `$DSH_HOME\skills` | 支持 | 仅写 manager 状态 | 支持 | 进入回收站 |
|
|
154
154
|
| `$DSH_AGENTS_HOME\skills` | 支持 | 仅写 manager 状态 | 不支持 | 不支持 |
|
|
155
155
|
| `~/.codex/skills`、`~/.claude/skills`、`~/.gemini/skills`、`~/.config/opencode/skills` | 支持 | 仅写 manager 状态 | 不支持 | 不支持 |
|
|
156
|
-
| `<project>/.dsh/skills` | 支持活动 Session 工作区 |
|
|
157
|
-
| `<project>/.agents/skills` | 支持活动 Session 工作区 |
|
|
156
|
+
| `<project>/.dsh/skills` | 支持活动 Session 工作区 | 仅写 manager 状态 | 设置页支持创建 | 进入回收站并恢复到原项目 |
|
|
157
|
+
| `<project>/.agents/skills` | 支持活动 Session 工作区 | 仅写 manager 状态 | 不支持 | 不支持 |
|
|
158
158
|
|
|
159
159
|
- 启用、停用和删除只接受单个普通技能名称,目录穿越名称会被拒绝。
|
|
160
160
|
- 项目根只从活动 Session 的 `cwd` 推导;客户端只提交不透明来源 key,不能指定任意工作区路径。
|
|
161
|
-
- 项目来源遵循 DSH 最近 `.git` 项目根和固定优先级(`project-dsh` 100、`project-agents` 200
|
|
161
|
+
- 项目来源遵循 DSH 最近 `.git` 项目根和固定优先级(`project-dsh` 100、`project-agents` 200)。管理器每次读取状态或详情时重新扫描;显式项目策略通过 workspace 作用域 rank 99/199 覆盖候选执行,用户 DSH 策略使用 rank 399;没有覆盖时仍由 DSH 官方 provider 负责。启停只写 manager 状态,项目文件写入仅发生在用户明确执行 `.dsh/skills` 创建、回收或恢复时。
|
|
162
162
|
- 当项目与 `$DSH_HOME` 位于不同磁盘时,回收站会降级为“复制后在源盘原子隐藏”;恢复使用反向的同一安全流程。
|
|
163
163
|
- 项目回收站条目保存原始不透明来源身份。仅当原项目仍由活动 Session 工作区提供时才允许恢复;客户端不能指定替代路径。
|
|
164
164
|
- 项目写入会拒绝链接形式的 `.dsh` 或 `.dsh/skills` 目录,避免仓库把创建、删除或恢复重定向到项目根之外。
|
|
165
|
-
-
|
|
165
|
+
- 列表和摘要统一使用“已启用/已停用”,不再使用“已加载”:这里描述的是调用策略,完整 Skill 正文仍由 DSH 按需加载。IDE、Git 或 shell 改动后可点击“刷新”;项目 catalog 的 watcher 与 invalidation 仍由官方 provider 负责。
|
|
166
166
|
- 没有 Skill 的项目根不会出现在主来源列表中,但仍保留在“创建技能”的目标选择中,确保可以创建第一个项目 Skill。项目 DSH 只提供逐 Skill 启停,不提供整个项目来源总开关。
|
|
167
167
|
- 覆盖前先复制到同目录临时路径;复制成功前不会改动现有技能。
|
|
168
168
|
- 全部接口(含 GET `/state`)只接受 loopback `Host`,或 DSH Web runtime 已通过 LAN 绑定和 `--trusted-host` 明确信任的 `host[:port]`;未知 Host 继续返回 403。
|
|
Binary file
|
package/lib/client.js
CHANGED
|
@@ -12,20 +12,20 @@ window.__ModuleLoader__.load({
|
|
|
12
12
|
var DICT = {
|
|
13
13
|
zh: {
|
|
14
14
|
"title": "技能", "desc": "统一加载和管理本机 Agent Skills。",
|
|
15
|
-
"btn.create": "创建技能", "btn.import": "导入", "btn.refresh": "刷新", "btn.cancel": "取消", "btn.close": "关闭", "btn.detail": "查看详情", "btn.trash": "移到回收站", "btn.restore": "恢复", "btn.delete.forever": "永久删除", "btn.file.pick": "选择文件", "btn.folder.pick": "选择文件夹", "btn.import.now": "安装", "btn.create.now": "创建技能", "btn.disable": "停用", "btn.enable": "启用",
|
|
16
|
-
"status.enabled": "
|
|
17
|
-
"summary.total": "{count} 个技能", "summary.
|
|
15
|
+
"btn.create": "创建技能", "btn.import": "导入", "btn.refresh": "刷新", "btn.cancel": "取消", "btn.close": "关闭", "btn.detail": "查看详情", "btn.trash": "移到回收站", "btn.restore": "恢复", "btn.delete.forever": "永久删除", "btn.file.pick": "选择文件", "btn.folder.pick": "选择文件夹", "btn.import.now": "安装", "btn.create.now": "创建技能", "btn.disable": "停用", "btn.enable": "启用",
|
|
16
|
+
"status.enabled": "已启用", "status.disabled": "已停用", "status.invalid": "诊断异常", "status.shadowed": "被覆盖", "status.readonly": "源文件只读", "status.manageable": "可管理", "status.project": "项目级", "status.rank": "优先级 {rank}", "status.source.on": "已启用", "status.source.off": "已停用", "status.bundle": "目录技能", "status.single": "单文件",
|
|
17
|
+
"summary.total.one": "{count} 个技能", "summary.total.other": "{count} 个技能", "summary.enabled.one": "{count} 个已启用", "summary.enabled.other": "{count} 个已启用", "summary.disabled.one": "{count} 个已停用", "summary.disabled.other": "{count} 个已停用", "summary.issues.one": "{count} 个诊断项", "summary.issues.other": "{count} 个诊断项", "summary.group.one": "{count} 个技能", "summary.group.other": "{count} 个技能", "table.skill": "技能名称与描述", "table.status": "调用状态",
|
|
18
18
|
"filter.source": "来源", "filter.all": "全部来源", "filter.option": "{name}({count})", "search": "搜索", "search.placeholder": "搜索技能名称或描述", "search.clear": "清除搜索",
|
|
19
19
|
"empty.search": "没有匹配的技能。", "empty.source": "该来源目录不存在或暂时没有技能。", "loading": "正在加载技能…", "note.missing": "未提供简介", "source.toggle": "启停来源", "skill.toggle": "启停技能",
|
|
20
|
-
"source.external.note": "通过 Skills Manager
|
|
20
|
+
"source.external.note": "通过 Skills Manager 接入,启停不会改写源文件。", "source.dsh.note": "DSH 本地技能可创建、导入和移到回收站;启停只更新管理器状态。",
|
|
21
21
|
"detail.title": "技能详情", "detail.body": "正文", "detail.frontmatter": "Frontmatter", "detail.diagnostics": "诊断", "detail.path": "源文件", "detail.noIssues": "未发现诊断问题。",
|
|
22
22
|
"create.title": "创建技能", "create.target": "创建位置", "create.name": "名称", "create.name.placeholder": "例如 code-review-helper", "create.description": "简介", "create.description.placeholder": "一句话说明什么时候使用", "create.body": "正文(Markdown)", "create.body.placeholder": "写下技能要遵循的指令、步骤和边界…", "create.chat.note": "对话中的 create_skill 仍创建用户级 DSH Skill;项目 Skill 可在这里选择活动项目后创建。",
|
|
23
|
-
"import.title": "导入技能", "upload.drop.title": "拖拽技能到此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.importing": "正在安装…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md。", "select.folder.invalid": "所选文件夹中没有找到 SKILL.md。", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
|
|
24
|
-
"trash.title": "回收站", "trash.count": "{count} 个待处理技能", "trash.empty": "回收站为空。", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
|
|
23
|
+
"import.title": "导入技能", "upload.drop.title": "拖拽技能到此处", "upload.drop.copy": "支持 .zip、技能文件夹或单个 SKILL.md", "upload.selected.one": "{count} 个文件 · {size}", "upload.selected.other": "{count} 个文件 · {size}", "upload.remove": "移除所选内容", "upload.requirements": "文件要求", "upload.requirement.skill": "压缩包或文件夹需包含 SKILL.md", "upload.requirement.frontmatter": "SKILL.md 需包含 YAML 格式的技能名称和描述", "upload.requirement.copy": "导入时复制完整内容,不修改原始来源", "upload.importing": "正在安装…", "status.selected": "已选择", "select.file.invalid": "请选择 .zip 或单个 SKILL.md。", "select.folder.invalid": "所选文件夹中没有找到 SKILL.md。", "error.browse.absolute": "目录路径必须是绝对路径:{path}", "error.browse.unreadable": "无法读取目录:{path}", "error.browse.notDirectory": "不是目录:{path}",
|
|
24
|
+
"trash.title": "回收站", "trash.count.one": "{count} 个待处理技能", "trash.count.other": "{count} 个待处理技能", "trash.empty": "回收站为空。", "trash.deletedAt": "删除于 {time}", "trash.source": "来源:{source}",
|
|
25
25
|
"confirm.trash.title": "移到回收站?", "confirm.trash.desc": "“{name}”将从当前技能来源移入回收站,之后可以恢复到原位置。", "confirm.delete.title": "永久删除?", "confirm.delete.desc": "“{name}”将从回收站永久删除,无法恢复。",
|
|
26
26
|
"result.created": "已创建技能:{name}", "result.imported": "导入完成:{names}", "result.importPartial": "已导入:{imported};已跳过同名技能:{skipped}", "result.importSkipped": "未导入任何技能;已跳过同名技能:{names}", "result.importEmpty": "未导入任何技能。", "result.importWarnings": "{result};警告:{warnings}", "result.restored": "已恢复技能:{name}", "result.trashed": "已移到回收站:{name}", "result.deleted": "已永久删除:{name}", "result.updated": "状态已更新。", "error.action": "操作失败:{error}",
|
|
27
|
-
"warning.state.invalid": "
|
|
28
|
-
"error.root.readonly": "该来源不允许{action}", "error.root.unknown": "未知技能来源:{root}", "error.root.unsafe": "项目技能目录不安全,拒绝写入:{path}", "error.skill.notFound": "技能不存在: {name}", "error.skill.noFrontmatter": "技能缺少完整 frontmatter,无法{action}: {name}",
|
|
27
|
+
"warning.state.invalid": "技能管理器状态文件不可读;所有技能已安全停用,修复文件前不会覆盖状态:{path}", "warning.backupUncleaned": "旧版本备份未清理:{path}({error})", "warning.project.unavailable": "无法从宿主读取活动工作区,项目技能未显示:{path}",
|
|
28
|
+
"error.root.readonly": "该来源不允许{action}", "error.root.unknown": "未知技能来源:{root}", "error.root.unsafe": "项目技能目录不安全,拒绝写入:{path}", "error.skill.notFound": "技能不存在: {name}", "error.skill.noFrontmatter": "技能缺少完整 frontmatter,无法{action}: {name}", "error.skill.notLoadable": "技能结构不完整,无法{action}: {name}",
|
|
29
29
|
"error.source.notFound": "路径不存在: {path}", "error.source.symlink": "不支持包含符号链接的 skill 来源: {path}", "error.source.unrecognized": "无法识别的 skill 来源: {path}", "error.source.tooDeep": "skill 来源目录层级超过 {depth} 层: {path}",
|
|
30
30
|
"error.import.overlap": "导入来源不能与 DSH 技能目录相同、包含或位于其中", "error.import.emptySource": "目录下未找到任何 skill 条目: {path}", "error.import.invalidName": "无法生成合法 kebab-case 名称(原始名: {name})", "error.import.duplicateName": "批量来源中存在多个同名技能: {name}", "error.import.failed": "导入失败", "error.import.rollbackFailed": "覆盖导入回滚失败,备份保留在: {path}({error})",
|
|
31
31
|
"error.upload.path": "上传内容包含非法路径:{path}", "error.upload.encoding": "上传内容编码无效", "error.upload.empty": "上传内容为空", "error.upload.tooMany": "上传文件过多,最多 {limit} 个", "error.upload.tooLarge": "上传内容过大,限制为 {limit} 字节", "error.upload.archiveTooLarge": "ZIP 压缩包过大,限制为 {limit} 字节", "error.upload.duplicate": "上传内容包含重复路径:{path}", "error.upload.zipInvalid": "ZIP 压缩包无法解压",
|
|
@@ -39,20 +39,20 @@ window.__ModuleLoader__.load({
|
|
|
39
39
|
},
|
|
40
40
|
en: {
|
|
41
41
|
"title": "Skills", "desc": "Load and manage Agent Skills on this computer in one place.",
|
|
42
|
-
"btn.create": "Create skill", "btn.import": "Import", "btn.refresh": "Refresh", "btn.cancel": "Cancel", "btn.close": "Close", "btn.detail": "View details", "btn.trash": "Move to trash", "btn.restore": "Restore", "btn.delete.forever": "Delete forever", "btn.file.pick": "Choose file", "btn.folder.pick": "Choose folder", "btn.import.now": "Install", "btn.create.now": "Create skill", "btn.disable": "Disable", "btn.enable": "Enable",
|
|
43
|
-
"status.enabled": "
|
|
44
|
-
"summary.total": "{count} skills", "summary.
|
|
42
|
+
"btn.create": "Create skill", "btn.import": "Import", "btn.refresh": "Refresh", "btn.cancel": "Cancel", "btn.close": "Close", "btn.detail": "View details", "btn.trash": "Move to trash", "btn.restore": "Restore", "btn.delete.forever": "Delete forever", "btn.file.pick": "Choose file", "btn.folder.pick": "Choose folder", "btn.import.now": "Install", "btn.create.now": "Create skill", "btn.disable": "Disable", "btn.enable": "Enable",
|
|
43
|
+
"status.enabled": "Enabled", "status.disabled": "Disabled", "status.invalid": "Needs attention", "status.shadowed": "Shadowed", "status.readonly": "Source read-only", "status.manageable": "Manageable", "status.project": "Project scoped", "status.rank": "Rank {rank}", "status.source.on": "Enabled", "status.source.off": "Disabled", "status.bundle": "Bundle", "status.single": "Single file",
|
|
44
|
+
"summary.total.one": "{count} skill", "summary.total.other": "{count} skills", "summary.enabled.one": "{count} enabled", "summary.enabled.other": "{count} enabled", "summary.disabled.one": "{count} disabled", "summary.disabled.other": "{count} disabled", "summary.issues.one": "{count} diagnostic", "summary.issues.other": "{count} diagnostics", "summary.group.one": "{count} skill", "summary.group.other": "{count} skills", "table.skill": "Skill name and description", "table.status": "Invocation status",
|
|
45
45
|
"filter.source": "Source", "filter.all": "All sources", "filter.option": "{name} ({count})", "search": "Search", "search.placeholder": "Search skill names or descriptions", "search.clear": "Clear search",
|
|
46
46
|
"empty.search": "No matching skills.", "empty.source": "This source does not exist or has no skills yet.", "loading": "Loading skills…", "note.missing": "No description provided", "source.toggle": "Toggle source", "skill.toggle": "Toggle skill",
|
|
47
|
-
"source.external.note": "
|
|
47
|
+
"source.external.note": "Managed through Skills Manager; toggles never rewrite source files.", "source.dsh.note": "Local DSH skills can be created, imported, and moved to trash; toggles update manager state only.",
|
|
48
48
|
"detail.title": "Skill details", "detail.body": "Body", "detail.frontmatter": "Frontmatter", "detail.diagnostics": "Diagnostics", "detail.path": "Source file", "detail.noIssues": "No diagnostic issues found.",
|
|
49
49
|
"create.title": "Create skill", "create.target": "Create in", "create.name": "Name", "create.name.placeholder": "e.g. code-review-helper", "create.description": "Description", "create.description.placeholder": "One sentence describing when to use it", "create.body": "Body (Markdown)", "create.body.placeholder": "Write the instructions, steps, and boundaries…", "create.chat.note": "The conversational create_skill tool still creates a user-level DSH Skill; choose an active project here for a project Skill.",
|
|
50
|
-
"import.title": "Import skill", "upload.drop.title": "Drop a skill here", "upload.drop.copy": "Supports .zip, a skill folder, or one SKILL.md", "upload.selected": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.importing": "Installing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
|
|
51
|
-
"trash.title": "Trash", "trash.count": "{count} skills pending", "trash.empty": "Trash is empty.", "trash.deletedAt": "Deleted {time}", "trash.source": "Source: {source}",
|
|
50
|
+
"import.title": "Import skill", "upload.drop.title": "Drop a skill here", "upload.drop.copy": "Supports .zip, a skill folder, or one SKILL.md", "upload.selected.one": "{count} file · {size}", "upload.selected.other": "{count} files · {size}", "upload.remove": "Remove selection", "upload.requirements": "File requirements", "upload.requirement.skill": "Archives and folders must contain SKILL.md", "upload.requirement.frontmatter": "SKILL.md must include a YAML name and description", "upload.requirement.copy": "Import copies all content and never modifies the source", "upload.importing": "Installing…", "status.selected": "Selected", "select.file.invalid": "Choose a .zip archive or one SKILL.md.", "select.folder.invalid": "No SKILL.md was found in the selected folder.", "error.browse.absolute": "Folder path must be absolute: {path}", "error.browse.unreadable": "Could not read folder: {path}", "error.browse.notDirectory": "Not a folder: {path}",
|
|
51
|
+
"trash.title": "Trash", "trash.count.one": "{count} skill pending", "trash.count.other": "{count} skills pending", "trash.empty": "Trash is empty.", "trash.deletedAt": "Deleted {time}", "trash.source": "Source: {source}",
|
|
52
52
|
"confirm.trash.title": "Move to trash?", "confirm.trash.desc": "“{name}” will move out of its current skill source and can be restored to the same location later.", "confirm.delete.title": "Delete forever?", "confirm.delete.desc": "“{name}” will be permanently deleted from trash and cannot be recovered.",
|
|
53
53
|
"result.created": "Created skill: {name}", "result.imported": "Import complete: {names}", "result.importPartial": "Imported: {imported}; skipped existing skills: {skipped}", "result.importSkipped": "No skills were imported; existing skills were skipped: {names}", "result.importEmpty": "No skills were imported.", "result.importWarnings": "{result}; warnings: {warnings}", "result.restored": "Restored skill: {name}", "result.trashed": "Moved to trash: {name}", "result.deleted": "Permanently deleted: {name}", "result.updated": "Status updated.", "error.action": "Action failed: {error}",
|
|
54
|
-
"warning.state.invalid": "The manager state file could not be read;
|
|
55
|
-
"error.root.readonly": "This source does not allow {action}", "error.root.unknown": "Unknown skill source: {root}", "error.root.unsafe": "The project skill directory is unsafe, so the write was refused: {path}", "error.skill.notFound": "Skill not found: {name}", "error.skill.noFrontmatter": "Skill lacks complete frontmatter, cannot {action}: {name}",
|
|
54
|
+
"warning.state.invalid": "The manager state file could not be read; all skills are disabled and state writes are blocked until it is repaired: {path}", "warning.backupUncleaned": "Old version backup was not cleaned up: {path} ({error})", "warning.project.unavailable": "The active workspace could not be read from the host, so its project skills are hidden: {path}",
|
|
55
|
+
"error.root.readonly": "This source does not allow {action}", "error.root.unknown": "Unknown skill source: {root}", "error.root.unsafe": "The project skill directory is unsafe, so the write was refused: {path}", "error.skill.notFound": "Skill not found: {name}", "error.skill.noFrontmatter": "Skill lacks complete frontmatter, cannot {action}: {name}", "error.skill.notLoadable": "Skill structure is incomplete, cannot {action}: {name}",
|
|
56
56
|
"error.source.notFound": "Path does not exist: {path}", "error.source.symlink": "Skill sources containing symbolic links are not supported: {path}", "error.source.unrecognized": "Unrecognized skill source: {path}", "error.source.tooDeep": "Skill source directory depth exceeds {depth} levels: {path}",
|
|
57
57
|
"error.import.overlap": "Import source cannot be the same as, contain, or be inside the DSH skills directory", "error.import.emptySource": "No skill entries found in the directory: {path}", "error.import.invalidName": "Cannot generate a valid kebab-case name (original: {name})", "error.import.duplicateName": "Batch source contains duplicate skill names: {name}", "error.import.failed": "Import failed", "error.import.rollbackFailed": "Overwrite import rollback failed; backups kept at: {path} ({error})",
|
|
58
58
|
"error.upload.path": "Upload contains an invalid path: {path}", "error.upload.encoding": "Upload encoding is invalid", "error.upload.empty": "Upload is empty", "error.upload.tooMany": "Too many uploaded files; maximum {limit}", "error.upload.tooLarge": "Upload is too large; limit {limit} bytes", "error.upload.archiveTooLarge": "ZIP archive is too large; limit {limit} bytes", "error.upload.duplicate": "Upload contains a duplicate path: {path}", "error.upload.zipInvalid": "ZIP archive could not be extracted",
|
|
@@ -72,7 +72,7 @@ window.__ModuleLoader__.load({
|
|
|
72
72
|
.dssm-summary{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dssm-stat{padding:12px 14px;border-right:1px solid var(--dsw-alias-border-l1);font-size:13px;color:var(--dsw-alias-label-secondary)}.dssm-stat:last-child{border-right:0}.dssm-stat strong{margin-right:5px;color:var(--dsw-alias-label-primary);font-size:17px;font-weight:680}.dssm-filters{display:flex;gap:9px}.dssm-search{flex:1}.dssm-source-filter{width:210px;flex:none}
|
|
73
73
|
.dssm-control,.dssm-select-trigger{box-sizing:border-box;width:100%;min-height:34px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary);font:inherit;font-size:13px}.dssm-control::placeholder{color:var(--dsw-alias-label-tertiary)}textarea.dssm-control{min-height:160px;padding-top:9px;resize:vertical;line-height:20px}.dssm-select{position:relative}.dssm-select-trigger{display:flex;align-items:center;justify-content:space-between;text-align:left;cursor:pointer}.dssm-select-menu{position:absolute;z-index:40;top:calc(100% + 5px);right:0;left:0;display:flex;max-height:260px;padding:5px;overflow:auto;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-3);box-shadow:var(--dsw-shadow-lv2)}.dssm-option{padding:8px;border:0;border-radius:6px;background:transparent;color:var(--dsw-alias-label-primary);font:inherit;font-size:13px;text-align:left;cursor:pointer}.dssm-option:hover,.dssm-option[aria-selected=true]{background:var(--dsw-alias-interactive-bg-hover)}
|
|
74
74
|
.dssm-sources{display:flex;flex-direction:column;gap:9px}.dssm-source{overflow:hidden;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2)}.dssm-source-head{box-sizing:border-box;display:flex;width:100%;min-height:48px;align-items:center;padding:0 13px}.dssm-source-head-main{display:flex;min-width:0;min-height:48px;flex:1;align-items:center;gap:10px;padding:0;border:0;background:transparent;color:inherit;font:inherit;text-align:left;cursor:pointer}.dssm-source-head:hover,.dssm-row:hover,.dssm-trash-row:hover{background:var(--dsw-alias-interactive-bg-hover)}.dssm-source-title{font-size:14px;font-weight:650}.dssm-count{color:var(--dsw-alias-label-tertiary);font-size:12px}.dssm-path{min-width:0;margin-left:auto;overflow:hidden;color:var(--dsw-alias-label-tertiary);font-size:11px;text-overflow:ellipsis;white-space:nowrap}.dssm-source-actions{display:flex;align-items:center;gap:9px;margin-left:8px}.dssm-source-body{border-top:1px solid var(--dsw-alias-border-l1)}.dssm-source-note{padding:9px 13px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}
|
|
75
|
-
.dssm-table-head,.dssm-row{display:grid;grid-template-columns:minmax(180px,1fr) 120px 90px max-content;align-items:center;column-gap:12px;padding:0 13px}.dssm-table-head{min-height:32px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px}.dssm-row{min-height:58px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-row:last-child{border-bottom:0}.dssm-main{min-width:0}.dssm-name{overflow:hidden;font-size:13px;font-weight:570;text-overflow:ellipsis;white-space:nowrap}.dssm-note{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dssm-tags{display:flex;align-items:center;gap:5px;flex-wrap:wrap}.dssm-tag{display:inline-flex;min-height:19px;align-items:center;padding:0 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:10px;white-space:nowrap}.dssm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dssm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dssm-
|
|
75
|
+
.dssm-table-head,.dssm-row{display:grid;grid-template-columns:minmax(180px,1fr) 120px 90px max-content;align-items:center;column-gap:12px;padding:0 13px}.dssm-table-head{min-height:32px;border-bottom:1px solid var(--dsw-alias-border-l1);color:var(--dsw-alias-label-tertiary);font-size:11px}.dssm-row{min-height:58px;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-row:last-child{border-bottom:0}.dssm-main{min-width:0}.dssm-name{overflow:hidden;font-size:13px;font-weight:570;text-overflow:ellipsis;white-space:nowrap}.dssm-note{overflow:hidden;margin-top:2px;color:var(--dsw-alias-label-secondary);font-size:11px;line-height:17px;text-overflow:ellipsis;white-space:nowrap}.dssm-tags{display:flex;align-items:center;gap:5px;flex-wrap:wrap}.dssm-tag{display:inline-flex;min-height:19px;align-items:center;padding:0 6px;border:1px solid var(--dsw-alias-border-l3);border-radius:4px;color:var(--dsw-alias-label-secondary);font-size:10px;white-space:nowrap}.dssm-tag-on{border-color:var(--dsw-alias-state-success-primary);color:var(--dsw-alias-state-success-primary)}.dssm-tag-off{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}.dssm-enabled{color:var(--dsw-alias-state-success-primary);font-size:12px;white-space:nowrap}.dssm-disabled{color:#d49245;font-size:12px;white-space:nowrap}.dssm-shadowed{color:var(--dsw-alias-label-tertiary);font-size:12px;white-space:nowrap}.dssm-row-actions{display:flex;align-items:center;justify-content:flex-end;gap:7px}
|
|
76
76
|
.dssm-switch{position:relative;width:34px;height:20px;flex:none;padding:0;border:0;border-radius:999px;background:var(--dsw-alias-border-l3);cursor:pointer}.dssm-switch:after{position:absolute;top:3px;left:3px;width:14px;height:14px;border-radius:50%;background:#fff;content:"";transition:transform 160ms ease}.dssm-switch-on{background:var(--dsw-alias-state-success-primary)}.dssm-switch-on:after{transform:translateX(14px)}.dssm-switch:disabled{opacity:.45;cursor:default}.dssm-trash-row{display:flex;min-height:48px;align-items:center;padding:0 13px;border:1px solid var(--dsw-alias-border-l2);border-radius:10px;background:var(--dsw-alias-bg-layer-2);color:inherit;font:inherit;font-size:13px;cursor:pointer}.dssm-trash-count{margin-left:auto;padding:2px 7px;border-radius:99px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:11px}.dssm-empty{padding:25px 14px;color:var(--dsw-alias-label-tertiary);font-size:12px;text-align:center}.dssm-feedback{padding:9px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;color:var(--dsw-alias-label-secondary);font-size:12px}.dssm-warning{border-color:#d49245;color:#d49245}.dssm-error{border-color:var(--dsw-alias-state-error-primary);color:var(--dsw-alias-state-error-primary)}
|
|
77
77
|
.dssm-mask{position:fixed;z-index:1100;inset:0;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(0,0,0,.62)}.dssm-modal{box-sizing:border-box;display:flex;width:min(560px,100%)!important;max-height:min(760px,calc(100vh - 48px));min-width:0;flex-direction:column;gap:16px;padding:22px;overflow:auto;border:1px solid var(--dsw-alias-border-l2);border-radius:14px;background:var(--dsw-alias-bg-layer-2);box-shadow:var(--dsw-shadow-lv3)}.dssm-modal-wide{width:min(720px,100%)!important}.dssm-modal-import{width:min(480px,100%)!important;padding:24px}.dssm-modal-head{display:flex;align-items:flex-start;gap:12px}.dssm-modal-title{margin:0;flex:1;font-size:17px;line-height:24px;font-weight:670}.dssm-form,.dssm-field,.dssm-detail-section{display:flex;flex-direction:column}.dssm-form{gap:12px}.dssm-field{gap:6px}.dssm-label,.dssm-detail-title{font-size:12px}.dssm-label{color:var(--dsw-alias-label-secondary)}.dssm-detail-title{font-weight:650}.dssm-help{margin:0;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:18px}.dssm-modal-actions{display:flex;justify-content:flex-end;flex-wrap:wrap;gap:8px}.dssm-hidden-input{display:none}.dssm-dropzone{box-sizing:border-box;display:flex;width:100%;min-height:170px;flex-direction:column;align-items:center;justify-content:center;gap:7px;padding:22px;border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);transition:border-color 180ms ease,background 180ms ease}.dssm-dropzone:hover,.dssm-dropzone-active{border-color:var(--dsw-alias-state-success-primary);background:var(--dsw-alias-interactive-bg-hover)}.dssm-dropzone-title{color:var(--dsw-alias-label-primary);font-size:14px;font-weight:620}.dssm-dropzone-copy{font-size:12px;line-height:18px;text-align:center}.dssm-upload-choices{display:flex;align-items:center;gap:7px}.dssm-upload-link,.dssm-file-remove{padding:0;border:0;background:transparent;font:inherit;font-size:12px;cursor:pointer}.dssm-upload-link{color:var(--dsw-alias-label-secondary)}.dssm-upload-link:hover{color:var(--dsw-alias-label-primary);text-decoration:underline}.dssm-upload-link:disabled{opacity:.45;cursor:default}.dssm-upload-divider{color:var(--dsw-alias-label-tertiary);font-size:11px}.dssm-file{display:flex;align-items:center;gap:9px;padding:10px 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:9px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);font-size:12px}.dssm-file-kind{display:inline-flex;min-width:30px;height:24px;align-items:center;justify-content:center;border-radius:5px;background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-secondary);font-size:9px;font-weight:700}.dssm-file-name{min-width:0;overflow:hidden;flex:1;text-overflow:ellipsis;white-space:nowrap}.dssm-file-meta{color:var(--dsw-alias-label-tertiary);font-size:11px;white-space:nowrap}.dssm-file-remove{width:24px;height:24px;color:var(--dsw-alias-label-secondary);font-size:18px}.dssm-upload-requirements{padding:1px 1px 0}.dssm-upload-requirements ul{display:flex;margin:7px 0 0;padding-left:18px;flex-direction:column;gap:5px;color:var(--dsw-alias-label-tertiary);font-size:11px;line-height:17px}.dssm-detail-section{gap:7px}.dssm-detail-path,.dssm-code{font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:11px}.dssm-detail-path{padding:8px 10px;border-radius:7px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);word-break:break-all}.dssm-code{max-height:280px;margin:0;padding:12px;overflow:auto;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);line-height:18px;white-space:pre-wrap}.dssm-diag{padding:8px 10px;border-left:2px solid #d49245;background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-secondary);font-size:12px}.dssm-trash-item{display:flex;align-items:center;gap:10px;padding:11px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}.dssm-trash-item:last-child{border-bottom:0}.dssm-trash-main{min-width:0;flex:1}
|
|
78
78
|
@container(max-width:780px){.dssm-table-head{display:none}.dssm-row{grid-template-columns:minmax(0,1fr) max-content;gap:8px;padding:11px 13px}.dssm-row>.dssm-tags,.dssm-row>.dssm-status{grid-column:1}.dssm-row-actions{grid-column:2;grid-row:1 / span 3}.dssm-path{display:none}}@media(max-width:760px){.dssm-summary{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:520px){.dssm-head{flex-direction:column}.dssm-actions{width:100%;margin-left:0}.dssm-actions .dssm-btn{flex:1}.dssm-filters{flex-direction:column}.dssm-source-filter{width:100%}.dssm-summary{grid-template-columns:1fr}}
|
|
@@ -89,7 +89,8 @@ window.__ModuleLoader__.load({
|
|
|
89
89
|
}
|
|
90
90
|
function parseApiResponse(response) { return response.json().catch(function () { var error = new Error("non-json response"); error.code = "error.proto.nonJson"; error.params = { status: response.status }; throw error; }).then(function (payload) { if (!response.ok || payload.ok === false) throw payload; return payload.data; }); }
|
|
91
91
|
function callApi(path, options) { return fetch("/api/dsh-skills-manager" + path, options).then(parseApiResponse); }
|
|
92
|
-
function isSkillEnabled(skill) { if (skill.
|
|
92
|
+
function isSkillEnabled(skill) { if (skill.enabled !== undefined) return skill.enabled === true; return skill.invocationPolicyValid && skill.modelInvocable && skill.userInvocable && skill.managerEnabled !== false; }
|
|
93
|
+
function countKey(key, count) { return key + (Number(count) === 1 ? ".one" : ".other"); }
|
|
93
94
|
function rootDisplayName(t, root) { var base = translateOrFallback(t, "root." + (root.localeKey || root.kind || root.key), root.label); return root.projectName ? base + " · " + root.projectName : base; }
|
|
94
95
|
function summarizeImportResult(t, data) {
|
|
95
96
|
var importedItems = data && data.imported || [];
|
|
@@ -176,24 +177,25 @@ window.__ModuleLoader__.load({
|
|
|
176
177
|
function selectUploadFiles(files) { pickerOpenRef.current = false; var selected = inspectUploadSelection(files); if (!selected) return; if (selected.error) { setUpload(null); setResult({ ok: false, text: translateError(t, selected.error) }); return; } setUpload(selected); setResult(null); }
|
|
177
178
|
function openNativePicker(ref) { if (busy || pickerOpenRef.current || !ref.current) return; pickerOpenRef.current = true; ref.current.value = ""; function release() { setTimeout(function () { pickerOpenRef.current = false; }, 0); } window.addEventListener("focus", release, { once: true }); ref.current.click(); setTimeout(function () { pickerOpenRef.current = false; }, 30000); }
|
|
178
179
|
function submitImport() { if (!upload) return; buildUploadPayload(upload).then(function (payload) { return post("/upload", payload, null); }).then(function (data) { var summary = summarizeImportResult(t, data); setResult({ ok: summary.ok, warning: summary.warning, text: summary.text }); if (summary.imported) { setModal(null); setUpload(null); } }).catch(function () {}); }
|
|
179
|
-
var data = snapshot.data || { roots: [], trash: [], summary: { total: 0,
|
|
180
|
+
var data = snapshot.data || { roots: [], trash: [], summary: { total: 0, enabled: 0, disabled: 0, issues: 0 } }, allRoots = data.roots || [], roots = visibleSkillRoots(allRoots), activeSource = roots.some(function (root) { return root.key === source; }) ? source : "";
|
|
180
181
|
var createRoots = allRoots.filter(function (root) { return root.mutable === true; }), createOptions = createRoots.map(function (root) { return { value: root.key, label: rootDisplayName(t, root) }; }); if (!createOptions.length) createOptions.push({ value: "dsh", label: t("root.dsh") });
|
|
181
182
|
function openCreate() { var selectedRoot = createRoots.some(function (root) { return root.key === activeSource; }) ? activeSource : "dsh"; setForm(Object.assign({}, form, { root: selectedRoot })); setModal("create"); }
|
|
182
183
|
function trashRootLabel(item) { return item.root && item.root.scope === "project" ? t("root.projectDsh") + " · " + (item.root.projectName || item.root.projectRoot) : t("root.dsh"); }
|
|
183
184
|
var options = [{ value: "", label: t("filter.all") }].concat(roots.map(function (root) { return { value: root.key, label: t("filter.option", { name: rootDisplayName(t, root), count: root.count == null ? root.skills.length : root.count }) }; }));
|
|
184
185
|
|
|
185
186
|
function renderSkill(root, skill) {
|
|
186
|
-
var enabled = isSkillEnabled(skill),
|
|
187
|
-
return h("div", { key: skill.name, className: "dssm-row" }, h("div", { className: "dssm-main" }, h("div", { className: "dssm-name" }, skill.declaredName || skill.name), h("div", { className: "dssm-note" }, skill.description || t("note.missing"))), h("div", { className: "dssm-tags" }, h("span", { className: "dssm-tag" }, rootDisplayName(t, root)), !root.mutable ? h("span", { className: "dssm-tag" }, t("status.readonly")) : null), h("div", { className: "dssm-status " + cls }, t(key)), h("div", { className: "dssm-row-actions" },
|
|
187
|
+
var enabled = isSkillEnabled(skill), key = skill.shadowedBy ? "status.shadowed" : skill.loadable === false ? "status.invalid" : enabled ? "status.enabled" : "status.disabled", cls = skill.shadowedBy ? "dssm-shadowed" : enabled ? "dssm-enabled" : "dssm-disabled";
|
|
188
|
+
return h("div", { key: skill.name, className: "dssm-row" }, h("div", { className: "dssm-main" }, h("div", { className: "dssm-name" }, skill.declaredName || skill.name), h("div", { className: "dssm-note" }, skill.description || t("note.missing"))), h("div", { className: "dssm-tags" }, h("span", { className: "dssm-tag" }, rootDisplayName(t, root)), !root.mutable ? h("span", { className: "dssm-tag" }, t("status.readonly")) : null), h("div", { className: "dssm-status " + cls }, t(key)), h("div", { className: "dssm-row-actions" }, root.toggleable !== false ? h(Switch, { on: enabled, disabled: busy || root.enabled === false || !!skill.shadowedBy || skill.loadable === false, label: t("skill.toggle") + " " + skill.name, onClick: function () { post(enabled ? "/disable" : "/enable", { root: root.key, name: skill.name }); } }) : null, h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", onClick: function () { openDetail(root, skill); } }, t("btn.detail")), root.mutable ? h("button", { type: "button", className: "dssm-btn dssm-btn-quiet", onClick: function () { setModal({ type: "trash-confirm", root: root.key, name: skill.name }); } }, t("btn.trash")) : null));
|
|
188
189
|
}
|
|
189
190
|
function renderRoot(root) {
|
|
190
191
|
if (activeSource && activeSource !== root.key) return null;
|
|
191
192
|
var displayName = rootDisplayName(t, root), filtered = root.skills.filter(function (skill) { return matchSkillQuery(Object.assign({}, skill, { rootKey: root.key, rootLabel: displayName }), query); }); if (query && !filtered.length) return null; var open = !!expanded[root.key] || !!query;
|
|
192
|
-
|
|
193
|
+
var rootCount = root.count == null ? root.skills.length : root.count;
|
|
194
|
+
return h("section", { key: root.key, className: "dssm-source" }, h("div", { className: "dssm-source-head" }, h("button", { type: "button", className: "dssm-source-head-main", "aria-expanded": open, onClick: function () { setExpanded(Object.assign({}, expanded, { [root.key]: !open })); } }, h("span", { className: "dssm-source-title" }, displayName), h("span", { className: "dssm-count" }, t(countKey("summary.group", rootCount), { count: rootCount })), h("span", { className: "dssm-tag " + (root.scope === "project" ? "dssm-tag-on" : root.key === "dsh" ? "" : root.enabled ? "dssm-tag-on" : "dssm-tag-off") }, root.scope === "project" ? t("status.project") : root.key === "dsh" ? t("status.manageable") : t(root.enabled ? "status.source.on" : "status.source.off")), root.scope === "project" ? h("span", { className: "dssm-tag" }, t("status.rank", { rank: root.rank })) : null, h("span", { className: "dssm-path", title: root.path }, root.path)), root.scope !== "project" && root.key !== "dsh" && root.toggleable !== false ? h("span", { className: "dssm-source-actions" }, h(Switch, { on: root.enabled, disabled: busy, label: t("source.toggle") + " " + displayName, onClick: function () { post(root.enabled ? "/source-disable" : "/source-enable", { root: root.key }); } })) : null), open ? h("div", { className: "dssm-source-body" }, filtered.length ? h(react.Fragment, null, h("div", { className: "dssm-table-head" }, h("span", null, t("table.skill")), h("span", null, t("filter.source")), h("span", null, t("table.status")), h("span", null, "")), filtered.map(function (skill) { return renderSkill(root, skill); })) : h("div", { className: "dssm-empty" }, query ? t("empty.search") : t("empty.source"))) : null);
|
|
193
195
|
}
|
|
194
196
|
|
|
195
|
-
var summary = data.summary || { total: 0,
|
|
196
|
-
var content = [h("style", { key: "css" }, CSS), h("div", { key: "head", className: "dssm-head" }, h("div", null, h("h2", { className: "dssm-title" }, t("title")), h("p", { className: "dssm-desc" }, t("desc"))), h("div", { className: "dssm-actions" }, h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }, t("btn.refresh")), h("button", { type: "button", className: "dssm-btn", onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: function () { setResult(null); setUpload(null); setModal("import"); } }, t("btn.import")))), h("div", { key: "summary", className: "dssm-summary" }, [[summary.total, "summary.total"], [summary.
|
|
197
|
+
var summary = data.summary || { total: 0, enabled: 0, disabled: 0, issues: 0 };
|
|
198
|
+
var content = [h("style", { key: "css" }, CSS), h("div", { key: "head", className: "dssm-head" }, h("div", null, h("h2", { className: "dssm-title" }, t("title")), h("p", { className: "dssm-desc" }, t("desc"))), h("div", { className: "dssm-actions" }, h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", disabled: busy || snapshot.loading, onClick: function () { refresh(false); } }, t("btn.refresh")), h("button", { type: "button", className: "dssm-btn", onClick: openCreate }, t("btn.create")), h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: function () { setResult(null); setUpload(null); setModal("import"); } }, t("btn.import")))), h("div", { key: "summary", className: "dssm-summary" }, [[summary.total, "summary.total"], [summary.enabled, "summary.enabled"], [summary.disabled, "summary.disabled"]].map(function (item) { return h("div", { key: item[1], className: "dssm-stat" }, h("strong", null, item[0]), t(countKey(item[1], item[0]), { count: item[0] }).replace(String(item[0]), "")); })), h("div", { key: "filters", className: "dssm-filters" }, h("input", { className: "dssm-control dssm-search", value: query, "aria-label": t("search"), placeholder: t("search.placeholder"), onChange: function (e) { setQuery(e.target.value); } }), h("div", { className: "dssm-source-filter" }, h(SourceSelect, { value: activeSource, options: options, onChange: setSource }))), result && modal !== "import" ? h("div", { key: "result", className: "dssm-feedback" + (result.warning ? " dssm-warning" : result.ok ? "" : " dssm-error") }, result.text) : null].concat((data.warnings || []).map(function (warning, index) { return h("div", { key: "warning-" + index, className: "dssm-feedback dssm-warning", role: "alert" }, translateError(t, warning)); }), [snapshot.error ? h("div", { key: "error", className: "dssm-feedback dssm-error" }, snapshot.error) : null, snapshot.loading && !snapshot.data ? h("div", { key: "loading", className: "dssm-empty" }, t("loading")) : h("div", { key: "sources", className: "dssm-sources" }, roots.map(renderRoot)), h("button", { key: "trash", type: "button", className: "dssm-trash-row", onClick: function () { setModal("trash"); } }, h("span", null, t("trash.title")), h("span", { className: "dssm-trash-count" }, (data.trash || []).length))]);
|
|
197
199
|
|
|
198
200
|
if (modal === "create") content.push(h(Modal, { key: "create", title: t("create.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dssm-form" }, h("label", { className: "dssm-field" }, h("span", { className: "dssm-label" }, t("create.target")), h(SourceSelect, { value: form.root, options: createOptions, onChange: function (value) { updateForm("root", value); } })), [["name", "create.name", "create.name.placeholder"], ["description", "create.description", "create.description.placeholder"]].map(function (field) { return h("label", { key: field[0], className: "dssm-field" }, h("span", { className: "dssm-label" }, t(field[1])), h("input", { className: "dssm-control", value: form[field[0]], placeholder: t(field[2]), onChange: function (e) { updateForm(field[0], e.target.value); } })); }), h("label", { className: "dssm-field" }, h("span", { className: "dssm-label" }, t("create.body")), h("textarea", { className: "dssm-control", value: form.body, placeholder: t("create.body.placeholder"), onChange: function (e) { updateForm("body", e.target.value); } })), h("p", { className: "dssm-help" }, t("create.chat.note"))), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn", disabled: busy || !form.name.trim() || !form.description.trim() || !form.body.trim(), onClick: submitCreate }, t("btn.create.now")))));
|
|
199
201
|
if (modal === "import") content.push(h(Modal, { key: "import", className: "dssm-modal-import", title: t("import.title"), closeLabel: t("btn.close"), onClose: function () { pickerOpenRef.current = false; setUpload(null); setResult(null); setModal(null); } },
|
|
@@ -204,12 +206,12 @@ window.__ModuleLoader__.load({
|
|
|
204
206
|
h("span", { className: "dssm-dropzone-copy" }, t("upload.drop.copy")),
|
|
205
207
|
h("div", { className: "dssm-upload-choices" }, h("button", { type: "button", className: "dssm-upload-link", disabled: busy, onClick: function (event) { event.stopPropagation(); openNativePicker(folderInputRef); } }, t("btn.folder.pick")), h("span", { className: "dssm-upload-divider" }, "/"), h("button", { type: "button", className: "dssm-upload-link", disabled: busy, onClick: function (event) { event.stopPropagation(); openNativePicker(importInputRef); } }, t("btn.file.pick")))
|
|
206
208
|
),
|
|
207
|
-
upload ? h("div", { className: "dssm-file", title: upload.name }, h("span", { className: "dssm-file-kind", "aria-hidden": "true" }, upload.kind === "zip" ? "ZIP" : upload.kind === "folder" ? "DIR" : "MD"), h("span", { className: "dssm-file-name" }, upload.name), h("span", { className: "dssm-file-meta" }, t("upload.selected", { count: upload.count, size: upload.size < 1024 ? upload.size + " B" : Math.ceil(upload.size / 1024) + " KB" })), h("button", { type: "button", className: "dssm-file-remove", "aria-label": t("upload.remove"), onClick: function () { setUpload(null); } }, "×")) : null,
|
|
209
|
+
upload ? h("div", { className: "dssm-file", title: upload.name }, h("span", { className: "dssm-file-kind", "aria-hidden": "true" }, upload.kind === "zip" ? "ZIP" : upload.kind === "folder" ? "DIR" : "MD"), h("span", { className: "dssm-file-name" }, upload.name), h("span", { className: "dssm-file-meta" }, t(countKey("upload.selected", upload.count), { count: upload.count, size: upload.size < 1024 ? upload.size + " B" : Math.ceil(upload.size / 1024) + " KB" })), h("button", { type: "button", className: "dssm-file-remove", "aria-label": t("upload.remove"), onClick: function () { setUpload(null); } }, "×")) : null,
|
|
208
210
|
result ? h("div", { className: "dssm-feedback" + (result.warning ? " dssm-warning" : result.ok ? "" : " dssm-error"), role: "alert" }, result.text) : null,
|
|
209
211
|
h("div", { className: "dssm-upload-requirements" }, h("div", { className: "dssm-label" }, t("upload.requirements")), h("ul", null, h("li", null, t("upload.requirement.skill")), h("li", null, t("upload.requirement.frontmatter")), h("li", null, t("upload.requirement.copy")))),
|
|
210
212
|
h("div", { className: "dssm-modal-actions" }, h("button", { type: "button", className: "dssm-btn dssm-btn-secondary", onClick: function () { pickerOpenRef.current = false; setUpload(null); setResult(null); setModal(null); } }, t("btn.cancel")), h("button", { type: "button", className: "dssm-btn", disabled: busy || !upload, onClick: submitImport }, busy ? t("upload.importing") : t("btn.import.now")))
|
|
211
213
|
)); if (modal === "detail") content.push(h(Modal, { key: "detail", wide: true, title: t("detail.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, detail ? h(react.Fragment, null, h("div", { className: "dssm-detail-path" }, detail.path), h("div", { className: "dssm-detail-section" }, h("div", { className: "dssm-detail-title" }, t("detail.diagnostics")), detail.diagnostics.length ? detail.diagnostics.map(function (item, index) { return h("div", { key: index, className: "dssm-diag" }, t(item.code, item.params || {})); }) : h("div", { className: "dssm-note" }, t("detail.noIssues"))), h("div", { className: "dssm-detail-section" }, h("div", { className: "dssm-detail-title" }, t("detail.body")), h("pre", { className: "dssm-code" }, detail.body || "")), h("div", { className: "dssm-detail-section" }, h("div", { className: "dssm-detail-title" }, t("detail.frontmatter")), h("pre", { className: "dssm-code" }, JSON.stringify(detail.frontmatter, null, 2)))) : h("div", { className: "dssm-empty" }, t("loading"))));
|
|
212
|
-
if (modal === "trash") content.push(h(Modal, { key: "trash-modal", title: t("trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dssm-count" }, t("trash.count", { count: data.trash.length })), data.trash.length ? data.trash.map(function (item) { return h("div", { key: item.id, className: "dssm-trash-item" }, h("div", { className: "dssm-trash-main" }, h("div", { className: "dssm-name" }, item.name), h("div", { className: "dssm-note" }, t("trash.deletedAt", { time: new Date(item.deletedAt).toLocaleString() }) + " · " + t("trash.source", { source: trashRootLabel(item) }))), h("button", { className: "dssm-btn dssm-btn-quiet", disabled: busy, onClick: function () { post("/trash-restore", { id: item.id }, "result.restored", { name: item.name }); } }, t("btn.restore")), h("button", { className: "dssm-btn dssm-btn-quiet dssm-btn-danger", disabled: busy, onClick: function () { setModal({ type: "delete-confirm", id: item.id, name: item.name }); } }, t("btn.delete.forever"))); }) : h("div", { className: "dssm-empty" }, t("trash.empty"))));
|
|
214
|
+
if (modal === "trash") content.push(h(Modal, { key: "trash-modal", title: t("trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("div", { className: "dssm-count" }, t(countKey("trash.count", data.trash.length), { count: data.trash.length })), data.trash.length ? data.trash.map(function (item) { return h("div", { key: item.id, className: "dssm-trash-item" }, h("div", { className: "dssm-trash-main" }, h("div", { className: "dssm-name" }, item.name), h("div", { className: "dssm-note" }, t("trash.deletedAt", { time: new Date(item.deletedAt).toLocaleString() }) + " · " + t("trash.source", { source: trashRootLabel(item) }))), h("button", { className: "dssm-btn dssm-btn-quiet", disabled: busy, onClick: function () { post("/trash-restore", { id: item.id }, "result.restored", { name: item.name }); } }, t("btn.restore")), h("button", { className: "dssm-btn dssm-btn-quiet dssm-btn-danger", disabled: busy, onClick: function () { setModal({ type: "delete-confirm", id: item.id, name: item.name }); } }, t("btn.delete.forever"))); }) : h("div", { className: "dssm-empty" }, t("trash.empty"))));
|
|
213
215
|
if (modal && modal.type === "trash-confirm") content.push(h(Modal, { key: "trash-confirm", title: t("confirm.trash.title"), closeLabel: t("btn.close"), onClose: function () { setModal(null); } }, h("p", { className: "dssm-desc" }, t("confirm.trash.desc", { name: modal.name })), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setModal(null); } }, t("btn.cancel")), h("button", { className: "dssm-btn", disabled: busy, onClick: function () { post("/delete", { root: modal.root, name: modal.name }, "result.trashed", { name: modal.name }).then(function () { setModal(null); }).catch(function () {}); } }, t("btn.trash")))));
|
|
214
216
|
if (modal && modal.type === "delete-confirm") content.push(h(Modal, { key: "delete-confirm", title: t("confirm.delete.title"), closeLabel: t("btn.close"), onClose: function () { setModal("trash"); } }, h("p", { className: "dssm-desc" }, t("confirm.delete.desc", { name: modal.name })), h("div", { className: "dssm-modal-actions" }, h("button", { className: "dssm-btn dssm-btn-secondary", onClick: function () { setModal("trash"); } }, t("btn.cancel")), h("button", { className: "dssm-btn dssm-btn-danger", disabled: busy, onClick: function () { post("/trash-delete", { id: modal.id }, "result.deleted", { name: modal.name }).then(function () { setModal("trash"); }).catch(function () {}); } }, t("btn.delete.forever")))));
|
|
215
217
|
return h("section", { className: "dssm-section" }, content);
|
|
@@ -217,7 +219,7 @@ window.__ModuleLoader__.load({
|
|
|
217
219
|
|
|
218
220
|
var inject = ["slots", "locale"];
|
|
219
221
|
function apply(ctx) { ctx.effect(function () { return ctx.locale.register(NS, DICT); }); ctx.slots.inject("settings.section", function () { return ctx.slots.register({ name: "settings.section", id: "skills-manager", order: 17, label: function () { return ctx.locale.bind(NS)("title"); }, icon: "skill", locale: NS }, SkillManagerSection); }); }
|
|
220
|
-
module.exports.DICT = DICT; module.exports.translateError = translateError; module.exports.parseApiResponse = parseApiResponse; module.exports.isSkillEnabled = isSkillEnabled; module.exports.rootDisplayName = rootDisplayName; module.exports.summarizeImportResult = summarizeImportResult; module.exports.normalizeSkillQuery = normalizeSkillQuery; module.exports.matchSkillQuery = matchSkillQuery; module.exports.filterSkills = filterSkills; module.exports.visibleSkillRoots = visibleSkillRoots; module.exports.trapModalFocus = trapModalFocus; module.exports.handleModalEscape = handleModalEscape; module.exports.inspectUploadSelection = inspectUploadSelection; module.exports.apply = apply; module.exports.inject = inject;
|
|
222
|
+
module.exports.DICT = DICT; module.exports.translateError = translateError; module.exports.parseApiResponse = parseApiResponse; module.exports.isSkillEnabled = isSkillEnabled; module.exports.countKey = countKey; module.exports.rootDisplayName = rootDisplayName; module.exports.summarizeImportResult = summarizeImportResult; module.exports.normalizeSkillQuery = normalizeSkillQuery; module.exports.matchSkillQuery = matchSkillQuery; module.exports.filterSkills = filterSkills; module.exports.visibleSkillRoots = visibleSkillRoots; module.exports.trapModalFocus = trapModalFocus; module.exports.handleModalEscape = handleModalEscape; module.exports.inspectUploadSelection = inspectUploadSelection; module.exports.apply = apply; module.exports.inject = inject;
|
|
221
223
|
return module.exports;
|
|
222
224
|
}
|
|
223
225
|
});
|
package/lib/core.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// 覆盖 DSH 用户级与活动 Session 项目级技能根:
|
|
4
4
|
// - 用户根目录:~/.dsh/skills 与常见 Agent 用户目录
|
|
5
|
-
// - 项目根目录:<project>/.dsh/skills(可启停、创建、回收)、<project>/.agents/skills
|
|
5
|
+
// - 项目根目录:<project>/.dsh/skills(可启停、创建、回收)、<project>/.agents/skills(本地策略启停、源文件只读)
|
|
6
6
|
// - 条目形态:<root>/<name>/SKILL.md(bundle)或 <root>/<name>.md(flat),只扫一层
|
|
7
7
|
// - 前端展示 name、description 与启停状态,不做格式检查或自动修复
|
|
8
8
|
//
|
|
@@ -18,6 +18,8 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
18
18
|
import { unzipSync } from "fflate";
|
|
19
19
|
|
|
20
20
|
const KEBAB_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
21
|
+
const PROJECT_ROOT_KEY_RE = /^project-(?:dsh|agents):[a-f0-9]{16}$/;
|
|
22
|
+
const USER_DSH_POLICY_RANK = 399;
|
|
21
23
|
const WINDOWS_DEVICE_NAME_RE = /^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\..*)?$/i;
|
|
22
24
|
const MAX_SOURCE_DEPTH = 64;
|
|
23
25
|
const MAX_ENTRY_NAME_LENGTH = 128;
|
|
@@ -143,7 +145,7 @@ export async function projectRoots(projectCwds = [], diagnostics) {
|
|
|
143
145
|
};
|
|
144
146
|
const candidates = [
|
|
145
147
|
{ ...common, key: `project-dsh:${id}`, kind: "project-dsh", localeKey: "projectDsh", path: join(project.root, ".dsh", "skills"), label: "Project DSH", rank: 100, mutable: true, toggleable: true },
|
|
146
|
-
{ ...common, key: `project-agents:${id}`, kind: "project-agents", localeKey: "projectAgents", path: join(project.root, ".agents", "skills"), label: "Project Agent", rank: 200 },
|
|
148
|
+
{ ...common, key: `project-agents:${id}`, kind: "project-agents", localeKey: "projectAgents", path: join(project.root, ".agents", "skills"), label: "Project Agent", rank: 200, toggleable: true },
|
|
147
149
|
];
|
|
148
150
|
for (const candidate of candidates) {
|
|
149
151
|
if (await projectSourceSafe(candidate)) roots.push(candidate);
|
|
@@ -376,8 +378,7 @@ function stripBom(text) {
|
|
|
376
378
|
}
|
|
377
379
|
|
|
378
380
|
/** 解析 SKILL.md 的 frontmatter。返回 { fields, map, body },map 保留键序。
|
|
379
|
-
* 只识别顶层 key: value;缩进嵌套字段(如 metadata.source)不进入 map。
|
|
380
|
-
* 写入走 updateInvocationPolicy 的文本级替换,因此嵌套块会被原样保留。 */
|
|
381
|
+
* 只识别顶层 key: value;缩进嵌套字段(如 metadata.source)不进入 map。 */
|
|
381
382
|
export function parseSkillDoc(text) {
|
|
382
383
|
const src = stripBom(String(text));
|
|
383
384
|
const lines = src.split(/\r?\n/);
|
|
@@ -443,20 +444,6 @@ export function unquote(v) {
|
|
|
443
444
|
return s;
|
|
444
445
|
}
|
|
445
446
|
|
|
446
|
-
/** 仅更新调用策略行,避免重写并破坏其他 YAML frontmatter。
|
|
447
|
-
* 启用时删除这两行以恢复缺省可调用,不还原这两行的原始文本或行内注释。 */
|
|
448
|
-
function updateInvocationPolicy(text, enabled) {
|
|
449
|
-
const source = String(text);
|
|
450
|
-
const newline = source.includes("\r\n") ? "\r\n" : "\n";
|
|
451
|
-
const lines = source.split(/\r?\n/);
|
|
452
|
-
if (lines[0]?.trim() !== "---") return null;
|
|
453
|
-
const end = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
454
|
-
if (end < 0) return null;
|
|
455
|
-
const fields = lines.slice(1, end).filter((line) => !/^(disable-model-invocation|user-invocable)\s*:/.test(line));
|
|
456
|
-
if (!enabled) fields.push("disable-model-invocation: true", "user-invocable: false");
|
|
457
|
-
return ["---", ...fields, "---", ...lines.slice(end + 1)].join(newline);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
447
|
/** 同目录临时文件加 rename,避免写入中断时截断原 SKILL.md。 */
|
|
461
448
|
/** Windows 上杀毒软件或索引器可能短暂占用目录;只重试明确可恢复的 rename 错误。 */
|
|
462
449
|
export async function renameWithRetry(source, destination, options = {}) {
|
|
@@ -584,7 +571,8 @@ function entryOf(name, kind, docPath, doc) {
|
|
|
584
571
|
userInvocable: !userDisabled,
|
|
585
572
|
invocationPolicyValid,
|
|
586
573
|
hasFrontmatter: doc.hasFrontmatter,
|
|
587
|
-
|
|
574
|
+
// 调用策略值异常可以由 manager 本地策略覆盖;结构本身合法即可加载。
|
|
575
|
+
loadable: doc.hasFrontmatter && KEBAB_RE.test(declaredName) && description !== "",
|
|
588
576
|
diagnostics,
|
|
589
577
|
};
|
|
590
578
|
}
|
|
@@ -631,12 +619,17 @@ export async function scanEntries(root) {
|
|
|
631
619
|
function defaultManagerState() {
|
|
632
620
|
const sources = Object.create(null);
|
|
633
621
|
const disabledSkills = Object.create(null);
|
|
622
|
+
const enabledSkills = Object.create(null);
|
|
634
623
|
for (const root of userRoots()) {
|
|
635
|
-
if (root.key
|
|
636
|
-
sources[root.key] = true;
|
|
624
|
+
if (root.key !== "dsh") sources[root.key] = true;
|
|
637
625
|
disabledSkills[root.key] = [];
|
|
626
|
+
enabledSkills[root.key] = [];
|
|
638
627
|
}
|
|
639
|
-
return { version: 1, sources, disabledSkills };
|
|
628
|
+
return { version: 1, sources, disabledSkills, enabledSkills };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function validStateSkillName(name) {
|
|
632
|
+
return typeof name === "string" && KEBAB_RE.test(name) && entryPath(resolveDshHome(), name) !== null;
|
|
640
633
|
}
|
|
641
634
|
|
|
642
635
|
/** 状态文件已存在但不可用时一律关闭外部来源,避免损坏配置重新暴露技能。 */
|
|
@@ -646,16 +639,31 @@ function failClosedManagerState() {
|
|
|
646
639
|
return state;
|
|
647
640
|
}
|
|
648
641
|
|
|
642
|
+
function validPolicyLists(value, allowMissing = false) {
|
|
643
|
+
if (value === undefined && allowMissing) return true;
|
|
644
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
645
|
+
for (const [key, list] of Object.entries(value)) {
|
|
646
|
+
if (!userRoots().some((root) => root.key === key) && !PROJECT_ROOT_KEY_RE.test(key)) continue;
|
|
647
|
+
if (!Array.isArray(list) || list.some((name) => !validStateSkillName(name))) return false;
|
|
648
|
+
}
|
|
649
|
+
return true;
|
|
650
|
+
}
|
|
651
|
+
|
|
649
652
|
function validManagerStateDocument(value) {
|
|
650
653
|
if (!value || typeof value !== "object" || Array.isArray(value) || value.version !== 1) return false;
|
|
651
654
|
if (!value.sources || typeof value.sources !== "object" || Array.isArray(value.sources)) return false;
|
|
652
|
-
if (!value.disabledSkills ||
|
|
655
|
+
if (!validPolicyLists(value.disabledSkills) || !validPolicyLists(value.enabledSkills, true)) return false;
|
|
653
656
|
for (const root of userRoots()) {
|
|
654
657
|
if (root.key === "dsh") continue;
|
|
655
658
|
if (typeof value.sources[root.key] !== "boolean") return false;
|
|
656
659
|
const list = value.disabledSkills[root.key];
|
|
657
660
|
if (!Array.isArray(list) || list.some((name) => typeof name !== "string" || entryPath(root.path, name) === null)) return false;
|
|
658
661
|
}
|
|
662
|
+
const enabledSkills = value.enabledSkills || {};
|
|
663
|
+
for (const key of new Set([...Object.keys(value.disabledSkills), ...Object.keys(enabledSkills)])) {
|
|
664
|
+
const disabled = new Set(value.disabledSkills[key] || []);
|
|
665
|
+
if ((enabledSkills[key] || []).some((name) => disabled.has(name))) return false;
|
|
666
|
+
}
|
|
659
667
|
return true;
|
|
660
668
|
}
|
|
661
669
|
|
|
@@ -663,10 +671,20 @@ function normalizeManagerState(value) {
|
|
|
663
671
|
const normalized = defaultManagerState();
|
|
664
672
|
if (!value || typeof value !== "object" || Array.isArray(value)) return normalized;
|
|
665
673
|
for (const root of userRoots()) {
|
|
666
|
-
if (root.key
|
|
667
|
-
if (value.sources && typeof value.sources[root.key] === "boolean") normalized.sources[root.key] = value.sources[root.key];
|
|
674
|
+
if (root.key !== "dsh" && value.sources && typeof value.sources[root.key] === "boolean") normalized.sources[root.key] = value.sources[root.key];
|
|
668
675
|
const list = value.disabledSkills && value.disabledSkills[root.key];
|
|
669
676
|
if (Array.isArray(list)) normalized.disabledSkills[root.key] = [...new Set(list.filter((name) => typeof name === "string" && entryPath(root.path, name) !== null))].sort();
|
|
677
|
+
const enabled = value.enabledSkills && value.enabledSkills[root.key];
|
|
678
|
+
if (Array.isArray(enabled)) normalized.enabledSkills[root.key] = [...new Set(enabled.filter((name) => typeof name === "string" && entryPath(root.path, name) !== null))].sort();
|
|
679
|
+
}
|
|
680
|
+
for (const field of ["disabledSkills", "enabledSkills"]) {
|
|
681
|
+
const source = value[field];
|
|
682
|
+
if (source && typeof source === "object" && !Array.isArray(source)) {
|
|
683
|
+
for (const [key, list] of Object.entries(source)) {
|
|
684
|
+
if (!PROJECT_ROOT_KEY_RE.test(key) || !Array.isArray(list)) continue;
|
|
685
|
+
normalized[field][key] = [...new Set(list.filter(validStateSkillName))].sort();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
670
688
|
}
|
|
671
689
|
return normalized;
|
|
672
690
|
}
|
|
@@ -682,7 +700,7 @@ export async function readManagerState() {
|
|
|
682
700
|
return {
|
|
683
701
|
state: failClosedManagerState(),
|
|
684
702
|
writable: false,
|
|
685
|
-
warning: { code: "warning.state.invalid", params: { path: managerStatePath() }, error:
|
|
703
|
+
warning: { code: "warning.state.invalid", params: { path: managerStatePath() }, error: `技能管理器状态文件不可读,所有技能已安全停用且状态写入已锁定: ${managerStatePath()}` },
|
|
686
704
|
};
|
|
687
705
|
}
|
|
688
706
|
}
|
|
@@ -692,8 +710,24 @@ async function writeManagerState(value) {
|
|
|
692
710
|
await writeFileAtomically(managerStatePath(), `${JSON.stringify(normalizeManagerState(value), null, 2)}\n`);
|
|
693
711
|
}
|
|
694
712
|
|
|
695
|
-
function
|
|
696
|
-
|
|
713
|
+
function managerSkillOverride(policy, rootKey, name) {
|
|
714
|
+
if ((policy.enabledSkills[rootKey] || []).includes(name)) return true;
|
|
715
|
+
if ((policy.disabledSkills[rootKey] || []).includes(name)) return false;
|
|
716
|
+
return undefined;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function effectiveSkillPolicy(policyResult, root, entry) {
|
|
720
|
+
const override = managerSkillOverride(policyResult.state, root.key, entry.name);
|
|
721
|
+
const sourceEnabled = root.key === "dsh" || root.scope === "project" || policyResult.state.sources[root.key] !== false;
|
|
722
|
+
if (policyResult.writable === false || !sourceEnabled || override === false) {
|
|
723
|
+
return { override, sourceEnabled, modelInvocable: false, userInvocable: false, enabled: false };
|
|
724
|
+
}
|
|
725
|
+
if (override === true) {
|
|
726
|
+
return { override, sourceEnabled, modelInvocable: true, userInvocable: true, enabled: true };
|
|
727
|
+
}
|
|
728
|
+
const modelInvocable = entry.invocationPolicyValid && entry.modelInvocable;
|
|
729
|
+
const userInvocable = entry.invocationPolicyValid && entry.userInvocable;
|
|
730
|
+
return { override, sourceEnabled, modelInvocable, userInvocable, enabled: modelInvocable && userInvocable };
|
|
697
731
|
}
|
|
698
732
|
|
|
699
733
|
function invalidManagerStateWrite() {
|
|
@@ -716,38 +750,62 @@ export async function setSourceEnabled(key, enabled, log) {
|
|
|
716
750
|
return { root: root.key, enabled: enabled === true };
|
|
717
751
|
}
|
|
718
752
|
|
|
719
|
-
async function
|
|
720
|
-
const
|
|
753
|
+
async function checkedPolicyRootDefinition(root) {
|
|
754
|
+
const definition = rootDefinition(root);
|
|
755
|
+
if (!definition || !definition.toggleable) return null;
|
|
756
|
+
if (definition.scope !== "project") {
|
|
757
|
+
const canonical = rootByKey(definition.key);
|
|
758
|
+
return canonical && resolve(canonical.path) === resolve(definition.path) ? canonical : null;
|
|
759
|
+
}
|
|
760
|
+
const validProjectRoot = (definition.kind === "project-dsh" || definition.kind === "project-agents")
|
|
761
|
+
&& PROJECT_ROOT_KEY_RE.test(definition.key)
|
|
762
|
+
&& typeof definition.projectRoot === "string"
|
|
763
|
+
&& isAbsolute(definition.projectRoot)
|
|
764
|
+
&& definition.key === `${definition.kind}:${projectIdentity(definition.projectRoot)}`
|
|
765
|
+
&& resolve(definition.path) === resolve(join(definition.projectRoot, definition.kind === "project-dsh" ? ".dsh" : ".agents", "skills"));
|
|
766
|
+
if (!validProjectRoot) return null;
|
|
767
|
+
if (!(await projectSourceSafe(definition))) {
|
|
768
|
+
return { ok: false, error: `项目技能目录不安全,拒绝写入状态: ${definition.path}`, code: "error.root.unsafe", params: { path: definition.path } };
|
|
769
|
+
}
|
|
770
|
+
return definition;
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
async function setPolicySkillEnabled(root, name, enabled, log) {
|
|
774
|
+
const definition = await checkedPolicyRootDefinition(root);
|
|
775
|
+
if (definition && definition.ok === false) return definition;
|
|
776
|
+
if (!definition) return readonlyError("toggle");
|
|
777
|
+
const resolved = await resolveEntry(definition.path, name);
|
|
721
778
|
if (resolved === null) return { ok: false, error: `技能不存在: ${name}`, code: "error.skill.notFound", params: { name } };
|
|
779
|
+
const summary = entryOf(name, resolved.kind, resolved.docPath, parseSkillDoc(await fs.readFile(resolved.docPath, "utf8")));
|
|
780
|
+
if (!summary.hasFrontmatter) {
|
|
781
|
+
return { ok: false, error: `技能缺少完整 frontmatter,无法${enabled ? "启用" : "停用"}: ${name}`, code: "error.skill.noFrontmatter", params: { name, action: enabled ? "enable" : "disable" } };
|
|
782
|
+
}
|
|
783
|
+
if (!summary.loadable) {
|
|
784
|
+
return { ok: false, error: `技能结构不完整,无法${enabled ? "启用" : "停用"}: ${name}`, code: "error.skill.notLoadable", params: { name, action: enabled ? "enable" : "disable" } };
|
|
785
|
+
}
|
|
722
786
|
const current = await readManagerState();
|
|
723
787
|
if (current.writable === false) return invalidManagerStateWrite();
|
|
724
|
-
const disabled = new Set(current.state.disabledSkills[
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
788
|
+
const disabled = new Set(current.state.disabledSkills[definition.key] || []);
|
|
789
|
+
const explicitlyEnabled = new Set(current.state.enabledSkills[definition.key] || []);
|
|
790
|
+
if (enabled) {
|
|
791
|
+
disabled.delete(name);
|
|
792
|
+
explicitlyEnabled.add(name);
|
|
793
|
+
} else {
|
|
794
|
+
explicitlyEnabled.delete(name);
|
|
795
|
+
disabled.add(name);
|
|
796
|
+
}
|
|
797
|
+
current.state.disabledSkills[definition.key] = [...disabled].sort();
|
|
798
|
+
current.state.enabledSkills[definition.key] = [...explicitlyEnabled].sort();
|
|
728
799
|
await writeManagerState(current.state);
|
|
729
|
-
if (log) log(enabled ? "
|
|
730
|
-
return { root:
|
|
800
|
+
if (log) log(enabled ? "policy-enable" : "policy-disable", `${enabled ? "启用" : "停用"} ${definition.key}/${name}(本地策略,源文件不变)`);
|
|
801
|
+
return { root: definition.key, name, enabled: enabled === true };
|
|
731
802
|
}
|
|
732
803
|
|
|
733
804
|
// ── 启用 / 停用(同时控制模型与 / 手动调用,非破坏)──────────────────────────
|
|
734
805
|
|
|
735
806
|
/** enabled=true 恢复模型与 / 手动调用;false 同时停用两种调用入口。 */
|
|
736
807
|
export async function setSkillEnabled(root, name, enabled, log) {
|
|
737
|
-
|
|
738
|
-
if (!definition || !definition.toggleable) return readonlyError("toggle");
|
|
739
|
-
if (definition.key !== "dsh" && definition.kind !== "project-dsh") return setReadOnlySkillEnabled(definition, name, enabled, log);
|
|
740
|
-
definition = await checkedWritableRootDefinition(definition);
|
|
741
|
-
if (definition && definition.ok === false) return definition;
|
|
742
|
-
if (!definition) return readonlyError("toggle");
|
|
743
|
-
const resolved = await resolveEntry(definition.path, name);
|
|
744
|
-
if (resolved === null) return { ok: false, error: `技能不存在: ${name}`, code: "error.skill.notFound", params: { name } };
|
|
745
|
-
const source = await fs.readFile(resolved.docPath, "utf8");
|
|
746
|
-
const updated = updateInvocationPolicy(source, enabled);
|
|
747
|
-
if (updated === null) return { ok: false, error: `技能缺少完整 frontmatter,无法${enabled ? "启用" : "停用"}: ${name}`, code: "error.skill.noFrontmatter", params: { name, action: enabled ? "enable" : "disable" } };
|
|
748
|
-
await writeFileAtomically(resolved.docPath, updated);
|
|
749
|
-
if (log) log(enabled ? "enable" : "disable", `${enabled ? "启用" : "停用"} ${resolved.docPath}`);
|
|
750
|
-
return { root: definition.key, name, enabled };
|
|
808
|
+
return setPolicySkillEnabled(root, name, enabled, log);
|
|
751
809
|
}
|
|
752
810
|
|
|
753
811
|
async function safeExistingEntryPaths(root, name) {
|
|
@@ -1410,37 +1468,67 @@ export async function skillDetail(key, name, options = {}) {
|
|
|
1410
1468
|
}
|
|
1411
1469
|
|
|
1412
1470
|
/** 生成 manager provider 候选:保留禁用候选以阻止低优先级重名副本意外激活。 */
|
|
1413
|
-
export async function listProviderCandidates() {
|
|
1414
|
-
const
|
|
1471
|
+
export async function listProviderCandidates(options = {}) {
|
|
1472
|
+
const policyResult = await readManagerState();
|
|
1415
1473
|
const candidates = [];
|
|
1416
1474
|
for (const root of userRoots()) {
|
|
1417
|
-
if (root.key === "dsh") continue;
|
|
1418
1475
|
const scanned = await scanEntries(root.path);
|
|
1419
1476
|
for (const entry of scanned.entries) {
|
|
1420
1477
|
if (!entry.loadable) continue;
|
|
1421
|
-
const
|
|
1478
|
+
const policy = effectiveSkillPolicy(policyResult, root, entry);
|
|
1479
|
+
const needsOverlay = root.key !== "dsh" || policy.override !== undefined || !entry.invocationPolicyValid || policyResult.writable === false;
|
|
1480
|
+
if (!needsOverlay) continue;
|
|
1422
1481
|
candidates.push({
|
|
1423
1482
|
name: entry.declaredName,
|
|
1424
1483
|
description: entry.description,
|
|
1425
|
-
invocation:
|
|
1484
|
+
invocation: { modelInvocable: policy.modelInvocable, userInvocable: policy.userInvocable },
|
|
1426
1485
|
provider: "dsh-skills-manager-external",
|
|
1427
|
-
source: `agent-${root.key}`,
|
|
1428
|
-
rank: root.rank,
|
|
1486
|
+
source: root.key === "dsh" ? "user-dsh" : `agent-${root.key}`,
|
|
1487
|
+
rank: root.key === "dsh" ? USER_DSH_POLICY_RANK : root.rank,
|
|
1429
1488
|
locator: { rootKey: root.key, entryName: entry.name, path: entry.docPath },
|
|
1430
1489
|
resourceBase: { kind: "directory", path: entry.kind === "bundle" ? dirname(entry.docPath) : root.path },
|
|
1431
1490
|
path: entry.docPath,
|
|
1432
|
-
metadata: { dshSkillsManager: { root: root.key, readOnly:
|
|
1491
|
+
metadata: { dshSkillsManager: { root: root.key, readOnly: !root.mutable, sourceReadOnly: !root.mutable, policyOnly: root.key === "dsh" } },
|
|
1433
1492
|
});
|
|
1434
1493
|
}
|
|
1435
1494
|
}
|
|
1495
|
+
const cwd = options && typeof options.cwd === "string" ? options.cwd : undefined;
|
|
1496
|
+
if (cwd) {
|
|
1497
|
+
const roots = await projectRoots([cwd]);
|
|
1498
|
+
for (const root of roots) {
|
|
1499
|
+
const scanned = await scanEntries(root.path);
|
|
1500
|
+
for (const entry of scanned.entries) {
|
|
1501
|
+
if (!entry.loadable) continue;
|
|
1502
|
+
const policy = effectiveSkillPolicy(policyResult, root, entry);
|
|
1503
|
+
const needsOverlay = policy.override !== undefined || !entry.invocationPolicyValid || policyResult.writable === false;
|
|
1504
|
+
// 没有本地覆盖时继续由 DSH 官方作用域 provider 负责加载。
|
|
1505
|
+
if (!needsOverlay) continue;
|
|
1506
|
+
candidates.push({
|
|
1507
|
+
name: entry.declaredName,
|
|
1508
|
+
description: entry.description,
|
|
1509
|
+
invocation: { modelInvocable: policy.modelInvocable, userInvocable: policy.userInvocable },
|
|
1510
|
+
provider: "dsh-skills-manager-external",
|
|
1511
|
+
source: root.kind,
|
|
1512
|
+
rank: root.rank - 1,
|
|
1513
|
+
locator: { rootKey: root.key, entryName: entry.name, path: entry.docPath },
|
|
1514
|
+
resourceBase: { kind: "directory", path: entry.kind === "bundle" ? dirname(entry.docPath) : root.path },
|
|
1515
|
+
path: entry.docPath,
|
|
1516
|
+
metadata: { dshSkillsManager: { root: root.key, readOnly: !root.mutable, sourceReadOnly: !root.mutable, policyOnly: true } },
|
|
1517
|
+
});
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1436
1521
|
return candidates;
|
|
1437
1522
|
}
|
|
1438
1523
|
|
|
1439
|
-
export async function getProviderSkill(candidate) {
|
|
1524
|
+
export async function getProviderSkill(candidate, options = {}) {
|
|
1440
1525
|
const locator = candidate && candidate.locator;
|
|
1441
1526
|
if (!locator || typeof locator.path !== "string" || typeof locator.rootKey !== "string") return undefined;
|
|
1442
|
-
|
|
1443
|
-
if (!root
|
|
1527
|
+
let root = rootByKey(locator.rootKey);
|
|
1528
|
+
if (!root && PROJECT_ROOT_KEY_RE.test(locator.rootKey) && typeof options.cwd === "string") {
|
|
1529
|
+
root = (await projectRoots([options.cwd])).find((item) => item.key === locator.rootKey);
|
|
1530
|
+
}
|
|
1531
|
+
if (!root) return undefined;
|
|
1444
1532
|
const entry = await resolveEntry(root.path, String(locator.entryName || ""));
|
|
1445
1533
|
if (!entry || resolve(entry.docPath) !== resolve(locator.path)) return undefined;
|
|
1446
1534
|
const doc = parseSkillDoc(await fs.readFile(entry.docPath, "utf8"));
|
|
@@ -1478,8 +1566,7 @@ function markWinners(items, options = {}) {
|
|
|
1478
1566
|
}
|
|
1479
1567
|
for (const [canonicalName, winner] of winners) {
|
|
1480
1568
|
if (options.markWinner !== false) winner.view.winner = true;
|
|
1481
|
-
|
|
1482
|
-
else winner.view.loaded = winner.managerEnabled && winner.entry.modelInvocable && winner.entry.userInvocable;
|
|
1569
|
+
winner.view.enabled = winner.policy.enabled;
|
|
1483
1570
|
winner.view.canonicalName = canonicalName;
|
|
1484
1571
|
}
|
|
1485
1572
|
return winners;
|
|
@@ -1500,7 +1587,8 @@ export async function state(options = {}) {
|
|
|
1500
1587
|
if (root.scope === "project" && !exists && root.kind !== "project-dsh") continue;
|
|
1501
1588
|
const skills = [];
|
|
1502
1589
|
for (const e of entries) {
|
|
1503
|
-
const
|
|
1590
|
+
const policy = effectiveSkillPolicy(policyResult, root, e);
|
|
1591
|
+
const managerEnabled = policyResult.writable !== false && policy.sourceEnabled && policy.override !== false;
|
|
1504
1592
|
skills.push({
|
|
1505
1593
|
name: e.name,
|
|
1506
1594
|
declaredName: e.declaredName,
|
|
@@ -1512,10 +1600,13 @@ export async function state(options = {}) {
|
|
|
1512
1600
|
hasFrontmatter: e.hasFrontmatter,
|
|
1513
1601
|
loadable: e.loadable,
|
|
1514
1602
|
managerEnabled,
|
|
1603
|
+
managerOverride: policy.override === undefined ? null : policy.override,
|
|
1604
|
+
effectiveModelInvocable: policy.modelInvocable,
|
|
1605
|
+
effectiveUserInvocable: policy.userInvocable,
|
|
1515
1606
|
diagnostics: e.diagnostics,
|
|
1516
1607
|
path: e.docPath,
|
|
1517
1608
|
});
|
|
1518
|
-
all.push({ root, entry: e, managerEnabled, view: skills[skills.length - 1] });
|
|
1609
|
+
all.push({ root, entry: e, managerEnabled, policy, view: skills[skills.length - 1] });
|
|
1519
1610
|
}
|
|
1520
1611
|
result.roots.push({
|
|
1521
1612
|
key: root.key,
|
|
@@ -1530,7 +1621,7 @@ export async function state(options = {}) {
|
|
|
1530
1621
|
scope: root.scope || "user",
|
|
1531
1622
|
...(root.projectRoot ? { projectRoot: root.projectRoot, projectName: root.projectName, workspaceCwds: root.workspaceCwds } : {}),
|
|
1532
1623
|
exists,
|
|
1533
|
-
enabled: root.scope === "project" || root.key === "dsh"
|
|
1624
|
+
enabled: policyResult.writable !== false && (root.scope === "project" || root.key === "dsh" || policyResult.state.sources[root.key] !== false),
|
|
1534
1625
|
skills,
|
|
1535
1626
|
});
|
|
1536
1627
|
}
|
|
@@ -1545,7 +1636,7 @@ export async function state(options = {}) {
|
|
|
1545
1636
|
for (const projectItems of projectGroups.values()) {
|
|
1546
1637
|
// 用户级条目参与该 workspace 的优先级判定,但不把其全局视图标记为“被某项目覆盖”。
|
|
1547
1638
|
const userCopies = userItems.map((item) => ({ ...item, view: { ...item.view } }));
|
|
1548
|
-
markWinners([...projectItems, ...userCopies]
|
|
1639
|
+
markWinners([...projectItems, ...userCopies]);
|
|
1549
1640
|
}
|
|
1550
1641
|
const seenProjects = new Set();
|
|
1551
1642
|
for (const root of result.roots.filter((item) => item.scope === "project")) {
|
|
@@ -1555,8 +1646,8 @@ export async function state(options = {}) {
|
|
|
1555
1646
|
}
|
|
1556
1647
|
result.summary = {
|
|
1557
1648
|
total: all.length,
|
|
1558
|
-
|
|
1559
|
-
disabled: all.filter((item) => item.entry.loadable &&
|
|
1649
|
+
enabled: all.filter((item) => item.view.enabled === true).length,
|
|
1650
|
+
disabled: all.filter((item) => item.entry.loadable && item.policy.enabled === false).length,
|
|
1560
1651
|
issues: all.reduce((count, item) => count + item.entry.diagnostics.length + (item.view.shadowedBy ? 1 : 0), 0),
|
|
1561
1652
|
};
|
|
1562
1653
|
return result;
|
package/lib/index.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
// - host 路由仅依赖 node: 内置与本地 core.js;ZIP 解压封装在 core
|
|
3
3
|
// - 路由:状态、启停、详情、创建、导入、回收站
|
|
4
4
|
// - 全部接口先校验 DSH Web 信任的 Host;写接口再校验请求标记与 JSON
|
|
5
|
-
// -
|
|
6
|
-
// -
|
|
5
|
+
// - 所有来源的启停只写 manager 状态,不改原始 Skill 文件
|
|
6
|
+
// - $DSH_HOME\skills 与活动项目 .dsh/skills 可创建、回收与恢复;Agent 来源文件保持只读
|
|
7
7
|
|
|
8
8
|
import { appendFile, rename, rm, stat } from "node:fs/promises";
|
|
9
9
|
import {
|
|
@@ -268,8 +268,8 @@ function externalSkillProvider(control, invalidators) {
|
|
|
268
268
|
}
|
|
269
269
|
return {
|
|
270
270
|
name: "dsh-skills-manager-external",
|
|
271
|
-
list: async () => listProviderCandidates(),
|
|
272
|
-
get: async (candidate) => getProviderSkill(candidate),
|
|
271
|
+
list: async (options) => listProviderCandidates(options),
|
|
272
|
+
get: async (candidate, options) => getProviderSkill(candidate, options),
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
275
|
|
package/package.json
CHANGED