mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-11 18:42:54 +00:00
perf: 添加ChatGPT、Claude智能机器人
This commit is contained in:
parent
efdc4c5229
commit
569145196e
@ -658,7 +658,7 @@ class DialogController extends AbstractController
|
||||
$userid = intval(Request::input('userid'));
|
||||
$stream_url = trim(Request::input('stream_url'));
|
||||
//
|
||||
if ($userid < 1 || !str_starts_with($stream_url, 'http')) {
|
||||
if ($userid <= 0) {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
//
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\WebSocketDialog;
|
||||
use App\Models\WebSocketDialogMsg;
|
||||
use Request;
|
||||
use Session;
|
||||
use Response;
|
||||
@ -228,6 +230,72 @@ class SystemController extends AbstractController
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/aibot 03. 获取会议设置、保存AI机器人设置(限管理员)
|
||||
*
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName setting__aibot
|
||||
*
|
||||
* @apiParam {String} type
|
||||
* - get: 获取(默认)
|
||||
* - save: 保存设置(参数:['openai_key', 'openai_agency', 'claude_token', 'claude_agency'])
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function setting__aibot()
|
||||
{
|
||||
$user = User::auth('admin');
|
||||
//
|
||||
$type = trim(Request::input('type'));
|
||||
$setting = Base::setting('aibotSetting');
|
||||
if ($type == 'save') {
|
||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
||||
return Base::retError('当前环境禁止修改');
|
||||
}
|
||||
$all = Request::input();
|
||||
foreach ($all as $key => $value) {
|
||||
if (!in_array($key, [
|
||||
'openai_key',
|
||||
'openai_agency',
|
||||
'claude_token',
|
||||
'claude_agency',
|
||||
])) {
|
||||
unset($all[$key]);
|
||||
}
|
||||
}
|
||||
$backup = $setting;
|
||||
$setting = Base::setting('aibotSetting', Base::newTrim($all));
|
||||
//
|
||||
if ($backup['openai_key'] != $setting['openai_key']) {
|
||||
$botUser = User::botGetOrCreate('ai-openai');
|
||||
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "设置成功"], $botUser->userid, true, false, true);
|
||||
}
|
||||
}
|
||||
if ($backup['claude_token'] != $setting['claude_token']) {
|
||||
$botUser = User::botGetOrCreate('ai-claude');
|
||||
if ($botUser && $dialog = WebSocketDialog::checkUserDialog($botUser, $user->userid)) {
|
||||
WebSocketDialogMsg::sendMsg(null, $dialog->id, 'text', ['text' => "设置成功"], $botUser->userid, true, false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
//
|
||||
if (env("SYSTEM_SETTING") == 'disabled') {
|
||||
foreach ([
|
||||
'openai_key',
|
||||
'openai_agency',
|
||||
'claude_token',
|
||||
'claude_agency',
|
||||
] as $item) {
|
||||
$setting[$item] = substr($setting[$item], 0, 4) . str_repeat('*', strlen($setting[$item]) - 8) . substr($setting[$item], -4);
|
||||
}
|
||||
}
|
||||
//
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/checkin 04. 获取签到设置、保存签到设置(限管理员)
|
||||
*
|
||||
|
||||
@ -533,6 +533,10 @@ class User extends AbstractModel
|
||||
return url("images/avatar/default_anon.png");
|
||||
case 'approval-alert@bot.system':
|
||||
return url("images/avatar/default_approval.png");
|
||||
case 'ai-openai@bot.system':
|
||||
return url("images/avatar/default_openai.png");
|
||||
case 'ai-claude@bot.system':
|
||||
return url("images/avatar/default_claude.png");
|
||||
case 'bot-manager@bot.system':
|
||||
return url("images/avatar/default_bot.png");
|
||||
}
|
||||
@ -619,6 +623,12 @@ class User extends AbstractModel
|
||||
case 'approval-alert':
|
||||
$update['nickname'] = '审批';
|
||||
break;
|
||||
case 'ai-openai':
|
||||
$update['nickname'] = 'ChatGPT';
|
||||
break;
|
||||
case 'ai-claude':
|
||||
$update['nickname'] = 'Claude';
|
||||
break;
|
||||
case 'bot-manager':
|
||||
$update['nickname'] = '机器人管理';
|
||||
break;
|
||||
|
||||
@ -81,22 +81,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
if ($command
|
||||
&& !str_starts_with($command, '/')
|
||||
&& ($dialog->type === 'user' || $this->mention)) {
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->first();
|
||||
if ($userBot && preg_match("/^https*:\/\//", $userBot->webhook_url)) {
|
||||
Ihttp::ihttp_post($userBot->webhook_url, [
|
||||
'text' => $command,
|
||||
'token' => User::generateToken($botUser),
|
||||
'dialog_id' => $dialog->id,
|
||||
'dialog_type' => $dialog->type,
|
||||
'msg_id' => $msg->id,
|
||||
'msg_uid' => $msg->userid,
|
||||
'mention' => $this->mention ? 1 : 0,
|
||||
'bot_uid' => $botUser->userid,
|
||||
'version' => Base::getVersion(),
|
||||
], 10);
|
||||
$userBot->webhook_num++;
|
||||
$userBot->save();
|
||||
}
|
||||
$this->botManagerWebhook($command, $msg, $botUser, $dialog);
|
||||
}
|
||||
if ($dialog->type !== 'user') {
|
||||
return;
|
||||
@ -366,6 +351,76 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 机器人处理 Webhook
|
||||
* @param string $command
|
||||
* @param WebSocketDialogMsg $msg
|
||||
* @param User $botUser
|
||||
* @param WebSocketDialog $dialog
|
||||
* @return void
|
||||
*/
|
||||
private function botManagerWebhook(string $command, WebSocketDialogMsg $msg, User $botUser, WebSocketDialog $dialog)
|
||||
{
|
||||
$serverUrl = 'http://' . env('APP_IPPR') . '.3';
|
||||
$userBot = null;
|
||||
$extras = [];
|
||||
switch ($botUser->email) {
|
||||
// ChatGPT 机器人
|
||||
case 'ai-openai@bot.system':
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$webhookUrl = "{$serverUrl}/ai/openai/send";
|
||||
$extras = [
|
||||
'openai_key' => $setting['openai_key'],
|
||||
'openai_agency' => $setting['openai_agency'],
|
||||
'server_url' => $serverUrl,
|
||||
];
|
||||
if (empty($extras['openai_key'])) {
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => 'Robot disabled'], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
|
||||
return;
|
||||
}
|
||||
break;
|
||||
// Claude 机器人
|
||||
case 'ai-claude@bot.system':
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$webhookUrl = "{$serverUrl}/ai/claude/send";
|
||||
$extras = [
|
||||
'claude_token' => $setting['claude_token'],
|
||||
'claude_agency' => $setting['claude_agency'],
|
||||
'server_url' => $serverUrl,
|
||||
];
|
||||
if (empty($extras['claude_token'])) {
|
||||
WebSocketDialogMsg::sendMsg(null, $msg->dialog_id, 'text', ['text' => 'Robot disabled'], $botUser->userid, false, false, true); // todo 未能在任务end事件来发送任务
|
||||
return;
|
||||
}
|
||||
break;
|
||||
// 其他机器人
|
||||
default:
|
||||
$userBot = UserBot::whereBotId($botUser->userid)->first();
|
||||
$webhookUrl = $userBot?->webhook_url;
|
||||
break;
|
||||
}
|
||||
if (!preg_match("/^https*:\/\//", $webhookUrl)) {
|
||||
return;
|
||||
}
|
||||
//
|
||||
Ihttp::ihttp_post($webhookUrl, [
|
||||
'text' => $command,
|
||||
'token' => User::generateToken($botUser),
|
||||
'dialog_id' => $dialog->id,
|
||||
'dialog_type' => $dialog->type,
|
||||
'msg_id' => $msg->id,
|
||||
'msg_uid' => $msg->userid,
|
||||
'mention' => $this->mention ? 1 : 0,
|
||||
'bot_uid' => $botUser->userid,
|
||||
'version' => Base::getVersion(),
|
||||
'extras' => Base::array2json($extras)
|
||||
], 10);
|
||||
if ($userBot) {
|
||||
$userBot->webhook_num++;
|
||||
$userBot->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $botId
|
||||
* @param $userid
|
||||
|
||||
@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use App\Models\User;
|
||||
use App\Models\WebSocketDialog;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
|
||||
class CreateDefaultRobot extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function up()
|
||||
{
|
||||
User::botGetOrCreate('ai-openai');
|
||||
User::botGetOrCreate('ai-claude');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function down()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
@ -3,7 +3,7 @@ version: '3'
|
||||
services:
|
||||
php:
|
||||
container_name: "dootask-php-${APP_ID}"
|
||||
image: "kuaifan/php:swoole-8.0.rc10"
|
||||
image: "kuaifan/php:swoole-8.0.rc11"
|
||||
shm_size: "2gb"
|
||||
ulimits:
|
||||
core:
|
||||
@ -12,6 +12,7 @@ services:
|
||||
volumes:
|
||||
- ./docker/crontab/crontab.conf:/etc/supervisor/conf.d/crontab.conf
|
||||
- ./docker/php/php.conf:/etc/supervisor/conf.d/php.conf
|
||||
- ./docker/ai/ai.conf:/etc/supervisor/conf.d/ai.conf
|
||||
- ./docker/php/php.ini:/usr/local/etc/php/php.ini
|
||||
- ./docker/log/supervisor:/var/log/supervisor
|
||||
- ./:/var/www
|
||||
|
||||
1
docker/ai/.gitignore
vendored
Normal file
1
docker/ai/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
.cache
|
||||
10
docker/ai/ai.conf
Normal file
10
docker/ai/ai.conf
Normal file
@ -0,0 +1,10 @@
|
||||
[program:ai]
|
||||
directory=/var/www/docker/ai
|
||||
command=/usr/lib/doo/doo_cli ai
|
||||
numprocs=1
|
||||
autostart=true
|
||||
autorestart=true
|
||||
startretries=100
|
||||
user=root
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/supervisor/%(program_name)s.log
|
||||
@ -183,6 +183,15 @@ server {
|
||||
proxy_set_header Content-Length $request_length;
|
||||
proxy_pass http://service/api/approve/verifyToken;
|
||||
}
|
||||
|
||||
# AI
|
||||
location /ai/ {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Scheme $scheme;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection $connection_upgrade;
|
||||
proxy_pass http://php:8881/;
|
||||
}
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/conf.d/*.conf;
|
||||
|
||||
BIN
public/images/avatar/default_claude.png
Normal file
BIN
public/images/avatar/default_claude.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
BIN
public/images/avatar/default_openai.png
Normal file
BIN
public/images/avatar/default_openai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<div class="setting-component-item">
|
||||
<Form ref="formData" :model="formData" :rules="ruleData" label-width="auto" @submit.native.prevent>
|
||||
<div class="block-setting-box">
|
||||
<h3>{{ $L('ChatGTP') }}</h3>
|
||||
<div class="form-box">
|
||||
<FormItem label="Key" prop="openai_key">
|
||||
<Input :maxlength="255" v-model="formData.openai_key"/>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('使用代理')" prop="openai_agency">
|
||||
<Input :maxlength="500" v-model="formData.openai_agency" :placeholder="$L('支持 http 或 socks 代理')"/>
|
||||
</FormItem>
|
||||
</div>
|
||||
</div>
|
||||
<div class="block-setting-box">
|
||||
<h3>{{ $L('Claude') }}</h3>
|
||||
<div class="form-box">
|
||||
<FormItem label="Token" prop="claude_token">
|
||||
<Input :maxlength="255" v-model="formData.claude_token"/>
|
||||
</FormItem>
|
||||
<FormItem :label="$L('使用代理')" prop="claude_agency">
|
||||
<Input :maxlength="500" v-model="formData.claude_agency" :placeholder="$L('支持 http 或 socks 代理')"/>
|
||||
</FormItem>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
<div class="setting-footer">
|
||||
<Button :loading="loadIng > 0" type="primary" @click="submitForm">{{ $L('提交') }}</Button>
|
||||
<Button :loading="loadIng > 0" @click="resetForm" style="margin-left: 8px">{{ $L('重置') }}</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "SystemAibot",
|
||||
data() {
|
||||
return {
|
||||
loadIng: 0,
|
||||
formData: {},
|
||||
ruleData: {},
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.systemSetting();
|
||||
},
|
||||
|
||||
methods: {
|
||||
submitForm() {
|
||||
this.$refs.formData.validate((valid) => {
|
||||
if (valid) {
|
||||
this.systemSetting(true);
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
resetForm() {
|
||||
this.formData = $A.cloneJSON(this.formDatum_bak);
|
||||
},
|
||||
|
||||
systemSetting(save) {
|
||||
this.loadIng++;
|
||||
this.$store.dispatch("call", {
|
||||
url: 'system/setting/aibot?type=' + (save ? 'save' : 'all'),
|
||||
data: this.formData,
|
||||
}).then(({data}) => {
|
||||
if (save) {
|
||||
$A.messageSuccess('修改成功');
|
||||
}
|
||||
this.formData = data;
|
||||
this.formDatum_bak = $A.cloneJSON(this.formData);
|
||||
}).catch(({msg}) => {
|
||||
if (save) {
|
||||
$A.modalError(msg);
|
||||
}
|
||||
}).finally(_ => {
|
||||
this.loadIng--;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@ -10,6 +10,9 @@
|
||||
<TabPane :label="$L('项目模板')" name="columnTemplate">
|
||||
<SystemColumnTemplate/>
|
||||
</TabPane>
|
||||
<TabPane :label="$L('AI机器人')" name="aibot">
|
||||
<SystemAibot/>
|
||||
</TabPane>
|
||||
<TabPane :label="$L('会议功能')" name="meeting">
|
||||
<SystemMeeting/>
|
||||
</TabPane>
|
||||
@ -38,9 +41,11 @@ import SystemAppPush from "./components/SystemAppPush";
|
||||
import SystemMeeting from "./components/SystemMeeting";
|
||||
import SystemCheckin from "./components/SystemCheckin";
|
||||
import SystemThirdAccess from "./components/SystemThirdAccess";
|
||||
import SystemAibot from "./components/SystemAibot.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
SystemAibot,
|
||||
SystemThirdAccess,
|
||||
SystemCheckin,
|
||||
SystemMeeting,
|
||||
|
||||
BIN
resources/assets/statics/public/images/avatar/default_claude.png
Normal file
BIN
resources/assets/statics/public/images/avatar/default_claude.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
BIN
resources/assets/statics/public/images/avatar/default_openai.png
Normal file
BIN
resources/assets/statics/public/images/avatar/default_openai.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
Loading…
x
Reference in New Issue
Block a user