初版
This commit is contained in:
commit
ebc5c01eb0
8
.idea/.gitignore
vendored
Normal file
8
.idea/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# 默认忽略的文件
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# 基于编辑器的 HTTP 客户端请求
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
||||||
14
.idea/deployment.xml
Normal file
14
.idea/deployment.xml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="PublishConfigData" remoteFilesAllowedToDisappearOnAutoupload="false">
|
||||||
|
<serverData>
|
||||||
|
<paths name="pi@home.heshunme.xyz:6666 password">
|
||||||
|
<serverdata>
|
||||||
|
<mappings>
|
||||||
|
<mapping local="$PROJECT_DIR$" web="/" />
|
||||||
|
</mappings>
|
||||||
|
</serverdata>
|
||||||
|
</paths>
|
||||||
|
</serverData>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
8
.idea/hw_ocr_backend.iml
Normal file
8
.idea/hw_ocr_backend.iml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="PYTHON_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$" />
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
52
.idea/inspectionProfiles/Project_Default.xml
Normal file
52
.idea/inspectionProfiles/Project_Default.xml
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<profile version="1.0">
|
||||||
|
<option name="myName" value="Project Default" />
|
||||||
|
<inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<Languages>
|
||||||
|
<language minSize="110" name="Python" />
|
||||||
|
</Languages>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||||
|
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredPackages">
|
||||||
|
<value>
|
||||||
|
<list size="6">
|
||||||
|
<item index="0" class="java.lang.String" itemvalue="alibabacloud_tea_console" />
|
||||||
|
<item index="1" class="java.lang.String" itemvalue="alibabacloud_tea_util" />
|
||||||
|
<item index="2" class="java.lang.String" itemvalue="alibabacloud_alidns20150109" />
|
||||||
|
<item index="3" class="java.lang.String" itemvalue="alibabacloud_tea_openapi" />
|
||||||
|
<item index="4" class="java.lang.String" itemvalue="gradio" />
|
||||||
|
<item index="5" class="java.lang.String" itemvalue="torch" />
|
||||||
|
</list>
|
||||||
|
</value>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPep8Inspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredErrors">
|
||||||
|
<list>
|
||||||
|
<option value="E722" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredErrors">
|
||||||
|
<list>
|
||||||
|
<option value="N806" />
|
||||||
|
<option value="N802" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
|
<option name="ignoredIdentifiers">
|
||||||
|
<list>
|
||||||
|
<option value="list.*" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
</inspection_tool>
|
||||||
|
<inspection_tool class="SpellCheckingInspection" enabled="true" level="INFORMATION" enabled_by_default="true">
|
||||||
|
<option name="processCode" value="true" />
|
||||||
|
<option name="processLiterals" value="true" />
|
||||||
|
<option name="processComments" value="true" />
|
||||||
|
</inspection_tool>
|
||||||
|
</profile>
|
||||||
|
</component>
|
||||||
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
6
.idea/inspectionProfiles/profiles_settings.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<component name="InspectionProjectProfileManager">
|
||||||
|
<settings>
|
||||||
|
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||||
|
<version value="1.0" />
|
||||||
|
</settings>
|
||||||
|
</component>
|
||||||
32
.idea/misc.xml
Normal file
32
.idea/misc.xml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="Black">
|
||||||
|
<option name="sdkName" value="D:\Anaconda\envs\web" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectInspectionProfilesVisibleTreeState">
|
||||||
|
<entry key="Project Default">
|
||||||
|
<profile-state>
|
||||||
|
<expanded-state>
|
||||||
|
<State>
|
||||||
|
<id>EditorConfig</id>
|
||||||
|
</State>
|
||||||
|
<State>
|
||||||
|
<id>GitHub 操作</id>
|
||||||
|
</State>
|
||||||
|
<State>
|
||||||
|
<id>正则表达式</id>
|
||||||
|
</State>
|
||||||
|
<State>
|
||||||
|
<id>版本控制</id>
|
||||||
|
</State>
|
||||||
|
</expanded-state>
|
||||||
|
<selected-state>
|
||||||
|
<State>
|
||||||
|
<id>用户定义</id>
|
||||||
|
</State>
|
||||||
|
</selected-state>
|
||||||
|
</profile-state>
|
||||||
|
</entry>
|
||||||
|
</component>
|
||||||
|
<component name="ProjectRootManager" version="2" project-jdk-name="D:\Anaconda\envs\web" project-jdk-type="Python SDK" />
|
||||||
|
</project>
|
||||||
8
.idea/modules.xml
Normal file
8
.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/hw_ocr_backend.iml" filepath="$PROJECT_DIR$/.idea/hw_ocr_backend.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
6
.idea/vcs.xml
Normal file
6
.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
115
main.py
Normal file
115
main.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
from fastapi import FastAPI, WebSocket, WebSocketDisconnect, HTTPException, UploadFile, File
|
||||||
|
from fastapi.responses import StreamingResponse, HTMLResponse
|
||||||
|
from pydantic import BaseModel
|
||||||
|
from typing import List
|
||||||
|
import base64
|
||||||
|
import asyncio
|
||||||
|
import openai
|
||||||
|
|
||||||
|
app = FastAPI()
|
||||||
|
with open("key", "r") as f:
|
||||||
|
key = f.read()
|
||||||
|
client = openai.OpenAI(api_key=key,
|
||||||
|
base_url="https://open.bigmodel.cn/api/paas/v4/"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/")
|
||||||
|
async def root():
|
||||||
|
return {"message": "Hello World"}
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/hello/{name}")
|
||||||
|
async def say_hello(name: str):
|
||||||
|
return {"message": f"Hello {name}"}
|
||||||
|
|
||||||
|
|
||||||
|
# WebSocket连接管理器
|
||||||
|
class ConnectionManager:
|
||||||
|
def __init__(self):
|
||||||
|
self.active_connections: List[WebSocket] = []
|
||||||
|
|
||||||
|
async def connect(self, websocket: WebSocket):
|
||||||
|
await websocket.accept()
|
||||||
|
self.active_connections.append(websocket)
|
||||||
|
|
||||||
|
def disconnect(self, websocket: WebSocket):
|
||||||
|
self.active_connections.remove(websocket)
|
||||||
|
|
||||||
|
async def send_personal_message(self, message: str, websocket: WebSocket):
|
||||||
|
await websocket.send_text(message)
|
||||||
|
|
||||||
|
async def broadcast(self, message: str):
|
||||||
|
for connection in self.active_connections:
|
||||||
|
await connection.send_text(message)
|
||||||
|
|
||||||
|
async def broadcast_json(self, data: dict):
|
||||||
|
for connection in self.active_connections:
|
||||||
|
await connection.send_json(data)
|
||||||
|
|
||||||
|
|
||||||
|
manager = ConnectionManager()
|
||||||
|
|
||||||
|
|
||||||
|
# WebSocket端点
|
||||||
|
@app.websocket("/event")
|
||||||
|
async def event(websocket: WebSocket):
|
||||||
|
await manager.connect(websocket)
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
# 这里可以添加逻辑处理来自客户端的消息
|
||||||
|
data = await websocket.receive_text()
|
||||||
|
print(f"Received message from client: {data}")
|
||||||
|
except WebSocketDisconnect:
|
||||||
|
manager.disconnect(websocket)
|
||||||
|
|
||||||
|
|
||||||
|
# POST请求端点
|
||||||
|
@app.post("/predict")
|
||||||
|
async def predict(file: UploadFile = File(...)):
|
||||||
|
# 读取图片文件并转换为base64编码
|
||||||
|
image_data = await file.read()
|
||||||
|
image_base64 = base64.b64encode(image_data).decode('utf-8')
|
||||||
|
|
||||||
|
# 构造请求给API
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="glm-4v",
|
||||||
|
messages=[
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "请描述该图片",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": f"data:image/jpeg;base64,{image_base64}"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
stream=True
|
||||||
|
)
|
||||||
|
# 广播图片数据
|
||||||
|
await manager.broadcast_json({"type": "image", "content": image_base64})
|
||||||
|
|
||||||
|
# 流式返回结果
|
||||||
|
async def stream_response():
|
||||||
|
for chunk in response:
|
||||||
|
content = chunk.choices[0].delta.content
|
||||||
|
if content:
|
||||||
|
yield content
|
||||||
|
# 同时将内容广播给所有WebSocket连接的客户端
|
||||||
|
# await manager.broadcast(content)
|
||||||
|
await manager.broadcast_json({"type": "text", "content": content})
|
||||||
|
|
||||||
|
return StreamingResponse(stream_response(), media_type="text/plain")
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/test", response_class=HTMLResponse)
|
||||||
|
async def test():
|
||||||
|
with open("test.html", "r", encoding="utf-8") as f:
|
||||||
|
return f.read()
|
||||||
132
test.html
Normal file
132
test.html
Normal file
@ -0,0 +1,132 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>WebSocket 文本和图片接收器</title>
|
||||||
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-plus/dist/index.css">
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background-color: #f4f4f4;
|
||||||
|
}
|
||||||
|
#app {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: 0 4px 8px rgba(0,0,0,0.1);
|
||||||
|
background-color: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.el-textarea__inner {
|
||||||
|
min-height: 150px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
.el-row {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.image-container {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
#app {
|
||||||
|
width: 90%;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app">
|
||||||
|
<div class="image-container" v-if="imageUrl">
|
||||||
|
<img :src="imageUrl" alt="Received Image">
|
||||||
|
</div>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-input
|
||||||
|
v-model="text"
|
||||||
|
type="textarea"
|
||||||
|
:rows="4"
|
||||||
|
readonly
|
||||||
|
placeholder="接收到的文本将显示在这里..."
|
||||||
|
></el-input>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<el-col :span="24">
|
||||||
|
<el-button type="primary" @click="copyText">复制</el-button>
|
||||||
|
<el-button type="danger" @click="clearText">清空</el-button>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/vue@3"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/element-plus"></script>
|
||||||
|
<script>
|
||||||
|
const { createApp } = Vue;
|
||||||
|
const { ElInput, ElButton, ElRow, ElCol, ElMessage } = ElementPlus;
|
||||||
|
|
||||||
|
createApp({
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
text: '',
|
||||||
|
imageUrl: '',
|
||||||
|
websocket: null,
|
||||||
|
reconnectInterval: 10000,
|
||||||
|
uri: "ws://localhost:8000/event"
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.connectWebSocket();
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
connectWebSocket() {
|
||||||
|
this.websocket = new WebSocket(this.uri);
|
||||||
|
this.websocket.onopen = () => {
|
||||||
|
console.log("已连接到服务器。等待接收消息...");
|
||||||
|
};
|
||||||
|
this.websocket.onmessage = (event) => {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
if (data.type === 'text') {
|
||||||
|
this.text += data.content; // + '\n';
|
||||||
|
} else if (data.type === 'image') {
|
||||||
|
this.imageUrl = `data:image/jpeg;base64,${data.content}`;
|
||||||
|
this.text = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.websocket.onclose = () => {
|
||||||
|
console.log("服务器关闭了连接。10秒后重新连接...");
|
||||||
|
setTimeout(() => {
|
||||||
|
this.connectWebSocket();
|
||||||
|
}, this.reconnectInterval);
|
||||||
|
};
|
||||||
|
this.websocket.onerror = (error) => {
|
||||||
|
console.error("WebSocket 错误:", error);
|
||||||
|
this.websocket.close();
|
||||||
|
};
|
||||||
|
},
|
||||||
|
copyText() {
|
||||||
|
navigator.clipboard.writeText(this.text).then(() => {
|
||||||
|
ElMessage.success('文本已复制到剪贴板');
|
||||||
|
}).catch((error) => {
|
||||||
|
ElMessage.error('复制文本失败: ' + error);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clearText() {
|
||||||
|
this.text = '';
|
||||||
|
this.imageUrl = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}).use(ElementPlus).mount('#app');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
36
test.py
Normal file
36
test.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import base64
|
||||||
|
import openai
|
||||||
|
|
||||||
|
with open("test.png", "rb") as f:
|
||||||
|
image_data = f.read()
|
||||||
|
image_base64 = base64.b64encode(image_data).decode('utf-8')
|
||||||
|
with open("key", "r") as f:
|
||||||
|
key = f.read()
|
||||||
|
client = openai.OpenAI(api_key=key,
|
||||||
|
base_url="https://open.bigmodel.cn/api/paas/v4/"
|
||||||
|
)
|
||||||
|
# 构造请求给API
|
||||||
|
response = client.chat.completions.create(
|
||||||
|
model="glm-4v",
|
||||||
|
messages=[
|
||||||
|
{
|
||||||
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{
|
||||||
|
"type": "text",
|
||||||
|
"text": "请描述该图片",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "image_url",
|
||||||
|
"image_url": {
|
||||||
|
"url": f"data:image/jpeg;base64,{image_base64}"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
stream=True
|
||||||
|
)
|
||||||
|
|
||||||
|
for chunk in response:
|
||||||
|
print(chunk.choices[0].delta.content)
|
||||||
6
test/__init__.py
Normal file
6
test/__init__.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
# @Time : 2024/10/25 下午5:18
|
||||||
|
# @Author : 河瞬
|
||||||
|
# @FileName: __init__.py.py
|
||||||
|
# @Software: PyCharm
|
||||||
|
# @Github :
|
||||||
33
test/clientTest.py
Normal file
33
test/clientTest.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import unittest
|
||||||
|
import asyncio
|
||||||
|
import websockets
|
||||||
|
import requests
|
||||||
|
from time import sleep
|
||||||
|
|
||||||
|
def send_post_request():
|
||||||
|
url = "http://localhost:8000/example"
|
||||||
|
data = {"content": "Hello, WebSocket!"}
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
response = requests.post(url, json=data, headers=headers)
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
print(f"Response: {response.json()}")
|
||||||
|
class MyTestCase(unittest.TestCase):
|
||||||
|
def test_something(self):
|
||||||
|
async def test_client():
|
||||||
|
uri = "ws://localhost:8000/event"
|
||||||
|
async with websockets.connect(uri) as websocket:
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
message = await websocket.recv()
|
||||||
|
self.assertGreater(len(message), 0)
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
raise Exception("Connection closed")
|
||||||
|
|
||||||
|
asyncio.run(test_client())
|
||||||
|
sleep(1)
|
||||||
|
send_post_request()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
23
test/test.py
Normal file
23
test/test.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import asyncio
|
||||||
|
import websockets
|
||||||
|
|
||||||
|
async def test_client():
|
||||||
|
uri = "ws://localhost:8000/event"
|
||||||
|
async with websockets.connect(uri) as websocket:
|
||||||
|
print("Connected to server. Waiting for messages...")
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
message = await websocket.recv()
|
||||||
|
print(f"Received message: {message}")
|
||||||
|
except websockets.exceptions.ConnectionClosed:
|
||||||
|
print("Connection closed by the server.")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Keyboard interrupt received, closing connection...")
|
||||||
|
await websocket.close()
|
||||||
|
print("Connection closed.")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
asyncio.run(test_client())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("Program terminated by user.")
|
||||||
13
test/test2.py
Normal file
13
test/test2.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import requests
|
||||||
|
|
||||||
|
def send_post_request():
|
||||||
|
url = "http://localhost:8000/example"
|
||||||
|
data = {"content": "Hello, WebSocket!"}
|
||||||
|
headers = {"Content-Type": "application/json"}
|
||||||
|
|
||||||
|
response = requests.post(url, json=data, headers=headers)
|
||||||
|
print(f"Status Code: {response.status_code}")
|
||||||
|
print(f"Response: {response.json()}")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
send_post_request()
|
||||||
11
test_main.http
Normal file
11
test_main.http
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# Test your FastAPI endpoints
|
||||||
|
|
||||||
|
GET http://127.0.0.1:8000/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
|
|
||||||
|
GET http://127.0.0.1:8000/hello/User
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
Loading…
Reference in New Issue
Block a user