dsh-plugin-yanyan-toolkit 0.0.0-stage → 1.4.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/LICENSE +29 -0
  3. package/README.en.md +82 -0
  4. package/README.i18n.yaml +7 -0
  5. package/README.md +81 -2
  6. package/README.zh.md +82 -0
  7. package/cordis.patch.yml +43 -0
  8. package/icon.svg +5 -0
  9. package/lib/client.js +201 -0
  10. package/lib/index.js +149 -0
  11. package/locale/en.json +6 -0
  12. package/locale/zh.json +6 -0
  13. package/package.json +82 -4
  14. package/packages/dsh-plugin-yanyan-icon/README.md +33 -0
  15. package/packages/dsh-plugin-yanyan-icon/README.zh.md +33 -0
  16. package/packages/dsh-plugin-yanyan-icon/cordis.patch.yml +7 -0
  17. package/packages/dsh-plugin-yanyan-icon/icon.svg +7 -0
  18. package/packages/dsh-plugin-yanyan-icon/lib/client.js +388 -0
  19. package/packages/dsh-plugin-yanyan-icon/lib/index.js +237 -0
  20. package/packages/dsh-plugin-yanyan-icon/locale/en.json +6 -0
  21. package/packages/dsh-plugin-yanyan-icon/locale/zh.json +6 -0
  22. package/packages/dsh-plugin-yanyan-icon/package.json +32 -0
  23. package/packages/dsh-plugin-yanyan-subagent/README.md +73 -0
  24. package/packages/dsh-plugin-yanyan-subagent/README.zh.md +73 -0
  25. package/packages/dsh-plugin-yanyan-subagent/cordis.patch.yml +8 -0
  26. package/packages/dsh-plugin-yanyan-subagent/icon.svg +8 -0
  27. package/packages/dsh-plugin-yanyan-subagent/lib/client.js +1374 -0
  28. package/packages/dsh-plugin-yanyan-subagent/lib/index.js +992 -0
  29. package/packages/dsh-plugin-yanyan-subagent/lib/legacy-hidden-ids.json +29 -0
  30. package/packages/dsh-plugin-yanyan-subagent/lib/log-rewrite.js +411 -0
  31. package/packages/dsh-plugin-yanyan-subagent/lib/zstd-frames.js +81 -0
  32. package/packages/dsh-plugin-yanyan-subagent/locale/en.json +1 -0
  33. package/packages/dsh-plugin-yanyan-subagent/locale/zh.json +1 -0
  34. package/packages/dsh-plugin-yanyan-subagent/package.json +32 -0
  35. package/packages/dsh-plugin-yanyan-update/README.md +25 -0
  36. package/packages/dsh-plugin-yanyan-update/README.zh.md +25 -0
  37. package/packages/dsh-plugin-yanyan-update/cordis.patch.yml +7 -0
  38. package/packages/dsh-plugin-yanyan-update/icon.svg +5 -0
  39. package/packages/dsh-plugin-yanyan-update/lib/client.js +400 -0
  40. package/packages/dsh-plugin-yanyan-update/lib/index.js +924 -0
  41. package/packages/dsh-plugin-yanyan-update/locale/en.json +1 -0
  42. package/packages/dsh-plugin-yanyan-update/locale/zh.json +1 -0
  43. package/packages/dsh-plugin-yanyan-update/package.json +32 -0
  44. package/scripts/cleanup-ghost-subagents.mjs +241 -0
package/lib/client.js ADDED
@@ -0,0 +1,201 @@
1
+ // dsh-plugin-yanyan-toolkit — Client (browser) half.
2
+ //
3
+ // 根包是整包元数据的天然宿主:在插件详情页「包含的组件」列表里,给
4
+ // 每个组件自己的中文标题右侧补一个版本标签(v1.2.3 这种)。
5
+ //
6
+ // 官方「包含的组件」列表(app.asar 内 PluginManagerPage 的 RowsSection)
7
+ // 渲染的是 InstalledPluginChild,其协议只有 id/name/enabled/locked,
8
+ // 没有 version 字段,所以版本显示必须由 client 半区自行注入:
9
+ // - section 有 [data-plugin-rows],每行 li[data-plugin-row];
10
+ // - rowMain 里两个 <code> 分别写着 loader id 与「真实包名」;
11
+ // 按真实包名去 host /components 返回的版本表里查,只处理本包 4 个组件,
12
+ // 不会误伤其它插件的详情页。
13
+ // - CSS 类名是 CSS Modules 编译过的 hash,不能依赖类名字符串,
14
+ // 一律靠 data 属性 + <code> 文本定位。
15
+ //
16
+ // 装载骨架与「observer + 16ms 节流 + ctx.effect 统一清理」严格照抄现成轮子
17
+ // packages/dsh-plugin-yanyan-update/lib/client.js:354-392,容器/标签自持、
18
+ // 不改 React 管理的 DOM、不干扰 reconciliation;React 重渲染清掉我们的
19
+ // span 后由 MutationObserver 自动重挂。inject 保持 [](fetch / document 原生,
20
+ // 不依赖任何服务),标签文案只有 v + 版本号,无需 locale。
21
+
22
+ (function (root) {
23
+ if (!root) return;
24
+ if (!root.__ModuleLoader__ || typeof root.__ModuleLoader__.load !== 'function') return;
25
+
26
+ root.__ModuleLoader__.load({
27
+ id: 'dsh-plugin-yanyan-toolkit',
28
+ factory: () => {
29
+ var module = { exports: {} };
30
+ var exports = module.exports;
31
+ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
32
+
33
+ var BASE = '/__dsh-plugin-yanyan-toolkit/components';
34
+ var MARK = 'data-yanyan-component-version';
35
+ var STYLE_MARK = 'data-yanyan-component-version-style';
36
+
37
+ // 用主题变量带回退色,照抄 update 包 CSS 的写法。
38
+ var CSS = [
39
+ '[' + MARK + ']{display:inline-block;margin-left:6px;padding:0 6px;border-radius:999px;' +
40
+ 'font-size:11px;line-height:1.6;font-weight:500;vertical-align:middle;white-space:nowrap;' +
41
+ 'background:var(--dsw-alias-fill-subtle,#f3f4f6);' +
42
+ 'color:var(--dsw-alias-label-secondary-foreground,#6b7280);' +
43
+ 'border:1px solid var(--dsw-alias-stroke-subtle,#e5e7eb);}',
44
+ ];
45
+
46
+ var versions = null;
47
+ var fetching = false;
48
+ var observer = null;
49
+ var ensureTimer = null;
50
+ var disposed = false;
51
+
52
+ function ensureStyle() {
53
+ try {
54
+ if (document.querySelector('style[' + STYLE_MARK + ']')) return;
55
+ var st = document.createElement('style');
56
+ st.setAttribute(STYLE_MARK, '');
57
+ st.textContent = CSS.join('\n');
58
+ (document.head || document.documentElement).appendChild(st);
59
+ } catch (e) {}
60
+ }
61
+
62
+ function loadVersions() {
63
+ if (disposed || versions || fetching) return;
64
+ fetching = true;
65
+ try {
66
+ fetch(BASE, { credentials: 'same-origin' })
67
+ .then(function (r) { return r.json(); })
68
+ .then(function (d) {
69
+ if (d && d.ok === true && d.components && typeof d.components === 'object') {
70
+ versions = d.components;
71
+ scheduleEnsure();
72
+ }
73
+ })
74
+ .catch(function () {})
75
+ .then(function () { fetching = false; });
76
+ } catch (e) {
77
+ fetching = false;
78
+ }
79
+ }
80
+
81
+ // 两个 <code> 的直接父容器就是 rowMain;其第一个元素子是标题。
82
+ // configure 形态下第一个元素子是 button.rowOpen,标题 span 在按钮内部。
83
+ function titleElement(codes) {
84
+ var main = null;
85
+ for (var i = 0; i < codes.length; i++) {
86
+ if (codes[i].parentElement) { main = codes[i].parentElement; break; }
87
+ }
88
+ if (!main) return null;
89
+ var first = main.firstElementChild;
90
+ if (!first) return null;
91
+ if (first.tagName === 'BUTTON') {
92
+ for (var j = 0; j < first.children.length; j++) {
93
+ if (first.children[j].tagName === 'SPAN') return first.children[j];
94
+ }
95
+ }
96
+ return first;
97
+ }
98
+
99
+ function ensureTags() {
100
+ if (disposed) return;
101
+ if (!versions) { loadVersions(); return; }
102
+ ensureStyle();
103
+ var lists;
104
+ try {
105
+ lists = document.querySelectorAll('section[data-plugin-rows] li[data-plugin-row]');
106
+ } catch (e) {
107
+ return;
108
+ }
109
+ for (var i = 0; i < lists.length; i++) {
110
+ var li = lists[i];
111
+ var codes;
112
+ try { codes = li.querySelectorAll('code'); } catch (e) { continue; }
113
+ if (!codes || codes.length === 0) continue;
114
+
115
+ // 只认本聚合包里的组件包名,其它插件详情页一概不动。
116
+ var ver = null;
117
+ for (var k = 0; k < codes.length; k++) {
118
+ var pkg = (codes[k].textContent || '').trim();
119
+ if (Object.prototype.hasOwnProperty.call(versions, pkg)) {
120
+ ver = versions[pkg];
121
+ break;
122
+ }
123
+ }
124
+ if (!ver) continue;
125
+
126
+ var host = titleElement(codes);
127
+ if (!host) continue;
128
+ var label = 'v' + ver;
129
+ var existing = null;
130
+ try { existing = host.querySelector('span[' + MARK + ']'); } catch (e) {}
131
+ if (existing) {
132
+ if (existing.getAttribute('data-version') === ver) continue;
133
+ existing.textContent = label;
134
+ existing.setAttribute('data-version', ver);
135
+ continue;
136
+ }
137
+ var tag = document.createElement('span');
138
+ tag.setAttribute(MARK, '');
139
+ tag.setAttribute('data-version', ver);
140
+ tag.textContent = label;
141
+ try { host.appendChild(tag); } catch (e) {}
142
+ }
143
+ }
144
+
145
+ function scheduleEnsure() {
146
+ if (ensureTimer !== null) return;
147
+ ensureTimer = setTimeout(function () {
148
+ ensureTimer = null;
149
+ ensureTags();
150
+ }, 16);
151
+ }
152
+
153
+ var inject = [];
154
+ var apply = function (ctx) {
155
+ // ctx.effect 缺失时退化为「尽力跑一次」,不影响所在页面。
156
+ if (!ctx || typeof ctx.effect !== 'function') {
157
+ loadVersions();
158
+ ensureTags();
159
+ return;
160
+ }
161
+ // 所有 observer / timer 集中在这个 effect 内注册与撤销。
162
+ ctx.effect(function () {
163
+ disposed = false;
164
+ loadVersions();
165
+ ensureTags();
166
+ try {
167
+ observer = new MutationObserver(function () { scheduleEnsure(); });
168
+ observer.observe(document.body, { childList: true, subtree: true });
169
+ } catch (e) {
170
+ observer = null;
171
+ }
172
+
173
+ return function () {
174
+ disposed = true;
175
+ try { if (observer) observer.disconnect(); } catch (e) {}
176
+ observer = null;
177
+ if (ensureTimer !== null) {
178
+ try { clearTimeout(ensureTimer); } catch (e) {}
179
+ ensureTimer = null;
180
+ }
181
+ try {
182
+ var tags = document.querySelectorAll('span[' + MARK + ']');
183
+ for (var i = 0; i < tags.length; i++) {
184
+ if (tags[i].parentNode) tags[i].parentNode.removeChild(tags[i]);
185
+ }
186
+ } catch (e) {}
187
+ try {
188
+ var st = document.querySelector('style[' + STYLE_MARK + ']');
189
+ if (st && st.parentNode) st.parentNode.removeChild(st);
190
+ } catch (e) {}
191
+ versions = null;
192
+ };
193
+ }, 'yanyan-toolkit: component versions');
194
+ };
195
+
196
+ exports.inject = inject;
197
+ exports.apply = apply;
198
+ return module.exports;
199
+ },
200
+ });
201
+ })(typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : null);
package/lib/index.js ADDED
@@ -0,0 +1,149 @@
1
+ // dsh-plugin-yanyan-toolkit — Host (Node) half.
2
+ //
3
+ // 聚合包的自身 loader 行只用于承载五项 patch 元数据与依赖声明,
4
+ // 四个功能插件各自有独立的 loader 行,由 DSH loader 直接挂载。
5
+ // 因此这里不重复官方全家桶的 dynamic shell(await import + ctx.plugin)
6
+ // 机制,只贡献两条同源回环路由:
7
+ // 1) /health —— 确认本包已被装载;
8
+ // 2) /components —— 动态给出各组件自己的版本,供 client 半区在
9
+ // 插件详情页「包含的组件」列表逐个显示。
10
+ //
11
+ // 版本一律从各组件自身的 package.json 读取,绝不写死,避免文档/DOM
12
+ // 与真实版本失同步(聚合包与四个功能插件可以各自独立发版)。
13
+
14
+ import { readFileSync } from 'node:fs';
15
+ import { createRequire } from 'node:module';
16
+ import { dirname, join } from 'node:path';
17
+ import { fileURLToPath } from 'node:url';
18
+
19
+ export const name = 'dsh-plugin-yanyan-toolkit';
20
+ export const inject = ['webServer', 'connection'];
21
+
22
+ export const HEALTH_ROUTE = '/__dsh-plugin-yanyan-toolkit/health';
23
+ export const COMPONENTS_ROUTE = '/__dsh-plugin-yanyan-toolkit/components';
24
+
25
+ const ROOT_DIR = dirname(dirname(fileURLToPath(import.meta.url)));
26
+ const EXTERNAL_PKG = '@linxin666/dsh-session-archive';
27
+ const SUBPACKAGES = ['dsh-plugin-yanyan-update', 'dsh-plugin-yanyan-subagent', 'dsh-plugin-yanyan-icon'];
28
+
29
+ const FORBIDDEN_BODY = { ok: false, error: 'forbidden', message: '请求未通过 DSH 准入裁决。' };
30
+ const METHOD_BODY = { ok: false, error: 'method-not-allowed', message: '只接受 GET 或 POST 请求。' };
31
+
32
+ function readJsonSafe(file) {
33
+ try {
34
+ return JSON.parse(readFileSync(file, 'utf8'));
35
+ } catch (error) {
36
+ return null;
37
+ }
38
+ }
39
+
40
+ // 根包版本也从 package.json 动态读取,避免再出现 VERSION 常量与包声明失同步。
41
+ export const VERSION = String((readJsonSafe(join(ROOT_DIR, 'package.json')) || {}).version || '0.0.0');
42
+
43
+ /**
44
+ * 收集聚合包内每个组件自己的版本:{ 包名: version }。
45
+ * - 自身与三个自写子包:读各自 package.json 的 version。
46
+ * - 外部复用的会话归档包:优先用 createRequire 解析实际安装包的
47
+ * package.json(拿到真实版本,而非声明范围);解析不到时回退根包
48
+ * dependencies 的声明,并去掉 ^ / ~ 等范围前缀。
49
+ * 纯函数,rootDir 可注入以便单测,读不到一律给 'unknown' 而不抛错。
50
+ */
51
+ export function collectVersions(rootDir = ROOT_DIR) {
52
+ const out = {};
53
+ const rootPkg = readJsonSafe(join(rootDir, 'package.json')) || {};
54
+ out[rootPkg.name || 'dsh-plugin-yanyan-toolkit'] = String(rootPkg.version || 'unknown');
55
+
56
+ for (const sub of SUBPACKAGES) {
57
+ const pkg = readJsonSafe(join(rootDir, 'packages', sub, 'package.json')) || {};
58
+ out[sub] = String(pkg.version || 'unknown');
59
+ }
60
+
61
+ let external = null;
62
+ // 先走规范的 require.resolve(拿到真实安装版本);部分包的 exports 不暴露
63
+ // ./package.json 会抛 ERR_PACKAGE_PATH_NOT_EXPORTED,再直接定位 node_modules
64
+ // 文件路径兜底;两者都失败才回退根包 dependencies 的声明范围(去掉 ^ / ~)。
65
+ const candidates = [];
66
+ try {
67
+ const require = createRequire(join(rootDir, 'package.json'));
68
+ candidates.push(require.resolve(EXTERNAL_PKG + '/package.json', { paths: [rootDir] }));
69
+ } catch (error) {
70
+ // 继续走文件路径兜底
71
+ }
72
+ let dir = rootDir;
73
+ for (let i = 0; i < 4; i++) {
74
+ candidates.push(join(dir, 'node_modules', EXTERNAL_PKG, 'package.json'));
75
+ dir = dirname(dir);
76
+ }
77
+ for (const file of candidates) {
78
+ const v = (readJsonSafe(file) || {}).version;
79
+ if (v) { external = v; break; }
80
+ }
81
+ if (!external) {
82
+ const spec = (rootPkg.dependencies || {})[EXTERNAL_PKG];
83
+ if (spec) external = String(spec).replace(/^[\^~>=<\s]+/, '');
84
+ }
85
+ out[EXTERNAL_PKG] = String(external || 'unknown');
86
+ return out;
87
+ }
88
+
89
+ function respond(res, status, body) {
90
+ const payload = JSON.stringify(body);
91
+ res.writeHead(status, {
92
+ 'content-type': 'application/json; charset=utf-8',
93
+ 'cache-control': 'no-store',
94
+ 'content-length': Buffer.byteLength(payload),
95
+ });
96
+ res.end(payload);
97
+ }
98
+
99
+ // 统一准入:仅当 requestRejection 返回 undefined 时放行(不自建信任例外)。
100
+ function makeFence(ctx) {
101
+ return (req) => {
102
+ try {
103
+ return ctx.connection.requestRejection(req) === undefined;
104
+ } catch (error) {
105
+ return false;
106
+ }
107
+ };
108
+ }
109
+
110
+ function makeRouteHandler(fence, payload) {
111
+ return (req, res) => {
112
+ if (!fence(req)) {
113
+ respond(res, 403, FORBIDDEN_BODY);
114
+ return;
115
+ }
116
+ const method = String(req.method || 'GET').toUpperCase();
117
+ if (method !== 'GET' && method !== 'POST') {
118
+ respond(res, 405, METHOD_BODY);
119
+ return;
120
+ }
121
+ try {
122
+ respond(res, 200, payload());
123
+ } catch (error) {
124
+ respond(res, 200, { ok: false, error: 'internal', message: String((error && error.message) || error) });
125
+ }
126
+ };
127
+ }
128
+
129
+ // register 的返回值直接作为 effect 的清理函数,禁用 / HMR 时路由自动撤销。
130
+ // 复用 dsh-plugin-yanyan-update/lib/index.js:907-918 的多路由注册骨架。
131
+ export function apply(ctx) {
132
+ const fence = makeFence(ctx);
133
+ const routes = [
134
+ {
135
+ path: HEALTH_ROUTE,
136
+ handler: makeRouteHandler(fence, () => ({ ok: true, name, version: VERSION })),
137
+ },
138
+ {
139
+ path: COMPONENTS_ROUTE,
140
+ handler: makeRouteHandler(fence, () => ({ ok: true, components: collectVersions() })),
141
+ },
142
+ ];
143
+ for (const route of routes) {
144
+ ctx.effect(
145
+ () => ctx.webServer.register({ kind: 'exact', path: route.path, handler: route.handler }),
146
+ 'dsh-plugin-yanyan-toolkit: route ' + route.path,
147
+ );
148
+ }
149
+ }
package/locale/en.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "Yanyan Toolkit",
4
+ "description": "One bundle with four independently toggleable plugins: Plugin Update (checks local and registry plugin versions, updates them one by one), Session Management (auto-archive / auto-delete, single and batch delete, reusing the existing dsh-session-archive package), Subagent Management (session header tab, permanent per-row delete with log root-cause removal + in-memory projection fix, clean all idle, and idle-time auto-cleanup), and Icon Generator (center-crops an uploaded image and generates an icon.svg fitted for the plugin list artwork slot). Install this one package to get all four."
5
+ }
6
+ }
package/locale/zh.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "meta": {
3
+ "title": "研研工具包",
4
+ "description": "一个包带上四个可独立开关的插件:插件更新(检查本地与线上插件版本、逐个更新)、会话管理(自动归档 / 自动删除、单条与批量删除,直接复用现成的 dsh-session-archive)、子智能体管理(会话顶部标签、逐个彻底删除:日志根除 + 内存投影根治、清理全部空闲、按空闲时间自动清理)、图标生成(上传图片居中裁切并生成适配插件列表图标位的 icon.svg)。装这一个包即可全部获得。"
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,6 +1,84 @@
1
1
  {
2
2
  "name": "dsh-plugin-yanyan-toolkit",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.4.0",
4
+ "description": "研研工具包:聚合「插件更新」「会话管理」「子智能体管理(日志根除 + 内存投影根治)」「图标生成」四个可独立开关的插件。",
5
+ "type": "module",
6
+ "main": "lib/index.js",
7
+ "icon": "icon.svg",
8
+ "exports": {
9
+ ".": "./lib/index.js",
10
+ "./client": "./lib/client.js",
11
+ "./package.json": "./package.json",
12
+ "./locale/*.json": "./locale/*.json",
13
+ "./icon": "./icon.svg"
14
+ },
15
+ "files": [
16
+ "lib/index.js",
17
+ "lib/client.js",
18
+ "cordis.patch.yml",
19
+ "icon.svg",
20
+ "locale/en.json",
21
+ "locale/zh.json",
22
+ "README.md",
23
+ "README.en.md",
24
+ "README.zh.md",
25
+ "README.i18n.yaml",
26
+ "CHANGELOG.md",
27
+ "LICENSE",
28
+ "scripts/cleanup-ghost-subagents.mjs",
29
+ "packages",
30
+ "!packages/*/test"
31
+ ],
32
+ "keywords": [
33
+ "deepseek-harness",
34
+ "dsh-plugin",
35
+ "dsh",
36
+ "plugin-manager",
37
+ "session-archive",
38
+ "subagent",
39
+ "icon-generator",
40
+ "toolkit",
41
+ "cordis",
42
+ "npm-package",
43
+ "gitee-mirror"
44
+ ],
45
+ "author": "sweet560",
46
+ "license": "MIT",
47
+ "publishConfig": {
48
+ "access": "public"
49
+ },
50
+ "repository": {
51
+ "type": "git",
52
+ "url": "git+https://github.com/sweet560/dsh-plugin-yanyan-toolkit.git"
53
+ },
54
+ "bugs": {
55
+ "url": "https://github.com/sweet560/dsh-plugin-yanyan-toolkit/issues"
56
+ },
57
+ "homepage": "https://github.com/sweet560/dsh-plugin-yanyan-toolkit/blob/main#readme",
58
+ "engines": {
59
+ "node": "^22.19.0 || >=24.0.0"
60
+ },
61
+ "scripts": {
62
+ "test": "node --test test/*.test.mjs",
63
+ "pack": "npm pack --dry-run"
64
+ },
65
+ "dependencies": {
66
+ "dsh-plugin-yanyan-update": "file:./packages/dsh-plugin-yanyan-update",
67
+ "dsh-plugin-yanyan-subagent": "file:./packages/dsh-plugin-yanyan-subagent",
68
+ "dsh-plugin-yanyan-icon": "file:./packages/dsh-plugin-yanyan-icon",
69
+ "@linxin666/dsh-session-archive": "^0.4.18"
70
+ },
71
+ "dsh": {
72
+ "engines": {
73
+ "dsh": ">=0.2.2-rc.2"
74
+ },
75
+ "bundle": {
76
+ "patch": "./cordis.patch.yml"
77
+ },
78
+ "client": {
79
+ "inject": [],
80
+ "platform": "web"
81
+ },
82
+ "manifestVersion": 1
83
+ }
84
+ }
@@ -0,0 +1,33 @@
1
+ # 研研工具包 - 图标生成
2
+
3
+ 根据上传的图片,居中裁切为正方形后生成适配 DSH 插件列表图标位(截图红框处)的 `icon.svg`。
4
+
5
+ ## 功能
6
+
7
+ - 在插件页「已安装插件」标题行右侧提供「图标生成」按钮,点开下拉面板。
8
+ - 选择 PNG / JPEG / WebP 图片后,浏览器用原生 Canvas 居中裁切(cover)并缩到 48×48 PNG。
9
+ - POST 到本包 host 半区 `/__dsh-plugin-yanyan-icon/generate`,由纯函数 `buildIconSvg` 包装成 64×64 viewBox、圆角矩形背景、居中内嵌位图的自包含静态 SVG。
10
+ - 按插件列表真实的 30px 行尺寸与 36px 卡片尺寸双预览,支持一键下载 `icon.svg`。
11
+ - 生成物无脚本、无外链、无 foreignObject、无 currentColor(`<img>` 上下文不继承页面色),解码后不超过 32 KiB,与 DSH 市场索引的内联图标上限对齐。
12
+
13
+ ## 为什么这么做
14
+
15
+ DSH 插件列表的图标由 `<img src="data:...;base64,...">` 渲染(见 `dsh-client-ui-plugin-manager` 的 `PackageArtwork`),行 30px / 卡片 36px,且明确「render as an image, not inline markup」。package.json 的 `icon` 字段或 `exports["./icon"]` 会被读成 base64 data URL。因此产物必须是纯静态、自带背景与显式颜色的图片,不能依赖页面 CSS 变量或脚本。
16
+
17
+ ## 约束
18
+
19
+ - 纯 JavaScript ESM host 半区 + `__ModuleLoader__` client 半区,不依赖 React / TS / JSX / 构建工具 / 第三方依赖。
20
+ - UI 文案中文,不使用装饰性 emoji。
21
+ - 所有同源回环路由经 `ctx.connection.requestRejection` 准入;请求体上限 2 MiB。
22
+ - 只接受位图 base64 data URL,拒绝 SVG / javascript: / 外链,颜色只接受 `#rgb` / `#rrggbb`,避免注入。
23
+ - `buildIconSvg` / `normalizeOptions` / `validateDataUrl` / `parseGenerateRequest` 为无副作用纯函数,模块顶层不访问 document / window / ctx。
24
+
25
+ ## 用法
26
+
27
+ 把下载的 `icon.svg` 放到插件包根目录,并在 package.json 声明 `"icon": "icon.svg"` 与 `exports["./icon"]`,重启 DSH 后即可在插件列表显示。
28
+
29
+ ## 测试
30
+
31
+ ```
32
+ node --test test/*.test.mjs
33
+ ```
@@ -0,0 +1,33 @@
1
+ # 研研工具包 - 图标生成
2
+
3
+ 根据上传的图片,居中裁切为正方形后生成适配 DSH 插件列表图标位(截图红框处)的 `icon.svg`。
4
+
5
+ ## 功能
6
+
7
+ - 在插件页「已安装插件」标题行右侧提供「图标生成」按钮,点开下拉面板。
8
+ - 选择 PNG / JPEG / WebP 图片后,浏览器用原生 Canvas 居中裁切(cover)并缩到 48×48 PNG。
9
+ - POST 到本包 host 半区 `/__dsh-plugin-yanyan-icon/generate`,由纯函数 `buildIconSvg` 包装成 64×64 viewBox、圆角矩形背景、居中内嵌位图的自包含静态 SVG。
10
+ - 按插件列表真实的 30px 行尺寸与 36px 卡片尺寸双预览,支持一键下载 `icon.svg`。
11
+ - 生成物无脚本、无外链、无 foreignObject、无 currentColor(`<img>` 上下文不继承页面色),解码后不超过 32 KiB,与 DSH 市场索引的内联图标上限对齐。
12
+
13
+ ## 为什么这么做
14
+
15
+ DSH 插件列表的图标由 `<img src="data:...;base64,...">` 渲染(见 `dsh-client-ui-plugin-manager` 的 `PackageArtwork`),行 30px / 卡片 36px,且明确「render as an image, not inline markup」。package.json 的 `icon` 字段或 `exports["./icon"]` 会被读成 base64 data URL。因此产物必须是纯静态、自带背景与显式颜色的图片,不能依赖页面 CSS 变量或脚本。
16
+
17
+ ## 约束
18
+
19
+ - 纯 JavaScript ESM host 半区 + `__ModuleLoader__` client 半区,不依赖 React / TS / JSX / 构建工具 / 第三方依赖。
20
+ - UI 文案中文,不使用装饰性 emoji。
21
+ - 所有同源回环路由经 `ctx.connection.requestRejection` 准入;请求体上限 2 MiB。
22
+ - 只接受位图 base64 data URL,拒绝 SVG / javascript: / 外链,颜色只接受 `#rgb` / `#rrggbb`,避免注入。
23
+ - `buildIconSvg` / `normalizeOptions` / `validateDataUrl` / `parseGenerateRequest` 为无副作用纯函数,模块顶层不访问 document / window / ctx。
24
+
25
+ ## 用法
26
+
27
+ 把下载的 `icon.svg` 放到插件包根目录,并在 package.json 声明 `"icon": "icon.svg"` 与 `exports["./icon"]`,重启 DSH 后即可在插件列表显示。
28
+
29
+ ## 测试
30
+
31
+ ```
32
+ node --test test/*.test.mjs
33
+ ```
@@ -0,0 +1,7 @@
1
+ - insert:
2
+ - id: dsh-plugin-yanyan-icon
3
+ name: 'dsh-plugin-yanyan-icon'
4
+ config:
5
+ radius: 14
6
+ padding: 4
7
+ borderWidth: 1
@@ -0,0 +1,7 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="icon generator">
2
+ <rect x="6" y="6" width="52" height="52" rx="14" fill="#eef2ff" stroke="#c7d2fe" stroke-width="2"/>
3
+ <rect x="14" y="16" width="36" height="28" rx="4" fill="#ffffff" stroke="#6366f1" stroke-width="2"/>
4
+ <circle cx="24" cy="26" r="3.5" fill="#f59e0b"/>
5
+ <path d="M16 40 L27 30 L34 36 L41 29 L48 40 Z" fill="#6366f1"/>
6
+ <path d="M32 46 L32 52 M28 49 L32 53 L36 49" fill="none" stroke="#4f46e5" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/>
7
+ </svg>