结果分享闭环 Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 让工具结果卡片带上可扫的二维码,并把「分享出去」和「扫码回来」两端都变成可测量的埋点。
Architecture: 改的是两个共用组件 —— ResultShareCard.vue(卡片视觉,加二维码)与 AssessmentResult.vue(按钮与埋点)。二维码内容由纯函数 utils/shareLink.ts 生成,可单测。回流识别放在 utils/events.ts,由工具页 pages/tools/[slug].vue 在挂载时调用一次。20 个接入 AssessmentResult 的工具自动获得全部改动。
Tech Stack: Nuxt 3 + Vue 3 <script setup> + TypeScript · qrcode@^1.5.4(已在依赖里)· html-to-image(经 utils/captureNode.ts)· Vitest 2(environment: 'node',只测纯函数)· @vercel/analytics 的 track
Spec: docs/afit/specs/2026-09-17-result-share-loop.md
Global Constraints
- 隐私红线:分数/等级/日期/任何用户输入绝不能进 URL。二维码 URL 只允许一个参数
s=qr。分数只出现在图片里。 - 埋点绝不能影响工具本身:一律走
utils/events.ts的safeTrack(内部 try/catch 静默吞掉;isDevelopmentMode()下不发)。 - 二维码必须是朴素黑白方码,纠错级别
M。禁止使用qr-code-styling(微信长按识别兼容差)。 - 导出参数:
utils/captureNode.ts用pixelRatio: 1.5。二维码 CSS 尺寸 84px,生成的 dataURL 宽度 336px(4×,缩放后仍清晰)。 - 五种语言:新增的每个 i18n key 必须同时补齐
web/i18n/lang/下的en.json/zh.json/zh-TW.json/ja.json/ko.json。 - 测试范围:只对纯函数写 Vitest 单测(仓库现状:
utils/events.ts等依赖 window 的模块无单测,不在本期引入测试基建改造)。 - 所有路径相对
web/目录。
与规格的一处有意偏离
规格 §5.2 写「基础 URL 走 utils/seo.ts 的 getCanonicalUrl()」。实现改为取当前页面的 window.location.origin + pathname,原因:工具页当前 URL 本身就是它的 canonical,直接用可避免重新推导语言前缀(getCanonicalUrl 需要传入不带语言前缀的 path + 当前语言 + 默认语言,在组件里再推一遍反而更容易错)。若将来 AssessmentResult 会出现在非 canonical 路径上,需回头修正。
Task 1: 分享链接纯函数 + 单测
Files:
- Create:
web/utils/shareLink.ts - Create:
web/__tests__/unit/shareLink.test.ts - Modify:
web/vitest.config.ts:13(coverageinclude数组)
Interfaces:
Consumes: 无
Produces:
SHARE_PARAM: 's'、SHARE_VALUE: 'qr'buildShareUrl(currentUrl: string): string—— 输入任意完整 URL,输出剥掉 query/hash、补尾斜杠、且只带?s=qr的分享 URLisShareArrival(search: string): boolean—— 输入location.search,判断是否由二维码而来
[ ] Step 1: 写失败的测试
// web/__tests__/unit/shareLink.test.ts
import { describe, it, expect } from 'vitest'
import { SHARE_PARAM, SHARE_VALUE, buildShareUrl, isShareArrival } from '../../utils/shareLink'
describe('buildShareUrl — 二维码里编码的 URL', () => {
it('should append ?s=qr to a clean tool URL', () => {
expect(buildShareUrl('https://www.moodji.net/zh/tools/dass-21/')).toBe(
'https://www.moodji.net/zh/tools/dass-21/?s=qr'
)
})
it('should add the trailing slash when missing', () => {
expect(buildShareUrl('https://www.moodji.net/tools/focus-assessment')).toBe(
'https://www.moodji.net/tools/focus-assessment/?s=qr'
)
})
// 隐私红线:任何已有参数都必须被丢弃,绝不能把分数带进二维码
it('should drop every existing query param, including a score-like one', () => {
expect(buildShareUrl('https://www.moodji.net/zh/tools/dass-21/?score=42&ch=abc')).toBe(
'https://www.moodji.net/zh/tools/dass-21/?s=qr'
)
})
it('should drop the hash fragment', () => {
expect(buildShareUrl('https://www.moodji.net/zh/tools/dass-21/#result')).toBe(
'https://www.moodji.net/zh/tools/dass-21/?s=qr'
)
})
it('should not double-append when the input already carries s=qr', () => {
expect(buildShareUrl('https://www.moodji.net/zh/tools/dass-21/?s=qr')).toBe(
'https://www.moodji.net/zh/tools/dass-21/?s=qr'
)
})
it('should return an empty string for an unparseable input instead of throwing', () => {
expect(buildShareUrl('not a url')).toBe('')
})
it('should expose the param names it uses', () => {
expect(`${SHARE_PARAM}=${SHARE_VALUE}`).toBe('s=qr')
})
})
describe('isShareArrival — 落地页是否来自二维码', () => {
it('should detect the param as the only one', () => {
expect(isShareArrival('?s=qr')).toBe(true)
})
it('should detect the param among others', () => {
expect(isShareArrival('?utm_source=x&s=qr')).toBe(true)
})
it('should return false when absent', () => {
expect(isShareArrival('?utm_source=x')).toBe(false)
})
it('should return false for an empty search string', () => {
expect(isShareArrival('')).toBe(false)
})
it('should not match a different value', () => {
expect(isShareArrival('?s=app')).toBe(false)
})
it('should not match a param that merely ends with s', () => {
expect(isShareArrival('?ns=qr')).toBe(false)
})
})- [ ] Step 2: 运行测试,确认失败
Run: cd web && npx vitest run __tests__/unit/shareLink.test.ts Expected: FAIL —— Failed to resolve import "../../utils/shareLink"
- [ ] Step 3: 写最小实现
// web/utils/shareLink.ts
/**
* 分享二维码链接。
*
* 🔴 隐私红线:分数只进图片,绝不进 URL。链接会被转发、缓存、进浏览器历史与
* 服务端日志,而图片是用户自己决定发给谁的。因此这里**先丢掉输入 URL 上的所有
* 参数**,再只补一个 `s=qr` —— 即使调用方传进来一个带分数的地址也无法泄露。
*/
export const SHARE_PARAM = 's'
export const SHARE_VALUE = 'qr'
export function buildShareUrl(currentUrl: string): string {
let url: URL
try {
url = new URL(currentUrl)
} catch {
// 拿不到合法 URL 时返回空串,由调用方决定不渲染二维码——绝不抛错打断结果页。
return ''
}
const path = url.pathname.endsWith('/') ? url.pathname : `${url.pathname}/`
return `${url.origin}${path}?${SHARE_PARAM}=${SHARE_VALUE}`
}
export function isShareArrival(search: string): boolean {
if (!search) return false
return new URLSearchParams(search).get(SHARE_PARAM) === SHARE_VALUE
}- [ ] Step 4: 运行测试,确认通过
Run: cd web && npx vitest run __tests__/unit/shareLink.test.ts Expected: PASS(13 个用例)
- [ ] Step 5: 把新模块纳入覆盖率统计
web/vitest.config.ts 第 13 行:
include: ['utils/reactionNorms.ts', 'utils/shareLink.ts'],Run: cd web && npm run coverage(必须跑全量套件,不要带单个文件路径 —— coverage 阈值是全局的,只跑一个文件会让 include 里其余模块记 0%,进而以「未达 80%」失败退出,那是假失败) Expected: 退出码 0,且 utils/shareLink.ts 四项指标均 ≥80%
- [ ] Step 6: 提交
git add web/utils/shareLink.ts web/__tests__/unit/shareLink.test.ts web/vitest.config.ts
git commit -m "feat(share): 分享链接纯函数 — 只允许 s=qr,丢弃一切既有参数
隐私红线落在函数里而不是调用方:即使传进来带分数的 URL 也无法泄露。"Task 2: 回流埋点 share_arrival
Files:
- Modify:
web/utils/events.ts(文件末尾追加;safeTrack在第 144 行,currentToolSlug在第 130 行) - Modify:
web/pages/tools/[slug].vue(<script setup>内新增onMounted)
Interfaces:
- Consumes: Task 1 的
isShareArrival(search: string): boolean - Produces:
reportShareArrival(): void
为什么这个任务没有单测:reportShareArrival 读 window.location 并调用 track,而仓库的 Vitest 跑在 environment: 'node',utils/events.ts 现有函数同样无单测。可测的判定逻辑已经在 Task 1 的 isShareArrival 里测过。本任务靠预览环境实测验证(Task 6)。
- [ ] Step 1: 在
utils/events.ts末尾追加上报函数
在文件顶部的 import 区补上:
import { isShareArrival } from './shareLink';在文件末尾追加:
// 扫二维码进来的人。微信/QQ 打开外链会剥掉 referrer,这批人在 Analytics 里
// 混在「直接访问」里(2026-09 实测 CN 流量 89% 无来源),只能靠 URL 上的
// s=qr 把他们认出来。去重口径与 tool_calculate 一致:同一次页面浏览只报一次。
let reportedArrivalPath = '';
export function reportShareArrival() {
if (typeof window === 'undefined') return;
if (!isShareArrival(window.location.search)) return;
const slug = currentToolSlug();
if (!slug) return;
const path = window.location.pathname;
if (reportedArrivalPath === path) return;
reportedArrivalPath = path;
safeTrack('share_arrival', { slug, locale: getCurrentLocale() });
}- [ ] Step 2: 在工具页挂载时调用
web/pages/tools/[slug].vue 的 <script setup> 中,在已有 import 之后加入:
import { onMounted } from 'vue'
import { reportShareArrival } from '~/utils/events'
onMounted(() => {
reportShareArrival()
})若该文件已从 vue 引入其他具名导出,把 onMounted 并入现有 import,不要重复 import。
- [ ] Step 3: 确认类型与构建通过
Run: cd web && npx nuxi typecheck 2>&1 | tail -20 Expected: 无与 events.ts / [slug].vue 相关的报错。 若仓库未配置 typecheck,改跑 cd web && npm run build 2>&1 | tail -5,Expected: 构建成功。
- [ ] Step 4: 提交
git add web/utils/events.ts web/pages/tools/[slug].vue
git commit -m "feat(share): 新增 share_arrival 埋点,把扫码回流从直接访问里认出来"Task 3: 卡片内嵌二维码
Files:
- Modify:
web/components/tools/common/ResultShareCard.vue(script 第 1–3 行 import 区;props 第 24–50 行;template 底部bottom-row第 154–161 行;<style scoped>末尾)
Interfaces:
Consumes: 无(URL 由 Task 4 以 prop 传入)
Produces:
ResultShareCard新增可选 propqrUrl?: string与qrHint?: string。两者都为空时不渲染二维码块,卡片版式与现在完全一致。[ ] Step 1: 在 script 顶部引入依赖并生成 dataURL
ResultShareCard.vue 第 1–3 行改为:
<script setup lang="ts">
import { computed, ref, watch } from 'vue'
import QRCode from 'qrcode'
import RecommendationList from './RecommendationList.vue'在 props 定义(withDefaults(defineProps<{...}>(), {...}))的类型里追加两个可选字段:
qrUrl?: string
qrHint?: string并在默认值对象里追加:
qrUrl: '',
qrHint: '',在 props 之后追加生成逻辑:
// 二维码用 dataURL 而不是外链 <img src>:导出走 html-to-image,跨域外链图会被
// 静默丢掉,导出的卡片上就会少一块。dataURL 没有这个问题。
// 朴素黑白方码 + 纠错 M:微信长按识别对花式码兼容差。
// 宽度 336 = 展示尺寸 84px 的 4 倍,导出时 pixelRatio 1.5 缩放后仍清晰。
const qrDataUrl = ref('')
watch(
() => props.qrUrl,
async (url) => {
if (!url) {
qrDataUrl.value = ''
return
}
try {
qrDataUrl.value = await QRCode.toDataURL(url, {
errorCorrectionLevel: 'M',
margin: 1,
width: 336,
color: { dark: '#1B1B1B', light: '#FFFFFF' },
})
} catch {
// 生成失败就不显示二维码,卡片其余部分照常——分享图不能因此打不开。
qrDataUrl.value = ''
}
},
{ immediate: true }
)- [ ] Step 2: 在底部行渲染二维码
ResultShareCard.vue 的 bottom-row 块(第 154–161 行)替换为:
<div class="bottom-row">
<span class="foot-line">
<span class="foot-label">{{ dateLabel }}</span><span class="date">{{ dateStr }}</span>
</span>
<span v-if="showSource" class="foot-line">
<span class="foot-label">{{ sourceLabel }}</span><span class="watermark">{{ sourceNote }}</span>
</span>
<span v-if="qrDataUrl" class="qr-block">
<span v-if="qrHint" class="qr-hint">{{ qrHint }}</span>
<img :src="qrDataUrl" class="qr-img" alt="" />
</span>
</div>- [ ] Step 3: 加样式
在 <style scoped> 末尾追加:
.qr-block {
display: flex;
align-items: center;
gap: 8px;
margin-left: auto;
}
.qr-hint {
font-size: 12px;
line-height: 1.3;
max-width: 96px;
opacity: 0.75;
}
.qr-img {
width: 84px;
height: 84px;
border-radius: 6px;
background: #fff;
display: block;
}
@media (max-width: 380px) {
.qr-hint { display: none; }
.qr-img { width: 72px; height: 72px; }
}- [ ] Step 4: 构建确认不报错
Run: cd web && npm run build 2>&1 | tail -5 Expected: 构建成功(此时二维码还没有调用方传值,卡片应与改动前完全一致)。
- [ ] Step 5: 提交
git add web/components/tools/common/ResultShareCard.vue
git commit -m "feat(share): 结果卡片支持内嵌二维码(dataURL、黑白方码、纠错 M)"Task 4: 分享按钮、复制图片与 result_share 埋点
Files:
- Modify:
web/utils/events.ts(末尾追加) - Modify:
web/components/tools/common/AssessmentResult.vue(script 第 1–8 行 import 区、第 57–70 行saveImage、template 第 112–125 行按钮区、ResultShareCard的 props 绑定)
Interfaces:
Consumes: Task 1 的
buildShareUrl;Task 3 的qrUrl/qrHintpropsProduces:
reportResultShare(method: 'shared' | 'downloaded' | 'copied'): void[ ] Step 1: 在
utils/events.ts末尾追加分享上报
// 用户导出了一次结果卡片。
// ⚠️ method='shared' 的口径是「调起了系统分享面板」,不是「确实发出去了」:
// 用户在面板上点取消时浏览器抛 AbortError,saveOrShareNode 现有实现把它当作
// shared 返回,而 Web Share API 本身不告诉我们最终发给了谁,做不到更准。
// 读数时按上限理解;真实回流看 share_arrival。
export function reportResultShare(method: 'shared' | 'downloaded' | 'copied') {
const slug = currentToolSlug();
if (!slug) return;
safeTrack('result_share', { slug, method, locale: getCurrentLocale() });
}- [ ] Step 2: 在
AssessmentResult.vue引入依赖
第 1–8 行的 import 区改为:
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { useI18n } from 'vue-i18n'
import { saveOrShareNode, captureNodeToBlob, prewarmCapture } from '~/utils/captureNode'
import { triggerDownload } from '~/composables/useDownloadModal'
import { reportToolCalculate, reportResultShare } from '~/utils/events'
import { buildShareUrl } from '~/utils/shareLink'
import ResultShareCard from './ResultShareCard.vue'
import RecommendationList from './RecommendationList.vue'注:reportShareArrival 由 Task 2 的页面调用,不要在这里 import —— 上面这份 import 里刻意没有它。
- [ ] Step 3: 计算二维码 URL 与提示文案
在 const sourceLabel = computed(...) 之后追加:
// 当前工具页的 URL 本身就是它的 canonical,直接用可避免在组件里重推语言前缀。
// buildShareUrl 会剥掉所有既有参数,因此分数不可能被带进二维码。
const shareUrl = ref('')
const qrHint = computed(() => t('result_share_qr_hint'))
onMounted(() => {
shareUrl.value = buildShareUrl(window.location.origin + window.location.pathname)
})- [ ] Step 4: 分享成功后上报,并新增复制图片
第 57–70 行的 saveImage 替换为:
const imgCopied = ref(false)
async function saveImage() {
if (!cardRef.value || busy.value) return
busy.value = true
try {
const outcome = await saveOrShareNode(cardRef.value, 'moodji-result.png')
reportResultShare(outcome)
saved.value = true
setTimeout(() => { saved.value = false }, 2500)
} catch (error) {
console.error('Failed to generate share image:', error)
} finally {
busy.value = false
}
}
async function copyImage() {
if (!cardRef.value || busy.value) return
busy.value = true
try {
const blob = await captureNodeToBlob(cardRef.value)
if (!blob) throw new Error('empty blob')
// @ts-ignore - ClipboardItem 在旧 lib 类型里可能缺失
await navigator.clipboard.write([new ClipboardItem({ 'image/png': blob })])
reportResultShare('copied')
imgCopied.value = true
setTimeout(() => { imgCopied.value = false }, 2500)
} catch (error) {
console.error('Copy image failed, falling back to save:', error)
try {
const outcome = await saveOrShareNode(cardRef.value, 'moodji-result.png')
reportResultShare(outcome)
} catch (e2) {
console.error('Save fallback failed:', e2)
}
} finally {
busy.value = false
}
}- [ ] Step 5: 改按钮文案并加复制按钮
template 第 117–120 行的保存按钮替换为:
<button class="btn btn-save" :disabled="busy" @click="saveImage">
<Icon :name="saved ? 'mdi:check' : 'mdi:share-variant'" class="btn-icon" />
{{ saved ? t('result_saved') : t('result_share_image') }}
</button>
<button class="btn btn-save" :disabled="busy" @click="copyImage">
<Icon :name="imgCopied ? 'mdi:check' : 'mdi:content-copy'" class="btn-icon" />
{{ imgCopied ? t('result_copied') : t('result_copy_image') }}
</button>- [ ] Step 6: 把二维码传给卡片
在 template 里 <ResultShareCard 的属性列表末尾(:description="description" 之后的任意位置)追加:
:qr-url="shareUrl"
:qr-hint="qrHint"- [ ] Step 7: 构建确认
Run: cd web && npm run build 2>&1 | tail -5 Expected: 构建成功。
- [ ] Step 8: 提交
git add web/utils/events.ts web/components/tools/common/AssessmentResult.vue
git commit -m "feat(share): 分享结果按钮 + 复制图片 + result_share 埋点"Task 5: 五语言文案
Files:
- Modify:
web/i18n/lang/en.json、zh.json、zh-TW.json、ja.json、ko.json
Interfaces:
Consumes: Task 3/4 用到的 key
Produces:
result_share_image、result_copy_image、result_copied、result_share_qr_hint四个 key ×5 语言[ ] Step 1: 确认旧 key 的引用已消失
Run: cd web && grep -rn "result_save_image" components pages composables utils Expected: 无输出(Task 4 已替换掉唯一引用)。若仍有输出,先改完再继续。
- [ ] Step 2: 在五份语言文件里新增 key
在每份文件中 result_save_image 所在位置附近加入(JSON 无序,放在相邻位置便于维护):
en.json:
"result_share_image": "Share result",
"result_copy_image": "Copy image",
"result_copied": "Copied",
"result_share_qr_hint": "Scan to test yourself",zh.json:
"result_share_image": "分享结果",
"result_copy_image": "复制图片",
"result_copied": "已复制",
"result_share_qr_hint": "扫码测你的",zh-TW.json:
"result_share_image": "分享結果",
"result_copy_image": "複製圖片",
"result_copied": "已複製",
"result_share_qr_hint": "掃碼測你的",ja.json:
"result_share_image": "結果をシェア",
"result_copy_image": "画像をコピー",
"result_copied": "コピー済み",
"result_share_qr_hint": "スキャンして診断",ko.json:
"result_share_image": "결과 공유",
"result_copy_image": "이미지 복사",
"result_copied": "복사됨",
"result_share_qr_hint": "스캔해서 검사하기",- [ ] Step 3: 删除已无引用的旧 key
从五份文件中删除 result_save_image 这一行。
- [ ] Step 4: 校验 JSON 合法且五份 key 齐全
cd web && python3 -c "
import json
keys = ['result_share_image','result_copy_image','result_copied','result_share_qr_hint']
for L in ['en','zh','zh-TW','ja','ko']:
d = json.load(open(f'i18n/lang/{L}.json', encoding='utf-8'))
missing = [k for k in keys if k not in d]
assert not missing, (L, missing)
assert 'result_save_image' not in d, (L, 'old key still present')
print(L, 'ok')
"Expected: 五行 ok。
- [ ] Step 5: 提交
git add web/i18n/lang/
git commit -m "feat(share): 分享结果/复制图片/扫码提示文案(5 语言)"Task 6: 导出实测与上线验证
Files: 无代码改动(仅在发现问题时回到 Task 3/4 修)
Interfaces: 无
这一步不能省:html-to-image 是否把二维码画进图里,只能靠导出后肉眼看 —— 组件在页面上渲染正常不代表导出正常。
- [ ] Step 1: 本地起服务并导出一张图
cd web && npm run dev浏览器打开 http://localhost:3000/zh/tools/dass-21/,做完测评,点「分享结果」,保存下来的 PNG 用看图工具打开。
Expected:二维码完整出现在卡片右下角,边缘清晰不糊。 若二维码缺失 → 回到 Task 3 检查是否用了 dataURL(外链图会被丢弃)。
- [ ] Step 2: 用手机扫导出的图
Expected:落地到 …/zh/tools/dass-21/?s=qr,页面正常打开。 若扫不出 → 回到 Task 3 把 .qr-img 尺寸从 84px 调到 96px 后重测。
- [ ] Step 3: 真机在微信里长按识别
把图片发到微信,长按图片选「识别图中二维码」。 Expected:能识别并跳转。 若识别失败 → 确认用的是 qrcode 而非 qr-code-styling。
- [ ] Step 4: 窄屏版式检查
浏览器开发者工具切到 375px 宽,查看结果卡片。 Expected:卡片不横向溢出,二维码不被裁切(≤380px 时提示文案自动隐藏)。
- [ ] Step 5: 五语言各导出一次
逐个打开 /tools/、/zh/tools/、/zh-TW/tools/、/ja/tools/、/ko/tools/ 下的 dass-21,各导出一张。 Expected:无 i18n key 原样显示(如出现 result_share_qr_hint 字样即为漏配),文案不溢出卡片。
- [ ] Step 6: 非心理类工具回归抽查
打开 /tools/bmi-calculator/ 与 /ja/tools/ffmi-calculator/,各导出一张。 Expected:卡片与导出均正常,二维码指向各自的工具页。
- [ ] Step 7: 埋点验证(必须在预览环境,本地 dev 不发)
utils/events.ts 的 safeTrack 在 isDevelopmentMode() 下直接 return,所以本地点按钮不会产生任何事件。推送到分支拿 Vercel 预览 URL,在预览环境上:分享一次、复制一次、再用二维码扫一次落地。
隔日在 Vercel Analytics 用事件维度核对(筛选格式见 docs/afit/ops-website.md): Expected:result_share 出现 method=shared(或 downloaded)与 method=copied;share_arrival ≥1。
- [ ] Step 8: 合并上线并记录
git push origin main部署完成后用线上 URL 复验一次导出(仅信构建绿不够),并在 docs/afit/ops-website.md 的跟踪日志追加一条:上线日期、预测 #(30 天内 share_arrival ≥ 30,验收日 2026-10-01)。
Self-Review
1. 规格覆盖
| 规格章节 | 对应任务 |
|---|---|
| §5.1 二维码(位置/dataURL/黑白方码/≥72px) | Task 3 |
§5.2 URL 只带 s=qr、不带分数 | Task 1(纯函数强制)+ Task 4(调用) |
§5.3 result_share / share_arrival 两个事件 + 去重 | Task 2、Task 4 |
| §5.4 按钮文案 + 复制图片 + 删旧 key | Task 4、Task 5 |
| §5.5 共用组件影响 20 个工具 | Task 3/4 天然覆盖;Task 6 Step 6 做回归抽查 |
| §6 验证表 7 项 | Task 6 的 7 个 Step 一一对应 |
| §7 预测与验收 | Task 6 Step 8 记录 |
2. 占位符扫描:无 TBD/TODO;每个代码步骤都给了完整可粘贴的代码;每个验证步骤都给了命令与预期。
3. 类型一致性:buildShareUrl / isShareArrival / reportShareArrival / reportResultShare 的名称与签名在定义处与使用处一致;saveOrShareNode 返回的 'shared' | 'downloaded' 与 reportResultShare 的入参联合类型对齐,'copied' 仅由复制路径传入。