diff --git a/canvas-agent/src/modules/depth-estimation-http.ts b/canvas-agent/src/modules/depth-estimation-http.ts index 8005ad32b..6496db5c4 100644 --- a/canvas-agent/src/modules/depth-estimation-http.ts +++ b/canvas-agent/src/modules/depth-estimation-http.ts @@ -6,6 +6,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import type { Request, RequestHandler, Response } from "express"; import type { LocalRuntimeModule } from "../local-runtime.js"; +import { resolveEnvPath } from "./runtime-env-path.js"; const DEFAULT_MODEL_ID = "depth-anything/Depth-Anything-V2-Small-hf"; const MAX_INPUT_BYTES = 12 * 1024 * 1024; @@ -103,12 +104,12 @@ export function resolveDepthRuntimePaths(env: NodeJS.ProcessEnv = process.env): const moduleRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const configuredProjectRoot = env.CANVAS_PROJECT_ROOT?.trim(); const projectRoots = [ - configuredProjectRoot ? path.resolve(configuredProjectRoot) : undefined, + configuredProjectRoot ? resolveEnvPath(configuredProjectRoot) : undefined, path.resolve(moduleRoot, ".."), path.resolve(process.cwd()), ].filter((value): value is string => Boolean(value)); const projectRoot = configuredProjectRoot - ? path.resolve(configuredProjectRoot) + ? resolveEnvPath(configuredProjectRoot) : projectRoots.find((value) => fs.existsSync(path.join(value, ".local"))) || projectRoots[0]!; const scriptCandidates = [ path.join(moduleRoot, "python", "depth_anything_runtime.py"), @@ -116,12 +117,12 @@ export function resolveDepthRuntimePaths(env: NodeJS.ProcessEnv = process.env): ]; const scriptPath = scriptCandidates.find((value) => fs.existsSync(value)) || scriptCandidates[0]!; const pythonCandidates = env.CANVAS_DEPTH_PYTHON?.trim() - ? [path.resolve(env.CANVAS_DEPTH_PYTHON.trim())] + ? [resolveEnvPath(env.CANVAS_DEPTH_PYTHON)] : process.platform === "win32" ? [path.join(projectRoot, ".local", "depth-anything-v2", "venv", "Scripts", "python.exe")] : [path.join(projectRoot, ".local", "depth-anything-v2", "venv", "bin", "python")]; const pythonPath = pythonCandidates[0]!; - const hfHome = path.resolve(env.CANVAS_DEPTH_HF_HOME?.trim() || path.join(projectRoot, ".local", "cache", "huggingface")); + const hfHome = resolveEnvPath(env.CANVAS_DEPTH_HF_HOME?.trim() || path.join(projectRoot, ".local", "cache", "huggingface")); const modelId = env.CANVAS_DEPTH_MODEL_ID?.trim() || DEFAULT_MODEL_ID; const modelCacheDir = path.join(hfHome, "hub", `models--${modelId.replaceAll("/", "--")}`); const workerExists = fs.existsSync(scriptPath); diff --git a/canvas-agent/src/modules/lineart-estimation-http.ts b/canvas-agent/src/modules/lineart-estimation-http.ts index d6fbee882..c8bd506f4 100644 --- a/canvas-agent/src/modules/lineart-estimation-http.ts +++ b/canvas-agent/src/modules/lineart-estimation-http.ts @@ -6,6 +6,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import type { Request, RequestHandler, Response } from "express"; import type { LocalRuntimeModule } from "../local-runtime.js"; +import { resolveEnvPath } from "./runtime-env-path.js"; const DEFAULT_MODEL_ID = "lllyasviel/Annotators"; const MAX_INPUT_BYTES = 12 * 1024 * 1024; @@ -103,12 +104,12 @@ export function resolveLineartRuntimePaths(env: NodeJS.ProcessEnv = process.env) const moduleRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const configuredProjectRoot = env.CANVAS_PROJECT_ROOT?.trim(); const projectRoots = [ - configuredProjectRoot ? path.resolve(configuredProjectRoot) : undefined, + configuredProjectRoot ? resolveEnvPath(configuredProjectRoot) : undefined, path.resolve(moduleRoot, ".."), path.resolve(process.cwd()), ].filter((value): value is string => Boolean(value)); const projectRoot = configuredProjectRoot - ? path.resolve(configuredProjectRoot) + ? resolveEnvPath(configuredProjectRoot) : projectRoots.find((value) => fs.existsSync(path.join(value, ".local"))) || projectRoots[0]!; const scriptCandidates = [ path.join(moduleRoot, "python", "lineart_runtime.py"), @@ -116,12 +117,12 @@ export function resolveLineartRuntimePaths(env: NodeJS.ProcessEnv = process.env) ]; const scriptPath = scriptCandidates.find((value) => fs.existsSync(value)) || scriptCandidates[0]!; const pythonCandidates = env.CANVAS_DEPTH_PYTHON?.trim() - ? [path.resolve(env.CANVAS_DEPTH_PYTHON.trim())] + ? [resolveEnvPath(env.CANVAS_DEPTH_PYTHON)] : process.platform === "win32" ? [path.join(projectRoot, ".local", "depth-anything-v2", "venv", "Scripts", "python.exe")] : [path.join(projectRoot, ".local", "depth-anything-v2", "venv", "bin", "python")]; const pythonPath = pythonCandidates[0]!; - const hfHome = path.resolve(env.CANVAS_LINEART_HF_HOME?.trim() || env.CANVAS_DEPTH_HF_HOME?.trim() || path.join(projectRoot, ".local", "cache", "huggingface")); + const hfHome = resolveEnvPath(env.CANVAS_LINEART_HF_HOME?.trim() || env.CANVAS_DEPTH_HF_HOME?.trim() || path.join(projectRoot, ".local", "cache", "huggingface")); const modelId = env.CANVAS_LINEART_MODEL_ID?.trim() || DEFAULT_MODEL_ID; const modelCacheDir = path.join(hfHome, "hub", `models--${modelId.replaceAll("/", "--")}`); const workerExists = fs.existsSync(scriptPath); diff --git a/canvas-agent/src/modules/pose-estimation-http.ts b/canvas-agent/src/modules/pose-estimation-http.ts index 972961d64..181667a89 100644 --- a/canvas-agent/src/modules/pose-estimation-http.ts +++ b/canvas-agent/src/modules/pose-estimation-http.ts @@ -6,6 +6,7 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import type { Request, RequestHandler, Response } from "express"; import type { LocalRuntimeModule } from "../local-runtime.js"; +import { resolveEnvPath } from "./runtime-env-path.js"; const DEFAULT_MODEL_ID = "lllyasviel/Annotators"; const DEFAULT_BACKEND = "openpose-body" as const; @@ -108,12 +109,12 @@ export function resolvePoseRuntimePaths(env: NodeJS.ProcessEnv = process.env): P const moduleRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); const configuredProjectRoot = env.CANVAS_PROJECT_ROOT?.trim(); const projectRoots = [ - configuredProjectRoot ? path.resolve(configuredProjectRoot) : undefined, + configuredProjectRoot ? resolveEnvPath(configuredProjectRoot) : undefined, path.resolve(moduleRoot, ".."), path.resolve(process.cwd()), ].filter((value): value is string => Boolean(value)); const projectRoot = configuredProjectRoot - ? path.resolve(configuredProjectRoot) + ? resolveEnvPath(configuredProjectRoot) : projectRoots.find((value) => fs.existsSync(path.join(value, ".local"))) || projectRoots[0]!; const scriptCandidates = [ path.join(moduleRoot, "python", "pose_runtime.py"), @@ -121,12 +122,12 @@ export function resolvePoseRuntimePaths(env: NodeJS.ProcessEnv = process.env): P ]; const scriptPath = scriptCandidates.find((value) => fs.existsSync(value)) || scriptCandidates[0]!; const pythonCandidates = env.CANVAS_POSE_PYTHON?.trim() - ? [path.resolve(env.CANVAS_POSE_PYTHON.trim())] + ? [resolveEnvPath(env.CANVAS_POSE_PYTHON)] : process.platform === "win32" ? [path.join(projectRoot, ".local", "depth-anything-v2", "venv", "Scripts", "python.exe")] : [path.join(projectRoot, ".local", "depth-anything-v2", "venv", "bin", "python")]; const pythonPath = pythonCandidates[0]!; - const hfHome = path.resolve( + const hfHome = resolveEnvPath( env.CANVAS_POSE_HF_HOME?.trim() || env.CANVAS_LINEART_HF_HOME?.trim() || env.CANVAS_DEPTH_HF_HOME?.trim() @@ -278,7 +279,7 @@ export class PoseEstimationWorker { || typeof value.modelId !== "string" || value.device !== "cpu" || value.backend !== DEFAULT_BACKEND - || !isPositiveInteger(value.personCount) + || !isNonNegativeInteger(value.personCount) || typeof value.pngBase64 !== "string" || !value.pngBase64) { pending.reject(new PoseRuntimeError("pose_response_invalid", "本机姿态响应无效", 500)); @@ -355,3 +356,7 @@ function isRecord(value: unknown): value is Record { function isPositiveInteger(value: unknown): value is number { return typeof value === "number" && Number.isInteger(value) && value > 0; } + +function isNonNegativeInteger(value: unknown): value is number { + return typeof value === "number" && Number.isInteger(value) && value >= 0; +} diff --git a/canvas-agent/src/modules/runtime-env-path.ts b/canvas-agent/src/modules/runtime-env-path.ts new file mode 100644 index 000000000..b324cb601 --- /dev/null +++ b/canvas-agent/src/modules/runtime-env-path.ts @@ -0,0 +1,12 @@ +import path from "node:path"; + +/** + * Resolve env-configured filesystem paths without mangling Windows absolutes on POSIX hosts. + * `path.resolve("C:\\foo")` on macOS/Linux incorrectly joins cwd, which breaks overrides and tests. + */ +export function resolveEnvPath(value: string): string { + const trimmed = value.trim(); + if (!trimmed) return trimmed; + if (/^[A-Za-z]:[\\/]/.test(trimmed) || trimmed.startsWith("\\\\")) return trimmed; + return path.resolve(trimmed); +} diff --git a/web/src/services/pose-runtime.ts b/web/src/services/pose-runtime.ts index c5c53c47b..bcfc26991 100644 --- a/web/src/services/pose-runtime.ts +++ b/web/src/services/pose-runtime.ts @@ -144,7 +144,7 @@ async function boundedText(response: Response, limit: number) { } function parseResult(value: Record): LocalPoseResult { - if (value.mimeType !== "image/png" || !isPositiveInteger(value.width) || !isPositiveInteger(value.height) || typeof value.modelId !== "string" || value.device !== "cpu" || value.backend !== "openpose-body" || !isPositiveInteger(value.personCount) || typeof value.pngBase64 !== "string") { + if (value.mimeType !== "image/png" || !isPositiveInteger(value.width) || !isPositiveInteger(value.height) || typeof value.modelId !== "string" || value.device !== "cpu" || value.backend !== "openpose-body" || !isNonNegativeInteger(value.personCount) || typeof value.pngBase64 !== "string") { throw new LocalRuntimeClientError("runtime_response_invalid", "本机姿态结果无效"); } const bytes = bytesFromBase64(value.pngBase64, "runtime_response_invalid", "本机姿态结果无效"); @@ -207,3 +207,7 @@ function isRecord(value: unknown): value is Record { function isPositiveInteger(value: unknown): value is number { return typeof value === "number" && Number.isInteger(value) && value > 0; } + +function isNonNegativeInteger(value: unknown): value is number { + return typeof value === "number" && Number.isInteger(value) && value >= 0; +}