merge: server architecture modularization

This commit is contained in:
john
2026-07-24 19:18:14 +08:00
113 changed files with 29618 additions and 5864 deletions
+77 -4
View File
@@ -34,6 +34,7 @@ import {
extractCurrentTurnImageUrls,
scrubConversationHistoricalImageAttachments,
} from './chat-image-turn-scope.mjs';
import { repairConversationToolHistory } from './chat-tool-history-repair.mjs';
import { buildVisionThumbnailBuffer } from './vision-image-thumb.mjs';
import {
memoryLimitForIntervention,
@@ -461,6 +462,8 @@ const PUBLIC_HTML_MARKDOWN_LINK_PATTERN =
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 MISSING_PUBLIC_HTML_NOTICE_PATTERN =
/(页面生成未完成,已阻止显示失效链接:([^()\n]+\.html)。)/g;
const USER_IDENTITY_BLOCK_PATTERN = /^\[用户身份\][\s\S]*?(?:\n{2,}|$)/;
const IMAGE_URL_LINES_PATTERN = /\n*\[图片\d+]: [^\n]+/g;
const TKMIND_VISION_NOTE_PATTERN = /\n*【TKMind 图片分析结果[\s\S]*$/;
@@ -503,6 +506,25 @@ function buildMissingPublicHtmlNotice(filename) {
return `(页面生成未完成,已阻止显示失效链接:${filename || '页面'}。)`;
}
function publicHtmlRootsForUser(owner) {
const roots = [
path.resolve(process.cwd(), PUBLISH_ROOT_DIR, owner),
];
const sharedPublishRoots = [
process.env.GOOSED_SANDBOX_PUBLISH_ROOT,
process.env.MEMIND_SHARED_PUBLISH_ROOT,
];
for (const configuredRoot of sharedPublishRoots) {
const value = String(configuredRoot ?? '').trim();
if (value) roots.push(path.resolve(value, owner));
}
const usersRoot = String(process.env.H5_USERS_ROOT ?? '').trim();
if (usersRoot) {
roots.push(path.resolve(path.dirname(usersRoot), PUBLISH_ROOT_DIR, owner));
}
return [...new Set(roots)];
}
function publicHtmlExistsForUser(owner, relativePath, currentUser) {
const normalizedOwner = String(owner ?? '').trim().toLowerCase();
const normalizedUserId = String(currentUser?.id ?? '').trim().toLowerCase();
@@ -511,10 +533,12 @@ function publicHtmlExistsForUser(owner, relativePath, currentUser) {
if (normalizedOwner !== normalizedUserId && normalizedOwner !== normalizedUsername) return true;
const normalizedRelativePath = normalizeStaticHtmlRelativePath(relativePath);
if (!normalizedRelativePath || !normalizedRelativePath.toLowerCase().endsWith('.html')) return false;
const root = path.resolve(process.cwd(), PUBLISH_ROOT_DIR, normalizedOwner);
const target = path.resolve(root, normalizedRelativePath);
if (target !== root && !target.startsWith(`${root}${path.sep}`)) return false;
return fs.existsSync(target) && fs.statSync(target).isFile();
for (const root of publicHtmlRootsForUser(normalizedOwner)) {
const target = path.resolve(root, normalizedRelativePath);
if (target !== root && !target.startsWith(`${root}${path.sep}`)) continue;
if (fs.existsSync(target) && fs.statSync(target).isFile()) return true;
}
return false;
}
function sanitizeOwnPublicHtmlUrl(publicUrl, owner, rawRelativePath, currentUser) {
@@ -585,6 +609,13 @@ function rewriteOwnPublicationRouteLinks(text, currentUser) {
export function sanitizePublicHtmlLinksInText(text, currentUser) {
let next = String(text ?? '').replace(
MISSING_PUBLIC_HTML_NOTICE_PATTERN,
(match, filename) => {
const result = sanitizeOwnPublicHtmlRelativePath(`public/${path.posix.basename(filename)}`, currentUser);
return result ? `[${result.label}](${result.url})` : match;
},
);
next = next.replace(
PUBLIC_HTML_MARKDOWN_LINK_PATTERN,
(match, label, url, owner, rawRelativePath) => {
const result = sanitizeOwnPublicHtmlUrl(url, owner, rawRelativePath, currentUser);
@@ -1634,6 +1665,47 @@ export function createTkmindProxy({
}
}
async function repairSessionToolHistory(sessionId) {
if (!sessionId) return { changed: false, updated: false };
const target = await resolveTarget(sessionId);
const upstream = await apiFetch(
target,
apiSecret,
`/sessions/${encodeURIComponent(sessionId)}`,
);
if (!upstream.ok) {
return { changed: false, updated: false, status: upstream.status };
}
const session = await upstream.json().catch(() => null);
const repair = repairConversationToolHistory(session?.conversation);
if (!repair.changed) return { ...repair, updated: false, status: upstream.status };
const update = await apiFetch(
target,
apiSecret,
`/sessions/${encodeURIComponent(sessionId)}`,
{
method: 'PUT',
body: JSON.stringify({ conversation: repair.conversation }),
},
);
if (!update.ok) {
const freshSessionRequired = update.status === 404 || update.status === 405;
const error = new Error(
freshSessionRequired
? '当前会话包含不完整的工具调用历史,需要迁移到新会话'
: `会话工具历史修复失败 (${update.status})`,
);
error.code = freshSessionRequired
? 'SESSION_TOOL_HISTORY_FRESH_SESSION_REQUIRED'
: 'SESSION_TOOL_HISTORY_REPAIR_FAILED';
error.retryable = !freshSessionRequired;
error.repairedConversation = repair.conversation;
throw error;
}
return { ...repair, updated: true, status: update.status };
}
async function prepareSessionReplyBody(
userId,
sessionId,
@@ -1689,6 +1761,7 @@ export function createTkmindProxy({
forceDeepReasoning,
});
await applySessionLlmProvider(sessionId);
await repairSessionToolHistory(sessionId);
const user = await userAuth.getUserById(userId);
if (!user) throw new Error('用户不存在');