fix(chat): 发送后切会话不再误标未读,管理端撤回实时同步到对话页
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
55
frontend/lib/linkify.ts
Normal file
55
frontend/lib/linkify.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 纯文本 URL 切分(QQ 风格:识别 http(s):// 与 www.)
|
||||
* 仅产出 http/https,拒绝其它协议。
|
||||
*/
|
||||
|
||||
const URL_RE = /(?:https?:\/\/|www\.)[^\s\u4e00-\u9fff<>"'`]+/gi;
|
||||
|
||||
/** 常见尾随标点从 URL 剥离(中英) */
|
||||
const TRAIL_PUNCT = /[.,;:!?。,;:!?、)\]}》〉』」"'`]+$/;
|
||||
|
||||
export type LinkifyPart =
|
||||
| { type: "text"; value: string }
|
||||
| { type: "url"; value: string; href: string };
|
||||
|
||||
function toHref(raw: string): string | null {
|
||||
const href = /^www\./i.test(raw) ? `https://${raw}` : raw;
|
||||
try {
|
||||
const u = new URL(href);
|
||||
if (u.protocol !== "http:" && u.protocol !== "https:") return null;
|
||||
return u.href;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function peel(raw: string): { core: string; trail: string } {
|
||||
const m = raw.match(TRAIL_PUNCT);
|
||||
if (!m) return { core: raw, trail: "" };
|
||||
return { core: raw.slice(0, -m[0].length), trail: m[0] };
|
||||
}
|
||||
|
||||
/** 将纯文本切成文本段与可安全打开的链接段 */
|
||||
export function linkifyParts(text: string): LinkifyPart[] {
|
||||
if (!text) return [];
|
||||
const parts: LinkifyPart[] = [];
|
||||
let last = 0;
|
||||
URL_RE.lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = URL_RE.exec(text)) !== null) {
|
||||
if (m.index > last) {
|
||||
parts.push({ type: "text", value: text.slice(last, m.index) });
|
||||
}
|
||||
const { core, trail } = peel(m[0]);
|
||||
const href = core ? toHref(core) : null;
|
||||
if (href && core) {
|
||||
parts.push({ type: "url", value: core, href });
|
||||
if (trail) parts.push({ type: "text", value: trail });
|
||||
} else {
|
||||
parts.push({ type: "text", value: m[0] });
|
||||
}
|
||||
last = m.index + m[0].length;
|
||||
}
|
||||
if (last < text.length) parts.push({ type: "text", value: text.slice(last) });
|
||||
return parts.length ? parts : [{ type: "text", value: text }];
|
||||
}
|
||||
Reference in New Issue
Block a user