diff --git a/server/routes/company.js b/server/routes/company.js index 068ab20..e33ba60 100644 --- a/server/routes/company.js +++ b/server/routes/company.js @@ -1431,6 +1431,138 @@ router.get('/:companyId/files/content', (req, res) => { } }); +// PUT /api/company/:companyId/files/content — 保存文件内容 +router.put('/:companyId/files/content', (req, res) => { + try { + const DATA_DIR = path.resolve(__dirname, '../../data/companies'); + const filePath = req.body.path; + if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); + + const absPath = path.resolve(DATA_DIR, req.params.companyId, filePath); + const safeBoundary = path.resolve(DATA_DIR, req.params.companyId); + if (!absPath.startsWith(safeBoundary)) { + return res.status(403).json({ error: '路径越界' }); + } + + const dir = path.dirname(absPath); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + + fs.writeFileSync(absPath, req.body.content || '', 'utf-8'); + console.log(`[Company] 💾 已保存文件 ${req.params.companyId}/${filePath}`); + res.json({ success: true, path: filePath }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// ═══════════════════════════════════════ +// Company Workspace — 工作区文件管理 +// ═══════════════════════════════════════ + +// GET /api/company/:companyId/workspace/files — 列出 workspace 文件树 +router.get('/:companyId/workspace/files', (req, res) => { + try { + const DATA_DIR = path.resolve(__dirname, '../../data/companies'); + const wsDir = path.join(DATA_DIR, req.params.companyId, 'workspace'); + + if (!fs.existsSync(wsDir)) { + return res.json({ tree: [], totalFiles: 0 }); + } + + function scanDir(dirPath, relativePath = '') { + const entries = fs.readdirSync(dirPath, { withFileTypes: true }); + const nodes = []; + for (const entry of entries) { + const fullPath = path.join(dirPath, entry.name); + const relPath = relativePath ? `${relativePath}/${entry.name}` : entry.name; + if (entry.name.startsWith('.') || entry.name === 'node_modules') continue; + + if (entry.isDirectory()) { + const children = scanDir(fullPath, relPath); + nodes.push({ + name: entry.name, path: relPath, type: 'directory', children, + fileCount: children.reduce((sum, c) => sum + (c.type === 'file' ? 1 : (c.fileCount || 0)), 0), + }); + } else { + const stat = fs.statSync(fullPath); + nodes.push({ + name: entry.name, path: relPath, type: 'file', + size: stat.size, + createdAt: stat.birthtime.toISOString(), + modifiedAt: stat.mtime.toISOString(), + }); + } + } + return nodes.sort((a, b) => { + if (a.type !== b.type) return a.type === 'directory' ? -1 : 1; + return a.name.localeCompare(b.name); + }); + } + + const tree = scanDir(wsDir); + const countFiles = (nodes) => nodes.reduce((sum, n) => sum + (n.type === 'file' ? 1 : countFiles(n.children || [])), 0); + res.json({ tree, totalFiles: countFiles(tree) }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// GET /api/company/:companyId/workspace/files/content?path=xxx — 读取 workspace 文件内容 +router.get('/:companyId/workspace/files/content', (req, res) => { + try { + const DATA_DIR = path.resolve(__dirname, '../../data/companies'); + const filePath = String(req.query.path || ''); + if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); + + const absPath = path.resolve(DATA_DIR, req.params.companyId, 'workspace', filePath); + const safeBoundary = path.resolve(DATA_DIR, req.params.companyId, 'workspace'); + if (!absPath.startsWith(safeBoundary)) { + return res.status(403).json({ error: '路径越界' }); + } + if (!fs.existsSync(absPath)) { + return res.status(404).json({ error: '文件不存在' }); + } + + const stat = fs.statSync(absPath); + if (stat.size > 512 * 1024) { + return res.json({ content: '(文件过大,跳过预览)', truncated: true, size: stat.size }); + } + + const content = fs.readFileSync(absPath, 'utf-8'); + res.json({ + content, size: stat.size, path: filePath, + createdAt: stat.birthtime.toISOString(), + modifiedAt: stat.mtime.toISOString(), + }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + +// PUT /api/company/:companyId/workspace/files/content — 保存 workspace 文件 +router.put('/:companyId/workspace/files/content', (req, res) => { + try { + const DATA_DIR = path.resolve(__dirname, '../../data/companies'); + const filePath = req.body.path; + if (!filePath) return res.status(400).json({ error: 'path 参数为必填' }); + + const absPath = path.resolve(DATA_DIR, req.params.companyId, 'workspace', filePath); + const safeBoundary = path.resolve(DATA_DIR, req.params.companyId, 'workspace'); + if (!absPath.startsWith(safeBoundary)) { + return res.status(403).json({ error: '路径越界' }); + } + + const dir = path.dirname(absPath); + if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); + + fs.writeFileSync(absPath, req.body.content || '', 'utf-8'); + console.log(`[Workspace] 💾 已保存 ${req.params.companyId}/workspace/${filePath}`); + res.json({ success: true, path: filePath }); + } catch (err) { + res.status(500).json({ error: err.message }); + } +}); + // ═══════════════════════════════════════ // Company Messages — 全局消息时间线 // ═══════════════════════════════════════ diff --git a/web/src/components/AppNav.vue b/web/src/components/AppNav.vue index adb7529..8bf3962 100644 --- a/web/src/components/AppNav.vue +++ b/web/src/components/AppNav.vue @@ -5,21 +5,21 @@
diff --git a/web/src/router/index.js b/web/src/router/index.js index 7e00324..5d8e8ab 100644 --- a/web/src/router/index.js +++ b/web/src/router/index.js @@ -33,7 +33,7 @@ const routes = [ component: () => import('@/views/CompanyDashboard.vue'), }, { - path: '/:id/:tab(overview|agents|pipelines|goals|sop|okr|team|files|messages|skills|jobs)', + path: '/:id/:tab(overview|agents|pipelines|goals|sop|okr|team|files|messages|skills|jobs|workspace)', name: 'CompanyDetail', component: () => import('@/views/CompanyDashboard.vue'), props: true diff --git a/web/src/views/CompanyDashboard.vue b/web/src/views/CompanyDashboard.vue index 8e6881d..dfe37e8 100644 --- a/web/src/views/CompanyDashboard.vue +++ b/web/src/views/CompanyDashboard.vue @@ -445,6 +445,95 @@ + +{{ wsContent }}
+
+
+
+
+ 从左侧选择一个文件查看详情与内容
+