mirror of
https://github.com/kuaifan/dootask.git
synced 2026-02-09 07:55:36 +00:00
faet: 新增文本消息长按翻译功能
This commit is contained in:
parent
a4a9ab8d2d
commit
0c64cf0546
@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api;
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
use App\Tasks\PushTask;
|
|
||||||
use DB;
|
use DB;
|
||||||
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
|
||||||
use Request;
|
use Request;
|
||||||
use Redirect;
|
use Redirect;
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use App\Tasks\PushTask;
|
||||||
use App\Models\File;
|
use App\Models\File;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Module\Base;
|
use App\Module\Base;
|
||||||
@ -20,6 +19,8 @@ use App\Models\WebSocketDialogMsg;
|
|||||||
use App\Models\WebSocketDialogUser;
|
use App\Models\WebSocketDialogUser;
|
||||||
use App\Models\WebSocketDialogMsgRead;
|
use App\Models\WebSocketDialogMsgRead;
|
||||||
use App\Models\WebSocketDialogMsgTodo;
|
use App\Models\WebSocketDialogMsgTodo;
|
||||||
|
use App\Models\WebSocketDialogMsgTranslate;
|
||||||
|
use Hhxsv5\LaravelS\Swoole\Task\Task;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @apiDefine dialog
|
* @apiDefine dialog
|
||||||
@ -1539,6 +1540,75 @@ class DialogController extends AbstractController
|
|||||||
return Base::retSuccess("success", $msg);
|
return Base::retSuccess("success", $msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @api {get} api/dialog/msg/translation 31. 翻译消息
|
||||||
|
*
|
||||||
|
* @apiDescription 将文本消息翻译成当前语言,需要token身份
|
||||||
|
* @apiVersion 1.0.0
|
||||||
|
* @apiGroup dialog
|
||||||
|
* @apiName msg__translation
|
||||||
|
*
|
||||||
|
* @apiParam {Number} msg_id 消息ID
|
||||||
|
*
|
||||||
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
|
* @apiSuccess {Object} data 返回数据
|
||||||
|
*/
|
||||||
|
public function msg__translation()
|
||||||
|
{
|
||||||
|
User::auth();
|
||||||
|
//
|
||||||
|
$msg_id = intval(Request::input("msg_id"));
|
||||||
|
$language = Base::headerOrInput('language');
|
||||||
|
$targetLanguage = match ($language) {
|
||||||
|
"zh" => "简体中文",
|
||||||
|
"zh-CHT" => "繁体中文",
|
||||||
|
"en" => "英语",
|
||||||
|
"ko" => "韩语",
|
||||||
|
"ja" => "日语",
|
||||||
|
"de" => "德语",
|
||||||
|
"fr" => "法语",
|
||||||
|
"id" => "印度尼西亚语",
|
||||||
|
"ru" => "俄语",
|
||||||
|
default => '',
|
||||||
|
};
|
||||||
|
//
|
||||||
|
if (empty($targetLanguage)) {
|
||||||
|
return Base::retError("参数错误");
|
||||||
|
}
|
||||||
|
$msg = WebSocketDialogMsg::whereId($msg_id)->first();
|
||||||
|
if (empty($msg)) {
|
||||||
|
return Base::retError("消息不存在或已被删除");
|
||||||
|
}
|
||||||
|
if (!in_array($msg->type, ['text', 'record'])) {
|
||||||
|
return Base::retError("此消息不支持翻译");
|
||||||
|
}
|
||||||
|
WebSocketDialog::checkDialog($msg->dialog_id);
|
||||||
|
//
|
||||||
|
$row = WebSocketDialogMsgTranslate::whereMsgId($msg_id)->whereLanguage($language)->first();
|
||||||
|
if ($row) {
|
||||||
|
return Base::retSuccess("success", $row->only(['msg_id', 'language', 'content']));
|
||||||
|
}
|
||||||
|
//
|
||||||
|
$msgData = Base::json2array($msg->getRawOriginal('msg'));
|
||||||
|
if (empty($msgData['text'])) {
|
||||||
|
return Base::retError("消息内容为空");
|
||||||
|
}
|
||||||
|
$res = Extranet::openAItranslations($msgData['text'], $targetLanguage);
|
||||||
|
if (Base::isError($res)) {
|
||||||
|
return $res;
|
||||||
|
}
|
||||||
|
$row = WebSocketDialogMsgTranslate::createInstance([
|
||||||
|
'dialog_id' => $msg->dialog_id,
|
||||||
|
'msg_id' => $msg_id,
|
||||||
|
'language' => $language,
|
||||||
|
'content' => $res['data'],
|
||||||
|
]);
|
||||||
|
$row->save();
|
||||||
|
//
|
||||||
|
return Base::retSuccess("success", $row->only(['msg_id', 'language', 'content']));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @api {get} api/dialog/msg/mark 32. 消息标记操作
|
* @api {get} api/dialog/msg/mark 32. 消息标记操作
|
||||||
*
|
*
|
||||||
|
|||||||
@ -40,7 +40,7 @@ class SystemController extends AbstractController
|
|||||||
* @apiParam {String} type
|
* @apiParam {String} type
|
||||||
* - get: 获取(默认)
|
* - get: 获取(默认)
|
||||||
* - all: 获取所有(需要管理员权限)
|
* - all: 获取所有(需要管理员权限)
|
||||||
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'image_compress', 'image_save_local', 'start_home'])
|
* - save: 保存设置(参数:['reg', 'reg_identity', 'reg_invite', 'temp_account_alias', 'login_code', 'password_policy', 'project_invite', 'chat_information', 'anon_message', 'voice2text', 'translation', 'e2e_message', 'auto_archived', 'archived_day', 'task_visible', 'task_default_time', 'all_group_mute', 'all_group_autoin', 'user_private_chat_mute', 'user_group_chat_mute', 'image_compress', 'image_save_local', 'start_home'])
|
||||||
|
|
||||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||||
@ -67,6 +67,7 @@ class SystemController extends AbstractController
|
|||||||
'chat_information',
|
'chat_information',
|
||||||
'anon_message',
|
'anon_message',
|
||||||
'voice2text',
|
'voice2text',
|
||||||
|
'translation',
|
||||||
'e2e_message',
|
'e2e_message',
|
||||||
'auto_archived',
|
'auto_archived',
|
||||||
'archived_day',
|
'archived_day',
|
||||||
@ -97,6 +98,9 @@ class SystemController extends AbstractController
|
|||||||
if ($all['voice2text'] == 'open' && empty(Base::settingFind('aibotSetting', 'openai_key'))) {
|
if ($all['voice2text'] == 'open' && empty(Base::settingFind('aibotSetting', 'openai_key'))) {
|
||||||
return Base::retError('开启语音转文字功能需要在应用中开启 ChatGPT AI 机器人。');
|
return Base::retError('开启语音转文字功能需要在应用中开启 ChatGPT AI 机器人。');
|
||||||
}
|
}
|
||||||
|
if ($all['translation'] == 'open' && empty(Base::settingFind('aibotSetting', 'openai_key'))) {
|
||||||
|
return Base::retError('开启翻译功能需要在应用中开启 ChatGPT AI 机器人。');
|
||||||
|
}
|
||||||
$setting = Base::setting('system', Base::newTrim($all));
|
$setting = Base::setting('system', Base::newTrim($all));
|
||||||
} else {
|
} else {
|
||||||
$setting = Base::setting('system');
|
$setting = Base::setting('system');
|
||||||
@ -118,6 +122,7 @@ class SystemController extends AbstractController
|
|||||||
$setting['chat_information'] = $setting['chat_information'] ?: 'optional';
|
$setting['chat_information'] = $setting['chat_information'] ?: 'optional';
|
||||||
$setting['anon_message'] = $setting['anon_message'] ?: 'open';
|
$setting['anon_message'] = $setting['anon_message'] ?: 'open';
|
||||||
$setting['voice2text'] = $setting['voice2text'] ?: 'close';
|
$setting['voice2text'] = $setting['voice2text'] ?: 'close';
|
||||||
|
$setting['translation'] = $setting['translation'] ?: 'close';
|
||||||
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
|
$setting['e2e_message'] = $setting['e2e_message'] ?: 'close';
|
||||||
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
|
$setting['auto_archived'] = $setting['auto_archived'] ?: 'close';
|
||||||
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
|
$setting['archived_day'] = floatval($setting['archived_day']) ?: 7;
|
||||||
|
|||||||
36
app/Models/WebSocketDialogMsgTranslate.php
Normal file
36
app/Models/WebSocketDialogMsgTranslate.php
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* App\Models\WebSocketDialogMsgTranslate
|
||||||
|
*
|
||||||
|
* @property int $id
|
||||||
|
* @property int|null $dialog_id 对话ID
|
||||||
|
* @property int|null $msg_id 消息ID
|
||||||
|
* @property string|null $language 语言
|
||||||
|
* @property string|null $content 翻译内容
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelAppend()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel cancelHidden()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel change($array)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel getKeyValue()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate newModelQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate newQuery()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate query()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel remove()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|AbstractModel saveOrIgnore()
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereContent($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereDialogId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereId($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereLanguage($value)
|
||||||
|
* @method static \Illuminate\Database\Eloquent\Builder|WebSocketDialogMsgTranslate whereMsgId($value)
|
||||||
|
* @mixin \Eloquent
|
||||||
|
*/
|
||||||
|
class WebSocketDialogMsgTranslate extends AbstractModel
|
||||||
|
{
|
||||||
|
function __construct(array $attributes = [])
|
||||||
|
{
|
||||||
|
parent::__construct($attributes);
|
||||||
|
$this->timestamps = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -55,6 +55,59 @@ class Extranet
|
|||||||
return Base::retSuccess("success", $resData['text']);
|
return Base::retSuccess("success", $resData['text']);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 通过 openAI 翻译
|
||||||
|
* @param $text
|
||||||
|
* @param $targetLanguage
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public static function openAItranslations($text, $targetLanguage)
|
||||||
|
{
|
||||||
|
$systemSetting = Base::setting('system');
|
||||||
|
$aibotSetting = Base::setting('aibotSetting');
|
||||||
|
if ($systemSetting['translation'] !== 'open' || empty($aibotSetting['openai_key'])) {
|
||||||
|
return Base::retError("翻译功能未开启");
|
||||||
|
}
|
||||||
|
$extra = [
|
||||||
|
'Content-Type' => 'application/json',
|
||||||
|
'Authorization' => 'Bearer ' . $aibotSetting['openai_key'],
|
||||||
|
];
|
||||||
|
if ($aibotSetting['openai_agency']) {
|
||||||
|
$extra['CURLOPT_PROXY'] = $aibotSetting['openai_agency'];
|
||||||
|
if (str_contains($aibotSetting['openai_agency'], 'socks')) {
|
||||||
|
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_SOCKS5;
|
||||||
|
} else {
|
||||||
|
$extra['CURLOPT_PROXYTYPE'] = CURLPROXY_HTTP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$res = Ihttp::ihttp_request('https://api.openai.com/v1/chat/completions', json_encode([
|
||||||
|
"model" => "gpt-3.5-turbo",
|
||||||
|
"messages" => [
|
||||||
|
[
|
||||||
|
"role" => "system",
|
||||||
|
"content" => "你是一个专业的翻译器,翻译的结果尽量符合“项目任务管理系统”的使用,并且翻译的结果不用额外添加换行尽量保持原格式,将提供的文本翻译成“{$targetLanguage}”语言。"
|
||||||
|
],
|
||||||
|
[
|
||||||
|
"role" => "user",
|
||||||
|
"content" => $text
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]), $extra, 15);
|
||||||
|
if (Base::isError($res)) {
|
||||||
|
return Base::retError("翻译失败", $res);
|
||||||
|
}
|
||||||
|
$resData = Base::json2array($res['data']);
|
||||||
|
if (empty($resData['choices'])) {
|
||||||
|
return Base::retError("翻译失败", $resData);
|
||||||
|
}
|
||||||
|
$result = $resData['choices'][0]['message']['content'];
|
||||||
|
$result = preg_replace('/^\"|\"$/', '', $result);
|
||||||
|
if (empty($result)) {
|
||||||
|
return Base::retError("翻译失败", $result);
|
||||||
|
}
|
||||||
|
return Base::retSuccess("success", $result);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取IP地址经纬度
|
* 获取IP地址经纬度
|
||||||
* @param string $ip
|
* @param string $ip
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
class CreateWebSocketDialogMsgTranslatesTable extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function up()
|
||||||
|
{
|
||||||
|
Schema::create('web_socket_dialog_msg_translates', function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('id');
|
||||||
|
$table->bigInteger('dialog_id')->nullable()->default(0)->comment('对话ID');
|
||||||
|
$table->bigInteger('msg_id')->nullable()->default(0)->comment('消息ID');
|
||||||
|
$table->string('language', 50)->nullable()->default('')->comment('语言');
|
||||||
|
$table->longText('content')->nullable()->comment('翻译内容');
|
||||||
|
$table->index(['msg_id', 'language']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function down()
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('web_socket_dialog_msg_translates');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,11 +26,11 @@
|
|||||||
<!--详情-->
|
<!--详情-->
|
||||||
<div ref="content" class="dialog-content" :class="contentClass">
|
<div ref="content" class="dialog-content" :class="contentClass">
|
||||||
<!--文本-->
|
<!--文本-->
|
||||||
<TextMsg v-if="msgData.type === 'text'" :msg="msgData.msg" @viewText="viewText"/>
|
<TextMsg v-if="msgData.type === 'text'" :msgId="msgData.id" :msg="msgData.msg" @viewText="viewText"/>
|
||||||
<!--文件-->
|
<!--文件-->
|
||||||
<FileMsg v-else-if="msgData.type === 'file'" :msg="msgData.msg" @viewFile="viewFile" @downFile="downFile"/>
|
<FileMsg v-else-if="msgData.type === 'file'" :msg="msgData.msg" @viewFile="viewFile" @downFile="downFile"/>
|
||||||
<!--录音-->
|
<!--录音-->
|
||||||
<RecordMsg v-else-if="msgData.type === 'record'" :msg="msgData.msg" @playRecord="playRecord"/>
|
<RecordMsg v-else-if="msgData.type === 'record'" :msgId="msgData.id" :msg="msgData.msg" @playRecord="playRecord"/>
|
||||||
<!--会议-->
|
<!--会议-->
|
||||||
<MeetingMsg v-else-if="msgData.type === 'meeting'" :msg="msgData.msg" @openMeeting="openMeeting"/>
|
<MeetingMsg v-else-if="msgData.type === 'meeting'" :msg="msgData.msg" @openMeeting="openMeeting"/>
|
||||||
<!--接龙-->
|
<!--接龙-->
|
||||||
|
|||||||
@ -4,21 +4,45 @@
|
|||||||
<div class="record-time">{{recordDuration(msg.duration)}}</div>
|
<div class="record-time">{{recordDuration(msg.duration)}}</div>
|
||||||
<div class="record-icon taskfont"></div>
|
<div class="record-icon taskfont"></div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="msg.text" class="dialog-record-text">
|
|
||||||
{{msg.text}}
|
<template v-if="msg.text">
|
||||||
</div>
|
<div class="content-divider">
|
||||||
|
<span class="divider-full"></span>
|
||||||
|
</div>
|
||||||
|
<div class="content-additional">{{msg.text}}</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="translation">
|
||||||
|
<div class="content-divider">
|
||||||
|
<span></span>
|
||||||
|
<div class="divider-label">{{ translation.label }}</div>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<div class="content-additional">{{translation.value}}</div>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {mapState} from "vuex";
|
import {mapState} from "vuex";
|
||||||
|
import DialogMarkdown from "../DialogMarkdown.vue";
|
||||||
|
import {languageName} from "../../../../language";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
components: {DialogMarkdown},
|
||||||
props: {
|
props: {
|
||||||
|
msgId: Number,
|
||||||
msg: Object,
|
msg: Object,
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState(['audioPlaying']),
|
...mapState(['audioPlaying', 'cacheTranslations']),
|
||||||
|
|
||||||
|
translation() {
|
||||||
|
const translation = this.cacheTranslations.find(item => {
|
||||||
|
return item.key === `msg-${this.msgId}` && item.lang === languageName;
|
||||||
|
});
|
||||||
|
return translation ? translation : null;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
playRecord() {
|
playRecord() {
|
||||||
|
|||||||
@ -2,17 +2,40 @@
|
|||||||
<div class="content-text no-dark-content">
|
<div class="content-text no-dark-content">
|
||||||
<DialogMarkdown v-if="msg.type === 'md'" @click="viewText" :text="msg.text"/>
|
<DialogMarkdown v-if="msg.type === 'md'" @click="viewText" :text="msg.text"/>
|
||||||
<pre v-else @click="viewText" v-html="$A.formatTextMsg(msg.text, userId)"></pre>
|
<pre v-else @click="viewText" v-html="$A.formatTextMsg(msg.text, userId)"></pre>
|
||||||
|
|
||||||
|
<template v-if="translation">
|
||||||
|
<div class="content-divider">
|
||||||
|
<span></span>
|
||||||
|
<div class="divider-label">{{ translation.label }}</div>
|
||||||
|
<span></span>
|
||||||
|
</div>
|
||||||
|
<DialogMarkdown v-if="msg.type === 'md'" :text="translation.value"/>
|
||||||
|
<pre v-else v-html="$A.formatTextMsg(translation.value, userId)"></pre>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import {mapState} from "vuex";
|
||||||
import DialogMarkdown from "../DialogMarkdown.vue";
|
import DialogMarkdown from "../DialogMarkdown.vue";
|
||||||
|
import {languageName} from "../../../../language";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {DialogMarkdown},
|
components: {DialogMarkdown},
|
||||||
props: {
|
props: {
|
||||||
|
msgId: Number,
|
||||||
msg: Object,
|
msg: Object,
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState(['cacheTranslations']),
|
||||||
|
|
||||||
|
translation() {
|
||||||
|
const translation = this.cacheTranslations.find(item => {
|
||||||
|
return item.key === `msg-${this.msgId}` && item.lang === languageName;
|
||||||
|
});
|
||||||
|
return translation ? translation : null;
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
viewText(e) {
|
viewText(e) {
|
||||||
this.$emit('viewText', e);
|
this.$emit('viewText', e);
|
||||||
|
|||||||
@ -310,6 +310,10 @@
|
|||||||
<i class="taskfont"></i>
|
<i class="taskfont"></i>
|
||||||
<span>{{ $L('转文字') }}</span>
|
<span>{{ $L('转文字') }}</span>
|
||||||
</li>
|
</li>
|
||||||
|
<li v-if="actionPermission(operateItem, 'translation')" @click="onOperate('translation')">
|
||||||
|
<i class="taskfont"></i>
|
||||||
|
<span>{{ $L('翻译') }}</span>
|
||||||
|
</li>
|
||||||
<li v-for="item in operateCopys" @click="onOperate('copy', item)">
|
<li v-for="item in operateCopys" @click="onOperate('copy', item)">
|
||||||
<i class="taskfont" v-html="item.icon"></i>
|
<i class="taskfont" v-html="item.icon"></i>
|
||||||
<span>{{ $L(item.label) }}</span>
|
<span>{{ $L(item.label) }}</span>
|
||||||
@ -2897,6 +2901,10 @@ export default {
|
|||||||
this.onVoice2text()
|
this.onVoice2text()
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "translation":
|
||||||
|
this.onTranslation()
|
||||||
|
break;
|
||||||
|
|
||||||
case "copy":
|
case "copy":
|
||||||
this.onCopy(value)
|
this.onCopy(value)
|
||||||
break;
|
break;
|
||||||
@ -3020,6 +3028,9 @@ export default {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {id: msg_id} = this.operateItem
|
const {id: msg_id} = this.operateItem
|
||||||
|
if (this.isLoad(`msg-${msg_id}`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.$store.dispatch("setLoad", `msg-${msg_id}`)
|
this.$store.dispatch("setLoad", `msg-${msg_id}`)
|
||||||
this.$store.dispatch("call", {
|
this.$store.dispatch("call", {
|
||||||
url: 'dialog/msg/voice2text',
|
url: 'dialog/msg/voice2text',
|
||||||
@ -3035,6 +3046,32 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onTranslation() {
|
||||||
|
if (!this.actionPermission(this.operateItem, 'translation')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {id: msg_id} = this.operateItem
|
||||||
|
if (this.isLoad(`msg-${msg_id}`)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.$store.dispatch("setLoad", `msg-${msg_id}`)
|
||||||
|
this.$store.dispatch("call", {
|
||||||
|
url: 'dialog/msg/translation',
|
||||||
|
data: {
|
||||||
|
msg_id
|
||||||
|
},
|
||||||
|
}).then(({data}) => {
|
||||||
|
this.$store.dispatch("saveTranslation", {
|
||||||
|
key: `msg-${msg_id}`,
|
||||||
|
value: data.content,
|
||||||
|
});
|
||||||
|
}).catch(({msg}) => {
|
||||||
|
$A.messageError(msg);
|
||||||
|
}).finally(_ => {
|
||||||
|
this.$store.dispatch("cancelLoad", `msg-${msg_id}`)
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
onCopy(data) {
|
onCopy(data) {
|
||||||
if (!$A.isJson(data)) {
|
if (!$A.isJson(data)) {
|
||||||
return
|
return
|
||||||
@ -3621,10 +3658,10 @@ export default {
|
|||||||
if (item.msg.text) {
|
if (item.msg.text) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (this.isLoad(`msg-${item.id}`)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'translation':
|
||||||
|
return ['text', 'record'].includes(item.type) && item.msg.text // 文本、语音消息 支持翻译
|
||||||
}
|
}
|
||||||
return true // 返回 true 允许操作
|
return true // 返回 true 允许操作
|
||||||
},
|
},
|
||||||
|
|||||||
@ -189,6 +189,14 @@
|
|||||||
<div v-if="formDatum.voice2text == 'open'" class="form-tip">{{$L('长按语音消息可转换成文字。')}} ({{$L('需要在应用中开启 ChatGPT AI 机器人')}})</div>
|
<div v-if="formDatum.voice2text == 'open'" class="form-tip">{{$L('长按语音消息可转换成文字。')}} ({{$L('需要在应用中开启 ChatGPT AI 机器人')}})</div>
|
||||||
<div v-else class="form-tip">{{$L('关闭语音转文字功能。')}}</div>
|
<div v-else class="form-tip">{{$L('关闭语音转文字功能。')}}</div>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
<FormItem :label="$L('翻译消息')" prop="translation">
|
||||||
|
<RadioGroup v-model="formDatum.translation">
|
||||||
|
<Radio label="open">{{$L('开启')}}</Radio>
|
||||||
|
<Radio label="close">{{$L('关闭')}}</Radio>
|
||||||
|
</RadioGroup>
|
||||||
|
<div v-if="formDatum.translation == 'open'" class="form-tip">{{$L('长按文本消息可翻译成当前设置的语言。')}} ({{$L('需要在应用中开启 ChatGPT AI 机器人')}})</div>
|
||||||
|
<div v-else class="form-tip">{{$L('关闭文本消息翻译功能。')}}</div>
|
||||||
|
</FormItem>
|
||||||
<FormItem :label="$L('端到端加密')" prop="e2eMessage">
|
<FormItem :label="$L('端到端加密')" prop="e2eMessage">
|
||||||
<RadioGroup v-model="formDatum.e2e_message">
|
<RadioGroup v-model="formDatum.e2e_message">
|
||||||
<Radio label="open">{{$L('开启')}}</Radio>
|
<Radio label="open">{{$L('开启')}}</Radio>
|
||||||
|
|||||||
26
resources/assets/js/store/actions.js
vendored
26
resources/assets/js/store/actions.js
vendored
@ -1,6 +1,6 @@
|
|||||||
import {Store} from 'le5le-store';
|
import {Store} from 'le5le-store';
|
||||||
import * as openpgp from 'openpgp_hi/lightweight';
|
import * as openpgp from 'openpgp_hi/lightweight';
|
||||||
import {languageName} from "../language";
|
import {languageList, languageName} from "../language";
|
||||||
import {$callData, $urlSafe, SSEClient} from './utils'
|
import {$callData, $urlSafe, SSEClient} from './utils'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -826,6 +826,7 @@ export default {
|
|||||||
const cacheLoginEmail = await $A.IDBString("cacheLoginEmail");
|
const cacheLoginEmail = await $A.IDBString("cacheLoginEmail");
|
||||||
const cacheFileSort = await $A.IDBJson("cacheFileSort");
|
const cacheFileSort = await $A.IDBJson("cacheFileSort");
|
||||||
const cacheTaskBrowse = await $A.IDBArray("cacheTaskBrowse")
|
const cacheTaskBrowse = await $A.IDBArray("cacheTaskBrowse")
|
||||||
|
const cacheTranslations = await $A.IDBArray("cacheTranslations")
|
||||||
const cacheEmojis = await $A.IDBArray("cacheEmojis")
|
const cacheEmojis = await $A.IDBArray("cacheEmojis")
|
||||||
const userInfo = await $A.IDBJson("userInfo")
|
const userInfo = await $A.IDBJson("userInfo")
|
||||||
await $A.IDBClear();
|
await $A.IDBClear();
|
||||||
@ -835,6 +836,7 @@ export default {
|
|||||||
await $A.IDBSet("cacheLoginEmail", cacheLoginEmail);
|
await $A.IDBSet("cacheLoginEmail", cacheLoginEmail);
|
||||||
await $A.IDBSet("cacheFileSort", cacheFileSort);
|
await $A.IDBSet("cacheFileSort", cacheFileSort);
|
||||||
await $A.IDBSet("cacheTaskBrowse", cacheTaskBrowse);
|
await $A.IDBSet("cacheTaskBrowse", cacheTaskBrowse);
|
||||||
|
await $A.IDBSet("cacheTranslations", cacheTranslations);
|
||||||
await $A.IDBSet("cacheEmojis", cacheEmojis);
|
await $A.IDBSet("cacheEmojis", cacheEmojis);
|
||||||
await $A.IDBSet("cacheVersion", state.cacheVersion)
|
await $A.IDBSet("cacheVersion", state.cacheVersion)
|
||||||
|
|
||||||
@ -865,6 +867,7 @@ export default {
|
|||||||
state.cacheTasks = await $A.IDBArray("cacheTasks")
|
state.cacheTasks = await $A.IDBArray("cacheTasks")
|
||||||
state.cacheProjectParameter = await $A.IDBArray("cacheProjectParameter")
|
state.cacheProjectParameter = await $A.IDBArray("cacheProjectParameter")
|
||||||
state.cacheTaskBrowse = await $A.IDBArray("cacheTaskBrowse")
|
state.cacheTaskBrowse = await $A.IDBArray("cacheTaskBrowse")
|
||||||
|
state.cacheTranslations = await $A.IDBArray("cacheTranslations")
|
||||||
state.dialogMsgs = await $A.IDBArray("dialogMsgs")
|
state.dialogMsgs = await $A.IDBArray("dialogMsgs")
|
||||||
state.fileLists = await $A.IDBArray("fileLists")
|
state.fileLists = await $A.IDBArray("fileLists")
|
||||||
state.userInfo = await $A.IDBJson("userInfo")
|
state.userInfo = await $A.IDBJson("userInfo")
|
||||||
@ -3341,6 +3344,27 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存翻译
|
||||||
|
* @param state
|
||||||
|
* @param dispatch
|
||||||
|
* @param data {key, value}
|
||||||
|
*/
|
||||||
|
saveTranslation({state, dispatch}, data) {
|
||||||
|
if (!$A.isJson(data)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const item = state.cacheTranslations.find(item => item.key == data.key && item.lang == languageName)
|
||||||
|
if (item) {
|
||||||
|
item.value = data.value
|
||||||
|
} else {
|
||||||
|
data.lang = languageName
|
||||||
|
data.label = languageList[languageName] || languageName
|
||||||
|
state.cacheTranslations.push(data)
|
||||||
|
}
|
||||||
|
$A.IDBSave("cacheTranslations", state.cacheTranslations.slice(-200))
|
||||||
|
},
|
||||||
|
|
||||||
/** *****************************************************************************************/
|
/** *****************************************************************************************/
|
||||||
/** ************************************* loads *********************************************/
|
/** ************************************* loads *********************************************/
|
||||||
/** *****************************************************************************************/
|
/** *****************************************************************************************/
|
||||||
|
|||||||
5
resources/assets/js/store/state.js
vendored
5
resources/assets/js/store/state.js
vendored
@ -231,5 +231,8 @@ export default {
|
|||||||
model: 'details',
|
model: 'details',
|
||||||
id: 0,
|
id: 0,
|
||||||
show: false
|
show: false
|
||||||
}
|
},
|
||||||
|
|
||||||
|
// 翻译
|
||||||
|
cacheTranslations: [],
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1096,22 +1096,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-record-text {
|
|
||||||
position: relative;
|
|
||||||
margin-top: 8px;
|
|
||||||
padding-top: 8px;
|
|
||||||
&:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 1px;
|
|
||||||
background-color: rgba(255, 255, 255, 0.2);
|
|
||||||
transform: scaleY(0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.content-meeting {
|
.content-meeting {
|
||||||
@ -1412,6 +1396,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.content-divider {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 6px 0;
|
||||||
|
> span {
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background-color: rgba(255, 255, 255, 0.2);
|
||||||
|
transform: scaleY(0.5);
|
||||||
|
min-width: 18px;
|
||||||
|
}
|
||||||
|
.divider-label {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 8px;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.mention {
|
.mention {
|
||||||
color: $flow-status-end-color;
|
color: $flow-status-end-color;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user