itismyskillmarket 1.3.20 → 1.3.22
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/dist/index.js +67 -5
- package/gui/app.js +151 -4
- package/gui/index.html +18 -1
- package/gui/style.css +7 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2081,6 +2081,8 @@ API_ROUTES.GET["/api/skills"] = async (_req, res, url) => {
|
|
|
2081
2081
|
const page = Math.max(1, parseInt(url.searchParams.get("page") || "1"));
|
|
2082
2082
|
const limit = Math.min(100, Math.max(1, parseInt(url.searchParams.get("limit") || "20")));
|
|
2083
2083
|
const search = url.searchParams.get("search") || "";
|
|
2084
|
+
const sort = url.searchParams.get("sort") || "name";
|
|
2085
|
+
const platform = url.searchParams.get("platform") || "";
|
|
2084
2086
|
const cacheKey = `search:${search}:limit:${limit}`;
|
|
2085
2087
|
let searchResult = getCached(cacheKey);
|
|
2086
2088
|
if (!searchResult) {
|
|
@@ -2118,16 +2120,38 @@ API_ROUTES.GET["/api/skills"] = async (_req, res, url) => {
|
|
|
2118
2120
|
platforms: meta?.platforms || [],
|
|
2119
2121
|
author: info.author?.name || pkg?.author?.name || "",
|
|
2120
2122
|
homepage: pkg?.homepage || "",
|
|
2121
|
-
repository: pkg?.repository?.url || ""
|
|
2123
|
+
repository: pkg?.repository?.url || "",
|
|
2124
|
+
updated: info.time?.[latestVersion] || info.time?.modified || ""
|
|
2122
2125
|
};
|
|
2123
2126
|
} catch {
|
|
2124
2127
|
fetchErrors++;
|
|
2125
2128
|
return null;
|
|
2126
2129
|
}
|
|
2127
2130
|
}, 3);
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
+
let skills = skillDetails.filter(Boolean);
|
|
2132
|
+
if (platform) {
|
|
2133
|
+
skills = skills.filter(
|
|
2134
|
+
(s) => Array.isArray(s.platforms) && s.platforms.includes(platform)
|
|
2135
|
+
);
|
|
2136
|
+
}
|
|
2137
|
+
skills.sort((a, b) => {
|
|
2138
|
+
const nameA = (a.displayName || a.id || "").toLowerCase();
|
|
2139
|
+
const nameB = (b.displayName || b.id || "").toLowerCase();
|
|
2140
|
+
switch (sort) {
|
|
2141
|
+
case "-name":
|
|
2142
|
+
return nameB.localeCompare(nameA);
|
|
2143
|
+
case "updated":
|
|
2144
|
+
return (a.updated || "").localeCompare(b.updated || "");
|
|
2145
|
+
case "-updated":
|
|
2146
|
+
return (b.updated || "").localeCompare(a.updated || "");
|
|
2147
|
+
case "name":
|
|
2148
|
+
default:
|
|
2149
|
+
return nameA.localeCompare(nameB);
|
|
2150
|
+
}
|
|
2151
|
+
});
|
|
2152
|
+
const filteredTotal = skills.length;
|
|
2153
|
+
const totalPages = Math.ceil(filteredTotal / limit) || 1;
|
|
2154
|
+
jsonResponse(res, 200, { skills, page, totalPages, total: filteredTotal, fetchErrors });
|
|
2131
2155
|
} catch (err) {
|
|
2132
2156
|
jsonResponse(res, 500, {
|
|
2133
2157
|
error: String(err),
|
|
@@ -2164,7 +2188,8 @@ API_ROUTES.GET["/api/platforms"] = async (_req, res, _url) => {
|
|
|
2164
2188
|
id: adapter.id,
|
|
2165
2189
|
name: adapter.name,
|
|
2166
2190
|
available: !!isAvailable,
|
|
2167
|
-
installedCount: Array.isArray(installed) ? installed.length : 0
|
|
2191
|
+
installedCount: Array.isArray(installed) ? installed.length : 0,
|
|
2192
|
+
installedSkills: Array.isArray(installed) ? installed : []
|
|
2168
2193
|
};
|
|
2169
2194
|
})
|
|
2170
2195
|
);
|
|
@@ -2173,6 +2198,34 @@ API_ROUTES.GET["/api/platforms"] = async (_req, res, _url) => {
|
|
|
2173
2198
|
jsonResponse(res, 500, { error: String(err) });
|
|
2174
2199
|
}
|
|
2175
2200
|
};
|
|
2201
|
+
API_ROUTES.GET["/api/platform-info"] = async (_req, res, url) => {
|
|
2202
|
+
try {
|
|
2203
|
+
const platformId = url.searchParams.get("id") || "";
|
|
2204
|
+
if (!platformId) {
|
|
2205
|
+
jsonResponse(res, 400, { error: 'Missing "id" query parameter' });
|
|
2206
|
+
return;
|
|
2207
|
+
}
|
|
2208
|
+
const allAdapters = getAllAdapters();
|
|
2209
|
+
const adapter = allAdapters.find((a) => a.id === platformId);
|
|
2210
|
+
if (!adapter) {
|
|
2211
|
+
jsonResponse(res, 404, { error: `Platform "${platformId}" not found` });
|
|
2212
|
+
return;
|
|
2213
|
+
}
|
|
2214
|
+
const available = await detectPlatforms();
|
|
2215
|
+
const isAvailable = available.find((a) => a.id === adapter.id);
|
|
2216
|
+
const installed = await adapter.listInstalled();
|
|
2217
|
+
const installedSkills = Array.isArray(installed) ? installed : [];
|
|
2218
|
+
jsonResponse(res, 200, {
|
|
2219
|
+
id: adapter.id,
|
|
2220
|
+
name: adapter.name,
|
|
2221
|
+
available: !!isAvailable,
|
|
2222
|
+
installedCount: installedSkills.length,
|
|
2223
|
+
installedSkills
|
|
2224
|
+
});
|
|
2225
|
+
} catch (err) {
|
|
2226
|
+
jsonResponse(res, 500, { error: String(err) });
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2176
2229
|
API_ROUTES.GET["/api/skill-info"] = async (_req, res, url) => {
|
|
2177
2230
|
try {
|
|
2178
2231
|
const skillName = url.searchParams.get("skill") || "";
|
|
@@ -2213,6 +2266,15 @@ API_ROUTES.GET["/api/skill-info"] = async (_req, res, url) => {
|
|
|
2213
2266
|
jsonResponse(res, 500, { error: String(err) });
|
|
2214
2267
|
}
|
|
2215
2268
|
};
|
|
2269
|
+
API_ROUTES.GET["/api/version"] = async (_req, res, _url) => {
|
|
2270
|
+
try {
|
|
2271
|
+
const pkgPath = join4(__dirname2, "..", "package.json");
|
|
2272
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
2273
|
+
jsonResponse(res, 200, { version: pkg.version || "1.0.0" });
|
|
2274
|
+
} catch {
|
|
2275
|
+
jsonResponse(res, 200, { version: "1.0.0" });
|
|
2276
|
+
}
|
|
2277
|
+
};
|
|
2216
2278
|
API_ROUTES.GET["/api/config"] = async (_req, res, _url) => {
|
|
2217
2279
|
jsonResponse(res, 200, {
|
|
2218
2280
|
npmScope: NPM_SCOPE,
|
package/gui/app.js
CHANGED
|
@@ -11,6 +11,8 @@ const state = {
|
|
|
11
11
|
currentPage: 1,
|
|
12
12
|
pageSize: 20,
|
|
13
13
|
searchQuery: '',
|
|
14
|
+
sortBy: 'name',
|
|
15
|
+
platformFilter: '',
|
|
14
16
|
totalPages: 1,
|
|
15
17
|
previousView: 'skills',
|
|
16
18
|
};
|
|
@@ -470,6 +472,23 @@ function applyI18nToStaticElements() {
|
|
|
470
472
|
if (backBtn) backBtn.innerHTML = `← ${t('nav.back')}`;
|
|
471
473
|
}
|
|
472
474
|
|
|
475
|
+
// -----------------------------------------------------------------------------
|
|
476
|
+
// 版本号加载
|
|
477
|
+
// -----------------------------------------------------------------------------
|
|
478
|
+
|
|
479
|
+
async function loadVersion() {
|
|
480
|
+
try {
|
|
481
|
+
const response = await fetch('/api/version');
|
|
482
|
+
const data = await response.json();
|
|
483
|
+
const el = document.getElementById('gui-version');
|
|
484
|
+
if (el && data.version) {
|
|
485
|
+
el.textContent = `v${data.version}`;
|
|
486
|
+
}
|
|
487
|
+
} catch {
|
|
488
|
+
// 静默失败,保留 HTML 中的占位符
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
473
492
|
// 重新渲染当前视图
|
|
474
493
|
function reRenderCurrentView() {
|
|
475
494
|
// 更新语言选项
|
|
@@ -508,6 +527,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
|
508
527
|
initializeNavigation();
|
|
509
528
|
initializeControls();
|
|
510
529
|
initializeCollapsibleSections();
|
|
530
|
+
loadVersion();
|
|
511
531
|
loadSkills();
|
|
512
532
|
});
|
|
513
533
|
|
|
@@ -561,8 +581,8 @@ function switchView(view) {
|
|
|
561
581
|
targetView.classList.add('active');
|
|
562
582
|
}
|
|
563
583
|
|
|
564
|
-
// 加载对应数据 (skill-detail
|
|
565
|
-
if (view !== 'skill-detail') {
|
|
584
|
+
// 加载对应数据 (skill-detail / platform-detail 视图的数据由各自函数加载)
|
|
585
|
+
if (view !== 'skill-detail' && view !== 'platform-detail') {
|
|
566
586
|
switch(view) {
|
|
567
587
|
case 'skills':
|
|
568
588
|
loadSkills();
|
|
@@ -608,6 +628,26 @@ function initializeControls() {
|
|
|
608
628
|
loadSkills();
|
|
609
629
|
});
|
|
610
630
|
|
|
631
|
+
// 排序
|
|
632
|
+
const sortSelect = document.getElementById('sort-select');
|
|
633
|
+
if (sortSelect) {
|
|
634
|
+
sortSelect.addEventListener('change', () => {
|
|
635
|
+
state.sortBy = sortSelect.value;
|
|
636
|
+
state.currentPage = 1;
|
|
637
|
+
loadSkills();
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 平台分类过滤
|
|
642
|
+
const platformFilter = document.getElementById('platform-filter');
|
|
643
|
+
if (platformFilter) {
|
|
644
|
+
platformFilter.addEventListener('change', () => {
|
|
645
|
+
state.platformFilter = platformFilter.value;
|
|
646
|
+
state.currentPage = 1;
|
|
647
|
+
loadSkills();
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
|
|
611
651
|
// 刷新按钮
|
|
612
652
|
document.getElementById('refresh-skills').addEventListener('click', () => loadSkills());
|
|
613
653
|
document.getElementById('update-all').addEventListener('click', updateAllSkills);
|
|
@@ -651,11 +691,15 @@ async function loadSkills() {
|
|
|
651
691
|
const params = new URLSearchParams({
|
|
652
692
|
page: state.currentPage.toString(),
|
|
653
693
|
limit: state.pageSize.toString(),
|
|
694
|
+
sort: state.sortBy,
|
|
654
695
|
});
|
|
655
696
|
|
|
656
697
|
if (state.searchQuery) {
|
|
657
698
|
params.append('search', state.searchQuery);
|
|
658
699
|
}
|
|
700
|
+
if (state.platformFilter) {
|
|
701
|
+
params.append('platform', state.platformFilter);
|
|
702
|
+
}
|
|
659
703
|
|
|
660
704
|
const response = await fetch(`/api/skills?${params}`);
|
|
661
705
|
const data = await response.json();
|
|
@@ -668,6 +712,7 @@ async function loadSkills() {
|
|
|
668
712
|
renderSkills(data.skills || data, container);
|
|
669
713
|
renderPagination(data.page, data.totalPages || 1);
|
|
670
714
|
renderFetchWarning(data.fetchErrors);
|
|
715
|
+
updatePlatformFilterOptions(data.skills || []);
|
|
671
716
|
} catch (err) {
|
|
672
717
|
container.innerHTML = `<div class="loading">${t('error.generic')}: ${err.message}</div>`;
|
|
673
718
|
}
|
|
@@ -745,6 +790,34 @@ function renderFetchWarning(fetchErrors) {
|
|
|
745
790
|
// 分页
|
|
746
791
|
// -----------------------------------------------------------------------------
|
|
747
792
|
|
|
793
|
+
/** 收集所有可用平台,更新分类过滤下拉框 */
|
|
794
|
+
function updatePlatformFilterOptions(skills) {
|
|
795
|
+
const select = document.getElementById('platform-filter');
|
|
796
|
+
if (!select) return;
|
|
797
|
+
|
|
798
|
+
// 收集所有唯一的平台名
|
|
799
|
+
const allPlatforms = new Set();
|
|
800
|
+
(skills || []).forEach(s => {
|
|
801
|
+
if (Array.isArray(s.platforms)) {
|
|
802
|
+
s.platforms.forEach(p => allPlatforms.add(p));
|
|
803
|
+
}
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
const currentVal = select.value;
|
|
807
|
+
const sortedPlatforms = [...allPlatforms].sort();
|
|
808
|
+
|
|
809
|
+
// 只有当平台列表有变化时才重新渲染
|
|
810
|
+
const currentOptions = Array.from(select.options).slice(1).map(o => o.value).sort().join(',');
|
|
811
|
+
const newOptions = sortedPlatforms.join(',');
|
|
812
|
+
if (currentOptions === newOptions) return;
|
|
813
|
+
|
|
814
|
+
select.innerHTML = `
|
|
815
|
+
<option value="">All Platforms</option>
|
|
816
|
+
${sortedPlatforms.map(p => `<option value="${p}"${currentVal === p ? ' selected' : ''}>${p}</option>`).join('')}
|
|
817
|
+
`;
|
|
818
|
+
select.value = currentVal && allPlatforms.has(currentVal) ? currentVal : '';
|
|
819
|
+
}
|
|
820
|
+
|
|
748
821
|
function renderPagination(currentPage, totalPages) {
|
|
749
822
|
state.currentPage = currentPage;
|
|
750
823
|
state.totalPages = totalPages;
|
|
@@ -801,7 +874,7 @@ function renderPlatforms(platforms, container) {
|
|
|
801
874
|
}
|
|
802
875
|
|
|
803
876
|
container.innerHTML = platforms.map(platform => `
|
|
804
|
-
<div class="platform-card">
|
|
877
|
+
<div class="platform-card" onclick="showPlatformDetail('${platform.id}')" style="cursor: pointer;">
|
|
805
878
|
<div>
|
|
806
879
|
<h3>${platform.name}</h3>
|
|
807
880
|
<div class="status ${platform.available ? 'status-available' : 'status-unavailable'}">
|
|
@@ -809,12 +882,86 @@ function renderPlatforms(platforms, container) {
|
|
|
809
882
|
</div>
|
|
810
883
|
</div>
|
|
811
884
|
<div>
|
|
812
|
-
${platform.installedCount ? `<span>${t('status.skillsInstalled', { count: platform.installedCount })}</span>` : ''}
|
|
885
|
+
${platform.installedCount ? `<span>${t('status.skillsInstalled', { count: platform.installedCount })}</span>` : '<span style="color: var(--text-muted); font-size: 0.85rem;">0 installed</span>'}
|
|
813
886
|
</div>
|
|
814
887
|
</div>
|
|
815
888
|
`).join('');
|
|
816
889
|
}
|
|
817
890
|
|
|
891
|
+
// -----------------------------------------------------------------------------
|
|
892
|
+
// Platform 详情视图
|
|
893
|
+
// -----------------------------------------------------------------------------
|
|
894
|
+
|
|
895
|
+
async function showPlatformDetail(platformId) {
|
|
896
|
+
const content = document.getElementById('platform-detail-content');
|
|
897
|
+
content.innerHTML = `<div class="loading">${t('loading.generic')}</div>`;
|
|
898
|
+
|
|
899
|
+
state.previousView = state.currentView;
|
|
900
|
+
state.currentView = 'platform-detail';
|
|
901
|
+
|
|
902
|
+
document.querySelectorAll('.view').forEach(v => v.classList.remove('active'));
|
|
903
|
+
const targetView = document.getElementById('view-platform-detail');
|
|
904
|
+
if (targetView) targetView.classList.add('active');
|
|
905
|
+
|
|
906
|
+
try {
|
|
907
|
+
const response = await fetch(`/api/platform-info?id=${encodeURIComponent(platformId)}`);
|
|
908
|
+
const platform = await response.json();
|
|
909
|
+
|
|
910
|
+
if (platform.error) {
|
|
911
|
+
content.innerHTML = `<div class="loading">${t('error.generic')}: ${platform.error}</div>`;
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
const skills = platform.installedSkills || [];
|
|
916
|
+
|
|
917
|
+
content.innerHTML = `
|
|
918
|
+
<div class="platform-detail">
|
|
919
|
+
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px;">
|
|
920
|
+
<div>
|
|
921
|
+
<h2 style="color: var(--accent); font-size: 1.5rem; margin: 0;">${platform.name}</h2>
|
|
922
|
+
<span class="status ${platform.available ? 'status-available' : 'status-unavailable'}">
|
|
923
|
+
${platform.available ? t('status.available') : t('status.unavailable')}
|
|
924
|
+
</span>
|
|
925
|
+
<span style="color: var(--text-muted); font-size: 0.85rem; margin-left: 12px;">
|
|
926
|
+
${t('status.skillsInstalled', { count: platform.installedCount })}
|
|
927
|
+
</span>
|
|
928
|
+
</div>
|
|
929
|
+
</div>
|
|
930
|
+
<div class="admin-skill-row" style="background: var(--bg-card); padding: 10px 16px; margin-bottom: 12px;">
|
|
931
|
+
<span style="color: var(--text-muted); font-size: 0.85rem;">Platform ID</span>
|
|
932
|
+
<span style="color: var(--text-secondary); font-size: 0.9rem; font-family: monospace;">${platform.id}</span>
|
|
933
|
+
</div>
|
|
934
|
+
<h3 style="color: var(--text-secondary); margin-bottom: 10px; font-size: 1rem;">
|
|
935
|
+
Installed Skills (${skills.length})
|
|
936
|
+
</h3>
|
|
937
|
+
${skills.length === 0 ? '<div class="loading" style="padding: 20px;">No skills installed on this platform.</div>' : `
|
|
938
|
+
<div style="display: flex; flex-direction: column; gap: 6px;">
|
|
939
|
+
${skills.map((skill, i) => `
|
|
940
|
+
<div class="admin-skill-row" style="cursor: pointer;" onclick="showSkillDetail('${skill.id}')">
|
|
941
|
+
<div class="admin-skill-info">
|
|
942
|
+
<h4>${skill.displayName || skill.id}</h4>
|
|
943
|
+
<div class="admin-skill-meta">${skill.id}@${skill.version || 'latest'}</div>
|
|
944
|
+
</div>
|
|
945
|
+
<button class="btn btn-secondary btn-sm" onclick="event.stopPropagation(); showSkillDetail('${skill.id}')">${t('btn.info')}</button>
|
|
946
|
+
</div>
|
|
947
|
+
`).join('')}
|
|
948
|
+
</div>
|
|
949
|
+
`}
|
|
950
|
+
</div>
|
|
951
|
+
`;
|
|
952
|
+
} catch (err) {
|
|
953
|
+
content.innerHTML = `<div class="loading">${t('error.generic')}: ${err.message}</div>`;
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function goBackFromPlatformDetail() {
|
|
958
|
+
const target = state.previousView || 'platforms';
|
|
959
|
+
document.querySelectorAll('.nav-btn').forEach(b => {
|
|
960
|
+
b.classList.toggle('active', b.dataset.view === target);
|
|
961
|
+
});
|
|
962
|
+
switchView(target);
|
|
963
|
+
}
|
|
964
|
+
|
|
818
965
|
// -----------------------------------------------------------------------------
|
|
819
966
|
// Help 视图
|
|
820
967
|
// -----------------------------------------------------------------------------
|
package/gui/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<header class="topbar">
|
|
13
13
|
<div class="topbar-left">
|
|
14
14
|
<span class="topbar-logo">📦 SkillMarket</span>
|
|
15
|
-
<span class="topbar-version" id="gui-version"
|
|
15
|
+
<span class="topbar-version" id="gui-version">...</span>
|
|
16
16
|
</div>
|
|
17
17
|
<nav class="topbar-nav">
|
|
18
18
|
<button class="nav-btn active" data-view="skills">📋 Skills</button>
|
|
@@ -37,6 +37,15 @@
|
|
|
37
37
|
<h2>Available Skills</h2>
|
|
38
38
|
<div class="controls">
|
|
39
39
|
<input type="text" id="search-input" placeholder="🔍 Search skills...">
|
|
40
|
+
<select id="sort-select">
|
|
41
|
+
<option value="name">Name A-Z</option>
|
|
42
|
+
<option value="-name">Name Z-A</option>
|
|
43
|
+
<option value="-updated">Recently Updated</option>
|
|
44
|
+
<option value="updated">Least Recently Updated</option>
|
|
45
|
+
</select>
|
|
46
|
+
<select id="platform-filter">
|
|
47
|
+
<option value="">All Platforms</option>
|
|
48
|
+
</select>
|
|
40
49
|
<select id="page-size">
|
|
41
50
|
<option value="10">10 per page</option>
|
|
42
51
|
<option value="20" selected>20 per page</option>
|
|
@@ -66,6 +75,14 @@
|
|
|
66
75
|
<div id="platforms-list" class="platforms-list"></div>
|
|
67
76
|
</div>
|
|
68
77
|
|
|
78
|
+
<!-- Platform 详情视图 -->
|
|
79
|
+
<div id="view-platform-detail" class="view">
|
|
80
|
+
<div class="view-header">
|
|
81
|
+
<button class="btn btn-secondary" onclick="goBackFromPlatformDetail()">← Back to Platforms</button>
|
|
82
|
+
</div>
|
|
83
|
+
<div id="platform-detail-content"></div>
|
|
84
|
+
</div>
|
|
85
|
+
|
|
69
86
|
<!-- Help 视图 -->
|
|
70
87
|
<div id="view-help" class="view">
|
|
71
88
|
<div class="view-header">
|
package/gui/style.css
CHANGED
|
@@ -40,6 +40,13 @@ body {
|
|
|
40
40
|
overflow: hidden;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
#app {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
flex: 1;
|
|
47
|
+
min-height: 0;
|
|
48
|
+
}
|
|
49
|
+
|
|
43
50
|
/* -----------------------------------------------------------------------------
|
|
44
51
|
顶部导航栏
|
|
45
52
|
----------------------------------------------------------------------------- */
|