@caoruhua/open-claude-remote 1.0.9 → 1.3.6
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/README.md +48 -27
- package/dist/backend/src/api/file-routes.d.ts +0 -24
- package/dist/backend/src/api/file-routes.d.ts.map +1 -1
- package/dist/backend/src/api/file-routes.js +464 -68
- package/dist/backend/src/api/file-routes.js.map +1 -1
- package/dist/backend/src/api/instance-routes.d.ts +4 -0
- package/dist/backend/src/api/instance-routes.d.ts.map +1 -1
- package/dist/backend/src/api/instance-routes.js +27 -2
- package/dist/backend/src/api/instance-routes.js.map +1 -1
- package/dist/backend/src/cli-utils.d.ts +2 -0
- package/dist/backend/src/cli-utils.d.ts.map +1 -1
- package/dist/backend/src/cli-utils.js +11 -1
- package/dist/backend/src/cli-utils.js.map +1 -1
- package/dist/backend/src/cli.d.ts +6 -0
- package/dist/backend/src/cli.d.ts.map +1 -1
- package/dist/backend/src/cli.js +46 -7
- package/dist/backend/src/cli.js.map +1 -1
- package/dist/backend/src/config.d.ts +11 -0
- package/dist/backend/src/config.d.ts.map +1 -1
- package/dist/backend/src/config.js +36 -1
- package/dist/backend/src/config.js.map +1 -1
- package/dist/backend/src/daemon/daemon-client.d.ts +1 -0
- package/dist/backend/src/daemon/daemon-client.d.ts.map +1 -1
- package/dist/backend/src/daemon/daemon-client.js +1 -0
- package/dist/backend/src/daemon/daemon-client.js.map +1 -1
- package/dist/backend/src/daemon/daemon-launcher.d.ts.map +1 -1
- package/dist/backend/src/daemon/daemon-launcher.js +4 -1
- package/dist/backend/src/daemon/daemon-launcher.js.map +1 -1
- package/dist/backend/src/daemon/restart-state.d.ts +1 -0
- package/dist/backend/src/daemon/restart-state.d.ts.map +1 -1
- package/dist/backend/src/daemon/restart-state.js.map +1 -1
- package/dist/backend/src/deps/detector.js +1 -1
- package/dist/backend/src/deps/index.d.ts +1 -1
- package/dist/backend/src/deps/index.js +22 -22
- package/dist/backend/src/deps/installer.js +6 -6
- package/dist/backend/src/deps/installer.js.map +1 -1
- package/dist/backend/src/deps/types.d.ts +1 -1
- package/dist/backend/src/deps/types.d.ts.map +1 -1
- package/dist/backend/src/deps/types.js +9 -11
- package/dist/backend/src/deps/types.js.map +1 -1
- package/dist/backend/src/index.d.ts.map +1 -1
- package/dist/backend/src/index.js +29 -6
- package/dist/backend/src/index.js.map +1 -1
- package/dist/backend/src/instance/instance-manager.d.ts +4 -0
- package/dist/backend/src/instance/instance-manager.d.ts.map +1 -1
- package/dist/backend/src/instance/instance-manager.js +30 -7
- package/dist/backend/src/instance/instance-manager.js.map +1 -1
- package/dist/backend/src/instance/instance-session.d.ts +6 -1
- package/dist/backend/src/instance/instance-session.d.ts.map +1 -1
- package/dist/backend/src/instance/instance-session.js +9 -3
- package/dist/backend/src/instance/instance-session.js.map +1 -1
- package/dist/backend/src/instance/types.d.ts +1 -0
- package/dist/backend/src/instance/types.d.ts.map +1 -1
- package/dist/backend/src/pty/fix-pty-permissions.d.ts +1 -1
- package/dist/backend/src/pty/fix-pty-permissions.js +1 -1
- package/dist/backend/src/pty/output-buffer.d.ts +6 -0
- package/dist/backend/src/pty/output-buffer.d.ts.map +1 -1
- package/dist/backend/src/pty/output-buffer.js +10 -0
- package/dist/backend/src/pty/output-buffer.js.map +1 -1
- package/dist/backend/src/registry/stop-instances.js +1 -1
- package/dist/backend/src/services/pptx-converter.d.ts +66 -0
- package/dist/backend/src/services/pptx-converter.d.ts.map +1 -0
- package/dist/backend/src/services/pptx-converter.js +282 -0
- package/dist/backend/src/services/pptx-converter.js.map +1 -0
- package/dist/backend/src/update.d.ts +2 -2
- package/dist/backend/src/update.d.ts.map +1 -1
- package/dist/backend/src/update.js +9 -9
- package/dist/backend/src/update.js.map +1 -1
- package/dist/backend/src/utils/banner.d.ts +2 -0
- package/dist/backend/src/utils/banner.d.ts.map +1 -1
- package/dist/backend/src/utils/banner.js +2 -1
- package/dist/backend/src/utils/banner.js.map +1 -1
- package/dist/backend/src/utils/network.d.ts +22 -0
- package/dist/backend/src/utils/network.d.ts.map +1 -1
- package/dist/backend/src/utils/network.js +54 -0
- package/dist/backend/src/utils/network.js.map +1 -1
- package/dist/shared/file-types.d.ts +176 -0
- package/dist/shared/file-types.d.ts.map +1 -0
- package/dist/shared/file-types.js +108 -0
- package/dist/shared/file-types.js.map +1 -0
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/index.js +1 -0
- package/dist/shared/index.js.map +1 -1
- package/dist/shared/instance.d.ts +2 -0
- package/dist/shared/instance.d.ts.map +1 -1
- package/dist/shared/instance.js.map +1 -1
- package/frontend-dist/assets/{index-UXAwH56Q.css → index-BY0fnkbW.css} +1 -1
- package/frontend-dist/assets/index-qwSai8-t.js +211 -0
- package/frontend-dist/index.html +2 -2
- package/package.json +8 -4
- package/scripts/build.sh +3 -9
- package/scripts/dev.sh +1 -4
- package/scripts/git-hooks/pre-commit +0 -4
- package/scripts/publish-npm.sh +61 -0
- package/scripts/stop.sh +1 -1
- package/frontend-dist/assets/index-CVRtrLyp.js +0 -199
|
@@ -1,30 +1,17 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { readFile, stat, readdir } from 'node:fs/promises';
|
|
3
3
|
import { createReadStream } from 'node:fs';
|
|
4
|
-
import { extname, join, relative, resolve, normalize } from 'node:path';
|
|
4
|
+
import { extname, join, relative, basename, resolve, normalize } from 'node:path';
|
|
5
5
|
import { loadUserConfig } from '../config.js';
|
|
6
6
|
import { logger } from '../logger/logger.js';
|
|
7
|
+
import { SUPPORTED_EXTENSIONS, IMAGE_EXTENSIONS, PREVIEWABLE_EXTENSIONS, CONVERTIBLE_TO_PDF, FILE_MIME_TYPES, getFileCategory, } from '../../../shared/index.js';
|
|
8
|
+
import { createConversionTask, getTaskStatus, startConversion, isSofficeAvailable, getLibreOfficeInstallGuide, TEMP_DIR } from '../services/pptx-converter.js';
|
|
7
9
|
/** 文件大小限制:5MB(与 PRD 一致) */
|
|
8
10
|
const MAX_FILE_SIZE = 5 * 1024 * 1024;
|
|
9
|
-
/** 允许的文件扩展名 */
|
|
10
|
-
const ALLOWED_EXTENSIONS = ['.md', '.markdown'];
|
|
11
|
-
/** 允许的图片扩展名 */
|
|
12
|
-
const ALLOWED_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg', '.ico', '.bmp'];
|
|
13
|
-
/** 图片 MIME 类型映射 */
|
|
14
|
-
const IMAGE_MIME_TYPES = {
|
|
15
|
-
'.png': 'image/png',
|
|
16
|
-
'.jpg': 'image/jpeg',
|
|
17
|
-
'.jpeg': 'image/jpeg',
|
|
18
|
-
'.gif': 'image/gif',
|
|
19
|
-
'.webp': 'image/webp',
|
|
20
|
-
'.svg': 'image/svg+xml',
|
|
21
|
-
'.ico': 'image/x-icon',
|
|
22
|
-
'.bmp': 'image/bmp',
|
|
23
|
-
};
|
|
24
11
|
/** 最大扫描文件数限制 */
|
|
25
12
|
const MAX_FILES = 1000;
|
|
26
13
|
/** 跳过的目录 */
|
|
27
|
-
const SKIP_DIRS = ['node_modules', 'dist', 'build', '.git'];
|
|
14
|
+
const SKIP_DIRS = ['node_modules', 'dist', 'build', '.git', '.svn', '.hg'];
|
|
28
15
|
/**
|
|
29
16
|
* 检查路径是否在允许的目录白名单范围内
|
|
30
17
|
* @param path - 要检查的路径(绝对路径)
|
|
@@ -33,6 +20,10 @@ const SKIP_DIRS = ['node_modules', 'dist', 'build', '.git'];
|
|
|
33
20
|
*/
|
|
34
21
|
function isPathAllowed(path, allowedCwds) {
|
|
35
22
|
const normalizedPath = normalize(path);
|
|
23
|
+
// 允许访问 PPTX 转换输出目录
|
|
24
|
+
if (normalizedPath.startsWith(TEMP_DIR + '/') || normalizedPath === TEMP_DIR) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
36
27
|
return allowedCwds.some((cwd) => {
|
|
37
28
|
const normalizedCwd = normalize(cwd);
|
|
38
29
|
return normalizedPath.startsWith(normalizedCwd + '/') || normalizedPath === normalizedCwd;
|
|
@@ -87,24 +78,39 @@ function getAllowedCwds(instanceManager) {
|
|
|
87
78
|
return [...new Set([...configWorkspaces, ...instanceCwds])];
|
|
88
79
|
}
|
|
89
80
|
/**
|
|
90
|
-
*
|
|
91
|
-
* @param dir - 要扫描的目录
|
|
92
|
-
* @param baseDir - 基准目录(用于计算相对路径)
|
|
93
|
-
* @param allowedCwds - 允许的目录白名单
|
|
94
|
-
* @param files - 已收集的文件列表(递归用)
|
|
95
|
-
* @returns Markdown 文件列表
|
|
81
|
+
* 构建目录树节点
|
|
96
82
|
*/
|
|
97
|
-
|
|
83
|
+
function buildDirectoryNode(name, path, relativePath) {
|
|
84
|
+
return {
|
|
85
|
+
name,
|
|
86
|
+
path,
|
|
87
|
+
relativePath,
|
|
88
|
+
children: [],
|
|
89
|
+
fileCount: 0,
|
|
90
|
+
typeCounts: {
|
|
91
|
+
markdown: 0,
|
|
92
|
+
pdf: 0,
|
|
93
|
+
presentation: 0,
|
|
94
|
+
image: 0,
|
|
95
|
+
all: 0,
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
async function scanFiles(dir, baseDir, allowedCwds, result, category = 'all', searchQuery, currentNode) {
|
|
98
100
|
// 安全检查:路径必须在白名单范围内
|
|
99
101
|
if (!isPathAllowed(dir, allowedCwds)) {
|
|
100
102
|
logger.warn({ dir, allowedCwds }, 'Scan rejected: directory not allowed');
|
|
101
|
-
return
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// 初始化当前节点
|
|
106
|
+
if (!currentNode) {
|
|
107
|
+
currentNode = result.rootNode;
|
|
102
108
|
}
|
|
103
109
|
try {
|
|
104
110
|
const entries = await readdir(dir, { withFileTypes: true });
|
|
105
111
|
for (const entry of entries) {
|
|
106
112
|
// 达到最大文件数限制时停止扫描
|
|
107
|
-
if (files.length >= MAX_FILES) {
|
|
113
|
+
if (result.files.length >= MAX_FILES) {
|
|
108
114
|
logger.warn({ dir, maxFiles: MAX_FILES }, 'Scan stopped: max file limit reached');
|
|
109
115
|
break;
|
|
110
116
|
}
|
|
@@ -114,27 +120,64 @@ async function scanMarkdownFiles(dir, baseDir, allowedCwds, files = []) {
|
|
|
114
120
|
if (entry.name.startsWith('.') || SKIP_DIRS.includes(entry.name)) {
|
|
115
121
|
continue;
|
|
116
122
|
}
|
|
123
|
+
// 创建子目录节点
|
|
124
|
+
const relativePath = relative(baseDir, fullPath);
|
|
125
|
+
const childNode = buildDirectoryNode(entry.name, fullPath, relativePath.startsWith('.') ? relativePath : `./${relativePath}`);
|
|
126
|
+
currentNode.children.push(childNode);
|
|
117
127
|
// 递归扫描子目录
|
|
118
|
-
await
|
|
128
|
+
await scanFiles(fullPath, baseDir, allowedCwds, result, category, searchQuery, childNode);
|
|
129
|
+
// 汇总子目录统计
|
|
130
|
+
currentNode.fileCount += childNode.fileCount;
|
|
131
|
+
for (const cat of Object.keys(childNode.typeCounts)) {
|
|
132
|
+
currentNode.typeCounts[cat] += childNode.typeCounts[cat];
|
|
133
|
+
}
|
|
119
134
|
}
|
|
120
135
|
else if (entry.isFile()) {
|
|
121
136
|
// 检查扩展名
|
|
122
137
|
const ext = extname(entry.name).toLowerCase();
|
|
123
|
-
if (!
|
|
138
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
124
139
|
continue;
|
|
125
140
|
}
|
|
141
|
+
// 获取文件分类
|
|
142
|
+
const fileCategory = getFileCategory(entry.name);
|
|
143
|
+
// ✅ 先统计 typeCounts(不受 category 影响)
|
|
144
|
+
currentNode.typeCounts[fileCategory]++;
|
|
145
|
+
currentNode.typeCounts.all++;
|
|
146
|
+
// 分类筛选(只影响 files 数组,不影响 typeCounts)
|
|
147
|
+
if (category !== 'all' && fileCategory !== category) {
|
|
148
|
+
continue; // 不加入 files,但 typeCounts 已统计
|
|
149
|
+
}
|
|
126
150
|
try {
|
|
127
151
|
const fileStat = await stat(fullPath);
|
|
128
152
|
const relativePath = relative(baseDir, fullPath);
|
|
129
|
-
|
|
153
|
+
// 搜索筛选(搜索时需要回退 typeCounts,因为搜索结果不应显示被排除的文件)
|
|
154
|
+
if (searchQuery) {
|
|
155
|
+
const searchLower = searchQuery.toLowerCase();
|
|
156
|
+
if (!entry.name.toLowerCase().includes(searchLower) &&
|
|
157
|
+
!relativePath.toLowerCase().includes(searchLower)) {
|
|
158
|
+
// 搜索过滤时回退 typeCounts
|
|
159
|
+
currentNode.typeCounts[fileCategory]--;
|
|
160
|
+
currentNode.typeCounts.all--;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
const fileItem = {
|
|
130
165
|
filename: entry.name,
|
|
131
166
|
absolutePath: fullPath,
|
|
132
167
|
relativePath: relativePath.startsWith('.') ? relativePath : `./${relativePath}`,
|
|
133
168
|
size: fileStat.size,
|
|
134
169
|
modifiedAt: fileStat.mtime.toISOString(),
|
|
135
|
-
|
|
170
|
+
category: fileCategory,
|
|
171
|
+
extension: ext,
|
|
172
|
+
};
|
|
173
|
+
result.files.push(fileItem);
|
|
174
|
+
// 更新当前目录统计(fileCount 统计过滤后的文件数)
|
|
175
|
+
currentNode.fileCount++;
|
|
136
176
|
}
|
|
137
177
|
catch (err) {
|
|
178
|
+
// stat 失败时回退 typeCounts
|
|
179
|
+
currentNode.typeCounts[fileCategory]--;
|
|
180
|
+
currentNode.typeCounts.all--;
|
|
138
181
|
logger.debug({ path: fullPath, err: String(err) }, 'Failed to stat file during scan');
|
|
139
182
|
}
|
|
140
183
|
}
|
|
@@ -143,15 +186,16 @@ async function scanMarkdownFiles(dir, baseDir, allowedCwds, files = []) {
|
|
|
143
186
|
catch (err) {
|
|
144
187
|
logger.warn({ dir, err: String(err) }, 'Failed to read directory during scan');
|
|
145
188
|
}
|
|
146
|
-
return files;
|
|
147
189
|
}
|
|
148
190
|
export function createFileRoutes(authModule, instanceManager) {
|
|
149
191
|
const router = Router();
|
|
150
192
|
/**
|
|
151
|
-
* GET /api/file/list -
|
|
193
|
+
* GET /api/file/list - 列出当前工作目录下的文件
|
|
152
194
|
* Query params:
|
|
153
195
|
* - instanceId: 可选,指定实例 ID,优先使用该实例的 cwd
|
|
154
196
|
* - cwd: 可选,指定扫描目录(默认使用第一个实例的 cwd)
|
|
197
|
+
* - category: 可选,文件分类筛选(markdown|pdf|presentation|image|all)
|
|
198
|
+
* - search: 可选,搜索关键词(匹配文件名和路径)
|
|
155
199
|
*/
|
|
156
200
|
router.get('/file/list', authModule.requireAuth, async (req, res) => {
|
|
157
201
|
try {
|
|
@@ -164,25 +208,40 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
164
208
|
files: [],
|
|
165
209
|
cwd: '',
|
|
166
210
|
total: 0,
|
|
211
|
+
directoryTree: undefined,
|
|
212
|
+
typeCounts: undefined,
|
|
167
213
|
});
|
|
168
214
|
return;
|
|
169
215
|
}
|
|
170
|
-
//
|
|
216
|
+
// 确定扫描目录:
|
|
217
|
+
// - 如果提供了 cwd,使用 cwd(需要验证是否在允许范围内)
|
|
218
|
+
// - 如果只提供了 instanceId,使用实例的 cwd
|
|
219
|
+
// - 否则使用默认第一个允许目录
|
|
171
220
|
const instanceId = req.query.instanceId;
|
|
172
221
|
const queryCwd = req.query.cwd;
|
|
173
222
|
let baseDir;
|
|
223
|
+
// 首先获取实例的 cwd(用于验证 cwd 是否在实例范围内)
|
|
224
|
+
let instanceCwd;
|
|
174
225
|
if (instanceId) {
|
|
175
|
-
// 通过 instanceId 获取实例的 cwd
|
|
176
226
|
const instance = instanceManager.getInstance(instanceId);
|
|
177
227
|
if (!instance) {
|
|
178
228
|
logger.warn({ instanceId }, 'File list rejected: instance not found');
|
|
179
229
|
res.status(404).json({ error: 'Instance not found' });
|
|
180
230
|
return;
|
|
181
231
|
}
|
|
182
|
-
|
|
232
|
+
instanceCwd = instance.cwd;
|
|
183
233
|
}
|
|
184
|
-
|
|
185
|
-
|
|
234
|
+
if (queryCwd) {
|
|
235
|
+
// 使用 cwd 参数,但需要验证路径是否在允许范围内
|
|
236
|
+
const resolvedCwd = normalizePath(queryCwd, instanceCwd || allowedCwds[0]);
|
|
237
|
+
if (!resolvedCwd) {
|
|
238
|
+
res.status(400).json({ error: 'Invalid cwd path format' });
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
baseDir = resolvedCwd;
|
|
242
|
+
}
|
|
243
|
+
else if (instanceCwd) {
|
|
244
|
+
baseDir = instanceCwd;
|
|
186
245
|
}
|
|
187
246
|
else {
|
|
188
247
|
baseDir = allowedCwds[0];
|
|
@@ -195,22 +254,33 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
195
254
|
});
|
|
196
255
|
return;
|
|
197
256
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
257
|
+
// 解析查询参数
|
|
258
|
+
const category = req.query.category || 'all';
|
|
259
|
+
const searchQuery = req.query.search;
|
|
260
|
+
logger.info({ baseDir, category, search: searchQuery }, 'Scanning for files');
|
|
261
|
+
// 初始化扫描结果
|
|
262
|
+
const scanResult = {
|
|
263
|
+
files: [],
|
|
264
|
+
rootNode: buildDirectoryNode(basename(baseDir), baseDir, './'),
|
|
265
|
+
};
|
|
266
|
+
// 递归扫描文件
|
|
267
|
+
await scanFiles(baseDir, baseDir, allowedCwds, scanResult, category, searchQuery);
|
|
201
268
|
// 按修改时间倒序排序
|
|
202
|
-
files.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
|
|
269
|
+
scanResult.files.sort((a, b) => new Date(b.modifiedAt).getTime() - new Date(a.modifiedAt).getTime());
|
|
203
270
|
// 限制返回数量
|
|
204
|
-
const limitedFiles = files.slice(0, 100);
|
|
271
|
+
const limitedFiles = scanResult.files.slice(0, 100);
|
|
205
272
|
logger.info({
|
|
206
|
-
scanned: files.length,
|
|
273
|
+
scanned: scanResult.files.length,
|
|
207
274
|
returned: limitedFiles.length,
|
|
208
275
|
cwd: baseDir,
|
|
209
|
-
|
|
276
|
+
category,
|
|
277
|
+
}, 'File scan completed');
|
|
210
278
|
const response = {
|
|
211
279
|
files: limitedFiles,
|
|
212
280
|
cwd: baseDir,
|
|
213
281
|
total: limitedFiles.length,
|
|
282
|
+
directoryTree: scanResult.rootNode.children.length > 0 ? scanResult.rootNode : undefined,
|
|
283
|
+
typeCounts: scanResult.rootNode.typeCounts,
|
|
214
284
|
};
|
|
215
285
|
res.json(response);
|
|
216
286
|
}
|
|
@@ -220,15 +290,17 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
220
290
|
}
|
|
221
291
|
});
|
|
222
292
|
/**
|
|
223
|
-
* GET /api/file/
|
|
293
|
+
* GET /api/file/content - 读取文件内容(统一接口,支持文本和二进制)
|
|
224
294
|
* Query params:
|
|
225
295
|
* - instanceId: 可选,指定实例 ID,用于解析相对路径
|
|
226
296
|
* - path: 文件路径(绝对路径或相对于 cwd 的相对路径)
|
|
297
|
+
* - asBase64: 可选,是否以 base64 返回(默认为文本,图片/PDF自动转为base64)
|
|
227
298
|
*/
|
|
228
|
-
router.get('/file/
|
|
299
|
+
router.get('/file/content', authModule.requireAuth, async (req, res) => {
|
|
229
300
|
try {
|
|
230
301
|
const rawPath = req.query.path;
|
|
231
302
|
const instanceId = req.query.instanceId;
|
|
303
|
+
const asBase64 = req.query.asBase64 === 'true';
|
|
232
304
|
if (!rawPath) {
|
|
233
305
|
res.status(400).json({ error: 'Missing path parameter' });
|
|
234
306
|
return;
|
|
@@ -237,7 +309,7 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
237
309
|
const allowedCwds = getAllowedCwds(instanceManager);
|
|
238
310
|
// 如果白名单为空,拒绝所有请求
|
|
239
311
|
if (allowedCwds.length === 0) {
|
|
240
|
-
logger.warn({ path: rawPath }, 'File
|
|
312
|
+
logger.warn({ path: rawPath }, 'File content request rejected: no allowed directories');
|
|
241
313
|
res.status(403).json({
|
|
242
314
|
error: 'No allowed directories configured. Start an instance first.',
|
|
243
315
|
});
|
|
@@ -248,7 +320,7 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
248
320
|
if (instanceId) {
|
|
249
321
|
const instance = instanceManager.getInstance(instanceId);
|
|
250
322
|
if (!instance) {
|
|
251
|
-
logger.warn({ instanceId }, 'File
|
|
323
|
+
logger.warn({ instanceId }, 'File content request rejected: instance not found');
|
|
252
324
|
res.status(404).json({ error: 'Instance not found' });
|
|
253
325
|
return;
|
|
254
326
|
}
|
|
@@ -264,17 +336,17 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
264
336
|
}
|
|
265
337
|
// 安全检查:路径必须在白名单范围内
|
|
266
338
|
if (!isPathAllowed(absolutePath, allowedCwds)) {
|
|
267
|
-
logger.warn({ path: absolutePath, allowedCwds }, 'File
|
|
339
|
+
logger.warn({ path: absolutePath, allowedCwds }, 'File content request rejected: path not allowed');
|
|
268
340
|
res.status(403).json({
|
|
269
341
|
error: 'Access denied. File is outside allowed directories.',
|
|
270
342
|
});
|
|
271
343
|
return;
|
|
272
344
|
}
|
|
273
|
-
//
|
|
345
|
+
// 扩展名检查(只允许支持的文件类型)
|
|
274
346
|
const ext = extname(absolutePath).toLowerCase();
|
|
275
|
-
if (!
|
|
347
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext) && !PREVIEWABLE_EXTENSIONS.includes(ext)) {
|
|
276
348
|
res.status(400).json({
|
|
277
|
-
error:
|
|
349
|
+
error: `Unsupported file type. Supported: ${SUPPORTED_EXTENSIONS.join(', ')}`,
|
|
278
350
|
});
|
|
279
351
|
return;
|
|
280
352
|
}
|
|
@@ -300,28 +372,133 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
300
372
|
});
|
|
301
373
|
return;
|
|
302
374
|
}
|
|
375
|
+
const filename = basename(absolutePath);
|
|
376
|
+
const mimeType = FILE_MIME_TYPES[ext] || 'application/octet-stream';
|
|
377
|
+
const isBinary = IMAGE_EXTENSIONS.includes(ext) || ext === '.pdf';
|
|
303
378
|
// 读取文件内容
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
379
|
+
let content;
|
|
380
|
+
let responseIsBase64 = false;
|
|
381
|
+
if (isBinary || asBase64) {
|
|
382
|
+
// 二进制文件或强制 base64:读取并转为 base64
|
|
383
|
+
const buffer = await readFile(absolutePath);
|
|
384
|
+
content = buffer.toString('base64');
|
|
385
|
+
responseIsBase64 = true;
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
// 文本文件:直接读取
|
|
389
|
+
content = await readFile(absolutePath, 'utf-8');
|
|
390
|
+
}
|
|
391
|
+
logger.info({ path: absolutePath, size: fileStat.size, mimeType }, 'File content served');
|
|
307
392
|
const response = {
|
|
308
393
|
content,
|
|
309
394
|
path: absolutePath,
|
|
310
395
|
filename,
|
|
311
396
|
size: fileStat.size,
|
|
397
|
+
isBase64: responseIsBase64,
|
|
398
|
+
mimeType,
|
|
312
399
|
};
|
|
313
400
|
res.json(response);
|
|
314
401
|
}
|
|
402
|
+
catch (err) {
|
|
403
|
+
logger.error({ err }, 'Failed to read file content');
|
|
404
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
/**
|
|
408
|
+
* GET /api/file/read - 读取 Markdown 文件内容(向后兼容)
|
|
409
|
+
* 保留原有接口行为,仅支持 Markdown 文件
|
|
410
|
+
*/
|
|
411
|
+
router.get('/file/read', authModule.requireAuth, async (req, res) => {
|
|
412
|
+
try {
|
|
413
|
+
const rawPath = req.query.path;
|
|
414
|
+
const instanceId = req.query.instanceId;
|
|
415
|
+
if (!rawPath) {
|
|
416
|
+
res.status(400).json({ error: 'Missing path parameter' });
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
// 获取允许的目录白名单
|
|
420
|
+
const allowedCwds = getAllowedCwds(instanceManager);
|
|
421
|
+
if (allowedCwds.length === 0) {
|
|
422
|
+
logger.warn({ path: rawPath }, 'File read rejected: no allowed directories');
|
|
423
|
+
res.status(403).json({
|
|
424
|
+
error: 'No allowed directories configured. Start an instance first.',
|
|
425
|
+
});
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
// 确定基准目录
|
|
429
|
+
let baseDir;
|
|
430
|
+
if (instanceId) {
|
|
431
|
+
const instance = instanceManager.getInstance(instanceId);
|
|
432
|
+
if (!instance) {
|
|
433
|
+
logger.warn({ instanceId }, 'File read rejected: instance not found');
|
|
434
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
baseDir = instance.cwd;
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
baseDir = allowedCwds[0];
|
|
441
|
+
}
|
|
442
|
+
const absolutePath = normalizePath(rawPath, baseDir);
|
|
443
|
+
if (!absolutePath) {
|
|
444
|
+
res.status(400).json({ error: 'Invalid path format' });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
// 安全检查
|
|
448
|
+
if (!isPathAllowed(absolutePath, allowedCwds)) {
|
|
449
|
+
logger.warn({ path: absolutePath, allowedCwds }, 'File read rejected: path not allowed');
|
|
450
|
+
res.status(403).json({
|
|
451
|
+
error: 'Access denied. File is outside allowed directories.',
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// 仅允许 Markdown 文件(向后兼容)
|
|
456
|
+
const ext = extname(absolutePath).toLowerCase();
|
|
457
|
+
if (!['.md', '.markdown'].includes(ext)) {
|
|
458
|
+
res.status(400).json({
|
|
459
|
+
error: 'Only Markdown files (.md, .markdown) are allowed',
|
|
460
|
+
});
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
// 检查文件
|
|
464
|
+
let fileStat;
|
|
465
|
+
try {
|
|
466
|
+
fileStat = await stat(absolutePath);
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
logger.debug({ path: absolutePath }, 'File not found');
|
|
470
|
+
res.status(404).json({ error: 'File not found' });
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
if (!fileStat.isFile()) {
|
|
474
|
+
res.status(400).json({ error: 'Path is not a file' });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (fileStat.size > MAX_FILE_SIZE) {
|
|
478
|
+
res.status(413).json({
|
|
479
|
+
error: `File too large. Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB`,
|
|
480
|
+
});
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
// 读取文件内容
|
|
484
|
+
const content = await readFile(absolutePath, 'utf-8');
|
|
485
|
+
const filename = basename(absolutePath);
|
|
486
|
+
logger.info({ path: absolutePath, size: fileStat.size }, 'Markdown file read successfully');
|
|
487
|
+
res.json({
|
|
488
|
+
content,
|
|
489
|
+
path: absolutePath,
|
|
490
|
+
filename,
|
|
491
|
+
size: fileStat.size,
|
|
492
|
+
});
|
|
493
|
+
}
|
|
315
494
|
catch (err) {
|
|
316
495
|
logger.error({ err }, 'Failed to read file');
|
|
317
496
|
res.status(500).json({ error: 'Internal server error' });
|
|
318
497
|
}
|
|
319
498
|
});
|
|
320
499
|
/**
|
|
321
|
-
* GET /api/file/asset -
|
|
322
|
-
*
|
|
323
|
-
* - instanceId: 可选,指定实例 ID,用于解析相对路径
|
|
324
|
-
* - path: 文件路径(绝对路径或相对于 cwd 的相对路径)
|
|
500
|
+
* GET /api/file/asset - 获取图片等静态资源文件(向后兼容)
|
|
501
|
+
* 支持流式传输,保留原有行为
|
|
325
502
|
*/
|
|
326
503
|
router.get('/file/asset', authModule.requireAuth, async (req, res) => {
|
|
327
504
|
try {
|
|
@@ -333,7 +510,6 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
333
510
|
}
|
|
334
511
|
// 获取允许的目录白名单
|
|
335
512
|
const allowedCwds = getAllowedCwds(instanceManager);
|
|
336
|
-
// 如果白名单为空,拒绝所有请求
|
|
337
513
|
if (allowedCwds.length === 0) {
|
|
338
514
|
logger.warn({ path: rawPath }, 'Asset request rejected: no allowed directories');
|
|
339
515
|
res.status(403).json({
|
|
@@ -341,7 +517,7 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
341
517
|
});
|
|
342
518
|
return;
|
|
343
519
|
}
|
|
344
|
-
//
|
|
520
|
+
// 确定基准目录
|
|
345
521
|
let baseDir;
|
|
346
522
|
if (instanceId) {
|
|
347
523
|
const instance = instanceManager.getInstance(instanceId);
|
|
@@ -360,7 +536,7 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
360
536
|
res.status(400).json({ error: 'Invalid path format' });
|
|
361
537
|
return;
|
|
362
538
|
}
|
|
363
|
-
//
|
|
539
|
+
// 安全检查
|
|
364
540
|
if (!isPathAllowed(absolutePath, allowedCwds)) {
|
|
365
541
|
logger.warn({ path: absolutePath, allowedCwds }, 'Asset request rejected: path not allowed');
|
|
366
542
|
res.status(403).json({
|
|
@@ -370,13 +546,13 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
370
546
|
}
|
|
371
547
|
// 扩展名检查(仅允许图片格式)
|
|
372
548
|
const ext = extname(absolutePath).toLowerCase();
|
|
373
|
-
if (!
|
|
549
|
+
if (!IMAGE_EXTENSIONS.includes(ext)) {
|
|
374
550
|
res.status(400).json({
|
|
375
|
-
error: `Only image files (${
|
|
551
|
+
error: `Only image files (${IMAGE_EXTENSIONS.join(', ')}) are allowed`,
|
|
376
552
|
});
|
|
377
553
|
return;
|
|
378
554
|
}
|
|
379
|
-
//
|
|
555
|
+
// 检查文件
|
|
380
556
|
let fileStat;
|
|
381
557
|
try {
|
|
382
558
|
fileStat = await stat(absolutePath);
|
|
@@ -386,12 +562,10 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
386
562
|
res.status(404).json({ error: 'File not found' });
|
|
387
563
|
return;
|
|
388
564
|
}
|
|
389
|
-
// 检查是否为文件
|
|
390
565
|
if (!fileStat.isFile()) {
|
|
391
566
|
res.status(400).json({ error: 'Path is not a file' });
|
|
392
567
|
return;
|
|
393
568
|
}
|
|
394
|
-
// 文件大小检查
|
|
395
569
|
if (fileStat.size > MAX_FILE_SIZE) {
|
|
396
570
|
res.status(413).json({
|
|
397
571
|
error: `File too large. Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB`,
|
|
@@ -399,7 +573,7 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
399
573
|
return;
|
|
400
574
|
}
|
|
401
575
|
// 设置 Content-Type
|
|
402
|
-
const contentType =
|
|
576
|
+
const contentType = FILE_MIME_TYPES[ext] || 'application/octet-stream';
|
|
403
577
|
res.setHeader('Content-Type', contentType);
|
|
404
578
|
res.setHeader('Content-Length', fileStat.size);
|
|
405
579
|
res.setHeader('Cache-Control', 'public, max-age=31536000');
|
|
@@ -419,6 +593,228 @@ export function createFileRoutes(authModule, instanceManager) {
|
|
|
419
593
|
res.status(500).json({ error: 'Internal server error' });
|
|
420
594
|
}
|
|
421
595
|
});
|
|
596
|
+
/**
|
|
597
|
+
* GET /api/file/download - 下载文件(支持所有受支持的文件类型)
|
|
598
|
+
* 设置 Content-Disposition: attachment 触发浏览器原生下载
|
|
599
|
+
*/
|
|
600
|
+
router.get('/file/download', authModule.requireAuth, async (req, res) => {
|
|
601
|
+
try {
|
|
602
|
+
const rawPath = req.query.path;
|
|
603
|
+
const instanceId = req.query.instanceId;
|
|
604
|
+
if (!rawPath) {
|
|
605
|
+
res.status(400).json({ error: 'Missing path parameter' });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
const allowedCwds = getAllowedCwds(instanceManager);
|
|
609
|
+
if (allowedCwds.length === 0) {
|
|
610
|
+
logger.warn({ path: rawPath }, 'Download request rejected: no allowed directories');
|
|
611
|
+
res.status(403).json({
|
|
612
|
+
error: 'No allowed directories configured. Start an instance first.',
|
|
613
|
+
});
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
let baseDir;
|
|
617
|
+
if (instanceId) {
|
|
618
|
+
const instance = instanceManager.getInstance(instanceId);
|
|
619
|
+
if (!instance) {
|
|
620
|
+
logger.warn({ instanceId }, 'Download request rejected: instance not found');
|
|
621
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
baseDir = instance.cwd;
|
|
625
|
+
}
|
|
626
|
+
else {
|
|
627
|
+
baseDir = allowedCwds[0];
|
|
628
|
+
}
|
|
629
|
+
const absolutePath = normalizePath(rawPath, baseDir);
|
|
630
|
+
if (!absolutePath) {
|
|
631
|
+
res.status(400).json({ error: 'Invalid path format' });
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
if (!isPathAllowed(absolutePath, allowedCwds)) {
|
|
635
|
+
logger.warn({ path: absolutePath, allowedCwds }, 'Download request rejected: path not allowed');
|
|
636
|
+
res.status(403).json({
|
|
637
|
+
error: 'Access denied. File is outside allowed directories.',
|
|
638
|
+
});
|
|
639
|
+
return;
|
|
640
|
+
}
|
|
641
|
+
// 扩展名检查(允许所有受支持的文件类型)
|
|
642
|
+
const ext = extname(absolutePath).toLowerCase();
|
|
643
|
+
if (!SUPPORTED_EXTENSIONS.includes(ext)) {
|
|
644
|
+
res.status(400).json({
|
|
645
|
+
error: `Unsupported file type. Supported: ${SUPPORTED_EXTENSIONS.join(', ')}`,
|
|
646
|
+
});
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
let fileStat;
|
|
650
|
+
try {
|
|
651
|
+
fileStat = await stat(absolutePath);
|
|
652
|
+
}
|
|
653
|
+
catch {
|
|
654
|
+
logger.debug({ path: absolutePath }, 'Download file not found');
|
|
655
|
+
res.status(404).json({ error: 'File not found' });
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
if (!fileStat.isFile()) {
|
|
659
|
+
res.status(400).json({ error: 'Path is not a file' });
|
|
660
|
+
return;
|
|
661
|
+
}
|
|
662
|
+
if (fileStat.size > MAX_FILE_SIZE) {
|
|
663
|
+
res.status(413).json({
|
|
664
|
+
error: `File too large. Maximum size is ${MAX_FILE_SIZE / 1024 / 1024}MB`,
|
|
665
|
+
});
|
|
666
|
+
return;
|
|
667
|
+
}
|
|
668
|
+
const filename = basename(absolutePath);
|
|
669
|
+
const contentType = FILE_MIME_TYPES[ext] || 'application/octet-stream';
|
|
670
|
+
const isTextFile = contentType.startsWith('text/');
|
|
671
|
+
// 文本类型显式声明 charset=utf-8,避免浏览器按系统默认编码解析导致中文乱码
|
|
672
|
+
res.setHeader('Content-Type', isTextFile ? `${contentType}; charset=utf-8` : contentType);
|
|
673
|
+
// 文本文件添加 UTF-8 BOM (3 bytes),帮助移动端应用正确检测编码
|
|
674
|
+
const UTF8_BOM = Buffer.from([0xEF, 0xBB, 0xBF]);
|
|
675
|
+
res.setHeader('Content-Length', isTextFile ? fileStat.size + UTF8_BOM.length : fileStat.size);
|
|
676
|
+
// RFC 5987 编码文件名,支持中文等非 ASCII 字符
|
|
677
|
+
const encodedFilename = encodeURIComponent(filename).replace(/'/g, '%27');
|
|
678
|
+
// ASCII fallback for filename, UTF-8 encoded for filename*
|
|
679
|
+
const asciiFilename = filename.replace(/[^\x20-\x7E]/g, '_');
|
|
680
|
+
res.setHeader('Content-Disposition', `attachment; filename="${asciiFilename}"; filename*=UTF-8''${encodedFilename}`);
|
|
681
|
+
// 文本文件先写入 BOM,再 pipe 文件流
|
|
682
|
+
if (isTextFile) {
|
|
683
|
+
res.write(UTF8_BOM);
|
|
684
|
+
}
|
|
685
|
+
const fileStream = createReadStream(absolutePath);
|
|
686
|
+
fileStream.pipe(res);
|
|
687
|
+
fileStream.on('error', (err) => {
|
|
688
|
+
logger.error({ path: absolutePath, err: String(err) }, 'Failed to stream download file');
|
|
689
|
+
if (!res.headersSent) {
|
|
690
|
+
res.status(500).json({ error: 'Failed to read file' });
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
logger.info({ path: absolutePath, size: fileStat.size, filename }, 'File download served');
|
|
694
|
+
}
|
|
695
|
+
catch (err) {
|
|
696
|
+
logger.error({ err }, 'Failed to serve download');
|
|
697
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
/**
|
|
701
|
+
* POST /api/file/convert - 创建 PPTX 转换任务
|
|
702
|
+
* Body: { path: string, instanceId?: string }
|
|
703
|
+
*/
|
|
704
|
+
router.post('/file/convert', authModule.requireAuth, async (req, res) => {
|
|
705
|
+
try {
|
|
706
|
+
const rawPath = req.body.path;
|
|
707
|
+
const instanceId = req.body.instanceId;
|
|
708
|
+
if (!rawPath) {
|
|
709
|
+
res.status(400).json({ error: 'Missing path parameter' });
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
// 检查文件扩展名
|
|
713
|
+
const ext = extname(rawPath).toLowerCase();
|
|
714
|
+
if (!CONVERTIBLE_TO_PDF.includes(ext)) {
|
|
715
|
+
res.status(400).json({
|
|
716
|
+
error: 'Only PPTX files can be converted',
|
|
717
|
+
supported: CONVERTIBLE_TO_PDF,
|
|
718
|
+
});
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
721
|
+
// 获取允许的目录白名单
|
|
722
|
+
const allowedCwds = getAllowedCwds(instanceManager);
|
|
723
|
+
if (allowedCwds.length === 0) {
|
|
724
|
+
res.status(403).json({ error: 'No allowed directories configured' });
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
// 确定基准目录
|
|
728
|
+
let baseDir;
|
|
729
|
+
if (instanceId) {
|
|
730
|
+
const instance = instanceManager.getInstance(instanceId);
|
|
731
|
+
if (!instance) {
|
|
732
|
+
res.status(404).json({ error: 'Instance not found' });
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
baseDir = instance.cwd;
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
baseDir = allowedCwds[0];
|
|
739
|
+
}
|
|
740
|
+
const absolutePath = normalizePath(rawPath, baseDir);
|
|
741
|
+
if (!absolutePath) {
|
|
742
|
+
res.status(400).json({ error: 'Invalid path format' });
|
|
743
|
+
return;
|
|
744
|
+
}
|
|
745
|
+
// 安全检查
|
|
746
|
+
if (!isPathAllowed(absolutePath, allowedCwds)) {
|
|
747
|
+
res.status(403).json({ error: 'Access denied' });
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
// 检查文件是否存在
|
|
751
|
+
try {
|
|
752
|
+
await stat(absolutePath);
|
|
753
|
+
}
|
|
754
|
+
catch {
|
|
755
|
+
res.status(404).json({ error: 'File not found' });
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
// 创建转换任务
|
|
759
|
+
const task = await createConversionTask(absolutePath);
|
|
760
|
+
// 异步启动转换
|
|
761
|
+
startConversion(task.id);
|
|
762
|
+
logger.info({ taskId: task.id, path: absolutePath }, 'Created conversion task');
|
|
763
|
+
res.json({
|
|
764
|
+
taskId: task.id,
|
|
765
|
+
status: task.status,
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
catch (err) {
|
|
769
|
+
logger.error({ err }, 'Failed to create conversion task');
|
|
770
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
/**
|
|
774
|
+
* GET /api/file/convert/status - 查询转换任务状态
|
|
775
|
+
* Query: { taskId: string }
|
|
776
|
+
*/
|
|
777
|
+
router.get('/file/convert/status', authModule.requireAuth, async (req, res) => {
|
|
778
|
+
try {
|
|
779
|
+
const taskId = req.query.taskId;
|
|
780
|
+
if (!taskId) {
|
|
781
|
+
res.status(400).json({ error: 'Missing taskId parameter' });
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const task = getTaskStatus(taskId);
|
|
785
|
+
if (!task) {
|
|
786
|
+
res.status(404).json({ error: 'Task not found' });
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
const response = {
|
|
790
|
+
taskId: task.id,
|
|
791
|
+
status: task.status,
|
|
792
|
+
pdfPath: task.status === 'completed' ? task.outputPath : undefined,
|
|
793
|
+
error: task.error,
|
|
794
|
+
};
|
|
795
|
+
res.json(response);
|
|
796
|
+
}
|
|
797
|
+
catch (err) {
|
|
798
|
+
logger.error({ err }, 'Failed to get conversion status');
|
|
799
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
/**
|
|
803
|
+
* GET /api/file/convert/check - 检查 LibreOffice 是否可用
|
|
804
|
+
*/
|
|
805
|
+
router.get('/file/convert/check', authModule.requireAuth, async (req, res) => {
|
|
806
|
+
try {
|
|
807
|
+
const isAvailable = await isSofficeAvailable();
|
|
808
|
+
res.json({
|
|
809
|
+
available: isAvailable,
|
|
810
|
+
installGuide: !isAvailable ? getLibreOfficeInstallGuide() : undefined,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
catch (err) {
|
|
814
|
+
logger.error({ err }, 'Failed to check LibreOffice availability');
|
|
815
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
816
|
+
}
|
|
817
|
+
});
|
|
422
818
|
return router;
|
|
423
819
|
}
|
|
424
820
|
//# sourceMappingURL=file-routes.js.map
|