feat:前端不显示模型api-key
隐藏GitHub链接
This commit is contained in:
@@ -56,9 +56,16 @@ from pydantic import (
|
|||||||
|
|
||||||
from docutranslate import __version__
|
from docutranslate import __version__
|
||||||
from docutranslate.agents.glossary_agent import GlossaryAgentConfig
|
from docutranslate.agents.glossary_agent import GlossaryAgentConfig
|
||||||
|
from docutranslate.core.model_presets import apply_model_preset_to_payload
|
||||||
from docutranslate.core.schemas import TranslatePayload, MarkdownWorkflowParams, TextWorkflowParams, JsonWorkflowParams, \
|
from docutranslate.core.schemas import TranslatePayload, MarkdownWorkflowParams, TextWorkflowParams, JsonWorkflowParams, \
|
||||||
XlsxWorkflowParams, DocxWorkflowParams, SrtWorkflowParams, EpubWorkflowParams, HtmlWorkflowParams, \
|
XlsxWorkflowParams, DocxWorkflowParams, SrtWorkflowParams, EpubWorkflowParams, HtmlWorkflowParams, \
|
||||||
AssWorkflowParams, PPTXWorkflowParams
|
AssWorkflowParams, PPTXWorkflowParams
|
||||||
|
from docutranslate.environment import (
|
||||||
|
DOCUTRANSLATE_RPM,
|
||||||
|
DOCUTRANSLATE_TPM,
|
||||||
|
get_default_model_preset,
|
||||||
|
get_public_model_presets,
|
||||||
|
)
|
||||||
from docutranslate.exporter.md.types import ConvertEngineType
|
from docutranslate.exporter.md.types import ConvertEngineType
|
||||||
# --- 核心代码 Imports ---
|
# --- 核心代码 Imports ---
|
||||||
from docutranslate.global_values.conditional_import import DOCLING_EXIST
|
from docutranslate.global_values.conditional_import import DOCLING_EXIST
|
||||||
@@ -1341,6 +1348,11 @@ async def _start_translation_task(
|
|||||||
file_contents: bytes,
|
file_contents: bytes,
|
||||||
original_filename: str,
|
original_filename: str,
|
||||||
):
|
):
|
||||||
|
try:
|
||||||
|
payload = apply_model_preset_to_payload(payload)
|
||||||
|
except ValueError as e:
|
||||||
|
raise HTTPException(status_code=400, detail=str(e))
|
||||||
|
|
||||||
# --- 新增: Auto 工作流路由逻辑 ---
|
# --- 新增: Auto 工作流路由逻辑 ---
|
||||||
if payload.workflow_type == "auto":
|
if payload.workflow_type == "auto":
|
||||||
detected_type = get_workflow_type_from_filename(original_filename)
|
detected_type = get_workflow_type_from_filename(original_filename)
|
||||||
@@ -2210,6 +2222,7 @@ async def service_get_app_version():
|
|||||||
async def service_flat_translate(
|
async def service_flat_translate(
|
||||||
request: Request,
|
request: Request,
|
||||||
file: UploadFile = File(..., description="要翻译的文件"),
|
file: UploadFile = File(..., description="要翻译的文件"),
|
||||||
|
model_preset: str = Form("", description="服务端模型预设ID"),
|
||||||
model_id: str = Form("", description="模型ID (例如: gpt-4o, glm-4-air),当 skip_translate=False 时必填"),
|
model_id: str = Form("", description="模型ID (例如: gpt-4o, glm-4-air),当 skip_translate=False 时必填"),
|
||||||
base_url: Optional[str] = Form("", description="LLM API 基础 URL (如不填则依赖环境变量或默认值,当 skip_translate=False 时必填)"),
|
base_url: Optional[str] = Form("", description="LLM API 基础 URL (如不填则依赖环境变量或默认值,当 skip_translate=False 时必填)"),
|
||||||
api_key: str = Form("xx", description="API Key (默认xx)"),
|
api_key: str = Form("xx", description="API Key (默认xx)"),
|
||||||
@@ -2307,6 +2320,7 @@ async def service_flat_translate(
|
|||||||
payload_dict = {
|
payload_dict = {
|
||||||
# --- 基础参数 ---
|
# --- 基础参数 ---
|
||||||
"workflow_type": workflow_type,
|
"workflow_type": workflow_type,
|
||||||
|
"model_preset": model_preset,
|
||||||
"base_url": base_url,
|
"base_url": base_url,
|
||||||
"api_key": api_key,
|
"api_key": api_key,
|
||||||
"model_id": model_id,
|
"model_id": model_id,
|
||||||
@@ -2389,6 +2403,7 @@ async def service_flat_translate(
|
|||||||
try:
|
try:
|
||||||
# 使用 TypeAdapter 进行多态校验,将扁平字典转为嵌套的 TranslatePayload 对象
|
# 使用 TypeAdapter 进行多态校验,将扁平字典转为嵌套的 TranslatePayload 对象
|
||||||
payload_obj = TypeAdapter(TranslatePayload).validate_python(payload_dict)
|
payload_obj = TypeAdapter(TranslatePayload).validate_python(payload_dict)
|
||||||
|
payload_obj = apply_model_preset_to_payload(payload_obj)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=f"参数配置校验失败: {str(e)}")
|
raise HTTPException(status_code=400, detail=f"参数配置校验失败: {str(e)}")
|
||||||
|
|
||||||
@@ -2474,18 +2489,10 @@ async def service_flat_translate(
|
|||||||
|
|
||||||
@app.get("/api/config", tags=["Config"], summary="获取服务端环境变量默认配置")
|
@app.get("/api/config", tags=["Config"], summary="获取服务端环境变量默认配置")
|
||||||
async def get_config():
|
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({
|
return JSONResponse({
|
||||||
"base_url": DOCUTRANSLATE_BASE_URL,
|
"model_presets": get_public_model_presets(),
|
||||||
"api_key": DOCUTRANSLATE_API_KEY,
|
"default_model_preset": get_default_model_preset(),
|
||||||
"model_id": DOCUTRANSLATE_MODEL_ID,
|
|
||||||
"rpm": DOCUTRANSLATE_RPM,
|
"rpm": DOCUTRANSLATE_RPM,
|
||||||
"tpm": DOCUTRANSLATE_TPM,
|
"tpm": DOCUTRANSLATE_TPM,
|
||||||
})
|
})
|
||||||
@@ -2578,4 +2585,4 @@ def run_app(host=None, port: int | None = None, enable_CORS=False,
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run_app()
|
run_app()
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import logging
|
import logging
|
||||||
|
|
||||||
from docutranslate.agents.glossary_agent import GlossaryAgentConfig
|
from docutranslate.agents.glossary_agent import GlossaryAgentConfig
|
||||||
|
from docutranslate.core.model_presets import apply_model_preset_to_payload
|
||||||
from docutranslate.core.schemas import TranslatePayload, MarkdownWorkflowParams, TextWorkflowParams, JsonWorkflowParams, \
|
from docutranslate.core.schemas import TranslatePayload, MarkdownWorkflowParams, TextWorkflowParams, JsonWorkflowParams, \
|
||||||
XlsxWorkflowParams, DocxWorkflowParams, SrtWorkflowParams, EpubWorkflowParams, HtmlWorkflowParams, \
|
XlsxWorkflowParams, DocxWorkflowParams, SrtWorkflowParams, EpubWorkflowParams, HtmlWorkflowParams, \
|
||||||
AssWorkflowParams, PPTXWorkflowParams
|
AssWorkflowParams, PPTXWorkflowParams
|
||||||
@@ -48,6 +49,8 @@ def create_workflow_from_payload(payload: TranslatePayload, logger: logging.Logg
|
|||||||
"""
|
"""
|
||||||
根据扁平化的 Payload 配置对象,构建并返回对应的 Workflow 实例。
|
根据扁平化的 Payload 配置对象,构建并返回对应的 Workflow 实例。
|
||||||
"""
|
"""
|
||||||
|
payload = apply_model_preset_to_payload(payload)
|
||||||
|
|
||||||
if logger is None:
|
if logger is None:
|
||||||
logger = logging.getLogger("docutranslate.factory")
|
logger = logging.getLogger("docutranslate.factory")
|
||||||
|
|
||||||
@@ -115,7 +118,7 @@ def create_workflow_from_payload(payload: TranslatePayload, logger: logging.Logg
|
|||||||
for param_type, (TransConf, WorkConf, WorkClass, ExpConf) in mapping.items():
|
for param_type, (TransConf, WorkConf, WorkClass, ExpConf) in mapping.items():
|
||||||
if isinstance(payload, param_type):
|
if isinstance(payload, param_type):
|
||||||
# 提取通用 Translator 参数
|
# 提取通用 Translator 参数
|
||||||
dump_exclude = {"workflow_type"}
|
dump_exclude = {"workflow_type", "model_preset"}
|
||||||
# 特定类型的特殊参数需要保留,例如 json_paths, insert_mode 等
|
# 特定类型的特殊参数需要保留,例如 json_paths, insert_mode 等
|
||||||
# model_dump 会自动包含定义在 param_type 中的所有字段
|
# model_dump 会自动包含定义在 param_type 中的所有字段
|
||||||
translator_args = payload.model_dump(exclude=dump_exclude, exclude_none=True)
|
translator_args = payload.model_dump(exclude=dump_exclude, exclude_none=True)
|
||||||
@@ -140,4 +143,4 @@ def create_workflow_from_payload(payload: TranslatePayload, logger: logging.Logg
|
|||||||
|
|
||||||
return WorkClass(config=workflow_config)
|
return WorkClass(config=workflow_config)
|
||||||
|
|
||||||
raise ValueError(f"未知的 Payload 类型: {type(payload)}")
|
raise ValueError(f"未知的 Payload 类型: {type(payload)}")
|
||||||
|
|||||||
42
docutranslate/core/model_presets.py
Normal file
42
docutranslate/core/model_presets.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# SPDX-FileCopyrightText: 2025 QinHan
|
||||||
|
# SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from pydantic import TypeAdapter
|
||||||
|
|
||||||
|
from docutranslate.core.schemas import TranslatePayload
|
||||||
|
from docutranslate.environment import resolve_model_preset
|
||||||
|
|
||||||
|
|
||||||
|
def apply_model_preset_to_payload_data(
|
||||||
|
payload_data: dict[str, Any],
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
if payload_data.get("skip_translate"):
|
||||||
|
return payload_data
|
||||||
|
|
||||||
|
model_preset = str(payload_data.get("model_preset") or "").strip()
|
||||||
|
if not model_preset:
|
||||||
|
return payload_data
|
||||||
|
|
||||||
|
preset = resolve_model_preset(model_preset)
|
||||||
|
hydrated = dict(payload_data)
|
||||||
|
hydrated["base_url"] = preset["base_url"]
|
||||||
|
hydrated["api_key"] = preset["api_key"]
|
||||||
|
hydrated["model_id"] = preset["model_id"]
|
||||||
|
hydrated["provider"] = preset.get("provider")
|
||||||
|
|
||||||
|
if hydrated.get("rpm") in (None, "") and preset.get("rpm") is not None:
|
||||||
|
hydrated["rpm"] = preset["rpm"]
|
||||||
|
if hydrated.get("tpm") in (None, "") and preset.get("tpm") is not None:
|
||||||
|
hydrated["tpm"] = preset["tpm"]
|
||||||
|
|
||||||
|
return hydrated
|
||||||
|
|
||||||
|
|
||||||
|
def apply_model_preset_to_payload(payload: TranslatePayload) -> TranslatePayload:
|
||||||
|
payload_data = payload.model_dump()
|
||||||
|
hydrated_data = apply_model_preset_to_payload_data(payload_data)
|
||||||
|
if hydrated_data == payload_data:
|
||||||
|
return payload
|
||||||
|
return TypeAdapter(TranslatePayload).validate_python(hydrated_data)
|
||||||
@@ -99,6 +99,11 @@ class BaseWorkflowParams(BaseModel):
|
|||||||
default=False,
|
default=False,
|
||||||
description="是否跳过翻译步骤。如果为True,则仅执行文档解析和格式转换。",
|
description="是否跳过翻译步骤。如果为True,则仅执行文档解析和格式转换。",
|
||||||
)
|
)
|
||||||
|
model_preset: Optional[str] = Field(
|
||||||
|
default="",
|
||||||
|
description="服务端模型预设ID。设置后会由服务端从环境变量中注入模型配置。",
|
||||||
|
examples=["default"],
|
||||||
|
)
|
||||||
# 修改: 默认值改为 ""
|
# 修改: 默认值改为 ""
|
||||||
base_url: Optional[str] = Field(
|
base_url: Optional[str] = Field(
|
||||||
default="",
|
default="",
|
||||||
@@ -196,14 +201,17 @@ class BaseWorkflowParams(BaseModel):
|
|||||||
|
|
||||||
if isinstance(values, dict):
|
if isinstance(values, dict):
|
||||||
if not values.get("skip_translate"):
|
if not values.get("skip_translate"):
|
||||||
|
has_model_preset = bool(str(values.get("model_preset") or "").strip())
|
||||||
# 如果是空字符串 "" (即默认值),not "" 为 True,会触发错误,符合预期
|
# 如果是空字符串 "" (即默认值),not "" 为 True,会触发错误,符合预期
|
||||||
if not (values.get("base_url") or values.get("baseurl")):
|
if not has_model_preset and not (
|
||||||
|
values.get("base_url") or values.get("baseurl")
|
||||||
|
):
|
||||||
# Auto 模式在校验前不强制要求 base_url
|
# Auto 模式在校验前不强制要求 base_url
|
||||||
if values.get("workflow_type") != "auto":
|
if values.get("workflow_type") != "auto":
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"当 `skip_translate` 为 `False` 时, `base_url` 或 `baseurl` 字段是必须的。"
|
"当 `skip_translate` 为 `False` 时, `base_url` 或 `baseurl` 字段是必须的。"
|
||||||
)
|
)
|
||||||
if not values.get("model_id"):
|
if not has_model_preset and not values.get("model_id"):
|
||||||
if values.get("workflow_type") != "auto":
|
if values.get("workflow_type") != "auto":
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"当 `skip_translate` 为 `False` 时, `model_id` 字段是必须的。"
|
"当 `skip_translate` 为 `False` 时, `model_id` 字段是必须的。"
|
||||||
@@ -469,4 +477,4 @@ TranslatePayload = Annotated[
|
|||||||
PPTXWorkflowParams,
|
PPTXWorkflowParams,
|
||||||
],
|
],
|
||||||
Field(discriminator="workflow_type"),
|
Field(discriminator="workflow_type"),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -4,7 +4,10 @@
|
|||||||
集中管理所有环境变量。
|
集中管理所有环境变量。
|
||||||
所有 os.getenv() 调用应在此处统一声明,其他模块从这里导入。
|
所有 os.getenv() 调用应在此处统一声明,其他模块从这里导入。
|
||||||
"""
|
"""
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
|
from functools import lru_cache
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
|
|
||||||
@@ -39,3 +42,144 @@ DOCUTRANSLATE_RPM: int | None = int(_rpm_str) if _rpm_str.strip() else None
|
|||||||
# 默认 TPM 限制 (Tokens Per Minute),不设置则不限制
|
# 默认 TPM 限制 (Tokens Per Minute),不设置则不限制
|
||||||
_tpm_str = os.getenv("DOCUTRANSLATE_TPM", "")
|
_tpm_str = os.getenv("DOCUTRANSLATE_TPM", "")
|
||||||
DOCUTRANSLATE_TPM: int | None = int(_tpm_str) if _tpm_str.strip() else None
|
DOCUTRANSLATE_TPM: int | None = int(_tpm_str) if _tpm_str.strip() else None
|
||||||
|
|
||||||
|
# 模型预设配置(JSON 字符串)
|
||||||
|
DOCUTRANSLATE_MODEL_PRESETS: str = os.getenv("DOCUTRANSLATE_MODEL_PRESETS", "").strip()
|
||||||
|
|
||||||
|
# 前端默认选中的模型预设 ID
|
||||||
|
DOCUTRANSLATE_DEFAULT_MODEL_PRESET: str = os.getenv(
|
||||||
|
"DOCUTRANSLATE_DEFAULT_MODEL_PRESET", ""
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
# 兼容旧版单模型配置时的展示名称
|
||||||
|
DOCUTRANSLATE_DEFAULT_MODEL_PRESET_LABEL: str = os.getenv(
|
||||||
|
"DOCUTRANSLATE_DEFAULT_MODEL_PRESET_LABEL", "环境默认模型"
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_optional_int(value: Any) -> int | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, int):
|
||||||
|
return value
|
||||||
|
text = str(value).strip()
|
||||||
|
return int(text) if text else None
|
||||||
|
|
||||||
|
|
||||||
|
def _clean_text(value: Any) -> str:
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
return str(value).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_model_preset(preset_id: str, raw: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
base_url = _clean_text(raw.get("base_url", ""))
|
||||||
|
model_id = _clean_text(raw.get("model_id", ""))
|
||||||
|
if not base_url or not model_id:
|
||||||
|
raise ValueError(
|
||||||
|
f"模型预设 '{preset_id}' 缺少必要字段 `base_url` 或 `model_id`。"
|
||||||
|
)
|
||||||
|
|
||||||
|
api_key_env = _clean_text(raw.get("api_key_env", ""))
|
||||||
|
api_key = os.getenv(api_key_env, "").strip() if api_key_env else _clean_text(
|
||||||
|
raw.get("api_key", "")
|
||||||
|
)
|
||||||
|
if not api_key:
|
||||||
|
api_key = DOCUTRANSLATE_API_KEY.strip()
|
||||||
|
|
||||||
|
provider = _clean_text(raw.get("provider", "")) or None
|
||||||
|
description = _clean_text(raw.get("description", "")) or None
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": preset_id,
|
||||||
|
"label": _clean_text(raw.get("label") or raw.get("name") or preset_id),
|
||||||
|
"description": description,
|
||||||
|
"base_url": base_url,
|
||||||
|
"api_key": api_key or "xx",
|
||||||
|
"model_id": model_id,
|
||||||
|
"provider": provider,
|
||||||
|
"rpm": _parse_optional_int(raw.get("rpm", DOCUTRANSLATE_RPM)),
|
||||||
|
"tpm": _parse_optional_int(raw.get("tpm", DOCUTRANSLATE_TPM)),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=1)
|
||||||
|
def get_model_presets() -> dict[str, dict[str, Any]]:
|
||||||
|
presets: dict[str, dict[str, Any]] = {}
|
||||||
|
|
||||||
|
if DOCUTRANSLATE_MODEL_PRESETS:
|
||||||
|
parsed = json.loads(DOCUTRANSLATE_MODEL_PRESETS)
|
||||||
|
if isinstance(parsed, list):
|
||||||
|
for item in parsed:
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise ValueError("DOCUTRANSLATE_MODEL_PRESETS 列表项必须是对象。")
|
||||||
|
preset_id = str(item.get("id") or item.get("name") or "").strip()
|
||||||
|
if not preset_id:
|
||||||
|
raise ValueError(
|
||||||
|
"DOCUTRANSLATE_MODEL_PRESETS 的列表项必须包含 `id` 或 `name`。"
|
||||||
|
)
|
||||||
|
presets[preset_id] = _normalize_model_preset(preset_id, item)
|
||||||
|
elif isinstance(parsed, dict):
|
||||||
|
for preset_id, item in parsed.items():
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
raise ValueError("DOCUTRANSLATE_MODEL_PRESETS 对象成员必须是对象。")
|
||||||
|
normalized_id = str(preset_id).strip()
|
||||||
|
if not normalized_id:
|
||||||
|
raise ValueError(
|
||||||
|
"DOCUTRANSLATE_MODEL_PRESETS 的对象键不能是空字符串。"
|
||||||
|
)
|
||||||
|
presets[normalized_id] = _normalize_model_preset(normalized_id, item)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
"DOCUTRANSLATE_MODEL_PRESETS 必须是 JSON 对象或 JSON 数组。"
|
||||||
|
)
|
||||||
|
|
||||||
|
if not presets and DOCUTRANSLATE_BASE_URL.strip() and DOCUTRANSLATE_MODEL_ID.strip():
|
||||||
|
presets["default"] = {
|
||||||
|
"id": "default",
|
||||||
|
"label": DOCUTRANSLATE_DEFAULT_MODEL_PRESET_LABEL,
|
||||||
|
"description": None,
|
||||||
|
"base_url": DOCUTRANSLATE_BASE_URL.strip(),
|
||||||
|
"api_key": DOCUTRANSLATE_API_KEY.strip() or "xx",
|
||||||
|
"model_id": DOCUTRANSLATE_MODEL_ID.strip(),
|
||||||
|
"provider": None,
|
||||||
|
"rpm": DOCUTRANSLATE_RPM,
|
||||||
|
"tpm": DOCUTRANSLATE_TPM,
|
||||||
|
}
|
||||||
|
|
||||||
|
return presets
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_model_preset() -> str | None:
|
||||||
|
presets = get_model_presets()
|
||||||
|
if not presets:
|
||||||
|
return None
|
||||||
|
if DOCUTRANSLATE_DEFAULT_MODEL_PRESET:
|
||||||
|
if DOCUTRANSLATE_DEFAULT_MODEL_PRESET not in presets:
|
||||||
|
raise ValueError(
|
||||||
|
"DOCUTRANSLATE_DEFAULT_MODEL_PRESET 指向了不存在的模型预设。"
|
||||||
|
)
|
||||||
|
return DOCUTRANSLATE_DEFAULT_MODEL_PRESET
|
||||||
|
return next(iter(presets))
|
||||||
|
|
||||||
|
|
||||||
|
def get_public_model_presets() -> list[dict[str, str]]:
|
||||||
|
public_presets: list[dict[str, str]] = []
|
||||||
|
for preset_id, preset in get_model_presets().items():
|
||||||
|
item = {"id": preset_id, "label": preset["label"]}
|
||||||
|
if preset.get("description"):
|
||||||
|
item["description"] = preset["description"]
|
||||||
|
public_presets.append(item)
|
||||||
|
return public_presets
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_model_preset(preset_id: str) -> dict[str, Any]:
|
||||||
|
preset_key = str(preset_id or "").strip()
|
||||||
|
if not preset_key:
|
||||||
|
raise ValueError("模型预设不能为空。")
|
||||||
|
|
||||||
|
presets = get_model_presets()
|
||||||
|
if preset_key not in presets:
|
||||||
|
raise ValueError(f"未找到模型预设 '{preset_key}'。")
|
||||||
|
|
||||||
|
return dict(presets[preset_key])
|
||||||
|
|||||||
@@ -74,6 +74,10 @@
|
|||||||
"codeOcrLabel": "代码识别",
|
"codeOcrLabel": "代码识别",
|
||||||
"aiSettingsTitleText": "翻译模型",
|
"aiSettingsTitleText": "翻译模型",
|
||||||
"skipTranslationLabel": "跳过翻译",
|
"skipTranslationLabel": "跳过翻译",
|
||||||
|
"modelPresetLabel": "模型预设",
|
||||||
|
"modelPresetPlaceholder": "请选择模型预设",
|
||||||
|
"modelPresetEmpty": "请先在服务端环境变量中配置模型预设",
|
||||||
|
"modelPresetRuntimeHint": "运行时将从服务端环境变量读取供应商、模型端点与 API Key。",
|
||||||
"platformLabel": "选择平台",
|
"platformLabel": "选择平台",
|
||||||
"platformCustom": "自定义接口",
|
"platformCustom": "自定义接口",
|
||||||
"baseUrlLabel": "API 地址 (Base URL)",
|
"baseUrlLabel": "API 地址 (Base URL)",
|
||||||
@@ -247,6 +251,10 @@
|
|||||||
"codeOcrLabel": "Code Recognition",
|
"codeOcrLabel": "Code Recognition",
|
||||||
"aiSettingsTitleText": "Translation Model",
|
"aiSettingsTitleText": "Translation Model",
|
||||||
"skipTranslationLabel": "Skip Translation",
|
"skipTranslationLabel": "Skip Translation",
|
||||||
|
"modelPresetLabel": "Model Preset",
|
||||||
|
"modelPresetPlaceholder": "Select a model preset",
|
||||||
|
"modelPresetEmpty": "Configure model presets in server environment variables first",
|
||||||
|
"modelPresetRuntimeHint": "Provider, endpoint, and API key will be loaded from server environment variables at runtime.",
|
||||||
"platformLabel": "Select Platform",
|
"platformLabel": "Select Platform",
|
||||||
"platformCustom": "Custom API",
|
"platformCustom": "Custom API",
|
||||||
"baseUrlLabel": "API Address (Base URL)",
|
"baseUrlLabel": "API Address (Base URL)",
|
||||||
@@ -420,6 +428,10 @@
|
|||||||
"codeOcrLabel": "Nhận dạng mã (Code)",
|
"codeOcrLabel": "Nhận dạng mã (Code)",
|
||||||
"aiSettingsTitleText": "Mô hình dịch",
|
"aiSettingsTitleText": "Mô hình dịch",
|
||||||
"skipTranslationLabel": "Bỏ qua dịch thuật",
|
"skipTranslationLabel": "Bỏ qua dịch thuật",
|
||||||
|
"modelPresetLabel": "Mẫu mô hình",
|
||||||
|
"modelPresetPlaceholder": "Chọn mẫu mô hình",
|
||||||
|
"modelPresetEmpty": "Hãy cấu hình sẵn mẫu mô hình trong biến môi trường phía máy chủ",
|
||||||
|
"modelPresetRuntimeHint": "Nhà cung cấp, endpoint và API Key sẽ được đọc từ biến môi trường phía máy chủ khi chạy.",
|
||||||
"platformLabel": "Chọn nền tảng",
|
"platformLabel": "Chọn nền tảng",
|
||||||
"platformCustom": "API tùy chỉnh",
|
"platformCustom": "API tùy chỉnh",
|
||||||
"baseUrlLabel": "Địa chỉ API (Base URL)",
|
"baseUrlLabel": "Địa chỉ API (Base URL)",
|
||||||
|
|||||||
@@ -225,16 +225,6 @@
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<div class="d-flex align-items-center">
|
<div class="d-flex align-items-center">
|
||||||
<h4 class="mb-0 me-3 fw-bold" :title="t('pageTitle')">DocuTranslate</h4>
|
<h4 class="mb-0 me-3 fw-bold" :title="t('pageTitle')">DocuTranslate</h4>
|
||||||
<div class="btn-group">
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-info" data-bs-toggle="modal"
|
|
||||||
data-bs-target="#tutorialModal">
|
|
||||||
<i class="bi bi-question-circle-fill me-1"></i><span>{{ t('tutorialBtn') }}</span>
|
|
||||||
</button>
|
|
||||||
<button type="button" class="btn btn-sm btn-outline-warning" data-bs-toggle="modal"
|
|
||||||
data-bs-target="#contributorsModal">
|
|
||||||
<i class="bi bi-people-fill me-1"></i><span>{{ t('projectContributeBtn') }}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -536,17 +526,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="!form.skip_translate">
|
<div v-show="!form.skip_translate">
|
||||||
<platform-selector
|
<model-preset-selector
|
||||||
v-model:platform="form.platform"
|
v-model:model-preset="form.model_preset"
|
||||||
v-model:base-url="form.base_url"
|
:presets="modelPresets"
|
||||||
v-model:api-key="form.api_key"
|
:invalid-model-preset="errors.model_preset"
|
||||||
v-model:model-id="form.model_id"
|
|
||||||
v-model:provider="form.provider"
|
|
||||||
:invalid-api-key="errors.api_key"
|
|
||||||
:invalid-base-url="errors.base_url"
|
|
||||||
:invalid-model-id="errors.model_id"
|
|
||||||
@clear-error="clearError"
|
@clear-error="clearError"
|
||||||
:t="t" prefix="translator_platform"></platform-selector>
|
:t="t"></model-preset-selector>
|
||||||
|
|
||||||
<div class="form-check form-switch mb-3">
|
<div class="form-check form-switch mb-3">
|
||||||
<input class="form-check-input" type="checkbox" role="switch"
|
<input class="form-check-input" type="checkbox" role="switch"
|
||||||
@@ -717,10 +702,6 @@
|
|||||||
|
|
||||||
<!-- Project Info -->
|
<!-- Project Info -->
|
||||||
<div class="mt-4 text-center text-muted small project-info">
|
<div class="mt-4 text-center text-muted small project-info">
|
||||||
<p class="bi bi-github mb-2"> GitHub主页(欢迎star❤): <br/><a
|
|
||||||
href="https://github.com/xunbu/docutranslate" target="_blank">https://github.com/xunbu/docutranslate</a>
|
|
||||||
</p>
|
|
||||||
<p class="bi bi-tencent-qq mb-2"> 交流QQ群: 1047781902 </p>
|
|
||||||
<p class="bi mb-0">version:<span>{{ version ? 'v' + version : '' }}</span></p>
|
<p class="bi mb-0">version:<span>{{ version ? 'v' + version : '' }}</span></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -882,58 +863,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="tutorialModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header"><h5 class="modal-title"><i
|
|
||||||
class="bi bi-book-half me-2"></i>{{ t('tutorialModalTitle') }}</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body" v-html="t('tutorialModalBody')"></div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-primary" data-bs-dismiss="modal">{{ t('tutorialUnderstandBtn')
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal fade" id="contributorsModal" tabindex="-1">
|
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header"><h5 class="modal-title"><i
|
|
||||||
class="bi bi-heart-fill me-2 text-danger"></i>{{ t('contributorsModalTitle') }}</h5>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>{{ t('contributorsPara1') }}</p>
|
|
||||||
<p>{{ t('contributorsPara2') }}</p>
|
|
||||||
<div class="alert alert-success mt-4" role="alert">
|
|
||||||
<p>{{ t('contributorsWelcome') }}</p>
|
|
||||||
<hr>
|
|
||||||
<p class="mb-0">
|
|
||||||
<a href="https://github.com/xunbu/docutranslate" target="_blank"
|
|
||||||
class="btn btn-info btn-sm ms-2"><i
|
|
||||||
class="bi bi-github me-1"></i><span>{{ t('contributorsGithub') }}</span></a>
|
|
||||||
<a href="https://github.com/xunbu/docutranslate/pulls" target="_blank"
|
|
||||||
class="btn btn-success btn-sm ms-2"><i
|
|
||||||
class="bi bi-git me-1"></i><span>{{ t('contributorsPR') }}</span></a>
|
|
||||||
<a href="https://github.com/xunbu/docutranslate/issues" target="_blank"
|
|
||||||
class="btn btn-warning btn-sm ms-2"><i
|
|
||||||
class="bi bi-bug-fill me-1"></i><span>{{ t('contributorsIssue') }}</span></a>
|
|
||||||
</p>
|
|
||||||
<hr>
|
|
||||||
<p>{{ t('contributorsQQ') }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ t('closeBtn') }}</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Preview Offcanvas -->
|
<!-- Preview Offcanvas -->
|
||||||
<div class="offcanvas offcanvas-end" tabindex="-1" id="previewOffcanvas" ref="previewOffcanvas">
|
<div class="offcanvas offcanvas-end" tabindex="-1" id="previewOffcanvas" ref="previewOffcanvas">
|
||||||
<div class="offcanvas-header border-bottom">
|
<div class="offcanvas-header border-bottom">
|
||||||
@@ -1066,149 +995,31 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const KNOWN_PLATFORMS = [
|
const ModelPresetSelector = {
|
||||||
{val: "custom", label: "platformCustom", provider: "default"},
|
props: ['modelPreset', 'presets', 't', 'invalidModelPreset'],
|
||||||
{val: "https://api.302.ai/v1", label: "302.AI", provider: ""},
|
|
||||||
{val: "https://api.openai.com/v1", label: "OpenAI", provider: "default"},
|
|
||||||
{
|
|
||||||
val: "https://generativelanguage.googleapis.com/v1beta/openai/",
|
|
||||||
label: "Gemini",
|
|
||||||
provider: "google"
|
|
||||||
},
|
|
||||||
{val: "https://api.deepseek.com/v1", label: "DeepSeek", provider: ""},
|
|
||||||
{
|
|
||||||
val: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
|
||||||
label: "阿里云百炼(DashScope)",
|
|
||||||
provider: "aliyuncs"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
val: "https://ark.cn-beijing.volces.com/api/v3",
|
|
||||||
label: "火山引擎(volces)",
|
|
||||||
provider: "volces"
|
|
||||||
},
|
|
||||||
{val: "https://api.siliconflow.cn/v1", label: "硅基流动(siliconflow CN)", provider: "siliconflow"},
|
|
||||||
{val: "https://open.bigmodel.cn/api/paas/v4", label: "智谱AI(bigmodel CN)", provider: "bigmodel"},
|
|
||||||
{val: "https://www.dmxapi.cn/v1", label: "DMXAPI_CN", provider: ""},
|
|
||||||
{val: "https://www.dmxapi.com/v1", label: "DMXAPI_GLOBAL", provider: ""},
|
|
||||||
{val: "https://ai.juguang.chat/v1", label: "聚光AI(juguang CN)", provider: ""},
|
|
||||||
{val: "https://openrouter.ai/api/v1", label: "OpenRouter", provider: ""},
|
|
||||||
{val: "http://127.0.0.1:1234/v1", label: "LM Studio", provider: ""},
|
|
||||||
{val: "http://127.0.0.1:11434/v1", label: "Ollama", provider: "ollama"}
|
|
||||||
];
|
|
||||||
|
|
||||||
const PlatformSelector = {
|
|
||||||
props: ['platform', 'baseUrl', 'apiKey', 'modelId', 'provider', 't', 'prefix', 'invalidApiKey', 'invalidBaseUrl', 'invalidModelId'],
|
|
||||||
template: `
|
template: `
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-2">
|
|
||||||
<label class="form-label">{{ t('platformLabel') }}</label>
|
|
||||||
<select class="form-select" :value="platform" @change="handlePlatformChange($event.target.value)">
|
|
||||||
<option v-for="p in platforms" :value="p.val">{{ p.val === 'custom' ? t(p.label) : p.label }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="mb-3" v-if="platform === 'custom'">
|
|
||||||
<label class="form-label">{{ t('providerLabel') }}</label>
|
|
||||||
<select class="form-select" :value="provider" @change="handleProviderChange($event.target.value)">
|
|
||||||
<option v-for="prov in providers" :value="prov">{{ prov }}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-text mb-3">Base URL: <code ref="baseUrlDisplay">{{ baseUrl }}</code></div>
|
|
||||||
<div class="mb-3" v-if="platform === 'custom'">
|
|
||||||
<label class="form-label">{{ t('baseUrlLabel') }}</label>
|
|
||||||
<input type="url" class="form-control" :class="{'is-invalid': invalidBaseUrl}"
|
|
||||||
:value="baseUrl" @input="handleBaseUrlChange($event.target.value)" required
|
|
||||||
placeholder="OpenAi Compatible URL">
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label class="form-label">API Key <a v-if="apiHref" :href="apiHref[0]" target="_blank" class="ms-1"><i
|
<label class="form-label">{{ t('modelPresetLabel') }}</label>
|
||||||
class="bi bi-box-arrow-up-right"></i></a> <span
|
<select class="form-select" :class="{'is-invalid': invalidModelPreset}"
|
||||||
class="ms-2 text-muted small">{{ apiHref && apiHref[1] ? t(apiHref[1]) : '' }}</span></label>
|
:value="modelPreset" :disabled="!presets.length"
|
||||||
<div class="input-group">
|
@change="handlePresetChange($event.target.value)">
|
||||||
<input :type="showPass?'text':'password'" class="form-control" :class="{'is-invalid': invalidApiKey}"
|
<option value="" disabled>{{ presets.length ? t('modelPresetPlaceholder') : t('modelPresetEmpty') }}</option>
|
||||||
:value="apiKey" @input="handleApiKeyChange($event.target.value)"
|
<option v-for="preset in presets" :key="preset.id" :value="preset.id">{{ preset.label }}</option>
|
||||||
:placeholder="t('apiKeyPlaceholder')">
|
</select>
|
||||||
<button class="btn btn-outline-secondary" type="button" @click="showPass=!showPass"><i class="bi"
|
<div class="form-text mt-2" v-if="presets.length">{{ t('modelPresetRuntimeHint') }}</div>
|
||||||
:class="showPass?'bi-eye':'bi-eye-slash'"></i>
|
<div class="form-text mt-2" v-else>{{ t('modelPresetEmpty') }}</div>
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3">
|
|
||||||
<label class="form-label">{{ t('modelIdLabel') }}</label>
|
|
||||||
<input type="text" class="form-control" :class="{'is-invalid': invalidModelId}"
|
|
||||||
:value="modelId" @input="handleModelChange($event.target.value)" required
|
|
||||||
:placeholder="t('modelIdPlaceholder')">
|
|
||||||
</div>
|
</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
setup(props, {emit}) {
|
setup(props, {emit}) {
|
||||||
const showPass = ref(false);
|
const handlePresetChange = (val) => {
|
||||||
const platforms = KNOWN_PLATFORMS;
|
emit('update:modelPreset', val);
|
||||||
|
emit('clearError', 'model_preset');
|
||||||
// ProviderType Literal values
|
localStorage.setItem('translator_model_preset', val);
|
||||||
const providers = [
|
|
||||||
"default", "ollama", "bigmodel", "aliyuncs", "volces", "google", "siliconflow"
|
|
||||||
];
|
|
||||||
|
|
||||||
const apiHrefMap = {
|
|
||||||
"https://api.302.ai/v1": ["https://share.302.ai/BgRLAe", "apiHrefInfo302ai"],
|
|
||||||
"https://openrouter.ai/api/v1": ["https://openrouter.ai/settings/keys", null],
|
|
||||||
"https://api.openai.com/v1": ["https://platform.openai.com/api-keys", null],
|
|
||||||
"https://api.deepseek.com/v1": ["https://platform.deepseek.com/api_keys", null],
|
|
||||||
"https://open.bigmodel.cn/api/paas/v4": ["https://open.bigmodel.cn/usercenter/apikeys", null],
|
|
||||||
"https://dashscope.aliyuncs.com/compatible-mode/v1": ["https://bailian.console.aliyun.com/?tab=model#/api-key", null],
|
|
||||||
"https://ark.cn-beijing.volces.com/api/v3": ["https://console.volcengine.com/ark/region:ark+cn-beijing/apiKey?apikey=%7B%7D", null],
|
|
||||||
"https://api.siliconflow.cn/v1": ["https://cloud.siliconflow.cn/account/ak", null],
|
|
||||||
"https://ai.juguang.chat/v1": ["https://ai.juguang.chat/console/token", null],
|
|
||||||
"https://www.dmxapi.cn/v1": ["https://www.dmxapi.cn/token", null],
|
|
||||||
"https://www.dmxapi.com/v1": ["https://www.dmxapi.com/console/token", null],
|
|
||||||
"https://generativelanguage.googleapis.com/v1beta/openai/": ["https://aistudio.google.com/u/0/apikey", null]
|
|
||||||
};
|
|
||||||
const apiHref = computed(() => apiHrefMap[props.baseUrl]);
|
|
||||||
|
|
||||||
const save = (key, val) => localStorage.setItem(key, val);
|
|
||||||
const handlePlatformChange = (val) => {
|
|
||||||
emit('update:platform', val);
|
|
||||||
save(`${props.prefix}_last_platform`, val);
|
|
||||||
|
|
||||||
// Determine provider based on platform
|
|
||||||
const selected = platforms.find(p => p.val === val);
|
|
||||||
if (val === 'custom') {
|
|
||||||
emit('update:provider', 'default');
|
|
||||||
} else if (selected) {
|
|
||||||
emit('update:provider', selected.provider);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const handleBaseUrlChange = (val) => {
|
|
||||||
emit('update:baseUrl', val);
|
|
||||||
emit('clearError', 'base_url');
|
|
||||||
if (props.platform === 'custom') save(`${props.prefix}_custom_base_url`, val);
|
|
||||||
};
|
|
||||||
const handleApiKeyChange = (val) => {
|
|
||||||
emit('update:apiKey', val);
|
|
||||||
emit('clearError', 'api_key');
|
|
||||||
save(`${props.prefix}_${props.platform}_apikey`, val);
|
|
||||||
};
|
|
||||||
const handleModelChange = (val) => {
|
|
||||||
emit('update:modelId', val);
|
|
||||||
emit('clearError', 'model_id');
|
|
||||||
save(`${props.prefix}_${props.platform}_model_id`, val);
|
|
||||||
};
|
|
||||||
const handleProviderChange = (val) => {
|
|
||||||
emit('update:provider', val);
|
|
||||||
save(`${props.prefix}_${props.platform}_provider`, val);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
showPass,
|
handlePresetChange
|
||||||
platforms,
|
|
||||||
providers,
|
|
||||||
apiHref,
|
|
||||||
handlePlatformChange,
|
|
||||||
handleBaseUrlChange,
|
|
||||||
handleApiKeyChange,
|
|
||||||
handleModelChange,
|
|
||||||
handleProviderChange
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1234,7 +1045,7 @@
|
|||||||
];
|
];
|
||||||
|
|
||||||
createApp({
|
createApp({
|
||||||
components: {SliderControl, PlatformSelector},
|
components: {SliderControl, ModelPresetSelector},
|
||||||
setup() {
|
setup() {
|
||||||
const version = ref("");
|
const version = ref("");
|
||||||
const currentLang = ref(localStorage.getItem('ui_language') || 'zh');
|
const currentLang = ref(localStorage.getItem('ui_language') || 'zh');
|
||||||
@@ -1243,6 +1054,8 @@
|
|||||||
const tasks = ref([]);
|
const tasks = ref([]);
|
||||||
const enginList = ref([]);
|
const enginList = ref([]);
|
||||||
const defaultParams = reactive({});
|
const defaultParams = reactive({});
|
||||||
|
const modelPresets = ref([]);
|
||||||
|
const defaultModelPreset = ref('');
|
||||||
|
|
||||||
// Refs for DOM elements
|
// Refs for DOM elements
|
||||||
const glossaryInput = ref(null);
|
const glossaryInput = ref(null);
|
||||||
@@ -1257,9 +1070,7 @@
|
|||||||
|
|
||||||
// Validation State
|
// Validation State
|
||||||
const errors = reactive({
|
const errors = reactive({
|
||||||
model_id: false,
|
model_preset: false,
|
||||||
api_key: false,
|
|
||||||
base_url: false,
|
|
||||||
mineru_token: false,
|
mineru_token: false,
|
||||||
mineru_deploy_base_url: false,
|
mineru_deploy_base_url: false,
|
||||||
custom_to_lang: false,
|
custom_to_lang: false,
|
||||||
@@ -1289,11 +1100,7 @@
|
|||||||
formula_ocr: true,
|
formula_ocr: true,
|
||||||
code_ocr: true,
|
code_ocr: true,
|
||||||
skip_translate: false,
|
skip_translate: false,
|
||||||
platform: 'https://api.302.ai/v1',
|
model_preset: '',
|
||||||
base_url: '',
|
|
||||||
api_key: '',
|
|
||||||
model_id: '',
|
|
||||||
provider: 'api.openai.com', // Default provider
|
|
||||||
system_proxy_enable: false,
|
system_proxy_enable: false,
|
||||||
force_json: false,
|
force_json: false,
|
||||||
to_lang: 'Simplified Chinese',
|
to_lang: 'Simplified Chinese',
|
||||||
@@ -1336,6 +1143,10 @@
|
|||||||
const v = localStorage.getItem(k);
|
const v = localStorage.getItem(k);
|
||||||
return (v === null || v === '' || v === 'null') ? null : Number(v);
|
return (v === null || v === '' || v === 'null') ? null : Number(v);
|
||||||
};
|
};
|
||||||
|
const validPresetIds = modelPresets.value.map(p => p.id);
|
||||||
|
const fallbackPreset = validPresetIds.includes(defaultModelPreset.value)
|
||||||
|
? defaultModelPreset.value
|
||||||
|
: (validPresetIds[0] || '');
|
||||||
|
|
||||||
form.workflow_type = get('translator_last_workflow', 'docx');
|
form.workflow_type = get('translator_last_workflow', 'docx');
|
||||||
form.auto_workflow_enabled = getBool('translator_auto_workflow_enabled', true);
|
form.auto_workflow_enabled = getBool('translator_auto_workflow_enabled', true);
|
||||||
@@ -1356,7 +1167,7 @@
|
|||||||
form.formula_ocr = getBool('translator_formula_ocr', true);
|
form.formula_ocr = getBool('translator_formula_ocr', true);
|
||||||
form.code_ocr = getBool('translator_code_ocr', true);
|
form.code_ocr = getBool('translator_code_ocr', true);
|
||||||
form.skip_translate = getBool('translator_skip_translate', false);
|
form.skip_translate = getBool('translator_skip_translate', false);
|
||||||
form.platform = get('translator_platform_last_platform', 'custom');
|
form.model_preset = get('translator_model_preset', fallbackPreset);
|
||||||
form.system_proxy_enable = getBool('translator_system_proxy_enable', false);
|
form.system_proxy_enable = getBool('translator_system_proxy_enable', false);
|
||||||
form.force_json = getBool('translator_force_json', false);
|
form.force_json = getBool('translator_force_json', false);
|
||||||
form.to_lang = get('translator_to_lang', 'Simplified Chinese');
|
form.to_lang = get('translator_to_lang', 'Simplified Chinese');
|
||||||
@@ -1370,13 +1181,8 @@
|
|||||||
form.rpm = getNumOrNull('rpm'); // Load RPM
|
form.rpm = getNumOrNull('rpm'); // Load RPM
|
||||||
form.tpm = getNumOrNull('tpm'); // Load TPM
|
form.tpm = getNumOrNull('tpm'); // Load TPM
|
||||||
|
|
||||||
// Determine Provider
|
if (!validPresetIds.includes(form.model_preset)) {
|
||||||
const platObj = KNOWN_PLATFORMS.find(p => p.val === form.platform);
|
form.model_preset = fallbackPreset;
|
||||||
if (form.platform === 'custom') {
|
|
||||||
// 修正:读取组件实际保存的 Key (translator_platform_custom_provider)
|
|
||||||
form.provider = get('translator_platform_custom_provider', 'default');
|
|
||||||
} else {
|
|
||||||
form.provider = platObj ? platObj.provider : '';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore workflow specific params
|
// Restore workflow specific params
|
||||||
@@ -1387,9 +1193,6 @@
|
|||||||
workflowParams.txt.segment_mode = get('translator_txt_segment_mode', 'line');
|
workflowParams.txt.segment_mode = get('translator_txt_segment_mode', 'line');
|
||||||
workflowParams.xlsx.translate_regions = get('translator_xlsx_translate_regions', '');
|
workflowParams.xlsx.translate_regions = get('translator_xlsx_translate_regions', '');
|
||||||
workflowParams.json.json_paths = get('translator_json_paths', '');
|
workflowParams.json.json_paths = get('translator_json_paths', '');
|
||||||
|
|
||||||
// Trigger platform updates to load API keys/models
|
|
||||||
updatePlatformParams(form.platform, 'translator_platform', form);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- 新增:专门用于将当前 form 数据全部写入 localStorage 的函数 ---
|
// --- 新增:专门用于将当前 form 数据全部写入 localStorage 的函数 ---
|
||||||
@@ -1419,7 +1222,7 @@
|
|||||||
s('translator_formula_ocr', f.formula_ocr);
|
s('translator_formula_ocr', f.formula_ocr);
|
||||||
s('translator_code_ocr', f.code_ocr);
|
s('translator_code_ocr', f.code_ocr);
|
||||||
s('translator_skip_translate', f.skip_translate);
|
s('translator_skip_translate', f.skip_translate);
|
||||||
s('translator_platform_last_platform', f.platform);
|
s('translator_model_preset', f.model_preset);
|
||||||
s('translator_system_proxy_enable', f.system_proxy_enable);
|
s('translator_system_proxy_enable', f.system_proxy_enable);
|
||||||
s('translator_force_json', f.force_json);
|
s('translator_force_json', f.force_json);
|
||||||
s('translator_to_lang', f.to_lang);
|
s('translator_to_lang', f.to_lang);
|
||||||
@@ -1435,12 +1238,6 @@
|
|||||||
s('rpm', f.rpm || '');
|
s('rpm', f.rpm || '');
|
||||||
s('tpm', f.tpm || '');
|
s('tpm', f.tpm || '');
|
||||||
|
|
||||||
// 平台相关 (API Key 等)
|
|
||||||
s(`translator_platform_${f.platform}_apikey`, f.api_key);
|
|
||||||
s(`translator_platform_${f.platform}_model_id`, f.model_id);
|
|
||||||
s('translator_provider', f.provider);
|
|
||||||
if (f.platform === 'custom') s('translator_platform_custom_base_url', f.base_url);
|
|
||||||
|
|
||||||
// 2. 自动循环保存所有具体工作流参数 (txt, docx, xlsx...)
|
// 2. 自动循环保存所有具体工作流参数 (txt, docx, xlsx...)
|
||||||
for (const [wfType, params] of Object.entries(workflowParams)) {
|
for (const [wfType, params] of Object.entries(workflowParams)) {
|
||||||
for (const [key, val] of Object.entries(params)) {
|
for (const [key, val] of Object.entries(params)) {
|
||||||
@@ -1449,17 +1246,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const updatePlatformParams = (plat, prefix, target) => {
|
|
||||||
const get = (k) => localStorage.getItem(k) || '';
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
|
|
||||||
const t = (k) => {
|
const t = (k) => {
|
||||||
const dict = i18nData.value[currentLang.value] || i18nData.value['zh'] || {};
|
const dict = i18nData.value[currentLang.value] || i18nData.value['zh'] || {};
|
||||||
return dict[k] || k;
|
return dict[k] || k;
|
||||||
@@ -1467,6 +1253,15 @@
|
|||||||
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||||
const saveSetting = (k, v) => localStorage.setItem(k, v);
|
const saveSetting = (k, v) => localStorage.setItem(k, v);
|
||||||
const saveSettingArray = (k, v) => localStorage.setItem(k, JSON.stringify(v));
|
const saveSettingArray = (k, v) => localStorage.setItem(k, JSON.stringify(v));
|
||||||
|
const syncModelPresetSelection = () => {
|
||||||
|
const validPresetIds = modelPresets.value.map(p => p.id);
|
||||||
|
const fallbackPreset = validPresetIds.includes(defaultModelPreset.value)
|
||||||
|
? defaultModelPreset.value
|
||||||
|
: (validPresetIds[0] || '');
|
||||||
|
if (!validPresetIds.includes(form.model_preset)) {
|
||||||
|
form.model_preset = fallbackPreset;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const saveWorkflowParam = (keySuffix) => {
|
const saveWorkflowParam = (keySuffix) => {
|
||||||
const wf = form.workflow_type;
|
const wf = form.workflow_type;
|
||||||
@@ -1642,10 +1437,7 @@
|
|||||||
// Clone basic form
|
// Clone basic form
|
||||||
const basePayload = {
|
const basePayload = {
|
||||||
skip_translate: form.skip_translate,
|
skip_translate: form.skip_translate,
|
||||||
base_url: emptyToNull(form.base_url),
|
model_preset: emptyToNull(form.model_preset),
|
||||||
api_key: form.api_key || "",
|
|
||||||
model_id: emptyToNull(form.model_id),
|
|
||||||
provider: emptyToNull(form.provider), // Add provider
|
|
||||||
to_lang: form.to_lang === 'custom' ? form.custom_to_lang : form.to_lang,
|
to_lang: form.to_lang === 'custom' ? form.custom_to_lang : form.to_lang,
|
||||||
thinking: form.thinking,
|
thinking: form.thinking,
|
||||||
chunk_size: Number(form.chunk_size),
|
chunk_size: Number(form.chunk_size),
|
||||||
@@ -1718,12 +1510,8 @@
|
|||||||
Object.keys(errors).forEach(k => errors[k] = false);
|
Object.keys(errors).forEach(k => errors[k] = false);
|
||||||
|
|
||||||
if (!form.skip_translate) {
|
if (!form.skip_translate) {
|
||||||
if (!form.model_id) {
|
if (!form.model_preset) {
|
||||||
errors.model_id = true;
|
errors.model_preset = true;
|
||||||
isValid = false;
|
|
||||||
}
|
|
||||||
if (form.platform === 'custom' && !form.base_url) {
|
|
||||||
errors.base_url = true;
|
|
||||||
isValid = false;
|
isValid = false;
|
||||||
}
|
}
|
||||||
if (form.to_lang === 'custom' && !form.custom_to_lang) {
|
if (form.to_lang === 'custom' && !form.custom_to_lang) {
|
||||||
@@ -2062,6 +1850,10 @@
|
|||||||
const data = JSON.parse(ev.target.result);
|
const data = JSON.parse(ev.target.result);
|
||||||
if (data.form) Object.assign(form, data.form);
|
if (data.form) Object.assign(form, data.form);
|
||||||
if (data.workflowParams) Object.assign(workflowParams, data.workflowParams);
|
if (data.workflowParams) Object.assign(workflowParams, data.workflowParams);
|
||||||
|
['platform', 'base_url', 'api_key', 'model_id', 'provider'].forEach((key) => {
|
||||||
|
if (key in form) delete form[key];
|
||||||
|
});
|
||||||
|
syncModelPresetSelection();
|
||||||
saveAllSettings();
|
saveAllSettings();
|
||||||
alert(t('configImportSuccess'));
|
alert(t('configImportSuccess'));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -2110,6 +1902,10 @@
|
|||||||
projectContributeBtn: "项目协作",
|
projectContributeBtn: "项目协作",
|
||||||
workflowTitle: "选择工作流",
|
workflowTitle: "选择工作流",
|
||||||
autoWorkflowLabel: "自动选择工作流",
|
autoWorkflowLabel: "自动选择工作流",
|
||||||
|
modelPresetLabel: "模型预设",
|
||||||
|
modelPresetPlaceholder: "请选择模型预设",
|
||||||
|
modelPresetEmpty: "请先在服务端环境变量中配置模型预设",
|
||||||
|
modelPresetRuntimeHint: "运行时将从服务端环境变量读取供应商、模型端点与 API Key。",
|
||||||
workflowOptionPptx: "PPTX 演示文稿",
|
workflowOptionPptx: "PPTX 演示文稿",
|
||||||
pptxSettingsTitleText: "PPTX 设置",
|
pptxSettingsTitleText: "PPTX 设置",
|
||||||
mineruDeployServerUrlLabel: "Server URL",
|
mineruDeployServerUrlLabel: "Server URL",
|
||||||
@@ -2123,6 +1919,10 @@
|
|||||||
tutorialBtn: "Tutorial",
|
tutorialBtn: "Tutorial",
|
||||||
projectContributeBtn: "Contribute",
|
projectContributeBtn: "Contribute",
|
||||||
workflowTitle: "Select Workflow",
|
workflowTitle: "Select Workflow",
|
||||||
|
modelPresetLabel: "Model Preset",
|
||||||
|
modelPresetPlaceholder: "Select a model preset",
|
||||||
|
modelPresetEmpty: "Configure model presets in server environment variables first",
|
||||||
|
modelPresetRuntimeHint: "Provider, endpoint, and API key will be loaded from server environment variables at runtime.",
|
||||||
workflowOptionPptx: "PPTX Presentation",
|
workflowOptionPptx: "PPTX Presentation",
|
||||||
pptxSettingsTitleText: "PPTX Settings",
|
pptxSettingsTitleText: "PPTX Settings",
|
||||||
mineruDeployServerUrlLabel: "Server URL",
|
mineruDeployServerUrlLabel: "Server URL",
|
||||||
@@ -2145,15 +1945,10 @@
|
|||||||
enginList.value = await enginRes.json();
|
enginList.value = await enginRes.json();
|
||||||
Object.assign(defaultParams, await paramsRes.json());
|
Object.assign(defaultParams, await paramsRes.json());
|
||||||
const envConfig = await configRes.json().catch(() => ({}));
|
const envConfig = await configRes.json().catch(() => ({}));
|
||||||
// 将服务端环境变量作为 localStorage 未设置时的回退默认值
|
modelPresets.value = Array.isArray(envConfig.model_presets) ? envConfig.model_presets : [];
|
||||||
if (envConfig.base_url && !localStorage.getItem('translator_platform_custom_base_url')) {
|
defaultModelPreset.value = envConfig.default_model_preset || (modelPresets.value[0]?.id || '');
|
||||||
localStorage.setItem('translator_platform_custom_base_url', envConfig.base_url);
|
if (defaultModelPreset.value && !localStorage.getItem('translator_model_preset')) {
|
||||||
}
|
localStorage.setItem('translator_model_preset', defaultModelPreset.value);
|
||||||
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')) {
|
if (envConfig.rpm != null && !localStorage.getItem('rpm')) {
|
||||||
localStorage.setItem('rpm', String(envConfig.rpm));
|
localStorage.setItem('rpm', String(envConfig.rpm));
|
||||||
@@ -2195,6 +1990,7 @@
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
version, currentLang, i18nData, glossaryData, glossaryCount, tasks, enginList, defaultParams,
|
version, currentLang, i18nData, glossaryData, glossaryCount, tasks, enginList, defaultParams,
|
||||||
|
modelPresets,
|
||||||
form, workflowParams, showMineruToken, previewMode, syncScrollEnabled, showIdentityOption,
|
form, workflowParams, showMineruToken, previewMode, syncScrollEnabled, showIdentityOption,
|
||||||
errors, clearError,
|
errors, clearError,
|
||||||
t, createNewTask, removeTask, handleTaskFileSelect, handleTaskFileDrop, toggleTaskState,
|
t, createNewTask, removeTask, handleTaskFileSelect, handleTaskFileDrop, toggleTaskState,
|
||||||
|
|||||||
35
run.bat
Normal file
35
run.bat
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
@echo off
|
||||||
|
setlocal
|
||||||
|
|
||||||
|
REM 切换到脚本所在目录
|
||||||
|
cd /d "%~dp0"
|
||||||
|
|
||||||
|
REM 检查 .venv 是否存在
|
||||||
|
if not exist ".venv\Scripts\activate.bat" (
|
||||||
|
echo [ERROR] 未找到 .venv\Scripts\activate.bat
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
|
REM 激活虚拟环境
|
||||||
|
call ".venv\Scripts\activate.bat"
|
||||||
|
|
||||||
|
REM 如果传了参数,就用参数;否则默认用当前目录
|
||||||
|
if "%~1"=="" (
|
||||||
|
python docutranslate/cli.py -i --host 0.0.0.0
|
||||||
|
) else (
|
||||||
|
python docutranslate/cli.py -i "%~1"
|
||||||
|
)
|
||||||
|
|
||||||
|
set EXIT_CODE=%ERRORLEVEL%
|
||||||
|
|
||||||
|
REM 退出虚拟环境
|
||||||
|
call deactivate >nul 2>nul
|
||||||
|
|
||||||
|
if not "%EXIT_CODE%"=="0" (
|
||||||
|
echo.
|
||||||
|
echo [ERROR] 程序退出,返回码: %EXIT_CODE%
|
||||||
|
pause
|
||||||
|
)
|
||||||
|
|
||||||
|
exit /b %EXIT_CODE%
|
||||||
Reference in New Issue
Block a user