@xiaobuyu/nodesign 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +22 -22
- package/server/api/assets/notes.js +144 -144
- package/server/api/assets.js +896 -896
- package/server/api/board.js +57 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +464 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1024 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +339 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +155 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +103 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +110 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
- package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +60 -60
- package/server/lib/board-kind-sizes.js +62 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-store.js +640 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +54 -54
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +36 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
- package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
- package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
- package/web/dist/index.html +17 -17
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
|
@@ -1,180 +1,180 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* server/lib/browse-proxy.js — 浏览通道的出网闸,装在**代理层**(2026-08-18)
|
|
3
|
-
*
|
|
4
|
-
* ## 为什么闸从 CDP 挪到代理
|
|
5
|
-
*
|
|
6
|
-
* 第一版把闸装在 CDP 的 `Fetch.requestPaused` 上。它拦得住导航、子资源、跳转每一跳、
|
|
7
|
-
* 跨源 iframe —— 但审查真攻出来四条它**根本看不见**的路:
|
|
8
|
-
*
|
|
9
|
-
* - **WebSocket 握手**(`ws://127.0.0.1:4001` 双向读到了数据)
|
|
10
|
-
* - **`<link rel=prefetch>` / speculation rules**(打到了生产 :4001)
|
|
11
|
-
* - **`navigator.sendBeacon`**
|
|
12
|
-
* - **还没装上闸的弹窗**(`window.open` / `target=_blank`,在那 250ms 窗口里
|
|
13
|
-
* 能发 GET 和 POST,还能循环开)
|
|
14
|
-
*
|
|
15
|
-
* 共同点是它们都不走 `Fetch` 那个阶段。**逐条补是补不完的** —— 下一个 web 平台特性
|
|
16
|
-
* 又会开一条。所以闸挪到**所有出网都必经的那一层**:给这个浏览器挂一个我们自己的
|
|
17
|
-
* HTTP 代理,`--proxy-server` 之后 chromium 的每一次连接都从这儿走。
|
|
18
|
-
* 实测代理确实看得到上面四条全部(包括 WS 的 `CONNECT 127.0.0.1:4001`)。
|
|
19
|
-
*
|
|
20
|
-
* ## 附带白拿的两件
|
|
21
|
-
*
|
|
22
|
-
* 1. ⭐ **DNS 重绑定被根除,不再是"缓解"。** 我们自己解析、自己**连那个验过的 IP**
|
|
23
|
-
* (pin),浏览器拿不到第二次解析的机会。CDP 那版只能是"解析一次→浏览器再解析
|
|
24
|
-
* 一次",中间有 TOCTOU 窗口,而且主机名缓存把窗口变成了保证 30 秒的洞。
|
|
25
|
-
* 2. **没有初始化竞态。** 代理在浏览器启动参数里,第一个字节之前就生效;
|
|
26
|
-
* CDP 那版要等 `Fetch.enable` 握手完,弹窗根本等不到。
|
|
27
|
-
*
|
|
28
|
-
* CDP 那道闸**保留**当纵深(它能给出漂亮的按请求记账),但不再是唯一一道。
|
|
29
|
-
*
|
|
30
|
-
* ## 感知通道不挂代理
|
|
31
|
-
*
|
|
32
|
-
* 感知通道(看自己产物)**故意**走 loopback 的 artifact-file,它必须能到 127.0.0.1。
|
|
33
|
-
* 所以只有浏览通道的浏览器带 `--proxy-server`,两条通道在这一层物理分开。
|
|
34
|
-
*/
|
|
35
|
-
|
|
36
|
-
import http from 'node:http';
|
|
37
|
-
import net from 'node:net';
|
|
38
|
-
import dns from 'node:dns/promises';
|
|
39
|
-
import { blockReason } from './ssrf-guard.js';
|
|
40
|
-
|
|
41
|
-
/** 允许连的端口。80/443 之外的公网端口极少是设计参考站,而放开等于多一片攻击面。 */
|
|
42
|
-
const ALLOWED_PORTS = new Set([80, 443, 8080, 8443]);
|
|
43
|
-
const RESOLVE_TIMEOUT_MS = 4000;
|
|
44
|
-
const CONNECT_TIMEOUT_MS = 15_000;
|
|
45
|
-
|
|
46
|
-
let server = null;
|
|
47
|
-
let port = 0;
|
|
48
|
-
const blocked = [];
|
|
49
|
-
|
|
50
|
-
function note(target, reason) {
|
|
51
|
-
blocked.push({ target, reason, at: Date.now() });
|
|
52
|
-
if (blocked.length > 200) blocked.shift();
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* 解析 + 判定,返回可以连的那个 IP(**pin 住它**,不把主机名交给下游再解析一次)。
|
|
57
|
-
* @returns {Promise<{ip: string, family: number} | {deny: string}>}
|
|
58
|
-
*/
|
|
59
|
-
async function resolveAllowed(host, portNum) {
|
|
60
|
-
if (!ALLOWED_PORTS.has(portNum)) {
|
|
61
|
-
return { deny: `port ${portNum} is not allowed (only ${[...ALLOWED_PORTS].join('/')})` };
|
|
62
|
-
}
|
|
63
|
-
const bare = host.toLowerCase().replace(/^\[|\]$/g, '');
|
|
64
|
-
if (bare === 'localhost' || bare.endsWith('.localhost') || bare.endsWith('.local')) {
|
|
65
|
-
return { deny: `${bare} resolves inside the local network` };
|
|
66
|
-
}
|
|
67
|
-
// 字面量 IP 不劳烦 DNS
|
|
68
|
-
if (/^\d+\.\d+\.\d+\.\d+$/.test(bare) || bare.includes(':')) {
|
|
69
|
-
const why = blockReason(bare);
|
|
70
|
-
return why ? { deny: why } : { ip: bare, family: bare.includes(':') ? 6 : 4 };
|
|
71
|
-
}
|
|
72
|
-
let addrs;
|
|
73
|
-
try {
|
|
74
|
-
addrs = await Promise.race([
|
|
75
|
-
dns.lookup(bare, { all: true, verbatim: true }),
|
|
76
|
-
new Promise((_, rej) => setTimeout(() => rej(new Error('dns timeout')), RESOLVE_TIMEOUT_MS)),
|
|
77
|
-
]);
|
|
78
|
-
} catch (err) {
|
|
79
|
-
// ⚠️ kind:'dns' 是给 denyBody 分文案用的:解析失败**不是策略拦截**。
|
|
80
|
-
// 第一版对 NXDOMAIN 也甩「内网与本机地址是硬边界」,agent 上报时据此
|
|
81
|
-
// 推理出一整套"代理有两个解析器"的错误理论 —— 错误提示比没提示更贵。
|
|
82
|
-
return { deny: `cannot resolve ${bare}: ${err.message}`, kind: 'dns' };
|
|
83
|
-
}
|
|
84
|
-
if (!addrs.length) return { deny: `${bare} resolved to nothing`, kind: 'dns' };
|
|
85
|
-
// **任一**地址落在禁止段就整个拒 —— 多 A 记录里混一条内网是标准起手式,
|
|
86
|
-
// "挑一个能用的"等于自己开门
|
|
87
|
-
for (const a of addrs) {
|
|
88
|
-
const why = blockReason(a.address);
|
|
89
|
-
if (why) return { deny: `${bare} → ${why}` };
|
|
90
|
-
}
|
|
91
|
-
return { ip: addrs[0].address, family: addrs[0].family };
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 按拒因分文案:DNS 解析失败要说清"这个域名现在指不到任何地方",
|
|
95
|
-
// 策略拦截才配那句硬边界 —— 两种拒混着说,agent 会朝错误的方向排障。
|
|
96
|
-
const denyTail = (kind) => (kind === 'dns'
|
|
97
|
-
? '这是域名解析失败(域名可能不存在、拼错或已下线),不是出网策略拦截。'
|
|
98
|
-
: '内网与本机地址是硬边界,不是可配置项。');
|
|
99
|
-
|
|
100
|
-
const denyBody = (reason, kind) => {
|
|
101
|
-
const body = `拒绝出网:${reason}\n\n${denyTail(kind)}`;
|
|
102
|
-
return `HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain; charset=utf-8\r\n`
|
|
103
|
-
+ `Content-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
/** 启动(幂等)。@returns {Promise<{port:number, blocked:Array}>} */
|
|
107
|
-
export async function startBrowseProxy() {
|
|
108
|
-
if (server) return { port, blocked };
|
|
109
|
-
|
|
110
|
-
server = http.createServer();
|
|
111
|
-
|
|
112
|
-
// 明文 http:绝对形式的请求行(`GET http://host/path`)
|
|
113
|
-
server.on('request', async (req, res) => {
|
|
114
|
-
let u;
|
|
115
|
-
try { u = new URL(req.url); } catch {
|
|
116
|
-
res.writeHead(400); return res.end('proxy: bad absolute-form request');
|
|
117
|
-
}
|
|
118
|
-
const p = Number(u.port || (u.protocol === 'https:' ? 443 : 80));
|
|
119
|
-
const verdict = await resolveAllowed(u.hostname, p);
|
|
120
|
-
if (verdict.deny) {
|
|
121
|
-
note(`${u.hostname}:${p}`, verdict.deny);
|
|
122
|
-
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
123
|
-
return res.end(`拒绝出网:${verdict.deny}\n\n${denyTail(verdict.kind)}`);
|
|
124
|
-
}
|
|
125
|
-
// 连**验过的那个 IP**,Host 头保留原主机名(虚拟主机才认得出)
|
|
126
|
-
const upstream = http.request({
|
|
127
|
-
host: verdict.ip, port: p, method: req.method,
|
|
128
|
-
path: u.pathname + u.search,
|
|
129
|
-
headers: { ...req.headers, host: u.host },
|
|
130
|
-
timeout: CONNECT_TIMEOUT_MS,
|
|
131
|
-
setHost: false,
|
|
132
|
-
}, (up) => {
|
|
133
|
-
res.writeHead(up.statusCode || 502, up.headers);
|
|
134
|
-
up.pipe(res);
|
|
135
|
-
});
|
|
136
|
-
upstream.on('error', (err) => {
|
|
137
|
-
if (!res.headersSent) res.writeHead(502, { 'Content-Type': 'text/plain' });
|
|
138
|
-
res.end(`proxy upstream error: ${err.message}`);
|
|
139
|
-
});
|
|
140
|
-
req.pipe(upstream);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
// CONNECT:https / wss 都走这条(**WS 就是靠它被盖住的**)
|
|
144
|
-
server.on('connect', async (req, socket, head) => {
|
|
145
|
-
const [rawHost, rawPort] = String(req.url || '').split(':');
|
|
146
|
-
const p = Number(rawPort || 443);
|
|
147
|
-
const verdict = await resolveAllowed(rawHost || '', p);
|
|
148
|
-
if (verdict.deny) {
|
|
149
|
-
note(`${rawHost}:${p}`, verdict.deny);
|
|
150
|
-
socket.write(denyBody(verdict.deny, verdict.kind));
|
|
151
|
-
return socket.destroy();
|
|
152
|
-
}
|
|
153
|
-
const up = net.connect({ host: verdict.ip, port: p }, () => {
|
|
154
|
-
socket.write('HTTP/1.1 200 Connection Established\r\nProxy-Agent: nodesign-browse\r\n\r\n');
|
|
155
|
-
if (head?.length) up.write(head);
|
|
156
|
-
socket.pipe(up); up.pipe(socket);
|
|
157
|
-
});
|
|
158
|
-
up.setTimeout(CONNECT_TIMEOUT_MS, () => up.destroy());
|
|
159
|
-
up.on('error', () => { try { socket.destroy(); } catch { /* */ } });
|
|
160
|
-
socket.on('error', () => { try { up.destroy(); } catch { /* */ } });
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
await new Promise((resolve, reject) => {
|
|
164
|
-
server.once('error', reject);
|
|
165
|
-
server.listen(0, '127.0.0.1', resolve);
|
|
166
|
-
});
|
|
167
|
-
port = server.address().port;
|
|
168
|
-
server.unref?.();
|
|
169
|
-
console.log(`[browse-proxy] listening on 127.0.0.1:${port}`);
|
|
170
|
-
return { port, blocked };
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/** 给工具的返回值用:这次调用期间拦了什么 */
|
|
174
|
-
export function blockedSince(n) {
|
|
175
|
-
return blocked.slice(n);
|
|
176
|
-
}
|
|
177
|
-
export function blockedCount() {
|
|
178
|
-
return blocked.length;
|
|
179
|
-
}
|
|
180
|
-
export const _proxy = { ALLOWED_PORTS };
|
|
1
|
+
/**
|
|
2
|
+
* server/lib/browse-proxy.js — 浏览通道的出网闸,装在**代理层**(2026-08-18)
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么闸从 CDP 挪到代理
|
|
5
|
+
*
|
|
6
|
+
* 第一版把闸装在 CDP 的 `Fetch.requestPaused` 上。它拦得住导航、子资源、跳转每一跳、
|
|
7
|
+
* 跨源 iframe —— 但审查真攻出来四条它**根本看不见**的路:
|
|
8
|
+
*
|
|
9
|
+
* - **WebSocket 握手**(`ws://127.0.0.1:4001` 双向读到了数据)
|
|
10
|
+
* - **`<link rel=prefetch>` / speculation rules**(打到了生产 :4001)
|
|
11
|
+
* - **`navigator.sendBeacon`**
|
|
12
|
+
* - **还没装上闸的弹窗**(`window.open` / `target=_blank`,在那 250ms 窗口里
|
|
13
|
+
* 能发 GET 和 POST,还能循环开)
|
|
14
|
+
*
|
|
15
|
+
* 共同点是它们都不走 `Fetch` 那个阶段。**逐条补是补不完的** —— 下一个 web 平台特性
|
|
16
|
+
* 又会开一条。所以闸挪到**所有出网都必经的那一层**:给这个浏览器挂一个我们自己的
|
|
17
|
+
* HTTP 代理,`--proxy-server` 之后 chromium 的每一次连接都从这儿走。
|
|
18
|
+
* 实测代理确实看得到上面四条全部(包括 WS 的 `CONNECT 127.0.0.1:4001`)。
|
|
19
|
+
*
|
|
20
|
+
* ## 附带白拿的两件
|
|
21
|
+
*
|
|
22
|
+
* 1. ⭐ **DNS 重绑定被根除,不再是"缓解"。** 我们自己解析、自己**连那个验过的 IP**
|
|
23
|
+
* (pin),浏览器拿不到第二次解析的机会。CDP 那版只能是"解析一次→浏览器再解析
|
|
24
|
+
* 一次",中间有 TOCTOU 窗口,而且主机名缓存把窗口变成了保证 30 秒的洞。
|
|
25
|
+
* 2. **没有初始化竞态。** 代理在浏览器启动参数里,第一个字节之前就生效;
|
|
26
|
+
* CDP 那版要等 `Fetch.enable` 握手完,弹窗根本等不到。
|
|
27
|
+
*
|
|
28
|
+
* CDP 那道闸**保留**当纵深(它能给出漂亮的按请求记账),但不再是唯一一道。
|
|
29
|
+
*
|
|
30
|
+
* ## 感知通道不挂代理
|
|
31
|
+
*
|
|
32
|
+
* 感知通道(看自己产物)**故意**走 loopback 的 artifact-file,它必须能到 127.0.0.1。
|
|
33
|
+
* 所以只有浏览通道的浏览器带 `--proxy-server`,两条通道在这一层物理分开。
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
import http from 'node:http';
|
|
37
|
+
import net from 'node:net';
|
|
38
|
+
import dns from 'node:dns/promises';
|
|
39
|
+
import { blockReason } from './ssrf-guard.js';
|
|
40
|
+
|
|
41
|
+
/** 允许连的端口。80/443 之外的公网端口极少是设计参考站,而放开等于多一片攻击面。 */
|
|
42
|
+
const ALLOWED_PORTS = new Set([80, 443, 8080, 8443]);
|
|
43
|
+
const RESOLVE_TIMEOUT_MS = 4000;
|
|
44
|
+
const CONNECT_TIMEOUT_MS = 15_000;
|
|
45
|
+
|
|
46
|
+
let server = null;
|
|
47
|
+
let port = 0;
|
|
48
|
+
const blocked = [];
|
|
49
|
+
|
|
50
|
+
function note(target, reason) {
|
|
51
|
+
blocked.push({ target, reason, at: Date.now() });
|
|
52
|
+
if (blocked.length > 200) blocked.shift();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 解析 + 判定,返回可以连的那个 IP(**pin 住它**,不把主机名交给下游再解析一次)。
|
|
57
|
+
* @returns {Promise<{ip: string, family: number} | {deny: string}>}
|
|
58
|
+
*/
|
|
59
|
+
async function resolveAllowed(host, portNum) {
|
|
60
|
+
if (!ALLOWED_PORTS.has(portNum)) {
|
|
61
|
+
return { deny: `port ${portNum} is not allowed (only ${[...ALLOWED_PORTS].join('/')})` };
|
|
62
|
+
}
|
|
63
|
+
const bare = host.toLowerCase().replace(/^\[|\]$/g, '');
|
|
64
|
+
if (bare === 'localhost' || bare.endsWith('.localhost') || bare.endsWith('.local')) {
|
|
65
|
+
return { deny: `${bare} resolves inside the local network` };
|
|
66
|
+
}
|
|
67
|
+
// 字面量 IP 不劳烦 DNS
|
|
68
|
+
if (/^\d+\.\d+\.\d+\.\d+$/.test(bare) || bare.includes(':')) {
|
|
69
|
+
const why = blockReason(bare);
|
|
70
|
+
return why ? { deny: why } : { ip: bare, family: bare.includes(':') ? 6 : 4 };
|
|
71
|
+
}
|
|
72
|
+
let addrs;
|
|
73
|
+
try {
|
|
74
|
+
addrs = await Promise.race([
|
|
75
|
+
dns.lookup(bare, { all: true, verbatim: true }),
|
|
76
|
+
new Promise((_, rej) => setTimeout(() => rej(new Error('dns timeout')), RESOLVE_TIMEOUT_MS)),
|
|
77
|
+
]);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
// ⚠️ kind:'dns' 是给 denyBody 分文案用的:解析失败**不是策略拦截**。
|
|
80
|
+
// 第一版对 NXDOMAIN 也甩「内网与本机地址是硬边界」,agent 上报时据此
|
|
81
|
+
// 推理出一整套"代理有两个解析器"的错误理论 —— 错误提示比没提示更贵。
|
|
82
|
+
return { deny: `cannot resolve ${bare}: ${err.message}`, kind: 'dns' };
|
|
83
|
+
}
|
|
84
|
+
if (!addrs.length) return { deny: `${bare} resolved to nothing`, kind: 'dns' };
|
|
85
|
+
// **任一**地址落在禁止段就整个拒 —— 多 A 记录里混一条内网是标准起手式,
|
|
86
|
+
// "挑一个能用的"等于自己开门
|
|
87
|
+
for (const a of addrs) {
|
|
88
|
+
const why = blockReason(a.address);
|
|
89
|
+
if (why) return { deny: `${bare} → ${why}` };
|
|
90
|
+
}
|
|
91
|
+
return { ip: addrs[0].address, family: addrs[0].family };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 按拒因分文案:DNS 解析失败要说清"这个域名现在指不到任何地方",
|
|
95
|
+
// 策略拦截才配那句硬边界 —— 两种拒混着说,agent 会朝错误的方向排障。
|
|
96
|
+
const denyTail = (kind) => (kind === 'dns'
|
|
97
|
+
? '这是域名解析失败(域名可能不存在、拼错或已下线),不是出网策略拦截。'
|
|
98
|
+
: '内网与本机地址是硬边界,不是可配置项。');
|
|
99
|
+
|
|
100
|
+
const denyBody = (reason, kind) => {
|
|
101
|
+
const body = `拒绝出网:${reason}\n\n${denyTail(kind)}`;
|
|
102
|
+
return `HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain; charset=utf-8\r\n`
|
|
103
|
+
+ `Content-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`;
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
/** 启动(幂等)。@returns {Promise<{port:number, blocked:Array}>} */
|
|
107
|
+
export async function startBrowseProxy() {
|
|
108
|
+
if (server) return { port, blocked };
|
|
109
|
+
|
|
110
|
+
server = http.createServer();
|
|
111
|
+
|
|
112
|
+
// 明文 http:绝对形式的请求行(`GET http://host/path`)
|
|
113
|
+
server.on('request', async (req, res) => {
|
|
114
|
+
let u;
|
|
115
|
+
try { u = new URL(req.url); } catch {
|
|
116
|
+
res.writeHead(400); return res.end('proxy: bad absolute-form request');
|
|
117
|
+
}
|
|
118
|
+
const p = Number(u.port || (u.protocol === 'https:' ? 443 : 80));
|
|
119
|
+
const verdict = await resolveAllowed(u.hostname, p);
|
|
120
|
+
if (verdict.deny) {
|
|
121
|
+
note(`${u.hostname}:${p}`, verdict.deny);
|
|
122
|
+
res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
|
|
123
|
+
return res.end(`拒绝出网:${verdict.deny}\n\n${denyTail(verdict.kind)}`);
|
|
124
|
+
}
|
|
125
|
+
// 连**验过的那个 IP**,Host 头保留原主机名(虚拟主机才认得出)
|
|
126
|
+
const upstream = http.request({
|
|
127
|
+
host: verdict.ip, port: p, method: req.method,
|
|
128
|
+
path: u.pathname + u.search,
|
|
129
|
+
headers: { ...req.headers, host: u.host },
|
|
130
|
+
timeout: CONNECT_TIMEOUT_MS,
|
|
131
|
+
setHost: false,
|
|
132
|
+
}, (up) => {
|
|
133
|
+
res.writeHead(up.statusCode || 502, up.headers);
|
|
134
|
+
up.pipe(res);
|
|
135
|
+
});
|
|
136
|
+
upstream.on('error', (err) => {
|
|
137
|
+
if (!res.headersSent) res.writeHead(502, { 'Content-Type': 'text/plain' });
|
|
138
|
+
res.end(`proxy upstream error: ${err.message}`);
|
|
139
|
+
});
|
|
140
|
+
req.pipe(upstream);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// CONNECT:https / wss 都走这条(**WS 就是靠它被盖住的**)
|
|
144
|
+
server.on('connect', async (req, socket, head) => {
|
|
145
|
+
const [rawHost, rawPort] = String(req.url || '').split(':');
|
|
146
|
+
const p = Number(rawPort || 443);
|
|
147
|
+
const verdict = await resolveAllowed(rawHost || '', p);
|
|
148
|
+
if (verdict.deny) {
|
|
149
|
+
note(`${rawHost}:${p}`, verdict.deny);
|
|
150
|
+
socket.write(denyBody(verdict.deny, verdict.kind));
|
|
151
|
+
return socket.destroy();
|
|
152
|
+
}
|
|
153
|
+
const up = net.connect({ host: verdict.ip, port: p }, () => {
|
|
154
|
+
socket.write('HTTP/1.1 200 Connection Established\r\nProxy-Agent: nodesign-browse\r\n\r\n');
|
|
155
|
+
if (head?.length) up.write(head);
|
|
156
|
+
socket.pipe(up); up.pipe(socket);
|
|
157
|
+
});
|
|
158
|
+
up.setTimeout(CONNECT_TIMEOUT_MS, () => up.destroy());
|
|
159
|
+
up.on('error', () => { try { socket.destroy(); } catch { /* */ } });
|
|
160
|
+
socket.on('error', () => { try { up.destroy(); } catch { /* */ } });
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
await new Promise((resolve, reject) => {
|
|
164
|
+
server.once('error', reject);
|
|
165
|
+
server.listen(0, '127.0.0.1', resolve);
|
|
166
|
+
});
|
|
167
|
+
port = server.address().port;
|
|
168
|
+
server.unref?.();
|
|
169
|
+
console.log(`[browse-proxy] listening on 127.0.0.1:${port}`);
|
|
170
|
+
return { port, blocked };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** 给工具的返回值用:这次调用期间拦了什么 */
|
|
174
|
+
export function blockedSince(n) {
|
|
175
|
+
return blocked.slice(n);
|
|
176
|
+
}
|
|
177
|
+
export function blockedCount() {
|
|
178
|
+
return blocked.length;
|
|
179
|
+
}
|
|
180
|
+
export const _proxy = { ALLOWED_PORTS };
|
package/server/lib/canvas-id.js
CHANGED
|
@@ -1,36 +1,36 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* canvas-id —— agent 侧画布 id 归一化(2026-08-14,摆位批)
|
|
3
|
-
*
|
|
4
|
-
* id = kind 前缀 + 工作区相对路径(board 的铁律)。agent 传进来的写法五花八门
|
|
5
|
-
* (反斜杠 / ./ 前缀 / 裸 .html),read_board / arrange_on_board / create_on_board
|
|
6
|
-
* 共用这一份归一。跟 pin_to_board 内联的那段同源同规则 —— 收敛计划里它也该
|
|
7
|
-
* 迁过来(现在没动它:改稳定工具要单独一刀)。
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
export function normalizeCanvasId(raw) {
|
|
11
|
-
let id = String(raw || '').trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
|
|
12
|
-
if (!id || id.includes('..')) return null;
|
|
13
|
-
if (id.endsWith('agent-memory/brand/memory.md')) return 'doc:brand';
|
|
14
|
-
if (id.endsWith('agent-memory/memory.md')) return 'doc:_root';
|
|
15
|
-
if (!/^(deck|site|doc|text|scribble):/.test(id) && /\.html?$/i.test(id)) return `deck:${id}`;
|
|
16
|
-
return id;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* 它住在哪一层(服务端近似版):显式 zone 字段优先,其次沿路径往上找第一个
|
|
21
|
-
* 已知文件夹。已知集 = board.zones 的 key —— 比前端少了"服务端扫出来的任务
|
|
22
|
-
* 目录"这一路,没摆过的深层目录会归到根。read_board 的输出里写明这是近似。
|
|
23
|
-
*/
|
|
24
|
-
export function layerOf(id, entry, knownFolders) {
|
|
25
|
-
if (entry && typeof entry.zone === 'string') return entry.zone;
|
|
26
|
-
const s = String(id);
|
|
27
|
-
if (s.startsWith('doc:')) return '';
|
|
28
|
-
const c = s.indexOf(':');
|
|
29
|
-
const p = (c > 0 && /^[a-z]+$/.test(s.slice(0, c))) ? s.slice(c + 1) : s;
|
|
30
|
-
let d = p.includes('/') ? p.slice(0, p.lastIndexOf('/')) : '';
|
|
31
|
-
while (d && !knownFolders.has(d)) {
|
|
32
|
-
const i = d.lastIndexOf('/');
|
|
33
|
-
d = i > 0 ? d.slice(0, i) : '';
|
|
34
|
-
}
|
|
35
|
-
return d;
|
|
36
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* canvas-id —— agent 侧画布 id 归一化(2026-08-14,摆位批)
|
|
3
|
+
*
|
|
4
|
+
* id = kind 前缀 + 工作区相对路径(board 的铁律)。agent 传进来的写法五花八门
|
|
5
|
+
* (反斜杠 / ./ 前缀 / 裸 .html),read_board / arrange_on_board / create_on_board
|
|
6
|
+
* 共用这一份归一。跟 pin_to_board 内联的那段同源同规则 —— 收敛计划里它也该
|
|
7
|
+
* 迁过来(现在没动它:改稳定工具要单独一刀)。
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export function normalizeCanvasId(raw) {
|
|
11
|
+
let id = String(raw || '').trim().replace(/\\/g, '/').replace(/^\.\//, '').replace(/^\/+|\/+$/g, '');
|
|
12
|
+
if (!id || id.includes('..')) return null;
|
|
13
|
+
if (id.endsWith('agent-memory/brand/memory.md')) return 'doc:brand';
|
|
14
|
+
if (id.endsWith('agent-memory/memory.md')) return 'doc:_root';
|
|
15
|
+
if (!/^(deck|site|doc|text|scribble):/.test(id) && /\.html?$/i.test(id)) return `deck:${id}`;
|
|
16
|
+
return id;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* 它住在哪一层(服务端近似版):显式 zone 字段优先,其次沿路径往上找第一个
|
|
21
|
+
* 已知文件夹。已知集 = board.zones 的 key —— 比前端少了"服务端扫出来的任务
|
|
22
|
+
* 目录"这一路,没摆过的深层目录会归到根。read_board 的输出里写明这是近似。
|
|
23
|
+
*/
|
|
24
|
+
export function layerOf(id, entry, knownFolders) {
|
|
25
|
+
if (entry && typeof entry.zone === 'string') return entry.zone;
|
|
26
|
+
const s = String(id);
|
|
27
|
+
if (s.startsWith('doc:')) return '';
|
|
28
|
+
const c = s.indexOf(':');
|
|
29
|
+
const p = (c > 0 && /^[a-z]+$/.test(s.slice(0, c))) ? s.slice(c + 1) : s;
|
|
30
|
+
let d = p.includes('/') ? p.slice(0, p.lastIndexOf('/')) : '';
|
|
31
|
+
while (d && !knownFolders.has(d)) {
|
|
32
|
+
const i = d.lastIndexOf('/');
|
|
33
|
+
d = i > 0 ? d.slice(0, i) : '';
|
|
34
|
+
}
|
|
35
|
+
return d;
|
|
36
|
+
}
|