Skip to content

Commit 9cd9379

Browse files
committed
ai-node-canvas 0.84.0: a stopped call says where the limit is, and the MCP server gets a minute
A call the Host stops is stopped by the Host, not by us. The connector API takes a signal and nothing else, and a picture is asked for in one synchronous call - there is no task to poll, the way a video has one. The message on the card now says where that limit is (about thirty seconds, and it is the Host's), and what the user can still change: a smaller request. A video is submitted and polled, which is why the same limit reads as a mystery for a picture and not for a video. The MCP server is given a minute to answer instead of half of one. A call that starts a run hands the work to the page and waits for the page's answer, and a page that is busy must not turn the call into a failure. 248 tests, 248 mutations, 0 errors.
1 parent c0e7cd4 commit 9cd9379

5 files changed

Lines changed: 66 additions & 8 deletions

File tree

‎plugins/antianqi/ai-node-canvas/.minimax-plugin/plugin.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"schemaVersion": 1,
33
"name": "ai-node-canvas",
44
"displayName": "节点画布",
5-
"version": "0.83.0",
5+
"version": "0.84.0",
66
"description": "无限画布节点编排:把 MiniMax 模型生成的文本、图片、视频串成工作流,连线即数据流,上游产物可直接被下游引用。",
77
"author": "antianqi",
88
"icon": "icon.png",

‎plugins/antianqi/ai-node-canvas/miniapp/node/connector-runner.mjs‎

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,41 @@ const TOOL = {
3232
assetUrl: 'get_asset_url',
3333
};
3434

35+
/**
36+
* What a connector call that the Host stopped says.
37+
*
38+
* The Host gives every connector call a window of its own — about thirty
39+
* seconds — and a call that runs longer is stopped. That window is the Host's
40+
* and not ours to widen, so the message has to say that it is there, and what
41+
* the user can do about it.
42+
*
43+
* A picture is asked for in one synchronous call: there is no task number to
44+
* poll, so a picture that takes too long is simply lost. The way out is a
45+
* smaller request. A video is submitted and polled, which is why it can run
46+
* for a quarter of an hour — and that difference is worth saying out loud,
47+
* because the same limit reads as a mystery otherwise.
48+
*
49+
* @param {string} suffix the tool that was called
50+
* @param {string} code the Host's error code
51+
* @returns {string|null} the note, or null when the failure is not a stop
52+
*/
53+
export function connectorStopNote(suffix, code) {
54+
const isImage = suffix === TOOL.image;
55+
if (code === 'CONNECTOR_OUTCOME_UNKNOWN') {
56+
return '调用已经发出但没收到回执,结果未知(可能已经扣费);宿主对单次连接器调用有约 30 秒的超时上限,这个上限在宿主那边,画布改不了。'
57+
+ (isImage
58+
? '出图是一次调用直接出结果,没有可轮询的任务号,所以只能把请求变小:把清晰度降到 1K、少给几张参考图,再点一次生成。'
59+
: '');
60+
}
61+
if (code === 'CONNECTOR_TIMEOUT') {
62+
return '这次调用超过了宿主的超时上限(约 30 秒)被中止;结果未知(可能已经扣费),先核对业务状态再决定要不要重试。'
63+
+ (isImage
64+
? '出图是一次调用直接出结果,没有可轮询的任务号,所以只能把请求变小:把清晰度降到 1K、少给几张参考图,再点一次生成。'
65+
: '');
66+
}
67+
return null;
68+
}
69+
3570
// The provider takes media URLs, never local paths or data URIs.
3671
//
3772
// The link comes under a few names, and the video task answers with its own:
@@ -215,11 +250,7 @@ export function createConnectorRunner(context, options = {}) {
215250
// The time shown is the call's own time. The time it spent waiting for
216251
// the machine is said separately, because that is the difference
217252
// between "the provider was slow" and "it stood behind another call".
218-
const note = error?.code === 'CONNECTOR_OUTCOME_UNKNOWN'
219-
? '调用已经发出但没收到回执,结果未知(可能已经扣费);宿主对连接器调用有超时上限,太慢的调用会被中止。'
220-
: error?.code === 'CONNECTOR_TIMEOUT'
221-
? '这次调用超过了宿主的时限(默认 30 秒)被中止;结果未知,先核对业务状态再决定要不要重试。'
222-
: String(error?.message ?? error);
253+
const note = connectorStopNote(suffix, error?.code) ?? String(error?.message ?? error);
223254
throw Object.assign(
224255
new Error(`调用 ${suffix} 失败(耗时 ${(callMs / 1000).toFixed(1)}s${queueNote(waitedMs)}):${note}`),
225256
{ code: error?.code ?? 'CONNECTOR_ERROR' },

‎plugins/antianqi/ai-node-canvas/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "ai-node-canvas",
3-
"version": "0.83.0",
3+
"version": "0.84.0",
44
"private": true,
55
"description": "Node-graph canvas MiniApp for MiniMax Code: wire MiniMax model runs into a visual workflow.",
66
"mcode": {

‎plugins/antianqi/ai-node-canvas/servers.mcp.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"url": "http://127.0.0.1:9/mcp/canvas",
77
"headers": {},
88
"description": "节点画布的托管 MCP:读画布(canvas_list / canvas_read),也在画布上加节点、连线、改节点、删节点、发起运行(canvas_add_node / canvas_connect / canvas_update_node / canvas_remove_node / canvas_run),还有现成的工作流模板(canvas_list_templates / canvas_apply_template)。写操作改的是用户画布上看得见的东西;canvas_remove_node 删掉的节点没法恢复;canvas_run 会真的发起一次生成、花掉用户自己的额度(视频可能跑十几分钟,但这次调用很快就会返回「已经开始」,结果用 canvas_read 看);canvas_apply_template 只把模板的骨架铺到画布上(节点、连线、提示词、参数),它自己不跑、不花钱,要跑得再调 canvas_run。",
9-
"timeout": 30000
9+
"timeout": 60000
1010
}
1111
}
1212
}

‎plugins/antianqi/ai-node-canvas/tests/smoke.test.mjs‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8767,3 +8767,30 @@ test('the README says that a deleted template is gone for good', async () => {
87678767
assert.match(en, /cannot be undone/, '英文 README 要写明删掉找不回来');
87688768
assert.match(zh, /找不回来/, '中文 README 要写明删掉找不回来');
87698769
});
8770+
8771+
test('a call the Host stopped says where the limit is, and what to do', async () => {
8772+
// The Host gives every connector call a window of its own, and a picture is
8773+
// asked for in one call — there is no task to poll, so a picture that takes
8774+
// too long is lost. The message has to say where that limit is, and what the
8775+
// user can still change.
8776+
const { connectorStopNote } = await import('../miniapp/node/connector-runner.mjs');
8777+
const image = connectorStopNote('generate_image', 'CONNECTOR_OUTCOME_UNKNOWN');
8778+
assert.match(image, /30 秒/, '要说出这个窗口有多长');
8779+
assert.match(image, /宿主/, '要说出这个上限在宿主那边');
8780+
assert.match(image, /1K/, '出图要给出马上能用的办法');
8781+
// A call that was not stopped is not one of these.
8782+
assert.equal(connectorStopNote('generate_image', 'CONNECTOR_PROVIDER_ERROR'), null);
8783+
// A video is submitted and polled, so it does not need a smaller request.
8784+
const video = connectorStopNote('submit_video_generation', 'CONNECTOR_TIMEOUT');
8785+
assert.match(video, /30 秒/);
8786+
assert.equal(/1K/.test(video), false, '视频不是一次调用出结果,不用教人降清晰度');
8787+
});
8788+
8789+
test('the MCP server is given a minute', async () => {
8790+
// The Host waits this long for the MCP server to answer. A call that starts
8791+
// a run has to hand the work to the page and wait for the page's answer, so
8792+
// the window is a minute: a page that is busy must not turn the call into a
8793+
// failure.
8794+
const raw = await readFile(fileURLToPath(new URL('../servers.mcp.json', import.meta.url)), 'utf8');
8795+
assert.equal(JSON.parse(raw).mcpServers.canvas.timeout, 60000);
8796+
});

0 commit comments

Comments
 (0)