增加token检测

This commit is contained in:
扫地僧 2026-05-05 23:55:17 +08:00
parent 6170d5a619
commit e05ac23cc3
5 changed files with 260 additions and 6 deletions

View File

@ -58,3 +58,12 @@ export function replenishAccountPool(module, data) {
data, data,
}); });
} }
/** 使用厂商 Token 探测是否可用服务端转发。Cursor 传 { id, accessToken }(会话 JWT以便回写 is_used仅传 accessToken 也可探测但不更新库Windsurf/Kiro 传 { id } */
export function probeAccountPoolToken(module, data) {
return request({
url: `${base(module)}/probeToken`,
method: 'post',
data,
});
}

View File

@ -104,6 +104,17 @@ function copyToken() {
row.extractStatus === 2 ? '补号' : row.extracted ? '已提取' : '未提取' row.extractStatus === 2 ? '补号' : row.extracted ? '已提取' : '未提取'
}} }}
</el-descriptions-item> </el-descriptions-item>
<el-descriptions-item label="探测可用">
{{
row.isUsed === null || row.isUsed === undefined
? '未探测'
: Number(row.isUsed) === 1
? '可用'
: Number(row.isUsed) === 0
? '已用完'
: String(row.isUsed)
}}
</el-descriptions-item>
<el-descriptions-item label="提取时间">{{ row.extractedAt || '-' }}</el-descriptions-item> <el-descriptions-item label="提取时间">{{ row.extractedAt || '-' }}</el-descriptions-item>
<el-descriptions-item label="提取平台"> <el-descriptions-item label="提取平台">
<el-tag v-if="row.extractedPlatform" :type="platformType" size="small"> <el-tag v-if="row.extractedPlatform" :type="platformType" size="small">

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { computed, nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue"; import { computed, h, nextTick, onMounted, onUnmounted, reactive, ref, watch } from "vue";
import { ElMessage } from "element-plus"; import { ElMessage, ElMessageBox } from "element-plus";
import Edit from "./components/edit.vue"; import Edit from "./components/edit.vue";
import DetailDialog from "./components/detail.vue"; import DetailDialog from "./components/detail.vue";
import ExtractDialog from "./components/extract.vue"; import ExtractDialog from "./components/extract.vue";
@ -13,6 +13,7 @@ import {
getAccountPoolList, getAccountPoolList,
updateAccountPoolRemark, updateAccountPoolRemark,
replenishAccountPool, replenishAccountPool,
probeAccountPoolToken,
} from "@/api/accountPool"; } from "@/api/accountPool";
const moduleKey = "cursor"; const moduleKey = "cursor";
@ -33,6 +34,7 @@ const patchVisible = ref(false);
const query = reactive({ const query = reactive({
keyword: "", keyword: "",
status: "", status: "",
usable: "",
}); });
const activeTypeTab = ref("all"); const activeTypeTab = ref("all");
@ -49,6 +51,7 @@ const total = ref(0);
const selectedRows = ref([]); const selectedRows = ref([]);
const detailRow = ref(null); const detailRow = ref(null);
const detailRemarkSaving = ref(false); const detailRemarkSaving = ref(false);
const probeLoadingId = ref(null);
const isMobile = ref(false); const isMobile = ref(false);
const pagination = reactive({ const pagination = reactive({
page: 1, page: 1,
@ -63,6 +66,7 @@ const pagedList = computed(() => tableData.value);
function resetQuery() { function resetQuery() {
query.keyword = ""; query.keyword = "";
query.status = ""; query.status = "";
query.usable = "";
} }
const typeTabs = computed(() => { const typeTabs = computed(() => {
@ -75,7 +79,7 @@ const typeTabs = computed(() => {
}); });
watch( watch(
() => [query.keyword, query.status, activeTypeTab.value], () => [query.keyword, query.status, query.usable, activeTypeTab.value],
() => { () => {
if (skipWatchFetchDuringUnusedJump.value) return; if (skipWatchFetchDuringUnusedJump.value) return;
pagination.page = 1; pagination.page = 1;
@ -348,6 +352,21 @@ function platformTagType(platform) {
return PLATFORM_MAP[platform]?.type || "info"; return PLATFORM_MAP[platform]?.type || "info";
} }
function isUsedLabel(isUsed) {
if (isUsed === null || isUsed === undefined) return "未探测";
const n = Number(isUsed);
if (n === 1) return "可用";
if (n === 0) return "已用完";
return String(isUsed);
}
function isUsedTagType(isUsed) {
const n = Number(isUsed);
if (n === 1) return "success";
if (n === 0) return "danger";
return "info";
}
function normalizeRow(raw) { function normalizeRow(raw) {
const pick = (...keys) => { const pick = (...keys) => {
for (const key of keys) { for (const key of keys) {
@ -371,6 +390,11 @@ function normalizeRow(raw) {
}; };
const st = Number(pick("is_extracted", "isExtracted", "IsExtracted")); const st = Number(pick("is_extracted", "isExtracted", "IsExtracted"));
const extractStatus = Number.isFinite(st) ? st : 0; const extractStatus = Number.isFinite(st) ? st : 0;
const isUsedRaw = pickNullable("is_used", "isUsed", "IsUsed");
const isUsedNum =
isUsedRaw === null || isUsedRaw === undefined || isUsedRaw === ""
? null
: Number(isUsedRaw);
return { return {
id: pick("id", "Id", "ID"), id: pick("id", "Id", "ID"),
type: pick("data_type", "dataType", "type"), type: pick("data_type", "dataType", "type"),
@ -380,6 +404,7 @@ function normalizeRow(raw) {
remark: pick("remark", "Remark"), remark: pick("remark", "Remark"),
extractStatus, extractStatus,
extracted: extractStatus !== 0, extracted: extractStatus !== 0,
isUsed: Number.isFinite(isUsedNum) ? isUsedNum : null,
extractedAt: formatTime(pickNullable("extracted_time", "extractedAt")), extractedAt: formatTime(pickNullable("extracted_time", "extractedAt")),
extractedPlatform: pickNullable("extracted_platform", "extractedPlatform"), extractedPlatform: pickNullable("extracted_platform", "extractedPlatform"),
createdAt: formatTime(pick("create_time", "createdAt")), createdAt: formatTime(pick("create_time", "createdAt")),
@ -394,6 +419,7 @@ async function fetchList() {
pageSize: pagination.pageSize, pageSize: pagination.pageSize,
keyword: query.keyword || undefined, keyword: query.keyword || undefined,
status: query.status || undefined, status: query.status || undefined,
usable: query.usable === "1" || query.usable === "0" ? query.usable : undefined,
type: activeTypeTab.value === "all" ? undefined : activeTypeTab.value, type: activeTypeTab.value === "all" ? undefined : activeTypeTab.value,
}); });
if (res?.code !== 200) { if (res?.code !== 200) {
@ -416,6 +442,7 @@ async function jumpToLastUnusedPage() {
pageSize: pagination.pageSize, pageSize: pagination.pageSize,
keyword: query.keyword || undefined, keyword: query.keyword || undefined,
status: "unused", status: "unused",
usable: query.usable === "1" || query.usable === "0" ? query.usable : undefined,
type, type,
}); });
if (res?.code !== 200) { if (res?.code !== 200) {
@ -525,6 +552,105 @@ function copyCardInfo(row) {
}); });
} }
const CURSOR_PRO_LIMIT_TEXT = 'Get Cursor Pro for more Agent usage, unlimited Tab, and more.';
function formatCursorProbeDialogText(d) {
const detail = String(d?.detail || '');
const rawPreview = String(d?.rawPreview || '');
if (detail.includes(CURSOR_PRO_LIMIT_TEXT) || rawPreview.includes(CURSOR_PRO_LIMIT_TEXT)) {
return '该TOKEN已用完';
}
return '该TOKEN可用';
}
async function handleProbeToken(row) {
if (!row?.token) {
ElMessage.warning('该行无 Token');
return;
}
probeLoadingId.value = row.id;
try {
const res = await probeAccountPoolToken(moduleKey, {
id: row.id,
accessToken: row.token,
});
if (res?.code !== 200) {
ElMessage.error(res?.msg || '探测失败');
return;
}
const d = res?.data || {};
const text = formatCursorProbeDialogText(d);
try {
await ElMessageBox({
title: '检测结果',
message: h('div', { class: 'cursor-probe-result' }, text),
confirmButtonText: '关闭',
customClass: 'cursor-probe-dialog',
closeOnClickModal: true,
});
} catch {
/* 用户关闭弹窗 */
}
await fetchList();
} catch {
ElMessage.error('探测请求失败');
} finally {
probeLoadingId.value = null;
}
}
async function handleBatchProbe() {
if (!selectedRows.value.length) {
ElMessage.warning("请先选择数据");
return;
}
const rows = selectedRows.value.filter((r) => r?.token);
if (!rows.length) {
ElMessage.warning("所选行均无 Token无法检测");
return;
}
const skipped = selectedRows.value.length - rows.length;
const skipHint =
skipped > 0 ? `(已跳过 ${skipped} 条无 Token` : "";
try {
await ElMessageBox.confirm(
`将对 ${rows.length} 条 Token进行检测是否继续`,
"批量检测",
{ type: "info", confirmButtonText: "开始", cancelButtonText: "取消" },
);
} catch {
return;
}
loading.value = true;
let ok = 0;
let fail = 0;
try {
for (const row of rows) {
try {
const res = await probeAccountPoolToken(moduleKey, {
id: row.id,
accessToken: row.token,
});
if (res?.code === 200) {
ok += 1;
} else {
fail += 1;
}
} catch {
fail += 1;
}
}
if (fail > 0) {
ElMessage.warning(`批量检测完成:成功 ${ok} 条,失败 ${fail}`);
} else {
ElMessage.success(`批量检测完成:共 ${ok}`);
}
await fetchList();
} finally {
loading.value = false;
}
}
</script> </script>
<template> <template>
@ -553,6 +679,15 @@ function copyCardInfo(row) {
<el-option label="未提取" value="unused" /> <el-option label="未提取" value="unused" />
<el-option label="已提取" value="extracted" /> <el-option label="已提取" value="extracted" />
</el-select> </el-select>
<el-select
v-model="query.usable"
placeholder="探测可用"
clearable
class="w-140"
>
<el-option label="可用" value="1" />
<el-option label="已用完" value="0" />
</el-select>
<el-button <el-button
type="primary" type="primary"
plain plain
@ -568,6 +703,7 @@ function copyCardInfo(row) {
<el-button type="success" @click="openAddDialog('batch')">批量添加</el-button> <el-button type="success" @click="openAddDialog('batch')">批量添加</el-button>
<el-button type="warning" @click="replenishVisible = true">补号</el-button> <el-button type="warning" @click="replenishVisible = true">补号</el-button>
<el-button @click="markExtractForSelected">批量标记提取</el-button> <el-button @click="markExtractForSelected">批量标记提取</el-button>
<el-button type="info" plain @click="handleBatchProbe">批量检测</el-button>
<el-button @click="apiDocVisible = true">接口说明</el-button> <el-button @click="apiDocVisible = true">接口说明</el-button>
</div> </div>
</div> </div>
@ -613,6 +749,13 @@ function copyCardInfo(row) {
</el-tag> </el-tag>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="探测可用" width="100" align="center">
<template #default="{ row }">
<el-tag :type="isUsedTagType(row.isUsed)" size="small">
{{ isUsedLabel(row.isUsed) }}
</el-tag>
</template>
</el-table-column>
<el-table-column prop="extractedAt" label="提取时间" width="180" /> <el-table-column prop="extractedAt" label="提取时间" width="180" />
<el-table-column label="提取平台" width="120"> <el-table-column label="提取平台" width="120">
<template #default="{ row }"> <template #default="{ row }">
@ -626,8 +769,16 @@ function copyCardInfo(row) {
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center"> <el-table-column label="操作" width="300" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button
v-if="row.token"
link
type="info"
:loading="probeLoadingId === row.id"
@click="handleProbeToken(row)"
>检测</el-button
>
<el-button link type="primary" @click="openDetail(row)" <el-button link type="primary" @click="openDetail(row)"
>详情</el-button >详情</el-button
> >
@ -999,4 +1150,17 @@ function copyCardInfo(row) {
.pool-tooltip .el-popper__arrow { .pool-tooltip .el-popper__arrow {
display: block; display: block;
} }
/* Cursor 探测结果弹窗teleport 到 body需非 scoped */
.cursor-probe-dialog .el-message-box__message {
padding: 12px 8px 4px;
}
.cursor-probe-dialog .cursor-probe-result {
margin: 0;
text-align: center;
font-size: 16px;
line-height: 1.6;
font-weight: 600;
word-break: break-all;
}
</style> </style>

View File

@ -14,6 +14,7 @@ import {
getAccountPoolList, getAccountPoolList,
updateAccountPoolRemark, updateAccountPoolRemark,
replenishAccountPool, replenishAccountPool,
probeAccountPoolToken,
} from '@/api/accountPool'; } from '@/api/accountPool';
const moduleKey = "krio"; const moduleKey = "krio";
@ -41,6 +42,7 @@ const total = ref(0);
const selectedRows = ref([]); const selectedRows = ref([]);
const detailRow = ref(null); const detailRow = ref(null);
const detailRemarkSaving = ref(false); const detailRemarkSaving = ref(false);
const probeLoadingId = ref(null);
const isMobile = ref(false); const isMobile = ref(false);
const pagination = reactive({ page: 1, pageSize: 30 }); const pagination = reactive({ page: 1, pageSize: 30 });
@ -454,6 +456,31 @@ function copyCardInfo(row) {
navigator.clipboard.writeText(parts.join('\n')).then(() => { ElMessage.success('已复制'); }); navigator.clipboard.writeText(parts.join('\n')).then(() => { ElMessage.success('已复制'); });
} }
async function handleProbeToken(row) {
if (!row?.token) {
ElMessage.warning('该行无 Token');
return;
}
probeLoadingId.value = row.id;
try {
const res = await probeAccountPoolToken(moduleKey, { id: row.id });
if (res?.code !== 200) {
ElMessage.error(res?.msg || '探测失败');
return;
}
const d = res?.data || {};
if (d.ok) {
ElMessage.success(d.detail || '官方接口响应正常');
} else {
ElMessage.error(d.detail || '不可用或校验失败');
}
} catch {
ElMessage.error('探测请求失败');
} finally {
probeLoadingId.value = null;
}
}
</script> </script>
<template> <template>
@ -543,11 +570,19 @@ function copyCardInfo(row) {
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center"> <el-table-column label="操作" width="300" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)" <el-button link type="primary" @click="openDetail(row)"
>详情</el-button >详情</el-button
> >
<el-button
v-if="row.token"
link
type="info"
:loading="probeLoadingId === row.id"
@click="handleProbeToken(row)"
>查可用</el-button
>
<el-button <el-button
v-if="!row.extractedAt && !row.extracted" v-if="!row.extractedAt && !row.extracted"
link link

View File

@ -13,6 +13,7 @@ import {
getAccountPoolList, getAccountPoolList,
updateAccountPoolRemark, updateAccountPoolRemark,
replenishAccountPool, replenishAccountPool,
probeAccountPoolToken,
} from '@/api/accountPool'; } from '@/api/accountPool';
const moduleKey = "windsurf"; const moduleKey = "windsurf";
@ -40,6 +41,7 @@ const total = ref(0);
const selectedRows = ref([]); const selectedRows = ref([]);
const detailRow = ref(null); const detailRow = ref(null);
const detailRemarkSaving = ref(false); const detailRemarkSaving = ref(false);
const probeLoadingId = ref(null);
const isMobile = ref(false); const isMobile = ref(false);
const pagination = reactive({ page: 1, pageSize: 30 }); const pagination = reactive({ page: 1, pageSize: 30 });
@ -453,6 +455,31 @@ function copyCardInfo(row) {
navigator.clipboard.writeText(parts.join('\n')).then(() => { ElMessage.success('已复制'); }); navigator.clipboard.writeText(parts.join('\n')).then(() => { ElMessage.success('已复制'); });
} }
async function handleProbeToken(row) {
if (!row?.token) {
ElMessage.warning('该行无 Token');
return;
}
probeLoadingId.value = row.id;
try {
const res = await probeAccountPoolToken(moduleKey, { id: row.id });
if (res?.code !== 200) {
ElMessage.error(res?.msg || '探测失败');
return;
}
const d = res?.data || {};
if (d.ok) {
ElMessage.success(d.detail || '官方接口响应正常');
} else {
ElMessage.error(d.detail || '不可用或校验失败');
}
} catch {
ElMessage.error('探测请求失败');
} finally {
probeLoadingId.value = null;
}
}
</script> </script>
<template> <template>
@ -542,11 +569,19 @@ function copyCardInfo(row) {
<span v-else>-</span> <span v-else>-</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="操作" width="220" fixed="right" align="center"> <el-table-column label="操作" width="300" fixed="right" align="center">
<template #default="{ row }"> <template #default="{ row }">
<el-button link type="primary" @click="openDetail(row)" <el-button link type="primary" @click="openDetail(row)"
>详情</el-button >详情</el-button
> >
<el-button
v-if="row.token"
link
type="info"
:loading="probeLoadingId === row.id"
@click="handleProbeToken(row)"
>查可用</el-button
>
<el-button <el-button
v-if="!row.extractedAt && !row.extracted" v-if="!row.extractedAt && !row.extracted"
link link