mirror of
https://gh-proxy.org/https://github.com/klinsmaya/SQLBot.git
synced 2026-11-08 01:06:59 +08:00
Merge branch 'main' of https://github.com/dataease/SQLBot
This commit is contained in:
@@ -1,16 +1,7 @@
|
||||
|
||||
# Before running, please copy .env.example to .env
|
||||
DOMAIN=localhost
|
||||
FRONTEND_HOST=http://localhost:5173
|
||||
ENVIRONMENT=local
|
||||
PROJECT_NAME="SQLBot"
|
||||
STACK_NAME=SQLBot
|
||||
|
||||
# Backend
|
||||
BACKEND_CORS_ORIGINS="http://localhost,http://localhost:5173,https://localhost,https://localhost:5173"
|
||||
SECRET_KEY=y5txe1mRmS_JpOrUzFzHEu-kIQn3lf7ll0AOv9DQh0s
|
||||
FIRST_SUPERUSER=[email protected]
|
||||
FIRST_SUPERUSER_PASSWORD=123456 # Change this to your pwd
|
||||
|
||||
TOKEN_KEY="X-SQLBOT-TOKEN"
|
||||
DEFAULT_PWD="SQLBot@123456"
|
||||
@@ -22,7 +13,6 @@ LOG_FORMAT="%(asctime)s - %(name)s - %(levelname)s:%(lineno)d - %(message)s"
|
||||
SQL_DEBUG=False
|
||||
|
||||
CACHE_TYPE="memory"
|
||||
CACHE_REDIS_URL="redis://127.0.0.1:6379"
|
||||
|
||||
# Postgres
|
||||
POSTGRES_SERVER=localhost
|
||||
@@ -31,12 +21,6 @@ POSTGRES_DB=sqlbot
|
||||
POSTGRES_USER=root
|
||||
POSTGRES_PASSWORD=132456 # Change this to your pwd
|
||||
|
||||
SENTRY_DSN=
|
||||
|
||||
# Configure these with your own Docker registry images
|
||||
DOCKER_IMAGE_BACKEND=backend
|
||||
DOCKER_IMAGE_FRONTEND=frontend
|
||||
|
||||
MCP_IMAGE_PATH=/opt/sqlbot/images
|
||||
MCP_IMAGE_HOST=http://localhost:3000
|
||||
SERVER_IMAGE_HOST=https://sqlbot.fit2cloud.cn/images/
|
||||
|
||||
@@ -439,6 +439,7 @@ class LLMService:
|
||||
_ds = self.out_ds_instance.get_ds(data['id'])
|
||||
self.ds = _ds
|
||||
self.chat_question.engine = _ds.type
|
||||
self.chat_question.db_schema = self.out_ds_instance.get_db_schema(self.ds.id)
|
||||
_engine_type = self.chat_question.engine
|
||||
_chat.engine_type = _ds.type
|
||||
else:
|
||||
@@ -448,6 +449,7 @@ class LLMService:
|
||||
raise Exception(f"Datasource configuration with id {_datasource} not found")
|
||||
self.ds = CoreDatasource(**_ds.model_dump())
|
||||
self.chat_question.engine = _ds.type_name if _ds.type != 'excel' else 'PostgreSQL'
|
||||
self.chat_question.db_schema = get_table_schema(session=self.session, current_user=self.current_user, ds=self.ds)
|
||||
_engine_type = self.chat_question.engine
|
||||
_chat.engine_type = _ds.type_name
|
||||
# save chat
|
||||
@@ -834,7 +836,8 @@ class LLMService:
|
||||
self.ds.id) if self.out_ds_instance else get_table_schema(session=self.session,
|
||||
current_user=self.current_user,
|
||||
ds=self.ds)
|
||||
|
||||
else:
|
||||
self.validate_history_ds()
|
||||
# generate sql
|
||||
sql_res = self.generate_sql()
|
||||
full_sql_text = ''
|
||||
@@ -1023,7 +1026,13 @@ class LLMService:
|
||||
finally:
|
||||
# end
|
||||
pass
|
||||
|
||||
|
||||
def validate_history_ds(self):
|
||||
_invalid_ds = False
|
||||
_ds = self.ds
|
||||
_assistant = self.current_assistant
|
||||
if _invalid_ds:
|
||||
yield orjson.dumps({'content': 'ds is invalid', 'type': 'error'}).decode() + '\n\n'
|
||||
|
||||
def execute_sql_with_db(db: SQLDatabase, sql: str) -> str:
|
||||
"""Execute SQL query using SQLDatabase
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import secrets
|
||||
import warnings
|
||||
from typing import Annotated, Any, Literal
|
||||
|
||||
from pydantic import (
|
||||
AnyUrl,
|
||||
BeforeValidator,
|
||||
EmailStr,
|
||||
HttpUrl,
|
||||
PostgresDsn,
|
||||
computed_field,
|
||||
model_validator,
|
||||
)
|
||||
from pydantic_core import MultiHostUrl
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
from typing_extensions import Self
|
||||
|
||||
|
||||
def parse_cors(v: Any) -> list[str] | str:
|
||||
@@ -31,12 +26,12 @@ class Settings(BaseSettings):
|
||||
env_ignore_empty=True,
|
||||
extra="ignore",
|
||||
)
|
||||
PROJECT_NAME: str = "SQLBot"
|
||||
API_V1_STR: str = "/api/v1"
|
||||
SECRET_KEY: str = secrets.token_urlsafe(32)
|
||||
# 60 minutes * 24 hours * 8 days = 8 days
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 24 * 8
|
||||
FRONTEND_HOST: str = "http://localhost:5173"
|
||||
ENVIRONMENT: Literal["local", "staging", "production"] = "local"
|
||||
|
||||
BACKEND_CORS_ORIGINS: Annotated[
|
||||
list[AnyUrl] | str, BeforeValidator(parse_cors)
|
||||
@@ -49,8 +44,6 @@ class Settings(BaseSettings):
|
||||
self.FRONTEND_HOST
|
||||
]
|
||||
|
||||
PROJECT_NAME: str
|
||||
SENTRY_DSN: HttpUrl | None = None
|
||||
POSTGRES_SERVER: str
|
||||
POSTGRES_PORT: int = 5432
|
||||
POSTGRES_USER: str
|
||||
@@ -81,55 +74,8 @@ class Settings(BaseSettings):
|
||||
path=self.POSTGRES_DB,
|
||||
)
|
||||
|
||||
SMTP_TLS: bool = True
|
||||
SMTP_SSL: bool = False
|
||||
SMTP_PORT: int = 587
|
||||
SMTP_HOST: str | None = None
|
||||
SMTP_USER: str | None = None
|
||||
SMTP_PASSWORD: str | None = None
|
||||
EMAILS_FROM_EMAIL: EmailStr | None = None
|
||||
EMAILS_FROM_NAME: EmailStr | None = None
|
||||
MCP_IMAGE_PATH: str
|
||||
MCP_IMAGE_HOST: str
|
||||
SERVER_IMAGE_HOST: str
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _set_default_emails_from(self) -> Self:
|
||||
if not self.EMAILS_FROM_NAME:
|
||||
self.EMAILS_FROM_NAME = self.PROJECT_NAME
|
||||
return self
|
||||
|
||||
EMAIL_RESET_TOKEN_EXPIRE_HOURS: int = 48
|
||||
|
||||
@computed_field # type: ignore[prop-decorator]
|
||||
@property
|
||||
def emails_enabled(self) -> bool:
|
||||
return bool(self.SMTP_HOST and self.EMAILS_FROM_EMAIL)
|
||||
|
||||
EMAIL_TEST_USER: EmailStr = "[email protected]"
|
||||
FIRST_SUPERUSER: EmailStr
|
||||
FIRST_SUPERUSER_PASSWORD: str
|
||||
|
||||
def _check_default_secret(self, var_name: str, value: str | None) -> None:
|
||||
if value == "changethis":
|
||||
message = (
|
||||
f'The value of {var_name} is "changethis", '
|
||||
"for security, please change it, at least for deployments."
|
||||
)
|
||||
if self.ENVIRONMENT == "local":
|
||||
warnings.warn(message, stacklevel=1)
|
||||
else:
|
||||
raise ValueError(message)
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _enforce_non_default_secrets(self) -> Self:
|
||||
self._check_default_secret("SECRET_KEY", self.SECRET_KEY)
|
||||
self._check_default_secret("POSTGRES_PASSWORD", self.POSTGRES_PASSWORD)
|
||||
self._check_default_secret(
|
||||
"FIRST_SUPERUSER_PASSWORD", self.FIRST_SUPERUSER_PASSWORD
|
||||
)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
settings = Settings() # type: ignore
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from fastapi.concurrency import asynccontextmanager
|
||||
import sentry_sdk
|
||||
from fastapi import FastAPI
|
||||
from fastapi.routing import APIRoute
|
||||
from starlette.middleware.cors import CORSMiddleware
|
||||
@@ -38,9 +37,6 @@ def custom_generate_unique_id(route: APIRoute) -> str:
|
||||
return f"{tag}-{route.name}"
|
||||
|
||||
|
||||
if settings.SENTRY_DSN and settings.ENVIRONMENT != "local":
|
||||
sentry_sdk.init(dsn=str(settings.SENTRY_DSN), enable_tracing=True)
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.PROJECT_NAME,
|
||||
openapi_url=f"{settings.API_V1_STR}/openapi.json",
|
||||
|
||||
@@ -6,6 +6,7 @@ template:
|
||||
任务:
|
||||
根据给定的表结构(M-Schema)和用户问题生成符合{engine}数据库引擎规范的sql语句,以及sql中所用到的表名(不要包含schema和database,用数组返回)。
|
||||
你必须遵守以下规则:
|
||||
- 不要编造没有提供给你的表结构
|
||||
- 生成的SQL必须符合{engine}的规范。
|
||||
- 你的回答必须使用如下JSON格式返回:
|
||||
{{"success":true,"sql":"生成的SQL语句","tables":["表名1","表名2",...]}}
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
<path d="M9.95317 8.73169L15.5511 3.13376C15.7138 2.97104 15.9776 2.97104 16.1403 3.13376L16.7296 3.72301C16.8923 3.88573 16.8923 4.14955 16.7296 4.31227L11.1317 9.9102L16.7296 15.5081C16.8923 15.6708 16.8923 15.9347 16.7296 16.0974L16.1403 16.6866C15.9776 16.8494 15.7138 16.8494 15.5511 16.6866L9.95317 11.0887L4.35524 16.6866C4.19252 16.8494 3.9287 16.8494 3.76598 16.6866L3.17673 16.0974C3.01401 15.9347 3.01401 15.6708 3.17673 15.5081L8.77465 9.9102L3.17673 4.31227C3.01401 4.14955 3.01401 3.88573 3.17673 3.72301L3.76598 3.13376C3.9287 2.97104 4.19252 2.97104 4.35524 3.13376L9.95317 8.73169Z" fill="#ffffff"></path>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="sqlbot-assistant-title"> 🌟 遇见问题,不再有障碍!</div>
|
||||
<p>你好,我是你的智能小助手。<br/>
|
||||
点我,开启高效解答模式,让问题变成过去式。</p>
|
||||
@@ -217,9 +217,9 @@
|
||||
bottom: 0!important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* 引导 */
|
||||
|
||||
|
||||
#sqlbot-assistant .sqlbot-assistant-mask {
|
||||
position: fixed;
|
||||
z-index: 10001;
|
||||
@@ -289,7 +289,7 @@
|
||||
right: 20px;
|
||||
top: 20px;
|
||||
cursor: pointer;
|
||||
|
||||
|
||||
}
|
||||
#sqlbot-assistant-chat-container {
|
||||
width: 460px;
|
||||
@@ -303,7 +303,7 @@
|
||||
right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#sqlbot-assistant .sqlbot-assistant-chat-button{
|
||||
position: fixed;
|
||||
${data.x_type}: ${data.x_value}px;
|
||||
@@ -362,7 +362,7 @@
|
||||
from {
|
||||
height: 0;;
|
||||
}
|
||||
|
||||
|
||||
to {
|
||||
height: 600px;
|
||||
}
|
||||
@@ -433,7 +433,6 @@
|
||||
}
|
||||
if (event.data?.busi == 'ready' && event.data?.ready) {
|
||||
const certificate = parsrCertificate(data)
|
||||
console.log(certificate)
|
||||
params = {
|
||||
busi: 'certificate',
|
||||
certificate,
|
||||
|
||||
@@ -58,8 +58,6 @@ const valueText = (field: string, val: string, options: any[]) => {
|
||||
return val
|
||||
}
|
||||
export const convertFilterText = (conditions: any[], options: any[]) => {
|
||||
console.log('conditions', conditions, options)
|
||||
|
||||
const result: any[] = []
|
||||
conditions.forEach((condition) => {
|
||||
const field = condition.field
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
"add_dashboard_name_tips": "请输入仪表板名称",
|
||||
"existing_dashboard": "存量仪表板",
|
||||
"add_success": "添加成功",
|
||||
"no_data": "没找到相关内容",
|
||||
"no_data": "没有找到相关内容",
|
||||
"new_tab": "新建Tab",
|
||||
"length_limit64": "字段长度需要在1-64之间",
|
||||
"sort_column": "排序字段",
|
||||
|
||||
@@ -31,7 +31,7 @@ export const watchRouter = (router: any) => {
|
||||
return
|
||||
}
|
||||
if (to.path === '/login') {
|
||||
console.log(from)
|
||||
console.info(from)
|
||||
next('/chat')
|
||||
} else {
|
||||
next()
|
||||
|
||||
@@ -133,9 +133,6 @@ const sendMessage = async () => {
|
||||
_list.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(_list)
|
||||
|
||||
for (const str of _list) {
|
||||
let data
|
||||
try {
|
||||
@@ -161,7 +158,7 @@ const sendMessage = async () => {
|
||||
_currentChat.value.records[index.value].id = data.id
|
||||
break
|
||||
case 'info':
|
||||
console.log(data.msg)
|
||||
console.info(data.msg)
|
||||
break
|
||||
case 'error':
|
||||
currentRecord.error = data.content
|
||||
|
||||
@@ -137,9 +137,6 @@ const sendMessage = async () => {
|
||||
_list.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(_list)
|
||||
|
||||
for (const str of _list) {
|
||||
let data
|
||||
try {
|
||||
@@ -165,7 +162,7 @@ const sendMessage = async () => {
|
||||
_currentChat.value.records[index.value].id = data.id
|
||||
break
|
||||
case 'info':
|
||||
console.log(data.msg)
|
||||
console.info(data.msg)
|
||||
break
|
||||
case 'brief':
|
||||
_currentChat.value.brief = data.brief
|
||||
|
||||
@@ -133,9 +133,6 @@ const sendMessage = async () => {
|
||||
_list.push(line)
|
||||
}
|
||||
}
|
||||
|
||||
// console.log(_list)
|
||||
|
||||
for (const str of _list) {
|
||||
let data
|
||||
try {
|
||||
@@ -160,7 +157,7 @@ const sendMessage = async () => {
|
||||
_currentChat.value.records[index.value].id = data.id
|
||||
break
|
||||
case 'info':
|
||||
console.log(data.msg)
|
||||
console.info(data.msg)
|
||||
break
|
||||
case 'error':
|
||||
currentRecord.error = data.content
|
||||
|
||||
@@ -8,10 +8,10 @@ const props = defineProps<{
|
||||
const { t } = useI18n()
|
||||
|
||||
function clickAnalysis() {
|
||||
console.log('analysis_record_id: ' + props.message?.record?.analysis_record_id)
|
||||
console.info('analysis_record_id: ' + props.message?.record?.analysis_record_id)
|
||||
}
|
||||
function clickPredict() {
|
||||
console.log('predict_record_id: ' + props.message?.record?.predict_record_id)
|
||||
console.info('predict_record_id: ' + props.message?.record?.predict_record_id)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -461,11 +461,11 @@ const currentChatEngineType = computed(() => {
|
||||
})
|
||||
|
||||
function onChatDeleted(id: number) {
|
||||
console.log('deleted', id)
|
||||
console.info('deleted', id)
|
||||
}
|
||||
|
||||
function onChatRenamed(chat: Chat) {
|
||||
console.log('renamed', chat)
|
||||
console.info('renamed', chat)
|
||||
}
|
||||
|
||||
const chatListSideBarShow = ref<boolean>(true)
|
||||
|
||||
@@ -52,7 +52,7 @@ const initOptions = {
|
||||
}
|
||||
const handleInit = (editor: any) => {
|
||||
editorRef.value = editor
|
||||
console.log('TinyMCE 初始化完成', editor)
|
||||
console.info('TinyMCE 初始化完成', editor)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -126,11 +126,11 @@ const refreshData = () => {
|
||||
}
|
||||
|
||||
const panelClick = () => {
|
||||
console.log('panelClick')
|
||||
console.info('panelClick')
|
||||
}
|
||||
|
||||
const smartClick = () => {
|
||||
console.log('smartClick')
|
||||
console.info('smartClick')
|
||||
}
|
||||
|
||||
const deleteHandler = (item: any) => {
|
||||
|
||||
@@ -114,11 +114,10 @@ const state = reactive({
|
||||
},
|
||||
})
|
||||
const handleSearch = (e: any) => {
|
||||
console.log('search', e)
|
||||
console.info('search', e)
|
||||
}
|
||||
const editHandler = (id: any) => {
|
||||
termApi.query(id).then((res: any) => {
|
||||
console.log('term detail', res)
|
||||
state.form = res
|
||||
dialogFormVisible.value = true
|
||||
})
|
||||
@@ -181,7 +180,7 @@ const addTerm = () => {
|
||||
}
|
||||
const editTerm = () => {
|
||||
termApi.edit(state.form).then((res: any) => {
|
||||
console.log('edit term', res)
|
||||
console.info('edit term', res)
|
||||
dialogFormVisible.value = false
|
||||
search()
|
||||
ElMessage({
|
||||
|
||||
@@ -31,7 +31,7 @@ import Professional from './Professional.vue'
|
||||
const activeName = ref('0')
|
||||
|
||||
const handleClick = (tab: any, event: MouseEvent) => {
|
||||
console.log(tab, event)
|
||||
console.info(tab, event)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ const beforeClose = () => {
|
||||
}
|
||||
|
||||
const handleActive = (row: any) => {
|
||||
console.log('row', row)
|
||||
console.info('row', row)
|
||||
}
|
||||
|
||||
const handlePrivate = (row: any) => {
|
||||
|
||||
@@ -35,7 +35,7 @@ const activeName = ref('')
|
||||
const activeType = ref('')
|
||||
const modelFormRef = ref()
|
||||
const cardRefs = ref<any[]>([])
|
||||
const showCardError = ref(true) // if you don`t want card mask error, just change this to false
|
||||
// const showCardError = ref(true) // if you don`t want card mask error, just change this to false
|
||||
reactive({
|
||||
form: {
|
||||
id: '',
|
||||
@@ -71,6 +71,8 @@ const defaultModelListWithSearch = computed(() => {
|
||||
})
|
||||
|
||||
const modelCheckHandler = async (item: any) => {
|
||||
console.log(item)
|
||||
/**
|
||||
const response = await modelApi.check(item)
|
||||
const reader = response.body.getReader()
|
||||
const decoder = new TextDecoder()
|
||||
@@ -110,6 +112,7 @@ const modelCheckHandler = async (item: any) => {
|
||||
currentRef?.showErrorMask(checkMsg)
|
||||
}
|
||||
})
|
||||
*/
|
||||
}
|
||||
const duplicateName = async (item: any) => {
|
||||
const res = await modelApi.queryAll()
|
||||
@@ -201,7 +204,7 @@ const deleteHandler = (item: any) => {
|
||||
customClass: 'confirm-no_icon',
|
||||
autofocus: false,
|
||||
callback: (val: string) => {
|
||||
console.log(val)
|
||||
console.info(val)
|
||||
},
|
||||
})
|
||||
return
|
||||
|
||||
@@ -93,7 +93,7 @@ const filterTypeChange = () => {
|
||||
initEnumOptions()
|
||||
}
|
||||
const initEnumOptions = () => {
|
||||
console.log('initEnumOptions')
|
||||
console.info('initEnumOptions')
|
||||
}
|
||||
|
||||
const selectItem = ({ field_name, id }: any) => {
|
||||
|
||||
@@ -552,7 +552,7 @@ const handleEditPassword = (id: any) => {
|
||||
const handleConfirmPassword = () => {
|
||||
passwordRef.value.validate((val: any) => {
|
||||
if (val) {
|
||||
console.log(val)
|
||||
console.info(val)
|
||||
}
|
||||
})
|
||||
dialogVisiblePassword.value = false
|
||||
|
||||
@@ -152,10 +152,10 @@ const state = reactive({
|
||||
},
|
||||
})
|
||||
const handleSearch = (e: any) => {
|
||||
console.log('search', e)
|
||||
console.info('search', e)
|
||||
}
|
||||
const editHandler = (id: any) => {
|
||||
console.log('editHandler', id)
|
||||
console.info('editHandler', id)
|
||||
/* userApi.query(id).then((res: any) => {
|
||||
console.log('term detail', res)
|
||||
state.form = res
|
||||
@@ -163,7 +163,7 @@ const editHandler = (id: any) => {
|
||||
}) */
|
||||
}
|
||||
const deleteHandler = (id: any) => {
|
||||
console.log('deleteHandler', id)
|
||||
console.info('deleteHandler', id)
|
||||
ElMessageBox.confirm('Are you sure to delete?', 'Warning', {
|
||||
confirmButtonText: 'OK',
|
||||
cancelButtonText: 'Cancel',
|
||||
@@ -222,7 +222,7 @@ const addTerm = () => {
|
||||
}
|
||||
const editTerm = () => {
|
||||
userApi.edit(state.form).then((res: any) => {
|
||||
console.log('edit term', res)
|
||||
console.info('edit term', res)
|
||||
dialogFormVisible.value = false
|
||||
search()
|
||||
ElMessage({
|
||||
|
||||
@@ -7,8 +7,8 @@ import path from 'path'
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
console.log(mode)
|
||||
console.log(env)
|
||||
console.info(mode)
|
||||
console.info(env)
|
||||
return {
|
||||
base: './',
|
||||
plugins: [
|
||||
|
||||
+1
-4
@@ -17,7 +17,7 @@ http.createServer((req, res) => {
|
||||
toPost(req, res);
|
||||
}
|
||||
}).listen(port, () => {
|
||||
console.log(`Server listening on: http://localhost:${port}`);
|
||||
console.info(`Server listening on: http://localhost:${port}`);
|
||||
});
|
||||
|
||||
function getOptions(type, axis, data) {
|
||||
@@ -50,9 +50,7 @@ function getOptions(type, axis, data) {
|
||||
|
||||
// 创建 Chart 和配置
|
||||
async function GenerateCharts(obj) {
|
||||
console.log(obj)
|
||||
const options = getOptions(obj.type, JSON.parse(obj.axis), JSON.parse(obj.data));
|
||||
console.log(options);
|
||||
const chart = await createChart(options);
|
||||
|
||||
// 导出
|
||||
@@ -69,7 +67,6 @@ async function GenerateCharts(obj) {
|
||||
function toGet(req, res) {
|
||||
let data = 'GET请求内容:\n' + util.inspect(url.parse(req.url));
|
||||
res.end(data);
|
||||
console.log(data);
|
||||
}
|
||||
|
||||
//获取POST请求内容、cookie
|
||||
|
||||
Reference in New Issue
Block a user