- 新增 /api/hashtag/related:从作品描述实时解析 hashtag,按「同一作品里一起出现」计算相关 tag - 打分取子话题(候选多与目标同现)与上位话题(目标多与热门候选同现)两者较大值, 过滤 cos、二次元等万金油 tag 与平台活动、纯数字 tag - 播放页侧栏的 hashtag 列表顶部显示相关 tag,点击在侧栏内预览(不打断当前播放), 在预览里点作品才切换到该 tag 的列表 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
68 lines
2.5 KiB
TypeScript
68 lines
2.5 KiB
TypeScript
// 从作品描述里解析抖音 hashtag(#xxx)。
|
||
// 前端渲染可点击的 hashtag 与后端按 hashtag 精确筛选作品共用这里的边界规则,
|
||
// 保证「点了 #丰川祥子 看到的列表」不会混进 #丰川祥子cos。
|
||
|
||
// hashtag 在遇到空白、下一个 #、或这些标点时结束
|
||
const STOP_CHARS =
|
||
"#,。!?、;:,.!?;:()()【】\\[\\]《》<>\"'“”‘’…~~|/\\\\@";
|
||
const MAX_HASHTAG_LENGTH = 50;
|
||
|
||
const HASHTAG_RE = new RegExp(`#([^\\s${STOP_CHARS}]+)`, "gu");
|
||
|
||
export type DescPart =
|
||
| { type: "text"; text: string }
|
||
| { type: "hashtag"; text: string; name: string };
|
||
|
||
/** 把描述切成普通文字与 hashtag 片段。 */
|
||
export function parseDescHashtags(desc: string): DescPart[] {
|
||
const parts: DescPart[] = [];
|
||
let cursor = 0;
|
||
for (const m of desc.matchAll(HASHTAG_RE)) {
|
||
const start = m.index ?? 0;
|
||
if (start > cursor) {
|
||
parts.push({ type: "text", text: desc.slice(cursor, start) });
|
||
}
|
||
parts.push({ type: "hashtag", text: m[0], name: m[1] });
|
||
cursor = start + m[0].length;
|
||
}
|
||
if (cursor < desc.length) {
|
||
parts.push({ type: "text", text: desc.slice(cursor) });
|
||
}
|
||
return parts;
|
||
}
|
||
|
||
/** 规范化 hashtag 名(去掉开头的 #、首尾空白,限制长度);不合法时返回 null。 */
|
||
export function normalizeHashtag(raw: string): string | null {
|
||
const name = raw.trim().replace(/^#+/, "").slice(0, MAX_HASHTAG_LENGTH);
|
||
if (!name) return null;
|
||
if (new RegExp(`[\\s${STOP_CHARS}]`, "u").test(name)) return null;
|
||
return name;
|
||
}
|
||
|
||
/**
|
||
* 匹配 lower() 后描述文本的 Postgres 正则(ARE):#名字 之后必须是结束符或文本末尾。
|
||
* 名字里的正则特殊字符会被转义。
|
||
*/
|
||
export function hashtagToPgPattern(name: string): string {
|
||
const escaped = name.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||
return `#${escaped}([\\s${STOP_CHARS}]|$)`;
|
||
}
|
||
|
||
/** 从描述里提取全部 hashtag 名的 Postgres 正则(ARE),配合 regexp_matches(..., 'g') 使用 */
|
||
export const HASHTAG_EXTRACT_PG = `#([^\\s${STOP_CHARS}]+)`;
|
||
|
||
// 「万金油」tag:几乎和什么都一起出现,不提供话题信息,不作为相关 tag 推荐
|
||
export const META_HASHTAGS = [
|
||
"cos",
|
||
"cosplay",
|
||
"coser",
|
||
"二次元",
|
||
"ooc致歉",
|
||
"搞笑",
|
||
"抽象",
|
||
"vlog日常",
|
||
"内容过于真实",
|
||
];
|
||
/** 平台活动类 tag(xx计划、xx挑战赛…)和纯数字 tag 的 Postgres 正则 */
|
||
export const META_HASHTAG_PG = "(计划|挑战赛|大赛|扶持|热门|浪潮)$|^[0-9]+$";
|