evomeme 0.1.7 → 0.1.9

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