修复了日志重复的问题

This commit is contained in:
xunbu
2025-05-17 10:26:46 +08:00
parent 79dae2272b
commit a25ec1fae9
4 changed files with 241 additions and 222 deletions

13
.idea/workspace.xml generated
View File

@@ -6,6 +6,9 @@
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="6b18b44a-df57-4212-a857-9e291ebe5dd2" name="更改" comment=""> <list default="true" id="6b18b44a-df57-4212-a857-9e291ebe5dd2" name="更改" comment="">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/agents/markdown_agent.py" beforeDir="false" afterPath="$PROJECT_DIR$/docutranslate/agents/markdown_agent.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docutranslate/app.py" beforeDir="false" afterPath="$PROJECT_DIR$/docutranslate/app.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/pyproject.toml" beforeDir="false" afterPath="$PROJECT_DIR$/pyproject.toml" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -545,10 +548,10 @@
<recent_temporary> <recent_temporary>
<list> <list>
<item itemvalue="Python.app_test (1)" /> <item itemvalue="Python.app_test (1)" />
<item itemvalue="Python.app" />
<item itemvalue="Python.test2" /> <item itemvalue="Python.test2" />
<item itemvalue="Python.test" /> <item itemvalue="Python.test" />
<item itemvalue="Python.切分测试" /> <item itemvalue="Python.切分测试" />
<item itemvalue="Python.app_test" />
</list> </list>
</recent_temporary> </recent_temporary>
</component> </component>
@@ -628,20 +631,20 @@
<option name="version" value="3" /> <option name="version" value="3" />
</component> </component>
<component name="com.intellij.coverage.CoverageDataManagerImpl"> <component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/filetranslate$app_test__1_.coverage" NAME="app_test (1) 覆盖结果" MODIFIED="1747441337057" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" /> <SUITE FILE_PATH="coverage/filetranslate$app_test__1_.coverage" NAME="app_test (1) 覆盖结果" MODIFIED="1747448568953" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />
<SUITE FILE_PATH="coverage/filetranslate$test.coverage" NAME="test 覆盖结果" MODIFIED="1747301959211" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" /> <SUITE FILE_PATH="coverage/filetranslate$test.coverage" NAME="test 覆盖结果" MODIFIED="1747301959211" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />
<SUITE FILE_PATH="coverage/filetranslate$convert.coverage" NAME="convert 覆盖结果" MODIFIED="1746963490689" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate/utils" /> <SUITE FILE_PATH="coverage/filetranslate$convert.coverage" NAME="convert 覆盖结果" MODIFIED="1746963490689" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate/utils" />
<SUITE FILE_PATH="coverage/PDFtranslate$PDFtranslater__1_.coverage" NAME="PDFtranslater (1) 覆盖结果" MODIFIED="1746633258205" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/pdftranslate_packages" /> <SUITE FILE_PATH="coverage/PDFtranslate$PDFtranslater__1_.coverage" NAME="PDFtranslater (1) 覆盖结果" MODIFIED="1746633258205" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/pdftranslate_packages" />
<SUITE FILE_PATH="coverage/PDFtranslate$agent_utils.coverage" NAME="agent_utils 覆盖结果" MODIFIED="1746617703678" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/pdftranslate_packages/utils" /> <SUITE FILE_PATH="coverage/PDFtranslate$agent_utils.coverage" NAME="agent_utils 覆盖结果" MODIFIED="1746617703678" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/pdftranslate_packages/utils" />
<SUITE FILE_PATH="coverage/filetranslate$app2.coverage" NAME="app2 覆盖结果" MODIFIED="1747108180309" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate" /> <SUITE FILE_PATH="coverage/filetranslate$app2.coverage" NAME="app2 覆盖结果" MODIFIED="1747108180309" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate" />
<SUITE FILE_PATH="coverage/filetranslate$app.coverage" NAME="app 覆盖结果" MODIFIED="1747130360790" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate" /> <SUITE FILE_PATH="coverage/filetranslate$app.coverage" NAME="app 覆盖结果" MODIFIED="1747448464521" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate" />
<SUITE FILE_PATH="coverage/PDFtranslate$markdown_splitter.coverage" NAME="markdown_splitter 覆盖结果" MODIFIED="1746599883603" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/pdftranslate_packages/utils" /> <SUITE FILE_PATH="coverage/PDFtranslate$markdown_splitter.coverage" NAME="markdown_splitter 覆盖结果" MODIFIED="1746599883603" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/pdftranslate_packages/utils" />
<SUITE FILE_PATH="coverage/filetranslate$test3.coverage" NAME="test3 覆盖结果" MODIFIED="1746884110572" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" /> <SUITE FILE_PATH="coverage/filetranslate$test3.coverage" NAME="test3 覆盖结果" MODIFIED="1746884110572" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />
<SUITE FILE_PATH="coverage/filetranslate$app__1_.coverage" NAME="app (1) 覆盖结果" MODIFIED="1747136094477" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" /> <SUITE FILE_PATH="coverage/filetranslate$app__1_.coverage" NAME="app (1) 覆盖结果" MODIFIED="1747136094477" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />
<SUITE FILE_PATH="coverage/filetranslate$markdown_splitter.coverage" NAME="markdown_splitter 覆盖结果" MODIFIED="1746805063874" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate/utils" /> <SUITE FILE_PATH="coverage/filetranslate$markdown_splitter.coverage" NAME="markdown_splitter 覆盖结果" MODIFIED="1746805063874" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate/utils" />
<SUITE FILE_PATH="coverage/PDFtranslate$test.coverage" NAME="test 覆盖结果" MODIFIED="1746629433597" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />
<SUITE FILE_PATH="coverage/filetranslate$PDFtranslater__2_.coverage" NAME="PDFtranslater (2) 覆盖结果" MODIFIED="1746679546680" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/filetranslate_packages" />
<SUITE FILE_PATH="coverage/filetranslate$agent.coverage" NAME="agent 覆盖结果" MODIFIED="1746805293987" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate/Agents" /> <SUITE FILE_PATH="coverage/filetranslate$agent.coverage" NAME="agent 覆盖结果" MODIFIED="1746805293987" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate/Agents" />
<SUITE FILE_PATH="coverage/filetranslate$PDFtranslater__2_.coverage" NAME="PDFtranslater (2) 覆盖结果" MODIFIED="1746679546680" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/filetranslate_packages" />
<SUITE FILE_PATH="coverage/PDFtranslate$test.coverage" NAME="test 覆盖结果" MODIFIED="1746629433597" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />
<SUITE FILE_PATH="coverage/filetranslate$agent_utils.coverage" NAME="agent_utils 覆盖结果" MODIFIED="1746708534311" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate/utils" /> <SUITE FILE_PATH="coverage/filetranslate$agent_utils.coverage" NAME="agent_utils 覆盖结果" MODIFIED="1746708534311" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/docutranslate/utils" />
<SUITE FILE_PATH="coverage/filetranslate$.coverage" NAME="切分测试 覆盖结果" MODIFIED="1747187128847" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" /> <SUITE FILE_PATH="coverage/filetranslate$.coverage" NAME="切分测试 覆盖结果" MODIFIED="1747187128847" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />
<SUITE FILE_PATH="coverage/filetranslate$test1.coverage" NAME="test1 覆盖结果" MODIFIED="1746936018440" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" /> <SUITE FILE_PATH="coverage/filetranslate$test1.coverage" NAME="test1 覆盖结果" MODIFIED="1746936018440" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/tests" />

View File

@@ -56,7 +56,7 @@ class MDTranslateAgent(Agent):
形如<ph-abc123>的占位符不要改变 形如<ph-abc123>的占位符不要改变
code、latex和HTML只翻译说明文字其余保持原文 code、latex和HTML只翻译说明文字其余保持原文
公式必须表示为合法的latex公式,行内公式需被$正确包裹 公式必须表示为合法的latex公式,行内公式需被$正确包裹
去掉异常字词 去掉异常字词,修复错误格式
# 输出 # 输出
翻译后的markdown纯文本不是markdown代码块 翻译后的markdown纯文本不是markdown代码块
# 示例 # 示例
@@ -65,7 +65,7 @@ code、latex和HTML只翻译说明文字其余保持原文
hello<ph-aaaaaa>, what's your name? hello<ph-aaaaaa>, what's your name?
输出: 输出:
你好<ph-aaaaaa>,你叫什么名字? 你好<ph-aaaaaa>,你叫什么名字?
## 公式要为合法latex行内公式使用$包裹) ## 公式要为合法latex行内公式必须使用$包裹)
输入: 输入:
The equation is E=mc 2. This is famous. The equation is E=mc 2. This is famous.
({{c_0,c_1,c^2}})is a set. ({{c_0,c_1,c^2}})is a set.

View File

@@ -37,7 +37,7 @@ HTML_TEMPLATE = """<!DOCTYPE html>
background-color: #f5f5f5; background-color: #f5f5f5;
border: 1px solid #e0e0e0; border: 1px solid #e0e0e0;
padding: 10px; padding: 10px;
height: 200px; height: 300px;
overflow-y: scroll; overflow-y: scroll;
white-space: pre-wrap; white-space: pre-wrap;
font-family: monospace; font-family: monospace;
@@ -149,7 +149,8 @@ HTML_TEMPLATE = """<!DOCTYPE html>
#fileDropArea p { /* General style for <p> inside drop area */ #fileDropArea p { /* General style for <p> inside drop area */
margin: 0.5rem 0; margin: 0.5rem 0;
color: #555; color: #555;
} } \
/* #fileDropPrompt will be hidden/shown by JS using .hidden class */ /* #fileDropPrompt will be hidden/shown by JS using .hidden class */
@@ -167,7 +168,8 @@ HTML_TEMPLATE = """<!DOCTYPE html>
#fileDropArea.input-error { #fileDropArea.input-error {
border-color: var(--pico-form-element-invalid-border-color, #d32f2f) !important; border-color: var(--pico-form-element-invalid-border-color, #d32f2f) !important;
} } \
#fileNameDisplay.input-error-text { #fileNameDisplay.input-error-text {
color: var(--pico-form-element-invalid-border-color, #d32f2f) !important; color: var(--pico-form-element-invalid-border-color, #d32f2f) !important;
font-weight: bold; font-weight: bold;
@@ -315,17 +317,15 @@ HTML_TEMPLATE = """<!DOCTYPE html>
const closePreviewBtn = document.getElementById('closePreviewBtn'); const closePreviewBtn = document.getElementById('closePreviewBtn');
const printFromPreview = document.getElementById('printFromPreview'); const printFromPreview = document.getElementById('printFromPreview');
// File input and drag-drop elements
const fileInput = document.getElementById('file'); const fileInput = document.getElementById('file');
const fileDropArea = document.getElementById('fileDropArea'); const fileDropArea = document.getElementById('fileDropArea');
const fileNameDisplay = document.getElementById('fileNameDisplay'); const fileNameDisplay = document.getElementById('fileNameDisplay');
const fileDropPrompt = document.getElementById('fileDropPrompt'); // <-- 获取提示文字元素 const fileDropPrompt = document.getElementById('fileDropPrompt');
let logPollIntervalId = null; let logPollIntervalId = null;
let statusPollIntervalId = null; let statusPollIntervalId = null;
let lastLogCount = 0; // let lastLogCount = 0; // No longer needed for fetching logs
let isTranslating = false; // Flag to track translation state for cancel button let isTranslating = false;
function saveToStorage(key, value) { function saveToStorage(key, value) {
try { try {
@@ -387,9 +387,8 @@ HTML_TEMPLATE = """<!DOCTYPE html>
} }
}); });
// --- Drag and Drop File Handling ---
fileDropArea.addEventListener('click', () => { fileDropArea.addEventListener('click', () => {
fileInput.click(); // Trigger click on hidden file input fileInput.click();
}); });
fileInput.addEventListener('change', () => { fileInput.addEventListener('change', () => {
@@ -397,7 +396,7 @@ HTML_TEMPLATE = """<!DOCTYPE html>
fileNameDisplay.textContent = `已选择: ${fileInput.files[0].name}`; fileNameDisplay.textContent = `已选择: ${fileInput.files[0].name}`;
fileDropArea.classList.add('file-selected'); fileDropArea.classList.add('file-selected');
fileNameDisplay.classList.add('has-file'); fileNameDisplay.classList.add('has-file');
fileDropPrompt.classList.add('hidden'); // <-- 隐藏提示文字 fileDropPrompt.classList.add('hidden');
fileDropArea.classList.remove('input-error'); fileDropArea.classList.remove('input-error');
fileNameDisplay.classList.remove('input-error-text'); fileNameDisplay.classList.remove('input-error-text');
statusMsg.textContent = ''; statusMsg.textContent = '';
@@ -406,7 +405,7 @@ HTML_TEMPLATE = """<!DOCTYPE html>
fileNameDisplay.textContent = '未选择文件'; fileNameDisplay.textContent = '未选择文件';
fileDropArea.classList.remove('file-selected'); fileDropArea.classList.remove('file-selected');
fileNameDisplay.classList.remove('has-file'); fileNameDisplay.classList.remove('has-file');
fileDropPrompt.classList.remove('hidden'); // <-- 显示提示文字 fileDropPrompt.classList.remove('hidden');
} }
}); });
@@ -444,12 +443,10 @@ HTML_TEMPLATE = """<!DOCTYPE html>
} }
}, false); }, false);
// --- End Drag and Drop ---
async function pollLogs() { async function pollLogs() {
try { try {
const response = await fetch(`/get-logs?since=${lastLogCount}`); // const response = await fetch(`/get-logs?since=${lastLogCount}`); // OLD
const response = await fetch('/get-logs'); // NEW: No 'since' parameter
if (!response.ok) { if (!response.ok) {
console.warn(`Log polling failed: ${response.status}`); console.warn(`Log polling failed: ${response.status}`);
return; return;
@@ -460,9 +457,9 @@ HTML_TEMPLATE = """<!DOCTYPE html>
const escapedLog = log.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); const escapedLog = log.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
logArea.innerHTML += escapedLog + "<br>"; logArea.innerHTML += escapedLog + "<br>";
}); });
logArea.scrollTop = logArea.scrollHeight; logArea.scrollTop = logArea.scrollHeight; // Scroll to bottom
} }
lastLogCount = data.total_count; // lastLogCount = data.total_count; // OLD: No longer tracking count this way
} catch (error) { } catch (error) {
console.warn("Error polling logs:", error); console.warn("Error polling logs:", error);
} }
@@ -574,7 +571,7 @@ HTML_TEMPLATE = """<!DOCTYPE html>
} else { } else {
downloadBtns.style.display = 'none'; downloadBtns.style.display = 'none';
} }
} else { // Task is still processing } else {
submitButton.textContent = '取消翻译'; submitButton.textContent = '取消翻译';
submitButton.classList.remove('primary'); submitButton.classList.remove('primary');
submitButton.classList.add('secondary'); submitButton.classList.add('secondary');
@@ -592,10 +589,10 @@ HTML_TEMPLATE = """<!DOCTYPE html>
function startPolling() { function startPolling() {
stopPolling(); stopPolling();
lastLogCount = 0; // lastLogCount = 0; // No longer needed
logArea.innerHTML = ''; logArea.innerHTML = ''; // Clear log area for new task
pollLogs(); pollLogs(); // Initial poll for logs
pollStatus(); pollStatus(); // Initial poll for status
logPollIntervalId = setInterval(pollLogs, 2000); logPollIntervalId = setInterval(pollLogs, 2000);
statusPollIntervalId = setInterval(pollStatus, 1500); statusPollIntervalId = setInterval(pollStatus, 1500);
} }
@@ -605,7 +602,6 @@ HTML_TEMPLATE = """<!DOCTYPE html>
if (statusPollIntervalId) clearInterval(statusPollIntervalId); if (statusPollIntervalId) clearInterval(statusPollIntervalId);
logPollIntervalId = null; logPollIntervalId = null;
statusPollIntervalId = null; statusPollIntervalId = null;
setTimeout(pollLogs, 100);
} }
function loadSettings() { function loadSettings() {
@@ -617,7 +613,6 @@ HTML_TEMPLATE = """<!DOCTYPE html>
refineCheckbox.checked = getFromStorage('translator_refine_markdown') === 'true'; refineCheckbox.checked = getFromStorage('translator_refine_markdown') === 'true';
} }
async function cancelTranslation() { async function cancelTranslation() {
submitButton.disabled = true; submitButton.disabled = true;
submitButton.textContent = '正在取消...'; submitButton.textContent = '正在取消...';
@@ -661,14 +656,13 @@ HTML_TEMPLATE = """<!DOCTYPE html>
fileNameDisplay.textContent = '请选择文件!'; fileNameDisplay.textContent = '请选择文件!';
fileNameDisplay.classList.add('input-error-text'); fileNameDisplay.classList.add('input-error-text');
fileDropArea.classList.add('input-error'); fileDropArea.classList.add('input-error');
fileDropPrompt.classList.remove('hidden'); // 确保错误时提示文字可见 fileDropPrompt.classList.remove('hidden');
setTimeout(() => { setTimeout(() => {
fileDropArea.classList.remove('input-error'); fileDropArea.classList.remove('input-error');
fileNameDisplay.classList.remove('input-error-text'); fileNameDisplay.classList.remove('input-error-text');
if (fileNameDisplay.textContent === '请选择文件!') { if (fileNameDisplay.textContent === '请选择文件!') {
fileNameDisplay.textContent = '未选择文件'; fileNameDisplay.textContent = '未选择文件';
} }
// 如果没有文件被选中,提示文字应该保持可见
if (fileInput.files.length === 0) { if (fileInput.files.length === 0) {
fileDropPrompt.classList.remove('hidden'); fileDropPrompt.classList.remove('hidden');
} }
@@ -677,7 +671,6 @@ HTML_TEMPLATE = """<!DOCTYPE html>
return; return;
} }
stopPolling(); stopPolling();
submitButton.disabled = true; submitButton.disabled = true;
submitButton.setAttribute('aria-busy', 'true'); submitButton.setAttribute('aria-busy', 'true');
@@ -686,7 +679,7 @@ HTML_TEMPLATE = """<!DOCTYPE html>
statusMsg.textContent = '正在提交任务...'; statusMsg.textContent = '正在提交任务...';
statusMsg.className = ''; statusMsg.className = '';
downloadBtns.style.display = 'none'; downloadBtns.style.display = 'none';
lastLogCount = 0; // lastLogCount = 0; // No longer needed
const formData = new FormData(form); const formData = new FormData(form);
try { try {
@@ -719,14 +712,14 @@ HTML_TEMPLATE = """<!DOCTYPE html>
isTranslating = false; isTranslating = false;
} }
}); });
</script> </script>
</body> </body>
</html>""" </html>"""
app = FastAPI() app = FastAPI()
# --- 全局配置 --- # --- 全局配置 ---
log_queue = asyncio.Queue() log_queue: Optional[asyncio.Queue] = None # Will be initialized in startup_event
current_state: Dict[str, Any] = { current_state: Dict[str, Any] = {
"is_processing": False, "is_processing": False,
"status_message": "空闲", "status_message": "空闲",
@@ -737,63 +730,74 @@ current_state: Dict[str, Any] = {
"original_filename_stem": None, "original_filename_stem": None,
"task_start_time": 0, "task_start_time": 0,
"task_end_time": 0, "task_end_time": 0,
"current_task_ref": None, # Stores the asyncio.Task object "current_task_ref": None,
} }
templates = Jinja2Templates(directory=".") templates = Jinja2Templates(directory=".")
MAX_LOG_HISTORY = 200 MAX_LOG_HISTORY = 200 # Max items for the persistent log_history list
log_history: List[str] = [] log_history: List[str] = [] # Keeps a longer history, not directly for "unread"
# --- 日志处理器 --- # --- 日志处理器 ---
class QueueAndHistoryHandler(logging.Handler): class QueueAndHistoryHandler(logging.Handler):
def __init__(self, queue: asyncio.Queue, history: List[str], max_history: int): def __init__(self, queue_ref: asyncio.Queue, history_list_ref: List[str], max_history_items: int):
super().__init__() super().__init__()
self.queue = queue self.queue = queue_ref
self.history = history self.history_list = history_list_ref
self.max_history = max_history self.max_history = max_history_items
def emit(self, record: logging.LogRecord): def emit(self, record: logging.LogRecord):
log_entry = self.format(record) log_entry = self.format(record)
print(log_entry)
self.history.append(log_entry) # Add to the persistent history (capped)
if len(self.history) > self.max_history: self.history_list.append(log_entry)
del self.history[:len(self.history) - self.max_history] if len(self.history_list) > self.max_history:
del self.history_list[:len(self.history_list) - self.max_history]
# Add to the "unread" queue for frontend consumption
try: try:
# Ensure self.queue is not None (it's initialized at startup)
if self.queue is not None:
main_loop = getattr(app.state, "main_event_loop", None) main_loop = getattr(app.state, "main_event_loop", None)
if main_loop and main_loop.is_running(): if main_loop and main_loop.is_running():
main_loop.call_soon_threadsafe(self.queue.put_nowait, log_entry) main_loop.call_soon_threadsafe(self.queue.put_nowait, log_entry)
else: else:
# Fallback if loop isn't available or running (e.g. during shutdown) self.queue.put_nowait(log_entry) # Fallback
self.queue.put_nowait(log_entry) else:
print(f"CRITICAL: Log queue not initialized. Log: {log_entry}")
except asyncio.QueueFull:
print(f"Log queue is full. Log dropped: {log_entry}") # Or handle differently
except Exception as e: except Exception as e:
# Avoid crashing the logger if queue operations fail print(f"Error putting log to queue: {e}. Log: {log_entry}")
print(f"Error putting log to queue: {e}")
# --- 应用生命周期事件 --- # --- 应用生命周期事件 ---
@app.on_event("startup") @app.on_event("startup")
async def startup_event(): async def startup_event():
global log_queue
app.state.main_event_loop = asyncio.get_running_loop() app.state.main_event_loop = asyncio.get_running_loop()
log_queue = asyncio.Queue() # Initialize the global log_queue
# 清除所有现有的处理器
for handler in translater_logger.handlers[:]: for handler in translater_logger.handlers[:]:
translater_logger.removeHandler(handler) translater_logger.removeHandler(handler)
# 配置新的处理器
queue_handler = QueueAndHistoryHandler(log_queue, log_history, MAX_LOG_HISTORY) queue_handler = QueueAndHistoryHandler(log_queue, log_history, MAX_LOG_HISTORY)
queue_handler.setLevel(logging.INFO) queue_handler.setLevel(logging.INFO)
queue_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) queue_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
# 添加处理器并配置日志记录器
translater_logger.addHandler(queue_handler) translater_logger.addHandler(queue_handler)
translater_logger.propagate = False # 这一点很重要,防止日志重复 translater_logger.propagate = False
translater_logger.setLevel(logging.INFO) translater_logger.setLevel(logging.INFO)
# 清空日志历史,重新开始
log_history.clear() log_history.clear()
while not log_queue.empty(): # Clear queue just in case
try:
log_queue.get_nowait()
except asyncio.QueueEmpty:
break
translater_logger.info("应用启动完成,日志队列/历史处理器已正确配置。") translater_logger.info("应用启动完成,日志队列/历史处理器已正确配置。")
# --- Background Task Logic --- # --- Background Task Logic ---
async def _perform_translation(params: Dict[str, Any], file_contents: bytes, original_filename: str): async def _perform_translation(params: Dict[str, Any], file_contents: bytes, original_filename: str):
global current_state global current_state
@@ -844,19 +848,17 @@ async def _perform_translation(params: Dict[str, Any], file_contents: bytes, ori
translater_logger.info(f"翻译任务 '{original_filename}' 已被取消 (用时 {duration:.2f} 秒).") translater_logger.info(f"翻译任务 '{original_filename}' 已被取消 (用时 {duration:.2f} 秒).")
current_state.update({ current_state.update({
"status_message": f"翻译任务已取消(若有转换任务仍会后台进行) (用时 {duration:.2f} 秒).", "status_message": f"翻译任务已取消(若有转换任务仍会后台进行) (用时 {duration:.2f} 秒).",
"error_flag": False, "error_flag": False, # Cancellation is not an error in this context
"download_ready": False, "download_ready": False,
"markdown_content": None, "markdown_content": None,
"html_content": None, "html_content": None,
"task_end_time": end_time, "task_end_time": end_time,
}) })
# Do not re-raise CancelledError, it's handled.
except Exception as e: except Exception as e:
end_time = time.time() end_time = time.time()
duration = end_time - current_state["task_start_time"] duration = end_time - current_state["task_start_time"]
error_message = f"翻译失败: {e}" error_message = f"翻译失败: {e}"
translater_logger.error(error_message, exc_info=True) translater_logger.error(error_message, exc_info=True)
# tb_str = traceback.format_exc() # Not used directly, exc_info=True logs it
current_state.update({ current_state.update({
"status_message": f"翻译过程中发生错误 (用时 {duration:.2f} 秒): {e}", "status_message": f"翻译过程中发生错误 (用时 {duration:.2f} 秒): {e}",
"error_flag": True, "error_flag": True,
@@ -867,7 +869,7 @@ async def _perform_translation(params: Dict[str, Any], file_contents: bytes, ori
}) })
finally: finally:
current_state["is_processing"] = False current_state["is_processing"] = False
current_state["current_task_ref"] = None # Clear the task reference current_state["current_task_ref"] = None
translater_logger.info(f"后台翻译任务 '{original_filename}' 处理结束。") translater_logger.info(f"后台翻译任务 '{original_filename}' 处理结束。")
@@ -879,7 +881,6 @@ async def main_page(request: Request):
@app.post("/translate") @app.post("/translate")
async def handle_translate( async def handle_translate(
# No BackgroundTasks needed here for the main task
base_url: str = Form(...), base_url: str = Form(...),
apikey: str = Form(...), apikey: str = Form(...),
model_id: str = Form(...), model_id: str = Form(...),
@@ -889,7 +890,7 @@ async def handle_translate(
refine_markdown: bool = Form(False), refine_markdown: bool = Form(False),
file: UploadFile = File(...) file: UploadFile = File(...)
): ):
global current_state global current_state, log_queue, log_history
if current_state["is_processing"] and \ if current_state["is_processing"] and \
current_state["current_task_ref"] and \ current_state["current_task_ref"] and \
not current_state["current_task_ref"].done(): not current_state["current_task_ref"].done():
@@ -898,13 +899,13 @@ async def handle_translate(
content={"task_started": False, "message": "另一个翻译任务正在进行中,请稍后再试。"} content={"task_started": False, "message": "另一个翻译任务正在进行中,请稍后再试。"}
) )
if not file or not file.filename: # Check if a file was actually uploaded if not file or not file.filename:
return JSONResponse( return JSONResponse(
status_code=400, status_code=400,
content={"task_started": False, "message": "没有选择文件或文件无效。"} content={"task_started": False, "message": "没有选择文件或文件无效。"}
) )
current_state["is_processing"] = True # Set this immediately current_state["is_processing"] = True
original_filename_for_init = file.filename or "uploaded_file" original_filename_for_init = file.filename or "uploaded_file"
current_state.update({ current_state.update({
@@ -916,17 +917,34 @@ async def handle_translate(
"original_filename_stem": Path(original_filename_for_init).stem, "original_filename_stem": Path(original_filename_for_init).stem,
"task_start_time": time.time(), "task_start_time": time.time(),
"task_end_time": 0, "task_end_time": 0,
"current_task_ref": None, # Will be set after task creation "current_task_ref": None,
}) })
log_history.clear() # Clear logs for the new task
log_history.append(translater_logger.handlers[0].format(logging.LogRecord( # Clear logs for the new task
log_history.clear()
if log_queue: # Ensure log_queue is initialized
while not log_queue.empty():
try:
log_queue.get_nowait()
except asyncio.QueueEmpty:
break
# Add initial log entry for the new task
# We create a LogRecord manually to ensure it goes through the formatter and handler
initial_log_msg = f"收到新的翻译请求: {original_filename_for_init}"
if translater_logger.handlers and isinstance(translater_logger.handlers[0], QueueAndHistoryHandler):
# Use the existing handler to format and queue/store the log
record = logging.LogRecord(
name=translater_logger.name, level=logging.INFO, pathname="", lineno=0, name=translater_logger.name, level=logging.INFO, pathname="", lineno=0,
msg=f"收到新的翻译请求: {original_filename_for_init}", args=[], exc_info=None, func="" msg=initial_log_msg, args=(), exc_info=None, func=""
))) )
translater_logger.handlers[0].emit(record) # This will add to both queue and history
else: # Fallback if handler setup is unusual
translater_logger.info(initial_log_msg)
try: try:
file_contents = await file.read() file_contents = await file.read()
original_filename = file.filename # Use the actual filename (already checked it's not None) original_filename = file.filename
await file.close() await file.close()
task_params = { task_params = {
@@ -944,10 +962,10 @@ async def handle_translate(
return JSONResponse(content={"task_started": True, "message": "翻译任务已成功启动,请稍候..."}) return JSONResponse(content={"task_started": True, "message": "翻译任务已成功启动,请稍候..."})
except Exception as e: except Exception as e:
translater_logger.error(f"启动翻译任务失败: {e}", exc_info=True) translater_logger.error(f"启动翻译任务失败: {e}", exc_info=True)
current_state["is_processing"] = False # Reset processing flag current_state["is_processing"] = False
current_state["status_message"] = f"启动任务失败: {e}" current_state["status_message"] = f"启动任务失败: {e}"
current_state["error_flag"] = True current_state["error_flag"] = True
current_state["current_task_ref"] = None # Ensure task ref is cleared current_state["current_task_ref"] = None
return JSONResponse(status_code=500, content={"task_started": False, "message": f"启动翻译任务时出错: {e}"}) return JSONResponse(status_code=500, content={"task_started": False, "message": f"启动翻译任务时出错: {e}"})
@@ -963,8 +981,7 @@ async def cancel_translate_task():
task_to_cancel: Optional[asyncio.Task] = current_state["current_task_ref"] task_to_cancel: Optional[asyncio.Task] = current_state["current_task_ref"]
if not task_to_cancel or task_to_cancel.done(): if not task_to_cancel or task_to_cancel.done():
# Task might have finished or been cancelled just before this request arrived current_state["is_processing"] = False
current_state["is_processing"] = False # Ensure state consistency
current_state["current_task_ref"] = None current_state["current_task_ref"] = None
return JSONResponse( return JSONResponse(
status_code=400, status_code=400,
@@ -973,24 +990,17 @@ async def cancel_translate_task():
translater_logger.info("收到取消翻译任务的请求。") translater_logger.info("收到取消翻译任务的请求。")
task_to_cancel.cancel() task_to_cancel.cancel()
current_state["status_message"] = "正在取消任务..." # Optimistic update current_state["status_message"] = "正在取消任务..."
try: try:
# Give the task a moment to process cancellation
await asyncio.wait_for(task_to_cancel, timeout=2.0) await asyncio.wait_for(task_to_cancel, timeout=2.0)
except asyncio.CancelledError: except asyncio.CancelledError:
translater_logger.info("任务已成功取消并结束。") translater_logger.info("任务已成功取消并结束。")
# State update (is_processing=False, status_message="已取消") is handled by _perform_translation's finally/except block
except asyncio.TimeoutError: except asyncio.TimeoutError:
translater_logger.warning("任务取消请求已发送但任务未在2秒内结束。可能仍在清理中。") translater_logger.warning("任务取消请求已发送但任务未在2秒内结束。可能仍在清理中。")
# The task is cancelled, but it might take longer. Frontend polling will get the final state.
except Exception as e: except Exception as e:
# This might happen if the task errored out while we were waiting for it after cancellation.
translater_logger.error(f"等待任务取消时发生意外错误: {e}") translater_logger.error(f"等待任务取消时发生意外错误: {e}")
# The task's own error handling should manage state.
# The final state (is_processing=False, specific status message) will be set by _perform_translation.
# This endpoint just initiates the cancellation.
return JSONResponse(content={"cancelled": True, "message": "取消请求已发送。请等待状态更新。"}) return JSONResponse(content={"cancelled": True, "message": "取消请求已发送。请等待状态更新。"})
@@ -1018,12 +1028,19 @@ async def get_status():
@app.get("/get-logs") @app.get("/get-logs")
async def get_logs(since: int = 0): async def get_logs_from_queue(): # Renamed for clarity, though path is the same
global log_history global log_queue
# Ensure 'since' is within bounds new_logs = []
since = max(0, min(since, len(log_history))) if log_queue: # Ensure log_queue is initialized
new_logs = log_history[since:] while not log_queue.empty():
return JSONResponse(content={"logs": new_logs, "total_count": len(log_history)}) try:
log_entry = log_queue.get_nowait() # Consume from queue
new_logs.append(log_entry)
log_queue.task_done() # Important for queue management if using join() elsewhere
except asyncio.QueueEmpty:
break
# No total_count, as the frontend just appends what it receives
return JSONResponse(content={"logs": new_logs})
@app.get("/download/markdown/{filename_with_ext}") @app.get("/download/markdown/{filename_with_ext}")
@@ -1032,7 +1049,6 @@ async def download_markdown(filename_with_ext: str):
"original_filename_stem"]: "original_filename_stem"]:
raise HTTPException(status_code=404, detail="Markdown 内容尚未准备好或不可用。") raise HTTPException(status_code=404, detail="Markdown 内容尚未准备好或不可用。")
# Basic check to prevent arbitrary filename access, though content is from current_state
requested_stem = Path(filename_with_ext).stem.replace("_translated", "") requested_stem = Path(filename_with_ext).stem.replace("_translated", "")
if requested_stem != current_state["original_filename_stem"]: if requested_stem != current_state["original_filename_stem"]:
raise HTTPException(status_code=404, detail="请求的文件名与当前结果不符。") raise HTTPException(status_code=404, detail="请求的文件名与当前结果不符。")
@@ -1058,8 +1074,8 @@ async def download_html(filename_with_ext: str):
actual_filename = f"{current_state['original_filename_stem']}_translated.html" actual_filename = f"{current_state['original_filename_stem']}_translated.html"
return HTMLResponse( return HTMLResponse(
content=current_state["html_content"], content=current_state["html_content"],
media_type="text/html", # For direct viewing, browser decides on download based on Content-Disposition media_type="text/html",
headers={"Content-Disposition": f"attachment; filename=\"{actual_filename}\""} # Prompts download headers={"Content-Disposition": f"attachment; filename=\"{actual_filename}\""}
) )

View File

@@ -1,6 +1,6 @@
[project] [project]
name = "docutranslate" name = "docutranslate"
version = "0.2.9" version = "0.2.10"
description = "文件翻译工具" description = "文件翻译工具"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.10"