dsh-plugin-labels 0.0.0-stage → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +169 -3
- package/cordis.patch.yml +4 -0
- package/icon.svg +5 -0
- package/lib/client.js +407 -0
- package/lib/index.js +1075 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +56 -6
- package/scripts/gen-doc.mjs +306 -0
package/locale/en.json
ADDED
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "dsh-plugin-labels",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-plugin-labels",
|
|
3
|
+
"version": "0.7.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"description": "Chinese names and one-line explanations for installed DSH plugins; automatic translation and in-place display on the official Plugins page.",
|
|
7
|
+
"author": "qyy2005",
|
|
8
|
+
"homepage": "https://github.com/qyy2005/dsh-plugin-labels#readme",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/qyy2005/dsh-plugin-labels.git"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/qyy2005/dsh-plugin-labels/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"dsh",
|
|
18
|
+
"deepseek-harness",
|
|
19
|
+
"plugin",
|
|
20
|
+
"dsh-plugin",
|
|
21
|
+
"labels",
|
|
22
|
+
"chinese",
|
|
23
|
+
"localization"
|
|
24
|
+
],
|
|
25
|
+
"main": "lib/index.js",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./lib/index.js",
|
|
28
|
+
"./client": "./lib/client.js",
|
|
29
|
+
"./locale/*.json": "./locale/*.json",
|
|
30
|
+
"./icon": "./icon.svg",
|
|
31
|
+
"./package.json": "./package.json"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"lib/index.js",
|
|
35
|
+
"lib/client.js",
|
|
36
|
+
"cordis.patch.yml",
|
|
37
|
+
"locale/*.json",
|
|
38
|
+
"icon.svg",
|
|
39
|
+
"scripts/gen-doc.mjs"
|
|
40
|
+
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"docs": "node scripts/gen-doc.mjs"
|
|
43
|
+
},
|
|
44
|
+
"dsh": {
|
|
45
|
+
"bundle": {
|
|
46
|
+
"patch": "./cordis.patch.yml"
|
|
47
|
+
},
|
|
48
|
+
"client": {
|
|
49
|
+
"platform": "web",
|
|
50
|
+
"immediately": true,
|
|
51
|
+
"inject": [
|
|
52
|
+
"@deepseek-ai/dsh-client-locale"
|
|
53
|
+
]
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Generate the public plugin index for dsh-plugin-labels.
|
|
4
|
+
*
|
|
5
|
+
* Reads the label cache (`plugin-labels.json`) plus the installed profile
|
|
6
|
+
* manifest and every bundle's package.json, then writes two committed docs:
|
|
7
|
+
* - PLUGINS.zh.md Markdown table for the repository
|
|
8
|
+
* - PLUGINS.zh.html single-file, searchable HTML (inline CSS, no dependencies)
|
|
9
|
+
*
|
|
10
|
+
* The generated files are public: they only contain package names, Chinese
|
|
11
|
+
* labels, descriptions and versions — never local paths, the DSH home, user
|
|
12
|
+
* names or tokens.
|
|
13
|
+
*
|
|
14
|
+
* Usage:
|
|
15
|
+
* node scripts/gen-doc.mjs [--home <DSH_HOME>] [--profile <name>] [--out-dir <dir>]
|
|
16
|
+
*
|
|
17
|
+
* Defaults: `$DSH_HOME` (or ~/.dsh), profile "desktop", this repository root.
|
|
18
|
+
*/
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { fileURLToPath } from "node:url";
|
|
23
|
+
|
|
24
|
+
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
25
|
+
const OUTPUT_MD = "PLUGINS.zh.md";
|
|
26
|
+
const OUTPUT_HTML = "PLUGINS.zh.html";
|
|
27
|
+
const DESCRIPTION_LIMIT = 120;
|
|
28
|
+
|
|
29
|
+
function fail(message) {
|
|
30
|
+
console.error("gen-doc: " + message);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function usage() {
|
|
35
|
+
return [
|
|
36
|
+
"Usage: node scripts/gen-doc.mjs [--home <DSH_HOME>] [--profile <name>] [--out-dir <dir>]",
|
|
37
|
+
"",
|
|
38
|
+
" --home DSH home directory (default: $DSH_HOME or ~/.dsh)",
|
|
39
|
+
" --profile profile name (default: desktop)",
|
|
40
|
+
" --out-dir output directory (default: this repository root)",
|
|
41
|
+
].join("\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseArgs(argv) {
|
|
45
|
+
const fromEnv = typeof process.env.DSH_HOME === "string" ? process.env.DSH_HOME.trim() : "";
|
|
46
|
+
const options = {
|
|
47
|
+
home: fromEnv !== "" ? fromEnv : path.join(os.homedir(), ".dsh"),
|
|
48
|
+
profile: "desktop",
|
|
49
|
+
outDir: REPO_ROOT,
|
|
50
|
+
};
|
|
51
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
52
|
+
const arg = argv[index];
|
|
53
|
+
if (arg === "--help" || arg === "-h") {
|
|
54
|
+
console.log(usage());
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
if (arg !== "--home" && arg !== "--profile" && arg !== "--out-dir") {
|
|
58
|
+
fail("unknown argument: " + arg + "\n" + usage());
|
|
59
|
+
}
|
|
60
|
+
const value = argv[index + 1];
|
|
61
|
+
if (typeof value !== "string" || value.trim() === "") fail(arg + " requires a value\n" + usage());
|
|
62
|
+
index += 1;
|
|
63
|
+
if (arg === "--home") options.home = value.trim();
|
|
64
|
+
else if (arg === "--profile") options.profile = value.trim();
|
|
65
|
+
else options.outDir = value.trim();
|
|
66
|
+
}
|
|
67
|
+
return options;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function readJson(file, label) {
|
|
71
|
+
let raw;
|
|
72
|
+
try {
|
|
73
|
+
raw = fs.readFileSync(file, "utf8");
|
|
74
|
+
} catch (error) {
|
|
75
|
+
fail(
|
|
76
|
+
"cannot read " + label + " (" + (error && error.code ? error.code : "unknown error") + "); " +
|
|
77
|
+
"pass --home <DSH_HOME> / --profile <name> when the layout is not the default",
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
try {
|
|
81
|
+
return JSON.parse(raw);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
fail(label + " is not valid JSON (" + String((error && error.message) || error) + ")");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function cleanText(value, fallback) {
|
|
88
|
+
if (typeof value !== "string") return fallback;
|
|
89
|
+
const text = value.replace(/\s+/g, " ").trim();
|
|
90
|
+
return text === "" ? fallback : text;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function truncate(value, limit) {
|
|
94
|
+
const characters = Array.from(value);
|
|
95
|
+
if (characters.length <= limit) return value;
|
|
96
|
+
return characters.slice(0, limit - 1).join("") + "…";
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function loadEntries(home) {
|
|
100
|
+
const cache = readJson(path.join(home, "plugin-labels.json"), "plugin-labels.json");
|
|
101
|
+
const entries = cache && cache.entries;
|
|
102
|
+
if (!entries || typeof entries !== "object" || Array.isArray(entries)) {
|
|
103
|
+
fail("plugin-labels.json has no usable entries object");
|
|
104
|
+
}
|
|
105
|
+
return entries;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function loadBundles(home, profile) {
|
|
109
|
+
const manifest = readJson(path.join(home, "profiles", profile, "package.json"), "the profile manifest (profiles/<profile>/package.json)");
|
|
110
|
+
const bundles = manifest && manifest.dsh && manifest.dsh.profile && manifest.dsh.profile.bundles;
|
|
111
|
+
if (!Array.isArray(bundles)) fail("the profile manifest has no dsh.profile.bundles array");
|
|
112
|
+
const nodeModules = path.join(home, "profiles", profile, "node_modules");
|
|
113
|
+
const names = bundles.filter((name) => typeof name === "string" && name.trim() !== "");
|
|
114
|
+
if (names.length === 0) fail("the profile manifest lists no bundles; nothing to generate");
|
|
115
|
+
return names.map((name) => {
|
|
116
|
+
let version = "";
|
|
117
|
+
let description = "";
|
|
118
|
+
try {
|
|
119
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(nodeModules, name, "package.json"), "utf8"));
|
|
120
|
+
version = cleanText(pkg.version, "");
|
|
121
|
+
description = cleanText(pkg.description, "");
|
|
122
|
+
} catch {
|
|
123
|
+
// A missing bundle manifest only leaves version/description empty.
|
|
124
|
+
}
|
|
125
|
+
return { name, version, description };
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function buildRows(bundles, entries) {
|
|
130
|
+
return bundles.map((bundle) => {
|
|
131
|
+
const raw = entries[bundle.name];
|
|
132
|
+
const label = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
|
|
133
|
+
const title = label ? cleanText(label.title, "") : "";
|
|
134
|
+
const oneLine = label ? cleanText(label.oneLine, "") : "";
|
|
135
|
+
const translated = title !== "" && oneLine !== "";
|
|
136
|
+
const status = !translated ? "待翻译" : label.source === "manual" ? "手改" : label.source === "ai" ? "AI 生成" : "—";
|
|
137
|
+
return {
|
|
138
|
+
name: bundle.name,
|
|
139
|
+
title: translated ? title : "未翻译",
|
|
140
|
+
oneLine: translated ? oneLine : truncate(bundle.description === "" ? "(无描述)" : bundle.description, DESCRIPTION_LIMIT),
|
|
141
|
+
version: bundle.version,
|
|
142
|
+
status,
|
|
143
|
+
translated,
|
|
144
|
+
};
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function timestamp(date) {
|
|
149
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
150
|
+
return date.getFullYear() + "-" + pad(date.getMonth() + 1) + "-" + pad(date.getDate()) + " " +
|
|
151
|
+
pad(date.getHours()) + ":" + pad(date.getMinutes()) + ":" + pad(date.getSeconds());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Refuse to publish anything that looks like a local path or a secret. */
|
|
155
|
+
function assertPublic(text) {
|
|
156
|
+
const patterns = [
|
|
157
|
+
[/\b[A-Za-z]:[\\/]/, "a Windows drive path"],
|
|
158
|
+
[/[\\/]Users[\\/]/i, "a user home path"],
|
|
159
|
+
[/\.dsh[\\/]/i, "the DSH home path"],
|
|
160
|
+
[/ghp_[A-Za-z0-9]{10,}/, "a GitHub token"],
|
|
161
|
+
[/github_pat_[A-Za-z0-9_]{10,}/, "a GitHub token"],
|
|
162
|
+
[/sk-[A-Za-z0-9]{10,}/, "an API key"],
|
|
163
|
+
[/-{5}BEGIN/, "a private key"],
|
|
164
|
+
];
|
|
165
|
+
for (const [pattern, description] of patterns) {
|
|
166
|
+
if (pattern.test(text)) fail("refusing to write documentation containing " + description);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function markdownCell(value) {
|
|
171
|
+
return String(value).replace(/\\/g, "\\\\").replace(/\|/g, "\\|");
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderMarkdown(rows, translated, generatedAt) {
|
|
175
|
+
const lines = [
|
|
176
|
+
"# DSH 插件中文名速查表",
|
|
177
|
+
"",
|
|
178
|
+
"> 由 dsh-plugin-labels 的标签缓存生成,请勿手工修改;重新生成:`node scripts/gen-doc.mjs`。",
|
|
179
|
+
">",
|
|
180
|
+
"> 生成时间:" + generatedAt + "(本地时间) · 已翻译 " + translated + " / " + rows.length + " 个已安装插件。",
|
|
181
|
+
"",
|
|
182
|
+
"| 中文名 | 原始包名 | 一句话作用 | 版本 | 说明 |",
|
|
183
|
+
"| --- | --- | --- | --- | --- |",
|
|
184
|
+
];
|
|
185
|
+
for (const row of rows) {
|
|
186
|
+
lines.push(
|
|
187
|
+
"| " + markdownCell(row.title) + " | `" + markdownCell(row.name) + "` | " + markdownCell(row.oneLine) +
|
|
188
|
+
" | " + markdownCell(row.version) + " | " + markdownCell(row.status) + " |",
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
lines.push("");
|
|
192
|
+
return lines.join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function escapeHtml(value) {
|
|
196
|
+
return String(value).replace(/[&<>"']/g, (character) => ({
|
|
197
|
+
"&": "&",
|
|
198
|
+
"<": "<",
|
|
199
|
+
">": ">",
|
|
200
|
+
'"': """,
|
|
201
|
+
"'": "'",
|
|
202
|
+
}[character]));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const HTML_STYLE = [
|
|
206
|
+
":root{color-scheme:light dark;--bg:#ffffff;--fg:#1f2328;--muted:#656d76;--line:#d0d7de;--code:#f6f8fa;--accent:#0969da}",
|
|
207
|
+
"@media (prefers-color-scheme:dark){:root{--bg:#0d1117;--fg:#e6edf3;--muted:#8b949e;--line:#30363d;--code:#161b22;--accent:#2f81f7}}",
|
|
208
|
+
"*{box-sizing:border-box}",
|
|
209
|
+
'body{margin:0;background:var(--bg);color:var(--fg);font:14px/1.6 -apple-system,BlinkMacSystemFont,"Segoe UI","Noto Sans SC","Microsoft YaHei",sans-serif}',
|
|
210
|
+
"main{max-width:1080px;margin:0 auto;padding:32px 20px 64px}",
|
|
211
|
+
"h1{font-size:22px;margin:0 0 8px}",
|
|
212
|
+
".meta{color:var(--muted);font-size:12.5px;margin:0 0 18px}",
|
|
213
|
+
".meta code,td.name code{background:var(--code);padding:1px 5px;border-radius:5px;font-size:12px}",
|
|
214
|
+
".search{display:flex;gap:10px;align-items:center;margin:0 0 12px}",
|
|
215
|
+
".search input{flex:1 1 auto;max-width:420px;padding:7px 10px;font:inherit;color:inherit;background:var(--code);border:1px solid var(--line);border-radius:8px;outline:none}",
|
|
216
|
+
".search input:focus{border-color:var(--accent)}",
|
|
217
|
+
"#count{color:var(--muted);font-size:12px}",
|
|
218
|
+
"table{width:100%;border-collapse:collapse;font-size:13px}",
|
|
219
|
+
"th,td{text-align:left;vertical-align:top;padding:8px 10px;border-bottom:1px solid var(--line)}",
|
|
220
|
+
"th{color:var(--muted);font-weight:600;white-space:nowrap}",
|
|
221
|
+
"tbody tr:hover{background:var(--code)}",
|
|
222
|
+
"td.title{font-weight:700}",
|
|
223
|
+
"td.status{color:var(--muted);white-space:nowrap}",
|
|
224
|
+
".empty{color:var(--muted)}",
|
|
225
|
+
"@media (max-width:720px){th:nth-child(4),td:nth-child(4){display:none}}",
|
|
226
|
+
].join("");
|
|
227
|
+
|
|
228
|
+
function renderHtml(rows, translated, generatedAt) {
|
|
229
|
+
const body = rows.map((row) => [
|
|
230
|
+
"<tr>",
|
|
231
|
+
'<td class="title">' + escapeHtml(row.title) + "</td>",
|
|
232
|
+
'<td class="name"><code>' + escapeHtml(row.name) + "</code></td>",
|
|
233
|
+
"<td>" + escapeHtml(row.oneLine) + "</td>",
|
|
234
|
+
"<td>" + escapeHtml(row.version) + "</td>",
|
|
235
|
+
'<td class="status">' + escapeHtml(row.status) + "</td>",
|
|
236
|
+
"</tr>",
|
|
237
|
+
].join("")).join("\n");
|
|
238
|
+
return [
|
|
239
|
+
"<!DOCTYPE html>",
|
|
240
|
+
'<html lang="zh-CN">',
|
|
241
|
+
"<head>",
|
|
242
|
+
'<meta charset="utf-8">',
|
|
243
|
+
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
244
|
+
"<title>DSH 插件中文名速查表</title>",
|
|
245
|
+
"<style>" + HTML_STYLE + "</style>",
|
|
246
|
+
"</head>",
|
|
247
|
+
"<body>",
|
|
248
|
+
"<main>",
|
|
249
|
+
"<h1>DSH 插件中文名速查表</h1>",
|
|
250
|
+
'<p class="meta">由 dsh-plugin-labels 的标签缓存生成,请勿手工修改;重新生成:<code>node scripts/gen-doc.mjs</code>。<br>生成时间:' +
|
|
251
|
+
escapeHtml(generatedAt) + "(本地时间) · 已翻译 " + translated + " / " + rows.length + " 个已安装插件。</p>",
|
|
252
|
+
'<p class="search"><input id="q" type="search" placeholder="搜索中文名、包名或作用…" autocomplete="off"><span id="count"></span></p>',
|
|
253
|
+
"<table>",
|
|
254
|
+
"<thead><tr><th>中文名</th><th>原始包名</th><th>一句话作用</th><th>版本</th><th>说明</th></tr></thead>",
|
|
255
|
+
'<tbody id="rows">',
|
|
256
|
+
body,
|
|
257
|
+
"</tbody>",
|
|
258
|
+
"</table>",
|
|
259
|
+
'<p class="empty" id="empty" hidden>没有匹配的插件</p>',
|
|
260
|
+
"</main>",
|
|
261
|
+
"<script>",
|
|
262
|
+
"(function () {",
|
|
263
|
+
' var input = document.getElementById("q");',
|
|
264
|
+
' var rows = Array.prototype.slice.call(document.querySelectorAll("#rows tr"));',
|
|
265
|
+
' var count = document.getElementById("count");',
|
|
266
|
+
' var empty = document.getElementById("empty");',
|
|
267
|
+
" function render() {",
|
|
268
|
+
" var query = input.value.trim().toLowerCase();",
|
|
269
|
+
" var shown = 0;",
|
|
270
|
+
" rows.forEach(function (row) {",
|
|
271
|
+
" var visible = query === \"\" || row.textContent.toLowerCase().indexOf(query) >= 0;",
|
|
272
|
+
" row.hidden = !visible;",
|
|
273
|
+
" if (visible) shown += 1;",
|
|
274
|
+
" });",
|
|
275
|
+
" count.textContent = shown + \" / \" + rows.length;",
|
|
276
|
+
" empty.hidden = shown !== 0;",
|
|
277
|
+
" }",
|
|
278
|
+
' input.addEventListener("input", render);',
|
|
279
|
+
" render();",
|
|
280
|
+
"})();",
|
|
281
|
+
"</script>",
|
|
282
|
+
"</body>",
|
|
283
|
+
"</html>",
|
|
284
|
+
"",
|
|
285
|
+
].join("\n");
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function writeOutput(file, content) {
|
|
289
|
+
assertPublic(content);
|
|
290
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
291
|
+
fs.writeFileSync(file, content, "utf8");
|
|
292
|
+
console.log("gen-doc: wrote " + path.basename(file) + " (" + Buffer.byteLength(content, "utf8") + " bytes)");
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const options = parseArgs(process.argv.slice(2));
|
|
296
|
+
const entries = loadEntries(options.home);
|
|
297
|
+
const bundles = loadBundles(options.home, options.profile);
|
|
298
|
+
if (Object.keys(entries).length === 0) {
|
|
299
|
+
console.error("gen-doc: the label cache is empty; every row will be marked 未翻译");
|
|
300
|
+
}
|
|
301
|
+
const rows = buildRows(bundles, entries);
|
|
302
|
+
const translated = rows.filter((row) => row.translated).length;
|
|
303
|
+
const generatedAt = timestamp(new Date());
|
|
304
|
+
writeOutput(path.join(options.outDir, OUTPUT_MD), renderMarkdown(rows, translated, generatedAt));
|
|
305
|
+
writeOutput(path.join(options.outDir, OUTPUT_HTML), renderHtml(rows, translated, generatedAt));
|
|
306
|
+
console.log("gen-doc: " + rows.length + " plugins, " + translated + " translated");
|