diff --git a/apps/backend/src/routes/claims.js b/apps/backend/src/routes/claims.js
index d7dbd627..15353574 100644
--- a/apps/backend/src/routes/claims.js
+++ b/apps/backend/src/routes/claims.js
@@ -1,11 +1,13 @@
import { Router } from 'express'
import {
+ closeClaimSession,
confirmClaimRole,
createClaimSession,
getClaimDetail,
getClaimScreenshotPath,
getClaimSessionSummary,
+ reloadClaimSession,
redeemClaimTask,
} from '../services/claim/claim-session-service.js'
import { buildNotFoundPayload, buildSuccessPayload, sendRouteError } from '../utils/http.js'
@@ -39,6 +41,24 @@ router.get('/:token/session/summary', async (req, res) => {
}
})
+router.post('/:token/session/refresh', async (req, res) => {
+ try {
+ const data = await reloadClaimSession(req.params.token)
+ res.json(buildSuccessPayload(data, data.session ? (data.session.notice || '后端页面已刷新') : '领取会话已重置'))
+ } catch (error) {
+ sendRouteError(res, error, '刷新领取会话失败', '[claims/:token/session/refresh]')
+ }
+})
+
+router.delete('/:token/session', async (req, res) => {
+ try {
+ const data = await closeClaimSession(req.params.token)
+ res.json(buildSuccessPayload(data, '领取会话已关闭'))
+ } catch (error) {
+ sendRouteError(res, error, '关闭领取会话失败', '[claims/:token/session]')
+ }
+})
+
router.post('/:token/confirm-role', async (req, res) => {
try {
const data = await confirmClaimRole(req.params.token)
diff --git a/apps/backend/src/services/claim/claim-session-service.js b/apps/backend/src/services/claim/claim-session-service.js
index 79149c28..49e2ab10 100644
--- a/apps/backend/src/services/claim/claim-session-service.js
+++ b/apps/backend/src/services/claim/claim-session-service.js
@@ -1,8 +1,10 @@
import {
+ closeTencentBrowserSession,
createTencentBrowserSession,
getTencentBrowserSession,
getTencentBrowserSessionSummary,
getTencentBrowserSessionScreenshotPath,
+ reloadTencentBrowserSession,
redeemTencentBrowserSession,
} from '../session/session.js'
import { findClaimTokenByToken, getClaimTokenById, updateClaimToken } from '../../repositories/claim-token-repo.js'
@@ -24,8 +26,8 @@ const CLAIM_TERMINAL_STATUSES = new Set(['expired', 'closed'])
export async function getClaimDetail(token, { includeQrImage = true } = {}) {
const context = await getClaimContext(token)
- const session = await loadTaskSession(context.task, { includeQrImage })
- const syncedTask = session ? await syncTaskWithSession(context.task, session) : context.task
+ const { task, session } = await loadTaskSession(context.task, { includeQrImage })
+ const syncedTask = session ? await syncTaskWithSession(task, session) : task
return buildClaimDetailPayload({
claimToken: context.claimToken,
@@ -39,30 +41,54 @@ export async function getClaimDetail(token, { includeQrImage = true } = {}) {
export async function createClaimSession(token, payload = {}) {
const context = await getClaimContext(token)
assertTaskCanProceed(context.task)
+ const requestedLoginType = normalizeClaimLoginType(payload.loginType)
+ const forceRecreate = Boolean(payload.forceRecreate)
+ let task = context.task
- if (context.task.browser_session_id) {
- const existingSession = await getTencentBrowserSession(context.task.browser_session_id)
- const syncedTask = await syncTaskWithSession(context.task, existingSession)
+ if (task.browser_session_id) {
+ const existing = await loadTaskSession(task, { includeQrImage: true })
+ task = existing.task
- return buildClaimDetailPayload({
- claimToken: context.claimToken,
- task: syncedTask,
- order: context.order,
- orderItem: context.orderItem,
- session: existingSession,
- })
+ if (existing.session) {
+ const existingLoginType = normalizeClaimLoginType(existing.session.loginType || task.login_type)
+
+ if (!forceRecreate && existingLoginType === requestedLoginType) {
+ const syncedTask = await syncTaskWithSession(task, existing.session)
+
+ return buildClaimDetailPayload({
+ claimToken: context.claimToken,
+ task: syncedTask,
+ order: context.order,
+ orderItem: context.orderItem,
+ session: existing.session,
+ })
+ }
+
+ try {
+ await closeTencentBrowserSession(existing.session.sessionId)
+ } catch (error) {
+ if (!isRecoverableSessionError(error)) {
+ throw error
+ }
+ }
+
+ task = await clearTaskSession(task, {
+ lastError: '',
+ })
+ }
}
const session = await createTencentBrowserSession({
- loginType: payload.loginType,
+ loginType: requestedLoginType,
})
- const updatedTask = await updateTask(context.task.id, {
+ const updatedTask = await updateTask(task.id, {
task_status: 'claimed',
browser_session_id: session.sessionId,
login_type: session.loginType,
user_action_status: 'claimed',
- claimed_at: context.task.claimed_at || nowIso(),
+ claimed_at: task.claimed_at || nowIso(),
updated_at: nowIso(),
+ last_error: '',
})
return buildClaimDetailPayload({
@@ -76,16 +102,21 @@ export async function createClaimSession(token, payload = {}) {
export async function getClaimSessionSummary(token) {
const context = await getClaimContext(token)
+ const { task, session } = await loadTaskSession(context.task, {
+ includeQrImage: false,
+ })
- if (!context.task.browser_session_id) {
- throw createHttpError('当前任务还没有创建浏览器会话', {
- statusCode: 409,
- errorCode: 'claim_session_not_created',
+ if (!session) {
+ return buildClaimDetailPayload({
+ claimToken: context.claimToken,
+ task,
+ order: context.order,
+ orderItem: context.orderItem,
+ session: null,
})
}
- const session = await getTencentBrowserSession(context.task.browser_session_id)
- const syncedTask = await syncTaskWithSession(context.task, session)
+ const syncedTask = await syncTaskWithSession(task, session)
return buildClaimDetailPayload({
claimToken: context.claimToken,
@@ -96,6 +127,72 @@ export async function getClaimSessionSummary(token) {
})
}
+export async function reloadClaimSession(token) {
+ const context = await getClaimContext(token)
+ assertTaskCanProceed(context.task)
+
+ const active = await loadTaskSession(context.task, {
+ includeQrImage: true,
+ })
+
+ if (!active.session) {
+ return buildClaimDetailPayload({
+ claimToken: context.claimToken,
+ task: active.task,
+ order: context.order,
+ orderItem: context.orderItem,
+ session: null,
+ })
+ }
+
+ const session = await reloadTencentBrowserSession(active.session.sessionId)
+ const syncedTask = await syncTaskWithSession(active.task, session)
+
+ return buildClaimDetailPayload({
+ claimToken: context.claimToken,
+ task: syncedTask,
+ order: context.order,
+ orderItem: context.orderItem,
+ session,
+ })
+}
+
+export async function closeClaimSession(token) {
+ const context = await getClaimContext(token)
+ assertTaskCanProceed(context.task)
+ let task = context.task
+
+ if (!task.browser_session_id) {
+ return buildClaimDetailPayload({
+ claimToken: context.claimToken,
+ task,
+ order: context.order,
+ orderItem: context.orderItem,
+ session: null,
+ })
+ }
+
+ try {
+ await closeTencentBrowserSession(task.browser_session_id)
+ } catch (error) {
+ if (!isRecoverableSessionError(error)) {
+ throw error
+ }
+ }
+
+ task = await clearTaskSession(task, {
+ lastError: '',
+ })
+
+ return buildClaimDetailPayload({
+ claimToken: context.claimToken,
+ task,
+ order: context.order,
+ orderItem: context.orderItem,
+ session: null,
+ })
+}
+
export async function confirmClaimRole(token) {
const context = await getClaimContext(token)
assertPublicClaimActionAllowed(context.task, 'confirm')
@@ -430,12 +527,35 @@ async function expireClaimContext(claimToken, task) {
async function loadTaskSession(task, { includeQrImage = false } = {}) {
if (!task.browser_session_id) {
- return null
+ return {
+ task,
+ session: null,
+ }
}
- return includeQrImage
- ? getTencentBrowserSession(task.browser_session_id)
- : getTencentBrowserSessionSummary(task.browser_session_id)
+ try {
+ const session = includeQrImage
+ ? await getTencentBrowserSession(task.browser_session_id)
+ : await getTencentBrowserSessionSummary(task.browser_session_id)
+
+ return {
+ task,
+ session,
+ }
+ } catch (error) {
+ if (!isRecoverableSessionError(error)) {
+ throw error
+ }
+
+ const nextTask = await clearTaskSession(task, {
+ lastError: '浏览器会话已失效,请重新初始化登录',
+ })
+
+ return {
+ task: nextTask,
+ session: null,
+ }
+ }
}
async function syncTaskWithSession(task, session) {
@@ -550,3 +670,36 @@ function parseTaskState(task) {
function isAssistedClaimTask(task) {
return String(task?.executor_key || '').trim() === 'tencent_claim_assisted'
}
+
+async function clearTaskSession(task, { lastError = '' } = {}) {
+ const shouldResetClaimProgress = ['link_generated', 'claimed', 'role_confirmed'].includes(String(task.task_status || ''))
+ const nextTaskStatus = shouldResetClaimProgress ? 'link_generated' : task.task_status
+ const nextUserActionStatus = shouldResetClaimProgress ? 'pending_claim' : task.user_action_status
+ const patch = {
+ task_status: nextTaskStatus,
+ user_action_status: nextUserActionStatus,
+ browser_session_id: '',
+ login_type: '',
+ nickname: '',
+ role_id: '',
+ role_name: '',
+ area: '',
+ partition_name: '',
+ artifacts_json: '{}',
+ state_json: '{}',
+ role_confirmed_at: nextTaskStatus === 'link_generated' ? null : task.role_confirmed_at,
+ last_error: String(lastError || ''),
+ updated_at: nowIso(),
+ }
+
+ return updateTask(task.id, patch)
+}
+
+function normalizeClaimLoginType(loginType) {
+ return String(loginType || '').trim() === 'wx' ? 'wx' : 'qq'
+}
+
+function isRecoverableSessionError(error) {
+ const errorCode = String(error?.errorCode || error?.code || '').trim()
+ return errorCode === 'session_not_found' || errorCode === 'session_closed'
+}
diff --git a/apps/backend/src/services/session/session.js b/apps/backend/src/services/session/session.js
index a02051a3..ac6b18e6 100644
--- a/apps/backend/src/services/session/session.js
+++ b/apps/backend/src/services/session/session.js
@@ -172,6 +172,7 @@ export async function reloadTencentBrowserSession(sessionId) {
session.qrImagePath = ''
session.qrUpdatedAt = ''
await ensureLoginTab(session.page, session.loginType)
+ await captureSessionQr(session)
}
session.notice = credentialReady
@@ -247,6 +248,11 @@ export async function refreshTencentBrowserSession(sessionId, { includeQrImage =
session.expiresAt = Date.now() + SESSION_TTL_MS
if (!session.qrImageBase64 || session.status === 'expired') {
+ if (session.status === 'expired') {
+ await reloadActivityPageForPresentation(session.page)
+ await ensureLoginTab(session.page, session.loginType)
+ }
+
await captureSessionQr(session)
if (session.status === 'expired') {
session.notice = '二维码已刷新,请重新扫码'
diff --git a/apps/frontend/src/components/tencent/TencentAuthCard.vue b/apps/frontend/src/components/tencent/TencentAuthCard.vue
index 37c731f1..bb6677b7 100644
--- a/apps/frontend/src/components/tencent/TencentAuthCard.vue
+++ b/apps/frontend/src/components/tencent/TencentAuthCard.vue
@@ -23,6 +23,7 @@ const props = defineProps<{
}>()
const emit = defineEmits<{
+ closeSession: []
createSession: [loginType: TencentLoginType]
openQrPreview: []
qrImageLoad: [event: Event]
@@ -110,23 +111,33 @@ const emit = defineEmits<{