Repository navigation
Add CadQuery Viewer: STL/STEP viewer that sends annotated positions to the AI - #18
Conversation
…o the AI A local 3D viewer for MiniMax Code: - Browse a project folder and open .stl / .step / .stp; open files reload on change. - STEP is converted locally (Python + CadQuery/OCP) into a mesh that keeps the assembly: part names, placements, colours, and real B-rep edges. - python/cqview.py pushes CadQuery parts and assemblies into the viewer live. - Pick mode with confirmation: mark several surface points, add notes and an instruction, then send the composed message to the conversation (MCP Apps ui/message when granted, otherwise the MiniMax Code page bridge). npm run check: plugins/hanzijie/cq-viewer OK (0 errors, 0 warnings).
MyPrototypeWhat
left a comment
There was a problem hiding this comment.
感谢贡献,这个 Viewer 做得很完整!看到 PR 已经合并了,我这边补充一些合并后的复查结果,想请你开一个后续 PR 修一下下面两处需要修改的问题(可选建议也欢迎一起处理)。
我在本地用临时 home 跑了 Node 端,也在 headless Chrome 里打开了页面:路径约束很严,../、编码路径、绝对路径、软链、扩展名、home 外和隐藏目录的登记都被正确拒绝;STEP 转换(CadQuery 2.8)能保留装配的零件名和颜色,同一文件的并发请求只转换一次,损坏文件和缺少 CadQuery 时的提示也很清楚;页面没有任何外部请求,vendor 的 three.js r170 与上游一致。npm run check 通过。
需要修改(请在后续 PR 中修复)
~/.cqview的并发写入(server.mjs的writeJsonAtomic/addRoot,详见行内评论):tmp 文件名只用了 pid 和毫秒时间戳,同一毫秒内的两次写入会撞名。实测 40 个并发保存标注时有 20 个返回 500(ENOENT);8 个并发登记文件夹时有 5 个返回 500,roots.json 也丢了条目。docs/security.md要求写 dataDir 之外的文件时串行化并发写入。建议 tmp 名加随机后缀,并让读改写排队执行。- dispose 之后仍会启动 Python(详见行内评论):转换过程中 Host 停止 Mini App 时,dispose 会杀掉 step_mesh.py,但这个失败(
signal_SIGKILL)会让代码走进 fallback,再启动一个新的python -c转换进程。这个进程不在清理范围内,而 Host 不会回收入口进程派生的子进程(docs/runtime.md)。我实测它在 dispose 之后被启动,并且把结果写进了缓存。建议 dispose 后runChild直接拒绝,被信号结束(包括超时)时不再进入 fallback。
建议(可选)
- 服务端没有校验 Host / Origin。带
Host: evil.example和外部 Origin 的POST /api/roots能成功登记目录,通过 DNS rebinding 的网页可以借此读取 home 下的模型文件。建议只接受本机的 Host,非同源的写请求返回 403。 step-cache/只增不减,可以考虑清理旧条目。host-bridge.js里APP_INFO.version是 1.3.0,plugin.json 是 1.0.0,建议统一。
后续 PR 提上来后我会尽快复查,有问题随时交流,谢谢!
| /** @param {string} file @param {unknown} data */ | ||
| async function writeJsonAtomic(file, data) { | ||
| await mkdir(EXCHANGE_DIR, { recursive: true }); | ||
| const tmp = `${file}.${process.pid}.${Date.now()}.tmp`; |
There was a problem hiding this comment.
同一毫秒内的两次写入会使用同一个 tmp,后一次 rename 会 ENOENT。实测 40 个并发 POST /api/selection 有 20 个 500;roots.json 的读改写也会丢条目。建议 tmp 名加 randomUUID(),并把 roots/selection 的读改写放进一个 promise 队列。
| if (code === 'exit_3' || code === 'exit_4') { | ||
| throw new HttpError(422, 'step_convert_failed', code === 'exit_3' ? '这个 STEP 文件读不出来,可能已损坏' : '这个 STEP 文件里没有实体'); | ||
| } | ||
| log.warn('cqviewer.step.mesh_failed', { code: code ?? 'unknown' }); |
There was a problem hiding this comment.
dispose 用 SIGKILL 结束 step_mesh.py 后,这里只记一条日志就继续走 fallback,在 dispose 之后又启动了一个 Python 进程(实测会运行完并写入缓存)。超时也一样,最坏会跑 2×300 s。建议 signal_* 时直接抛错,并在 runChild 里检查 disposed。
|
您好,您的邮件我已经收到,我会尽快回复您,谢谢!
|
… loopback-only routes (#20) Follow-up to the post-merge review of #18. Required: - Concurrent writes: temp files are named <file>.<pid>.<uuid>.tmp (two writes in the same millisecond collided before: 40 concurrent selection saves gave 27x 500), and the selection save joins the existing write queue with roots/scene/request writes. The temp file is removed on failure and takes the existing file's mode (docs/security.md). - dispose during STEP conversion: runChild refuses once disposed, so the fallback can no longer start a Python nobody cleans up. A conversion stopped by the time limit, by dispose, or killed with SIGKILL/SIGTERM fails without the fallback (worst case is now 1x300 s, not 2x). A native crash (SIGSEGV, SIGABRT ...) still tries the simpler fallback, which is what it is for. Watchers are not recreated after dispose, and dispose waits for a write already in progress. Optional suggestions, also done: - Every route (page included) refuses a non-loopback Host and an Origin other than this listener (DNS rebinding could register folders and read models before). - step-cache pruning on the first conversion after start: unused for 30 days, beyond 1 GB (LRU, a hit refreshes mtime), temp files older than a day; only its own files. - host-bridge APP_INFO and MCP serverInfo versions follow plugin.json (test checks). tests/server.test.mjs (9 tests, fake Python for the conversion paths); 19/19 plugin tests. npm run check: plugins/hanzijie/cq-viewer OK (0 errors, 0 warnings). Co-authored-by: HanZijie <zijiehan1998@gmail.com>
What
New Mini App
plugins/hanzijie/cq-viewer— CadQuery Viewer, a local 3D viewer for MiniMax Code..stl/.step/.stpin a registered folder (or the folder passed via/viewer?root=); tick to open; open files reload when they change on disk.dataDir/step-cache/; falls back to a single merged mesh if needed.python/cqview.py→show(part_or_assembly, "name")writes to~/.cqview/scene/and the viewer shows it immediately (assemblies as STEP).ui/messagerequest when the Host grantsmessage; otherwise it falls back towindow.mcode.miniapp.message.append(fills the conversation input). The same data is mirrored to~/.cqview/selection.jsonfor agents/scripts.Checks
npm run check:plugins/hanzijie/cq-viewer: OK— 0 errors, 0 warnings; 55/55 tests pass. (The two existing warnings belong to other packages.)Data & access (summary — full detail in the README)
.stl/.step/.stpunder folders the user registers (inside$HOME, not$HOMEitself,~/Library, or hidden top-level dirs; no symlinks; depth/entry/file caps) and~/.cqview/scene/.dataDironly~/.cqview/roots.jsonand~/.cqview/selection.json, atomically.-c "import cadquery"probe,step_mesh.py <in> <out>, inline fallback); argument arrays, no shell, killed on dispose.Tested environment
3.1.1-preview.4597(Electron 42), macOS 26 (Apple Silicon); Python 3.9, CadQuery 2.5.2 / OCP 7.7.2.?root=folder registration, Host bridge detection.ui/messageagainst a simulated MCP Apps host, the page-bridge fallback against a simulated bridge, copy mode without a Host.Not verified
ui/initializehandshake is not answered, so Send usesmcode.miniapp.message.append; that the text actually lands in the conversation input was not confirmed.Need help on this PR? Tag
@codesmith-botwith what you need.