Merge branch 'ai-hub' into ai-hub-dark
This commit is contained in:
@@ -0,0 +1,42 @@
|
|||||||
|
namespace Yi.Framework.AiHub.Application.Contracts.Dtos.Channel;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AI应用快捷配置DTO
|
||||||
|
/// </summary>
|
||||||
|
public class AiAppShortcutDto
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 应用ID
|
||||||
|
/// </summary>
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用名称
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用终结点
|
||||||
|
/// </summary>
|
||||||
|
public string Endpoint { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 额外URL
|
||||||
|
/// </summary>
|
||||||
|
public string? ExtraUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用Key
|
||||||
|
/// </summary>
|
||||||
|
public string ApiKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序
|
||||||
|
/// </summary>
|
||||||
|
public int OrderNum { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建时间
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CreationTime { get; set; }
|
||||||
|
}
|
||||||
@@ -83,4 +83,14 @@ public interface IChannelService
|
|||||||
Task DeleteModelAsync(Guid id);
|
Task DeleteModelAsync(Guid id);
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region AI应用快捷配置
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取AI应用快捷配置列表
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>快捷配置列表</returns>
|
||||||
|
Task<List<AiAppShortcutDto>> GetAppShortcutListAsync();
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,13 +19,16 @@ public class ChannelService : ApplicationService, IChannelService
|
|||||||
{
|
{
|
||||||
private readonly ISqlSugarRepository<AiAppAggregateRoot, Guid> _appRepository;
|
private readonly ISqlSugarRepository<AiAppAggregateRoot, Guid> _appRepository;
|
||||||
private readonly ISqlSugarRepository<AiModelEntity, Guid> _modelRepository;
|
private readonly ISqlSugarRepository<AiModelEntity, Guid> _modelRepository;
|
||||||
|
private readonly ISqlSugarRepository<AiAppShortcutAggregateRoot, Guid> _appShortcutRepository;
|
||||||
|
|
||||||
public ChannelService(
|
public ChannelService(
|
||||||
ISqlSugarRepository<AiAppAggregateRoot, Guid> appRepository,
|
ISqlSugarRepository<AiAppAggregateRoot, Guid> appRepository,
|
||||||
ISqlSugarRepository<AiModelEntity, Guid> modelRepository)
|
ISqlSugarRepository<AiModelEntity, Guid> modelRepository,
|
||||||
|
ISqlSugarRepository<AiAppShortcutAggregateRoot, Guid> appShortcutRepository)
|
||||||
{
|
{
|
||||||
_appRepository = appRepository;
|
_appRepository = appRepository;
|
||||||
_modelRepository = modelRepository;
|
_modelRepository = modelRepository;
|
||||||
|
_appShortcutRepository = appShortcutRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
#region AI应用管理
|
#region AI应用管理
|
||||||
@@ -239,4 +242,22 @@ public class ChannelService : ApplicationService, IChannelService
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region AI应用快捷配置
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 获取AI应用快捷配置列表
|
||||||
|
/// </summary>
|
||||||
|
[HttpGet("channel/app-shortcut")]
|
||||||
|
public async Task<List<AiAppShortcutDto>> GetAppShortcutListAsync()
|
||||||
|
{
|
||||||
|
var entities = await _appShortcutRepository._DbQueryable
|
||||||
|
.OrderBy(x => x.OrderNum)
|
||||||
|
.OrderByDescending(x => x.CreationTime)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return entities.Adapt<List<AiAppShortcutDto>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
@@ -109,41 +109,107 @@ public static class GeminiGenerateContentAcquirer
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 获取图片 base64(包含 data:image 前缀)
|
/// 获取图片 base64(包含 data:image 前缀)
|
||||||
/// 从最后一个 part 开始查找 inlineData,找不到再从最后一个 part 开始查找 text
|
/// Step 1: 递归遍历整个 JSON 查找最后一个 base64
|
||||||
|
/// Step 2: 从 text 中查找 markdown 图片
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static string GetImagePrefixBase64(JsonElement response)
|
public static string GetImagePrefixBase64(JsonElement response)
|
||||||
{
|
{
|
||||||
var parts = response.GetPath("candidates", 0, "content", "parts");
|
// Step 1: 递归遍历整个 JSON 查找最后一个 base64
|
||||||
if (!parts.HasValue || parts.Value.ValueKind != JsonValueKind.Array)
|
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();
|
// Step 2: 从 text 中查找 markdown 图片
|
||||||
if (partsArray.Count == 0)
|
return FindMarkdownImageInResponse(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 递归遍历 JSON 查找最后一个 base64
|
||||||
|
/// </summary>
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 检查字符串是否像 base64
|
||||||
|
/// </summary>
|
||||||
|
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
|
// 检查前100个字符是否都是 base64 合法字符
|
||||||
for (int i = partsArray.Count - 1; i >= 0; i--)
|
return str.Take(100).All(c => char.IsLetterOrDigit(c) || c == '+' || c == '/' || c == '=');
|
||||||
{
|
}
|
||||||
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}";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Step 2: 从最后一个 part 开始查找 text 中的 markdown 图片
|
/// <summary>
|
||||||
for (int i = partsArray.Count - 1; i >= 0; i--)
|
/// 递归查找 text 字段中的 markdown 图片
|
||||||
|
/// </summary>
|
||||||
|
private static string FindMarkdownImageInResponse(JsonElement element)
|
||||||
|
{
|
||||||
|
var allTexts = new List<string>();
|
||||||
|
CollectTextFields(element, allTexts);
|
||||||
|
|
||||||
|
// 从最后一个 text 开始查找
|
||||||
|
for (int i = allTexts.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
var text = partsArray[i].GetPath("text").GetString();
|
var text = allTexts[i];
|
||||||
if (string.IsNullOrEmpty(text))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// markdown 图片格式: 
|
// markdown 图片格式: 
|
||||||
var startMarker = "(data:image/";
|
var startMarker = "(data:image/";
|
||||||
@@ -163,4 +229,38 @@ public static class GeminiGenerateContentAcquirer
|
|||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 递归收集所有 text 字段
|
||||||
|
/// </summary>
|
||||||
|
private static void CollectTextFields(JsonElement element, List<string> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
using Volo.Abp.Domain.Entities.Auditing;
|
||||||
|
using Yi.Framework.Core.Data;
|
||||||
|
|
||||||
|
namespace Yi.Framework.AiHub.Domain.Entities.Model;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// AI应用快捷配置表
|
||||||
|
/// </summary>
|
||||||
|
[SugarTable("Ai_App_Shortcut")]
|
||||||
|
public class AiAppShortcutAggregateRoot : FullAuditedAggregateRoot<Guid>, IOrderNum
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 应用名称
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用终结点
|
||||||
|
/// </summary>
|
||||||
|
public string Endpoint { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 额外url
|
||||||
|
/// </summary>
|
||||||
|
public string? ExtraUrl { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 应用key
|
||||||
|
/// </summary>
|
||||||
|
public string ApiKey { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 排序
|
||||||
|
/// </summary>
|
||||||
|
public int OrderNum { get; set; }
|
||||||
|
}
|
||||||
@@ -1076,11 +1076,19 @@ public class AiGateWayManager : DomainService
|
|||||||
LazyServiceProvider.GetRequiredKeyedService<IGeminiGenerateContentService>(modelDescribe.HandlerName);
|
LazyServiceProvider.GetRequiredKeyedService<IGeminiGenerateContentService>(modelDescribe.HandlerName);
|
||||||
var data = await chatService.GenerateContentAsync(modelDescribe, request, cancellationToken);
|
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字符串
|
//解析json,获取base64字符串
|
||||||
var imagePrefixBase64 = GeminiGenerateContentAcquirer.GetImagePrefixBase64(data);
|
var imagePrefixBase64 = GeminiGenerateContentAcquirer.GetImagePrefixBase64(data);
|
||||||
if (string.IsNullOrWhiteSpace(imagePrefixBase64))
|
if (string.IsNullOrWhiteSpace(imagePrefixBase64))
|
||||||
{
|
{
|
||||||
_logger.LogError($"图片生成解析失败,模型id:,请求信息:【{request}】,请求响应信息:{imagePrefixBase64}");
|
_logger.LogError($"图片生成解析失败,模型:【{modelId}】,请求信息:【{request}】,请求响应信息:【{data}】");
|
||||||
throw new UserFriendlyException("大模型没有返回图片,请调整提示词或稍后再试");
|
throw new UserFriendlyException("大模型没有返回图片,请调整提示词或稍后再试");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ namespace Yi.Framework.Rbac.Domain.Managers
|
|||||||
if (UserConst.Admin.Equals(dto.User.UserName))
|
if (UserConst.Admin.Equals(dto.User.UserName))
|
||||||
{
|
{
|
||||||
AddToClaim(claims, TokenTypeConst.Permission, UserConst.AdminPermissionCode);
|
AddToClaim(claims, TokenTypeConst.Permission, UserConst.AdminPermissionCode);
|
||||||
AddToClaim(claims, TokenTypeConst.Roles, UserConst.AdminRolesCode);
|
AddToClaim(claims, ClaimTypes.Role, UserConst.AdminRolesCode);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Security.Claims;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
using System.Text.Json.Serialization.Metadata;
|
using System.Text.Json.Serialization.Metadata;
|
||||||
@@ -280,7 +281,7 @@ namespace Yi.Abp.Web
|
|||||||
{
|
{
|
||||||
options.TokenValidationParameters = new TokenValidationParameters
|
options.TokenValidationParameters = new TokenValidationParameters
|
||||||
{
|
{
|
||||||
RoleClaimType = "Roles",
|
RoleClaimType = ClaimTypes.Role,
|
||||||
ClockSkew = TimeSpan.Zero,
|
ClockSkew = TimeSpan.Zero,
|
||||||
ValidateIssuerSigningKey = true,
|
ValidateIssuerSigningKey = true,
|
||||||
ValidIssuer = jwtOptions.Issuer,
|
ValidIssuer = jwtOptions.Issuer,
|
||||||
|
|||||||
60
Yi.Ai.Vue3/.build/plugins/git-hash.ts
Normal file
60
Yi.Ai.Vue3/.build/plugins/git-hash.ts
Normal file
@@ -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()),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -10,12 +10,14 @@ import Components from 'unplugin-vue-components/vite';
|
|||||||
import viteCompression from 'vite-plugin-compression';
|
import viteCompression from 'vite-plugin-compression';
|
||||||
|
|
||||||
import envTyped from 'vite-plugin-env-typed';
|
import envTyped from 'vite-plugin-env-typed';
|
||||||
|
import gitHashPlugin from './git-hash';
|
||||||
import createSvgIcon from './svg-icon';
|
import createSvgIcon from './svg-icon';
|
||||||
|
|
||||||
const root = path.resolve(__dirname, '../../');
|
const root = path.resolve(__dirname, '../../');
|
||||||
|
|
||||||
function plugins({ mode, command }: ConfigEnv): PluginOption[] {
|
function plugins({ mode, command }: ConfigEnv): PluginOption[] {
|
||||||
return [
|
return [
|
||||||
|
gitHashPlugin(),
|
||||||
UnoCSS(),
|
UnoCSS(),
|
||||||
envTyped({
|
envTyped({
|
||||||
mode,
|
mode,
|
||||||
|
|||||||
@@ -112,7 +112,7 @@
|
|||||||
<body>
|
<body>
|
||||||
<!-- 加载动画容器 -->
|
<!-- 加载动画容器 -->
|
||||||
<div id="yixinai-loader" class="loader-container">
|
<div id="yixinai-loader" class="loader-container">
|
||||||
<div class="loader-title">意心Ai 3.2</div>
|
<div class="loader-title">意心Ai 3.3</div>
|
||||||
<div class="loader-subtitle">海外地址,仅首次访问预计加载约10秒,无需梯子</div>
|
<div class="loader-subtitle">海外地址,仅首次访问预计加载约10秒,无需梯子</div>
|
||||||
<div class="loader-logo">
|
<div class="loader-logo">
|
||||||
<div class="pulse-box"></div>
|
<div class="pulse-box"></div>
|
||||||
|
|||||||
@@ -26,6 +26,17 @@ for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
|
|||||||
|
|
||||||
app.use(store);
|
app.use(store);
|
||||||
|
|
||||||
|
// 输出构建信息
|
||||||
|
console.log(
|
||||||
|
`%c 意心AI 3.3 %c Build Info `,
|
||||||
|
'background:#35495e; padding: 4px; border-radius: 3px 0 0 3px; color: #fff',
|
||||||
|
'background:#41b883; padding: 4px; border-radius: 0 3px 3px 0; color: #fff',
|
||||||
|
);
|
||||||
|
// console.log(`🔹 Git Branch: ${__GIT_BRANCH__}`);
|
||||||
|
console.log(`🔹 Git Commit: ${__GIT_HASH__}`);
|
||||||
|
// console.log(`🔹 Commit Date: ${__GIT_DATE__}`);
|
||||||
|
// console.log(`🔹 Build Time: ${__BUILD_TIME__}`);
|
||||||
|
|
||||||
// 挂载 Vue 应用
|
// 挂载 Vue 应用
|
||||||
// mount 完成说明应用初始化完毕,此时手动通知 loading 动画结束
|
// mount 完成说明应用初始化完毕,此时手动通知 loading 动画结束
|
||||||
app.mount('#app');
|
app.mount('#app');
|
||||||
|
|||||||
@@ -130,6 +130,49 @@ function handleRemove(file: UploadFile) {
|
|||||||
fileList.value.splice(index, 1);
|
fileList.value.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle paste event for reference images
|
||||||
|
function handlePaste(event: ClipboardEvent) {
|
||||||
|
const items = event.clipboardData?.items;
|
||||||
|
if (!items) return;
|
||||||
|
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type.startsWith('image/')) {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
if (fileList.value.length >= 2) {
|
||||||
|
ElMessage.warning('最多只能上传2张参考图');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = item.getAsFile();
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// Check file size
|
||||||
|
const isLt5M = file.size / 1024 / 1024 < 5;
|
||||||
|
if (!isLt5M) {
|
||||||
|
ElMessage.error('图片大小不能超过 5MB!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create object URL for preview
|
||||||
|
const url = URL.createObjectURL(file);
|
||||||
|
const filename = `pasted-image-${Date.now()}.${file.type.split('/')[1] || 'png'}`;
|
||||||
|
|
||||||
|
const uploadFile: UploadUserFile = {
|
||||||
|
name: filename,
|
||||||
|
url,
|
||||||
|
raw: file,
|
||||||
|
uid: Date.now(),
|
||||||
|
status: 'ready',
|
||||||
|
};
|
||||||
|
|
||||||
|
fileList.value.push(uploadFile);
|
||||||
|
ElMessage.success('已粘贴图片');
|
||||||
|
break; // Only handle the first image
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function fileToBase64(file: File): Promise<string> {
|
function fileToBase64(file: File): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
@@ -356,10 +399,14 @@ defineExpose({
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
fetchTokens();
|
fetchTokens();
|
||||||
fetchModels();
|
fetchModels();
|
||||||
|
// Add paste event listener for reference images
|
||||||
|
document.addEventListener('paste', handlePaste);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopPolling();
|
stopPolling();
|
||||||
|
// Remove paste event listener
|
||||||
|
document.removeEventListener('paste', handlePaste);
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -474,7 +521,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
</el-upload>
|
</el-upload>
|
||||||
<div class="text-xs text-gray-400 mt-2 flex justify-between items-center flex-wrap gap-2">
|
<div class="text-xs text-gray-400 mt-2 flex justify-between items-center flex-wrap gap-2">
|
||||||
<span>最多2张,< 5MB (支持 JPG/PNG/WEBP)</span>
|
<span>最多2张,< 5MB (支持 JPG/PNG/WEBP,可粘贴)</span>
|
||||||
<el-checkbox v-model="compressImage" label="压缩图片" size="small" />
|
<el-checkbox v-model="compressImage" label="压缩图片" size="small" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
11
Yi.Ai.Vue3/types/global.d.ts
vendored
11
Yi.Ai.Vue3/types/global.d.ts
vendored
@@ -1 +1,12 @@
|
|||||||
declare module "virtual:svg-icons-register";
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user