Compare commits

..

3 Commits

Author SHA1 Message Date
gaoziman
b3d151c9f9 chore(安全): 强制环境变量配置并扩展路由保护
- auth.ts: JWT_SECRET必须通过环境变量配置
- crypto.ts: ENCRYPTION_KEY必须通过环境变量配置
- middleware.ts: 添加/assistants和/notes到受保护路由
- db.ts: 更新默认数据库名称
2025-12-23 14:33:37 +08:00
gaoziman
a7972f8768 feat(配置): 集成翻译工具到默认配置
- seed.ts: 添加翻译工具到种子数据
  - 配置工具ID、名称、图标
  - 定义输入参数schema
  - 设为默认启用工具

- register/route.ts: 新用户默认启用翻译工具
2025-12-23 14:33:17 +08:00
gaoziman
d16f72c035 feat(工具): 添加有道智云翻译功能
- 新增 translate.ts: 实现有道翻译API调用
  - 支持100+种语言互译
  - 自动语言检测
  - SHA256签名验证
  - 完善的错误码处理

- executor.ts: 添加翻译工具执行器
  - 支持源语言/目标语言参数
  - 格式化翻译结果输出

- route.ts: 添加翻译工具定义
  - Claude/OpenAI/Codex三种格式支持
2025-12-23 14:33:00 +08:00
9 changed files with 454 additions and 12 deletions

View File

@ -129,7 +129,7 @@ export async function POST(request: NextRequest) {
cchApiKey: null,
cchApiKeyConfigured: false,
defaultModel: 'claude-sonnet-4-5-20250929',
defaultTools: ['web_search', 'web_fetch'],
defaultTools: ['web_search', 'web_fetch', 'youdao_translate'],
theme: 'light',
language: 'zh-CN',
fontSize: 15,

View File

@ -2042,6 +2042,28 @@ function buildClaudeToolDefinitions(toolIds: string[]) {
required: ['url'],
},
},
youdao_translate: {
name: 'youdao_translate',
description: '有道智云高质量多语言翻译。当用户需要翻译文本、句子、段落或询问某个词/短语的翻译时请使用此工具。支持100+种语言互译,包括中英日韩法德西俄阿拉伯语等。',
input_schema: {
type: 'object',
properties: {
text: {
type: 'string',
description: '待翻译的文本内容',
},
from: {
type: 'string',
description: '源语言代码,如 auto(自动检测)/en(英语)/zh-CHS(简体中文)/ja(日语)/ko(韩语)/fr(法语)/de(德语) 等默认auto',
},
to: {
type: 'string',
description: '目标语言代码,如 zh-CHS(简体中文)/en(英语)/ja(日语)/ko(韩语)/fr(法语)/de(德语) 等默认zh-CHS',
},
},
required: ['text'],
},
},
};
return toolIds
@ -2129,6 +2151,31 @@ function buildOpenAIToolDefinitions(toolIds: string[]) {
},
},
},
youdao_translate: {
type: 'function',
function: {
name: 'youdao_translate',
description: '有道智云高质量多语言翻译。当用户需要翻译文本、句子、段落或询问某个词/短语的翻译时请使用此工具。支持100+种语言互译,包括中英日韩法德西俄阿拉伯语等。',
parameters: {
type: 'object',
properties: {
text: {
type: 'string',
description: '待翻译的文本内容',
},
from: {
type: 'string',
description: '源语言代码,如 auto(自动检测)/en(英语)/zh-CHS(简体中文)/ja(日语)/ko(韩语)/fr(法语)/de(德语) 等默认auto',
},
to: {
type: 'string',
description: '目标语言代码,如 zh-CHS(简体中文)/en(英语)/ja(日语)/ko(韩语)/fr(法语)/de(德语) 等默认zh-CHS',
},
},
required: ['text'],
},
},
},
};
return toolIds
@ -2208,6 +2255,29 @@ function buildCodexToolDefinitions(toolIds: string[]) {
required: ['url'],
},
},
youdao_translate: {
type: 'function',
name: 'youdao_translate',
description: '有道智云高质量多语言翻译。当用户需要翻译文本、句子、段落或询问某个词/短语的翻译时请使用此工具。支持100+种语言互译,包括中英日韩法德西俄阿拉伯语等。',
parameters: {
type: 'object',
properties: {
text: {
type: 'string',
description: '待翻译的文本内容',
},
from: {
type: 'string',
description: '源语言代码,如 auto(自动检测)/en(英语)/zh-CHS(简体中文)/ja(日语)/ko(韩语)/fr(法语)/de(德语) 等默认auto',
},
to: {
type: 'string',
description: '目标语言代码,如 zh-CHS(简体中文)/en(英语)/ja(日语)/ko(韩语)/fr(法语)/de(德语) 等默认zh-CHS',
},
},
required: ['text'],
},
},
};
return toolIds

View File

@ -8,7 +8,7 @@ const pool = new Pool({
port: parseInt(process.env.DB_PORT || '35433'),
user: process.env.DB_USER || 'postgres',
password: process.env.DB_PASSWORD || 'postgres',
database: process.env.DB_NAME || 'lioncode_ui',
database: process.env.DB_NAME || 'cchcode_ui',
max: 10, // 最大连接数
idleTimeoutMillis: 30000, // 空闲超时
connectionTimeoutMillis: 5000, // 连接超时

View File

@ -15,7 +15,7 @@ async function seedUserSettings() {
cchApiKeyConfigured: false,
metasoApiKeyConfigured: false,
defaultModel: 'claude-sonnet-4-5-20250929',
defaultTools: ['web_search', 'web_fetch', 'mita_search', 'mita_reader'],
defaultTools: ['web_search', 'web_fetch', 'youdao_translate'],
theme: 'light',
language: 'zh-CN',
enableThinking: false,
@ -99,6 +99,31 @@ async function seedTools() {
isDefault: true,
sortOrder: 4,
},
{
toolId: 'youdao_translate',
name: 'youdao_translate',
displayName: 'Translate',
description: '有道智云高质量多语言翻译支持100+种语言互译',
icon: 'Languages',
inputSchema: {
type: 'object',
properties: {
text: { type: 'string', description: '待翻译的文本内容' },
from: {
type: 'string',
description: '源语言代码,如 en/zh-CHS/ja/ko/fr/de 等默认auto自动检测',
},
to: {
type: 'string',
description: '目标语言代码,如 zh-CHS/en/ja/ko/fr/de 等默认zh-CHS简体中文',
},
},
required: ['text'],
},
isEnabled: true,
isDefault: true,
sortOrder: 5,
},
];
for (const tool of toolsData) {

View File

@ -1,10 +1,11 @@
import { SignJWT, jwtVerify } from 'jose';
import { cookies } from 'next/headers';
// JWT 密钥(生产环境应使用环境变量)
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || 'lioncode-jwt-secret-key-2024'
);
// JWT 密钥(必须通过环境变量配置)
if (!process.env.JWT_SECRET) {
throw new Error('环境变量 JWT_SECRET 未配置,请在 .env.local 中设置');
}
const JWT_SECRET = new TextEncoder().encode(process.env.JWT_SECRET);
// Token 有效期7 天
const TOKEN_EXPIRY = '7d';

View File

@ -1,8 +1,11 @@
import crypto from 'crypto';
// 加密密钥32字节 = 256位用于 AES-256
// 生产环境应使用环境变量
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY || 'lioncode-encryption-key-2024-sec';
// 必须通过环境变量配置
if (!process.env.ENCRYPTION_KEY) {
throw new Error('环境变量 ENCRYPTION_KEY 未配置,请在 .env.local 中设置');
}
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY;
// 确保密钥长度为32字节
const getKey = (): Buffer => {

View File

@ -2,16 +2,16 @@ import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
import { jwtVerify } from 'jose';
// JWT 密钥
// JWT 密钥(必须通过环境变量配置)
const JWT_SECRET = new TextEncoder().encode(
process.env.JWT_SECRET || 'lioncode-jwt-secret-key-2024'
process.env.JWT_SECRET || ''
);
// Cookie 名称
const AUTH_COOKIE_NAME = 'lioncode_auth_token';
// 需要登录才能访问的路由
const protectedRoutes = ['/', '/chat', '/settings'];
const protectedRoutes = ['/', '/chat', '/settings', '/assistants', '/notes'];
// 公开路由(已登录用户访问会重定向到首页)
const publicRoutes = ['/login', '/register', '/reset-password'];

View File

@ -42,6 +42,13 @@ import {
type MetasoReaderInput,
type MetasoReaderResponse,
} from './metasoReader';
import {
translate,
formatTranslateResult,
formatTranslateResultShort,
type TranslateInput,
type TranslateResponse,
} from './translate';
import { shouldUsePyodide, analyzeCode, type LoadingCallback } from './codeAnalyzer';
// 导出代码分析函数供外部使用
@ -208,6 +215,22 @@ export async function executeTool(
};
}
case 'youdao_translate': {
const text = String(input.text || '');
const from = input.from ? String(input.from) : 'auto';
const to = input.to ? String(input.to) : 'zh-CHS';
const translateInput: TranslateInput = { text, from, to };
const response: TranslateResponse = await translate(translateInput);
return {
success: response.success,
fullResult: formatTranslateResult(response, text),
displayResult: formatTranslateResultShort(response, text),
rawData: response,
};
}
default:
return {
success: false,
@ -255,5 +278,11 @@ export function getAvailableTools() {
description: '秘塔AI网页读取返回Markdown格式',
icon: '📄',
},
{
id: 'youdao_translate',
name: '翻译',
description: '有道智云高质量多语言翻译支持100+种语言',
icon: '🌐',
},
];
}

View File

@ -0,0 +1,314 @@
/**
*
* 使API实现高质量多语言翻译
*
* API文档: https://ai.youdao.com/DOCSIRMA/html/trans/api/wbfy/index.html
*/
import crypto from 'crypto';
// ============== 类型定义 ==============
export interface TranslateInput {
/** 待翻译文本 */
text: string;
/** 源语言代码默认auto自动检测 */
from?: string;
/** 目标语言代码默认zh-CHS简体中文 */
to?: string;
}
export interface TranslateResponse {
/** 是否成功 */
success: boolean;
/** 翻译结果 */
translation?: string;
/** 检测到的源语言 */
from?: string;
/** 目标语言 */
to?: string;
/** 错误信息 */
error?: string;
/** 原始API响应 */
rawResponse?: YoudaoApiResponse;
}
/** 有道API原始响应 */
interface YoudaoApiResponse {
errorCode: string;
query?: string;
translation?: string[];
l?: string;
dict?: { url: string };
webdict?: { url: string };
tSpeakUrl?: string;
speakUrl?: string;
}
// ============== 有道API配置 ==============
const YOUDAO_APP_KEY = process.env.YOUDAO_APP_KEY || '';
const YOUDAO_APP_SECRET = process.env.YOUDAO_APP_SECRET || '';
const YOUDAO_API_URL = 'https://openapi.youdao.com/api';
// ============== 错误码映射 ==============
const ERROR_CODE_MAP: Record<string, string> = {
'101': '缺少必填参数',
'102': '不支持的语言类型',
'103': '翻译文本过长最大5000字符',
'108': '应用ID无效',
'110': '无相关服务的有效应用',
'111': '开发者账号无效',
'112': '请求服务无效',
'113': '翻译文本不能为空',
'202': '签名检验失败',
'203': '访问IP地址不在可访问IP列表',
'206': '时间戳无效导致签名校验失败',
'207': '重放请求',
'301': '辞典查询失败',
'302': '翻译查询失败',
'303': '服务端异常',
'401': '账户已欠费',
'411': '访问频率受限',
'412': '长请求过于频繁',
};
// ============== 工具函数 ==============
/**
* SHA256签名
*/
function generateSign(
appKey: string,
input: string,
salt: string,
curtime: string,
appSecret: string
): string {
const signStr = appKey + input + salt + curtime + appSecret;
return crypto.createHash('sha256').update(signStr).digest('hex');
}
/**
* input参数
* >2010++10
*/
function truncate(q: string): string {
const len = q.length;
if (len <= 20) return q;
return q.substring(0, 10) + len + q.substring(len - 10, len);
}
/**
*
*/
function getErrorMessage(errorCode: string): string {
return ERROR_CODE_MAP[errorCode] || `未知错误 (错误码: ${errorCode})`;
}
// ============== 语言代码映射 ==============
/** 常用语言代码对照表 */
export const LANGUAGE_MAP: Record<string, string> = {
// 自动检测
'auto': '自动检测',
// 中文
'zh-CHS': '简体中文',
'zh-CHT': '繁体中文',
// 主要语言
'en': '英语',
'ja': '日语',
'ko': '韩语',
'fr': '法语',
'de': '德语',
'es': '西班牙语',
'pt': '葡萄牙语',
'it': '意大利语',
'ru': '俄语',
'ar': '阿拉伯语',
// 其他常用语言
'vi': '越南语',
'th': '泰语',
'id': '印度尼西亚语',
'ms': '马来语',
'hi': '印地语',
'nl': '荷兰语',
'pl': '波兰语',
'tr': '土耳其语',
'uk': '乌克兰语',
'sv': '瑞典语',
'da': '丹麦语',
'no': '挪威语',
'fi': '芬兰语',
'el': '希腊语',
'cs': '捷克语',
'hu': '匈牙利语',
'ro': '罗马尼亚语',
'bg': '保加利亚语',
'he': '希伯来语',
'yue': '粤语',
};
/**
*
*/
export function getLanguageName(code: string): string {
return LANGUAGE_MAP[code] || code;
}
// ============== 主翻译函数 ==============
/**
*
* @param input
* @returns
*/
export async function translate(input: TranslateInput): Promise<TranslateResponse> {
const { text, from = 'auto', to = 'zh-CHS' } = input;
// API Key 验证
if (!YOUDAO_APP_KEY || !YOUDAO_APP_SECRET) {
return {
success: false,
error: '有道翻译 API 未配置,请在环境变量中设置 YOUDAO_APP_KEY 和 YOUDAO_APP_SECRET',
};
}
// 参数验证
if (!text || text.trim().length === 0) {
return {
success: false,
error: '翻译文本不能为空',
};
}
if (text.length > 5000) {
return {
success: false,
error: '翻译文本过长最大支持5000字符',
};
}
try {
// 生成请求参数
const salt = crypto.randomUUID();
const curtime = Math.round(Date.now() / 1000).toString();
const sign = generateSign(
YOUDAO_APP_KEY,
truncate(text),
salt,
curtime,
YOUDAO_APP_SECRET
);
// 构建请求体
const params = new URLSearchParams({
q: text,
from,
to,
appKey: YOUDAO_APP_KEY,
salt,
sign,
signType: 'v3',
curtime,
});
// 发送请求
const response = await fetch(YOUDAO_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: params.toString(),
});
if (!response.ok) {
return {
success: false,
error: `HTTP请求失败: ${response.status} ${response.statusText}`,
};
}
const data: YoudaoApiResponse = await response.json();
// 检查API返回结果
if (data.errorCode === '0') {
// 解析语言方向 (如 "en2zh-CHS")
const langParts = data.l?.split('2') || [from, to];
const detectedFrom = langParts[0] || from;
const detectedTo = langParts[1] || to;
return {
success: true,
translation: data.translation?.join('\n') || '',
from: detectedFrom,
to: detectedTo,
rawResponse: data,
};
} else {
return {
success: false,
error: getErrorMessage(data.errorCode),
rawResponse: data,
};
}
} catch (error) {
console.error('Translate API error:', error);
return {
success: false,
error: error instanceof Error ? error.message : '翻译请求失败',
};
}
}
// ============== 格式化函数 ==============
/**
* - AI
*/
export function formatTranslateResult(
response: TranslateResponse,
originalText: string
): string {
if (!response.success) {
return `翻译失败: ${response.error}`;
}
const fromLang = getLanguageName(response.from || 'auto');
const toLang = getLanguageName(response.to || 'zh-CHS');
let result = `## 翻译结果\n\n`;
result += `**源语言**: ${fromLang}\n`;
result += `**目标语言**: ${toLang}\n\n`;
result += `### 原文\n${originalText}\n\n`;
result += `### 译文\n${response.translation}`;
return result;
}
/**
* -
*/
export function formatTranslateResultShort(
response: TranslateResponse,
originalText: string
): string {
if (!response.success) {
return `翻译失败: ${response.error}`;
}
const fromLang = getLanguageName(response.from || 'auto');
const toLang = getLanguageName(response.to || 'zh-CHS');
// 截取原文预览最多30字符
const textPreview = originalText.length > 30
? originalText.substring(0, 30) + '...'
: originalText;
let result = `> 🌐 已翻译「${textPreview}\n`;
result += `> **${fromLang}** → **${toLang}**\n\n`;
result += `${response.translation}`;
return result;
}