diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Channel/AiAppShortcutDto.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Channel/AiAppShortcutDto.cs new file mode 100644 index 00000000..799823ff --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/Dtos/Channel/AiAppShortcutDto.cs @@ -0,0 +1,42 @@ +namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel; + +/// +/// AI应用快捷配置DTO +/// +public class AiAppShortcutDto +{ + /// + /// 应用ID + /// + public Guid Id { get; set; } + + /// + /// 应用名称 + /// + public string Name { get; set; } + + /// + /// 应用终结点 + /// + public string Endpoint { get; set; } + + /// + /// 额外URL + /// + public string? ExtraUrl { get; set; } + + /// + /// 应用Key + /// + public string ApiKey { get; set; } + + /// + /// 排序 + /// + public int OrderNum { get; set; } + + /// + /// 创建时间 + /// + public DateTime CreationTime { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IChannelService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IChannelService.cs index d45b542e..bf703fe1 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IChannelService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application.Contracts/IServices/IChannelService.cs @@ -83,4 +83,14 @@ public interface IChannelService Task DeleteModelAsync(Guid id); #endregion + + #region AI应用快捷配置 + + /// + /// 获取AI应用快捷配置列表 + /// + /// 快捷配置列表 + Task> GetAppShortcutListAsync(); + + #endregion } diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/ChannelService.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/ChannelService.cs index 5a609297..bdbe66b2 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/ChannelService.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Application/Services/ChannelService.cs @@ -19,13 +19,16 @@ public class ChannelService : ApplicationService, IChannelService { private readonly ISqlSugarRepository _appRepository; private readonly ISqlSugarRepository _modelRepository; + private readonly ISqlSugarRepository _appShortcutRepository; public ChannelService( ISqlSugarRepository appRepository, - ISqlSugarRepository modelRepository) + ISqlSugarRepository modelRepository, + ISqlSugarRepository appShortcutRepository) { _appRepository = appRepository; _modelRepository = modelRepository; + _appShortcutRepository = appShortcutRepository; } #region AI应用管理 @@ -239,4 +242,22 @@ public class ChannelService : ApplicationService, IChannelService } #endregion + + #region AI应用快捷配置 + + /// + /// 获取AI应用快捷配置列表 + /// + [HttpGet("channel/app-shortcut")] + public async Task> GetAppShortcutListAsync() + { + var entities = await _appShortcutRepository._DbQueryable + .OrderBy(x => x.OrderNum) + .OrderByDescending(x => x.CreationTime) + .ToListAsync(); + + return entities.Adapt>(); + } + + #endregion } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Gemini/GeminiGenerateContentAcquirer.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Gemini/GeminiGenerateContentAcquirer.cs index ca70532a..bcd00370 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Gemini/GeminiGenerateContentAcquirer.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain.Shared/Dtos/Gemini/GeminiGenerateContentAcquirer.cs @@ -109,41 +109,107 @@ public static class GeminiGenerateContentAcquirer /// /// 获取图片 base64(包含 data:image 前缀) - /// 从最后一个 part 开始查找 inlineData,找不到再从最后一个 part 开始查找 text + /// Step 1: 递归遍历整个 JSON 查找最后一个 base64 + /// Step 2: 从 text 中查找 markdown 图片 /// public static string GetImagePrefixBase64(JsonElement response) { - var parts = response.GetPath("candidates", 0, "content", "parts"); - if (!parts.HasValue || parts.Value.ValueKind != JsonValueKind.Array) + // Step 1: 递归遍历整个 JSON 查找最后一个 base64 + string? lastBase64 = null; + string? lastMimeType = null; + CollectLastBase64(response, ref lastBase64, ref lastMimeType); + + if (!string.IsNullOrEmpty(lastBase64)) { - return string.Empty; + var mimeType = lastMimeType ?? "image/png"; + return $"data:{mimeType};base64,{lastBase64}"; } - var partsArray = parts.Value.EnumerateArray().ToList(); - if (partsArray.Count == 0) + // Step 2: 从 text 中查找 markdown 图片 + return FindMarkdownImageInResponse(response); + } + + /// + /// 递归遍历 JSON 查找最后一个 base64 + /// + private static void CollectLastBase64(JsonElement element, ref string? lastBase64, ref string? lastMimeType, int minLength = 100) + { + switch (element.ValueKind) { - return string.Empty; + case JsonValueKind.Object: + string? currentMimeType = null; + string? currentData = null; + + foreach (var prop in element.EnumerateObject()) + { + var name = prop.Name.ToLowerInvariant(); + + // 记录 mimeType / mime_type + if (name is "mimetype" or "mime_type" && prop.Value.ValueKind == JsonValueKind.String) + { + currentMimeType = prop.Value.GetString(); + } + // 记录 data 字段(检查是否像 base64) + else if (name == "data" && prop.Value.ValueKind == JsonValueKind.String) + { + var val = prop.Value.GetString(); + if (!string.IsNullOrEmpty(val) && val.Length >= minLength && LooksLikeBase64(val)) + { + currentData = val; + } + } + else + { + // 递归处理其他属性 + CollectLastBase64(prop.Value, ref lastBase64, ref lastMimeType, minLength); + } + } + + // 如果当前对象有 data,更新为"最后一个" + if (currentData != null) + { + lastBase64 = currentData; + lastMimeType = currentMimeType; + } + break; + + case JsonValueKind.Array: + foreach (var item in element.EnumerateArray()) + { + CollectLastBase64(item, ref lastBase64, ref lastMimeType, minLength); + } + break; + } + } + + /// + /// 检查字符串是否像 base64 + /// + private static bool LooksLikeBase64(string str) + { + // 常见图片 base64 开头: JPEG(/9j/), PNG(iVBOR), GIF(R0lGO), WebP(UklGR) + if (str.StartsWith("/9j/") || str.StartsWith("iVBOR") || + str.StartsWith("R0lGO") || str.StartsWith("UklGR")) + { + return true; } - // Step 1: 从最后一个 part 开始查找 inlineData - for (int i = partsArray.Count - 1; i >= 0; i--) - { - var inlineBase64 = partsArray[i].GetPath("inlineData", "data").GetString(); - if (!string.IsNullOrEmpty(inlineBase64)) - { - var mimeType = partsArray[i].GetPath("inlineData", "mimeType").GetString() ?? "image/png"; - return $"data:{mimeType};base64,{inlineBase64}"; - } - } + // 检查前100个字符是否都是 base64 合法字符 + return str.Take(100).All(c => char.IsLetterOrDigit(c) || c == '+' || c == '/' || c == '='); + } - // Step 2: 从最后一个 part 开始查找 text 中的 markdown 图片 - for (int i = partsArray.Count - 1; i >= 0; i--) + /// + /// 递归查找 text 字段中的 markdown 图片 + /// + private static string FindMarkdownImageInResponse(JsonElement element) + { + var allTexts = new List(); + CollectTextFields(element, allTexts); + + // 从最后一个 text 开始查找 + for (int i = allTexts.Count - 1; i >= 0; i--) { - var text = partsArray[i].GetPath("text").GetString(); - if (string.IsNullOrEmpty(text)) - { - continue; - } + var text = allTexts[i]; // markdown 图片格式: ![image](data:image/png;base64,xxx) var startMarker = "(data:image/"; @@ -163,4 +229,38 @@ public static class GeminiGenerateContentAcquirer return string.Empty; } + + /// + /// 递归收集所有 text 字段 + /// + private static void CollectTextFields(JsonElement element, List texts) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + foreach (var prop in element.EnumerateObject()) + { + if (prop.Name == "text" && prop.Value.ValueKind == JsonValueKind.String) + { + var val = prop.Value.GetString(); + if (!string.IsNullOrEmpty(val)) + { + texts.Add(val); + } + } + else + { + CollectTextFields(prop.Value, texts); + } + } + break; + + case JsonValueKind.Array: + foreach (var item in element.EnumerateArray()) + { + CollectTextFields(item, texts); + } + break; + } + } } \ No newline at end of file diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiAppShortcutAggregateRoot.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiAppShortcutAggregateRoot.cs new file mode 100644 index 00000000..5cae2def --- /dev/null +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Entities/Model/AiAppShortcutAggregateRoot.cs @@ -0,0 +1,37 @@ +using SqlSugar; +using Volo.Abp.Domain.Entities.Auditing; +using Yi.Framework.Core.Data; + +namespace Yi.Framework.AiHub.Domain.Entities.Model; + +/// +/// AI应用快捷配置表 +/// +[SugarTable("Ai_App_Shortcut")] +public class AiAppShortcutAggregateRoot : FullAuditedAggregateRoot, IOrderNum +{ + /// + /// 应用名称 + /// + public string Name { get; set; } + + /// + /// 应用终结点 + /// + public string Endpoint { get; set; } + + /// + /// 额外url + /// + public string? ExtraUrl { get; set; } + + /// + /// 应用key + /// + public string ApiKey { get; set; } + + /// + /// 排序 + /// + public int OrderNum { get; set; } +} diff --git a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs index a0d4f355..90708bbe 100644 --- a/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs +++ b/Yi.Abp.Net8/module/ai-hub/Yi.Framework.AiHub.Domain/Managers/AiGateWayManager.cs @@ -1076,11 +1076,19 @@ public class AiGateWayManager : DomainService LazyServiceProvider.GetRequiredKeyedService(modelDescribe.HandlerName); var data = await chatService.GenerateContentAsync(modelDescribe, request, cancellationToken); + // 检查是否被大模型内容安全策略拦截 + var rawResponse = data.GetRawText(); + if (rawResponse.Contains("policies.google.com/terms/generative-ai/use-policy")) + { + _logger.LogWarning($"图片生成被内容安全策略拦截,模型:【{modelId}】,请求信息:【{request}】"); + throw new UserFriendlyException("您的提示词涉及敏感信息,已被大模型拦截,请调整提示词后再试!"); + } + //解析json,获取base64字符串 var imagePrefixBase64 = GeminiGenerateContentAcquirer.GetImagePrefixBase64(data); if (string.IsNullOrWhiteSpace(imagePrefixBase64)) { - _logger.LogError($"图片生成解析失败,模型id:,请求信息:【{request}】,请求响应信息:{imagePrefixBase64}"); + _logger.LogError($"图片生成解析失败,模型:【{modelId}】,请求信息:【{request}】,请求响应信息:【{data}】"); throw new UserFriendlyException("大模型没有返回图片,请调整提示词或稍后再试"); } diff --git a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs index a4f226e4..b346e760 100644 --- a/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs +++ b/Yi.Abp.Net8/module/rbac/Yi.Framework.Rbac.Domain/Managers/AccountManager.cs @@ -235,7 +235,7 @@ namespace Yi.Framework.Rbac.Domain.Managers if (UserConst.Admin.Equals(dto.User.UserName)) { AddToClaim(claims, TokenTypeConst.Permission, UserConst.AdminPermissionCode); - AddToClaim(claims, TokenTypeConst.Roles, UserConst.AdminRolesCode); + AddToClaim(claims, ClaimTypes.Role, UserConst.AdminRolesCode); } else { diff --git a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs index 4feeace4..da304784 100644 --- a/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs +++ b/Yi.Abp.Net8/src/Yi.Abp.Web/YiAbpWebModule.cs @@ -1,4 +1,5 @@ using System.Globalization; +using System.Security.Claims; using System.Text; using System.Text.Json.Serialization; using System.Text.Json.Serialization.Metadata; @@ -280,7 +281,7 @@ namespace Yi.Abp.Web { options.TokenValidationParameters = new TokenValidationParameters { - RoleClaimType = "Roles", + RoleClaimType = ClaimTypes.Role, ClockSkew = TimeSpan.Zero, ValidateIssuerSigningKey = true, ValidIssuer = jwtOptions.Issuer, diff --git a/Yi.Ai.Vue3/.build/plugins/git-hash.ts b/Yi.Ai.Vue3/.build/plugins/git-hash.ts new file mode 100644 index 00000000..f1845b0f --- /dev/null +++ b/Yi.Ai.Vue3/.build/plugins/git-hash.ts @@ -0,0 +1,60 @@ +import type { Plugin } from 'vite'; +import { execSync } from 'node:child_process'; +import path from 'node:path'; + +/** + * 获取 Git 提交哈希值插件 + * Git 仓库在上一级目录 + */ +export default function gitHashPlugin(): Plugin { + let gitHash = 'unknown'; + let gitBranch = 'unknown'; + let gitDate = 'unknown'; + + try { + // Git 仓库在上一级目录 + const execOptions = { cwd: path.resolve(__dirname, '../../..'), encoding: 'utf-8' as BufferEncoding }; + + // 获取完整的 commit hash + gitHash = execSync('git rev-parse HEAD', execOptions) + .toString() + .trim(); + + // 获取短 hash (前7位) + const shortHash = gitHash.substring(0, 7); + + // 获取分支名 + gitBranch = execSync('git rev-parse --abbrev-ref HEAD', execOptions) + .toString() + .trim(); + + // 获取提交时间 + gitDate = execSync('git log -1 --format=%cd --date=iso', execOptions) + .toString() + .trim(); + + console.log(`\n📦 Git Info:`); + console.log(` Branch: ${gitBranch}`); + console.log(` Commit: ${shortHash}`); + console.log(` Date: ${gitDate}\n`); + + gitHash = shortHash; // 使用短 hash + } catch (error: any) { + console.warn('⚠️ 无法获取 Git 信息:', error?.message || error); + } + + return { + name: 'vite-plugin-git-hash', + config() { + // 在 config 钩子中返回配置 + return { + define: { + __GIT_HASH__: JSON.stringify(gitHash), + __GIT_BRANCH__: JSON.stringify(gitBranch), + __GIT_DATE__: JSON.stringify(gitDate), + __BUILD_TIME__: JSON.stringify(new Date().toISOString()), + }, + }; + }, + }; +} diff --git a/Yi.Ai.Vue3/.build/plugins/index.ts b/Yi.Ai.Vue3/.build/plugins/index.ts index 8138f0ca..122c0fe2 100644 --- a/Yi.Ai.Vue3/.build/plugins/index.ts +++ b/Yi.Ai.Vue3/.build/plugins/index.ts @@ -10,12 +10,14 @@ import Components from 'unplugin-vue-components/vite'; import viteCompression from 'vite-plugin-compression'; import envTyped from 'vite-plugin-env-typed'; +import gitHashPlugin from './git-hash'; import createSvgIcon from './svg-icon'; const root = path.resolve(__dirname, '../../'); function plugins({ mode, command }: ConfigEnv): PluginOption[] { return [ + gitHashPlugin(), UnoCSS(), envTyped({ mode, diff --git a/Yi.Ai.Vue3/index.html b/Yi.Ai.Vue3/index.html index cc241305..5d0feec1 100644 --- a/Yi.Ai.Vue3/index.html +++ b/Yi.Ai.Vue3/index.html @@ -112,7 +112,7 @@
-
意心Ai 3.2
+
意心Ai 3.3
海外地址,仅首次访问预计加载约10秒,无需梯子
- 最多2张,< 5MB (支持 JPG/PNG/WEBP) + 最多2张,< 5MB (支持 JPG/PNG/WEBP,可粘贴)
diff --git a/Yi.Ai.Vue3/types/global.d.ts b/Yi.Ai.Vue3/types/global.d.ts index 829fa098..9ebb4c37 100644 --- a/Yi.Ai.Vue3/types/global.d.ts +++ b/Yi.Ai.Vue3/types/global.d.ts @@ -1 +1,12 @@ declare module "virtual:svg-icons-register"; + +// Git 构建信息 +declare const __GIT_HASH__: string; +declare const __GIT_BRANCH__: string; +declare const __GIT_DATE__: string; +declare const __BUILD_TIME__: string; + +// 全局加载器方法 +interface Window { + __hideAppLoader?: () => void; +}