Files
memind/plaza-seo.mjs
T
John 2e14873f2d Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:04:43 -07:00

151 lines
4.6 KiB
JavaScript

import crypto from 'node:crypto';
function hashIp(ip) {
return crypto.createHash('sha256').update(String(ip ?? 'unknown')).digest('hex').slice(0, 32);
}
function normalizeUtm(value, maxLen = 100) {
return String(value ?? '')
.trim()
.slice(0, maxLen);
}
function normalizeEventType(value) {
const type = String(value ?? '').trim();
if (type === 'landing' || type === 'signup') return type;
throw Object.assign(new Error('不支持的 attribution 事件类型'), { code: 'invalid_input' });
}
export const seoInternals = {
normalizeUtm,
normalizeEventType,
buildPostPublicUrl,
};
export function buildPostPublicUrl(postId, siteBase = process.env.PLAZA_PUBLIC_BASE ?? 'https://plaza.tkmind.cn') {
const base = String(siteBase).replace(/\/$/, '');
return `${base}/plaza/p/${postId}`;
}
export function createPlazaSeoService(
pool,
{
idFactory = () => crypto.randomUUID(),
siteBase = process.env.PLAZA_PUBLIC_BASE ?? 'https://plaza.tkmind.cn',
baiduToken = process.env.PLAZA_BAIDU_PUSH_TOKEN ?? '',
baiduSite = process.env.PLAZA_BAIDU_SITE ?? 'plaza.tkmind.cn',
} = {},
) {
const listSitemapData = async ({ postLimit = 1000, userLimit = 500 } = {}) => {
const [categoryRows] = await pool.query(
`SELECT slug, name FROM plaza_categories WHERE is_active = 1 ORDER BY sort_order ASC`,
);
const [postRows] = await pool.query(
`SELECT id, updated_at, published_at
FROM plaza_posts
WHERE status = 'published'
ORDER BY published_at DESC
LIMIT ?`,
[Math.min(Math.max(Number(postLimit) || 1000, 1), 5000)],
);
const [userRows] = await pool.query(
`SELECT u.slug, u.username, MAX(pp.published_at) AS last_post_at
FROM h5_users u
JOIN plaza_posts pp ON pp.user_id = u.id AND pp.status = 'published'
GROUP BY u.id, u.slug, u.username
ORDER BY last_post_at DESC
LIMIT ?`,
[Math.min(Math.max(Number(userLimit) || 500, 1), 2000)],
);
return {
categories: categoryRows.map((row) => ({
slug: row.slug,
name: row.name,
})),
posts: postRows.map((row) => ({
id: row.id,
updated_at: new Date(Number(row.updated_at ?? row.published_at)).toISOString(),
published_at: new Date(Number(row.published_at)).toISOString(),
})),
users: userRows.map((row) => ({
slug: row.slug || row.username,
last_post_at: row.last_post_at
? new Date(Number(row.last_post_at)).toISOString()
: new Date().toISOString(),
})),
};
};
const recordAttribution = async (
{
event_type: eventTypeInput,
utm_source: utmSource,
utm_medium: utmMedium,
utm_campaign: utmCampaign,
ref_id: refId,
user_id: userId = null,
},
ip = 'unknown',
) => {
const eventType = normalizeEventType(eventTypeInput);
const source = normalizeUtm(utmSource);
if (!source) throw Object.assign(new Error('utm_source 不能为空'), { code: 'invalid_input' });
const id = idFactory();
const now = Date.now();
await pool.query(
`INSERT INTO plaza_attribution_events
(id, event_type, utm_source, utm_medium, utm_campaign, ref_id, user_id, ip_hash, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
id,
eventType,
source,
normalizeUtm(utmMedium),
normalizeUtm(utmCampaign),
normalizeUtm(refId, 200),
userId,
hashIp(ip),
now,
],
);
return { id, event_type: eventType };
};
const pingBaidu = async (urls) => {
const list = Array.isArray(urls) ? urls.filter(Boolean) : [urls].filter(Boolean);
if (!baiduToken || list.length === 0) {
return { pushed: false, reason: 'disabled_or_empty' };
}
const endpoint = `http://data.zz.baidu.com/urls?site=${encodeURIComponent(baiduSite)}&token=${encodeURIComponent(baiduToken)}`;
const response = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'text/plain' },
body: list.join('\n'),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw Object.assign(new Error('百度推送失败'), { code: 'baidu_push_failed', details: payload });
}
return { pushed: true, result: payload };
};
const notifyPostPublished = async (postId) => {
const url = buildPostPublicUrl(postId, siteBase);
try {
return await pingBaidu([url]);
} catch (error) {
console.error('Plaza Baidu push failed:', error.message);
return { pushed: false, reason: error.message };
}
};
return {
listSitemapData,
recordAttribution,
pingBaidu,
notifyPostPublished,
};
}