dsh-theme-palette 1.4.2 → 1.4.3
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/CHANGELOG.md +13 -0
- package/README-es.md +2 -2
- package/README-hi.md +2 -2
- package/README-pt.md +2 -2
- package/README-zh.md +2 -2
- package/README.md +2 -2
- package/client.js +43 -22
- package/docs/DESIGN.md +2 -2
- package/package.json +18 -12
- package/palette-dev.mjs +0 -140
- package/tools/README.md +0 -66
- package/tools/compare.mjs +0 -62
- package/tools/preview-settings.mjs +0 -134
- package/tools/release.mjs +0 -130
- package/tools/sheet.mjs +0 -219
- package/tools/shoot-desktop.mjs +0 -112
- package/tools/shoot-html.mjs +0 -123
- package/tools/shoot-sheet.mjs +0 -122
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 把插件设置页渲染成静态 HTML(浅色 + 深色两块),用于**看板级别的视觉检查**。
|
|
3
|
-
* 组件本身是 React 元素树;这里用一个极小的树→HTML 渲染器把它铺开,
|
|
4
|
-
* 样式取自插件真正注入的 SETTINGS_CSS(经 dev 钩子拿到),token 由生产引擎按模式算出。
|
|
5
|
-
*
|
|
6
|
-
* 用法:node tools/preview-settings.mjs → tools/preview.html
|
|
7
|
-
*/
|
|
8
|
-
import fs from 'node:fs'
|
|
9
|
-
import path from 'node:path'
|
|
10
|
-
import { fileURLToPath } from 'node:url'
|
|
11
|
-
|
|
12
|
-
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
13
|
-
const root = path.resolve(here, '..')
|
|
14
|
-
const argv = process.argv.slice(2)
|
|
15
|
-
const li = argv.indexOf('--lang')
|
|
16
|
-
const LANG = li >= 0 ? argv[li + 1] : 'auto' // auto | zh-CN | zh-TW | en | ja
|
|
17
|
-
const source = fs.readFileSync(path.join(root, 'client.js'), 'utf8')
|
|
18
|
-
|
|
19
|
-
let engine = null
|
|
20
|
-
const sandbox = {
|
|
21
|
-
window: {
|
|
22
|
-
__ModuleLoader__: { load: (d) => { sandbox.def = d } },
|
|
23
|
-
__DSH_THEME_PALETTE_DEV__: (api) => { engine = api },
|
|
24
|
-
},
|
|
25
|
-
localStorage: { getItem: () => null, setItem: () => {} },
|
|
26
|
-
document: { head: { appendChild() {} }, createElement: () => ({ dataset: {}, remove() {} }), querySelector: () => null, documentElement: { lang: LANG === 'auto' ? 'zh-CN' : LANG } },
|
|
27
|
-
navigator: { language: 'zh' },
|
|
28
|
-
}
|
|
29
|
-
new Function('window', 'localStorage', 'document', 'navigator', source)(
|
|
30
|
-
sandbox.window, sandbox.localStorage, sandbox.document, sandbox.navigator,
|
|
31
|
-
)
|
|
32
|
-
const React = {
|
|
33
|
-
createElement: (type, props, ...kids) => ({ type, props: props || {}, kids }),
|
|
34
|
-
useState: (v) => (typeof v === 'boolean' ? [true, () => {}] : [v, () => {}]),
|
|
35
|
-
useSyncExternalStore: (sub, get) => get(),
|
|
36
|
-
}
|
|
37
|
-
const plugin = sandbox.def.factory(() => React)
|
|
38
|
-
|
|
39
|
-
let section = null
|
|
40
|
-
const ctx = {
|
|
41
|
-
get: (n) => (n === 'theme' ? { overrideTokens: () => () => {}, getTheme: () => ({ preference: 'light' }), setTheme: () => {} } : undefined),
|
|
42
|
-
effect: (f) => { const d = f(); return d || (() => {}) },
|
|
43
|
-
on: () => () => {},
|
|
44
|
-
inject: () => {},
|
|
45
|
-
slots: { inject: (n, fn) => fn(), register: (o, C) => { section = C; return () => {} } },
|
|
46
|
-
}
|
|
47
|
-
plugin.apply(ctx)
|
|
48
|
-
if (!section || !engine) throw new Error('拿不到设置页组件或引擎')
|
|
49
|
-
|
|
50
|
-
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]))
|
|
51
|
-
const VOID = new Set(['br', 'img', 'input', 'hr'])
|
|
52
|
-
|
|
53
|
-
function toHtml(node) {
|
|
54
|
-
if (node == null || node === false) return ''
|
|
55
|
-
if (typeof node === 'string' || typeof node === 'number') return esc(node)
|
|
56
|
-
if (Array.isArray(node)) return node.map(toHtml).join('')
|
|
57
|
-
const { type, props, kids } = node
|
|
58
|
-
if (typeof type === 'function') return toHtml(type({ ...props, ...props.kids }))
|
|
59
|
-
const attrs = []
|
|
60
|
-
for (const [k, v] of Object.entries(props)) {
|
|
61
|
-
if (v == null || k === 'kids' || k === 'children') continue
|
|
62
|
-
if (k === 'className') attrs.push(`class="${esc(v)}"`)
|
|
63
|
-
else if (k === 'style' && typeof v === 'object') {
|
|
64
|
-
attrs.push(`style="${Object.entries(v).map(([sk, sv]) => `${sk}:${esc(sv)}`).join(';')}"`)
|
|
65
|
-
} else if (k.startsWith('aria-') || k === 'role' || k === 'type') attrs.push(`${k}="${esc(v)}"`)
|
|
66
|
-
}
|
|
67
|
-
if (VOID.has(type)) return `<${type} ${attrs.join(' ')}>`
|
|
68
|
-
return `<${type}${attrs.length ? ' ' + attrs.join(' ') : ''}>${(kids || []).map(toHtml).join('')}</${type}>`
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const { buildTokens, cardColors } = engine
|
|
72
|
-
const scheme = engine.SCHEMES.find((s) => s.id === 'warm')
|
|
73
|
-
|
|
74
|
-
/** 一个模式下设置页会用到的 token → CSS 变量 */
|
|
75
|
-
function vars(mode) {
|
|
76
|
-
const t = buildTokens(scheme, 'deep', 'tinted', 'sidebar', 'tinted', 'accent', 'on')
|
|
77
|
-
const pick = (name, fallback) => (t[name] ? t[name][mode] : fallback)
|
|
78
|
-
const stat = (step, fallback) => pick('--dsw-static-neutral-bluish-' + step, fallback)
|
|
79
|
-
const h = mode === 'dark'
|
|
80
|
-
? { canvas: '#151517', card: '#2c2c2e', text: '#f9fafb', sec: '#cfd3d6', ter: '#adb2b8' }
|
|
81
|
-
: { canvas: '#ffffff', card: '#f5f6f7', text: '#0f1115', sec: '#61666b', ter: '#81858c' }
|
|
82
|
-
const steps = mode === 'dark'
|
|
83
|
-
? { canvas: '950', card: '850', text: '50', sec: '300', ter: '400' }
|
|
84
|
-
: { canvas: '00', card: '60', text: '1000', sec: '700', ter: '600' }
|
|
85
|
-
return {
|
|
86
|
-
canvas: stat(steps.canvas, h.canvas),
|
|
87
|
-
card: stat(steps.card, h.card),
|
|
88
|
-
vars: {
|
|
89
|
-
'--dsw-alias-label-primary': stat(steps.text, h.text),
|
|
90
|
-
'--dsw-alias-label-secondary': stat(steps.sec, h.sec),
|
|
91
|
-
'--dsw-alias-label-tertiary': stat(steps.ter, h.ter),
|
|
92
|
-
'--dsw-alias-border-l2': mode === 'dark' ? '#ffffff1f' : '#0000001a',
|
|
93
|
-
'--dsw-alias-border-l4': mode === 'dark' ? '#ffffff33' : '#00000029',
|
|
94
|
-
'--dsw-alias-interactive-bg-hover': mode === 'dark' ? '#f9fafb14' : '#12110f0f',
|
|
95
|
-
'--dsw-alias-bg-layer-1': stat(steps.card, h.card),
|
|
96
|
-
'--dsw-alias-bg-module-platform': stat(mode === 'dark' ? '800' : '60', h.card),
|
|
97
|
-
'--dsw-alias-state-error-primary': '#ef4444',
|
|
98
|
-
'--dsh-card-picked': pick('--dsh-card-picked', h.card),
|
|
99
|
-
},
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const props = {
|
|
104
|
-
getCurrent: () => scheme.id,
|
|
105
|
-
subscribe: () => () => {}, setPalette: () => {},
|
|
106
|
-
getSidebarMode: () => 'deep', subscribeSidebar: () => () => {}, setSidebarMode: () => {},
|
|
107
|
-
getTextMode: () => 'tinted', subscribeText: () => () => {}, setTextMode: () => {},
|
|
108
|
-
getRightbarMode: () => 'sidebar', subscribeRightbar: () => () => {}, setRightbarMode: () => {},
|
|
109
|
-
getScrollMode: () => 'tinted', subscribeScroll: () => () => {}, setScrollMode: () => {},
|
|
110
|
-
getControlMode: () => 'accent', subscribeControl: () => () => {}, setControlMode: () => {},
|
|
111
|
-
getMdMode: () => 'on', subscribeMd: () => () => {}, setMdMode: () => {},
|
|
112
|
-
getLangPref: () => (LANG === 'auto' ? 'auto' : LANG), getLocale: () => (LANG === 'auto' ? 'zh-CN' : LANG),
|
|
113
|
-
subscribeLocale: () => () => {}, setLangPref: () => {},
|
|
114
|
-
getHealth: () => ({ ok: true, checked: 5 }), subscribeHealth: () => () => {},
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const body = section(props)
|
|
118
|
-
// 深色必须靠 body 上的属性切换(生产环境正是 `body[data-ds-dark-theme]`),
|
|
119
|
-
// 所以明暗各出一个文件,而不是一页放两块。
|
|
120
|
-
for (const mode of ['light', 'dark']) {
|
|
121
|
-
const { canvas, vars: vv } = vars(mode)
|
|
122
|
-
const style = Object.entries(vv).map(([k, v]) => `${k}:${v}`).join(';')
|
|
123
|
-
const dark = mode === 'dark' ? ' data-ds-dark-theme' : ''
|
|
124
|
-
const html = `<!doctype html><html lang="zh"><head><meta charset="utf-8">
|
|
125
|
-
<title>设置页预览 · ${mode}</title><style>
|
|
126
|
-
html,body{margin:0;background:#0a0a0a;font-family:-apple-system,"Segoe UI","Microsoft YaHei",sans-serif}
|
|
127
|
-
.page{width:720px;padding:16px}
|
|
128
|
-
.wrap{padding:18px;border-radius:16px;border:1px solid rgba(128,128,128,.3);background:${canvas};${style};color:var(--dsw-alias-label-primary)}
|
|
129
|
-
</style><style>${engine.settingsCss}</style></head>
|
|
130
|
-
<body${dark}><div class="page"><div class="wrap">${toHtml(body)}</div></div></body></html>`
|
|
131
|
-
const out = path.join(here, `preview-${mode}${LANG === 'auto' ? '' : '-' + LANG}.html`)
|
|
132
|
-
fs.writeFileSync(out, html)
|
|
133
|
-
console.log('✓ ' + out + '(' + Math.round(html.length / 1024) + ' KB)')
|
|
134
|
-
}
|
package/tools/release.mjs
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 一键发版:npm publish + 等 registry 出现该版本 + 校验 dist-tag + 可选打 tag 推 Git。
|
|
3
|
-
*
|
|
4
|
-
* 用法:
|
|
5
|
-
* node tools/release.mjs --dry # 只做本地检查(语法、打包内容、版本可用性)
|
|
6
|
-
* node tools/release.mjs # 真发布(需要 npm 登录态)
|
|
7
|
-
* node tools/release.mjs --tag # 发布后再打 tag 并推 main + tag
|
|
8
|
-
* node tools/release.mjs --skip-publish # 只打 tag(版本已经在 registry 上了)
|
|
9
|
-
*
|
|
10
|
-
* 为什么需要它:本机 npm 默认源是 npmmirror,publish 必须显式指官方源;而 npm 对新包名
|
|
11
|
-
* 会走「暂存审核」,CLI 照样打印 `+ pkg@ver`,必须回查 registry 才知道有没有真的放出来。
|
|
12
|
-
* 另外 latest 不保证自动前移,得验一次。
|
|
13
|
-
*/
|
|
14
|
-
import { execFileSync } from 'node:child_process'
|
|
15
|
-
import { readFileSync, existsSync } from 'node:fs'
|
|
16
|
-
import path from 'node:path'
|
|
17
|
-
import { fileURLToPath } from 'node:url'
|
|
18
|
-
|
|
19
|
-
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
20
|
-
const root = path.resolve(here, '..')
|
|
21
|
-
const args = process.argv.slice(2)
|
|
22
|
-
const has = (f) => args.includes('--' + f)
|
|
23
|
-
|
|
24
|
-
const REGISTRY = 'https://registry.npmjs.org'
|
|
25
|
-
// Windows 上 `npm` 实际是 npm.cmd,execFileSync 不做 PATHEXT 解析,必须写全名。
|
|
26
|
-
const NPM = process.platform === 'win32' ? 'npm.cmd' : 'npm'
|
|
27
|
-
const CURL = process.platform === 'win32' ? 'curl.exe' : 'curl'
|
|
28
|
-
const pkg = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
29
|
-
const { name, version } = pkg
|
|
30
|
-
const tag = 'v' + version
|
|
31
|
-
|
|
32
|
-
const run = (cmd, cmdArgs, opts = {}) => {
|
|
33
|
-
const out = execFileSync(cmd, cmdArgs, { cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], ...opts })
|
|
34
|
-
return typeof out === 'string' ? out.trim() : ''
|
|
35
|
-
}
|
|
36
|
-
const sh = (cmd) => run(process.platform === 'win32' ? 'cmd' : 'sh', process.platform === 'win32' ? ['/c', cmd] : ['-c', cmd])
|
|
37
|
-
const ok = (m) => console.log(' ✓ ' + m)
|
|
38
|
-
const die = (m) => { console.error(' ✗ ' + m); process.exit(1) }
|
|
39
|
-
|
|
40
|
-
console.log(`\n发版 ${name}@${version}${has('dry') ? '(dry run)' : ''}\n`)
|
|
41
|
-
|
|
42
|
-
// 1. 本地检查
|
|
43
|
-
try { run(process.execPath, ['--check', 'client.js']); ok('client.js 语法') } catch { die('client.js 语法错误') }
|
|
44
|
-
// 1b. dsh.client 必须配 exports["./client"] —— 缺了宿主 client-modules 会直接启动失败
|
|
45
|
-
// (2026-10-21 实测:加 exports 却漏掉 ./client 导致重启后整个 profile 起不来)
|
|
46
|
-
{
|
|
47
|
-
const manifest = JSON.parse(readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
48
|
-
if (manifest.dsh && manifest.dsh.client) {
|
|
49
|
-
const clientRel = manifest.exports && manifest.exports['./client']
|
|
50
|
-
if (!clientRel) die('dsh.client 已声明但 exports 缺 "./client"(宿主会报 exports no "./client" bundle)')
|
|
51
|
-
const rel = typeof clientRel === 'string' ? clientRel : clientRel.default
|
|
52
|
-
if (typeof rel !== 'string' || !existsSync(path.join(root, rel))) die('exports["./client"] 指向的文件不存在:' + rel)
|
|
53
|
-
ok('client 子路径可解析:exports["./client"] → ' + rel)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const dirty = sh('git status --porcelain')
|
|
58
|
-
if (dirty) die('工作区不干净,先提交:\n' + dirty)
|
|
59
|
-
ok('工作区干净')
|
|
60
|
-
|
|
61
|
-
const packed = run(process.platform === 'win32' ? 'cmd' : 'sh',
|
|
62
|
-
process.platform === 'win32' ? ['/c', 'npm pack --dry-run --json'] : ['-c', 'npm pack --dry-run --json'])
|
|
63
|
-
try {
|
|
64
|
-
const info = JSON.parse(packed.slice(packed.indexOf('[')))[0]
|
|
65
|
-
ok(`打包 ${info.entryCount} 个文件 / ${(info.size / 1024).toFixed(1)} KB`)
|
|
66
|
-
} catch { die('读不出 npm pack --dry-run 的结果') }
|
|
67
|
-
|
|
68
|
-
// 2. 版本是否已在 registry
|
|
69
|
-
let remote = null
|
|
70
|
-
try { remote = JSON.parse(sh(`${NPM} view ${name} versions --json --registry=${REGISTRY}`)) } catch { remote = null }
|
|
71
|
-
const remoteList = Array.isArray(remote) ? remote : remote ? [remote] : []
|
|
72
|
-
if (remoteList.includes(version)) {
|
|
73
|
-
ok(`registry 上已有 ${version}`)
|
|
74
|
-
if (!has('skip-publish')) die('该版本已发布,先改 package.json 的 version')
|
|
75
|
-
} else {
|
|
76
|
-
ok(`registry 上没有 ${version}(当前 ${remoteList.length} 个版本)`)
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
if (has('dry')) { console.log('\n干跑结束,没有发布。\n'); process.exit(0) }
|
|
80
|
-
|
|
81
|
-
// 3. 发布
|
|
82
|
-
if (!has('skip-publish')) {
|
|
83
|
-
console.log('\n发布中…')
|
|
84
|
-
try {
|
|
85
|
-
// Node 20+ 在 Windows 上不允许直接 spawn .cmd(CVE-2024-27980 的缓解),
|
|
86
|
-
// 必须走 shell;所以统一用 sh(),而不是 execFileSync。
|
|
87
|
-
const out = sh(`${NPM} publish --registry=${REGISTRY}`)
|
|
88
|
-
if (out) console.log(out)
|
|
89
|
-
} catch (err) {
|
|
90
|
-
die('npm publish 失败:' + (err.stderr || err.message))
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// 4. 回查 registry(新包会进暂存审核,CLI 打印成功不代表已放出)
|
|
95
|
-
console.log('\n等 registry 出现该版本(最多 10 分钟,暂存审核通过前会一直 404)…')
|
|
96
|
-
const started = Date.now()
|
|
97
|
-
let live = false
|
|
98
|
-
while (Date.now() - started < 10 * 60 * 1000) {
|
|
99
|
-
try {
|
|
100
|
-
const code = sh(`${CURL} -s -o ${process.platform === 'win32' ? 'NUL' : '/dev/null'} -w "%{http_code}" ${REGISTRY}/${name}/${version}`)
|
|
101
|
-
if (code.trim() === '200') { live = true; break }
|
|
102
|
-
} catch { /* 网络抖动继续等 */ }
|
|
103
|
-
await new Promise((r) => setTimeout(r, 15000))
|
|
104
|
-
}
|
|
105
|
-
if (!live) die('10 分钟内没出现在 registry —— 可能仍在审核,稍后手动跑 npm view ' + name + ' versions')
|
|
106
|
-
ok(`registry 上已可下载 ${version}`)
|
|
107
|
-
|
|
108
|
-
// 5. dist-tag
|
|
109
|
-
const latest = sh(`${NPM} view ${name} dist-tags.latest --registry=${REGISTRY}`).trim()
|
|
110
|
-
if (latest === version) ok(`latest = ${version}`)
|
|
111
|
-
else {
|
|
112
|
-
console.log(` ! latest 指向 ${latest},手工前移:npm dist-tag add ${name}@${version} latest --registry=${REGISTRY}`)
|
|
113
|
-
try {
|
|
114
|
-
sh(`${NPM} dist-tag add ${name}@${version} latest --registry=${REGISTRY}`)
|
|
115
|
-
ok(`已前移 latest → ${version}`)
|
|
116
|
-
} catch { console.log(' ! 前移失败,需要人工处理') }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// 6. Git tag(可选)
|
|
120
|
-
if (has('tag')) {
|
|
121
|
-
try {
|
|
122
|
-
sh(`git tag -a ${tag} -m "${name} ${version}"`)
|
|
123
|
-
sh(`git push origin main ${tag}`)
|
|
124
|
-
ok(`已打 tag 并推送 ${tag}`)
|
|
125
|
-
} catch (err) {
|
|
126
|
-
console.log(' ! tag/推送失败:' + (err.stderr || err.message))
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
console.log(`\n完成:https://www.npmjs.com/package/${name}/v/${version}\n`)
|
package/tools/sheet.mjs
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 生成配色看板 HTML(截图回归的"被测页面")。
|
|
3
|
-
*
|
|
4
|
-
* 为什么需要它:插件的真实渲染在 DSH 的 Web GUI 里,而那个页面要鉴权 token,
|
|
5
|
-
* 无头浏览器拿不到;但**配色系统本身的视觉回归**不需要整个 App —— 把每个 token
|
|
6
|
-
* 渲染成真实的界面块(侧栏条目、气泡、按钮、开关、滚动条、正文层级),
|
|
7
|
-
* 就足以发现"对比度掉了、色相反了、层级糊了"这类问题。
|
|
8
|
-
*
|
|
9
|
-
* 颜色全部来自 client.js 的生产引擎(经 __DSH_THEME_PALETTE_DEV__ 钩子),
|
|
10
|
-
* 所以看板 = 上线配色的忠实投影,不是另画一套。
|
|
11
|
-
*
|
|
12
|
-
* 用法:node tools/sheet.mjs → 生成 tools/sheet.html
|
|
13
|
-
* node tools/shoot.mjs → 用无头 Chrome 截 4 张(每套配色一张,含明暗两版)
|
|
14
|
-
*/
|
|
15
|
-
import fs from 'node:fs'
|
|
16
|
-
import path from 'node:path'
|
|
17
|
-
import { fileURLToPath } from 'node:url'
|
|
18
|
-
|
|
19
|
-
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
20
|
-
const root = path.resolve(here, '..')
|
|
21
|
-
const source = fs.readFileSync(path.join(root, 'client.js'), 'utf8')
|
|
22
|
-
|
|
23
|
-
let engine = null
|
|
24
|
-
const sandbox = {
|
|
25
|
-
window: {
|
|
26
|
-
__ModuleLoader__: { load: (d) => { sandbox.def = d } },
|
|
27
|
-
__DSH_THEME_PALETTE_DEV__: (api) => { engine = api },
|
|
28
|
-
},
|
|
29
|
-
localStorage: { getItem: () => null, setItem: () => {} },
|
|
30
|
-
document: { head: { appendChild() {} }, createElement: () => ({ dataset: {}, remove() {} }), querySelector: () => null, documentElement: { lang: 'zh' } },
|
|
31
|
-
navigator: { language: 'zh' },
|
|
32
|
-
}
|
|
33
|
-
new Function('window', 'localStorage', 'document', 'navigator', source)(
|
|
34
|
-
sandbox.window, sandbox.localStorage, sandbox.document, sandbox.navigator,
|
|
35
|
-
)
|
|
36
|
-
sandbox.def.factory(() => ({ createElement: () => null, useState: () => [null, () => {}], useSyncExternalStore: () => 'mono' }))
|
|
37
|
-
if (!engine) throw new Error('生产引擎没有暴露 dev 钩子')
|
|
38
|
-
|
|
39
|
-
const { buildTokens, SCHEMES } = engine
|
|
40
|
-
|
|
41
|
-
/** 宿主原值(黑白配色走宿主,缺的 token 用它兜底) */
|
|
42
|
-
const HOST = {
|
|
43
|
-
light: {
|
|
44
|
-
canvas: '#ffffff', card: '#f5f6f7', overlay: '#e9ecf2', sidebar: '#f9fafb', hover: '#f1f3f5', active: '#ebeef2',
|
|
45
|
-
text: '#0f1115', secondary: '#61666b', tertiary: '#81858c', caption: '#adb2b8',
|
|
46
|
-
link: '#4176e6', bubble: '#edf3fe', button: '#ec1313', switchOn: '#0f1115',
|
|
47
|
-
scroll: '#e5e5e5', scrollHover: '#d4d4d4',
|
|
48
|
-
},
|
|
49
|
-
dark: {
|
|
50
|
-
canvas: '#151517', card: '#2c2c2e', overlay: '#61666b', sidebar: '#1b1b1c', hover: '#2c2c2e', active: '#43454a',
|
|
51
|
-
text: '#f9fafb', secondary: '#cfd3d6', tertiary: '#adb2b8', caption: '#81858c',
|
|
52
|
-
link: '#7aaaff', bubble: '#2c2c2e', button: '#f9fafb', switchOn: '#f9fafb',
|
|
53
|
-
scroll: '#3c3c3d', scrollHover: '#545557',
|
|
54
|
-
},
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** 取某套配色在某模式下的界面色板(缺省回落到宿主值)。 */
|
|
58
|
-
const isDarkFallback = (h) => h.card
|
|
59
|
-
|
|
60
|
-
function palette(scheme, mode) {
|
|
61
|
-
const t = buildTokens(scheme, 'deep', 'tinted', 'sidebar', 'tinted', 'accent')
|
|
62
|
-
const pick = (name, fallback) => {
|
|
63
|
-
const entry = t[name]
|
|
64
|
-
return entry ? entry[mode] : fallback
|
|
65
|
-
}
|
|
66
|
-
// 注意:色阶 token 的 light/dark 两值是同一个(色阶本身与模式无关),
|
|
67
|
-
// 真正的"深色画布"是**另一个步**(950 而不是 00)。所以按宿主的别名映射取步号。
|
|
68
|
-
const steps = mode === 'dark'
|
|
69
|
-
? { canvas: '950', card: '850', overlay: '800', text: '50', secondary: '300', tertiary: '400' }
|
|
70
|
-
: { canvas: '00', card: '60', overlay: '150', text: '1000', secondary: '700', tertiary: '600' }
|
|
71
|
-
const stat = (step, fallback) => pick('--dsw-static-neutral-bluish-' + step, fallback)
|
|
72
|
-
const h = HOST[mode]
|
|
73
|
-
return {
|
|
74
|
-
canvas: stat(steps.canvas, h.canvas),
|
|
75
|
-
card: stat(steps.card, h.card),
|
|
76
|
-
overlay: stat(steps.overlay, h.overlay),
|
|
77
|
-
sidebar: pick('--dsw-specific-sidebar-fill', h.sidebar),
|
|
78
|
-
hover: pick('--dsw-specific-sidebar-nav-item-hover', h.hover),
|
|
79
|
-
active: pick('--dsw-specific-sidebar-nav-item-active', h.active),
|
|
80
|
-
text: stat(steps.text, h.text),
|
|
81
|
-
secondary: stat(steps.secondary, h.secondary),
|
|
82
|
-
tertiary: stat(steps.tertiary, h.tertiary),
|
|
83
|
-
caption: pick('--dsw-alias-label-caption', h.caption),
|
|
84
|
-
link: pick('--dsw-alias-link', h.link),
|
|
85
|
-
bubble: pick('--dsw-specific-bubble', h.bubble),
|
|
86
|
-
button: pick('--dsw-alias-button-primary-fill', h.button),
|
|
87
|
-
switchOn: pick('--dsw-alias-brand-primary', h.switchOn),
|
|
88
|
-
scroll: stat('200', h.scroll),
|
|
89
|
-
scrollHover: stat('300', h.scrollHover),
|
|
90
|
-
code: pick('--dsh-code-fill', isDarkFallback(h)),
|
|
91
|
-
codeInline: pick('--dsh-code-inline', h.card),
|
|
92
|
-
picked: pick('--dsh-card-picked', h.card),
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]))
|
|
97
|
-
|
|
98
|
-
function panel(scheme, mode) {
|
|
99
|
-
const c = palette(scheme, mode)
|
|
100
|
-
const isDark = mode === 'dark'
|
|
101
|
-
const label = mode === 'dark' ? '深色' : '浅色'
|
|
102
|
-
const bg = c.canvas
|
|
103
|
-
const code = c.code
|
|
104
|
-
return `
|
|
105
|
-
<section class="panel" data-scheme="${scheme.id}" data-mode="${mode}" style="
|
|
106
|
-
--canvas:${c.canvas}; --card:${c.card}; --sidebar:${c.sidebar}; --hover:${c.hover}; --active:${c.active};
|
|
107
|
-
--text:${c.text}; --secondary:${c.secondary}; --tertiary:${c.tertiary}; --caption:${c.caption};
|
|
108
|
-
--link:${c.link}; --bubble:${c.bubble}; --button:${c.button}; --switch:${c.switchOn};
|
|
109
|
-
--scroll:${c.scroll}; --scrollHover:${c.scrollHover}; --picked:${c.picked}; --code:${c.code}; --codeInline:${c.codeInline};
|
|
110
|
-
background:${bg};">
|
|
111
|
-
<header>
|
|
112
|
-
<b>${esc(scheme.label)}</b><span class="dim">${label}模式</span>
|
|
113
|
-
<span class="hexes">画布 ${c.canvas} · 侧栏 ${c.sidebar} · 卡层 ${c.card} · 代码 ${c.code} · 气泡 ${c.bubble}</span>
|
|
114
|
-
</header>
|
|
115
|
-
|
|
116
|
-
<div class="frame">
|
|
117
|
-
<aside class="col-side">
|
|
118
|
-
<div class="brand">deepseek <b>HARNESS</b></div>
|
|
119
|
-
<div class="new-chat">+ 新会话</div>
|
|
120
|
-
<div class="row active">论衡插件其他文类模板需求</div>
|
|
121
|
-
<div class="row">定位错配审计-2026-10-20</div>
|
|
122
|
-
<div class="row hover">行政国家理念跨国流动</div>
|
|
123
|
-
<div class="row dim2">1 天前</div>
|
|
124
|
-
<div class="section-label">工作区</div>
|
|
125
|
-
<div class="row">论衡开发</div>
|
|
126
|
-
<div class="scroll-track"><i></i></div>
|
|
127
|
-
</aside>
|
|
128
|
-
|
|
129
|
-
<main class="col-main">
|
|
130
|
-
<div class="tabs"><span class="on">对话</span><span>轨迹</span><span>上下文</span><span>画廊</span></div>
|
|
131
|
-
<h3>其余批次可以先做;B3/B4/B5 各自会改变判定结果</h3>
|
|
132
|
-
<p class="body">正文示例:这段文字用来检查正文在画布上的可读性,以及它是否带上配色的色味。
|
|
133
|
-
次级说明文字出现在这里,<span class="sec">像这样</span>;更弱的注释<span class="cap">(例如 13:17)</span>再弱一档。
|
|
134
|
-
链接形如 <a>定位错配审计</a>,保证不丢锚点感。</p>
|
|
135
|
-
<div class="bubble">主人发出的消息:这个对话条目的文字底色会不会太红?</div>
|
|
136
|
-
<div class="card">
|
|
137
|
-
<div class="card-title">定位错配审计-2026-10-20-人文社科长文-v1.md</div>
|
|
138
|
-
<div class="card-desc">人文社科长文定位 × 论衡 v19.21.0 的错配审计</div>
|
|
139
|
-
</div>
|
|
140
|
-
<pre class="code">const theme = ctx.get('theme')
|
|
141
|
-
theme.overrideTokens('mine', { '--dsw-alias-link': { light, dark } })</pre>
|
|
142
|
-
<div class="controls">
|
|
143
|
-
<button class="btn">打开</button>
|
|
144
|
-
<span class="chip">定题 1/1</span>
|
|
145
|
-
<span class="chip ghost">检索 0/3</span>
|
|
146
|
-
<span class="switch on"><i></i></span>
|
|
147
|
-
<span class="switch"><i></i></span>
|
|
148
|
-
<span class="state err">错误</span><span class="state warn">警告</span><span class="state ok">成功</span>
|
|
149
|
-
</div>
|
|
150
|
-
</main>
|
|
151
|
-
</div>
|
|
152
|
-
</section>`
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
const css = `
|
|
156
|
-
*{box-sizing:border-box}
|
|
157
|
-
html,body{margin:0;padding:0;background:#0a0a0a}
|
|
158
|
-
.page{width:720px;padding:16px;display:flex;flex-direction:column;gap:16px;font-family:-apple-system,"Segoe UI","Microsoft YaHei",sans-serif}
|
|
159
|
-
.panel{border-radius:14px;overflow:hidden;color:var(--text);font-size:13px;line-height:1.55;border:1px solid rgba(128,128,128,.25)}
|
|
160
|
-
.panel header{display:flex;align-items:baseline;gap:10px;padding:10px 14px;border-bottom:1px solid rgba(128,128,128,.18)}
|
|
161
|
-
.panel header b{font-size:14px}
|
|
162
|
-
.panel .dim{opacity:.6;font-size:12px}
|
|
163
|
-
.hexes{margin-left:auto;font:11px/1.4 ui-monospace,Consolas,monospace;opacity:.55}
|
|
164
|
-
.frame{display:flex;min-height:330px}
|
|
165
|
-
.col-side{width:210px;background:var(--sidebar);padding:10px 8px;display:flex;flex-direction:column;gap:4px}
|
|
166
|
-
.brand{font-size:12px;font-weight:600;padding:2px 6px 8px;letter-spacing:.2px}
|
|
167
|
-
.new-chat{border:1px solid var(--hover);border-radius:9px;padding:7px 10px;text-align:center;font-size:12px;margin-bottom:6px}
|
|
168
|
-
.row{padding:6px 8px;border-radius:7px;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
169
|
-
.row.active{background:var(--active)}
|
|
170
|
-
.row.hover{background:var(--hover)}
|
|
171
|
-
.row.dim2{color:var(--tertiary);font-size:11px}
|
|
172
|
-
.section-label{color:var(--tertiary);font-size:11px;padding:8px 8px 2px}
|
|
173
|
-
.scroll-track{position:relative;flex:1;min-height:16px}
|
|
174
|
-
.scroll-track i{position:absolute;right:-6px;top:6px;bottom:6px;width:4px;border-radius:3px;background:var(--scroll)}
|
|
175
|
-
.col-main{flex:1;background:var(--canvas);padding:10px 14px;display:flex;flex-direction:column;gap:9px;min-width:0}
|
|
176
|
-
.tabs{display:flex;gap:14px;font-size:12px;color:var(--tertiary);border-bottom:1px solid var(--hover);padding-bottom:6px}
|
|
177
|
-
.tabs .on{color:var(--text);box-shadow:inset 0 -2px 0 var(--button)}
|
|
178
|
-
h3{margin:2px 0 0;font-size:15px;line-height:1.45;font-weight:600}
|
|
179
|
-
.body{margin:0;font-size:13px}
|
|
180
|
-
.sec{color:var(--secondary)}
|
|
181
|
-
.cap{color:var(--caption);font-size:12px}
|
|
182
|
-
a{color:var(--link);text-decoration:none;border-bottom:1px dotted color-mix(in srgb,var(--link) 60%,transparent)}
|
|
183
|
-
.bubble{align-self:flex-end;max-width:78%;background:var(--bubble);border-radius:14px;padding:9px 12px;font-size:12.5px;line-height:1.5}
|
|
184
|
-
.card{background:var(--card);border-radius:10px;padding:9px 11px;border:1px solid var(--hover)}
|
|
185
|
-
.card-title{font-size:12.5px}
|
|
186
|
-
.card-desc{color:var(--tertiary);font-size:11.5px;margin-top:2px}
|
|
187
|
-
.code{margin:0;background:var(--code);border-radius:9px;padding:9px 11px;font:11.5px/1.5 ui-monospace,Consolas,monospace;color:var(--secondary);white-space:pre-wrap}
|
|
188
|
-
.controls{display:flex;align-items:center;gap:8px;flex-wrap:wrap;margin-top:2px}
|
|
189
|
-
.btn{background:var(--button);color:var(--canvas);border:0;border-radius:8px;padding:6px 14px;font:inherit;font-size:12px}
|
|
190
|
-
.chip{border:1px solid var(--hover);border-radius:999px;padding:2px 9px;font-size:11.5px;color:var(--secondary)}
|
|
191
|
-
.chip.ghost{border-style:dashed}
|
|
192
|
-
.switch{width:26px;height:14px;border-radius:999px;background:var(--hover);position:relative;display:inline-block}
|
|
193
|
-
.switch i{position:absolute;top:4px;left:4px;width:6px;height:6px;border-radius:50%;background:var(--card)}
|
|
194
|
-
.switch.on{background:var(--switchOn)}
|
|
195
|
-
.switch.on i{left:16px;background:var(--canvas)}
|
|
196
|
-
.state{font-size:11.5px;padding:1px 6px;border-radius:6px}
|
|
197
|
-
.state.err{color:#ef4444}.state.warn{color:#f59e0b}.state.ok{color:#22c55e}
|
|
198
|
-
`
|
|
199
|
-
|
|
200
|
-
const body = SCHEMES.map((s, i) => `
|
|
201
|
-
<div class="view" data-i="${i + 1}">${panel(s, 'light')}${panel(s, 'dark')}</div>`).join('')
|
|
202
|
-
|
|
203
|
-
const html = `<!doctype html>
|
|
204
|
-
<html lang="zh"><head><meta charset="utf-8"><title>dsh-theme-palette · 配色看板</title>
|
|
205
|
-
<style>${css}
|
|
206
|
-
.view{display:none}
|
|
207
|
-
.view.on{display:flex;flex-direction:column;gap:16px}
|
|
208
|
-
</style></head>
|
|
209
|
-
<body><div class="page">${body}</div>
|
|
210
|
-
<script>
|
|
211
|
-
// ?v=N 选第 N 套配色(默认 1);每张截图含该套的浅色 + 深色两个面板
|
|
212
|
-
const v = Number(new URLSearchParams(location.search).get('v') || 1)
|
|
213
|
-
document.querySelectorAll('.view').forEach(el => el.classList.toggle('on', Number(el.dataset.i) === v))
|
|
214
|
-
document.documentElement.dataset.ready = '1'
|
|
215
|
-
</script></body></html>`
|
|
216
|
-
|
|
217
|
-
const out = path.join(here, 'sheet.html')
|
|
218
|
-
fs.writeFileSync(out, html)
|
|
219
|
-
console.log('✓ ' + out + '(' + Math.round(html.length / 1024) + ' KB,' + SCHEMES.length + ' 个视图)')
|
package/tools/shoot-desktop.mjs
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 桌面版截图回归:通过 CDP 连到运行中的 DSH 桌面窗口,按「配色 × 明暗」逐张截图。
|
|
3
|
-
*
|
|
4
|
-
* 前提:DSH 桌面版必须以调试端口启动一次(见 tools/README.md):
|
|
5
|
-
* "DeepSeek Harness.exe" --remote-debugging-port=9333
|
|
6
|
-
*
|
|
7
|
-
* 做法:不改配置、不点菜单 —— 走插件自己的开发期接口
|
|
8
|
-
* window.__DSH_THEME_PALETTE_DEV__.setPalette('warm')
|
|
9
|
-
* window.__DSH_THEME_PALETTE_DEV__.setTheme('dark')
|
|
10
|
-
* 抓完会把「原来的配色与明暗偏好」还原(theme 会写进宿主设置,不还会改用户配置)。
|
|
11
|
-
*
|
|
12
|
-
* 用法:
|
|
13
|
-
* node tools/shoot-desktop.mjs # 抓全部 4 套 × 浅/深 → tools/out/<时间戳>/
|
|
14
|
-
* node tools/shoot-desktop.mjs --only warm,dark # 只抓指定配色
|
|
15
|
-
* node tools/shoot-desktop.mjs --port 9333 --out tools/out/run1
|
|
16
|
-
*/
|
|
17
|
-
import fs from 'node:fs'
|
|
18
|
-
import path from 'node:path'
|
|
19
|
-
import { createHash } from 'node:crypto'
|
|
20
|
-
import { setTimeout as sleep } from 'node:timers/promises'
|
|
21
|
-
import { fileURLToPath } from 'node:url'
|
|
22
|
-
|
|
23
|
-
const here = path.dirname(fileURLToPath(import.meta.url))
|
|
24
|
-
const argv = process.argv.slice(2)
|
|
25
|
-
const arg = (k, d) => { const i = argv.indexOf('--' + k); return i >= 0 ? argv[i + 1] : d }
|
|
26
|
-
const port = Number(arg('port', '9333'))
|
|
27
|
-
const only = String(arg('only', '')).split(',').map((s) => s.trim()).filter(Boolean)
|
|
28
|
-
const stamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)
|
|
29
|
-
const outDir = path.resolve(arg('out', path.join(here, 'out', stamp)))
|
|
30
|
-
|
|
31
|
-
class CDP {
|
|
32
|
-
constructor(ws) { this.ws = ws; this.id = 0; this.pending = new Map(); this.events = new Map() }
|
|
33
|
-
static async attach(url) {
|
|
34
|
-
const ws = new WebSocket(url)
|
|
35
|
-
const c = new CDP(ws)
|
|
36
|
-
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = () => rej(new Error('ws 连接失败')) })
|
|
37
|
-
ws.onmessage = (m) => {
|
|
38
|
-
const msg = JSON.parse(m.data)
|
|
39
|
-
if (msg.id && c.pending.has(msg.id)) {
|
|
40
|
-
const { res, rej } = c.pending.get(msg.id)
|
|
41
|
-
c.pending.delete(msg.id)
|
|
42
|
-
msg.error ? rej(new Error(JSON.stringify(msg.error))) : res(msg.result)
|
|
43
|
-
} else if (msg.method) {
|
|
44
|
-
const list = c.events.get(msg.method) || []
|
|
45
|
-
list.forEach((fn) => fn(msg.params))
|
|
46
|
-
c.events.set(msg.method, [])
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return c
|
|
50
|
-
}
|
|
51
|
-
send(method, params = {}) {
|
|
52
|
-
const id = ++this.id
|
|
53
|
-
this.ws.send(JSON.stringify({ id, method, params }))
|
|
54
|
-
return new Promise((res, rej) => this.pending.set(id, { res, rej }))
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async function pickPageTarget() {
|
|
59
|
-
const res = await fetch(`http://127.0.0.1:${port}/json/list`)
|
|
60
|
-
if (!res.ok) throw new Error('拿不到调试目标')
|
|
61
|
-
const list = await res.json()
|
|
62
|
-
const pages = list.filter((t) => t.type === 'page' && /^https?:/.test(t.url))
|
|
63
|
-
if (pages.length === 0) throw new Error('没有找到页面目标(窗口是否已加载?)')
|
|
64
|
-
// 优先 dsh 的页面
|
|
65
|
-
return pages.find((t) => /dsh|127\.0\.0\.1|localhost/.test(t.url)) || pages[0]
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const evaluate = async (cdp, expression) => {
|
|
69
|
-
const r = await cdp.send('Runtime.evaluate', { expression, returnByValue: true, awaitPromise: true })
|
|
70
|
-
if (r.exceptionDetails) throw new Error(expression + ' → ' + JSON.stringify(r.exceptionDetails).slice(0, 200))
|
|
71
|
-
return r.result.value
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
async function main() {
|
|
75
|
-
const target = await pickPageTarget()
|
|
76
|
-
const cdp = await CDP.attach(target.webSocketDebuggerUrl)
|
|
77
|
-
await cdp.send('Page.enable')
|
|
78
|
-
await cdp.send('Runtime.enable')
|
|
79
|
-
|
|
80
|
-
const ready = await evaluate(cdp, "!!(window.__DSH_THEME_PALETTE_DEV__ && window.__DSH_THEME_PALETTE_DEV__.attach)")
|
|
81
|
-
if (!ready) throw new Error('插件未暴露自动化接口(版本 < 1.0.1?或插件没加载)')
|
|
82
|
-
|
|
83
|
-
const api = 'window.__DSH_THEME_PALETTE_DEV__'
|
|
84
|
-
const all = only.length ? only : await evaluate(cdp, `${api}.schemes`)
|
|
85
|
-
const originalPref = await evaluate(cdp, `${api}.getPreference()`)
|
|
86
|
-
const originalPalette = await evaluate(cdp, "localStorage.getItem('dsh-theme-palette.v1') || 'mono'")
|
|
87
|
-
fs.mkdirSync(outDir, { recursive: true })
|
|
88
|
-
const manifest = []
|
|
89
|
-
|
|
90
|
-
for (const scheme of all) {
|
|
91
|
-
for (const mode of ['light', 'dark']) {
|
|
92
|
-
await evaluate(cdp, `window.__DSH_THEME_PALETTE_DEV__.setPalette(${JSON.stringify(scheme)})`)
|
|
93
|
-
await evaluate(cdp, `window.__DSH_THEME_PALETTE_DEV__.setTheme(${JSON.stringify(mode)})`)
|
|
94
|
-
await sleep(700) // 等 presenter 落样式 + 字体
|
|
95
|
-
const shot = await cdp.send('Page.captureScreenshot', { format: 'png', captureBeyondViewport: false })
|
|
96
|
-
const buf = Buffer.from(shot.data, 'base64')
|
|
97
|
-
const name = `${scheme}-${mode}.png`
|
|
98
|
-
fs.writeFileSync(path.join(outDir, name), buf)
|
|
99
|
-
manifest.push({ name, scheme, mode, bytes: buf.length, sha256: createHash('sha256').update(buf).digest('hex').slice(0, 16) })
|
|
100
|
-
console.log(`✓ ${name} ${(buf.length / 1024).toFixed(0)} KB`)
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
// 还原用户原来的偏好(theme 会写进宿主设置,不还会改用户配置)
|
|
105
|
-
await evaluate(cdp, `${api}.setPalette(${JSON.stringify(originalPalette)})`)
|
|
106
|
-
await evaluate(cdp, `${api}.setTheme(${JSON.stringify(originalPref)})`)
|
|
107
|
-
fs.writeFileSync(path.join(outDir, 'manifest.json'), JSON.stringify({ target: target.url, viewport: await evaluate(cdp, '[innerWidth, innerHeight]'), originalPreference: originalPref, originalPalette, shots: manifest }, null, 2) + '\n')
|
|
108
|
-
console.log(`\n输出目录:${outDir}`)
|
|
109
|
-
console.log(`已还原明暗偏好:${original}`)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
main().catch((e) => { console.error('✗ ' + e.message); process.exit(1) })
|