dsh-visualkit 0.0.0-stage → 0.0.2
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 +74 -0
- package/LICENSE +26 -0
- package/README.md +443 -2
- package/THIRD_PARTY.md +55 -0
- package/cordis.patch.yml +29 -0
- package/data/assets/README.md +38 -0
- package/data/assets/icons/brands/bilibili.svg +1 -0
- package/data/assets/icons/brands/discord.svg +1 -0
- package/data/assets/icons/brands/neteasecloudmusic.svg +1 -0
- package/data/assets/icons/brands/qq.svg +1 -0
- package/data/assets/icons/brands/sinaweibo.svg +1 -0
- package/data/assets/icons/brands/steam.svg +1 -0
- package/data/assets/icons/brands/telegram.svg +1 -0
- package/data/assets/icons/brands/tiktok.svg +1 -0
- package/data/assets/icons/brands/twitch.svg +1 -0
- package/data/assets/icons/brands/wechat.svg +1 -0
- package/data/assets/icons/brands/x.svg +1 -0
- package/data/assets/icons/brands/youtube.svg +1 -0
- package/data/assets/icons/lucide/bell.svg +1 -0
- package/data/assets/icons/lucide/calendar.svg +1 -0
- package/data/assets/icons/lucide/clock.svg +1 -0
- package/data/assets/icons/lucide/gamepad-2.svg +1 -0
- package/data/assets/icons/lucide/gift.svg +1 -0
- package/data/assets/icons/lucide/heart.svg +1 -0
- package/data/assets/icons/lucide/mic.svg +1 -0
- package/data/assets/icons/lucide/play.svg +1 -0
- package/data/assets/icons/lucide/star.svg +1 -0
- package/data/assets/icons/lucide/video.svg +1 -0
- package/data/assets/icons/phosphor/LICENSE +21 -0
- package/data/assets/icons/phosphor/atom-duotone.svg +1 -0
- package/data/assets/icons/phosphor/binary-duotone.svg +1 -0
- package/data/assets/icons/phosphor/blueprint-duotone.svg +1 -0
- package/data/assets/icons/phosphor/circuitry-duotone.svg +1 -0
- package/data/assets/icons/phosphor/cloud-arrow-up-duotone.svg +1 -0
- package/data/assets/icons/phosphor/code-duotone.svg +1 -0
- package/data/assets/icons/phosphor/compass-duotone.svg +1 -0
- package/data/assets/icons/phosphor/cpu-duotone.svg +1 -0
- package/data/assets/icons/phosphor/cube-duotone.svg +1 -0
- package/data/assets/icons/phosphor/database-duotone.svg +1 -0
- package/data/assets/icons/phosphor/flask-duotone.svg +1 -0
- package/data/assets/icons/phosphor/frame-corners-duotone.svg +1 -0
- package/data/assets/icons/phosphor/function-duotone.svg +1 -0
- package/data/assets/icons/phosphor/hard-drives-duotone.svg +1 -0
- package/data/assets/icons/phosphor/hexagon-duotone.svg +1 -0
- package/data/assets/icons/phosphor/infinity-duotone.svg +1 -0
- package/data/assets/icons/phosphor/lightning-duotone.svg +1 -0
- package/data/assets/icons/phosphor/magic-wand-duotone.svg +1 -0
- package/data/assets/icons/phosphor/moon-stars-duotone.svg +1 -0
- package/data/assets/icons/phosphor/paint-brush-duotone.svg +1 -0
- package/data/assets/icons/phosphor/pen-nib-duotone.svg +1 -0
- package/data/assets/icons/phosphor/planet-duotone.svg +1 -0
- package/data/assets/icons/phosphor/rocket-launch-duotone.svg +1 -0
- package/data/assets/icons/phosphor/ruler-duotone.svg +1 -0
- package/data/assets/icons/phosphor/selection-plus-duotone.svg +1 -0
- package/data/assets/icons/phosphor/shapes-duotone.svg +1 -0
- package/data/assets/icons/phosphor/sparkle-duotone.svg +1 -0
- package/data/assets/icons/phosphor/sun-horizon-duotone.svg +1 -0
- package/data/assets/icons/phosphor/terminal-window-duotone.svg +1 -0
- package/data/assets/icons/phosphor/triangle-duotone.svg +1 -0
- package/data/assets/icons/phosphor/waveform-duotone.svg +1 -0
- package/data/assets/icons/tabler/alarm.svg +1 -0
- package/data/assets/icons/tabler/badge-ad.svg +1 -0
- package/data/assets/icons/tabler/bell.svg +1 -0
- package/data/assets/icons/tabler/bolt.svg +1 -0
- package/data/assets/icons/tabler/brand-bilibili.svg +1 -0
- package/data/assets/icons/tabler/brand-discord.svg +1 -0
- package/data/assets/icons/tabler/brand-qq.svg +1 -0
- package/data/assets/icons/tabler/brand-twitch.svg +1 -0
- package/data/assets/icons/tabler/brand-wechat.svg +1 -0
- package/data/assets/icons/tabler/brand-weibo.svg +1 -0
- package/data/assets/icons/tabler/brand-youtube.svg +1 -0
- package/data/assets/icons/tabler/broadcast.svg +1 -0
- package/data/assets/icons/tabler/calendar-event.svg +1 -0
- package/data/assets/icons/tabler/calendar-week.svg +1 -0
- package/data/assets/icons/tabler/clock-hour-3.svg +1 -0
- package/data/assets/icons/tabler/coin.svg +1 -0
- package/data/assets/icons/tabler/crown.svg +1 -0
- package/data/assets/icons/tabler/currency-yen.svg +1 -0
- package/data/assets/icons/tabler/device-gamepad-2.svg +1 -0
- package/data/assets/icons/tabler/diamond.svg +1 -0
- package/data/assets/icons/tabler/flame.svg +1 -0
- package/data/assets/icons/tabler/gift.svg +1 -0
- package/data/assets/icons/tabler/headphones.svg +1 -0
- package/data/assets/icons/tabler/heart-filled.svg +1 -0
- package/data/assets/icons/tabler/heart.svg +1 -0
- package/data/assets/icons/tabler/live-view.svg +1 -0
- package/data/assets/icons/tabler/microphone.svg +1 -0
- package/data/assets/icons/tabler/mood-smile.svg +1 -0
- package/data/assets/icons/tabler/mood-tongue.svg +1 -0
- package/data/assets/icons/tabler/movie.svg +1 -0
- package/data/assets/icons/tabler/player-play.svg +1 -0
- package/data/assets/icons/tabler/playlist.svg +1 -0
- package/data/assets/icons/tabler/rocket.svg +1 -0
- package/data/assets/icons/tabler/sparkles.svg +1 -0
- package/data/assets/icons/tabler/star-filled.svg +1 -0
- package/data/assets/icons/tabler/star.svg +1 -0
- package/data/assets/icons/tabler/tag.svg +1 -0
- package/data/assets/icons/tabler/thumb-up.svg +1 -0
- package/data/assets/icons/tabler/ticket.svg +1 -0
- package/data/assets/icons/tabler/video.svg +1 -0
- package/data/assets/illustrations/openpeeps/LICENSE +8 -0
- package/data/assets/illustrations/openpeeps/peep-10.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-100.svg +32 -0
- package/data/assets/illustrations/openpeeps/peep-101.svg +25 -0
- package/data/assets/illustrations/openpeeps/peep-102.svg +28 -0
- package/data/assets/illustrations/openpeeps/peep-103.svg +32 -0
- package/data/assets/illustrations/openpeeps/peep-105.svg +25 -0
- package/data/assets/illustrations/openpeeps/peep-12.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-13.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-16.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-17.svg +31 -0
- package/data/assets/illustrations/openpeeps/peep-22.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-25.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-26.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-27.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-30.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-31.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-34.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-38.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-39.svg +31 -0
- package/data/assets/illustrations/openpeeps/peep-4.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-41.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-44.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-47.svg +26 -0
- package/data/assets/illustrations/openpeeps/peep-48.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-50.svg +31 -0
- package/data/assets/illustrations/openpeeps/peep-51.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-53.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-54.svg +26 -0
- package/data/assets/illustrations/openpeeps/peep-55.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-56.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-58.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-61.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-64.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-68.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-69.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-7.svg +31 -0
- package/data/assets/illustrations/openpeeps/peep-72.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-76.svg +31 -0
- package/data/assets/illustrations/openpeeps/peep-77.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-80.svg +27 -0
- package/data/assets/illustrations/openpeeps/peep-81.svg +29 -0
- package/data/assets/illustrations/openpeeps/peep-83.svg +29 -0
- package/data/assets/manifest.json +1 -0
- package/data/assets/parts//342/206/223/346/212/225/345/275/261.png +0 -0
- package/data/assets/parts//344/270/211/350/247/222/345/217/221/345/244/2711.png +0 -0
- package/data/assets/parts//344/270/211/350/247/222/345/217/221/345/244/2712.png +0 -0
- package/data/assets/parts//344/270/211/350/247/222/345/217/221/345/244/2713.png +0 -0
- package/data/assets/parts//344/270/211/350/247/222/345/217/221/345/244/2714.png +0 -0
- package/data/assets/parts//345/217/214/351/251/254/345/260/276.png +0 -0
- package/data/assets/parts//345/217/214/351/251/254/345/260/2761.png +0 -0
- package/data/assets/parts//345/217/2211.png +0 -0
- package/data/assets/parts//345/217/2212.png +0 -0
- package/data/assets/parts//345/217/2213.png +0 -0
- package/data/assets/parts//345/217/221/345/234/210.png +0 -0
- package/data/assets/parts//345/217/221/345/244/2711.png +0 -0
- package/data/assets/parts//345/217/221/345/244/2712.png +0 -0
- package/data/assets/parts//345/217/243/346/260/264/345/267/276/345/211/215.png +0 -0
- package/data/assets/parts//345/217/243/346/260/264/345/267/276/345/220/216.png +0 -0
- package/data/assets/parts//345/217/263/344/270/212/347/234/274/347/235/221.png +0 -0
- package/data/assets/parts//345/217/263/346/211/213.png +0 -0
- package/data/assets/parts//345/217/263/346/211/2131.png +0 -0
- package/data/assets/parts//345/217/263/347/234/274/347/220/203.png +0 -0
- package/data/assets/parts//345/217/263/347/234/274/347/231/275.png +0 -0
- package/data/assets/parts//345/217/263/350/205/277.png +0 -0
- package/data/assets/parts//345/221/206/346/257/233.png +0 -0
- package/data/assets/parts//345/230/264/345/267/264.png +0 -0
- package/data/assets/parts//345/260/276/345/267/264.png +0 -0
- package/data/assets/parts//345/267/246/344/270/212/347/234/274/347/235/221.png +0 -0
- package/data/assets/parts//345/267/246/345/217/214/347/234/274/347/232/256.png +0 -0
- package/data/assets/parts//345/267/246/346/211/213.png +0 -0
- package/data/assets/parts//345/267/246/346/211/2131.png +0 -0
- package/data/assets/parts//345/267/246/347/234/274/347/220/203.png +0 -0
- package/data/assets/parts//345/267/246/347/234/274/347/231/275.png +0 -0
- package/data/assets/parts//345/267/246/350/205/277.png +0 -0
- package/data/assets/parts//346/212/225/345/275/261.png +0 -0
- package/data/assets/parts//347/234/211/346/257/233.png +0 -0
- package/data/assets/parts//347/234/274/347/275/251.png +0 -0
- package/data/assets/parts//350/200/263/346/234/2651.png +0 -0
- package/data/assets/parts//350/200/263/346/234/2652.png +0 -0
- package/data/assets/parts//350/204/221/345/243/263.png +0 -0
- package/data/assets/parts//350/241/243/346/234/215/345/211/215.png +0 -0
- package/data/assets/parts//350/241/243/346/234/215/345/220/216.png +0 -0
- package/data/assets/parts//350/241/243/346/234/215/351/273/221.png +0 -0
- package/data/assets/parts//350/241/243/346/234/215/351/273/2211.png +0 -0
- package/data/assets/parts//350/272/253/344/275/223.png +0 -0
- package/data/assets/parts//351/223/201/346/235/277.png +0 -0
- package/data/assets/parts//351/273/221/347/234/274/345/234/210.png +0 -0
- package/data/assets/parts//351/273/221/347/234/274/345/234/2102.png +0 -0
- package/data/assets/patterns/checker.svg +1 -0
- package/data/assets/patterns/circles-overlap.svg +1 -0
- package/data/assets/patterns/crosshatch.svg +1 -0
- package/data/assets/patterns/diagonal.svg +1 -0
- package/data/assets/patterns/dots.svg +1 -0
- package/data/assets/patterns/grid.svg +1 -0
- package/data/assets/patterns/halftone.svg +1 -0
- package/data/assets/patterns/plus.svg +1 -0
- package/data/assets/patterns/scales.svg +1 -0
- package/data/assets/patterns/triangles.svg +1 -0
- package/data/assets/patterns/waves.svg +1 -0
- package/data/assets/patterns/zigzag.svg +1 -0
- package/data/assets/stickers/openmoji/1F375.svg +12 -0
- package/data/assets/stickers/openmoji/1F9E8.svg +21 -0
- package/data/assets/stickers/twemoji/1f389.svg +1 -0
- package/data/assets/stickers/twemoji/1f3a4.svg +1 -0
- package/data/assets/stickers/twemoji/1f3a7.svg +1 -0
- package/data/assets/stickers/twemoji/1f3ae.svg +1 -0
- package/data/assets/stickers/twemoji/1f44b.svg +1 -0
- package/data/assets/stickers/twemoji/1f44d.svg +1 -0
- package/data/assets/stickers/twemoji/1f494.svg +1 -0
- package/data/assets/stickers/twemoji/1f4a2.svg +1 -0
- package/data/assets/stickers/twemoji/1f4a8.svg +1 -0
- package/data/assets/stickers/twemoji/1f4b5.svg +1 -0
- package/data/assets/stickers/twemoji/1f4d6.svg +1 -0
- package/data/assets/stickers/twemoji/1f525.svg +1 -0
- package/data/assets/stickers/twemoji/1f600.svg +1 -0
- package/data/assets/stickers/twemoji/1f602.svg +1 -0
- package/data/assets/stickers/twemoji/1f60d.svg +1 -0
- package/data/assets/stickers/twemoji/1f62d.svg +1 -0
- package/data/assets/stickers/twemoji/1f680.svg +1 -0
- package/data/assets/stickers/twemoji/1f979.svg +1 -0
- package/data/assets/stickers/twemoji/1f97a.svg +1 -0
- package/data/assets/stickers/twemoji/26a1.svg +1 -0
- package/data/assets/stickers/twemoji/2705.svg +1 -0
- package/data/assets/stickers/twemoji/274c.svg +1 -0
- package/data/assets/stickers/twemoji/2764.svg +1 -0
- package/data/assets/stickers/twemoji/2b50.svg +1 -0
- package/data/assets/stickers/twemoji_1f525.svg +1 -0
- package/data/assets/stickers/twemoji_1f600.svg +1 -0
- package/data/assets/stickers/twemoji_1f979.svg +1 -0
- package/data/assets/stickers/twemoji_2764.svg +1 -0
- package/data/assets/textures/grain-dark.png +0 -0
- package/data/assets/textures/grain-light.png +0 -0
- package/data/assets/textures/grain-warm.png +0 -0
- package/data/lucide-icons.json +1 -0
- package/data/packs/tabler.icons.json +1 -0
- package/icon.svg +26 -0
- package/lib/assets.js +228 -0
- package/lib/audit.js +284 -0
- package/lib/contrast-render.js +213 -0
- package/lib/css-rewrite.js +379 -0
- package/lib/detect.js +701 -0
- package/lib/diff.js +153 -0
- package/lib/emit.js +557 -0
- package/lib/html-context.js +315 -0
- package/lib/icon-render.js +107 -0
- package/lib/icondb.js +402 -0
- package/lib/image-normalize.js +140 -0
- package/lib/index.js +1289 -0
- package/lib/layouts/common.js +48 -0
- package/lib/layouts/image.js +1010 -0
- package/lib/layouts/index.js +10 -0
- package/lib/layouts/intent.js +66 -0
- package/lib/layouts/metrics-text.js +91 -0
- package/lib/layouts/png.js +75 -0
- package/lib/layouts/rules-contrast.js +575 -0
- package/lib/layouts/rules-punct.js +486 -0
- package/lib/layouts/rules-structure.js +182 -0
- package/lib/layouts/source.js +1961 -0
- package/lib/layouts/svg.js +1204 -0
- package/lib/morphcore.js +139 -0
- package/lib/preview.js +225 -0
- package/lib/render-html.js +255 -0
- package/lib/render.js +486 -0
- package/lib/rules.js +267 -0
- package/lib/shadow.js +83 -0
- package/lib/skip.js +56 -0
- package/lib/style.js +688 -0
- package/lib/svg.js +729 -0
- package/lib/taste-image.js +323 -0
- package/lib/taste.js +219 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +92 -4
- package/vendor/morphicons-adapters.iife.js +903 -0
- package/vendor/morphicons.iife.js +2441 -0
- package/vendor/resvg/LICENSE +373 -0
- package/vendor/resvg/index.d.ts +98 -0
- package/vendor/resvg/index.js +23 -0
- package/vendor/resvg/js-binding.d.ts +52 -0
- package/vendor/resvg/js-binding.js +1 -0
- package/vendor/resvg/package.json +138 -0
- package/vendor/resvg/resvgjs.win32-x64-msvc.node +0 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* taste-image.js — 位图的「AI 味」体检。
|
|
3
|
+
*
|
|
4
|
+
* 源码层的 `taste` 只能看写了什么;但用户手里常是**一张图**:
|
|
5
|
+
* 别人发的参考、模型直接出的设计稿、截屏。没有这个能力,「像不像 AI 做的」
|
|
6
|
+
* 就永远只能靠人眼——而人手边最缺的正是判断力。
|
|
7
|
+
*
|
|
8
|
+
* 设计纪律与源码层完全一致:
|
|
9
|
+
* - 只数**可量的像素事实**,不做「这是 AI 画的」这种归因;
|
|
10
|
+
* - 只在**成组出现**时提示模板感(单项对称/单项渐变都是正常设计);
|
|
11
|
+
* - 宁可漏报:阈值按真实产品校过,成熟界面应当接近 0。
|
|
12
|
+
*
|
|
13
|
+
* PNG 由自带解码器处理;JPEG/WebP 先用本机浏览器转成同尺寸 PNG(见 image-normalize.js),
|
|
14
|
+
* 浏览器不可用时明确报错并给方案,不静默降级。
|
|
15
|
+
*/
|
|
16
|
+
import { basename } from "node:path";
|
|
17
|
+
|
|
18
|
+
import { readAsPng } from "./image-normalize.js";
|
|
19
|
+
|
|
20
|
+
import { decodePng } from "./layouts/png.js";
|
|
21
|
+
import { cardGridOf } from "./layouts/image.js";
|
|
22
|
+
import { PROFILES, gradeOf } from "./rules.js";
|
|
23
|
+
|
|
24
|
+
/** 位图层的三条判据(与源码层规则同构:id/scope/weight/fingerprint/fix)。 */
|
|
25
|
+
const IMG_RULES = [
|
|
26
|
+
{
|
|
27
|
+
id: "img-indigo-hue",
|
|
28
|
+
fingerprint: true,
|
|
29
|
+
label: "紫蓝相主导",
|
|
30
|
+
scope: "style",
|
|
31
|
+
weight: 12,
|
|
32
|
+
noteLabel: "紫/蓝紫是 AI 出图最强的像素指纹",
|
|
33
|
+
fix: "换成同色相家族或直接去色相:用明度差做层次,别用蓝紫渐变撑高级感"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "img-ramp",
|
|
37
|
+
fingerprint: true,
|
|
38
|
+
label: "大面积平滑渐变",
|
|
39
|
+
scope: "style",
|
|
40
|
+
weight: 8,
|
|
41
|
+
noteLabel: "整片背景是平滑渐变的区域占比",
|
|
42
|
+
fix: "渐变收进单个元素(按钮/卡片),背景用纯色;整片渐变会让画面没有层次落点"
|
|
43
|
+
},
|
|
44
|
+
// 【曾经做过、实测后删掉的一条】中轴对称(居中版式)。
|
|
45
|
+
// 三种量法都被数据否掉:
|
|
46
|
+
// ① 逐像素比镜像:所有图都 86–91%——纯色背景本来就镜像重合;
|
|
47
|
+
// ② 只比边缘像素:真实产品全部跌到 0,AI 样本也是 0(字形不镜像);
|
|
48
|
+
// ③ 版面占位 / 色彩质量左右平衡:AI 0.982–0.989,而视频站 0.951、安全软件 0.956——
|
|
49
|
+
// 差距只有 0.03,且真实落地页本来就会居中。阈值定哪儿都是择脆的。
|
|
50
|
+
// 对称是**设计选择**,不是 AI 指纹;宁可少一条判据,也不报不可靠的。
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* ④ 同构卡片网格 —— **只量,不判**。
|
|
55
|
+
*
|
|
56
|
+
* 复用 layout_check image 已校准的连通域卡片检测(近白面 + 包围盒),
|
|
57
|
+
* 但**不计分**:实测“同一行 3 个等宽卡片”在真实产品里也是常规布局,
|
|
58
|
+
* 我们自己做的苹果风界面(3 个数据卡)就会被判 0.8。分不开就不报。
|
|
59
|
+
*
|
|
60
|
+
* 所以这里只把事实交出去(几张、最多一行几个、是否等宽),
|
|
61
|
+
* 由模型结合色相/渐变等计分判据自行判断。真要判分,需要先有
|
|
62
|
+
* 十人以上的真实产品截图样本来定阈值。
|
|
63
|
+
*/
|
|
64
|
+
function cardSummary(grid) {
|
|
65
|
+
const cards = grid.cards ?? [];
|
|
66
|
+
if (cards.length === 0) return { count: 0, maxPerRow: 0, equalWidth: false };
|
|
67
|
+
const rows = [];
|
|
68
|
+
for (const card of cards) {
|
|
69
|
+
const row = rows.find((r) => Math.abs(r.y - card.y) <= 8);
|
|
70
|
+
if (row === undefined) rows.push({ y: card.y, cards: [card] });
|
|
71
|
+
else row.cards.push(card);
|
|
72
|
+
}
|
|
73
|
+
const best = rows.sort((a, b) => b.cards.length - a.cards.length)[0];
|
|
74
|
+
const widths = best.cards.map((c) => c.w);
|
|
75
|
+
const ratio = Math.min(...widths) === 0 ? 99 : Math.max(...widths) / Math.min(...widths);
|
|
76
|
+
return { count: cards.length, maxPerRow: best.cards.length, equalWidth: ratio <= 1.15 };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const clamp01 = (v) => Math.max(0, Math.min(1, v));
|
|
80
|
+
|
|
81
|
+
/** RGB → HSV(h 0–360,s/v 0–1)。灰色返回 h = -1。 */
|
|
82
|
+
function hsv(r, g, b) {
|
|
83
|
+
const max = Math.max(r, g, b);
|
|
84
|
+
const min = Math.min(r, g, b);
|
|
85
|
+
const d = max - min;
|
|
86
|
+
if (d === 0) return { h: -1, s: 0, v: max / 255 };
|
|
87
|
+
let h;
|
|
88
|
+
if (max === r) h = ((g - b) / d) % 6;
|
|
89
|
+
else if (max === g) h = (b - r) / d + 2;
|
|
90
|
+
else h = (r - g) / d + 4;
|
|
91
|
+
h *= 60;
|
|
92
|
+
if (h < 0) h += 360;
|
|
93
|
+
return { h, s: d / max, v: max / 255 };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** 按面积自适应采样步长:大图不逐像素,避免几千万次循环。 */
|
|
97
|
+
function sampleStep(w, h, target = 180) {
|
|
98
|
+
return Math.max(1, Math.floor(Math.sqrt((w * h) / (target * target))));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* ① 紫蓝相主导:只看**有颜色**的像素里,落在 245–300° 的比例。
|
|
103
|
+
* 灰色(s≈0)不参与——一页黑白界面不该因为「没有色彩」被判错方向。
|
|
104
|
+
*/
|
|
105
|
+
function indigoShare(img, step) {
|
|
106
|
+
let colored = 0;
|
|
107
|
+
let indigo = 0;
|
|
108
|
+
for (let y = 0; y < img.height; y += step) {
|
|
109
|
+
for (let x = 0; x < img.width; x += step) {
|
|
110
|
+
const i = (y * img.width + x) * img.channels;
|
|
111
|
+
if (img.channels === 4 && img.data[i + 3] < 128) continue;
|
|
112
|
+
const { h, s, v } = hsv(img.data[i], img.data[i + 1], img.data[i + 2]);
|
|
113
|
+
if (s < 0.35 || v < 0.15) continue;
|
|
114
|
+
colored += 1;
|
|
115
|
+
// 238–300°:从“深蓝略偏紫”到底紫。实测一个典型 AI 海报的主背景就在 240°
|
|
116
|
+
// (低饱和深蓝紫),旧区间从 245 起把它整张图判成“没有紫蓝”;
|
|
117
|
+
// 下界不再降:210–235° 是真实产品最常用的蓝(某在线文档产品 216°、视频站 195°)。
|
|
118
|
+
if (h >= 238 && h <= 300) indigo += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { share: colored === 0 ? 0 : indigo / colored, colored };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* ② 大面积平滑渐变:把图切成 3×3 区域,每个区域看**一维线性渐变**的指纹。
|
|
126
|
+
*
|
|
127
|
+
* 关键:必须同时看两个方向。只看一个方向时,**照片**(安全软件首页的 banner)
|
|
128
|
+
* 每个区域的列均色也是平滑变化的,实测四个区域全部被当成渐变(误报)。
|
|
129
|
+
* 真正的线性渐变有一个方向在平滑变化、正交方向几乎不变(等色带);
|
|
130
|
+
* 照片两个方向都在变。所以判据是「单方向平滑 + 正交方向平坦」。
|
|
131
|
+
*/
|
|
132
|
+
function rampRegions(img, step) {
|
|
133
|
+
const R = 3;
|
|
134
|
+
let hit = 0;
|
|
135
|
+
const detail = [];
|
|
136
|
+
// 沿某一轴取 N 个截面的均色
|
|
137
|
+
const means = (x0, x1, y0, y1, axis) => {
|
|
138
|
+
const span = axis === "x" ? x1 - x0 : y1 - y0;
|
|
139
|
+
const n = Math.max(4, Math.min(24, Math.floor(span / Math.max(step, 1))));
|
|
140
|
+
const out = [];
|
|
141
|
+
for (let k = 0; k < n; k += 1) {
|
|
142
|
+
const c = Math.floor((axis === "x" ? x0 : y0) + (span * (k + 0.5)) / n);
|
|
143
|
+
let sr = 0;
|
|
144
|
+
let sg = 0;
|
|
145
|
+
let sb = 0;
|
|
146
|
+
let cnt = 0;
|
|
147
|
+
for (let t = axis === "x" ? y0 : x0; t < (axis === "x" ? y1 : x1); t += step) {
|
|
148
|
+
const px = axis === "x" ? c : t;
|
|
149
|
+
const py = axis === "x" ? t : c;
|
|
150
|
+
const i = (py * img.width + px) * img.channels;
|
|
151
|
+
if (img.channels === 4 && img.data[i + 3] < 128) continue;
|
|
152
|
+
sr += img.data[i];
|
|
153
|
+
sg += img.data[i + 1];
|
|
154
|
+
sb += img.data[i + 2];
|
|
155
|
+
cnt += 1;
|
|
156
|
+
}
|
|
157
|
+
if (cnt > 0) out.push([sr / cnt, sg / cnt, sb / cnt]);
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
};
|
|
161
|
+
// 一维签名:跳变次数 / 真正在动的步数 / 累计色差
|
|
162
|
+
const signature = (list) => {
|
|
163
|
+
if (list.length < 4) return { big: 99, moving: 0, total: 0 };
|
|
164
|
+
let big = 0;
|
|
165
|
+
let moving = 0;
|
|
166
|
+
for (let k = 1; k < list.length; k += 1) {
|
|
167
|
+
const d =
|
|
168
|
+
(Math.abs(list[k][0] - list[k - 1][0]) + Math.abs(list[k][1] - list[k - 1][1]) + Math.abs(list[k][2] - list[k - 1][2])) / 3;
|
|
169
|
+
if (d > 8) big += 1;
|
|
170
|
+
else if (d >= 0.4) moving += 1;
|
|
171
|
+
}
|
|
172
|
+
const total =
|
|
173
|
+
(Math.abs(list[list.length - 1][0] - list[0][0]) +
|
|
174
|
+
Math.abs(list[list.length - 1][1] - list[0][1]) +
|
|
175
|
+
Math.abs(list[list.length - 1][2] - list[0][2])) /
|
|
176
|
+
3;
|
|
177
|
+
return { big, moving, total };
|
|
178
|
+
};
|
|
179
|
+
// 渐变方向存活条件:没有跳变(≤1)、确实在动(≥2 步)、累计色差够(≥14)
|
|
180
|
+
const smooth = (sig) => sig.big <= 1 && sig.moving >= 2 && sig.total >= 14;
|
|
181
|
+
|
|
182
|
+
for (let ry = 0; ry < R; ry += 1) {
|
|
183
|
+
for (let rx = 0; rx < R; rx += 1) {
|
|
184
|
+
const x0 = Math.floor((img.width * rx) / R);
|
|
185
|
+
const x1 = Math.floor((img.width * (rx + 1)) / R);
|
|
186
|
+
const y0 = Math.floor((img.height * ry) / R);
|
|
187
|
+
const y1 = Math.floor((img.height * (ry + 1)) / R);
|
|
188
|
+
const sx = signature(means(x0, x1, y0, y1, "x"));
|
|
189
|
+
const sy = signature(means(x0, x1, y0, y1, "y"));
|
|
190
|
+
// 横向渐变:x 方向平滑变化 + y 方向几乎不变(等色带)
|
|
191
|
+
const horizontal = smooth(sx) && sy.total < 10;
|
|
192
|
+
// 纵向渐变:y 方向平滑变化 + x 方向几乎不变
|
|
193
|
+
const vertical = smooth(sy) && sx.total < 10;
|
|
194
|
+
if (horizontal || vertical) hit += 1;
|
|
195
|
+
detail.push(`${rx},${ry}:x=${Math.round(sx.total)}/${sx.big} y=${Math.round(sy.total)}/${sy.big}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return { hit, detail };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 位图 AI 味体检。
|
|
204
|
+
* @param {string} target 图片路径(PNG 直读;JPEG / WebP 经浏览器转码)
|
|
205
|
+
* @param {{profile?:string, allow?:string[]}} [opts]
|
|
206
|
+
*/
|
|
207
|
+
export async function tasteImage(target, opts = {}) {
|
|
208
|
+
// PNG 直读;JPEG / WebP 由 readAsPng 内部转码(与 layout_check image 共用同一入口)。
|
|
209
|
+
const buf = await readAsPng(target, { browserPath: opts.browserPath });
|
|
210
|
+
const img = decodePng(buf);
|
|
211
|
+
const step = sampleStep(img.width, img.height);
|
|
212
|
+
|
|
213
|
+
const indigo = indigoShare(img, step);
|
|
214
|
+
const grid = cardSummary(cardGridOf(buf));
|
|
215
|
+
const ramp = rampRegions(img, step);
|
|
216
|
+
|
|
217
|
+
// 各判据打分:只在「有足够证据」时才给分
|
|
218
|
+
const indigoScore =
|
|
219
|
+
indigo.colored / Math.max(1, Math.ceil((img.width / step) * (img.height / step))) < 0.03
|
|
220
|
+
? 0
|
|
221
|
+
: indigo.share >= 0.6
|
|
222
|
+
? 1
|
|
223
|
+
: indigo.share >= 0.4
|
|
224
|
+
? 0.6
|
|
225
|
+
: indigo.share >= 0.25
|
|
226
|
+
? 0.3
|
|
227
|
+
: 0;
|
|
228
|
+
const rampScore = ramp.hit >= 5 ? 1 : ramp.hit >= 3 ? 0.7 : ramp.hit >= 2 ? 0.4 : 0;
|
|
229
|
+
|
|
230
|
+
const raw = {
|
|
231
|
+
"img-indigo-hue": {
|
|
232
|
+
score: indigoScore,
|
|
233
|
+
note: `有色像素里紫蓝相占 ${Math.round(indigo.share * 100)}%(有色 ${indigo.colored} 个采样点)`
|
|
234
|
+
},
|
|
235
|
+
"img-ramp": { score: rampScore, note: `9 个区域里 ${ramp.hit} 个是平滑渐变` },
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const profileName = PROFILES[opts.profile] === undefined ? "balanced" : opts.profile;
|
|
239
|
+
const factors = PROFILES[profileName];
|
|
240
|
+
const allow = new Set(opts.allow ?? []);
|
|
241
|
+
|
|
242
|
+
let weighted = 0;
|
|
243
|
+
let weightSum = 0;
|
|
244
|
+
const metrics = [];
|
|
245
|
+
const actions = [];
|
|
246
|
+
const advisories = [];
|
|
247
|
+
for (const rule of IMG_RULES) {
|
|
248
|
+
const r = raw[rule.id];
|
|
249
|
+
const factor = factors[rule.scope] ?? 1;
|
|
250
|
+
const counted = factor > 0 && allow.has(rule.id) === false;
|
|
251
|
+
if (counted) {
|
|
252
|
+
weighted += r.score * rule.weight * factor;
|
|
253
|
+
weightSum += rule.weight * factor;
|
|
254
|
+
}
|
|
255
|
+
const score = Math.round(r.score * 100);
|
|
256
|
+
metrics.push({
|
|
257
|
+
id: rule.id,
|
|
258
|
+
label: rule.label,
|
|
259
|
+
scope: rule.scope,
|
|
260
|
+
weight: rule.weight,
|
|
261
|
+
counted,
|
|
262
|
+
fingerprint: rule.fingerprint === true,
|
|
263
|
+
score,
|
|
264
|
+
note: r.note
|
|
265
|
+
});
|
|
266
|
+
if (score > 0) {
|
|
267
|
+
const entry = { rule: rule.id, label: rule.label, scope: rule.scope, score, note: r.note, fix: rule.fix };
|
|
268
|
+
if (counted) actions.push(entry);
|
|
269
|
+
else advisories.push(entry);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
const score = weightSum === 0 ? 0 : Math.round((weighted / weightSum) * 100);
|
|
273
|
+
const counted = metrics.filter((m) => m.score > 0 && m.counted);
|
|
274
|
+
const distinctHits = counted.filter((m) => m.fingerprint === true && m.score >= 40).length;
|
|
275
|
+
// 位图只有 2 条判据,不能照搬源码层的 3/5 档:这里 2 条全中就已是“成组”。
|
|
276
|
+
const coOccurrence =
|
|
277
|
+
distinctHits >= 2
|
|
278
|
+
? { level: "pattern", text: "两套视觉默认同时出现(色相 + 渐变)——像素层能拿到的最强组合" }
|
|
279
|
+
: distinctHits === 0
|
|
280
|
+
? { level: "none", text: "" }
|
|
281
|
+
: { level: "scattered", text: `只命中 ${distinctHits} 项(零星),不足以说明风格——先确认是否刻意选择` };
|
|
282
|
+
|
|
283
|
+
return {
|
|
284
|
+
kind: "image",
|
|
285
|
+
// 能力边界必须随结果一起交出去:否则“0 分”很容易被误读成“不是 AI 做的”
|
|
286
|
+
scopeNote:
|
|
287
|
+
"位图层只能量两类像素事实:紫蓝色相占比、整片线性渐变。它**量不了**字号体系、间距网格、" +
|
|
288
|
+
"图标成套性、文案套路、交互态——这五项才是 AI 界面与成熟项目的真实差距,0 分不代表这些没问题。",
|
|
289
|
+
// 量不了的维度必须显式列出来,否则模型会把“0 分”当“没问题”归档。
|
|
290
|
+
// 实测(2026-11-08,Flow-Pilot 截图):像素层 0 分,同一份源码在源码层被报
|
|
291
|
+
// 字号塔缩(1.00×)、间距 38% 不在 4px 网格且出现 13.6/22.4 非整数像素、AI 文案 30 处。
|
|
292
|
+
blindSpots: [
|
|
293
|
+
"字号体系(多少档、最大/正文比)",
|
|
294
|
+
"间距是否在 4px 网格",
|
|
295
|
+
"图标是否同一套(线宽/光学尺寸)",
|
|
296
|
+
"文案套路与口癖",
|
|
297
|
+
"交互态覆盖(hover/focus/disabled)"
|
|
298
|
+
],
|
|
299
|
+
score,
|
|
300
|
+
grade: gradeOf(score),
|
|
301
|
+
profile: profileName,
|
|
302
|
+
profileLabel: PROFILES[profileName].label,
|
|
303
|
+
root: target,
|
|
304
|
+
image: { file: basename(target), width: img.width, height: img.height, sampleStep: step },
|
|
305
|
+
// 原始量值:校准时靠它看阈值定得对不对(模型也靠它解释为什么没报)
|
|
306
|
+
raw: {
|
|
307
|
+
indigoShare: Math.round(indigo.share * 1000) / 1000,
|
|
308
|
+
indigoColored: indigo.colored,
|
|
309
|
+
rampRegions: ramp.hit,
|
|
310
|
+
cardGrid: grid,
|
|
311
|
+
},
|
|
312
|
+
filesScanned: 1,
|
|
313
|
+
truncated: false,
|
|
314
|
+
coOccurrence,
|
|
315
|
+
metrics: metrics.filter((m) => m.score > 0),
|
|
316
|
+
findings: [],
|
|
317
|
+
findingsTotal: 0,
|
|
318
|
+
findingsTruncated: false,
|
|
319
|
+
actions,
|
|
320
|
+
advisories,
|
|
321
|
+
exempted: [...allow]
|
|
322
|
+
};
|
|
323
|
+
}
|
package/lib/taste.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* taste.js — ui_taste 工具的实现:扫描前端产物 → 量化「AI 味」→ 给返工指令。
|
|
3
|
+
*
|
|
4
|
+
* 与 ui_audit 的分工:
|
|
5
|
+
* ui_audit 查「违反项目契约」(emoji 图标禁令、MorphIcon 误用、字典引用),
|
|
6
|
+
* 是硬规则、有明确对错;
|
|
7
|
+
* ui_taste 查「像不像 AI 默认产物」,是审美诊断、带分数与改进方向。
|
|
8
|
+
* 两者都扫源码,但一个报 error/warn/info,一个报 score + fix。
|
|
9
|
+
*
|
|
10
|
+
* 支持单文件(.html/.css)与目录(自动发现前端文件)。
|
|
11
|
+
*/
|
|
12
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
13
|
+
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
14
|
+
import { PROFILES, RULES } from "./rules.js";
|
|
15
|
+
import { tasteImage } from "./taste-image.js";
|
|
16
|
+
import { runRules } from "./detect.js";
|
|
17
|
+
import { isSkippableDir } from "./skip.js";
|
|
18
|
+
|
|
19
|
+
/** 项目内的设计意图声明文件名(与扫描同级目录)。 */
|
|
20
|
+
const INTENT_FILE = ".design-intent.json";
|
|
21
|
+
|
|
22
|
+
// 跳过列表与 layout_check 共用一份(lib/skip.js)——两处各写一份的话,
|
|
23
|
+
// 修了一边另一边就会漏(实测:taste 曾扫到 test/fixtures 把自己报成 12 条 AI 味)。
|
|
24
|
+
/**
|
|
25
|
+
* 参与扫描的前端产物扩展名。
|
|
26
|
+
* 含 .js/.mjs/.ts 是因为 CSS-in-JS 与「样式字符串内联在 JS 里」是主流写法
|
|
27
|
+
* (styled-components / emotion / 插件把 CSS 拼成字符串注入)——不含它们会
|
|
28
|
+
* 把这类项目整体漏掉。代价是纯逻辑文件也会被扫,但除 emoji 外的规则都基于
|
|
29
|
+
* 具体样式声明,没有样式的文件不会命中。
|
|
30
|
+
*/
|
|
31
|
+
const TASTE_EXTS = [".html", ".htm", ".css", ".vue", ".svelte", ".jsx", ".tsx", ".astro", ".js", ".mjs", ".ts"];
|
|
32
|
+
const PER_FILE_CAP = 2 * 1024 * 1024;
|
|
33
|
+
/** 默认文件上限:taste 只做统计,不需要扫全仓库。 */
|
|
34
|
+
const DEFAULT_MAX_FILES = 600;
|
|
35
|
+
/** findings 上限:报告要能读完,热点位置比穷举更有用。 */
|
|
36
|
+
const DEFAULT_MAX_FINDINGS = 24;
|
|
37
|
+
|
|
38
|
+
async function readCapped(file) {
|
|
39
|
+
try {
|
|
40
|
+
const info = await stat(file);
|
|
41
|
+
if (!info.isFile() || info.size > PER_FILE_CAP) return null;
|
|
42
|
+
return await readFile(file, "utf8");
|
|
43
|
+
} catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** 目录/单文件 → [{path, rel, raw}];跳过依赖与构建产物目录。 */
|
|
49
|
+
async function collect(target, maxFiles) {
|
|
50
|
+
const abs = resolve(target);
|
|
51
|
+
let info;
|
|
52
|
+
try {
|
|
53
|
+
info = await stat(abs);
|
|
54
|
+
} catch {
|
|
55
|
+
throw new Error(`path not found: ${abs}`);
|
|
56
|
+
}
|
|
57
|
+
const files = [];
|
|
58
|
+
if (info.isFile()) {
|
|
59
|
+
const raw = await readCapped(abs);
|
|
60
|
+
if (raw !== null) files.push({ path: abs, rel: abs.split(sep).pop() ?? abs, raw });
|
|
61
|
+
return { files, root: abs, truncated: false };
|
|
62
|
+
}
|
|
63
|
+
const root = abs;
|
|
64
|
+
const queue = [root];
|
|
65
|
+
if (!info.isDirectory()) throw new Error(`not a file or directory: ${abs}`);
|
|
66
|
+
while (queue.length > 0 && files.length < maxFiles) {
|
|
67
|
+
const dir = queue.shift();
|
|
68
|
+
let entries;
|
|
69
|
+
try {
|
|
70
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
71
|
+
} catch {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
if (files.length >= maxFiles) break;
|
|
76
|
+
const full = join(dir, entry.name);
|
|
77
|
+
if (entry.isDirectory()) {
|
|
78
|
+
if (!isSkippableDir(entry.name) && !entry.name.startsWith(".")) queue.push(full);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!entry.isFile()) continue;
|
|
82
|
+
if (!TASTE_EXTS.some((ext) => entry.name.endsWith(ext))) continue;
|
|
83
|
+
if (entry.name.endsWith(".min.js") || entry.name.endsWith(".map")) continue;
|
|
84
|
+
const raw = await readCapped(full);
|
|
85
|
+
if (raw !== null) files.push({ path: full, rel: relative(root, full).split(sep).join("/"), raw });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { files, root, truncated: files.length >= maxFiles };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 读取项目内的 .design-intent.json(设计意图声明)。
|
|
93
|
+
* 格式:{ "skin": "playful-geometric", "allow": ["radius-blob"], "reason": "..." }
|
|
94
|
+
* 解析失败不报错——一个坏的声明文件不该让整次体检失败,应当静默忽略并在返回里标明。
|
|
95
|
+
*/
|
|
96
|
+
async function readIntent(root) {
|
|
97
|
+
const path = join(root, INTENT_FILE);
|
|
98
|
+
let raw;
|
|
99
|
+
try {
|
|
100
|
+
raw = await readFile(path, "utf8");
|
|
101
|
+
} catch {
|
|
102
|
+
return { found: false };
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
const parsed = JSON.parse(raw);
|
|
106
|
+
return {
|
|
107
|
+
found: true,
|
|
108
|
+
skin: typeof parsed?.skin === "string" ? parsed.skin : undefined,
|
|
109
|
+
allow: Array.isArray(parsed?.allow) ? parsed.allow.filter((id) => typeof id === "string") : [],
|
|
110
|
+
reason: typeof parsed?.reason === "string" ? parsed.reason : undefined
|
|
111
|
+
};
|
|
112
|
+
} catch {
|
|
113
|
+
return { found: true, invalid: true, allow: [] };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 量化一次「AI 味」体检。
|
|
119
|
+
* @param {string} target 目录或单文件(绝对路径优先;相对路径由调用方解析)
|
|
120
|
+
* @param {{maxFiles?:number, maxFindings?:number, ignore?:string[], profile?:string, allow?:string[]}} [opts]
|
|
121
|
+
*/
|
|
122
|
+
async function taste(target, opts = {}) {
|
|
123
|
+
// 位图入口:用户手里常常只有一张图(别人发的参考 / 模型直接出的稿 / 截屏),
|
|
124
|
+
// 这种情况根本没有源码可扫。同一个问题(像不像 AI)、另一种输入,所以入口同一个。
|
|
125
|
+
// 位图:PNG 直读;JPEG/WebP 由 taste-image 内部转码(需本机浏览器)。
|
|
126
|
+
if (/\.(?:png|jpe?g|webp)$/i.test(String(target))) return await tasteImage(target, opts);
|
|
127
|
+
const maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;
|
|
128
|
+
const maxFindings = opts.maxFindings ?? DEFAULT_MAX_FINDINGS;
|
|
129
|
+
const { files, root, truncated } = await collect(target, maxFiles);
|
|
130
|
+
if (files.length === 0) {
|
|
131
|
+
throw new Error(`未扫到前端文件(${TASTE_EXTS.join(" / ")}):${root}`);
|
|
132
|
+
}
|
|
133
|
+
// 豁免来源两处:命令行 allow 参数 + 项目内 .design-intent.json 的 allow。
|
|
134
|
+
// 两者取并集——声明是「这个偏离是有意的」,不是「不检测」。
|
|
135
|
+
// 单文件目标:声明文件在它所在目录(旧实现把文件路径当目录 join,单文件模式永远读不到)
|
|
136
|
+
const intentRoot = files.length === 1 && files[0].path === root ? dirname(root) : root;
|
|
137
|
+
const intent = await readIntent(intentRoot);
|
|
138
|
+
const explicitAllow = Array.isArray(opts.allow) ? opts.allow : [];
|
|
139
|
+
const declared = [...new Set([...(intent.allow ?? []), ...explicitAllow])];
|
|
140
|
+
const ignore = [...new Set([...(opts.ignore ?? []), ...declared])];
|
|
141
|
+
const result = runRules(files, { ignore, profile: opts.profile });
|
|
142
|
+
const profileName = result.profile;
|
|
143
|
+
const findings = result.findings.slice(0, maxFindings);
|
|
144
|
+
const byRule = new Map();
|
|
145
|
+
for (const finding of findings) {
|
|
146
|
+
if (!byRule.has(finding.rule)) byRule.set(finding.rule, []);
|
|
147
|
+
byRule.get(finding.rule).push(finding);
|
|
148
|
+
}
|
|
149
|
+
const counted = result.metrics.filter((metric) => metric.score > 0 && metric.counted);
|
|
150
|
+
// **成组出现**才是指纹:单个视觉元素或单个口癖不能证明什么,
|
|
151
|
+
// 但 3 项以上默认选择同时出现,用户就会认定「这是 AI 做的」。
|
|
152
|
+
// (口径来自「AI 味儿图鉴」的核心判断法:数特征组合,而不是数单项严重度。)
|
|
153
|
+
// 只数**风格默认项**(fingerprint)。字号档位、间距网格、交互态覆盖这些是
|
|
154
|
+
// 通用质量缺陷——桌面应用本来就有 6 档小字号、语义色有 7 个,把它们算进
|
|
155
|
+
// 「AI 味组合」会把真实的成熟产品判成模板(实测在线文档参照稿就是这样)。
|
|
156
|
+
// 只数「够分量的」指纹:单个 emoji(20 分)、5 个卡片里 3 种(35 分)这种
|
|
157
|
+
// 属于零星迹象,把它们凑进“3 项组合”会把正常界面也判成模板。
|
|
158
|
+
const distinctHits = counted.filter((metric) => metric.fingerprint === true && metric.score >= 40).length;
|
|
159
|
+
// 措辞只说「风格风险」,不判定作者——单项风格元素证明不了什么,
|
|
160
|
+
// 这一点与「AI 味儿图鉴」的口径一致。
|
|
161
|
+
const coOccurrence =
|
|
162
|
+
distinctHits >= 5
|
|
163
|
+
? { level: "strong", text: `${distinctHits} 组风格默认同时出现——页面会明显显得模板化,建议优先改这一组` }
|
|
164
|
+
: distinctHits >= 3
|
|
165
|
+
? { level: "pattern", text: `${distinctHits} 组风格默认同时出现——单看每项都不致命,组合起来会显得像套模板` }
|
|
166
|
+
: distinctHits === 0
|
|
167
|
+
? { level: "none", text: "" }
|
|
168
|
+
: { level: "scattered", text: `只命中 ${distinctHits} 项(零星),不足以说明风格——先确认是不是刻意选择` };
|
|
169
|
+
return {
|
|
170
|
+
score: result.score,
|
|
171
|
+
grade: result.grade,
|
|
172
|
+
profile: profileName,
|
|
173
|
+
profileLabel: PROFILES[profileName].label,
|
|
174
|
+
root,
|
|
175
|
+
filesScanned: files.length,
|
|
176
|
+
truncated,
|
|
177
|
+
// 只返回「正在失分」的规则,干净的规则不进模型上下文
|
|
178
|
+
coOccurrence,
|
|
179
|
+
metrics: result.metrics.filter((metric) => metric.score > 0),
|
|
180
|
+
findings,
|
|
181
|
+
findingsTotal: result.findings.length,
|
|
182
|
+
findingsTruncated: result.findings.length > maxFindings,
|
|
183
|
+
// 返工清单:只包含「当前档位下计分」的规则;风格层被降为 0 权重的进 advisories
|
|
184
|
+
actions: counted.map((metric) => {
|
|
185
|
+
const rule = RULES.find((item) => item.id === metric.id);
|
|
186
|
+
return {
|
|
187
|
+
rule: metric.id,
|
|
188
|
+
label: metric.label,
|
|
189
|
+
scope: metric.scope,
|
|
190
|
+
score: metric.score,
|
|
191
|
+
weight: metric.weight,
|
|
192
|
+
note: metric.note,
|
|
193
|
+
fix: rule.fix,
|
|
194
|
+
count: byRule.get(metric.id)?.length ?? 0
|
|
195
|
+
};
|
|
196
|
+
}),
|
|
197
|
+
// 风格取向提示:不计分,供有判断力的人/模型自行取舍
|
|
198
|
+
advisories: [...new Set(result.metrics.filter((m) => m.score > 0 && !m.counted).map((m) => m.id))].map((id) => {
|
|
199
|
+
const metric = result.metrics.find((m) => m.id === id);
|
|
200
|
+
const rule = RULES.find((item) => item.id === id);
|
|
201
|
+
return { rule: id, label: metric.label, score: metric.score, note: metric.note, fix: rule.fix };
|
|
202
|
+
}),
|
|
203
|
+
// 不能把 undefined 作为自身属性放进去:宿主会把 undefined 当 INVALID_TOOL_OUTPUT,
|
|
204
|
+
// 而缺 skin/reason(含 JSON 写坏)是很常见的声明形态。
|
|
205
|
+
intent: intent.found
|
|
206
|
+
? {
|
|
207
|
+
found: true,
|
|
208
|
+
invalid: intent.invalid === true,
|
|
209
|
+
...(intent.skin !== undefined ? { skin: intent.skin } : {}),
|
|
210
|
+
allow: declared,
|
|
211
|
+
...(intent.reason !== undefined ? { reason: intent.reason } : {})
|
|
212
|
+
}
|
|
213
|
+
: { found: false, allow: declared },
|
|
214
|
+
exempted: declared
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export { DEFAULT_MAX_FILES, DEFAULT_MAX_FINDINGS, TASTE_EXTS, taste };
|
|
219
|
+
export { tasteImage };
|
package/locale/en.json
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
{
|
|
2
|
+
"meta": {
|
|
3
|
+
"title": "Visual Design Toolkit",
|
|
4
|
+
"description": "One plugin that takes visual work all the way to deliverable: search 8000+ icons with zh/en keywords and emit project-ready code, check two-state SVG morph compatibility, score the \"AI look\" of a UI or a single PNG with concrete fixes, audit typography and composition automatically (source font system, real text geometry in SVG, composition of rendered PNGs), get skin x skeleton style and interaction tokens, render and composite locally with multi-size and ICO export, and browse 221 built-in assets. Core features are fully offline; only HTML-to-PNG needs a local browser."
|
|
5
|
+
}
|
|
6
|
+
}
|
package/locale/zh.json
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,94 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-visualkit",
|
|
3
|
-
"version": "0.0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "视觉设计工具箱(Visual Design Toolkit) — 给工具和框架,不给风格目录:同一套原语(配色/字体/形状/图案/栅格/动效/交互态)可落成 UI、网页、平面、海报与文本版式。含图标检索出码、AI 味评分与返工、排版几何体检、风格规格、本地渲染合成。零外部进程(resvg vendored)。",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "ikeyuchen",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"main": "lib/index.js",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./lib/index.js",
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
"./cordis.patch.yml": "./cordis.patch.yml",
|
|
13
|
+
"./locale/*.json": "./locale/*.json",
|
|
14
|
+
"./icon": "./icon.svg"
|
|
15
|
+
},
|
|
16
|
+
"icon": "./icon.svg",
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/ikeyuchen/dsh-visualkit.git"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/ikeyuchen/dsh-visualkit#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/ikeyuchen/dsh-visualkit/issues"
|
|
27
|
+
},
|
|
28
|
+
"os": [
|
|
29
|
+
"win32"
|
|
30
|
+
],
|
|
31
|
+
"cpu": [
|
|
32
|
+
"x64"
|
|
33
|
+
],
|
|
34
|
+
"keywords": [
|
|
35
|
+
"dsh",
|
|
36
|
+
"dsh-plugin",
|
|
37
|
+
"deepseek-harness",
|
|
38
|
+
"cordis",
|
|
39
|
+
"art",
|
|
40
|
+
"aesthetic",
|
|
41
|
+
"design",
|
|
42
|
+
"ui",
|
|
43
|
+
"icons",
|
|
44
|
+
"svg",
|
|
45
|
+
"morph",
|
|
46
|
+
"resvg",
|
|
47
|
+
"design-tokens",
|
|
48
|
+
"ai-look",
|
|
49
|
+
"graphic-design",
|
|
50
|
+
"poster",
|
|
51
|
+
"typography",
|
|
52
|
+
"interaction"
|
|
53
|
+
],
|
|
54
|
+
"files": [
|
|
55
|
+
"lib/",
|
|
56
|
+
"data/",
|
|
57
|
+
"vendor/",
|
|
58
|
+
"cordis.patch.yml",
|
|
59
|
+
"icon.svg",
|
|
60
|
+
"locale/*.json",
|
|
61
|
+
"README.md",
|
|
62
|
+
"LICENSE",
|
|
63
|
+
"CHANGELOG.md",
|
|
64
|
+
"THIRD_PARTY.md"
|
|
65
|
+
],
|
|
66
|
+
"dsh": {
|
|
67
|
+
"manifestVersion": 1,
|
|
68
|
+
"bundle": {
|
|
69
|
+
"patch": "./cordis.patch.yml"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"scripts": {
|
|
73
|
+
"test": "node --test \"test/*.test.mjs\"",
|
|
74
|
+
"test:spec": "node --test --test-reporter=spec \"test/*.test.mjs\"",
|
|
75
|
+
"test:pack": "node --test test/pack.test.mjs",
|
|
76
|
+
"check": "node --test \"test/*.test.mjs\"",
|
|
77
|
+
"pack:dry": "npm pack --dry-run",
|
|
78
|
+
"prepublishOnly": "npm run release:check && npm test",
|
|
79
|
+
"selfcheck": "node tools/selfcheck.mjs",
|
|
80
|
+
"release:check": "node tools/release-check.mjs"
|
|
81
|
+
},
|
|
82
|
+
"peerDependencies": {
|
|
83
|
+
"@deepseek-ai/dsh-tools": ">=0.2.0-rc.2 <0.3.0",
|
|
84
|
+
"@deepseek-ai/schemastery": "~3.18.4"
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@deepseek-ai/dsh-tools": "0.2.1-rc.2",
|
|
88
|
+
"@deepseek-ai/schemastery": "~3.18.4"
|
|
89
|
+
},
|
|
90
|
+
"engines": {
|
|
91
|
+
"node": ">=22.19.0",
|
|
92
|
+
"dsh": ">=0.2.0-rc.2 <0.3.0"
|
|
93
|
+
}
|
|
94
|
+
}
|