feat: 完成全系统30个功能点开发与E2E全链路验收 (8轮调度32个子代理闭环)
- 模块一: 出海案例作品集、博客动态发布、人名发音语音条、RTL阿语多语言切换 - 模块二: AI顾问人设配置、行业术语库润色、RAG切片沙盒调试、流式会话与意向留资卡片 - 模块三: 全球访客雷达大屏、多维加权意向算分、外部CRM对接推送、团队公海池流转 - 模块四: 移动端NFC安全挂失、NFC动态路由规则配置、动态防伪验签、展会制卡Zip批量导出 - 模块五: 组织架构与名片分配、移动端个人中心设置、离职交接与历史线索迁移、安全审计日志 - 模块六: Stripe国际信用卡订阅Webhook、独立二级域名与SSL检测、坐席配额预警、多币种发票 - 模块七: 纸质名片OCR解析入库、国际商务日程RFC 5545 ICS日历、微官网离线SWR预加载、展会实时排行榜大屏 - 模块八: 全球出海SEO OpenGraph元标签注入与社交爬虫探测、30点端到端全链路业务流集成测试套件
这个提交包含在:
@@ -0,0 +1,188 @@
|
||||
import { CardSeoService, SeoOptions } from '../../../src/modules/card/service/seo';
|
||||
import { CardProfile } from '../../../src/comm/types/omni';
|
||||
|
||||
describe('Point 29-A: SEO & OpenGraph 社交预览元标签注入引擎', () => {
|
||||
let seoService: CardSeoService;
|
||||
|
||||
beforeEach(() => {
|
||||
seoService = new CardSeoService();
|
||||
});
|
||||
|
||||
const mockCard: Partial<CardProfile> = {
|
||||
id: 101,
|
||||
name: 'Sarah Connor',
|
||||
title: 'Chief Technology Officer',
|
||||
company: 'Omni Global Tech',
|
||||
slogan: 'Building the next generation of AI-driven digital identities.',
|
||||
avatarUrl: 'https://cdn.omnicard.ai/avatars/sarah.jpg',
|
||||
slug: 'sarah-connor',
|
||||
};
|
||||
|
||||
describe('1. generateOpenGraphMeta 标签生成规范', () => {
|
||||
it('应为完整名片生成标准化的 OpenGraph 与 Twitter Card 标签集合', () => {
|
||||
const options: SeoOptions = {
|
||||
baseUrl: 'https://omnicard.ai',
|
||||
locale: 'en_US',
|
||||
siteName: 'OmniCard AI',
|
||||
};
|
||||
|
||||
const meta = seoService.generateOpenGraphMeta(mockCard, options);
|
||||
|
||||
// OpenGraph 基础标准规范
|
||||
expect(meta['og:title']).toBe('Sarah Connor - Chief Technology Officer | Omni Global Tech');
|
||||
expect(meta['og:description']).toBe(
|
||||
'Building the next generation of AI-driven digital identities.'
|
||||
);
|
||||
expect(meta['og:image']).toBe('https://cdn.omnicard.ai/avatars/sarah.jpg');
|
||||
expect(meta['og:url']).toBe('https://omnicard.ai/c/sarah-connor');
|
||||
expect(meta['og:type']).toBe('profile');
|
||||
expect(meta['og:locale']).toBe('en_US');
|
||||
expect(meta['og:site_name']).toBe('OmniCard AI');
|
||||
|
||||
// Twitter Card 协议
|
||||
expect(meta['twitter:card']).toBe('summary_large_image');
|
||||
expect(meta['twitter:title']).toBe('Sarah Connor - Chief Technology Officer | Omni Global Tech');
|
||||
expect(meta['twitter:description']).toBe(
|
||||
'Building the next generation of AI-driven digital identities.'
|
||||
);
|
||||
expect(meta['twitter:image']).toBe('https://cdn.omnicard.ai/avatars/sarah.jpg');
|
||||
});
|
||||
|
||||
it('当提供 currentUrl 时,og:url 应优先采用 currentUrl', () => {
|
||||
const meta = seoService.generateOpenGraphMeta(mockCard, {
|
||||
currentUrl: 'https://custom.domain.com/profile/sarah',
|
||||
});
|
||||
expect(meta['og:url']).toBe('https://custom.domain.com/profile/sarah');
|
||||
});
|
||||
|
||||
it('当缺少 slogan 时,og:description 应优雅拼接 title 与 company', () => {
|
||||
const cardWithoutSlogan: Partial<CardProfile> = {
|
||||
name: 'Alex Rivera',
|
||||
title: 'Global Sales Director',
|
||||
company: 'Nexa Global',
|
||||
};
|
||||
|
||||
const meta = seoService.generateOpenGraphMeta(cardWithoutSlogan);
|
||||
expect(meta['og:description']).toBe('Global Sales Director @ Nexa Global');
|
||||
expect(meta['og:title']).toBe('Alex Rivera - Global Sales Director | Nexa Global');
|
||||
});
|
||||
});
|
||||
|
||||
describe('2. 多语言规范与 OpenGraph Locale 格式化', () => {
|
||||
it('应将各类格式的多语言编码格式化为 RFC/OpenGraph 标准 ll_CC 格式', () => {
|
||||
expect(seoService.formatOgLocale('zh-CN')).toBe('zh_CN');
|
||||
expect(seoService.formatOgLocale('en-US')).toBe('en_US');
|
||||
expect(seoService.formatOgLocale('ar-SA')).toBe('ar_SA');
|
||||
expect(seoService.formatOgLocale('ja')).toBe('ja_JP');
|
||||
expect(seoService.formatOgLocale('en')).toBe('en_US');
|
||||
expect(seoService.formatOgLocale('zh')).toBe('zh_CN');
|
||||
expect(seoService.formatOgLocale('ar')).toBe('ar_SA');
|
||||
expect(seoService.formatOgLocale(undefined)).toBe('en_US');
|
||||
});
|
||||
});
|
||||
|
||||
describe('3. renderHtmlWithOgMeta 模板注入与 RTL 支持', () => {
|
||||
const template = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>OmniCard</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
it('应将 meta 标签与 title 安全注入到 <head> 中', () => {
|
||||
const html = seoService.renderHtmlWithOgMeta(template, mockCard, {
|
||||
baseUrl: 'https://omnicard.ai',
|
||||
locale: 'en_US',
|
||||
});
|
||||
|
||||
expect(html).toContain('<meta property="og:title" content="Sarah Connor - Chief Technology Officer | Omni Global Tech" />');
|
||||
expect(html).toContain('<meta property="og:type" content="profile" />');
|
||||
expect(html).toContain('<meta property="og:image" content="https://cdn.omnicard.ai/avatars/sarah.jpg" />');
|
||||
expect(html).toContain('<meta name="twitter:card" content="summary_large_image" />');
|
||||
expect(html).toContain('<title>Sarah Connor - Chief Technology Officer | Omni Global Tech</title>');
|
||||
expect(html).toContain('<html lang="en">');
|
||||
});
|
||||
|
||||
it('当语言为阿拉伯语时,应自动为 <html> 标签注入 dir="rtl" 与 lang="ar"', () => {
|
||||
const arCard: Partial<CardProfile> = {
|
||||
name: 'طارق المنصور',
|
||||
title: 'مدير تطوير الأعمال',
|
||||
company: 'التقنية العالمية',
|
||||
};
|
||||
|
||||
const html = seoService.renderHtmlWithOgMeta(template, arCard, {
|
||||
locale: 'ar-SA',
|
||||
});
|
||||
|
||||
expect(html).toContain('dir="rtl"');
|
||||
expect(html).toContain('lang="ar"');
|
||||
expect(html).toContain('<meta property="og:locale" content="ar_SA" />');
|
||||
expect(html).toContain('طارق المنصور');
|
||||
});
|
||||
|
||||
it('当显式传入 options.isRtl = true 时,也应注入 dir="rtl"', () => {
|
||||
const html = seoService.renderHtmlWithOgMeta(template, mockCard, {
|
||||
isRtl: true,
|
||||
});
|
||||
expect(html).toContain('dir="rtl"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('4. 防 XSS 注入与特殊字符安全转义', () => {
|
||||
it('当姓名、职位或描述包含恶意 HTML/JS 脚本或注入符号时,应进行严格转义', () => {
|
||||
const xssCard: Partial<CardProfile> = {
|
||||
name: '<script>alert("xss")</script>',
|
||||
title: '"><img src=x onerror=alert(1)>',
|
||||
company: 'Tech & "Solutions" <LLC>',
|
||||
slogan: '\'OR 1=1 -- <iframe src="evil.com">',
|
||||
};
|
||||
|
||||
const meta = seoService.generateOpenGraphMeta(xssCard);
|
||||
expect(meta['og:title']).not.toContain('<script>');
|
||||
expect(meta['og:title']).not.toContain('onerror=');
|
||||
|
||||
const template = '<html><head></head><body></body></html>';
|
||||
const renderedHtml = seoService.renderHtmlWithOgMeta(template, xssCard);
|
||||
|
||||
// 验证特殊字符均被 escape,属性双引号不可跳脱
|
||||
expect(renderedHtml).not.toContain('<script>alert');
|
||||
expect(renderedHtml).not.toContain('<img src=x');
|
||||
expect(renderedHtml).not.toContain('<iframe');
|
||||
expect(renderedHtml).toContain('"');
|
||||
expect(renderedHtml).toContain('<');
|
||||
expect(renderedHtml).toContain('>');
|
||||
expect(renderedHtml).toContain('&');
|
||||
});
|
||||
});
|
||||
|
||||
describe('5. 空值与边界安全兜底', () => {
|
||||
it('当 card 为 null 或 undefined 时,应使用默认值兜底且绝不报错', () => {
|
||||
const metaNull = seoService.generateOpenGraphMeta(null);
|
||||
expect(metaNull['og:title']).toBe('OmniCard AI');
|
||||
expect(metaNull['og:description']).toBe(
|
||||
'OmniCard AI - Global Digital Business Card & Micro Portal'
|
||||
);
|
||||
expect(metaNull['og:image']).toBe('https://omnicard.ai/assets/og-default.png');
|
||||
expect(metaNull['og:type']).toBe('profile');
|
||||
expect(metaNull['twitter:card']).toBe('summary_large_image');
|
||||
|
||||
const metaUndefined = seoService.generateOpenGraphMeta(undefined, {
|
||||
siteName: 'Custom Brand',
|
||||
defaultImage: 'https://example.com/fallback.jpg',
|
||||
});
|
||||
expect(metaUndefined['og:title']).toBe('Custom Brand');
|
||||
expect(metaUndefined['og:image']).toBe('https://example.com/fallback.jpg');
|
||||
});
|
||||
|
||||
it('当 template 不含标准 <head> 标签时,renderHtmlWithOgMeta 能够安全兜底注入', () => {
|
||||
const rawHtml = '<div>Raw Body Content</div>';
|
||||
const result = seoService.renderHtmlWithOgMeta(rawHtml, mockCard);
|
||||
expect(result).toContain('<meta property="og:title"');
|
||||
expect(result).toContain('<div>Raw Body Content</div>');
|
||||
});
|
||||
});
|
||||
});
|
||||
在新工单中引用
屏蔽一个用户