feat(prompt):添加prompt模板管理功能

- 新增 PromptTemplate 模型和相关 API
- 实现 prompt 模板的添加、删除和查询功能
- 在前端添加 prompt 管理界面
- 修改聊天流 API,支持使用自定义 prompt
This commit is contained in:
ArvinLovegood 2025-03-29 21:31:06 +08:00
parent 2b41dc11c1
commit fd3046b2c3
10 changed files with 300 additions and 16 deletions

21
app.go
View File

@ -564,8 +564,8 @@ func (a *App) SendDingDingMessageByType(message string, stockCode string, msgTyp
return data.NewDingDingAPI().SendDingDingMessage(message)
}
func (a *App) NewChatStream(stock, stockCode, question string) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question)
func (a *App) NewChatStream(stock, stockCode, question string, sysPromptId *int) {
msgs := data.NewDeepSeekOpenAi(a.ctx).NewChatStream(stock, stockCode, question, sysPromptId)
for msg := range msgs {
runtime.EventsEmit(a.ctx, "newChatStream", msg)
}
@ -802,6 +802,23 @@ func (a *App) SaveAsMarkdown(stockCode, stockName string) string {
}
return "分析结果异常,无法保存。"
}
func (a *App) GetPromptTemplates(name, promptType string) *[]models.PromptTemplate {
return data.NewPromptTemplateApi().GetPromptTemplates(name, promptType)
}
func (a *App) AddPrompt(prompt models.Prompt) string {
promptTemplate := models.PromptTemplate{
ID: prompt.ID,
Content: prompt.Content,
Name: prompt.Name,
Type: prompt.Type,
}
return data.NewPromptTemplateApi().AddPrompt(promptTemplate)
}
func (a *App) DelPrompt(id uint) string {
return data.NewPromptTemplateApi().DelPrompt(id)
}
func OnSecondInstanceLaunch(secondInstanceData options.SecondInstanceData) {
notification := toast.Notification{
AppID: "go-stock",

View File

@ -96,7 +96,7 @@ type AiResponse struct {
SystemFingerprint string `json:"system_fingerprint"`
}
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[string]any {
func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string, sysPromptId *int) <-chan map[string]any {
ch := make(chan map[string]any, 512)
defer func() {
@ -113,12 +113,25 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
}
}()
defer close(ch)
logger.SugaredLogger.Errorf("NewChatStream stock:%s stockCode:%s,sysPromptId:%d", stock, stockCode, *sysPromptId)
sysPrompt := ""
if sysPromptId == nil || *sysPromptId == 0 {
sysPrompt = o.Prompt
} else {
sysPrompt = NewPromptTemplateApi().GetPromptTemplateByID(*sysPromptId)
}
if sysPrompt == "" {
sysPrompt = o.Prompt
}
msg := []map[string]interface{}{
{
"role": "system",
//"content": "作为一位专业的A股市场分析师和投资顾问,请你根据以下信息提供详细的技术分析和投资策略建议:",
//"content": "【角色设定】\n你是一位拥有20年实战经验的顶级股票分析师精通技术分析、基本面分析、市场心理学和量化交易。擅长发现成长股、捕捉行业轮动机会在牛熊市中都能保持稳定收益。你的风格是价值投资与技术择时相结合注重风险控制。\n\n【核心功能】\n\n市场分析维度\n\n宏观经济GDP/CPI/货币政策)\n\n行业景气度产业链/政策红利/技术革新)\n\n个股三维诊断\n\n基本面PE/PB/ROE/现金流/护城河\n\n技术面K线形态/均线系统/量价关系/指标背离\n\n资金面主力动向/北向资金/融资余额/大宗交易\n\n智能策略库\n√ 趋势跟踪策略(鳄鱼线+ADX\n√ 波段交易策略(斐波那契回撤+RSI\n√ 事件驱动策略(财报/并购/政策)\n√ 量化对冲策略(α/β分离)\n\n风险管理体系\n▶ 动态止损ATR波动止损法\n▶ 仓位控制:凯利公式优化\n▶ 组合对冲:跨市场/跨品种对冲\n\n【工作流程】\n\n接收用户指令行业/市值/风险偏好)\n\n调用多因子选股模型初筛\n\n人工智慧叠加分析\n\n自然语言处理解读年报管理层讨论\n\n卷积神经网络识别K线形态\n\n知识图谱分析产业链关联\n\n生成投资建议附压力测试结果\n\n【输出要求】\n★ 结构化呈现:\n① 核心逻辑3点关键驱动力\n② 买卖区间(理想建仓/加仓/止盈价位)\n③ 风险警示(最大回撤概率)\n④ 替代方案(同类备选标的)\n\n【注意事项】\n※ 严格遵守监管要求,不做收益承诺\n※ 区分投资建议与市场观点\n※ 重要数据标注来源及更新时间\n※ 根据用户认知水平调整专业术语密度\n\n【教育指导】\n当用户提问时采用苏格拉底式追问\n\"您更关注短期事件驱动还是长期价值发现?\"\n\"当前仓位是否超过总资产的30%\"\n\"是否了解科创板与主板的交易规则差异?\"\n\n示例输出格式\n📈 标的名称XXXXXX\n⚖ 多空信号:金叉确认/顶背离预警\n🎯 关键价位支撑位XX.XX/压力位XX.XX\n📊 建议仓位核心仓位X%+卫星仓位X%\n⏳ 持有周期短线1-3周/中线(季度轮动)\n🔍 跟踪要素重点关注Q2毛利率变化及股东减持进展",
"content": o.Prompt,
"content": sysPrompt,
},
}
@ -142,7 +155,7 @@ func (o OpenAi) NewChatStream(stock, stockCode, userQuestion string) <-chan map[
}
logger.SugaredLogger.Infof("NewChatStream stock:%s stockCode:%s", stock, stockCode)
logger.SugaredLogger.Infof("Prompt%s", o.Prompt)
logger.SugaredLogger.Infof("Prompt%s", sysPrompt)
logger.SugaredLogger.Infof("User Prompt config:%v", o.QuestionTemplate)
logger.SugaredLogger.Infof("User question:%s", userQuestion)
logger.SugaredLogger.Infof("final question:%s", question)

View File

@ -0,0 +1,75 @@
package data
import (
"go-stock/backend/db"
"go-stock/backend/logger"
"go-stock/backend/models"
)
type PromptTemplateApi struct {
}
func (t PromptTemplateApi) GetPromptTemplates(name string, promptType string) *[]models.PromptTemplate {
var result []models.PromptTemplate
if name != "" && promptType != "" {
db.Dao.Model(&models.PromptTemplate{}).Where("name=? and type=?", name, promptType).Find(&result)
}
if name != "" && promptType == "" {
db.Dao.Model(&models.PromptTemplate{}).Where("name=?", name).Find(&result)
}
if name == "" && promptType != "" {
db.Dao.Model(&models.PromptTemplate{}).Where("type=?", promptType).Find(&result)
}
if name == "" && promptType == "" {
db.Dao.Model(&models.PromptTemplate{}).Find(&result)
}
return &result
}
func (t PromptTemplateApi) AddPrompt(template models.PromptTemplate) string {
var tmp models.PromptTemplate
db.Dao.Model(&models.PromptTemplate{}).Where("id=?", template.ID).First(&tmp)
if tmp.ID == 0 {
err := db.Dao.Model(&models.PromptTemplate{}).Create(&models.PromptTemplate{
Content: template.Content,
Name: template.Name,
Type: template.Type,
}).Error
if err != nil {
return "添加失败"
} else {
return "添加成功"
}
} else {
err := db.Dao.Model(&models.PromptTemplate{}).Where("id=?", template.ID).Updates(template).Error
if err != nil {
return "更新失败"
} else {
return "更新成功"
}
}
}
func (t PromptTemplateApi) DelPrompt(Id uint) string {
template := &models.PromptTemplate{}
db.Dao.Model(template).Where("id=?", Id).Find(template)
if template.ID > 0 {
err := db.Dao.Model(template).Delete(template).Error
if err != nil {
return "删除失败"
} else {
return "删除成功"
}
}
return "模板信息不存在"
}
func (t PromptTemplateApi) GetPromptTemplateByID(id int) string {
prompt := &models.PromptTemplate{}
db.Dao.Model(&models.PromptTemplate{}).Where("id=?", id).First(prompt)
logger.SugaredLogger.Infof("GetPromptTemplateByID:%d %s", id, prompt.Content)
return prompt.Content
}
func NewPromptTemplateApi() *PromptTemplateApi {
return &PromptTemplateApi{}
}

View File

@ -195,3 +195,23 @@ type Resp struct {
Code int `json:"code"`
Message string `json:"message"`
}
type PromptTemplate struct {
ID int `gorm:"primarykey"`
CreatedAt time.Time
UpdatedAt time.Time
Name string `json:"name"`
Content string `json:"content"`
Type string `json:"type"`
}
func (p PromptTemplate) TableName() string {
return "prompt_templates"
}
type Prompt struct {
ID int `json:"ID"`
Name string `json:"name"`
Content string `json:"content"`
Type string `json:"type"`
}

View File

@ -1,9 +1,16 @@
<script setup>
import {computed, onMounted, ref} from "vue";
import {ExportConfig, GetConfig, SendDingDingMessageByType, UpdateConfig} from "../../wailsjs/go/main/App";
import {
AddPrompt, DelPrompt,
ExportConfig,
GetConfig,
GetPromptTemplates,
SendDingDingMessageByType,
UpdateConfig
} from "../../wailsjs/go/main/App";
import {useMessage} from "naive-ui";
import {data} from "../../wailsjs/go/models";
import {data, models} from "../../wailsjs/go/models";
import {EventsEmit} from "../../wailsjs/runtime";
const message = useMessage()
@ -38,7 +45,7 @@ const formValue = ref({
enableNews:false,
darkTheme:true,
})
const promptTemplates=ref([])
onMounted(()=>{
GetConfig().then(res=>{
formValue.value.ID = res.ID
@ -73,6 +80,11 @@ onMounted(()=>{
console.log(res)
})
//message.info("")
GetPromptTemplates("","").then(res=>{
console.log(res)
promptTemplates.value=res
})
})
@ -196,6 +208,49 @@ window.onerror = function (event, source, lineno, colno, error) {
//message.error(":"+event)
return true;
};
const showManagePromptsModal=ref(false)
const promptTypeOptions=[
{label:"模型系统Prompt",value:'模型系统Prompt'},
{label:"模型用户Prompt",value:'模型用户Prompt'},]
const formPromptRef=ref(null)
const formPrompt=ref({
ID:0,
Name:'',
Content:'',
Type:'',
})
function managePrompts(){
formPrompt.value.ID=0
showManagePromptsModal.value=true
}
function savePrompt(){
AddPrompt(formPrompt.value).then(res=>{
message.success(res)
GetPromptTemplates("","").then(res=>{
console.log(res)
promptTemplates.value=res
})
showManagePromptsModal.value=false
})
}
function editPrompt(prompt){
console.log(prompt)
formPrompt.value.ID=prompt.ID
formPrompt.value.Name=prompt.name
formPrompt.value.Content=prompt.content
formPrompt.value.Type=prompt.type
showManagePromptsModal.value=true
}
function deletePrompt(ID){
DelPrompt(ID).then(res=>{
message.success(res)
GetPromptTemplates("","").then(res=>{
console.log(res)
promptTemplates.value=res
})
})
}
</script>
<template>
@ -304,6 +359,9 @@ window.onerror = function (event, source, lineno, colno, error) {
</n-grid>
<n-gi :span="24">
<n-space justify="center">
<n-button type="warning" @click="managePrompts">
添加提示词模板
</n-button>
<n-button type="primary" @click="saveConfig">
保存
</n-button>
@ -316,7 +374,52 @@ window.onerror = function (event, source, lineno, colno, error) {
</n-space>
</n-gi>
</n-form>
<n-gi :span="24" v-if="promptTemplates.length>0" v-for="prompt in promptTemplates" >
<n-flex justify="start">
<n-tag closable @close="deletePrompt(prompt.ID)" @click="editPrompt(prompt)" :title="prompt.content" :type="prompt.type==='模型系统Prompt'?'success':'info'" :bordered="false"> {{prompt.name}} </n-tag>
</n-flex>
</n-gi>
</n-flex>
<n-modal v-model:show="showManagePromptsModal" closable :mask-closable="false">
<n-card
style="width: 800px;height: 600px;text-align: left"
:bordered="false"
:title="(formPrompt.ID>0?'修改':'添加')+'提示词'"
size="huge"
role="dialog"
aria-modal="true"
>
<n-form ref="formPromptRef" :label-placement="'left'" :label-align="'left'" >
<n-form-item label="名称">
<n-input v-model:value="formPrompt.Name" placeholder="请输入提示词名称" />
</n-form-item>
<n-form-item label="类型">
<n-select v-model:value="formPrompt.Type" :options="promptTypeOptions" placeholder="请选择提示词类型" />
</n-form-item>
<n-form-item label="内容">
<n-input v-model:value="formPrompt.Content"
type="textarea"
:show-count="true"
placeholder="请输入prompt"
:autosize="{
minRows: 12,
maxRows: 12,
}"
/>
</n-form-item>
</n-form>
<template #footer>
<n-flex justify="end">
<n-button type="primary" @click="savePrompt">
保存
</n-button>
<n-button type="warning" @click="showManagePromptsModal=false">
取消
</n-button>
</n-flex>
</template>
</n-card>
</n-modal>
</template>
<style scoped>

View File

@ -15,7 +15,7 @@ import {
SetCostPriceAndVolume,
SetStockSort,
UnFollow,
ShareAnalysis, SaveAsMarkdown
ShareAnalysis, SaveAsMarkdown, GetPromptTemplates
} from '../../wailsjs/go/main/App'
import {
NAvatar,
@ -82,11 +82,14 @@ const formModel = ref({
alarmPrice:0,
sort:999,
})
const promptTemplates=ref([])
const sysPromptOptions=ref([])
const userPromptOptions=ref([])
const data = reactive({
modelName:"",
chatId: "",
question:"",
sysPromptId:null,
name: "",
code: "",
fenshiURL:"",
@ -155,6 +158,16 @@ onBeforeMount(()=>{
data.darkTheme = true
}
})
GetPromptTemplates("","").then(res=>{
promptTemplates.value=res
sysPromptOptions.value=promptTemplates.value.filter(item => item.type === '模型系统Prompt')
userPromptOptions.value=promptTemplates.value.filter(item => item.type === '模型用户Prompt')
console.log("userPromptOptions",userPromptOptions.value)
console.log("sysPromptOptions",sysPromptOptions.value)
})
})
onMounted(() => {
@ -649,7 +662,8 @@ function aiReCheckStock(stock,stockCode) {
})
//
NewChatStream(stock,stockCode,data.question)
//message.info("sysPromptId:"+data.sysPromptId)
NewChatStream(stock,stockCode,data.question,data.sysPromptId)
}
function aiCheckStock(stock,stockCode){
GetAIResponseResult(stockCode).then(result => {
@ -682,7 +696,7 @@ function aiCheckStock(stock,stockCode){
message.loading("ai检测中...",{
duration: 0,
})
NewChatStream(stock,stockCode)
NewChatStream(stock,stockCode,"",data.sysPromptId)
}
})
}
@ -993,6 +1007,11 @@ function share(code,name){
</n-flex>
</template>
<template #action>
<n-flex justify="space-between" style="margin-bottom: 10px">
<n-select style="width: 49%" v-model:value="data.sysPromptId" label-field="name" value-field="ID" :options="sysPromptOptions" placeholder="请选择系统提示词" />
<n-select style="width: 49%" v-model:value="data.question" label-field="name" value-field="content" :options="userPromptOptions" placeholder="请选择用户提示词" />
</n-flex>
<n-flex justify="right">
<n-input v-model:value="data.question" style="text-align: left" clearable
type="textarea"

View File

@ -3,8 +3,12 @@
import {models} from '../models';
import {data} from '../models';
export function AddPrompt(arg1:models.Prompt):Promise<string>;
export function CheckUpdate():Promise<void>;
export function DelPrompt(arg1:number):Promise<string>;
export function ExportConfig():Promise<string>;
export function Follow(arg1:string):Promise<string>;
@ -19,6 +23,8 @@ export function GetFollowList():Promise<any>;
export function GetFollowedFund():Promise<Array<data.FollowedFund>>;
export function GetPromptTemplates(arg1:string,arg2:string):Promise<any>;
export function GetStockList(arg1:string):Promise<Array<data.StockBasic>>;
export function GetVersionInfo():Promise<models.VersionInfo>;
@ -27,7 +33,7 @@ export function GetfundList(arg1:string):Promise<Array<data.FundBasic>>;
export function Greet(arg1:string):Promise<data.StockInfo>;
export function NewChatStream(arg1:string,arg2:string,arg3:string):Promise<void>;
export function NewChatStream(arg1:string,arg2:string,arg3:string,arg4:any):Promise<void>;
export function SaveAIResponseResult(arg1:string,arg2:string,arg3:string,arg4:string,arg5:string):Promise<void>;

View File

@ -2,10 +2,18 @@
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
// This file is automatically generated. DO NOT EDIT
export function AddPrompt(arg1) {
return window['go']['main']['App']['AddPrompt'](arg1);
}
export function CheckUpdate() {
return window['go']['main']['App']['CheckUpdate']();
}
export function DelPrompt(arg1) {
return window['go']['main']['App']['DelPrompt'](arg1);
}
export function ExportConfig() {
return window['go']['main']['App']['ExportConfig']();
}
@ -34,6 +42,10 @@ export function GetFollowedFund() {
return window['go']['main']['App']['GetFollowedFund']();
}
export function GetPromptTemplates(arg1, arg2) {
return window['go']['main']['App']['GetPromptTemplates'](arg1, arg2);
}
export function GetStockList(arg1) {
return window['go']['main']['App']['GetStockList'](arg1);
}
@ -50,8 +62,8 @@ export function Greet(arg1) {
return window['go']['main']['App']['Greet'](arg1);
}
export function NewChatStream(arg1, arg2, arg3) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3);
export function NewChatStream(arg1, arg2, arg3, arg4) {
return window['go']['main']['App']['NewChatStream'](arg1, arg2, arg3, arg4);
}
export function SaveAIResponseResult(arg1, arg2, arg3, arg4, arg5) {

View File

@ -494,6 +494,24 @@ export namespace models {
return a;
}
}
export class Prompt {
ID: number;
name: string;
content: string;
type: string;
static createFrom(source: any = {}) {
return new Prompt(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ID = source["ID"];
this.name = source["name"];
this.content = source["content"];
this.type = source["type"];
}
}
export class VersionInfo {
ID: number;
// Go type: time

View File

@ -65,6 +65,7 @@ func main() {
db.Dao.AutoMigrate(&models.StockInfoUS{})
db.Dao.AutoMigrate(&data.FollowedFund{})
db.Dao.AutoMigrate(&data.FundBasic{})
db.Dao.AutoMigrate(&models.PromptTemplate{})
if stocksBin != nil && len(stocksBin) > 0 {
go initStockData()
@ -141,7 +142,7 @@ func main() {
//Width: width * 4 / 5,
//Height: height * 4 / 5,
MinWidth: 1456,
MinHeight: 820,
MinHeight: 900,
//MaxWidth: width,
//MaxHeight: height,
DisableResize: false,