@co0ontty/wand 1.21.11 → 1.21.13
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/server.js +294 -31
- package/dist/structured-session-manager.d.ts +6 -2
- package/dist/structured-session-manager.js +174 -97
- package/dist/types.d.ts +24 -0
- package/dist/web-ui/content/scripts.js +970 -211
- package/dist/web-ui/content/styles.css +812 -51
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -2,7 +2,7 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import compression from "compression";
|
|
3
3
|
import express from "express";
|
|
4
4
|
import { createReadStream, existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
5
|
-
import { mkdir, readdir, readFile, stat } from "node:fs/promises";
|
|
5
|
+
import { lstat, mkdir, readdir, readFile, stat } from "node:fs/promises";
|
|
6
6
|
import { createServer as createHttpServer } from "node:http";
|
|
7
7
|
import { createServer as createHttpsServer } from "node:https";
|
|
8
8
|
import { exec, spawn } from "node:child_process";
|
|
@@ -13,6 +13,7 @@ import { WebSocketServer } from "ws";
|
|
|
13
13
|
import { ensureAvatarSeed, getAvatarSvg } from "./avatar.js";
|
|
14
14
|
import { createSession, revokeSession, setAuthStorage, validateSession } from "./auth.js";
|
|
15
15
|
import { ensureCertificates } from "./cert.js";
|
|
16
|
+
import { buildChildEnv } from "./env-utils.js";
|
|
16
17
|
import { isExecutionMode, PREFERENCE_KEYS, resolveConfigDir, saveConfig, writePreferenceToStorage, } from "./config.js";
|
|
17
18
|
import { getCachedModels, refreshModels } from "./models.js";
|
|
18
19
|
import { ProcessManager } from "./process-manager.js";
|
|
@@ -535,6 +536,106 @@ function getLanguageFromExt(ext, filePath) {
|
|
|
535
536
|
return "plaintext";
|
|
536
537
|
return map[ext] || "plaintext";
|
|
537
538
|
}
|
|
539
|
+
// ── File preview classification ──
|
|
540
|
+
const TEXT_PREVIEWABLE_EXTS = new Set([
|
|
541
|
+
".md", ".markdown", ".mdown", ".mkd", ".mkdn", ".mdx",
|
|
542
|
+
".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs",
|
|
543
|
+
".json", ".jsonc", ".html", ".htm", ".css", ".scss", ".less",
|
|
544
|
+
".py", ".rb", ".go", ".rs", ".java", ".c", ".cpp", ".h", ".hpp",
|
|
545
|
+
".cs", ".swift", ".kt", ".scala", ".php", ".sh", ".bash", ".zsh", ".fish",
|
|
546
|
+
".yaml", ".yml", ".toml", ".ini", ".cfg", ".conf", ".env",
|
|
547
|
+
".xml", ".sql", ".graphql", ".proto",
|
|
548
|
+
".dockerfile", ".gitignore", ".editorconfig",
|
|
549
|
+
".vue", ".svelte",
|
|
550
|
+
".txt", ".log", ".diff", ".patch",
|
|
551
|
+
".lua", ".r", ".dart", ".pl", ".pm",
|
|
552
|
+
]);
|
|
553
|
+
const IMAGE_EXTS = new Set([
|
|
554
|
+
".png", ".jpg", ".jpeg", ".gif", ".webp", ".svg", ".avif",
|
|
555
|
+
".bmp", ".ico", ".heic", ".heif",
|
|
556
|
+
]);
|
|
557
|
+
const VIDEO_EXTS = new Set([
|
|
558
|
+
".mp4", ".webm", ".mov", ".mkv", ".m4v", ".ogv",
|
|
559
|
+
]);
|
|
560
|
+
const AUDIO_EXTS = new Set([
|
|
561
|
+
".mp3", ".wav", ".ogg", ".m4a", ".flac", ".aac", ".opus",
|
|
562
|
+
]);
|
|
563
|
+
const PDF_EXTS = new Set([".pdf"]);
|
|
564
|
+
const MIME_BY_EXT = {
|
|
565
|
+
".png": "image/png",
|
|
566
|
+
".jpg": "image/jpeg",
|
|
567
|
+
".jpeg": "image/jpeg",
|
|
568
|
+
".gif": "image/gif",
|
|
569
|
+
".webp": "image/webp",
|
|
570
|
+
".svg": "image/svg+xml",
|
|
571
|
+
".avif": "image/avif",
|
|
572
|
+
".bmp": "image/bmp",
|
|
573
|
+
".ico": "image/x-icon",
|
|
574
|
+
".heic": "image/heic",
|
|
575
|
+
".heif": "image/heif",
|
|
576
|
+
".pdf": "application/pdf",
|
|
577
|
+
".mp4": "video/mp4",
|
|
578
|
+
".webm": "video/webm",
|
|
579
|
+
".mov": "video/quicktime",
|
|
580
|
+
".mkv": "video/x-matroska",
|
|
581
|
+
".m4v": "video/x-m4v",
|
|
582
|
+
".ogv": "video/ogg",
|
|
583
|
+
".mp3": "audio/mpeg",
|
|
584
|
+
".wav": "audio/wav",
|
|
585
|
+
".ogg": "audio/ogg",
|
|
586
|
+
".m4a": "audio/mp4",
|
|
587
|
+
".flac": "audio/flac",
|
|
588
|
+
".aac": "audio/aac",
|
|
589
|
+
".opus": "audio/opus",
|
|
590
|
+
};
|
|
591
|
+
const TEXT_BASENAME_ALLOW = new Set([
|
|
592
|
+
"dockerfile", ".gitignore", ".dockerignore", ".env", ".env.local",
|
|
593
|
+
".env.development", ".env.production", ".env.test",
|
|
594
|
+
"makefile", "readme", "license", "changelog",
|
|
595
|
+
]);
|
|
596
|
+
function classifyFile(ext, baseName) {
|
|
597
|
+
const lowerExt = ext.toLowerCase();
|
|
598
|
+
const lowerBase = baseName.toLowerCase();
|
|
599
|
+
if (IMAGE_EXTS.has(lowerExt))
|
|
600
|
+
return "image";
|
|
601
|
+
if (PDF_EXTS.has(lowerExt))
|
|
602
|
+
return "pdf";
|
|
603
|
+
if (VIDEO_EXTS.has(lowerExt))
|
|
604
|
+
return "video";
|
|
605
|
+
if (AUDIO_EXTS.has(lowerExt))
|
|
606
|
+
return "audio";
|
|
607
|
+
if (TEXT_PREVIEWABLE_EXTS.has(lowerExt))
|
|
608
|
+
return "text";
|
|
609
|
+
if (TEXT_BASENAME_ALLOW.has(lowerBase))
|
|
610
|
+
return "text";
|
|
611
|
+
// Files with no extension that look like text-y dotfiles
|
|
612
|
+
if (lowerExt === "" && /^[a-z0-9._-]+$/i.test(lowerBase))
|
|
613
|
+
return "text";
|
|
614
|
+
return "binary";
|
|
615
|
+
}
|
|
616
|
+
function mimeForExt(ext) {
|
|
617
|
+
return MIME_BY_EXT[ext.toLowerCase()] || "application/octet-stream";
|
|
618
|
+
}
|
|
619
|
+
/** Hidden files that should still surface even when "show hidden" is off. */
|
|
620
|
+
const HIDDEN_ALLOWLIST = new Set([
|
|
621
|
+
".gitignore", ".gitattributes", ".gitmodules",
|
|
622
|
+
".env", ".env.local", ".env.example",
|
|
623
|
+
".editorconfig", ".prettierrc", ".eslintrc",
|
|
624
|
+
".dockerignore", ".npmrc", ".nvmrc",
|
|
625
|
+
".browserslistrc", ".babelrc",
|
|
626
|
+
]);
|
|
627
|
+
function isHiddenEntry(name) {
|
|
628
|
+
if (!name.startsWith("."))
|
|
629
|
+
return false;
|
|
630
|
+
if (HIDDEN_ALLOWLIST.has(name))
|
|
631
|
+
return false;
|
|
632
|
+
// Common patterns like `.env.production` are also kept visible
|
|
633
|
+
for (const allowed of HIDDEN_ALLOWLIST) {
|
|
634
|
+
if (name.startsWith(allowed + "."))
|
|
635
|
+
return false;
|
|
636
|
+
}
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
538
639
|
export async function startServer(config, configPath) {
|
|
539
640
|
const app = express();
|
|
540
641
|
const storage = new WandStorage(resolveDatabasePath(configPath));
|
|
@@ -804,6 +905,45 @@ export async function startServer(config, configPath) {
|
|
|
804
905
|
github: ghApk ? { fileName: ghApk.fileName, version: ghApk.version, size: ghApk.size, downloadUrl: ghApk.downloadUrl } : null,
|
|
805
906
|
});
|
|
806
907
|
});
|
|
908
|
+
// 返回当前 inheritEnv 配置下,wand 启动 PTY / 结构化子进程时实际会传给
|
|
909
|
+
// claude / codex 的环境变量集合。值会按下面的规则做掩码:
|
|
910
|
+
// - 名字里含 KEY/TOKEN/SECRET/PASSWORD/AUTH/CREDENTIAL/COOKIE/SESSION 的视为敏感
|
|
911
|
+
// - 敏感值默认显示为 ***(保留长度提示),可通过 ?reveal=1 取消掩码
|
|
912
|
+
// 即使开启 reveal,仍只对已认证用户可见(路由由全局 requireAuth 保护)。
|
|
913
|
+
app.get("/api/settings/env-preview", (req, res) => {
|
|
914
|
+
const inheritEnv = config.inheritEnv !== false;
|
|
915
|
+
// 复用与 process-manager / structured-session-manager 相同的组装逻辑,
|
|
916
|
+
// 这样 UI 上看到的就是真正会被注入到子进程的那一份环境。
|
|
917
|
+
const env = buildChildEnv(inheritEnv, {
|
|
918
|
+
// PTY runner 还会注入 WAND_* 用于 mode 协调,这里也展示出来便于排查。
|
|
919
|
+
WAND_MODE: "<runtime>",
|
|
920
|
+
WAND_AUTO_CONFIRM: "<runtime>",
|
|
921
|
+
WAND_AUTO_EDIT: "<runtime>",
|
|
922
|
+
});
|
|
923
|
+
const reveal = req.query.reveal === "1" || req.query.reveal === "true";
|
|
924
|
+
const SENSITIVE_PATTERN = /(KEY|TOKEN|SECRET|PASSWORD|AUTH|CREDENTIAL|COOKIE|SESSION)/i;
|
|
925
|
+
const entries = Object.keys(env)
|
|
926
|
+
.sort()
|
|
927
|
+
.map((name) => {
|
|
928
|
+
const raw = env[name] ?? "";
|
|
929
|
+
const sensitive = SENSITIVE_PATTERN.test(name);
|
|
930
|
+
const masked = sensitive && !reveal;
|
|
931
|
+
// WAND_* 占位值不算敏感,保持原样。
|
|
932
|
+
const isPlaceholder = raw.startsWith("<") && raw.endsWith(">");
|
|
933
|
+
return {
|
|
934
|
+
name,
|
|
935
|
+
value: masked && !isPlaceholder ? "***" : raw,
|
|
936
|
+
length: raw.length,
|
|
937
|
+
sensitive,
|
|
938
|
+
};
|
|
939
|
+
});
|
|
940
|
+
res.json({
|
|
941
|
+
inheritEnv,
|
|
942
|
+
total: entries.length,
|
|
943
|
+
reveal,
|
|
944
|
+
entries,
|
|
945
|
+
});
|
|
946
|
+
});
|
|
807
947
|
app.get("/api/app-connect-code", requireAuth, (req, res) => {
|
|
808
948
|
const dbPassword = storage.getPassword();
|
|
809
949
|
const effectivePassword = dbPassword ?? config.password;
|
|
@@ -994,9 +1134,11 @@ export async function startServer(config, configPath) {
|
|
|
994
1134
|
}
|
|
995
1135
|
});
|
|
996
1136
|
// ── File browsing ──
|
|
1137
|
+
const DIRECTORY_MAX_ITEMS = 200;
|
|
997
1138
|
app.get("/api/directory", async (req, res) => {
|
|
998
1139
|
const q = typeof req.query.q === "string" ? req.query.q : "";
|
|
999
1140
|
const includeGitStatus = req.query.gitStatus === "true";
|
|
1141
|
+
const showHidden = req.query.showHidden === "true";
|
|
1000
1142
|
const targetPath = path.resolve(q || config.defaultCwd);
|
|
1001
1143
|
if (isBlockedFolderPath(targetPath)) {
|
|
1002
1144
|
res.status(403).json({ error: "访问被拒绝:无法访问系统敏感目录。" });
|
|
@@ -1004,30 +1146,49 @@ export async function startServer(config, configPath) {
|
|
|
1004
1146
|
}
|
|
1005
1147
|
try {
|
|
1006
1148
|
const entries = await readdir(targetPath, { withFileTypes: true });
|
|
1007
|
-
|
|
1008
|
-
|
|
1149
|
+
const visible = showHidden ? entries : entries.filter((e) => !isHiddenEntry(e.name));
|
|
1150
|
+
const sorted = visible.sort((a, b) => {
|
|
1009
1151
|
if (a.isDirectory() && !b.isDirectory())
|
|
1010
1152
|
return -1;
|
|
1011
1153
|
if (!a.isDirectory() && b.isDirectory())
|
|
1012
1154
|
return 1;
|
|
1013
1155
|
return a.name.localeCompare(b.name);
|
|
1014
|
-
})
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1156
|
+
});
|
|
1157
|
+
const total = sorted.length;
|
|
1158
|
+
const truncated = total > DIRECTORY_MAX_ITEMS;
|
|
1159
|
+
const sliced = sorted.slice(0, DIRECTORY_MAX_ITEMS);
|
|
1160
|
+
// Fetch size/mtime in parallel; tolerate per-entry failures.
|
|
1161
|
+
let items = await Promise.all(sliced.map(async (entry) => {
|
|
1162
|
+
const fullPath = path.join(targetPath, entry.name);
|
|
1163
|
+
const isDir = entry.isDirectory();
|
|
1164
|
+
const base = {
|
|
1165
|
+
path: fullPath,
|
|
1166
|
+
name: entry.name,
|
|
1167
|
+
type: isDir ? "dir" : "file",
|
|
1168
|
+
};
|
|
1169
|
+
if (isDir)
|
|
1170
|
+
return base;
|
|
1171
|
+
try {
|
|
1172
|
+
const st = await lstat(fullPath);
|
|
1173
|
+
base.size = st.size;
|
|
1174
|
+
base.mtime = st.mtime.toISOString();
|
|
1175
|
+
}
|
|
1176
|
+
catch {
|
|
1177
|
+
// Permission errors etc — leave size/mtime undefined.
|
|
1178
|
+
}
|
|
1179
|
+
return base;
|
|
1020
1180
|
}));
|
|
1021
1181
|
if (includeGitStatus) {
|
|
1022
1182
|
items = await enrichWithGitStatus(items, targetPath);
|
|
1023
1183
|
}
|
|
1024
|
-
|
|
1184
|
+
const payload = { items, truncated, total };
|
|
1185
|
+
res.json(payload);
|
|
1025
1186
|
}
|
|
1026
1187
|
catch (error) {
|
|
1027
1188
|
res.status(400).json({ error: getErrorMessage(error, "无法读取目录。可能原因:路径不存在或权限不足。") });
|
|
1028
1189
|
}
|
|
1029
1190
|
});
|
|
1030
|
-
const
|
|
1191
|
+
const MAX_TEXT_PREVIEW_SIZE = 512 * 1024;
|
|
1031
1192
|
app.get("/api/file-preview", async (req, res) => {
|
|
1032
1193
|
const filePath = typeof req.query.path === "string" ? req.query.path : "";
|
|
1033
1194
|
if (!filePath) {
|
|
@@ -1045,32 +1206,134 @@ export async function startServer(config, configPath) {
|
|
|
1045
1206
|
res.status(400).json({ error: "Cannot preview a directory" });
|
|
1046
1207
|
return;
|
|
1047
1208
|
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1209
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1210
|
+
const baseName = path.basename(filePath);
|
|
1211
|
+
const kind = classifyFile(ext, baseName);
|
|
1212
|
+
const mime = mimeForExt(ext);
|
|
1213
|
+
// Non-text kinds: respond with metadata so the client can pick a renderer.
|
|
1214
|
+
if (kind !== "text") {
|
|
1215
|
+
const payload = {
|
|
1216
|
+
kind,
|
|
1217
|
+
path: resolvedPath,
|
|
1218
|
+
name: baseName,
|
|
1219
|
+
ext,
|
|
1220
|
+
size: fileStat.size,
|
|
1221
|
+
mime,
|
|
1222
|
+
};
|
|
1223
|
+
res.json(payload);
|
|
1050
1224
|
return;
|
|
1051
1225
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
".dockerfile", ".gitignore", ".env", ".editorconfig",
|
|
1061
|
-
".mdx", ".vue", ".svelte",
|
|
1062
|
-
".txt", ".log", ".diff", ".patch",
|
|
1063
|
-
];
|
|
1064
|
-
const isText = previewableExts.includes(ext) ||
|
|
1065
|
-
ext === "" ||
|
|
1066
|
-
[".gitignore", "dockerfile", ".env.local", ".env.development"].some((e) => filePath.toLowerCase().endsWith(e));
|
|
1067
|
-
if (!isText) {
|
|
1068
|
-
res.status(415).json({ error: "Unsupported file type", ext });
|
|
1226
|
+
// Text/code preview path — still subject to the 512 KB cap.
|
|
1227
|
+
if (fileStat.size > MAX_TEXT_PREVIEW_SIZE) {
|
|
1228
|
+
res.status(413).json({
|
|
1229
|
+
error: "文件太大,无法在线预览(限 512 KB)。",
|
|
1230
|
+
truncated: true,
|
|
1231
|
+
size: fileStat.size,
|
|
1232
|
+
maxSize: MAX_TEXT_PREVIEW_SIZE,
|
|
1233
|
+
});
|
|
1069
1234
|
return;
|
|
1070
1235
|
}
|
|
1071
1236
|
const content = await readFile(resolvedPath, "utf-8");
|
|
1072
1237
|
const lang = getLanguageFromExt(ext, filePath);
|
|
1073
|
-
|
|
1238
|
+
const payload = {
|
|
1239
|
+
kind: "text",
|
|
1240
|
+
path: resolvedPath,
|
|
1241
|
+
name: baseName,
|
|
1242
|
+
ext,
|
|
1243
|
+
size: fileStat.size,
|
|
1244
|
+
mime,
|
|
1245
|
+
lang,
|
|
1246
|
+
content,
|
|
1247
|
+
};
|
|
1248
|
+
res.json(payload);
|
|
1249
|
+
}
|
|
1250
|
+
catch (error) {
|
|
1251
|
+
res.status(400).json({ error: getErrorMessage(error, "Failed to read file") });
|
|
1252
|
+
}
|
|
1253
|
+
});
|
|
1254
|
+
// Streams the raw bytes of a file for inline media previews (image/PDF/video/audio)
|
|
1255
|
+
// and downloads. Honors HTTP Range so video/audio scrubbing works.
|
|
1256
|
+
const RAW_MAX_BYTES_BY_KIND = {
|
|
1257
|
+
text: 5 * 1024 * 1024,
|
|
1258
|
+
image: 50 * 1024 * 1024,
|
|
1259
|
+
pdf: 50 * 1024 * 1024,
|
|
1260
|
+
video: 200 * 1024 * 1024,
|
|
1261
|
+
audio: 200 * 1024 * 1024,
|
|
1262
|
+
binary: 50 * 1024 * 1024,
|
|
1263
|
+
};
|
|
1264
|
+
app.get("/api/file-raw", async (req, res) => {
|
|
1265
|
+
const filePath = typeof req.query.path === "string" ? req.query.path : "";
|
|
1266
|
+
const asDownload = req.query.download === "1" || req.query.download === "true";
|
|
1267
|
+
if (!filePath) {
|
|
1268
|
+
res.status(400).json({ error: "Missing path parameter" });
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
const resolvedPath = path.resolve(filePath);
|
|
1272
|
+
if (isBlockedFolderPath(resolvedPath)) {
|
|
1273
|
+
res.status(403).json({ error: "Access denied" });
|
|
1274
|
+
return;
|
|
1275
|
+
}
|
|
1276
|
+
try {
|
|
1277
|
+
const fileStat = await stat(resolvedPath);
|
|
1278
|
+
if (!fileStat.isFile()) {
|
|
1279
|
+
res.status(400).json({ error: "Not a regular file" });
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
1283
|
+
const baseName = path.basename(filePath);
|
|
1284
|
+
const kind = classifyFile(ext, baseName);
|
|
1285
|
+
const cap = RAW_MAX_BYTES_BY_KIND[kind] ?? RAW_MAX_BYTES_BY_KIND.binary;
|
|
1286
|
+
if (fileStat.size > cap) {
|
|
1287
|
+
res.status(413).json({
|
|
1288
|
+
error: `文件超出可在线预览的上限(${Math.round(cap / 1024 / 1024)} MB)。`,
|
|
1289
|
+
size: fileStat.size,
|
|
1290
|
+
maxSize: cap,
|
|
1291
|
+
});
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const mime = mimeForExt(ext);
|
|
1295
|
+
// SVG can be served with its proper type; binary fallback uses octet-stream.
|
|
1296
|
+
const contentType = kind === "binary" ? "application/octet-stream" : mime;
|
|
1297
|
+
// Encode the filename for Content-Disposition (RFC 5987).
|
|
1298
|
+
const encodedName = encodeURIComponent(baseName);
|
|
1299
|
+
const disposition = asDownload
|
|
1300
|
+
? `attachment; filename*=UTF-8''${encodedName}`
|
|
1301
|
+
: `inline; filename*=UTF-8''${encodedName}`;
|
|
1302
|
+
const total = fileStat.size;
|
|
1303
|
+
const range = req.headers.range;
|
|
1304
|
+
// Safe to cache raw bytes briefly inside the user's browser.
|
|
1305
|
+
res.setHeader("Cache-Control", "private, max-age=60");
|
|
1306
|
+
res.setHeader("Content-Type", contentType);
|
|
1307
|
+
res.setHeader("Content-Disposition", disposition);
|
|
1308
|
+
res.setHeader("Accept-Ranges", "bytes");
|
|
1309
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
1310
|
+
if (range && /^bytes=/.test(range)) {
|
|
1311
|
+
const match = /^bytes=(\d*)-(\d*)$/.exec(range);
|
|
1312
|
+
if (!match) {
|
|
1313
|
+
res.status(416).setHeader("Content-Range", `bytes */${total}`).end();
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
const startStr = match[1];
|
|
1317
|
+
const endStr = match[2];
|
|
1318
|
+
let start = startStr === "" ? 0 : parseInt(startStr, 10);
|
|
1319
|
+
let end = endStr === "" ? total - 1 : parseInt(endStr, 10);
|
|
1320
|
+
if (Number.isNaN(start) || Number.isNaN(end) || start > end || start < 0 || end >= total) {
|
|
1321
|
+
res.status(416).setHeader("Content-Range", `bytes */${total}`).end();
|
|
1322
|
+
return;
|
|
1323
|
+
}
|
|
1324
|
+
const chunkSize = end - start + 1;
|
|
1325
|
+
res.status(206);
|
|
1326
|
+
res.setHeader("Content-Range", `bytes ${start}-${end}/${total}`);
|
|
1327
|
+
res.setHeader("Content-Length", String(chunkSize));
|
|
1328
|
+
const stream = createReadStream(resolvedPath, { start, end });
|
|
1329
|
+
stream.on("error", () => res.destroy());
|
|
1330
|
+
stream.pipe(res);
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
res.setHeader("Content-Length", String(total));
|
|
1334
|
+
const stream = createReadStream(resolvedPath);
|
|
1335
|
+
stream.on("error", () => res.destroy());
|
|
1336
|
+
stream.pipe(res);
|
|
1074
1337
|
}
|
|
1075
1338
|
catch (error) {
|
|
1076
1339
|
res.status(400).json({ error: getErrorMessage(error, "Failed to read file") });
|
|
@@ -18,6 +18,12 @@ export declare class StructuredSessionManager {
|
|
|
18
18
|
private readonly sessions;
|
|
19
19
|
private readonly pendingChildren;
|
|
20
20
|
private readonly pendingSdkAbort;
|
|
21
|
+
/**
|
|
22
|
+
* Active SDK Query handle per session, kept around so we can call
|
|
23
|
+
* `query.interrupt()` for a graceful stop instead of aborting via signal.
|
|
24
|
+
* Only populated while an SDK call is in flight.
|
|
25
|
+
*/
|
|
26
|
+
private readonly pendingSdkQueries;
|
|
21
27
|
private readonly interruptedWith;
|
|
22
28
|
/** Last wall-clock time (ms) we did a full saveSession for a streaming session. */
|
|
23
29
|
private readonly lastStreamSaveAt;
|
|
@@ -70,7 +76,6 @@ export declare class StructuredSessionManager {
|
|
|
70
76
|
private emit;
|
|
71
77
|
private resolvePermission;
|
|
72
78
|
private incrementApprovalStats;
|
|
73
|
-
private buildPermissionArgs;
|
|
74
79
|
private buildCodexArgs;
|
|
75
80
|
private runCodexStreaming;
|
|
76
81
|
/**
|
|
@@ -96,7 +101,6 @@ export declare class StructuredSessionManager {
|
|
|
96
101
|
* SDKAssistantMessage with the authoritative complete content.
|
|
97
102
|
*/
|
|
98
103
|
private runClaudeSdkStreaming;
|
|
99
|
-
private _runClaudeSdkStreamingAsync;
|
|
100
104
|
private extractAssistantMessage;
|
|
101
105
|
private compactContentBlocks;
|
|
102
106
|
private normalizeToolInput;
|