feat:自动根据环境变量填写前端输入项,支持术语表与领域知识

This commit is contained in:
r-earth-or
2026-03-04 15:28:28 +08:00
committed by toy
parent 9e82daa2a1
commit 47a3e9126a
13 changed files with 150 additions and 276 deletions

29
.env.example Normal file
View File

@@ -0,0 +1,29 @@
# DocuTranslate 环境变量配置示例
# 复制此文件为 .env 并按需修改(需配合 python-dotenv 或容器环境变量使用)
# --- 代理配置 ---
# 是否启用系统代理,设置为 true 开启
# DOCUTRANSLATE_PROXY_ENABLED=true
# --- 缓存配置 ---
# 任务缓存数量(默认 10
# DOCUTRANSLATE_CACHE_NUM=10
# --- 翻译 API 默认配置 ---
# 前端"自定义接口-default"平台的默认值,留空则不预填
# API 地址Base URL例如 https://api.openai.com/v1
DOCUTRANSLATE_BASE_URL=
# API 密钥
DOCUTRANSLATE_API_KEY=
# 模型 ID例如 qwen-mt-turbo
DOCUTRANSLATE_MODEL_ID=
# --- 限流配置 ---
# RPM 限制(每分钟请求数),留空则不限制
DOCUTRANSLATE_RPM=
# TPM 限制(每分钟 Token 数),留空则不限制
DOCUTRANSLATE_TPM=

3
.gitignore vendored
View File

@@ -14,3 +14,6 @@ docutranslate/output/
.idea/
#claude
.claude/
/.omc/
# Environment variables
.env

View File

@@ -59,6 +59,7 @@ class AgentConfig:
rpm: int | None = None # 每分钟请求数限制
tpm: int | None = None # 每分钟Token数限制
provider: ProviderType | None = None
source_lang: str | None = None # qwen-mt: 源语言
class TotalErrorCounter:
@@ -534,9 +535,10 @@ class Agent:
self.provider = config.provider if config.provider is not None else get_provider_by_domain(self.domain)
self.is_mt_mode = "mt" in self.model_id.lower()
self.mt_source_lang = getattr(config, "source_lang", "auto")
self.mt_source_lang = config.source_lang if config.source_lang else "auto"
self.mt_target_lang = getattr(config, "to_lang", None)
self.mt_domains = getattr(config, "custom_prompt", None)
self.mt_glossary_dict = getattr(config, "glossary_dict", None)
def _estimate_tokens(self, text: str) -> int:
"""
@@ -588,7 +590,7 @@ class Agent:
return _MT_LANG_ALIASES[key]
return lang_text
def _build_mt_translation_options(self) -> dict:
def _build_mt_translation_options(self, prompt: str = "") -> dict:
translation_options = {}
source_lang = self._normalize_mt_lang(self.mt_source_lang)
@@ -603,6 +605,15 @@ class Agent:
if domains:
translation_options["domains"] = domains
if self.mt_glossary_dict:
terminology_list = [
{"source": src, "target": tgt}
for src, tgt in self.mt_glossary_dict.items()
if src and tgt and src.lower() in prompt.lower()
]
if terminology_list:
translation_options["terms"] = terminology_list
return translation_options
def _build_mt_user_prompt(self, prompt: str, system_prompt: str) -> str:
@@ -627,7 +638,7 @@ class Agent:
{"role": "user", "content": self._build_mt_user_prompt(prompt, system_prompt)},
],
}
translation_options = self._build_mt_translation_options()
translation_options = self._build_mt_translation_options(prompt=prompt)
if translation_options:
data["translation_options"] = translation_options
return headers, data

View File

@@ -2472,6 +2472,25 @@ async def service_flat_translate(
})
@app.get("/api/config", tags=["Config"], summary="获取服务端环境变量默认配置")
async def get_config():
"""返回由服务端环境变量预设的前端默认配置不含敏感信息API key 仅返回是否存在)。"""
from docutranslate.environment import (
DOCUTRANSLATE_BASE_URL,
DOCUTRANSLATE_API_KEY,
DOCUTRANSLATE_MODEL_ID,
DOCUTRANSLATE_RPM,
DOCUTRANSLATE_TPM,
)
return JSONResponse({
"base_url": DOCUTRANSLATE_BASE_URL,
"api_key": DOCUTRANSLATE_API_KEY,
"model_id": DOCUTRANSLATE_MODEL_ID,
"rpm": DOCUTRANSLATE_RPM,
"tpm": DOCUTRANSLATE_TPM,
})
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def main_page():
index_path = Path(STATIC_DIR) / "index.html"

View File

@@ -60,7 +60,7 @@ def create_workflow_from_payload(payload: TranslatePayload, logger: logging.Logg
# 1. Markdown Based Workflow
if isinstance(payload, MarkdownWorkflowParams):
translator_args = payload.model_dump(
include={"skip_translate", "base_url", "api_key", "model_id", "to_lang", "custom_prompt",
include={"skip_translate", "base_url", "api_key", "model_id", "to_lang", "source_lang", "custom_prompt",
"temperature", "thinking", "chunk_size", "concurrent", "glossary_dict", "timeout",
"retry", "system_proxy_enable", "force_json", "rpm", "tpm", "provider"},
exclude_none=True,

View File

@@ -157,6 +157,9 @@ class BaseWorkflowParams(BaseModel):
custom_prompt: Optional[str] = Field(
default="", description="用户自定义的翻译Prompt。", alias="custom_prompt"
)
source_lang: Optional[str] = Field(
default=None, description="源语言qwen-mt系列模型专用'Chinese''English')。", examples=[None]
)
glossary_dict: Optional[Dict[str, str]] = Field(
None, description="术语表字典key为原文value为译文。", examples=[None]
)

View File

@@ -0,0 +1,41 @@
# SPDX-FileCopyrightText: 2025 QinHan
# SPDX-License-Identifier: MPL-2.0
"""
集中管理所有环境变量。
所有 os.getenv() 调用应在此处统一声明,其他模块从这里导入。
"""
import os
from dotenv import load_dotenv
load_dotenv() # 自动从项目根目录的 .env 文件加载环境变量(不覆盖已有的 shell 变量)
# --- 代理配置 ---
# 是否启用系统代理,设置为 "true" 开启
DOCUTRANSLATE_PROXY_ENABLED: bool = (
os.getenv("DOCUTRANSLATE_PROXY_ENABLED", "").lower() == "true"
)
# --- 缓存配置 ---
# 任务缓存数量
DOCUTRANSLATE_CACHE_NUM: int = int(os.getenv("DOCUTRANSLATE_CACHE_NUM", "10"))
# --- 翻译 API 默认配置 ---
# 默认 API 地址 (自定义接口的 Base URL)
DOCUTRANSLATE_BASE_URL: str = os.getenv("DOCUTRANSLATE_BASE_URL", "")
# 默认 API 密钥
DOCUTRANSLATE_API_KEY: str = os.getenv("DOCUTRANSLATE_API_KEY", "")
# 默认模型 ID
DOCUTRANSLATE_MODEL_ID: str = os.getenv("DOCUTRANSLATE_MODEL_ID", "")
# --- 限流默认配置 ---
# 默认 RPM 限制 (Requests Per Minute),不设置则不限制
_rpm_str = os.getenv("DOCUTRANSLATE_RPM", "")
DOCUTRANSLATE_RPM: int | None = int(_rpm_str) if _rpm_str.strip() else None
# 默认 TPM 限制 (Tokens Per Minute),不设置则不限制
_tpm_str = os.getenv("DOCUTRANSLATE_TPM", "")
DOCUTRANSLATE_TPM: int | None = int(_tpm_str) if _tpm_str.strip() else None

View File

@@ -1,16 +1,9 @@
# SPDX-FileCopyrightText: 2025 QinHan
# SPDX-License-Identifier: MPL-2.0
import os
from docutranslate.environment import DOCUTRANSLATE_PROXY_ENABLED
from .conditional_import import available_packages, conditional_import
USE_PROXY = (
True
if (
os.getenv("DOCUTRANSLATE_PROXY_ENABLED")
and os.getenv("DOCUTRANSLATE_PROXY_ENABLED").lower() == "true"
)
else False
)
USE_PROXY = DOCUTRANSLATE_PROXY_ENABLED
if USE_PROXY:
print(f"USE_PROXY:{USE_PROXY}")

View File

@@ -198,6 +198,7 @@ class Client:
retry: Optional[int] = None,
thinking: Optional[ThinkingMode] = None,
custom_prompt: Optional[str] = None,
source_lang: Optional[str] = None,
system_proxy_enable: Optional[bool] = None,
force_json: Optional[bool] = None,
rpm: Optional[int] = None,
@@ -264,6 +265,7 @@ class Client:
retry: Optional[int] = None,
thinking: Optional[ThinkingMode] = None,
custom_prompt: Optional[str] = None,
source_lang: Optional[str] = None,
system_proxy_enable: Optional[bool] = None,
force_json: Optional[bool] = None,
rpm: Optional[int] = None,

View File

@@ -596,6 +596,7 @@
<option value="Portuguese">葡萄牙文(Português)</option>
<option value="Arabic">阿拉伯文(العَرَبِيَّة)</option>
<option value="Vietnamese">越南文(tiếng Việt)</option>
<option value="Indonesian">印尼文(Bahasa Indonesia)</option>
<option value="custom">{{ t('targetLanguageCustom') }}</option>
</select>
<div class="mt-2" v-if="form.to_lang === 'custom'">
@@ -673,7 +674,7 @@
<h2 class="accordion-header">
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse"
data-bs-target="#collapseGlossary">
<strong><span class="step-number">{{ stepMap.glossary }} </span><i
<strong><i
class="bi bi-journal-bookmark me-2"></i><span>{{ t('glossaryGenTitle')
}}</span></strong>
</button>
@@ -698,148 +699,6 @@
</button>
</div>
</div>
<div class="form-check form-switch mb-3 border-top pt-3">
<input class="form-check-input" type="checkbox" role="switch"
v-model="form.glossary_generate_enable"
@change="saveSetting('glossary_generate_enable', form.glossary_generate_enable)">
<label class="form-check-label">{{ t('glossaryGenEnableLabel') }}</label>
</div>
<div v-if="form.glossary_generate_enable">
<div class="mb-3">
<label class="form-label">{{ t('glossaryCustomPromptLabel') }}</label>
<textarea class="form-control"
v-model="form.glossary_agent_custom_prompt"
@input="saveSetting('glossary_agent_custom_prompt', form.glossary_agent_custom_prompt)"
rows="3"
:placeholder="t('glossaryCustomPromptPlaceholder')"></textarea>
</div>
<div class="mb-3">
<label class="form-label">{{ t('glossaryGenConfigLabel') }}</label>
<div class="btn-group w-100">
<input type="radio" class="btn-check" value="same" id="gSame"
v-model="form.glossary_agent_config_choice"
@change="saveSetting('glossary_agent_config_choice', 'same')">
<label class="btn btn-outline-primary"
for="gSame">{{ t('glossaryGenConfigSame') }}</label>
<input type="radio" class="btn-check" value="custom" id="gCustom"
v-model="form.glossary_agent_config_choice"
@change="saveSetting('glossary_agent_config_choice', 'custom')">
<label class="btn btn-outline-primary"
for="gCustom">{{ t('glossaryGenConfigCustom') }}</label>
</div>
</div>
<div v-if="form.glossary_agent_config_choice === 'custom'"
class="border p-3 rounded">
<platform-selector
v-model:platform="form.glossary_agent_platform"
v-model:base-url="form.glossary_agent_baseurl"
v-model:api-key="form.glossary_agent_key"
v-model:model-id="form.glossary_agent_model_id"
v-model:provider="form.glossary_agent_provider"
:t="t" prefix="glossary_agent_platform"></platform-selector>
<div class="mb-3">
<label class="form-label">{{ t('targetLanguageLabel') }}</label>
<select class="form-select" v-model="form.glossary_agent_to_lang"
@change="saveSetting('glossary_agent_to_lang', form.glossary_agent_to_lang)">
<option value="Simplified Chinese">中文(简体中文)</option>
<option value="English">英文(English)</option>
<option value="Spanish">西班牙文(Español)</option>
<option value="French">法文(Français)</option>
<option value="German">德文(Deutsch)</option>
<option value="Japanese">日文(日本語)</option>
<option value="Korean">韩文(한국어)</option>
<option value="Russian">俄文(Русский)</option>
<option value="Portuguese">葡萄牙文(Português)</option>
<option value="Arabic">阿拉伯文(العَرَبِيَّة)</option>
<option value="Vietnamese">越南文(tiếng Việt)</option>
<option value="custom">{{ t('targetLanguageCustom') }}</option>
</select>
<div class="mt-2" v-if="form.glossary_agent_to_lang === 'custom'">
<input type="text" class="form-control"
v-model="form.glossary_agent_custom_to_lang"
@input="saveSetting('glossary_agent_custom_to_lang', form.glossary_agent_custom_to_lang)"
:placeholder="t('customLangPlaceholder')">
</div>
</div>
<slider-control :label="t('chunkSizeLabel')"
v-model="form.glossary_agent_chunk_size"
save-key="glossary_agent_chunk_size"
:default-val="defaultParams.chunk_size" :min="1000"
:max="8000" :step="100" :t="t"></slider-control>
<slider-control :label="t('concurrentLabel')"
v-model="form.glossary_agent_concurrent"
save-key="glossary_agent_concurrent"
:default-val="defaultParams.concurrent" :min="1"
:max="120" :step="1" :t="t"></slider-control>
<slider-control label="Temperature"
v-model="form.glossary_agent_temperature"
save-key="glossary_agent_temperature" :default-val="0.7"
:min="0" :max="2" :step="0.1" :t="t"></slider-control>
<slider-control :label="t('retryLabel')"
v-model="form.glossary_agent_retry"
save-key="glossary_agent_retry"
:default-val="defaultParams.retry" :min="1" :max="6"
:step="1" :t="t"></slider-control>
<!-- Glossary Agent RPM/TPM [Vertical Layout] -->
<div class="mb-3">
<label class="form-label">RPM <small
class="text-muted">({{ t('rpmLabel')
}})</small></label>
<input type="number" class="form-control"
v-model="form.glossary_agent_rpm"
@input="saveSetting('glossary_agent_rpm', form.glossary_agent_rpm)"
min="1" :placeholder="t('unlimitedPlaceholder')">
</div>
<div class="mb-3">
<label class="form-label">TPM <small
class="text-muted">({{ t('tpmLabel')
}})</small></label>
<input type="number" class="form-control"
v-model="form.glossary_agent_tpm"
@input="saveSetting('glossary_agent_tpm', form.glossary_agent_tpm)"
min="1" :placeholder="t('unlimitedPlaceholder')">
</div>
<div class="mb-3">
<label class="form-label">{{ t('thinkingModeLabel') }}</label>
<div class="btn-group w-100">
<input type="radio" class="btn-check" value="enable"
id="gtEnable" v-model="form.glossary_agent_thinking"
@change="saveSetting('glossary_agent_thinking_mode', 'enable')">
<label class="btn btn-outline-primary"
for="gtEnable">{{ t('thinkingModeEnable') }}</label>
<input type="radio" class="btn-check" value="disable"
id="gtDisable" v-model="form.glossary_agent_thinking"
@change="saveSetting('glossary_agent_thinking_mode', 'disable')">
<label class="btn btn-outline-primary"
for="gtDisable">{{ t('thinkingModeDisable') }}</label>
<input type="radio" class="btn-check" value="default"
id="gtDefault" v-model="form.glossary_agent_thinking"
@change="saveSetting('glossary_agent_thinking_mode', 'default')">
<label class="btn btn-outline-primary"
for="gtDefault">{{ t('thinkingModeDefault') }}</label>
</div>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" role="switch"
v-model="form.glossary_agent_system_proxy_enable"
@change="saveSetting('glossary_agent_system_proxy_enable', form.glossary_agent_system_proxy_enable)">
<label class="form-check-label">{{ t('systemProxyLabel') }}</label>
</div>
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" role="switch"
v-model="form.glossary_agent_force_json"
@change="saveSetting('glossary_agent_force_json', form.glossary_agent_force_json)">
<label class="form-check-label">{{ t('forceJson') }}</label>
</div>
</div>
</div>
</div>
</div>
</div>
@@ -1447,25 +1306,6 @@
retry: 3,
rpm: null, // New RPM
tpm: null, // New TPM
glossary_generate_enable: false,
glossary_agent_custom_prompt: '',
glossary_agent_config_choice: 'same',
glossary_agent_platform: 'https://api.302.ai/v1',
glossary_agent_baseurl: '',
glossary_agent_key: '',
glossary_agent_model_id: '',
glossary_agent_provider: 'api.openai.com', // Default glossary provider
glossary_agent_to_lang: 'Simplified Chinese',
glossary_agent_custom_to_lang: '',
glossary_agent_chunk_size: 1000,
glossary_agent_concurrent: 5,
glossary_agent_temperature: 0.7,
glossary_agent_retry: 3,
glossary_agent_thinking: 'default',
glossary_agent_system_proxy_enable: false,
glossary_agent_force_json: false,
glossary_agent_rpm: null, // New Glossary Agent RPM
glossary_agent_tpm: null // New Glossary Agent TPM
});
// Nested Params for specific workflows
@@ -1497,7 +1337,7 @@
return (v === null || v === '' || v === 'null') ? null : Number(v);
};
form.workflow_type = get('translator_last_workflow', 'markdown_based');
form.workflow_type = get('translator_last_workflow', 'docx');
form.auto_workflow_enabled = getBool('translator_auto_workflow_enabled', true);
form.convert_engine = get('translator_convert_engin', 'mineru');
form.mineru_token = get('translator_mineru_token', '');
@@ -1516,12 +1356,12 @@
form.formula_ocr = getBool('translator_formula_ocr', true);
form.code_ocr = getBool('translator_code_ocr', true);
form.skip_translate = getBool('translator_skip_translate', false);
form.platform = get('translator_platform_last_platform', 'https://api.302.ai/v1');
form.platform = get('translator_platform_last_platform', 'custom');
form.system_proxy_enable = getBool('translator_system_proxy_enable', false);
form.force_json = getBool('translator_force_json', false);
form.to_lang = get('translator_to_lang', 'Simplified Chinese');
form.custom_to_lang = get('translator_custom_to_lang', '');
form.thinking = get('translator_thinking_mode', 'disable');
form.thinking = get('translator_thinking_mode', 'default');
form.custom_prompt = get('custom_prompt', '');
form.chunk_size = getNum('chunk_size', 1000);
form.concurrent = getNum('concurrent', 5);
@@ -1539,30 +1379,6 @@
form.provider = platObj ? platObj.provider : '';
}
form.glossary_generate_enable = getBool('glossary_generate_enable', false);
form.glossary_agent_custom_prompt = get('glossary_agent_custom_prompt', '');
form.glossary_agent_config_choice = get('glossary_agent_config_choice', 'same');
form.glossary_agent_platform = get('glossary_agent_platform_last_platform', 'https://api.302.ai/v1');
form.glossary_agent_to_lang = get('glossary_agent_to_lang', 'Simplified Chinese');
form.glossary_agent_custom_to_lang = get('glossary_agent_custom_to_lang', '');
form.glossary_agent_chunk_size = getNum('glossary_agent_chunk_size', 1000);
form.glossary_agent_concurrent = getNum('glossary_agent_concurrent', 5);
form.glossary_agent_temperature = getNum('glossary_agent_temperature', 0.7);
form.glossary_agent_retry = getNum('glossary_agent_retry', 3);
form.glossary_agent_thinking = get('glossary_agent_thinking_mode', 'default');
form.glossary_agent_system_proxy_enable = getBool('glossary_agent_system_proxy_enable', false);
form.glossary_agent_force_json = getBool('glossary_agent_force_json', false);
form.glossary_agent_rpm = getNumOrNull('glossary_agent_rpm'); // Load Glossary RPM
form.glossary_agent_tpm = getNumOrNull('glossary_agent_tpm'); // Load Glossary TPM
// Determine Glossary Provider
const gPlatObj = KNOWN_PLATFORMS.find(p => p.val === form.glossary_agent_platform);
if (form.glossary_agent_platform === 'custom') {
form.glossary_agent_provider = get('glossary_agent_platform_custom_provider', 'default');
} else {
form.glossary_agent_provider = gPlatObj ? gPlatObj.provider : '';
}
// Restore workflow specific params
['txt', 'xlsx', 'docx', 'srt', 'epub', 'html', 'ass', 'pptx'].forEach(t => {
workflowParams[t].insert_mode = get(`translator_${t}_insert_mode`, 'replace');
@@ -1574,7 +1390,6 @@
// Trigger platform updates to load API keys/models
updatePlatformParams(form.platform, 'translator_platform', form);
updatePlatformParams(form.glossary_agent_platform, 'glossary_agent_platform', form, true);
};
// --- 新增:专门用于将当前 form 数据全部写入 localStorage 的函数 ---
@@ -1626,30 +1441,7 @@
s('translator_provider', f.provider);
if (f.platform === 'custom') s('translator_platform_custom_base_url', f.base_url);
// 2. 术语表相关
s('glossary_generate_enable', f.glossary_generate_enable);
s('glossary_agent_custom_prompt', f.glossary_agent_custom_prompt);
s('glossary_agent_config_choice', f.glossary_agent_config_choice);
s('glossary_agent_platform_last_platform', f.glossary_agent_platform);
s('glossary_agent_to_lang', f.glossary_agent_to_lang);
s('glossary_agent_custom_to_lang', f.glossary_agent_custom_to_lang);
s('glossary_agent_chunk_size', f.glossary_agent_chunk_size);
s('glossary_agent_concurrent', f.glossary_agent_concurrent);
s('glossary_agent_temperature', f.glossary_agent_temperature);
s('glossary_agent_retry', f.glossary_agent_retry);
s('glossary_agent_thinking_mode', f.glossary_agent_thinking);
s('glossary_agent_system_proxy_enable', f.glossary_agent_system_proxy_enable);
s('glossary_agent_force_json', f.glossary_agent_force_json);
s('glossary_agent_rpm', f.glossary_agent_rpm || '');
s('glossary_agent_tpm', f.glossary_agent_tpm || '');
// 术语表平台 Key
s(`glossary_agent_platform_${f.glossary_agent_platform}_apikey`, f.glossary_agent_key);
s(`glossary_agent_platform_${f.glossary_agent_platform}_model_id`, f.glossary_agent_model_id);
s('glossary_agent_provider', f.glossary_agent_provider);
if (f.glossary_agent_platform === 'custom') s('glossary_agent_platform_custom_base_url', f.glossary_agent_baseurl);
// 3. 自动循环保存所有具体工作流参数 (txt, docx, xlsx...)
// 2. 自动循环保存所有具体工作流参数 (txt, docx, xlsx...)
for (const [wfType, params] of Object.entries(workflowParams)) {
for (const [key, val] of Object.entries(params)) {
s(`translator_${wfType}_${key}`, val);
@@ -1657,27 +1449,17 @@
}
};
const updatePlatformParams = (plat, prefix, target, isGlossary = false) => {
const updatePlatformParams = (plat, prefix, target) => {
const get = (k) => localStorage.getItem(k) || '';
if (isGlossary) {
target.glossary_agent_key = get(`${prefix}_${plat}_apikey`);
target.glossary_agent_model_id = get(`${prefix}_${plat}_model_id`);
target.glossary_agent_baseurl = plat === 'custom' ? get(`${prefix}_custom_base_url`) : plat;
} else {
target.api_key = get(`${prefix}_${plat}_apikey`);
target.model_id = get(`${prefix}_${plat}_model_id`);
target.base_url = plat === 'custom' ? get(`${prefix}_custom_base_url`) : plat;
}
target.api_key = get(`${prefix}_${plat}_apikey`);
target.model_id = get(`${prefix}_${plat}_model_id`);
target.base_url = plat === 'custom' ? get(`${prefix}_custom_base_url`) : plat;
};
watch(() => form.platform, (n) => {
updatePlatformParams(n, 'translator_platform', form);
});
watch(() => form.glossary_agent_platform, (n) => {
updatePlatformParams(n, 'glossary_agent_platform', form, true);
});
const t = (k) => {
const dict = i18nData.value[currentLang.value] || i18nData.value['zh'] || {};
return dict[k] || k;
@@ -1751,12 +1533,11 @@
// Dynamic Step Numbering
const stepMap = computed(() => {
let step = 2;
const map = {specific: 0, parsing: 0, ai: 0, trans: 0, glossary: 0};
const map = {specific: 0, parsing: 0, ai: 0, trans: 0};
if (currentWorkflowConfig.value) map.specific = step++;
if (form.workflow_type === 'markdown_based') map.parsing = step++;
map.ai = step++;
if (!form.skip_translate) map.trans = step++;
map.glossary = step++;
return map;
});
@@ -1875,33 +1656,11 @@
glossary_dict: Object.keys(glossaryData.value).length ? glossaryData.value : null,
system_proxy_enable: form.system_proxy_enable,
force_json: form.force_json,
glossary_generate_enable: form.glossary_generate_enable,
workflow_type: form.workflow_type,
rpm: emptyToNull(form.rpm),
tpm: emptyToNull(form.tpm)
};
// Agent Config
if (basePayload.glossary_generate_enable) {
const isCustom = form.glossary_agent_config_choice === 'custom';
basePayload.glossary_agent_config = {
base_url: isCustom ? emptyToNull(form.glossary_agent_baseurl) : basePayload.base_url,
api_key: isCustom ? (form.glossary_agent_key || "") : basePayload.api_key,
model_id: isCustom ? emptyToNull(form.glossary_agent_model_id) : basePayload.model_id,
provider: isCustom ? emptyToNull(form.glossary_agent_provider) : basePayload.provider, // Add provider
to_lang: isCustom ? (form.glossary_agent_to_lang === 'custom' ? form.glossary_agent_custom_to_lang : form.glossary_agent_to_lang) : basePayload.to_lang,
custom_prompt: emptyToNull(form.glossary_agent_custom_prompt),
temperature: isCustom ? Number(form.glossary_agent_temperature) : basePayload.temperature,
concurrent: isCustom ? Number(form.glossary_agent_concurrent) : basePayload.concurrent,
retry: isCustom ? Number(form.glossary_agent_retry) : basePayload.retry,
thinking: isCustom ? form.glossary_agent_thinking : basePayload.thinking,
system_proxy_enable: isCustom ? form.glossary_agent_system_proxy_enable : basePayload.system_proxy_enable,
chunk_size: isCustom ? Number(form.glossary_agent_chunk_size) : basePayload.chunk_size,
force_json: isCustom ? form.glossary_agent_force_json : basePayload.force_json,
rpm: isCustom ? emptyToNull(form.glossary_agent_rpm) : basePayload.rpm,
tpm: isCustom ? emptyToNull(form.glossary_agent_tpm) : basePayload.tpm
};
}
// Specific Workflow Params
if (form.workflow_type === 'markdown_based') {
@@ -2377,13 +2136,31 @@
// Backend Metadata
try {
const [metaRes, enginRes, paramsRes] = await Promise.all([
fetch("/service/meta"), fetch('/service/engin-list'), fetch("/service/default-params")
const [metaRes, enginRes, paramsRes, configRes] = await Promise.all([
fetch("/service/meta"), fetch('/service/engin-list'), fetch("/service/default-params"),
fetch("/api/config")
]);
const meta = await metaRes.json();
version.value = meta.version;
enginList.value = await enginRes.json();
Object.assign(defaultParams, await paramsRes.json());
const envConfig = await configRes.json().catch(() => ({}));
// 将服务端环境变量作为 localStorage 未设置时的回退默认值
if (envConfig.base_url && !localStorage.getItem('translator_platform_custom_base_url')) {
localStorage.setItem('translator_platform_custom_base_url', envConfig.base_url);
}
if (envConfig.api_key && !localStorage.getItem('translator_platform_custom_apikey')) {
localStorage.setItem('translator_platform_custom_apikey', envConfig.api_key);
}
if (envConfig.model_id && !localStorage.getItem('translator_platform_custom_model_id')) {
localStorage.setItem('translator_platform_custom_model_id', envConfig.model_id);
}
if (envConfig.rpm != null && !localStorage.getItem('rpm')) {
localStorage.setItem('rpm', String(envConfig.rpm));
}
if (envConfig.tpm != null && !localStorage.getItem('tpm')) {
localStorage.setItem('tpm', String(envConfig.tpm));
}
} catch (e) {
console.error("Backend init failed", e);
}

View File

@@ -31,6 +31,7 @@ class MDTranslator(AiTranslator):
if not self.skip_translate:
agent_config = MDTranslateAgentConfig(custom_prompt=config.custom_prompt,
to_lang=config.to_lang,
source_lang=config.source_lang,
base_url=config.base_url,
api_key=config.api_key,
model_id=config.model_id,

View File

@@ -22,6 +22,7 @@ dependencies = [
"pypdf>=6.4.2",
"regex>=2025.11.3",
"charset-normalizer>=3.4.4",
"python-dotenv>=1.0.0",
]
dynamic = ["version"]

10
uv.lock generated
View File

@@ -383,6 +383,7 @@ dependencies = [
{ name = "pypdf" },
{ name = "pysubs2" },
{ name = "python-docx" },
{ name = "python-dotenv" },
{ name = "python-pptx" },
{ name = "regex" },
{ name = "srt" },
@@ -422,6 +423,7 @@ requires-dist = [
{ name = "pypdf", specifier = ">=6.4.2" },
{ name = "pysubs2", specifier = ">=1.8.0" },
{ name = "python-docx", specifier = ">=1.2.0" },
{ name = "python-dotenv", specifier = ">=1.0.0" },
{ name = "python-pptx", specifier = ">=1.0.2" },
{ name = "regex", specifier = ">=2025.11.3" },
{ name = "srt", specifier = ">=3.5.3" },
@@ -1738,14 +1740,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906 },
{ url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607 },
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769 },
{ url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441 },
{ url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291 },
{ url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632 },
{ url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905 },
{ url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495 },
{ url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388 },
{ url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879 },
{ url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017 },
{ url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980 },
{ url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865 },
{ url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256 },