front/src/views/xhs/startService/index.vue
xiaolipro a0dfdffa14
Some checks failed
Build and Deploy Front / deploy (push) Failing after 2m17s
fix:编码乱码
2026-09-07 14:36:45 +08:00

547 lines
16 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="xhs-start-service-container">
<div class="xhs-page-card start-service-card">
<div class="table-toolbar">
<div class="toolbar-left">
<span class="xhs-panel-title">启动服务</span>
<span class="xhs-panel-desc">
本机 Agent + 浏览器引擎:找对标、写稿、发笔记、互动留痕
</span>
</div>
<div class="toolbar-right">
<a-select
v-if="agents.length > 0"
v-model="selectedAgentId"
placeholder="选择 Agent"
:style="{ width: '200px' }"
>
<a-option
v-for="a in agents"
:key="a.id"
:value="a.id"
:label="a.deviceName || a.deviceId"
/>
</a-select>
<a-button @click="showSetupDrawer = true">Agent 连接指南</a-button>
</div>
</div>
<div class="start-service-body">
<a-alert
v-if="offlineCount > 0 && agents.length > 0"
type="warning"
style="margin-bottom: 12px"
:title="`${offlineCount} 个 Agent 离线,请检查本机 Agent 是否在运行`"
closable
/>
<a-alert
v-if="!loading && agents.length === 0"
type="info"
style="margin-bottom: 12px"
title="尚未连接 Agent,请运行 xhs-agent-desktop 并连接 OMS"
closable
/>
<div class="service-cards-row">
<div class="service-card xhs-inner-card">
<div class="card-header">
<span class="card-title">小红书 · 浏览器引擎</span>
<XhsStatusBadge :kind="engineBadge.kind" :text="engineBadge.text" />
</div>
<p class="card-hint">{{ bridgeUrlText }}</p>
<p class="card-hint card-note">
内嵌 xiaohongshu-skills:Bridge Server + Chrome 扩展,在本机真实浏览器中操作小红书。
</p>
<div class="card-btn-row">
<a-button type="primary" :disabled="!canControl" @click="control('engine_start')">
启动引擎
</a-button>
<a-button :disabled="!canControl" @click="control('engine_stop')">停止引擎</a-button>
<a-button @click="loadAgents">刷新状态</a-button>
</div>
</div>
<div class="service-card xhs-inner-card">
<div class="card-header">
<span class="card-title">小红书账号(可选)</span>
<XhsStatusBadge :kind="loginBadge.kind" :text="loginBadge.text" />
</div>
<p class="card-hint card-note">{{ loginGuideText }}</p>
<div class="card-btn-row">
<a-button :disabled="!canControl" @click="control('clear_cookies')">退出登录</a-button>
<a-button type="primary" :disabled="!canControl" @click="control('browser_login')">
浏览器登录
</a-button>
<a-button :disabled="!canControl" @click="control('check_login')">登录校验</a-button>
</div>
</div>
<div class="service-card xhs-inner-card coming-soon">
<div class="card-header">
<span class="card-title">抖音</span>
<XhsStatusBadge kind="idle" text="即将支持" />
</div>
<p class="card-hint card-note">
抖音 / Chrome 扩展与自动化引擎正在接入中。上线后可与本站一样使用分析、评论与创作能力。
</p>
<div class="card-btn-row">
<a-button disabled>敬请期待</a-button>
</div>
</div>
</div>
<div v-if="agents.length > 0" class="agent-table-section">
<div class="table-toolbar">
<div class="toolbar-left">
<span class="section-title">已连接设备</span>
<a-button @click="loadAgents">
<template #icon><icon-refresh /></template>
刷新
</a-button>
</div>
<div class="toolbar-right">
<a-pagination
v-model:current="pagination.current"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-size-options="['10', '30', '50', '100']"
show-total
show-page-size
size="small"
@change="onPageChange"
@page-size-change="onPageSizeChange"
/>
</div>
</div>
<a-table
:loading="loading"
:data="agents"
:pagination="false"
row-key="id"
size="small"
:row-class="agentRowClass"
@row-click="onAgentRowClick"
>
<template #columns>
<a-table-column title="设备名称" data-index="deviceName">
<template #cell="{ record }">
{{ record.deviceName || record.deviceId }}
</template>
</a-table-column>
<a-table-column title="状态" :width="90">
<template #cell="{ record }">
<XhsStatusBadge
:kind="record.status === 'online' ? 'ok' : 'idle'"
:text="record.status === 'online' ? '在线' : '离线'"
/>
</template>
</a-table-column>
<a-table-column title="引擎" :width="90">
<template #cell="{ record }">
<XhsStatusBadge
:kind="record.engineRunning ? 'ok' : 'err'"
:text="record.engineRunning ? '运行中' : '未启动'"
/>
</template>
</a-table-column>
<a-table-column title="扩展" :width="90">
<template #cell="{ record }">
<XhsStatusBadge
:kind="record.extensionConnected ? 'ok' : 'warn'"
:text="record.extensionConnected ? '已连接' : '未连接'"
/>
</template>
</a-table-column>
<a-table-column title="小红书" :width="90">
<template #cell="{ record }">
<XhsStatusBadge
:kind="record.xhsLoggedIn ? 'ok' : 'warn'"
:text="record.xhsLoggedIn ? '已登录' : '未登录'"
/>
</template>
</a-table-column>
<a-table-column title="版本" data-index="agentVersion" :width="100" />
<a-table-column title="最后心跳" :width="170">
<template #cell="{ record }">
{{ formatTime(record.lastHeartbeatTime) }}
</template>
</a-table-column>
</template>
</a-table>
</div>
</div>
</div>
<a-drawer
v-model:visible="showSetupDrawer"
title="Agent 连接指南"
:width="480"
:footer="false"
>
<a-steps :current="0" direction="vertical" size="small" style="margin-bottom: 20px">
<a-step description="运行 xhs-agent-desktop/run.ps1 打开桌面端">
打开 Agent 桌面端
</a-step>
<a-step description="桌面端点击「连接 OMS」;保持本页打开即可自动授权">
一键连接
</a-step>
<a-step description="启动引擎,Chrome 加载内置扩展并登录小红书">
本机引擎与扩展
</a-step>
<a-step description="在此页远程启停引擎、扫码登录小红书">
Web 远程控制
</a-step>
</a-steps>
<a-alert type="info" style="margin-bottom: 12px" title="无需填写 API / Token">
请先在浏览器登录 OMS,再打开桌面端点击「连接 OMS」。若本页已打开,授权会自动完成。
</a-alert>
<div class="cmd-block">
<div class="cmd-label">桌面端</div>
<pre>{{ desktopAgentHint }}</pre>
</div>
</a-drawer>
</div>
</template>
<script setup lang="ts">
import { Message, Modal } from '@arco-design/web-vue';
import { computed, onMounted, onUnmounted, reactive, ref, watch } from 'vue';
import {
controlXhsAgent,
listXhsAgents,
type XhsAgentItem,
} from '@/api/xhs';
import {
isAgentWaitingPair,
pushCredentialsToAgent,
} from '@/utils/xhsAgentPair';
import { useRbacUserStore } from '@/store/modules/rbac-user';
import XhsStatusBadge, { type XhsBadgeKind } from '../_shared/XhsStatusBadge.vue';
const rbacStore = useRbacUserStore();
const loading = ref(false);
const agents = ref<XhsAgentItem[]>([]);
const selectedAgentId = ref<number | undefined>();
const showSetupDrawer = ref(false);
const pagination = reactive({
current: 1,
pageSize: 50,
total: 0,
});
let refreshTimer: ReturnType<typeof setInterval> | null = null;
let pairTimer: ReturnType<typeof setInterval> | null = null;
let pairPushed = false;
const selectedAgent = computed(() =>
agents.value.find((a) => a.id === selectedAgentId.value),
);
const canControl = computed(
() => !!selectedAgent.value && selectedAgent.value.status === 'online',
);
const offlineCount = computed(
() => agents.value.filter((a) => a.status !== 'online').length,
);
const bridgeUrlText = computed(() => {
const agent = selectedAgent.value;
if (!agent) return 'Bridge: 请先连接 Agent';
const port = agent.bridgePort || 9333;
return `Bridge: ws://127.0.0.1:${port}`;
});
const engineBadge = computed((): { kind: XhsBadgeKind; text: string } => {
const agent = selectedAgent.value;
if (!agent || agent.status !== 'online') return { kind: 'idle', text: '待刷新' };
if (agent.engineRunning) return { kind: 'ok', text: '运行中' };
if (agent.extensionConnected) return { kind: 'warn', text: '待扩展' };
return { kind: 'err', text: '未启动' };
});
const loginBadge = computed((): { kind: XhsBadgeKind; text: string } => {
const agent = selectedAgent.value;
if (!agent || agent.status !== 'online') return { kind: 'idle', text: '待刷新' };
if (agent.xhsLoggedIn) return { kind: 'ok', text: '已登录' };
if (agent.engineRunning && agent.extensionConnected) return { kind: 'ok', text: '扩展就绪' };
if (agent.engineRunning) return { kind: 'warn', text: '未登录' };
return { kind: 'warn', text: '待启动' };
});
const loginGuideText = computed(() => {
const agent = selectedAgent.value;
if (!agent) {
return '只需:连接 Agent → 启动引擎 → 在 Chrome 加载扩展 → 打开小红书并登录。';
}
if (agent.xhsLoggedIn) {
return '校验通过:可在「内容分析」或「爆款创作」页使用工具。';
}
if (agent.engineRunning && agent.extensionConnected) {
return '引擎与扩展已就绪。请在 Chrome 中打开小红书并登录;可点击扩展图标查看实时状态。';
}
return '只需:启动引擎 → 在 Chrome 加载扩展 → 打开小红书并登录。扩展图标可查看 Bridge 与登录状态;下方按钮用于快捷操作。';
});
const desktopAgentHint = computed(() =>
[
'1. 运行 oms0414/xhs-agent-desktop/run.ps1',
'2. 桌面端点击「连接 OMS」(无需填 API / Token)',
'3. 保持本页打开 → 自动授权;或按提示打开授权页',
'4. 启动引擎 → Chrome 加载扩展目录 → 登录小红书',
].join('\n'),
);
function formatTime(value?: string) {
if (!value) return '-';
return value.replace('T', ' ').slice(0, 19);
}
function agentRowClass(record: XhsAgentItem) {
return record.id === selectedAgentId.value ? 'agent-row-selected' : '';
}
function onAgentRowClick(record: XhsAgentItem) {
selectedAgentId.value = record.id;
}
function pickDefaultAgent() {
if (agents.value.length === 0) {
selectedAgentId.value = undefined;
return;
}
const current = agents.value.find((a) => a.id === selectedAgentId.value);
if (current) return;
const online = agents.value.find((a) => a.status === 'online');
selectedAgentId.value = (online ?? agents.value[0]).id;
}
watch(agents, pickDefaultAgent);
async function tryAutoPair() {
const waiting = await isAgentWaitingPair();
if (!waiting) {
pairPushed = false;
return;
}
if (pairPushed) return;
const token = rbacStore.accessToken || localStorage.getItem('rbac_access_token') || '';
if (!token) return;
const ok = await pushCredentialsToAgent(token);
if (ok) {
pairPushed = true;
Message.success('已向桌面 Agent 发送授权');
setTimeout(loadAgents, 1500);
}
}
async function loadAgents() {
loading.value = true;
try {
const res = await listXhsAgents(pagination.current, pagination.pageSize);
agents.value = res.items ?? [];
pagination.total = res.total ?? 0;
pickDefaultAgent();
} catch (e: any) {
Message.error(e?.message || '加载失败');
} finally {
loading.value = false;
}
}
function onPageChange(page: number) {
pagination.current = page;
loadAgents();
}
function onPageSizeChange(size: number) {
pagination.pageSize = size;
pagination.current = 1;
loadAgents();
}
async function control(action: string) {
const agent = selectedAgent.value;
if (!agent) {
Message.warning('请先连接 Agent');
return;
}
if (agent.status !== 'online') {
Message.warning('所有 Agent 离线,请检查本机是否在运行');
return;
}
if (action === 'clear_cookies') {
Modal.confirm({
title: '退出登录',
content: '将在浏览器中退出小红书登录状态,确定继续?',
onOk: () => sendControl(agent.id, action),
});
return;
}
await sendControl(agent.id, action);
}
async function sendControl(agentId: number, action: string) {
try {
const res = await controlXhsAgent(agentId, action);
Message.success(`已创建任务 #${res.taskId}`);
setTimeout(loadAgents, 2000);
} catch (e: any) {
Message.error(e?.message || '操作失败');
}
}
onMounted(() => {
loadAgents();
refreshTimer = setInterval(loadAgents, 15000);
tryAutoPair();
pairTimer = setInterval(tryAutoPair, 2000);
});
onUnmounted(() => {
if (refreshTimer) clearInterval(refreshTimer);
if (pairTimer) clearInterval(pairTimer);
});
</script>
<style scoped lang="less">
@import '../_shared/xhs-page.less';
.xhs-start-service-container {
box-sizing: border-box;
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
height: 100%;
padding: 16px;
background: var(--app-page-bg);
overflow: hidden;
}
.start-service-card {
padding: 16px;
}
.start-service-body {
flex: 1;
min-height: 0;
overflow: auto;
}
.toolbar-left {
flex-direction: column;
align-items: flex-start !important;
gap: 2px !important;
}
.service-cards-row {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.service-card {
flex: 1;
min-width: 0;
min-height: 180px;
display: flex;
flex-direction: column;
padding: 12px 14px;
&.coming-soon {
background: var(--color-fill-1);
border-style: dashed;
.card-title,
.card-hint {
color: var(--color-text-3);
}
}
}
.card-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 8px;
margin-bottom: 6px;
}
.card-title {
color: var(--color-text-1);
font-size: 14px;
font-weight: 600;
}
.card-hint {
margin: 0;
color: var(--color-text-3);
font-size: 12px;
line-height: 1.55;
}
.card-note {
flex: 1;
overflow: hidden;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.card-btn-row {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: auto;
padding-top: 8px;
}
.agent-table-section {
border: 1px solid var(--color-border-2);
border-radius: 8px;
padding: 12px 16px;
}
.section-title {
color: var(--color-text-1);
font-size: 14px;
font-weight: 600;
}
:deep(.agent-row-selected) {
.arco-table-td {
background: color-mix(in srgb, var(--app-primary-color) 8%, var(--app-card-bg)) !important;
}
}
.cmd-block {
margin-top: 12px;
padding: 12px;
background: var(--color-fill-1);
border-radius: 4px;
.cmd-label {
font-size: 12px;
color: var(--color-text-3);
margin-bottom: 8px;
}
pre {
margin: 0;
white-space: pre-wrap;
word-break: break-all;
font-size: 12px;
}
}
@media (max-width: 1200px) {
.service-cards-row {
flex-direction: column;
}
}
</style>