refactor: share platform file and route helpers

This commit is contained in:
Cita
2026-03-22 22:17:50 +08:00
parent 9290e51ecc
commit 32d7037629
24 changed files with 375 additions and 333 deletions
+17 -21
View File
@@ -19,9 +19,9 @@ import { clearAppInstallationState } from '../appLocalState.js';
import { tr } from '../i18n.js';
import {
isExactModelPattern,
parseBrandIconValue,
resolveRouteBrand,
resolveRouteIcon,
resolveRouteTitle,
ROUTE_ICON_NONE_VALUE,
} from './token-routes/utils.js';
import { generateDownstreamSkKey } from './helpers/generateDownstreamSkKey.js';
@@ -178,12 +178,6 @@ function inferUrlDialect(connectionString: string): 'mysql' | 'postgres' | null
return null;
}
function resolveRouteBrandSource(route: RouteSelectorItem): string {
const title = resolveRouteTitle(route);
if (getBrand(title)) return title;
return route.modelPattern;
}
export default function Settings() {
const navigate = useNavigate();
const isMobile = useIsMobile();
@@ -1907,9 +1901,9 @@ export default function Settings() {
<div style={{ fontSize: 12, color: 'var(--color-text-muted)' }}>没有匹配的群组</div>
) : filteredGroupRouteOptions.map((route) => {
const checked = downstreamCreate.selectedGroupRouteIds.includes(route.id);
const explicitBrandIcon = parseBrandIconValue(route.displayIcon || '');
const explicitNoIcon = (route.displayIcon || '').trim() === ROUTE_ICON_NONE_VALUE;
const textIcon = explicitBrandIcon || explicitNoIcon ? '' : (route.displayIcon || '').trim();
const routeTitle = resolveRouteTitle(route);
const routeIcon = resolveRouteIcon(route);
const routeBrand = resolveRouteBrand(route);
return (
<label
key={route.id}
@@ -1963,21 +1957,23 @@ export default function Settings() {
lineHeight: 1,
}}
>
{explicitBrandIcon ? (
{routeIcon.kind === 'brand' ? (
<BrandGlyph
icon={explicitBrandIcon}
alt={resolveRouteTitle(route)}
icon={routeIcon.value}
alt={routeTitle}
size={18}
fallbackText={resolveRouteTitle(route)}
fallbackText={routeTitle}
/>
) : textIcon ? (
textIcon
) : explicitNoIcon ? null : (
<InlineBrandIcon model={resolveRouteBrandSource(route)} size={18} />
)}
) : routeIcon.kind === 'text' ? (
routeIcon.value
) : routeIcon.kind === 'auto' && routeBrand ? (
<BrandGlyph brand={routeBrand} alt={routeTitle} size={18} fallbackText={routeTitle} />
) : routeIcon.kind === 'auto' ? (
<InlineBrandIcon model={route.modelPattern} size={18} />
) : null}
</span>
<span style={{ overflow: 'hidden', textOverflow: 'ellipsis' }}>
{resolveRouteTitle(route)}
{routeTitle}
</span>
{!route.enabled && (
<span
@@ -1,4 +1,8 @@
import type { PlaygroundProtocol } from './modelTesterSession.js';
import {
buildConversationAcceptList,
detectConversationFileKind,
} from '../../../shared/conversationFileTypes.js';
export type ConversationFileTransportMode = 'native' | 'inline_only' | 'unsupported';
@@ -10,44 +14,13 @@ export type ConversationFileCapability = {
reason: string;
};
const DOCUMENT_ACCEPT_PARTS = ['.pdf', '.txt', '.md', '.markdown', '.json'] as const;
const DOCUMENT_MIME_TYPES = new Set([
'application/json',
'application/pdf',
'text/markdown',
'text/plain',
]);
const DOCUMENT_EXTENSIONS = ['.json', '.md', '.markdown', '.pdf', '.txt'] as const;
const IMAGE_EXTENSIONS = ['.avif', '.bmp', '.gif', '.jpeg', '.jpg', '.png', '.svg', '.webp'] as const;
const AUDIO_EXTENSIONS = ['.aac', '.flac', '.m4a', '.mp3', '.ogg', '.wav', '.weba'] as const;
type ConversationFileDescriptor = {
filename?: string | null;
mimeType?: string | null;
};
type ConversationFileKind = 'document' | 'image' | 'audio' | 'unknown';
const isSupportedMode = (mode: ConversationFileTransportMode): boolean => mode !== 'unsupported';
const normalizeName = (value: string | null | undefined): string => (value || '').trim().toLowerCase();
function detectKindFromFilename(filename: string): ConversationFileKind {
if (!filename) return 'unknown';
if (DOCUMENT_EXTENSIONS.some((extension) => filename.endsWith(extension))) return 'document';
if (IMAGE_EXTENSIONS.some((extension) => filename.endsWith(extension))) return 'image';
if (AUDIO_EXTENSIONS.some((extension) => filename.endsWith(extension))) return 'audio';
return 'unknown';
}
function resolveConversationFileKind(file: ConversationFileDescriptor): ConversationFileKind {
const mimeType = normalizeName(file.mimeType);
if (mimeType.startsWith('image/')) return 'image';
if (mimeType.startsWith('audio/')) return 'audio';
if (DOCUMENT_MIME_TYPES.has(mimeType)) return 'document';
return detectKindFromFilename(normalizeName(file.filename));
}
function buildSupportedTypeLabel(capability: ConversationFileCapability): string {
const labels: string[] = [];
if (isSupportedMode(capability.documentMode)) labels.push('PDF / TXT / Markdown / JSON');
@@ -111,11 +84,11 @@ export function resolveConversationFileCapability(
}
export function buildConversationFileAccept(capability: ConversationFileCapability): string {
const parts: string[] = [];
if (isSupportedMode(capability.documentMode)) parts.push(...DOCUMENT_ACCEPT_PARTS);
if (isSupportedMode(capability.imageMode)) parts.push('image/*');
if (isSupportedMode(capability.audioMode)) parts.push('audio/*');
return parts.join(',');
return buildConversationAcceptList({
document: isSupportedMode(capability.documentMode),
image: isSupportedMode(capability.imageMode),
audio: isSupportedMode(capability.audioMode),
});
}
export function buildConversationFileHint(capability: ConversationFileCapability): string {
@@ -147,7 +120,7 @@ export function isConversationUploadedFileSupported(
capability: ConversationFileCapability,
file: ConversationFileDescriptor,
): boolean {
const kind = resolveConversationFileKind(file);
const kind = detectConversationFileKind(file);
if (kind === 'document') return isSupportedMode(capability.documentMode);
if (kind === 'image') return isSupportedMode(capability.imageMode);
if (kind === 'audio') return isSupportedMode(capability.audioMode);
+2 -6
View File
@@ -1,3 +1,5 @@
import { isTruthyFlag } from './accountConnection.js';
const FOCUS_SITE_ID_KEY = 'focusSiteId';
const FOCUS_ANNOUNCEMENT_ID_KEY = 'focusAnnouncementId';
const FOCUS_ACCOUNT_ID_KEY = 'focusAccountId';
@@ -10,12 +12,6 @@ function normalizePositiveId(input: unknown): number | null {
return value;
}
function isTruthyFlag(value: string | null): boolean {
if (!value) return false;
const normalized = value.trim().toLowerCase();
return normalized === '1' || normalized === 'true' || normalized === 'yes';
}
export function buildSiteFocusPath(siteId: number): string {
const normalizedId = normalizePositiveId(siteId);
if (!normalizedId) return '/sites';
+8 -13
View File
@@ -1,3 +1,5 @@
import { parseProxyLogMetadata } from '../../../shared/proxyLogMeta.js';
type ProxyLogPathMeta = {
clientFamily: string | null;
sessionId: string | null;
@@ -8,20 +10,13 @@ type ProxyLogPathMeta = {
export function parseProxyLogPathMeta(message?: string): ProxyLogPathMeta {
const raw = typeof message === 'string' ? message.trim() : '';
const clientMatch = raw.match(/\[client:([^\]]+)\]/i);
const sessionMatch = raw.match(/\[session:([^\]]+)\]/i);
const downstreamMatch = raw.match(/\[downstream:([^\]]+)\]/i);
const upstreamMatch = raw.match(/\[upstream:([^\]]+)\]/i);
const stripped = raw.replace(
/^\s*(?:\[(?:client|session|downstream|upstream):[^\]]+\]\s*)+/i,
'',
).trim();
const parsed = parseProxyLogMetadata(raw);
return {
clientFamily: clientMatch?.[1]?.trim() || null,
sessionId: sessionMatch?.[1]?.trim() || null,
downstreamPath: downstreamMatch?.[1]?.trim() || null,
upstreamPath: upstreamMatch?.[1]?.trim() || null,
errorMessage: stripped,
clientFamily: parsed.clientKind,
sessionId: parsed.sessionId,
downstreamPath: parsed.downstreamPath,
upstreamPath: parsed.upstreamPath,
errorMessage: parsed.messageText,
};
}