feat(ECR-012): 星座对齐收口,并 Closed ECR-007/008

对齐 outlook/分享 type=star/报告页运势面板与测试;流程上关闭 Ops-B/C 两张 ECR。真支付仍后置。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-07 15:48:19 +08:00
co-authored by Cursor
parent 7ab9add5dd
commit d33c8fdfe9
42 changed files with 658 additions and 129 deletions
+24 -1
View File
@@ -135,7 +135,27 @@ export function signCardsFrom(summary: Record<string, unknown>): SignCard[] {
export function fortuneBundleFrom(summary: Record<string, unknown>): Record<string, FortunePeriod> {
const o = summary.outlook
return o && typeof o === 'object' ? (o as Record<string, FortunePeriod>) : {}
if (!o || typeof o !== 'object') return {}
const src = o as Record<string, unknown>
const out: Record<string, FortunePeriod> = {}
for (const k of reportFortuneKeys) {
const raw = src[k]
if (raw && typeof raw === 'object') out[k] = periodFromRaw(raw as Record<string, unknown>)
}
return out
}
function periodFromRaw(p: Record<string, unknown>): FortunePeriod {
return {
title: typeof p.title === 'string' ? p.title : undefined,
label: typeof p.label === 'string' ? p.label : undefined,
score: typeof p.score === 'number' ? p.score : undefined,
tip: typeof p.tip === 'string' ? p.tip : undefined,
focus: typeof p.focus === 'string' ? p.focus : undefined,
boost: typeof p.boost === 'string' ? p.boost : undefined,
caution: typeof p.caution === 'string' ? p.caution : undefined,
dims: p.dims && typeof p.dims === 'object' ? (p.dims as FortunePeriod['dims']) : undefined,
}
}
export function planetsFrom(summary: Record<string, unknown>): PlanetRow[] {
@@ -172,6 +192,9 @@ export function buildReportSharePayload(
keywords,
} as RelationSharePayload
}
if (report.type === 'star') {
return { type: 'star', title: headline, line: oneLiner, keywords }
}
return {
type: 'portrait',
title: headline,
+17
View File
@@ -39,8 +39,25 @@ describe('shareLink', () => {
})
})
it('round-trips star payload', () => {
const path = buildSharePath({
type: 'star',
title: '稳健沉淀者',
line: '星座一句',
keywords: ['金牛'],
})
const q = Object.fromEntries(new URLSearchParams(path.slice(path.indexOf('?'))))
expect(parseShareQuery(q)).toEqual({
type: 'star',
title: '稳健沉淀者',
line: '星座一句',
keywords: ['金牛'],
})
})
it('returns null for empty/invalid', () => {
expect(parseShareQuery({})).toBeNull()
expect(parseShareQuery({ type: 'portrait' })).toBeNull()
expect(parseShareQuery({ type: 'star' })).toBeNull()
})
})
+22 -7
View File
@@ -7,6 +7,14 @@ export type PortraitSharePayload = {
keywords: string[]
}
/** Same card shape as portrait; type discriminates 星座 landing copy. */
export type StarSharePayload = {
type: 'star'
title: string
line: string
keywords: string[]
}
export type RelationSharePayload = {
type: 'relation'
me: string
@@ -15,15 +23,22 @@ export type RelationSharePayload = {
keywords: string[]
}
export type SharePayload = PortraitSharePayload | RelationSharePayload
export type SharePayload = PortraitSharePayload | StarSharePayload | RelationSharePayload
function appendTitleLineKeywords(
q: URLSearchParams,
payload: PortraitSharePayload | StarSharePayload,
) {
if (payload.title) q.set('t', payload.title)
if (payload.line) q.set('l', payload.line)
if (payload.keywords.length) q.set('k', payload.keywords.slice(0, 6).join(','))
}
export function buildSharePath(payload: SharePayload): string {
const q = new URLSearchParams()
q.set('type', payload.type)
if (payload.type === 'portrait') {
if (payload.title) q.set('t', payload.title)
if (payload.line) q.set('l', payload.line)
if (payload.keywords.length) q.set('k', payload.keywords.slice(0, 6).join(','))
if (payload.type === 'portrait' || payload.type === 'star') {
appendTitleLineKeywords(q, payload)
} else {
if (payload.me) q.set('me', payload.me)
if (payload.other) q.set('ot', payload.other)
@@ -39,11 +54,11 @@ export function parseShareQuery(query: Record<string, unknown>): SharePayload |
.split(',')
.map((s) => s.trim())
.filter(Boolean)
if (type === 'portrait') {
if (type === 'portrait' || type === 'star') {
const title = String(query.t || '')
const line = String(query.l || '')
if (!title && !line && !keywords.length) return null
return { type: 'portrait', title, line, keywords }
return { type: type as 'portrait' | 'star', title, line, keywords }
}
if (type === 'relation') {
const me = String(query.me || '')