mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-10 18:02:55 +00:00
no message
- 添加AI助手流式会话凭证生成方法 - 优化AI助手模型获取逻辑 - 更新相关接口调用
This commit is contained in:
parent
7a6bbfac75
commit
f6e4ed7c60
@ -3,8 +3,9 @@
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Models\User;
|
||||
use App\Module\AI;
|
||||
use App\Module\Apps;
|
||||
use App\Module\Base;
|
||||
use App\Module\Ihttp;
|
||||
use Request;
|
||||
|
||||
/**
|
||||
@ -14,6 +15,11 @@ use Request;
|
||||
*/
|
||||
class AssistantController extends AbstractController
|
||||
{
|
||||
public function __construct()
|
||||
{
|
||||
Apps::isInstalledThrow('ai');
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {post} api/assistant/auth 生成授权码
|
||||
*
|
||||
@ -40,104 +46,28 @@ class AssistantController extends AbstractController
|
||||
$modelName = trim(Request::input('model_name', ''));
|
||||
$contextInput = Request::input('context', []);
|
||||
|
||||
if ($modelType === '' || $modelName === '') {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
|
||||
if (is_string($contextInput)) {
|
||||
$decoded = json_decode($contextInput, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$contextInput = $decoded;
|
||||
}
|
||||
}
|
||||
if (!is_array($contextInput)) {
|
||||
return Base::retError('context 参数格式错误');
|
||||
}
|
||||
|
||||
$context = [];
|
||||
foreach ($contextInput as $item) {
|
||||
if (!is_array($item) || count($item) < 2) {
|
||||
continue;
|
||||
}
|
||||
$role = trim((string)($item[0] ?? ''));
|
||||
$message = trim((string)($item[1] ?? ''));
|
||||
if ($role === '' || $message === '') {
|
||||
continue;
|
||||
}
|
||||
$context[] = [$role, $message];
|
||||
}
|
||||
|
||||
$contextJson = json_encode($context, JSON_UNESCAPED_UNICODE);
|
||||
if ($contextJson === false) {
|
||||
return Base::retError('context 参数格式错误');
|
||||
}
|
||||
return AI::createStreamKey($modelType, $modelName, $contextInput);
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/assistant/models 获取AI模型
|
||||
*
|
||||
* @apiDescription 获取所有AI机器人模型设置
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup assistant
|
||||
* @apiName models
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function models()
|
||||
{
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$apiKey = Base::val($setting, $modelType . '_key');
|
||||
if ($modelType === 'wenxin') {
|
||||
$wenxinSecret = Base::val($setting, 'wenxin_secret');
|
||||
if ($wenxinSecret) {
|
||||
$apiKey = trim(($apiKey ?: '') . ':' . $wenxinSecret);
|
||||
}
|
||||
}
|
||||
if ($modelType === 'ollama' && empty($apiKey)) {
|
||||
$apiKey = Base::strRandom(6);
|
||||
}
|
||||
if (empty($apiKey)) {
|
||||
return Base::retError('模型未启用');
|
||||
}
|
||||
$setting = array_filter($setting, function ($value, $key) {
|
||||
return str_ends_with($key, '_models') || str_ends_with($key, '_model');
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
|
||||
$remoteModelType = match ($modelType) {
|
||||
'qianwen' => 'qwen',
|
||||
default => $modelType,
|
||||
};
|
||||
|
||||
$authParams = [
|
||||
'api_key' => $apiKey,
|
||||
'model_type' => $remoteModelType,
|
||||
'model_name' => $modelName,
|
||||
'context' => $contextJson,
|
||||
];
|
||||
|
||||
if ($setting[$modelType . '_base_url']) {
|
||||
$authParams['base_url'] = $setting[$modelType . '_base_url'];
|
||||
}
|
||||
if ($setting[$modelType . '_agency']) {
|
||||
$authParams['agency'] = $setting[$modelType . '_agency'];
|
||||
}
|
||||
|
||||
$thinkPatterns = [
|
||||
"/^(.+?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/",
|
||||
"/^(.+?)\s*\(\s*(think|thinking|reasoning)\s*\)\s*$/"
|
||||
];
|
||||
$thinkMatch = [];
|
||||
foreach ($thinkPatterns as $pattern) {
|
||||
if (preg_match($pattern, $authParams['model_name'], $thinkMatch)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($thinkMatch && !empty($thinkMatch[1])) {
|
||||
$authParams['model_name'] = $thinkMatch[1];
|
||||
}
|
||||
|
||||
$authResult = Ihttp::ihttp_post('http://nginx/ai/invoke/auth', $authParams, 30);
|
||||
|
||||
if (Base::isError($authResult)) {
|
||||
return Base::retError($authResult['msg']);
|
||||
}
|
||||
|
||||
$body = Base::json2array($authResult['data']);
|
||||
if ($body['code'] !== 200) {
|
||||
return Base::retError($body['error'] ?: 'AI 接口返回异常', $body);
|
||||
}
|
||||
|
||||
$streamKey = Base::val($body, 'data.stream_key');
|
||||
if (empty($streamKey)) {
|
||||
return Base::retError('AI 接口返回数据异常');
|
||||
}
|
||||
|
||||
return Base::retSuccess('success', [
|
||||
'stream_key' => $streamKey,
|
||||
]);
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
}
|
||||
|
||||
@ -333,27 +333,6 @@ class SystemController extends AbstractController
|
||||
return Base::retSuccess($type == 'save' ? '保存成功' : 'success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/aibot_models 获取AI模型
|
||||
*
|
||||
* @apiDescription 获取所有AI机器人模型设置
|
||||
* @apiVersion 1.0.0
|
||||
* @apiGroup system
|
||||
* @apiName aibot_models
|
||||
*
|
||||
* @apiSuccess {Number} ret 返回状态码(1正确、0错误)
|
||||
* @apiSuccess {String} msg 返回信息(错误描述)
|
||||
* @apiSuccess {Object} data 返回数据
|
||||
*/
|
||||
public function setting__aibot_models()
|
||||
{
|
||||
$setting = Base::setting('aibotSetting');
|
||||
$setting = array_filter($setting, function($value, $key) {
|
||||
return str_ends_with($key, '_models') || str_ends_with($key, '_model');
|
||||
}, ARRAY_FILTER_USE_BOTH);
|
||||
return Base::retSuccess('success', $setting ?: json_decode('{}'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @api {get} api/system/setting/checkin 获取签到设置、保存签到设置(限管理员)
|
||||
*
|
||||
|
||||
@ -132,6 +132,126 @@ class AI
|
||||
return Base::retSuccess("success", $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成 AI 流式会话凭证
|
||||
* @param string $modelType
|
||||
* @param string $modelName
|
||||
* @param mixed $contextInput
|
||||
* @return array
|
||||
*/
|
||||
public static function createStreamKey($modelType, $modelName, $contextInput = [])
|
||||
{
|
||||
$modelType = trim((string)$modelType);
|
||||
$modelName = trim((string)$modelName);
|
||||
|
||||
if ($modelType === '' || $modelName === '') {
|
||||
return Base::retError('参数错误');
|
||||
}
|
||||
|
||||
if (is_string($contextInput)) {
|
||||
$decoded = json_decode($contextInput, true);
|
||||
if (json_last_error() === JSON_ERROR_NONE) {
|
||||
$contextInput = $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_array($contextInput)) {
|
||||
return Base::retError('context 参数格式错误');
|
||||
}
|
||||
|
||||
$context = [];
|
||||
foreach ($contextInput as $item) {
|
||||
if (!is_array($item) || count($item) < 2) {
|
||||
continue;
|
||||
}
|
||||
$role = trim((string)($item[0] ?? ''));
|
||||
$message = trim((string)($item[1] ?? ''));
|
||||
if ($role === '' || $message === '') {
|
||||
continue;
|
||||
}
|
||||
$context[] = [$role, $message];
|
||||
}
|
||||
|
||||
$contextJson = json_encode($context, JSON_UNESCAPED_UNICODE);
|
||||
if ($contextJson === false) {
|
||||
return Base::retError('context 参数格式错误');
|
||||
}
|
||||
|
||||
$setting = Base::setting('aibotSetting');
|
||||
if (!is_array($setting)) {
|
||||
$setting = [];
|
||||
}
|
||||
|
||||
$apiKey = Base::val($setting, $modelType . '_key');
|
||||
if ($modelType === 'wenxin') {
|
||||
$wenxinSecret = Base::val($setting, 'wenxin_secret');
|
||||
if ($wenxinSecret) {
|
||||
$apiKey = trim(($apiKey ?: '') . ':' . $wenxinSecret);
|
||||
}
|
||||
}
|
||||
if ($modelType === 'ollama' && empty($apiKey)) {
|
||||
$apiKey = Base::strRandom(6);
|
||||
}
|
||||
if (empty($apiKey)) {
|
||||
return Base::retError('模型未启用');
|
||||
}
|
||||
|
||||
$remoteModelType = match ($modelType) {
|
||||
'qianwen' => 'qwen',
|
||||
default => $modelType,
|
||||
};
|
||||
|
||||
$authParams = [
|
||||
'api_key' => $apiKey,
|
||||
'model_type' => $remoteModelType,
|
||||
'model_name' => $modelName,
|
||||
'context' => $contextJson,
|
||||
];
|
||||
|
||||
$baseUrl = trim((string)($setting[$modelType . '_base_url'] ?? ''));
|
||||
if ($baseUrl !== '') {
|
||||
$authParams['base_url'] = $baseUrl;
|
||||
}
|
||||
|
||||
$agency = trim((string)($setting[$modelType . '_agency'] ?? ''));
|
||||
if ($agency !== '') {
|
||||
$authParams['agency'] = $agency;
|
||||
}
|
||||
|
||||
$thinkPatterns = [
|
||||
"/^(.+?)(\s+|\s*[_-]\s*)(think|thinking|reasoning)\s*$/",
|
||||
"/^(.+?)\s*\(\s*(think|thinking|reasoning)\s*\)\s*$/"
|
||||
];
|
||||
$thinkMatch = [];
|
||||
foreach ($thinkPatterns as $pattern) {
|
||||
if (preg_match($pattern, $authParams['model_name'], $thinkMatch)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($thinkMatch && !empty($thinkMatch[1])) {
|
||||
$authParams['model_name'] = $thinkMatch[1];
|
||||
}
|
||||
|
||||
$authResult = Ihttp::ihttp_post('http://nginx/ai/invoke/auth', $authParams, 30);
|
||||
if (Base::isError($authResult)) {
|
||||
return Base::retError($authResult['msg']);
|
||||
}
|
||||
|
||||
$body = Base::json2array($authResult['data']);
|
||||
if (($body['code'] ?? null) !== 200) {
|
||||
return Base::retError(($body['error'] ?? '') ?: 'AI 接口返回异常', $body);
|
||||
}
|
||||
|
||||
$streamKey = Base::val($body, 'data.stream_key');
|
||||
if (empty($streamKey)) {
|
||||
return Base::retError('AI 接口返回数据异常');
|
||||
}
|
||||
|
||||
return Base::retSuccess('success', [
|
||||
'stream_key' => $streamKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/** ******************************************************************************************** */
|
||||
/** ******************************************************************************************** */
|
||||
/** ******************************************************************************************** */
|
||||
@ -146,6 +266,8 @@ class AI
|
||||
*/
|
||||
public static function transcriptions($filePath, $extParams = [], $extHeaders = [], $noCache = false)
|
||||
{
|
||||
Apps::isInstalledThrow('ai');
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
return Base::retError("语音文件不存在");
|
||||
}
|
||||
@ -202,6 +324,8 @@ class AI
|
||||
*/
|
||||
public static function translations($text, $targetLanguage, $noCache = false)
|
||||
{
|
||||
Apps::isInstalledThrow('ai');
|
||||
|
||||
$cacheKey = "openAItranslations::" . md5($text . '_' . $targetLanguage);
|
||||
if ($noCache) {
|
||||
Cache::forget($cacheKey);
|
||||
@ -285,6 +409,10 @@ class AI
|
||||
*/
|
||||
public static function generateTitle($text, $noCache = false)
|
||||
{
|
||||
if (!Apps::isInstalled('ai')) {
|
||||
return Base::retError('应用「AI Assistant」未安装');
|
||||
}
|
||||
|
||||
$cacheKey = "openAIGenerateTitle::" . md5($text);
|
||||
if ($noCache) {
|
||||
Cache::forget($cacheKey);
|
||||
@ -362,6 +490,10 @@ class AI
|
||||
*/
|
||||
public static function generateJokeAndSoup($noCache = false)
|
||||
{
|
||||
if (!Apps::isInstalled('ai')) {
|
||||
return Base::retError('应用「AI Assistant」未安装');
|
||||
}
|
||||
|
||||
$cacheKey = "openAIJokeAndSoup::" . md5(date('Y-m-d'));
|
||||
if ($noCache) {
|
||||
Cache::forget($cacheKey);
|
||||
|
||||
@ -44,7 +44,7 @@ class Apps
|
||||
{
|
||||
if (!self::isInstalled($appId)) {
|
||||
$name = match ($appId) {
|
||||
'ai' => 'AI Robot',
|
||||
'ai' => 'AI Assistant',
|
||||
'face' => 'Face check-in',
|
||||
'appstore' => 'AppStore',
|
||||
'approve' => 'Approval',
|
||||
|
||||
@ -446,7 +446,7 @@ class BotReceiveMsgTask extends AbstractTask
|
||||
}
|
||||
// 判断AI应用是否安装
|
||||
if (!Apps::isInstalled('ai')) {
|
||||
throw new Exception('应用「AI Robot」未安装');
|
||||
throw new Exception('应用「AI Assistant」未安装');
|
||||
}
|
||||
// 整理机器人参数
|
||||
$setting = Base::setting('aibotSetting');
|
||||
|
||||
@ -4,10 +4,7 @@
|
||||
:title="$L('AI 助手')"
|
||||
:mask-closable="false"
|
||||
:closable="false"
|
||||
:styles="{
|
||||
width: '90%',
|
||||
maxWidth: shouldCreateNewSession ? '420px' : '600px',
|
||||
}"
|
||||
:width="shouldCreateNewSession ? '420px' : '600px'"
|
||||
class-name="ai-assistant-modal">
|
||||
<div class="ai-assistant-content">
|
||||
<div
|
||||
@ -126,6 +123,7 @@ export default {
|
||||
inputModel: '',
|
||||
modelGroups: [],
|
||||
modelMap: {},
|
||||
modelsFirstLoad: true,
|
||||
modelsLoading: false,
|
||||
modelCacheKey: 'aiAssistant.model',
|
||||
cachedModelId: '',
|
||||
@ -139,7 +137,7 @@ export default {
|
||||
},
|
||||
mounted() {
|
||||
emitter.on('openAIAssistant', this.onOpenAIAssistant);
|
||||
this.initModelCache();
|
||||
this.loadCachedModel();
|
||||
},
|
||||
beforeDestroy() {
|
||||
emitter.off('openAIAssistant', this.onOpenAIAssistant);
|
||||
@ -181,6 +179,7 @@ export default {
|
||||
//
|
||||
this.responses = [];
|
||||
this.showModal = true;
|
||||
this.fetchModelOptions();
|
||||
this.clearActiveSSEClients();
|
||||
this.clearAutoSubmitTimer();
|
||||
this.$nextTick(() => {
|
||||
@ -188,14 +187,6 @@ export default {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化模型缓存与下拉数据
|
||||
*/
|
||||
async initModelCache() {
|
||||
await this.loadCachedModel();
|
||||
this.fetchModelOptions();
|
||||
},
|
||||
|
||||
/**
|
||||
* 读取缓存的模型ID
|
||||
*/
|
||||
@ -222,16 +213,30 @@ export default {
|
||||
* 拉取模型配置
|
||||
*/
|
||||
async fetchModelOptions() {
|
||||
this.modelsLoading = true;
|
||||
const needFetch = this.modelsFirstLoad
|
||||
if (needFetch) {
|
||||
this.modelsFirstLoad = false;
|
||||
this.modelsLoading = true;
|
||||
}
|
||||
try {
|
||||
const {data} = await this.$store.dispatch("call", {
|
||||
url: 'system/setting/aibot_models',
|
||||
url: 'assistant/models',
|
||||
});
|
||||
this.normalizeModelOptions(data);
|
||||
} catch (error) {
|
||||
$A.modalError(error?.msg || error || '获取模型列表失败');
|
||||
if (this.modelGroups.length > 0) {
|
||||
return;
|
||||
}
|
||||
$A.modalError({
|
||||
content: error?.msg || error || '获取模型列表失败',
|
||||
onOk: _ => {
|
||||
this.showModal = false;
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
this.modelsLoading = false;
|
||||
if (needFetch) {
|
||||
this.modelsLoading = false;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -488,6 +493,7 @@ export default {
|
||||
if (!streamKey) {
|
||||
throw new Error('获取 stream_key 失败');
|
||||
}
|
||||
this.clearActiveSSEClients();
|
||||
const sse = new SSEClient($A.mainUrl(`ai/invoke/stream/${streamKey}`));
|
||||
this.registerSSEClient(sse);
|
||||
sse.subscribe(['append', 'replace', 'done'], (type, event) => {
|
||||
@ -925,10 +931,16 @@ export default {
|
||||
|
||||
.ai-assistant-footer {
|
||||
display: flex;
|
||||
justify-content: between;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
.ai-assistant-footer-models {
|
||||
text-align: left;
|
||||
.ivu-select-disabled {
|
||||
.ivu-select-selection {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
.ivu-select-selection {
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
|
||||
@ -490,10 +490,7 @@
|
||||
v-model="delayTaskShow"
|
||||
:title="$L('任务延期')"
|
||||
:mask-closable="false"
|
||||
:styles="{
|
||||
width: '90%',
|
||||
maxWidth: '450px'
|
||||
}">
|
||||
width="450px">
|
||||
<Form
|
||||
ref="formDelayTaskRef"
|
||||
:model="delayTaskForm"
|
||||
|
||||
@ -108,10 +108,7 @@
|
||||
v-model="moveTaskShow"
|
||||
:title="$L('移动任务')"
|
||||
:mask-closable="false"
|
||||
:styles="{
|
||||
width: '90%',
|
||||
maxWidth: '540px'
|
||||
}"
|
||||
width="540px"
|
||||
footer-hide>
|
||||
<TaskMove ref="addTask" v-model="moveTaskShow" :task="task"/>
|
||||
</Modal>
|
||||
@ -121,10 +118,7 @@
|
||||
v-model="copyTaskShow"
|
||||
:title="$L('复制任务')"
|
||||
:mask-closable="false"
|
||||
:styles="{
|
||||
width: '90%',
|
||||
maxWidth: '540px'
|
||||
}"
|
||||
width="540px"
|
||||
footer-hide>
|
||||
<TaskMove v-model="copyTaskShow" :task="task" type="copy"/>
|
||||
</Modal>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user