feat: improve API key validation and enhance error handling in TTS service

This commit is contained in:
王锦强
2025-03-11 21:35:37 +08:00
parent 3356e267db
commit 7958e56bdf
3 changed files with 241 additions and 141 deletions

View File

@@ -95,7 +95,7 @@ func (c *Client) getEndpoint(ctx context.Context) (map[string]interface{}, error
// 更新缓存
c.endpointMu.Lock()
c.endpoint = endpoint
c.endpointExpiry = time.Now().Add(30 * time.Minute) // 令牌有效期通常是1小时提前刷新
c.endpointExpiry = time.Now().Add(5 * time.Minute)
c.endpointMu.Unlock()
return endpoint, nil

View File

@@ -1,7 +1,6 @@
const encoder = new TextEncoder();
let expiredAt = null;
let endpoint = null;
const API_KEY = 'your-secret-api-key'; // 添加 API 密钥常量,可以修改为你想要的值
// 定义需要保留的 SSML 标签模式
const preserveTags = [
@@ -73,7 +72,15 @@ async function handleRequest(request) {
// 验证 API 密钥
if (!validateApiKey(apiKey)) {
return new Response('Unauthorized: Invalid API key', { status: 401 });
// 改进 401 错误响应,提供更友好的错误信息
return new Response(JSON.stringify({
error: 'Unauthorized',
message: '无效的 API 密钥,请确保您提供了正确的密钥。',
status: 401
}), {
status: 401,
headers: { 'Content-Type': 'application/json; charset=utf-8' }
});
}
const text = requestUrl.searchParams.get('t') || '';
@@ -160,7 +167,7 @@ async function handleRequest(request) {
</head>
<body class="bg-gray-50 text-gray-800">
<!-- 导航栏 -->
<nav class="bg-ms-blue shadow-lg">
<nav class="bg-ms-blue shadow-2xl">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
<div class="flex items-center">
@@ -174,146 +181,148 @@ async function handleRequest(request) {
</nav>
<!-- 主内容 -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<!-- 主要功能区 -->
<div class="flex flex-col lg:flex-row gap-8">
<!-- 左边栏:语音转换 -->
<div class="lg:w-2/3">
<div class="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-200">
<div class="px-4 py-5 sm:px-6">
<h2 class="text-lg font-medium text-gray-900">在线文本转语音</h2>
<p class="mt-1 text-sm text-gray-500">输入文本并选择语音进行转换</p>
</div>
<div class="px-4 py-5 sm:p-6">
<form id="ttsForm" class="space-y-6">
<div>
<label for="apiKey" class="block text-sm font-medium text-gray-700">API Key</label>
<input type="text" id="apiKey" name="apiKey" required
class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md focus:ring-ms-blue focus:border-ms-blue"
placeholder="输入API Key" />
</div>
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
<div class="border-b border-gray-200">
<nav class="-mb-px flex space-x-8 justify-center" aria-label="Tabs">
<button class="tab-link py-4 px-1 text-ms-blue border-b-2 border-ms-blue hover:text-ms-dark-blue" data-tab="tab1">在线文本转语音</button>
<button class="tab-link py-4 px-1 text-gray-500 hover:text-ms-dark-blue" data-tab="tab2">API文档</button>
<button class="tab-link py-4 px-1 text-gray-500 hover:text-ms-dark-blue" data-tab="tab3">关于服务</button>
</nav>
</div>
<div>
<label for="text" class="block text-sm font-medium text-gray-700">输入文本</label>
<textarea id="text" name="text" rows="4" required
class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md focus:ring-ms-blue focus:border-ms-blue"
placeholder="请输入要转换的文本"></textarea>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div class="flex justify-between mb-1">
<label for="languageFilter" class="block text-sm font-medium text-gray-700">语言</label>
</div>
<select id="languageFilter" name="languageFilter"
class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-ms-blue focus:border-ms-blue sm:text-sm"
onchange="filterVoicesByLanguage()">
<option value="zh">中文 (Chinese)</option>
<option value="all">所有语言</option>
<option value="en">英文 (English)</option>
<option value="ja">日文 (Japanese)</option>
<option value="ko">韩文 (Korean)</option>
<option value="fr">法语 (French)</option>
<option value="de">德语 (German)</option>
<option value="es">西班牙语 (Spanish)</option>
<option value="ru">俄语 (Russian)</option>
</select>
</div>
<div>
<div class="flex justify-between mb-1">
<label for="voice" class="block text-sm font-medium text-gray-700">选择语音</label>
</div>
<select id="voice" name="voice"
class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-ms-blue focus:border-ms-blue sm:text-sm">
</select>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="rate" class="block text-sm font-medium text-gray-700">语速调整</label>
<div class="flex items-center mt-2">
<span id="rateValue" class="w-8 text-sm text-gray-500">0</span>
<input type="range" id="rate" name="rate" min="-100" max="100" value="0"
class="mt-1 block w-full" oninput="document.getElementById('rateValue').textContent=this.value" />
</div>
</div>
<div>
<label for="pitch" class="block text-sm font-medium text-gray-700">音调调整</label>
<div class="flex items-center mt-2">
<span id="pitchValue" class="w-8 text-sm text-gray-500">0</span>
<input type="range" id="pitch" name="pitch" min="-100" max="100" value="0"
class="mt-1 block w-full" oninput="document.getElementById('pitchValue').textContent=this.value" />
</div>
</div>
</div>
<div class="flex flex-col sm:flex-row gap-3">
<button type="submit"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-ms-blue hover:bg-ms-dark-blue focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-ms-blue">
生成语音
</button>
<button type="button" id="downloadBtn" style="display:none;"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
下载音频
</button>
</div>
<div id="voiceLoadError" role="alert" class="mt-4 rounded-md bg-red-50 p-4" style="display: none;">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293-1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">无法加载语音列表</h3>
<div class="mt-2 text-sm text-red-700">
<p>显示默认语音列表。请检查网络连接或稍后再试。</p>
<div id="tab1" class="tab-content mt-6">
<!-- 在线文本转语音表单内容 -->
<div class="flex flex-col lg:flex-row gap-8">
<!-- 左边栏:语音转换 -->
<div class="lg:w-3/4 mx-auto">
<div class="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-200">
<div class="px-4 py-5 sm:px-6">
<h2 class="text-lg font-medium text-gray-900">在线文本转语音</h2>
<p class="mt-1 text-sm text-gray-500">输入文本并选择语音进行转换</p>
</div>
<div class="px-4 py-5 sm:p-6">
<form id="ttsForm" class="space-y-6">
<!-- 添加错误提示区域 -->
<div id="apiErrorAlert" class="rounded-md bg-red-50 p-4" style="display: none;">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 000 16zM8.707 7.293a1 1 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 101.414 1.414L10 11.414l1.293-1.293a1 1 001.414-1.414L11.414 10l1.293-1.293a1 1 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800" id="apiErrorTitle">错误</h3>
<div class="mt-2 text-sm text-red-700">
<p id="apiErrorMessage"></p>
</div>
</div>
</div>
</div>
</div>
</form>
<div id="audioContainer" class="mt-6 rounded-md bg-gray-50 p-4 border border-gray-200" style="display: none;">
<audio id="audioPlayer" controls class="w-full"></audio>
<div>
<label for="apiKey" class="block text-sm font-medium text-gray-700">API Key</label>
<input type="text" id="apiKey" name="apiKey" required
class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md focus:ring-ms-blue focus:border-ms-blue"
placeholder="输入API Key" />
</div>
<div>
<label for="text" class="block text-sm font-medium text-gray-700">输入文本</label>
<textarea id="text" name="text" rows="4" required
class="mt-1 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md focus:ring-ms-blue focus:border-ms-blue"
placeholder="请输入要转换的文本"></textarea>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<div class="flex justify-between mb-1">
<label for="languageFilter" class="block text-sm font-medium text-gray-700">语言</label>
</div>
<select id="languageFilter" name="languageFilter"
class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-ms-blue focus:border-ms-blue sm:text-sm"
onchange="filterVoicesByLanguage()">
<option value="zh">中文 (Chinese)</option>
<option value="all">所有语言</option>
<option value="en">英文 (English)</option>
<option value="ja">日文 (Japanese)</option>
<option value="ko">韩文 (Korean)</option>
<option value="fr">法语 (French)</option>
<option value="de">德语 (German)</option>
<option value="es">西班牙语 (Spanish)</option>
<option value="ru">俄语 (Russian)</option>
</select>
</div>
<div>
<div class="flex justify-between mb-1">
<label for="voice" class="block text-sm font-medium text-gray-700">选择语音</label>
</div>
<select id="voice" name="voice"
class="mt-1 block w-full py-2 px-3 border border-gray-300 bg-white rounded-md shadow-sm focus:outline-none focus:ring-ms-blue focus:border-ms-blue sm:text-sm">
</select>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="rate" class="block text-sm font-medium text-gray-700">语速调整</label>
<div class="flex items-center mt-2">
<span id="rateValue" class="w-8 text-sm text-gray-500">0</span>
<input type="range" id="rate" name="rate" min="-100" max="100" value="0"
class="mt-1 block w-full" oninput="document.getElementById('rateValue').textContent=this.value" />
</div>
</div>
<div>
<label for="pitch" class="block text-sm font-medium text-gray-700">音调调整</label>
<div class="flex items-center mt-2">
<span id="pitchValue" class="w-8 text-sm text-gray-500">0</span>
<input type="range" id="pitch" name="pitch" min="-100" max="100" value="0"
class="mt-1 block w-full" oninput="document.getElementById('pitchValue').textContent=this.value" />
</div>
</div>
</div>
<div class="flex flex-col sm:flex-row gap-3">
<button type="submit"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-ms-blue hover:bg-ms-dark-blue focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-ms-blue">
生成语音
</button>
<button type="button" id="downloadBtn" style="display:none;"
class="inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
下载音频
</button>
</div>
<div id="voiceLoadError" role="alert" class="mt-4 rounded-md bg-red-50 p-4" style="display: none;">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-red-400" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 000 16zM8.707 7.293a1 1 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 101.414 1.414L10 11.414l1.293-1.293a1 1 001.414-1.414L11.414 10l1.293-1.293a1 1 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800">无法加载语音列表</h3>
<div class="mt-2 text-sm text-red-700">
<p>显示默认语音列表。请检查网络连接或稍后再试。</p>
</div>
</div>
</div>
</div>
</form>
<div id="audioContainer" class="mt-6 rounded-md bg-gray-50 p-4 border border-gray-200" style="display: none;">
<audio id="audioPlayer" controls class="w-full"></audio>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 右边栏API文档 -->
<div class="lg:w-1/3 space-y-6">
<!-- 关于卡片 -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">关于服务</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500">
<p>
Microsoft TTS API 是一个高质量的文本转语音服务,支持多种语言和声音。
通过简单的 API 调用,可以将文本转换为自然流畅的语音。
</p>
</div>
<div class="mt-3 bg-blue-50 p-3 rounded-md">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3 flex-1 md:flex md:justify-between">
<p class="text-sm text-blue-700">
支持 SSML 标签和 OpenAI 兼容接口
</p>
</div>
</div>
</div>
</div>
</div>
<div id="tab2" class="tab-content mt-6" style="display: none;">
<!-- API文档部分 -->
<div class="w-full lg:w-2/3 mx-auto space-y-6">
<!-- API 文档链接 -->
<div class="bg-white overflow-hidden shadow rounded-lg divide-y divide-gray-200">
<div class="px-4 py-5 sm:px-6">
@@ -366,7 +375,7 @@ curl ${baseUrl}/v1/audio/speech \\
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-amber-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 0 11-2 0 1 1 0 012 0zm-1-8a1 1 0 00-1 1v3a1 1 0 002 0V6a1 1 0 00-1-1z" clip-rule="evenodd" />
<path fill-rule="evenodd" d="M8.257 3.099c.765-1.36 2.722-1.36 3.486 0l5.58 9.92c.75 1.334-.213 2.98-1.742 2.98H4.42c-1.53 0-2.493-1.646-1.743-2.98l5.58-9.92zM11 13a1 1 11-2 0 1 1 12 0zm-1-8a1 1 00-1 1v3a1 1 001 1h1a1 1 100-2v-3a1 1 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3">
@@ -384,6 +393,38 @@ curl ${baseUrl}/v1/audio/speech \\
</div>
</div>
</div>
<div id="tab3" class="tab-content mt-6" style="display: none;">
<!-- 关于服务部分 -->
<div class="w-full lg:w-2/3 mx-auto space-y-6">
<!-- 关于卡片 -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">关于服务</h3>
<div class="mt-2 max-w-xl text-sm text-gray-500">
<p>
Microsoft TTS API 是一个高质量的文本转语音服务,支持多种语言和声音。
通过简单的 API 调用,可以将文本转换为自然流畅的语音。
</p>
</div>
<div class="mt-3 bg-blue-50 p-3 rounded-md">
<div class="flex">
<div class="flex-shrink-0">
<svg class="h-5 w-5 text-blue-500" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0116 0zm-7-4a1 1 11-2 0 1 1 12 0zm-1-8a1 1 00-1 1v3a1 1 001 1h1a1 1 100-2v-3a1 1 00-1-1H9z" clip-rule="evenodd" />
</svg>
</div>
<div class="ml-3 flex-1 md:flex md:justify-between">
<p class="text-sm text-blue-700">
支持 SSML 标签和 OpenAI 兼容接口
</p>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
<!-- 页脚 -->
@@ -401,6 +442,9 @@ curl ${baseUrl}/v1/audio/speech \\
document.getElementById('ttsForm').addEventListener('submit', async function(e) {
e.preventDefault();
// 隐藏先前的错误信息
document.getElementById('apiErrorAlert').style.display = 'none';
const apiKey = document.getElementById('apiKey').value;
const text = encodeURIComponent(document.getElementById('text').value);
const voice = document.getElementById('voice').value;
@@ -408,18 +452,36 @@ curl ${baseUrl}/v1/audio/speech \\
const pitch = document.getElementById('pitch').value;
if (!text) {
alert('请输入要转换的文本');
showError('请输入要转换的文本', '文本内容不能为空');
return;
}
if (!apiKey) {
alert('请输入API Key');
showError('请输入API Key', 'API密钥不能为空');
return;
}
const url = \`${baseUrl}/tts?api_key=\${apiKey}&t=\${text}&v=\${voice}&r=\${rate}&p=\${pitch}\`;
try {
const response = await fetch(url);
if (!response.ok) {
// 处理错误响应
if (response.status === 401) {
try {
const errorData = await response.json();
showError('认证失败', errorData.message || '无效的API密钥请确保您提供了正确的密钥');
} catch (e) {
showError('认证失败', '无效的API密钥请确保您提供了正确的密钥');
}
return;
} else {
showError('请求失败');
return;
}
}
const audioPlayer = document.getElementById('audioPlayer');
audioPlayer.src = url;
audioPlayer.play();
@@ -432,10 +494,21 @@ curl ${baseUrl}/v1/audio/speech \\
window.location.href = downloadUrl;
};
} catch (error) {
alert('生成音频失败: ' + error.message);
showError('生成音频失败', error.message);
}
});
// 显示错误信息的函数
function showError(title, message) {
const errorAlert = document.getElementById('apiErrorAlert');
document.getElementById('apiErrorTitle').textContent = title;
document.getElementById('apiErrorMessage').textContent = message;
errorAlert.style.display = 'block';
// 滚动到错误信息
errorAlert.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
// 按语言筛选语音
function filterVoicesByLanguage() {
const languageFilter = document.getElementById('languageFilter').value;
@@ -593,10 +666,31 @@ curl ${baseUrl}/v1/audio/speech \\
// 页面加载完成后加载语音列表
window.onload = loadVoices;
document.addEventListener('DOMContentLoaded', function() {
const tabLinks = document.querySelectorAll('.tab-link');
const tabContents = document.querySelectorAll('.tab-content');
tabLinks.forEach(link => {
link.addEventListener('click', () => {
tabLinks.forEach(l => {
l.classList.remove('text-ms-blue', 'border-ms-blue');
l.classList.add('text-gray-500');
});
link.classList.add('text-ms-blue', 'border-ms-blue');
link.classList.remove('text-gray-500');
const targetId = link.getAttribute('data-tab');
tabContents.forEach(tc => {
tc.style.display = (tc.id === targetId) ? '' : 'none';
});
});
});
});
</script>
</body>
</html>
`, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8' }});
`, { status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8'}});
}
addEventListener('fetch', event => {
@@ -708,7 +802,9 @@ async function bytesToBase64(bytes) {
// API 密钥验证函数
function validateApiKey(apiKey) {
return apiKey === API_KEY;
// 从环境变量获取 API 密钥并进行验证
const expectedApiKey = API_KEY || '';
return apiKey === expectedApiKey;
}
async function getVoice(text, voiceName = 'zh-CN-XiaoxiaoMultilingualNeural', rate = 0, pitch = 0, outputFormat='audio-24khz-48kbitrate-mono-mp3', download=false) {

View File

@@ -2,4 +2,8 @@ name = "wstrans"
main = "src/index.js"
compatibility_date = "2024-04-15"
workers_dev = true
node_compat = true
compatibility_flags = [ "nodejs_compat" ]
[vars]
API_KEY = "zuoban"