Add pagx preview CLI and MCP service with file watching, integrated into pagx CLI - #3642
Conversation
…AI coding assistants.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #3642 +/- ##
==========================================
- Coverage 83.04% 83.03% -0.01%
==========================================
Files 712 712
Lines 94876 94876
Branches 26597 26597
==========================================
- Hits 78793 78784 -9
- Misses 10410 10413 +3
- Partials 5673 5679 +6 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| "files": [ | ||
| "bin/", | ||
| "html-snapshot/", | ||
| "preview/", |
There was a problem hiding this comment.
[发布流程] 打包未自动拷贝 preview/,依赖手工发布
files 声明了 preview/,但仓库里的 pack.sh 与 prepack(仅跑 build-html-snapshot.js)都没有把 playground/pagx-preview/(含已构建的 wasm/ 产物)拷进 cli/npm/preview/ 的步骤。
目前能发出可用的包依赖发布者手工拷贝(已验证已发布的 0.4.39 里 preview/ 存在,但该拷贝逻辑不在本仓脚本中)。换人发布或 CI 自动发布时,只跑仓库脚本会漏掉 preview/ 与其 wasm/ 产物,发出无法使用 pagx preview 的坏包。
建议:在 pack.sh/prepack 中固化「构建 pagx-preview → 拷贝到 cli/npm/preview/(含 wasm 产物,剔除 devDeps/map)」,并把 preview 产物校验接入 prepublishOnly。
| process.exit(1); | ||
| } | ||
| const { spawn } = require('child_process'); | ||
| const child = spawn('node', [previewEntry, ...process.argv.slice(3)], { stdio: 'inherit' }); |
There was a problem hiding this comment.
[严重] 硬编码 spawn('node') 且缺少 error 事件处理
两个隐患:
- 硬编码
'node':依赖 PATH 中存在node。用户经 nvm/Volta/asdf 切换、Windows 未加入 PATH、或通过打包运行时启动pagx时,node未必可解析。应使用当前解释器process.execPath—— 同 PR 的playground/pagx-preview/src/daemon.js里spawn(process.execPath, ...)正是这样做的,两处不一致。 - 无
child.on('error'):spawn 失败(ENOENT/EACCES)会触发error事件,未监听将抛未捕获异常使进程崩溃(stack trace),而非bin/pagx.js:77那样的友好提示。
建议:
const child = spawn(process.execPath, [previewEntry, ...process.argv.slice(3)], { stdio: 'inherit' });
child.on('error', (err) => { console.error(`pagx: failed to start preview: ${err.message}`); process.exit(1); });
child.on('exit', (code) => process.exit(code != null ? code : 1));| // The write itself will trigger the session's file watcher, which broadcasts a `reload` SSE | ||
| // event that all connected tabs pick up naturally - so this endpoint stops at persistence and | ||
| // leaves refresh to the existing reload path. | ||
| app.put( |
There was a problem hiding this comment.
[安全] 写盘接口 PUT /session/:id/pagx 缺 CSRF 校验(配合全局 CORS *)
POST /sessions 已通过 Sec-Fetch-Site/Origin 校验防跨站(index.js:183-187,很好),但这个会覆盖磁盘文件的写接口没有同类校验;而 index.js:96-105 对所有路由发 Access-Control-Allow-Origin: * 且允许所有方法。
服务器也未校验 Host 头,127.0.0.1 绑定挡不住 DNS rebinding:恶意页面把域名重绑到 127.0.0.1、扫到端口后,对可猜的 session id(文件会话 id = sha1(绝对路径).slice(0,10),低熵)发起 PUT,即可用攻击者字节覆盖用户正在预览的 .pagx。虽为本地工具、前置条件较多,但确实暴露了一个跨站可达的写文件原语。
建议:对 PUT / POST /session/:id/{resources,document} 等写类接口施加与 /sessions 相同的 Sec-Fetch-Site 校验;并增加 Host 头白名单(仅 127.0.0.1/localhost)以防 rebinding。
| { | ||
| name: 'get_document', | ||
| description: | ||
| 'Get a summary of the loaded pagx document: dimensions and animation duration. The summary is uploaded by the client after load; it may be null if the client has not finished loading yet. If it keeps returning "not loaded" for several consecutive calls, the inline widget is likely not rendering in this host and the result will switch to a fallback that returns a browser-openable url (fallbackToWebview: true) — open that url in a webview / browser instead of polling further.', |
There was a problem hiding this comment.
[对外接口] get_document 描述宣称返回 nodeCount,但实际恒为 0
该工具描述与相关注释宣称返回文档节点信息,但客户端上传的 summary.nodeCount 恒为 0 —— 见 static/mcp-widget.js:135 与 src/server/index.js:391 的注释均写明「nodeCount is not exposed by the viewer yet; we send 0」。对外工具描述承诺了未实现的字段,会误导调用它的 LLM(例如据此判断文档复杂度)。
建议:在 viewer 真正暴露节点数之前,从描述中删除节点数相关措辞(只保留 dimensions + duration),或在 viewer 暴露真实值后再宣称。
| // that support MCP Apps. It is the default "open in a webview panel / browser" path. | ||
| }, | ||
| { | ||
| name: 'preview_pagx_widget', |
There was a problem hiding this comment.
[对外接口精简度] preview_pagx_widget 与 preview_pagx 高度重叠,且 widget 在主流宿主均不可用
两个工具入参 schema 完全相同,实现共用 handlePreviewPagx(tools.js:102-107),仅差一个 _meta.ui 和返回文案。而据 PR 描述,inline widget 在 Claude Desktop / CodeBuddy IDE / VS Code Copilot 三大宿主均不可用,PR 也已把它降级为非默认。
为一个「多数宿主跑不起来」的能力长期占用一个独立对外工具位,会增加 LLM 的选择成本与误用概率(需靠冗长 description 反复引导「仅在用户明确要小窗时用」)。
建议:合并为单个 preview_pagx + 可选布尔参数(如 inline),由一处逻辑分派;或在各宿主对 MCP Apps inline widget 支持成熟前先不暴露 widget 工具,仅保留可靠的 webview/browser 路径。
概述
新增
pagx-preview工具,为 PAGX 动画文件提供本地实时预览能力,并作为pagx preview子命令集成进@libpag/pagxCLI。终端用户无需单独安装,直接通过主 CLI 即可使用。工具提供两种使用形态:
.pagx文件。主要改动
新增
playground/pagx-preview/工具stop/--log等参数。.pagx文件变更,通过 SSE 推送到浏览器实时重新渲染。.pagx文件到窗口预览。--mcp):通过 stdio 通信 MCP 协议,自动启动本地 HTTP 服务用于 WASM 渲染,提供preview_pagx、preview_pagx_widget、reload_file、get_document四个工具。~/.pagx/fonts/。集成进
@libpag/pagxCLI(cli/npm/)bin/pagx.js拦截preview子命令,委托给内置的 preview 模块运行;并将preview注入到pagx --help输出。package.json新增@modelcontextprotocol/sdk、chokidar、express依赖,并把preview/目录加入发布文件列表。当前对话内小窗预览(
preview_pagx_widget)的宿主支持并不完善,这是本 PR 的已知限制:内联 widget 已在官方 ext-apps basic-host 参考宿主中验证通过,但各桌面端宿主对 MCP Apps 内联 widget 的支持情况不一:
因此
preview_pagx(在 IDE webview 面板 / 浏览器中打开)被设为默认工具,preview_pagx_widget仅在用户明确要求「内联 / 小窗」预览时才使用。无论哪种宿主,session URL(
http://127.0.0.1:<端口>/session/<id>/)都能提供带实时重载的完整预览作为可靠回退。后续待各宿主对 MCP Apps 内联渲染的支持成熟后,再完善小窗体验。
测试情况
pagx-preview)与主 CLI 子命令(pagx preview)两种入口均验证通过。备注
@libpag/pagx以pagx preview子命令分发,终端用户无需单独安装。playground/pagx-preview/README.zh_CN.md。