@ian2018cs/agenthub 0.1.93 → 0.1.95

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.
@@ -797,21 +797,14 @@ router.post('/refresh', async (_req, res) => {
797
797
  });
798
798
 
799
799
  /**
800
- * POST /api/agents/install
801
- * Install an agent: create project, copy CLAUDE.md, install Skills + MCPs
800
+ * Core agent install logic, extracted for reuse (e.g., clone flow).
801
+ * Returns { project, skillResults, mcpResults, gitRepoResults } or throws.
802
802
  */
803
- router.post('/install', async (req, res) => {
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) return res.status(404).json({ error: `Agent "${agentName}" not found` });
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
- return res.status(409).json({
847
+ throw Object.assign(new Error('本地 CLAUDE.md 已被修改,更新将覆盖本地改动'), {
855
848
  conflict: true,
856
- error: '本地 CLAUDE.md 已被修改,更新将覆盖本地改动',
857
- localContent,
858
- repoContent
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: { ...project, agentInfo: config[projectKey].agentInfo },
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 };
@@ -1,10 +1,14 @@
1
1
  import express from 'express';
2
2
  import fs from 'fs';
3
- import { shareDb, userDb, imageDb } from '../database/db.js';
4
- import { getImagePath } from '../services/image-storage.js';
5
- import { getSessionMessages, loadProjectConfig } from '../projects.js';
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;