@ian2018cs/agenthub 0.1.93 → 0.1.94
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/assets/{index-DpHkMTtJ.js → index-TP3SFSHb.js} +55 -55
- package/dist/assets/index-b3adGRUG.css +32 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/projects.js +94 -2
- package/server/routes/agents.js +39 -19
- package/server/routes/share.js +118 -3
- package/dist/assets/index-DB64ms-M.css +0 -32
package/server/routes/agents.js
CHANGED
|
@@ -797,21 +797,14 @@ router.post('/refresh', async (_req, res) => {
|
|
|
797
797
|
});
|
|
798
798
|
|
|
799
799
|
/**
|
|
800
|
-
*
|
|
801
|
-
*
|
|
800
|
+
* Core agent install logic, extracted for reuse (e.g., clone flow).
|
|
801
|
+
* Returns { project, skillResults, mcpResults, gitRepoResults } or throws.
|
|
802
802
|
*/
|
|
803
|
-
|
|
804
|
-
try {
|
|
805
|
-
const userUuid = req.user?.uuid;
|
|
806
|
-
if (!userUuid) return res.status(401).json({ error: 'User authentication required' });
|
|
807
|
-
|
|
808
|
-
const { agentName, force = false } = req.body;
|
|
809
|
-
if (!agentName) return res.status(400).json({ error: 'agentName is required' });
|
|
810
|
-
|
|
803
|
+
async function installAgentForUser(agentName, userUuid, force = false) {
|
|
811
804
|
// Scan to find the agent
|
|
812
805
|
const agents = await scanAgents();
|
|
813
806
|
const agent = agents.find(a => a.name === agentName || a.dirName === agentName);
|
|
814
|
-
if (!agent)
|
|
807
|
+
if (!agent) throw Object.assign(new Error(`Agent "${agentName}" not found`), { notFound: true });
|
|
815
808
|
|
|
816
809
|
const userPaths = getUserPaths(userUuid);
|
|
817
810
|
const projectDir = path.join(userPaths.projectsDir, agentName);
|
|
@@ -851,11 +844,14 @@ router.post('/install', async (req, res) => {
|
|
|
851
844
|
if (localHash !== storedHash) {
|
|
852
845
|
let repoContent = '';
|
|
853
846
|
try { repoContent = await fs.readFile(path.join(agent.path, 'CLAUDE.md'), 'utf-8'); } catch {}
|
|
854
|
-
|
|
847
|
+
throw Object.assign(new Error('本地 CLAUDE.md 已被修改,更新将覆盖本地改动'), {
|
|
855
848
|
conflict: true,
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
849
|
+
conflictData: {
|
|
850
|
+
conflict: true,
|
|
851
|
+
error: '本地 CLAUDE.md 已被修改,更新将覆盖本地改动',
|
|
852
|
+
localContent,
|
|
853
|
+
repoContent,
|
|
854
|
+
},
|
|
859
855
|
});
|
|
860
856
|
}
|
|
861
857
|
} catch {}
|
|
@@ -1018,14 +1014,37 @@ router.post('/install', async (req, res) => {
|
|
|
1018
1014
|
};
|
|
1019
1015
|
await saveProjectConfig(config, userUuid);
|
|
1020
1016
|
|
|
1017
|
+
return {
|
|
1018
|
+
project: { ...project, agentInfo: config[projectKey].agentInfo },
|
|
1019
|
+
skillResults,
|
|
1020
|
+
mcpResults,
|
|
1021
|
+
gitRepoResults,
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
/**
|
|
1026
|
+
* POST /api/agents/install
|
|
1027
|
+
* Install an agent: create project, copy CLAUDE.md, install Skills + MCPs
|
|
1028
|
+
*/
|
|
1029
|
+
router.post('/install', async (req, res) => {
|
|
1030
|
+
try {
|
|
1031
|
+
const userUuid = req.user?.uuid;
|
|
1032
|
+
if (!userUuid) return res.status(401).json({ error: 'User authentication required' });
|
|
1033
|
+
|
|
1034
|
+
const { agentName, force = false } = req.body;
|
|
1035
|
+
if (!agentName) return res.status(400).json({ error: 'agentName is required' });
|
|
1036
|
+
|
|
1037
|
+
const result = await installAgentForUser(agentName, userUuid, force);
|
|
1021
1038
|
res.json({
|
|
1022
1039
|
success: true,
|
|
1023
|
-
project:
|
|
1024
|
-
skills: skillResults,
|
|
1025
|
-
mcps: mcpResults,
|
|
1026
|
-
gitRepos: gitRepoResults
|
|
1040
|
+
project: result.project,
|
|
1041
|
+
skills: result.skillResults,
|
|
1042
|
+
mcps: result.mcpResults,
|
|
1043
|
+
gitRepos: result.gitRepoResults,
|
|
1027
1044
|
});
|
|
1028
1045
|
} catch (error) {
|
|
1046
|
+
if (error.notFound) return res.status(404).json({ error: error.message });
|
|
1047
|
+
if (error.conflict) return res.status(409).json(error.conflictData);
|
|
1029
1048
|
console.error('Error installing agent:', error);
|
|
1030
1049
|
res.status(500).json({ error: 'Failed to install agent', details: error.message });
|
|
1031
1050
|
}
|
|
@@ -2083,3 +2102,4 @@ router.post('/submissions/:id/reject', async (req, res) => {
|
|
|
2083
2102
|
});
|
|
2084
2103
|
|
|
2085
2104
|
export default router;
|
|
2105
|
+
export { installAgentForUser };
|
package/server/routes/share.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
2
|
import fs from 'fs';
|
|
3
|
-
import
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { shareDb, userDb, imageDb, agentSubmissionDb } from '../database/db.js';
|
|
6
|
+
import { getImagePath, saveImage } from '../services/image-storage.js';
|
|
7
|
+
import { getSessionMessages, loadProjectConfig, addProjectManually, cloneSession } from '../projects.js';
|
|
6
8
|
import { extractShareMessages, saveStaticSnapshot, readStaticSnapshot, stripImagePaths } from '../services/share-renderer.js';
|
|
7
9
|
import { authenticateToken } from '../middleware/auth.js';
|
|
10
|
+
import { getUserPaths } from '../services/user-directories.js';
|
|
11
|
+
import { installAgentForUser } from './agents.js';
|
|
8
12
|
|
|
9
13
|
const router = express.Router();
|
|
10
14
|
|
|
@@ -155,6 +159,117 @@ router.delete('/:sessionId', authenticateToken, (req, res) => {
|
|
|
155
159
|
res.json({ ok: true });
|
|
156
160
|
});
|
|
157
161
|
|
|
162
|
+
// POST /api/share/:userUuid/:sessionId/clone — 克隆公开分享的会话(需要鉴权)
|
|
163
|
+
router.post('/:userUuid/:sessionId/clone', authenticateToken, async (req, res) => {
|
|
164
|
+
const { userUuid: sourceUserUuid, sessionId: sourceSessionId } = req.params;
|
|
165
|
+
const targetUserUuid = req.user.uuid;
|
|
166
|
+
|
|
167
|
+
const share = shareDb.getShare(sourceUserUuid, sourceSessionId);
|
|
168
|
+
if (!share || share.status !== 'active') {
|
|
169
|
+
return res.status(404).json({ error: '分享不存在或已关闭' });
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// 读取源项目配置,判断是否为 Agent 项目
|
|
173
|
+
const sourceConfig = await loadProjectConfig(sourceUserUuid);
|
|
174
|
+
const sourceProjectEntry = sourceConfig[share.project_name];
|
|
175
|
+
const isAgentProject = sourceProjectEntry?.agentInfo?.isAgent === true;
|
|
176
|
+
const agentName = sourceProjectEntry?.agentInfo?.agentName || null;
|
|
177
|
+
const sourceDisplayName = sourceProjectEntry?.displayName || share.session_title || 'Cloned Project';
|
|
178
|
+
|
|
179
|
+
let targetProjectName;
|
|
180
|
+
let targetProjectDir;
|
|
181
|
+
|
|
182
|
+
if (isAgentProject && agentName) {
|
|
183
|
+
// 检查目标用户是否有该 agent 的可见权限
|
|
184
|
+
const targetUser = userDb.getUserByUuid(targetUserUuid);
|
|
185
|
+
const isAdmin = targetUser?.role === 'admin' || targetUser?.role === 'super_admin';
|
|
186
|
+
let canInstallAgent = isAdmin;
|
|
187
|
+
if (!isAdmin && targetUser?.id) {
|
|
188
|
+
const visibleRaw = userDb.getVisibleAgents(targetUser.id);
|
|
189
|
+
const visibleSet = new Set();
|
|
190
|
+
if (visibleRaw) {
|
|
191
|
+
try { JSON.parse(visibleRaw).forEach(n => visibleSet.add(n)); } catch (_) {}
|
|
192
|
+
}
|
|
193
|
+
agentSubmissionDb.getApprovedAgentNamesByUser(targetUser.id).forEach(n => visibleSet.add(n));
|
|
194
|
+
canInstallAgent = visibleSet.has(agentName);
|
|
195
|
+
}
|
|
196
|
+
if (!canInstallAgent) {
|
|
197
|
+
return res.status(403).json({ error: '您没有该 Agent 项目的使用权限,请联系管理员授权后再克隆' });
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Agent 项目:触发完整安装流程(复制模板文件、skills、MCPs、写 agentInfo)
|
|
201
|
+
try {
|
|
202
|
+
const result = await installAgentForUser(agentName, targetUserUuid);
|
|
203
|
+
targetProjectDir = result.project.fullPath;
|
|
204
|
+
targetProjectName = result.project.name;
|
|
205
|
+
} catch (err) {
|
|
206
|
+
// Agent 可能已下架 → 降级为普通项目克隆
|
|
207
|
+
console.warn('[clone] agent install failed, falling back to plain project:', err.message);
|
|
208
|
+
const targetUserPaths = getUserPaths(targetUserUuid);
|
|
209
|
+
targetProjectDir = path.join(targetUserPaths.projectsDir, agentName);
|
|
210
|
+
await fs.promises.mkdir(targetProjectDir, { recursive: true });
|
|
211
|
+
try {
|
|
212
|
+
const proj = await addProjectManually(targetProjectDir, sourceDisplayName, targetUserUuid);
|
|
213
|
+
targetProjectName = proj.name;
|
|
214
|
+
} catch {
|
|
215
|
+
targetProjectName = targetProjectDir.replace(/\//g, '-');
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} else {
|
|
219
|
+
// 普通项目:创建同名目录并注册
|
|
220
|
+
const targetUserPaths = getUserPaths(targetUserUuid);
|
|
221
|
+
const safeDirName = sourceDisplayName.replace(/[^a-zA-Z0-9\u4e00-\u9fff._-]/g, '_').substring(0, 80);
|
|
222
|
+
targetProjectDir = path.join(targetUserPaths.projectsDir, safeDirName);
|
|
223
|
+
await fs.promises.mkdir(targetProjectDir, { recursive: true });
|
|
224
|
+
try {
|
|
225
|
+
const proj = await addProjectManually(targetProjectDir, sourceDisplayName, targetUserUuid);
|
|
226
|
+
targetProjectName = proj.name;
|
|
227
|
+
} catch {
|
|
228
|
+
targetProjectName = targetProjectDir.replace(/\//g, '-');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 克隆 JSONL 会话
|
|
233
|
+
const newSessionId = await cloneSession(
|
|
234
|
+
sourceUserUuid, share.project_name, sourceSessionId,
|
|
235
|
+
targetUserUuid, targetProjectDir
|
|
236
|
+
);
|
|
237
|
+
if (!newSessionId) {
|
|
238
|
+
return res.status(500).json({ error: '克隆会话失败:源会话为空' });
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 克隆图片(文件 + DB 记录)
|
|
242
|
+
const sourceImages = imageDb.getImagesBySession(sourceSessionId);
|
|
243
|
+
for (const img of sourceImages) {
|
|
244
|
+
const srcPath = getImagePath(sourceUserUuid, img.file_hash, img.file_ext);
|
|
245
|
+
try {
|
|
246
|
+
const buffer = await fs.promises.readFile(srcPath);
|
|
247
|
+
await saveImage(targetUserUuid, buffer, img.file_ext);
|
|
248
|
+
const batchId = randomUUID();
|
|
249
|
+
imageDb.insertImage({
|
|
250
|
+
upload_batch_id: batchId,
|
|
251
|
+
user_uuid: targetUserUuid,
|
|
252
|
+
original_name: img.original_name,
|
|
253
|
+
file_hash: img.file_hash,
|
|
254
|
+
file_ext: img.file_ext,
|
|
255
|
+
file_size: buffer.length,
|
|
256
|
+
mime_type: img.mime_type,
|
|
257
|
+
});
|
|
258
|
+
imageDb.associateBatch(batchId, newSessionId, img.message_content);
|
|
259
|
+
} catch {
|
|
260
|
+
// 图片文件可能已被清理,跳过不阻塞整体克隆
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
res.json({
|
|
265
|
+
success: true,
|
|
266
|
+
sessionId: newSessionId,
|
|
267
|
+
projectName: targetProjectName,
|
|
268
|
+
isAgentProject,
|
|
269
|
+
agentName,
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
158
273
|
// GET /api/share/:userUuid/:sessionId/images/:imageId — 公开访问,分享页图片
|
|
159
274
|
router.get('/:userUuid/:sessionId/images/:imageId', (req, res) => {
|
|
160
275
|
const { userUuid, sessionId, imageId } = req.params;
|