@co0ontty/wand 1.49.0 → 1.49.1
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/build-info.json +3 -3
- package/dist/server.js +111 -89
- package/dist/structured-session-manager.d.ts +2 -0
- package/dist/structured-session-manager.js +25 -43
- package/dist/types.d.ts +0 -5
- package/dist/upload-routes.js +10 -4
- package/dist/web-ui/content/scripts.js +31 -31
- package/dist/web-ui/content/styles.css +1 -1
- package/dist/web-ui/embedded-assets.d.ts +1 -1
- package/dist/web-ui/embedded-assets.js +3 -3
- package/package.json +1 -1
package/dist/build-info.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"commit": "
|
|
3
|
-
"builtAt": "2026-06-
|
|
4
|
-
"version": "1.49.
|
|
2
|
+
"commit": "b7e61be45180349fc6c31b05ead4290ca1c1781a",
|
|
3
|
+
"builtAt": "2026-06-03T15:02:08.515Z",
|
|
4
|
+
"version": "1.49.1",
|
|
5
5
|
"channel": "stable"
|
|
6
6
|
}
|
package/dist/server.js
CHANGED
|
@@ -595,7 +595,22 @@ async function resolveMacosDmgAsset(configDir, config) {
|
|
|
595
595
|
fileStat,
|
|
596
596
|
};
|
|
597
597
|
}));
|
|
598
|
-
candidates.sort((a, b) =>
|
|
598
|
+
candidates.sort((a, b) => {
|
|
599
|
+
const va = extractMacosDmgVersion(a.entry.name);
|
|
600
|
+
const vb = extractMacosDmgVersion(b.entry.name);
|
|
601
|
+
if (va && vb) {
|
|
602
|
+
const cmp = compareSemver(vb, va);
|
|
603
|
+
if (cmp !== 0)
|
|
604
|
+
return cmp;
|
|
605
|
+
}
|
|
606
|
+
else if (va && !vb) {
|
|
607
|
+
return -1;
|
|
608
|
+
}
|
|
609
|
+
else if (!va && vb) {
|
|
610
|
+
return 1;
|
|
611
|
+
}
|
|
612
|
+
return b.fileStat.mtimeMs - a.fileStat.mtimeMs;
|
|
613
|
+
});
|
|
599
614
|
const selected = candidates[0];
|
|
600
615
|
return {
|
|
601
616
|
fileName: selected.entry.name,
|
|
@@ -807,6 +822,73 @@ function classifyFile(ext, baseName) {
|
|
|
807
822
|
function mimeForExt(ext) {
|
|
808
823
|
return MIME_BY_EXT[ext.toLowerCase()] || "application/octet-stream";
|
|
809
824
|
}
|
|
825
|
+
function parseByteRange(rangeHeader, total) {
|
|
826
|
+
if (!rangeHeader)
|
|
827
|
+
return null;
|
|
828
|
+
const trimmed = rangeHeader.trim();
|
|
829
|
+
if (!trimmed.startsWith("bytes="))
|
|
830
|
+
return null;
|
|
831
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(trimmed);
|
|
832
|
+
if (!match || (match[1] === "" && match[2] === ""))
|
|
833
|
+
return "invalid";
|
|
834
|
+
let start;
|
|
835
|
+
let end;
|
|
836
|
+
if (match[1] === "") {
|
|
837
|
+
const suffixLength = Number(match[2]);
|
|
838
|
+
if (!Number.isSafeInteger(suffixLength) || suffixLength <= 0)
|
|
839
|
+
return "invalid";
|
|
840
|
+
start = Math.max(0, total - suffixLength);
|
|
841
|
+
end = total - 1;
|
|
842
|
+
}
|
|
843
|
+
else {
|
|
844
|
+
start = Number(match[1]);
|
|
845
|
+
end = match[2] === "" ? total - 1 : Math.min(Number(match[2]), total - 1);
|
|
846
|
+
}
|
|
847
|
+
if (!Number.isSafeInteger(start) || !Number.isSafeInteger(end) || start < 0 || start > end || start >= total) {
|
|
848
|
+
return "invalid";
|
|
849
|
+
}
|
|
850
|
+
return { start, end };
|
|
851
|
+
}
|
|
852
|
+
function streamFileWithRange(req, res, options) {
|
|
853
|
+
res.setHeader("Content-Type", options.contentType);
|
|
854
|
+
if (options.disposition)
|
|
855
|
+
res.setHeader("Content-Disposition", options.disposition);
|
|
856
|
+
for (const [name, value] of Object.entries(options.headers ?? {})) {
|
|
857
|
+
res.setHeader(name, value);
|
|
858
|
+
}
|
|
859
|
+
res.setHeader("Accept-Ranges", "bytes");
|
|
860
|
+
if (options.size === 0) {
|
|
861
|
+
if (req.headers.range?.trim().startsWith("bytes=")) {
|
|
862
|
+
res.status(416).setHeader("Content-Range", "bytes */0").end();
|
|
863
|
+
return;
|
|
864
|
+
}
|
|
865
|
+
res.setHeader("Content-Length", "0");
|
|
866
|
+
res.end();
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const parsedRange = parseByteRange(req.headers.range, options.size);
|
|
870
|
+
if (parsedRange === "invalid") {
|
|
871
|
+
res.status(416).setHeader("Content-Range", `bytes */${options.size}`).end();
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const start = parsedRange?.start ?? 0;
|
|
875
|
+
const end = parsedRange?.end ?? options.size - 1;
|
|
876
|
+
if (parsedRange) {
|
|
877
|
+
res.status(206);
|
|
878
|
+
res.setHeader("Content-Range", `bytes ${start}-${end}/${options.size}`);
|
|
879
|
+
}
|
|
880
|
+
res.setHeader("Content-Length", String(end - start + 1));
|
|
881
|
+
const stream = createReadStream(options.filePath, { start, end });
|
|
882
|
+
stream.on("error", (err) => {
|
|
883
|
+
if (!res.headersSent) {
|
|
884
|
+
res.status(500).json({ error: getErrorMessage(err, options.readErrorMessage ?? "读取文件失败。") });
|
|
885
|
+
}
|
|
886
|
+
else {
|
|
887
|
+
res.destroy();
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
stream.pipe(res);
|
|
891
|
+
}
|
|
810
892
|
export async function startServer(config, configPath) {
|
|
811
893
|
// 关键:在创建 ProcessManager / 任何 spawn 之前先修 PATH。
|
|
812
894
|
// 服务被注册为 systemd / launchd 时,unit 文件里的 PATH 是安装那一刻烧死的,
|
|
@@ -1021,53 +1103,13 @@ export async function startServer(config, configPath) {
|
|
|
1021
1103
|
res.status(404).json({ error: "当前没有可下载的 APK 文件。" });
|
|
1022
1104
|
return;
|
|
1023
1105
|
}
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
let start = 0;
|
|
1031
|
-
let end = total - 1;
|
|
1032
|
-
let isPartial = false;
|
|
1033
|
-
const rangeHeader = req.headers.range;
|
|
1034
|
-
if (rangeHeader) {
|
|
1035
|
-
const match = /^bytes=(\d*)-(\d*)$/.exec(rangeHeader.trim());
|
|
1036
|
-
if (match && (match[1] !== "" || match[2] !== "")) {
|
|
1037
|
-
if (match[1] === "") {
|
|
1038
|
-
// bytes=-N: 最后 N 字节
|
|
1039
|
-
start = Math.max(0, total - Number(match[2]));
|
|
1040
|
-
end = total - 1;
|
|
1041
|
-
}
|
|
1042
|
-
else {
|
|
1043
|
-
start = Number(match[1]);
|
|
1044
|
-
end = match[2] === "" ? total - 1 : Math.min(Number(match[2]), total - 1);
|
|
1045
|
-
}
|
|
1046
|
-
isPartial = true;
|
|
1047
|
-
if (start > end || start < 0 || start >= total) {
|
|
1048
|
-
res.status(416);
|
|
1049
|
-
res.setHeader("Content-Range", `bytes */${total}`);
|
|
1050
|
-
res.end();
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
if (isPartial) {
|
|
1056
|
-
res.status(206);
|
|
1057
|
-
res.setHeader("Content-Range", `bytes ${start}-${end}/${total}`);
|
|
1058
|
-
}
|
|
1059
|
-
res.setHeader("Content-Length", String(end - start + 1));
|
|
1060
|
-
const stream = createReadStream(androidApk.filePath, { start, end });
|
|
1061
|
-
stream.on("error", (err) => {
|
|
1062
|
-
// 文件在 stat 之后被删/读错误时, 让客户端尽快感知断流, 而不是等满读超时。
|
|
1063
|
-
if (!res.headersSent) {
|
|
1064
|
-
res.status(500).json({ error: getErrorMessage(err, "读取 APK 文件失败。") });
|
|
1065
|
-
}
|
|
1066
|
-
else {
|
|
1067
|
-
res.destroy();
|
|
1068
|
-
}
|
|
1106
|
+
streamFileWithRange(req, res, {
|
|
1107
|
+
filePath: androidApk.filePath,
|
|
1108
|
+
size: androidApk.size,
|
|
1109
|
+
contentType: "application/vnd.android.package-archive",
|
|
1110
|
+
disposition: `attachment; filename="${encodeURIComponent(androidApk.fileName)}"`,
|
|
1111
|
+
readErrorMessage: "读取 APK 文件失败。",
|
|
1069
1112
|
});
|
|
1070
|
-
stream.pipe(res);
|
|
1071
1113
|
});
|
|
1072
1114
|
// ── macOS DMG update & download (no auth required) ──
|
|
1073
1115
|
app.get("/api/macos-dmg-update", async (req, res) => {
|
|
@@ -1092,7 +1134,7 @@ export async function startServer(config, configPath) {
|
|
|
1092
1134
|
source: latest.source,
|
|
1093
1135
|
});
|
|
1094
1136
|
});
|
|
1095
|
-
app.get("/macos/download", async (
|
|
1137
|
+
app.get("/macos/download", async (req, res) => {
|
|
1096
1138
|
if (config.macos?.enabled !== true) {
|
|
1097
1139
|
res.status(404).json({ error: "macOS DMG 下载未启用。" });
|
|
1098
1140
|
return;
|
|
@@ -1102,10 +1144,13 @@ export async function startServer(config, configPath) {
|
|
|
1102
1144
|
res.status(404).json({ error: "当前没有可下载的 DMG 文件。" });
|
|
1103
1145
|
return;
|
|
1104
1146
|
}
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1147
|
+
streamFileWithRange(req, res, {
|
|
1148
|
+
filePath: macosDmg.filePath,
|
|
1149
|
+
size: macosDmg.size,
|
|
1150
|
+
contentType: "application/x-apple-diskimage",
|
|
1151
|
+
disposition: `attachment; filename="${encodeURIComponent(macosDmg.fileName)}"`,
|
|
1152
|
+
readErrorMessage: "读取 DMG 文件失败。",
|
|
1153
|
+
});
|
|
1109
1154
|
});
|
|
1110
1155
|
// Public probe so the unauthenticated browser does not log a 401 on /api/config
|
|
1111
1156
|
app.get("/api/session-check", (req, res) => {
|
|
@@ -1773,41 +1818,17 @@ export async function startServer(config, configPath) {
|
|
|
1773
1818
|
const disposition = asDownload
|
|
1774
1819
|
? `attachment; filename*=UTF-8''${encodedName}`
|
|
1775
1820
|
: `inline; filename*=UTF-8''${encodedName}`;
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
res.status(416).setHeader("Content-Range", `bytes */${total}`).end();
|
|
1788
|
-
return;
|
|
1789
|
-
}
|
|
1790
|
-
const startStr = match[1];
|
|
1791
|
-
const endStr = match[2];
|
|
1792
|
-
let start = startStr === "" ? 0 : parseInt(startStr, 10);
|
|
1793
|
-
let end = endStr === "" ? total - 1 : parseInt(endStr, 10);
|
|
1794
|
-
if (Number.isNaN(start) || Number.isNaN(end) || start > end || start < 0 || end >= total) {
|
|
1795
|
-
res.status(416).setHeader("Content-Range", `bytes */${total}`).end();
|
|
1796
|
-
return;
|
|
1797
|
-
}
|
|
1798
|
-
const chunkSize = end - start + 1;
|
|
1799
|
-
res.status(206);
|
|
1800
|
-
res.setHeader("Content-Range", `bytes ${start}-${end}/${total}`);
|
|
1801
|
-
res.setHeader("Content-Length", String(chunkSize));
|
|
1802
|
-
const stream = createReadStream(resolvedPath, { start, end });
|
|
1803
|
-
stream.on("error", () => res.destroy());
|
|
1804
|
-
stream.pipe(res);
|
|
1805
|
-
return;
|
|
1806
|
-
}
|
|
1807
|
-
res.setHeader("Content-Length", String(total));
|
|
1808
|
-
const stream = createReadStream(resolvedPath);
|
|
1809
|
-
stream.on("error", () => res.destroy());
|
|
1810
|
-
stream.pipe(res);
|
|
1821
|
+
streamFileWithRange(req, res, {
|
|
1822
|
+
filePath: resolvedPath,
|
|
1823
|
+
size: fileStat.size,
|
|
1824
|
+
contentType,
|
|
1825
|
+
disposition,
|
|
1826
|
+
headers: {
|
|
1827
|
+
"Cache-Control": "private, max-age=60",
|
|
1828
|
+
"X-Content-Type-Options": "nosniff",
|
|
1829
|
+
},
|
|
1830
|
+
readErrorMessage: "Failed to read file",
|
|
1831
|
+
});
|
|
1811
1832
|
}
|
|
1812
1833
|
catch (error) {
|
|
1813
1834
|
res.status(400).json({ error: getErrorMessage(error, "Failed to read file") });
|
|
@@ -1837,10 +1858,11 @@ export async function startServer(config, configPath) {
|
|
|
1837
1858
|
res.json({ currentPath: targetPath, items });
|
|
1838
1859
|
}
|
|
1839
1860
|
catch (error) {
|
|
1840
|
-
|
|
1861
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "";
|
|
1862
|
+
if (code === "ENOENT") {
|
|
1841
1863
|
res.status(404).json({ error: "路径不存在:" + q, currentPath: q, items: [] });
|
|
1842
1864
|
}
|
|
1843
|
-
else if (
|
|
1865
|
+
else if (code === "EACCES") {
|
|
1844
1866
|
res.status(403).json({ error: "权限不足,无法访问:" + q, currentPath: q, items: [] });
|
|
1845
1867
|
}
|
|
1846
1868
|
else {
|
|
@@ -149,6 +149,8 @@ export declare class StructuredSessionManager {
|
|
|
149
149
|
private runClaudeSdkStreaming;
|
|
150
150
|
private extractAssistantMessage;
|
|
151
151
|
private compactContentBlocks;
|
|
152
|
+
private buildCompletedAssistantMessages;
|
|
153
|
+
private resolveQueuedMessagesAfterInterrupt;
|
|
152
154
|
private normalizeToolInput;
|
|
153
155
|
private normalizeToolResultContent;
|
|
154
156
|
private extractCodexText;
|
|
@@ -1315,17 +1315,7 @@ export class StructuredSessionManager {
|
|
|
1315
1315
|
reject(new Error(errorText));
|
|
1316
1316
|
return;
|
|
1317
1317
|
}
|
|
1318
|
-
const
|
|
1319
|
-
role: "assistant",
|
|
1320
|
-
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
1321
|
-
usage: turnState.usage,
|
|
1322
|
-
};
|
|
1323
|
-
const msgs = [...(current.messages ?? [])];
|
|
1324
|
-
const lastMsg = msgs[msgs.length - 1];
|
|
1325
|
-
if (lastMsg && lastMsg.role === "assistant")
|
|
1326
|
-
msgs[msgs.length - 1] = assistantTurn;
|
|
1327
|
-
else
|
|
1328
|
-
msgs.push(assistantTurn);
|
|
1318
|
+
const msgs = this.buildCompletedAssistantMessages(current, turnState);
|
|
1329
1319
|
const keepRunning = !!interruptPrompt;
|
|
1330
1320
|
const finished = {
|
|
1331
1321
|
...current,
|
|
@@ -1335,7 +1325,7 @@ export class StructuredSessionManager {
|
|
|
1335
1325
|
output: turnState.result,
|
|
1336
1326
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
1337
1327
|
messages: msgs,
|
|
1338
|
-
queuedMessages:
|
|
1328
|
+
queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
|
|
1339
1329
|
pendingEscalation: null,
|
|
1340
1330
|
permissionBlocked: false,
|
|
1341
1331
|
structuredState: {
|
|
@@ -1843,22 +1833,7 @@ export class StructuredSessionManager {
|
|
|
1843
1833
|
reject(new Error(errorText));
|
|
1844
1834
|
return;
|
|
1845
1835
|
}
|
|
1846
|
-
|
|
1847
|
-
const finalContent = this.compactContentBlocks([...turnState.blocks], turnState.result);
|
|
1848
|
-
const assistantTurn = {
|
|
1849
|
-
role: "assistant",
|
|
1850
|
-
content: finalContent,
|
|
1851
|
-
usage: turnState.usage,
|
|
1852
|
-
};
|
|
1853
|
-
// Ensure the final messages list has the completed assistant turn.
|
|
1854
|
-
const msgs = [...(current.messages ?? [])];
|
|
1855
|
-
const lastMsg = msgs[msgs.length - 1];
|
|
1856
|
-
if (lastMsg && lastMsg.role === "assistant") {
|
|
1857
|
-
msgs[msgs.length - 1] = assistantTurn;
|
|
1858
|
-
}
|
|
1859
|
-
else {
|
|
1860
|
-
msgs.push(assistantTurn);
|
|
1861
|
-
}
|
|
1836
|
+
const msgs = this.buildCompletedAssistantMessages(current, turnState);
|
|
1862
1837
|
// 被 AskUserQuestion 检测或用户中断主动 kill 时,保持 status="running"
|
|
1863
1838
|
// 让 UI 不跳到"已停止"。inFlight=false 才能触发后续 sendMessage。
|
|
1864
1839
|
const interruptPrompt = this.interruptedWith.get(sessionId);
|
|
@@ -1871,7 +1846,7 @@ export class StructuredSessionManager {
|
|
|
1871
1846
|
output: turnState.result,
|
|
1872
1847
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
1873
1848
|
messages: msgs,
|
|
1874
|
-
queuedMessages:
|
|
1849
|
+
queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
|
|
1875
1850
|
pendingEscalation: null,
|
|
1876
1851
|
permissionBlocked: false,
|
|
1877
1852
|
structuredState: {
|
|
@@ -2352,19 +2327,7 @@ export class StructuredSessionManager {
|
|
|
2352
2327
|
sessionId: turnState.sessionId,
|
|
2353
2328
|
});
|
|
2354
2329
|
const interruptedByUser = this.interruptedWith.has(sessionId);
|
|
2355
|
-
|
|
2356
|
-
const finalContent = this.compactContentBlocks([...turnState.blocks], turnState.result);
|
|
2357
|
-
const assistantTurn = {
|
|
2358
|
-
role: "assistant",
|
|
2359
|
-
content: finalContent,
|
|
2360
|
-
usage: turnState.usage,
|
|
2361
|
-
};
|
|
2362
|
-
const msgs = [...(current.messages ?? [])];
|
|
2363
|
-
const lastMsg = msgs[msgs.length - 1];
|
|
2364
|
-
if (lastMsg && lastMsg.role === "assistant")
|
|
2365
|
-
msgs[msgs.length - 1] = assistantTurn;
|
|
2366
|
-
else
|
|
2367
|
-
msgs.push(assistantTurn);
|
|
2330
|
+
const msgs = this.buildCompletedAssistantMessages(current, turnState);
|
|
2368
2331
|
const interruptPrompt = this.interruptedWith.get(sessionId);
|
|
2369
2332
|
const keepRunning = killedForAskUserQuestion || !!interruptPrompt;
|
|
2370
2333
|
const finished = {
|
|
@@ -2375,7 +2338,7 @@ export class StructuredSessionManager {
|
|
|
2375
2338
|
output: turnState.result,
|
|
2376
2339
|
claudeSessionId: turnState.sessionId ?? current.claudeSessionId,
|
|
2377
2340
|
messages: msgs,
|
|
2378
|
-
queuedMessages:
|
|
2341
|
+
queuedMessages: this.resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt),
|
|
2379
2342
|
pendingEscalation: null,
|
|
2380
2343
|
permissionBlocked: false,
|
|
2381
2344
|
structuredState: {
|
|
@@ -2496,6 +2459,25 @@ export class StructuredSessionManager {
|
|
|
2496
2459
|
}
|
|
2497
2460
|
return compacted;
|
|
2498
2461
|
}
|
|
2462
|
+
buildCompletedAssistantMessages(current, turnState) {
|
|
2463
|
+
const assistantTurn = {
|
|
2464
|
+
role: "assistant",
|
|
2465
|
+
content: this.compactContentBlocks([...turnState.blocks], turnState.result),
|
|
2466
|
+
usage: turnState.usage,
|
|
2467
|
+
};
|
|
2468
|
+
const msgs = [...(current.messages ?? [])];
|
|
2469
|
+
const lastMsg = msgs[msgs.length - 1];
|
|
2470
|
+
if (lastMsg && lastMsg.role === "assistant")
|
|
2471
|
+
msgs[msgs.length - 1] = assistantTurn;
|
|
2472
|
+
else
|
|
2473
|
+
msgs.push(assistantTurn);
|
|
2474
|
+
return msgs;
|
|
2475
|
+
}
|
|
2476
|
+
resolveQueuedMessagesAfterInterrupt(sessionId, current, interruptPrompt) {
|
|
2477
|
+
if (interruptPrompt && !this.preserveQueueOnInterrupt.has(sessionId))
|
|
2478
|
+
return [];
|
|
2479
|
+
return current.queuedMessages;
|
|
2480
|
+
}
|
|
2499
2481
|
normalizeToolInput(input) {
|
|
2500
2482
|
if (!input || typeof input !== "object" || Array.isArray(input)) {
|
|
2501
2483
|
return {};
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
export type SessionKind = "pty" | "structured";
|
|
2
|
-
export type SessionCreateKind = "pty" | "structured";
|
|
3
2
|
export type SessionProvider = "claude" | "codex";
|
|
4
3
|
export type SessionRunner = "claude-cli" | "claude-cli-print" | "claude-sdk" | "codex-cli-exec" | "pty";
|
|
5
4
|
export type ExecutionMode = "assist" | "agent" | "agent-max" | "default" | "auto-edit" | "full-access" | "native" | "managed";
|
|
@@ -310,10 +309,6 @@ export interface FilePreviewResponse {
|
|
|
310
309
|
/** File content; only present when kind === "text". */
|
|
311
310
|
content?: string;
|
|
312
311
|
}
|
|
313
|
-
export interface ChatMessage {
|
|
314
|
-
role: "user" | "assistant";
|
|
315
|
-
content: string;
|
|
316
|
-
}
|
|
317
312
|
/**
|
|
318
313
|
* Meta marker attached to blocks emitted by a Task-spawned subagent. Present
|
|
319
314
|
* on every block (text / thinking / tool_use / tool_result) whose origin is a
|
package/dist/upload-routes.js
CHANGED
|
@@ -10,9 +10,11 @@ function sanitizeFilename(name) {
|
|
|
10
10
|
export function registerUploadRoutes(app, processes) {
|
|
11
11
|
const storage = multer.diskStorage({
|
|
12
12
|
destination(_req, _file, cb) {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
const cwd = _req.uploadCwd;
|
|
14
|
+
if (!cwd) {
|
|
15
|
+
cb(new Error("会话不存在。"), "");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
16
18
|
const uploadDir = path.join(cwd, ".wand-uploads");
|
|
17
19
|
if (!existsSync(uploadDir)) {
|
|
18
20
|
mkdirSync(uploadDir, { recursive: true });
|
|
@@ -30,13 +32,17 @@ export function registerUploadRoutes(app, processes) {
|
|
|
30
32
|
storage,
|
|
31
33
|
limits: { fileSize: MAX_FILE_SIZE, files: MAX_FILES },
|
|
32
34
|
});
|
|
33
|
-
|
|
35
|
+
function requireUploadSession(req, res, next) {
|
|
34
36
|
const sessionId = req.params.id;
|
|
35
37
|
const session = processes.get(sessionId);
|
|
36
38
|
if (!session) {
|
|
37
39
|
res.status(404).json({ error: "会话不存在。" });
|
|
38
40
|
return;
|
|
39
41
|
}
|
|
42
|
+
req.uploadCwd = session.cwd || "/tmp";
|
|
43
|
+
next();
|
|
44
|
+
}
|
|
45
|
+
app.post("/api/sessions/:id/upload", requireUploadSession, upload.array("files", MAX_FILES), (req, res) => {
|
|
40
46
|
const files = req.files || [];
|
|
41
47
|
if (files.length === 0) {
|
|
42
48
|
res.status(400).json({ error: "未收到文件。" });
|