merge: server architecture modularization
This commit is contained in:
+77
-4
@@ -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('用户不存在');
|
||||
|
||||
Reference in New Issue
Block a user