evomeme 0.1.7 → 0.1.8

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