dsh-skill-store 0.1.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/lib/client.js ADDED
@@ -0,0 +1,296 @@
1
+ window.__ModuleLoader__.load({ id: "dsh-skill-store", factory: (require) => {
2
+ var module = { exports: {} };
3
+ var exports = module.exports;
4
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
5
+ var react = require("react");
6
+ var react_dom_client = require("react-dom/client");
7
+
8
+ // ── Simple React skill store panel ─────────────────────────────────
9
+
10
+ function useSkillsApi() {
11
+ const [skills, setSkills] = react.useState([]);
12
+ const [total, setTotal] = react.useState(0);
13
+ const [categories, setCategories] = react.useState({});
14
+ const [installed, setInstalled] = react.useState(new Set());
15
+ const [loading, setLoading] = react.useState(false);
16
+ const [error, setError] = react.useState(null);
17
+
18
+ const apiFetch = react.useCallback(async (path, opts) => {
19
+ const res = await fetch(`/api/dsh-skill-store${path}`, {
20
+ headers: { "Content-Type": "application/json" },
21
+ ...opts,
22
+ });
23
+ if (!res.ok) {
24
+ const e = await res.json().catch(() => ({ error: `HTTP ${res.status}` }));
25
+ throw new Error(e.error ?? `HTTP ${res.status}`);
26
+ }
27
+ return res.json();
28
+ }, []);
29
+
30
+ const loadSkills = react.useCallback(async (search = "", category = "", source = "github", sort = "") => {
31
+ setLoading(true);
32
+ setError(null);
33
+ try {
34
+ const qs = new URLSearchParams({ limit: "100", source });
35
+ if (search) qs.set("search", search);
36
+ if (category) qs.set("category", category);
37
+ if (sort) qs.set("sort", sort);
38
+ const data = await apiFetch(`/list?${qs.toString()}`);
39
+ setSkills(data.entries);
40
+ setTotal(data.total);
41
+ } catch (err) {
42
+ setError(err.message);
43
+ } finally {
44
+ setLoading(false);
45
+ }
46
+ }, [apiFetch]);
47
+
48
+ const loadCategories = react.useCallback(async (source = "github") => {
49
+ try {
50
+ const data = await apiFetch(`/categories?source=${encodeURIComponent(source)}`);
51
+ setCategories(data);
52
+ } catch {}
53
+ }, [apiFetch]);
54
+
55
+ const loadInstalled = react.useCallback(async () => {
56
+ try {
57
+ const data = await apiFetch("/installed");
58
+ setInstalled(new Set(data.skills.map(s => s.name)));
59
+ } catch {}
60
+ }, [apiFetch]);
61
+
62
+ const install = react.useCallback(async (name, source = "github") => {
63
+ await apiFetch("/install", { method: "POST", body: JSON.stringify({ name, source }) });
64
+ await loadInstalled();
65
+ }, [apiFetch, loadInstalled]);
66
+
67
+ const uninstall = react.useCallback(async (name) => {
68
+ await apiFetch("/uninstall", { method: "POST", body: JSON.stringify({ name }) });
69
+ await loadInstalled();
70
+ }, [apiFetch, loadInstalled]);
71
+
72
+ const refresh = react.useCallback(async () => {
73
+ await apiFetch("/refresh", { method: "POST" });
74
+ await loadSkills();
75
+ await loadCategories();
76
+ }, [apiFetch, loadSkills, loadCategories]);
77
+
78
+ return { skills, total, categories, installed, loading, error, loadSkills, loadCategories, loadInstalled, install, uninstall, refresh };
79
+ }
80
+
81
+ function SkillStoreUI() {
82
+ const { skills, total, categories, installed, loading, error, loadSkills, loadCategories, loadInstalled, install, uninstall, refresh } = useSkillsApi();
83
+ const [search, setSearch] = react.useState("");
84
+ const [category, setCategory] = react.useState("");
85
+ const [selected, setSelected] = react.useState(null);
86
+ const [content, setContent] = react.useState(null);
87
+ const [busy, setBusy] = react.useState(null);
88
+ const [source, setSource] = react.useState("github");
89
+ const [sort, setSort] = react.useState(source === "clawhub" ? "stars" : "");
90
+
91
+ react.useEffect(() => { loadCategories(source); loadInstalled(); }, [source]);
92
+ react.useEffect(() => { loadSkills(search, category, source, sort); }, [search, category, source, sort]);
93
+
94
+ const viewDetail = async (skill) => {
95
+ setSelected(skill);
96
+ setContent(null);
97
+ try {
98
+ const res = await fetch(`/api/dsh-skill-store/detail/${encodeURIComponent(skill.name)}?source=${encodeURIComponent(source)}`);
99
+ const data = await res.json();
100
+ setContent(data.content ?? data.fetchError ?? "(no content)");
101
+ } catch { setContent("(failed to load)"); }
102
+ };
103
+
104
+ const doInstall = async (sk) => { setBusy(sk.name); try { await install(sk.name, source); } catch (e) { alert(e.message); } finally { setBusy(null); } };
105
+ const doUninstall = async (s) => { if (!confirm(`Remove "${s.name}"?`)) return; setBusy(s.name); try { await uninstall(s.name); } catch (e) { alert(e.message); } finally { setBusy(null); } };
106
+
107
+ const catNames = Object.keys(categories).sort();
108
+ const catCounts = categories;
109
+
110
+ return react.createElement("div", { style: s.container },
111
+ // Header
112
+ react.createElement("div", { style: s.header },
113
+ react.createElement("h2", { style: s.title }, "🧩 技能商店"),
114
+ react.createElement("span", { style: s.count }, total > 0 ? `${total} 个技能` : ""),
115
+ react.createElement("button", { onClick: refresh, style: s.refreshBtn }, "🔄 刷新索引")
116
+ ),
117
+ // Source switch
118
+ react.createElement("div", { style: s.sourceBar },
119
+ react.createElement("button", {
120
+ onClick: () => { setSource("github"); setCategory(""); setSort(""); },
121
+ style: { ...s.sourceBtn, ...(source === "github" ? s.sourceBtnOn : {}) }
122
+ }, "GitHub 镜像"),
123
+ react.createElement("button", {
124
+ onClick: () => { setSource("skillhub"); setCategory(""); setSort("stars"); },
125
+ style: { ...s.sourceBtn, ...(source === "skillhub" ? s.sourceBtnOn : {}) }
126
+ }, "SkillHub 中文社区"),
127
+ react.createElement("button", {
128
+ onClick: () => { setSource("clawhub"); setCategory(""); setSort("stars"); },
129
+ style: { ...s.sourceBtn, ...(source === "clawhub" ? s.sourceBtnOn : {}) }
130
+ }, "ClawHub"),
131
+ (source === "clawhub" || source === "skillhub") && react.createElement("select", {
132
+ value: sort, onChange: (e) => setSort(e.target.value), style: { ...s.select, marginLeft: "auto" }
133
+ },
134
+ react.createElement("option", { value: "stars" }, "按星标排序"),
135
+ react.createElement("option", { value: "downloads" }, "按下载量排序"),
136
+ react.createElement("option", { value: "installs" }, "按安装量排序"),
137
+ react.createElement("option", { value: "recent" }, "按更新时间排序")
138
+ )
139
+ ),
140
+ // Search & filter
141
+ react.createElement("div", { style: s.bar },
142
+ react.createElement("input", { type: "text", placeholder: "搜索技能名称或描述...", value: search, onChange: (e) => setSearch(e.target.value), style: s.input }),
143
+ react.createElement("select", { value: category, onChange: (e) => setCategory(e.target.value), style: s.select },
144
+ react.createElement("option", { value: "" }, "全部分类"),
145
+ ...catNames.map(c => react.createElement("option", { key: c, value: c }, `${c} (${catCounts[c]?.length ?? 0})`))
146
+ )
147
+ ),
148
+ // Status
149
+ error && react.createElement("div", { style: s.error }, "⚠ ", error),
150
+ loading && react.createElement("div", { style: s.loading },
151
+ source === "skillhub"
152
+ ? "⏳ 正在从 SkillHub 索引技能,首次约需 30-60 秒..."
153
+ : source === "clawhub"
154
+ ? "⏳ 正在从 ClawHub 索引技能,首次约需 2 分钟..."
155
+ : "⏳ 正在从 GitHub 索引技能数据,首次加载可能需要 1-2 分钟..."),
156
+ // Card grid
157
+ !loading && !error && react.createElement("div", { style: s.grid },
158
+ skills.length === 0 && react.createElement("div", { style: s.empty }, "没有找到技能,试试不同的关键词或刷新索引"),
159
+ skills.map(skill => {
160
+ const isInstalled = installed.has(skill.name);
161
+ return react.createElement("div", { key: skill.name, style: { ...s.card, ...(isInstalled ? s.cardOn : {}) }, onClick: () => viewDetail(skill) },
162
+ react.createElement("div", { style: s.cardTop },
163
+ react.createElement("strong", { style: s.cardTitle }, skill.displayName || skill.name),
164
+ skill.author && react.createElement("span", { style: s.author }, skill.author)
165
+ ),
166
+ // Popularity row — only ClawHub carries these numbers
167
+ (skill.stars != null || skill.downloads != null) && react.createElement("div", { style: s.stats },
168
+ skill.stars != null && react.createElement("span", { style: s.stat, title: "星标" }, `★ ${fmtNum(skill.stars)}`),
169
+ skill.downloads != null && react.createElement("span", { style: s.stat, title: "下载量" }, `↓ ${fmtNum(skill.downloads)}`),
170
+ skill.installs != null && react.createElement("span", { style: s.stat, title: "安装量" }, `⬇ ${fmtNum(skill.installs)}`),
171
+ skill.upstreamStars != null && react.createElement("span", { style: s.stat, title: "上游仓库星标" }, `repo ★ ${fmtNum(skill.upstreamStars)}`)
172
+ ),
173
+ react.createElement("p", { style: s.desc }, skill.description),
174
+ ((skill.topics?.length > 0) || (skill.tags?.length > 0)) && react.createElement("div", { style: s.tags },
175
+ ...(skill.topics || skill.tags || []).slice(0, 5).map(t => react.createElement("span", { key: t, style: s.tag }, t))
176
+ ),
177
+ (skill.collectionLabel || skill.categoryLabel) && react.createElement("div", { style: s.collLabel },
178
+ skill.collectionLabel || skill.categoryLabel
179
+ ),
180
+ react.createElement("button", {
181
+ onClick: (e) => { e.stopPropagation(); isInstalled ? doUninstall(skill) : doInstall(skill); },
182
+ disabled: busy === skill.name,
183
+ style: { ...s.btn, ...(isInstalled ? s.btnOn : {}) }
184
+ }, busy === skill.name ? "处理中..." : isInstalled ? "✅ 已安装 (点击卸载)" : "📦 安装")
185
+ );
186
+ })
187
+ ),
188
+ // Detail modal
189
+ selected && react.createElement("div", { style: s.modalBg, onClick: () => setSelected(null) },
190
+ react.createElement("div", { style: s.modal, onClick: (e) => e.stopPropagation() },
191
+ react.createElement("div", { style: s.modalTop },
192
+ react.createElement("h3", null, selected.name),
193
+ react.createElement("button", { onClick: () => setSelected(null), style: s.closeBtn }, "×")
194
+ ),
195
+ react.createElement("p", { style: { color: "#555", marginBottom: "12px" } }, selected.description),
196
+ (selected.stars != null || selected.downloads != null) && react.createElement("div", { style: s.modalStats },
197
+ selected.stars != null && react.createElement("span", { style: s.stat }, `★ ${fmtNum(selected.stars)} 星标`),
198
+ selected.downloads != null && react.createElement("span", { style: s.stat }, `↓ ${fmtNum(selected.downloads)} 下载`),
199
+ selected.installs != null && react.createElement("span", { style: s.stat }, `⬇ ${fmtNum(selected.installs)} 安装`),
200
+ selected.upstreamStars != null && react.createElement("span", { style: s.stat }, `repo ★ ${fmtNum(selected.upstreamStars)}`),
201
+ selected.version && react.createElement("span", { style: s.stat }, `v${selected.version}`)
202
+ ),
203
+ ((selected.topics?.length > 0) || (selected.tags?.length > 0)) && react.createElement("div", { style: { ...s.tags, marginBottom: "10px" } },
204
+ ...(selected.topics || selected.tags || []).map(t => react.createElement("span", { key: t, style: s.tag }, t))
205
+ ),
206
+ content ? react.createElement("pre", { style: s.preview }, content.length > 8000 ? content.slice(0, 8000) + "\n\n... (content truncated)" : content)
207
+ : react.createElement("p", { style: { color: "#999" } }, "加载中..."),
208
+ react.createElement("div", { style: s.modalBtns },
209
+ react.createElement("a", { href: selected.browseUrl, target: "_blank", rel: "noopener", style: s.linkBtn }, "📂 源文件"),
210
+ react.createElement("button", {
211
+ onClick: () => installed.has(selected.name) ? doUninstall(selected) : doInstall(selected),
212
+ style: { ...s.btn, ...(installed.has(selected.name) ? s.btnOn : {}) }
213
+ }, installed.has(selected.name) ? "卸载" : "安装")
214
+ )
215
+ )
216
+ )
217
+ );
218
+ }
219
+
220
+ /** Compact number formatting: 476827 → "476.8k". */
221
+ function fmtNum(n) {
222
+ const v = Number(n) || 0;
223
+ if (v >= 1e6) return (v / 1e6).toFixed(1) + "m";
224
+ if (v >= 1e3) return (v / 1e3).toFixed(1) + "k";
225
+ return String(v);
226
+ }
227
+
228
+ const s = {
229
+ container: { padding: "16px 20px", maxWidth: "1200px", margin: "0 auto", fontFamily: "system-ui, sans-serif" },
230
+ header: { display: "flex", alignItems: "center", gap: "12px", marginBottom: "16px" },
231
+ title: { margin: 0, fontSize: "20px", fontWeight: 600 },
232
+ count: { color: "#888", fontSize: "14px" },
233
+ refreshBtn: { marginLeft: "auto", padding: "6px 14px", border: "1px solid #ddd", borderRadius: "6px", background: "#f5f5f5", cursor: "pointer", fontSize: "13px" },
234
+ sourceBar: { display: "flex", gap: "8px", alignItems: "center", marginBottom: "12px" },
235
+ sourceBtn: { padding: "6px 16px", border: "1px solid #ddd", borderRadius: "18px", background: "#fff", cursor: "pointer", fontSize: "13px", color: "#555" },
236
+ sourceBtnOn: { background: "#2563eb", color: "#fff", borderColor: "#2563eb" },
237
+ stats: { display: "flex", gap: "10px", flexWrap: "wrap", marginTop: "4px" },
238
+ stat: { fontSize: "11px", color: "#c2410c", background: "#fff7ed", padding: "2px 7px", borderRadius: "10px" },
239
+ collLabel: { marginTop: "8px", fontSize: "11px", color: "#6b7280", background: "#f3f4f6", display: "inline-block", padding: "2px 8px", borderRadius: "10px" },
240
+ bar: { display: "flex", gap: "10px", marginBottom: "16px" },
241
+ input: { flex: 1, padding: "8px 12px", border: "1px solid #ddd", borderRadius: "6px", fontSize: "14px" },
242
+ select: { padding: "8px 12px", border: "1px solid #ddd", borderRadius: "6px", fontSize: "14px", minWidth: "160px" },
243
+ loading: { textAlign: "center", padding: "40px", color: "#666", lineHeight: "1.6" },
244
+ error: { padding: "16px", background: "#fff0f0", border: "1px solid #fcc", borderRadius: "8px", color: "#c00", marginBottom: "16px" },
245
+ empty: { textAlign: "center", padding: "40px", color: "#999" },
246
+ grid: { display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(280px, 1fr))", gap: "12px" },
247
+ card: { border: "1px solid #e0e0e0", borderRadius: "8px", padding: "14px", cursor: "pointer", background: "#fff", transition: "box-shadow 0.15s" },
248
+ cardOn: { borderColor: "#4caf50", background: "#f6fdf6" },
249
+ cardTop: { display: "flex", justifyContent: "space-between", alignItems: "center", marginBottom: "6px" },
250
+ cardTitle: { fontSize: "15px", color: "#333" },
251
+ author: { fontSize: "12px", color: "#999" },
252
+ desc: { fontSize: "13px", color: "#666", margin: "6px 0", lineHeight: "1.4", display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden" },
253
+ tags: { display: "flex", gap: "4px", flexWrap: "wrap", marginTop: "8px" },
254
+ tag: { padding: "2px 8px", background: "#e8e8ff", color: "#445", borderRadius: "10px", fontSize: "11px" },
255
+ btn: { marginTop: "10px", padding: "6px 16px", border: "none", borderRadius: "6px", background: "#2563eb", color: "#fff", cursor: "pointer", fontSize: "13px", width: "100%" },
256
+ btnOn: { background: "#4caf50" },
257
+ modalBg: { position: "fixed", inset: 0, background: "rgba(0,0,0,0.4)", display: "flex", alignItems: "center", justifyContent: "center", zIndex: 1000 },
258
+ modal: { background: "#fff", borderRadius: "12px", padding: "24px", maxWidth: "640px", width: "90%", maxHeight: "80vh", overflowY: "auto" },
259
+ modalTop: { display: "flex", justifyContent: "space-between", alignItems: "center" },
260
+ modalStats: { display: "flex", gap: "8px", flexWrap: "wrap", marginBottom: "10px" },
261
+ closeBtn: { padding: "4px 10px", border: "none", background: "none", fontSize: "22px", cursor: "pointer" },
262
+ preview: { background: "#f5f5f5", padding: "12px", borderRadius: "6px", fontSize: "12px", maxHeight: "300px", overflowY: "auto", whiteSpace: "pre-wrap", wordBreak: "break-word" },
263
+ modalBtns: { display: "flex", gap: "10px", marginTop: "16px", justifyContent: "flex-end" },
264
+ linkBtn: { padding: "6px 14px", border: "1px solid #ddd", borderRadius: "6px", background: "#f5f5f5", cursor: "pointer", textDecoration: "none", color: "#333", fontSize: "13px" },
265
+ };
266
+
267
+ // ── DSH Plugin Registration ────────────────────────────────────────
268
+
269
+ var NS = "dsh-skill-store";
270
+
271
+ function apply(ctx) {
272
+ ctx.effect(() => {
273
+ // Register as a settings section using slots
274
+ if (ctx.slots?.inject) {
275
+ ctx.slots.inject("settings.section", () => ctx.slots.register({
276
+ name: "settings.section",
277
+ id: "skill-store",
278
+ order: 60,
279
+ label: () => "🧩 技能商店",
280
+ locale: NS,
281
+ inject: () => ({
282
+ hooks: {},
283
+ viewState: {},
284
+ }),
285
+ }, SkillStoreUI));
286
+ }
287
+ }, "dsh-skill-store: slot registration");
288
+ }
289
+
290
+ exports.name = "dsh-skill-store";
291
+ exports.inject = ["slots"];
292
+ exports.apply = apply;
293
+ exports.NS = NS;
294
+
295
+ return module.exports;
296
+ } });