@michengai/dsh-skills-manager 0.1.25 → 0.1.26

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 CHANGED
@@ -4,6 +4,14 @@
4
4
 
5
5
  The five most recent published versions are listed below.
6
6
 
7
+ ## 0.1.26 — 2026-08-27
8
+
9
+ - Fixed browser folder imports that exceeded the former 16 MiB Base64 JSON request ceiling while still fitting within the documented 25 MiB decoded-content limit.
10
+ - Added client-side checks for the existing 10 MiB ZIP, 5 MiB per-file, 25 MiB total, and 500-entry upload limits so oversized selections fail with a specific message before being read.
11
+ - Replaced the large-string Base64 validation regex with stack-safe validation to prevent multi-MiB files from exhausting the JavaScript call stack.
12
+
13
+ Published package: [`@michengai/dsh-skills-manager@0.1.26`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.26).
14
+
7
15
  ## 0.1.25 — 2026-08-27
8
16
 
9
17
  - Added provider-backed loading for public Agents, Codex, Claude, Gemini, and OpenCode while keeping external source files read-only and persisting toggles in manager state.
@@ -31,10 +39,3 @@ Release tag: [`v0.1.23`](https://github.com/MichengAI/dsh-skills-manager/tree/v0
31
39
  - Preserved symlink-rejection errors while collecting batch import candidates.
32
40
 
33
41
  Release tag: [`v0.1.22`](https://github.com/MichengAI/dsh-skills-manager/tree/v0.1.22).
34
-
35
- ## 0.1.21 — 2026-08-17
36
-
37
- - Applied the same symlink and depth checks during import dry runs.
38
- - Aligned dry-run cleanup, deletion handling, and HEAD behavior.
39
-
40
- Release tag: [`v0.1.21`](https://github.com/MichengAI/dsh-skills-manager/tree/v0.1.21).
@@ -4,6 +4,14 @@
4
4
 
5
5
  以下记录最近发布的五个版本。
6
6
 
7
+ ## 0.1.26 — 2026-08-27
8
+
9
+ - 修复浏览器上传文件夹时,原始内容仍在 25 MiB 总量限制内,却因 Base64 JSON 超过旧 16 MiB 请求体上限而失败的问题。
10
+ - 在浏览器读取内容前检查既有的 ZIP 10 MiB、单文件 5 MiB、总量 25 MiB 和 500 个条目限制,超限时直接显示具体原因。
11
+ - 将大字符串 Base64 正则校验改为栈安全校验,避免数 MiB 文件耗尽 JavaScript 调用栈。
12
+
13
+ 发布包:[`@michengai/dsh-skills-manager@0.1.26`](https://www.npmjs.com/package/@michengai/dsh-skills-manager/v/0.1.26)。
14
+
7
15
  ## 0.1.25 — 2026-08-27
8
16
 
9
17
  - 新增公共 Agent、Codex、Claude、Gemini 与 OpenCode 技能源加载;启停策略仅写入管理器状态,不修改外部源文件。
@@ -31,10 +39,3 @@
31
39
  - 批量收集导入候选项时不再吞掉符号链接拒绝错误。
32
40
 
33
41
  发布标签:[`v0.1.22`](https://github.com/MichengAI/dsh-skills-manager/tree/v0.1.22)。
34
-
35
- ## 0.1.21 — 2026-08-17
36
-
37
- - 导入 dry-run 阶段执行与正式导入一致的符号链接和目录深度检查。
38
- - 统一 dry-run 清理、删除处理和 HEAD 请求行为。
39
-
40
- 发布标签:[`v0.1.21`](https://github.com/MichengAI/dsh-skills-manager/tree/v0.1.21)。
package/README.md CHANGED
@@ -34,17 +34,13 @@ Browse by source or search in **Settings → Skills**. External Agent sources ar
34
34
 
35
35
  ![Skills Manager source-first settings page](assets/screenshots/skills-manager-v2-preview.png)
36
36
 
37
- Shared Agent skills stay visible when the DSH-local group is empty:
37
+ Open any skill to inspect its source path, diagnostics, Markdown body, and parsed frontmatter:
38
38
 
39
- ![Shared Agent skills remain visible](assets/screenshots/skills-public.png)
39
+ ![Skill details and diagnostics](assets/screenshots/skill-detail.png)
40
40
 
41
- The compact upload dialog accepts `.zip`, a skill folder, a single `SKILL.md`, or drag-and-drop in one flow. It uses native browser pickers and never asks for an absolute path or opens a second chooser:
41
+ Moving a DSH-local skill to Trash requires confirmation and remains recoverable until it is permanently deleted:
42
42
 
43
- ![Upload plugin dialog](assets/screenshots/upload-plugin.png)
44
-
45
- Enabling a DSH-local skill restores its `/` command in the chat composer:
46
-
47
- ![Chat slash command after enabling a local skill](assets/screenshots/slash-command.png)
43
+ ![Move a skill to Trash confirmation](assets/screenshots/delete-plugin.png)
48
44
 
49
45
  ## DSH product ecosystem
50
46
 
@@ -145,8 +141,6 @@ Open **Settings → Skills**, then use the panel as follows:
145
141
  | Create from conversation | Let an Agent call `create_skill`; DSH asks for approval before writing. | `$DSH_HOME/skills` |
146
142
  | Delete and recover | Move to Trash, restore, or permanently delete in a second step. | DSH-local skills |
147
143
 
148
- ![Delete plugin confirmation](assets/screenshots/delete-plugin.png)
149
-
150
144
  > Toggling a shared source never changes its files; only DSH-local skills can move to Trash.
151
145
 
152
146
  Escape closes only the frontmost upload or confirmation dialog and leaves Settings open.
package/README.zh-CN.md CHANGED
@@ -34,17 +34,13 @@
34
34
 
35
35
  ![按来源管理技能的设置页面](assets/screenshots/skills-manager-v2-preview.png)
36
36
 
37
- 本地 DSH 技能为空时,公共 Agent 技能仍然可见:
37
+ 打开任意技能可查看来源路径、诊断结果、Markdown 正文与解析后的 frontmatter:
38
38
 
39
- ![未安装本地技能时仍可查看公共 Agent 技能](assets/screenshots/skills-public.png)
39
+ ![技能正文与诊断详情](assets/screenshots/skill-detail.png)
40
40
 
41
- 紧凑上传弹窗在同一个流程中支持 `.zip`、技能文件夹、单个 `SKILL.md` 与拖放。它只使用浏览器原生选择器,不要求绝对路径,也不会再打开第二个选择器:
41
+ DSH 本地技能移入回收站前需要确认;永久删除前仍可恢复:
42
42
 
43
- ![上传插件弹窗](assets/screenshots/upload-plugin.png)
44
-
45
- 启用 DSH 本地技能后,聊天输入框会恢复对应的 `/` 命令:
46
-
47
- ![启用本地技能后的斜杠命令](assets/screenshots/slash-command.png)
43
+ ![移到回收站确认框](assets/screenshots/delete-plugin.png)
48
44
 
49
45
  ## DSH 产品生态
50
46
 
@@ -145,8 +141,6 @@ dsh --profile web --dump-config
145
141
  | 从对话创建 | 让 Agent 调用 `create_skill`;写入前由 DSH 审批界面确认。 | `$DSH_HOME\skills` |
146
142
  | 删除与恢复 | 删除先进入回收站;可恢复或永久二次删除。 | DSH 本地技能 |
147
143
 
148
- ![删除插件确认框](assets/screenshots/delete-plugin.png)
149
-
150
144
  > 共享来源的启停不会修改源文件;只有 DSH 本地技能可以移入回收站。
151
145
 
152
146
  按 ESC 只关闭最上层上传框或确认框,设置页会保持打开。
package/lib/client.js CHANGED
@@ -112,18 +112,25 @@ window.__ModuleLoader__.load({
112
112
  function normalizeSkillQuery(query) { return String(query == null ? "" : query).trim().toLowerCase(); }
113
113
  function matchSkillQuery(skill, query) { var q = normalizeSkillQuery(query); if (!q) return true; return [skill.name, skill.declaredName, skill.description, skill.kind, skill.kindLabel, skill.statusLabel, skill.rootKey, skill.rootLabel].some(function (value) { return String(value == null ? "" : value).toLowerCase().includes(q); }); }
114
114
  function filterSkills(list, options) { var rootKey = options && options.rootKey != null ? options.rootKey : ""; return list.filter(function (skill) { return (!rootKey || skill.rootKey === rootKey) && matchSkillQuery(skill, options && options.query); }); }
115
+ var MAX_UPLOAD_ARCHIVE_BYTES = 10 << 20, MAX_UPLOAD_ENTRY_BYTES = 5 << 20, MAX_UPLOAD_TOTAL_BYTES = 25 << 20, MAX_UPLOAD_ENTRIES = 500;
115
116
  function uploadFilePath(file) { return String(file && (file._dssmPath || file.webkitRelativePath || file.name) || "").replace(/\\/g, "/"); }
116
117
  function inspectUploadSelection(files) {
117
118
  var list = Array.prototype.slice.call(files || []); if (!list.length) return null;
119
+ if (list.length > MAX_UPLOAD_ENTRIES) return { error: { code: "error.upload.tooMany", params: { limit: MAX_UPLOAD_ENTRIES } } };
118
120
  var relative = list.some(function (file) { return uploadFilePath(file).includes("/"); });
119
121
  if (relative) {
120
122
  if (!list.some(function (file) { return /(^|\/)skill\.md$/i.test(uploadFilePath(file)); })) return { error: { code: "select.folder.invalid" } };
121
- return { kind: "folder", name: uploadFilePath(list[0]).split("/")[0], files: list, count: list.length, size: list.reduce(function (sum, file) { return sum + Number(file.size || 0); }, 0) };
123
+ var oversized = list.find(function (file) { return Number(file.size || 0) > MAX_UPLOAD_ENTRY_BYTES; });
124
+ if (oversized) return { error: { code: "error.upload.tooLarge", params: { limit: MAX_UPLOAD_ENTRY_BYTES } } };
125
+ var total = list.reduce(function (sum, file) { return sum + Number(file.size || 0); }, 0);
126
+ if (total > MAX_UPLOAD_TOTAL_BYTES) return { error: { code: "error.upload.tooLarge", params: { limit: MAX_UPLOAD_TOTAL_BYTES } } };
127
+ return { kind: "folder", name: uploadFilePath(list[0]).split("/")[0], files: list, count: list.length, size: total };
122
128
  }
123
129
  if (list.length !== 1) return { error: { code: "select.file.invalid" } };
124
130
  var name = String(list[0].name || ""), lower = name.toLowerCase();
125
- if (lower === "skill.md") return { kind: "skill", name: name, files: list, count: 1, size: Number(list[0].size || 0) };
126
- if (lower.endsWith(".zip")) return { kind: "zip", name: name, files: list, count: 1, size: Number(list[0].size || 0) };
131
+ var size = Number(list[0].size || 0);
132
+ if (lower === "skill.md") return size > MAX_UPLOAD_ENTRY_BYTES ? { error: { code: "error.upload.tooLarge", params: { limit: MAX_UPLOAD_ENTRY_BYTES } } } : { kind: "skill", name: name, files: list, count: 1, size: size };
133
+ if (lower.endsWith(".zip")) return size > MAX_UPLOAD_ARCHIVE_BYTES ? { error: { code: "error.upload.archiveTooLarge", params: { limit: MAX_UPLOAD_ARCHIVE_BYTES } } } : { kind: "zip", name: name, files: list, count: 1, size: size };
127
134
  return { error: { code: "select.file.invalid" } };
128
135
  }
129
136
  function bytesToBase64(buffer) { var bytes = new Uint8Array(buffer), binary = "", chunk = 0x8000; for (var i = 0; i < bytes.length; i += chunk) binary += String.fromCharCode.apply(null, bytes.subarray(i, Math.min(i + chunk, bytes.length))); return btoa(binary); }
package/lib/core.js CHANGED
@@ -1012,11 +1012,22 @@ function normalizeUploadPath(input) {
1012
1012
 
1013
1013
  function decodeUploadBase64(value, maxBytes, code = "error.upload.tooLarge") {
1014
1014
  const raw = String(value == null ? "" : value);
1015
- if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(raw)) {
1015
+ const padding = raw.endsWith("==") ? 2 : raw.endsWith("=") ? 1 : 0;
1016
+ const dataLength = raw.length - padding;
1017
+ const firstPadding = raw.indexOf("=");
1018
+ if (raw.length % 4 !== 0 || (firstPadding !== -1 && firstPadding !== dataLength)) {
1016
1019
  throw codedError("上传内容不是合法 Base64", "error.upload.encoding");
1017
1020
  }
1021
+ const decodedLength = raw.length / 4 * 3 - padding;
1022
+ if (decodedLength > maxBytes) throw codedError(`上传内容超过 ${maxBytes} 字节限制`, code, { limit: maxBytes });
1023
+ // 避免对数 MiB 字符串使用带重复分组的正则;V8 可能在合法大文件上耗尽调用栈。
1024
+ for (let index = 0; index < dataLength; index += 1) {
1025
+ const char = raw.charCodeAt(index);
1026
+ if (!((char >= 65 && char <= 90) || (char >= 97 && char <= 122) || (char >= 48 && char <= 57) || char === 43 || char === 47)) {
1027
+ throw codedError("上传内容不是合法 Base64", "error.upload.encoding");
1028
+ }
1029
+ }
1018
1030
  const bytes = Buffer.from(raw, "base64");
1019
- if (bytes.length > maxBytes) throw codedError(`上传内容超过 ${maxBytes} 字节限制`, code, { limit: maxBytes });
1020
1031
  return bytes;
1021
1032
  }
1022
1033
 
package/lib/index.js CHANGED
@@ -27,7 +27,8 @@ const name = "skills-manager";
27
27
  const inject = ["webServer", "skills", "tools"];
28
28
  const CLIENT_MARKER_HEADER = "x-dsh-skills-manager";
29
29
  const MAX_LOG_BYTES = 1 << 20;
30
- const MAX_UPLOAD_BODY_BYTES = 16 << 20;
30
+ // 文件夹上传允许 25 MiB 原始内容;Base64 会膨胀约 1/3,再为最多 500 条路径预留余量。
31
+ const MAX_UPLOAD_BODY_BYTES = 36 << 20;
31
32
 
32
33
  function makeLog() {
33
34
  const file = logPath();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michengai/dsh-skills-manager",
3
- "version": "0.1.25",
3
+ "version": "0.1.26",
4
4
  "description": "NPM-installable DSH Web plugin for safely loading and managing skills across DSH and common local Agents.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {