diff --git a/QYZH.InteractiveMagazine.IService/IAiChatService.cs b/QYZH.InteractiveMagazine.IService/IAiChatService.cs
new file mode 100644
index 0000000..d355f8c
--- /dev/null
+++ b/QYZH.InteractiveMagazine.IService/IAiChatService.cs
@@ -0,0 +1,16 @@
+using QYZH.InteractiveMagazine.Models.Dto;
+
+namespace QYZH.InteractiveMagazine.IService;
+
+///
+/// AI聊天服务接口
+///
+public interface IAiChatService
+{
+ ///
+ /// 发送消息进行AI对话
+ ///
+ /// 聊天输入
+ /// AI返回的提示词内容
+ Task ChatAsync(AiChatInput input);
+}
diff --git a/QYZH.InteractiveMagazine.Models/Dto/Ai/AiChatDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Ai/AiChatDto.cs
new file mode 100644
index 0000000..ca56d5c
--- /dev/null
+++ b/QYZH.InteractiveMagazine.Models/Dto/Ai/AiChatDto.cs
@@ -0,0 +1,23 @@
+namespace QYZH.InteractiveMagazine.Models.Dto;
+
+///
+/// AI聊天输入
+///
+public class AiChatInput
+{
+ ///
+ /// 用户需求文本
+ ///
+ public string Message { get; set; } = string.Empty;
+}
+
+///
+/// AI聊天输出
+///
+public class AiChatOutput
+{
+ ///
+ /// AI返回的内容(Markdown格式提示词)
+ ///
+ public string Content { get; set; } = string.Empty;
+}
diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs
index 4a531fd..7bb9e41 100644
--- a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs
+++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskOutput.cs
@@ -19,11 +19,6 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
///
public long JournalPageId { get; set; }
- ///
- /// 学科Id
- ///
- public long SubjectId { get; set; }
-
///
/// 题号
///
@@ -36,33 +31,10 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
public string ShowType => Type.GetDescription();
- private string _task;
- ///
- /// 问题
- ///
- public string Task
- {
- get { return _task.AddDomain(DomainHelper.OssDomain); }
- set { _task = value; }
- }
-
///
/// 答案列表
///
public List Answers { get; set; }
-
- private string _imageUrl;
- ///
- /// 图片地址
- ///
- public string ImageUrl
- {
- get { return DomainHelper.OssFullUrl(_imageUrl); }
- set { _imageUrl = value; }
- }
-
- public List ImageUrls { get; set; }
-
///
/// 积分
///
@@ -98,55 +70,10 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
///
public int AnswerTime { get; set; }
- private string _audioUrl;
///
- /// 音频地址
+ /// Prompt配置
///
- public string AudioUrl
- {
- get { return DomainHelper.OssFullUrl(_audioUrl); }
- set { _audioUrl = value; }
- }
-
- private string _videoUrl;
- ///
- /// 视频地址
- ///
- public string VideoUrl
- {
- get { return DomainHelper.OssFullUrl(_videoUrl); }
- set { _videoUrl = value; }
- }
-
- private string _pointsUrl;
- ///
- /// 点位数据地址
- ///
- public string PointsUrl
- {
- get { return DomainHelper.OssFullUrl(_pointsUrl); }
- set { _pointsUrl = value; }
- }
-
- ///
- /// 音频开始时间
- ///
- public long AudioStartTime { get; set; }
-
- ///
- /// 音频结束时间
- ///
- public long AudioEndTime { get; set; }
-
- ///
- /// 知识点
- ///
- public string KnowledgePointIds { get; set; }
-
- ///
- /// 年级
- ///
- public int Grade { get; set; }
+ public string Prompt { get; set; }
}
///
diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs
index dc0b78d..c797da2 100644
--- a/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs
+++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/JournalPageTaskUpdateInput.cs
@@ -1,4 +1,4 @@
-using QYZH.InteractiveMagazine.Models.Enum;
+using QYZH.InteractiveMagazine.Models.Enum;
using QYZH.InteractiveMagazine.Models.Enum;
using System;
using System.Collections.Generic;
@@ -67,18 +67,9 @@ namespace QYZH.InteractiveMagazine.Models.Dto.Journal
///
public int AnswerTime { get; set; }
///
- /// Desc:基础Prompt配置ID(为空则使用IsDefault=1的模板)
- /// Default:
- /// Nullable:True
- ///
- public string BasePrompt { get; set; }
-
- ///
- /// Desc:个性化Prompt(追加到基础Prompt后面,可为空)
- /// Default:
- /// Nullable:True
- ///
- public string CustomPrompt { get; set; }
+ /// Prompt配置
+ ///
+ public string Prompt { get; set; }
}
diff --git a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTask.cs b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTask.cs
index ee69e2c..c7927b9 100644
--- a/QYZH.InteractiveMagazine.Models/Entity/JournalPageTask.cs
+++ b/QYZH.InteractiveMagazine.Models/Entity/JournalPageTask.cs
@@ -122,18 +122,11 @@ namespace QYZH.InteractiveMagazine.Models.Entity
public bool NeedAiProcess { get; set; }
///
- /// Desc:基础Prompt配置ID(为空则使用IsDefault=1的模板)
+ /// Desc:Prompt配置
/// Default:
/// Nullable:True
///
- public string BasePrompt { get; set; }
-
- ///
- /// Desc:个性化Prompt(追加到基础Prompt后面,可为空)
- /// Default:
- /// Nullable:True
- ///
- public string CustomPrompt { get; set; }
+ public string Prompt { get; set; }
///
/// Desc:作答参考时间
diff --git a/QYZH.InteractiveMagazine.Models/Enum/JournalPageTaskTypeEnum.cs b/QYZH.InteractiveMagazine.Models/Enum/JournalPageTaskTypeEnum.cs
index 88d562d..af045c4 100644
--- a/QYZH.InteractiveMagazine.Models/Enum/JournalPageTaskTypeEnum.cs
+++ b/QYZH.InteractiveMagazine.Models/Enum/JournalPageTaskTypeEnum.cs
@@ -16,5 +16,5 @@ public enum JournalPageTaskTypeEnum
/// 问答题
///
[Description("主观题")]
- TaskAnswer = 4
+ TaskAnswer = 0
}
diff --git a/QYZH.InteractiveMagazine.Models/Settings/AiChatSettings.cs b/QYZH.InteractiveMagazine.Models/Settings/AiChatSettings.cs
new file mode 100644
index 0000000..fc9ef85
--- /dev/null
+++ b/QYZH.InteractiveMagazine.Models/Settings/AiChatSettings.cs
@@ -0,0 +1,37 @@
+namespace QYZH.InteractiveMagazine.Models.Settings;
+
+///
+/// AI聊天配置
+///
+public class AiChatSettings
+{
+ ///
+ /// API密钥
+ ///
+ public string? ApiKey { get; set; }
+
+ ///
+ /// API基础地址(OpenAI兼容)
+ ///
+ public string? BaseUrl { get; set; }
+
+ ///
+ /// 模型名称
+ ///
+ public string? Model { get; set; }
+
+ ///
+ /// 请求超时时间(秒)
+ ///
+ public int TimeoutSeconds { get; set; } = 300;
+
+ ///
+ /// 最大生成Token数
+ ///
+ public int MaxTokens { get; set; } = 2000;
+
+ ///
+ /// 采样温度(0-2,值越低输出越确定)
+ ///
+ public double Temperature { get; set; } = 0.5;
+}
diff --git a/QYZH.InteractiveMagazine.Service/AiChatService.cs b/QYZH.InteractiveMagazine.Service/AiChatService.cs
new file mode 100644
index 0000000..2f29ce4
--- /dev/null
+++ b/QYZH.InteractiveMagazine.Service/AiChatService.cs
@@ -0,0 +1,154 @@
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Logging;
+using QYZH.InteractiveMagazine.IService;
+using QYZH.InteractiveMagazine.Models.Common;
+using QYZH.InteractiveMagazine.Models.Dto;
+using System.Net.Http.Headers;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+
+namespace QYZH.InteractiveMagazine.Service;
+
+///
+/// AI聊天服务实现(OpenAI API规范)
+///
+public class AiChatService(
+ IHttpClientFactory httpClientFactory,
+ IConfiguration configuration,
+ ILogger logger) : IAiChatService
+{
+ ///
+ /// 预设系统提示词
+ ///
+ private const string SystemPrompt = """
+ # 提示词:AI 优化“题目评分/报告提示词”
+
+ 你是一个专业的提示词优化专家。你的任务是**将用户输入的粗糙、口语化的“题目评分/报告提示词”重构为结构清晰、指令明确、便于 AI 直接执行的标准化提示词**。
+
+ 用户(题目制作者)当前输入的内容可能包含:题目描述、作答要求、打分规则、评分维度、报告层级、示例等,但组织混乱、表述模糊。你需要**保留所有原始信息,不增删任何实质性规则**,仅优化其结构、逻辑层次和表达精度。
+
+ ---
+
+ ## 优化原则
+
+ 1. **信息完整**:用户原文中的所有要点、示例、层级区分必须全部保留。
+ 2. **结构清晰**:将内容归入标准模块(如 `任务目标`、`输入格式`、`评分标准`、`报告生成要求`)。
+ 3. **指令明确**:将模糊表述(如“要写得好一点”)转化为可操作的判定条件(如“必须包含三层结构:解释原因 + 表达情绪 + 提出方案”)。
+ 4. **语言精练**:去除冗余修饰,保留关键限定词(如“仅”“必须”“至少”)。
+ 5. **格式统一**:最终输出为 Markdown 格式,使用标题(`#` `##` `###`)、列表(`-`)、加粗/斜体等提升可读性。
+
+ ---
+
+ ## 优化步骤
+
+ 1. **解析原文**:通读用户输入,标注出:
+ - 题目背景 / 情境
+ - 学生作答要求
+ - 评分维度(优秀/一般/薄弱)及其具体判定条件
+ - 报告层级及其对应的评语模板或生成逻辑
+ - 任何示例答案
+ 2. **重组结构**:按以下标准模板组织内容(可灵活调整章节名称):
+ - `## 任务概述`(题目 + 要求)
+ - `## 输入数据`(学生作答的内容)
+ - `## 评分标准`(分档描述,含判定规则)
+ - `## 报告生成规范`(层级名称、触发条件、评语示例/生成规则)
+ - `## 注意事项`(如有特殊约束)
+ 3. **润色表达**:
+ - 将“可能”“大概”等不确定词改为明确的“必须”“至少”;
+ - 将并列条件用编号列表(1. 2. 3.)拆分;
+ - 将示例单独以代码块或引用块呈现,避免与规则混淆。
+ 4. **保持原意**:不得添加用户未提及的新评分维度或报告层级,也不得删除任何已有规则。
+
+ ---
+
+ ## 输出格式要求
+
+ - 只输出优化后的完整提示词(即 AI 可直接使用的评分/报告 prompt),不要输出分析过程。
+ - 使用 Markdown 语法,标题层级从 `#` 开始。
+ - 所有原有示例必须原样保留,并标明“示例”或放在引用块中。
+
+ ---
+
+ 现在,请对**当前用户输入的原始提示词**执行上述优化过程,仅输出优化后的 Markdown 格式提示词。
+ """;
+
+ ///
+ /// AI聊天,根据用户需求生成提示词
+ ///
+ public async Task ChatAsync(AiChatInput input)
+ {
+ if (string.IsNullOrWhiteSpace(input.Message))
+ {
+ throw new BusinessException("消息内容不能为空", 400);
+ }
+
+ var apiKey = configuration["AiChat:ApiKey"];
+ var baseUrl = configuration["AiChat:BaseUrl"];
+ var model = configuration["AiChat:Model"];
+ var timeoutSeconds = configuration.GetValue("AiChat:TimeoutSeconds");
+ var maxTokens = configuration.GetValue("AiChat:MaxTokens");
+ var temperature = configuration.GetValue("AiChat:Temperature");
+
+ if (string.IsNullOrWhiteSpace(apiKey) || string.IsNullOrWhiteSpace(baseUrl) || string.IsNullOrWhiteSpace(model))
+ {
+ throw new BusinessException("AI聊天服务配置不完整,请检查 AiChat 配置节", 500);
+ }
+
+ logger.LogInformation("开始调用AI聊天服务,消息长度:{Length}", input.Message.Length);
+
+ // 构建 OpenAI Chat Completions 请求体
+ var requestBody = new
+ {
+ model = model,
+ messages = new[]
+ {
+ new { role = "system", content = SystemPrompt },
+ new { role = "user", content = input.Message }
+ },
+ max_tokens = maxTokens,
+ temperature = temperature
+ };
+
+ var jsonContent = JsonSerializer.Serialize(requestBody, new JsonSerializerOptions
+ {
+ DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
+ });
+
+ // 创建 HttpClient 并发起请求
+ var client = httpClientFactory.CreateClient();
+ client.Timeout = TimeSpan.FromSeconds(timeoutSeconds > 0 ? timeoutSeconds : 300);
+ var request = new HttpRequestMessage(HttpMethod.Post, $"{baseUrl.TrimEnd('/')}/chat/completions");
+ request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
+ request.Content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
+
+ var response = await client.SendAsync(request);
+ var responseContent = await response.Content.ReadAsStringAsync();
+
+ if (!response.IsSuccessStatusCode)
+ {
+ logger.LogError("AI聊天服务调用失败,状态码:{StatusCode},响应:{Response}", response.StatusCode, responseContent);
+ throw new BusinessException($"AI服务调用失败:{response.StatusCode}", 500);
+ }
+
+ // 解析 OpenAI 响应
+ using var doc = JsonDocument.Parse(responseContent);
+ var choices = doc.RootElement.GetProperty("choices");
+ if (choices.GetArrayLength() == 0)
+ {
+ throw new BusinessException("AI未返回有效内容", 500);
+ }
+
+ var content = choices[0]
+ .GetProperty("message")
+ .GetProperty("content")
+ .GetString();
+
+ logger.LogInformation("AI聊天服务调用成功,返回内容长度:{Length}", content?.Length ?? 0);
+
+ return new AiChatOutput
+ {
+ Content = content ?? string.Empty
+ };
+ }
+}
diff --git a/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs b/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs
index e4d55ae..504f9e5 100644
--- a/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs
+++ b/QYZH.InteractiveMagazine.Service/JournalPageTaskService.cs
@@ -1,4 +1,4 @@
-using Mapster;
+using Mapster;
using QYZH.InteractiveMagazine.Common.Extensions;
using QYZH.InteractiveMagazine.Infrastructure.OSS;
using QYZH.InteractiveMagazine.IService;
@@ -66,8 +66,7 @@ public class JournalPageTaskService(BaseRepository journalRepository, O
task.Expression = input.Expression;
task.Persuasiveness = input.Persuasiveness;
task.AnswerTime = input.AnswerTime;
- task.BasePrompt = input.BasePrompt;
- task.CustomPrompt = input.CustomPrompt;
+ task.Prompt = input.Prompt;
var no = input.No.Split('-').Select(int.Parse).ToArray();
@@ -97,7 +96,6 @@ public class JournalPageTaskService(BaseRepository journalRepository, O
JournalPageId = task.JournalPageId,
No = task.No,
Type = task.Type,
- Task = task.Task,
Points = task.Points,
GrowthPoint = task.GrowthPoint ?? 0,
Comprehension = task.Comprehension,
@@ -105,6 +103,7 @@ public class JournalPageTaskService(BaseRepository journalRepository, O
Expression = task.Expression,
Persuasiveness = task.Persuasiveness,
AnswerTime = task.AnswerTime,
+ Prompt = task.Prompt
};
// 查询答案列表
diff --git a/QYZH.InteractiveMagazine.WebApi/Controllers/AiChatController.cs b/QYZH.InteractiveMagazine.WebApi/Controllers/AiChatController.cs
new file mode 100644
index 0000000..8e1d9c9
--- /dev/null
+++ b/QYZH.InteractiveMagazine.WebApi/Controllers/AiChatController.cs
@@ -0,0 +1,50 @@
+using Microsoft.AspNetCore.Mvc;
+using QYZH.InteractiveMagazine.IService;
+using QYZH.InteractiveMagazine.Models.Common;
+using QYZH.InteractiveMagazine.Models.Dto;
+using QYZH.InteractiveMagazine.Models.Enum;
+
+namespace QYZH.InteractiveMagazine.WebApi.Controllers;
+
+///
+/// AI聊天控制器
+///
+[Route("api/[controller]")]
+[ApiController]
+[ApiExplorerSettings(GroupName = nameof(ApiVersionEnum.Platform))]
+public class AiChatController : BaseController
+{
+ private readonly IAiChatService _aiChatService;
+ private readonly ILogger _logger;
+
+ public AiChatController(IAiChatService aiChatService, ILogger logger)
+ {
+ _aiChatService = aiChatService;
+ _logger = logger;
+ }
+
+ ///
+ /// AI聊天,根据需求生成提示词
+ ///
+ /// 用户需求消息
+ /// AI生成的提示词(Markdown格式)
+ [HttpPost]
+ public async Task> ChatAsync([FromBody] AiChatInput input)
+ {
+ try
+ {
+ var result = await _aiChatService.ChatAsync(input);
+ return Success(result, "AI聊天成功");
+ }
+ catch (BusinessException ex)
+ {
+ _logger.LogWarning(ex, "AI聊天业务异常: {Message}", ex.Message);
+ return BaseResponse.Fail(ex.Message);
+ }
+ catch (Exception ex)
+ {
+ _logger.LogError(ex, "AI聊天系统异常,参数:{Input}", input);
+ return BaseResponse.Fail("AI聊天失败,请稍后重试");
+ }
+ }
+}
diff --git a/QYZH.InteractiveMagazine.WebApi/appsettings.json b/QYZH.InteractiveMagazine.WebApi/appsettings.json
index c2f66a5..d6d78f9 100644
--- a/QYZH.InteractiveMagazine.WebApi/appsettings.json
+++ b/QYZH.InteractiveMagazine.WebApi/appsettings.json
@@ -47,6 +47,14 @@
]
},
"AllowedHosts": "*",
+ "AiChat": {
+ "ApiKey": "Ollama",
+ "BaseUrl": "http://172.16.10.130:11434/v1/",
+ "Model": "qwen2.5vl:7b",
+ "TimeoutSeconds": 300,
+ "MaxTokens": 2000,
+ "Temperature": 0.5
+ },
"AliyunOSSConfigs": {
"AccessKeyID": "LTAI5tEBXGewpHSLiSxyx6Bf",
"AccessKeySecret": "w29b8wkw6XQVL8GWXgp3ZesgYeDKvf",