dsh-design-studio 0.9.0 → 0.11.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.
Files changed (95) hide show
  1. package/README.en.md +6 -4
  2. package/README.md +6 -4
  3. package/assets/demo.gif +0 -0
  4. package/assets/hero-sm.jpg +0 -0
  5. package/cordis.patch.yml +5 -0
  6. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/_shots/menu.jpg +0 -0
  7. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/app.css +64 -0
  8. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/cart.html +120 -0
  9. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/design.json +56 -0
  10. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/menu.html +200 -0
  11. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/order.html +135 -0
  12. package/gallery-seed/d/orderg-x__alley-noodles/08e9e40834f9/tokens.css +43 -0
  13. package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/_shots/email.jpg +0 -0
  14. package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/design.json +20 -0
  15. package/gallery-seed/d/orderg-x__autumn-sale/08e9e40834f9/email.html +268 -0
  16. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/_shots/courses.jpg +0 -0
  17. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/courses.html +364 -0
  18. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/design.json +49 -0
  19. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/lesson.html +196 -0
  20. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/tokens.css +55 -0
  21. package/gallery-seed/d/orderg-x__little-painter/08e9e40834f9/ui.css +255 -0
  22. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/_shots/poster.jpg +0 -0
  23. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/design.json +21 -0
  24. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/poster.html +165 -0
  25. package/gallery-seed/d/orderg-x__moon-fair/08e9e40834f9/tokens.css +35 -0
  26. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/_shots/home.jpg +0 -0
  27. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/app.css +155 -0
  28. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/booking.html +232 -0
  29. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/design.json +57 -0
  30. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/health.html +221 -0
  31. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/home.html +225 -0
  32. package/gallery-seed/d/orderg-x__paw-diary/08e9e40834f9/tokens.css +54 -0
  33. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/_shots/overview.jpg +0 -0
  34. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/app.css +79 -0
  35. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/design.json +51 -0
  36. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/entries.html +161 -0
  37. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/icons.svg +30 -0
  38. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/overview.html +181 -0
  39. package/gallery-seed/d/orderg-x__pocket-ledger/08e9e40834f9/tokens.css +50 -0
  40. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/_shots/home.jpg +0 -0
  41. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/book.html +279 -0
  42. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/design.json +21 -0
  43. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/home.html +348 -0
  44. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/style.css +297 -0
  45. package/gallery-seed/d/orderg-x__time-bookshop/08e9e40834f9/tokens.css +49 -0
  46. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/01-cover.html +133 -0
  47. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/02-highlights.html +67 -0
  48. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/03-growth.html +89 -0
  49. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/04-milestones.html +92 -0
  50. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/05-next.html +99 -0
  51. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/06-thanks.html +61 -0
  52. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/_shots/01-cover.jpg +0 -0
  53. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/deck.css +67 -0
  54. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/design.json +25 -0
  55. package/gallery-seed/d/orderg-x__year-review/08e9e40834f9/tokens.css +30 -0
  56. package/gallery-seed/index.json +1 -0
  57. package/gallery-seed/thumbs/orderg-x__alley-noodles.webp +0 -0
  58. package/gallery-seed/thumbs/orderg-x__autumn-sale.webp +0 -0
  59. package/gallery-seed/thumbs/orderg-x__little-painter.webp +0 -0
  60. package/gallery-seed/thumbs/orderg-x__moon-fair.webp +0 -0
  61. package/gallery-seed/thumbs/orderg-x__paw-diary.webp +0 -0
  62. package/gallery-seed/thumbs/orderg-x__pocket-ledger.webp +0 -0
  63. package/gallery-seed/thumbs/orderg-x__time-bookshop.webp +0 -0
  64. package/gallery-seed/thumbs/orderg-x__year-review.webp +0 -0
  65. package/lib/browser.js +23 -9
  66. package/lib/bundle.js +12 -3
  67. package/lib/client.js +3106 -111
  68. package/lib/core.js +44 -11
  69. package/lib/gallery/css.js +230 -0
  70. package/lib/gallery/html.js +313 -0
  71. package/lib/gallery/index.js +22 -0
  72. package/lib/gallery/market-routes.js +306 -0
  73. package/lib/gallery/market.js +623 -0
  74. package/lib/gallery/pack.js +311 -0
  75. package/lib/gallery/policy.js +1560 -0
  76. package/lib/gallery/qrfind.js +181 -0
  77. package/lib/gallery/render.js +148 -0
  78. package/lib/gallery/routes.js +360 -0
  79. package/lib/gallery/tar.js +118 -0
  80. package/lib/gallery/use.js +125 -0
  81. package/lib/gallery/zip.js +212 -0
  82. package/lib/handoff.js +291 -29
  83. package/lib/history.js +3 -1
  84. package/lib/index.js +118 -24
  85. package/lib/inspire.js +142 -0
  86. package/lib/offline.js +17 -0
  87. package/lib/picker.js +425 -32
  88. package/lib/serve.js +88 -11
  89. package/lib/works.js +238 -0
  90. package/locale/en.json +2 -1
  91. package/locale/offline/en.json +6 -0
  92. package/locale/offline/zh.json +6 -0
  93. package/locale/zh.json +2 -1
  94. package/package.json +10 -3
  95. package/skills/ui-design/SKILL.md +9 -0
package/lib/index.js CHANGED
@@ -40,13 +40,19 @@
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'
51
57
  import { createHistory } from './history.js'
52
58
  import { createPhone } from './phone.js'
@@ -56,7 +62,7 @@ import { createBrowser } from './browser.js'
56
62
  import { designOf, metaOfFile, sizeOf, pickScreens, measureAll, shootAll, cropIssues, formatMeasure, issueSnapshot, formatShots, whereOf, MAX_SCREENS, MAX_SIZES, MAX_SHOTS, MAX_CROPS } from './look.js'
57
63
  import { SYSTEMS_DIR, extractSystem, listSystems, setDefault, skillSection } from './system.js'
58
64
  import { readTweaks, applyTweaks, lintTweaks } from './tweaks.js'
59
- import { FRAMEWORKS, detectProject, buildHandoff, compareScreens, implUrl, defaultDest, within } from './handoff.js'
65
+ import { FRAMEWORKS, detectProject, projectFor, defaultFramework, buildHandoff, compareScreens, implUrl, defaultDest, pickedDest, within } from './handoff.js'
60
66
 
61
67
  export const name = 'dsh-design-studio'
62
68
  export const inject = ['webServer']
@@ -82,7 +88,7 @@ const SKILL_RANK = 600 // 和 DSH 自带的技能同一档;用户 / 项目里
82
88
  * 几行之间靠这个互通(同一个包里的模块 Node 只加载一次):工作台起来后放 tracker、无头浏览器(browser);
83
89
  * 「自动打开预览」那行开着时 autoOpen > 0
84
90
  */
85
- export const studio = { tracker: null, autoOpen: 0, check: null, browser: null, log: () => {} }
91
+ export const studio = { tracker: null, autoOpen: 0, offline: 0, check: null, browser: null, market: null, log: () => {} }
86
92
 
87
93
  /** 设计技能:模型做界面设计时加载(DSH 的 skill 工具),也能让用户手动调 */
88
94
  export const designSkill = {
@@ -439,11 +445,11 @@ export const handoffTool = {
439
445
  name: 'handoff_design',
440
446
  description:
441
447
  '把右侧「设计工作台」里的设计(本地 HTML 那几屏)整理成交接包,准备做成真项目的代码:写好 <设计目录>/handoff/HANDOFF.md(每屏的文件、建议路由、跳转、设计变量、几屏共用该抽成组件的部分、交互和状态、调节滑块、素材、做法和验收),和每屏从上到下的截图。' +
442
- '用户说「转成代码」「做成真的项目」「用 React / Vue 做出来」时先调它,读 HANDOFF.md 照着做;每做完一屏用 compare_design 和设计稿比一遍,差异降到 3% 以下再做下一屏。' +
443
- 'framework:same 跟目标目录现有项目一样(有 package.json 时默认)、react、vue、next、html;dest:代码放哪个目录(不填放在会话目录里新建)。要先用 preview_page 打开过设计。',
448
+ '用户说「转成代码」「做成真的项目」「用 React / Vue / uni-app 做出来」「做成小程序」时先调它,读 HANDOFF.md 照着做;每做完一屏用 compare_design 和设计稿比一遍,差异降到 3% 以下再做下一屏(原生小程序在浏览器里跑不起来,照 HANDOFF.md 说的请用户在微信开发者工具里看)。' +
449
+ 'framework:same 跟目标目录现有项目一样(认得出框架时默认,uni-app(含 HBuilderX 建的、没有 package.json 的)、Taro、微信原生小程序也认得出)、react、vue、next、html、uniapp、weapp;dest:代码放哪个目录(不填:会话目录是项目就放进去,不是就新建 <设计名>-app)。要先用 preview_page 打开过设计。',
444
450
  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,没有就 react' },
446
- dest: { type: 'string', description: '代码放哪:现有项目的目录,或要新建的项目目录(绝对路径或相对会话目录)。不填:会话目录有项目就放进去,没有就在会话目录下新建' },
451
+ 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' },
452
+ dest: { type: 'string', description: '代码放哪:现有项目的目录(或它里面的子目录),或要新建的项目目录(绝对路径或相对会话目录)。不填:会话目录有 package.json 就放进去,没有就新建 <设计名>-app(设计目录在会话目录下就建在会话目录里,不然建在设计目录旁边)' },
447
453
  screens: { type: 'array', items: { type: 'string' }, description: '只转哪几屏(文件名);不填全部' },
448
454
  }),
449
455
  output: {
@@ -469,16 +475,18 @@ export const handoffTool = {
469
475
  // 先定下最终放哪(没给就和右侧栏建议的一样),再按真实路径查是不是在设计目录里
470
476
  const dest = typeof a.dest === 'string' && a.dest.trim() ? resolvePath(a.dest, cwd) : defaultDest(t.root, cwd, detectProject(cwd))
471
477
  if (within(dest, t.root)) return { text: `代码别放进设计目录(${t.root})里:设计稿留着做对比。换一个目录。` }
472
- const project = detectProject(dest)
473
- const framework = typeof a.framework === 'string' && (a.framework === 'same' || FRAMEWORKS[a.framework]) ? a.framework : project.pkg && project.framework ? 'same' : 'react'
474
- if (framework === 'same' && !(project.pkg && project.framework)) return { text: `${dest} 里没有认得出的前端项目(package.json 里没有 React / Vue / Next …):framework 选 react、vue、next 或 html。` }
478
+ const project = projectFor(dest)
479
+ const framework = typeof a.framework === 'string' && (a.framework === 'same' || FRAMEWORKS[a.framework]) ? a.framework : defaultFramework(project, kind)
480
+ if (framework === 'same' && !project.framework) return { text: `${dest} 里没有认得出的前端项目(package.json 里没有 React / Vue / Next / uni-app …,也不是 HBuilderX 或微信原生小程序的项目):framework 选 react、vue、next、html、uniapp 或 weapp。` }
475
481
  let r
476
482
  try {
477
483
  r = await buildHandoff(studio.browser, { root: t.root, screens: pick.list, kind, size, framework, dest, project, system: systemOfDir(t.root) })
478
484
  } catch (e) {
479
485
  return { text: `没写成交接包:${String((e && e.message) || e).slice(0, 200)}` }
480
486
  }
481
- const head = `交接包写好了:${r.file}(${r.shots} 张截图在 handoff/shots/)。下面是全文,照「做法和验收」一屏一屏做,每屏做完用 compare_design 比。\n\n`
487
+ // 原生小程序跑不了网页版:比不了,照 HANDOFF.md 请用户在微信开发者工具里看
488
+ const native = framework === 'weapp' || (framework === 'same' && project.framework === 'weapp')
489
+ const head = `交接包写好了:${r.file}(${r.shots} 张截图在 handoff/shots/)。下面是全文,照「做法和验收」一屏一屏做,${native ? '每屏做完请用户在微信开发者工具里看' : '每屏做完用 compare_design 比'}。\n\n`
482
490
  // 每屏顶上一张附上,直接看到要做成什么样
483
491
  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
492
  const kept = firsts.length ? await keepImages(ctx, id, firsts) : {}
@@ -569,6 +577,8 @@ async function keepImages(ctx, id, list, toFile = false) {
569
577
  return { files }
570
578
  }
571
579
 
580
+ const VERSION = (() => { try { return JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8')).version || '' } catch { return '' } })()
581
+
572
582
  const fileStore = {
573
583
  load: () => JSON.parse(fs.readFileSync(STORE_FILE, 'utf8')),
574
584
  save: (data) => {
@@ -641,9 +651,16 @@ export function apply(ctx) {
641
651
  const previewPorts = new Set()
642
652
  let tracker = null
643
653
  const history = createHistory({ base: HISTORY_DIR, log })
654
+ // 我的作品 + 哪些文件是别人做的(设计库来的):别人做的、没改过的页面预览时加硬规矩
655
+ const library = createLibrary({ log })
656
+ // 设计市场:目录和批次包从 npm 镜像来,验过签名、逐个文件核对过才用;市场上的页面原样预览时也加硬规矩
657
+ const seedDir = fileURLToPath(new URL('../gallery-seed', import.meta.url))
658
+ // 「设计库不联网」那行开着:市场不访问任何网址
659
+ const market = createMarket({ version: VERSION, log, seedDir: fs.existsSync(path.join(seedDir, 'index.json')) ? seedDir : '', online: () => studio.offline === 0 })
644
660
  const hub = createHub({
645
661
  log,
646
662
  history,
663
+ strict: { html: (buf) => library.strictHtml(buf) || market.strictHtml(buf), root: (dir) => library.strictRoot(dir) || market.isStaging(dir), versions: (dir) => library.strictVersions(dir) },
647
664
  // 桌面版的 dsh-app://app 是默认就认的;这里补上 DSH 网页端的地址,和 DSH 配置里声明的对外地址(反向代理 / 域名)
648
665
  parents: () => {
649
666
  let own = []
@@ -664,9 +681,12 @@ export function apply(ctx) {
664
681
  open,
665
682
  log,
666
683
  history,
667
- store: fileStore,
684
+ // 记会话的同时,在做的设计目录补进「我的作品」
685
+ store: { load: fileStore.load, save: (data) => { fileStore.save(data); try { library.sync(data) } catch (e) { log(e) } } },
668
686
  alive: (origin) => hub.alive(origin),
669
687
  autoOpen: () => studio.autoOpen > 0,
688
+ // 装好的别人的设计系统退回旧版:退回来的页面先记成别人做的(预览照样加规矩)
689
+ beforeRestore: (root, html) => { if (html.length) library.addPast(root, html) },
670
690
  skipPorts: () => {
671
691
  const s = new Set(previewPorts)
672
692
  const own = ownHost(ctx).split(':')[1]
@@ -678,7 +698,11 @@ export function apply(ctx) {
678
698
  ctx.effect(() => {
679
699
  studio.tracker = tracker
680
700
  studio.log = log
681
- return () => { if (studio.tracker === tracker) { studio.tracker = null; studio.log = () => {} } }
701
+ studio.market = market
702
+ return () => {
703
+ if (studio.tracker === tracker) { studio.tracker = null; studio.log = () => {} }
704
+ if (studio.market === market) { market.abort(); studio.market = null }
705
+ }
682
706
  }, 'dsh-design-studio: tracker')
683
707
  // check_design / look_design 用的无头浏览器:用到时才开,闲置自己关,这一行停用时关掉
684
708
  ctx.effect(() => {
@@ -746,7 +770,33 @@ export function apply(ctx) {
746
770
  const t = tracker.stateOf(session).target
747
771
  let tweaks = null
748
772
  if (t && t.kind === 'file') { try { tweaks = readTweaks(t.root) } catch (e) { log(e) } }
749
- json(res, 200, { ...tracker.snapshot(session), phone: { on: ph.on, seen: ph.seen || 0 }, tweaks, ...(c ? { checkReq: c.id } : {}) })
773
+ const snap = tracker.snapshot(session)
774
+ json(res, 200, { ...snap, strict: strictScreens(snap), phone: { on: ph.on, seen: ph.seen || 0 }, tweaks, ...(c ? { checkReq: c.id } : {}) })
775
+ }
776
+
777
+ /** 正在看的几屏里,哪些是别人做的、没改过的(地址列表):右侧栏给它们的 iframe 加沙盒、不给剪贴板 */
778
+ const strictCache = new Map() // 文件 → { sig, strict }
779
+ function strictScreens(snap) {
780
+ const t = snap.target
781
+ if (!t || t.kind !== 'file' || !Array.isArray(snap.screens)) return []
782
+ const out = []
783
+ const files = new Set([...snap.screens.map((s) => s.key), snap.focus].filter(Boolean))
784
+ for (const key of files) {
785
+ const file = path.join(t.root, key)
786
+ let st
787
+ try { st = fs.statSync(file) } catch { continue }
788
+ const sig = `${st.mtimeMs}:${st.size}:${library.file && fs.existsSync(library.file) ? fs.statSync(library.file).mtimeMs : 0}`
789
+ let hit = strictCache.get(file)
790
+ if (!hit || hit.sig !== sig) {
791
+ let strict = false
792
+ try { strict = st.size <= 8 * 1024 * 1024 && library.strictHtml(fs.readFileSync(file)) } catch { strict = false }
793
+ hit = { sig, strict }
794
+ strictCache.set(file, hit)
795
+ if (strictCache.size > 500) strictCache.delete(strictCache.keys().next().value)
796
+ }
797
+ if (hit.strict) out.push(`${snap.origin}/${encodeURIComponent(key)}`)
798
+ }
799
+ return out
750
800
  }
751
801
 
752
802
  /** 调节滑块写回文件:values = { id: 值 } */
@@ -806,16 +856,24 @@ export function apply(ctx) {
806
856
  }
807
857
 
808
858
  /** 转成代码前:会话目录是不是个前端项目(右侧栏默认选「跟项目一样」、填好放哪) */
809
- function projectRoute(req, res) {
859
+ async function projectRoute(req, res) {
810
860
  if (!admit(req, res)) return
811
- const session = new URL(req.url, 'http://x').searchParams.get('session') || ''
812
- const cwd = tracker.cwdOf(session)
861
+ const q = new URL(req.url, 'http://x').searchParams
862
+ const session = q.get('session') || ''
863
+ // 新会话(还没跑过工具,比如从设计库「用这个开始」、浏览打开的)插件不知道目录:问 DSH,别拿家目录当项目找
864
+ const cwd = await sessionCwd(session)
813
865
  const p = detectProject(cwd)
814
866
  const t = tracker.stateOf(session).target
815
867
  const root = t && t.kind === 'file' ? t.root : ''
816
868
  // root 一起回去:右侧栏按它认这份建议是不是给现在这套设计的;project 按最终放的目录认(默认框架跟着它选)
817
- const dest = defaultDest(root, cwd, p)
818
- json(res, 200, { cwd, root, project: dest === cwd ? p : detectProject(dest), dest, frameworks: Object.fromEntries(Object.entries(FRAMEWORKS).map(([k, v]) => [k, v.label])) })
869
+ // 带 dest:用户刚选了个文件夹,按它定最终放哪
870
+ const picked = (q.get('dest') || '').trim()
871
+ const pk = picked ? pickedDest(resolvePath(picked, cwd), root) : null
872
+ const dest = pk ? pk.dest : defaultDest(root, cwd, p)
873
+ const project = projectFor(dest)
874
+ // 设计类型跟着回去:小程序的设计右侧栏多两个选项(uni-app、原生小程序),默认 uni-app
875
+ const kind = root ? designFor(session).kind : ''
876
+ 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])) })
819
877
  }
820
878
 
821
879
  async function checkRoute(req, res) {
@@ -843,7 +901,8 @@ export function apply(ctx) {
843
901
  const session = String(body.session || '')
844
902
  if (!session) return json(res, 400, { error: '缺 session' })
845
903
  try {
846
- const target = parseTarget(body.target, tracker.cwdOf(session))
904
+ // 输入框里写的相对路径按会话目录算(新会话也问 DSH 要)
905
+ const target = parseTarget(body.target, await sessionCwd(session))
847
906
  await tracker.setTarget(session, target, { source: 'user', popup: false })
848
907
  json(res, 200, tracker.snapshot(session))
849
908
  } catch (e) {
@@ -969,9 +1028,13 @@ export function apply(ctx) {
969
1028
  const page = read(key)
970
1029
  if (!page) return json(res, 404, { error: 'no-screen' })
971
1030
  const href = st.origin ? `${st.origin}/${v ? `__dsh_design__/v/${v}/` : ''}${key.split('/').map(encodeURIComponent).join('/')}` : ''
972
- const html = bundle(page.toString('utf8'), { key, read, base: href, lead: ERR_CATCHER, tail: inlinePicker(fs.readFileSync(PICKER_FILE, 'utf8'), parent, href) })
1031
+ // 别人做的设计(设计库来的、没改过):加硬规矩,右侧栏的沙盒也不放行表单
1032
+ const strict = library.strictHtml(page) || (v > 0 && library.strictVersions(t.root))
1033
+ const nonce = strict ? crypto.randomBytes(16).toString('base64') : ''
1034
+ const lead = strict ? strictMeta(nonce) + ERR_CATCHER.replace('<script>', `<script nonce="${nonce}">`) : ERR_CATCHER
1035
+ 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) })
973
1036
  // 用到的文件太多就不列(null = 右侧栏不按文件筛,目录里一有改动就重新打包)
974
- json(res, 200, { html, deps: deps.size > 200 ? null : [...deps] })
1037
+ json(res, 200, { html, deps: deps.size > 200 ? null : [...deps], strict })
975
1038
  } catch (e) {
976
1039
  json(res, 500, { error: e.message })
977
1040
  }
@@ -1063,6 +1126,8 @@ export function apply(ctx) {
1063
1126
  const ext = imageExt(buf)
1064
1127
  if (!ext) return json(res, 400, { error: '不是图片' })
1065
1128
  const st = tracker.stateOf(String(body.session || ''))
1129
+ // 画笔标注带着画的那份设计的目录来:这期间换了设计,别把旧图存进新设计
1130
+ if (typeof body.root === 'string' && !(st.target && st.target.kind === 'file' && st.target.root === body.root)) return json(res, 409, { error: 'moved' })
1066
1131
  const dir = st.target && st.target.kind === 'file' ? path.join(st.target.root, 'refs') : REFS_DIR
1067
1132
  const base = String(body.name || 'ref').replace(/\.[a-z0-9]{2,5}$/i, '').replace(/[^\w\u4e00-\u9fff-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 60) || 'ref'
1068
1133
  try {
@@ -1109,7 +1174,36 @@ export function apply(ctx) {
1109
1174
  json(res, 200, st.on ? { ...st, qr: qrMatrix(st.url).map((row) => row.map((c) => (c ? 1 : 0)).join('')) } : st)
1110
1175
  }
1111
1176
 
1112
- const routes = { 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 }
1177
+ // 设计库:我的作品、灵感、打 zip、拖 zip 进来用
1178
+ const inspiration = createInspiration({ log })
1179
+ /**
1180
+ * 会话目录:跑过工具的会话插件自己记着;新会话(还没跑过工具)问 DSH——先看活着的会话,再查存下来的会话头。
1181
+ * 都拿不到给家目录(「用这个开始」就放到 ~/DSH 设计)
1182
+ */
1183
+ async function sessionCwd(id) {
1184
+ const known = tracker.cwdOf(id)
1185
+ if (known && path.resolve(known) !== path.resolve(os.homedir())) return known
1186
+ const ok = (c) => typeof c === 'string' && path.isAbsolute(c) ? c : ''
1187
+ try {
1188
+ const ss = ctx && typeof ctx.get === 'function' ? ctx.get('sessions') : null
1189
+ const live = ss && typeof ss.list === 'function' ? [...ss.list()].find((s) => s && s.id === id) : null
1190
+ const c = live && ok(live.header && live.header.cwd)
1191
+ if (c) return c
1192
+ } catch (e) { log(e) }
1193
+ try {
1194
+ const q = ctx && typeof ctx.get === 'function' ? ctx.get('sessionQuery') : null
1195
+ if (q && typeof q.listSessions === 'function') {
1196
+ const rec = (await q.listSessions()).find((r) => r && r.header && r.header.id === id)
1197
+ const c = rec && ok(rec.header.cwd)
1198
+ if (c) return c
1199
+ }
1200
+ } catch (e) { log(e) }
1201
+ return known
1202
+ }
1203
+ const lib = createLibraryRoutes({ admit, readBody, json, tracker, library, inspiration, studio, open, storeLoad: fileStore.load, version: VERSION, exported, log, sessionCwd })
1204
+ const mk = createMarketRoutes({ admit, readBody, json, tracker, library, market, open, systemsDir: SYSTEMS_DIR, log, sessionCwd })
1205
+
1206
+ 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 }
1113
1207
  for (const [key, handler] of Object.entries(routes)) {
1114
1208
  ctx.effect(() => ctx.webServer.register({
1115
1209
  kind: 'exact',
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
+ }