itismyskillmarket 1.3.37 → 1.3.39

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.
Binary file
Binary file
@@ -0,0 +1,214 @@
1
+ /**
2
+ * =============================================================================
3
+ * SkillMarket - Electron Main Process
4
+ * =============================================================================
5
+ *
6
+ * 原生桌面应用窗口:
7
+ * - 内嵌 HTTP 服务器(非浏览器模式)
8
+ * - 系统托盘(关闭到托盘)
9
+ * - 原生菜单栏
10
+ * =============================================================================
11
+ */
12
+
13
+ import { app, BrowserWindow, Tray, Menu, dialog, shell, nativeImage } from 'electron';
14
+ import path from 'path';
15
+ import { fileURLToPath, pathToFileURL } from 'url';
16
+
17
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
18
+ const ROOT = path.resolve(__dirname, '..');
19
+
20
+ let mainWindow = null;
21
+ let tray = null;
22
+ let server = null;
23
+ const PORT = 18770;
24
+
25
+ // 单实例锁
26
+ const gotLock = app.requestSingleInstanceLock();
27
+ if (!gotLock) {
28
+ app.quit();
29
+ } else {
30
+ app.on('second-instance', () => {
31
+ if (mainWindow) {
32
+ if (mainWindow.isMinimized()) mainWindow.restore();
33
+ mainWindow.show();
34
+ mainWindow.focus();
35
+ }
36
+ });
37
+ }
38
+
39
+ // ---------------------------------------------------------------------------
40
+ // 启动内嵌 HTTP 服务器
41
+ // ---------------------------------------------------------------------------
42
+
43
+ async function startServer(port) {
44
+ const entryPath = pathToFileURL(
45
+ path.join(ROOT, 'dist', 'electron-entry.js')
46
+ ).href;
47
+ const { startGuiServer } = await import(entryPath);
48
+
49
+ const srv = startGuiServer(port);
50
+ server = srv;
51
+
52
+ return new Promise((resolve, reject) => {
53
+ srv.on('listening', () => resolve(port));
54
+ srv.on('error', (err) => {
55
+ if (err.code === 'EADDRINUSE') {
56
+ srv.close();
57
+ resolve(startServer(port + 1));
58
+ } else {
59
+ reject(err);
60
+ }
61
+ });
62
+ });
63
+ }
64
+
65
+ // ---------------------------------------------------------------------------
66
+ // 创建主窗口
67
+ // ---------------------------------------------------------------------------
68
+
69
+ function createWindow(port) {
70
+ mainWindow = new BrowserWindow({
71
+ width: 1200,
72
+ height: 800,
73
+ minWidth: 900,
74
+ minHeight: 600,
75
+ title: 'SkillMarket',
76
+ show: false,
77
+ webPreferences: {
78
+ preload: path.join(__dirname, 'preload.mjs'),
79
+ contextIsolation: true,
80
+ nodeIntegration: false,
81
+ },
82
+ });
83
+
84
+ mainWindow.loadURL(`http://127.0.0.1:${port}`);
85
+
86
+ mainWindow.once('ready-to-show', () => mainWindow.show());
87
+
88
+ // 关闭 → 隐藏到托盘
89
+ mainWindow.on('close', (event) => {
90
+ if (!app.isQuitting) {
91
+ event.preventDefault();
92
+ mainWindow.hide();
93
+ }
94
+ });
95
+ mainWindow.on('closed', () => { mainWindow = null; });
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // 系统托盘
100
+ // ---------------------------------------------------------------------------
101
+
102
+ function createTray(port) {
103
+ const iconPath = path.join(ROOT, 'electron', 'tray-icon.png');
104
+ let icon;
105
+ try {
106
+ icon = nativeImage.createFromPath(iconPath).resize({ width: 16, height: 16 });
107
+ } catch {
108
+ icon = nativeImage.createEmpty();
109
+ }
110
+
111
+ tray = new Tray(icon);
112
+ tray.setToolTip('SkillMarket');
113
+
114
+ tray.setContextMenu(Menu.buildFromTemplate([
115
+ {
116
+ label: '打开 SkillMarket',
117
+ click: () => {
118
+ if (mainWindow) { mainWindow.show(); mainWindow.focus(); }
119
+ else { createWindow(port); }
120
+ },
121
+ },
122
+ { type: 'separator' },
123
+ {
124
+ label: '关于',
125
+ click: () => dialog.showMessageBox({
126
+ type: 'info',
127
+ title: '关于 SkillMarket',
128
+ message: 'SkillMarket v1.3.37',
129
+ detail: 'Cross-platform skill manager for AI coding tools',
130
+ }),
131
+ },
132
+ { type: 'separator' },
133
+ {
134
+ label: '退出',
135
+ click: () => { app.isQuitting = true; app.quit(); },
136
+ },
137
+ ]));
138
+
139
+ tray.on('double-click', () => {
140
+ if (mainWindow) { mainWindow.show(); mainWindow.focus(); }
141
+ });
142
+ }
143
+
144
+ // ---------------------------------------------------------------------------
145
+ // 应用菜单
146
+ // ---------------------------------------------------------------------------
147
+
148
+ function createMenu() {
149
+ const isMac = process.platform === 'darwin';
150
+ const template = [
151
+ {
152
+ label: 'SkillMarket',
153
+ submenu: [
154
+ ...(isMac ? [{ role: 'about' }, { type: 'separator' }] : []),
155
+ { role: 'quit' },
156
+ ],
157
+ },
158
+ {
159
+ label: '编辑',
160
+ submenu: [
161
+ { role: 'undo' }, { role: 'redo' }, { type: 'separator' },
162
+ { role: 'cut' }, { role: 'copy' }, { role: 'paste' }, { role: 'selectAll' },
163
+ ],
164
+ },
165
+ {
166
+ label: '视图',
167
+ submenu: [
168
+ { role: 'reload' }, { role: 'forceReload' },
169
+ { type: 'separator' },
170
+ { role: 'resetZoom' }, { role: 'zoomIn' }, { role: 'zoomOut' },
171
+ { type: 'separator' },
172
+ { role: 'togglefullscreen' },
173
+ ],
174
+ },
175
+ {
176
+ label: '帮助',
177
+ submenu: [
178
+ { label: 'GitHub', click: () => shell.openExternal('https://github.com/wxc2004/market') },
179
+ { label: '报告问题', click: () => shell.openExternal('https://github.com/wxc2004/market/issues') },
180
+ ],
181
+ },
182
+ ];
183
+ Menu.setApplicationMenu(Menu.buildFromTemplate(template));
184
+ }
185
+
186
+ // ---------------------------------------------------------------------------
187
+ // 启动
188
+ // ---------------------------------------------------------------------------
189
+
190
+ app.whenReady().then(async () => {
191
+ try {
192
+ const actualPort = await startServer(PORT);
193
+ createWindow(actualPort);
194
+ try { createTray(actualPort); } catch {}
195
+ createMenu();
196
+
197
+ app.on('activate', () => {
198
+ if (mainWindow === null) createWindow(actualPort);
199
+ else mainWindow.show();
200
+ });
201
+ } catch (err) {
202
+ dialog.showErrorBox('启动失败', err.message);
203
+ app.quit();
204
+ }
205
+ });
206
+
207
+ app.on('window-all-closed', () => {
208
+ if (process.platform !== 'darwin') app.quit();
209
+ });
210
+
211
+ app.on('before-quit', () => {
212
+ app.isQuitting = true;
213
+ if (server) try { server.close(); } catch {}
214
+ });
@@ -0,0 +1,18 @@
1
+ /**
2
+ * =============================================================================
3
+ * SkillMarket - Electron Preload Script
4
+ * =============================================================================
5
+ *
6
+ * 桥接 Node.js 和渲染进程。通过 contextBridge 安全地暴露 API。
7
+ * =============================================================================
8
+ */
9
+
10
+ import { contextBridge, ipcRenderer } from 'electron';
11
+
12
+ contextBridge.exposeInMainWorld('electronAPI', {
13
+ // 平台信息
14
+ platform: process.platform,
15
+
16
+ // 获取应用版本
17
+ getVersion: () => ipcRenderer.invoke('get-version'),
18
+ });
Binary file
@@ -0,0 +1,3 @@
1
+ @echo off
2
+ cd /d "%~dp0.."
3
+ start "" "node_modules\electron\dist\electron.exe" "electron/main.mjs"
package/gui/app.js CHANGED
@@ -1933,8 +1933,12 @@ const uploadState = {
1933
1933
  skillName: '',
1934
1934
  file: null,
1935
1935
  data: null, // Parsed result from backend
1936
+ tempDir: '', // Temporary directory path from server
1936
1937
  };
1937
1938
 
1939
+ /** 上传文件大小限制:50 MB */
1940
+ const MAX_UPLOAD_SIZE = 50 * 1024 * 1024;
1941
+
1938
1942
  /** 初始化 Upload 控件 */
1939
1943
  function initializeUploadControls() {
1940
1944
  const dropzone = document.getElementById('upload-dropzone');
@@ -1946,16 +1950,30 @@ function initializeUploadControls() {
1946
1950
  if (!dropzone) return;
1947
1951
 
1948
1952
  // 文件选择
1949
- selectBtn.addEventListener('click', () => fileInput.click());
1953
+ selectBtn.addEventListener('click', (e) => {
1954
+ e.stopPropagation(); // 防止冒泡到 dropzone 的 click 事件
1955
+ fileInput.click();
1956
+ });
1950
1957
  fileInput.addEventListener('change', (e) => {
1951
1958
  if (e.target.files.length > 0) {
1952
- uploadState.file = e.target.files[0];
1959
+ const file = e.target.files[0];
1960
+ if (file.size > MAX_UPLOAD_SIZE) {
1961
+ const sizeMB = (file.size / 1024 / 1024).toFixed(1);
1962
+ showToast(`File too large (${sizeMB} MB). Maximum is 50 MB.`, 'error');
1963
+ e.target.value = '';
1964
+ return;
1965
+ }
1966
+ uploadState.file = file;
1953
1967
  submitBtn.disabled = false;
1954
1968
  }
1955
1969
  });
1956
1970
 
1957
- // 拖拽上传
1958
- dropzone.addEventListener('click', () => fileInput.click());
1971
+ // 拖拽上传 — 仅处理拖拽事件,点击由 Choose File 按钮或单独点击处理
1972
+ dropzone.addEventListener('click', (e) => {
1973
+ // 如果点击的是内部按钮,不重复触发文件选择(由按钮的 stopPropagation 处理)
1974
+ if (e.target.closest('button')) return;
1975
+ fileInput.click();
1976
+ });
1959
1977
 
1960
1978
  dropzone.addEventListener('dragover', (e) => {
1961
1979
  e.preventDefault();
@@ -1970,7 +1988,13 @@ function initializeUploadControls() {
1970
1988
  e.preventDefault();
1971
1989
  dropzone.classList.remove('drag-over');
1972
1990
  if (e.dataTransfer.files.length > 0) {
1973
- uploadState.file = e.dataTransfer.files[0];
1991
+ const file = e.dataTransfer.files[0];
1992
+ if (file.size > MAX_UPLOAD_SIZE) {
1993
+ const sizeMB = (file.size / 1024 / 1024).toFixed(1);
1994
+ showToast(`File too large (${sizeMB} MB). Maximum is 50 MB.`, 'error');
1995
+ return;
1996
+ }
1997
+ uploadState.file = file;
1974
1998
  submitBtn.disabled = false;
1975
1999
  }
1976
2000
  });
@@ -1981,6 +2005,12 @@ function initializeUploadControls() {
1981
2005
  showToast(t('upload.errorNoFile'), 'error');
1982
2006
  return;
1983
2007
  }
2008
+ // 文件大小校验
2009
+ if (uploadState.file.size > MAX_UPLOAD_SIZE) {
2010
+ const sizeMB = (uploadState.file.size / 1024 / 1024).toFixed(1);
2011
+ showToast(`File too large (${sizeMB} MB). Maximum is 50 MB.`, 'error');
2012
+ return;
2013
+ }
1984
2014
  // Use skill name override if provided
1985
2015
  const override = skillNameInput.value.trim();
1986
2016
  handleUpload(uploadState.file, override || undefined);
@@ -2007,6 +2037,7 @@ function resetUploadView() {
2007
2037
  uploadState.file = null;
2008
2038
  uploadState.data = null;
2009
2039
  uploadState.skillName = '';
2040
+ uploadState.tempDir = '';
2010
2041
  document.getElementById('upload-phase1').classList.remove('hidden');
2011
2042
  document.getElementById('upload-phase2').classList.add('hidden');
2012
2043
  document.getElementById('upload-submit-btn').disabled = true;
@@ -2068,6 +2099,7 @@ async function handleUpload(file, skillNameOverride) {
2068
2099
 
2069
2100
  uploadState.data = result;
2070
2101
  uploadState.skillName = skillNameOverride || result.skillName;
2102
+ uploadState.tempDir = result.tempDir || '';
2071
2103
 
2072
2104
  // Switch to preview phase
2073
2105
  setTimeout(() => {
@@ -2151,6 +2183,7 @@ async function executeUploadAction(action) {
2151
2183
  body: JSON.stringify({
2152
2184
  skillName: uploadState.skillName,
2153
2185
  action: action,
2186
+ tempDir: uploadState.tempDir,
2154
2187
  }),
2155
2188
  });
2156
2189
 
package/package.json CHANGED
@@ -1,16 +1,23 @@
1
1
  {
2
2
  "name": "itismyskillmarket",
3
- "version": "1.3.37",
3
+ "version": "1.3.39",
4
4
  "description": "Cross-platform skill manager for AI coding tools",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "skm": "dist/index.js"
8
8
  },
9
+ "main": "electron/main.mjs",
9
10
  "scripts": {
10
11
  "build": "tsup",
11
12
  "dev": "tsup --watch",
12
13
  "test": "vitest",
13
- "build:exe": "node scripts/build-exe.mjs"
14
+ "build:exe": "node scripts/build-exe.mjs",
15
+ "build:installer": "npm run build:exe && ISCC scripts/skillmarket.iss",
16
+ "build:installer:ps": "npm run build:exe && powershell -ExecutionPolicy Bypass -File scripts/install.ps1 -Silent",
17
+ "install:local": "powershell -ExecutionPolicy Bypass -File scripts/install.ps1",
18
+ "uninstall:local": "powershell -ExecutionPolicy Bypass -File scripts/install.ps1 -Uninstall",
19
+ "electron:dev": "npm run build && npx electron electron/main.mjs",
20
+ "electron:build": "npm run build && npx electron-builder build --win"
14
21
  },
15
22
  "dependencies": {
16
23
  "adm-zip": "^0.5.17",
@@ -19,15 +26,65 @@
19
26
  "tar": "^7.5.15"
20
27
  },
21
28
  "devDependencies": {
29
+ "@types/adm-zip": "^0.5.8",
22
30
  "@types/fs-extra": "^11.0.4",
23
31
  "@yao-pkg/pkg": "^6.19.0",
32
+ "electron": "^33.0.0",
33
+ "electron-builder": "^25.0.0",
24
34
  "postject": "^1.0.0-alpha.6",
25
35
  "tsup": "^8.0.0",
26
36
  "typescript": "^5.3.0",
27
37
  "vitest": "^1.2.0"
28
38
  },
39
+ "build": {
40
+ "appId": "com.skillmarket.app",
41
+ "productName": "SkillMarket",
42
+ "directories": {
43
+ "output": "release"
44
+ },
45
+ "files": [
46
+ "dist/**/*",
47
+ "electron/**/*",
48
+ "gui/**/*",
49
+ "package.json"
50
+ ],
51
+ "win": {
52
+ "target": [
53
+ {
54
+ "target": "nsis",
55
+ "arch": [
56
+ "x64"
57
+ ]
58
+ }
59
+ ],
60
+ "icon": "electron/icon.ico"
61
+ },
62
+ "nsis": {
63
+ "oneClick": false,
64
+ "allowToChangeInstallationDirectory": true,
65
+ "createDesktopShortcut": true,
66
+ "createStartMenuShortcut": true,
67
+ "shortcutName": "SkillMarket",
68
+ "uninstallDisplayName": "SkillMarket",
69
+ "installerIcon": "electron/icon.ico",
70
+ "uninstallerIcon": "electron/icon.ico"
71
+ },
72
+ "extraResources": [
73
+ {
74
+ "from": "scripts",
75
+ "to": "scripts",
76
+ "filter": [
77
+ "*.ps1",
78
+ "*.iss"
79
+ ]
80
+ }
81
+ ],
82
+ "asar": true,
83
+ "compression": "maximum"
84
+ },
29
85
  "files": [
30
86
  "dist",
31
- "gui"
87
+ "gui",
88
+ "electron"
32
89
  ]
33
90
  }