Compare commits

...

14 Commits

Author SHA1 Message Date
ccnetcore
4ab4d7b6db fix: 修复markdown渲染脚本问题 2026-02-12 18:09:45 +08:00
ccnetcore
d4d89b989c feat: Token 支持请求日志开关并记录 OpenAPI 请求日志
新增 Token 的 IsEnableLog 字段,贯穿领域、应用与 DTO;在 OpenApiService 中根据 Token 配置异步记录请求日志,包含请求体、模型与接口类型,用于后续审计与分析。
2026-02-12 17:36:31 +08:00
ccnetcore
d463053c16 chore: 调整 Hangfire 相关日志级别
降低 Hangfire.Redis.StackExchange 和 Hangfire.Processing 相关组件的日志输出级别为 Warning,减少无关日志噪音,提升日志可读性。
2026-02-12 16:00:26 +08:00
ccnetcore
dbe5a95b47 Merge remote-tracking branch 'origin/ai-hub' into ai-hub 2026-02-09 23:35:45 +08:00
ccnetcore
c1c43c1464 fix: 修复api样式问题 2026-02-09 23:24:41 +08:00
Gsh
13d6fc228a fix: 网页端Anthropic Claude对话格式去除system角色,改为assistant角色 2026-02-07 02:19:29 +08:00
ccnetcore
58ce45ec92 fix: 修复api示例问题 2026-02-07 02:11:35 +08:00
ccnetcore
048a9b9601 chore: 优化日志输出格式并调整组件类型声明
- 统一 Serilog 文件与控制台日志的输出模板,提升可读性
- 降低部分 ASP.NET Core 内部组件的日志级别,减少无关噪音
- 移除前端 types 中未使用的 ElSegmented 组件声明
2026-02-07 01:58:44 +08:00
ccnetcore
097798268b style: 优化vip到期时间展示 2026-02-07 01:31:25 +08:00
ccnetcore
f7eb1b7048 Merge branch 'url' into ai-hub 2026-02-07 01:29:06 +08:00
Gsh
4133b80d49 fix: 网页端Anthropic Claude对话格式去除system角色,改为assistant角色 2026-02-07 01:17:51 +08:00
Gsh
57b03436f3 fix: 动画超时加载时间设置60秒 2026-02-07 00:48:42 +08:00
Gsh
836ea90145 fix: Anthropic Claude 网页对话格式修改 2026-02-07 00:47:22 +08:00
chenchun
a040b7a16a fix: 修复bug
将 responseResult.Item1 的比较从严格等于改为包含判断,兼容部分 AI 工具返回带前后缀的异常字符串(例如 "exception: ..." 等),避免漏掉异常分支处理。修改文件:Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs。
2026-02-06 16:56:37 +08:00
20 changed files with 292 additions and 52 deletions

View File

@@ -40,6 +40,11 @@ public class TokenGetListOutputDto
/// </summary> /// </summary>
public bool IsDisabled { get; set; } public bool IsDisabled { get; set; }
/// <summary>
/// 是否启用请求日志记录
/// </summary>
public bool IsEnableLog { get; set; }
/// <summary> /// <summary>
/// 创建时间 /// 创建时间
/// </summary> /// </summary>

View File

@@ -83,6 +83,7 @@ public class TokenService : ApplicationService
PremiumQuotaLimit = t.PremiumQuotaLimit, PremiumQuotaLimit = t.PremiumQuotaLimit,
PremiumUsedQuota = usedQuota, PremiumUsedQuota = usedQuota,
IsDisabled = t.IsDisabled, IsDisabled = t.IsDisabled,
IsEnableLog = t.IsEnableLog,
CreationTime = t.CreationTime CreationTime = t.CreationTime
}; };
}).ToList(); }).ToList();
@@ -158,6 +159,7 @@ public class TokenService : ApplicationService
PremiumQuotaLimit = token.PremiumQuotaLimit, PremiumQuotaLimit = token.PremiumQuotaLimit,
PremiumUsedQuota = 0, PremiumUsedQuota = 0,
IsDisabled = token.IsDisabled, IsDisabled = token.IsDisabled,
IsEnableLog = token.IsEnableLog,
CreationTime = token.CreationTime CreationTime = token.CreationTime
}; };
} }

View File

@@ -1,12 +1,15 @@
using System.Text.Json; using System.Text.Json;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Volo.Abp.Application.Services; using Volo.Abp.Application.Services;
using Volo.Abp.Uow;
using Volo.Abp.Users; using Volo.Abp.Users;
using Yi.Framework.AiHub.Domain.Entities; using Yi.Framework.AiHub.Domain.Entities;
using Yi.Framework.AiHub.Domain.Entities.Chat; using Yi.Framework.AiHub.Domain.Entities.Chat;
using Yi.Framework.AiHub.Domain.Entities.Model; using Yi.Framework.AiHub.Domain.Entities.Model;
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
using Yi.Framework.AiHub.Domain.Extensions; using Yi.Framework.AiHub.Domain.Extensions;
using Yi.Framework.AiHub.Domain.Managers; using Yi.Framework.AiHub.Domain.Managers;
using Yi.Framework.AiHub.Domain.Shared.Consts; using Yi.Framework.AiHub.Domain.Shared.Consts;
@@ -33,10 +36,12 @@ public class OpenApiService : ApplicationService
private readonly PremiumPackageManager _premiumPackageManager; private readonly PremiumPackageManager _premiumPackageManager;
private readonly ISqlSugarRepository<ImageStoreTaskAggregateRoot> _imageStoreRepository; private readonly ISqlSugarRepository<ImageStoreTaskAggregateRoot> _imageStoreRepository;
private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository; private readonly ISqlSugarRepository<AiModelEntity> _aiModelRepository;
private readonly IServiceScopeFactory _serviceScopeFactory;
public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger<OpenApiService> logger, public OpenApiService(IHttpContextAccessor httpContextAccessor, ILogger<OpenApiService> logger,
TokenManager tokenManager, AiGateWayManager aiGateWayManager, TokenManager tokenManager, AiGateWayManager aiGateWayManager,
ModelManager modelManager, AiBlacklistManager aiBlacklistManager, ModelManager modelManager, AiBlacklistManager aiBlacklistManager,
IAccountService accountService, PremiumPackageManager premiumPackageManager, ISqlSugarRepository<ImageStoreTaskAggregateRoot> imageStoreRepository, ISqlSugarRepository<AiModelEntity> aiModelRepository) IAccountService accountService, PremiumPackageManager premiumPackageManager, ISqlSugarRepository<ImageStoreTaskAggregateRoot> imageStoreRepository, ISqlSugarRepository<AiModelEntity> aiModelRepository,
IServiceScopeFactory serviceScopeFactory)
{ {
_httpContextAccessor = httpContextAccessor; _httpContextAccessor = httpContextAccessor;
_logger = logger; _logger = logger;
@@ -48,6 +53,7 @@ public class OpenApiService : ApplicationService
_premiumPackageManager = premiumPackageManager; _premiumPackageManager = premiumPackageManager;
_imageStoreRepository = imageStoreRepository; _imageStoreRepository = imageStoreRepository;
_aiModelRepository = aiModelRepository; _aiModelRepository = aiModelRepository;
_serviceScopeFactory = serviceScopeFactory;
} }
/// <summary> /// <summary>
@@ -91,6 +97,12 @@ public class OpenApiService : ApplicationService
null, tokenId, null, tokenId,
CancellationToken.None); CancellationToken.None);
} }
// 记录请求日志
if (tokenValidation.IsEnableLog)
{
FireAndForgetMessageLog(JsonSerializer.Serialize(input), tokenValidation.Token, tokenValidation.TokenName, input.Model, ModelApiTypeEnum.Completions);
}
} }
@@ -206,6 +218,12 @@ public class OpenApiService : ApplicationService
null, tokenId, null, tokenId,
CancellationToken.None); CancellationToken.None);
} }
// 记录请求日志
if (tokenValidation.IsEnableLog)
{
FireAndForgetMessageLog(JsonSerializer.Serialize(input), tokenValidation.Token, tokenValidation.TokenName, input.Model, ModelApiTypeEnum.Messages);
}
} }
@@ -258,6 +276,12 @@ public class OpenApiService : ApplicationService
null, tokenId, null, tokenId,
CancellationToken.None); CancellationToken.None);
} }
// 记录请求日志
if (tokenValidation.IsEnableLog)
{
FireAndForgetMessageLog(JsonSerializer.Serialize(input), tokenValidation.Token, tokenValidation.TokenName, input.Model, ModelApiTypeEnum.Responses);
}
} }
@@ -318,6 +342,12 @@ public class OpenApiService : ApplicationService
null, tokenId, null, tokenId,
CancellationToken.None); CancellationToken.None);
} }
// 记录请求日志
if (tokenValidation.IsEnableLog)
{
FireAndForgetMessageLog(input.GetRawText(), tokenValidation.Token, tokenValidation.TokenName, modelId, ModelApiTypeEnum.GenerateContent);
}
} }
#region #region
@@ -357,5 +387,25 @@ public class OpenApiService : ApplicationService
} }
} }
private void FireAndForgetMessageLog(string requestBody, string apiKey, string apiKeyName, string modelId, ModelApiTypeEnum apiType)
{
_ = Task.Run(async () =>
{
try
{
using var scope = _serviceScopeFactory.CreateScope();
var uowManager = scope.ServiceProvider.GetRequiredService<IUnitOfWorkManager>();
var manager = scope.ServiceProvider.GetRequiredService<MessageLogManager>();
using var uow = uowManager.Begin(requiresNew: true);
await manager.CreateAsync(requestBody, apiKey, apiKeyName, modelId, apiType);
await uow.CompleteAsync();
}
catch (Exception ex)
{
_logger.LogError(ex, "记录消息日志失败, 请求体长度: {RequestBodyLength}", requestBody?.Length ?? 0);
}
});
}
#endregion #endregion
} }

View File

@@ -99,8 +99,8 @@ public enum GoodsTypeEnum
[Price(83.7, 3, 27.9)] [DisplayName("YiXinVip 3 month", "3个月", "短期体验")] [GoodsCategory(GoodsCategoryType.Vip)] [Price(83.7, 3, 27.9)] [DisplayName("YiXinVip 3 month", "3个月", "短期体验")] [GoodsCategory(GoodsCategoryType.Vip)]
YiXinVip3 = 3, YiXinVip3 = 3,
[Price(114.5, 5, 22.9)] [DisplayName("YiXinVip 5 month", "5个月", "年度热销")] [GoodsCategory(GoodsCategoryType.Vip)] [Price(91.6, 4, 22.9)] [DisplayName("YiXinVip 4 month", "4个月", "年度热销")] [GoodsCategory(GoodsCategoryType.Vip)]
YiXinVip5 = 15, YiXinVip5 = 14,
// 尊享包服务 - 需要VIP资格才能购买 // 尊享包服务 - 需要VIP资格才能购买
[Price(188.9, 0, 1750)] [Price(188.9, 0, 1750)]

View File

@@ -0,0 +1,49 @@
using SqlSugar;
using Volo.Abp.Domain.Entities;
using Yi.Framework.AiHub.Domain.Shared.Enums;
namespace Yi.Framework.AiHub.Domain.Entities.OpenApi;
[SugarTable("Ai_Message_Log")]
public class MessageLogAggregateRoot : Entity<Guid>
{
/// <summary>
/// 请求内容httpbody
/// </summary>
[SugarColumn(ColumnDataType = "text")]
public string? RequestBody { get; set; }
/// <summary>
/// 请求apikey
/// </summary>
[SugarColumn(Length = 255)]
public string ApiKey { get; set; }
/// <summary>
/// 请求apikey名称
/// </summary>
[SugarColumn(Length = 255)]
public string ApiKeyName { get; set; }
/// <summary>
/// 创建时间
/// </summary>
public DateTime CreationTime { get; set; }
/// <summary>
/// 模型id
/// </summary>
[SugarColumn(Length = 64)]
public string ModelId { get; set; }
/// <summary>
/// api类型
/// </summary>
public ModelApiTypeEnum ApiType { get; set; }
/// <summary>
/// api类型名称
/// </summary>
[SugarColumn(Length = 16)]
public string ApiTypeName { get; set; }
}

View File

@@ -51,6 +51,11 @@ public class TokenAggregateRoot : FullAuditedAggregateRoot<Guid>
/// </summary> /// </summary>
public bool IsDisabled { get; set; } public bool IsDisabled { get; set; }
/// <summary>
/// 是否启用请求日志记录(仅数据库手动修改)
/// </summary>
public bool IsEnableLog { get; set; }
/// <summary> /// <summary>
/// 检查Token是否可用 /// 检查Token是否可用
/// </summary> /// </summary>

View File

@@ -642,7 +642,7 @@ public class AiGateWayManager : DomainService
isFirst = false; isFirst = false;
} }
if (responseResult.Item1=="exception") if (responseResult.Item1.Contains("exception"))
{ {
//兼容部分ai工具问题 //兼容部分ai工具问题
continue; continue;

View File

@@ -0,0 +1,34 @@
using Volo.Abp.Domain.Services;
using Yi.Framework.AiHub.Domain.Entities.OpenApi;
using Yi.Framework.AiHub.Domain.Shared.Enums;
using Yi.Framework.SqlSugarCore.Abstractions;
namespace Yi.Framework.AiHub.Domain.Managers;
public class MessageLogManager : DomainService
{
private readonly ISqlSugarRepository<MessageLogAggregateRoot> _repository;
public MessageLogManager(ISqlSugarRepository<MessageLogAggregateRoot> repository)
{
_repository = repository;
}
/// <summary>
/// 创建消息日志
/// </summary>
public async Task CreateAsync(string requestBody, string apiKey, string apiKeyName, string modelId, ModelApiTypeEnum apiType)
{
var entity = new MessageLogAggregateRoot
{
RequestBody = requestBody,
ApiKey = apiKey,
ApiKeyName = apiKeyName,
ModelId = modelId,
ApiType = apiType,
ApiTypeName = apiType.ToString(),
CreationTime = DateTime.Now
};
await _repository.InsertAsync(entity);
}
}

View File

@@ -27,6 +27,16 @@ public class TokenValidationResult
/// token /// token
/// </summary> /// </summary>
public string Token { get; set; } public string Token { get; set; }
/// <summary>
/// Token名称
/// </summary>
public string TokenName { get; set; }
/// <summary>
/// 是否启用请求日志记录
/// </summary>
public bool IsEnableLog { get; set; }
} }
public class TokenManager : DomainService public class TokenManager : DomainService
@@ -117,7 +127,9 @@ public class TokenManager : DomainService
{ {
UserId = entity.UserId, UserId = entity.UserId,
TokenId = entity.Id, TokenId = entity.Id,
Token = entity.Token Token = entity.Token,
TokenName = entity.Name,
IsEnableLog = entity.IsEnableLog
}; };
} }

View File

@@ -4,6 +4,7 @@ using Serilog.Events;
using Yi.Abp.Web; using Yi.Abp.Web;
//创建日志,可使用{SourceContext}记录 //创建日志,可使用{SourceContext}记录
var outputTemplate = "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz}【{SourceContext}】[{Level:u3}]{Message:lj}{NewLine}{Exception}";
Log.Logger = new LoggerConfiguration() Log.Logger = new LoggerConfiguration()
//由于后端处理请求中,前端请求已经结束,此类日志可不记录 //由于后端处理请求中,前端请求已经结束,此类日志可不记录
.Filter.ByExcluding(log =>log.Exception?.GetType() == typeof(TaskCanceledException)||log.MessageTemplate.Text.Contains("\"message\": \"A task was canceled.\"")) .Filter.ByExcluding(log =>log.Exception?.GetType() == typeof(TaskCanceledException)||log.MessageTemplate.Text.Contains("\"message\": \"A task was canceled.\""))
@@ -11,10 +12,17 @@ Log.Logger = new LoggerConfiguration()
.MinimumLevel.Override("Microsoft", LogEventLevel.Information) .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
.MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Error) .MinimumLevel.Override("Microsoft.AspNetCore.Hosting.Diagnostics", LogEventLevel.Error)
.MinimumLevel.Override("Quartz", LogEventLevel.Warning) .MinimumLevel.Override("Quartz", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Cors.Infrastructure.CorsService", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Authorization.DefaultAuthorizationService", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore.Routing.EndpointMiddleware", LogEventLevel.Warning)
.MinimumLevel.Override("Hangfire.Server.ServerHeartbeatProcess", LogEventLevel.Warning)
.MinimumLevel.Override("Hangfire.Redis.StackExchange.FetchedJobsWatcher", LogEventLevel.Warning)
.MinimumLevel.Override("Hangfire.Processing.BackgroundExecution", LogEventLevel.Warning)
.Enrich.FromLogContext() .Enrich.FromLogContext()
.WriteTo.Async(c => c.File("logs/all/log-.txt", rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Debug)) .WriteTo.Async(c => c.File("logs/all/log-.txt", rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Debug,outputTemplate:outputTemplate))
.WriteTo.Async(c => c.File("logs/error/errorlog-.txt", rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Error)) .WriteTo.Async(c => c.File("logs/error/errorlog-.txt", rollingInterval: RollingInterval.Day, restrictedToMinimumLevel: LogEventLevel.Error,outputTemplate:outputTemplate))
.WriteTo.Async(c => c.Console()) .WriteTo.Async(c => c.Console(outputTemplate:outputTemplate))
.CreateLogger(); .CreateLogger();
try try

View File

@@ -361,7 +361,7 @@ namespace Yi.Abp.Web
var app = context.GetApplicationBuilder(); var app = context.GetApplicationBuilder();
app.UseRouting(); app.UseRouting();
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<RankingItemAggregateRoot>(); //app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<MessageLogAggregateRoot>();
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<ActivationCodeRecordAggregateRoot>(); // app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<ActivationCodeRecordAggregateRoot>();
// app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<UsageStatisticsAggregateRoot>(); // app.ApplicationServices.GetRequiredService<ISqlSugarDbContext>().SqlSugarClient.CodeFirst.InitTables<UsageStatisticsAggregateRoot>();

View File

@@ -10,7 +10,9 @@
"Bash(npm install marked --save)", "Bash(npm install marked --save)",
"Bash(pnpm add marked)", "Bash(pnpm add marked)",
"Bash(pnpm lint:*)", "Bash(pnpm lint:*)",
"Bash(pnpm list:*)" "Bash(pnpm list:*)",
"Bash(pnpm vue-tsc:*)",
"Bash(pnpm build:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -283,7 +283,7 @@
appRendered = true; appRendered = true;
checkAndHideLoader(); checkAndHideLoader();
} }
}, 30000); }, 60000);
})(); })();
</script> </script>

View File

@@ -10,11 +10,13 @@ import { useDesignStore } from '@/stores';
interface Props { interface Props {
content: string; content: string;
theme?: 'light' | 'dark' | 'auto'; theme?: 'light' | 'dark' | 'auto';
sanitize?: boolean;
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
content: '', content: '',
theme: 'auto', theme: 'auto',
sanitize: true,
}); });
const designStore = useDesignStore(); const designStore = useDesignStore();
@@ -94,7 +96,12 @@ const renderer = {
// 行内代码 // 行内代码
codespan(token: { text: string }) { codespan(token: { text: string }) {
return `<code class="inline-code">${token.text}</code>`; // 转义 HTML 标签,防止 <script> 等标签被浏览器解析
const escapedText = token.text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
return `<code class="inline-code">${escapedText}</code>`;
}, },
// 链接 // 链接
@@ -148,11 +155,23 @@ async function renderContent(content: string) {
// 包装表格,添加 table-wrapper 以支持横向滚动 // 包装表格,添加 table-wrapper 以支持横向滚动
rawHtml = rawHtml.replace(/<table>/g, '<div class="table-wrapper"><table>'); rawHtml = rawHtml.replace(/<table>/g, '<div class="table-wrapper"><table>');
rawHtml = rawHtml.replace(/<\/table>/g, '</table></div>'); rawHtml = rawHtml.replace(/<\/table>/g, '</table></div>');
// 使用 DOMPurify 清理 HTML防止 XSS // 转义 script 标签,防止浏览器将其当作真实脚本解析
renderedHtml.value = DOMPurify.sanitize(rawHtml, { // 使用字符串拼接避免在源码中出现 script 标签字面量
ADD_TAGS: ['iframe'], const scriptStart = '<' + 'script';
ADD_ATTR: ['target', 'data-code', 'data-html'], const scriptEnd = '<' + '/script' + '>';
}); rawHtml = rawHtml.replace(new RegExp(scriptStart + '(.*?)>', 'gi'), '&lt;script$1&gt;');
rawHtml = rawHtml.replace(new RegExp(scriptEnd.replace('/', '\\/'), 'gi'), '&lt;/script&gt;');
// 使用 DOMPurify 清理 HTML防止 XSS可通过 sanitize 属性禁用)
if (props.sanitize) {
renderedHtml.value = DOMPurify.sanitize(rawHtml, {
ADD_TAGS: ['iframe'],
ADD_ATTR: ['target', 'data-code', 'data-html'],
});
}
else {
renderedHtml.value = rawHtml;
}
// 渲染后绑定按钮事件 // 渲染后绑定按钮事件
nextTick(() => { nextTick(() => {

View File

@@ -8,6 +8,7 @@ interface TokenFormData {
expireTime: string; expireTime: string;
premiumQuotaLimit: number | null; premiumQuotaLimit: number | null;
quotaUnit: string; quotaUnit: string;
isEnableLog?: boolean;
} }
interface Props { interface Props {
@@ -42,6 +43,7 @@ const localFormData = ref<TokenFormData>({
const submitting = ref(false); const submitting = ref(false);
const neverExpire = ref(false); // 永不过期开关 const neverExpire = ref(false); // 永不过期开关
const unlimitedQuota = ref(false); // 无限制额度开关 const unlimitedQuota = ref(false); // 无限制额度开关
const isEnableLog = ref(false); // 是否启用请求日志(只读)
// 移动端检测 // 移动端检测
const isMobile = ref(false); const isMobile = ref(false);
@@ -107,6 +109,9 @@ watch(() => props.visible, (newVal) => {
// 判断是否永不过期 // 判断是否永不过期
neverExpire.value = !props.formData.expireTime; neverExpire.value = !props.formData.expireTime;
// 读取是否启用请求日志(只读字段)
isEnableLog.value = props.formData.isEnableLog || false;
localFormData.value = { localFormData.value = {
...props.formData, ...props.formData,
premiumQuotaLimit: displayValue, premiumQuotaLimit: displayValue,
@@ -196,13 +201,13 @@ const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥'
<el-dialog <el-dialog
:model-value="visible" :model-value="visible"
:title="dialogTitle" :title="dialogTitle"
:width="isMobile ? '95%' : '540px'" :width="isMobile ? '95%' : '640px'"
:fullscreen="isMobile" :fullscreen="isMobile"
:close-on-click-modal="false" :close-on-click-modal="false"
:show-close="!submitting" :show-close="!submitting"
@close="handleClose" @close="handleClose"
> >
<el-form :model="localFormData" :label-width="isMobile ? '100%' : '110px'" :label-position="isMobile ? 'top' : 'right'"> <el-form :model="localFormData" :label-width="isMobile ? '100%' : '150px'" :label-position="isMobile ? 'top' : 'right'">
<el-form-item label="API密钥名称" required> <el-form-item label="API密钥名称" required>
<el-input <el-input
v-model="localFormData.name" v-model="localFormData.name"
@@ -288,6 +293,21 @@ const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥'
超出配额后API密钥将无法继续使用 超出配额后API密钥将无法继续使用
</div> </div>
</el-form-item> </el-form-item>
<!-- 仅编辑模式显示:请求日志开关(只读) -->
<el-form-item v-if="mode === 'edit'" label="请求日志存储">
<div class="form-item-inline">
<el-switch
v-model="isEnableLog"
disabled
/>
<span class="switch-status-text">{{ isEnableLog ? '已开启' : '已关闭' }}</span>
</div>
<div class="form-hint warning-hint">
<el-icon><i-ep-warning-filled /></el-icon>
此临时存储功能仅面向企业套餐用户,仅用于企业内部审计
</div>
</el-form-item>
</el-form> </el-form>
<template #footer> <template #footer>
@@ -356,6 +376,15 @@ const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥'
color: #409eff; color: #409eff;
flex-shrink: 0; flex-shrink: 0;
} }
&.warning-hint {
background: #fdf6ec;
border-left-color: #e6a23c;
.el-icon {
color: #e6a23c;
}
}
} }
.dialog-footer { .dialog-footer {
@@ -364,6 +393,18 @@ const dialogTitle = computed(() => props.mode === 'create' ? '新增 API密钥'
gap: 12px; gap: 12px;
} }
.switch-status-text {
font-size: 14px;
color: #606266;
margin-left: 8px;
}
.form-item-inline {
display: flex;
align-items: center;
gap: 8px;
}
:deep(.el-form-item__label) { :deep(.el-form-item__label) {
font-weight: 600; font-weight: 600;
color: #303133; color: #303133;

View File

@@ -54,7 +54,7 @@ const vipRemainingDays = computed(() => {
// VIP到期状态文本 // VIP到期状态文本
const vipExpireStatusText = computed(() => { const vipExpireStatusText = computed(() => {
if (!userVipStatus.value) return ''; if (!userVipStatus.value) return '';
if (!vipExpireTime.value) return '永久VIP'; if (!vipExpireTime.value) return '-';
if (vipRemainingDays.value === null) return ''; if (vipRemainingDays.value === null) return '';
if (vipRemainingDays.value < 0) return '已过期'; if (vipRemainingDays.value < 0) return '已过期';
if (vipRemainingDays.value === 0) return '今日到期'; if (vipRemainingDays.value === 0) return '今日到期';
@@ -202,7 +202,7 @@ function bindWechat() {
{{ formatDate(vipExpireTime)?.split(' ')[0] || '-' }} {{ formatDate(vipExpireTime)?.split(' ')[0] || '-' }}
</template> </template>
<template v-else> <template v-else>
永久 -
</template> </template>
</div> </div>
<div class="stat-label"> <div class="stat-label">

View File

@@ -1,12 +1,13 @@
import { useHookFetch } from 'hook-fetch/vue';
import { ElMessage } from 'element-plus';
import { ref, computed } from 'vue';
import type { AnyObject } from 'typescript-api-pro'; import type { AnyObject } from 'typescript-api-pro';
import { deleteMessages, unifiedSend } from '@/api';
import { useModelStore } from '@/stores/modules/model';
import { convertToApiFormat, parseStreamChunk, type UnifiedMessage } from '@/utils/apiFormatConverter';
import type { BubbleProps } from 'vue-element-plus-x/types/Bubble'; import type { BubbleProps } from 'vue-element-plus-x/types/Bubble';
import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking'; import type { ThinkingStatus } from 'vue-element-plus-x/types/Thinking';
import type { UnifiedMessage } from '@/utils/apiFormatConverter';
import { ElMessage } from 'element-plus';
import { useHookFetch } from 'hook-fetch/vue';
import { ref } from 'vue';
import { unifiedSend } from '@/api';
import { useModelStore } from '@/stores/modules/model';
import { convertToApiFormat, parseStreamChunk } from '@/utils/apiFormatConverter';
export type MessageRole = 'ai' | 'user' | 'assistant' | string; export type MessageRole = 'ai' | 'user' | 'assistant' | string;
@@ -83,7 +84,8 @@ export function useChatSender(options: UseChatSenderOptions) {
); );
const latest = messages[messages.length - 1]; const latest = messages[messages.length - 1];
if (!latest) return; if (!latest)
return;
// 处理 token 使用情况 // 处理 token 使用情况
if (parsed.usage) { if (parsed.usage) {
@@ -99,7 +101,8 @@ export function useChatSender(options: UseChatSenderOptions) {
latest.thinkingStatus = 'thinking'; latest.thinkingStatus = 'thinking';
latest.loading = true; latest.loading = true;
latest.thinlCollapse = true; latest.thinlCollapse = true;
if (!latest.reasoning_content) latest.reasoning_content = ''; if (!latest.reasoning_content)
latest.reasoning_content = '';
latest.reasoning_content += parsed.reasoning_content; latest.reasoning_content += parsed.reasoning_content;
} }
@@ -108,21 +111,26 @@ export function useChatSender(options: UseChatSenderOptions) {
const thinkStart = parsed.content.includes('<think>'); const thinkStart = parsed.content.includes('<think>');
const thinkEnd = parsed.content.includes('</think>'); const thinkEnd = parsed.content.includes('</think>');
if (thinkStart) isThinking.value = true; if (thinkStart)
if (thinkEnd) isThinking.value = false; isThinking.value = true;
if (thinkEnd)
isThinking.value = false;
if (isThinking.value) { if (isThinking.value) {
latest.thinkingStatus = 'thinking'; latest.thinkingStatus = 'thinking';
latest.loading = true; latest.loading = true;
latest.thinlCollapse = true; latest.thinlCollapse = true;
if (!latest.reasoning_content) latest.reasoning_content = ''; if (!latest.reasoning_content)
latest.reasoning_content = '';
latest.reasoning_content += parsed.content latest.reasoning_content += parsed.content
.replace('<think>', '') .replace('<think>', '')
.replace('</think>', ''); .replace('</think>', '');
} else { }
else {
latest.thinkingStatus = 'end'; latest.thinkingStatus = 'end';
latest.loading = false; latest.loading = false;
if (!latest.content) latest.content = ''; if (!latest.content)
latest.content = '';
latest.content += parsed.content; latest.content += parsed.content;
} }
} }
@@ -144,7 +152,8 @@ export function useChatSender(options: UseChatSenderOptions) {
textFiles: any[], textFiles: any[],
onUpdate: (messages: MessageItem[]) => void, onUpdate: (messages: MessageItem[]) => void,
): Promise<void> { ): Promise<void> {
if (isSending.value) return; if (isSending.value)
return;
isSending.value = true; isSending.value = true;
currentRequestApiType.value = modelStore.currentModelInfo.modelApiType || 'Completions'; currentRequestApiType.value = modelStore.currentModelInfo.modelApiType || 'Completions';
@@ -207,7 +216,8 @@ export function useChatSender(options: UseChatSenderOptions) {
fileContent += `<FILE_NAME>${fileItem.name}</FILE_NAME>\n`; fileContent += `<FILE_NAME>${fileItem.name}</FILE_NAME>\n`;
fileContent += `<FILE_CONTENT>\n${fileItem.fileContent}\n</FILE_CONTENT>\n`; fileContent += `<FILE_CONTENT>\n${fileItem.fileContent}\n</FILE_CONTENT>\n`;
fileContent += `</ATTACHMENT_FILE>\n`; fileContent += `</ATTACHMENT_FILE>\n`;
if (index < textFiles.length - 1) fileContent += '\n'; if (index < textFiles.length - 1)
fileContent += '\n';
}); });
contentArray.push({ type: 'text', text: fileContent }); contentArray.push({ type: 'text', text: fileContent });
} }
@@ -225,7 +235,8 @@ export function useChatSender(options: UseChatSenderOptions) {
baseMessage.content = contentArray.length > 1 || imageFiles.length > 0 || textFiles.length > 0 baseMessage.content = contentArray.length > 1 || imageFiles.length > 0 || textFiles.length > 0
? contentArray ? contentArray
: item.content; : item.content;
} else { }
else {
baseMessage.content = (item.role === 'ai' || item.role === 'assistant') && item.content.length > 10000 baseMessage.content = (item.role === 'ai' || item.role === 'assistant') && item.content.length > 10000
? `${item.content.substring(0, 10000)}...(内容过长,已省略)` ? `${item.content.substring(0, 10000)}...(内容过长,已省略)`
: item.content; : item.content;

View File

@@ -20,7 +20,7 @@ const apiList = [
} }
], ],
"stream": true, "stream": true,
"model": "gpt-5.3-codex" "model": "gpt-5.2-chat"
} }
}, },
{ {
@@ -148,7 +148,7 @@ async function copyText(text: string) {
> >
<template #default> <template #default>
<div class="leading-normal text-sm"> <div class="leading-normal text-sm">
2025 年末起AI 领域接口标准逐渐分化原有的统一接口 <code class="bg-yellow-100 dark:bg-yellow-900 px-1 rounded">/v1/chat/completions</code> 已不再兼容所有模型各厂商推出的新接口差异较大接入第三方工具时请务必根据具体模型选择正确的 API 类型您可前往 2025 年末起AI 领域接口标准逐渐分化原有的统一接口 <code class="bg-yellow-100 px-1 rounded">/v1/chat/completions</code> 已不再兼容所有模型各厂商推出的新接口差异较大接入第三方工具时请务必根据具体模型选择正确的 API 类型您可前往
<router-link to="/model-library" class="text-primary font-bold hover:underline">模型库</router-link> <router-link to="/model-library" class="text-primary font-bold hover:underline">模型库</router-link>
查看各模型对应的 API 信息 查看各模型对应的 API 信息
</div> </div>
@@ -203,7 +203,7 @@ async function copyText(text: string) {
<span class="font-bold text-sm">调用示例 (cURL)</span> <span class="font-bold text-sm">调用示例 (cURL)</span>
</div> </div>
</template> </template>
<div class="code-block bg-gray-50 dark:bg-[#161b22] p-3 rounded-md border border-gray-200 dark:border-gray-700"> <div class="code-block bg-gray-50 p-3 rounded-md border border-gray-200 ">
<pre class="text-xs overflow-x-auto font-mono m-0"><code class="language-bash">curl {{ fullUrl }} \ <pre class="text-xs overflow-x-auto font-mono m-0"><code class="language-bash">curl {{ fullUrl }} \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \ -H "Authorization: Bearer YOUR_API_KEY" \

View File

@@ -127,17 +127,20 @@ export function toResponsesFormat(messages: UnifiedMessage[]): ResponsesMessage[
* 将统一格式的消息转换为 Anthropic Claude 格式 * 将统一格式的消息转换为 Anthropic Claude 格式
*/ */
export function toClaudeFormat(messages: UnifiedMessage[]): { messages: ClaudeMessage[]; system?: string } { export function toClaudeFormat(messages: UnifiedMessage[]): { messages: ClaudeMessage[]; system?: string } {
let systemPrompt: string | undefined;
const claudeMessages: ClaudeMessage[] = []; const claudeMessages: ClaudeMessage[] = [];
for (const msg of messages) { for (const msg of messages) {
// Claude 的 system 消息需要单独提取 // system 消息转换为 assistant 角色放入 messages 数组
let role: 'user' | 'assistant';
if (msg.role === 'system') { if (msg.role === 'system') {
systemPrompt = typeof msg.content === 'string' ? msg.content : msg.content.map(c => c.text || '').join(''); role = 'assistant';
continue; }
else if (msg.role === 'model') {
role = 'assistant';
}
else {
role = msg.role as 'user' | 'assistant';
} }
const role = msg.role === 'model' ? 'assistant' : msg.role;
// 处理内容格式 // 处理内容格式
let content: string | ClaudeContent[]; let content: string | ClaudeContent[];
@@ -181,7 +184,7 @@ export function toClaudeFormat(messages: UnifiedMessage[]): { messages: ClaudeMe
}); });
} }
return { messages: claudeMessages, system: systemPrompt }; return { messages: claudeMessages };
} }
/** /**
@@ -518,16 +521,16 @@ export function convertToApiFormat(
}; };
} }
case ApiFormatType.Messages: { case ApiFormatType.Messages: {
const { messages: claudeMessages, system } = toClaudeFormat(messages); const { messages: claudeMessages } = toClaudeFormat(messages);
const request: any = { const request: any = {
model, model,
messages: claudeMessages, messages: claudeMessages,
max_tokens: 32000, max_tokens: 32000,
stream, stream,
}; };
if (system) { // if (system) {
request.system = system; // request.system = system;
} // }
return request; return request;
} }
case ApiFormatType.GenerateContent: { case ApiFormatType.GenerateContent: {

View File

@@ -54,7 +54,6 @@ declare module 'vue' {
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']
ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] ElScrollbar: typeof import('element-plus/es')['ElScrollbar']
ElSegmented: typeof import('element-plus/es')['ElSegmented']
ElSelect: typeof import('element-plus/es')['ElSelect'] ElSelect: typeof import('element-plus/es')['ElSelect']
ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu']