dsh-design-studio 0.10.0 → 0.12.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 +4 -3
- package/README.md +6 -5
- package/assets/demo.gif +0 -0
- package/assets/hero-sm.jpg +0 -0
- package/cordis.patch.yml +5 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/_shots/menu.jpg +0 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/app.css +64 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/cart.html +120 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/design.json +56 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/menu.html +200 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/order.html +135 -0
- package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/tokens.css +43 -0
- package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/_shots/email.jpg +0 -0
- package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/design.json +20 -0
- package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/email.html +268 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/_shots/courses.jpg +0 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/courses.html +364 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/design.json +49 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/lesson.html +196 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/tokens.css +55 -0
- package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/ui.css +255 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/_shots/poster.jpg +0 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/design.json +21 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/poster.html +165 -0
- package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/tokens.css +35 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/_shots/home.jpg +0 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/app.css +155 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/booking.html +232 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/design.json +57 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/health.html +221 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/home.html +225 -0
- package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/tokens.css +54 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/_shots/overview.jpg +0 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/app.css +79 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/design.json +51 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/entries.html +161 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/icons.svg +30 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/overview.html +181 -0
- package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/tokens.css +50 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/_shots/home.jpg +0 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/book.html +279 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/design.json +21 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/home.html +348 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/style.css +297 -0
- package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/tokens.css +49 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/01-cover.html +133 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/02-highlights.html +67 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/03-growth.html +89 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/04-milestones.html +92 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/05-next.html +99 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/06-thanks.html +61 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/_shots/01-cover.jpg +0 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/deck.css +67 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/design.json +25 -0
- package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/tokens.css +30 -0
- package/gallery-seed/index.json +1 -0
- package/gallery-seed/thumbs/orderg-x__alley-noodles.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__autumn-sale.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__little-painter.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__moon-fair.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__paw-diary.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__pocket-ledger.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__time-bookshop.webp +0 -0
- package/gallery-seed/thumbs/orderg-x__year-review.webp +0 -0
- package/lib/browser.js +23 -9
- package/lib/bundle.js +12 -3
- package/lib/client.js +3253 -483
- package/lib/core.js +231 -7
- package/lib/draft-page.js +763 -0
- package/lib/draft.js +151 -0
- package/lib/gallery/css.js +230 -0
- package/lib/gallery/html.js +313 -0
- package/lib/gallery/index.js +22 -0
- package/lib/gallery/market-routes.js +306 -0
- package/lib/gallery/market.js +623 -0
- package/lib/gallery/pack.js +311 -0
- package/lib/gallery/policy.js +1560 -0
- package/lib/gallery/qrfind.js +181 -0
- package/lib/gallery/render.js +148 -0
- package/lib/gallery/routes.js +360 -0
- package/lib/gallery/tar.js +118 -0
- package/lib/gallery/use.js +125 -0
- package/lib/gallery/zip.js +212 -0
- package/lib/handoff.js +190 -36
- package/lib/history.js +3 -1
- package/lib/index.js +123 -22
- package/lib/inspire.js +142 -0
- package/lib/offline.js +17 -0
- package/lib/picker.js +260 -1
- package/lib/serve.js +264 -11
- package/lib/tweaks.js +58 -24
- package/lib/works.js +238 -0
- package/locale/en.json +2 -1
- package/locale/offline/en.json +6 -0
- package/locale/offline/zh.json +6 -0
- package/locale/zh.json +2 -1
- package/package.json +9 -2
- package/skills/ui-design/SKILL.md +9 -0
package/lib/index.js
CHANGED
|
@@ -40,14 +40,21 @@
|
|
|
40
40
|
* 页面本身由 serve.js 开的独立端口提供(见那边的说明)。
|
|
41
41
|
*/
|
|
42
42
|
|
|
43
|
+
import crypto from 'node:crypto'
|
|
43
44
|
import fs from 'node:fs'
|
|
44
45
|
import path from 'node:path'
|
|
45
46
|
import { fileURLToPath } from 'node:url'
|
|
46
47
|
import os from 'node:os'
|
|
47
48
|
import { execFile } from 'node:child_process'
|
|
48
|
-
import { createHub, webOrigins, trustedOrigins, resolveInRoot, ERR_CATCHER } from './serve.js'
|
|
49
|
-
import { bundle, inlinePicker, MAX_TOTAL as BUNDLE_MAX } from './bundle.js'
|
|
49
|
+
import { createHub, webOrigins, trustedOrigins, resolveInRoot, ERR_CATCHER, defang } from './serve.js'
|
|
50
|
+
import { bundle, inlinePicker, strictMeta, MAX_TOTAL as BUNDLE_MAX } from './bundle.js'
|
|
51
|
+
import { createLibrary } from './works.js'
|
|
52
|
+
import { createInspiration } from './inspire.js'
|
|
53
|
+
import { createLibraryRoutes } from './gallery/routes.js'
|
|
54
|
+
import { createMarket } from './gallery/market.js'
|
|
55
|
+
import { createMarketRoutes } from './gallery/market-routes.js'
|
|
50
56
|
import { createTracker, parseTarget, findRule, KINDS } from './core.js'
|
|
57
|
+
import { createDrafts } from './draft.js'
|
|
51
58
|
import { createHistory } from './history.js'
|
|
52
59
|
import { createPhone } from './phone.js'
|
|
53
60
|
import { qrMatrix } from './qr.js'
|
|
@@ -82,7 +89,7 @@ const SKILL_RANK = 600 // 和 DSH 自带的技能同一档;用户 / 项目里
|
|
|
82
89
|
* 几行之间靠这个互通(同一个包里的模块 Node 只加载一次):工作台起来后放 tracker、无头浏览器(browser);
|
|
83
90
|
* 「自动打开预览」那行开着时 autoOpen > 0
|
|
84
91
|
*/
|
|
85
|
-
export const studio = { tracker: null, autoOpen: 0, check: null, browser: null, log: () => {} }
|
|
92
|
+
export const studio = { tracker: null, autoOpen: 0, offline: 0, check: null, browser: null, market: null, log: () => {} }
|
|
86
93
|
|
|
87
94
|
/** 设计技能:模型做界面设计时加载(DSH 的 skill 工具),也能让用户手动调 */
|
|
88
95
|
export const designSkill = {
|
|
@@ -439,10 +446,10 @@ export const handoffTool = {
|
|
|
439
446
|
name: 'handoff_design',
|
|
440
447
|
description:
|
|
441
448
|
'把右侧「设计工作台」里的设计(本地 HTML 那几屏)整理成交接包,准备做成真项目的代码:写好 <设计目录>/handoff/HANDOFF.md(每屏的文件、建议路由、跳转、设计变量、几屏共用该抽成组件的部分、交互和状态、调节滑块、素材、做法和验收),和每屏从上到下的截图。' +
|
|
442
|
-
'用户说「转成代码」「做成真的项目」「用 React / Vue
|
|
443
|
-
'framework:same
|
|
449
|
+
'用户说「转成代码」「做成真的项目」「用 React / Vue / uni-app 做出来」「做成小程序」时先调它,读 HANDOFF.md 照着做;每做完一屏用 compare_design 和设计稿比一遍,差异降到 3% 以下再做下一屏(原生小程序在浏览器里跑不起来,照 HANDOFF.md 说的请用户在微信开发者工具里看)。' +
|
|
450
|
+
'framework:same 跟目标目录现有项目一样(认得出框架时默认,uni-app(含 HBuilderX 建的、没有 package.json 的)、Taro、微信原生小程序也认得出)、react、vue、next、html、uniapp、weapp;dest:代码放哪个目录(不填:会话目录是项目就放进去,不是就新建 <设计名>-app)。要先用 preview_page 打开过设计。',
|
|
444
451
|
parameters: specToJsonSchema({
|
|
445
|
-
framework: { type: 'string', enum: ['same', ...Object.keys(FRAMEWORKS)], description: 'same 跟现有项目一样;react(React + Vite)、vue(Vue 3 + Vite)、next(Next.js)、html(纯 HTML / CSS)。不填:目标目录(或它所在的项目)认得出框架就 same,没有 package.json 的纯 HTML 站点 html,别的 react' },
|
|
452
|
+
framework: { type: 'string', enum: ['same', ...Object.keys(FRAMEWORKS)], description: 'same 跟现有项目一样;react(React + Vite)、vue(Vue 3 + Vite)、next(Next.js)、html(纯 HTML / CSS)、uniapp(uni-app,出微信小程序和网页版)、weapp(微信原生小程序 WXML / WXSS)。不填:目标目录(或它所在的项目)认得出框架就 same,没有 package.json 的纯 HTML 站点 html,小程序的设计 uniapp,别的 react' },
|
|
446
453
|
dest: { type: 'string', description: '代码放哪:现有项目的目录(或它里面的子目录),或要新建的项目目录(绝对路径或相对会话目录)。不填:会话目录有 package.json 就放进去,没有就新建 <设计名>-app(设计目录在会话目录下就建在会话目录里,不然建在设计目录旁边)' },
|
|
447
454
|
screens: { type: 'array', items: { type: 'string' }, description: '只转哪几屏(文件名);不填全部' },
|
|
448
455
|
}),
|
|
@@ -470,15 +477,17 @@ export const handoffTool = {
|
|
|
470
477
|
const dest = typeof a.dest === 'string' && a.dest.trim() ? resolvePath(a.dest, cwd) : defaultDest(t.root, cwd, detectProject(cwd))
|
|
471
478
|
if (within(dest, t.root)) return { text: `代码别放进设计目录(${t.root})里:设计稿留着做对比。换一个目录。` }
|
|
472
479
|
const project = projectFor(dest)
|
|
473
|
-
const framework = typeof a.framework === 'string' && (a.framework === 'same' || FRAMEWORKS[a.framework]) ? a.framework : defaultFramework(project)
|
|
474
|
-
if (framework === 'same' && !
|
|
480
|
+
const framework = typeof a.framework === 'string' && (a.framework === 'same' || FRAMEWORKS[a.framework]) ? a.framework : defaultFramework(project, kind)
|
|
481
|
+
if (framework === 'same' && !project.framework) return { text: `${dest} 里没有认得出的前端项目(package.json 里没有 React / Vue / Next / uni-app …,也不是 HBuilderX 或微信原生小程序的项目):framework 选 react、vue、next、html、uniapp 或 weapp。` }
|
|
475
482
|
let r
|
|
476
483
|
try {
|
|
477
484
|
r = await buildHandoff(studio.browser, { root: t.root, screens: pick.list, kind, size, framework, dest, project, system: systemOfDir(t.root) })
|
|
478
485
|
} catch (e) {
|
|
479
486
|
return { text: `没写成交接包:${String((e && e.message) || e).slice(0, 200)}` }
|
|
480
487
|
}
|
|
481
|
-
|
|
488
|
+
// 原生小程序跑不了网页版:比不了,照 HANDOFF.md 请用户在微信开发者工具里看
|
|
489
|
+
const native = framework === 'weapp' || (framework === 'same' && project.framework === 'weapp')
|
|
490
|
+
const head = `交接包写好了:${r.file}(${r.shots} 张截图在 handoff/shots/)。下面是全文,照「做法和验收」一屏一屏做,${native ? '每屏做完请用户在微信开发者工具里看' : '每屏做完用 compare_design 比'}。\n\n`
|
|
482
491
|
// 每屏顶上一张附上,直接看到要做成什么样
|
|
483
492
|
const firsts = r.rows.filter((row) => row.shots.length).slice(0, 4).map((row) => ({ key: row.key, jpg: fs.readFileSync(path.join(r.dir, 'shots', row.shots[0])) }))
|
|
484
493
|
const kept = firsts.length ? await keepImages(ctx, id, firsts) : {}
|
|
@@ -569,6 +578,8 @@ async function keepImages(ctx, id, list, toFile = false) {
|
|
|
569
578
|
return { files }
|
|
570
579
|
}
|
|
571
580
|
|
|
581
|
+
const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '' } catch { return '' } })()
|
|
582
|
+
|
|
572
583
|
const fileStore = {
|
|
573
584
|
load: () => JSON.parse(fs.readFileSync(STORE_FILE, 'utf8')),
|
|
574
585
|
save: (data) => {
|
|
@@ -641,9 +652,18 @@ export function apply(ctx) {
|
|
|
641
652
|
const previewPorts = new Set()
|
|
642
653
|
let tracker = null
|
|
643
654
|
const history = createHistory({ base: HISTORY_DIR, log })
|
|
655
|
+
// 我的作品 + 哪些文件是别人做的(设计库来的):别人做的、没改过的页面预览时加硬规矩
|
|
656
|
+
const library = createLibrary({ log })
|
|
657
|
+
// 设计市场:目录和批次包从 npm 镜像来,验过签名、逐个文件核对过才用;市场上的页面原样预览时也加硬规矩
|
|
658
|
+
const seedDir = fileURLToPath(new URL('../gallery-seed', import.meta.url))
|
|
659
|
+
// 「设计库不联网」那行开着:市场不访问任何网址
|
|
660
|
+
const market = createMarket({ version: VERSION, log, seedDir: fs.existsSync(path.join(seedDir, 'index.json')) ? seedDir : '', online: () => studio.offline === 0 })
|
|
661
|
+
const drafts = createDrafts({ log }) // 正在写的页面(边写边看):tracker 往里推,预览服务器往外吐
|
|
644
662
|
const hub = createHub({
|
|
645
663
|
log,
|
|
646
664
|
history,
|
|
665
|
+
drafts,
|
|
666
|
+
strict: { html: (buf) => library.strictHtml(buf) || market.strictHtml(buf), root: (dir) => library.strictRoot(dir) || market.isStaging(dir), versions: (dir) => library.strictVersions(dir) },
|
|
647
667
|
// 桌面版的 dsh-app://app 是默认就认的;这里补上 DSH 网页端的地址,和 DSH 配置里声明的对外地址(反向代理 / 域名)
|
|
648
668
|
parents: () => {
|
|
649
669
|
let own = []
|
|
@@ -662,11 +682,15 @@ export function apply(ctx) {
|
|
|
662
682
|
}
|
|
663
683
|
tracker = createTracker({
|
|
664
684
|
open,
|
|
685
|
+
drafts,
|
|
665
686
|
log,
|
|
666
687
|
history,
|
|
667
|
-
|
|
688
|
+
// 记会话的同时,在做的设计目录补进「我的作品」
|
|
689
|
+
store: { load: fileStore.load, save: (data) => { fileStore.save(data); try { library.sync(data) } catch (e) { log(e) } } },
|
|
668
690
|
alive: (origin) => hub.alive(origin),
|
|
669
691
|
autoOpen: () => studio.autoOpen > 0,
|
|
692
|
+
// 装好的别人的设计系统退回旧版:退回来的页面先记成别人做的(预览照样加规矩)
|
|
693
|
+
beforeRestore: (root, html) => { if (html.length) library.addPast(root, html) },
|
|
670
694
|
skipPorts: () => {
|
|
671
695
|
const s = new Set(previewPorts)
|
|
672
696
|
const own = ownHost(ctx).split(':')[1]
|
|
@@ -674,11 +698,15 @@ export function apply(ctx) {
|
|
|
674
698
|
return s
|
|
675
699
|
},
|
|
676
700
|
})
|
|
677
|
-
ctx.effect(() => () => hub.closeAll(), 'dsh-design-studio: preview servers')
|
|
701
|
+
ctx.effect(() => () => { drafts.clear(); hub.closeAll() }, 'dsh-design-studio: preview servers')
|
|
678
702
|
ctx.effect(() => {
|
|
679
703
|
studio.tracker = tracker
|
|
680
704
|
studio.log = log
|
|
681
|
-
|
|
705
|
+
studio.market = market
|
|
706
|
+
return () => {
|
|
707
|
+
if (studio.tracker === tracker) { studio.tracker = null; studio.log = () => {} }
|
|
708
|
+
if (studio.market === market) { market.abort(); studio.market = null }
|
|
709
|
+
}
|
|
682
710
|
}, 'dsh-design-studio: tracker')
|
|
683
711
|
// check_design / look_design 用的无头浏览器:用到时才开,闲置自己关,这一行停用时关掉
|
|
684
712
|
ctx.effect(() => {
|
|
@@ -746,10 +774,36 @@ export function apply(ctx) {
|
|
|
746
774
|
const t = tracker.stateOf(session).target
|
|
747
775
|
let tweaks = null
|
|
748
776
|
if (t && t.kind === 'file') { try { tweaks = readTweaks(t.root) } catch (e) { log(e) } }
|
|
749
|
-
|
|
777
|
+
const snap = tracker.snapshot(session)
|
|
778
|
+
json(res, 200, { ...snap, strict: strictScreens(snap), phone: { on: ph.on, seen: ph.seen || 0 }, tweaks, ...(c ? { checkReq: c.id } : {}) })
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/** 正在看的几屏里,哪些是别人做的、没改过的(地址列表):右侧栏给它们的 iframe 加沙盒、不给剪贴板 */
|
|
782
|
+
const strictCache = new Map() // 文件 → { sig, strict }
|
|
783
|
+
function strictScreens(snap) {
|
|
784
|
+
const t = snap.target
|
|
785
|
+
if (!t || t.kind !== 'file' || !Array.isArray(snap.screens)) return []
|
|
786
|
+
const out = []
|
|
787
|
+
const files = new Set([...snap.screens.map((s) => s.key), snap.focus].filter(Boolean))
|
|
788
|
+
for (const key of files) {
|
|
789
|
+
const file = path.join(t.root, key)
|
|
790
|
+
let st
|
|
791
|
+
try { st = fs.statSync(file) } catch { continue }
|
|
792
|
+
const sig = `${st.mtimeMs}:${st.size}:${library.file && fs.existsSync(library.file) ? fs.statSync(library.file).mtimeMs : 0}`
|
|
793
|
+
let hit = strictCache.get(file)
|
|
794
|
+
if (!hit || hit.sig !== sig) {
|
|
795
|
+
let strict = false
|
|
796
|
+
try { strict = st.size <= 8 * 1024 * 1024 && library.strictHtml(fs.readFileSync(file)) } catch { strict = false }
|
|
797
|
+
hit = { sig, strict }
|
|
798
|
+
strictCache.set(file, hit)
|
|
799
|
+
if (strictCache.size > 500) strictCache.delete(strictCache.keys().next().value)
|
|
800
|
+
}
|
|
801
|
+
if (hit.strict) out.push(`${snap.origin}/${encodeURIComponent(key)}`)
|
|
802
|
+
}
|
|
803
|
+
return out
|
|
750
804
|
}
|
|
751
805
|
|
|
752
|
-
/** 调节滑块写回文件:values = { id: 值 } */
|
|
806
|
+
/** 调节滑块写回文件:values = { id: 值 };外观面板里直接改的设计变量:vars = { '--名字': 值 } */
|
|
753
807
|
async function tweakRoute(req, res) {
|
|
754
808
|
if (!admit(req, res)) return
|
|
755
809
|
if (req.method !== 'POST') return json(res, 405, { error: 'method not allowed' })
|
|
@@ -757,7 +811,8 @@ export function apply(ctx) {
|
|
|
757
811
|
try { body = await readBody(req) } catch (e) { return json(res, 400, { error: e.message }) }
|
|
758
812
|
const values = body.values && typeof body.values === 'object' && !Array.isArray(body.values) ? body.values : {}
|
|
759
813
|
try {
|
|
760
|
-
const
|
|
814
|
+
const vars = body.vars && typeof body.vars === 'object' && !Array.isArray(body.vars) ? body.vars : null
|
|
815
|
+
const r = tracker.editDir(String(body.session || ''), (root) => applyTweaks(root, values, vars), (x) => `调节:${x.changed.map((c) => `${c.label} ${c.to}`).join('、')}`.slice(0, 60), String(body.root || ''))
|
|
761
816
|
json(res, 200, r)
|
|
762
817
|
} catch (e) {
|
|
763
818
|
json(res, 500, { error: e.message })
|
|
@@ -806,11 +861,12 @@ export function apply(ctx) {
|
|
|
806
861
|
}
|
|
807
862
|
|
|
808
863
|
/** 转成代码前:会话目录是不是个前端项目(右侧栏默认选「跟项目一样」、填好放哪) */
|
|
809
|
-
function projectRoute(req, res) {
|
|
864
|
+
async function projectRoute(req, res) {
|
|
810
865
|
if (!admit(req, res)) return
|
|
811
866
|
const q = new URL(req.url, 'http://x').searchParams
|
|
812
867
|
const session = q.get('session') || ''
|
|
813
|
-
|
|
868
|
+
// 新会话(还没跑过工具,比如从设计库「用这个开始」、浏览打开的)插件不知道目录:问 DSH,别拿家目录当项目找
|
|
869
|
+
const cwd = await sessionCwd(session)
|
|
814
870
|
const p = detectProject(cwd)
|
|
815
871
|
const t = tracker.stateOf(session).target
|
|
816
872
|
const root = t && t.kind === 'file' ? t.root : ''
|
|
@@ -820,7 +876,9 @@ export function apply(ctx) {
|
|
|
820
876
|
const pk = picked ? pickedDest(resolvePath(picked, cwd), root) : null
|
|
821
877
|
const dest = pk ? pk.dest : defaultDest(root, cwd, p)
|
|
822
878
|
const project = projectFor(dest)
|
|
823
|
-
|
|
879
|
+
// 设计类型跟着回去:小程序的设计右侧栏多两个选项(uni-app、原生小程序),默认 uni-app
|
|
880
|
+
const kind = root ? designFor(session).kind : ''
|
|
881
|
+
json(res, 200, { cwd, root, kind, project, fw: defaultFramework(project, kind), dest, why: pk ? pk.why : '', frameworks: Object.fromEntries(Object.entries(FRAMEWORKS).map(([k, v]) => [k, v.label])) })
|
|
824
882
|
}
|
|
825
883
|
|
|
826
884
|
async function checkRoute(req, res) {
|
|
@@ -848,7 +906,8 @@ export function apply(ctx) {
|
|
|
848
906
|
const session = String(body.session || '')
|
|
849
907
|
if (!session) return json(res, 400, { error: '缺 session' })
|
|
850
908
|
try {
|
|
851
|
-
|
|
909
|
+
// 输入框里写的相对路径按会话目录算(新会话也问 DSH 要)
|
|
910
|
+
const target = parseTarget(body.target, await sessionCwd(session))
|
|
852
911
|
await tracker.setTarget(session, target, { source: 'user', popup: false })
|
|
853
912
|
json(res, 200, tracker.snapshot(session))
|
|
854
913
|
} catch (e) {
|
|
@@ -974,9 +1033,13 @@ export function apply(ctx) {
|
|
|
974
1033
|
const page = read(key)
|
|
975
1034
|
if (!page) return json(res, 404, { error: 'no-screen' })
|
|
976
1035
|
const href = st.origin ? `${st.origin}/${v ? `__dsh_design__/v/${v}/` : ''}${key.split('/').map(encodeURIComponent).join('/')}` : ''
|
|
977
|
-
|
|
1036
|
+
// 别人做的设计(设计库来的、没改过):加硬规矩,右侧栏的沙盒也不放行表单
|
|
1037
|
+
const strict = library.strictHtml(page) || (v > 0 && library.strictVersions(t.root))
|
|
1038
|
+
const nonce = strict ? crypto.randomBytes(16).toString('base64') : ''
|
|
1039
|
+
const lead = strict ? strictMeta(nonce) + ERR_CATCHER.replace('<script>', `<script nonce="${nonce}">`) : ERR_CATCHER
|
|
1040
|
+
const html = bundle(strict ? defang(page.toString('utf8')) : page.toString('utf8'), { key, read, base: href, lead, tail: inlinePicker(fs.readFileSync(PICKER_FILE, 'utf8'), parent, href, nonce) })
|
|
978
1041
|
// 用到的文件太多就不列(null = 右侧栏不按文件筛,目录里一有改动就重新打包)
|
|
979
|
-
json(res, 200, { html, deps: deps.size > 200 ? null : [...deps] })
|
|
1042
|
+
json(res, 200, { html, deps: deps.size > 200 ? null : [...deps], strict })
|
|
980
1043
|
} catch (e) {
|
|
981
1044
|
json(res, 500, { error: e.message })
|
|
982
1045
|
}
|
|
@@ -1116,7 +1179,36 @@ export function apply(ctx) {
|
|
|
1116
1179
|
json(res, 200, st.on ? { ...st, qr: qrMatrix(st.url).map((row) => row.map((c) => (c ? 1 : 0)).join('')) } : st)
|
|
1117
1180
|
}
|
|
1118
1181
|
|
|
1119
|
-
|
|
1182
|
+
// 设计库:我的作品、灵感、打 zip、拖 zip 进来用
|
|
1183
|
+
const inspiration = createInspiration({ log })
|
|
1184
|
+
/**
|
|
1185
|
+
* 会话目录:跑过工具的会话插件自己记着;新会话(还没跑过工具)问 DSH——先看活着的会话,再查存下来的会话头。
|
|
1186
|
+
* 都拿不到给家目录(「用这个开始」就放到 ~/DSH 设计)
|
|
1187
|
+
*/
|
|
1188
|
+
async function sessionCwd(id) {
|
|
1189
|
+
const known = tracker.cwdOf(id)
|
|
1190
|
+
if (known && path.resolve(known) !== path.resolve(os.homedir())) return known
|
|
1191
|
+
const ok = (c) => typeof c === 'string' && path.isAbsolute(c) ? c : ''
|
|
1192
|
+
try {
|
|
1193
|
+
const ss = ctx && typeof ctx.get === 'function' ? ctx.get('sessions') : null
|
|
1194
|
+
const live = ss && typeof ss.list === 'function' ? [...ss.list()].find((s) => s && s.id === id) : null
|
|
1195
|
+
const c = live && ok(live.header && live.header.cwd)
|
|
1196
|
+
if (c) return c
|
|
1197
|
+
} catch (e) { log(e) }
|
|
1198
|
+
try {
|
|
1199
|
+
const q = ctx && typeof ctx.get === 'function' ? ctx.get('sessionQuery') : null
|
|
1200
|
+
if (q && typeof q.listSessions === 'function') {
|
|
1201
|
+
const rec = (await q.listSessions()).find((r) => r && r.header && r.header.id === id)
|
|
1202
|
+
const c = rec && ok(rec.header.cwd)
|
|
1203
|
+
if (c) return c
|
|
1204
|
+
}
|
|
1205
|
+
} catch (e) { log(e) }
|
|
1206
|
+
return known
|
|
1207
|
+
}
|
|
1208
|
+
const lib = createLibraryRoutes({ admit, readBody, json, tracker, library, inspiration, studio, open, storeLoad: fileStore.load, version: VERSION, exported, log, sessionCwd })
|
|
1209
|
+
const mk = createMarketRoutes({ admit, readBody, json, tracker, library, market, open, systemsDir: SYSTEMS_DIR, log, sessionCwd })
|
|
1210
|
+
|
|
1211
|
+
const routes = { works: lib.works, workthumb: lib.workthumb, pack: lib.pack, usezip: lib.usezip, inspire: lib.inspire, inspimg: lib.inspimg, ...mk, state, open: openRoute, view: viewRoute, close: closeRoute, history: historyRoute, restore: restoreRoute, edit: editRoute, style: styleRoute, bundle: bundleRoute, ref: refRoute, export: exportRoute, doc: docRoute, reveal: revealRoute, phone: phoneRoute, check: checkRoute, locate: locateRoute, tweak: tweakRoute, systems: systemsRoute, systhumb: systhumbRoute, project: projectRoute }
|
|
1120
1212
|
for (const [key, handler] of Object.entries(routes)) {
|
|
1121
1213
|
ctx.effect(() => ctx.webServer.register({
|
|
1122
1214
|
kind: 'exact',
|
|
@@ -1140,5 +1232,14 @@ export function apply(ctx) {
|
|
|
1140
1232
|
log(e)
|
|
1141
1233
|
}
|
|
1142
1234
|
})
|
|
1235
|
+
// 模型边说边推的流:写大文件时参数要流好几分钟,靠它提前知道在写哪个文件(老版本 DSH 没有这个事件,就只是收不到)
|
|
1236
|
+
ctx.on('agent/assistant-stream', (e) => {
|
|
1237
|
+
try {
|
|
1238
|
+
const id = e && e.agent && e.agent.session && e.agent.session.id
|
|
1239
|
+
if (id && e.frame) tracker.stream(id, e.frame)
|
|
1240
|
+
} catch (err) {
|
|
1241
|
+
log(err)
|
|
1242
|
+
}
|
|
1243
|
+
}, { global: true })
|
|
1143
1244
|
}
|
|
1144
1245
|
}
|
package/lib/inspire.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 设计库 › 灵感:只存在用户电脑上,不上传、不统计
|
|
3
|
+
*
|
|
4
|
+
* <库>/inspiration.json { format: 1, items: [{ id, type, at, … }] },新的在前
|
|
5
|
+
* image { name, file, w, h } 拖进来的参考图(图在 <库>/inspiration/<file>)→「照这个做」走参考图流程
|
|
6
|
+
* url { url, name } 存下的网址 →「提取设计系统」
|
|
7
|
+
* style { name, style, mid? } 我的风格(从当前设计存下来的一组配色、字体、圆角;mid=从市场收藏的那张)→ 和内置风格卡放一起用
|
|
8
|
+
* market { mid, mtype, name } 收藏的市场设计(模板 / 设计系统):只记 id;离线看靠市场缓存,下架了界面标「已下架」
|
|
9
|
+
*
|
|
10
|
+
* 风格的字段和风格卡一样(policy.checkStyle 的白名单);本机存的不卡对比度(分享时才卡)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import crypto from 'node:crypto'
|
|
14
|
+
import fs from 'node:fs'
|
|
15
|
+
import path from 'node:path'
|
|
16
|
+
import { LIBRARY_DIR } from './works.js'
|
|
17
|
+
import { checkStyle, imageInfo } from './gallery/policy.js'
|
|
18
|
+
import { ID_RE } from './gallery/market.js'
|
|
19
|
+
|
|
20
|
+
const MAX_ITEMS = 300
|
|
21
|
+
const MAX_IMAGE = 6 * 1024 * 1024
|
|
22
|
+
const IMG_EXT = { png: 'png', jpg: 'jpg', webp: 'webp', gif: 'gif', avif: 'avif' }
|
|
23
|
+
const oneLine = (s, n) => [...String(s == null ? '' : s).replace(/[\x00-\x1f\x7f\u2028\u2029]+/g, ' ').replace(/\s+/g, ' ').trim()].slice(0, n).join('')
|
|
24
|
+
|
|
25
|
+
export function createInspiration({ dir = LIBRARY_DIR, log = () => {}, now = Date.now } = {}) {
|
|
26
|
+
const file = path.join(dir, 'inspiration.json')
|
|
27
|
+
const imgDir = path.join(dir, 'inspiration')
|
|
28
|
+
|
|
29
|
+
function load() {
|
|
30
|
+
try {
|
|
31
|
+
const raw = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
32
|
+
if (raw && Array.isArray(raw.items)) return { format: 1, items: raw.items.filter((x) => x && typeof x.id === 'string' && /^[a-z0-9]{8,32}$/.test(x.id)) }
|
|
33
|
+
} catch (e) {
|
|
34
|
+
if (e.code !== 'ENOENT') {
|
|
35
|
+
log(e)
|
|
36
|
+
try { fs.copyFileSync(file, `${file}.bad-${now()}`) } catch { /* 算了 */ }
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { format: 1, items: [] }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function save(data) {
|
|
43
|
+
// 太多了:最旧的图片 / 网址先删(我的风格、收藏留着)
|
|
44
|
+
while (data.items.length > MAX_ITEMS) {
|
|
45
|
+
let i = data.items.length - 1
|
|
46
|
+
while (i >= 0 && (data.items[i].type === 'style' || data.items[i].type === 'market')) i--
|
|
47
|
+
if (i < 0) break
|
|
48
|
+
const [gone] = data.items.splice(i, 1)
|
|
49
|
+
if (gone.type === 'image' && gone.file) try { fs.rmSync(path.join(imgDir, gone.file), { force: true }) } catch { /* 算了 */ }
|
|
50
|
+
}
|
|
51
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
52
|
+
const tmp = `${file}.${process.pid}.tmp`
|
|
53
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 1), { mode: 0o600 })
|
|
54
|
+
fs.renameSync(tmp, file)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const newId = () => crypto.randomBytes(8).toString('hex')
|
|
58
|
+
|
|
59
|
+
function add(item) {
|
|
60
|
+
const data = load()
|
|
61
|
+
const it = { id: newId(), at: now(), ...item }
|
|
62
|
+
data.items.unshift(it)
|
|
63
|
+
save(data)
|
|
64
|
+
return it
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 参考图:data 是图片的 Buffer */
|
|
68
|
+
function addImage({ name, data }) {
|
|
69
|
+
if (!Buffer.isBuffer(data) || !data.length) throw new Error('不是图片')
|
|
70
|
+
if (data.length > MAX_IMAGE) throw new Error('图片太大(最多 6MB)')
|
|
71
|
+
const info = imageInfo(data)
|
|
72
|
+
const ext = info && IMG_EXT[info.type]
|
|
73
|
+
if (!ext) throw new Error('不是图片')
|
|
74
|
+
const id = newId()
|
|
75
|
+
fs.mkdirSync(imgDir, { recursive: true })
|
|
76
|
+
fs.writeFileSync(path.join(imgDir, `${id}.${ext}`), data, { mode: 0o600 })
|
|
77
|
+
const data2 = load()
|
|
78
|
+
const it = { id, at: now(), type: 'image', name: oneLine(name, 60) || 'image', file: `${id}.${ext}`, w: info.w, h: info.h }
|
|
79
|
+
data2.items.unshift(it)
|
|
80
|
+
save(data2)
|
|
81
|
+
return it
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** 网址:只收 http(s),带不带协议都行 */
|
|
85
|
+
function addUrl({ url, name }) {
|
|
86
|
+
let s = String(url || '').trim()
|
|
87
|
+
if (!/^https?:\/\//i.test(s)) s = 'https://' + s
|
|
88
|
+
let u
|
|
89
|
+
try { u = new URL(s) } catch { throw new Error('不是网址') }
|
|
90
|
+
if (!/^https?:$/.test(u.protocol) || !u.hostname.includes('.') || s.length > 500) throw new Error('不是网址')
|
|
91
|
+
const data = load()
|
|
92
|
+
const href = u.href
|
|
93
|
+
const old = data.items.find((x) => x.type === 'url' && x.url === href)
|
|
94
|
+
if (old) return old
|
|
95
|
+
return add({ type: 'url', url: href, name: oneLine(name, 60) || u.hostname })
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** 我的风格:字段按风格卡的白名单查;对比度本机不卡 */
|
|
99
|
+
function addStyle({ name, style, mid }) {
|
|
100
|
+
const problems = checkStyle(style).filter((p) => p.code !== 'contrast')
|
|
101
|
+
if (problems.length) throw new Error(`风格不对:${problems.map((p) => p.x).join('、')}`)
|
|
102
|
+
const s = {}
|
|
103
|
+
for (const k of ['c', 'head', 'w', 'radius', 'shadow', 'nzh', 'nen', 'dzh', 'den', 'bgImage']) if (style[k] !== undefined) s[k] = style[k]
|
|
104
|
+
// 从市场收藏的:同一张只存一份
|
|
105
|
+
const from = typeof mid === 'string' && ID_RE.test(mid) ? mid : ''
|
|
106
|
+
if (from) {
|
|
107
|
+
const old = load().items.find((x) => x.type === 'style' && x.mid === from)
|
|
108
|
+
if (old) return old
|
|
109
|
+
}
|
|
110
|
+
return add({ type: 'style', name: oneLine(name, 24) || '我的风格', style: s, ...(from ? { mid: from } : {}) })
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** 收藏市场上的一套:同一套只记一次 */
|
|
114
|
+
function addMarket({ mid, mtype, name }) {
|
|
115
|
+
const id = String(mid || '')
|
|
116
|
+
if (!ID_RE.test(id) || (mtype !== 'template' && mtype !== 'system')) throw new Error('不是市场上的设计')
|
|
117
|
+
const data = load()
|
|
118
|
+
const old = data.items.find((x) => x.type === 'market' && x.mid === id)
|
|
119
|
+
if (old) return old
|
|
120
|
+
return add({ type: 'market', mid: id, mtype, name: oneLine(name, 40) || id.split('__')[1] })
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function remove(id) {
|
|
124
|
+
const data = load()
|
|
125
|
+
const i = data.items.findIndex((x) => x.id === id)
|
|
126
|
+
if (i < 0) return false
|
|
127
|
+
const [gone] = data.items.splice(i, 1)
|
|
128
|
+
if (gone.type === 'image' && gone.file) try { fs.rmSync(path.join(imgDir, path.basename(gone.file)), { force: true }) } catch { /* 算了 */ }
|
|
129
|
+
save(data)
|
|
130
|
+
return true
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** 图片文件的绝对路径(只认登记过的) */
|
|
134
|
+
function imageFile(id) {
|
|
135
|
+
const it = load().items.find((x) => x.id === id && x.type === 'image')
|
|
136
|
+
if (!it || !it.file || path.basename(it.file) !== it.file) return null
|
|
137
|
+
const p = path.join(imgDir, it.file)
|
|
138
|
+
return fs.existsSync(p) ? p : null
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { list: () => load().items, addImage, addUrl, addStyle, addMarket, remove, imageFile, file }
|
|
142
|
+
}
|
package/lib/offline.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-design-studio/offline:设计库不联网(默认关)。
|
|
3
|
+
* 打开后设计库的「市场」一个网址都不访问,只显示已经下载过的;「我的作品」「灵感」照常用。
|
|
4
|
+
* 默认关、反着来:老版本升级上来插件页里还没有这一行时,市场照常联网
|
|
5
|
+
*/
|
|
6
|
+
import { studio } from './index.js'
|
|
7
|
+
|
|
8
|
+
export const name = 'dsh-design-studio-offline'
|
|
9
|
+
|
|
10
|
+
export function apply(ctx) {
|
|
11
|
+
ctx.effect(() => {
|
|
12
|
+
studio.offline++
|
|
13
|
+
// 正在下载的目录 / 批次包马上掐掉(不等下完)
|
|
14
|
+
try { if (studio.market) studio.market.abort() } catch { /* 市场还没起来 */ }
|
|
15
|
+
return () => { studio.offline-- }
|
|
16
|
+
}, 'dsh-design-studio: gallery offline')
|
|
17
|
+
}
|