mirror of
https://github.com/Kakune55/PyGetGPT.git
synced 2025-05-06 10:19:25 +08:00
新建存储库 PyGetGPT正式发行版
This commit is contained in:
commit
2b8f18157d
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
*.json
|
21
LICENSE
Normal file
21
LICENSE
Normal file
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Kakune55
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
3
README.md
Normal file
3
README.md
Normal file
@ -0,0 +1,3 @@
|
||||
# PyGetGPT
|
||||
使用python构建的简易语言模型api调用系统
|
||||
使用web页面作为GUI易于部署
|
7
doc/clientConfig.md
Normal file
7
doc/clientConfig.md
Normal file
@ -0,0 +1,7 @@
|
||||
# 配置文件说明
|
||||
{"url": "<此处为api URL>", "userkey": "<此处为你的UserKey>", "context": <此处为1 开启上下位关联功能 默认0关闭>}
|
||||
注意:开启上下文关联会消耗大量的token
|
||||
# 什么是Token
|
||||
在大型语言模型中,"token"是指文本中的一个最小单位。 通常,一个token可以是一个单词、一个标点符号、一个数字、一个符号等。 在自然语言处理中,tokenization是将一个句子或文本分成tokens的过程。 在大型语言模型的训练和应用中,模型接收一串tokens作为输入,并尝试预测下一个最可能的token。
|
||||
# 上下文关联
|
||||
在使用上下文关联时,模型除了处理你当前的问题时还需要一并将历史对话的一部分共同处理。所以需要消耗更多的token。在本应用中token消耗平均值大约是不开启上下文功能的300%。
|
17
doc/serverapi.md
Normal file
17
doc/serverapi.md
Normal file
@ -0,0 +1,17 @@
|
||||
# server API 接口文档
|
||||
|
||||
## 请求格式(json)
|
||||
| 键 | 类型 | 必选 | 示例 |
|
||||
|---------|------|-----|-----|
|
||||
| prompt | str | yes | "你好" |
|
||||
| userkey | str | yes | "2b3j41b2xh1hz1" |
|
||||
| history | list | no | "history":[{"user":"XXXXXX","bot":"XXXXXX"},{"user":"XXXXXX""bot":"XXXXXX"}] |
|
||||
|
||||
|
||||
## 返回格式(json)
|
||||
| 键 | 类型 | 示例 |
|
||||
|----|------|------|
|
||||
| code | int | 200 |
|
||||
| output | str | "你好" |
|
||||
| surplus | int | 10000 |
|
||||
| |
|
27
src/Server/chatglmTurbo.py
Normal file
27
src/Server/chatglmTurbo.py
Normal file
@ -0,0 +1,27 @@
|
||||
import zhipuai , config
|
||||
|
||||
zhipuai.api_key = config.readConf()["chatglmturbo"]["Authorization"]
|
||||
|
||||
def service(prompt,history = ""):
|
||||
if history == "":
|
||||
response = zhipuai.model_api.invoke(
|
||||
model="chatglm_turbo",
|
||||
prompt=[
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
)
|
||||
else:
|
||||
response = zhipuai.model_api.invoke(
|
||||
model="chatglm_turbo",
|
||||
prompt=[
|
||||
{"role": "user", "content": history[1]["user"]},
|
||||
{"role": "assistant", "content": history[1]["bot"]},
|
||||
{"role": "user", "content": history[0]["user"]},
|
||||
{"role": "assistant", "content": history[0]["bot"]},
|
||||
{"role": "user", "content": prompt},
|
||||
]
|
||||
)
|
||||
if response["code"] == 200:
|
||||
return 200, response["data"]["choices"][0]["content"], response["data"]["usage"]['total_tokens']
|
||||
else:
|
||||
return 50 , str(response["code"])+response["msg"], 0
|
5
src/Server/config.py
Normal file
5
src/Server/config.py
Normal file
@ -0,0 +1,5 @@
|
||||
import json
|
||||
|
||||
def readConf():
|
||||
with open('config.json') as f:
|
||||
return json.load(f)
|
58
src/Server/db.py
Normal file
58
src/Server/db.py
Normal file
@ -0,0 +1,58 @@
|
||||
import pymysql , config
|
||||
|
||||
|
||||
def userSurplus(userkey):
|
||||
#打开数据库连接
|
||||
db = pymysql.connect(host=config.readConf()["db"]["host"],
|
||||
port=config.readConf()["db"]["port"],
|
||||
user=config.readConf()["db"]["user"],
|
||||
password=config.readConf()["db"]["passwd"],
|
||||
database=config.readConf()["db"]["database"])
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 使用 execute() 方法执行 SQL 查询
|
||||
cursor.execute(f"SELECT surplus FROM usersurplus WHERE userkey = '{userkey}';")
|
||||
# 使用 fetchone() 方法获取单条数据.
|
||||
data = cursor.fetchone()
|
||||
|
||||
# 关闭连接
|
||||
db.close()
|
||||
|
||||
if data != None:
|
||||
return data[0]
|
||||
return -99999
|
||||
|
||||
def reduce_value(userkey, value): # 减去对应的值
|
||||
#打开数据库连接
|
||||
db = pymysql.connect(host=config.readConf()["db"]["host"],
|
||||
port=config.readConf()["db"]["port"],
|
||||
user=config.readConf()["db"]["user"],
|
||||
password=config.readConf()["db"]["passwd"],
|
||||
database=config.readConf()["db"]["database"])
|
||||
# 使用 cursor() 方法创建一个游标对象 cursor
|
||||
cursor = db.cursor()
|
||||
|
||||
# 执行 SQL 查询以获取当前值
|
||||
cursor.execute(f"SELECT surplus FROM usersurplus WHERE userkey = '{userkey}';")
|
||||
current_value = cursor.fetchone()[0]
|
||||
|
||||
# 如果没有找到用户,则返回错误信息
|
||||
if current_value is None:
|
||||
db.close()
|
||||
return -1
|
||||
|
||||
# 计算新的值
|
||||
new_value = current_value - value
|
||||
|
||||
# 更新数据库中的值
|
||||
cursor.execute(f"UPDATE usersurplus SET surplus={new_value} WHERE userkey='{userkey}'")
|
||||
|
||||
# 提交事务
|
||||
db.commit()
|
||||
|
||||
# 关闭连接
|
||||
db.close()
|
||||
|
||||
# 返回新值
|
||||
return 0
|
48
src/Server/main.py
Normal file
48
src/Server/main.py
Normal file
@ -0,0 +1,48 @@
|
||||
import flask , requests , json
|
||||
from flask_cors import CORS
|
||||
import db , qwenTurbo ,chatglmTurbo
|
||||
|
||||
|
||||
|
||||
|
||||
app = flask.Flask(__name__)
|
||||
CORS(app,origins="*")
|
||||
|
||||
@app.route('/api/user', methods=['POST'])
|
||||
def post_data():
|
||||
userRequest = flask.request.json
|
||||
surplusToken = db.userSurplus(userRequest['userkey'])
|
||||
|
||||
if userRequest["prompt"] == "":
|
||||
return {"code":42,"output":"Input is empty"}
|
||||
|
||||
if userRequest["prompt"] == "":
|
||||
return {"code":42,"output":"UserKey is empty"}
|
||||
|
||||
if surplusToken == -99999: # 判断用户是否存在和余额
|
||||
return {"code":41,"output":"UserKey not found"}
|
||||
elif surplusToken <= 0:
|
||||
return {"code":40,"output":"Token has been use up"}
|
||||
|
||||
if userRequest["model"] == "qwen-turbo": # 调用qwen-Turbo
|
||||
if userRequest["context"] == 1: # 是否使用上文关联
|
||||
code , output , tokenUsed = qwenTurbo.service(userRequest['prompt'],userRequest['history'])
|
||||
elif userRequest["context"] == 0:
|
||||
code , output , tokenUsed = qwenTurbo.service(userRequest['prompt'])
|
||||
|
||||
if userRequest["model"] == "chatglm-turbo": # 调用chatglm-turbo
|
||||
if userRequest["context"] == 1: # 是否使用上文关联
|
||||
code , output , tokenUsed = chatglmTurbo.service(userRequest['prompt'],userRequest['history'])
|
||||
elif userRequest["context"] == 0:
|
||||
code , output , tokenUsed = chatglmTurbo.service(userRequest['prompt'])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
db.reduce_value(userRequest['userkey'], tokenUsed)
|
||||
return {"code":code,"output":output,"surplus":surplusToken}
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True,host='0.0.0.0',port=5000)
|
31
src/Server/qwenTurbo.py
Normal file
31
src/Server/qwenTurbo.py
Normal file
@ -0,0 +1,31 @@
|
||||
import requests , json , config
|
||||
|
||||
# 设置请求的目标URL
|
||||
url = config.readConf()["qwenturbo"]["url"] # 替换为你的API端点URL
|
||||
header = {
|
||||
"Content-Type":"application/json",
|
||||
"Authorization":config.readConf()["qwenturbo"]["Authorization"]
|
||||
}
|
||||
|
||||
def service(prompt,history = ""):
|
||||
# 设置请求数据
|
||||
if history == "":
|
||||
data = {
|
||||
"model": "qwen-turbo",
|
||||
"input":{
|
||||
"prompt":f"{prompt}"
|
||||
}
|
||||
}
|
||||
else:
|
||||
data = {
|
||||
"model": "qwen-turbo",
|
||||
"input":{
|
||||
"prompt":f"{prompt}",
|
||||
"history":history
|
||||
}
|
||||
}
|
||||
# 发送POST请求
|
||||
response = json.loads(requests.post(url, json=data ,headers=header).text)
|
||||
if 'code' in response:
|
||||
return 50,response['code']+response['message'],0
|
||||
return 200,response['output']['text'],response["usage"]["total_tokens"]
|
288
src/Web/index.html
Normal file
288
src/Web/index.html
Normal file
@ -0,0 +1,288 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>KakuAI</title>
|
||||
<link rel="stylesheet" type="text/css" href="popup.css">
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
background-color: #f5f5f5;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
background-image: url("img/bg_circles.png");
|
||||
background-repeat: repeat;
|
||||
}
|
||||
#chat-container {
|
||||
width: 80%; /* 使用百分比宽度 */
|
||||
max-width: 1300px; /* 最大宽度,防止界面变得过宽 */
|
||||
background-color: #fff;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 10px;
|
||||
padding: 20px;
|
||||
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
#chat-messages {
|
||||
max-height: 500px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 5px;
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
word-wrap: break-word; /* 自动换行 */
|
||||
white-space: pre-wrap; /* 保留空格并自动换行 */
|
||||
}
|
||||
.message-divider {
|
||||
border-top: 1px solid #ccc;
|
||||
margin: 10px 0;
|
||||
}
|
||||
#user-input {
|
||||
width: 97%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 5px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
#user-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
#user-input::placeholder {
|
||||
color: #999;
|
||||
}
|
||||
#user-input-button {
|
||||
background-color: #007bff;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 10px 20px;
|
||||
margin-top: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#user-input-button:disabled {
|
||||
background-color: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
/* 新增样式 */
|
||||
#additional-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between; /* 左右对齐 */
|
||||
margin-top: 10px;
|
||||
}
|
||||
#additional-controls input[type="checkbox"] {
|
||||
margin-right: 10px;
|
||||
}
|
||||
#additional-controls button {
|
||||
background-color: #466d2b;
|
||||
color: #fff;
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
padding: 10px 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body onload="checkCookie()">
|
||||
<div id="chat-container">
|
||||
<div id="chat-messages"></div>
|
||||
<input type="text" id="user-input" placeholder="输入消息..."> <!-- 用户输入消息的输入框 -->
|
||||
<button id="user-input-button" disabled>发送</button> <!-- 发送消息按钮初始状态禁用 -->
|
||||
<!-- 新增复选框、文本和附加功能按钮 -->
|
||||
<div id="additional-controls">
|
||||
<label>
|
||||
<input type="checkbox" id="additional-checkbox"> 联系上文 <a id="showtoken"><br>剩余Token将会被显示在这里</a>
|
||||
</label>
|
||||
<button id="additional-button">设置</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="popup-container" class="hidden"> <!--菜单部分-->
|
||||
<div id="popup">
|
||||
<button id="close-popup">关闭</button>
|
||||
<h1>设置</h1>
|
||||
<p>使用的AI模型</p>
|
||||
<select id="setUpDropdown" defaultValue="qwen-turbo">
|
||||
<option value="qwen-turbo">qwen-turbo</option>
|
||||
<option value="chatglm-turbo">chatglmTurbo</option>
|
||||
</select>
|
||||
<hr>
|
||||
<h3>当前UserKey</h3>
|
||||
<a id="showUserKey"><br>当前UserKey将会被显示在这里</a>
|
||||
<hr>
|
||||
<div id="buttons">
|
||||
<button id="setUpButton1">设置UserKey</button>
|
||||
<button id="setUpButton2" onclick="window.location.href = 'https://afdian.net/a/kaku55'">获取更多Token</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const chatMessages = document.getElementById('chat-messages'); // 聊天消息容器
|
||||
const userInput = document.getElementById('user-input'); // 用户输入消息的输入框
|
||||
const sendButton = document.getElementById('user-input-button'); // 发送消息按钮
|
||||
const additionalButton = document.getElementById('additional-button'); // 附加功能按钮
|
||||
const additionalCheckbox = document.getElementById('additional-checkbox'); // 复选框
|
||||
const popupContainer = document.getElementById('popup-container'); // 菜单容器
|
||||
const closePopup = document.getElementById('close-popup'); // 关闭菜单按钮
|
||||
const setUpButton1 = document.getElementById('setUpButton1');// 菜单按钮1
|
||||
|
||||
var userhs1 = "x"; // 历史记录的保存
|
||||
var userhs0 = "x";
|
||||
var boths1 = "x";
|
||||
var boths0 = "x";
|
||||
|
||||
// 关闭菜单
|
||||
closePopup.addEventListener('click', () => {
|
||||
popupContainer.style.right = '-100%';
|
||||
popupContainer.classList.add('hidden');
|
||||
});
|
||||
|
||||
// 点击发送按钮后的处理函数
|
||||
sendButton.addEventListener('click', sendMessage);
|
||||
// 用户输入消息后的处理函数
|
||||
userInput.addEventListener('input', handleUserInput);
|
||||
// 点击附加功能按钮后的处理函数
|
||||
additionalButton.addEventListener('click', additionalFunction);
|
||||
// 菜单按钮的处理函数
|
||||
setUpButton1.addEventListener('click',resetCookie);
|
||||
|
||||
// 发送消息函数
|
||||
function sendMessage() {
|
||||
const userMessage = userInput.value; // 获取用户输入的消息
|
||||
appendMessage('你', userMessage); // 在聊天界面中添加用户消息
|
||||
userInput.value = ''; // 清空输入框
|
||||
|
||||
// 立即禁用发送按钮
|
||||
sendButton.disabled = true;
|
||||
|
||||
// 在实际应用中,你可以将用户消息发送到后端进行处理
|
||||
// 在这个示例中,我们模拟了来自助手的响应
|
||||
setTimeout(function() {
|
||||
requestAPI(userMessage);
|
||||
}, 500);
|
||||
}
|
||||
|
||||
// 在聊天界面中添加消息
|
||||
function appendMessage(sender, message) {
|
||||
const messageDiv = document.createElement('div');
|
||||
messageDiv.innerHTML = `<strong>${sender}:</strong> ${message}`;
|
||||
chatMessages.appendChild(messageDiv);
|
||||
|
||||
// 在每条消息后添加分隔线
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'message-divider';
|
||||
chatMessages.appendChild(divider);
|
||||
}
|
||||
|
||||
// 附加功能按钮的处理函数
|
||||
function additionalFunction() {
|
||||
// 处理附加功能按钮的点击事件
|
||||
popupContainer.classList.remove('hidden');
|
||||
popupContainer.style.right = '0';
|
||||
}
|
||||
|
||||
// 用户输入框的输入事件处理函数
|
||||
function handleUserInput() {
|
||||
// 根据用户输入的内容启用或禁用发送按钮
|
||||
sendButton.disabled = userInput.value.trim() === '';
|
||||
}
|
||||
|
||||
// 使用Cookie保存用户的UserKey
|
||||
function setCookie(cname, cvalue, exdays) {
|
||||
var d = new Date();
|
||||
d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000));
|
||||
var expires = "expires=" + d.toGMTString();
|
||||
document.cookie = cname + "=" + cvalue + "; " + expires;
|
||||
}
|
||||
function getCookie(cname) {
|
||||
var name = cname + "=";
|
||||
var ca = document.cookie.split(';');
|
||||
for (var i = 0; i < ca.length; i++) {
|
||||
var c = ca[i].trim();
|
||||
if (c.indexOf(name) == 0) { return c.substring(name.length, c.length); }
|
||||
}
|
||||
return "";
|
||||
}
|
||||
function checkCookie() {
|
||||
var user = getCookie("userkey");
|
||||
if (user != "") {
|
||||
alert("欢迎回来 UserKey:" + user);
|
||||
document.getElementById("showUserKey").innerHTML = user;
|
||||
}
|
||||
else {
|
||||
user = prompt("请输入你的Userkey:", "");
|
||||
if (user != "" && user != null) {
|
||||
setCookie("userkey", user, 265);
|
||||
}
|
||||
}
|
||||
}
|
||||
function resetCookie() {
|
||||
user = prompt("请输入你的Userkey:", "");
|
||||
if (user != "" && user != null) {
|
||||
setCookie("userkey", user, 265);
|
||||
alert("UserKey已更新");
|
||||
document.getElementById("showUserKey").innerHTML = user;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function requestAPI(message) {
|
||||
// 创建包含JSON数据的对象
|
||||
var requestData = {
|
||||
"model":document.getElementById("setUpDropdown").value,
|
||||
"prompt": message,
|
||||
"userkey": getCookie("userkey"),
|
||||
"context": Number(additionalCheckbox.checked),
|
||||
"history": [
|
||||
{
|
||||
"user": userhs1 ,
|
||||
"bot": boths1
|
||||
},
|
||||
{
|
||||
"user": userhs0,
|
||||
"bot": boths0
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
|
||||
fetch("http://chat.kakuweb.top:5000/api/user", {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
// 在这里处理返回的JSON数据
|
||||
if (data["code"] == 200) {
|
||||
appendMessage("KakuAI"+"("+document.getElementById("setUpDropdown").value+")",data["output"]);
|
||||
document.getElementById("showtoken").innerHTML = "<br>剩余Tokens:" + data["surplus"];
|
||||
}
|
||||
else{
|
||||
alert("ErrCode:"+data["code"]+" "+data["output"])
|
||||
}
|
||||
userhs1 = userhs0;
|
||||
boths1 = boths0;
|
||||
userhs0 = message;
|
||||
boths0 = data["output"];
|
||||
loading = false;
|
||||
// 可以根据返回的数据执行相应的操作
|
||||
})
|
||||
.catch(error => {
|
||||
console.error('请求出错:', error);
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
47
src/Web/popup.css
Normal file
47
src/Web/popup.css
Normal file
@ -0,0 +1,47 @@
|
||||
#popup-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
#popup {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
width: 20%;
|
||||
min-width: 150px;
|
||||
min-width: 200px;
|
||||
background: white;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
#close-popup {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
#dropdown {
|
||||
margin: auto;
|
||||
width: 80%; /* 让<select>元素宽度占满容器 */
|
||||
margin-bottom: 10px; /* 添加一些底部间距 */
|
||||
}
|
||||
|
||||
#buttons {
|
||||
margin: auto;
|
||||
width: 50%;
|
||||
display: flex;
|
||||
flex-direction: column; /* 设置按钮垂直排列 */
|
||||
}
|
||||
|
||||
#buttons button {
|
||||
margin: 10px 0; /* 添加按钮之间的垂直间距 */
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user