Skip to content

Commit b832893

Browse files
committed
refactor: 图标策略对齐 #6,目录改 public/skills
参考 #6 把图标目录从 public/icons/tech 改成 public/skills、index 文件改名 skills-index.json 并加 schemaVersion 包一层;normalize 函数对齐 issue 提案:小写 + & → and + + → plus,字符集放宽到 [a-z0-9 一-龥] 让百度统计 / 飞书 / 微信登录这类纯中文规则名也能正常入索引(后续可在 custom-icons/ 补中文品牌图标);沿用我们 ` / ` 拆别名和品牌首词兜底逻辑、覆盖到 2514 个 slug 映射 / 1659 个物理文件;无图标的依旧走文字首字母色块,不动现有兜底语义;将版本号提升到 1.3.69。 Refs #6
1 parent d5a087f commit b832893

1,665 files changed

Lines changed: 54 additions & 74 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

build-scripts/extract-wappalyzer-icons.mjs

Lines changed: 28 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
#!/usr/bin/env node
2-
// 从本地安装的 Wappalyzer 扩展抽取与我们 rules name 命中的图标,放到 public/icons/tech/<slug>.{svg,png}
2+
// 从本地安装的 Wappalyzer 扩展抽取与我们 rules name 命中的图标,放到 public/skills/<slug>.{svg,png}
33
// 用法:WAPPALYZER_ICON_DIR=<wappalyzer 安装目录的 images/icons> node build-scripts/extract-wappalyzer-icons.mjs
44
// 默认到百分浏览器(Cent Browser)的 Wappalyzer 6.12.2 安装路径找
5+
// 设计参考:https://github.com/setube/stackprism/issues/6
56
import fs from 'node:fs'
67
import path from 'node:path'
78
import { fileURLToPath } from 'node:url'
@@ -13,8 +14,9 @@ const DEFAULT_DIR =
1314
'C:/Users/19622/AppData/Local/CentBrowser/User Data/Default/Extensions/gppongmhjkpfnbhagpmjfkannfbllamg/6.12.2_0/images/icons'
1415
const ICON_DIR = process.env.WAPPALYZER_ICON_DIR || DEFAULT_DIR
1516
const RULES_DIR = path.join(repoRoot, 'public', 'rules')
16-
const OUTPUT_DIR = path.join(repoRoot, 'public', 'icons', 'tech')
17+
const OUTPUT_DIR = path.join(repoRoot, 'public', 'skills')
1718
const CUSTOM_DIR = path.join(__dirname, 'custom-icons')
19+
const MANIFEST_PATH = path.join(repoRoot, 'src', 'ui', 'components', 'skills-index.json')
1820

1921
// service worker / page-detector 在运行时硬编码塞到识别结果里、不在 rules JSON 中出现的技术名
2022
const EXTRA_NAMES = ['HTTP/2', 'HTTP/3', 'HTTPS']
@@ -44,12 +46,16 @@ const walk = (dir, files = []) => {
4446
return files
4547
}
4648

47-
const slugify = raw =>
48-
String(raw || '')
49+
// 规范化函数:跟 TechChip / issue#6 保持一致
50+
// 小写 + `&`→`and` + `+`→`plus`,保留 [a-z0-9] 和 CJK 统一表意文字(让"百度统计"、"飞书"这类纯中文名也能查到)
51+
const normalize = raw =>
52+
String(raw ?? '')
4953
.toLowerCase()
50-
.replace(/[^a-z0-9]/g, '')
54+
.replace(/&/g, 'and')
55+
.replace(/\+/g, 'plus')
56+
.replace(/[^a-z0-9-]+/g, '')
5157

52-
// 跟 TechChip 里的 toSlug 保持一致:用 ` / `(带空格)拆别名,保留 HTTP/2 这类纯版本号写法
58+
// 拆别名:` / `(带空格)只在别名分隔时出现,保留 HTTP/2 这类纯版本号写法
5359
const primaryName = raw =>
5460
String(raw || '')
5561
.split(' / ')[0]
@@ -59,7 +65,7 @@ const primaryName = raw =>
5965
const iconBySlug = new Map()
6066
for (const f of fs.readdirSync(ICON_DIR)) {
6167
if (!f.endsWith('.svg') && !f.endsWith('.png')) continue
62-
const slug = slugify(f.replace(/\.(svg|png)$/i, ''))
68+
const slug = normalize(f.replace(/\.(svg|png)$/i, ''))
6369
if (!slug) continue
6470
if (!iconBySlug.has(slug)) iconBySlug.set(slug, [])
6571
iconBySlug.get(slug).push(f)
@@ -82,13 +88,13 @@ fs.mkdirSync(OUTPUT_DIR, { recursive: true })
8288
// 给定一个 name,返回命中的 Wappalyzer slug(可能跟 localKey 不同,例如 cloudflarewebanalytics → cloudflare)
8389
const matchWappalyzerSlug = name => {
8490
const base = primaryName(name)
85-
const fullSlug = slugify(base)
91+
const fullSlug = normalize(base)
8692
if (iconBySlug.has(fullSlug)) return fullSlug
8793
// 首词兜底:"Cloudflare Web Analytics" → "Cloudflare" → cloudflare;
8894
// "Microsoft Teams" → "Microsoft"。会用品牌主 logo,牺牲一点准确度换覆盖率
8995
const firstWord = base.split(/\s+/)[0]
9096
if (!firstWord) return null
91-
const firstSlug = slugify(firstWord)
97+
const firstSlug = normalize(firstWord)
9298
if (firstSlug && firstSlug !== fullSlug && iconBySlug.has(firstSlug)) return firstSlug
9399
return null
94100
}
@@ -99,14 +105,14 @@ let svgCount = 0
99105
let pngCount = 0
100106
let customCount = 0
101107
let totalBytes = 0
102-
// manifest:localKey(规则名 slug) → 实际磁盘文件名(可能多个 key 共用一个文件,节省体积)
103-
const manifest = {}
108+
// skillsIndex:localKey(规则名 slug) → 实际磁盘文件名(可能多个 key 共用一个文件,节省体积)
109+
const skillsIndex = {}
104110
// 已写到磁盘的 Wappalyzer slug → 文件名,避免重复写
105111
const writtenFiles = new Map()
106112
const seenLocalKeys = new Set()
107113

108114
for (const name of ruleNames) {
109-
const localKey = slugify(primaryName(name))
115+
const localKey = normalize(primaryName(name))
110116
if (!localKey || seenLocalKeys.has(localKey)) continue
111117
const matchedSlug = matchWappalyzerSlug(name)
112118
if (!matchedSlug) continue
@@ -127,7 +133,7 @@ for (const name of ruleNames) {
127133
}
128134

129135
seenLocalKeys.add(localKey)
130-
manifest[localKey] = filename
136+
skillsIndex[localKey] = filename
131137
mappedKeys++
132138
if (matchedSlug !== localKey) prefixMatched++
133139
}
@@ -137,29 +143,29 @@ for (const name of ruleNames) {
137143
if (fs.existsSync(CUSTOM_DIR)) {
138144
for (const f of fs.readdirSync(CUSTOM_DIR)) {
139145
if (!f.endsWith('.svg') && !f.endsWith('.png')) continue
140-
const slug = slugify(f.replace(/\.(svg|png)$/i, ''))
146+
const slug = normalize(f.replace(/\.(svg|png)$/i, ''))
141147
if (!slug) continue
142148
const ext = path.extname(f).toLowerCase().slice(1)
143149
const filename = slug + '.' + ext
144150
const dst = path.join(OUTPUT_DIR, filename)
145151
fs.copyFileSync(path.join(CUSTOM_DIR, f), dst)
146-
if (!manifest[slug]) mappedKeys++
147-
manifest[slug] = filename
152+
if (!skillsIndex[slug]) mappedKeys++
153+
skillsIndex[slug] = filename
148154
customCount++
149155
totalBytes += fs.statSync(dst).size
150156
}
151157
}
152158

153-
// 写出 manifest,运行时 TechChip 用它判断本地是否有图标、是 svg 还是 png,避免无意义 404
154-
const sortedManifest = {}
155-
for (const slug of Object.keys(manifest).sort()) sortedManifest[slug] = manifest[slug]
156-
const manifestPath = path.join(repoRoot, 'src', 'ui', 'components', 'local-icon-manifest.json')
157-
fs.writeFileSync(manifestPath, JSON.stringify(sortedManifest) + '\n', 'utf8')
159+
// 写出 skills-index.json,运行时 TechChip 用它判断本地是否有图标,避免无意义 404
160+
const sortedIndex = {}
161+
for (const slug of Object.keys(skillsIndex).sort()) sortedIndex[slug] = skillsIndex[slug]
162+
const out = { schemaVersion: 1, skillsIndex: sortedIndex }
163+
fs.writeFileSync(MANIFEST_PATH, JSON.stringify(out) + '\n', 'utf8')
158164

159165
console.log(
160166
`抽取完成:${mappedKeys} 个映射 → ${writtenFiles.size + customCount} 个物理文件 ` +
161167
`(svg: ${svgCount}, png: ${pngCount}, custom: ${customCount}, 首词兜底: ${prefixMatched})`
162168
)
163169
console.log(`输出目录:${OUTPUT_DIR}`)
164170
console.log(`总大小:${(totalBytes / 1024 / 1024).toFixed(2)} MB`)
165-
console.log(`manifest:${manifestPath}`)
171+
console.log(`索引:${MANIFEST_PATH}`)

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "stackprism",
33
"private": true,
4-
"version": "1.3.68",
4+
"version": "1.3.69",
55
"type": "module",
66
"description": "StackPrism 用于检测网页前端、后端、CDN、SaaS、广告营销、统计、登录、支付、网站程序和主题模板线索。",
77
"scripts": {

public/icons/tech/crm.svg

Lines changed: 0 additions & 26 deletions
This file was deleted.

0 commit comments

Comments
 (0)