mirror of
https://github.com/kuaifan/dootask.git
synced 2025-12-15 21:32:49 +00:00
perf: 消息api支持markdown
This commit is contained in:
parent
d2e04843a4
commit
a5be461021
@ -20,7 +20,6 @@ use Carbon\Carbon;
|
|||||||
use DB;
|
use DB;
|
||||||
use Redirect;
|
use Redirect;
|
||||||
use Request;
|
use Request;
|
||||||
use Str;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @apiDefine dialog
|
* @apiDefine dialog
|
||||||
@ -700,6 +699,7 @@ class DialogController extends AbstractController
|
|||||||
$text = trim(Request::input('text'));
|
$text = trim(Request::input('text'));
|
||||||
$text_type = strtolower(trim(Request::input('text_type')));
|
$text_type = strtolower(trim(Request::input('text_type')));
|
||||||
$silence = in_array(strtolower(trim(Request::input('silence'))), ['yes', 'true', '1']);
|
$silence = in_array(strtolower(trim(Request::input('silence'))), ['yes', 'true', '1']);
|
||||||
|
$markdown = in_array($text_type, ['md', 'markdown']);
|
||||||
//
|
//
|
||||||
WebSocketDialog::checkDialog($dialog_id);
|
WebSocketDialog::checkDialog($dialog_id);
|
||||||
//
|
//
|
||||||
@ -711,11 +711,9 @@ class DialogController extends AbstractController
|
|||||||
$action = "";
|
$action = "";
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
if (in_array($text_type, ['md', 'markdown'])) {
|
if (!$markdown) {
|
||||||
$text = Str::markdown($text);
|
$text = WebSocketDialogMsg::formatMsg($text, $dialog_id);
|
||||||
$text = preg_replace("/\>\r?\n\s*+\</", "><", $text);
|
|
||||||
}
|
}
|
||||||
$text = WebSocketDialogMsg::formatMsg($text, $dialog_id);
|
|
||||||
$strlen = mb_strlen($text);
|
$strlen = mb_strlen($text);
|
||||||
$noimglen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
|
$noimglen = mb_strlen(preg_replace("/<img[^>]*?>/i", "", $text));
|
||||||
if ($strlen < 1) {
|
if ($strlen < 1) {
|
||||||
@ -735,8 +733,9 @@ class DialogController extends AbstractController
|
|||||||
if (empty($size)) {
|
if (empty($size)) {
|
||||||
return Base::retError('消息发送保存失败');
|
return Base::retError('消息发送保存失败');
|
||||||
}
|
}
|
||||||
|
$ext = $markdown ? 'md' : 'htm';
|
||||||
$fileData = [
|
$fileData = [
|
||||||
'name' => "LongText-{$strlen}.htm",
|
'name' => "LongText-{$strlen}.{$ext}",
|
||||||
'size' => $size,
|
'size' => $size,
|
||||||
'file' => $file,
|
'file' => $file,
|
||||||
'path' => $path,
|
'path' => $path,
|
||||||
@ -744,12 +743,16 @@ class DialogController extends AbstractController
|
|||||||
'thumb' => '',
|
'thumb' => '',
|
||||||
'width' => -1,
|
'width' => -1,
|
||||||
'height' => -1,
|
'height' => -1,
|
||||||
'ext' => 'htm',
|
'ext' => $ext,
|
||||||
];
|
];
|
||||||
return WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid, false, false, $silence);
|
return WebSocketDialogMsg::sendMsg($action, $dialog_id, 'file', $fileData, $user->userid, false, false, $silence);
|
||||||
}
|
}
|
||||||
//
|
//
|
||||||
return WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', ['text' => $text], $user->userid, false, false, $silence);
|
$msgData = ['text' => $text];
|
||||||
|
if ($markdown) {
|
||||||
|
$msgData['type'] = 'md';
|
||||||
|
}
|
||||||
|
return WebSocketDialogMsg::sendMsg($action, $dialog_id, 'text', $msgData, $user->userid, false, false, $silence);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -19,6 +19,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@chenfengyuan/vue-qrcode": "^1.0.2",
|
"@chenfengyuan/vue-qrcode": "^1.0.2",
|
||||||
|
"@traptitech/markdown-it-katex": "^3.6.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"axios": "^0.24.0",
|
"axios": "^0.24.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
@ -27,6 +28,7 @@
|
|||||||
"echarts": "^5.2.2",
|
"echarts": "^5.2.2",
|
||||||
"element-ui": "git+https://github.com/kuaifan/element.git#master",
|
"element-ui": "git+https://github.com/kuaifan/element.git#master",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
|
"highlight.js": "^11.7.0",
|
||||||
"inquirer": "^8.2.0",
|
"inquirer": "^8.2.0",
|
||||||
"internal-ip": "^6.2.0",
|
"internal-ip": "^6.2.0",
|
||||||
"jquery": "^3.6.4",
|
"jquery": "^3.6.4",
|
||||||
@ -36,6 +38,8 @@
|
|||||||
"less-loader": "^10.2.0",
|
"less-loader": "^10.2.0",
|
||||||
"localforage": "^1.10.0",
|
"localforage": "^1.10.0",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
|
"markdown-it": "^13.0.1",
|
||||||
|
"markdown-it-link-attributes": "^4.0.1",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.1",
|
||||||
"node-sass": "^6.0.1",
|
"node-sass": "^6.0.1",
|
||||||
"notification-koro1": "^1.1.1",
|
"notification-koro1": "^1.1.1",
|
||||||
|
|||||||
@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<div class="markdown-body" v-html="html"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import '../../../../sass/pages/components/dialog-markdown/markdown.less'
|
||||||
|
import MarkdownIt from 'markdown-it'
|
||||||
|
import mdKatex from '@traptitech/markdown-it-katex'
|
||||||
|
import mila from 'markdown-it-link-attributes'
|
||||||
|
import hljs from 'highlight.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "DialogMarkdown",
|
||||||
|
props: {
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
mdi: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.copyCodeBlock()
|
||||||
|
},
|
||||||
|
|
||||||
|
updated() {
|
||||||
|
this.copyCodeBlock()
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
html() {
|
||||||
|
const {text} = this
|
||||||
|
if (this.mdi === null) {
|
||||||
|
const {highlightBlock} = this
|
||||||
|
this.mdi = new MarkdownIt({
|
||||||
|
linkify: true,
|
||||||
|
highlight(code, language) {
|
||||||
|
const validLang = !!(language && hljs.getLanguage(language))
|
||||||
|
if (validLang) {
|
||||||
|
const lang = language ?? ''
|
||||||
|
return highlightBlock(hljs.highlight(code, {language: lang}).value, lang)
|
||||||
|
}
|
||||||
|
return highlightBlock(hljs.highlightAuto(code).value, '')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
this.mdi.use(mila, {attrs: {target: '_blank', rel: 'noopener'}})
|
||||||
|
this.mdi.use(mdKatex, {blockClass: 'katexmath-block rounded-md p-[10px]', errorColor: ' #cc0000'})
|
||||||
|
}
|
||||||
|
return this.mdi.render(text)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
highlightBlock(str, lang = '') {
|
||||||
|
return `<pre class="code-block-wrapper"><div class="code-block-header"><span class="code-block-header__lang">${lang}</span><span class="code-block-header__copy">${this.$L('复制代码')}</span></div><code class="hljs code-block-body ${lang}">${str}</code></pre>`
|
||||||
|
},
|
||||||
|
|
||||||
|
copyCodeBlock() {
|
||||||
|
const codeBlockWrapper = this.$el.querySelectorAll('.code-block-wrapper')
|
||||||
|
codeBlockWrapper.forEach((wrapper) => {
|
||||||
|
const copyBtn = wrapper.querySelector('.code-block-header__copy')
|
||||||
|
const codeBlock = wrapper.querySelector('.code-block-body')
|
||||||
|
if (copyBtn && codeBlock && copyBtn.getAttribute("data-copy") !== "click") {
|
||||||
|
copyBtn.setAttribute("data-copy", "click")
|
||||||
|
copyBtn.addEventListener('click', () => {
|
||||||
|
if (navigator.clipboard?.writeText)
|
||||||
|
navigator.clipboard.writeText(codeBlock.textContent ?? '')
|
||||||
|
else
|
||||||
|
this.copyText({text: codeBlock.textContent ?? '', origin: true})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
},
|
||||||
|
|
||||||
|
copyText(options) {
|
||||||
|
const props = {origin: true, ...options}
|
||||||
|
|
||||||
|
let input
|
||||||
|
|
||||||
|
if (props.origin)
|
||||||
|
input = document.createElement('textarea')
|
||||||
|
else
|
||||||
|
input = document.createElement('input')
|
||||||
|
|
||||||
|
input.setAttribute('readonly', 'readonly')
|
||||||
|
input.value = props.text
|
||||||
|
document.body.appendChild(input)
|
||||||
|
input.select()
|
||||||
|
if (document.execCommand('copy'))
|
||||||
|
document.execCommand('copy')
|
||||||
|
document.body.removeChild(input)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -18,7 +18,8 @@
|
|||||||
<div class="dialog-content" :class="contentClass">
|
<div class="dialog-content" :class="contentClass">
|
||||||
<!--文本-->
|
<!--文本-->
|
||||||
<div v-if="msgData.type === 'text'" class="content-text no-dark-content">
|
<div v-if="msgData.type === 'text'" class="content-text no-dark-content">
|
||||||
<pre @click="viewText" v-html="$A.formatTextMsg(msgData.msg.text, userId)"></pre>
|
<DialogMarkdown v-if="msgData.msg.type === 'md'" @click="viewText" :text="msgData.msg.text"/>
|
||||||
|
<pre v-else @click="viewText" v-html="$A.formatTextMsg(msgData.msg.text, userId)"></pre>
|
||||||
</div>
|
</div>
|
||||||
<!--文件-->
|
<!--文件-->
|
||||||
<div v-else-if="msgData.type === 'file'" :class="`content-file ${msgData.msg.type}`">
|
<div v-else-if="msgData.type === 'file'" :class="`content-file ${msgData.msg.type}`">
|
||||||
@ -177,10 +178,11 @@ import WCircle from "../../../components/WCircle";
|
|||||||
import {mapGetters, mapState} from "vuex";
|
import {mapGetters, mapState} from "vuex";
|
||||||
import {Store} from "le5le-store";
|
import {Store} from "le5le-store";
|
||||||
import longpress from "../../../directives/longpress";
|
import longpress from "../../../directives/longpress";
|
||||||
|
import DialogMarkdown from "./DialogMarkdown.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "DialogView",
|
name: "DialogView",
|
||||||
components: {WCircle},
|
components: {DialogMarkdown, WCircle},
|
||||||
directives: {longpress},
|
directives: {longpress},
|
||||||
props: {
|
props: {
|
||||||
msgData: {
|
msgData: {
|
||||||
|
|||||||
1102
resources/assets/sass/pages/components/dialog-markdown/github.less
Normal file
1102
resources/assets/sass/pages/components/dialog-markdown/github.less
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,203 @@
|
|||||||
|
body.dark-mode-reverse {
|
||||||
|
.markdown-body {
|
||||||
|
pre code.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
color: #abb2bf;
|
||||||
|
background: #282c34
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-keyword,
|
||||||
|
.hljs-operator,
|
||||||
|
.hljs-pattern-match {
|
||||||
|
color: #f92672
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-function,
|
||||||
|
.hljs-pattern-match .hljs-constructor {
|
||||||
|
color: #61aeee
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-function .hljs-params {
|
||||||
|
color: #a6e22e
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-function .hljs-params .hljs-typing {
|
||||||
|
color: #fd971f
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-module-access .hljs-module {
|
||||||
|
color: #7e57c2
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-constructor {
|
||||||
|
color: #e2b93d
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-constructor .hljs-string {
|
||||||
|
color: #9ccc65
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: #b18eb1;
|
||||||
|
font-style: italic
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-formula {
|
||||||
|
color: #c678dd
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-section,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-subst {
|
||||||
|
color: #e06c75
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-literal {
|
||||||
|
color: #56b6c2
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-addition,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-meta .hljs-string,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-string {
|
||||||
|
color: #98c379
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-class .hljs-title,
|
||||||
|
.hljs-title.class_ {
|
||||||
|
color: #e6c07b
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-class,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-variable {
|
||||||
|
color: #d19a66
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-title {
|
||||||
|
color: #61aeee
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: 700
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-link {
|
||||||
|
text-decoration: underline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
.markdown-body {
|
||||||
|
pre code.hljs {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.hljs {
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
height: 4px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs {
|
||||||
|
color: #383a42;
|
||||||
|
background: #ffffff
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-comment,
|
||||||
|
.hljs-quote {
|
||||||
|
color: #a0a1a7;
|
||||||
|
font-style: italic
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-doctag,
|
||||||
|
.hljs-formula,
|
||||||
|
.hljs-keyword {
|
||||||
|
color: #a626a4
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-deletion,
|
||||||
|
.hljs-name,
|
||||||
|
.hljs-section,
|
||||||
|
.hljs-selector-tag,
|
||||||
|
.hljs-subst {
|
||||||
|
color: #e45649
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-literal {
|
||||||
|
color: #0184bb
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-addition,
|
||||||
|
.hljs-attribute,
|
||||||
|
.hljs-meta .hljs-string,
|
||||||
|
.hljs-regexp,
|
||||||
|
.hljs-string {
|
||||||
|
color: #50a14f
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-attr,
|
||||||
|
.hljs-number,
|
||||||
|
.hljs-selector-attr,
|
||||||
|
.hljs-selector-class,
|
||||||
|
.hljs-selector-pseudo,
|
||||||
|
.hljs-template-variable,
|
||||||
|
.hljs-type,
|
||||||
|
.hljs-variable {
|
||||||
|
color: #986801
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-bullet,
|
||||||
|
.hljs-link,
|
||||||
|
.hljs-meta,
|
||||||
|
.hljs-selector-id,
|
||||||
|
.hljs-symbol,
|
||||||
|
.hljs-title {
|
||||||
|
color: #4078f2
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-built_in,
|
||||||
|
.hljs-class .hljs-title,
|
||||||
|
.hljs-title.class_ {
|
||||||
|
color: #c18401
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-emphasis {
|
||||||
|
font-style: italic
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-strong {
|
||||||
|
font-weight: 700
|
||||||
|
}
|
||||||
|
|
||||||
|
.hljs-link {
|
||||||
|
text-decoration: underline
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,81 @@
|
|||||||
|
@import "highlight";
|
||||||
|
@import "github";
|
||||||
|
|
||||||
|
body {
|
||||||
|
&.dark-mode-reverse {
|
||||||
|
.markdown-body {
|
||||||
|
color: #ffffff;
|
||||||
|
.highlight pre,
|
||||||
|
pre {
|
||||||
|
background-color: #282c34;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.markdown-body {
|
||||||
|
color: #303133;
|
||||||
|
background-color: transparent;
|
||||||
|
font-size: 14px;
|
||||||
|
|
||||||
|
p {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol {
|
||||||
|
list-style-type: decimal;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
list-style-type: disc;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre code,
|
||||||
|
pre tt {
|
||||||
|
line-height: 1.65;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight pre,
|
||||||
|
pre {
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
code.hljs {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-block {
|
||||||
|
&-wrapper {
|
||||||
|
position: relative;
|
||||||
|
padding-top: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&-header {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 0;
|
||||||
|
width: 100%;
|
||||||
|
padding: 0 1rem;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
color: #b3b3b3;
|
||||||
|
|
||||||
|
&__copy {
|
||||||
|
cursor: pointer;
|
||||||
|
margin-left: 0.5rem;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #65a665;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.self {
|
||||||
|
.markdown-body {
|
||||||
|
color: #ffffff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -510,6 +510,7 @@
|
|||||||
min-width: 32px;
|
min-width: 32px;
|
||||||
border-radius: 2px 8px 8px 8px;
|
border-radius: 2px 8px 8px 8px;
|
||||||
transition: box-shadow 0.3s ease;
|
transition: box-shadow 0.3s ease;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
&.transparent {
|
&.transparent {
|
||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
@ -586,6 +587,7 @@
|
|||||||
.content-text {
|
.content-text {
|
||||||
color: $primary-title-color;
|
color: $primary-title-color;
|
||||||
padding: 2px;
|
padding: 2px;
|
||||||
|
max-width: 100%;
|
||||||
|
|
||||||
> pre {
|
> pre {
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
@ -94,6 +94,7 @@
|
|||||||
--header 'token: <span style="color:#84c56a">{机器人Token}</span>' \
|
--header 'token: <span style="color:#84c56a">{机器人Token}</span>' \
|
||||||
--form 'dialog_id="<span style="color:#84c56a">{对话ID}</span>"' \
|
--form 'dialog_id="<span style="color:#84c56a">{对话ID}</span>"' \
|
||||||
--form 'text="<span style="color:#84c56a">{消息内容}</span>"'
|
--form 'text="<span style="color:#84c56a">{消息内容}</span>"'
|
||||||
|
--form 'text_type="<span style="color:#84c56a">[md|html]</span>"'
|
||||||
--form 'silence="<span style="color:#84c56a">[yes|no]</span>"'
|
--form 'silence="<span style="color:#84c56a">[yes|no]</span>"'
|
||||||
|
|
||||||
<b>Webhook说明:</b>
|
<b>Webhook说明:</b>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user