shaomang/mtnode-aio/app-dev

Claude Code and agent skill by shaomang/mtnode-aio

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

开发 MTNode「应用」(顶栏「应用中心」下载 / 自建后独立窗口运行,或「插件」对话框里 kind=window 的窗口类应用):静态 HTML/JS/CSS 契约(本身不依赖 appHost 也能跑)、两套宿主桥的能力清单与调用样例(应用中心 window.appHost · 插件窗口 window.pluginApi:数据落盘、数据文件夹、账号摘要、创意工坊请求、图片选择与缓存、生命周期事件)、**开发页中栏预览里也有宿主桥**(预览里 close/quit 回 preview_no_window;应用已在独立窗口运行时预览只读、写类回 readonly_preview)、模型能力正解(文本模型与**图像后端**都从 MTNode 继承、界面上必须有选择位、文字+图像多模态输入、无模型/断网时不降级只给明确提示)、应用能力位 capabilities(textInput 带不带语音模块、showDictate 底部宿主听写条、showAuthor 左下角作者标签(平时隐藏 / hover 显示 / 点击看详情)、imageGen 出图;新建默认全开;静态声明不是权限闸)、三件基础设施(正确关闭的数据冲刷握手 / 内容落盘与自动迁移 / 数据文件夹)、应用目录结构与 app.json 字段、默认随应用生成 AGENTS.md 共识文件、常见坑(iframe 无 window.api、sandbox 与 file:// 资源路径、数据只写数据目录)。配套脚手架 templates/app-scaffold/ 与 templates/app-agents/AGENTS.md。

Install

npx vskill@latest install shaomang/mtnode-aio/app-dev

SKILL.md

MTNode 应用开发

MTNode 里的「应用」=一份静态 HTML / JS / CSS,由宿主用独立窗口装载,并注入一座白名单能力桥。两种窗口宿主:

宿主桥(window 上的名字)谁来装数据落点
应用中心(顶栏「应用」;用户自建 / 云端下载)`window.appHost`(preload-app.js + apps-store.js)apps-store.js openAppWindow,loadFile 应用安装根目录 <id>/ 的入口页<数据目录>/apps-data/<id>/data.json(可在窗口里改)
插件窗口(「插件」对话框里 kind: "window" 的卡片)window.pluginApi(= window.forumApi,plugins/preload-window.js + plugins/main-app-plugins.js)插件宿主,解包到 <数据目录>/app-plugins/<id>/runtime/<数据目录>/app-plugins/<id>/data.json

新写应用优先照「应用中心」那一套(脚手架的主路);插件窗口那套的接口差异见第二节的对照表,脚手架 apphost.js 会自动探测两套,写一套代码两边都能跑。

本技能管这两种窗口应用。带本地后端 + 控制台窗 + 画布节点的插件(music3 / h3 / tts / llama / …)见 mtnode-plugin-dev,两者不要混。

所有结论来自当前仓库真实代码,唯一真源:preload-app.js + apps-store.js(应用中心桥与宿主)、plugins/preload-window.js(插件窗口桥)、build.json(打包白名单)。与代码冲突时以代码为准,并回来修订本文件。

脚手架:templates/app-scaffold/(随包分发,build.json 的 files 已含 templates/**)= 首次开发某个应用时复制进它的源码目录的源,占位符替换即得最小可用应用; 里面五件套:apphost.js(桥探测与降级 + 模型四件 + 多模态 text() + 图像三件 `imageModels` / `image` / `cancelImage`)、 app-model.js(右上「模型」选择位,下拉分「文本模型 / 图像后端」两区)、model.css、store.js(脏标记 + 防抖存盘 + flush)、 close.js(关窗收尾注册)、app.js(业务示范:便签 + 文字/图像提问)。

按 `capabilities` 决定复制哪些文件:目标应用 app.json 的 capabilities.textInput 为真才复制 speech.js / speech.css。新建应用默认四位全开(textInput / showDictate / showAuthor / imageGen 缺省 true,见 renderer/app-app-flow.js 的 appsCreateApp), 所以新建出来的应用带这两个文件;只有用户在中栏「预览」条的 ⚙「应用设置」里把 textInput 关掉时才不带。 能力位改动只写 `app.json` 的声明,绝不重写入口页(apps-store.js 的 appCapabilitiesSet)—— index.html 一个字都不动,手改过的页面内容原样保留;听写条与作者标签都只由宿主按声明注入。 capabilities.showDictate 决定应用窗口底部那条宿主注入的听写条显不显示:这一位为真,宿主才注入 renderer/app-speech-ui.js,注入进来即默认可见(markShown,没有隐藏标记);条尾「收起」/ apSpeechHide() 只在本次窗口内藏它(不写回 app.json),apSpeechReveal() 唤回,apSpeechHidden() 问现状。 这条条有两种转录模式(与画布那枚话筒同一套口径,2026-02 二轮):点话筒 / 点左下角那枚常驻浮动话筒按 「关闭 → 持续转录 → 按键转录 → 关闭」循环 —— 持续转录按「说完一句的停顿」(默认 1500ms,apSpeechSilenceMs(ms) 可调) 逐句转写;按键转录按住 F1 说话、松开立刻识别这一句(判键走 ev.code,前卷 240ms,浮动话筒按住同样能说)。 F2 是持续转录的开 / 关(按键转录态下按它回关闭),Esc 与「收起」停止。 每句定稿落进采集开始时焦点所在的那个输入框(input/textarea/contenteditable,原生 execCommand("insertText"), 行尾补空格;焦点跑了先挂住、focusin 补写)—— 自己监听 "mtnode-dictate" 又自己写一遍的应用请调 apSpeechInsert(false)。 话筒右侧还有一枚 ✨ AI 增强 Toggle(默认关):走 appHost.textGenStream(不带 model = 跟随该应用选择 / 宿主默认,thinking:"off",不传 maxTokens),system prompt 只做同音字 / 标点 / 断句 / 语气词清理, user = 这一句 + 极短上下文(约 400 字符);失败 / 超时 / 没配模型一律回落原始识别(绝不吞话)。 入口页里不再内联任何听写 UI(templates/app-default/dict.js 与 apps-store.js 的 dictScriptTag() 内联路径已随本轮改动删除);老应用页面里那份旧内联段在打开 「应用设置」时由 clearLegacyDictInline() 幂等清掉(只认我们自己的标记)。 capabilities.showAuthor 决定应用窗口左下角那枚宿主注入的作者标签显不显示(本轮新增,与听写条同一条机制): 宿主注入 renderer/app-author-ui.js,平时 opacity:0 隐形、鼠标 hover(或键盘聚焦)才浮现 @作者 - v版本(如 @ms2308 - v1.0.1),点一下弹只读「应用详情」(图标 / 标题 / 应用 id / 作者 / 版本 / 说明 / 标签 / 数据目录,出口只有 ✕ 与 Esc —— 浮层不做「点外部即关」)。名字与版本由宿主给: app.json 的 author → 当前登录账号昵称(两级回落,系统占位名 u_xxxxxxxx 不算名字); 两者缺任一个 / 未登录且没写过作者 → 这枚标签整枚不出现(不写「未知作者」)。配色只取应用自己主题的 --bg/--fg/--muted/--accent/--line(与听写条同一份 palette 口径),深浅底都不突兀;左下角那枚听写浮动话筒 与它共用一个角落列(#mtnode-dictate-zone),此时标签排在整列之上(按容器实际顶边算)。 注入闸门只看 capabilities.showAuthor(preload-app.js 的 maybeInjectAppUi + apps-store.js 的 hostCapabilities), 应用侧不用写任何代码;要在别处显示「谁做的、哪一版」,调 appHost.authorTag()(见第二节桥表)。 capabilities.imageGen 只是声明,出图接口与文本接口一样始终可调。 新建应用的默认欢迎页在 templates/app-default/index.html(apps-store.js 的 defaultPageHtml() 读它),与脚手架同一套设计语言与同一段上手文案。

默认随应用生成 AGENTS.md(不必问用户):复制脚手架时,把 templates/app-agents/AGENTS.md 一并复制成该应用根目录的 `AGENTS.md` (与 index.html 同级),再按该应用的实际情况补全其中的目录约定与「不要修改」清单。目标目录里已经有 `AGENTS.md` 的不要覆盖 (那是用户自己写的共识);已有 agent.md 入口文件同理,别动它。这份文件是下个会话(人写的或 AI 写的)接手时先读的东西。

界面设计先按 impeccable 规范:动手写应用界面(默认页 / 脚手架 / 任何窗口页)之前,先加载 mtnode-agent-skills/app/impeccable/(SKILL.md 入门,reference/craft-floor.md 是底线,reference/operate.md 讲产品界面);项目里若已有 PRODUCT.md / DESIGN.md 就先读它们,改完用该技能的检测器自查一遍。


一、应用契约(四条硬线)

  1. 纯静态:只有 HTML / JS / CSS(+ 图标、字体、json 数据等静态资源)。没有 Node、没有 require、没有 import 主程序模块。

index.html 用相对路径引用同目录资源(./style.css、./app.js、./assets/…)。

  1. 本身不依赖 appHost:window.appHost / window.pluginApi 都可能不存在(用浏览器直接打开 index.html、旧版本宿主、桥被裁剪)。

应用必须在没有任何 appHost 的情况下仍能启动并可用:数据退回内存、功能按能力缺失降级、界面明确告知而不是白屏或抛异常。 优先只调用 typeof host.x === "function" 判过的能力(脚手架 apphost.js 的 window.AppHost.cap 已把这层包好)。

  1. 内容要落盘:存档只走桥(应用中心 dataWrite / 插件窗口 dataSet),不要用 localStorage 当存档、不要往应用目录写文件;

自动存盘(脏标记 + 防抖)+ 关窗前强制冲刷是默认姿势(脚手架 store.js + close.js,见第三节)。

  1. 需要后端 / 模型 / 文件能力时才调 appHost,且必须优雅降级:没有对应接口时不假装成功、不静默丢数据。

appHost 是白名单桥,只暴露第二节列出的那些方法;没有的接口就是没有,不要发明(见第六节)。

窗口内的运行环境事实(openWindowPlugin 固定):contextIsolation: true、nodeIntegration: false、sandbox: false; setWindowOpenHandler 只把 http/https 交给系统浏览器,其余一律 deny;will-navigate 阻止离开本页。


二、appHost 能力清单(两套桥对照)

不要假设还有 window.api(那是主窗口渲染层的桥,应用窗口里没有)。

能力应用中心窗口 window.appHost(preload-app.js)插件窗口 window.pluginApi(= window.forumApi)
应用 idid(宿主按发送方窗口认应用,应用侧拿不到别人的)id(--mtnode-plugin-id= 注入,取不到是 "")
数据读写(整份)dataRead({file?}) → {ok,data,file,migrated};dataWrite(data,{file?}) → {ok,bytes}dataGet() → {ok,data};dataSet(data) → {ok}
数据文件夹dataDirGet() → {ok,dir,root,def,exists,file,allow};dataDirPick()(用户亲自选);dataDirOpen();dataDirReset()(回默认)—(路径由宿主管,应用看不到)
键值存储(老写法)storageGet/Set/All/Remove(内部走同一个 data.json 的 kv)—(用 dataGet/dataSet 自己存对象)
账号摘要account() → {ok,loggedIn,user,encryption}(无 token)authGetState() / authMe() / authLoginPassword / authChangePassword / authLogout / onAuthChanged(cb)
本机清单摘要(谁做的 / 哪一版)authorTag() → {ok,name,version,id,title,description,tags,icon,dataDir}(icon 是宿主读好的 data URL;名字版本凑不齐回 {ok:false,code:"no_author_tag"})。注入左下角那枚作者标签用的就是它;应用自己想在别处显示也可以用(与语音 / 图像同一条纪律:能力位只管宿主注不注入,桥上接口始终在)—
创意工坊请求—(应用中心窗口没有网络面)storeRequest({method,path,json,anon})(主进程持 token)
选图 / 压图 / 缓存—pickImage() / compressImage() / cacheImage(id,base64) / readCachedImage(id)
模型能力(可选)textGenStream(opts, cb)(文字 + 图像多模态;默认关思考,opts.thinking 认 off / on(=high) / low / high / max,非法值回 bad_thinking;回执带 finishReason / truncated / reasoningChars)。宿主注入的听写条「AI 增强」就是它的标准用法:{ messages:[{role:"system",…},{role:"user",…}], thinking:"off", temperature:0.2 } —— 不带 `model`(跟随该应用的选择 / 宿主默认)、不传 `maxTokens`,失败 / 超时回落原文(见第七节坑表)· 脚手架另有 AppHost.json(opts)(要 JSON 就用它)· imageGen(opts) / imageEdit(opts)(出图 / 图生图,见第六节) · hostModels() / hostModel() / hostSetModel(id)(模型从 MTNode 继承)· pickImage() 系统选图。服务商与 Key 只留主进程,应用只能给 prompt / messages / model(清单内 id) / 温度 / 图像路径或 dataURL;maxTokens 别随手设(它是思考+正文共用预算,见第七节)—(插件窗口这套没有模型桥:见第六节的正路 —— pickImage + 自己的后端,或升级为本地后端插件)
生命周期close()(关自己窗口)· quit()(连 MTNode 一起退)· `onWillClose(cb)`(关窗前收尾,见第三节)close() · onShown(cb) · onAuthChanged(cb)

调用前一律先探测:typeof host.dataWrite === "function",缺就走降级分支。脚手架的 `window.AppHost` 把两套桥统一成一套名字 (getData/setData/dataDir*/storage*/accountText/request/on/onShown/onWillClose/close/quit),新应用照它写即可。

调用样例(先探测、再调用、失败不炸):

js
/* 脚手架统一层:两套桥都认,缺能力自动降级 */
const H = window.AppHost;
/* 数据落盘:带脏标记 + 防抖 + 关窗冲刷(store.js) */
const store = window.Store.create({ host: H, file: "data.json", debounceMs: 400, initial: { notes: [] } });
await store.load(); // 启动读回(老 storage/store.json 首次读会自动迁移过来)
store.set({ notes: notes }); // 改完标脏,防抖后自动写盘
store.flush(); // 想立刻落盘时手动调
if (!store.persisted) showBanner("本次数据不会保存"); // 桥缺席:内存态 + 明确提示
/* 数据文件夹:显示 / 打开 / 让用户改 */
const info = await H.dataDirGet(); // { ok, dir, root, def, exists }
// info.dir 显示给用户;info.def=false 说明是用户改过的地方
await H.dataDirPick(); // 弹系统目录框(只有用户亲自选的那一次生效)
await H.dataDirOpen(); // 在资源管理器中打开
await H.dataDirReset(); // 回默认数据根(只删指针,不删文件)
/* 账号摘要(两套桥都走这一个名字) */
const line = await H.accountText(); // "未登录" / 昵称 / "未接入宿主"

生命周期 / 事件:窗口 ready-to-show 与 did-finish-load 后宿主会 show 并置顶(应用中心窗口还会按需发 apps:windowChanged)。 事件订阅都返回退订函数,应用在收尾时退订(见第三节),避免重复绑定。


三、三件基础设施(每个应用都要有)

3.1 正确关闭(关窗 = 先请应用收尾,再关)

宿主 close() 不会直接销毁窗口,而是:

按钮 → host.close() → 主进程 closeAppWindow(id) → webContents.send("apps:willClose")
→ 应用把所有收尾动作跑完 → ipcRenderer.invoke("apps:ackClose") → 主进程 w.close()
(应用忘了回包 / 卡住:WILL_CLOSE_MS = 1500ms 到点直接关,绝不钉住窗口)

主程序退出(before-quit → shutdownApps())走的同一条;quit()(连 MTNode 一起退)也是先让该应用收尾,再请主进程正常退出。

应用侧要做的就一件事:把「写盘 + 退订」挂进收尾钩子(脚手架 close.js):

js
window.AppClose.on(() => Promise.all([store.flush(), Promise.resolve(H.offAll())]));

close.js 还挂了 visibilitychange(hidden) / pagehide / beforeunload 三处兜底冲刷(主程序被强杀时最后一道闸), 但主路是 willClose 握手:只有它会等在飞的那次写盘完成。

3.2 内容落盘(默认数据根)
  • 默认落点:<数据目录>/apps-data/<id>/data.json(apps-store.js 的 appDataRoot)。不跟应用安装目录走 —— 升级 / 卸载不带走数据。
  • 形态:整份 JSON,原子写(tmp + rename),整份上限 2MB(超了回 {ok:false,error:"应用数据超出上限(2MB)"})。
  • 文件名白名单:data.json(老名字 store.json 兼容);不许带分隔符 / `..` / 盘符(normDataFileName 会拒)。
  • 老数据自动迁移:首次读时若 data.json 不在、而应用安装目录里的 <id>/storage/store.json 在,

宿主自动把它迁到默认数据根(旧文件保留、不删);只在「还没有 data.json」时搬,且只搬默认数据根,绝不动用户另选过的文件夹。

  • 撤写白名单:只有「默认数据根 + 用户亲自选过的文件夹」是合法落点(dataAllowList),别的路径一律拒绝。
3.3 数据文件夹(用户可改)
  • 默认 = 默认数据根;用户可以在应用窗口里(dataDirPick())改 —— 应用中心那一行本轮已撤,

现在只在右上角齿轮那枚「应用设置」窗里只读显示该应用的数据目录 + 一个「打开」按钮。

  • 路径只能来自用户在系统目录框里亲自选的那一次:dataDirPick 是宿主自己弹的框,应用(与 agent)传不了路径;主进程侧 pickAppDataDir 无宿主窗口直接拒绝。
  • 改完不搬数据:新目录当场生效(没有就建),旧目录文件原样留着;「恢复默认」只删指针(dataDir.json),也不删文件。
  • 指针存 <数据目录>/apps-data/<id>/dataDir.json:选在根内记相对名(换数据目录 / 换机器仍认),选在根外记绝对路径;落在应用目录里 / 盘根一律不认(回落默认数据根)。
  • 应用窗口里还有状态徽标(「自定义位置」「还没写过数据」)与打开 / 更改… / 恢复默认三个动作;应用中心的

「应用设置」窗只给只读路径 + 打开(这一格不再有改数据文件夹的入口)。


四、应用目录结构

<应用 id>/ zip 根;应用中心的源码目录 / 插件窗口解包后到 runtime/
index.html 入口(app.json 的 entry,默认 index.html;宿主只认 .html)
AGENTS.md 本应用的开发共识(目录约定 / 不要修改清单 / 能力桥用法 / 数据落盘 / 设计规范)
—— 从 templates/app-agents/AGENTS.md 复制,**默认就建、不询问**;已有则保留
apphost.js 桥探测与优雅降级(脚手架之一;两套桥都认 + 模型四件 + 多模态 text())
app-model.js 模型选择位(右上「模型」按钮 + 下拉,模型从 MTNode 继承)
model.css 模型选择位与示例区样式
store.js 内容落盘:脏标记 + 防抖自动存盘 + flush()
close.js 关窗收尾:AppClose.on(cb) 登记,宿主关窗前跑完
app.js 逻辑
style.css 样式
app.json 自描述元数据(下节;离线 / 本地开发 / 发布自检用)
assets/… 图标 / 图片 / 字体 / 静态 json

宿主机上落点(应用自己不要写这些路径,只能通过 appHost 间接读写):

应用中心这套:
应用根目录/<AppName>/ 源码目录(**固定**:库 `<数据目录>/apps`、开发 `<数据目录>/apps-dev`;
升级 / 卸载 / 删除应用 = 整目录替换或进回收站,别往里写数据)
<数据目录>/apps-data/<id>/data.json 应用数据(默认数据根;dataRead / dataWrite 的落点)
<数据目录>/apps-data/<id>/dataDir.json 数据文件夹指针(用户改过才有)
<数据目录>/apps-models.json 各应用选的模型(宿主侧,按应用 id;卸载时清掉那一项)
插件窗口这套:
<数据目录>/app-plugins/<id>/ 默认 %APPDATA%\pipeline-console\app-plugins\<id>\
runtime/ 解包后的应用静态文件(升级 = 整目录替换)
installed.json 宿主写的安装元数据
data.json appPlugins:dataGet / dataSet 的落点

本地开发目录建议:直接把应用源码放在一个普通文件夹里(如 apps/<id>/),目录名与 app.json 的 id 保持一致; 应用中心「开发」页可以直接把这个目录当预览源(mtnode-preview://<id>/<entry>?_host=1),改完刷新即见。

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
5

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/shaomang/mtnode-aio/app-dev/badge)](https://verified-skill.com/skills/shaomang/mtnode-aio/app-dev)

Something wrong with this skill or its page? Tell us.