feat: 完成意心ai agent
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
|
||||
|
||||
public class SessionCreateAndUpdateInput
|
||||
{
|
||||
public string SessionTitle { get; set; }
|
||||
public string SessionContent { get; set; }
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会话类型
|
||||
/// </summary>
|
||||
public SessionTypeEnum SessionType { get; set; } = SessionTypeEnum.Chat;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using Volo.Abp.Application.Dtos;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
|
||||
|
||||
@@ -7,4 +8,9 @@ public class SessionDto : FullAuditedEntityDto<Guid>
|
||||
public string SessionTitle { get; set; }
|
||||
public string SessionContent { get; set; }
|
||||
public string Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会话类型
|
||||
/// </summary>
|
||||
public SessionTypeEnum SessionType { get; set; }
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
using Yi.Framework.Ddd.Application.Contracts;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
using Yi.Framework.Ddd.Application.Contracts;
|
||||
|
||||
namespace Yi.Framework.AiHub.Application.Contracts.Dtos;
|
||||
|
||||
public class SessionGetListInput:PagedAllResultRequestDto
|
||||
public class SessionGetListInput : PagedAllResultRequestDto
|
||||
{
|
||||
public string? SessionTitle { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会话类型
|
||||
/// </summary>
|
||||
public SessionTypeEnum? SessionType { get; set; }
|
||||
}
|
||||
@@ -84,7 +84,8 @@ public class SessionService : CrudAppService<SessionAggregateRoot, SessionDto, G
|
||||
RefAsync<int> total = 0;
|
||||
var userId = CurrentUser.GetId();
|
||||
var entities = await _repository._DbQueryable
|
||||
.Where(x=>x.UserId == userId)
|
||||
.Where(x => x.UserId == userId)
|
||||
.WhereIF(input.SessionType.HasValue, x => x.SessionType == input.SessionType!.Value)
|
||||
.OrderByDescending(x => x.Id)
|
||||
.ToPageListAsync(input.SkipCount, input.MaxResultCount, total);
|
||||
return new PagedResultDto<SessionDto>(total, entities.Adapt<List<SessionDto>>());
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 会话类型枚举
|
||||
/// </summary>
|
||||
public enum SessionTypeEnum
|
||||
{
|
||||
/// <summary>
|
||||
/// 普通聊天
|
||||
/// </summary>
|
||||
Chat = 0,
|
||||
|
||||
/// <summary>
|
||||
/// Agent智能体
|
||||
/// </summary>
|
||||
Agent = 1
|
||||
}
|
||||
@@ -75,10 +75,7 @@ public class AnthropicChatCompletionsService(
|
||||
{
|
||||
Guid errorId = Guid.NewGuid();
|
||||
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
logger.LogError("Anthropic非流式对话异常 请求地址:{Address},ErrorId:{errorId}, StatusCode: {StatusCode.GetHashCode()} Response: {Response}",
|
||||
options.Endpoint,
|
||||
errorId,
|
||||
response.StatusCode, error);
|
||||
logger.LogError($"Anthropic非流式对话异常 请求地址:{options.Endpoint},ErrorId:{errorId}, StatusCode: {response.StatusCode.GetHashCode()}, Response: {error}");
|
||||
|
||||
throw new Exception( $"恭喜你运气爆棚遇到了错误,尊享包对话异常:StatusCode【{response.StatusCode.GetHashCode()}】,ErrorId【{errorId}】");
|
||||
}
|
||||
@@ -125,10 +122,7 @@ public class AnthropicChatCompletionsService(
|
||||
{
|
||||
Guid errorId = Guid.NewGuid();
|
||||
var error = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
|
||||
logger.LogError("Anthropic流式对话异常 请求地址:{Address},ErrorId:{errorId}, StatusCode: {StatusCode.GetHashCode()} Response: {Response}",
|
||||
options.Endpoint,
|
||||
errorId,
|
||||
response.StatusCode, error);
|
||||
logger.LogError($"Anthropic流式对话异常 请求地址:{options.Endpoint},ErrorId:{errorId}, StatusCode: {response.StatusCode.GetHashCode()}, Response: {error}");
|
||||
|
||||
throw new Exception( $"恭喜你运气爆棚遇到了错误,尊享包对话异常:StatusCode【{response.StatusCode.GetHashCode()}】,ErrorId【{errorId}】");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using SqlSugar;
|
||||
using Volo.Abp.Domain.Entities.Auditing;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Enums;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Entities.Chat;
|
||||
|
||||
@@ -9,8 +10,13 @@ public class SessionAggregateRoot : FullAuditedAggregateRoot<Guid>
|
||||
{
|
||||
public Guid UserId { get; set; }
|
||||
public string SessionTitle { get; set; }
|
||||
|
||||
|
||||
[SugarColumn(ColumnDataType = StaticConfig.CodeFirst_BigString)]
|
||||
public string SessionContent { get; set; }
|
||||
public string? Remark { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 会话类型:0-普通聊天,1-Agent智能体
|
||||
/// </summary>
|
||||
public SessionTypeEnum SessionType { get; set; } = SessionTypeEnum.Chat;
|
||||
}
|
||||
@@ -1,16 +1,199 @@
|
||||
using System.ComponentModel;
|
||||
using ModelContextProtocol.Server;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Volo.Abp.DependencyInjection;
|
||||
using Yi.Framework.AiHub.Domain.Shared.Attributes;
|
||||
|
||||
namespace Yi.Framework.AiHub.Domain.Mcp;
|
||||
|
||||
[YiAgentTool]
|
||||
public class OnlineSearchTool:ISingletonDependency
|
||||
public class OnlineSearchTool : ISingletonDependency
|
||||
{
|
||||
[YiAgentTool("联网搜索"),DisplayName("OnlineSearch"), Description("进行在线搜索")]
|
||||
public string OnlineSearch(string keyword)
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
private readonly ILogger<OnlineSearchTool> _logger;
|
||||
private readonly string _baiduApiKey;
|
||||
private const string BaiduSearchUrl = "https://qianfan.baidubce.com/v2/ai_search/web_search";
|
||||
|
||||
public OnlineSearchTool(
|
||||
IHttpClientFactory httpClientFactory,
|
||||
ILogger<OnlineSearchTool> logger,
|
||||
IConfiguration configuration)
|
||||
{
|
||||
return "奥德赛第一中学学生会会长是:郭老板";
|
||||
_httpClientFactory = httpClientFactory;
|
||||
_logger = logger;
|
||||
_baiduApiKey = configuration["BaiduSearch:ApiKey"] ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
[YiAgentTool("联网搜索"), DisplayName("OnlineSearch"), Description("进行在线搜索,获取最新的网络信息")]
|
||||
public async Task<string> OnlineSearch(string keyword)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
return "搜索关键词不能为空";
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
|
||||
// 构建请求体
|
||||
var requestBody = new BaiduSearchRequest
|
||||
{
|
||||
Messages = new List<BaiduSearchMessage>
|
||||
{
|
||||
new() { Role = "user", Content = keyword }
|
||||
}
|
||||
};
|
||||
|
||||
var jsonContent = JsonSerializer.Serialize(requestBody, BaiduJsonContext.Default.BaiduSearchRequest);
|
||||
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
|
||||
|
||||
// 设置请求头
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, BaiduSearchUrl)
|
||||
{
|
||||
Content = content
|
||||
};
|
||||
request.Headers.Add("Authorization", $"Bearer {_baiduApiKey}");
|
||||
|
||||
// 发送请求
|
||||
var response = await client.SendAsync(request);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errorContent = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError("百度搜索接口调用失败: {StatusCode}, {Error}", response.StatusCode, errorContent);
|
||||
return $"搜索失败: {response.StatusCode}";
|
||||
}
|
||||
|
||||
var responseJson = await response.Content.ReadAsStringAsync();
|
||||
var searchResult = JsonSerializer.Deserialize(responseJson, BaiduJsonContext.Default.BaiduSearchResponse);
|
||||
|
||||
if (searchResult?.References == null || searchResult.References.Count == 0)
|
||||
{
|
||||
return "未找到相关搜索结果";
|
||||
}
|
||||
|
||||
// 格式化搜索结果返回给AI
|
||||
return FormatSearchResults(searchResult.References);
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
_logger.LogError(ex, "百度搜索网络请求异常");
|
||||
return "搜索服务暂时不可用,请稍后重试";
|
||||
}
|
||||
catch (TaskCanceledException ex)
|
||||
{
|
||||
_logger.LogError(ex, "百度搜索请求超时");
|
||||
return "搜索请求超时,请稍后重试";
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
_logger.LogError(ex, "百度搜索结果解析失败");
|
||||
return "搜索结果解析失败";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "百度搜索发生未知异常");
|
||||
return "搜索发生异常,请稍后重试";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化搜索结果
|
||||
/// </summary>
|
||||
private string FormatSearchResults(List<BaiduSearchReference> references)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("搜索结果:");
|
||||
sb.AppendLine();
|
||||
|
||||
var count = 0;
|
||||
foreach (var item in references.Take(10)) // 最多返回10条
|
||||
{
|
||||
count++;
|
||||
sb.AppendLine($"【{count}】{item.Title}");
|
||||
sb.AppendLine($"来源:{item.Website} | 时间:{item.Date}");
|
||||
sb.AppendLine($"摘要:{item.Snippet}");
|
||||
sb.AppendLine($"链接:{item.Url}");
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
#region 百度搜索 DTO
|
||||
|
||||
/// <summary>
|
||||
/// 百度搜索请求
|
||||
/// </summary>
|
||||
public class BaiduSearchRequest
|
||||
{
|
||||
[JsonPropertyName("messages")]
|
||||
public List<BaiduSearchMessage> Messages { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 百度搜索消息
|
||||
/// </summary>
|
||||
public class BaiduSearchMessage
|
||||
{
|
||||
[JsonPropertyName("role")]
|
||||
public string Role { get; set; } = "user";
|
||||
|
||||
[JsonPropertyName("content")]
|
||||
public string Content { get; set; } = "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 百度搜索响应
|
||||
/// </summary>
|
||||
public class BaiduSearchResponse
|
||||
{
|
||||
[JsonPropertyName("request_id")]
|
||||
public string? RequestId { get; set; }
|
||||
|
||||
[JsonPropertyName("references")]
|
||||
public List<BaiduSearchReference>? References { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 百度搜索结果项
|
||||
/// </summary>
|
||||
public class BaiduSearchReference
|
||||
{
|
||||
[JsonPropertyName("id")]
|
||||
public int Id { get; set; }
|
||||
|
||||
[JsonPropertyName("url")]
|
||||
public string? Url { get; set; }
|
||||
|
||||
[JsonPropertyName("title")]
|
||||
public string? Title { get; set; }
|
||||
|
||||
[JsonPropertyName("date")]
|
||||
public string? Date { get; set; }
|
||||
|
||||
[JsonPropertyName("snippet")]
|
||||
public string? Snippet { get; set; }
|
||||
|
||||
[JsonPropertyName("website")]
|
||||
public string? Website { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region JSON 序列化上下文
|
||||
|
||||
[JsonSerializable(typeof(BaiduSearchRequest))]
|
||||
[JsonSerializable(typeof(BaiduSearchResponse))]
|
||||
[JsonSourceGenerationOptions(PropertyNamingPolicy = JsonKnownNamingPolicy.CamelCase)]
|
||||
internal partial class BaiduJsonContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
@@ -360,7 +360,7 @@ namespace Yi.Abp.Web
|
||||
var app = context.GetApplicationBuilder();
|
||||
app.UseRouting();
|
||||
|
||||
//app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<AiModelEntity>();
|
||||
//app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<SessionAggregateRoot>();
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<ActivationCodeRecordAggregateRoot>();
|
||||
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<UsageStatisticsAggregateRoot>();
|
||||
|
||||
|
||||
25
Yi.Ai.Vue3/src/api/agent/index.ts
Normal file
25
Yi.Ai.Vue3/src/api/agent/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import type { AgentSendInput, AgentToolOutput } from './types';
|
||||
import { get, post } from '@/utils/request';
|
||||
|
||||
/**
|
||||
* Agent 发送消息
|
||||
*/
|
||||
export function agentSend(data: AgentSendInput) {
|
||||
return post('/ai-chat/agent/send', data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 工具列表
|
||||
*/
|
||||
export function getAgentTools() {
|
||||
return post<AgentToolOutput[]>('/ai-chat/agent/tool').json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 Agent 上下文
|
||||
*/
|
||||
export function getAgentContext(sessionId: string) {
|
||||
return post<string>(`/ai-chat/agent/context/${sessionId}`).json();
|
||||
}
|
||||
|
||||
export * from './types';
|
||||
51
Yi.Ai.Vue3/src/api/agent/types.ts
Normal file
51
Yi.Ai.Vue3/src/api/agent/types.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* Agent 发送消息输入
|
||||
*/
|
||||
export interface AgentSendInput {
|
||||
/** 会话id */
|
||||
sessionId: string;
|
||||
/** 用户内容 */
|
||||
content: string;
|
||||
/** api密钥Id */
|
||||
tokenId: string;
|
||||
/** 模型id */
|
||||
modelId: string;
|
||||
/** 已选择工具 */
|
||||
tools: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 工具输出
|
||||
*/
|
||||
export interface AgentToolOutput {
|
||||
/** 工具代码 */
|
||||
code: string;
|
||||
/** 工具名称 */
|
||||
name: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 结果类型
|
||||
*/
|
||||
export type AgentResultType = 'text' | 'toolCalling' | 'toolCalled' | 'usage' | 'toolCallUsage';
|
||||
|
||||
/**
|
||||
* Agent 流式结果输出
|
||||
*/
|
||||
export interface AgentResultOutput {
|
||||
/** 类型 */
|
||||
type: AgentResultType;
|
||||
/** 内容载体 */
|
||||
content: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 用量信息
|
||||
*/
|
||||
export interface AgentUsage {
|
||||
input_tokens?: number;
|
||||
output_tokens?: number;
|
||||
total_tokens?: number;
|
||||
prompt_tokens?: number;
|
||||
completion_tokens?: number;
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
export * from './announcement'
|
||||
export * from './agent';
|
||||
export * from './auth';
|
||||
export * from './chat';
|
||||
export * from './file';
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
/**
|
||||
* 会话类型枚举
|
||||
*/
|
||||
export enum SessionTypeEnum {
|
||||
/** 普通聊天 */
|
||||
Chat = 0,
|
||||
/** Agent智能体 */
|
||||
Agent = 1,
|
||||
}
|
||||
|
||||
export interface GetSessionListParams {
|
||||
/**
|
||||
* 创建者
|
||||
@@ -61,6 +71,10 @@ export interface GetSessionListParams {
|
||||
* 用户id
|
||||
*/
|
||||
userId: number;
|
||||
/**
|
||||
* 会话类型
|
||||
*/
|
||||
sessionType?: SessionTypeEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,6 +110,10 @@ export interface ChatSessionVo {
|
||||
* 自定义的消息前缀图标字段
|
||||
*/
|
||||
prefixIcon?: Component;
|
||||
/**
|
||||
* 会话类型
|
||||
*/
|
||||
sessionType?: SessionTypeEnum;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -147,6 +165,10 @@ export interface CreateSessionDTO {
|
||||
* 用户id
|
||||
*/
|
||||
userId: number;
|
||||
/**
|
||||
* 会话类型
|
||||
*/
|
||||
sessionType?: SessionTypeEnum;
|
||||
}
|
||||
|
||||
// export interface CreateSessionVO {
|
||||
|
||||
BIN
Yi.Ai.Vue3/src/assets/images/czld.png
Normal file
BIN
Yi.Ai.Vue3/src/assets/images/czld.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 86 KiB |
@@ -1,26 +1,939 @@
|
||||
<script setup lang="ts">
|
||||
// 智能体功能 - 预留
|
||||
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
|
||||
import type { BubbleListInstance } from 'vue-element-plus-x/types/BubbleList';
|
||||
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
|
||||
import { Loading, Tools, Check, Plus, Delete } from '@element-plus/icons-vue';
|
||||
import { ElIcon, ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { Sender } from 'vue-element-plus-x';
|
||||
import { agentSend, getAgentTools, getAgentContext } from '@/api/agent';
|
||||
import type { AgentToolOutput, AgentResultOutput, AgentUsage } from '@/api/agent/types';
|
||||
import { getSelectableTokenInfo } from '@/api';
|
||||
import { useUserStore } from '@/stores/modules/user';
|
||||
import { useAgentSessionStore } from '@/stores/modules/agentSession';
|
||||
import { getUserProfilePicture } from '@/utils/user.ts';
|
||||
import YMarkdown from '@/vue-element-plus-y/components/XMarkdown/index.vue';
|
||||
import agentAvatar from '@/assets/images/czld.png';
|
||||
import '@/styles/github-markdown.css';
|
||||
import '@/styles/yixin-markdown.scss';
|
||||
|
||||
// 消息类型定义
|
||||
type MessageItem = BubbleProps & {
|
||||
key: number;
|
||||
role: 'ai' | 'user' | 'assistant';
|
||||
avatar: string;
|
||||
thinkingStatus?: ThinkingStatus;
|
||||
thinlCollapse?: boolean;
|
||||
reasoning_content?: string;
|
||||
toolCalls?: { name: string; status: 'calling' | 'called'; result?: any; usage?: { prompt: number; completion: number; total: number } }[];
|
||||
tokenUsage?: { prompt: number; completion: number; total: number };
|
||||
};
|
||||
|
||||
const userStore = useUserStore();
|
||||
const agentSessionStore = useAgentSessionStore();
|
||||
|
||||
// 响应式数据
|
||||
const inputValue = ref('');
|
||||
const bubbleItems = ref<MessageItem[]>([]);
|
||||
const bubbleListRef = ref<BubbleListInstance | null>(null);
|
||||
const isSending = ref(false);
|
||||
const sessionId = ref('');
|
||||
|
||||
// 会话列表相关
|
||||
const showSessionList = ref(true);
|
||||
|
||||
// 工具相关
|
||||
const availableTools = ref<AgentToolOutput[]>([]);
|
||||
const selectedTools = ref<string[]>([]);
|
||||
const showToolsPanel = ref(false);
|
||||
|
||||
// 配置相关
|
||||
const tokenId = ref('');
|
||||
const tokenOptions = ref<any[]>([]);
|
||||
const tokenLoading = ref(false);
|
||||
const modelId = ref('gpt-5.2-chat');
|
||||
|
||||
// 加载Token列表
|
||||
async function loadTokens() {
|
||||
tokenLoading.value = true;
|
||||
try {
|
||||
const res = await getSelectableTokenInfo();
|
||||
const data = Array.isArray(res) ? res : (res as any).data || [];
|
||||
tokenOptions.value = data;
|
||||
// 默认选中第一个可用的token
|
||||
if (tokenOptions.value.length > 0 && !tokenId.value) {
|
||||
const firstAvailable = tokenOptions.value.find(t => !t.isDisabled);
|
||||
if (firstAvailable) {
|
||||
tokenId.value = firstAvailable.tokenId;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载Token列表失败:', error);
|
||||
} finally {
|
||||
tokenLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载工具列表
|
||||
async function loadTools() {
|
||||
try {
|
||||
const res = await getAgentTools();
|
||||
availableTools.value = res.data || [];
|
||||
// 默认选中所有工具
|
||||
selectedTools.value = availableTools.value.map(t => t.code);
|
||||
} catch (error) {
|
||||
console.error('加载工具列表失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadTokens();
|
||||
loadTools();
|
||||
// 加载会话列表
|
||||
if (userStore.token) {
|
||||
agentSessionStore.requestSessionList(1);
|
||||
}
|
||||
});
|
||||
|
||||
// 创建新会话
|
||||
async function createNewSession() {
|
||||
sessionId.value = '';
|
||||
bubbleItems.value = [];
|
||||
agentSessionStore.setCurrentSession(null);
|
||||
}
|
||||
|
||||
// 选择会话
|
||||
async function selectSession(session: any) {
|
||||
sessionId.value = session.id;
|
||||
agentSessionStore.setCurrentSession(session);
|
||||
bubbleItems.value = [];
|
||||
|
||||
// 加载历史上下文
|
||||
await loadSessionContext(session.id);
|
||||
}
|
||||
|
||||
// 加载会话历史上下文
|
||||
async function loadSessionContext(sid: string) {
|
||||
try {
|
||||
const res = await getAgentContext(sid);
|
||||
|
||||
// 获取实际数据,可能是 res.data 或直接是 res
|
||||
let rawData = res?.data ?? res;
|
||||
if (!rawData) return;
|
||||
|
||||
// 如果是字符串则解析JSON
|
||||
let contextData = typeof rawData === 'string' ? JSON.parse(rawData) : rawData;
|
||||
|
||||
// 获取消息列表
|
||||
const messages = contextData?.storeState?.messages || [];
|
||||
|
||||
if (messages.length === 0) return;
|
||||
|
||||
// 转换为消息列表
|
||||
messages.forEach((msg: any, index: number) => {
|
||||
const isUser = msg.role === 'user';
|
||||
const content = msg.contents
|
||||
?.filter((c: any) => c.$type === 'text')
|
||||
?.map((c: any) => c.text)
|
||||
?.join('') || '';
|
||||
|
||||
if (content) {
|
||||
const obj: MessageItem = {
|
||||
key: index,
|
||||
avatar: isUser ? getUserProfilePicture() : agentAvatar,
|
||||
avatarSize: '32px',
|
||||
role: isUser ? 'user' : 'assistant',
|
||||
placement: isUser ? 'end' : 'start',
|
||||
isMarkdown: !isUser,
|
||||
loading: false,
|
||||
content,
|
||||
reasoning_content: '',
|
||||
thinkingStatus: 'end',
|
||||
thinlCollapse: false,
|
||||
noStyle: !isUser,
|
||||
toolCalls: [],
|
||||
};
|
||||
bubbleItems.value.push(obj);
|
||||
}
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
setTimeout(() => {
|
||||
bubbleListRef.value?.scrollToBottom();
|
||||
}, 100);
|
||||
} catch (error) {
|
||||
console.error('加载会话上下文失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 删除会话
|
||||
async function handleDeleteSession(session: any, event: Event) {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要删除该会话吗?', '提示', {
|
||||
confirmButtonText: '确定',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
});
|
||||
await agentSessionStore.deleteSession([session.id]);
|
||||
if (sessionId.value === session.id) {
|
||||
createNewSession();
|
||||
}
|
||||
ElMessage.success('删除成功');
|
||||
} catch {
|
||||
// 取消删除
|
||||
}
|
||||
}
|
||||
|
||||
// 切换工具选择
|
||||
function toggleTool(code: string) {
|
||||
const index = selectedTools.value.indexOf(code);
|
||||
if (index > -1) {
|
||||
selectedTools.value.splice(index, 1);
|
||||
} else {
|
||||
selectedTools.value.push(code);
|
||||
}
|
||||
}
|
||||
|
||||
// 添加消息
|
||||
function addMessage(message: string, isUser: boolean) {
|
||||
const i = bubbleItems.value.length;
|
||||
const obj: MessageItem = {
|
||||
key: i,
|
||||
avatar: isUser ? getUserProfilePicture() : agentAvatar,
|
||||
avatarSize: '32px',
|
||||
role: isUser ? 'user' : 'assistant',
|
||||
placement: isUser ? 'end' : 'start',
|
||||
isMarkdown: !isUser,
|
||||
loading: !isUser,
|
||||
content: message || '',
|
||||
reasoning_content: '',
|
||||
thinkingStatus: 'start',
|
||||
thinlCollapse: false,
|
||||
noStyle: !isUser,
|
||||
toolCalls: [],
|
||||
};
|
||||
bubbleItems.value.push(obj);
|
||||
}
|
||||
|
||||
// AbortController 用于取消请求
|
||||
let abortController: AbortController | null = null;
|
||||
|
||||
// 临时存储工具调用用量
|
||||
let pendingToolUsage: { prompt: number; completion: number; total: number } | null = null;
|
||||
|
||||
// 处理Agent流式数据
|
||||
function handleAgentChunk(data: AgentResultOutput) {
|
||||
const latest = bubbleItems.value[bubbleItems.value.length - 1];
|
||||
if (!latest) return;
|
||||
|
||||
switch (data.type) {
|
||||
case 'text':
|
||||
latest.content += data.content || '';
|
||||
latest.loading = false;
|
||||
break;
|
||||
case 'toolCalling':
|
||||
// 工具调用中
|
||||
if (!latest.toolCalls) latest.toolCalls = [];
|
||||
latest.toolCalls.push({
|
||||
name: data.content as string,
|
||||
status: 'calling',
|
||||
});
|
||||
// 清空待处理的用量
|
||||
pendingToolUsage = null;
|
||||
break;
|
||||
case 'toolCallUsage':
|
||||
// 工具调用用量统计 - 先保存,等 toolCalled 时再设置
|
||||
const toolUsage = data.content as AgentUsage;
|
||||
pendingToolUsage = {
|
||||
prompt: toolUsage.input_tokens || toolUsage.prompt_tokens || 0,
|
||||
completion: toolUsage.output_tokens || toolUsage.completion_tokens || 0,
|
||||
total: toolUsage.total_tokens || 0,
|
||||
};
|
||||
// 同时尝试设置到最后一个工具调用
|
||||
if (latest.toolCalls && latest.toolCalls.length > 0) {
|
||||
const lastTool = latest.toolCalls[latest.toolCalls.length - 1];
|
||||
if (lastTool) {
|
||||
lastTool.usage = pendingToolUsage;
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'toolCalled':
|
||||
// 工具调用完成
|
||||
if (latest.toolCalls && latest.toolCalls.length > 0) {
|
||||
const lastTool = latest.toolCalls[latest.toolCalls.length - 1];
|
||||
if (lastTool) {
|
||||
lastTool.status = 'called';
|
||||
lastTool.result = data.content;
|
||||
// 如果有待处理的用量,设置到这个工具调用
|
||||
if (pendingToolUsage && !lastTool.usage) {
|
||||
lastTool.usage = pendingToolUsage;
|
||||
}
|
||||
}
|
||||
}
|
||||
pendingToolUsage = null;
|
||||
break;
|
||||
case 'usage':
|
||||
// 对话用量统计
|
||||
const chatUsage = data.content as AgentUsage;
|
||||
latest.tokenUsage = {
|
||||
prompt: chatUsage.input_tokens || chatUsage.prompt_tokens || 0,
|
||||
completion: chatUsage.output_tokens || chatUsage.completion_tokens || 0,
|
||||
total: chatUsage.total_tokens || 0,
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 发送消息
|
||||
async function startSSE(chatContent: string) {
|
||||
if (isSending.value) return;
|
||||
if (!chatContent.trim()) {
|
||||
ElMessage.warning('请输入消息内容');
|
||||
return;
|
||||
}
|
||||
if (!tokenId.value) {
|
||||
ElMessage.warning('请先选择 API 密钥');
|
||||
showToolsPanel.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
isSending.value = true;
|
||||
inputValue.value = '';
|
||||
|
||||
// 如果没有会话ID,先创建会话
|
||||
let currentSessionId = sessionId.value;
|
||||
if (!currentSessionId && userStore.token) {
|
||||
const newSession = await agentSessionStore.createSession({
|
||||
sessionTitle: chatContent.slice(0, 20),
|
||||
sessionContent: chatContent,
|
||||
userId: userStore.userInfo?.userId as number,
|
||||
});
|
||||
if (newSession) {
|
||||
currentSessionId = newSession.id!;
|
||||
sessionId.value = currentSessionId;
|
||||
} else {
|
||||
currentSessionId = crypto.randomUUID();
|
||||
sessionId.value = currentSessionId;
|
||||
}
|
||||
} else if (!currentSessionId) {
|
||||
currentSessionId = crypto.randomUUID();
|
||||
sessionId.value = currentSessionId;
|
||||
}
|
||||
|
||||
// 添加用户消息和AI消息占位
|
||||
addMessage(chatContent, true);
|
||||
addMessage('', false);
|
||||
|
||||
// 滚动到底部
|
||||
setTimeout(() => {
|
||||
bubbleListRef.value?.scrollToBottom();
|
||||
}, 100);
|
||||
|
||||
abortController = new AbortController();
|
||||
|
||||
try {
|
||||
const response = await fetch(`${import.meta.env.VITE_WEB_BASE_API}/ai-chat/agent/send`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${userStore.token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
sessionId: currentSessionId,
|
||||
content: chatContent,
|
||||
tokenId: tokenId.value,
|
||||
modelId: modelId.value,
|
||||
tools: selectedTools.value,
|
||||
}),
|
||||
signal: abortController.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const reader = response.body?.getReader();
|
||||
if (!reader) throw new Error('No reader available');
|
||||
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split('\n');
|
||||
buffer = lines.pop() || '';
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6).trim();
|
||||
if (data === '[DONE]') {
|
||||
break;
|
||||
}
|
||||
try {
|
||||
const parsed = JSON.parse(data) as AgentResultOutput;
|
||||
handleAgentChunk(parsed);
|
||||
} catch (e) {
|
||||
console.error('解析数据失败:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
if (err.name === 'AbortError') {
|
||||
console.log('请求已取消');
|
||||
} else {
|
||||
console.error('请求失败:', err);
|
||||
ElMessage.error(err.message || '请求失败');
|
||||
}
|
||||
} finally {
|
||||
isSending.value = false;
|
||||
abortController = null;
|
||||
// 停止加载状态
|
||||
if (bubbleItems.value.length) {
|
||||
const latest = bubbleItems.value[bubbleItems.value.length - 1];
|
||||
latest.loading = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 取消请求
|
||||
function cancelSSE() {
|
||||
if (abortController) {
|
||||
abortController.abort();
|
||||
isSending.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="image-generation-page">
|
||||
<el-empty description="智能体功能开发中,敬请期待">
|
||||
<template #image>
|
||||
<el-icon style="font-size: 80px; color: var(--el-color-primary);">
|
||||
<i-ep-picture />
|
||||
<div class="agent-page">
|
||||
<!-- 左侧会话列表 -->
|
||||
<div class="session-sidebar" :class="{ collapsed: !showSessionList }">
|
||||
<div class="sidebar-header">
|
||||
<span class="sidebar-title">会话列表</span>
|
||||
<el-button type="primary" :icon="Plus" circle size="small" @click="createNewSession" />
|
||||
</div>
|
||||
<div class="session-list">
|
||||
<div
|
||||
v-for="session in agentSessionStore.sessionList"
|
||||
:key="session.id"
|
||||
class="session-item"
|
||||
:class="{ active: sessionId === session.id }"
|
||||
@click="selectSession(session)"
|
||||
>
|
||||
<span class="session-title">{{ session.sessionTitle }}</span>
|
||||
<el-icon class="delete-icon" @click="handleDeleteSession(session, $event)">
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div v-if="agentSessionStore.sessionList.length === 0" class="empty-tip">
|
||||
暂无会话记录
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧主内容区 -->
|
||||
<div class="main-content">
|
||||
<!-- 头部 -->
|
||||
<div class="agent-header">
|
||||
<div class="header-left">
|
||||
<el-icon :size="24" color="var(--el-color-primary)">
|
||||
<Tools />
|
||||
</el-icon>
|
||||
</template>
|
||||
</el-empty>
|
||||
<span class="header-title">AI 智能体</span>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<el-button type="primary" plain size="small" @click="showToolsPanel = !showToolsPanel">
|
||||
<el-icon><Tools /></el-icon>
|
||||
<span>配置</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 工具配置面板 -->
|
||||
<el-collapse-transition>
|
||||
<div v-show="showToolsPanel" class="tools-panel">
|
||||
<div class="tools-header">
|
||||
<span>可用工具</span>
|
||||
<el-button link type="primary" size="small" @click="selectedTools = availableTools.map(t => t.code)">
|
||||
全选
|
||||
</el-button>
|
||||
<el-button link type="info" size="small" @click="selectedTools = []">
|
||||
清空
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="tools-list">
|
||||
<div
|
||||
v-for="tool in availableTools"
|
||||
:key="tool.code"
|
||||
class="tool-item"
|
||||
:class="{ active: selectedTools.includes(tool.code) }"
|
||||
@click="toggleTool(tool.code)"
|
||||
>
|
||||
<el-icon v-if="selectedTools.includes(tool.code)" class="check-icon">
|
||||
<Check />
|
||||
</el-icon>
|
||||
<span>{{ tool.name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="tools-config">
|
||||
<el-select
|
||||
v-model="tokenId"
|
||||
placeholder="请选择"
|
||||
size="small"
|
||||
style="width: 300px;"
|
||||
:loading="tokenLoading"
|
||||
clearable
|
||||
>
|
||||
<template #prefix>
|
||||
<span style="color: var(--el-text-color-regular);">API密钥(可选)</span>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="token in tokenOptions"
|
||||
:key="token.tokenId"
|
||||
:label="token.name"
|
||||
:value="token.tokenId"
|
||||
:disabled="token.isDisabled"
|
||||
/>
|
||||
</el-select>
|
||||
<!-- 模型ID固定为 gpt-5.2-chat,不允许用户修改 -->
|
||||
<!-- <el-input v-model="modelId" placeholder="请输入模型ID" size="small" style="width: 300px;">
|
||||
<template #prepend>模型</template>
|
||||
</el-input> -->
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
|
||||
<!-- 聊天区域 -->
|
||||
<div class="chat-area">
|
||||
<!-- 消息为空时的欢迎界面 -->
|
||||
<div v-if="bubbleItems.length === 0" class="welcome-container">
|
||||
<div class="welcome-icon">
|
||||
<img :src="agentAvatar" alt="Agent" class="welcome-avatar" />
|
||||
</div>
|
||||
<h2 class="welcome-title">意心Ai 智能体</h2>
|
||||
<p class="welcome-desc">我叫橙子老弟,我,啥都会</p>
|
||||
<div class="welcome-tips">
|
||||
<div class="tip-item">可选择 API 密钥后开始对话</div>
|
||||
<div class="tip-item">选择需要的工具来增强我的能力</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 消息列表 -->
|
||||
<BubbleList v-else ref="bubbleListRef" :list="bubbleItems" max-height="calc(100vh - 320px)">
|
||||
<template #header="{ item }">
|
||||
<!-- 工具调用状态 -->
|
||||
<div v-if="item.toolCalls && item.toolCalls.length > 0" class="tool-calls-container">
|
||||
<div v-for="(tc, idx) in item.toolCalls" :key="idx" class="tool-call-item">
|
||||
<el-icon v-if="tc.status === 'calling'" class="is-loading">
|
||||
<Loading />
|
||||
</el-icon>
|
||||
<el-icon v-else color="var(--el-color-success)">
|
||||
<Check />
|
||||
</el-icon>
|
||||
<span class="tool-name">{{ tc.name }}</span>
|
||||
<span class="tool-status">{{ tc.status === 'calling' ? '调用中...' : '已完成' }}</span>
|
||||
<span v-if="tc.usage?.total" class="tool-usage">token: {{ tc.usage.total }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #content="{ item }">
|
||||
<YMarkdown
|
||||
v-if="item.content && (item.role === 'assistant' || item.role === 'system')"
|
||||
class="markdown-body"
|
||||
:markdown="item.content"
|
||||
:themes="{ light: 'github-light', dark: 'github-dark' }"
|
||||
default-theme-mode="dark"
|
||||
/>
|
||||
<div v-if="item.role === 'user'" class="user-content">
|
||||
{{ item.content }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer="{ item }">
|
||||
<div v-if="item.tokenUsage?.total" class="footer-wrapper">
|
||||
<span class="footer-token">token: {{ item.tokenUsage.total }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</BubbleList>
|
||||
|
||||
<!-- 输入区域 -->
|
||||
<Sender
|
||||
v-model="inputValue"
|
||||
class="agent-sender"
|
||||
:auto-size="{ maxRows: 6, minRows: 2 }"
|
||||
variant="updown"
|
||||
clearable
|
||||
allow-speech
|
||||
:loading="isSending"
|
||||
:disabled="!tokenId"
|
||||
@submit="startSSE"
|
||||
@cancel="cancelSSE"
|
||||
>
|
||||
<template #suffix>
|
||||
<ElIcon v-if="isSending" class="is-loading">
|
||||
<Loading />
|
||||
</ElIcon>
|
||||
</template>
|
||||
</Sender>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.image-generation-page {
|
||||
.agent-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: row;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: var(--el-bg-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.session-sidebar {
|
||||
width: 240px;
|
||||
height: 100%;
|
||||
border-right: 1px solid var(--el-border-color-lighter);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
background: var(--el-bg-color);
|
||||
|
||||
&.collapsed {
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
||||
|
||||
.sidebar-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.session-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.session-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 4px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
background: var(--el-fill-color-light);
|
||||
|
||||
.delete-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.session-title {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.delete-icon {
|
||||
opacity: 0;
|
||||
color: var(--el-text-color-secondary);
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--el-color-danger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.empty-tip {
|
||||
text-align: center;
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 13px;
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.main-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.agent-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
height: 60px;
|
||||
flex-shrink: 0;
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.header-title {
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.tools-panel {
|
||||
background: var(--el-bg-color-page);
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.tools-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.tools-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.tool-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--el-border-color);
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: var(--el-color-primary-light-9);
|
||||
border-color: var(--el-color-primary);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.tools-config {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--el-border-color-lighter);
|
||||
}
|
||||
}
|
||||
|
||||
.chat-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 1000px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.welcome-container {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
|
||||
.welcome-icon {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.welcome-avatar {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-title {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 12px;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.welcome-desc {
|
||||
font-size: 14px;
|
||||
color: var(--el-text-color-secondary);
|
||||
margin: 0 0 24px;
|
||||
}
|
||||
|
||||
.welcome-tips {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
|
||||
.tip-item {
|
||||
font-size: 13px;
|
||||
color: var(--el-text-color-regular);
|
||||
padding: 8px 16px;
|
||||
background: var(--el-fill-color-light);
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tool-calls-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 12px;
|
||||
padding: 8px 12px;
|
||||
background: var(--el-fill-color-lighter);
|
||||
border-radius: 8px;
|
||||
border-left: 3px solid var(--el-color-primary);
|
||||
}
|
||||
|
||||
.tool-call-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
|
||||
.tool-name {
|
||||
font-weight: 500;
|
||||
color: var(--el-text-color-primary);
|
||||
}
|
||||
|
||||
.tool-status {
|
||||
color: var(--el-text-color-secondary);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.tool-usage {
|
||||
font-size: 11px;
|
||||
background: rgba(255, 153, 0, 0.6);
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.user-content {
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.footer-wrapper {
|
||||
margin-top: 4px;
|
||||
|
||||
.footer-token {
|
||||
font-size: 12px;
|
||||
background: rgba(1, 183, 86, 0.53);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: #ffffff;
|
||||
}
|
||||
}
|
||||
|
||||
.agent-sender {
|
||||
margin-top: auto;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.is-loading {
|
||||
animation: rotating 2s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotating {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
:deep(.el-bubble-list) {
|
||||
padding-top: 24px;
|
||||
}
|
||||
|
||||
:deep(.el-bubble) {
|
||||
padding: 0 12px;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
:deep(.markdown-body) {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/* 移动端适配 */
|
||||
@media (max-width: 768px) {
|
||||
.agent-page {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.agent-header {
|
||||
height: 50px;
|
||||
|
||||
.header-title {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.tools-panel {
|
||||
padding: 12px;
|
||||
|
||||
.tools-config {
|
||||
flex-direction: column;
|
||||
|
||||
:deep(.el-input) {
|
||||
width: 100% !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.welcome-container {
|
||||
padding: 20px 12px;
|
||||
|
||||
.welcome-title {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
202
Yi.Ai.Vue3/src/stores/modules/agentSession.ts
Normal file
202
Yi.Ai.Vue3/src/stores/modules/agentSession.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import type { ChatSessionVo, CreateSessionDTO, GetSessionListParams } from '@/api/session/types';
|
||||
import { SessionTypeEnum } from '@/api/session/types';
|
||||
import { Monitor } from '@element-plus/icons-vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { markRaw, ref } from 'vue';
|
||||
import {
|
||||
create_session,
|
||||
delete_session,
|
||||
get_session,
|
||||
get_session_list,
|
||||
update_session,
|
||||
} from '@/api';
|
||||
import { useUserStore } from './user';
|
||||
|
||||
export const useAgentSessionStore = defineStore('agentSession', () => {
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 当前选中的会话信息
|
||||
const currentSession = ref<ChatSessionVo | null>(null);
|
||||
|
||||
// 设置当前会话
|
||||
const setCurrentSession = (session: ChatSessionVo | null) => {
|
||||
currentSession.value = session;
|
||||
};
|
||||
|
||||
// 会话列表核心状态
|
||||
const sessionList = ref<ChatSessionVo[]>([]);
|
||||
const currentPage = ref(1);
|
||||
const pageSize = ref(25);
|
||||
const hasMore = ref(true);
|
||||
const isLoading = ref(false);
|
||||
const isLoadingMore = ref(false);
|
||||
|
||||
// 获取会话列表
|
||||
const requestSessionList = async (page: number = currentPage.value, force: boolean = false) => {
|
||||
if (!userStore.token) {
|
||||
sessionList.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
if (!force && ((page > 1 && !hasMore.value) || isLoading.value || isLoadingMore.value))
|
||||
return;
|
||||
|
||||
isLoading.value = page === 1;
|
||||
isLoadingMore.value = page > 1;
|
||||
|
||||
try {
|
||||
const params: GetSessionListParams = {
|
||||
userId: userStore.userInfo?.userId as number,
|
||||
skipCount: page,
|
||||
maxResultCount: pageSize.value,
|
||||
isAsc: 'desc',
|
||||
orderByColumn: 'createTime',
|
||||
sessionType: SessionTypeEnum.Agent, // 只查询Agent类型
|
||||
};
|
||||
|
||||
const resArr = await get_session_list(params);
|
||||
const res = processSessions(resArr.data.items);
|
||||
|
||||
const allSessions = new Map(sessionList.value.map(item => [item.id, item]));
|
||||
res.forEach(item => allSessions.set(item.id, { ...item }));
|
||||
|
||||
if (page === 1) {
|
||||
sessionList.value = [
|
||||
...res,
|
||||
...Array.from(allSessions.values()).filter(item => !res.some(r => r.id === item.id)),
|
||||
];
|
||||
}
|
||||
else {
|
||||
sessionList.value = [
|
||||
...sessionList.value.filter(item => !res.some(r => r.id === item.id)),
|
||||
...res,
|
||||
];
|
||||
}
|
||||
|
||||
if (!force)
|
||||
hasMore.value = (res?.length || 0) === pageSize.value;
|
||||
if (!force)
|
||||
currentPage.value = page;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('requestSessionList错误:', error);
|
||||
}
|
||||
finally {
|
||||
isLoading.value = false;
|
||||
isLoadingMore.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 创建新会话
|
||||
const createSession = async (data: Omit<CreateSessionDTO, 'id' | 'sessionType'>) => {
|
||||
if (!userStore.token) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await create_session({
|
||||
...data,
|
||||
sessionType: SessionTypeEnum.Agent,
|
||||
});
|
||||
|
||||
await requestSessionList(1, true);
|
||||
|
||||
const newSessionRes = await get_session(`${res.data.id}`);
|
||||
setCurrentSession(newSessionRes.data);
|
||||
|
||||
return newSessionRes.data;
|
||||
}
|
||||
catch (error) {
|
||||
console.error('createSession错误:', error);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// 加载更多会话
|
||||
const loadMoreSessions = async () => {
|
||||
if (hasMore.value)
|
||||
await requestSessionList(currentPage.value + 1);
|
||||
};
|
||||
|
||||
// 更新会话
|
||||
const updateSession = async (item: ChatSessionVo) => {
|
||||
try {
|
||||
await update_session(item);
|
||||
const targetIndex = sessionList.value.findIndex(session => session.id === item.id);
|
||||
const targetPage = targetIndex >= 0
|
||||
? Math.floor(targetIndex / pageSize.value) + 1
|
||||
: 1;
|
||||
await requestSessionList(targetPage, true);
|
||||
}
|
||||
catch (error) {
|
||||
console.error('updateSession错误:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 删除会话
|
||||
const deleteSession = async (ids: string[]) => {
|
||||
try {
|
||||
await delete_session(ids);
|
||||
const targetIndex = sessionList.value.findIndex(session => session.id === ids[0]);
|
||||
const targetPage = targetIndex >= 0
|
||||
? Math.floor(targetIndex / pageSize.value) + 1
|
||||
: 1;
|
||||
await requestSessionList(targetPage, true);
|
||||
|
||||
// 如果删除的是当前会话,清空当前会话
|
||||
if (currentSession.value && ids.includes(currentSession.value.id!)) {
|
||||
setCurrentSession(null);
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error('deleteSession错误:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// 预处理会话
|
||||
function processSessions(sessions: ChatSessionVo[]) {
|
||||
const currentDate = new Date();
|
||||
|
||||
return sessions.map((session) => {
|
||||
const createDate = new Date(session.creationTime!);
|
||||
const diffDays = Math.floor(
|
||||
(currentDate.getTime() - createDate.getTime()) / (1000 * 60 * 60 * 24),
|
||||
);
|
||||
|
||||
let group: string;
|
||||
if (diffDays < 7) {
|
||||
group = '7 天内';
|
||||
}
|
||||
else if (diffDays < 30) {
|
||||
group = '30 天内';
|
||||
}
|
||||
else {
|
||||
const year = createDate.getFullYear();
|
||||
const month = String(createDate.getMonth() + 1).padStart(2, '0');
|
||||
group = `${year}-${month}`;
|
||||
}
|
||||
|
||||
return {
|
||||
...session,
|
||||
group,
|
||||
prefixIcon: markRaw(Monitor),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
currentSession,
|
||||
setCurrentSession,
|
||||
sessionList,
|
||||
currentPage,
|
||||
pageSize,
|
||||
hasMore,
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
createSession,
|
||||
requestSessionList,
|
||||
loadMoreSessions,
|
||||
updateSession,
|
||||
deleteSession,
|
||||
};
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { ChatSessionVo, CreateSessionDTO, GetSessionListParams } from '@/api/session/types';
|
||||
import { SessionTypeEnum } from '@/api/session/types';
|
||||
import { ChatLineRound } from '@element-plus/icons-vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { markRaw } from 'vue';
|
||||
@@ -64,6 +65,7 @@ export const useSessionStore = defineStore('session', () => {
|
||||
maxResultCount: pageSize.value,
|
||||
isAsc: 'desc',
|
||||
orderByColumn: 'createTime',
|
||||
sessionType: SessionTypeEnum.Chat,
|
||||
};
|
||||
|
||||
const resArr = await get_session_list(params);
|
||||
|
||||
1
Yi.Ai.Vue3/types/components.d.ts
vendored
1
Yi.Ai.Vue3/types/components.d.ts
vendored
@@ -23,6 +23,7 @@ declare module 'vue' {
|
||||
ElCol: typeof import('element-plus/es')['ElCol']
|
||||
ElCollapse: typeof import('element-plus/es')['ElCollapse']
|
||||
ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem']
|
||||
ElCollapseTransition: typeof import('element-plus/es')['ElCollapseTransition']
|
||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||
|
||||
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
1
Yi.Ai.Vue3/types/import_meta.d.ts
vendored
@@ -7,7 +7,6 @@ interface ImportMetaEnv {
|
||||
readonly VITE_WEB_BASE_API: string;
|
||||
readonly VITE_API_URL: string;
|
||||
readonly VITE_FILE_UPLOAD_API: string;
|
||||
readonly VITE_BUILD_COMPRESS: string;
|
||||
readonly VITE_SSO_SEVER_URL: string;
|
||||
readonly VITE_APP_VERSION: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user