# LangChain V1.0 30日学习计划 --- Day 3:Prompt + Model + Message + OutputParser 工业写法

1. PromptTemplate

1.1 导包方式

LangChain v1.0.2 的 PromptTemplate 统一在 langchain_core.prompts 包下

from langchain_core.prompts import PromptTemplate

PromptTemplate 生成结构化的 PromptMessageModel 使用,是整条链的第一块砖。

1.2 工业级写法

参数解析:

  • template:模板字符串,可以写任意自然语言,但必须包含占位符(变量),占位符格式为:{占位符}

    示例:

    template = "请将下面文字总结成一句话:{text}"
    

    注意: 占位符 {text} 必须和 input_variables 对得上

  • input_variables:告诉 PromptTemplate 这个模板会用到哪些变量

    示例:

    input_variables = ["text"]
    

    模板里出现什么占位符,就必须在 input_variables 中列出来
    多一个不行,少一个也不行

  • partial_variables(可选参数):提前写好一些不变的变量,用作常量

    partial_variables = {"常量名": "值"}
    

完整示例:

from langchain_core.prompts import PromptTemplate

prompt = PromptTemplate(
    template = "请将下面文字总结成一句话:{text}",
    input_variables = ["text"],
    validate_template = True  # 开启自动验证
)

工业化模板管理:

# 使用配置化的模板管理
TEMPLATE_REGISTRY = {
    "summarization": "请总结以下内容:{text},要求{format}",
    "translation": "将{source_lang}翻译成{target_lang}:{text}",
    # ... 更多提示词模板
}

def get_prompt(template_name, **kwargs):
    return PromptTemplate(
        # 通过模板名称调用相应模板
        template = TEMPLATE_REGISTRY[template_name],
        # 自动推断 input_variables,不用手写 "text", "format"
        input_variables = list(kwargs.keys())
    )
  • TEMPLATE_REGISTRY 是"提示模板字典",像仓库一样存储常用 prompt
  • get_prompt(template_name, **kwargs) 是"模板工厂",自动创建完整的 PromptTemplate

1.3 PromptTemplate 的子类

ChatPromptTemplate(对话格式)

当 Prompt 需要多轮对话角色时使用,包含 3 个标准角色:

  • system:设定模型的"底层行为方式"

    • 角色设定
    • 输出格式约束
    • 行为限制或风格要求
    • 安全规约
  • user:用户输入的内容(指令、问题)

    • “现在我要你做啥?”
    • 每次调用模型都会构造一个 user message
  • assistant:模型上一轮的回答(用于多轮对话 context)

示例:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("system", "你是翻译专家,翻译时要简洁准确。"),
    ("user", "请翻译:{text}"),
])

自定义角色

ChatPromptTemplate 允许自定义角色:

from langchain_core.prompts import ChatPromptTemplate

prompt = ChatPromptTemplate.from_messages([
    ("planner", "你的任务是先分析需求,再给出步骤。"),
    ("critic", "你的任务是审查 planner 的方案是否合理。"),
    ("user", "现在开始处理:{text}")
])

模型会根据角色名推断"这个角色说话的风格/职责",最终根据上下文决定输出。

2. Model

2.1 LangChain v1 的 Model 概念

在 LangChain V1 中,所有模型都是 Runnable

流程:PromptTemplate → ChatModel → Parser

2.2 使用方法

导包:

from langchain_openai import ChatOpenAI

构造:

model = ChatOpenAI(
    model = "str",           # 要调用的模型标识
    temperature = 0.7,       # 控制输出随机性;工业默认常设为 0
    max_tokens = 1000,       # 控制生成长度上限
    stop = list[str],        # 指定终止序列
    timeout = 30,            # 超时设置
    max_retries = 2,         # 重试机制
    streaming = True,        # 流式输出(生产环境推荐)
)

2.3 Model 层工业级写法

完整的 ChatOpenAI 配置

from langchain_openai import ChatOpenAI

# 工业级配置:包含超时、重试、流式输出
model = ChatOpenAI(
    model = "gpt-3.5-turbo",
    temperature = 0.7,       # 控制创造性
    max_tokens = 1000,
    timeout = 30,            # 超时设置
    max_retries = 2,         # 重试机制
    streaming = True,        # 流式输出(生产环境推荐)
)

模型配置管理

from dataclasses import dataclass

# 定义模型配置类
@dataclass
class ModelConfig:
    model_name: str = "gpt-3.5-turbo"
    temperature: float = 0.7
    max_tokens: int = 1000
    timeout: int = 30

def create_model(config: ModelConfig):
    # 自动解包,将一个字典解包成关键字参数
    return ChatOpenAI(**config.__dict__)

工程建议:模型工厂模式

  1. 配置文件 (config.yaml)
# 默认使用的提供商名称
provider: openai 

openai:
  model: gpt-4o-mini
  temperature: 0

azure:
  model: gpt-4o-mini
  deployment: my-deployment
  base_url: https://xxx.openai.azure.com/
  api_key: YOUR_AZURE_KEY

local:
  model: qwen2:7b
  base_url: http://localhost:8000/v1

openrouter:
  model: deepseek-chat
  base_url: https://openrouter.ai/api/v1
  api_key: YOUR_OPENROUTER_KEY
  1. 工厂函数
import yaml
from langchain_openai import ChatOpenAI, AzureChatOpenAI
from langchain_core.language_models.chat_models import BaseChatModel

def load_config(path = "配置文件路径"):
    """加载 YAML 配置文件"""
    with open(path, "r", encoding = "utf-8") as f:
        return yaml.safe_load(f)  # 返回字典

def create_model(config) -> BaseChatModel:
    """根据配置动态创建模型节点"""
    provider = config["provider"]
    
    if provider == "openai":
        cfg = config["openai"]
        return ChatOpenAI(
            model = cfg["model"],
            temperature = cfg["temperature"]
        )
    elif provider == "azure":
        cfg = config["azure"]
        return AzureChatOpenAI(
            model = cfg["model"],
            deployment_id = cfg["deployment"],
            base_url = cfg["base_url"],
            api_key = cfg["api_key"],
            temperature = 0
        )
    elif provider == "local":
        cfg = config["local"]
        return ChatOpenAI(
            model = cfg["model"],
            base_url = cfg["base_url"],
            temperature = 0
        )
    elif provider == "openrouter":
        cfg = config["openrouter"]
        return ChatOpenAI(
            model = cfg["model"],
            base_url = cfg["base_url"],
            api_key = cfg["api_key"],
            temperature = 0
        )
    else:
        raise ValueError(f"Unknown provider: {provider}")
  1. 配置化 PromptTemplate 工厂
from langchain_core.prompts import PromptTemplate

TEMPLATE_REGISTRY = {
    "summarization": "请总结以下内容:{text},要求{format}",
    "translation": "将{source_lang}翻译成{target_lang}:{text}"
}

def get_prompt(name, **kwargs):
    template = TEMPLATE_REGISTRY[name]
    return PromptTemplate(
        template = template,
        input_variables = list(kwargs.keys())
    )
  1. 整合链路
from langchain_core.output_parsers import StrOutputParser

def build_pipeline(config):
    """构建模型和解析器"""
    model = create_model(config)
    parser = StrOutputParser()
    return model, parser

# 使用示例
config = load_config()
model, parser = build_pipeline(config)

# 构建提示词模板
prompt = get_prompt("summarization", text = "秦总今天学习模型工厂", format = "一句话总结")

# 构建链
chain = prompt | model | parser

result = chain.invoke({
    "text": "秦总今天学习模型工厂",
    "format": "一句话总结"
})

print(result)

3. Message

3.1 什么是 Message?

Message = 一条带角色的对话内容

它是 ChatModel 的最小输入单位,模型不吃纯字符串,它吃的是「多条 message」

3.2 Message 类型

from langchain_core.messages import (
    SystemMessage,
    HumanMessage,
    AIMessage,
    ToolMessage
)
  • SystemMessage:告诉模型"你是谁、你应该怎么做"

    SystemMessage(content = "你是翻译专家。")
    
  • HumanMessage:用户的输入

    HumanMessage(content = "请翻译:你好世界")
    
  • AIMessage:模型生成的回答

    AIMessage(content = "Hello world")
    
  • ToolMessage:工具返回给模型的内容(做 Agent 时才用)

3.3 为什么大多数时候不手写这些类?

因为 ChatPromptTemplate 会自动帮你创建对应类型。

当你在写:

("system", "你是翻译专家")
("user", "请翻译:{text}")

底层实际上是:

  • system → SystemMessage
  • user → HumanMessage
  • assistant → AIMessage

流程:Message 列表 → 模型 → AIMessage

ChatPromptTemplate 就是一个 Message 工厂

4. OutputParser

4.1 Parser 的作用

Model 的输出是 "AIMessage"对象,但真正需要的是:结构化数据

Parser = 把模型的"话"变成产品能用的数据

4.2 导包

from langchain_core.output_parsers import XXXOutputParser

4.3 Parser 的类别

4.3.1 StrOutputParser

功能: 把 AIMessage → 解析成纯字符串

from langchain_core.output_parsers import StrOutputParser

parser = StrOutputParser()

# 构建链路
chain = prompt | model | parser

4.3.2 JsonOutputParser

功能: 令模型直接输出结构化数据(工业最常用)

from langchain_core.output_parsers import JsonOutputParser

parser = JsonOutputParser()

完整示例:

# Step 1:创建稳健的 system message
from langchain_core.prompts import ChatPromptTemplate

json_prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "你是信息抽取专家。所有输出必须是严格可解析的 JSON。"
        "只输出 JSON,不要出现自然语言、注释、解释,也不要输出 ```json 代码块格式。"
        "输出字段:姓名、事件。"
    ),
    ("user", "请抽取信息:{text}")
])

# Step 2:创建 JSON Parser
from langchain_core.output_parsers import JsonOutputParser

parser = JsonOutputParser()

# Step 3:创建模型节点
from langchain_openai import ChatOpenAI

model = ChatOpenAI(
    model = "gpt-4o-mini",
    temperature = 0
)

# Step 4:组成链
chain = json_prompt | model | parser

# 执行链
result = chain.invoke({
    "text": "张三今天去买了瓶酱油,然后遇到超市抽奖,中了五百万"
})

4.3.3 PydanticOutputParser(强类型数据类)

功能: 抽取出的字段必须是满足需求的"数据类结构"

特点:

  • 字段不允许多
  • 字段不允许少
  • 字段类型必须对(str / int / list / bool …)
  • 模型如果乱写,直接报错,避免脏数据流入

使用步骤:

  1. 定义数据类的 schema
from pydantic import BaseModel, Field

class Info(BaseModel):  # 继承 BaseModel
    # Field 提供字段的元信息
    # description:字段描述,告诉 LLM 这个字段应该填什么内容
    name: str = Field(description = "人物姓名")
    event: str = Field(description = "发生的事件")
  1. 创建 PydanticOutputParser
from langchain_core.output_parsers import PydanticOutputParser

# 给 parser 一个"数据类"
parser = PydanticOutputParser(pydantic_object = Info)

# 获取格式说明
format_instructions = parser.get_format_instructions()
  1. 把 format_instructions 注入 Prompt
from langchain_core.prompts import ChatPromptTemplate

json_prompt = ChatPromptTemplate.from_messages([
    (
        "system",
        "你是信息抽取专家,只输出 JSON,不要输出解释、注释、自然语言。"
    ),
    (
        "user", 
        "请从下面内容中抽取信息。\n\n内容:{text}\n\n格式要求:{format_instructions}"
    )
])
  1. 构建完整链路
chain = json_prompt | model | parser

# 调用链
result = chain.invoke({
    "text": "张三今天去买了瓶酱油,然后遇到超市抽奖,中了五百万",
    "format_instructions": parser.get_format_instructions()
})

print(result)
# 输出:Info(name='张三', event='去买了瓶酱油,然后遇到超市抽奖,中了五百万')

总结

  • PromptTemplate / ChatPromptTemplate —— 负责把变量灌进提示词
  • Model(ChatOpenAI、Ollama、DeepSeek 等) —— 负责推理
  • OutputParser —— 负责结构化输出(尤其 JSON / Pydantic)
  • Runnable 链符号:| —— 像管道一样把节点串起来
  • invoke() —— 喂数据、启动链路流动
  • 输出就是强类型 Python 对象(适合自动化系统)
<template> <div> <header class="ant-layout-header header_sd-header_common"> <div class="left_sd-header_common"> <div class="logo_sd-webflow_webflow"> <h3>激励考核指下发</h3> </div> </div> <div class="right_sd-header_common"> <a-button v-if="!readonly" type="link" icon="save" class="button_sd-webflow-button_webflow" @click="save" > 保存 </a-button> <a-button v-if="!readonly" type="link" icon="save" class="button_sd-webflow-button_webflow" @click="xfClick" > 下发 </a-button> <a-button type="link" icon="close-circle" class="button_sd-webflow-button_webflow" @click="btnClose" > 退出 </a-button> </div> </header> <div class="budget-record"> <div class="budget-record-table"> <h2 class="budget-record-table-title">激励考核指下发</h2> <a-form-model ref="ruleForm" :rules="rules" :model="project" class="sitd-apply" layout="horizontal" > <table> <colgroup> <col width="170px" /> <col /> <col width="170px" /> <col /> </colgroup> <tr> <td class="ant-form-item-label cell_sd-form-item-td-label_common"> <label title="指标编号">指标编号</label> </td> <td class="ant-form-item-control-wrapper" :colspan="3"> <a-form-model-item prop="zbbh"> <template>{{ project[&#39;zbbh&#39;] }}</template> </a-form-model-item> </td> </tr> <tr> <td class="ant-form-item-label cell_sd-form-item-td-label_common"> <label title="指标年度" class="ant-form-item-required">指标年度</label> </td> <td class="ant-form-item-control-wrapper"> <a-form-model-item prop="zbnd"> <!-- 修改为下拉选择框 --> <a-select v-if="!readonly" v-model="project[&#39;zbnd&#39;]" placeholder="请选择年份" style="width: 100%" > <!-- 生成年份选项 --> <a-select-option v-for="year in yearOptions" :key="year" :value="year"> {{ year }} </a-select-option> </a-select> <template v-else> {{ project[&#39;zbnd&#39;] }} </template> </a-form-model-item> </td> <td class="ant-form-item-label cell_sd-form-item-td-label_common"> <label title="考核周期">考核周期</label> </td> <td class="ant-form-item-control-wrapper"> <a-form-model-item prop="khzq"> <a-input v-if="!readonly" v-model="project[&#39;khzq&#39;]" placeholder="考核周期" /> <template v-else>{{ project[&#39;khzq&#39;] }}</template> </a-form-model-item> </td> </tr> <tr> <td class="ant-form-item-label cell_sd-form-item-td-label_common"> <label title="考核对象">考核对象</label> </td> <td class="ant-form-item-control-wrapper"> <a-form-model-item prop="khdx"> <a-select v-if="!readonly" v-model="selectedKhdxIds" mode="multiple" style="width: 100%" placeholder="请选择被考核对象" @change="handleKhdxChange" > <template v-for="(item, _i) in khdxList"> <a-select-option :key="item.code" :value="item.code">{{ item.value }}</a-select-option> </template> </a-select> <template v-else>{{ project[&#39;khdx&#39;] }}</template> </a-form-model-item> </td> <td class="ant-form-item-label cell_sd-form-item-td-label_common"> <label title="创建期" class="ant-form-item-required">创建期</label> </td> <td class="ant-form-item-control-wrapper"> <a-form-model-item prop="cjrq"> <template>{{ project[&#39;cjrq&#39;] ? refreshTime(project[&#39;cjrq&#39;]) : &#39;&#39; }}</template> </a-form-model-item> </td> </tr> <tr> <td class="ant-form-item-label cell_sd-form-item-td-label_common upload-td"> <label title="说明">说明</label> </td> <td class="ant-form-item-control-wrapper" colspan="3"> <a-form-model-item prop="sm"> <div v-if="!readonly" class="textarea-wrapper"> <a-textarea v-model="project[&#39;sm&#39;]" placeholder="请输入内容" :max-length="300" class="m-textarea" :auto-size="{ minRows: 3, maxRows: 5 }" /> <span class="m-count">{{ `${project[&#39;sm&#39;]?.length || 0}/300` }}</span> </div> <template v-else>{{ project[&#39;sm&#39;] }}</template> </a-form-model-item> </td> </tr> </table> </a-form-model> <!-- 这里暂时去掉,根据客户描述,一年只进行一次年度考核和激励考核,因此根据年度和单位关联即可 --> <!-- <h3 class="budget-record-table-title">关联年度考核</h3> <a-button v-if="!readonly" type="primary" :style="{ marginLeft: &#39;88px&#39; }" class="button_sd-webflow-button_webflow" @click="openDialogGl" > 关联年度考核 </a-button> <a-button v-if="!readonly" :style="{ marginLeft: &#39;8px&#39; }" class="button_sd-webflow-button_webflow" :disabled="!ndkhSelectedRowKeys.length" @click="ndkhBeforDelete" > 删除 </a-button> <a-modal title="选择年度考核" :visible="dialogVisibleGl" width="80%" @ok="handleDialogConfirm" @cancel="dialogVisibleGl = false" > <div class="apply-search"> <a-form-model :form="searchForm" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }"> <a-row :gutter="24"> <a-col :span="8" class="chd"> <a-form-model-item label="考核年度"> <a-input v-model="searchForm[&#39;khnd&#39;]" allow-clear placeholder="请输入指标年度" /> </a-form-model-item> </a-col> <a-col :span="8" class="chd"> <a-form-model-item label="考核周期"> <a-input v-model="searchForm[&#39;khzq&#39;]" allow-clear placeholder="请输入考核周期" /> </a-form-model-item> </a-col> </a-row> <a-row> <a-col :span="24" class="chd" :style="{ textAlign: &#39;right&#39;, lineHeight: &#39;45px&#39; }"> <a-button :style="{ marginRight: &#39;8px&#39; }" @click="reset"> 重置 </a-button> <a-button type="primary" html-type="submit" @click="btnSearchBusinessList"> 查询 </a-button> </a-col> </a-row> </a-form-model> </div> <a-divider style="margin: 12px 0" /> <SdTable ref="ndkh" row-key="id" :columns="ndkhColumns" :data-source.sync="dialogResult" :pagination="paginationOpt" :loading="dialogLoading" :scroll="{ x: 1500 }" :row-selection="{ selectedRowKeys: selectedRowKeysAdd, onChange: btnSelectProjectAdd, }" > </SdTable> </a-modal> <SdTable ref="ndkhTable" row-key="id" :columns="ndkhColumns" :data-source.sync="ndkhResult" :loading="ndkhLoading" :pagination="false" class="ant-table" :row-selection="{ selectedRowKeys: ndkhSelectedRowKeys, onChange: btnSelectProjectNdkh, }" > </SdTable> --> <h3 class="budget-record-table-title" style="margin-top: 18px">激励考核指标</h3> <a-button v-if="!readonly" type="primary" :style="{ marginLeft: &#39;88px&#39; }" class="button_sd-webflow-button_webflow" @click="openDialog" > 新增 </a-button> <a-button v-if="!readonly" :style="{ marginLeft: &#39;8px&#39; }" class="button_sd-webflow-button_webflow" :disabled="!selectedRowKeys.length" @click="btnBeforDelete" > 删除 </a-button> <SdTable ref="jlkhzbxf" row-key="id" :columns="jlkhzbxfColumns" :data-source.sync="jlkhzbxfResult" :loading="loading" :pagination="false" class="ant-table" bordered :row-selection="{ selectedRowKeys: selectedRowKeys, onChange: btnSelectProject, }" > <!-- 指标内容列自定义渲染 --> <template slot="指标内容" slot-scope="text, record, index"> <div style="white-space: pre-line; text-align: left; padding: 8px 0"> {{ text }} </div> </template> <template slot="分值" slot-scope="text, record, index"> <div style="white-space: pre-line; text-align: left; padding: 8px 0"> {{ text }} </div> </template> <!-- 操作列 --> <template v-if="!readonly" slot="操作" slot-scope="text, record"> <a-button class="action-button" type="link" size="small" @click="btnOpenProjectDetails(record)" > 编辑 </a-button> </template> </SdTable> <a-modal title="激励考核指标" :visible="dialogVisible" width="35%" @ok="handleConfirm" @cancel="handleCancel" > <div class="apply-search"> <a-form-model ref="subFormRef" :rules="jlkhzbxfSubRules" :form="jlkhzbxfSubForm" :label-col="{ span: 8 }" :wrapper-col="{ span: 16 }" > <a-row> <a-col :span="18"> <a-form-model-item label="指标类别"> <a-input v-model="jlkhzbxfSubForm[&#39;zblb&#39;]" allow-clear placeholder="请输入指标类别" /> </a-form-model-item> </a-col> </a-row> <a-row> <a-col :span="18"> <a-form-model-item label="指标内容"> <a-textarea v-model="jlkhzbxfSubForm[&#39;zbnr&#39;]" placeholder="请输入指标内容(按Enter换行)" :max-length="300" class="m-textarea" :auto-size="{ minRows: 3, maxRows: 6 }" /> </a-form-model-item> </a-col> </a-row> <a-row> <a-col :span="18"> <a-form-model-item label="分值"> <a-textarea v-model="jlkhzbxfSubForm[&#39;fz&#39;]" placeholder="请输入分值(按Enter换行)" :max-length="300" class="m-textarea" :auto-size="{ minRows: 2, maxRows: 4 }" /> </a-form-model-item> </a-col> </a-row> <a-row> <a-col :span="18"> <a-form-model-item label="分值上限"> <a-input v-model="jlkhzbxfSubForm[&#39;fzsx&#39;]" allow-clear placeholder=&#39;例如:"+1分"、"-1.5分"或"不设上限"&#39; /> </a-form-model-item> </a-col> </a-row> </a-form-model> </div> </a-modal> </div> </div> </div> </template> <script> import { Message, Modal } from &#39;ant-design-vue&#39; import moment from &#39;moment&#39; import components from &#39;./_import-components/zk-jlkh-zbxf-import&#39; import KhypjService from &#39;./khypj-service&#39; import crossWindowWatcher from &#39;@/common/services/cross-window-watcher&#39; import SdTable from &#39;@/common/components/sd-table.vue&#39; export default { name: &#39;ZkJlkhZbxf&#39;, metaInfo: { title: &#39;激励考核指标下发&#39;, }, components: { ...components, SdTable, }, props: { id: { type: String, default: undefined, }, }, data() { // 添加校验规则 const validateFzsx = (rule, value, callback) => { if (!value) { callback() return } // 允许"不设上限" if (value === &#39;不设上限&#39;) { callback() return } // 验证格式: ±数字(可含小数) + 单位(非数字字符) const regex = /^[+-]\d+(\.\d+)?[^\d]+$/ if (!regex.test(value)) { callback(new Error(&#39;格式错误,请填写"不设上限"或类似"+1分"、"-1.5分"的格式&#39;)) } else { callback() } } return { loading: false, ndkhLoading: false, dialogLoading: false, readonly: true, yearOptions: this.generateYearOptions(), rules: { zbnd: [{ required: true, message: &#39;请选择指标年度&#39;, trigger: &#39;blur&#39; }], }, jlkhzbxfSubRules: { fzsx: [{ validator: validateFzsx, trigger: &#39;blur&#39; }], }, // 新增搜索表单数据 searchForm: { khnd: undefined, // 考核年度 khzq: undefined, // 考核周期 }, project: { id: undefined, // 主键id zbbh: undefined, // 指标编号 cjrq: undefined, // 创建期 zbnd: undefined, // 指标年度 khzq: undefined, // 考核周期 khdx: undefined, // 考核对象 khdxId: undefined, // 考核对象id sm: undefined, // 说明 status: undefined, // 指标状态:0暂存,1下发(不可修改) ndkhId: undefined, // 关联的年度考核id }, jlkhzbxfResult: [], jlkhzbxfSubForm: { id: undefined, // id mainId: undefined, // 主表id zblb: undefined, // 指标类别 zbnr: undefined, // 指标内容 fz: undefined, // 分值 fzsx: undefined, // 分值上限 }, ndkhResult: [], khdxList: [], selectedKhdxIds: [], // 用于多选绑定的临时ID数组 dialogVisible: false, dialogVisibleGl: false, selectedKeys: [], selectedRows: [], selectedRowKeys: [], ndkhSelectedRowKeys: [], // 年度考核表格的选中项 ndkhSelectedRows: [], // 年度考核表格选中的行数据 selectedKeysAdd: [], selectedRowKeysAdd: [], // 年度考核表格的选中项 selectedRowsAdd: [], // 年度考核表格选中的行数据 isUpdateJlkhzbxfSub: false, isXf: false, ndkhColumns: [ { title: &#39;考核编号&#39;, dataIndex: &#39;khbh&#39;, scopedSlots: { customRender: &#39;考核编号&#39;, }, width: &#39;200px&#39;, align: &#39;center&#39;, }, { title: &#39;考核年度&#39;, dataIndex: &#39;khnd&#39;, scopedSlots: { customRender: &#39;指标年度&#39;, }, width: &#39;70px&#39;, align: &#39;center&#39;, }, { title: &#39;考核对象&#39;, dataIndex: &#39;khdx&#39;, scopedSlots: { customRender: &#39;责任主体&#39;, }, width: &#39;200px&#39;, align: &#39;center&#39;, }, { title: &#39;考核周期&#39;, dataIndex: &#39;khzq&#39;, scopedSlots: { customRender: &#39;考核周期&#39;, }, width: &#39;100px&#39;, align: &#39;center&#39;, }, ], dialogResult: [], dialogColumns: [], paginationOpt: { current: 1, // 当前页码 pageSize: 10, // 当前每页大小 total: 0, // 总数 showSizeChanger: true, showQuickJumper: false, pageSizeOptions: [&#39;10&#39;, &#39;20&#39;, &#39;40&#39;, &#39;60&#39;, &#39;80&#39;, &#39;100&#39;], showTotal: (total) => `共 ${total} 条`, onShowSizeChange: (current, pageSize) => { this.paginationOpt.current = 1 this.paginationOpt.pageSize = pageSize this.btnSearchBusinessList() }, onChange: (current, size) => { this.paginationOpt.current = current this.paginationOpt.pageSize = size this.btnSearchBusinessList() }, }, } }, computed: { // 动态计算列配置 jlkhzbxfColumns() { const baseColumns = [ { title: &#39;序号&#39;, customRender: (text, record, index) => { return index + 1 }, align: &#39;center&#39;, width: &#39;10%&#39;, }, { title: &#39;指标类别&#39;, dataIndex: &#39;zblb&#39;, customRender: (text, record) => { // 只有显示行才渲染内容 if (record.rowSpan > 0) { return { children: text, attrs: { rowSpan: record.rowSpan }, } } return { attrs: { rowSpan: 0 } } }, align: &#39;center&#39;, width: &#39;10%&#39;, }, { title: &#39;指标内容&#39;, dataIndex: &#39;zbnr&#39;, scopedSlots: { customRender: &#39;指标内容&#39;, }, align: &#39;center&#39;, width: &#39;40%&#39;, }, { title: &#39;分值&#39;, dataIndex: &#39;fz&#39;, scopedSlots: { customRender: &#39;分值&#39;, }, align: &#39;center&#39;, width: &#39;20%&#39;, }, { title: &#39;分值上限&#39;, dataIndex: &#39;fzsx&#39;, scopedSlots: { customRender: &#39;分值上限&#39;, }, align: &#39;center&#39;, width: &#39;10%&#39;, }, ] // 非只读模式下添加操作列 if (!this.readonly) { baseColumns.push({ title: &#39;操作&#39;, dataIndex: &#39;action&#39;, scopedSlots: { customRender: &#39;操作&#39; }, width: &#39;10%&#39;, align: &#39;center&#39;, }) } return baseColumns }, }, watch: { jlkhzbxfResult: { handler(newVal) { this.calculateRowSpans(newVal) }, immediate: true, // 初始化时立即执行 deep: true, // 深度监听 }, }, created() { this.getKhdxList() this.jlkhzbxfResult = this.calculateRowSpans(this.jlkhzbxfResult) this.initJlkhzbxfInfo() }, mounted() { if (!this.$route.query.id) { this.project.zbbh = this.getZBbh() this.project.cjrq = moment(new Date()).format(&#39;YYYY-MM-DD&#39;) this.readonly = false } }, methods: { // 年度考核表格选中处理 btnSelectProjectNdkh(selectedRowKeys, selectedRows) { this.ndkhSelectedRowKeys = selectedRowKeys this.ndkhSelectedRows = [...selectedRows] }, btnSelectProjectAdd(selectedRowKeys, selectedRows) { this.selectedRowKeysAdd = selectedRowKeys this.selectedRowsAdd = [...selectedRows] }, // 重置搜索表单 reset() { this.searchForm = { khnd: undefined, // 考核年度 khzq: undefined, // 考核周期 } this.paginationOpt.current = 1 this.paginationOpt.pageSize = 10 this.btnSearchBusinessList() }, getKhzbxfInfo() { console.log(&#39;1111111----------&#39;, this.project) console.log(&#39;22222222----------&#39;, this.project.ndkhId) if (this.project.ndkhId) { KhypjService.getKhzbxfById(this.project.ndkhId).then((res) => { if (res.data.code === 200) { if (res.data.data) { this.ndkhResult.push(res.data.data) } else { this.ndkhResult = [] } } else { Message.error(&#39;获取考核指标下发信息失败!&#39;) } }) } }, ndkhBeforDelete() { const _this = this if (_this.ndkhSelectedRowKeys.length === 0) { Message.warning(&#39;请选择要删除的数据!&#39;) return } Modal.confirm({ content: &#39;是否确认删除?&#39;, onOk() { _this.project.ndkhId = &#39;-1&#39; _this.saveJlkhzbxf() _this.ndkhResult = [] _this.ndkhSelectedRowKeys = [] // 清空选中状态 }, onCancel() {}, }) }, openDialogGl() { console.log(&#39;3333----------&#39;, this.ndkhResult) if (this.ndkhResult.length > 0) { Message.warning(&#39;只能关联一条年度考核!&#39;) return } this.dialogVisibleGl = true this.selectedKeysAdd = [] this.selectedRowsAdd = [] this.btnSearchBusinessList() }, handleDialogConfirm() { if (this.selectedRowsAdd.length === 0) { Message.warning(&#39;请选择一条年度考核!&#39;) return } if (this.selectedRowsAdd.length > 1) { Message.warning(&#39;只能关联一条年度考核!&#39;) return } this.project.ndkhId = this.selectedRowsAdd[0].id this.saveJlkhzbxf() this.getKhzbxfInfo() this.dialogVisibleGl = false }, btnSearchBusinessList() { this.dialogLoading = true const khzbxfSearchVo = { khnd: this.searchForm.khnd, khzq: this.searchForm.khzq, } const params = { current: this.paginationOpt.current, // 直接使用当前页码 pageSize: this.paginationOpt.pageSize, // 直接使用当前每页大小 searchVo: khzbxfSearchVo, } KhypjService.getNdkhList(params) .then((res) => { if (res.data.code === 200) { this.paginationOpt.total = res.data.data.total this.dialogResult = res.data.data.records } else { Message.error(&#39;查询失败!&#39;) } }) .catch((err) => { Message.error(err.message || err.data) this.paginationOpt.total = 0 this.dialogResult = [] }) .finally(() => { this.dialogLoading = false }) }, getKhdxList() { KhypjService.getCodeList(&#39;khdx&#39;).then((res) => { if (res.data.code === 200) { this.khdxList = res.data.data } else { Message.error(&#39;查询失败!&#39;) } }) }, // 处理考核对象选择变化 handleKhdxChange(selectedIds) { // 1. 拼接ID字符串 this.project.khdxId = selectedIds.join(&#39;;&#39;) // 2. 拼接名称字符串 const selectedNames = [] selectedIds.forEach((code) => { const item = this.khdxList.find((d) => d.code === code) if (item) { selectedNames.push(item.value) } }) this.project.khdx = selectedNames.join(&#39;;&#39;) }, btnBeforDelete() { const _this = this if (this.selectedRowKeys.length === 0) { Message.warning(&#39;请选择要删除的数据!&#39;) return } Modal.confirm({ content: &#39;是否确认删除?&#39;, onOk() { _this.deleteJlkhzbxfSub() _this.selectedRowKeys = [] }, onCancel() {}, }) }, deleteJlkhzbxfSub() { KhypjService.removeJlkhzbxfSubByIds(this.selectedRowKeys).then((res) => { if (res.data.code === 200) { Message.success(&#39;删除成功!&#39;) this.getJlkhzbxfSubList() } else { Message.error(&#39;查询失败!&#39;) } }) }, btnSelectProject(selectedRowKeys, selectedRows) { this.selectedRowKeys = selectedRowKeys this.selectedRows = [...selectedRows] }, openDialog() { this.dialogVisible = true this.selectedKeys = [] this.selectedRows = [] this.jlkhzbxfSubForm = { id: undefined, // id mainId: this.$route.query.id, // 主表id zblb: undefined, // 指标类别 zbnr: undefined, // 指标内容 fz: undefined, // 分值 fzsx: undefined, // 分值上限 } // 新增子数据自动保存主数据 if (!this.$route.query.id) { this.saveJlkhzbxf() } }, btnOpenProjectDetails(record) { this.dialogVisible = true this.jlkhzbxfSubForm = { id: record.id, // id mainId: record.mainId, // 主表id zblb: record.zblb, // 指标类别 zbnr: record.zbnr, // 指标内容 fz: record.fz, // 分值 fzsx: record.fzsx, // 分值上限 } this.isUpdateJlkhzbxfSub = true console.log(&#39;121212------&#39;, record) console.log(&#39;33333222------&#39;, this.jlkhzbSubxfForm) }, handleCancel() { this.dialogVisible = false }, // 弹窗确认 handleConfirm() { // 添加表单校验 this.$refs.subFormRef.validate((valid) => { if (!valid) { return false } // 原有的保存逻辑... if (!this.isUpdateJlkhzbxfSub) { this.jlkhzbxfSubForm.mainId = this.$route.query.id // 保存子表数据 KhypjService.saveJlkhzbxfSub(this.jlkhzbxfSubForm).then((res) => { if (res.data.code === 200) { Message.success(`添加成功!`) this.getJlkhzbxfSubList() } else { Message.error(&#39;添加失败!&#39;) } }) } else { // 保存子表数据 KhypjService.updateJlkhzbxfSub(this.jlkhzbxfSubForm).then((res) => { if (res.data.code === 200) { Message.success(`修改成功!`) this.getJlkhzbxfSubList() } else { Message.error(&#39;修改失败!&#39;) } }) this.isUpdateJlkhzbxfSub = false } this.dialogVisible = false }) }, // handleConfirm() { // if (!this.isUpdateJlkhzbxfSub) { // this.jlkhzbxfSubForm.mainId = this.$route.query.id // // 保存子表数据 // KhypjService.saveJlkhzbxfSub(this.jlkhzbxfSubForm).then((res) => { // if (res.data.code === 200) { // Message.success(`添加成功!`) // this.getJlkhzbxfSubList() // } else { // Message.error(&#39;添加失败!&#39;) // } // }) // } else { // // 保存子表数据 // KhypjService.updateJlkhzbxfSub(this.jlkhzbxfSubForm).then((res) => { // if (res.data.code === 200) { // Message.success(`修改成功!`) // this.getJlkhzbxfSubList() // } else { // Message.error(&#39;修改失败!&#39;) // } // }) // this.isUpdateJlkhzbxfSub = false // } // this.dialogVisible = false // }, // 计算行合并 calculateRowSpans(data) { if (!data || data.length === 0) return data const categoryMap = {} // 第一步:统计每个类别的行数 data.forEach((item) => { categoryMap[item.zblb] = (categoryMap[item.zblb] || 0) + 1 }) // 第二步:设置行合并属性 let lastCategory = null return data.map((item) => { // 重置rowSpan属性 item.rowSpan = undefined if (item.zblb !== lastCategory) { lastCategory = item.zblb // 只有每个类别的第一行设置rowSpan item.rowSpan = categoryMap[item.zblb] return item } // 同类别的后续行设置rowSpan为0 item.rowSpan = 0 return item }) }, // 生成年份选项 generateYearOptions() { const currentYear = new Date().getFullYear() const years = [] // 添加年份:当前年后1年、本年、前1年、前2年 years.push(currentYear + 1) // 后1年 years.push(currentYear) // 本年 years.push(currentYear - 1) // 前1年 years.push(currentYear - 2) // 前2年 return years }, btnClose() { window.close() }, initJlkhzbxfInfo() { if (this.$route.query.id) { KhypjService.getJlkhzbxfById(this.$route.query.id).then((res) => { if (res.data.code === 200) { this.project = res.data.data this.project.cjrq = this.project.cjrq ? moment(new Date(this.project.cjrq)).format(&#39;YYYY-MM-DD&#39;) : null this.readonly = this.project.status === 1 // 初始化选中项 if (this.project.khdxId) { this.selectedKhdxIds = this.project.khdxId.split(&#39;;&#39;).filter(Boolean) } this.getJlkhzbxfSubList() this.getKhzbxfInfo() } else { Message.error(&#39;获取考核指标库信息失败!&#39;) } }) } }, save() { const _this = this this.$refs.ruleForm.validate((valid, obj) => { if (!valid) { Message.warning(&#39;请输入必填项!&#39;) return false } else { _this.saveJlkhzbxf() } }) }, xfClick() { this.readonly = true this.isXf = true this.save() KhypjService.jlkhzbzpXfStart(this.$route.query.id).then((res) => { if (res.data.code === 200) { Message.success(&#39;下发成功!&#39;) } else { Message.error(&#39;下发失败!&#39;) } }) }, // 修改 saveJlkhzbxf 方法返回 Promise saveJlkhzbxf() { const _this = this if (this.isXf) { this.project.status = 1 } else { this.project.status = 0 } // 创建数据副本,避免污染原始数据 const postData = { ...this.project } // 仅在值是字符串时才进行转换 if (postData.cjrq && typeof postData.cjrq === &#39;string&#39;) { postData.cjrq = this.strToTimestamp(postData.cjrq) } // 返回一个 Promise return new Promise((resolve, reject) => { // 判断是新增还是更新 const apiCall = !this.$route.query.id ? KhypjService.saveJlkhzbxf(postData) : KhypjService.updateJlkhzbxf(postData) apiCall .then((res) => { if (res.data.code === 200) { // 处理新增保存成功 if (!_this.$route.query.id) { Message.success(&#39;保存成功!&#39;) if (res.data.data) { const currentUrl = window.location.href const newUrl = currentUrl.replace(&#39;zbxf&#39;, `zbxf?id=${res.data.data}`) // 直接跳转页面,不需要等待初始化 window.location.href = newUrl } resolve() // 解析 Promise } // 处理更新保存成功 else { if (_this.project.status === 1) { Message.success(&#39;提交成功!&#39;) } else { Message.success(&#39;保存成功!&#39;) } resolve() // 解析 Promise } } else { // 处理失败情况 const errorMessage = !_this.project.id ? &#39;保存失败!&#39; : _this.project.status === 1 ? &#39;提交失败!&#39; : &#39;保存失败!&#39; Message.error(errorMessage) reject(new Error(res.data.message || errorMessage)) } }) .catch((error) => { // 处理 API 错误 const errorMessage = !_this.project.id ? &#39;保存失败!&#39; : _this.project.status === 1 ? &#39;提交失败!&#39; : &#39;保存失败!&#39; console.error(&#39;API 错误:&#39;, error) Message.error(errorMessage) reject(error) }) }) }, getJlkhzbxfSubList() { KhypjService.getJlkhzbxfSubList(this.$route.query.id).then((res) => { if (res.data.code === 200) { this.jlkhzbxfResult = res.data.data } else { Message.error(&#39;获取子表信息失败!&#39;) } }) }, // 获取唯一指标编号 getZBbh() { // 获取当前时间 const now = new Date() // 获取当前时间的年、月、、时、分、秒、毫秒 const year = now.getFullYear() const month = String(now.getMonth() + 1).padStart(2, &#39;0&#39;) const day = String(now.getDate()).padStart(2, &#39;0&#39;) const hour = String(now.getHours()).padStart(2, &#39;0&#39;) const minute = String(now.getMinutes()).padStart(2, &#39;0&#39;) const second = String(now.getSeconds()).padStart(2, &#39;0&#39;) const millisecond = String(now.getMilliseconds()).padStart(3, &#39;0&#39;) // 生成唯一标识 const uniqueId = `${year}${month}${day}${hour}${minute}${second}${millisecond}` return &#39;JLKHZBXF-&#39; + uniqueId }, strToTimestamp(dateStr) { const date = moment(dateStr, &#39;YYYY-MM-DD&#39;) return date.valueOf() }, refreshTime(item) { return moment(item).format(&#39;YYYY-MM-DD&#39;) }, }, } </script> <style lang="scss" scoped> /* 设置表格宽度 */ .ant-table { width: 90%; margin: auto; /* 居中显示 */ } .sitd-collapse-block { width: 90%; margin: auto; &::v-deep(.ant-collapse-content-box) { padding: unset !important; } } table { width: 90%; margin: auto; border-top: 1px solid #e8e8e8; margin-bottom: 16px; table-layout: fixed; tr td { border-left: 1px solid #e8e8e8; border-bottom: 1px solid #e8e8e8; min-height: 100px; &:first-child { border-left: unset; } &:nth-child(2n + 1) { background: #fafafa; width: 170px; } &.upload-td { background: #fafafa; width: 170px !important; } &.upload-time-td { background: #fafafa; width: 200px !important; } &.ant-form-item-check { width: 194px !important; } .ant-form-item { margin-bottom: unset !important; padding: 5px; .ant-form-item-label { width: 170px; padding-bottom: 1px; text-align: center; background-color: #fafafa; } } &::v-deep(label) { display: block; margin: 0 auto; text-align: right; white-space: normal; line-height: 20px; padding: 0 5px; } .ant-form-item-prompt { display: block; margin: 0 auto; text-align: right; white-space: normal; line-height: 20px; padding: 0 14px 0 5px; font-size: 16px; color: rgba(0, 0, 0, 0.85); } .prompt-red { color: #f5222d !important; } .ant-form-item-radio { &::v-deep(label) { display: inline !important; } } &::v-deep(.ant-checkbox + span) { padding-right: 0 !important; } } ::v-deep(.ant-form-item-control-wrapper) { min-height: 40px; } } .ant-form-long-label { tr td { &:nth-child(2n + 1) { // width: 200px !important; } } } .ant-collapse { border-top: 1px solid #d9d9d9; border-bottom: unset; border-left: unset; border-right: unset; width: 90%; margin: auto; & > .ant-collapse-item { border-bottom: unset; /*& > .ant-collapse-content{*/ /* border-top: 1px solid #d9d9d9;*/ /* border-bottom: 1px solid #d9d9d9;*/ /* border-radius: 4px;*/ /*}*/ &::v-deep(.ant-collapse-content) { border-top: unset; } } } .textarea-wrapper { position: relative; display: block; .m-textarea { padding: 8px 12px; height: 100%; } .m-count { color: #808080; background: #fff; position: absolute; font-size: 12px; bottom: 12px; right: 12px; line-height: 16px; } } .ant-spin-loading { width: 100vw; height: 100vh; display: block; background-color: rgba(0, 0, 0, 0.5); position: fixed; top: 0; left: 0; z-index: 99; } .apply-block { display: flex; align-items: center; .dynamic-delete-button { cursor: pointer; position: relative; font-size: 24px; color: #999; transition: all 0.3s; } .dynamic-delete-button:hover { color: #777; } .dynamic-delete-button[disabled] { cursor: not-allowed; opacity: 0.5; } div { flex: 1; } } .btn-group { display: flex; align-items: center; justify-content: right; width: 90%; margin: 0 auto 20px; button { margin-left: 10px; } } .ant-form-budget-border { border-bottom: 1px solid #d9d9d9; margin-left: -1px; } .budget-box { height: 100%; overflow: hidden; display: flex; flex: 1; flex-direction: column; } .budget-record { width: 100%; height: 100%; overflow: auto; display: flex; justify-content: center; background: #fff; } .budget-record-table { width: 90%; //调整页面表格宽度 // min-width: 840px; padding: 16px; } .budget-record-table-title { margin-bottom: 20px; text-align: center; white-space: pre-wrap; } .header_sd-header_common.ant-layout-header { padding: 0 20px 0 20px; color: #fff; background-image: linear-gradient(90deg, #1890ff 0%, #0162eb 100%); height: 64px; line-height: 64px; } .header_sd-header_common { z-index: 1; } .ant-layout-header { background: #001529; } .ant-layout-header, .ant-layout-footer { flex: 0 0 auto; } header { display: block; } .ant-layout, .ant-layout * { box-sizing: border-box; } .left_sd-header_common { float: left; height: 100%; } .right_sd-header_common { float: right; height: 100%; } .logo_sd-webflow_webflow h3 { color: #fff; } .header_sd-header_common .ant-btn-link { color: #fff; } .button_sd-webflow-button_webflow { max-width: 100%; } </style> 检查代码,针对分值上限设置的校验规则并未生效,请检查代码并修改
07-31
from fastapi import FastAPI, HTTPException, Request, WebSocketDisconnect from fastapi.responses import HTMLResponse, JSONResponse from fastapi.staticfiles import StaticFiles import uvicorn from server.generate.user_portrait_generate import * from server.data.sensitiveWord_server import * from server.chat_group.clientManager_server import * from server.ai_api.base.base_feiying_service import * from server.ai_api.base.base_doubao_service import * from typing import AsyncGenerator from contextlib import asynccontextmanager from server.generate.story_generate import * from fastapi.middleware.cors import CORSMiddleware # 在初始化属性之前的函数,用于定时任务的制作 @asynccontextmanager async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: # 在应用启动时执行的代码 # 启动定时生成 HTML 的任务 asyncio.create_task(all_periodic_html_generation()) asyncio.create_task(random_periodic_html_generation()) asyncio.create_task(all_treemap_html_generation()) asyncio.create_task(personality_update_story_generate_day()) # 应用启动后会在这里暂停,等待应用关闭 yield # 函数后面是在应用关闭时执行的代码 # region 基础属性初始化生成 # 客户端管理器 clients_manager = ClientsManager() # 消息存储,使用字典来存储群组的消息 message_store = [] # 全局知识图谱实例 girl_knowledge_graph = MemoryKnowledgeGraph() # 过滤词系统 filter_system = SensitiveWordFilter(serialized_path="sensitive_filter.pkl") # FastAPI # 创建 FastAPI 实例并传入 lifespan app = FastAPI(lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], # 允许所有来源 allow_credentials=True, allow_methods=["*"], # 允许所有方法 allow_headers=["*"], # 允许所有头 ) # 可信的记忆 cached_memories = set() app.mount("/static", StaticFiles(directory="static"), name="static") app.mount("/memory/using", StaticFiles(directory="memory/using"), name="memory_using") # 获取单独一次的girl_setting system_content = "" # 用于发送给UE的聊天历史 chat_history_to_ue = [] # 语音识别文本模型 audio_to_text_model_path = "model/vosk-model-small-cn-0.22/" # 音频保存位置 audio_save_file_path = "uploads/audio" ai_model_name = "" ai_api_key = "" deepseek_api = "" # 飞影设置 YOUR_TOKEN = "" VOICE_ID = "" AVATAR_ID = "" SHOW_Subtitle = "" num = 0 remember_me_update_state = 0 # 虚拟市民图像保存 virtual_cities_only_save_image_path = &#39;memory/using/virtual_image.png&#39; # 生成的讲故事视频的链接 remember_me_video_url = "" global_task_id = "" #的API key dou_bao_api_key = "" app_port = 10808 # 人设记录 current_user_profile = "" # 是否更新 全部 all_video_update_state = 0 # 人设更新系统 # personality_system = None # endregion def begin_init(): global system_content,ai_model_name,ai_api_key, deepseek_api, app_port # 声明使用全局变量 global YOUR_TOKEN, VOICE_ID, AVATAR_ID, SHOW_Subtitle, dou_bao_api_key # 人设加载 file_path = r"memory\GirlSetting.txt" girl_config_path = r"memory\Girl_config.txt" with open(girl_config_path, "r", encoding="utf-8") as f: for line in f: if "AI_MODEL =" in line: ai_model_name = line.split(&#39;"&#39;)[1] print(f"AI_MODEL: {ai_model_name}") elif "AI_API_KEY =" in line: ai_api_key = line.split(&#39;"&#39;)[1] print(f"AI_API_KEY: {ai_api_key}") elif "Deepseek_API =" in line: deepseek_api = line.split(&#39;"&#39;)[1] print(f"Deepseek_API: {deepseek_api}") elif "FeiYing_YOUR_TOKEN =" in line: YOUR_TOKEN = line.split(&#39;"&#39;)[1] print(f"FeiYing_Token: {YOUR_TOKEN}") elif "FeiYing_VOICE_ID =" in line: VOICE_ID = line.split(&#39;"&#39;)[1] print(f"FeiYing_Voice ID: {VOICE_ID}") elif "FeiYing_AVATAR_ID =" in line: AVATAR_ID = line.split(&#39;"&#39;)[1] print(f"Avatar ID: {AVATAR_ID}") elif "FeiYing_SHOW_Subtitle =" in line: SHOW_Subtitle = int(line.split("=")[1].strip()) print(f"FeiYing_SHOW_Subtitle: {SHOW_Subtitle}") elif "DouBao_API_KEY =" in line: dou_bao_api_key = line.split(&#39;"&#39;)[1] print(f"DouBao_API_KEY: {dou_bao_api_key}") elif "APP_Port =" in line: app_port = int(line.split("=")[1].strip()) print(f"APP_Port: {app_port}") with open(file_path, "r", encoding="utf-8") as file: system_content = file.read().strip() # 知识图谱记忆库加载 girl_knowledge_graph.load_graph("girl_knowledge_graph.graphml") # 初始化 begin_init() # 创建服务实例 service = GirlZhipuAIService(ai_api_key, system_content, ai_model_name) dou_bao_service = BaseDoubaoService(api_key=dou_bao_api_key) # API检测 def check_api_key(api_key: str): if api_key != "88888888": return HTTPException(status_code=401, detail="无效的API密钥") # 请求写法:http://127.0.0.1:8888/chat_without_memory?api_key=88888888 json:{message:xxx} @app.post("/chat_with_memory") async def chat_with_memory(request: Request, api_key: str = &#39;&#39;): check_api_key(api_key) data = await request.json() user_input = data.get("message", "") if not user_input: raise HTTPException(status_code=400, detail="请求内容不能为空") # 使用记忆库 temp_memory = girl_knowledge_graph.search_memory(user_input) retries = 0 max_retries = 3 while retries < max_retries: reply = service.chat_graph_knowledge(user_input, temp_memory, True) # 不修改记忆库 result = girl_knowledge_graph.parse_and_store_response(reply,False) if result: return {"response": result} retries += 1 return {"response": "emmm...我得仔细思考一下你说的问题,因为我现在脑子比较混乱"} @app.post("/chat_without_memory") async def chat_without_memory(request: Request, api_key: str = &#39;&#39;): check_api_key(api_key) data = await request.json() user_input = data.get("message", "") if not user_input: raise HTTPException(status_code=400, detail="请求内容不能为空") # 不使用记忆库 retries = 0 max_retries = 3 while retries < max_retries: # 不使用记忆库 reply = service.chat_graph_knowledge(user_input, "", False,False) result = girl_knowledge_graph.parse_and_store_response(reply,False) if result: return {"response": result} retries += 1 return {"response": "emmm...我得仔细思考一下你说的问题,因为我现在脑子比较混乱"} @app.get("/all_memory", response_class=HTMLResponse) async def show_girl_graph(request: Request): file_path = &#39;static/All_Girl_Graph.html&#39; if os.path.exists(file_path): # 如果文件存在,读取并返回文件内容 with open(file_path, &#39;r&#39;, encoding=&#39;utf-8&#39;) as file: content = file.read() return HTMLResponse(content=content) else: return "404" @app.get("/memory", response_class=HTMLResponse) async def show_girl_graph(request: Request): file_path = &#39;static/Random_Girl_Graph.html&#39; if os.path.exists(file_path): # 如果文件存在,读取并返回文件内容 with open(file_path, &#39;r&#39;, encoding=&#39;utf-8&#39;) as file: content = file.read() return HTMLResponse(content=content) else: return "404" @app.get("/remember_me_not_save_me/treemap_memory", response_class=HTMLResponse) async def show_girl_graph(request: Request): file_path = &#39;static/Treemap_Girl_Graph.html&#39; if os.path.exists(file_path): # 如果文件存在,读取并返回文件内容 with open(file_path, &#39;r&#39;, encoding=&#39;utf-8&#39;) as file: content = file.read() return HTMLResponse(content=content) else: return "404" @app.post("/get_app_history") async def get_app_history(): if chat_history_to_ue: # 获取历史记录 history = chat_history_to_ue.copy() # 清空历史记录 chat_history_to_ue.clear() return history else: return "" @app.post("/get_credible_memory") async def get_credible_memory(request: Request): # Unreal 的残留用代码 global cached_memories data = await request.body() temp_data = data.decode("utf-8") print(temp_data) # 从知识图谱中获取“认为可信的记忆” new_memories = girl_knowledge_graph.get_memories_connections("认为可信的记忆") # 转换为集合以便处理 new_memories_set = set(new_memories.split("|")) if temp_data == "First": print("First") if not new_memories_set: return "" return "|".join(new_memories_set) else: print("NoFirst") # 计算新增的记忆 unique_memories = new_memories_set - cached_memories # 更新已缓存的记忆 cached_memories.update(unique_memories) if not unique_memories: return "" return "|".join(unique_memories) @app.post("/remember_me_not_save_me/get_user_portrait") async def get_user_portrait(): base_profile_path = "memory/using/user_portrait.txt" with open(base_profile_path, "r", encoding="utf-8") as f: content= f.read() print(content) return content @app.get("/remember_me_not_save_me/get_video_update_state") async def get_video_update_state(): global remember_me_update_state,YOUR_TOKEN,global_task_id,remember_me_video_url, all_video_update_state video_url = check_video_status(YOUR_TOKEN, global_task_id) if video_url in ["1", "2", "4"]: return JSONResponse({ "update": all_video_update_state, "generate": 0 }) else: remember_me_video_url = video_url remember_me_update_state = 1 timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") record = f"{timestamp}:{video_url}\n" # 追加写入到文件(如果文件不存在会自动创建) with open("memory/video_link_records.txt", "a", encoding="utf-8") as file: file.write(record) return JSONResponse({ "update": all_video_update_state, "generate": remember_me_update_state }) @app.get("/remember_me_not_save_me/get_video_link") async def get_video_url(): global remember_me_video_url,remember_me_update_state, all_video_update_state remember_me_update_state = 0 all_video_update_state = 0 return remember_me_video_url @app.websocket("/connect_server") async def websocket_endpoint(websocket: WebSocket): # 接受 WebSocket 连接 await websocket.accept() # 记录客户端连接 await clients_manager.connect(websocket) try: while True: # 等待客户端发送消息 message = await websocket.receive_text() # 判断消息类型:如果是 Base64 编码的图片 if message.startswith("IMAGE_MESSAGE:"): # 处理 Base64 图片 image_data = message[len("IMAGE_MESSAGE:"):].strip() image_path = await save_image_from_base64(image_data) # 将保存的图片路径广播给其他客户端 await clients_manager.broadcast(f"Image received: {image_path}", sender=websocket, all_return=False) append_to_log("User", image_path) asyncio.create_task(add_memory_image(image_path)) elif message.startswith("USER_MESSAGE:"): # 广播消息给除了自己以外的所有客户端 user_message = message[len("USER_MESSAGE:"):].strip() user_message = filter_system.process_text(user_message) await clients_manager.broadcast(user_message, sender=websocket, all_return=False) message_store.append({"message": user_message, "sender": "user"}) # 写入本地 append_to_log("User", user_message) asyncio.create_task(add_memory_str(user_message)) else: append_to_log("System", "Unknown message type received") # 启动一个定时任务来检查消息是否已经超过 5 秒 asyncio.create_task(check_message_timeout()) except WebSocketDisconnect: # 如果客户端断开连接,则从管理器中删除该连接 await clients_manager.disconnect(websocket) async def all_periodic_html_generation(): while True: # 生成 HTML girl_knowledge_graph.generate_graph_dynamic_html(&#39;static/All_Girl_Graph.html&#39;) # 去重 - 0.8 girl_knowledge_graph.remove_similar_nodes_distance(0.8) # 每60秒执行一次 await asyncio.sleep(300) async def random_periodic_html_generation(): while True: # 生成 HTML girl_knowledge_graph.random_Generate_dynamic_html(&#39;static/Random_Girl_Graph.html&#39;) # 每20秒执行一次 await asyncio.sleep(20) async def all_treemap_html_generation(): while True: # 生成 HTML result = girl_knowledge_graph.get_recent_memories(days=500) categorized = girl_knowledge_graph.categorize_memories_by_type(result) girl_knowledge_graph.generate_fullscreen_treemap_html(categorized, &#39;static/Treemap_Girl_Graph.html&#39;) #10分钟执行一次 await asyncio.sleep(600) async def personality_update_story_generate_day(): # 一整个更新视频流程 global virtual_cities_only_save_image_path, num, current_user_profile, all_video_update_state global YOUR_TOKEN, VOICE_ID, AVATAR_ID, SHOW_Subtitle global global_task_id, deepseek_api if num !=0: # 更新人设 current_girl_system_content = service.system_girl # 记录历史 --> 用于回滚过去人设 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") save_file_path = f"memory/personality_history/GirlSetting_{timestamp}.txt" os.makedirs(os.path.dirname(save_file_path), exist_ok=True) write_to_txt(save_file_path,current_girl_system_content) # 生成新的人设 并 修改 recent_memory = girl_knowledge_graph.get_recent_memories(days=1) new_setting = service.chat_update_girl_setting(current_girl_system_content,recent_memory) write_to_txt("memory/GirlSetting.txt",new_setting) service.system_girl = new_setting print("新人设生成修改完毕") # 基于新的记忆生成新的用户画像-->"memory/using/user_portrait.txt" new_profile = merge_profiles_with_deepseek(service=service, deepseek_api=deepseek_api, days=0.5, log_path="log.txt", new_setting=new_setting, recent_user_profile=current_user_profile) current_user_profile = new_profile # 生成新的画图提示词 image_prompt = "基于这个用户画像与人设去生成一个专门用于绘画的json提示词,画一个大家心目中的人物,正面人脸,这个是用户画像,超写实:" + new_profile + "\n人设:" + new_setting user_portrait_prompt = service.chat(image_prompt) # 画图提示词-->生成图 """ # chatglm画图 image_url = service.generate_image( prompt=user_portrait_prompt, model="cogview-3-flash", size="1024x1024", watermark_enabled=False # 去水印 )""" image_url = dou_bao_service.generate_image_and_get_url( prompt=user_portrait_prompt, watermark=False ) # 目录存在 os.makedirs("memory/virtual_image", exist_ok=True) # 保存位置 image_timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") time_save_path = f&#39;memory/virtual_image/VI_{image_timestamp}.png&#39; response = requests.get(image_url, stream=True) # 下载 + 保存图片 image_data = b"" for chunk in response.iter_content(chunk_size=8192): image_data += chunk with open(virtual_cities_only_save_image_path, &#39;wb&#39;) as file: file.write(image_data) # 保存到带时间戳的路径(历史记录) with open(time_save_path, &#39;wb&#39;) as file: file.write(image_data) # 故事生成 -> "memory/using/merged_stories.txt" final_merged_story = await merge_stories_with_deepseek(deepseek_api=deepseek_api, service=service,open_log_path="log.txt", knowledge_graph=girl_knowledge_graph, recent_user_profile= current_user_profile) # 将图片转为视频 # generate_video_from_image(used_image_path, "static/video/loop_video.mp4", 5) # 创建视频生成任务 text_prompt = "电影感特写镜头,固定机位。人物凝视镜头,缓慢地眨一次眼,随后做一个微小的自然动作(例如:嘴角浅笑),然后恢复初始的平静表情。动态微妙,镜头稳定,风格化画面,无缝循环。" # 虚拟形象图片 image_url = "http://150.158.170.63:" + str(app_port) + "/" + virtual_cities_only_save_image_path video_task = dou_bao_service.create_video_generation_task( text_prompt=text_prompt, image_url=image_url, watermark=False ) task_id = video_task.get("id") if task_id: # 等待任务完成 video_url, status = dou_bao_service.wait_for_video_task(task_id) if status == "succeeded": print("视频生成成功,URL:", video_url) timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") video_record = f"{timestamp}:{video_url}" os.makedirs(os.path.dirname(save_file_path), exist_ok=True) with open("memory/dou_bao_no_voice_video.txt", "a", encoding="utf-8") as file: file.write(video_record) # 下载并覆盖视频 await dou_bao_service.download_file(video_url, "static/video/loop_video.mp4") # 上传飞影视频 avatar_id = create_avatar_from_local_video_back_avatar_id(token=YOUR_TOKEN, video_path="static/video/loop_video.mp4", title="test") timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") record = f"{timestamp}:{avatar_id}\n" # 追加写入到文件(如果文件不存在会自动创建) with open("memory/avatar_id_records.txt", "a", encoding="utf-8") as file: file.write(record) result = create_feiying_video_by_tts( token=YOUR_TOKEN, voice_id=VOICE_ID, avatar_id=avatar_id, text=final_merged_story, show_subtitle=SHOW_Subtitle ) global_task_id = result.get("task_id") timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") record = f"{timestamp}:https://hfw-api.hifly.cc/api/v2/hifly/video/task?task_id={global_task_id}\n" # 追加写入到文件(如果文件不存在会自动创建) with open("memory/video_task_records.txt", "a", encoding="utf-8") as file: file.write(record) all_video_update_state = 1 elif num == 0: num = 1 #1个小时生成一次新的人设表、故事、用户画像、飞影视频 await asyncio.sleep(3600) # 定时检查是否有新的消息,如果没有消息,则调用 AI 生成回复 async def check_message_timeout(): # 等待 5 秒钟 await asyncio.sleep(5) # 检查当前消息列表 if len(message_store) > 0: # 随机选择一条消息 selected_message = random.choice(message_store) question = selected_message["message"] # 调用 AI 获取回复 ai_response = await ai_chat(question) append_to_log("AI", ai_response) # 广播消息给所有客户端 await clients_manager.broadcast(ai_response, sender="ai", all_return=True) chat_history_to_ue.append({"question": question, "answer": ai_response}) # 清空消息存储 message_store.clear() async def ai_chat(player_message: str): # 检索回忆 temp_memory = girl_knowledge_graph.search_memory(player_message) retries = 0 max_retries = 3 while retries < max_retries: # 请求聊天生成的回答 reply = service.chat_graph_knowledge(player_message, temp_memory, True) # 解析并存储响应 result = girl_knowledge_graph.parse_and_store_response(reply) if result: # 如果解析成功,返回结果 return result else: retries += 1 if retries>= max_retries: return """emmm...我得仔细思考一下你说的问题,因为我现在脑子比较混乱""" async def add_memory_str(new_memory: str): memory_text = service.chat_graph_knowledge(new_memory, "", False) girl_knowledge_graph.parse_and_store_response(memory_text) async def add_memory_image(file_path: str): res = service.chat_image_solve(file_path) answer = """这个是别人给你发来照片后,你自己对照片的描述:""" + res + """\n请你根据人设和相关回忆设定回答""" message_store.append({"message": answer, "sender": "user"}) # 存入记忆 memory_text = service.chat_graph_knowledge(res, "", False) girl_knowledge_graph.parse_and_store_response(memory_text) async def audio_get(): wav_path = await asyncio.to_thread(record_audio_manual_stop, "output/audio_files") return wav_path async def audio_to_text(audio_file_path: str): audio_text = transcribe_audio(audio_to_text_model_path,audio_file_path=audio_file_path) return audio_text if __name__ == &#39;__main__&#39;: # a = asyncio.run(audio_to_text("C:\\Users\\ADMIN\\Desktop\\声音\\小黑塔\\小黑塔的初次见面语音.wav")) # print(a) # merge_stories_with_deepseek() # 用于自己使用 # asyncio.run(personality_update_story_generate_day()) # 人设重新设置和故事生成 uvicorn.run(app, host=&#39;0.0.0.0&#39;, port=app_port, log_level="info") # asyncio.run(t_2()) 打后的python文件,打开报错: Error in sys.excepthook: Original exception was: Error in sys.excepthook! Original exception was:
10-09
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值