tgStart ждёт старт QR до показа картинки (устранена гонка с 404). Вкладка Telegram опрашивает статус и обновляет QR по таймеру, не останавливаясь до авторизации. Статус: account берётся из живого статуса сервиса, connected = phase ready (авторизация), а не транспорт.
This commit is contained in:
@@ -22,6 +22,7 @@ public sealed class TgStatusService(
|
|||||||
TelegramKeysService keys)
|
TelegramKeysService keys)
|
||||||
{
|
{
|
||||||
private const string IdlePhase = "idle";
|
private const string IdlePhase = "idle";
|
||||||
|
private const string ReadyPhase = "ready";
|
||||||
|
|
||||||
// Опции JSON KV-значений статуса: camelCase (как пишет ингресс) + терпимость регистра.
|
// Опции JSON KV-значений статуса: camelCase (как пишет ингресс) + терпимость регистра.
|
||||||
private static readonly JsonSerializerOptions KvJsonOptions = new()
|
private static readonly JsonSerializerOptions KvJsonOptions = new()
|
||||||
@@ -37,13 +38,19 @@ public sealed class TgStatusService(
|
|||||||
public async Task<TgStatusDto> GetAsync(CancellationToken ct)
|
public async Task<TgStatusDto> GetAsync(CancellationToken ct)
|
||||||
{
|
{
|
||||||
TelegramAccountStatusDto live = await ReadLiveAsync(ct).ConfigureAwait(false);
|
TelegramAccountStatusDto live = await ReadLiveAsync(ct).ConfigureAwait(false);
|
||||||
string account = await ReadAccountAsync(ct).ConfigureAwait(false);
|
// «Подключён» для UI = авторизован (phase ready). Транспортный connected сервиса
|
||||||
|
// означает лишь живость соединения и не гарантирует вход — в UI он даёт «зависание».
|
||||||
|
bool authorized = string.Equals(live.Phase, ReadyPhase, StringComparison.Ordinal);
|
||||||
|
// Живой account (имя из Telegram) приоритетнее KV: при QR-входе KV ещё не заполнен.
|
||||||
|
string account = string.IsNullOrEmpty(live.Account)
|
||||||
|
? await ReadAccountAsync(ct).ConfigureAwait(false)
|
||||||
|
: live.Account;
|
||||||
int monitored = (await dialogs.ListMonitoredIdsAsync(ct).ConfigureAwait(false)).Count;
|
int monitored = (await dialogs.ListMonitoredIdsAsync(ct).ConfigureAwait(false)).Count;
|
||||||
TgKeysSnapshot snapshot = await keys.GetAsync(ct).ConfigureAwait(false);
|
TgKeysSnapshot snapshot = await keys.GetAsync(ct).ConfigureAwait(false);
|
||||||
|
|
||||||
return new TgStatusDto(
|
return new TgStatusDto(
|
||||||
Phase: live.Phase,
|
Phase: live.Phase,
|
||||||
Connected: live.Connected,
|
Connected: authorized,
|
||||||
Listener: live.Listener,
|
Listener: live.Listener,
|
||||||
Account: account,
|
Account: account,
|
||||||
Monitored: monitored,
|
Monitored: monitored,
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
// Вкладка «Telegram» настроек: подключение аккаунта (QR / номер / код /
|
// Вкладка «Telegram» настроек: подключение аккаунта (QR / номер / код /
|
||||||
// облачный пароль) и авто-мониторинг новых чатов. Ключи приложения
|
// облачный пароль) и авто-мониторинг новых чатов. Ключи приложения
|
||||||
// (api_id/api_hash) задаёт оператор глобально — у тенанта их нет.
|
// (api_id/api_hash) задаёт оператор глобально — у тенанта их нет.
|
||||||
// Опрос статуса при QR-входе живёт в SettingsView (watch на state.tgState),
|
// Пока идёт QR-вход, вкладка сама опрашивает статус и обновляет картинку.
|
||||||
// чтобы поведение при переключении вкладок осталось прежним.
|
import { onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { ref } from 'vue'
|
import { state, connectTgStep, tgStart, disconnectTg, refreshTgStatus } from '../../store.js'
|
||||||
import { state, connectTgStep, tgStart, disconnectTg } from '../../store.js'
|
|
||||||
import Icon from '../Icon.vue'
|
import Icon from '../Icon.vue'
|
||||||
import UiToggle from '../ui/ToggleSwitch.vue'
|
import UiToggle from '../ui/ToggleSwitch.vue'
|
||||||
|
|
||||||
@@ -14,6 +13,58 @@ const code = ref('')
|
|||||||
const tgPass = ref('')
|
const tgPass = ref('')
|
||||||
const qrTick = ref(0)
|
const qrTick = ref(0)
|
||||||
|
|
||||||
|
// QR-ссылка приходит после старта: перезагружаем картинку, когда она появилась/сменилась.
|
||||||
|
watch(
|
||||||
|
() => state.tgQrUrl,
|
||||||
|
() => {
|
||||||
|
if (state.tgState === 'qr') qrTick.value++
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Таймер QR: пока идёт вход — периодически обновляем статус и саму картинку
|
||||||
|
// (токен Telegram меняется); при подключении показываем учётку и останавливаемся.
|
||||||
|
const QR_POLL_MS = 5000
|
||||||
|
let qrTimer = null
|
||||||
|
|
||||||
|
function stopQrTimer() {
|
||||||
|
if (qrTimer) {
|
||||||
|
clearInterval(qrTimer)
|
||||||
|
qrTimer = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startQrTimer() {
|
||||||
|
stopQrTimer()
|
||||||
|
qrTimer = setInterval(async () => {
|
||||||
|
await refreshTgStatus()
|
||||||
|
// Продолжаем опрос, пока не авторизованы: переходной статус (phase ready до
|
||||||
|
// подтверждения транспорта) не должен останавливать обновление.
|
||||||
|
if (state.tgConnected) {
|
||||||
|
stopQrTimer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (state.tgState === 'idle') {
|
||||||
|
stopQrTimer()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
qrTick.value++
|
||||||
|
}, QR_POLL_MS)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Таймер стартует при входе в режим QR и сам останавливается при подключении/сбросе.
|
||||||
|
watch(
|
||||||
|
() => state.tgState,
|
||||||
|
(s) => {
|
||||||
|
if (s === 'qr') startQrTimer()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await refreshTgStatus()
|
||||||
|
if (state.tgState === 'qr') startQrTimer()
|
||||||
|
})
|
||||||
|
onBeforeUnmount(stopQrTimer)
|
||||||
|
|
||||||
async function submitCode() {
|
async function submitCode() {
|
||||||
await connectTgStep(code.value)
|
await connectTgStep(code.value)
|
||||||
if (state.tgState === 'done') code.value = ''
|
if (state.tgState === 'done') code.value = ''
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { state, toast, errMsg, fmtMsgTime } from './core.js'
|
|||||||
|
|
||||||
export function mapTgStatus(st) {
|
export function mapTgStatus(st) {
|
||||||
if (!st) return
|
if (!st) return
|
||||||
state.tgConnected = !!st.connected
|
const phase = st.phase || 'idle'
|
||||||
|
// «Подключён» = авторизован (phase ready); транспортный connected ненадёжен для UI.
|
||||||
|
state.tgConnected = phase === 'ready' || !!st.connected
|
||||||
state.tgAccount = st.account || ''
|
state.tgAccount = st.account || ''
|
||||||
state.tgKeysSet = !!st.keysSet
|
state.tgKeysSet = !!st.keysSet
|
||||||
if (st.error) state.tgError = st.error
|
if (st.error) state.tgError = st.error
|
||||||
const phase = st.phase || 'idle'
|
|
||||||
if (phase === 'ready') {
|
if (phase === 'ready') {
|
||||||
state.tgState = 'done'
|
state.tgState = 'done'
|
||||||
} else if (['phone', 'code', 'password', 'qr'].includes(phase)) {
|
} else if (['phone', 'code', 'password', 'qr'].includes(phase)) {
|
||||||
@@ -147,10 +148,12 @@ export async function refreshTgStatus() {
|
|||||||
export async function tgStart() {
|
export async function tgStart() {
|
||||||
state.tgError = ''
|
state.tgError = ''
|
||||||
if (state.tgQrMode) {
|
if (state.tgQrMode) {
|
||||||
state.tgState = 'qr'
|
|
||||||
try {
|
try {
|
||||||
|
// Сначала стартуем QR на бэке, затем показываем картинку: иначе <img> успевает запросить
|
||||||
|
// /api/tg/qr-image до готовности QR и остаётся пустым.
|
||||||
const r = await api.post('/api/tg/start-qr')
|
const r = await api.post('/api/tg/start-qr')
|
||||||
state.tgQrUrl = r.qrUrl || ''
|
state.tgQrUrl = r.qrUrl || ''
|
||||||
|
state.tgState = 'qr'
|
||||||
toast(t('settings.ssylka-dlya-vhoda-sgenerirovana-otkrojte'), { icon: 'send' })
|
toast(t('settings.ssylka-dlya-vhoda-sgenerirovana-otkrojte'), { icon: 'send' })
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
state.tgError = errMsg(e)
|
state.tgError = errMsg(e)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { t } from '@/i18n/index.js'
|
import { t } from '@/i18n/index.js'
|
||||||
import { onBeforeUnmount, watch } from 'vue'
|
import { state } from '../store.js'
|
||||||
import { state, refreshTgStatus } from '../store.js'
|
|
||||||
import Icon from '../components/Icon.vue'
|
import Icon from '../components/Icon.vue'
|
||||||
import TelegramTab from '../components/settings/TelegramTab.vue'
|
import TelegramTab from '../components/settings/TelegramTab.vue'
|
||||||
import AiTab from '../components/settings/AiTab.vue'
|
import AiTab from '../components/settings/AiTab.vue'
|
||||||
@@ -26,32 +25,6 @@ const TABS = [
|
|||||||
{ id: 'appearance', name: t('settings.vneshnij-vid'), icon: 'palette' },
|
{ id: 'appearance', name: t('settings.vneshnij-vid'), icon: 'palette' },
|
||||||
{ id: 'profile', name: t('settings.profil'), icon: 'key' },
|
{ id: 'profile', name: t('settings.profil'), icon: 'key' },
|
||||||
]
|
]
|
||||||
|
|
||||||
// пока идёт QR-вход (вкладка Telegram) — опрашиваем статус, чтобы поймать
|
|
||||||
// момент подтверждения. Watch живёт на уровне экрана настроек: при
|
|
||||||
// переключении вкладок опрос не прерывается (как в исходном SettingsView).
|
|
||||||
let qrTimer = null
|
|
||||||
watch(
|
|
||||||
() => state.tgState,
|
|
||||||
(s) => {
|
|
||||||
if (s === 'qr') {
|
|
||||||
stopQrPoll()
|
|
||||||
qrTimer = setInterval(async () => {
|
|
||||||
await refreshTgStatus()
|
|
||||||
if (state.tgState === 'done') stopQrPoll()
|
|
||||||
}, 4000)
|
|
||||||
} else {
|
|
||||||
stopQrPoll()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
function stopQrPoll() {
|
|
||||||
if (qrTimer) {
|
|
||||||
clearInterval(qrTimer)
|
|
||||||
qrTimer = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
onBeforeUnmount(stopQrPoll)
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
Reference in New Issue
Block a user