@co0ontty/wand 1.15.1 → 1.17.4
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/claude-pty-bridge.d.ts +3 -0
- package/dist/claude-pty-bridge.js +35 -1
- package/dist/config.js +2 -0
- package/dist/models.d.ts +13 -0
- package/dist/models.js +54 -0
- package/dist/process-manager.d.ts +7 -0
- package/dist/process-manager.js +47 -15
- package/dist/pty-text-utils.d.ts +7 -0
- package/dist/pty-text-utils.js +14 -0
- package/dist/pwa.js +2 -4
- package/dist/server-session-routes.js +25 -1
- package/dist/server.js +113 -15
- package/dist/structured-session-manager.d.ts +4 -0
- package/dist/structured-session-manager.js +30 -1
- package/dist/types.d.ts +16 -0
- package/dist/upload-routes.d.ts +3 -0
- package/dist/upload-routes.js +53 -0
- package/dist/web-ui/content/scripts.js +950 -517
- package/dist/web-ui/content/styles.css +312 -118
- package/dist/web-ui/content/vendor/wterm/terminal.css +162 -0
- package/dist/web-ui/content/vendor/wterm/wterm.bundle.js +1 -0
- package/dist/web-ui/index.js +2 -4
- package/dist/ws-broadcast.js +12 -7
- package/package.json +6 -5
package/dist/server.js
CHANGED
|
@@ -14,10 +14,12 @@ import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
|
|
|
14
14
|
import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
|
|
15
15
|
import { ensureCertificates } from "./cert.js";
|
|
16
16
|
import { isExecutionMode, normalizeCardDefaults, resolveConfigDir, saveConfig } from "./config.js";
|
|
17
|
+
import { getCachedModels, refreshModels } from "./models.js";
|
|
17
18
|
import { ProcessManager } from "./process-manager.js";
|
|
18
19
|
import { StructuredSessionManager } from "./structured-session-manager.js";
|
|
19
20
|
import { generatePwaManifest, generateServiceWorker } from "./pwa.js";
|
|
20
21
|
import { getErrorMessage, registerClaudeHistoryRoutes, registerSessionRoutes } from "./server-session-routes.js";
|
|
22
|
+
import { registerUploadRoutes } from "./upload-routes.js";
|
|
21
23
|
import { resolveDatabasePath, WandStorage } from "./storage.js";
|
|
22
24
|
import { renderApp } from "./web-ui/index.js";
|
|
23
25
|
import { WsBroadcastManager } from "./ws-broadcast.js";
|
|
@@ -488,9 +490,10 @@ export async function startServer(config, configPath) {
|
|
|
488
490
|
app.use(express.json({ limit: "1mb" }));
|
|
489
491
|
app.use(compression({ threshold: 1024 }));
|
|
490
492
|
const vendorCacheOpts = { maxAge: "7d", immutable: true };
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
493
|
+
const contentDir = existsSync(path.join(SERVER_MODULE_DIR, "web-ui", "content"))
|
|
494
|
+
? path.join(SERVER_MODULE_DIR, "web-ui", "content")
|
|
495
|
+
: path.join(RUNTIME_ROOT_DIR, "src", "web-ui", "content");
|
|
496
|
+
app.use("/vendor/wterm", express.static(path.join(contentDir, "vendor", "wterm"), vendorCacheOpts));
|
|
494
497
|
// ── Web UI and PWA endpoints ──
|
|
495
498
|
app.get("/", (_req, res) => {
|
|
496
499
|
res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
@@ -694,6 +697,10 @@ export async function startServer(config, configPath) {
|
|
|
694
697
|
hasCert: existsSync(certPaths.keyPath) && existsSync(certPaths.certPath),
|
|
695
698
|
updateAvailable: cachedUpdateInfo?.updateAvailable ?? false,
|
|
696
699
|
latestVersion: cachedUpdateInfo?.latest ?? null,
|
|
700
|
+
autoUpdate: {
|
|
701
|
+
web: storage.getConfigValue("autoUpdateWeb") === "true",
|
|
702
|
+
apk: storage.getConfigValue("autoUpdateApk") === "true",
|
|
703
|
+
},
|
|
697
704
|
androidApk: {
|
|
698
705
|
enabled: config.android?.enabled === true,
|
|
699
706
|
apkDir,
|
|
@@ -745,7 +752,7 @@ export async function startServer(config, configPath) {
|
|
|
745
752
|
});
|
|
746
753
|
app.post("/api/settings/config", async (req, res) => {
|
|
747
754
|
const body = req.body;
|
|
748
|
-
const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "language"];
|
|
755
|
+
const allowedFields = ["host", "port", "https", "defaultMode", "defaultCwd", "shell", "language", "defaultModel"];
|
|
749
756
|
let changed = false;
|
|
750
757
|
for (const field of allowedFields) {
|
|
751
758
|
if (field in body && body[field] !== undefined) {
|
|
@@ -779,6 +786,9 @@ export async function startServer(config, configPath) {
|
|
|
779
786
|
else if (field === "language") {
|
|
780
787
|
config.language = typeof body.language === "string" ? body.language.trim() : "";
|
|
781
788
|
}
|
|
789
|
+
else if (field === "defaultModel") {
|
|
790
|
+
config.defaultModel = typeof body.defaultModel === "string" ? body.defaultModel.trim() : "";
|
|
791
|
+
}
|
|
782
792
|
changed = true;
|
|
783
793
|
}
|
|
784
794
|
}
|
|
@@ -802,6 +812,29 @@ export async function startServer(config, configPath) {
|
|
|
802
812
|
res.status(500).json({ error: getErrorMessage(error, "保存配置失败。") });
|
|
803
813
|
}
|
|
804
814
|
});
|
|
815
|
+
app.get("/api/models", (_req, res) => {
|
|
816
|
+
const cached = getCachedModels();
|
|
817
|
+
res.json({
|
|
818
|
+
models: cached.models,
|
|
819
|
+
claudeVersion: cached.claudeVersion,
|
|
820
|
+
refreshedAt: cached.refreshedAt,
|
|
821
|
+
defaultModel: config.defaultModel ?? "",
|
|
822
|
+
});
|
|
823
|
+
});
|
|
824
|
+
app.post("/api/models/refresh", async (_req, res) => {
|
|
825
|
+
try {
|
|
826
|
+
const refreshed = await refreshModels();
|
|
827
|
+
res.json({
|
|
828
|
+
models: refreshed.models,
|
|
829
|
+
claudeVersion: refreshed.claudeVersion,
|
|
830
|
+
refreshedAt: refreshed.refreshedAt,
|
|
831
|
+
defaultModel: config.defaultModel ?? "",
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
catch (error) {
|
|
835
|
+
res.status(500).json({ error: getErrorMessage(error, "刷新模型列表失败。") });
|
|
836
|
+
}
|
|
837
|
+
});
|
|
805
838
|
app.post("/api/settings/upload-cert", async (req, res) => {
|
|
806
839
|
const { key, cert } = req.body;
|
|
807
840
|
if (!key || !cert) {
|
|
@@ -857,6 +890,7 @@ export async function startServer(config, configPath) {
|
|
|
857
890
|
});
|
|
858
891
|
registerSessionRoutes(app, processes, structuredSessions, storage, config.defaultMode);
|
|
859
892
|
registerClaudeHistoryRoutes(app, processes, storage);
|
|
893
|
+
registerUploadRoutes(app, processes);
|
|
860
894
|
// ── Path suggestion ──
|
|
861
895
|
app.get("/api/path-suggestions", async (req, res) => {
|
|
862
896
|
const query = typeof req.query.q === "string" ? req.query.q : "";
|
|
@@ -872,10 +906,9 @@ export async function startServer(config, configPath) {
|
|
|
872
906
|
app.get("/api/directory", async (req, res) => {
|
|
873
907
|
const q = typeof req.query.q === "string" ? req.query.q : "";
|
|
874
908
|
const includeGitStatus = req.query.gitStatus === "true";
|
|
875
|
-
const targetPath = path.resolve(
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
res.status(403).json({ error: "访问被拒绝:路径必须在项目目录内。" });
|
|
909
|
+
const targetPath = path.resolve(q || config.defaultCwd);
|
|
910
|
+
if (isBlockedFolderPath(targetPath)) {
|
|
911
|
+
res.status(403).json({ error: "访问被拒绝:无法访问系统敏感目录。" });
|
|
879
912
|
return;
|
|
880
913
|
}
|
|
881
914
|
try {
|
|
@@ -911,8 +944,7 @@ export async function startServer(config, configPath) {
|
|
|
911
944
|
return;
|
|
912
945
|
}
|
|
913
946
|
const resolvedPath = path.resolve(filePath);
|
|
914
|
-
|
|
915
|
-
if (!isPathWithinBase(resolvedPath, allowedBase)) {
|
|
947
|
+
if (isBlockedFolderPath(resolvedPath)) {
|
|
916
948
|
res.status(403).json({ error: "Access denied" });
|
|
917
949
|
return;
|
|
918
950
|
}
|
|
@@ -1128,9 +1160,12 @@ export async function startServer(config, configPath) {
|
|
|
1128
1160
|
}
|
|
1129
1161
|
const initialInput = body.initialInput?.trim();
|
|
1130
1162
|
try {
|
|
1163
|
+
const rawModel = typeof body.model === "string" ? body.model.trim() : "";
|
|
1164
|
+
const effectiveModel = rawModel || (config.defaultModel ?? "").trim() || undefined;
|
|
1131
1165
|
const snapshot = processes.start(body.command, body.cwd, normalizeMode(body.mode, config.defaultMode), initialInput || undefined, {
|
|
1132
1166
|
worktreeEnabled: body.worktreeEnabled === true,
|
|
1133
1167
|
provider: body.provider,
|
|
1168
|
+
model: effectiveModel,
|
|
1134
1169
|
});
|
|
1135
1170
|
res.status(201).json(snapshot);
|
|
1136
1171
|
}
|
|
@@ -1207,17 +1242,80 @@ export async function startServer(config, configPath) {
|
|
|
1207
1242
|
}
|
|
1208
1243
|
// Start configured background sessions after the server is already reachable.
|
|
1209
1244
|
processes.runStartupCommands();
|
|
1210
|
-
//
|
|
1211
|
-
|
|
1245
|
+
// ── Auto-update endpoints ──
|
|
1246
|
+
app.get("/api/auto-update", (_req, res) => {
|
|
1247
|
+
const web = storage.getConfigValue("autoUpdateWeb") === "true";
|
|
1248
|
+
const apk = storage.getConfigValue("autoUpdateApk") === "true";
|
|
1249
|
+
res.json({ web, apk });
|
|
1250
|
+
});
|
|
1251
|
+
app.post("/api/auto-update", express.json(), (req, res) => {
|
|
1252
|
+
const { web, apk } = req.body;
|
|
1253
|
+
if (typeof web === "boolean") {
|
|
1254
|
+
storage.setConfigValue("autoUpdateWeb", String(web));
|
|
1255
|
+
}
|
|
1256
|
+
if (typeof apk === "boolean") {
|
|
1257
|
+
storage.setConfigValue("autoUpdateApk", String(apk));
|
|
1258
|
+
}
|
|
1259
|
+
res.json({
|
|
1260
|
+
web: storage.getConfigValue("autoUpdateWeb") === "true",
|
|
1261
|
+
apk: storage.getConfigValue("autoUpdateApk") === "true",
|
|
1262
|
+
});
|
|
1263
|
+
});
|
|
1264
|
+
// ── Auto-update logic ──
|
|
1265
|
+
async function performAutoUpdate() {
|
|
1266
|
+
const info = await checkNpmLatestVersion(true);
|
|
1212
1267
|
cachedUpdateInfo = info;
|
|
1213
|
-
if (info.updateAvailable)
|
|
1268
|
+
if (!info.updateAvailable)
|
|
1269
|
+
return;
|
|
1270
|
+
const autoEnabled = storage.getConfigValue("autoUpdateWeb") === "true";
|
|
1271
|
+
if (!autoEnabled) {
|
|
1272
|
+
// Not auto-updating, just notify
|
|
1214
1273
|
process.stdout.write(`[wand] 发现新版本 ${info.latest}(当前 ${info.current})。运行 npm install -g ${PKG_NAME}@latest 进行更新。\n`);
|
|
1215
|
-
// Broadcast update notification to all connected WS clients
|
|
1216
1274
|
wsManager.emitEvent({
|
|
1217
1275
|
type: "notification",
|
|
1218
1276
|
sessionId: "__system__",
|
|
1219
1277
|
data: { kind: "update", current: info.current, latest: info.latest },
|
|
1220
1278
|
});
|
|
1279
|
+
return;
|
|
1280
|
+
}
|
|
1281
|
+
// Auto-update: install and restart
|
|
1282
|
+
process.stdout.write(`[wand] 自动更新:正在从 ${info.current} 更新到 ${info.latest}...\n`);
|
|
1283
|
+
wsManager.emitEvent({
|
|
1284
|
+
type: "notification",
|
|
1285
|
+
sessionId: "__system__",
|
|
1286
|
+
data: { kind: "auto-update-start", current: info.current, latest: info.latest },
|
|
1287
|
+
});
|
|
1288
|
+
try {
|
|
1289
|
+
await execAsync(`npm install -g ${PKG_NAME}@latest`, { timeout: 120000 });
|
|
1290
|
+
process.stdout.write(`[wand] 自动更新完成,正在重启...\n`);
|
|
1291
|
+
wsManager.emitEvent({
|
|
1292
|
+
type: "notification",
|
|
1293
|
+
sessionId: "__system__",
|
|
1294
|
+
data: { kind: "auto-update-restart", current: info.current, latest: info.latest },
|
|
1295
|
+
});
|
|
1296
|
+
// Restart after a brief delay
|
|
1297
|
+
setTimeout(() => {
|
|
1298
|
+
wss.clients.forEach((client) => client.close());
|
|
1299
|
+
server.close(() => {
|
|
1300
|
+
spawn(process.execPath, process.argv.slice(1), {
|
|
1301
|
+
detached: true,
|
|
1302
|
+
stdio: "inherit",
|
|
1303
|
+
cwd: process.cwd(),
|
|
1304
|
+
env: process.env,
|
|
1305
|
+
}).unref();
|
|
1306
|
+
process.exit(0);
|
|
1307
|
+
});
|
|
1308
|
+
setTimeout(() => process.exit(0), 5000);
|
|
1309
|
+
}, 1000);
|
|
1310
|
+
}
|
|
1311
|
+
catch (error) {
|
|
1312
|
+
process.stdout.write(`[wand] 自动更新失败: ${getErrorMessage(error, "未知错误")}\n`);
|
|
1221
1313
|
}
|
|
1222
|
-
}
|
|
1314
|
+
}
|
|
1315
|
+
// Background update check on startup
|
|
1316
|
+
performAutoUpdate().catch(() => { });
|
|
1317
|
+
// Periodic update check (every 30 minutes)
|
|
1318
|
+
setInterval(() => {
|
|
1319
|
+
performAutoUpdate().catch(() => { });
|
|
1320
|
+
}, 30 * 60 * 1000);
|
|
1223
1321
|
}
|
|
@@ -6,6 +6,8 @@ interface CreateStructuredSessionOptions {
|
|
|
6
6
|
prompt?: string;
|
|
7
7
|
runner?: SessionRunner;
|
|
8
8
|
worktreeEnabled?: boolean;
|
|
9
|
+
/** 用户指定的 Claude 模型(别名或完整 ID)。留空则 spawn 时不加 --model。 */
|
|
10
|
+
model?: string;
|
|
9
11
|
}
|
|
10
12
|
export declare class StructuredSessionManager {
|
|
11
13
|
private readonly storage;
|
|
@@ -25,6 +27,8 @@ export declare class StructuredSessionManager {
|
|
|
25
27
|
approvePermission(sessionId: string): SessionSnapshot;
|
|
26
28
|
/** Deny a pending permission request. */
|
|
27
29
|
denyPermission(sessionId: string): SessionSnapshot;
|
|
30
|
+
/** Update the selected model for a structured session. Takes effect on the next spawn. */
|
|
31
|
+
setSessionModel(sessionId: string, model: string | null): SessionSnapshot;
|
|
28
32
|
/** Toggle auto-approve for the session. */
|
|
29
33
|
toggleAutoApprove(sessionId: string): SessionSnapshot;
|
|
30
34
|
/** Resolve a specific escalation by requestId. */
|
|
@@ -73,11 +73,12 @@ export class StructuredSessionManager {
|
|
|
73
73
|
structuredState: {
|
|
74
74
|
provider: snapshot.structuredState?.provider ?? snapshot.provider ?? "claude",
|
|
75
75
|
runner: snapshot.runner ?? "claude-cli-print",
|
|
76
|
-
model: snapshot.structuredState?.model,
|
|
76
|
+
model: snapshot.structuredState?.model ?? snapshot.selectedModel ?? undefined,
|
|
77
77
|
lastError: snapshot.structuredState?.lastError ?? null,
|
|
78
78
|
inFlight: false,
|
|
79
79
|
activeRequestId: null,
|
|
80
80
|
},
|
|
81
|
+
selectedModel: snapshot.selectedModel ?? null,
|
|
81
82
|
};
|
|
82
83
|
this.sessions.set(restored.id, restored);
|
|
83
84
|
this.storage.saveSession(restored);
|
|
@@ -112,6 +113,7 @@ export class StructuredSessionManager {
|
|
|
112
113
|
const worktreeSetup = options.worktreeEnabled
|
|
113
114
|
? prepareSessionWorktree({ cwd: options.cwd, sessionId: id })
|
|
114
115
|
: null;
|
|
116
|
+
const selectedModel = options.model?.trim() || null;
|
|
115
117
|
const snapshot = {
|
|
116
118
|
id,
|
|
117
119
|
sessionKind: "structured",
|
|
@@ -135,6 +137,7 @@ export class StructuredSessionManager {
|
|
|
135
137
|
structuredState: {
|
|
136
138
|
provider: "claude",
|
|
137
139
|
runner: options.runner ?? "claude-cli-print",
|
|
140
|
+
model: selectedModel ?? undefined,
|
|
138
141
|
inFlight: false,
|
|
139
142
|
activeRequestId: null,
|
|
140
143
|
lastError: null,
|
|
@@ -142,6 +145,7 @@ export class StructuredSessionManager {
|
|
|
142
145
|
autoRecovered: false,
|
|
143
146
|
autoApprovePermissions: shouldAutoApproveForMode(options.mode),
|
|
144
147
|
approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
|
|
148
|
+
selectedModel,
|
|
145
149
|
};
|
|
146
150
|
this.sessions.set(id, snapshot);
|
|
147
151
|
this.storage.saveSession(snapshot);
|
|
@@ -262,6 +266,27 @@ export class StructuredSessionManager {
|
|
|
262
266
|
denyPermission(sessionId) {
|
|
263
267
|
return this.resolvePermission(sessionId, false);
|
|
264
268
|
}
|
|
269
|
+
/** Update the selected model for a structured session. Takes effect on the next spawn. */
|
|
270
|
+
setSessionModel(sessionId, model) {
|
|
271
|
+
const session = this.requireSession(sessionId);
|
|
272
|
+
const normalized = model?.trim() || null;
|
|
273
|
+
const updated = {
|
|
274
|
+
...session,
|
|
275
|
+
selectedModel: normalized,
|
|
276
|
+
structuredState: {
|
|
277
|
+
...(session.structuredState ?? { provider: "claude", runner: "claude-cli-print", inFlight: false, activeRequestId: null, lastError: null }),
|
|
278
|
+
model: normalized ?? undefined,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
this.sessions.set(sessionId, updated);
|
|
282
|
+
this.storage.saveSession(updated);
|
|
283
|
+
this.emit({
|
|
284
|
+
type: "status",
|
|
285
|
+
sessionId,
|
|
286
|
+
data: { sessionKind: "structured", selectedModel: normalized, structuredState: updated.structuredState },
|
|
287
|
+
});
|
|
288
|
+
return updated;
|
|
289
|
+
}
|
|
265
290
|
/** Toggle auto-approve for the session. */
|
|
266
291
|
toggleAutoApprove(sessionId) {
|
|
267
292
|
const session = this.requireSession(sessionId);
|
|
@@ -500,6 +525,10 @@ export class StructuredSessionManager {
|
|
|
500
525
|
? "请使用中文回复。所有解释、注释和对话文本都使用中文。"
|
|
501
526
|
: `Please respond in ${language}. Use ${language} for all your explanations, comments, and conversational text.`);
|
|
502
527
|
}
|
|
528
|
+
const modelChoice = session.selectedModel?.trim();
|
|
529
|
+
if (modelChoice && modelChoice !== "default") {
|
|
530
|
+
args.push("--model", modelChoice);
|
|
531
|
+
}
|
|
503
532
|
if (session.claudeSessionId) {
|
|
504
533
|
args.push("--resume", session.claudeSessionId);
|
|
505
534
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -86,6 +86,18 @@ export interface WandConfig {
|
|
|
86
86
|
android?: AndroidApkConfig;
|
|
87
87
|
/** Default expand/collapse state for card types in structured chat view */
|
|
88
88
|
cardDefaults?: CardExpandDefaults;
|
|
89
|
+
/** 新建会话时默认使用的 Claude 模型(别名或完整 ID)。留空则不传 --model,由 claude 自行决定。 */
|
|
90
|
+
defaultModel?: string;
|
|
91
|
+
}
|
|
92
|
+
export interface ClaudeModelInfo {
|
|
93
|
+
/** 传给 --model 的值(别名或完整模型 ID) */
|
|
94
|
+
id: string;
|
|
95
|
+
/** UI 显示的友好标签 */
|
|
96
|
+
label: string;
|
|
97
|
+
/** 可选备注:例如 "当前默认"、"最新" */
|
|
98
|
+
note?: string;
|
|
99
|
+
/** 是否为别名(opus/sonnet 等);完整 ID 为 false */
|
|
100
|
+
alias?: boolean;
|
|
89
101
|
}
|
|
90
102
|
interface WorktreeInfo {
|
|
91
103
|
branch: string;
|
|
@@ -130,6 +142,8 @@ export interface CommandRequest {
|
|
|
130
142
|
mode?: ExecutionMode;
|
|
131
143
|
initialInput?: string;
|
|
132
144
|
worktreeEnabled?: boolean;
|
|
145
|
+
/** Claude 模型(别名或完整 ID)。仅对 claude provider 生效。留空则回落到 config.defaultModel。 */
|
|
146
|
+
model?: string;
|
|
133
147
|
}
|
|
134
148
|
export interface InputRequest {
|
|
135
149
|
input?: string;
|
|
@@ -273,6 +287,8 @@ export interface SessionSnapshot {
|
|
|
273
287
|
summary?: string;
|
|
274
288
|
/** 当前正在执行的任务标题(用于会话列表展示) */
|
|
275
289
|
currentTaskTitle?: string;
|
|
290
|
+
/** 用户为此会话选定的 Claude 模型(别名或完整 ID)。结构化会话下次 spawn 时使用;PTY 会话仅用于展示。 */
|
|
291
|
+
selectedModel?: string | null;
|
|
276
292
|
}
|
|
277
293
|
export type SessionLifecycleState = "initializing" | "running" | "idle" | "thinking" | "waiting-input" | "archived";
|
|
278
294
|
export interface SessionLifecycle {
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { mkdirSync, existsSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import multer from "multer";
|
|
5
|
+
const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
|
|
6
|
+
const MAX_FILES = 5;
|
|
7
|
+
function sanitizeFilename(name) {
|
|
8
|
+
return name.replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 200);
|
|
9
|
+
}
|
|
10
|
+
export function registerUploadRoutes(app, processes) {
|
|
11
|
+
const storage = multer.diskStorage({
|
|
12
|
+
destination(_req, _file, cb) {
|
|
13
|
+
const sessionId = _req.params?.id;
|
|
14
|
+
const session = sessionId ? processes.get(sessionId) : null;
|
|
15
|
+
const cwd = session?.cwd || "/tmp";
|
|
16
|
+
const uploadDir = path.join(cwd, ".wand-uploads");
|
|
17
|
+
if (!existsSync(uploadDir)) {
|
|
18
|
+
mkdirSync(uploadDir, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
cb(null, uploadDir);
|
|
21
|
+
},
|
|
22
|
+
filename(_req, file, cb) {
|
|
23
|
+
const ts = Date.now();
|
|
24
|
+
const rand = randomBytes(4).toString("hex");
|
|
25
|
+
const safe = sanitizeFilename(file.originalname);
|
|
26
|
+
cb(null, `${ts}-${rand}-${safe}`);
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const upload = multer({
|
|
30
|
+
storage,
|
|
31
|
+
limits: { fileSize: MAX_FILE_SIZE, files: MAX_FILES },
|
|
32
|
+
});
|
|
33
|
+
app.post("/api/sessions/:id/upload", upload.array("files", MAX_FILES), (req, res) => {
|
|
34
|
+
const sessionId = req.params.id;
|
|
35
|
+
const session = processes.get(sessionId);
|
|
36
|
+
if (!session) {
|
|
37
|
+
res.status(404).json({ error: "会话不存在。" });
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const files = req.files || [];
|
|
41
|
+
if (files.length === 0) {
|
|
42
|
+
res.status(400).json({ error: "未收到文件。" });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const result = files.map((f) => ({
|
|
46
|
+
originalName: f.originalname,
|
|
47
|
+
savedPath: f.path,
|
|
48
|
+
size: f.size,
|
|
49
|
+
mimeType: f.mimetype,
|
|
50
|
+
}));
|
|
51
|
+
res.json({ files: result });
|
|
52
|
+
});
|
|
53
|
+
}
|