@dev-anywhere/proxy 0.2.0 → 0.2.2
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/{chunk-4YQ2JUM7.js → chunk-ENA4NCWK.js} +2 -6
- package/dist/chunk-ENA4NCWK.js.map +1 -0
- package/dist/{chunk-NBRBO5GS.js → chunk-NZZXBVO2.js} +38 -2
- package/dist/chunk-NZZXBVO2.js.map +1 -0
- package/dist/index.js +2 -2
- package/dist/serve.js +386 -65
- package/dist/serve.js.map +1 -1
- package/dist/session-worker.js +2 -2
- package/dist/{terminal-YO2D2OJU.js → terminal-YGYIRO7H.js} +2 -2
- package/package.json +3 -3
- package/dist/chunk-4YQ2JUM7.js.map +0 -1
- package/dist/chunk-NBRBO5GS.js.map +0 -1
- /package/dist/{terminal-YO2D2OJU.js.map → terminal-YGYIRO7H.js.map} +0 -0
package/dist/serve.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
StreamJsonEventSchema,
|
|
7
7
|
disposeSeqCounter,
|
|
8
8
|
getSeqCounterFor
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-ENA4NCWK.js";
|
|
10
10
|
import {
|
|
11
11
|
decidePtySemanticTransition,
|
|
12
12
|
extractOscSequences,
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
serializeControl,
|
|
36
36
|
serializeIpc,
|
|
37
37
|
serializeWorkerMsg
|
|
38
|
-
} from "./chunk-
|
|
38
|
+
} from "./chunk-NZZXBVO2.js";
|
|
39
39
|
import {
|
|
40
40
|
buildProviderEnv,
|
|
41
41
|
loadConfig,
|
|
@@ -65,7 +65,7 @@ import {
|
|
|
65
65
|
|
|
66
66
|
// src/serve.ts
|
|
67
67
|
import { createServer as createServer2 } from "net";
|
|
68
|
-
import { unlinkSync as unlinkSync4, writeFileSync as
|
|
68
|
+
import { unlinkSync as unlinkSync4, writeFileSync as writeFileSync4, chmodSync, rmSync as rmSync2 } from "fs";
|
|
69
69
|
|
|
70
70
|
// src/serve/session-manager.ts
|
|
71
71
|
import { readFileSync, existsSync } from "fs";
|
|
@@ -219,7 +219,18 @@ var SessionManager = class {
|
|
|
219
219
|
this.sessions.delete(id);
|
|
220
220
|
this.save();
|
|
221
221
|
serviceLogger.info({ sessionId: id, mode: session.mode, pid }, "Session terminated");
|
|
222
|
-
|
|
222
|
+
try {
|
|
223
|
+
this.onSessionRemoved?.(id, context);
|
|
224
|
+
} catch (err) {
|
|
225
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
226
|
+
serviceLogger.warn(
|
|
227
|
+
{
|
|
228
|
+
sessionId: id,
|
|
229
|
+
err: { message: error.message, stack: error.stack, cause: error.cause }
|
|
230
|
+
},
|
|
231
|
+
"onSessionRemoved callback threw; session already removed from registry"
|
|
232
|
+
);
|
|
233
|
+
}
|
|
223
234
|
return { success: true, pid };
|
|
224
235
|
}
|
|
225
236
|
terminateAll() {
|
|
@@ -957,7 +968,7 @@ function extractConversationText(msg) {
|
|
|
957
968
|
return null;
|
|
958
969
|
}
|
|
959
970
|
async function extractTitleAndCwd(filePath) {
|
|
960
|
-
return new Promise((
|
|
971
|
+
return new Promise((resolve5) => {
|
|
961
972
|
const rl = createInterface({
|
|
962
973
|
input: createReadStream(filePath, { encoding: "utf-8" }),
|
|
963
974
|
crlfDelay: Infinity
|
|
@@ -985,10 +996,10 @@ async function extractTitleAndCwd(filePath) {
|
|
|
985
996
|
}
|
|
986
997
|
});
|
|
987
998
|
rl.on("close", () => {
|
|
988
|
-
if (!resolved)
|
|
989
|
-
else
|
|
999
|
+
if (!resolved) resolve5({ title, cwd });
|
|
1000
|
+
else resolve5({ title, cwd });
|
|
990
1001
|
});
|
|
991
|
-
rl.on("error", () =>
|
|
1002
|
+
rl.on("error", () => resolve5({ title, cwd }));
|
|
992
1003
|
});
|
|
993
1004
|
}
|
|
994
1005
|
async function collectJsonlFiles(root) {
|
|
@@ -1010,7 +1021,7 @@ async function collectJsonlFiles(root) {
|
|
|
1010
1021
|
return files;
|
|
1011
1022
|
}
|
|
1012
1023
|
async function extractCodexTitleAndCwd(filePath) {
|
|
1013
|
-
return new Promise((
|
|
1024
|
+
return new Promise((resolve5) => {
|
|
1014
1025
|
const rl = createInterface({
|
|
1015
1026
|
input: createReadStream(filePath, { encoding: "utf-8" }),
|
|
1016
1027
|
crlfDelay: Infinity
|
|
@@ -1034,8 +1045,8 @@ async function extractCodexTitleAndCwd(filePath) {
|
|
|
1034
1045
|
} catch {
|
|
1035
1046
|
}
|
|
1036
1047
|
});
|
|
1037
|
-
rl.on("close", () =>
|
|
1038
|
-
rl.on("error", () =>
|
|
1048
|
+
rl.on("close", () => resolve5({ id, title, cwd }));
|
|
1049
|
+
rl.on("error", () => resolve5({ id, title, cwd }));
|
|
1039
1050
|
});
|
|
1040
1051
|
}
|
|
1041
1052
|
function extractCodexUserText(payload) {
|
|
@@ -1600,7 +1611,7 @@ var WorkerRegistry = class {
|
|
|
1600
1611
|
return workerPid;
|
|
1601
1612
|
}
|
|
1602
1613
|
connect(sessionId, sockPath) {
|
|
1603
|
-
return new Promise((
|
|
1614
|
+
return new Promise((resolve5) => {
|
|
1604
1615
|
const sock = connect(sockPath);
|
|
1605
1616
|
sock.on("connect", () => {
|
|
1606
1617
|
this.sockets.set(sessionId, sock);
|
|
@@ -1616,9 +1627,9 @@ var WorkerRegistry = class {
|
|
|
1616
1627
|
);
|
|
1617
1628
|
sock.on("close", () => this.onDisconnect(sessionId));
|
|
1618
1629
|
sock.on("error", () => this.onDisconnect(sessionId));
|
|
1619
|
-
|
|
1630
|
+
resolve5(sock);
|
|
1620
1631
|
});
|
|
1621
|
-
sock.on("error", () =>
|
|
1632
|
+
sock.on("error", () => resolve5(null));
|
|
1622
1633
|
});
|
|
1623
1634
|
}
|
|
1624
1635
|
// 枚举 DATA_DIR 下所有 session 目录,尝试连接存活的 worker.sock;失败则清理 stale socket。
|
|
@@ -2034,7 +2045,6 @@ function saveClipboardImageUpload(request, options = {}) {
|
|
|
2034
2045
|
if (!extension) {
|
|
2035
2046
|
return {
|
|
2036
2047
|
success: false,
|
|
2037
|
-
path: "",
|
|
2038
2048
|
error: "\u4E0D\u652F\u6301\u8FD9\u79CD\u56FE\u7247\u683C\u5F0F",
|
|
2039
2049
|
errorCode: ControlErrorCode.UNKNOWN
|
|
2040
2050
|
};
|
|
@@ -2060,34 +2070,228 @@ function saveClipboardImageUpload(request, options = {}) {
|
|
|
2060
2070
|
} catch (err) {
|
|
2061
2071
|
return {
|
|
2062
2072
|
success: false,
|
|
2063
|
-
path: "",
|
|
2064
2073
|
error: err instanceof Error ? err.message : String(err),
|
|
2065
2074
|
errorCode: ControlErrorCode.UNKNOWN
|
|
2066
2075
|
};
|
|
2067
2076
|
}
|
|
2068
2077
|
}
|
|
2069
2078
|
|
|
2070
|
-
// src/serve/
|
|
2079
|
+
// src/serve/file-download.ts
|
|
2071
2080
|
import { readFileSync as readFileSync5, realpathSync, statSync as statSync2 } from "fs";
|
|
2081
|
+
import { extname, isAbsolute as isAbsolute3, resolve as resolve2 } from "path";
|
|
2082
|
+
var MAX_FILE_DOWNLOAD_BYTES = 100 * 1024 * 1024;
|
|
2083
|
+
var EXT_MIME_MAP = {
|
|
2084
|
+
".png": "image/png",
|
|
2085
|
+
".jpg": "image/jpeg",
|
|
2086
|
+
".jpeg": "image/jpeg",
|
|
2087
|
+
".gif": "image/gif",
|
|
2088
|
+
".webp": "image/webp",
|
|
2089
|
+
".svg": "image/svg+xml",
|
|
2090
|
+
".bmp": "image/bmp",
|
|
2091
|
+
".pdf": "application/pdf",
|
|
2092
|
+
".json": "application/json",
|
|
2093
|
+
".xml": "application/xml",
|
|
2094
|
+
".html": "text/html",
|
|
2095
|
+
".htm": "text/html",
|
|
2096
|
+
".txt": "text/plain",
|
|
2097
|
+
".md": "text/markdown",
|
|
2098
|
+
".log": "text/plain",
|
|
2099
|
+
".csv": "text/csv",
|
|
2100
|
+
".js": "application/javascript",
|
|
2101
|
+
".mjs": "application/javascript",
|
|
2102
|
+
".ts": "application/typescript",
|
|
2103
|
+
".tsx": "application/typescript",
|
|
2104
|
+
".zip": "application/zip",
|
|
2105
|
+
".tar": "application/x-tar",
|
|
2106
|
+
".gz": "application/gzip",
|
|
2107
|
+
".mp4": "video/mp4",
|
|
2108
|
+
".mp3": "audio/mpeg",
|
|
2109
|
+
".wav": "audio/wav"
|
|
2110
|
+
};
|
|
2111
|
+
function guessMimeType(filePath) {
|
|
2112
|
+
const ext = extname(filePath).toLowerCase();
|
|
2113
|
+
return EXT_MIME_MAP[ext] ?? "application/octet-stream";
|
|
2114
|
+
}
|
|
2115
|
+
function resolveDownloadPath(rawPath, cwd) {
|
|
2116
|
+
const candidate = isAbsolute3(rawPath) ? resolve2(rawPath) : resolve2(cwd, rawPath);
|
|
2117
|
+
return realpathSync(candidate);
|
|
2118
|
+
}
|
|
2119
|
+
function errorCode(err) {
|
|
2120
|
+
if (err instanceof Error && "errorCode" in err && typeof err.errorCode === "string") {
|
|
2121
|
+
return err.errorCode;
|
|
2122
|
+
}
|
|
2123
|
+
return classifyPathError(err);
|
|
2124
|
+
}
|
|
2125
|
+
function loadFileDownload(request, options) {
|
|
2126
|
+
try {
|
|
2127
|
+
const resolvedPath = resolveDownloadPath(request.path, options.cwd);
|
|
2128
|
+
const stat2 = statSync2(resolvedPath);
|
|
2129
|
+
if (!stat2.isFile()) {
|
|
2130
|
+
return {
|
|
2131
|
+
success: false,
|
|
2132
|
+
sessionId: request.sessionId,
|
|
2133
|
+
path: request.path,
|
|
2134
|
+
error: "\u8DEF\u5F84\u4E0D\u662F\u666E\u901A\u6587\u4EF6",
|
|
2135
|
+
errorCode: ControlErrorCode.INVALID_PATH
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
const maxBytes = options.maxBytes ?? MAX_FILE_DOWNLOAD_BYTES;
|
|
2139
|
+
if (stat2.size > maxBytes) {
|
|
2140
|
+
return {
|
|
2141
|
+
success: false,
|
|
2142
|
+
sessionId: request.sessionId,
|
|
2143
|
+
path: request.path,
|
|
2144
|
+
error: `\u6587\u4EF6\u8D85\u8FC7 ${Math.round(maxBytes / 1024 / 1024)}MB \u9650\u5236`,
|
|
2145
|
+
errorCode: ControlErrorCode.UNKNOWN
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
const buffer = readFileSync5(resolvedPath);
|
|
2149
|
+
return {
|
|
2150
|
+
success: true,
|
|
2151
|
+
sessionId: request.sessionId,
|
|
2152
|
+
path: request.path,
|
|
2153
|
+
mimeType: guessMimeType(resolvedPath),
|
|
2154
|
+
dataBase64: buffer.toString("base64"),
|
|
2155
|
+
size: buffer.length
|
|
2156
|
+
};
|
|
2157
|
+
} catch (err) {
|
|
2158
|
+
return {
|
|
2159
|
+
success: false,
|
|
2160
|
+
sessionId: request.sessionId,
|
|
2161
|
+
path: request.path,
|
|
2162
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2163
|
+
errorCode: errorCode(err)
|
|
2164
|
+
};
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// src/serve/file-upload.ts
|
|
2169
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync2, readFileSync as readFileSync6, statSync as statSync3, writeFileSync as writeFileSync2 } from "fs";
|
|
2170
|
+
import { basename, isAbsolute as isAbsolute4, join as join6, relative as relative2, resolve as resolve3 } from "path";
|
|
2171
|
+
import { nanoid as nanoid4 } from "nanoid";
|
|
2172
|
+
var MAX_FILE_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
2173
|
+
var MAX_FILE_UPLOAD_BASE64_LENGTH = Math.ceil(MAX_FILE_UPLOAD_BYTES / 3) * 4;
|
|
2174
|
+
var SAFE_FILENAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
2175
|
+
function normalizeBase642(input) {
|
|
2176
|
+
return input.replace(/^data:[^;]+;base64,/i, "").replace(/\s/g, "");
|
|
2177
|
+
}
|
|
2178
|
+
function decodeBase64File(dataBase64) {
|
|
2179
|
+
const normalized = normalizeBase642(dataBase64);
|
|
2180
|
+
if (normalized.length > MAX_FILE_UPLOAD_BASE64_LENGTH) {
|
|
2181
|
+
throw new Error("\u6587\u4EF6\u8D85\u8FC7 100MB \u9650\u5236");
|
|
2182
|
+
}
|
|
2183
|
+
if (!normalized || !/^[A-Za-z0-9+/]*={0,2}$/.test(normalized)) {
|
|
2184
|
+
throw new Error("\u6587\u4EF6\u6570\u636E\u4E0D\u662F\u6709\u6548\u7684 base64");
|
|
2185
|
+
}
|
|
2186
|
+
const buffer = Buffer.from(normalized, "base64");
|
|
2187
|
+
if (buffer.length === 0) throw new Error("\u6587\u4EF6\u6570\u636E\u4E3A\u7A7A");
|
|
2188
|
+
if (buffer.length > MAX_FILE_UPLOAD_BYTES) {
|
|
2189
|
+
throw new Error("\u6587\u4EF6\u8D85\u8FC7 100MB \u9650\u5236");
|
|
2190
|
+
}
|
|
2191
|
+
return buffer;
|
|
2192
|
+
}
|
|
2193
|
+
function sanitizeFileName(fileName, fallbackPrefix, suffix) {
|
|
2194
|
+
const base = basename(fileName).trim();
|
|
2195
|
+
if (base && SAFE_FILENAME_RE.test(base)) return base;
|
|
2196
|
+
const extMatch = base.match(/\.([A-Za-z0-9]{1,6})$/);
|
|
2197
|
+
const ext = extMatch ? `.${extMatch[1]}` : "";
|
|
2198
|
+
return `${fallbackPrefix}-${suffix}${ext}`;
|
|
2199
|
+
}
|
|
2200
|
+
function resolveChildDir2(rootPath, ...segments) {
|
|
2201
|
+
const root = resolve3(rootPath);
|
|
2202
|
+
const target = resolve3(root, ...segments);
|
|
2203
|
+
const rel = relative2(root, target);
|
|
2204
|
+
if (!rel || rel.startsWith("..") || isAbsolute4(rel)) {
|
|
2205
|
+
throw new Error("\u4F1A\u8BDD\u8DEF\u5F84\u65E0\u6548");
|
|
2206
|
+
}
|
|
2207
|
+
return target;
|
|
2208
|
+
}
|
|
2209
|
+
function normalizeGitignoreLine2(line) {
|
|
2210
|
+
return line.trim().replace(/^\/+/, "").replace(/\/+$/, "");
|
|
2211
|
+
}
|
|
2212
|
+
function ensureProjectUploadIgnored(cwd) {
|
|
2213
|
+
const gitignorePath = join6(cwd, ".gitignore");
|
|
2214
|
+
if (!existsSync5(gitignorePath)) return;
|
|
2215
|
+
try {
|
|
2216
|
+
const current = readFileSync6(gitignorePath, "utf-8");
|
|
2217
|
+
const alreadyIgnored = current.split(/\r?\n/).some((line) => normalizeGitignoreLine2(line) === ".dev-anywhere");
|
|
2218
|
+
if (alreadyIgnored) return;
|
|
2219
|
+
const separator = current.length > 0 && !current.endsWith("\n") ? "\n" : "";
|
|
2220
|
+
writeFileSync2(gitignorePath, `${current}${separator}.dev-anywhere/
|
|
2221
|
+
`);
|
|
2222
|
+
} catch {
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
function trySaveProjectUpload(options) {
|
|
2226
|
+
if (!options.cwd) return null;
|
|
2227
|
+
try {
|
|
2228
|
+
const cwd = resolve3(options.cwd);
|
|
2229
|
+
if (!statSync3(cwd).isDirectory()) return null;
|
|
2230
|
+
const uploadsRoot = resolve3(cwd, ".dev-anywhere", "uploads");
|
|
2231
|
+
const uploadDir = resolveChildDir2(uploadsRoot, options.sessionId);
|
|
2232
|
+
const path = join6(uploadDir, options.fileName);
|
|
2233
|
+
mkdirSync2(uploadDir, { recursive: true });
|
|
2234
|
+
writeFileSync2(path, options.buffer, { mode: 384 });
|
|
2235
|
+
ensureProjectUploadIgnored(cwd);
|
|
2236
|
+
return { success: true, path: relative2(cwd, path) };
|
|
2237
|
+
} catch (err) {
|
|
2238
|
+
serviceLogger.warn(
|
|
2239
|
+
{ sessionId: options.sessionId, cwd: options.cwd, error: String(err) },
|
|
2240
|
+
"Project upload write failed; falling back to data dir"
|
|
2241
|
+
);
|
|
2242
|
+
return null;
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
async function saveFileUpload(request, options = {}) {
|
|
2246
|
+
try {
|
|
2247
|
+
const buffer = decodeBase64File(request.dataBase64);
|
|
2248
|
+
const now = options.now ?? Date.now;
|
|
2249
|
+
const suffix = options.randomSuffix?.() ?? nanoid4(6);
|
|
2250
|
+
const stamped = new Date(now()).toISOString().replace(/[-:T.Z]/g, "").slice(0, 14);
|
|
2251
|
+
const fileName = sanitizeFileName(request.fileName, `upload-${stamped}`, suffix);
|
|
2252
|
+
const projectResult = trySaveProjectUpload({
|
|
2253
|
+
cwd: options.cwd,
|
|
2254
|
+
sessionId: request.sessionId,
|
|
2255
|
+
fileName,
|
|
2256
|
+
buffer
|
|
2257
|
+
});
|
|
2258
|
+
if (projectResult) return projectResult;
|
|
2259
|
+
const dataDir = options.dataDir ?? DATA_DIR;
|
|
2260
|
+
const uploadDir = resolveChildDir2(dataDir, request.sessionId, "uploads");
|
|
2261
|
+
const path = join6(uploadDir, fileName);
|
|
2262
|
+
mkdirSync2(uploadDir, { recursive: true });
|
|
2263
|
+
writeFileSync2(path, buffer, { mode: 384 });
|
|
2264
|
+
return { success: true, path };
|
|
2265
|
+
} catch (err) {
|
|
2266
|
+
return {
|
|
2267
|
+
success: false,
|
|
2268
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2269
|
+
errorCode: ControlErrorCode.UNKNOWN
|
|
2270
|
+
};
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
|
|
2274
|
+
// src/serve/image-preview.ts
|
|
2275
|
+
import { readFileSync as readFileSync7, realpathSync as realpathSync2, statSync as statSync4 } from "fs";
|
|
2072
2276
|
import { tmpdir } from "os";
|
|
2073
|
-
import { isAbsolute as
|
|
2277
|
+
import { isAbsolute as isAbsolute5, relative as relative3, resolve as resolve4 } from "path";
|
|
2074
2278
|
var MAX_IMAGE_PREVIEW_BYTES = 10 * 1024 * 1024;
|
|
2075
2279
|
function isInsideRoot(realFilePath, realRootPath) {
|
|
2076
|
-
const rel =
|
|
2077
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
2280
|
+
const rel = relative3(realRootPath, realFilePath);
|
|
2281
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
|
|
2078
2282
|
}
|
|
2079
2283
|
function allowedRoots(options) {
|
|
2080
2284
|
return [options.cwd, options.tmpDir ?? tmpdir(), ...options.previewRoots ?? []].map((root) => root.trim()).filter(Boolean).flatMap((root) => {
|
|
2081
2285
|
try {
|
|
2082
|
-
return [
|
|
2286
|
+
return [realpathSync2(root)];
|
|
2083
2287
|
} catch {
|
|
2084
2288
|
return [];
|
|
2085
2289
|
}
|
|
2086
2290
|
});
|
|
2087
2291
|
}
|
|
2088
2292
|
function resolvePreviewPath(rawPath, options) {
|
|
2089
|
-
const candidate =
|
|
2090
|
-
const realCandidate =
|
|
2293
|
+
const candidate = isAbsolute5(rawPath) ? resolve4(rawPath) : resolve4(options.cwd, rawPath);
|
|
2294
|
+
const realCandidate = realpathSync2(candidate);
|
|
2091
2295
|
if (!allowedRoots(options).some((root) => isInsideRoot(realCandidate, root))) {
|
|
2092
2296
|
throw Object.assign(new Error("\u56FE\u7247\u8DEF\u5F84\u4E0D\u5728\u5141\u8BB8\u9884\u89C8\u7684\u76EE\u5F55\u5185"), {
|
|
2093
2297
|
errorCode: ControlErrorCode.INVALID_PATH
|
|
@@ -2110,7 +2314,7 @@ function detectImageMime(buffer) {
|
|
|
2110
2314
|
}
|
|
2111
2315
|
return void 0;
|
|
2112
2316
|
}
|
|
2113
|
-
function
|
|
2317
|
+
function errorCode2(err) {
|
|
2114
2318
|
if (err instanceof Error && "errorCode" in err && typeof err.errorCode === "string") {
|
|
2115
2319
|
return err.errorCode;
|
|
2116
2320
|
}
|
|
@@ -2119,7 +2323,7 @@ function errorCode(err) {
|
|
|
2119
2323
|
function loadImagePreview(request, options) {
|
|
2120
2324
|
try {
|
|
2121
2325
|
const resolvedPath = resolvePreviewPath(request.path, options);
|
|
2122
|
-
const stat2 =
|
|
2326
|
+
const stat2 = statSync4(resolvedPath);
|
|
2123
2327
|
if (!stat2.isFile()) {
|
|
2124
2328
|
return {
|
|
2125
2329
|
success: false,
|
|
@@ -2139,7 +2343,7 @@ function loadImagePreview(request, options) {
|
|
|
2139
2343
|
errorCode: ControlErrorCode.UNKNOWN
|
|
2140
2344
|
};
|
|
2141
2345
|
}
|
|
2142
|
-
const buffer =
|
|
2346
|
+
const buffer = readFileSync7(resolvedPath);
|
|
2143
2347
|
const mimeType = detectImageMime(buffer);
|
|
2144
2348
|
if (!mimeType) {
|
|
2145
2349
|
return {
|
|
@@ -2164,7 +2368,7 @@ function loadImagePreview(request, options) {
|
|
|
2164
2368
|
sessionId: request.sessionId,
|
|
2165
2369
|
path: request.path,
|
|
2166
2370
|
error: err instanceof Error ? err.message : String(err),
|
|
2167
|
-
errorCode:
|
|
2371
|
+
errorCode: errorCode2(err)
|
|
2168
2372
|
};
|
|
2169
2373
|
}
|
|
2170
2374
|
}
|
|
@@ -2248,7 +2452,6 @@ var RelayInputHandlers = class {
|
|
|
2248
2452
|
requestId,
|
|
2249
2453
|
sessionId,
|
|
2250
2454
|
success: false,
|
|
2251
|
-
path: "",
|
|
2252
2455
|
error: "\u4F1A\u8BDD\u4E0D\u5B58\u5728",
|
|
2253
2456
|
errorCode: ControlErrorCode.SESSION_NOT_FOUND
|
|
2254
2457
|
})
|
|
@@ -2310,7 +2513,92 @@ var RelayInputHandlers = class {
|
|
|
2310
2513
|
...result
|
|
2311
2514
|
})
|
|
2312
2515
|
);
|
|
2313
|
-
|
|
2516
|
+
if (result.success) {
|
|
2517
|
+
serviceLogger.info({ sessionId, path, size: result.size }, "Image preview handled");
|
|
2518
|
+
} else {
|
|
2519
|
+
serviceLogger.warn(
|
|
2520
|
+
{ sessionId, path, errorCode: result.errorCode, error: result.error },
|
|
2521
|
+
"Image preview failed"
|
|
2522
|
+
);
|
|
2523
|
+
}
|
|
2524
|
+
}
|
|
2525
|
+
onFileDownloadRequest(msg) {
|
|
2526
|
+
const { sessionId, requestId, path } = msg;
|
|
2527
|
+
if (!sessionId || !path) return;
|
|
2528
|
+
const session = this.deps.sessionManager.getSession(sessionId);
|
|
2529
|
+
if (!session) {
|
|
2530
|
+
this.deps.relayConnection.sendRaw(
|
|
2531
|
+
serializeControl({
|
|
2532
|
+
type: "file_download_response",
|
|
2533
|
+
requestId,
|
|
2534
|
+
sessionId,
|
|
2535
|
+
success: false,
|
|
2536
|
+
path,
|
|
2537
|
+
error: "\u4F1A\u8BDD\u4E0D\u5B58\u5728",
|
|
2538
|
+
errorCode: ControlErrorCode.SESSION_NOT_FOUND
|
|
2539
|
+
})
|
|
2540
|
+
);
|
|
2541
|
+
serviceLogger.warn({ sessionId }, "File download rejected: session not found");
|
|
2542
|
+
return;
|
|
2543
|
+
}
|
|
2544
|
+
const result = loadFileDownload({ sessionId, path }, { cwd: session.cwd });
|
|
2545
|
+
this.deps.relayConnection.sendRaw(
|
|
2546
|
+
serializeControl({
|
|
2547
|
+
type: "file_download_response",
|
|
2548
|
+
requestId,
|
|
2549
|
+
...result
|
|
2550
|
+
})
|
|
2551
|
+
);
|
|
2552
|
+
if (result.success) {
|
|
2553
|
+
serviceLogger.info(
|
|
2554
|
+
{ sessionId, path, size: result.size },
|
|
2555
|
+
"File download handled"
|
|
2556
|
+
);
|
|
2557
|
+
} else {
|
|
2558
|
+
serviceLogger.warn(
|
|
2559
|
+
{ sessionId, path, errorCode: result.errorCode, error: result.error },
|
|
2560
|
+
"File download failed"
|
|
2561
|
+
);
|
|
2562
|
+
}
|
|
2563
|
+
}
|
|
2564
|
+
async onFileUploadRequest(msg) {
|
|
2565
|
+
const { sessionId, requestId, mimeType, dataBase64, fileName } = msg;
|
|
2566
|
+
if (!sessionId) return;
|
|
2567
|
+
const session = this.deps.sessionManager.getSession(sessionId);
|
|
2568
|
+
if (!session) {
|
|
2569
|
+
this.deps.relayConnection.sendRaw(
|
|
2570
|
+
serializeControl({
|
|
2571
|
+
type: "file_upload_response",
|
|
2572
|
+
requestId,
|
|
2573
|
+
sessionId,
|
|
2574
|
+
success: false,
|
|
2575
|
+
error: "\u4F1A\u8BDD\u4E0D\u5B58\u5728",
|
|
2576
|
+
errorCode: ControlErrorCode.SESSION_NOT_FOUND
|
|
2577
|
+
})
|
|
2578
|
+
);
|
|
2579
|
+
serviceLogger.warn({ sessionId }, "File upload rejected: session not found");
|
|
2580
|
+
return;
|
|
2581
|
+
}
|
|
2582
|
+
const result = await saveFileUpload(
|
|
2583
|
+
{ sessionId, mimeType, dataBase64, fileName },
|
|
2584
|
+
{ cwd: session.cwd }
|
|
2585
|
+
);
|
|
2586
|
+
this.deps.relayConnection.sendRaw(
|
|
2587
|
+
serializeControl({
|
|
2588
|
+
type: "file_upload_response",
|
|
2589
|
+
requestId,
|
|
2590
|
+
sessionId,
|
|
2591
|
+
...result
|
|
2592
|
+
})
|
|
2593
|
+
);
|
|
2594
|
+
if (result.success) {
|
|
2595
|
+
serviceLogger.info({ sessionId, fileName, path: result.path }, "File upload handled");
|
|
2596
|
+
} else {
|
|
2597
|
+
serviceLogger.warn(
|
|
2598
|
+
{ sessionId, fileName, errorCode: result.errorCode, error: result.error },
|
|
2599
|
+
"File upload failed"
|
|
2600
|
+
);
|
|
2601
|
+
}
|
|
2314
2602
|
}
|
|
2315
2603
|
};
|
|
2316
2604
|
|
|
@@ -2505,14 +2793,14 @@ var RelayPermissionHandlers = class {
|
|
|
2505
2793
|
|
|
2506
2794
|
// src/serve/relay-resource-handlers.ts
|
|
2507
2795
|
import { homedir as homedir4 } from "os";
|
|
2508
|
-
import { accessSync, constants, statSync as
|
|
2796
|
+
import { accessSync, constants, statSync as statSync5 } from "fs";
|
|
2509
2797
|
function errorMessage(err) {
|
|
2510
2798
|
return err instanceof Error ? err.message : String(err);
|
|
2511
2799
|
}
|
|
2512
2800
|
function validateExecutablePath(path) {
|
|
2513
2801
|
const normalized = path.trim();
|
|
2514
2802
|
if (!normalized.startsWith("/")) throw new Error("CLI \u8DEF\u5F84\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84");
|
|
2515
|
-
const stat2 =
|
|
2803
|
+
const stat2 = statSync5(normalized);
|
|
2516
2804
|
if (!stat2.isFile()) throw new Error("CLI \u8DEF\u5F84\u4E0D\u662F\u53EF\u6267\u884C\u6587\u4EF6");
|
|
2517
2805
|
accessSync(normalized, constants.X_OK);
|
|
2518
2806
|
return normalized;
|
|
@@ -2620,9 +2908,9 @@ var RelayResourceHandlers = class {
|
|
|
2620
2908
|
};
|
|
2621
2909
|
|
|
2622
2910
|
// src/serve/relay-session-create-handler.ts
|
|
2623
|
-
import { rmSync, statSync as
|
|
2624
|
-
import { isAbsolute as
|
|
2625
|
-
import { nanoid as
|
|
2911
|
+
import { rmSync, statSync as statSync6 } from "fs";
|
|
2912
|
+
import { isAbsolute as isAbsolute6 } from "path";
|
|
2913
|
+
import { nanoid as nanoid5 } from "nanoid";
|
|
2626
2914
|
|
|
2627
2915
|
// src/serve/hosted-pty-registry.ts
|
|
2628
2916
|
import * as pty from "node-pty";
|
|
@@ -2873,11 +3161,11 @@ function validateSessionCwd(cwd) {
|
|
|
2873
3161
|
return { message: "\u8BF7\u8F93\u5165\u5DE5\u4F5C\u76EE\u5F55", code: ControlErrorCode.INVALID_PATH };
|
|
2874
3162
|
}
|
|
2875
3163
|
const trimmed = cwd.trim();
|
|
2876
|
-
if (!
|
|
3164
|
+
if (!isAbsolute6(trimmed)) {
|
|
2877
3165
|
return { message: "\u5DE5\u4F5C\u76EE\u5F55\u5FC5\u987B\u662F\u7EDD\u5BF9\u8DEF\u5F84", code: ControlErrorCode.INVALID_PATH };
|
|
2878
3166
|
}
|
|
2879
3167
|
try {
|
|
2880
|
-
const stat2 =
|
|
3168
|
+
const stat2 = statSync6(trimmed);
|
|
2881
3169
|
return stat2.isDirectory() ? null : { message: "\u5DE5\u4F5C\u76EE\u5F55\u4E0D\u662F\u76EE\u5F55", code: ControlErrorCode.PATH_NOT_DIRECTORY };
|
|
2882
3170
|
} catch (err) {
|
|
2883
3171
|
return {
|
|
@@ -2940,7 +3228,7 @@ var RelaySessionCreateHandler = class {
|
|
|
2940
3228
|
const resumeSessionId = msg.resumeSessionId;
|
|
2941
3229
|
const streamDelta = false;
|
|
2942
3230
|
const name = tildify(sessionCwd);
|
|
2943
|
-
const pendingId =
|
|
3231
|
+
const pendingId = nanoid5();
|
|
2944
3232
|
const hook = this.deps.createHookContext(pendingId, provider);
|
|
2945
3233
|
const workerPid = this.deps.workerRegistry.spawn(pendingId, {
|
|
2946
3234
|
cwd: sessionCwd,
|
|
@@ -3033,7 +3321,7 @@ var RelaySessionCreateHandler = class {
|
|
|
3033
3321
|
return;
|
|
3034
3322
|
}
|
|
3035
3323
|
const resumeSessionId = msg.resumeSessionId;
|
|
3036
|
-
const pendingId =
|
|
3324
|
+
const pendingId = nanoid5();
|
|
3037
3325
|
const name = tildify(cwd);
|
|
3038
3326
|
const hook = this.deps.createHookContext(pendingId, provider);
|
|
3039
3327
|
try {
|
|
@@ -3226,6 +3514,12 @@ var RelayRouter = class {
|
|
|
3226
3514
|
case "image_preview_request":
|
|
3227
3515
|
this.inputHandlers.onImagePreviewRequest(msg);
|
|
3228
3516
|
return;
|
|
3517
|
+
case "file_download_request":
|
|
3518
|
+
this.inputHandlers.onFileDownloadRequest(msg);
|
|
3519
|
+
return;
|
|
3520
|
+
case "file_upload_request":
|
|
3521
|
+
void this.inputHandlers.onFileUploadRequest(msg);
|
|
3522
|
+
return;
|
|
3229
3523
|
case "tool_approve":
|
|
3230
3524
|
this.permissionHandlers.onToolApprove(msg);
|
|
3231
3525
|
return;
|
|
@@ -3468,11 +3762,11 @@ var PermissionBroker = class {
|
|
|
3468
3762
|
if (this.pending.has(request.requestId)) {
|
|
3469
3763
|
return Promise.resolve(DUPLICATE_DECISION);
|
|
3470
3764
|
}
|
|
3471
|
-
return new Promise((
|
|
3765
|
+
return new Promise((resolve5) => {
|
|
3472
3766
|
this.pending.set(request.requestId, {
|
|
3473
3767
|
...request,
|
|
3474
3768
|
source: "hook",
|
|
3475
|
-
resolve:
|
|
3769
|
+
resolve: resolve5,
|
|
3476
3770
|
createdAt: Date.now()
|
|
3477
3771
|
});
|
|
3478
3772
|
});
|
|
@@ -3811,11 +4105,32 @@ function createEventBridge(deps) {
|
|
|
3811
4105
|
deps.relayConnection.sendRaw(serializeControl({ type: "agent_status", sessionId, payload }));
|
|
3812
4106
|
};
|
|
3813
4107
|
const cleanupSessionResources = (sessionId) => {
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
4108
|
+
const safe = (fn, step) => {
|
|
4109
|
+
try {
|
|
4110
|
+
fn();
|
|
4111
|
+
} catch (err) {
|
|
4112
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
4113
|
+
serviceLogger.warn(
|
|
4114
|
+
{
|
|
4115
|
+
sessionId,
|
|
4116
|
+
step,
|
|
4117
|
+
err: { message: error.message, stack: error.stack, cause: error.cause }
|
|
4118
|
+
},
|
|
4119
|
+
"Session cleanup step failed; continuing"
|
|
4120
|
+
);
|
|
4121
|
+
}
|
|
4122
|
+
};
|
|
4123
|
+
safe(() => deps.controlHandlers.cleanup(sessionId), "controlHandlers.cleanup");
|
|
4124
|
+
safe(() => deps.agentStatusRegistry.delete(sessionId), "agentStatusRegistry.delete");
|
|
4125
|
+
safe(() => disposeSeqCounter(sessionId), "disposeSeqCounter");
|
|
4126
|
+
safe(
|
|
4127
|
+
() => deps.permissionBroker.cleanupSession(sessionId, "Session closed"),
|
|
4128
|
+
"permissionBroker.cleanupSession"
|
|
4129
|
+
);
|
|
4130
|
+
safe(
|
|
4131
|
+
() => broadcastSessionList(deps.relayConnection, deps.sessionManager),
|
|
4132
|
+
"broadcastSessionList"
|
|
4133
|
+
);
|
|
3819
4134
|
};
|
|
3820
4135
|
return {
|
|
3821
4136
|
changeSessionState: changeState,
|
|
@@ -3827,14 +4142,14 @@ function createEventBridge(deps) {
|
|
|
3827
4142
|
|
|
3828
4143
|
// src/serve/service-files.ts
|
|
3829
4144
|
import { execSync } from "child_process";
|
|
3830
|
-
import { existsSync as
|
|
4145
|
+
import { existsSync as existsSync6, readFileSync as readFileSync8, unlinkSync as unlinkSync2 } from "fs";
|
|
3831
4146
|
import { hostname } from "os";
|
|
3832
4147
|
import { connect as connect2 } from "net";
|
|
3833
4148
|
function tryConnectSocket(sockPath) {
|
|
3834
|
-
return new Promise((
|
|
4149
|
+
return new Promise((resolve5) => {
|
|
3835
4150
|
const s = connect2(sockPath);
|
|
3836
|
-
s.on("connect", () =>
|
|
3837
|
-
s.on("error", () =>
|
|
4151
|
+
s.on("connect", () => resolve5(s));
|
|
4152
|
+
s.on("error", () => resolve5(null));
|
|
3838
4153
|
});
|
|
3839
4154
|
}
|
|
3840
4155
|
function isProcessAlive(pid) {
|
|
@@ -3846,7 +4161,7 @@ function isProcessAlive(pid) {
|
|
|
3846
4161
|
}
|
|
3847
4162
|
}
|
|
3848
4163
|
async function cleanupStaleResources() {
|
|
3849
|
-
if (
|
|
4164
|
+
if (existsSync6(SOCK_PATH)) {
|
|
3850
4165
|
const existing = await tryConnectSocket(SOCK_PATH);
|
|
3851
4166
|
if (existing) {
|
|
3852
4167
|
existing.destroy();
|
|
@@ -3859,8 +4174,8 @@ async function cleanupStaleResources() {
|
|
|
3859
4174
|
unlinkSync2(SOCK_PATH);
|
|
3860
4175
|
serviceLogger.info("Removed stale socket file");
|
|
3861
4176
|
}
|
|
3862
|
-
if (
|
|
3863
|
-
const pidStr =
|
|
4177
|
+
if (existsSync6(PID_PATH)) {
|
|
4178
|
+
const pidStr = readFileSync8(PID_PATH, "utf-8").trim();
|
|
3864
4179
|
const pid = parseInt(pidStr, 10);
|
|
3865
4180
|
if (!isNaN(pid) && isProcessAlive(pid)) {
|
|
3866
4181
|
const msg = `Another service is already running with PID ${pid}`;
|
|
@@ -4141,8 +4456,14 @@ function handleTerminalConnection(socket, deps) {
|
|
|
4141
4456
|
relayConnection.sendBinary(encodeBinaryFrame(sessionId, outputSeq, data));
|
|
4142
4457
|
},
|
|
4143
4458
|
(err, line) => {
|
|
4459
|
+
const cause = err instanceof Error ? err.cause : void 0;
|
|
4144
4460
|
serviceLogger.warn(
|
|
4145
|
-
{
|
|
4461
|
+
{
|
|
4462
|
+
err: err.message,
|
|
4463
|
+
cause: cause instanceof Error ? cause.message : cause,
|
|
4464
|
+
lineLen: line.length,
|
|
4465
|
+
linePreview: line.slice(0, 200)
|
|
4466
|
+
},
|
|
4146
4467
|
"Terminal IPC message dropped (parse/schema error)"
|
|
4147
4468
|
);
|
|
4148
4469
|
}
|
|
@@ -4186,7 +4507,7 @@ function handleTerminalConnection(socket, deps) {
|
|
|
4186
4507
|
|
|
4187
4508
|
// src/serve/hook-registry.ts
|
|
4188
4509
|
import { createHash, randomBytes } from "crypto";
|
|
4189
|
-
import { existsSync as
|
|
4510
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync9, renameSync, writeFileSync as writeFileSync3 } from "fs";
|
|
4190
4511
|
import { dirname } from "path";
|
|
4191
4512
|
import { z } from "zod";
|
|
4192
4513
|
var PersistedHookSessionBindingSchema = z.object({
|
|
@@ -4247,10 +4568,10 @@ var HookRegistry = class {
|
|
|
4247
4568
|
}
|
|
4248
4569
|
}
|
|
4249
4570
|
load() {
|
|
4250
|
-
if (!this.persistPath || !
|
|
4571
|
+
if (!this.persistPath || !existsSync7(this.persistPath)) return;
|
|
4251
4572
|
try {
|
|
4252
4573
|
const parsed = PersistedHookRegistrySchema.parse(
|
|
4253
|
-
JSON.parse(
|
|
4574
|
+
JSON.parse(readFileSync9(this.persistPath, "utf8"))
|
|
4254
4575
|
);
|
|
4255
4576
|
this.bindingsBySession.clear();
|
|
4256
4577
|
for (const binding of parsed.bindings) {
|
|
@@ -4266,9 +4587,9 @@ var HookRegistry = class {
|
|
|
4266
4587
|
save() {
|
|
4267
4588
|
if (!this.persistPath) return;
|
|
4268
4589
|
try {
|
|
4269
|
-
|
|
4590
|
+
mkdirSync3(dirname(this.persistPath), { recursive: true });
|
|
4270
4591
|
const tmpPath = `${this.persistPath}.${process.pid}.${Date.now()}.tmp`;
|
|
4271
|
-
|
|
4592
|
+
writeFileSync3(
|
|
4272
4593
|
tmpPath,
|
|
4273
4594
|
JSON.stringify(
|
|
4274
4595
|
{
|
|
@@ -4314,7 +4635,7 @@ var HookServer = class {
|
|
|
4314
4635
|
this.writeJson(res, 500, { error: "internal_error" });
|
|
4315
4636
|
});
|
|
4316
4637
|
});
|
|
4317
|
-
return new Promise((
|
|
4638
|
+
return new Promise((resolve5, reject) => {
|
|
4318
4639
|
const onError = (err) => {
|
|
4319
4640
|
this.server?.off("listening", onListening);
|
|
4320
4641
|
reject(err);
|
|
@@ -4322,7 +4643,7 @@ var HookServer = class {
|
|
|
4322
4643
|
const onListening = () => {
|
|
4323
4644
|
this.server?.off("error", onError);
|
|
4324
4645
|
serviceLogger.info({ host: this.host, port: this.options.port }, "Hook server listening");
|
|
4325
|
-
|
|
4646
|
+
resolve5();
|
|
4326
4647
|
};
|
|
4327
4648
|
this.server.once("error", onError);
|
|
4328
4649
|
this.server.once("listening", onListening);
|
|
@@ -4333,8 +4654,8 @@ var HookServer = class {
|
|
|
4333
4654
|
if (!this.server) return Promise.resolve();
|
|
4334
4655
|
const server = this.server;
|
|
4335
4656
|
this.server = null;
|
|
4336
|
-
return new Promise((
|
|
4337
|
-
server.close((err) => err ? reject(err) :
|
|
4657
|
+
return new Promise((resolve5, reject) => {
|
|
4658
|
+
server.close((err) => err ? reject(err) : resolve5());
|
|
4338
4659
|
});
|
|
4339
4660
|
}
|
|
4340
4661
|
getListeningPort() {
|
|
@@ -4448,7 +4769,7 @@ var HookServer = class {
|
|
|
4448
4769
|
this.writeJson(res, 200, payload);
|
|
4449
4770
|
}
|
|
4450
4771
|
readBody(req) {
|
|
4451
|
-
return new Promise((
|
|
4772
|
+
return new Promise((resolve5, reject) => {
|
|
4452
4773
|
let body = "";
|
|
4453
4774
|
let size = 0;
|
|
4454
4775
|
req.setEncoding("utf8");
|
|
@@ -4461,7 +4782,7 @@ var HookServer = class {
|
|
|
4461
4782
|
}
|
|
4462
4783
|
body += chunk;
|
|
4463
4784
|
});
|
|
4464
|
-
req.on("end", () =>
|
|
4785
|
+
req.on("end", () => resolve5(body));
|
|
4465
4786
|
req.on("error", reject);
|
|
4466
4787
|
});
|
|
4467
4788
|
}
|
|
@@ -4808,7 +5129,7 @@ async function startService(options) {
|
|
|
4808
5129
|
});
|
|
4809
5130
|
});
|
|
4810
5131
|
server.listen(SOCK_PATH, () => {
|
|
4811
|
-
|
|
5132
|
+
writeFileSync4(PID_PATH, String(process.pid));
|
|
4812
5133
|
chmodSync(SOCK_PATH, 384);
|
|
4813
5134
|
serviceLogger.info({ pid: process.pid, sock: SOCK_PATH }, "Service started");
|
|
4814
5135
|
});
|