Skip to content

Commit b149e04

Browse files
committed
feat: 原始线索面板加下载按钮
popup 的「原始线索」面板顶部加下载按钮,跟复制按钮并列。没有选具体技术(rawSourceContext 为空)时下载完整 raw JSON,选了某条技术的「原始线索」按钮后下载 scoped JSON(只含该技术相关的源)。文件名格式 `stackprism_<host>_[tech]_[source]_<ISO 时间戳>.json`,自动从当前页面 URL 拿 hostname,去掉非法路径字符。同时把 scoped raw 默认带上响应头字段(之前只有「响应头」来源才包含),方便 spoof 场景下交叉对照伪造头与具体识别项。 将版本号提升到 1.3.59。
1 parent 6fb3eee commit b149e04

2 files changed

Lines changed: 55 additions & 1 deletion

File tree

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.58",
4+
"version": "1.3.59",
55
"type": "module",
66
"description": "StackPrism 用于检测网页前端、后端、CDN、SaaS、广告营销、统计、登录、支付、网站程序和主题模板线索。",
77
"scripts": {

src/ui/popup/Popup.vue

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,9 @@
182182
<header class="footer-panel-head">
183183
<span class="footer-panel-title">{{ footerPanelTitle }}</span>
184184
<div class="footer-panel-actions">
185+
<RippleButton v-if="footerPanel === 'raw'" class="footer-panel-copy" title="下载原始线索" @click="downloadRawOutput">
186+
<Download :size="12" :stroke-width="2" />
187+
</RippleButton>
185188
<RippleButton v-if="footerPanel === 'raw'" class="footer-panel-copy" title="复制原始线索" @click="copyRawOutput">
186189
<Copy :size="12" :stroke-width="2" />
187190
</RippleButton>
@@ -270,6 +273,7 @@
270273
ArrowUp,
271274
Ban,
272275
Copy,
276+
Download,
273277
ExternalLink,
274278
FileCode,
275279
Flag,
@@ -808,6 +812,56 @@
808812
}
809813
}
810814
815+
const sanitizeFilenameSegment = (input: string) => {
816+
const cleaned = String(input || '')
817+
.trim()
818+
.replace(/[\s/\\:*?"<>|]+/g, '-')
819+
.replace(/^-+|-+$/g, '')
820+
return cleaned.slice(0, 64)
821+
}
822+
823+
const buildRawDownloadFilename = () => {
824+
let host = 'page'
825+
try {
826+
const target = state.result?.url || state.rawResult?.url || ''
827+
if (target) host = new URL(target).hostname.replace(/^www\./, '')
828+
} catch {
829+
// ignore parse error, fall back to default host
830+
}
831+
const ctx = rawSourceContext.value
832+
const parts = ['stackprism', host || 'page']
833+
if (ctx) {
834+
const techPart = sanitizeFilenameSegment(ctx.tech?.name || '')
835+
const sourcePart = sanitizeFilenameSegment(ctx.source || '')
836+
if (techPart) parts.push(techPart)
837+
if (sourcePart) parts.push(sourcePart)
838+
}
839+
parts.push(new Date().toISOString().replace(/[:.]/g, '-').replace(/Z$/, ''))
840+
return `${parts.filter(Boolean).join('_')}.json`
841+
}
842+
843+
const downloadRawOutput = () => {
844+
const text = rawOutputText.value
845+
if (!text || text === RAW_PLACEHOLDER || text === RAW_LOADING_TEXT) {
846+
setStatus('暂无可下载的原始线索。', 'error')
847+
return
848+
}
849+
try {
850+
const blob = new Blob([text], { type: 'application/json;charset=utf-8' })
851+
const objectUrl = URL.createObjectURL(blob)
852+
const anchor = document.createElement('a')
853+
anchor.href = objectUrl
854+
anchor.download = buildRawDownloadFilename()
855+
document.body.appendChild(anchor)
856+
anchor.click()
857+
anchor.remove()
858+
setTimeout(() => URL.revokeObjectURL(objectUrl), 1000)
859+
setStatus(rawSourceContext.value ? '已下载来源详情。' : '已下载完整原始线索。', 'ok')
860+
} catch (error: any) {
861+
setStatus(`下载失败:${String(error?.message || error)}`, 'error')
862+
}
863+
}
864+
811865
const getRawResult = async () => {
812866
if (state.rawLoaded) return state.rawResult
813867
const tabId = state.currentTabId || (await getActiveTabId())

0 commit comments

Comments
 (0)