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