fix(runtime): 本机估计运行时 - 兼容 Windows 路径覆盖并允许零人数姿态结果

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ddCat
2026-09-09 09:46:18 +08:00
parent 6abd2a7e0e
commit d79b323af0
5 changed files with 37 additions and 14 deletions

View File

@@ -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);

View File

@@ -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);

View File

@@ -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<string, unknown> {
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;
}

View File

@@ -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);
}

View File

@@ -144,7 +144,7 @@ async function boundedText(response: Response, limit: number) {
}
function parseResult(value: Record<string, unknown>): 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<string, unknown> {
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;
}