feat: Implement Qwen adapter and enhance AI platform detection

- Added a new Qwen adapter to facilitate monitoring through Playwright, leveraging internal text/chat managers.
- Updated account detection logic to recognize Qwen sessions based on persisted cookies, improving binding reliability.
- Relaxed authentication requirements for monitor tasks, allowing anonymous execution for certain AI platforms.
- Enhanced the generic AI platform detection to include Qwen-specific logic, ensuring accurate session identification.
- Modified the monitoring dashboard to include runtime state indicating if the current user's desktop client is online.
- Updated various files including `account-binder.ts`, `runtime-controller.ts`, and `monitoring_service.go` to support new features and improvements.
This commit is contained in:
2026-04-20 19:10:35 +08:00
parent 69fd182755
commit 7abac1e9c4
13 changed files with 973 additions and 105 deletions
@@ -346,7 +346,10 @@ const enUS = {
brandPlaceholder: "Select brand",
keywordPlaceholder: "Select keyword",
collectNow: "Collect Now",
collectBrandRequired: "Select a brand before collecting.",
collectKeywordRequired: "Select a keyword before collecting the current question set.",
collectClientChecking: "Checking whether the current account's desktop client is online.",
collectClientOnlineRequired: "The current account's desktop client must be online before collecting.",
pluginMode: "Plugin sampling",
pluginHint: "Plugin-mode data depends on device uptime. Low coverage should be treated as directional only.",
awaitingSnapshot: "Waiting for the next valid snapshot",
@@ -346,7 +346,10 @@ const zhCN = {
brandPlaceholder: "选择品牌",
keywordPlaceholder: "选择关键词",
collectNow: "立即采集",
collectBrandRequired: "请先选择品牌,再发起采集",
collectKeywordRequired: "请先选择关键词,再立即采集当前问题",
collectClientChecking: "正在检查当前账号的桌面客户端状态",
collectClientOnlineRequired: "当前登录账号的 desktop-client 未在线,暂时不能立即采集",
pluginMode: "插件采样",
pluginHint: "插件模式下结果受设备在线率影响,低覆盖样本仅供趋势参考。",
awaitingSnapshot: "等待下一次有效快照",
+1
View File
@@ -52,6 +52,7 @@ const errorMessageMap: Record<string, string> = {
publish_cover_required: "已选择百家号,请先上传封面图",
desktop_account_client_missing: "所选账号还没有绑定桌面客户端,暂时无法发布",
desktop_account_not_found: "所选桌面账号不存在,请刷新后重试",
desktop_client_offline: "当前登录账号的桌面客户端未在线,请先打开 desktop-client",
desktop_publish_job_store_unavailable: "发布队列暂时不可用,请稍后重试",
desktop_media_api_removed: "旧版媒体接口已下线,请刷新页面后重试",
publisher_plugin_timeout: "浏览器插件响应超时,请刷新当前页面后重试",
+44 -68
View File
@@ -13,7 +13,7 @@ import type {
MonitoringTimeBucket,
} from "@geo/shared-types";
import { aiPlatformCatalog } from "@geo/shared-types";
import { computed, ref, watch } from "vue";
import { computed, watch } from "vue";
import { useI18n } from "vue-i18n";
import { useRoute, useRouter } from "vue-router";
@@ -21,8 +21,6 @@ import PageHero from "@/components/PageHero.vue";
import { brandsApi, monitoringApi, tenantAccountsApi } from "@/lib/api";
import { formatDateTime } from "@/lib/display";
import { formatError } from "@/lib/errors";
import { kickoffMonitoringCycle } from "@/lib/monitoring-plugin";
import { loadPublisherRuntimeState } from "@/lib/publisher-runtime";
const { t } = useI18n();
const route = useRoute();
@@ -102,7 +100,6 @@ const selectedBrandId = useStorage<number | null>("tracking_selected_brand", nul
const selectedKeywordId = useStorage<number | null>("tracking_selected_keyword", null);
const selectedPlatformId = useStorage<string>("tracking_selected_ai_platform_id", "all");
const selectedBusinessDate = useStorage<string>("tracking_selected_business_date", trackingToday);
const pluginPreparing = ref(false);
selectedPlatformId.value = normalizeTrackingPlatformId(selectedPlatformId.value);
selectedBusinessDate.value = normalizeTrackingBusinessDate(selectedBusinessDate.value) ?? trackingToday;
@@ -228,25 +225,6 @@ const trackingWindow = computed(() => {
};
});
async function ensureMonitoringPluginReady(options?: { silent?: boolean }): Promise<void> {
pluginPreparing.value = true;
try {
const runtime = await loadPublisherRuntimeState();
if (!runtime.ping.installed) {
throw new Error("monitoring_plugin_required");
}
if (!runtime.pluginInstallationId) {
throw new Error("monitoring_plugin_register_required");
}
} catch (error) {
if (!options?.silent) {
throw error;
}
} finally {
pluginPreparing.value = false;
}
}
const dashboardQuery = useQuery({
queryKey: computed(() => [
"tracking",
@@ -274,52 +252,28 @@ const aiAccountsQuery = useQuery({
const collectNowMutation = useMutation({
mutationFn: async () => {
if (!selectedBrandId.value) {
throw new Error("tracking_brand_required");
}
if (!selectedKeywordId.value) {
throw new Error("tracking_keyword_required");
}
await ensureMonitoringPluginReady();
const collectNowResult = await monitoringApi.collectNow(selectedBrandId.value as number, {
return monitoringApi.collectNow(selectedBrandId.value, {
keyword_id: selectedKeywordId.value,
});
try {
const kickoffResult = await kickoffMonitoringCycle();
return {
collectNowResult,
kickoffResult,
kickoffError: null,
};
} catch (error) {
return {
collectNowResult,
kickoffResult: null,
kickoffError: formatError(error),
};
}
},
onSuccess: ({ collectNowResult, kickoffResult, kickoffError }) => {
if (kickoffError) {
message.warning(`${collectNowResult.message},但后台采集启动失败:${kickoffError}`);
void dashboardQuery.refetch();
return;
}
if (kickoffResult?.status === "already_running") {
message.success(`${collectNowResult.message},插件后台已继续执行`);
void dashboardQuery.refetch();
return;
}
onSuccess: (collectNowResult) => {
message.success(collectNowResult.message);
void dashboardQuery.refetch();
},
onError: (error) => {
if (error instanceof Error && error.message === "tracking_keyword_required") {
message.warning(t("tracking.collectKeywordRequired"));
if (error instanceof Error && error.message === "tracking_brand_required") {
message.warning(t("tracking.collectBrandRequired"));
return;
}
if (error instanceof Error && (error.message === "monitoring_plugin_required" || error.message === "monitoring_plugin_register_required")) {
message.warning(formatError(error));
if (error instanceof Error && error.message === "tracking_keyword_required") {
message.warning(t("tracking.collectKeywordRequired"));
return;
}
message.error(formatError(error));
@@ -330,6 +284,26 @@ const selectedBrand = computed<Brand | null>(() => {
return brandsQuery.data.value?.find((item) => item.id === selectedBrandId.value) ?? null;
});
const currentUserClientOnline = computed(() => {
return dashboardQuery.data.value?.runtime.current_user_client_online === true;
});
const collectNowDisabledReason = computed(() => {
if (!selectedBrandId.value) {
return t("tracking.collectBrandRequired");
}
if (!selectedKeywordId.value) {
return t("tracking.collectKeywordRequired");
}
if (dashboardQuery.isLoading.value && !dashboardQuery.data.value) {
return t("tracking.collectClientChecking");
}
if (!currentUserClientOnline.value) {
return t("tracking.collectClientOnlineRequired");
}
return null;
});
const selectedDateHasData = computed(() => {
return (dashboardQuery.data.value?.overview.actual_sample_count ?? 0) > 0;
});
@@ -514,8 +488,6 @@ const citationChartStyle = computed(() => {
};
});
void ensureMonitoringPluginReady({ silent: true });
const heroDescription = computed(() => {
const overview = dashboardQuery.data.value?.overview;
if (!overview?.last_sampled_at) {
@@ -767,15 +739,19 @@ function disableTrackingDate(current: dayjs.Dayjs): boolean {
:disabled-date="disableTrackingDate"
/>
</div>
<a-button
type="primary"
:disabled="!selectedBrandId || !selectedKeywordId"
:loading="collectNowMutation.isPending.value || pluginPreparing"
@click="collectNowMutation.mutate()"
>
<template #icon><ReloadOutlined /></template>
{{ t("tracking.collectNow") }}
</a-button>
<a-tooltip :title="collectNowDisabledReason ?? undefined">
<span>
<a-button
type="primary"
:disabled="Boolean(collectNowDisabledReason)"
:loading="collectNowMutation.isPending.value"
@click="collectNowMutation.mutate()"
>
<template #icon><ReloadOutlined /></template>
{{ t("tracking.collectNow") }}
</a-button>
</span>
</a-tooltip>
</a-space>
</template>
</PageHero>