dsh-design-studio 0.4.1 → 0.6.0
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.en.md +19 -7
- package/README.md +46 -11
- package/cordis.patch.yml +1 -1
- package/lib/browser.js +273 -0
- package/lib/bundle.js +228 -0
- package/lib/client.js +1442 -47
- package/lib/core.js +263 -25
- package/lib/doc.js +356 -0
- package/lib/handoff.js +555 -0
- package/lib/history.js +1 -1
- package/lib/index.js +683 -24
- package/lib/look.js +550 -0
- package/lib/picker.js +1153 -24
- package/lib/serve.js +25 -8
- package/lib/system.js +1360 -0
- package/lib/tool.js +9 -3
- package/lib/tweaks.js +362 -0
- package/locale/tool/en.json +2 -2
- package/locale/tool/zh.json +2 -2
- package/package.json +1 -1
- package/skills/ui-design/SKILL.md +67 -3
- package/skills/ui-design/references/app.md +1 -1
package/README.en.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
|
-
> **What it is:** a DeepSeek Harness (DSH) plugin that shows the model's web pages, app screens, posters and slides live on a canvas in the right sidebar, in phone or browser frames. Click an element or drag a region, add a sentence, and the model edits it. Versions are saved every turn; export showcase images or preview on a phone.
|
|
18
|
+
> **What it is:** a DeepSeek Harness (DSH) plugin that shows the model's web pages, app screens, posters and slides live on a canvas in the right sidebar, in phone or browser frames. Click an element or drag a region, add a sentence, and the model edits it. Versions are saved every turn; export showcase images, PDF or PowerPoint, or preview on a phone. A one-click design check finds overflowing text, low contrast and small tap targets.
|
|
19
19
|
>
|
|
20
20
|
> **Use it for:** "make a landing page", "design an app screen", "make a poster", "make a few slides".
|
|
21
21
|
>
|
|
@@ -48,25 +48,30 @@ For development, link a local checkout with `dsh plugin --profile desktop add li
|
|
|
48
48
|
1. **Opens by itself, but only for design work.** Design work means the model loaded the design skill, called `preview_page`, or wrote a page with a design-type tag (`<meta name="dsh-design">`). After that, the tab pops up when the model writes an `.html` file or starts a dev server (`npm run dev` and the like). HTML written by other tasks doesn't pop up. Instead, a blue dot appears on the header button, and clicking it previews the file. Ports the model only probes with `curl` or the browser tools are never opened.
|
|
49
49
|
2. **Frames by design type.** Supported types are web (browser window), app (phone with Dynamic Island), tablet, poster (1080×1350), slides (16:9) and email. The model names the type in the tool call, or the page declares it with `<meta name="dsh-design" content="app">`. You can add a size, as in `poster 1080x1920`. The toolbar's type menu switches it at any time.
|
|
50
50
|
3. **Shows several screens on one canvas.** HTML files in the same folder line up in creation order. Pan with two fingers and zoom with a pinch or ⌘ + scroll. Click a screen to select it and double-click to view it alone.
|
|
51
|
-
4. **Lets you watch it build.** A placeholder appears while the model works. Each new screen fades in from top to bottom with a scan line. While more screens are coming, a "generating…" card waits at the end of the canvas.
|
|
51
|
+
4. **Lets you watch it build.** A placeholder appears while the model works. Each new screen fades in from top to bottom with a scan line. While more screens are coming, a "generating…" card waits at the end of the canvas. When the model asks you something with a question card, the spinner turns into an orange dot reading "The model is waiting for your answer".
|
|
52
52
|
5. **Refreshes live without a white flash.** The next frame loads in the background and cross-fades in, keeping the scroll position. Dev servers keep their own hot reload; Vite and Next WebSockets pass through, including dev servers that only listen on IPv6 `::1` (recent Vite on macOS).
|
|
53
53
|
6. **Highlights what changed.** Changed and new elements glow briefly, and the preview scrolls to them if they are off screen. Shared CSS changes refresh every screen. A change to one screen's HTML leaves the others alone.
|
|
54
54
|
7. **Picks elements for the chat.** Hover outlines elements with their size. Click picks one element. Drag picks a region along with up to 10 main elements in it. The pick becomes a block in the chat input with the file, the CSS path, the HTML and key styles.
|
|
55
|
-
8. **Ships a built-in design guide.** The DSH skill `ui-design` loads before the model designs. It sets the type and canvas, writes `tokens.css` first and uses one file per screen. It covers spacing, type scale, color, contrast, Chinese typography, an "AI-look" blacklist and a self-check list, with notes for each design type.
|
|
55
|
+
8. **Ships a built-in design guide.** The DSH skill `ui-design` loads before the model designs. It sets the type and canvas, writes `tokens.css` first and uses one file per screen. It covers spacing, type scale, color, contrast, Chinese typography, an "AI-look" blacklist and a self-check list, with notes for each design type. For a brand-new design the model first asks two or three key questions, then lays out three directions for the first screen (`home-a/b/c.html`, each with its own colors and fonts) side by side; you pick one and it builds the rest, moving the others to `drafts/`. If you already chose a feel in the questions, the three directions all stay in that feel and differ in layout and emphasis. Say "just build it", or give a specific brief, to skip this.
|
|
56
56
|
9. **Has a design tokens panel.** It lists the `:root` variables grouped by color, font, size, spacing, radius and shadow, with swatches. Click one to send it to the chat, say "make it forest green", and every screen updates together.
|
|
57
|
-
10. **Exports
|
|
57
|
+
10. **Exports.** You can export one screen framed or plain, a full-page capture, or a **showcase** of all screens. The showcase uses the brand name as its title and takes its background and accent from your tokens. You can also export every screen as a **PDF** (one page per screen; web and email pages are full length, and page size follows the canvas) or a **PowerPoint** file (one slide per screen as an image, with the slide shape following the canvas; opens in PowerPoint, WPS and Keynote). Links between screens still work: in the PDF a button jumps to its page (and every screen has a bookmark), and in a slideshow it jumps to its slide, so the export is a clickable prototype. Files go to `exports/` in the design folder, and images are also copied to the clipboard.
|
|
58
58
|
11. **Keeps versions.** A version is saved before and after every model turn, named after what you asked (a picked element becomes a short name such as “「3.2 km」 · make it green”, not a selector dump). Open any version, drag the slider to **compare** it with now, or **restore** it. The current state is saved first, so restoring is always reversible.
|
|
59
59
|
12. **Switches to dark mode.** If the page has dark styles (`prefers-color-scheme: dark`, `[data-theme="dark"]` or `.dark`), a moon button toggles them, and exports follow.
|
|
60
|
-
13. **Makes two more versions.** Select a screen and ask for two alternative directions. They appear right next to the original on the canvas
|
|
60
|
+
13. **Makes two more versions.** Select a screen and ask for two alternative directions. They appear right next to the original on the canvas (the three starting directions `home-a/b/c` too, kept in a, b, c order). Click the check mark on the one you want: the chat input gets "use this one, move the other takes to `drafts/`, build the rest", ready to send.
|
|
61
61
|
14. **Views on your phone.** The scan button shows a QR code in the sidebar. Scan it with a phone on the same Wi-Fi and the design opens in the phone's browser at real size, fully tappable. The phone follows the screen you pick on the computer and refreshes when the model changes the design. The panel shows when the phone has opened it, lets you copy the link and stop sharing, and closes the link after 30 minutes idle.
|
|
62
62
|
15. **Works as a clickable prototype.** Screens link to each other with `<a href="detail.html">` or `data-href` (the built-in skill asks the model to do this). Select a screen on the canvas to see arrows for where it leads and what leads to it. Arrows that would cross another screen route around it, above the titles. The play button starts a demo: tap a button in the preview and the linked screen slides in, from the right going forward and from the left going back. The banner has back, restart and exit (Esc).
|
|
63
63
|
16. **Shows one screen at several sizes.** The third view button lays the current screen out at several widths: phone, tablet and desktop for web; small, standard and large for apps; three sizes for tablets. Cramped small screens show at a glance. Picking works there too and notes the width.
|
|
64
64
|
17. **Edits text in place.** Turn on the text button and click any text in the preview to edit it. Enter saves and Esc cancels. Local HTML is written back to the source file, only that one spot, with a version saved before and after. For dev-server pages, or when the text isn't in the source, a "change X to Y" request goes into the chat input for the model.
|
|
65
65
|
18. **Takes a reference image.** Drop an image onto the sidebar, paste it there, or use "Restyle from a reference…" in the tokens panel. The image is saved to `refs/` in the design folder (or the plugin cache when there is none). Its main colors with their shares, any small vivid accent, and whether it reads light or dark go into the chat input with the file path and an instruction to update `tokens.css` first.
|
|
66
|
+
19. **Checks the design.** The stethoscope button measures the rendered pages: text running off the screen, clipped, overflowing its box or overlapping; text contrast below WCAG (3:1 for large text, 4.5:1 for body); tap targets below 44 px in apps or 24 px on the web; tiny text; images that failed to load or are stretched; lines that are too long on web, email and tablet (about 45 Chinese characters or 90 letters); short labels and prices squeezed onto two lines; and too many font sizes. Every screen on the canvas is checked, and numbered markers appear on the problems in the preview (red for serious ones). Click an item to scroll to it. "Ask the model to fix" puts the list (screen, element, problem, by how much) into the chat input. After every model turn the sidebar measures again by itself: an open panel refreshes, and a closed one shows the number of remaining problems on its button (no number means all clear). Clicking an item brings its screen into view when the panel covers it or it is off the canvas. The model can run the check itself: in Design mode it calls `check_design` after writing or changing screens. The plugin opens each screen at real sizes in a hidden Chrome or Edge on this computer (several sizes at once, such as a short 375×667 and a large 430×932, and dark mode if asked). Besides the same list, it reports each screen's measurements (page height, top and bottom bar heights and their gap to the content) and catches four more problems: fixed top or bottom bars covering content or sitting too close to it (like the last row hidden under a tab bar; content merely scrolling under a bar is fine), script errors, stylesheets, scripts or fonts that failed to load, and links to screens that don't exist. Low-contrast and tiny text also name the CSS rule that sets them, with file and line. Serious problems (covered by a bar, overlapping, clipped or overflowing text, running off the screen, broken images, very low contrast) come with a close-up screenshot of that spot (up to 4, taken at the size and scroll position where it was found: the element outlined in red with the amount on it, the other one of an overlapping pair in orange, a covering bar made see-through, nearby problems sharing one picture), so the model doesn't need a separate screenshot. Overlapping text also gets a likely cause (negative margin, absolute positioning or a transform) with the rule's file and line, and overlaps with the same cause are listed as one. A repeat run says how many problems were fixed and how many are new (new ones are marked), and an incomplete run says so. The sidebar doesn't need to be open; without Chrome or Edge the sidebar measures instead.
|
|
67
|
+
20. **Lets the model look.** When the model wants to see the real rendering it calls `look_design`. The same hidden browser takes a screenshot at a given size (the top, scrolled to the bottom, the whole page in parts, or one element such as the tab bar, in light or dark) and hands the image to the model. No servers or screenshot scripts of its own.
|
|
68
|
+
21. **Learns your brand.** Give the model a website or a project folder ("follow our site's style") and it calls `extract_design_system`. The hidden browser opens the site and measures what it actually renders: page and surface colors, body and secondary text, brand and accent colors, borders, fonts and the type scale, radii, shadows, and how the primary and secondary buttons, inputs, cards and header look. A project folder is also scanned for CSS variables, hard-coded colors, Tailwind config and component libraries. The result is a full set of files in `~/.dsh/design-systems/<brand>/`: `tokens.css` (same variable names as the skill's starter), `components.css` (buttons, inputs, cards, text styles), a `components.html` specimen and a `DESIGN.md`, plus screenshots of the site and its components. Once it is the default, every new design starts from it: the model skips the "what feel?" question and varies the three directions in layout only. "Brand design systems…" in the tokens panel (or the More menu) lists them with a specimen thumbnail and swatches, and lets you set or clear the default, apply one to the current design, or enter an address to extract a new one.
|
|
69
|
+
22. **Tweaks.** The model picks the dimensions most worth trying for this design (spacing, radius, brand color, card layout…) and writes them to `tweaks.json`. The dial button shows them as sliders, color pickers and choices. Dragging updates every screen on the canvas at once (only CSS variables and `<html>` attributes on the live page; files are untouched). When you like it, "Write back" puts variables into the defaults in `tokens.css` and attributes onto each screen's `<html>`, with a version saved before and after; "Ask the model" has it apply the values and adjust related details. With no tweaks yet, one click asks the model for some, and `check_design` also reports tweaks that aren't wired up.
|
|
70
|
+
23. **Builds it as code.** The `</>` button asks what to build with (same as the existing project, React, Vue, Next.js or plain HTML), where to put the code and whether to do all screens or just this one, then "Start building". The model first calls `handoff_design`, which writes `handoff/HANDOFF.md` in the design folder (each screen's file, suggested route and links; the tokens; parts shared by several screens that should become components; scripts, forms and hover states; tweaks; images and fonts; steps and acceptance) plus top-to-bottom screenshots of every screen. It then builds one screen at a time, and after each one runs the dev server and calls `compare_design`: both pages are captured at the same size and compared pixel by pixel, returning a "design | built | difference" image with the worst areas boxed and numbered, which element each one is on both sides, and the overall difference. It moves on once that is under 3%.
|
|
66
71
|
|
|
67
72
|
On a narrow sidebar the less-used toolbar buttons move into a "More" menu.
|
|
68
73
|
|
|
69
|
-
`preview_page` takes `target` (a file path, port or URL), `kind` (web / app / tablet / poster / slides / email), and optionally `size` (such as `1080x1920`) and `screens` (dev-server routes to show side by side).
|
|
74
|
+
`preview_page` takes `target` (a file path, port or URL), `kind` (web / app / tablet / poster / slides / email), and optionally `size` (such as `1080x1920`) and `screens` (dev-server routes to show side by side). `check_design` takes `screens` (which ones to measure), `sizes` (such as `["375x667","430x932"]`), `dark` and `images` (`false` for no screenshots). `look_design` takes `screens`, `size`, `at` (top / bottom / full), `selector` (just this element) and `dark`. `extract_design_system` takes `sources` (1–5 URLs, project folders or HTML files), `name` and `default`. `handoff_design` takes `framework` (same / react / vue / next / html), `dest` and `screens`. `compare_design` takes `url` (the built page: a local address or port), `screen`, `size`, `at` (top / bottom) and `dark`.
|
|
70
75
|
|
|
71
76
|
## Design mode and switches
|
|
72
77
|
|
|
@@ -78,7 +83,7 @@ The plugin is split into parts, each with its own switch under DSH Plugins → D
|
|
|
78
83
|
| --- | --- | --- |
|
|
79
84
|
| Design Studio | The sidebar and preview servers (turning it off means not using the plugin) | On |
|
|
80
85
|
| Open previews automatically | While designing, a written HTML file or a started dev server opens in the sidebar; off, the sidebar only offers a "Preview …" button | On |
|
|
81
|
-
| Preview
|
|
86
|
+
| Preview and check tools (other modes) | Standard, Creator and other modes can use `preview_page`, `check_design`, `look_design`, `extract_design_system`, `handoff_design` and `compare_design` | On |
|
|
82
87
|
| Design skill (other modes) | Standard, Creator and other modes can load the `ui-design` skill | On |
|
|
83
88
|
| Design mode | The Design entry in the mode menu | On |
|
|
84
89
|
|
|
@@ -104,6 +109,13 @@ node test/test-client.mjs
|
|
|
104
109
|
node test/test-skill.mjs
|
|
105
110
|
node test/test-history.mjs
|
|
106
111
|
node test/test-phone.mjs
|
|
112
|
+
node test/test-doc.mjs # PDF and PowerPoint export
|
|
113
|
+
node test/test-look.mjs # check_design / look_design (the real-browser part needs Chrome or Edge)
|
|
114
|
+
node test/test-system.mjs # brand design systems (the real-browser part needs Chrome or Edge)
|
|
115
|
+
node test/test-tweaks.mjs # tweaks: tweaks.json, write-back (every top-level copy of a variable), wiring check, live page
|
|
116
|
+
node test/test-handoff.mjs # build as code: handoff note and screenshots, default destination, symlink guards, lossless pixel compare (inner scroll areas, data-theme dark, redirects off localhost blocked)
|
|
117
|
+
node e2e/check-page.mjs # design check in headless Chrome
|
|
118
|
+
node e2e/e2e-studio.mjs # end to end: a throwaway DSH from npm, a fake model and headless Chrome
|
|
107
119
|
```
|
|
108
120
|
|
|
109
121
|
## Feedback
|
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
|
-
> **一句话**:DeepSeek Harness(DSH)插件,模型做网页、App 界面、海报、幻灯片时,右侧栏画布实时显示每一屏,套上手机 /
|
|
18
|
+
> **一句话**:DeepSeek Harness(DSH)插件,模型做网页、App 界面、海报、幻灯片时,右侧栏画布实时显示每一屏,套上手机 / 浏览器外框;点一下元素或框一块区域,补一句话就改。每轮自动存版本,能导出展示图、PDF、PPT,手机扫码看;一键体检文字溢出、对比度、点击区。
|
|
19
19
|
>
|
|
20
20
|
> **什么时候用**:「做个落地页」「设计一个 App 界面」「做张海报」「做几页幻灯片」。新会话可以直接选「设计模式」。
|
|
21
21
|
>
|
|
@@ -48,7 +48,7 @@ dsh plugin add https://github.com/OrderG-X/dsh-design-studio/releases/latest/dow
|
|
|
48
48
|
1. **做设计时自动打开**:模型加载了设计技能、调了 `preview_page`,或页面写了设计类型标记(`<meta name="dsh-design">`)之后,再写 `.html` 文件或用命令起开发服务器(`npm run dev` 这类),右侧栏自动弹出「设计工作台」。别的任务写的网页不弹,只在标题栏按钮上亮个蓝点,点开能一键预览;`curl`、浏览器工具去看别的端口,一律不当成要预览的页面。
|
|
49
49
|
2. **分类型显示**:网页(浏览器窗口)、App(手机外框带灵动岛)、平板、海报(1080×1350)、幻灯片(16:9)、邮件。模型调工具时说明类型,或在页面里写 `<meta name="dsh-design" content="app">`(可带尺寸,如 `poster 1080x1920`);工具栏「类型」菜单随时能换。
|
|
50
50
|
3. **多屏画布**:同一个目录里的几个 HTML 文件按生成顺序排成网格,能拖、能缩放(触控板双指滑 = 平移,捏合 / ⌘ 滚轮 = 缩放,右下角适合画布)。点一下选中一屏,双击只看这一屏;工具栏「画布 / 单页」切换。开发服务器可以让模型给几个路由并排看。
|
|
51
|
-
4.
|
|
51
|
+
4. **看着它生成**:模型干活时右侧先放一个占位;新的一屏写出来,元素从上到下依次浮现,一道扫描光带过去;这一轮里还在出新屏时,画布末尾留一个「生成中…」的占位。模型用提问卡问你时,底部提示换成橙点「模型在等你回答」,不再转圈。
|
|
52
52
|
5. **实时刷新,不白屏**:本地 HTML 所在目录里任何文件一改,后台先载好新的一帧,再淡入换上来(滚动位置不丢);开发服务器走它自己的热更新(Vite / Next 等的 WebSocket 照常通)。
|
|
53
53
|
6. **改了哪里一眼看到**:刷新 / 热更新前后各记一份页面「指纹」,改过、新加的元素在预览里亮一下(带一道扫光),都不在屏幕里就先滚过去;底下报「已更新 index.html · 1 处改动」。改的是共用的 CSS,画布上每一屏都会换帧、各自闪改动;只改某一屏的 HTML,别的屏不动。
|
|
54
54
|
7. **模型在干活时**:预览四周一圈流光,底下标「模型正在工作」或「正在写 style.css」。
|
|
@@ -67,21 +67,29 @@ dsh plugin add https://github.com/OrderG-X/dsh-design-studio/releases/latest/dow
|
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
React / Vue / Svelte 的开发模式下还会带上组件名和源码位置。
|
|
70
|
-
10. **内置设计规范**:插件带一个 DSH 技能 `ui-design`,模型做界面前会加载:先定类型和画幅、先写 `tokens.css`(设计变量)、一屏一个文件;排版 / 间距 / 字号阶梯 / 配色 / 对比度 / 中文排版的规则,避开「AI 味」的清单,写完的自查表;网页、App
|
|
70
|
+
10. **内置设计规范**:插件带一个 DSH 技能 `ui-design`,模型做界面前会加载:先定类型和画幅、先写 `tokens.css`(设计变量)、一屏一个文件;排版 / 间距 / 字号阶梯 / 配色 / 对比度 / 中文排版的规则,避开「AI 味」的清单,写完的自查表;网页、App、海报、幻灯片、邮件各有细则。从零做一套新设计时,模型先问两三个关键问题(给谁用、什么气质、要哪几屏),再出三个方向的首屏(`home-a/b/c.html`,各带一套配色字体)并排摆在画布上,你挑一个它再做全套,没选中的挪进 `drafts/`(问的时候已经挑了气质,三个方向就都在这个气质里,换版式和重点);说「直接做」、或者需求已经很具体,就跳过这一步。在输入框打 `/ui-design` 也能手动加载。
|
|
71
71
|
11. **设计规范面板**:工具栏调色板按钮,列出这套设计 `:root` 上的设计变量,按颜色、字体、字号、间距、圆角、阴影分组,带色块和样例。点一个放进输入框(`〔设计变量〕--brand: #c26a32` 加文件位置),补一句「换成墨绿」,模型改这一个变量,所有屏一起变。还没有变量时,一键让模型按规范建一份。
|
|
72
|
-
12.
|
|
72
|
+
12. **导出**:工具栏下载按钮,或画布上选中一屏后标签旁的按钮。可以导出这一屏(带手机 / 浏览器外框,或纯画面)、整页长图,以及几屏合成的一张展示图(品牌名做大标题、背景和主色取自设计变量,适合直接发朋友圈 / 小红书 / 作品集)。也能把所有屏导成 **PDF**(每屏一页,网页 / 邮件是整页长图,页面尺寸跟着画幅)或 **PPT**(每屏一张幻灯片,整页图片,幻灯片比例跟着画幅,PowerPoint / WPS / Keynote 都能打开)。屏和屏之间的跳转照样能点:PDF 里点「去点单」翻到菜单那页(每屏还有一条书签),PPT 放映时点了跳到那一张,导出的文件就是能点的原型。文件存到设计目录的 `exports/`,图片同时复制到剪贴板,点「打开文件夹」直接定位。
|
|
73
73
|
13. **版本**:每轮对话开始前、结束后各自动存一版(只存设计文件,名字就是你那句话,例如「主色换成深海蓝」;点选元素发出去的,名字是「「3.2 km」 · 改成绿色」这样,不会是一长串选择器)。工具栏时钟按钮列出所有版本;点一版就看当时的样子(画布 / 单页都行),点「对比」左右拖动把手,比较那一版和现在;「回到这一版」把文件写回去(回之前先把现在的存一版,随时能再切回来)。
|
|
74
74
|
14. **深色模式**:页面写了深色样式(`@media (prefers-color-scheme: dark)`,或 `[data-theme="dark"]` / `.dark`),工具栏多一个月亮按钮,一键切换看深色版,导出的图片也是深色的。
|
|
75
|
-
15. **再出两版**:画布上选中一屏,点标签旁的闪光按钮,让模型给这一屏出两版不同方向的设计(`login-b.html`、`login-c.html
|
|
75
|
+
15. **再出两版**:画布上选中一屏,点标签旁的闪光按钮,让模型给这一屏出两版不同方向的设计(`login-b.html`、`login-c.html`),紧挨着原版排在画布上(开局的三个方向 `home-a/b/c` 也一样,按 a、b、c 排)。挑中哪版就点它标签旁的对勾「就要这个」:输入框里写好「用这一版、其他版本挪进 `drafts/`、接着做全套」,发出去就行。
|
|
76
76
|
16. **在手机上看**:工具栏扫码按钮,右侧栏出一个二维码,手机(和电脑连同一个 Wi-Fi)相机一扫就在手机浏览器里打开这套设计,按真机尺寸看、点得动。电脑上点哪一屏,手机跟着跳;模型改了设计,手机自动刷新。面板上看得到「手机已打开」,也能复制链接、随时停止;30 分钟没人看自动关。
|
|
77
77
|
17. **可点原型**:屏和屏之间用链接连起来(`<a href="detail.html">` 或 `data-href`,内置技能会让模型这么写),画布上选中一屏,就画出它跳去哪、从哪跳来的箭头;隔着别的屏的箭头从屏和屏之间的空当、标题上面绕过去,不穿过中间那屏。点工具栏播放按钮进入演示:在预览里点按钮跳到对应那一屏,前进从右边滑进来、返回从左边;顶上能后退、从头来、退出(Esc)。
|
|
78
78
|
18. **多尺寸并排**:工具栏第三个视图按钮,把现在这一屏按几档宽度并排看:网页是手机 / 平板 / 桌面,App 是小屏 / 标准 / 大屏(小屏是 SE 那种矮屏,没有灵动岛),平板是三种尺寸,小屏挤不挤一眼看出来。并排时也能选择元素,带上是哪一档屏宽。
|
|
79
79
|
19. **预览里直接改字**:工具栏「改字」按钮,点页面里的任意文字就地改,回车保存、Esc 取消。本地 HTML 直接写回源文件(只改那一处,改前改后各存一版,能回退);开发服务器的页面,或源码里找不到那段字时,改成一句「把「…」改成「…」」放进输入框,交给模型改。
|
|
80
80
|
20. **参考图**:把图片拖进右侧栏、在右侧栏里粘贴,或在设计规范面板点「照参考图改风格…」。图片存进设计目录的 `refs/`(还没有设计目录时存插件缓存),右侧栏从图里取出主要颜色(带占比,少见的鲜艳点缀色也会单独挑出来)和整体深浅,连同图片路径和一句「照这张图的风格改,先更新 tokens.css」放进输入框。
|
|
81
|
+
21. **设计体检**:工具栏听诊器按钮,在画好的页面上实测:文字超出屏幕、被截断、超出框、叠在一起,文字和背景对比度不够(按 WCAG,大字 3:1、正文 4.5:1),点击区太小(App 44、网页 24),字太小,图片没加载出来或被拉变形,一行太长(网页 / 邮件 / 平板,中文超过约 45 字、英文约 90 个字母),按钮、价格这种短字被挤得断了行(「再来一 / 单」),字号太杂。画布上每一屏都查,问题按屏列出来,预览里在对应位置标上编号(严重的标红);点一条就滚过去闪一下。点「交给模型修」,清单(哪一屏、哪个元素、什么问题、差多少)放进输入框,发出去模型逐条改源文件。模型每干完一轮,右侧会自己量一遍:面板开着就直接刷新,关着就在按钮上标出还剩几个问题(没有数字就是都好了)。点一条时,那一屏要是被面板挡着或在画布外面,画布自己挪过来。模型自己也能体检:设计模式里它写完、改完会调 `check_design`——插件在电脑上另开一个看不见的 Chrome / Edge,按真实尺寸打开每一屏量一遍(一次可以量几种尺寸,比如 375×667 的矮屏和 430×932 的大屏,也能量深色),清单里还有每屏的测量数字:页面多高、顶栏 / 底栏多高、离内容多远。这条路另外查四类问题:固定的顶栏 / 底栏压住了内容或贴得太近(滚到底最后一行被底栏挡住这种;滚动中从栏下面经过不算)、页面脚本报错、样式表 / 脚本 / 字体没载到、跳转指向不存在的页面。对比度不够、字太小这两类还写明由哪条样式规则决定、在哪个文件第几行;严重的问题(被栏压住、文字叠在一起 / 被截断 / 超出框、超出屏幕、图片没出来、对比度很低)自动附上那一块的局部截图(最多 4 张,按发现时的尺寸和滚动位置截;红框圈出元素、框上标差多少,叠着的另一个用橙框;被栏压住的把栏调成半透明,透出下面的字;挨着的几条共用一张),模型不用再单独截图对照。文字叠在一起时还找出可能的原因(负 margin、绝对定位、transform),写明那条规则在哪个文件第几行;同一个原因引起的几处并成一条。再量一次时写「比上次修好几个、新出几个」,新出的标出来;有没量成的就明说检查不完整。不用右侧栏开着;电脑上没有 Chrome / Edge 时退回右侧栏量。
|
|
82
|
+
22. **调样式**:工具栏滑块按钮,点页面里的一个元素,右边出属性面板:字号、字重、文字颜色、背景、圆角、上下 / 左右内边距(排成一行 / 一列的容器还有间距)。拖滑块页面上马上变;颜色旁边列着这套设计的色块,元素原来用的是设计变量(如 `--brand`)会标出来。「写回文件」把改动写进 HTML 里这个元素的 `style` 属性(改前改后各存一版,能回退);「交给模型」把「字号 16px → 18px」这样的清单放进输入框,让模型写进样式表、能用设计变量就用设计变量;还附上每项现在由哪条样式规则决定、在哪个文件第几行(行内 style 盖过了规则也说清楚),模型不用再翻源码找。没写回就退出,页面恢复原样。开发服务器的页面只能交给模型。
|
|
83
|
+
23. **风格卡**:九套现成风格——极简白、温暖手作、杂志编辑、深色高级、活泼年轻、清新自然、专业科技、玻璃质感、柔和立体,每张卡是一套配色、字体、圆角、阴影的小样。还没开始设计时,空白页上点一张,输入框里写好〔风格〕和整套配色,再补一句要做什么;已经有设计了,在设计规范面板点「换个风格…」,模型先改 `tokens.css`,再把写死的颜色换掉,版式和文字不动,改完自己体检一遍。
|
|
84
|
+
24. **远程也能看**:从手机(dsh-mobile 走外网)或别的电脑打开 DSH 网页时,电脑本机的预览端口那边连不上。本地 HTML 做的设计,插件把一屏连同样式、脚本、图片打成一份传过去,放进沙盒 iframe 显示,选择、改字、调样式、体检、导出照样能用;页面里点跳转就换到那一屏,文件改了跟着刷新(只刷用到这个文件的屏)。开发服务器的页面打包不了,还是只给一句说明。
|
|
85
|
+
25. **模型自己看图**:模型想看真实渲染的样子时调 `look_design`:同样用那个看不见的浏览器,按指定尺寸截一屏——顶上一屏、滚到底、整页分几段,或者只截某个元素(比如底栏),也能截深色——图直接交给模型看。不用自己起服务器、写截图脚本。
|
|
86
|
+
26. **品牌设计系统**:给一个网站地址或项目文件夹(「照我们官网的风格」),模型调 `extract_design_system`,插件用看不见的浏览器打开网页量真实渲染的样式——底色、正文和次要文字、主色、点睛色、边框,字体和字号阶梯,圆角、阴影,主按钮 / 次按钮 / 输入框 / 卡片 / 顶栏的样子——项目文件夹还会扫 CSS 变量、写死的颜色、Tailwind 配置和组件库。提出来的是一整套文件:`tokens.css`(和技能起手模板同一套变量名)、`components.css`(按钮、输入框、卡片、文字样式)、`components.html` 样张、`DESIGN.md` 说明,连同网站和组件截图存进 `~/.dsh/design-systems/<品牌>/`。设成默认后,以后每次做新设计模型都先复制这一套,不再问「想要什么感觉」,三个方向只在版式上拉开。右侧栏设计规范面板里「品牌设计系统…」(窄的时候在「更多」里)列出所有套,带样张缩略图和色块,能设默认、取消默认、一键套用到当前设计,也能直接填地址让模型提一套。
|
|
87
|
+
27. **调节滑块**:模型给这套设计挑几个最值得试的维度(留白疏密、圆角、主色、卡片排法……)写进 `tweaks.json`,工具栏旋钮按钮列成滑块、取色、几选一;拖的时候画布上每一屏一起变(只改页面上的 CSS 变量和 `<html>` 上的属性,不动文件),满意了「写回文件」(变量写进 `tokens.css` 的默认值,属性写到每屏的 `<html>` 上,改前改后各存一版),或「交给模型」照这几个值改、顺手协调相关的地方。还没有滑块时一键让模型做几个;`check_design` 会顺带查滑块有没有真接上(变量没人用、没有默认值、选项没有对应的样式)。
|
|
88
|
+
28. **转成代码**:工具栏 `</>` 按钮,选用什么做(跟现有项目一样 / React / Vue / Next.js / 纯 HTML)、代码放哪、转全部还是这一屏,点「开始转成代码」。模型先调 `handoff_design`:插件在设计目录写一份 `handoff/HANDOFF.md`(每屏的文件、建议路由、跳去哪,设计变量,几屏共用该抽成组件的部分,脚本 / 表单 / 悬停这些交互,滑块,图片字体,做法和验收)和每屏从上到下的截图;模型照着一屏一屏写,每屏写完把开发服务器跑起来调 `compare_design`:同样尺寸各截一张逐像素比,回一张「设计稿 | 做出来的 | 差异」拼图,差得多的地方红框标号、写明两边分别是哪个元素,差异降到 3% 以下再做下一屏。
|
|
81
89
|
|
|
82
90
|
工具栏还有:地址(点一下能改,输文件路径 / 端口 / localhost 地址都行)、刷新、在浏览器里打开。右侧栏拉窄时,不常用的几个按钮收进「更多」菜单。会话标题栏右边有个「设计工作台」按钮,随时打开。DSH 重启后,每个会话接着预览原来的页面(类型、视图也记着)。
|
|
83
91
|
|
|
84
|
-
`preview_page` 工具的参数:`target`(文件路径 / 端口 / 地址)、`kind`(web / app / tablet / poster / slides / email)、`size`(如 `1080x1920`,可选)、`screens
|
|
92
|
+
`preview_page` 工具的参数:`target`(文件路径 / 端口 / 地址)、`kind`(web / app / tablet / poster / slides / email)、`size`(如 `1080x1920`,可选)、`screens`(开发服务器上并排看的几个路由,可选)。`check_design`:`screens`(只量哪几屏)、`sizes`(如 `["375x667","430x932"]`)、`dark`、`images`(`false` 不附截图)。`look_design`:`screens`、`size`、`at`(top / bottom / full)、`selector`(只截这个元素)、`dark`。`extract_design_system`:`sources`(网址 / 项目文件夹 / HTML 文件,1–5 个)、`name`、`default`。`handoff_design`:`framework`(same / react / vue / next / html)、`dest`、`screens`。`compare_design`:`url`(做出来的页面,本机地址或端口)、`screen`、`size`、`at`(top / bottom)、`dark`。
|
|
85
93
|
|
|
86
94
|
## 设计模式和开关
|
|
87
95
|
|
|
@@ -93,7 +101,7 @@ dsh plugin add https://github.com/OrderG-X/dsh-design-studio/releases/latest/dow
|
|
|
93
101
|
| --- | --- | --- |
|
|
94
102
|
| 设计工作台 | 右侧栏和预览服务器(关掉就等于不用这个插件) | 开 |
|
|
95
103
|
| 自动打开预览 | 做设计时写了 HTML、起了开发服务器,右侧自动打开;关掉后只在右侧栏给一个「预览刚写的 …」按钮 | 开 |
|
|
96
|
-
|
|
|
104
|
+
| 预览和体检工具(其他模式) | 标准、创造等模式也能用 `preview_page`、`check_design`、`look_design`、`extract_design_system`、`handoff_design` 和 `compare_design` | 开 |
|
|
97
105
|
| 设计技能(其他模式) | 标准、创造等模式也能加载 `ui-design` 技能 | 开 |
|
|
98
106
|
| 设计模式 | 模式菜单里的「设计模式」 | 开 |
|
|
99
107
|
|
|
@@ -117,6 +125,8 @@ dsh plugin add https://github.com/OrderG-X/dsh-design-studio/releases/latest/dow
|
|
|
117
125
|
- **接口**(`/dsh-design-studio/api/*`):交给 DSH 自己的 `connection` 服务验登录态;老版本 DSH 没有这个服务时只收本机、同源请求。
|
|
118
126
|
- **手机上看**:默认不开,点了才在局域网开一个入口;只监听本机在局域网里的那个地址(不是 0.0.0.0),Host 也得对。链接带一次性的随机口令(128 位),手机第一次打开换成只给这个端口的 Cookie(HttpOnly),地址栏里不留口令;没口令、口令不对一律 403。停止或 30 分钟没人看就关端口,再开换新口令。同一个 Wi-Fi 里拿到这个链接的人也能看,所以别把链接发到群里。
|
|
119
127
|
- 代理开发服务器时会拆掉 `X-Frame-Options` 和 CSP(不然嵌不进右侧栏),只影响预览端口,开发服务器本身不动。
|
|
128
|
+
- **远程看**:打包好的页面放在 `sandbox="allow-scripts allow-forms"` 的 iframe 里(不透明的源,碰不到 DSH 的 Cookie 和接口),页面里放一个 `<base>` 指着原来的预览地址,拿不到 DSH 网页的地址;只内联设计目录里的文件(越界、软链接到外面的不读),总量 12 MB 封顶;接口回 JSON,不在 DSH 的地址下直接给 HTML。
|
|
129
|
+
- **调样式写回**:只收字号、颜色、圆角这几个属性,值里不许有 `url(`、引号、分号这类东西;按标签、序号、id、class 在源码里认出那个元素,认不准就不写,改成交给模型。
|
|
120
130
|
|
|
121
131
|
## 架构
|
|
122
132
|
|
|
@@ -129,6 +139,13 @@ dsh plugin add https://github.com/OrderG-X/dsh-design-studio/releases/latest/dow
|
|
|
129
139
|
/dsh-design-studio/api/export|reveal 存导出的图片、在文件夹里显示
|
|
130
140
|
/dsh-design-studio/api/phone 手机上看:开 / 关局域网入口、给二维码、电脑换屏手机跟着跳
|
|
131
141
|
/dsh-design-studio/api/edit|ref 预览里改的字写回 HTML、存参考图
|
|
142
|
+
/dsh-design-studio/api/style|locate 调样式:写进元素在源码里的 style 属性;按规则找回源文件和行号
|
|
143
|
+
/dsh-design-studio/api/bundle 远程看:一屏连同样式、图片打成一份(放进沙盒 iframe)
|
|
144
|
+
/dsh-design-studio/api/tweak 调节滑块写回文件(tokens.css 默认值、每屏 <html> 属性、tweaks.json)
|
|
145
|
+
/dsh-design-studio/api/systems|systhumb 品牌设计系统库:列出来、设 / 取消默认、样张缩略图
|
|
146
|
+
/dsh-design-studio/api/project 转成代码前:会话目录是不是前端项目、建议放哪
|
|
147
|
+
check_design / look_design → 无头浏览器(browser.js)按真实尺寸开页面,picker 里量、截图(look.js)
|
|
148
|
+
extract_design_system → system.js;handoff_design / compare_design → handoff.js
|
|
132
149
|
页面 picker.js 注入到 HTML:悬停框选、点选 / 拖选、屏间链接、演示时接住跳转、就地改字、postMessage 回右侧栏、改动后刷新
|
|
133
150
|
客户端 右侧栏标签页(sidebarRightTabs)+ 标题栏按钮(session.header.utilities)
|
|
134
151
|
```
|
|
@@ -137,11 +154,17 @@ dsh plugin add https://github.com/OrderG-X/dsh-design-studio/releases/latest/dow
|
|
|
137
154
|
- `lib/tool.js`、`lib/skill.js`、`lib/auto.js`、`lib/preset.js` —— 能单独开关的几个组件:挂工具、挂技能、自动打开预览、设计模式(`cordis.patch.yml` 里列着;设计模式照抄 DSH 标准模式的插件,再挂上工具和技能)
|
|
138
155
|
- `lib/core.js` —— 地址解析、自动发现、会话状态和存盘、每轮前后存版本、改字写回(只在看得到的文字里找,认 HTML 实体)
|
|
139
156
|
- `lib/history.js` —— 设计版本:按内容存(没改的文件只存一份),最多 40 版,回到某一版
|
|
140
|
-
- `lib/serve.js` —— 预览服务器:静态文件 / 代理 / 改动推送 /
|
|
157
|
+
- `lib/serve.js` —— 预览服务器:静态文件 / 代理 / 改动推送 / 注入(picker 和一段收集页面报错、没载到的资源的小脚本)
|
|
158
|
+
- `lib/browser.js` —— 无头浏览器:找本机的 Chrome / Edge / Chromium / Brave,`--remote-debugging-pipe` 说话(不开端口),临时用户目录,闲置自己关
|
|
159
|
+
- `lib/look.js` —— `check_design` / `look_design` 的量和截:挑屏、解析尺寸、每种尺寸开一页量问题和测量数字、挑严重问题截局部图、截顶上 / 滚到底 / 整页分段 / 元素,整理成交给模型的字
|
|
160
|
+
- `lib/system.js` —— 品牌设计系统:在网页里收集颜色 / 字体 / 圆角 / 阴影 / 组件,扫项目文件夹,按 OKLab 聚类定角色(对比度够不够也算),写 tokens.css / components.css / 样张 / DESIGN.md,库(默认那套)和给技能补的一节
|
|
161
|
+
- `lib/tweaks.js` —— 调节滑块:读 tweaks.json、写回(:root 上的默认值、<html> 属性)、查有没有接上
|
|
162
|
+
- `lib/handoff.js` —— 转成代码:认现有项目、交接说明和截图、compare_design 的逐像素比(在无头浏览器里算差异、找成片的差异块、拼图)
|
|
163
|
+
- `lib/bundle.js` —— 远程看:把一屏的样式表(含 `@import`)、脚本、图片、字体内联成一份 HTML,放 `<base>` 和 picker
|
|
141
164
|
- `lib/phone.js` —— 手机上看:局域网入口(口令换 Cookie、转发到预览服务器、补 viewport、跟随跳屏、WebSocket 透传、闲置自动关)
|
|
142
165
|
- `lib/qr.js` —— 二维码生成(字节模式、纠错 M、版本 1–10,不引第三方库)
|
|
143
|
-
- `lib/picker.js` —— 页面里的脚本:选取、改动高亮(指纹比对)、生成动画、和右侧栏握手换帧(关闭的 shadow DOM,不污染页面样式)、把页面画成 PNG
|
|
144
|
-
- `lib/client.js` —— 右侧栏、工具栏、画布(排版、拖动、缩放)、设备外框、动画、两帧 iframe
|
|
166
|
+
- `lib/picker.js` —— 页面里的脚本:选取、改动高亮(指纹比对)、生成动画、和右侧栏握手换帧(关闭的 shadow DOM,不污染页面样式)、把页面画成 PNG(样式、图片、字体都内联)、深色模式切换、报告屏间链接、演示时把点链接变成跳屏、就地改字、调样式(选中元素、实时套样式、认出用了哪些设计变量、每项由哪条规则决定)、远程看时接住跳页、给无头浏览器用的 `window.__dshDesign`(量、切深色、滚动、元素位置)
|
|
167
|
+
- `lib/client.js` —— 右侧栏、工具栏、画布(排版、拖动、缩放)、设备外框、动画、两帧 iframe 交替(新的载好再淡入)、设计规范面板、版本面板和左右对比、导出(套外框、合成展示图)、把选取整理成文字插进输入框、原型箭头和演示、多尺寸并排、改字、参考图取色、窄栏「更多」菜单、调样式面板、风格卡、远程看(打包的页面放进沙盒 iframe,跟着改动重新打包)
|
|
145
168
|
- `skills/ui-design/` —— 内置设计规范(DSH 技能):`SKILL.md` 流程和原则、`references/` 各类型细则、`tokens.css` 起手模板
|
|
146
169
|
- `assets/demo.gif`、`assets/demo-versions.gif` —— 演示动图(测试台里跑插件真代码录的);`assets/showcase.jpg` —— 导出的展示图样例
|
|
147
170
|
- `docs/feasibility.zh.md` —— 做之前的可行性调研
|
|
@@ -150,15 +173,27 @@ dsh plugin add https://github.com/OrderG-X/dsh-design-studio/releases/latest/dow
|
|
|
150
173
|
|
|
151
174
|
```sh
|
|
152
175
|
node test/test-serve.mjs # 静态文件、跳不出目录、Host 把门、改动推送、代理、WebSocket、只听 ::1 的开发服务器、连不上的提示页
|
|
153
|
-
node test/test-core.mjs # 地址解析、自动发现(只在做设计时,后台跑的开发服务器也认)、模型 / 用户指定的不被顶掉、重放不动手、存盘恢复(恢复中先不显示空状态)、类型 / 几屏 /
|
|
176
|
+
node test/test-core.mjs # 地址解析、自动发现(只在做设计时,后台跑的开发服务器也认)、模型 / 用户指定的不被顶掉、重放不动手、存盘恢复(恢复中先不显示空状态)、类型 / 几屏 / 换屏、改字写回、调样式写回(认元素、合并 style、只收安全的值)
|
|
154
177
|
node test/test-client.mjs # 右侧栏标签页 / 标题栏按钮注册、只给当前会话自动弹出、选中内容整理成的文字、页面 meta、画布排版、设计变量分组、原型箭头(隔屏的绕开别的屏)、多尺寸排版、矮屏手机没有灵动岛、参考图取色
|
|
155
178
|
node test/test-skill.mjs # 内置技能:名字 / 来源合规、内容读得到、引用的细则文件都在;拆开的几个组件、设计模式带齐标准模式的工具
|
|
156
179
|
node test/test-history.mjs # 版本:存一版 / 没变不存 / 只存设计文件 / 回到某一版 / 上限、每轮前后存、版本名(点选 / 变量 / 参考图换成短名字)、旧版本文件、导出的图不算改动
|
|
180
|
+
node test/test-bundle.mjs # 远程看的打包:样式表 / @import / url() / 脚本 / 图片 / srcset 内联,外网的、找不到的、越界的原样留着,<base> 和 picker 放对地方,总量上限,@import 循环
|
|
157
181
|
node test/test-phone.mjs # 手机上看:二维码结构、挑网卡、补 viewport、口令换 Cookie、没口令 / Host 不对 403、转发、跟随跳屏、WebSocket、停止、闲置自动关
|
|
182
|
+
node test/test-doc.mjs # PDF / PPT:JPEG 尺寸、PDF 结构和交叉引用、PPT 的各个部件(解压校验 CRC)、幻灯片比例和居中、接口存进 exports/、重名加序号、坏输入拒掉
|
|
183
|
+
node test/test-look.mjs # check_design / look_design:挑屏、尺寸、整理给模型的字;假浏览器走完工具和附图;本机有 Chrome / Edge 时真开页面:底栏压住最后一行(矮屏报、大屏不报)、App 壳里的滚动区、脚本报错、缺样式表、截图尺寸、元素裁剪、深色、调样式每项由哪条规则决定、严重问题附图(底栏压住、App 壳、叠字,浅色深色)
|
|
184
|
+
node test/test-system.mjs # 品牌设计系统:颜色解析和对比度、扫项目、定角色、写出来的文件、库和默认、给技能补的一节;有 Chrome / Edge 时真从一个网页提一套(品牌名、角色、字体、圆角、组件、截图、再提覆盖)
|
|
185
|
+
node test/test-tweaks.mjs # 调节滑块:读 tweaks.json(写错了怎么说)、值规整、写回 tokens.css(同一个变量写了几遍都改)/ <html> / tweaks.json、体检、写回走版本;真浏览器里拖了页面真变、撤掉还原
|
|
186
|
+
node test/test-handoff.mjs # 转成代码:认项目、路由、共用部分、只比本机地址、代码默认放哪(不放进设计目录,软链接别名也认)、交接包不跟软链接写到外面、交接说明的各节;真浏览器:交接包截图、一样的页面差异 < 1%、改过的页面标出差在哪、淡灰对白也算差异、滚到底连里面能滚的区域、data-theme 深色两边都切、本机地址跳到外网被拦下
|
|
187
|
+
node e2e/check-page.mjs # 设计体检(要本机 Chrome):十二类问题都查得出、该放过的放过(省略号、动画中的半透明字、深色页面、限宽的段落、object-fit 的图、正文里跟着折行的字…),干净的页面零问题;加 --dir <设计目录> 拿真设计稿试
|
|
188
|
+
node e2e/e2e-studio.mjs # 真机(要本机 Chrome,会从 npm 装一个临时 DSH):假模型调 preview_page 开三屏 App;干完一轮按钮上标出问题数,体检查出埋的问题、交给模型修、改完自动再量;面板开着时模型改坏别的屏,面板自己刷新、那屏挪出来;模型调 `check_design` 拿回清单和严重问题的局部截图;模型调 `look_design` 拿回一张真实渲染的图;导出的 PDF / PPT 里跳转能点;调样式拖滑块、写回、交给模型;换风格;换个地址打开(像手机走外网)时预览打包传过来、点得动、跟着改动刷新
|
|
158
189
|
```
|
|
159
190
|
|
|
160
191
|
## 已验证
|
|
161
192
|
|
|
193
|
+
第一期之后又打磨了一轮,同样在全新的 DSH 0.2.1-rc.3 网页版里实测(`e2e/e2e-studio.mjs`,40 项全过):模型用提问卡问话时,右侧栏提示「模型在等你回答」,答完提示消失;模型调 `check_design` 前后各一次,第二次只剩它改坏的那屏;「再出一版」写的 home-b 在画布上紧挨着首页;选中它点「就要这个」,输入框里是〔选定方向〕,让模型把原版挪进 `drafts/`。真模型(DeepSeek-V4.1-Flash)在桌面端设计模式里写完首页,自己调了两次 `check_design`(右侧栏量完把清单交回),改完再量一遍「0 个问题」,没有再另开浏览器截图;它写的 `_probe.html` 这类临时文件不再出现在画布上。新加的「文字被挤断行」拿真模型写的首页试,查出 5 处(「可外带」「再来一单」和三个价格)。
|
|
194
|
+
|
|
195
|
+
第一期(导出 PDF / PPT、设计体检)在一个全新的 DSH 0.2.1-rc.3 网页版里实测(`e2e/e2e-studio.mjs`,36 项全过):打开设计后按钮上自动标出 4 个问题;模型调 `check_design` 拿回同样的清单(只列首屏,面板关着时再调只列购物车);模型调 `preview_page` 开三屏 App,体检只在首屏查出埋的四个问题(文字截断、对比度 1.59:1、26×26 的按钮、8px 小字),另两屏零问题,预览里标上编号,点一条闪一下;「交给模型修」把清单放进输入框,发出去模型收到;改好首屏「再查一次」没问题;模型改完这一轮自动再量、按钮上的数字没了;面板开着时模型改坏购物车,面板自己刷新,被挡住的那屏挪出来;导出的 PDF 三页、PPT 三张竖版幻灯片,存进 `exports/`,不当成设计改动,里面「去点单」「加入购物车」能点着跳页,PDF 带每屏书签。PPT 也在 PowerPoint 里打开过,三张都正常、没有修复提示。桌面端重启后插件照常加载。真模型(DeepSeek-V4.1-Flash)在桌面端设计模式里说「帮我设计一个咖啡外带 App 的界面」:先用提问卡问了三个问题(感觉、场景、必须有的),答完写出 home-a / b / c 三个方向并排在画布上,最后再用提问卡问选哪个(也可以混搭)。这次发现两点已改:问完感觉后三个方向不该再跨风格;模型靠另开浏览器截图自查,一轮用掉一百多万 token——所以加了 `check_design`,并让三个方向在画布上按 a、b、c 排。
|
|
196
|
+
|
|
162
197
|
DeepSeek Harness 桌面端实测:让模型「设计一个咖啡外带 App 的三屏界面」,它先自己加载 `ui-design` 技能、建 `tokens.css`、一屏一个文件,右侧画布套手机外框一屏屏长出来(末尾带「生成中…」),写完按自查表找出文字重叠和对比度问题自己改掉;在「设计规范」面板点主色进输入框,模型问清方向后只改 `tokens.css` 一个变量,三屏一起变色、改动处高亮。之后又实测了第三期:每轮对话前后自动存版本(名字就是那句话)、版本面板、看第 1 版、左右拖动对比、导出整套展示图(3144×2541);点「再出两版」后模型没动原版和 tokens.css,写了 login-b / login-c 两个方向,画布上紧跟在登录屏后面。更早还测了:模型写好 HTML 后右侧栏自动弹出预览;地址栏手动打开;选择模式下点选元素后文字进输入框;改文件后预览自动刷新;DSH 重启后每个会话接着预览原来的页面。浏览器里另测了悬停框、点选、拖选区域、改 CSS 后自动刷新。测试台(插件真代码 + 仿 DSH 外壳)里测了:生成占位、一屏屏生成的动画、四屏 App 画布、画布里点选、改一屏只刷一屏、改共用 CSS 全部刷新、双击单看、类型切换、窄侧栏排版、设计规范面板(分组、点一个进输入框、没有变量时的提示)、导出(单屏带外框、深色页面、三屏合成展示图)、版本列表 / 看旧版 / 左右对比拖把手 / 回到这一版、深色模式切换、再出两版的提示和排序;手机上看:面板里的二维码用系统识别读得出链接,模拟 iPhone(390 宽)扫码打开,地址栏不留口令,「手机已打开」亮起,电脑上选别的屏手机跟着跳,改设计文件手机自动刷新,点停止后链接立刻失效。还测了第五期:原型箭头和演示里点按钮跳屏、多尺寸三档并排、点字改字写回文件并存版本(单页,和画布上的另一屏)、拖入 / 粘贴参考图后取色进输入框、窄侧栏的「更多」菜单。第五期又在 DSH 桌面端实测:非设计任务(命令里 curl 本机端口)不弹出;模型调 `preview_page` 后画布上带跳转箭头,侧栏窄时次要按钮进「更多」;演示里点「获取验证码」滑到首页;多尺寸三档并排;改字回车后写回源文件,版本里多一条「改字「…」」;手机二维码能识别出局域网链接、能打开,点停止后端口关掉;在右侧栏粘贴参考图后,图存进设计目录的 `refs/`,主要颜色和点缀色(如 `#C8763E 1%(点缀)`)进了输入框;发出去后模型先写 `tokens.css`,四屏一起换成参考图的深色加焦糖色;选图按钮能弹出系统选文件框。从访达拖图进来这一步模拟不出系统拖拽,只在测试台验过。之后在 DSH 里从零跑了一遍:模型自己加载技能、调 `preview_page`,三屏宠物领养 App 一屏屏长出来,页面之间自己连好了跳转;点选进输入框、模型改完高亮、版本 / 对比 / 回到第 1 版(文件确实回去了)、导出展示图、窗口在前台时导出也复制进剪贴板、设计规范面板、单页改字写回、多尺寸都正常。这一轮在 DSH 里找出并修了四个只在真 DSH 出现的问题:画布 iframe 刚载完时视口是 0×0,跳转箭头和演示认不出按钮(现在尺寸到了会补报);模型用后台任务(`run_in_background` + `job_output`)起的开发服务器认不出;新版 Vite 在 Mac 上只听 `::1`,代理只连 127.0.0.1,全是「连不上」;DSH 重启时右侧栏先闪一下空状态。修完后实测开发服务器:五个路由并排、改源文件热更新、开发服务器页面上改字变成一句请求、模型改完自动刷新、手机扫码打开(没口令 403)、停止后端口关掉。0.4.0 的设计模式和开关在一个全新的 DSH(0.2.1-rc.2)里实测:模式菜单里有「设计模式」,选它发消息,模型拿到 `preview_page`、`ui-design` 和做设计的人设,调 `preview_page` 后右侧工作台套手机外框打开页面;标准模式默认也有这两样;插件页关掉两个「(其他模式)」后,新开的标准模式会话里没有了,设计模式里照样有;关掉「设计模式」,模式菜单里就没有它,再打开又回来。再早一轮修了几个小毛病,也在 DSH 里验过:新屏播生成动画时底部按钮是透明的,跳转箭头漏了它(现在播动画的按钮也算,播完再补报一次位置);隔着一屏的箭头改成绕到标题上面走;点选后的版本名从「〔选中元素〕em…」变成「「3.2 km」 · 把这个距离文字改成绿色…」;先打字再点选时,选中的内容另起一行,不再粘在那句话后面;多尺寸里的小屏不再画灵动岛;提示条和顶上的提示消息不再叠在一起。
|
|
163
198
|
|
|
164
199
|
## 反馈
|
package/cordis.patch.yml
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
config:
|
|
28
28
|
prefix: |-
|
|
29
29
|
You are a design agent powered by the {{model}} model. You design user interfaces and visuals — web pages, app and tablet screens, posters and social images, slides, emails, components — and build them as real HTML/CSS, or inside the user's front-end project.
|
|
30
|
-
Before designing, load the ui-design skill and follow its process and self-check list. Once the first HTML file is written, or a dev server is up, call preview_page so the user can watch the design live in the Design Studio sidebar; later edits refresh on their own. When a message starts with an element or region the user selected in the preview, change exactly
|
|
30
|
+
Before designing, load the ui-design skill and follow its process and self-check list. For a new design, first ask the two or three questions that matter and show three clearly different directions of one key screen, as the skill describes, unless the user asked you to just build it. Once the first HTML file is written, or a dev server is up, call preview_page so the user can watch the design live in the Design Studio sidebar; later edits refresh on their own. After writing or changing screens, call check_design to measure them at real sizes and fix what it lists, and call look_design to see real screenshots for the visual pass; do not build your own inspection pages, servers or browser screenshots. Stop after at most three measure-and-fix rounds and tell the user what is left. When a message starts with an element or region the user selected in the preview, or with a design-check list, change exactly those parts in the source. If the user points you at their own website or project as the brand to follow, call extract_design_system; when the ui-design skill names a default design system, build new designs on its tokens.css and components.css. When a message asks to turn the design into real code, call handoff_design, follow its HANDOFF.md, and after each screen call compare_design until it matches. If the user only wants an answer shown as a page (a comparison, table, checklist, chart or calculator) and the show_page tool is available, call show_page instead of this design process.
|
|
31
31
|
- id: agent-instructions
|
|
32
32
|
name: '@deepseek-ai/dsh-agent-instructions'
|
|
33
33
|
config:
|
package/lib/browser.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 无头浏览器:check_design / look_design 在电脑上另开一个看不见的 Chrome(或 Edge、Chromium、Brave),
|
|
3
|
+
* 按指定尺寸打开预览页面,量问题、截真实像素的图。和右侧栏开没开、用户在看哪一屏、画布怎么摆都无关。
|
|
4
|
+
*
|
|
5
|
+
* 用 --remote-debugging-pipe 和它说话(不开端口,别的程序连不进来);临时用户目录,用完删掉;
|
|
6
|
+
* 闲置一会儿自己关,插件停用时一起关。找不到浏览器 → available() 是 false,调用的地方退回右侧栏量。
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import os from 'node:os'
|
|
11
|
+
import path from 'node:path'
|
|
12
|
+
import { spawn, execFile } from 'node:child_process'
|
|
13
|
+
|
|
14
|
+
const IDLE_MS = 90000 // 闲置多久关掉
|
|
15
|
+
const CALL_MS = 30000 // 一条指令最多等多久
|
|
16
|
+
const LOAD_MS = 15000 // 页面载入最多等多久(到点照样往下走:有个图一直载不完也能量)
|
|
17
|
+
const MAX_PAGES = 3 // 同时开几个页面
|
|
18
|
+
|
|
19
|
+
/** 这台电脑上的 Chromium 系浏览器;DSH_DESIGN_BROWSER / CHROME_BIN 可以指定 */
|
|
20
|
+
export function findBrowser({ env = process.env, platform = process.platform, exists = fs.existsSync, home = os.homedir() } = {}) {
|
|
21
|
+
const roots = [env.PROGRAMFILES, env['PROGRAMFILES(X86)'], env.LOCALAPPDATA].filter(Boolean)
|
|
22
|
+
const mac = (app, bin) => [`/Applications/${app}.app/Contents/MacOS/${bin}`, `${home}/Applications/${app}.app/Contents/MacOS/${bin}`]
|
|
23
|
+
const list = {
|
|
24
|
+
darwin: [...mac('Google Chrome', 'Google Chrome'), ...mac('Microsoft Edge', 'Microsoft Edge'), ...mac('Chromium', 'Chromium'), ...mac('Brave Browser', 'Brave Browser')],
|
|
25
|
+
win32: roots.flatMap((d) => [`${d}\\Google\\Chrome\\Application\\chrome.exe`, `${d}\\Microsoft\\Edge\\Application\\msedge.exe`, `${d}\\BraveSoftware\\Brave-Browser\\Application\\brave.exe`]),
|
|
26
|
+
linux: ['/usr/bin/google-chrome', '/usr/bin/google-chrome-stable', '/usr/bin/chromium', '/usr/bin/chromium-browser', '/snap/bin/chromium', '/usr/bin/microsoft-edge', '/usr/bin/brave-browser'],
|
|
27
|
+
}
|
|
28
|
+
for (const p of [env.DSH_DESIGN_BROWSER, env.CHROME_BIN, ...(list[platform] || [])]) {
|
|
29
|
+
if (!p) continue
|
|
30
|
+
try { if (exists(p)) return p } catch { /* 读不了就下一个 */ }
|
|
31
|
+
}
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** CDP 管道上的消息:JSON 后面跟一个 \0。一块数据里可能有半条、也可能有好几条 */
|
|
36
|
+
export function frameReader(onMsg) {
|
|
37
|
+
let buf = Buffer.alloc(0)
|
|
38
|
+
return (chunk) => {
|
|
39
|
+
buf = buf.length ? Buffer.concat([buf, chunk]) : chunk
|
|
40
|
+
let i
|
|
41
|
+
while ((i = buf.indexOf(0)) >= 0) {
|
|
42
|
+
const text = buf.subarray(0, i).toString('utf8')
|
|
43
|
+
buf = buf.subarray(i + 1)
|
|
44
|
+
let msg
|
|
45
|
+
try { msg = JSON.parse(text) } catch { continue }
|
|
46
|
+
onMsg(msg)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const PREFIX = 'dsh-design-browser-'
|
|
52
|
+
const STALE_MS = 3600 * 1000 // 一小时没动过的临时用户目录:上回没删掉的(DSH 被强退、插件进程直接退出)
|
|
53
|
+
let swept = false
|
|
54
|
+
|
|
55
|
+
/** 清掉以前留下的临时用户目录(每个进程只扫一次;正在用的浏览器闲置 90 秒就关,不会一小时不动) */
|
|
56
|
+
function sweep(dir = os.tmpdir(), now = Date.now()) {
|
|
57
|
+
if (swept) return
|
|
58
|
+
swept = true
|
|
59
|
+
let names = []
|
|
60
|
+
try { names = fs.readdirSync(dir) } catch { return }
|
|
61
|
+
for (const n of names) {
|
|
62
|
+
if (!n.startsWith(PREFIX)) continue
|
|
63
|
+
const p = path.join(dir, n)
|
|
64
|
+
try { if (now - fs.statSync(p).mtimeMs > STALE_MS) fs.rmSync(p, { recursive: true, force: true }) } catch { /* 下次再说 */ }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function createBrowser({ bin = findBrowser(), log = () => {}, idleMs = IDLE_MS, launch = spawn, platform = process.platform } = {}) {
|
|
69
|
+
let proc = null
|
|
70
|
+
let starting = null
|
|
71
|
+
let dir = ''
|
|
72
|
+
let seq = 0
|
|
73
|
+
let open = 0
|
|
74
|
+
let idle = null
|
|
75
|
+
let dead = false // destroy() 之后:不再开浏览器,排队的、新来的都直接失败
|
|
76
|
+
const pending = new Map() // 指令 id → { resolve, reject, timer }
|
|
77
|
+
const waiters = new Set() // 等事件的:{ method, sessionId, done }
|
|
78
|
+
const queue = [] // 等空位开页面的
|
|
79
|
+
|
|
80
|
+
const fail = (err) => {
|
|
81
|
+
for (const p of pending.values()) { clearTimeout(p.timer); p.reject(err) }
|
|
82
|
+
pending.clear()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const onMsg = (msg) => {
|
|
86
|
+
if (msg.id) {
|
|
87
|
+
const p = pending.get(msg.id)
|
|
88
|
+
if (!p) return
|
|
89
|
+
pending.delete(msg.id)
|
|
90
|
+
clearTimeout(p.timer)
|
|
91
|
+
if (msg.error) p.reject(new Error(msg.error.message || 'cdp error'))
|
|
92
|
+
else p.resolve(msg.result || {})
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
for (const w of [...waiters]) if (w.method === msg.method && (!w.sessionId || w.sessionId === msg.sessionId)) w.done(msg.params || {})
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function call(method, params = {}, sessionId) {
|
|
99
|
+
if (!proc) return Promise.reject(new Error('browser not running'))
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const id = ++seq
|
|
102
|
+
const timer = setTimeout(() => { if (pending.delete(id)) reject(new Error(`${method} timeout`)) }, CALL_MS)
|
|
103
|
+
pending.set(id, { resolve, reject, timer })
|
|
104
|
+
try { proc.stdio[3].write(`${JSON.stringify({ id, method, params, ...(sessionId ? { sessionId } : {}) })}\0`) } catch (e) { pending.delete(id); clearTimeout(timer); reject(e) }
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** 等一个事件(到点就当等到了,返回 null) */
|
|
109
|
+
function once(method, sessionId, ms) {
|
|
110
|
+
let w
|
|
111
|
+
const p = new Promise((resolve) => {
|
|
112
|
+
const timer = setTimeout(() => { waiters.delete(w); resolve(null) }, ms)
|
|
113
|
+
w = { method, sessionId, done: (v) => { clearTimeout(timer); waiters.delete(w); resolve(v) } }
|
|
114
|
+
waiters.add(w)
|
|
115
|
+
})
|
|
116
|
+
return p
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** 一直听某个事件(不像 once 只等一次);返回取消的函数 */
|
|
120
|
+
function listen(method, sessionId, fn) {
|
|
121
|
+
const w = { method, sessionId, done: (v) => { if (v) fn(v) } }
|
|
122
|
+
waiters.add(w)
|
|
123
|
+
return () => waiters.delete(w)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// 宿主进程退出时浏览器来不及报「退出了」:那时同步删掉临时目录
|
|
127
|
+
let onHostExit = null
|
|
128
|
+
function cleanup() {
|
|
129
|
+
if (onHostExit) { process.removeListener('exit', onHostExit); onHostExit = null }
|
|
130
|
+
proc = null
|
|
131
|
+
starting = null
|
|
132
|
+
fail(new Error('browser exited'))
|
|
133
|
+
for (const w of [...waiters]) w.done(null) // 等页面载入的不用再等了
|
|
134
|
+
const d = dir
|
|
135
|
+
dir = ''
|
|
136
|
+
// 自己建的临时用户目录:浏览器退了马上删(Windows 上进程刚退时文件可能还占着,隔一会儿再试,最多三次;
|
|
137
|
+
// 还删不掉的,下次开浏览器时按时间清掉)
|
|
138
|
+
if (d) {
|
|
139
|
+
let tries = 0
|
|
140
|
+
const rm = () => {
|
|
141
|
+
try { fs.rmSync(d, { recursive: true, force: true }) } catch (e) {
|
|
142
|
+
if (++tries < 4) setTimeout(rm, 2000).unref?.()
|
|
143
|
+
else log(new Error(`临时目录删不掉,留在 ${d}:${e && e.message}`))
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
rm()
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function start() {
|
|
151
|
+
if (proc) return
|
|
152
|
+
if (starting) return starting
|
|
153
|
+
if (!bin) throw new Error('no-browser')
|
|
154
|
+
if (dead) throw new Error('browser closed')
|
|
155
|
+
sweep()
|
|
156
|
+
starting = (async () => {
|
|
157
|
+
dir = fs.mkdtempSync(path.join(os.tmpdir(), PREFIX))
|
|
158
|
+
const d = dir
|
|
159
|
+
onHostExit = () => { try { fs.rmSync(d, { recursive: true, force: true }) } catch { /* 下次开浏览器时按时间清 */ } }
|
|
160
|
+
process.once('exit', onHostExit)
|
|
161
|
+
const args = ['--headless=new', '--remote-debugging-pipe', `--user-data-dir=${dir}`, '--no-first-run', '--no-default-browser-check',
|
|
162
|
+
'--disable-extensions', '--disable-background-networking', '--disable-sync', '--disable-default-apps', '--disable-component-update',
|
|
163
|
+
'--mute-audio', '--hide-scrollbars', '--force-color-profile=srgb',
|
|
164
|
+
...(platform === 'darwin' ? ['--use-mock-keychain'] : []),
|
|
165
|
+
...(platform === 'linux' && process.getuid && process.getuid() === 0 ? ['--no-sandbox'] : []),
|
|
166
|
+
'about:blank']
|
|
167
|
+
const p = launch(bin, args, { stdio: ['ignore', 'ignore', 'ignore', 'pipe', 'pipe'], windowsHide: true })
|
|
168
|
+
proc = p
|
|
169
|
+
p.on('exit', () => { if (proc === p) cleanup() })
|
|
170
|
+
p.on('error', (e) => { log(e); if (proc === p) cleanup() })
|
|
171
|
+
p.stdio[4].on('data', frameReader(onMsg))
|
|
172
|
+
p.stdio[3].on('error', () => {})
|
|
173
|
+
await call('Browser.getVersion')
|
|
174
|
+
})()
|
|
175
|
+
try { await starting } catch (e) { stop(); throw e } finally { starting = null }
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function stop() {
|
|
179
|
+
clearTimeout(idle)
|
|
180
|
+
const p = proc
|
|
181
|
+
if (!p) return
|
|
182
|
+
try { p.stdio[3].end() } catch { /* 已经断了 */ }
|
|
183
|
+
if (platform === 'win32') {
|
|
184
|
+
execFile('taskkill', ['/pid', String(p.pid), '/T', '/F'], (err) => {
|
|
185
|
+
if (!err) return
|
|
186
|
+
log(err)
|
|
187
|
+
try { p.kill() } catch { /* 已经退了 */ }
|
|
188
|
+
})
|
|
189
|
+
} else { try { p.kill('SIGKILL') } catch { /* 已经退了 */ } }
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/** 插件停用:关掉浏览器,排队等页面的都失败,之后再也不开 */
|
|
193
|
+
function destroy() {
|
|
194
|
+
dead = true
|
|
195
|
+
for (const next of queue.splice(0)) next()
|
|
196
|
+
stop()
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const touch = () => {
|
|
200
|
+
clearTimeout(idle)
|
|
201
|
+
if (!open) idle = setTimeout(() => { if (!open) stop() }, idleMs)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 开一个 w×h 的页面载入 url,fn(page) 里量 / 截图,完了关掉。
|
|
206
|
+
* page.eval(表达式):在页面里算(Promise 会等),返回 JSON 值
|
|
207
|
+
* page.shot({ clip, format }):截视口;clip({ x, y, width, height },CSS 像素,按整页坐标 = 视口位置 + 滚动距离)只截那一块 → JPEG Buffer(format: 'png' 无损)
|
|
208
|
+
* allow(网址):给了就只让它说行的页面载入(主页面、iframe,连跳转、脚本改地址都算),别的拦下,记在 page.blocked
|
|
209
|
+
*/
|
|
210
|
+
async function withPage({ url, w, h, scale = 1, dark = false, allow = null }, fn) {
|
|
211
|
+
if (dead) throw new Error('browser closed')
|
|
212
|
+
if (open >= MAX_PAGES) await new Promise((r) => queue.push(r))
|
|
213
|
+
if (dead) throw new Error('browser closed')
|
|
214
|
+
open++
|
|
215
|
+
clearTimeout(idle)
|
|
216
|
+
let targetId = ''
|
|
217
|
+
let unlisten = null
|
|
218
|
+
const blocked = []
|
|
219
|
+
try {
|
|
220
|
+
await start()
|
|
221
|
+
;({ targetId } = await call('Target.createTarget', { url: 'about:blank' }))
|
|
222
|
+
const { sessionId } = await call('Target.attachToTarget', { targetId, flatten: true })
|
|
223
|
+
const c = (m, p) => call(m, p, sessionId)
|
|
224
|
+
if (allow) {
|
|
225
|
+
// 每个页面请求(包括 302 之后的每一跳)先停下来问一遍,不行就当被拦了
|
|
226
|
+
unlisten = listen('Fetch.requestPaused', sessionId, (q) => {
|
|
227
|
+
let ok = false
|
|
228
|
+
try { ok = !!allow(q.request.url) } catch { ok = false }
|
|
229
|
+
if (!ok && blocked.length < 10) blocked.push(q.request.url)
|
|
230
|
+
c(ok ? 'Fetch.continueRequest' : 'Fetch.failRequest', ok ? { requestId: q.requestId } : { requestId: q.requestId, errorReason: 'BlockedByClient' }).catch(() => {})
|
|
231
|
+
})
|
|
232
|
+
await c('Fetch.enable', { patterns: [{ urlPattern: '*', resourceType: 'Document', requestStage: 'Request' }] })
|
|
233
|
+
}
|
|
234
|
+
await c('Page.enable')
|
|
235
|
+
// 和右侧栏里一样按桌面方式排版(mobile: false):页面宽就是 w,不看 <meta viewport>
|
|
236
|
+
await c('Emulation.setDeviceMetricsOverride', { width: w, height: h, deviceScaleFactor: scale, mobile: false })
|
|
237
|
+
await c('Emulation.setEmulatedMedia', { features: [{ name: 'prefers-color-scheme', value: dark ? 'dark' : 'light' }, { name: 'prefers-reduced-motion', value: 'reduce' }] })
|
|
238
|
+
const loaded = once('Page.loadEventFired', sessionId, LOAD_MS)
|
|
239
|
+
const nav = await c('Page.navigate', { url })
|
|
240
|
+
if (nav.errorText) throw new Error(blocked.length ? `不是允许的地址,没打开:${blocked[0].slice(0, 120)}` : nav.errorText)
|
|
241
|
+
await loaded
|
|
242
|
+
const page = {
|
|
243
|
+
eval: async (expression) => {
|
|
244
|
+
const r = await c('Runtime.evaluate', { expression, awaitPromise: true, returnByValue: true })
|
|
245
|
+
if (r.exceptionDetails) throw new Error((r.exceptionDetails.exception && r.exceptionDetails.exception.description) || r.exceptionDetails.text || 'eval failed')
|
|
246
|
+
return r.result ? r.result.value : undefined
|
|
247
|
+
},
|
|
248
|
+
shot: async ({ clip, quality = 82, format = 'jpeg' } = {}) => {
|
|
249
|
+
const r = await c('Page.captureScreenshot', { format, ...(format === 'jpeg' ? { quality } : {}), ...(clip ? { clip: { ...clip, scale: 1 } } : {}) })
|
|
250
|
+
return Buffer.from(r.data, 'base64')
|
|
251
|
+
},
|
|
252
|
+
blocked,
|
|
253
|
+
}
|
|
254
|
+
return await fn(page)
|
|
255
|
+
} finally {
|
|
256
|
+
if (unlisten) unlisten()
|
|
257
|
+
if (targetId && proc) call('Target.closeTarget', { targetId }).catch(() => {})
|
|
258
|
+
open--
|
|
259
|
+
const next = queue.shift()
|
|
260
|
+
if (next) next()
|
|
261
|
+
touch()
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
return {
|
|
266
|
+
available: () => !!bin,
|
|
267
|
+
bin: () => bin,
|
|
268
|
+
withPage,
|
|
269
|
+
stop,
|
|
270
|
+
destroy,
|
|
271
|
+
running: () => !!proc,
|
|
272
|
+
}
|
|
273
|
+
}
|