evomeme 0.1.7 → 0.1.9
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/canvas-public/app.js +1759 -1496
- package/dist/canvas-public/index.html +43 -99
- package/dist/canvas-public/smart-split.js +1 -1
- package/dist/canvas-public/styles.css +349 -378
- package/dist/index.js +1111 -507
- package/package.json +1 -1
- package/skills/evomeme-canvas/SKILL.md +52 -0
- package/skills/evomeme-canvas/agents/openai.yaml +7 -0
- package/skills/evomeme-game-maker/SKILL.md +83 -12
- package/dist/canvas-public/sequence-sheet.js +0 -59
package/dist/index.js
CHANGED
|
@@ -3234,8 +3234,8 @@ var require_utils = __commonJS({
|
|
|
3234
3234
|
}
|
|
3235
3235
|
return ind;
|
|
3236
3236
|
}
|
|
3237
|
-
function removeDotSegments(
|
|
3238
|
-
let input =
|
|
3237
|
+
function removeDotSegments(path26) {
|
|
3238
|
+
let input = path26;
|
|
3239
3239
|
const output = [];
|
|
3240
3240
|
let nextSlash = -1;
|
|
3241
3241
|
let len = 0;
|
|
@@ -3487,8 +3487,8 @@ var require_schemes = __commonJS({
|
|
|
3487
3487
|
wsComponent.secure = void 0;
|
|
3488
3488
|
}
|
|
3489
3489
|
if (wsComponent.resourceName) {
|
|
3490
|
-
const [
|
|
3491
|
-
wsComponent.path =
|
|
3490
|
+
const [path26, query] = wsComponent.resourceName.split("?");
|
|
3491
|
+
wsComponent.path = path26 && path26 !== "/" ? path26 : void 0;
|
|
3492
3492
|
wsComponent.query = query;
|
|
3493
3493
|
wsComponent.resourceName = void 0;
|
|
3494
3494
|
}
|
|
@@ -6906,7 +6906,7 @@ var require_windows = __commonJS({
|
|
|
6906
6906
|
module.exports = isexe;
|
|
6907
6907
|
isexe.sync = sync;
|
|
6908
6908
|
var fs14 = __require("fs");
|
|
6909
|
-
function checkPathExt(
|
|
6909
|
+
function checkPathExt(path26, options) {
|
|
6910
6910
|
var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
|
|
6911
6911
|
if (!pathext) {
|
|
6912
6912
|
return true;
|
|
@@ -6917,25 +6917,25 @@ var require_windows = __commonJS({
|
|
|
6917
6917
|
}
|
|
6918
6918
|
for (var i = 0; i < pathext.length; i++) {
|
|
6919
6919
|
var p = pathext[i].toLowerCase();
|
|
6920
|
-
if (p &&
|
|
6920
|
+
if (p && path26.substr(-p.length).toLowerCase() === p) {
|
|
6921
6921
|
return true;
|
|
6922
6922
|
}
|
|
6923
6923
|
}
|
|
6924
6924
|
return false;
|
|
6925
6925
|
}
|
|
6926
|
-
function checkStat(stat7,
|
|
6926
|
+
function checkStat(stat7, path26, options) {
|
|
6927
6927
|
if (!stat7.isSymbolicLink() && !stat7.isFile()) {
|
|
6928
6928
|
return false;
|
|
6929
6929
|
}
|
|
6930
|
-
return checkPathExt(
|
|
6930
|
+
return checkPathExt(path26, options);
|
|
6931
6931
|
}
|
|
6932
|
-
function isexe(
|
|
6933
|
-
fs14.stat(
|
|
6934
|
-
cb(er, er ? false : checkStat(stat7,
|
|
6932
|
+
function isexe(path26, options, cb) {
|
|
6933
|
+
fs14.stat(path26, function(er, stat7) {
|
|
6934
|
+
cb(er, er ? false : checkStat(stat7, path26, options));
|
|
6935
6935
|
});
|
|
6936
6936
|
}
|
|
6937
|
-
function sync(
|
|
6938
|
-
return checkStat(fs14.statSync(
|
|
6937
|
+
function sync(path26, options) {
|
|
6938
|
+
return checkStat(fs14.statSync(path26), path26, options);
|
|
6939
6939
|
}
|
|
6940
6940
|
}
|
|
6941
6941
|
});
|
|
@@ -6946,13 +6946,13 @@ var require_mode = __commonJS({
|
|
|
6946
6946
|
module.exports = isexe;
|
|
6947
6947
|
isexe.sync = sync;
|
|
6948
6948
|
var fs14 = __require("fs");
|
|
6949
|
-
function isexe(
|
|
6950
|
-
fs14.stat(
|
|
6949
|
+
function isexe(path26, options, cb) {
|
|
6950
|
+
fs14.stat(path26, function(er, stat7) {
|
|
6951
6951
|
cb(er, er ? false : checkStat(stat7, options));
|
|
6952
6952
|
});
|
|
6953
6953
|
}
|
|
6954
|
-
function sync(
|
|
6955
|
-
return checkStat(fs14.statSync(
|
|
6954
|
+
function sync(path26, options) {
|
|
6955
|
+
return checkStat(fs14.statSync(path26), options);
|
|
6956
6956
|
}
|
|
6957
6957
|
function checkStat(stat7, options) {
|
|
6958
6958
|
return stat7.isFile() && checkMode(stat7, options);
|
|
@@ -6985,7 +6985,7 @@ var require_isexe = __commonJS({
|
|
|
6985
6985
|
}
|
|
6986
6986
|
module.exports = isexe;
|
|
6987
6987
|
isexe.sync = sync;
|
|
6988
|
-
function isexe(
|
|
6988
|
+
function isexe(path26, options, cb) {
|
|
6989
6989
|
if (typeof options === "function") {
|
|
6990
6990
|
cb = options;
|
|
6991
6991
|
options = {};
|
|
@@ -6995,7 +6995,7 @@ var require_isexe = __commonJS({
|
|
|
6995
6995
|
throw new TypeError("callback not provided");
|
|
6996
6996
|
}
|
|
6997
6997
|
return new Promise(function(resolve, reject) {
|
|
6998
|
-
isexe(
|
|
6998
|
+
isexe(path26, options || {}, function(er, is) {
|
|
6999
6999
|
if (er) {
|
|
7000
7000
|
reject(er);
|
|
7001
7001
|
} else {
|
|
@@ -7004,7 +7004,7 @@ var require_isexe = __commonJS({
|
|
|
7004
7004
|
});
|
|
7005
7005
|
});
|
|
7006
7006
|
}
|
|
7007
|
-
core(
|
|
7007
|
+
core(path26, options || {}, function(er, is) {
|
|
7008
7008
|
if (er) {
|
|
7009
7009
|
if (er.code === "EACCES" || options && options.ignoreErrors) {
|
|
7010
7010
|
er = null;
|
|
@@ -7014,9 +7014,9 @@ var require_isexe = __commonJS({
|
|
|
7014
7014
|
cb(er, is);
|
|
7015
7015
|
});
|
|
7016
7016
|
}
|
|
7017
|
-
function sync(
|
|
7017
|
+
function sync(path26, options) {
|
|
7018
7018
|
try {
|
|
7019
|
-
return core.sync(
|
|
7019
|
+
return core.sync(path26, options || {});
|
|
7020
7020
|
} catch (er) {
|
|
7021
7021
|
if (options && options.ignoreErrors || er.code === "EACCES") {
|
|
7022
7022
|
return false;
|
|
@@ -7032,7 +7032,7 @@ var require_isexe = __commonJS({
|
|
|
7032
7032
|
var require_which = __commonJS({
|
|
7033
7033
|
"node_modules/which/which.js"(exports, module) {
|
|
7034
7034
|
var isWindows = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys";
|
|
7035
|
-
var
|
|
7035
|
+
var path26 = __require("path");
|
|
7036
7036
|
var COLON = isWindows ? ";" : ":";
|
|
7037
7037
|
var isexe = require_isexe();
|
|
7038
7038
|
var getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: "ENOENT" });
|
|
@@ -7070,7 +7070,7 @@ var require_which = __commonJS({
|
|
|
7070
7070
|
return opt.all && found.length ? resolve(found) : reject(getNotFoundError(cmd));
|
|
7071
7071
|
const ppRaw = pathEnv[i];
|
|
7072
7072
|
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
|
|
7073
|
-
const pCmd =
|
|
7073
|
+
const pCmd = path26.join(pathPart, cmd);
|
|
7074
7074
|
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
|
|
7075
7075
|
resolve(subStep(p, i, 0));
|
|
7076
7076
|
});
|
|
@@ -7097,7 +7097,7 @@ var require_which = __commonJS({
|
|
|
7097
7097
|
for (let i = 0; i < pathEnv.length; i++) {
|
|
7098
7098
|
const ppRaw = pathEnv[i];
|
|
7099
7099
|
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
|
|
7100
|
-
const pCmd =
|
|
7100
|
+
const pCmd = path26.join(pathPart, cmd);
|
|
7101
7101
|
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
|
|
7102
7102
|
for (let j = 0; j < pathExt.length; j++) {
|
|
7103
7103
|
const cur = p + pathExt[j];
|
|
@@ -7145,7 +7145,7 @@ var require_path_key = __commonJS({
|
|
|
7145
7145
|
var require_resolveCommand = __commonJS({
|
|
7146
7146
|
"node_modules/cross-spawn/lib/util/resolveCommand.js"(exports, module) {
|
|
7147
7147
|
"use strict";
|
|
7148
|
-
var
|
|
7148
|
+
var path26 = __require("path");
|
|
7149
7149
|
var which = require_which();
|
|
7150
7150
|
var getPathKey = require_path_key();
|
|
7151
7151
|
function resolveCommandAttempt(parsed, withoutPathExt) {
|
|
@@ -7163,7 +7163,7 @@ var require_resolveCommand = __commonJS({
|
|
|
7163
7163
|
try {
|
|
7164
7164
|
resolved = which.sync(parsed.command, {
|
|
7165
7165
|
path: env[getPathKey({ env })],
|
|
7166
|
-
pathExt: withoutPathExt ?
|
|
7166
|
+
pathExt: withoutPathExt ? path26.delimiter : void 0
|
|
7167
7167
|
});
|
|
7168
7168
|
} catch (e) {
|
|
7169
7169
|
} finally {
|
|
@@ -7172,7 +7172,7 @@ var require_resolveCommand = __commonJS({
|
|
|
7172
7172
|
}
|
|
7173
7173
|
}
|
|
7174
7174
|
if (resolved) {
|
|
7175
|
-
resolved =
|
|
7175
|
+
resolved = path26.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
|
|
7176
7176
|
}
|
|
7177
7177
|
return resolved;
|
|
7178
7178
|
}
|
|
@@ -7226,8 +7226,8 @@ var require_shebang_command = __commonJS({
|
|
|
7226
7226
|
if (!match) {
|
|
7227
7227
|
return null;
|
|
7228
7228
|
}
|
|
7229
|
-
const [
|
|
7230
|
-
const binary =
|
|
7229
|
+
const [path26, argument] = match[0].replace(/#! ?/, "").split(" ");
|
|
7230
|
+
const binary = path26.split("/").pop();
|
|
7231
7231
|
if (binary === "env") {
|
|
7232
7232
|
return argument;
|
|
7233
7233
|
}
|
|
@@ -7262,7 +7262,7 @@ var require_readShebang = __commonJS({
|
|
|
7262
7262
|
var require_parse = __commonJS({
|
|
7263
7263
|
"node_modules/cross-spawn/lib/parse.js"(exports, module) {
|
|
7264
7264
|
"use strict";
|
|
7265
|
-
var
|
|
7265
|
+
var path26 = __require("path");
|
|
7266
7266
|
var resolveCommand = require_resolveCommand();
|
|
7267
7267
|
var escape2 = require_escape();
|
|
7268
7268
|
var readShebang = require_readShebang();
|
|
@@ -7287,7 +7287,7 @@ var require_parse = __commonJS({
|
|
|
7287
7287
|
const needsShell = !isExecutableRegExp.test(commandFile);
|
|
7288
7288
|
if (parsed.options.forceShell || needsShell) {
|
|
7289
7289
|
const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile);
|
|
7290
|
-
parsed.command =
|
|
7290
|
+
parsed.command = path26.normalize(parsed.command);
|
|
7291
7291
|
parsed.command = escape2.command(parsed.command);
|
|
7292
7292
|
parsed.args = parsed.args.map((arg) => escape2.argument(arg, needsDoubleEscapeMetaChars));
|
|
7293
7293
|
const shellCommand = [parsed.command].concat(parsed.args).join(" ");
|
|
@@ -7377,7 +7377,7 @@ var require_cross_spawn = __commonJS({
|
|
|
7377
7377
|
var cp = __require("child_process");
|
|
7378
7378
|
var parse3 = require_parse();
|
|
7379
7379
|
var enoent = require_enoent();
|
|
7380
|
-
function
|
|
7380
|
+
function spawn7(command, args, options) {
|
|
7381
7381
|
const parsed = parse3(command, args, options);
|
|
7382
7382
|
const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
|
|
7383
7383
|
enoent.hookChildProcess(spawned, parsed);
|
|
@@ -7389,8 +7389,8 @@ var require_cross_spawn = __commonJS({
|
|
|
7389
7389
|
result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
|
|
7390
7390
|
return result;
|
|
7391
7391
|
}
|
|
7392
|
-
module.exports =
|
|
7393
|
-
module.exports.spawn =
|
|
7392
|
+
module.exports = spawn7;
|
|
7393
|
+
module.exports.spawn = spawn7;
|
|
7394
7394
|
module.exports.sync = spawnSync;
|
|
7395
7395
|
module.exports._parse = parse3;
|
|
7396
7396
|
module.exports._enoent = enoent;
|
|
@@ -9753,10 +9753,10 @@ function mergeDefs(...defs) {
|
|
|
9753
9753
|
function cloneDef(schema) {
|
|
9754
9754
|
return mergeDefs(schema._zod.def);
|
|
9755
9755
|
}
|
|
9756
|
-
function getElementAtPath(obj,
|
|
9757
|
-
if (!
|
|
9756
|
+
function getElementAtPath(obj, path26) {
|
|
9757
|
+
if (!path26)
|
|
9758
9758
|
return obj;
|
|
9759
|
-
return
|
|
9759
|
+
return path26.reduce((acc, key) => acc?.[key], obj);
|
|
9760
9760
|
}
|
|
9761
9761
|
function promiseAllObject(promisesObj) {
|
|
9762
9762
|
const keys = Object.keys(promisesObj);
|
|
@@ -10165,11 +10165,11 @@ function explicitlyAborted(x, startIndex = 0) {
|
|
|
10165
10165
|
}
|
|
10166
10166
|
return false;
|
|
10167
10167
|
}
|
|
10168
|
-
function prefixIssues(
|
|
10168
|
+
function prefixIssues(path26, issues) {
|
|
10169
10169
|
return issues.map((iss) => {
|
|
10170
10170
|
var _a3;
|
|
10171
10171
|
(_a3 = iss).path ?? (_a3.path = []);
|
|
10172
|
-
iss.path.unshift(
|
|
10172
|
+
iss.path.unshift(path26);
|
|
10173
10173
|
return iss;
|
|
10174
10174
|
});
|
|
10175
10175
|
}
|
|
@@ -10316,16 +10316,16 @@ function flattenError(error2, mapper = (issue2) => issue2.message) {
|
|
|
10316
10316
|
}
|
|
10317
10317
|
function formatError(error2, mapper = (issue2) => issue2.message) {
|
|
10318
10318
|
const fieldErrors = { _errors: [] };
|
|
10319
|
-
const processError = (error3,
|
|
10319
|
+
const processError = (error3, path26 = []) => {
|
|
10320
10320
|
for (const issue2 of error3.issues) {
|
|
10321
10321
|
if (issue2.code === "invalid_union" && issue2.errors.length) {
|
|
10322
|
-
issue2.errors.map((issues) => processError({ issues }, [...
|
|
10322
|
+
issue2.errors.map((issues) => processError({ issues }, [...path26, ...issue2.path]));
|
|
10323
10323
|
} else if (issue2.code === "invalid_key") {
|
|
10324
|
-
processError({ issues: issue2.issues }, [...
|
|
10324
|
+
processError({ issues: issue2.issues }, [...path26, ...issue2.path]);
|
|
10325
10325
|
} else if (issue2.code === "invalid_element") {
|
|
10326
|
-
processError({ issues: issue2.issues }, [...
|
|
10326
|
+
processError({ issues: issue2.issues }, [...path26, ...issue2.path]);
|
|
10327
10327
|
} else {
|
|
10328
|
-
const fullpath = [...
|
|
10328
|
+
const fullpath = [...path26, ...issue2.path];
|
|
10329
10329
|
if (fullpath.length === 0) {
|
|
10330
10330
|
fieldErrors._errors.push(mapper(issue2));
|
|
10331
10331
|
} else {
|
|
@@ -18267,7 +18267,7 @@ var StdioClientTransport = class {
|
|
|
18267
18267
|
};
|
|
18268
18268
|
|
|
18269
18269
|
// src/version.ts
|
|
18270
|
-
var VERSION = "0.1.
|
|
18270
|
+
var VERSION = "0.1.9";
|
|
18271
18271
|
|
|
18272
18272
|
// src/publishChannel.generated.ts
|
|
18273
18273
|
var PUBLISH_NPM_NAME = "evomeme";
|
|
@@ -18487,6 +18487,7 @@ function escapeRegExp(value) {
|
|
|
18487
18487
|
|
|
18488
18488
|
// src/cli/install/ides/codex.ts
|
|
18489
18489
|
var MCP_SERVER_NAME = "evomeme";
|
|
18490
|
+
var MCP_TOOL_TIMEOUT_SECONDS = 60 * 60;
|
|
18490
18491
|
function getCodexConfigPath(homeDir) {
|
|
18491
18492
|
return path3.join(homeDir ?? os.homedir(), ".codex", "config.toml");
|
|
18492
18493
|
}
|
|
@@ -18510,6 +18511,7 @@ function mergeCodexMcpConfig(configPath, options) {
|
|
|
18510
18511
|
`[mcp_servers."${MCP_SERVER_NAME}"]`,
|
|
18511
18512
|
`command = "${escapeToml(launch.command)}"`,
|
|
18512
18513
|
`args = [${launch.args.map((arg) => `"${escapeToml(arg)}"`).join(", ")}]`,
|
|
18514
|
+
`tool_timeout_sec = ${MCP_TOOL_TIMEOUT_SECONDS}`,
|
|
18513
18515
|
...envSection,
|
|
18514
18516
|
""
|
|
18515
18517
|
].join("\n");
|
|
@@ -18970,8 +18972,8 @@ async function performRequest(config2, fetchFn, request) {
|
|
|
18970
18972
|
}
|
|
18971
18973
|
return envelope.data;
|
|
18972
18974
|
}
|
|
18973
|
-
function buildUrl(baseUrl,
|
|
18974
|
-
const url = new URL(`${baseUrl}${
|
|
18975
|
+
function buildUrl(baseUrl, path26, query) {
|
|
18976
|
+
const url = new URL(`${baseUrl}${path26.startsWith("/") ? path26 : `/${path26}`}`);
|
|
18975
18977
|
if (query) {
|
|
18976
18978
|
for (const [key, value] of Object.entries(query)) {
|
|
18977
18979
|
if (value !== void 0) {
|
|
@@ -19262,7 +19264,7 @@ async function getImageJobStatus(token, jobId, options = {}) {
|
|
|
19262
19264
|
path: `/v1/tools/image/jobs/${encodeURIComponent(jobId)}`,
|
|
19263
19265
|
token
|
|
19264
19266
|
});
|
|
19265
|
-
if (typeof data.job_id !== "string" || data.job_id.length === 0 || !IMAGE_JOB_KINDS.includes(data.kind ?? "") || !IMAGE_JOB_STATUSES.includes(data.status ?? "") || typeof data.asset_url !== "string" || data.asset_url.length === 0 || typeof data.pending !== "boolean" || typeof data.attempts !== "number" || typeof data.max_attempts !== "number" || data.error_code !== void 0 && data.error_code !== null && typeof data.error_code !== "string" || data.error_stage !== void 0 && data.error_stage !== null && typeof data.error_stage !== "string" || typeof data.created_at !== "string" || typeof data.updated_at !== "string" || data.completed_at !== void 0 && data.completed_at !== null && typeof data.completed_at !== "string" || data.layers !== void 0 && !Array.isArray(data.layers)) {
|
|
19267
|
+
if (typeof data.job_id !== "string" || data.job_id.length === 0 || !IMAGE_JOB_KINDS.includes(data.kind ?? "") || !IMAGE_JOB_STATUSES.includes(data.status ?? "") || typeof data.asset_url !== "string" || data.asset_url.length === 0 || typeof data.pending !== "boolean" || typeof data.attempts !== "number" || typeof data.max_attempts !== "number" || data.error_code !== void 0 && data.error_code !== null && typeof data.error_code !== "string" || data.error_stage !== void 0 && data.error_stage !== null && typeof data.error_stage !== "string" || data.error_message !== void 0 && data.error_message !== null && typeof data.error_message !== "string" || typeof data.created_at !== "string" || typeof data.updated_at !== "string" || data.completed_at !== void 0 && data.completed_at !== null && typeof data.completed_at !== "string" || data.layers !== void 0 && !Array.isArray(data.layers)) {
|
|
19266
19268
|
throw incompatibleResponseError(JSON.stringify(data));
|
|
19267
19269
|
}
|
|
19268
19270
|
const layers = (data.layers ?? []).map((layer) => {
|
|
@@ -19288,6 +19290,7 @@ async function getImageJobStatus(token, jobId, options = {}) {
|
|
|
19288
19290
|
maxAttempts: data.max_attempts,
|
|
19289
19291
|
errorCode: data.error_code ?? void 0,
|
|
19290
19292
|
errorStage: data.error_stage ?? void 0,
|
|
19293
|
+
...data.error_message == null ? {} : { errorMessage: data.error_message },
|
|
19291
19294
|
createdAt: data.created_at,
|
|
19292
19295
|
updatedAt: data.updated_at,
|
|
19293
19296
|
completedAt: data.completed_at ?? void 0,
|
|
@@ -20161,21 +20164,23 @@ async function runInit(parsed, deps = defaultDeps2) {
|
|
|
20161
20164
|
}
|
|
20162
20165
|
|
|
20163
20166
|
// src/cli/canvas/command.ts
|
|
20164
|
-
import
|
|
20167
|
+
import path22 from "node:path";
|
|
20165
20168
|
|
|
20166
20169
|
// src/canvas/server.ts
|
|
20167
|
-
import { randomUUID as
|
|
20170
|
+
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
20168
20171
|
import { createReadStream, existsSync, watch } from "node:fs";
|
|
20169
20172
|
import {
|
|
20170
20173
|
copyFile as copyFile3,
|
|
20171
|
-
mkdir as
|
|
20172
|
-
|
|
20174
|
+
mkdir as mkdir7,
|
|
20175
|
+
mkdtemp as mkdtemp2,
|
|
20176
|
+
readFile as readFile4,
|
|
20177
|
+
rm as rm4,
|
|
20173
20178
|
stat as stat6,
|
|
20174
20179
|
unlink,
|
|
20175
|
-
writeFile as
|
|
20180
|
+
writeFile as writeFile4
|
|
20176
20181
|
} from "node:fs/promises";
|
|
20177
20182
|
import http from "node:http";
|
|
20178
|
-
import
|
|
20183
|
+
import path21 from "node:path";
|
|
20179
20184
|
|
|
20180
20185
|
// src/image/service.ts
|
|
20181
20186
|
import { copyFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
@@ -20350,7 +20355,7 @@ async function applyImageMask(sourcePath, maskPath, outputPath) {
|
|
|
20350
20355
|
"-i",
|
|
20351
20356
|
maskPath,
|
|
20352
20357
|
"-filter_complex",
|
|
20353
|
-
"[1:v]format=gray,negate[
|
|
20358
|
+
"[0:v]format=rgba,split=2[source][source_alpha_input];[source_alpha_input]alphaextract[source_alpha];[1:v]format=gray,negate[mask_alpha];[source_alpha][mask_alpha]blend=all_mode=multiply[final_alpha];[source][final_alpha]alphamerge[out]",
|
|
20354
20359
|
"-map",
|
|
20355
20360
|
"[out]",
|
|
20356
20361
|
"-frames:v",
|
|
@@ -20571,6 +20576,7 @@ async function waitForImageJob(jobId, initialAssetUrl, deps = defaultImageServic
|
|
|
20571
20576
|
await hooks.onStatus?.({ ...status, assetUrl: lastAssetUrl });
|
|
20572
20577
|
if (status.status === "SUCCEEDED") return { ...status, assetUrl: lastAssetUrl };
|
|
20573
20578
|
if (status.status === "FAILED") {
|
|
20579
|
+
if (status.errorMessage) throw new Error(status.errorMessage);
|
|
20574
20580
|
throw new Error(
|
|
20575
20581
|
[
|
|
20576
20582
|
"图片任务失败。",
|
|
@@ -21093,11 +21099,13 @@ function splitAnchorFactors(anchor) {
|
|
|
21093
21099
|
const y = anchor.startsWith("top") || anchor === "top" ? 0 : anchor.startsWith("bottom") || anchor === "bottom" ? 1 : 0.5;
|
|
21094
21100
|
return { x, y };
|
|
21095
21101
|
}
|
|
21096
|
-
async function manualSplitImage(sourcePath, outputDir, fileNameForIndex, rows, columns) {
|
|
21102
|
+
async function manualSplitImage(sourcePath, outputDir, fileNameForIndex, rows, columns, options = {}) {
|
|
21097
21103
|
const rowValue = Number(rows);
|
|
21098
21104
|
const columnValue = Number(columns);
|
|
21099
|
-
if (!Number.isInteger(rowValue) || !Number.isInteger(columnValue) || rowValue < 1 || columnValue < 1 || rowValue > 64 || columnValue > 64 || rowValue * columnValue < 2) {
|
|
21100
|
-
throw new Error(
|
|
21105
|
+
if (!Number.isInteger(rowValue) || !Number.isInteger(columnValue) || rowValue < 1 || columnValue < 1 || rowValue > 64 || columnValue > 64 || !options.allowSingleCell && rowValue * columnValue < 2) {
|
|
21106
|
+
throw new Error(
|
|
21107
|
+
options.allowSingleCell ? "图片切分的行数与列数需为 1–64 的整数" : "手动切分的行数与列数需为 1–64 的整数,且至少切为两格"
|
|
21108
|
+
);
|
|
21101
21109
|
}
|
|
21102
21110
|
const r = rowValue;
|
|
21103
21111
|
const c = columnValue;
|
|
@@ -21108,34 +21116,39 @@ async function manualSplitImage(sourcePath, outputDir, fileNameForIndex, rows, c
|
|
|
21108
21116
|
await mkdir3(outputDir, { recursive: true });
|
|
21109
21117
|
const files = [];
|
|
21110
21118
|
const boxes = [];
|
|
21111
|
-
|
|
21119
|
+
const requestedIndexes = options.indexes == null ? Array.from({ length: r * c }, (_, index) => index) : [...new Set(options.indexes.map(Number))];
|
|
21120
|
+
if (!requestedIndexes.length || requestedIndexes.some(
|
|
21121
|
+
(index) => !Number.isInteger(index) || index < 0 || index >= r * c
|
|
21122
|
+
)) {
|
|
21123
|
+
throw new Error("图片切分索引无效");
|
|
21124
|
+
}
|
|
21125
|
+
for (const index of requestedIndexes) {
|
|
21126
|
+
const row = Math.floor(index / c);
|
|
21127
|
+
const column = index % c;
|
|
21112
21128
|
const y = Math.floor(row * source.height / r);
|
|
21113
21129
|
const bottom = Math.floor((row + 1) * source.height / r);
|
|
21114
|
-
|
|
21115
|
-
|
|
21116
|
-
|
|
21117
|
-
|
|
21118
|
-
|
|
21119
|
-
|
|
21120
|
-
|
|
21121
|
-
|
|
21122
|
-
|
|
21123
|
-
|
|
21124
|
-
|
|
21125
|
-
|
|
21126
|
-
|
|
21127
|
-
|
|
21128
|
-
|
|
21129
|
-
|
|
21130
|
-
|
|
21131
|
-
|
|
21132
|
-
|
|
21133
|
-
|
|
21134
|
-
|
|
21135
|
-
|
|
21136
|
-
boxes.push(box);
|
|
21137
|
-
files.push(output);
|
|
21138
|
-
}
|
|
21130
|
+
const x = Math.floor(column * source.width / c);
|
|
21131
|
+
const right = Math.floor((column + 1) * source.width / c);
|
|
21132
|
+
const box = { x, y, width: right - x, height: bottom - y };
|
|
21133
|
+
const output = path17.join(outputDir, fileNameForIndex(index));
|
|
21134
|
+
await runBinary(ffmpegPath2(), [
|
|
21135
|
+
"-hide_banner",
|
|
21136
|
+
"-loglevel",
|
|
21137
|
+
"error",
|
|
21138
|
+
"-y",
|
|
21139
|
+
"-i",
|
|
21140
|
+
sourcePath,
|
|
21141
|
+
"-vf",
|
|
21142
|
+
`format=rgba,crop=${box.width}:${box.height}:${box.x}:${box.y}`,
|
|
21143
|
+
"-frames:v",
|
|
21144
|
+
"1",
|
|
21145
|
+
"-c:v",
|
|
21146
|
+
"png",
|
|
21147
|
+
output
|
|
21148
|
+
]);
|
|
21149
|
+
await assertFile(output, "手动切分输出为空");
|
|
21150
|
+
boxes.push(box);
|
|
21151
|
+
files.push(output);
|
|
21139
21152
|
}
|
|
21140
21153
|
return {
|
|
21141
21154
|
files,
|
|
@@ -21146,7 +21159,7 @@ async function manualSplitImage(sourcePath, outputDir, fileNameForIndex, rows, c
|
|
|
21146
21159
|
sourceHeight: source.height
|
|
21147
21160
|
};
|
|
21148
21161
|
}
|
|
21149
|
-
async function composeSequenceSheet(framePaths, outputPath, rows, columns) {
|
|
21162
|
+
async function composeSequenceSheet(framePaths, outputPath, rows, columns, options = {}) {
|
|
21150
21163
|
const r = Math.round(Number(rows));
|
|
21151
21164
|
const c = Math.round(Number(columns));
|
|
21152
21165
|
if (r < 1 || c < 1 || !Number.isFinite(r) || !Number.isFinite(c)) {
|
|
@@ -21157,13 +21170,15 @@ async function composeSequenceSheet(framePaths, outputPath, rows, columns) {
|
|
|
21157
21170
|
throw new Error(`格子不足:${r}×${c}=${r * c},但有 ${framePaths.length} 帧`);
|
|
21158
21171
|
}
|
|
21159
21172
|
const dimensions = await Promise.all(framePaths.map(probeImageDimensions));
|
|
21160
|
-
const
|
|
21161
|
-
const
|
|
21173
|
+
const requestedCellWidth = Math.round(Number(options.cellWidth));
|
|
21174
|
+
const requestedCellHeight = Math.round(Number(options.cellHeight));
|
|
21175
|
+
const cellWidth = requestedCellWidth > 0 ? requestedCellWidth : Math.max(...dimensions.map((item) => item.width));
|
|
21176
|
+
const cellHeight = requestedCellHeight > 0 ? requestedCellHeight : Math.max(...dimensions.map((item) => item.height));
|
|
21162
21177
|
const width = cellWidth * c;
|
|
21163
21178
|
const height = cellHeight * r;
|
|
21164
21179
|
const inputs = framePaths.flatMap((frame) => ["-i", frame]);
|
|
21165
21180
|
const normalized = framePaths.map(
|
|
21166
|
-
(_, index) => `[${index}:v]format=rgba,pad=${cellWidth}:${cellHeight}:0:0:color=0x00000000[f${index}]`
|
|
21181
|
+
(_, index) => options.fit === "contain" ? `[${index}:v]format=rgba,scale=${cellWidth}:${cellHeight}:force_original_aspect_ratio=decrease,pad=${cellWidth}:${cellHeight}:(ow-iw)/2:(oh-ih)/2:color=0x00000000[f${index}]` : `[${index}:v]format=rgba,pad=${cellWidth}:${cellHeight}:0:0:color=0x00000000[f${index}]`
|
|
21167
21182
|
);
|
|
21168
21183
|
const layouts = framePaths.map(
|
|
21169
21184
|
(_, index) => `${index % c * cellWidth}_${Math.floor(index / c) * cellHeight}`
|
|
@@ -21297,11 +21312,11 @@ async function probeVideoDimensions(videoPath) {
|
|
|
21297
21312
|
if (!stream?.width || !stream.height) throw new Error("无法读取视频尺寸");
|
|
21298
21313
|
return { width: stream.width, height: stream.height };
|
|
21299
21314
|
}
|
|
21300
|
-
async function
|
|
21315
|
+
async function writeVideoSegment(videoPath, outputPath, startSeconds, endSeconds) {
|
|
21301
21316
|
const totalDuration = await probeVideoDuration(videoPath);
|
|
21302
21317
|
const start = Math.max(0, Number(startSeconds) || 0);
|
|
21303
21318
|
const end = Math.min(totalDuration, Number(endSeconds) || totalDuration);
|
|
21304
|
-
if (!(end > start + 0.01)) throw new Error("
|
|
21319
|
+
if (!(end > start + 0.01)) throw new Error("视频区间过短");
|
|
21305
21320
|
await mkdir4(path18.dirname(outputPath), { recursive: true });
|
|
21306
21321
|
await run2(ffmpegPath3(), [
|
|
21307
21322
|
"-hide_banner",
|
|
@@ -21328,7 +21343,7 @@ async function trimVideo(videoPath, outputPath, startSeconds, endSeconds) {
|
|
|
21328
21343
|
"+faststart",
|
|
21329
21344
|
outputPath
|
|
21330
21345
|
]);
|
|
21331
|
-
if (!await fileExists(outputPath)) throw new Error("
|
|
21346
|
+
if (!await fileExists(outputPath)) throw new Error("视频片段生成失败");
|
|
21332
21347
|
return { start, end, duration: end - start };
|
|
21333
21348
|
}
|
|
21334
21349
|
async function probeTotalFrames(videoPath) {
|
|
@@ -21496,7 +21511,7 @@ async function detectFirstVideoCycle(videoPath, options = {}) {
|
|
|
21496
21511
|
}
|
|
21497
21512
|
async function extractVideoCycle(videoPath, outputPath, options = {}) {
|
|
21498
21513
|
const detected = await detectFirstVideoCycle(videoPath, options);
|
|
21499
|
-
await
|
|
21514
|
+
await writeVideoSegment(videoPath, outputPath, detected.start, detected.end);
|
|
21500
21515
|
return { ...detected, file: outputPath };
|
|
21501
21516
|
}
|
|
21502
21517
|
function zscore(frame) {
|
|
@@ -21520,16 +21535,172 @@ function nccScore(left, right) {
|
|
|
21520
21535
|
}
|
|
21521
21536
|
|
|
21522
21537
|
// src/canvas/workspace.ts
|
|
21523
|
-
import { createHash, randomUUID as
|
|
21538
|
+
import { createHash, randomUUID as randomUUID3 } from "node:crypto";
|
|
21524
21539
|
import {
|
|
21525
|
-
mkdir as
|
|
21526
|
-
readFile as
|
|
21540
|
+
mkdir as mkdir6,
|
|
21541
|
+
readFile as readFile3,
|
|
21527
21542
|
readdir,
|
|
21528
|
-
rename as
|
|
21543
|
+
rename as rename3,
|
|
21529
21544
|
stat as stat5,
|
|
21530
|
-
writeFile as
|
|
21545
|
+
writeFile as writeFile3
|
|
21531
21546
|
} from "node:fs/promises";
|
|
21547
|
+
import path20 from "node:path";
|
|
21548
|
+
|
|
21549
|
+
// src/canvas/layoutStore.ts
|
|
21550
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
21551
|
+
import { mkdir as mkdir5, readFile as readFile2, rename as rename2, writeFile as writeFile2 } from "node:fs/promises";
|
|
21532
21552
|
import path19 from "node:path";
|
|
21553
|
+
var CANVAS_LAYOUT_VERSION = 2;
|
|
21554
|
+
var layoutLocks = /* @__PURE__ */ new Map();
|
|
21555
|
+
var CanvasLayoutRevisionConflictError = class extends Error {
|
|
21556
|
+
constructor(expectedRevision, actualRevision) {
|
|
21557
|
+
super(`Canvas layout revision conflict: expected ${expectedRevision}, actual ${actualRevision}`);
|
|
21558
|
+
this.status = 409;
|
|
21559
|
+
this.name = "CanvasLayoutRevisionConflictError";
|
|
21560
|
+
this.expectedRevision = expectedRevision;
|
|
21561
|
+
this.actualRevision = actualRevision;
|
|
21562
|
+
}
|
|
21563
|
+
};
|
|
21564
|
+
function createCanvasNodeId() {
|
|
21565
|
+
return `node_${randomUUID2().replace(/-/g, "")}`;
|
|
21566
|
+
}
|
|
21567
|
+
function normalizeCanvasLayoutItem(value) {
|
|
21568
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
21569
|
+
const source = value;
|
|
21570
|
+
const id = typeof source.id === "string" ? source.id.trim() : "";
|
|
21571
|
+
if (!id || source.type === "sequence") return void 0;
|
|
21572
|
+
const explicitAssetId = typeof source.assetId === "string" ? source.assetId.trim() : "";
|
|
21573
|
+
const assetId = explicitAssetId || (id.startsWith("assets/") ? id : void 0);
|
|
21574
|
+
const explicitOperationId = typeof source.operationId === "string" ? source.operationId.trim() : "";
|
|
21575
|
+
const operationId = explicitOperationId || (source.type === "pending" || id.startsWith("job_") || id.startsWith("pending_") ? id : void 0);
|
|
21576
|
+
const explicitNodeId = typeof source.nodeId === "string" ? source.nodeId.trim() : "";
|
|
21577
|
+
const nodeId = explicitNodeId || (operationId ? `node:${operationId}` : assetId ? `node:${assetId}` : `node:${id}`);
|
|
21578
|
+
return {
|
|
21579
|
+
...source,
|
|
21580
|
+
id,
|
|
21581
|
+
nodeId,
|
|
21582
|
+
...assetId ? { assetId } : {},
|
|
21583
|
+
...operationId ? { operationId } : {}
|
|
21584
|
+
};
|
|
21585
|
+
}
|
|
21586
|
+
function normalizeCanvasLayoutItems(value) {
|
|
21587
|
+
if (!Array.isArray(value)) return [];
|
|
21588
|
+
const items = [];
|
|
21589
|
+
const nodeIds = /* @__PURE__ */ new Set();
|
|
21590
|
+
const assetIds = /* @__PURE__ */ new Set();
|
|
21591
|
+
for (const valueItem of value) {
|
|
21592
|
+
const item = normalizeCanvasLayoutItem(valueItem);
|
|
21593
|
+
if (!item) continue;
|
|
21594
|
+
const nodeId = String(item.nodeId);
|
|
21595
|
+
const assetId = typeof item.assetId === "string" ? item.assetId : void 0;
|
|
21596
|
+
if (nodeIds.has(nodeId) || assetId && assetIds.has(assetId)) continue;
|
|
21597
|
+
nodeIds.add(nodeId);
|
|
21598
|
+
if (assetId) assetIds.add(assetId);
|
|
21599
|
+
items.push(item);
|
|
21600
|
+
}
|
|
21601
|
+
return items;
|
|
21602
|
+
}
|
|
21603
|
+
function normalizeCanvasLayout(value) {
|
|
21604
|
+
const source = value && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
21605
|
+
const revision = Number.isInteger(source.revision) && Number(source.revision) >= 0 ? Number(source.revision) : 0;
|
|
21606
|
+
return {
|
|
21607
|
+
version: CANVAS_LAYOUT_VERSION,
|
|
21608
|
+
revision,
|
|
21609
|
+
items: normalizeCanvasLayoutItems(source.items)
|
|
21610
|
+
};
|
|
21611
|
+
}
|
|
21612
|
+
async function withLayoutLock(layoutPath, action) {
|
|
21613
|
+
const key = path19.resolve(layoutPath).toLowerCase();
|
|
21614
|
+
const previous = layoutLocks.get(key) ?? Promise.resolve();
|
|
21615
|
+
let release;
|
|
21616
|
+
const gate = new Promise((resolve) => {
|
|
21617
|
+
release = resolve;
|
|
21618
|
+
});
|
|
21619
|
+
const queued = previous.then(() => gate);
|
|
21620
|
+
layoutLocks.set(key, queued);
|
|
21621
|
+
await previous;
|
|
21622
|
+
try {
|
|
21623
|
+
return await action();
|
|
21624
|
+
} finally {
|
|
21625
|
+
release?.();
|
|
21626
|
+
if (layoutLocks.get(key) === queued) layoutLocks.delete(key);
|
|
21627
|
+
}
|
|
21628
|
+
}
|
|
21629
|
+
var CanvasLayoutStore = class {
|
|
21630
|
+
constructor(workspaceDir) {
|
|
21631
|
+
this.workspaceDir = path19.resolve(workspaceDir);
|
|
21632
|
+
this.layoutPath = path19.join(this.workspaceDir, ".evomeme", "canvas", "layout.json");
|
|
21633
|
+
}
|
|
21634
|
+
async load() {
|
|
21635
|
+
return withLayoutLock(this.layoutPath, async () => {
|
|
21636
|
+
const raw = await this.readUnlocked();
|
|
21637
|
+
const normalized = normalizeCanvasLayout(raw);
|
|
21638
|
+
if (JSON.stringify(raw) !== JSON.stringify(normalized)) {
|
|
21639
|
+
const migrated = {
|
|
21640
|
+
...normalized,
|
|
21641
|
+
revision: Number(normalized.revision ?? 0) + 1
|
|
21642
|
+
};
|
|
21643
|
+
await this.writeUnlocked(migrated);
|
|
21644
|
+
return migrated;
|
|
21645
|
+
}
|
|
21646
|
+
return normalized;
|
|
21647
|
+
});
|
|
21648
|
+
}
|
|
21649
|
+
async replace(layout, expectedRevision) {
|
|
21650
|
+
return withLayoutLock(this.layoutPath, async () => {
|
|
21651
|
+
const current = normalizeCanvasLayout(await this.readUnlocked());
|
|
21652
|
+
if (expectedRevision !== void 0 && Number.isInteger(expectedRevision) && expectedRevision !== Number(current.revision ?? 0)) {
|
|
21653
|
+
throw new CanvasLayoutRevisionConflictError(
|
|
21654
|
+
expectedRevision,
|
|
21655
|
+
Number(current.revision ?? 0)
|
|
21656
|
+
);
|
|
21657
|
+
}
|
|
21658
|
+
const normalized = normalizeCanvasLayout(layout);
|
|
21659
|
+
const next = {
|
|
21660
|
+
...normalized,
|
|
21661
|
+
version: CANVAS_LAYOUT_VERSION,
|
|
21662
|
+
revision: Number(current.revision ?? 0) + 1
|
|
21663
|
+
};
|
|
21664
|
+
await this.writeUnlocked(next);
|
|
21665
|
+
return next;
|
|
21666
|
+
});
|
|
21667
|
+
}
|
|
21668
|
+
async mutate(mutateItems) {
|
|
21669
|
+
return withLayoutLock(this.layoutPath, async () => {
|
|
21670
|
+
const raw = await this.readUnlocked();
|
|
21671
|
+
const current = normalizeCanvasLayout(raw);
|
|
21672
|
+
const draft = JSON.parse(JSON.stringify(current.items));
|
|
21673
|
+
const result = await mutateItems(draft);
|
|
21674
|
+
const items = normalizeCanvasLayoutItems(result ?? draft);
|
|
21675
|
+
const normalizedRaw = normalizeCanvasLayout(raw);
|
|
21676
|
+
if (JSON.stringify(items) === JSON.stringify(current.items) && JSON.stringify(raw) === JSON.stringify(normalizedRaw)) {
|
|
21677
|
+
return current;
|
|
21678
|
+
}
|
|
21679
|
+
const next = {
|
|
21680
|
+
version: CANVAS_LAYOUT_VERSION,
|
|
21681
|
+
revision: Number(current.revision ?? 0) + 1,
|
|
21682
|
+
items
|
|
21683
|
+
};
|
|
21684
|
+
await this.writeUnlocked(next);
|
|
21685
|
+
return next;
|
|
21686
|
+
});
|
|
21687
|
+
}
|
|
21688
|
+
async readUnlocked() {
|
|
21689
|
+
try {
|
|
21690
|
+
return JSON.parse(await readFile2(this.layoutPath, "utf8"));
|
|
21691
|
+
} catch {
|
|
21692
|
+
return { version: CANVAS_LAYOUT_VERSION, revision: 0, items: [] };
|
|
21693
|
+
}
|
|
21694
|
+
}
|
|
21695
|
+
async writeUnlocked(layout) {
|
|
21696
|
+
await mkdir5(path19.dirname(this.layoutPath), { recursive: true });
|
|
21697
|
+
const temporaryPath = `${this.layoutPath}.${process.pid}.${randomUUID2()}.tmp`;
|
|
21698
|
+
await writeFile2(temporaryPath, JSON.stringify(layout, null, 2), "utf8");
|
|
21699
|
+
await rename2(temporaryPath, this.layoutPath);
|
|
21700
|
+
}
|
|
21701
|
+
};
|
|
21702
|
+
|
|
21703
|
+
// src/canvas/workspace.ts
|
|
21533
21704
|
var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
21534
21705
|
".png",
|
|
21535
21706
|
".jpg",
|
|
@@ -21542,31 +21713,31 @@ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
21542
21713
|
]);
|
|
21543
21714
|
var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov", ".m4v"]);
|
|
21544
21715
|
function resolveCanvasWorkspace(workspaceDir) {
|
|
21545
|
-
return
|
|
21716
|
+
return path20.resolve(workspaceDir?.trim() || process.cwd());
|
|
21546
21717
|
}
|
|
21547
21718
|
function canvasWorkspacePaths(workspaceDir) {
|
|
21548
21719
|
const workspace = resolveCanvasWorkspace(workspaceDir);
|
|
21549
|
-
const canvasDir =
|
|
21720
|
+
const canvasDir = path20.join(workspace, ".evomeme", "canvas");
|
|
21550
21721
|
return {
|
|
21551
21722
|
workspaceDir: workspace,
|
|
21552
|
-
assetsDir:
|
|
21723
|
+
assetsDir: path20.join(workspace, "assets"),
|
|
21553
21724
|
canvasDir,
|
|
21554
|
-
layoutPath:
|
|
21555
|
-
jobsDir:
|
|
21556
|
-
provenanceDir:
|
|
21725
|
+
layoutPath: path20.join(canvasDir, "layout.json"),
|
|
21726
|
+
jobsDir: path20.join(canvasDir, "jobs"),
|
|
21727
|
+
provenanceDir: path20.join(canvasDir, "provenance")
|
|
21557
21728
|
};
|
|
21558
21729
|
}
|
|
21559
21730
|
async function ensureCanvasWorkspace(workspaceDir) {
|
|
21560
21731
|
const paths = canvasWorkspacePaths(workspaceDir);
|
|
21561
21732
|
await Promise.all([
|
|
21562
|
-
|
|
21563
|
-
|
|
21564
|
-
|
|
21733
|
+
mkdir6(paths.assetsDir, { recursive: true }),
|
|
21734
|
+
mkdir6(paths.jobsDir, { recursive: true }),
|
|
21735
|
+
mkdir6(paths.provenanceDir, { recursive: true })
|
|
21565
21736
|
]);
|
|
21566
21737
|
return paths;
|
|
21567
21738
|
}
|
|
21568
21739
|
function createCanvasOperationId() {
|
|
21569
|
-
return `job_${
|
|
21740
|
+
return `job_${randomUUID3().replace(/-/g, "")}`;
|
|
21570
21741
|
}
|
|
21571
21742
|
function assetIdFromRelativePath(relativePath) {
|
|
21572
21743
|
return `assets/${normalizeRelativePath(relativePath)}`;
|
|
@@ -21581,7 +21752,7 @@ function relativePathFromAssetId(assetId) {
|
|
|
21581
21752
|
function resolveAssetPath(workspaceDir, assetId) {
|
|
21582
21753
|
const paths = canvasWorkspacePaths(workspaceDir);
|
|
21583
21754
|
const relative = relativePathFromAssetId(assetId);
|
|
21584
|
-
const full =
|
|
21755
|
+
const full = path20.resolve(paths.assetsDir, ...relative.split("/"));
|
|
21585
21756
|
if (!pathContained(paths.assetsDir, full)) {
|
|
21586
21757
|
throw new Error(`非法素材路径:${assetId}`);
|
|
21587
21758
|
}
|
|
@@ -21598,9 +21769,15 @@ async function scanCanvasAssets(workspaceDir) {
|
|
|
21598
21769
|
async function beginCanvasJob(input) {
|
|
21599
21770
|
const paths = await ensureCanvasWorkspace(input.workspaceDir);
|
|
21600
21771
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21772
|
+
const operationId = input.operationId || createCanvasOperationId();
|
|
21773
|
+
const existingLayout = await new CanvasLayoutStore(paths.workspaceDir).load();
|
|
21774
|
+
const existingNode = existingLayout.items.find(
|
|
21775
|
+
(item) => item.operationId === operationId || item.id === operationId
|
|
21776
|
+
);
|
|
21601
21777
|
const job = {
|
|
21602
21778
|
version: 1,
|
|
21603
|
-
operationId
|
|
21779
|
+
operationId,
|
|
21780
|
+
nodeId: input.nodeId || existingNode?.nodeId || createCanvasNodeId(),
|
|
21604
21781
|
kind: input.kind,
|
|
21605
21782
|
status: "SUBMITTING",
|
|
21606
21783
|
workspaceDir: paths.workspaceDir,
|
|
@@ -21612,13 +21789,12 @@ async function beginCanvasJob(input) {
|
|
|
21612
21789
|
referenceImageUrls: input.referenceImageUrls,
|
|
21613
21790
|
parentAssetIds: [...new Set(input.parentAssetIds ?? [])],
|
|
21614
21791
|
outputKind: input.outputKind,
|
|
21615
|
-
presentation: input.presentation ?? "assets",
|
|
21616
|
-
outputLayoutId: input.outputLayoutId,
|
|
21617
21792
|
metadata: input.metadata,
|
|
21618
21793
|
createdAt: now,
|
|
21619
21794
|
updatedAt: now
|
|
21620
21795
|
};
|
|
21621
21796
|
await writeJob(paths, job);
|
|
21797
|
+
if (!existingNode) await reconcileCanvasLayout(paths.workspaceDir);
|
|
21622
21798
|
return job;
|
|
21623
21799
|
}
|
|
21624
21800
|
async function updateCanvasJob(workspaceDir, operationId, patch) {
|
|
@@ -21643,15 +21819,24 @@ async function updateCanvasJob(workspaceDir, operationId, patch) {
|
|
|
21643
21819
|
}
|
|
21644
21820
|
async function readCanvasJob(workspaceDir, operationId) {
|
|
21645
21821
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
21646
|
-
|
|
21822
|
+
const job = await readJson(jobPath(paths, operationId));
|
|
21823
|
+
return normalizeCanvasJob(job);
|
|
21647
21824
|
}
|
|
21648
21825
|
async function readCanvasJobs(workspaceDir) {
|
|
21649
21826
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
21650
21827
|
const names = await readdir(paths.jobsDir).catch(() => []);
|
|
21651
21828
|
const jobs = await Promise.all(
|
|
21652
|
-
names.filter((name) => name.endsWith(".json")).map((name) => readJson(
|
|
21829
|
+
names.filter((name) => name.endsWith(".json")).map((name) => readJson(path20.join(paths.jobsDir, name)))
|
|
21653
21830
|
);
|
|
21654
|
-
return jobs.filter((job) => Boolean(job
|
|
21831
|
+
return jobs.map(normalizeCanvasJob).filter((job) => Boolean(job)).sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
21832
|
+
}
|
|
21833
|
+
function normalizeCanvasJob(job) {
|
|
21834
|
+
if (!job?.operationId) return void 0;
|
|
21835
|
+
return {
|
|
21836
|
+
...job,
|
|
21837
|
+
nodeId: job.nodeId || `node:${job.operationId}`,
|
|
21838
|
+
parentAssetIds: Array.isArray(job.parentAssetIds) ? job.parentAssetIds : []
|
|
21839
|
+
};
|
|
21655
21840
|
}
|
|
21656
21841
|
async function completeCanvasJob(input) {
|
|
21657
21842
|
const paths = await ensureCanvasWorkspace(input.workspaceDir);
|
|
@@ -21665,16 +21850,17 @@ async function completeCanvasJob(input) {
|
|
|
21665
21850
|
}] : [];
|
|
21666
21851
|
if (!outputs.length) throw new Error("操作完成但没有产物");
|
|
21667
21852
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
21853
|
+
const metadata = input.metadata ?? current.metadata;
|
|
21668
21854
|
const outputAssetIds = [];
|
|
21669
21855
|
const outputLocalPaths = [];
|
|
21670
21856
|
for (const output of outputs) {
|
|
21671
|
-
const full =
|
|
21857
|
+
const full = path20.resolve(output.localPath);
|
|
21672
21858
|
if (!pathContained(paths.assetsDir, full)) {
|
|
21673
21859
|
throw new Error(`操作产物不在 assets 目录:${full}`);
|
|
21674
21860
|
}
|
|
21675
|
-
const relativePath = normalizeRelativePath(
|
|
21861
|
+
const relativePath = normalizeRelativePath(path20.relative(paths.assetsDir, full));
|
|
21676
21862
|
const assetId = assetIdFromRelativePath(relativePath);
|
|
21677
|
-
const kind = output.kind ?? kindFromExtension(
|
|
21863
|
+
const kind = output.kind ?? kindFromExtension(path20.extname(full)) ?? current.outputKind ?? "image";
|
|
21678
21864
|
await writeAssetProvenance(paths.workspaceDir, {
|
|
21679
21865
|
version: 1,
|
|
21680
21866
|
assetId,
|
|
@@ -21689,6 +21875,7 @@ async function completeCanvasJob(input) {
|
|
|
21689
21875
|
imageType: current.imageType,
|
|
21690
21876
|
size: current.size,
|
|
21691
21877
|
assetUrl: output.assetUrl ?? (outputs.length === 1 ? input.assetUrl : void 0),
|
|
21878
|
+
metadata,
|
|
21692
21879
|
parents: buildParents(current),
|
|
21693
21880
|
createdAt: now,
|
|
21694
21881
|
updatedAt: now
|
|
@@ -21696,7 +21883,7 @@ async function completeCanvasJob(input) {
|
|
|
21696
21883
|
outputAssetIds.push(assetId);
|
|
21697
21884
|
outputLocalPaths.push(full);
|
|
21698
21885
|
}
|
|
21699
|
-
|
|
21886
|
+
const completed = await updateCanvasJob(paths.workspaceDir, current.operationId, {
|
|
21700
21887
|
status: "SUCCEEDED",
|
|
21701
21888
|
jobId: input.jobId ?? current.jobId,
|
|
21702
21889
|
assetUrl: input.assetUrl,
|
|
@@ -21704,15 +21891,16 @@ async function completeCanvasJob(input) {
|
|
|
21704
21891
|
outputLocalPath: outputLocalPaths[0],
|
|
21705
21892
|
outputAssetIds,
|
|
21706
21893
|
outputLocalPaths,
|
|
21707
|
-
|
|
21708
|
-
outputLayoutId: input.outputLayoutId ?? current.outputLayoutId,
|
|
21709
|
-
metadata: input.metadata ?? current.metadata,
|
|
21894
|
+
metadata,
|
|
21710
21895
|
error: void 0
|
|
21711
21896
|
});
|
|
21897
|
+
await reconcileCanvasLayout(paths.workspaceDir);
|
|
21898
|
+
return completed;
|
|
21712
21899
|
}
|
|
21713
21900
|
async function failCanvasJob(workspaceDir, operationId, error2) {
|
|
21714
21901
|
const detail = error2 instanceof Error ? error2.message : String(error2);
|
|
21715
21902
|
await updateCanvasJob(workspaceDir, operationId, { status: "FAILED", error: detail });
|
|
21903
|
+
await reconcileCanvasLayout(workspaceDir);
|
|
21716
21904
|
}
|
|
21717
21905
|
async function hideCanvasJob(workspaceDir, operationId) {
|
|
21718
21906
|
await updateCanvasJob(workspaceDir, operationId, { hidden: true });
|
|
@@ -21740,7 +21928,7 @@ async function recordUploadedAsset(workspaceDir, assetId, assetUrl) {
|
|
|
21740
21928
|
version: 1,
|
|
21741
21929
|
assetId,
|
|
21742
21930
|
relativePath: relativePathFromAssetId(assetId),
|
|
21743
|
-
kind: existing?.kind ?? kindFromExtension(
|
|
21931
|
+
kind: existing?.kind ?? kindFromExtension(path20.extname(assetId)) ?? "image",
|
|
21744
21932
|
source: existing?.source ?? "uploaded",
|
|
21745
21933
|
operation: existing?.operation ?? "upload",
|
|
21746
21934
|
operationId: existing?.operationId,
|
|
@@ -21750,6 +21938,7 @@ async function recordUploadedAsset(workspaceDir, assetId, assetUrl) {
|
|
|
21750
21938
|
imageType: existing?.imageType,
|
|
21751
21939
|
size: existing?.size,
|
|
21752
21940
|
assetUrl,
|
|
21941
|
+
metadata: existing?.metadata,
|
|
21753
21942
|
parents: existing?.parents ?? [],
|
|
21754
21943
|
createdAt: existing?.createdAt ?? now,
|
|
21755
21944
|
updatedAt: now
|
|
@@ -21764,167 +21953,189 @@ async function recordCopiedAsset(workspaceDir, sourceAssetId, outputAssetId) {
|
|
|
21764
21953
|
version: 1,
|
|
21765
21954
|
assetId: outputAssetId,
|
|
21766
21955
|
relativePath: relativePathFromAssetId(outputAssetId),
|
|
21767
|
-
kind: source?.kind ?? kindFromExtension(
|
|
21956
|
+
kind: source?.kind ?? kindFromExtension(path20.extname(outputAssetId)) ?? "image",
|
|
21768
21957
|
source: "copied",
|
|
21769
21958
|
operation: "copy",
|
|
21959
|
+
metadata: source?.metadata,
|
|
21770
21960
|
parents: [{ assetId: sourceAssetId, role: "source" }],
|
|
21771
21961
|
createdAt: now,
|
|
21772
21962
|
updatedAt: now
|
|
21773
21963
|
});
|
|
21774
21964
|
}
|
|
21775
|
-
async function
|
|
21965
|
+
async function saveCanvasLayout(workspaceDir, layout, expectedRevision) {
|
|
21776
21966
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
21777
|
-
|
|
21778
|
-
return { items: Array.isArray(raw.items) ? raw.items : [] };
|
|
21967
|
+
return new CanvasLayoutStore(paths.workspaceDir).replace(layout, expectedRevision);
|
|
21779
21968
|
}
|
|
21780
|
-
async function
|
|
21969
|
+
async function mutateCanvasLayout(workspaceDir, mutateItems) {
|
|
21781
21970
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
21782
|
-
|
|
21783
|
-
|
|
21784
|
-
|
|
21971
|
+
return new CanvasLayoutStore(paths.workspaceDir).mutate(mutateItems);
|
|
21972
|
+
}
|
|
21973
|
+
function automaticFramePreview(job) {
|
|
21974
|
+
if (job.kind !== "video_frame_extract" && job.kind !== "frame_sheet_edit" || job.metadata?.kind !== "frame_sheet") return void 0;
|
|
21975
|
+
const rows = Math.round(Number(job.metadata.rows));
|
|
21976
|
+
const columns = Math.round(Number(job.metadata.columns));
|
|
21977
|
+
const frameCount = Math.round(Number(job.metadata.frameCount));
|
|
21978
|
+
const fps = Number(job.metadata.fps);
|
|
21979
|
+
if (!Number.isFinite(rows) || !Number.isFinite(columns) || !Number.isFinite(frameCount) || !Number.isFinite(fps) || rows < 1 || columns < 1 || frameCount < 1 || frameCount > rows * columns || fps <= 0) {
|
|
21980
|
+
return void 0;
|
|
21981
|
+
}
|
|
21982
|
+
return { enabled: true, rows, columns, frameCount, fps };
|
|
21983
|
+
}
|
|
21984
|
+
function completedGenerationMeta(job) {
|
|
21985
|
+
if (job.kind === "generate_game_image") {
|
|
21986
|
+
return {
|
|
21987
|
+
kind: "image",
|
|
21988
|
+
prompt: job.prompt ?? "",
|
|
21989
|
+
model: job.imageModel ?? "",
|
|
21990
|
+
size: job.size ?? "",
|
|
21991
|
+
references: job.parentAssetIds.map((id) => ({ id })),
|
|
21992
|
+
createdAt: job.createdAt
|
|
21993
|
+
};
|
|
21994
|
+
}
|
|
21995
|
+
if (job.kind === "generate_game_video") {
|
|
21996
|
+
return {
|
|
21997
|
+
kind: "video",
|
|
21998
|
+
prompt: job.prompt ?? "",
|
|
21999
|
+
model: job.imageModel ?? "",
|
|
22000
|
+
ratio: String(job.metadata?.ratio ?? ""),
|
|
22001
|
+
duration: job.metadata?.duration ?? null,
|
|
22002
|
+
resolution: String(job.metadata?.resolution ?? ""),
|
|
22003
|
+
generateAudio: job.metadata?.generateAudio === true,
|
|
22004
|
+
seed: job.metadata?.seed ?? null,
|
|
22005
|
+
references: Array.isArray(job.metadata?.references) ? job.metadata.references : job.parentAssetIds.map((id) => ({ id })),
|
|
22006
|
+
createdAt: job.createdAt
|
|
22007
|
+
};
|
|
22008
|
+
}
|
|
22009
|
+
return void 0;
|
|
21785
22010
|
}
|
|
21786
22011
|
async function reconcileCanvasLayout(workspaceDir, scannedAssets) {
|
|
21787
22012
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
21788
|
-
const [assets, jobs
|
|
22013
|
+
const [assets, jobs] = await Promise.all([
|
|
21789
22014
|
scannedAssets ?? scanCanvasAssets(paths.workspaceDir),
|
|
21790
|
-
readCanvasJobs(paths.workspaceDir)
|
|
21791
|
-
loadCanvasLayout(paths.workspaceDir)
|
|
22015
|
+
readCanvasJobs(paths.workspaceDir)
|
|
21792
22016
|
]);
|
|
21793
22017
|
const assetIds = new Set(assets.map((asset) => asset.id));
|
|
21794
22018
|
const assetKinds = new Map(assets.map((asset) => [asset.id, asset.kind]));
|
|
21795
|
-
|
|
21796
|
-
|
|
21797
|
-
|
|
21798
|
-
|
|
21799
|
-
|
|
21800
|
-
|
|
21801
|
-
|
|
21802
|
-
|
|
21803
|
-
|
|
21804
|
-
|
|
21805
|
-
|
|
21806
|
-
|
|
21807
|
-
|
|
21808
|
-
|
|
21809
|
-
|
|
21810
|
-
|
|
21811
|
-
|
|
21812
|
-
|
|
21813
|
-
|
|
21814
|
-
|
|
21815
|
-
|
|
21816
|
-
|
|
21817
|
-
|
|
21818
|
-
|
|
21819
|
-
|
|
21820
|
-
|
|
21821
|
-
|
|
21822
|
-
|
|
21823
|
-
|
|
21824
|
-
items = items.filter((item) => !outputIds.includes(item.id));
|
|
21825
|
-
items.push(...layerItems);
|
|
21826
|
-
} else if (job.presentation === "sequence" && outputIds.length) {
|
|
21827
|
-
const sequenceId = job.outputLayoutId || `sequence_${job.operationId}`;
|
|
21828
|
-
const existingSequence = items.findIndex((item) => item.id === sequenceId);
|
|
21829
|
-
if (existingSequence >= 0) {
|
|
21830
|
-
const existingFrames = sequenceFrames(items[existingSequence], assetIds);
|
|
21831
|
-
sequenceFrameIdsFor(existingFrames, sequenceFrameIds);
|
|
21832
|
-
items = items.filter(
|
|
21833
|
-
(item) => item.type === "sequence" || !existingFrames.includes(item.id)
|
|
22019
|
+
const store = new CanvasLayoutStore(paths.workspaceDir);
|
|
22020
|
+
return store.mutate(async (currentItems) => {
|
|
22021
|
+
let items = currentItems.filter((item) => {
|
|
22022
|
+
if (!item?.id || item.type === "sequence") return false;
|
|
22023
|
+
const assetId = layoutItemAssetId(item);
|
|
22024
|
+
return !assetId || assetIds.has(assetId);
|
|
22025
|
+
});
|
|
22026
|
+
for (const job of jobs) {
|
|
22027
|
+
const pendingIndex = items.findIndex(
|
|
22028
|
+
(item) => item.type === "pending" && (item.operationId === job.operationId || item.id === job.operationId)
|
|
22029
|
+
);
|
|
22030
|
+
const previous = pendingIndex >= 0 ? items[pendingIndex] : void 0;
|
|
22031
|
+
if (job.status === "SUCCEEDED") {
|
|
22032
|
+
const outputIds = (job.outputAssetIds?.length ? job.outputAssetIds : [job.outputAssetId]).filter((id) => Boolean(id && assetIds.has(id)));
|
|
22033
|
+
const existingOutputs = outputIds.map((assetId) => items.find((item) => layoutItemAssetId(item) === assetId)).filter((item) => Boolean(item));
|
|
22034
|
+
if (pendingIndex >= 0) items.splice(pendingIndex, 1);
|
|
22035
|
+
if (!outputIds.length) continue;
|
|
22036
|
+
const alreadyCompleted = !previous && existingOutputs.length === outputIds.length && existingOutputs.every((item) => item.operationId === job.operationId);
|
|
22037
|
+
if (alreadyCompleted) continue;
|
|
22038
|
+
if (job.kind === "decompose_image_layers") {
|
|
22039
|
+
const alreadyPositioned = !previous && existingOutputs.length === outputIds.length && existingOutputs.every((item) => layerGroupId(item) === job.operationId);
|
|
22040
|
+
if (alreadyPositioned) continue;
|
|
22041
|
+
const placementJob = await withDecomposedLayerDimensions(job);
|
|
22042
|
+
const layerItems = buildDecomposedLayerItems(
|
|
22043
|
+
placementJob,
|
|
22044
|
+
outputIds,
|
|
22045
|
+
previous,
|
|
22046
|
+
existingOutputs,
|
|
22047
|
+
items.length
|
|
21834
22048
|
);
|
|
22049
|
+
items = items.filter((item) => {
|
|
22050
|
+
const assetId = layoutItemAssetId(item);
|
|
22051
|
+
return !assetId || !outputIds.includes(assetId);
|
|
22052
|
+
});
|
|
22053
|
+
items.push(...layerItems);
|
|
21835
22054
|
continue;
|
|
21836
22055
|
}
|
|
21837
|
-
|
|
21838
|
-
|
|
21839
|
-
|
|
21840
|
-
|
|
21841
|
-
|
|
21842
|
-
|
|
21843
|
-
id: sequenceId,
|
|
21844
|
-
type: "sequence",
|
|
21845
|
-
name: String(job.metadata?.name || "视频序列帧"),
|
|
21846
|
-
x: previous?.x,
|
|
21847
|
-
y: previous?.y,
|
|
21848
|
-
width: previous?.width ?? 220,
|
|
21849
|
-
height: previous?.height ?? null,
|
|
21850
|
-
frames: outputIds,
|
|
21851
|
-
fps: Number(job.metadata?.fps) || Math.min(24, Math.max(1, outputIds.length)),
|
|
21852
|
-
loop: job.metadata?.loop !== false,
|
|
21853
|
-
sourceId: job.parentAssetIds[0],
|
|
21854
|
-
origin: {
|
|
21855
|
-
kind: "video_sequence",
|
|
21856
|
-
parentId: job.parentAssetIds[0],
|
|
21857
|
-
frameCount: outputIds.length
|
|
21858
|
-
}
|
|
21859
|
-
};
|
|
21860
|
-
items.push(withAutoPosition(sequence, items.length));
|
|
21861
|
-
} else if (outputIds.length) {
|
|
21862
|
-
const base = previous ?? withAutoPosition({ id: job.operationId, width: 220 }, items.length);
|
|
22056
|
+
const base = previous ?? existingOutputs[0] ?? withAutoPosition({
|
|
22057
|
+
id: job.operationId,
|
|
22058
|
+
nodeId: job.nodeId,
|
|
22059
|
+
operationId: job.operationId,
|
|
22060
|
+
width: 220
|
|
22061
|
+
}, items.length);
|
|
21863
22062
|
const columns = Math.min(4, outputIds.length);
|
|
22063
|
+
const framePreview = automaticFramePreview(job);
|
|
22064
|
+
const gen = completedGenerationMeta(job);
|
|
22065
|
+
items = items.filter((item) => {
|
|
22066
|
+
const assetId = layoutItemAssetId(item);
|
|
22067
|
+
return !assetId || !outputIds.includes(assetId);
|
|
22068
|
+
});
|
|
21864
22069
|
outputIds.forEach((assetId, outputIndex) => {
|
|
21865
|
-
|
|
21866
|
-
|
|
22070
|
+
const existing = existingOutputs.find(
|
|
22071
|
+
(item) => layoutItemAssetId(item) === assetId
|
|
22072
|
+
);
|
|
21867
22073
|
const col = outputIndex % columns;
|
|
21868
22074
|
const row = Math.floor(outputIndex / columns);
|
|
21869
22075
|
items.push({
|
|
21870
22076
|
id: assetId,
|
|
22077
|
+
nodeId: outputIndex === 0 ? job.nodeId : existing?.nodeId || `node:${job.operationId}:${outputIndex}`,
|
|
22078
|
+
assetId,
|
|
22079
|
+
operationId: job.operationId,
|
|
21871
22080
|
...assetKinds.get(assetId) === "video" ? { type: "video" } : {},
|
|
21872
|
-
x: Number(base.x ?? 80) + col * (Number(base.width ?? 220) + 40),
|
|
21873
|
-
y: Number(base.y ?? 80) + row * (Number(base.height ?? 220) + 80),
|
|
21874
|
-
width: base.width ?? 220,
|
|
21875
|
-
height: base.height ?? null
|
|
22081
|
+
x: outputIndex === 0 ? Number(base.x ?? 80) : Number(existing?.x ?? base.x ?? 80) + col * (Number(base.width ?? 220) + 40),
|
|
22082
|
+
y: outputIndex === 0 ? Number(base.y ?? 80) : Number(existing?.y ?? base.y ?? 80) + row * (Number(base.height ?? 220) + 80),
|
|
22083
|
+
width: outputIndex === 0 ? base.width ?? 220 : existing?.width ?? base.width ?? 220,
|
|
22084
|
+
height: outputIndex === 0 ? base.height ?? null : existing?.height ?? base.height ?? null,
|
|
22085
|
+
...gen ? { gen } : {},
|
|
22086
|
+
...framePreview ? { framePreview } : {}
|
|
21876
22087
|
});
|
|
21877
22088
|
});
|
|
22089
|
+
continue;
|
|
21878
22090
|
}
|
|
21879
|
-
|
|
21880
|
-
|
|
21881
|
-
|
|
21882
|
-
|
|
21883
|
-
|
|
21884
|
-
|
|
21885
|
-
|
|
21886
|
-
|
|
21887
|
-
|
|
22091
|
+
if (job.status === "FAILED" || job.hidden) {
|
|
22092
|
+
if (pendingIndex >= 0) items.splice(pendingIndex, 1);
|
|
22093
|
+
continue;
|
|
22094
|
+
}
|
|
22095
|
+
const pending = {
|
|
22096
|
+
id: job.operationId,
|
|
22097
|
+
nodeId: job.nodeId,
|
|
22098
|
+
operationId: job.operationId,
|
|
22099
|
+
type: "pending",
|
|
22100
|
+
name: job.metadata?.interactiveEdit === true ? "交互编辑生成中" : operationLabel(job.kind),
|
|
22101
|
+
x: previous?.x,
|
|
22102
|
+
y: previous?.y,
|
|
22103
|
+
width: previous?.width ?? 220,
|
|
22104
|
+
height: previous?.height,
|
|
22105
|
+
prompt: job.prompt ?? "",
|
|
22106
|
+
size: job.size ?? "",
|
|
22107
|
+
model: job.imageModel ?? "",
|
|
22108
|
+
media: job.outputKind ?? (job.kind === "generate_game_video" || job.kind === "video_cycle" ? "video" : "image"),
|
|
22109
|
+
ratio: String(job.metadata?.ratio ?? ""),
|
|
22110
|
+
error: job.error ?? null,
|
|
22111
|
+
references: job.parentAssetIds.map((id) => ({
|
|
22112
|
+
id,
|
|
22113
|
+
role: parentRole(job.kind)
|
|
22114
|
+
}))
|
|
22115
|
+
};
|
|
22116
|
+
if (pendingIndex >= 0) items[pendingIndex] = { ...previous, ...pending };
|
|
22117
|
+
else items.push(withAutoPosition(pending, items.length));
|
|
22118
|
+
}
|
|
22119
|
+
for (const asset of assets) {
|
|
22120
|
+
if (items.some((item) => layoutItemAssetId(item) === asset.id)) continue;
|
|
22121
|
+
items.push(withAutoPosition({
|
|
22122
|
+
id: asset.id,
|
|
22123
|
+
nodeId: `node:${asset.id}`,
|
|
22124
|
+
assetId: asset.id,
|
|
22125
|
+
...asset.kind === "video" ? { type: "video" } : {},
|
|
22126
|
+
width: asset.kind === "video" ? 280 : 220,
|
|
22127
|
+
height: null
|
|
22128
|
+
}, items.length));
|
|
21888
22129
|
}
|
|
21889
|
-
|
|
21890
|
-
|
|
21891
|
-
|
|
21892
|
-
|
|
21893
|
-
|
|
21894
|
-
|
|
21895
|
-
width: index >= 0 ? items[index].width : 220,
|
|
21896
|
-
height: index >= 0 ? items[index].height : void 0,
|
|
21897
|
-
prompt: job.prompt ?? "",
|
|
21898
|
-
size: job.size ?? "",
|
|
21899
|
-
model: job.imageModel ?? "",
|
|
21900
|
-
media: job.outputKind ?? (job.kind === "generate_game_video" || job.kind === "video_cycle" || job.kind === "video_trim" ? "video" : "image"),
|
|
21901
|
-
ratio: String(job.metadata?.ratio ?? ""),
|
|
21902
|
-
error: job.error ?? null,
|
|
21903
|
-
references: job.parentAssetIds.map((id) => ({
|
|
21904
|
-
id,
|
|
21905
|
-
role: parentRole(job.kind)
|
|
21906
|
-
}))
|
|
21907
|
-
};
|
|
21908
|
-
if (index >= 0) items[index] = { ...items[index], ...pending };
|
|
21909
|
-
else items.push(withAutoPosition(pending, items.length));
|
|
21910
|
-
}
|
|
21911
|
-
for (const asset of assets) {
|
|
21912
|
-
if (sequenceFrameIds.has(asset.id)) continue;
|
|
21913
|
-
if (items.some((item2) => item2.id === asset.id)) continue;
|
|
21914
|
-
const item = {
|
|
21915
|
-
id: asset.id,
|
|
21916
|
-
...asset.kind === "video" ? { type: "video" } : {},
|
|
21917
|
-
width: asset.kind === "video" ? 280 : 220,
|
|
21918
|
-
height: null
|
|
21919
|
-
};
|
|
21920
|
-
items.push(withAutoPosition(item, items.length));
|
|
21921
|
-
}
|
|
21922
|
-
const reconciled = { items };
|
|
21923
|
-
await saveCanvasLayout(paths.workspaceDir, reconciled);
|
|
21924
|
-
return reconciled;
|
|
22130
|
+
return items;
|
|
22131
|
+
});
|
|
22132
|
+
}
|
|
22133
|
+
function layoutItemAssetId(item) {
|
|
22134
|
+
if (typeof item.assetId === "string" && item.assetId) return item.assetId;
|
|
22135
|
+
return item.id.startsWith("assets/") ? item.id : void 0;
|
|
21925
22136
|
}
|
|
21926
22137
|
async function walkAssets(root, relativeDir, output) {
|
|
21927
|
-
const dir =
|
|
22138
|
+
const dir = path20.join(root, ...relativeDir.split("/").filter(Boolean));
|
|
21928
22139
|
const entries = await readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
21929
22140
|
for (const entry of entries) {
|
|
21930
22141
|
const relativePath = normalizeRelativePath(
|
|
@@ -21935,9 +22146,9 @@ async function walkAssets(root, relativeDir, output) {
|
|
|
21935
22146
|
continue;
|
|
21936
22147
|
}
|
|
21937
22148
|
if (!entry.isFile()) continue;
|
|
21938
|
-
const kind = kindFromExtension(
|
|
22149
|
+
const kind = kindFromExtension(path20.extname(entry.name));
|
|
21939
22150
|
if (!kind) continue;
|
|
21940
|
-
const localPath =
|
|
22151
|
+
const localPath = path20.join(root, ...relativePath.split("/"));
|
|
21941
22152
|
const info = await stat5(localPath);
|
|
21942
22153
|
output.push({
|
|
21943
22154
|
id: assetIdFromRelativePath(relativePath),
|
|
@@ -22037,6 +22248,9 @@ function buildDecomposedLayerItems(job, outputIds, previous, existingOutputs, au
|
|
|
22037
22248
|
const isBase = metadata.zIndex === 0 || !bounds;
|
|
22038
22249
|
return {
|
|
22039
22250
|
id: assetId,
|
|
22251
|
+
nodeId: metadata.zIndex === 0 ? job.nodeId : `node:${job.operationId}:${metadata.zIndex}`,
|
|
22252
|
+
assetId,
|
|
22253
|
+
operationId: job.operationId,
|
|
22040
22254
|
name: metadata.name,
|
|
22041
22255
|
x: isBase ? baseX : baseX + bounds.left * baseDisplayWidth,
|
|
22042
22256
|
y: isBase ? baseY : baseY + bounds.top * baseDisplayHeight,
|
|
@@ -22115,18 +22329,11 @@ function operationLabel(kind) {
|
|
|
22115
22329
|
cutout_game_image: "智能抠图中",
|
|
22116
22330
|
smart_split: "智能切分中",
|
|
22117
22331
|
manual_split: "手动切分中",
|
|
22118
|
-
|
|
22119
|
-
|
|
22120
|
-
video_cycle: "循环提取中"
|
|
22121
|
-
video_trim: "视频裁剪中"
|
|
22332
|
+
video_frame_extract: "视频序列帧图生成中",
|
|
22333
|
+
frame_sheet_edit: "序列帧保存中",
|
|
22334
|
+
video_cycle: "循环提取中"
|
|
22122
22335
|
}[kind] ?? "处理中";
|
|
22123
22336
|
}
|
|
22124
|
-
function sequenceFrameIdsFor(ids, target) {
|
|
22125
|
-
for (const id of ids) target.add(id);
|
|
22126
|
-
}
|
|
22127
|
-
function sequenceFrames(item, assetIds) {
|
|
22128
|
-
return Array.isArray(item.frames) ? item.frames.map(String).filter((id) => assetIds.has(id)) : [];
|
|
22129
|
-
}
|
|
22130
22337
|
function withAutoPosition(item, index) {
|
|
22131
22338
|
const col = index % 6;
|
|
22132
22339
|
const row = Math.floor(index / 6);
|
|
@@ -22137,27 +22344,27 @@ function withAutoPosition(item, index) {
|
|
|
22137
22344
|
};
|
|
22138
22345
|
}
|
|
22139
22346
|
function jobPath(paths, operationId) {
|
|
22140
|
-
return
|
|
22347
|
+
return path20.join(paths.jobsDir, `${safeRecordName(operationId)}.json`);
|
|
22141
22348
|
}
|
|
22142
22349
|
function provenancePath(paths, relativePath) {
|
|
22143
22350
|
const digest = createHash("sha256").update(normalizeRelativePath(relativePath)).digest("hex");
|
|
22144
|
-
return
|
|
22351
|
+
return path20.join(paths.provenanceDir, `${digest}.json`);
|
|
22145
22352
|
}
|
|
22146
22353
|
async function writeJob(paths, job) {
|
|
22147
22354
|
await atomicWriteJson(jobPath(paths, job.operationId), job);
|
|
22148
22355
|
}
|
|
22149
22356
|
async function readJson(filePath) {
|
|
22150
22357
|
try {
|
|
22151
|
-
return JSON.parse(await
|
|
22358
|
+
return JSON.parse(await readFile3(filePath, "utf8"));
|
|
22152
22359
|
} catch {
|
|
22153
22360
|
return void 0;
|
|
22154
22361
|
}
|
|
22155
22362
|
}
|
|
22156
22363
|
async function atomicWriteJson(filePath, value) {
|
|
22157
|
-
await
|
|
22158
|
-
const tempPath = `${filePath}.${process.pid}.${
|
|
22159
|
-
await
|
|
22160
|
-
await
|
|
22364
|
+
await mkdir6(path20.dirname(filePath), { recursive: true });
|
|
22365
|
+
const tempPath = `${filePath}.${process.pid}.${randomUUID3()}.tmp`;
|
|
22366
|
+
await writeFile3(tempPath, JSON.stringify(value, null, 2), "utf8");
|
|
22367
|
+
await rename3(tempPath, filePath);
|
|
22161
22368
|
}
|
|
22162
22369
|
function kindFromExtension(extension) {
|
|
22163
22370
|
const ext = extension.toLowerCase();
|
|
@@ -22177,9 +22384,9 @@ function safeRecordName(value) {
|
|
|
22177
22384
|
return String(value || "").replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 180);
|
|
22178
22385
|
}
|
|
22179
22386
|
function pathContained(base, target) {
|
|
22180
|
-
const root =
|
|
22181
|
-
const full =
|
|
22182
|
-
const prefix = root.endsWith(
|
|
22387
|
+
const root = path20.resolve(base);
|
|
22388
|
+
const full = path20.resolve(target);
|
|
22389
|
+
const prefix = root.endsWith(path20.sep) ? root : `${root}${path20.sep}`;
|
|
22183
22390
|
return full === root || full.startsWith(prefix);
|
|
22184
22391
|
}
|
|
22185
22392
|
|
|
@@ -22261,7 +22468,11 @@ async function runWindowsPowerShell(script, extraEnv = {}) {
|
|
|
22261
22468
|
|
|
22262
22469
|
// src/canvas/server.ts
|
|
22263
22470
|
var MAX_JSON_BODY_BYTES = 80 * 1024 * 1024;
|
|
22471
|
+
var INTERACTIVE_EDIT_MIN_PIXELS = 921600;
|
|
22472
|
+
var INTERACTIVE_EDIT_MAX_PIXELS = 4624220;
|
|
22264
22473
|
var TRANSPARENT_BACKGROUND_REFERENCE_ERROR = "透明背景图功能要求有且只有一个PNG格式参考图输入";
|
|
22474
|
+
var CANVAS_QUOTA_ERROR = "当前账号的生成额度已用完,请充值或补充额度后重试";
|
|
22475
|
+
var CANVAS_NETWORK_ERROR = "网络连接失败,请重试";
|
|
22265
22476
|
var CANVAS_VENDOR_ASSETS = {
|
|
22266
22477
|
"/vendor/painterro.min.js": {
|
|
22267
22478
|
packageName: "painterro",
|
|
@@ -22305,7 +22516,7 @@ async function startCanvasServer(options = {}) {
|
|
|
22305
22516
|
} catch (error2) {
|
|
22306
22517
|
const status = statusFromError(error2);
|
|
22307
22518
|
if (!response.headersSent) {
|
|
22308
|
-
sendJson(response, status, { detail:
|
|
22519
|
+
sendJson(response, status, { detail: canvasGenerationErrorMessage(error2) });
|
|
22309
22520
|
} else {
|
|
22310
22521
|
response.end();
|
|
22311
22522
|
}
|
|
@@ -22379,7 +22590,9 @@ async function handleApi(request, response, url, workspaceDir, eventHub, videoGe
|
|
|
22379
22590
|
const layout = await layoutWithProvenance(workspaceDir, assets);
|
|
22380
22591
|
sendJson(response, 200, {
|
|
22381
22592
|
images: assets.map(publicAsset),
|
|
22382
|
-
items: layout.items
|
|
22593
|
+
items: layout.items,
|
|
22594
|
+
version: layout.version,
|
|
22595
|
+
revision: layout.revision
|
|
22383
22596
|
});
|
|
22384
22597
|
return;
|
|
22385
22598
|
}
|
|
@@ -22391,8 +22604,13 @@ async function handleApi(request, response, url, workspaceDir, eventHub, videoGe
|
|
|
22391
22604
|
if (request.method === "PUT" && pathname === "/api/layout") {
|
|
22392
22605
|
const body = await readJsonBody(request);
|
|
22393
22606
|
const layout = { items: sanitizeLayoutItems(body.items) };
|
|
22394
|
-
|
|
22395
|
-
|
|
22607
|
+
const revision = Number(body.revision);
|
|
22608
|
+
const saved = await saveCanvasLayout(
|
|
22609
|
+
workspaceDir,
|
|
22610
|
+
layout,
|
|
22611
|
+
Number.isInteger(revision) && revision >= 0 ? revision : void 0
|
|
22612
|
+
);
|
|
22613
|
+
sendJson(response, 200, saved);
|
|
22396
22614
|
return;
|
|
22397
22615
|
}
|
|
22398
22616
|
if (request.method === "GET" && pathname === "/api/provenance") {
|
|
@@ -22413,7 +22631,7 @@ async function handleApi(request, response, url, workspaceDir, eventHub, videoGe
|
|
|
22413
22631
|
}
|
|
22414
22632
|
if (interactiveEditSourceId) {
|
|
22415
22633
|
const sourcePath = resolveAssetPath(workspaceDir, interactiveEditSourceId);
|
|
22416
|
-
if (mediaKind(
|
|
22634
|
+
if (mediaKind(path21.extname(sourcePath)) !== "image") {
|
|
22417
22635
|
throw httpError(400, "交互编辑来源必须是图片");
|
|
22418
22636
|
}
|
|
22419
22637
|
}
|
|
@@ -22446,7 +22664,7 @@ async function handleApi(request, response, url, workspaceDir, eventHub, videoGe
|
|
|
22446
22664
|
}
|
|
22447
22665
|
if (smartSplitSourceId) {
|
|
22448
22666
|
const sourcePath = resolveAssetPath(workspaceDir, smartSplitSourceId);
|
|
22449
|
-
if (mediaKind(
|
|
22667
|
+
if (mediaKind(path21.extname(sourcePath)) !== "image") {
|
|
22450
22668
|
throw httpError(400, "智能切分来源必须是图片");
|
|
22451
22669
|
}
|
|
22452
22670
|
}
|
|
@@ -22487,10 +22705,9 @@ async function handleApi(request, response, url, workspaceDir, eventHub, videoGe
|
|
|
22487
22705
|
const filePaths = ids.map((assetId) => resolveAssetPath(workspaceDir, assetId));
|
|
22488
22706
|
await Promise.all(filePaths.map(async (filePath) => {
|
|
22489
22707
|
const info = await stat6(filePath).catch(() => void 0);
|
|
22490
|
-
if (!info?.isFile()) throw httpError(404, `素材文件不存在:${
|
|
22708
|
+
if (!info?.isFile()) throw httpError(404, `素材文件不存在:${path21.basename(filePath)}`);
|
|
22491
22709
|
}));
|
|
22492
|
-
|
|
22493
|
-
await fileClipboard.writeFiles(filePaths, sequence);
|
|
22710
|
+
await fileClipboard.writeFiles(filePaths);
|
|
22494
22711
|
sendJson(response, 200, { count: filePaths.length });
|
|
22495
22712
|
return;
|
|
22496
22713
|
}
|
|
@@ -22498,10 +22715,8 @@ async function handleApi(request, response, url, workspaceDir, eventHub, videoGe
|
|
|
22498
22715
|
const clipboard = await fileClipboard.readFiles();
|
|
22499
22716
|
const pasted = await importClipboardFiles(workspaceDir, clipboard.filePaths);
|
|
22500
22717
|
if (!pasted.length) throw httpError(400, "剪贴板中没有支持的图片或视频文件");
|
|
22501
|
-
const sequence = normalizeClipboardSequence(clipboard.metadata, pasted.length);
|
|
22502
22718
|
sendJson(response, 200, {
|
|
22503
|
-
assets: pasted.map(publicAsset)
|
|
22504
|
-
...sequence && pasted.every((asset) => asset.kind === "image") ? { sequence } : {}
|
|
22719
|
+
assets: pasted.map(publicAsset)
|
|
22505
22720
|
});
|
|
22506
22721
|
return;
|
|
22507
22722
|
}
|
|
@@ -22529,26 +22744,20 @@ async function handleApi(request, response, url, workspaceDir, eventHub, videoGe
|
|
|
22529
22744
|
sendJson(response, 200, result);
|
|
22530
22745
|
return;
|
|
22531
22746
|
}
|
|
22532
|
-
if (request.method === "POST" && pathname === "/api/video-
|
|
22747
|
+
if (request.method === "POST" && pathname === "/api/video-frames") {
|
|
22533
22748
|
const body = await readJsonBody(request);
|
|
22534
|
-
const result = await
|
|
22749
|
+
const result = await extractVideoAssetFrames(
|
|
22535
22750
|
workspaceDir,
|
|
22536
22751
|
String(body.id || ""),
|
|
22537
|
-
Number(body.
|
|
22538
|
-
Number(body.end),
|
|
22752
|
+
Math.round(Number(body.count) || 0),
|
|
22539
22753
|
String(body.client_operation_id || "")
|
|
22540
22754
|
);
|
|
22541
22755
|
sendJson(response, 200, result);
|
|
22542
22756
|
return;
|
|
22543
22757
|
}
|
|
22544
|
-
if (request.method === "POST" && pathname === "/api/
|
|
22758
|
+
if (request.method === "POST" && pathname === "/api/frame-sheet/edit") {
|
|
22545
22759
|
const body = await readJsonBody(request);
|
|
22546
|
-
const result = await
|
|
22547
|
-
workspaceDir,
|
|
22548
|
-
String(body.id || ""),
|
|
22549
|
-
Math.round(Number(body.count) || 0),
|
|
22550
|
-
String(body.client_operation_id || "")
|
|
22551
|
-
);
|
|
22760
|
+
const result = await editFrameSheetAsset(workspaceDir, body);
|
|
22552
22761
|
sendJson(response, 200, result);
|
|
22553
22762
|
return;
|
|
22554
22763
|
}
|
|
@@ -22570,12 +22779,6 @@ async function handleApi(request, response, url, workspaceDir, eventHub, videoGe
|
|
|
22570
22779
|
sendJson(response, 200, result);
|
|
22571
22780
|
return;
|
|
22572
22781
|
}
|
|
22573
|
-
if (request.method === "POST" && pathname === "/api/sequence-sheet") {
|
|
22574
|
-
const body = await readJsonBody(request);
|
|
22575
|
-
const result = await composeSequenceAsset(workspaceDir, body);
|
|
22576
|
-
sendJson(response, 200, result);
|
|
22577
|
-
return;
|
|
22578
|
-
}
|
|
22579
22782
|
if (request.method === "POST" && pathname === "/api/generate-video") {
|
|
22580
22783
|
const body = await readJsonBody(request);
|
|
22581
22784
|
const result = await generateVideoFromCanvas(workspaceDir, body, videoGenerationDeps);
|
|
@@ -22587,7 +22790,7 @@ async function handleApi(request, response, url, workspaceDir, eventHub, videoGe
|
|
|
22587
22790
|
async function generateFromCanvas(workspaceDir, body, imageServiceDeps) {
|
|
22588
22791
|
const prompt = String(body.prompt || "").trim();
|
|
22589
22792
|
if (!prompt) throw httpError(400, "prompt 不能为空");
|
|
22590
|
-
const operationId = String(body.client_operation_id || `job_${
|
|
22793
|
+
const operationId = String(body.client_operation_id || `job_${randomUUID4().replace(/-/g, "")}`);
|
|
22591
22794
|
const referenceIds = Array.isArray(body.reference_ids) ? body.reference_ids.map(String).filter(Boolean) : [];
|
|
22592
22795
|
if (referenceIds.length > 14) throw httpError(400, "最多支持 14 张参考图");
|
|
22593
22796
|
const interactiveReferenceData = String(body.reference_image_data || "").trim();
|
|
@@ -22605,7 +22808,7 @@ async function generateFromCanvas(workspaceDir, body, imageServiceDeps) {
|
|
|
22605
22808
|
if (imageModel !== "asset-pro") {
|
|
22606
22809
|
throw httpError(400, "透明背景仅支持 asset-pro 模型");
|
|
22607
22810
|
}
|
|
22608
|
-
if (referenceIds.length !== 1 ||
|
|
22811
|
+
if (referenceIds.length !== 1 || path21.extname(resolveAssetPath(workspaceDir, referenceIds[0])).toLowerCase() !== ".png") {
|
|
22609
22812
|
throw httpError(400, TRANSPARENT_BACKGROUND_REFERENCE_ERROR);
|
|
22610
22813
|
}
|
|
22611
22814
|
}
|
|
@@ -22626,8 +22829,8 @@ async function generateFromCanvas(workspaceDir, body, imageServiceDeps) {
|
|
|
22626
22829
|
}
|
|
22627
22830
|
const interactiveReference = interactiveReferenceData ? await uploadInteractiveReference(workspaceDir, interactiveReferenceData, imageServiceDeps) : void 0;
|
|
22628
22831
|
const referenceImageUrls = interactiveReference ? [interactiveReference.assetUrl] : referenceIds.length ? await Promise.all(referenceIds.map((assetId) => ensureReferenceAssetUrl(workspaceDir, assetId))) : void 0;
|
|
22629
|
-
const background = interactiveReference ?
|
|
22630
|
-
const operationSize = layerDecomposition
|
|
22832
|
+
const background = interactiveReference ? path21.extname(resolveAssetPath(workspaceDir, referenceIds[0])).toLowerCase() === ".png" && interactiveReference.isPng && interactiveReference.hasTransparentPixel ? "transparent" : "opaque" : requestedBackground;
|
|
22833
|
+
const operationSize = layerDecomposition ? "auto" : interactiveReference ? resolveInteractiveEditSize(interactiveReference.width, interactiveReference.height) : size;
|
|
22631
22834
|
const canvasJob = await beginCanvasJob({
|
|
22632
22835
|
operationId,
|
|
22633
22836
|
workspaceDir,
|
|
@@ -22714,7 +22917,7 @@ async function generateFromCanvas(workspaceDir, body, imageServiceDeps) {
|
|
|
22714
22917
|
return {
|
|
22715
22918
|
images: outputIds.map((id, index) => ({
|
|
22716
22919
|
id,
|
|
22717
|
-
name: decomposed.layers[index].name ||
|
|
22920
|
+
name: decomposed.layers[index].name || path21.basename(completed2.outputLocalPaths[index]),
|
|
22718
22921
|
url: `/files/${id}`,
|
|
22719
22922
|
asset_url: decomposed.layers[index].assetUrl,
|
|
22720
22923
|
z_index: decomposed.layers[index].zIndex,
|
|
@@ -22768,7 +22971,7 @@ async function generateFromCanvas(workspaceDir, body, imageServiceDeps) {
|
|
|
22768
22971
|
}
|
|
22769
22972
|
return {
|
|
22770
22973
|
id: completed.outputAssetId,
|
|
22771
|
-
name:
|
|
22974
|
+
name: path21.basename(completed.outputLocalPath),
|
|
22772
22975
|
url: `/files/${completed.outputAssetId}`,
|
|
22773
22976
|
size: operationSize,
|
|
22774
22977
|
model: imageModel,
|
|
@@ -22778,15 +22981,16 @@ async function generateFromCanvas(workspaceDir, body, imageServiceDeps) {
|
|
|
22778
22981
|
tos_url: generated.assetUrl
|
|
22779
22982
|
};
|
|
22780
22983
|
} catch (error2) {
|
|
22781
|
-
|
|
22782
|
-
|
|
22984
|
+
const normalized = normalizeCanvasGenerationError(error2);
|
|
22985
|
+
await failCanvasJob(canvasJob.workspaceDir, canvasJob.operationId, normalized).catch(() => void 0);
|
|
22986
|
+
throw normalized;
|
|
22783
22987
|
}
|
|
22784
22988
|
}
|
|
22785
22989
|
async function generateVideoFromCanvas(workspaceDir, body, videoGenerationDeps) {
|
|
22786
22990
|
const prompt = String(body.prompt || "").trim();
|
|
22787
22991
|
if (!prompt) throw httpError(400, "prompt 不能为空");
|
|
22788
22992
|
const operationId = String(
|
|
22789
|
-
body.client_operation_id || `job_${
|
|
22993
|
+
body.client_operation_id || `job_${randomUUID4().replace(/-/g, "")}`
|
|
22790
22994
|
);
|
|
22791
22995
|
const videoModel = String(body.model || GAME_VIDEO_MODELS[0]);
|
|
22792
22996
|
const ratio = String(body.ratio || "16:9");
|
|
@@ -22825,7 +23029,7 @@ async function generateVideoFromCanvas(workspaceDir, body, videoGenerationDeps)
|
|
|
22825
23029
|
throw httpError(400, `不支持的引用类型:${type}`);
|
|
22826
23030
|
}
|
|
22827
23031
|
const sourcePath = resolveAssetPath(workspaceDir, assetId);
|
|
22828
|
-
const actualKind = mediaKind(
|
|
23032
|
+
const actualKind = mediaKind(path21.extname(sourcePath));
|
|
22829
23033
|
if (actualKind !== type) {
|
|
22830
23034
|
throw httpError(400, `引用类型与文件不匹配:${assetId}`);
|
|
22831
23035
|
}
|
|
@@ -22910,7 +23114,7 @@ async function generateVideoFromCanvas(workspaceDir, body, videoGenerationDeps)
|
|
|
22910
23114
|
}
|
|
22911
23115
|
return {
|
|
22912
23116
|
id: completed.outputAssetId,
|
|
22913
|
-
name:
|
|
23117
|
+
name: path21.basename(completed.outputLocalPath),
|
|
22914
23118
|
kind: "video",
|
|
22915
23119
|
url: `/files/${completed.outputAssetId}`,
|
|
22916
23120
|
job_id: generated.jobId,
|
|
@@ -22925,19 +23129,20 @@ async function generateVideoFromCanvas(workspaceDir, body, videoGenerationDeps)
|
|
|
22925
23129
|
reference_ids: parentAssetIds
|
|
22926
23130
|
};
|
|
22927
23131
|
} catch (error2) {
|
|
22928
|
-
|
|
22929
|
-
|
|
23132
|
+
const normalized = normalizeCanvasGenerationError(error2);
|
|
23133
|
+
await failCanvasJob(canvasJob.workspaceDir, canvasJob.operationId, normalized).catch(() => void 0);
|
|
23134
|
+
throw normalized;
|
|
22930
23135
|
}
|
|
22931
23136
|
}
|
|
22932
23137
|
async function cutoutFromCanvas(workspaceDir, body) {
|
|
22933
23138
|
const sourceAssetId = String(body.id || "").trim();
|
|
22934
23139
|
if (!sourceAssetId) throw httpError(400, "id 不能为空");
|
|
22935
23140
|
const sourcePath = resolveAssetPath(workspaceDir, sourceAssetId);
|
|
22936
|
-
if (mediaKind(
|
|
23141
|
+
if (mediaKind(path21.extname(sourcePath)) !== "image") {
|
|
22937
23142
|
throw httpError(400, "仅支持对图片进行抠图");
|
|
22938
23143
|
}
|
|
22939
23144
|
const operationId = String(
|
|
22940
|
-
body.client_operation_id || `job_${
|
|
23145
|
+
body.client_operation_id || `job_${randomUUID4().replace(/-/g, "")}`
|
|
22941
23146
|
);
|
|
22942
23147
|
const sourceUrl = await ensureReferenceAssetUrl(workspaceDir, sourceAssetId);
|
|
22943
23148
|
const canvasJob = await beginCanvasJob({
|
|
@@ -22978,15 +23183,16 @@ async function cutoutFromCanvas(workspaceDir, body) {
|
|
|
22978
23183
|
}
|
|
22979
23184
|
return {
|
|
22980
23185
|
id: completed.outputAssetId,
|
|
22981
|
-
name:
|
|
23186
|
+
name: path21.basename(completed.outputLocalPath),
|
|
22982
23187
|
url: `/files/${completed.outputAssetId}`,
|
|
22983
23188
|
source_id: sourceAssetId,
|
|
22984
23189
|
job_id: result.jobId,
|
|
22985
23190
|
tos_url: result.assetUrl
|
|
22986
23191
|
};
|
|
22987
23192
|
} catch (error2) {
|
|
22988
|
-
|
|
22989
|
-
|
|
23193
|
+
const normalized = normalizeCanvasGenerationError(error2);
|
|
23194
|
+
await failCanvasJob(canvasJob.workspaceDir, canvasJob.operationId, normalized).catch(() => void 0);
|
|
23195
|
+
throw normalized;
|
|
22990
23196
|
}
|
|
22991
23197
|
}
|
|
22992
23198
|
async function ensureReferenceAssetUrl(workspaceDir, assetId) {
|
|
@@ -23004,11 +23210,12 @@ async function uploadInteractiveReference(workspaceDir, dataUrl, imageServiceDep
|
|
|
23004
23210
|
if (!bytes.length) throw httpError(400, "交互编辑图片数据为空");
|
|
23005
23211
|
const extension = match[1].toLowerCase() === "jpeg" ? "jpg" : match[1].toLowerCase();
|
|
23006
23212
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
23007
|
-
const tempDir =
|
|
23008
|
-
await
|
|
23009
|
-
const localPath =
|
|
23010
|
-
await
|
|
23213
|
+
const tempDir = path21.join(paths.canvasDir, "interactive-references");
|
|
23214
|
+
await mkdir7(tempDir, { recursive: true });
|
|
23215
|
+
const localPath = path21.join(tempDir, `${randomUUID4()}.${extension}`);
|
|
23216
|
+
await writeFile4(localPath, bytes);
|
|
23011
23217
|
try {
|
|
23218
|
+
const dimensions = await probeImageDimensions(localPath);
|
|
23012
23219
|
const uploaded = await uploadImageAsset(
|
|
23013
23220
|
localPath,
|
|
23014
23221
|
{ folder: "canvas-interactive-edit" },
|
|
@@ -23018,109 +23225,261 @@ async function uploadInteractiveReference(workspaceDir, dataUrl, imageServiceDep
|
|
|
23018
23225
|
return {
|
|
23019
23226
|
assetUrl: uploaded.assetUrl,
|
|
23020
23227
|
isPng,
|
|
23021
|
-
hasTransparentPixel: isPng && pngHasTransparentPixel(bytes)
|
|
23228
|
+
hasTransparentPixel: isPng && pngHasTransparentPixel(bytes),
|
|
23229
|
+
width: dimensions.width,
|
|
23230
|
+
height: dimensions.height
|
|
23022
23231
|
};
|
|
23023
23232
|
} finally {
|
|
23024
23233
|
await unlink(localPath).catch(() => void 0);
|
|
23025
23234
|
}
|
|
23026
23235
|
}
|
|
23027
|
-
|
|
23236
|
+
function resolveInteractiveEditSize(width, height) {
|
|
23237
|
+
if (!Number.isSafeInteger(width) || width <= 0 || !Number.isSafeInteger(height) || height <= 0) {
|
|
23238
|
+
throw new Error(`交互编辑图片尺寸无效:${width}x${height}`);
|
|
23239
|
+
}
|
|
23240
|
+
const pixels = width * height;
|
|
23241
|
+
if (pixels >= INTERACTIVE_EDIT_MIN_PIXELS && pixels <= INTERACTIVE_EDIT_MAX_PIXELS) {
|
|
23242
|
+
return `${width}x${height}`;
|
|
23243
|
+
}
|
|
23244
|
+
const targetPixels = pixels < INTERACTIVE_EDIT_MIN_PIXELS ? INTERACTIVE_EDIT_MIN_PIXELS : INTERACTIVE_EDIT_MAX_PIXELS;
|
|
23245
|
+
const aspectRatio = width / height;
|
|
23246
|
+
const targetHeight = Math.max(1, Math.round(Math.sqrt(targetPixels / aspectRatio)));
|
|
23247
|
+
const targetWidth = pixels < INTERACTIVE_EDIT_MIN_PIXELS ? Math.ceil(targetPixels / targetHeight) : Math.max(1, Math.floor(targetPixels / targetHeight));
|
|
23248
|
+
return `${targetWidth}x${targetHeight}`;
|
|
23249
|
+
}
|
|
23250
|
+
async function extractVideoAssetFrames(workspaceDir, assetId, count, requestedOperationId = "") {
|
|
23251
|
+
if (count < 2 || count > 30) throw httpError(400, "抽取帧数需在 2–30 之间");
|
|
23028
23252
|
const sourcePath = resolveAssetPath(workspaceDir, assetId);
|
|
23029
|
-
if (mediaKind(
|
|
23030
|
-
throw httpError(400, "
|
|
23253
|
+
if (mediaKind(path21.extname(sourcePath)) !== "video") {
|
|
23254
|
+
throw httpError(400, "仅支持将视频转换为序列帧图");
|
|
23031
23255
|
}
|
|
23032
23256
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
23033
|
-
const
|
|
23034
|
-
|
|
23257
|
+
const batch = randomUUID4().slice(0, 8);
|
|
23258
|
+
let extractionDir = "";
|
|
23259
|
+
let outputPath = "";
|
|
23035
23260
|
const job = await beginCanvasJob({
|
|
23036
23261
|
operationId: requestedOperationId || void 0,
|
|
23037
23262
|
workspaceDir,
|
|
23038
|
-
kind: "
|
|
23263
|
+
kind: "video_frame_extract",
|
|
23039
23264
|
parentAssetIds: [assetId],
|
|
23040
|
-
outputKind: "
|
|
23265
|
+
outputKind: "image",
|
|
23266
|
+
metadata: { name: `序列帧图 · ${path21.basename(sourcePath)}` }
|
|
23041
23267
|
});
|
|
23042
23268
|
try {
|
|
23269
|
+
extractionDir = await mkdtemp2(path21.join(paths.canvasDir, "video-frames-"));
|
|
23043
23270
|
await updateCanvasJob(workspaceDir, job.operationId, { status: "RUNNING" });
|
|
23044
|
-
const result = await
|
|
23271
|
+
const [result, sourceDuration] = await Promise.all([
|
|
23272
|
+
extractVideoFrames(
|
|
23273
|
+
sourcePath,
|
|
23274
|
+
extractionDir,
|
|
23275
|
+
count,
|
|
23276
|
+
(index) => `frame_${String(index + 1).padStart(2, "0")}.jpg`
|
|
23277
|
+
),
|
|
23278
|
+
probeVideoDuration(sourcePath)
|
|
23279
|
+
]);
|
|
23280
|
+
const columns = Math.max(1, Math.ceil(Math.sqrt(result.files.length)));
|
|
23281
|
+
const rows = Math.max(1, Math.ceil(result.files.length / columns));
|
|
23282
|
+
const fps = result.files.length / sourceDuration;
|
|
23283
|
+
const sourceName = path21.parse(sourcePath).name;
|
|
23284
|
+
const filename = uniqueAssetName(`${sourceName}_${batch}_${result.files.length}frames.png`);
|
|
23285
|
+
outputPath = path21.join(paths.assetsDir, filename);
|
|
23286
|
+
const sheet = await composeSequenceSheet(result.files, outputPath, rows, columns);
|
|
23045
23287
|
const completed = await completeCanvasJob({
|
|
23046
23288
|
operationId: job.operationId,
|
|
23047
23289
|
workspaceDir,
|
|
23048
|
-
outputs: [{ localPath:
|
|
23049
|
-
metadata: {
|
|
23290
|
+
outputs: [{ localPath: sheet.file, kind: "image" }],
|
|
23291
|
+
metadata: {
|
|
23292
|
+
...job.metadata,
|
|
23293
|
+
kind: "frame_sheet",
|
|
23294
|
+
frameCount: result.files.length,
|
|
23295
|
+
fps,
|
|
23296
|
+
sourceDuration,
|
|
23297
|
+
frameNumbers: result.frameNumbers,
|
|
23298
|
+
totalFrames: result.totalFrames,
|
|
23299
|
+
rows,
|
|
23300
|
+
columns,
|
|
23301
|
+
cellWidth: sheet.cellWidth,
|
|
23302
|
+
cellHeight: sheet.cellHeight,
|
|
23303
|
+
width: sheet.width,
|
|
23304
|
+
height: sheet.height,
|
|
23305
|
+
order: "row-major"
|
|
23306
|
+
}
|
|
23050
23307
|
});
|
|
23051
23308
|
const outputId = completed?.outputAssetId;
|
|
23052
|
-
if (!outputId) throw new Error("
|
|
23309
|
+
if (!outputId) throw new Error("视频序列帧图完成但没有产物记录");
|
|
23053
23310
|
return {
|
|
23054
23311
|
id: outputId,
|
|
23055
23312
|
name: filename,
|
|
23056
|
-
|
|
23313
|
+
source: "assets",
|
|
23314
|
+
kind: "image",
|
|
23057
23315
|
url: `/files/${outputId}`,
|
|
23058
23316
|
operation_id: job.operationId,
|
|
23059
|
-
|
|
23317
|
+
count: result.files.length,
|
|
23318
|
+
fps,
|
|
23319
|
+
source_duration: sourceDuration,
|
|
23320
|
+
total_frames: result.totalFrames,
|
|
23321
|
+
frame_numbers: result.frameNumbers,
|
|
23322
|
+
rows,
|
|
23323
|
+
columns,
|
|
23324
|
+
width: sheet.width,
|
|
23325
|
+
height: sheet.height,
|
|
23326
|
+
cell_width: sheet.cellWidth,
|
|
23327
|
+
cell_height: sheet.cellHeight
|
|
23060
23328
|
};
|
|
23061
23329
|
} catch (error2) {
|
|
23330
|
+
if (outputPath) await unlink(outputPath).catch(() => void 0);
|
|
23062
23331
|
await failCanvasJob(workspaceDir, job.operationId, error2).catch(() => void 0);
|
|
23063
23332
|
throw error2;
|
|
23333
|
+
} finally {
|
|
23334
|
+
if (extractionDir) {
|
|
23335
|
+
await rm4(extractionDir, { recursive: true, force: true }).catch(() => void 0);
|
|
23336
|
+
}
|
|
23064
23337
|
}
|
|
23065
23338
|
}
|
|
23066
|
-
async function
|
|
23067
|
-
|
|
23068
|
-
const
|
|
23069
|
-
|
|
23070
|
-
|
|
23339
|
+
async function editFrameSheetAsset(workspaceDir, body) {
|
|
23340
|
+
const sourceAssetId = String(body.source_id || "").trim();
|
|
23341
|
+
const sourceRows = Number(body.source_rows);
|
|
23342
|
+
const sourceColumns = Number(body.source_columns);
|
|
23343
|
+
const rows = Number(body.rows);
|
|
23344
|
+
const columns = Number(body.columns);
|
|
23345
|
+
const fps = Number(body.fps);
|
|
23346
|
+
if (!Number.isInteger(sourceRows) || !Number.isInteger(sourceColumns) || sourceRows < 1 || sourceColumns < 1 || sourceRows > 64 || sourceColumns > 64) {
|
|
23347
|
+
throw httpError(400, "原图切分的行数与列数需为 1–64 的整数");
|
|
23348
|
+
}
|
|
23349
|
+
if (!Number.isInteger(rows) || !Number.isInteger(columns) || rows < 1 || columns < 1 || rows > 64 || columns > 64 || rows * columns < 2) {
|
|
23350
|
+
throw httpError(400, "输出序列帧的行数与列数需为 1–64 的整数,且至少两格");
|
|
23351
|
+
}
|
|
23352
|
+
if (!Number.isFinite(fps) || fps <= 0) {
|
|
23353
|
+
throw httpError(400, "序列帧 FPS 必须大于 0");
|
|
23354
|
+
}
|
|
23355
|
+
const sourcePath = resolveAssetPath(workspaceDir, sourceAssetId);
|
|
23356
|
+
if (mediaKind(path21.extname(sourcePath)) !== "image") {
|
|
23357
|
+
throw httpError(400, "序列帧编辑来源必须是图片");
|
|
23071
23358
|
}
|
|
23359
|
+
const rawFrames = Array.isArray(body.frames) ? body.frames : [];
|
|
23360
|
+
if (rawFrames.length < 2) throw httpError(400, "至少需要两帧才能保存序列帧");
|
|
23361
|
+
if (rawFrames.length > rows * columns) {
|
|
23362
|
+
throw httpError(400, `格子不足:${rows}×${columns}=${rows * columns},但有 ${rawFrames.length} 帧`);
|
|
23363
|
+
}
|
|
23364
|
+
if (rawFrames.length > 256) throw httpError(400, "一次最多编辑 256 帧");
|
|
23365
|
+
const frames = rawFrames.map((value, frameIndex) => {
|
|
23366
|
+
if (!value || typeof value !== "object") {
|
|
23367
|
+
throw httpError(400, `第 ${frameIndex + 1} 帧无效`);
|
|
23368
|
+
}
|
|
23369
|
+
const record2 = value;
|
|
23370
|
+
if (record2.kind === "source_cell") {
|
|
23371
|
+
const index = Number(record2.index);
|
|
23372
|
+
if (!Number.isInteger(index) || index < 0 || index >= sourceRows * sourceColumns) {
|
|
23373
|
+
throw httpError(400, `第 ${frameIndex + 1} 帧的原图格子索引无效`);
|
|
23374
|
+
}
|
|
23375
|
+
return { kind: "source_cell", index };
|
|
23376
|
+
}
|
|
23377
|
+
if (record2.kind === "asset") {
|
|
23378
|
+
const assetId = String(record2.asset_id || "").trim();
|
|
23379
|
+
if (!assetId) throw httpError(400, `第 ${frameIndex + 1} 帧缺少图片素材`);
|
|
23380
|
+
const assetPath = resolveAssetPath(workspaceDir, assetId);
|
|
23381
|
+
if (mediaKind(path21.extname(assetPath)) !== "image") {
|
|
23382
|
+
throw httpError(400, `第 ${frameIndex + 1} 帧引用的素材不是图片`);
|
|
23383
|
+
}
|
|
23384
|
+
return { kind: "asset", assetId };
|
|
23385
|
+
}
|
|
23386
|
+
throw httpError(400, `第 ${frameIndex + 1} 帧类型无效`);
|
|
23387
|
+
});
|
|
23072
23388
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
23073
|
-
const batch =
|
|
23389
|
+
const batch = randomUUID4().slice(0, 8);
|
|
23390
|
+
const sourceName = path21.parse(sourcePath).name;
|
|
23391
|
+
const filename = uniqueAssetName(`${sourceName}_edited_${batch}_${frames.length}frames.png`);
|
|
23392
|
+
const outputPath = path21.join(paths.assetsDir, filename);
|
|
23393
|
+
const temporaryDir = await mkdtemp2(path21.join(paths.canvasDir, "frame-sheet-edit-"));
|
|
23394
|
+
const parentAssetIds = [
|
|
23395
|
+
sourceAssetId,
|
|
23396
|
+
...frames.filter(
|
|
23397
|
+
(frame) => frame.kind === "asset" && Boolean(frame.assetId)
|
|
23398
|
+
).map((frame) => frame.assetId)
|
|
23399
|
+
].filter((value, index, values) => values.indexOf(value) === index);
|
|
23074
23400
|
const job = await beginCanvasJob({
|
|
23075
|
-
operationId:
|
|
23401
|
+
operationId: String(body.client_operation_id || "") || void 0,
|
|
23076
23402
|
workspaceDir,
|
|
23077
|
-
kind: "
|
|
23078
|
-
parentAssetIds
|
|
23403
|
+
kind: "frame_sheet_edit",
|
|
23404
|
+
parentAssetIds,
|
|
23079
23405
|
outputKind: "image",
|
|
23080
|
-
|
|
23081
|
-
outputLayoutId: `sequence_${randomUUID3().replace(/-/g, "")}`,
|
|
23082
|
-
metadata: { name: `序列 · ${path20.basename(sourcePath)}`, loop: true }
|
|
23406
|
+
metadata: { name: `序列帧编辑 · ${path21.basename(sourcePath)}` }
|
|
23083
23407
|
});
|
|
23084
23408
|
try {
|
|
23085
23409
|
await updateCanvasJob(workspaceDir, job.operationId, { status: "RUNNING" });
|
|
23086
|
-
const
|
|
23087
|
-
|
|
23088
|
-
|
|
23089
|
-
|
|
23090
|
-
(
|
|
23091
|
-
);
|
|
23410
|
+
const sourceDimensions = await probeImageDimensions(sourcePath);
|
|
23411
|
+
const sourceCellWidth = Math.ceil(sourceDimensions.width / sourceColumns);
|
|
23412
|
+
const sourceCellHeight = Math.ceil(sourceDimensions.height / sourceRows);
|
|
23413
|
+
const sourceIndexes = [...new Set(
|
|
23414
|
+
frames.filter((frame) => frame.kind === "source_cell").map((frame) => Number(frame.index))
|
|
23415
|
+
)];
|
|
23416
|
+
const sourceFiles = /* @__PURE__ */ new Map();
|
|
23417
|
+
if (sourceIndexes.length) {
|
|
23418
|
+
const split = await manualSplitImage(
|
|
23419
|
+
sourcePath,
|
|
23420
|
+
temporaryDir,
|
|
23421
|
+
(index) => `source_cell_${String(index + 1).padStart(4, "0")}.png`,
|
|
23422
|
+
sourceRows,
|
|
23423
|
+
sourceColumns,
|
|
23424
|
+
{ allowSingleCell: true, indexes: sourceIndexes }
|
|
23425
|
+
);
|
|
23426
|
+
sourceIndexes.forEach((index, position) => sourceFiles.set(index, split.files[position]));
|
|
23427
|
+
}
|
|
23428
|
+
const framePaths = frames.map((frame) => {
|
|
23429
|
+
if (frame.kind === "asset") return resolveAssetPath(workspaceDir, frame.assetId || "");
|
|
23430
|
+
const sourceFile = sourceFiles.get(Number(frame.index));
|
|
23431
|
+
if (!sourceFile) throw new Error(`无法读取原图第 ${Number(frame.index) + 1} 格`);
|
|
23432
|
+
return sourceFile;
|
|
23433
|
+
});
|
|
23434
|
+
const sheet = await composeSequenceSheet(framePaths, outputPath, rows, columns, {
|
|
23435
|
+
cellWidth: sourceCellWidth,
|
|
23436
|
+
cellHeight: sourceCellHeight,
|
|
23437
|
+
fit: "contain"
|
|
23438
|
+
});
|
|
23092
23439
|
const completed = await completeCanvasJob({
|
|
23093
23440
|
operationId: job.operationId,
|
|
23094
23441
|
workspaceDir,
|
|
23095
|
-
outputs:
|
|
23096
|
-
presentation: "sequence",
|
|
23097
|
-
outputLayoutId: job.outputLayoutId,
|
|
23442
|
+
outputs: [{ localPath: sheet.file, kind: "image" }],
|
|
23098
23443
|
metadata: {
|
|
23099
23444
|
...job.metadata,
|
|
23100
|
-
|
|
23101
|
-
|
|
23102
|
-
|
|
23445
|
+
kind: "frame_sheet",
|
|
23446
|
+
frameCount: frames.length,
|
|
23447
|
+
fps,
|
|
23448
|
+
rows,
|
|
23449
|
+
columns,
|
|
23450
|
+
cellWidth: sheet.cellWidth,
|
|
23451
|
+
cellHeight: sheet.cellHeight,
|
|
23452
|
+
width: sheet.width,
|
|
23453
|
+
height: sheet.height,
|
|
23454
|
+
sourceRows,
|
|
23455
|
+
sourceColumns,
|
|
23456
|
+
order: "row-major"
|
|
23103
23457
|
}
|
|
23104
23458
|
});
|
|
23105
|
-
const
|
|
23106
|
-
|
|
23107
|
-
|
|
23108
|
-
|
|
23459
|
+
const outputId = completed?.outputAssetId;
|
|
23460
|
+
if (!outputId) throw new Error("序列帧编辑完成但没有产物记录");
|
|
23461
|
+
return {
|
|
23462
|
+
id: outputId,
|
|
23463
|
+
name: filename,
|
|
23109
23464
|
source: "assets",
|
|
23110
23465
|
kind: "image",
|
|
23111
|
-
url: `/files/${
|
|
23112
|
-
}));
|
|
23113
|
-
return {
|
|
23114
|
-
images,
|
|
23115
|
-
sequence_id: completed?.outputLayoutId,
|
|
23466
|
+
url: `/files/${outputId}`,
|
|
23116
23467
|
operation_id: job.operationId,
|
|
23117
|
-
count:
|
|
23118
|
-
|
|
23119
|
-
|
|
23468
|
+
count: frames.length,
|
|
23469
|
+
fps,
|
|
23470
|
+
rows,
|
|
23471
|
+
columns,
|
|
23472
|
+
width: sheet.width,
|
|
23473
|
+
height: sheet.height,
|
|
23474
|
+
cell_width: sheet.cellWidth,
|
|
23475
|
+
cell_height: sheet.cellHeight
|
|
23120
23476
|
};
|
|
23121
23477
|
} catch (error2) {
|
|
23478
|
+
await unlink(outputPath).catch(() => void 0);
|
|
23122
23479
|
await failCanvasJob(workspaceDir, job.operationId, error2).catch(() => void 0);
|
|
23123
23480
|
throw error2;
|
|
23481
|
+
} finally {
|
|
23482
|
+
await rm4(temporaryDir, { recursive: true, force: true }).catch(() => void 0);
|
|
23124
23483
|
}
|
|
23125
23484
|
}
|
|
23126
23485
|
async function manualSplitImageAsset(workspaceDir, body) {
|
|
@@ -23131,11 +23490,11 @@ async function manualSplitImageAsset(workspaceDir, body) {
|
|
|
23131
23490
|
throw httpError(400, "手动切分的行数与列数需为 1–64 的整数,且至少切为两格");
|
|
23132
23491
|
}
|
|
23133
23492
|
const sourcePath = resolveAssetPath(workspaceDir, sourceAssetId);
|
|
23134
|
-
if (mediaKind(
|
|
23493
|
+
if (mediaKind(path21.extname(sourcePath)) !== "image") {
|
|
23135
23494
|
throw httpError(400, "手动切分来源必须是图片");
|
|
23136
23495
|
}
|
|
23137
23496
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
23138
|
-
const batch =
|
|
23497
|
+
const batch = randomUUID4().slice(0, 8);
|
|
23139
23498
|
const job = await beginCanvasJob({
|
|
23140
23499
|
operationId: String(body.client_operation_id || "") || void 0,
|
|
23141
23500
|
workspaceDir,
|
|
@@ -23168,7 +23527,7 @@ async function manualSplitImageAsset(workspaceDir, body) {
|
|
|
23168
23527
|
});
|
|
23169
23528
|
const images = (completed?.outputAssetIds ?? []).map((id) => ({
|
|
23170
23529
|
id,
|
|
23171
|
-
name:
|
|
23530
|
+
name: path21.basename(relativePathFromAssetId(id)),
|
|
23172
23531
|
source: "assets",
|
|
23173
23532
|
kind: "image",
|
|
23174
23533
|
url: `/files/${id}`
|
|
@@ -23194,9 +23553,9 @@ async function splitImageAsset(workspaceDir, body) {
|
|
|
23194
23553
|
throw httpError(400, "智能切分基准点无效");
|
|
23195
23554
|
}
|
|
23196
23555
|
const sourcePath = resolveAssetPath(workspaceDir, sourceAssetId);
|
|
23197
|
-
if (mediaKind(
|
|
23556
|
+
if (mediaKind(path21.extname(sourcePath)) !== "image") throw httpError(400, "智能切分来源必须是图片");
|
|
23198
23557
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
23199
|
-
const batch =
|
|
23558
|
+
const batch = randomUUID4().slice(0, 8);
|
|
23200
23559
|
const job = await beginCanvasJob({
|
|
23201
23560
|
operationId: String(body.client_operation_id || "") || void 0,
|
|
23202
23561
|
workspaceDir,
|
|
@@ -23226,7 +23585,7 @@ async function splitImageAsset(workspaceDir, body) {
|
|
|
23226
23585
|
});
|
|
23227
23586
|
const images = (completed?.outputAssetIds ?? []).map((id) => ({
|
|
23228
23587
|
id,
|
|
23229
|
-
name:
|
|
23588
|
+
name: path21.basename(relativePathFromAssetId(id)),
|
|
23230
23589
|
source: "assets",
|
|
23231
23590
|
kind: "image",
|
|
23232
23591
|
url: `/files/${id}`
|
|
@@ -23245,57 +23604,13 @@ async function splitImageAsset(workspaceDir, body) {
|
|
|
23245
23604
|
throw error2;
|
|
23246
23605
|
}
|
|
23247
23606
|
}
|
|
23248
|
-
async function composeSequenceAsset(workspaceDir, body) {
|
|
23249
|
-
const frameIds = Array.isArray(body.frame_ids) ? body.frame_ids.map(String).filter(Boolean) : [];
|
|
23250
|
-
if (!frameIds.length) throw httpError(400, "序列没有可拼接的帧");
|
|
23251
|
-
const rows = Math.round(Number(body.rows));
|
|
23252
|
-
const columns = Math.round(Number(body.columns));
|
|
23253
|
-
const framePaths = frameIds.map((id) => resolveAssetPath(workspaceDir, id));
|
|
23254
|
-
if (framePaths.some((file) => mediaKind(path20.extname(file)) !== "image")) {
|
|
23255
|
-
throw httpError(400, "序列拼图只支持图片帧");
|
|
23256
|
-
}
|
|
23257
|
-
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
23258
|
-
const filename = uniqueAssetName(`sheet_${rows}x${columns}.png`);
|
|
23259
|
-
const outputPath = path20.join(paths.assetsDir, filename);
|
|
23260
|
-
const job = await beginCanvasJob({
|
|
23261
|
-
operationId: String(body.client_operation_id || "") || void 0,
|
|
23262
|
-
workspaceDir,
|
|
23263
|
-
kind: "sequence_sheet",
|
|
23264
|
-
parentAssetIds: frameIds,
|
|
23265
|
-
outputKind: "image",
|
|
23266
|
-
metadata: { rows, columns, frameCount: frameIds.length }
|
|
23267
|
-
});
|
|
23268
|
-
try {
|
|
23269
|
-
await updateCanvasJob(workspaceDir, job.operationId, { status: "RUNNING" });
|
|
23270
|
-
const result = await composeSequenceSheet(framePaths, outputPath, rows, columns);
|
|
23271
|
-
const completed = await completeCanvasJob({
|
|
23272
|
-
operationId: job.operationId,
|
|
23273
|
-
workspaceDir,
|
|
23274
|
-
outputs: [{ localPath: result.file, kind: "image" }],
|
|
23275
|
-
metadata: { ...job.metadata, width: result.width, height: result.height }
|
|
23276
|
-
});
|
|
23277
|
-
const outputId = completed?.outputAssetId;
|
|
23278
|
-
if (!outputId) throw new Error("序列拼图完成但没有产物记录");
|
|
23279
|
-
return {
|
|
23280
|
-
id: outputId,
|
|
23281
|
-
name: filename,
|
|
23282
|
-
kind: "image",
|
|
23283
|
-
url: `/files/${outputId}`,
|
|
23284
|
-
operation_id: job.operationId,
|
|
23285
|
-
...result
|
|
23286
|
-
};
|
|
23287
|
-
} catch (error2) {
|
|
23288
|
-
await failCanvasJob(workspaceDir, job.operationId, error2).catch(() => void 0);
|
|
23289
|
-
throw error2;
|
|
23290
|
-
}
|
|
23291
|
-
}
|
|
23292
23607
|
async function extractVideoCycleAsset(workspaceDir, body) {
|
|
23293
23608
|
const assetId = String(body.id || "").trim();
|
|
23294
23609
|
const sourcePath = resolveAssetPath(workspaceDir, assetId);
|
|
23295
|
-
if (mediaKind(
|
|
23610
|
+
if (mediaKind(path21.extname(sourcePath)) !== "video") throw httpError(400, "循环提取来源必须是视频");
|
|
23296
23611
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
23297
23612
|
const filename = uniqueAssetName("cycle.mp4");
|
|
23298
|
-
const outputPath =
|
|
23613
|
+
const outputPath = path21.join(paths.assetsDir, filename);
|
|
23299
23614
|
const job = await beginCanvasJob({
|
|
23300
23615
|
operationId: String(body.client_operation_id || "") || void 0,
|
|
23301
23616
|
workspaceDir,
|
|
@@ -23339,13 +23654,13 @@ async function layoutWithProvenance(workspaceDir, scannedAssets) {
|
|
|
23339
23654
|
return provenance ? { ...item, provenance } : item;
|
|
23340
23655
|
})
|
|
23341
23656
|
);
|
|
23342
|
-
return { items };
|
|
23657
|
+
return { ...layout, items };
|
|
23343
23658
|
}
|
|
23344
23659
|
async function saveUploadedFile(workspaceDir, rawName, rawData, requestedKind) {
|
|
23345
23660
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
23346
23661
|
const fallbackName = requestedKind === "video" ? "video.mp4" : requestedKind === "audio" ? "audio.mp3" : "image.png";
|
|
23347
|
-
const originalName =
|
|
23348
|
-
const extension =
|
|
23662
|
+
const originalName = path21.basename(String(rawName || fallbackName));
|
|
23663
|
+
const extension = path21.extname(originalName).toLowerCase();
|
|
23349
23664
|
const kind = mediaKind(extension);
|
|
23350
23665
|
if (!kind || kind !== requestedKind) {
|
|
23351
23666
|
throw httpError(400, `不支持的${requestedKind === "video" ? "视频" : "图片"}类型:${extension || "(无后缀)"}`);
|
|
@@ -23356,8 +23671,8 @@ async function saveUploadedFile(workspaceDir, rawName, rawData, requestedKind) {
|
|
|
23356
23671
|
const bytes = Buffer.from(base642, "base64");
|
|
23357
23672
|
if (!bytes.length) throw httpError(400, "媒体数据为空");
|
|
23358
23673
|
const filename = uniqueAssetName(originalName);
|
|
23359
|
-
const localPath =
|
|
23360
|
-
await
|
|
23674
|
+
const localPath = path21.join(paths.assetsDir, filename);
|
|
23675
|
+
await writeFile4(localPath, bytes);
|
|
23361
23676
|
const info = await stat6(localPath);
|
|
23362
23677
|
const asset = {
|
|
23363
23678
|
id: assetIdFromRelativePath(filename),
|
|
@@ -23386,19 +23701,19 @@ async function saveUploadedFile(workspaceDir, rawName, rawData, requestedKind) {
|
|
|
23386
23701
|
async function duplicateAsset(workspaceDir, assetId) {
|
|
23387
23702
|
const sourcePath = resolveAssetPath(workspaceDir, assetId);
|
|
23388
23703
|
const relative = relativePathFromAssetId(assetId);
|
|
23389
|
-
const parsed =
|
|
23390
|
-
const outputRelative = [parsed.dir, `${parsed.name}_${
|
|
23704
|
+
const parsed = path21.posix.parse(relative);
|
|
23705
|
+
const outputRelative = [parsed.dir, `${parsed.name}_${randomUUID4().slice(0, 8)}${parsed.ext}`].filter(Boolean).join("/");
|
|
23391
23706
|
const outputId = assetIdFromRelativePath(outputRelative);
|
|
23392
23707
|
const outputPath = resolveAssetPath(workspaceDir, outputId);
|
|
23393
|
-
await
|
|
23708
|
+
await mkdir7(path21.dirname(outputPath), { recursive: true });
|
|
23394
23709
|
await copyFile3(sourcePath, outputPath);
|
|
23395
23710
|
await recordCopiedAsset(workspaceDir, assetId, outputId);
|
|
23396
23711
|
const info = await stat6(outputPath);
|
|
23397
|
-
const kind = mediaKind(
|
|
23712
|
+
const kind = mediaKind(path21.extname(outputPath));
|
|
23398
23713
|
if (!kind) throw httpError(400, "不支持的素材类型");
|
|
23399
23714
|
return {
|
|
23400
23715
|
id: outputId,
|
|
23401
|
-
name:
|
|
23716
|
+
name: path21.basename(outputPath),
|
|
23402
23717
|
relativePath: outputRelative,
|
|
23403
23718
|
localPath: outputPath,
|
|
23404
23719
|
kind,
|
|
@@ -23411,13 +23726,13 @@ async function importClipboardFiles(workspaceDir, sourcePaths) {
|
|
|
23411
23726
|
const paths = await ensureCanvasWorkspace(workspaceDir);
|
|
23412
23727
|
const imported = [];
|
|
23413
23728
|
for (const rawSourcePath of [...new Set(sourcePaths.map(String).filter(Boolean))]) {
|
|
23414
|
-
const sourcePath =
|
|
23729
|
+
const sourcePath = path21.resolve(rawSourcePath);
|
|
23415
23730
|
const info = await stat6(sourcePath).catch(() => void 0);
|
|
23416
23731
|
if (!info?.isFile()) continue;
|
|
23417
|
-
const kind = mediaKind(
|
|
23732
|
+
const kind = mediaKind(path21.extname(sourcePath));
|
|
23418
23733
|
if (!kind || kind === "audio") continue;
|
|
23419
|
-
const filename = uniqueAssetName(
|
|
23420
|
-
const localPath =
|
|
23734
|
+
const filename = uniqueAssetName(path21.basename(sourcePath));
|
|
23735
|
+
const localPath = path21.join(paths.assetsDir, filename);
|
|
23421
23736
|
await copyFile3(sourcePath, localPath);
|
|
23422
23737
|
const copiedInfo = await stat6(localPath);
|
|
23423
23738
|
const assetId = assetIdFromRelativePath(filename);
|
|
@@ -23431,12 +23746,12 @@ async function importClipboardFiles(workspaceDir, sourcePaths) {
|
|
|
23431
23746
|
url: `/files/${assetId}`,
|
|
23432
23747
|
mtime: copiedInfo.mtimeMs
|
|
23433
23748
|
};
|
|
23434
|
-
const relativeSource =
|
|
23435
|
-
const isWorkspaceAsset = relativeSource !== "" && !relativeSource.startsWith(`..${
|
|
23749
|
+
const relativeSource = path21.relative(paths.assetsDir, sourcePath);
|
|
23750
|
+
const isWorkspaceAsset = relativeSource !== "" && !relativeSource.startsWith(`..${path21.sep}`) && relativeSource !== ".." && !path21.isAbsolute(relativeSource);
|
|
23436
23751
|
if (isWorkspaceAsset) {
|
|
23437
23752
|
await recordCopiedAsset(
|
|
23438
23753
|
workspaceDir,
|
|
23439
|
-
assetIdFromRelativePath(relativeSource.split(
|
|
23754
|
+
assetIdFromRelativePath(relativeSource.split(path21.sep).join("/")),
|
|
23440
23755
|
asset.id
|
|
23441
23756
|
);
|
|
23442
23757
|
} else {
|
|
@@ -23457,32 +23772,12 @@ async function importClipboardFiles(workspaceDir, sourcePaths) {
|
|
|
23457
23772
|
}
|
|
23458
23773
|
return imported;
|
|
23459
23774
|
}
|
|
23460
|
-
function normalizeClipboardSequence(value, fileCount) {
|
|
23461
|
-
if (!value || typeof value !== "object" || Array.isArray(value) || fileCount < 1) return void 0;
|
|
23462
|
-
const raw = value;
|
|
23463
|
-
if (raw.kind !== "sequence") return void 0;
|
|
23464
|
-
const expectedFrameCount = Number(raw.frameCount);
|
|
23465
|
-
if (Number.isFinite(expectedFrameCount) && expectedFrameCount !== fileCount) return void 0;
|
|
23466
|
-
const fps = Number(raw.fps);
|
|
23467
|
-
return {
|
|
23468
|
-
kind: "sequence",
|
|
23469
|
-
version: 1,
|
|
23470
|
-
name: String(raw.name || "序列").slice(0, 200),
|
|
23471
|
-
fps: Number.isFinite(fps) && fps > 0 ? Math.min(120, fps) : 8,
|
|
23472
|
-
loop: raw.loop !== false,
|
|
23473
|
-
frameCount: fileCount
|
|
23474
|
-
};
|
|
23475
|
-
}
|
|
23476
23775
|
async function deleteCanvasItems(workspaceDir, body) {
|
|
23477
23776
|
const imageIds = Array.isArray(body.image_ids) ? body.image_ids.map(String) : [];
|
|
23478
23777
|
const layoutIds = Array.isArray(body.layout_ids) ? body.layout_ids.map(String) : [];
|
|
23479
|
-
const layout = await loadCanvasLayout(workspaceDir);
|
|
23480
23778
|
const files = new Set(imageIds);
|
|
23481
23779
|
for (const layoutId of layoutIds) {
|
|
23482
|
-
|
|
23483
|
-
if (item?.type === "sequence" && Array.isArray(item.frames)) {
|
|
23484
|
-
item.frames.map(String).forEach((frame) => files.add(frame));
|
|
23485
|
-
} else if (layoutId.startsWith("assets/")) {
|
|
23780
|
+
if (layoutId.startsWith("assets/")) {
|
|
23486
23781
|
files.add(layoutId);
|
|
23487
23782
|
} else if (layoutId.startsWith("job_") || layoutId.startsWith("pending_")) {
|
|
23488
23783
|
await hideCanvasJob(workspaceDir, layoutId);
|
|
@@ -23500,26 +23795,33 @@ async function deleteCanvasItems(workspaceDir, body) {
|
|
|
23500
23795
|
}
|
|
23501
23796
|
}
|
|
23502
23797
|
const remove = /* @__PURE__ */ new Set([...files, ...layoutIds]);
|
|
23503
|
-
await
|
|
23504
|
-
|
|
23505
|
-
|
|
23506
|
-
|
|
23507
|
-
});
|
|
23798
|
+
await mutateCanvasLayout(
|
|
23799
|
+
workspaceDir,
|
|
23800
|
+
(items) => items.filter((item) => !remove.has(item.id) && item.type !== "sequence")
|
|
23801
|
+
);
|
|
23508
23802
|
return { deleted, missing, layout: await reconcileCanvasLayout(workspaceDir) };
|
|
23509
23803
|
}
|
|
23510
23804
|
async function placeAssets(workspaceDir, assets) {
|
|
23511
|
-
|
|
23512
|
-
|
|
23513
|
-
|
|
23514
|
-
|
|
23515
|
-
|
|
23516
|
-
|
|
23517
|
-
|
|
23805
|
+
await reconcileCanvasLayout(workspaceDir);
|
|
23806
|
+
await mutateCanvasLayout(workspaceDir, (items) => {
|
|
23807
|
+
const ids = new Set(items.map((item) => item.assetId || item.id));
|
|
23808
|
+
for (const asset of assets) {
|
|
23809
|
+
if (ids.has(asset.id)) continue;
|
|
23810
|
+
items.push({
|
|
23811
|
+
id: asset.id,
|
|
23812
|
+
assetId: asset.id,
|
|
23813
|
+
...asset.kind === "video" ? { type: "video" } : {}
|
|
23814
|
+
});
|
|
23815
|
+
ids.add(asset.id);
|
|
23816
|
+
}
|
|
23817
|
+
});
|
|
23518
23818
|
}
|
|
23519
23819
|
function sanitizeLayoutItems(value) {
|
|
23520
23820
|
if (!Array.isArray(value)) return [];
|
|
23521
23821
|
return value.filter(
|
|
23522
|
-
(item) => Boolean(
|
|
23822
|
+
(item) => Boolean(
|
|
23823
|
+
item && typeof item === "object" && typeof item.id === "string" && item.type !== "sequence"
|
|
23824
|
+
)
|
|
23523
23825
|
);
|
|
23524
23826
|
}
|
|
23525
23827
|
function publicAsset(asset) {
|
|
@@ -23536,7 +23838,7 @@ function projectPayload(workspaceDir) {
|
|
|
23536
23838
|
return { project: projectInfo(workspaceDir), recent: [] };
|
|
23537
23839
|
}
|
|
23538
23840
|
function projectInfo(workspaceDir) {
|
|
23539
|
-
return { path: workspaceDir, name:
|
|
23841
|
+
return { path: workspaceDir, name: path21.basename(workspaceDir) || workspaceDir };
|
|
23540
23842
|
}
|
|
23541
23843
|
async function readJsonBody(request) {
|
|
23542
23844
|
const chunks = [];
|
|
@@ -23556,10 +23858,10 @@ async function readJsonBody(request) {
|
|
|
23556
23858
|
}
|
|
23557
23859
|
async function servePublic(request, response, publicDir, pathname) {
|
|
23558
23860
|
const relative = pathname === "/" ? "index.html" : decodeURIComponent(pathname).replace(/^\/+/, "");
|
|
23559
|
-
const full =
|
|
23861
|
+
const full = path21.resolve(publicDir, relative);
|
|
23560
23862
|
if (!pathContained2(publicDir, full) || !existsSync(full)) throw httpError(404, "Not Found");
|
|
23561
23863
|
if (relative === "styles.css") {
|
|
23562
|
-
const css = await
|
|
23864
|
+
const css = await readFile4(full, "utf8");
|
|
23563
23865
|
const extra = "\n#switch-project-btn { display: none !important; }\n";
|
|
23564
23866
|
sendText(response, 200, css + extra, "text/css; charset=utf-8");
|
|
23565
23867
|
return;
|
|
@@ -23569,22 +23871,22 @@ async function servePublic(request, response, publicDir, pathname) {
|
|
|
23569
23871
|
function resolveCanvasVendorAsset(pathname, publicDir) {
|
|
23570
23872
|
const asset = CANVAS_VENDOR_ASSETS[pathname];
|
|
23571
23873
|
if (!asset) return void 0;
|
|
23572
|
-
const modulePath =
|
|
23573
|
-
const roots = [publicDir, process.cwd(),
|
|
23874
|
+
const modulePath = path21.join(asset.packageName, asset.relativePath);
|
|
23875
|
+
const roots = [publicDir, process.cwd(), path21.dirname(path21.resolve(process.argv[1] || process.cwd()))];
|
|
23574
23876
|
const checked = /* @__PURE__ */ new Set();
|
|
23575
23877
|
for (const root of roots) {
|
|
23576
|
-
let current =
|
|
23878
|
+
let current = path21.resolve(root);
|
|
23577
23879
|
for (; ; ) {
|
|
23578
23880
|
const candidates = [
|
|
23579
|
-
|
|
23580
|
-
...
|
|
23881
|
+
path21.join(current, "node_modules", modulePath),
|
|
23882
|
+
...path21.basename(current).toLowerCase() === "node_modules" ? [path21.join(current, modulePath)] : []
|
|
23581
23883
|
];
|
|
23582
23884
|
for (const candidate of candidates) {
|
|
23583
23885
|
if (checked.has(candidate)) continue;
|
|
23584
23886
|
checked.add(candidate);
|
|
23585
23887
|
if (existsSync(candidate)) return candidate;
|
|
23586
23888
|
}
|
|
23587
|
-
const parent =
|
|
23889
|
+
const parent = path21.dirname(current);
|
|
23588
23890
|
if (parent === current) break;
|
|
23589
23891
|
current = parent;
|
|
23590
23892
|
}
|
|
@@ -23626,28 +23928,28 @@ async function serveFileWithRange(request, response, filePath) {
|
|
|
23626
23928
|
createReadStream(filePath, { start, end }).pipe(response);
|
|
23627
23929
|
}
|
|
23628
23930
|
function resolveCanvasPublicDir() {
|
|
23629
|
-
const executableDir =
|
|
23931
|
+
const executableDir = path21.dirname(path21.resolve(process.argv[1] || process.cwd()));
|
|
23630
23932
|
const sourceCandidates = [
|
|
23631
|
-
|
|
23632
|
-
|
|
23633
|
-
|
|
23634
|
-
|
|
23933
|
+
path21.resolve(executableDir, "canvas/public"),
|
|
23934
|
+
path21.resolve(executableDir, "../src/canvas/public"),
|
|
23935
|
+
path21.resolve(process.cwd(), "src/canvas/public"),
|
|
23936
|
+
path21.resolve(process.cwd(), "plugin_client/src/canvas/public")
|
|
23635
23937
|
];
|
|
23636
23938
|
const packagedCandidates = [
|
|
23637
|
-
|
|
23638
|
-
|
|
23639
|
-
|
|
23939
|
+
path21.resolve(executableDir, "../dist/canvas-public"),
|
|
23940
|
+
path21.resolve(executableDir, "canvas-public"),
|
|
23941
|
+
path21.resolve(process.cwd(), "dist/canvas-public")
|
|
23640
23942
|
];
|
|
23641
23943
|
const candidates = [...sourceCandidates, ...packagedCandidates];
|
|
23642
|
-
const found = candidates.find((candidate) => existsSync(
|
|
23944
|
+
const found = candidates.find((candidate) => existsSync(path21.join(candidate, "index.html")));
|
|
23643
23945
|
if (!found) throw new Error("找不到 Image Canvas 前端资源,请先运行 npm run build");
|
|
23644
23946
|
return found;
|
|
23645
23947
|
}
|
|
23646
23948
|
function uniqueAssetName(originalName) {
|
|
23647
|
-
const extension =
|
|
23648
|
-
const stem =
|
|
23949
|
+
const extension = path21.extname(originalName);
|
|
23950
|
+
const stem = path21.basename(originalName, extension).replace(/[^\p{L}\p{N}_.-]+/gu, "_") || "asset";
|
|
23649
23951
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[-:TZ.]/g, "").slice(0, 14);
|
|
23650
|
-
return `${stamp}_${
|
|
23952
|
+
return `${stamp}_${randomUUID4().slice(0, 8)}_${stem}${extension.toLowerCase()}`;
|
|
23651
23953
|
}
|
|
23652
23954
|
function mediaKind(extension) {
|
|
23653
23955
|
const ext = extension.toLowerCase();
|
|
@@ -23657,7 +23959,7 @@ function mediaKind(extension) {
|
|
|
23657
23959
|
return void 0;
|
|
23658
23960
|
}
|
|
23659
23961
|
function mimeType(filePath) {
|
|
23660
|
-
const ext =
|
|
23962
|
+
const ext = path21.extname(filePath).toLowerCase();
|
|
23661
23963
|
return {
|
|
23662
23964
|
".html": "text/html; charset=utf-8",
|
|
23663
23965
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -23770,9 +24072,9 @@ async function listenOnAvailablePort(server, host, preferredPort) {
|
|
|
23770
24072
|
throw new Error(`端口 ${preferredPort}-${preferredPort + 19} 均被占用`);
|
|
23771
24073
|
}
|
|
23772
24074
|
function pathContained2(base, target) {
|
|
23773
|
-
const root =
|
|
23774
|
-
const full =
|
|
23775
|
-
const prefix = root.endsWith(
|
|
24075
|
+
const root = path21.resolve(base);
|
|
24076
|
+
const full = path21.resolve(target);
|
|
24077
|
+
const prefix = root.endsWith(path21.sep) ? root : `${root}${path21.sep}`;
|
|
23776
24078
|
return full === root || full.startsWith(prefix);
|
|
23777
24079
|
}
|
|
23778
24080
|
function httpError(status, message) {
|
|
@@ -23785,10 +24087,72 @@ function statusFromError(error2) {
|
|
|
23785
24087
|
function errorMessage(error2) {
|
|
23786
24088
|
return error2 instanceof Error ? error2.message : String(error2);
|
|
23787
24089
|
}
|
|
24090
|
+
function canvasGenerationErrorMessage(error2) {
|
|
24091
|
+
const message = incompatibleResponseDetail(error2) ?? errorMessage(error2);
|
|
24092
|
+
const normalized = message.toLowerCase();
|
|
24093
|
+
if (/insufficient[_ -]?quota|quota (?:has been )?(?:exceeded|exhausted)|insufficient (?:balance|credit|funds)|credit balance (?:is )?too low/i.test(message) || /(?:额度(?:不足|已?用完|耗尽)|余额(?:不足|已?用完))/.test(message)) {
|
|
24094
|
+
return CANVAS_QUOTA_ERROR;
|
|
24095
|
+
}
|
|
24096
|
+
const code = String(error2?.code || "").toUpperCase();
|
|
24097
|
+
const name = error2 instanceof Error ? error2.name : "";
|
|
24098
|
+
if (name === "NetworkError" || ["ECONNABORTED", "ECONNREFUSED", "ECONNRESET", "EHOSTUNREACH", "ENETUNREACH", "ETIMEDOUT"].includes(code) || /fetch failed|network(?: request)? (?:error|failed)|socket hang up|connection (?:error|failed|refused|reset|timed out)|request timed out|connecttimeout|网络异常|连接失败/.test(normalized)) {
|
|
24099
|
+
return CANVAS_NETWORK_ERROR;
|
|
24100
|
+
}
|
|
24101
|
+
return message;
|
|
24102
|
+
}
|
|
24103
|
+
function incompatibleResponseDetail(error2) {
|
|
24104
|
+
if (!(error2 instanceof Error) || error2.name !== "IncompatibleResponseError") return void 0;
|
|
24105
|
+
const rawBody = error2.rawBody;
|
|
24106
|
+
if (typeof rawBody !== "string" || !rawBody.trim()) {
|
|
24107
|
+
return "服务端返回了无法解析的响应";
|
|
24108
|
+
}
|
|
24109
|
+
const raw = rawBody.trim();
|
|
24110
|
+
try {
|
|
24111
|
+
const parsed = JSON.parse(raw);
|
|
24112
|
+
const extracted = responseErrorMessage(parsed);
|
|
24113
|
+
if (extracted) return providerMessageFromText(extracted);
|
|
24114
|
+
} catch {
|
|
24115
|
+
}
|
|
24116
|
+
return providerMessageFromText(raw);
|
|
24117
|
+
}
|
|
24118
|
+
function responseErrorMessage(value) {
|
|
24119
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
24120
|
+
const record2 = value;
|
|
24121
|
+
if (typeof record2.error_message === "string" && record2.error_message.trim()) {
|
|
24122
|
+
return record2.error_message.trim();
|
|
24123
|
+
}
|
|
24124
|
+
if (record2.error && typeof record2.error === "object" && !Array.isArray(record2.error)) {
|
|
24125
|
+
const nested = responseErrorMessage(record2.error);
|
|
24126
|
+
if (nested) return nested;
|
|
24127
|
+
}
|
|
24128
|
+
if (record2.data && typeof record2.data === "object" && !Array.isArray(record2.data)) {
|
|
24129
|
+
const nested = responseErrorMessage(record2.data);
|
|
24130
|
+
if (nested) return nested;
|
|
24131
|
+
}
|
|
24132
|
+
if (typeof record2.detail === "string" && record2.detail.trim()) return record2.detail.trim();
|
|
24133
|
+
if (typeof record2.message === "string" && record2.message.trim() && record2.message !== "ok") {
|
|
24134
|
+
return record2.message.trim();
|
|
24135
|
+
}
|
|
24136
|
+
return void 0;
|
|
24137
|
+
}
|
|
24138
|
+
function providerMessageFromText(message) {
|
|
24139
|
+
const pythonPayloadMessage = message.match(
|
|
24140
|
+
/['"]message['"]\s*:\s*(['"])([\s\S]*?)\1\s*,\s*['"](?:type|param|code)['"]\s*:/
|
|
24141
|
+
);
|
|
24142
|
+
return pythonPayloadMessage?.[2]?.trim() || message;
|
|
24143
|
+
}
|
|
24144
|
+
function normalizeCanvasGenerationError(error2) {
|
|
24145
|
+
const message = canvasGenerationErrorMessage(error2);
|
|
24146
|
+
if (error2 instanceof Error && message === error2.message) return error2;
|
|
24147
|
+
const normalized = new Error(message);
|
|
24148
|
+
const status = error2?.status;
|
|
24149
|
+
if (typeof status === "number") Object.assign(normalized, { status });
|
|
24150
|
+
return normalized;
|
|
24151
|
+
}
|
|
23788
24152
|
|
|
23789
24153
|
// src/cli/canvas/command.ts
|
|
23790
24154
|
async function runCanvas(parsed) {
|
|
23791
|
-
const workspaceDir =
|
|
24155
|
+
const workspaceDir = path22.resolve(process.cwd());
|
|
23792
24156
|
const portOption = stringOption(parsed, "port");
|
|
23793
24157
|
const port = portOption === void 0 ? 5178 : Number(portOption);
|
|
23794
24158
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
@@ -24739,7 +25103,7 @@ async function handleLoginEvomeme(deps = defaultDeps4) {
|
|
|
24739
25103
|
}
|
|
24740
25104
|
|
|
24741
25105
|
// src/mcp/tools/submitGameCode.ts
|
|
24742
|
-
import
|
|
25106
|
+
import path23 from "node:path";
|
|
24743
25107
|
var SUBMIT_GAME_CODE_TOOL_NAME = "evomeme_submit_game_code";
|
|
24744
25108
|
var SUBMIT_GAME_CODE_DESCRIPTION = [
|
|
24745
25109
|
"将当前工作区发布为 evomeme云端作品(全量覆盖对应 thread_id 下的全部文件)。",
|
|
@@ -24798,7 +25162,7 @@ var defaultDeps5 = {
|
|
|
24798
25162
|
};
|
|
24799
25163
|
async function handleSubmitGameCode(args = {}, deps = defaultDeps5) {
|
|
24800
25164
|
try {
|
|
24801
|
-
const workspaceDir =
|
|
25165
|
+
const workspaceDir = path23.resolve(args.workspace_dir?.trim() || deps.cwd());
|
|
24802
25166
|
const submitOptions = {};
|
|
24803
25167
|
const name = args.name?.trim();
|
|
24804
25168
|
if (name) {
|
|
@@ -24829,7 +25193,7 @@ function formatSuccess(workspaceDir, result) {
|
|
|
24829
25193
|
}
|
|
24830
25194
|
|
|
24831
25195
|
// src/mcp/tools/uploadGameAsset.ts
|
|
24832
|
-
import
|
|
25196
|
+
import path24 from "node:path";
|
|
24833
25197
|
var UPLOAD_GAME_ASSET_TOOL_NAME = "evomeme_upload_game_asset";
|
|
24834
25198
|
var UPLOAD_GAME_ASSET_DESCRIPTION = [
|
|
24835
25199
|
"将本地二进制文件交给 evomeme服务端上传到对象存储(TOS),返回公开可访问的 asset_url。",
|
|
@@ -24883,7 +25247,7 @@ async function handleUploadGameAsset(args, deps = defaultDeps6) {
|
|
|
24883
25247
|
if (!args.local_path?.trim()) {
|
|
24884
25248
|
return toolError(new Error("缺少必填参数 local_path"));
|
|
24885
25249
|
}
|
|
24886
|
-
const localPath =
|
|
25250
|
+
const localPath = path24.isAbsolute(args.local_path) ? args.local_path : path24.resolve(deps.cwd(), args.local_path);
|
|
24887
25251
|
const uploadOptions = {};
|
|
24888
25252
|
if (args.folder !== void 0) {
|
|
24889
25253
|
uploadOptions.folder = args.folder;
|
|
@@ -24931,6 +25295,174 @@ function imageArtifactToolResult(input) {
|
|
|
24931
25295
|
);
|
|
24932
25296
|
}
|
|
24933
25297
|
|
|
25298
|
+
// src/background/generationTask.ts
|
|
25299
|
+
import { randomUUID as randomUUID5 } from "node:crypto";
|
|
25300
|
+
import { spawn as spawn6 } from "node:child_process";
|
|
25301
|
+
import { mkdir as mkdir8, readFile as readFile5, rename as rename4, rm as rm5, writeFile as writeFile5 } from "node:fs/promises";
|
|
25302
|
+
import path25 from "node:path";
|
|
25303
|
+
var INTERNAL_GENERATION_WORKER_COMMAND = "__evomeme_generation_worker";
|
|
25304
|
+
var TASK_FORMAT_VERSION = 1;
|
|
25305
|
+
var DEFAULT_RESULT_POLL_INTERVAL_MS = 250;
|
|
25306
|
+
var DEFAULT_RESULT_WAIT_TIMEOUT_MS = 65 * 6e4;
|
|
25307
|
+
var defaultClientDeps = {
|
|
25308
|
+
spawnFn: spawn6,
|
|
25309
|
+
sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
25310
|
+
entryPath: () => {
|
|
25311
|
+
const entryPath = process.argv[1];
|
|
25312
|
+
if (!entryPath) throw new Error("Cannot locate the evomeme CLI entry point for the background worker");
|
|
25313
|
+
return path25.resolve(entryPath);
|
|
25314
|
+
},
|
|
25315
|
+
now: Date.now,
|
|
25316
|
+
waitTimeoutMs: positiveEnvNumber3(
|
|
25317
|
+
"EVOMEME_BACKGROUND_GENERATION_TIMEOUT_MS",
|
|
25318
|
+
DEFAULT_RESULT_WAIT_TIMEOUT_MS
|
|
25319
|
+
),
|
|
25320
|
+
pollIntervalMs: DEFAULT_RESULT_POLL_INTERVAL_MS
|
|
25321
|
+
};
|
|
25322
|
+
var runDetachedGenerationTask = async (kind, args, workspaceDir) => runDetachedGenerationTaskWithDeps(kind, args, workspaceDir, defaultClientDeps);
|
|
25323
|
+
async function runDetachedGenerationTaskWithDeps(kind, args, workspaceDir, deps) {
|
|
25324
|
+
const resolvedWorkspace = path25.resolve(workspaceDir);
|
|
25325
|
+
const taskId = randomUUID5();
|
|
25326
|
+
const paths = backgroundTaskPaths(resolvedWorkspace, taskId);
|
|
25327
|
+
const request = {
|
|
25328
|
+
version: TASK_FORMAT_VERSION,
|
|
25329
|
+
taskId,
|
|
25330
|
+
kind,
|
|
25331
|
+
workspaceDir: resolvedWorkspace,
|
|
25332
|
+
args,
|
|
25333
|
+
createdAt: new Date(deps.now()).toISOString()
|
|
25334
|
+
};
|
|
25335
|
+
await mkdir8(paths.directory, { recursive: true });
|
|
25336
|
+
await writeJsonAtomic(paths.requestPath, request);
|
|
25337
|
+
let child;
|
|
25338
|
+
try {
|
|
25339
|
+
const entryPath = deps.entryPath();
|
|
25340
|
+
child = deps.spawnFn(
|
|
25341
|
+
process.execPath,
|
|
25342
|
+
[
|
|
25343
|
+
...workerExecArgv(entryPath),
|
|
25344
|
+
entryPath,
|
|
25345
|
+
INTERNAL_GENERATION_WORKER_COMMAND,
|
|
25346
|
+
paths.requestPath
|
|
25347
|
+
],
|
|
25348
|
+
{
|
|
25349
|
+
cwd: resolvedWorkspace,
|
|
25350
|
+
env: process.env,
|
|
25351
|
+
detached: true,
|
|
25352
|
+
stdio: "ignore",
|
|
25353
|
+
windowsHide: true
|
|
25354
|
+
}
|
|
25355
|
+
);
|
|
25356
|
+
} catch (error2) {
|
|
25357
|
+
await rm5(paths.requestPath, { force: true }).catch(() => void 0);
|
|
25358
|
+
throw error2;
|
|
25359
|
+
}
|
|
25360
|
+
let workerError;
|
|
25361
|
+
let workerExit;
|
|
25362
|
+
child.once("error", (error2) => {
|
|
25363
|
+
workerError = error2;
|
|
25364
|
+
});
|
|
25365
|
+
child.once("exit", (code, signal) => {
|
|
25366
|
+
workerExit = { code, signal };
|
|
25367
|
+
});
|
|
25368
|
+
child.unref();
|
|
25369
|
+
const deadline = deps.now() + deps.waitTimeoutMs;
|
|
25370
|
+
try {
|
|
25371
|
+
for (; ; ) {
|
|
25372
|
+
const completed = await tryReadBackgroundTaskResult(paths.resultPath, taskId);
|
|
25373
|
+
if (completed) return completed;
|
|
25374
|
+
if (workerError) throw workerError;
|
|
25375
|
+
if (workerExit) {
|
|
25376
|
+
await deps.sleep(Math.min(100, deps.pollIntervalMs));
|
|
25377
|
+
const finalResult = await tryReadBackgroundTaskResult(paths.resultPath, taskId);
|
|
25378
|
+
if (finalResult) return finalResult;
|
|
25379
|
+
throw new Error(
|
|
25380
|
+
`Background generation worker exited before writing a result (code=${workerExit.code ?? "null"}, signal=${workerExit.signal ?? "none"})`
|
|
25381
|
+
);
|
|
25382
|
+
}
|
|
25383
|
+
if (deps.now() >= deadline) {
|
|
25384
|
+
throw new Error(
|
|
25385
|
+
`Timed out waiting for the background generation result. task_id: ${taskId}`
|
|
25386
|
+
);
|
|
25387
|
+
}
|
|
25388
|
+
await deps.sleep(Math.min(deps.pollIntervalMs, Math.max(1, deadline - deps.now())));
|
|
25389
|
+
}
|
|
25390
|
+
} finally {
|
|
25391
|
+
await Promise.all([
|
|
25392
|
+
rm5(paths.requestPath, { force: true }).catch(() => void 0),
|
|
25393
|
+
rm5(paths.resultPath, { force: true }).catch(() => void 0)
|
|
25394
|
+
]);
|
|
25395
|
+
}
|
|
25396
|
+
}
|
|
25397
|
+
function backgroundTaskPaths(workspaceDir, taskId) {
|
|
25398
|
+
const directory = path25.join(path25.resolve(workspaceDir), ".evomeme", "background-tasks");
|
|
25399
|
+
return {
|
|
25400
|
+
directory,
|
|
25401
|
+
requestPath: path25.join(directory, `${taskId}.request.json`),
|
|
25402
|
+
resultPath: path25.join(directory, `${taskId}.result.json`)
|
|
25403
|
+
};
|
|
25404
|
+
}
|
|
25405
|
+
async function readBackgroundTaskRequest(requestPath) {
|
|
25406
|
+
const value = JSON.parse(await readFile5(requestPath, "utf8"));
|
|
25407
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
25408
|
+
throw new Error("Invalid background generation request");
|
|
25409
|
+
}
|
|
25410
|
+
const request = value;
|
|
25411
|
+
if (request.version !== TASK_FORMAT_VERSION || typeof request.taskId !== "string" || !isBackgroundGenerationKind(request.kind) || typeof request.workspaceDir !== "string" || !request.args || typeof request.args !== "object" || Array.isArray(request.args)) {
|
|
25412
|
+
throw new Error("Invalid background generation request");
|
|
25413
|
+
}
|
|
25414
|
+
return request;
|
|
25415
|
+
}
|
|
25416
|
+
async function writeBackgroundTaskResult(requestPath, taskId, result) {
|
|
25417
|
+
const resultPath = requestPath.replace(/\.request\.json$/u, ".result.json");
|
|
25418
|
+
if (resultPath === requestPath) throw new Error("Invalid background generation request path");
|
|
25419
|
+
const envelope = {
|
|
25420
|
+
version: TASK_FORMAT_VERSION,
|
|
25421
|
+
taskId,
|
|
25422
|
+
completedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25423
|
+
result
|
|
25424
|
+
};
|
|
25425
|
+
await writeJsonAtomic(resultPath, envelope);
|
|
25426
|
+
}
|
|
25427
|
+
async function tryReadBackgroundTaskResult(resultPath, expectedTaskId) {
|
|
25428
|
+
let text;
|
|
25429
|
+
try {
|
|
25430
|
+
text = await readFile5(resultPath, "utf8");
|
|
25431
|
+
} catch (error2) {
|
|
25432
|
+
if (error2.code === "ENOENT") return void 0;
|
|
25433
|
+
throw error2;
|
|
25434
|
+
}
|
|
25435
|
+
const envelope = JSON.parse(text);
|
|
25436
|
+
if (envelope.version !== TASK_FORMAT_VERSION || envelope.taskId !== expectedTaskId || !envelope.result || !Array.isArray(envelope.result.content)) {
|
|
25437
|
+
throw new Error(`Invalid background generation result for task_id: ${expectedTaskId}`);
|
|
25438
|
+
}
|
|
25439
|
+
return envelope.result;
|
|
25440
|
+
}
|
|
25441
|
+
async function writeJsonAtomic(filePath, value) {
|
|
25442
|
+
const tempPath = `${filePath}.${process.pid}.${randomUUID5()}.tmp`;
|
|
25443
|
+
try {
|
|
25444
|
+
await writeFile5(tempPath, `${JSON.stringify(value)}
|
|
25445
|
+
`, { encoding: "utf8", mode: 384 });
|
|
25446
|
+
await rename4(tempPath, filePath);
|
|
25447
|
+
} catch (error2) {
|
|
25448
|
+
await rm5(tempPath, { force: true }).catch(() => void 0);
|
|
25449
|
+
throw error2;
|
|
25450
|
+
}
|
|
25451
|
+
}
|
|
25452
|
+
function workerExecArgv(entryPath) {
|
|
25453
|
+
if (path25.extname(entryPath).toLowerCase() !== ".ts") return [];
|
|
25454
|
+
return process.execArgv.filter(
|
|
25455
|
+
(argument) => !argument.startsWith("--inspect") && !argument.startsWith("--debug")
|
|
25456
|
+
);
|
|
25457
|
+
}
|
|
25458
|
+
function isBackgroundGenerationKind(value) {
|
|
25459
|
+
return value === "generate_game_image" || value === "cutout_game_image" || value === "generate_game_video";
|
|
25460
|
+
}
|
|
25461
|
+
function positiveEnvNumber3(name, fallback) {
|
|
25462
|
+
const parsed = Number(process.env[name]);
|
|
25463
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
25464
|
+
}
|
|
25465
|
+
|
|
24934
25466
|
// src/mcp/tools/generateGameImage.ts
|
|
24935
25467
|
var GENERATE_GAME_IMAGE_TOOL_NAME = "evomeme_generate_game_image";
|
|
24936
25468
|
var GENERATE_GAME_IMAGE_DESCRIPTION = [
|
|
@@ -24989,6 +25521,7 @@ var generateGameImageTool = {
|
|
|
24989
25521
|
};
|
|
24990
25522
|
var defaultDeps7 = {
|
|
24991
25523
|
...defaultImageServiceDeps,
|
|
25524
|
+
backgroundTaskRunner: runDetachedGenerationTask,
|
|
24992
25525
|
canvasTracking: {
|
|
24993
25526
|
begin: beginCanvasJob,
|
|
24994
25527
|
update: updateCanvasJob,
|
|
@@ -24997,6 +25530,21 @@ var defaultDeps7 = {
|
|
|
24997
25530
|
}
|
|
24998
25531
|
};
|
|
24999
25532
|
async function handleGenerateGameImage(args, deps = defaultDeps7) {
|
|
25533
|
+
if (deps.backgroundTaskRunner) {
|
|
25534
|
+
const workspaceDir = args.workspace_dir?.trim() || deps.cwd();
|
|
25535
|
+
try {
|
|
25536
|
+
return await deps.backgroundTaskRunner(
|
|
25537
|
+
"generate_game_image",
|
|
25538
|
+
{ ...args, workspace_dir: workspaceDir },
|
|
25539
|
+
workspaceDir
|
|
25540
|
+
);
|
|
25541
|
+
} catch (error2) {
|
|
25542
|
+
return toolError(error2);
|
|
25543
|
+
}
|
|
25544
|
+
}
|
|
25545
|
+
return executeGenerateGameImage(args, deps);
|
|
25546
|
+
}
|
|
25547
|
+
async function executeGenerateGameImage(args, deps = defaultDeps7) {
|
|
25000
25548
|
let canvasJob;
|
|
25001
25549
|
try {
|
|
25002
25550
|
if (!args.prompt?.trim()) {
|
|
@@ -25178,6 +25726,7 @@ var cutoutGameImageTool = {
|
|
|
25178
25726
|
};
|
|
25179
25727
|
var defaultDeps8 = {
|
|
25180
25728
|
...defaultImageServiceDeps,
|
|
25729
|
+
backgroundTaskRunner: runDetachedGenerationTask,
|
|
25181
25730
|
canvasTracking: {
|
|
25182
25731
|
begin: beginCanvasJob,
|
|
25183
25732
|
update: updateCanvasJob,
|
|
@@ -25186,6 +25735,21 @@ var defaultDeps8 = {
|
|
|
25186
25735
|
}
|
|
25187
25736
|
};
|
|
25188
25737
|
async function handleCutoutGameImage(args, deps = defaultDeps8) {
|
|
25738
|
+
if (deps.backgroundTaskRunner) {
|
|
25739
|
+
const workspaceDir = args.workspace_dir?.trim() || deps.cwd();
|
|
25740
|
+
try {
|
|
25741
|
+
return await deps.backgroundTaskRunner(
|
|
25742
|
+
"cutout_game_image",
|
|
25743
|
+
{ ...args, workspace_dir: workspaceDir },
|
|
25744
|
+
workspaceDir
|
|
25745
|
+
);
|
|
25746
|
+
} catch (error2) {
|
|
25747
|
+
return toolError(error2);
|
|
25748
|
+
}
|
|
25749
|
+
}
|
|
25750
|
+
return executeCutoutGameImage(args, deps);
|
|
25751
|
+
}
|
|
25752
|
+
async function executeCutoutGameImage(args, deps = defaultDeps8) {
|
|
25189
25753
|
let canvasJob;
|
|
25190
25754
|
try {
|
|
25191
25755
|
if (!args.image_url?.trim()) {
|
|
@@ -25305,6 +25869,7 @@ var generateGameVideoTool = {
|
|
|
25305
25869
|
};
|
|
25306
25870
|
var defaultDeps9 = {
|
|
25307
25871
|
...defaultVideoGenerationDeps,
|
|
25872
|
+
backgroundTaskRunner: runDetachedGenerationTask,
|
|
25308
25873
|
canvasTracking: {
|
|
25309
25874
|
begin: beginCanvasJob,
|
|
25310
25875
|
update: updateCanvasJob,
|
|
@@ -25313,6 +25878,21 @@ var defaultDeps9 = {
|
|
|
25313
25878
|
}
|
|
25314
25879
|
};
|
|
25315
25880
|
async function handleGenerateGameVideo(args, deps = defaultDeps9) {
|
|
25881
|
+
if (deps.backgroundTaskRunner) {
|
|
25882
|
+
const workspaceDir = args.workspace_dir?.trim() || deps.cwd();
|
|
25883
|
+
try {
|
|
25884
|
+
return await deps.backgroundTaskRunner(
|
|
25885
|
+
"generate_game_video",
|
|
25886
|
+
{ ...args, workspace_dir: workspaceDir },
|
|
25887
|
+
workspaceDir
|
|
25888
|
+
);
|
|
25889
|
+
} catch (error2) {
|
|
25890
|
+
return toolError(error2);
|
|
25891
|
+
}
|
|
25892
|
+
}
|
|
25893
|
+
return executeGenerateGameVideo(args, deps);
|
|
25894
|
+
}
|
|
25895
|
+
async function executeGenerateGameVideo(args, deps = defaultDeps9) {
|
|
25316
25896
|
let canvasJob;
|
|
25317
25897
|
try {
|
|
25318
25898
|
const input = buildVideoInput(args);
|
|
@@ -25569,9 +26149,33 @@ async function startServer() {
|
|
|
25569
26149
|
await server.connect(transport);
|
|
25570
26150
|
}
|
|
25571
26151
|
|
|
26152
|
+
// src/background/generationWorker.ts
|
|
26153
|
+
var defaultExecutors = {
|
|
26154
|
+
generate_game_image: (args) => executeGenerateGameImage(args),
|
|
26155
|
+
cutout_game_image: (args) => executeCutoutGameImage(args),
|
|
26156
|
+
generate_game_video: (args) => executeGenerateGameVideo(args)
|
|
26157
|
+
};
|
|
26158
|
+
async function runBackgroundGenerationWorker(requestPath, executors = defaultExecutors) {
|
|
26159
|
+
const request = await readBackgroundTaskRequest(requestPath);
|
|
26160
|
+
let result;
|
|
26161
|
+
try {
|
|
26162
|
+
result = await executors[request.kind](request.args);
|
|
26163
|
+
} catch (error2) {
|
|
26164
|
+
result = toolError(error2);
|
|
26165
|
+
}
|
|
26166
|
+
await writeBackgroundTaskResult(requestPath, request.taskId, result);
|
|
26167
|
+
return 0;
|
|
26168
|
+
}
|
|
26169
|
+
|
|
25572
26170
|
// src/index.ts
|
|
25573
26171
|
async function main() {
|
|
25574
26172
|
const command = process.argv[2];
|
|
26173
|
+
if (command === INTERNAL_GENERATION_WORKER_COMMAND) {
|
|
26174
|
+
const requestPath = process.argv[3];
|
|
26175
|
+
if (!requestPath) throw new Error("Missing background generation request path");
|
|
26176
|
+
process.exitCode = await runBackgroundGenerationWorker(requestPath);
|
|
26177
|
+
return;
|
|
26178
|
+
}
|
|
25575
26179
|
if (!command) {
|
|
25576
26180
|
await startServer();
|
|
25577
26181
|
return;
|