feat:前端不显示模型api-key
隐藏GitHub链接
This commit is contained in:
@@ -56,9 +56,16 @@ from pydantic import (
|
||||
|
||||
from docutranslate import __version__
|
||||
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, \
|
||||
XlsxWorkflowParams, DocxWorkflowParams, SrtWorkflowParams, EpubWorkflowParams, HtmlWorkflowParams, \
|
||||
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
|
||||
# --- 核心代码 Imports ---
|
||||
from docutranslate.global_values.conditional_import import DOCLING_EXIST
|
||||
@@ -1341,6 +1348,11 @@ async def _start_translation_task(
|
||||
file_contents: bytes,
|
||||
original_filename: str,
|
||||
):
|
||||
try:
|
||||
payload = apply_model_preset_to_payload(payload)
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
# --- 新增: Auto 工作流路由逻辑 ---
|
||||
if payload.workflow_type == "auto":
|
||||
detected_type = get_workflow_type_from_filename(original_filename)
|
||||
@@ -2210,6 +2222,7 @@ async def service_get_app_version():
|
||||
async def service_flat_translate(
|
||||
request: Request,
|
||||
file: UploadFile = File(..., description="要翻译的文件"),
|
||||
model_preset: str = Form("", description="服务端模型预设ID"),
|
||||
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 时必填)"),
|
||||
api_key: str = Form("xx", description="API Key (默认xx)"),
|
||||
@@ -2307,6 +2320,7 @@ async def service_flat_translate(
|
||||
payload_dict = {
|
||||
# --- 基础参数 ---
|
||||
"workflow_type": workflow_type,
|
||||
"model_preset": model_preset,
|
||||
"base_url": base_url,
|
||||
"api_key": api_key,
|
||||
"model_id": model_id,
|
||||
@@ -2389,6 +2403,7 @@ async def service_flat_translate(
|
||||
try:
|
||||
# 使用 TypeAdapter 进行多态校验,将扁平字典转为嵌套的 TranslatePayload 对象
|
||||
payload_obj = TypeAdapter(TranslatePayload).validate_python(payload_dict)
|
||||
payload_obj = apply_model_preset_to_payload(payload_obj)
|
||||
except Exception as 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="获取服务端环境变量默认配置")
|
||||
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,
|
||||
"model_presets": get_public_model_presets(),
|
||||
"default_model_preset": get_default_model_preset(),
|
||||
"rpm": DOCUTRANSLATE_RPM,
|
||||
"tpm": DOCUTRANSLATE_TPM,
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import logging
|
||||
|
||||
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, \
|
||||
XlsxWorkflowParams, DocxWorkflowParams, SrtWorkflowParams, EpubWorkflowParams, HtmlWorkflowParams, \
|
||||
AssWorkflowParams, PPTXWorkflowParams
|
||||
@@ -48,6 +49,8 @@ def create_workflow_from_payload(payload: TranslatePayload, logger: logging.Logg
|
||||
"""
|
||||
根据扁平化的 Payload 配置对象,构建并返回对应的 Workflow 实例。
|
||||
"""
|
||||
payload = apply_model_preset_to_payload(payload)
|
||||
|
||||
if logger is None:
|
||||
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():
|
||||
if isinstance(payload, param_type):
|
||||
# 提取通用 Translator 参数
|
||||
dump_exclude = {"workflow_type"}
|
||||
dump_exclude = {"workflow_type", "model_preset"}
|
||||
# 特定类型的特殊参数需要保留,例如 json_paths, insert_mode 等
|
||||
# model_dump 会自动包含定义在 param_type 中的所有字段
|
||||
translator_args = payload.model_dump(exclude=dump_exclude, exclude_none=True)
|
||||
|
||||
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,
|
||||
description="是否跳过翻译步骤。如果为True,则仅执行文档解析和格式转换。",
|
||||
)
|
||||
model_preset: Optional[str] = Field(
|
||||
default="",
|
||||
description="服务端模型预设ID。设置后会由服务端从环境变量中注入模型配置。",
|
||||
examples=["default"],
|
||||
)
|
||||
# 修改: 默认值改为 ""
|
||||
base_url: Optional[str] = Field(
|
||||
default="",
|
||||
@@ -196,14 +201,17 @@ class BaseWorkflowParams(BaseModel):
|
||||
|
||||
if isinstance(values, dict):
|
||||
if not values.get("skip_translate"):
|
||||
has_model_preset = bool(str(values.get("model_preset") or "").strip())
|
||||
# 如果是空字符串 "" (即默认值),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
|
||||
if values.get("workflow_type") != "auto":
|
||||
raise ValueError(
|
||||
"当 `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":
|
||||
raise ValueError(
|
||||
"当 `skip_translate` 为 `False` 时, `model_id` 字段是必须的。"
|
||||
|
||||
@@ -4,7 +4,10 @@
|
||||
集中管理所有环境变量。
|
||||
所有 os.getenv() 调用应在此处统一声明,其他模块从这里导入。
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
|
||||
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_str = os.getenv("DOCUTRANSLATE_TPM", "")
|
||||
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": "代码识别",
|
||||
"aiSettingsTitleText": "翻译模型",
|
||||
"skipTranslationLabel": "跳过翻译",
|
||||
"modelPresetLabel": "模型预设",
|
||||
"modelPresetPlaceholder": "请选择模型预设",
|
||||
"modelPresetEmpty": "请先在服务端环境变量中配置模型预设",
|
||||
"modelPresetRuntimeHint": "运行时将从服务端环境变量读取供应商、模型端点与 API Key。",
|
||||
"platformLabel": "选择平台",
|
||||
"platformCustom": "自定义接口",
|
||||
"baseUrlLabel": "API 地址 (Base URL)",
|
||||
@@ -247,6 +251,10 @@
|
||||
"codeOcrLabel": "Code Recognition",
|
||||
"aiSettingsTitleText": "Translation Model",
|
||||
"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",
|
||||
"platformCustom": "Custom API",
|
||||
"baseUrlLabel": "API Address (Base URL)",
|
||||
@@ -420,6 +428,10 @@
|
||||
"codeOcrLabel": "Nhận dạng mã (Code)",
|
||||
"aiSettingsTitleText": "Mô hình dịch",
|
||||
"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",
|
||||
"platformCustom": "API tùy chỉnh",
|
||||
"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 align-items-center">
|
||||
<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>
|
||||
|
||||
@@ -536,17 +526,12 @@
|
||||
</div>
|
||||
|
||||
<div v-show="!form.skip_translate">
|
||||
<platform-selector
|
||||
v-model:platform="form.platform"
|
||||
v-model:base-url="form.base_url"
|
||||
v-model:api-key="form.api_key"
|
||||
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"
|
||||
<model-preset-selector
|
||||
v-model:model-preset="form.model_preset"
|
||||
:presets="modelPresets"
|
||||
:invalid-model-preset="errors.model_preset"
|
||||
@clear-error="clearError"
|
||||
:t="t" prefix="translator_platform"></platform-selector>
|
||||
:t="t"></model-preset-selector>
|
||||
|
||||
<div class="form-check form-switch mb-3">
|
||||
<input class="form-check-input" type="checkbox" role="switch"
|
||||
@@ -717,10 +702,6 @@
|
||||
|
||||
<!-- 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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -882,58 +863,6 @@
|
||||
</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 -->
|
||||
<div class="offcanvas offcanvas-end" tabindex="-1" id="previewOffcanvas" ref="previewOffcanvas">
|
||||
<div class="offcanvas-header border-bottom">
|
||||
@@ -1066,149 +995,31 @@
|
||||
}
|
||||
};
|
||||
|
||||
const KNOWN_PLATFORMS = [
|
||||
{val: "custom", label: "platformCustom", provider: "default"},
|
||||
{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'],
|
||||
const ModelPresetSelector = {
|
||||
props: ['modelPreset', 'presets', 't', 'invalidModelPreset'],
|
||||
template: `
|
||||
<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">
|
||||
<label class="form-label">API Key <a v-if="apiHref" :href="apiHref[0]" target="_blank" class="ms-1"><i
|
||||
class="bi bi-box-arrow-up-right"></i></a> <span
|
||||
class="ms-2 text-muted small">{{ apiHref && apiHref[1] ? t(apiHref[1]) : '' }}</span></label>
|
||||
<div class="input-group">
|
||||
<input :type="showPass?'text':'password'" class="form-control" :class="{'is-invalid': invalidApiKey}"
|
||||
:value="apiKey" @input="handleApiKeyChange($event.target.value)"
|
||||
:placeholder="t('apiKeyPlaceholder')">
|
||||
<button class="btn btn-outline-secondary" type="button" @click="showPass=!showPass"><i class="bi"
|
||||
:class="showPass?'bi-eye':'bi-eye-slash'"></i>
|
||||
</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')">
|
||||
<label class="form-label">{{ t('modelPresetLabel') }}</label>
|
||||
<select class="form-select" :class="{'is-invalid': invalidModelPreset}"
|
||||
:value="modelPreset" :disabled="!presets.length"
|
||||
@change="handlePresetChange($event.target.value)">
|
||||
<option value="" disabled>{{ presets.length ? t('modelPresetPlaceholder') : t('modelPresetEmpty') }}</option>
|
||||
<option v-for="preset in presets" :key="preset.id" :value="preset.id">{{ preset.label }}</option>
|
||||
</select>
|
||||
<div class="form-text mt-2" v-if="presets.length">{{ t('modelPresetRuntimeHint') }}</div>
|
||||
<div class="form-text mt-2" v-else>{{ t('modelPresetEmpty') }}</div>
|
||||
</div>
|
||||
</div>`,
|
||||
setup(props, {emit}) {
|
||||
const showPass = ref(false);
|
||||
const platforms = KNOWN_PLATFORMS;
|
||||
|
||||
// ProviderType Literal values
|
||||
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);
|
||||
const handlePresetChange = (val) => {
|
||||
emit('update:modelPreset', val);
|
||||
emit('clearError', 'model_preset');
|
||||
localStorage.setItem('translator_model_preset', val);
|
||||
};
|
||||
|
||||
return {
|
||||
showPass,
|
||||
platforms,
|
||||
providers,
|
||||
apiHref,
|
||||
handlePlatformChange,
|
||||
handleBaseUrlChange,
|
||||
handleApiKeyChange,
|
||||
handleModelChange,
|
||||
handleProviderChange
|
||||
handlePresetChange
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -1234,7 +1045,7 @@
|
||||
];
|
||||
|
||||
createApp({
|
||||
components: {SliderControl, PlatformSelector},
|
||||
components: {SliderControl, ModelPresetSelector},
|
||||
setup() {
|
||||
const version = ref("");
|
||||
const currentLang = ref(localStorage.getItem('ui_language') || 'zh');
|
||||
@@ -1243,6 +1054,8 @@
|
||||
const tasks = ref([]);
|
||||
const enginList = ref([]);
|
||||
const defaultParams = reactive({});
|
||||
const modelPresets = ref([]);
|
||||
const defaultModelPreset = ref('');
|
||||
|
||||
// Refs for DOM elements
|
||||
const glossaryInput = ref(null);
|
||||
@@ -1257,9 +1070,7 @@
|
||||
|
||||
// Validation State
|
||||
const errors = reactive({
|
||||
model_id: false,
|
||||
api_key: false,
|
||||
base_url: false,
|
||||
model_preset: false,
|
||||
mineru_token: false,
|
||||
mineru_deploy_base_url: false,
|
||||
custom_to_lang: false,
|
||||
@@ -1289,11 +1100,7 @@
|
||||
formula_ocr: true,
|
||||
code_ocr: true,
|
||||
skip_translate: false,
|
||||
platform: 'https://api.302.ai/v1',
|
||||
base_url: '',
|
||||
api_key: '',
|
||||
model_id: '',
|
||||
provider: 'api.openai.com', // Default provider
|
||||
model_preset: '',
|
||||
system_proxy_enable: false,
|
||||
force_json: false,
|
||||
to_lang: 'Simplified Chinese',
|
||||
@@ -1336,6 +1143,10 @@
|
||||
const v = localStorage.getItem(k);
|
||||
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.auto_workflow_enabled = getBool('translator_auto_workflow_enabled', true);
|
||||
@@ -1356,7 +1167,7 @@
|
||||
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', 'custom');
|
||||
form.model_preset = get('translator_model_preset', fallbackPreset);
|
||||
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');
|
||||
@@ -1370,13 +1181,8 @@
|
||||
form.rpm = getNumOrNull('rpm'); // Load RPM
|
||||
form.tpm = getNumOrNull('tpm'); // Load TPM
|
||||
|
||||
// Determine Provider
|
||||
const platObj = KNOWN_PLATFORMS.find(p => p.val === form.platform);
|
||||
if (form.platform === 'custom') {
|
||||
// 修正:读取组件实际保存的 Key (translator_platform_custom_provider)
|
||||
form.provider = get('translator_platform_custom_provider', 'default');
|
||||
} else {
|
||||
form.provider = platObj ? platObj.provider : '';
|
||||
if (!validPresetIds.includes(form.model_preset)) {
|
||||
form.model_preset = fallbackPreset;
|
||||
}
|
||||
|
||||
// Restore workflow specific params
|
||||
@@ -1387,9 +1193,6 @@
|
||||
workflowParams.txt.segment_mode = get('translator_txt_segment_mode', 'line');
|
||||
workflowParams.xlsx.translate_regions = get('translator_xlsx_translate_regions', '');
|
||||
workflowParams.json.json_paths = get('translator_json_paths', '');
|
||||
|
||||
// Trigger platform updates to load API keys/models
|
||||
updatePlatformParams(form.platform, 'translator_platform', form);
|
||||
};
|
||||
|
||||
// --- 新增:专门用于将当前 form 数据全部写入 localStorage 的函数 ---
|
||||
@@ -1419,7 +1222,7 @@
|
||||
s('translator_formula_ocr', f.formula_ocr);
|
||||
s('translator_code_ocr', f.code_ocr);
|
||||
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_force_json', f.force_json);
|
||||
s('translator_to_lang', f.to_lang);
|
||||
@@ -1435,12 +1238,6 @@
|
||||
s('rpm', f.rpm || '');
|
||||
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...)
|
||||
for (const [wfType, params] of Object.entries(workflowParams)) {
|
||||
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 dict = i18nData.value[currentLang.value] || i18nData.value['zh'] || {};
|
||||
return dict[k] || k;
|
||||
@@ -1467,6 +1253,15 @@
|
||||
const capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1);
|
||||
const saveSetting = (k, v) => localStorage.setItem(k, 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 wf = form.workflow_type;
|
||||
@@ -1642,10 +1437,7 @@
|
||||
// Clone basic form
|
||||
const basePayload = {
|
||||
skip_translate: form.skip_translate,
|
||||
base_url: emptyToNull(form.base_url),
|
||||
api_key: form.api_key || "",
|
||||
model_id: emptyToNull(form.model_id),
|
||||
provider: emptyToNull(form.provider), // Add provider
|
||||
model_preset: emptyToNull(form.model_preset),
|
||||
to_lang: form.to_lang === 'custom' ? form.custom_to_lang : form.to_lang,
|
||||
thinking: form.thinking,
|
||||
chunk_size: Number(form.chunk_size),
|
||||
@@ -1718,12 +1510,8 @@
|
||||
Object.keys(errors).forEach(k => errors[k] = false);
|
||||
|
||||
if (!form.skip_translate) {
|
||||
if (!form.model_id) {
|
||||
errors.model_id = true;
|
||||
isValid = false;
|
||||
}
|
||||
if (form.platform === 'custom' && !form.base_url) {
|
||||
errors.base_url = true;
|
||||
if (!form.model_preset) {
|
||||
errors.model_preset = true;
|
||||
isValid = false;
|
||||
}
|
||||
if (form.to_lang === 'custom' && !form.custom_to_lang) {
|
||||
@@ -2062,6 +1850,10 @@
|
||||
const data = JSON.parse(ev.target.result);
|
||||
if (data.form) Object.assign(form, data.form);
|
||||
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();
|
||||
alert(t('configImportSuccess'));
|
||||
} catch (err) {
|
||||
@@ -2110,6 +1902,10 @@
|
||||
projectContributeBtn: "项目协作",
|
||||
workflowTitle: "选择工作流",
|
||||
autoWorkflowLabel: "自动选择工作流",
|
||||
modelPresetLabel: "模型预设",
|
||||
modelPresetPlaceholder: "请选择模型预设",
|
||||
modelPresetEmpty: "请先在服务端环境变量中配置模型预设",
|
||||
modelPresetRuntimeHint: "运行时将从服务端环境变量读取供应商、模型端点与 API Key。",
|
||||
workflowOptionPptx: "PPTX 演示文稿",
|
||||
pptxSettingsTitleText: "PPTX 设置",
|
||||
mineruDeployServerUrlLabel: "Server URL",
|
||||
@@ -2123,6 +1919,10 @@
|
||||
tutorialBtn: "Tutorial",
|
||||
projectContributeBtn: "Contribute",
|
||||
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",
|
||||
pptxSettingsTitleText: "PPTX Settings",
|
||||
mineruDeployServerUrlLabel: "Server URL",
|
||||
@@ -2145,15 +1945,10 @@
|
||||
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);
|
||||
modelPresets.value = Array.isArray(envConfig.model_presets) ? envConfig.model_presets : [];
|
||||
defaultModelPreset.value = envConfig.default_model_preset || (modelPresets.value[0]?.id || '');
|
||||
if (defaultModelPreset.value && !localStorage.getItem('translator_model_preset')) {
|
||||
localStorage.setItem('translator_model_preset', defaultModelPreset.value);
|
||||
}
|
||||
if (envConfig.rpm != null && !localStorage.getItem('rpm')) {
|
||||
localStorage.setItem('rpm', String(envConfig.rpm));
|
||||
@@ -2195,6 +1990,7 @@
|
||||
|
||||
return {
|
||||
version, currentLang, i18nData, glossaryData, glossaryCount, tasks, enginList, defaultParams,
|
||||
modelPresets,
|
||||
form, workflowParams, showMineruToken, previewMode, syncScrollEnabled, showIdentityOption,
|
||||
errors, clearError,
|
||||
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