Merge origin/master: resolve CSS conflicts, keep both sides

This commit is contained in:
2026-06-04 16:22:40 +08:00
80 changed files with 5491 additions and 3557 deletions
+16 -66
View File
@@ -10,6 +10,7 @@ import {
} from "@ant-design/icons";
import { useEffect, useRef, useState, type ChangeEvent, type KeyboardEvent } from "react";
import { evaluateScript } from "../../api/scriptEvalClient";
import { buildApiUrl, getStoredToken } from "../../api/serverConnection";
import { useSessionStore } from "../../stores";
interface ScoreDimension {
@@ -175,61 +176,6 @@ function normalizeUploadedText(raw: string, ext: string): string {
return raw;
}
async function extractDocxText(bytes: Uint8Array): Promise<string> {
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
const entries: Array<{ name: string; offset: number; size: number; compressed: boolean }> = [];
let pos = 0;
while (pos < bytes.length - 30) {
if (view.getUint32(pos, true) !== 0x04034b50) break;
const compressed = view.getUint16(pos + 10, true) !== 0;
const compressedSize = view.getUint32(pos + 18, true);
const fileNameLen = view.getUint16(pos + 26, true);
const extraLen = view.getUint16(pos + 28, true);
const name = new TextDecoder().decode(bytes.slice(pos + 30, pos + 30 + fileNameLen));
const dataStart = pos + 30 + fileNameLen + extraLen;
entries.push({ name, offset: dataStart, size: compressedSize, compressed });
pos = dataStart + compressedSize;
}
const docEntry = entries.find((e) => e.name === "word/document.xml");
if (!docEntry) return "";
const xmlBytes = bytes.slice(docEntry.offset, docEntry.offset + docEntry.size);
let xmlText: string;
if (docEntry.compressed) {
try {
const ds = new DecompressionStream("deflate-raw");
const writer = ds.writable.getWriter();
writer.write(xmlBytes);
writer.close();
const reader = ds.readable.getReader();
const chunks: Uint8Array[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const totalLen = chunks.reduce((s, c) => s + c.length, 0);
const combined = new Uint8Array(totalLen);
let offset = 0;
for (const c of chunks) { combined.set(c, offset); offset += c.length; }
xmlText = new TextDecoder().decode(combined);
} catch {
xmlText = new TextDecoder().decode(xmlBytes);
}
} else {
xmlText = new TextDecoder().decode(xmlBytes);
}
const textMatches = xmlText.match(/<w:t[^>]*>([\s\S]*?)<\/w:t>/g);
if (!textMatches) return "";
const paraMatches = xmlText.match(/<w:p[ >][\s\S]*?<\/w:p>/g);
if (paraMatches) {
return paraMatches.map((p) => {
const tMatches = p.match(/<w:t[^>]*>([\s\S]*?)<\/w:t>/g);
if (!tMatches) return "";
return tMatches.map((m) => m.replace(/<[^>]+>/g, "").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&quot;/g, "\"")).join("");
}).filter(Boolean).join("\n").trim();
}
return "";
}
function formatFileSize(size: number): string {
if (size < 1024) return `${size} B`;
@@ -321,22 +267,26 @@ function ScriptTokensPage() {
const ext = getFileExtension(file.name);
const readable = isReadableTextFile(file, ext);
setUploadedFile({ name: file.name, size: file.size });
if (ext === ".docx") {
if (ext === ".docx" || ext === ".doc") {
try {
const bytes = new Uint8Array(await file.arrayBuffer());
const text = await extractDocxText(bytes);
if (text) {
setScript(text);
const formData = new FormData();
formData.append("file", file);
const token = getStoredToken();
const resp = await fetch(buildApiUrl("files/extract-text"), {
method: "POST",
headers: token ? { Authorization: `Bearer ${token}` } : {},
body: formData,
});
if (resp.ok) {
const { text } = await resp.json();
setScript(text || "");
} else {
setScript(`[已上传文件:${file.name}]\n\n无法从 DOCX 文件中提取文本,请尝试另存为 TXT 格式后重新上传。`);
const err = await resp.json().catch(() => ({ error: "解析失败" }));
setScript(`[已上传文件:${file.name}]\n\n${err.error || "文件解析失败,请尝试另存为 TXT 格式后重新上传。"}`);
}
} catch {
setScript(`[已上传文件:${file.name}]\n\n解析 DOCX 文件失败,请尝试另存为 TXT 格式后重新上传`);
setScript(`[已上传文件:${file.name}]\n\n文件解析请求失败,请检查网络连接后重试`);
}
} else if (ext === ".doc") {
const text = await decodeTextFile(file);
const cleaned = text.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f]/g, "").replace(/\s{3,}/g, "\n\n").trim();
setScript(cleaned || `[已上传文件:${file.name}]\n\n无法从 .doc 文件中提取文本,请另存为 .docx 或 .txt 格式。`);
} else if (readable) {
const text = normalizeUploadedText(await decodeTextFile(file), ext);
setScript(text);
@@ -142,6 +142,8 @@ function TokenUsagePage({
onSelectView,
}: TokenUsagePageProps) {
const [enterpriseUsage, setEnterpriseUsage] = useState<WebEnterpriseUsageSummary | null>(null);
const [enterpriseUsageLoading, setEnterpriseUsageLoading] = useState(false);
const [enterpriseUsageError, setEnterpriseUsageError] = useState<string | null>(null);
const isEnterpriseAdmin = session?.user.enterpriseRole === "admin";
const isEnterpriseAccount = Boolean(session?.user.enterpriseId || session?.user.accountType === "enterprise");
@@ -152,10 +154,15 @@ function TokenUsagePage({
setEnterpriseUsage(null);
return;
}
setEnterpriseUsageLoading(true);
setEnterpriseUsageError(null);
try {
setEnterpriseUsage(await loader());
} catch (error) {
setEnterpriseUsage(null);
setEnterpriseUsageError(error instanceof Error ? error.message : "加载失败");
} finally {
setEnterpriseUsageLoading(false);
}
}, [session, isEnterpriseAdmin, loadEnterpriseUsage, loadPersonalUsage]);