shaomang/mtnode-aio/app-dev
Claude Code and agent skill by shaomang/mtnode-aio
- community
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-devSKILL.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 就先读它们,改完用该技能的检测器自查一遍。
一、应用契约(四条硬线)
- 纯静态:只有 HTML / JS / CSS(+ 图标、字体、json 数据等静态资源)。没有 Node、没有
require、没有import主程序模块。
index.html 用相对路径引用同目录资源(./style.css、./app.js、./assets/…)。
- 本身不依赖 appHost:
window.appHost/window.pluginApi都可能不存在(用浏览器直接打开 index.html、旧版本宿主、桥被裁剪)。
应用必须在没有任何 appHost 的情况下仍能启动并可用:数据退回内存、功能按能力缺失降级、界面明确告知而不是白屏或抛异常。 优先只调用 typeof host.x === "function" 判过的能力(脚手架 apphost.js 的 window.AppHost.cap 已把这层包好)。
- 内容要落盘:存档只走桥(应用中心
dataWrite/ 插件窗口dataSet),不要用localStorage当存档、不要往应用目录写文件;
自动存盘(脏标记 + 防抖)+ 关窗前强制冲刷是默认姿势(脚手架 store.js + close.js,见第三节)。
- 需要后端 / 模型 / 文件能力时才调 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) | |
|---|---|---|---|
| 应用 id | id(宿主按发送方窗口认应用,应用侧拿不到别人的) | 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),新应用照它写即可。
调用样例(先探测、再调用、失败不炸):
生命周期 / 事件:窗口 ready-to-show 与 did-finish-load 后宿主会 show 并置顶(应用中心窗口还会按需发 apps:windowChanged)。 事件订阅都返回退订函数,应用在收尾时退订(见第三节),避免重复绑定。
三、三件基础设施(每个应用都要有)
3.1 正确关闭(关窗 = 先请应用收尾,再关)
宿主 close() 不会直接销毁窗口,而是:
主程序退出(before-quit → shutdownApps())走的同一条;quit()(连 MTNode 一起退)也是先让该应用收尾,再请主进程正常退出。
应用侧要做的就一件事:把「写盘 + 退订」挂进收尾钩子(脚手架 close.js):
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:选在根内记相对名(换数据目录 / 换机器仍认),选在根外记绝对路径;落在应用目录里 / 盘根一律不认(回落默认数据根)。 - 应用窗口里还有状态徽标(「自定义位置」「还没写过数据」)与打开 / 更改… / 恢复默认三个动作;应用中心的
「应用设置」窗只给只读路径 + 打开(这一格不再有改数据文件夹的入口)。
四、应用目录结构
宿主机上落点(应用自己不要写这些路径,只能通过 appHost 间接读写):
本地开发目录建议:直接把应用源码放在一个普通文件夹里(如 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
- SKILL.md in the repositoryCheckingmtnode-agent-skills/app/app-dev/SKILL.md
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
[](https://verified-skill.com/skills/shaomang/mtnode-aio/app-dev)