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/LICENSE +21 -0
- package/README.md +184 -0
- package/cordis.patch.yml +3 -0
- package/lib/clawhub.js +253 -0
- package/lib/client.js +296 -0
- package/lib/index.js +951 -0
- package/lib/skillhub.js +304 -0
- package/package.json +57 -0
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
|
+
} });
|