feat: improve chat actions and rule management
This commit is contained in:
+68
-16
@@ -51,6 +51,7 @@ const logsRefreshing = ref(false);
|
|||||||
const copyingLogs = ref(false);
|
const copyingLogs = ref(false);
|
||||||
const logWrap = ref(false);
|
const logWrap = ref(false);
|
||||||
const newMessage = ref('');
|
const newMessage = ref('');
|
||||||
|
const replyTarget = ref<ChatItem | null>(null);
|
||||||
const sending = ref(false);
|
const sending = ref(false);
|
||||||
const restarting = ref(false);
|
const restarting = ref(false);
|
||||||
const restartingBot = ref(false);
|
const restartingBot = ref(false);
|
||||||
@@ -121,7 +122,7 @@ function toDatetimeLocal(date: Date) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pageTitle = computed(() => ({
|
const pageTitle = computed(() => ({
|
||||||
dashboard: '群组安全总览', shop: '商城管理', lottery: '抽奖管理', rules: '规则测试'
|
dashboard: '群组安全总览', shop: '商城管理', lottery: '抽奖管理', rules: '规则管理'
|
||||||
}[activePage.value]));
|
}[activePage.value]));
|
||||||
const pageDesc = computed(() => ({
|
const pageDesc = computed(() => ({
|
||||||
dashboard: '实时审计聊天、封禁、日志和封禁记录', shop: '商品 CRUD 与兑换交易记录', lottery: '抽奖项目、开奖条件与参与记录', rules: '检测广告/灰产文本并维护规则库'
|
dashboard: '实时审计聊天、封禁、日志和封禁记录', shop: '商品 CRUD 与兑换交易记录', lottery: '抽奖项目、开奖条件与参与记录', rules: '检测广告/灰产文本并维护规则库'
|
||||||
@@ -134,6 +135,14 @@ function goPage(page: typeof activePage.value) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function isTransientBotRecord(item: ChatItem) {
|
||||||
|
const text = String(item?.text || '').trim();
|
||||||
|
return Number(item?.user_id || 0) === 0 && ['🔍 正在搜索知识库', '正在搜索知识库', '🔎 正在搜索知识库', '正在整理详细答案'].some(prefix => text.startsWith(prefix));
|
||||||
|
}
|
||||||
|
function visibleChatRows(rows: ChatItem[]) {
|
||||||
|
return rows.filter(item => !isTransientBotRecord(item));
|
||||||
|
}
|
||||||
|
|
||||||
function formatTime(value?: string) {
|
function formatTime(value?: string) {
|
||||||
if (!value) return '-';
|
if (!value) return '-';
|
||||||
const text = String(value);
|
const text = String(value);
|
||||||
@@ -221,7 +230,7 @@ async function fetchJson(url: string, options?: RequestInit) {
|
|||||||
async function loadChatHistory() {
|
async function loadChatHistory() {
|
||||||
try {
|
try {
|
||||||
const data = await fetchJson(`/api/chat${selectedQuery.value}`);
|
const data = await fetchJson(`/api/chat${selectedQuery.value}`);
|
||||||
chatLogs.value = data.map((m: ChatItem) => ({ ...m, _deleting: false }));
|
chatLogs.value = visibleChatRows(data.map((m: ChatItem) => ({ ...m, _deleting: false })));
|
||||||
} catch {}
|
} catch {}
|
||||||
}
|
}
|
||||||
async function loadBans() {
|
async function loadBans() {
|
||||||
@@ -443,6 +452,7 @@ function connectSSE() {
|
|||||||
es.addEventListener('connected', () => { sseConnected.value = true; });
|
es.addEventListener('connected', () => { sseConnected.value = true; });
|
||||||
es.addEventListener('chat', (e) => {
|
es.addEventListener('chat', (e) => {
|
||||||
const data = JSON.parse((e as MessageEvent).data);
|
const data = JSON.parse((e as MessageEvent).data);
|
||||||
|
if (isTransientBotRecord(data)) return;
|
||||||
chatLogs.value.unshift(data);
|
chatLogs.value.unshift(data);
|
||||||
if (chatLogs.value.length > 120) chatLogs.value.pop();
|
if (chatLogs.value.length > 120) chatLogs.value.pop();
|
||||||
});
|
});
|
||||||
@@ -488,9 +498,10 @@ async function sendMessage() {
|
|||||||
await fetchJson('/api/send', {
|
await fetchJson('/api/send', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ text, chat_id: selectedChatId.value })
|
body: JSON.stringify({ text, chat_id: selectedChatId.value, reply_to_message_id: replyTarget.value?.msg_id || null })
|
||||||
});
|
});
|
||||||
newMessage.value = '';
|
newMessage.value = '';
|
||||||
|
replyTarget.value = null;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
showToast(`发送失败:${e.message}`);
|
showToast(`发送失败:${e.message}`);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -545,6 +556,14 @@ async function contextCopyText() {
|
|||||||
showToast('复制失败,浏览器未允许剪贴板权限');
|
showToast('复制失败,浏览器未允许剪贴板权限');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
function contextReplyMessage() {
|
||||||
|
const item = chatContextMenu.item;
|
||||||
|
closeChatContextMenu();
|
||||||
|
if (!item?.msg_id) return showToast('当前消息无法回复');
|
||||||
|
replyTarget.value = item;
|
||||||
|
nextTick(() => document.querySelector<HTMLInputElement>('.floating-chat-send input')?.focus());
|
||||||
|
}
|
||||||
|
function cancelReply() { replyTarget.value = null; }
|
||||||
|
|
||||||
async function banChatMessage(item: ChatItem) {
|
async function banChatMessage(item: ChatItem) {
|
||||||
if (!item.user_id) return showToast('当前记录没有用户 ID,无法封禁');
|
if (!item.user_id) return showToast('当前记录没有用户 ID,无法封禁');
|
||||||
@@ -689,7 +708,7 @@ onBeforeUnmount(() => {
|
|||||||
<button :class="{active: activePage === 'dashboard'}" @click="goPage('dashboard')"><span>总览</span><em>Dashboard</em></button>
|
<button :class="{active: activePage === 'dashboard'}" @click="goPage('dashboard')"><span>总览</span><em>Dashboard</em></button>
|
||||||
<button :class="{active: activePage === 'shop'}" @click="goPage('shop')"><span>商城管理</span><em>Shop</em></button>
|
<button :class="{active: activePage === 'shop'}" @click="goPage('shop')"><span>商城管理</span><em>Shop</em></button>
|
||||||
<button :class="{active: activePage === 'lottery'}" @click="goPage('lottery')"><span>抽奖管理</span><em>Lottery</em></button>
|
<button :class="{active: activePage === 'lottery'}" @click="goPage('lottery')"><span>抽奖管理</span><em>Lottery</em></button>
|
||||||
<button :class="{active: activePage === 'rules'}" @click="goPage('rules')"><span>规则测试</span><em>Rules</em></button>
|
<button :class="{active: activePage === 'rules'}" @click="goPage('rules')"><span>规则管理</span><em>Rules</em></button>
|
||||||
</nav>
|
</nav>
|
||||||
<div class="side-footer">
|
<div class="side-footer">
|
||||||
<span class="conn" :class="sseConnected ? 'on' : 'off'">{{ sseConnected ? '实时连接' : '连接中' }}</span>
|
<span class="conn" :class="sseConnected ? 'on' : 'off'">{{ sseConnected ? '实时连接' : '连接中' }}</span>
|
||||||
@@ -734,7 +753,7 @@ onBeforeUnmount(() => {
|
|||||||
<span class="chat-time">{{ formatTime(item.time) }}</span>
|
<span class="chat-time">{{ formatTime(item.time) }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="chat-text">{{ item.text || '(空消息)' }}</div>
|
<div class="chat-text" :title="item.text || '(空消息)'">{{ item.text || '(空消息)' }}</div>
|
||||||
<div class="chat-foot">
|
<div class="chat-foot">
|
||||||
<STag v-if="item.spam" class="reason-tag" size="xs" color="destructive" variant="soft" :title="item.reason || item.spam_reason">{{ item.reason || item.spam_reason }}</STag>
|
<STag v-if="item.spam" class="reason-tag" size="xs" color="destructive" variant="soft" :title="item.reason || item.spam_reason">{{ item.reason || item.spam_reason }}</STag>
|
||||||
<span v-else />
|
<span v-else />
|
||||||
@@ -797,22 +816,51 @@ onBeforeUnmount(() => {
|
|||||||
@click.stop
|
@click.stop
|
||||||
@contextmenu.prevent
|
@contextmenu.prevent
|
||||||
>
|
>
|
||||||
<button @click="contextCopyText">📋 复制内容</button>
|
<button class="danger" :disabled="!!chatContextMenu.item?.deleted" @click="contextDeleteMessage(false)">🗑️ 删除</button>
|
||||||
|
<button class="warning" :disabled="!chatContextMenu.item?.user_id" @click="contextDeleteAndBanMessage">⛔ 删除并封禁</button>
|
||||||
|
<button class="warning" :disabled="!chatContextMenu.item?.user_id" @click="contextBanMessage">🚫 封印用户</button>
|
||||||
<button @click="contextAddToRules">📥 写入拦截库</button>
|
<button @click="contextAddToRules">📥 写入拦截库</button>
|
||||||
<button v-if="chatContextMenu.item && chatContextMenu.item.user_id" class="warning" @click="contextBanMessage">🚫 封禁用户</button>
|
<button @click="contextCopyText">📋 复制内容</button>
|
||||||
<button v-if="chatContextMenu.item && !chatContextMenu.item.deleted" class="danger" @click="contextDeleteMessage(false)">🗑️ 删除消息</button>
|
<button @click="contextReplyMessage">↩️ 回复</button>
|
||||||
<button v-if="chatContextMenu.item && chatContextMenu.item.user_id" class="warning" @click="contextDeleteAndBanMessage">⛔ 删除并封禁</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<section v-show="activePage === 'rules'" class="tab-page rule-test-page">
|
<section v-show="activePage === 'rules'" class="tab-page rule-manage-page">
|
||||||
<SCard class="panel page-panel" title="🧪 广告/灰产检测" description="检测文本、查看评分,并维护外部规则库">
|
<SCard class="panel page-panel rule-detect-card" title="🧪 规则检测" description="输入聊天内容,检测是否为广告 / 垃圾 / 灰产信息,并可一键写入规则库">
|
||||||
<div class="rule-test-grid">
|
<div class="rule-test-grid">
|
||||||
<div class="rule-editor-block"><label>待检测文本<textarea v-model="spamTestText" class="form-input rule-textarea" placeholder="粘贴可疑聊天内容"></textarea></label><label class="switch-line"><input v-model="spamUseAi" type="checkbox" /> 同时调用 AI 最终检测</label><div class="form-actions"><SButtonLoading color="primary" :loading="spamTesting" :disabled="spamTesting" @click="testSpamText">开始检测</SButtonLoading></div></div>
|
<div class="rule-editor-block">
|
||||||
<div class="rule-result-card"><div v-if="!spamTestResult" class="empty">暂无检测结果</div><template v-else><div class="rule-result-head"><STag :color="spamTestResult.final_is_spam ? 'destructive' : spamTestResult.final_is_spam === null ? 'warning' : 'success'" variant="soft">{{ spamTestResult.final_is_spam ? '广告/灰产' : spamTestResult.final_is_spam === null ? '需 AI 判断' : '正常' }}</STag><strong>规则评分:{{ spamTestResult.score }}</strong></div><div class="rule-reason"><b>本地原因</b><span>{{ spamTestResult.reason }}</span></div><div v-if="spamTestResult.ai_reason" class="rule-reason"><b>AI 结果</b><span>{{ spamTestResult.ai_reason }}</span></div><div class="rule-counts">当前规则:关键词 {{ spamTestResult.rule_counts?.keywords }} 条 / 正则 {{ spamTestResult.rule_counts?.regex }} 条</div></template></div>
|
<label>聊天内容<textarea v-model="spamTestText" class="form-input rule-textarea" placeholder="粘贴可疑聊天内容,系统会给出规则评分和命中原因"></textarea></label>
|
||||||
|
<label class="switch-line"><input v-model="spamUseAi" type="checkbox" /> 同时调用 AI 最终检测</label>
|
||||||
|
<div class="form-actions">
|
||||||
|
<SButtonLoading color="primary" :loading="spamTesting" :disabled="spamTesting" @click="testSpamText">开始检测</SButtonLoading>
|
||||||
|
<SButtonLoading variant="outline" color="warning" :loading="ruleSaving" :disabled="ruleSaving || !spamTestText.trim()" @click="ruleForm.pattern = spamTestText; ruleForm.type = 'keyword'; ruleForm.score = 90; saveSpamRule()">将内容写入规则库</SButtonLoading>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rule-result-card">
|
||||||
|
<div v-if="!spamTestResult" class="empty">暂无检测结果</div>
|
||||||
|
<template v-else>
|
||||||
|
<div class="rule-result-head"><STag :color="spamTestResult.final_is_spam ? 'destructive' : spamTestResult.final_is_spam === null ? 'warning' : 'success'" variant="soft">{{ spamTestResult.final_is_spam ? '广告/垃圾/灰产' : spamTestResult.final_is_spam === null ? '需 AI 判断' : '正常' }}</STag><strong>评分:{{ spamTestResult.score }}</strong></div>
|
||||||
|
<div class="rule-reason"><b>本地命中</b><span>{{ spamTestResult.reason }}</span></div>
|
||||||
|
<div v-if="spamTestResult.ai_reason" class="rule-reason"><b>AI 结果</b><span>{{ spamTestResult.ai_reason }}</span></div>
|
||||||
|
<div class="rule-counts">规则库:关键词 {{ spamTestResult.rule_counts?.keywords }} 条 / 正则 {{ spamTestResult.rule_counts?.regex }} 条</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="rule-write-grid inline-rule-writer">
|
||||||
|
<label>类型<select v-model="ruleForm.type" class="form-select"><option value="keyword">关键词</option><option value="regex">正则</option></select></label>
|
||||||
|
<label>分数<SInput v-model="ruleForm.score" type="number" placeholder="75" /></label>
|
||||||
|
<label class="rule-pattern-field">规则条目<SInput v-model="ruleForm.pattern" :placeholder="ruleForm.type === 'keyword' ? '如 黑U' : '如 (?:黑\s*[uU]).{0,20}(?:联系|项目)'" /></label>
|
||||||
|
<SButtonLoading color="primary" :loading="ruleSaving" :disabled="ruleSaving" @click="saveSpamRule">写入规则</SButtonLoading>
|
||||||
|
</div>
|
||||||
|
</SCard>
|
||||||
|
|
||||||
|
<SCard class="panel page-panel rule-library-card" title="📚 规则库" :description="`当前拦截规则:关键词 ${spamRules.counts.keywords} 条 / 正则 ${spamRules.counts.regex} 条`">
|
||||||
|
<template #extra><SButtonLoading size="xs" variant="outline" :loading="rulesLoading" :disabled="rulesLoading" @click="loadSpamRules">刷新</SButtonLoading></template>
|
||||||
|
<div class="rule-library-toolbar"><SInput v-model="ruleKeyword" placeholder="搜索规则条目" /></div>
|
||||||
|
<div class="rule-library-grid">
|
||||||
|
<div><h4>关键词规则</h4><div class="rule-list"><div v-for="r in filteredKeywordRules" :key="`kw-${r.line}-${r.pattern}`" class="rule-row"><span :title="r.pattern">{{ r.pattern }}</span><em>{{ r.score || '-' }}</em></div><div v-if="!filteredKeywordRules.length" class="empty">暂无关键词规则</div></div></div>
|
||||||
|
<div><h4>正则规则</h4><div class="rule-list"><div v-for="r in filteredRegexRules" :key="`re-${r.line}-${r.pattern}`" class="rule-row"><span :title="r.pattern">{{ r.pattern }}</span><em>{{ r.score || '-' }}</em></div><div v-if="!filteredRegexRules.length" class="empty">暂无正则规则</div></div></div>
|
||||||
</div>
|
</div>
|
||||||
</SCard>
|
</SCard>
|
||||||
<SCard class="panel page-panel rule-write-card" title="➕ 写入规则库" description="关键词适合固定词,正则适合组合话术;写入后自动热加载"><div class="rule-write-grid"><label>类型<select v-model="ruleForm.type" class="form-select"><option value="keyword">关键词</option><option value="regex">正则</option></select></label><label>分数<SInput v-model="ruleForm.score" type="number" placeholder="75" /></label><label class="rule-pattern-field">规则内容<SInput v-model="ruleForm.pattern" :placeholder="ruleForm.type === 'keyword' ? '如 黑U' : '如 (?:黑\s*[uU]).{0,20}(?:联系|项目)'" /></label><SButtonLoading color="primary" :loading="ruleSaving" :disabled="ruleSaving" @click="saveSpamRule">写入规则</SButtonLoading></div><p class="field-help">建议:强灰产 80~100,普通引流 45~70;不要添加过短泛词。</p></SCard>
|
|
||||||
<SCard class="panel page-panel rule-library-card" title="📚 当前规则库" :description="`关键词 ${spamRules.counts.keywords} 条 / 正则 ${spamRules.counts.regex} 条`"><template #extra><SButtonLoading size="xs" variant="outline" :loading="rulesLoading" :disabled="rulesLoading" @click="loadSpamRules">刷新</SButtonLoading></template><div class="rule-library-toolbar"><SInput v-model="ruleKeyword" placeholder="搜索规则内容" /></div><div class="rule-library-grid"><div><h4>关键词规则</h4><div class="rule-list"><div v-for="r in filteredKeywordRules" :key="`kw-${r.line}`" class="rule-row"><span>{{ r.pattern }}</span><em>{{ r.score ?? '-' }}</em></div></div></div><div><h4>正则规则</h4><div class="rule-list"><div v-for="r in filteredRegexRules" :key="`rx-${r.line}`" class="rule-row"><span>{{ r.pattern }}</span><em>{{ r.score ?? '-' }}</em></div></div></div></div></SCard>
|
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section v-show="activePage === 'shop'" class="tab-page manager-layout single-panel-page">
|
<section v-show="activePage === 'shop'" class="tab-page manager-layout single-panel-page">
|
||||||
@@ -851,7 +899,11 @@ onBeforeUnmount(() => {
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<form v-if="activePage === 'dashboard'" class="send-box floating-chat-send" @submit.prevent="sendMessage">
|
<form v-if="activePage === 'dashboard'" class="send-box floating-chat-send" @submit.prevent="sendMessage">
|
||||||
<SInput v-model="newMessage" placeholder="发送消息到 TG 群" />
|
<div v-if="replyTarget" class="reply-chip">
|
||||||
|
<span>回复 @{{ replyTarget.username || replyTarget.first_name || replyTarget.user_id }}:{{ replyTarget.text || '(空消息)' }}</span>
|
||||||
|
<button type="button" @click="cancelReply">×</button>
|
||||||
|
</div>
|
||||||
|
<SInput v-model="newMessage" :placeholder="replyTarget ? '输入回复内容' : '发送消息到 TG 群'" />
|
||||||
<SButton type="submit" color="primary" :loading="sending" :disabled="sending || !newMessage.trim()">发送</SButton>
|
<SButton type="submit" color="primary" :loading="sending" :disabled="sending || !newMessage.trim()">发送</SButton>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|||||||
@@ -458,3 +458,13 @@ body { background: #f3f6fb; }
|
|||||||
.participant-row span { color: #64748b; font-size: 12px; }
|
.participant-row span { color: #64748b; font-size: 12px; }
|
||||||
.participant-meta { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; color: #64748b; font-size: 12px; }
|
.participant-meta { flex: 0 0 auto; display: flex; align-items: center; gap: 8px; color: #64748b; font-size: 12px; }
|
||||||
.participants-dialog .confirm-actions { padding: 10px 14px 14px; border-top: 1px solid #e2e8f0; }
|
.participants-dialog .confirm-actions { padding: 10px 14px 14px; border-top: 1px solid #e2e8f0; }
|
||||||
|
|
||||||
|
/* Chat text clamp and reply composer */
|
||||||
|
.chat-text { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; max-height: calc(1.38em * 3); overflow: hidden; }
|
||||||
|
.send-box.floating-chat-send { grid-template-columns: minmax(0, 1fr) auto; }
|
||||||
|
.reply-chip { grid-column: 1 / -1; display: flex; align-items: center; justify-content: space-between; gap: 8px; min-width: 0; padding: 6px 9px; border-radius: 10px; background: #eef2ff; color: #3730a3; font-size: 12px; }
|
||||||
|
.reply-chip span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.reply-chip button { border: 0; background: transparent; color: #64748b; cursor: pointer; font-size: 16px; line-height: 1; }
|
||||||
|
.rule-manage-page { display: grid; gap: 12px; }
|
||||||
|
.inline-rule-writer { margin-top: 12px; padding-top: 12px; border-top: 1px solid #e2e8f0; }
|
||||||
|
.chat-context-menu button:disabled { opacity: .45; cursor: not-allowed; }
|
||||||
|
|||||||
@@ -8,6 +8,20 @@ import config
|
|||||||
|
|
||||||
logger = logging.getLogger("spam_guard")
|
logger = logging.getLogger("spam_guard")
|
||||||
|
|
||||||
|
TRANSIENT_BOT_RECORD_PREFIXES = (
|
||||||
|
"🔍 正在搜索知识库",
|
||||||
|
"正在搜索知识库",
|
||||||
|
"🔎 正在搜索知识库",
|
||||||
|
"正在整理详细答案",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def should_record_bot_message(text: str) -> bool:
|
||||||
|
body = (text or "").strip()
|
||||||
|
if not body:
|
||||||
|
return False
|
||||||
|
return not any(body.startswith(prefix) for prefix in TRANSIENT_BOT_RECORD_PREFIXES)
|
||||||
|
|
||||||
|
|
||||||
async def auto_delete(message, seconds: int | None = None) -> None:
|
async def auto_delete(message, seconds: int | None = None) -> None:
|
||||||
"""延迟删除消息。删除失败不影响主流程。"""
|
"""延迟删除消息。删除失败不影响主流程。"""
|
||||||
@@ -55,6 +69,8 @@ async def record_bot_message(message, text: str | None = None, username: str = "
|
|||||||
from web.api import broadcast_sse, json_safe
|
from web.api import broadcast_sse, json_safe
|
||||||
|
|
||||||
body = text if text is not None else (getattr(message, "text", None) or getattr(message, "caption", None) or "")
|
body = text if text is not None else (getattr(message, "text", None) or getattr(message, "caption", None) or "")
|
||||||
|
if not should_record_bot_message(str(body)):
|
||||||
|
return None
|
||||||
rec = await Message.get_or_none(chat_id=message.chat_id, msg_id=message.message_id)
|
rec = await Message.get_or_none(chat_id=message.chat_id, msg_id=message.message_id)
|
||||||
if rec:
|
if rec:
|
||||||
return rec
|
return rec
|
||||||
|
|||||||
+7
-2
@@ -233,6 +233,7 @@ class LoginPayload(BaseModel):
|
|||||||
class SendPayload(BaseModel):
|
class SendPayload(BaseModel):
|
||||||
text: str = Field(default='', max_length=4000)
|
text: str = Field(default='', max_length=4000)
|
||||||
chat_id: int | None = None
|
chat_id: int | None = None
|
||||||
|
reply_to_message_id: int | None = None
|
||||||
|
|
||||||
|
|
||||||
class ShopItemPayload(BaseModel):
|
class ShopItemPayload(BaseModel):
|
||||||
@@ -729,8 +730,12 @@ async def api_send(data: SendPayload):
|
|||||||
if len(text) > 3500:
|
if len(text) > 3500:
|
||||||
return JSONResponse({"ok": False, "error": "消息太长"}, status_code=400)
|
return JSONResponse({"ok": False, "error": "消息太长"}, status_code=400)
|
||||||
|
|
||||||
chat_id = int(data.get("chat_id") or config.TG_CHAT_ID)
|
chat_id = int(data.chat_id or config.TG_CHAT_ID)
|
||||||
msg = await run_tg_request(lambda bot: bot.send_message(chat_id=chat_id, text=text), label="send_message")
|
reply_to_message_id = data.reply_to_message_id
|
||||||
|
msg = await run_tg_request(
|
||||||
|
lambda bot: bot.send_message(chat_id=chat_id, text=text, reply_to_message_id=reply_to_message_id),
|
||||||
|
label="send_message",
|
||||||
|
)
|
||||||
rec = await Message.create(
|
rec = await Message.create(
|
||||||
msg_id=msg.message_id,
|
msg_id=msg.message_id,
|
||||||
chat_id=chat_id,
|
chat_id=chat_id,
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -5,8 +5,8 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/static/icon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/static/icon.svg" />
|
||||||
<title>TG Spam Guard</title>
|
<title>TG Spam Guard</title>
|
||||||
<script type="module" crossorigin src="/static/assets/index-BtxpMffg.js"></script>
|
<script type="module" crossorigin src="/static/assets/index-CLi70OmX.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/static/assets/index-es9pSdo5.css">
|
<link rel="stylesheet" crossorigin href="/static/assets/index-NLxQkQW1.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
Reference in New Issue
Block a user