feat(AiService, Config, DatabaseInitializer, SystemConfig): enhance AI prompt configurations and improve UI for prompt management

This commit is contained in:
shiyu
2025-05-26 14:43:56 +08:00
parent 086d466975
commit da8c19c2e8
4 changed files with 187 additions and 50 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ public class Config : BaseModel
public string Key { get; set; } = string.Empty; public string Key { get; set; } = string.Empty;
[Required] [Required]
[StringLength(255)] [StringLength(1000)]
public string Value { get; set; } = string.Empty; public string Value { get; set; } = string.Empty;
[StringLength(255)] [StringLength(255)]
+21 -8
View File
@@ -59,7 +59,7 @@ public class AiService : IAiService
var textContent = new TextContent var textContent = new TextContent
{ {
Type = "text", Type = "text",
Text = Text = _configService["AI:ImageAnalysisPrompt"] ??
"请详细分析这张图片,并提供全面的描述,以便用于向量嵌入和基于文本的图像搜索。描述需要包含:主体对象、场景环境、色彩特点、构图布局、风格特征、情绪氛围、细节特征等关键元素。请提供一个简短有力的标题,然后提供详细描述。\n\n请以JSON格式返回,格式如下:\n{\"title\": \"简短概括图片的核心内容\", \"description\": \"全面详细的描述,包含上述所有元素,使用丰富精确的词汇,避免笼统表达\"}\n\n请确保返回有效的JSON格式。" "请详细分析这张图片,并提供全面的描述,以便用于向量嵌入和基于文本的图像搜索。描述需要包含:主体对象、场景环境、色彩特点、构图布局、风格特征、情绪氛围、细节特征等关键元素。请提供一个简短有力的标题,然后提供详细描述。\n\n请以JSON格式返回,格式如下:\n{\"title\": \"简短概括图片的核心内容\", \"description\": \"全面详细的描述,包含上述所有元素,使用丰富精确的词汇,避免笼统表达\"}\n\n请确保返回有效的JSON格式。"
}; };
@@ -103,7 +103,6 @@ public class AiService : IAiService
{ {
try try
{ {
// 获取配置好的 HttpClient
var client = ConfigureHttpClient(); var client = ConfigureHttpClient();
if (availableTags.Count == 0) if (availableTags.Count == 0)
@@ -111,11 +110,19 @@ public class AiService : IAiService
string model = _configService["AI:Model"]; string model = _configService["AI:Model"];
var tagsText = string.Join(", ", availableTags); var tagsText = string.Join(", ", availableTags);
string promptTemplate = _configService["AI:TagMatchingPrompt"] ??
"以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与下面描述内容高度相关的标签(最多选择5个)。只选择确实匹配的标签,如果找不到完全匹配或高度相关的标签,宁可返回空数组也不要选择不太相关的标签。\n\n描述内容:{description}\n\n请以JSON格式返回,格式如下:\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}}\n\n请确保返回有效的JSON格式前面不要加```,并且只包含确实匹配的标签名称。";
// 替换占位符
string promptText = promptTemplate
.Replace("{tagsText}", tagsText)
.Replace("{description}", description);
var textContent = new TextContent var textContent = new TextContent
{ {
Type = "text", Type = "text",
Text = Text = promptText
$"以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与下面描述内容高度相关的标签(最多选择5个)。只选择确实匹配的标签,如果找不到完全匹配或高度相关的标签,宁可返回空数组也不要选择不太相关的标签。\n\n描述内容:{description}\n\n请以JSON格式返回,格式如下:\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}}\n\n请确保返回有效的JSON格式前面不要加```,并且只包含确实匹配的标签名称。"
}; };
var message = new ChatMessage var message = new ChatMessage
@@ -232,10 +239,14 @@ public class AiService : IAiService
if (allowNewTags) if (allowNewTags)
{ {
// 获取配置的标签生成提示词,如果没有则使用默认提示词
string defaultPrompt = _configService["AI:TagGenerationPrompt"] ??
"请为图片生成5个最相关的标签,每个标签应该是简短且描述性的词语或短语。\n\n请以JSON格式返回,格式如下:\n{\"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]}\n\n请确保返回有效的JSON格式。";
// 如果允许新标签,则提供现有标签作为参考,但允许生成新标签 // 如果允许新标签,则提供现有标签作为参考,但允许生成新标签
promptText = availableTags.Count > 0 promptText = availableTags.Count > 0
? $"可以参考这些现有标签:[{string.Join(", ", availableTags)}],但也可以生成其他与图片内容相关的新标签。\n\n请为图片生成5个最相关的标签,优先使用已有标签,但如果有更恰当的新标签也可以使用。\n\n请以JSON格式返回,格式如下:\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]}}\n\n请确保返回有效的JSON格式。" ? $"可以参考这些现有标签:[{string.Join(", ", availableTags)}],但也可以生成其他与图片内容相关的新标签。\n\n{defaultPrompt}"
: "请为图片生成5个最相关的标签,每个标签应该是简短且描述性的词语或短语。\n\n请以JSON格式返回,格式如下:\n{\"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]}\n\n请确保返回有效的JSON格式。"; : defaultPrompt;
} }
else else
{ {
@@ -244,8 +255,10 @@ public class AiService : IAiService
return new List<string>(); return new List<string>();
var tagsText = string.Join(", ", availableTags); var tagsText = string.Join(", ", availableTags);
promptText = string templatePrompt = _configService["AI:TagMatchingPrompt"] ??
$"以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与图片内容高度相关的标签(最多选择5个)。只选择确实匹配的标签,如果找不到完全匹配或高度相关的标签,宁可返回空数组也不要选择不太相关的标签。\n\n请以JSON格式返回,格式如下:\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}}\n\n请确保返回有效的JSON格式,并且只包含上述列表中的标签名称。"; "以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与图片内容高度相关的标签(最多选择5个)。只选择确实匹配的标签,如果找不到完全匹配或高度相关的标签,宁可返回空数组也不要选择不太相关的标签。\n\n请以JSON格式返回,格式如下:\n{{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}}\n\n请确保返回有效的JSON格式,并且只包含上述列表中的标签名称。";
promptText = templatePrompt.Replace("{tagsText}", tagsText);
} }
var textContent = new TextContent var textContent = new TextContent
+37 -20
View File
@@ -21,7 +21,7 @@ public class DatabaseInitializer(
// 检查是否已经完成初始化 // 检查是否已经完成初始化
if (await configService.ExistsAsync(InitializationFlag) && if (await configService.ExistsAsync(InitializationFlag) &&
configService[InitializationFlag] == "true") configService[InitializationFlag] == "true")
{ {
logger.LogInformation("数据库已完成初始化,跳过初始化步骤"); logger.LogInformation("数据库已完成初始化,跳过初始化步骤");
return; return;
@@ -34,27 +34,43 @@ public class DatabaseInitializer(
// 确保数据库已创建 // 确保数据库已创建
await context.Database.EnsureCreatedAsync(); await context.Database.EnsureCreatedAsync();
// 初始化JWT配置 // 初始化默认配置
await EnsureConfigExistsAsync("Jwt:SecretKey", "ChAtPiCdEfAuLtSeCrEtKeY2023_Extended_Secure_Key"); var defaultConfigs = new Dictionary<string, string>
await EnsureConfigExistsAsync("Jwt:Issuer", "Foxel"); {
await EnsureConfigExistsAsync("Jwt:Audience", "FoxelUsers"); // JWT配置
["Jwt:SecretKey"] = "ChAtPiCdEfAuLtSeCrEtKeY2023_Extended_Secure_Key",
["Jwt:Issuer"] = "Foxel",
["Jwt:Audience"] = "FoxelUsers",
// 初始化GitHub认证配置 // GitHub认证配置
await EnsureConfigExistsAsync("Authentication:GitHubClientId", "placeholder_replace_with_actual_github_client_id"); ["Authentication:GitHubClientId"] = "placeholder_replace_with_actual_github_client_id",
await EnsureConfigExistsAsync("Authentication:GitHubClientSecret", "placeholder_replace_with_actual_github_client_secret"); ["Authentication:GitHubClientSecret"] = "placeholder_replace_with_actual_github_client_secret",
await EnsureConfigExistsAsync("Authentication:GitHubCallbackUrl", ""); ["Authentication:GitHubCallbackUrl"] = "",
// 初始化AI相关配置 // AI相关配置
await EnsureConfigExistsAsync("AI:ApiEndpoint", ""); ["AI:ApiEndpoint"] = "",
await EnsureConfigExistsAsync("AI:ApiKey", ""); ["AI:ApiKey"] = "",
await EnsureConfigExistsAsync("AI:Model", ""); ["AI:Model"] = "",
await EnsureConfigExistsAsync("AI:EmbeddingModel", ""); ["AI:EmbeddingModel"] = "",
// 初始化存储配置 ["AI:ImageAnalysisPrompt"] =
await EnsureConfigExistsAsync("Storage:TelegramStorageBotToken", ""); "Please analyze the given image in detail and provide a comprehensive description suitable for **vector embedding** and **text-based image retrieval**. Your description must include the following key elements:\n\n- **Main subject**\n- **Scene environment**\n- **Color characteristics**\n- **Composition layout**\n- **Stylistic features**\n- **Emotional atmosphere**\n- **Fine-grained details**\n\nReturn your response in **valid JSON format** as shown below:\n\n```json\n{\n \"title\": \"用中文简要概括图像核心内容的标题\",\n \"description\": \"使用中文全面、详细地描述图像内容,涵盖上述所有要素。使用丰富且精确的词汇,避免模糊或通用的表述。描述不得超过2000个字符。\"\n}\n```\n\n⚠️ Make sure:\n- Both `title` and `description` must be written in **Chinese**.\n- The `description` must be **rich, accurate**, and **strictly under 2000 characters**.\n- The output must be **valid JSON only**, with no code fences or extra formatting.",
await EnsureConfigExistsAsync("Storage:TelegramStorageChatId", ""); ["AI:TagGenerationPrompt"] =
await EnsureConfigExistsAsync("Storage:DefaultStorage", "Local"); "Please generate **5 most relevant tags** for the given image. Each tag should be a **short and descriptive** word or phrase that accurately reflects key visual or thematic elements of the image.\n\nReturn your response in **valid JSON format** as shown below:\n\n```json\n{\n \"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]\n}\n```\n\nMake sure the output is **strictly valid JSON**.",
// 初始化其他配置 ["AI:TagMatchingPrompt"] =
await EnsureConfigExistsAsync("AppSettings:ServerUrl", ""); "Given a list of tags: `[{tagsText}]`\n\nPlease strictly select only those tags that are **highly relevant** to the following description (select **up to 5**). Only include tags that **exactly or strongly match** the content. If **none** of the tags are a good match, return an **empty array** instead of including loosely related ones.\n\n**Description:**\n{description}\n\nReturn your response in **valid JSON format** as shown below:\n\n```json\n{\n \"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]\n}\n```\n\n⚠️ Do **not** include code fences (no triple backticks), and ensure the JSON is **valid** and includes only truly matching tag names.",
// 存储配置
["Storage:TelegramStorageBotToken"] = "",
["Storage:TelegramStorageChatId"] = "",
["Storage:DefaultStorage"] = "Local",
// 其他配置
["AppSettings:ServerUrl"] = ""
};
foreach (var (key, value) in defaultConfigs)
{
await EnsureConfigExistsAsync(key, value);
}
// 初始化管理员角色和用户 // 初始化管理员角色和用户
await InitializeAdminRoleAndUserAsync(); await InitializeAdminRoleAndUserAsync();
@@ -111,6 +127,7 @@ public class DatabaseInitializer(
await context.Roles.AddAsync(adminRole); await context.Roles.AddAsync(adminRole);
await context.SaveChangesAsync(); await context.SaveChangesAsync();
} }
logger.LogInformation("请注意,第一个注册的用户将自动成为管理员"); logger.LogInformation("请注意,第一个注册的用户将自动成为管理员");
} }
} }
+126 -19
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Tabs, Card, message, Spin, Select, Button, Upload, Modal, Space, Tooltip } from 'antd'; import { Tabs, Card, message, Spin, Select, Button, Upload, Modal, Space, Tooltip, Input } from 'antd';
import { CloudOutlined, DatabaseOutlined, CloudServerOutlined, GlobalOutlined, DownloadOutlined, UploadOutlined, QuestionCircleOutlined } from '@ant-design/icons'; import { CloudOutlined, DatabaseOutlined, CloudServerOutlined, GlobalOutlined, DownloadOutlined, UploadOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import { getAllConfigs, setConfig, backupConfigs, restoreConfigs } from '../../api'; import { getAllConfigs, setConfig, backupConfigs, restoreConfigs } from '../../api';
import ConfigGroup from './ConfigGroup.tsx'; import ConfigGroup from './ConfigGroup.tsx';
@@ -243,24 +243,131 @@ const SystemConfig: React.FC = () => {
}} }}
> >
<TabPane tab="AI 设置" key="AI"> <TabPane tab="AI 设置" key="AI">
<ConfigGroup <Tabs defaultActiveKey="basic" type="card" size={isMobile ? "small" : "middle"}>
groupName="AI" <TabPane tab="基础配置" key="basic">
configs={{ <ConfigGroup
ApiEndpoint: configs.AI?.ApiEndpoint || '', groupName="AI"
ApiKey: configs.AI?.ApiKey || '', configs={{
Model: configs.AI?.Model || '', ApiEndpoint: configs.AI?.ApiEndpoint || '',
EmbeddingModel: configs.AI?.EmbeddingModel || '' ApiKey: configs.AI?.ApiKey || '',
}} Model: configs.AI?.Model || '',
onSave={handleSaveConfig} EmbeddingModel: configs.AI?.EmbeddingModel || ''
descriptions={{ }}
ApiEndpoint: 'AI 服务的API端点地址', onSave={handleSaveConfig}
ApiKey: 'AI 服务的API密钥', descriptions={{
Model: 'AI 模型名称', ApiEndpoint: 'AI 服务的API端点地址',
EmbeddingModel: '嵌入向量模型名称' ApiKey: 'AI 服务的API密钥',
}} Model: 'AI 模型名称',
secretFields={secretFields.AI || []} EmbeddingModel: '嵌入向量模型名称'
isMobile={isMobile} }}
/> secretFields={secretFields.AI || []}
isMobile={isMobile}
/>
</TabPane>
<TabPane tab="提示词设置" key="prompts">
<Card
size="small"
title="图片分析提示词"
style={{ marginBottom: isMobile ? 16 : 24 }}
bodyStyle={{ padding: isMobile ? '12px' : '16px' }}
>
<Input.TextArea
rows={8}
value={configs.AI?.ImageAnalysisPrompt ||
"请详细分析这张图片,并提供全面的描述,以便用于向量嵌入和基于文本的图像搜索。描述需要包含:主体对象、场景环境、色彩特点、构图布局、风格特征、情绪氛围、细节特征等关键元素。请提供一个简短有力的标题,然后提供详细描述。\n\n请以JSON格式返回,格式如下:\n{\"title\": \"简短概括图片的核心内容\", \"description\": \"全面详细的描述,包含上述所有元素,使用丰富精确的词汇,避免笼统表达\"}\n\n请确保返回有效的JSON格式。"}
onChange={(e) => {
const newConfigs = { ...configs };
if (!newConfigs.AI) newConfigs.AI = {};
newConfigs.AI.ImageAnalysisPrompt = e.target.value;
setConfigs(newConfigs);
}}
/>
<div style={{ marginTop: 8, textAlign: 'right' }}>
<Button
type="primary"
onClick={() => handleSaveConfig('AI', 'ImageAnalysisPrompt', configs.AI?.ImageAnalysisPrompt || '')}
>
</Button>
</div>
<div style={{
fontSize: 12,
color: '#999',
marginTop: 8
}}>
JSON格式的指示(title)(description)
</div>
</Card>
<Card
size="small"
title="标签生成提示词"
style={{ marginBottom: isMobile ? 16 : 24 }}
bodyStyle={{ padding: isMobile ? '12px' : '16px' }}
>
<Input.TextArea
rows={8}
value={configs.AI?.TagGenerationPrompt ||
"请为图片生成5个最相关的标签,每个标签应该是简短且描述性的词语或短语。\n\n请以JSON格式返回,格式如下:\n{\"tags\": [\"标签1\", \"标签2\", \"标签3\", \"标签4\", \"标签5\"]}\n\n请确保返回有效的JSON格式。"}
onChange={(e) => {
const newConfigs = { ...configs };
if (!newConfigs.AI) newConfigs.AI = {};
newConfigs.AI.TagGenerationPrompt = e.target.value;
setConfigs(newConfigs);
}}
/>
<div style={{ marginTop: 8, textAlign: 'right' }}>
<Button
type="primary"
onClick={() => handleSaveConfig('AI', 'TagGenerationPrompt', configs.AI?.TagGenerationPrompt || '')}
>
</Button>
</div>
<div style={{
fontSize: 12,
color: '#999',
marginTop: 8
}}>
JSON格式的指示tags数组字段
</div>
</Card>
<Card
size="small"
title="标签匹配提示词"
style={{ marginBottom: isMobile ? 16 : 24 }}
bodyStyle={{ padding: isMobile ? '12px' : '16px' }}
>
<Input.TextArea
rows={8}
value={configs.AI?.TagMatchingPrompt ||
"以下是一组标签:[{tagsText}]。\n\n请从这些标签中严格选择与下面描述内容高度相关的标签(最多选择5个)。只选择确实匹配的标签,如果找不到完全匹配或高度相关的标签,宁可返回空数组也不要选择不太相关的标签。\n\n描述内容:{description}\n\n请以JSON格式返回,格式如下:\n{\"tags\": [\"标签1\", \"标签2\", \"标签3\"]}\n\n请确保返回有效的JSON格式前面不要加```,并且只包含确实匹配的标签名称。"}
onChange={(e) => {
const newConfigs = { ...configs };
if (!newConfigs.AI) newConfigs.AI = {};
newConfigs.AI.TagMatchingPrompt = e.target.value;
setConfigs(newConfigs);
}}
/>
<div style={{ marginTop: 8, textAlign: 'right' }}>
<Button
type="primary"
onClick={() => handleSaveConfig('AI', 'TagMatchingPrompt', configs.AI?.TagMatchingPrompt || '')}
>
</Button>
</div>
<div style={{
fontSize: 12,
color: '#999',
marginTop: 8
}}>
{'{'+'tagsText'+'}'}{'{'+'description'+'}'}
</div>
</Card>
</TabPane>
</Tabs>
</TabPane> </TabPane>
<TabPane tab="授权配置" key="Authorization"> <TabPane tab="授权配置" key="Authorization">