Initial commit
This commit is contained in:
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Node / tooling
|
||||||
|
node_modules/
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Env / secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
|
||||||
|
# Misc
|
||||||
|
*.tmp
|
||||||
|
*.swp
|
||||||
BIN
bin/gitea-mcp
Normal file
BIN
bin/gitea-mcp
Normal file
Binary file not shown.
238
readme.md
Normal file
238
readme.md
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
# Cloudflare Workers AI 翻译服务
|
||||||
|
|
||||||
|
一个基于 **Cloudflare Workers + Workers AI** 的轻量级 AI 翻译服务,支持:
|
||||||
|
|
||||||
|
- 🌐 **网页访问翻译**(默认:英语 → 中文)
|
||||||
|
- 📄 **长文本自动分页 / 翻页**
|
||||||
|
- 🔌 **HTTP API 调用**
|
||||||
|
- 🌎 **多语言互译**(基于 `m2m100-1.2b` 模型)
|
||||||
|
- ⚡ **全球边缘节点低延迟部署**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ 功能特性
|
||||||
|
|
||||||
|
### 1. 网页翻译(Web UI)
|
||||||
|
|
||||||
|
- 默认语言:**en → zh**
|
||||||
|
- 支持选择源语言 / 目标语言
|
||||||
|
- 自动按字符数分页
|
||||||
|
- 翻页只翻译当前页,响应更快
|
||||||
|
- 适合:
|
||||||
|
- 阅读外文文章
|
||||||
|
- 翻译长文档
|
||||||
|
- 快速人工校对
|
||||||
|
|
||||||
|
访问根路径即可使用:https://your-worker-domain/
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. HTTP API 翻译
|
||||||
|
|
||||||
|
#### 接口一览
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|----|----|----|
|
||||||
|
| GET | `/api/languages` | 获取常用语言列表 |
|
||||||
|
| POST | `/api/translate` | 翻译文本 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 API 使用说明
|
||||||
|
|
||||||
|
### 2.1 单页 / 分页翻译
|
||||||
|
|
||||||
|
**POST** `/api/translate`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "Hello world, this is a long text...",
|
||||||
|
"source_lang": "en",
|
||||||
|
"target_lang": "zh",
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 1800
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 返回示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 1800,
|
||||||
|
"total_pages": 3,
|
||||||
|
"source_lang": "en",
|
||||||
|
"target_lang": "zh",
|
||||||
|
"translated_text": "你好,世界,这是一段很长的文本……"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
### 2.2 一次翻译全文(自动分段)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"text": "Very long text...",
|
||||||
|
"source_lang": "en",
|
||||||
|
"target_lang": "zh",
|
||||||
|
"translate_all": true,
|
||||||
|
"page_size": 1800
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ 默认最多批量翻译 **50 段**(防止超时 / 过载)
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
### 2.3 curl 示例
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST https://your-worker-domain/api/translate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"text": "Hello World",
|
||||||
|
"source_lang": "en",
|
||||||
|
"target_lang": "zh"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## 🌐 支持语言(示例)
|
||||||
|
|
||||||
|
基于 **Meta M2M100 多语言模型**,支持数十种语言互译,例如:
|
||||||
|
|
||||||
|
- en – English
|
||||||
|
- zh – 中文
|
||||||
|
- ja – 日本語
|
||||||
|
- ko – 한국어
|
||||||
|
- fr – Français
|
||||||
|
- de – Deutsch
|
||||||
|
- es – Español
|
||||||
|
- ru – Русский
|
||||||
|
- ar – العربية
|
||||||
|
- hi – हिन्दी
|
||||||
|
- vi – Tiếng Việt
|
||||||
|
等……
|
||||||
|
|
||||||
|
> 实际支持语言以模型能力为准
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## 🧠 使用的 AI 模型
|
||||||
|
|
||||||
|
- **模型**:`@cf/meta/m2m100-1.2b`
|
||||||
|
- **类型**:多语言 → 多语言翻译
|
||||||
|
- **提供方**:Cloudflare Workers AI
|
||||||
|
|
||||||
|
官方文档:
|
||||||
|
https://developers.cloudflare.com/workers-ai/models/m2m100-1.2b/
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## 📦 项目结构
|
||||||
|
|
||||||
|
```text
|
||||||
|
.
|
||||||
|
├── index.js # Cloudflare Worker 主逻辑
|
||||||
|
├── wrangler.toml # Wrangler 配置
|
||||||
|
└── README.md
|
||||||
|
```
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## ⚙️ 部署方式
|
||||||
|
|
||||||
|
### 1️⃣ 安装 Wrangler
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g wrangler
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2️⃣ 登录 Cloudflare
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wrangler login
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3️⃣ 配置 `wrangler.toml`
|
||||||
|
|
||||||
|
```toml
|
||||||
|
name = "ai-translate-worker"
|
||||||
|
main = "index.js"
|
||||||
|
compatibility_date = "2026-01-27"
|
||||||
|
|
||||||
|
[ai]
|
||||||
|
binding = "AI"
|
||||||
|
```
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
### 4️⃣ 发布 Worker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wrangler deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## 🔐 (可选)API Key 保护
|
||||||
|
|
||||||
|
### 设置密钥
|
||||||
|
|
||||||
|
```bash
|
||||||
|
wrangler secret put API_KEY
|
||||||
|
```
|
||||||
|
|
||||||
|
### 调用方式
|
||||||
|
|
||||||
|
```http
|
||||||
|
X-API-Key: your_api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
或:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/api/translate?key=your_api_key
|
||||||
|
```
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## 🧩 适用场景
|
||||||
|
|
||||||
|
- 个人翻译工具
|
||||||
|
- 博客 / 内容平台翻译接口
|
||||||
|
- 海外资讯聚合
|
||||||
|
- AI 工具链中的翻译节点
|
||||||
|
- Cloudflare 边缘 AI Demo
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## 📌 TODO / 可扩展方向
|
||||||
|
|
||||||
|
- 自动语言识别
|
||||||
|
- 翻译历史记录
|
||||||
|
- 左右对照 UI
|
||||||
|
- Markdown / HTML 翻译
|
||||||
|
- 翻译缓存(KV / Cache API)
|
||||||
|
- 流式翻译(Streaming)
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## 📄 License
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
------
|
||||||
|
|
||||||
|
## ❤️ 致谢
|
||||||
|
|
||||||
|
- Cloudflare Workers
|
||||||
|
- Cloudflare Workers AI
|
||||||
|
- Meta M2M100 Multilingual Model
|
||||||
401
worker.js
Normal file
401
worker.js
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
export default {
|
||||||
|
async fetch(request, env, ctx) {
|
||||||
|
const url = new URL(request.url);
|
||||||
|
const { pathname } = url;
|
||||||
|
|
||||||
|
// CORS preflight for API
|
||||||
|
if (request.method === "OPTIONS" && pathname.startsWith("/api/")) {
|
||||||
|
return new Response(null, { headers: corsHeaders() });
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- API: list languages (common) ---
|
||||||
|
if (pathname === "/api/languages") {
|
||||||
|
return jsonResponse({ languages: COMMON_LANGUAGES }, 200, corsHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- API: translate ---
|
||||||
|
if (pathname === "/api/translate") {
|
||||||
|
if (request.method !== "POST") {
|
||||||
|
return jsonResponse(
|
||||||
|
{ error: "Use POST /api/translate with JSON body." },
|
||||||
|
405,
|
||||||
|
corsHeaders()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional API key
|
||||||
|
const apiKey = env.API_KEY;
|
||||||
|
if (apiKey) {
|
||||||
|
const got =
|
||||||
|
request.headers.get("x-api-key") ||
|
||||||
|
url.searchParams.get("key") ||
|
||||||
|
"";
|
||||||
|
if (got !== apiKey) {
|
||||||
|
return jsonResponse({ error: "Unauthorized" }, 401, corsHeaders());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let body;
|
||||||
|
try {
|
||||||
|
body = await request.json();
|
||||||
|
} catch {
|
||||||
|
return jsonResponse({ error: "Invalid JSON body." }, 400, corsHeaders());
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = (body.text ?? "").toString();
|
||||||
|
const target_lang = (body.target_lang ?? "").toString().trim();
|
||||||
|
const source_lang_raw = (body.source_lang ?? "").toString().trim(); // optional
|
||||||
|
|
||||||
|
if (!text) return jsonResponse({ error: "text is required" }, 400, corsHeaders());
|
||||||
|
if (!target_lang) return jsonResponse({ error: "target_lang is required" }, 400, corsHeaders());
|
||||||
|
|
||||||
|
const page_size = clampInt(body.page_size ?? 1800, 200, 5000);
|
||||||
|
const page = clampInt(body.page ?? 1, 1, 999999);
|
||||||
|
const translate_all = Boolean(body.translate_all);
|
||||||
|
|
||||||
|
const chunks = splitText(text, page_size);
|
||||||
|
const total_pages = chunks.length;
|
||||||
|
const safePage = clampInt(page, 1, total_pages);
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (translate_all) {
|
||||||
|
// Use batch requests (Workers AI supports batch schema on this model) :contentReference[oaicite:2]{index=2}
|
||||||
|
const requests = chunks.slice(0, 50).map((t) => ({
|
||||||
|
text: t,
|
||||||
|
...(source_lang_raw ? { source_lang: normalizeLang(source_lang_raw) } : {}),
|
||||||
|
target_lang: normalizeLang(target_lang),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const out = await env.AI.run("@cf/meta/m2m100-1.2b", { requests });
|
||||||
|
// Some bindings return array-like results; normalize defensively:
|
||||||
|
const translated_pages = Array.isArray(out)
|
||||||
|
? out.map((x) => x?.translated_text ?? "")
|
||||||
|
: (out?.responses ?? out?.results ?? out?.translated_pages ?? null);
|
||||||
|
|
||||||
|
// Fallback: if unknown, just translate one-by-one
|
||||||
|
if (Array.isArray(translated_pages)) {
|
||||||
|
return jsonResponse(
|
||||||
|
{
|
||||||
|
page: 1,
|
||||||
|
page_size,
|
||||||
|
total_pages,
|
||||||
|
translated_pages,
|
||||||
|
translated_text: translated_pages.join(""),
|
||||||
|
truncated: chunks.length > 50,
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
corsHeaders()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: translate one page
|
||||||
|
const chunk = chunks[safePage - 1] ?? "";
|
||||||
|
const out = await env.AI.run("@cf/meta/m2m100-1.2b", {
|
||||||
|
text: chunk,
|
||||||
|
...(source_lang_raw ? { source_lang: normalizeLang(source_lang_raw) } : {}),
|
||||||
|
target_lang: normalizeLang(target_lang),
|
||||||
|
});
|
||||||
|
|
||||||
|
return jsonResponse(
|
||||||
|
{
|
||||||
|
page: safePage,
|
||||||
|
page_size,
|
||||||
|
total_pages,
|
||||||
|
source_lang: source_lang_raw || "(default en)",
|
||||||
|
target_lang,
|
||||||
|
input_text: chunk,
|
||||||
|
translated_text: out?.translated_text ?? "",
|
||||||
|
},
|
||||||
|
200,
|
||||||
|
corsHeaders()
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
return jsonResponse(
|
||||||
|
{ error: "AI run failed", detail: String(e?.message || e) },
|
||||||
|
500,
|
||||||
|
corsHeaders()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Web UI ---
|
||||||
|
if (pathname === "/" || pathname === "/index.html") {
|
||||||
|
if (request.method === "GET") {
|
||||||
|
return new Response(renderPage({}), {
|
||||||
|
headers: { "content-type": "text/html; charset=utf-8" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (request.method === "POST") {
|
||||||
|
const form = await request.formData();
|
||||||
|
const text = (form.get("text") ?? "").toString();
|
||||||
|
const source_lang = (form.get("source_lang") ?? "en").toString().trim();
|
||||||
|
const target_lang = (form.get("target_lang") ?? "zh").toString().trim();
|
||||||
|
const page_size = clampInt(form.get("page_size") ?? 1800, 200, 5000);
|
||||||
|
const page = clampInt(form.get("page") ?? 1, 1, 999999);
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
return new Response(renderPage({ error: "请输入要翻译的文本。", text, source_lang, target_lang, page_size, page }), {
|
||||||
|
headers: { "content-type": "text/html; charset=utf-8" },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const chunks = splitText(text, page_size);
|
||||||
|
const total_pages = chunks.length;
|
||||||
|
const safePage = clampInt(page, 1, total_pages);
|
||||||
|
const chunk = chunks[safePage - 1] ?? "";
|
||||||
|
|
||||||
|
let translated = "";
|
||||||
|
let err = "";
|
||||||
|
try {
|
||||||
|
const out = await env.AI.run("@cf/meta/m2m100-1.2b", {
|
||||||
|
text: chunk,
|
||||||
|
source_lang: normalizeLang(source_lang),
|
||||||
|
target_lang: normalizeLang(target_lang),
|
||||||
|
});
|
||||||
|
translated = out?.translated_text ?? "";
|
||||||
|
} catch (e) {
|
||||||
|
err = `翻译失败:${String(e?.message || e)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
renderPage({
|
||||||
|
text,
|
||||||
|
source_lang,
|
||||||
|
target_lang,
|
||||||
|
page_size,
|
||||||
|
page: safePage,
|
||||||
|
total_pages,
|
||||||
|
input_chunk: chunk,
|
||||||
|
translated_text: translated,
|
||||||
|
error: err,
|
||||||
|
}),
|
||||||
|
{ headers: { "content-type": "text/html; charset=utf-8" } }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response("Method Not Allowed", { status: 405 });
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Response("Not Found", { status: 404 });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------- helpers ----------------
|
||||||
|
|
||||||
|
const COMMON_LANGUAGES = [
|
||||||
|
{ code: "en", name: "English" },
|
||||||
|
{ code: "zh", name: "中文" },
|
||||||
|
{ code: "ja", name: "日本語" },
|
||||||
|
{ code: "ko", name: "한국어" },
|
||||||
|
{ code: "fr", name: "Français" },
|
||||||
|
{ code: "de", name: "Deutsch" },
|
||||||
|
{ code: "es", name: "Español" },
|
||||||
|
{ code: "pt", name: "Português" },
|
||||||
|
{ code: "it", name: "Italiano" },
|
||||||
|
{ code: "ru", name: "Русский" },
|
||||||
|
{ code: "ar", name: "العربية" },
|
||||||
|
{ code: "hi", name: "हिन्दी" },
|
||||||
|
{ code: "id", name: "Bahasa Indonesia" },
|
||||||
|
{ code: "th", name: "ไทย" },
|
||||||
|
{ code: "vi", name: "Tiếng Việt" },
|
||||||
|
{ code: "tr", name: "Türkçe" },
|
||||||
|
{ code: "nl", name: "Nederlands" },
|
||||||
|
{ code: "sv", name: "Svenska" },
|
||||||
|
{ code: "pl", name: "Polski" },
|
||||||
|
{ code: "uk", name: "Українська" },
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeLang(lang) {
|
||||||
|
// Docs say language code like 'en'/'es' etc. :contentReference[oaicite:3]{index=3}
|
||||||
|
// But examples sometimes use english/french; we accept both by passing through.
|
||||||
|
return String(lang || "").trim().toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clampInt(v, min, max) {
|
||||||
|
const n = Number.parseInt(String(v), 10);
|
||||||
|
if (!Number.isFinite(n)) return min;
|
||||||
|
return Math.min(max, Math.max(min, n));
|
||||||
|
}
|
||||||
|
|
||||||
|
function splitText(text, maxChars) {
|
||||||
|
const t = String(text || "");
|
||||||
|
if (t.length <= maxChars) return [t];
|
||||||
|
|
||||||
|
// Prefer paragraph-based splitting
|
||||||
|
const parts = t.split(/\n{2,}/);
|
||||||
|
const chunks = [];
|
||||||
|
let buf = "";
|
||||||
|
|
||||||
|
for (const p of parts) {
|
||||||
|
const piece = (p + "\n\n");
|
||||||
|
if (piece.length > maxChars) {
|
||||||
|
// Hard split long paragraph
|
||||||
|
if (buf) {
|
||||||
|
chunks.push(buf);
|
||||||
|
buf = "";
|
||||||
|
}
|
||||||
|
for (let i = 0; i < piece.length; i += maxChars) {
|
||||||
|
chunks.push(piece.slice(i, i + maxChars));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (buf.length + piece.length > maxChars) {
|
||||||
|
chunks.push(buf);
|
||||||
|
buf = piece;
|
||||||
|
} else {
|
||||||
|
buf += piece;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (buf) chunks.push(buf);
|
||||||
|
return chunks;
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s || "")
|
||||||
|
.replaceAll("&", "&")
|
||||||
|
.replaceAll("<", "<")
|
||||||
|
.replaceAll(">", ">")
|
||||||
|
.replaceAll('"', """)
|
||||||
|
.replaceAll("'", "'");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderPage(state) {
|
||||||
|
const {
|
||||||
|
text = "",
|
||||||
|
source_lang = "en",
|
||||||
|
target_lang = "zh",
|
||||||
|
page_size = 1800,
|
||||||
|
page = 1,
|
||||||
|
total_pages = 1,
|
||||||
|
input_chunk = "",
|
||||||
|
translated_text = "",
|
||||||
|
error = "",
|
||||||
|
} = state || {};
|
||||||
|
|
||||||
|
const langOptions = (selected) =>
|
||||||
|
COMMON_LANGUAGES.map(
|
||||||
|
(l) =>
|
||||||
|
`<option value="${escapeHtml(l.code)}" ${
|
||||||
|
l.code === selected ? "selected" : ""
|
||||||
|
}>${escapeHtml(l.name)} (${escapeHtml(l.code)})</option>`
|
||||||
|
).join("");
|
||||||
|
|
||||||
|
const hasResult = Boolean(text);
|
||||||
|
|
||||||
|
return `<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Workers AI 翻译</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial; margin: 24px; }
|
||||||
|
.wrap { max-width: 1100px; margin: 0 auto; }
|
||||||
|
textarea { width: 100%; min-height: 220px; padding: 12px; font-size: 14px; }
|
||||||
|
pre { white-space: pre-wrap; word-break: break-word; background: #f6f7f9; padding: 12px; border-radius: 10px; }
|
||||||
|
.row { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
|
||||||
|
.card { border: 1px solid #e6e7ea; border-radius: 14px; padding: 14px; margin-top: 14px; }
|
||||||
|
.btn { padding: 10px 14px; border-radius: 10px; border: 1px solid #d0d3d9; background: #fff; cursor: pointer; }
|
||||||
|
.btn-primary { border-color: #111; }
|
||||||
|
.muted { color: #666; font-size: 12px; }
|
||||||
|
.err { color: #b00020; }
|
||||||
|
input[type="number"], input[type="text"], select { padding: 8px 10px; border-radius: 10px; border: 1px solid #d0d3d9; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<h2>Workers AI 翻译(m2m100-1.2b)</h2>
|
||||||
|
<div class="muted">
|
||||||
|
默认网页访问:英译中(en → zh)。语言参数遵循语言代码(如 en/zh/es…) :contentReference[oaicite:4]{index=4}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
${error ? `<p class="err">${escapeHtml(error)}</p>` : ""}
|
||||||
|
|
||||||
|
<form method="POST" action="/">
|
||||||
|
<div class="card">
|
||||||
|
<div class="row">
|
||||||
|
<label>源语言:</label>
|
||||||
|
<select name="source_lang">
|
||||||
|
${langOptions(source_lang)}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>目标语言:</label>
|
||||||
|
<select name="target_lang">
|
||||||
|
${langOptions(target_lang)}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<label>每页字符数:</label>
|
||||||
|
<input name="page_size" type="number" min="200" max="5000" value="${escapeHtml(page_size)}" />
|
||||||
|
|
||||||
|
<input type="hidden" name="page" value="1" />
|
||||||
|
<button class="btn btn-primary" type="submit">翻译(从第 1 页开始)</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 10px;">
|
||||||
|
<textarea name="text" placeholder="粘贴要翻译的文本…">${escapeHtml(text)}</textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="muted">提示:长文本会自动分页;翻页时只翻译当前页,响应更快。</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
${
|
||||||
|
hasResult
|
||||||
|
? `<div class="card">
|
||||||
|
<div class="row" style="justify-content: space-between;">
|
||||||
|
<div><b>第 ${page} / ${total_pages} 页</b></div>
|
||||||
|
<div class="row">
|
||||||
|
${page > 1 ? navForm("上一页", page - 1, text, source_lang, target_lang, page_size) : ""}
|
||||||
|
${page < total_pages ? navForm("下一页", page + 1, text, source_lang, target_lang, page_size) : ""}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>原文(本页)</h3>
|
||||||
|
<pre>${escapeHtml(input_chunk || "")}</pre>
|
||||||
|
|
||||||
|
<h3>译文</h3>
|
||||||
|
<pre>${escapeHtml(translated_text || "")}</pre>
|
||||||
|
|
||||||
|
<div class="muted">
|
||||||
|
API:POST /api/translate(JSON)|GET /api/languages
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function navForm(label, page, text, source_lang, target_lang, page_size) {
|
||||||
|
return `<form method="POST" action="/" style="display:inline;">
|
||||||
|
<input type="hidden" name="page" value="${escapeHtml(page)}" />
|
||||||
|
<input type="hidden" name="source_lang" value="${escapeHtml(source_lang)}" />
|
||||||
|
<input type="hidden" name="target_lang" value="${escapeHtml(target_lang)}" />
|
||||||
|
<input type="hidden" name="page_size" value="${escapeHtml(page_size)}" />
|
||||||
|
<input type="hidden" name="text" value="${escapeHtml(text)}" />
|
||||||
|
<button class="btn" type="submit">${escapeHtml(label)}</button>
|
||||||
|
</form>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function corsHeaders() {
|
||||||
|
return {
|
||||||
|
"Access-Control-Allow-Origin": "*",
|
||||||
|
"Access-Control-Allow-Methods": "POST, GET, OPTIONS",
|
||||||
|
"Access-Control-Allow-Headers": "Content-Type, X-API-Key",
|
||||||
|
"Access-Control-Max-Age": "86400",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonResponse(obj, status = 200, extraHeaders = {}) {
|
||||||
|
return new Response(JSON.stringify(obj, null, 2), {
|
||||||
|
status,
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json; charset=utf-8",
|
||||||
|
...extraHeaders,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user