
如图;国内通过调用openai接口进行互动,实现图文互动/文本互动
注意:请求人数较多,需要等待
⚠️ 不再推荐使用本接口,后面将废弃。
接口地址 (POST请求)
请求参数
| 参数名 | 类型 | 长度 | 必须 | 备注 |
|---|---|---|---|---|
| apiKey | String | 64 | 是 | OpenAI 的 ApiKey |
| sessionId | String | 64 | 是 | 会话ID,关联上下文,推荐使用UUID作为sessionId |
| content | String | 1000 | 是 | 发送的内容 |
请求示例(Content-Type = application/json)
{
"apiKey": "<your_openai_api_key>",
"sessionId": "8d1cb9b0-d535-43a8-b738-4f61b1608579",
"content": "你是谁?"
}
响应示例
{
"code": 200,
"message": "执行成功",
"data": "我是一名人工智能助手,用于协助回答问题和提供建议。您可以通过与我进行对话来获取您需要的信息或帮助。"
}
本接口使用的是 gpt-3.5-turbo 模型,支持通过上下文内容进行连续对话。
本接口对官方的接口进行了封装,开发者只需为每个 ApiKey 下的每个会话分配一个独立的 sessionId 即可实现连续对话 。
推荐使用 uuid 作为 sessionId 以保证全局唯一 ,否则对话可能会“串线”。
对话中的上下文信息有效期为30分钟,过期后再次发送消息无法关联上下文。
# 测试聊天
curl https://api.openai.com/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your_openai_api_key>" \
-d '{
"model": "gpt-3.5-turbo",
"messages": [{"role": "user", "content": "Hello!"}]
}'
# 响应结果
{
"id": "chatcmpl-21lvNzPaxlsQJh0BEIb9DqoO0pZUY",
"object": "chat.completion",
"created": 1680656905,
"model": "gpt-3.5-turbo-0301",
"usage": {
"prompt_tokens": 10,
"completion_tokens": 10,
"total_tokens": 20
},
"choices": [
{
"message": {
"role": "assistant",
"content": "Hello there! How can I assist you today?"
},
"finish_reason": "stop",
"index": 0
}
]
}
# 测试生成图片
curl https://api.openai.com/v1/images/generations \
-H "Content-Type: application/json" \
-H "Authorization: Bearer <your_openai_api_key>" \
-d '{
"prompt": "A bikini girl",
"n": 2,
"size": "512x512"
}'
# 响应结果
{
"created": 1680705608,
"data": [
{
"url": "https://oaidalleapiprodscus.blob.core.windows.net/private/org-xxxxxxx"
},
{
"url": "https://oaidalleapiprodscus.blob.core.windows.net/private/org-xxxxxxx"
}
]
}
OpenAI本身是没有记忆的,如果你不告诉他你之前说了什么以及他之前回答了什么,那么他只会根据你最近一次发送的内容进行回答。
所以,要想实现“连续对话”,每次发送消息时,你需要将你之前发送的内容(user)以及OpenAI之前返回的内容(assistant),再结合你本次想发送的内容(user) 按 时序 组合成一个 messages[] 数组,然后再将这个数组发送给OpenAI就行了,就是这么简单。
如果你用的是第三方开发者开发的基于OpenAI 的应用可以参考以下:
接口地址 (GET请求)
GET https://api.openai.com/pro/balance?apiKey=sk-xxxxxxxxxxxxxx
请求参数
| 参数名 | 类型 | 长度 | 必须 | 备注 |
|---|---|---|---|---|
| apiKey | String | 64 | 是 | OpenAI 的 ApiKey |
响应示例
{
"code": 200,
"message": "执行成功",
"data": {
"total": 18.00,
"balance": 17.92,
"used": 0.08
}
}
原OpenAI官方后台查询余额的接口由于被用户滥用,官方给撤销了,现在提供一个折中的方式去计算账户余额。
逻辑是先得到OpenAI给你账户授权的总金额,然后减去最近100天你账户消耗的金额,得到的 balance 即为账户可用余额。
<template>
<div class="chat-window">
<div class="top">
<div class="head-pic">
<HeadPortrait :imgUrl="frinedInfo.headImg"></HeadPortrait>
</div>
<div class="info-detail">
<div class="name">{{ frinedInfo.name }}</div>
<div class="detail">{{ frinedInfo.detail }}</div>
</div>
<div class="other-fun">
<span class="iconfont icon-shipin" @click="video"> </span>
<span class="iconfont icon-gf-telephone" @click="telephone"></span>
<label for="docFile">
<span class="iconfont icon-wenjian"></span>
</label>
<label for="imgFile">
<span class="iconfont icon-tupian"></span>
</label>
<input
type="file"
name=""
id="imgFile"
@change="sendImg"
accept="image/*"
/>
<input
type="file"
name=""
id="docFile"
@change="sendFile"
accept="application/*,text/*"
/>
<!-- accept="application/*" -->
</div>
</div>
<div class="botoom">
<div class="chat-content" ref="chatContent">
<div
class="chat-wrapper"
v-for="(item, index) in chatList"
:key="item.id"
>
<!-- <div v-if="isSend && index == chatList.length - 1">
<div class="chat-friend" v-if="item.uid !== '1001'">
<div class="info-time">
<img :src="item.headImg" alt="" />
<span>{{ item.name }}</span>
<span>{{ item.time }}</span>
</div>
<div class="chat-text" v-if="item.chatType == 0">
<span class="flash_cursor"></span>
</div>
</div>
</div>-->
<div class="chat-friend" v-if="item.uid !== '1001'">
<div class="info-time">
<img :src="item.headImg" alt="" />
<span>{{ item.name }}</span>
<span>{{ item.time }}</span>
</div>
<div class="chat-text" v-if="item.chatType == 0&&item.type != 1">
<template v-if="isSend && index == chatList.length - 1">
<span class="flash_cursor">请稍后...</span>
</template>
<template v-else>
<pre>{{ item.msg }}</pre>
</template>
</div>
<div class="chat-img" v-if="item.chatType == 1">
<img
:src="item.msg"
alt="表情"
v-if="item.extend.imgType == 1"
style="width: 100px; height: 100px"
/>
<el-image :src="item.msg" :preview-src-list="srcImgList" v-else>
</el-image>
</div>
<div class="chat-img" v-if="item.type == 1">
<img
v-for="(items,index) in item.images"
:src="items"
:key="index"
alt="表情"
style="width: 100px; height: 100px"
/>
</div>
<div class="chat-img" v-if="item.chatType == 2">
<div class="word-file">
<FileCard
:fileType="item.extend.fileType"
:file="item.msg"
></FileCard>
</div>
</div>
</div>
<div class="chat-me" v-else>
<div class="info-time">
<span>{{ item.name }}</span>
<span>{{ item.time }}</span>
<img :src="item.headImg" alt="" />
</div>
<div class="chat-text" v-if="item.chatType == 0">
{{ item.msg }}
</div>
<div class="chat-img" v-if="item.chatType == 1">
<img
:src="item.msg"
alt="表情"
v-if="item.extend.imgType == 1"
style="width: 100px; height: 100px"
/>
<el-image
style="max-width: 300px; border-radius: 10px"
:src="item.msg"
:preview-src-list="srcImgList"
v-else
>
</el-image>
</div>
<div class="chat-img" v-if="item.chatType == 2">
<div class="word-file">
<FileCard
:fileType="item.extend.fileType"
:file="item.msg"
></FileCard>
</div>
</div>
</div>
</div>
</div>
<div class="chatInputs">
<!--
v-show="showEmoji"
-->
<!-- <div class="emoji boxinput" @click="clickEmoji">
<img src="@/assets/img/emoji/smiling-face.png" alt="" />
</div> -->
<div class="emoji-content">
<Emoji
@sendEmoji="sendEmoji"
v-show="showEmoji"
@closeEmoji="clickEmoji"
></Emoji>
</div>
<input class="inputs" v-model="inputMsg" @keyup.enter="sendText" />
<el-button class="send boxinput" :disabled="isSend" @click="sendText">
<img src="@/assets/img/emoji/rocket.png" alt="" />
</el-button>
</div>
</div>
</div>
</template>
<script>
import { animation } from "@/util/util";
import { getChatMsg, chatgpt } from "@/api/getData";
import HeadPortrait from "@/components/HeadPortrait";
import Emoji from "@/components/Emoji";
import FileCard from "@/components/FileCard.vue";
export default {
components: {
HeadPortrait,
Emoji,
FileCard,
},
props: {
frinedInfo: Object,
default() {
return {};
},
},
watch: {
frinedInfo() {
this.getFriendChatMsg();
},
},
data() {
return {
chatList: [],
inputMsg: "",
showEmoji: false,
friendInfo: {},
srcImgList: [],
isSend: false,
};
},
mounted() {
this.getFriendChatMsg();
},
methods: {
//获取聊天记录
getFriendChatMsg() {
let params = {
frinedId: this.frinedInfo.id,
};
getChatMsg(params).then((res) => {
console.error("333", res);
this.chatList = res;
// this.chatList.forEach((item) => {
// if (item.chatType == 2 && item.extend.imgType == 2) {
// this.srcImgList.push(item.msg);
// }
// });
this.scrollBottom();
});
},
//发送信息
sendMsg(msgList) {
this.chatList.push(msgList);
this.scrollBottom();
},
//获取窗口高度并滚动至最底层
scrollBottom() {
this.$nextTick(() => {
const scrollDom = this.$refs.chatContent;
animation(scrollDom, scrollDom.scrollHeight - scrollDom.offsetHeight);
});
},
//关闭标签框
clickEmoji() {
this.showEmoji = !this.showEmoji;
},
//发送文字信息
sendText() {
if (this.inputMsg) {
let chatMsg = {
headImg: require("@/assets/img/head_portrait.jpg"),
name: "",
time: new Date().toLocaleTimeString(),
msg: this.inputMsg,
chatType: 0, //信息类型,0文字,1图片
uid: "1001", //uid
};
this.sendMsg(chatMsg);
this.$emit("personCardSort", this.frinedInfo.id);
this.inputMsg = "";
let data = {
// prompt: chatMsg.msg,
// temperature: 1,
// top_p: 1,
// // model: "text-davinci-003",
// max_tokens: 2048,
// frequency_penalty: 0,
// "model": "gpt-3.5-turbo",
// "messages": [{"role": "user", "content": "Hello!"}],
// presence_penalty: 0,
// stop: ["Human:", "AI:"],
model: "gpt-3.5-turbo",
messages: [{ role: "user", content: chatMsg.msg }],
};
this.loading = true;
this.isSend = true;
let chatGPT = {
headImg: require("@/assets/img/head_portrait1.jpg"),
name: "小五",
time: new Date().toLocaleTimeString(),
msg: "",
images: [],
chatType: 0, //信息类型,0文字,1图片
uid: "1002", //uid
};
this.sendMsg(chatGPT);
chatgpt(data).then((res) => {
this.isSend = false;
if (res.data) {
this.chatList[this.chatList.length - 1].images=[];
console.error("555", this.chatList);
this.chatList[this.chatList.length - 1].type = 1;
this.chatList[this.chatList.length - 1].images.push(res.data[0].url);
} else {
this.chatList[this.chatList.length - 1].msg =
res.choices[0].message.content;
}
});
} else {
this.$message({
message: "消息不能为空哦~",
type: "warning",
});
}
},
//发送表情
sendEmoji(msg) {
let chatMsg = {
headImg: require("@/assets/img/head_portrait.jpg"),
name: "大毛是小白",
time: new Date().toLocaleTimeString(),
msg: msg,
chatType: 1, //信息类型,0文字,1图片
extend: {
imgType: 1, //(1表情,2本地图片)
},
uid: "1001",
};
this.sendMsg(chatMsg);
this.clickEmoji();
},
//发送本地图片
sendImg(e) {
let _this = this;
console.log(e.target.files);
let chatMsg = {
headImg: require("@/assets/img/head_portrait.jpg"),
name: "大毛是小白",
time: "09:12 AM",
msg: "",
chatType: 1, //信息类型,0文字,1图片, 2文件
extend: {
imgType: 2, //(1表情,2本地图片)
},
uid: "1001",
};
let files = e.target.files[0]; //图片文件名
if (!e || !window.FileReader) return; // 看是否支持FileReader
let reader = new FileReader();
reader.readAsDataURL(files); // 关键一步,在这里转换的
reader.onloadend = function () {
chatMsg.msg = this.result; //赋值
_this.srcImgList.push(chatMsg.msg);
};
this.sendMsg(chatMsg);
e.target.files = null;
},
//发送文件
sendFile(e) {
let chatMsg = {
headImg: require("@/assets/img/head_portrait.jpg"),
name: "大毛是小白",
time: "09:12 AM",
msg: "",
chatType: 2, //信息类型,0文字,1图片, 2文件
extend: {
fileType: "", //(1word,2excel,3ppt,4pdf,5zpi, 6txt)
},
uid: "1001",
};
let files = e.target.files[0]; //图片文件名
chatMsg.msg = files;
console.log(files);
if (files) {
switch (files.type) {
case "application/msword":
case "application/vnd.openxmlformats-officedocument.wordprocessingml.document":
chatMsg.extend.fileType = 1;
break;
case "application/vnd.ms-excel":
case "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":
chatMsg.extend.fileType = 2;
break;
case "application/vnd.ms-powerpoint":
case "application/vnd.openxmlformats-officedocument.presentationml.presentation":
chatMsg.extend.fileType = 3;
break;
case "application/pdf":
chatMsg.extend.fileType = 4;
break;
case "application/zip":
case "application/x-zip-compressed":
chatMsg.extend.fileType = 5;
break;
case "text/plain":
chatMsg.extend.fileType = 6;
break;
default:
chatMsg.extend.fileType = 0;
}
this.sendMsg(chatMsg);
e.target.files = null;
}
},
// 发送语音
telephone() {
this.$message("敬请期待一下吧~🥳");
},
//发送视频
video() {
this.$message("敬请期待一下吧~🥳");
},
},
};
</script>
<style lang="scss" scoped>
.flash_cursor {
width: auto;
height: 30px;
display: inline-block;
background: #d6e3f5;
opacity: 1;
animation: glow 800ms ease-out infinite alternate;
}
@keyframes glow {
0% {
opacity: 1;
}
25% {
opacity: 0.5;
}
50% {
opacity: 0;
}
75% {
opacity: 0.5;
}
100% {
opacity: 1;
}
}
.chat-window {
width: 100%;
height: 100%;
margin: 0 auto;
position: relative;
.top {
margin-bottom: 20px;
&::after {
content: "";
display: block;
clear: both;
}
.head-pic {
float: left;
}
.info-detail {
float: left;
margin: 5px 20px 0;
.name {
font-size: 20px;
font-weight: 600;
color: #fff;
}
.detail {
color: #9e9e9e;
font-size: 12px;
margin-top: 2px;
}
}
.other-fun {
float: right;
margin-top: 20px;
span {
margin-left: 30px;
cursor: pointer;
}
// .icon-tupian {
// }
input {
display: none;
}
}
}
.botoom {
width: 100%;
height: 70vh;
background-color: rgb(50, 54, 68);
border-radius: 20px;
padding: 20px;
box-sizing: border-box;
position: relative;
.chat-content {
width: 100%;
height: 85%;
overflow-y: scroll;
padding: 20px;
box-sizing: border-box;
&::-webkit-scrollbar {
width: 0;
/* Safari,Chrome 隐藏滚动条 */
height: 0;
/* Safari,Chrome 隐藏滚动条 */
display: none;
/* 移动端、pad 上Safari,Chrome,隐藏滚动条 */
}
.chat-wrapper {
position: relative;
word-break: break-all;
.chat-friend {
width: 100%;
float: left;
margin-bottom: 20px;
display: flex;
flex-direction: column;
justify-content: flex-start;
align-items: flex-start;
.chat-text {
max-width: 90%;
padding: 20px;
border-radius: 20px 20px 20px 5px;
background-color: rgb(56, 60, 75);
color: #fff;
&:hover {
background-color: rgb(39, 42, 55);
}
pre {
white-space: break-spaces;
}
}
.chat-img {
img {
width: 100px;
height: 100px;
}
}
.info-time {
margin: 10px 0;
color: #fff;
font-size: 14px;
img {
width: 30px;
height: 30px;
border-radius: 50%;
vertical-align: middle;
margin-right: 10px;
}
span:last-child {
color: rgb(101, 104, 115);
margin-left: 10px;
vertical-align: middle;
}
}
}
.chat-me {
width: 100%;
float: right;
margin-bottom: 20px;
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-end;
align-items: flex-end;
.chat-text {
float: right;
max-width: 90%;
padding: 20px;
border-radius: 20px 20px 5px 20px;
background-color: rgb(29, 144, 245);
color: #fff;
&:hover {
background-color: rgb(26, 129, 219);
}
}
.chat-img {
img {
max-width: 300px;
max-height: 200px;
border-radius: 10px;
}
}
.info-time {
margin: 10px 0;
color: #fff;
font-size: 14px;
display: flex;
justify-content: flex-end;
img {
width: 30px;
height: 30px;
border-radius: 50%;
vertical-align: middle;
margin-left: 10px;
}
span {
line-height: 30px;
}
span:first-child {
color: rgb(101, 104, 115);
margin-right: 10px;
vertical-align: middle;
}
}
}
}
}
.chatInputs {
width: 100%;
position: absolute;
bottom: 0;
margin: 0 auto;
align-items: center;
display: flex;
padding-bottom: 15px;
.boxinput {
width: 50px;
height: 50px;
background-color: rgb(66, 70, 86);
border-radius: 15px;
border: 1px solid rgb(80, 85, 103);
position: relative;
cursor: pointer;
img {
width: 30px;
height: 30px;
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
}
.emoji {
transition: 0.3s;
&:hover {
background-color: rgb(46, 49, 61);
border: 1px solid rgb(71, 73, 82);
}
}
.inputs {
width: 70%;
height: 50px;
background-color: rgb(66, 70, 86);
border-radius: 15px;
border: 2px solid rgb(34, 135, 225);
padding: 10px;
box-sizing: border-box;
transition: 0.2s;
margin-right: 10px;
font-size: 20px;
color: #fff;
font-weight: 100;
&:focus {
outline: none;
}
}
.send {
background-color: rgb(29, 144, 245);
border: 0;
transition: 0.3s;
box-shadow: 0px 0px 5px 0px rgba(0, 136, 255);
&:hover {
box-shadow: 0px 0px 10px 0px rgba(0, 136, 255);
}
}
}
}
}
</style>
我想将html转换为纯文本。不过,我不想只删除标签,我想智能地保留尽可能多的格式。为插入换行符标签,检测段落并格式化它们等。输入非常简单,通常是格式良好的html(不是整个文档,只是一堆内容,通常没有anchor或图像)。我可以将几个正则表达式放在一起,让我达到80%,但我认为可能有一些现有的解决方案更智能。 最佳答案 首先,不要尝试为此使用正则表达式。很有可能你会想出一个脆弱/脆弱的解决方案,它会随着HTML的变化而崩溃,或者很难管理和维护。您可以使用Nokogiri快速解析HTML并提取文本:require'nokogiri'h
我是Rails的新手,所以请原谅简单的问题。我正在为一家公司创建一个网站。那家公司想在网站上展示它的客户。我想让客户自己管理这个。我正在为“客户”生成一个表格,我想要的三列是:公司名称、公司描述和Logo。对于名称,我使用的是name:string但不确定如何在脚本/生成脚手架终端命令中最好地创建描述列(因为我打算将其设置为文本区域)和图片。我怀疑描述(我想成为一个文本区域)应该仍然是描述:字符串,然后以实际形式进行调整。不确定如何处理图片字段。那么……说来话长:我在脚手架命令中输入什么来生成描述和图片列? 最佳答案 对于“文本”数
英文版英文链接关注公众号在“亚特兰蒂斯的回声”中踏上一段难忘的冒险之旅,深入未知的海洋深处。足智多谋的考古学家AriaSeaborne偶然发现了一件古代神器,揭示了一张通往失落之城亚特兰蒂斯的隐藏地图。在她神秘的导师内森·兰登教授的指导和勇敢的冒险家亚历克斯·默瑟的帮助下,阿丽亚开始了一段危险的旅程,以揭开这座传说中城市的真相。他们的冒险之旅带领他们穿越险恶的大海、神秘的岛屿和充满陷阱和谜语的致命迷宫。随着Aria潜在的魔法能力的觉醒,她被睿智勇敢的QueenNeria的幻象所指引,她让她为即将到来的挑战做好准备。三人组揭开亚特兰蒂斯令人惊叹的隐藏文明,并了解到邪恶的巫师马拉卡勋爵试图利用其古
项目介绍随着我国经济迅速发展,人们对手机的需求越来越大,各种手机软件也都在被广泛应用,但是对于手机进行数据信息管理,对于手机的各种软件也是备受用户的喜爱小学生兴趣延时班预约小程序的设计与开发被用户普遍使用,为方便用户能够可以随时进行小学生兴趣延时班预约小程序的设计与开发的数据信息管理,特开发了小程序的设计与开发的管理系统。小学生兴趣延时班预约小程序的设计与开发的开发利用现有的成熟技术参考,以源代码为模板,分析功能调整与小学生兴趣延时班预约小程序的设计与开发的实际需求相结合,讨论了小学生兴趣延时班预约小程序的设计与开发的使用。开发环境开发说明:前端使用微信微信小程序开发工具:后端使用ssm:VU
我有这个代码:context"Visitingtheusers#indexpage."dobefore(:each){visitusers_path}subject{page}pending('iii'){shouldhave_no_css('table#users')}pending{shouldhavecontent('Youhavereachedthispageduetoapermissionic错误')}它会导致几个待处理,例如ManagingUsersGivenapractitionerloggedin.Visitingtheusers#indexpage.#Noreason
我一直在玩一个脚本,它在Chrome中获取选定的文本并在Google中查找它,提供四个最佳选择,然后粘贴相关链接。它以不同的格式粘贴,具体取决于当前在Chrome中打开的页面-DokuWiki打开的DokuWiki格式,普通网站的HTML,我想要我的WordPress所见即所得编辑器的富文本。我尝试使用pbpaste-Preferrtf来查看没有其他样式的富文本链接在粘贴板上的样子,但它仍然输出纯文本。在文本编辑中保存文件并进行试验后,我想出了以下内容text=%q|{\rtf1{\field{\*\fldinst{HYPERLINK"URL"}}{\fldrsltTEXT}}}|te
我正在尝试找出一种方法来显示来自不在RAILS_ROOT下(在RedHat或Ubuntu环境中)的已安装文件系统的图像。我不想使用符号链接(symboliclink),因为这个应用程序实际上是通过Tomcat部署的,而当我关闭Tomcat时,Tomcat会尝试跟随符号链接(symboliclink)并删除挂载中的所有图像。由于这些文件的数量和大小,将图像放在public/images下也不是一种选择。我查看了send_file,但它只会显示一张图片。我需要在一个格式良好的页面中显示6个请求的图像。由于膨胀,我宁愿不使用Base64编码,但我不知道如何将图像数据与呈现的页面一起传递下去。
我使用“newapp_name”创建了一个新的Rails应用程序,我正在尝试编辑.gitignore文件,但在我的应用程序文件夹中找不到它。我在哪里可以找到它?我安装了Git。 最佳答案 .gitignore位于项目的root中,而不是app子目录中。首先打开终端并进入您的目录。您需要使用ls-a来显示stash文件。然后使用打开.gitignore 关于ruby-on-rails-尝试打开.gitignore以在文本编辑器中对其进行编辑,但在OSXMountainLion上找不到文件位
我想获取任意的ASCII文本字符串,例如“Helloworld”,并将其压缩为字符数较少(尽可能少)的版本,但要采用可以解压缩的方式。压缩版本应仅由ascii字符组成。有没有一种方法可以做到这一点,尤其是在Ruby中? 最佳答案 如果知道只会使用ASCII字符,那就是每个字节的低7位。通过位操作,您可以将每8个字节混合成7个字节(节省12.5%)。如果您可以将其放入更小的范围(仅限64个有效字符),则可以删除另一个字节。但是,因为您希望压缩形式也只包含ASCII字符,所以会丢失一个字节-除非您的输入可以限制为64个字符(例如,有损压
多年来,我在各种网站上遇到过各种问题,用户在字符串和文本字段的开头/结尾放置空格。有时这些会导致格式/布局问题,有时会导致搜索问题(即搜索顺序看起来不对,但实际上并非如此),有时它们实际上会使应用程序崩溃。我认为这会很有用,而不是像我过去所做的那样放入一堆before_save回调,向ActiveRecord添加一些功能以在保存之前自动调用任何字符串/文本字段上的.strip,除非我告诉它不是,例如do_not_strip:field_x,:field_y或类定义顶部的类似内容。在我去弄清楚如何做到这一点之前,有没有人看到更好的解决方案?明确一点,我已经知道我可以做到这一点:befor