Skip to content

Commit 75aed4f

Browse files
committed
feat: copy full tech stack report
1 parent 502e647 commit 75aed4f

10 files changed

Lines changed: 358 additions & 13 deletions

File tree

docs/config/disabled-technologies.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ Cloudflare
3131

3232
## 与原始线索的关系
3333

34-
被禁用的技术**只是不显示**,不会从 raw JSON 里删除。「原始线索」面板和「复制 JSON」按钮拿到的数据仍然完整,方便之后复核。
34+
被禁用的技术**只是不显示**,不会从 raw JSON 里删除。「原始线索」面板拿到的数据仍然完整,方便之后复核。
3535

3636
## 例子
3737

docs/dev/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ Chrome 扩展有四种执行环境,StackPrism 全部用上:
108108
| ----------------------------- | ------------- | ------------------------------------ |
109109
| `GET_HEADER_DATA` | popup → bg | 拉取响应头记录 |
110110
| `GET_POPUP_RESULT` | popup → bg | 拉取轻量缓存(弹窗主显示) |
111-
| `GET_POPUP_RAW_RESULT` | popup → bg | 拉取完整 raw(原始线索 / 复制 JSON) |
111+
| `GET_POPUP_RAW_RESULT` | popup → bg | 拉取完整 raw(原始线索 / 纠错反馈) |
112112
| `GET_TECH_LINK` | popup → bg | 兜底查询某技术的官网链接 |
113113
| `START_BACKGROUND_DETECTION` | popup → bg | 「刷新」按钮触发主动检测 |
114114
| `GET_WORDPRESS_THEME_DETAILS` | bg internal | 抓取主题 style.css header |

docs/dev/release.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ openssl genrsa -out extension.pem 2048
119119
- [ ] `pnpm run typecheck` 通过
120120
- [ ] `pnpm run lint` 通过
121121
- [ ] `pnpm run build` 通过
122-
- [ ] 在 chrome 里加载 `dist/` 手动测试关键路径(弹窗打开、识别一个站点、刷新、复制 JSON、设置页加规则)
122+
- [ ] 在 chrome 里加载 `dist/` 手动测试关键路径(弹窗打开、识别一个站点、刷新、复制完整技术栈报告、设置页加规则)
123123
- [ ]`package.json` 的 version bump
124124
- [ ] git commit + push
125125
- [ ] 如果版本符合 release 节点,在 GitHub UI 发布 release(tag 为 `v{version}`,与 package.json 对齐)

docs/guide/basic-usage.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
- **右边四个图标按钮**
1313
- **主题**(太阳 / 月亮 / 屏幕图标):点击在「自动 / 浅色 / 深色」三个状态间循环
1414
- **设置**:打开设置页(独立标签页)
15-
- **复制**把当前检测的完整 raw JSON 复制到剪贴板
15+
- **复制**把当前页 URL 复制到剪贴板
1616
- **刷新**:通知后台重新检测当前页面,已有缓存结果会先留在弹窗里
1717

1818
## 概览数据
@@ -59,10 +59,10 @@ React [高置信度]
5959
## 底部工具栏
6060

6161
```text
62-
[搜索] [原始线索] GitHub
62+
[搜索] [原始线索] [复制全部] GitHub
6363
```
6464

65-
左侧两个工具按钮,详情见 [辅助工具](./tools.md)点击后会从底部打开面板,再点同个按钮或面板里的关闭按钮即可收起。
65+
左侧工具按钮,详情见 [辅助工具](./tools.md)搜索和原始线索会从底部打开面板,再点同个按钮或面板里的关闭按钮即可收起;复制全部会直接把完整技术栈报告写入剪贴板
6666

6767
## 滚动行为
6868

docs/guide/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
- [安装与启用](./install.md) — 三种安装方式(Chrome Web Store、Edge 商店、本地加载)
88
- [基本使用](./basic-usage.md) — 弹窗里五个区域是干什么的
99
- [结果解读](./results.md) — 置信度、证据、来源、纠正按钮怎么看
10-
- [辅助工具](./tools.md) — 网页源代码搜索、原始线索面板、复制检测 JSON
10+
- [辅助工具](./tools.md) — 网页源代码搜索、原始线索面板、复制完整技术栈报告
1111

1212
## 最短路径
1313

docs/guide/tools.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# 辅助工具
22

3-
弹窗底部有两个工具:「搜索」「原始线索」。点击后会从底部打开面板,最高占弹窗高度 60%;再次点击按钮或点面板里的关闭按钮即可收起。
3+
弹窗底部有三个工具:「搜索」「原始线索」和「复制全部」。搜索与原始线索会从底部打开面板,最高占弹窗高度 60%;再次点击按钮或点面板里的关闭按钮即可收起。复制全部不会打开面板,它会直接把完整技术栈报告复制到剪贴板
44

55
## 网页源代码搜索
66

@@ -75,12 +75,18 @@
7575

7676
抽屉标题会变成「原始线索 · React · 响应头」这种形式提示当前是 scoped 视图。再点底部工具栏的「原始线索」按钮会切回完整 JSON。
7777

78-
## 复制 JSON
78+
## 复制完整技术栈报告
7979

80-
弹窗顶部的「复制」按钮会把当前的完整 raw JSON 复制到剪贴板(与「原始线索」面板内容一致)。
80+
弹窗底部的「复制全部」按钮会把当前页面的完整技术栈报告复制到剪贴板。这个报告包含:
81+
82+
- 人类可读的 Markdown 摘要
83+
- `stackprism.tech_stack_report.v1` 结构化 JSON
84+
- 当前页面的 URL、标题、生成时间、技术总数、资源数和主文档响应头数
85+
86+
它复制的是**当前弹窗结果**,不受「重点」视图或分类下拉筛选影响;设置页里的分类开关和禁用名单仍会生效。你可以直接粘贴到 issue、笔记、AI Agent 输入框或外部文本工具里。
8187

8288
常见用途:
8389

84-
- 提 issue / PR 时附上完整数据
85-
- 离线分析多个站点的检测结果差异
86-
- 在外部工具(jq、JSON Viewer)里浏览
90+
- 提 issue / PR 时附上完整技术栈上下文
91+
- 把某个站点的技术栈记录到文档或知识库
92+
- 交给 AI Agent 做进一步分析

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
"lint": "eslint src",
1010
"build:injected": "node build-scripts/build-injected.mjs",
1111
"build": "pnpm run build:injected && vite build",
12+
"test:unit": "node --test tests/*.test.mjs",
1213
"check:links": "node build-scripts/check-tech-links.mjs",
1314
"extract:icons": "node build-scripts/extract-wappalyzer-icons.mjs",
1415
"dev": "pnpm run build:injected && vite",

src/ui/popup/Popup.vue

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,10 @@
286286
<FileCode :size="13" :stroke-width="2" />
287287
<span>原始线索</span>
288288
</RippleButton>
289+
<RippleButton class="footer-tool-btn" title="复制全部技术栈报告" @click="copyTechStackReport">
290+
<ClipboardList :size="13" :stroke-width="2" />
291+
<span>复制全部</span>
292+
</RippleButton>
289293
</div>
290294
<a class="footer-repo" :href="REPOSITORY_URL" target="_blank" rel="noreferrer" @click="openRepository">GitHub</a>
291295
</footer>
@@ -297,6 +301,7 @@
297301
import {
298302
ArrowUp,
299303
Ban,
304+
ClipboardList,
300305
Copy,
301306
Download,
302307
ExternalLink,
@@ -332,6 +337,7 @@
332337
} from '@/utils/constants'
333338
import { cycleTheme, getStoredTheme, setStoredTheme, themeLabel, type ThemeMode } from '@/utils/theme'
334339
import { checkPageSupport } from '@/utils/page-support'
340+
import { formatTechStackReport } from '@/utils/format-tech-stack'
335341
336342
const RAW_LOADING_TEXT = '正在请求原始线索...'
337343
@@ -846,6 +852,24 @@
846852
}
847853
}
848854
855+
const copyTechStackReport = async () => {
856+
const result = state.result
857+
if (!result) {
858+
setStatus('暂无可复制的技术栈信息。', 'error')
859+
return
860+
}
861+
if (!navigator.clipboard?.writeText) {
862+
setStatus('当前浏览器不支持直接复制技术栈报告。', 'error')
863+
return
864+
}
865+
try {
866+
await navigator.clipboard.writeText(formatTechStackReport(result))
867+
setStatus('已复制完整技术栈报告。', 'ok')
868+
} catch (error: any) {
869+
setStatus(`复制失败:${String(error?.message || error)}`, 'error')
870+
}
871+
}
872+
849873
const sanitizeFilenameSegment = (input: string) => {
850874
const cleaned = String(input || '')
851875
.trim()

src/utils/format-tech-stack.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
interface TechnologyLike {
2+
category?: string
3+
name?: string
4+
kind?: string
5+
confidence?: string
6+
evidence?: string[]
7+
sources?: string[]
8+
source?: string
9+
url?: string
10+
version?: string
11+
}
12+
13+
interface TechStackReportInput {
14+
url?: string
15+
title?: string
16+
generatedAt?: string
17+
technologies?: TechnologyLike[]
18+
resources?: { total?: number } | null
19+
headerCount?: number
20+
headers?: unknown
21+
}
22+
23+
const cleanText = (value: unknown): string => String(value ?? '').trim()
24+
25+
const cleanInlineText = (value: unknown): string => cleanText(value).replace(/\s+/g, ' ')
26+
27+
const cleanList = (value: unknown): string[] => {
28+
if (!Array.isArray(value)) return []
29+
return value
30+
.filter(item => typeof item === 'string' || typeof item === 'number')
31+
.map(item => cleanInlineText(item))
32+
.filter(Boolean)
33+
}
34+
35+
const getHeaderCount = (input: TechStackReportInput): number => {
36+
if (typeof input.headerCount === 'number' && Number.isFinite(input.headerCount) && input.headerCount >= 0) {
37+
return input.headerCount
38+
}
39+
if (Array.isArray(input.headers)) return input.headers.length
40+
if (input.headers && typeof input.headers === 'object') return Object.keys(input.headers).length
41+
return 0
42+
}
43+
44+
const getResourceCount = (input: TechStackReportInput): number => {
45+
const total = Number(input.resources?.total || 0)
46+
return Number.isFinite(total) && total >= 0 ? total : 0
47+
}
48+
49+
const normalizeTechnologies = (items: TechnologyLike[] = []) =>
50+
items
51+
.map(item => ({
52+
category: cleanInlineText(item.category) || '未分类',
53+
name: cleanInlineText(item.name),
54+
version: cleanInlineText(item.version),
55+
kind: cleanInlineText(item.kind),
56+
confidence: cleanInlineText(item.confidence) || '未知',
57+
sources: cleanList(item.sources || (item.source ? [item.source] : [])),
58+
evidence: cleanList(item.evidence),
59+
url: cleanText(item.url)
60+
}))
61+
.filter(item => item.name)
62+
63+
type NormalizedTechnology = ReturnType<typeof normalizeTechnologies>[number]
64+
65+
const groupTechnologies = (items: NormalizedTechnology[]) => {
66+
const groups = new Map<string, typeof items>()
67+
for (const item of items) {
68+
const group = groups.get(item.category) || []
69+
group.push(item)
70+
groups.set(item.category, group)
71+
}
72+
return [...groups.entries()]
73+
}
74+
75+
const buildReportHeader = (
76+
input: TechStackReportInput,
77+
technologies: NormalizedTechnology[],
78+
resourcesTotal: number,
79+
headerCount: number,
80+
generatedAt: string
81+
) => [
82+
'# StackPrism 技术栈报告',
83+
'',
84+
`URL: ${cleanInlineText(input.url) || '未知'}`,
85+
`标题: ${cleanInlineText(input.title) || '未知'}`,
86+
`生成时间: ${generatedAt}`,
87+
'报告范围: 当前弹窗结果',
88+
`技术总数: ${technologies.length}`,
89+
`资源数: ${resourcesTotal}`,
90+
`主文档响应头数: ${headerCount}`,
91+
'',
92+
'## 人类阅读摘要'
93+
]
94+
95+
const appendHumanSummary = (lines: string[], technologies: NormalizedTechnology[]) => {
96+
if (!technologies.length) {
97+
lines.push('', '未检测到明确技术栈。')
98+
return
99+
}
100+
for (const [category, items] of groupTechnologies(technologies)) {
101+
lines.push('', `### ${category} (${items.length})`)
102+
for (const item of items) {
103+
const name = item.version ? `${item.name} ${item.version}` : item.name
104+
lines.push(`- ${name} [${item.confidence}]`)
105+
if (item.kind) lines.push(` - 类型: ${item.kind}`)
106+
if (item.sources.length) lines.push(` - 来源: ${item.sources.join(', ')}`)
107+
if (item.evidence.length) lines.push(` - 依据: ${item.evidence.join(' | ')}`)
108+
if (item.url) lines.push(` - 链接: ${item.url}`)
109+
}
110+
}
111+
}
112+
113+
const buildStructuredPayload = (
114+
input: TechStackReportInput,
115+
technologies: NormalizedTechnology[],
116+
resourcesTotal: number,
117+
headerCount: number,
118+
generatedAt: string
119+
) => ({
120+
schema: 'stackprism.tech_stack_report.v1',
121+
url: cleanText(input.url),
122+
title: cleanText(input.title),
123+
generatedAt,
124+
summary: {
125+
technologyCount: technologies.length,
126+
resourceCount: resourcesTotal,
127+
headerCount
128+
},
129+
technologies
130+
})
131+
132+
export const formatTechStackReport = (input: TechStackReportInput): string => {
133+
const technologies = normalizeTechnologies(input.technologies)
134+
const resourcesTotal = getResourceCount(input)
135+
const headerCount = getHeaderCount(input)
136+
const generatedAt = cleanText(input.generatedAt) || new Date().toISOString()
137+
const lines = buildReportHeader(input, technologies, resourcesTotal, headerCount, generatedAt)
138+
const structured = buildStructuredPayload(input, technologies, resourcesTotal, headerCount, generatedAt)
139+
140+
appendHumanSummary(lines, technologies)
141+
lines.push('', '## AI Agent 结构化数据', '', '````json', JSON.stringify(structured, null, 2), '````')
142+
return lines.join('\n')
143+
}

0 commit comments

Comments
 (0)