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.
Files changed (98) hide show
  1. package/README.en.md +4 -3
  2. package/README.md +6 -5
  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 +3253 -483
  68. package/lib/core.js +231 -7
  69. package/lib/draft-page.js +763 -0
  70. package/lib/draft.js +151 -0
  71. package/lib/gallery/css.js +230 -0
  72. package/lib/gallery/html.js +313 -0
  73. package/lib/gallery/index.js +22 -0
  74. package/lib/gallery/market-routes.js +306 -0
  75. package/lib/gallery/market.js +623 -0
  76. package/lib/gallery/pack.js +311 -0
  77. package/lib/gallery/policy.js +1560 -0
  78. package/lib/gallery/qrfind.js +181 -0
  79. package/lib/gallery/render.js +148 -0
  80. package/lib/gallery/routes.js +360 -0
  81. package/lib/gallery/tar.js +118 -0
  82. package/lib/gallery/use.js +125 -0
  83. package/lib/gallery/zip.js +212 -0
  84. package/lib/handoff.js +190 -36
  85. package/lib/history.js +3 -1
  86. package/lib/index.js +123 -22
  87. package/lib/inspire.js +142 -0
  88. package/lib/offline.js +17 -0
  89. package/lib/picker.js +260 -1
  90. package/lib/serve.js +264 -11
  91. package/lib/tweaks.js +58 -24
  92. package/lib/works.js +238 -0
  93. package/locale/en.json +2 -1
  94. package/locale/offline/en.json +6 -0
  95. package/locale/offline/zh.json +6 -0
  96. package/locale/zh.json +2 -1
  97. package/package.json +9 -2
  98. 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 做出来」时先调它,读 HANDOFF.md 照着做;每做完一屏用 compare_design 和设计稿比一遍,差异降到 3% 以下再做下一屏。' +
443
- 'framework:same 跟目标目录现有项目一样(认得出框架时默认)、react、vue、next、html;dest:代码放哪个目录(不填:会话目录是项目就放进去,不是就新建 <设计名>-app)。要先用 preview_page 打开过设计。',
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' && !(project.pkg && project.framework)) return { text: `${dest} 里没有认得出的前端项目(package.json 里没有 React / Vue / Next …):framework 选 react、vue、next 或 html。` }
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
- const head = `交接包写好了:${r.file}(${r.shots} 张截图在 handoff/shots/)。下面是全文,照「做法和验收」一屏一屏做,每屏做完用 compare_design 比。\n\n`
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
- store: fileStore,
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
- return () => { if (studio.tracker === tracker) { studio.tracker = null; studio.log = () => {} } }
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
- json(res, 200, { ...tracker.snapshot(session), phone: { on: ph.on, seen: ph.seen || 0 }, tweaks, ...(c ? { checkReq: c.id } : {}) })
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 r = tracker.editDir(String(body.session || ''), (root) => applyTweaks(root, values), (x) => `调节:${x.changed.map((c) => `${c.label} ${c.to}`).join('、')}`.slice(0, 60), String(body.root || ''))
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
- const cwd = tracker.cwdOf(session)
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
- json(res, 200, { cwd, root, project, fw: defaultFramework(project), dest, why: pk ? pk.why : '', frameworks: Object.fromEntries(Object.entries(FRAMEWORKS).map(([k, v]) => [k, v.label])) })
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
- const target = parseTarget(body.target, tracker.cwdOf(session))
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
- const html = bundle(page.toString('utf8'), { key, read, base: href, lead: ERR_CATCHER, tail: inlinePicker(fs.readFileSync(PICKER_FILE, 'utf8'), parent, href) })
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
- 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 }
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
+ }