diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f19e98c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/vendor/ +composer.lock +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..82b84d2 --- /dev/null +++ b/README.md @@ -0,0 +1,26 @@ +# baidu-translate-plugin + +炸毛框架插件 - 百度翻译 API 插件。 + +安装框架后,可直接安装插件到框架。 + +## 安装方法 + +```bash +./zhamao plugin:install https://github.com/zhamao-robot/baidu-translate-plugin.git +``` + +## 配置方法 + +安装插件后第一次启动将会生成一个 `config/baidu-translate.json` 配置文件,需要填入百度翻译开发平台的 appid 和 sec_key。 + +appid 和 sec_key 可在 获取。 + +## 使用方法 + +安装插件后,可匹配如下对话: + +- `翻译 xxx` +- `把xx翻译成yy` +- `xx用yy怎么翻译` +- `用yy咋翻译xx` diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..fe68497 --- /dev/null +++ b/composer.json @@ -0,0 +1,7 @@ +{ + "autoload": { + "psr-4": { + "BaiduTranslate\\": "src/" + } + } +} \ No newline at end of file diff --git a/src/BaiduTranslate.php b/src/BaiduTranslate.php new file mode 100644 index 0000000..e9ec0c3 --- /dev/null +++ b/src/BaiduTranslate.php @@ -0,0 +1,270 @@ + "zh", + "汉语" => "zh", + "中文" => "zh", + "普通话" => "zh", + "英语" => "en", + "英文" => "en", + "法语" => "fra", + "日语" => "jp", + "日本语" => "jp", + "日文" => "jp", + "韩语" => "kor", + "西班牙语" => "spa", + "泰语" => "th", + "阿拉伯语" => "ara", + "俄语" => "ru", + "葡萄牙语" => "pt", + "德语" => "de", + "意大利语" => "it", + "希腊语" => "el", + "荷兰语" => "nl", + "波兰语" => "pl", + "保加利亚语" => "bul", + "丹麦语" => "dan", + "爱沙尼亚语" => "est", + "芬兰语" => "fin", + "捷克语" => "cs", + "罗马尼亚语" => "rom", + "斯洛文尼亚语" => "slo", + "瑞典语" => "swe", + "匈牙利语" => "hu", + "繁体中文" => "cht", + "越南语" => "vie" + + /* + "世界语" => "eo", + "夏威夷语" => "haw", + "土耳其语" => "tr", + "乌克兰语" => "uk", + "马来语" => "ms", + "拉丁语" => "la", + "爪哇语" => "jv", + */ + ]; + + #[Init()] + public function init(): void + { + // 初始化配置文件 + if (config('baidu-translate') === null) { + logger()->notice('百度翻译插件还没有配置文件,正在为你生成,请到 config/baidu-translate.json 填入你的配置项'); + file_put_contents(WORKING_DIR . '/config/baidu-translate.json', json_encode(['appid' => '','seckey' => '','req_timeout' => 10], JSON_PRETTY_PRINT)); + } + } + + /** + * 匹配 翻译 命令、翻译 xxx 命令 + * @throws OneBot12Exception + */ + #[BotCommand(match: '翻译', level: 23)] + #[BotCommand(start_with: '翻译', level: 21)] + #[CommandArgument(name: 'content', type: 'string', required: true, prompt: '请输入你要翻译的内容')] + public function translateMatch(BotContext $ctx): void + { + $this->emitTranslate($ctx, $ctx->getParamString('content')); + } + + /** + * 用{英语}怎么翻译{你好} + * @throws OneBot12Exception + */ + #[BotCommand(pattern: '*用*咋翻译*', level: 25)] + #[BotCommand(pattern: '*用*怎么翻译*', level: 24)] + #[CommandArgument(name: 'b1', type: 'string', required: false, default: '')] + #[CommandArgument(name: 'b2', type: 'string', required: false, default: '')] + #[CommandArgument(name: 'b3', type: 'string', required: false, default: '')] + public function translatePattern1(BotContext $ctx): void + { + $b2 = $ctx->getParamString('b2'); + // 语言如果是空的,那玩犊子 + if ($b2 === '') { + return; + } + // 右边的语气词去掉 + $b3 = $this->rtrimLang($ctx->getParamString('b3'), '吧呗啊啦呀!。?'); + $content = $b1 = $ctx->getParamString('b1'); + if ($b1 === '') { + $content = $b3; + } + // 空白是无效的 + if ($content === '') { + return; + } + // 获取目标语言,不在支持的列表里就不算命令 + if (($target_lang = $this->getTargetLanguage($b2)) === null) { + return; + } + // 调用翻译逻辑 + $this->emitTranslate($ctx, $content, target_lang: $target_lang); + } + + /** + * 把{你好}翻译成{日语} + * @throws OneBot12Exception + */ + #[BotCommand(regex: '[把将](.*)翻译[为成](.*)', level: 22)] + #[CommandArgument(name: 'b1', type: 'string', required: false, default: '')] + #[CommandArgument(name: 'b2', type: 'string', required: false, default: '')] + public function translatePattern2(BotContext $ctx): void + { + // 右边的语气词去掉 + $b1 = $this->rtrimLang($ctx->getParamString('b1'), '吧呗。.!'); + if ($b1 === '') { + return; + } + // 获取目标语言,不在支持的列表里就不算命令 + if (($target_lang = $this->getTargetLanguage($ctx->getParamString('b2'))) === null) { + return; + } + // 调用翻译逻辑 + $this->emitTranslate($ctx, $b1, target_lang: $target_lang); + } + + /** + * @throws OneBot12Exception + */ + private function emitTranslate(BotContext $ctx, string $content, ?string $origin_lang = null, ?string $target_lang = null): void + { + if (config('baidu-translate.appid', '') === '') { + $ctx->reply('你还没有配置百度翻译的 appid 和 seckey,请先到开发者平台生成并配置该配置项(config/baidu-translate.json)'); + return; + } + try { + // 没有目标语言,需要先查询源语言是啥,或自动设定为中文。中文要翻译为英文 + if ($target_lang === null) { + $language_type = $this->requestLanguageType($content); + $target_lang = $language_type === 'zh' ? 'en' : 'zh'; + } + // 设置参数 + $args = [ + 'q' => $content, + 'appid' => config('baidu-translate.appid'), + 'salt' => random_int(100000, 999999), + 'from' => $origin_lang ?? 'auto', + 'to' => $target_lang, + 'tts' => "0", + 'dict' => "0", + ]; + $args['sign'] = $this->buildSign($content, $args['appid'], $args['salt'], config('baidu-translate.seckey')); + // 发起请求 + $ret = ZMRequest::post( + url: 'http://api.fanyi.baidu.com/api/trans/vip/translate', + header: [], + data: $args, + config: ['timeout' => config('baidu-translate.req_timeout', 5)] + ); + if ($ret === false) { + throw new TranslateException('无法请求翻译 API'); + } + $ret = json_decode($ret, true); + if (($ret['error_code'] ?? 52000) !== 52000) { + throw new TranslateException('通用翻译API返回错误:' . $ret['error_code'], -1303); + } + $result = [ + "retcode" => 0, + "origin" => $ret["from"], + "target" => $ret["to"], + "result" => $ret["trans_result"][0]["dst"], + "src" => $ret["trans_result"][0]["src"] + ]; + // if (isset($ret["src_tts"])) $result["src_tts"] = $ret["src_tts"]; + // if (isset($ret["dst_tts"])) $result["dst_tts"] = $ret["dst_tts"]; + // if (isset($ret["dict"])) $result["dict"] = $ret["dict"]; + // 发送 + $msg = '翻译结果'; + $msg .= "\n[{$this->getLanguageName($result['origin'])} -> {$this->getLanguageName($result['target'])}]"; + $msg .= "\n{$result['src']}"; + $msg .= "\n\n{$result['result']}"; + $ctx->reply($msg); + } catch (TranslateException $e) { + $ctx->reply("翻译出错,错误代码:[{$e->getCode()}],内容:{$e->getMessage()}"); + } + } + + /** + * @throws TranslateException + */ + private function requestLanguageType(string $content): string + { + $args = [ + 'q' => $content, + 'appid' => config('baidu-translate.appid'), + 'salt' => random_int(100000, 999999), + ]; + $args['sign'] = $this->buildSign($content, $args['appid'], $args['salt'], config('baidu-translate.seckey')); + $ret = ZMRequest::post( + url: "http://api.fanyi.baidu.com/api/trans/vip/language", + header: [], + data: $args, + config: ['timeout' => config('baidu-translate.req_timeout', 5)] + ); + if ($ret === false) { + throw new TranslateException('请求语种查询出错', -1301); + } + $ret = json_decode($ret, true); + if ($ret['error_code'] !== 0) { + throw new TranslateException("语种查询出错,返回:{$ret['error_code']} -> {$ret['error_msg']}", -1302); + } + return $ret['data']['src']; + } + + private function buildSign($msg, $app_id, $salt, $sec_key): string + { + $str = $app_id . $msg . $salt . $sec_key; + return md5($str); + } + + private function getTargetLanguage(string $target_lang): ?string + { + return self::LANG_CH[trim($target_lang)] ?? null; + } + + private function getLanguageName($lang): ?string + { + foreach (self::LANG_CH as $k => $v) { + if ($v == $lang) return $k; + } + return null; + } + + private function rtrimLang($string, $trim, $encoding = "utf-8"): string + { + $mask = []; + $trimLength = mb_strlen($trim, $encoding); + for ($i = 0; $i < $trimLength; $i++) { + $item = mb_substr($trim, $i, 1, $encoding); + $mask[] = $item; + } + + $len = mb_strlen($string, $encoding); + if ($len > 0) { + $i = $len - 1; + do { + $item = mb_substr($string, $i, 1, $encoding); + if (in_array($item, $mask)) { + $len--; + } else { + break; + } + } while ($i-- != 0); + } + + return mb_substr($string, 0, $len, $encoding); + } +} diff --git a/src/TranslateException.php b/src/TranslateException.php new file mode 100644 index 0000000..4e66530 --- /dev/null +++ b/src/TranslateException.php @@ -0,0 +1,8 @@ +