evomeme 0.1.7-beta.5 → 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.
@@ -18,7 +18,8 @@ const state = {
18
18
  videoDialogs: {}, // id -> video dialog state
19
19
  fileInputDialogId: null,
20
20
  mediaInputDialogId: null,
21
- activeOperations: new Set(),
21
+ activeOperations: new Set(),
22
+ failedOperationIds: new Set(),
22
23
  deleting: false,
23
24
  refreshing: false,
24
25
  externalSyncPending: false,
@@ -1161,8 +1162,11 @@ async function api(path, options) {
1161
1162
  }
1162
1163
 
1163
1164
  /** 画布只显示 layout 里的元素;磁盘上的孤儿文件不会自动冒出来 */
1164
- function mergeLayout(_images, layoutItems) {
1165
- return (layoutItems || []).filter((item) => item?.type !== 'sequence').map((item) => {
1165
+ function mergeLayout(_images, layoutItems) {
1166
+ return (layoutItems || []).filter((item) =>
1167
+ item?.type !== 'sequence' &&
1168
+ !(isPending(item) && state.failedOperationIds.has(item.operationId || item.id))
1169
+ ).map((item) => {
1166
1170
  if (isFrameEditor(item)) {
1167
1171
  return {
1168
1172
  ...item,
@@ -1710,15 +1714,14 @@ async function submitGenDialog(dialogId) {
1710
1714
  ds.busy = true
1711
1715
  state.error = ''
1712
1716
  // 对话框立刻变成生成中图片节点
1713
- state.layoutItems = state.layoutItems.map((it) => (it.id === dialogId ? pendingItem : it))
1714
- setSelection([pendingId])
1715
- await saveLayout(state.layoutItems)
1716
- renderCanvas()
1717
- renderChrome()
1718
-
1719
- const activeOperation = beginActiveOperation('image-generation')
1720
- try {
1721
- const result = await api('/api/generate', {
1717
+ state.layoutItems = state.layoutItems.map((it) => (it.id === dialogId ? pendingItem : it))
1718
+ setSelection([pendingId])
1719
+ const activeOperation = beginActiveOperation('image-generation')
1720
+ try {
1721
+ await saveLayout(state.layoutItems)
1722
+ renderCanvas()
1723
+ renderChrome()
1724
+ const result = await api('/api/generate', {
1722
1725
  method: 'POST',
1723
1726
  headers: { 'Content-Type': 'application/json' },
1724
1727
  body: JSON.stringify({
@@ -1737,18 +1740,37 @@ async function submitGenDialog(dialogId) {
1737
1740
  setSelection([result.id])
1738
1741
  renderCanvas()
1739
1742
  renderChrome()
1740
- } catch (err) {
1741
- const detail = String(err.message || err)
1742
- state.error = detail
1743
- // 失败是终态:移除任务元素,不让失败 job 在画布上复活。
1744
- state.layoutItems = state.layoutItems.filter(
1745
- (it) => it.id !== pendingId && it.id !== dialogId,
1746
- )
1747
- delete state.genDialogs[dialogId]
1748
- await reload().catch(() => undefined)
1749
- clearSelection()
1750
- renderCanvas()
1751
- renderChrome()
1743
+ } catch (err) {
1744
+ const detail = String(err.message || err)
1745
+ state.error = detail
1746
+ ds.busy = false
1747
+ ds.prompt = text
1748
+ ds.messages.push({
1749
+ id: `e-${Date.now()}`,
1750
+ role: 'assistant',
1751
+ error: detail,
1752
+ })
1753
+ await discardFailedPendingOperation(pendingId)
1754
+ // Validation can fail before the server creates a job. In that case the
1755
+ // persisted pending node cannot be reconciled server-side, so replace it
1756
+ // explicitly with the original generator dialog.
1757
+ state.layoutItems = state.layoutItems.filter(
1758
+ (it) => it.id !== pendingId && it.id !== dialogId,
1759
+ )
1760
+ state.layoutItems.push({
1761
+ ...dialog,
1762
+ id: dialogId,
1763
+ type: 'gen_dialog',
1764
+ name: dialog.name || '图片生成',
1765
+ width: dialog.width || GEN_DIALOG_W,
1766
+ height: null,
1767
+ })
1768
+ await saveLayout(state.layoutItems).catch((saveError) => {
1769
+ console.warn('保存图片生成失败回滚布局失败', saveError)
1770
+ })
1771
+ setSelection([dialogId])
1772
+ renderCanvas()
1773
+ renderChrome()
1752
1774
  } finally {
1753
1775
  endActiveOperation(activeOperation)
1754
1776
  }
@@ -2005,15 +2027,14 @@ async function submitVideoGenDialog(dialogId) {
2005
2027
  references,
2006
2028
  }
2007
2029
  : it,
2008
- )
2009
- setSelection([pendingId])
2010
- await saveLayout(state.layoutItems)
2011
- renderCanvas()
2012
- renderChrome()
2013
-
2014
- const activeOperation = beginActiveOperation('video-generation')
2015
- try {
2016
- const seedRaw = String(ds.seed ?? '').trim()
2030
+ )
2031
+ setSelection([pendingId])
2032
+ const activeOperation = beginActiveOperation('video-generation')
2033
+ try {
2034
+ await saveLayout(state.layoutItems)
2035
+ renderCanvas()
2036
+ renderChrome()
2037
+ const seedRaw = String(ds.seed ?? '').trim()
2017
2038
  const result = await api('/api/generate-video', {
2018
2039
  method: 'POST',
2019
2040
  headers: { 'Content-Type': 'application/json' },
@@ -2039,33 +2060,23 @@ async function submitVideoGenDialog(dialogId) {
2039
2060
  state.error = detail
2040
2061
  ds.busy = false
2041
2062
  ds.prompt = text
2042
- ds.messages.push({
2063
+ ds.messages.push({
2043
2064
  id: `e-${Date.now()}`,
2044
2065
  role: 'assistant',
2045
2066
  error: detail,
2046
- })
2047
- await reload().catch(() => undefined)
2048
- state.layoutItems.push({
2049
- id: pendingId,
2050
- type: 'pending',
2051
- x: dialog.x,
2052
- y: dialog.y,
2053
- width: VIDEO_DIALOG_W,
2054
- height: null,
2055
- })
2056
- state.layoutItems = state.layoutItems.map((it) =>
2057
- it.id === pendingId
2058
- ? {
2059
- id: dialogId,
2060
- type: 'video_dialog',
2061
- name: '视频生成',
2062
- x: it.x,
2063
- y: it.y,
2064
- width: VIDEO_DIALOG_W,
2065
- height: null,
2066
- }
2067
- : it,
2068
- )
2067
+ })
2068
+ await discardFailedPendingOperation(pendingId)
2069
+ state.layoutItems = state.layoutItems.filter(
2070
+ (it) => it.id !== pendingId && it.id !== dialogId,
2071
+ )
2072
+ state.layoutItems.push({
2073
+ ...dialog,
2074
+ id: dialogId,
2075
+ type: 'video_dialog',
2076
+ name: dialog.name || '视频生成',
2077
+ width: VIDEO_DIALOG_W,
2078
+ height: null,
2079
+ })
2069
2080
  await saveLayout(state.layoutItems)
2070
2081
  setSelection([dialogId])
2071
2082
  renderCanvas()
@@ -2510,10 +2521,10 @@ async function saveFrameEditorAsSheet(editorId) {
2510
2521
  if (result.id) setSelection([result.id])
2511
2522
  renderCanvas()
2512
2523
  renderChrome()
2513
- } catch (err) {
2514
- state.error = String(err.message || err)
2515
- await reload().catch(() => undefined)
2516
- state.layoutItems = [
2524
+ } catch (err) {
2525
+ state.error = String(err.message || err)
2526
+ await discardFailedPendingOperation(operationId)
2527
+ state.layoutItems = [
2517
2528
  ...state.layoutItems.filter(
2518
2529
  (candidate) => candidate.id !== operationId && candidate.id !== editorId,
2519
2530
  ),
@@ -3541,7 +3552,8 @@ function resetSessionState() {
3541
3552
  state.videoDialogs = {}
3542
3553
  state.fileInputDialogId = null
3543
3554
  state.mediaInputDialogId = null
3544
- state.activeOperations.clear()
3555
+ state.activeOperations.clear()
3556
+ state.failedOperationIds.clear()
3545
3557
  state.deleting = false
3546
3558
  state.error = ''
3547
3559
  state.camera = { x: 0, y: 0, scale: 1 }
@@ -4701,8 +4713,9 @@ async function confirmVideoCycle() {
4701
4713
  els.videoCycleStatus.textContent = '正在扫描周期…'
4702
4714
  renderChrome()
4703
4715
 
4704
- try {
4705
- const operationId = await createProcessingPlaceholder(node, {
4716
+ let operationId = ''
4717
+ try {
4718
+ operationId = await createProcessingPlaceholder(node, {
4706
4719
  name: '循环提取中',
4707
4720
  media: 'video',
4708
4721
  references: [videoId],
@@ -4726,8 +4739,9 @@ async function confirmVideoCycle() {
4726
4739
  closeVideoCycleModal(true)
4727
4740
  renderCanvas()
4728
4741
  renderChrome()
4729
- } catch (err) {
4730
- if (err?.name === 'AbortError') {
4742
+ } catch (err) {
4743
+ await discardFailedPendingOperation(operationId)
4744
+ if (err?.name === 'AbortError') {
4731
4745
  els.videoCycleModal.hidden = true
4732
4746
  state.videoCycleId = null
4733
4747
  els.videoCycleStatus.hidden = true
@@ -4750,7 +4764,7 @@ async function confirmVideoCycle() {
4750
4764
  }
4751
4765
  }
4752
4766
 
4753
- async function createProcessingPlaceholder(source, { name, media = 'image', references = [] } = {}) {
4767
+ async function createProcessingPlaceholder(source, { name, media = 'image', references = [] } = {}) {
4754
4768
  const id = createCanvasClientId('pending')
4755
4769
  const width = source?.width || (media === 'video' ? 280 : DEFAULT_W)
4756
4770
  const item = {
@@ -4766,12 +4780,42 @@ async function createProcessingPlaceholder(source, { name, media = 'image', refe
4766
4780
  id: referenceId,
4767
4781
  role: 'source',
4768
4782
  })),
4769
- }
4770
- state.layoutItems.push(item)
4771
- await saveLayout(state.layoutItems)
4772
- renderCanvas()
4773
- return id
4774
- }
4783
+ }
4784
+ state.layoutItems.push(item)
4785
+ try {
4786
+ await saveLayout(state.layoutItems)
4787
+ } catch (error) {
4788
+ state.layoutItems = state.layoutItems.filter((candidate) => candidate.id !== id)
4789
+ throw error
4790
+ }
4791
+ renderCanvas()
4792
+ return id
4793
+ }
4794
+
4795
+ async function discardFailedPendingOperation(operationId) {
4796
+ if (!operationId) return
4797
+ state.failedOperationIds.add(operationId)
4798
+ state.layoutItems = state.layoutItems.filter(
4799
+ (item) => !(isPending(item) && (item.id === operationId || item.operationId === operationId)),
4800
+ )
4801
+ state.selectedIds.delete(operationId)
4802
+ renderCanvas()
4803
+ renderChrome()
4804
+
4805
+ try {
4806
+ await api('/api/delete', {
4807
+ method: 'POST',
4808
+ headers: { 'Content-Type': 'application/json' },
4809
+ body: JSON.stringify({ layout_ids: [operationId], image_ids: [] }),
4810
+ })
4811
+ await reload().catch(() => undefined)
4812
+ } catch (error) {
4813
+ console.warn('后端清理失败的载入节点失败', error)
4814
+ await saveLayout(state.layoutItems).catch((error) => {
4815
+ console.warn('保存失败节点清理结果失败', error)
4816
+ })
4817
+ }
4818
+ }
4775
4819
 
4776
4820
  const INTERACTIVE_EDITOR_TRANSLATION = {
4777
4821
  name: 'zh-CN-canvas',
@@ -5248,10 +5292,10 @@ async function confirmInteractiveEdit() {
5248
5292
  if (result.id) setSelection([result.id])
5249
5293
  renderCanvas()
5250
5294
  renderChrome()
5251
- } catch (error) {
5252
- state.error = String(error.message || error)
5253
- await reload().catch(() => undefined)
5254
- renderChrome()
5295
+ } catch (error) {
5296
+ state.error = String(error.message || error)
5297
+ await discardFailedPendingOperation(operationId)
5298
+ renderChrome()
5255
5299
  } finally {
5256
5300
  endActiveOperation(activeOperation)
5257
5301
  renderChrome()
@@ -5339,10 +5383,10 @@ async function confirmLayerDecomposition() {
5339
5383
  setSelection(images.map((image) => image.id).filter(Boolean))
5340
5384
  renderCanvas()
5341
5385
  renderChrome()
5342
- } catch (err) {
5343
- state.error = String(err.message || err)
5344
- await reload().catch(() => undefined)
5345
- renderChrome()
5386
+ } catch (err) {
5387
+ state.error = String(err.message || err)
5388
+ await discardFailedPendingOperation(operationId)
5389
+ renderChrome()
5346
5390
  } finally {
5347
5391
  endActiveOperation(activeOperation)
5348
5392
  setLayerDecompositionControlsDisabled(false)
@@ -5405,8 +5449,9 @@ async function confirmFrameExtract() {
5405
5449
  els.frameExtractStatus.textContent = `正在抽取 ${count} 帧并拼接 PNG…`
5406
5450
  renderChrome()
5407
5451
 
5408
- try {
5409
- const operationId = await createProcessingPlaceholder(node, {
5452
+ let operationId = ''
5453
+ try {
5454
+ operationId = await createProcessingPlaceholder(node, {
5410
5455
  name: '视频序列帧图生成中',
5411
5456
  references: [videoId],
5412
5457
  })
@@ -5423,9 +5468,10 @@ async function confirmFrameExtract() {
5423
5468
  state.frameExtractVideoId = null
5424
5469
  renderCanvas()
5425
5470
  renderChrome()
5426
- } catch (err) {
5427
- state.error = String(err.message || err)
5428
- els.frameExtractStatus.hidden = false
5471
+ } catch (err) {
5472
+ state.error = String(err.message || err)
5473
+ await discardFailedPendingOperation(operationId)
5474
+ els.frameExtractStatus.hidden = false
5429
5475
  els.frameExtractStatus.textContent = state.error
5430
5476
  renderChrome()
5431
5477
  } finally {
@@ -5780,8 +5826,9 @@ async function runSmartSplit(imageId, anchor = 'center') {
5780
5826
  els.smartSplitStatus.hidden = false
5781
5827
  els.smartSplitStatus.textContent = '正在识别主体并统一尺寸…'
5782
5828
  renderChrome()
5783
- try {
5784
- const operationId = await createProcessingPlaceholder(node, {
5829
+ let operationId = ''
5830
+ try {
5831
+ operationId = await createProcessingPlaceholder(node, {
5785
5832
  name: '智能切分中',
5786
5833
  references: [imageId],
5787
5834
  })
@@ -5800,9 +5847,10 @@ async function runSmartSplit(imageId, anchor = 'center') {
5800
5847
  state.smartSplitImageId = null
5801
5848
  renderCanvas()
5802
5849
  renderChrome()
5803
- } catch (err) {
5804
- state.error = String(err.message || err)
5805
- els.smartSplitStatus.hidden = false
5850
+ } catch (err) {
5851
+ state.error = String(err.message || err)
5852
+ await discardFailedPendingOperation(operationId)
5853
+ els.smartSplitStatus.hidden = false
5806
5854
  els.smartSplitStatus.textContent = state.error
5807
5855
  renderChrome()
5808
5856
  } finally {
@@ -5902,8 +5950,9 @@ async function confirmManualSplit() {
5902
5950
  els.manualSplitCols.disabled = true
5903
5951
  renderChrome()
5904
5952
 
5905
- try {
5906
- const operationId = await createProcessingPlaceholder(node, {
5953
+ let operationId = ''
5954
+ try {
5955
+ operationId = await createProcessingPlaceholder(node, {
5907
5956
  name: '手动切分中',
5908
5957
  references: [imageId],
5909
5958
  })
@@ -5927,9 +5976,10 @@ async function confirmManualSplit() {
5927
5976
  setSelection(images.map((image) => image.id))
5928
5977
  renderCanvas()
5929
5978
  renderChrome()
5930
- } catch (err) {
5931
- state.error = String(err.message || err)
5932
- els.manualSplitStatus.hidden = false
5979
+ } catch (err) {
5980
+ state.error = String(err.message || err)
5981
+ await discardFailedPendingOperation(operationId)
5982
+ els.manualSplitStatus.hidden = false
5933
5983
  els.manualSplitStatus.textContent = state.error
5934
5984
  renderChrome()
5935
5985
  } finally {
@@ -6039,9 +6089,9 @@ async function runCutout(imageId) {
6039
6089
  })
6040
6090
  await reload()
6041
6091
  setSelection([result.id])
6042
- } catch (err) {
6043
- state.error = String(err.message || err)
6044
- await reload()
6092
+ } catch (err) {
6093
+ state.error = String(err.message || err)
6094
+ await discardFailedPendingOperation(pendingId)
6045
6095
  } finally {
6046
6096
  endActiveOperation(activeOperation)
6047
6097
  renderCanvas()
package/dist/index.js CHANGED
@@ -18267,7 +18267,7 @@ var StdioClientTransport = class {
18267
18267
  };
18268
18268
 
18269
18269
  // src/version.ts
18270
- var VERSION = "0.1.7-beta.5";
18270
+ var VERSION = "0.1.8";
18271
18271
 
18272
18272
  // src/publishChannel.generated.ts
18273
18273
  var PUBLISH_NPM_NAME = "evomeme";
@@ -19200,7 +19200,7 @@ var GAME_IMAGE_SIZES = [
19200
19200
  "2496x1664",
19201
19201
  "3024x1296"
19202
19202
  ];
19203
- var GAME_IMAGE_MODELS = ["asset-pro", "asset-lite"];
19203
+ var GAME_IMAGE_MODELS = ["asset-pro", "asset-lite", "gpt-image-2"];
19204
19204
  var IMAGE_JOB_STATUSES = [
19205
19205
  "QUEUED",
19206
19206
  "RUNNING",
@@ -19215,7 +19215,8 @@ var IMAGE_JOB_KINDS = [
19215
19215
  ];
19216
19216
  var GAME_IMAGE_MODEL_BACKEND = {
19217
19217
  "asset-pro": "doubao-seedream-5-0-pro-260628",
19218
- "asset-lite": "doubao-seedream-5-0-260128"
19218
+ "asset-lite": "doubao-seedream-5-0-260128",
19219
+ "gpt-image-2": "gpt-image-2"
19219
19220
  };
19220
19221
  function resolveGameImageModel(model) {
19221
19222
  return GAME_IMAGE_MODEL_BACKEND[model];
@@ -19264,7 +19265,7 @@ async function getImageJobStatus(token, jobId, options = {}) {
19264
19265
  path: `/v1/tools/image/jobs/${encodeURIComponent(jobId)}`,
19265
19266
  token
19266
19267
  });
19267
- if (typeof data.job_id !== "string" || data.job_id.length === 0 || !IMAGE_JOB_KINDS.includes(data.kind ?? "") || !IMAGE_JOB_STATUSES.includes(data.status ?? "") || typeof data.asset_url !== "string" || data.asset_url.length === 0 || typeof data.pending !== "boolean" || typeof data.attempts !== "number" || typeof data.max_attempts !== "number" || data.error_code !== void 0 && data.error_code !== null && typeof data.error_code !== "string" || data.error_stage !== void 0 && data.error_stage !== null && typeof data.error_stage !== "string" || 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)) {
19268
19269
  throw incompatibleResponseError(JSON.stringify(data));
19269
19270
  }
19270
19271
  const layers = (data.layers ?? []).map((layer) => {
@@ -19290,6 +19291,7 @@ async function getImageJobStatus(token, jobId, options = {}) {
19290
19291
  maxAttempts: data.max_attempts,
19291
19292
  errorCode: data.error_code ?? void 0,
19292
19293
  errorStage: data.error_stage ?? void 0,
19294
+ ...data.error_message == null ? {} : { errorMessage: data.error_message },
19293
19295
  createdAt: data.created_at,
19294
19296
  updatedAt: data.updated_at,
19295
19297
  completedAt: data.completed_at ?? void 0,
@@ -20575,6 +20577,7 @@ async function waitForImageJob(jobId, initialAssetUrl, deps = defaultImageServic
20575
20577
  await hooks.onStatus?.({ ...status, assetUrl: lastAssetUrl });
20576
20578
  if (status.status === "SUCCEEDED") return { ...status, assetUrl: lastAssetUrl };
20577
20579
  if (status.status === "FAILED") {
20580
+ if (status.errorMessage) throw new Error(status.errorMessage);
20578
20581
  throw new Error(
20579
20582
  [
20580
20583
  "图片任务失败。",
@@ -22469,6 +22472,8 @@ var MAX_JSON_BODY_BYTES = 80 * 1024 * 1024;
22469
22472
  var INTERACTIVE_EDIT_MIN_PIXELS = 921600;
22470
22473
  var INTERACTIVE_EDIT_MAX_PIXELS = 4624220;
22471
22474
  var TRANSPARENT_BACKGROUND_REFERENCE_ERROR = "透明背景图功能要求有且只有一个PNG格式参考图输入";
22475
+ var CANVAS_QUOTA_ERROR = "当前账号的生成额度已用完,请充值或补充额度后重试";
22476
+ var CANVAS_NETWORK_ERROR = "网络连接失败,请重试";
22472
22477
  var CANVAS_VENDOR_ASSETS = {
22473
22478
  "/vendor/painterro.min.js": {
22474
22479
  packageName: "painterro",
@@ -22512,7 +22517,7 @@ async function startCanvasServer(options = {}) {
22512
22517
  } catch (error2) {
22513
22518
  const status = statusFromError(error2);
22514
22519
  if (!response.headersSent) {
22515
- sendJson(response, status, { detail: errorMessage(error2) });
22520
+ sendJson(response, status, { detail: canvasGenerationErrorMessage(error2) });
22516
22521
  } else {
22517
22522
  response.end();
22518
22523
  }
@@ -22977,8 +22982,9 @@ async function generateFromCanvas(workspaceDir, body, imageServiceDeps) {
22977
22982
  tos_url: generated.assetUrl
22978
22983
  };
22979
22984
  } catch (error2) {
22980
- await failCanvasJob(canvasJob.workspaceDir, canvasJob.operationId, error2).catch(() => void 0);
22981
- throw error2;
22985
+ const normalized = normalizeCanvasGenerationError(error2);
22986
+ await failCanvasJob(canvasJob.workspaceDir, canvasJob.operationId, normalized).catch(() => void 0);
22987
+ throw normalized;
22982
22988
  }
22983
22989
  }
22984
22990
  async function generateVideoFromCanvas(workspaceDir, body, videoGenerationDeps) {
@@ -23124,8 +23130,9 @@ async function generateVideoFromCanvas(workspaceDir, body, videoGenerationDeps)
23124
23130
  reference_ids: parentAssetIds
23125
23131
  };
23126
23132
  } catch (error2) {
23127
- await failCanvasJob(canvasJob.workspaceDir, canvasJob.operationId, error2).catch(() => void 0);
23128
- throw error2;
23133
+ const normalized = normalizeCanvasGenerationError(error2);
23134
+ await failCanvasJob(canvasJob.workspaceDir, canvasJob.operationId, normalized).catch(() => void 0);
23135
+ throw normalized;
23129
23136
  }
23130
23137
  }
23131
23138
  async function cutoutFromCanvas(workspaceDir, body) {
@@ -23184,8 +23191,9 @@ async function cutoutFromCanvas(workspaceDir, body) {
23184
23191
  tos_url: result.assetUrl
23185
23192
  };
23186
23193
  } catch (error2) {
23187
- await failCanvasJob(canvasJob.workspaceDir, canvasJob.operationId, error2).catch(() => void 0);
23188
- throw error2;
23194
+ const normalized = normalizeCanvasGenerationError(error2);
23195
+ await failCanvasJob(canvasJob.workspaceDir, canvasJob.operationId, normalized).catch(() => void 0);
23196
+ throw normalized;
23189
23197
  }
23190
23198
  }
23191
23199
  async function ensureReferenceAssetUrl(workspaceDir, assetId) {
@@ -24080,6 +24088,68 @@ function statusFromError(error2) {
24080
24088
  function errorMessage(error2) {
24081
24089
  return error2 instanceof Error ? error2.message : String(error2);
24082
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
+ }
24083
24153
 
24084
24154
  // src/cli/canvas/command.ts
24085
24155
  async function runCanvas(parsed) {
@@ -25432,7 +25502,7 @@ var generateGameImageTool = {
25432
25502
  image_model: {
25433
25503
  type: "string",
25434
25504
  enum: [...GAME_IMAGE_MODELS],
25435
- description: "生图模型,默认 asset-lite;可选 asset-pro。"
25505
+ description: "生图模型,默认 asset-lite;可选 asset-pro 或 gpt-image-2。"
25436
25506
  },
25437
25507
  workspace_dir: {
25438
25508
  type: "string",
@@ -25747,90 +25817,6 @@ function canvasHooks2(workspaceDir, operationId, deps) {
25747
25817
  };
25748
25818
  }
25749
25819
 
25750
- // src/mcp/server.ts
25751
- function listMcpTools() {
25752
- return [
25753
- checkEvomemeLoginTool,
25754
- loginEvomemeTool,
25755
- submitGameCodeTool,
25756
- uploadGameAssetTool,
25757
- generateGameImageTool,
25758
- cutoutGameImageTool
25759
- // Video generation is temporarily offline for this release. Keep the
25760
- // implementation in place so it can be restored in a later version.
25761
- // generateGameVideoTool,
25762
- // ...listMeshTools(),
25763
- ];
25764
- }
25765
- async function dispatchToolCall(name, rawArgs = {}) {
25766
- if (name === CHECK_EVOMEME_LOGIN_TOOL_NAME) {
25767
- return handleCheckEvomemeLogin();
25768
- }
25769
- if (name === LOGIN_EVOMEME_TOOL_NAME) {
25770
- return handleLoginEvomeme();
25771
- }
25772
- if (name === SUBMIT_GAME_CODE_TOOL_NAME) {
25773
- return handleSubmitGameCode(parseSubmitArgs(rawArgs));
25774
- }
25775
- if (name === UPLOAD_GAME_ASSET_TOOL_NAME) {
25776
- return handleUploadGameAsset(parseUploadArgs(rawArgs));
25777
- }
25778
- if (name === GENERATE_GAME_IMAGE_TOOL_NAME) {
25779
- return handleGenerateGameImage(parseGenerateImageArgs(rawArgs));
25780
- }
25781
- if (name === CUTOUT_GAME_IMAGE_TOOL_NAME) {
25782
- return handleCutoutGameImage(parseCutoutImageArgs(rawArgs));
25783
- }
25784
- return toolError(new Error(`未知工具:${name}`));
25785
- }
25786
- function createMcpServer(options = {}) {
25787
- const getUpdateNotice = options.getUpdateNotice ?? (async () => void 0);
25788
- const server = new Server(
25789
- { name: "evomeme", version: VERSION },
25790
- { capabilities: { tools: { listChanged: true } } }
25791
- );
25792
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: listMcpTools() }));
25793
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
25794
- const args = request.params.arguments ?? {};
25795
- const [result, updateNotice] = await Promise.all([
25796
- dispatchToolCall(request.params.name, args),
25797
- getUpdateNotice()
25798
- ]);
25799
- return appendToolNotice(result, updateNotice);
25800
- });
25801
- return server;
25802
- }
25803
- function parseSubmitArgs(args) {
25804
- return {
25805
- workspace_dir: typeof args.workspace_dir === "string" ? args.workspace_dir : void 0,
25806
- name: typeof args.name === "string" ? args.name : void 0,
25807
- cover_url: typeof args.cover_url === "string" ? args.cover_url : void 0
25808
- };
25809
- }
25810
- function parseUploadArgs(args) {
25811
- return {
25812
- local_path: typeof args.local_path === "string" ? args.local_path : "",
25813
- folder: typeof args.folder === "string" ? args.folder : void 0,
25814
- extension: typeof args.extension === "string" ? args.extension : void 0
25815
- };
25816
- }
25817
- function parseGenerateImageArgs(args) {
25818
- return {
25819
- prompt: typeof args.prompt === "string" ? args.prompt : "",
25820
- image_type: typeof args.image_type === "string" ? args.image_type : void 0,
25821
- size: typeof args.size === "string" ? args.size : void 0,
25822
- reference_image_url: typeof args.reference_image_url === "string" ? args.reference_image_url : Array.isArray(args.reference_image_url) ? args.reference_image_url.filter((value) => typeof value === "string") : void 0,
25823
- image_model: typeof args.image_model === "string" ? args.image_model : void 0,
25824
- workspace_dir: typeof args.workspace_dir === "string" ? args.workspace_dir : void 0
25825
- };
25826
- }
25827
- function parseCutoutImageArgs(args) {
25828
- return {
25829
- image_url: typeof args.image_url === "string" ? args.image_url : "",
25830
- workspace_dir: typeof args.workspace_dir === "string" ? args.workspace_dir : void 0
25831
- };
25832
- }
25833
-
25834
25820
  // src/mcp/tools/generateGameVideo.ts
25835
25821
  var GENERATE_GAME_VIDEO_TOOL_NAME = "evomeme_generate_game_video";
25836
25822
  var GENERATE_GAME_VIDEO_DESCRIPTION = [
@@ -25892,6 +25878,21 @@ var defaultDeps9 = {
25892
25878
  fail: failCanvasJob
25893
25879
  }
25894
25880
  };
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
+ }
25895
25896
  async function executeGenerateGameVideo(args, deps = defaultDeps9) {
25896
25897
  let canvasJob;
25897
25898
  try {
@@ -26038,6 +26039,110 @@ function buildVideoInput(args) {
26038
26039
  return input;
26039
26040
  }
26040
26041
 
26042
+ // src/mcp/server.ts
26043
+ function listMcpTools() {
26044
+ return [
26045
+ checkEvomemeLoginTool,
26046
+ loginEvomemeTool,
26047
+ submitGameCodeTool,
26048
+ uploadGameAssetTool,
26049
+ generateGameImageTool,
26050
+ cutoutGameImageTool,
26051
+ generateGameVideoTool
26052
+ // ...listMeshTools(),
26053
+ ];
26054
+ }
26055
+ async function dispatchToolCall(name, rawArgs = {}) {
26056
+ if (name === CHECK_EVOMEME_LOGIN_TOOL_NAME) {
26057
+ return handleCheckEvomemeLogin();
26058
+ }
26059
+ if (name === LOGIN_EVOMEME_TOOL_NAME) {
26060
+ return handleLoginEvomeme();
26061
+ }
26062
+ if (name === SUBMIT_GAME_CODE_TOOL_NAME) {
26063
+ return handleSubmitGameCode(parseSubmitArgs(rawArgs));
26064
+ }
26065
+ if (name === UPLOAD_GAME_ASSET_TOOL_NAME) {
26066
+ return handleUploadGameAsset(parseUploadArgs(rawArgs));
26067
+ }
26068
+ if (name === GENERATE_GAME_IMAGE_TOOL_NAME) {
26069
+ return handleGenerateGameImage(parseGenerateImageArgs(rawArgs));
26070
+ }
26071
+ if (name === CUTOUT_GAME_IMAGE_TOOL_NAME) {
26072
+ return handleCutoutGameImage(parseCutoutImageArgs(rawArgs));
26073
+ }
26074
+ if (name === GENERATE_GAME_VIDEO_TOOL_NAME) {
26075
+ return handleGenerateGameVideo(parseGenerateVideoArgs(rawArgs));
26076
+ }
26077
+ return toolError(new Error(`未知工具:${name}`));
26078
+ }
26079
+ function createMcpServer(options = {}) {
26080
+ const getUpdateNotice = options.getUpdateNotice ?? (async () => void 0);
26081
+ const server = new Server(
26082
+ { name: "evomeme", version: VERSION },
26083
+ { capabilities: { tools: { listChanged: true } } }
26084
+ );
26085
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: listMcpTools() }));
26086
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
26087
+ const args = request.params.arguments ?? {};
26088
+ const [result, updateNotice] = await Promise.all([
26089
+ dispatchToolCall(request.params.name, args),
26090
+ getUpdateNotice()
26091
+ ]);
26092
+ return appendToolNotice(result, updateNotice);
26093
+ });
26094
+ return server;
26095
+ }
26096
+ function parseSubmitArgs(args) {
26097
+ return {
26098
+ workspace_dir: typeof args.workspace_dir === "string" ? args.workspace_dir : void 0,
26099
+ name: typeof args.name === "string" ? args.name : void 0,
26100
+ cover_url: typeof args.cover_url === "string" ? args.cover_url : void 0
26101
+ };
26102
+ }
26103
+ function parseUploadArgs(args) {
26104
+ return {
26105
+ local_path: typeof args.local_path === "string" ? args.local_path : "",
26106
+ folder: typeof args.folder === "string" ? args.folder : void 0,
26107
+ extension: typeof args.extension === "string" ? args.extension : void 0
26108
+ };
26109
+ }
26110
+ function parseGenerateImageArgs(args) {
26111
+ return {
26112
+ prompt: typeof args.prompt === "string" ? args.prompt : "",
26113
+ image_type: typeof args.image_type === "string" ? args.image_type : void 0,
26114
+ size: typeof args.size === "string" ? args.size : void 0,
26115
+ reference_image_url: typeof args.reference_image_url === "string" ? args.reference_image_url : Array.isArray(args.reference_image_url) ? args.reference_image_url.filter((value) => typeof value === "string") : void 0,
26116
+ image_model: typeof args.image_model === "string" ? args.image_model : void 0,
26117
+ workspace_dir: typeof args.workspace_dir === "string" ? args.workspace_dir : void 0
26118
+ };
26119
+ }
26120
+ function parseCutoutImageArgs(args) {
26121
+ return {
26122
+ image_url: typeof args.image_url === "string" ? args.image_url : "",
26123
+ workspace_dir: typeof args.workspace_dir === "string" ? args.workspace_dir : void 0
26124
+ };
26125
+ }
26126
+ function parseGenerateVideoArgs(args) {
26127
+ const references = Array.isArray(args.references) ? args.references.filter((value) => Boolean(value && typeof value === "object")).map((value) => ({
26128
+ type: typeof value.type === "string" ? value.type : void 0,
26129
+ url: typeof value.url === "string" ? value.url : void 0,
26130
+ role: typeof value.role === "string" ? value.role : void 0
26131
+ })) : void 0;
26132
+ return {
26133
+ prompt: typeof args.prompt === "string" ? args.prompt : "",
26134
+ ratio: typeof args.ratio === "string" ? args.ratio : void 0,
26135
+ duration: typeof args.duration === "number" ? args.duration : void 0,
26136
+ resolution: typeof args.resolution === "string" ? args.resolution : void 0,
26137
+ generate_audio: typeof args.generate_audio === "boolean" ? args.generate_audio : void 0,
26138
+ watermark: typeof args.watermark === "boolean" ? args.watermark : void 0,
26139
+ seed: typeof args.seed === "number" ? args.seed : void 0,
26140
+ video_model: typeof args.video_model === "string" ? args.video_model : void 0,
26141
+ references,
26142
+ workspace_dir: typeof args.workspace_dir === "string" ? args.workspace_dir : void 0
26143
+ };
26144
+ }
26145
+
26041
26146
  // src/mcp/index.ts
26042
26147
  async function startServer() {
26043
26148
  const server = createMcpServer({ getUpdateNotice: getMcpUpdateNotice });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "evomeme",
3
- "version": "0.1.7-beta.5",
3
+ "version": "0.1.8",
4
4
  "type": "module",
5
5
  "description": "evomemeplugin_client - local stdio MCP server. Install: npx -y evomeme@latest install --ide cursor. Init: npx -y evomeme@latest init.",
6
6
  "main": "dist/index.js",
@@ -0,0 +1,52 @@
1
+ ---
2
+ name: evomeme-canvas
3
+ description: 在生成或编辑任何游戏素材前,必须确保当前工作区的 evomeme素材画布在用户可见,并把画布链接告知用户。当任务涉及生图、图片编辑、抠图、图层分离、视频生成或其他 evomeme素材工具时使用。
4
+ ---
5
+
6
+ # evomeme素材画布
7
+
8
+ 在调用任何素材生成或编辑工具前,先完成本技能的画布启动流程。画布必须对应当前任务的工作区。
9
+
10
+ ## 启动前检查
11
+
12
+ 只根据当前对话记录判断一个画布服务是否可复用,不得仅凭端口已占用或 `/api/health` 响应正常推断服务属于当前工作区。
13
+
14
+ - 当前对话中明确记录了由本次对话为同一工作区启动的前台画布终端和地址时,访问 `<画布地址>/api/health`;响应正常则复用,地址未知、健康检查失败或终端已关闭则重新启动。
15
+ - 当前对话中不存在画布启动记录时,即使发现已有 evomeme画布服务也不得复用。该服务可能属于其他工作区;先关闭已有服务,再按照下方流程为当前工作区启动新的前台服务。
16
+ - 当前对话记录中的画布属于其他工作区时,同样先关闭该服务,再为当前工作区重新启动。
17
+
18
+ 关闭已有服务时只关闭能够确认是 evomeme画布的进程,不得终止身份不明的端口占用进程。
19
+
20
+ ## 前台启动
21
+
22
+ 1. 在当前工作区根目录打开一个用户可见的独立交互式终端或命令框。
23
+ 2. 在该终端以前台方式执行且只执行:
24
+
25
+ ```text
26
+ npx -y evomeme@latest canvas
27
+ ```
28
+
29
+ 3. 保持这个终端打开。命令必须持续占用该终端,用户必须能够在其中按 `Ctrl+C`,或直接关闭终端窗口来停止服务。
30
+ 4. 等待终端输出“素材画布已启动”和“画布地址”。启动失败时,先处理或向用户说明错误,不得继续调用素材生成工具。
31
+
32
+ 不得将命令作为隐藏、分离或后台进程运行。不得使用 `&`、`nohup`、`start /B`、`Start-Job`、隐藏窗口、服务管理器或其他后台启动方式,也不要添加 `--no-open`。如果当前执行环境不能提供用户可见且可自行中断的终端,请让用户在可见终端中执行上述命令;确认启动输出或健康检查通过后再继续。
33
+
34
+ 画布终端保持前台运行不妨碍使用另一个终端或其他工具继续工作;不要为了继续生成而关闭画布终端。
35
+
36
+ ## 告知并引导用户
37
+
38
+ 取得终端实际输出的地址后,在调用第一个素材工具前立即向用户发送可点击链接,并说明:
39
+
40
+ - 保持画布终端打开;在终端中`Ctrl+C` 或关闭该终端会停止服务。
41
+ - 在画布中可以实时查看当前工作区的素材、生成任务状态和生成结果。
42
+ - 用户可以直接在画布中选择参考图、生成图片或继续编辑;AI 也会继续使用当前工作区的素材工具,结果会同步显示在画布中。
43
+
44
+ 使用实际地址,不要猜测端口。可以采用下面的简短表达:
45
+
46
+ ```markdown
47
+ 素材画布已启动:[打开 evomeme素材画布](<实际地址>)。请保持运行画布的命令行窗口打开;你可以在画布中监控任务,也可以直接选择参考图并使用生图或编辑功能。在命令行中按 Ctrl+C 或关闭该命令行即可停止服务。
48
+ ```
49
+
50
+ ## 生成期间
51
+
52
+ 确认画布可用并告知用户后,继续执行已经获得授权的素材任务,无需仅为打开画布再次请求确认。后续生成只能复用当前对话明确记录的同一工作区健康画布;如果服务中途停止,在下一次素材工具调用前重新完成启动流程。
@@ -0,0 +1,7 @@
1
+ interface:
2
+ display_name: "evomemeCanvas"
3
+ short_description: "在素材生成前启动并引导使用 evomeme素材画布"
4
+ default_prompt: "使用 $evomeme-canvas 启动当前工作区的素材画布,并在画布运行后继续生成素材。"
5
+
6
+ policy:
7
+ allow_implicit_invocation: true