Skip to content

ai-node-canvas: an infinite-canvas node graph for MiniMax model runs - #71

Open
antianqi wants to merge 16 commits into
MiniMax-AI:mainfrom
antianqi:feat/ai-node-canvas
Open

antianqi wants to merge 16 commits into
MiniMax-AI:mainfrom
antianqi:feat/ai-node-canvas

Conversation

@antianqi

@antianqi antianqi commented Oct 24, 2026 •

Copy link
Copy Markdown

What it does

An infinite-canvas node graph for MiniMax Code. Every node is one model run and
every line is a data path
: connect a text node to an image node and the upstream
text lands in the downstream prompt. A result becomes a node of its own, so it can
be worked on again — 宫格切分 / 剪裁 / 智能抠图 / 变清晰 / 无损改尺寸 / 证件照 run on
the picture a result holds, and 多角度 / 打光 ask the model for another version of it.

  • 18 node types, one shared form and parameter table — the page and the Node
    service read the same rules, so they cannot drift apart.
  • 14 local media tools (ffmpeg on this machine) and 2 tools that draw in the
    browser and publish a real file.
  • A hosted MCP server: the Agent can read the board, add nodes, connect them and
    start a run.
  • Templates that are the skeleton of a chain — a product picture and then an
    ad, an e-commerce hero shot, a shot list and a short film, a batch of
    pictures, a clip read back into a prompt, a picture redrawn. A template
    carries the steps only, so it works on another machine; putting one on the
    canvas does not run it. The Agent lists them and puts them on the canvas
    too (canvas_list_templates, canvas_apply_template), and a chain of the
    user's own can be kept the same way.
  • Thirteen ready-made workflows, under the headings they belong to —
    从文字出发 (four chains that start from words), 从素材出发 (a picture
    redrawn, a picture imitated and a clip imitated: the material is read
    back into a prompt and the prompt is what is generated from), 读回提示词
    (a clip read back into a prompt) and H3 (one for each shape the request can
    carry: T2VA, I2VA, FL2VA, L2VA and the full-reference set — each of them
    carries the prompt node that writes the structure H3 expects).
  • A template travels as a file, and it goes where the user says — 导出模板
    writes the steps of the canvas that is open now into a JSON file and asks for
    the path: the Downloads folder, the desktop, the documents, or any folder the
    user types. 导入模板 opens a panel with the ready-made workflows and the
    template files this machine has, and a template that is picked there gets a
    canvas of its own
    — the canvas that is open now is a piece of work in
    progress and is left alone. A file carries the steps and nothing else, so it
    opens whole on another machine: no results, no material, nothing that would
    break. The empty input a chain starts from does travel, so the person who
    puts the template on their board puts their own file in it.
  • The canvas is a workbench, and it saves itself — a change is written down
    about a second later, the state beside the canvas name says whether it
    happened, and a window that goes away takes the local draft with it. There is
    no 保存 button: doing by hand what the page already does is only a second
    way to do it. What is worth keeping is a template (the steps) and the
    results (in the library, savable to the disk) — and the delete dialog
    says so before it takes anything away.
  • A Skill (skills/ai-node-canvas/SKILL.md, written in Chinese) that tells the
    Agent what the canvas is for, when the prompt node is the right step, how the
    reference numbers work and which chains are worth building. The prompt node
    carries a template, so a node the Agent makes expands the sentence the same
    way the page would — the H3 shapes included.
  • A built-in Agent turn for the text mode.

Install

Copy the ai-node-canvas directory, including the hidden .minimax-plugin
directory, into ~/.minimax/plugins/ai-node-canvas/, restart MiniMax Code, and ask
the Agent to open "节点画布". The README has the details.

Tested environment

  • MiniMax Code 3.1.2 on Windows 11 (Node 22)
  • MiniMax Code 3.1.2 on macOS 15

What I verified

  • npm run check in this repository: 0 errors, 1 warning (explained below).
  • The package's own suite: 206 unit tests, plus a 188-case mutation
    self-check
    that breaks each rule on purpose and requires the tests to go red.
  • On the desktop client, with the same directory this PR ships (byte-identical:
    38 files, 0 differences):
    • image, video, audio and music generation through the Host connector;
    • 宫格切分 → 剪裁 → 智能抠图 on a generated picture, each step publishing a real
      file, and the picture the tool worked on surviving the run;
    • 多角度 with two angles: both runs finish, the second one waits for the
      machine and says so in the run log;
    • a canvas of 56 nodes with no two cards overlapping while a batch is placed;
    • a picture-to-video ad end to end: a product picture → a 5.9 s 1366×768
      clip (submitted, polled to running, finished in 88 s, and the file fetched
      back and written into the result node);
    • a card holding a video carries no toolbar: the finished clip shows the
      clip and 保存 and nothing else — no tool buttons, and no empty strip left
      below it either — while the picture results beside it still carry their
      own eight tools;
    • 保存 clicked once on the real machine: the copy landed in the
      Downloads folder under the node's own name, the picture it was made from
      stayed where it was, and the page said where the file went;
    • the prompt node the Agent made carries its template: the card shows
      电商主图 in the template drop-down, and the MCP call can name the H3
      shape it wants instead;
    • 新建 / 重命名 / 删除 ask inside the page: 新建 made a canvas and put the
      cursor into a name field in the header, the typed name was kept on Enter and
      shown in the title button; 重命名 brought the same field back with the
      current name in it and Escape left the name as it was; 删除 asked in a
      dialog that belongs to the page — 取消 left the canvas alone, 确定 removed it
      and the page fell back to the canvas that was left. Before this, all three
      asked through a native prompt() / confirm(), which is a separate OS
      window that never reaches the screen inside the MiniApp host, so the
      buttons did nothing at all;
    • 导出模板 writes the file where the user says: the file landed on the
      Desktop as 图片模仿.模板.json — the panel asks for the path, the
      Downloads folder, the desktop, the documents or a folder the user types —
      and its content is the template itself: the steps and the lines between
      them, with no result and no material in it. The runtime writes the file,
      because a download the browser starts inside the Host never arrives under
      the name it was given: it stays a temporary name in the folder;
    • 导入模板 opens a canvas of its own: the panel held the ready-made
      workflows and the template files this machine has; picking 图片模仿 left
      the canvas that was open alone and made a new canvas of its own —
      named after the template — holding the chain 参考图 → 反推提示词(图生文) →
      复刻(文生图). A file that is not a template is not offered at all;
    • the toolbar of v0.71.0 on the real machine: 新建 / 重命名 / 导出模板 /
      导入模板 / 模板 / 删除, with the state chip beside the canvas name and a
      line between the verbs of the canvas and the verbs of a template; the
      模板 panel lists 我的模板 and the four groups of ready-made workflows under
      their own headings, and the delete dialog names the two ways of keeping the
      work (存成模板, and the library) before it takes the steps away.

Not verified

  • A video with a sound track: the provider's account-credit models (H3,
    H3-Max) were refused because this account has credit auto-consumption turned
    off, so the clip above was made with the Token Plan model (silent). The H3
    path — the one that renders a native audio track — is therefore still
    unverified against the real provider.
  • The community install path was not exercised a second time: the same plugin
    ID is already installed here as a local development copy, and the Host rejects
    a second package with the same ID. The directory in this PR is byte-identical
    to the one that was tested.
  • A whole canvas cannot be moved to another machine as a file any more: the
    copy of a canvas carried the result and the material with it, and the
    material does not exist on the other machine, so it opened with broken
    pictures. What travels now is the template (the steps) and the results, which
    the user saves to the disk from the library.

The one warning

HOST_CONNECTOR_UNVERIFIED — the manifest declares the matrix provider under
hostConnectorAccess, and the Host grants the declared providers at install time.
This repository does not check them, so the warning is expected. The package does
call that provider; the README says which calls it makes and what it reaches.

@MyPrototypeWhat MyPrototypeWhat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at abc8ea9.

Thanks for this. The latest update (hiding the toolbar and footer on video cards via cardChrome in miniapp/client/forms.mjs, with a test) looks good and doesn't touch the points below. The connector is used only through context.hostConnector, nothing reads login credentials or other plugins' data, agent approvals resolve only when the user clicks, and ffmpeg is spawned with an argument array and shell: false (miniapp/node/ffmpeg.mjs:136). A few things need to change before merge.

Must fix

  1. A save name of .. escapes the Downloads folder. miniapp/node/server.mjs:373 strips path separators from wanted but not .., and miniapp/node/media-store.mjs:576 joins it onto the Downloads folder. Reading the code, a name of .. resolves the target to the home directory itself, and the "(1)" rename at media-store.mjs:580-586 then creates a file next to ~/Downloads. Please reject . and .. (and any name that resolves outside the target folder) before the copy.

  2. The MCP server description is wrong. servers.mcp.json:8 says the server is read-only ("只读"). miniapp/node/mcp.mjs also exposes canvas_add_node (:54), canvas_connect (:87), canvas_update_node (:108), canvas_remove_node (:137, which can't be undone) and canvas_run (:152, which spends the user's generation quota; its own description says video runs can take more than ten minutes). Please describe what the server can actually do, including that it can delete nodes and start paid runs. The tool-level annotations are already right; the server description has to match them.

Optional

  1. Writing into ~/Downloads. The library "save" action copies assets into ~/Downloads (miniapp/node/media-store.mjs:88-89, :561-587; miniapp/node/server.mjs:372-374), outside context.dataDir. Consider handing the file to the user from the page instead (for example a browser download of the asset URL), so the user picks where it goes.
  2. Downloads of arbitrary URLs. miniapp/node/media-store.mjs:151-155 fetches any http(s) URL with redirect: 'follow' and doesn't block private or loopback addresses; this now also covers the provider's video URL from connector-runner.mjs. Consider allowlisting the provider's media domains, or rejecting private, link-local and loopback targets, including after redirects.
  3. Tests cancel at the end of the suite. On Node 22.23 and 20.19, the plugin's own tests cancel from #172 ("a page that goes quiet keeps the canvas its own", tests/smoke.test.mjs:6395) onward. The cause is the timer.unref() on the ack timeout in miniapp/node/action-queue.mjs:186: with stubbed store and runner, nothing else keeps the event loop alive, so the wait never settles. Removing that unref makes all 182 pass locally. Please drop the unref, or keep a handle alive in the test.
  4. The HOST_CONNECTOR_UNVERIFIED warning from npm run check is expected for a hostConnectorAccess declaration. A line in the README's "Data & access" section about what the matrix connector is used for (image, video and speech generation) would help reviewers and users.

…download cannot reach this machine, and the MCP server says what it really does
@antianqi

antianqi commented Oct 24, 2026 •

Copy link
Copy Markdown
Author

Thanks for the review — all four points were fair, and the two must-fix items are fixed in v0.60.0 (commit fca90ec).

Must fix 1 — a save name of .. escaping the Downloads folder

Fixed. The name is now judged before the copy, and the judgement lives in one place:

  • plainName(name) — what reaches the file system is one plain segment: no separator survives, and a dot segment (., ..) is not a name at all.
  • resolveInside(folder, name) — the name is judged as it was given, on the resolved path: . is the folder itself, .. is its parent, and anything that resolves outside the folder is refused too. A name that only looks harmless cannot escape.

So .., ., ../x.png and sub/../../x.png are all refused, with a 400 and a message the user can act on (这个名字不能用,换一个普通的文件名试试) instead of a confusing 404. The name is not quietly tidied up into something that looks safe — that was my first attempt, and my own test caught it.

Two related things I fixed while in there, because they were the same rule written twice:

  • The material id (the part of the route that names the asset) is judged by the same plain-name rule, so it cannot name the folder above assets/ either.
  • discard() had its own copy of "a path inside a folder, or nothing". It now calls the same resolveInside — one rule, not two copies that can drift.

Must fix 2 — the MCP server description

Rewritten to say what the server can actually do: read the board (canvas_list / canvas_read) and also add, connect, update and remove nodes and start runs (canvas_add_node / canvas_connect / canvas_update_node / canvas_remove_node / canvas_run). It now states that a removal cannot be undone, and that a run really does start a generation and spends the user's own quota (a video can take minutes — the call itself returns "started" quickly, and the result is read with canvas_read).

Optional 1 — writing into ~/Downloads

We kept this deliberately, and I would like to keep it: the file is already on the machine, and the whole point of 保存 is that the copy lands where a person looks for a picture, not in a runtime folder nobody opens. What I did do is make the write safe and honest about what it does:

  • the name is judged (see above) and refused rather than trimmed;
  • it is a copy — the original stays where the canvas points at it;
  • a file that is already there is not overwritten, it gets a number;
  • the page says where the file landed (已保存到 …).

If you would rather have the page hand the file to the browser (a normal download, the user picks the folder), say so — that is a small change on the page side and I am happy to switch.

Optional 2 — downloads of arbitrary URLs

Fixed. A download now judges the address before the request goes out:

  • a link that spells out a private address is refused straight away (loopback, 10/8, 172.16/12, 192.168/16, link-local, and the reserved ranges, in IPv4 and IPv6);
  • a name is looked up first, and the download is refused if any of the addresses it points at is private;
  • redirects are followed by hand, so every hop is judged — a public address that redirects into the loopback is refused as well.

Optional 3 — the tests cancelling at the end of the suite

Fixed: the timer.unref() on the ack timeout in action-queue.mjs is gone. The timeout is 8 s, so the cost of keeping the handle is at most 8 seconds of process lifetime when a wait is left hanging.

One honest caveat: I could not reproduce the cancellation here — this machine runs Node 24.18, where the suite passed with the unref() still in place. I applied the change you asked for and my suite is green, but if it still cancels on 22.23 / 20.19, tell me and I will dig further.

Optional 4 — the matrix connector in "Data & access"

Added a line at the top of the section (and at the top of 「数据与访问」 in the Chinese README): the connector is used for media generation only — images, video, speech and music, plus reading a video back for understanding — and the Host holds the credentials and the egress.

Where it stands now

  • The package's own suite: 186 unit tests (up from 181) — the new ones cover the refused save names, the refused material id, the private-address rules, and the per-hop redirect rule.
  • The mutation self-check: 155 cases (up from 149) — every rule above is broken on purpose and the tests have to go red. Six of the new cases cover this round's rules; two of them failed on the first run, which is how I found the duplicated judgement and the "tidy up the name instead of refusing it" mistake.
  • npm run check in this repository: 0 errors, 1 warning (the expected HOST_CONNECTOR_UNVERIFIED).
  • On the desktop client, with the same directory this PR ships (byte-identical, 35 files, 0 differences): the video card shows the clip and 保存 and nothing else, and 保存 was clicked once on the real machine — the file landed in the Downloads folder and the page said where.

Happy to go through any of it again if something reads differently to you.

…he lines that carry what each one made to the next
…opus with a beret, a brush and a palette, standing on the board
… dark and the light theme both drawn by the model
…he canvas, and let it name the prompt template
…ady-made workflows the user and the Agent can put on the canvas
…name and a deletion are settled inside the board
@antianqi

Copy link
Copy Markdown
Author

Two more things landed on this branch since the last reply.

v0.68.0 — the page asks in itself (commit e0b0ef9). 新建 / 重命名 / 删除 used to ask through window.prompt / window.confirm. Inside the MiniApp host those are separate OS windows that never reach the screen, so all three buttons looked dead. They now ask inside the board: the name is typed into a field in the header, and the deletion is confirmed in a dialog that belongs to the page (Escape and 取消 both back out). Verified on the desktop client — see the new bullet in the description.

The numbers in the description are the true ones now: 193 unit tests, a 164-case mutation self-check (every case breaks one rule on purpose and requires the tests to go red), and 38 files that are byte-identical between this branch and the directory that was tested on the client.

npm run check in this repository: 0 errors, 1 warning (HOST_CONNECTOR_UNVERIFIED, explained in the description).

…res templates, and the runtime writes the file
@antianqi

Copy link
Copy Markdown
Author

v0.69.0 — a template travels as a file (commit 89d7b2f).

The toolbar now reads 导出模板 | 导入模板 | 模板. 导出模板 writes the steps of the open canvas into a JSON file in the Downloads folder and says where it landed; 导入模板 opens a panel that lists the template files this machine has, and one click puts one on the board. A file carries the steps and nothing else, so it opens whole on another machine — no results, no material, nothing that would break.

One thing worth knowing for review: the runtime writes the file, not the browser. A download the browser starts inside the Host never arrives under the name it was given — it stays a temporary name in the Downloads folder, which is why the file is written by the Node side and the page is told the path. The whole-canvas export moved the same way, and now lives in the 模板 panel next to the import of a canvas, so the toolbar carries templates only.

The numbers in the description are the true ones now: 199 unit tests, a 177-case mutation self-check, and 38 files that are byte-identical between this branch and the directory that was tested on the client.

npm run check in this repository: 0 errors, 1 warning (HOST_CONNECTOR_UNVERIFIED, explained in the description).

…- no save button, the toolbar carries templates only
@antianqi

Copy link
Copy Markdown
Author

v0.70.0 — the canvas is a workbench, and it saves itself (commit ffe024c).

The toolbar now carries three kinds of thing, and nothing else: 新建 / 重命名 / 删除 act on the canvas, 导出模板 / 导入模板 / 模板 act on a template, and a line between the two groups says where one ends and the other begins.

保存 is gone. A change is written down about a second later by itself, the state chip beside the canvas name says whether it happened, and a window that goes away takes the local draft with it — so a change made between the timer and the close is still there when the canvas is opened again. A button that does by hand what the page already does is only a second way to do it.

The 模板 panel holds templates only. It lists 我的模板 and 内置工作流 under their own headings, and it no longer offers a copy of the whole canvas: what a run left behind is in the library, and the steps travel as a template — and a copy of a canvas opened with broken pictures on another machine anyway, because the material does not travel with it. The delete dialog names the two ways of keeping the work before it takes the steps away.

The numbers in the description are the true ones now: 202 unit tests, a 182-case mutation self-check, and 38 files that are byte-identical between this branch and the directory that was tested on the client.

npm run check in this repository: 0 errors, 1 warning (HOST_CONNECTOR_UNVERIFIED, explained in the description).

…made chains, a place to save it, and a canvas of its own when it comes back
@antianqi

Copy link
Copy Markdown
Author

v0.71.0 — a template travels as a file (commit 3d6c73a).

导出模板 now asks where the file should go — the panel takes a path, and the Downloads folder, the desktop, the documents and the home directory are one click away. The runtime writes the file there and says where it landed (the browser's own download never arrives under the name it was given — it stays a temporary name in the folder).

导入模板 opens a canvas of its own. The panel holds the ready-made workflows and the template files this machine has; picking one leaves the canvas that is open alone and makes a new canvas named after the template. The canvas that is open now is a piece of work in progress, and a new chain should not be poured into it.

Thirteen ready-made workflows now, under four headings — 从文字出发 (four chains that start from words), 从素材出发 (a picture redrawn, a picture imitated, a clip imitated — the material is read back into a prompt and the prompt is what is generated from), 读回提示词, and H3: one for each shape the request can carry — T2VA, I2VA, FL2VA, L2VA and the full-reference set. Each H3 template carries the prompt node that writes the structure H3 expects; the runtime adds the line that names the picture.

One thing that came out of testing the export: the empty input a chain starts from travels with it. A template used to drop the input node, so a chain that began with a picture came out of the export without the picture and could not be used. The holder travels now — empty — and the material itself stays behind.

The numbers in the description are the true ones now: 205 unit tests, a 187-case mutation self-check, and 38 files that are byte-identical between this branch and the directory that was tested on the client.

npm run check in this repository: 0 errors, 1 warning (HOST_CONNECTOR_UNVERIFIED, explained in the description).

…e ready-made chains stay in the template panel, the import panel is about files
@antianqi

Copy link
Copy Markdown
Author

v0.72.0 — a template is listed in one menu, not two (commit 64e9862).

You were right that 导入模板 and 模板 had grown into the same thing. The overlap was exactly one thing: the thirteen ready-made workflows appeared in both panels, with a different landing place in each — which is one thing done twice.

They are now split by where the template comes from, so each button has one job:

  • 模板 — the list: the ready-made workflows and the ones the user kept. The one that is picked is put on the canvas that is open.
  • 导入模板 — the files: the template files this machine has, and a file picked from anywhere else. The one that is picked gets a canvas of its own, because a chain that came from somewhere else is a separate piece of work.
  • 导出模板 — the canvas becomes a file.

The import panel says where the ready-made ones are («现成的工作流在「模板」里»), so nothing goes missing.

The numbers in the description are the true ones now: 206 unit tests, a 188-case mutation self-check, and 38 files that are byte-identical between this branch and the directory that was tested on the client.

npm run check in this repository: 0 errors, 1 warning (HOST_CONNECTOR_UNVERIFIED, explained in the description).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants