Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 880cd59ca5 | |||
| adc13a167e |
+58
-1
@@ -444,6 +444,9 @@ const PUBLIC_HTML_LINK_PATTERN =
|
|||||||
/https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/([^\s<>"')\]]+\.html)/gi;
|
/https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/([^\s<>"')\]]+\.html)/gi;
|
||||||
const PUBLIC_HTML_MARKDOWN_LINK_PATTERN =
|
const PUBLIC_HTML_MARKDOWN_LINK_PATTERN =
|
||||||
/\[([^\]\n]*)\]\((https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/([^\s<>"')\]]+\.html))\)/gi;
|
/\[([^\]\n]*)\]\((https?:\/\/[^\s<>"')\]]+\/MindSpace\/([0-9a-f-]{36}|[a-z0-9._-]+)\/([^\s<>"')\]]+\.html))\)/gi;
|
||||||
|
const MARKDOWN_LINK_PATTERN = /\[([^\]\n]*)\]\((https?:\/\/[^\s<>"')\]]+)\)/g;
|
||||||
|
const PLAIN_HTML_URL_PATTERN = /https?:\/\/[^\s<>"')\]]+\.html/gi;
|
||||||
|
const INLINE_PUBLIC_HTML_PATH_PATTERN = /`((?:\.\/)?public\/[^`\s<>"')\]]+\.html)`/gi;
|
||||||
const USER_IDENTITY_BLOCK_PATTERN = /^\[用户身份\][\s\S]*?(?:\n{2,}|$)/;
|
const USER_IDENTITY_BLOCK_PATTERN = /^\[用户身份\][\s\S]*?(?:\n{2,}|$)/;
|
||||||
const IMAGE_URL_LINES_PATTERN = /\n*\[图片\d+]: [^\n]+/g;
|
const IMAGE_URL_LINES_PATTERN = /\n*\[图片\d+]: [^\n]+/g;
|
||||||
const TKMIND_VISION_NOTE_PATTERN = /\n*【TKMind 图片分析结果[\s\S]*$/;
|
const TKMIND_VISION_NOTE_PATTERN = /\n*【TKMind 图片分析结果[\s\S]*$/;
|
||||||
@@ -515,6 +518,48 @@ function sanitizeOwnPublicHtmlUrl(publicUrl, owner, rawRelativePath, currentUser
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCurrentUserPublicOwner(owner, currentUser) {
|
||||||
|
const normalizedOwner = String(owner ?? '').trim().toLowerCase();
|
||||||
|
const normalizedUserId = String(currentUser?.id ?? '').trim().toLowerCase();
|
||||||
|
const normalizedUsername = String(currentUser?.username ?? '').trim().toLowerCase();
|
||||||
|
return Boolean(normalizedOwner && (
|
||||||
|
normalizedOwner === normalizedUserId ||
|
||||||
|
normalizedOwner === normalizedUsername
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeMissingMindSpacePublicHtmlUrl(url, currentUser) {
|
||||||
|
let parsed;
|
||||||
|
try {
|
||||||
|
parsed = new URL(url);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const parts = parsed.pathname
|
||||||
|
.split('/')
|
||||||
|
.filter(Boolean)
|
||||||
|
.map((part) => decodePathSegment(part));
|
||||||
|
if (parts[0]?.toLowerCase() === 'mindspace') return null;
|
||||||
|
const owner = parts[0];
|
||||||
|
if (!isCurrentUserPublicOwner(owner, currentUser)) return null;
|
||||||
|
const rawRelativePath = parts.slice(1).join('/');
|
||||||
|
if (!rawRelativePath.toLowerCase().endsWith('.html')) return null;
|
||||||
|
const result = sanitizeOwnPublicHtmlUrl(url, owner, rawRelativePath, currentUser);
|
||||||
|
return result.ok ? result.url : result.notice;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeOwnPublicHtmlRelativePath(rawRelativePath, currentUser) {
|
||||||
|
const owner = currentUser?.id || currentUser?.username;
|
||||||
|
if (!owner) return null;
|
||||||
|
const normalizedRelativePath = normalizeStaticHtmlRelativePath(rawRelativePath);
|
||||||
|
if (!normalizedRelativePath.toLowerCase().endsWith('.html')) return null;
|
||||||
|
if (!publicHtmlExistsForUser(owner, normalizedRelativePath, currentUser)) return null;
|
||||||
|
return {
|
||||||
|
label: path.posix.basename(normalizedRelativePath),
|
||||||
|
url: buildPublicUrl(resolvePublicBaseUrl(), owner, normalizedRelativePath),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function sanitizePublicHtmlLinksInText(text, currentUser) {
|
export function sanitizePublicHtmlLinksInText(text, currentUser) {
|
||||||
let next = String(text ?? '').replace(
|
let next = String(text ?? '').replace(
|
||||||
PUBLIC_HTML_MARKDOWN_LINK_PATTERN,
|
PUBLIC_HTML_MARKDOWN_LINK_PATTERN,
|
||||||
@@ -524,10 +569,22 @@ export function sanitizePublicHtmlLinksInText(text, currentUser) {
|
|||||||
return label ? `[${label}](${result.url})` : result.url;
|
return label ? `[${label}](${result.url})` : result.url;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
return next.replace(PUBLIC_HTML_LINK_PATTERN, (match, owner, rawRelativePath) => {
|
next = next.replace(PUBLIC_HTML_LINK_PATTERN, (match, owner, rawRelativePath) => {
|
||||||
const result = sanitizeOwnPublicHtmlUrl(match, owner, rawRelativePath, currentUser);
|
const result = sanitizeOwnPublicHtmlUrl(match, owner, rawRelativePath, currentUser);
|
||||||
return result.ok ? result.url : result.notice;
|
return result.ok ? result.url : result.notice;
|
||||||
});
|
});
|
||||||
|
next = next.replace(MARKDOWN_LINK_PATTERN, (match, label, url) => {
|
||||||
|
const sanitizedUrl = sanitizeMissingMindSpacePublicHtmlUrl(url, currentUser);
|
||||||
|
if (!sanitizedUrl) return match;
|
||||||
|
return label ? `[${label}](${sanitizedUrl})` : sanitizedUrl;
|
||||||
|
});
|
||||||
|
next = next.replace(PLAIN_HTML_URL_PATTERN, (match) => (
|
||||||
|
sanitizeMissingMindSpacePublicHtmlUrl(match, currentUser) ?? match
|
||||||
|
));
|
||||||
|
return next.replace(INLINE_PUBLIC_HTML_PATH_PATTERN, (match, rawRelativePath) => {
|
||||||
|
const result = sanitizeOwnPublicHtmlRelativePath(rawRelativePath, currentUser);
|
||||||
|
return result ? `[${result.label}](${result.url})` : match;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeUserVisibleMessageText(text, currentUser) {
|
export function sanitizeUserVisibleMessageText(text, currentUser) {
|
||||||
|
|||||||
@@ -216,6 +216,59 @@ test('sanitizePublicHtmlLinksInText canonicalizes wrong MindSpace public hosts',
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('sanitizePublicHtmlLinksInText canonicalizes public html links missing MindSpace segment', () => {
|
||||||
|
const owner = `test-user-${Date.now()}-missing-mindspace`;
|
||||||
|
const previousBase = process.env.H5_PUBLIC_BASE_URL;
|
||||||
|
process.env.H5_PUBLIC_BASE_URL = 'https://m.tkmind.cn';
|
||||||
|
const htmlPath = path.join(process.cwd(), 'MindSpace', owner, 'public', 'kuanting-plan.html');
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
||||||
|
fs.writeFileSync(htmlPath, '<!doctype html><title>宽庭</title>');
|
||||||
|
const text =
|
||||||
|
`[宽庭方案](https://mindspace.tkmind.com/${owner}/public/kuanting-plan.html)`;
|
||||||
|
const next = sanitizePublicHtmlLinksInText(text, { id: owner, username: 'john' });
|
||||||
|
assert.doesNotMatch(next, /mindspace\.tkmind\.com/);
|
||||||
|
assert.match(
|
||||||
|
next,
|
||||||
|
new RegExp(`https://m\\.tkmind\\.cn/MindSpace/${owner}/public/kuanting-plan\\.html`),
|
||||||
|
);
|
||||||
|
assert.doesNotMatch(next, /页面生成未完成/);
|
||||||
|
} finally {
|
||||||
|
if (previousBase == null) delete process.env.H5_PUBLIC_BASE_URL;
|
||||||
|
else process.env.H5_PUBLIC_BASE_URL = previousBase;
|
||||||
|
fs.rmSync(path.join(process.cwd(), 'MindSpace', owner), { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sanitizePublicHtmlLinksInText leaves other users missing-MindSpace links unchanged', () => {
|
||||||
|
const owner = `test-user-${Date.now()}-other-missing-mindspace`;
|
||||||
|
const text = `[外部页面](https://mindspace.tkmind.com/${owner}/public/kuanting-plan.html)`;
|
||||||
|
const next = sanitizePublicHtmlLinksInText(text, { id: `current-${owner}`, username: 'john' });
|
||||||
|
assert.equal(next, text);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('sanitizePublicHtmlLinksInText links existing inline public html paths', () => {
|
||||||
|
const owner = `test-user-${Date.now()}-inline-public-path`;
|
||||||
|
const previousBase = process.env.H5_PUBLIC_BASE_URL;
|
||||||
|
process.env.H5_PUBLIC_BASE_URL = 'https://m.tkmind.cn';
|
||||||
|
const htmlPath = path.join(process.cwd(), 'MindSpace', owner, 'public', 'thailand-travel-guide.html');
|
||||||
|
try {
|
||||||
|
fs.mkdirSync(path.dirname(htmlPath), { recursive: true });
|
||||||
|
fs.writeFileSync(htmlPath, '<!doctype html><title>Thailand</title>');
|
||||||
|
const text = '攻略页面已成功创建!文件位于 **`public/thailand-travel-guide.html`**(约 37KB)。';
|
||||||
|
const next = sanitizePublicHtmlLinksInText(text, { id: owner, username: 'john2' });
|
||||||
|
assert.doesNotMatch(next, /`public\/thailand-travel-guide\.html`/);
|
||||||
|
assert.match(
|
||||||
|
next,
|
||||||
|
new RegExp(`\\[thailand-travel-guide\\.html\\]\\(https://m\\.tkmind\\.cn/MindSpace/${owner}/public/thailand-travel-guide\\.html\\)`),
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
if (previousBase == null) delete process.env.H5_PUBLIC_BASE_URL;
|
||||||
|
else process.env.H5_PUBLIC_BASE_URL = previousBase;
|
||||||
|
fs.rmSync(path.join(process.cwd(), 'MindSpace', owner), { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test('sanitizeSessionConversationPublicHtmlLinks updates text items inside conversation messages', () => {
|
test('sanitizeSessionConversationPublicHtmlLinks updates text items inside conversation messages', () => {
|
||||||
const owner = `test-user-${Date.now()}-conversation`;
|
const owner = `test-user-${Date.now()}-conversation`;
|
||||||
const publicRoot = path.join(process.cwd(), 'MindSpace', owner, 'public');
|
const publicRoot = path.join(process.cwd(), 'MindSpace', owner, 'public');
|
||||||
|
|||||||
Reference in New Issue
Block a user